diff --git a/.codebeatsettings b/.codebeatsettings new file mode 100644 index 00000000..c79ed644 --- /dev/null +++ b/.codebeatsettings @@ -0,0 +1,28 @@ +{ + "GOLANG": { + "TOO_MANY_IVARS": [ + 10, + 12, + 15, + 18 + ], + "LOC": [ + 30, + 50, + 80, + 100 + ], + "TOTAL_LOC": [ + 200, + 400, + 500, + 600 + ], + "TOO_MANY_FUNCTIONS": [ + 30, + 40, + 45, + 46 + ] + } +} \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..5f004fc9 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,305 @@ +# This file contains all available configuration options +# with their default values. + +# options for analysis running +run: + # default concurrency is a available CPU number + concurrency: 4 + + # timeout for analysis, e.g. 30s, 5m, default is 1m + timeout: 1m + + # exit code when at least one issue was found, default is 1 + issues-exit-code: 1 + + # include test files or not, default is true + tests: true + + # list of build tags, all linters use it. Default is empty list. + # build-tags: + # - mytag + + # which dirs to skip: issues from them won't be reported; + # can use regexp here: generated.*, regexp is applied on full path; + # default value is empty list, but default dirs are skipped independently + # from this option's value (see skip-dirs-use-default). + # skip-dirs: + # - src/external_libs + # - autogenerated_by_my_lib + + # default is true. Enables skipping of directories: + # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ + skip-dirs-use-default: true + + # which files to skip: they will be analyzed, but issues from them + # won't be reported. Default value is empty list, but there is + # no need to include all autogenerated files, we confidently recognize + # autogenerated files. If it's not please let us know. + # skip-files: + # - ".*\\.my\\.go$" + # - lib/bad.go + # by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules": + # If invoked with -mod=readonly, the go command is disallowed from the implicit + # automatic updating of go.mod described above. Instead, it fails when any changes + # to go.mod are needed. This setting is most useful to check that go.mod does + # not need updates, such as in a continuous integration and testing system. + # If invoked with -mod=vendor, the go command assumes that the vendor + # directory holds the correct copies of dependencies and ignores + # the dependency descriptions in go.mod. + # modules-download-mode: readonly|release|vendor + +# output configuration options +output: + # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number" + format: colored-line-number + + # print lines of code with issue, default is true + print-issued-lines: true + + # print linter name in the end of issue text, default is true + print-linter-name: true + +# all available settings of specific linters +linters-settings: + errcheck: + # report about not checking of errors in type assetions: `a := b.(MyStruct)`; + # default is false: such cases aren't reported by default. + check-type-assertions: true + + # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; + # default is false: such cases aren't reported by default. + check-blank: false + + # [deprecated] comma-separated list of pairs of the form pkg:regex + # the regex is used to ignore names within pkg. (default "fmt:.*"). + # see https://github.com/kisielk/errcheck#the-deprecated-method for details + # ignore: fmt:.*,io/ioutil:^Read.* + # path to a file containing a list of functions to exclude from checking + # see https://github.com/kisielk/errcheck#excluding-functions for details + # exclude: /path/to/file.txt + + funlen: + lines: 60 + statements: 40 + + govet: + # report about shadowed variables + check-shadowing: true + + # settings per analyzer + settings: + printf: # analyzer name, run `go tool vet help` to see all analyzers + funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf + + # enable or disable analyzers by name + enable: + - atomicalign + enable-all: false + disable: + # - shadow + disable-all: false + golint: + # minimal confidence for issues, default is 0.8 + min-confidence: 0.8 + gofmt: + # simplify code: gofmt with `-s` option, true by default + simplify: true + goimports: + # put imports beginning with prefix after 3rd-party packages; + # it's a comma-separated list of prefixes + local-prefixes: github.com/org/project + gocyclo: + # minimal code complexity to report, 30 by default (but we recommend 10-20) + min-complexity: 10 + gocognit: + # minimal code complexity to report, 30 by default (but we recommend 10-20) + min-complexity: 20 + maligned: + # print struct with more effective memory layout or not, false by default + suggest-new: true + dupl: + # tokens count to trigger issue, 150 by default + threshold: 100 + goconst: + # minimal length of string constant, 3 by default + min-len: 3 + # minimal occurrences count to trigger, 3 by default + min-occurrences: 3 + depguard: + list-type: blacklist + include-go-root: false + packages: + - github.com/sirupsen/logrus + packages-with-error-messages: + # specify an error message to output when a blacklisted package is used + github.com/sirupsen/logrus: "logging is allowed only by logutils.Log" + misspell: + # Correct spellings using locale preferences for US or UK. + # Default is to use a neutral variety of English. + # Setting locale to US will correct the British spelling of 'colour' to 'color'. + locale: US + ignore-words: + - someword + lll: + # max line length, lines longer will be reported. Default is 120. + # '\t' is counted as 1 character by default, and can be changed with the tab-width option + line-length: 120 + # tab width in spaces. Default to 1. + tab-width: 1 + unused: + # treat code as a program (not a library) and report unused exported identifiers; default is false. + # XXX: if you enable this setting, unused will report a lot of false-positives in text editors: + # if it's called for subdir of a project it can't find funcs usages. All text editor integrations + # with golangci-lint call it on a directory with the changed file. + check-exported: false + unparam: + # Inspect exported functions, default is false. Set to true if no external program/library imports your code. + # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: + # if it's called for subdir of a project it can't find external interfaces. All text editor integrations + # with golangci-lint call it on a directory with the changed file. + check-exported: false + nakedret: + # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 + max-func-lines: 30 + prealloc: + # XXX: we don't recommend using this linter before doing performance profiling. + # For most programs usage of prealloc will be a premature optimization. + + # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. + # True by default. + simple: true + range-loops: true # Report preallocation suggestions on range loops, true by default + for-loops: false # Report preallocation suggestions on for loops, false by default + gocritic: + # Which checks should be enabled; can't be combined with 'disabled-checks'; + # See https://go-critic.github.io/overview#checks-overview + # To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run` + # By default list of stable checks is used. + # enabled-checks: + # - rangeValCopy + + # Which checks should be disabled; can't be combined with 'enabled-checks'; default is empty + disabled-checks: + - regexpMust + + # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks. + # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". + enabled-tags: + - performance + + settings: # settings passed to gocritic + captLocal: # must be valid enabled check name + paramsOnly: true + rangeValCopy: + sizeThreshold: 32 + godox: + # report any comments starting with keywords, this is useful for TODO or FIXME comments that + # might be left in the code accidentally and should be resolved before merging + keywords: # default keywords are TODO, BUG, and FIXME, these can be overwritten by this setting + - NOTE + - OPTIMIZE # marks code that should be optimized before merging + - HACK # marks hack-arounds that should be removed before merging + dogsled: + # checks assignments with too many blank identifiers; default is 2 + max-blank-identifiers: 2 + + whitespace: + multi-if: false # Enforces newlines (or comments) after every multi-line if statement + multi-func: false # Enforces newlines (or comments) after every multi-line function signature + wsl: + # If true append is only allowed to be cuddled if appending value is + # matching variables, fields or types on line above. Default is true. + strict-append: true + # Allow calls and assignments to be cuddled as long as the lines have any + # matching variables, fields or types. Default is true. + allow-assign-and-call: true + # Allow multiline assignments to be cuddled. Default is true. + allow-multiline-assign: true + # Allow declarations (var) to be cuddled. + allow-cuddle-declarations: false + # Allow trailing comments in ending of blocks + allow-trailing-comment: false + # Force newlines in end of case at this limit (0 = never). + force-case-trailing-whitespace: 0 + +linters: + enable: + - megacheck + - govet + - funlen + - gocyclo + disable: + - maligned + - prealloc + - gosec + disable-all: false + presets: + - bugs + - unused + fast: false + +issues: + # List of regexps of issue texts to exclude, empty list by default. + # But independently from this option we use default exclude patterns, + # it can be disabled by `exclude-use-default: false`. To list all + # excluded by default patterns execute `golangci-lint run --help` + exclude: + - abcdef + + # Excluding configuration per-path, per-linter, per-text and per-source + exclude-rules: + # Exclude some linters from running on tests files. + - path: _test\.go + linters: + - gocyclo + - errcheck + - dupl + - gosec + - funlen + - goconst + - gocognit + + # Exclude known linters from partially hard-vendored code, + # which is impossible to exclude via "nolint" comments. + - path: internal/hmac/ + text: "weak cryptographic primitive" + linters: + - gosec + + # Exclude some staticcheck messages + - linters: + - staticcheck + text: "SA9003:" + + # Exclude lll issues for long lines with go:generate + - linters: + - lll + source: "^//go:generate " + + # Independently from option `exclude` we use default exclude patterns, + # it can be disabled by this option. To list all + # excluded by default patterns execute `golangci-lint run --help`. + # Default value for this option is true. + exclude-use-default: false + + # Maximum issues count per one linter. Set to 0 to disable. Default is 50. + max-issues-per-linter: 0 + + # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. + max-same-issues: 0 + + # Show only new issues: if there are unstaged changes or untracked files, + # only those changes are analyzed, else only changes in HEAD~ are analyzed. + # It's a super-useful option for integration of golangci-lint into existing + # large codebase. It's not practical to fix all existing issues at the moment + # of integration: much better don't allow issues in new code. + # Default is false. + new: false + # Show only new issues created after git revision `REV` + # new-from-rev: REV + # Show only new issues created in git patch with set file path. + # new-from-patch: path/to/patch/file diff --git a/README.md b/README.md index 15342e54..791aae27 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,6 @@ K9s is available on Linux, OSX and Windows platforms. 1. Deployments - --- ## Demo Video @@ -112,7 +111,7 @@ K9s uses aliases to navigate most K8s resources. | `Ctrl-a` | Show all available resource alias | select+`` to view | | `/`filter`ENTER` | Filter out a resource view given a filter | `/bumblebeetuna` | | `/`-l label-selector`ENTER` | Filter resource view by labels | `/-l app=fred` | -| `` | Bails out of command/filter mode | | +| `` | Bails out of view/command/filter mode | | | `d`,`v`, `e`, `l`,... | Key mapping to describe, view, edit, view logs,... | `d` (describes a resource) | | `:`ctx`` | To view and switch to another Kubernetes context | `:`+`ctx`+`` | | `:`ns`` | To view and switch to another Kubernetes namespace | `:`+`ns`+`` | @@ -124,11 +123,12 @@ K9s uses aliases to navigate most K8s resources. ## K9s config file ($HOME/.k9s/config.yml) - K9s keeps its configurations in a dot file in your home directory. + K9s keeps its configurations in a .k9s directory in your home directory. > NOTE: This is still in flux and will change while in pre-release stage! ```yaml + # config.yml k9s: # Indicates api-server poll intervals. refreshRate: 2 @@ -162,9 +162,10 @@ K9s uses aliases to navigate most K8s resources. ``` --- + ## Aliases -In K9s you can define your own command aliases (shortnames) to access your resources. In your `$HOME/.k9s` define a file called `alias.yml`. A K9s alias defines pairs of alias:gvr. A gvr represents a fully qualified Kubernetes resource identifier. Here is an example of an alias file: +In K9s, you can define your own command aliases (shortnames) to access your resources. In your `$HOME/.k9s` define a file called `alias.yml`. A K9s alias defines pairs of alias:gvr. A gvr represents a fully qualified Kubernetes resource identifier. Here is an example of an alias file: ```yaml # $HOME/.k9s/alias.yml @@ -173,9 +174,10 @@ alias: crb: rbac.authorization.k8s.io/v1/clusterrolebindings ``` -Using this alias file, you can now type pp/crb to list pods, clusterrolebindings respectively. +Using this alias file, you can now type pp/crb to list pods or clusterrolebindings respectively. --- + ## Plugins K9s allows you to define your own cluster commands via plugins. K9s will look at `$HOME/.k9s/plugin.yml` to locate available plugins. A plugin is defined as follows: @@ -183,9 +185,10 @@ K9s allows you to define your own cluster commands via plugins. K9s will look at ```yaml # $HOME/.k9s/plugin.yml plugin: + # Defines a plugin to provide a `Ctrl-L` shorcut to tail the logs while in pod view. fred: shortCut: Ctrl-L - description: "Pod logs" + description: Pod logs scopes: - po command: /usr/local/bin/kubectl @@ -202,7 +205,7 @@ plugin: This defines a plugin for viewing logs on a selected pod using `CtrlL` mnemonic. -The shortcut option represents the command a user would type to activate the plugin. The command represents adhoc commands the plugin runs upon activation. The scopes defines a collection of views shortnames for which the plugin shortcut will be made available to the user. +The shortcut option represents the command a user would type to activate the plugin. The command represents adhoc commands the plugin runs upon activation. The scopes defines a collection of resources names/shortnames for which the plugin shortcut will be made available to the user. You can specify all to provide this shortcut for all views. K9s does provide additional environment variables for you to customize your plugins. Currently, the available environment variables are as follows: @@ -283,6 +286,41 @@ benchmarks: --- +## HotKeys + +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: + +1. In your .k9s home directory create a file named `hotkey.yml` +2. Add the following to your `hotkey.yml`. You can use short names or resource name to specify a command ie same as typing it in command mode. + + ```yaml + hotKey: + shift-0: + shortCut: Shift-0 + description: View pods + command: pods + shift-1: + shortCut: Shift-1 + description: View deployments + command: dp + shift-2: + shortCut: Shift-2 + description: View services + command: service + shift-3: + shortCut: Shift-3 + description: View statefulsets + command: sts + ``` + + Not feeling so hot? Your custom hotkeys list 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. + + You can choose any keyboard shotcuts that make sense to you, provided they are not part of the standard K9s shortcuts list. + +NOTE: This feature/configuration might change in future releases! + +--- + ## K9s RBAC FU On RBAC enabled clusters, you would need to give your users/groups capabilities so that they can use K9s to explore their Kubernetes cluster. K9s needs minimally read privileges at both the cluster and namespace level to display resources and metrics. @@ -290,7 +328,6 @@ On RBAC enabled clusters, you would need to give your users/groups capabilities These rules below are just suggestions. You will need to customize them based on your environment policies. If you need to edit/delete resources extra Fu will be necessary. > NOTE! Cluster/Namespace access may change in the future as K9s evolves. - > NOTE! We expect K9s to keep running even in atrophied clusters/namespaces. Please file issues if this is not the case! ### Cluster RBAC scope @@ -315,7 +352,7 @@ rules: - apiGroups: ["apiextensions.k8s.io"] resources: ["customresourcedefinitions"] verbs: ["get", "list", "watch"] - # Grants RO access to metric server + # Grants RO access to metric server (if present) - apiGroups: ["metrics.k8s.io"] resources: ["nodes", "pods"] verbs: ["get", "list", "watch"] @@ -355,7 +392,7 @@ rules: verbs: ["get", "list", "watch"] # Grants RO access to metric server - apiGroups: ["metrics.k8s.io"] - resources: ["pods"] + resources: ["pods", "nodes"] verbs: - get - list @@ -382,7 +419,7 @@ roleRef: ## Skins -You can style K9s based on your own sense of style and look. This is very much an experimental feature at this time, more will be added/modified if this feature has legs so thread accordingly! +You can style K9s based on your own sense of look and style. This is very much an experimental feature at this time, more will be added/modified if this feature has legs so thread accordingly! By default a K9s view displays resource information using the following coloring scheme: @@ -392,7 +429,8 @@ By default a K9s view displays resource information using the following coloring Skins are YAML files, that enable a user to change K9s presentation layer. K9s skins are loaded from `$HOME/.k9s/skin.yml`. If a skin file is detected then the skin would be loaded if not the current stock skin remains in effect. -Below is a sample skin file, more skins would be available in the skins directory, just simply copy any of these in your user's home dir as `skin.yml`. +You can also change K9s skins based on the cluster you are connecting too. In this case, you can specify the skin file name as `$HOME/.k9s/mycluster_skin.yml` +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 user's home dir as `skin.yml`. ```yaml # InTheNavy Skin... @@ -424,7 +462,8 @@ k9s: activeColor: skyblue # Resource status and update styles status: - newColor: blue + # You can also use hex colors! + newColor: #0000ff modifyColor: powderblue addColor: lightskyblue errorColor: indianred @@ -501,7 +540,7 @@ Available color names are defined below: This initial drop is brittle. K9s will most likely blow up... -1. You're running older versions of Kubernetes. K9s works best Kubernetes 1.12+. +1. You're running older versions of Kubernetes. K9s works best Kubernetes 1.15+. 2. You don't have enough RBAC fu to manage your cluster. --- @@ -516,7 +555,7 @@ dig this effort, please let us know that too! ## ATTA Girls/Boys! -K9s sits on top of many of opensource projects and libraries. Our *sincere* +K9s sits on top of many open source projects and libraries. Our *sincere* appreciations to all the OSS contributors that work nights and weekends to make this project a reality! diff --git a/change_logs/release_0.10.0.md b/change_logs/release_0.10.0.md new file mode 100644 index 00000000..c6b1cfca --- /dev/null +++ b/change_logs/release_0.10.0.md @@ -0,0 +1,88 @@ + + +# Release v0.10.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 is as ever very much noticed and appreciated! + +Also if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer) + +--- + +## Change Logs + +First off, Happy 2020 to you and yours!! Best wishes for good health and good fortune! + +This release represents a major overall of K9s core. It's been a long time coming and indeed a long day in the saddle ;( There has been many code changes and hopefully improvements from previous releases. I think some of it is better but I've probably borked a bunch of functionality in the process. I look to you to help me flesh out issues and bugs, so we can move on to bigger and exciting features in 2020! Please do thread lightly on this one and make sure to keep a previous release handy just in case... This was a boatload of work to make this happen, my hope is you'll enjoy some of the improvements... In any case, and as always, if you feel they're better ways or imperfections by all means please pipe in! + +I would also like to take this opportunity to thank all of you for your kind PRs and issues and for your support and patience with K9s. I understand this release might be a bit torked, but I will work hard to make sure we reach stability quickly in the next few drops. Thank you for your understanding!! + +## VatDoesDisDo? + +Most of the refactors are around K8s resource fetching and viewing as well as navigation changes. Based on our observations this release might load resources a bit slower than usual but should make navigation much faster once the cache is primed. We've made some improvements to be more consistent with navigation and shortcuts management. We've got ride off the breadcrumbs navigation ie no more `p` to nav back. Crumbs are now just tracking a natural resoure navigation ie pod -> containers -> logs and no longer commands history. Each new command will now load a brand new breadcrumb. You can press `` to nav back to the previous page. We're also introducing a new hotkeys feature, that afforts creating shortcuts to navigate to your favorite resources ie shift-0 -> view pods, shift-1 -> view deployments (See HotKey section below). I know there were many outstanding PRS (Thank you to all that I've submitted!) and given the extent of the changes, I've resolved to incorporate them in manually vs having to deal with merge conflicts. + +## Custom Skins Per Cluster + +In this release, we've added support for skins at the cluster level. Do you want K9s to look differently based on which cluster you're connecting to? All you'll need is to name the skin file in the K9s home directory as follows `mycluster_skin.yml`. If no cluster specific skin file is found, the standard `skin.yml` file will be loaded if present. Please checkout the `skins` directory in this repo or PR me if you have cool skins you'd like to share with your fellow K9ers as they will be featured in these release notes and the project README. + +## Hot(Ness)? + +Feeling like you want to be able to quickly switch around your favorite resources with your very own shortcut? Wouldn't it be dandy to navigate to your deployments using shift-0 vs entering a command `:dp`? Here is what you'll need to do to add HotKeys to your K9s sessions: + +1. In your .k9s home directory create a file named `hotkey.yml` +2. For example add the following to your `hotkey.yml`. You can use short names or resource name to specify a command ie same as typing it in command mode. + + ```yaml + hotKey: + shift-0: + shortCut: Shift-0 + description: View pods + command: pods + shift-1: + shortCut: Shift-1 + description: View deployments + command: dp + shift-2: + shortCut: Shift-2 + description: View services + command: service + shift-3: + shortCut: Shift-3 + description: View statefulsets + command: statefulsets + ``` + + Not feeling so hot? Your custom hotkeys list will be listed in the help view.``. + + You can choose any keyboard shotcuts that make sense to you, provided they are not part of the standard K9s shortcuts list. + +## PullRequests + +* [PR #447](https://github.com/derailed/k9s/pull/447) K9s MacPorts support. Thank you! [Nils Breunese](https://github.com/breun) +* [PR #446](https://github.com/derailed/k9s/pull/446) Same key invert sort. Big thanks!! [James Hiew](https://github.com/jameshiew) +* [PR #445](https://github.com/derailed/k9s/pull/445) Use `?` to toggle help. Major thanks!! [Ramz](https://github.com/ageekymonk) +* [PR #443](https://github.com/derailed/k9s/pull/443) Hex color skin support. Great work! [Gavin Ray](https://github.com/gavinray97) +* [PR #442](https://github.com/derailed/k9s/pull/442) Full screen/Wrap support on log view. ATTA BOY! [Shiv3](https://github.com/shiv3) +* [PR #412](https://github.com/derailed/k9s/pull/412) Simplify cruder interface. ATTA BOY!! (as always)[Gustavo Silva Paiva](https://github.com/paivagustavo) +* [PR #350](https://github.com/derailed/k9s/pull/350) Sanitize file name before saving. All credits to [Tuomo Syvänperä](https://github.com/syvanpera) + +--- + +## Resolved Bugs/Features + +* [Issue #437](https://github.com/derailed/k9s/issues/437) Error when viewing cluster role on a role binding. +* [Issue #434](https://github.com/derailed/k9s/issues/434) Same key `?` toggle help. +* [Issue #432](https://github.com/derailed/k9s/issues/432) Add address field to port forwards. +* [Issue #431](https://github.com/derailed/k9s/issues/431) Add LimitRange resource support. +* [Issue #430](https://github.com/derailed/k9s/issues/430) Add HotKey support. +* [Issue #426](https://github.com/derailed/k9s/issues/426) Address slow scroll while in table view. +* [Issue #417](https://github.com/derailed/k9s/issues/417) Ensure code lints correctly. Thank you Gustavo!! +* [Issue #415](https://github.com/derailed/k9s/issues/415) Add provisions to support longer clusterinfo/namespace header. +* [Issue #408](https://github.com/derailed/k9s/issues/408) Same key toggle inverse sort. +* [Issue #402](https://github.com/derailed/k9s/issues/402) Add `all` support to plugin scope. +* [Issue #401](https://github.com/derailed/k9s/issues/401) Add support for custom plugins on all views. + +--- + + © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/change_logs/release_0.4.2.md b/change_logs/release_0.4.2.md index 7f3c98b1..63458a65 100644 --- a/change_logs/release_0.4.2.md +++ b/change_logs/release_0.4.2.md @@ -26,7 +26,7 @@ Also if you dig this tool, please make some noise on social! [@kitesurfer](https ### o YAML Highlighter - Describe and YAML commands will now yield syntax highlighted views. + Describe and YAML commands will now yield syntax highlighted view. [Feature #142](https://github.com/derailed/k9s/issues/142) --- diff --git a/change_logs/release_0.6.7.md b/change_logs/release_0.6.7.md index 8ea5fd7b..20e53064 100644 --- a/change_logs/release_0.6.7.md +++ b/change_logs/release_0.6.7.md @@ -20,8 +20,7 @@ This is a maintenance release to mainly resolve outstanding issues and bugs. ### Tracking Percentages -Added two new columns to track cpu/memory utilization on metrics-server enabled clusters. These apply to pod,container and node views. - +Added two new columns to track cpu/memory utilization on metrics-server enabled clusters. These apply to pod,container and node view. --- diff --git a/change_logs/release_0.7.0.md b/change_logs/release_0.7.0.md index e431ccfb..ca15b270 100644 --- a/change_logs/release_0.7.0.md +++ b/change_logs/release_0.7.0.md @@ -36,7 +36,7 @@ This feature is still work in progress. It does require a new config file to hel This is one feature that I think is, pardon my french.., totally `Bitch'n`! K9s now bundles [Hey](https://github.com/rakyll/hey) CLI tool from the extremely talented Jaana Dogan of Google fame. Hey allows you to benchmark HTTP service endpoints similar to apache bench. -Benchmarking is enabled via keyboard shortcuts `Ctrl-B` and `Alt-B` to activate/cancel a benchmark while in PortForward and Service views. Benchmarking a service assumes the HTTP service is exposed either as NodePort or LoadBalancer. To view your benchmarks, navigate to the new Benchmark view aka `:be` to list your benchmarks and runs statistics. +Benchmarking is enabled via keyboard shortcuts `Ctrl-B` and `Alt-B` to activate/cancel a benchmark while in PortForward and Service view. Benchmarking a service assumes the HTTP service is exposed either as NodePort or LoadBalancer. To view your benchmarks, navigate to the new Benchmark view aka `:be` to list your benchmarks and runs statistics. So you now have the ability to stretch out your cluster legs by benchmarking your pods and services and gather all kind of interesting statistics directly in K9s. Generating load on your resources will help you tune your cluster resources, exercise your auto scalers, compare Canary builds perf, etc... diff --git a/change_logs/release_0.8.0.md b/change_logs/release_0.8.0.md index 8cdbdb09..a4f036b4 100644 --- a/change_logs/release_0.8.0.md +++ b/change_logs/release_0.8.0.md @@ -43,7 +43,7 @@ dialogs. This was totally a reasonable thing to do! However in case of managed p This one is cool! I think this thought came about from (Markus)[https://github.com/Makusi75]. Thank you Markus!! This feature allows K9s users to now customize K9s with their own plugin commands. You will be able to add a new menu shortcut to the K9s menu and fire off a custom command on a selected resource. Some of you might be leveraging kubectl plugins and now you will be able to fire these off directly from K9s along with many other shell commands. -In order to specify a custom plugin command, you will need to modify your .k9s/config.yml file. For example here is a sample extension to list out all the pods in the `fred` namespace while in a pod or deployment views. When this plugin is available a new command `` will show only while in pod and deploy views. +In order to specify a custom plugin command, you will need to modify your .k9s/config.yml file. For example here is a sample extension to list out all the pods in the `fred` namespace while in a pod or deployment view. When this plugin is available a new command `` will show only while in pod and deploy view. ```yaml plugins: diff --git a/change_logs/release_0.8.3.md b/change_logs/release_0.8.3.md index 3051dcaa..a77af23f 100644 --- a/change_logs/release_0.8.3.md +++ b/change_logs/release_0.8.3.md @@ -14,7 +14,7 @@ Also if you dig this tool, please make some noise on social! [@kitesurfer](https ### NetworkPolicy -NetworkPolicy resource is now available under the command `np` while in command mode. It behaves like other K9s resource views. You will get a bit more information in K9s vs `kubectl` as it includes information about ingress and egress rules. +NetworkPolicy resource is now available under the command `np` while in command mode. It behaves like other K9s resource view. You will get a bit more information in K9s vs `kubectl` as it includes information about ingress and egress rules. ### Arrrggg! New CLI Argument diff --git a/cmd/root.go b/cmd/root.go index 7c516181..bfda7541 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,11 +5,11 @@ import ( "fmt" "runtime/debug" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/color" "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/views" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/view" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -34,20 +34,29 @@ var ( Long: longAppDesc, Run: run, } - _ config.KubeSettings = &k8s.Config{} + _ config.KubeSettings = &client.Config{} ) func init() { + const falseFlag = "false" rootCmd.AddCommand(versionCmd(), infoCmd()) initK9sFlags() initK8sFlags() // Klogs (of course) want to print stuff to the screen ;( klog.InitFlags(nil) - flag.Set("log_file", config.K9sLogs) - flag.Set("stderrthreshold", "fatal") - flag.Set("alsologtostderr", "false") - flag.Set("logtostderr", "false") + if err := flag.Set("log_file", config.K9sLogs); err != nil { + log.Error().Err(err) + } + if err := flag.Set("stderrthreshold", "fatal"); err != nil { + log.Error().Err(err) + } + if err := flag.Set("alsologtostderr", falseFlag); err != nil { + log.Error().Err(err) + } + if err := flag.Set("logtostderr", falseFlag); err != nil { + log.Error().Err(err) + } } // Execute root command @@ -59,22 +68,24 @@ func Execute() { func run(cmd *cobra.Command, args []string) { defer func() { - // views.ClearScreen() + // view.ClearScreen() if err := recover(); err != nil { log.Error().Msgf("Boom! %v", err) log.Error().Msg(string(debug.Stack())) printLogo(color.Red) - fmt.Printf(color.Colorize("Boom!! ", color.Red)) + fmt.Printf("%s", color.Colorize("Boom!! ", color.Red)) fmt.Println(color.Colorize(fmt.Sprintf("%v.", err), color.White)) } }() zerolog.SetGlobalLevel(parseLevel(*k9sFlags.LogLevel)) cfg := loadConfiguration() - app := views.NewApp(cfg) + app := view.NewApp(cfg) { defer app.BailOut() - app.Init(version, *k9sFlags.RefreshRate) + if err := app.Init(version, *k9sFlags.RefreshRate); err != nil { + panic(err) + } app.Run() } } @@ -83,8 +94,9 @@ func loadConfiguration() *config.Config { log.Info().Msg("🐶 K9s starting up...") // Load K9s config file... - k8sCfg := k8s.NewConfig(k8sFlags) + k8sCfg := client.NewConfig(k8sFlags) k9sCfg := config.NewConfig(k8sCfg) + if err := k9sCfg.Load(config.K9sConfigFile); err != nil { log.Warn().Msg("Unable to locate K9s config. Generating new configuration...") } @@ -101,25 +113,31 @@ func loadConfiguration() *config.Config { k9sCfg.K9s.OverrideCommand(*k9sFlags.Command) } - if k9sFlags.AllNamespaces != nil && *k9sFlags.AllNamespaces { - k9sCfg.SetActiveNamespace(resource.AllNamespaces) + if isBoolSet(k9sFlags.AllNamespaces) && k9sCfg.SetActiveNamespace(render.AllNamespaces) != nil { + log.Error().Msg("Setting active namespace") } if err := k9sCfg.Refine(k8sFlags); err != nil { log.Panic().Err(err).Msg("Unable to locate kubeconfig file") } - k9sCfg.SetConnection(k8s.InitConnectionOrDie(k8sCfg, log.Logger)) + k9sCfg.SetConnection(client.InitConnectionOrDie(k8sCfg)) // Try to access server version if that fail. Connectivity issue? if _, err := k9sCfg.GetConnection().ServerVersion(); err != nil { log.Panic().Err(err).Msg("K9s can't connect to cluster") } log.Info().Msg("✅ Kubernetes connectivity") - k9sCfg.Save() + if err := k9sCfg.Save(); err != nil { + log.Error().Err(err).Msg("Config save") + } return k9sCfg } +func isBoolSet(b *bool) bool { + return b != nil && *b +} + func parseLevel(level string) zerolog.Level { switch level { case "debug": diff --git a/go.mod b/go.mod index e9363461..54ea0506 100644 --- a/go.mod +++ b/go.mod @@ -28,14 +28,15 @@ replace ( require ( github.com/atotto/clipboard v0.1.2 - github.com/derailed/tview v0.3.2 + github.com/derailed/tview v0.3.3 github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f // indirect github.com/fsnotify/fsnotify v1.4.7 github.com/gdamore/tcell v1.3.0 - github.com/ghodss/yaml v1.0.0 // indirect + github.com/ghodss/yaml v1.0.0 github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef // indirect + github.com/golang/mock v1.2.0 github.com/google/btree v1.0.0 // indirect github.com/googleapis/gnostic v0.2.0 // indirect github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect @@ -44,22 +45,23 @@ require ( github.com/mattn/go-runewidth v0.0.5 github.com/petergtz/pegomock v2.6.0+incompatible github.com/rakyll/hey v0.1.2 - github.com/rs/zerolog v1.14.3 + github.com/rs/zerolog v1.17.2 github.com/sahilm/fuzzy v0.1.0 github.com/spf13/cobra v0.0.5 github.com/stretchr/testify v1.3.0 - github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 // indirect golang.org/x/text v0.3.2 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v2 v2.2.2 + gopkg.in/yaml.v2 v2.2.4 gotest.tools v2.2.0+incompatible k8s.io/api v0.0.0 + k8s.io/apiextensions-apiserver v0.0.0 k8s.io/apimachinery v0.0.0 k8s.io/cli-runtime v0.0.0 k8s.io/client-go v0.0.0 k8s.io/klog v0.4.0 k8s.io/kubectl v0.0.0 + k8s.io/kubernetes v1.16.3 k8s.io/metrics v0.0.0 sigs.k8s.io/yaml v1.1.0 vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 diff --git a/go.sum b/go.sum index 71bb79d7..6f5b8c15 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,9 @@ +bitbucket.org/bertimus9/systemstat v0.0.0-20180207000608-0eeff89b0690/go.mod h1:Ulb78X89vxKYgdL24HMTiXYHlyHEvruOj1ZPlqeNEZM= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +github.com/Azure/azure-sdk-for-go v32.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-autorest/autorest v0.9.0 h1:MRvx8gncNaXJqOoLmhNjUAKh33JJF8LyxPhomEtOsjs= github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= @@ -11,33 +13,81 @@ github.com/Azure/go-autorest/autorest/date v0.1.0 h1:YGrhWfrgtFs84+h0o46rJrlmsZt github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc= +github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8= github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY= github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k= github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/GoogleCloudPlatform/k8s-cloud-provider v0.0.0-20190822182118-27a4ced34534/go.mod h1:iroGtC8B3tQiqtds1l+mgk/BBOrxbqjH+eUfFQYRc14= +github.com/JeffAshton/win_pdh v0.0.0-20161109143554-76bb4ee9f0ab/go.mod h1:3VYc5hodBMJ5+l/7J4xAyMeuM2PNuepvHlGs8yilUCA= github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= +github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= +github.com/Microsoft/hcsshim v0.0.0-20190417211021-672e52e9209d/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46 h1:lsxEuwrXEAokXB9qhlbKWPpo3KMLZQ5WB5WLQRW1uq0= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/Rican7/retry v0.1.0/go.mod h1:FgOROf8P5bebcC1DS0PdOQiqGUridaZvikzUmkFW6gg= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY= github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/auth0/go-jwt-middleware v0.0.0-20170425171159-5493cabe49f7/go.mod h1:LWMyo4iOLWXHGdBki7NIht1kHru/0wM179h+d3g8ATM= +github.com/aws/aws-sdk-go v1.16.26/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/bazelbuild/bazel-gazelle v0.0.0-20181012220611-c728ce9f663e/go.mod h1:uHBSeeATKpVazAACZBDPL/Nk/UhQDDsJWDlqYJo8/Us= +github.com/bazelbuild/buildtools v0.0.0-20180226164855-80c7f0d45d7e/go.mod h1:5JP0TXzWDHXv8qvxRC4InIazwdyDseBDbzESUMKk1yU= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/bifurcation/mint v0.0.0-20180715133206-93c51c6ce115/go.mod h1:zVt7zX3K/aDCk9Tj+VM7YymsX66ERvzCJzw8rFCX2JU= +github.com/blang/semver v3.5.0+incompatible h1:CGxCgetQ64DKk7rdZ++Vfnb1+ogGNnB17OJKJXD2Cfs= github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/caddyserver/caddy v1.0.3/go.mod h1:G+ouvOY32gENkJC+jhgl62TyhvqEsFaDiZ4uw0RzP1E= +github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cespare/prettybench v0.0.0-20150116022406-03b8cfe5406c/go.mod h1:Xe6ZsFhtM8HrDku0pxJ3/Lr51rwykrzgFwpmTzleatY= github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= +github.com/checkpoint-restore/go-criu v0.0.0-20190109184317-bdb7599cd87b/go.mod h1:TrMrLQfeENAPYPRsJuq3jsqdlRh3lvi6trTZJG8+tho= +github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/cfssl v0.0.0-20180726162950-56268a613adf/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= +github.com/clusterhq/flocker-go v0.0.0-20160920122132-2b8b7259d313/go.mod h1:P1wt9Z3DP8O6W3rvwCt0REIlshg1InHImaLW0t3ObY0= +github.com/codegangsta/negroni v1.0.0/go.mod h1:v0y3T5G7Y1UlFfyxFn/QLRU4a2EuNau2iZY63YTKWo0= +github.com/container-storage-interface/spec v1.1.0/go.mod h1:6URME8mwIBbpVyZV93Ce5St17xBiQJQY67NDsuohiy4= +github.com/containerd/console v0.0.0-20170925154832-84eeaae905fa/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/containerd v1.0.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/typeurl v0.0.0-20190228175220-2a93cfde8c20/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/coredns/corefile-migration v1.0.2/go.mod h1:OFwBp/Wc9dJt5cAZzHWMNhK1r5L0p0jDwIBc6j8NC8E= +github.com/coreos/bbolt v1.3.1-coreos.6/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.15+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.17+incompatible h1:f/Z3EoDSx1yjaIjLQGo1diYUlQYSBrrAQ5vP8NjwXwo= +github.com/coreos/etcd v3.3.17+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea h1:n2Ltr3SrfQlf/9nOna1DoGKxLx3qTSI8Ttl6Xrqp6mw= +github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/rkt v1.30.0/go.mod h1:O634mlH6U7qk87poQifK6M2rsFNt+FyUTWNMnP1hF1U= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -45,24 +95,34 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE= github.com/derailed/tview v0.3.2 h1:By43yu6kbGvA+iL09VAhTKxKEd02BBOtUPIlrkeHxT4= github.com/derailed/tview v0.3.2/go.mod h1:yApPszFU62FoaGkf7swy2nIdV/h7Nid3dhMSVy6+OFI= +github.com/derailed/tview v0.3.3 h1:tipPwxcDhx0zRBZuc8VKIrNgWL40FL5JeF/30XVieUE= +github.com/derailed/tview v0.3.3/go.mod h1:yApPszFU62FoaGkf7swy2nIdV/h7Nid3dhMSVy6+OFI= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libnetwork v0.0.0-20180830151422-a9cd636e3789/go.mod h1:93m0aTqz6z+g32wla4l4WxTrdtvBRmVzYRkYvasA5Z8= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c h1:ZfSZ3P3BedhKGUhzj7BQlPSU4OvT6tfOKe3DVHzOA7s= github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/euank/go-kmsg-parser v2.0.0+incompatible/go.mod h1:MhmAMZ8V4CYH4ybgdRwPr2TU5ThnS43puaKEMpja1uw= github.com/evanphx/json-patch v4.2.0+incompatible h1:fUDGZCv/7iAN7u0puUVhvKCcsR6vRfwrJatElLBEf0I= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= +github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= @@ -74,25 +134,66 @@ github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2H github.com/ghodss/yaml v0.0.0-20180820084758-c7ce16629ff4/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7aW59XpK7Qymp8iy83xq74fLr21is= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-acme/lego v2.5.0+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M= +github.com/go-bindata/go-bindata v3.1.1+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= +github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.19.2 h1:ophLETFestFZHk3ji7niPEL4d466QjW+0Tdg5VyDq7E= +github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.19.2 h1:a2kIyV3w+OS3S97zxUndRVD46+FhGOUBDFY7nmu4CsY= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.19.2 h1:A9+F4Dc/MCNB5jibxf6rRvOvR/iFgQdyNx9eIhnGqq0= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.19.2 h1:o20suLFB4Ri0tuzpWtyHlh7E7HnkqTNLq6aR6WVNS1w= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.2 h1:rf5ArTHmIJxyV5Oiks+Su0mUens1+AjpkPoWr5xFRcI= +github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= +github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.19.0 h1:sU6pp4dSV2sGlNKKyHxZzi1m1kG4WnYtWcJ+HYbygjE= +github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= github.com/go-openapi/spec v0.19.2 h1:SStNd1jRcYtfKCN7R0laGNs80WYYvn5CbBjM2sOmCrE= github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= +github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.19.0 h1:0Dn9qy1G9+UJfRU7TR8bmdGxb4uifB7HNrJjOnV0yPk= +github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-openapi/swag v0.19.2 h1:jvO6bCMBEilGwMfHhrd61zIID4oIFdwb76V17SM88dE= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-openapi/validate v0.19.2 h1:ky5l57HjyVRrsJfd2+Ro5Z9PjGuKbsmftwyMtk8H7js= +github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= +github.com/go-ozzo/ozzo-validation v3.5.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= +github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I= github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -104,6 +205,8 @@ github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA// github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/cadvisor v0.34.0/go.mod h1:1nql6U13uTHaLYB8rLS5x9IJc2qT6Xd/Tr1sTX6NE48= +github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -112,6 +215,9 @@ github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= @@ -119,46 +225,88 @@ github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhp github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/gophercloud/gophercloud v0.1.0 h1:P/nh25+rzXouhytV2pUHBb65fnds26Ghl8/391+sT5o= github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc h1:f8eY6cV/x1x+HLjOp4r72s/31/V2aTUtg5oKRRPf8/Q= github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20190222133341-cfaf5686ec79/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.3.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/heketi/heketi v9.0.0+incompatible/go.mod h1:bB9ly3RchcQqsQ9CpyaQwvva7RS5ytVoSoholZQON6o= +github.com/heketi/rest v0.0.0-20180404230133-aa6a65207413/go.mod h1:BeS3M108VzVlmAue3lv2WcGuPAX94/KN63MUURzbYSI= +github.com/heketi/tests v0.0.0-20151005000721-f3775cbcefd6/go.mod h1:xGMAM8JLi7UkZt1i4FQeQy0R2T8GLUwQhOP5M1gBhy4= +github.com/heketi/utils v0.0.0-20170317161834-435bc5bdfa64/go.mod h1:RYlF4ghFZPPmk2TC5REt5OFwvfb6lzxFWrTWB+qs28s= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/karrick/godirwalk v1.7.5/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/libopenstorage/openstorage v1.0.0/go.mod h1:Sp1sIObHjat1BeXhfMqLZ14wnOzEhNx2YQedreMcUyc= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= +github.com/lpabon/godbc v0.1.1/go.mod h1:Jo9QV0cf3U6jZABgiJ2skINAXb9j8m51r07g4KI92ZA= +github.com/lucas-clemente/aes12 v0.0.0-20171027163421-cd47fb39b79f/go.mod h1:JpH9J1c9oX6otFSgdUHwUBUizmKlrMjxWnIAjff4m04= +github.com/lucas-clemente/quic-clients v0.1.0/go.mod h1:y5xVIEoObKqULIKivu+gD/LU90pL73bTdtQjPBvtCBk= +github.com/lucas-clemente/quic-go v0.10.2/go.mod h1:hvaRS9IHjFLMq76puFJeWNfmn+H70QZ/CXoxqw9bzao= +github.com/lucas-clemente/quic-go-certificates v0.0.0-20160823095156-d2f86524cced/go.mod h1:NCcRLrOTZbzhZvixZLlERbJtDtYsmMw8Jc4vS8Z0g58= github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4= github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63 h1:nTT4s92Dgz2HlrB2NaMgvlfqHH39OgMhA7z3PK7PGD4= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.5 h1:jrGtp51JOKTWgvLFzfG6OtZOJcK2sEnzc/U+zw7TtbA= github.com/mattn/go-runewidth v0.0.5/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-shellwords v1.0.5/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mesos/mesos-go v0.0.9/go.mod h1:kPYCMQ9gsOXVAle1OsoY4I1+9kPu8GHkf88aV59fDr4= +github.com/mholt/certmagic v0.6.2-0.20190624175158-6a42ef9fe8c2/go.mod h1:g4cOPxcjV0oFq3qwpjSA30LReKD8AoIfwAY9VvG35NY= +github.com/miekg/dns v1.1.3/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.4/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mindprince/gonvml v0.0.0-20171110221305-fee913ce8fb2/go.mod h1:2eu9pRWp8mo84xCg6KswZ+USQHjwgRhNp06sozOdsTY= +github.com/mistifyio/go-zfs v2.1.1+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= @@ -167,15 +315,30 @@ github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lN github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mohae/deepcopy v0.0.0-20170603005431-491d3605edfb/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mrunalp/fileutils v0.0.0-20160930181131-4ee1cc9a8058/go.mod h1:x8F1gnqOkIEiO4rqoeEEEqQbo7HjGMTvyoq3gej4iT0= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d h1:7PxY7LVfSZm7PEeBTyK1rj1gABdCO2mbri6GKO1cMDs= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mvdan/xurls v1.1.0/go.mod h1:tQlNn3BED8bE/15hnSL2HLkDeLWpNPAwtw7wkEq44oU= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= +github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v1.0.0-rc2.0.20190611121236-6cc515888830/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runtime-spec v1.0.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.2.2/go.mod h1:+BLncwf63G4dgOzykXAxcmnFlUaOlkDdmw/CqsW6pjs= +github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.0.1/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= @@ -187,10 +350,17 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/pquerna/ffjson v0.0.0-20180717144149-af8b230fcd20/go.mod h1:YARuvh7BUWHNhzDq2OM5tzR2RiCcN2D7sapiKyCel/M= +github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740= github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 h1:PnBWHBf+6L0jOqq0gIVUe6Yk0/QMZ640k6NvkxcBf+8= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/quobyte/api v0.1.2/go.mod h1:jL7lIHrmqQ7yh05OJ+eEEdHr0u/kmT1Ff9iHd+4H6VI= github.com/rakyll/hey v0.1.2 h1:XlGaKcBdmXJaPImiTnE+TGLDUWQ2toYuHCwdrylLjmg= github.com/rakyll/hey v0.1.2/go.mod h1:S5M+++KwbmxA7w68S92B5NdWiCB+cIhITaMUkq9W608= github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= @@ -198,39 +368,75 @@ github.com/rivo/tview v0.0.0-20191018115645-bacbf5155bc1/go.mod h1:+rKjP5+h9HMwW github.com/rivo/uniseg v0.0.0-20190513083848-b9f5b9457d44/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/robfig/cron v1.1.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.14.3 h1:4EGfSkR2hJDB0s3oFfrlPqjU1e4WLncergLil3nEKW0= github.com/rs/zerolog v1.14.3/go.mod h1:3WXPzbXEEliJ+a6UFE4vhIxV8qR1EML6ngzP9ug4eYg= +github.com/rs/zerolog v1.17.2 h1:RMRHFw2+wF7LO0QqtELQwo8hqSmqISyCJeFeAAuWcRo= +github.com/rs/zerolog v1.17.2/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I= +github.com/rubiojr/go-vhd v0.0.0-20160810183302-0bfd3b39853c/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto= +github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= +github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.3/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/storageos/go-api v0.0.0-20180912212459-343b3eff91fc/go.mod h1:ZrLn+e0ZuF3Y65PNF6dIwbJPZqfmtCXxFm9ckv0agOY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/syndtr/gocapability v0.0.0-20160928074757-e7cb7fa329f4/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/thecodeteam/goscaleio v0.1.0/go.mod h1:68sdkZAsK8bvEwBlbQnlLS+xU+hvLYM/iQ8KXej1AwM= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/vishvananda/netlink v0.0.0-20171020171820-b2de5d10e38e/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= +github.com/vishvananda/netns v0.0.0-20171111001504-be1fbeda1936/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= +github.com/vmware/govmomi v0.20.1/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= +github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 h1:j2hhcujLRHAg872RWAV5yaUrEjHEObwDv3aImCaNLek= github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.uber.org/atomic v0.0.0-20181018215023-8dc6146f7569 h1:nSQar3Y0E3VQF/VdZ8PTAilaXpER+d7ypdABCrpwMdg= +go.uber.org/atomic v0.0.0-20181018215023-8dc6146f7569/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v0.0.0-20180122172545-ddea229ff1df h1:shvkWr0NAZkg4nPuE3XrKP0VuBPijjk3TfX6Y6acFNg= +go.uber.org/multierr v0.0.0-20180122172545-ddea229ff1df/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v0.0.0-20180814183419-67bc79d13d15 h1:Z2sc4+v0JHV6Mn4kX1f2a5nruNjmV+Th32sugE8zwz8= +go.uber.org/zap v0.0.0-20180814183419-67bc79d13d15/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8 h1:1wopBVtVdWnn03fZelqdXTqk7U7zPQCb+T4rbU9ZEoU= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -240,18 +446,28 @@ golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMx golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181017193950-04a2e542c03f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc h1:gkKoSkUmnU6bpS/VhkuO27bzQeSA51uaEfbOW5dNb68= golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -266,12 +482,17 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181004145325-8469e314837c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -283,64 +504,113 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20170824195420-5d2fd3ccab98/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.6.1-0.20190607001116-5213b8090861/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873 h1:nfPFGzJkUDX6uBmpN/pSw7MbOAWegH5QDQuoXFHedLg= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.23.0 h1:AzbTB6ux+okLTzP8Ru1Xs41C303zdcfEht7MQnYJt5A= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.0/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/mcuadros/go-syslog.v2 v2.2.1/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.1/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY= 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= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.2/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= k8s.io/api v0.0.0-20190918155943-95b840bb6a1f h1:8FRUST8oUkEI45WYKyD8ed7Ad0Kg5v11zHyPkEVb2xo= k8s.io/api v0.0.0-20190918155943-95b840bb6a1f/go.mod h1:uWuOHnjmNrtQomJrvEBg0c0HRNyQ+8KTEERVsK0PW48= +k8s.io/apiextensions-apiserver v0.0.0-20190918161926-8f644eb6e783 h1:V6ndwCPoao1yZ52agqOKaUAl7DYWVGiXjV7ePA2i610= +k8s.io/apiextensions-apiserver v0.0.0-20190918161926-8f644eb6e783/go.mod h1:xvae1SZB3E17UpV59AWc271W/Ph25N+bjPyR63X6tPY= k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655 h1:CS1tBQz3HOXiseWZu6ZicKX361CZLT97UFnnPx0aqBw= k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655/go.mod h1:nL6pwRT8NgfF8TT68DBI8uEePRt89cSvoXUVqbkWHq4= +k8s.io/apiserver v0.0.0-20190918160949-bfa5e2e684ad h1:IMoNR9pilTBaCS5WpwWnAdmoVYVeXowOD3bLrwxIAtQ= +k8s.io/apiserver v0.0.0-20190918160949-bfa5e2e684ad/go.mod h1:XPCXEwhjaFN29a8NldXA901ElnKeKLrLtREO9ZhFyhg= k8s.io/cli-runtime v0.0.0-20190918162238-f783a3654da8 h1:W3zT6wRwUKkEGnUu1OAAJFwcgETlCu1BLdNP/VCTFuM= k8s.io/cli-runtime v0.0.0-20190918162238-f783a3654da8/go.mod h1:WRliO+M6Osz7/zdOF0RI42IsJgSYHUwbLgqAWJPneSs= k8s.io/client-go v0.0.0-20190918160344-1fbdaa4c8d90 h1:mLmhKUm1X+pXu0zXMEzNsOF5E2kKFGe5o6BZBIIqA6A= k8s.io/client-go v0.0.0-20190918160344-1fbdaa4c8d90/go.mod h1:J69/JveO6XESwVgG53q3Uz5OSfgsv4uxpScmmyYOOlk= +k8s.io/cloud-provider v0.0.0-20190918163234-a9c1f33e9fb9/go.mod h1:YfUBehfPUDgnhqAFcuXj8haXt/v86nhy8r4ZOuSvXhg= +k8s.io/cluster-bootstrap v0.0.0-20190918163108-da9fdfce26bb/go.mod h1:mQVbtFRxlw/BzBqBaQwIMzjDTST1KrGtzWaR4CGlsTU= k8s.io/code-generator v0.0.0-20190912054826-cd179ad6a269/go.mod h1:V5BD6M4CyaN5m+VthcclXWsVcT1Hu+glwa1bi3MIsyE= +k8s.io/component-base v0.0.0-20190918160511-547f6c5d7090 h1:0UWOjjag5IcVoAko0g+3qGhegdwWkRf4v4AHCIMVwnc= k8s.io/component-base v0.0.0-20190918160511-547f6c5d7090/go.mod h1:933PBGtQFJky3TEwYx4aEPZ4IxqhWh3R6DCmzqIn1hA= +k8s.io/cri-api v0.0.0-20190828162817-608eb1dad4ac/go.mod h1:BvtUaNBr0fEpzb11OfrQiJLsLPtqbmulpo1fPwcpP6Q= +k8s.io/csi-translation-lib v0.0.0-20190918163402-db86a8c7bb21/go.mod h1:Ja9f0K9MkTuUSyBgpjFt2am69TOjrmkQUN25WTF3CCM= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/heapster v1.2.0-beta.1/go.mod h1:h1uhptVXMwC8xtZBYsPXKVi8fpdlYkTs6k949KozGrM= k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.4.0 h1:lCJCxf/LIowc2IGS9TPjWDyXY4nOmdGdfcwwDQCOURQ= k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/kube-aggregator v0.0.0-20190918161219-8c8f079fddc3/go.mod h1:NJisPUqwlg1A99RhO1BTnNtwC4pKUyXJ2f3Xc4PxKQg= +k8s.io/kube-controller-manager v0.0.0-20190918162944-7a93a0ddadd8/go.mod h1:+HrHoqJm0UqnlrBEKXGzs2701YN4+ozi76oG7iYvJ8s= k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf h1:EYm5AW/UUDbnmnI+gK0TJDVK9qPLhM+sRHYanNKw0EQ= k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/kube-proxy v0.0.0-20190918162534-de037b596c1e/go.mod h1:/48p8Y6dkWJrll4tsceAoGKudGpRmtQu/u1zlG14NnI= +k8s.io/kube-scheduler v0.0.0-20190918162820-3b5c1246eb18/go.mod h1:k2dnGirIGylr51dpqxn2Zv6Yt47A+6NiynBIYfAU67I= k8s.io/kubectl v0.0.0-20190918164019-21692a0861df h1:EwjdCG4HveZxJkI650+g4UoIuSvH7vODn55VmBjxIAo= k8s.io/kubectl v0.0.0-20190918164019-21692a0861df/go.mod h1:AjffgL1ZYSrbpRJHER9vC+/INYwTSdmoZD0DXhMKzxQ= +k8s.io/kubelet v0.0.0-20190918162654-250a1838aa2c/go.mod h1:LGhpyzd/3AkWcFcQJ3yO1UxMnJ6urMkCYfCp4iVxhjs= +k8s.io/kubernetes v1.16.3 h1:Bk2cKOdTtuGeod3+ytBeXxqIVHbh7Pu+aq0c+YJLX7g= +k8s.io/kubernetes v1.16.3/go.mod h1:hJd0X6w7E/MiE7PcDp11XHhdgQBYc33vP+WtTJqG/AU= +k8s.io/legacy-cloud-providers v0.0.0-20190918163543-cfa506e53441/go.mod h1:Phw/j+7dcoTPXRkv9Nyi3RJuA6SVSoHlc7M5K1pHizM= k8s.io/metrics v0.0.0-20190918162108-227c654b2546 h1:GmR5FKUvbcVV2TLAVFusUFWENjlIg7KLldAST5DqalY= k8s.io/metrics v0.0.0-20190918162108-227c654b2546/go.mod h1:XUFuIsGbIqaUga6Ivs02cCzxNjY4RPRvYnW0KhmnpQY= +k8s.io/repo-infra v0.0.0-20181204233714-00fe14e3d1a3/go.mod h1:+G1xBfZDfVFsm1Tj/HNCvg4QqWx8rJ2Fxpqr1rqp/gQ= +k8s.io/sample-apiserver v0.0.0-20190918161442-d4c9c65c82af/go.mod h1:HP/BmiRyZTMIZ5RI2p4tCz/b2kre7URuKLQ7/KHqWAs= k8s.io/utils v0.0.0-20190801114015-581e00157fb1 h1:+ySTxfHnfzZb9ys375PXNlLhkJPLKgHajBU0N62BDvE= k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= @@ -351,7 +621,10 @@ modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0= sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/structured-merge-diff v0.0.0-20190817042607-6149e4549fca h1:6dsH6AYQWbyZmtttJNe8Gq1cXOeS1BdV3eW37zHilAQ= +sigs.k8s.io/structured-merge-diff v0.0.0-20190817042607-6149e4549fca/go.mod h1:IIgPezJWb76P0hotTxzDbWsMYB8APh18qZnxkomBpxA= sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +vbom.ml/util v0.0.0-20160121211510-db5cfe13f5cc/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI= vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 h1:O69FD9pJA4WUZlEwYatBEEkRWKQ5cKodWpdKTrCS/iQ= vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI= diff --git a/internal/k8s/assets/config b/internal/client/assets/config similarity index 100% rename from internal/k8s/assets/config rename to internal/client/assets/config diff --git a/internal/k8s/assets/config.1 b/internal/client/assets/config.1 similarity index 100% rename from internal/k8s/assets/config.1 rename to internal/client/assets/config.1 diff --git a/internal/k8s/api.go b/internal/client/client.go similarity index 66% rename from internal/k8s/api.go rename to internal/client/client.go index c71382a1..2b5d2caa 100644 --- a/internal/k8s/api.go +++ b/internal/client/client.go @@ -1,22 +1,17 @@ -package k8s +package client import ( - "fmt" "path/filepath" - "strings" "sync" "time" - "k8s.io/client-go/discovery/cached/disk" - - "github.com/rs/zerolog" "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/fields" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/version" + "k8s.io/client-go/discovery/cached/disk" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" restclient "k8s.io/client-go/rest" @@ -29,116 +24,78 @@ const NA = "n/a" var supportedMetricsAPIVersions = []string{"v1beta1"} -type ( - // Collection of empty interfaces. - Collection []interface{} +// Authorizer checks what a user can or cannot do to a resource. +type Authorizer interface { + // CanI returns true if the user can use these actions for a given resource. + CanI(ns, gvr string, verbs []string) (bool, error) +} - // Cruder represent a crudable Kubernetes resource. - Cruder interface { - Get(ns string, name string) (interface{}, error) - List(ns string) (Collection, error) - Delete(ns string, name string) error - SetFieldSelector(string) - SetLabelSelector(string) - } +// BOZO!! Refactor! +// Connection represents a Kubenetes apiserver connection. +type Connection interface { + Authorizer - // Connection represents a Kubenetes apiserver connection. - Connection interface { - Config() *Config - DialOrDie() kubernetes.Interface - SwitchContextOrDie(ctx string) - NSDialOrDie() dynamic.NamespaceableResourceInterface - CachedDiscovery() (*disk.CachedDiscoveryClient, error) - RestConfigOrDie() *restclient.Config - MXDial() (*versioned.Clientset, error) - DynDialOrDie() dynamic.Interface - HasMetrics() bool - IsNamespaced(n string) bool - SupportsResource(group string) bool - ValidNamespaces() ([]v1.Namespace, error) - NodePods(node string) (*v1.PodList, error) - SupportsRes(grp string, versions []string) (string, bool, error) - ServerVersion() (*version.Info, error) - FetchNodes() (*v1.NodeList, error) - CurrentNamespaceName() (string, error) - CheckNSAccess(ns string) error - CheckListNSAccess() error - CanIAccess(ns, rvg string, verbs []string) (bool, error) - } + Config() *Config + DialOrDie() kubernetes.Interface + SwitchContextOrDie(ctx string) + NSDialOrDie() dynamic.NamespaceableResourceInterface + CachedDiscovery() (*disk.CachedDiscoveryClient, error) + RestConfigOrDie() *restclient.Config + MXDial() (*versioned.Clientset, error) + DynDialOrDie() dynamic.Interface + HasMetrics() bool + IsNamespaced(n string) bool + SupportsResource(group string) bool + ValidNamespaces() ([]v1.Namespace, error) + SupportsRes(grp string, versions []string) (string, bool, error) + ServerVersion() (*version.Info, error) + FetchNodes() (*v1.NodeList, error) + CurrentNamespaceName() (string, error) +} - k8sClient struct { - client kubernetes.Interface - dClient dynamic.Interface - nsClient dynamic.NamespaceableResourceInterface - mxsClient *versioned.Clientset - } - - // APIClient represents a Kubernetes api client. - APIClient struct { - k8sClient - - cachedDiscovery *disk.CachedDiscoveryClient - config *Config - useMetricServer bool - log zerolog.Logger - mx sync.Mutex - } -) +// APIClient represents a Kubernetes api client. +type APIClient struct { + client kubernetes.Interface + dClient dynamic.Interface + nsClient dynamic.NamespaceableResourceInterface + mxsClient *versioned.Clientset + cachedDiscovery *disk.CachedDiscoveryClient + config *Config + useMetricServer bool + mx sync.Mutex +} // InitConnectionOrDie initialize connection from command line args. // Checks for connectivity with the api server. -func InitConnectionOrDie(config *Config, logger zerolog.Logger) *APIClient { - conn := APIClient{config: config, log: logger} +func InitConnectionOrDie(config *Config) *APIClient { + conn := APIClient{config: config} conn.useMetricServer = conn.supportsMxServer() return &conn } -// CheckListNSAccess check if current user can list namespaces. -func (a *APIClient) CheckListNSAccess() error { - ns := NewNamespace(a) - _, err := ns.List("", metav1.ListOptions{}) - return err -} - -// CheckNSAccess asserts if user can access a namespace. -func (a *APIClient) CheckNSAccess(n string) error { - ns := NewNamespace(a) - if n == "" { - _, err := ns.List(n, metav1.ListOptions{}) - return err - } - - _, err := ns.Get("", n) - return err -} - -func makeSAR(ns, rvg string) *authorizationv1.SelfSubjectAccessReview { - gvr, _ := schema.ParseResourceArg(strings.ToLower(rvg)) - if gvr == nil { - panic(fmt.Errorf("Unable to get GVR from url %s", rvg)) - } - log.Debug().Msgf("GVR for %s -- %#v", rvg, *gvr) +func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview { + res := GVR(gvr).AsGVR() return &authorizationv1.SelfSubjectAccessReview{ Spec: authorizationv1.SelfSubjectAccessReviewSpec{ ResourceAttributes: &authorizationv1.ResourceAttributes{ Namespace: ns, - Group: gvr.Group, - Resource: gvr.Resource, + Group: res.Group, + Resource: res.Resource, }, }, } } -// CanIAccess checks if user has access to a certain resource. -func (a *APIClient) CanIAccess(ns, rvg string, verbs []string) (bool, error) { - sar := makeSAR(ns, rvg) +// CanI checks if user has access to a certain resource. +func (a *APIClient) CanI(ns, gvr string, verbs []string) (bool, error) { + sar := makeSAR(ns, gvr) dial := a.DialOrDie().AuthorizationV1().SelfSubjectAccessReviews() for _, v := range verbs { sar.Spec.ResourceAttributes.Verb = v resp, err := dial.Create(sar) if err != nil { - log.Error().Err(err).Msgf("CanIAccess") + log.Error().Err(err).Msgf("CanI") return false, err } if !resp.Status.Allowed { @@ -177,19 +134,6 @@ func (a *APIClient) ValidNamespaces() ([]v1.Namespace, error) { return nn.Items, nil } -// NodePods returns a collection of all available pods on a given node. -func (a *APIClient) NodePods(node string) (*v1.PodList, error) { - const selFmt = "spec.nodeName=%s,status.phase!=%s,status.phase!=%s" - fieldSelector, err := fields.ParseSelector(fmt.Sprintf(selFmt, node, v1.PodSucceeded, v1.PodFailed)) - if err != nil { - return nil, err - } - - return a.DialOrDie().CoreV1().Pods("").List(metav1.ListOptions{ - FieldSelector: fieldSelector.String(), - }) -} - // IsNamespaced check on server if given resource is namespaced func (a *APIClient) IsNamespaced(res string) bool { discovery, err := a.CachedDiscovery() @@ -247,7 +191,7 @@ func (a *APIClient) DialOrDie() kubernetes.Interface { var err error if a.client, err = kubernetes.NewForConfig(a.RestConfigOrDie()); err != nil { - a.log.Fatal().Msgf("Unable to connect to api server %v", err) + log.Fatal().Msgf("Unable to connect to api server %v", err) } return a.client } @@ -256,7 +200,7 @@ func (a *APIClient) DialOrDie() kubernetes.Interface { func (a *APIClient) RestConfigOrDie() *restclient.Config { cfg, err := a.config.RESTConfig() if err != nil { - a.log.Panic().Msgf("Unable to connect to api server %v", err) + log.Panic().Msgf("Unable to connect to api server %v", err) } return cfg } @@ -286,7 +230,7 @@ func (a *APIClient) DynDialOrDie() dynamic.Interface { var err error if a.dClient, err = dynamic.NewForConfig(a.RestConfigOrDie()); err != nil { - a.log.Panic().Err(err) + log.Panic().Err(err) } return a.dClient } @@ -318,7 +262,7 @@ func (a *APIClient) MXDial() (*versioned.Clientset, error) { } var err error if a.mxsClient, err = versioned.NewForConfig(a.RestConfigOrDie()); err != nil { - a.log.Error().Err(err) + log.Error().Err(err) } return a.mxsClient, err @@ -328,14 +272,14 @@ func (a *APIClient) MXDial() (*versioned.Clientset, error) { func (a *APIClient) SwitchContextOrDie(ctx string) { currentCtx, err := a.config.CurrentContextName() if err != nil { - panic(err) + log.Fatal().Err(err).Msg("Fetching current context") } if currentCtx != ctx { a.cachedDiscovery = nil a.reset() if err := a.config.SwitchContext(ctx); err != nil { - panic(err) + log.Fatal().Err(err).Msg("Switching context") } a.useMetricServer = a.supportsMxServer() } diff --git a/internal/k8s/config.go b/internal/client/config.go similarity index 97% rename from internal/k8s/config.go rename to internal/client/config.go index ac4a2bc4..364e6236 100644 --- a/internal/k8s/config.go +++ b/internal/client/config.go @@ -1,8 +1,9 @@ -package k8s +package client import ( "errors" "fmt" + "sync" "github.com/rs/zerolog/log" v1 "k8s.io/api/core/v1" @@ -12,8 +13,6 @@ import ( clientcmdapi "k8s.io/client-go/tools/clientcmd/api" ) -const defaultNamespace = "default" - // Config tracks a kubernetes configuration. type Config struct { flags *genericclioptions.ConfigFlags @@ -21,11 +20,15 @@ type Config struct { currentContext string rawConfig *clientcmdapi.Config restConfig *restclient.Config + mutex *sync.RWMutex } // NewConfig returns a new k8s config or an error if the flags are invalid. func NewConfig(f *genericclioptions.ConfigFlags) *Config { - return &Config{flags: f} + return &Config{ + flags: f, + mutex: &sync.RWMutex{}, + } } // Flags returns configuration flags. @@ -233,12 +236,18 @@ func (c *Config) NamespaceNames(nns []v1.Namespace) []string { // ConfigAccess return the current kubeconfig api server access configuration. func (c *Config) ConfigAccess() (clientcmd.ConfigAccess, error) { + c.mutex.RLock() + defer c.mutex.RUnlock() + c.ensureConfig() return c.clientConfig.ConfigAccess(), nil } // RawConfig fetch the current kubeconfig with no overrides. func (c *Config) RawConfig() (clientcmdapi.Config, error) { + c.mutex.Lock() + defer c.mutex.Unlock() + if c.rawConfig != nil { if c.rawConfig.CurrentContext == c.currentContext { return *c.rawConfig, nil @@ -283,7 +292,6 @@ func (c *Config) ensureConfig() { log.Debug().Msg("Loading raw config from flags...") c.clientConfig = c.flags.ToRawKubeConfigLoader() - return } // ---------------------------------------------------------------------------- diff --git a/internal/k8s/config_test.go b/internal/client/config_test.go similarity index 90% rename from internal/k8s/config_test.go rename to internal/client/config_test.go index 5e568c16..19a95703 100644 --- a/internal/k8s/config_test.go +++ b/internal/client/config_test.go @@ -1,11 +1,11 @@ -package k8s_test +package client_test import ( "errors" "fmt" "testing" - "github.com/derailed/k9s/internal/k8s" + "github.com/derailed/k9s/internal/client" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" @@ -28,7 +28,7 @@ func TestConfigCurrentContext(t *testing.T) { } for _, u := range uu { - cfg := k8s.NewConfig(u.flags) + cfg := client.NewConfig(u.flags) ctx, err := cfg.CurrentContextName() assert.Nil(t, err) assert.Equal(t, u.context, ctx) @@ -46,7 +46,7 @@ func TestConfigCurrentCluster(t *testing.T) { } for _, u := range uu { - cfg := k8s.NewConfig(u.flags) + cfg := client.NewConfig(u.flags) ctx, err := cfg.CurrentClusterName() assert.Nil(t, err) assert.Equal(t, u.cluster, ctx) @@ -64,7 +64,7 @@ func TestConfigCurrentUser(t *testing.T) { } for _, u := range uu { - cfg := k8s.NewConfig(u.flags) + cfg := client.NewConfig(u.flags) ctx, err := cfg.CurrentUserName() assert.Nil(t, err) assert.Equal(t, u.user, ctx) @@ -83,7 +83,7 @@ func TestConfigCurrentNamespace(t *testing.T) { } for _, u := range uu { - cfg := k8s.NewConfig(u.flags) + cfg := client.NewConfig(u.flags) ns, err := cfg.CurrentNamespaceName() assert.Equal(t, u.err, err) assert.Equal(t, u.namespace, ns) @@ -102,7 +102,7 @@ func TestConfigGetContext(t *testing.T) { } for _, u := range uu { - cfg := k8s.NewConfig(u.flags) + cfg := client.NewConfig(u.flags) ctx, err := cfg.GetContext(u.cluster) if err != nil { assert.Equal(t, u.err, err) @@ -120,7 +120,7 @@ func TestConfigSwitchContext(t *testing.T) { ClusterName: &cluster, } - cfg := k8s.NewConfig(&flags) + cfg := client.NewConfig(&flags) err := cfg.SwitchContext("blee") assert.Nil(t, err) ctx, err := cfg.CurrentContextName() @@ -135,7 +135,7 @@ func TestConfigClusterNameFromContext(t *testing.T) { ClusterName: &cluster, } - cfg := k8s.NewConfig(&flags) + cfg := client.NewConfig(&flags) cl, err := cfg.ClusterNameFromContext("blee") assert.Nil(t, err) assert.Equal(t, "blee", cl) @@ -148,7 +148,7 @@ func TestConfigAccess(t *testing.T) { ClusterName: &cluster, } - cfg := k8s.NewConfig(&flags) + cfg := client.NewConfig(&flags) acc, err := cfg.ConfigAccess() assert.Nil(t, err) assert.True(t, len(acc.GetDefaultFilename()) > 0) @@ -161,7 +161,7 @@ func TestConfigContexts(t *testing.T) { ClusterName: &cluster, } - cfg := k8s.NewConfig(&flags) + cfg := client.NewConfig(&flags) cc, err := cfg.Contexts() assert.Nil(t, err) assert.Equal(t, 3, len(cc)) @@ -174,7 +174,7 @@ func TestConfigContextNames(t *testing.T) { ClusterName: &cluster, } - cfg := k8s.NewConfig(&flags) + cfg := client.NewConfig(&flags) cc, err := cfg.ContextNames() assert.Nil(t, err) assert.Equal(t, 3, len(cc)) @@ -187,7 +187,7 @@ func TestConfigClusterNames(t *testing.T) { ClusterName: &cluster, } - cfg := k8s.NewConfig(&flags) + cfg := client.NewConfig(&flags) cc, err := cfg.ClusterNames() assert.Nil(t, err) assert.Equal(t, 3, len(cc)) @@ -200,7 +200,7 @@ func TestConfigDelContext(t *testing.T) { ClusterName: &cluster, } - cfg := k8s.NewConfig(&flags) + cfg := client.NewConfig(&flags) err := cfg.DelContext("fred") assert.Nil(t, err) cc, err := cfg.ContextNames() @@ -214,7 +214,7 @@ func TestConfigRestConfig(t *testing.T) { KubeConfig: &kubeConfig, } - cfg := k8s.NewConfig(&flags) + cfg := client.NewConfig(&flags) rc, err := cfg.RESTConfig() assert.Nil(t, err) assert.Equal(t, "https://localhost:3000", rc.Host) @@ -226,7 +226,7 @@ func TestConfigBadConfig(t *testing.T) { KubeConfig: &kubeConfig, } - cfg := k8s.NewConfig(&flags) + cfg := client.NewConfig(&flags) _, err := cfg.RESTConfig() assert.NotNil(t, err) } @@ -238,7 +238,7 @@ func TestNamespaceNames(t *testing.T) { KubeConfig: &kubeConfig, } - cfg := k8s.NewConfig(&flags) + cfg := client.NewConfig(&flags) nn := []v1.Namespace{ {ObjectMeta: metav1.ObjectMeta{Name: "ns1"}}, diff --git a/internal/client/gvr.go b/internal/client/gvr.go new file mode 100644 index 00000000..264cfa81 --- /dev/null +++ b/internal/client/gvr.go @@ -0,0 +1,142 @@ +package client + +import ( + "fmt" + "path" + "strings" + + "github.com/rs/zerolog/log" + "k8s.io/apimachinery/pkg/runtime/schema" + "vbom.ml/util/sortorder" +) + +// GVR represents a kubernetes resource schema as a string. +// Format is group/version/resources +type GVR string + +// NewGVR builds a new gvr from a group, version, resource. +func NewGVR(g, v, r string) GVR { + return GVR(path.Join(g, v, r)) +} + +// FromGVAndR builds a gvr from a group/version and resource. +func FromGVAndR(gv, r string) GVR { + return GVR(path.Join(gv, r)) +} + +// ResName returns a resource . separated descriptor in the shape of kind.version.group. +func (g GVR) ResName() string { + return g.ToR() + "." + g.ToV() + "." + g.ToG() +} + +// String returns gvr as string. +func (g GVR) String() string { + return string(g) +} + +// AsGV returns the group version scheme representation. +func (g GVR) AsGV() schema.GroupVersion { + return schema.GroupVersion{ + Group: g.ToG(), + Version: g.ToV(), + } +} + +// AsGVR returns a a full schema representation. +func (g GVR) AsGVR() schema.GroupVersionResource { + return schema.GroupVersionResource{ + Group: g.ToG(), + Version: g.ToV(), + Resource: g.ToR(), + } +} + +// ToV returns the resource version. +func (g GVR) ToV() string { + tokens := strings.Split(string(g), "/") + if len(tokens) < 2 { + return "" + } + return tokens[len(tokens)-2] +} + +func (g GVR) ToRAndG() (string, string) { + tokens := strings.Split(string(g), "/") + switch len(tokens) { + case 3: + return tokens[2], tokens[0] + case 2: + return tokens[1], "core" + default: + return tokens[0], "core" + } +} + +// ToR returns the resource name. +func (g GVR) ToR() string { + tokens := strings.Split(string(g), "/") + return tokens[len(tokens)-1] +} + +// ToG returns the resource group name. +func (g GVR) ToG() string { + tokens := strings.Split(string(g), "/") + switch len(tokens) { + case 3: + return tokens[0] + default: + return "" + } +} + +// +type GVRs []GVR + +func (g GVRs) Len() int { + return len(g) +} + +func (g GVRs) Swap(i, j int) { + g[i], g[j] = g[j], g[i] +} + +func (g GVRs) Less(i, j int) bool { + g1, g2 := g[i].ToG(), g[j].ToG() + + return sortorder.NaturalLess(g1, g2) +} + +// Helper... + +// Can determines the available actions for a given resource. +func Can(verbs []string, v string) bool { + for _, verb := range verbs { + candidates, err := mapVerb(v) + if err != nil { + log.Error().Err(err).Msgf("verb mapping failed") + return false + } + for _, c := range candidates { + if verb == c { + return true + } + } + } + + return false +} + +func mapVerb(v string) ([]string, error) { + switch v { + case "describe": + return []string{"get"}, nil + case "view": + return []string{"get", "list"}, nil + case "delete": + return []string{"delete"}, nil + case "edit": + return []string{"patch", "update"}, nil + default: + return []string{}, fmt.Errorf("no standard verb for %q", v) + } +} diff --git a/internal/client/gvr_test.go b/internal/client/gvr_test.go new file mode 100644 index 00000000..471f3181 --- /dev/null +++ b/internal/client/gvr_test.go @@ -0,0 +1,185 @@ +package client_test + +import ( + "sort" + "testing" + + "github.com/derailed/k9s/internal/client" + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func TestGVRSort(t *testing.T) { + gg := client.GVRs{"v1/pods", "v1/services", "apps/v1/deployments"} + sort.Sort(gg) + assert.Equal(t, client.GVRs{"v1/pods", "v1/services", "apps/v1/deployments"}, gg) +} + +func TestGVRCan(t *testing.T) { + uu := map[string]struct { + vv []string + v string + e bool + }{ + "describe": {[]string{"get"}, "describe", true}, + "view": {[]string{"get", "list", "watch"}, "view", true}, + "delete": {[]string{"delete", "list", "watch"}, "delete", true}, + "no_delete": {[]string{"get", "list", "watch"}, "delete", false}, + "edit": {[]string{"path", "update", "watch"}, "edit", true}, + "no_edit": {[]string{"get", "list", "watch"}, "edit", false}, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, client.Can(u.vv, u.v)) + }) + } +} + +func TestAsGVR(t *testing.T) { + uu := map[string]struct { + gvr string + e schema.GroupVersionResource + }{ + "full": {"apps/v1/deployments", schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}}, + "core": {"v1/pods", schema.GroupVersionResource{Version: "v1", Resource: "pods"}}, + "bork": {"users", schema.GroupVersionResource{Resource: "users"}}, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, client.GVR(u.gvr).AsGVR()) + }) + } +} + +func TestAsGV(t *testing.T) { + uu := map[string]struct { + gvr string + e schema.GroupVersion + }{ + "full": {"apps/v1/deployments", schema.GroupVersion{Group: "apps", Version: "v1"}}, + "core": {"v1/pods", schema.GroupVersion{Version: "v1"}}, + "bork": {"users", schema.GroupVersion{}}, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, client.GVR(u.gvr).AsGV()) + }) + } +} + +func TestNewGVR(t *testing.T) { + uu := map[string]struct { + g, v, r string + e string + }{ + "full": {"apps", "v1", "deployments", "apps/v1/deployments"}, + "core": {"", "v1", "pods", "v1/pods"}, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, client.NewGVR(u.g, u.v, u.r).String()) + }) + } +} + +func TestResName(t *testing.T) { + uu := map[string]struct { + gvr string + e string + }{ + "full": {"apps/v1/deployments", "deployments.v1.apps"}, + "core": {"v1/pods", "pods.v1."}, + "k9s": {"users", "users.."}, + "empty": {"", ".."}, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, client.GVR(u.gvr).ResName()) + }) + } +} + +func TestToR(t *testing.T) { + uu := map[string]struct { + gvr string + e string + }{ + "full": {"apps/v1/deployments", "deployments"}, + "core": {"v1/pods", "pods"}, + "k9s": {"users", "users"}, + "empty": {"", ""}, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, client.GVR(u.gvr).ToR()) + }) + } +} + +func TestToG(t *testing.T) { + uu := map[string]struct { + gvr string + e string + }{ + "full": {"apps/v1/deployments", "apps"}, + "core": {"v1/pods", ""}, + "k9s": {"users", ""}, + "empty": {"", ""}, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, client.GVR(u.gvr).ToG()) + }) + } +} + +func TestToV(t *testing.T) { + uu := map[string]struct { + gvr string + e string + }{ + "full": {"apps/v1/deployments", "v1"}, + "core": {"v1beta1/pods", "v1beta1"}, + "k9s": {"users", ""}, + "empty": {"", ""}, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, client.GVR(u.gvr).ToV()) + }) + } +} + +func TestToString(t *testing.T) { + uu := map[string]struct { + gvr string + }{ + "full": {"apps/v1/deployments"}, + "core": {"v1beta1/pods"}, + "k9s": {"users"}, + "empty": {""}, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.gvr, client.GVR(u.gvr).String()) + }) + } +} diff --git a/internal/client/helper_test.go b/internal/client/helper_test.go new file mode 100644 index 00000000..4a4c5091 --- /dev/null +++ b/internal/client/helper_test.go @@ -0,0 +1,37 @@ +package client_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/client" + "github.com/stretchr/testify/assert" +) + +func TestNamespaced(t *testing.T) { + uu := []struct { + p, ns, n string + }{ + {"fred/blee", "fred", "blee"}, + {"blee", "", "blee"}, + } + + for _, u := range uu { + ns, n := client.Namespaced(u.p) + assert.Equal(t, u.ns, ns) + assert.Equal(t, u.n, n) + } +} + +func TestFQN(t *testing.T) { + uu := []struct { + ns, n string + e string + }{ + {"fred", "blee", "fred/blee"}, + {"", "blee", "blee"}, + } + + for _, u := range uu { + assert.Equal(t, u.e, client.FQN(u.ns, u.n)) + } +} diff --git a/internal/client/helpers.go b/internal/client/helpers.go new file mode 100644 index 00000000..c78c6aec --- /dev/null +++ b/internal/client/helpers.go @@ -0,0 +1,40 @@ +package client + +import ( + "os/user" + "path" + "regexp" + "strings" + + "github.com/rs/zerolog/log" +) + +var toFileName = regexp.MustCompile(`[^(\w/\.)]`) + +// Namespaced converts a resource path to namespace and resource name. +func Namespaced(p string) (string, string) { + ns, n := path.Split(p) + + return strings.Trim(ns, "/"), n +} + +// FQN returns a fully qualified resource name. +func FQN(ns, n string) string { + if ns == "" { + return n + } + return ns + "/" + n +} + +func mustHomeDir() string { + usr, err := user.Current() + if err != nil { + log.Fatal().Err(err).Msg("Die getting user home directory") + } + return usr.HomeDir +} + +func toHostDir(host string) string { + h := strings.Replace(strings.Replace(host, "https://", "", 1), "http://", "", 1) + return toFileName.ReplaceAllString(h, "_") +} diff --git a/internal/k8s/metrics.go b/internal/client/metrics.go similarity index 64% rename from internal/k8s/metrics.go rename to internal/client/metrics.go index 95a76d30..0a4448e3 100644 --- a/internal/k8s/metrics.go +++ b/internal/client/metrics.go @@ -1,6 +1,8 @@ -package k8s +package client import ( + "math" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" @@ -9,7 +11,6 @@ import ( type ( // MetricsServer serves cluster metrics for nodes and pods. MetricsServer struct { - *base Connection } @@ -45,47 +46,44 @@ type ( // NewMetricsServer return a metric server instance. func NewMetricsServer(c Connection) *MetricsServer { - return &MetricsServer{&base{}, c} + return &MetricsServer{Connection: c} } // NodesMetrics retrieves metrics for a given set of nodes. -func (m *MetricsServer) NodesMetrics(nodes Collection, metrics *mv1beta1.NodeMetricsList, mmx NodesMetrics) { - for _, n := range nodes { - no := n.(*v1.Node) +func (m *MetricsServer) NodesMetrics(nodes *v1.NodeList, metrics *mv1beta1.NodeMetricsList, mmx NodesMetrics) { + for _, no := range nodes.Items { mmx[no.Name] = NodeMetrics{ AvailCPU: no.Status.Allocatable.Cpu().MilliValue(), - AvailMEM: ToMB(no.Status.Allocatable.Memory().Value()), + AvailMEM: toMB(no.Status.Allocatable.Memory().Value()), TotalCPU: no.Status.Capacity.Cpu().MilliValue(), - TotalMEM: ToMB(no.Status.Capacity.Memory().Value()), + TotalMEM: toMB(no.Status.Capacity.Memory().Value()), } } for _, c := range metrics.Items { if mx, ok := mmx[c.Name]; ok { mx.CurrentCPU = c.Usage.Cpu().MilliValue() - mx.CurrentMEM = ToMB(c.Usage.Memory().Value()) + mx.CurrentMEM = toMB(c.Usage.Memory().Value()) mmx[c.Name] = mx } } } // ClusterLoad retrieves all cluster nodes metrics. -func (m *MetricsServer) ClusterLoad(nos Collection, nmx Collection, mx *ClusterMetrics) { - nodeMetrics := make(NodesMetrics, len(nos)) - for _, n := range nos { - no := n.(*v1.Node) +func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsList, mx *ClusterMetrics) error { + nodeMetrics := make(NodesMetrics, len(nos.Items)) + for _, no := range nos.Items { nodeMetrics[no.Name] = NodeMetrics{ AvailCPU: no.Status.Allocatable.Cpu().MilliValue(), - AvailMEM: ToMB(no.Status.Allocatable.Memory().Value()), + AvailMEM: toMB(no.Status.Allocatable.Memory().Value()), } } - for _, mx := range nmx { - mxx := mx.(*mv1beta1.NodeMetrics) - if m, ok := nodeMetrics[mxx.Name]; ok { - m.CurrentCPU = mxx.Usage.Cpu().MilliValue() - m.CurrentMEM = ToMB(mxx.Usage.Memory().Value()) - nodeMetrics[mxx.Name] = m + for _, mx := range nmx.Items { + if m, ok := nodeMetrics[mx.Name]; ok { + m.CurrentCPU = mx.Usage.Cpu().MilliValue() + m.CurrentMEM = toMB(mx.Usage.Memory().Value()) + nodeMetrics[mx.Name] = m } } @@ -96,8 +94,9 @@ func (m *MetricsServer) ClusterLoad(nos Collection, nmx Collection, mx *ClusterM mem += mx.CurrentMEM tmem += mx.AvailMEM } - mx.PercCPU, mx.PercMEM = toPerc(cpu, tcpu), toPerc(mem, tmem) + + return nil } // FetchNodesMetrics return all metrics for pods in a given namespace. @@ -120,6 +119,16 @@ func (m *MetricsServer) FetchPodsMetrics(ns string) (*mv1beta1.PodMetricsList, e return client.MetricsV1beta1().PodMetricses(ns).List(metav1.ListOptions{}) } +// FetchPodsMetrics return all metrics for pods in a given namespace. +func (m *MetricsServer) FetchPodMetrics(ns, sel string) (*mv1beta1.PodMetrics, error) { + client, err := m.MXDial() + if err != nil { + return nil, err + } + + return client.MetricsV1beta1().PodMetricses(ns).Get(sel, metav1.GetOptions{}) +} + // PodsMetrics retrieves metrics for all pods in a given namespace. func (m *MetricsServer) PodsMetrics(pods *mv1beta1.PodMetricsList, mmx PodsMetrics) { // Compute all pod's containers metrics. @@ -127,8 +136,25 @@ func (m *MetricsServer) PodsMetrics(pods *mv1beta1.PodMetricsList, mmx PodsMetri var mx PodMetrics for _, c := range p.Containers { mx.CurrentCPU += c.Usage.Cpu().MilliValue() - mx.CurrentMEM += ToMB(c.Usage.Memory().Value()) + mx.CurrentMEM += toMB(c.Usage.Memory().Value()) } mmx[p.Namespace+"/"+p.Name] = mx } } + +// 0--------------------------------------------------------------------------- +// Helpers... + +const megaByte = 1024 * 1024 + +// toMB converts bytes to megabytes. +func toMB(v int64) float64 { + return float64(v) / megaByte +} + +func toPerc(v1, v2 float64) float64 { + if v2 == 0 { + return 0 + } + return math.Round((v1 / v2) * 100) +} diff --git a/internal/k8s/metrics_test.go b/internal/client/metrics_test.go similarity index 73% rename from internal/k8s/metrics_test.go rename to internal/client/metrics_test.go index 05dd8043..17331135 100644 --- a/internal/k8s/metrics_test.go +++ b/internal/client/metrics_test.go @@ -1,4 +1,4 @@ -package k8s +package client import ( "testing" @@ -7,6 +7,7 @@ import ( v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" v1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) @@ -52,9 +53,11 @@ func BenchmarkPodsMetrics(b *testing.B) { func TestNodesMetrics(t *testing.T) { m := NewMetricsServer(nil) - nodes := Collection{ - makeNode("n1", "32", "128Gi", "50m", "2Mi"), - makeNode("n2", "8", "4Gi", "50m", "2Mi"), + nodes := v1.NodeList{ + Items: []v1.Node{ + makeNode("n1", "32", "128Gi", "50m", "2Mi"), + makeNode("n2", "8", "4Gi", "50m", "10Mi"), + }, } metrics := v1beta1.NodeMetricsList{ @@ -65,7 +68,7 @@ func TestNodesMetrics(t *testing.T) { } mmx := make(NodesMetrics) - m.NodesMetrics(nodes, &metrics, mmx) + m.NodesMetrics(&nodes, &metrics, mmx) assert.Equal(t, 2, len(mmx)) mx, ok := mmx["n1"] assert.True(t, ok) @@ -78,9 +81,11 @@ func TestNodesMetrics(t *testing.T) { } func BenchmarkNodesMetrics(b *testing.B) { - nodes := Collection{ - makeNode("n1", "100m", "4Mi", "50m", "2Mi"), - makeNode("n2", "100m", "4Mi", "50m", "2Mi"), + nodes := v1.NodeList{ + Items: []v1.Node{ + makeNode("n1", "100m", "4Mi", "100m", "2Mi"), + makeNode("n2", "100m", "4Mi", "100m", "2Mi"), + }, } metrics := v1beta1.NodeMetricsList{ @@ -96,38 +101,46 @@ func BenchmarkNodesMetrics(b *testing.B) { b.ResetTimer() b.ReportAllocs() for n := 0; n < b.N; n++ { - m.NodesMetrics(nodes, &metrics, mmx) + m.NodesMetrics(&nodes, &metrics, mmx) } } func TestClusterLoad(t *testing.T) { m := NewMetricsServer(nil) - nodes := Collection{ - makeNode("n1", "100m", "4Mi", "50m", "2Mi"), - makeNode("n2", "100m", "4Mi", "50m", "2Mi"), + nodes := v1.NodeList{ + Items: []v1.Node{ + makeNode("n1", "100m", "4Mi", "50m", "2Mi"), + makeNode("n2", "100m", "4Mi", "50m", "2Mi"), + }, } - metrics := Collection{ - makeMxNode("n1", "50m", "1Mi"), - makeMxNode("n2", "50m", "1Mi"), + metrics := mv1beta1.NodeMetricsList{ + Items: []mv1beta1.NodeMetrics{ + *makeMxNode("n1", "50m", "1Mi"), + *makeMxNode("n2", "50m", "1Mi"), + }, } var mx ClusterMetrics - m.ClusterLoad(nodes, metrics, &mx) + m.ClusterLoad(&nodes, &metrics, &mx) assert.Equal(t, 100.0, mx.PercCPU) assert.Equal(t, 50.0, mx.PercMEM) } func BenchmarkClusterLoad(b *testing.B) { - nodes := Collection{ - makeNode("n1", "100m", "4Mi", "50m", "2Mi"), - makeNode("n2", "100m", "4Mi", "50m", "2Mi"), + nodes := v1.NodeList{ + Items: []v1.Node{ + makeNode("n1", "100m", "4Mi", "50m", "2Mi"), + makeNode("n2", "100m", "4Mi", "50m", "2Mi"), + }, } - metrics := Collection{ - makeMxNode("n1", "50m", "1Mi"), - makeMxNode("n2", "50m", "1Mi"), + metrics := mv1beta1.NodeMetricsList{ + Items: []mv1beta1.NodeMetrics{ + *makeMxNode("n1", "50m", "1Mi"), + *makeMxNode("n2", "50m", "1Mi"), + }, } m := NewMetricsServer(nil) @@ -135,7 +148,7 @@ func BenchmarkClusterLoad(b *testing.B) { b.ResetTimer() b.ReportAllocs() for n := 0; n < b.N; n++ { - m.ClusterLoad(nodes, metrics, &mx) + m.ClusterLoad(&nodes, &metrics, &mx) } } @@ -156,8 +169,8 @@ func makeMxPod(name, cpu, mem string) *v1beta1.PodMetrics { } } -func makeNode(name, tcpu, tmem, acpu, amem string) *v1.Node { - return &v1.Node{ +func makeNode(name, tcpu, tmem, acpu, amem string) v1.Node { + return v1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: name, }, diff --git a/internal/color/colorize_test.go b/internal/color/colorize_test.go index 126f54d1..41f829f3 100644 --- a/internal/color/colorize_test.go +++ b/internal/color/colorize_test.go @@ -17,7 +17,8 @@ func TestColorize(t *testing.T) { "default": {"blee", 0, "\x1b[37mblee\x1b[0m"}, } - for k, u := range uu { + for k := range uu { + u := uu[k] t.Run(k, func(t *testing.T) { assert.Equal(t, u.e, Colorize(u.s, u.c)) }) diff --git a/internal/config/alias.go b/internal/config/alias.go index aa7df636..bc1dcbd2 100644 --- a/internal/config/alias.go +++ b/internal/config/alias.go @@ -14,6 +14,9 @@ var K9sAlias = filepath.Join(K9sHome, "alias.yml") // Alias tracks shortname to GVR mappings. type Alias map[string]string +// ShortNames represents a collection of shortnames for aliases. +type ShortNames map[string][]string + // Aliases represents a collection of aliases. type Aliases struct { Alias Alias `yaml:"alias"` @@ -21,54 +24,64 @@ type Aliases struct { // NewAliases return a new alias. func NewAliases() Aliases { - aa := Aliases{Alias: make(Alias, 50)} - aa.loadDefaults() - return aa + return Aliases{ + Alias: make(Alias, 50), + } } func (a Aliases) loadDefaults() { + const ( + contexts = "contexts" + portFwds = "portforwards" + benchmarks = "benchmarks" + dumps = "screendumps" + groups = "groups" + users = "users" + ) + 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["rob"] = "rbac.authorization.k8s.io/v1/rolebindings" + a.Alias["rb"] = "rbac.authorization.k8s.io/v1/rolebindings" a.Alias["np"] = "networking.k8s.io/v1/networkpolicies" { - a.Alias["ctx"] = "contexts" - a.Alias["contexts"] = "contexts" - a.Alias["context"] = "contexts" + a.Alias["ctx"] = contexts + a.Alias[contexts] = contexts + a.Alias["context"] = contexts } { - a.Alias["usr"] = "users" - a.Alias["users"] = "users" - a.Alias["user"] = "user" + a.Alias["usr"] = users + a.Alias[users] = users + a.Alias["user"] = users } { - a.Alias["grp"] = "groups" - a.Alias["group"] = "groups" - a.Alias["groups"] = "groups" + a.Alias["grp"] = groups + a.Alias["group"] = groups + a.Alias[groups] = groups } { - a.Alias["pf"] = "portforwards" - a.Alias["portforwards"] = "portforwards" - a.Alias["portforward"] = "portforwards" + a.Alias["pf"] = portFwds + a.Alias[portFwds] = portFwds + a.Alias["portforward"] = portFwds } { - a.Alias["be"] = "benchmarks" - a.Alias["benchmark"] = "benchmarks" - a.Alias["benchmarks"] = "benchmarks" + a.Alias["be"] = benchmarks + a.Alias["benchmark"] = benchmarks + a.Alias[benchmarks] = benchmarks } { - a.Alias["sd"] = "screendumps" - a.Alias["screendump"] = "screendumps" - a.Alias["screendumps"] = "screendumps" + a.Alias["sd"] = dumps + a.Alias["screendump"] = dumps + a.Alias[dumps] = dumps } } // Load K9s aliases. func (a Aliases) Load() error { + a.loadDefaults() return a.LoadAliases(K9sAlias) } @@ -79,20 +92,21 @@ func (a Aliases) Get(k string) (string, bool) { } // Define declares a new alias. -func (a Aliases) Define(command, alias string) { - if _, ok := a.Alias[alias]; ok { - // Don't override aliases. Take order of alias registration as precedence. - return +func (a Aliases) Define(gvr string, aliases ...string) { + for _, alias := range aliases { + if _, ok := a.Alias[alias]; ok { + continue + } + a.Alias[alias] = gvr } - - a.Alias[alias] = command } // LoadAliases loads alias from a given file. func (a Aliases) LoadAliases(path string) error { f, err := ioutil.ReadFile(path) if err != nil { - return err + log.Warn().Err(err).Msgf("No custom aliases found") + return nil } var aa Aliases diff --git a/internal/config/alias_test.go b/internal/config/alias_test.go index 94a14e4b..80416d70 100644 --- a/internal/config/alias_test.go +++ b/internal/config/alias_test.go @@ -13,7 +13,7 @@ func TestAliasDefine(t *testing.T) { aliases []string } - tts := []struct { + uu := []struct { name string aliases []aliasDef registeredCommands map[string]string @@ -51,15 +51,16 @@ func TestAliasDefine(t *testing.T) { }, } - for _, tt := range tts { - t.Run(tt.name, func(t *testing.T) { + for i := range uu { + u := uu[i] + t.Run(u.name, func(t *testing.T) { configAlias := config.NewAliases() - for _, aliases := range tt.aliases { + for _, aliases := range u.aliases { for _, a := range aliases.aliases { configAlias.Define(aliases.cmd, a) } } - for alias, cmd := range tt.registeredCommands { + for alias, cmd := range u.registeredCommands { v, ok := configAlias.Get(alias) assert.True(t, ok) assert.Equal(t, cmd, v, "Wrong command for alias "+alias) @@ -70,18 +71,17 @@ func TestAliasDefine(t *testing.T) { func TestAliasesLoad(t *testing.T) { a := config.NewAliases() - assert.Nil(t, a.LoadAliases("test_assets/alias.yml")) - assert.Equal(t, 27, len(a.Alias)) + assert.Nil(t, a.LoadAliases("test_assets/alias.yml")) + assert.Equal(t, 2, len(a.Alias)) } func TestAliasesSave(t *testing.T) { a := config.NewAliases() - a.Alias["test"] = "fred" a.Alias["blee"] = "duh" - a.SaveAliases("/tmp/a.yml") + assert.Nil(t, a.SaveAliases("/tmp/a.yml")) assert.Nil(t, a.LoadAliases("/tmp/a.yml")) - assert.Equal(t, 28, len(a.Alias)) + assert.Equal(t, 2, len(a.Alias)) } diff --git a/internal/config/bench_test.go b/internal/config/bench_test.go index bd15f659..8f9aad1e 100644 --- a/internal/config/bench_test.go +++ b/internal/config/bench_test.go @@ -16,7 +16,8 @@ func TestBenchEmpty(t *testing.T) { "notEmpty": {newBenchmark(), false}, } - for k, u := range uu { + for k := range uu { + u := uu[k] t.Run(k, func(t *testing.T) { assert.Equal(t, u.e, u.b.empty()) }) @@ -46,7 +47,8 @@ func TestBenchLoad(t *testing.T) { }, } - for k, u := range uu { + for k := range uu { + u := uu[k] t.Run(k, func(t *testing.T) { b, err := NewBench(u.file) @@ -95,7 +97,8 @@ func TestBenchServiceLoad(t *testing.T) { }, } - for k, u := range uu { + for k := range uu { + u := uu[k] t.Run(k, func(t *testing.T) { b, err := NewBench("test_assets/b_good.yml") @@ -165,7 +168,8 @@ func TestBenchContainerLoad(t *testing.T) { }, } - for k, u := range uu { + for k := range uu { + u := uu[k] t.Run(k, func(t *testing.T) { b, err := NewBench("test_assets/b_containers.yml") diff --git a/internal/config/cluster.go b/internal/config/cluster.go index 2cf83dd2..d056e9b8 100644 --- a/internal/config/cluster.go +++ b/internal/config/cluster.go @@ -1,5 +1,7 @@ package config +import "github.com/derailed/k9s/internal/client" + // Cluster tracks K9s cluster configuration. type Cluster struct { Namespace *Namespace `yaml:"namespace"` @@ -12,7 +14,7 @@ func NewCluster() *Cluster { } // Validate a cluster config. -func (c *Cluster) Validate(conn Connection, ks KubeSettings) { +func (c *Cluster) Validate(conn client.Connection, ks KubeSettings) { if c.Namespace == nil { c.Namespace = NewNamespace() } diff --git a/internal/config/config.go b/internal/config/config.go index 8a6a9c22..b52cd1d6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,8 +1,5 @@ package config -// BOZO!! Once yaml is stable implement validation -// go get gopkg.in/validator.v2 - import ( "errors" "fmt" @@ -10,8 +7,7 @@ import ( "os" "path/filepath" - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" + "github.com/derailed/k9s/internal/client" "github.com/rs/zerolog/log" "gopkg.in/yaml.v2" v1 "k8s.io/api/core/v1" @@ -30,9 +26,6 @@ var ( ) type ( - // Connection represents a kubernetes api server connection. - Connection k8s.Connection - // KubeSettings exposes kubeconfig context information. KubeSettings interface { // CurrentContextName returns the name of the current context. @@ -54,7 +47,7 @@ type ( // Config tracks K9s configuration options. Config struct { K9s *K9s `yaml:"k9s"` - client Connection + client client.Connection settings KubeSettings } ) @@ -84,7 +77,9 @@ func (c *Config) Refine(flags *genericclioptions.ConfigFlags) error { } c.K9s.CurrentCluster = ctx.Cluster if len(ctx.Namespace) != 0 { - c.SetActiveNamespace(ctx.Namespace) + if err := c.SetActiveNamespace(ctx.Namespace); err != nil { + return err + } } if isSet(flags.ClusterName) { @@ -92,7 +87,9 @@ func (c *Config) Refine(flags *genericclioptions.ConfigFlags) error { } if isSet(flags.Namespace) { - c.SetActiveNamespace(*flags.Namespace) + if err := c.SetActiveNamespace(*flags.Namespace); err != nil { + return err + } } return nil @@ -119,7 +116,7 @@ func (c *Config) ActiveNamespace() string { return cl.Namespace.Active } } - return resource.DefaultNamespace + return "default" } // FavNamespaces returns fav namespaces in the current cluster. @@ -165,12 +162,12 @@ func (c *Config) SetActiveView(view string) { } // GetConnection return an api server connection. -func (c *Config) GetConnection() Connection { +func (c *Config) GetConnection() client.Connection { return c.client } // SetConnection set an api server connection. -func (c *Config) SetConnection(conn Connection) { +func (c *Config) SetConnection(conn client.Connection) { c.client = conn } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index e5763750..7a2d078c 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -54,7 +54,8 @@ func TestConfigRefine(t *testing.T) { }, } - for k, u := range uu { + for k := range uu { + u := uu[k] t.Run(k, func(t *testing.T) { mc := NewMockConnection() m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) @@ -142,7 +143,7 @@ func TestConfigSetActiveNamespace(t *testing.T) { cfg := config.NewConfig(mk) assert.Nil(t, cfg.Load("test_assets/k9s.yml")) - cfg.SetActiveNamespace("default") + assert.Nil(t, cfg.SetActiveNamespace("default")) assert.Equal(t, "default", cfg.ActiveNamespace()) } @@ -202,7 +203,7 @@ func TestConfigSaveFile(t *testing.T) { cfg := config.NewConfig(mk) cfg.SetConnection(mc) - cfg.Load("test_assets/k9s.yml") + assert.Nil(t, cfg.Load("test_assets/k9s.yml")) cfg.K9s.RefreshRate = 100 cfg.K9s.LogBufferSize = 500 cfg.K9s.LogRequestSize = 100 @@ -231,7 +232,7 @@ func TestConfigReset(t *testing.T) { cfg := config.NewConfig(mk) cfg.SetConnection(mc) - cfg.Load("test_assets/k9s.yml") + assert.Nil(t, cfg.Load("test_assets/k9s.yml")) cfg.Reset() cfg.Validate() diff --git a/internal/config/helpers.go b/internal/config/helpers.go index 91faf1ff..1e6449dc 100644 --- a/internal/config/helpers.go +++ b/internal/config/helpers.go @@ -40,7 +40,7 @@ func InNSList(nn []interface{}, ns string) bool { func mustK9sHome() string { usr, err := user.Current() if err != nil { - panic(err) + log.Fatal().Err(err).Msg("Die on retriving user home") } return usr.HomeDir } @@ -49,7 +49,7 @@ func mustK9sHome() string { func MustK9sUser() string { usr, err := user.Current() if err != nil { - panic(err) + log.Fatal().Err(err).Msg("Die on retriving user info") } return usr.Username } @@ -59,8 +59,7 @@ func EnsurePath(path string, mod os.FileMode) { dir := filepath.Dir(path) if _, err := os.Stat(dir); os.IsNotExist(err) { if err = os.MkdirAll(dir, mod); err != nil { - log.Error().Msgf("Unable to create K9s home config dir: %v", err) - panic(err) + log.Fatal().Msgf("Unable to create K9s home config dir: %v", err) } } } diff --git a/internal/config/hotkey.go b/internal/config/hotkey.go new file mode 100644 index 00000000..e0817f1f --- /dev/null +++ b/internal/config/hotkey.go @@ -0,0 +1,53 @@ +package config + +import ( + "io/ioutil" + "path/filepath" + + "gopkg.in/yaml.v2" +) + +// K9sHotKeys manages K9s hotKeys. +var K9sHotKeys = filepath.Join(K9sHome, "hotkey.yml") + +// HotKeys represents a collection of plugins. +type HotKeys struct { + HotKey map[string]HotKey `yaml:"hotKey"` +} + +// HotKey describes a K9s hotkey. +type HotKey struct { + ShortCut string `yaml:"shortCut"` + Description string `yaml:"description"` + Command string `yaml:"command"` +} + +// NewHotKeys returns a new plugin. +func NewHotKeys() HotKeys { + return HotKeys{ + HotKey: make(map[string]HotKey), + } +} + +// Load K9s plugins. +func (h HotKeys) Load() error { + return h.LoadHotKeys(K9sHotKeys) +} + +// LoadHotKeys loads plugins from a given file. +func (h HotKeys) LoadHotKeys(path string) error { + f, err := ioutil.ReadFile(path) + if err != nil { + return err + } + + var hh HotKeys + if err := yaml.Unmarshal(f, &hh); err != nil { + return err + } + for k, v := range hh.HotKey { + h.HotKey[k] = v + } + + return nil +} diff --git a/internal/config/hotkey_test.go b/internal/config/hotkey_test.go new file mode 100644 index 00000000..38f8242b --- /dev/null +++ b/internal/config/hotkey_test.go @@ -0,0 +1,21 @@ +package config_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/stretchr/testify/assert" +) + +func TestHotKeyLoad(t *testing.T) { + h := config.NewHotKeys() + assert.Nil(t, h.LoadHotKeys("test_assets/hot_key.yml")) + + assert.Equal(t, 1, len(h.HotKey)) + + k, ok := h.HotKey["pods"] + assert.True(t, ok) + assert.Equal(t, "shift-0", k.ShortCut) + assert.Equal(t, "Launch pod view", k.Description) + assert.Equal(t, "pods", k.Command) +} diff --git a/internal/config/k9s.go b/internal/config/k9s.go index 66cb4f4e..7ec28426 100644 --- a/internal/config/k9s.go +++ b/internal/config/k9s.go @@ -1,8 +1,6 @@ package config -import ( - "github.com/derailed/k9s/internal/k8s" -) +import "github.com/derailed/k9s/internal/client" const ( defaultRefreshRate = 2 @@ -114,7 +112,7 @@ func (k *K9s) checkClusters(ks KubeSettings) { } // Validate the current configuration. -func (k *K9s) Validate(c k8s.Connection, ks KubeSettings) { +func (k *K9s) Validate(c client.Connection, ks KubeSettings) { k.validateDefaults() if k.Clusters == nil { diff --git a/internal/config/mock_connection_test.go b/internal/config/mock_connection_test.go index 66672945..5f2c4271 100644 --- a/internal/config/mock_connection_test.go +++ b/internal/config/mock_connection_test.go @@ -1,10 +1,10 @@ // Code generated by pegomock. DO NOT EDIT. -// Source: github.com/derailed/k9s/internal/config (interfaces: Connection) +// Source: github.com/derailed/k9s/internal/client (interfaces: Connection) package config_test import ( - k8s "github.com/derailed/k9s/internal/k8s" + client "github.com/derailed/k9s/internal/client" pegomock "github.com/petergtz/pegomock" v1 "k8s.io/api/core/v1" version "k8s.io/apimachinery/pkg/version" @@ -51,12 +51,12 @@ func (mock *MockConnection) CachedDiscovery() (*disk.CachedDiscoveryClient, erro return ret0, ret1 } -func (mock *MockConnection) CanIAccess(_param0 string, _param1 string, _param2 []string) (bool, error) { +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("CanIAccess", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + 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 { @@ -70,46 +70,16 @@ func (mock *MockConnection) CanIAccess(_param0 string, _param1 string, _param2 [ return ret0, ret1 } -func (mock *MockConnection) CheckListNSAccess() error { +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("CheckListNSAccess", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 error + 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].(error) - } - } - return ret0 -} - -func (mock *MockConnection) CheckNSAccess(_param0 string) error { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("CheckNSAccess", 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) Config() *k8s.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((**k8s.Config)(nil)).Elem()}) - var ret0 *k8s.Config - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*k8s.Config) + ret0 = result[0].(*client.Config) } } return ret0 @@ -419,23 +389,23 @@ func (c *MockConnection_CachedDiscovery_OngoingVerification) GetCapturedArgument func (c *MockConnection_CachedDiscovery_OngoingVerification) GetAllCapturedArguments() { } -func (verifier *VerifierMockConnection) CanIAccess(_param0 string, _param1 string, _param2 []string) *MockConnection_CanIAccess_OngoingVerification { +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, "CanIAccess", params, verifier.timeout) - return &MockConnection_CanIAccess_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} + 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_CanIAccess_OngoingVerification struct { +type MockConnection_CanI_OngoingVerification struct { mock *MockConnection methodInvocations []pegomock.MethodInvocation } -func (c *MockConnection_CanIAccess_OngoingVerification) GetCapturedArguments() (string, string, []string) { +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_CanIAccess_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string, _param2 [][]string) { +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(params[0])) @@ -454,50 +424,6 @@ func (c *MockConnection_CanIAccess_OngoingVerification) GetAllCapturedArguments( return } -func (verifier *VerifierMockConnection) CheckListNSAccess() *MockConnection_CheckListNSAccess_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CheckListNSAccess", params, verifier.timeout) - return &MockConnection_CheckListNSAccess_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_CheckListNSAccess_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_CheckListNSAccess_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_CheckListNSAccess_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) CheckNSAccess(_param0 string) *MockConnection_CheckNSAccess_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CheckNSAccess", params, verifier.timeout) - return &MockConnection_CheckNSAccess_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_CheckNSAccess_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_CheckNSAccess_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockConnection_CheckNSAccess_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 *VerifierMockConnection) Config() *MockConnection_Config_OngoingVerification { params := []pegomock.Param{} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Config", params, verifier.timeout) diff --git a/internal/config/ns.go b/internal/config/ns.go index 2415b1b3..10ee1b0f 100644 --- a/internal/config/ns.go +++ b/internal/config/ns.go @@ -1,12 +1,13 @@ package config import ( + "github.com/derailed/k9s/internal/client" "github.com/rs/zerolog/log" ) const ( // MaxFavoritesNS number # favorite namespaces to keep in the configuration. - MaxFavoritesNS = 10 + MaxFavoritesNS = 9 defaultNS = "default" allNS = "all" ) @@ -26,7 +27,7 @@ func NewNamespace() *Namespace { } // Validate a namespace is setup correctly -func (n *Namespace) Validate(c Connection, ks KubeSettings) { +func (n *Namespace) Validate(c client.Connection, ks KubeSettings) { nns, err := c.ValidNamespaces() if err != nil { return diff --git a/internal/config/plugin_test.go b/internal/config/plugin_test.go index 3b5148e1..7a81412b 100644 --- a/internal/config/plugin_test.go +++ b/internal/config/plugin_test.go @@ -12,4 +12,11 @@ func TestPluginLoad(t *testing.T) { assert.Nil(t, p.LoadPlugins("test_assets/plugin.yml")) assert.Equal(t, 1, len(p.Plugin)) + k, ok := p.Plugin["blah"] + assert.True(t, ok) + assert.Equal(t, "shift-s", k.ShortCut) + assert.Equal(t, "blee", k.Description) + assert.Equal(t, []string{"po", "dp"}, k.Scopes) + assert.Equal(t, "duh", k.Command) + assert.Equal(t, []string{"-n", "$NAMESPACE", "-boolean"}, k.Args) } diff --git a/internal/config/style.go b/internal/config/style.go index 63c40add..2d1186ec 100644 --- a/internal/config/style.go +++ b/internal/config/style.go @@ -14,10 +14,15 @@ var ( K9sStylesFile = filepath.Join(K9sHome, "skin.yml") ) +type StyleListener interface { + StylesChanged(*Styles) +} + type ( // Styles tracks K9s styling options. Styles struct { - K9s Style `yaml:"k9s"` + K9s Style `yaml:"k9s"` + listeners []StyleListener } // Body tracks body styles. @@ -132,7 +137,7 @@ func newStyle() Style { Body: newBody(), Frame: newFrame(), Info: newInfo(), - Table: newTable(), + Table: newGetTable(), Views: newViews(), } } @@ -211,12 +216,12 @@ func newInfo() Info { } // NewTable returns a new table style. -func newTable() Table { +func newGetTable() Table { return Table{ FgColor: "aqua", BgColor: "black", CursorColor: "aqua", - MarkColor: "darkgoldenrod", + MarkColor: "violet", Header: newTableHeader(), } } @@ -257,9 +262,10 @@ func newMenu() Menu { } // NewStyles creates a new default config. -func NewStyles(path string) (*Styles, error) { - s := &Styles{K9s: newStyle()} - return s, s.load(path) +func NewStyles() *Styles { + return &Styles{ + K9s: newStyle(), + } } // FgColor returns the foreground color. @@ -272,6 +278,30 @@ func (s *Styles) BgColor() tcell.Color { return AsColor(s.Body().BgColor) } +func (s *Styles) AddListener(l StyleListener) { + s.listeners = append(s.listeners, l) +} + +func (s *Styles) RemoveListener(l StyleListener) { + victim := -1 + for i, lis := range s.listeners { + if lis == l { + victim = i + break + } + } + if victim == -1 { + return + } + s.listeners = append(s.listeners[:victim], s.listeners[victim+1:]...) +} + +func (s *Styles) fireStylesChanged() { + for _, list := range s.listeners { + list.StylesChanged(s) + } +} + // Body returns body styles. func (s *Styles) Body() Body { return s.K9s.Body @@ -293,7 +323,7 @@ func (s *Styles) Title() Title { } // Table returns table styles. -func (s *Styles) Table() Table { +func (s *Styles) GetTable() Table { return s.K9s.Table } @@ -303,7 +333,7 @@ func (s *Styles) Views() Views { } // Load K9s configuration from file -func (s *Styles) load(path string) error { +func (s *Styles) Load(path string) error { f, err := ioutil.ReadFile(path) if err != nil { return err @@ -312,6 +342,7 @@ func (s *Styles) load(path string) error { if err := yaml.Unmarshal(f, s); err != nil { return err } + s.fireStylesChanged() return nil } @@ -327,10 +358,9 @@ func (s *Styles) Update() { // AsColor checks color index, if match return color otherwise pink it is. func AsColor(c string) tcell.Color { - // Use tcell.GetColor to support hex codes. - // "Creates a Color from a color name (W3C name). A hex value may be supplied as a string in the format "#ffffff"." - if color := tcell.GetColor(c); color != -1 { + if color, ok := tcell.ColorNames[c]; ok { return color } - return tcell.ColorPink + + return tcell.GetColor(c) } diff --git a/internal/config/style_test.go b/internal/config/style_test.go index 45fb2a56..56c2d78d 100644 --- a/internal/config/style_test.go +++ b/internal/config/style_test.go @@ -1,51 +1,62 @@ -package config +package config_test import ( "testing" + "github.com/derailed/k9s/internal/config" "github.com/derailed/tview" "github.com/gdamore/tcell" "github.com/stretchr/testify/assert" ) -func TestSkinNone(t *testing.T) { - s, err := NewStyles("test_assets/empty_skin.yml") - assert.Nil(t, err) +func TestAsColor(t *testing.T) { + uu := map[string]tcell.Color{ + "blah": tcell.ColorDefault, + "blue": tcell.ColorBlue, + "#ffffff": tcell.NewHexColor(33554431), + "#ff0000": tcell.NewHexColor(33488896), + } + for k := range uu { + c, u := k, uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u, config.AsColor(c)) + }) + } +} + +func TestSkinNone(t *testing.T) { + s := config.NewStyles() + assert.Nil(t, s.Load("test_assets/empty_skin.yml")) s.Update() assert.Equal(t, "cadetblue", s.Body().FgColor) assert.Equal(t, "black", s.Body().BgColor) - assert.Equal(t, "black", s.Table().BgColor) + assert.Equal(t, "black", s.GetTable().BgColor) assert.Equal(t, tcell.ColorCadetBlue, s.FgColor()) assert.Equal(t, tcell.ColorBlack, s.BgColor()) assert.Equal(t, tcell.ColorBlack, tview.Styles.PrimitiveBackgroundColor) - assert.Equal(t, tcell.ColorPink, AsColor("blah")) - assert.Equal(t, tcell.ColorWhite, AsColor("white")) } func TestSkin(t *testing.T) { - s, err := NewStyles("test_assets/black_and_wtf.yml") - assert.Nil(t, err) - + s := config.NewStyles() + assert.Nil(t, s.Load("test_assets/black_and_wtf.yml")) s.Update() assert.Equal(t, "white", s.Body().FgColor) assert.Equal(t, "black", s.Body().BgColor) - assert.Equal(t, "black", s.Table().BgColor) + assert.Equal(t, "black", s.GetTable().BgColor) assert.Equal(t, tcell.ColorWhite, s.FgColor()) assert.Equal(t, tcell.ColorBlack, s.BgColor()) assert.Equal(t, tcell.ColorBlack, tview.Styles.PrimitiveBackgroundColor) - assert.Equal(t, tcell.ColorPink, AsColor("blah")) - assert.Equal(t, tcell.ColorWhite, AsColor("white")) } func TestSkinNotExits(t *testing.T) { - _, err := NewStyles("test_assets/blee.yml") - assert.NotNil(t, err) + s := config.NewStyles() + assert.NotNil(t, s.Load("test_assets/blee.yml")) } func TestSkinBoarked(t *testing.T) { - _, err := NewStyles("test_assets/skin_boarked.yml") - assert.NotNil(t, err) + s := config.NewStyles() + assert.NotNil(t, s.Load("test_assets/skin_boarked.yml")) } diff --git a/internal/config/test_assets/bench-fred.yml b/internal/config/test_assets/bench-fred.yml new file mode 100644 index 00000000..d418453c --- /dev/null +++ b/internal/config/test_assets/bench-fred.yml @@ -0,0 +1,41 @@ +benchmarks: + defaults: + concurrency: 2 + requests: 1000 + services: + default/nginx: + concurrency: 2 + requests: 1000 + http: + method: GET + http2: true + host: 10.10.10.10 + path: / + body: |- + {"fred": "blee"} + headers: + Accept: + - text/html + Content-Type: + - application/json + auth: + user: "fred" + password: "blee" + blee/fred: + concurrency: 10 + requests: 1500 + http: + method: POST + http2: false + host: 20.20.20.20 + path: /zorg + body: |- + {"fred": "blee"} + headers: + Accept: + - text/html + Content-Type: + - application/json + auth: + user: "fred" + password: "blee" diff --git a/internal/config/test_assets/hot_key.yml b/internal/config/test_assets/hot_key.yml new file mode 100644 index 00000000..81f16319 --- /dev/null +++ b/internal/config/test_assets/hot_key.yml @@ -0,0 +1,5 @@ +hotKey: + pods: + shortCut: shift-0 + description: Launch pod view + command: pods diff --git a/internal/dao/alias.go b/internal/dao/alias.go new file mode 100644 index 00000000..3a9ae707 --- /dev/null +++ b/internal/dao/alias.go @@ -0,0 +1,64 @@ +package dao + +import ( + "strings" + + "github.com/derailed/k9s/internal/config" +) + +// Alias tracks standard and custom command aliases. +type Alias struct { + config.Aliases + factory Factory +} + +// NewAlias returns a new set of aliases. +func NewAlias(f Factory) *Alias { + return &Alias{ + Aliases: config.NewAliases(), + factory: f, + } +} + +// ClearAliases remove all aliases. +func (a *Alias) Clear() { + for k := range a.Alias { + delete(a.Alias, k) + } +} + +// Ensure makes sure alias are loaded. +func (a *Alias) Ensure() (config.Alias, error) { + if len(a.Alias) == 0 { + if err := LoadResources(a.factory); err != nil { + return config.Alias{}, err + } + return a.Alias, a.load() + } + return a.Alias, nil +} + +func (a *Alias) load() error { + if err := a.Load(); err != nil { + return err + } + + for _, gvr := range AllGVRs() { + meta, err := MetaFor(gvr) + if err != nil { + return err + } + if _, ok := a.Alias[meta.Kind]; ok { + continue + } + a.Define(string(gvr), strings.ToLower(meta.Kind), meta.Name) + if meta.SingularName != "" { + a.Define(string(gvr), meta.SingularName) + } + if meta.ShortNames != nil { + a.Define(string(gvr), meta.ShortNames...) + } + } + + return nil +} diff --git a/internal/dao/assets/config b/internal/dao/assets/config new file mode 100644 index 00000000..5541a687 --- /dev/null +++ b/internal/dao/assets/config @@ -0,0 +1,43 @@ +apiVersion: v1 +kind: Config +preferences: {} +clusters: +- cluster: + insecure-skip-tls-verify: true + server: https://localhost:3000 + name: fred +- cluster: + insecure-skip-tls-verify: true + server: https://localhost:3001 + name: blee +- cluster: + insecure-skip-tls-verify: true + server: https://localhost:3002 + name: duh +contexts: +- context: + cluster: fred + user: fred + name: fred +- context: + cluster: blee + user: blee + name: blee +- context: + cluster: duh + user: duh + name: duh +current-context: fred +users: +- name: fred + user: + client-certificate-data: ZnJlZA== + client-key-data: ZnJlZA== +- name: blee + user: + client-certificate-data: ZnJlZA== + client-key-data: ZnJlZA== +- name: duh + user: + client-certificate-data: ZnJlZA== + client-key-data: ZnJlZA== diff --git a/internal/dao/assets/config.1 b/internal/dao/assets/config.1 new file mode 100644 index 00000000..9c2ff1e3 --- /dev/null +++ b/internal/dao/assets/config.1 @@ -0,0 +1,39 @@ +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: duh +- cluster: + insecure-skip-tls-verify: true + server: https://localhost:3000 + name: fred +contexts: +- context: + cluster: blee + user: blee + name: blee +- context: + cluster: duh + user: duh + name: duh +current-context: fred +kind: Config +preferences: {} +users: +- name: blee + user: + client-certificate-data: ZnJlZA== + client-key-data: ZnJlZA== +- name: duh + user: + client-certificate-data: ZnJlZA== + client-key-data: ZnJlZA== +- name: fred + user: + client-certificate-data: ZnJlZA== + client-key-data: ZnJlZA== diff --git a/internal/dao/benchmark.go b/internal/dao/benchmark.go new file mode 100644 index 00000000..c547678e --- /dev/null +++ b/internal/dao/benchmark.go @@ -0,0 +1,18 @@ +package dao + +import ( + "os" +) + +// Benchmark represents a benchmark resource. +type Benchmark struct { + Generic +} + +var _ Accessor = &Benchmark{} +var _ Nuker = &Benchmark{} + +// Delete a Benchmark. +func (d *Benchmark) Delete(path string, cascade, force bool) error { + return os.Remove(path) +} diff --git a/internal/dao/container.go b/internal/dao/container.go new file mode 100644 index 00000000..aca7aefe --- /dev/null +++ b/internal/dao/container.go @@ -0,0 +1,47 @@ +package dao + +import ( + "context" + "errors" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/watch" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + restclient "k8s.io/client-go/rest" +) + +type Container struct { + Generic +} + +var _ Accessor = &Container{} +var _ Loggable = &Container{} + +// Logs tails a given container logs +func (c *Container) TailLogs(ctx context.Context, logChan chan<- string, opts LogOptions) error { + fac, ok := ctx.Value(internal.KeyFactory).(*watch.Factory) + if !ok { + return errors.New("Expecting an informer") + } + o, err := fac.Get("v1/pods", opts.Path, labels.Everything()) + if err != nil { + return err + } + + var po v1.Pod + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po); err != nil { + return err + } + + return tailLogs(ctx, c, logChan, opts) +} + +// Logs fetch container logs for a given pod and container. +func (c *Container) Logs(path string, opts *v1.PodLogOptions) *restclient.Request { + ns, n := client.Namespaced(path) + return c.Client().DialOrDie().CoreV1().Pods(ns).GetLogs(n, opts) +} diff --git a/internal/dao/context.go b/internal/dao/context.go new file mode 100644 index 00000000..28877ad5 --- /dev/null +++ b/internal/dao/context.go @@ -0,0 +1,121 @@ +package dao + +import ( + "fmt" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" + "github.com/rs/zerolog/log" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/clientcmd" +) + +type Context struct { + Generic +} + +var _ Accessor = &Context{} +var _ Switchable = &Context{} + +func (c *Context) config() *client.Config { + return c.Factory.Client().Config() +} + +// Get a Context. +func (c *Context) Get(_, n string) (runtime.Object, error) { + ctx, err := c.config().GetContext(n) + if err != nil { + return nil, err + } + return &render.NamedContext{Name: n, Context: ctx}, nil +} + +// List all Contexts on the current cluster. +func (c *Context) List(string, metav1.ListOptions) ([]runtime.Object, error) { + ctxs, err := c.config().Contexts() + if err != nil { + return nil, err + } + cc := make([]runtime.Object, 0, len(ctxs)) + for k, v := range ctxs { + cc = append(cc, render.NewNamedContext(c.config(), k, v)) + } + + return cc, nil +} + +// Delete a Context. +func (c *Context) Delete(path string, cascade, force bool) error { + ctx, err := c.config().CurrentContextName() + if err != nil { + return err + } + if ctx == path { + return fmt.Errorf("trying to delete your current context %s", path) + } + + return c.config().DelContext(path) +} + +// MustCurrentContextName return the active context name. +func (c *Context) MustCurrentContextName() string { + cl, err := c.config().CurrentContextName() + if err != nil { + log.Fatal().Err(err).Msg("Fetching current context") + } + return cl +} + +// Switch to another context. +func (c *Context) Switch(ctx string) error { + c.Factory.Client().SwitchContextOrDie(ctx) + return nil +} + +// KubeUpdate modifies kubeconfig default context. +func (c *Context) KubeUpdate(n string) error { + config, err := c.config().RawConfig() + if err != nil { + return err + } + if err := c.Switch(n); err != nil { + return err + } + return clientcmd.ModifyConfig( + clientcmd.NewDefaultPathOptions(), config, true, + ) +} + +// ---------------------------------------------------------------------------- + +// // NamedContext represents a named cluster context. +// type NamedContext struct { +// Name string +// Context *api.Context +// config *client.Config +// } + +// // NewNamedContext returns a new named context. +// func NewNamedContext(c *client.Config, n string, ctx *api.Context) *NamedContext { +// return &NamedContext{Name: n, Context: ctx, config: c} +// } + +// // MustCurrentContextName return the active context name. +// func (c *NamedContext) MustCurrentContextName() string { +// cl, err := c.config.CurrentContextName() +// if err != nil { +// log.Fatal().Err(err).Msg("Fetching current context") +// } +// return cl +// } + +// // GetObjectKind returns a schema object. +// func (c *NamedContext) GetObjectKind() schema.ObjectKind { +// return nil +// } + +// // DeepCopyObject returns a container copy. +// func (c *NamedContext) DeepCopyObject() runtime.Object { +// return c +// } diff --git a/internal/dao/cronjob.go b/internal/dao/cronjob.go new file mode 100644 index 00000000..3886fb35 --- /dev/null +++ b/internal/dao/cronjob.go @@ -0,0 +1,43 @@ +package dao + +import ( + "github.com/derailed/k9s/internal/client" + batchv1 "k8s.io/api/batch/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/rand" +) + +const maxJobNameSize = 42 + +type CronJob struct { + Generic +} + +var _ Accessor = &CronJob{} +var _ Runnable = &CronJob{} + +// Run a CronJob. +func (c *CronJob) Run(path string) error { + ns, n := client.Namespaced(path) + cj, err := c.Client().DialOrDie().BatchV1beta1().CronJobs(ns).Get(n, metav1.GetOptions{}) + if err != nil { + return err + } + + var jobName = cj.Name + if len(cj.Name) >= maxJobNameSize { + jobName = cj.Name[0:maxJobNameSize] + } + + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: jobName + "-manual-" + rand.String(3), + Namespace: ns, + Labels: cj.Spec.JobTemplate.Labels, + }, + Spec: cj.Spec.JobTemplate.Spec, + } + _, err = c.Client().DialOrDie().BatchV1().Jobs(ns).Create(job) + + return err +} diff --git a/internal/dao/describe.go b/internal/dao/describe.go new file mode 100644 index 00000000..eeb2c9a4 --- /dev/null +++ b/internal/dao/describe.go @@ -0,0 +1,38 @@ +package dao + +import ( + "github.com/derailed/k9s/internal/client" + "github.com/rs/zerolog/log" + "k8s.io/kubectl/pkg/describe" + "k8s.io/kubectl/pkg/describe/versioned" +) + +func Describe(c client.Connection, gvr client.GVR, ns, n string) (string, error) { + mapper := RestMapper{Connection: c} + m, err := mapper.ToRESTMapper() + if err != nil { + log.Error().Err(err).Msgf("No REST mapper for resource %s", gvr) + return "", err + } + + GVR := client.GVR(gvr) + gvk, err := m.KindFor(GVR.AsGVR()) + if err != nil { + log.Error().Err(err).Msgf("No GVK for resource %s", gvr) + return "", err + } + + mapping, err := mapper.ResourceFor(GVR.ResName(), gvk.Kind) + if err != nil { + log.Error().Err(err).Msgf("Unable to find mapper for %s %s", gvr, n) + return "", err + } + d, err := versioned.Describer(c.Config().Flags(), mapping) + if err != nil { + log.Error().Err(err).Msgf("Unable to find describer for %#v", mapping) + return "", err + } + + log.Debug().Msgf("DESCRIBE FOR %q -- %q:%q", gvr, ns, n) + return d.Describe(ns, n, describe.DescriberSettings{ShowEvents: true}) +} diff --git a/internal/dao/dp.go b/internal/dao/dp.go new file mode 100644 index 00000000..58a35213 --- /dev/null +++ b/internal/dao/dp.go @@ -0,0 +1,80 @@ +package dao + +import ( + "context" + "errors" + "fmt" + + "github.com/derailed/k9s/internal/client" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/kubectl/pkg/polymorphichelpers" +) + +type Deployment struct { + Generic +} + +var _ Accessor = &Deployment{} +var _ Loggable = &Deployment{} +var _ Restartable = &Deployment{} +var _ Scalable = &Deployment{} + +// Scale a Deployment. +func (d *Deployment) Scale(path string, replicas int32) error { + ns, n := client.Namespaced(path) + scale, err := d.Client().DialOrDie().AppsV1().Deployments(ns).GetScale(n, metav1.GetOptions{}) + if err != nil { + return err + } + scale.Spec.Replicas = replicas + _, err = d.Client().DialOrDie().AppsV1().Deployments(ns).UpdateScale(n, scale) + + return err +} + +// Restart a Deployment rollout. +func (d *Deployment) Restart(path string) error { + o, err := d.Get(string(d.gvr), path, labels.Everything()) + if err != nil { + return err + } + + var ds appsv1.Deployment + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds) + if err != nil { + return err + } + + update, err := polymorphichelpers.ObjectRestarterFn(&ds) + if err != nil { + return err + } + + _, err = d.Client().DialOrDie().AppsV1().Deployments(ds.Namespace).Patch(ds.Name, types.StrategicMergePatchType, update) + return err +} + +// Logs tail logs for all pods represented by this Deployment. +func (d *Deployment) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error { + o, err := d.Get(string(d.gvr), opts.Path, labels.Everything()) + if err != nil { + return err + } + + var dp appsv1.Deployment + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &dp) + if err != nil { + return errors.New("expecting Deployment resource") + } + + if dp.Spec.Selector == nil || len(dp.Spec.Selector.MatchLabels) == 0 { + return fmt.Errorf("No valid selector found on Deployment %s", opts.Path) + } + + return podLogs(ctx, c, dp.Spec.Selector.MatchLabels, opts) +} diff --git a/internal/dao/ds.go b/internal/dao/ds.go new file mode 100644 index 00000000..fc52bf58 --- /dev/null +++ b/internal/dao/ds.go @@ -0,0 +1,124 @@ +package dao + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/watch" + "github.com/rs/zerolog/log" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/kubectl/pkg/polymorphichelpers" +) + +type DaemonSet struct { + Generic +} + +var _ Accessor = &DaemonSet{} +var _ Loggable = &DaemonSet{} +var _ Restartable = &DaemonSet{} + +// Restart a DaemonSet rollout. +func (d *DaemonSet) Restart(path string) error { + o, err := d.Get(string(d.gvr), path, labels.Everything()) + if err != nil { + return err + } + + var ds appsv1.DaemonSet + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds) + if err != nil { + return err + } + + update, err := polymorphichelpers.ObjectRestarterFn(&ds) + if err != nil { + return err + } + + _, err = d.Client().DialOrDie().AppsV1().DaemonSets(ds.Namespace).Patch(ds.Name, types.StrategicMergePatchType, update) + return err +} + +// Logs tail logs for all pods represented by this DaemonSet. +func (d *DaemonSet) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error { + o, err := d.Get("apps/v1/daemonsets", opts.Path, labels.Everything()) + if err != nil { + return err + } + + var ds appsv1.DaemonSet + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds) + if err != nil { + return errors.New("expecting daemonset resource") + } + + if ds.Spec.Selector == nil || len(ds.Spec.Selector.MatchLabels) == 0 { + return fmt.Errorf("no valid selector found on daemonset %q", opts.Path) + } + + return podLogs(ctx, c, ds.Spec.Selector.MatchLabels, opts) +} + +func podLogs(ctx context.Context, c chan<- string, sel map[string]string, opts LogOptions) error { + f, ok := ctx.Value(internal.KeyFactory).(*watch.Factory) + if !ok { + return errors.New("expecting a context factory") + } + ls, err := metav1.ParseToLabelSelector(toSelector(sel)) + if err != nil { + return err + } + lsel, err := metav1.LabelSelectorAsSelector(ls) + if err != nil { + return err + } + + ns, _ := client.Namespaced(opts.Path) + oo, err := f.List("v1/pods", ns, lsel) + if err != nil { + return err + } + + if len(oo) > 1 { + opts.MultiPods = true + } + + po := Pod{} + po.Init(f, "v1/pods") + for _, o := range oo { + var pod v1.Pod + err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod) + if err != nil { + return err + } + log.Debug().Msgf("TAILING logs on pod %q", pod.Name) + opts.Path = client.FQN(pod.Namespace, pod.Name) + if err := po.TailLogs(ctx, c, opts); err != nil { + return err + } + } + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func toSelector(m map[string]string) string { + s := make([]string, 0, len(m)) + for k, v := range m { + s = append(s, k+"="+v) + } + + return strings.Join(s, ",") +} diff --git a/internal/dao/generic.go b/internal/dao/generic.go new file mode 100644 index 00000000..4bf2ce43 --- /dev/null +++ b/internal/dao/generic.go @@ -0,0 +1,36 @@ +package dao + +import ( + "github.com/derailed/k9s/internal/client" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/dynamic" +) + +type Generic struct { + Factory + + gvr client.GVR +} + +func (r *Generic) Init(f Factory, gvr client.GVR) { + r.Factory, r.gvr = f, gvr +} + +// Delete a Generic. +func (g *Generic) Delete(path string, cascade, force bool) error { + p := metav1.DeletePropagationOrphan + if cascade { + p = metav1.DeletePropagationBackground + } + + ns, n := client.Namespaced(path) + opts := metav1.DeleteOptions{PropagationPolicy: &p} + if ns != "-" { + return g.dynClient().Namespace(ns).Delete(n, &opts) + } + return g.dynClient().Delete(n, &opts) +} + +func (g *Generic) dynClient() dynamic.NamespaceableResourceInterface { + return g.Client().DynDialOrDie().Resource(g.gvr.AsGVR()) +} diff --git a/internal/dao/helpers.go b/internal/dao/helpers.go new file mode 100644 index 00000000..6aa0c968 --- /dev/null +++ b/internal/dao/helpers.go @@ -0,0 +1,20 @@ +package dao + +import ( + "math" + + "github.com/derailed/tview" + runewidth "github.com/mattn/go-runewidth" +) + +func toPerc(v1, v2 float64) float64 { + if v2 == 0 { + return 0 + } + return math.Round((v1 / v2) * 100) +} + +// Truncate a string to the given l and suffix ellipsis if needed. +func Truncate(str string, width int) string { + return runewidth.Truncate(str, width, string(tview.SemigraphicsHorizontalEllipsis)) +} diff --git a/internal/k8s/helpers_test.go b/internal/dao/helpers_test.go similarity index 55% rename from internal/k8s/helpers_test.go rename to internal/dao/helpers_test.go index df159933..1f6280e8 100644 --- a/internal/k8s/helpers_test.go +++ b/internal/dao/helpers_test.go @@ -1,4 +1,4 @@ -package k8s +package dao import ( "testing" @@ -19,18 +19,3 @@ func TestToPerc(t *testing.T) { assert.Equal(t, u.e, toPerc(u.v1, u.v2)) } } - -func TestToMB(t *testing.T) { - uu := []struct { - v int64 - e float64 - }{ - {0, 0}, - {2 * megaByte, 2}, - {10 * megaByte, 10}, - } - - for _, u := range uu { - assert.Equal(t, u.e, ToMB(u.v)) - } -} diff --git a/internal/dao/job.go b/internal/dao/job.go new file mode 100644 index 00000000..5b81d7ab --- /dev/null +++ b/internal/dao/job.go @@ -0,0 +1,39 @@ +package dao + +import ( + "context" + "errors" + "fmt" + + batchv1 "k8s.io/api/batch/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" +) + +type Job struct { + Generic +} + +var _ Accessor = &Job{} +var _ Loggable = &Job{} + +// Logs tail logs for all pods represented by this Job. +func (j *Job) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error { + o, err := j.Get(string(j.gvr), opts.Path, labels.Everything()) + if err != nil { + return err + } + + var job batchv1.Job + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &job) + if err != nil { + return errors.New("expecting a job resource") + } + + if job.Spec.Selector == nil || len(job.Spec.Selector.MatchLabels) == 0 { + return fmt.Errorf("No valid selector found on Job %s", opts.Path) + } + + return podLogs(ctx, c, job.Spec.Selector.MatchLabels, opts) +} diff --git a/internal/dao/log_options.go b/internal/dao/log_options.go new file mode 100644 index 00000000..ce31c575 --- /dev/null +++ b/internal/dao/log_options.go @@ -0,0 +1,65 @@ +package dao + +import ( + "strings" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/color" +) + +// LogOptions represent logger options. +type LogOptions struct { + Path string + Container string + Lines int64 + Color color.Paint + Previous bool + SingleContainer bool + MultiPods bool +} + +// HasContainer checks if a container is present. +func (o LogOptions) HasContainer() bool { + return o.Container != "" +} + +// FixedSizeName returns a normalize fixed size pod name if possible. +func (o LogOptions) FixedSizeName() string { + _, n := client.Namespaced(o.Path) + tokens := strings.Split(n, "-") + if len(tokens) < 3 { + return n + } + var s []string + for i := 0; i < len(tokens)-1; i++ { + s = append(s, tokens[i]) + } + + return Truncate(strings.Join(s, "-"), 15) + "-" + tokens[len(tokens)-1] +} + +func colorize(c color.Paint, txt string) string { + if c == 0 { + return "" + } + + return color.Colorize(txt, c) +} + +// DecorateLog add a log header to display po/co information along with the log message. +func (o LogOptions) DecorateLog(msg string) string { + _, n := client.Namespaced(o.Path) + if msg == "" { + return msg + } + + if o.MultiPods { + return colorize(o.Color, n+":"+o.Container+" ") + msg + } + + if !o.SingleContainer { + return colorize(o.Color, o.Container+" ") + msg + } + + return msg +} diff --git a/internal/dao/pod.go b/internal/dao/pod.go new file mode 100644 index 00000000..b67f0d5e --- /dev/null +++ b/internal/dao/pod.go @@ -0,0 +1,196 @@ +package dao + +import ( + "bufio" + "context" + "errors" + "fmt" + "io" + "sync/atomic" + "time" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/color" + "github.com/derailed/k9s/internal/watch" + "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" + restclient "k8s.io/client-go/rest" +) + +const defaultTimeout = 1 * time.Second + +// Pod represents a pod resource. +type Pod struct { + Generic +} + +var _ Accessor = &Pod{} +var _ Loggable = &Pod{} + +// Logs fetch container logs for a given pod and container. +func (p *Pod) Logs(path string, opts *v1.PodLogOptions) *restclient.Request { + ns, n := client.Namespaced(path) + return p.Client().DialOrDie().CoreV1().Pods(ns).GetLogs(n, opts) +} + +// Containers returns all container names on pod +func (p *Pod) Containers(path string, includeInit bool) ([]string, error) { + o, err := p.Get("v1/pod", path, labels.Everything()) + if err != nil { + return nil, err + } + + var pod v1.Pod + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod) + if err != nil { + return nil, err + } + + cc := []string{} + for _, c := range pod.Spec.Containers { + cc = append(cc, c.Name) + } + + if includeInit { + for _, c := range pod.Spec.InitContainers { + cc = append(cc, c.Name) + } + } + + return cc, nil +} + +// Logs tails a given container logs +func (p *Pod) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error { + if !opts.HasContainer() { + return p.logs(ctx, c, opts) + } + return tailLogs(ctx, p, c, opts) +} + +// PodLogs tail logs for all containers in a running Pod. +func (p *Pod) logs(ctx context.Context, c chan<- string, opts LogOptions) error { + fac, ok := ctx.Value(internal.KeyFactory).(*watch.Factory) + if !ok { + return errors.New("Expecting an informer") + } + o, err := fac.Get("v1/pods", opts.Path, labels.Everything()) + if err != nil { + return err + } + + var po v1.Pod + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po); err != nil { + return err + } + opts.Color = asColor(po.Name) + if len(po.Spec.InitContainers)+len(po.Spec.Containers) == 1 { + opts.SingleContainer = true + } + + for _, co := range po.Spec.InitContainers { + opts.Container = co.Name + if err := p.TailLogs(ctx, c, opts); err != nil { + return err + } + } + rcos := loggableContainers(po.Status) + for _, co := range po.Spec.Containers { + if in(rcos, co.Name) { + opts.Container = co.Name + if err := p.TailLogs(ctx, c, opts); err != nil { + log.Error().Err(err).Msgf("Getting logs for %s failed", co.Name) + return err + } + } + } + + return nil +} + +func tailLogs(ctx context.Context, logger Logger, c chan<- string, opts LogOptions) error { + log.Debug().Msgf("Tailing logs for %q -- %q", opts.Path, opts.Container) + o := v1.PodLogOptions{ + Container: opts.Container, + Follow: true, + TailLines: &opts.Lines, + Previous: opts.Previous, + } + req := logger.Logs(opts.Path, &o) + ctxt, cancelFunc := context.WithCancel(ctx) + req.Context(ctxt) + + var blocked int32 = 1 + go logsTimeout(cancelFunc, &blocked) + + // This call will block if nothing is in the stream!! + stream, err := req.Stream() + atomic.StoreInt32(&blocked, 0) + if err != nil { + log.Error().Err(err).Msgf("Log stream failed for `%s", opts.Path) + return fmt.Errorf("Unable to obtain log stream for %s", opts.Path) + } + go readLogs(ctx, stream, c, opts) + + return nil +} + +func logsTimeout(cancel context.CancelFunc, blocked *int32) { + <-time.After(defaultTimeout) + if atomic.LoadInt32(blocked) == 1 { + log.Debug().Msg("Timed out reading the log stream") + cancel() + } +} + +func readLogs(ctx context.Context, stream io.ReadCloser, c chan<- string, opts LogOptions) { + defer func() { + log.Debug().Msgf(">>> Closing stream `%s", opts.Path) + if err := stream.Close(); err != nil { + log.Error().Err(err).Msg("Cloing stream") + } + }() + + scanner := bufio.NewScanner(stream) + for scanner.Scan() { + select { + case <-ctx.Done(): + return + default: + c <- opts.DecorateLog(scanner.Text()) + } + } +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func loggableContainers(s v1.PodStatus) []string { + var rcos []string + for _, c := range s.ContainerStatuses { + rcos = append(rcos, c.Name) + } + return rcos +} + +func asColor(n string) color.Paint { + var sum int + for _, r := range n { + sum += int(r) + } + return color.Paint(30 + 2 + sum%6) +} + +// Check if string is in a string list. +func in(ll []string, s string) bool { + for _, l := range ll { + if l == s { + return true + } + } + return false +} diff --git a/internal/k8s/port_forward.go b/internal/dao/port_forwarder.go similarity index 59% rename from internal/k8s/port_forward.go rename to internal/dao/port_forwarder.go index 9fdc369d..c6acf075 100644 --- a/internal/k8s/port_forward.go +++ b/internal/dao/port_forwarder.go @@ -1,14 +1,14 @@ -package k8s +package dao import ( "fmt" "net/http" "net/url" - "strconv" "strings" "time" - "github.com/rs/zerolog" + "github.com/derailed/k9s/internal/client" + "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" @@ -19,18 +19,16 @@ import ( "k8s.io/client-go/rest" "k8s.io/client-go/tools/portforward" "k8s.io/client-go/transport/spdy" - "k8s.io/kubectl/pkg/util" ) const localhost = "localhost" -// PortForward tracks a port forward stream. -type PortForward struct { - Connection +// PortForwarder tracks a port forward stream. +type PortForwarder struct { + client.Connection genericclioptions.IOStreams stopChan, readyChan chan struct{} - logger *zerolog.Logger active bool path string container string @@ -38,63 +36,62 @@ type PortForward struct { age time.Time } -// NewPortForward returns a new port forward streamer. -func NewPortForward(c Connection, l *zerolog.Logger) *PortForward { - return &PortForward{ +// NewPortForwarder returns a new port forward streamer. +func NewPortForwarder(c client.Connection) *PortForwarder { + return &PortForwarder{ Connection: c, - logger: l, stopChan: make(chan struct{}), readyChan: make(chan struct{}), } } // Age returns the port forward age. -func (p *PortForward) Age() string { +func (p *PortForwarder) Age() string { return time.Since(p.age).String() } // Active returns the forward status. -func (p *PortForward) Active() bool { +func (p *PortForwarder) Active() bool { return p.active } // SetActive mark a portforward as active. -func (p *PortForward) SetActive(b bool) { +func (p *PortForwarder) SetActive(b bool) { p.active = b } // Ports returns the forwarded ports mappings. -func (p *PortForward) Ports() []string { +func (p *PortForwarder) Ports() []string { return p.ports } // Path returns the pod resource path. -func (p *PortForward) Path() string { - return p.path +func (p *PortForwarder) Path() string { + return p.path + ":" + p.container } // Container returns the targetes container. -func (p *PortForward) Container() string { +func (p *PortForwarder) Container() string { return p.container } // Stop terminates a port forard -func (p *PortForward) Stop() { - p.logger.Debug().Msgf("<<< Stopping port forward %q %v", p.path, p.ports) +func (p *PortForwarder) Stop() { + log.Debug().Msgf("<<< Stopping PortForward %q %v", p.path, p.ports) p.active = false close(p.stopChan) } // FQN returns the portforward unique id. -func (p *PortForward) FQN() string { +func (p *PortForwarder) FQN() string { return p.path + ":" + p.container } // Start initiates a port forward session for a given pod and ports. -func (p *PortForward) Start(path, co string, ports []string) (*portforward.PortForwarder, error) { +func (p *PortForwarder) Start(path, co, address string, ports []string) (*portforward.PortForwarder, error) { p.path, p.container, p.ports, p.age = path, co, ports, time.Now() - ns, n := namespaced(path) + ns, n := client.Namespaced(path) pod, err := p.DialOrDie().CoreV1().Pods(ns).Get(n, metav1.GetOptions{}) if err != nil { return nil, err @@ -110,7 +107,7 @@ func (p *PortForward) Start(path, co string, ports []string) (*portforward.PortF rcfg.NegotiatedSerializer = codec.WithoutConversion() clt, err := rest.RESTClientFor(rcfg) if err != nil { - p.logger.Debug().Msgf("Boom! %#v", err) + log.Debug().Msgf("Boom! %#v", err) return nil, err } req := clt.Post(). @@ -119,10 +116,10 @@ func (p *PortForward) Start(path, co string, ports []string) (*portforward.PortF Name(n). SubResource("portforward") - return p.forwardPorts("POST", req.URL(), ports) + return p.forwardPorts("POST", req.URL(), address, ports) } -func (p *PortForward) forwardPorts(method string, url *url.URL, ports []string) (*portforward.PortForwarder, error) { +func (p *PortForwarder) forwardPorts(method string, url *url.URL, address string, ports []string) (*portforward.PortForwarder, error) { cfg, err := p.Config().RESTConfig() if err != nil { return nil, err @@ -133,7 +130,10 @@ func (p *PortForward) forwardPorts(method string, url *url.URL, ports []string) } dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, method, url) - addrs := []string{localhost} + if address == "" { + address = localhost + } + addrs := strings.Split(address, ",") return portforward.NewOnAddresses(dialer, addrs, ports, p.stopChan, p.readyChan, p.Out, p.ErrOut) } @@ -149,40 +149,3 @@ func codec() (serializer.CodecFactory, runtime.ParameterCodec) { return serializer.NewCodecFactory(scheme), runtime.NewParameterCodec(scheme) } - -func svcPortToTargetPort(ports []string, svc v1.Service, pod v1.Pod) ([]string, error) { - var translated []string - for _, port := range ports { - localPort, remotePort := splitPort(port) - portnum, err := strconv.Atoi(remotePort) - if err != nil { - svcPort, err := util.LookupServicePortNumberByName(svc, remotePort) - if err != nil { - return nil, err - } - portnum = int(svcPort) - if localPort == remotePort { - localPort = strconv.Itoa(portnum) - } - } - containerPort, err := util.LookupContainerPortNumberByServicePort(svc, pod, int32(portnum)) - if err != nil { - return nil, err - } - if int32(portnum) != containerPort { - port = fmt.Sprintf("%s:%d", localPort, containerPort) - } - translated = append(translated, port) - } - - return translated, nil -} - -func splitPort(port string) (local, remote string) { - parts := strings.Split(port, ":") - if len(parts) == 2 { - return parts[0], parts[1] - } - - return parts[0], parts[0] -} diff --git a/internal/dao/portforward.go b/internal/dao/portforward.go new file mode 100644 index 00000000..f389d560 --- /dev/null +++ b/internal/dao/portforward.go @@ -0,0 +1,14 @@ +package dao + +type PortForward struct { + Generic +} + +var _ Accessor = &PortForward{} +var _ Nuker = &PortForward{} + +// Delete a portforward. +func (p *PortForward) Delete(path string, cascade, force bool) error { + p.Factory.DeleteForwarder(path) + return nil +} diff --git a/internal/dao/registry.go b/internal/dao/registry.go new file mode 100644 index 00000000..e10313bb --- /dev/null +++ b/internal/dao/registry.go @@ -0,0 +1,287 @@ +package dao + +import ( + "fmt" + "sort" + + "github.com/derailed/k9s/internal/client" + "github.com/rs/zerolog/log" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" +) + +// MetaViewers represents a collection of meta viewers. +type ResourceMetas map[client.GVR]metav1.APIResource + +// Accessors represents a collection of dao accessors. +type Accessors map[client.GVR]Accessor + +var resMetas = ResourceMetas{} + +// AccessorFor returns a client accessor for a resource if registered. +// Otherwise it returns a generic accessor. +// Customize here for non resource types or types with metrics or logs. +func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) { + m := Accessors{ + "contexts": &Context{}, + "containers": &Container{}, + "screendumps": &ScreenDump{}, + "benchmarks": &Benchmark{}, + "portforwards": &PortForward{}, + "v1/services": &Service{}, + "v1/pods": &Pod{}, + "apps/v1/deployments": &Deployment{}, + "apps/v1/daemonsets": &DaemonSet{}, + "extensions/v1beta1/daemonsets": &DaemonSet{}, + "apps/v1/statefulsets": &StatefulSet{}, + "batch/v1beta1/cronjobs": &CronJob{}, + "batch/v1/jobs": &Job{}, + } + + r, ok := m[gvr] + if !ok { + r = &Generic{} + log.Warn().Msgf("No DAO registry entry for %q. Using factory!", gvr) + } + r.Init(f, gvr) + + return r, nil +} + +// RegisterMeta registers a new resource meta object. +func RegisterMeta(gvr string, res metav1.APIResource) { + resMetas[client.GVR(gvr)] = res +} + +func AllGVRs() client.GVRs { + kk := make(client.GVRs, 0, len(resMetas)) + for k := range resMetas { + kk = append(kk, k) + } + sort.Sort(kk) + + return kk +} + +// MetaFor returns a resource metadata for a given gvr. +func MetaFor(gvr client.GVR) (metav1.APIResource, error) { + m, ok := resMetas[gvr] + if !ok { + return metav1.APIResource{}, fmt.Errorf("no resource meta defined for %q", gvr) + } + return m, nil +} + +// IsK9sMeta checks for non resource meta. +func IsK9sMeta(m metav1.APIResource) bool { + for _, c := range m.Categories { + if c == "k9s" { + return true + } + } + + return false +} + +// Load hydrates server preferred+CRDs resource metadata. +func LoadResources(f Factory) error { + resMetas = make(ResourceMetas, 100) + if err := loadPreferred(f, resMetas); err != nil { + return err + } + loadNonResource(resMetas) + + return loadCRDs(f, resMetas) +} + +// BOZO!! Need contermeasure for direct commands! +func loadNonResource(m ResourceMetas) { + m["aliases"] = metav1.APIResource{ + Name: "aliases", + Kind: "Aliases", + Categories: []string{"k9s"}, + } + m["contexts"] = metav1.APIResource{ + Name: "contexts", + Kind: "Contexts", + ShortNames: []string{"ctx"}, + Categories: []string{"k9s"}, + } + m["screendumps"] = metav1.APIResource{ + Name: "screendumps", + Kind: "ScreenDumps", + ShortNames: []string{"sd"}, + Verbs: []string{"delete"}, + Categories: []string{"k9s"}, + } + m["benchmarks"] = metav1.APIResource{ + Name: "benchmarks", + Kind: "Benchmarks", + ShortNames: []string{"be"}, + Verbs: []string{"delete"}, + Categories: []string{"k9s"}, + } + m["portforwards"] = metav1.APIResource{ + Name: "portforwards", + Namespaced: true, + Kind: "PortForwards", + ShortNames: []string{"pf"}, + Verbs: []string{"delete"}, + Categories: []string{"k9s"}, + } + m["containers"] = metav1.APIResource{ + Name: "containers", + Kind: "Containers", + Categories: []string{"k9s"}, + } + + loadRBAC(m) +} + +func loadRBAC(m ResourceMetas) { + m["rbac"] = metav1.APIResource{ + Name: "rbacs", + Kind: "Rules", + Categories: []string{"k9s"}, + } + m["policy"] = metav1.APIResource{ + Name: "policies", + Kind: "Rules", + Namespaced: true, + Categories: []string{"k9s"}, + } + m["users"] = metav1.APIResource{ + Name: "users", + Kind: "User", + Categories: []string{"k9s"}, + } + m["groups"] = metav1.APIResource{ + Name: "groups", + Kind: "Group", + Categories: []string{"k9s"}, + } +} + +func loadPreferred(f Factory, m ResourceMetas) error { + discovery, err := f.Client().CachedDiscovery() + if err != nil { + return err + } + rr, err := discovery.ServerPreferredResources() + if err != nil { + return err + } + for _, r := range rr { + for _, res := range r.APIResources { + gvr := client.FromGVAndR(r.GroupVersion, res.Name) + log.Debug().Msgf("GVR %s", gvr) + res.Group, res.Version = gvr.ToG(), gvr.ToV() + m[gvr] = res + } + } + + return nil +} + +func loadCRDs(f Factory, m ResourceMetas) error { + oo, err := f.List("apiextensions.k8s.io/v1beta1/customresourcedefinitions", "", labels.Everything()) + if err != nil { + log.Error().Err(err).Msgf("Fail CRDs load") + return nil + } + f.WaitForCacheSync() + + for _, o := range oo { + meta, errs := extractMeta(o) + if len(errs) > 0 { + log.Error().Err(errs[0]).Msgf("Fail to extract CRD meta (%d) errors", len(errs)) + continue + } + gvr := client.NewGVR(meta.Group, meta.Version, meta.Name) + m[gvr] = meta + } + + return nil +} + +func extractMeta(o runtime.Object) (metav1.APIResource, []error) { + var ( + m metav1.APIResource + errs []error + ) + + crd, ok := o.(*unstructured.Unstructured) + if !ok { + return m, append(errs, fmt.Errorf("Expected CustomResourceDefinition, but got %T", o)) + } + + var spec map[string]interface{} + spec, errs = extractMap(crd.Object, "spec", errs) + + var meta map[string]interface{} + meta, errs = extractMap(crd.Object, "metadata", errs) + m.Name, errs = extractStr(meta, "name", errs) + + m.Group, errs = extractStr(spec, "group", errs) + m.Version, errs = extractStr(spec, "version", errs) + + var scope string + scope, errs = extractStr(spec, "scope", errs) + + m.Namespaced = isNamespaced(scope) + + var names map[string]interface{} + names, errs = extractMap(spec, "names", errs) + m.Kind, errs = extractStr(names, "kind", errs) + m.SingularName, errs = extractStr(names, "singular", errs) + m.Name, errs = extractStr(names, "plural", errs) + m.ShortNames, errs = extractSlice(names, "shortNames", errs) + + return m, errs +} + +func isNamespaced(scope string) bool { + return scope == "Namespaced" +} + +func extractSlice(m map[string]interface{}, n string, errs []error) ([]string, []error) { + if m[n] == nil { + return nil, errs + } + s, ok := m[n].([]string) + if ok { + return s, errs + } + + ii, ok := m[n].([]interface{}) + if !ok { + return s, append(errs, fmt.Errorf("failed to extract slice %s -- %#v", n, m)) + } + + ss := make([]string, len(ii)) + for i, name := range ii { + ss[i], ok = name.(string) + if !ok { + return s, append(errs, fmt.Errorf("expecting string shortnames")) + } + } + return s, errs +} + +func extractStr(m map[string]interface{}, n string, errs []error) (string, []error) { + s, ok := m[n].(string) + if !ok { + return s, append(errs, fmt.Errorf("failed to extract string %s", n)) + } + return s, errs +} + +func extractMap(m map[string]interface{}, n string, errs []error) (map[string]interface{}, []error) { + v, ok := m[n].(map[string]interface{}) + if !ok { + return v, append(errs, fmt.Errorf("failed to extract field %s", n)) + } + return v, errs +} diff --git a/internal/k8s/mapper.go b/internal/dao/rest_mapper.go similarity index 80% rename from internal/k8s/mapper.go rename to internal/dao/rest_mapper.go index 09be277e..915a71cf 100644 --- a/internal/k8s/mapper.go +++ b/internal/dao/rest_mapper.go @@ -1,26 +1,21 @@ -package k8s +package dao import ( "fmt" - "os/user" - "regexp" "strings" - "github.com/rs/zerolog/log" + "github.com/derailed/k9s/internal/client" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/restmapper" ) -var ( - // RestMapping holds k8s resource mapping - RestMapping = &RestMapper{} - toFileName = regexp.MustCompile(`[^(\w/\.)]`) -) +// RestMapping holds k8s resource mapping +var RestMapping = &RestMapper{} // RestMapper map resource to REST mapping ie kind, group, version. type RestMapper struct { - Connection + client.Connection } // ToRESTMapper map resources to kind, and map kind and version to interfaces for manipulating K8s objects. @@ -34,19 +29,6 @@ func (r *RestMapper) ToRESTMapper() (meta.RESTMapper, error) { return expander, nil } -func toHostDir(host string) string { - h := strings.Replace(strings.Replace(host, "https://", "", 1), "http://", "", 1) - return toFileName.ReplaceAllString(h, "_") -} - -func mustHomeDir() string { - usr, err := user.Current() - if err != nil { - panic(err) - } - return usr.HomeDir -} - // ResourceFor produces a rest mapping from a given resource. // Support full res name ie deployment.v1.apps. func (r *RestMapper) ResourceFor(resourceArg, kind string) (*meta.RESTMapping, error) { @@ -73,7 +55,6 @@ func (r *RestMapper) resourceFor(resourceArg string) (schema.GroupVersionResourc } fullGVR, gr := schema.ParseResourceArg(strings.ToLower(resourceArg)) - log.Debug().Msgf("GVR %#v -- %#v", fullGVR, gr) if fullGVR != nil { return mapper.ResourceFor(*fullGVR) } diff --git a/internal/dao/screen_dump.go b/internal/dao/screen_dump.go new file mode 100644 index 00000000..29c08f1d --- /dev/null +++ b/internal/dao/screen_dump.go @@ -0,0 +1,17 @@ +package dao + +import ( + "os" +) + +type ScreenDump struct { + Generic +} + +var _ Accessor = &ScreenDump{} +var _ Nuker = &ScreenDump{} + +// Delete a ScreenDump. +func (d *ScreenDump) Delete(path string, cascade, force bool) error { + return os.Remove(path) +} diff --git a/internal/dao/sts.go b/internal/dao/sts.go new file mode 100644 index 00000000..89a7b531 --- /dev/null +++ b/internal/dao/sts.go @@ -0,0 +1,80 @@ +package dao + +import ( + "context" + "errors" + "fmt" + + "github.com/derailed/k9s/internal/client" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/kubectl/pkg/polymorphichelpers" +) + +type StatefulSet struct { + Generic +} + +var _ Accessor = &StatefulSet{} +var _ Loggable = &StatefulSet{} +var _ Restartable = &StatefulSet{} +var _ Scalable = &StatefulSet{} + +// Scale a StatefulSet. +func (s *StatefulSet) Scale(path string, replicas int32) error { + ns, n := client.Namespaced(path) + scale, err := s.Client().DialOrDie().AppsV1().StatefulSets(ns).GetScale(n, metav1.GetOptions{}) + if err != nil { + return err + } + scale.Spec.Replicas = replicas + _, err = s.Client().DialOrDie().AppsV1().StatefulSets(ns).UpdateScale(n, scale) + + return err +} + +// Restart a StatefulSet rollout. +func (s *StatefulSet) Restart(path string) error { + o, err := s.Get(string(s.gvr), path, labels.Everything()) + if err != nil { + return err + } + + var ds appsv1.StatefulSet + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds) + if err != nil { + return err + } + + update, err := polymorphichelpers.ObjectRestarterFn(&ds) + if err != nil { + return err + } + + _, err = s.Client().DialOrDie().AppsV1().StatefulSets(ds.Namespace).Patch(ds.Name, types.StrategicMergePatchType, update) + return err +} + +// Logs tail logs for all pods represented by this StatefulSet. +func (s *StatefulSet) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error { + o, err := s.Get(string(s.gvr), opts.Path, labels.Everything()) + if err != nil { + return err + } + + var dp appsv1.StatefulSet + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &dp) + if err != nil { + return errors.New("expecting StatefulSet resource") + } + + if dp.Spec.Selector == nil || len(dp.Spec.Selector.MatchLabels) == 0 { + return fmt.Errorf("No valid selector found on StatefulSet %s", opts.Path) + } + + return podLogs(ctx, c, dp.Spec.Selector.MatchLabels, opts) +} diff --git a/internal/dao/svc.go b/internal/dao/svc.go new file mode 100644 index 00000000..cabb7797 --- /dev/null +++ b/internal/dao/svc.go @@ -0,0 +1,38 @@ +package dao + +import ( + "context" + "errors" + "fmt" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" +) + +type Service struct { + Generic +} + +var _ Accessor = &Service{} +var _ Loggable = &Service{} + +// Logs tail logs for all pods represented by this Service. +func (s *Service) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error { + o, err := s.Get(string(s.gvr), opts.Path, labels.Everything()) + if err != nil { + return err + } + var svc v1.Service + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &svc) + if err != nil { + return errors.New("expecting Service resource") + } + + if svc.Spec.Selector == nil || len(svc.Spec.Selector) == 0 { + return fmt.Errorf("no valid selector found on Service %s", opts.Path) + } + + return podLogs(ctx, c, svc.Spec.Selector, opts) +} diff --git a/internal/dao/types.go b/internal/dao/types.go new file mode 100644 index 00000000..553d8857 --- /dev/null +++ b/internal/dao/types.go @@ -0,0 +1,83 @@ +package dao + +import ( + "context" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/watch" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/informers" + restclient "k8s.io/client-go/rest" +) + +type Factory interface { + // Client retrieves an api client. + Client() client.Connection + + // Get fetch a given resource. + Get(gvr, path string, sel labels.Selector) (runtime.Object, error) + + // List fetch a collection of resources. + List(ns, gvr string, sel labels.Selector) ([]runtime.Object, error) + + // ForResource fetch an informer for a given resource. + ForResource(ns, gvr string) informers.GenericInformer + + // WaitForCacheSync synchronize the cache. + WaitForCacheSync() + + // DeleteForwarder deletes a pod forwarder. + DeleteForwarder(path string) + + // Forwards returns all portforwards. + Forwarders() watch.Forwarders +} + +// Accessor represents an accessible k8s resource. +type Accessor interface { + Nuker + + // Init the resource with a factory object. + Init(Factory, client.GVR) +} + +// Loggable represents resources with logs. +type Loggable interface { + // TaiLogs streams resource logs. + TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error +} + +type Scalable interface { + Scale(path string, replicas int32) error +} + +// Nuker represents a resource deleter. +type Nuker interface { + // Delete removes a resource from the api server. + Delete(path string, cascade, force bool) error +} + +// Switchable represents a switchable resource. +type Switchable interface { + // Switch changes the active context. + Switch(ctx string) error +} + +// Restartable represents a restartable resource. +type Restartable interface { + // Restart performs a rollout restart. + Restart(path string) error +} + +// Runnable represents a runnable resource. +type Runnable interface { + // Run triggers a run. + Run(path string) error +} + +// Loggers represents a resource that exposes logs. +type Logger interface { + Logs(path string, opts *v1.PodLogOptions) *restclient.Request +} diff --git a/internal/k8s/base.go b/internal/k8s/base.go deleted file mode 100644 index 94a7081c..00000000 --- a/internal/k8s/base.go +++ /dev/null @@ -1,8 +0,0 @@ -package k8s - -type base struct { -} - -func (b *base) Kill(ns, n string) error { - return nil -} diff --git a/internal/k8s/cluster.go b/internal/k8s/cluster.go deleted file mode 100644 index 632b13c6..00000000 --- a/internal/k8s/cluster.go +++ /dev/null @@ -1,63 +0,0 @@ -package k8s - -import ( - "github.com/rs/zerolog" - v1 "k8s.io/api/core/v1" -) - -// Cluster represents a Kubernetes cluster. -type Cluster struct { - Connection - - logger *zerolog.Logger -} - -// NewCluster instantiates a new cluster. -func NewCluster(c Connection, l *zerolog.Logger) *Cluster { - return &Cluster{c, l} -} - -// Version returns the current cluster git version. -func (c *Cluster) Version() (string, error) { - rev, err := c.ServerVersion() - if err != nil { - c.logger.Warn().Msgf("%s", err) - return "", err - } - return rev.GitVersion, nil -} - -// ContextName returns the currently active context. -func (c *Cluster) ContextName() string { - ctx, err := c.Config().CurrentContextName() - if err != nil { - c.logger.Warn().Msgf("%s", err) - return "N/A" - } - return ctx -} - -// ClusterName return the currently active cluster name. -func (c *Cluster) ClusterName() string { - ctx, err := c.Config().CurrentClusterName() - if err != nil { - c.logger.Warn().Msgf("%s", err) - return "N/A" - } - return ctx -} - -// UserName returns the currently active user. -func (c *Cluster) UserName() string { - usr, err := c.Config().CurrentUserName() - if err != nil { - c.logger.Warn().Msgf("%s", err) - return "N/A" - } - return usr -} - -// GetNodes get all available nodes in the cluster. -func (c *Cluster) GetNodes() (*v1.NodeList, error) { - return c.FetchNodes() -} diff --git a/internal/k8s/cluster_role.go b/internal/k8s/cluster_role.go deleted file mode 100644 index abe707e8..00000000 --- a/internal/k8s/cluster_role.go +++ /dev/null @@ -1,40 +0,0 @@ -package k8s - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// ClusterRole represents a Kubernetes ClusterRole -type ClusterRole struct { - *base - Connection -} - -// NewClusterRole returns a new ClusterRole. -func NewClusterRole(c Connection) *ClusterRole { - return &ClusterRole{&base{}, c} -} - -// Get a cluster role. -func (c *ClusterRole) Get(_, n string) (interface{}, error) { - return c.DialOrDie().RbacV1().ClusterRoles().Get(n, metav1.GetOptions{}) -} - -// List all ClusterRoles on a cluster. -func (c *ClusterRole) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := c.DialOrDie().RbacV1().ClusterRoles().List(opts) - if err != nil { - return nil, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - - return cc, nil -} - -// Delete a ClusterRole. -func (c *ClusterRole) Delete(_, n string, cascade, force bool) error { - return c.DialOrDie().RbacV1().ClusterRoles().Delete(n, nil) -} diff --git a/internal/k8s/cluster_roleb.go b/internal/k8s/cluster_roleb.go deleted file mode 100644 index fc853e34..00000000 --- a/internal/k8s/cluster_roleb.go +++ /dev/null @@ -1,40 +0,0 @@ -package k8s - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// ClusterRoleBinding represents a Kubernetes ClusterRoleBinding -type ClusterRoleBinding struct { - *base - Connection -} - -// NewClusterRoleBinding returns a new ClusterRoleBinding. -func NewClusterRoleBinding(c Connection) *ClusterRoleBinding { - return &ClusterRoleBinding{&base{}, c} -} - -// Get a service. -func (c *ClusterRoleBinding) Get(_, n string) (interface{}, error) { - return c.DialOrDie().RbacV1().ClusterRoleBindings().Get(n, metav1.GetOptions{}) -} - -// List all ClusterRoleBindings on a cluster. -func (c *ClusterRoleBinding) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := c.DialOrDie().RbacV1().ClusterRoleBindings().List(opts) - if err != nil { - return Collection{}, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - - return cc, nil -} - -// Delete a ClusterRoleBinding. -func (c *ClusterRoleBinding) Delete(_, n string, cascade, force bool) error { - return c.DialOrDie().RbacV1().ClusterRoleBindings().Delete(n, nil) -} diff --git a/internal/k8s/context.go b/internal/k8s/context.go deleted file mode 100644 index 0572b93b..00000000 --- a/internal/k8s/context.go +++ /dev/null @@ -1,106 +0,0 @@ -package k8s - -import ( - "fmt" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "k8s.io/client-go/tools/clientcmd" - "k8s.io/client-go/tools/clientcmd/api" -) - -// NamedContext represents a named cluster context. -type NamedContext struct { - Name string - Context *api.Context - config *Config -} - -// NewNamedContext returns a new named context. -func NewNamedContext(c *Config, n string, ctx *api.Context) *NamedContext { - return &NamedContext{Name: n, Context: ctx, config: c} -} - -// MustCurrentContextName return the active context name. -func (c *NamedContext) MustCurrentContextName() string { - cl, err := c.config.CurrentContextName() - if err != nil { - panic(err) - } - return cl -} - -// ---------------------------------------------------------------------------- - -// Context represents a Kubernetes Context. -type Context struct { - *base - Connection -} - -// NewContext returns a new Context. -func NewContext(c Connection) *Context { - return &Context{&base{}, c} -} - -// Get a Context. -func (c *Context) Get(_, n string) (interface{}, error) { - ctx, err := c.Config().GetContext(n) - if err != nil { - return nil, err - } - return &NamedContext{Name: n, Context: ctx}, nil -} - -// List all Contexts on the current cluster. -func (c *Context) List(string, metav1.ListOptions) (Collection, error) { - ctxs, err := c.Config().Contexts() - if err != nil { - return nil, err - } - cc := make([]interface{}, 0, len(ctxs)) - for k, v := range ctxs { - cc = append(cc, NewNamedContext(c.Config(), k, v)) - } - - return cc, nil -} - -// Delete a Context. -func (c *Context) Delete(_, n string, cascade, force bool) error { - ctx, err := c.Config().CurrentContextName() - if err != nil { - return err - } - if ctx == n { - return fmt.Errorf("trying to delete your current context %s", n) - } - return c.Config().DelContext(n) -} - -// MustCurrentContextName return the active context name. -func (c *Context) MustCurrentContextName() string { - cl, err := c.Config().CurrentContextName() - if err != nil { - panic(err) - } - return cl -} - -// Switch to another context. -func (c *Context) Switch(ctx string) error { - c.SwitchContextOrDie(ctx) - return nil -} - -// KubeUpdate modifies kubeconfig default context. -func (c *Context) KubeUpdate(n string) error { - config, err := c.Config().RawConfig() - if err != nil { - return err - } - c.Switch(n) - return clientcmd.ModifyConfig( - clientcmd.NewDefaultPathOptions(), config, true, - ) -} diff --git a/internal/k8s/crd.go b/internal/k8s/crd.go deleted file mode 100644 index feaad0d4..00000000 --- a/internal/k8s/crd.go +++ /dev/null @@ -1,40 +0,0 @@ -package k8s - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// CustomResourceDefinition represents a Kubernetes CustomResourceDefinition -type CustomResourceDefinition struct { - *base - Connection -} - -// NewCustomResourceDefinition returns a new CustomResourceDefinition. -func NewCustomResourceDefinition(c Connection) *CustomResourceDefinition { - return &CustomResourceDefinition{&base{}, c} -} - -// Get a CustomResourceDefinition. -func (c *CustomResourceDefinition) Get(_, n string) (interface{}, error) { - return c.NSDialOrDie().Get(n, metav1.GetOptions{}) -} - -// List all CustomResourceDefinitions in a given namespace. -func (c *CustomResourceDefinition) List(_ string, opts metav1.ListOptions) (Collection, error) { - rr, err := c.NSDialOrDie().List(opts) - if err != nil { - return nil, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - - return cc, nil -} - -// Delete a CustomResourceDefinition. -func (c *CustomResourceDefinition) Delete(_, n string, cascade, force bool) error { - return c.NSDialOrDie().Delete(n, nil) -} diff --git a/internal/k8s/cronjob.go b/internal/k8s/cronjob.go deleted file mode 100644 index 24a9957d..00000000 --- a/internal/k8s/cronjob.go +++ /dev/null @@ -1,71 +0,0 @@ -package k8s - -import ( - batchv1 "k8s.io/api/batch/v1" - batchv1beta1 "k8s.io/api/batch/v1beta1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/rand" -) - -const maxJobNameSize = 42 - -// CronJob represents a Kubernetes CronJob. -type CronJob struct { - *base - Connection -} - -// NewCronJob returns a new CronJob. -func NewCronJob(c Connection) *CronJob { - return &CronJob{&base{}, c} -} - -// Get a CronJob. -func (c *CronJob) Get(ns, n string) (interface{}, error) { - return c.DialOrDie().BatchV1beta1().CronJobs(ns).Get(n, metav1.GetOptions{}) -} - -// List all CronJobs in a given namespace. -func (c *CronJob) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := c.DialOrDie().BatchV1beta1().CronJobs(ns).List(opts) - if err != nil { - return nil, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - - return cc, nil -} - -// Delete a CronJob. -func (c *CronJob) Delete(ns, n string, cascade, force bool) error { - return c.DialOrDie().BatchV1beta1().CronJobs(ns).Delete(n, nil) -} - -// Run the job associated with this cronjob. -func (c *CronJob) Run(ns, n string) error { - cj, err := c.Get(ns, n) - if err != nil { - return err - } - cronJob := cj.(*batchv1beta1.CronJob) - - var jobName = cronJob.Name - if len(cronJob.Name) >= maxJobNameSize { - jobName = cronJob.Name[0:maxJobNameSize] - } - - job := &batchv1.Job{ - ObjectMeta: metav1.ObjectMeta{ - Name: jobName + "-manual-" + rand.String(3), - Namespace: ns, - Labels: cronJob.Spec.JobTemplate.Labels, - }, - Spec: cronJob.Spec.JobTemplate.Spec, - } - - _, err = c.DialOrDie().BatchV1().Jobs(ns).Create(job) - return err -} diff --git a/internal/k8s/dp.go b/internal/k8s/dp.go deleted file mode 100644 index 2b1993d2..00000000 --- a/internal/k8s/dp.go +++ /dev/null @@ -1,70 +0,0 @@ -package k8s - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/kubectl/pkg/polymorphichelpers" -) - -// Deployment represents a Kubernetes Deployment. -type Deployment struct { - *base - Connection -} - -// NewDeployment returns a new Deployment. -func NewDeployment(c Connection) *Deployment { - return &Deployment{&base{}, c} -} - -// Get a deployment. -func (d *Deployment) Get(ns, n string) (interface{}, error) { - return d.DialOrDie().AppsV1().Deployments(ns).Get(n, metav1.GetOptions{}) -} - -// List all Deployments in a given namespace. -func (d *Deployment) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := d.DialOrDie().AppsV1().Deployments(ns).List(opts) - if err != nil { - return nil, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - - return cc, nil -} - -// Delete a Deployment. -func (d *Deployment) Delete(ns, n string, cascade, force bool) error { - return d.DialOrDie().AppsV1().Deployments(ns).Delete(n, nil) -} - -// Scale a Deployment. -func (d *Deployment) Scale(ns, n string, replicas int32) error { - scale, err := d.DialOrDie().AppsV1().Deployments(ns).GetScale(n, metav1.GetOptions{}) - if err != nil { - return err - } - - scale.Spec.Replicas = replicas - _, err = d.DialOrDie().AppsV1().Deployments(ns).UpdateScale(n, scale) - return err -} - -// Restart a Deployment rollout. -func (d *Deployment) Restart(ns, n string) error { - - dp, err := d.DialOrDie().AppsV1().Deployments(ns).Get(n, metav1.GetOptions{}) - if err != nil { - return err - } - update, err := polymorphichelpers.ObjectRestarterFn(dp) - if err != nil { - return err - } - - _, err = d.DialOrDie().AppsV1().Deployments(ns).Patch(dp.Name, types.StrategicMergePatchType, update) - return err -} diff --git a/internal/k8s/ds.go b/internal/k8s/ds.go deleted file mode 100644 index 4df02ea4..00000000 --- a/internal/k8s/ds.go +++ /dev/null @@ -1,64 +0,0 @@ -package k8s - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/kubectl/pkg/polymorphichelpers" -) - -// DaemonSet represents a Kubernetes DaemonSet -type DaemonSet struct { - *base - Connection -} - -// NewDaemonSet returns a new DaemonSet. -func NewDaemonSet(c Connection) *DaemonSet { - return &DaemonSet{&base{}, c} -} - -// Get a DaemonSet. -func (d *DaemonSet) Get(ns, n string) (interface{}, error) { - return d.DialOrDie().AppsV1().DaemonSets(ns).Get(n, metav1.GetOptions{}) -} - -// List all DaemonSets in a given namespace. -func (d *DaemonSet) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := d.DialOrDie().AppsV1().DaemonSets(ns).List(opts) - if err != nil { - return nil, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - - return cc, nil -} - -// Delete a DaemonSet. -func (d *DaemonSet) Delete(ns, n string, cascade, force bool) error { - p := metav1.DeletePropagationOrphan - if cascade { - p = metav1.DeletePropagationBackground - } - return d.DialOrDie().AppsV1().DaemonSets(ns).Delete(n, &metav1.DeleteOptions{ - PropagationPolicy: &p, - }) -} - -// Restart a DaemonSet rollout. -func (d *DaemonSet) Restart(ns, n string) error { - - ds, err := d.DialOrDie().AppsV1().DaemonSets(ns).Get(n, metav1.GetOptions{}) - if err != nil { - return err - } - update, err := polymorphichelpers.ObjectRestarterFn(ds) - if err != nil { - return err - } - - _, err = d.DialOrDie().AppsV1().DaemonSets(ns).Patch(ds.Name, types.StrategicMergePatchType, update) - return err -} diff --git a/internal/k8s/ep.go b/internal/k8s/ep.go deleted file mode 100644 index cc74157f..00000000 --- a/internal/k8s/ep.go +++ /dev/null @@ -1,40 +0,0 @@ -package k8s - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// Endpoints represents a Kubernetes Endpoints. -type Endpoints struct { - *base - Connection -} - -// NewEndpoints returns a new Endpoints. -func NewEndpoints(c Connection) *Endpoints { - return &Endpoints{&base{}, c} -} - -// Get a Endpoint. -func (e *Endpoints) Get(ns, n string) (interface{}, error) { - return e.DialOrDie().CoreV1().Endpoints(ns).Get(n, metav1.GetOptions{}) -} - -// List all Endpoints in a given namespace. -func (e *Endpoints) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := e.DialOrDie().CoreV1().Endpoints(ns).List(opts) - if err != nil { - return nil, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - - return cc, nil -} - -// Delete a Endpoint. -func (e *Endpoints) Delete(ns, n string, cascade, force bool) error { - return e.DialOrDie().CoreV1().Endpoints(ns).Delete(n, nil) -} diff --git a/internal/k8s/evt.go b/internal/k8s/evt.go deleted file mode 100644 index a1b061e2..00000000 --- a/internal/k8s/evt.go +++ /dev/null @@ -1,40 +0,0 @@ -package k8s - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// Event represents a Kubernetes Event. -type Event struct { - *base - Connection -} - -// NewEvent returns a new Event. -func NewEvent(c Connection) *Event { - return &Event{&base{}, c} -} - -// Get a Event. -func (e *Event) Get(ns, n string) (interface{}, error) { - return e.DialOrDie().CoreV1().Events(ns).Get(n, metav1.GetOptions{}) -} - -// List all Events in a given namespace. -func (e *Event) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := e.DialOrDie().CoreV1().Events(ns).List(opts) - if err != nil { - return nil, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - - return cc, nil -} - -// Delete an Event. -func (e *Event) Delete(ns, n string, cascade, force bool) error { - return e.DialOrDie().CoreV1().Events(ns).Delete(n, nil) -} diff --git a/internal/k8s/gvr.go b/internal/k8s/gvr.go deleted file mode 100644 index 5d7ea45f..00000000 --- a/internal/k8s/gvr.go +++ /dev/null @@ -1,74 +0,0 @@ -package k8s - -import ( - "path" - "strings" - - "k8s.io/apimachinery/pkg/runtime/schema" -) - -// GVR represents a fully qualified kubernetes resource. -type GVR string - -// NewGVR returns a new gvr. -func NewGVR(g, v, r string) GVR { - return GVR(path.Join(g, v, r)) -} - -// ToGVR returns a new gvr from a string. -func ToGVR(gv, r string) GVR { - return GVR(path.Join(gv, r)) -} - -// ResName returns a full res name ie dp.v1.apps. -func (g GVR) ResName() string { - return g.ToR() + "." + g.ToV() + "." + g.ToG() -} - -// AsGR returns the group version. -func (g GVR) AsGR() schema.GroupVersion { - return schema.GroupVersion{ - Group: g.ToG(), - Version: g.ToV(), - } -} - -// AsGVR returns a schema gvr instance. -func (g GVR) AsGVR() schema.GroupVersionResource { - return schema.GroupVersionResource{ - Group: g.ToG(), - Version: g.ToV(), - Resource: g.ToR(), - } -} - -// String returns a GVR as a string. -func (g GVR) String() string { - return string(g) -} - -// ToV returns the resource version. -func (g GVR) ToV() string { - tokens := strings.Split(string(g), "/") - if len(tokens) < 2 { - return "" - } - return tokens[len(tokens)-2] -} - -// ToR returns the resource name. -func (g GVR) ToR() string { - tokens := strings.Split(string(g), "/") - return tokens[len(tokens)-1] -} - -// ToG returns the resource group name. -func (g GVR) ToG() string { - tokens := strings.Split(string(g), "/") - switch len(tokens) { - case 3: - return tokens[0] - default: - return "" - } -} diff --git a/internal/k8s/gvr_test.go b/internal/k8s/gvr_test.go deleted file mode 100644 index 3b0b93d7..00000000 --- a/internal/k8s/gvr_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package k8s_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/k8s" - "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -func TestAsGR(t *testing.T) { - uu := map[string]struct { - gvr string - e schema.GroupVersion - }{ - "full": {"apps/v1/deployments", schema.GroupVersion{"apps", "v1"}}, - "core": {"v1/pods", schema.GroupVersion{"", "v1"}}, - "bork": {"users", schema.GroupVersion{"", ""}}, - } - - for k, u := range uu { - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, k8s.GVR(u.gvr).AsGR()) - }) - } -} - -func TestNewGVR(t *testing.T) { - uu := map[string]struct { - g, v, r string - e string - }{ - "full": {"apps", "v1", "deployments", "apps/v1/deployments"}, - "core": {"", "v1", "pods", "v1/pods"}, - } - - for k, u := range uu { - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, k8s.NewGVR(u.g, u.v, u.r).String()) - }) - } -} - -func TestToGVR(t *testing.T) { - uu := map[string]struct { - gv, r, e string - }{ - "full": {"apps/v1", "deployments", "apps/v1/deployments"}, - "core": {"v1", "pods", "v1/pods"}, - } - - for k, u := range uu { - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, k8s.ToGVR(u.gv, u.r).String()) - }) - } -} - -func TestResName(t *testing.T) { - uu := map[string]struct { - gvr string - e string - }{ - "full": {"apps/v1/deployments", "deployments.v1.apps"}, - "core": {"v1/pods", "pods.v1."}, - "k9s": {"users", "users.."}, - "empty": {"", ".."}, - } - - for k, u := range uu { - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, k8s.GVR(u.gvr).ResName()) - }) - } -} - -func TestToR(t *testing.T) { - uu := map[string]struct { - gvr string - e string - }{ - "full": {"apps/v1/deployments", "deployments"}, - "core": {"v1/pods", "pods"}, - "k9s": {"users", "users"}, - "empty": {"", ""}, - } - - for k, u := range uu { - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, k8s.GVR(u.gvr).ToR()) - }) - } -} - -func TestToG(t *testing.T) { - uu := map[string]struct { - gvr string - e string - }{ - "full": {"apps/v1/deployments", "apps"}, - "core": {"v1/pods", ""}, - "k9s": {"users", ""}, - "empty": {"", ""}, - } - - for k, u := range uu { - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, k8s.GVR(u.gvr).ToG()) - }) - } -} - -func TestToV(t *testing.T) { - uu := map[string]struct { - gvr string - e string - }{ - "full": {"apps/v1/deployments", "v1"}, - "core": {"v1beta1/pods", "v1beta1"}, - "k9s": {"users", ""}, - "empty": {"", ""}, - } - - for k, u := range uu { - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, k8s.GVR(u.gvr).ToV()) - }) - } -} - -func TestToStringer(t *testing.T) { - uu := map[string]struct { - gvr string - }{ - "full": {"apps/v1/deployments"}, - "core": {"v1beta1/pods"}, - "k9s": {"users"}, - "empty": {""}, - } - - for k, u := range uu { - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.gvr, k8s.GVR(u.gvr).String()) - }) - } -} diff --git a/internal/k8s/helpers.go b/internal/k8s/helpers.go deleted file mode 100644 index ca946311..00000000 --- a/internal/k8s/helpers.go +++ /dev/null @@ -1,27 +0,0 @@ -package k8s - -import ( - "math" - "path" - "strings" -) - -const megaByte = 1024 * 1024 - -// ToMB converts bytes to megabytes. -func ToMB(v int64) float64 { - return float64(v) / megaByte -} - -func toPerc(v1, v2 float64) float64 { - if v2 == 0 { - return 0 - } - return math.Round((v1 / v2) * 100) -} - -func namespaced(n string) (string, string) { - ns, po := path.Split(n) - - return strings.Trim(ns, "/"), po -} diff --git a/internal/k8s/hpa_v1.go b/internal/k8s/hpa_v1.go deleted file mode 100644 index c5302b33..00000000 --- a/internal/k8s/hpa_v1.go +++ /dev/null @@ -1,40 +0,0 @@ -package k8s - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// HorizontalPodAutoscalerV1 represents am HorizontalPodAutoscaler. -type HorizontalPodAutoscalerV1 struct { - *base - Connection -} - -// NewHorizontalPodAutoscalerV1 returns a new HorizontalPodAutoscaler. -func NewHorizontalPodAutoscalerV1(c Connection) *HorizontalPodAutoscalerV1 { - return &HorizontalPodAutoscalerV1{&base{}, c} -} - -// Get a HorizontalPodAutoscaler. -func (h *HorizontalPodAutoscalerV1) Get(ns, n string) (interface{}, error) { - return h.DialOrDie().AutoscalingV1().HorizontalPodAutoscalers(ns).Get(n, metav1.GetOptions{}) -} - -// List all HorizontalPodAutoscalers in a given namespace. -func (h *HorizontalPodAutoscalerV1) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := h.DialOrDie().AutoscalingV1().HorizontalPodAutoscalers(ns).List(opts) - if err != nil { - return nil, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - - return cc, nil -} - -// Delete a HorizontalPodAutoscaler. -func (h *HorizontalPodAutoscalerV1) Delete(ns, n string, cascade, force bool) error { - return h.DialOrDie().AutoscalingV1().HorizontalPodAutoscalers(ns).Delete(n, nil) -} diff --git a/internal/k8s/hpa_v2beta1.go b/internal/k8s/hpa_v2beta1.go deleted file mode 100644 index 30e0ab9e..00000000 --- a/internal/k8s/hpa_v2beta1.go +++ /dev/null @@ -1,41 +0,0 @@ -package k8s - -import ( - "github.com/rs/zerolog/log" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// HorizontalPodAutoscalerV2Beta1 represents am HorizontalPodAutoscaler. -type HorizontalPodAutoscalerV2Beta1 struct { - *base - Connection -} - -// NewHorizontalPodAutoscalerV2Beta1 returns a new HorizontalPodAutoscaler. -func NewHorizontalPodAutoscalerV2Beta1(c Connection) *HorizontalPodAutoscalerV2Beta1 { - return &HorizontalPodAutoscalerV2Beta1{&base{}, c} -} - -// Get a HorizontalPodAutoscaler. -func (h *HorizontalPodAutoscalerV2Beta1) Get(ns, n string) (interface{}, error) { - return h.DialOrDie().AutoscalingV2beta1().HorizontalPodAutoscalers(ns).Get(n, metav1.GetOptions{}) -} - -// List all HorizontalPodAutoscalers in a given namespace. -func (h *HorizontalPodAutoscalerV2Beta1) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := h.DialOrDie().AutoscalingV2beta1().HorizontalPodAutoscalers(ns).List(opts) - if err != nil { - log.Error().Err(err).Msg("Beta1 Failed!") - return nil, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - return cc, nil -} - -// Delete a HorizontalPodAutoscaler. -func (h *HorizontalPodAutoscalerV2Beta1) Delete(ns, n string, cascade, force bool) error { - return h.DialOrDie().AutoscalingV2beta1().HorizontalPodAutoscalers(ns).Delete(n, nil) -} diff --git a/internal/k8s/hpa_v2beta2.go b/internal/k8s/hpa_v2beta2.go deleted file mode 100644 index aed5892f..00000000 --- a/internal/k8s/hpa_v2beta2.go +++ /dev/null @@ -1,41 +0,0 @@ -package k8s - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -var supportedAutoScalingAPIVersions = []string{"v2beta2", "v2beta1", "v1"} - -// HorizontalPodAutoscalerV2Beta2 represents am HorizontalPodAutoscaler. -type HorizontalPodAutoscalerV2Beta2 struct { - *base - Connection -} - -// NewHorizontalPodAutoscalerV2Beta2 returns a new HorizontalPodAutoscalerV2Beta2. -func NewHorizontalPodAutoscalerV2Beta2(c Connection) *HorizontalPodAutoscalerV2Beta2 { - return &HorizontalPodAutoscalerV2Beta2{&base{}, c} -} - -// Get a HorizontalPodAutoscalerV2Beta2. -func (h *HorizontalPodAutoscalerV2Beta2) Get(ns, n string) (interface{}, error) { - return h.DialOrDie().AutoscalingV2beta2().HorizontalPodAutoscalers(ns).Get(n, metav1.GetOptions{}) -} - -// List all HorizontalPodAutoscalerV2Beta2s in a given namespace. -func (h *HorizontalPodAutoscalerV2Beta2) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := h.DialOrDie().AutoscalingV2beta2().HorizontalPodAutoscalers(ns).List(opts) - if err != nil { - return nil, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - return cc, nil -} - -// Delete a HorizontalPodAutoscalerV2Beta2. -func (h *HorizontalPodAutoscalerV2Beta2) Delete(ns, n string, cascade, force bool) error { - return h.DialOrDie().AutoscalingV2beta2().HorizontalPodAutoscalers(ns).Delete(n, nil) -} diff --git a/internal/k8s/ing.go b/internal/k8s/ing.go deleted file mode 100644 index cd506e86..00000000 --- a/internal/k8s/ing.go +++ /dev/null @@ -1,40 +0,0 @@ -package k8s - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// Ingress represents a Kubernetes Ingress. -type Ingress struct { - *base - Connection -} - -// NewIngress returns a new Ingress. -func NewIngress(c Connection) *Ingress { - return &Ingress{&base{}, c} -} - -// Get a Ingress. -func (i *Ingress) Get(ns, n string) (interface{}, error) { - return i.DialOrDie().ExtensionsV1beta1().Ingresses(ns).Get(n, metav1.GetOptions{}) -} - -// List all Ingresses in a given namespace. -func (i *Ingress) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := i.DialOrDie().ExtensionsV1beta1().Ingresses(ns).List(opts) - if err != nil { - return nil, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - - return cc, nil -} - -// Delete a Ingress. -func (i *Ingress) Delete(ns, n string, cascade, force bool) error { - return i.DialOrDie().ExtensionsV1beta1().Ingresses(ns).Delete(n, nil) -} diff --git a/internal/k8s/job.go b/internal/k8s/job.go deleted file mode 100644 index bace25a0..00000000 --- a/internal/k8s/job.go +++ /dev/null @@ -1,94 +0,0 @@ -package k8s - -import ( - "fmt" - "strings" - - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - restclient "k8s.io/client-go/rest" -) - -type ( - // Job represents a Kubernetes Job. - Job struct { - *base - Connection - } - - // Loggable represents a K8s resource that has containers and can be logged. - Loggable interface { - Containers(ns, n string, includeInit bool) ([]string, error) - Logs(ns, n string, opts *v1.PodLogOptions) *restclient.Request - } -) - -// NewJob returns a new Job. -func NewJob(c Connection) *Job { - return &Job{&base{}, c} -} - -// Get a Job. -func (j *Job) Get(ns, n string) (interface{}, error) { - return j.DialOrDie().BatchV1().Jobs(ns).Get(n, metav1.GetOptions{}) -} - -// List all Jobs in a given namespace. -func (j *Job) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := j.DialOrDie().BatchV1().Jobs(ns).List(opts) - if err != nil { - return nil, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - - return cc, nil -} - -// Delete a Job. -func (j *Job) Delete(ns, n string, cascade, force bool) error { - return j.DialOrDie().BatchV1().Jobs(ns).Delete(n, nil) -} - -// Containers returns all container names on job. -func (j *Job) Containers(ns, n string, includeInit bool) ([]string, error) { - pod, err := j.assocPod(ns, n) - if err != nil { - return nil, err - } - return NewPod(j).Containers(ns, pod, includeInit) -} - -// Logs fetch container logs for a given job and container. -func (j *Job) Logs(ns, n string, opts *v1.PodLogOptions) *restclient.Request { - pod, err := j.assocPod(ns, n) - if err != nil { - return nil - } - - return NewPod(j).Logs(ns, pod, opts) -} - -// Events retrieved jobs events. -func (j *Job) Events(ns, n string) (*v1.EventList, error) { - e := j.DialOrDie().CoreV1().Events(ns) - return e.List(metav1.ListOptions{ - FieldSelector: e.GetFieldSelector(&n, &ns, nil, nil).String(), - }) -} - -func (j *Job) assocPod(ns, n string) (string, error) { - ee, err := j.Events(ns, n) - if err != nil { - return "", err - } - - for _, e := range ee.Items { - if strings.Contains(e.Message, "Created pod: ") { - return strings.TrimSpace(strings.Replace(e.Message, "Created pod: ", "", 1)), nil - } - } - return "", fmt.Errorf("unable to find associated pod name for job: %s/%s", ns, n) -} diff --git a/internal/k8s/no.go b/internal/k8s/no.go deleted file mode 100644 index f75793f8..00000000 --- a/internal/k8s/no.go +++ /dev/null @@ -1,40 +0,0 @@ -package k8s - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// Node represents a Kubernetes node. -type Node struct { - *base - Connection -} - -// NewNode returns a new Node. -func NewNode(c Connection) *Node { - return &Node{&base{}, c} -} - -// Get a node. -func (n *Node) Get(_, name string) (interface{}, error) { - return n.DialOrDie().CoreV1().Nodes().Get(name, metav1.GetOptions{}) -} - -// List all nodes on the cluster. -func (n *Node) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := n.DialOrDie().CoreV1().Nodes().List(opts) - if err != nil { - return nil, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - - return cc, nil -} - -// Delete a node. -func (n *Node) Delete(_, name string, cascade, force bool) error { - return n.DialOrDie().CoreV1().Nodes().Delete(name, nil) -} diff --git a/internal/k8s/np.go b/internal/k8s/np.go deleted file mode 100644 index d91fc9e9..00000000 --- a/internal/k8s/np.go +++ /dev/null @@ -1,46 +0,0 @@ -package k8s - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// NetworkPolicy represents a Kubernetes NetworkPolicy -type NetworkPolicy struct { - *base - Connection -} - -// NewNetworkPolicy returns a new NetworkPolicy. -func NewNetworkPolicy(c Connection) *NetworkPolicy { - return &NetworkPolicy{&base{}, c} -} - -// Get a NetworkPolicy. -func (d *NetworkPolicy) Get(ns, n string) (interface{}, error) { - return d.DialOrDie().NetworkingV1().NetworkPolicies(ns).Get(n, metav1.GetOptions{}) -} - -// List all NetworkPolicys in a given namespace. -func (d *NetworkPolicy) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := d.DialOrDie().NetworkingV1().NetworkPolicies(ns).List(opts) - if err != nil { - return nil, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - - return cc, nil -} - -// Delete a NetworkPolicy. -func (d *NetworkPolicy) Delete(ns, n string, cascade, force bool) error { - p := metav1.DeletePropagationOrphan - if cascade { - p = metav1.DeletePropagationBackground - } - return d.DialOrDie().NetworkingV1().NetworkPolicies(ns).Delete(n, &metav1.DeleteOptions{ - PropagationPolicy: &p, - }) -} diff --git a/internal/k8s/ns.go b/internal/k8s/ns.go deleted file mode 100644 index 04baeb8b..00000000 --- a/internal/k8s/ns.go +++ /dev/null @@ -1,39 +0,0 @@ -package k8s - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// Namespace represents a Kubernetes namespace. -type Namespace struct { - *base - Connection -} - -// NewNamespace returns a new Namespace. -func NewNamespace(c Connection) *Namespace { - return &Namespace{&base{}, c} -} - -// Get a active namespace. -func (n *Namespace) Get(_, name string) (interface{}, error) { - return n.DialOrDie().CoreV1().Namespaces().Get(name, metav1.GetOptions{}) -} - -// List all active namespaces on the cluster. -func (n *Namespace) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := n.DialOrDie().CoreV1().Namespaces().List(opts) - if err != nil { - return nil, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - return cc, nil -} - -// Delete a namespace. -func (n *Namespace) Delete(_, name string, cascade, force bool) error { - return n.DialOrDie().CoreV1().Namespaces().Delete(name, nil) -} diff --git a/internal/k8s/pdb.go b/internal/k8s/pdb.go deleted file mode 100644 index 22815983..00000000 --- a/internal/k8s/pdb.go +++ /dev/null @@ -1,40 +0,0 @@ -package k8s - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// PodDisruptionBudget represents a Kubernetes PodDisruptionBudget. -type PodDisruptionBudget struct { - *base - Connection -} - -// NewPodDisruptionBudget returns a new PodDisruptionBudget. -func NewPodDisruptionBudget(c Connection) *PodDisruptionBudget { - return &PodDisruptionBudget{&base{}, c} -} - -// Get a pdb. -func (p *PodDisruptionBudget) Get(ns, n string) (interface{}, error) { - return p.DialOrDie().PolicyV1beta1().PodDisruptionBudgets(ns).Get(n, metav1.GetOptions{}) -} - -// List all pdbs in a given namespace. -func (p *PodDisruptionBudget) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := p.DialOrDie().PolicyV1beta1().PodDisruptionBudgets(ns).List(opts) - if err != nil { - return nil, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - - return cc, nil -} - -// Delete a pdb. -func (p *PodDisruptionBudget) Delete(ns, n string, cascade, force bool) error { - return p.DialOrDie().PolicyV1beta1().PodDisruptionBudgets(ns).Delete(n, nil) -} diff --git a/internal/k8s/pod.go b/internal/k8s/pod.go deleted file mode 100644 index 44c0bf01..00000000 --- a/internal/k8s/pod.go +++ /dev/null @@ -1,78 +0,0 @@ -package k8s - -import ( - "github.com/rs/zerolog/log" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - restclient "k8s.io/client-go/rest" -) - -const defaultKillGrace int64 = 5 - -// Pod represents a Kubernetes Pod. -type Pod struct { - *base - Connection -} - -// NewPod returns a new Pod. -func NewPod(c Connection) *Pod { - return &Pod{base: &base{}, Connection: c} -} - -// Get a pod. -func (p *Pod) Get(ns, name string) (interface{}, error) { - return p.DialOrDie().CoreV1().Pods(ns).Get(name, metav1.GetOptions{}) -} - -// List all pods in a given namespace. -func (p *Pod) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := p.DialOrDie().CoreV1().Pods(ns).List(opts) - if err != nil { - return nil, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - - return cc, nil -} - -// Delete a pod. -func (p *Pod) Delete(ns, n string, cascade, force bool) error { - log.Debug().Msgf("Killing Pod %s %t:%t", n, cascade, force) - grace := defaultKillGrace - if force { - grace = 0 - } - return p.DialOrDie().CoreV1().Pods(ns).Delete(n, &metav1.DeleteOptions{ - GracePeriodSeconds: &grace, - }) -} - -// Containers returns all container names on pod -func (p *Pod) Containers(ns, n string, includeInit bool) ([]string, error) { - po, err := p.DialOrDie().CoreV1().Pods(ns).Get(n, metav1.GetOptions{}) - if err != nil { - return nil, err - } - - cc := []string{} - for _, c := range po.Spec.Containers { - cc = append(cc, c.Name) - } - - if includeInit { - for _, c := range po.Spec.InitContainers { - cc = append(cc, c.Name) - } - } - - return cc, nil -} - -// Logs fetch container logs for a given pod and container. -func (p *Pod) Logs(ns, n string, opts *v1.PodLogOptions) *restclient.Request { - return p.DialOrDie().CoreV1().Pods(ns).GetLogs(n, opts) -} diff --git a/internal/k8s/pv.go b/internal/k8s/pv.go deleted file mode 100644 index ff2d628a..00000000 --- a/internal/k8s/pv.go +++ /dev/null @@ -1,41 +0,0 @@ -package k8s - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// PersistentVolume represents a Kubernetes PersistentVolume. -type PersistentVolume struct { - *base - Connection -} - -// NewPersistentVolume returns a new PersistentVolume. -func NewPersistentVolume(c Connection) *PersistentVolume { - return &PersistentVolume{&base{}, c} -} - -// Get a PersistentVolume. -func (p *PersistentVolume) Get(_, n string) (interface{}, error) { - return p.DialOrDie().CoreV1().PersistentVolumes().Get(n, metav1.GetOptions{}) -} - -// List all PersistentVolumes in a given namespace. -func (p *PersistentVolume) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := p.DialOrDie().CoreV1().PersistentVolumes().List(opts) - if err != nil { - return nil, err - } - - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - - return cc, nil -} - -// Delete a PersistentVolume. -func (p *PersistentVolume) Delete(_, n string, cascade, force bool) error { - return p.DialOrDie().CoreV1().PersistentVolumes().Delete(n, nil) -} diff --git a/internal/k8s/pvc.go b/internal/k8s/pvc.go deleted file mode 100644 index 90e447ea..00000000 --- a/internal/k8s/pvc.go +++ /dev/null @@ -1,40 +0,0 @@ -package k8s - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// PersistentVolumeClaim represents a Kubernetes PersistentVolumeClaim. -type PersistentVolumeClaim struct { - *base - Connection -} - -// NewPersistentVolumeClaim returns a new PersistentVolumeClaim. -func NewPersistentVolumeClaim(c Connection) *PersistentVolumeClaim { - return &PersistentVolumeClaim{&base{}, c} -} - -// Get a PersistentVolumeClaim. -func (p *PersistentVolumeClaim) Get(ns, n string) (interface{}, error) { - return p.DialOrDie().CoreV1().PersistentVolumeClaims(ns).Get(n, metav1.GetOptions{}) -} - -// List all PersistentVolumeClaims in a given namespace. -func (p *PersistentVolumeClaim) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := p.DialOrDie().CoreV1().PersistentVolumeClaims(ns).List(opts) - if err != nil { - return nil, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - - return cc, nil -} - -// Delete a PersistentVolumeClaim. -func (p *PersistentVolumeClaim) Delete(ns, n string, cascade, force bool) error { - return p.DialOrDie().CoreV1().PersistentVolumeClaims(ns).Delete(n, nil) -} diff --git a/internal/k8s/rc.go b/internal/k8s/rc.go deleted file mode 100644 index dc050ce2..00000000 --- a/internal/k8s/rc.go +++ /dev/null @@ -1,58 +0,0 @@ -package k8s - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// ReplicationController represents a Kubernetes ReplicationController. -type ReplicationController struct { - *base - Connection -} - -// NewReplicationController returns a new ReplicationController. -func NewReplicationController(c Connection) *ReplicationController { - return &ReplicationController{&base{}, c} -} - -// Get a RC. -func (r *ReplicationController) Get(ns, n string) (interface{}, error) { - return r.DialOrDie().CoreV1().ReplicationControllers(ns).Get(n, metav1.GetOptions{}) -} - -// List all RCs in a given namespace. -func (r *ReplicationController) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := r.DialOrDie().CoreV1().ReplicationControllers(ns).List(opts) - if err != nil { - return nil, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - - return cc, nil -} - -// Delete a RC. -func (r *ReplicationController) Delete(ns, n string, cascade, force bool) error { - p := metav1.DeletePropagationOrphan - if cascade { - p = metav1.DeletePropagationBackground - } - return r.DialOrDie().CoreV1().ReplicationControllers(ns).Delete(n, &metav1.DeleteOptions{ - PropagationPolicy: &p, - }) -} - -// Scale a ReplicationController. -func (r *ReplicationController) Scale(ns, n string, replicas int32) error { - scale, err := r.DialOrDie().CoreV1().ReplicationControllers(ns).GetScale(n, metav1.GetOptions{}) - if err != nil { - return err - } - - scale.Spec.Replicas = replicas - _, err = r.DialOrDie().CoreV1().ReplicationControllers(ns).UpdateScale(n, scale) - return err -} diff --git a/internal/k8s/resource.go b/internal/k8s/resource.go deleted file mode 100644 index ec5874c6..00000000 --- a/internal/k8s/resource.go +++ /dev/null @@ -1,103 +0,0 @@ -package k8s - -import ( - "fmt" - - 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/serializer" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/rest" -) - -// Resource represents a Kubernetes Resource -type Resource struct { - *base - Connection - - gvr GVR -} - -// NewResource returns a new Resource. -func NewResource(c Connection, gvr GVR) *Resource { - return &Resource{base: &base{}, Connection: c, gvr: gvr} -} - -// GetInfo returns info about apigroup. -func (r *Resource) GetInfo() GVR { - return r.gvr -} - -func (r *Resource) nsRes() dynamic.NamespaceableResourceInterface { - return r.DynDialOrDie().Resource(r.gvr.AsGVR()) -} - -// Get a Resource. -func (r *Resource) Get(ns, n string) (interface{}, error) { - return r.nsRes().Namespace(ns).Get(n, metav1.GetOptions{}) -} - -// List all Resources in a given namespace. -func (r *Resource) List(ns string, opts metav1.ListOptions) (Collection, error) { - obj, err := r.listAll(ns, r.gvr.ToR()) - if err != nil { - return nil, err - } - return Collection{obj.(*metav1beta1.Table)}, nil -} - -// Delete a Resource. -func (r *Resource) Delete(ns, n string, cascade, force bool) error { - return r.nsRes().Namespace(ns).Delete(n, nil) -} - -// ---------------------------------------------------------------------------- -// Helpers... - -const gvFmt = "application/json;as=Table;v=%s;g=%s, application/json" - -func (r *Resource) listAll(ns, n string) (runtime.Object, error) { - a := fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName) - _, codec := r.codec() - - c, err := r.getClient() - if err != nil { - return nil, err - } - - return c.Get(). - SetHeader("Accept", a). - Namespace(ns). - Resource(n). - VersionedParams(&metav1beta1.TableOptions{}, codec). - Do().Get() -} - -func (r *Resource) getClient() (*rest.RESTClient, error) { - crConfig := r.RestConfigOrDie() - gv := r.gvr.AsGR() - crConfig.GroupVersion = &gv - crConfig.APIPath = "/apis" - if len(r.gvr.ToG()) == 0 { - crConfig.APIPath = "/api" - } - codec, _ := r.codec() - crConfig.NegotiatedSerializer = codec.WithoutConversion() - - crRestClient, err := rest.RESTClientFor(crConfig) - if err != nil { - return nil, err - } - return crRestClient, nil -} - -func (r *Resource) codec() (serializer.CodecFactory, runtime.ParameterCodec) { - scheme := runtime.NewScheme() - gv := r.gvr.AsGR() - metav1.AddToGroupVersion(scheme, gv) - scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) - scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) - - return serializer.NewCodecFactory(scheme), runtime.NewParameterCodec(scheme) -} diff --git a/internal/k8s/role.go b/internal/k8s/role.go deleted file mode 100644 index 011a8cda..00000000 --- a/internal/k8s/role.go +++ /dev/null @@ -1,41 +0,0 @@ -package k8s - -import ( - // rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// Role represents a Kubernetes Role. -type Role struct { - *base - Connection -} - -// NewRole returns a new Role. -func NewRole(c Connection) *Role { - return &Role{&base{}, c} -} - -// Get a Role. -func (r *Role) Get(ns, n string) (interface{}, error) { - return r.DialOrDie().RbacV1().Roles(ns).Get(n, metav1.GetOptions{}) -} - -// List all Roles in a given namespace. -func (r *Role) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := r.DialOrDie().RbacV1().Roles(ns).List(opts) - if err != nil { - return nil, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - - return cc, nil -} - -// Delete a Role. -func (r *Role) Delete(ns, n string, cascade, force bool) error { - return r.DialOrDie().RbacV1().Roles(ns).Delete(n, nil) -} diff --git a/internal/k8s/role_binding.go b/internal/k8s/role_binding.go deleted file mode 100644 index 1d5f24b2..00000000 --- a/internal/k8s/role_binding.go +++ /dev/null @@ -1,38 +0,0 @@ -package k8s - -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - -// RoleBinding represents a Kubernetes RoleBinding. -type RoleBinding struct { - *base - Connection -} - -// NewRoleBinding returns a new RoleBinding. -func NewRoleBinding(c Connection) *RoleBinding { - return &RoleBinding{&base{}, c} -} - -// Get a RoleBinding. -func (r *RoleBinding) Get(ns, n string) (interface{}, error) { - return r.DialOrDie().RbacV1().RoleBindings(ns).Get(n, metav1.GetOptions{}) -} - -// List all RoleBindings in a given namespace. -func (r *RoleBinding) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := r.DialOrDie().RbacV1().RoleBindings(ns).List(opts) - if err != nil { - return nil, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - - return cc, nil -} - -// Delete a RoleBinding. -func (r *RoleBinding) Delete(ns, n string, cascade, force bool) error { - return r.DialOrDie().RbacV1().RoleBindings(ns).Delete(n, nil) -} diff --git a/internal/k8s/rs.go b/internal/k8s/rs.go deleted file mode 100644 index bb3843a1..00000000 --- a/internal/k8s/rs.go +++ /dev/null @@ -1,40 +0,0 @@ -package k8s - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// ReplicaSet represents a Kubernetes ReplicaSet. -type ReplicaSet struct { - *base - Connection -} - -// NewReplicaSet returns a new ReplicaSet. -func NewReplicaSet(c Connection) *ReplicaSet { - return &ReplicaSet{&base{}, c} -} - -// Get a ReplicaSet. -func (r *ReplicaSet) Get(ns, n string) (interface{}, error) { - return r.DialOrDie().AppsV1().ReplicaSets(ns).Get(n, metav1.GetOptions{}) -} - -// List all ReplicaSets in a given namespace. -func (r *ReplicaSet) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := r.DialOrDie().AppsV1().ReplicaSets(ns).List(opts) - if err != nil { - return nil, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - - return cc, nil -} - -// Delete a ReplicaSet. -func (r *ReplicaSet) Delete(ns, n string, cascade, force bool) error { - return r.DialOrDie().AppsV1().ReplicaSets(ns).Delete(n, nil) -} diff --git a/internal/k8s/sa.go b/internal/k8s/sa.go deleted file mode 100644 index 95334d58..00000000 --- a/internal/k8s/sa.go +++ /dev/null @@ -1,40 +0,0 @@ -package k8s - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// ServiceAccount manages a Kubernetes ServiceAccount. -type ServiceAccount struct { - *base - Connection -} - -// NewServiceAccount instantiates a new ServiceAccount. -func NewServiceAccount(c Connection) *ServiceAccount { - return &ServiceAccount{&base{}, c} -} - -// Get a ServiceAccount. -func (s *ServiceAccount) Get(ns, n string) (interface{}, error) { - return s.DialOrDie().CoreV1().ServiceAccounts(ns).Get(n, metav1.GetOptions{}) -} - -// List all ServiceAccounts in a given namespace. -func (s *ServiceAccount) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := s.DialOrDie().CoreV1().ServiceAccounts(ns).List(opts) - if err != nil { - return nil, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - return cc, nil - -} - -// Delete a ServiceAccount. -func (s *ServiceAccount) Delete(ns, n string, cascade, force bool) error { - return s.DialOrDie().CoreV1().ServiceAccounts(ns).Delete(n, nil) -} diff --git a/internal/k8s/sc.go b/internal/k8s/sc.go deleted file mode 100644 index c7fec730..00000000 --- a/internal/k8s/sc.go +++ /dev/null @@ -1,41 +0,0 @@ -package k8s - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// StorageClass represents a Kubernetes StorageClass. -type StorageClass struct { - *base - Connection -} - -// NewStorageClass returns a new StorageClass. -func NewStorageClass(c Connection) *StorageClass { - return &StorageClass{&base{}, c} -} - -// Get a StorageClass. -func (p *StorageClass) Get(_, n string) (interface{}, error) { - return p.DialOrDie().StorageV1().StorageClasses().Get(n, metav1.GetOptions{}) -} - -// List all StorageClasses in a given namespace. -func (p *StorageClass) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := p.DialOrDie().StorageV1().StorageClasses().List(opts) - if err != nil { - return nil, err - } - - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - - return cc, nil -} - -// Delete a StorageClass. -func (p *StorageClass) Delete(_, n string, cascade, force bool) error { - return p.DialOrDie().StorageV1().StorageClasses().Delete(n, nil) -} diff --git a/internal/k8s/sts.go b/internal/k8s/sts.go deleted file mode 100644 index f032d109..00000000 --- a/internal/k8s/sts.go +++ /dev/null @@ -1,76 +0,0 @@ -package k8s - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/kubectl/pkg/polymorphichelpers" -) - -// StatefulSet manages a Kubernetes StatefulSet. -type StatefulSet struct { - *base - Connection -} - -// NewStatefulSet instantiates a new StatefulSet. -func NewStatefulSet(c Connection) *StatefulSet { - return &StatefulSet{&base{}, c} -} - -// Get a StatefulSet. -func (s *StatefulSet) Get(ns, n string) (interface{}, error) { - return s.DialOrDie().AppsV1().StatefulSets(ns).Get(n, metav1.GetOptions{}) -} - -// List all StatefulSets in a given namespace. -func (s *StatefulSet) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := s.DialOrDie().AppsV1().StatefulSets(ns).List(opts) - if err != nil { - return nil, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - - return cc, nil -} - -// Delete a StatefulSet. -func (s *StatefulSet) Delete(ns, n string, cascade, force bool) error { - p := metav1.DeletePropagationOrphan - if cascade { - p = metav1.DeletePropagationBackground - } - return s.DialOrDie().AppsV1().StatefulSets(ns).Delete(n, &metav1.DeleteOptions{ - PropagationPolicy: &p, - }) -} - -// Scale a StatefulSet. -func (s *StatefulSet) Scale(ns, n string, replicas int32) error { - scale, err := s.DialOrDie().AppsV1().StatefulSets(ns).GetScale(n, metav1.GetOptions{}) - if err != nil { - return err - } - - scale.Spec.Replicas = replicas - _, err = s.DialOrDie().AppsV1().StatefulSets(ns).UpdateScale(n, scale) - return err -} - -// Restart a StatefulSet rollout. -func (s *StatefulSet) Restart(ns, n string) error { - - sts, err := s.DialOrDie().AppsV1().StatefulSets(ns).Get(n, metav1.GetOptions{}) - if err != nil { - return err - } - update, err := polymorphichelpers.ObjectRestarterFn(sts) - if err != nil { - return err - } - - _, err = s.DialOrDie().AppsV1().StatefulSets(ns).Patch(sts.Name, types.StrategicMergePatchType, update) - return err -} diff --git a/internal/k8s/svc.go b/internal/k8s/svc.go deleted file mode 100644 index 6a570e78..00000000 --- a/internal/k8s/svc.go +++ /dev/null @@ -1,40 +0,0 @@ -package k8s - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// Service represents a Kubernetes Service. -type Service struct { - *base - Connection -} - -// NewService returns a new Service. -func NewService(c Connection) *Service { - return &Service{&base{}, c} -} - -// Get a service. -func (s *Service) Get(ns, n string) (interface{}, error) { - return s.DialOrDie().CoreV1().Services(ns).Get(n, metav1.GetOptions{}) -} - -// List all Services in a given namespace. -func (s *Service) List(ns string, opts metav1.ListOptions) (Collection, error) { - rr, err := s.DialOrDie().CoreV1().Services(ns).List(opts) - if err != nil { - return nil, err - } - cc := make(Collection, len(rr.Items)) - for i, r := range rr.Items { - cc[i] = r - } - - return cc, nil -} - -// Delete a Service. -func (s *Service) Delete(ns, n string, cascade, force bool) error { - return s.DialOrDie().CoreV1().Services(ns).Delete(n, nil) -} diff --git a/internal/keys.go b/internal/keys.go new file mode 100644 index 00000000..f02d55e4 --- /dev/null +++ b/internal/keys.go @@ -0,0 +1,27 @@ +package internal + +// ContextKey represents context key. +type ContextKey string + +// A collection of context keys. +const ( + KeyFactory ContextKey = "factory" + KeyLabels ContextKey = "labels" + KeyFields ContextKey = "fields" + KeyTable ContextKey = "table" + KeyDir ContextKey = "dir" + KeyPath ContextKey = "path" + KeySubject ContextKey = "subject" + KeyGVR ContextKey = "gvr" + KeyForwards ContextKey = "forwards" + KeyContainers ContextKey = "containers" + KeyBenchCfg ContextKey = "benchcfg" + KeyAliases ContextKey = "aliases" + KeyUID ContextKey = "uid" + KeySubjectKind ContextKey = "subjectKind" + KeySubjectName ContextKey = "subjectName" + KeyNamespace ContextKey = "namespace" + KeyCluster ContextKey = "cluster" + KeyApp ContextKey = "app" + KeyStyles ContextKey = "styles" +) diff --git a/internal/model/alias.go b/internal/model/alias.go new file mode 100644 index 00000000..a417621b --- /dev/null +++ b/internal/model/alias.go @@ -0,0 +1,43 @@ +package model + +import ( + "context" + "errors" + "sort" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/render" + "k8s.io/apimachinery/pkg/runtime" +) + +// Alias represents a collection of aliases. +type Alias struct { + Resource +} + +// List returns a collection of screen dumps. +func (b *Alias) List(ctx context.Context) ([]runtime.Object, error) { + a, ok := ctx.Value(internal.KeyAliases).(*dao.Alias) + if !ok { + return nil, errors.New("no aliases found in context") + } + + m := make(config.ShortNames, len(a.Alias)) + for alias, gvr := range a.Alias { + if _, ok := m[gvr]; ok { + m[gvr] = append(m[gvr], alias) + } else { + m[gvr] = []string{alias} + } + } + + 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}) + } + + return oo, nil +} diff --git a/internal/model/alias_test.go b/internal/model/alias_test.go new file mode 100644 index 00000000..aa886515 --- /dev/null +++ b/internal/model/alias_test.go @@ -0,0 +1,87 @@ +package model_test + +import ( + "context" + "testing" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "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/watch" + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/informers" +) + +func TestAliasList(t *testing.T) { + a := model.Alias{} + a.Init(render.ClusterScope, "aliases", makeFactory()) + + ctx := context.WithValue(context.Background(), internal.KeyAliases, makeAliases()) + oo, err := a.List(ctx) + + assert.Nil(t, err) + assert.Equal(t, 2, len(oo)) + assert.Equal(t, 2, len(oo[0].(render.AliasRes).Aliases)) +} + +func TestAliasHydrate(t *testing.T) { + a := model.Alias{} + a.Init(render.ClusterScope, "aliases", makeFactory()) + + ctx := context.WithValue(context.Background(), internal.KeyAliases, makeAliases()) + oo, err := a.List(ctx) + assert.Nil(t, err) + + rr := make(render.Rows, len(oo)) + assert.Nil(t, a.Hydrate(oo, rr, render.Alias{})) + assert.Equal(t, 2, len(rr)) +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func makeAliases() *dao.Alias { + return &dao.Alias{ + Aliases: config.Aliases{ + Alias: config.Alias{ + "fred": "v1/fred", + "f": "v1/fred", + "blee": "v1/blee", + "b": "v1/blee", + }, + }, + } +} + +type testFactory struct{} + +var _ model.Factory = testFactory{} + +func (f testFactory) Client() client.Connection { + return nil +} +func (f testFactory) Get(gvr, path string, sel labels.Selector) (runtime.Object, error) { + return nil, nil +} +func (f testFactory) List(gvr, ns string, sel labels.Selector) ([]runtime.Object, error) { + return nil, nil +} +func (f testFactory) ForResource(ns, gvr string) informers.GenericInformer { + return nil +} +func (f testFactory) CanForResource(ns, gvr string, verbs ...string) (informers.GenericInformer, error) { + return nil, nil +} +func (f testFactory) WaitForCacheSync() {} +func (f testFactory) Forwarders() watch.Forwarders { + return nil +} + +func makeFactory() model.Factory { + return testFactory{} +} diff --git a/internal/model/benchmark.go b/internal/model/benchmark.go new file mode 100644 index 00000000..9b923254 --- /dev/null +++ b/internal/model/benchmark.go @@ -0,0 +1,37 @@ +package model + +import ( + "context" + "errors" + "io/ioutil" + "path/filepath" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/render" + "k8s.io/apimachinery/pkg/runtime" +) + +// Benchmark represents a collection of benchmarks. +type Benchmark struct { + Resource +} + +// List returns a collection of screen dumps. +func (b *Benchmark) List(ctx context.Context) ([]runtime.Object, error) { + dir, ok := ctx.Value(internal.KeyDir).(string) + if !ok { + return nil, errors.New("no benchmark dir found in context") + } + + ff, err := ioutil.ReadDir(dir) + if err != nil { + return nil, err + } + + oo := make([]runtime.Object, len(ff)) + for i, f := range ff { + oo[i] = render.BenchInfo{File: f, Path: filepath.Join(dir, f.Name())} + } + + return oo, nil +} diff --git a/internal/model/benchmark_test.go b/internal/model/benchmark_test.go new file mode 100644 index 00000000..9646d210 --- /dev/null +++ b/internal/model/benchmark_test.go @@ -0,0 +1,49 @@ +package model_test + +import ( + "context" + "testing" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestBenchmarkList(t *testing.T) { + a := model.Benchmark{} + a.Init(render.ClusterScope, "benchmarks", makeFactory()) + + ctx := context.WithValue(context.Background(), internal.KeyDir, "test_assets/bench") + oo, err := a.List(ctx) + + assert.Nil(t, err) + assert.Equal(t, 1, len(oo)) + assert.Equal(t, "test_assets/bench/default_fred_1577308050814961000.txt", oo[0].(render.BenchInfo).Path) +} + +func TestBenchmarkHydrate(t *testing.T) { + a := model.Benchmark{} + a.Init(render.ClusterScope, "benchmarks", makeFactory()) + + ctx := context.WithValue(context.Background(), internal.KeyDir, "test_assets/bench") + oo, err := a.List(ctx) + assert.Nil(t, err) + + rr := make(render.Rows, len(oo)) + assert.Nil(t, a.Hydrate(oo, rr, render.Benchmark{})) + assert.Equal(t, 1, len(rr)) + assert.Equal(t, "test_assets/bench/default_fred_1577308050814961000.txt", rr[0].ID) + assert.Equal(t, render.Fields{ + "default", + "fred", + "fail", + "816.6403", + "0.0122", + "0", + "0", + "default_fred_1577308050814961000.txt", + }, + rr[0].Fields[:len(rr[0].Fields)-1], + ) +} diff --git a/internal/resource/cluster.go b/internal/model/cluster.go similarity index 50% rename from internal/resource/cluster.go rename to internal/model/cluster.go index f9e69718..625b45ef 100644 --- a/internal/resource/cluster.go +++ b/internal/model/cluster.go @@ -1,31 +1,19 @@ -package resource +package model import ( - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog" + "github.com/derailed/k9s/internal/client" v1 "k8s.io/api/core/v1" mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) type ( - // ClusterMeta represents metadata about a Kubernetes cluster. - ClusterMeta interface { - Connection - - Version() (string, error) - ContextName() string - ClusterName() string - UserName() string - GetNodes() (*v1.NodeList, error) - } - // MetricsServer gather metrics information from pods and nodes. MetricsServer interface { MetricsService - ClusterLoad(nodes k8s.Collection, metrics k8s.Collection, cmx *k8s.ClusterMetrics) - NodesMetrics(k8s.Collection, *mv1beta1.NodeMetricsList, k8s.NodesMetrics) - PodsMetrics(*mv1beta1.PodMetricsList, k8s.PodsMetrics) + ClusterLoad(*v1.NodeList, *mv1beta1.NodeMetricsList, *client.ClusterMetrics) error + NodesMetrics(*v1.NodeList, *mv1beta1.NodeMetricsList, client.NodesMetrics) + PodsMetrics(*mv1beta1.PodMetricsList, client.PodsMetrics) } // MetricsService calls the metrics server for metrics info. @@ -37,47 +25,59 @@ type ( // Cluster represents a kubernetes resource. Cluster struct { - api ClusterMeta - mx MetricsServer + client client.Connection + mx MetricsServer } ) // NewCluster returns a new cluster info resource. -func NewCluster(c Connection, log *zerolog.Logger, mx MetricsServer) *Cluster { - return NewClusterWithArgs(k8s.NewCluster(c, log), mx) +func NewCluster(c client.Connection, mx MetricsServer) *Cluster { + return NewClusterWithArgs(c, mx) } // NewClusterWithArgs for tests only! -func NewClusterWithArgs(ci ClusterMeta, mx MetricsServer) *Cluster { - return &Cluster{api: ci, mx: mx} +func NewClusterWithArgs(c client.Connection, mx MetricsServer) *Cluster { + return &Cluster{client: c, mx: mx} } // Version returns the current K8s cluster version. func (c *Cluster) Version() string { - info, err := c.api.Version() + info, err := c.client.ServerVersion() if err != nil { return "n/a" } - return info + return info.GitVersion } // ContextName returns the context name. func (c *Cluster) ContextName() string { - return c.api.ContextName() + n, err := c.client.Config().CurrentContextName() + if err != nil { + return "n/a" + } + return n } // ClusterName returns the cluster name. func (c *Cluster) ClusterName() string { - return c.api.ClusterName() + n, err := c.client.Config().CurrentClusterName() + if err != nil { + return "n/a" + } + return n } // UserName returns the user name. func (c *Cluster) UserName() string { - return c.api.UserName() + n, err := c.client.Config().CurrentUserName() + if err != nil { + return "n/a" + } + return n } // Metrics gathers node level metrics and compute utilization percentages. -func (c *Cluster) Metrics(nos k8s.Collection, nmx k8s.Collection, mx *k8s.ClusterMetrics) { - c.mx.ClusterLoad(nos, nmx, mx) +func (c *Cluster) Metrics(nn *v1.NodeList, nmx *mv1beta1.NodeMetricsList, mx *client.ClusterMetrics) error { + return c.mx.ClusterLoad(nn, nmx, mx) } diff --git a/internal/model/container.go b/internal/model/container.go new file mode 100644 index 00000000..899955a7 --- /dev/null +++ b/internal/model/container.go @@ -0,0 +1,114 @@ +package model + +import ( + "context" + "fmt" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" + "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" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +// Container represents a container model. +type Container struct { + Resource + + pod *v1.Pod +} + +// List returns a collection of containers +func (c *Container) List(ctx context.Context) ([]runtime.Object, error) { + c.pod = nil + path, ok := ctx.Value(internal.KeyPath).(string) + if !ok { + return nil, fmt.Errorf("no context path for %q", c.gvr) + } + ns, _ := render.Namespaced(path) + c.namespace = ns + o, err := c.factory.Get("v1/pods", path, labels.Everything()) + if err != nil { + return nil, err + } + + var po v1.Pod + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po) + if err != nil { + return nil, err + } + c.pod = &po + res := make([]runtime.Object, 0, len(po.Spec.InitContainers)+len(po.Spec.Containers)) + mx := client.NewMetricsServer(c.factory.Client()) + var pmx *mv1beta1.PodMetrics + if c.factory.Client() != nil { + var err error + pmx, err = mx.FetchPodMetrics(c.namespace, c.pod.Name) + if err != nil { + log.Warn().Err(err).Msgf("No metrics found for pod %q:%q", c.namespace, c.pod.Name) + } + } + + for _, co := range po.Spec.InitContainers { + res = append(res, makeContainerRes(co, po, pmx, true)) + } + for _, co := range po.Spec.Containers { + res = append(res, makeContainerRes(co, po, pmx, false)) + } + + return res, nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func makeContainerRes(co v1.Container, po v1.Pod, pmx *mv1beta1.PodMetrics, isInit bool) render.ContainerRes { + cmx, err := containerMetrics(co.Name, pmx) + if err != nil { + log.Warn().Err(err).Msgf("Container metrics for %s", co.Name) + } + + return render.ContainerRes{ + Container: co, + Status: getContainerStatus(co.Name, po.Status), + Metrics: cmx, + IsInit: isInit, + Age: po.ObjectMeta.CreationTimestamp, + } +} + +func containerMetrics(n string, mx runtime.Object) (*mv1beta1.ContainerMetrics, error) { + pmx, ok := mx.(*mv1beta1.PodMetrics) + if !ok { + return nil, fmt.Errorf("expecting podmetrics but got `%T", mx) + } + if pmx == nil { + return nil, fmt.Errorf("no metrics for container %s", n) + } + for _, m := range pmx.Containers { + if m.Name == n { + return &m, nil + } + } + return nil, nil +} + +func getContainerStatus(co string, status v1.PodStatus) *v1.ContainerStatus { + for _, c := range status.ContainerStatuses { + if c.Name == co { + return &c + } + } + + for _, c := range status.InitContainerStatuses { + if c.Name == co { + return &c + } + } + + return nil +} diff --git a/internal/model/container_test.go b/internal/model/container_test.go new file mode 100644 index 00000000..018a5733 --- /dev/null +++ b/internal/model/container_test.go @@ -0,0 +1,116 @@ +package model_test + +import ( + "context" + "testing" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/watch" + "github.com/ghodss/yaml" + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/informers" +) + +func TestContainerList(t *testing.T) { + c := model.Container{} + c.Init(render.ClusterScope, "containers", makePodFactory()) + + ctx := context.WithValue(context.Background(), internal.KeyPath, "fred/p1") + oo, err := c.List(ctx) + assert.Nil(t, err) + assert.Equal(t, 1, len(oo)) +} + +func TestContainerHydrate(t *testing.T) { + c := model.Container{} + c.Init(render.ClusterScope, "containers", makePodFactory()) + + ctx := context.WithValue(context.Background(), internal.KeyPath, "fred/p1") + oo, err := c.List(ctx) + assert.Nil(t, err) + + rr := make(render.Rows, len(oo)) + assert.Nil(t, c.Hydrate(oo, rr, render.Container{})) + assert.Equal(t, 1, len(rr)) + assert.Equal(t, "fred", rr[0].ID) + assert.Equal(t, render.Fields{"fred", "blee", "false", "Running", "false", "0", "off:off", "n/a", "n/a", "n/a", "n/a", ""}, rr[0].Fields[0:len(rr[0].Fields)-1]) +} + +// ---------------------------------------------------------------------------- +// Helpers... + +type podFactory struct{} + +var _ model.Factory = testFactory{} + +func (f podFactory) Client() client.Connection { + return nil +} +func (f podFactory) Get(gvr, path string, sel labels.Selector) (runtime.Object, error) { + var m map[string]interface{} + if err := yaml.Unmarshal([]byte(poYaml()), &m); err != nil { + return nil, err + } + return &unstructured.Unstructured{Object: m}, nil +} +func (f podFactory) List(gvr, ns string, sel labels.Selector) ([]runtime.Object, error) { + return nil, nil +} +func (f podFactory) ForResource(ns, gvr string) informers.GenericInformer { return nil } +func (f podFactory) CanForResource(ns, gvr string, verbs ...string) (informers.GenericInformer, error) { + return nil, nil +} +func (f podFactory) WaitForCacheSync() {} +func (f podFactory) Forwarders() watch.Forwarders { return nil } + +func makePodFactory() model.Factory { + return podFactory{} +} + +func poYaml() string { + return `apiVersion: v1 +kind: Pod +metadata: + creationTimestamp: "2018-12-14T17:36:43Z" + labels: + blee: duh + name: fred + namespace: blee +spec: + containers: + - env: + - name: fred + value: "1" + valueFrom: + configMapKeyRef: + key: blee + image: blee + name: fred + resources: {} + priority: 1 + priorityClassName: bozo + volumes: + - hostPath: + path: /blee + type: Directory + name: fred +status: + containerStatuses: + - image: "" + imageID: "" + lastState: {} + name: fred + ready: false + restartCount: 0 + state: + running: + startedAt: null + phase: Running +` +} diff --git a/internal/model/context.go b/internal/model/context.go new file mode 100644 index 00000000..c92a4357 --- /dev/null +++ b/internal/model/context.go @@ -0,0 +1,28 @@ +package model + +import ( + "context" + + "github.com/derailed/k9s/internal/render" + "k8s.io/apimachinery/pkg/runtime" +) + +// Context represents a kube context model. +type Context struct { + Resource +} + +// List returns a collection of node resources. +func (c *Context) List(_ context.Context) ([]runtime.Object, error) { + cfg := c.factory.Client().Config() + ctxs, err := cfg.Contexts() + if err != nil { + return nil, err + } + cc := make([]runtime.Object, 0, len(ctxs)) + for name, ctx := range ctxs { + cc = append(cc, render.NewNamedContext(cfg, name, ctx)) + } + + return cc, nil +} diff --git a/internal/model/generic.go b/internal/model/generic.go new file mode 100644 index 00000000..c049c038 --- /dev/null +++ b/internal/model/generic.go @@ -0,0 +1,136 @@ +package model + +import ( + "context" + "fmt" + "time" + + "github.com/derailed/k9s/internal/client" + "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" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/client-go/rest" +) + +const gvFmt = "application/json;as=Table;v=%s;g=%s, application/json" + +// Generic represents a generic model. +type Generic struct { + Resource + + table *metav1beta1.Table +} + +// List returns a collection of node resources. +func (g *Generic) List(ctx context.Context) ([]runtime.Object, error) { + // Ensures the factory is tracking this resource + _, err := g.factory.CanForResource(g.namespace, g.gvr) + if err != nil { + return nil, err + } + + gvr := client.GVR(g.gvr) + fcodec, codec := g.codec(gvr.AsGV()) + + c, err := g.client(fcodec, gvr) + if err != nil { + return nil, err + } + + // BOZO!! Need to know if gvr is namespaced or not + o, err := c.Get(). + SetHeader("Accept", fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName)). + Namespace(g.namespace). + Resource(gvr.ToR()). + VersionedParams(&metav1beta1.TableOptions{}, codec). + Do().Get() + if err != nil { + return nil, err + } + table, ok := o.(*metav1beta1.Table) + if !ok { + return nil, fmt.Errorf("expecting table but got %T", o) + } + g.table = table + res := make([]runtime.Object, len(g.table.Rows)) + for i := range g.table.Rows { + res[i] = RowRes{&g.table.Rows[i]} + } + + log.Debug().Msgf("!!!!GENERIC lister returns %d", len(res)) + return res, err +} + +// Hydrate returns nodes as rows. +func (g *Generic) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error { + defer func(t time.Time) { + log.Debug().Msgf("HYDRATE elapsed: %v", time.Since(t)) + }(time.Now()) + + gr, ok := re.(*render.Generic) + if !ok { + return fmt.Errorf("expecting generic renderer for %s but got %T", g.gvr, re) + } + gr.SetTable(g.table) + for i, o := range oo { + res, ok := o.(RowRes) + if !ok { + return fmt.Errorf("expecting RowRes but got %#v", o) + } + if err := gr.Render(res.TableRow, g.namespace, &rr[i]); err != nil { + return err + } + } + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func (g *Generic) client(codec serializer.CodecFactory, gvr client.GVR) (*rest.RESTClient, error) { + crConfig := g.factory.Client().RestConfigOrDie() + gv := gvr.AsGV() + crConfig.GroupVersion = &gv + crConfig.APIPath = "/apis" + if len(gvr.ToG()) == 0 { + crConfig.APIPath = "/api" + } + crConfig.NegotiatedSerializer = codec.WithoutConversion() + + crRestClient, err := rest.RESTClientFor(crConfig) + if err != nil { + return nil, err + } + return crRestClient, nil +} + +func (r *Resource) codec(gv schema.GroupVersion) (serializer.CodecFactory, runtime.ParameterCodec) { + scheme := runtime.NewScheme() + metav1.AddToGroupVersion(scheme, gv) + scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) + scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) + + return serializer.NewCodecFactory(scheme), runtime.NewParameterCodec(scheme) +} + +// ---------------------------------------------------------------------------- + +// RowRes represents a table row. +type RowRes struct { + *metav1beta1.TableRow +} + +// GetObjectKind returns a schema object. +func (r RowRes) GetObjectKind() schema.ObjectKind { + return nil +} + +// DeepCopyObject returns a container copy. +func (r RowRes) DeepCopyObject() runtime.Object { + return r +} diff --git a/internal/model/helpers.go b/internal/model/helpers.go new file mode 100644 index 00000000..401ae815 --- /dev/null +++ b/internal/model/helpers.go @@ -0,0 +1,60 @@ +package model + +import ( + "fmt" + + "github.com/derailed/tview" + runewidth "github.com/mattn/go-runewidth" + "github.com/rs/zerolog/log" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +func extractFQN(o runtime.Object) string { + u, ok := o.(*unstructured.Unstructured) + if !ok { + log.Error().Err(fmt.Errorf("expecting unstructured but got %T", o)) + return "na" + } + m, ok := u.Object["metadata"].(map[string]interface{}) + if !ok { + log.Error().Err(fmt.Errorf("expecting interface map for metadata but got %T", u.Object["metadata"])) + return "na" + } + + n, ok := m["name"].(string) + if !ok { + log.Error().Err(fmt.Errorf("expecting interface map for name but got %T", m["name"])) + return "na" + } + + ns, ok := m["namespace"].(string) + if !ok { + return FQN("", n) + } + + return FQN(ns, n) +} + +// MetaFQN returns a fully qualified resource name. +func MetaFQN(m metav1.ObjectMeta) string { + if m.Namespace == "" { + return m.Name + } + + return FQN(m.Namespace, m.Name) +} + +// FQN returns a fully qualified resource name. +func FQN(ns, n string) string { + if ns == "" { + return n + } + return ns + "/" + n +} + +// Truncate a string to the given l and suffix ellipsis if needed. +func Truncate(str string, width int) string { + return runewidth.Truncate(str, width, string(tview.SemigraphicsHorizontalEllipsis)) +} diff --git a/internal/model/hint.go b/internal/model/hint.go new file mode 100644 index 00000000..84a5ecdd --- /dev/null +++ b/internal/model/hint.go @@ -0,0 +1,54 @@ +package model + +// HintListener represents a menu hints listener. +type HintListener interface { + HintsChanged(MenuHints) +} + +// Hint represent a hint model. +type Hint struct { + data MenuHints + listeners []HintListener +} + +// NewHint return new hint model. +func NewHint() *Hint { + return &Hint{} +} + +// RemoveListener deletes a listener. +func (h *Hint) RemoveListener(l HintListener) { + victim := -1 + for i, lis := range h.listeners { + if lis == l { + victim = i + break + } + } + if victim == -1 { + return + } + h.listeners = append(h.listeners[:victim], h.listeners[victim+1:]...) +} + +// AddListener adds a hint listener. +func (h *Hint) AddListener(l HintListener) { + h.listeners = append(h.listeners, l) +} + +// SetHints set model hints. +func (h *Hint) SetHints(hh MenuHints) { + h.data = hh + h.fireChanged() +} + +// Peek returns the model data. +func (h *Hint) Peek() MenuHints { + return h.data +} + +func (h *Hint) fireChanged() { + for _, l := range h.listeners { + l.HintsChanged(h.data) + } +} diff --git a/internal/model/hint_test.go b/internal/model/hint_test.go new file mode 100644 index 00000000..c46f6a41 --- /dev/null +++ b/internal/model/hint_test.go @@ -0,0 +1,68 @@ +package model_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/model" + "github.com/stretchr/testify/assert" +) + +func TestHints(t *testing.T) { + uu := map[string]struct { + hh model.MenuHints + e int + }{ + "none": { + model.MenuHints{}, + 0, + }, + "hints": { + model.MenuHints{ + {Mnemonic: "a", Description: "blee"}, + {Mnemonic: "b", Description: "fred"}, + }, + 2, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + h := model.NewHint() + l := hintL{count: -1} + h.AddListener(&l) + h.SetHints(u.hh) + + assert.Equal(t, u.e, l.count) + }) + } +} + +func TestHintRemoveListener(t *testing.T) { + h := model.NewHint() + l1, l2, l3 := &hintL{}, &hintL{}, &hintL{} + h.AddListener(l1) + h.AddListener(l2) + h.AddListener(l3) + + h.RemoveListener(l2) + h.RemoveListener(l3) + h.RemoveListener(l1) + + h.SetHints(model.MenuHints{ + model.MenuHint{Mnemonic: "a", Description: "Blee"}, + }) + + assert.Equal(t, 0, l1.count) + assert.Equal(t, 0, l2.count) + assert.Equal(t, 0, l3.count) +} + +type hintL struct { + count int +} + +func (h *hintL) HintsChanged(hh model.MenuHints) { + h.count++ + h.count += len(hh) +} diff --git a/internal/model/job.go b/internal/model/job.go new file mode 100644 index 00000000..bbde5a77 --- /dev/null +++ b/internal/model/job.go @@ -0,0 +1,69 @@ +package model + +import ( + "context" + "errors" + "strings" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/rs/zerolog/log" + batchv1 "k8s.io/api/batch/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// Job represents a collections of jobs. +type Job struct { + Resource +} + +// List returns a collection of screen dumps. +func (c *Job) List(ctx context.Context) ([]runtime.Object, error) { + uid, ok := ctx.Value(internal.KeyUID).(string) + if !ok { + log.Debug().Msgf("NO UID in context") + } + path, ok := ctx.Value(internal.KeyPath).(string) + if !ok { + return nil, errors.New("no cronjob path found in context") + } + + log.Debug().Msgf("Listing jobs %q %q--%q", c.gvr, uid, path) + oo, err := c.Resource.List(ctx) + if err != nil { + return nil, err + } + if uid == "" { + return oo, nil + } + + _, cronName := client.Namespaced(path) + jj := make([]runtime.Object, 0, len(oo)) + for _, j := range oo { + var job batchv1.Job + err = runtime.DefaultUnstructuredConverter.FromUnstructured(j.(*unstructured.Unstructured).Object, &job) + if err != nil { + return nil, err + } + log.Debug().Msgf("Looking at job %q -- %q", job.Name, cronName) + if !isNamedAfter(cronName, job.Name) { + continue + } + log.Debug().Msgf("GOT Job %s -- %#v -- %q -- %q", job.Name, job.Labels, uid, path) + jj = append(jj, j) + } + + return jj, nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func isNamedAfter(p, n string) bool { + tokens := strings.Split(n, "-") + if len(tokens) == 0 || tokens[0] != p { + return false + } + return true +} diff --git a/internal/resource/log_options.go b/internal/model/log_options.go similarity index 99% rename from internal/resource/log_options.go rename to internal/model/log_options.go index f96ab362..3a0ae168 100644 --- a/internal/resource/log_options.go +++ b/internal/model/log_options.go @@ -1,4 +1,4 @@ -package resource +package model import ( "strings" @@ -17,8 +17,8 @@ type ( Fqn Lines int64 - Previous bool Color color.Paint + Previous bool SingleContainer bool MultiPods bool } diff --git a/internal/model/menu_hint.go b/internal/model/menu_hint.go new file mode 100644 index 00000000..4e1f0123 --- /dev/null +++ b/internal/model/menu_hint.go @@ -0,0 +1,44 @@ +package model + +import ( + "strconv" + "strings" +) + +// MenuHint represents keyboard mnemonic. +type MenuHint struct { + Mnemonic string + Description string + Visible bool +} + +// IsBlank checks if menu hint is a place holder. +func (m MenuHint) IsBlank() bool { + return m.Mnemonic == "" && m.Description == "" && !m.Visible +} + +// MenuHints represents a collection of hints. +type MenuHints []MenuHint + +func (h MenuHints) Len() int { + return len(h) +} + +func (h MenuHints) Swap(i, j int) { + h[i], h[j] = h[j], h[i] +} + +func (h MenuHints) Less(i, j int) bool { + n, err1 := strconv.Atoi(h[i].Mnemonic) + m, err2 := strconv.Atoi(h[j].Mnemonic) + if err1 == nil && err2 == nil { + return n < m + } + if err1 == nil && err2 != nil { + return true + } + if err1 != nil && err2 == nil { + return false + } + return strings.Compare(h[i].Description, h[j].Description) < 0 +} diff --git a/internal/model/menu_hint_test.go b/internal/model/menu_hint_test.go new file mode 100644 index 00000000..14c1c0d6 --- /dev/null +++ b/internal/model/menu_hint_test.go @@ -0,0 +1,22 @@ +package model_test + +import ( + "sort" + "testing" + + "github.com/derailed/k9s/internal/model" + "github.com/stretchr/testify/assert" +) + +func TestMenuHintOrder(t *testing.T) { + h1 := model.MenuHint{Mnemonic: "b", Description: "Duh"} + h2 := model.MenuHint{Mnemonic: "a", Description: "Blee"} + h3 := model.MenuHint{Mnemonic: "1", Description: "Zorg"} + + hh := model.MenuHints{h1, h2, h3} + sort.Sort(hh) + + assert.Equal(t, h3, hh[0]) + assert.Equal(t, h2, hh[1]) + assert.Equal(t, h1, hh[2]) +} diff --git a/internal/resource/mock_clustermeta_test.go b/internal/model/mock_clustermeta_test.go similarity index 86% rename from internal/resource/mock_clustermeta_test.go rename to internal/model/mock_clustermeta_test.go index 43fcfa5e..87109ebe 100644 --- a/internal/resource/mock_clustermeta_test.go +++ b/internal/model/mock_clustermeta_test.go @@ -1,10 +1,10 @@ // Code generated by pegomock. DO NOT EDIT. -// Source: github.com/derailed/k9s/internal/resource (interfaces: ClusterMeta) +// Source: github.com/derailed/k9s/internal/model (interfaces: ClusterMeta) -package resource_test +package model_test import ( - k8s "github.com/derailed/k9s/internal/k8s" + client "github.com/derailed/k9s/internal/client" pegomock "github.com/petergtz/pegomock" v1 "k8s.io/api/core/v1" version "k8s.io/apimachinery/pkg/version" @@ -51,12 +51,12 @@ func (mock *MockClusterMeta) CachedDiscovery() (*disk.CachedDiscoveryClient, err return ret0, ret1 } -func (mock *MockClusterMeta) CanIAccess(_param0 string, _param1 string, _param2 []string) (bool, error) { +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("CanIAccess", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + 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 { @@ -70,79 +70,57 @@ func (mock *MockClusterMeta) CanIAccess(_param0 string, _param1 string, _param2 return ret0, ret1 } -func (mock *MockClusterMeta) CheckListNSAccess() error { +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("CheckListNSAccess", 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 *MockClusterMeta) CheckNSAccess(_param0 string) error { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("CheckNSAccess", 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 *MockClusterMeta) ClusterName() string { - 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()}) + 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 + return ret0, ret1 } -func (mock *MockClusterMeta) Config() *k8s.Config { +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((**k8s.Config)(nil)).Elem()}) - var ret0 *k8s.Config + 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].(*k8s.Config) + ret0 = result[0].(*client.Config) } } return ret0 } -func (mock *MockClusterMeta) ContextName() string { +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()}) + 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 + return ret0, ret1 } func (mock *MockClusterMeta) CurrentNamespaceName() (string, error) { @@ -395,19 +373,23 @@ func (mock *MockClusterMeta) SwitchContextOrDie(_param0 string) { pegomock.GetGenericMockFrom(mock).Invoke("SwitchContextOrDie", params, []reflect.Type{}) } -func (mock *MockClusterMeta) UserName() string { +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()}) + 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 + return ret0, ret1 } func (mock *MockClusterMeta) ValidNamespaces() ([]v1.Namespace, error) { @@ -502,34 +484,34 @@ func (c *MockClusterMeta_CachedDiscovery_OngoingVerification) GetCapturedArgumen func (c *MockClusterMeta_CachedDiscovery_OngoingVerification) GetAllCapturedArguments() { } -func (verifier *VerifierMockClusterMeta) CanIAccess(_param0 string, _param1 string, _param2 []string) *MockClusterMeta_CanIAccess_OngoingVerification { +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, "CanIAccess", params, verifier.timeout) - return &MockClusterMeta_CanIAccess_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} + 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_CanIAccess_OngoingVerification struct { +type MockClusterMeta_CanI_OngoingVerification struct { mock *MockClusterMeta methodInvocations []pegomock.MethodInvocation } -func (c *MockClusterMeta_CanIAccess_OngoingVerification) GetCapturedArguments() (string, string, []string) { +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_CanIAccess_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string, _param2 [][]string) { +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(c.methodInvocations)) + _param0 = make([]string, len(params[0])) for u, param := range params[0] { _param0[u] = param.(string) } - _param1 = make([]string, len(c.methodInvocations)) + _param1 = make([]string, len(params[1])) for u, param := range params[1] { _param1[u] = param.(string) } - _param2 = make([][]string, len(c.methodInvocations)) + _param2 = make([][]string, len(params[2])) for u, param := range params[2] { _param2[u] = param.([]string) } @@ -537,50 +519,6 @@ func (c *MockClusterMeta_CanIAccess_OngoingVerification) GetAllCapturedArguments return } -func (verifier *VerifierMockClusterMeta) CheckListNSAccess() *MockClusterMeta_CheckListNSAccess_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CheckListNSAccess", params, verifier.timeout) - return &MockClusterMeta_CheckListNSAccess_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_CheckListNSAccess_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_CheckListNSAccess_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_CheckListNSAccess_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) CheckNSAccess(_param0 string) *MockClusterMeta_CheckNSAccess_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CheckNSAccess", params, verifier.timeout) - return &MockClusterMeta_CheckNSAccess_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_CheckNSAccess_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_CheckNSAccess_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockClusterMeta_CheckNSAccess_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 *VerifierMockClusterMeta) ClusterName() *MockClusterMeta_ClusterName_OngoingVerification { params := []pegomock.Param{} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ClusterName", params, verifier.timeout) @@ -753,7 +691,7 @@ func (c *MockClusterMeta_IsNamespaced_OngoingVerification) GetCapturedArguments( func (c *MockClusterMeta_IsNamespaced_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) if len(params) > 0 { - _param0 = make([]string, len(c.methodInvocations)) + _param0 = make([]string, len(params[0])) for u, param := range params[0] { _param0[u] = param.(string) } @@ -814,7 +752,7 @@ func (c *MockClusterMeta_NodePods_OngoingVerification) GetCapturedArguments() st func (c *MockClusterMeta_NodePods_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) if len(params) > 0 { - _param0 = make([]string, len(c.methodInvocations)) + _param0 = make([]string, len(params[0])) for u, param := range params[0] { _param0[u] = param.(string) } @@ -875,11 +813,11 @@ func (c *MockClusterMeta_SupportsRes_OngoingVerification) GetCapturedArguments() 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(c.methodInvocations)) + _param0 = make([]string, len(params[0])) for u, param := range params[0] { _param0[u] = param.(string) } - _param1 = make([][]string, len(c.methodInvocations)) + _param1 = make([][]string, len(params[1])) for u, param := range params[1] { _param1[u] = param.([]string) } @@ -906,7 +844,7 @@ func (c *MockClusterMeta_SupportsResource_OngoingVerification) GetCapturedArgume func (c *MockClusterMeta_SupportsResource_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) if len(params) > 0 { - _param0 = make([]string, len(c.methodInvocations)) + _param0 = make([]string, len(params[0])) for u, param := range params[0] { _param0[u] = param.(string) } @@ -933,7 +871,7 @@ func (c *MockClusterMeta_SwitchContextOrDie_OngoingVerification) GetCapturedArgu func (c *MockClusterMeta_SwitchContextOrDie_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) if len(params) > 0 { - _param0 = make([]string, len(c.methodInvocations)) + _param0 = make([]string, len(params[0])) for u, param := range params[0] { _param0[u] = param.(string) } diff --git a/internal/watch/mock_connection_test.go b/internal/model/mock_connection_test.go similarity index 86% rename from internal/watch/mock_connection_test.go rename to internal/model/mock_connection_test.go index d0795670..26d86571 100644 --- a/internal/watch/mock_connection_test.go +++ b/internal/model/mock_connection_test.go @@ -1,18 +1,18 @@ // Code generated by pegomock. DO NOT EDIT. -// Source: github.com/derailed/k9s/internal/watch (interfaces: Connection) +// Source: github.com/derailed/k9s/internal/client (interfaces: Connection) -package watch +package model_test import ( - "github.com/derailed/k9s/internal/k8s" - "github.com/petergtz/pegomock" + client "github.com/derailed/k9s/internal/client" + pegomock "github.com/petergtz/pegomock" v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/version" - "k8s.io/client-go/discovery/cached/disk" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - "k8s.io/metrics/pkg/client/clientset/versioned" + 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" ) @@ -51,12 +51,12 @@ func (mock *MockConnection) CachedDiscovery() (*disk.CachedDiscoveryClient, erro return ret0, ret1 } -func (mock *MockConnection) CanIAccess(_param0 string, _param1 string, _param2 []string) (bool, error) { +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("CanIAccess", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + 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 { @@ -70,46 +70,16 @@ func (mock *MockConnection) CanIAccess(_param0 string, _param1 string, _param2 [ return ret0, ret1 } -func (mock *MockConnection) CheckListNSAccess() error { +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("CheckListNSAccess", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 error + 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].(error) - } - } - return ret0 -} - -func (mock *MockConnection) CheckNSAccess(_param0 string) error { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("CheckNSAccess", 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) Config() *k8s.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((**k8s.Config)(nil)).Elem()}) - var ret0 *k8s.Config - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*k8s.Config) + ret0 = result[0].(*client.Config) } } return ret0 @@ -419,23 +389,23 @@ func (c *MockConnection_CachedDiscovery_OngoingVerification) GetCapturedArgument func (c *MockConnection_CachedDiscovery_OngoingVerification) GetAllCapturedArguments() { } -func (verifier *VerifierMockConnection) CanIAccess(_param0 string, _param1 string, _param2 []string) *MockConnection_CanIAccess_OngoingVerification { +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, "CanIAccess", params, verifier.timeout) - return &MockConnection_CanIAccess_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} + 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_CanIAccess_OngoingVerification struct { +type MockConnection_CanI_OngoingVerification struct { mock *MockConnection methodInvocations []pegomock.MethodInvocation } -func (c *MockConnection_CanIAccess_OngoingVerification) GetCapturedArguments() (string, string, []string) { +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_CanIAccess_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string, _param2 [][]string) { +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(params[0])) @@ -454,50 +424,6 @@ func (c *MockConnection_CanIAccess_OngoingVerification) GetAllCapturedArguments( return } -func (verifier *VerifierMockConnection) CheckListNSAccess() *MockConnection_CheckListNSAccess_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CheckListNSAccess", params, verifier.timeout) - return &MockConnection_CheckListNSAccess_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_CheckListNSAccess_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_CheckListNSAccess_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_CheckListNSAccess_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) CheckNSAccess(_param0 string) *MockConnection_CheckNSAccess_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CheckNSAccess", params, verifier.timeout) - return &MockConnection_CheckNSAccess_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_CheckNSAccess_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_CheckNSAccess_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockConnection_CheckNSAccess_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 *VerifierMockConnection) Config() *MockConnection_Config_OngoingVerification { params := []pegomock.Param{} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Config", params, verifier.timeout) diff --git a/internal/resource/mock_metricsserver_test.go b/internal/model/mock_metricsserver_test.go similarity index 82% rename from internal/resource/mock_metricsserver_test.go rename to internal/model/mock_metricsserver_test.go index 81335100..8447f578 100644 --- a/internal/resource/mock_metricsserver_test.go +++ b/internal/model/mock_metricsserver_test.go @@ -1,11 +1,12 @@ // Code generated by pegomock. DO NOT EDIT. -// Source: github.com/derailed/k9s/internal/resource (interfaces: MetricsServer) +// Source: github.com/derailed/k9s/internal/model (interfaces: MetricsServer) -package resource_test +package model_test import ( - k8s "github.com/derailed/k9s/internal/k8s" + 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" @@ -26,12 +27,19 @@ func NewMockMetricsServer(options ...pegomock.Option) *MockMetricsServer { func (mock *MockMetricsServer) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } func (mock *MockMetricsServer) FailHandler() pegomock.FailHandler { return mock.fail } -func (mock *MockMetricsServer) ClusterLoad(_param0 k8s.Collection, _param1 k8s.Collection, _param2 *k8s.ClusterMetrics) { +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} - pegomock.GetGenericMockFrom(mock).Invoke("ClusterLoad", params, []reflect.Type{}) + 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) { @@ -87,7 +95,7 @@ func (mock *MockMetricsServer) HasMetrics() bool { return ret0 } -func (mock *MockMetricsServer) NodesMetrics(_param0 k8s.Collection, _param1 *v1beta1.NodeMetricsList, _param2 k8s.NodesMetrics) { +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().") } @@ -95,7 +103,7 @@ func (mock *MockMetricsServer) NodesMetrics(_param0 k8s.Collection, _param1 *v1b pegomock.GetGenericMockFrom(mock).Invoke("NodesMetrics", params, []reflect.Type{}) } -func (mock *MockMetricsServer) PodsMetrics(_param0 *v1beta1.PodMetricsList, _param1 k8s.PodsMetrics) { +func (mock *MockMetricsServer) PodsMetrics(_param0 *v1beta1.PodMetricsList, _param1 client.PodsMetrics) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockMetricsServer().") } @@ -140,7 +148,7 @@ type VerifierMockMetricsServer struct { timeout time.Duration } -func (verifier *VerifierMockMetricsServer) ClusterLoad(_param0 k8s.Collection, _param1 k8s.Collection, _param2 *k8s.ClusterMetrics) *MockMetricsServer_ClusterLoad_OngoingVerification { +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} @@ -151,25 +159,25 @@ type MockMetricsServer_ClusterLoad_OngoingVerification struct { methodInvocations []pegomock.MethodInvocation } -func (c *MockMetricsServer_ClusterLoad_OngoingVerification) GetCapturedArguments() (k8s.Collection, k8s.Collection, *k8s.ClusterMetrics) { +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 []k8s.Collection, _param1 []k8s.Collection, _param2 []*k8s.ClusterMetrics) { +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([]k8s.Collection, len(params[0])) + _param0 = make([]*v1.NodeList, len(params[0])) for u, param := range params[0] { - _param0[u] = param.(k8s.Collection) + _param0[u] = param.(*v1.NodeList) } - _param1 = make([]k8s.Collection, len(params[1])) + _param1 = make([]*v1beta1.NodeMetricsList, len(params[1])) for u, param := range params[1] { - _param1[u] = param.(k8s.Collection) + _param1[u] = param.(*v1beta1.NodeMetricsList) } - _param2 = make([]*k8s.ClusterMetrics, len(params[2])) + _param2 = make([]*client.ClusterMetrics, len(params[2])) for u, param := range params[2] { - _param2[u] = param.(*k8s.ClusterMetrics) + _param2[u] = param.(*client.ClusterMetrics) } } return @@ -236,7 +244,7 @@ func (c *MockMetricsServer_HasMetrics_OngoingVerification) GetCapturedArguments( func (c *MockMetricsServer_HasMetrics_OngoingVerification) GetAllCapturedArguments() { } -func (verifier *VerifierMockMetricsServer) NodesMetrics(_param0 k8s.Collection, _param1 *v1beta1.NodeMetricsList, _param2 k8s.NodesMetrics) *MockMetricsServer_NodesMetrics_OngoingVerification { +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} @@ -247,31 +255,31 @@ type MockMetricsServer_NodesMetrics_OngoingVerification struct { methodInvocations []pegomock.MethodInvocation } -func (c *MockMetricsServer_NodesMetrics_OngoingVerification) GetCapturedArguments() (k8s.Collection, *v1beta1.NodeMetricsList, k8s.NodesMetrics) { +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 []k8s.Collection, _param1 []*v1beta1.NodeMetricsList, _param2 []k8s.NodesMetrics) { +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([]k8s.Collection, len(params[0])) + _param0 = make([]*v1.NodeList, len(params[0])) for u, param := range params[0] { - _param0[u] = param.(k8s.Collection) + _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([]k8s.NodesMetrics, len(params[2])) + _param2 = make([]client.NodesMetrics, len(params[2])) for u, param := range params[2] { - _param2[u] = param.(k8s.NodesMetrics) + _param2[u] = param.(client.NodesMetrics) } } return } -func (verifier *VerifierMockMetricsServer) PodsMetrics(_param0 *v1beta1.PodMetricsList, _param1 k8s.PodsMetrics) *MockMetricsServer_PodsMetrics_OngoingVerification { +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} @@ -282,21 +290,21 @@ type MockMetricsServer_PodsMetrics_OngoingVerification struct { methodInvocations []pegomock.MethodInvocation } -func (c *MockMetricsServer_PodsMetrics_OngoingVerification) GetCapturedArguments() (*v1beta1.PodMetricsList, k8s.PodsMetrics) { +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 []k8s.PodsMetrics) { +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([]k8s.PodsMetrics, len(params[1])) + _param1 = make([]client.PodsMetrics, len(params[1])) for u, param := range params[1] { - _param1[u] = param.(k8s.PodsMetrics) + _param1[u] = param.(client.PodsMetrics) } } return diff --git a/internal/model/node.go b/internal/model/node.go new file mode 100644 index 00000000..2df9ae80 --- /dev/null +++ b/internal/model/node.go @@ -0,0 +1,152 @@ +package model + +import ( + "context" + "fmt" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" + "github.com/rs/zerolog/log" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +var _ render.NodeWithMetrics = &NodeWithMetrics{} + +// Node represents a node model. +type Node struct { + Resource +} + +// List returns a collection of node resources. +func (n *Node) List(_ context.Context) ([]runtime.Object, error) { + nn, err := n.factory.Client().DialOrDie().CoreV1().Nodes().List(metav1.ListOptions{}) + if err != nil { + return nil, err + } + + oo := make([]runtime.Object, len(nn.Items)) + for i := range nn.Items { + o, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&nn.Items[i]) + if err != nil { + return nil, err + } + oo[i] = &unstructured.Unstructured{Object: o} + } + return oo, nil +} + +func nameFromMeta(m map[string]interface{}) string { + meta, ok := m["metadata"].(map[string]interface{}) + if !ok { + return "n/a" + } + + name, ok := meta["name"].(string) + if !ok { + return "n/a" + } + + return name +} + +// Hydrate returns nodes as rows. +func (n *Node) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error { + mx := client.NewMetricsServer(n.factory.Client()) + mmx, err := mx.FetchNodesMetrics() + if err != nil { + log.Warn().Err(err).Msg("No node metrics") + } + + for i, o := range oo { + no, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("expecting unstructured but got %T", o) + } + pods, err := n.nodePods(n.factory, nameFromMeta(no.Object)) + if err != nil { + return err + } + + var ( + row render.Row + nmx = NodeWithMetrics{ + object: no, + mx: nodeMetricsFor(o, mmx), + pods: pods, + } + ) + if err := re.Render(&nmx, "", &row); err != nil { + return err + } + rr[i] = row + } + + return nil +} + +func nodeMetricsFor(o runtime.Object, mmx *mv1beta1.NodeMetricsList) *mv1beta1.NodeMetrics { + fqn := extractFQN(o) + for _, mx := range mmx.Items { + if MetaFQN(mx.ObjectMeta) == fqn { + return &mx + } + } + return nil +} + +func (n *Node) nodePods(f Factory, node string) ([]*v1.Pod, error) { + pp, err := f.List("v1/pods", render.AllNamespaces, labels.Everything()) + if err != nil { + return nil, err + } + + pods := make([]*v1.Pod, 0, len(pp)) + for _, p := range pp { + o, ok := p.(*unstructured.Unstructured) + if !ok { + return nil, fmt.Errorf("expecting unstructured but got %T", p) + } + var pod v1.Pod + err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.Object, &pod) + if err != nil { + log.Error().Err(err).Msg("Converting Pod") + return nil, err + } + if pod.Spec.NodeName != node || pod.Status.Phase != v1.PodSucceeded { + continue + } + pods = append(pods, &pod) + } + + return pods, nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +// NodeWithMetrics represents a node with its associated metrics. +type NodeWithMetrics struct { + object runtime.Object + mx *mv1beta1.NodeMetrics + pods []*v1.Pod +} + +// Object returns a node. +func (n *NodeWithMetrics) Object() runtime.Object { + return n.object +} + +// Metrics returns the node metrics. +func (n *NodeWithMetrics) Metrics() *mv1beta1.NodeMetrics { + return n.mx +} + +// Pods return pods running on this node. +func (n *NodeWithMetrics) Pods() []*v1.Pod { + return n.pods +} diff --git a/internal/model/pod.go b/internal/model/pod.go new file mode 100644 index 00000000..f74ada55 --- /dev/null +++ b/internal/model/pod.go @@ -0,0 +1,105 @@ +package model + +import ( + "context" + "fmt" + + "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/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +// Pod represents a pod model. +type Pod struct { + Resource +} + +// List returns a collection of nodes. +func (p *Pod) List(ctx context.Context) ([]runtime.Object, error) { + oo, err := p.Resource.List(ctx) + if err != nil { + return oo, err + } + + sel, ok := ctx.Value(internal.KeyFields).(string) + if !ok { + return oo, nil + } + fsel, err := labels.ConvertSelectorToLabelsMap(sel) + if err != nil { + return nil, err + } + nodeName := fsel["spec.nodeName"] + + var res []runtime.Object + for _, o := range oo { + u, ok := o.(*unstructured.Unstructured) + if !ok { + return res, fmt.Errorf("expecting unstructured but got `%T", o) + } + spec, ok := u.Object["spec"].(map[string]interface{}) + if !ok { + return res, fmt.Errorf("expecting interface map but got `%T", o) + } + if nodeName == "" || spec["nodeName"] == nodeName { + res = append(res, o) + } + } + + return res, nil +} + +// Render returns pod resources as rows. +func (p *Pod) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error { + mx := client.NewMetricsServer(p.factory.Client()) + mmx, err := mx.FetchPodsMetrics(p.namespace) + if err != nil { + log.Warn().Err(err).Msgf("No metrics found for pod") + } + + var index int + for _, o := range oo { + var ( + row render.Row + pmx = PodWithMetrics{object: o, mx: podMetricsFor(o, mmx)} + ) + if err := re.Render(&pmx, p.namespace, &row); err != nil { + return err + } + rr[index] = row + index++ + } + + return nil +} + +func podMetricsFor(o runtime.Object, mmx *mv1beta1.PodMetricsList) *mv1beta1.PodMetrics { + fqn := extractFQN(o) + for _, mx := range mmx.Items { + if MetaFQN(mx.ObjectMeta) == fqn { + return &mx + } + } + return nil +} + +// PodWithMetrics represents a pod and its metrics. +type PodWithMetrics struct { + object runtime.Object + mx *mv1beta1.PodMetrics +} + +// Object returns a pod. +func (p *PodWithMetrics) Object() runtime.Object { + return p.object +} + +// Metrics returns the metrics associated with the pod. +func (p *PodWithMetrics) Metrics() *mv1beta1.PodMetrics { + return p.mx +} diff --git a/internal/model/policy.go b/internal/model/policy.go new file mode 100644 index 00000000..25b1c120 --- /dev/null +++ b/internal/model/policy.go @@ -0,0 +1,236 @@ +package model + +import ( + "context" + "fmt" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/render" + "github.com/rs/zerolog/log" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" +) + +type Policy struct { + Resource +} + +func (p *Policy) List(ctx context.Context) ([]runtime.Object, error) { + gvr, ok := ctx.Value(internal.KeyGVR).(string) + if !ok { + return nil, fmt.Errorf("expecting a context gvr") + } + kind, ok := ctx.Value(internal.KeySubjectKind).(string) + if !ok { + return nil, fmt.Errorf("expecting a context subject kind") + } + name, ok := ctx.Value(internal.KeySubjectName).(string) + if !ok { + return nil, fmt.Errorf("expecting a context subject name") + } + + p.gvr = gvr + crps, err := p.loadClusterRoleBinding(kind, name) + if err != nil { + return nil, err + } + rps, err := p.loadRoleBinding(kind, name) + if err != nil { + return nil, err + } + + oo := make([]runtime.Object, 0, len(crps)+len(rps)) + for _, p := range crps { + oo = append(oo, p) + } + for _, p := range rps { + oo = append(oo, p) + } + + return oo, nil +} + +func (p *Policy) loadClusterRoleBinding(kind, name string) (render.Policies, error) { + crbs, err := fetchClusterRoleBindings(p.factory) + if err != nil { + return nil, err + } + + var nn []string + for _, crb := range crbs { + for _, s := range crb.Subjects { + if s.Kind == kind && s.Name == name { + nn = append(nn, crb.RoleRef.Name) + } + } + } + crs, err := p.fetchClusterRoles() + if err != nil { + return nil, err + } + + rows := make(render.Policies, 0, len(nn)) + for _, cr := range crs { + if !in(nn, cr.Name) { + continue + } + rows = append(rows, parseRules("*", "CR:"+cr.Name, cr.Rules)...) + } + + return rows, nil +} + +func (p *Policy) loadRoleBinding(kind, name string) (render.Policies, error) { + ss, err := p.fetchRoleBindingSubjects(kind, name) + if err != nil { + return nil, err + } + + crs, err := p.fetchClusterRoles() + if err != nil { + return nil, err + } + rows := make(render.Policies, 0, len(crs)) + for _, cr := range crs { + if !in(ss, "ClusterRole:"+cr.Name) { + continue + } + rows = append(rows, parseRules("*", "CR:"+cr.Name, cr.Rules)...) + } + + ros, err := p.fetchRoles() + if err != nil { + return nil, err + } + for _, ro := range ros { + if !in(ss, "Role:"+ro.Name) { + continue + } + log.Debug().Msgf("Loading rules for role %q:%q", ro.Namespace, ro.Name) + rows = append(rows, parseRules(ro.Namespace, "RO:"+ro.Name, ro.Rules)...) + } + + return rows, nil +} + +func fetchClusterRoleBindings(f Factory) ([]rbacv1.ClusterRoleBinding, error) { + oo, err := f.List("rbac.authorization.k8s.io/v1/clusterrolebindings", render.ClusterScope, labels.Everything()) + if err != nil { + return nil, err + } + + crbs := make([]rbacv1.ClusterRoleBinding, len(oo)) + for i, o := range oo { + var crb rbacv1.ClusterRoleBinding + if e := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &crb); e != nil { + return nil, e + } + crbs[i] = crb + } + + return crbs, nil +} + +func fetchRoleBindings(f Factory) ([]rbacv1.RoleBinding, error) { + oo, err := f.List("rbac.authorization.k8s.io/v1/rolebindings", render.ClusterScope, labels.Everything()) + if err != nil { + return nil, err + } + + rbs := make([]rbacv1.RoleBinding, 0, len(oo)) + for _, o := range oo { + var rb rbacv1.RoleBinding + if e := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &rb); e != nil { + return nil, e + } + rbs = append(rbs, rb) + } + + return rbs, nil +} + +func (p *Policy) fetchRoleBindingSubjects(kind, name string) ([]string, error) { + rbs, err := fetchRoleBindings(p.factory) + if err != nil { + return nil, err + } + ss := make([]string, 0, len(rbs)) + for _, rb := range rbs { + for _, s := range rb.Subjects { + if s.Kind == kind && s.Name == name { + ss = append(ss, rb.RoleRef.Kind+":"+rb.Name) + } + } + } + + return ss, nil +} + +func (p *Policy) fetchClusterRoles() ([]rbacv1.ClusterRole, error) { + oo, err := p.factory.List("rbac.authorization.k8s.io/v1/clusterroles", render.ClusterScope, labels.Everything()) + if err != nil { + return nil, err + } + + crs := make([]rbacv1.ClusterRole, len(oo)) + for i, o := range oo { + var cr rbacv1.ClusterRole + if e := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cr); e != nil { + return nil, err + } + crs[i] = cr + } + + return crs, nil +} + +func (p *Policy) fetchRoles() ([]rbacv1.Role, error) { + oo, err := p.factory.List("rbac.authorization.k8s.io/v1/roles", render.AllNamespaces, labels.Everything()) + if err != nil { + return nil, err + } + + rr := make([]rbacv1.Role, len(oo)) + for i, o := range oo { + var ro rbacv1.Role + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ro); err != nil { + return nil, err + } + rr[i] = ro + } + + return rr, nil +} + +func in(nn []string, match string) bool { + for _, n := range nn { + if n == match { + return true + } + } + return false +} + +func parseRules(ns, binding string, rules []rbacv1.PolicyRule) render.Policies { + pp := make(render.Policies, 0, len(rules)) + for _, rule := range rules { + for _, grp := range rule.APIGroups { + for _, res := range rule.Resources { + for _, na := range rule.ResourceNames { + pp = pp.Upsert(render.NewPolicyRes(ns, binding, FQN(res, na), grp, rule.Verbs)) + } + pp = pp.Upsert(render.NewPolicyRes(ns, binding, FQN(grp, res), grp, rule.Verbs)) + } + } + for _, nres := range rule.NonResourceURLs { + if nres[0] != '/' { + nres = "/" + nres + } + pp = pp.Upsert(render.NewPolicyRes(ns, binding, nres, "n/a", rule.Verbs)) + } + } + + return pp +} diff --git a/internal/model/portforward.go b/internal/model/portforward.go new file mode 100644 index 00000000..ceeab7ed --- /dev/null +++ b/internal/model/portforward.go @@ -0,0 +1,56 @@ +package model + +import ( + "context" + "fmt" + "strings" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/render" + "k8s.io/apimachinery/pkg/runtime" +) + +// PortForward represents a portforward model. +type PortForward struct { + Resource +} + +// List returns a collection of screen dumps. +func (c *PortForward) List(ctx context.Context) ([]runtime.Object, error) { + config, ok := ctx.Value(internal.KeyBenchCfg).(*config.Bench) + if !ok { + return nil, fmt.Errorf("no benchconfig found in context") + } + + cc := config.Benchmarks.Containers + oo := make([]runtime.Object, 0, len(c.factory.Forwarders())) + for _, f := range c.factory.Forwarders() { + cfg := render.BenchCfg{ + C: config.Benchmarks.Defaults.C, + N: config.Benchmarks.Defaults.N, + } + if config, ok := cc[containerID(f.Path(), f.Container())]; ok { + cfg.C, cfg.N = config.C, config.N + cfg.Host, cfg.Path = config.HTTP.Host, config.HTTP.Path + } + oo = append(oo, render.ForwardRes{ + Forwarder: f, + Config: cfg, + }) + } + + return oo, nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +// ContainerID computes container ID based on ns/po/co. +func containerID(path, co string) string { + ns, n := client.Namespaced(path) + po := strings.Split(n, "-")[0] + + return ns + "/" + po + ":" + co +} diff --git a/internal/model/rbac.go b/internal/model/rbac.go new file mode 100644 index 00000000..9bf89b8c --- /dev/null +++ b/internal/model/rbac.go @@ -0,0 +1,153 @@ +package model + +import ( + "context" + "fmt" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" +) + +const ( + crbGVR = "rbac.authorization.k8s.io/v1/clusterrolebindings" + crGVR = "rbac.authorization.k8s.io/v1/clusterroles" + rbGVR = "rbac.authorization.k8s.io/v1/rolebindings" + rGVR = "rbac.authorization.k8s.io/v1/roles" +) + +// Rbac represents a model for listing rbac resources. +type Rbac struct { + Resource +} + +// List lists out rbac resources. +func (r *Rbac) List(ctx context.Context) ([]runtime.Object, error) { + gvr, ok := ctx.Value(internal.KeyGVR).(string) + if !ok { + return nil, fmt.Errorf("expecting a context gvr") + } + r.gvr = gvr + path, ok := ctx.Value(internal.KeyPath).(string) + if !ok || path == "" { + return r.Resource.List(ctx) + } + + switch client.GVR(r.gvr).ToR() { + case "clusterrolebindings": + return r.loadClusterRoleBinding(path) + case "rolebindings": + return r.loadRoleBinding(path) + case "clusterroles": + return r.loadClusterRole(path) + case "roles": + return r.loadRole(path) + default: + return nil, fmt.Errorf("expecting clusterrole/role but found %s", client.GVR(r.gvr).ToR()) + } +} + +func (r *Rbac) loadClusterRoleBinding(path string) ([]runtime.Object, error) { + o, err := r.factory.Get(crbGVR, path, labels.Everything()) + if err != nil { + return nil, err + } + + var crb rbacv1.ClusterRoleBinding + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &crb) + if err != nil { + return nil, err + } + + crbo, err := r.factory.Get(crGVR, client.FQN("-", crb.RoleRef.Name), labels.Everything()) + if err != nil { + return nil, err + } + var cr rbacv1.ClusterRole + err = runtime.DefaultUnstructuredConverter.FromUnstructured(crbo.(*unstructured.Unstructured).Object, &cr) + if err != nil { + return nil, err + } + + return asRuntimeObjects(parseRules(render.ClusterScope, "-", cr.Rules)), nil +} + +func (r *Rbac) loadRoleBinding(path string) ([]runtime.Object, error) { + o, err := r.factory.Get(rbGVR, path, labels.Everything()) + if err != nil { + return nil, err + } + + var rb rbacv1.RoleBinding + if err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &rb); err != nil { + return nil, err + } + + if rb.RoleRef.Kind == "ClusterRole" { + o, e := r.factory.Get(crGVR, client.FQN("-", rb.RoleRef.Name), labels.Everything()) + if e != nil { + return nil, e + } + var cr rbacv1.ClusterRole + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cr) + if err != nil { + return nil, err + } + return asRuntimeObjects(parseRules(render.ClusterScope, "-", cr.Rules)), nil + } + + ro, err := r.factory.Get(rGVR, client.FQN(rb.Namespace, rb.RoleRef.Name), labels.Everything()) + if err != nil { + return nil, err + } + var role rbacv1.Role + err = runtime.DefaultUnstructuredConverter.FromUnstructured(ro.(*unstructured.Unstructured).Object, &role) + if err != nil { + return nil, err + } + + return asRuntimeObjects(parseRules(render.ClusterScope, "-", role.Rules)), nil +} + +func (r *Rbac) loadClusterRole(path string) ([]runtime.Object, error) { + o, err := r.factory.Get(crGVR, path, labels.Everything()) + if err != nil { + return nil, err + } + + var cr rbacv1.ClusterRole + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cr) + if err != nil { + return nil, err + } + + return asRuntimeObjects(parseRules(render.ClusterScope, "-", cr.Rules)), nil +} + +func (r *Rbac) loadRole(path string) ([]runtime.Object, error) { + o, err := r.factory.Get(rGVR, path, labels.Everything()) + if err != nil { + return nil, err + } + + var ro rbacv1.Role + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ro) + if err != nil { + return nil, err + } + + return asRuntimeObjects(parseRules(render.ClusterScope, "-", ro.Rules)), nil +} + +func asRuntimeObjects(rr render.Policies) []runtime.Object { + oo := make([]runtime.Object, len(rr)) + for i, r := range rr { + oo[i] = r + } + + return oo +} diff --git a/internal/model/registry.go b/internal/model/registry.go new file mode 100644 index 00000000..4d8c076f --- /dev/null +++ b/internal/model/registry.go @@ -0,0 +1,150 @@ +package model + +import ( + "github.com/derailed/k9s/internal/render" +) + +// BOZO!! Break up deps and merge into single registrar +var Registry = map[string]ResourceMeta{ + // Custom... + "containers": ResourceMeta{ + Model: &Container{}, + Renderer: &render.Container{}, + }, + "contexts": ResourceMeta{ + Model: &Context{}, + Renderer: &render.Context{}, + }, + "screendumps": ResourceMeta{ + Model: &ScreenDump{}, + Renderer: &render.ScreenDump{}, + }, + "rbac": ResourceMeta{ + Model: &Rbac{}, + Renderer: &render.Rbac{}, + }, + "policy": ResourceMeta{ + Model: &Policy{}, + Renderer: &render.Policy{}, + }, + "users": ResourceMeta{ + Model: &Subject{}, + Renderer: &render.Subject{}, + }, + "groups": ResourceMeta{ + Model: &Subject{}, + Renderer: &render.Subject{}, + }, + "portforwards": ResourceMeta{ + Model: &PortForward{}, + Renderer: &render.PortForward{}, + }, + "benchmarks": ResourceMeta{ + Model: &Benchmark{}, + Renderer: &render.Benchmark{}, + }, + "aliases": ResourceMeta{ + Model: &Alias{}, + Renderer: &render.Alias{}, + }, + + // Core... + "v1/configmaps": ResourceMeta{ + Renderer: &render.ConfigMap{}, + }, + "v1/endpoints": ResourceMeta{ + Renderer: &render.Endpoints{}, + }, + "v1/events": ResourceMeta{ + Renderer: &render.Event{}, + }, + "v1/pods": ResourceMeta{ + Model: &Pod{}, + Renderer: &render.Pod{}, + }, + "v1/namespaces": ResourceMeta{ + Renderer: &render.Namespace{}, + }, + "v1/nodes": ResourceMeta{ + Model: &Node{}, + Renderer: &render.Node{}, + }, + "v1/secrets": ResourceMeta{ + Renderer: &render.Secret{}, + }, + "v1/services": ResourceMeta{ + Renderer: &render.Service{}, + }, + "v1/serviceaccounts": ResourceMeta{ + Renderer: &render.ServiceAccount{}, + }, + + // Apps... + "apps/v1/deployments": ResourceMeta{ + Renderer: &render.Deployment{}, + }, + "apps/v1/replicasets": ResourceMeta{ + Renderer: &render.ReplicaSet{}, + }, + "apps/v1/statefulsets": ResourceMeta{ + Renderer: &render.StatefulSet{}, + }, + "apps/v1/daemonsets": ResourceMeta{ + Renderer: &render.DaemonSet{}, + }, + + // Extensions... + "extensions/v1beta1/daemonsets": ResourceMeta{ + Renderer: &render.DaemonSet{}, + }, + "extensions/v1beta1/ingresses": ResourceMeta{ + Renderer: &render.Ingress{}, + }, + "extensions/v1beta1/networkpolicies": ResourceMeta{ + Renderer: &render.NetworkPolicy{}, + }, + + // Batch... + "batch/v1beta1/cronjobs": ResourceMeta{ + Renderer: &render.CronJob{}, + }, + "batch/v1/jobs": ResourceMeta{ + Model: &Job{}, + Renderer: &render.Job{}, + }, + + // Autoscaling... + "autoscaling/v1/horizontalpodautoscalers": ResourceMeta{ + Renderer: &render.HorizontalPodAutoscaler{}, + }, + + // CRDs... + "apiextensions.k8s.io/v1beta1/customresourcedefinitions": ResourceMeta{ + Renderer: &render.CustomResourceDefinition{}, + }, + + // Storage... + "storage.k8s.io/v1/storageclasses": ResourceMeta{ + Renderer: &render.StorageClass{}, + }, + + // Policy... + "policy/v1beta1/poddisruptionbudgets": ResourceMeta{ + Renderer: &render.PodDisruptionBudget{}, + }, + + // RBAC... + "rbac.authorization.k8s.io/v1/clusterroles": ResourceMeta{ + Model: &Rbac{}, + Renderer: &render.ClusterRole{}, + }, + "rbac.authorization.k8s.io/v1/clusterrolebindings": ResourceMeta{ + Renderer: &render.ClusterRoleBinding{}, + }, + "rbac.authorization.k8s.io/v1/roles": ResourceMeta{ + Renderer: &render.Role{}, + }, + "rbac.authorization.k8s.io/v1/rolebindings": ResourceMeta{ + Renderer: &render.RoleBinding{}, + }, +} diff --git a/internal/model/resource.go b/internal/model/resource.go new file mode 100644 index 00000000..45fb6051 --- /dev/null +++ b/internal/model/resource.go @@ -0,0 +1,41 @@ +package model + +import ( + "context" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/render" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" +) + +// Resource represents a generic resource model. +type Resource struct { + namespace, gvr string + factory Factory +} + +func (r *Resource) Init(ns, gvr string, f Factory) { + r.namespace, r.gvr, r.factory = ns, gvr, f +} + +// List returns a collection of nodes. +func (r *Resource) List(ctx context.Context) ([]runtime.Object, error) { + strLabel, ok := ctx.Value(internal.KeyLabels).(string) + lsel := labels.Everything() + if sel, err := labels.ConvertSelectorToLabelsMap(strLabel); ok && err == nil { + lsel = sel.AsSelector() + } + return r.factory.List(r.gvr, r.namespace, lsel) +} + +// Render returns a node as a row. +func (r *Resource) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error { + for i, o := range oo { + if err := re.Render(o, r.namespace, &rr[i]); err != nil { + return err + } + } + + return nil +} diff --git a/internal/model/screen_dump.go b/internal/model/screen_dump.go new file mode 100644 index 00000000..2d445ce1 --- /dev/null +++ b/internal/model/screen_dump.go @@ -0,0 +1,36 @@ +package model + +import ( + "context" + "errors" + "io/ioutil" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/render" + "k8s.io/apimachinery/pkg/runtime" +) + +// ScreenDump represents a collections of screendumps. +type ScreenDump struct { + Resource +} + +// List returns a collection of screen dumps. +func (c *ScreenDump) List(ctx context.Context) ([]runtime.Object, error) { + dir, ok := ctx.Value(internal.KeyDir).(string) + if !ok { + return nil, errors.New("no screendump dir found in context") + } + + ff, err := ioutil.ReadDir(dir) + if err != nil { + return nil, err + } + + oo := make([]runtime.Object, len(ff)) + for i, f := range ff { + oo[i] = render.FileRes{File: f, Dir: dir} + } + + return oo, nil +} diff --git a/internal/model/stack.go b/internal/model/stack.go new file mode 100644 index 00000000..c02b240d --- /dev/null +++ b/internal/model/stack.go @@ -0,0 +1,166 @@ +package model + +import ( + "github.com/rs/zerolog/log" +) + +const ( + // StackPush denotes an add on the stack. + StackPush StackAction = 1 << iota + + // StackPop denotes a delete on the stack. + StackPop +) + +// StackAction represents an action on the stack. +type StackAction int + +// StackEvent represents an operation on a view stack. +type StackEvent struct { + // Kind represents the event condition. + Action StackAction + + // Item represents the targetted item. + Component Component +} + +// StackListener represents a stack listener. +type StackListener interface { + // StackPushed indicates a new item was added. + StackPushed(Component) + + // StackPopped indicates an item was deleted + StackPopped(old, new Component) + + // StackTop indicates the top of the stack + StackTop(Component) +} + +// Stack represents a stacks of components. +type Stack struct { + components []Component + listeners []StackListener +} + +// NewStack returns a new initialized stack. +func NewStack() *Stack { + return &Stack{} +} + +// Flatten retuns a string representation of the component stack. +func (s *Stack) Flatten() []string { + ss := make([]string, len(s.components)) + for i, c := range s.components { + ss[i] = c.Name() + } + return ss +} + +// RemoveListener removes a listener. +func (s *Stack) RemoveListener(l StackListener) { + victim := -1 + for i, lis := range s.listeners { + if lis == l { + victim = i + break + } + } + if victim == -1 { + return + } + s.listeners = append(s.listeners[:victim], s.listeners[victim+1:]...) +} + +// AddListener registers a stack listener. +func (s *Stack) AddListener(l StackListener) { + s.listeners = append(s.listeners, l) + if !s.Empty() { + l.StackTop(s.Top()) + } +} + +// Dump prints out the stack. +func (s *Stack) DumpStack() { + log.Debug().Msgf("--- Stack Dump %p---", s) + for i, c := range s.components { + log.Debug().Msgf("%d -- %s -- %#v", i, c.Name(), c) + } + log.Debug().Msg("------------------") +} + +// Push adds a new item. +func (s *Stack) Push(c Component) { + if top := s.Top(); top != nil { + top.Stop() + } + s.components = append(s.components, c) + s.notify(StackPush, c) +} + +// Pop removed the top item and returns it. +func (s *Stack) Pop() (Component, bool) { + if s.Empty() { + return nil, false + } + + c := s.components[s.size()] + s.components = s.components[:s.size()] + s.notify(StackPop, c) + + return c, true +} + +// Peek returns stack state. +func (s *Stack) Peek() []Component { + return s.components +} + +// ClearHistory clear out the stack history up to most recent. +func (s *Stack) ClearHistory() { + for range s.components { + s.Pop() + } +} + +// Empty returns true if the stack is empty. +func (s *Stack) Empty() bool { + return len(s.components) == 0 +} + +// IsLast indicates if stack only has one item left. +func (s *Stack) IsLast() bool { + return len(s.components) == 1 +} + +// Previous returns the previous component if any. +func (s *Stack) Previous() Component { + if s.IsLast() { + return s.Top() + } + + return s.components[len(s.components)-2] +} + +// Top returns the top most item or nil if the stack is empty. +func (s *Stack) Top() Component { + if s.Empty() { + return nil + } + + return s.components[s.size()] +} + +func (s *Stack) size() int { + return len(s.components) - 1 +} + +func (s *Stack) notify(a StackAction, c Component) { + for _, l := range s.listeners { + switch a { + case StackPush: + l.StackPushed(c) + case StackPop: + l.StackPopped(c, s.Top()) + } + } +} diff --git a/internal/model/stack_test.go b/internal/model/stack_test.go new file mode 100644 index 00000000..01b399ab --- /dev/null +++ b/internal/model/stack_test.go @@ -0,0 +1,153 @@ +package model_test + +import ( + "context" + "testing" + + "github.com/derailed/k9s/internal/model" + "github.com/derailed/tview" + "github.com/gdamore/tcell" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" +) + +func init() { + zerolog.SetGlobalLevel(zerolog.FatalLevel) +} + +func TestStackPush(t *testing.T) { + top := c{} + uu := map[string]struct { + items []model.Component + pop int + e bool + top model.Component + }{ + "empty": { + items: []model.Component{}, + pop: 3, + e: true, + }, + "full": { + items: []model.Component{c{}, c{}, top}, + pop: 3, + e: true, + }, + "pop": { + items: []model.Component{c{}, c{}, top}, + pop: 2, + e: false, + top: top, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + s := model.NewStack() + for _, c := range u.items { + s.Push(c) + } + for i := 0; i < u.pop; i++ { + s.Pop() + } + assert.Equal(t, u.e, s.Empty()) + if !u.e { + assert.Equal(t, u.top, s.Top()) + } + }) + } +} + +func TestStackTop(t *testing.T) { + top := c{} + uu := map[string]struct { + items []model.Component + e model.Component + }{ + "blank": { + items: []model.Component{}, + }, + "push3": { + items: []model.Component{c{}, c{}, top}, + e: top, + }, + "push1": { + items: []model.Component{top}, + e: top, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + s := model.NewStack() + for _, item := range u.items { + s.Push(item) + } + v := s.Top() + assert.Equal(t, u.e, v) + }) + } +} + +func TestStackListener(t *testing.T) { + items := []model.Component{c{}, c{}, c{}} + s := model.NewStack() + l := stackL{} + s.AddListener(&l) + for _, item := range items { + s.Push(item) + } + assert.Equal(t, 3, l.count) + + for range items { + s.Pop() + } + assert.Equal(t, 0, l.count) +} + +func TestStackRemoveListener(t *testing.T) { + s := model.NewStack() + l1, l2, l3 := &stackL{}, &stackL{}, &stackL{} + s.AddListener(l1) + s.AddListener(l2) + s.AddListener(l3) + + s.RemoveListener(l2) + s.RemoveListener(l3) + s.RemoveListener(l1) + + s.Push(c{}) + + assert.Equal(t, 0, l1.count) + assert.Equal(t, 0, l2.count) + assert.Equal(t, 0, l3.count) +} + +type stackL struct { + count int +} + +func (s *stackL) StackPushed(model.Component) { + s.count++ +} +func (s *stackL) StackPopped(c, top model.Component) { + s.count-- +} +func (s *stackL) StackTop(model.Component) {} + +type c struct{} + +func (c c) Name() string { return "test" } +func (c c) Hints() model.MenuHints { return nil } +func (c c) Draw(tcell.Screen) {} +func (c c) InputHandler() func(*tcell.EventKey, func(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 } diff --git a/internal/model/subject.go b/internal/model/subject.go new file mode 100644 index 00000000..eb4b779c --- /dev/null +++ b/internal/model/subject.go @@ -0,0 +1,97 @@ +package model + +import ( + "context" + "errors" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/render" + "k8s.io/apimachinery/pkg/runtime" +) + +// Subject represents a subject model. +type Subject struct { + Resource +} + +// List returns a collection of subjects. +func (s *Subject) List(ctx context.Context) ([]runtime.Object, error) { + kind, ok := ctx.Value(internal.KeySubjectKind).(string) + if !ok { + return nil, errors.New("expecting a SubjectKind") + } + + crbs, err := s.listClusterRoleBindings(kind) + if err != nil { + return nil, err + } + + rbs, err := s.listRoleBindings(kind) + if err != nil { + return nil, err + } + + return append(crbs, rbs...), nil +} + +func (s *Subject) listClusterRoleBindings(kind string) ([]runtime.Object, error) { + crbs, err := fetchClusterRoleBindings(s.factory) + if err != nil { + return nil, err + } + + oo := make([]runtime.Object, 0, len(crbs)) + for _, crb := range crbs { + for _, su := range crb.Subjects { + if su.Kind != kind || inSubjectRes(oo, su.Name) { + continue + } + oo = append(oo, render.SubjectRef{ + Name: su.Name, + Kind: "ClusterRoleBinding", + FirstLocation: crb.Name, + }) + } + } + + return oo, nil +} + +func (s *Subject) listRoleBindings(kind string) ([]runtime.Object, error) { + rbs, err := fetchRoleBindings(s.factory) + if err != nil { + return nil, err + } + + oo := make([]runtime.Object, 0, len(rbs)) + for _, rb := range rbs { + for _, su := range rb.Subjects { + if su.Kind != kind || inSubjectRes(oo, su.Name) { + continue + } + oo = append(oo, render.SubjectRef{ + Name: su.Name, + Kind: "RoleBinding", + FirstLocation: rb.Name, + }) + } + } + + return oo, nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func inSubjectRes(oo []runtime.Object, match string) bool { + for _, o := range oo { + res, ok := o.(render.SubjectRef) + if !ok { + continue + } + if res.Name == match { + return true + } + } + return false +} diff --git a/internal/model/table.go b/internal/model/table.go index 47fe1a2f..48d86405 100644 --- a/internal/model/table.go +++ b/internal/model/table.go @@ -1,37 +1,187 @@ package model import ( - "github.com/derailed/k9s/internal/resource" + "context" + "fmt" + "sync/atomic" + "time" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/render" + "github.com/rs/zerolog/log" ) -// TableListener tracks tabular data changes. +const refreshRate = 1 * time.Second + type TableListener interface { - Refreshed(resource.TableData) - RowAdded(resource.RowEvent) - RowUpdated(resource.RowEvent) - RowDeleted(resource.RowEvent) + TableDataChanged(render.TableData) + TableLoadFailed(error) } -// Table represents tabular data. type Table struct { - data resource.TableData - - listeners []TableListener + gvr string + namespace string + data *render.TableData + listeners []TableListener + inUpdate int32 + refreshRate time.Duration } -// NewTable returns a new table. -func NewTable() *Table { - return &Table{} -} - -// Load the initial tabular data -func (t *Table) Load(data resource.TableData) { - t.data = data - t.fireTableRefreshed() -} - -func (t *Table) fireTableRefreshed() { - for _, l := range t.listeners { - l.Refreshed(t.data) +// NewTable returns a new table model. +func NewTable(gvr string) *Table { + return &Table{ + gvr: gvr, + data: render.NewTableData(), + refreshRate: 2 * time.Second, } } + +// Watch initiates model updates. +func (t *Table) Watch(ctx context.Context) { + t.Refresh(ctx) + go t.updater(ctx) +} + +// Refresh update the model now. +func (t *Table) Refresh(ctx context.Context) { + t.refresh(ctx) +} + +// GetNamespace returns the model namespace. +func (t *Table) GetNamespace() string { + return t.namespace +} + +// SetNamespace sets up model namespace. +func (t *Table) SetNamespace(ns string) { + t.namespace = ns + t.data.Clear() +} + +// SetRefreshRate sets model refresh duration. +func (t *Table) SetRefreshRate(d time.Duration) { + t.refreshRate = d +} + +// ClusterWide checks if resource is scope for all namespaces. +func (t *Table) ClusterWide() bool { + return t.namespace == render.AllNamespaces +} + +// InNamespace checks if current namespace matches desired namespace. +func (t *Table) InNamespace(ns string) bool { + return t.namespace == ns +} + +// Empty return true if no model data. +func (t *Table) Empty() bool { + return len(t.data.RowEvents) == 0 +} + +// Peek returns model data. +func (t *Table) Peek() render.TableData { + return *t.data +} + +func (t *Table) updater(ctx context.Context) { + defer log.Debug().Msgf("Model canceled -- %q", t.gvr) + for { + select { + case <-ctx.Done(): + return + case <-time.After(refreshRate): + t.refresh(ctx) + } + } +} + +func (t *Table) refresh(ctx context.Context) { + if !atomic.CompareAndSwapInt32(&t.inUpdate, 0, 1) { + log.Debug().Msgf("Dropping update...") + return + } + defer atomic.StoreInt32(&t.inUpdate, 0) + + if err := t.reconcile(ctx); err != nil { + log.Error().Err(err).Msg("Reconcile failed") + t.fireTableLoadFailed(err) + } + t.fireTableChanged(*t.data) +} + +// AddListener adds a new model listener. +func (t *Table) AddListener(l TableListener) { + t.listeners = append(t.listeners, l) + t.fireTableChanged(*t.data) +} + +// RemoveListener delete a listener from the list. +func (t *Table) RemoveListener(l TableListener) { + victim := -1 + for i, lis := range t.listeners { + if lis == l { + victim = i + break + } + } + + if victim >= 0 { + t.listeners = append(t.listeners[:victim], t.listeners[victim+1:]...) + } +} + +func (t *Table) fireTableChanged(data render.TableData) { + for _, l := range t.listeners { + l.TableDataChanged(data) + } +} + +func (t *Table) fireTableLoadFailed(err error) { + for _, l := range t.listeners { + l.TableLoadFailed(err) + } +} + +func (t *Table) reconcile(ctx context.Context) error { + t.data.Mutex.Lock() + defer t.data.Mutex.Unlock() + + factory, ok := ctx.Value(internal.KeyFactory).(Factory) + if !ok { + return fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory)) + } + m, ok := Registry[string(t.gvr)] + if !ok { + log.Warn().Msgf("Resource %s not found in registry. Going generic!", t.gvr) + m = ResourceMeta{ + Model: &Generic{}, + Renderer: &render.Generic{}, + } + } + + if m.Model == nil { + m.Model = &Resource{} + } + m.Model.Init(t.namespace, string(t.gvr), factory) + + oo, err := m.Model.List(ctx) + if err != nil { + return err + } + + rows := make(render.Rows, len(oo)) + if err := m.Model.Hydrate(oo, rows, m.Renderer); err != nil { + return err + } + + // if labelSelector in place might as well clear the model data. + sel, ok := ctx.Value(internal.KeyLabels).(string) + if ok && sel != "" { + t.data.Clear() + } + + t.data.Update(rows) + t.data.Namespace, t.data.Header = t.namespace, m.Renderer.Header(t.namespace) + + return nil +} diff --git a/internal/model/test_assets/bench/default_fred_1577308050814961000.txt b/internal/model/test_assets/bench/default_fred_1577308050814961000.txt new file mode 100644 index 00000000..00109e0c --- /dev/null +++ b/internal/model/test_assets/bench/default_fred_1577308050814961000.txt @@ -0,0 +1,24 @@ +Summary: + Total: 816.6403 secs + Slowest: 0.0000 secs + Fastest: 0.0000 secs + Average: NaN secs + Requests/sec: 0.0122 + + +Response time histogram: + + +Latency distribution: + +Details (average, fastest, slowest): + DNS+dialup: NaN secs, 0.0000 secs, 0.0000 secs + DNS-lookup: NaN secs, 0.0000 secs, 0.0000 secs + req write: NaN secs, 0.0000 secs, 0.0000 secs + resp wait: NaN secs, 0.0000 secs, 0.0000 secs + resp read: NaN secs, 0.0000 secs, 0.0000 secs + +Status code distribution: + +Error distribution: + [10] Get http://192.168.64.126:30805/: dial tcp 192.168.64.126:30805: connect: operation timed out diff --git a/internal/model/types.go b/internal/model/types.go new file mode 100644 index 00000000..f0acad0d --- /dev/null +++ b/internal/model/types.go @@ -0,0 +1,99 @@ +package model + +import ( + "context" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/watch" + "github.com/derailed/tview" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/informers" +) + +// Igniter represents a runnable view. +type Igniter interface { + // Start starts a component. + Init(ctx context.Context) error + + // Start starts a component. + Start() + + // Stop terminates a component. + Stop() +} + +// Hinter represent a menu mnemonic provider. +type Hinter interface { + // Hints returns a collection of menu hints. + Hints() MenuHints +} + +// Primitive represents a UI primitive. +type Primitive interface { + tview.Primitive + + // Name returns the view name. + Name() string +} + +// Component represents a ui component +type Component interface { + Primitive + Igniter + Hinter +} + +// Renderer represents a resource renderer. +type Renderer interface { + // Render converts raw resources to tabular data. + Render(o interface{}, ns string, row *render.Row) error + + // Header returns the resource header. + Header(ns string) render.HeaderRow + + // ColorerFunc returns a row colorer function. + ColorerFunc() render.ColorerFunc +} + +// Lister represents a resource lister. +type Lister interface { + // Init initializes a resource. + Init(ns, gvr string, f Factory) + + // List returns a collection of resources. + List(context.Context) ([]runtime.Object, error) + + // Hydrate converts resource rows into tabular data. + Hydrate(oo []runtime.Object, rr render.Rows, r Renderer) error +} + +type Factory interface { + // Client retrieves an api client. + Client() client.Connection + + // Get fetch a given resource. + Get(gvr, path string, sel labels.Selector) (runtime.Object, error) + + // List fetch a collection of resources. + List(gvr, ns string, sel labels.Selector) ([]runtime.Object, error) + + // ForResource fetch an informer for a given resource. + ForResource(ns, gvr string) informers.GenericInformer + + // CanForResource fetch an informer for a given resource. + CanForResource(ns, gvr string, verbs ...string) (informers.GenericInformer, error) + + // WaitForCacheSync synchronize the cache. + WaitForCacheSync() + + // Forwards returns all portforwards. + Forwarders() watch.Forwarders +} + +// ResourceMeta represents model info about a resource. +type ResourceMeta struct { + Model Lister + Renderer Renderer +} diff --git a/internal/perf/benchmark.go b/internal/perf/benchmark.go index 05c84bd4..aa1775a0 100644 --- a/internal/perf/benchmark.go +++ b/internal/perf/benchmark.go @@ -10,8 +10,8 @@ import ( "path/filepath" "time" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/resource" "github.com/rakyll/hey/requester" "github.com/rs/zerolog/log" ) @@ -75,10 +75,6 @@ func (b *Benchmark) init(base string) error { return nil } -func (b *Benchmark) annulled() bool { - return b.canceled -} - // Cancel kills the benchmark in progress. func (b *Benchmark) Cancel() { if b == nil { @@ -112,20 +108,25 @@ func (b *Benchmark) save(cluster string, r io.Reader) error { return err } - ns, n := resource.Namespaced(b.config.Name) + ns, n := client.Namespaced(b.config.Name) file := filepath.Join(dir, fmt.Sprintf(benchFmat, ns, n, time.Now().UnixNano())) f, err := os.Create(file) if err != nil { return err } - defer f.Close() + defer func() { + if e := f.Close(); e != nil { + log.Fatal().Err(e).Msg("Bench save") + } + }() bb, err := ioutil.ReadAll(r) if err != nil { return err } - - f.Write(bb) + if _, err := f.Write(bb); err != nil { + return err + } return nil } diff --git a/internal/render/alias.go b/internal/render/alias.go new file mode 100644 index 00000000..dce1edc0 --- /dev/null +++ b/internal/render/alias.go @@ -0,0 +1,69 @@ +package render + +import ( + "fmt" + "strings" + + "github.com/derailed/k9s/internal/client" + "github.com/gdamore/tcell" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// Alias renders a aliases to screen. +type Alias struct{} + +// ColorerFunc colors a resource row. +func (Alias) ColorerFunc() ColorerFunc { + return func(ns string, re RowEvent) tcell.Color { + return tcell.ColorMediumSpringGreen + } +} + +// Header returns a header row. +func (Alias) Header(ns string) HeaderRow { + return HeaderRow{ + Header{Name: "RESOURCE"}, + Header{Name: "COMMAND"}, + Header{Name: "APIGROUP"}, + } +} + +// Render renders a K8s resource to screen. +// BOZO!! Pass in a row with pre-alloc fields?? +func (Alias) Render(o interface{}, ns string, r *Row) error { + a, ok := o.(AliasRes) + if !ok { + return fmt.Errorf("expected AliasRes, but got %T", o) + } + + r.ID = a.GVR + gvr := client.GVR(a.GVR) + res, grp := gvr.ToRAndG() + r.Fields = append(r.Fields, + res, + strings.Join(a.Aliases, ","), + grp, + ) + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +// AliasRes represents an alias resource. +type AliasRes struct { + GVR string + Aliases []string +} + +// GetObjectKind returns a schema object. +func (AliasRes) GetObjectKind() schema.ObjectKind { + return nil +} + +// DeepCopyObject returns a container copy. +func (a AliasRes) DeepCopyObject() runtime.Object { + return a +} diff --git a/internal/render/alias_test.go b/internal/render/alias_test.go new file mode 100644 index 00000000..6caa6763 --- /dev/null +++ b/internal/render/alias_test.go @@ -0,0 +1,81 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/gdamore/tcell" + "github.com/stretchr/testify/assert" +) + +func TestAliasColorer(t *testing.T) { + var a render.Alias + + r := render.Row{ID: "g/v/r", Fields: render.Fields{"r", "blee", "g"}} + uu := map[string]struct { + ns string + re render.RowEvent + e tcell.Color + }{ + "addAll": { + ns: render.AllNamespaces, + re: render.RowEvent{Kind: render.EventAdd, Row: r}, + e: tcell.ColorMediumSpringGreen}, + "deleteAll": { + ns: render.AllNamespaces, + re: render.RowEvent{Kind: render.EventDelete, Row: r}, + e: tcell.ColorMediumSpringGreen}, + "updateAll": { + ns: render.AllNamespaces, + re: render.RowEvent{Kind: render.EventUpdate, Row: r}, + e: tcell.ColorMediumSpringGreen, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, a.ColorerFunc()(u.ns, u.re)) + }) + } +} + +func TestAliasHeader(t *testing.T) { + h := render.HeaderRow{ + render.Header{Name: "RESOURCE"}, + render.Header{Name: "COMMAND"}, + render.Header{Name: "APIGROUP"}, + } + + var a render.Alias + assert.Equal(t, h, a.Header("fred")) + assert.Equal(t, h, a.Header(render.AllNamespaces)) +} + +func TestAliasRender(t *testing.T) { + a := render.Alias{} + + o := render.AliasRes{ + GVR: "fred/v1/blee", + Aliases: []string{"a", "b", "c"}, + } + + var r render.Row + assert.Nil(t, a.Render(o, "fred/v1/blee", &r)) + assert.Equal(t, render.Row{ID: "fred/v1/blee", Fields: render.Fields{"blee", "a,b,c", "fred"}}, r) +} + +func BenchmarkAlias(b *testing.B) { + o := render.AliasRes{ + GVR: "fred/v1/blee", + Aliases: []string{"a", "b", "c"}, + } + var a render.Alias + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + var r render.Row + a.Render(o, "aliases", &r) + } +} diff --git a/internal/views/test_assets/b1.txt b/internal/render/assets/b1.txt similarity index 100% rename from internal/views/test_assets/b1.txt rename to internal/render/assets/b1.txt diff --git a/internal/views/test_assets/b2.txt b/internal/render/assets/b2.txt similarity index 100% rename from internal/views/test_assets/b2.txt rename to internal/render/assets/b2.txt diff --git a/internal/views/test_assets/b3.txt b/internal/render/assets/b3.txt similarity index 100% rename from internal/views/test_assets/b3.txt rename to internal/render/assets/b3.txt diff --git a/internal/views/test_assets/b4.txt b/internal/render/assets/b4.txt similarity index 100% rename from internal/views/test_assets/b4.txt rename to internal/render/assets/b4.txt diff --git a/internal/render/assets/cj.json b/internal/render/assets/cj.json new file mode 100644 index 00000000..25d1ed72 --- /dev/null +++ b/internal/render/assets/cj.json @@ -0,0 +1,59 @@ +{ + "apiVersion": "batch/v1beta1", + "kind": "CronJob", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"batch/v1beta1\",\"kind\":\"CronJob\",\"metadata\":{\"annotations\":{},\"name\":\"hello\",\"namespace\":\"default\"},\"spec\":{\"concurrencyPolicy\":\"Forbid\",\"jobTemplate\":{\"spec\":{\"template\":{\"spec\":{\"containers\":[{\"args\":[\"/bin/bash\",\"-c\",\"for i in {1..5}; do echo c1 $i; sleep 1; done\"],\"image\":\"blang/busybox-bash\",\"name\":\"c1\"}],\"restartPolicy\":\"OnFailure\"}}}},\"schedule\":\"*/1 * * * *\"}}\n" + }, + "creationTimestamp": "2019-08-30T15:19:01Z", + "name": "hello", + "namespace": "default", + "resourceVersion": "49753699", + "selfLink": "/apis/batch/v1beta1/namespaces/default/cronjobs/hello", + "uid": "7f0b856c-cb39-11e9-990f-42010a800218" + }, + "spec": { + "concurrencyPolicy": "Forbid", + "failedJobsHistoryLimit": 1, + "jobTemplate": { + "metadata": { + "creationTimestamp": null + }, + "spec": { + "template": { + "metadata": { + "creationTimestamp": null + }, + "spec": { + "containers": [ + { + "args": [ + "/bin/bash", + "-c", + "for i in {1..5}; do echo c1 $i; sleep 1; done" + ], + "image": "blang/busybox-bash", + "imagePullPolicy": "Always", + "name": "c1", + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "OnFailure", + "schedulerName": "default-scheduler", + "securityContext": {}, + "terminationGracePeriodSeconds": 30 + } + } + } + }, + "schedule": "*/1 * * * *", + "successfulJobsHistoryLimit": 3, + "suspend": false + }, + "status": { + "lastScheduleTime": "2019-08-30T17:01:00Z" + } +} \ No newline at end of file diff --git a/internal/render/assets/cm.json b/internal/render/assets/cm.json new file mode 100644 index 00000000..c8705071 --- /dev/null +++ b/internal/render/assets/cm.json @@ -0,0 +1,19 @@ +{ + "apiVersion": "v1", + "data": { + "key1": "very", + "key2": "charm" + }, + "kind": "ConfigMap", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"data\":{\"key1\":\"very\",\"key2\":\"charm\"},\"kind\":\"ConfigMap\",\"metadata\":{\"annotations\":{},\"name\":\"blee\",\"namespace\":\"default\"}}\n" + }, + "creationTimestamp": "2019-06-05T21:56:55Z", + "name": "blee", + "namespace": "default", + "resourceVersion": "27009817", + "selfLink": "/api/v1/namespaces/default/configmaps/blee", + "uid": "d587a666-87dc-11e9-a8e8-42010a80015b" + } +} \ No newline at end of file diff --git a/internal/render/assets/cr.json b/internal/render/assets/cr.json new file mode 100644 index 00000000..39a576ce --- /dev/null +++ b/internal/render/assets/cr.json @@ -0,0 +1,69 @@ +{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"rbac.authorization.k8s.io/v1\",\"kind\":\"ClusterRole\",\"metadata\":{\"annotations\":{},\"name\":\"blee\"},\"rules\":[{\"apiGroups\":[\"metrics.k8s.io\"],\"resources\":[\"nodes\"],\"verbs\":[\"list\",\"watch\"]},{\"apiGroups\":[\"\"],\"resources\":[\"nodes\",\"configmaps\"],\"verbs\":[\"list\"]},{\"apiGroups\":[\"\"],\"resourceNames\":[\"kube-system\"],\"resources\":[\"namespaces\"],\"verbs\":[\"get\",\"watch\"]},{\"apiGroups\":[\"\"],\"resources\":[\"pods\"],\"verbs\":[\"get\",\"list\",\"watch\",\"delete\"]}]}\n" + }, + "creationTimestamp": "2019-06-04T16:48:34Z", + "name": "blee", + "resourceVersion": "26708289", + "selfLink": "/apis/rbac.authorization.k8s.io/v1/clusterroles/blee", + "uid": "97dbe984-86e8-11e9-a8e8-42010a80015b" + }, + "rules": [ + { + "apiGroups": [ + "metrics.k8s.io" + ], + "resources": [ + "nodes" + ], + "verbs": [ + "list", + "watch" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "nodes", + "configmaps" + ], + "verbs": [ + "list" + ] + }, + { + "apiGroups": [ + "" + ], + "resourceNames": [ + "kube-system" + ], + "resources": [ + "namespaces" + ], + "verbs": [ + "get", + "watch" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "pods" + ], + "verbs": [ + "get", + "list", + "watch", + "delete" + ] + } + ] +} \ No newline at end of file diff --git a/internal/render/assets/crb.json b/internal/render/assets/crb.json new file mode 100644 index 00000000..297a1a8b --- /dev/null +++ b/internal/render/assets/crb.json @@ -0,0 +1,26 @@ +{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRoleBinding", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"rbac.authorization.k8s.io/v1\",\"kind\":\"ClusterRoleBinding\",\"metadata\":{\"annotations\":{},\"name\":\"blee\"},\"roleRef\":{\"apiGroup\":\"rbac.authorization.k8s.io\",\"kind\":\"ClusterRole\",\"name\":\"blee\"},\"subjects\":[{\"apiGroup\":\"rbac.authorization.k8s.io\",\"kind\":\"User\",\"name\":\"fernand\"}]}\n" + }, + "creationTimestamp": "2019-06-04T16:48:35Z", + "name": "blee", + "resourceVersion": "26689100", + "selfLink": "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/blee", + "uid": "97e5f84d-86e8-11e9-a8e8-42010a80015b" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "ClusterRole", + "name": "blee" + }, + "subjects": [ + { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "User", + "name": "fernand" + } + ] +} \ No newline at end of file diff --git a/internal/render/assets/crd.json b/internal/render/assets/crd.json new file mode 100644 index 00000000..2f0db2b1 --- /dev/null +++ b/internal/render/assets/crd.json @@ -0,0 +1,84 @@ +{ + "apiVersion": "apiextensions.k8s.io/v1beta1", + "kind": "CustomResourceDefinition", + "metadata": { + "annotations": { + "helm.sh/hook": "crd-install", + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apiextensions.k8s.io/v1beta1\",\"kind\":\"CustomResourceDefinition\",\"metadata\":{\"annotations\":{\"helm.sh/hook\":\"crd-install\"},\"labels\":{\"addonmanager.kubernetes.io/mode\":\"Reconcile\",\"app\":\"mixer\",\"istio\":\"mixer-adapter\",\"k8s-app\":\"istio\",\"package\":\"adapter\"},\"name\":\"adapters.config.istio.io\",\"namespace\":\"\"},\"spec\":{\"group\":\"config.istio.io\",\"names\":{\"categories\":[\"istio-io\",\"policy-istio-io\"],\"kind\":\"adapter\",\"plural\":\"adapters\",\"singular\":\"adapter\"},\"scope\":\"Namespaced\",\"version\":\"v1alpha2\"}}\n" + }, + "creationTimestamp": "2019-02-05T22:04:29Z", + "generation": 1, + "labels": { + "addonmanager.kubernetes.io/mode": "Reconcile", + "app": "mixer", + "istio": "mixer-adapter", + "k8s-app": "istio", + "package": "adapter" + }, + "name": "adapters.config.istio.io", + "resourceVersion": "37115599", + "selfLink": "/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions/adapters.config.istio.io", + "uid": "029b8c3e-2992-11e9-81cd-42010a80005b" + }, + "spec": { + "additionalPrinterColumns": [ + { + "JSONPath": ".metadata.creationTimestamp", + "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata", + "name": "Age", + "type": "date" + } + ], + "group": "config.istio.io", + "names": { + "categories": [ + "istio-io", + "policy-istio-io" + ], + "kind": "adapter", + "listKind": "adapterList", + "plural": "adapters", + "singular": "adapter" + }, + "scope": "Namespaced", + "version": "v1alpha2", + "versions": [ + { + "name": "v1alpha2", + "served": true, + "storage": true + } + ] + }, + "status": { + "acceptedNames": { + "categories": [ + "istio-io", + "policy-istio-io" + ], + "kind": "adapter", + "listKind": "adapterList", + "plural": "adapters", + "singular": "adapter" + }, + "conditions": [ + { + "lastTransitionTime": "2019-02-05T22:04:29Z", + "message": "no conflicts found", + "reason": "NoConflicts", + "status": "True", + "type": "NamesAccepted" + }, + { + "lastTransitionTime": null, + "message": "the initial names have been accepted", + "reason": "InitialNamesAccepted", + "status": "True", + "type": "Established" + } + ], + "storedVersions": [ + "v1alpha2" + ] + } +} \ No newline at end of file diff --git a/internal/render/assets/dp.json b/internal/render/assets/dp.json new file mode 100644 index 00000000..f28f88b2 --- /dev/null +++ b/internal/render/assets/dp.json @@ -0,0 +1,123 @@ +{ + "apiVersion": "extensions/v1beta1", + "kind": "Deployment", + "metadata": { + "annotations": { + "deployment.kubernetes.io/revision": "1", + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1beta1\",\"kind\":\"Deployment\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"icx-db\"},\"name\":\"icx-db\",\"namespace\":\"icx\"},\"spec\":{\"replicas\":1,\"selector\":{\"matchLabels\":{\"app\":\"icx-db\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"icx-db\"}},\"spec\":{\"containers\":[{\"env\":[{\"name\":\"POSTGRES_USER\",\"valueFrom\":{\"secretKeyRef\":{\"key\":\"pg_user\",\"name\":\"icx-creds\"}}},{\"name\":\"POSTGRES_PASSWORD\",\"valueFrom\":{\"secretKeyRef\":{\"key\":\"pg_pwd\",\"name\":\"icx-creds\"}}}],\"image\":\"postgres:9.2-alpine\",\"imagePullPolicy\":\"IfNotPresent\",\"name\":\"icx-db\",\"ports\":[{\"containerPort\":5432,\"name\":\"client\"}],\"resources\":{\"limits\":{\"cpu\":\"250m\",\"memory\":\"512Mi\"},\"requests\":{\"cpu\":\"250m\",\"memory\":\"256Mi\"}}}]}}}}\n" + }, + "creationTimestamp": "2019-07-14T04:54:17Z", + "generation": 1, + "labels": { + "app": "icx-db" + }, + "name": "icx-db", + "namespace": "icx", + "resourceVersion": "37116271", + "selfLink": "/apis/extensions/v1beta1/namespaces/icx/deployments/icx-db", + "uid": "6f6143bc-a5f3-11e9-990f-42010a800218" + }, + "spec": { + "progressDeadlineSeconds": 600, + "replicas": 1, + "revisionHistoryLimit": 2, + "selector": { + "matchLabels": { + "app": "icx-db" + } + }, + "strategy": { + "rollingUpdate": { + "maxSurge": "25%", + "maxUnavailable": "25%" + }, + "type": "RollingUpdate" + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "icx-db" + } + }, + "spec": { + "containers": [ + { + "env": [ + { + "name": "POSTGRES_USER", + "valueFrom": { + "secretKeyRef": { + "key": "pg_user", + "name": "icx-creds" + } + } + }, + { + "name": "POSTGRES_PASSWORD", + "valueFrom": { + "secretKeyRef": { + "key": "pg_pwd", + "name": "icx-creds" + } + } + } + ], + "image": "postgres:9.2-alpine", + "imagePullPolicy": "IfNotPresent", + "name": "icx-db", + "ports": [ + { + "containerPort": 5432, + "name": "client", + "protocol": "TCP" + } + ], + "resources": { + "limits": { + "cpu": "250m", + "memory": "512Mi" + }, + "requests": { + "cpu": "250m", + "memory": "256Mi" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "terminationGracePeriodSeconds": 30 + } + } + }, + "status": { + "availableReplicas": 1, + "conditions": [ + { + "lastTransitionTime": "2019-07-14T04:54:20Z", + "lastUpdateTime": "2019-07-14T04:54:20Z", + "message": "Deployment has minimum availability.", + "reason": "MinimumReplicasAvailable", + "status": "True", + "type": "Available" + }, + { + "lastTransitionTime": "2019-07-14T04:54:17Z", + "lastUpdateTime": "2019-07-14T04:54:20Z", + "message": "ReplicaSet \"icx-db-7d4b578979\" has successfully progressed.", + "reason": "NewReplicaSetAvailable", + "status": "True", + "type": "Progressing" + } + ], + "observedGeneration": 1, + "readyReplicas": 1, + "replicas": 1, + "updatedReplicas": 1 + } +} \ No newline at end of file diff --git a/internal/render/assets/ds.json b/internal/render/assets/ds.json new file mode 100644 index 00000000..d8dd6c9b --- /dev/null +++ b/internal/render/assets/ds.json @@ -0,0 +1,207 @@ +{ + "apiVersion": "extensions/v1beta1", + "kind": "DaemonSet", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"extensions/v1beta1\",\"kind\":\"DaemonSet\",\"metadata\":{\"annotations\":{},\"labels\":{\"addonmanager.kubernetes.io/mode\":\"Reconcile\",\"k8s-app\":\"fluentd-gcp\",\"kubernetes.io/cluster-service\":\"true\",\"version\":\"v3.2.0\"},\"name\":\"fluentd-gcp-v3.2.0\",\"namespace\":\"kube-system\"},\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"scheduler.alpha.kubernetes.io/critical-pod\":\"\"},\"labels\":{\"k8s-app\":\"fluentd-gcp\",\"kubernetes.io/cluster-service\":\"true\",\"version\":\"v3.2.0\"}},\"spec\":{\"containers\":[{\"env\":[{\"name\":\"NODE_NAME\",\"valueFrom\":{\"fieldRef\":{\"apiVersion\":\"v1\",\"fieldPath\":\"spec.nodeName\"}}},{\"name\":\"STACKDRIVER_METADATA_AGENT_URL\",\"value\":\"http://$(NODE_NAME):8799\"}],\"image\":\"gcr.io/stackdriver-agents/stackdriver-logging-agent:0.6-1.6.0-1\",\"livenessProbe\":{\"exec\":{\"command\":[\"/bin/sh\",\"-c\",\"LIVENESS_THRESHOLD_SECONDS=${LIVENESS_THRESHOLD_SECONDS:-300}; STUCK_THRESHOLD_SECONDS=${LIVENESS_THRESHOLD_SECONDS:-900}; if [ ! -e /var/log/fluentd-buffers ]; then\\n exit 1;\\nfi; touch -d \\\"${STUCK_THRESHOLD_SECONDS} seconds ago\\\" /tmp/marker-stuck; if [[ -z \\\"$(find /var/log/fluentd-buffers -type f -newer /tmp/marker-stuck -print -quit)\\\" ]]; then\\n rm -rf /var/log/fluentd-buffers;\\n exit 1;\\nfi; touch -d \\\"${LIVENESS_THRESHOLD_SECONDS} seconds ago\\\" /tmp/marker-liveness; if [[ -z \\\"$(find /var/log/fluentd-buffers -type f -newer /tmp/marker-liveness -print -quit)\\\" ]]; then\\n exit 1;\\nfi;\\n\"]},\"initialDelaySeconds\":600,\"periodSeconds\":60},\"name\":\"fluentd-gcp\",\"volumeMounts\":[{\"mountPath\":\"/var/log\",\"name\":\"varlog\"},{\"mountPath\":\"/var/lib/docker/containers\",\"name\":\"varlibdockercontainers\",\"readOnly\":true},{\"mountPath\":\"/etc/google-fluentd/config.d\",\"name\":\"config-volume\"}]},{\"command\":[\"/monitor\",\"--stackdriver-prefix=container.googleapis.com/internal/addons\",\"--api-override=https://monitoring.googleapis.com/\",\"--source=fluentd:http://localhost:24231?whitelisted=stackdriver_successful_requests_count,stackdriver_failed_requests_count,stackdriver_ingested_entries_count,stackdriver_dropped_entries_count\",\"--pod-id=$(POD_NAME)\",\"--namespace-id=$(POD_NAMESPACE)\"],\"env\":[{\"name\":\"POD_NAME\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.name\"}}},{\"name\":\"POD_NAMESPACE\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.namespace\"}}}],\"image\":\"k8s.gcr.io/prometheus-to-sd:v0.3.1\",\"name\":\"prometheus-to-sd-exporter\"}],\"dnsPolicy\":\"Default\",\"hostNetwork\":true,\"nodeSelector\":{\"beta.kubernetes.io/fluentd-ds-ready\":\"true\"},\"priorityClassName\":\"system-node-critical\",\"serviceAccountName\":\"fluentd-gcp\",\"terminationGracePeriodSeconds\":60,\"tolerations\":[{\"effect\":\"NoExecute\",\"operator\":\"Exists\"},{\"effect\":\"NoSchedule\",\"operator\":\"Exists\"}],\"volumes\":[{\"hostPath\":{\"path\":\"/var/log\"},\"name\":\"varlog\"},{\"hostPath\":{\"path\":\"/var/lib/docker/containers\"},\"name\":\"varlibdockercontainers\"},{\"configMap\":{\"name\":\"fluentd-gcp-config-old-v1.2.5\"},\"name\":\"config-volume\"}]}},\"updateStrategy\":{\"type\":\"RollingUpdate\"}}}\n" + }, + "creationTimestamp": "2019-04-12T23:35:36Z", + "generation": 2, + "labels": { + "addonmanager.kubernetes.io/mode": "Reconcile", + "k8s-app": "fluentd-gcp", + "kubernetes.io/cluster-service": "true", + "version": "v3.2.0" + }, + "name": "fluentd-gcp-v3.2.0", + "namespace": "kube-system", + "resourceVersion": "34805583", + "selfLink": "/apis/extensions/v1beta1/namespaces/kube-system/daemonsets/fluentd-gcp-v3.2.0", + "uid": "ac95611f-5d7b-11e9-af05-42010a800018" + }, + "spec": { + "revisionHistoryLimit": 10, + "selector": { + "matchLabels": { + "k8s-app": "fluentd-gcp", + "kubernetes.io/cluster-service": "true", + "version": "v3.2.0" + } + }, + "template": { + "metadata": { + "annotations": { + "scheduler.alpha.kubernetes.io/critical-pod": "" + }, + "creationTimestamp": null, + "labels": { + "k8s-app": "fluentd-gcp", + "kubernetes.io/cluster-service": "true", + "version": "v3.2.0" + } + }, + "spec": { + "containers": [ + { + "env": [ + { + "name": "NODE_NAME", + "valueFrom": { + "fieldRef": { + "apiVersion": "v1", + "fieldPath": "spec.nodeName" + } + } + }, + { + "name": "STACKDRIVER_METADATA_AGENT_URL", + "value": "http://$(NODE_NAME):8799" + } + ], + "image": "gcr.io/stackdriver-agents/stackdriver-logging-agent:0.6-1.6.0-1", + "imagePullPolicy": "IfNotPresent", + "livenessProbe": { + "exec": { + "command": [ + "/bin/sh", + "-c", + "LIVENESS_THRESHOLD_SECONDS=${LIVENESS_THRESHOLD_SECONDS:-300}; STUCK_THRESHOLD_SECONDS=${LIVENESS_THRESHOLD_SECONDS:-900}; if [ ! -e /var/log/fluentd-buffers ]; then\n exit 1;\nfi; touch -d \"${STUCK_THRESHOLD_SECONDS} seconds ago\" /tmp/marker-stuck; if [[ -z \"$(find /var/log/fluentd-buffers -type f -newer /tmp/marker-stuck -print -quit)\" ]]; then\n rm -rf /var/log/fluentd-buffers;\n exit 1;\nfi; touch -d \"${LIVENESS_THRESHOLD_SECONDS} seconds ago\" /tmp/marker-liveness; if [[ -z \"$(find /var/log/fluentd-buffers -type f -newer /tmp/marker-liveness -print -quit)\" ]]; then\n exit 1;\nfi;\n" + ] + }, + "failureThreshold": 3, + "initialDelaySeconds": 600, + "periodSeconds": 60, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "name": "fluentd-gcp", + "resources": { + "limits": { + "cpu": "1", + "memory": "500Mi" + }, + "requests": { + "cpu": "100m", + "memory": "200Mi" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/log", + "name": "varlog" + }, + { + "mountPath": "/var/lib/docker/containers", + "name": "varlibdockercontainers", + "readOnly": true + }, + { + "mountPath": "/etc/google-fluentd/config.d", + "name": "config-volume" + } + ] + }, + { + "command": [ + "/monitor", + "--stackdriver-prefix=container.googleapis.com/internal/addons", + "--api-override=https://monitoring.googleapis.com/", + "--source=fluentd:http://localhost:24231?whitelisted=stackdriver_successful_requests_count,stackdriver_failed_requests_count,stackdriver_ingested_entries_count,stackdriver_dropped_entries_count", + "--pod-id=$(POD_NAME)", + "--namespace-id=$(POD_NAMESPACE)" + ], + "env": [ + { + "name": "POD_NAME", + "valueFrom": { + "fieldRef": { + "apiVersion": "v1", + "fieldPath": "metadata.name" + } + } + }, + { + "name": "POD_NAMESPACE", + "valueFrom": { + "fieldRef": { + "apiVersion": "v1", + "fieldPath": "metadata.namespace" + } + } + } + ], + "image": "k8s.gcr.io/prometheus-to-sd:v0.3.1", + "imagePullPolicy": "IfNotPresent", + "name": "prometheus-to-sd-exporter", + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "dnsPolicy": "Default", + "hostNetwork": true, + "nodeSelector": { + "beta.kubernetes.io/fluentd-ds-ready": "true" + }, + "priorityClassName": "system-node-critical", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "fluentd-gcp", + "serviceAccountName": "fluentd-gcp", + "terminationGracePeriodSeconds": 60, + "tolerations": [ + { + "effect": "NoExecute", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "operator": "Exists" + } + ], + "volumes": [ + { + "hostPath": { + "path": "/var/log", + "type": "" + }, + "name": "varlog" + }, + { + "hostPath": { + "path": "/var/lib/docker/containers", + "type": "" + }, + "name": "varlibdockercontainers" + }, + { + "configMap": { + "defaultMode": 420, + "name": "fluentd-gcp-config-old-v1.2.5" + }, + "name": "config-volume" + } + ] + } + }, + "templateGeneration": 2, + "updateStrategy": { + "rollingUpdate": { + "maxUnavailable": 1 + }, + "type": "RollingUpdate" + } + }, + "status": { + "currentNumberScheduled": 2, + "desiredNumberScheduled": 2, + "numberAvailable": 2, + "numberMisscheduled": 0, + "numberReady": 2, + "observedGeneration": 2, + "updatedNumberScheduled": 2 + } +} \ No newline at end of file diff --git a/internal/render/assets/ep.json b/internal/render/assets/ep.json new file mode 100644 index 00000000..659472af --- /dev/null +++ b/internal/render/assets/ep.json @@ -0,0 +1,12 @@ +{ + "apiVersion": "v1", + "kind": "Endpoints", + "metadata": { + "creationTimestamp": "2019-07-10T23:10:43Z", + "name": "dictionary1", + "namespace": "default", + "resourceVersion": "36684456", + "selfLink": "/api/v1/namespaces/default/endpoints/dictionary1", + "uid": "f119c74f-a367-11e9-990f-42010a800218" + } +} \ No newline at end of file diff --git a/internal/render/assets/ev.json b/internal/render/assets/ev.json new file mode 100644 index 00000000..78dca2a7 --- /dev/null +++ b/internal/render/assets/ev.json @@ -0,0 +1,34 @@ +{ + "apiVersion": "v1", + "count": 1, + "eventTime": null, + "firstTimestamp": "2019-08-30T20:43:05Z", + "involvedObject": { + "apiVersion": "v1", + "fieldPath": "spec.containers{c1}", + "kind": "Pod", + "name": "hello-1567197780-mn4mv", + "namespace": "default", + "resourceVersion": "49798867", + "uid": "c31fdeb8-cb66-11e9-990f-42010a800218" + }, + "kind": "Event", + "lastTimestamp": "2019-08-30T20:43:05Z", + "message": "Successfully pulled image \"blang/busybox-bash\"", + "metadata": { + "creationTimestamp": "2019-08-30T20:43:05Z", + "name": "hello-1567197780-mn4mv.15bfce150bd764dd", + "namespace": "default", + "resourceVersion": "590733", + "selfLink": "/api/v1/namespaces/default/events/hello-1567197780-mn4mv.15bfce150bd764dd", + "uid": "c443d4b3-cb66-11e9-990f-42010a800218" + }, + "reason": "Pulled", + "reportingComponent": "", + "reportingInstance": "", + "source": { + "component": "kubelet", + "host": "gke-k9s-default-pool-0fa2fb89-qnkc" + }, + "type": "Normal" +} \ No newline at end of file diff --git a/internal/render/assets/hpa.json b/internal/render/assets/hpa.json new file mode 100644 index 00000000..6b8db65d --- /dev/null +++ b/internal/render/assets/hpa.json @@ -0,0 +1,30 @@ +{ + "apiVersion": "autoscaling/v1", + "kind": "HorizontalPodAutoscaler", + "metadata": { + "annotations": { + "autoscaling.alpha.kubernetes.io/conditions": "[{\"type\":\"AbleToScale\",\"status\":\"False\",\"lastTransitionTime\":\"2019-07-19T20:56:05Z\",\"reason\":\"FailedGetScale\",\"message\":\"the HPA controller was unable to get the target's current scale: deployments/scale.extensions \\\"nginx\\\" not found\"}]", + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"autoscaling/v1\",\"kind\":\"HorizontalPodAutoscaler\",\"metadata\":{\"annotations\":{},\"name\":\"nginx\",\"namespace\":\"default\"},\"spec\":{\"maxReplicas\":10,\"minReplicas\":1,\"scaleTargetRef\":{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"name\":\"nginx\"},\"targetCPUUtilizationPercentage\":10}}\n" + }, + "creationTimestamp": "2019-07-19T20:55:50Z", + "name": "nginx", + "namespace": "default", + "resourceVersion": "38623948", + "selfLink": "/apis/autoscaling/v1/namespaces/default/horizontalpodautoscalers/nginx", + "uid": "97104229-aa67-11e9-990f-42010a800218" + }, + "spec": { + "maxReplicas": 10, + "minReplicas": 1, + "scaleTargetRef": { + "apiVersion": "apps/v1", + "kind": "Deployment", + "name": "nginx" + }, + "targetCPUUtilizationPercentage": 10 + }, + "status": { + "currentReplicas": 0, + "desiredReplicas": 0 + } +} \ No newline at end of file diff --git a/internal/render/assets/ing.json b/internal/render/assets/ing.json new file mode 100644 index 00000000..4dfb8b7f --- /dev/null +++ b/internal/render/assets/ing.json @@ -0,0 +1,37 @@ +{ + "apiVersion": "extensions/v1beta1", + "kind": "Ingress", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"extensions/v1beta1\",\"kind\":\"Ingress\",\"metadata\":{\"annotations\":{\"nginx.ingress.kubernetes.io/rewrite-target\":\"/\"},\"name\":\"test-ingress\",\"namespace\":\"default\"},\"spec\":{\"rules\":[{\"http\":{\"paths\":[{\"backend\":{\"serviceName\":\"test\",\"servicePort\":80},\"path\":\"/testpath\"}]}}]}}\n", + "nginx.ingress.kubernetes.io/rewrite-target": "/" + }, + "creationTimestamp": "2019-08-30T20:53:52Z", + "generation": 1, + "name": "test-ingress", + "namespace": "default", + "resourceVersion": "49801063", + "selfLink": "/apis/extensions/v1beta1/namespaces/default/ingresses/test-ingress", + "uid": "45e44c1d-cb68-11e9-990f-42010a800218" + }, + "spec": { + "rules": [ + { + "http": { + "paths": [ + { + "backend": { + "serviceName": "test", + "servicePort": 80 + }, + "path": "/testpath" + } + ] + } + } + ] + }, + "status": { + "loadBalancer": {} + } +} \ No newline at end of file diff --git a/internal/render/assets/job.json b/internal/render/assets/job.json new file mode 100644 index 00000000..0ffc2152 --- /dev/null +++ b/internal/render/assets/job.json @@ -0,0 +1,80 @@ +{ + "apiVersion": "batch/v1", + "kind": "Job", + "metadata": { + "creationTimestamp": "2019-08-30T15:33:02Z", + "labels": { + "controller-uid": "7473e6d0-cb3b-11e9-990f-42010a800218", + "job-name": "hello-1567179180" + }, + "name": "hello-1567179180", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "batch/v1beta1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "CronJob", + "name": "hello", + "uid": "7f0b856c-cb39-11e9-990f-42010a800218" + } + ], + "resourceVersion": "49735780", + "selfLink": "/apis/batch/v1/namespaces/default/jobs/hello-1567179180", + "uid": "7473e6d0-cb3b-11e9-990f-42010a800218" + }, + "spec": { + "backoffLimit": 6, + "completions": 1, + "parallelism": 1, + "selector": { + "matchLabels": { + "controller-uid": "7473e6d0-cb3b-11e9-990f-42010a800218" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "controller-uid": "7473e6d0-cb3b-11e9-990f-42010a800218", + "job-name": "hello-1567179180" + } + }, + "spec": { + "containers": [ + { + "args": [ + "/bin/bash", + "-c", + "for i in {1..5}; do echo c1 $i; sleep 1; done" + ], + "image": "blang/busybox-bash", + "imagePullPolicy": "Always", + "name": "c1", + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "OnFailure", + "schedulerName": "default-scheduler", + "securityContext": {}, + "terminationGracePeriodSeconds": 30 + } + } + }, + "status": { + "completionTime": "2019-08-30T15:33:10Z", + "conditions": [ + { + "lastProbeTime": "2019-08-30T15:33:10Z", + "lastTransitionTime": "2019-08-30T15:33:10Z", + "status": "True", + "type": "Complete" + } + ], + "startTime": "2019-08-30T15:33:02Z", + "succeeded": 1 + } +} \ No newline at end of file diff --git a/internal/render/assets/no.json b/internal/render/assets/no.json new file mode 100644 index 00000000..8b7d3fb5 --- /dev/null +++ b/internal/render/assets/no.json @@ -0,0 +1,217 @@ +{ + "apiVersion": "v1", + "kind": "Node", + "metadata": { + "annotations": { + "kubeadm.alpha.kubernetes.io/cri-socket": "/var/run/dockershim.sock", + "node.alpha.kubernetes.io/ttl": "0", + "volumes.kubernetes.io/controller-managed-attach-detach": "true" + }, + "creationTimestamp": "2019-08-26T21:52:09Z", + "labels": { + "beta.kubernetes.io/arch": "amd64", + "beta.kubernetes.io/os": "linux", + "kubernetes.io/arch": "amd64", + "kubernetes.io/hostname": "minikube", + "kubernetes.io/os": "linux", + "node-role.kubernetes.io/master": "" + }, + "name": "minikube", + "resourceVersion": "500588", + "selfLink": "/api/v1/nodes/minikube", + "uid": "3a554aa2-fee7-435b-ae1b-e67bdaac069a" + }, + "spec": {}, + "status": { + "addresses": [ + { + "address": "192.168.64.107", + "type": "InternalIP" + }, + { + "address": "minikube", + "type": "Hostname" + } + ], + "allocatable": { + "cpu": "4", + "ephemeral-storage": "15625027559", + "hugepages-2Mi": "0", + "memory": "8063156Ki", + "pods": "110" + }, + "capacity": { + "cpu": "4", + "ephemeral-storage": "16954240Ki", + "hugepages-2Mi": "0", + "memory": "8165556Ki", + "pods": "110" + }, + "conditions": [ + { + "lastHeartbeatTime": "2019-08-31T04:43:11Z", + "lastTransitionTime": "2019-08-26T21:52:06Z", + "message": "kubelet has sufficient memory available", + "reason": "KubeletHasSufficientMemory", + "status": "False", + "type": "MemoryPressure" + }, + { + "lastHeartbeatTime": "2019-08-31T04:43:11Z", + "lastTransitionTime": "2019-08-26T21:52:06Z", + "message": "kubelet has no disk pressure", + "reason": "KubeletHasNoDiskPressure", + "status": "False", + "type": "DiskPressure" + }, + { + "lastHeartbeatTime": "2019-08-31T04:43:11Z", + "lastTransitionTime": "2019-08-26T21:52:06Z", + "message": "kubelet has sufficient PID available", + "reason": "KubeletHasSufficientPID", + "status": "False", + "type": "PIDPressure" + }, + { + "lastHeartbeatTime": "2019-08-31T04:43:11Z", + "lastTransitionTime": "2019-08-26T21:52:06Z", + "message": "kubelet is posting ready status", + "reason": "KubeletReady", + "status": "True", + "type": "Ready" + } + ], + "daemonEndpoints": { + "kubeletEndpoint": { + "Port": 10250 + } + }, + "images": [ + { + "names": [ + "quay.io/kubernetes-ingress-controller/nginx-ingress-controller@sha256:464db4880861bd9d1e74e67a4a9c975a6e74c1e9968776d8d4cc73492a56dfa5", + "quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.25.0" + ], + "sizeBytes": 508299926 + }, + { + "names": [ + "k8s.gcr.io/etcd@sha256:17da501f5d2a675be46040422a27b7cc21b8a43895ac998b171db1c346f361f7", + "k8s.gcr.io/etcd:3.3.10" + ], + "sizeBytes": 258116302 + }, + { + "names": [ + "k8s.gcr.io/kube-apiserver@sha256:5fae387bacf1def6c3915b4a3035cf8c8a4d06158b2e676721776d3d4afc05a2", + "k8s.gcr.io/kube-apiserver:v1.15.2" + ], + "sizeBytes": 206823358 + }, + { + "names": [ + "k8s.gcr.io/kube-controller-manager@sha256:7d3fc48cf83aa0a7b8f129fa4255bb5530908e1a5b194be269ea8329b48e9598", + "k8s.gcr.io/kube-controller-manager:v1.15.2" + ], + "sizeBytes": 158718526 + }, + { + "names": [ + "k8s.gcr.io/kubernetes-dashboard-amd64:v1.10.1" + ], + "sizeBytes": 121711221 + }, + { + "names": [ + "k8s.gcr.io/nginx-slim@sha256:8b4501fe0fe221df663c22e16539f399e89594552f400408303c42f3dd8d0e52", + "k8s.gcr.io/nginx-slim:0.8" + ], + "sizeBytes": 110487599 + }, + { + "names": [ + "k8s.gcr.io/kube-addon-manager:v9.0" + ], + "sizeBytes": 83077558 + }, + { + "names": [ + "k8s.gcr.io/kube-proxy@sha256:626f983f25f8b7799ca7ab001fd0985a72c2643c0acb877d2888c0aa4fcbdf56", + "k8s.gcr.io/kube-proxy:v1.15.2" + ], + "sizeBytes": 82408284 + }, + { + "names": [ + "k8s.gcr.io/kube-scheduler@sha256:8fd3c3251f07234a234469e201900e4274726f1fe0d5dc6fb7da911f1c851a1a", + "k8s.gcr.io/kube-scheduler:v1.15.2" + ], + "sizeBytes": 81107582 + }, + { + "names": [ + "gcr.io/k8s-minikube/storage-provisioner:v1.8.1" + ], + "sizeBytes": 80815640 + }, + { + "names": [ + "k8s.gcr.io/k8s-dns-kube-dns-amd64:1.14.13" + ], + "sizeBytes": 51157394 + }, + { + "names": [ + "k8s.gcr.io/k8s-dns-sidecar-amd64:1.14.13" + ], + "sizeBytes": 42852039 + }, + { + "names": [ + "k8s.gcr.io/metrics-server-amd64@sha256:49a9f12f7067d11f42c803dbe61ed2c1299959ad85cb315b25ff7eef8e6b8892", + "k8s.gcr.io/metrics-server-amd64:v0.2.1" + ], + "sizeBytes": 42541759 + }, + { + "names": [ + "k8s.gcr.io/k8s-dns-dnsmasq-nanny-amd64:1.14.13" + ], + "sizeBytes": 41372492 + }, + { + "names": [ + "k8s.gcr.io/coredns@sha256:02382353821b12c21b062c59184e227e001079bb13ebd01f9d3270ba0fcbf1e4", + "k8s.gcr.io/coredns:1.3.1" + ], + "sizeBytes": 40303560 + }, + { + "names": [ + "blang/busybox-bash@sha256:b4675e303209bfdaeb6cad4c0c90ec3ba2cda85a75b5d965daa91bca86d0d77c", + "blang/busybox-bash:latest" + ], + "sizeBytes": 5912460 + }, + { + "names": [ + "k8s.gcr.io/pause@sha256:f78411e19d84a252e53bff71a4407a5686c46983a2c2eeed83929b888179acea", + "k8s.gcr.io/pause:3.1" + ], + "sizeBytes": 742472 + } + ], + "nodeInfo": { + "architecture": "amd64", + "bootID": "97588c94-edf3-420d-b5ef-226d5a27d348", + "containerRuntimeVersion": "docker://18.9.8", + "kernelVersion": "4.15.0", + "kubeProxyVersion": "v1.15.2", + "kubeletVersion": "v1.15.2", + "machineID": "fc8b6c7d6c8449bf9066f42449d97619", + "operatingSystem": "linux", + "osImage": "Buildroot 2018.05.3", + "systemUUID": "98F211E9-0000-0000-AC5E-AC87A33863C5" + } + } +} \ No newline at end of file diff --git a/internal/render/assets/np.json b/internal/render/assets/np.json new file mode 100644 index 00000000..44138e40 --- /dev/null +++ b/internal/render/assets/np.json @@ -0,0 +1,80 @@ +{ + "apiVersion": "extensions/v1beta1", + "kind": "NetworkPolicy", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"networking.k8s.io/v1\",\"kind\":\"NetworkPolicy\",\"metadata\":{\"annotations\":{},\"name\":\"fred\",\"namespace\":\"default\"},\"spec\":{\"egress\":[{\"ports\":[{\"port\":5978,\"protocol\":\"TCP\"}],\"to\":[{\"ipBlock\":{\"cidr\":\"10.0.0.0/24\"}}]}],\"ingress\":[{\"from\":[{\"ipBlock\":{\"cidr\":\"172.17.0.0/16\",\"except\":[\"172.17.1.0/24\",\"172.17.3.0/24\",\"172.17.4.0/24\"]}},{\"namespaceSelector\":{\"matchLabels\":{\"app\":\"blee\"}}},{\"podSelector\":{\"matchLabels\":{\"app\":\"fred\"}}}],\"ports\":[{\"port\":6379,\"protocol\":\"TCP\"}]}],\"podSelector\":{\"matchLabels\":{\"app\":\"nginx\"}},\"policyTypes\":[\"Ingress\",\"Egress\"]}}\n" + }, + "creationTimestamp": "2019-08-27T19:07:20Z", + "generation": 2, + "name": "fred", + "namespace": "default", + "resourceVersion": "48999995", + "selfLink": "/apis/extensions/v1beta1/namespaces/default/networkpolicies/fred", + "uid": "e4aada4d-c8fd-11e9-990f-42010a800218" + }, + "spec": { + "egress": [ + { + "ports": [ + { + "port": 5978, + "protocol": "TCP" + } + ], + "to": [ + { + "ipBlock": { + "cidr": "10.0.0.0/24" + } + } + ] + } + ], + "ingress": [ + { + "from": [ + { + "ipBlock": { + "cidr": "172.17.0.0/16", + "except": [ + "172.17.1.0/24", + "172.17.3.0/24", + "172.17.4.0/24" + ] + } + }, + { + "namespaceSelector": { + "matchLabels": { + "app": "blee" + } + } + }, + { + "podSelector": { + "matchLabels": { + "app": "fred" + } + } + } + ], + "ports": [ + { + "port": 6379, + "protocol": "TCP" + } + ] + } + ], + "podSelector": { + "matchLabels": { + "app": "nginx" + } + }, + "policyTypes": [ + "Ingress", + "Egress" + ] + } +} \ No newline at end of file diff --git a/internal/render/assets/ns.json b/internal/render/assets/ns.json new file mode 100644 index 00000000..8d77e8bc --- /dev/null +++ b/internal/render/assets/ns.json @@ -0,0 +1,22 @@ +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"kube-system\",\"namespace\":\"\"}}\n" + }, + "creationTimestamp": "2019-02-05T22:03:54Z", + "name": "kube-system", + "resourceVersion": "36", + "selfLink": "/api/v1/namespaces/kube-system", + "uid": "ed757b6f-2991-11e9-81cd-42010a80005b" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} \ No newline at end of file diff --git a/internal/render/assets/pdb.json b/internal/render/assets/pdb.json new file mode 100644 index 00000000..0e4a3601 --- /dev/null +++ b/internal/render/assets/pdb.json @@ -0,0 +1,31 @@ +{ + "apiVersion": "policy/v1beta1", + "kind": "PodDisruptionBudget", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"policy/v1beta1\",\"kind\":\"PodDisruptionBudget\",\"metadata\":{\"annotations\":{},\"name\":\"fred\",\"namespace\":\"default\"},\"spec\":{\"minAvailable\":2,\"selector\":{\"matchLabels\":{\"app\":\"nginx\"}}}}\n" + }, + "creationTimestamp": "2019-08-31T03:48:10Z", + "generation": 1, + "name": "fred", + "namespace": "default", + "resourceVersion": "49885429", + "selfLink": "/apis/policy/v1beta1/namespaces/default/poddisruptionbudgets/fred", + "uid": "26b6cf70-cba2-11e9-990f-42010a800218" + }, + "spec": { + "minAvailable": 2, + "selector": { + "matchLabels": { + "app": "nginx" + } + } + }, + "status": { + "currentHealthy": 0, + "desiredHealthy": 2, + "disruptionsAllowed": 0, + "expectedPods": 0, + "observedGeneration": 1 + } +} \ No newline at end of file diff --git a/internal/render/assets/po.json b/internal/render/assets/po.json new file mode 100644 index 00000000..57d2c30b --- /dev/null +++ b/internal/render/assets/po.json @@ -0,0 +1,140 @@ +{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"name\":\"nginx\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"image\":\"nginx:alpine\",\"name\":\"nginx\",\"ports\":[{\"containerPort\":80}],\"volumeMounts\":[{\"mountPath\":\"/usr/share/nginx/html\",\"name\":\"index\"}]}],\"terminationGracePeriodSeconds\":0,\"volumes\":[{\"name\":\"index\",\"persistentVolumeClaim\":{\"claimName\":\"web\"}}]}}\n" + }, + "creationTimestamp": "2019-08-09T05:12:19Z", + "name": "nginx", + "namespace": "default", + "resourceVersion": "1482816", + "selfLink": "/api/v1/namespaces/default/pods/nginx", + "uid": "614908ed-415b-4506-8370-e3e36fa8cc13" + }, + "spec": { + "containers": [ + { + "image": "nginx:alpine", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": { + "limits": { + "memory": "170Mi" + }, + "requests": { + "cpu": "100m", + "memory": "70Mi" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/usr/share/nginx/html", + "name": "index" + }, + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-9ph8s", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "minikube", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 0, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + } + ], + "volumes": [ + { + "name": "index", + "persistentVolumeClaim": { + "claimName": "web" + } + }, + { + "name": "default-token-9ph8s", + "secret": { + "defaultMode": 420, + "secretName": "default-token-9ph8s" + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2019-08-09T05:12:19Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2019-08-09T05:12:21Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2019-08-09T05:12:21Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2019-08-09T05:12:19Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://421bd26d6c682f14b5ea1dcaf06e14a509b2b702fc7793e820520eb1e28e2eaf", + "image": "nginx:alpine", + "imageID": "docker-pullable://nginx@sha256:482ead44b2203fa32b3390abdaf97cbdc8ad15c07fb03a3e68d7c35a19ad7595", + "lastState": {}, + "name": "nginx", + "ready": true, + "restartCount": 0, + "state": { + "running": { + "startedAt": "2019-08-09T05:12:20Z" + } + } + } + ], + "hostIP": "192.168.64.104", + "phase": "Running", + "podIP": "172.17.0.6", + "qosClass": "BestEffort", + "startTime": "2019-08-09T05:12:19Z" + } +} \ No newline at end of file diff --git a/internal/render/assets/po_init.json b/internal/render/assets/po_init.json new file mode 100644 index 00000000..2b0ad6ad --- /dev/null +++ b/internal/render/assets/po_init.json @@ -0,0 +1,191 @@ +{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"name\":\"nginx\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"image\":\"nginx:alpine\",\"name\":\"nginx\",\"ports\":[{\"containerPort\":80}],\"volumeMounts\":[{\"mountPath\":\"/usr/share/nginx/html\",\"name\":\"index\"}]}],\"terminationGracePeriodSeconds\":0,\"volumes\":[{\"name\":\"index\",\"persistentVolumeClaim\":{\"claimName\":\"web\"}}]}}\n" + }, + "creationTimestamp": "2019-08-09T05:12:19Z", + "name": "nginx", + "namespace": "default", + "resourceVersion": "1482816", + "selfLink": "/api/v1/namespaces/default/pods/nginx", + "uid": "614908ed-415b-4506-8370-e3e36fa8cc13" + }, + "spec": { + "initContainers": [ + { + "image": "nginx:alpine", + "imagePullPolicy": "IfNotPresent", + "name": "ic1", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": { + "limits": { + "memory": "170Mi" + }, + "requests": { + "cpu": "100m", + "memory": "70Mi" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/usr/share/nginx/html", + "name": "index" + }, + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-9ph8s", + "readOnly": true + } + ] + } + ], + "containers": [ + { + "image": "nginx:alpine", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": { + "limits": { + "memory": "170Mi" + }, + "requests": { + "cpu": "100m", + "memory": "70Mi" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/usr/share/nginx/html", + "name": "index" + }, + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-9ph8s", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "minikube", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 0, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + } + ], + "volumes": [ + { + "name": "index", + "persistentVolumeClaim": { + "claimName": "web" + } + }, + { + "name": "default-token-9ph8s", + "secret": { + "defaultMode": 420, + "secretName": "default-token-9ph8s" + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2019-08-09T05:12:19Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2019-08-09T05:12:21Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2019-08-09T05:12:21Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2019-08-09T05:12:19Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://421bd26d6c682f14b5ea1dcaf06e14a509b2b702fc7793e820520eb1e28e2eaf", + "image": "nginx:alpine", + "imageID": "docker-pullable://nginx@sha256:482ead44b2203fa32b3390abdaf97cbdc8ad15c07fb03a3e68d7c35a19ad7595", + "lastState": {}, + "name": "nginx", + "ready": true, + "restartCount": 0, + "state": { + "running": { + "startedAt": "2019-08-09T05:12:20Z" + } + } + } + ], + "initContainerStatuses": [ + { + "containerID": "docker://421bd26d6c682f14b5ea1dcaf06e14a509b2b702fc7793e820520eb1e28e2eaf", + "image": "nginx:alpine", + "imageID": "docker-pullable://nginx@sha256:482ead44b2203fa32b3390abdaf97cbdc8ad15c07fb03a3e68d7c35a19ad7595", + "lastState": {}, + "name": "ic1", + "ready": true, + "restartCount": 0, + "state": { + "running": { + "startedAt": "2019-08-09T05:12:20Z" + } + } + } + ], + "hostIP": "192.168.64.104", + "phase": "Running", + "podIP": "172.17.0.6", + "qosClass": "BestEffort", + "startTime": "2019-08-09T05:12:19Z" + } +} \ No newline at end of file diff --git a/internal/render/assets/pv.json b/internal/render/assets/pv.json new file mode 100644 index 00000000..83768583 --- /dev/null +++ b/internal/render/assets/pv.json @@ -0,0 +1,72 @@ +{ + "apiVersion": "v1", + "kind": "PersistentVolume", + "metadata": { + "annotations": { + "kubernetes.io/createdby": "gce-pd-dynamic-provisioner", + "pv.kubernetes.io/bound-by-controller": "yes", + "pv.kubernetes.io/provisioned-by": "kubernetes.io/gce-pd" + }, + "creationTimestamp": "2019-06-05T00:08:24Z", + "finalizers": [ + "kubernetes.io/pv-protection" + ], + "labels": { + "failure-domain.beta.kubernetes.io/region": "us-central1", + "failure-domain.beta.kubernetes.io/zone": "us-central1-a" + }, + "name": "pvc-07aa4e2c-8726-11e9-a8e8-42010a80015b", + "resourceVersion": "26769902", + "selfLink": "/api/v1/persistentvolumes/pvc-07aa4e2c-8726-11e9-a8e8-42010a80015b", + "uid": "093234ed-8726-11e9-a8e8-42010a80015b" + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "capacity": { + "storage": "1Gi" + }, + "claimRef": { + "apiVersion": "v1", + "kind": "PersistentVolumeClaim", + "name": "www-nginx-sts-1", + "namespace": "default", + "resourceVersion": "26769889", + "uid": "07aa4e2c-8726-11e9-a8e8-42010a80015b" + }, + "gcePersistentDisk": { + "fsType": "ext4", + "pdName": "gke-k9s-fd5bf60e-dynam-pvc-07aa4e2c-8726-11e9-a8e8-42010a80015b" + }, + "nodeAffinity": { + "required": { + "nodeSelectorTerms": [ + { + "matchExpressions": [ + { + "key": "failure-domain.beta.kubernetes.io/zone", + "operator": "In", + "values": [ + "us-central1-a" + ] + }, + { + "key": "failure-domain.beta.kubernetes.io/region", + "operator": "In", + "values": [ + "us-central1" + ] + } + ] + } + ] + } + }, + "persistentVolumeReclaimPolicy": "Delete", + "storageClassName": "standard" + }, + "status": { + "phase": "Bound" + } +} \ No newline at end of file diff --git a/internal/render/assets/pvc.json b/internal/render/assets/pvc.json new file mode 100644 index 00000000..edb54cf7 --- /dev/null +++ b/internal/render/assets/pvc.json @@ -0,0 +1,45 @@ +{ + "apiVersion": "v1", + "kind": "PersistentVolumeClaim", + "metadata": { + "annotations": { + "pv.kubernetes.io/bind-completed": "yes", + "pv.kubernetes.io/bound-by-controller": "yes", + "volume.beta.kubernetes.io/storage-provisioner": "kubernetes.io/gce-pd" + }, + "creationTimestamp": "2019-06-05T00:08:01Z", + "finalizers": [ + "kubernetes.io/pvc-protection" + ], + "labels": { + "app": "nginx-sts" + }, + "name": "www-nginx-sts-0", + "namespace": "default", + "resourceVersion": "26769829", + "selfLink": "/api/v1/namespaces/default/persistentvolumeclaims/www-nginx-sts-0", + "uid": "fbabd470-8725-11e9-a8e8-42010a80015b" + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "dataSource": null, + "resources": { + "requests": { + "storage": "1Mi" + } + }, + "storageClassName": "standard", + "volumeName": "pvc-fbabd470-8725-11e9-a8e8-42010a80015b" + }, + "status": { + "accessModes": [ + "ReadWriteOnce" + ], + "capacity": { + "storage": "1Gi" + }, + "phase": "Bound" + } +} \ No newline at end of file diff --git a/internal/render/assets/rb.json b/internal/render/assets/rb.json new file mode 100644 index 00000000..29b5a4ce --- /dev/null +++ b/internal/render/assets/rb.json @@ -0,0 +1,27 @@ +{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"rbac.authorization.k8s.io/v1\",\"kind\":\"RoleBinding\",\"metadata\":{\"annotations\":{},\"name\":\"blee\",\"namespace\":\"default\"},\"roleRef\":{\"apiGroup\":\"rbac.authorization.k8s.io\",\"kind\":\"Role\",\"name\":\"blee\"},\"subjects\":[{\"kind\":\"ServiceAccount\",\"name\":\"fernand\",\"namespace\":\"default\"}]}\n" + }, + "creationTimestamp": "2019-03-27T22:26:49Z", + "name": "blee", + "namespace": "default", + "resourceVersion": "11177042", + "selfLink": "/apis/rbac.authorization.k8s.io/v1/namespaces/default/rolebindings/blee", + "uid": "69ed0b23-50df-11e9-83c8-42010a800018" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "Role", + "name": "blee" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "fernand", + "namespace": "default" + } + ] +} \ No newline at end of file diff --git a/internal/render/assets/ro.json b/internal/render/assets/ro.json new file mode 100644 index 00000000..e42781a0 --- /dev/null +++ b/internal/render/assets/ro.json @@ -0,0 +1,33 @@ +{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"rbac.authorization.k8s.io/v1\",\"kind\":\"Role\",\"metadata\":{\"annotations\":{},\"name\":\"blee\",\"namespace\":\"default\"},\"rules\":[{\"apiGroups\":[\"\"],\"resources\":[\"pods\",\"namespaces\"],\"verbs\":[\"get\",\"list\",\"deletecollection\",\"patch\",\"watch\"]}]}\n" + }, + "creationTimestamp": "2019-05-24T02:58:58Z", + "name": "blee", + "namespace": "default", + "resourceVersion": "23720646", + "selfLink": "/apis/rbac.authorization.k8s.io/v1/namespaces/default/roles/blee", + "uid": "e017e058-7dcf-11e9-b9e0-42010a800003" + }, + "rules": [ + { + "apiGroups": [ + "" + ], + "resources": [ + "pods", + "namespaces" + ], + "verbs": [ + "get", + "list", + "deletecollection", + "patch", + "watch" + ] + } + ] +} \ No newline at end of file diff --git a/internal/render/assets/rs.json b/internal/render/assets/rs.json new file mode 100644 index 00000000..4819a187 --- /dev/null +++ b/internal/render/assets/rs.json @@ -0,0 +1,110 @@ +{ + "apiVersion": "extensions/v1beta1", + "kind": "ReplicaSet", + "metadata": { + "annotations": { + "deployment.kubernetes.io/desired-replicas": "1", + "deployment.kubernetes.io/max-replicas": "2", + "deployment.kubernetes.io/revision": "1" + }, + "creationTimestamp": "2019-07-14T04:54:17Z", + "generation": 1, + "labels": { + "app": "icx-db", + "pod-template-hash": "7d4b578979" + }, + "name": "icx-db-7d4b578979", + "namespace": "icx", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "Deployment", + "name": "icx-db", + "uid": "6f6143bc-a5f3-11e9-990f-42010a800218" + } + ], + "resourceVersion": "37116270", + "selfLink": "/apis/extensions/v1beta1/namespaces/icx/replicasets/icx-db-7d4b578979", + "uid": "6f637a60-a5f3-11e9-990f-42010a800218" + }, + "spec": { + "replicas": 1, + "selector": { + "matchLabels": { + "app": "icx-db", + "pod-template-hash": "7d4b578979" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "icx-db", + "pod-template-hash": "7d4b578979" + } + }, + "spec": { + "containers": [ + { + "env": [ + { + "name": "POSTGRES_USER", + "valueFrom": { + "secretKeyRef": { + "key": "pg_user", + "name": "icx-creds" + } + } + }, + { + "name": "POSTGRES_PASSWORD", + "valueFrom": { + "secretKeyRef": { + "key": "pg_pwd", + "name": "icx-creds" + } + } + } + ], + "image": "postgres:9.2-alpine", + "imagePullPolicy": "IfNotPresent", + "name": "icx-db", + "ports": [ + { + "containerPort": 5432, + "name": "client", + "protocol": "TCP" + } + ], + "resources": { + "limits": { + "cpu": "250m", + "memory": "512Mi" + }, + "requests": { + "cpu": "250m", + "memory": "256Mi" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "terminationGracePeriodSeconds": 30 + } + } + }, + "status": { + "availableReplicas": 1, + "fullyLabeledReplicas": 1, + "observedGeneration": 1, + "readyReplicas": 1, + "replicas": 1 + } +} \ No newline at end of file diff --git a/internal/render/assets/sa.json b/internal/render/assets/sa.json new file mode 100644 index 00000000..640be88f --- /dev/null +++ b/internal/render/assets/sa.json @@ -0,0 +1,23 @@ +{ + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"ServiceAccount\",\"metadata\":{\"annotations\":{},\"name\":\"blee\",\"namespace\":\"default\"},\"secrets\":[{\"name\":\"blee\",\"namespace\":\"default\"}]}\n" + }, + "creationTimestamp": "2019-06-05T21:56:55Z", + "name": "blee", + "namespace": "default", + "resourceVersion": "27009820", + "selfLink": "/api/v1/namespaces/default/serviceaccounts/blee", + "uid": "d5919410-87dc-11e9-a8e8-42010a80015b" + }, + "secrets": [ + { + "name": "blee" + }, + { + "name": "blee-token-k42bt" + } + ] +} \ No newline at end of file diff --git a/internal/render/assets/sc.json b/internal/render/assets/sc.json new file mode 100644 index 00000000..afd1d892 --- /dev/null +++ b/internal/render/assets/sc.json @@ -0,0 +1,24 @@ +{ + "apiVersion": "storage.k8s.io/v1", + "kind": "StorageClass", + "metadata": { + "annotations": { + "storageclass.beta.kubernetes.io/is-default-class": "true" + }, + "creationTimestamp": "2019-02-05T22:04:14Z", + "labels": { + "addonmanager.kubernetes.io/mode": "EnsureExists", + "kubernetes.io/cluster-service": "true" + }, + "name": "standard", + "resourceVersion": "277", + "selfLink": "/apis/storage.k8s.io/v1/storageclasses/standard", + "uid": "f9d4c94a-2991-11e9-81cd-42010a80005b" + }, + "parameters": { + "type": "pd-standard" + }, + "provisioner": "kubernetes.io/gce-pd", + "reclaimPolicy": "Delete", + "volumeBindingMode": "Immediate" +} \ No newline at end of file diff --git a/internal/render/assets/sec.json b/internal/render/assets/sec.json new file mode 100644 index 00000000..a126ecef --- /dev/null +++ b/internal/render/assets/sec.json @@ -0,0 +1,23 @@ +{ + "apiVersion": "v1", + "data": { + "password": "YnVtYmxlYmVldHVuYQo=", + "token": "ZmVybmFuZAo=" + }, + "kind": "Secret", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"data\":{\"password\":\"YnVtYmxlYmVldHVuYQo=\",\"token\":\"ZmVybmFuZAo=\"},\"kind\":\"Secret\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"fred\"},\"name\":\"s1\",\"namespace\":\"default\"},\"type\":\"Opaque\"}\n" + }, + "creationTimestamp": "2019-08-30T14:30:50Z", + "labels": { + "app": "fred" + }, + "name": "s1", + "namespace": "default", + "resourceVersion": "49724026", + "selfLink": "/api/v1/namespaces/default/secrets/s1", + "uid": "c3e3d3f3-cb32-11e9-990f-42010a800218" + }, + "type": "Opaque" +} \ No newline at end of file diff --git a/internal/render/assets/sts.json b/internal/render/assets/sts.json new file mode 100644 index 00000000..35516896 --- /dev/null +++ b/internal/render/assets/sts.json @@ -0,0 +1,110 @@ +{ + "apiVersion": "apps/v1", + "kind": "StatefulSet", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1\",\"kind\":\"StatefulSet\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"nginx-sts\"},\"name\":\"nginx-sts\",\"namespace\":\"default\"},\"spec\":{\"replicas\":2,\"selector\":{\"matchLabels\":{\"app\":\"nginx-sts\"}},\"serviceName\":\"nginx-sts\",\"template\":{\"metadata\":{\"labels\":{\"app\":\"nginx-sts\"}},\"spec\":{\"containers\":[{\"image\":\"k8s.gcr.io/nginx-slim:0.8\",\"name\":\"nginx\",\"ports\":[{\"containerPort\":80,\"name\":\"web\"}],\"volumeMounts\":[{\"mountPath\":\"/usr/share/nginx/html\",\"name\":\"www\"}]}]}},\"volumeClaimTemplates\":[{\"metadata\":{\"name\":\"www\"},\"spec\":{\"accessModes\":[\"ReadWriteOnce\"],\"resources\":{\"requests\":{\"storage\":\"1Mi\"}}}}]}}\n" + }, + "creationTimestamp": "2019-11-30T15:41:42Z", + "generation": 5, + "labels": { + "app": "nginx-sts" + }, + "name": "nginx-sts", + "namespace": "default", + "resourceVersion": "82973198", + "selfLink": "/apis/apps/v1/namespaces/default/statefulsets/nginx-sts", + "uid": "e87310a8-1387-11ea-aa02-42010a800053" + }, + "spec": { + "podManagementPolicy": "OrderedReady", + "replicas": 4, + "revisionHistoryLimit": 10, + "selector": { + "matchLabels": { + "app": "nginx-sts" + } + }, + "serviceName": "nginx-sts", + "template": { + "metadata": { + "annotations": { + "kubectl.kubernetes.io/restartedAt": "2019-12-01T13:50:44-07:00" + }, + "creationTimestamp": null, + "labels": { + "app": "nginx-sts" + } + }, + "spec": { + "containers": [ + { + "image": "k8s.gcr.io/nginx-slim:0.8", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "name": "web", + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/usr/share/nginx/html", + "name": "www" + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "terminationGracePeriodSeconds": 30 + } + }, + "updateStrategy": { + "rollingUpdate": { + "partition": 0 + }, + "type": "RollingUpdate" + }, + "volumeClaimTemplates": [ + { + "metadata": { + "creationTimestamp": null, + "name": "www" + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "dataSource": null, + "resources": { + "requests": { + "storage": "1Mi" + } + }, + "volumeMode": "Filesystem" + }, + "status": { + "phase": "Pending" + } + } + ] + }, + "status": { + "collisionCount": 0, + "currentReplicas": 4, + "currentRevision": "nginx-sts-5b89ffb894", + "observedGeneration": 5, + "readyReplicas": 4, + "replicas": 4, + "updateRevision": "nginx-sts-5b89ffb894", + "updatedReplicas": 4 + } +} \ No newline at end of file diff --git a/internal/render/assets/svc.json b/internal/render/assets/svc.json new file mode 100644 index 00000000..5825b224 --- /dev/null +++ b/internal/render/assets/svc.json @@ -0,0 +1,34 @@ +{ + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"name\":\"dictionary1\",\"namespace\":\"default\"},\"spec\":{\"ports\":[{\"name\":\"http\",\"port\":4001,\"targetPort\":\"http\"}],\"selector\":{\"app\":\"dictionary1\"},\"type\":\"ClusterIP\"}}\n" + }, + "creationTimestamp": "2019-07-10T23:10:43Z", + "name": "dictionary1", + "namespace": "default", + "resourceVersion": "36257616", + "selfLink": "/api/v1/namespaces/default/services/dictionary1", + "uid": "f1007a5c-a367-11e9-990f-42010a800218" + }, + "spec": { + "clusterIP": "10.47.248.116", + "ports": [ + { + "name": "http", + "port": 4001, + "protocol": "TCP", + "targetPort": "http" + } + ], + "selector": { + "app": "dictionary1" + }, + "sessionAffinity": "None", + "type": "ClusterIP" + }, + "status": { + "loadBalancer": {} + } +} \ No newline at end of file diff --git a/internal/render/benchmark.go b/internal/render/benchmark.go new file mode 100644 index 00000000..68da849a --- /dev/null +++ b/internal/render/benchmark.go @@ -0,0 +1,170 @@ +package render + +import ( + "fmt" + "io/ioutil" + "os" + "regexp" + "strconv" + "strings" + + "github.com/derailed/tview" + "github.com/gdamore/tcell" + "golang.org/x/text/language" + "golang.org/x/text/message" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + totalRx = regexp.MustCompile(`Total:\s+([0-9.]+)\ssecs`) + reqRx = regexp.MustCompile(`Requests/sec:\s+([0-9.]+)`) + okRx = regexp.MustCompile(`\[2\d{2}\]\s+(\d+)\s+responses`) + errRx = regexp.MustCompile(`\[[4-5]\d{2}\]\s+(\d+)\s+responses`) + toastRx = regexp.MustCompile(`Error distribution`) +) + +// Benchmark renders a benchmarks to screen. +type Benchmark struct{} + +// ColorerFunc colors a resource row. +func (Benchmark) ColorerFunc() ColorerFunc { + return func(ns string, re RowEvent) tcell.Color { + c := tcell.ColorPaleGreen + statusCol := 2 + if strings.TrimSpace(re.Row.Fields[statusCol]) != "pass" { + c = ErrColor + } + return c + } +} + +// Header returns a header row. +func (Benchmark) Header(ns string) HeaderRow { + return HeaderRow{ + Header{Name: "NAMESPACE"}, + Header{Name: "NAME"}, + Header{Name: "STATUS"}, + Header{Name: "TIME"}, + Header{Name: "REQ/S", Align: tview.AlignRight}, + Header{Name: "2XX", Align: tview.AlignRight}, + Header{Name: "4XX/5XX", Align: tview.AlignRight}, + Header{Name: "REPORT"}, + Header{Name: "AGE", Decorator: AgeDecorator}, + } +} + +// Render renders a K8s resource to screen. +func (b Benchmark) Render(o interface{}, ns string, r *Row) error { + bench, ok := o.(BenchInfo) + if !ok { + return fmt.Errorf("expecting benchinfo but got `%T", o) + } + + data, err := b.readFile(bench.Path) + if err != nil { + return fmt.Errorf("Unable to load bench file %s", bench.Path) + } + + r.ID = bench.Path + r.Fields = make(Fields, len(b.Header(ns))) + if err := b.initRow(r.Fields, bench.File); err != nil { + return err + } + b.augmentRow(r.Fields, data) + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func (Benchmark) readFile(file string) (string, error) { + data, err := ioutil.ReadFile(file) + if err != nil { + return "", err + } + return string(data), nil +} + +func (Benchmark) initRow(row Fields, f os.FileInfo) error { + tokens := strings.Split(f.Name(), "_") + if len(tokens) < 2 { + return fmt.Errorf("Invalid file name %s", f.Name()) + } + row[0] = tokens[0] + row[1] = tokens[1] + row[7] = f.Name() + row[8] = timeToAge(f.ModTime()) + + return nil +} + +func (b Benchmark) augmentRow(fields Fields, data string) { + if len(data) == 0 { + return + } + + col := 2 + fields[col] = "pass" + mf := toastRx.FindAllStringSubmatch(data, 1) + if len(mf) > 0 { + fields[col] = "fail" + } + col++ + + mt := totalRx.FindAllStringSubmatch(data, 1) + if len(mt) > 0 { + fields[col] = mt[0][1] + } + col++ + + mr := reqRx.FindAllStringSubmatch(data, 1) + if len(mr) > 0 { + fields[col] = mr[0][1] + } + col++ + + ms := okRx.FindAllStringSubmatch(data, -1) + fields[col] = b.countReq(ms) + col++ + + me := errRx.FindAllStringSubmatch(data, -1) + fields[col] = b.countReq(me) +} + +func (Benchmark) countReq(rr [][]string) string { + if len(rr) == 0 { + return "0" + } + + var sum int + for _, m := range rr { + if m, err := strconv.Atoi(string(m[1])); err == nil { + sum += m + } + } + return asNum(sum) +} + +// AsNumb prints a number with thousand separator. +func asNum(n int) string { + p := message.NewPrinter(language.English) + return p.Sprintf("%d", n) +} + +// BenchInfo represents benchmark run info. +type BenchInfo struct { + File os.FileInfo + Path string +} + +// GetObjectKind returns a schema object. +func (BenchInfo) GetObjectKind() schema.ObjectKind { + return nil +} + +// DeepCopyObject returns a container copy. +func (b BenchInfo) DeepCopyObject() runtime.Object { + return b +} diff --git a/internal/render/benchmark_int_test.go b/internal/render/benchmark_int_test.go new file mode 100644 index 00000000..7489b914 --- /dev/null +++ b/internal/render/benchmark_int_test.go @@ -0,0 +1,50 @@ +package render + +import ( + "io/ioutil" + "testing" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" +) + +func init() { + zerolog.SetGlobalLevel(zerolog.FatalLevel) +} + +func TestAugmentRow(t *testing.T) { + uu := map[string]struct { + file string + e Fields + }{ + "cool": { + "assets/b1.txt", + Fields{"pass", "3.3544", "29.8116", "100", "0"}, + }, + "2XX": { + "assets/b4.txt", + Fields{"pass", "3.3544", "29.8116", "160", "0"}, + }, + "4XX/5XX": { + "assets/b2.txt", + Fields{"pass", "3.3544", "29.8116", "100", "12"}, + }, + "toast": { + "assets/b3.txt", + Fields{"fail", "2.3688", "35.4606", "0", "0"}, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + data, err := ioutil.ReadFile(u.file) + + assert.Nil(t, err) + fields := make(Fields, 8) + b := Benchmark{} + b.augmentRow(fields, string(data)) + assert.Equal(t, u.e, fields[2:7]) + }) + } +} diff --git a/internal/render/cm.go b/internal/render/cm.go new file mode 100644 index 00000000..602496f3 --- /dev/null +++ b/internal/render/cm.go @@ -0,0 +1,59 @@ +package render + +import ( + "fmt" + "strconv" + + "github.com/derailed/tview" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// ConfigMap renders a K8s ConfigMap to screen. +type ConfigMap struct{} + +// ColorerFunc colors a resource row. +func (ConfigMap) ColorerFunc() ColorerFunc { + return DefaultColorer +} + +// Header returns a header row. +func (ConfigMap) Header(ns string) HeaderRow { + var h HeaderRow + if isAllNamespace(ns) { + h = append(h, Header{Name: "NAMESPACE"}) + } + + return append(h, + Header{Name: "NAME"}, + Header{Name: "DATA", Align: tview.AlignRight}, + Header{Name: "AGE", Decorator: AgeDecorator}, + ) +} + +// Render renders a K8s resource to screen. +func (c ConfigMap) Render(o interface{}, ns string, r *Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("Expected ConfigMap, but got %T", o) + } + var cm v1.ConfigMap + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &cm) + if err != nil { + return err + } + + r.ID = MetaFQN(cm.ObjectMeta) + r.Fields = make(Fields, 0, len(c.Header(ns))) + if isAllNamespace(ns) { + r.Fields = append(r.Fields, cm.Namespace) + } + r.Fields = append(r.Fields, + cm.Name, + strconv.Itoa(len(cm.Data)), + toAge(cm.ObjectMeta.CreationTimestamp), + ) + + return nil +} diff --git a/internal/render/cm_test.go b/internal/render/cm_test.go new file mode 100644 index 00000000..62b615a3 --- /dev/null +++ b/internal/render/cm_test.go @@ -0,0 +1,34 @@ +package render_test + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestCMRender(t *testing.T) { + c := render.ConfigMap{} + r := render.NewRow(4) + c.Render(load(t, "cm"), "", &r) + + assert.Equal(t, "default/blee", r.ID) + assert.Equal(t, render.Fields{"default", "blee", "2"}, r.Fields[:3]) +} + +// Helpers... + +func load(t *testing.T, n string) *unstructured.Unstructured { + raw, err := ioutil.ReadFile(fmt.Sprintf("assets/%s.json", n)) + assert.Nil(t, err) + + var o unstructured.Unstructured + err = json.Unmarshal(raw, &o) + assert.Nil(t, err) + + return &o +} diff --git a/internal/ui/colorer.go b/internal/render/color.go similarity index 55% rename from internal/ui/colorer.go rename to internal/render/color.go index 46472873..dba824b0 100644 --- a/internal/ui/colorer.go +++ b/internal/render/color.go @@ -1,10 +1,6 @@ -package ui +package render -import ( - "github.com/derailed/k9s/internal/resource" - "github.com/gdamore/tcell" - "k8s.io/apimachinery/pkg/watch" -) +import "github.com/gdamore/tcell" var ( // ModColor row modified color. @@ -23,14 +19,20 @@ var ( CompletedColor tcell.Color ) +// ColorerFunc represents a resource row colorer. +type ColorerFunc func(ns string, evt RowEvent) tcell.Color + // DefaultColorer set the default table row colors. -func DefaultColorer(ns string, r *resource.RowEvent) tcell.Color { - c := StdColor - switch r.Action { - case watch.Added, resource.New: - c = AddColor - case watch.Modified: - c = ModColor +func DefaultColorer(ns string, evt RowEvent) tcell.Color { + var col = StdColor + switch evt.Kind { + case EventAdd: + col = AddColor + case EventUpdate: + col = ModColor + case EventDelete: + col = KillColor } - return c + + return col } diff --git a/internal/render/container.go b/internal/render/container.go new file mode 100644 index 00000000..ab7decbb --- /dev/null +++ b/internal/render/container.go @@ -0,0 +1,204 @@ +package render + +import ( + "fmt" + "strconv" + "strings" + + "github.com/derailed/tview" + "github.com/gdamore/tcell" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +// ContainerWithMetrics represents a container and it's metrics. +type ContainerWithMetrics interface { + // Container returns the container + Container() *v1.Container + + // ContainerStatus returns the current container status. + ContainerStatus() *v1.ContainerStatus + + // Metrics returns the container metrics. + Metrics() *mv1beta1.ContainerMetrics + + // Age returns the pod age. + Age() metav1.Time + + // IsInit indicates a init container. + IsInit() bool +} + +// Container renders a K8s Container to screen. +type Container struct{} + +// ColorerFunc colors a resource row. +func (Container) ColorerFunc() ColorerFunc { + return func(ns string, r RowEvent) tcell.Color { + c := DefaultColorer(ns, r) + + readyCol := 2 + if strings.TrimSpace(r.Row.Fields[readyCol]) == "false" { + c = ErrColor + } + + stateCol := readyCol + 1 + switch strings.TrimSpace(r.Row.Fields[stateCol]) { + case ContainerCreating, PodInitializing: + return AddColor + case Terminating, Initialized: + return HighlightColor + case Completed: + return CompletedColor + case Running: + default: + c = ErrColor + } + + return c + } +} + +// Header returns a header row. +func (Container) Header(ns string) HeaderRow { + return HeaderRow{ + Header{Name: "NAME"}, + Header{Name: "IMAGE"}, + Header{Name: "READY"}, + Header{Name: "STATE"}, + Header{Name: "INIT"}, + Header{Name: "RS", Align: tview.AlignRight}, + Header{Name: "PROBES(L:R)"}, + Header{Name: "CPU", Align: tview.AlignRight}, + Header{Name: "MEM", Align: tview.AlignRight}, + Header{Name: "%CPU", Align: tview.AlignRight}, + Header{Name: "%MEM", Align: tview.AlignRight}, + Header{Name: "PORTS"}, + Header{Name: "AGE", Decorator: AgeDecorator}, + } +} + +// Render renders a K8s resource to screen. +func (c Container) Render(o interface{}, name string, r *Row) error { + co, ok := o.(ContainerRes) + if !ok { + return fmt.Errorf("Expected ContainerRes, but got %T", o) + } + + cur, perc := gatherMetrics(co) + ready, state, restarts := "false", MissingValue, "0" + if co.Status != nil { + ready, state, restarts = boolToStr(co.Status.Ready), toState(co.Status.State), strconv.Itoa(int(co.Status.RestartCount)) + } + + r.ID = co.Container.Name + r.Fields = make(Fields, 0, len(c.Header(AllNamespaces))) + r.Fields = append(r.Fields, + co.Container.Name, + co.Container.Image, + ready, + state, + boolToStr(co.IsInit), + restarts, + probe(co.Container.LivenessProbe)+":"+probe(co.Container.ReadinessProbe), + cur.cpu, + cur.mem, + perc.cpu, + perc.mem, + toStrPorts(co.Container.Ports), + toAge(co.Age), + ) + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func gatherMetrics(co ContainerRes) (c, p metric) { + c, p = noMetric(), noMetric() + if co.Metrics == nil { + return + } + + cpu := co.Metrics.Usage.Cpu().MilliValue() + mem := ToMB(co.Metrics.Usage.Memory().Value()) + c = metric{ + cpu: ToMillicore(cpu), + mem: ToMi(mem), + } + + rcpu, rmem := containerResources(co.Container) + if rcpu != nil { + p.cpu = AsPerc(toPerc(float64(cpu), float64(rcpu.MilliValue()))) + } + if rmem != nil { + p.mem = AsPerc(toPerc(mem, ToMB(rmem.Value()))) + } + + return +} + +func toStrPorts(pp []v1.ContainerPort) string { + ports := make([]string, len(pp)) + for i, p := range pp { + if len(p.Name) > 0 { + ports[i] = p.Name + ":" + } + ports[i] += strconv.Itoa(int(p.ContainerPort)) + if p.Protocol != "TCP" { + ports[i] += "╱" + string(p.Protocol) + } + } + + return strings.Join(ports, ",") +} + +func toState(s v1.ContainerState) string { + switch { + case s.Waiting != nil: + if s.Waiting.Reason != "" { + return s.Waiting.Reason + } + return "Waiting" + + case s.Terminated != nil: + if s.Terminated.Reason != "" { + return s.Terminated.Reason + } + return "Terminated" + case s.Running != nil: + return "Running" + default: + return MissingValue + } +} + +func probe(p *v1.Probe) string { + if p == nil { + return "off" + } + return "on" +} + +// ContainerRes represents a container and its metrics. +type ContainerRes struct { + Container v1.Container + Status *v1.ContainerStatus + Metrics *mv1beta1.ContainerMetrics + IsInit bool + Age metav1.Time +} + +// GetObjectKind returns a schema object. +func (c ContainerRes) GetObjectKind() schema.ObjectKind { + return nil +} + +// DeepCopyObject returns a container copy. +func (c ContainerRes) DeepCopyObject() runtime.Object { + return c +} diff --git a/internal/render/container_test.go b/internal/render/container_test.go new file mode 100644 index 00000000..bf01314a --- /dev/null +++ b/internal/render/container_test.go @@ -0,0 +1,106 @@ +package render_test + +import ( + "fmt" + "testing" + "time" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +func TestContainer(t *testing.T) { + var c render.Container + + cres := render.ContainerRes{ + Container: makeContainer(), + Status: makeContainerStatus(), + Metrics: makeContainerMetrics(), + IsInit: false, + Age: makeAge(), + } + var r render.Row + assert.Nil(t, c.Render(cres, "blee", &r)) + assert.Equal(t, "fred", r.ID) + assert.Equal(t, render.Fields{ + "fred", + "img", + "false", + "Running", + "false", + "0", + "off:off", + "10", + "20", + "50", + "20", + "", + }, + r.Fields[:len(r.Fields)-1], + ) +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func toQty(s string) resource.Quantity { + q, _ := resource.ParseQuantity(s) + return q + +} + +func makeContainerMetrics() *mv1beta1.ContainerMetrics { + return &mv1beta1.ContainerMetrics{ + Name: "fred", + Usage: v1.ResourceList{ + v1.ResourceCPU: toQty("10m"), + v1.ResourceMemory: toQty("20Mi"), + }, + } +} + +func makeAge() metav1.Time { + return metav1.Time{Time: testTime()} +} + +func makeContainer() v1.Container { + return v1.Container{ + Name: "fred", + Image: "img", + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + v1.ResourceCPU: toQty("20m"), + v1.ResourceMemory: toQty("100Mi"), + }, + }, + Env: []v1.EnvVar{ + { + Name: "fred", + Value: "1", + ValueFrom: &v1.EnvVarSource{ + ConfigMapKeyRef: &v1.ConfigMapKeySelector{Key: "blee"}, + }, + }, + }, + } +} + +func makeContainerStatus() *v1.ContainerStatus { + return &v1.ContainerStatus{ + Name: "fred", + State: v1.ContainerState{Running: &v1.ContainerStateRunning{}}, + RestartCount: 0, + } +} + +func testTime() time.Time { + t, err := time.Parse(time.RFC3339, "2018-12-14T10:36:43.326972-07:00") + if err != nil { + fmt.Println("TestTime Failed", err) + } + return t +} diff --git a/internal/render/context.go b/internal/render/context.go new file mode 100644 index 00000000..8847447c --- /dev/null +++ b/internal/render/context.go @@ -0,0 +1,101 @@ +package render + +import ( + "fmt" + "strings" + + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/tools/clientcmd/api" +) + +// Context renders a K8s ConfigMap to screen. +type Context struct{} + +// ColorerFunc colors a resource row. +func (Context) ColorerFunc() ColorerFunc { + return func(ns string, r RowEvent) tcell.Color { + c := DefaultColorer(ns, r) + if r.Kind == EventAdd || r.Kind == EventUpdate { + return c + } + if strings.Contains(strings.TrimSpace(r.Row.Fields[0]), "*") { + c = HighlightColor + } + + return c + } +} + +// Header returns a header row. +func (Context) Header(ns string) HeaderRow { + return HeaderRow{ + Header{Name: "NAME"}, + Header{Name: "CLUSTER"}, + Header{Name: "AUTHINFO"}, + Header{Name: "NAMESPACE"}, + } +} + +// Render renders a K8s resource to screen. +func (c Context) Render(o interface{}, _ string, r *Row) error { + ctx, ok := o.(*NamedContext) + if !ok { + return fmt.Errorf("expected *NamedContext, but got %T", o) + } + + name := ctx.Name + if ctx.IsCurrentContext(ctx.Name) { + name += "(*)" + } + + r.ID = ctx.Name + r.Fields = Fields{ + name, + ctx.Context.Cluster, + ctx.Context.AuthInfo, + ctx.Context.Namespace, + } + + return nil +} + +// Helpers... + +// NamedContext represents a named cluster context. +type NamedContext struct { + Name string + Context *api.Context + Config ContextNamer +} + +type ContextNamer interface { + CurrentContextName() (string, error) +} + +// NewNamedContext returns a new named context. +func NewNamedContext(c ContextNamer, n string, ctx *api.Context) *NamedContext { + return &NamedContext{Name: n, Context: ctx, Config: c} +} + +// MustCurrentContextName return the active context name. +func (c *NamedContext) IsCurrentContext(n string) bool { + cl, err := c.Config.CurrentContextName() + if err != nil { + log.Fatal().Err(err).Msg("Fetching current context") + return false + } + return cl == n +} + +// GetObjectKind returns a schema object. +func (c *NamedContext) GetObjectKind() schema.ObjectKind { + return nil +} + +// DeepCopyObject returns a container copy. +func (c *NamedContext) DeepCopyObject() runtime.Object { + return c +} diff --git a/internal/render/context_test.go b/internal/render/context_test.go new file mode 100644 index 00000000..b1ebbd96 --- /dev/null +++ b/internal/render/context_test.go @@ -0,0 +1,60 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" + "k8s.io/client-go/tools/clientcmd/api" +) + +func TestContextHeader(t *testing.T) { + var c render.Context + + assert.Equal(t, 4, len(c.Header(""))) +} + +func TestContextRender(t *testing.T) { + uu := map[string]struct { + ctx *render.NamedContext + e render.Row + }{ + "active": { + ctx: &render.NamedContext{ + Name: "c1", + Context: &api.Context{ + LocationOfOrigin: "fred", + Cluster: "c1", + AuthInfo: "u1", + Namespace: "ns1", + }, + Config: &config{}, + }, + e: render.Row{ + ID: "c1", + Fields: render.Fields{"c1", "c1", "u1", "ns1"}, + }, + }, + } + + var r render.Context + for k := range uu { + uc := uu[k] + t.Run(k, func(t *testing.T) { + row := render.NewRow(4) + err := r.Render(uc.ctx, "", &row) + + assert.Nil(t, err) + assert.Equal(t, uc.e, row) + }) + } +} + +// ---------------------------------------------------------------------------- +// Helpers... + +type config struct{} + +func (k config) CurrentContextName() (string, error) { + return "fred", nil +} diff --git a/internal/render/cr.go b/internal/render/cr.go new file mode 100644 index 00000000..29746eec --- /dev/null +++ b/internal/render/cr.go @@ -0,0 +1,46 @@ +package render + +import ( + "fmt" + + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// ClusterRole renders a K8s ClusterRole to screen. +type ClusterRole struct{} + +// ColorerFunc colors a resource row. +func (ClusterRole) ColorerFunc() ColorerFunc { + return DefaultColorer +} + +// Header returns a header rbw. +func (ClusterRole) Header(string) HeaderRow { + return HeaderRow{ + Header{Name: "NAME"}, + Header{Name: "AGE", Decorator: AgeDecorator}, + } +} + +// Render renders a K8s resource to screen. +func (ClusterRole) Render(o interface{}, ns string, r *Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("expecting clusterrole, but got %T", o) + } + var cr rbacv1.ClusterRole + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &cr) + if err != nil { + return err + } + + r.ID = FQN("-", cr.ObjectMeta.Name) + r.Fields = Fields{ + cr.Name, + toAge(cr.ObjectMeta.CreationTimestamp), + } + + return nil +} diff --git a/internal/render/cr_test.go b/internal/render/cr_test.go new file mode 100644 index 00000000..74ffa346 --- /dev/null +++ b/internal/render/cr_test.go @@ -0,0 +1,17 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestClusterRoleRender(t *testing.T) { + c := render.ClusterRole{} + r := render.NewRow(2) + c.Render(load(t, "cr"), "-", &r) + + assert.Equal(t, "-/blee", r.ID) + assert.Equal(t, render.Fields{"blee"}, r.Fields[:1]) +} diff --git a/internal/render/crb.go b/internal/render/crb.go new file mode 100644 index 00000000..2004ea49 --- /dev/null +++ b/internal/render/crb.go @@ -0,0 +1,54 @@ +package render + +import ( + "fmt" + + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// ClusterRoleBinding renders a K8s ClusterRoleBinding to screen. +type ClusterRoleBinding struct{} + +// ColorerFunc colors a resource row. +func (ClusterRoleBinding) ColorerFunc() ColorerFunc { + return DefaultColorer +} + +// Header returns a header rbw. +func (ClusterRoleBinding) Header(string) HeaderRow { + return HeaderRow{ + Header{Name: "NAME"}, + Header{Name: "CLUSTERROLE"}, + Header{Name: "KIND"}, + Header{Name: "SUBJECTS"}, + Header{Name: "AGE", Decorator: AgeDecorator}, + } +} + +// Render renders a K8s resource to screen. +func (ClusterRoleBinding) Render(o interface{}, ns string, r *Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("Expected ClusterRoleBinding, but got %T", o) + } + var crb rbacv1.ClusterRoleBinding + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &crb) + if err != nil { + return err + } + + kind, ss := renderSubjects(crb.Subjects) + + r.ID = FQN("-", crb.ObjectMeta.Name) + r.Fields = Fields{ + crb.Name, + crb.RoleRef.Name, + kind, + ss, + toAge(crb.ObjectMeta.CreationTimestamp), + } + + return nil +} diff --git a/internal/render/crb_test.go b/internal/render/crb_test.go new file mode 100644 index 00000000..08f41b35 --- /dev/null +++ b/internal/render/crb_test.go @@ -0,0 +1,17 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestClusterRoleBindingRender(t *testing.T) { + c := render.ClusterRoleBinding{} + r := render.NewRow(5) + c.Render(load(t, "crb"), "-", &r) + + assert.Equal(t, "-/blee", r.ID) + assert.Equal(t, render.Fields{"blee", "blee", "USR", "fernand"}, r.Fields[:4]) +} diff --git a/internal/render/crd.go b/internal/render/crd.go new file mode 100644 index 00000000..2218b25a --- /dev/null +++ b/internal/render/crd.go @@ -0,0 +1,67 @@ +package render + +import ( + "fmt" + "time" + + "github.com/rs/zerolog/log" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// CustomResourceDefinition renders a K8s CustomResourceDefinition to screen. +type CustomResourceDefinition struct{} + +// ColorerFunc colors a resource row. +func (CustomResourceDefinition) ColorerFunc() ColorerFunc { + return DefaultColorer +} + +// Header returns a header rbw. +func (CustomResourceDefinition) Header(string) HeaderRow { + return HeaderRow{ + Header{Name: "NAME"}, + Header{Name: "AGE", Decorator: AgeDecorator}, + } +} + +// Render renders a K8s resource to screen. +func (CustomResourceDefinition) Render(o interface{}, ns string, r *Row) error { + crd, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("Expected CustomResourceDefinition, but got %T", o) + } + + meta, ok := crd.Object["metadata"].(map[string]interface{}) + if !ok { + return fmt.Errorf("expecting an interface map but got %T", crd.Object["metadata"]) + } + t, err := time.Parse(time.RFC3339, extractMetaField(meta, "creationTimestamp")) + if err != nil { + log.Error().Err(err).Msgf("Fields timestamp %v", err) + } + + r.ID = FQN(ClusterScope, extractMetaField(meta, "name")) + r.Fields = Fields{ + extractMetaField(meta, "name"), + toAge(metav1.Time{Time: t}), + } + + return nil +} + +func extractMetaField(m map[string]interface{}, field string) string { + f, ok := m[field] + if !ok { + log.Error().Err(fmt.Errorf("failed to extract field from meta %s", field)) + return "n/a" + } + + fs, ok := f.(string) + if !ok { + log.Error().Err(fmt.Errorf("failed to extract string from field %s", field)) + return "n/a" + } + + return fs +} diff --git a/internal/render/crd_test.go b/internal/render/crd_test.go new file mode 100644 index 00000000..96dcd085 --- /dev/null +++ b/internal/render/crd_test.go @@ -0,0 +1,17 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestCustomResourceDefinitionRender(t *testing.T) { + c := render.CustomResourceDefinition{} + r := render.NewRow(2) + c.Render(load(t, "crd"), "", &r) + + assert.Equal(t, "-/adapters.config.istio.io", r.ID) + assert.Equal(t, render.Fields{"adapters.config.istio.io"}, r.Fields[:1]) +} diff --git a/internal/render/cronjob.go b/internal/render/cronjob.go new file mode 100644 index 00000000..c68127a4 --- /dev/null +++ b/internal/render/cronjob.go @@ -0,0 +1,69 @@ +package render + +import ( + "fmt" + "strconv" + + batchv1beta1 "k8s.io/api/batch/v1beta1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// CronJob renders a K8s CronJob to screen. +type CronJob struct{} + +// ColorerFunc colors a resource row. +func (CronJob) ColorerFunc() ColorerFunc { + return DefaultColorer +} + +// Header returns a header row. +func (CronJob) Header(ns string) HeaderRow { + var h HeaderRow + if isAllNamespace(ns) { + h = append(h, Header{Name: "NAMESPACE"}) + } + + return append(h, + Header{Name: "NAME"}, + Header{Name: "SCHEDULE"}, + Header{Name: "SUSPEND"}, + Header{Name: "ACTIVE"}, + Header{Name: "LAST_SCHEDULE"}, + Header{Name: "AGE", Decorator: AgeDecorator}, + ) +} + +// Render renders a K8s resource to screen. +func (c CronJob) Render(o interface{}, ns string, r *Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("Expected CronJob, but got %T", o) + } + var cj batchv1beta1.CronJob + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &cj) + if err != nil { + return err + } + + lastScheduled := "" + if cj.Status.LastScheduleTime != nil { + lastScheduled = toAgeHuman(toAge(*cj.Status.LastScheduleTime)) + } + + r.ID = MetaFQN(cj.ObjectMeta) + r.Fields = make(Fields, 0, len(c.Header(ns))) + if isAllNamespace(ns) { + r.Fields = append(r.Fields, cj.Namespace) + } + r.Fields = append(r.Fields, + cj.Name, + cj.Spec.Schedule, + boolPtrToStr(cj.Spec.Suspend), + strconv.Itoa(len(cj.Status.Active)), + lastScheduled, + toAge(cj.ObjectMeta.CreationTimestamp), + ) + + return nil +} diff --git a/internal/render/cronjob_test.go b/internal/render/cronjob_test.go new file mode 100644 index 00000000..ae149fe8 --- /dev/null +++ b/internal/render/cronjob_test.go @@ -0,0 +1,17 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestCronJobRender(t *testing.T) { + c := render.CronJob{} + r := render.NewRow(6) + 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]) +} diff --git a/internal/render/delta.go b/internal/render/delta.go new file mode 100644 index 00000000..229a6093 --- /dev/null +++ b/internal/render/delta.go @@ -0,0 +1,46 @@ +package render + +// DeltaRow represents a collection of row detlas between old and new row. +type DeltaRow []string + +// NewDeltaRow computes the delta between 2 rows. +func NewDeltaRow(o, n Row, excludeLast bool) DeltaRow { + deltas := make(DeltaRow, len(o.Fields)) + // Exclude age col + oldFields := o.Fields[:len(o.Fields)-1] + if !excludeLast { + oldFields = o.Fields[:len(o.Fields)] + } + for i, old := range oldFields { + if old != "" && old != n.Fields[i] { + deltas[i] = old + } + } + + return deltas +} + +// IsBlank asserts a row has no values in it. +func (d DeltaRow) IsBlank() bool { + if len(d) == 0 { + return true + } + + for _, v := range d { + if v != "" { + return false + } + } + + return true +} + +// Clone returns a delta copy. +func (d DeltaRow) Clone() DeltaRow { + res := make(DeltaRow, len(d)) + for i, f := range d { + res[i] = f + } + + return res +} diff --git a/internal/render/delta_test.go b/internal/render/delta_test.go new file mode 100644 index 00000000..5b492b45 --- /dev/null +++ b/internal/render/delta_test.go @@ -0,0 +1,90 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestDelta(t *testing.T) { + uu := map[string]struct { + o render.Row + n render.Row + blank bool + e render.DeltaRow + }{ + "same": { + o: render.Row{ + Fields: render.Fields{"a", "b", "c"}, + }, + n: render.Row{ + Fields: render.Fields{"a", "b", "c"}, + }, + blank: true, + e: render.DeltaRow{"", "", ""}, + }, + "diff": { + o: render.Row{ + Fields: render.Fields{"a1", "b", "c"}, + }, + n: render.Row{ + Fields: render.Fields{"a", "b", "c"}, + }, + e: render.DeltaRow{"a1", "", ""}, + }, + "diff2": { + o: render.Row{ + Fields: render.Fields{"a", "b", "c"}, + }, + n: render.Row{ + Fields: render.Fields{"a", "b1", "c"}, + }, + e: render.DeltaRow{"", "b", ""}, + }, + "diffLast": { + o: render.Row{ + Fields: render.Fields{"a", "b", "c"}, + }, + n: render.Row{ + Fields: render.Fields{"a", "b", "c1"}, + }, + e: render.DeltaRow{"", "", "c"}, + }, + } + + for k := range uu { + uc := uu[k] + t.Run(k, func(t *testing.T) { + d := render.NewDeltaRow(uc.o, uc.n, false) + assert.Equal(t, uc.e, d) + assert.Equal(t, uc.blank, d.IsBlank()) + }) + } +} + +func TestDeltaBlank(t *testing.T) { + uu := map[string]struct { + r render.DeltaRow + e bool + }{ + "empty": { + r: render.DeltaRow{}, + e: true, + }, + "blank": { + r: render.DeltaRow{"", "", ""}, + e: true, + }, + "notblank": { + r: render.DeltaRow{"", "", "z"}, + }, + } + + for k := range uu { + uc := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, uc.e, uc.r.IsBlank()) + }) + } +} diff --git a/internal/render/dp.go b/internal/render/dp.go new file mode 100644 index 00000000..6175c4f9 --- /dev/null +++ b/internal/render/dp.go @@ -0,0 +1,81 @@ +package render + +import ( + "fmt" + "strconv" + "strings" + + "github.com/derailed/tview" + "github.com/gdamore/tcell" + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// Deployment renders a K8s Deployment to screen. +type Deployment struct{} + +// ColorerFunc colors a resource row. +func (Deployment) ColorerFunc() ColorerFunc { + return func(ns string, r RowEvent) tcell.Color { + c := DefaultColorer(ns, r) + if r.Kind == EventAdd || r.Kind == EventUpdate { + return c + } + + markCol := 2 + if ns != AllNamespaces { + markCol = 1 + } + tokens := strings.Split(r.Row.Fields[markCol], "/") + if tokens[0] != tokens[1] { + return ErrColor + } + + return StdColor + } +} + +// Header returns a header row. +func (Deployment) Header(ns string) HeaderRow { + var h HeaderRow + if isAllNamespace(ns) { + h = append(h, Header{Name: "NAMESPACE"}) + } + + return append(h, + Header{Name: "NAME"}, + Header{Name: "READY"}, + Header{Name: "UP-TO-DATE", Align: tview.AlignRight}, + Header{Name: "AVAILABLE", Align: tview.AlignRight}, + Header{Name: "AGE", Decorator: AgeDecorator}, + ) +} + +// Render renders a K8s resource to screen. +func (d Deployment) Render(o interface{}, ns string, r *Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("Expected Deployment, but got %T", o) + } + var dp appsv1.Deployment + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &dp) + if err != nil { + return err + } + + r.ID = MetaFQN(dp.ObjectMeta) + r.Fields = make(Fields, 0, len(d.Header(ns))) + if isAllNamespace(ns) { + r.Fields = append(r.Fields, dp.Namespace) + } + r.Fields = append(r.Fields, + dp.Name, + strconv.Itoa(int(dp.Status.AvailableReplicas))+"/"+strconv.Itoa(int(*dp.Spec.Replicas)), + strconv.Itoa(int(dp.Status.UpdatedReplicas)), + strconv.Itoa(int(dp.Status.AvailableReplicas)), + toAge(dp.ObjectMeta.CreationTimestamp), + ) + + return nil +} diff --git a/internal/render/dp_test.go b/internal/render/dp_test.go new file mode 100644 index 00000000..71c1003d --- /dev/null +++ b/internal/render/dp_test.go @@ -0,0 +1,17 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestDeploymentRender(t *testing.T) { + c := render.Deployment{} + r := render.NewRow(7) + 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]) +} diff --git a/internal/render/ds.go b/internal/render/ds.go new file mode 100644 index 00000000..cc2e14fd --- /dev/null +++ b/internal/render/ds.go @@ -0,0 +1,84 @@ +package render + +import ( + "fmt" + "strconv" + "strings" + + "github.com/derailed/tview" + "github.com/gdamore/tcell" + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// DaemonSet renders a K8s DaemonSet to screen. +type DaemonSet struct{} + +// ColorerFunc colors a resource row. +func (DaemonSet) ColorerFunc() ColorerFunc { + return func(ns string, r RowEvent) tcell.Color { + c := DefaultColorer(ns, r) + if r.Kind == EventAdd || r.Kind == EventUpdate { + return c + } + + markCol := 2 + if ns != AllNamespaces { + markCol = 1 + } + if strings.TrimSpace(r.Row.Fields[markCol]) != strings.TrimSpace(r.Row.Fields[markCol+2]) { + return ErrColor + } + + return StdColor + } +} + +// Header returns a header row. +func (DaemonSet) Header(ns string) HeaderRow { + var h HeaderRow + if isAllNamespace(ns) { + h = append(h, Header{Name: "NAMESPACE"}) + } + + return append(h, + Header{Name: "NAME"}, + Header{Name: "DESIRED", Align: tview.AlignRight}, + Header{Name: "CURRENT", Align: tview.AlignRight}, + Header{Name: "READY", Align: tview.AlignRight}, + Header{Name: "UP-TO-DATE", Align: tview.AlignRight}, + Header{Name: "AVAILABLE", Align: tview.AlignRight}, + Header{Name: "AGE", Decorator: AgeDecorator}, + ) +} + +// Render renders a K8s resource to screen. +func (d DaemonSet) Render(o interface{}, ns string, r *Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("Expected DaemonSet, but got %T", o) + } + var ds appsv1.DaemonSet + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ds) + if err != nil { + return err + } + + r.ID = MetaFQN(ds.ObjectMeta) + r.Fields = make(Fields, 0, len(d.Header(ns))) + if isAllNamespace(ns) { + r.Fields = append(r.Fields, ds.Namespace) + } + r.Fields = append(r.Fields, + ds.Name, + strconv.Itoa(int(ds.Status.DesiredNumberScheduled)), + strconv.Itoa(int(ds.Status.CurrentNumberScheduled)), + strconv.Itoa(int(ds.Status.NumberReady)), + strconv.Itoa(int(ds.Status.UpdatedNumberScheduled)), + strconv.Itoa(int(ds.Status.NumberAvailable)), + toAge(ds.ObjectMeta.CreationTimestamp), + ) + + return nil +} diff --git a/internal/render/ds_test.go b/internal/render/ds_test.go new file mode 100644 index 00000000..136984f4 --- /dev/null +++ b/internal/render/ds_test.go @@ -0,0 +1,17 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestDaemonSetRender(t *testing.T) { + c := render.DaemonSet{} + r := render.NewRow(9) + 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]) +} diff --git a/internal/render/ep.go b/internal/render/ep.go new file mode 100644 index 00000000..37f496b8 --- /dev/null +++ b/internal/render/ep.go @@ -0,0 +1,99 @@ +package render + +import ( + "fmt" + "strconv" + "strings" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// Endpoints renders a K8s Endpoints to screen. +type Endpoints struct{} + +// ColorerFunc colors a resource row. +func (Endpoints) ColorerFunc() ColorerFunc { + return DefaultColorer +} + +// Header returns a header row. +func (Endpoints) Header(ns string) HeaderRow { + var h HeaderRow + if isAllNamespace(ns) { + h = append(h, Header{Name: "NAMESPACE"}) + } + + return append(h, + Header{Name: "NAME"}, + Header{Name: "ENDPOINTS"}, + Header{Name: "AGE", Decorator: AgeDecorator}, + ) +} + +// Render renders a K8s resource to screen. +func (e Endpoints) Render(o interface{}, ns string, r *Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("Expected Endpoints, but got %T", o) + } + var ep v1.Endpoints + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ep) + if err != nil { + return err + } + + r.ID = MetaFQN(ep.ObjectMeta) + r.Fields = make(Fields, 0, len(e.Header(ns))) + if isAllNamespace(ns) { + r.Fields = append(r.Fields, ep.Namespace) + } + r.Fields = append(r.Fields, + ep.Name, + missing(toEPs(ep.Subsets)), + toAge(ep.ObjectMeta.CreationTimestamp), + ) + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func toEPs(ss []v1.EndpointSubset) string { + aa := make([]string, 0, len(ss)) + for _, s := range ss { + pp := make([]string, len(s.Ports)) + portsToStrs(s.Ports, pp) + a := make([]string, len(s.Addresses)) + proccessIPs(a, pp, s.Addresses) + aa = append(aa, strings.Join(a, ",")) + } + return strings.Join(aa, ",") +} + +func portsToStrs(pp []v1.EndpointPort, ss []string) { + for i := 0; i < len(pp); i++ { + ss[i] = strconv.Itoa(int(pp[i].Port)) + } +} + +func proccessIPs(aa []string, pp []string, addrs []v1.EndpointAddress) { + const maxIPs = 3 + var i int + for _, a := range addrs { + if len(a.IP) == 0 { + continue + } + if len(pp) == 0 { + aa[i], i = a.IP, i+1 + continue + } + if len(pp) > maxIPs { + aa[i], i = a.IP+":"+strings.Join(pp[:maxIPs], ",")+"...", i+1 + } else { + aa[i], i = a.IP+":"+strings.Join(pp, ","), i+1 + } + } +} diff --git a/internal/render/ep_test.go b/internal/render/ep_test.go new file mode 100644 index 00000000..14185526 --- /dev/null +++ b/internal/render/ep_test.go @@ -0,0 +1,17 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestEndpointsRender(t *testing.T) { + c := render.Endpoints{} + r := render.NewRow(4) + c.Render(load(t, "ep"), "", &r) + + assert.Equal(t, "default/dictionary1", r.ID) + assert.Equal(t, render.Fields{"default", "dictionary1", ""}, r.Fields[:3]) +} diff --git a/internal/render/ev.go b/internal/render/ev.go new file mode 100644 index 00000000..5a87191d --- /dev/null +++ b/internal/render/ev.go @@ -0,0 +1,85 @@ +package render + +import ( + "fmt" + "strconv" + "strings" + + "github.com/derailed/tview" + "github.com/gdamore/tcell" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// Event renders a K8s Event to screen. +type Event struct{} + +// ColorerFunc colors a resource row. +func (Event) ColorerFunc() ColorerFunc { + return func(ns string, r RowEvent) tcell.Color { + c := DefaultColorer(ns, r) + + markCol := 3 + if ns != AllNamespaces { + markCol = 2 + } + switch strings.TrimSpace(r.Row.Fields[markCol]) { + case "Failed": + c = ErrColor + case "Killing": + c = KillColor + } + + return c + } +} + +// Header returns a header rbw. +func (Event) Header(ns string) HeaderRow { + var h HeaderRow + if isAllNamespace(ns) { + h = append(h, Header{Name: "NAMESPACE"}) + } + + return append(h, + Header{Name: "NAME"}, + Header{Name: "REASON"}, + Header{Name: "SOURCE"}, + Header{Name: "COUNT", Align: tview.AlignRight}, + Header{Name: "MESSAGE"}, + Header{Name: "AGE", Decorator: AgeDecorator}, + ) +} + +// Render renders a K8s resource to screen. +func (e Event) Render(o interface{}, ns string, r *Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("Expected Event, but got %T", o) + } + var ev v1.Event + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ev) + if err != nil { + return err + } + + r.ID = MetaFQN(ev.ObjectMeta) + r.Fields = make(Fields, 0, len(e.Header(ns))) + if isAllNamespace(ns) { + r.Fields = append(r.Fields, ev.Namespace) + } + r.Fields = append(r.Fields, + asRef(ev.InvolvedObject), + ev.Reason, + ev.Source.Component, + strconv.Itoa(int(ev.Count)), + Truncate(ev.Message, 80), + toAge(ev.LastTimestamp)) + + return nil +} + +func asRef(r v1.ObjectReference) string { + return strings.ToLower(r.Kind) + ":" + r.Name +} diff --git a/internal/render/ev_test.go b/internal/render/ev_test.go new file mode 100644 index 00000000..766e5c7a --- /dev/null +++ b/internal/render/ev_test.go @@ -0,0 +1,17 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestEventRender(t *testing.T) { + c := render.Event{} + r := render.NewRow(7) + c.Render(load(t, "ev"), "", &r) + + assert.Equal(t, "default/hello-1567197780-mn4mv.15bfce150bd764dd", r.ID) + assert.Equal(t, render.Fields{"default", "pod:hello-1567197780-mn4mv", "Pulled", "kubelet", "1", `Successfully pulled image "blang/busybox-bash"`}, r.Fields[:6]) +} diff --git a/internal/render/generic.go b/internal/render/generic.go new file mode 100644 index 00000000..2b755718 --- /dev/null +++ b/internal/render/generic.go @@ -0,0 +1,98 @@ +package render + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" +) + +// Generic renders a generic resource to screen. +type Generic struct { + table *metav1beta1.Table +} + +func (g *Generic) SetTable(t *metav1beta1.Table) { + g.table = t +} + +// ColorerFunc colors a resource row. +func (Generic) ColorerFunc() ColorerFunc { + return DefaultColorer +} + +// Header returns a header row. +func (g *Generic) Header(ns string) HeaderRow { + if g.table == nil { + return HeaderRow{} + } + + h := make(HeaderRow, 0, len(g.table.ColumnDefinitions)) + if ns == "" { + h = append(h, Header{Name: "NAMESPACE"}) + } + for _, c := range g.table.ColumnDefinitions { + h = append(h, Header{Name: strings.ToUpper(c.Name)}) + } + + return h +} + +// Render renders a K8s resource to screen. +func (g *Generic) Render(o interface{}, ns string, r *Row) error { + row, ok := o.(*metav1beta1.TableRow) + if !ok { + return fmt.Errorf("expecting a TableRow but got %T", o) + } + + count := len(row.Cells) + if ns == AllNamespaces { + count++ + } + r.ID, ok = row.Cells[0].(string) + if !ok { + return fmt.Errorf("expecting row id to be a string but got %#v", row.Cells[0]) + } + + r.Fields = make(Fields, count) + var index int + if ns == AllNamespaces { + rns, err := extractNamespace(row.Object.Raw) + if err != nil { + return err + } + r.Fields[index] = rns + r.ID = FQN(rns, r.ID) + index++ + } + for _, c := range row.Cells { + r.Fields[index] = fmt.Sprintf("%v", c) + index++ + } + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func extractNamespace(raw []byte) (string, error) { + var obj map[string]interface{} + err := json.Unmarshal(raw, &obj) + if err != nil { + return "", err + } + + meta, ok := obj["metadata"].(map[string]interface{}) + if !ok { + return "", errors.New("no metadata found on generic resource") + } + ns, ok := meta["namespace"].(string) + if !ok { + return "", errors.New("invalid namespace found on generic metadata") + } + + return ns, nil +} diff --git a/internal/render/generic_test.go b/internal/render/generic_test.go new file mode 100644 index 00000000..236170c1 --- /dev/null +++ b/internal/render/generic_test.go @@ -0,0 +1,51 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" + metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + "k8s.io/apimachinery/pkg/runtime" +) + +func TestGenericRender(t *testing.T) { + var g render.Generic + + var r render.Row + row := makeGeneric().Rows[0] + assert.Nil(t, g.Render(&row, "blee", &r)) + + assert.Equal(t, "a", r.ID) + assert.Equal(t, render.Fields{"a", "b", "c"}, r.Fields) +} + +// Helpers... + +func makeGeneric() *metav1beta1.Table { + return &metav1beta1.Table{ + ColumnDefinitions: []metav1beta1.TableColumnDefinition{ + {Name: "A"}, + {Name: "B"}, + {Name: "C"}, + }, + Rows: []metav1beta1.TableRow{ + { + Object: runtime.RawExtension{ + Raw: []byte(`{ + "kind": "fred", + "apiVersion": "v1", + "metadata": { + "namespace": "blee", + "name": "fred" + }}`), + }, + Cells: []interface{}{ + "a", + "b", + "c", + }, + }, + }, + } +} diff --git a/internal/resource/helpers.go b/internal/render/helpers.go similarity index 76% rename from internal/resource/helpers.go rename to internal/render/helpers.go index 4ae61fc4..1a223a76 100644 --- a/internal/resource/helpers.go +++ b/internal/render/helpers.go @@ -1,4 +1,4 @@ -package resource +package render import ( "path" @@ -9,31 +9,39 @@ import ( "github.com/derailed/tview" runewidth "github.com/mattn/go-runewidth" + "github.com/rs/zerolog/log" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/duration" - "k8s.io/apimachinery/pkg/watch" ) -const ( - // DefaultNamespace indicator to fetch default namespace. - DefaultNamespace = "default" - // AllNamespace namespace name to span all namespaces. - AllNamespace = "all" - // AllNamespaces indicator to retrieve K8s resource for all namespaces. - AllNamespaces = "" - // NotNamespaced indicator for non namespaced resource. - NotNamespaced = "-" +const megaByte = 1024 * 1024 - // New track new resource events. - New watch.EventType = "NEW" - // Unchanged provides no change events. - Unchanged watch.EventType = "UNCHANGED" +// ToMB converts bytes to megabytes. +func ToMB(v int64) float64 { + return float64(v) / megaByte +} - // MissingValue indicates an unset value. - MissingValue = "" - // NAValue indicates a value that does not pertain. - NAValue = "n/a" -) +func asSelector(s *metav1.LabelSelector) string { + sel, err := metav1.LabelSelectorAsSelector(s) + if err != nil { + log.Error().Err(err).Msg("Selector conversion failed") + return NAValue + } + + return sel.String() +} + +func isAllNamespace(ns string) bool { + return ns == AllNamespaces +} + +type metric struct { + cpu, mem string +} + +func noMetric() metric { + return metric{cpu: NAValue, mem: NAValue} +} // MetaFQN returns a fully qualified resource name. func MetaFQN(m metav1.ObjectMeta) string { @@ -52,6 +60,7 @@ func FQN(ns, n string) string { return ns + "/" + n } +// ToSelector flattens a map selector to a string selector. func toSelector(m map[string]string) string { s := make([]string, 0, len(m)) for k, v := range m { @@ -61,7 +70,8 @@ func toSelector(m map[string]string) string { return strings.Join(s, ",") } -func empty(s []string) bool { +// Blank checks if a collection is empty or all values are blank. +func blank(s []string) bool { for _, v := range s { if len(v) != 0 { return false @@ -141,10 +151,6 @@ func check(s, sub string) string { return s } -func intToStr(i int64) string { - return strconv.Itoa(int(i)) -} - func boolToStr(b bool) string { switch b { case true: @@ -161,7 +167,7 @@ func toAge(timestamp metav1.Time) string { func toAgeHuman(s string) string { d, err := time.ParseDuration(s) if err != nil { - return "" + return NAValue } return duration.HumanDuration(d) @@ -220,3 +226,16 @@ func in(ll []string, s string) bool { } return false } + +// Pad a string up to the given length or truncates if greater than length. +func Pad(s string, width int) string { + if len(s) == width { + return s + } + + if len(s) > width { + return Truncate(s, width) + } + + return s + strings.Repeat(" ", width-len(s)) +} diff --git a/internal/render/helpers_test.go b/internal/render/helpers_test.go new file mode 100644 index 00000000..9b8b6765 --- /dev/null +++ b/internal/render/helpers_test.go @@ -0,0 +1,380 @@ +package render + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestToMB(t *testing.T) { + uu := []struct { + v int64 + e float64 + }{ + {0, 0}, + {2 * megaByte, 2}, + {10 * megaByte, 10}, + } + + for _, u := range uu { + assert.Equal(t, u.e, ToMB(u.v)) + } +} + +func TestToPerc(t *testing.T) { + uu := []struct { + v1, v2, e float64 + }{ + {0, 0, 0}, + {100, 200, 50}, + {200, 100, 200}, + } + + for _, u := range uu { + assert.Equal(t, u.e, toPerc(u.v1, u.v2)) + } +} + +func TestToAge(t *testing.T) { + uu := map[string]struct { + t time.Time + e string + }{ + "good": { + t: time.Now().Add(-10 * time.Second), + e: "10", + }, + } + + for k := range uu { + uc := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, uc.e, toAge(metav1.Time{Time: uc.t})[:2]) + }) + } +} + +func TestToAgeHuma(t *testing.T) { + uu := map[string]struct { + t time.Time + e string + }{ + "good": { + t: time.Now().Add(-10 * time.Second), + e: "10", + }, + } + + for k := range uu { + uc := uu[k] + t.Run(k, func(t *testing.T) { + ti := toAge(metav1.Time{Time: uc.t}) + assert.Equal(t, uc.e, toAgeHuman(ti)[:2]) + }) + } +} + +func TestJoin(t *testing.T) { + uu := map[string]struct { + i []string + e string + }{ + "zero": {[]string{}, ""}, + "std": {[]string{"a", "b", "c"}, "a,b,c"}, + "blank": {[]string{"", "", ""}, ""}, + "sparse": {[]string{"a", "", "c"}, "a,c"}, + } + + for k := range uu { + uc := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, uc.e, join(uc.i, ",")) + }) + } +} + +func TestBoolPtrToStr(t *testing.T) { + tv, fv := true, false + + uu := []struct { + p *bool + e string + }{ + {nil, "false"}, + {&tv, "true"}, + {&fv, "false"}, + } + + for _, u := range uu { + assert.Equal(t, u.e, boolPtrToStr(u.p)) + } +} + +func TestNamespaced(t *testing.T) { + uu := []struct { + p, ns, n string + }{ + {"fred/blee", "fred", "blee"}, + } + + for _, u := range uu { + ns, n := Namespaced(u.p) + assert.Equal(t, u.ns, ns) + assert.Equal(t, u.n, n) + } +} + +func TestMissing(t *testing.T) { + uu := []struct { + i, e string + }{ + {"fred", "fred"}, + {"", MissingValue}, + } + + for _, u := range uu { + assert.Equal(t, u.e, missing(u.i)) + } +} + +func TestBoolToStr(t *testing.T) { + uu := []struct { + i bool + e string + }{ + {true, "true"}, + {false, "false"}, + } + + for _, u := range uu { + assert.Equal(t, u.e, boolToStr(u.i)) + } +} + +func TestNa(t *testing.T) { + uu := []struct { + i, e string + }{ + {"fred", "fred"}, + {"", NAValue}, + } + + for _, u := range uu { + assert.Equal(t, u.e, na(u.i)) + } +} + +func TestTruncate(t *testing.T) { + uu := []struct { + s string + l int + e string + }{ + {"fred", 3, "fr…"}, + {"fred", 2, "f…"}, + {"fred", 10, "fred"}, + } + + for _, u := range uu { + assert.Equal(t, u.e, Truncate(u.s, u.l)) + } +} + +func TestToSelector(t *testing.T) { + uu := map[string]struct { + m map[string]string + e []string + }{ + "cool": { + map[string]string{"app": "fred", "env": "test"}, + []string{"app=fred,env=test", "env=test,app=fred"}, + }, + "empty": { + map[string]string{}, + []string{""}, + }, + } + + for k := range uu { + uc := uu[k] + t.Run(k, func(t *testing.T) { + s := toSelector(uc.m) + var match bool + for _, e := range uc.e { + if e == s { + match = true + } + } + assert.True(t, match) + }) + } +} + +func TestBlank(t *testing.T) { + uu := map[string]struct { + a []string + e bool + }{ + "full": { + a: []string{"fred", "blee"}, + }, + "empty": { + e: true, + }, + "blank": { + a: []string{"fred", ""}, + }, + } + + for k := range uu { + uc := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, uc.e, blank(uc.a)) + }) + } +} + +func TestIn(t *testing.T) { + uu := map[string]struct { + a []string + v string + e bool + }{ + "in": { + a: []string{"fred", "blee"}, + v: "blee", + e: true, + }, + "empty": { + v: "blee", + }, + "missing": { + a: []string{"fred", "blee"}, + v: "duh", + }, + } + + for k := range uu { + uc := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, uc.e, in(uc.a, uc.v)) + }) + } +} + +func TestMetaFQN(t *testing.T) { + uu := map[string]struct { + m metav1.ObjectMeta + e string + }{ + "full": {metav1.ObjectMeta{Namespace: "fred", Name: "blee"}, "fred/blee"}, + "nons": {metav1.ObjectMeta{Name: "blee"}, "blee"}, + } + + for k := range uu { + uc := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, uc.e, MetaFQN(uc.m)) + }) + } +} + +func TestFQN(t *testing.T) { + uu := map[string]struct { + ns, n string + e string + }{ + "full": {ns: "fred", n: "blee", e: "fred/blee"}, + "nons": {n: "blee", e: "blee"}, + } + + for k := range uu { + uc := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, uc.e, FQN(uc.ns, uc.n)) + }) + } +} + +func TestMapToStr(t *testing.T) { + uu := []struct { + i map[string]string + e string + }{ + {map[string]string{"blee": "duh", "aa": "bb"}, "aa=bb,blee=duh"}, + {map[string]string{}, MissingValue}, + } + for _, u := range uu { + assert.Equal(t, u.e, mapToStr(u.i)) + } +} + +func BenchmarkMapToStr(b *testing.B) { + ll := map[string]string{ + "blee": "duh", + "aa": "bb", + } + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + mapToStr(ll) + } +} + +func TestToMillicore(t *testing.T) { + uu := []struct { + v int64 + e string + }{ + {0, "0"}, + {2, "2"}, + {1000, "1000"}, + } + + for _, u := range uu { + assert.Equal(t, u.e, ToMillicore(u.v)) + } +} + +func TestToMi(t *testing.T) { + uu := []struct { + v float64 + e string + }{ + {0, "0"}, + {2, "2"}, + {1000, "1000"}, + } + + for _, u := range uu { + assert.Equal(t, u.e, ToMi(u.v)) + } +} + +func TestAsPerc(t *testing.T) { + uu := []struct { + v float64 + e string + }{ + {0, "0"}, + {10.5, "10"}, + {10, "10"}, + {0.05, "0"}, + } + + for _, u := range uu { + assert.Equal(t, u.e, AsPerc(u.v)) + } +} + +func BenchmarkAsPerc(b *testing.B) { + v := 10.5 + b.ResetTimer() + b.ReportAllocs() + for n := 0; n < b.N; n++ { + AsPerc(v) + } +} diff --git a/internal/render/hpa.go b/internal/render/hpa.go new file mode 100644 index 00000000..f21eaf64 --- /dev/null +++ b/internal/render/hpa.go @@ -0,0 +1,84 @@ +package render + +import ( + "fmt" + "strconv" + + "github.com/derailed/tview" + autoscalingv1 "k8s.io/api/autoscaling/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// HorizontalPodAutoscaler renders a K8s HorizontalPodAutoscaler to screen. +type HorizontalPodAutoscaler struct{} + +// ColorerFunc colors a resource row. +func (HorizontalPodAutoscaler) ColorerFunc() ColorerFunc { + return DefaultColorer +} + +// Header returns a header row. +func (HorizontalPodAutoscaler) Header(ns string) HeaderRow { + var h HeaderRow + if isAllNamespace(ns) { + h = append(h, Header{Name: "NAMESPACE"}) + } + + return append(h, + Header{Name: "NAME"}, + Header{Name: "REFERENCE"}, + Header{Name: "TARGETS"}, + Header{Name: "MINPODS", Align: tview.AlignRight}, + Header{Name: "MAXPODS", Align: tview.AlignRight}, + Header{Name: "REPLICAS", Align: tview.AlignRight}, + Header{Name: "AGE", Decorator: AgeDecorator}, + ) +} + +// Render renders a K8s resource to screen. +func (h HorizontalPodAutoscaler) Render(o interface{}, ns string, r *Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("Expected HorizontalPodAutoscaler, but got %T", o) + } + + var hpa autoscalingv1.HorizontalPodAutoscaler + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &hpa) + if err != nil { + return err + } + + r.ID = MetaFQN(hpa.ObjectMeta) + r.Fields = make(Fields, 0, len(h.Header(ns))) + if isAllNamespace(ns) { + r.Fields = append(r.Fields, hpa.Namespace) + } + r.Fields = append(r.Fields, + hpa.ObjectMeta.Name, + hpa.Spec.ScaleTargetRef.Name, + toMetrics(hpa.Spec, hpa.Status), + strconv.Itoa(int(*hpa.Spec.MinReplicas)), + strconv.Itoa(int(hpa.Spec.MaxReplicas)), + strconv.Itoa(int(hpa.Status.CurrentReplicas)), + toAge(hpa.ObjectMeta.CreationTimestamp), + ) + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func toMetrics(spec autoscalingv1.HorizontalPodAutoscalerSpec, status autoscalingv1.HorizontalPodAutoscalerStatus) string { + current := "" + if status.CurrentCPUUtilizationPercentage != nil { + current = strconv.Itoa(int(*status.CurrentCPUUtilizationPercentage)) + "%" + } + + target := "" + if spec.TargetCPUUtilizationPercentage != nil { + target = strconv.Itoa(int(*spec.TargetCPUUtilizationPercentage)) + } + return current + "/" + target + "%" +} diff --git a/internal/render/hpa_test.go b/internal/render/hpa_test.go new file mode 100644 index 00000000..9bad78cb --- /dev/null +++ b/internal/render/hpa_test.go @@ -0,0 +1,17 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestHorizontalPodAutoscalerRender(t *testing.T) { + c := render.HorizontalPodAutoscaler{} + r := render.NewRow(7) + c.Render(load(t, "hpa"), "", &r) + + assert.Equal(t, "default/nginx", r.ID) + assert.Equal(t, render.Fields{"default", "nginx", "nginx", "/10%", "1", "10"}, r.Fields[:6]) +} diff --git a/internal/render/ing.go b/internal/render/ing.go new file mode 100644 index 00000000..4f4fe747 --- /dev/null +++ b/internal/render/ing.go @@ -0,0 +1,100 @@ +package render + +import ( + "fmt" + "strings" + + v1 "k8s.io/api/core/v1" + "k8s.io/api/extensions/v1beta1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// Ingress renders a K8s Ingress to screen. +type Ingress struct{} + +// ColorerFunc colors a resource row. +func (Ingress) ColorerFunc() ColorerFunc { + return DefaultColorer +} + +// Header returns a header row. +func (Ingress) Header(ns string) HeaderRow { + var h HeaderRow + if isAllNamespace(ns) { + h = append(h, Header{Name: "NAMESPACE"}) + } + + return append(h, + Header{Name: "NAME"}, + Header{Name: "HOSTS"}, + Header{Name: "ADDRESS"}, + Header{Name: "PORT"}, + Header{Name: "AGE", Decorator: AgeDecorator}, + ) +} + +// Render renders a K8s resource to screen. +func (i Ingress) Render(o interface{}, ns string, r *Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("Expected Ingress, but got %T", o) + } + var ing v1beta1.Ingress + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ing) + if err != nil { + return err + } + + r.ID = MetaFQN(ing.ObjectMeta) + r.Fields = make(Fields, 0, len(i.Header(ns))) + if isAllNamespace(ns) { + r.Fields = append(r.Fields, ing.Namespace) + } + r.Fields = append(r.Fields, + ing.Name, + toHosts(ing.Spec.Rules), + toAddress(ing.Status.LoadBalancer), + toTLSPorts(ing.Spec.TLS), + toAge(ing.ObjectMeta.CreationTimestamp), + ) + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func toAddress(lbs v1.LoadBalancerStatus) string { + ings := lbs.Ingress + res := make([]string, 0, len(ings)) + for _, lb := range ings { + if len(lb.IP) > 0 { + res = append(res, lb.IP) + } else if len(lb.Hostname) != 0 { + res = append(res, lb.Hostname) + } + } + + return strings.Join(res, ",") +} + +func toTLSPorts(tls []v1beta1.IngressTLS) string { + if len(tls) != 0 { + return "80, 443" + } + + return "80" +} + +func toHosts(rr []v1beta1.IngressRule) string { + hh := make([]string, 0, len(rr)) + for _, r := range rr { + if r.Host == "" { + r.Host = "*" + } + hh = append(hh, r.Host) + } + + return strings.Join(hh, ",") +} diff --git a/internal/render/ing_test.go b/internal/render/ing_test.go new file mode 100644 index 00000000..deb968a8 --- /dev/null +++ b/internal/render/ing_test.go @@ -0,0 +1,17 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestIngressRender(t *testing.T) { + c := render.Ingress{} + r := render.NewRow(6) + c.Render(load(t, "ing"), "", &r) + + assert.Equal(t, "default/test-ingress", r.ID) + assert.Equal(t, render.Fields{"default", "test-ingress", "*", "", "80"}, r.Fields[:5]) +} diff --git a/internal/render/job.go b/internal/render/job.go new file mode 100644 index 00000000..f9130112 --- /dev/null +++ b/internal/render/job.go @@ -0,0 +1,135 @@ +package render + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/rs/zerolog/log" + batchv1 "k8s.io/api/batch/v1" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/duration" +) + +// Job renders a K8s Job to screen. +type Job struct{} + +// ColorerFunc colors a resource row. +func (Job) ColorerFunc() ColorerFunc { + return DefaultColorer +} + +// Header returns a header row. +func (Job) Header(ns string) HeaderRow { + var h HeaderRow + if isAllNamespace(ns) { + h = append(h, Header{Name: "NAMESPACE"}) + } + + return append(h, + Header{Name: "NAME"}, + Header{Name: "COMPLETIONS"}, + Header{Name: "DURATION"}, + Header{Name: "CONTAINERS"}, + Header{Name: "IMAGES"}, + Header{Name: "AGE", Decorator: AgeDecorator}, + ) +} + +// Render renders a K8s resource to screen. +func (j Job) Render(o interface{}, ns string, r *Row) error { + log.Debug().Msgf("JOB RENDER %q", ns) + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("Expected Job, but got %T", o) + } + var job batchv1.Job + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &job) + if err != nil { + return err + } + + r.ID = MetaFQN(job.ObjectMeta) + r.Fields = make(Fields, 0, len(j.Header(ns))) + if isAllNamespace(ns) { + r.Fields = append(r.Fields, job.Namespace) + } + cc, ii := toContainers(job.Spec.Template.Spec) + r.Fields = append(r.Fields, + job.Name, + toCompletion(job.Spec, job.Status), + toDuration(job.Status), + cc, + ii, + toAge(job.ObjectMeta.CreationTimestamp), + ) + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +const maxShow = 2 + +func toContainers(p v1.PodSpec) (string, string) { + cc, ii := parseContainers(p.InitContainers) + cn, ci := parseContainers(p.Containers) + + cc, ii = append(cc, cn...), append(ii, ci...) + + // Limit to 2 of each... + if len(cc) > maxShow { + cc = append(cc[:2], "(+"+strconv.Itoa(len(cc)-maxShow)+")...") + } + if len(ii) > maxShow { + ii = append(ii[:2], "(+"+strconv.Itoa(len(ii)-maxShow)+")...") + } + + return strings.Join(cc, ","), strings.Join(ii, ",") +} + +func parseContainers(cos []v1.Container) (nn, ii []string) { + for _, co := range cos { + nn = append(nn, co.Name) + ii = append(ii, co.Image) + } + + return nn, ii +} + +func toCompletion(spec batchv1.JobSpec, status batchv1.JobStatus) (s string) { + if spec.Completions != nil { + return strconv.Itoa(int(status.Succeeded)) + "/" + strconv.Itoa(int(*spec.Completions)) + } + + if spec.Parallelism == nil { + return strconv.Itoa(int(status.Succeeded)) + "/1" + } + + p := *spec.Parallelism + if p > 1 { + return strconv.Itoa(int(status.Succeeded)) + "/1 of " + strconv.Itoa(int(p)) + } + + return strconv.Itoa(int(status.Succeeded)) + "/1" +} + +func toDuration(status batchv1.JobStatus) string { + if status.StartTime == nil { + return MissingValue + } + + var d time.Duration + switch { + case status.CompletionTime == nil: + d = time.Since(status.StartTime.Time) + default: + d = status.CompletionTime.Sub(status.StartTime.Time) + } + + return duration.HumanDuration(d) +} diff --git a/internal/render/job_test.go b/internal/render/job_test.go new file mode 100644 index 00000000..7d375d57 --- /dev/null +++ b/internal/render/job_test.go @@ -0,0 +1,17 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestJobRender(t *testing.T) { + c := render.Job{} + r := render.NewRow(4) + 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", "c1", "blang/busybox-bash"}, r.Fields[:6]) +} diff --git a/internal/render/node.go b/internal/render/node.go new file mode 100644 index 00000000..a28edba2 --- /dev/null +++ b/internal/render/node.go @@ -0,0 +1,203 @@ +package render + +import ( + "fmt" + "strings" + + "github.com/derailed/tview" + "github.com/rs/zerolog/log" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +const ( + labelNodeRolePrefix = "node-role.kubernetes.io/" + nodeLabelRole = "kubernetes.io/role" +) + +// NodeWithMetrics represents a resourve object with usage metrics. +type NodeWithMetrics interface { + Object() runtime.Object + Metrics() *mv1beta1.NodeMetrics + Pods() []*v1.Pod +} + +// Node renders a K8s Node to screen. +type Node struct{} + +// ColorerFunc colors a resource row. +func (Node) ColorerFunc() ColorerFunc { + return DefaultColorer +} + +// Header returns a header row. +func (Node) Header(_ string) HeaderRow { + return HeaderRow{ + Header{Name: "NAME"}, + Header{Name: "STATUS"}, + Header{Name: "ROLE"}, + Header{Name: "VERSION"}, + Header{Name: "KERNEL"}, + Header{Name: "INTERNAL-IP"}, + Header{Name: "EXTERNAL-IP"}, + Header{Name: "CPU", Align: tview.AlignRight}, + Header{Name: "MEM", Align: tview.AlignRight}, + Header{Name: "%CPU", Align: tview.AlignRight}, + Header{Name: "%MEM", Align: tview.AlignRight}, + Header{Name: "ACPU", Align: tview.AlignRight}, + Header{Name: "AMEM", Align: tview.AlignRight}, + Header{Name: "AGE", Decorator: AgeDecorator}, + } +} + +// Render renders a K8s resource to screen. +func (n Node) Render(o interface{}, ns string, r *Row) error { + oo, ok := o.(NodeWithMetrics) + if !ok { + return fmt.Errorf("Expected NodeAndMetrics, but got %T", o) + } + + var no v1.Node + err := runtime.DefaultUnstructuredConverter.FromUnstructured(oo.Object().(*unstructured.Unstructured).Object, &no) + if err != nil { + log.Error().Err(err).Msg("Converting Node") + return err + } + + iIP, eIP := getIPs(no.Status.Addresses) + iIP, eIP = missing(iIP), missing(eIP) + + c, a, p := gatherNodeMX(&no, oo.Metrics()) + + sta := make([]string, 10) + status(no.Status, no.Spec.Unschedulable, sta) + ro := make([]string, 10) + nodeRoles(&no, ro) + + r.ID = MetaFQN(no.ObjectMeta) + r.Fields = make(Fields, 0, len(n.Header(ns))) + r.Fields = append(r.Fields, + no.Name, + join(sta, ","), + join(ro, ","), + no.Status.NodeInfo.KubeletVersion, + no.Status.NodeInfo.KernelVersion, + iIP, + eIP, + c.cpu, + c.mem, + p.cpu, + p.mem, + a.cpu, + a.mem, + toAge(no.ObjectMeta.CreationTimestamp), + ) + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func gatherNodeMX(no *v1.Node, mx *mv1beta1.NodeMetrics) (c metric, a metric, p metric) { + c, a, p = noMetric(), noMetric(), noMetric() + if mx == nil { + return + } + + cpu := mx.Usage.Cpu().MilliValue() + mem := ToMB(mx.Usage.Memory().Value()) + c = metric{ + cpu: ToMillicore(cpu), + mem: ToMi(mem), + } + + acpu := no.Status.Allocatable.Cpu().MilliValue() + amem := ToMB(no.Status.Allocatable.Memory().Value()) + a = metric{ + cpu: ToMillicore(acpu), + mem: ToMi(amem), + } + + p = metric{ + cpu: AsPerc(toPerc(float64(cpu), float64(acpu))), + mem: AsPerc(toPerc(mem, amem)), + } + + return +} + +func nodeRoles(node *v1.Node, res []string) { + index := 0 + for k, v := range node.Labels { + switch { + case strings.HasPrefix(k, labelNodeRolePrefix): + if role := strings.TrimPrefix(k, labelNodeRolePrefix); len(role) > 0 { + res[index] = role + index++ + } + case k == nodeLabelRole && v != "": + res[index] = v + index++ + } + } + + if empty(res) { + res[index] = MissingValue + } +} + +func getIPs(addrs []v1.NodeAddress) (iIP, eIP string) { + for _, a := range addrs { + switch a.Type { + case v1.NodeExternalIP: + eIP = a.Address + case v1.NodeInternalIP: + iIP = a.Address + } + } + + return +} + +func status(status v1.NodeStatus, exempt bool, res []string) { + var index int + conditions := make(map[v1.NodeConditionType]*v1.NodeCondition) + for n := range status.Conditions { + cond := status.Conditions[n] + conditions[cond.Type] = &cond + } + + validConditions := []v1.NodeConditionType{v1.NodeReady} + for _, validCondition := range validConditions { + condition, ok := conditions[validCondition] + if !ok { + continue + } + neg := "" + if condition.Status != v1.ConditionTrue { + neg = "Not" + } + res[index] = neg + string(condition.Type) + index++ + + } + if len(res) == 0 { + res[index] = "Unknown" + index++ + } + if exempt { + res[index] = "SchedulingDisabled" + } +} + +func empty(s []string) bool { + for _, v := range s { + if len(v) != 0 { + return false + } + } + return true +} diff --git a/internal/render/node_test.go b/internal/render/node_test.go new file mode 100644 index 00000000..ff19fe5b --- /dev/null +++ b/internal/render/node_test.go @@ -0,0 +1,61 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +func TestNodeRender(t *testing.T) { + pom := nodeMetrics{ + load(t, "no"), + makeNodeMX("n1", "10m", "10Mi"), + []*v1.Pod{}, + } + + var no render.Node + r := render.NewRow(14) + err := no.Render(pom, "", &r) + assert.Nil(t, err) + + assert.Equal(t, "minikube", r.ID) + e := render.Fields{"minikube", "Ready", "master", "v1.15.2", "4.15.0", "192.168.64.107", "", "10", "10", "0", "0", "4000", "7874"} + assert.Equal(t, e, r.Fields[:13]) +} + +// ---------------------------------------------------------------------------- +// Helpers... + +type nodeMetrics struct { + o *unstructured.Unstructured + m *mv1beta1.NodeMetrics + pod []*v1.Pod +} + +func (p nodeMetrics) Object() runtime.Object { + return p.o +} + +func (p nodeMetrics) Metrics() *mv1beta1.NodeMetrics { + return p.m +} + +func (p nodeMetrics) Pods() []*v1.Pod { + return p.pod +} + +func makeNodeMX(name, cpu, mem string) *mv1beta1.NodeMetrics { + return &mv1beta1.NodeMetrics{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: "default", + }, + Usage: makeRes(cpu, mem), + } +} diff --git a/internal/render/np.go b/internal/render/np.go new file mode 100644 index 00000000..f3a45a08 --- /dev/null +++ b/internal/render/np.go @@ -0,0 +1,187 @@ +package render + +import ( + "fmt" + "strings" + + v1beta1 "k8s.io/api/extensions/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// NetworkPolicy renders a K8s NetworkPolicy to screen. +type NetworkPolicy struct{} + +// ColorerFunc colors a resource row. +func (NetworkPolicy) ColorerFunc() ColorerFunc { + return DefaultColorer +} + +// Header returns a header row. +func (NetworkPolicy) Header(ns string) HeaderRow { + var h HeaderRow + if isAllNamespace(ns) { + h = append(h, Header{Name: "NAMESPACE"}) + } + + return append(h, + Header{Name: "NAME"}, + Header{Name: "ING-SELECTOR"}, + Header{Name: "ING-PORTS"}, + Header{Name: "ING-BLOCK"}, + Header{Name: "EGR-SELECTOR"}, + Header{Name: "EGR-PORTS"}, + Header{Name: "EGR-BLOCK"}, + Header{Name: "AGE", Decorator: AgeDecorator}, + ) +} + +// Render renders a K8s resource to screen. +func (n NetworkPolicy) Render(o interface{}, ns string, r *Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("Expected NetworkPolicy, but got %T", o) + } + var np v1beta1.NetworkPolicy + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &np) + if err != nil { + return err + } + + ip, is, ib := ingress(np.Spec.Ingress) + ep, es, eb := egress(np.Spec.Egress) + + r.ID = MetaFQN(np.ObjectMeta) + r.Fields = make(Fields, 0, len(n.Header(ns))) + if isAllNamespace(ns) { + r.Fields = append(r.Fields, np.Namespace) + } + r.Fields = append(r.Fields, + np.Name, + is, + ip, + ib, + es, + ep, + eb, + toAge(np.ObjectMeta.CreationTimestamp), + ) + + return nil +} + +// Helpers... + +func ingress(ii []v1beta1.NetworkPolicyIngressRule) (string, string, string) { + var ports, sels, blocks []string + for _, i := range ii { + if p := portsToStr(i.Ports); p != "" { + ports = append(ports, p) + } + ll, pp := peersToStr(i.From) + if ll != "" { + sels = append(sels, ll) + } + if pp != "" { + blocks = append(blocks, pp) + } + } + return strings.Join(ports, ","), strings.Join(sels, ","), strings.Join(blocks, ",") +} + +func egress(ee []v1beta1.NetworkPolicyEgressRule) (string, string, string) { + var ports, sels, blocks []string + for _, e := range ee { + if p := portsToStr(e.Ports); p != "" { + ports = append(ports, p) + } + ll, pp := peersToStr(e.To) + if ll != "" { + sels = append(sels, ll) + } + if pp != "" { + blocks = append(blocks, pp) + } + } + return strings.Join(ports, ","), strings.Join(sels, ","), strings.Join(blocks, ",") +} + +func portsToStr(pp []v1beta1.NetworkPolicyPort) string { + ports := make([]string, 0, len(pp)) + for _, p := range pp { + ports = append(ports, string(*p.Protocol)+":"+p.Port.String()) + } + return strings.Join(ports, ",") +} + +func peersToStr(pp []v1beta1.NetworkPolicyPeer) (string, string) { + sels := make([]string, 0, len(pp)) + ips := make([]string, 0, len(pp)) + for _, p := range pp { + if peer := renderPeer(p); peer != "" { + sels = append(sels, peer) + } + + if p.IPBlock == nil { + continue + } + if b := renderBlock(p.IPBlock); b != "" { + ips = append(ips, b) + } + } + return strings.Join(sels, ","), strings.Join(ips, ",") +} + +func renderBlock(b *v1beta1.IPBlock) string { + s := b.CIDR + + if len(b.Except) == 0 { + return s + } + + e, more := b.Except, false + if len(b.Except) > 2 { + e, more = e[:2], true + } + if more { + return s + "[" + strings.Join(e, ",") + "...]" + } + return s + "[" + strings.Join(b.Except, ",") + "]" +} + +func renderPeer(i v1beta1.NetworkPolicyPeer) string { + var s string + + if i.PodSelector != nil { + if m := mapToStr(i.PodSelector.MatchLabels); m != "" { + s += "po:" + m + } + if e := expToStr(i.PodSelector.MatchExpressions); e != "" { + s += "--" + e + } + } + + if i.NamespaceSelector != nil { + if m := mapToStr(i.NamespaceSelector.MatchLabels); m != "" { + s += "ns:" + m + } + if e := expToStr(i.NamespaceSelector.MatchExpressions); e != "" { + s += "--" + e + } + } + + return s +} + +func expToStr(ee []metav1.LabelSelectorRequirement) string { + ss := make([]string, len(ee)) + for i, e := range ee { + ss[i] = labToStr(e) + } + return strings.Join(ss, ",") +} + +func labToStr(e metav1.LabelSelectorRequirement) string { + return fmt.Sprintf("%s-%s%s", e.Key, e.Operator, strings.Join(e.Values, ",")) +} diff --git a/internal/render/np_test.go b/internal/render/np_test.go new file mode 100644 index 00000000..ab622d7a --- /dev/null +++ b/internal/render/np_test.go @@ -0,0 +1,17 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestNetworkPolicyRender(t *testing.T) { + c := render.NetworkPolicy{} + r := render.NewRow(9) + c.Render(load(t, "np"), "", &r) + + assert.Equal(t, "default/fred", r.ID) + assert.Equal(t, render.Fields{"default", "fred", "ns:app=blee,po:app=fred", "TCP:6379", "172.17.0.0/16[172.17.1.0/24,172.17.3.0/24...]", "", "TCP:5978", "10.0.0.0/24"}, r.Fields[:8]) +} diff --git a/internal/render/ns.go b/internal/render/ns.go new file mode 100644 index 00000000..7b7c76fd --- /dev/null +++ b/internal/render/ns.go @@ -0,0 +1,68 @@ +package render + +import ( + "fmt" + "strings" + + "github.com/gdamore/tcell" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// Namespace renders a K8s Namespace to screen. +type Namespace struct{} + +// ColorerFunc colors a resource row. +func (Namespace) ColorerFunc() ColorerFunc { + return func(ns string, r RowEvent) tcell.Color { + c := DefaultColorer(ns, r) + if r.Kind == EventAdd { + return c + } + + if r.Kind == EventUpdate { + c = StdColor + } + switch strings.TrimSpace(r.Row.Fields[1]) { + case "Inactive", Terminating: + c = ErrColor + } + if strings.Contains(strings.TrimSpace(r.Row.Fields[0]), "*") { + c = HighlightColor + } + + return c + } +} + +// Header returns a header rbw. +func (Namespace) Header(string) HeaderRow { + return HeaderRow{ + Header{Name: "NAME"}, + Header{Name: "STATUS"}, + Header{Name: "AGE", Decorator: AgeDecorator}, + } +} + +// Render renders a K8s resource to screen. +func (Namespace) Render(o interface{}, _ string, r *Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("Expected Namespace, but got %T", o) + } + var ns v1.Namespace + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ns) + if err != nil { + return err + } + + r.ID = MetaFQN(ns.ObjectMeta) + r.Fields = Fields{ + ns.Name, + string(ns.Status.Phase), + toAge(ns.ObjectMeta.CreationTimestamp), + } + + return nil +} diff --git a/internal/render/ns_test.go b/internal/render/ns_test.go new file mode 100644 index 00000000..89736e22 --- /dev/null +++ b/internal/render/ns_test.go @@ -0,0 +1,44 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestNSColorer(t *testing.T) { + var ( + ns = render.Row{Fields: render.Fields{"blee", "Active"}} + term = render.Row{Fields: render.Fields{"blee", render.Terminating}} + dead = render.Row{Fields: render.Fields{"blee", "Inactive"}} + ) + + uu := colorerUCs{ + // Add AllNS + {"", render.RowEvent{Kind: render.EventAdd, Row: ns}, render.AddColor}, + // Mod AllNS + {"", render.RowEvent{Kind: render.EventUpdate, Row: ns}, render.ModColor}, + // MoChange AllNS + {"", render.RowEvent{Kind: render.EventUnchanged, Row: ns}, render.StdColor}, + // Bust NS + {"", render.RowEvent{Kind: render.EventUnchanged, Row: term}, render.ErrColor}, + // Bust NS + {"", render.RowEvent{Kind: render.EventUnchanged, Row: dead}, render.ErrColor}, + } + + var n render.Namespace + f := n.ColorerFunc() + for _, u := range uu { + assert.Equal(t, u.e, f(u.ns, u.r)) + } +} + +func TestNamespaceRender(t *testing.T) { + c := render.Namespace{} + r := render.NewRow(3) + c.Render(load(t, "ns"), "-", &r) + + assert.Equal(t, "kube-system", r.ID) + assert.Equal(t, render.Fields{"kube-system", "Active"}, r.Fields[:2]) +} diff --git a/internal/render/pdb.go b/internal/render/pdb.go new file mode 100644 index 00000000..167492a7 --- /dev/null +++ b/internal/render/pdb.go @@ -0,0 +1,97 @@ +package render + +import ( + "fmt" + "strconv" + "strings" + + "github.com/derailed/tview" + "github.com/gdamore/tcell" + v1beta1 "k8s.io/api/policy/v1beta1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" +) + +// PodDisruptionBudget renders a K8s PodDisruptionBudget to screen. +type PodDisruptionBudget struct{} + +// ColorerFunc colors a resource row. +func (PodDisruptionBudget) ColorerFunc() ColorerFunc { + return func(ns string, r RowEvent) tcell.Color { + c := DefaultColorer(ns, r) + if r.Kind == EventAdd || r.Kind == EventUpdate { + return c + } + + markCol := 5 + if ns != AllNamespaces { + markCol = 4 + } + if strings.TrimSpace(r.Row.Fields[markCol]) != strings.TrimSpace(r.Row.Fields[markCol+1]) { + return ErrColor + } + + return StdColor + } + +} + +// Header returns a header row. +func (PodDisruptionBudget) Header(ns string) HeaderRow { + var h HeaderRow + if isAllNamespace(ns) { + h = append(h, Header{Name: "NAMESPACE"}) + } + + return append(h, + Header{Name: "NAME"}, + Header{Name: "MIN AVAILABLE", Align: tview.AlignRight}, + Header{Name: "MAX_ UNAVAILABLE", Align: tview.AlignRight}, + Header{Name: "ALLOWED DISRUPTIONS", Align: tview.AlignRight}, + Header{Name: "CURRENT", Align: tview.AlignRight}, + Header{Name: "DESIRED", Align: tview.AlignRight}, + Header{Name: "EXPECTED", Align: tview.AlignRight}, + Header{Name: "AGE", Decorator: AgeDecorator}, + ) +} + +// Render renders a K8s resource to screen. +func (p PodDisruptionBudget) Render(o interface{}, ns string, r *Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("Expected PodDisruptionBudget, but got %T", o) + } + var pdb v1beta1.PodDisruptionBudget + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &pdb) + if err != nil { + return err + } + + r.ID = MetaFQN(pdb.ObjectMeta) + r.Fields = make(Fields, 0, len(p.Header(ns))) + if isAllNamespace(ns) { + r.Fields = append(r.Fields, pdb.Namespace) + } + r.Fields = append(r.Fields, + pdb.Name, + numbToStr(pdb.Spec.MinAvailable), + numbToStr(pdb.Spec.MaxUnavailable), + strconv.Itoa(int(pdb.Status.PodDisruptionsAllowed)), + strconv.Itoa(int(pdb.Status.CurrentHealthy)), + strconv.Itoa(int(pdb.Status.DesiredHealthy)), + strconv.Itoa(int(pdb.Status.ExpectedPods)), + toAge(pdb.ObjectMeta.CreationTimestamp), + ) + + return nil +} + +// Helpers... + +func numbToStr(n *intstr.IntOrString) string { + if n == nil { + return NAValue + } + return strconv.Itoa(int(n.IntVal)) +} diff --git a/internal/render/pdb_test.go b/internal/render/pdb_test.go new file mode 100644 index 00000000..7aa2dee7 --- /dev/null +++ b/internal/render/pdb_test.go @@ -0,0 +1,17 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestPodDisruptionBudgetRender(t *testing.T) { + c := render.PodDisruptionBudget{} + r := render.NewRow(9) + c.Render(load(t, "pdb"), "", &r) + + assert.Equal(t, "default/fred", r.ID) + assert.Equal(t, render.Fields{"default", "fred", "2", "n/a", "0", "0", "2", "0"}, r.Fields[:8]) +} diff --git a/internal/render/pod.go b/internal/render/pod.go new file mode 100644 index 00000000..060f762c --- /dev/null +++ b/internal/render/pod.go @@ -0,0 +1,301 @@ +package render + +import ( + "fmt" + "strconv" + "strings" + + "github.com/derailed/tview" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kubernetes/pkg/util/node" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +// PodWithMetrics represents a resourve object with usage metrics. +type PodWithMetrics interface { + Object() runtime.Object + Metrics() *mv1beta1.PodMetrics +} + +// Pod renders a K8s Pod to screen. +type Pod struct{} + +// ColorerFunc colors a resource row. +func (p Pod) ColorerFunc() ColorerFunc { + return func(ns string, re RowEvent) tcell.Color { + c := DefaultColorer(ns, re) + + readyCol := 2 + if len(ns) != 0 { + readyCol = 1 + } + statusCol := readyCol + 1 + + ready, status := strings.TrimSpace(re.Row.Fields[readyCol]), strings.TrimSpace(re.Row.Fields[statusCol]) + c = p.checkReadyCol(ready, status, c) + + switch status { + case ContainerCreating, PodInitializing: + return AddColor + case Initialized: + return HighlightColor + case Completed: + return CompletedColor + case Running: + case Terminating: + return KillColor + default: + return ErrColor + } + + return c + } +} + +func (Pod) checkReadyCol(readyCol, statusCol string, c tcell.Color) tcell.Color { + if statusCol == "Completed" { + return c + } + + tokens := strings.Split(readyCol, "/") + if len(tokens) == 2 && (tokens[0] == "0" || tokens[0] != tokens[1]) { + return ErrColor + } + return c +} + +// Header returns a header row. +func (Pod) Header(ns string) HeaderRow { + var h HeaderRow + if isAllNamespace(ns) { + h = append(h, Header{Name: "NAMESPACE"}) + } + + return append(h, + Header{Name: "NAME"}, + Header{Name: "READY"}, + Header{Name: "STATUS"}, + Header{Name: "RS", Align: tview.AlignRight}, + Header{Name: "CPU", Align: tview.AlignRight}, + Header{Name: "MEM", Align: tview.AlignRight}, + Header{Name: "%CPU", Align: tview.AlignRight}, + Header{Name: "%MEM", Align: tview.AlignRight}, + Header{Name: "IP"}, + Header{Name: "NODE"}, + Header{Name: "QOS"}, + Header{Name: "AGE", Decorator: AgeDecorator}, + ) +} + +// Render renders a K8s resource to screen. +func (p Pod) Render(o interface{}, ns string, r *Row) error { + oo, ok := o.(PodWithMetrics) + if !ok { + return fmt.Errorf("Expected PodAndMetrics, but got %T", o) + } + + var po v1.Pod + err := runtime.DefaultUnstructuredConverter.FromUnstructured(oo.Object().(*unstructured.Unstructured).Object, &po) + if err != nil { + log.Error().Err(err).Msg("Expecting a pod resource") + return err + } + + ss := po.Status.ContainerStatuses + cr, _, rc := p.statuses(ss) + c, perc := p.gatherPodMX(&po, oo.Metrics()) + + r.ID = MetaFQN(po.ObjectMeta) + r.Fields = make(Fields, 0, len(p.Header(ns))) + if isAllNamespace(ns) { + r.Fields = append(r.Fields, po.Namespace) + } + r.Fields = append(r.Fields, + po.ObjectMeta.Name, + strconv.Itoa(cr)+"/"+strconv.Itoa(len(ss)), + p.phase(&po), + strconv.Itoa(rc), + c.cpu, + c.mem, + perc.cpu, + perc.mem, + na(po.Status.PodIP), + na(po.Spec.NodeName), + p.mapQOS(po.Status.QOSClass), + toAge(po.ObjectMeta.CreationTimestamp), + ) + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func (*Pod) gatherPodMX(pod *v1.Pod, mx *mv1beta1.PodMetrics) (c, p metric) { + c, p = noMetric(), noMetric() + if mx == nil { + return + } + + cpu, mem := currentRes(mx) + c = metric{ + cpu: ToMillicore(cpu.MilliValue()), + mem: ToMi(ToMB(mem.Value())), + } + + rc, rm := requestedRes(pod) + p = metric{ + cpu: AsPerc(toPerc(float64(cpu.MilliValue()), float64(rc.MilliValue()))), + mem: AsPerc(toPerc(ToMB(mem.Value()), ToMB(rm.Value()))), + } + + return +} + +func containerResources(co v1.Container) (cpu, mem *resource.Quantity) { + req, limit := co.Resources.Requests, co.Resources.Limits + + switch { + case len(req) != 0: + cpu, mem = req.Cpu(), req.Memory() + case len(limit) != 0: + cpu, mem = limit.Cpu(), limit.Memory() + } + + return +} + +func requestedRes(po *v1.Pod) (cpu, mem resource.Quantity) { + for _, co := range po.Spec.Containers { + c, m := containerResources(co) + if c != nil { + cpu.Add(*c) + } + if m != nil { + mem.Add(*m) + } + } + return +} + +func currentRes(mx *mv1beta1.PodMetrics) (cpu, mem resource.Quantity) { + for _, co := range mx.Containers { + c, m := co.Usage.Cpu(), co.Usage.Memory() + cpu.Add(*c) + mem.Add(*m) + } + return +} + +func (*Pod) mapQOS(class v1.PodQOSClass) string { + switch class { + case v1.PodQOSGuaranteed: + return "GA" + case v1.PodQOSBurstable: + return "BU" + default: + return "BE" + } +} + +func (*Pod) statuses(ss []v1.ContainerStatus) (cr, ct, rc int) { + for _, c := range ss { + if c.State.Terminated != nil { + ct++ + } + if c.Ready { + cr = cr + 1 + } + rc += int(c.RestartCount) + } + + return +} + +func (p *Pod) phase(po *v1.Pod) string { + status := string(po.Status.Phase) + if po.Status.Reason != "" { + if po.DeletionTimestamp != nil && po.Status.Reason == node.NodeUnreachablePodReason { + return "Unknown" + } + status = po.Status.Reason + } + + status, ok := p.initContainerPhase(po.Status, len(po.Spec.InitContainers), status) + if ok { + return status + } + + status, ok = p.containerPhase(po.Status, status) + if ok && status == "Completed" { + status = "Running" + } + if po.DeletionTimestamp == nil { + return status + } + + return "Terminated" +} + +func (*Pod) containerPhase(st v1.PodStatus, status string) (string, bool) { + var running bool + for i := len(st.ContainerStatuses) - 1; i >= 0; i-- { + cs := st.ContainerStatuses[i] + switch { + case cs.State.Waiting != nil && cs.State.Waiting.Reason != "": + status = cs.State.Waiting.Reason + case cs.State.Terminated != nil && cs.State.Terminated.Reason != "": + status = cs.State.Terminated.Reason + case cs.State.Terminated != nil: + if cs.State.Terminated.Signal != 0 { + status = "Signal:" + strconv.Itoa(int(cs.State.Terminated.Signal)) + } else { + status = "ExitCode:" + strconv.Itoa(int(cs.State.Terminated.ExitCode)) + } + case cs.Ready && cs.State.Running != nil: + running = true + } + } + + return status, running +} + +func (*Pod) initContainerPhase(st v1.PodStatus, initCount int, status string) (string, bool) { + for i, cs := range st.InitContainerStatuses { + s := checkContainerStatus(cs, i, initCount) + if s == "" { + continue + } + return s, true + } + + return status, false +} + +// ---------------------------------------------------------------------------- +// Helpers.. + +func checkContainerStatus(cs v1.ContainerStatus, i, initCount int) string { + switch { + case cs.State.Terminated != nil: + if cs.State.Terminated.ExitCode == 0 { + return "" + } + if cs.State.Terminated.Reason != "" { + return "Init:" + cs.State.Terminated.Reason + } + if cs.State.Terminated.Signal != 0 { + return "Init:Signal:" + strconv.Itoa(int(cs.State.Terminated.Signal)) + } + return "Init:ExitCode:" + strconv.Itoa(int(cs.State.Terminated.ExitCode)) + case cs.State.Waiting != nil && cs.State.Waiting.Reason != "" && cs.State.Waiting.Reason != "PodInitializing": + return "Init:" + cs.State.Waiting.Reason + default: + return "Init:" + strconv.Itoa(i) + "/" + strconv.Itoa(initCount) + } +} diff --git a/internal/render/pod_test.go b/internal/render/pod_test.go new file mode 100644 index 00000000..a735e6fd --- /dev/null +++ b/internal/render/pod_test.go @@ -0,0 +1,125 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/gdamore/tcell" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + res "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +type ( + colorerUC struct { + ns string + r render.RowEvent + e tcell.Color + } + + colorerUCs []colorerUC +) + +func TestPodColorer(t *testing.T) { + var ( + nsRow = render.Row{Fields: render.Fields{"blee", "fred", "1/1", "Running"}} + toastNS = render.Row{Fields: render.Fields{"blee", "fred", "1/1", "Boom"}} + notReadyNS = render.Row{Fields: render.Fields{"blee", "fred", "0/1", "Boom"}} + row = render.Row{Fields: render.Fields{"fred", "1/1", "Running"}} + toast = render.Row{Fields: render.Fields{"fred", "1/1", "Boom"}} + notReady = render.Row{Fields: render.Fields{"fred", "0/1", "Boom"}} + ) + + uu := colorerUCs{ + // Add allNS + {"", render.RowEvent{Kind: render.EventAdd, Row: nsRow}, render.AddColor}, + // Add Namespaced + {"blee", render.RowEvent{Kind: render.EventAdd, Row: row}, render.AddColor}, + // Mod AllNS + {"", render.RowEvent{Kind: render.EventUpdate, Row: nsRow}, render.ModColor}, + // Mod Namespaced + {"blee", render.RowEvent{Kind: render.EventUpdate, Row: row}, render.ModColor}, + // Mod Busted AllNS + {"", render.RowEvent{Kind: render.EventUpdate, Row: toastNS}, render.ErrColor}, + // Mod Busted Namespaced + {"blee", render.RowEvent{Kind: render.EventUpdate, Row: toast}, render.ErrColor}, + // NotReady AllNS + {"", render.RowEvent{Kind: render.EventUpdate, Row: notReadyNS}, render.ErrColor}, + // NotReady Namespaced + {"blee", render.RowEvent{Kind: render.EventUpdate, Row: notReady}, render.ErrColor}, + } + + var p render.Pod + f := p.ColorerFunc() + for _, u := range uu { + assert.Equal(t, u.e, f(u.ns, u.r)) + } +} + +func TestPodRender(t *testing.T) { + pom := podMetrics{load(t, "po"), makePodMX("nginx", "10m", "10Mi")} + + var po render.Pod + r := render.NewRow(12) + err := po.Render(pom, "", &r) + assert.Nil(t, err) + + assert.Equal(t, "default/nginx", r.ID) + e := render.Fields{"default", "nginx", "1/1", "Running", "0", "10", "10", "10", "14", "172.17.0.6", "minikube", "BE"} + assert.Equal(t, e, r.Fields[:12]) +} + +func TestPodInitRender(t *testing.T) { + pom := podMetrics{load(t, "po_init"), makePodMX("nginx", "10m", "10Mi")} + + var po render.Pod + r := render.NewRow(12) + err := po.Render(pom, "", &r) + assert.Nil(t, err) + + assert.Equal(t, "default/nginx", r.ID) + e := render.Fields{"default", "nginx", "1/1", "Init:0/1", "0", "10", "10", "10", "14", "172.17.0.6", "minikube", "BE"} + assert.Equal(t, e, r.Fields[:12]) +} + +// ---------------------------------------------------------------------------- +// Helpers... + +type podMetrics struct { + o *unstructured.Unstructured + m *mv1beta1.PodMetrics +} + +func (p podMetrics) Object() runtime.Object { + return p.o +} + +func (p podMetrics) Metrics() *mv1beta1.PodMetrics { + return p.m +} + +func makePodMX(name, cpu, mem string) *mv1beta1.PodMetrics { + return &mv1beta1.PodMetrics{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: "default", + }, + Containers: []mv1beta1.ContainerMetrics{ + {Usage: makeRes(cpu, mem)}, + }, + } +} + +func makeRes(c, m string) v1.ResourceList { + cpu, _ := res.ParseQuantity(c) + mem, _ := res.ParseQuantity(m) + + return v1.ResourceList{ + v1.ResourceCPU: cpu, + v1.ResourceMemory: mem, + } +} diff --git a/internal/render/policy.go b/internal/render/policy.go new file mode 100644 index 00000000..ad6fbdcc --- /dev/null +++ b/internal/render/policy.go @@ -0,0 +1,120 @@ +package render + +import ( + "fmt" + + "github.com/gdamore/tcell" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func rbacVerbHeader() HeaderRow { + return HeaderRow{ + Header{Name: "GET "}, + Header{Name: "LIST "}, + Header{Name: "WATCH "}, + Header{Name: "CREATE"}, + Header{Name: "PATCH "}, + Header{Name: "UPDATE"}, + Header{Name: "DELETE"}, + Header{Name: "DLIST "}, + Header{Name: "EXTRAS"}, + } +} + +// Policy renders a rbac policy to screen. +type Policy struct{} + +// ColorerFunc colors a resource row. +func (Policy) ColorerFunc() ColorerFunc { + return func(ns string, re RowEvent) tcell.Color { + return tcell.ColorMediumSpringGreen + } +} + +// Header returns a header row. +func (Policy) Header(ns string) HeaderRow { + h := HeaderRow{ + Header{Name: "NAMESPACE"}, + Header{Name: "NAME"}, + Header{Name: "API GROUP"}, + Header{Name: "BINDING"}, + } + return append(h, rbacVerbHeader()...) +} + +// Render renders a K8s resource to screen. +func (Policy) Render(o interface{}, gvr string, r *Row) error { + p, ok := o.(PolicyRes) + if !ok { + return fmt.Errorf("expecting PolicyRes but got %T", o) + } + + r.ID = FQN(p.Namespace, p.Resource) + r.Fields = append(r.Fields, p.Namespace, cleanseResource(p.Resource), p.Group, p.Binding) + r.Fields = append(r.Fields, asVerbs(p.Verbs)...) + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func cleanseResource(r string) string { + if r[0] == '/' { + return r + } + _, n := Namespaced(r) + return n +} + +type PolicyRes struct { + Namespace, Binding string + Resource, Group string + ResourceName string + NonResourceURL string + Verbs []string +} + +func NewPolicyRes(ns, binding, res, grp string, vv []string) PolicyRes { + return PolicyRes{ + Namespace: ns, + Binding: binding, + Resource: res, + Group: grp, + Verbs: vv, + } +} + +// GetObjectKind returns a schema object. +func (p PolicyRes) GetObjectKind() schema.ObjectKind { + return nil +} + +// DeepCopyObject returns a container copy. +func (p PolicyRes) DeepCopyObject() runtime.Object { + return p +} + +type Policies []PolicyRes + +func (pp Policies) Upsert(p PolicyRes) Policies { + idx, ok := pp.findPol(p.Resource) + if !ok { + return append(pp, p) + } + pp[idx] = p + + return pp +} + +// Find locates a row by id. Retturns false is not found. +func (pp Policies) findPol(res string) (int, bool) { + for i, p := range pp { + if p.Resource == res { + return i, true + } + } + + return 0, false +} diff --git a/internal/render/policy_test.go b/internal/render/policy_test.go new file mode 100644 index 00000000..61a30ddf --- /dev/null +++ b/internal/render/policy_test.go @@ -0,0 +1,41 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestPolicyRender(t *testing.T) { + var p render.Policy + + var r render.Row + o := render.PolicyRes{ + Namespace: "blee", + Binding: "fred", + Resource: "res", + Group: "grp", + ResourceName: "bob", + NonResourceURL: "/blee", + Verbs: []string{"get", "list", "watch"}, + } + + assert.Nil(t, p.Render(o, "fred", &r)) + assert.Equal(t, "blee/res", r.ID) + assert.Equal(t, render.Fields{ + "blee", + "res", + "grp", + "fred", + "[green::b] ✓ [::]", + "[green::b] ✓ [::]", + "[green::b] ✓ [::]", + "[orangered::b] 𐄂 [::]", + "[orangered::b] 𐄂 [::]", + "[orangered::b] 𐄂 [::]", + "[orangered::b] 𐄂 [::]", + "[orangered::b] 𐄂 [::]", + "", + }, r.Fields) +} diff --git a/internal/render/port_forward_test.go b/internal/render/port_forward_test.go new file mode 100644 index 00000000..3ce102f1 --- /dev/null +++ b/internal/render/port_forward_test.go @@ -0,0 +1,59 @@ +package render_test + +import ( + "testing" + + "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{ + C: 1, + N: 1, + Host: "0.0.0.0", + Path: "/", + }, + } + + assert.Nil(t, p.Render(o, "fred", &r)) + assert.Equal(t, "blee/fred", r.ID) + assert.Equal(t, render.Fields{ + "blee", + "fred", + "co", + "p1", + "http://0.0.0.0:p1/", + "1", + "1", + "2m", + }, r.Fields) +} + +// Helpers... + +type fwd struct{} + +func (f fwd) Path() string { + return "blee/fred" +} + +func (f fwd) Container() string { + return "co" +} + +func (f fwd) Ports() []string { + return []string{"p1"} +} + +func (f fwd) Active() bool { + return true +} + +func (f fwd) Age() string { + return "2m" +} diff --git a/internal/render/portforward.go b/internal/render/portforward.go new file mode 100644 index 00000000..aad1b8c7 --- /dev/null +++ b/internal/render/portforward.go @@ -0,0 +1,121 @@ +package render + +import ( + "fmt" + "strings" + + "github.com/gdamore/tcell" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// Forwarder represents a port forwarder. +type Forwarder interface { + // Path returns a resource FQN. + Path() string + + // Container returns a container name. + Container() string + + // Ports returns container exposed ports. + Ports() []string + + // Active returns forwarder current state. + Active() bool + + // Age returns forwarder age. + Age() string +} + +// PortForward renders a portforwards to screen. +type PortForward struct{} + +// ColorerFunc colors a resource row. +func (PortForward) ColorerFunc() ColorerFunc { + return func(ns string, re RowEvent) tcell.Color { + return tcell.ColorSkyblue + } +} + +// Header returns a header row. +func (PortForward) Header(ns string) HeaderRow { + return HeaderRow{ + Header{Name: "NAMESPACE"}, + Header{Name: "POD"}, + Header{Name: "CONTAINER"}, + Header{Name: "PORTS"}, + Header{Name: "URL"}, + Header{Name: "C"}, + Header{Name: "N"}, + Header{Name: "AGE", Decorator: AgeDecorator}, + } +} + +// Render renders a K8s resource to screen. +func (f PortForward) Render(o interface{}, gvr string, r *Row) error { + pf, ok := o.(ForwardRes) + if !ok { + return fmt.Errorf("expecting a ForwardRes but got %T", o) + } + + ports := strings.Split(pf.Ports()[0], ":") + ns, n := Namespaced(pf.Path()) + + r.ID = pf.Path() + r.Fields = Fields{ + ns, + trimContainer(n), + pf.Container(), + strings.Join(pf.Ports(), ","), + UrlFor(pf.Config.Host, pf.Config.Path, ports[0]), + asNum(pf.Config.C), + asNum(pf.Config.N), + pf.Age(), + } + + return nil +} + +// Helpers... + +func trimContainer(n string) string { + tokens := strings.Split(n, ":") + if len(tokens) == 0 { + return n + } + return tokens[0] +} + +// UrlFor computes fq url for a given benchmark configuration. +func UrlFor(host, path, port string) string { + if host == "" { + host = "localhost" + } + if path == "" { + path = "/" + } + + return "http://" + host + ":" + port + path +} + +// BenchCfg represents a benchmark configuration. +type BenchCfg struct { + C, N int + Host, Path string +} + +// ForwardRes represents a benchmark resource. +type ForwardRes struct { + Forwarder + Config BenchCfg +} + +// GetObjectKind returns a schema object. +func (f ForwardRes) GetObjectKind() schema.ObjectKind { + return nil +} + +// DeepCopyObject returns a container copy. +func (f ForwardRes) DeepCopyObject() runtime.Object { + return f +} diff --git a/internal/render/pv.go b/internal/render/pv.go new file mode 100644 index 00000000..c142abb0 --- /dev/null +++ b/internal/render/pv.go @@ -0,0 +1,137 @@ +package render + +import ( + "fmt" + "path" + "strings" + + "github.com/gdamore/tcell" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// PersistentVolume renders a K8s PersistentVolume to screen. +type PersistentVolume struct{} + +// ColorerFunc colors a resource row. +func (PersistentVolume) ColorerFunc() ColorerFunc { + return func(ns string, r RowEvent) tcell.Color { + c := DefaultColorer(ns, r) + if r.Kind == EventAdd || r.Kind == EventUpdate { + return c + } + + status := strings.TrimSpace(r.Row.Fields[4]) + switch status { + case "Bound": + c = StdColor + case "Available": + c = tcell.ColorYellow + default: + c = ErrColor + } + + return c + } + +} + +// Header returns a header rbw. +func (PersistentVolume) Header(string) HeaderRow { + return HeaderRow{ + Header{Name: "NAME"}, + Header{Name: "CAPACITY"}, + Header{Name: "ACCESS MODES"}, + Header{Name: "RECLAIM POLICY"}, + Header{Name: "STATUS"}, + Header{Name: "CLAIM"}, + Header{Name: "STORAGECLASS"}, + Header{Name: "REASON"}, + Header{Name: "AGE", Decorator: AgeDecorator}, + } +} + +// Render renders a K8s resource to screen. +func (p PersistentVolume) Render(o interface{}, ns string, r *Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("Expected PersistentVolume, but got %T", o) + } + var pv v1.PersistentVolume + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &pv) + if err != nil { + return err + } + + phase := pv.Status.Phase + if pv.ObjectMeta.DeletionTimestamp != nil { + phase = "Terminating" + } + var claim string + if pv.Spec.ClaimRef != nil { + claim = path.Join(pv.Spec.ClaimRef.Namespace, pv.Spec.ClaimRef.Name) + } + class, found := pv.Annotations[v1.BetaStorageClassAnnotation] + if !found { + class = pv.Spec.StorageClassName + } + + size := pv.Spec.Capacity[v1.ResourceStorage] + + r.ID = MetaFQN(pv.ObjectMeta) + r.Fields = Fields{ + pv.Name, + size.String(), + accessMode(pv.Spec.AccessModes), + string(pv.Spec.PersistentVolumeReclaimPolicy), + string(phase), + claim, + class, + pv.Status.Reason, + toAge(pv.ObjectMeta.CreationTimestamp), + } + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func accessMode(aa []v1.PersistentVolumeAccessMode) string { + dd := accessDedup(aa) + s := make([]string, 0, len(dd)) + for i := 0; i < len(aa); i++ { + switch { + case accessContains(dd, v1.ReadWriteOnce): + s = append(s, "RWO") + case accessContains(dd, v1.ReadOnlyMany): + s = append(s, "ROX") + case accessContains(dd, v1.ReadWriteMany): + s = append(s, "RWX") + } + } + + return strings.Join(s, ",") +} + +func accessContains(cc []v1.PersistentVolumeAccessMode, a v1.PersistentVolumeAccessMode) bool { + for _, c := range cc { + if c == a { + return true + } + } + + return false +} + +func accessDedup(cc []v1.PersistentVolumeAccessMode) []v1.PersistentVolumeAccessMode { + set := []v1.PersistentVolumeAccessMode{} + for _, c := range cc { + if !accessContains(set, c) { + set = append(set, c) + } + } + + return set +} diff --git a/internal/render/pv_test.go b/internal/render/pv_test.go new file mode 100644 index 00000000..995e5b24 --- /dev/null +++ b/internal/render/pv_test.go @@ -0,0 +1,17 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestPersistentVolumeRender(t *testing.T) { + c := render.PersistentVolume{} + r := render.NewRow(9) + c.Render(load(t, "pv"), "-", &r) + + assert.Equal(t, "pvc-07aa4e2c-8726-11e9-a8e8-42010a80015b", r.ID) + assert.Equal(t, render.Fields{"pvc-07aa4e2c-8726-11e9-a8e8-42010a80015b", "1Gi", "RWO", "Delete", "Bound", "default/www-nginx-sts-1", "standard"}, r.Fields[:7]) +} diff --git a/internal/render/pvc.go b/internal/render/pvc.go new file mode 100644 index 00000000..6cb71af6 --- /dev/null +++ b/internal/render/pvc.go @@ -0,0 +1,103 @@ +package render + +import ( + "fmt" + "strings" + + "github.com/gdamore/tcell" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// PersistentVolumeClaim renders a K8s PersistentVolumeClaim to screen. +type PersistentVolumeClaim struct{} + +// ColorerFunc colors a resource row. +func (PersistentVolumeClaim) ColorerFunc() ColorerFunc { + return func(ns string, r RowEvent) tcell.Color { + c := DefaultColorer(ns, r) + if r.Kind == EventAdd || r.Kind == EventUpdate { + return c + } + + markCol := 2 + if ns != AllNamespaces { + markCol = 1 + } + + if strings.TrimSpace(r.Row.Fields[markCol]) != "Bound" { + c = ErrColor + } + + return c + } + +} + +// Header returns a header rbw. +func (PersistentVolumeClaim) Header(ns string) HeaderRow { + var h HeaderRow + if isAllNamespace(ns) { + h = append(h, Header{Name: "NAMESPACE"}) + } + + return append(h, + Header{Name: "NAME"}, + Header{Name: "STATUS"}, + Header{Name: "VOLUME"}, + Header{Name: "CAPACITY"}, + Header{Name: "ACCESS MODES"}, + Header{Name: "STORAGECLASS"}, + Header{Name: "AGE", Decorator: AgeDecorator}, + ) +} + +// Render renders a K8s resource to screen. +func (p PersistentVolumeClaim) Render(o interface{}, ns string, r *Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("Expected PersistentVolumeClaim, but got %T", o) + } + var pvc v1.PersistentVolumeClaim + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &pvc) + if err != nil { + return err + } + + phase := pvc.Status.Phase + if pvc.ObjectMeta.DeletionTimestamp != nil { + phase = "Terminating" + } + + storage := pvc.Spec.Resources.Requests[v1.ResourceStorage] + var capacity, accessModes string + if pvc.Spec.VolumeName != "" { + accessModes = accessMode(pvc.Status.AccessModes) + storage = pvc.Status.Capacity[v1.ResourceStorage] + capacity = storage.String() + } + class, found := pvc.Annotations[v1.BetaStorageClassAnnotation] + if !found { + if pvc.Spec.StorageClassName != nil { + class = *pvc.Spec.StorageClassName + } + } + + r.ID = MetaFQN(pvc.ObjectMeta) + r.Fields = make(Fields, 0, len(p.Header(ns))) + if isAllNamespace(ns) { + r.Fields = append(r.Fields, pvc.Namespace) + } + r.Fields = append(r.Fields, + pvc.Name, + string(phase), + pvc.Spec.VolumeName, + capacity, + accessModes, + class, + toAge(pvc.ObjectMeta.CreationTimestamp), + ) + + return nil +} diff --git a/internal/render/pvc_test.go b/internal/render/pvc_test.go new file mode 100644 index 00000000..aab7b07a --- /dev/null +++ b/internal/render/pvc_test.go @@ -0,0 +1,17 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestPersistentVolumeClaimRender(t *testing.T) { + c := render.PersistentVolumeClaim{} + r := render.NewRow(8) + c.Render(load(t, "pvc"), "", &r) + + assert.Equal(t, "default/www-nginx-sts-0", r.ID) + assert.Equal(t, render.Fields{"default", "www-nginx-sts-0", "Bound", "pvc-fbabd470-8725-11e9-a8e8-42010a80015b", "1Gi", "RWO", "standard"}, r.Fields[:7]) +} diff --git a/internal/render/rbac.go b/internal/render/rbac.go new file mode 100644 index 00000000..87396131 --- /dev/null +++ b/internal/render/rbac.go @@ -0,0 +1,165 @@ +package render + +import ( + "fmt" + "strings" + + "github.com/gdamore/tcell" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const allVerbs = "*" + +var ( + k8sVerbs = []string{ + "get", + "list", + "watch", + "create", + "patch", + "update", + "delete", + "deletecollection", + } + + httpTok8sVerbs = map[string]string{ + "post": "create", + "put": "update", + } +) + +// Rbac renders a rbac to screen. +type Rbac struct{} + +// ColorerFunc colors a resource row. +func (Rbac) ColorerFunc() ColorerFunc { + return func(ns string, re RowEvent) tcell.Color { + return tcell.ColorMediumSpringGreen + } +} + +// Header returns a header row. +func (Rbac) Header(ns string) HeaderRow { + h := HeaderRow{ + Header{Name: "NAME"}, + Header{Name: "API GROUP"}, + } + + return append(h, rbacVerbHeader()...) +} + +// Render renders a K8s resource to screen. +func (Rbac) Render(o interface{}, gvr string, r *Row) error { + p, ok := o.(PolicyRes) + if !ok { + return fmt.Errorf("expecting RuleRes but got %T", o) + } + + r.ID = p.Resource + r.Fields = append(r.Fields, cleanseResource(p.Resource), p.Group) + r.Fields = append(r.Fields, asVerbs(p.Verbs)...) + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func asVerbs(verbs []string) []string { + const ( + verbLen = 4 + unknownLen = 30 + ) + + r := make([]string, 0, len(k8sVerbs)+1) + for _, v := range k8sVerbs { + r = append(r, toVerbIcon(hasVerb(verbs, v))) + } + + var unknowns []string + for _, v := range verbs { + if hv, ok := httpTok8sVerbs[v]; ok { + v = hv + } + if !hasVerb(k8sVerbs, v) && v != allVerbs { + unknowns = append(unknowns, v) + } + } + + return append(r, Truncate(strings.Join(unknowns, ","), unknownLen)) +} + +func toVerbIcon(ok bool) string { + if ok { + return "[green::b] ✓ [::]" + } + return "[orangered::b] 𐄂 [::]" +} + +func hasVerb(verbs []string, verb string) bool { + if len(verbs) == 1 && verbs[0] == allVerbs { + return true + } + + for _, v := range verbs { + if hv, ok := httpTok8sVerbs[v]; ok { + if hv == verb { + return true + } + } + if v == verb { + return true + } + } + + return false +} + +type RuleRes struct { + Resource, Group string + ResourceName string + NonResourceURL string + Verbs []string +} + +func NewRuleRes(res, grp string, vv []string) RuleRes { + return RuleRes{ + Resource: res, + Group: grp, + Verbs: vv, + } +} + +// GetObjectKind returns a schema object. +func (r RuleRes) GetObjectKind() schema.ObjectKind { + return nil +} + +// DeepCopyObject returns a container copy. +func (r RuleRes) DeepCopyObject() runtime.Object { + return r +} + +type Rules []RuleRes + +func (rr Rules) Upsert(r RuleRes) Rules { + idx, ok := rr.find(r.Resource) + if !ok { + return append(rr, r) + } + rr[idx] = r + + return rr +} + +// Find locates a row by id. Retturns false is not found. +func (rr Rules) find(res string) (int, bool) { + for i, r := range rr { + if r.Resource == res { + return i, true + } + } + + return 0, false +} diff --git a/internal/render/ro.go b/internal/render/ro.go new file mode 100644 index 00000000..5a67eb53 --- /dev/null +++ b/internal/render/ro.go @@ -0,0 +1,55 @@ +package render + +import ( + "fmt" + + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// Role renders a K8s Role to screen. +type Role struct{} + +// ColorerFunc colors a resource row. +func (Role) ColorerFunc() ColorerFunc { + return DefaultColorer +} + +// Header returns a header row. +func (Role) Header(ns string) HeaderRow { + var h HeaderRow + if isAllNamespace(ns) { + h = append(h, Header{Name: "NAMESPACE"}) + } + + return append(h, + Header{Name: "NAME"}, + Header{Name: "AGE", Decorator: AgeDecorator}, + ) +} + +// Render renders a K8s resource to screen. +func (r Role) Render(o interface{}, ns string, row *Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("Expected Role, but got %T", o) + } + var ro rbacv1.Role + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ro) + if err != nil { + return err + } + + row.ID = MetaFQN(ro.ObjectMeta) + row.Fields = make(Fields, 0, len(r.Header(ns))) + if isAllNamespace(ns) { + row.Fields = append(row.Fields, ro.Namespace) + } + row.Fields = append(row.Fields, + ro.Name, + toAge(ro.ObjectMeta.CreationTimestamp), + ) + + return nil +} diff --git a/internal/render/ro_test.go b/internal/render/ro_test.go new file mode 100644 index 00000000..8b39bfc5 --- /dev/null +++ b/internal/render/ro_test.go @@ -0,0 +1,17 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestRoleRender(t *testing.T) { + c := render.Role{} + r := render.NewRow(3) + c.Render(load(t, "ro"), "", &r) + + assert.Equal(t, "default/blee", r.ID) + assert.Equal(t, render.Fields{"default", "blee"}, r.Fields[:2]) +} diff --git a/internal/render/rob.go b/internal/render/rob.go new file mode 100644 index 00000000..e8fb34f6 --- /dev/null +++ b/internal/render/rob.go @@ -0,0 +1,97 @@ +package render + +import ( + "fmt" + "strings" + + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// RoleBinding renders a K8s RoleBinding to screen. +type RoleBinding struct{} + +// ColorerFunc colors a resource row. +func (RoleBinding) ColorerFunc() ColorerFunc { + return DefaultColorer +} + +// Header returns a header rbw. +func (RoleBinding) Header(ns string) HeaderRow { + var h HeaderRow + if isAllNamespace(ns) { + h = append(h, Header{Name: "NAMESPACE"}) + } + + return append(h, + Header{Name: "NAME"}, + Header{Name: "ROLE"}, + Header{Name: "KIND"}, + Header{Name: "SUBJECTS"}, + Header{Name: "AGE", Decorator: AgeDecorator}, + ) +} + +// Render renders a K8s resource to screen. +func (r RoleBinding) Render(o interface{}, ns string, row *Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("Expected RoleBinding, but got %T", o) + } + var rb rbacv1.RoleBinding + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &rb) + if err != nil { + return err + } + + kind, ss := renderSubjects(rb.Subjects) + + row.ID = MetaFQN(rb.ObjectMeta) + row.Fields = make(Fields, 0, len(r.Header(ns))) + if isAllNamespace(ns) { + row.Fields = append(row.Fields, rb.Namespace) + } + row.Fields = append(row.Fields, + rb.Name, + rb.RoleRef.Name, + kind, + ss, + toAge(rb.ObjectMeta.CreationTimestamp), + ) + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func renderSubjects(ss []rbacv1.Subject) (kind string, subjects string) { + if len(ss) == 0 { + return NAValue, "" + } + + var tt []string + for _, s := range ss { + kind = toSubjectAlias(s.Kind) + tt = append(tt, s.Name) + } + return kind, strings.Join(tt, ",") +} + +func toSubjectAlias(s string) string { + if len(s) == 0 { + return s + } + + switch s { + case rbacv1.UserKind: + return "USR" + case rbacv1.GroupKind: + return "GRP" + case rbacv1.ServiceAccountKind: + return "SA" + default: + return strings.ToUpper(s) + } +} diff --git a/internal/render/rob_test.go b/internal/render/rob_test.go new file mode 100644 index 00000000..66afcf6a --- /dev/null +++ b/internal/render/rob_test.go @@ -0,0 +1,17 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestRoleBindingRender(t *testing.T) { + c := render.RoleBinding{} + r := render.NewRow(6) + c.Render(load(t, "rb"), "", &r) + + assert.Equal(t, "default/blee", r.ID) + assert.Equal(t, render.Fields{"default", "blee", "blee", "SA", "fernand"}, r.Fields[:5]) +} diff --git a/internal/render/row.go b/internal/render/row.go new file mode 100644 index 00000000..d9d72ada --- /dev/null +++ b/internal/render/row.go @@ -0,0 +1,147 @@ +package render + +import ( + "sort" + "time" + + "vbom.ml/util/sortorder" +) + +// Fields represents a collection of row fields. +type Fields []string + +// Clone returns a copy of the fields. +func (f Fields) Clone() Fields { + cp := make(Fields, len(f)) + for i, v := range f { + cp[i] = v + } + return cp +} + +// ---------------------------------------------------------------------------- + +// Row represents a colllection of columns. +type Row struct { + ID string + Fields Fields +} + +// NewRow returns a new row with initialized fields. +func NewRow(cols int) Row { + return Row{Fields: make([]string, cols)} +} + +// Clone copies a row. +func (r Row) Clone() Row { + return Row{ + ID: r.ID, + Fields: r.Fields.Clone(), + } +} + +// ---------------------------------------------------------------------------- + +// Rows represents a collection of rows. +type Rows []Row + +// Delete removes an element by id. +func (rr Rows) Delete(id string) Rows { + idx, ok := rr.Find(id) + if !ok { + return rr + } + + if idx == 0 { + return rr[1:] + } + if idx+1 == len(rr) { + return rr[:len(rr)-1] + } + + return append(rr[:idx], rr[idx+1:]...) +} + +func (rr Rows) Upsert(r Row) Rows { + idx, ok := rr.Find(r.ID) + if !ok { + return append(rr, r) + } + rr[idx] = r + + return rr +} + +// Find locates a row by id. Retturns false is not found. +func (rr Rows) Find(id string) (int, bool) { + for i, r := range rr { + if r.ID == id { + return i, true + } + } + + return 0, false +} + +// Sort rows based on column index and order. +func (rr Rows) Sort(col int, asc bool) { + t := RowSorter{Rows: rr, Index: col, Asc: asc} + sort.Sort(t) +} + +// ---------------------------------------------------------------------------- + +// RowSorter sorts rows. +type RowSorter struct { + Rows Rows + Index int + Asc bool +} + +func (s RowSorter) Len() int { + return len(s.Rows) +} + +func (s RowSorter) Swap(i, j int) { + s.Rows[i], s.Rows[j] = s.Rows[j], s.Rows[i] +} + +func (s RowSorter) Less(i, j int) bool { + return Less(s.Asc, s.Rows[i].Fields[s.Index], s.Rows[j].Fields[s.Index]) +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func Less(asc bool, c1, c2 string) bool { + if o, ok := isDurationSort(asc, c1, c2); ok { + return o + } + + b := sortorder.NaturalLess(c1, c2) + if asc { + return b + } + return !b +} + +func isDurationSort(asc bool, s1, s2 string) (bool, bool) { + d1, ok1 := isDuration(s1) + d2, ok2 := isDuration(s2) + if !ok1 || !ok2 { + return false, false + } + + if asc { + return d1 <= d2, true + } + return d1 >= d2, true +} + +func isDuration(s string) (time.Duration, bool) { + d, err := time.ParseDuration(s) + if err != nil { + return d, false + } + return d, true +} diff --git a/internal/render/row_event.go b/internal/render/row_event.go new file mode 100644 index 00000000..ecd75dc3 --- /dev/null +++ b/internal/render/row_event.go @@ -0,0 +1,252 @@ +package render + +import ( + "fmt" + "reflect" + "sort" + + "github.com/rs/zerolog/log" +) + +const ( + // EventUnchanged notifies listener resource has not changed. + EventUnchanged ResEvent = 1 << iota + + // EventAdd notifies listener of a resource was added. + EventAdd + + // EventUpdate notifies listener of a resource updated. + EventUpdate + + // EventDelete notifies listener of a resource was deleted. + EventDelete + + // EventClear the stack was reset. + EventClear +) + +// ResEvent represents a resource event. +type ResEvent int + +// RowEvent tracks resource instance events. +type RowEvent struct { + Kind ResEvent + Row Row + Deltas DeltaRow +} + +// NewRowEvent returns a new row event. +func NewRowEvent(kind ResEvent, row Row) RowEvent { + return RowEvent{ + Kind: kind, + Row: row, + } +} + +// NewDeltaRowEvent returns a new row event with deltas. +func NewDeltaRowEvent(row Row, delta DeltaRow) RowEvent { + return RowEvent{ + Kind: EventUpdate, + Row: row, + Deltas: delta, + } +} + +// Clone returns a rowevent deep copy. +func (r RowEvent) Clone() RowEvent { + return RowEvent{ + Kind: r.Kind, + Row: r.Row.Clone(), + Deltas: r.Deltas.Clone(), + } +} + +func (r RowEvent) Changed(re RowEvent) bool { + if r.Kind != re.Kind { + log.Debug().Msgf("KIND Changed") + return true + } + if !reflect.DeepEqual(r.Deltas, re.Deltas) { + log.Debug().Msgf("DELTAS CHANGED") + return true + } + + return !reflect.DeepEqual(r.Row.Fields[:len(r.Row.Fields)-1], re.Row.Fields[:len(re.Row.Fields)-1]) +} + +// ---------------------------------------------------------------------------- + +// RowEvents a collection of row events. +type RowEvents []RowEvent + +// Changed returns true if the header changed. +func (rr RowEvents) Changed(r RowEvents) bool { + if len(rr) != len(r) { + return true + } + + for i := range rr { + if rr[i].Changed(r[i]) { + return true + } + } + + return false +} + +// Clone returns a rowevents deep copy. +func (rr RowEvents) Clone() RowEvents { + res := make(RowEvents, len(rr)) + for i, r := range rr { + res[i] = r.Clone() + } + + return res +} + +// Upsert add or update a row if it exists. +func (rr RowEvents) Upsert(e RowEvent) RowEvents { + if idx, ok := rr.FindIndex(e.Row.ID); ok { + rr[idx] = e + } else { + rr = append(rr, e) + } + return rr +} + +// Delete removes an element by id. +func (rr RowEvents) Delete(id string) RowEvents { + idx, ok := rr.FindIndex(id) + if !ok { + return rr + } + + if idx == 0 { + return rr[1:] + } + if idx == len(rr)-1 { + return rr[:len(rr)-1] + } + + return append(rr[:idx], rr[idx+1:]...) +} + +// Clear delete all row events +func (rr RowEvents) Clear() RowEvents { + return RowEvents{} +} + +// FindIndex locates a row index by id. Returns false is not found. +func (rr RowEvents) FindIndex(id string) (int, bool) { + for i, e := range rr { + if e.Row.ID == id { + return i, true + } + } + + return 0, false +} + +// Sort rows based on column index and order. +func (rr RowEvents) Sort(ns string, col int, asc bool) { + t := RowEventSorter{NS: ns, Events: rr, Index: col, Asc: asc} + sort.Sort(t) + + gg, kk := map[string][]string{}, make(StringSet, 0, len(rr)) + for _, e := range rr { + g := e.Row.Fields[col] + kk = kk.Add(g) + if ss, ok := gg[g]; ok { + gg[g] = append(ss, e.Row.ID) + } else { + gg[g] = []string{e.Row.ID} + } + } + + ids := make([]string, 0, len(rr)) + for _, k := range kk { + sort.StringSlice(gg[k]).Sort() + ids = append(ids, gg[k]...) + } + s := IdSorter{Ids: ids, Events: rr} + sort.Sort(s) +} + +// ---------------------------------------------------------------------------- + +// RowEventSorter sorts row events by a given colon. +type RowEventSorter struct { + Events RowEvents + Index int + NS string + Asc bool +} + +func (r RowEventSorter) Len() int { + return len(r.Events) +} + +func (r RowEventSorter) Swap(i, j int) { + r.Events[i], r.Events[j] = r.Events[j], r.Events[i] +} + +func (r RowEventSorter) Less(i, j int) bool { + f1, f2 := r.Events[i].Row.Fields, r.Events[j].Row.Fields + return Less(r.Asc, f1[r.Index], f2[r.Index]) +} + +// ---------------------------------------------------------------------------- + +// IdSorter sorts row events by a given id. +type IdSorter struct { + Ids []string + Events RowEvents +} + +func (s IdSorter) Len() int { + return len(s.Events) +} + +func (s IdSorter) Swap(i, j int) { + s.Events[i], s.Events[j] = s.Events[j], s.Events[i] +} + +func (s IdSorter) Less(i, j int) bool { + id1, id2 := s.Events[i].Row.ID, s.Events[j].Row.ID + i1, i2 := findIndex(s.Ids, id1), findIndex(s.Ids, id2) + return i1 < i2 +} + +func findIndex(ss []string, s string) int { + for i := range ss { + if ss[i] == s { + return i + } + } + log.Error().Err(fmt.Errorf("Doh! index not found for %s", s)) + return -1 +} + +// ---------------------------------------------------------------------------- + +type StringSet []string + +func (ss StringSet) Add(item string) StringSet { + if ss.In(item) { + return ss + } + return append(ss, item) +} + +func (ss StringSet) In(item string) bool { + return ss.indexOf(item) >= 0 +} + +func (ss StringSet) indexOf(item string) int { + for i, s := range ss { + if s == item { + return i + } + } + return -1 +} diff --git a/internal/render/row_event_test.go b/internal/render/row_event_test.go new file mode 100644 index 00000000..a0d4b31d --- /dev/null +++ b/internal/render/row_event_test.go @@ -0,0 +1,60 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/gdamore/tcell" + "github.com/stretchr/testify/assert" +) + +func TestSort(t *testing.T) { + uu := map[string]struct { + re render.RowEvents + col int + asc bool + e render.RowEvents + }{ + "col0": { + re: render.RowEvents{ + {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, + {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, + {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, + }, + col: 0, + asc: true, + e: render.RowEvents{ + {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, + {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, + {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, + }, + }, + } + + for k := range uu { + uc := uu[k] + t.Run(k, func(t *testing.T) { + uc.re.Sort("", uc.col, uc.asc) + assert.Equal(t, uc.e, uc.re) + }) + } +} + +func TestDefaultColorer(t *testing.T) { + uu := map[string]struct { + k render.ResEvent + e tcell.Color + }{ + "add": {render.EventAdd, render.AddColor}, + "update": {render.EventUpdate, render.ModColor}, + "delete": {render.EventDelete, render.KillColor}, + "std": {100, render.StdColor}, + } + + for k := range uu { + uc := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, uc.e, render.DefaultColorer("", render.RowEvent{})) + }) + } +} diff --git a/internal/render/row_header.go b/internal/render/row_header.go new file mode 100644 index 00000000..1562d048 --- /dev/null +++ b/internal/render/row_header.go @@ -0,0 +1,73 @@ +package render + +import "reflect" + +const ageCol = "AGE" + +// Header represent a table header +type Header struct { + Name string + Align int + Decorator DecoratorFunc +} + +// Clone copies a header. +func (h Header) Clone() Header { + return h +} + +// ---------------------------------------------------------------------------- + +// HeaderRow represents a table header. +type HeaderRow []Header + +func (hh HeaderRow) Clone() HeaderRow { + h := make(HeaderRow, len(hh)) + for i, v := range hh { + h[i] = v.Clone() + } + + return h +} + +// Clear clears out the header row. +func (hh HeaderRow) Clear() HeaderRow { + return HeaderRow{} +} + +// Changed returns true if the header changed. +func (hh HeaderRow) Changed(h HeaderRow) bool { + if len(hh) != len(h) { + return true + } + return !reflect.DeepEqual(hh.Columns(), h.Columns()) +} + +// Columns return header as a collection of strings. +func (h HeaderRow) Columns() []string { + cc := make([]string, len(h)) + for i, c := range h { + cc[i] = c.Name + } + + return cc +} + +// HasAge returns true if table has an age column. +func (h HeaderRow) HasAge() bool { + for _, r := range h { + if r.Name == ageCol { + return true + } + } + + return false +} + +// AgeCol checks if given column index is the age column. +func (h HeaderRow) AgeCol(col int) bool { + if !h.HasAge() { + return false + } + return col == len(h)-1 +} diff --git a/internal/render/row_test.go b/internal/render/row_test.go new file mode 100644 index 00000000..24b3b0bf --- /dev/null +++ b/internal/render/row_test.go @@ -0,0 +1,239 @@ +package render_test + +import ( + "fmt" + "reflect" + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestFieldClone(t *testing.T) { + f := render.Fields{"a", "b", "c"} + f1 := f.Clone() + + assert.True(t, reflect.DeepEqual(f, f1)) + assert.NotEqual(t, fmt.Sprintf("%p", f), fmt.Sprintf("%p", f1)) +} + +func TestRowsDelete(t *testing.T) { + uu := map[string]struct { + rows render.Rows + id string + e render.Rows + }{ + "first": { + rows: render.Rows{ + {ID: "a", Fields: []string{"blee", "duh"}}, + {ID: "b", Fields: []string{"albert", "blee"}}, + }, + id: "a", + e: render.Rows{ + {ID: "b", Fields: []string{"albert", "blee"}}, + }, + }, + "last": { + rows: render.Rows{ + {ID: "a", Fields: []string{"blee", "duh"}}, + {ID: "b", Fields: []string{"albert", "blee"}}, + }, + id: "b", + e: render.Rows{ + {ID: "a", Fields: []string{"blee", "duh"}}, + }, + }, + "middle": { + rows: render.Rows{ + {ID: "a", Fields: []string{"blee", "duh"}}, + {ID: "b", Fields: []string{"albert", "blee"}}, + {ID: "c", Fields: []string{"fred", "zorg"}}, + }, + id: "b", + e: render.Rows{ + {ID: "a", Fields: []string{"blee", "duh"}}, + {ID: "c", Fields: []string{"fred", "zorg"}}, + }, + }, + "missing": { + rows: render.Rows{ + {ID: "a", Fields: []string{"blee", "duh"}}, + {ID: "b", Fields: []string{"albert", "blee"}}, + }, + id: "zorg", + e: render.Rows{ + {ID: "a", Fields: []string{"blee", "duh"}}, + {ID: "b", Fields: []string{"albert", "blee"}}, + }, + }, + } + + for k := range uu { + uc := uu[k] + t.Run(k, func(t *testing.T) { + rows := uc.rows.Delete(uc.id) + assert.Equal(t, uc.e, rows) + }) + } +} + +func TestRowsSortText(t *testing.T) { + uu := map[string]struct { + rows render.Rows + col int + asc bool + e render.Rows + }{ + "plainAsc": { + rows: render.Rows{ + {Fields: []string{"blee", "duh"}}, + {Fields: []string{"albert", "blee"}}, + }, + col: 0, + asc: true, + e: render.Rows{ + {Fields: []string{"albert", "blee"}}, + {Fields: []string{"blee", "duh"}}, + }, + }, + "plainDesc": { + rows: render.Rows{ + {Fields: []string{"blee", "duh"}}, + {Fields: []string{"albert", "blee"}}, + }, + col: 0, + asc: false, + e: render.Rows{ + {Fields: []string{"blee", "duh"}}, + {Fields: []string{"albert", "blee"}}, + }, + }, + "numericAsc": { + rows: render.Rows{ + {Fields: []string{"10", "duh"}}, + {Fields: []string{"1", "blee"}}, + }, + col: 0, + asc: true, + e: render.Rows{ + {Fields: []string{"1", "blee"}}, + {Fields: []string{"10", "duh"}}, + }, + }, + "numericDesc": { + rows: render.Rows{ + {Fields: []string{"10", "duh"}}, + {Fields: []string{"1", "blee"}}, + }, + col: 0, + asc: false, + e: render.Rows{ + {Fields: []string{"10", "duh"}}, + {Fields: []string{"1", "blee"}}, + }, + }, + "composite": { + rows: render.Rows{ + {Fields: []string{"blee-duh", "duh"}}, + {Fields: []string{"blee", "blee"}}, + }, + col: 0, + asc: true, + e: render.Rows{ + {Fields: []string{"blee", "blee"}}, + {Fields: []string{"blee-duh", "duh"}}, + }, + }, + } + + for k := range uu { + uc := uu[k] + t.Run(k, func(t *testing.T) { + uc.rows.Sort(uc.col, uc.asc) + assert.Equal(t, uc.e, uc.rows) + }) + } +} + +func TestRowsSortDuration(t *testing.T) { + uu := map[string]struct { + rows render.Rows + col int + asc bool + e render.Rows + }{ + "durationAsc": { + rows: render.Rows{ + {Fields: []string{"10m10s", "duh"}}, + {Fields: []string{"19s", "blee"}}, + }, + col: 0, + asc: true, + e: render.Rows{ + {Fields: []string{"19s", "blee"}}, + {Fields: []string{"10m10s", "duh"}}, + }, + }, + "durationDesc": { + rows: render.Rows{ + {Fields: []string{"10m10s", "duh"}}, + {Fields: []string{"19s", "blee"}}, + }, + col: 0, + e: render.Rows{ + {Fields: []string{"10m10s", "duh"}}, + {Fields: []string{"19s", "blee"}}, + }, + }, + } + + for k := range uu { + uc := uu[k] + t.Run(k, func(t *testing.T) { + uc.rows.Sort(uc.col, uc.asc) + assert.Equal(t, uc.e, uc.rows) + }) + } +} + +func TestRowsSortMetrics(t *testing.T) { + uu := map[string]struct { + rows render.Rows + col int + asc bool + e render.Rows + }{ + "metricAsc": { + rows: render.Rows{ + {Fields: []string{"10m", "duh"}}, + {Fields: []string{"1m", "blee"}}, + }, + col: 0, + asc: true, + e: render.Rows{ + {Fields: []string{"1m", "blee"}}, + {Fields: []string{"10m", "duh"}}, + }, + }, + "metricDesc": { + rows: render.Rows{ + {Fields: []string{"10m", "100Mi"}}, + {Fields: []string{"1m", "50Mi"}}, + }, + col: 1, + asc: false, + e: render.Rows{ + {Fields: []string{"10m", "100Mi"}}, + {Fields: []string{"1m", "50Mi"}}, + }, + }, + } + + for k := range uu { + uc := uu[k] + t.Run(k, func(t *testing.T) { + uc.rows.Sort(uc.col, uc.asc) + assert.Equal(t, uc.e, uc.rows) + }) + } +} diff --git a/internal/render/rs.go b/internal/render/rs.go new file mode 100644 index 00000000..880d6b12 --- /dev/null +++ b/internal/render/rs.go @@ -0,0 +1,81 @@ +package render + +import ( + "fmt" + "strconv" + "strings" + + "github.com/derailed/tview" + "github.com/gdamore/tcell" + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// ReplicaSet renders a K8s ReplicaSet to screen. +type ReplicaSet struct{} + +// ColorerFunc colors a resource row. +func (ReplicaSet) ColorerFunc() ColorerFunc { + return func(ns string, r RowEvent) tcell.Color { + c := DefaultColorer(ns, r) + if r.Kind == EventAdd || r.Kind == EventUpdate { + return c + } + + markCol := 2 + if ns != AllNamespaces { + markCol = 1 + } + if strings.TrimSpace(r.Row.Fields[markCol]) != strings.TrimSpace(r.Row.Fields[markCol+1]) { + return ErrColor + } + + return StdColor + } + +} + +// Header returns a header row. +func (ReplicaSet) Header(ns string) HeaderRow { + var h HeaderRow + if isAllNamespace(ns) { + h = append(h, Header{Name: "NAMESPACE"}) + } + + return append(h, + Header{Name: "NAME"}, + Header{Name: "DESIRED", Align: tview.AlignRight}, + Header{Name: "CURRENT", Align: tview.AlignRight}, + Header{Name: "READY", Align: tview.AlignRight}, + Header{Name: "AGE", Decorator: AgeDecorator}, + ) +} + +// Render renders a K8s resource to screen. +func (s ReplicaSet) Render(o interface{}, ns string, r *Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("Expected ReplicaSet, but got %T", o) + } + var rs appsv1.ReplicaSet + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &rs) + if err != nil { + return err + } + + r.ID = MetaFQN(rs.ObjectMeta) + r.Fields = make(Fields, 0, len(s.Header(ns))) + if isAllNamespace(ns) { + r.Fields = append(r.Fields, rs.Namespace) + } + r.Fields = append(r.Fields, + rs.Name, + strconv.Itoa(int(*rs.Spec.Replicas)), + strconv.Itoa(int(rs.Status.Replicas)), + strconv.Itoa(int(rs.Status.ReadyReplicas)), + toAge(rs.ObjectMeta.CreationTimestamp), + ) + + return nil +} diff --git a/internal/render/rs_test.go b/internal/render/rs_test.go new file mode 100644 index 00000000..0437f816 --- /dev/null +++ b/internal/render/rs_test.go @@ -0,0 +1,17 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestReplicaSetRender(t *testing.T) { + c := render.ReplicaSet{} + r := render.NewRow(4) + 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]) +} diff --git a/internal/render/sa.go b/internal/render/sa.go new file mode 100644 index 00000000..760a77a9 --- /dev/null +++ b/internal/render/sa.go @@ -0,0 +1,58 @@ +package render + +import ( + "fmt" + "strconv" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// ServiceAccount renders a K8s ServiceAccount to screen. +type ServiceAccount struct{} + +// ColorerFunc colors a resource row. +func (ServiceAccount) ColorerFunc() ColorerFunc { + return DefaultColorer +} + +// Header returns a header row. +func (ServiceAccount) Header(ns string) HeaderRow { + var h HeaderRow + if isAllNamespace(ns) { + h = append(h, Header{Name: "NAMESPACE"}) + } + + return append(h, + Header{Name: "NAME"}, + Header{Name: "SECRET"}, + Header{Name: "AGE", Decorator: AgeDecorator}, + ) +} + +// Render renders a K8s resource to screen. +func (s ServiceAccount) Render(o interface{}, ns string, r *Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("Expected ServiceAccount, but got %T", o) + } + var sa v1.ServiceAccount + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sa) + if err != nil { + return err + } + + r.ID = MetaFQN(sa.ObjectMeta) + r.Fields = make(Fields, 0, len(s.Header(ns))) + if isAllNamespace(ns) { + r.Fields = append(r.Fields, sa.Namespace) + } + r.Fields = append(r.Fields, + sa.Name, + strconv.Itoa(len(sa.Secrets)), + toAge(sa.ObjectMeta.CreationTimestamp), + ) + + return nil +} diff --git a/internal/render/sa_test.go b/internal/render/sa_test.go new file mode 100644 index 00000000..74c40c18 --- /dev/null +++ b/internal/render/sa_test.go @@ -0,0 +1,17 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestServiceAccountRender(t *testing.T) { + c := render.ServiceAccount{} + r := render.NewRow(4) + c.Render(load(t, "sa"), "", &r) + + assert.Equal(t, "default/blee", r.ID) + assert.Equal(t, render.Fields{"default", "blee", "2"}, r.Fields[:3]) +} diff --git a/internal/render/sc.go b/internal/render/sc.go new file mode 100644 index 00000000..a626a918 --- /dev/null +++ b/internal/render/sc.go @@ -0,0 +1,48 @@ +package render + +import ( + "fmt" + + storagev1 "k8s.io/api/storage/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// StorageClass renders a K8s StorageClass to screen. +type StorageClass struct{} + +// ColorerFunc colors a resource row. +func (StorageClass) ColorerFunc() ColorerFunc { + return DefaultColorer +} + +// Header returns a header row. +func (StorageClass) Header(ns string) HeaderRow { + return HeaderRow{ + Header{Name: "NAME"}, + Header{Name: "PROVISIONER"}, + Header{Name: "AGE", Decorator: AgeDecorator}, + } +} + +// Render renders a K8s resource to screen. +func (StorageClass) Render(o interface{}, ns string, r *Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("Expected StorageClass, but got %T", o) + } + var sc storagev1.StorageClass + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sc) + if err != nil { + return err + } + + r.ID = FQN(ClusterScope, sc.ObjectMeta.Name) + r.Fields = Fields{ + sc.Name, + string(sc.Provisioner), + toAge(sc.ObjectMeta.CreationTimestamp), + } + + return nil +} diff --git a/internal/render/sc_test.go b/internal/render/sc_test.go new file mode 100644 index 00000000..70096e8d --- /dev/null +++ b/internal/render/sc_test.go @@ -0,0 +1,17 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestStorageClassRender(t *testing.T) { + c := render.StorageClass{} + r := render.NewRow(4) + c.Render(load(t, "sc"), "", &r) + + assert.Equal(t, "-/standard", r.ID) + assert.Equal(t, render.Fields{"standard", "kubernetes.io/gce-pd"}, r.Fields[:2]) +} diff --git a/internal/render/screen_dump.go b/internal/render/screen_dump.go new file mode 100644 index 00000000..dd3e643b --- /dev/null +++ b/internal/render/screen_dump.go @@ -0,0 +1,76 @@ +package render + +import ( + "fmt" + "os" + "path/filepath" + "time" + + "github.com/gdamore/tcell" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// ScreenDump renders a screendumps to screen. +type ScreenDump struct{} + +// ColorerFunc colors a resource row. +func (ScreenDump) ColorerFunc() ColorerFunc { + return func(ns string, re RowEvent) tcell.Color { + return tcell.ColorNavajoWhite + } +} + +type DecoratorFunc func(string) string + +// AgeDecorator represents a timestamped as human column. +var AgeDecorator = func(a string) string { + return toAgeHuman(a) +} + +// Header returns a header row. +func (ScreenDump) Header(ns string) HeaderRow { + return HeaderRow{ + Header{Name: "NAME"}, + Header{Name: "AGE", Decorator: AgeDecorator}, + } +} + +// Render renders a K8s resource to screen. +func (b ScreenDump) Render(o interface{}, ns string, r *Row) error { + f, ok := o.(FileRes) + if !ok { + return fmt.Errorf("expecting screendumper, but got %T", o) + } + + r.ID = filepath.Join(f.Dir, f.File.Name()) + r.Fields = Fields{ + f.File.Name(), + timeToAge(f.File.ModTime()), + } + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func timeToAge(timestamp time.Time) string { + return time.Since(timestamp).String() +} + +// FileRes represents a file resource. +type FileRes struct { + File os.FileInfo + Dir string +} + +// GetObjectKind returns a schema object. +func (c FileRes) GetObjectKind() schema.ObjectKind { + return nil +} + +// DeepCopyObject returns a container copy. +func (c FileRes) DeepCopyObject() runtime.Object { + return c +} diff --git a/internal/render/screen_dump_test.go b/internal/render/screen_dump_test.go new file mode 100644 index 00000000..ce6413ab --- /dev/null +++ b/internal/render/screen_dump_test.go @@ -0,0 +1,38 @@ +package render_test + +import ( + "os" + "testing" + "time" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestScreenDumpRender(t *testing.T) { + var s render.ScreenDump + var r render.Row + o := render.FileRes{ + File: fileInfo{}, + Dir: "fred/blee", + } + + assert.Nil(t, s.Render(o, "fred", &r)) + assert.Equal(t, "fred/blee/bob", r.ID) + assert.Equal(t, render.Fields{ + "bob", + }, r.Fields[:len(r.Fields)-1]) +} + +// Helpers... + +type fileInfo struct{} + +var _ os.FileInfo = fileInfo{} + +func (f fileInfo) Name() string { return "bob" } +func (f fileInfo) Size() int64 { return 100 } +func (f fileInfo) Mode() os.FileMode { return os.FileMode(644) } +func (f fileInfo) ModTime() time.Time { return testTime() } +func (f fileInfo) IsDir() bool { return false } +func (f fileInfo) Sys() interface{} { return nil } diff --git a/internal/render/secret.go b/internal/render/secret.go new file mode 100644 index 00000000..0280d0ba --- /dev/null +++ b/internal/render/secret.go @@ -0,0 +1,61 @@ +package render + +import ( + "fmt" + "strconv" + + "github.com/derailed/tview" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// Secret renders a K8s Secret to screen. +type Secret struct{} + +// ColorerFunc colors a resource row. +func (Secret) ColorerFunc() ColorerFunc { + return DefaultColorer +} + +// Header returns a header row. +func (Secret) Header(ns string) HeaderRow { + var h HeaderRow + if isAllNamespace(ns) { + h = append(h, Header{Name: "NAMESPACE"}) + } + + return append(h, + Header{Name: "NAME"}, + Header{Name: "TYPE"}, + Header{Name: "DATA", Align: tview.AlignRight}, + Header{Name: "AGE", Decorator: AgeDecorator}, + ) +} + +// Render renders a K8s resource to screen. +func (s Secret) Render(o interface{}, ns string, r *Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("Expected Secret, but got %T", o) + } + var sec v1.Secret + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sec) + if err != nil { + return err + } + + r.ID = MetaFQN(sec.ObjectMeta) + r.Fields = make(Fields, 0, len(s.Header(ns))) + if isAllNamespace(ns) { + r.Fields = append(r.Fields, sec.Namespace) + } + r.Fields = append(r.Fields, + sec.Name, + string(sec.Type), + strconv.Itoa(len(sec.Data)), + toAge(sec.ObjectMeta.CreationTimestamp), + ) + + return nil +} diff --git a/internal/render/secret_test.go b/internal/render/secret_test.go new file mode 100644 index 00000000..e9ea35f8 --- /dev/null +++ b/internal/render/secret_test.go @@ -0,0 +1,17 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestSecRender(t *testing.T) { + c := render.Secret{} + r := render.NewRow(4) + + c.Render(load(t, "sec"), "", &r) + assert.Equal(t, "default/s1", r.ID) + assert.Equal(t, render.Fields{"default", "s1", "Opaque", "2"}, r.Fields[:4]) +} diff --git a/internal/render/sts.go b/internal/render/sts.go new file mode 100644 index 00000000..b68a1503 --- /dev/null +++ b/internal/render/sts.go @@ -0,0 +1,81 @@ +package render + +import ( + "fmt" + "strconv" + "strings" + + "github.com/gdamore/tcell" + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// StatefulSet renders a K8s StatefulSet to screen. +type StatefulSet struct{} + +// ColorerFunc colors a resource row. +func (StatefulSet) ColorerFunc() ColorerFunc { + return func(ns string, r RowEvent) tcell.Color { + c := DefaultColorer(ns, r) + if r.Kind == EventAdd || r.Kind == EventUpdate { + return c + } + + readyCol := 2 + if ns != AllNamespaces { + readyCol-- + } + tokens := strings.Split(strings.TrimSpace(r.Row.Fields[readyCol]), "/") + curr, des := tokens[0], tokens[1] + if curr != des { + return ErrColor + } + + return StdColor + } +} + +// Header returns a header row. +func (StatefulSet) Header(ns string) HeaderRow { + var h HeaderRow + if isAllNamespace(ns) { + h = append(h, Header{Name: "NAMESPACE"}) + } + + return append(h, + Header{Name: "NAME"}, + Header{Name: "READY"}, + Header{Name: "SELECTOR"}, + Header{Name: "SERVICE"}, + Header{Name: "AGE", Decorator: AgeDecorator}, + ) +} + +// Render renders a K8s resource to screen. +func (s StatefulSet) Render(o interface{}, ns string, r *Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("Expected StatefulSet, but got %T", o) + } + var sts appsv1.StatefulSet + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sts) + if err != nil { + return err + } + + r.ID = MetaFQN(sts.ObjectMeta) + r.Fields = make(Fields, 0, len(s.Header(ns))) + if isAllNamespace(ns) { + r.Fields = append(r.Fields, sts.Namespace) + } + r.Fields = append(r.Fields, + sts.Name, + strconv.Itoa(int(sts.Status.Replicas))+"/"+strconv.Itoa(int(*sts.Spec.Replicas)), + asSelector(sts.Spec.Selector), + na(sts.Spec.ServiceName), + toAge(sts.ObjectMeta.CreationTimestamp), + ) + + return nil +} diff --git a/internal/render/sts_test.go b/internal/render/sts_test.go new file mode 100644 index 00000000..6fe8e4ae --- /dev/null +++ b/internal/render/sts_test.go @@ -0,0 +1,17 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestStatefulSetRender(t *testing.T) { + c := render.StatefulSet{} + r := render.NewRow(4) + + 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"}, r.Fields[:len(r.Fields)-1]) +} diff --git a/internal/render/subject.go b/internal/render/subject.go new file mode 100644 index 00000000..505b42df --- /dev/null +++ b/internal/render/subject.go @@ -0,0 +1,63 @@ +package render + +import ( + "fmt" + + "github.com/gdamore/tcell" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// Subject renders a rbac to screen. +type Subject struct{} + +// ColorerFunc colors a resource row. +func (Subject) ColorerFunc() ColorerFunc { + return func(ns string, re RowEvent) tcell.Color { + return tcell.ColorMediumSpringGreen + } +} + +// Header returns a header row. +func (Subject) Header(ns string) HeaderRow { + return HeaderRow{ + Header{Name: "NAME"}, + Header{Name: "KIND"}, + Header{Name: "FIRST LOCATION"}, + } +} + +// Render renders a K8s resource to screen. +func (s Subject) Render(o interface{}, ns string, r *Row) error { + res, ok := o.(SubjectRef) + if !ok { + return fmt.Errorf("Expected SubjectRef, but got %T", s) + } + + r.ID = res.Name + r.Fields = make(Fields, 0, len(s.Header(ns))) + r.Fields = append(r.Fields, + res.Name, + res.Kind, + res.FirstLocation, + ) + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +type SubjectRef struct { + Name, Kind, FirstLocation string +} + +// GetObjectKind returns a schema object. +func (SubjectRef) GetObjectKind() schema.ObjectKind { + return nil +} + +// DeepCopyObject returns a container copy. +func (s SubjectRef) DeepCopyObject() runtime.Object { + return s +} diff --git a/internal/render/svc.go b/internal/render/svc.go new file mode 100644 index 00000000..e41935a5 --- /dev/null +++ b/internal/render/svc.go @@ -0,0 +1,140 @@ +package render + +import ( + "fmt" + "sort" + "strconv" + "strings" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// Service renders a K8s Service to screen. +type Service struct{} + +// ColorerFunc colors a resource row. +func (Service) ColorerFunc() ColorerFunc { + return DefaultColorer +} + +// Header returns a header row. +func (Service) Header(ns string) HeaderRow { + var h HeaderRow + if isAllNamespace(ns) { + h = append(h, Header{Name: "NAMESPACE"}) + } + + return append(h, + Header{Name: "NAME"}, + Header{Name: "TYPE"}, + Header{Name: "CLUSTER-IP"}, + Header{Name: "EXTERNAL-IP"}, + Header{Name: "SELECTOR"}, + Header{Name: "PORTS"}, + Header{Name: "AGE", Decorator: AgeDecorator}, + ) +} + +// Render renders a K8s resource to screen. +func (s Service) Render(o interface{}, ns string, r *Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("Expected Service, but got %T", o) + } + var svc v1.Service + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &svc) + if err != nil { + return err + } + + r.ID = MetaFQN(svc.ObjectMeta) + r.Fields = make(Fields, 0, len(s.Header(ns))) + if isAllNamespace(ns) { + r.Fields = append(r.Fields, svc.Namespace) + } + r.Fields = append(r.Fields, + svc.ObjectMeta.Name, + string(svc.Spec.Type), + svc.Spec.ClusterIP, + toIPs(svc.Spec.Type, getSvcExtIPS(&svc)), + mapToStr(svc.Spec.Selector), + toPorts(svc.Spec.Ports), + toAge(svc.ObjectMeta.CreationTimestamp), + ) + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func getSvcExtIPS(svc *v1.Service) []string { + results := []string{} + + switch svc.Spec.Type { + case v1.ServiceTypeClusterIP: + fallthrough + case v1.ServiceTypeNodePort: + return svc.Spec.ExternalIPs + case v1.ServiceTypeLoadBalancer: + lbIps := lbIngressIP(svc.Status.LoadBalancer) + if len(svc.Spec.ExternalIPs) > 0 { + if len(lbIps) > 0 { + results = append(results, lbIps) + } + return append(results, svc.Spec.ExternalIPs...) + } + if len(lbIps) > 0 { + results = append(results, lbIps) + } + case v1.ServiceTypeExternalName: + results = append(results, svc.Spec.ExternalName) + } + + return results +} + +func lbIngressIP(s v1.LoadBalancerStatus) string { + ingress := s.Ingress + result := []string{} + for i := range ingress { + if len(ingress[i].IP) > 0 { + result = append(result, ingress[i].IP) + } else if len(ingress[i].Hostname) > 0 { + result = append(result, ingress[i].Hostname) + } + } + + return strings.Join(result, ",") +} + +func toIPs(svcType v1.ServiceType, ips []string) string { + if len(ips) == 0 { + if svcType == v1.ServiceTypeLoadBalancer { + return "" + } + return MissingValue + } + sort.Strings(ips) + + return strings.Join(ips, ",") +} + +func toPorts(pp []v1.ServicePort) string { + ports := make([]string, len(pp)) + for i, p := range pp { + if len(p.Name) > 0 { + ports[i] = p.Name + ":" + } + ports[i] += strconv.Itoa(int(p.Port)) + + "►" + + strconv.Itoa(int(p.NodePort)) + if p.Protocol != "TCP" { + ports[i] += "╱" + string(p.Protocol) + } + } + + return strings.Join(ports, " ") +} diff --git a/internal/render/svc_test.go b/internal/render/svc_test.go new file mode 100644 index 00000000..b74bdf64 --- /dev/null +++ b/internal/render/svc_test.go @@ -0,0 +1,17 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestServiceRender(t *testing.T) { + c := render.Service{} + r := render.NewRow(4) + c.Render(load(t, "svc"), "", &r) + + assert.Equal(t, "default/dictionary1", r.ID) + assert.Equal(t, render.Fields{"default", "dictionary1", "ClusterIP", "10.47.248.116", "", "app=dictionary1", "http:4001►0"}, r.Fields[:7]) +} diff --git a/internal/render/table.go b/internal/render/table.go new file mode 100644 index 00000000..c6c5ddf4 --- /dev/null +++ b/internal/render/table.go @@ -0,0 +1,91 @@ +package render + +import "sync" + +// TableData tracks a K8s resource for tabular display. +type TableData struct { + Header HeaderRow + RowEvents RowEvents + Namespace string + Mutex *sync.RWMutex +} + +// NewTableData returns a new table. +func NewTableData() *TableData { + return &TableData{Mutex: &sync.RWMutex{}} +} + +// Clear clears out the entire table. +func (t *TableData) Clear() { + t.Header, t.RowEvents = t.Header.Clear(), t.RowEvents.Clear() +} + +// Clone returns a copy of the table +func (t *TableData) Clone() TableData { + return cloneTable(*t) +} + +func cloneTable(t TableData) TableData { + return t +} + +// Update computes row deltas and update the table data. +func (t *TableData) Update(rows Rows) { + empty := len(t.RowEvents) == 0 + kk := make([]string, 0, len(rows)) + var blankDelta DeltaRow + for _, row := range rows { + kk = append(kk, row.ID) + if empty { + t.RowEvents = append(t.RowEvents, NewRowEvent(EventAdd, row)) + continue + } + if index, ok := t.RowEvents.FindIndex(row.ID); ok { + delta := NewDeltaRow(t.RowEvents[index].Row, row, t.Header.HasAge()) + if delta.IsBlank() { + t.RowEvents[index].Kind, t.RowEvents[index].Deltas = EventUnchanged, blankDelta + t.RowEvents[index].Row = row + } else { + t.RowEvents[index] = NewDeltaRowEvent(row, delta) + } + continue + } + t.RowEvents = append(t.RowEvents, NewRowEvent(EventAdd, row)) + } + + if !empty { + t.Delete(kk) + } +} + +// EnsureDeletes delete items in cache that are no longer valid. +func (t *TableData) Delete(newKeys []string) { + for _, re := range t.RowEvents { + var found bool + for i, key := range newKeys { + if key == re.Row.ID { + found = true + newKeys = append(newKeys[:i], newKeys[i+1:]...) + break + } + } + if !found { + t.RowEvents = t.RowEvents.Delete(re.Row.ID) + } + } +} + +// Diff checks if two tables are equal. +func (t *TableData) Diff(table TableData) bool { + if t.Namespace != table.Namespace { + return true + } + if t.Header.Changed(table.Header) { + return true + } + if t.RowEvents.Changed(table.RowEvents) { + return true + } + + return false +} diff --git a/internal/render/types.go b/internal/render/types.go new file mode 100644 index 00000000..499fe639 --- /dev/null +++ b/internal/render/types.go @@ -0,0 +1,46 @@ +package render + +const ( + // AllNamespaces represents all namespaces. + AllNamespaces = "" + + // NamespaceAll represent the all namespace. + NamespaceAll = "all" + + // ClusterScope represents cluster wide resources. + ClusterScope = "-" + + // NonResource represents a custom resource. + NonResource = "*" +) + +const ( + // Terminating represents a pod terminating status. + Terminating = "Terminating" + + // Running represents a pod running status. + Running = "Running" + + // Initialized represents a pod intialized status. + Initialized = "Initialized" + + // Completed represents a pod completed status. + Completed = "Completed" + + // ContainerCreating represents a pod container status. + ContainerCreating = "ContainerCreating" + + // PodInitializing represents a pod initializing status. + PodInitializing = "PodInitializing" +) + +const ( + // MissingValue indicates an unset value. + MissingValue = "" + + // NAValue indicates a value that does not pertain. + NAValue = "n/a" + + // UnknownValue represents an unknown. + UnknownValue = "" +) diff --git a/internal/resource/base.go b/internal/resource/base.go deleted file mode 100644 index 1dd56c35..00000000 --- a/internal/resource/base.go +++ /dev/null @@ -1,188 +0,0 @@ -package resource - -import ( - "bytes" - "context" - "errors" - "path" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/watch" - "github.com/rs/zerolog/log" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - genericprinters "k8s.io/cli-runtime/pkg/printers" - "k8s.io/kubectl/pkg/describe" - "k8s.io/kubectl/pkg/describe/versioned" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -type ( - // Cruder represents a crudable Kubernetes resource. - Cruder interface { - Get(ns string, name string) (interface{}, error) - List(ns string, opts metav1.ListOptions) (k8s.Collection, error) - Delete(ns string, name string, cascade, force bool) error - } - - // Scalable represents a scalable Kubernetes resource. - Scalable interface { - Scale(ns string, name string, replicas int32) error - } - - // Restartable represents a rollout restartable Kubernetes resource. - Restartable interface { - Restart(ns string, name string) error - } - - // Connection represents a Kubenetes apiserver connection. - Connection k8s.Connection - - // Factory creates new tabular resources. - Factory interface { - New(interface{}) Columnar - } - - // Base resource. - Base struct { - Factory - - Connection Connection - path string - Resource Cruder - } -) - -// NewBase returns a new base -func NewBase(c Connection, r Cruder) *Base { - return &Base{Connection: c, Resource: r} -} - -// SetPodMetrics attach pod metrics to resource. -func (b *Base) SetPodMetrics(*mv1beta1.PodMetrics) {} - -// SetNodeMetrics attach node metrics to resource. -func (b *Base) SetNodeMetrics(*mv1beta1.NodeMetrics) {} - -// Name returns the resource namespaced name. -func (b *Base) Name() string { - return b.path -} - -// NumCols designates if column is numerical. -func (*Base) NumCols(n string) map[string]bool { - return map[string]bool{} -} - -// ExtFields returns extended fields in relation to headers. -func (*Base) ExtFields() (TypeMeta, error) { - return TypeMeta{}, errors.New("Base does not have extended fields.") -} - -// Get a resource by name -func (b *Base) Get(path string) (Columnar, error) { - ns, n := Namespaced(path) - i, err := b.Resource.Get(ns, n) - if err != nil { - return nil, err - } - - return b.New(i), nil -} - -// List all resources -func (b *Base) List(ns string, opts metav1.ListOptions) (Columnars, error) { - ii, err := b.Resource.List(ns, opts) - if err != nil { - return nil, err - } - - cc := make(Columnars, 0, len(ii)) - for i := 0; i < len(ii); i++ { - cc = append(cc, b.New(ii[i])) - } - - return cc, nil -} - -// Describe a given resource. -func (b *Base) Describe(gvr, pa string) (string, error) { - mapper := k8s.RestMapper{Connection: b.Connection} - m, err := mapper.ToRESTMapper() - if err != nil { - log.Error().Err(err).Msgf("No REST mapper for resource %s", gvr) - return "", err - } - - GVR := k8s.GVR(gvr) - gvk, err := m.KindFor(GVR.AsGVR()) - if err != nil { - log.Error().Err(err).Msgf("No GVK for resource %s", gvr) - return "", err - } - - mapping, err := mapper.ResourceFor(GVR.ResName(), gvk.Kind) - if err != nil { - log.Error().Err(err).Msgf("Unable to find mapper for %s %s", gvr, pa) - return "", err - } - ns, n := Namespaced(pa) - d, err := versioned.Describer(b.Connection.Config().Flags(), mapping) - if err != nil { - log.Error().Err(err).Msgf("Unable to find describer for %#v", mapping) - return "", err - } - - return d.Describe(ns, n, describe.DescriberSettings{ShowEvents: true}) -} - -// Delete a resource by name. -func (b *Base) Delete(path string, cascade, force bool) error { - ns, n := Namespaced(path) - - return b.Resource.Delete(ns, n, cascade, force) -} - -func (*Base) namespacedName(m metav1.ObjectMeta) string { - return path.Join(m.Namespace, m.Name) -} - -func (*Base) marshalObject(o runtime.Object) (string, error) { - var ( - buff bytes.Buffer - p genericprinters.YAMLPrinter - ) - err := p.PrintObj(o, &buff) - if err != nil { - log.Error().Msgf("Marshal Error %v", err) - return "", err - } - - return buff.String(), nil -} - -func (b *Base) podLogs(ctx context.Context, c chan<- string, sel map[string]string, opts LogOptions) error { - i := ctx.Value(IKey("informer")).(*watch.Informer) - pods, err := i.List(watch.PodIndex, opts.Namespace, metav1.ListOptions{ - LabelSelector: toSelector(sel), - }) - if err != nil { - return err - } - - if len(pods) > 1 { - opts.MultiPods = true - } - pr := NewPod(b.Connection) - for _, p := range pods { - po := p.(*v1.Pod) - if po.Status.Phase == v1.PodRunning { - opts.Namespace, opts.Name = po.Namespace, po.Name - if err := pr.PodLogs(ctx, c, opts); err != nil { - return err - } - } - } - return nil -} diff --git a/internal/resource/cluster_test.go b/internal/resource/cluster_test.go deleted file mode 100644 index c4b89ce6..00000000 --- a/internal/resource/cluster_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package resource_test - -import ( - "fmt" - "testing" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - m "github.com/petergtz/pegomock" - "github.com/rs/zerolog" - "github.com/stretchr/testify/assert" -) - -func init() { - zerolog.SetGlobalLevel(zerolog.Disabled) -} - -func TestClusterVersion(t *testing.T) { - mm, mx := NewMockClusterMeta(), NewMockMetricsServer() - m.When(mm.Version()).ThenReturn("1.2.3", nil) - - ci := resource.NewClusterWithArgs(mm, mx) - assert.Equal(t, "1.2.3", ci.Version()) -} - -func TestClusterNoVersion(t *testing.T) { - mm, mx := NewMockClusterMeta(), NewMockMetricsServer() - m.When(mm.Version()).ThenReturn("bad", fmt.Errorf("No data")) - - ci := resource.NewClusterWithArgs(mm, mx) - assert.Equal(t, "n/a", ci.Version()) -} - -func TestClusterName(t *testing.T) { - mm, mx := NewMockClusterMeta(), NewMockMetricsServer() - m.When(mm.ClusterName()).ThenReturn("fred") - - ci := resource.NewClusterWithArgs(mm, mx) - assert.Equal(t, "fred", ci.ClusterName()) -} - -func TestContextName(t *testing.T) { - mm, mx := NewMockClusterMeta(), NewMockMetricsServer() - m.When(mm.ContextName()).ThenReturn("fred") - - ci := resource.NewClusterWithArgs(mm, mx) - assert.Equal(t, "fred", ci.ContextName()) -} - -func TestUserName(t *testing.T) { - mm, mx := NewMockClusterMeta(), NewMockMetricsServer() - m.When(mm.UserName()).ThenReturn("fred") - - ci := resource.NewClusterWithArgs(mm, mx) - assert.Equal(t, "fred", ci.UserName()) -} - -func TestClusterMetrics(t *testing.T) { - mm, mx := NewMockClusterMeta(), NewMockMetricsServer() - - mxx := clusterMetric() - - c := resource.NewClusterWithArgs(mm, mx) - c.Metrics(k8s.Collection{}, k8s.Collection{}, &mxx) - assert.Equal(t, clusterMetric(), mxx) -} - -// Helpers... - -func TestUsingMocks(t *testing.T) { - m.RegisterMockTestingT(t) - m.RegisterMockFailHandler(func(m string, i ...int) { - fmt.Println("Boom!", m, i) - }) -} - -func clusterMetric() k8s.ClusterMetrics { - return k8s.ClusterMetrics{ - PercCPU: 100, - PercMEM: 1000, - } -} diff --git a/internal/resource/cm.go b/internal/resource/cm.go deleted file mode 100644 index 08317942..00000000 --- a/internal/resource/cm.go +++ /dev/null @@ -1,6 +0,0 @@ -package resource - -// NewConfigMapList returns a new resource list. -func NewConfigMapList(c Connection, ns string) List { - return NewCustomList(c, true, "", "v1/configmaps") -} diff --git a/internal/resource/container.go b/internal/resource/container.go deleted file mode 100644 index bd5ceec3..00000000 --- a/internal/resource/container.go +++ /dev/null @@ -1,259 +0,0 @@ -package resource - -import ( - "context" - "fmt" - "strconv" - "strings" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/derailed/k9s/internal/k8s" - v1 "k8s.io/api/core/v1" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -type ( - // Container represents a container on a pod. - Container struct { - *Base - - pod *v1.Pod - instance v1.Container - metrics *mv1beta1.PodMetrics - } -) - -// NewContainerList returns a collection of container. -func NewContainerList(c Connection, pod *v1.Pod) List { - return NewList( - "", - "co", - NewContainer(c, pod), - 0, - ) -} - -// NewContainer returns a new set of containers. -func NewContainer(c Connection, pod *v1.Pod) *Container { - co := Container{ - Base: &Base{Connection: c, Resource: k8s.NewPod(c)}, - pod: pod, - } - co.Factory = &co - - return &co -} - -// New builds a new Container instance from a k8s resource. -func (r *Container) New(i interface{}) Columnar { - co := NewContainer(r.Connection, r.pod) - co.instance = i.(v1.Container) - co.path = r.namespacedName(r.pod.ObjectMeta) + ":" + co.instance.Name - - return co -} - -// SetPodMetrics set the current k8s resource metrics on associated pod. -func (r *Container) SetPodMetrics(m *mv1beta1.PodMetrics) { - r.metrics = m -} - -// Marshal resource to yaml. -func (r *Container) Marshal(path string) (string, error) { - return "", nil -} - -// // PodLogs tail logs for all containers in a running Pod. -// func (r *Container) PodLogs(ctx context.Context, c chan<- string, ns, n string, lines int64, prev bool) error { -// return nil -// } - -// Logs tails a given container logs -func (r *Container) Logs(ctx context.Context, c chan<- string, opts LogOptions) error { - res, ok := r.Resource.(k8s.Loggable) - if !ok { - return fmt.Errorf("Resource %T is not Loggable", r.Resource) - } - - return tailLogs(ctx, res, c, opts) -} - -// List resources for a given namespace. -func (r *Container) List(ns string, opts metav1.ListOptions) (Columnars, error) { - icos := r.pod.Spec.InitContainers - cos := r.pod.Spec.Containers - - cc := make(Columnars, 0, len(icos)+len(cos)) - for _, co := range icos { - ci := r.New(co) - cc = append(cc, ci) - } - for _, co := range cos { - cc = append(cc, r.New(co)) - } - - return cc, nil -} - -// Header return resource header. -func (*Container) Header(ns string) Row { - return append(Row{}, - "NAME", - "IMAGE", - "READY", - "STATE", - "RS", - "PROBES(L:R)", - "CPU", - "MEM", - "%CPU", - "%MEM", - "PORTS", - "AGE", - ) -} - -// NumCols designates if column is numerical. -func (*Container) NumCols(n string) map[string]bool { - return map[string]bool{ - "CPU": true, - "MEM": true, - "%CPU": true, - "%MEM": true, - "RS": true, - } -} - -// Fields retrieves displayable fields. -func (r *Container) Fields(ns string) Row { - ff := make(Row, 0, len(r.Header(ns))) - i := r.instance - - c, p := gatherMetrics(i, r.metrics) - - ready, state, restarts := "false", MissingValue, "0" - cs := getContainerStatus(i.Name, r.pod.Status) - if cs != nil { - ready, state, restarts = boolToStr(cs.Ready), toState(cs.State), strconv.Itoa(int(cs.RestartCount)) - } - - return append(ff, - i.Name, - i.Image, - ready, - state, - restarts, - probe(i.LivenessProbe)+":"+probe(i.ReadinessProbe), - c.cpu, - c.mem, - p.cpu, - p.mem, - toStrPorts(i.Ports), - toAge(r.pod.CreationTimestamp), - ) -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func gatherMetrics(co v1.Container, mx *mv1beta1.PodMetrics) (c, p metric) { - c, p = noMetric(), noMetric() - if mx == nil { - return - } - - var ( - cpu int64 - mem float64 - ) - for _, c := range mx.Containers { - if c.Name == co.Name { - cpu = c.Usage.Cpu().MilliValue() - mem = k8s.ToMB(c.Usage.Memory().Value()) - break - } - } - c = metric{ - cpu: ToMillicore(cpu), - mem: ToMi(mem), - } - - rcpu, rmem := containerResources(co) - if rcpu != nil { - p.cpu = AsPerc(toPerc(float64(cpu), float64(rcpu.MilliValue()))) - } - if rmem != nil { - p.mem = AsPerc(toPerc(mem, k8s.ToMB(rmem.Value()))) - } - - return -} - -func getContainerStatus(co string, status v1.PodStatus) *v1.ContainerStatus { - for _, c := range status.ContainerStatuses { - if c.Name == co { - return &c - } - } - - for _, c := range status.InitContainerStatuses { - if c.Name == co { - return &c - } - } - - return nil -} - -func toStrPorts(pp []v1.ContainerPort) string { - ports := make([]string, len(pp)) - for i, p := range pp { - if len(p.Name) > 0 { - ports[i] = p.Name + ":" - } - ports[i] += strconv.Itoa(int(p.ContainerPort)) - if p.Protocol != "TCP" { - ports[i] += "╱" + string(p.Protocol) - } - } - - return strings.Join(ports, ",") -} - -func toState(s v1.ContainerState) string { - switch { - case s.Waiting != nil: - if s.Waiting.Reason != "" { - return s.Waiting.Reason - } - return "Waiting" - - case s.Terminated != nil: - if s.Terminated.Reason != "" { - return s.Terminated.Reason - } - return "Terminating" - case s.Running != nil: - return "Running" - default: - return MissingValue - } -} - -func toRes(r v1.ResourceList) (string, string) { - cpu, mem := r[v1.ResourceCPU], r[v1.ResourceMemory] - - return ToMillicore(cpu.MilliValue()), ToMi(k8s.ToMB(mem.Value())) -} - -func probe(p *v1.Probe) string { - if p == nil { - return "off" - } - return "on" -} - -func asMi(v int64) float64 { - return float64(v) / 1024 * 1024 -} diff --git a/internal/resource/container_test.go b/internal/resource/container_test.go deleted file mode 100644 index 5e4458ed..00000000 --- a/internal/resource/container_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package resource - -import ( - "testing" - - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" -) - -func TestProbe(t *testing.T) { - uu := map[string]struct { - probe *v1.Probe - e string - }{ - "defined": {&v1.Probe{}, "on"}, - "undefined": {nil, "off"}, - } - - for k, u := range uu { - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, probe(u.probe)) - }) - } -} - -func TestAsMi(t *testing.T) { - uu := map[string]struct { - mem int64 - e float64 - }{ - "zero": {0, 0}, - "1Mb": {1024 * 1024, 1.048576e+06}, - "10Mb": {10 * 1024 * 1024, 1.048576e+07}, - } - - for k, u := range uu { - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, asMi(u.mem)) - }) - } -} - -func TestToRes(t *testing.T) { - uu := map[string]struct { - res v1.ResourceList - ecpu, emem string - }{ - "cool": {v1.ResourceList{ - v1.ResourceCPU: toQty("10m"), - v1.ResourceMemory: toQty("20Mi"), - }, - "10", "20"}, - "noRes": {v1.ResourceList{}, - "0", "0"}, - } - - for k, u := range uu { - t.Run(k, func(t *testing.T) { - cpu, mem := toRes(u.res) - assert.Equal(t, u.ecpu, cpu) - assert.Equal(t, u.emem, mem) - }) - } -} - -func TestToState(t *testing.T) { - uu := map[string]struct { - state v1.ContainerState - e string - }{ - "empty": {v1.ContainerState{}, - MissingValue}, - "running": { - v1.ContainerState{Running: &v1.ContainerStateRunning{}}, - "Running", - }, - "waiting": { - v1.ContainerState{Waiting: &v1.ContainerStateWaiting{}}, - "Waiting", - }, - "waitingReason": { - v1.ContainerState{Waiting: &v1.ContainerStateWaiting{Reason: "blee"}}, - "blee", - }, - "terminating": { - v1.ContainerState{Terminated: &v1.ContainerStateTerminated{}}, - "Terminating", - }, - "terminatedReason": { - v1.ContainerState{Terminated: &v1.ContainerStateTerminated{Reason: "blee"}}, - "blee", - }, - } - - for k, u := range uu { - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, toState(u.state)) - }) - } -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func toQty(s string) resource.Quantity { - q, _ := resource.ParseQuantity(s) - - return q -} diff --git a/internal/resource/context.go b/internal/resource/context.go deleted file mode 100644 index de54d18f..00000000 --- a/internal/resource/context.go +++ /dev/null @@ -1,87 +0,0 @@ -package resource - -import ( - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" -) - -type ( - // Switchable represents a switchable resource. - Switchable interface { - Switch(ctx string) error - MustCurrentContextName() string - } - - // SwitchableCruder represents a resource that can be switched. - SwitchableCruder interface { - Cruder - Switchable - } - - // Context tracks a kubernetes resource. - Context struct { - *Base - instance *k8s.NamedContext - } -) - -// NewContextList returns a new resource list. -func NewContextList(c Connection, ns string) List { - return NewList(NotNamespaced, "ctx", NewContext(c), SwitchAccess) -} - -// NewContext instantiates a new Context. -func NewContext(c Connection) *Context { - ctx := &Context{Base: NewBase(c, k8s.NewContext(c))} - ctx.Factory = ctx - - return ctx -} - -// New builds a new Context instance from a k8s resource. -func (r *Context) New(i interface{}) Columnar { - c := NewContext(r.Connection) - switch instance := i.(type) { - case *k8s.NamedContext: - c.instance = instance - case k8s.NamedContext: - c.instance = &instance - default: - log.Fatal().Msgf("unknown context type %#v", i) - } - c.path = c.instance.Name - - return c -} - -// Switch out current context. -func (r *Context) Switch(c string) error { - return r.Resource.(Switchable).Switch(c) -} - -// Marshal the resource to yaml. -func (r *Context) Marshal(path string) (string, error) { - return "", nil -} - -// Header return resource header. -func (*Context) Header(string) Row { - return append(Row{}, "NAME", "CLUSTER", "AUTHINFO", "NAMESPACE") -} - -// Fields retrieves displayable fields. -func (r *Context) Fields(ns string) Row { - ff := make(Row, 0, len(r.Header(ns))) - - i := r.instance - if i.MustCurrentContextName() == i.Name { - i.Name += "*" - } - - return append(ff, - i.Name, - i.Context.Cluster, - i.Context.AuthInfo, - i.Context.Namespace, - ) -} diff --git a/internal/resource/context_test.go b/internal/resource/context_test.go deleted file mode 100644 index ed63a252..00000000 --- a/internal/resource/context_test.go +++ /dev/null @@ -1,141 +0,0 @@ -package resource_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - m "github.com/petergtz/pegomock" - "github.com/stretchr/testify/assert" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/cli-runtime/pkg/genericclioptions" - api "k8s.io/client-go/tools/clientcmd/api" -) - -func NewContextListWithArgs(ns string, ctx *resource.Context) resource.List { - return resource.NewList(resource.NotNamespaced, "ctx", ctx, resource.SwitchAccess) -} - -func NewContextWithArgs(c k8s.Connection, s resource.SwitchableCruder) *resource.Context { - ctx := &resource.Context{Base: resource.NewBase(c, s)} - ctx.Factory = ctx - return ctx -} - -func TestCTXSwitch(t *testing.T) { - mc := NewMockConnection() - mr := NewMockSwitchableCruder() - m.When(mr.Switch("fred")).ThenReturn(nil) - - ctx := NewContextWithArgs(mc, mr) - err := ctx.Switch("fred") - - assert.Nil(t, err) - mr.VerifyWasCalledOnce().Switch("fred") -} - -func TestCTXList(t *testing.T) { - mc := NewMockConnection() - mr := NewMockSwitchableCruder() - m.When(mr.List("blee", metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sNamedCTX()}, nil) - - ctx := NewContextWithArgs(mc, mr) - cc, err := ctx.List("blee", metav1.ListOptions{}) - - assert.Nil(t, err) - assert.Equal(t, resource.Columnars{ctx.New(k8sNamedCTX())}, cc) - mr.VerifyWasCalledOnce().List("blee", metav1.ListOptions{}) -} - -func TestCTXDelete(t *testing.T) { - mc := NewMockConnection() - mr := NewMockSwitchableCruder() - m.When(mr.Delete("", "fred", true, true)).ThenReturn(nil) - - ctx := NewContextWithArgs(mc, mr) - - assert.Nil(t, ctx.Delete("fred", true, true)) - mr.VerifyWasCalledOnce().Delete("", "fred", true, true) -} - -func TestCTXListHasName(t *testing.T) { - mc := NewMockConnection() - mr := NewMockSwitchableCruder() - - ctx := NewContextWithArgs(mc, mr) - l := NewContextListWithArgs("blee", ctx) - - assert.Equal(t, "ctx", l.GetName()) -} - -func TestCTXListHasNamespace(t *testing.T) { - mc := NewMockConnection() - mr := NewMockSwitchableCruder() - - ctx := NewContextWithArgs(mc, mr) - l := NewContextListWithArgs("blee", ctx) - - assert.Equal(t, resource.NotNamespaced, l.GetNamespace()) -} - -func TestCTXListHasResource(t *testing.T) { - mc := NewMockConnection() - mr := NewMockSwitchableCruder() - - ctx := NewContextWithArgs(mc, mr) - l := NewContextListWithArgs("blee", ctx) - - assert.NotNil(t, l.Resource()) -} - -func TestCTXHeader(t *testing.T) { - mc := NewMockConnection() - mr := NewMockSwitchableCruder() - - ctx := NewContextWithArgs(mc, mr) - - assert.Equal(t, 4, len(ctx.Header(""))) -} - -func TestCTXFields(t *testing.T) { - mc := NewMockConnection() - m.When(mc.Config()).ThenReturn(k8sConfig()) - mr := NewMockSwitchableCruder() - m.When(mr.MustCurrentContextName()).ThenReturn("test") - - ctx := NewContextWithArgs(mc, mr) - c := ctx.New(k8sNamedCTX()) - - assert.Equal(t, 4, len(c.Fields(""))) - assert.Equal(t, "test*", c.Fields("")[0]) -} - -// Helpers... - -func k8sConfig() *k8s.Config { - ctx := "test" - f := genericclioptions.ConfigFlags{ - Context: &ctx, - } - return k8s.NewConfig(&f) -} - -func k8sCTX() *api.Context { - return &api.Context{ - LocationOfOrigin: "fred", - Cluster: "blee", - AuthInfo: "secret", - } -} - -func k8sNamedCTX() *k8s.NamedContext { - return k8s.NewNamedContext( - k8sConfig(), - "test", - &api.Context{ - LocationOfOrigin: "fred", - Cluster: "blee", - AuthInfo: "secret", - }, - ) -} diff --git a/internal/resource/cr.go b/internal/resource/cr.go deleted file mode 100644 index 02caa399..00000000 --- a/internal/resource/cr.go +++ /dev/null @@ -1,78 +0,0 @@ -package resource - -import ( - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - v1 "k8s.io/api/rbac/v1" -) - -// ClusterRole tracks a kubernetes resource. -type ClusterRole struct { - *Base - instance *v1.ClusterRole -} - -// NewClusterRoleList returns a new resource list. -func NewClusterRoleList(c Connection, ns string) List { - return NewList( - NotNamespaced, - "clusterrole", - NewClusterRole(c), - CRUDAccess|DescribeAccess, - ) -} - -// NewClusterRole instantiates a new ClusterRole. -func NewClusterRole(c Connection) *ClusterRole { - cr := &ClusterRole{&Base{Connection: c, Resource: k8s.NewClusterRole(c)}, nil} - cr.Factory = cr - - return cr -} - -// New builds a new ClusterRole instance from a k8s resource. -func (r *ClusterRole) New(i interface{}) Columnar { - c := NewClusterRole(r.Connection) - switch instance := i.(type) { - case *v1.ClusterRole: - c.instance = instance - case v1.ClusterRole: - c.instance = &instance - default: - log.Fatal().Msgf("unknown context type %#v", i) - } - c.path = c.instance.Name - - return c -} - -// Marshal resource to yaml. -func (r *ClusterRole) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - - cr := i.(*v1.ClusterRole) - cr.TypeMeta.APIVersion = "rbac.authorization.k8s.io/v1" - cr.TypeMeta.Kind = "ClusterRole" - - return r.marshalObject(cr) -} - -// Header return resource header. -func (*ClusterRole) Header(ns string) Row { - return append(Row{}, "NAME", "AGE") -} - -// Fields retrieves displayable fields. -func (r *ClusterRole) Fields(ns string) Row { - ff := make(Row, 0, len(r.Header(ns))) - i := r.instance - - return append(ff, - i.Name, - toAge(i.ObjectMeta.CreationTimestamp), - ) -} diff --git a/internal/resource/cr_binding.go b/internal/resource/cr_binding.go deleted file mode 100644 index 889fcf8a..00000000 --- a/internal/resource/cr_binding.go +++ /dev/null @@ -1,118 +0,0 @@ -package resource - -import ( - "strings" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - v1 "k8s.io/api/rbac/v1" -) - -// ClusterRoleBinding tracks a kubernetes resource. -type ClusterRoleBinding struct { - *Base - instance *v1.ClusterRoleBinding -} - -// NewClusterRoleBindingList returns a new resource list. -func NewClusterRoleBindingList(c Connection, _ string) List { - return NewList( - NotNamespaced, - "clusterrolebinding", - NewClusterRoleBinding(c), - ViewAccess|DeleteAccess|DescribeAccess, - ) -} - -// NewClusterRoleBinding instantiates a new ClusterRoleBinding. -func NewClusterRoleBinding(c Connection) *ClusterRoleBinding { - crb := &ClusterRoleBinding{&Base{Connection: c, Resource: k8s.NewClusterRoleBinding(c)}, nil} - crb.Factory = crb - - return crb -} - -// New builds a new tabular instance from a k8s resource. -func (r *ClusterRoleBinding) New(i interface{}) Columnar { - crb := NewClusterRoleBinding(r.Connection) - switch instance := i.(type) { - case *v1.ClusterRoleBinding: - crb.instance = instance - case v1.ClusterRoleBinding: - crb.instance = &instance - default: - log.Fatal().Msgf("unknown context type %#v", i) - } - crb.path = crb.instance.Name - - return crb -} - -// Marshal resource to yaml. -func (r *ClusterRoleBinding) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - - crb := i.(*v1.ClusterRoleBinding) - crb.TypeMeta.APIVersion = "rbac.authorization.k8s.io/v1" - crb.TypeMeta.Kind = "ClusterRoleBinding" - - return r.marshalObject(crb) -} - -// Header return resource header. -func (*ClusterRoleBinding) Header(_ string) Row { - return append(Row{}, "NAME", "ROLE", "KIND", "SUBJECTS", "AGE") -} - -// Fields retrieves displayable fields. -func (r *ClusterRoleBinding) Fields(ns string) Row { - ff := make(Row, 0, len(r.Header(ns))) - - i := r.instance - kind, ss := renderSubjects(i.Subjects) - - return append(ff, - i.Name, - i.RoleRef.Name, - kind, - ss, - toAge(i.ObjectMeta.CreationTimestamp), - ) -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func renderSubjects(ss []v1.Subject) (kind string, subjects string) { - if len(ss) == 0 { - return NAValue, "" - } - - var tt []string - for _, s := range ss { - kind = toSubjectAlias(s.Kind) - tt = append(tt, s.Name) - } - return kind, strings.Join(tt, ",") -} - -func toSubjectAlias(s string) string { - if len(s) == 0 { - return s - } - - switch s { - case v1.UserKind: - return "USR" - case v1.GroupKind: - return "GRP" - case v1.ServiceAccountKind: - return "SA" - default: - return strings.ToUpper(s) - } -} diff --git a/internal/resource/cr_binding_test.go b/internal/resource/cr_binding_test.go deleted file mode 100644 index c614257b..00000000 --- a/internal/resource/cr_binding_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package resource_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - m "github.com/petergtz/pegomock" - "github.com/stretchr/testify/assert" - rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func NewClusterRoleBindingListWithArgs(ns string, r *resource.ClusterRoleBinding) resource.List { - return resource.NewList(resource.NotNamespaced, "clusterrolebinding", r, resource.ViewAccess|resource.DeleteAccess|resource.DescribeAccess) -} - -func NewClusterRoleBindingWithArgs(conn k8s.Connection, res resource.Cruder) *resource.ClusterRoleBinding { - r := &resource.ClusterRoleBinding{Base: resource.NewBase(conn, res)} - r.Factory = r - return r -} - -func TestCRBFields(t *testing.T) { - conn := NewMockConnection() - - r := newCRB(conn).Fields(resource.AllNamespaces) - - assert.Equal(t, "fred", r[0]) -} - -func TestCRBMarshal(t *testing.T) { - conn := NewMockConnection() - ca := NewMockCruder() - m.When(ca.Get("blee", "fred")).ThenReturn(k8sCRB(), nil) - - cm := NewClusterRoleBindingWithArgs(conn, ca) - ma, err := cm.Marshal("blee/fred") - - ca.VerifyWasCalledOnce().Get("blee", "fred") - assert.Nil(t, err) - assert.Equal(t, crbYaml(), ma) -} - -func TestCRBListData(t *testing.T) { - conn := NewMockConnection() - ca := NewMockCruder() - m.When(ca.List(resource.NotNamespaced, metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sCRB()}, nil) - - l := NewClusterRoleBindingListWithArgs("-", NewClusterRoleBindingWithArgs(conn, ca)) - // Make sure we can get deltas! - for i := 0; i < 2; i++ { - err := l.Reconcile(nil, nil) - assert.Nil(t, err) - } - - ca.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced, metav1.ListOptions{}) - td := l.Data() - assert.Equal(t, 1, len(td.Rows)) - assert.Equal(t, resource.NotNamespaced, l.GetNamespace()) - row := td.Rows["fred"] - assert.Equal(t, 5, len(row.Deltas)) - for _, d := range row.Deltas { - assert.Equal(t, "", d) - } - assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) -} - -// Helpers... - -func k8sCRB() *rbacv1.ClusterRoleBinding { - return &rbacv1.ClusterRoleBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: "fred", - Namespace: "blee", - CreationTimestamp: metav1.Time{testTime()}, - }, - Subjects: []rbacv1.Subject{ - {Kind: "test", Name: "fred", Namespace: "blee"}, - }, - } -} - -func newCRB(c resource.Connection) resource.Columnar { - return resource.NewClusterRoleBinding(c).New(k8sCRB()) -} - -func crbYaml() string { - return `apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - creationTimestamp: "2018-12-14T17:36:43Z" - name: fred - namespace: blee -roleRef: - apiGroup: "" - kind: "" - name: "" -subjects: -- kind: test - name: fred - namespace: blee -` -} diff --git a/internal/resource/cr_test.go b/internal/resource/cr_test.go deleted file mode 100644 index 81ceec41..00000000 --- a/internal/resource/cr_test.go +++ /dev/null @@ -1,138 +0,0 @@ -package resource_test - -import ( - "fmt" - "testing" - "time" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - m "github.com/petergtz/pegomock" - "github.com/stretchr/testify/assert" - rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func NewClusterRoleListWithArgs(ns string, r *resource.ClusterRole) resource.List { - return resource.NewList(resource.NotNamespaced, "clusterrole", r, resource.CRUDAccess|resource.DescribeAccess) -} - -func NewClusterRoleWithArgs(mc resource.Connection, res resource.Cruder) *resource.ClusterRole { - r := &resource.ClusterRole{Base: resource.NewBase(mc, res)} - r.Factory = r - return r -} - -func TestCRListAccess(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - - ns := "blee" - r := NewClusterRoleWithArgs(mc, mr) - l := NewClusterRoleListWithArgs(resource.AllNamespaces, r) - l.SetNamespace(ns) - - assert.Equal(t, resource.NotNamespaced, l.GetNamespace()) - assert.Equal(t, "clusterrole", l.GetName()) - for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} { - assert.True(t, l.Access(a)) - } -} - -func TestCRFields(t *testing.T) { - r := newClusterRole().Fields("blee") - assert.Equal(t, "fred", r[0]) -} - -func TestCRFieldsAllNS(t *testing.T) { - r := newClusterRole().Fields(resource.AllNamespaces) - assert.Equal(t, "fred", r[0]) -} - -func TestCRMarshal(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("blee", "fred")).ThenReturn(k8sCR(), nil) - - cr := NewClusterRoleWithArgs(mc, mr) - ma, err := cr.Marshal("blee/fred") - - mr.VerifyWasCalledOnce().Get("blee", "fred") - assert.Nil(t, err) - assert.Equal(t, mrYaml(), ma) -} - -func TestCRListData(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.List(resource.NotNamespaced, metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sCR()}, nil) - - l := NewClusterRoleListWithArgs("-", NewClusterRoleWithArgs(mc, mr)) - // Make sure we mcn get deltas! - for i := 0; i < 2; i++ { - err := l.Reconcile(nil, nil) - assert.Nil(t, err) - } - - mr.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced, metav1.ListOptions{}) - - td := l.Data() - assert.Equal(t, 1, len(td.Rows)) - assert.Equal(t, resource.NotNamespaced, l.GetNamespace()) - row := td.Rows["fred"] - assert.Equal(t, 2, len(row.Deltas)) - for _, d := range row.Deltas { - assert.Equal(t, "", d) - } - assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) -} - -// Helpers... - -func k8sCR() *rbacv1.ClusterRole { - return &rbacv1.ClusterRole{ - ObjectMeta: metav1.ObjectMeta{ - Name: "fred", - Namespace: "blee", - CreationTimestamp: metav1.Time{testTime()}, - }, - Rules: []rbacv1.PolicyRule{ - { - Verbs: []string{"get", "list"}, - APIGroups: []string{""}, - ResourceNames: []string{"pod"}, - }, - }, - } -} - -func newClusterRole() resource.Columnar { - conn := NewMockConnection() - return resource.NewClusterRole(conn).New(k8sCR()) -} - -func testTime() time.Time { - t, err := time.Parse(time.RFC3339, "2018-12-14T10:36:43.326972-07:00") - if err != nil { - fmt.Println("TestTime Failed", err) - } - return t -} - -func mrYaml() string { - return `apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - creationTimestamp: "2018-12-14T17:36:43Z" - name: fred - namespace: blee -rules: -- apiGroups: - - "" - resourceNames: - - pod - verbs: - - get - - list -` -} diff --git a/internal/resource/crd.go b/internal/resource/crd.go deleted file mode 100644 index e7e09608..00000000 --- a/internal/resource/crd.go +++ /dev/null @@ -1,126 +0,0 @@ -package resource - -import ( - "errors" - "time" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - "gopkg.in/yaml.v2" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -// CustomResourceDefinition tracks a kubernetes resource. -type CustomResourceDefinition struct { - *Base - instance *unstructured.Unstructured -} - -var _ Columnar = (*CustomResourceDefinition)(nil) - -// NewCustomResourceDefinitionList returns a new resource list. -func NewCustomResourceDefinitionList(c Connection, ns string) List { - return NewList( - NotNamespaced, - "crd", - NewCustomResourceDefinition(c), - CRUDAccess|DescribeAccess, - ) -} - -// NewCustomResourceDefinition instantiates a new CustomResourceDefinition. -func NewCustomResourceDefinition(c Connection) *CustomResourceDefinition { - crd := &CustomResourceDefinition{&Base{Connection: c, Resource: k8s.NewCustomResourceDefinition(c)}, nil} - crd.Factory = crd - - return crd -} - -// New builds a new CustomResourceDefinition instance from a k8s resource. -func (r *CustomResourceDefinition) New(i interface{}) Columnar { - c := NewCustomResourceDefinition(r.Connection) - switch instance := i.(type) { - case *unstructured.Unstructured: - c.instance = instance - case unstructured.Unstructured: - c.instance = &instance - default: - log.Fatal().Msgf("unknown CustomResourceDefinition type %#v", i) - } - meta := c.instance.Object["metadata"].(map[string]interface{}) - c.path = meta["name"].(string) - - return c -} - -// Marshal a resource. -func (r *CustomResourceDefinition) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - - raw, err := yaml.Marshal(i) - if err != nil { - return "", err - } - - // BOZO!! Need to figure out apiGroup+Version - // return r.marshalObject(i.(*unstructured.Unstructured)) - return string(raw), nil -} - -// Header return the resource header. -func (*CustomResourceDefinition) Header(ns string) Row { - return Row{"NAME", "AGE"} -} - -// Fields retrieves displayable fields. -func (r *CustomResourceDefinition) Fields(ns string) Row { - ff := make(Row, 0, len(r.Header(ns))) - - i := r.instance - meta := i.Object["metadata"].(map[string]interface{}) - t, err := time.Parse(time.RFC3339, meta["creationTimestamp"].(string)) - if err != nil { - log.Error().Msgf("Fields timestamp %v", err) - } - - return append(ff, meta["name"].(string), toAge(metav1.Time{t})) -} - -// ExtFields returns extended fields. -func (r *CustomResourceDefinition) ExtFields() (TypeMeta, error) { - m := TypeMeta{} - i := r.instance - spec, ok := i.Object["spec"].(map[string]interface{}) - if !ok { - return m, errors.New("missing crd specs") - } - - if meta, ok := i.Object["metadata"].(map[string]interface{}); ok { - m.Name = meta["name"].(string) - } - m.Group, m.Version = spec["group"].(string), spec["version"].(string) - m.Namespaced = isNamespaced(spec["scope"].(string)) - names, ok := spec["names"].(map[string]interface{}) - if !ok { - return m, errors.New("missing crd names") - } - m.Kind = names["kind"].(string) - m.Singular, m.Plural = names["singular"].(string), names["plural"].(string) - if names["shortNames"] != nil { - for _, s := range names["shortNames"].([]interface{}) { - m.ShortNames = append(m.ShortNames, s.(string)) - } - } else { - m.ShortNames = nil - } - return m, nil -} - -func isNamespaced(scope string) bool { - return scope == "Namespaced" -} diff --git a/internal/resource/crd_test.go b/internal/resource/crd_test.go deleted file mode 100644 index 74edbdc7..00000000 --- a/internal/resource/crd_test.go +++ /dev/null @@ -1,145 +0,0 @@ -package resource_test - -import ( - "testing" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - m "github.com/petergtz/pegomock" - "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -func NewCRDListWithArgs(ns string, r *resource.CustomResourceDefinition) resource.List { - return resource.NewList("-", "crd", r, resource.CRUDAccess|resource.DescribeAccess) -} - -func NewCRDWithArgs(conn k8s.Connection, res resource.Cruder) *resource.CustomResourceDefinition { - r := &resource.CustomResourceDefinition{Base: resource.NewBase(conn, res)} - r.Factory = r - - return r -} - -func TestCRDListAccess(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - - ns := "blee" - r := NewCRDWithArgs(mc, mr) - l := NewCRDListWithArgs(resource.AllNamespaces, r) - l.SetNamespace(ns) - - assert.Equal(t, resource.NotNamespaced, l.GetNamespace()) - assert.Equal(t, "crd", l.GetName()) - for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} { - assert.True(t, l.Access(a)) - } -} - -func TestCRDFields(t *testing.T) { - r := newCRD().Fields("blee") - - assert.Equal(t, "fred", r[0]) -} - -func TestCRDFieldsAllNS(t *testing.T) { - r := newCRD().Fields(resource.AllNamespaces) - - assert.Equal(t, "fred", r[0]) -} - -func TestCRDMarshal(t *testing.T) { - mc := NewMockConnection() - cr := NewMockCruder() - m.When(cr.Get("blee", "fred")).ThenReturn(k8sCRD(), nil) - - r := NewCRDWithArgs(mc, cr) - ma, err := r.Marshal("blee/fred") - - cr.VerifyWasCalledOnce().Get("blee", "fred") - assert.Nil(t, err) - assert.Equal(t, crdYaml(), ma) -} - -func TestCRDListData(t *testing.T) { - mc := NewMockConnection() - cr := NewMockCruder() - m.When(cr.List(resource.NotNamespaced, v1.ListOptions{})).ThenReturn(k8s.Collection{*k8sCRD()}, nil) - - l := NewCRDListWithArgs("-", NewCRDWithArgs(mc, cr)) - // Make sure we can get deltas! - for i := 0; i < 2; i++ { - err := l.Reconcile(nil, nil) - assert.Nil(t, err) - } - - cr.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced, metav1.ListOptions{}) - td := l.Data() - assert.Equal(t, 1, len(td.Rows)) - assert.Equal(t, resource.NotNamespaced, l.GetNamespace()) - row := td.Rows["fred"] - assert.Equal(t, 2, len(row.Deltas)) - for _, d := range row.Deltas { - assert.Equal(t, "", d) - } - assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) -} - -// Helpers... - -func k8sCRD() *unstructured.Unstructured { - return &unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "namespace": "blee", - "name": "fred", - "creationTimestamp": "2018-12-14T10:36:43.326972Z", - }, - }, - } -} - -func k8sCRDFull() *unstructured.Unstructured { - return &unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "namespace": "blee", - "name": "fred", - "creationTimestamp": "2018-12-14T10:36:43.326972Z", - }, - "spec": map[string]interface{}{ - "group": "apps", - "version": "v1", - "names": map[string]interface{}{ - "kind": "cool", - "singular": "cool", - "plural": "cools", - "shortNamed": []string{"co", "cos"}, - }, - }, - }, - } -} - -func newCRDFull() resource.Columnar { - mc := NewMockConnection() - return resource.NewCustomResourceDefinition(mc).New(k8sCRDFull()) -} - -func newCRD() resource.Columnar { - mc := NewMockConnection() - return resource.NewCustomResourceDefinition(mc).New(k8sCRD()) -} - -func crdYaml() string { - return `object: - metadata: - creationTimestamp: "2018-12-14T10:36:43.326972Z" - name: fred - namespace: blee -` -} diff --git a/internal/resource/cronjob.go b/internal/resource/cronjob.go deleted file mode 100644 index d8192024..00000000 --- a/internal/resource/cronjob.go +++ /dev/null @@ -1,121 +0,0 @@ -package resource - -import ( - "fmt" - "strconv" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - batchv1beta1 "k8s.io/api/batch/v1beta1" -) - -type ( - // CronJob tracks a kubernetes resource. - CronJob struct { - *Base - instance *batchv1beta1.CronJob - } - - // Runner can run jobs. - Runner interface { - Run(path string) error - } - - // Runnable can run jobs. - Runnable interface { - Run(ns, n string) error - } -) - -// NewCronJobList returns a new resource list. -func NewCronJobList(c Connection, ns string) List { - return NewList( - ns, - "cronjob", - NewCronJob(c), - AllVerbsAccess|DescribeAccess, - ) -} - -// NewCronJob instantiates a new CronJob. -func NewCronJob(c Connection) *CronJob { - cj := &CronJob{&Base{Connection: c, Resource: k8s.NewCronJob(c)}, nil} - cj.Factory = cj - - return cj -} - -// New builds a new CronJob instance from a k8s resource. -func (r *CronJob) New(i interface{}) Columnar { - c := NewCronJob(r.Connection) - switch instance := i.(type) { - case *batchv1beta1.CronJob: - c.instance = instance - case batchv1beta1.CronJob: - c.instance = &instance - default: - log.Fatal().Msgf("unknown CronJob type %#v", i) - } - c.path = c.namespacedName(c.instance.ObjectMeta) - - return c -} - -// Marshal resource to yaml. -func (r *CronJob) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - - cj := i.(*batchv1beta1.CronJob) - cj.TypeMeta.APIVersion = "extensions/batchv1beta1" - cj.TypeMeta.Kind = "CronJob" - - return r.marshalObject(cj) -} - -// Run a given cronjob. -func (r *CronJob) Run(pa string) error { - ns, n := Namespaced(pa) - if c, ok := r.Resource.(Runnable); ok { - return c.Run(ns, n) - } - - return fmt.Errorf("unable to run cronjob %s", pa) -} - -// Header return resource header. -func (*CronJob) Header(ns string) Row { - hh := Row{} - if ns == AllNamespaces { - hh = append(hh, "NAMESPACE") - } - - return append(hh, "NAME", "SCHEDULE", "SUSPEND", "ACTIVE", "LAST_SCHEDULE", "AGE") -} - -// Fields retrieves displayable fields. -func (r *CronJob) Fields(ns string) Row { - ff := make([]string, 0, len(r.Header(ns))) - - i := r.instance - if ns == AllNamespaces { - ff = append(ff, i.Namespace) - } - - lastScheduled := MissingValue - if i.Status.LastScheduleTime != nil { - lastScheduled = toAgeHuman(toAge(*i.Status.LastScheduleTime)) - } - - return append(ff, - i.Name, - i.Spec.Schedule, - boolPtrToStr(i.Spec.Suspend), - strconv.Itoa(len(i.Status.Active)), - lastScheduled, - toAge(i.ObjectMeta.CreationTimestamp), - ) -} diff --git a/internal/resource/cronjob_test.go b/internal/resource/cronjob_test.go deleted file mode 100644 index ff9e6c58..00000000 --- a/internal/resource/cronjob_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package resource_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - m "github.com/petergtz/pegomock" - "github.com/stretchr/testify/assert" - batchv1beta1 "k8s.io/api/batch/v1beta1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func NewCronJobListWithArgs(ns string, r *resource.CronJob) resource.List { - return resource.NewList(ns, "cj", r, resource.AllVerbsAccess|resource.DescribeAccess) -} - -func NewCronJobWithArgs(conn k8s.Connection, res resource.Cruder) *resource.CronJob { - r := &resource.CronJob{Base: resource.NewBase(conn, res)} - r.Factory = r - return r -} - -func TestCronJobListAccess(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - - ns := "blee" - r := NewCronJobWithArgs(mc, mr) - l := NewCronJobListWithArgs(resource.AllNamespaces, r) - l.SetNamespace(ns) - - assert.Equal(t, ns, l.GetNamespace()) - assert.Equal(t, "cj", l.GetName()) - for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} { - assert.True(t, l.Access(a)) - } -} - -func TestCronJobFields(t *testing.T) { - r := newCronJob().Fields("blee") - assert.Equal(t, "fred", r[0]) -} - -func TestCronJobMarshal(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("blee", "fred")).ThenReturn(k8sCronJob(), nil) - - cm := NewCronJobWithArgs(mc, mr) - ma, err := cm.Marshal("blee/fred") - mr.VerifyWasCalledOnce().Get("blee", "fred") - assert.Nil(t, err) - assert.Equal(t, cronjobYaml(), ma) -} - -func TestCronJobListData(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.List(resource.NotNamespaced, metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sCronJob()}, nil) - - l := NewCronJobListWithArgs("-", NewCronJobWithArgs(mc, mr)) - // Make sure we can get deltas! - for i := 0; i < 2; i++ { - err := l.Reconcile(nil, nil) - assert.Nil(t, err) - } - - mr.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced, metav1.ListOptions{}) - td := l.Data() - assert.Equal(t, 1, len(td.Rows)) - assert.Equal(t, resource.NotNamespaced, l.GetNamespace()) - row := td.Rows["blee/fred"] - assert.Equal(t, 6, len(row.Deltas)) - for _, d := range row.Deltas { - assert.Equal(t, "", d) - } - assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) -} - -// Helpers... - -func k8sCronJob() *batchv1beta1.CronJob { - var b bool - return &batchv1beta1.CronJob{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "blee", - Name: "fred", - CreationTimestamp: metav1.Time{Time: testTime()}, - }, - Spec: batchv1beta1.CronJobSpec{ - Schedule: "*/1 * * * *", - Suspend: &b, - }, - Status: batchv1beta1.CronJobStatus{ - LastScheduleTime: &metav1.Time{Time: testTime()}, - }, - } -} - -func newCronJob() resource.Columnar { - mc := NewMockConnection() - return resource.NewCronJob(mc).New(k8sCronJob()) -} - -func cronjobYaml() string { - return `apiVersion: extensions/batchv1beta1 -kind: CronJob -metadata: - creationTimestamp: "2018-12-14T17:36:43Z" - name: fred - namespace: blee -spec: - jobTemplate: - metadata: - creationTimestamp: null - spec: - template: - metadata: - creationTimestamp: null - spec: - containers: null - schedule: '*/1 * * * *' - suspend: false -status: - lastScheduleTime: "2018-12-14T17:36:43Z" -` -} diff --git a/internal/resource/custom.go b/internal/resource/custom.go deleted file mode 100644 index dd23f8ff..00000000 --- a/internal/resource/custom.go +++ /dev/null @@ -1,163 +0,0 @@ -package resource - -import ( - "encoding/json" - "errors" - "fmt" - "path" - "strings" - - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - "gopkg.in/yaml.v2" - metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" -) - -// Custom tracks a kubernetes resource. -type Custom struct { - *Base - - instance *metav1beta1.TableRow - gvr k8s.GVR - headers Row -} - -// NewCustomList returns a new resource list. -func NewCustomList(c k8s.Connection, namespaced bool, ns, gvr string) List { - if !namespaced { - ns = NotNamespaced - } - g := k8s.GVR(gvr) - return NewList( - ns, - g.ToR(), - NewCustom(c, g), AllVerbsAccess|DescribeAccess, - ) -} - -// NewCustom instantiates a new Kubernetes Resource. -func NewCustom(c k8s.Connection, gvr k8s.GVR) *Custom { - cr := &Custom{Base: &Base{Connection: c, Resource: k8s.NewResource(c, gvr)}} - cr.Factory = cr - cr.gvr = gvr - - return cr -} - -// New builds a new Custom instance from a k8s resource. -func (r *Custom) New(i interface{}) Columnar { - cr := NewCustom(r.Connection, "") - switch instance := i.(type) { - case *metav1beta1.TableRow: - cr.instance = instance - case metav1beta1.TableRow: - cr.instance = &instance - default: - log.Fatal().Msgf("Unknown %#v", i) - } - var obj map[string]interface{} - err := json.Unmarshal(cr.instance.Object.Raw, &obj) - if err != nil { - log.Error().Err(err) - } - meta := obj["metadata"].(map[string]interface{}) - ns := "" - if n, ok := meta["namespace"]; ok { - ns = n.(string) - } - name := meta["name"].(string) - cr.path = path.Join(ns, name) - cr.gvr = k8s.NewGVR(obj["kind"].(string), obj["apiVersion"].(string), name) - - return cr -} - -// Marshal resource to yaml. -func (r *Custom) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - switch v := i.(type) { - case *unstructured.Unstructured: - i = v.Object - } - - raw, err := yaml.Marshal(i) - if err != nil { - return "", err - } - - return string(raw), nil -} - -// List all resources -func (r *Custom) List(ns string, opts v1.ListOptions) (Columnars, error) { - ii, err := r.Resource.List(ns, opts) - if err != nil { - return nil, err - } - - if len(ii) == 0 { - return Columnars{}, errors.New("no resources found") - } - - table := ii[0].(*metav1beta1.Table) - r.headers = make(Row, len(table.ColumnDefinitions)) - for i, h := range table.ColumnDefinitions { - r.headers[i] = h.Name - } - rows := table.Rows - cc := make(Columnars, 0, len(rows)) - for i := 0; i < len(rows); i++ { - cc = append(cc, r.New(rows[i])) - } - - return cc, nil -} - -// Header return resource header. -func (r *Custom) Header(ns string) Row { - hh := make(Row, 0, len(r.headers)+1) - - if ns == AllNamespaces { - hh = append(hh, "NAMESPACE") - } - for _, h := range r.headers { - hh = append(hh, strings.ToUpper(h)) - } - - return hh -} - -// Fields retrieves displayable fields. -func (r *Custom) Fields(ns string) Row { - ff := make(Row, 0, len(r.Header(ns))) - - var obj map[string]interface{} - err := json.Unmarshal(r.instance.Object.Raw, &obj) - if err != nil { - log.Error().Err(err) - return Row{} - } - - meta := obj["metadata"].(map[string]interface{}) - rns, ok := meta["namespace"].(string) - - if ns == AllNamespaces { - if ok { - ff = append(ff, rns) - } - } - - for _, c := range r.instance.Cells { - ff = append(ff, fmt.Sprintf("%v", c)) - } - - return ff -} diff --git a/internal/resource/custom_test.go b/internal/resource/custom_test.go deleted file mode 100644 index 852dba12..00000000 --- a/internal/resource/custom_test.go +++ /dev/null @@ -1,351 +0,0 @@ -package resource_test - -import ( - "testing" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - m "github.com/petergtz/pegomock" - "github.com/stretchr/testify/assert" - metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" - "k8s.io/apimachinery/pkg/runtime" -) - -func NewCustomListWithArgs(ns, name string, r *resource.Custom) resource.List { - return resource.NewList(ns, name, r, resource.AllVerbsAccess) -} - -func NewCustomWithArgs(conn k8s.Connection, res resource.Cruder) *resource.Custom { - r := &resource.Custom{Base: resource.NewBase(conn, res)} - r.Factory = r - return r -} - -func TestCustomListAccess(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - - ns := "blee" - r := NewCustomWithArgs(mc, mr) - l := NewCustomListWithArgs(resource.AllNamespaces, "fred", r) - l.SetNamespace(ns) - - assert.Equal(t, ns, l.GetNamespace()) - assert.Equal(t, "fred", l.GetName()) - for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} { - assert.True(t, l.Access(a)) - } -} - -func TestCustomFields(t *testing.T) { - r := newCustom().Fields("blee") - assert.Equal(t, "a", r[0]) -} - -func TestCustomMarshal(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("blee", "fred")).ThenReturn(k8sCustomTable(), nil) - - cm := NewCustomWithArgs(mc, mr) - ma, err := cm.Marshal("blee/fred") - mr.VerifyWasCalledOnce().Get("blee", "fred") - - assert.Nil(t, err) - assert.Equal(t, customYaml(), ma) -} - -func TestCustomMarshalWithUnstructured(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("blee", "fred")).ThenReturn(k8sUnstructured(), nil) - - cm := NewCustomWithArgs(mc, mr) - ma, err := cm.Marshal("blee/fred") - mr.VerifyWasCalledOnce().Get("blee", "fred") - - assert.Nil(t, err) - assert.Equal(t, unstructuredYAML(), ma) -} - -func TestCustomListData(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.List("blee", metav1.ListOptions{})).ThenReturn(k8s.Collection{k8sCustomTable()}, nil) - - l := NewCustomListWithArgs("blee", "fred", NewCustomWithArgs(mc, mr)) - // Make sure we can get deltas! - for i := 0; i < 2; i++ { - err := l.Reconcile(nil, nil) - assert.Nil(t, err) - } - - mr.VerifyWasCalled(m.Times(2)).List("blee", metav1.ListOptions{}) - td := l.Data() - assert.Equal(t, 1, len(td.Rows)) - assert.Equal(t, "blee", l.GetNamespace()) - row := td.Rows["blee/fred"] - assert.Equal(t, 3, len(row.Deltas)) - for _, d := range row.Deltas { - assert.Equal(t, "", d) - } - assert.Equal(t, resource.Row{"a"}, row.Fields[:1]) -} - -// Helpers... - -func k8sCustomTable() *metav1beta1.Table { - return &metav1beta1.Table{ - ColumnDefinitions: []metav1beta1.TableColumnDefinition{ - {Name: "A"}, - {Name: "B"}, - {Name: "C"}, - }, - Rows: []metav1beta1.TableRow{ - { - Object: runtime.RawExtension{ - Raw: []byte(`{ - "kind": "fred", - "apiVersion": "v1", - "metadata": { - "namespace": "blee", - "name": "fred" - }}`), - }, - Cells: []interface{}{ - "a", - "b", - "c", - }, - }, - }, - } -} - -func k8sUnstructured() *unstructured.Unstructured { - return &unstructured.Unstructured{ - Object: map[string]interface{}{ - "kind": "fred", - "apiVersion": "v1", - "metadata": map[string]interface{}{ - "namespace": "blee", - "name": "fred", - }, - }, - } -} - -func unstructuredYAML() string { - return `apiVersion: v1 -kind: fred -metadata: - name: fred - namespace: blee -` -} - -func k8sCustomRow() *metav1beta1.TableRow { - return &metav1beta1.TableRow{ - Object: runtime.RawExtension{ - Raw: []byte(`{ - "kind": "fred", - "apiVersion": "v1", - "metadata": { - "namespace": "blee", - "name": "fred" - }}`), - }, - Cells: []interface{}{ - "a", - "b", - "c", - }, - } -} - -func newCustom() resource.Columnar { - mc := NewMockConnection() - return resource.NewCustom(mc, "g/v1/fred").New(k8sCustomRow()) -} - -func customYaml() string { - return `typemeta: - kind: "" - apiversion: "" -listmeta: - selflink: "" - resourceversion: "" - continue: "" - remainingitemcount: null -columndefinitions: -- name: A - type: "" - format: "" - description: "" - priority: 0 -- name: B - type: "" - format: "" - description: "" - priority: 0 -- name: C - type: "" - format: "" - description: "" - priority: 0 -rows: -- cells: - - a - - b - - c - conditions: [] - object: - raw: - - 123 - - 10 - - 32 - - 32 - - 32 - - 32 - - 32 - - 32 - - 32 - - 32 - - 34 - - 107 - - 105 - - 110 - - 100 - - 34 - - 58 - - 32 - - 34 - - 102 - - 114 - - 101 - - 100 - - 34 - - 44 - - 10 - - 32 - - 32 - - 32 - - 32 - - 32 - - 32 - - 32 - - 32 - - 34 - - 97 - - 112 - - 105 - - 86 - - 101 - - 114 - - 115 - - 105 - - 111 - - 110 - - 34 - - 58 - - 32 - - 34 - - 118 - - 49 - - 34 - - 44 - - 10 - - 32 - - 32 - - 32 - - 32 - - 32 - - 32 - - 32 - - 32 - - 34 - - 109 - - 101 - - 116 - - 97 - - 100 - - 97 - - 116 - - 97 - - 34 - - 58 - - 32 - - 123 - - 10 - - 32 - - 32 - - 32 - - 32 - - 32 - - 32 - - 32 - - 32 - - 32 - - 32 - - 34 - - 110 - - 97 - - 109 - - 101 - - 115 - - 112 - - 97 - - 99 - - 101 - - 34 - - 58 - - 32 - - 34 - - 98 - - 108 - - 101 - - 101 - - 34 - - 44 - - 10 - - 32 - - 32 - - 32 - - 32 - - 32 - - 32 - - 32 - - 32 - - 32 - - 32 - - 34 - - 110 - - 97 - - 109 - - 101 - - 34 - - 58 - - 32 - - 34 - - 102 - - 114 - - 101 - - 100 - - 34 - - 10 - - 32 - - 32 - - 32 - - 32 - - 32 - - 32 - - 32 - - 32 - - 125 - - 125 - object: null -` -} diff --git a/internal/resource/dp.go b/internal/resource/dp.go deleted file mode 100644 index b1fb76c9..00000000 --- a/internal/resource/dp.go +++ /dev/null @@ -1,140 +0,0 @@ -package resource - -import ( - "context" - "fmt" - "strconv" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - appsv1 "k8s.io/api/apps/v1" -) - -// Compile time checks to ensure type satisfies interface -var _ Restartable = (*Deployment)(nil) -var _ Scalable = (*Deployment)(nil) - -// Deployment tracks a kubernetes resource. -type Deployment struct { - *Base - instance *appsv1.Deployment -} - -// NewDeploymentList returns a new resource list. -func NewDeploymentList(c Connection, ns string) List { - return NewList( - ns, - "deploy", - NewDeployment(c), - AllVerbsAccess|DescribeAccess, - ) -} - -// NewDeployment instantiates a new Deployment. -func NewDeployment(c Connection) *Deployment { - d := &Deployment{&Base{Connection: c, Resource: k8s.NewDeployment(c)}, nil} - d.Factory = d - - return d -} - -// New builds a new Deployment instance from a k8s resource. -func (r *Deployment) New(i interface{}) Columnar { - c := NewDeployment(r.Connection) - switch instance := i.(type) { - case *appsv1.Deployment: - c.instance = instance - case appsv1.Deployment: - c.instance = &instance - default: - log.Fatal().Msgf("unknown Deployment type %#v", i) - } - c.path = c.namespacedName(c.instance.ObjectMeta) - - return c -} - -// Marshal resource to yaml. -func (r *Deployment) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - - dp := i.(*appsv1.Deployment) - dp.TypeMeta.APIVersion = "apps/v1" - dp.TypeMeta.Kind = "Deployment" - - return r.marshalObject(dp) -} - -// Logs tail logs for all pods represented by this deployment. -func (r *Deployment) Logs(ctx context.Context, c chan<- string, opts LogOptions) error { - instance, err := r.Resource.Get(opts.Namespace, opts.Name) - if err != nil { - return err - } - dp := instance.(*appsv1.Deployment) - if dp.Spec.Selector == nil || len(dp.Spec.Selector.MatchLabels) == 0 { - return fmt.Errorf("No valid selector found on deployment %s", opts.Name) - } - - return r.podLogs(ctx, c, dp.Spec.Selector.MatchLabels, opts) -} - -// Header return resource header. -func (*Deployment) Header(ns string) Row { - var hh Row - if ns == AllNamespaces { - hh = append(hh, "NAMESPACE") - } - - return append(hh, - "NAME", - "DESIRED", - "CURRENT", - "UP-TO-DATE", - "AVAILABLE", - "AGE", - ) -} - -// NumCols designates if column is numerical. -func (*Deployment) NumCols(n string) map[string]bool { - return map[string]bool{ - "DESIRED": true, - "CURRENT": true, - "UP-TO-DATE": true, - "AVAILABLE": true, - } -} - -// Fields retrieves displayable fields. -func (r *Deployment) Fields(ns string) Row { - ff := make([]string, 0, len(r.Header(ns))) - - i := r.instance - if ns == AllNamespaces { - ff = append(ff, i.Namespace) - } - - return append(ff, - i.Name, - strconv.Itoa(int(*i.Spec.Replicas)), - strconv.Itoa(int(i.Status.Replicas)), - strconv.Itoa(int(i.Status.UpdatedReplicas)), - strconv.Itoa(int(i.Status.AvailableReplicas)), - toAge(i.ObjectMeta.CreationTimestamp), - ) -} - -// Scale the specified resource. -func (r *Deployment) Scale(ns, n string, replicas int32) error { - return r.Resource.(Scalable).Scale(ns, n, replicas) -} - -// Restart the rollout of the specified resource. -func (r *Deployment) Restart(ns, n string) error { - return r.Resource.(Restartable).Restart(ns, n) -} diff --git a/internal/resource/dp_test.go b/internal/resource/dp_test.go deleted file mode 100644 index 7c6a69cf..00000000 --- a/internal/resource/dp_test.go +++ /dev/null @@ -1,120 +0,0 @@ -package resource_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - m "github.com/petergtz/pegomock" - "github.com/stretchr/testify/assert" - appsv1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func NewDeploymentListWithArgs(ns string, r *resource.Deployment) resource.List { - return resource.NewList(ns, "deploy", r, resource.AllVerbsAccess|resource.DescribeAccess) -} - -func NewDeploymentWithArgs(conn k8s.Connection, res resource.Cruder) *resource.Deployment { - r := &resource.Deployment{Base: resource.NewBase(conn, res)} - r.Factory = r - return r -} - -func TestDeploymentListAccess(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - - ns := "blee" - l := NewDeploymentListWithArgs(resource.AllNamespaces, NewDeploymentWithArgs(mc, mr)) - l.SetNamespace(ns) - - assert.Equal(t, "blee", l.GetNamespace()) - assert.Equal(t, "deploy", l.GetName()) - for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} { - assert.True(t, l.Access(a)) - } -} - -func TestDeploymentFields(t *testing.T) { - r := newDeployment().Fields("blee") - assert.Equal(t, "fred", r[0]) -} - -func TestDeploymentMarshal(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("blee", "fred")).ThenReturn(k8sDeployment(), nil) - - cm := NewDeploymentWithArgs(mc, mr) - ma, err := cm.Marshal("blee/fred") - - mr.VerifyWasCalledOnce().Get("blee", "fred") - assert.Nil(t, err) - assert.Equal(t, dpYaml(), ma) -} - -func TestDeploymentListData(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.List(resource.NotNamespaced, metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sDeployment()}, nil) - - l := NewDeploymentListWithArgs("-", NewDeploymentWithArgs(mc, mr)) - // Make sure we can get deltas! - for i := 0; i < 2; i++ { - err := l.Reconcile(nil, nil) - assert.Nil(t, err) - } - - mr.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced, metav1.ListOptions{}) - td := l.Data() - assert.Equal(t, 1, len(td.Rows)) - assert.Equal(t, resource.NotNamespaced, l.GetNamespace()) - row := td.Rows["blee/fred"] - assert.Equal(t, 6, len(row.Deltas)) - for _, d := range row.Deltas { - assert.Equal(t, "", d) - } - assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) -} - -// Helpers... - -func k8sDeployment() *appsv1.Deployment { - var i int32 = 1 - return &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "blee", - Name: "fred", - CreationTimestamp: metav1.Time{Time: testTime()}, - }, - Spec: appsv1.DeploymentSpec{ - Replicas: &i, - }, - } -} - -func newDeployment() resource.Columnar { - mc := NewMockConnection() - return resource.NewDeployment(mc).New(k8sDeployment()) -} - -func dpYaml() string { - return `apiVersion: apps/v1 -kind: Deployment -metadata: - creationTimestamp: "2018-12-14T17:36:43Z" - name: fred - namespace: blee -spec: - replicas: 1 - selector: null - strategy: {} - template: - metadata: - creationTimestamp: null - spec: - containers: null -status: {} -` -} diff --git a/internal/resource/ds.go b/internal/resource/ds.go deleted file mode 100644 index 31c40d7e..00000000 --- a/internal/resource/ds.go +++ /dev/null @@ -1,122 +0,0 @@ -package resource - -import ( - "context" - "fmt" - "strconv" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - appsv1 "k8s.io/api/apps/v1" -) - -// Compile time checks to ensure type satisfies interface -var _ Restartable = (*DaemonSet)(nil) - -// DaemonSet tracks a kubernetes resource. -type DaemonSet struct { - *Base - instance *appsv1.DaemonSet -} - -// NewDaemonSetList returns a new resource list. -func NewDaemonSetList(c Connection, ns string) List { - return NewList( - ns, - "ds", - NewDaemonSet(c), - AllVerbsAccess|DescribeAccess, - ) -} - -// NewDaemonSet instantiates a new DaemonSet. -func NewDaemonSet(c Connection) *DaemonSet { - ds := &DaemonSet{&Base{Connection: c, Resource: k8s.NewDaemonSet(c)}, nil} - ds.Factory = ds - - return ds -} - -// New builds a new DaemonSet instance from a k8s resource. -func (r *DaemonSet) New(i interface{}) Columnar { - c := NewDaemonSet(r.Connection) - switch instance := i.(type) { - case *appsv1.DaemonSet: - c.instance = instance - case appsv1.DaemonSet: - c.instance = &instance - default: - log.Fatal().Msgf("unknown DaemonSet type %#v", i) - } - c.path = c.namespacedName(c.instance.ObjectMeta) - - return c -} - -// Marshal resource to yaml. -func (r *DaemonSet) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - - ds := i.(*appsv1.DaemonSet) - ds.TypeMeta.APIVersion = "apps/v1" - ds.TypeMeta.Kind = "DaemonSet" - - return r.marshalObject(ds) -} - -// Logs tail logs for all pods represented by this DaemonSet. -func (r *DaemonSet) Logs(ctx context.Context, c chan<- string, opts LogOptions) error { - instance, err := r.Resource.Get(opts.Namespace, opts.Name) - if err != nil { - return err - } - - ds := instance.(*appsv1.DaemonSet) - if ds.Spec.Selector == nil || len(ds.Spec.Selector.MatchLabels) == 0 { - return fmt.Errorf("No valid selector found on daemonset %s", opts.FQN()) - } - - return r.podLogs(ctx, c, ds.Spec.Selector.MatchLabels, opts) -} - -// Header return resource header. -func (*DaemonSet) Header(ns string) Row { - hh := Row{} - if ns == AllNamespaces { - hh = append(hh, "NAMESPACE") - } - hh = append(hh, "NAME", "DESIRED", "CURRENT", "READY", "UP-TO-DATE") - hh = append(hh, "AVAILABLE", "NODE_SELECTOR", "AGE") - - return hh -} - -// Fields retrieves displayable fields. -func (r *DaemonSet) Fields(ns string) Row { - ff := make([]string, 0, len(r.Header(ns))) - - i := r.instance - if ns == AllNamespaces { - ff = append(ff, i.Namespace) - } - - return append(ff, - i.Name, - strconv.Itoa(int(i.Status.DesiredNumberScheduled)), - strconv.Itoa(int(i.Status.CurrentNumberScheduled)), - strconv.Itoa(int(i.Status.NumberReady)), - strconv.Itoa(int(i.Status.UpdatedNumberScheduled)), - strconv.Itoa(int(i.Status.NumberAvailable)), - mapToStr(i.Spec.Template.Spec.NodeSelector), - toAge(i.ObjectMeta.CreationTimestamp), - ) -} - -// Restart the rollout of the specified resource. -func (r *DaemonSet) Restart(ns, n string) error { - return r.Resource.(Restartable).Restart(ns, n) -} diff --git a/internal/resource/ds_test.go b/internal/resource/ds_test.go deleted file mode 100644 index 48b11af7..00000000 --- a/internal/resource/ds_test.go +++ /dev/null @@ -1,132 +0,0 @@ -package resource_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - m "github.com/petergtz/pegomock" - "github.com/stretchr/testify/assert" - appsv1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func NewDaemonSetListWithArgs(ns string, r *resource.DaemonSet) resource.List { - return resource.NewList(ns, "ds", r, resource.AllVerbsAccess|resource.DescribeAccess) -} - -func NewDaemonSetWithArgs(conn k8s.Connection, res resource.Cruder) *resource.DaemonSet { - r := &resource.DaemonSet{Base: resource.NewBase(conn, res)} - r.Factory = r - return r -} - -func TestDSListAccess(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - - ns := "blee" - l := NewDaemonSetListWithArgs(resource.AllNamespaces, NewDaemonSetWithArgs(mc, mr)) - l.SetNamespace(ns) - - assert.Equal(t, "blee", l.GetNamespace()) - assert.Equal(t, "ds", l.GetName()) - for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} { - assert.True(t, l.Access(a)) - } -} - -func TestDSFields(t *testing.T) { - r := newDS().Fields("blee") - assert.Equal(t, "fred", r[0]) -} - -func TestDSMarshal(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("blee", "fred")).ThenReturn(k8sDS(), nil) - - cm := NewDaemonSetWithArgs(mc, mr) - ma, err := cm.Marshal("blee/fred") - mr.VerifyWasCalledOnce().Get("blee", "fred") - assert.Nil(t, err) - assert.Equal(t, dsYaml(), ma) -} - -func TestDSListData(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.List("blee", metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sDS()}, nil) - - l := NewDaemonSetListWithArgs("blee", NewDaemonSetWithArgs(mc, mr)) - // Make sure we can get deltas! - for i := 0; i < 2; i++ { - err := l.Reconcile(nil, nil) - assert.Nil(t, err) - } - - mr.VerifyWasCalled(m.Times(2)).List("blee", metav1.ListOptions{}) - td := l.Data() - assert.Equal(t, 1, len(td.Rows)) - assert.Equal(t, "blee", l.GetNamespace()) - row := td.Rows["blee/fred"] - assert.Equal(t, 8, len(row.Deltas)) - for _, d := range row.Deltas { - assert.Equal(t, "", d) - } - assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) -} - -// Helpers... - -func k8sDS() *appsv1.DaemonSet { - return &appsv1.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "blee", - Name: "fred", - CreationTimestamp: metav1.Time{Time: testTime()}, - }, - Spec: appsv1.DaemonSetSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"fred": "blee"}, - }, - }, - Status: appsv1.DaemonSetStatus{ - DesiredNumberScheduled: 1, - CurrentNumberScheduled: 1, - NumberReady: 1, - NumberAvailable: 1, - }, - } -} - -func newDS() resource.Columnar { - mc := NewMockConnection() - return resource.NewDaemonSet(mc).New(k8sDS()) -} - -func dsYaml() string { - return `apiVersion: apps/v1 -kind: DaemonSet -metadata: - creationTimestamp: "2018-12-14T17:36:43Z" - name: fred - namespace: blee -spec: - selector: - matchLabels: - fred: blee - template: - metadata: - creationTimestamp: null - spec: - containers: null - updateStrategy: {} -status: - currentNumberScheduled: 1 - desiredNumberScheduled: 1 - numberAvailable: 1 - numberMisscheduled: 0 - numberReady: 1 -` -} diff --git a/internal/resource/ep.go b/internal/resource/ep.go deleted file mode 100644 index 2cbefff0..00000000 --- a/internal/resource/ep.go +++ /dev/null @@ -1,130 +0,0 @@ -package resource - -import ( - "strconv" - "strings" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - v1 "k8s.io/api/core/v1" -) - -// Endpoints tracks a kubernetes resource. -type Endpoints struct { - *Base - instance *v1.Endpoints -} - -// NewEndpointsList returns a new resource list. -func NewEndpointsList(c Connection, ns string) List { - return NewList( - ns, - "ep", - NewEndpoints(c), - AllVerbsAccess|DescribeAccess, - ) -} - -// NewEndpoints instantiates a new Endpoints. -func NewEndpoints(c Connection) *Endpoints { - ep := &Endpoints{&Base{Connection: c, Resource: k8s.NewEndpoints(c)}, nil} - ep.Factory = ep - - return ep -} - -// New builds a new Endpoints instance from a k8s resource. -func (r *Endpoints) New(i interface{}) Columnar { - c := NewEndpoints(r.Connection) - switch instance := i.(type) { - case *v1.Endpoints: - c.instance = instance - case v1.Endpoints: - c.instance = &instance - default: - log.Fatal().Msgf("unknown Endpoints type %#v", i) - } - c.path = c.namespacedName(c.instance.ObjectMeta) - - return c -} - -// Marshal resource to yaml. -func (r *Endpoints) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - - ep := i.(*v1.Endpoints) - ep.TypeMeta.APIVersion = "v1" - ep.TypeMeta.Kind = "Endpoint" - - return r.marshalObject(ep) -} - -// Header return resource header. -func (*Endpoints) Header(ns string) Row { - hh := Row{} - if ns == AllNamespaces { - hh = append(hh, "NAMESPACE") - } - - return append(hh, "NAME", "ENDPOINTS", "AGE") -} - -// Fields retrieves displayable fields. -func (r *Endpoints) Fields(ns string) Row { - ff := make(Row, 0, len(r.Header(ns))) - i := r.instance - if ns == AllNamespaces { - ff = append(ff, i.Namespace) - } - - return append(ff, - i.Name, - missing(r.toEPs(i.Subsets)), - toAge(i.ObjectMeta.CreationTimestamp), - ) -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func (r *Endpoints) toEPs(ss []v1.EndpointSubset) string { - aa := make([]string, 0, len(ss)) - for _, s := range ss { - pp := make([]string, len(s.Ports)) - portsToStrs(s.Ports, pp) - a := make([]string, len(s.Addresses)) - proccessIPs(a, pp, s.Addresses) - aa = append(aa, strings.Join(a, ",")) - } - return strings.Join(aa, ",") -} - -func portsToStrs(pp []v1.EndpointPort, ss []string) { - for i := 0; i < len(pp); i++ { - ss[i] = strconv.Itoa(int(pp[i].Port)) - } -} - -func proccessIPs(aa []string, pp []string, addrs []v1.EndpointAddress) { - const maxIPs = 3 - var i int - for _, a := range addrs { - if len(a.IP) == 0 { - continue - } - if len(pp) == 0 { - aa[i], i = a.IP, i+1 - continue - } - if len(pp) > maxIPs { - aa[i], i = a.IP+":"+strings.Join(pp[:maxIPs], ",")+"...", i+1 - } else { - aa[i], i = a.IP+":"+strings.Join(pp, ","), i+1 - } - } -} diff --git a/internal/resource/ep_test.go b/internal/resource/ep_test.go deleted file mode 100644 index 9ff3eb83..00000000 --- a/internal/resource/ep_test.go +++ /dev/null @@ -1,119 +0,0 @@ -package resource_test - -import ( - "testing" - - // "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - m "github.com/petergtz/pegomock" - - // 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 NewEndpointsListWithArgs(ns string, r *resource.Endpoints) resource.List { - return resource.NewList(ns, "ep", r, resource.AllVerbsAccess|resource.DescribeAccess) -} - -func NewEndpointsWithArgs(conn k8s.Connection, res resource.Cruder) *resource.Endpoints { - r := &resource.Endpoints{Base: resource.NewBase(conn, res)} - r.Factory = r - return r -} - -func TestEndpointsListAccess(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - - ns := "blee" - l := NewEndpointsListWithArgs(resource.AllNamespaces, NewEndpointsWithArgs(mc, mr)) - l.SetNamespace(ns) - - assert.Equal(t, "blee", l.GetNamespace()) - assert.Equal(t, "ep", l.GetName()) - for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} { - assert.True(t, l.Access(a)) - } -} - -func TestEndpointsMarshal(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("blee", "fred")).ThenReturn(k8sEndpoints(), nil) - - cm := NewEndpointsWithArgs(mc, mr) - ma, err := cm.Marshal("blee/fred") - mr.VerifyWasCalledOnce().Get("blee", "fred") - assert.Nil(t, err) - assert.Equal(t, epYaml(), ma) -} - -func TestEndpointsListData(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.List(resource.NotNamespaced, metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sEndpoints()}, nil) - - l := NewEndpointsListWithArgs("-", NewEndpointsWithArgs(mc, mr)) - // Make sure we mrn get deltas! - for i := 0; i < 2; i++ { - err := l.Reconcile(nil, nil) - assert.Nil(t, err) - } - - mr.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced, metav1.ListOptions{}) - td := l.Data() - assert.Equal(t, 1, len(td.Rows)) - assert.Equal(t, resource.NotNamespaced, l.GetNamespace()) - row := td.Rows["blee/fred"] - assert.Equal(t, 3, len(row.Deltas)) - for _, d := range row.Deltas { - assert.Equal(t, "", d) - } - assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) -} - -// Helpers... - -func k8sEndpoints() *v1.Endpoints { - return &v1.Endpoints{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "blee", - Name: "fred", - CreationTimestamp: metav1.Time{Time: testTime()}, - }, - Subsets: []v1.EndpointSubset{ - { - Addresses: []v1.EndpointAddress{ - {IP: "1.1.1.1"}, - }, - Ports: []v1.EndpointPort{ - {Port: 80, Protocol: "TCP"}, - }, - }, - }, - } -} - -func newEndpoints() resource.Columnar { - mc := NewMockConnection() - return resource.NewEndpoints(mc).New(k8sEndpoints()) -} - -func epYaml() string { - return `apiVersion: v1 -kind: Endpoint -metadata: - creationTimestamp: "2018-12-14T17:36:43Z" - name: fred - namespace: blee -subsets: -- addresses: - - ip: 1.1.1.1 - ports: - - port: 80 - protocol: TCP -` -} diff --git a/internal/resource/evt.go b/internal/resource/evt.go deleted file mode 100644 index 41c3d3f6..00000000 --- a/internal/resource/evt.go +++ /dev/null @@ -1,127 +0,0 @@ -package resource - -import ( - "regexp" - "strconv" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - v1 "k8s.io/api/core/v1" -) - -// Event tracks a kubernetes resource. -type Event struct { - *Base - instance *v1.Event -} - -// NewEventList returns a new resource list. -func NewEventList(c Connection, ns string) List { - return NewList( - ns, - "ev", - NewEvent(c), - ListAccess+NamespaceAccess, - ) -} - -// NewEvent instantiates a new Event. -func NewEvent(c Connection) *Event { - ev := &Event{&Base{Connection: c, Resource: k8s.NewEvent(c)}, nil} - ev.Factory = ev - - return ev -} - -// New builds a new Event instance from a k8s resource. -func (r *Event) New(i interface{}) Columnar { - c := NewEvent(r.Connection) - switch instance := i.(type) { - case *v1.Event: - c.instance = instance - case v1.Event: - c.instance = &instance - default: - log.Fatal().Msgf("unknown Event type %#v", i) - } - c.path = c.namespacedName(c.instance.ObjectMeta) - - return c -} - -// Marshal resource to yaml. -func (r *Event) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - - ev := i.(*v1.Event) - ev.TypeMeta.APIVersion = "v1" - ev.TypeMeta.Kind = "Event" - - return r.marshalObject(ev) -} - -// Delete a resource by name. -func (r *Event) Delete(path string, cascade, force bool) error { - return nil -} - -// Header return resource header. -func (*Event) Header(ns string) Row { - var ff Row - if ns == AllNamespaces { - ff = append(ff, "NAMESPACE") - } - - return append(ff, "NAME", "REASON", "SOURCE", "COUNT", "MESSAGE", "AGE") -} - -var rx = regexp.MustCompile(`(.+)\.(.+)`) - -// Fields returns display fields. -func (r *Event) Fields(ns string) Row { - ff := make(Row, 0, len(r.Header(ns))) - i := r.instance - - if ns == AllNamespaces { - ff = append(ff, i.Namespace) - } - - return append(ff, - i.Name, - i.Reason, - i.Source.Component, - strconv.Itoa(int(i.Count)), - Truncate(i.Message, 80), - toAge(i.LastTimestamp), - ) -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func (*Event) toEmoji(t, r string) string { - switch t { - case "Warning": - switch r { - case "Failed": - return "😡" - case "Killing": - return "👿" - default: - return "😡" - } - default: - switch r { - case "Killing": - return "👿" - case "BackOff": - return "👹" - default: - return "😮" - } - } -} diff --git a/internal/resource/evt_test.go b/internal/resource/evt_test.go deleted file mode 100644 index 6d0ec0cf..00000000 --- a/internal/resource/evt_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package resource_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - 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 NewEventListWithArgs(ns string, r *resource.Event) resource.List { - return resource.NewList(ns, "ev", r, resource.AllVerbsAccess|resource.DescribeAccess) -} - -func NewEventWithArgs(conn k8s.Connection, res resource.Cruder) *resource.Event { - r := &resource.Event{Base: resource.NewBase(conn, res)} - r.Factory = r - return r -} - -func TestEventAccess(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - - ns := "blee" - l := NewEventListWithArgs(resource.AllNamespaces, NewEventWithArgs(mc, mr)) - l.SetNamespace(ns) - - assert.Equal(t, "blee", l.GetNamespace()) - assert.Equal(t, "ev", l.GetName()) - for _, a := range []int{resource.ListAccess, resource.NamespaceAccess} { - assert.True(t, l.Access(a)) - } -} - -func TestEventFields(t *testing.T) { - r := newEvent().Fields("blee") - assert.Equal(t, resource.Row{"fred", "blah", "", "1"}, r[:4]) -} - -func TestEventMarshal(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("blee", "fred")).ThenReturn(k8sEvent(), nil) - - cm := NewEventWithArgs(mc, mr) - ma, err := cm.Marshal("blee/fred") - mr.VerifyWasCalledOnce().Get("blee", "fred") - assert.Nil(t, err) - assert.Equal(t, evYaml(), ma) -} - -func TestEventData(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.List("blee", metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sEvent()}, nil) - - l := NewEventListWithArgs("blee", NewEventWithArgs(mc, mr)) - // Make sure we mrn get deltas! - for i := 0; i < 2; i++ { - err := l.Reconcile(nil, nil) - assert.Nil(t, err) - } - - mr.VerifyWasCalled(m.Times(2)).List("blee", metav1.ListOptions{}) - td := l.Data() - assert.Equal(t, 1, len(td.Rows)) - assert.Equal(t, "blee", l.GetNamespace()) - row := td.Rows["blee/fred"] - assert.Equal(t, 6, len(row.Deltas)) - for _, d := range row.Deltas { - assert.Equal(t, "", d) - } - assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) -} - -// Helpers... - -func k8sEvent() *v1.Event { - return &v1.Event{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "blee", - Name: "fred", - CreationTimestamp: metav1.Time{Time: testTime()}, - }, - Reason: "blah", - Message: "blee", - Count: 1, - } -} - -func newEvent() resource.Columnar { - mc := NewMockConnection() - return resource.NewEvent(mc).New(k8sEvent()) -} - -func evYaml() string { - return `apiVersion: v1 -count: 1 -eventTime: null -firstTimestamp: null -involvedObject: {} -kind: Event -lastTimestamp: null -message: blee -metadata: - creationTimestamp: "2018-12-14T17:36:43Z" - name: fred - namespace: blee -reason: blah -reportingComponent: "" -reportingInstance: "" -source: {} -` -} diff --git a/internal/resource/helpers_test.go b/internal/resource/helpers_test.go deleted file mode 100644 index 904ebaed..00000000 --- a/internal/resource/helpers_test.go +++ /dev/null @@ -1,193 +0,0 @@ -package resource - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestJoin(t *testing.T) { - uu := map[string]struct { - i []string - e string - }{ - "zero": {[]string{}, ""}, - "std": {[]string{"a", "b", "c"}, "a,b,c"}, - "blank": {[]string{"", "", ""}, ""}, - "sparse": {[]string{"a", "", "c"}, "a,c"}, - } - - for k, v := range uu { - t.Run(k, func(t *testing.T) { - assert.Equal(t, v.e, join(v.i, ",")) - }) - } -} - -func TestBoolPtrToStr(t *testing.T) { - tv, fv := true, false - - uu := []struct { - p *bool - e string - }{ - {nil, "false"}, - {&tv, "true"}, - {&fv, "false"}, - } - - for _, u := range uu { - assert.Equal(t, u.e, boolPtrToStr(u.p)) - } -} - -func TestNamespaced(t *testing.T) { - uu := []struct { - p, ns, n string - }{ - {"fred/blee", "fred", "blee"}, - } - - for _, u := range uu { - ns, n := Namespaced(u.p) - assert.Equal(t, u.ns, ns) - assert.Equal(t, u.n, n) - } -} - -func TestMissing(t *testing.T) { - uu := []struct { - i, e string - }{ - {"fred", "fred"}, - {"", MissingValue}, - } - - for _, u := range uu { - assert.Equal(t, u.e, missing(u.i)) - } -} - -func TestBoolToStr(t *testing.T) { - uu := []struct { - i bool - e string - }{ - {true, "true"}, - {false, "false"}, - } - - for _, u := range uu { - assert.Equal(t, u.e, boolToStr(u.i)) - } -} - -func TestNa(t *testing.T) { - uu := []struct { - i, e string - }{ - {"fred", "fred"}, - {"", NAValue}, - } - - for _, u := range uu { - assert.Equal(t, u.e, na(u.i)) - } -} - -func TestTruncate(t *testing.T) { - uu := []struct { - s string - l int - e string - }{ - {"fred", 3, "fr…"}, - {"fred", 2, "f…"}, - {"fred", 10, "fred"}, - } - - for _, u := range uu { - assert.Equal(t, u.e, Truncate(u.s, u.l)) - } -} - -func TestMapToStr(t *testing.T) { - uu := []struct { - i map[string]string - e string - }{ - {map[string]string{"blee": "duh", "aa": "bb"}, "aa=bb,blee=duh"}, - {map[string]string{}, MissingValue}, - } - for _, u := range uu { - assert.Equal(t, u.e, mapToStr(u.i)) - } -} - -func BenchmarkMapToStr(b *testing.B) { - ll := map[string]string{ - "blee": "duh", - "aa": "bb", - } - b.ResetTimer() - b.ReportAllocs() - - for i := 0; i < b.N; i++ { - mapToStr(ll) - } -} - -func TestToMillicore(t *testing.T) { - uu := []struct { - v int64 - e string - }{ - {0, "0"}, - {2, "2"}, - {1000, "1000"}, - } - - for _, u := range uu { - assert.Equal(t, u.e, ToMillicore(u.v)) - } -} - -func TestToMi(t *testing.T) { - uu := []struct { - v float64 - e string - }{ - {0, "0"}, - {2, "2"}, - {1000, "1000"}, - } - - for _, u := range uu { - assert.Equal(t, u.e, ToMi(u.v)) - } -} - -func TestAsPerc(t *testing.T) { - uu := []struct { - v float64 - e string - }{ - {0, "0"}, - {10.5, "10"}, - {10, "10"}, - {0.05, "0"}, - } - - for _, u := range uu { - assert.Equal(t, u.e, AsPerc(u.v)) - } -} - -func BenchmarkAsPerc(b *testing.B) { - v := 10.5 - b.ResetTimer() - b.ReportAllocs() - for n := 0; n < b.N; n++ { - AsPerc(v) - } -} diff --git a/internal/resource/hpa_v1.go b/internal/resource/hpa_v1.go deleted file mode 100644 index ccbe1d3e..00000000 --- a/internal/resource/hpa_v1.go +++ /dev/null @@ -1,117 +0,0 @@ -package resource - -import ( - "strconv" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - autoscalingv1 "k8s.io/api/autoscaling/v1" -) - -// HorizontalPodAutoscalerV1 tracks a kubernetes resource. -type HorizontalPodAutoscalerV1 struct { - *Base - instance *autoscalingv1.HorizontalPodAutoscaler -} - -// NewHorizontalPodAutoscalerV1List returns a new resource list. -func NewHorizontalPodAutoscalerV1List(c Connection, ns string) List { - return NewList( - ns, - "hpa", - NewHorizontalPodAutoscalerV1(c), - AllVerbsAccess|DescribeAccess, - ) -} - -// NewHorizontalPodAutoscalerV1 instantiates a new HorizontalPodAutoscalerV1. -func NewHorizontalPodAutoscalerV1(c Connection) *HorizontalPodAutoscalerV1 { - hpa := &HorizontalPodAutoscalerV1{&Base{Connection: c, Resource: k8s.NewHorizontalPodAutoscalerV1(c)}, nil} - hpa.Factory = hpa - - return hpa -} - -// New builds a new HorizontalPodAutoscalerV1 instance from a k8s resource. -func (r *HorizontalPodAutoscalerV1) New(i interface{}) Columnar { - c := NewHorizontalPodAutoscalerV1(r.Connection) - switch instance := i.(type) { - case *autoscalingv1.HorizontalPodAutoscaler: - c.instance = instance - case autoscalingv1.HorizontalPodAutoscaler: - c.instance = &instance - default: - log.Fatal().Msgf("unknown HorizontalPodAutoscalerV1 type %#v", i) - } - c.path = c.namespacedName(c.instance.ObjectMeta) - - return c -} - -// Marshal resource to yaml. -func (r *HorizontalPodAutoscalerV1) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - - hpa := i.(*autoscalingv1.HorizontalPodAutoscaler) - hpa.TypeMeta.APIVersion = extractVersion(hpa.Annotations) - hpa.TypeMeta.Kind = "HorizontalPodAutoscaler" - - return r.marshalObject(hpa) -} - -// Header return resource header. -func (*HorizontalPodAutoscalerV1) Header(ns string) Row { - hh := Row{} - if ns == AllNamespaces { - hh = append(hh, "NAMESPACE") - } - - return append(hh, - "NAME", - "REFERENCE", - "TARGETS", - "MINPODS", - "MAXPODS", - "REPLICAS", - "AGE") -} - -// Fields retrieves displayable fields. -func (r *HorizontalPodAutoscalerV1) Fields(ns string) Row { - ff := make(Row, 0, len(r.Header(ns))) - - i := r.instance - if ns == AllNamespaces { - ff = append(ff, i.Namespace) - } - - return append(ff, - i.ObjectMeta.Name, - i.Spec.ScaleTargetRef.Name, - r.toMetrics(i.Spec, i.Status), - strconv.Itoa(int(*i.Spec.MinReplicas)), - strconv.Itoa(int(i.Spec.MaxReplicas)), - strconv.Itoa(int(i.Status.CurrentReplicas)), - toAge(i.ObjectMeta.CreationTimestamp), - ) -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func (r *HorizontalPodAutoscalerV1) toMetrics(spec autoscalingv1.HorizontalPodAutoscalerSpec, status autoscalingv1.HorizontalPodAutoscalerStatus) string { - current := "" - if status.CurrentCPUUtilizationPercentage != nil { - current = strconv.Itoa(int(*status.CurrentCPUUtilizationPercentage)) + "%" - } - - target := "" - if spec.TargetCPUUtilizationPercentage != nil { - target = strconv.Itoa(int(*spec.TargetCPUUtilizationPercentage)) - } - return current + "/" + target + "%" -} diff --git a/internal/resource/hpa_v1_test.go b/internal/resource/hpa_v1_test.go deleted file mode 100644 index 9303cebf..00000000 --- a/internal/resource/hpa_v1_test.go +++ /dev/null @@ -1,163 +0,0 @@ -package resource_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - m "github.com/petergtz/pegomock" - "github.com/stretchr/testify/assert" - autoscalingv2beta2 "k8s.io/api/autoscaling/v2beta2" - v1 "k8s.io/api/core/v1" - res "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func NewHPAListWithArgs(ns string, r *resource.HorizontalPodAutoscaler) resource.List { - return resource.NewList(ns, "hpa", r, resource.AllVerbsAccess|resource.DescribeAccess) -} - -func NewHPAWithArgs(conn k8s.Connection, res resource.Cruder) *resource.HorizontalPodAutoscaler { - r := &resource.HorizontalPodAutoscaler{Base: resource.NewBase(conn, res)} - r.Factory = r - return r -} - -func TestHPAListAccess(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - - ns := "blee" - l := NewHPAListWithArgs(resource.AllNamespaces, NewHPAWithArgs(mc, mr)) - l.SetNamespace(ns) - - assert.Equal(t, "blee", l.GetNamespace()) - assert.Equal(t, "hpa", l.GetName()) - for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} { - assert.True(t, l.Access(a)) - } -} - -func TestHPAFields(t *testing.T) { - r := newHPA().Fields("blee") - assert.Equal(t, "fred", r[0]) -} - -func TestHPAMarshal(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("blee", "fred")).ThenReturn(k8sHPA(), nil) - - cm := NewHPAWithArgs(mc, mr) - ma, err := cm.Marshal("blee/fred") - mr.VerifyWasCalledOnce().Get("blee", "fred") - assert.Nil(t, err) - assert.Equal(t, hpaYaml(), ma) -} - -func TestHPAListData(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.List("blee", metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sHPA()}, nil) - - l := NewHPAListWithArgs("blee", NewHPAWithArgs(mc, mr)) - // Make sure we mrn get deltas! - for i := 0; i < 2; i++ { - err := l.Reconcile(nil, nil) - assert.Nil(t, err) - } - - mr.VerifyWasCalled(m.Times(2)).List("blee", metav1.ListOptions{}) - td := l.Data() - assert.Equal(t, 1, len(td.Rows)) - assert.Equal(t, "blee", l.GetNamespace()) - row := td.Rows["blee/fred"] - assert.Equal(t, 7, len(row.Deltas)) - for _, d := range row.Deltas { - assert.Equal(t, "", d) - } - assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) -} - -// Helpers... - -func k8sHPA() *autoscalingv2beta2.HorizontalPodAutoscaler { - var i int32 = 1 - return &autoscalingv2beta2.HorizontalPodAutoscaler{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "blee", - Name: "fred", - CreationTimestamp: metav1.Time{Time: testTime()}, - }, - Spec: autoscalingv2beta2.HorizontalPodAutoscalerSpec{ - ScaleTargetRef: autoscalingv2beta2.CrossVersionObjectReference{ - Kind: "fred", - Name: "blee", - }, - MinReplicas: &i, - MaxReplicas: 1, - Metrics: []autoscalingv2beta2.MetricSpec{ - { - Type: autoscalingv2beta2.ResourceMetricSourceType, - Resource: &autoscalingv2beta2.ResourceMetricSource{ - Name: v1.ResourceCPU, - Target: autoscalingv2beta2.MetricTarget{ - Type: autoscalingv2beta2.UtilizationMetricType, - }, - }, - }, - }, - }, - Status: autoscalingv2beta2.HorizontalPodAutoscalerStatus{ - CurrentReplicas: 1, - CurrentMetrics: []autoscalingv2beta2.MetricStatus{ - { - Type: autoscalingv2beta2.ResourceMetricSourceType, - Resource: &autoscalingv2beta2.ResourceMetricStatus{ - Name: v1.ResourceCPU, - Current: autoscalingv2beta2.MetricValueStatus{ - Value: &res.Quantity{}, - }, - }, - }, - }, - }, - } -} - -func newHPA() resource.Columnar { - mc := NewMockConnection() - return resource.NewHorizontalPodAutoscaler(mc).New(k8sHPA()) -} - -func hpaYaml() string { - return `apiVersion: autoscaling/v2beta2 -kind: HorizontalPodAutoscaler -metadata: - creationTimestamp: "2018-12-14T17:36:43Z" - name: fred - namespace: blee -spec: - maxReplicas: 1 - metrics: - - resource: - name: cpu - target: - type: Utilization - type: Resource - minReplicas: 1 - scaleTargetRef: - kind: fred - name: blee -status: - conditions: null - currentMetrics: - - resource: - current: - value: "0" - name: cpu - type: Resource - currentReplicas: 1 - desiredReplicas: 0 -` -} diff --git a/internal/resource/hpa_v2beta1.go b/internal/resource/hpa_v2beta1.go deleted file mode 100644 index faa16883..00000000 --- a/internal/resource/hpa_v2beta1.go +++ /dev/null @@ -1,198 +0,0 @@ -package resource - -import ( - "strconv" - "strings" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - autoscalingv2beta1 "k8s.io/api/autoscaling/v2beta1" -) - -// HorizontalPodAutoscalerV2Beta1 tracks a kubernetes resource. -type HorizontalPodAutoscalerV2Beta1 struct { - *Base - instance *autoscalingv2beta1.HorizontalPodAutoscaler -} - -// NewHorizontalPodAutoscalerV2Beta1List returns a new resource list. -func NewHorizontalPodAutoscalerV2Beta1List(c Connection, ns string) List { - return NewList( - ns, - "hpa", - NewHorizontalPodAutoscalerV2Beta1(c), - AllVerbsAccess|DescribeAccess, - ) -} - -// NewHorizontalPodAutoscalerV2Beta1 instantiates a new HorizontalPodAutoscalerV2Beta1. -func NewHorizontalPodAutoscalerV2Beta1(c Connection) *HorizontalPodAutoscalerV2Beta1 { - hpa := &HorizontalPodAutoscalerV2Beta1{&Base{Connection: c, Resource: k8s.NewHorizontalPodAutoscalerV2Beta1(c)}, nil} - hpa.Factory = hpa - - return hpa -} - -// New builds a new HorizontalPodAutoscalerV2Beta1 instance from a k8s resource. -func (r *HorizontalPodAutoscalerV2Beta1) New(i interface{}) Columnar { - c := NewHorizontalPodAutoscalerV2Beta1(r.Connection) - switch instance := i.(type) { - case *autoscalingv2beta1.HorizontalPodAutoscaler: - c.instance = instance - case autoscalingv2beta1.HorizontalPodAutoscaler: - c.instance = &instance - default: - log.Fatal().Msgf("unknown HorizontalPodAutoscalerV2Beta1 type %#v", i) - } - c.path = c.namespacedName(c.instance.ObjectMeta) - - return c -} - -// Marshal resource to yaml. -func (r *HorizontalPodAutoscalerV2Beta1) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - - hpa := i.(*autoscalingv2beta1.HorizontalPodAutoscaler) - hpa.TypeMeta.APIVersion = extractVersion(hpa.Annotations) - hpa.TypeMeta.Kind = "HorizontalPodAutoscaler" - - return r.marshalObject(hpa) -} - -// Header return resource header. -func (*HorizontalPodAutoscalerV2Beta1) Header(ns string) Row { - hh := Row{} - if ns == AllNamespaces { - hh = append(hh, "NAMESPACE") - } - - return append(hh, - "NAME", - "REFERENCE", - "TARGETS", - "MINPODS", - "MAXPODS", - "REPLICAS", - "AGE") -} - -// Fields retrieves displayable fields. -func (r *HorizontalPodAutoscalerV2Beta1) Fields(ns string) Row { - ff := make(Row, 0, len(r.Header(ns))) - - i := r.instance - if ns == AllNamespaces { - ff = append(ff, i.Namespace) - } - - return append(ff, - i.ObjectMeta.Name, - i.Spec.ScaleTargetRef.Name, - r.toMetrics(i.Spec.Metrics, i.Status.CurrentMetrics), - strconv.Itoa(int(*i.Spec.MinReplicas)), - strconv.Itoa(int(i.Spec.MaxReplicas)), - strconv.Itoa(int(i.Status.CurrentReplicas)), - toAge(i.ObjectMeta.CreationTimestamp), - ) -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func (r *HorizontalPodAutoscalerV2Beta1) toMetrics(specs []autoscalingv2beta1.MetricSpec, statuses []autoscalingv2beta1.MetricStatus) string { - if len(specs) == 0 { - return MissingValue - } - - list, count := []string{}, 0 - for i, spec := range specs { - list = append(list, r.checkHPAType(i, spec, statuses)) - count++ - } - - max, more := 2, false - if count > max { - list, more = list[:max], true - } - - ret := strings.Join(list, ", ") - if more { - return ret + " + " + strconv.Itoa(count-max) + "more..." - } - - return ret -} - -func (r *HorizontalPodAutoscalerV2Beta1) checkHPAType(i int, spec autoscalingv2beta1.MetricSpec, statuses []autoscalingv2beta1.MetricStatus) string { - current := "" - - switch spec.Type { - case autoscalingv2beta1.ExternalMetricSourceType: - return r.externalMetrics(i, spec, statuses) - case autoscalingv2beta1.PodsMetricSourceType: - if len(statuses) > i && statuses[i].Pods != nil { - current = statuses[i].Pods.CurrentAverageValue.String() - } - return current + "/" + spec.Pods.TargetAverageValue.String() - case autoscalingv2beta1.ObjectMetricSourceType: - if len(statuses) > i && statuses[i].Object != nil { - current = statuses[i].Object.CurrentValue.String() - } - return current + "/" + spec.Object.TargetValue.String() - case autoscalingv2beta1.ResourceMetricSourceType: - return r.resourceMetrics(i, spec, statuses) - } - - return "" -} - -func (*HorizontalPodAutoscalerV2Beta1) externalMetrics(i int, spec autoscalingv2beta1.MetricSpec, statuses []autoscalingv2beta1.MetricStatus) string { - current := "" - if spec.External.TargetAverageValue != nil { - if len(statuses) > i && statuses[i].External != nil && &statuses[i].External.CurrentAverageValue != nil { - current = statuses[i].External.CurrentAverageValue.String() - } - return current + "/" + spec.External.TargetAverageValue.String() + " (avg)" - } - if len(statuses) > i && statuses[i].External != nil { - current = statuses[i].External.CurrentValue.String() - } - - return current + "/" + spec.External.TargetValue.String() -} - -func (*HorizontalPodAutoscalerV2Beta1) resourceMetrics(i int, spec autoscalingv2beta1.MetricSpec, statuses []autoscalingv2beta1.MetricStatus) string { - current := "" - - if status := checkTargetMetrics(i, spec, statuses); status != "" { - return status - } - - if len(statuses) > i && statuses[i].Resource != nil && statuses[i].Resource.CurrentAverageUtilization != nil { - current = AsPerc(float64(*statuses[i].Resource.CurrentAverageUtilization)) - } - - target := "" - if spec.Resource.TargetAverageUtilization != nil { - target = AsPerc(float64(*spec.Resource.TargetAverageUtilization)) - } - - return current + "/" + target -} - -func checkTargetMetrics(i int, spec autoscalingv2beta1.MetricSpec, statuses []autoscalingv2beta1.MetricStatus) string { - if spec.Resource.TargetAverageValue == nil { - return "" - } - - var current string - if len(statuses) > i && statuses[i].Resource != nil { - current = statuses[i].Resource.CurrentAverageValue.String() - } - return current + "/" + spec.Resource.TargetAverageValue.String() -} diff --git a/internal/resource/hpa_v2beta2.go b/internal/resource/hpa_v2beta2.go deleted file mode 100644 index f837da4a..00000000 --- a/internal/resource/hpa_v2beta2.go +++ /dev/null @@ -1,197 +0,0 @@ -package resource - -import ( - "regexp" - "strconv" - "strings" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - autoscalingv2beta2 "k8s.io/api/autoscaling/v2beta2" -) - -// HorizontalPodAutoscaler tracks a kubernetes resource. -type HorizontalPodAutoscaler struct { - *Base - instance *autoscalingv2beta2.HorizontalPodAutoscaler -} - -// NewHorizontalPodAutoscalerList returns a new resource list. -func NewHorizontalPodAutoscalerList(c Connection, ns string) List { - return NewList( - ns, - "hpa", - NewHorizontalPodAutoscaler(c), - AllVerbsAccess|DescribeAccess, - ) -} - -// NewHorizontalPodAutoscaler instantiates a new HorizontalPodAutoscaler. -func NewHorizontalPodAutoscaler(c Connection) *HorizontalPodAutoscaler { - hpa := &HorizontalPodAutoscaler{&Base{Connection: c, Resource: k8s.NewHorizontalPodAutoscalerV2Beta2(c)}, nil} - hpa.Factory = hpa - - return hpa -} - -// New builds a new HorizontalPodAutoscaler instance from a k8s resource. -func (r *HorizontalPodAutoscaler) New(i interface{}) Columnar { - c := NewHorizontalPodAutoscaler(r.Connection) - switch instance := i.(type) { - case *autoscalingv2beta2.HorizontalPodAutoscaler: - c.instance = instance - case autoscalingv2beta2.HorizontalPodAutoscaler: - c.instance = &instance - default: - log.Fatal().Msgf("unknown HorizontalPodAutoscaler type %#v", i) - } - c.path = c.namespacedName(c.instance.ObjectMeta) - - return c -} - -// Marshal resource to yaml. -func (r *HorizontalPodAutoscaler) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - - hpa := i.(*autoscalingv2beta2.HorizontalPodAutoscaler) - hpa.TypeMeta.APIVersion = extractVersion(hpa.Annotations) - hpa.TypeMeta.Kind = "HorizontalPodAutoscaler" - - return r.marshalObject(hpa) -} - -func extractVersion(a map[string]string) string { - ann := a["kubectl.kubernetes.io/last-applied-configuration"] - rx := regexp.MustCompile(`\A{"apiVersion":"([\w|/]+)",`) - found := rx.FindAllStringSubmatch(ann, 1) - if len(found) == 0 || len(found[0]) < 1 { - return "autoscaling/v2beta2" - } - - return found[0][1] -} - -// Header return resource header. -func (*HorizontalPodAutoscaler) Header(ns string) Row { - hh := Row{} - if ns == AllNamespaces { - hh = append(hh, "NAMESPACE") - } - - return append(hh, - "NAME", - "REFERENCE", - "TARGETS", - "MINPODS", - "MAXPODS", - "REPLICAS", - "AGE") -} - -// Fields retrieves displayable fields. -func (r *HorizontalPodAutoscaler) Fields(ns string) Row { - ff := make(Row, 0, len(r.Header(ns))) - - i := r.instance - if ns == AllNamespaces { - ff = append(ff, i.Namespace) - } - - return append(ff, - i.ObjectMeta.Name, - i.Spec.ScaleTargetRef.Name, - toMetrics(i.Spec.Metrics, i.Status.CurrentMetrics), - strconv.Itoa(int(*i.Spec.MinReplicas)), - strconv.Itoa(int(i.Spec.MaxReplicas)), - strconv.Itoa(int(i.Status.CurrentReplicas)), - toAge(i.ObjectMeta.CreationTimestamp), - ) -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func toMetrics(specs []autoscalingv2beta2.MetricSpec, statuses []autoscalingv2beta2.MetricStatus) string { - if len(specs) == 0 { - return MissingValue - } - - list, max, more, count := []string{}, 2, false, 0 - for i, spec := range specs { - current := "" - - switch spec.Type { - case autoscalingv2beta2.ExternalMetricSourceType: - list = append(list, externalMetrics(i, spec, statuses)) - case autoscalingv2beta2.PodsMetricSourceType: - if len(statuses) > i && statuses[i].Pods != nil { - current = statuses[i].Pods.Current.AverageValue.String() - } - list = append(list, current+"/"+spec.Pods.Target.AverageValue.String()) - case autoscalingv2beta2.ObjectMetricSourceType: - if len(statuses) > i && statuses[i].Object != nil { - current = statuses[i].Object.Current.Value.String() - } - list = append(list, current+"/"+spec.Object.Target.Value.String()) - case autoscalingv2beta2.ResourceMetricSourceType: - list = append(list, resourceMetrics(i, spec, statuses)) - default: - list = append(list, "") - } - count++ - } - - if count > max { - list, more = list[:max], true - } - - ret := strings.Join(list, ", ") - if more { - return ret + " + " + strconv.Itoa(count-max) + "more..." - } - - return ret -} - -func externalMetrics(i int, spec autoscalingv2beta2.MetricSpec, statuses []autoscalingv2beta2.MetricStatus) string { - current := "" - - if spec.External.Target.AverageValue != nil { - if len(statuses) > i && statuses[i].External != nil && &statuses[i].External.Current.AverageValue != nil { - current = statuses[i].External.Current.AverageValue.String() - } - return current + "/" + spec.External.Target.AverageValue.String() + " (avg)" - } - if len(statuses) > i && statuses[i].External != nil { - current = statuses[i].External.Current.Value.String() - } - - return current + "/" + spec.External.Target.Value.String() -} - -func resourceMetrics(i int, spec autoscalingv2beta2.MetricSpec, statuses []autoscalingv2beta2.MetricStatus) string { - current := "" - - if spec.Resource.Target.AverageValue != nil { - if len(statuses) > i && statuses[i].Resource != nil { - current = statuses[i].Resource.Current.AverageValue.String() - } - return current + "/" + spec.Resource.Target.AverageValue.String() - } - - if len(statuses) > i && statuses[i].Resource != nil && statuses[i].Resource.Current.AverageUtilization != nil { - current = AsPerc(float64(*statuses[i].Resource.Current.AverageUtilization)) - } - - target := "" - if spec.Resource.Target.AverageUtilization != nil { - target = AsPerc(float64(*spec.Resource.Target.AverageUtilization)) - } - - return current + "/" + target -} diff --git a/internal/resource/hpa_v2beta2_int_test.go b/internal/resource/hpa_v2beta2_int_test.go deleted file mode 100644 index 4003e19f..00000000 --- a/internal/resource/hpa_v2beta2_int_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package resource - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestVersionFromAnnotation(t *testing.T) { - ann := map[string]string{ - "kubectl.kubernetes.io/last-applied-configuration": `{"apiVersion":"autoscaling/v1","kind":"HorizontalPodAutoscaler","metadata":{"annotations":{},"name":"nginx","namespace":"default"},"spec":{"maxReplicas":10,"minReplicas":1,"scaleTargetRef":{"apiVersion":"apps/v1","kind":"Deployment","name":"nginx"},"targetCPUUtilizationPercentage":10}}`, - } - - assert.Equal(t, "autoscaling/v1", extractVersion(ann)) -} diff --git a/internal/resource/ing.go b/internal/resource/ing.go deleted file mode 100644 index a5ca3e1f..00000000 --- a/internal/resource/ing.go +++ /dev/null @@ -1,132 +0,0 @@ -package resource - -import ( - "strings" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - v1 "k8s.io/api/core/v1" - "k8s.io/api/extensions/v1beta1" -) - -// Ingress tracks a kubernetes resource. -type Ingress struct { - *Base - instance *v1beta1.Ingress -} - -// NewIngressList returns a new resource list. -func NewIngressList(c Connection, ns string) List { - return NewList( - ns, - "ing", - NewIngress(c), - AllVerbsAccess|DescribeAccess, - ) -} - -// NewIngress instantiates a new Ingress. -func NewIngress(c Connection) *Ingress { - ing := &Ingress{&Base{Connection: c, Resource: k8s.NewIngress(c)}, nil} - ing.Factory = ing - - return ing -} - -// New builds a new Ingress instance from a k8s resource. -func (r *Ingress) New(i interface{}) Columnar { - c := NewIngress(r.Connection) - switch instance := i.(type) { - case *v1beta1.Ingress: - c.instance = instance - case v1beta1.Ingress: - c.instance = &instance - default: - log.Fatal().Msgf("unknown Ingress type %#v", i) - } - c.path = c.namespacedName(c.instance.ObjectMeta) - - return c -} - -// Marshal resource to yaml. -func (r *Ingress) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - - ing := i.(*v1beta1.Ingress) - ing.TypeMeta.APIVersion = "extensions/v1beta1" - ing.TypeMeta.Kind = "Ingress" - - return r.marshalObject(ing) -} - -// Header return resource header. -func (*Ingress) Header(ns string) Row { - hh := Row{} - if ns == AllNamespaces { - hh = append(hh, "NAMESPACE") - } - - return append(hh, "NAME", "HOSTS", "ADDRESS", "PORT", "AGE") -} - -// Fields retrieves displayable fields. -func (r *Ingress) Fields(ns string) Row { - ff := make([]string, 0, len(r.Header(ns))) - - i := r.instance - if ns == AllNamespaces { - ff = append(ff, i.Namespace) - } - - return append(ff, - i.Name, - r.toHosts(i.Spec.Rules), - r.toAddress(i.Status.LoadBalancer), - r.toPorts(i.Spec.TLS), - toAge(i.ObjectMeta.CreationTimestamp), - ) -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func (*Ingress) toAddress(lbs v1.LoadBalancerStatus) string { - ings := lbs.Ingress - res := make([]string, 0, len(ings)) - for _, lb := range ings { - if len(lb.IP) > 0 { - res = append(res, lb.IP) - } else if len(lb.Hostname) != 0 { - res = append(res, lb.Hostname) - } - } - - return strings.Join(res, ",") -} - -func (*Ingress) toPorts(tls []v1beta1.IngressTLS) string { - if len(tls) != 0 { - return "80, 443" - } - - return "80" -} - -func (*Ingress) toHosts(rr []v1beta1.IngressRule) string { - var s string - var i int - for _, r := range rr { - s += r.Host - if i < len(rr)-1 { - s += "," - } - i++ - } - - return s -} diff --git a/internal/resource/ing_test.go b/internal/resource/ing_test.go deleted file mode 100644 index 3851c6e9..00000000 --- a/internal/resource/ing_test.go +++ /dev/null @@ -1,115 +0,0 @@ -package resource_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - m "github.com/petergtz/pegomock" - "github.com/stretchr/testify/assert" - "k8s.io/api/extensions/v1beta1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func NewIngressListWithArgs(ns string, r *resource.Ingress) resource.List { - return resource.NewList(ns, "ing", r, resource.AllVerbsAccess|resource.DescribeAccess) -} - -func NewIngressWithArgs(conn k8s.Connection, res resource.Cruder) *resource.Ingress { - r := &resource.Ingress{Base: resource.NewBase(conn, res)} - r.Factory = r - return r -} - -func TestIngressListAccess(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - - ns := "blee" - l := NewIngressListWithArgs(resource.AllNamespaces, NewIngressWithArgs(mc, mr)) - l.SetNamespace(ns) - - assert.Equal(t, "blee", l.GetNamespace()) - assert.Equal(t, "ing", l.GetName()) - for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} { - assert.True(t, l.Access(a)) - } -} - -func TestIngressFields(t *testing.T) { - r := newIngress().Fields("blee") - assert.Equal(t, "fred", r[0]) -} - -func TestIngressMarshal(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("blee", "fred")).ThenReturn(k8sIngress(), nil) - - cm := NewIngressWithArgs(mc, mr) - ma, err := cm.Marshal("blee/fred") - mr.VerifyWasCalledOnce().Get("blee", "fred") - assert.Nil(t, err) - assert.Equal(t, ingYaml(), ma) -} - -func TestIngressListData(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.List("blee", metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sIngress()}, nil) - - l := NewIngressListWithArgs("blee", NewIngressWithArgs(mc, mr)) - // Make sure we mrn get deltas! - for i := 0; i < 2; i++ { - err := l.Reconcile(nil, nil) - assert.Nil(t, err) - } - - mr.VerifyWasCalled(m.Times(2)).List("blee", metav1.ListOptions{}) - td := l.Data() - assert.Equal(t, 1, len(td.Rows)) - assert.Equal(t, "blee", l.GetNamespace()) - row := td.Rows["blee/fred"] - assert.Equal(t, 5, len(row.Deltas)) - for _, d := range row.Deltas { - assert.Equal(t, "", d) - } - assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) -} - -// Helpers... - -func k8sIngress() *v1beta1.Ingress { - return &v1beta1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "blee", - Name: "fred", - CreationTimestamp: metav1.Time{Time: testTime()}, - }, - Spec: v1beta1.IngressSpec{ - Rules: []v1beta1.IngressRule{ - {Host: "blee"}, - }, - }, - } -} - -func newIngress() resource.Columnar { - mc := NewMockConnection() - return resource.NewIngress(mc).New(k8sIngress()) -} - -func ingYaml() string { - return `apiVersion: extensions/v1beta1 -kind: Ingress -metadata: - creationTimestamp: "2018-12-14T17:36:43Z" - name: fred - namespace: blee -spec: - rules: - - host: blee -status: - loadBalancer: {} -` -} diff --git a/internal/resource/job.go b/internal/resource/job.go deleted file mode 100644 index 0104edae..00000000 --- a/internal/resource/job.go +++ /dev/null @@ -1,191 +0,0 @@ -package resource - -import ( - "context" - "fmt" - "strconv" - "strings" - "sync" - "time" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - batchv1 "k8s.io/api/batch/v1" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/util/duration" -) - -// Job tracks a kubernetes resource. -type Job struct { - *Base - - instance *batchv1.Job - mx sync.RWMutex -} - -// NewJobList returns a new resource list. -func NewJobList(c Connection, ns string) List { - return NewList( - ns, - "job", - NewJob(c), - AllVerbsAccess|DescribeAccess, - ) -} - -// NewJob instantiates a new Job. -func NewJob(c Connection) *Job { - j := &Job{ - Base: &Base{Connection: c, Resource: k8s.NewJob(c)}, - } - j.Factory = j - - return j -} - -// New builds a new Job instance from a k8s resource. -func (r *Job) New(i interface{}) Columnar { - c := NewJob(r.Connection) - switch instance := i.(type) { - case *batchv1.Job: - c.instance = instance - case batchv1.Job: - c.instance = &instance - default: - log.Fatal().Msgf("unknown Job type %#v", i) - } - c.path = c.namespacedName(c.instance.ObjectMeta) - - return c -} - -// Marshal resource to yaml. -func (r *Job) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - - jo := i.(*batchv1.Job) - jo.TypeMeta.APIVersion = "extensions/v1beta1" - jo.TypeMeta.Kind = "Job" - - return r.marshalObject(jo) -} - -// Containers fetch all the containers on this job, may include init containers. -func (r *Job) Containers(path string, includeInit bool) ([]string, error) { - ns, n := Namespaced(path) - - return r.Resource.(k8s.Loggable).Containers(ns, n, includeInit) -} - -// Logs retrieves logs for a given container. -func (r *Job) Logs(ctx context.Context, c chan<- string, opts LogOptions) error { - instance, err := r.Resource.Get(opts.Namespace, opts.Name) - if err != nil { - return err - } - jo := instance.(*batchv1.Job) - if jo.Spec.Selector == nil || len(jo.Spec.Selector.MatchLabels) == 0 { - return fmt.Errorf("No valid selector found on job %s", opts.FQN()) - } - - return r.podLogs(ctx, c, jo.Spec.Selector.MatchLabels, opts) -} - -// Header return resource header. -func (*Job) Header(ns string) Row { - hh := Row{} - if ns == AllNamespaces { - hh = append(hh, "NAMESPACE") - } - - return append(hh, "NAME", "COMPLETIONS", "DURATION", "CONTAINERS", "IMAGES", "AGE") -} - -// Fields retrieves displayable fields. -func (r *Job) Fields(ns string) Row { - ff := make([]string, 0, len(r.Header(ns))) - - i := r.instance - if ns == AllNamespaces { - ff = append(ff, i.Namespace) - } - - cc, ii := r.toContainers(i.Spec.Template.Spec) - - return append(ff, - i.Name, - r.toCompletion(i.Spec, i.Status), - r.toDuration(i.Status), - cc, - ii, - toAge(i.ObjectMeta.CreationTimestamp), - ) -} - -// ---------------------------------------------------------------------------- -// Helpers... - -const maxShow = 2 - -func (*Job) toContainers(p v1.PodSpec) (string, string) { - cc, ii := parseContainers(p.InitContainers) - cn, ci := parseContainers(p.Containers) - - cc, ii = append(cc, cn...), append(ii, ci...) - - // Limit to 2 of each... - if len(cc) > maxShow { - cc = append(cc[:2], "(+"+strconv.Itoa(len(cc)-maxShow)+")...") - } - if len(ii) > maxShow { - ii = append(ii[:2], "(+"+strconv.Itoa(len(ii)-maxShow)+")...") - } - - return strings.Join(cc, ","), strings.Join(ii, ",") -} - -func parseContainers(cos []v1.Container) (nn, ii []string) { - for _, co := range cos { - nn = append(nn, co.Name) - ii = append(ii, co.Image) - } - - return nn, ii -} - -func (*Job) toCompletion(spec batchv1.JobSpec, status batchv1.JobStatus) (s string) { - if spec.Completions != nil { - return strconv.Itoa(int(status.Succeeded)) + "/" + strconv.Itoa(int(*spec.Completions)) - } - - if spec.Parallelism == nil { - return strconv.Itoa(int(status.Succeeded)) + "/1" - } - - p := *spec.Parallelism - if p > 1 { - return strconv.Itoa(int(status.Succeeded)) + "/1 of " + strconv.Itoa(int(p)) - } - - return strconv.Itoa(int(status.Succeeded)) + "/1" -} - -func (*Job) toDuration(status batchv1.JobStatus) string { - if status.StartTime == nil { - return MissingValue - } - - var d time.Duration - switch { - case status.CompletionTime == nil: - d = time.Since(status.StartTime.Time) - default: - d = status.CompletionTime.Sub(status.StartTime.Time) - } - - return duration.HumanDuration(d) -} diff --git a/internal/resource/job_int_test.go b/internal/resource/job_int_test.go deleted file mode 100644 index a8de3436..00000000 --- a/internal/resource/job_int_test.go +++ /dev/null @@ -1,153 +0,0 @@ -package resource - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - batchv1 "k8s.io/api/batch/v1" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestJobToCompletion(t *testing.T) { - t0 := testTime() - t1, t2 := metav1.Time{t0}, metav1.Time{t0.Add(10 * time.Second)} - var c, p int32 = 10, 20 - - uu := []struct { - j batchv1.JobSpec - s batchv1.JobStatus - e string - }{ - { - batchv1.JobSpec{ - Completions: &c, - Parallelism: &p, - }, - batchv1.JobStatus{ - Succeeded: 1, - Active: 1, - Failed: 0, - StartTime: &t1, - CompletionTime: &t2, - }, - "1/10", - }, - { - batchv1.JobSpec{ - Parallelism: &p, - }, - batchv1.JobStatus{ - Succeeded: 1, - Active: 1, - Failed: 0, - StartTime: &t1, - CompletionTime: &t2, - }, - "1/1 of 20", - }, - { - batchv1.JobSpec{ - Completions: &c, - }, - batchv1.JobStatus{ - Succeeded: 1, - Active: 1, - Failed: 0, - StartTime: &t1, - CompletionTime: &t2, - }, - "1/10", - }, - { - batchv1.JobSpec{}, - batchv1.JobStatus{ - Succeeded: 1, - Active: 1, - Failed: 0, - StartTime: &t1, - CompletionTime: &t2, - }, - "1/1", - }, - } - - var j *Job - for _, u := range uu { - assert.Equal(t, u.e, j.toCompletion(u.j, u.s)) - } -} - -func TestJobToDuration(t *testing.T) { - t0 := testTime().UTC() - t1, t2 := metav1.Time{t0}, metav1.Time{t0.Add(10 * time.Second)} - - uu := []struct { - s batchv1.JobStatus - e string - }{ - { - batchv1.JobStatus{ - StartTime: &t1, - CompletionTime: &t2, - }, - "10s", - }, - { - batchv1.JobStatus{ - StartTime: &metav1.Time{time.Now().Add(-10 * time.Second)}, - }, - "10s", - }, - { - batchv1.JobStatus{ - CompletionTime: &t2, - }, - MissingValue, - }, - } - - var j *Job - for _, u := range uu { - assert.Equal(t, u.e, j.toDuration(u.s)) - } -} - -func TestJobToContainers(t *testing.T) { - uu := []struct { - s v1.PodSpec - c, i string - }{ - { - v1.PodSpec{ - InitContainers: []v1.Container{ - {Name: "i1", Image: "fred"}, - }, - Containers: []v1.Container{ - {Name: "c1", Image: "blee"}, - }, - }, - "i1,c1", "fred,blee", - }, - { - v1.PodSpec{ - InitContainers: []v1.Container{ - {Name: "i1", Image: "fred"}, - }, - Containers: []v1.Container{ - {Name: "c1", Image: "blee"}, - {Name: "c2", Image: "duh"}, - }, - }, - "i1,c1,(+1)...", "fred,blee,(+1)...", - }, - } - - var j *Job - for _, u := range uu { - c, i := j.toContainers(u.s) - assert.Equal(t, u.c, c) - assert.Equal(t, u.i, i) - } -} diff --git a/internal/resource/job_test.go b/internal/resource/job_test.go deleted file mode 100644 index 3afdac1a..00000000 --- a/internal/resource/job_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package resource_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - m "github.com/petergtz/pegomock" - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/batch/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func NewJobListWithArgs(ns string, r *resource.Job) resource.List { - return resource.NewList(ns, "job", r, resource.AllVerbsAccess|resource.DescribeAccess) -} - -func NewJobWithArgs(conn k8s.Connection, res resource.Cruder) *resource.Job { - r := &resource.Job{Base: resource.NewBase(conn, res)} - r.Factory = r - return r -} - -func TestJobListAccess(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - - ns := "blee" - l := NewJobListWithArgs(resource.AllNamespaces, NewJobWithArgs(mc, mr)) - l.SetNamespace(ns) - - assert.Equal(t, "blee", l.GetNamespace()) - assert.Equal(t, "job", l.GetName()) - for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} { - assert.True(t, l.Access(a)) - } -} - -func TestJobFields(t *testing.T) { - r := newJob().Fields("blee") - assert.Equal(t, "fred", r[0]) -} - -func TestJobMarshal(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("blee", "fred")).ThenReturn(k8sJob(), nil) - - cm := NewJobWithArgs(mc, mr) - ma, err := cm.Marshal("blee/fred") - mr.VerifyWasCalledOnce().Get("blee", "fred") - assert.Nil(t, err) - assert.Equal(t, jobYaml(), ma) -} - -func TestJobListData(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.List("blee", metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sJob()}, nil) - - l := NewJobListWithArgs("blee", NewJobWithArgs(mc, mr)) - // Make sure we mrn get deltas! - for i := 0; i < 2; i++ { - err := l.Reconcile(nil, nil) - assert.Nil(t, err) - } - - mr.VerifyWasCalled(m.Times(2)).List("blee", metav1.ListOptions{}) - td := l.Data() - assert.Equal(t, 1, len(td.Rows)) - assert.Equal(t, "blee", l.GetNamespace()) - row := td.Rows["blee/fred"] - assert.Equal(t, 6, len(row.Deltas)) - for _, d := range row.Deltas { - assert.Equal(t, "", d) - } - assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) -} - -// Helpers... - -func k8sJob() *v1.Job { - var i int32 - return &v1.Job{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "blee", - Name: "fred", - CreationTimestamp: metav1.Time{Time: testTime()}, - }, - Spec: v1.JobSpec{ - Completions: &i, - Parallelism: &i, - }, - Status: v1.JobStatus{ - StartTime: &metav1.Time{Time: testTime()}, - CompletionTime: &metav1.Time{Time: testTime()}, - }, - } -} - -func newJob() resource.Columnar { - mc := NewMockConnection() - return resource.NewJob(mc).New(k8sJob()) -} - -func jobYaml() string { - return `apiVersion: extensions/v1beta1 -kind: Job -metadata: - creationTimestamp: "2018-12-14T17:36:43Z" - name: fred - namespace: blee -spec: - completions: 0 - parallelism: 0 - template: - metadata: - creationTimestamp: null - spec: - containers: null -status: - completionTime: "2018-12-14T17:36:43Z" - startTime: "2018-12-14T17:36:43Z" -` -} diff --git a/internal/resource/list.go b/internal/resource/list.go deleted file mode 100644 index bf115a77..00000000 --- a/internal/resource/list.go +++ /dev/null @@ -1,366 +0,0 @@ -package resource - -import ( - "fmt" - "reflect" - - wa "github.com/derailed/k9s/internal/watch" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/watch" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -const ( - // GetAccess set if resource can be fetched. - GetAccess = 1 << iota - // ListAccess set if resource can be listed. - ListAccess - // EditAccess set if resource can be edited. - EditAccess - // DeleteAccess set if resource can be deleted. - DeleteAccess - // ViewAccess set if resource can be viewed. - ViewAccess - // NamespaceAccess set if namespaced resource. - NamespaceAccess - // DescribeAccess set if resource can be described. - DescribeAccess - // SwitchAccess set if resource can be switched (Context). - SwitchAccess - - // CRUDAccess Verbs. - CRUDAccess = GetAccess | ListAccess | DeleteAccess | ViewAccess | EditAccess - - // AllVerbsAccess super powers. - AllVerbsAccess = CRUDAccess | NamespaceAccess -) - -type ( - // RowEvent represents a call for action after a resource reconciliation. - // Tracks whether a resource got added, deleted or updated. - RowEvent struct { - Action watch.EventType - Fields Row - Deltas Row - } - - // RowEvents tracks resource update events. - RowEvents map[string]*RowEvent - - // TypeMeta represents resource type meta data. - TypeMeta struct { - Name string - Namespaced bool - Group string - Version string - Kind string - Singular string - Plural string - ShortNames []string - } - - // TableData tracks a K8s resource for tabular display. - TableData struct { - Header Row - Rows RowEvents - NumCols map[string]bool - Namespace string - } - - // List protocol to display and update a collection of resources - List interface { - Data() TableData - Resource() Resource - Namespaced() bool - AllNamespaces() bool - GetNamespace() string - SetNamespace(string) - Reconcile(informer *wa.Informer, path *string) error - GetName() string - Access(flag int) bool - GetAccess() int - SetAccess(int) - SetFieldSelector(string) - SetLabelSelector(string) - HasSelectors() bool - } - - // Columnar tracks resources that can be diplayed in a tabular fashion. - Columnar interface { - Header(ns string) Row - Fields(ns string) Row - ExtFields() (TypeMeta, error) - Name() string - SetPodMetrics(*mv1beta1.PodMetrics) - SetNodeMetrics(*mv1beta1.NodeMetrics) - } - - // Columnars a collection of columnars. - Columnars []Columnar - - // Row represents a collection of string fields. - Row []string - - // Rows represents a collection of rows. - Rows []Row - - // Resource represents a tabular Kubernetes resource. - Resource interface { - New(interface{}) Columnar - Get(path string) (Columnar, error) - List(ns string, opts metav1.ListOptions) (Columnars, error) - Delete(path string, cascade, force bool) error - Describe(gvr, pa string) (string, error) - Marshal(pa string) (string, error) - Header(ns string) Row - NumCols(ns string) map[string]bool - } - - list struct { - namespace, name string - verbs int - resource Resource - cache RowEvents - fieldSelector string - labelSelector string - } -) - -func newRowEvent(a watch.EventType, f, d Row) *RowEvent { - return &RowEvent{Action: a, Fields: f, Deltas: d} -} - -// NewList returns a new resource list. -func NewList(ns, name string, res Resource, verbs int) *list { - return &list{ - namespace: ns, - name: name, - verbs: verbs, - resource: res, - cache: RowEvents{}, - } -} - -func (l *list) HasSelectors() bool { - return l.fieldSelector != "" || l.labelSelector != "" -} - -// SetFieldSelector narrows down resource query given fields selection. -func (l *list) SetFieldSelector(s string) { - l.fieldSelector = s -} - -// SetLabelSelector narrows down resource query via labels selections. -func (l *list) SetLabelSelector(s string) { - l.labelSelector = s -} - -// Access check access control on a given resource. -func (l *list) Access(f int) bool { - return l.verbs&f == f -} - -// Access check access control on a given resource. -func (l *list) GetAccess() int { - return l.verbs -} - -// Access check access control on a given resource. -func (l *list) SetAccess(f int) { - l.verbs = f -} - -// Namespaced checks if k8s resource is namespaced. -func (l *list) Namespaced() bool { - return l.namespace != NotNamespaced -} - -// AllNamespaces checks if this resource spans all namespaces. -func (l *list) AllNamespaces() bool { - return l.namespace == AllNamespaces -} - -// GetNamespace associated with the resource. -func (l *list) GetNamespace() string { - if !l.Access(NamespaceAccess) { - l.namespace = NotNamespaced - } - - return l.namespace -} - -// SetNamespace updates the namespace on the list. Default ns is "" for all -// namespaces. -func (l *list) SetNamespace(n string) { - if !l.Namespaced() { - return - } - - if n == AllNamespace { - n = AllNamespaces - } - if l.namespace == n { - return - } - l.cache = RowEvents{} - if l.Access(NamespaceAccess) { - l.namespace = n - if n == AllNamespace { - l.namespace = AllNamespaces - } - } -} - -// GetName returns the kubernetes resource name. -func (l *list) GetName() string { - return l.name -} - -// Resource returns a resource api connection. -func (l *list) Resource() Resource { - return l.resource -} - -// Cache tracks previous resource state. -func (l *list) Data() TableData { - return TableData{ - Header: l.resource.Header(l.namespace), - Rows: l.cache, - NumCols: l.resource.NumCols(l.namespace), - Namespace: l.namespace, - } -} - -func (l *list) load(informer *wa.Informer, ns string) (Columnars, error) { - rr, err := informer.List(l.name, ns, metav1.ListOptions{ - FieldSelector: l.fieldSelector, - LabelSelector: l.labelSelector, - }) - if err != nil { - return nil, err - } - - items := make(Columnars, 0, len(rr)) - for _, r := range rr { - res, err := l.fetchResource(informer, r, ns) - if err != nil { - return nil, err - } - items = append(items, res) - } - - return items, nil -} - -func (l *list) fetchResource(informer *wa.Informer, r interface{}, ns string) (Columnar, error) { - var err error - - res := l.resource.New(r) - switch o := r.(type) { - case *v1.Node: - fqn := MetaFQN(o.ObjectMeta) - nmx, err := informer.Get(wa.NodeMXIndex, fqn, metav1.GetOptions{}) - if err == nil { - res.SetNodeMetrics(nmx.(*mv1beta1.NodeMetrics)) - } - case *v1.Pod: - fqn := MetaFQN(o.ObjectMeta) - pmx, err := informer.Get(wa.PodMXIndex, fqn, metav1.GetOptions{}) - if err == nil { - res.SetPodMetrics(pmx.(*mv1beta1.PodMetrics)) - } - case v1.Container: - pmx, err := informer.Get(wa.PodMXIndex, ns, metav1.GetOptions{}) - if err == nil { - res.SetPodMetrics(pmx.(*mv1beta1.PodMetrics)) - } - default: - err = fmt.Errorf("No informer matched %s:%s", l.name, ns) - } - - return res, err -} - -// Reconcile previous vs current state and emits delta events. -func (l *list) Reconcile(informer *wa.Informer, path *string) error { - ns := l.namespace - if path != nil { - ns = *path - } - - items, err := l.load(informer, ns) - if err == nil { - l.update(items) - return nil - } - - opts := metav1.ListOptions{ - LabelSelector: l.labelSelector, - FieldSelector: l.fieldSelector, - } - - if items, err = l.resource.List(l.namespace, opts); err != nil { - return err - } - l.update(items) - - return nil -} - -func (l *list) update(items Columnars) { - first := len(l.cache) == 0 - kk := make([]string, 0, len(items)) - for _, i := range items { - kk = append(kk, i.Name()) - ff := i.Fields(l.namespace) - if first { - l.cache[i.Name()] = newRowEvent(New, ff, make(Row, len(ff))) - continue - } - dd := make(Row, len(ff)) - a := watch.Added - if evt, ok := l.cache[i.Name()]; ok { - a = computeDeltas(evt, ff[:len(ff)-1], dd) - } - l.cache[i.Name()] = newRowEvent(a, ff, dd) - } - - if first { - return - } - l.ensureDeletes(kk) -} - -// EnsureDeletes delete items in cache that are no longer valid. -func (l *list) ensureDeletes(kk []string) { - for k := range l.cache { - var found bool - for i, key := range kk { - if k == key { - found = true - kk = append(kk[:i], kk[i+1:]...) - break - } - } - if !found { - delete(l.cache, k) - } - } -} - -// Helpers... - -func computeDeltas(evt *RowEvent, newRow, deltas Row) watch.EventType { - oldRow := evt.Fields[:len(evt.Fields)-1] - a := Unchanged - if !reflect.DeepEqual(oldRow, newRow) { - for i, field := range oldRow { - if field != newRow[i] { - deltas[i] = field - } - } - a = watch.Modified - } - return a -} diff --git a/internal/resource/mock_connection_test.go b/internal/resource/mock_connection_test.go deleted file mode 100644 index d69887d0..00000000 --- a/internal/resource/mock_connection_test.go +++ /dev/null @@ -1,825 +0,0 @@ -// Code generated by pegomock. DO NOT EDIT. -// Source: github.com/derailed/k9s/internal/resource (interfaces: Connection) - -package resource_test - -import ( - k8s "github.com/derailed/k9s/internal/k8s" - 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) 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) CanIAccess(_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("CanIAccess", 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) CheckListNSAccess() error { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("CheckListNSAccess", 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) CheckNSAccess(_param0 string) error { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("CheckNSAccess", 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) Config() *k8s.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((**k8s.Config)(nil)).Elem()}) - var ret0 *k8s.Config - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*k8s.Config) - } - } - return ret0 -} - -func (mock *MockConnection) CurrentNamespaceName() (string, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - 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 *MockConnection) DialOrDie() kubernetes.Interface { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - 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 *MockConnection) DynDialOrDie() dynamic.Interface { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - 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 *MockConnection) FetchNodes() (*v1.NodeList, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("FetchNodes", 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 *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) IsNamespaced(_param0 string) bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - 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 *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) NSDialOrDie() dynamic.NamespaceableResourceInterface { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("NSDialOrDie", params, []reflect.Type{reflect.TypeOf((*dynamic.NamespaceableResourceInterface)(nil)).Elem()}) - var ret0 dynamic.NamespaceableResourceInterface - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(dynamic.NamespaceableResourceInterface) - } - } - return ret0 -} - -func (mock *MockConnection) NodePods(_param0 string) (*v1.PodList, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - 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 *MockConnection) RestConfigOrDie() *rest.Config { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - 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 *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) SupportsRes(_param0 string, _param1 []string) (string, bool, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - 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 *MockConnection) SupportsResource(_param0 string) bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - 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 *MockConnection) SwitchContextOrDie(_param0 string) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{_param0} - pegomock.GetGenericMockFrom(mock).Invoke("SwitchContextOrDie", params, []reflect.Type{}) -} - -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) 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) CanIAccess(_param0 string, _param1 string, _param2 []string) *MockConnection_CanIAccess_OngoingVerification { - params := []pegomock.Param{_param0, _param1, _param2} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CanIAccess", params, verifier.timeout) - return &MockConnection_CanIAccess_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_CanIAccess_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_CanIAccess_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_CanIAccess_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) CheckListNSAccess() *MockConnection_CheckListNSAccess_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CheckListNSAccess", params, verifier.timeout) - return &MockConnection_CheckListNSAccess_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_CheckListNSAccess_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_CheckListNSAccess_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_CheckListNSAccess_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) CheckNSAccess(_param0 string) *MockConnection_CheckNSAccess_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CheckNSAccess", params, verifier.timeout) - return &MockConnection_CheckNSAccess_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_CheckNSAccess_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_CheckNSAccess_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockConnection_CheckNSAccess_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) 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) CurrentNamespaceName() *MockConnection_CurrentNamespaceName_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CurrentNamespaceName", params, verifier.timeout) - return &MockConnection_CurrentNamespaceName_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_CurrentNamespaceName_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_CurrentNamespaceName_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_CurrentNamespaceName_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) DialOrDie() *MockConnection_DialOrDie_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "DialOrDie", params, verifier.timeout) - return &MockConnection_DialOrDie_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_DialOrDie_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_DialOrDie_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_DialOrDie_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) DynDialOrDie() *MockConnection_DynDialOrDie_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "DynDialOrDie", params, verifier.timeout) - return &MockConnection_DynDialOrDie_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_DynDialOrDie_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_DynDialOrDie_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_DynDialOrDie_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) FetchNodes() *MockConnection_FetchNodes_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "FetchNodes", params, verifier.timeout) - return &MockConnection_FetchNodes_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_FetchNodes_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_FetchNodes_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_FetchNodes_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) IsNamespaced(_param0 string) *MockConnection_IsNamespaced_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "IsNamespaced", params, verifier.timeout) - return &MockConnection_IsNamespaced_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_IsNamespaced_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_IsNamespaced_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockConnection_IsNamespaced_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) NSDialOrDie() *MockConnection_NSDialOrDie_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "NSDialOrDie", params, verifier.timeout) - return &MockConnection_NSDialOrDie_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_NSDialOrDie_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_NSDialOrDie_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_NSDialOrDie_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) NodePods(_param0 string) *MockConnection_NodePods_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "NodePods", params, verifier.timeout) - return &MockConnection_NodePods_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_NodePods_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_NodePods_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockConnection_NodePods_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) RestConfigOrDie() *MockConnection_RestConfigOrDie_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "RestConfigOrDie", params, verifier.timeout) - return &MockConnection_RestConfigOrDie_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_RestConfigOrDie_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_RestConfigOrDie_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_RestConfigOrDie_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) SupportsRes(_param0 string, _param1 []string) *MockConnection_SupportsRes_OngoingVerification { - params := []pegomock.Param{_param0, _param1} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SupportsRes", params, verifier.timeout) - return &MockConnection_SupportsRes_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_SupportsRes_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_SupportsRes_OngoingVerification) GetCapturedArguments() (string, []string) { - _param0, _param1 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1], _param1[len(_param1)-1] -} - -func (c *MockConnection_SupportsRes_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 [][]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) - } - } - return -} - -func (verifier *VerifierMockConnection) SupportsResource(_param0 string) *MockConnection_SupportsResource_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SupportsResource", params, verifier.timeout) - return &MockConnection_SupportsResource_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_SupportsResource_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_SupportsResource_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockConnection_SupportsResource_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) SwitchContextOrDie(_param0 string) *MockConnection_SwitchContextOrDie_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SwitchContextOrDie", params, verifier.timeout) - return &MockConnection_SwitchContextOrDie_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_SwitchContextOrDie_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_SwitchContextOrDie_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockConnection_SwitchContextOrDie_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/resource/mock_cruder_test.go b/internal/resource/mock_cruder_test.go deleted file mode 100644 index ec442c9d..00000000 --- a/internal/resource/mock_cruder_test.go +++ /dev/null @@ -1,218 +0,0 @@ -// Code generated by pegomock. DO NOT EDIT. -// Source: github.com/derailed/k9s/internal/resource (interfaces: Cruder) - -package resource_test - -import ( - k8s "github.com/derailed/k9s/internal/k8s" - pegomock "github.com/petergtz/pegomock" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "reflect" - "time" -) - -type MockCruder struct { - fail func(message string, callerSkip ...int) -} - -func NewMockCruder(options ...pegomock.Option) *MockCruder { - mock := &MockCruder{} - for _, option := range options { - option.Apply(mock) - } - return mock -} - -func (mock *MockCruder) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } -func (mock *MockCruder) FailHandler() pegomock.FailHandler { return mock.fail } - -func (mock *MockCruder) Delete(_param0 string, _param1 string, _param2 bool, _param3 bool) error { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockCruder().") - } - params := []pegomock.Param{_param0, _param1, _param2, _param3} - result := pegomock.GetGenericMockFrom(mock).Invoke("Delete", 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 *MockCruder) Get(_param0 string, _param1 string) (interface{}, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockCruder().") - } - params := []pegomock.Param{_param0, _param1} - result := pegomock.GetGenericMockFrom(mock).Invoke("Get", params, []reflect.Type{reflect.TypeOf((*interface{})(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 interface{} - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(interface{}) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockCruder) List(_param0 string, _param1 v1.ListOptions) (k8s.Collection, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockCruder().") - } - params := []pegomock.Param{_param0, _param1} - result := pegomock.GetGenericMockFrom(mock).Invoke("List", params, []reflect.Type{reflect.TypeOf((*k8s.Collection)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 k8s.Collection - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(k8s.Collection) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockCruder) VerifyWasCalledOnce() *VerifierMockCruder { - return &VerifierMockCruder{ - mock: mock, - invocationCountMatcher: pegomock.Times(1), - } -} - -func (mock *MockCruder) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockCruder { - return &VerifierMockCruder{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - } -} - -func (mock *MockCruder) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockCruder { - return &VerifierMockCruder{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - inOrderContext: inOrderContext, - } -} - -func (mock *MockCruder) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockCruder { - return &VerifierMockCruder{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - timeout: timeout, - } -} - -type VerifierMockCruder struct { - mock *MockCruder - invocationCountMatcher pegomock.Matcher - inOrderContext *pegomock.InOrderContext - timeout time.Duration -} - -func (verifier *VerifierMockCruder) Delete(_param0 string, _param1 string, _param2 bool, _param3 bool) *MockCruder_Delete_OngoingVerification { - params := []pegomock.Param{_param0, _param1, _param2, _param3} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Delete", params, verifier.timeout) - return &MockCruder_Delete_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockCruder_Delete_OngoingVerification struct { - mock *MockCruder - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockCruder_Delete_OngoingVerification) GetCapturedArguments() (string, string, bool, bool) { - _param0, _param1, _param2, _param3 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1], _param1[len(_param1)-1], _param2[len(_param2)-1], _param3[len(_param3)-1] -} - -func (c *MockCruder_Delete_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string, _param2 []bool, _param3 []bool) { - 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([]bool, len(c.methodInvocations)) - for u, param := range params[2] { - _param2[u] = param.(bool) - } - _param3 = make([]bool, len(c.methodInvocations)) - for u, param := range params[3] { - _param3[u] = param.(bool) - } - } - return -} - -func (verifier *VerifierMockCruder) Get(_param0 string, _param1 string) *MockCruder_Get_OngoingVerification { - params := []pegomock.Param{_param0, _param1} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Get", params, verifier.timeout) - return &MockCruder_Get_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockCruder_Get_OngoingVerification struct { - mock *MockCruder - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockCruder_Get_OngoingVerification) GetCapturedArguments() (string, string) { - _param0, _param1 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1], _param1[len(_param1)-1] -} - -func (c *MockCruder_Get_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []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) - } - } - return -} - -func (verifier *VerifierMockCruder) List(_param0 string, _param1 v1.ListOptions) *MockCruder_List_OngoingVerification { - params := []pegomock.Param{_param0, _param1} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "List", params, verifier.timeout) - return &MockCruder_List_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockCruder_List_OngoingVerification struct { - mock *MockCruder - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockCruder_List_OngoingVerification) GetCapturedArguments() (string, v1.ListOptions) { - _param0, _param1 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1], _param1[len(_param1)-1] -} - -func (c *MockCruder_List_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []v1.ListOptions) { - 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([]v1.ListOptions, len(c.methodInvocations)) - for u, param := range params[1] { - _param1[u] = param.(v1.ListOptions) - } - } - return -} diff --git a/internal/resource/mock_switchablecruder_test.go b/internal/resource/mock_switchablecruder_test.go deleted file mode 100644 index 85e02713..00000000 --- a/internal/resource/mock_switchablecruder_test.go +++ /dev/null @@ -1,292 +0,0 @@ -// Code generated by pegomock. DO NOT EDIT. -// Source: github.com/derailed/k9s/internal/resource (interfaces: SwitchableCruder) - -package resource_test - -import ( - k8s "github.com/derailed/k9s/internal/k8s" - pegomock "github.com/petergtz/pegomock" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "reflect" - "time" -) - -type MockSwitchableCruder struct { - fail func(message string, callerSkip ...int) -} - -func NewMockSwitchableCruder(options ...pegomock.Option) *MockSwitchableCruder { - mock := &MockSwitchableCruder{} - for _, option := range options { - option.Apply(mock) - } - return mock -} - -func (mock *MockSwitchableCruder) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } -func (mock *MockSwitchableCruder) FailHandler() pegomock.FailHandler { return mock.fail } - -func (mock *MockSwitchableCruder) Delete(_param0 string, _param1 string, _param2 bool, _param3 bool) error { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockSwitchableCruder().") - } - params := []pegomock.Param{_param0, _param1, _param2, _param3} - result := pegomock.GetGenericMockFrom(mock).Invoke("Delete", 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 *MockSwitchableCruder) Get(_param0 string, _param1 string) (interface{}, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockSwitchableCruder().") - } - params := []pegomock.Param{_param0, _param1} - result := pegomock.GetGenericMockFrom(mock).Invoke("Get", params, []reflect.Type{reflect.TypeOf((*interface{})(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 interface{} - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(interface{}) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockSwitchableCruder) List(_param0 string, _param1 v1.ListOptions) (k8s.Collection, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockSwitchableCruder().") - } - params := []pegomock.Param{_param0, _param1} - result := pegomock.GetGenericMockFrom(mock).Invoke("List", params, []reflect.Type{reflect.TypeOf((*k8s.Collection)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 k8s.Collection - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(k8s.Collection) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockSwitchableCruder) MustCurrentContextName() string { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockSwitchableCruder().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("MustCurrentContextName", 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 *MockSwitchableCruder) Switch(_param0 string) error { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockSwitchableCruder().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("Switch", 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 *MockSwitchableCruder) VerifyWasCalledOnce() *VerifierMockSwitchableCruder { - return &VerifierMockSwitchableCruder{ - mock: mock, - invocationCountMatcher: pegomock.Times(1), - } -} - -func (mock *MockSwitchableCruder) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockSwitchableCruder { - return &VerifierMockSwitchableCruder{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - } -} - -func (mock *MockSwitchableCruder) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockSwitchableCruder { - return &VerifierMockSwitchableCruder{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - inOrderContext: inOrderContext, - } -} - -func (mock *MockSwitchableCruder) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockSwitchableCruder { - return &VerifierMockSwitchableCruder{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - timeout: timeout, - } -} - -type VerifierMockSwitchableCruder struct { - mock *MockSwitchableCruder - invocationCountMatcher pegomock.Matcher - inOrderContext *pegomock.InOrderContext - timeout time.Duration -} - -func (verifier *VerifierMockSwitchableCruder) Delete(_param0 string, _param1 string, _param2 bool, _param3 bool) *MockSwitchableCruder_Delete_OngoingVerification { - params := []pegomock.Param{_param0, _param1, _param2, _param3} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Delete", params, verifier.timeout) - return &MockSwitchableCruder_Delete_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockSwitchableCruder_Delete_OngoingVerification struct { - mock *MockSwitchableCruder - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockSwitchableCruder_Delete_OngoingVerification) GetCapturedArguments() (string, string, bool, bool) { - _param0, _param1, _param2, _param3 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1], _param1[len(_param1)-1], _param2[len(_param2)-1], _param3[len(_param3)-1] -} - -func (c *MockSwitchableCruder_Delete_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string, _param2 []bool, _param3 []bool) { - 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([]bool, len(c.methodInvocations)) - for u, param := range params[2] { - _param2[u] = param.(bool) - } - _param3 = make([]bool, len(c.methodInvocations)) - for u, param := range params[3] { - _param3[u] = param.(bool) - } - } - return -} - -func (verifier *VerifierMockSwitchableCruder) Get(_param0 string, _param1 string) *MockSwitchableCruder_Get_OngoingVerification { - params := []pegomock.Param{_param0, _param1} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Get", params, verifier.timeout) - return &MockSwitchableCruder_Get_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockSwitchableCruder_Get_OngoingVerification struct { - mock *MockSwitchableCruder - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockSwitchableCruder_Get_OngoingVerification) GetCapturedArguments() (string, string) { - _param0, _param1 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1], _param1[len(_param1)-1] -} - -func (c *MockSwitchableCruder_Get_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []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) - } - } - return -} - -func (verifier *VerifierMockSwitchableCruder) List(_param0 string, _param1 v1.ListOptions) *MockSwitchableCruder_List_OngoingVerification { - params := []pegomock.Param{_param0, _param1} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "List", params, verifier.timeout) - return &MockSwitchableCruder_List_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockSwitchableCruder_List_OngoingVerification struct { - mock *MockSwitchableCruder - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockSwitchableCruder_List_OngoingVerification) GetCapturedArguments() (string, v1.ListOptions) { - _param0, _param1 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1], _param1[len(_param1)-1] -} - -func (c *MockSwitchableCruder_List_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []v1.ListOptions) { - 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([]v1.ListOptions, len(c.methodInvocations)) - for u, param := range params[1] { - _param1[u] = param.(v1.ListOptions) - } - } - return -} - -func (verifier *VerifierMockSwitchableCruder) MustCurrentContextName() *MockSwitchableCruder_MustCurrentContextName_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "MustCurrentContextName", params, verifier.timeout) - return &MockSwitchableCruder_MustCurrentContextName_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockSwitchableCruder_MustCurrentContextName_OngoingVerification struct { - mock *MockSwitchableCruder - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockSwitchableCruder_MustCurrentContextName_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockSwitchableCruder_MustCurrentContextName_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockSwitchableCruder) Switch(_param0 string) *MockSwitchableCruder_Switch_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Switch", params, verifier.timeout) - return &MockSwitchableCruder_Switch_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockSwitchableCruder_Switch_OngoingVerification struct { - mock *MockSwitchableCruder - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockSwitchableCruder_Switch_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockSwitchableCruder_Switch_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 -} diff --git a/internal/resource/no.go b/internal/resource/no.go deleted file mode 100644 index e5828ad7..00000000 --- a/internal/resource/no.go +++ /dev/null @@ -1,338 +0,0 @@ -package resource - -import ( - "strings" - - "k8s.io/apimachinery/pkg/util/sets" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -const ( - labelNodeRolePrefix = "node-role.kubernetes.io/" - nodeLabelRole = "kubernetes.io/role" -) - -// Node tracks a kubernetes resource. -type Node struct { - *Base - instance *v1.Node - metrics *mv1beta1.NodeMetrics -} - -// NewNodeList returns a new resource list. -func NewNodeList(c Connection, _ string) List { - return NewList( - NotNamespaced, - "no", - NewNode(c), - ViewAccess|DescribeAccess, - ) -} - -// NewNode instantiates a new Node. -func NewNode(c Connection) *Node { - n := &Node{ - Base: &Base{ - Connection: c, - Resource: k8s.NewNode(c), - }, - } - n.Factory = n - - return n -} - -// New builds a new Node instance from a k8s resource. -func (r *Node) New(i interface{}) Columnar { - c := NewNode(r.Connection) - switch instance := i.(type) { - case *v1.Node: - c.instance = instance - case v1.Node: - c.instance = &instance - default: - log.Fatal().Msgf("unknown Node type %#v", i) - } - c.path = c.namespacedName(c.instance.ObjectMeta) - - return c -} - -// SetNodeMetrics set the current k8s resource metrics on a given node. -func (r *Node) SetNodeMetrics(m *mv1beta1.NodeMetrics) { - r.metrics = m -} - -// List all resources for a given namespace. -func (r *Node) List(ns string, opts metav1.ListOptions) (Columnars, error) { - nn, err := r.Resource.List(ns, opts) - if err != nil { - return nil, err - } - - cc := make(Columnars, 0, len(nn)) - for i := range nn { - node := nn[i].(v1.Node) - no := r.New(&node).(*Node) - cc = append(cc, no) - } - - return cc, nil -} - -// Marshal a resource to yaml. -func (r *Node) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - log.Error().Err(err) - return "", err - } - - no := i.(*v1.Node) - no.TypeMeta.APIVersion = "v1" - no.TypeMeta.Kind = "Node" - - return r.marshalObject(no) -} - -// Header returns resource header. -func (*Node) Header(ns string) Row { - return Row{ - "NAME", - "STATUS", - "ROLE", - "VERSION", - "KERNEL", - "INTERNAL-IP", - "EXTERNAL-IP", - "CPU", - "MEM", - "%CPU", - "%MEM", - "ACPU", - "AMEM", - "AGE", - } -} - -// NumCols designates if column is numerical. -func (*Node) NumCols(n string) map[string]bool { - return map[string]bool{ - "CPU": true, - "MEM": true, - "%CPU": true, - "%MEM": true, - "ACPU": true, - "AMEM": true, - } -} - -// Fields returns displayable fields. -func (r *Node) Fields(ns string) Row { - ff := make(Row, 0, len(r.Header(ns))) - - no := r.instance - iIP, eIP := r.getIPs(no.Status.Addresses) - iIP, eIP = missing(iIP), missing(eIP) - - c, a, p := gatherNodeMX(no, r.metrics) - - sta := make([]string, 10) - r.status(no.Status, no.Spec.Unschedulable, sta) - ro := sets.NewString() - r.findNodeRoles(no, &ro) - - return append(ff, - no.Name, - join(sta, ","), - join(ro.List(), ","), - no.Status.NodeInfo.KubeletVersion, - no.Status.NodeInfo.KernelVersion, - iIP, - eIP, - c.cpu, - c.mem, - p.cpu, - p.mem, - a.cpu, - a.mem, - toAge(no.ObjectMeta.CreationTimestamp), - ) -} - -// ---------------------------------------------------------------------------- -// Helpers... - -type metric struct { - cpu, mem string -} - -func noMetric() metric { - return metric{cpu: NAValue, mem: NAValue} -} - -func gatherNodeMX(no *v1.Node, mx *mv1beta1.NodeMetrics) (c metric, a metric, p metric) { - c, a, p = noMetric(), noMetric(), noMetric() - if mx == nil { - return - } - - cpu := mx.Usage.Cpu().MilliValue() - mem := k8s.ToMB(mx.Usage.Memory().Value()) - c = metric{ - cpu: ToMillicore(cpu), - mem: ToMi(mem), - } - - acpu := no.Status.Allocatable.Cpu().MilliValue() - amem := k8s.ToMB(no.Status.Allocatable.Memory().Value()) - a = metric{ - cpu: ToMillicore(acpu), - mem: ToMi(amem), - } - - p = metric{ - cpu: AsPerc(toPerc(float64(cpu), float64(acpu))), - mem: AsPerc(toPerc(mem, amem)), - } - - return -} - -func (_ *Node) findNodeRoles(no *v1.Node, roles *sets.String) []string { - for k, v := range no.Labels { - switch { - case strings.HasPrefix(k, labelNodeRolePrefix): - if role := strings.TrimPrefix(k, labelNodeRolePrefix); len(role) > 0 { - roles.Insert(role) - } - case k == nodeLabelRole && v != "": - roles.Insert(v) - } - } - - if roles.Len() == 0 { - roles.Insert(MissingValue) - } - - return roles.List() -} - -func (*Node) getIPs(addrs []v1.NodeAddress) (iIP, eIP string) { - for _, a := range addrs { - switch a.Type { - case v1.NodeExternalIP: - eIP = a.Address - case v1.NodeInternalIP: - iIP = a.Address - } - } - - return -} - -func (*Node) status(status v1.NodeStatus, exempt bool, res []string) { - var index int - conditions := make(map[v1.NodeConditionType]*v1.NodeCondition) - for n := range status.Conditions { - cond := status.Conditions[n] - conditions[cond.Type] = &cond - } - - validConditions := []v1.NodeConditionType{v1.NodeReady} - for _, validCondition := range validConditions { - condition, ok := conditions[validCondition] - if !ok { - continue - } - neg := "" - if condition.Status != v1.ConditionTrue { - neg = "Not" - } - res[index] = neg + string(condition.Type) - index++ - - } - if len(res) == 0 { - res[index] = "Unknown" - index++ - } - if exempt { - res[index] = "SchedulingDisabled" - } -} - -func (r *Node) podsResources(name string) (v1.ResourceList, v1.ResourceList, error) { - reqs, limits := v1.ResourceList{}, v1.ResourceList{} - pods, err := r.Connection.NodePods(name) - if err != nil { - return reqs, limits, err - } - for _, p := range pods.Items { - preq, plim := podResources(&p) - for k, v := range preq { - if value, ok := reqs[k]; !ok { - reqs[k] = v.DeepCopy() - } else { - value.Add(v) - reqs[k] = value - } - } - for k, v := range plim { - if value, ok := limits[k]; !ok { - limits[k] = v.DeepCopy() - } else { - value.Add(v) - limits[k] = value - } - } - } - - return reqs, limits, nil -} - -func podResources(pod *v1.Pod) (v1.ResourceList, v1.ResourceList) { - reqs, limits := v1.ResourceList{}, v1.ResourceList{} - for _, container := range pod.Spec.Containers { - addResources(reqs, container.Resources.Requests) - addResources(limits, container.Resources.Limits) - } - // init containers define the minimum of any resource - for _, container := range pod.Spec.InitContainers { - maxResources(reqs, container.Resources.Requests) - maxResources(limits, container.Resources.Limits) - } - - return reqs, limits -} - -// AddResources adds the resources from l2 to l1. -func addResources(l1, l2 v1.ResourceList) { - for name, quantity := range l2 { - if value, ok := l1[name]; ok { - value.Add(quantity) - l1[name] = value - } else { - l1[name] = quantity.DeepCopy() - } - } -} - -// MaxResourceList sets list to the greater of l1/l2 for every resource. -func maxResources(l1, l2 v1.ResourceList) { - for name, quantity := range l2 { - if value, ok := l1[name]; ok { - if quantity.Cmp(value) > 0 { - l1[name] = quantity.DeepCopy() - } - } else { - l1[name] = quantity.DeepCopy() - } - } -} diff --git a/internal/resource/no_int_test.go b/internal/resource/no_int_test.go deleted file mode 100644 index c6e9460c..00000000 --- a/internal/resource/no_int_test.go +++ /dev/null @@ -1,120 +0,0 @@ -package resource - -import ( - "testing" - - "k8s.io/apimachinery/pkg/util/sets" - - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestNodeStatus(t *testing.T) { - uu := []struct { - s v1.NodeStatus - e string - }{ - { - v1.NodeStatus{ - Conditions: []v1.NodeCondition{ - { - Type: v1.NodeReady, - Status: v1.ConditionTrue, - }, - }, - }, - "Ready", - }, - } - - no := NewNode(nil) - for _, u := range uu { - res := make([]string, 5) - no.status(u.s, false, res) - assert.Equal(t, "Ready", join(res, ",")) - } -} - -func TestNodeRoles(t *testing.T) { - uu := []struct { - node v1.Node - roles []string - }{ - { - node: v1.Node{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - "kubernetes.io/role": "master", - "node-role.kubernetes.io/worker": "true", - }, - }, - }, - roles: []string{"master", "worker"}, - }, - - { - node: v1.Node{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - "node-role.kubernetes.io/worker": "true", - "kubernetes.io/role": "master", - }, - }, - }, - roles: []string{"master", "worker"}, - }, - - { - node: v1.Node{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - "kubernetes.io/role": "worker", - }, - }, - }, - roles: []string{"worker"}, - }, - - { - node: v1.Node{}, - roles: []string{""}, - }, - } - - no := NewNode(nil) - for _, u := range uu { - roles := sets.NewString() - no.findNodeRoles(&u.node, &roles) - assert.Equal(t, u.roles, roles.List()) - } -} - -func BenchmarkNodeFields(b *testing.B) { - n := NewNode(nil) - no := makeNode() - - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _ = n.New(no).Fields("") - } -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func makeNode() *v1.Node { - return &v1.Node{ - ObjectMeta: metav1.ObjectMeta{ - Name: "fred", - CreationTimestamp: metav1.Time{Time: testTime()}, - }, - Spec: v1.NodeSpec{}, - Status: v1.NodeStatus{ - Addresses: []v1.NodeAddress{ - {Address: "1.1.1.1"}, - }, - }, - } -} diff --git a/internal/resource/no_test.go b/internal/resource/no_test.go deleted file mode 100644 index d54da316..00000000 --- a/internal/resource/no_test.go +++ /dev/null @@ -1,159 +0,0 @@ -package resource_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - m "github.com/petergtz/pegomock" - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - res "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" - v1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -func NewNodeListWithArgs(ns string, r *resource.Node) resource.List { - return resource.NewList(resource.NotNamespaced, "no", r, resource.ViewAccess|resource.DescribeAccess) -} - -func NewNodeWithArgs(conn k8s.Connection, res resource.Cruder, mx resource.MetricsServer) *resource.Node { - r := &resource.Node{Base: resource.NewBase(conn, res)} - r.Factory = r - return r -} - -func TestNodeListAccess(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - mx := NewMockMetricsServer() - - ns := "blee" - l := NewNodeListWithArgs(resource.AllNamespaces, NewNodeWithArgs(mc, mr, mx)) - l.SetNamespace(ns) - - assert.Equal(t, resource.NotNamespaced, l.GetNamespace()) - assert.Equal(t, "no", l.GetName()) - for _, a := range []int{resource.ViewAccess} { - assert.True(t, l.Access(a)) - } -} - -func TestNodeFields(t *testing.T) { - r := newNode().Fields("blee") - assert.Equal(t, "fred", r[0]) -} - -func TestNodeMarshal(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("blee", "fred")).ThenReturn(k8sNode(), nil) - mx := NewMockMetricsServer() - - cm := NewNodeWithArgs(mc, mr, mx) - ma, err := cm.Marshal("blee/fred") - - mr.VerifyWasCalledOnce().Get("blee", "fred") - assert.Nil(t, err) - assert.Equal(t, noYaml(), ma) -} - -func TestNodeListData(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.List("-", metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sNode()}, nil) - mx := NewMockMetricsServer() - m.When(mx.HasMetrics()).ThenReturn(true) - m.When(mx.FetchNodesMetrics()). - ThenReturn(&mv1beta1.NodeMetricsList{Items: []mv1beta1.NodeMetrics{makeMxNode("fred", "100m", "100Mi")}}, nil) - - l := NewNodeListWithArgs("-", NewNodeWithArgs(mc, mr, mx)) - // Make sure we mrn get deltas! - for i := 0; i < 2; i++ { - err := l.Reconcile(nil, nil) - assert.Nil(t, err) - } - - mr.VerifyWasCalled(m.Times(2)).List("-", metav1.ListOptions{}) - td := l.Data() - assert.Equal(t, 1, len(td.Rows)) - assert.Equal(t, resource.NotNamespaced, l.GetNamespace()) - row, ok := td.Rows["fred"] - assert.True(t, ok) - assert.Equal(t, 14, len(row.Deltas)) - for _, d := range row.Deltas { - assert.Equal(t, "", d) - } - assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func k8sNode() *v1.Node { - return &v1.Node{ - ObjectMeta: metav1.ObjectMeta{ - Name: "fred", - CreationTimestamp: metav1.Time{Time: testTime()}, - }, - Spec: v1.NodeSpec{}, - Status: v1.NodeStatus{ - Addresses: []v1.NodeAddress{ - {Address: "1.1.1.1"}, - }, - }, - } -} - -func makeMxNode(name, cpu, mem string) mv1beta1.NodeMetrics { - return v1beta1.NodeMetrics{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - }, - Usage: makeRes(cpu, mem), - } -} - -func makeRes(c, m string) v1.ResourceList { - cpu, _ := res.ParseQuantity(c) - mem, _ := res.ParseQuantity(m) - - return v1.ResourceList{ - v1.ResourceCPU: cpu, - v1.ResourceMemory: mem, - } -} - -func newNode() resource.Columnar { - mc := NewMockConnection() - return resource.NewNode(mc).New(k8sNode()) -} - -func noYaml() string { - return `apiVersion: v1 -kind: Node -metadata: - creationTimestamp: "2018-12-14T17:36:43Z" - name: fred -spec: {} -status: - addresses: - - address: 1.1.1.1 - type: "" - daemonEndpoints: - kubeletEndpoint: - Port: 0 - nodeInfo: - architecture: "" - bootID: "" - containerRuntimeVersion: "" - kernelVersion: "" - kubeProxyVersion: "" - kubeletVersion: "" - machineID: "" - operatingSystem: "" - osImage: "" - systemUUID: "" -` -} diff --git a/internal/resource/np.go b/internal/resource/np.go deleted file mode 100644 index 14e33281..00000000 --- a/internal/resource/np.go +++ /dev/null @@ -1,218 +0,0 @@ -package resource - -import ( - "fmt" - "strings" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - networkingv1 "k8s.io/api/networking/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// NetworkPolicy tracks a kubernetes resource. -type NetworkPolicy struct { - *Base - instance *networkingv1.NetworkPolicy -} - -// NewNetworkPolicyList returns a new resource list. -func NewNetworkPolicyList(c Connection, ns string) List { - return NewList( - ns, - "netpol", - NewNetworkPolicy(c), - AllVerbsAccess|DescribeAccess, - ) -} - -// NewNetworkPolicy instantiates a new NetworkPolicy. -func NewNetworkPolicy(c Connection) *NetworkPolicy { - ds := &NetworkPolicy{&Base{Connection: c, Resource: k8s.NewNetworkPolicy(c)}, nil} - ds.Factory = ds - - return ds -} - -// New builds a new NetworkPolicy instance from a k8s resource. -func (r *NetworkPolicy) New(i interface{}) Columnar { - c := NewNetworkPolicy(r.Connection) - switch instance := i.(type) { - case *networkingv1.NetworkPolicy: - c.instance = instance - case networkingv1.NetworkPolicy: - c.instance = &instance - default: - log.Fatal().Msgf("unknown NetworkPolicy type %#v", i) - } - c.path = c.namespacedName(c.instance.ObjectMeta) - - return c -} - -// Marshal resource to yaml. -func (r *NetworkPolicy) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - - ds := i.(*networkingv1.NetworkPolicy) - ds.TypeMeta.APIVersion = "networking.k8s.io/v1" - ds.TypeMeta.Kind = "NetworkPolicy" - - return r.marshalObject(ds) -} - -// Header return resource header. -func (*NetworkPolicy) Header(ns string) Row { - hh := Row{} - if ns == AllNamespaces { - hh = append(hh, "NAMESPACE") - } - hh = append(hh, "NAME", "ING-SELECTOR", "ING-PORTS", "ING-BLOCK", "EGR-SELECTOR", "EGR-PORTS", "EGR-BLOCK", "AGE") - - return hh -} - -// Fields retrieves displayable fields. -func (r *NetworkPolicy) Fields(ns string) Row { - ff := make([]string, 0, len(r.Header(ns))) - - i := r.instance - if ns == AllNamespaces { - ff = append(ff, i.Namespace) - } - - ip, is, ib := ingress(i.Spec.Ingress) - ep, es, eb := egress(i.Spec.Egress) - - return append(ff, - i.Name, - is, - ip, - ib, - es, - ep, - eb, - toAge(i.ObjectMeta.CreationTimestamp), - ) -} - -// Helpers... - -func ingress(ii []networkingv1.NetworkPolicyIngressRule) (string, string, string) { - var ports, sels, blocks []string - for _, i := range ii { - if p := portsToStr(i.Ports); p != "" { - ports = append(ports, p) - } - ll, pp := peersToStr(i.From) - if ll != "" { - sels = append(sels, ll) - } - if pp != "" { - blocks = append(blocks, pp) - } - } - return strings.Join(ports, ","), strings.Join(sels, ","), strings.Join(blocks, ",") -} - -func egress(ee []networkingv1.NetworkPolicyEgressRule) (string, string, string) { - var ports, sels, blocks []string - for _, e := range ee { - if p := portsToStr(e.Ports); p != "" { - ports = append(ports, p) - } - ll, pp := peersToStr(e.To) - if ll != "" { - sels = append(sels, ll) - } - if pp != "" { - blocks = append(blocks, pp) - } - } - return strings.Join(ports, ","), strings.Join(sels, ","), strings.Join(blocks, ",") -} - -func portsToStr(pp []networkingv1.NetworkPolicyPort) string { - ports := make([]string, 0, len(pp)) - for _, p := range pp { - ports = append(ports, string(*p.Protocol)+":"+p.Port.String()) - } - return strings.Join(ports, ",") -} - -func peersToStr(pp []networkingv1.NetworkPolicyPeer) (string, string) { - sels := make([]string, 0, len(pp)) - ips := make([]string, 0, len(pp)) - for _, p := range pp { - if peer := renderPeer(p); peer != "" { - sels = append(sels, peer) - } - - if p.IPBlock == nil { - continue - } - if b := renderBlock(p.IPBlock); b != "" { - ips = append(ips, b) - } - } - return strings.Join(sels, ","), strings.Join(ips, ",") -} - -func renderBlock(b *networkingv1.IPBlock) string { - s := b.CIDR - - if len(b.Except) == 0 { - return s - } - - e, more := b.Except, false - if len(b.Except) > 2 { - e, more = e[:2], true - } - if more { - return s + "[" + strings.Join(e, ",") + "...]" - } - return s + "[" + strings.Join(b.Except, ",") + "]" -} - -func renderPeer(i networkingv1.NetworkPolicyPeer) string { - var s string - - if i.PodSelector != nil { - if len(i.PodSelector.MatchLabels) == 0 { - s += "po:all" - } else if m := mapToStr(i.PodSelector.MatchLabels); m != "" { - s += "po:" + m - } else if e := expToStr(i.PodSelector.MatchExpressions); e != "" { - s += "--" + e - } - } - - if i.NamespaceSelector != nil { - if len(i.NamespaceSelector.MatchLabels) == 0 { - s += "ns:all" - } else if m := mapToStr(i.NamespaceSelector.MatchLabels); m != "" { - s += "ns:" + m - } else if e := expToStr(i.NamespaceSelector.MatchExpressions); e != "" { - s += "--" + e - } - } - - return s -} - -func expToStr(ee []metav1.LabelSelectorRequirement) string { - ss := make([]string, len(ee)) - for i, e := range ee { - ss[i] = labToStr(e) - } - return strings.Join(ss, ",") -} - -func labToStr(e metav1.LabelSelectorRequirement) string { - return fmt.Sprintf("%s-%s%s", e.Key, e.Operator, strings.Join(e.Values, ",")) -} diff --git a/internal/resource/ns.go b/internal/resource/ns.go deleted file mode 100644 index 19335a79..00000000 --- a/internal/resource/ns.go +++ /dev/null @@ -1,80 +0,0 @@ -package resource - -import ( - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - v1 "k8s.io/api/core/v1" -) - -// Namespace tracks a kubernetes resource. -type Namespace struct { - *Base - instance *v1.Namespace -} - -// NewNamespaceList returns a new resource list. -func NewNamespaceList(c Connection, ns string) List { - return NewList( - NotNamespaced, - "ns", - NewNamespace(c), - CRUDAccess|DescribeAccess, - ) -} - -// NewNamespace instantiates a new Namespace. -func NewNamespace(c Connection) *Namespace { - n := &Namespace{&Base{Connection: c, Resource: k8s.NewNamespace(c)}, nil} - n.Factory = n - - return n -} - -// New builds a new Namespace instance from a k8s resource. -func (r *Namespace) New(i interface{}) Columnar { - c := NewNamespace(r.Connection) - switch instance := i.(type) { - case *v1.Namespace: - c.instance = instance - case v1.Namespace: - c.instance = &instance - default: - log.Fatal().Msgf("unknown Namespace type %#v", i) - } - c.path = c.namespacedName(c.instance.ObjectMeta) - - return c -} - -// Marshal a resource to yaml. -func (r *Namespace) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - log.Error().Err(err) - return "", err - } - - nss := i.(*v1.Namespace) - nss.TypeMeta.APIVersion = "v1" - nss.TypeMeta.Kind = "Namespace" - - return r.marshalObject(nss) -} - -// Header returns resource header. -func (*Namespace) Header(ns string) Row { - return Row{"NAME", "STATUS", "AGE"} -} - -// Fields returns displayable fields. -func (r *Namespace) Fields(ns string) Row { - ff := make(Row, 0, len(r.Header(ns))) - i := r.instance - - return append(ff, - i.Name, - string(i.Status.Phase), - toAge(i.ObjectMeta.CreationTimestamp), - ) -} diff --git a/internal/resource/ns_test.go b/internal/resource/ns_test.go deleted file mode 100644 index b7564670..00000000 --- a/internal/resource/ns_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package resource_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - 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 NewNamespaceListWithArgs(ns string, r *resource.Namespace) resource.List { - return resource.NewList(resource.NotNamespaced, "ns", r, resource.CRUDAccess|resource.DescribeAccess) -} - -func NewNamespaceWithArgs(conn k8s.Connection, res resource.Cruder) *resource.Namespace { - r := &resource.Namespace{Base: resource.NewBase(conn, res)} - r.Factory = r - return r -} - -func TestNamespaceListAccess(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - - ns := "blee" - l := NewNamespaceListWithArgs(resource.AllNamespaces, NewNamespaceWithArgs(mc, mr)) - l.SetNamespace(ns) - - assert.Equal(t, resource.NotNamespaced, l.GetNamespace()) - assert.Equal(t, "ns", l.GetName()) - for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} { - assert.True(t, l.Access(a)) - } -} - -func TestNamespaceFields(t *testing.T) { - r := newNamespace().Fields("blee") - assert.Equal(t, "fred", r[0]) -} - -func TestNamespaceMarshal(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("", "fred")).ThenReturn(k8sNamespace(), nil) - - cm := NewNamespaceWithArgs(mc, mr) - ma, err := cm.Marshal("fred") - - mr.VerifyWasCalledOnce().Get("", "fred") - assert.Nil(t, err) - assert.Equal(t, nsYaml(), ma) -} - -func TestNamespaceListData(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.List(resource.NotNamespaced, metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sNamespace()}, nil) - - l := NewNamespaceListWithArgs("-", NewNamespaceWithArgs(mc, mr)) - // Make sure we mrn get deltas! - for i := 0; i < 2; i++ { - err := l.Reconcile(nil, nil) - assert.Nil(t, err) - } - - mr.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced, metav1.ListOptions{}) - td := l.Data() - assert.Equal(t, 1, len(td.Rows)) - assert.Equal(t, resource.NotNamespaced, l.GetNamespace()) - row := td.Rows["blee/fred"] - assert.Equal(t, 3, len(row.Deltas)) - for _, d := range row.Deltas { - assert.Equal(t, "", d) - } - assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) -} - -// Helpers... - -func k8sNamespace() *v1.Namespace { - return &v1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "blee", - Name: "fred", - CreationTimestamp: metav1.Time{Time: testTime()}, - }, - } -} - -func newNamespace() resource.Columnar { - mc := NewMockConnection() - return resource.NewNamespace(mc).New(k8sNamespace()) -} - -func nsYaml() string { - return `apiVersion: v1 -kind: Namespace -metadata: - creationTimestamp: "2018-12-14T17:36:43Z" - name: fred - namespace: blee -spec: {} -status: {} -` -} diff --git a/internal/resource/pdb.go b/internal/resource/pdb.go deleted file mode 100644 index a7957f3f..00000000 --- a/internal/resource/pdb.go +++ /dev/null @@ -1,119 +0,0 @@ -package resource - -import ( - "strconv" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - v1beta1 "k8s.io/api/policy/v1beta1" - "k8s.io/apimachinery/pkg/util/intstr" -) - -// PodDisruptionBudget that can be displayed in a table and interacted with. -type PodDisruptionBudget struct { - *Base - - instance *v1beta1.PodDisruptionBudget -} - -// NewPDBList returns a new resource list. -func NewPDBList(c Connection, ns string) List { - return NewList( - ns, - "pdb", - NewPDB(c), - AllVerbsAccess|DescribeAccess, - ) -} - -// NewPDB instantiates a new PDB. -func NewPDB(c Connection) *PodDisruptionBudget { - p := &PodDisruptionBudget{&Base{Connection: c, Resource: k8s.NewPodDisruptionBudget(c)}, nil} - p.Factory = p - - return p -} - -// New builds a new PDB instance from a k8s resource. -func (r *PodDisruptionBudget) New(i interface{}) Columnar { - c := NewPDB(r.Connection) - switch instance := i.(type) { - case *v1beta1.PodDisruptionBudget: - c.instance = instance - case v1beta1.PodDisruptionBudget: - c.instance = &instance - case *interface{}: - ptr := *i.(*interface{}) - pdbi := ptr.(v1beta1.PodDisruptionBudget) - c.instance = &pdbi - default: - log.Fatal().Msgf("unknown PDB type %#v", i) - } - c.path = c.namespacedName(c.instance.ObjectMeta) - - return c -} - -// Marshal resource to yaml. -func (r *PodDisruptionBudget) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - - pdb := i.(*v1beta1.PodDisruptionBudget) - pdb.TypeMeta.APIVersion = "v1beta1" - pdb.TypeMeta.Kind = "PodDisruptionBudget" - - return r.marshalObject(pdb) -} - -// Header return resource header. -func (*PodDisruptionBudget) Header(ns string) Row { - hh := Row{} - if ns == AllNamespaces { - hh = append(hh, "NAMESPACE") - } - - return append(hh, - "NAME", - "MIN AVAILABLE", - "MAX_ UNAVAILABLE", - "ALLOWED DISRUPTIONS", - "CURRENT", - "DESIRED", - "EXPECTED", - "AGE", - ) -} - -// Fields retrieves displayable fields. -func (r *PodDisruptionBudget) Fields(ns string) Row { - ff := make(Row, 0, len(r.Header(ns))) - i := r.instance - - if ns == AllNamespaces { - ff = append(ff, i.Namespace) - } - - return append(ff, - i.Name, - numbToStr(i.Spec.MinAvailable), - numbToStr(i.Spec.MaxUnavailable), - strconv.Itoa(int(i.Status.PodDisruptionsAllowed)), - strconv.Itoa(int(i.Status.CurrentHealthy)), - strconv.Itoa(int(i.Status.DesiredHealthy)), - strconv.Itoa(int(i.Status.ExpectedPods)), - toAge(i.ObjectMeta.CreationTimestamp), - ) -} - -// Helpers... - -func numbToStr(n *intstr.IntOrString) string { - if n == nil { - return NAValue - } - return strconv.Itoa(int(n.IntVal)) -} diff --git a/internal/resource/pdb_test.go b/internal/resource/pdb_test.go deleted file mode 100644 index da6a343b..00000000 --- a/internal/resource/pdb_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package resource_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - m "github.com/petergtz/pegomock" - "github.com/stretchr/testify/assert" - v1beta1 "k8s.io/api/policy/v1beta1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func NewPDBListWithArgs(ns string, r *resource.PodDisruptionBudget) resource.List { - return resource.NewList(ns, "pdb", r, resource.AllVerbsAccess|resource.DescribeAccess) -} - -func NewPDBWithArgs(conn k8s.Connection, res resource.Cruder) *resource.PodDisruptionBudget { - r := &resource.PodDisruptionBudget{Base: resource.NewBase(conn, res)} - r.Factory = r - return r -} - -func TestPDBListAccess(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - - ns := "blee" - l := NewPDBListWithArgs(resource.AllNamespaces, NewPDBWithArgs(mc, mr)) - l.SetNamespace(ns) - - assert.Equal(t, ns, l.GetNamespace()) - assert.Equal(t, "pdb", l.GetName()) - for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} { - assert.True(t, l.Access(a)) - } -} - -func TestPDBFields(t *testing.T) { - r := newPDB().Fields("blee") - assert.Equal(t, "fred", r[0]) -} - -func TestPDBMarshal(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("blee", "fred")).ThenReturn(k8sPDB(), nil) - - cm := NewPDBWithArgs(mc, mr) - ma, err := cm.Marshal("blee/fred") - mr.VerifyWasCalledOnce().Get("blee", "fred") - assert.Nil(t, err) - assert.Equal(t, pdbYaml(), ma) -} - -func TestPDBListData(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.List("blee", metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sPDB()}, nil) - - l := NewPDBListWithArgs("blee", NewPDBWithArgs(mc, mr)) - // Make sure we mrn get deltas! - for i := 0; i < 2; i++ { - err := l.Reconcile(nil, nil) - assert.Nil(t, err) - } - - mr.VerifyWasCalled(m.Times(2)).List("blee", metav1.ListOptions{}) - td := l.Data() - assert.Equal(t, 1, len(td.Rows)) - assert.Equal(t, "blee", l.GetNamespace()) - row := td.Rows["blee/fred"] - assert.Equal(t, 8, len(row.Deltas)) - for _, d := range row.Deltas { - assert.Equal(t, "", d) - } - assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) -} - -// Helpers... - -func k8sPDB() *v1beta1.PodDisruptionBudget { - return &v1beta1.PodDisruptionBudget{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "blee", - Name: "fred", - CreationTimestamp: metav1.Time{Time: testTime()}, - }, - Spec: v1beta1.PodDisruptionBudgetSpec{}, - } -} - -func newPDB() resource.Columnar { - mc := NewMockConnection() - return resource.NewPDB(mc).New(k8sPDB()) -} - -func pdbYaml() string { - return `apiVersion: v1beta1 -kind: PodDisruptionBudget -metadata: - creationTimestamp: "2018-12-14T17:36:43Z" - name: fred - namespace: blee -spec: {} -status: - currentHealthy: 0 - desiredHealthy: 0 - disruptionsAllowed: 0 - expectedPods: 0 -` -} diff --git a/internal/resource/pod.go b/internal/resource/pod.go deleted file mode 100644 index b03c0c15..00000000 --- a/internal/resource/pod.go +++ /dev/null @@ -1,482 +0,0 @@ -package resource - -import ( - "bufio" - "context" - "fmt" - "io" - "strconv" - "sync/atomic" - "time" - - "github.com/derailed/k9s/internal/color" - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/watch" - "github.com/rs/zerolog/log" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -const ( - defaultTimeout = 1 * time.Second -) - -type ( - // IKey informer context key. - IKey string - - // Containers represents a resource that supports containers. - Containers interface { - Containers(path string, includeInit bool) ([]string, error) - } - - // Tailable represents a resource with tailable logs. - Tailable interface { - Logs(ctx context.Context, c chan<- string, opts LogOptions) error - } - - // TailableResource is a resource that have tailable logs. - TailableResource interface { - Resource - Tailable - } - - // Pod that can be displayed in a table and interacted with. - Pod struct { - *Base - instance *v1.Pod - metrics *mv1beta1.PodMetrics - } -) - -// NewPodList returns a new resource list. -func NewPodList(c Connection, ns string) List { - return NewList( - ns, - "po", - NewPod(c), - AllVerbsAccess|DescribeAccess, - ) -} - -// NewPod instantiates a new Pod. -func NewPod(c Connection) *Pod { - p := &Pod{ - Base: &Base{Connection: c, Resource: k8s.NewPod(c)}, - } - p.Factory = p - - return p -} - -// New builds a new Pod instance from a k8s resource. -func (r *Pod) New(i interface{}) Columnar { - c := NewPod(r.Connection) - switch instance := i.(type) { - case *v1.Pod: - c.instance = instance - case v1.Pod: - c.instance = &instance - case *interface{}: - ptr := *instance - po := ptr.(v1.Pod) - c.instance = &po - default: - log.Fatal().Msgf("unknown Pod type %#v", i) - } - c.path = c.namespacedName(c.instance.ObjectMeta) - - return c -} - -// SetPodMetrics set the current k8s resource metrics on a given pod. -func (r *Pod) SetPodMetrics(m *mv1beta1.PodMetrics) { - r.metrics = m -} - -// Marshal resource to yaml. -func (r *Pod) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - po := i.(*v1.Pod) - po.TypeMeta.APIVersion = "v1" - po.TypeMeta.Kind = "Pod" - - return r.marshalObject(po) -} - -// Containers lists out all the docker containers name contained in a pod. -func (r *Pod) Containers(path string, includeInit bool) ([]string, error) { - ns, po := Namespaced(path) - - return r.Resource.(k8s.Loggable).Containers(ns, po, includeInit) -} - -// PodLogs tail logs for all containers in a running Pod. -func (r *Pod) PodLogs(ctx context.Context, c chan<- string, opts LogOptions) error { - i := ctx.Value(IKey("informer")).(*watch.Informer) - p, err := i.Get(watch.PodIndex, opts.FQN(), metav1.GetOptions{}) - if err != nil { - return err - } - - po := p.(*v1.Pod) - opts.Color = asColor(po.Name) - if len(po.Spec.InitContainers)+len(po.Spec.Containers) == 1 { - opts.SingleContainer = true - } - - for _, co := range po.Spec.InitContainers { - opts.Container = co.Name - if err := r.Logs(ctx, c, opts); err != nil { - return err - } - } - rcos := r.loggableContainers(po.Status) - for _, co := range po.Spec.Containers { - if in(rcos, co.Name) { - opts.Container = co.Name - if err := r.Logs(ctx, c, opts); err != nil { - log.Error().Err(err).Msgf("Getting logs for %s failed", co.Name) - return err - } - } - } - - return nil -} - -// Logs tails a given container logs -func (r *Pod) Logs(ctx context.Context, c chan<- string, opts LogOptions) error { - if !opts.HasContainer() { - return r.PodLogs(ctx, c, opts) - } - res, ok := r.Resource.(k8s.Loggable) - if !ok { - return fmt.Errorf("Resource %T is not Loggable", r.Resource) - } - - return tailLogs(ctx, res, c, opts) -} - -func tailLogs(ctx context.Context, res k8s.Loggable, c chan<- string, opts LogOptions) error { - log.Debug().Msgf("Tailing logs for %q/%q:%q", opts.Namespace, opts.Name, opts.Container) - o := v1.PodLogOptions{ - Container: opts.Container, - Follow: true, - TailLines: &opts.Lines, - Previous: opts.Previous, - } - req := res.Logs(opts.Namespace, opts.Name, &o) - ctxt, cancelFunc := context.WithCancel(ctx) - req.Context(ctxt) - - var blocked int32 = 1 - go logsTimeout(cancelFunc, &blocked) - - // This call will block if nothing is in the stream!! - stream, err := req.Stream() - atomic.StoreInt32(&blocked, 0) - if err != nil { - log.Error().Err(err).Msgf("Log stream failed for `%s", opts.Path()) - return fmt.Errorf("Unable to obtain log stream for %s", opts.Path()) - } - go readLogs(ctx, stream, c, opts) - - return nil -} - -func logsTimeout(cancel context.CancelFunc, blocked *int32) { - select { - case <-time.After(defaultTimeout): - if atomic.LoadInt32(blocked) == 1 { - log.Debug().Msg("Timed out reading the log stream") - cancel() - } - } -} - -func readLogs(ctx context.Context, stream io.ReadCloser, c chan<- string, opts LogOptions) { - defer func() { - log.Debug().Msgf(">>> Closing stream `%s", opts.Path()) - stream.Close() - }() - - scanner := bufio.NewScanner(stream) - for scanner.Scan() { - select { - case <-ctx.Done(): - return - default: - c <- opts.DecorateLog(scanner.Text()) - } - } -} - -// List resources for a given namespace. -func (r *Pod) List(ns string, opts metav1.ListOptions) (Columnars, error) { - pods, err := r.Resource.List(ns, opts) - if err != nil { - return nil, err - } - - cc := make(Columnars, 0, len(pods)) - for i := range pods { - po := r.New(&pods[i]).(*Pod) - cc = append(cc, po) - } - - return cc, nil -} - -// Header return resource header. -func (*Pod) Header(ns string) Row { - hh := Row{} - if ns == AllNamespaces { - hh = append(hh, "NAMESPACE") - } - return append(hh, - "NAME", - "READY", - "STATUS", - "RS", - "CPU", - "MEM", - "%CPU", - "%MEM", - "IP", - "NODE", - "QOS", - "AGE", - ) -} - -// NumCols designates if column is numerical. -func (*Pod) NumCols(n string) map[string]bool { - return map[string]bool{ - "CPU": true, - "MEM": true, - "%CPU": true, - "%MEM": true, - "RS": true, - } -} - -// Fields retrieves displayable fields. -func (r *Pod) Fields(ns string) Row { - ff := make(Row, 0, len(r.Header(ns))) - i := r.instance - - if ns == AllNamespaces { - ff = append(ff, i.Namespace) - } - - ss := i.Status.ContainerStatuses - cr, _, rc := r.statuses(ss) - - c, p := r.gatherPodMX(i) - - return append(ff, - i.ObjectMeta.Name, - strconv.Itoa(cr)+"/"+strconv.Itoa(len(ss)), - r.phase(i), - strconv.Itoa(rc), - c.cpu, - c.mem, - p.cpu, - p.mem, - na(i.Status.PodIP), - na(i.Spec.NodeName), - r.mapQOS(i.Status.QOSClass), - toAge(i.ObjectMeta.CreationTimestamp), - ) -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func (r *Pod) gatherPodMX(po *v1.Pod) (c, p metric) { - c, p = noMetric(), noMetric() - if r.metrics == nil { - return - } - - cpu, mem := r.currentRes(r.metrics) - c = metric{ - cpu: ToMillicore(cpu.MilliValue()), - mem: ToMi(k8s.ToMB(mem.Value())), - } - - rc, rm := r.requestedRes(po) - p = metric{ - cpu: AsPerc(toPerc(float64(cpu.MilliValue()), float64(rc.MilliValue()))), - mem: AsPerc(toPerc(k8s.ToMB(mem.Value()), k8s.ToMB(rm.Value()))), - } - - return -} - -func containerResources(co v1.Container) (cpu, mem *resource.Quantity) { - req, limit := co.Resources.Requests, co.Resources.Limits - switch { - case len(req) != 0: - cpu, mem = req.Cpu(), req.Memory() - case len(limit) != 0: - cpu, mem = limit.Cpu(), limit.Memory() - } - return -} - -func (r *Pod) requestedRes(po *v1.Pod) (cpu, mem resource.Quantity) { - for _, co := range po.Spec.Containers { - c, m := containerResources(co) - if c != nil { - cpu.Add(*c) - } - if m != nil { - mem.Add(*m) - } - } - return -} - -func (*Pod) currentRes(mx *mv1beta1.PodMetrics) (cpu, mem resource.Quantity) { - for _, co := range mx.Containers { - c, m := co.Usage.Cpu(), co.Usage.Memory() - cpu.Add(*c) - mem.Add(*m) - } - return -} - -func (*Pod) mapQOS(class v1.PodQOSClass) string { - switch class { - case v1.PodQOSGuaranteed: - return "GA" - case v1.PodQOSBurstable: - return "BU" - default: - return "BE" - } -} - -func (r *Pod) statuses(ss []v1.ContainerStatus) (cr, ct, rc int) { - for _, c := range ss { - if c.State.Terminated != nil { - ct++ - } - if c.Ready { - cr = cr + 1 - } - rc += int(c.RestartCount) - } - - return -} - -func isSet(s *string) bool { - return s != nil && *s != "" -} - -func (r *Pod) phase(po *v1.Pod) string { - status := string(po.Status.Phase) - if po.Status.Reason != "" { - if po.DeletionTimestamp != nil && po.Status.Reason == "NodeLost" { - return "Unknown" - } - status = po.Status.Reason - } - - init, status := r.initContainerPhase(po.Status, len(po.Spec.InitContainers), status) - if init { - return status - } - - running, status := r.containerPhase(po.Status, status) - if running && status == "Completed" { - status = "Running" - } - if po.DeletionTimestamp == nil { - return status - } - - return "Terminating" -} - -func (*Pod) containerPhase(st v1.PodStatus, status string) (bool, string) { - var running bool - for i := len(st.ContainerStatuses) - 1; i >= 0; i-- { - cs := st.ContainerStatuses[i] - switch { - case cs.State.Waiting != nil && cs.State.Waiting.Reason != "": - status = cs.State.Waiting.Reason - case cs.State.Terminated != nil && cs.State.Terminated.Reason != "": - status = cs.State.Terminated.Reason - case cs.State.Terminated != nil: - if cs.State.Terminated.Signal != 0 { - status = "Signal:" + strconv.Itoa(int(cs.State.Terminated.Signal)) - } else { - status = "ExitCode:" + strconv.Itoa(int(cs.State.Terminated.ExitCode)) - } - case cs.Ready && cs.State.Running != nil: - running = true - } - } - - return running, status -} - -func (*Pod) initContainerPhase(st v1.PodStatus, initCount int, status string) (bool, string) { - for i, cs := range st.InitContainerStatuses { - status := checkContainerStatus(cs, i, initCount) - if status == "" { - continue - } - return true, status - } - - return false, status -} - -func checkContainerStatus(cs v1.ContainerStatus, i, initCount int) string { - switch { - case cs.State.Terminated != nil: - if cs.State.Terminated.ExitCode == 0 { - return "" - } - if cs.State.Terminated.Reason != "" { - return "Init:" + cs.State.Terminated.Reason - } - if cs.State.Terminated.Signal != 0 { - return "Init:Signal:" + strconv.Itoa(int(cs.State.Terminated.Signal)) - } - return "Init:ExitCode:" + strconv.Itoa(int(cs.State.Terminated.ExitCode)) - case cs.State.Waiting != nil && cs.State.Waiting.Reason != "" && cs.State.Waiting.Reason != "PodInitializing": - return "Init:" + cs.State.Waiting.Reason - default: - return "Init:" + strconv.Itoa(i) + "/" + strconv.Itoa(initCount) - } -} - -func (r *Pod) loggableContainers(s v1.PodStatus) []string { - var rcos []string - for _, c := range s.ContainerStatuses { - rcos = append(rcos, c.Name) - } - return rcos -} - -// Helpers.. - -func asColor(n string) color.Paint { - var sum int - for _, r := range n { - sum += int(r) - } - return color.Paint(30 + 2 + sum%6) -} diff --git a/internal/resource/pod_int_test.go b/internal/resource/pod_int_test.go deleted file mode 100644 index b332bcc4..00000000 --- a/internal/resource/pod_int_test.go +++ /dev/null @@ -1,182 +0,0 @@ -package resource - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestPodStatuses(t *testing.T) { - type counts struct { - ready, terminated, restarts int - } - - uu := []struct { - s []v1.ContainerStatus - e counts - }{ - { - []v1.ContainerStatus{ - { - Name: "c1", - Ready: true, - State: v1.ContainerState{ - Running: &v1.ContainerStateRunning{}, - }, - }, - { - Name: "c2", - Ready: false, - RestartCount: 10, - State: v1.ContainerState{ - Terminated: &v1.ContainerStateTerminated{}, - }, - }, - }, - counts{1, 1, 10}, - }, - } - - var p Pod - for _, u := range uu { - cr, ct, cs := p.statuses(u.s) - assert.Equal(t, u.e.ready, cr) - assert.Equal(t, u.e.terminated, ct) - assert.Equal(t, u.e.restarts, cs) - } -} - -func TestPodPhase(t *testing.T) { - uu := []struct { - p *v1.Pod - e string - }{ - {makePodStatus("p1", v1.PodRunning, ""), "Running"}, - {makePodStatus("p1", v1.PodRunning, "Evicted"), "Evicted"}, - {makePodStatus("p1", v1.PodPending, ""), "Pending"}, - {makePodStatus("p1", v1.PodSucceeded, ""), "Succeeded"}, - {makePodStatus("p1", v1.PodFailed, ""), "Failed"}, - {makePodStatus("p1", v1.PodUnknown, ""), "Unknown"}, - {makePodCoInitTerminated("p1"), "Init:OOMKilled"}, - {makePodCoInitWaiting("p1", ""), "Init:0/1"}, - {makePodCoInitWaiting("p1", "Waiting"), "Init:Waiting"}, - {makePodCoInitWaiting("p1", "PodInitializing"), "Init:0/1"}, - {makePodCoWaiting("p1", "Waiting"), "Waiting"}, - {makePodCoWaiting("p1", ""), ""}, - {makePodCoTerminated("p1", "OOMKilled", 0, true), "Terminating"}, - {makePodCoTerminated("p1", "OOMKilled", 0, false), "OOMKilled"}, - {makePodCoTerminated("p1", "", 0, true), "Terminating"}, - {makePodCoTerminated("p1", "", 0, false), "ExitCode:1"}, - {makePodCoTerminated("p1", "", 1, true), "Terminating"}, - {makePodCoTerminated("p1", "", 1, false), "Signal:1"}, - } - - var p Pod - for _, u := range uu { - assert.Equal(t, u.e, p.phase(u.p)) - } -} - -func makePodStatus(n string, phase v1.PodPhase, reason string) *v1.Pod { - po := makePod(n) - po.Status = v1.PodStatus{ - Phase: phase, - Reason: reason, - } - - return po -} - -func makePodCoInitTerminated(n string) *v1.Pod { - po := makePod(n) - - po.Status.InitContainerStatuses = []v1.ContainerStatus{ - { - State: v1.ContainerState{ - Terminated: &v1.ContainerStateTerminated{ - Reason: "OOMKilled", - ExitCode: 1, - }, - }, - }, - } - - return po -} - -func makePodCoInitWaiting(n, reason string) *v1.Pod { - po := makePod(n) - - po.Status.InitContainerStatuses = []v1.ContainerStatus{ - { - State: v1.ContainerState{ - Waiting: &v1.ContainerStateWaiting{ - Reason: reason, - }, - }, - }, - } - - return po -} - -func makePodCoTerminated(n, reason string, signal int32, deleted bool) *v1.Pod { - po := makePod(n) - - if deleted { - po.DeletionTimestamp = &metav1.Time{time.Now()} - } - po.Status.ContainerStatuses = []v1.ContainerStatus{ - { - State: v1.ContainerState{ - Terminated: &v1.ContainerStateTerminated{ - Reason: reason, - Signal: signal, - ExitCode: 1, - }, - }, - }, - } - - return po -} - -func makePodCoWaiting(n, reason string) *v1.Pod { - po := makePod(n) - - po.Status.ContainerStatuses = []v1.ContainerStatus{ - { - State: v1.ContainerState{ - Waiting: &v1.ContainerStateWaiting{ - Reason: reason, - }, - }, - }, - } - - return po -} - -func makePod(n string) *v1.Pod { - return &v1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: n, - Namespace: "default", - }, - Spec: v1.PodSpec{ - InitContainers: []v1.Container{ - { - Name: "ic1", - }, - }, - Containers: []v1.Container{ - { - Name: "c1", - }, - }, - }, - } -} diff --git a/internal/resource/pod_test.go b/internal/resource/pod_test.go deleted file mode 100644 index e0e6231e..00000000 --- a/internal/resource/pod_test.go +++ /dev/null @@ -1,272 +0,0 @@ -package resource_test - -import ( - "strings" - "testing" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - m "github.com/petergtz/pegomock" - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -func NewPodListWithArgs(ns string, r *resource.Pod) resource.List { - return resource.NewList(ns, "po", r, resource.AllVerbsAccess|resource.DescribeAccess) -} - -func NewPodWithArgs(conn k8s.Connection, res resource.Cruder, mx resource.MetricsServer) *resource.Pod { - r := &resource.Pod{Base: resource.NewBase(conn, res)} - r.Factory = r - return r -} - -func TestPodListAccess(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - mx := NewMockMetricsServer() - - ns := "blee" - l := NewPodListWithArgs(resource.AllNamespaces, NewPodWithArgs(mc, mr, mx)) - l.SetNamespace(ns) - - assert.Equal(t, "blee", l.GetNamespace()) - assert.Equal(t, "po", l.GetName()) - for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} { - assert.True(t, l.Access(a)) - } -} - -func TestPodFields(t *testing.T) { - r := newPod().Fields("blee") - assert.Equal(t, "fred", r[0]) -} - -func TestPodGatherMX(t *testing.T) { - uu := map[string]struct { - resources v1.ResourceRequirements - metrics mv1beta1.PodMetrics - expectedCpuPercentage string - expectedMemPercentage string - }{ - "request": { - v1.ResourceRequirements{ - Requests: makeRes("500m", "512Mi"), - }, - makeMxPod("fred", "250m", "256Mi"), - "150", - "150", - }, - "limit": { - v1.ResourceRequirements{ - Limits: makeRes("1000m", "1024Mi"), - }, - makeMxPod("fred", "250m", "256Mi"), - "75", - "75", - }, - "both": { - v1.ResourceRequirements{ - Requests: makeRes("500m", "512Mi"), - Limits: makeRes("1000m", "1024Mi"), - }, - makeMxPod("fred", "250m", "256Mi"), - "150", - "150", - }, - } - - for k, u := range uu { - t.Run(k, func(t *testing.T) { - r := NewPodWithMetrics(u.metrics, u.resources).Fields("blee") - - assert.Equal(t, u.expectedCpuPercentage, r[6]) - assert.Equal(t, u.expectedMemPercentage, r[7]) - }) - } -} - -func TestPodMarshal(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("blee", "fred")).ThenReturn(makePod(), nil) - mx := NewMockMetricsServer() - - cm := NewPodWithArgs(mc, mr, mx) - ma, err := cm.Marshal("blee/fred") - - mr.VerifyWasCalledOnce().Get("blee", "fred") - assert.Nil(t, err) - assert.Equal(t, poYaml(), ma) -} - -func TestPodListData(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.List("blee", metav1.ListOptions{})).ThenReturn(k8s.Collection{*makePod()}, nil) - mx := NewMockMetricsServer() - m.When(mx.HasMetrics()).ThenReturn(true) - m.When(mx.FetchPodsMetrics("blee")). - ThenReturn(&mv1beta1.PodMetricsList{Items: []mv1beta1.PodMetrics{makeMxPod("fred", "100m", "20Mi")}}, nil) - - l := NewPodListWithArgs("blee", NewPodWithArgs(mc, mr, mx)) - // Make sure we mcn get deltas! - for i := 0; i < 2; i++ { - err := l.Reconcile(nil, nil) - assert.Nil(t, err) - } - - mr.VerifyWasCalled(m.Times(2)).List("blee", metav1.ListOptions{}) - td := l.Data() - assert.Equal(t, 1, len(td.Rows)) - assert.Equal(t, "blee", l.GetNamespace()) - row := td.Rows["blee/fred"] - assert.Equal(t, 12, len(row.Deltas)) - for _, d := range row.Deltas { - assert.Equal(t, "", d) - } - assert.Equal(t, "fred", strings.TrimSpace(row.Fields[:1][0])) -} - -func BenchmarkPodFields(b *testing.B) { - p := resource.NewPod(nil) - po := makePod() - - b.ResetTimer() - b.ReportAllocs() - - for n := 0; n < b.N; n++ { - _ = p.New(po).Fields("") - } -} - -// ---------------------------------------------------------------------------- -// Helpers... -func makePodWithContainerSpec(resources v1.ResourceRequirements) *v1.Pod { - pod := makePod() - pod.Spec.Containers[0].Resources = resources - return pod -} - -func makePod() *v1.Pod { - var i int32 = 1 - var t = v1.HostPathDirectory - return &v1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "blee", - Name: "fred", - Labels: map[string]string{"blee": "duh"}, - CreationTimestamp: metav1.Time{Time: testTime()}, - }, - Spec: v1.PodSpec{ - Priority: &i, - PriorityClassName: "bozo", - Containers: []v1.Container{ - { - Name: "fred", - Image: "blee", - Env: []v1.EnvVar{ - { - Name: "fred", - Value: "1", - ValueFrom: &v1.EnvVarSource{ - ConfigMapKeyRef: &v1.ConfigMapKeySelector{Key: "blee"}, - }, - }, - }, - }, - }, - Volumes: []v1.Volume{ - { - Name: "fred", - VolumeSource: v1.VolumeSource{ - HostPath: &v1.HostPathVolumeSource{ - Path: "/blee", - Type: &t, - }, - }, - }, - }, - }, - Status: v1.PodStatus{ - Phase: "Running", - ContainerStatuses: []v1.ContainerStatus{ - { - Name: "fred", - State: v1.ContainerState{Running: &v1.ContainerStateRunning{}}, - RestartCount: 0, - }, - }, - }, - } -} - -func newPod() resource.Columnar { - mc := NewMockConnection() - return resource.NewPod(mc).New(makePod()) -} - -func NewPodWithMetrics(metrics mv1beta1.PodMetrics, resources v1.ResourceRequirements) resource.Columnar { - mc := NewMockConnection() - r := resource.NewPod(mc).New(makePodWithContainerSpec(resources)) - r.SetPodMetrics(&metrics) - return r -} - -func poYaml() string { - return `apiVersion: v1 -kind: Pod -metadata: - creationTimestamp: "2018-12-14T17:36:43Z" - labels: - blee: duh - name: fred - namespace: blee -spec: - containers: - - env: - - name: fred - value: "1" - valueFrom: - configMapKeyRef: - key: blee - image: blee - name: fred - resources: {} - priority: 1 - priorityClassName: bozo - volumes: - - hostPath: - path: /blee - type: Directory - name: fred -status: - containerStatuses: - - image: "" - imageID: "" - lastState: {} - name: fred - ready: false - restartCount: 0 - state: - running: - startedAt: null - phase: Running -` -} - -func makeMxPod(name, cpu, mem string) mv1beta1.PodMetrics { - return mv1beta1.PodMetrics{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: "default", - }, - Containers: []mv1beta1.ContainerMetrics{ - {Usage: makeRes(cpu, mem)}, - {Usage: makeRes(cpu, mem)}, - {Usage: makeRes(cpu, mem)}, - }, - } -} diff --git a/internal/resource/pv.go b/internal/resource/pv.go deleted file mode 100644 index 2701bb1c..00000000 --- a/internal/resource/pv.go +++ /dev/null @@ -1,154 +0,0 @@ -package resource - -import ( - "path" - "strings" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - v1 "k8s.io/api/core/v1" -) - -// PersistentVolume tracks a kubernetes resource. -type PersistentVolume struct { - *Base - instance *v1.PersistentVolume -} - -// NewPersistentVolumeList returns a new resource list. -func NewPersistentVolumeList(c Connection, ns string) List { - return NewList( - NotNamespaced, - "pv", - NewPersistentVolume(c), - CRUDAccess|DescribeAccess, - ) -} - -// NewPersistentVolume instantiates a new PersistentVolume. -func NewPersistentVolume(c Connection) *PersistentVolume { - p := &PersistentVolume{&Base{Connection: c, Resource: k8s.NewPersistentVolume(c)}, nil} - p.Factory = p - - return p -} - -// New builds a new PersistentVolume instance from a k8s resource. -func (r *PersistentVolume) New(i interface{}) Columnar { - c := NewPersistentVolume(r.Connection) - switch instance := i.(type) { - case *v1.PersistentVolume: - c.instance = instance - case v1.PersistentVolume: - c.instance = &instance - default: - log.Fatal().Msgf("unknown PersistentVolume type %#v", i) - } - c.path = c.namespacedName(c.instance.ObjectMeta) - - return c -} - -// Marshal resource to yaml. -func (r *PersistentVolume) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - - pv := i.(*v1.PersistentVolume) - pv.TypeMeta.APIVersion = "v1" - pv.TypeMeta.Kind = "PersistentVolume" - - return r.marshalObject(pv) -} - -// Header return resource header. -func (*PersistentVolume) Header(ns string) Row { - hh := Row{} - if ns == AllNamespaces { - hh = append(hh, "NAMESPACE") - } - - return append(hh, "NAME", "CAPACITY", "ACCESS MODES", "RECLAIM POLICY", "STATUS", "CLAIM", "STORAGECLASS", "REASON", "AGE") -} - -// Fields retrieves displayable fields. -func (r *PersistentVolume) Fields(ns string) Row { - ff := make(Row, 0, len(r.Header(ns))) - i := r.instance - if ns == AllNamespaces { - ff = append(ff, i.Namespace) - } - - phase := i.Status.Phase - if i.ObjectMeta.DeletionTimestamp != nil { - phase = "Terminating" - } - - var claim string - if i.Spec.ClaimRef != nil { - claim = path.Join(i.Spec.ClaimRef.Namespace, i.Spec.ClaimRef.Name) - } - - class, found := i.Annotations[v1.BetaStorageClassAnnotation] - if !found { - class = i.Spec.StorageClassName - } - - size := i.Spec.Capacity[v1.ResourceStorage] - - return append(ff, - i.Name, - size.String(), - r.accessMode(i.Spec.AccessModes), - string(i.Spec.PersistentVolumeReclaimPolicy), - string(phase), - claim, - class, - i.Status.Reason, - toAge(i.ObjectMeta.CreationTimestamp), - ) -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func (r *PersistentVolume) accessMode(aa []v1.PersistentVolumeAccessMode) string { - dd := r.accessDedup(aa) - s := make([]string, 0, len(dd)) - for i := 0; i < len(aa); i++ { - switch { - case r.accessContains(dd, v1.ReadWriteOnce): - s = append(s, "RWO") - case r.accessContains(dd, v1.ReadOnlyMany): - s = append(s, "ROX") - case r.accessContains(dd, v1.ReadWriteMany): - s = append(s, "RWX") - } - } - - return strings.Join(s, ",") -} - -func (r *PersistentVolume) accessContains(cc []v1.PersistentVolumeAccessMode, a v1.PersistentVolumeAccessMode) bool { - for _, c := range cc { - if c == a { - return true - } - } - - return false -} - -func (r *PersistentVolume) accessDedup(cc []v1.PersistentVolumeAccessMode) []v1.PersistentVolumeAccessMode { - set := []v1.PersistentVolumeAccessMode{} - for _, c := range cc { - if !r.accessContains(set, c) { - set = append(set, c) - } - } - - return set -} diff --git a/internal/resource/pv_test.go b/internal/resource/pv_test.go deleted file mode 100644 index 6436ee63..00000000 --- a/internal/resource/pv_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package resource_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - 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 NewPVListWithArgs(ns string, r *resource.PersistentVolume) resource.List { - return resource.NewList(resource.NotNamespaced, "pv", r, resource.CRUDAccess|resource.DescribeAccess) -} - -func NewPVWithArgs(conn k8s.Connection, res resource.Cruder) *resource.PersistentVolume { - r := &resource.PersistentVolume{Base: resource.NewBase(conn, res)} - r.Factory = r - return r -} - -func TestPVListAccess(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - - ns := "blee" - l := NewPVListWithArgs(resource.AllNamespaces, NewPVWithArgs(mc, mr)) - l.SetNamespace(ns) - - assert.Equal(t, resource.NotNamespaced, l.GetNamespace()) - assert.Equal(t, "pv", l.GetName()) - for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} { - assert.True(t, l.Access(a)) - } -} - -func TestPVFields(t *testing.T) { - r := newPV().Fields("blee") - assert.Equal(t, "fred", r[0]) -} - -func TestPVMarshal(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("blee", "fred")).ThenReturn(k8sPV(), nil) - - cm := NewPVWithArgs(mc, mr) - ma, err := cm.Marshal("blee/fred") - mr.VerifyWasCalledOnce().Get("blee", "fred") - assert.Nil(t, err) - assert.Equal(t, pvYaml(), ma) -} - -func TestPVListData(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.List(resource.NotNamespaced, metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sPV()}, nil) - - l := NewPVListWithArgs("-", NewPVWithArgs(mc, mr)) - // Make sure we mrn get deltas! - for i := 0; i < 2; i++ { - err := l.Reconcile(nil, nil) - assert.Nil(t, err) - } - - mr.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced, metav1.ListOptions{}) - td := l.Data() - assert.Equal(t, 1, len(td.Rows)) - assert.Equal(t, resource.NotNamespaced, l.GetNamespace()) - row := td.Rows["blee/fred"] - assert.Equal(t, 9, len(row.Deltas)) - for _, d := range row.Deltas { - assert.Equal(t, "", d) - } - assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) -} - -// Helpers... - -func k8sPV() *v1.PersistentVolume { - return &v1.PersistentVolume{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "blee", - Name: "fred", - CreationTimestamp: metav1.Time{Time: testTime()}, - }, - Spec: v1.PersistentVolumeSpec{}, - } -} - -func newPV() resource.Columnar { - mc := NewMockConnection() - return resource.NewPersistentVolume(mc).New(k8sPV()) -} - -func pvYaml() string { - return `apiVersion: v1 -kind: PersistentVolume -metadata: - creationTimestamp: "2018-12-14T17:36:43Z" - name: fred - namespace: blee -spec: {} -status: {} -` -} diff --git a/internal/resource/pvc.go b/internal/resource/pvc.go deleted file mode 100644 index 1dd97cc5..00000000 --- a/internal/resource/pvc.go +++ /dev/null @@ -1,112 +0,0 @@ -package resource - -import ( - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - v1 "k8s.io/api/core/v1" -) - -// PersistentVolumeClaim tracks a kubernetes resource. -type PersistentVolumeClaim struct { - *Base - instance *v1.PersistentVolumeClaim -} - -// NewPersistentVolumeClaimList returns a new resource list. -func NewPersistentVolumeClaimList(c Connection, ns string) List { - return NewList( - ns, - "pvc", - NewPersistentVolumeClaim(c), - AllVerbsAccess|DescribeAccess, - ) -} - -// NewPersistentVolumeClaim instantiates a new PersistentVolumeClaim. -func NewPersistentVolumeClaim(c Connection) *PersistentVolumeClaim { - p := &PersistentVolumeClaim{&Base{Connection: c, Resource: k8s.NewPersistentVolumeClaim(c)}, nil} - p.Factory = p - - return p -} - -// New builds a new PersistentVolumeClaim instance from a k8s resource. -func (r *PersistentVolumeClaim) New(i interface{}) Columnar { - c := NewPersistentVolumeClaim(r.Connection) - switch instance := i.(type) { - case *v1.PersistentVolumeClaim: - c.instance = instance - case v1.PersistentVolumeClaim: - c.instance = &instance - default: - log.Fatal().Msgf("unknown PersistentVolumeClaim type %#v", i) - } - c.path = c.namespacedName(c.instance.ObjectMeta) - - return c -} - -// Marshal resource to yaml. -func (r *PersistentVolumeClaim) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - - pvc := i.(*v1.PersistentVolumeClaim) - pvc.TypeMeta.APIVersion = "v1" - pvc.TypeMeta.Kind = "PersistentVolumeClaim" - - return r.marshalObject(pvc) -} - -// Header return resource header. -func (*PersistentVolumeClaim) Header(ns string) Row { - hh := Row{} - if ns == AllNamespaces { - hh = append(hh, "NAMESPACE") - } - - return append(hh, "NAME", "STATUS", "VOLUME", "CAPACITY", "ACCESS MODES", "STORAGECLASS", "AGE") -} - -// Fields retrieves displayable fields. -func (r *PersistentVolumeClaim) Fields(ns string) Row { - ff := make(Row, 0, len(r.Header(ns))) - i := r.instance - if ns == AllNamespaces { - ff = append(ff, i.Namespace) - } - - phase := i.Status.Phase - if i.ObjectMeta.DeletionTimestamp != nil { - phase = "Terminating" - } - - var pv PersistentVolume - storage := i.Spec.Resources.Requests[v1.ResourceStorage] - var capacity, accessModes string - if i.Spec.VolumeName != "" { - accessModes = pv.accessMode(i.Status.AccessModes) - storage = i.Status.Capacity[v1.ResourceStorage] - capacity = storage.String() - } - - class, found := i.Annotations[v1.BetaStorageClassAnnotation] - if !found { - if i.Spec.StorageClassName != nil { - class = *i.Spec.StorageClassName - } - } - - return append(ff, - i.Name, - string(phase), - i.Spec.VolumeName, - capacity, - accessModes, - class, - toAge(i.ObjectMeta.CreationTimestamp), - ) -} diff --git a/internal/resource/pvc_test.go b/internal/resource/pvc_test.go deleted file mode 100644 index b0a50454..00000000 --- a/internal/resource/pvc_test.go +++ /dev/null @@ -1,120 +0,0 @@ -package resource_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - m "github.com/petergtz/pegomock" - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - resv1 "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func NewPVCListWithArgs(ns string, r *resource.PersistentVolumeClaim) resource.List { - return resource.NewList(ns, "pvc", r, resource.AllVerbsAccess|resource.DescribeAccess) -} - -func NewPVCWithArgs(conn k8s.Connection, res resource.Cruder) *resource.PersistentVolumeClaim { - r := &resource.PersistentVolumeClaim{Base: resource.NewBase(conn, res)} - r.Factory = r - return r -} - -func TestPVCListAccess(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - - ns := "blee" - l := NewPVCListWithArgs(resource.AllNamespaces, NewPVCWithArgs(mc, mr)) - l.SetNamespace(ns) - - assert.Equal(t, "blee", l.GetNamespace()) - assert.Equal(t, "pvc", l.GetName()) - for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} { - assert.True(t, l.Access(a)) - } -} - -func TestPVCFields(t *testing.T) { - r := newPVC().Fields("blee") - assert.Equal(t, "fred", r[0]) -} - -func TestPVCMarshal(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("blee", "fred")).ThenReturn(k8sPVC(), nil) - - cm := NewPVCWithArgs(mc, mr) - ma, err := cm.Marshal("blee/fred") - mr.VerifyWasCalledOnce().Get("blee", "fred") - assert.Nil(t, err) - assert.Equal(t, pvcYaml(), ma) -} - -func TestPVCListData(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.List("blee", metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sPVC()}, nil) - - l := NewPVCListWithArgs("blee", NewPVCWithArgs(mc, mr)) - // Make sure we mrn get deltas! - for i := 0; i < 2; i++ { - err := l.Reconcile(nil, nil) - assert.Nil(t, err) - } - - mr.VerifyWasCalled(m.Times(2)).List("blee", metav1.ListOptions{}) - td := l.Data() - assert.Equal(t, 1, len(td.Rows)) - assert.Equal(t, "blee", l.GetNamespace()) - row := td.Rows["blee/fred"] - assert.Equal(t, 7, len(row.Deltas)) - for _, d := range row.Deltas { - assert.Equal(t, "", d) - } - assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) -} - -// Helpers... - -func k8sPVC() *v1.PersistentVolumeClaim { - return &v1.PersistentVolumeClaim{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "blee", - Name: "fred", - CreationTimestamp: metav1.Time{Time: testTime()}, - }, - Spec: v1.PersistentVolumeClaimSpec{ - VolumeName: "duh", - Resources: v1.ResourceRequirements{ - Requests: v1.ResourceList{ - v1.ResourceStorage: resv1.Quantity{}, - }, - }, - }, - } -} - -func newPVC() resource.Columnar { - mc := NewMockConnection() - return resource.NewPersistentVolumeClaim(mc).New(k8sPVC()) -} - -func pvcYaml() string { - return `apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - creationTimestamp: "2018-12-14T17:36:43Z" - name: fred - namespace: blee -spec: - resources: - requests: - storage: "0" - volumeName: duh -status: {} -` -} diff --git a/internal/resource/rc.go b/internal/resource/rc.go deleted file mode 100644 index ec8f621f..00000000 --- a/internal/resource/rc.go +++ /dev/null @@ -1,96 +0,0 @@ -package resource - -import ( - "strconv" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - v1 "k8s.io/api/core/v1" -) - -// ReplicationController tracks a kubernetes resource. -type ReplicationController struct { - *Base - instance *v1.ReplicationController -} - -// NewReplicationControllerList returns a new resource list. -func NewReplicationControllerList(c Connection, ns string) List { - return NewList( - ns, - "rc", - NewReplicationController(c), - AllVerbsAccess|DescribeAccess, - ) -} - -// NewReplicationController instantiates a new ReplicationController. -func NewReplicationController(c Connection) *ReplicationController { - r := &ReplicationController{&Base{Connection: c, Resource: k8s.NewReplicationController(c)}, nil} - r.Factory = r - - return r -} - -// New builds a new ReplicationController instance from a k8s resource. -func (r *ReplicationController) New(i interface{}) Columnar { - c := NewReplicationController(r.Connection) - switch instance := i.(type) { - case *v1.ReplicationController: - c.instance = instance - case v1.ReplicationController: - c.instance = &instance - default: - log.Fatal().Msgf("unknown ReplicationController type %#v", i) - } - c.path = c.namespacedName(c.instance.ObjectMeta) - - return c -} - -// Marshal a deployment given a namespaced name. -func (r *ReplicationController) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - - rc := i.(*v1.ReplicationController) - rc.TypeMeta.APIVersion = "v1" - rc.TypeMeta.Kind = "ReplicationController" - - return r.marshalObject(rc) -} - -// Header return resource header. -func (*ReplicationController) Header(ns string) Row { - hh := Row{} - if ns == AllNamespaces { - hh = append(hh, "NAMESPACE") - } - - return append(hh, "NAME", "DESIRED", "CURRENT", "READY", "AGE") -} - -// Fields retrieves displayable fields. -func (r *ReplicationController) Fields(ns string) Row { - ff := make(Row, 0, len(r.Header(ns))) - if ns == AllNamespaces { - ff = append(ff, r.instance.Namespace) - } - i := r.instance - - return append(ff, - i.Name, - strconv.Itoa(int(*i.Spec.Replicas)), - strconv.Itoa(int(i.Status.Replicas)), - strconv.Itoa(int(i.Status.ReadyReplicas)), - toAge(i.ObjectMeta.CreationTimestamp), - ) -} - -// Scale the specified resource. -func (r *ReplicationController) Scale(ns, n string, replicas int32) error { - return r.Resource.(Scalable).Scale(ns, n, replicas) -} diff --git a/internal/resource/rc_test.go b/internal/resource/rc_test.go deleted file mode 100644 index d01f4cc0..00000000 --- a/internal/resource/rc_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package resource_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - 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 NewRCListWithArgs(ns string, r *resource.ReplicationController) resource.List { - return resource.NewList(ns, "rc", r, resource.AllVerbsAccess|resource.DescribeAccess) -} - -func NewRCWithArgs(conn k8s.Connection, res resource.Cruder) *resource.ReplicationController { - r := &resource.ReplicationController{Base: resource.NewBase(conn, res)} - r.Factory = r - return r -} - -func TestRCListAccess(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - - ns := "blee" - l := NewRCListWithArgs(resource.AllNamespaces, NewRCWithArgs(mc, mr)) - l.SetNamespace(ns) - - assert.Equal(t, "blee", l.GetNamespace()) - assert.Equal(t, "rc", l.GetName()) - for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} { - assert.True(t, l.Access(a)) - } -} - -func TestRCFields(t *testing.T) { - r := newRC().Fields("blee") - assert.Equal(t, "fred", r[0]) -} - -func TestRCMarshal(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("blee", "fred")).ThenReturn(k8sRC(), nil) - - cm := NewRCWithArgs(mc, mr) - ma, err := cm.Marshal("blee/fred") - - mr.VerifyWasCalledOnce().Get("blee", "fred") - assert.Nil(t, err) - assert.Equal(t, rcYaml(), ma) -} - -func TestRCListData(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.List("blee", metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sRC()}, nil) - - l := NewRCListWithArgs("blee", NewRCWithArgs(mc, mr)) - // Make sure we mrn get deltas! - for i := 0; i < 2; i++ { - err := l.Reconcile(nil, nil) - assert.Nil(t, err) - } - - mr.VerifyWasCalled(m.Times(2)).List("blee", metav1.ListOptions{}) - td := l.Data() - assert.Equal(t, 1, len(td.Rows)) - assert.Equal(t, "blee", l.GetNamespace()) - row := td.Rows["blee/fred"] - assert.Equal(t, 5, len(row.Deltas)) - for _, d := range row.Deltas { - assert.Equal(t, "", d) - } - assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) -} - -// Helpers... - -func k8sRC() *v1.ReplicationController { - var c int32 = 10 - return &v1.ReplicationController{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "blee", - Name: "fred", - CreationTimestamp: metav1.Time{Time: testTime()}, - }, - Spec: v1.ReplicationControllerSpec{ - Replicas: &c, - }, - } -} - -func newRC() resource.Columnar { - mc := NewMockConnection() - return resource.NewReplicationController(mc).New(k8sRC()) -} - -func rcYaml() string { - return `apiVersion: v1 -kind: ReplicationController -metadata: - creationTimestamp: "2018-12-14T17:36:43Z" - name: fred - namespace: blee -spec: - replicas: 10 -status: - replicas: 0 -` -} diff --git a/internal/resource/ro.go b/internal/resource/ro.go deleted file mode 100644 index e515fb8f..00000000 --- a/internal/resource/ro.go +++ /dev/null @@ -1,106 +0,0 @@ -package resource - -import ( - "strings" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - v1 "k8s.io/api/rbac/v1" -) - -// Role tracks a kubernetes resource. -type Role struct { - *Base - instance *v1.Role -} - -// NewRoleList returns a new resource list. -func NewRoleList(c Connection, ns string) List { - return NewList( - ns, - "role", - NewRole(c), - AllVerbsAccess|DescribeAccess, - ) -} - -// NewRole instantiates a new Role. -func NewRole(c Connection) *Role { - r := &Role{&Base{Connection: c, Resource: k8s.NewRole(c)}, nil} - r.Factory = r - - return r -} - -// New builds a new Role instance from a k8s resource. -func (r *Role) New(i interface{}) Columnar { - c := NewRole(r.Connection) - switch instance := i.(type) { - case *v1.Role: - c.instance = instance - case v1.Role: - c.instance = &instance - default: - log.Fatal().Msgf("unknown Role type %#v", i) - } - c.path = c.namespacedName(c.instance.ObjectMeta) - - return c -} - -// Marshal resource to yaml. -func (r *Role) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - - role := i.(*v1.Role) - role.TypeMeta.APIVersion = "rbac.authorization.k8s.io/v1" - role.TypeMeta.Kind = "Role" - - return r.marshalObject(role) -} - -// Header return resource header. -func (*Role) Header(ns string) Row { - hh := Row{} - if ns == AllNamespaces { - hh = append(hh, "NAMESPACE") - } - - return append(hh, "NAME", "AGE") -} - -// Fields retrieves displayable fields. -func (r *Role) Fields(ns string) Row { - ff := make(Row, 0, len(r.Header(ns))) - - i := r.instance - if ns == AllNamespaces { - ff = append(ff, i.Namespace) - } - - return append(ff, - i.Name, - toAge(i.ObjectMeta.CreationTimestamp), - ) -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func (r *Role) parseRules(pp []v1.PolicyRule) []Row { - acc := make([]Row, len(pp)) - - for i, p := range pp { - acc[i] = make(Row, 0, 4) - acc[i] = append(acc[i], strings.Join(p.Resources, ", ")) - acc[i] = append(acc[i], strings.Join(p.NonResourceURLs, ", ")) - acc[i] = append(acc[i], strings.Join(p.ResourceNames, ", ")) - acc[i] = append(acc[i], strings.Join(p.Verbs, ", ")) - } - - return acc -} diff --git a/internal/resource/ro_binding.go b/internal/resource/ro_binding.go deleted file mode 100644 index bf665a46..00000000 --- a/internal/resource/ro_binding.go +++ /dev/null @@ -1,92 +0,0 @@ -package resource - -import ( - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - v1 "k8s.io/api/rbac/v1" -) - -// RoleBinding tracks a kubernetes resource. -type RoleBinding struct { - *Base - instance *v1.RoleBinding -} - -// NewRoleBindingList returns a new resource list. -func NewRoleBindingList(c Connection, ns string) List { - return NewList( - ns, - "rolebinding", - NewRoleBinding(c), - AllVerbsAccess|DescribeAccess, - ) -} - -// NewRoleBinding instantiates a new RoleBinding. -func NewRoleBinding(c Connection) *RoleBinding { - r := &RoleBinding{&Base{Connection: c, Resource: k8s.NewRoleBinding(c)}, nil} - r.Factory = r - - return r -} - -// New builds a new RoleBinding instance from a k8s resource. -func (r *RoleBinding) New(i interface{}) Columnar { - c := NewRoleBinding(r.Connection) - switch instance := i.(type) { - case *v1.RoleBinding: - c.instance = instance - case v1.RoleBinding: - c.instance = &instance - default: - log.Fatal().Msgf("unknown RoleBinding type %#v", i) - } - c.path = c.namespacedName(c.instance.ObjectMeta) - - return c -} - -// Marshal resource to yaml. -func (r *RoleBinding) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - - rb := i.(*v1.RoleBinding) - rb.TypeMeta.APIVersion = "rbac.authorization.k8s.io/v1" - rb.TypeMeta.Kind = "RoleBinding" - - return r.marshalObject(rb) -} - -// Header return resource header. -func (*RoleBinding) Header(ns string) Row { - hh := Row{} - if ns == AllNamespaces { - hh = append(hh, "NAMESPACE") - } - - return append(hh, "NAME", "ROLE", "KIND", "SUBJECTS", "AGE") -} - -// Fields retrieves displayable fields. -func (r *RoleBinding) Fields(ns string) Row { - i := r.instance - - ff := make(Row, 0, len(r.Header(ns))) - if ns == AllNamespaces { - ff = append(ff, i.Namespace) - } - - kind, ss := renderSubjects(i.Subjects) - - return append(ff, - i.Name, - i.RoleRef.Name, - kind, - ss, - toAge(i.ObjectMeta.CreationTimestamp), - ) -} diff --git a/internal/resource/ro_binding_int_test.go b/internal/resource/ro_binding_int_test.go deleted file mode 100644 index e1a47e74..00000000 --- a/internal/resource/ro_binding_int_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package resource - -import ( - "testing" - - "github.com/stretchr/testify/assert" - rbacv1 "k8s.io/api/rbac/v1" -) - -func TestToSubjectAlias(t *testing.T) { - uu := []struct { - i string - e string - }{ - {rbacv1.UserKind, "USR"}, - {rbacv1.GroupKind, "GRP"}, - {rbacv1.ServiceAccountKind, "SA"}, - {"fred", "FRED"}, - } - for _, u := range uu { - assert.Equal(t, u.e, toSubjectAlias(u.i)) - } -} - -func TestRenderSubjects(t *testing.T) { - uu := []struct { - ss []rbacv1.Subject - ek string - e string - }{ - { - []rbacv1.Subject{ - {Name: "blee", Kind: rbacv1.UserKind}, - }, - "USR", - "blee", - }, - { - []rbacv1.Subject{}, - NAValue, - "", - }, - } - for _, u := range uu { - kind, ss := renderSubjects(u.ss) - assert.Equal(t, u.e, ss) - assert.Equal(t, u.ek, kind) - } -} - -func BenchmarkToSubjects(b *testing.B) { - ss := []rbacv1.Subject{ - {Name: "blee", Kind: rbacv1.UserKind}, - } - - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - renderSubjects(ss) - } -} diff --git a/internal/resource/ro_binding_test.go b/internal/resource/ro_binding_test.go deleted file mode 100644 index 9c3d5c61..00000000 --- a/internal/resource/ro_binding_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package resource_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - m "github.com/petergtz/pegomock" - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func NewRBListWithArgs(ns string, r *resource.RoleBinding) resource.List { - return resource.NewList(ns, "rb", r, resource.AllVerbsAccess|resource.DescribeAccess) -} - -func NewRBWithArgs(conn k8s.Connection, res resource.Cruder) *resource.RoleBinding { - r := &resource.RoleBinding{Base: resource.NewBase(conn, res)} - r.Factory = r - return r -} - -func TestRBMarshal(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("blee", "fred")).ThenReturn(k8sRB(), nil) - - cm := NewRBWithArgs(mc, mr) - ma, err := cm.Marshal("blee/fred") - - mr.VerifyWasCalledOnce().Get("blee", "fred") - assert.Nil(t, err) - assert.Equal(t, rbYaml(), ma) -} - -func TestRBListData(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.List("blee", metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sRB()}, nil) - - l := NewRBListWithArgs("blee", NewRBWithArgs(mc, mr)) - // Make sure we mrn get deltas! - for i := 0; i < 2; i++ { - err := l.Reconcile(nil, nil) - assert.Nil(t, err) - } - - mr.VerifyWasCalled(m.Times(2)).List("blee", metav1.ListOptions{}) - td := l.Data() - assert.Equal(t, 1, len(td.Rows)) - assert.Equal(t, "blee", l.GetNamespace()) - row := td.Rows["blee/fred"] - assert.Equal(t, 5, len(row.Deltas)) - for _, d := range row.Deltas { - assert.Equal(t, "", d) - } - assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) -} - -// Helpers... - -func k8sRB() *v1.RoleBinding { - return &v1.RoleBinding{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "blee", - Name: "fred", - CreationTimestamp: metav1.Time{Time: testTime()}, - }, - Subjects: []v1.Subject{ - { - Kind: v1.UserKind, - Name: "fred", - Namespace: "blee", - }, - }, - RoleRef: v1.RoleRef{ - Kind: v1.UserKind, - Name: "duh", - }, - } -} - -func newRB() resource.Columnar { - mc := NewMockConnection() - return resource.NewRoleBinding(mc).New(k8sRB()) -} - -func rbYaml() string { - return `apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - creationTimestamp: "2018-12-14T17:36:43Z" - name: fred - namespace: blee -roleRef: - apiGroup: "" - kind: User - name: duh -subjects: -- kind: User - name: fred - namespace: blee -` -} diff --git a/internal/resource/ro_int_test.go b/internal/resource/ro_int_test.go deleted file mode 100644 index 8b251030..00000000 --- a/internal/resource/ro_int_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package resource - -import ( - "testing" - - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/rbac/v1" -) - -func TestRoleParseRules(t *testing.T) { - rules := []v1.PolicyRule{ - { - Resources: []string{"", "apps"}, - NonResourceURLs: []string{"/fred"}, - ResourceNames: []string{"pods", "deployments"}, - Verbs: []string{"get", "list"}, - }, - } - - var r Role - rows := r.parseRules(rules) - - assert.Equal(t, 1, len(rows)) - assert.Equal(t, 1, len(rows)) -} diff --git a/internal/resource/ro_test.go b/internal/resource/ro_test.go deleted file mode 100644 index d362e57b..00000000 --- a/internal/resource/ro_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package resource_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - m "github.com/petergtz/pegomock" - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func NewRoleListWithArgs(ns string, r *resource.Role) resource.List { - return resource.NewList(ns, "ro", r, resource.AllVerbsAccess|resource.DescribeAccess) -} - -func NewRoleWithArgs(conn k8s.Connection, res resource.Cruder) *resource.Role { - r := &resource.Role{Base: resource.NewBase(conn, res)} - r.Factory = r - return r -} - -func TestRoleMarshal(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("blee", "fred")).ThenReturn(k8sRole(), nil) - - cm := NewRoleWithArgs(mc, mr) - ma, err := cm.Marshal("blee/fred") - mr.VerifyWasCalledOnce().Get("blee", "fred") - assert.Nil(t, err) - assert.Equal(t, roleYaml(), ma) -} - -func TestRoleListData(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.List("blee", metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sRole()}, nil) - - l := NewRoleListWithArgs("blee", NewRoleWithArgs(mc, mr)) - // Make sure we mrn get deltas! - for i := 0; i < 2; i++ { - err := l.Reconcile(nil, nil) - assert.Nil(t, err) - } - - mr.VerifyWasCalled(m.Times(2)).List("blee", metav1.ListOptions{}) - td := l.Data() - assert.Equal(t, 1, len(td.Rows)) - assert.Equal(t, "blee", l.GetNamespace()) - row := td.Rows["blee/fred"] - assert.Equal(t, 2, len(row.Deltas)) - for _, d := range row.Deltas { - assert.Equal(t, "", d) - } - assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) -} - -// Helpers... - -func k8sRole() *v1.Role { - return &v1.Role{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "blee", - Name: "fred", - CreationTimestamp: metav1.Time{Time: testTime()}, - }, - } -} - -func newRole() resource.Columnar { - mc := NewMockConnection() - return resource.NewRole(mc).New(k8sRole()) -} - -func roleYaml() string { - return `apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - creationTimestamp: "2018-12-14T17:36:43Z" - name: fred - namespace: blee -rules: null -` -} diff --git a/internal/resource/rs.go b/internal/resource/rs.go deleted file mode 100644 index 895936f2..00000000 --- a/internal/resource/rs.go +++ /dev/null @@ -1,92 +0,0 @@ -package resource - -import ( - "strconv" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - v1 "k8s.io/api/apps/v1" -) - -// ReplicaSet tracks a kubernetes resource. -type ReplicaSet struct { - *Base - instance *v1.ReplicaSet -} - -// NewReplicaSetList returns a new resource list. -func NewReplicaSetList(c Connection, ns string) List { - return NewList( - ns, - "rs", - NewReplicaSet(c), - AllVerbsAccess|DescribeAccess, - ) -} - -// NewReplicaSet instantiates a new ReplicaSet. -func NewReplicaSet(c Connection) *ReplicaSet { - r := &ReplicaSet{&Base{Connection: c, Resource: k8s.NewReplicaSet(c)}, nil} - r.Factory = r - - return r -} - -// New builds a new ReplicaSet instance from a k8s resource. -func (r *ReplicaSet) New(i interface{}) Columnar { - c := NewReplicaSet(r.Connection) - switch instance := i.(type) { - case *v1.ReplicaSet: - c.instance = instance - case v1.ReplicaSet: - c.instance = &instance - default: - log.Fatal().Msgf("unknown ReplicaSet type %#v", i) - } - c.path = c.namespacedName(c.instance.ObjectMeta) - - return c -} - -// Marshal a deployment given a namespaced name. -func (r *ReplicaSet) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - - rs := i.(*v1.ReplicaSet) - rs.TypeMeta.APIVersion = "apps/v1" - rs.TypeMeta.Kind = "ReplicaSet" - - return r.marshalObject(rs) -} - -// Header return resource header. -func (*ReplicaSet) Header(ns string) Row { - hh := Row{} - if ns == AllNamespaces { - hh = append(hh, "NAMESPACE") - } - - return append(hh, "NAME", "DESIRED", "CURRENT", "READY", "AGE") -} - -// Fields retrieves displayable fields. -func (r *ReplicaSet) Fields(ns string) Row { - i := r.instance - - ff := make(Row, 0, len(r.Header(ns))) - if ns == AllNamespaces { - ff = append(ff, i.Namespace) - } - - return append(ff, - i.Name, - strconv.Itoa(int(*i.Spec.Replicas)), - strconv.Itoa(int(i.Status.Replicas)), - strconv.Itoa(int(i.Status.ReadyReplicas)), - toAge(i.ObjectMeta.CreationTimestamp), - ) -} diff --git a/internal/resource/rs_test.go b/internal/resource/rs_test.go deleted file mode 100644 index cec881dd..00000000 --- a/internal/resource/rs_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package resource_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - m "github.com/petergtz/pegomock" - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func NewReplicaSetListWithArgs(ns string, r *resource.ReplicaSet) resource.List { - return resource.NewList(ns, "rs", r, resource.AllVerbsAccess|resource.DescribeAccess) -} - -func NewReplicaSetWithArgs(conn k8s.Connection, res resource.Cruder) *resource.ReplicaSet { - r := &resource.ReplicaSet{Base: resource.NewBase(conn, res)} - r.Factory = r - return r -} - -func TestReplicaSetMarshal(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("blee", "fred")).ThenReturn(k8sReplicaSet(), nil) - - cm := NewReplicaSetWithArgs(mc, mr) - ma, err := cm.Marshal("blee/fred") - mr.VerifyWasCalledOnce().Get("blee", "fred") - assert.Nil(t, err) - assert.Equal(t, rsYaml(), ma) -} - -func TestReplicaSetListData(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.List("blee", metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sReplicaSet()}, nil) - - l := NewReplicaSetListWithArgs("blee", NewReplicaSetWithArgs(mc, mr)) - // Make sure we can get deltas! - for i := 0; i < 2; i++ { - err := l.Reconcile(nil, nil) - assert.Nil(t, err) - } - - mr.VerifyWasCalled(m.Times(2)).List("blee", metav1.ListOptions{}) - td := l.Data() - assert.Equal(t, 1, len(td.Rows)) - assert.Equal(t, "blee", l.GetNamespace()) - row := td.Rows["blee/fred"] - assert.Equal(t, 5, len(row.Deltas)) - for _, d := range row.Deltas { - assert.Equal(t, "", d) - } - assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) -} - -// Helpers... - -func k8sReplicaSet() *v1.ReplicaSet { - var i int32 = 1 - return &v1.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "blee", - Name: "fred", - CreationTimestamp: metav1.Time{Time: testTime()}, - }, - Spec: v1.ReplicaSetSpec{ - Replicas: &i, - }, - Status: v1.ReplicaSetStatus{ - ReadyReplicas: 1, - Replicas: 1, - }, - } -} - -func newReplicaSet() resource.Columnar { - mc := NewMockConnection() - return resource.NewReplicaSet(mc).New(k8sReplicaSet()) -} - -func rsYaml() string { - return `apiVersion: apps/v1 -kind: ReplicaSet -metadata: - creationTimestamp: "2018-12-14T17:36:43Z" - name: fred - namespace: blee -spec: - replicas: 1 - selector: null - template: - metadata: - creationTimestamp: null - spec: - containers: null -status: - readyReplicas: 1 - replicas: 1 -` -} diff --git a/internal/resource/sa.go b/internal/resource/sa.go deleted file mode 100644 index 61ebcb86..00000000 --- a/internal/resource/sa.go +++ /dev/null @@ -1,89 +0,0 @@ -package resource - -import ( - "strconv" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - v1 "k8s.io/api/core/v1" -) - -// ServiceAccount represents a Kubernetes resource. -type ServiceAccount struct { - *Base - instance *v1.ServiceAccount -} - -// NewServiceAccountList returns a new resource list. -func NewServiceAccountList(c Connection, ns string) List { - return NewList( - ns, - "sa", - NewServiceAccount(c), - AllVerbsAccess|DescribeAccess, - ) -} - -// NewServiceAccount instantiates a new ServiceAccount. -func NewServiceAccount(c Connection) *ServiceAccount { - s := &ServiceAccount{&Base{Connection: c, Resource: k8s.NewServiceAccount(c)}, nil} - s.Factory = s - - return s -} - -// New builds a new ServiceAccount instance from a k8s resource. -func (r *ServiceAccount) New(i interface{}) Columnar { - c := NewServiceAccount(r.Connection) - switch instance := i.(type) { - case *v1.ServiceAccount: - c.instance = instance - case v1.ServiceAccount: - c.instance = &instance - default: - log.Fatal().Msgf("unknown ServiceAccount type %#v", i) - } - c.path = c.namespacedName(c.instance.ObjectMeta) - - return c -} - -// Marshal resource to yaml. -func (r *ServiceAccount) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - - sa := i.(*v1.ServiceAccount) - sa.TypeMeta.APIVersion = "v1" - sa.TypeMeta.Kind = "ServiceAccount" - - return r.marshalObject(sa) -} - -// Header return resource header. -func (*ServiceAccount) Header(ns string) Row { - hh := Row{} - if ns == AllNamespaces { - hh = append(hh, "NAMESPACE") - } - - return append(hh, "NAME", "SECRET", "AGE") -} - -// Fields retrieves displayable fields. -func (r *ServiceAccount) Fields(ns string) Row { - ff := make(Row, 0, len(r.Header(ns))) - i := r.instance - if ns == AllNamespaces { - ff = append(ff, i.Namespace) - } - - return append(ff, - i.Name, - strconv.Itoa(len(i.Secrets)), - toAge(i.ObjectMeta.CreationTimestamp), - ) -} diff --git a/internal/resource/sa_test.go b/internal/resource/sa_test.go deleted file mode 100644 index f2597374..00000000 --- a/internal/resource/sa_test.go +++ /dev/null @@ -1,129 +0,0 @@ -package resource_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - 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 NewServiceAccountListWithArgs(ns string, r *resource.ServiceAccount) resource.List { - return resource.NewList(ns, "sa", r, resource.AllVerbsAccess|resource.DescribeAccess) -} - -func NewServiceAccountWithArgs(conn k8s.Connection, res resource.Cruder) *resource.ServiceAccount { - r := &resource.ServiceAccount{Base: resource.NewBase(conn, res)} - r.Factory = r - return r -} - -func TestSaListAccess(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - - ns := "blee" - l := NewServiceAccountListWithArgs(resource.AllNamespaces, NewServiceAccountWithArgs(mc, mr)) - l.SetNamespace(ns) - - assert.Equal(t, ns, l.GetNamespace()) - assert.Equal(t, "sa", l.GetName()) - for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} { - assert.True(t, l.Access(a)) - } -} - -func TestSaHeader(t *testing.T) { - s := newSa() - e := append(resource.Row{"NAMESPACE"}, saHeader()...) - assert.Equal(t, e, s.Header(resource.AllNamespaces)) - assert.Equal(t, saHeader(), s.Header("fred")) -} - -func TestSaFields(t *testing.T) { - uu := []struct { - i resource.Columnar - e resource.Row - }{ - {i: newSa(), e: resource.Row{"blee", "fred", "1"}}, - } - - for _, u := range uu { - assert.Equal(t, "blee/fred", u.i.Name()) - assert.Equal(t, u.e, u.i.Fields(resource.AllNamespaces)[:3]) - assert.Equal(t, u.e[1:], u.i.Fields("blee")[:2]) - } -} - -func TestSAMarshal(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("blee", "fred")).ThenReturn(k8sSA(), nil) - - cm := NewServiceAccountWithArgs(mc, mr) - ma, err := cm.Marshal("blee/fred") - mr.VerifyWasCalledOnce().Get("blee", "fred") - assert.Nil(t, err) - assert.Equal(t, saYaml(), ma) -} - -func TestSAListData(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.List("blee", metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sSA()}, nil) - - l := NewServiceAccountListWithArgs("blee", NewServiceAccountWithArgs(mc, mr)) - // Make sure we mrn get deltas! - for i := 0; i < 2; i++ { - err := l.Reconcile(nil, nil) - assert.Nil(t, err) - } - - mr.VerifyWasCalled(m.Times(2)).List("blee", metav1.ListOptions{}) - td := l.Data() - assert.Equal(t, 1, len(td.Rows)) - assert.Equal(t, "blee", l.GetNamespace()) - row := td.Rows["blee/fred"] - assert.Equal(t, 3, len(row.Deltas)) - for _, d := range row.Deltas { - assert.Equal(t, "", d) - } - assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) -} - -// Helpers... - -func k8sSA() *v1.ServiceAccount { - return &v1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Name: "fred", - Namespace: "blee", - CreationTimestamp: metav1.Time{testTime()}, - }, - Secrets: []v1.ObjectReference{{Name: "blee"}}, - } -} - -func newSa() resource.Columnar { - mc := NewMockConnection() - return resource.NewServiceAccount(mc).New(k8sSA()) -} - -func saHeader() resource.Row { - return resource.Row{"NAME", "SECRET", "AGE"} -} - -func saYaml() string { - return `apiVersion: v1 -kind: ServiceAccount -metadata: - creationTimestamp: "2018-12-14T17:36:43Z" - name: fred - namespace: blee -secrets: -- name: blee -` -} diff --git a/internal/resource/sc.go b/internal/resource/sc.go deleted file mode 100644 index 8b547c34..00000000 --- a/internal/resource/sc.go +++ /dev/null @@ -1,87 +0,0 @@ -package resource - -import ( - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - v1 "k8s.io/api/storage/v1" -) - -// StorageClass tracks a kubernetes resource. -type StorageClass struct { - *Base - instance *v1.StorageClass -} - -// NewStorageClassList returns a new resource list. -func NewStorageClassList(c Connection, ns string) List { - return NewList( - NotNamespaced, - "sc", - NewStorageClass(c), - CRUDAccess|DescribeAccess, - ) -} - -// NewStorageClass instantiates a new StorageClass. -func NewStorageClass(c Connection) *StorageClass { - p := &StorageClass{&Base{Connection: c, Resource: k8s.NewStorageClass(c)}, nil} - p.Factory = p - - return p -} - -// New builds a new StorageClass instance from a k8s resource. -func (r *StorageClass) New(i interface{}) Columnar { - c := NewStorageClass(r.Connection) - switch instance := i.(type) { - case *v1.StorageClass: - c.instance = instance - case v1.StorageClass: - c.instance = &instance - default: - log.Fatal().Msgf("unknown StorageClass type %#v", i) - } - c.path = c.namespacedName(c.instance.ObjectMeta) - - return c -} - -// Marshal resource to yaml. -func (r *StorageClass) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - - sc := i.(*v1.StorageClass) - sc.TypeMeta.APIVersion = "storage.k8s.io/v1" - sc.TypeMeta.Kind = "StorageClass" - - return r.marshalObject(sc) -} - -// Header return resource header. -func (*StorageClass) Header(ns string) Row { - hh := Row{} - if ns == AllNamespaces { - hh = append(hh, "NAMESPACE") - } - - return append(hh, "NAME", "PROVISIONER", "AGE") -} - -// Fields retrieves displayable fields. -func (r *StorageClass) Fields(ns string) Row { - ff := make(Row, 0, len(r.Header(ns))) - i := r.instance - if ns == AllNamespaces { - ff = append(ff, i.Namespace) - } - - return append(ff, - i.Name, - string(i.Provisioner), - toAge(i.ObjectMeta.CreationTimestamp), - ) -} diff --git a/internal/resource/sc_test.go b/internal/resource/sc_test.go deleted file mode 100644 index cd193096..00000000 --- a/internal/resource/sc_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package resource_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - m "github.com/petergtz/pegomock" - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/storage/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func NewSCListWithArgs(ns string, r *resource.StorageClass) resource.List { - return resource.NewList(resource.NotNamespaced, "sc", r, resource.CRUDAccess|resource.DescribeAccess) -} - -func NewSCWithArgs(conn k8s.Connection, res resource.Cruder) *resource.StorageClass { - r := &resource.StorageClass{Base: resource.NewBase(conn, res)} - r.Factory = r - return r -} - -func TestSCListAccess(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - - ns := "blee" - l := NewSCListWithArgs(resource.AllNamespaces, NewSCWithArgs(mc, mr)) - l.SetNamespace(ns) - - assert.Equal(t, resource.NotNamespaced, l.GetNamespace()) - assert.Equal(t, "sc", l.GetName()) - for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} { - assert.True(t, l.Access(a)) - } -} - -func TestSCFields(t *testing.T) { - r := newSC().Fields("blee") - assert.Equal(t, "storage-test", r[0]) -} - -func TestSCMarshal(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("blee", "storage-test")).ThenReturn(k8sSC(), nil) - - cm := NewSCWithArgs(mc, mr) - ma, err := cm.Marshal("blee/storage-test") - mr.VerifyWasCalledOnce().Get("blee", "storage-test") - assert.Nil(t, err) - assert.Equal(t, scYaml(), ma) -} - -func TestSCListData(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.List(resource.NotNamespaced, metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sSC()}, nil) - - l := NewSCListWithArgs("-", NewSCWithArgs(mc, mr)) - // Make sure we mrn get deltas! - for i := 0; i < 2; i++ { - err := l.Reconcile(nil, nil) - assert.Nil(t, err) - } - - mr.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced, metav1.ListOptions{}) - td := l.Data() - assert.Equal(t, 1, len(td.Rows)) - assert.Equal(t, resource.NotNamespaced, l.GetNamespace()) - row := td.Rows["storage-test"] - assert.Equal(t, 3, len(row.Deltas)) - for _, d := range row.Deltas { - assert.Equal(t, "", d) - } - assert.Equal(t, resource.Row{"storage-test"}, row.Fields[:1]) -} - -// Helpers... - -func k8sSC() *v1.StorageClass { - return &v1.StorageClass{ - ObjectMeta: metav1.ObjectMeta{ - Name: "storage-test", - CreationTimestamp: metav1.Time{Time: testTime()}, - }, - } -} - -func newSC() resource.Columnar { - mc := NewMockConnection() - return resource.NewStorageClass(mc).New(k8sSC()) -} - -func scYaml() string { - return `apiVersion: storage.k8s.io/v1 -kind: StorageClass -metadata: - creationTimestamp: "2018-12-14T17:36:43Z" - name: storage-test -provisioner: "" -` -} diff --git a/internal/resource/secret.go b/internal/resource/secret.go deleted file mode 100644 index 26adbb1d..00000000 --- a/internal/resource/secret.go +++ /dev/null @@ -1,6 +0,0 @@ -package resource - -// NewSecretList returns a new resource list. -func NewSecretList(c Connection, ns string) List { - return NewCustomList(c, true, "", "v1/secrets") -} diff --git a/internal/resource/sts.go b/internal/resource/sts.go deleted file mode 100644 index 72bb77e0..00000000 --- a/internal/resource/sts.go +++ /dev/null @@ -1,129 +0,0 @@ -package resource - -import ( - "context" - "fmt" - "strconv" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - appsv1 "k8s.io/api/apps/v1" -) - -// Compile time checks to ensure type satisfies interface -var _ Restartable = (*StatefulSet)(nil) -var _ Scalable = (*StatefulSet)(nil) - -// StatefulSet tracks a kubernetes resource. -type StatefulSet struct { - *Base - instance *appsv1.StatefulSet -} - -// NewStatefulSetList returns a new resource list. -func NewStatefulSetList(c Connection, ns string) List { - return NewList( - ns, - "sts", - NewStatefulSet(c), - AllVerbsAccess|DescribeAccess, - ) -} - -// NewStatefulSet instantiates a new StatefulSet. -func NewStatefulSet(c Connection) *StatefulSet { - s := &StatefulSet{&Base{Connection: c, Resource: k8s.NewStatefulSet(c)}, nil} - s.Factory = s - - return s -} - -// New builds a new StatefulSet instance from a k8s resource. -func (r *StatefulSet) New(i interface{}) Columnar { - c := NewStatefulSet(r.Connection) - switch instance := i.(type) { - case *appsv1.StatefulSet: - c.instance = instance - case appsv1.StatefulSet: - c.instance = &instance - default: - log.Fatal().Msgf("unknown StatefulSet type %#v", i) - } - c.path = c.namespacedName(c.instance.ObjectMeta) - - return c -} - -// Marshal resource to yaml. -func (r *StatefulSet) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - - sts := i.(*appsv1.StatefulSet) - sts.TypeMeta.APIVersion = "apps/v1" - sts.TypeMeta.Kind = "StatefulSet" - - return r.marshalObject(sts) -} - -// Logs tail logs for all pods represented by this statefulset. -func (r *StatefulSet) Logs(ctx context.Context, c chan<- string, opts LogOptions) error { - instance, err := r.Resource.Get(opts.Namespace, opts.Name) - if err != nil { - return err - } - - sts := instance.(*appsv1.StatefulSet) - if sts.Spec.Selector == nil || len(sts.Spec.Selector.MatchLabels) == 0 { - return fmt.Errorf("No valid selector found on statefulset %s", opts.FQN()) - } - - return r.podLogs(ctx, c, sts.Spec.Selector.MatchLabels, opts) -} - -// Header return resource header. -func (*StatefulSet) Header(ns string) Row { - hh := Row{} - if ns == AllNamespaces { - hh = append(hh, "NAMESPACE") - } - - return append(hh, "NAME", "DESIRED", "CURRENT", "AGE") -} - -// NumCols designates if column is numerical. -func (*StatefulSet) NumCols(n string) map[string]bool { - return map[string]bool{ - "DESIRED": true, - "CURRENT": true, - } -} - -// Fields retrieves displayable fields. -func (r *StatefulSet) Fields(ns string) Row { - ff := make(Row, 0, len(r.Header(ns))) - i := r.instance - if ns == AllNamespaces { - ff = append(ff, i.Namespace) - } - - return append(ff, - i.Name, - strconv.Itoa(int(*i.Spec.Replicas)), - strconv.Itoa(int(i.Status.ReadyReplicas)), - toAge(i.ObjectMeta.CreationTimestamp), - ) -} - -// Scale the specified resource. -func (r *StatefulSet) Scale(ns, n string, replicas int32) error { - return r.Resource.(Scalable).Scale(ns, n, replicas) -} - -// Restart the rollout of the specified resource. -func (r *StatefulSet) Restart(ns, n string) error { - return r.Resource.(Restartable).Restart(ns, n) -} diff --git a/internal/resource/sts_test.go b/internal/resource/sts_test.go deleted file mode 100644 index 199485f6..00000000 --- a/internal/resource/sts_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package resource_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - m "github.com/petergtz/pegomock" - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func NewStatefulSetListWithArgs(ns string, r *resource.StatefulSet) resource.List { - return resource.NewList(ns, "sts", r, resource.AllVerbsAccess|resource.DescribeAccess) -} - -func NewStatefulSetWithArgs(conn k8s.Connection, res resource.Cruder) *resource.StatefulSet { - r := &resource.StatefulSet{Base: resource.NewBase(conn, res)} - r.Factory = r - return r -} - -func TestStsListAccess(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - - ns := "blee" - l := NewStatefulSetListWithArgs(resource.AllNamespaces, NewStatefulSetWithArgs(mc, mr)) - l.SetNamespace(ns) - - assert.Equal(t, l.GetNamespace(), ns) - assert.Equal(t, "sts", l.GetName()) - for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} { - assert.True(t, l.Access(a)) - } -} - -func TestStsHeader(t *testing.T) { - s := newSts() - e := append(resource.Row{"NAMESPACE"}, stsHeader()...) - assert.Equal(t, e, s.Header(resource.AllNamespaces)) - assert.Equal(t, stsHeader(), s.Header("fred")) -} - -func TestStsFields(t *testing.T) { - uu := []struct { - i resource.Columnar - e resource.Row - }{ - {i: newSts(), e: resource.Row{"blee", "fred", "0", "1"}}, - } - - for _, u := range uu { - assert.Equal(t, "blee/fred", u.i.Name()) - assert.Equal(t, u.e, u.i.Fields(resource.AllNamespaces)[:4]) - assert.Equal(t, u.e[1:4], u.i.Fields("blee")[:3]) - } -} - -func TestSTSMarshal(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("blee", "fred")).ThenReturn(k8sSTS(), nil) - - cm := NewStatefulSetWithArgs(mc, mr) - ma, err := cm.Marshal("blee/fred") - - mr.VerifyWasCalledOnce().Get("blee", "fred") - assert.Nil(t, err) - assert.Equal(t, stsYaml(), ma) -} - -func TestSTSListData(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.List("blee", metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sSTS()}, nil) - - l := NewStatefulSetListWithArgs("blee", NewStatefulSetWithArgs(mc, mr)) - // Make sure we mrn get deltas! - for i := 0; i < 2; i++ { - err := l.Reconcile(nil, nil) - assert.Nil(t, err) - } - - mr.VerifyWasCalled(m.Times(2)).List("blee", metav1.ListOptions{}) - td := l.Data() - assert.Equal(t, 1, len(td.Rows)) - assert.Equal(t, "blee", l.GetNamespace()) - row := td.Rows["blee/fred"] - assert.Equal(t, 4, len(row.Deltas)) - for _, d := range row.Deltas { - assert.Equal(t, "", d) - } - assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) -} - -// Helpers... - -func k8sSTS() *v1.StatefulSet { - return &v1.StatefulSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "fred", - Namespace: "blee", - CreationTimestamp: metav1.Time{testTime()}, - }, - Spec: v1.StatefulSetSpec{ - Replicas: new(int32), - }, - Status: v1.StatefulSetStatus{ - ReadyReplicas: 1, - }, - } -} - -func newSts() resource.Columnar { - mc := NewMockConnection() - return resource.NewStatefulSet(mc).New(k8sSTS()) -} - -func stsHeader() resource.Row { - return resource.Row{"NAME", "DESIRED", "CURRENT", "AGE"} -} - -func stsYaml() string { - return `apiVersion: apps/v1 -kind: StatefulSet -metadata: - creationTimestamp: "2018-12-14T17:36:43Z" - name: fred - namespace: blee -spec: - replicas: 0 - selector: null - serviceName: "" - template: - metadata: - creationTimestamp: null - spec: - containers: null - updateStrategy: {} -status: - readyReplicas: 1 - replicas: 0 -` -} diff --git a/internal/resource/svc.go b/internal/resource/svc.go deleted file mode 100644 index b0a8f81a..00000000 --- a/internal/resource/svc.go +++ /dev/null @@ -1,197 +0,0 @@ -package resource - -import ( - "context" - "errors" - "sort" - "strconv" - "strings" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - v1 "k8s.io/api/core/v1" -) - -const lbIPWidth = 16 - -// Service tracks a kubernetes resource. -type Service struct { - *Base - instance *v1.Service -} - -// NewServiceList returns a new resource list. -func NewServiceList(c Connection, ns string) List { - return NewList( - ns, - "svc", - NewService(c), - AllVerbsAccess|DescribeAccess, - ) -} - -// NewService instantiates a new Service. -func NewService(c Connection) *Service { - s := &Service{&Base{Connection: c, Resource: k8s.NewService(c)}, nil} - s.Factory = s - - return s -} - -// New builds a new Service instance from a k8s resource. -func (r *Service) New(i interface{}) Columnar { - c := NewService(r.Connection) - switch instance := i.(type) { - case *v1.Service: - c.instance = instance - case v1.Service: - c.instance = &instance - default: - log.Fatal().Msgf("unknown Service type %#v", i) - } - c.path = c.namespacedName(c.instance.ObjectMeta) - - return c -} - -// Marshal resource to yaml. -// BOZO!! Why you need to fill type info?? -func (r *Service) Marshal(path string) (string, error) { - ns, n := Namespaced(path) - i, err := r.Resource.Get(ns, n) - if err != nil { - return "", err - } - - svc := i.(*v1.Service) - svc.TypeMeta.APIVersion = "v1" - svc.TypeMeta.Kind = "Service" - - return r.marshalObject(svc) -} - -// Logs tail logs for all pods represented by this service. -func (r *Service) Logs(ctx context.Context, c chan<- string, opts LogOptions) error { - instance, err := r.Resource.Get(opts.Namespace, opts.Name) - if err != nil { - return err - } - - svc := instance.(*v1.Service) - log.Debug().Msgf("Service %s--%s", svc.Name, svc.Spec.Selector) - if len(svc.Spec.Selector) == 0 { - return errors.New("No logs for headless service") - } - - return r.podLogs(ctx, c, svc.Spec.Selector, opts) -} - -// Header returns resource header. -func (*Service) Header(ns string) Row { - hh := Row{} - if ns == AllNamespaces { - hh = append(hh, "NAMESPACE") - } - - return append(hh, - "NAME", - "TYPE", - "CLUSTER-IP", - "EXTERNAL-IP", - "SELECTOR", - "PORTS", - "AGE", - ) -} - -// Fields retrieves displayable fields. -func (r *Service) Fields(ns string) Row { - ff := make(Row, 0, len(r.Header(ns))) - i := r.instance - - if ns == AllNamespaces { - ff = append(ff, i.Namespace) - } - - return append(ff, - i.ObjectMeta.Name, - string(i.Spec.Type), - i.Spec.ClusterIP, - r.toIPs(i.Spec.Type, r.getSvcExtIPS(i)), - mapToStr(i.Spec.Selector), - r.toPorts(i.Spec.Ports), - toAge(i.ObjectMeta.CreationTimestamp), - ) -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func (r *Service) getSvcExtIPS(svc *v1.Service) []string { - results := []string{} - - switch svc.Spec.Type { - case v1.ServiceTypeClusterIP: - fallthrough - case v1.ServiceTypeNodePort: - return svc.Spec.ExternalIPs - case v1.ServiceTypeLoadBalancer: - lbIps := r.lbIngressIP(svc.Status.LoadBalancer) - if len(svc.Spec.ExternalIPs) > 0 { - if len(lbIps) > 0 { - results = append(results, lbIps) - } - return append(results, svc.Spec.ExternalIPs...) - } - if len(lbIps) > 0 { - results = append(results, lbIps) - } - case v1.ServiceTypeExternalName: - results = append(results, svc.Spec.ExternalName) - } - - return results -} - -func (*Service) lbIngressIP(s v1.LoadBalancerStatus) string { - ingress := s.Ingress - result := []string{} - for i := range ingress { - if len(ingress[i].IP) > 0 { - result = append(result, ingress[i].IP) - } else if len(ingress[i].Hostname) > 0 { - result = append(result, ingress[i].Hostname) - } - } - - return strings.Join(result, ",") -} - -func (*Service) toIPs(svcType v1.ServiceType, ips []string) string { - if len(ips) == 0 { - if svcType == v1.ServiceTypeLoadBalancer { - return "" - } - return MissingValue - } - sort.Strings(ips) - - return strings.Join(ips, ",") -} - -func (*Service) toPorts(pp []v1.ServicePort) string { - ports := make([]string, len(pp)) - for i, p := range pp { - if len(p.Name) > 0 { - ports[i] = p.Name + ":" - } - ports[i] += strconv.Itoa(int(p.Port)) + - "►" + - strconv.Itoa(int(p.NodePort)) - if p.Protocol != "TCP" { - ports[i] += "╱" + string(p.Protocol) - } - } - - return strings.Join(ports, " ") -} diff --git a/internal/resource/svc_int_test.go b/internal/resource/svc_int_test.go deleted file mode 100644 index 5bc1fdfe..00000000 --- a/internal/resource/svc_int_test.go +++ /dev/null @@ -1,123 +0,0 @@ -package resource - -import ( - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestSvcExtIPs(t *testing.T) { - i := k8sSVCLb() - - var s Service - ips := s.getSvcExtIPS(i) - - assert.Equal(t, "10.0.0.0,2.2.2.2", s.toIPs(i.Spec.Type, ips)) -} - -func TestLbIngressIP(t *testing.T) { - lb := v1.LoadBalancerStatus{ - Ingress: []v1.LoadBalancerIngress{ - {"10.0.0.0", "fred"}, - {"10.0.0.1", "blee"}, - }, - } - - var s Service - assert.Equal(t, "10.0.0.0,10.0.0.1", s.lbIngressIP(lb)) -} - -func TestToIPs(t *testing.T) { - uu := []struct { - t v1.ServiceType - ii []string - e string - }{ - {v1.ServiceTypeLoadBalancer, []string{"2.2.2.2", "1.1.1.1"}, "1.1.1.1,2.2.2.2"}, - {v1.ServiceTypeLoadBalancer, []string{}, ""}, - {v1.ServiceTypeClusterIP, []string{}, MissingValue}, - } - - var s Service - for _, u := range uu { - assert.Equal(t, u.e, s.toIPs(u.t, u.ii)) - } -} - -func TestToPorts(t *testing.T) { - uu := []struct { - pp []v1.ServicePort - e string - }{ - {[]v1.ServicePort{ - {Name: "http", Port: 80, NodePort: 90, Protocol: "TCP"}}, - "http:80►90", - }, - {[]v1.ServicePort{ - {Port: 80, NodePort: 30080, Protocol: "UDP"}}, - "80►30080╱UDP", - }, - } - - var s Service - for _, u := range uu { - assert.Equal(t, u.e, s.toPorts(u.pp)) - } -} - -func BenchmarkToPorts(b *testing.B) { - sp := []v1.ServicePort{ - {Name: "http", Port: 80, NodePort: 90, Protocol: "TCP"}, - {Port: 80, NodePort: 90, Protocol: "TCP"}, - {Name: "http", Port: 80, NodePort: 90, Protocol: "TCP"}, - } - b.ResetTimer() - b.ReportAllocs() - - var s Service - for i := 0; i < b.N; i++ { - s.toPorts(sp) - } -} - -func k8sSVCLb() *v1.Service { - return &v1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: "fred", - Namespace: "blee", - CreationTimestamp: metav1.Time{testTime()}, - }, - Spec: v1.ServiceSpec{ - Type: v1.ServiceTypeLoadBalancer, - ClusterIP: "1.1.1.1", - ExternalIPs: []string{"2.2.2.2"}, - Selector: map[string]string{"fred": "blee"}, - Ports: []v1.ServicePort{ - { - Name: "http", - Port: 90, - Protocol: "TCP", - }, - }, - }, - Status: v1.ServiceStatus{ - LoadBalancer: v1.LoadBalancerStatus{ - Ingress: []v1.LoadBalancerIngress{ - {IP: "10.0.0.0", Hostname: "fred"}, - }, - }, - }, - } -} - -func testTime() time.Time { - t, err := time.Parse(time.RFC3339, "2018-12-14T10:36:43.326972-07:00") - if err != nil { - fmt.Println("TestTime Failed", err) - } - return t -} diff --git a/internal/resource/svc_test.go b/internal/resource/svc_test.go deleted file mode 100644 index bea77475..00000000 --- a/internal/resource/svc_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package resource_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - 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 NewServiceListWithArgs(ns string, r *resource.Service) resource.List { - return resource.NewList(ns, "svc", r, resource.AllVerbsAccess|resource.DescribeAccess) -} - -func NewServiceWithArgs(conn k8s.Connection, res resource.Cruder) *resource.Service { - r := &resource.Service{Base: resource.NewBase(conn, res)} - r.Factory = r - return r -} - -func TestSvcListAccess(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - - ns := "blee" - l := NewServiceListWithArgs(resource.AllNamespaces, NewServiceWithArgs(mc, mr)) - l.SetNamespace(ns) - - assert.Equal(t, l.GetNamespace(), ns) - assert.Equal(t, "svc", l.GetName()) - for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} { - assert.True(t, l.Access(a)) - } -} - -func TestSvcHeader(t *testing.T) { - s := newSvc() - e := append(resource.Row{"NAMESPACE"}, svcHeader()...) - - assert.Equal(t, e, s.Header(resource.AllNamespaces)) - assert.Equal(t, svcHeader(), s.Header("fred")) -} - -func TestSvcFields(t *testing.T) { - uu := []struct { - i resource.Columnar - e resource.Row - }{ - { - i: newSvc(), - e: resource.Row{ - "blee", - "fred", - "ClusterIP", - "1.1.1.1", - "2.2.2.2", - "fred=blee", - "http:90►0", - }, - }, - } - - for _, u := range uu { - assert.Equal(t, "blee/fred", u.i.Name()) - assert.Equal(t, u.e[1:6], u.i.Fields("blee")[:5]) - assert.Equal(t, u.e[:6], u.i.Fields(resource.AllNamespaces)[:6]) - } -} - -func TestSVCMarshal(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.Get("blee", "fred")).ThenReturn(k8sSVC(), nil) - - cm := NewServiceWithArgs(mc, mr) - ma, err := cm.Marshal("blee/fred") - mr.VerifyWasCalledOnce().Get("blee", "fred") - assert.Nil(t, err) - assert.Equal(t, svcYaml(), ma) -} - -func TestSVCListData(t *testing.T) { - mc := NewMockConnection() - mr := NewMockCruder() - m.When(mr.List("blee", metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sSVC()}, nil) - - l := NewServiceListWithArgs("blee", NewServiceWithArgs(mc, mr)) - // Make sure we mrn get deltas! - for i := 0; i < 2; i++ { - err := l.Reconcile(nil, nil) - assert.Nil(t, err) - } - - mr.VerifyWasCalled(m.Times(2)).List("blee", metav1.ListOptions{}) - td := l.Data() - assert.Equal(t, 1, len(td.Rows)) - assert.Equal(t, "blee", l.GetNamespace()) - row := td.Rows["blee/fred"] - assert.Equal(t, 7, len(row.Deltas)) - for _, d := range row.Deltas { - assert.Equal(t, "", d) - } - assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) -} - -// Helpers... - -func k8sSVC() *v1.Service { - return &v1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: "fred", - Namespace: "blee", - CreationTimestamp: metav1.Time{testTime()}, - }, - Spec: v1.ServiceSpec{ - Type: v1.ServiceTypeClusterIP, - ClusterIP: "1.1.1.1", - ExternalIPs: []string{"2.2.2.2"}, - Selector: map[string]string{"fred": "blee"}, - Ports: []v1.ServicePort{ - { - Name: "http", - Port: 90, - Protocol: "TCP", - }, - }, - }, - } -} - -func newSvc() resource.Columnar { - mc := NewMockConnection() - return resource.NewService(mc).New(k8sSVC()) -} - -func svcHeader() resource.Row { - return resource.Row{ - "NAME", - "TYPE", - "CLUSTER-IP", - "EXTERNAL-IP", - "SELECTOR", - "PORTS", - "AGE", - } -} - -func svcYaml() string { - return `apiVersion: v1 -kind: Service -metadata: - creationTimestamp: "2018-12-14T17:36:43Z" - name: fred - namespace: blee -spec: - clusterIP: 1.1.1.1 - externalIPs: - - 2.2.2.2 - ports: - - name: http - port: 90 - protocol: TCP - targetPort: 0 - selector: - fred: blee - type: ClusterIP -status: - loadBalancer: {} -` -} diff --git a/internal/ui/action.go b/internal/ui/action.go index ef865e7a..f9f561a9 100644 --- a/internal/ui/action.go +++ b/internal/ui/action.go @@ -3,6 +3,7 @@ package ui import ( "sort" + "github.com/derailed/k9s/internal/model" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" ) @@ -16,6 +17,7 @@ type ( Description string Action ActionHandler Visible bool + Shared bool } // KeyActions tracks mappings between keystrokes and actions. @@ -27,19 +29,53 @@ func NewKeyAction(d string, a ActionHandler, display bool) KeyAction { return KeyAction{Description: d, Action: a, Visible: display} } +func NewSharedKeyAction(d string, a ActionHandler, display bool) KeyAction { + return KeyAction{Description: d, Action: a, Visible: display, Shared: true} +} + +// Add sets up keyboard action listener. +func (a KeyActions) Add(aa KeyActions) { + for k, v := range aa { + a[k] = v + } +} + +// Clear +func (a KeyActions) Clear() { + for k := range a { + delete(a, k) + } +} + +// SetActions replace actions with new ones. +func (a KeyActions) Set(aa KeyActions) { + for k, v := range aa { + a[k] = v + } +} + +// Delete deletes actions by the given keys. +func (a KeyActions) Delete(kk ...tcell.Key) { + for _, k := range kk { + delete(a, k) + } +} + // Hints returns a collection of hints. -func (a KeyActions) Hints() Hints { +func (a KeyActions) Hints() model.MenuHints { kk := make([]int, 0, len(a)) for k := range a { - kk = append(kk, int(k)) + if !a[k].Shared { + kk = append(kk, int(k)) + } } sort.Ints(kk) - hh := make(Hints, 0, len(kk)) + hh := make(model.MenuHints, 0, len(kk)) for _, k := range kk { if name, ok := tcell.KeyNames[tcell.Key(k)]; ok { hh = append(hh, - Hint{ + model.MenuHint{ Mnemonic: name, Description: a[tcell.Key(k)].Description, Visible: a[tcell.Key(k)].Visible, diff --git a/internal/ui/action_test.go b/internal/ui/action_test.go new file mode 100644 index 00000000..34a7d669 --- /dev/null +++ b/internal/ui/action_test.go @@ -0,0 +1,22 @@ +package ui_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/ui" + "github.com/stretchr/testify/assert" +) + +func TestKeyActionsHints(t *testing.T) { + kk := ui.KeyActions{ + ui.KeyF: ui.NewKeyAction("fred", nil, true), + ui.KeyB: ui.NewKeyAction("blee", nil, true), + ui.KeyZ: ui.NewKeyAction("zorg", nil, false), + } + + hh := kk.Hints() + + assert.Equal(t, 3, len(hh)) + assert.Equal(t, model.MenuHint{Mnemonic: "b", Description: "blee", Visible: true}, hh[0]) +} diff --git a/internal/ui/app.go b/internal/ui/app.go index fd19f13f..97dd67ee 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -1,87 +1,41 @@ package ui import ( - "context" - - "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/k8s" + "github.com/derailed/k9s/internal/client" "github.com/derailed/tview" "github.com/gdamore/tcell" ) -// Igniter represents an initializable view. -type Igniter interface { - tview.Primitive +// App represents an application. +type App struct { + *tview.Application + Configurator - // Init initializes the view. - Init(ctx context.Context, ns string) + Main *Pages + actions KeyActions + views map[string]tview.Primitive + cmdBuff *CmdBuff } -type ( - keyHandler interface { - keyboard(evt *tcell.EventKey) *tcell.EventKey - } - - // ActionsFunc augments Keybindings. - ActionsFunc func(KeyActions) - - // Configurator represents an application configurations. - Configurator struct { - HasSkins bool - Config *config.Config - Styles *config.Styles - Bench *config.Bench - } - - // App represents an application. - App struct { - *tview.Application - Configurator - - actions KeyActions - pages *tview.Pages - content *tview.Pages - views map[string]tview.Primitive - cmdBuff *CmdBuff - hints Hints - } -) - // NewApp returns a new app. -func NewApp() *App { - s := App{ +func NewApp(cluster string) *App { + a := App{ Application: tview.NewApplication(), actions: make(KeyActions), - pages: tview.NewPages(), - content: tview.NewPages(), + Main: NewPages(), cmdBuff: NewCmdBuff(':', CommandBuff), } + a.ReloadStyles(cluster) - s.RefreshStyles() - - s.views = map[string]tview.Primitive{ - "menu": NewMenuView(s.Styles), - "logo": NewLogoView(s.Styles), - "cmd": NewCmdView(s.Styles), - "crumbs": NewCrumbsView(s.Styles), + a.views = map[string]tview.Primitive{ + "menu": NewMenu(a.Styles), + "logo": NewLogo(a.Styles), + "cmd": NewCommand(a.Styles), + "flash": NewFlash(&a, "Initializing..."), + "crumbs": NewCrumbs(a.Styles), } - return &s -} - -// Main returns main app frame. -func (a *App) Main() *tview.Pages { - return a.pages -} - -// Frame returns main app content frame. -func (a *App) Frame() *tview.Pages { - return a.content -} - -// Conn returns an api server connection. -func (a *App) Conn() k8s.Connection { - return a.Config.GetConnection() + return &a } // Init initializes the application. @@ -89,7 +43,16 @@ func (a *App) Init() { a.bindKeys() a.SetInputCapture(a.keyboard) a.cmdBuff.AddListener(a.Cmd()) - a.SetRoot(a.pages, true) + a.SetRoot(a.Main, true) +} + +func (a *App) ReloadStyles(cluster string) { + a.RefreshStyles(cluster) +} + +// Conn returns an api server connection. +func (a *App) Conn() client.Connection { + return a.Config.GetConnection() } func (a *App) bindKeys() { @@ -148,19 +111,19 @@ func (a *App) InCmdMode() bool { return a.Cmd().InCmdMode() } -// GetActions returns a collection of actions. +// GetActions returns a collection of actiona. func (a *App) GetActions() KeyActions { return a.actions } -// AddActions returns the application actions. +// AddActions returns the application actiona. func (a *App) AddActions(aa KeyActions) { for k, v := range aa { a.actions[k] = v } } -// Views return the application root views. +// Views return the application root viewa. func (a *App) Views() map[string]tview.Primitive { return a.views } @@ -215,53 +178,37 @@ func (a *App) redrawCmd(evt *tcell.EventKey) *tcell.EventKey { return evt } -// ActiveView returns the currently active view. -func (a *App) ActiveView() Igniter { - return a.content.GetPrimitive("main").(Igniter) -} - -// SetHints updates menu hints. -func (a *App) SetHints(h Hints) { - a.hints = h - a.views["menu"].(*MenuView).HydrateMenu(h) -} - -// GetHints retrieves the currently active hints. -func (a *App) GetHints() Hints { - return a.hints -} - // StatusReset reset log back to normal. func (a *App) StatusReset() { a.Logo().Reset() a.Draw() } -// View Accessors... +// View Accessora... -// Crumbs return app crumbs. -func (a *App) Crumbs() *CrumbsView { - return a.views["crumbs"].(*CrumbsView) +// Crumbs return app crumba. +func (a *App) Crumbs() *Crumbs { + return a.views["crumbs"].(*Crumbs) } // Logo return the app logo. -func (a *App) Logo() *LogoView { - return a.views["logo"].(*LogoView) +func (a *App) Logo() *Logo { + return a.views["logo"].(*Logo) } // Flash returns app flash. -func (a *App) Flash() *FlashView { - return a.views["flash"].(*FlashView) +func (a *App) Flash() *Flash { + return a.views["flash"].(*Flash) } // Cmd returns app cmd. -func (a *App) Cmd() *CmdView { - return a.views["cmd"].(*CmdView) +func (a *App) Cmd() *Command { + return a.views["cmd"].(*Command) } // Menu returns app menu. -func (a *App) Menu() *MenuView { - return a.views["menu"].(*MenuView) +func (a *App) Menu() *Menu { + return a.views["menu"].(*Menu) } // AsKey converts rune to keyboard key., diff --git a/internal/ui/app_test.go b/internal/ui/app_test.go new file mode 100644 index 00000000..db0047f8 --- /dev/null +++ b/internal/ui/app_test.go @@ -0,0 +1,75 @@ +package ui_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/ui" + "github.com/stretchr/testify/assert" +) + +func TestAppGetCmd(t *testing.T) { + a := ui.NewApp("") + a.Init() + a.CmdBuff().Set("blee") + + assert.Equal(t, "blee", a.GetCmd()) +} + +func TestAppInCmdMode(t *testing.T) { + a := ui.NewApp("") + a.Init() + a.CmdBuff().Set("blee") + assert.False(t, a.InCmdMode()) + + a.CmdBuff().SetActive(true) + assert.True(t, a.InCmdMode()) +} + +func TestAppResetCmd(t *testing.T) { + a := ui.NewApp("") + a.Init() + a.CmdBuff().Set("blee") + + a.ResetCmd() + + assert.Equal(t, "", a.CmdBuff().String()) +} + +func TestAppHasCmd(t *testing.T) { + a := ui.NewApp("") + a.Init() + + a.ActivateCmd(true) + assert.False(t, a.HasCmd()) + + a.CmdBuff().Set("blee") + assert.True(t, a.InCmdMode()) +} + +func TestAppGetActions(t *testing.T) { + a := ui.NewApp("") + a.Init() + + a.AddActions(ui.KeyActions{ui.KeyZ: ui.KeyAction{Description: "zorg"}}) + + assert.Equal(t, 8, len(a.GetActions())) +} + +func TestAppViews(t *testing.T) { + a := ui.NewApp("") + a.Init() + + vv := []string{"crumbs", "logo", "cmd", "flash", "menu"} + for i := range vv { + v := vv[i] + t.Run(v, func(t *testing.T) { + assert.NotNil(t, a.Views()[v]) + }) + } + + assert.NotNil(t, a.Crumbs()) + assert.NotNil(t, a.Flash()) + assert.NotNil(t, a.Logo()) + assert.NotNil(t, a.Cmd()) + assert.NotNil(t, a.Menu()) +} diff --git a/internal/ui/cmd.go b/internal/ui/cmd.go deleted file mode 100644 index 477731f3..00000000 --- a/internal/ui/cmd.go +++ /dev/null @@ -1,101 +0,0 @@ -package ui - -import ( - "fmt" - - "github.com/derailed/k9s/internal/config" - "github.com/derailed/tview" - "github.com/gdamore/tcell" -) - -const defaultPrompt = "%c> %s" - -// CmdView captures users free from command input. -type CmdView struct { - *tview.TextView - - activated bool - icon rune - text string - styles *config.Styles -} - -// NewCmdView returns a new command view. -func NewCmdView(styles *config.Styles) *CmdView { - v := CmdView{styles: styles, TextView: tview.NewTextView()} - { - v.SetWordWrap(true) - v.SetWrap(true) - v.SetDynamicColors(true) - v.SetBorder(true) - v.SetBorderPadding(0, 0, 1, 1) - v.SetBackgroundColor(styles.BgColor()) - // v.SetBorderColor(config.AsColor(styles.Frame().Border.FocusColor)) - v.SetTextColor(styles.FgColor()) - } - return &v -} - -// InCmdMode returns true if command is active, false otherwise. -func (v *CmdView) InCmdMode() bool { - return v.activated -} - -func (v *CmdView) activate() { - v.write(v.text) -} - -func (v *CmdView) update(s string) { - v.text = s - v.Clear() - v.write(s) -} - -func (v *CmdView) append(r rune) { - fmt.Fprintf(v, string(r)) -} - -func (v *CmdView) write(s string) { - fmt.Fprintf(v, defaultPrompt, v.icon, s) -} - -// ---------------------------------------------------------------------------- -// Event Listener protocol... - -// BufferChanged indicates the buffer was changed. -func (v *CmdView) BufferChanged(s string) { - v.update(s) -} - -// BufferActive indicates the buff activity changed. -func (v *CmdView) BufferActive(f bool, k BufferKind) { - v.activated = f - if f { - v.SetBorder(true) - v.icon = iconFor(k) - v.SetTextColor(v.styles.FgColor()) - v.SetBorderColor(colorFor(k)) - v.activate() - } else { - v.SetBorder(false) - v.SetBackgroundColor(v.styles.BgColor()) - v.Clear() - } -} - -func colorFor(k BufferKind) tcell.Color { - switch k { - case CommandBuff: - return tcell.ColorAqua - default: - return tcell.ColorSeaGreen - } -} -func iconFor(k BufferKind) rune { - switch k { - case CommandBuff: - return '🐶' - default: - return '🐩' - } -} diff --git a/internal/ui/cmd_buff.go b/internal/ui/cmd_buff.go index 6860faf6..bbb57804 100644 --- a/internal/ui/cmd_buff.go +++ b/internal/ui/cmd_buff.go @@ -25,11 +25,11 @@ type ( // CmdBuff represents user command input. CmdBuff struct { buff []rune + listeners []BuffWatcher + hotKey rune kind BufferKind sticky bool - hotKey rune active bool - listeners []BuffWatcher } ) @@ -53,6 +53,11 @@ func (c *CmdBuff) SetSticky(b bool) { c.sticky = b } +// InCmdMode checks if a command exists and the buffer is active. +func (c *CmdBuff) InCmdMode() bool { + return c.active || len(c.buff) > 0 +} + // IsActive checks if command buffer is active. func (c *CmdBuff) IsActive() bool { return c.active @@ -90,17 +95,13 @@ func (c *CmdBuff) Delete() { c.fireChanged() } -func (c *CmdBuff) wipe() { - c.buff = make([]rune, 0, maxBuff) -} - // Clear clears out command buffer. func (c *CmdBuff) Clear() { c.buff = make([]rune, 0, maxBuff) c.fireChanged() } -// Reset clears out the command buffer. +// Reset clears out the command buffer and deactives it. func (c *CmdBuff) Reset() { c.Clear() c.fireChanged() @@ -120,6 +121,21 @@ func (c *CmdBuff) AddListener(w ...BuffWatcher) { c.listeners = append(c.listeners, w...) } +func (c *CmdBuff) RemoveListener(l BuffWatcher) { + victim := -1 + for i, lis := range c.listeners { + if l == lis { + victim = i + break + } + } + + if victim == -1 { + return + } + c.listeners = append(c.listeners[:victim], c.listeners[victim+1:]...) +} + func (c *CmdBuff) fireChanged() { for _, l := range c.listeners { l.BufferChanged(c.String()) diff --git a/internal/ui/cmd_buff_test.go b/internal/ui/cmd_buff_test.go index e370c4a1..182f6109 100644 --- a/internal/ui/cmd_buff_test.go +++ b/internal/ui/cmd_buff_test.go @@ -1,8 +1,9 @@ -package ui +package ui_test import ( "testing" + "github.com/derailed/k9s/internal/ui" "github.com/stretchr/testify/assert" ) @@ -16,7 +17,7 @@ func (l *testListener) BufferChanged(s string) { l.text = s } -func (l *testListener) BufferActive(s bool, _ BufferKind) { +func (l *testListener) BufferActive(s bool, _ ui.BufferKind) { if s { l.act++ return @@ -25,27 +26,27 @@ func (l *testListener) BufferActive(s bool, _ BufferKind) { } func TestCmdBuffActivate(t *testing.T) { - b, l := NewCmdBuff('>', CommandBuff), testListener{} + b, l := ui.NewCmdBuff('>', ui.CommandBuff), testListener{} b.AddListener(&l) b.SetActive(true) assert.Equal(t, 1, l.act) assert.Equal(t, 0, l.inact) - assert.True(t, b.active) + assert.True(t, b.IsActive()) } func TestCmdBuffDeactivate(t *testing.T) { - b, l := NewCmdBuff('>', CommandBuff), testListener{} + b, l := ui.NewCmdBuff('>', ui.CommandBuff), testListener{} b.AddListener(&l) b.SetActive(false) assert.Equal(t, 0, l.act) assert.Equal(t, 1, l.inact) - assert.False(t, b.active) + assert.False(t, b.IsActive()) } func TestCmdBuffChanged(t *testing.T) { - b, l := NewCmdBuff('>', CommandBuff), testListener{} + b, l := ui.NewCmdBuff('>', ui.CommandBuff), testListener{} b.AddListener(&l) b.Add('b') @@ -77,7 +78,7 @@ func TestCmdBuffChanged(t *testing.T) { } func TestCmdBuffAdd(t *testing.T) { - b := NewCmdBuff('>', CommandBuff) + b := ui.NewCmdBuff('>', ui.CommandBuff) uu := []struct { runes []rune @@ -98,7 +99,7 @@ func TestCmdBuffAdd(t *testing.T) { } func TestCmdBuffDel(t *testing.T) { - b := NewCmdBuff('>', CommandBuff) + b := ui.NewCmdBuff('>', ui.CommandBuff) uu := []struct { runes []rune @@ -120,7 +121,7 @@ func TestCmdBuffDel(t *testing.T) { } func TestCmdBuffEmpty(t *testing.T) { - b := NewCmdBuff('>', CommandBuff) + b := ui.NewCmdBuff('>', ui.CommandBuff) uu := []struct { runes []rune diff --git a/internal/ui/cmd_stack.go b/internal/ui/cmd_stack.go deleted file mode 100644 index 94407bd9..00000000 --- a/internal/ui/cmd_stack.go +++ /dev/null @@ -1,58 +0,0 @@ -package ui - -const maxStackSize = 10 - -// CmdStack tracks users command breadcrumbs. -type CmdStack struct { - index int - stack []string -} - -// NewCmdStack returns a new cmd stack. -func NewCmdStack() *CmdStack { - return &CmdStack{stack: make([]string, 0, maxStackSize)} -} - -// Items returns current stack content. -func (s *CmdStack) Items() []string { - return s.stack -} - -// Push adds a new item, -func (s *CmdStack) Push(cmd string) { - if len(s.stack) == maxStackSize { - s.stack = s.stack[1 : len(s.stack)-1] - } - s.stack = append(s.stack, cmd) -} - -// Pop delete an item. -func (s *CmdStack) Pop() (string, bool) { - if s.Empty() { - return "", false - } - - top := s.stack[len(s.stack)-1] - s.stack = s.stack[:len(s.stack)-1] - - return top, true -} - -// Top return top element. -func (s *CmdStack) Top() (string, bool) { - if s.Empty() { - return "", false - } - - return s.stack[len(s.stack)-1], true -} - -// Empty check if stack is empty. -func (s *CmdStack) Empty() bool { - return len(s.stack) == 0 -} - -// Last returns the last command. -func (s *CmdStack) Last() bool { - return len(s.stack) == 1 -} diff --git a/internal/ui/cmd_stack_test.go b/internal/ui/cmd_stack_test.go deleted file mode 100644 index 11492497..00000000 --- a/internal/ui/cmd_stack_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package ui - -import ( - "fmt" - "testing" - - "github.com/rs/zerolog" - "github.com/stretchr/testify/assert" -) - -func init() { - zerolog.SetGlobalLevel(zerolog.FatalLevel) -} - -func TestCmdStackPushMax(t *testing.T) { - s := NewCmdStack() - for i := 0; i < 20; i++ { - s.Push(fmt.Sprintf("cmd_%d", i)) - } - top, ok := s.Top() - assert.True(t, ok) - assert.Equal(t, "cmd_19", top) -} - -func TestCmdStackPop(t *testing.T) { - type expect struct { - val string - ok bool - } - - uu := []struct { - cmds []string - popCount int - e expect - }{ - {[]string{}, 2, expect{"", false}}, - {[]string{"a", "b", "c"}, 2, expect{"a", true}}, - {[]string{"a", "b", "c"}, 1, expect{"b", true}}, - } - - for _, u := range uu { - s := NewCmdStack() - for _, v := range u.cmds { - s.Push(v) - } - for i := 0; i < u.popCount; i++ { - s.Pop() - } - top, ok := s.Pop() - assert.Equal(t, u.e.ok, ok) - assert.Equal(t, u.e.val, top) - } -} - -func TestCmdStackEmpty(t *testing.T) { - uu := []struct { - cmds []string - popCount int - e bool - }{ - {[]string{}, 0, true}, - {[]string{"a", "b", "c"}, 0, false}, - {[]string{"a", "b", "c"}, 3, true}, - } - - for _, u := range uu { - s := NewCmdStack() - for _, v := range u.cmds { - s.Push(v) - } - for i := 0; i < u.popCount; i++ { - s.Pop() - } - assert.Equal(t, u.e, s.Empty()) - } -} diff --git a/internal/ui/cmd_test.go b/internal/ui/cmd_test.go deleted file mode 100644 index ad33e552..00000000 --- a/internal/ui/cmd_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package ui - -import ( - "testing" - - "github.com/derailed/k9s/internal/config" - "github.com/stretchr/testify/assert" -) - -func TestNewCmdUpdate(t *testing.T) { - defaults, _ := config.NewStyles("") - v := NewCmdView(defaults) - v.update("blee") - - assert.Equal(t, "\x00> blee\n", v.GetText(false)) -} - -func TestCmdInCmdMode(t *testing.T) { - defaults, _ := config.NewStyles("") - v := NewCmdView(defaults) - v.update("blee") - v.append('!') - - assert.Equal(t, "\x00> blee!\n", v.GetText(false)) - assert.False(t, v.InCmdMode()) - v.BufferActive(true, CommandBuff) - assert.True(t, v.InCmdMode()) -} diff --git a/internal/ui/command.go b/internal/ui/command.go new file mode 100644 index 00000000..f50c4fc3 --- /dev/null +++ b/internal/ui/command.go @@ -0,0 +1,108 @@ +package ui + +import ( + "fmt" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/tview" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" +) + +const defaultPrompt = "%c> %s" + +// Command captures users free from command input. +type Command struct { + *tview.TextView + + activated bool + icon rune + text string + styles *config.Styles +} + +// NewCommand returns a new command view. +func NewCommand(styles *config.Styles) *Command { + c := Command{styles: styles, TextView: tview.NewTextView()} + c.SetWordWrap(true) + c.SetWrap(true) + c.SetDynamicColors(true) + c.SetBorder(true) + c.SetBorderPadding(0, 0, 1, 1) + c.SetBackgroundColor(styles.BgColor()) + c.SetTextColor(styles.FgColor()) + styles.AddListener(&c) + + return &c +} + +func (c *Command) StylesChanged(s *config.Styles) { + c.styles = s + c.SetBackgroundColor(s.BgColor()) + c.SetTextColor(s.FgColor()) +} + +// InCmdMode returns true if command is active, false otherwise. +func (c *Command) InCmdMode() bool { + return c.activated +} + +func (c *Command) activate() { + c.write(c.text) +} + +func (c *Command) update(s string) { + if c.text == s { + return + } + c.text = s + c.Clear() + c.write(c.text) +} + +func (c *Command) write(s string) { + fmt.Fprintf(c, defaultPrompt, c.icon, s) +} + +// ---------------------------------------------------------------------------- +// Event Listener protocol... + +// BufferChanged indicates the buffer was changed. +func (c *Command) BufferChanged(s string) { + c.update(s) +} + +// BufferActive indicates the buff activity changed. +func (c *Command) BufferActive(f bool, k BufferKind) { + if c.activated = f; f { + c.SetBorder(true) + c.SetTextColor(c.styles.FgColor()) + c.SetBorderColor(colorFor(k)) + c.icon = iconFor(k) + // c.reset() + c.activate() + } else { + c.SetBorder(false) + c.SetBackgroundColor(c.styles.BgColor()) + c.Clear() + } + log.Debug().Msgf("Command activated: %t", c.activated) +} + +func colorFor(k BufferKind) tcell.Color { + switch k { + case CommandBuff: + return tcell.ColorAqua + default: + return tcell.ColorSeaGreen + } +} + +func iconFor(k BufferKind) rune { + switch k { + case CommandBuff: + return '🐶' + default: + return '🐩' + } +} diff --git a/internal/ui/command_test.go b/internal/ui/command_test.go new file mode 100644 index 00000000..bfe82f74 --- /dev/null +++ b/internal/ui/command_test.go @@ -0,0 +1,44 @@ +package ui_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/ui" + "github.com/stretchr/testify/assert" +) + +func TestCmdNew(t *testing.T) { + v := ui.NewCommand(config.NewStyles()) + + buff := ui.NewCmdBuff(':', ui.CommandBuff) + buff.AddListener(v) + buff.Set("blee") + + assert.Equal(t, "\x00> blee\n", v.GetText(false)) +} + +func TestCmdUpdate(t *testing.T) { + v := ui.NewCommand(config.NewStyles()) + + buff := ui.NewCmdBuff(':', ui.CommandBuff) + buff.AddListener(v) + + buff.Set("blee") + buff.Add('!') + + assert.Equal(t, "\x00> blee!\n", v.GetText(false)) + assert.False(t, v.InCmdMode()) +} + +func TestCmdMode(t *testing.T) { + v := ui.NewCommand(config.NewStyles()) + + buff := ui.NewCmdBuff(':', ui.CommandBuff) + buff.AddListener(v) + + for _, f := range []bool{false, true} { + buff.SetActive(f) + assert.Equal(t, f, v.InCmdMode()) + } +} diff --git a/internal/ui/config.go b/internal/ui/config.go index 69d27e33..476dc79a 100644 --- a/internal/ui/config.go +++ b/internal/ui/config.go @@ -2,21 +2,40 @@ package ui import ( "context" + "fmt" "path/filepath" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/render" "github.com/derailed/tview" "github.com/fsnotify/fsnotify" "github.com/rs/zerolog/log" ) +// Synchronizer manages ui event queue. type synchronizer interface { QueueUpdateDraw(func()) *tview.Application QueueUpdate(func()) *tview.Application } +// Configurator represents an application configurationa. +type Configurator struct { + skinFile string + Config *config.Config + Styles *config.Styles + Bench *config.Bench +} + +func (c *Configurator) HasSkins() bool { + return c.skinFile != "" +} + // StylesUpdater watches for skin file changes. func (c *Configurator) StylesUpdater(ctx context.Context, s synchronizer) error { + if !c.HasSkins() { + return nil + } + w, err := fsnotify.NewWatcher() if err != nil { return err @@ -28,19 +47,23 @@ func (c *Configurator) StylesUpdater(ctx context.Context, s synchronizer) error case evt := <-w.Events: _ = evt s.QueueUpdateDraw(func() { - c.RefreshStyles() + c.RefreshStyles(c.Config.K9s.CurrentCluster) }) case err := <-w.Errors: log.Info().Err(err).Msg("Skin watcher failed") return case <-ctx.Done(): - w.Close() + log.Debug().Msgf("SkinWatcher Done `%s!!", c.skinFile) + if err := w.Close(); err != nil { + log.Error().Err(err).Msg("Closing watcher") + } return } } }() - return w.Add(config.K9sStylesFile) + log.Debug().Msgf("SkinWatcher watching `%s", c.skinFile) + return w.Add(c.skinFile) } // InitBench load benchmark configuration if any. @@ -51,26 +74,40 @@ func (c *Configurator) InitBench(cluster string) { } } -// RefreshStyles load for skin configuration changes. -func (c *Configurator) RefreshStyles() { - var err error - if c.Styles, err = config.NewStyles(config.K9sStylesFile); err != nil { - log.Info().Msg("No skin file found. Loading stock skins.") - } - if err == nil { - c.HasSkins = true - } - c.Styles.Update() - - StdColor = config.AsColor(c.Styles.Frame().Status.NewColor) - AddColor = config.AsColor(c.Styles.Frame().Status.AddColor) - ModColor = config.AsColor(c.Styles.Frame().Status.ModifyColor) - ErrColor = config.AsColor(c.Styles.Frame().Status.ErrorColor) - HighlightColor = config.AsColor(c.Styles.Frame().Status.HighlightColor) - CompletedColor = config.AsColor(c.Styles.Frame().Status.CompletedColor) -} - // BenchConfig location of the benchmarks configuration file. func BenchConfig(cluster string) string { return filepath.Join(config.K9sHome, config.K9sBench+"-"+cluster+".yml") } + +// RefreshStyles load for skin configuration changes. +func (c *Configurator) RefreshStyles(cluster string) { + clusterSkins := filepath.Join(config.K9sHome, fmt.Sprintf("%s_skin.yml", cluster)) + if c.Styles == nil { + c.Styles = config.NewStyles() + } + if err := c.Styles.Load(clusterSkins); err != nil { + log.Info().Msgf("No cluster specific skin file found -- %s", clusterSkins) + } else { + log.Debug().Msgf("Found cluster skins %s", clusterSkins) + c.updateStyles(clusterSkins) + return + } + + if err := c.Styles.Load(config.K9sStylesFile); err != nil { + log.Info().Msgf("No skin file found -- %s. Loading stock skins.", config.K9sStylesFile) + return + } + c.updateStyles(config.K9sStylesFile) +} + +func (c *Configurator) updateStyles(f string) { + c.skinFile = f + c.Styles.Update() + + render.StdColor = config.AsColor(c.Styles.Frame().Status.NewColor) + render.AddColor = config.AsColor(c.Styles.Frame().Status.AddColor) + render.ModColor = config.AsColor(c.Styles.Frame().Status.ModifyColor) + render.ErrColor = config.AsColor(c.Styles.Frame().Status.ErrorColor) + render.HighlightColor = config.AsColor(c.Styles.Frame().Status.HighlightColor) + render.CompletedColor = config.AsColor(c.Styles.Frame().Status.CompletedColor) +} diff --git a/internal/ui/config_test.go b/internal/ui/config_test.go new file mode 100644 index 00000000..0c409721 --- /dev/null +++ b/internal/ui/config_test.go @@ -0,0 +1,40 @@ +package ui_test + +import ( + "path/filepath" + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + "github.com/gdamore/tcell" + "github.com/stretchr/testify/assert" +) + +func TestBenchConfig(t *testing.T) { + config.K9sHome = "/tmp/blee" + assert.Equal(t, "/tmp/blee/bench-fred.yml", ui.BenchConfig("fred")) +} + +func TestConfiguratorRefreshStyle(t *testing.T) { + config.K9sStylesFile = filepath.Join("..", "config", "test_assets", "black_and_wtf.yml") + + cfg := ui.Configurator{} + cfg.RefreshStyles("") + + assert.True(t, cfg.HasSkins()) + assert.Equal(t, tcell.ColorGhostWhite, render.StdColor) + assert.Equal(t, tcell.ColorWhiteSmoke, render.ErrColor) +} + +func TestInitBench(t *testing.T) { + config.K9sHome = filepath.Join("..", "config", "test_assets") + + cfg := ui.Configurator{} + cfg.InitBench("fred") + + assert.NotNil(t, cfg.Bench) + assert.Equal(t, 2, cfg.Bench.Benchmarks.Defaults.C) + assert.Equal(t, 1000, cfg.Bench.Benchmarks.Defaults.N) + assert.Equal(t, 2, len(cfg.Bench.Benchmarks.Services)) +} diff --git a/internal/ui/crumbs.go b/internal/ui/crumbs.go index ec419578..37dd9293 100644 --- a/internal/ui/crumbs.go +++ b/internal/ui/crumbs.go @@ -2,42 +2,69 @@ package ui import ( "fmt" + "strings" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/model" "github.com/derailed/tview" ) -// CrumbsView represents user breadcrumbs. -type CrumbsView struct { +// Crumbs represents user breadcrumbs. +type Crumbs struct { *tview.TextView styles *config.Styles + stack *model.Stack } -// NewCrumbsView returns a new breadcrumb view. -func NewCrumbsView(styles *config.Styles) *CrumbsView { - v := CrumbsView{styles: styles, TextView: tview.NewTextView()} - { - v.SetBackgroundColor(styles.BgColor()) - v.SetTextAlign(tview.AlignLeft) - v.SetBorderPadding(0, 0, 1, 1) - v.SetDynamicColors(true) +// NewCrumbs returns a new breadcrumb view. +func NewCrumbs(styles *config.Styles) *Crumbs { + c := Crumbs{ + stack: model.NewStack(), + styles: styles, + TextView: tview.NewTextView(), } + c.SetBackgroundColor(styles.BgColor()) + c.SetTextAlign(tview.AlignLeft) + c.SetBorderPadding(0, 0, 1, 1) + c.SetDynamicColors(true) + styles.AddListener(&c) - return &v + return &c } +func (c *Crumbs) StylesChanged(s *config.Styles) { + c.styles = s + c.SetBackgroundColor(s.BgColor()) + c.refresh(c.stack.Flatten()) +} + +// StackPushed indicates a new item was added. +func (c *Crumbs) StackPushed(comp model.Component) { + c.stack.Push(comp) + c.refresh(c.stack.Flatten()) +} + +// StackPopped indicates an item was deleted +func (c *Crumbs) StackPopped(_, _ model.Component) { + c.stack.Pop() + c.refresh(c.stack.Flatten()) +} + +// StackTop indicates the top of the stack +func (c *Crumbs) StackTop(top model.Component) {} + // Refresh updates view with new crumbs. -func (v *CrumbsView) Refresh(crumbs []string) { - v.Clear() - last, bgColor := len(crumbs)-1, v.styles.Frame().Crumb.BgColor - for i, c := range crumbs { +func (c *Crumbs) refresh(crumbs []string) { + c.Clear() + last, bgColor := len(crumbs)-1, c.styles.Frame().Crumb.BgColor + for i, crumb := range crumbs { if i == last { - bgColor = v.styles.Frame().Crumb.ActiveColor + bgColor = c.styles.Frame().Crumb.ActiveColor } - fmt.Fprintf(v, "[%s:%s:b] <%s> [-:%s:-] ", - v.styles.Frame().Crumb.FgColor, - bgColor, c, - v.styles.Body().BgColor) + fmt.Fprintf(c, "[%s:%s:b] <%s> [-:%s:-] ", + c.styles.Frame().Crumb.FgColor, + bgColor, strings.Replace(strings.ToLower(crumb), " ", "", -1), + c.styles.Body().BgColor) } } diff --git a/internal/ui/crumbs_test.go b/internal/ui/crumbs_test.go index dbbb2df8..3d3c807f 100644 --- a/internal/ui/crumbs_test.go +++ b/internal/ui/crumbs_test.go @@ -1,16 +1,51 @@ -package ui +package ui_test import ( + "context" "testing" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/tview" + "github.com/gdamore/tcell" + "github.com/rs/zerolog" "github.com/stretchr/testify/assert" ) -func TestNewCrumbs(t *testing.T) { - defaults, _ := config.NewStyles("") - v := NewCrumbsView(defaults) - v.Refresh([]string{"blee", "duh"}) - - assert.Equal(t, "[black:aqua:b] [-:black:-] [black:orange:b] [-:black:-] \n", v.GetText(false)) +func init() { + zerolog.SetGlobalLevel(zerolog.FatalLevel) } + +func TestNewCrumbs(t *testing.T) { + v := ui.NewCrumbs(config.NewStyles()) + v.StackPushed(makeComponent("c1")) + v.StackPushed(makeComponent("c2")) + v.StackPushed(makeComponent("c3")) + + assert.Equal(t, "[black:aqua:b] [-:black:-] [black:aqua:b] [-:black:-] [black:orange:b] [-:black:-] \n", v.GetText(false)) +} + +// Helpers... + +type c struct { + name string +} + +func makeComponent(n string) c { + return c{name: n} +} + +func (c c) HasFocus() bool { return true } +func (c c) Hints() model.MenuHints { return nil } +func (c c) Name() string { return c.name } +func (c c) Draw(tcell.Screen) {} +func (c c) InputHandler() func(*tcell.EventKey, func(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 } diff --git a/internal/ui/deltas.go b/internal/ui/deltas.go index 8be6f137..049a89ea 100644 --- a/internal/ui/deltas.go +++ b/internal/ui/deltas.go @@ -6,7 +6,7 @@ import ( "strings" "time" - res "github.com/derailed/k9s/internal/resource" + "github.com/derailed/k9s/internal/render" "k8s.io/apimachinery/pkg/api/resource" ) @@ -21,59 +21,98 @@ const ( var percent = regexp.MustCompile(`\A(\d+)\%\z`) +func deltaNumb(o, n string) (string, bool) { + var delta string + + i, ok := numerical(o) + if !ok { + return delta, ok + } + + j, _ := numerical(n) + switch { + case i < j: + delta = PlusSign + case i > j: + delta = MinusSign + } + + return delta, ok +} + +func deltaPerc(o, n string) (string, bool) { + var delta string + i, ok := percentage(o) + if !ok { + return delta, ok + } + + j, _ := percentage(n) + switch { + case i < j: + delta = PlusSign + case i > j: + delta = MinusSign + } + + return delta, ok +} + +func deltaQty(o, n string) (string, bool) { + var delta string + q1, err := resource.ParseQuantity(o) + if err != nil { + return delta, false + } + + q2, _ := resource.ParseQuantity(n) + switch q1.Cmp(q2) { + case -1: + delta = PlusSign + case 1: + delta = MinusSign + } + return delta, true +} + +func deltaDur(o, n string) (string, bool) { + var delta string + d1, err := time.ParseDuration(o) + if err != nil { + return delta, false + } + + d2, _ := time.ParseDuration(n) + switch { + case d2-d1 > 0: + delta = PlusSign + case d2-d1 < 0: + delta = MinusSign + } + return delta, true +} + // Deltas signals diffs between 2 strings. func Deltas(o, n string) string { o, n = strings.TrimSpace(o), strings.TrimSpace(n) - if o == "" || o == res.NAValue { + if o == "" || o == render.NAValue { return "" } - if i, ok := numerical(o); ok { - j, _ := numerical(n) - switch { - case i < j: - return PlusSign - case i > j: - return MinusSign - default: - return "" - } + if d, ok := deltaNumb(o, n); ok { + return d } - if i, ok := percentage(o); ok { - j, _ := percentage(n) - switch { - case i < j: - return PlusSign - case i > j: - return MinusSign - default: - return "" - } + if d, ok := deltaPerc(o, n); ok { + return d } - if q1, err := resource.ParseQuantity(o); err == nil { - q2, _ := resource.ParseQuantity(n) - switch q1.Cmp(q2) { - case -1: - return PlusSign - case 1: - return MinusSign - default: - return "" - } + if d, ok := deltaQty(o, n); ok { + return d } - if d1, err := time.ParseDuration(o); err == nil { - d2, _ := time.ParseDuration(n) - switch { - case d2-d1 > 0: - return PlusSign - case d2-d1 < 0: - return MinusSign - default: - return "" - } + if d, ok := deltaDur(o, n); ok { + return d } switch strings.Compare(o, n) { diff --git a/internal/ui/deltas_test.go b/internal/ui/deltas_test.go index 90151b1c..6796d589 100644 --- a/internal/ui/deltas_test.go +++ b/internal/ui/deltas_test.go @@ -3,7 +3,7 @@ package ui import ( "testing" - "github.com/derailed/k9s/internal/resource" + "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) @@ -12,8 +12,8 @@ func TestDeltas(t *testing.T) { s1, s2, e string }{ {"", "", ""}, - {resource.MissingValue, "", DeltaSign}, - {resource.NAValue, "", ""}, + {render.MissingValue, "", DeltaSign}, + {render.NAValue, "", ""}, {"fred", "fred", ""}, {"fred", "blee", DeltaSign}, {"1", "1", ""}, diff --git a/internal/ui/dialog/confirm.go b/internal/ui/dialog/confirm.go index a3b9b6a1..315496c2 100644 --- a/internal/ui/dialog/confirm.go +++ b/internal/ui/dialog/confirm.go @@ -1,6 +1,7 @@ package dialog import ( + "github.com/derailed/k9s/internal/ui" "github.com/derailed/tview" "github.com/gdamore/tcell" ) @@ -12,7 +13,7 @@ type ( ) // ShowConfirm pops a confirmation dialog. -func ShowConfirm(pages *tview.Pages, title, msg string, ack confirmFunc, cancel cancelFunc) { +func ShowConfirm(pages *ui.Pages, title, msg string, ack confirmFunc, cancel cancelFunc) { f := tview.NewForm() f.SetItemPadding(0) f.SetButtonsAlign(tview.AlignCenter). @@ -30,7 +31,7 @@ func ShowConfirm(pages *tview.Pages, title, msg string, ack confirmFunc, cancel cancel() }) - modal := tview.NewModalForm(title, f) + modal := tview.NewModalForm(" <"+title+"> ", f) modal.SetText(msg) modal.SetDoneFunc(func(int, string) { dismissConfirm(pages) @@ -40,6 +41,6 @@ func ShowConfirm(pages *tview.Pages, title, msg string, ack confirmFunc, cancel pages.ShowPage(confirmKey) } -func dismissConfirm(pages *tview.Pages) { +func dismissConfirm(pages *ui.Pages) { pages.RemovePage(confirmKey) } diff --git a/internal/ui/dialog/confirm_test.go b/internal/ui/dialog/confirm_test.go index b4c61c2b..c83b8547 100644 --- a/internal/ui/dialog/confirm_test.go +++ b/internal/ui/dialog/confirm_test.go @@ -3,13 +3,14 @@ package dialog import ( "testing" + "github.com/derailed/k9s/internal/ui" "github.com/derailed/tview" "github.com/stretchr/testify/assert" ) func TestConfirmDialog(t *testing.T) { a := tview.NewApplication() - p := tview.NewPages() + p := ui.NewPages() a.SetRoot(p, false) ackFunc := func() { diff --git a/internal/ui/dialog/delete.go b/internal/ui/dialog/delete.go index 08658ddf..c0397794 100644 --- a/internal/ui/dialog/delete.go +++ b/internal/ui/dialog/delete.go @@ -1,6 +1,7 @@ package dialog import ( + "github.com/derailed/k9s/internal/ui" "github.com/derailed/tview" "github.com/gdamore/tcell" ) @@ -13,7 +14,7 @@ type ( ) // ShowDelete pops a resource deletion dialog. -func ShowDelete(pages *tview.Pages, msg string, ok okFunc, cancel cancelFunc) { +func ShowDelete(pages *ui.Pages, msg string, ok okFunc, cancel cancelFunc) { cascade, force := true, false f := tview.NewForm() f.SetItemPadding(0) @@ -48,6 +49,6 @@ func ShowDelete(pages *tview.Pages, msg string, ok okFunc, cancel cancelFunc) { pages.ShowPage(deleteKey) } -func dismissDelete(pages *tview.Pages) { +func dismissDelete(pages *ui.Pages) { pages.RemovePage(deleteKey) } diff --git a/internal/ui/dialog/delete_test.go b/internal/ui/dialog/delete_test.go index 01772fd7..1a8af243 100644 --- a/internal/ui/dialog/delete_test.go +++ b/internal/ui/dialog/delete_test.go @@ -3,12 +3,13 @@ package dialog import ( "testing" + "github.com/derailed/k9s/internal/ui" "github.com/derailed/tview" "github.com/stretchr/testify/assert" ) func TestDeleteDialog(t *testing.T) { - p := tview.NewPages() + p := ui.NewPages() okFunc := func(c, f bool) { assert.True(t, c) diff --git a/internal/ui/dialog/port_forward.go b/internal/ui/dialog/port_forward.go index 1c2c6e38..52a6cb7d 100644 --- a/internal/ui/dialog/port_forward.go +++ b/internal/ui/dialog/port_forward.go @@ -3,6 +3,7 @@ package dialog import ( "strings" + "github.com/derailed/k9s/internal/ui" "github.com/derailed/tview" "github.com/gdamore/tcell" ) @@ -10,7 +11,7 @@ import ( const portForwardKey = "portforward" // ShowPortForward pops a port forwarding configuration dialog. -func ShowPortForward(p *tview.Pages, port string, okFn func(lport, cport string)) { +func ShowPortForward(p *ui.Pages, port string, okFn func(address, lport, cport string)) { f := tview.NewForm() f.SetItemPadding(0) f.SetButtonsAlign(tview.AlignCenter). @@ -19,16 +20,19 @@ func ShowPortForward(p *tview.Pages, port string, okFn func(lport, cport string) SetLabelColor(tcell.ColorAqua). SetFieldTextColor(tcell.ColorOrange) - p1, p2 := port, port - f.AddInputField("Pod Port:", p1, 20, nil, func(port string) { - p1 = port + p1, p2, address := port, port, "localhost" + f.AddInputField("Pod Port:", p1, 20, nil, func(p string) { + p1 = p }) - f.AddInputField("Local Port:", p2, 20, nil, func(port string) { - p2 = port + f.AddInputField("Local Port:", p2, 20, nil, func(p string) { + p2 = p + }) + f.AddInputField("Address:", address, 20, nil, func(h string) { + address = h }) f.AddButton("OK", func() { - okFn(stripPort(p2), stripPort(p1)) + okFn(address, stripPort(p2), stripPort(p1)) }) f.AddButton("Cancel", func() { DismissPortForward(p) @@ -43,10 +47,11 @@ func ShowPortForward(p *tview.Pages, port string, okFn func(lport, cport string) } // DismissPortForward dismiss the port forward dialog. -func DismissPortForward(p *tview.Pages) { +func DismissPortForward(p *ui.Pages) { p.RemovePage(portForwardKey) } +// ---------------------------------------------------------------------------- // Helpers... // StripPort removes the named port id if present. diff --git a/internal/ui/dialog/port_forward_test.go b/internal/ui/dialog/port_forward_test.go index 7621b039..1c2461e1 100644 --- a/internal/ui/dialog/port_forward_test.go +++ b/internal/ui/dialog/port_forward_test.go @@ -3,14 +3,15 @@ package dialog import ( "testing" + "github.com/derailed/k9s/internal/ui" "github.com/derailed/tview" "github.com/stretchr/testify/assert" ) func TestPortForwardDialog(t *testing.T) { - p := tview.NewPages() + p := ui.NewPages() - okFunc := func(lport, cport string) { + okFunc := func(address, lport, cport string) { } ShowPortForward(p, "8080", okFunc) @@ -36,7 +37,8 @@ func TestStripPort(t *testing.T) { }, } - for k, u := range uu { + for k := range uu { + u := uu[k] t.Run(k, func(t *testing.T) { assert.Equal(t, u.e, stripPort(u.port)) }) diff --git a/internal/ui/flash.go b/internal/ui/flash.go index b5fea4d4..8c2c883d 100644 --- a/internal/ui/flash.go +++ b/internal/ui/flash.go @@ -6,7 +6,8 @@ import ( "strings" "time" - "github.com/derailed/k9s/internal/resource" + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/render" "github.com/derailed/tview" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" @@ -34,54 +35,60 @@ type ( // FlashLevel represents flash message severity. FlashLevel int - // FlashView represents a flash message indicator. - FlashView struct { + // Flash represents a flash message indicator. + Flash struct { *tview.TextView cancel context.CancelFunc - app *tview.Application + app *App } ) -// NewFlashView returns a new flash view. -func NewFlashView(app *tview.Application, m string) *FlashView { - f := FlashView{app: app, TextView: tview.NewTextView()} +// NewFlash returns a new flash view. +func NewFlash(app *App, m string) *Flash { + f := Flash{app: app, TextView: tview.NewTextView()} f.SetTextColor(tcell.ColorAqua) f.SetTextAlign(tview.AlignLeft) f.SetBorderPadding(0, 0, 1, 1) f.SetText("") + f.app.Styles.AddListener(&f) return &f } +func (f *Flash) StylesChanged(s *config.Styles) { + f.SetBackgroundColor(s.BgColor()) + f.SetTextColor(s.FgColor()) +} + // Info displays an info flash message. -func (v *FlashView) Info(msg string) { - v.setMessage(FlashInfo, msg) +func (f *Flash) Info(msg string) { + f.setMessage(FlashInfo, msg) } // Infof displays a formatted info flash message. -func (v *FlashView) Infof(fmat string, args ...interface{}) { - v.Info(fmt.Sprintf(fmat, args...)) +func (f *Flash) Infof(fmat string, args ...interface{}) { + f.Info(fmt.Sprintf(fmat, args...)) } // Warn displays a warning flash message. -func (v *FlashView) Warn(msg string) { - v.setMessage(FlashWarn, msg) +func (f *Flash) Warn(msg string) { + f.setMessage(FlashWarn, msg) } // Warnf displays a formatted warning flash message. -func (v *FlashView) Warnf(fmat string, args ...interface{}) { - v.Warn(fmt.Sprintf(fmat, args...)) +func (f *Flash) Warnf(fmat string, args ...interface{}) { + f.Warn(fmt.Sprintf(fmat, args...)) } // Err displays an error flash message. -func (v *FlashView) Err(err error) { +func (f *Flash) Err(err error) { log.Error().Err(err).Msgf("%v", err) - v.setMessage(FlashErr, err.Error()) + f.setMessage(FlashErr, err.Error()) } // Errf displays a formatted error flash message. -func (v *FlashView) Errf(fmat string, args ...interface{}) { +func (f *Flash) Errf(fmat string, args ...interface{}) { var err error for _, a := range args { switch e := a.(type) { @@ -90,30 +97,30 @@ func (v *FlashView) Errf(fmat string, args ...interface{}) { } } log.Error().Err(err).Msgf(fmat, args...) - v.setMessage(FlashErr, fmt.Sprintf(fmat, args...)) + f.setMessage(FlashErr, fmt.Sprintf(fmat, args...)) } -func (v *FlashView) setMessage(level FlashLevel, msg ...string) { - if v.cancel != nil { - v.cancel() +func (f *Flash) setMessage(level FlashLevel, msg ...string) { + if f.cancel != nil { + f.cancel() } var ctx1, ctx2 context.Context { var timerCancel context.CancelFunc - ctx1, v.cancel = context.WithCancel(context.TODO()) + ctx1, f.cancel = context.WithCancel(context.TODO()) ctx2, timerCancel = context.WithTimeout(context.TODO(), flashDelay*time.Second) - go v.refresh(ctx1, ctx2, timerCancel) + go f.refresh(ctx1, ctx2, timerCancel) } - _, _, width, _ := v.GetRect() + _, _, width, _ := f.GetRect() if width <= 15 { width = 100 } m := strings.Join(msg, " ") - v.SetTextColor(flashColor(level)) - v.SetText(resource.Truncate(flashEmoji(level)+" "+m, width-3)) + f.SetTextColor(flashColor(level)) + f.SetText(render.Truncate(flashEmoji(level)+" "+m, width-3)) } -func (v *FlashView) refresh(ctx1, ctx2 context.Context, cancel context.CancelFunc) { +func (f *Flash) refresh(ctx1, ctx2 context.Context, cancel context.CancelFunc) { defer cancel() for { select { @@ -122,9 +129,8 @@ func (v *FlashView) refresh(ctx1, ctx2 context.Context, cancel context.CancelFun return // Timed out clear and bail case <-ctx2.Done(): - v.app.QueueUpdateDraw(func() { - v.Clear() - v.app.Draw() + f.app.QueueUpdateDraw(func() { + f.Clear() }) return } diff --git a/internal/ui/flash_test.go b/internal/ui/flash_test.go index 3af0a469..7f3f022e 100644 --- a/internal/ui/flash_test.go +++ b/internal/ui/flash_test.go @@ -1,41 +1,40 @@ -package ui +package ui_test import ( + "errors" "testing" - "github.com/gdamore/tcell" + "github.com/derailed/k9s/internal/ui" "github.com/stretchr/testify/assert" ) -func TestFlashEmoji(t *testing.T) { - uu := []struct { - level FlashLevel - emoji string - }{ - {FlashWarn, emoDoh}, - {FlashErr, emoRed}, - {FlashFatal, emoDead}, - {FlashInfo, emoHappy}, - } +func TestFlashInfo(t *testing.T) { + f := ui.NewFlash(ui.NewApp(""), "YO!") - for _, u := range uu { - assert.Equal(t, u.emoji, flashEmoji(u.level)) - } + f.Info("Blee") + assert.Equal(t, "😎 Blee\n", f.GetText(false)) + + f.Infof("Blee %s", "duh") + assert.Equal(t, "😎 Blee duh\n", f.GetText(false)) } -func TestFlashColor(t *testing.T) { - uu := []struct { - level FlashLevel - color tcell.Color - }{ - {FlashWarn, tcell.ColorOrange}, - {FlashErr, tcell.ColorOrangeRed}, - {FlashFatal, tcell.ColorFuchsia}, - {FlashInfo, tcell.ColorNavajoWhite}, - } +func TestFlashWarn(t *testing.T) { + f := ui.NewFlash(ui.NewApp(""), "YO!") - for _, u := range uu { - assert.Equal(t, u.color, flashColor(u.level)) - } + f.Warn("Blee") + assert.Equal(t, "😗 Blee\n", f.GetText(false)) + + f.Warnf("Blee %s", "duh") + assert.Equal(t, "😗 Blee duh\n", f.GetText(false)) +} + +func TestFlashErr(t *testing.T) { + f := ui.NewFlash(ui.NewApp(""), "YO!") + + f.Err(errors.New("Blee")) + assert.Equal(t, "😡 Blee\n", f.GetText(false)) + + f.Errf("Blee %s", "duh") + assert.Equal(t, "😡 Blee duh\n", f.GetText(false)) } diff --git a/internal/ui/hint.go b/internal/ui/hint.go deleted file mode 100644 index 1845dffa..00000000 --- a/internal/ui/hint.go +++ /dev/null @@ -1,45 +0,0 @@ -package ui - -import ( - "strconv" - "strings" -) - -type ( - // Hint represents keyboard mnemonic. - Hint struct { - Mnemonic string - Description string - Visible bool - } - // Hints a collection of keyboard mnemonics. - Hints []Hint - - // Hinter returns a collection of mnemonics. - Hinter interface { - Hints() Hints - } -) - -func (h Hints) Len() int { - return len(h) -} - -func (h Hints) Swap(i, j int) { - h[i], h[j] = h[j], h[i] -} - -func (h Hints) Less(i, j int) bool { - n, err1 := strconv.Atoi(h[i].Mnemonic) - m, err2 := strconv.Atoi(h[j].Mnemonic) - if err1 == nil && err2 == nil { - return n < m - } - if err1 == nil && err2 != nil { - return true - } - if err1 != nil && err2 == nil { - return false - } - return strings.Compare(h[i].Description, h[j].Description) < 0 -} diff --git a/internal/ui/indicator.go b/internal/ui/indicator.go index 8d5bea25..dd46756b 100644 --- a/internal/ui/indicator.go +++ b/internal/ui/indicator.go @@ -10,8 +10,8 @@ import ( "github.com/gdamore/tcell" ) -// IndicatorView represents a status indicator. -type IndicatorView struct { +// StatusIndicator represents a status indicator when main header is collapsed. +type StatusIndicator struct { *tview.TextView app *App @@ -20,67 +20,74 @@ type IndicatorView struct { cancel context.CancelFunc } -// NewIndicatorView returns a new status indicator. -func NewIndicatorView(app *App, styles *config.Styles) *IndicatorView { - v := IndicatorView{ +// NewStatusIndicator returns a new status indicator. +func NewStatusIndicator(app *App, styles *config.Styles) *StatusIndicator { + s := StatusIndicator{ TextView: tview.NewTextView(), app: app, styles: styles, } - v.SetTextAlign(tview.AlignCenter) - v.SetTextColor(tcell.ColorWhite) - v.SetBackgroundColor(styles.BgColor()) - v.SetDynamicColors(true) + s.SetTextAlign(tview.AlignCenter) + s.SetTextColor(tcell.ColorWhite) + s.SetBackgroundColor(styles.BgColor()) + s.SetDynamicColors(true) + styles.AddListener(&s) - return &v + return &s +} + +func (s *StatusIndicator) StylesChanged(styles *config.Styles) { + s.styles = styles + s.SetBackgroundColor(styles.BgColor()) + s.SetTextColor(styles.FgColor()) } // SetPermanent sets permanent title to be reset to after updates -func (v *IndicatorView) SetPermanent(info string) { - v.permanent = info - v.SetText(info) +func (s *StatusIndicator) SetPermanent(info string) { + s.permanent = info + s.SetText(info) } // Reset clears out the logo view and resets colors. -func (v *IndicatorView) Reset() { - v.Clear() - v.SetPermanent(v.permanent) +func (s *StatusIndicator) Reset() { + s.Clear() + s.SetPermanent(s.permanent) } // Err displays a log error state. -func (v *IndicatorView) Err(msg string) { - v.update(msg, "orangered") +func (s *StatusIndicator) Err(msg string) { + s.update(msg, "orangered") } // Warn displays a log warning state. -func (v *IndicatorView) Warn(msg string) { - v.update(msg, "mediumvioletred") +func (s *StatusIndicator) Warn(msg string) { + s.update(msg, "mediumvioletred") } // Info displays a log info state. -func (v *IndicatorView) Info(msg string) { - v.update(msg, "lawngreen") +func (s *StatusIndicator) Info(msg string) { + s.update(msg, "lawngreen") } -func (v *IndicatorView) update(msg, c string) { - v.setText(fmt.Sprintf("[%s::b] <%s> ", c, msg)) +func (s *StatusIndicator) update(msg, c string) { + s.setText(fmt.Sprintf("[%s::b] <%s> ", c, msg)) } -func (v *IndicatorView) setText(msg string) { - if v.cancel != nil { - v.cancel() +func (s *StatusIndicator) setText(msg string) { + if s.cancel != nil { + s.cancel() } - v.SetText(msg) + s.SetText(msg) var ctx context.Context - ctx, v.cancel = context.WithCancel(context.Background()) + ctx, s.cancel = context.WithCancel(context.Background()) go func(ctx context.Context) { select { case <-ctx.Done(): return case <-time.After(5 * time.Second): - v.app.QueueUpdateDraw(func() { - v.Reset() + s.app.QueueUpdateDraw(func() { + s.Reset() }) } }(ctx) diff --git a/internal/ui/indicator_test.go b/internal/ui/indicator_test.go new file mode 100644 index 00000000..2e3c5a36 --- /dev/null +++ b/internal/ui/indicator_test.go @@ -0,0 +1,39 @@ +package ui_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/ui" + "github.com/stretchr/testify/assert" +) + +func TestIndicatorReset(t *testing.T) { + i := ui.NewStatusIndicator(ui.NewApp(""), config.NewStyles()) + i.SetPermanent("Blee") + i.Info("duh") + i.Reset() + + assert.Equal(t, "Blee\n", i.GetText(false)) +} + +func TestIndicatorInfo(t *testing.T) { + i := ui.NewStatusIndicator(ui.NewApp(""), 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.NewStyles()) + i.Warn("Blee") + + assert.Equal(t, "[mediumvioletred::b] \n", i.GetText(false)) +} + +func TestIndicatorErr(t *testing.T) { + i := ui.NewStatusIndicator(ui.NewApp(""), config.NewStyles()) + i.Err("Blee") + + assert.Equal(t, "[orangered::b] \n", i.GetText(false)) +} diff --git a/internal/ui/key.go b/internal/ui/key.go new file mode 100644 index 00000000..4bbed4f4 --- /dev/null +++ b/internal/ui/key.go @@ -0,0 +1,205 @@ +package ui + +import "github.com/gdamore/tcell" + +func init() { + initKeys() +} + +func initKeys() { + tcell.KeyNames[tcell.Key(KeyHelp)] = "?" + tcell.KeyNames[tcell.Key(KeySlash)] = "/" + tcell.KeyNames[tcell.Key(KeySpace)] = "space" + + initNumbKeys() + initStdKeys() + initShiftKeys() +} + +// Defines numeric keys for container actions +const ( + Key0 int32 = iota + 48 + Key1 + Key2 + Key3 + Key4 + Key5 + Key6 + Key7 + Key8 + Key9 +) + +// Defines numeric keys for container actions +const ( + KeyShift0 int32 = 41 + KeyShift1 int32 = 33 + KeyShift2 int32 = 64 + KeyShift3 int32 = 35 + KeyShift4 int32 = 36 + KeyShift5 int32 = 37 + KeyShift6 int32 = 94 + KeyShift7 int32 = 38 + KeyShift8 int32 = 42 + KeyShift9 int32 = 40 +) + +// Defines char keystrokes +const ( + KeyA tcell.Key = iota + 97 + KeyB + KeyC + KeyD + KeyE + KeyF + KeyG + KeyH + KeyI + KeyJ + KeyK + KeyL + KeyM + KeyN + KeyO + KeyP + KeyQ + KeyR + KeyS + KeyT + KeyU + KeyV + KeyW + KeyX + KeyY + KeyZ + KeyHelp = 63 + KeySlash = 47 + KeyColon = 58 + KeySpace = 32 +) + +// Define Shift Keys +const ( + KeyShiftA tcell.Key = iota + 65 + KeyShiftB + KeyShiftC + KeyShiftD + KeyShiftE + KeyShiftF + KeyShiftG + KeyShiftH + KeyShiftI + KeyShiftJ + KeyShiftK + KeyShiftL + KeyShiftM + KeyShiftN + KeyShiftO + KeyShiftP + KeyShiftQ + KeyShiftR + KeyShiftS + KeyShiftT + KeyShiftU + KeyShiftV + KeyShiftW + KeyShiftX + KeyShiftY + KeyShiftZ +) + +// NumKeys tracks number keys. +var NumKeys = map[int]int32{ + 0: Key0, + 1: Key1, + 2: Key2, + 3: Key3, + 4: Key4, + 5: Key5, + 6: Key6, + 7: Key7, + 8: Key8, + 9: Key9, +} + +func initNumbKeys() { + tcell.KeyNames[tcell.Key(Key0)] = "0" + tcell.KeyNames[tcell.Key(Key1)] = "1" + tcell.KeyNames[tcell.Key(Key2)] = "2" + tcell.KeyNames[tcell.Key(Key3)] = "3" + tcell.KeyNames[tcell.Key(Key4)] = "4" + tcell.KeyNames[tcell.Key(Key5)] = "5" + tcell.KeyNames[tcell.Key(Key6)] = "6" + tcell.KeyNames[tcell.Key(Key7)] = "7" + tcell.KeyNames[tcell.Key(Key8)] = "8" + tcell.KeyNames[tcell.Key(Key9)] = "9" +} + +func initStdKeys() { + tcell.KeyNames[tcell.Key(KeyA)] = "a" + tcell.KeyNames[tcell.Key(KeyB)] = "b" + tcell.KeyNames[tcell.Key(KeyC)] = "c" + tcell.KeyNames[tcell.Key(KeyD)] = "d" + tcell.KeyNames[tcell.Key(KeyE)] = "e" + tcell.KeyNames[tcell.Key(KeyF)] = "f" + tcell.KeyNames[tcell.Key(KeyG)] = "g" + tcell.KeyNames[tcell.Key(KeyH)] = "h" + tcell.KeyNames[tcell.Key(KeyI)] = "i" + tcell.KeyNames[tcell.Key(KeyJ)] = "j" + tcell.KeyNames[tcell.Key(KeyK)] = "k" + tcell.KeyNames[tcell.Key(KeyL)] = "l" + tcell.KeyNames[tcell.Key(KeyM)] = "m" + tcell.KeyNames[tcell.Key(KeyN)] = "n" + tcell.KeyNames[tcell.Key(KeyO)] = "o" + tcell.KeyNames[tcell.Key(KeyP)] = "p" + tcell.KeyNames[tcell.Key(KeyQ)] = "q" + tcell.KeyNames[tcell.Key(KeyR)] = "r" + tcell.KeyNames[tcell.Key(KeyS)] = "s" + tcell.KeyNames[tcell.Key(KeyT)] = "t" + tcell.KeyNames[tcell.Key(KeyU)] = "u" + tcell.KeyNames[tcell.Key(KeyV)] = "v" + tcell.KeyNames[tcell.Key(KeyW)] = "w" + tcell.KeyNames[tcell.Key(KeyX)] = "x" + tcell.KeyNames[tcell.Key(KeyY)] = "y" + tcell.KeyNames[tcell.Key(KeyZ)] = "z" +} + +func initShiftKeys() { + tcell.KeyNames[tcell.Key(KeyShift0)] = "Shift-0" + tcell.KeyNames[tcell.Key(KeyShift1)] = "Shift-1" + tcell.KeyNames[tcell.Key(KeyShift2)] = "Shift-2" + tcell.KeyNames[tcell.Key(KeyShift3)] = "Shift-3" + tcell.KeyNames[tcell.Key(KeyShift4)] = "Shift-4" + tcell.KeyNames[tcell.Key(KeyShift5)] = "Shift-5" + tcell.KeyNames[tcell.Key(KeyShift6)] = "Shift-6" + tcell.KeyNames[tcell.Key(KeyShift7)] = "Shift-7" + tcell.KeyNames[tcell.Key(KeyShift8)] = "Shift-8" + tcell.KeyNames[tcell.Key(KeyShift9)] = "Shift-9" + + tcell.KeyNames[tcell.Key(KeyShiftA)] = "Shift-A" + tcell.KeyNames[tcell.Key(KeyShiftB)] = "Shift-B" + tcell.KeyNames[tcell.Key(KeyShiftC)] = "Shift-C" + tcell.KeyNames[tcell.Key(KeyShiftD)] = "Shift-D" + tcell.KeyNames[tcell.Key(KeyShiftE)] = "Shift-E" + tcell.KeyNames[tcell.Key(KeyShiftF)] = "Shift-F" + tcell.KeyNames[tcell.Key(KeyShiftG)] = "Shift-G" + tcell.KeyNames[tcell.Key(KeyShiftH)] = "Shift-H" + tcell.KeyNames[tcell.Key(KeyShiftI)] = "Shift-I" + tcell.KeyNames[tcell.Key(KeyShiftJ)] = "Shift-J" + tcell.KeyNames[tcell.Key(KeyShiftK)] = "Shift-K" + tcell.KeyNames[tcell.Key(KeyShiftL)] = "Shift-L" + tcell.KeyNames[tcell.Key(KeyShiftM)] = "Shift-M" + tcell.KeyNames[tcell.Key(KeyShiftN)] = "Shift-N" + tcell.KeyNames[tcell.Key(KeyShiftO)] = "Shift-O" + tcell.KeyNames[tcell.Key(KeyShiftP)] = "Shift-P" + tcell.KeyNames[tcell.Key(KeyShiftQ)] = "Shift-Q" + tcell.KeyNames[tcell.Key(KeyShiftR)] = "Shift-R" + tcell.KeyNames[tcell.Key(KeyShiftS)] = "Shift-S" + tcell.KeyNames[tcell.Key(KeyShiftT)] = "Shift-T" + tcell.KeyNames[tcell.Key(KeyShiftU)] = "Shift-U" + tcell.KeyNames[tcell.Key(KeyShiftV)] = "Shift-V" + tcell.KeyNames[tcell.Key(KeyShiftW)] = "Shift-W" + tcell.KeyNames[tcell.Key(KeyShiftX)] = "Shift-X" + tcell.KeyNames[tcell.Key(KeyShiftY)] = "Shift-Y" + tcell.KeyNames[tcell.Key(KeyShiftZ)] = "Shift-Z" +} diff --git a/internal/ui/logo.go b/internal/ui/logo.go index 6ac7fe48..e01ae33d 100644 --- a/internal/ui/logo.go +++ b/internal/ui/logo.go @@ -7,67 +7,84 @@ import ( "github.com/derailed/tview" ) -// LogoView represents a K9s logo. -type LogoView struct { +// Logo represents a K9s logo. +type Logo struct { *tview.Flex + logo, status *tview.TextView styles *config.Styles } -// NewLogoView returns a new logo. -func NewLogoView(styles *config.Styles) *LogoView { - v := LogoView{ +// NewLogo returns a new logo. +func NewLogo(styles *config.Styles) *Logo { + l := Logo{ Flex: tview.NewFlex(), logo: logo(), status: status(), styles: styles, } - v.SetDirection(tview.FlexRow) - v.AddItem(v.logo, 0, 6, false) - v.AddItem(v.status, 0, 1, false) - v.refreshLogo(styles.Body().LogoColor) + l.SetDirection(tview.FlexRow) + l.AddItem(l.logo, 0, 6, false) + l.AddItem(l.status, 0, 1, false) + l.refreshLogo(styles.Body().LogoColor) + styles.AddListener(&l) - return &v + return &l +} + +func (l *Logo) Logo() *tview.TextView { + return l.logo +} + +func (l *Logo) Status() *tview.TextView { + return l.status +} + +func (l *Logo) StylesChanged(s *config.Styles) { + l.styles = s + l.Reset() } // Reset clears out the logo view and resets colors. -func (v *LogoView) Reset() { - v.status.Clear() - v.status.SetBackgroundColor(v.styles.BgColor()) - v.refreshLogo(v.styles.Body().LogoColor) +func (l *Logo) Reset() { + l.status.Clear() + l.SetBackgroundColor(l.styles.BgColor()) + l.status.SetBackgroundColor(l.styles.BgColor()) + l.logo.SetBackgroundColor(l.styles.BgColor()) + l.refreshLogo(l.styles.Body().LogoColor) } // Err displays a log error state. -func (v *LogoView) Err(msg string) { - v.update(msg, "red") +func (l *Logo) Err(msg string) { + l.update(msg, "red") } // Warn displays a log warning state. -func (v *LogoView) Warn(msg string) { - v.update(msg, "mediumvioletred") +func (l *Logo) Warn(msg string) { + l.update(msg, "mediumvioletred") } // Info displays a log info state. -func (v *LogoView) Info(msg string) { - v.update(msg, "green") +func (l *Logo) Info(msg string) { + l.update(msg, "green") } -func (v *LogoView) update(msg, c string) { - v.refreshStatus(msg, c) - v.refreshLogo(c) +func (l *Logo) update(msg, c string) { + l.refreshStatus(msg, c) + l.refreshLogo(c) } -func (v *LogoView) refreshStatus(msg, c string) { - v.status.SetBackgroundColor(config.AsColor(c)) - v.status.SetText(fmt.Sprintf("[white::b]%s", msg)) +func (l *Logo) refreshStatus(msg, c string) { + l.status.SetBackgroundColor(config.AsColor(c)) + l.status.SetText(fmt.Sprintf("[white::b]%s", msg)) } -func (v *LogoView) refreshLogo(c string) { - v.logo.Clear() +func (l *Logo) refreshLogo(c string) { + l.logo.Clear() for i, s := range LogoSmall { - fmt.Fprintf(v.logo, "[%s::b]%s", c, s) + fmt.Fprintf(l.logo, "[%s::b]%s", c, s) if i+1 < len(LogoSmall) { - fmt.Fprintf(v.logo, "\n") + fmt.Fprintf(l.logo, "\n") } } } diff --git a/internal/ui/logo_test.go b/internal/ui/logo_test.go index fc163090..ebb67ad5 100644 --- a/internal/ui/logo_test.go +++ b/internal/ui/logo_test.go @@ -1,20 +1,20 @@ -package ui +package ui_test import ( "testing" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/ui" "github.com/stretchr/testify/assert" ) func TestNewLogoView(t *testing.T) { - defaults, _ := config.NewStyles("") - v := NewLogoView(defaults) + v := ui.NewLogo(config.NewStyles()) v.Reset() const elogo = "[orange::b] ____ __.________ \n[orange::b]| |/ _/ __ \\______\n[orange::b]| < \\____ / ___/\n[orange::b]| | \\ / /\\___ \\ \n[orange::b]|____|__ \\ /____//____ >\n[orange::b] \\/ \\/ \n" - assert.Equal(t, elogo, v.logo.GetText(false)) - assert.Equal(t, "", v.status.GetText(false)) + assert.Equal(t, elogo, v.Logo().GetText(false)) + assert.Equal(t, "", v.Status().GetText(false)) } func TestLogoStatus(t *testing.T) { @@ -38,9 +38,9 @@ func TestLogoStatus(t *testing.T) { }, } - defaults, _ := config.NewStyles("") - v := NewLogoView(defaults) - for k, u := range uu { + v := ui.NewLogo(config.NewStyles()) + for n := range uu { + k, u := n, uu[n] t.Run(k, func(t *testing.T) { switch k { case "info": @@ -50,8 +50,8 @@ func TestLogoStatus(t *testing.T) { case "err": v.Err(u.msg) } - assert.Equal(t, u.logo, v.logo.GetText(false)) - assert.Equal(t, u.e, v.status.GetText(false)) + assert.Equal(t, u.logo, v.Logo().GetText(false)) + assert.Equal(t, u.e, v.Status().GetText(false)) }) } diff --git a/internal/ui/menu.go b/internal/ui/menu.go index a45eafcc..50cf47e7 100644 --- a/internal/ui/menu.go +++ b/internal/ui/menu.go @@ -9,80 +9,110 @@ import ( "strings" "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/resource" + "github.com/derailed/k9s/internal/model" "github.com/derailed/tview" - "github.com/gdamore/tcell" + runewidth "github.com/mattn/go-runewidth" ) -func init() { - initKeys() -} - const ( menuIndexFmt = " [key:bg:b]<%d> [fg:bg:d]%s " maxRows = 7 + chopWidth = 20 ) var menuRX = regexp.MustCompile(`\d`) -// MenuView represents menu options. -type MenuView struct { +// Menu presents menu options. +type Menu struct { *tview.Table styles *config.Styles } -// NewMenuView returns a new menu. -func NewMenuView(styles *config.Styles) *MenuView { - v := MenuView{Table: tview.NewTable(), styles: styles} - v.SetBackgroundColor(styles.BgColor()) +// NewMenu returns a new menu. +func NewMenu(styles *config.Styles) *Menu { + m := Menu{ + Table: tview.NewTable(), + styles: styles, + } + m.SetBackgroundColor(styles.BgColor()) + styles.AddListener(&m) - return &v + return &m +} + +func (m *Menu) StylesChanged(s *config.Styles) { + m.styles = s + m.SetBackgroundColor(s.BgColor()) +} + +func (m *Menu) StackPushed(c model.Component) { + m.HydrateMenu(c.Hints()) +} + +func (m *Menu) StackPopped(o, top model.Component) { + if top != nil { + m.HydrateMenu(top.Hints()) + } else { + m.Clear() + } +} + +func (m *Menu) StackTop(t model.Component) { + m.HydrateMenu(t.Hints()) } // HydrateMenu populate menu ui from hints. -func (v *MenuView) HydrateMenu(hh Hints) { - v.Clear() +func (m *Menu) HydrateMenu(hh model.MenuHints) { + m.Clear() sort.Sort(hh) - t := v.buildMenuTable(hh) + + table := make([]model.MenuHints, maxRows+1) + colCount := (len(hh) / maxRows) + 1 + if m.hasDigits(hh) { + colCount++ + } + for row := 0; row < maxRows; row++ { + table[row] = make(model.MenuHints, colCount) + } + t := m.buildMenuTable(hh, table, colCount) + for row := 0; row < len(t); row++ { for col := 0; col < len(t[row]); col++ { - if t[row][col] == "" { - continue - } c := tview.NewTableCell(t[row][col]) - c.SetBackgroundColor(v.styles.BgColor()) - v.SetCell(row, col, c) + if len(t[row][col]) == 0 { + c = tview.NewTableCell("") + } + c.SetBackgroundColor(m.styles.BgColor()) + m.SetCell(row, col, c) } } } -func isDigit(s string) bool { - return menuRX.MatchString(s) -} - -func (v *MenuView) buildMenuTable(hh Hints) [][]string { - table := make([][]Hint, maxRows) - colCount := len(hh) / maxRows - if colCount == 0 { - colCount = 1 - } - if isDigit(hh[0].Mnemonic) { - colCount++ - } - for row := 0; row < maxRows; row++ { - table[row] = make([]Hint, colCount) - } - var row, col, added int - firstCmd := true - maxKeys := make([]int, colCount+1) +func (m *Menu) hasDigits(hh model.MenuHints) bool { for _, h := range hh { if !h.Visible { continue } - if !isDigit(h.Mnemonic) && firstCmd { + if menuRX.MatchString(h.Mnemonic) { + return true + } + } + return false +} + +func (m *Menu) buildMenuTable(hh model.MenuHints, table []model.MenuHints, colCount int) [][]string { + var row, col int + firstCmd := true + maxKeys := make([]int, colCount) + for _, h := range hh { + if !h.Visible { + continue + } + + if !menuRX.MatchString(h.Mnemonic) && firstCmd { row, col, firstCmd = 0, col+1, false - if added == 0 { + if table[0][0].IsBlank() { col = 0 } } @@ -90,23 +120,40 @@ func (v *MenuView) buildMenuTable(hh Hints) [][]string { maxKeys[col] = len(h.Mnemonic) } table[row][col] = h - added, row = added+1, row+1 + row++ if row >= maxRows { row, col = 0, col+1 } } - strTable := make([][]string, maxRows+1) - for r := 0; r < len(table); r++ { - strTable[r] = make([]string, len(table[r])) + out := make([][]string, len(table)) + for r := range out { + out[r] = make([]string, len(table[r])) } - for row := range strTable { - for col := range strTable[row] { - strTable[row][col] = keyConv(v.formatMenu(table[row][col], maxKeys[col])) + m.layout(table, maxKeys, out) + + return out +} + +func (m *Menu) layout(table []model.MenuHints, mm []int, out [][]string) { + for r := range table { + for c := range table[r] { + out[r][c] = keyConv(m.formatMenu(table[r][c], mm[c])) } } - return strTable +} + +func (m *Menu) formatMenu(h model.MenuHint, size int) string { + if h.Mnemonic == "" || h.Description == "" { + return "" + } + i, err := strconv.Atoi(h.Mnemonic) + if err == nil { + return formatNSMenu(i, h.Description, m.styles.Frame()) + } + + return formatPlainMenu(h, size, m.styles.Frame()) } // ---------------------------------------------------------------------------- @@ -124,216 +171,30 @@ func keyConv(s string) string { return strings.Replace(s, "alt", "opt", 1) } +// Truncate a string to the given l and suffix ellipsis if needed. +func Truncate(str string, width int) string { + return runewidth.Truncate(str, width, string(tview.SemigraphicsHorizontalEllipsis)) +} + func toMnemonic(s string) string { if len(s) == 0 { return s } - return "<" + strings.ToLower(s) + ">" -} - -func (v *MenuView) formatMenu(h Hint, size int) string { - i, err := strconv.Atoi(h.Mnemonic) - if err == nil { - return formatNSMenu(i, h.Description, v.styles.Frame()) - } - - return formatPlainMenu(h, size, v.styles.Frame()) + return "<" + keyConv(strings.ToLower(s)) + ">" } func formatNSMenu(i int, name string, styles config.Frame) string { fmat := strings.Replace(menuIndexFmt, "[key", "["+styles.Menu.NumKeyColor, 1) fmat = strings.Replace(fmat, ":bg:", ":"+styles.Title.BgColor+":", -1) fmat = strings.Replace(fmat, "[fg", "["+styles.Menu.FgColor, 1) - return fmt.Sprintf(fmat, i, resource.Truncate(name, 14)) + return fmt.Sprintf(fmat, i, Truncate(name, chopWidth)) } -func formatPlainMenu(h Hint, size int, styles config.Frame) string { +func formatPlainMenu(h model.MenuHint, size int, styles config.Frame) string { menuFmt := " [key:bg:b]%-" + strconv.Itoa(size+2) + "s [fg:bg:d]%s " fmat := strings.Replace(menuFmt, "[key", "["+styles.Menu.KeyColor, 1) fmat = strings.Replace(fmat, "[fg", "["+styles.Menu.FgColor, 1) fmat = strings.Replace(fmat, ":bg:", ":"+styles.Title.BgColor+":", -1) return fmt.Sprintf(fmat, toMnemonic(h.Mnemonic), h.Description) } - -// ----------------------------------------------------------------------------- -// Key mapping Constants - -// Defines numeric keys for container actions -const ( - Key0 int32 = iota + 48 - Key1 - Key2 - Key3 - Key4 - Key5 - Key6 - Key7 - Key8 - Key9 -) - -// Defines char keystrokes -const ( - KeyA tcell.Key = iota + 97 - KeyB - KeyC - KeyD - KeyE - KeyF - KeyG - KeyH - KeyI - KeyJ - KeyK - KeyL - KeyM - KeyN - KeyO - KeyP - KeyQ - KeyR - KeyS - KeyT - KeyU - KeyV - KeyW - KeyX - KeyY - KeyZ - KeyHelp = 63 - KeySlash = 47 - KeyColon = 58 - KeySpace = 32 -) - -// Define Shift Keys -const ( - KeyShiftA tcell.Key = iota + 65 - KeyShiftB - KeyShiftC - KeyShiftD - KeyShiftE - KeyShiftF - KeyShiftG - KeyShiftH - KeyShiftI - KeyShiftJ - KeyShiftK - KeyShiftL - KeyShiftM - KeyShiftN - KeyShiftO - KeyShiftP - KeyShiftQ - KeyShiftR - KeyShiftS - KeyShiftT - KeyShiftU - KeyShiftV - KeyShiftW - KeyShiftX - KeyShiftY - KeyShiftZ -) - -// NumKeys tracks number keys. -var NumKeys = map[int]int32{ - 0: Key0, - 1: Key1, - 2: Key2, - 3: Key3, - 4: Key4, - 5: Key5, - 6: Key6, - 7: Key7, - 8: Key8, - 9: Key9, -} - -func initKeys() { - tcell.KeyNames[tcell.Key(KeyHelp)] = "?" - tcell.KeyNames[tcell.Key(KeySlash)] = "/" - tcell.KeyNames[tcell.Key(KeySpace)] = "space" - - initNumbKeys() - initStdKeys() - initShiftKeys() -} - -func initNumbKeys() { - tcell.KeyNames[tcell.Key(Key0)] = "0" - tcell.KeyNames[tcell.Key(Key1)] = "1" - tcell.KeyNames[tcell.Key(Key2)] = "2" - tcell.KeyNames[tcell.Key(Key3)] = "3" - tcell.KeyNames[tcell.Key(Key4)] = "4" - tcell.KeyNames[tcell.Key(Key5)] = "5" - tcell.KeyNames[tcell.Key(Key6)] = "6" - tcell.KeyNames[tcell.Key(Key7)] = "7" - tcell.KeyNames[tcell.Key(Key8)] = "8" - tcell.KeyNames[tcell.Key(Key9)] = "9" -} - -func initStdKeys() { - tcell.KeyNames[tcell.Key(KeyA)] = "a" - tcell.KeyNames[tcell.Key(KeyB)] = "b" - tcell.KeyNames[tcell.Key(KeyC)] = "c" - tcell.KeyNames[tcell.Key(KeyD)] = "d" - tcell.KeyNames[tcell.Key(KeyE)] = "e" - tcell.KeyNames[tcell.Key(KeyF)] = "f" - tcell.KeyNames[tcell.Key(KeyG)] = "g" - tcell.KeyNames[tcell.Key(KeyH)] = "h" - tcell.KeyNames[tcell.Key(KeyI)] = "i" - tcell.KeyNames[tcell.Key(KeyJ)] = "j" - tcell.KeyNames[tcell.Key(KeyK)] = "k" - tcell.KeyNames[tcell.Key(KeyL)] = "l" - tcell.KeyNames[tcell.Key(KeyM)] = "m" - tcell.KeyNames[tcell.Key(KeyN)] = "n" - tcell.KeyNames[tcell.Key(KeyO)] = "o" - tcell.KeyNames[tcell.Key(KeyP)] = "p" - tcell.KeyNames[tcell.Key(KeyQ)] = "q" - tcell.KeyNames[tcell.Key(KeyR)] = "r" - tcell.KeyNames[tcell.Key(KeyS)] = "s" - tcell.KeyNames[tcell.Key(KeyT)] = "t" - tcell.KeyNames[tcell.Key(KeyU)] = "u" - tcell.KeyNames[tcell.Key(KeyV)] = "v" - tcell.KeyNames[tcell.Key(KeyW)] = "w" - tcell.KeyNames[tcell.Key(KeyX)] = "x" - tcell.KeyNames[tcell.Key(KeyY)] = "y" - tcell.KeyNames[tcell.Key(KeyZ)] = "z" -} - -// BOZO!! No sure why these aren't mapped?? -func initCtrlKeys() { - tcell.KeyNames[tcell.KeyCtrlI] = "Ctrl-I" - tcell.KeyNames[tcell.KeyCtrlM] = "Ctrl-M" -} - -func initShiftKeys() { - tcell.KeyNames[tcell.Key(KeyShiftA)] = "Shift-A" - tcell.KeyNames[tcell.Key(KeyShiftB)] = "Shift-B" - tcell.KeyNames[tcell.Key(KeyShiftC)] = "Shift-C" - tcell.KeyNames[tcell.Key(KeyShiftD)] = "Shift-D" - tcell.KeyNames[tcell.Key(KeyShiftE)] = "Shift-E" - tcell.KeyNames[tcell.Key(KeyShiftF)] = "Shift-F" - tcell.KeyNames[tcell.Key(KeyShiftG)] = "Shift-G" - tcell.KeyNames[tcell.Key(KeyShiftH)] = "Shift-H" - tcell.KeyNames[tcell.Key(KeyShiftI)] = "Shift-I" - tcell.KeyNames[tcell.Key(KeyShiftJ)] = "Shift-J" - tcell.KeyNames[tcell.Key(KeyShiftK)] = "Shift-K" - tcell.KeyNames[tcell.Key(KeyShiftL)] = "Shift-L" - tcell.KeyNames[tcell.Key(KeyShiftM)] = "Shift-M" - tcell.KeyNames[tcell.Key(KeyShiftN)] = "Shift-N" - tcell.KeyNames[tcell.Key(KeyShiftO)] = "Shift-O" - tcell.KeyNames[tcell.Key(KeyShiftP)] = "Shift-P" - tcell.KeyNames[tcell.Key(KeyShiftQ)] = "Shift-Q" - tcell.KeyNames[tcell.Key(KeyShiftR)] = "Shift-R" - tcell.KeyNames[tcell.Key(KeyShiftS)] = "Shift-S" - tcell.KeyNames[tcell.Key(KeyShiftT)] = "Shift-T" - tcell.KeyNames[tcell.Key(KeyShiftU)] = "Shift-U" - tcell.KeyNames[tcell.Key(KeyShiftV)] = "Shift-V" - tcell.KeyNames[tcell.Key(KeyShiftW)] = "Shift-W" - tcell.KeyNames[tcell.Key(KeyShiftX)] = "Shift-X" - tcell.KeyNames[tcell.Key(KeyShiftY)] = "Shift-Y" - tcell.KeyNames[tcell.Key(KeyShiftZ)] = "Shift-Z" -} diff --git a/internal/ui/menu_test.go b/internal/ui/menu_test.go index 1be4b70f..ead592ca 100644 --- a/internal/ui/menu_test.go +++ b/internal/ui/menu_test.go @@ -1,20 +1,21 @@ -package ui +package ui_test import ( "testing" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/ui" "github.com/gdamore/tcell" "github.com/stretchr/testify/assert" ) -func TestNewMenuView(t *testing.T) { - defaults, _ := config.NewStyles("") - v := NewMenuView(defaults) - v.HydrateMenu(Hints{ - {"0", "zero", true}, - {"a", "bleeA", true}, - {"b", "bleeB", true}, +func TestNewMenu(t *testing.T) { + v := ui.NewMenu(config.NewStyles()) + v.HydrateMenu(model.MenuHints{ + {Mnemonic: "a", Description: "bleeA", Visible: true}, + {Mnemonic: "b", Description: "bleeB", Visible: true}, + {Mnemonic: "0", Description: "zero", Visible: true}, }) assert.Equal(t, " [fuchsia:black:b]<0> [white:black:d]zero ", v.GetCell(0, 0).Text) @@ -22,28 +23,29 @@ func TestNewMenuView(t *testing.T) { assert.Equal(t, " [dodgerblue:black:b] [white:black:d]bleeB ", v.GetCell(1, 1).Text) } -func TestKeyActions(t *testing.T) { +func TestActionHints(t *testing.T) { uu := map[string]struct { - aa KeyActions - e Hints + aa ui.KeyActions + e model.MenuHints }{ "a": { - aa: KeyActions{ - KeyB: NewKeyAction("bleeB", nil, true), - KeyA: NewKeyAction("bleeA", nil, true), - tcell.Key(Key0): NewKeyAction("zero", nil, true), - tcell.Key(Key1): NewKeyAction("one", nil, false), + aa: ui.KeyActions{ + ui.KeyB: ui.NewKeyAction("bleeB", nil, true), + ui.KeyA: ui.NewKeyAction("bleeA", nil, true), + tcell.Key(ui.Key0): ui.NewKeyAction("zero", nil, true), + tcell.Key(ui.Key1): ui.NewKeyAction("one", nil, false), }, - e: Hints{ - {"0", "zero", true}, - {"1", "one", false}, - {"a", "bleeA", true}, - {"b", "bleeB", true}, + e: model.MenuHints{ + {Mnemonic: "0", Description: "zero", Visible: true}, + {Mnemonic: "1", Description: "one", Visible: false}, + {Mnemonic: "a", Description: "bleeA", Visible: true}, + {Mnemonic: "b", Description: "bleeB", Visible: true}, }, }, } - for k, u := range uu { + for k := range uu { + u := uu[k] t.Run(k, func(t *testing.T) { assert.Equal(t, u.e, u.aa.Hints()) }) diff --git a/internal/ui/padding.go b/internal/ui/padding.go index 4a0c8e5a..a19e3b7c 100644 --- a/internal/ui/padding.go +++ b/internal/ui/padding.go @@ -5,7 +5,7 @@ import ( "time" "unicode" - "github.com/derailed/k9s/internal/resource" + "github.com/derailed/k9s/internal/render" "k8s.io/apimachinery/pkg/util/duration" ) @@ -13,29 +13,24 @@ import ( type MaxyPad []int // ComputeMaxColumns figures out column max size and necessary padding. -func ComputeMaxColumns(pads MaxyPad, sortCol int, table resource.TableData) { +func ComputeMaxColumns(pads MaxyPad, sortCol int, header render.HeaderRow, ee render.RowEvents) { const colPadding = 1 - for index, h := range table.Header { - pads[index] = len(h) + for index, h := range header { + pads[index] = len(h.Name) if index == sortCol { - pads[index] = len(h) + 2 + pads[index] = len(h.Name) + 2 } } var row int - for _, rev := range table.Rows { - ageIndex := len(rev.Fields) - 1 - for index, field := range rev.Fields { - // Date field comes out as timestamp. - if index == ageIndex { - dur, err := time.ParseDuration(field) - if err == nil { - field = duration.HumanDuration(dur) - } + for _, e := range ee { + for index, field := range e.Row.Fields { + if header.AgeCol(index) { + field = toAgeHuman(field) } width := len(field) + colPadding - if width > pads[index] { + if index < len(pads) && width > pads[index] { pads[index] = width } } @@ -60,8 +55,17 @@ func Pad(s string, width int) string { } if len(s) > width { - return resource.Truncate(s, width) + return render.Truncate(s, width) } return s + strings.Repeat(" ", width-len(s)) } + +func toAgeHuman(s string) string { + d, err := time.ParseDuration(s) + if err != nil { + return "n/a" + } + + return duration.HumanDuration(d) +} diff --git a/internal/ui/padding_test.go b/internal/ui/padding_test.go index d9ea5f35..526ce0d8 100644 --- a/internal/ui/padding_test.go +++ b/internal/ui/padding_test.go @@ -3,44 +3,68 @@ package ui import ( "testing" - "github.com/derailed/k9s/internal/resource" + "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestMaxColumn(t *testing.T) { - uu := []struct { - t resource.TableData + uu := map[string]struct { + t render.TableData s int e MaxyPad }{ - { - resource.TableData{ - Header: resource.Row{"A", "B"}, - Rows: resource.RowEvents{ - "r1": &resource.RowEvent{Fields: resource.Row{"hello", "world"}}, - "r2": &resource.RowEvent{Fields: resource.Row{"yo", "mama"}}, + "ascii col 0": { + render.TableData{ + Header: render.HeaderRow{render.Header{Name: "A"}, render.Header{Name: "B"}}, + RowEvents: render.RowEvents{ + render.RowEvent{ + Row: render.Row{ + Fields: render.Fields{"hello", "world"}, + }, + }, + render.RowEvent{ + Row: render.Row{ + Fields: render.Fields{"yo", "mama"}, + }, + }, }, }, 0, MaxyPad{6, 6}, }, - { - resource.TableData{ - Header: resource.Row{"A", "B"}, - Rows: resource.RowEvents{ - "r1": &resource.RowEvent{Fields: resource.Row{"hello", "world"}}, - "r2": &resource.RowEvent{Fields: resource.Row{"yo", "mama"}}, + "ascii col 1": { + render.TableData{ + Header: render.HeaderRow{render.Header{Name: "A"}, render.Header{Name: "B"}}, + RowEvents: render.RowEvents{ + render.RowEvent{ + Row: render.Row{ + Fields: render.Fields{"hello", "world"}, + }, + }, + render.RowEvent{ + Row: render.Row{ + Fields: render.Fields{"yo", "mama"}, + }, + }, }, }, 1, MaxyPad{6, 6}, }, - { - resource.TableData{ - Header: resource.Row{"A", "B"}, - Rows: resource.RowEvents{ - "r1": &resource.RowEvent{Fields: resource.Row{"Hello World lord of ipsums 😅", "world"}}, - "r2": &resource.RowEvent{Fields: resource.Row{"o", "mama"}}, + "non_ascii": { + render.TableData{ + Header: render.HeaderRow{render.Header{Name: "A"}, render.Header{Name: "B"}}, + RowEvents: render.RowEvents{ + render.RowEvent{ + Row: render.Row{ + Fields: render.Fields{"Hello World lord of ipsums 😅", "world"}, + }, + }, + render.RowEvent{ + Row: render.Row{ + Fields: render.Fields{"o", "mama"}, + }, + }, }, }, 0, @@ -50,7 +74,7 @@ func TestMaxColumn(t *testing.T) { for _, u := range uu { pads := make(MaxyPad, len(u.t.Header)) - ComputeMaxColumns(pads, u.s, u.t) + ComputeMaxColumns(pads, u.s, u.t.Header, u.t.RowEvents) assert.Equal(t, u.e, pads) } } @@ -89,11 +113,19 @@ func TestPad(t *testing.T) { } func BenchmarkMaxColumn(b *testing.B) { - table := resource.TableData{ - Header: resource.Row{"A", "B"}, - Rows: resource.RowEvents{ - "r1": &resource.RowEvent{Fields: resource.Row{"hello", "world"}}, - "r2": &resource.RowEvent{Fields: resource.Row{"yo", "mama"}}, + table := render.TableData{ + Header: render.HeaderRow{render.Header{Name: "A"}, render.Header{Name: "B"}}, + RowEvents: render.RowEvents{ + render.RowEvent{ + Row: render.Row{ + Fields: render.Fields{"hello", "world"}, + }, + }, + render.RowEvent{ + Row: render.Row{ + Fields: render.Fields{"yo", "mama"}, + }, + }, }, } @@ -102,6 +134,6 @@ func BenchmarkMaxColumn(b *testing.B) { b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - ComputeMaxColumns(pads, 0, table) + ComputeMaxColumns(pads, 0, table.Header, table.RowEvents) } } diff --git a/internal/ui/pages.go b/internal/ui/pages.go new file mode 100644 index 00000000..6b0a4283 --- /dev/null +++ b/internal/ui/pages.go @@ -0,0 +1,86 @@ +package ui + +import ( + "fmt" + + "github.com/derailed/k9s/internal/model" + "github.com/derailed/tview" + "github.com/rs/zerolog/log" +) + +type Pages struct { + *tview.Pages + *model.Stack +} + +func NewPages() *Pages { + p := Pages{ + Pages: tview.NewPages(), + Stack: model.NewStack(), + } + p.Stack.AddListener(&p) + + return &p +} + +func (p *Pages) Show(c model.Component) { + p.SwitchToPage(componentID(c)) +} + +func (p *Pages) Current() model.Component { + c := p.CurrentPage() + if c == nil { + return nil + } + + return c.Item.(model.Component) +} + +// AddAndShow adds a new page and bring it to front. +func (p *Pages) addAndShow(c model.Component) { + p.add(c) + p.Show(c) +} + +// Add adds a new page. +func (p *Pages) add(c model.Component) { + p.AddPage(componentID(c), c, true, true) +} + +// Delete removes a page. +func (p *Pages) delete(c model.Component) { + p.RemovePage(componentID(c)) +} + +func (p *Pages) DumpPages() { + log.Debug().Msgf("Dumping Pages %p", p) + for i, c := range p.Stack.Peek() { + log.Debug().Msgf("%d -- %s -- %#v", i, componentID(c), p.GetPrimitive(componentID(c))) + } +} + +// Stack Protocol... + +func (p *Pages) StackPushed(c model.Component) { + p.addAndShow(c) +} + +func (p *Pages) StackPopped(o, top model.Component) { + p.delete(o) +} + +func (p *Pages) StackTop(top model.Component) { + if top == nil { + return + } + p.Show(top) +} + +// Helpers... + +func componentID(c model.Component) string { + if c.Name() == "" { + panic("Component has no name") + } + return fmt.Sprintf("%s-%p", c.Name(), c) +} diff --git a/internal/ui/pages_test.go b/internal/ui/pages_test.go new file mode 100644 index 00000000..f6e447e4 --- /dev/null +++ b/internal/ui/pages_test.go @@ -0,0 +1,31 @@ +package ui_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/ui" + "github.com/stretchr/testify/assert" +) + +func TestPagesPush(t *testing.T) { + c1, c2 := makeComponent("c1"), makeComponent("c2") + + p := ui.NewPages() + p.Push(c1) + p.Push(c2) + + assert.Equal(t, 2, p.GetPageCount()) + assert.Equal(t, c2, p.CurrentPage().Item) +} + +func TestPagesPop(t *testing.T) { + c1, c2 := makeComponent("c1"), makeComponent("c2") + + p := ui.NewPages() + p.Push(c1) + p.Push(c2) + p.Pop() + + assert.Equal(t, 1, p.GetPageCount()) + assert.Equal(t, c1, p.CurrentPage().Item) +} diff --git a/internal/ui/select_table.go b/internal/ui/select_table.go new file mode 100644 index 00000000..b13e971c --- /dev/null +++ b/internal/ui/select_table.go @@ -0,0 +1,194 @@ +package ui + +import ( + "context" + "time" + + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/tview" + "github.com/gdamore/tcell" +) + +// Namespaceable represents a namespaceable model. +type Namespaceable interface { + // ClusterWide returns true if the model represents resource in all namespaces. + ClusterWide() bool + + // GetNamespace returns the model namespace. + GetNamespace() string + + // SetNamespace changes the model namespace. + SetNamespace(string) + + // InNamespace check if current namespace matches models. + InNamespace(string) bool +} + +// Tabular represents a tabular model. +type Tabular interface { + Namespaceable + + // Empty returns true if model has no data. + Empty() bool + + // Peek returns current model data. + Peek() render.TableData + + // Watch watches a given resource for changes. + Watch(context.Context) + + // SetRefreshRate sets the model watch loop rate. + SetRefreshRate(time.Duration) + + // AddListener registers a model listener. + AddListener(model.TableListener) +} + +// Selectable represents a table with selections. +type SelectTable struct { + *tview.Table + + model Tabular + selectedRow int + selectedFn func(string) string + selectionListeners []SelectedRowFunc + marks map[string]bool +} + +// SetModel sets the table model. +func (s *SelectTable) SetModel(m Tabular) { + s.model = m +} + +// GetModel returns the current model. +func (s *SelectTable) GetModel() Tabular { + return s.model +} + +// ClearSelection reset selected row. +func (s *SelectTable) ClearSelection() { + s.Select(0, 0) + s.ScrollToBeginning() +} + +// SelectFirstRow select first data row if any. +func (s *SelectTable) SelectFirstRow() { + if s.GetRowCount() > 0 { + s.Select(1, 0) + } +} + +// GetSelectedItems return currently marked or selected items names. +func (s *SelectTable) GetSelectedItems() []string { + if len(s.marks) == 0 { + return []string{s.GetSelectedItem()} + } + + var items []string + for item, marked := range s.marks { + if marked { + items = append(items, item) + } + } + + return items +} + +// GetSelectedItem returns the currently selected item name. +func (s *SelectTable) GetSelectedItem() string { + if s.GetSelectedRowIndex() == 0 || s.model.Empty() { + return "" + } + sel, ok := s.GetCell(s.GetSelectedRowIndex(), 0).GetReference().(string) + if !ok { + return "" + } + if s.selectedFn != nil { + return s.selectedFn(sel) + } + return sel +} + +// GetSelectedCell returns the content of a cell for the currently selected row. +func (s *SelectTable) GetSelectedCell(col int) string { + return TrimCell(s, s.selectedRow, col) +} + +// SetSelectedFn defines a function that cleanse the current selection. +func (s *SelectTable) SetSelectedFn(f func(string) string) { + s.selectedFn = f +} + +// GetSelectedRow fetch the currently selected row index. +func (s *SelectTable) GetSelectedRowIndex() int { + return s.selectedRow +} + +// SelectRow select a given row by index. +func (s *SelectTable) SelectRow(r int, broadcast bool) { + if !broadcast { + s.SetSelectionChangedFunc(nil) + } + defer s.SetSelectionChangedFunc(s.selectionChanged) + s.Select(r, 0) +} + +// UpdateSelection refresh selected row. +func (s *SelectTable) updateSelection(broadcast bool) { + s.SelectRow(s.selectedRow, broadcast) +} + +func (s *SelectTable) selectionChanged(r, c int) { + s.selectedRow = r + if r == 0 { + return + } + + if s.marks[s.GetSelectedItem()] { + s.SetSelectedStyle(tcell.ColorBlack, tcell.ColorCadetBlue, tcell.AttrBold) + } else { + cell := s.GetCell(r, c) + s.SetSelectedStyle(tcell.ColorBlack, cell.Color, tcell.AttrBold) + } + + for _, f := range s.selectionListeners { + f(r) + } +} + +// ClearMarks delete all marked items. +func (s *SelectTable) ClearMarks() { + for k := range s.marks { + delete(s.marks, k) + } +} + +// DeleteMark delete a marked item. +func (s *SelectTable) DeleteMark(k string) { + delete(s.marks, k) +} + +// ToggleMark toggles marked row +func (s *SelectTable) ToggleMark() { + s.marks[s.GetSelectedItem()] = !s.marks[s.GetSelectedItem()] + if !s.marks[s.GetSelectedItem()] { + return + } + + cell := s.GetCell(s.GetSelectedRowIndex(), 0) + s.SetSelectedStyle( + tcell.ColorBlack, + cell.Color, + tcell.AttrBold, + ) +} + +func (s *Table) IsMarked(item string) bool { + return s.marks[item] +} + +// AddSelectedRowListener add a new selected row listener. +func (s *SelectTable) AddSelectedRowListener(f SelectedRowFunc) { + s.selectionListeners = append(s.selectionListeners, f) +} diff --git a/internal/ui/sorter.go b/internal/ui/sorter.go deleted file mode 100644 index b2143f79..00000000 --- a/internal/ui/sorter.go +++ /dev/null @@ -1,138 +0,0 @@ -package ui - -import ( - "strconv" - "time" - - "github.com/derailed/k9s/internal/resource" - res "k8s.io/apimachinery/pkg/api/resource" - "vbom.ml/util/sortorder" -) - -type ( - // SortFn represent a function that can sort columnar data. - SortFn func(rows resource.Rows, sortCol SortColumn) - - // SortColumn represents a sortable column. - SortColumn struct { - index int - colCount int - asc bool - } - - // RowSorter sorts rows. - RowSorter struct { - rows resource.Rows - index int - asc bool - } -) - -func (s RowSorter) Len() int { - return len(s.rows) -} - -func (s RowSorter) Swap(i, j int) { - s.rows[i], s.rows[j] = s.rows[j], s.rows[i] -} - -func (s RowSorter) Less(i, j int) bool { - return less(s.asc, s.rows[i][s.index], s.rows[j][s.index]) -} - -// ---------------------------------------------------------------------------- - -// GroupSorter sorts a collection of rows. -type GroupSorter struct { - rows []string - asc bool -} - -func (s GroupSorter) Len() int { - return len(s.rows) -} - -func (s GroupSorter) Swap(i, j int) { - s.rows[i], s.rows[j] = s.rows[j], s.rows[i] -} - -func (s GroupSorter) Less(i, j int) bool { - return less(s.asc, s.rows[i], s.rows[j]) -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func less(asc bool, c1, c2 string) bool { - if c1 == resource.NAValue && c2 != resource.NAValue { - return false - } - if c1 != resource.NAValue && c2 == resource.NAValue { - return true - } - - if o, ok := isIntegerSort(asc, c1, c2); ok { - return o - } - - if o, ok := isMetricSort(asc, c1, c2); ok { - return o - } - - if o, ok := isDurationSort(asc, c1, c2); ok { - return o - } - - b := sortorder.NaturalLess(c1, c2) - if asc { - return b - } - return !b -} - -func isDurationSort(asc bool, s1, s2 string) (bool, bool) { - d1, ok1 := isDuration(s1) - d2, ok2 := isDuration(s2) - if !ok1 || !ok2 { - return false, false - } - - if asc { - return d1 <= d2, true - } - return d1 >= d2, true -} - -func isMetricSort(asc bool, c1, c2 string) (bool, bool) { - q1, err1 := res.ParseQuantity(c1) - q2, err2 := res.ParseQuantity(c2) - if err1 != nil || err2 != nil { - return false, false - } - - if asc { - return q1.Cmp(q2) <= 0, true - } - return q1.Cmp(q2) > 0, true -} - -func isIntegerSort(asc bool, c1, c2 string) (bool, bool) { - n1, err1 := strconv.Atoi(c1) - n2, err2 := strconv.Atoi(c2) - if err1 != nil || err2 != nil { - return false, false - } - - if asc { - return n1 <= n2, true - } - return n1 > n2, true -} - -func isDuration(s string) (time.Duration, bool) { - d, err := time.ParseDuration(s) - if err != nil { - return d, false - } - return d, true -} diff --git a/internal/ui/sorter_test.go b/internal/ui/sorter_test.go deleted file mode 100644 index 9016ce8a..00000000 --- a/internal/ui/sorter_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package ui - -import ( - "sort" - "testing" - - "github.com/derailed/k9s/internal/resource" - "github.com/stretchr/testify/assert" -) - -func TestGroupSort(t *testing.T) { - uu := []struct { - asc bool - rows []string - expect []string - }{ - {true, []string{"200m", "100m"}, []string{"100m", "200m"}}, - {true, []string{"200m", "100m"}, []string{"100m", "200m"}}, - {false, []string{"200m", "100m"}, []string{"200m", "100m"}}, - {true, []string{"10", "1"}, []string{"1", "10"}}, - {false, []string{"10", "1"}, []string{"10", "1"}}, - {true, []string{"100Mi", "10Mi"}, []string{"10Mi", "100Mi"}}, - {false, []string{"100Mi", "10Mi"}, []string{"100Mi", "10Mi"}}, - {true, []string{"xyz", "abc"}, []string{"abc", "xyz"}}, - {false, []string{"xyz", "abc"}, []string{"xyz", "abc"}}, - {true, []string{"2m30s", "1m10s"}, []string{"1m10s", "2m30s"}}, - {true, []string{"3d", "1d"}, []string{"1d", "3d"}}, - - {true, []string{"95h", "93h"}, []string{"93h", "95h"}}, - {true, []string{"95d", "93d"}, []string{"93d", "95d"}}, - {true, []string{"1h10m", "59m"}, []string{"59m", "1h10m"}}, - {true, []string{"95m", "1h30m"}, []string{"1h30m", "95m"}}, - {true, []string{"b-21", "b-2"}, []string{"b-2", "b-21"}}, - {false, []string{"b-21", "b-2"}, []string{"b-21", "b-2"}}, - {true, []string{"4m", "3m2s"}, []string{"3m2s", "4m"}}, - {true, []string{"3y37d", "2y4d"}, []string{"2y4d", "3y37d"}}, - } - - for _, u := range uu { - g := GroupSorter{rows: u.rows, asc: u.asc} - sort.Sort(g) - assert.Equal(t, u.expect, g.rows) - } -} - -func TestRowSort(t *testing.T) { - uu := []struct { - asc bool - rows, expect resource.Rows - }{ - { - true, - resource.Rows{resource.Row{"200m"}, resource.Row{"100m"}}, - resource.Rows{resource.Row{"100m"}, resource.Row{"200m"}}, - }, - { - false, - resource.Rows{resource.Row{"200m"}, resource.Row{"100m"}}, - resource.Rows{resource.Row{"200m"}, resource.Row{"100m"}}, - }, - { - true, - resource.Rows{resource.Row{"200Mi"}, resource.Row{"100Mi"}}, - resource.Rows{resource.Row{"100Mi"}, resource.Row{"200Mi"}}, - }, - { - false, - resource.Rows{resource.Row{"200Mi"}, resource.Row{"100Mi"}}, - resource.Rows{resource.Row{"200Mi"}, resource.Row{"100Mi"}}, - }, - { - true, - resource.Rows{resource.Row{"8m4s"}, resource.Row{"31m"}}, - resource.Rows{resource.Row{"8m4s"}, resource.Row{"31m"}}, - }, - { - true, - resource.Rows{resource.Row{"n/a"}, resource.Row{"31m"}}, - resource.Rows{resource.Row{"31m"}, resource.Row{"n/a"}}, - }, - { - true, - resource.Rows{resource.Row{"31m"}, resource.Row{"n/a"}}, - resource.Rows{resource.Row{"31m"}, resource.Row{"n/a"}}, - }, - { - false, - resource.Rows{resource.Row{"n/a"}, resource.Row{"31m"}}, - resource.Rows{resource.Row{"31m"}, resource.Row{"n/a"}}, - }, - { - false, - resource.Rows{resource.Row{"31m"}, resource.Row{"n/a"}}, - resource.Rows{resource.Row{"31m"}, resource.Row{"n/a"}}, - }, - } - - for _, u := range uu { - r := RowSorter{index: 0, rows: u.rows, asc: u.asc} - sort.Sort(r) - assert.Equal(t, u.expect, r.rows) - } -} - -func TestIsDurationSort(t *testing.T) { - uu := map[string]struct { - s1, s2 string - asc, e bool - }{ - "ascLess": {"10h5m", "2h10m", true, false}, - "descGreater": {"10h5m", "2h10m", false, true}, - "ascEqual": {"2h10m", "2h10m", true, true}, - "descEqual": {"2h10m", "2h10m", false, true}, - "ascGreater": {"10h10m", "2h5m", true, false}, - } - - for k, u := range uu { - t.Run(k, func(t *testing.T) { - less, ok := isDurationSort(u.asc, u.s1, u.s2) - assert.True(t, ok) - assert.Equal(t, u.e, less) - }) - } -} diff --git a/internal/ui/splash.go b/internal/ui/splash.go index 55321035..724cd079 100644 --- a/internal/ui/splash.go +++ b/internal/ui/splash.go @@ -9,11 +9,6 @@ import ( "github.com/gdamore/tcell" ) -const ( - company = "imhotep.io" - product = "Kubernetes CLI Island Style!" -) - // LogoSmall K9s small log. var LogoSmall = []string{ ` ____ __.________ `, @@ -25,7 +20,7 @@ var LogoSmall = []string{ } // Logo K9s big logo for splash page. -var Logo = []string{ +var LogoBig = []string{ ` ____ __.________ _________ .____ .___ `, `| |/ _/ __ \_____\_ ___ \| | | |`, `| < \____ / ___/ \ \/| | | |`, @@ -34,42 +29,42 @@ var Logo = []string{ ` \/ \/ \/ \/ `, } -// SplashView represents a splash screen. -type SplashView struct { +// Splash represents a splash screen. +type Splash struct { *tview.Flex } // NewSplash instantiates a new splash screen with product and company info. -func NewSplash(styles *config.Styles, version string) *SplashView { - v := SplashView{Flex: tview.NewFlex()} +func NewSplash(styles *config.Styles, version string) *Splash { + s := Splash{Flex: tview.NewFlex()} logo := tview.NewTextView() logo.SetDynamicColors(true) logo.SetBackgroundColor(tcell.ColorDefault) logo.SetTextAlign(tview.AlignCenter) - v.layoutLogo(logo, styles) + s.layoutLogo(logo, styles) vers := tview.NewTextView() vers.SetDynamicColors(true) vers.SetBackgroundColor(tcell.ColorDefault) vers.SetTextAlign(tview.AlignCenter) - v.layoutRev(vers, version, styles) + s.layoutRev(vers, version, styles) - v.SetDirection(tview.FlexRow) - v.AddItem(logo, 10, 1, false) - v.AddItem(vers, 1, 1, false) + s.SetDirection(tview.FlexRow) + s.AddItem(logo, 10, 1, false) + s.AddItem(vers, 1, 1, false) - return &v + return &s } -func (v *SplashView) layoutLogo(t *tview.TextView, styles *config.Styles) { - logo := strings.Join(Logo, fmt.Sprintf("\n[%s::b]", styles.Body().LogoColor)) +func (s *Splash) layoutLogo(t *tview.TextView, styles *config.Styles) { + logo := strings.Join(LogoBig, fmt.Sprintf("\n[%s::b]", styles.Body().LogoColor)) fmt.Fprintf(t, "%s[%s::b]%s\n", strings.Repeat("\n", 2), styles.Body().LogoColor, logo) } -func (v *SplashView) layoutRev(t *tview.TextView, rev string, styles *config.Styles) { +func (s *Splash) layoutRev(t *tview.TextView, rev string, styles *config.Styles) { fmt.Fprintf(t, "[%s::b]Revision [red::b]%s", styles.Body().FgColor, rev) } diff --git a/internal/ui/splash_test.go b/internal/ui/splash_test.go index f297ece3..2113819c 100644 --- a/internal/ui/splash_test.go +++ b/internal/ui/splash_test.go @@ -1,15 +1,15 @@ -package ui +package ui_test import ( "testing" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/ui" "github.com/stretchr/testify/assert" ) func TestNewSplash(t *testing.T) { - defaults, _ := config.NewStyles("") - s := NewSplash(defaults, "bozo") + s := ui.NewSplash(config.NewStyles(), "bozo") x, y, w, h := s.GetRect() assert.Equal(t, 0, x) diff --git a/internal/ui/table.go b/internal/ui/table.go index b1bab827..35116ecd 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -1,558 +1,334 @@ package ui import ( + "context" "errors" - "fmt" - "path" - "regexp" - "strings" - "time" "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/resource" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/render" "github.com/derailed/tview" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" - "github.com/sahilm/fuzzy" - "k8s.io/apimachinery/pkg/util/duration" ) type ( // ColorerFunc represents a row colorer. - ColorerFunc func(ns string, evt *resource.RowEvent) tcell.Color + ColorerFunc func(ns string, evt render.RowEvent) tcell.Color + + // DecorateFunc represents a row decorator. + DecorateFunc func(render.TableData) render.TableData // SelectedRowFunc a table selection callback. - SelectedRowFunc func(r, c int) + SelectedRowFunc func(r int) ) // Table represents tabular data. type Table struct { - *tview.Table + *SelectTable - baseTitle string - data resource.TableData - actions KeyActions - cmdBuff *CmdBuff - styles *config.Styles - activeNS string - sortCol SortColumn - sortFn SortFn - colorerFn ColorerFunc - selectedItem string - selectedRow int - selectedFn func(string) string - selListeners []SelectedRowFunc - marks map[string]bool + actions KeyActions + BaseTitle string + Path string + cmdBuff *CmdBuff + styles *config.Styles + sortCol SortColumn + colorerFn render.ColorerFunc + decorateFn DecorateFunc } // NewTable returns a new table view. -func NewTable(title string, styles *config.Styles) *Table { - v := Table{ - Table: tview.NewTable(), - styles: styles, +func NewTable(gvr string) *Table { + return &Table{ + SelectTable: &SelectTable{ + Table: tview.NewTable(), + model: model.NewTable(gvr), + marks: make(map[string]bool), + }, actions: make(KeyActions), cmdBuff: NewCmdBuff('/', FilterBuff), - baseTitle: title, - sortCol: SortColumn{0, 0, true}, - marks: make(map[string]bool), + BaseTitle: gvr, + sortCol: SortColumn{index: -1, colCount: 0, asc: true}, } +} - v.SetFixed(1, 0) - v.SetBorder(true) - v.SetBackgroundColor(config.AsColor(styles.Table().BgColor)) - v.SetBorderColor(config.AsColor(styles.Table().FgColor)) - v.SetBorderFocusColor(config.AsColor(styles.Frame().Border.FocusColor)) - v.SetBorderAttributes(tcell.AttrBold) - v.SetBorderPadding(0, 0, 1, 1) - v.SetSelectable(true, false) - v.SetSelectedStyle( +func (t *Table) Init(ctx context.Context) { + t.styles = mustExtractSyles(ctx) + + t.SetFixed(1, 0) + t.SetBorder(true) + t.SetBackgroundColor(config.AsColor(t.styles.GetTable().BgColor)) + t.SetBorderColor(config.AsColor(t.styles.GetTable().FgColor)) + t.SetBorderFocusColor(config.AsColor(t.styles.Frame().Border.FocusColor)) + t.SetBorderAttributes(tcell.AttrBold) + t.SetBorderPadding(0, 0, 1, 1) + t.SetSelectable(true, false) + t.SetSelectedStyle( tcell.ColorBlack, - config.AsColor(styles.Table().CursorColor), + config.AsColor(t.styles.GetTable().CursorColor), tcell.AttrBold, ) - - v.SetSelectionChangedFunc(v.selChanged) - v.SetInputCapture(v.keyboard) - - return &v + t.SetSelectionChangedFunc(t.selectionChanged) + t.SetInputCapture(t.keyboard) } -// GetRow retrieves the entire selected row. -func (v *Table) GetRow() resource.Row { - r := make(resource.Row, v.GetColumnCount()) - for i := 0; i < v.GetColumnCount(); i++ { - c := v.GetCell(v.selectedRow, i) - r[i] = strings.TrimSpace(c.Text) - } - return r +// Actions returns active menu bindings. +func (t *Table) Actions() KeyActions { + return t.actions } -// AddSelectedRowListener add a new selected row listener. -func (v *Table) AddSelectedRowListener(f SelectedRowFunc) { - v.selListeners = append(v.selListeners, f) +// SendKey sends an keyboard event (testing only!). +func (t *Table) SendKey(evt *tcell.EventKey) { + t.keyboard(evt) } -func (v *Table) selChanged(r, c int) { - v.selectedRow = r - v.updateSelectedItem(r) - if r == 0 { - return - } - - cell := v.GetCell(r, c) - v.SetSelectedStyle( - tcell.ColorBlack, - cell.Color, - tcell.AttrBold, - ) - - for _, f := range v.selListeners { - f(r, c) - } -} - -// UpdateSelection refresh selected row. -func (v *Table) updateSelection(broadcast bool) { - v.SelectRow(v.selectedRow, broadcast) -} - -// SelectRow select a given row by index. -func (v *Table) SelectRow(r int, broadcast bool) { - if !broadcast { - v.SetSelectionChangedFunc(nil) - } - defer v.SetSelectionChangedFunc(v.selChanged) - v.Select(r, 0) - v.updateSelectedItem(r) -} - -func (v *Table) updateSelectedItem(r int) { - if r == 0 || v.GetCell(r, 0) == nil { - v.selectedItem = "" - return - } - - col0 := TrimCell(v, r, 0) - switch v.activeNS { - case resource.NotNamespaced: - v.selectedItem = col0 - case resource.AllNamespace, resource.AllNamespaces: - v.selectedItem = path.Join(col0, TrimCell(v, r, 1)) - default: - v.selectedItem = path.Join(v.activeNS, col0) - } -} - -// SetSelectedFn defines a function that cleanse the current selection. -func (v *Table) SetSelectedFn(f func(string) string) { - v.selectedFn = f -} - -// RowSelected checks if there is an active row selection. -func (v *Table) RowSelected() bool { - return v.selectedItem != "" -} - -// GetSelectedCell returns the contant of a cell for the currently selected row. -func (v *Table) GetSelectedCell(col int) string { - return TrimCell(v, v.selectedRow, col) -} - -// GetSelectedRow fetch the currently selected row index. -func (v *Table) GetSelectedRow() int { - return v.selectedRow -} - -// GetSelectedItem returns the currently selected item name. -func (v *Table) GetSelectedItem() string { - if v.selectedFn != nil { - return v.selectedFn(v.selectedItem) - } - return v.selectedItem -} - -// GetSelectedItems return currently marked or selected items names. -func (v *Table) GetSelectedItems() []string { - if len(v.marks) > 0 { - var items []string - for item, marked := range v.marks { - if marked { - items = append(items, item) - } - } - return items - } - return []string{v.GetSelectedItem()} -} - -func (v *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey { +func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey { key := evt.Key() + if key == tcell.KeyUp || key == tcell.KeyDown { + return evt + } + if key == tcell.KeyRune { - if v.SearchBuff().IsActive() { - v.SearchBuff().Add(evt.Rune()) - v.ClearSelection() - v.doUpdate(v.filtered()) - v.UpdateTitle() - v.SelectFirstRow() + if t.SearchBuff().IsActive() { + t.SearchBuff().Add(evt.Rune()) + t.ClearSelection() + data := t.GetModel().Peek() + t.doUpdate(t.filtered(data), len(data.RowEvents) > 0) + t.UpdateTitle() + t.SelectFirstRow() return nil } key = asKey(evt) } - if a, ok := v.actions[key]; ok { + if a, ok := t.actions[key]; ok { return a.Action(evt) } return evt } -// GetData fetch tabular data. -func (v *Table) GetData() resource.TableData { - return v.data +func (t *Table) Hints() model.MenuHints { + return t.actions.Hints() } // GetFilteredData fetch filtered tabular data. -func (v *Table) GetFilteredData() resource.TableData { - return v.filtered() +func (t *Table) GetFilteredData() render.TableData { + return t.filtered(t.GetModel().Peek()) } -// SetBaseTitle set the table title. -func (v *Table) SetBaseTitle(s string) { - v.baseTitle = s +// SetDecorateFn specifies the default row decorator. +func (t *Table) SetDecorateFn(f DecorateFunc) { + t.decorateFn = f } -// GetBaseTitle fetch the current title. -func (v *Table) GetBaseTitle() string { - return v.baseTitle -} - -// SetColorerFn set the row colorer. -func (v *Table) SetColorerFn(f ColorerFunc) { - v.colorerFn = f -} - -// ActiveNS get the resource namespace. -func (v *Table) ActiveNS() string { - return v.activeNS -} - -// SetActiveNS set the resource namespace. -func (v *Table) SetActiveNS(ns string) { - v.activeNS = ns +// SetColorerFn specifies the default colorer. +func (t *Table) SetColorerFn(f render.ColorerFunc) { + t.colorerFn = f } // SetSortCol sets in sort column index and order. -func (v *Table) SetSortCol(index, count int, asc bool) { - v.sortCol.index, v.sortCol.colCount, v.sortCol.asc = index, count, asc +func (t *Table) SetSortCol(index, count int, asc bool) { + t.sortCol.index, t.sortCol.colCount, t.sortCol.asc = index, count, asc } // Update table content. -func (v *Table) Update(data resource.TableData) { - v.data = data - if v.cmdBuff.Empty() { - v.doUpdate(v.data) - } else { - v.doUpdate(v.filtered()) +func (t *Table) Update(data render.TableData) { + data.Mutex.RLock() + defer data.Mutex.RUnlock() + + var firstRow bool + if t.GetRowCount() == 0 { + firstRow = true } - v.UpdateTitle() - v.updateSelection(true) + + if t.decorateFn != nil { + data = t.decorateFn(data) + } + if !t.cmdBuff.Empty() { + data = t.filtered(data) + } + t.doUpdate(data, firstRow) + t.UpdateTitle() } -func (v *Table) doUpdate(data resource.TableData) { - v.activeNS = data.Namespace - if v.activeNS == resource.AllNamespaces && v.activeNS != "*" { - v.actions[KeyShiftP] = NewKeyAction("Sort Namespace", v.SortColCmd(-2), false) +func (t *Table) doUpdate(data render.TableData, firstRow bool) { + if data.Namespace == render.AllNamespaces { + t.actions[KeyShiftP] = NewKeyAction("Sort Namespace", t.SortColCmd(-2, true), false) } else { - delete(v.actions, KeyShiftP) + t.actions.Delete(KeyShiftP) } - v.Clear() - v.adjustSorter(data) - - var row int - fg := config.AsColor(v.styles.Table().Header.FgColor) - bg := config.AsColor(v.styles.Table().Header.BgColor) + t.Clear() + t.adjustSorter(data) + fg := config.AsColor(t.styles.GetTable().Header.FgColor) + bg := config.AsColor(t.styles.GetTable().Header.BgColor) for col, h := range data.Header { - v.AddHeaderCell(data.NumCols[h], col, h) - c := v.GetCell(0, col) + t.AddHeaderCell(col, h) + c := t.GetCell(0, col) c.SetBackgroundColor(bg) c.SetTextColor(fg) } - row++ + data.RowEvents.Sort(data.Namespace, t.sortCol.index, t.sortCol.asc) - v.sort(data, row) + pads := make(MaxyPad, len(data.Header)) + ComputeMaxColumns(pads, t.sortCol.index, data.Header, data.RowEvents) + for i, r := range data.RowEvents { + t.buildRow(data.Namespace, i+1, r, data.Header, pads) + } + + if firstRow { + t.SelectFirstRow() + } + t.updateSelection(true) } // SortColCmd designates a sorted column. -func (v *Table) SortColCmd(col int) func(evt *tcell.EventKey) *tcell.EventKey { +func (t *Table) SortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { return func(evt *tcell.EventKey) *tcell.EventKey { - v.sortCol.asc = true + var index int switch col { case -2: - v.sortCol.index = 0 + index = 0 case -1: - v.sortCol.index = v.GetColumnCount() - 1 + index = t.GetColumnCount() - 1 default: - v.sortCol.index = v.NameColIndex() + col - + index = t.NameColIndex() + col } - v.Refresh() - + t.sortCol.asc = !t.sortCol.asc + if t.sortCol.index != index { + t.sortCol.asc = asc + } + t.sortCol.index = index + t.Refresh() return nil } } -func (v *Table) adjustSorter(data resource.TableData) { +// SortInvertCmd reverses sorting order. +func (t *Table) SortInvertCmd(evt *tcell.EventKey) *tcell.EventKey { + t.sortCol.asc = !t.sortCol.asc + t.Refresh() + + return nil +} + +func (t *Table) adjustSorter(data render.TableData) { // Going from namespace to non namespace or vice-versa? switch { - case v.sortCol.colCount == 0: - case len(data.Header) > v.sortCol.colCount: - v.sortCol.index++ - case len(data.Header) < v.sortCol.colCount: - v.sortCol.index-- + case t.sortCol.colCount == 0: + case len(data.Header) > t.sortCol.colCount: + t.sortCol.index++ + case len(data.Header) < t.sortCol.colCount: + t.sortCol.index-- } - v.sortCol.colCount = len(data.Header) - if v.sortCol.index < 0 { - v.sortCol.index = 0 + t.sortCol.colCount = len(data.Header) + if t.sortCol.index < 0 { + t.sortCol.index = 0 } } -func (v *Table) sort(data resource.TableData, row int) { - pads := make(MaxyPad, len(data.Header)) - ComputeMaxColumns(pads, v.sortCol.index, data) - - sortFn := defaultSort - if v.sortFn != nil { - sortFn = v.sortFn +func (t *Table) buildRow(ns string, r int, re render.RowEvent, header render.HeaderRow, pads MaxyPad) { + color := render.DefaultColorer + if t.colorerFn != nil { + color = t.colorerFn } - prim, sec := sortAllRows(v.sortCol, data.Rows, sortFn) - for _, pk := range prim { - for _, sk := range sec[pk] { - v.buildRow(row, data, sk, pads) - row++ + marked := t.IsMarked(re.Row.ID) + for col, field := range re.Row.Fields { + if !re.Deltas.IsBlank() && !header.AgeCol(col) { + field += Deltas(re.Deltas[col], field) } - } -} -func (v *Table) buildRow(row int, data resource.TableData, sk string, pads MaxyPad) { - f := DefaultColorer - if v.colorerFn != nil { - f = v.colorerFn - } - m := v.isMarked(sk) - for col, field := range data.Rows[sk].Fields { - header := data.Header[col] - field, align := v.formatCell(data.NumCols[header], header, field+Deltas(data.Rows[sk].Deltas[col], field), pads[col]) + if header[col].Decorator != nil { + field = header[col].Decorator(field) + } + + if header[col].Align == tview.AlignLeft { + field = formatCell(field, pads[col]) + } c := tview.NewTableCell(field) - { - c.SetExpansion(1) - c.SetAlign(align) - c.SetTextColor(f(data.Namespace, data.Rows[sk])) - if m { - c.SetBackgroundColor(config.AsColor(v.styles.Table().MarkColor)) - } + c.SetExpansion(1) + c.SetAlign(header[col].Align) + c.SetTextColor(color(ns, re)) + if marked { + log.Debug().Msgf("Marked!") + c.SetTextColor(config.AsColor(t.styles.GetTable().MarkColor)) } - v.SetCell(row, col, c) + if col == 0 { + c.SetReference(re.Row.ID) + } + t.SetCell(r, col, c) } } -func (v *Table) formatCell(numerical bool, header, field string, padding int) (string, int) { - if header == "AGE" { - dur, err := time.ParseDuration(field) - if err == nil { - field = duration.HumanDuration(dur) - } - } - - if numerical || cpuRX.MatchString(header) || memRX.MatchString(header) { - return field, tview.AlignRight - } - - align := tview.AlignLeft - if IsASCII(field) { - return Pad(field, padding), align - } - - return field, align +func (t *Table) ClearMarks() { + t.marks = map[string]bool{} + t.Refresh() } // Refresh update the table data. -func (v *Table) Refresh() { - v.Update(v.data) +func (t *Table) Refresh() { + t.Update(t.model.Peek()) +} + +func (t *Table) GetSelectedRow() render.Row { + return t.model.Peek().RowEvents[t.GetSelectedRowIndex()].Row } // NameColIndex returns the index of the resource name column. -func (v *Table) NameColIndex() int { +func (t *Table) NameColIndex() int { col := 0 - if v.activeNS == resource.AllNamespaces { + if t.GetModel().ClusterWide() { col++ } return col } // AddHeaderCell configures a table cell header. -func (v *Table) AddHeaderCell(numerical bool, col int, name string) { - c := tview.NewTableCell(sortIndicator(v.sortCol, v.styles.Table(), col, name)) +func (t *Table) AddHeaderCell(col int, h render.Header) { + c := tview.NewTableCell(sortIndicator(t.sortCol, t.styles.GetTable(), col, h.Name)) c.SetExpansion(1) - if numerical || cpuRX.MatchString(name) || memRX.MatchString(name) { - c.SetAlign(tview.AlignRight) - } - v.SetCell(0, col, c) + c.SetAlign(h.Align) + t.SetCell(0, col, c) } -func (v *Table) filtered() resource.TableData { - if v.cmdBuff.Empty() || isLabelSelector(v.cmdBuff.String()) { - return v.data +func (t *Table) filtered(data render.TableData) render.TableData { + if t.cmdBuff.Empty() || IsLabelSelector(t.cmdBuff.String()) { + return data } - - q := v.cmdBuff.String() + q := t.cmdBuff.String() if isFuzzySelector(q) { - return v.fuzzyFilter(q[2:]) + return fuzzyFilter(q[2:], t.NameColIndex(), data) } - return v.rxFilter(q) -} - -func (v *Table) rxFilter(q string) resource.TableData { - rx, err := regexp.Compile(`(?i)` + v.cmdBuff.String()) + filtered, err := rxFilter(t.cmdBuff.String(), data) if err != nil { log.Error().Err(errors.New("Invalid filter expression")).Msg("Regexp") - v.cmdBuff.Clear() - return v.data + t.cmdBuff.Clear() + return data } - - filtered := resource.TableData{ - Header: v.data.Header, - Rows: resource.RowEvents{}, - Namespace: v.data.Namespace, - } - for k, row := range v.data.Rows { - f := strings.Join(row.Fields, " ") - if rx.MatchString(f) { - filtered.Rows[k] = row - } - } - return filtered } -func (v *Table) fuzzyFilter(q string) resource.TableData { - var ss, kk []string - for k, row := range v.data.Rows { - ss = append(ss, row.Fields[v.NameColIndex()]) - kk = append(kk, k) - } - - filtered := resource.TableData{ - Header: v.data.Header, - Rows: resource.RowEvents{}, - Namespace: v.data.Namespace, - } - mm := fuzzy.Find(q, ss) - for _, m := range mm { - filtered.Rows[kk[m.Index]] = v.data.Rows[kk[m.Index]] - } - - return filtered -} - -// KeyBindings returns the bounded keys. -func (v *Table) KeyBindings() KeyActions { - return v.actions -} - // SearchBuff returns the associated command buffer. -func (v *Table) SearchBuff() *CmdBuff { - return v.cmdBuff -} - -// ClearSelection reset selected row. -func (v *Table) ClearSelection() { - v.Select(0, 0) - v.ScrollToBeginning() -} - -// SelectFirstRow select first data row if any. -func (v *Table) SelectFirstRow() { - if v.GetRowCount() > 0 { - v.Select(1, 0) - } +func (t *Table) SearchBuff() *CmdBuff { + return t.cmdBuff } // ShowDeleted marks row as deleted. -func (v *Table) ShowDeleted() { - r, _ := v.GetSelection() - cols := v.GetColumnCount() +func (t *Table) ShowDeleted() { + r, _ := t.GetSelection() + cols := t.GetColumnCount() for x := 0; x < cols; x++ { - v.GetCell(r, x).SetAttributes(tcell.AttrDim) + t.GetCell(r, x).SetAttributes(tcell.AttrDim) } } -// SetActions sets up keyboard action listener. -func (v *Table) SetActions(aa KeyActions) { - for k, a := range aa { - v.actions[k] = a - } -} - -// RmAction delete a keyed action. -func (v *Table) RmAction(kk ...tcell.Key) { - for _, k := range kk { - delete(v.actions, k) - } -} - -// Hints options -func (v *Table) Hints() Hints { - if v.actions != nil { - return v.actions.Hints() - } - - return nil -} - // UpdateTitle refreshes the table title. -func (v *Table) UpdateTitle() { - var title string - - rc := v.GetRowCount() - if rc > 0 { - rc-- +func (t *Table) UpdateTitle() { + ns := t.GetModel().GetNamespace() + if ns == render.AllNamespaces { + ns = render.NamespaceAll } - switch v.activeNS { - case resource.NotNamespaced, "*": - title = skinTitle(fmt.Sprintf(titleFmt, v.baseTitle, rc), v.styles.Frame()) - default: - ns := v.activeNS - if ns == resource.AllNamespaces { - ns = resource.AllNamespace - } - title = skinTitle(fmt.Sprintf(nsTitleFmt, v.baseTitle, ns, rc), v.styles.Frame()) - } - - if !v.cmdBuff.Empty() { - cmd := v.cmdBuff.String() - if isLabelSelector(cmd) { - cmd = trimLabelSelector(cmd) - } - title += skinTitle(fmt.Sprintf(searchFmt, cmd), v.styles.Frame()) - } - v.SetTitle(title) -} - -// SortInvertCmd reverses sorting order. -func (v *Table) SortInvertCmd(evt *tcell.EventKey) *tcell.EventKey { - v.sortCol.asc = !v.sortCol.asc - v.Refresh() - - return nil -} - -// ToggleMark toggles marked row -func (v *Table) ToggleMark() { - v.marks[v.GetSelectedItem()] = !v.marks[v.GetSelectedItem()] -} - -func (v *Table) isMarked(item string) bool { - return v.marks[item] + t.SetTitle(styleTitle(t.GetRowCount(), ns, t.BaseTitle, t.Path, t.cmdBuff.String(), t.styles)) } diff --git a/internal/ui/table_helper.go b/internal/ui/table_helper.go index c8f90b30..e2d8036d 100644 --- a/internal/ui/table_helper.go +++ b/internal/ui/table_helper.go @@ -1,38 +1,51 @@ package ui import ( + "context" "fmt" "regexp" - "sort" "strings" + "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/resource" + "github.com/derailed/k9s/internal/render" "github.com/rs/zerolog/log" + "github.com/sahilm/fuzzy" ) const ( - titleFmt = "[fg:bg:b] %s[fg:bg:-][[count:bg:b]%d[fg:bg:-]] " - searchFmt = "<[filter:bg:r]/%s[fg:bg:-]> " - nsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-][[count:bg:b]%d[fg:bg:-]][fg:bg:-] " - labelSelIndicator = "-l" - descIndicator = "↓" - ascIndicator = "↑" - fullFmat = "%s-%s-%d.csv" - noNSFmat = "%s-%d.csv" + // SearchFmt represents a filter view title. + SearchFmt = "<[filter:bg:r]/%s[fg:bg:-]> " + + nsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-][[count:bg:b]%d[fg:bg:-]][fg:bg:-] " + titleFmt = "[fg:bg:b] %s[fg:bg:-][[count:bg:b]%d[fg:bg:-]][fg:bg:-] " + descIndicator = "↓" + ascIndicator = "↑" + + // FullFmat specifies a namespaced dump file name. + FullFmat = "%s-%s-%d.csv" + + // NoNSFmat specifies a cluster wide dump file name. + NoNSFmat = "%s-%d.csv" ) var ( - cpuRX = regexp.MustCompile(`\A.{0,1}CPU`) - memRX = regexp.MustCompile(`\A.{0,1}MEM`) - labelCmd = regexp.MustCompile(`\A\-l`) + // LabelCmd identifies a label query + LabelCmd = regexp.MustCompile(`\A\-l`) + fuzzyCmd = regexp.MustCompile(`\A\-f`) ) -type cleanseFn func(string) string +func mustExtractSyles(ctx context.Context) *config.Styles { + styles, ok := ctx.Value(internal.KeyStyles).(*config.Styles) + if !ok { + log.Fatal().Msg("Expecting valid styles") + } + return styles +} // TrimCell removes superfluous padding. -func TrimCell(tv *Table, row, col int) string { +func TrimCell(tv *SelectTable, row, col int) string { c := tv.GetCell(row, col) if c == nil { log.Error().Err(fmt.Errorf("No cell at location [%d:%d]", row, col)).Msg("Trim cell failed!") @@ -41,13 +54,15 @@ func TrimCell(tv *Table, row, col int) string { return strings.TrimSpace(c.Text) } -func isLabelSelector(s string) bool { +// IsLabelSelector checks if query is a label query. +func IsLabelSelector(s string) bool { if s == "" { return false } - return labelCmd.MatchString(s) + return LabelCmd.MatchString(s) } +// IsFuzztySelector checks if query is fuzzy. func isFuzzySelector(s string) bool { if s == "" { return false @@ -55,11 +70,12 @@ func isFuzzySelector(s string) bool { return fuzzyCmd.MatchString(s) } -func trimLabelSelector(s string) string { +// TrimLabelSelector extracts label query. +func TrimLabelSelector(s string) string { return strings.TrimSpace(s[2:]) } -func skinTitle(fmat string, style config.Frame) string { +func SkinTitle(fmat string, style config.Frame) string { fmat = strings.Replace(fmat, "[fg:bg", "["+style.Title.FgColor+":"+style.Title.BgColor, -1) fmat = strings.Replace(fmat, "[hilite", "["+style.Title.HighlightColor, 1) fmat = strings.Replace(fmat, "[key", "["+style.Menu.NumKeyColor, 1) @@ -70,44 +86,6 @@ func skinTitle(fmat string, style config.Frame) string { return fmat } -func sortRows(evts resource.RowEvents, sortFn SortFn, sortCol SortColumn, keys []string) { - rows := make(resource.Rows, 0, len(evts)) - for k, r := range evts { - rows = append(rows, append(r.Fields, k)) - } - sortFn(rows, sortCol) - - for i, r := range rows { - keys[i] = r[len(r)-1] - } -} - -func defaultSort(rows resource.Rows, sortCol SortColumn) { - t := RowSorter{rows: rows, index: sortCol.index, asc: sortCol.asc} - sort.Sort(t) -} - -func sortAllRows(col SortColumn, rows resource.RowEvents, sortFn SortFn) (resource.Row, map[string]resource.Row) { - keys := make([]string, len(rows)) - sortRows(rows, sortFn, col, keys) - - sec := make(map[string]resource.Row, len(rows)) - for _, k := range keys { - grp := rows[k].Fields[col.index] - sec[grp] = append(sec[grp], k) - } - - // Performs secondary to sort by name for each groups. - prim := make(resource.Row, 0, len(sec)) - for k, v := range sec { - sort.Strings(v) - prim = append(prim, k) - } - sort.Sort(GroupSorter{prim, col.asc}) - - return prim, sec -} - func sortIndicator(col SortColumn, style config.Table, index int, name string) string { if col.index != index { return name @@ -119,3 +97,85 @@ func sortIndicator(col SortColumn, style config.Table, index int, name string) s } return fmt.Sprintf("%s[%s::]%s[::]", name, style.Header.SorterColor, order) } + +func formatCell(field string, padding int) string { + if IsASCII(field) { + return Pad(field, padding) + } + + return field +} + +func rxFilter(q string, data render.TableData) (render.TableData, error) { + rx, err := regexp.Compile(`(?i)` + q) + if err != nil { + return data, err + } + + filtered := render.TableData{ + Header: data.Header, + RowEvents: make(render.RowEvents, 0, len(data.RowEvents)), + Namespace: data.Namespace, + } + for _, re := range data.RowEvents { + f := strings.Join(re.Row.Fields, " ") + if rx.MatchString(f) { + filtered.RowEvents = append(filtered.RowEvents, re) + } + } + + return filtered, nil +} + +func fuzzyFilter(q string, index int, data render.TableData) render.TableData { + var ss []string + for _, re := range data.RowEvents { + ss = append(ss, re.Row.Fields[index]) + } + + filtered := render.TableData{ + Header: data.Header, + RowEvents: make(render.RowEvents, 0, len(data.RowEvents)), + Namespace: data.Namespace, + } + mm := fuzzy.Find(q, ss) + for _, m := range mm { + filtered.RowEvents = append(filtered.RowEvents, data.RowEvents[m.Index]) + } + + return filtered +} + +// UpdateTitle refreshes the table title. +func styleTitle(rc int, ns, base, path, buff string, styles *config.Styles) string { + if rc > 0 { + rc-- + } + + if ns == render.AllNamespaces { + ns = render.NamespaceAll + } + info := ns + if path != "" { + info = path + cns, n := render.Namespaced(path) + if cns == render.ClusterScope { + info = n + } + } + + var title string + if info == "" || info == render.ClusterScope { + title = SkinTitle(fmt.Sprintf(titleFmt, base, rc), styles.Frame()) + } else { + title = SkinTitle(fmt.Sprintf(nsTitleFmt, base, info, rc), styles.Frame()) + } + if buff == "" { + return title + } + + if IsLabelSelector(buff) { + buff = TrimLabelSelector(buff) + } + return title + SkinTitle(fmt.Sprintf(SearchFmt, buff), styles.Frame()) +} diff --git a/internal/ui/table_helper_test.go b/internal/ui/table_helper_test.go new file mode 100644 index 00000000..8f1c18d9 --- /dev/null +++ b/internal/ui/table_helper_test.go @@ -0,0 +1,42 @@ +package ui + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsLabelSelector(t *testing.T) { + uu := map[string]struct { + sel string + e 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}, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, IsLabelSelector(u.sel)) + }) + } +} + +func TestTrimLabelSelector(t *testing.T) { + uu := map[string]struct { + sel, e string + }{ + "cool": {"-l app=fred,env=blee", "app=fred,env=blee"}, + "noSpace": {"-lapp=fred,env=blee", "app=fred,env=blee"}, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, TrimLabelSelector(u.sel)) + }) + } +} diff --git a/internal/ui/table_test.go b/internal/ui/table_test.go index dd9a62d2..5ee46d68 100644 --- a/internal/ui/table_test.go +++ b/internal/ui/table_test.go @@ -1,80 +1,96 @@ -package ui +package ui_test import ( + "context" "testing" + "time" - "github.com/derailed/k9s/internal/resource" + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" "github.com/stretchr/testify/assert" ) -func TestTVSortRows(t *testing.T) { - uu := []struct { - rows resource.RowEvents - col int - asc bool - first resource.Row - e []string - }{ - { - resource.RowEvents{ - "row1": {Fields: resource.Row{"x", "y"}}, - "row2": {Fields: resource.Row{"a", "b"}}, +func TestTableNew(t *testing.T) { + v := ui.NewTable("fred") + ctx := context.WithValue(context.Background(), internal.KeyStyles, config.NewStyles()) + v.Init(ctx) + + assert.Equal(t, "fred", v.BaseTitle) +} + +func TestTableUpdate(t *testing.T) { + v := ui.NewTable("fred") + ctx := context.WithValue(context.Background(), internal.KeyStyles, config.NewStyles()) + v.Init(ctx) + + v.Update(makeTableData()) + + assert.Equal(t, 3, v.GetRowCount()) + assert.Equal(t, 3, v.GetColumnCount()) +} + +func TestTableSelection(t *testing.T) { + v := ui.NewTable("fred") + ctx := context.WithValue(context.Background(), internal.KeyStyles, config.NewStyles()) + v.Init(ctx) + m := &testModel{} + v.SetModel(m) + v.Update(m.Peek()) + v.SelectRow(1, true) + + assert.Equal(t, "r1", v.GetSelectedItem()) + assert.Equal(t, render.Row{ID: "r2", Fields: render.Fields{"blee", "duh", "zorg"}}, v.GetSelectedRow()) + assert.Equal(t, "blee", v.GetSelectedCell(0)) + assert.Equal(t, 1, v.GetSelectedRowIndex()) + assert.Equal(t, []string{"r1"}, v.GetSelectedItems()) + + v.ClearSelection() + v.SelectFirstRow() + assert.Equal(t, 1, v.GetSelectedRowIndex()) +} + +// ---------------------------------------------------------------------------- +// Helpers... + +type testModel struct{} + +var _ ui.Tabular = &testModel{} + +func (t *testModel) Empty() bool { return false } +func (t *testModel) Peek() render.TableData { return makeTableData() } +func (t *testModel) ClusterWide() bool { return false } +func (t *testModel) GetNamespace() string { return "blee" } +func (t *testModel) SetNamespace(string) {} +func (t *testModel) AddListener(model.TableListener) {} +func (t *testModel) Watch(context.Context) {} +func (t *testModel) InNamespace(string) bool { return true } +func (t *testModel) SetRefreshRate(time.Duration) {} + +func makeTableData() render.TableData { + t := render.NewTableData() + t.Namespace = "" + t.Header = render.HeaderRow{ + render.Header{Name: "a"}, + render.Header{Name: "b"}, + render.Header{Name: "c"}, + } + t.RowEvents = render.RowEvents{ + render.RowEvent{ + Row: render.Row{ + ID: "r1", + Fields: render.Fields{"blee", "duh", "fred"}, }, - 0, - true, - resource.Row{"a", "b"}, - []string{"row2", "row1"}, }, - { - resource.RowEvents{ - "row1": {Fields: resource.Row{"x", "y"}}, - "row2": {Fields: resource.Row{"a", "b"}}, + render.RowEvent{ + Row: render.Row{ + ID: "r2", + Fields: render.Fields{"blee", "duh", "zorg"}, }, - 1, - true, - resource.Row{"a", "b"}, - []string{"row2", "row1"}, - }, - { - resource.RowEvents{ - "row1": {Fields: resource.Row{"x", "y"}}, - "row2": {Fields: resource.Row{"a", "b"}}, - }, - 1, - false, - resource.Row{"x", "y"}, - []string{"row1", "row2"}, - }, - { - resource.RowEvents{ - "row1": {Fields: resource.Row{"2175h48m0.06015s", "y"}}, - "row2": {Fields: resource.Row{"403h42m34.060166s", "b"}}, - }, - 0, - true, - resource.Row{"403h42m34.060166s", "b"}, - []string{"row2", "row1"}, }, } - for _, u := range uu { - keys := make([]string, len(u.rows)) - sortRows(u.rows, defaultSort, SortColumn{u.col, len(u.rows), u.asc}, keys) - assert.Equal(t, u.e, keys) - assert.Equal(t, u.first, u.rows[u.e[0]].Fields) - } -} - -func BenchmarkTableSortRows(b *testing.B) { - evts := resource.RowEvents{ - "row1": {Fields: resource.Row{"x", "y"}}, - "row2": {Fields: resource.Row{"a", "b"}}, - } - sc := SortColumn{0, 2, true} - keys := make([]string, len(evts)) - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - sortRows(evts, defaultSort, sc, keys) - } + return *t } diff --git a/internal/ui/types.go b/internal/ui/types.go new file mode 100644 index 00000000..1026cb67 --- /dev/null +++ b/internal/ui/types.go @@ -0,0 +1,15 @@ +package ui + +import "github.com/derailed/k9s/internal/render" + +type ( + // SortFn represent a function that can sort columnar data. + SortFn func(rows render.Rows, sortCol SortColumn) + + // SortColumn represents a sortable column. + SortColumn struct { + index int + colCount int + asc bool + } +) diff --git a/internal/view/actions.go b/internal/view/actions.go new file mode 100644 index 00000000..8037acc6 --- /dev/null +++ b/internal/view/actions.go @@ -0,0 +1,139 @@ +package view + +import ( + "fmt" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/ui" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" +) + +type Runner interface { + App() *App + GetSelectedItem() string + Aliases() []string + EnvFn() EnvFunc +} + +func hasAll(scopes []string) bool { + for _, s := range scopes { + if s == "all" { + return true + } + } + return false +} + +func includes(aliases []string, s string) bool { + for _, a := range aliases { + if a == s { + return true + } + } + return false +} + +func inScope(scopes, aliases []string) bool { + if hasAll(scopes) { + return true + } + for _, s := range scopes { + if includes(aliases, s) { + return true + } + } + + return false +} + +func hotKeyActions(r Runner, aa ui.KeyActions) { + hh := config.NewHotKeys() + if err := hh.Load(); err != nil { + log.Warn().Msgf("No HotKey configuration found") + return + } + + for k, hk := range hh.HotKey { + key, err := asKey(hk.ShortCut) + if err != nil { + log.Error().Err(err).Msg("Unable to map hotkey shortcut to a key") + continue + } + _, ok := aa[key] + if ok { + log.Error().Err(fmt.Errorf("Doh! you are trying to overide an existing command `%s", k)).Msg("Invalid shortcut") + continue + } + aa[key] = ui.NewSharedKeyAction( + hk.Description, + gotoCmd(r, hk.Command), + false) + } +} + +func gotoCmd(r Runner, cmd string) ui.ActionHandler { + return func(evt *tcell.EventKey) *tcell.EventKey { + if err := r.App().gotoResource(cmd, true); err != nil { + r.App().Flash().Err(err) + } + return nil + } +} + +func pluginActions(r Runner, aa ui.KeyActions) { + pp := config.NewPlugins() + if err := pp.Load(); err != nil { + log.Warn().Msgf("No plugin configuration found") + return + } + + for k, plugin := range pp.Plugin { + if !inScope(plugin.Scopes, r.Aliases()) { + continue + } + key, err := asKey(plugin.ShortCut) + if err != nil { + log.Error().Err(err).Msg("Unable to map plugin shortcut to a key") + continue + } + _, ok := aa[key] + if ok { + log.Error().Err(fmt.Errorf("Doh! you are trying to overide an existing command `%s", k)).Msg("Invalid shortcut") + continue + } + aa[key] = ui.NewKeyAction( + plugin.Description, + execCmd(r, plugin.Command, plugin.Background, plugin.Args...), + true) + } +} + +func execCmd(r Runner, bin string, bg bool, args ...string) ui.ActionHandler { + return func(evt *tcell.EventKey) *tcell.EventKey { + path := r.GetSelectedItem() + if path == "" { + return evt + } + + var ( + env = r.EnvFn()() + aa = make([]string, len(args)) + err error + ) + for i, a := range args { + aa[i], err = env.envFor(a) + if err != nil { + log.Error().Err(err).Msg("Args match failed") + return nil + } + } + if run(true, r.App(), bin, bg, aa...) { + r.App().Flash().Info("Custom CMD launched!") + } else { + r.App().Flash().Info("Custom CMD failed!") + } + + return nil + } +} diff --git a/internal/view/alias.go b/internal/view/alias.go new file mode 100644 index 00000000..8ae01453 --- /dev/null +++ b/internal/view/alias.go @@ -0,0 +1,66 @@ +package view + +import ( + "context" + "strings" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" +) + +const aliasTitle = "Aliases" + +// Alias represents a command alias view. +type Alias struct { + ResourceViewer +} + +// NewAlias returns a new alias view. +func NewAlias(gvr client.GVR) ResourceViewer { + a := Alias{ + ResourceViewer: NewBrowser(gvr), + } + a.GetTable().SetColorerFn(render.Alias{}.ColorerFunc()) + a.GetTable().SetBorderFocusColor(tcell.ColorMediumSpringGreen) + a.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorMediumSpringGreen, tcell.AttrNone) + a.SetBindKeysFn(a.bindKeys) + a.SetContextFn(a.aliasContext) + + return &a +} + +func (a *Alias) aliasContext(ctx context.Context) context.Context { + return context.WithValue(ctx, internal.KeyAliases, a.App().command.alias) +} + +func (a *Alias) bindKeys(aa ui.KeyActions) { + aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace) + aa.Add(ui.KeyActions{ + tcell.KeyEnter: ui.NewKeyAction("Goto", a.gotoCmd, true), + ui.KeyShiftR: ui.NewKeyAction("Sort Resource", a.GetTable().SortColCmd(0, true), false), + ui.KeyShiftC: ui.NewKeyAction("Sort Command", a.GetTable().SortColCmd(1, true), false), + ui.KeyShiftA: ui.NewKeyAction("Sort ApiGroup", a.GetTable().SortColCmd(2, true), false), + }) +} + +func (a *Alias) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { + log.Debug().Msgf("GOTO CMD") + r, _ := a.GetTable().GetSelection() + if r != 0 { + s := ui.TrimCell(a.GetTable().SelectTable, r, 1) + tokens := strings.Split(s, ",") + if err := a.App().gotoResource(tokens[0], true); err != nil { + a.App().Flash().Err(err) + } + return nil + } + + if a.GetTable().SearchBuff().IsActive() { + return a.GetTable().activateCmd(evt) + } + return evt +} diff --git a/internal/view/alias_test.go b/internal/view/alias_test.go new file mode 100644 index 00000000..cda2d4bc --- /dev/null +++ b/internal/view/alias_test.go @@ -0,0 +1,134 @@ +package view_test + +import ( + "context" + "testing" + "time" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/k9s/internal/view" + "github.com/gdamore/tcell" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" +) + +func TestAliasNew(t *testing.T) { + v := view.NewAlias(client.GVR("aliases")) + + assert.Nil(t, v.Init(makeContext())) + assert.Equal(t, "Aliases", v.Name()) + assert.Equal(t, 4, len(v.Hints())) +} + +func TestAliasSearch(t *testing.T) { + v := view.NewAlias(client.GVR("aliases")) + assert.Nil(t, v.Init(makeContext())) + v.GetTable().SetModel(&testModel{}) + v.GetTable().SearchBuff().SetActive(true) + v.GetTable().SearchBuff().Set("dump") + + v.GetTable().SendKey(tcell.NewEventKey(tcell.KeyRune, 'd', tcell.ModNone)) + + assert.Equal(t, 3, v.GetTable().GetColumnCount()) + assert.Equal(t, 1, v.GetTable().GetRowCount()) +} + +func TestAliasGoto(t *testing.T) { + v := view.NewAlias(client.GVR("aliases")) + assert.Nil(t, v.Init(makeContext())) + v.GetTable().Select(0, 0) + + b := buffL{} + v.GetTable().SearchBuff().SetActive(true) + v.GetTable().SearchBuff().AddListener(&b) + v.GetTable().SendKey(tcell.NewEventKey(tcell.KeyEnter, 256, tcell.ModNone)) + + assert.True(t, v.GetTable().SearchBuff().IsActive()) +} + +// ---------------------------------------------------------------------------- +// Helpers... + +type buffL struct { + active int + changed int +} + +func (b *buffL) BufferChanged(s string) { + b.changed++ +} +func (b *buffL) BufferActive(state bool, kind ui.BufferKind) { + b.active++ +} + +func makeContext() context.Context { + a := view.NewApp(config.NewConfig(ks{})) + 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() ([]string, error) { + return []string{"test"}, nil +} + +func (k ks) NamespaceNames(nn []v1.Namespace) []string { + return []string{"test"} +} + +type testModel struct{} + +var _ ui.Tabular = &testModel{} + +func (t *testModel) Empty() bool { return false } +func (t *testModel) Peek() render.TableData { return makeTableData() } +func (t *testModel) ClusterWide() bool { return false } +func (t *testModel) GetNamespace() string { return "blee" } +func (t *testModel) SetNamespace(string) {} +func (t *testModel) AddListener(model.TableListener) {} +func (t *testModel) Watch(context.Context) {} +func (t *testModel) InNamespace(string) bool { return true } +func (t *testModel) SetRefreshRate(time.Duration) {} + +func makeTableData() render.TableData { + return render.TableData{ + Namespace: render.ClusterScope, + Header: render.HeaderRow{ + render.Header{Name: "RESOURCE"}, + render.Header{Name: "COMMAND"}, + render.Header{Name: "APIGROUP"}, + }, + RowEvents: render.RowEvents{ + render.RowEvent{ + Row: render.Row{ + ID: "r1", + Fields: render.Fields{"blee", "duh", "fred"}, + }, + }, + render.RowEvent{ + Row: render.Row{ + ID: "r2", + Fields: render.Fields{"fred", "duh", "zorg"}, + }, + }, + }, + } +} diff --git a/internal/view/app.go b/internal/view/app.go new file mode 100644 index 00000000..25584a7a --- /dev/null +++ b/internal/view/app.go @@ -0,0 +1,465 @@ +package view + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/k9s/internal/watch" + "github.com/derailed/tview" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" +) + +const ( + splashTime = 1 + clusterRefresh = time.Duration(5 * time.Second) + statusIndicatorFmt = "[orange::b]K9s [aqua::]%s [white::]%s:%s:%s [lawngreen::]%s%%[white::]::[darkturquoise::]%s%%" +) + +// App represents an application view. +type App struct { + *ui.App + + Content *PageStack + command *Command + factory *watch.Factory + version string + showHeader bool + cancelFn context.CancelFunc +} + +// NewApp returns a K9s app instance. +func NewApp(cfg *config.Config) *App { + a := App{ + App: ui.NewApp(cfg.K9s.CurrentCluster), + Content: NewPageStack(), + } + a.Config = cfg + a.InitBench(cfg.K9s.CurrentCluster) + + a.Views()["statusIndicator"] = ui.NewStatusIndicator(a.App, a.Styles) + a.Views()["clusterInfo"] = NewClusterInfo(&a, client.NewMetricsServer(cfg.GetConnection())) + + return &a +} + +// ActiveView returns the currently active view. +func (a *App) ActiveView() model.Component { + return a.Content.GetPrimitive("main").(model.Component) +} + +func (a *App) PrevCmd(evt *tcell.EventKey) *tcell.EventKey { + if !a.Content.IsLast() { + a.Content.Pop() + } + + return nil +} + +func (a *App) Init(version string, rate int) error { + ctx := context.WithValue(context.Background(), internal.KeyApp, a) + if err := a.Content.Init(ctx); err != nil { + return err + } + a.Content.Stack.AddListener(a.Crumbs()) + a.Content.Stack.AddListener(a.Menu()) + + a.version = version + a.App.Init() + a.CmdBuff().AddListener(a) + a.bindKeys() + if a.Conn() == nil { + return errors.New("No client connection detected") + } + ns, err := a.Conn().Config().CurrentNamespaceName() + if err != nil { + log.Info().Msg("No namespace specified using all namespaces") + } + + a.factory = watch.NewFactory(a.Conn()) + a.initFactory(ns) + + a.command = NewCommand(a) + if err := a.command.Init(); err != nil { + return err + } + + a.clusterInfo().init(version) + if a.Config.K9s.GetHeadless() { + a.refreshIndicator() + } + + main := tview.NewFlex().SetDirection(tview.FlexRow) + main.AddItem(a.statusIndicator(), 1, 1, false) + main.AddItem(a.Content, 0, 10, true) + main.AddItem(a.Crumbs(), 2, 1, false) + main.AddItem(a.Flash(), 2, 1, false) + + a.Main.AddPage("main", main, true, false) + a.Main.AddPage("splash", ui.NewSplash(a.Styles, version), true, true) + a.toggleHeader(!a.Config.K9s.GetHeadless()) + + a.Styles.AddListener(a) + + return nil +} + +func (a *App) StylesChanged(s *config.Styles) { + a.Main.SetBackgroundColor(s.BgColor()) + if f, ok := a.Main.GetPrimitive("main").(*tview.Flex); ok { + f.SetBackgroundColor(s.BgColor()) + if h, ok := f.ItemAt(0).(*tview.Flex); ok { + h.SetBackgroundColor(s.BgColor()) + } else { + log.Error().Msgf("Header not found") + } + } else { + log.Error().Msgf("Main not found") + } +} + +func (a *App) bindKeys() { + a.AddActions(ui.KeyActions{ + ui.KeyH: ui.NewSharedKeyAction("ToggleHeader", a.toggleHeaderCmd, false), + ui.KeyHelp: ui.NewSharedKeyAction("Help", a.helpCmd, false), + tcell.KeyCtrlA: ui.NewSharedKeyAction("Aliases", a.aliasCmd, false), + tcell.KeyEnter: ui.NewKeyAction("Goto", a.gotoCmd, false), + tcell.KeyCtrlU: ui.NewSharedKeyAction("Clear Filter", a.clearCmd, false), + }) +} + +// Changed indicates the buffer was changed. +func (a *App) BufferChanged(s string) {} + +// Active indicates the buff activity changed. +func (a *App) BufferActive(state bool, _ ui.BufferKind) { + flex, ok := a.Main.GetPrimitive("main").(*tview.Flex) + if !ok { + return + } + + if state && flex.ItemAt(1) != a.Cmd() { + flex.AddItemAtIndex(1, a.Cmd(), 3, 1, false) + } else if !state && flex.ItemAt(1) == a.Cmd() { + flex.RemoveItemAtIndex(1) + } + a.Draw() +} + +func (a *App) toggleHeader(flag bool) { + a.showHeader = flag + flex, ok := a.Main.GetPrimitive("main").(*tview.Flex) + if !ok { + log.Fatal().Msg("Expecting valid flex view") + } + if a.showHeader { + flex.RemoveItemAtIndex(0) + flex.AddItemAtIndex(0, a.buildHeader(), 7, 1, false) + } else { + flex.RemoveItemAtIndex(0) + flex.AddItemAtIndex(0, a.statusIndicator(), 1, 1, false) + a.refreshIndicator() + } +} + +func (a *App) buildHeader() tview.Primitive { + header := tview.NewFlex() + header.SetBackgroundColor(a.Styles.BgColor()) + header.SetBorderPadding(0, 0, 1, 1) + header.SetDirection(tview.FlexColumn) + if !a.showHeader { + return header + } + header.AddItem(a.clusterInfo(), 35, 1, false) + header.AddItem(a.Menu(), 0, 1, false) + header.AddItem(a.Logo(), 26, 1, false) + + return header +} + +func (a *App) Halt() { + if a.cancelFn != nil { + a.cancelFn() + } +} + +func (a *App) Resume() { + var ctx context.Context + ctx, a.cancelFn = context.WithCancel(context.Background()) + go a.clusterUpdater(ctx) + if err := a.StylesUpdater(ctx, a); err != nil { + log.Error().Err(err).Msgf("Styles update failed") + } +} + +func (a *App) clusterUpdater(ctx context.Context) { + for { + select { + case <-ctx.Done(): + log.Debug().Msg("Cluster updater canceled!") + return + case <-time.After(clusterRefresh): + a.QueueUpdateDraw(func() { + a.refreshClusterInfo() + }) + } + } +} + +// BOZO!! Refact to use model/view strategy. +func (a *App) refreshClusterInfo() { + if !a.showHeader { + a.refreshIndicator() + } else { + a.clusterInfo().refresh() + } +} + +func (a *App) refreshIndicator() { + mx := client.NewMetricsServer(a.Conn()) + cluster := model.NewCluster(a.Conn(), mx) + var cmx client.ClusterMetrics + nos, nmx, err := fetchResources(a) + if err != nil { + log.Error().Err(err).Msgf("unable to refresh cluster statusIndicator") + return + } + + if err := cluster.Metrics(nos, nmx, &cmx); err != nil { + log.Error().Err(err).Msgf("unable to refresh cluster statusIndicator") + return + } + + cpu := render.AsPerc(cmx.PercCPU) + if cpu == "0" { + cpu = render.NAValue + } + mem := render.AsPerc(cmx.PercMEM) + if mem == "0" { + mem = render.NAValue + } + + a.statusIndicator().SetPermanent(fmt.Sprintf( + statusIndicatorFmt, + a.version, + cluster.ClusterName(), + cluster.UserName(), + cluster.Version(), + cpu, + mem, + )) +} + +func (a *App) switchNS(ns string) bool { + if ns == render.ClusterScope { + ns = render.AllNamespaces + } + if err := a.Config.SetActiveNamespace(ns); err != nil { + log.Error().Err(err).Msg("Config Set NS failed!") + return false + } + a.factory.SetActive(ns) + + return true +} + +func (a *App) switchCtx(name string, loadPods bool) error { + log.Debug().Msgf("Switching Context %q", name) + + 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") + } + a.initFactory(ns) + + if err := a.command.Reset(); err != nil { + return err + } + a.Config.Reset() + if err := a.Config.Save(); err != nil { + log.Error().Err(err).Msg("Config save failed!") + } + a.Flash().Infof("Switching context to %s", name) + if err := a.gotoResource("pods", true); loadPods && err != nil { + a.Flash().Err(err) + } + a.refreshClusterInfo() + a.ReloadStyles(name) + } + + return nil +} + +func (a *App) initFactory(ns string) { + a.factory.Terminate() + a.factory.Init() + a.factory.SetActive(ns) +} + +// BailOut exists the application. +func (a *App) BailOut() { + a.factory.Terminate() + a.App.BailOut() +} + +// Run starts the application loop +func (a *App) Run() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + a.Halt() + + if err := a.StylesUpdater(ctx, a); err != nil { + log.Error().Err(err).Msg("Unable to track skin changes") + } + + go func() { + <-time.After(splashTime * time.Second) + a.QueueUpdateDraw(func() { + a.Main.SwitchToPage("main") + }) + }() + + if err := a.command.defaultCmd(); err != nil { + panic(err) + } + if err := a.Application.Run(); err != nil { + panic(err) + } +} + +func (a *App) status(l ui.FlashLevel, msg string) { + a.Flash().Info(msg) + a.setIndicator(l, msg) + a.setLogo(l, msg) + a.Draw() +} + +func (a *App) setLogo(l ui.FlashLevel, msg string) { + switch l { + case ui.FlashErr: + a.Logo().Err(msg) + case ui.FlashWarn: + a.Logo().Warn(msg) + case ui.FlashInfo: + a.Logo().Info(msg) + default: + a.Logo().Reset() + } + a.Draw() +} + +func (a *App) setIndicator(l ui.FlashLevel, msg string) { + switch l { + case ui.FlashErr: + a.statusIndicator().Err(msg) + case ui.FlashWarn: + a.statusIndicator().Warn(msg) + case ui.FlashInfo: + a.statusIndicator().Info(msg) + default: + a.statusIndicator().Reset() + } + a.Draw() +} + +func (a *App) toggleHeaderCmd(evt *tcell.EventKey) *tcell.EventKey { + if a.Cmd().InCmdMode() { + return evt + } + + a.showHeader = !a.showHeader + a.toggleHeader(a.showHeader) + a.Draw() + + return nil +} + +func (a *App) clearCmd(evt *tcell.EventKey) *tcell.EventKey { + if !a.CmdBuff().IsActive() { + return evt + } + a.CmdBuff().Clear() + + return nil +} + +func (a *App) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { + if a.CmdBuff().IsActive() && !a.CmdBuff().Empty() { + if err := a.gotoResource(a.GetCmd(), true); err != nil { + log.Error().Err(err).Msgf("Goto resource for %q failed", a.GetCmd()) + a.Flash().Err(err) + return nil + } + a.ResetCmd() + return nil + } + a.ActivateCmd(false) + + return evt +} + +func (a *App) helpCmd(evt *tcell.EventKey) *tcell.EventKey { + if _, ok := a.Content.GetPrimitive("main").(*Help); ok { + return evt + } + if a.Content.Top() != nil && a.Content.Top().Name() == helpTitle { + a.Content.Pop() + return nil + } + + if err := a.inject(NewHelp()); err != nil { + a.Flash().Err(err) + } + + return nil +} + +func (a *App) aliasCmd(evt *tcell.EventKey) *tcell.EventKey { + if _, ok := a.Content.GetPrimitive("main").(*Alias); ok { + return evt + } + + if a.Content.Top() != nil && a.Content.Top().Name() == aliasTitle { + a.Content.Pop() + return nil + } + + if err := a.inject(NewAlias("aliases")); err != nil { + a.Flash().Err(err) + } + + return nil +} + +func (a *App) gotoResource(res string, clearStack bool) error { + return a.command.run(res, clearStack) +} + +func (a *App) inject(c model.Component) error { + ctx := context.WithValue(context.Background(), internal.KeyApp, a) + if err := c.Init(ctx); err != nil { + return fmt.Errorf("component init failed for %q %v", c.Name(), err) + } + a.Content.Push(c) + + return nil +} + +func (a *App) clusterInfo() *ClusterInfo { + return a.Views()["clusterInfo"].(*ClusterInfo) +} + +func (a *App) statusIndicator() *ui.StatusIndicator { + return a.Views()["statusIndicator"].(*ui.StatusIndicator) +} diff --git a/internal/view/app_test.go b/internal/view/app_test.go new file mode 100644 index 00000000..b1f3476f --- /dev/null +++ b/internal/view/app_test.go @@ -0,0 +1,16 @@ +package view_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/view" + "github.com/stretchr/testify/assert" +) + +func TestAppNew(t *testing.T) { + a := view.NewApp(config.NewConfig(ks{})) + a.Init("blee", 10) + + assert.Equal(t, 12, len(a.GetActions())) +} diff --git a/internal/view/benchmark.go b/internal/view/benchmark.go new file mode 100644 index 00000000..828a818c --- /dev/null +++ b/internal/view/benchmark.go @@ -0,0 +1,88 @@ +package view + +import ( + "context" + "io/ioutil" + "path/filepath" + "strings" + + "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/render" + "github.com/derailed/k9s/internal/ui" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" +) + +const resultTitle = "Benchmark Results" + +// Benchmark represents a service benchmark results view. +type Benchmark struct { + ResourceViewer + + details *Details +} + +// NewBench returns a new viewer. +func NewBenchmark(gvr client.GVR) ResourceViewer { + b := Benchmark{ + ResourceViewer: NewBrowser(gvr), + details: NewDetails(resultTitle), + } + b.GetTable().SetBorderFocusColor(tcell.ColorSeaGreen) + b.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorSeaGreen, tcell.AttrNone) + b.GetTable().SetColorerFn(render.Benchmark{}.ColorerFunc()) + b.GetTable().SetSortCol(b.GetTable().NameColIndex()+7, 0, true) + b.SetContextFn(b.benchContext) + b.GetTable().SetEnterFn(b.viewBench) + + return &b +} + +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, ns, res, path string) { + log.Debug().Msgf("VIEWBENCH %q -- %q -- %q", ns, res, path) + data, err := readBenchFile(app.Config, b.benchFile()) + if err != nil { + app.Flash().Errf("Unable to load bench file %s", err) + return + } + + b.details.SetText(data) + b.details.SetSubject(fileToSubject(path)) + if err := app.inject(b.details); err != nil { + app.Flash().Err(err) + } +} + +func fileToSubject(path string) string { + tokens := strings.Split(path, "/") + log.Debug().Msgf("TOKENS %v", tokens) + ee := strings.Split(tokens[len(tokens)-1], "_") + return ee[0] + "/" + ee[1] +} + +func (b *Benchmark) benchFile() string { + r := b.GetTable().GetSelectedRowIndex() + return ui.TrimCell(b.GetTable().SelectTable, r, 7) +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func benchDir(cfg *config.Config) string { + return filepath.Join(perf.K9sBenchDir, cfg.K9s.CurrentCluster) +} + +func readBenchFile(cfg *config.Config, n string) (string, error) { + data, err := ioutil.ReadFile(filepath.Join(benchDir(cfg), n)) + if err != nil { + return "", err + } + return string(data), nil +} diff --git a/internal/view/browser.go b/internal/view/browser.go new file mode 100644 index 00000000..4dfe8d95 --- /dev/null +++ b/internal/view/browser.go @@ -0,0 +1,520 @@ +package view + +import ( + "bytes" + "context" + "errors" + "fmt" + "strconv" + + "github.com/atotto/clipboard" + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/k9s/internal/ui/dialog" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/printers" +) + +// ContextFunc enhances a given context. +type ContextFunc func(context.Context) context.Context + +type BindKeysFunc func(ui.KeyActions) + +// Browser represents a generic resource browser. +type Browser struct { + *Table + + namespaces map[int]string + gvr client.GVR + envFn EnvFunc + meta metav1.APIResource + accessor dao.Accessor + contextFn ContextFunc + bindKeysFn BindKeysFunc + cancelFn context.CancelFunc +} + +// NewBrowser returns a new browser. +func NewBrowser(gvr client.GVR) ResourceViewer { + return &Browser{ + Table: NewTable(string(gvr)), + gvr: gvr, + } +} + +// Init watches all running pods in given namespace +func (b *Browser) Init(ctx context.Context) error { + var err error + b.meta, err = dao.MetaFor(b.gvr) + if err != nil { + return err + } + + if err = b.Table.Init(ctx); err != nil { + return err + } + if !dao.IsK9sMeta(b.meta) { + if _, e := b.app.factory.CanForResource(b.app.Config.ActiveNamespace(), b.GVR()); e != nil { + return e + } + } + + b.bindKeys() + if b.bindKeysFn != nil { + b.bindKeysFn(b.Actions()) + } + b.BaseTitle = b.meta.Kind + b.SetTitle(" [orange:i:]LOADING... ") + b.accessor, err = dao.AccessorFor(b.app.factory, b.gvr) + if err != nil { + return err + } + + b.envFn = b.defaultK9sEnv + b.setNamespace(b.App().Config.ActiveNamespace()) + row, _ := b.GetSelection() + if row == 0 && b.GetRowCount() > 0 { + b.Select(1, 0) + } + b.GetModel().AddListener(b) + + return nil +} + +func (b *Browser) bindKeys() { + b.Actions().Add(ui.KeyActions{ + tcell.KeyEscape: ui.NewSharedKeyAction("Filter Reset", b.resetCmd, false), + tcell.KeyEnter: ui.NewSharedKeyAction("Filter", b.filterCmd, false), + }) +} + +// Start initializes browser updates. +func (b *Browser) Start() { + b.Stop() + + b.Table.Start() + ctx := b.defaultContext() + ctx, b.cancelFn = context.WithCancel(ctx) + if b.contextFn != nil { + ctx = b.contextFn(ctx) + } + if path, ok := ctx.Value(internal.KeyPath).(string); ok && path != "" { + b.Path = path + } + b.GetModel().Watch(ctx) +} + +func (b *Browser) resetCmd(evt *tcell.EventKey) *tcell.EventKey { + if !b.SearchBuff().InCmdMode() { + b.SearchBuff().Reset() + return b.App().PrevCmd(evt) + } + + cmd := b.SearchBuff().String() + b.App().Flash().Info("Clearing filter...") + b.SearchBuff().Reset() + + if ui.IsLabelSelector(cmd) { + b.Start() + } else { + b.Refresh() + } + + return nil +} + +func (b *Browser) filterCmd(evt *tcell.EventKey) *tcell.EventKey { + if !b.SearchBuff().IsActive() { + return evt + } + + b.SearchBuff().SetActive(false) + + cmd := b.SearchBuff().String() + if ui.IsLabelSelector(cmd) { + b.Start() + return nil + } + b.Refresh() + + return nil +} + +// Stop terminates browser updates. +func (b *Browser) Stop() { + if b.cancelFn == nil { + return + } + b.Table.Stop() + log.Debug().Msgf("BROWSER %q", b.gvr) + b.cancelFn() + b.cancelFn = nil +} + +func (b *Browser) refresh() { + b.Start() +} + +// Name returns the component name. +func (b *Browser) Name() string { return b.meta.Kind } + +// SetContextFn populates a custom context. +func (b *Browser) SetContextFn(f ContextFunc) { b.contextFn = f } + +// SetBindKeysFn adds additional key bindings. +func (b *Browser) SetBindKeysFn(f BindKeysFunc) { b.bindKeysFn = f } + +// SetEnvFn sets a function to pull viewer env vars for plugins. +func (b *Browser) SetEnvFn(f EnvFunc) { b.envFn = f } + +// GVR returns a resource descriptor. +func (b *Browser) GVR() string { return string(b.gvr) } + +// GetTable returns the underlying table. +func (b *Browser) GetTable() *Table { return b.Table } + +// ---------------------------------------------------------------------------- +// Actions()... + +func (b *Browser) cpCmd(evt *tcell.EventKey) *tcell.EventKey { + path := b.GetSelectedItem() + if path == "" { + return evt + } + + _, n := client.Namespaced(path) + log.Debug().Msgf("Copied selection to clipboard %q", n) + b.app.Flash().Info("Current selection copied to clipboard...") + if err := clipboard.WriteAll(n); err != nil { + b.app.Flash().Err(err) + } + + return nil +} + +func (b *Browser) enterCmd(evt *tcell.EventKey) *tcell.EventKey { + path := b.GetSelectedItem() + if b.filterCmd(evt) == nil || path == "" { + return nil + } + + f := b.describeResource + if b.enterFn != nil { + f = b.enterFn + } + f(b.app, b.GetModel().GetNamespace(), string(b.gvr), path) + + return nil +} + +func (b *Browser) refreshCmd(*tcell.EventKey) *tcell.EventKey { + b.app.Flash().Info("Refreshing...") + b.refresh() + return nil +} + +func (b *Browser) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { + selections := b.GetSelectedItems() + if len(selections) == 0 { + return evt + } + log.Debug().Msgf("DEL SELECTIONS %#v", selections) + + b.Stop() + defer b.Start() + { + msg := fmt.Sprintf("Delete %s %s?", b.gvr, selections[0]) + if len(selections) > 1 { + msg = fmt.Sprintf("Delete %d marked %s?", len(selections), b.gvr) + } + + cancelFn := func() {} + if dao.IsK9sMeta(b.meta) { + dialog.ShowConfirm(b.app.Content.Pages, "Confirm Delete", msg, func() { + b.ShowDeleted() + if len(selections) > 1 { + b.app.Flash().Infof("Delete %d marked %s", len(selections), b.gvr) + } else { + b.app.Flash().Infof("Delete resource %s %s", b.gvr, selections[0]) + } + for _, sel := range selections { + if err := b.accessor.(dao.Nuker).Delete(sel, true, true); err != nil { + b.app.Flash().Errf("Delete failed with `%s", err) + } else { + b.GetTable().DeleteMark(sel) + } + } + b.refresh() + b.SelectRow(1, true) + }, cancelFn) + return nil + } + + dialog.ShowDelete(b.app.Content.Pages, msg, func(cascade, force bool) { + b.ShowDeleted() + if len(selections) > 1 { + b.app.Flash().Infof("Delete %d marked %s", len(selections), b.gvr) + } else { + b.app.Flash().Infof("Delete resource %s %s", b.gvr, selections[0]) + } + for _, sel := range selections { + if err := b.accessor.(dao.Nuker).Delete(sel, cascade, force); err != nil { + b.app.Flash().Errf("Delete failed with `%s", err) + } else { + b.app.factory.DeleteForwarder(sel) + b.GetTable().DeleteMark(sel) + } + } + b.refresh() + b.SelectRow(1, true) + }, cancelFn) + } + + return nil +} + +func (b *Browser) describeCmd(evt *tcell.EventKey) *tcell.EventKey { + path := b.GetSelectedItem() + if path == "" { + return evt + } + b.describeResource(b.app, b.GetModel().GetNamespace(), string(b.gvr), path) + + return nil +} + +func (b *Browser) describeResource(app *App, _, _, sel string) { + ns, n := client.Namespaced(sel) + yaml, err := dao.Describe(b.app.Conn(), b.gvr, ns, n) + if err != nil { + b.app.Flash().Errf("Describe command failed: %s", err) + return + } + + details := NewDetails("Describe") + ctx := context.WithValue(context.Background(), internal.KeyApp, b.App()) + if err := details.Init(ctx); err != nil { + log.Error().Err(err).Msg("Details init failed") + return + } + details.SetSubject(sel) + details.SetTextColor(b.app.Styles.FgColor()) + details.Update(yaml) + // BOZO!! + // details.SetText(colorizeYAML(b.app.Styles.Views().Yaml, yaml)) + // details.ScrollToBeginning() + if err := b.app.inject(details); err != nil { + b.app.Flash().Err(err) + } +} + +func (b *Browser) viewCmd(evt *tcell.EventKey) *tcell.EventKey { + path := b.GetSelectedItem() + if path == "" { + return evt + } + + log.Debug().Msgf("------ NAMESPACES %q vs %q", path, b.GetModel().GetNamespace()) + o, err := b.app.factory.Get(string(b.gvr), path, labels.Everything()) + if err != nil { + b.app.Flash().Errf("Unable to get resource %q -- %s", b.gvr, err) + return nil + } + + raw, err := toYAML(o) + if err != nil { + b.app.Flash().Errf("Unable to marshal resource %s", err) + return nil + } + + details := NewDetails("YAML") + details.SetSubject(path) + details.SetTextColor(b.app.Styles.FgColor()) + details.SetText(colorizeYAML(b.app.Styles.Views().Yaml, raw)) + details.ScrollToBeginning() + if err := b.app.inject(details); err != nil { + b.App().Flash().Err(err) + } + + return nil +} + +func toYAML(o runtime.Object) (string, error) { + var ( + buff bytes.Buffer + p printers.YAMLPrinter + ) + err := p.PrintObj(o, &buff) + if err != nil { + log.Error().Msgf("Marshal Error %v", err) + return "", err + } + + return buff.String(), nil +} + +func (b *Browser) editCmd(evt *tcell.EventKey) *tcell.EventKey { + path := b.GetSelectedItem() + if path == "" { + return evt + } + + b.Stop() + defer b.Start() + { + ns, n := client.Namespaced(path) + args := make([]string, 0, 10) + args = append(args, "edit") + args = append(args, b.meta.Kind) + args = append(args, "-n", ns) + args = append(args, "--context", b.app.Config.K9s.CurrentContext) + if cfg := b.app.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" { + args = append(args, "--kubeconfig", *cfg) + } + if !runK(true, b.app, append(args, n)...) { + b.app.Flash().Err(errors.New("Edit exec failed")) + } + } + + return evt +} + +func (b *Browser) setNamespace(ns string) { + if !b.meta.Namespaced { + b.GetModel().SetNamespace(render.ClusterScope) + return + } + if b.GetModel().InNamespace(ns) { + return + } + + if ns == render.NamespaceAll { + ns = render.AllNamespaces + } + log.Debug().Msgf("!!!!!! SETTING NS %q", ns) + b.GetModel().SetNamespace(ns) +} + +func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey { + i, _ := strconv.Atoi(string(evt.Rune())) + ns := b.namespaces[i] + if ns == "" { + ns = render.NamespaceAll + } + + b.app.switchNS(ns) + b.setNamespace(ns) + b.app.Flash().Infof("Viewing namespace `%s`...", ns) + b.refresh() + b.UpdateTitle() + b.SelectRow(1, 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!") + } + if err := b.app.Config.Save(); err != nil { + log.Error().Err(err).Msg("Config save failed!") + } + + return nil +} + +// TableLoadChanged notifies view something went south. +func (b *Browser) TableLoadFailed(err error) { + b.app.QueueUpdateDraw(func() { + b.app.Flash().Err(err) + }) +} + +// TableDataChanged notifies view new data is available. +func (b *Browser) TableDataChanged(data render.TableData) { + b.app.QueueUpdateDraw(func() { + b.refreshActions() + b.Update(data) + }) +} + +func (b *Browser) defaultContext() context.Context { + ctx := context.Background() + + ctx = context.WithValue(ctx, internal.KeyFactory, b.app.factory) + ctx = context.WithValue(ctx, internal.KeyGVR, string(b.gvr)) + ctx = context.WithValue(ctx, internal.KeyPath, b.Path) + + ctx = context.WithValue(ctx, internal.KeyLabels, "") + if ui.IsLabelSelector(b.SearchBuff().String()) { + ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(b.SearchBuff().String())) + } + ctx = context.WithValue(ctx, internal.KeyFields, "") + ctx = context.WithValue(ctx, internal.KeyNamespace, b.App().Config.ActiveNamespace()) + + return ctx +} + +func (b *Browser) namespaceActions(aa ui.KeyActions) { + if b.app.Conn() == nil || !b.meta.Namespaced || b.GetTable().Path != "" { + return + } + b.namespaces = make(map[int]string, config.MaxFavoritesNS) + aa[tcell.Key(ui.NumKeys[0])] = ui.NewKeyAction(render.NamespaceAll, b.switchNamespaceCmd, true) + b.namespaces[0] = render.NamespaceAll + index := 1 + for _, n := range b.app.Config.FavNamespaces() { + if n == render.NamespaceAll { + continue + } + aa[tcell.Key(ui.NumKeys[index])] = ui.NewKeyAction(n, b.switchNamespaceCmd, true) + b.namespaces[index] = n + index++ + } +} + +func (b *Browser) refreshActions() { + aa := ui.KeyActions{ + ui.KeyC: ui.NewKeyAction("Copy", b.cpCmd, false), + tcell.KeyEnter: ui.NewKeyAction("View", b.enterCmd, false), + tcell.KeyCtrlR: ui.NewKeyAction("Refresh", b.refreshCmd, false), + } + b.namespaceActions(aa) + + if client.Can(b.meta.Verbs, "edit") { + aa[ui.KeyE] = ui.NewKeyAction("Edit", b.editCmd, true) + } + if client.Can(b.meta.Verbs, "delete") { + aa[tcell.KeyCtrlD] = ui.NewKeyAction("Delete", b.deleteCmd, true) + } + if client.Can(b.meta.Verbs, "view") { + aa[ui.KeyY] = ui.NewKeyAction("YAML", b.viewCmd, true) + } + if client.Can(b.meta.Verbs, "describe") { + aa[ui.KeyD] = ui.NewKeyAction("Describe", b.describeCmd, true) + } + pluginActions(b, aa) + hotKeyActions(b, aa) + b.Actions().Add(aa) + + if b.bindKeysFn != nil { + b.bindKeysFn(b.Actions()) + } + b.app.Menu().HydrateMenu(b.Hints()) +} + +func (b *Browser) Aliases() []string { + return append(b.meta.ShortNames, b.meta.SingularName, b.meta.Name) +} + +func (b *Browser) EnvFn() EnvFunc { + return b.envFn +} + +func (b *Browser) defaultK9sEnv() K9sEnv { + return defaultK9sEnv(b.app, b.GetSelectedItem(), b.GetSelectedRow()) +} diff --git a/internal/view/cluster_info.go b/internal/view/cluster_info.go new file mode 100644 index 00000000..95aaa3f8 --- /dev/null +++ b/internal/view/cluster_info.go @@ -0,0 +1,189 @@ +package view + +import ( + "strings" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/tview" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +// ClusterInfo represents a cluster info view. +type ClusterInfo struct { + *tview.Table + + app *App + mxs *client.MetricsServer + styles *config.Styles +} + +// NewClusterInfo returns a new cluster info view. +func NewClusterInfo(app *App, mx *client.MetricsServer) *ClusterInfo { + return &ClusterInfo{ + app: app, + Table: tview.NewTable(), + mxs: mx, + styles: app.Styles, + } +} + +func (c *ClusterInfo) init(version string) { + cluster := model.NewCluster(c.app.Conn(), c.mxs) + + c.app.Styles.AddListener(c) + + row := c.initInfo(cluster) + row = c.initVersion(row, version, cluster) + + c.SetCell(row, 0, c.sectionCell("CPU")) + c.SetCell(row, 1, c.infoCell(render.NAValue)) + row++ + c.SetCell(row, 0, c.sectionCell("MEM")) + c.SetCell(row, 1, c.infoCell(render.NAValue)) + + c.refresh() +} + +// StylesChanges notifies skin changed. +func (c *ClusterInfo) StylesChanged(s *config.Styles) { + c.styles = s + c.SetBackgroundColor(s.BgColor()) + c.refresh() +} + +func (c *ClusterInfo) initInfo(cluster *model.Cluster) int { + var row int + c.SetCell(row, 0, c.sectionCell("Context")) + c.SetCell(row, 1, c.infoCell(cluster.ContextName())) + row++ + + c.SetCell(row, 0, c.sectionCell("Cluster")) + c.SetCell(row, 1, c.infoCell(cluster.ClusterName())) + row++ + + c.SetCell(row, 0, c.sectionCell("User")) + c.SetCell(row, 1, c.infoCell(cluster.UserName())) + row++ + + return row +} + +func (c *ClusterInfo) initVersion(row int, version string, cluster *model.Cluster) int { + c.SetCell(row, 0, c.sectionCell("K9s Rev")) + c.SetCell(row, 1, c.infoCell(version)) + row++ + + c.SetCell(row, 0, c.sectionCell("K8s Rev")) + c.SetCell(row, 1, c.infoCell(cluster.Version())) + row++ + + return row +} + +func (c *ClusterInfo) sectionCell(t string) *tview.TableCell { + cell := tview.NewTableCell(t + ":") + cell.SetAlign(tview.AlignLeft) + var s tcell.Style + cell.SetStyle(s.Bold(true).Foreground(config.AsColor(c.styles.K9s.Info.SectionColor))) + cell.SetBackgroundColor(c.app.Styles.BgColor()) + + return cell +} + +func (c *ClusterInfo) infoCell(t string) *tview.TableCell { + cell := tview.NewTableCell(t) + cell.SetExpansion(2) + cell.SetTextColor(config.AsColor(c.styles.K9s.Info.FgColor)) + cell.SetBackgroundColor(c.app.Styles.BgColor()) + + return cell +} + +func (c *ClusterInfo) refresh() { + var ( + cluster = model.NewCluster(c.app.Conn(), c.mxs) + row int + ) + + c.GetCell(row, 1).SetText(cluster.ContextName()) + row++ + c.GetCell(row, 1).SetText(cluster.ClusterName()) + row++ + c.GetCell(row, 1).SetText(cluster.UserName()) + row += 2 + c.GetCell(row, 1).SetText(cluster.Version()) + row++ + + cell := c.GetCell(row, 1) + cell.SetText(render.NAValue) + cell = c.GetCell(row+1, 1) + cell.SetText(render.NAValue) + + c.refreshMetrics(cluster, row) + c.updateStyle() +} + +func (c *ClusterInfo) updateStyle() { + for row := 0; row < c.GetRowCount(); row++ { + c.GetCell(row, 0).SetTextColor(config.AsColor(c.styles.K9s.Info.FgColor)) + c.GetCell(row, 0).SetBackgroundColor(c.styles.BgColor()) + var s tcell.Style + c.GetCell(row, 1).SetStyle(s.Bold(true).Foreground(config.AsColor(c.styles.K9s.Info.SectionColor))) + } +} + +func fetchResources(app *App) (*v1.NodeList, *mv1beta1.NodeMetricsList, error) { + nos, err := app.factory.Client().DialOrDie().CoreV1().Nodes().List(metav1.ListOptions{}) + if err != nil { + return nil, nil, err + } + + mx := client.NewMetricsServer(app.factory.Client()) + nmx, err := mx.FetchNodesMetrics() + if err != nil { + return nil, nil, err + } + + return nos, nmx, nil +} + +func (c *ClusterInfo) refreshMetrics(cluster *model.Cluster, row int) { + nos, nmx, err := fetchResources(c.app) + if err != nil { + log.Warn().Msgf("NodeMetrics %#v", err) + return + } + + var cmx client.ClusterMetrics + if err := cluster.Metrics(nos, nmx, &cmx); err != nil { + log.Error().Err(err).Msgf("failed to retrieve cluster metrics") + } + cell := c.GetCell(row, 1) + cpu := render.AsPerc(cmx.PercCPU) + if cpu == "0" { + cpu = render.NAValue + } + cell.SetText(cpu + "%" + ui.Deltas(strip(cell.Text), cpu)) + row++ + + cell = c.GetCell(row, 1) + mem := render.AsPerc(cmx.PercMEM) + if mem == "0" { + mem = render.NAValue + } + cell.SetText(mem + "%" + ui.Deltas(strip(cell.Text), mem)) +} + +func strip(s string) string { + t := strings.Replace(s, ui.PlusSign, "", 1) + t = strings.Replace(t, ui.MinusSign, "", 1) + return t +} diff --git a/internal/view/command.go b/internal/view/command.go new file mode 100644 index 00000000..43dd661b --- /dev/null +++ b/internal/view/command.go @@ -0,0 +1,162 @@ +package view + +import ( + "fmt" + "regexp" + "strings" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/model" + "github.com/rs/zerolog/log" +) + +var customViewers MetaViewers + +type Command struct { + app *App + + alias *dao.Alias +} + +func NewCommand(app *App) *Command { + return &Command{ + app: app, + } +} + +func (c *Command) Init() error { + c.alias = dao.NewAlias(c.app.factory) + if _, err := c.alias.Ensure(); err != nil { + return err + } + customViewers = loadCustomViewers() + + return nil +} + +// Reset resets Command and reload aliases. +func (c *Command) Reset() error { + c.alias.Clear() + if _, err := c.alias.Ensure(); err != nil { + return err + } + + return nil +} + +func (c *Command) defaultCmd() error { + return c.run(c.app.Config.ActiveView(), true) +} + +var canRX = regexp.MustCompile(`\Acan\s([u|g|s]):([\w-:]+)\b`) + +func (c *Command) specialCmd(cmd string) bool { + cmds := strings.Split(cmd, " ") + switch cmds[0] { + case "q", "Q", "quit": + c.app.BailOut() + return true + case "?", "h", "help": + c.app.helpCmd(nil) + return true + case "a", "alias": + c.app.aliasCmd(nil) + return true + default: + if !canRX.MatchString(cmd) { + return false + } + 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])); err != nil { + log.Error().Err(err).Msgf("policy view load failed") + return false + } + return true + } + } + return false +} + +func (c *Command) viewMetaFor(cmd string) (string, *MetaViewer, error) { + gvr, ok := c.alias.Get(cmd) + if !ok { + return "", nil, fmt.Errorf("Huh? `%s` Command not found", cmd) + } + + v, ok := customViewers[client.GVR(gvr)] + if !ok { + return gvr, &MetaViewer{viewerFn: NewBrowser}, nil + } + + return gvr, &v, nil +} + +// Exec the Command by showing associated display. +func (c *Command) run(cmd string, clearStack bool) error { + if c.specialCmd(cmd) { + return nil + } + + cmds := strings.Split(cmd, " ") + gvr, v, err := c.viewMetaFor(cmds[0]) + if err != nil { + return err + } + switch cmds[0] { + case "ctx", "context", "contexts": + if len(cmds) == 2 && c.app.switchCtx(cmds[1], true) != nil { + return fmt.Errorf("context switch failed!") + } + view := c.componentFor(gvr, v) + return c.exec(gvr, view, clearStack) + default: + // checks if Command includes a namespace + ns := c.app.Config.ActiveNamespace() + if len(cmds) == 2 { + ns = cmds[1] + } + if !c.app.switchNS(ns) { + return fmt.Errorf("namespace switch failed for ns %q", ns) + } + return c.exec(gvr, c.componentFor(gvr, v), clearStack) + } +} + +func (c *Command) componentFor(gvr string, v *MetaViewer) ResourceViewer { + var view ResourceViewer + if v.viewerFn != nil { + log.Debug().Msgf("Custom viewer for %s", gvr) + view = v.viewerFn(client.GVR(gvr)) + } else { + log.Debug().Msgf("Generic viewer for %s", gvr) + view = NewBrowser(client.GVR(gvr)) + } + + if v.enterFn != nil { + log.Debug().Msgf("SETTING CUSTOM ENTER ON %s", gvr) + view.GetTable().SetEnterFn(v.enterFn) + } + + return view +} + +func (c *Command) exec(gvr string, comp model.Component, clearStack bool) error { + if comp == nil { + return fmt.Errorf("No component given for %s", gvr) + } + + g := client.GVR(gvr) + c.app.Flash().Infof("Viewing %s resource...", g.ToR()) + log.Debug().Msgf("Running Command %s", gvr) + c.app.Config.SetActiveView(g.ToR()) + if err := c.app.Config.Save(); err != nil { + log.Error().Err(err).Msg("Config save failed!") + } + if clearStack { + c.app.Content.Stack.ClearHistory() + } + + return c.app.inject(comp) +} diff --git a/internal/view/container.go b/internal/view/container.go new file mode 100644 index 00000000..6664c42a --- /dev/null +++ b/internal/view/container.go @@ -0,0 +1,166 @@ +package view + +import ( + "errors" + "fmt" + "strings" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/k9s/internal/ui/dialog" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" + "k8s.io/client-go/tools/portforward" +) + +const containerTitle = "Containers" + +// Container represents a container view. +type Container struct { + ResourceViewer +} + +// New Container returns a new container view. +func NewContainer(gvr client.GVR) ResourceViewer { + c := Container{} + c.ResourceViewer = NewLogsExtender(NewBrowser(gvr), c.selectedContainer) + c.SetEnvFn(c.k9sEnv) + c.GetTable().SetEnterFn(c.viewLogs) + c.GetTable().SetColorerFn(render.Container{}.ColorerFunc()) + c.SetBindKeysFn(c.bindKeys) + + return &c +} + +// Name returns the component name. +func (c *Container) Name() string { return containerTitle } + +func (c *Container) bindKeys(aa ui.KeyActions) { + aa.Delete(tcell.KeyCtrlSpace, ui.KeySpace) + aa.Add(ui.KeyActions{ + ui.KeyShiftF: ui.NewKeyAction("PortForward", c.portFwdCmd, true), + ui.KeyS: ui.NewKeyAction("Shell", c.shellCmd, true), + ui.KeyShiftC: ui.NewKeyAction("Sort CPU", c.GetTable().SortColCmd(6, false), false), + ui.KeyShiftM: ui.NewKeyAction("Sort MEM", c.GetTable().SortColCmd(7, false), false), + ui.KeyShiftX: ui.NewKeyAction("Sort CPU%", c.GetTable().SortColCmd(8, false), false), + ui.KeyShiftZ: ui.NewKeyAction("Sort MEM%", c.GetTable().SortColCmd(9, false), false), + }) +} + +func (c *Container) k9sEnv() K9sEnv { + env := defaultK9sEnv(c.App(), c.GetTable().GetSelectedItem(), c.GetTable().GetSelectedRow()) + ns, n := client.Namespaced(c.GetTable().Path) + env["POD"] = n + env["NAMESPACE"] = ns + + return env +} + +func (c *Container) selectedContainer() string { + tokens := strings.Split(c.GetTable().GetSelectedItem(), "/") + return tokens[0] +} + +func (c *Container) viewLogs(app *App, ns, res, path string) { + status := c.GetTable().GetSelectedCell(3) + if status != "Running" && status != "Completed" { + app.Flash().Err(errors.New("No logs available")) + return + } + c.ResourceViewer.(*LogsExtender).showLogs(c.GetTable().Path, false) +} + +// Handlers... + +func (c *Container) shellCmd(evt *tcell.EventKey) *tcell.EventKey { + sel := c.GetTable().GetSelectedItem() + if sel == "" { + return evt + } + + c.Stop() + defer c.Start() + shellIn(c.App(), c.GetTable().Path, sel) + + return nil +} + +func (c *Container) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey { + sel := c.GetTable().GetSelectedItem() + if sel == "" { + return evt + } + + if _, ok := c.App().factory.ForwarderFor(fwFQN(c.GetTable().Path, sel)); ok { + c.App().Flash().Err(fmt.Errorf("A PortForward already exist on container %s", c.GetTable().Path)) + return nil + } + + state := c.GetTable().GetSelectedCell(3) + if state != "Running" { + c.App().Flash().Err(fmt.Errorf("Container %s is not running?", sel)) + return nil + } + + portC := c.GetTable().GetSelectedCell(11) + ports := strings.Split(portC, ",") + if len(ports) == 0 { + c.App().Flash().Err(errors.New("Container exposes no ports")) + return nil + } + + var port string + for _, p := range ports { + log.Debug().Msgf("Checking port %q", p) + if !isTCPPort(p) { + continue + } + port = strings.TrimSpace(p) + tokens := strings.Split(port, ":") + if len(tokens) == 2 { + port = tokens[1] + } + break + } + if port == "" { + c.App().Flash().Warn("No valid TCP port found on this container. User will specify...") + port = "MY_TCP_PORT!" + } + dialog.ShowPortForward(c.App().Content.Pages, port, c.portForward) + + return nil +} + +func (c *Container) portForward(address, lport, cport string) { + co := c.GetTable().GetSelectedCell(0) + pf := dao.NewPortForwarder(c.App().Conn()) + ports := []string{lport + ":" + cport} + fw, err := pf.Start(c.GetTable().Path, co, address, ports) + if err != nil { + c.App().Flash().Err(err) + return + } + + log.Debug().Msgf(">>> Starting port forward %q %v", c.GetTable().Path, ports) + go c.runForward(pf, fw) +} + +func (c *Container) runForward(pf *dao.PortForwarder, f *portforward.PortForwarder) { + c.App().QueueUpdateDraw(func() { + c.App().factory.AddForwarder(pf) + c.App().Flash().Infof("PortForward activated %s:%s", pf.Path(), pf.Ports()[0]) + dialog.DismissPortForward(c.App().Content.Pages) + }) + + pf.SetActive(true) + if err := f.ForwardPorts(); err != nil { + c.App().Flash().Err(err) + return + } + c.App().QueueUpdateDraw(func() { + c.App().factory.DeleteForwarder(pf.FQN()) + pf.SetActive(false) + }) +} diff --git a/internal/view/container_test.go b/internal/view/container_test.go new file mode 100644 index 00000000..1893a9b7 --- /dev/null +++ b/internal/view/container_test.go @@ -0,0 +1,17 @@ +package view_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/view" + "github.com/stretchr/testify/assert" +) + +func TestContainerNew(t *testing.T) { + c := view.NewContainer(client.GVR("containers")) + + assert.Nil(t, c.Init(makeCtx())) + assert.Equal(t, "Containers", c.Name()) + assert.Equal(t, 10, len(c.Hints())) +} diff --git a/internal/view/context.go b/internal/view/context.go new file mode 100644 index 00000000..eb2409c2 --- /dev/null +++ b/internal/view/context.go @@ -0,0 +1,66 @@ +package view + +import ( + "errors" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" +) + +// Context presents a context viewer. +type Context struct { + ResourceViewer +} + +// NewContext returns a new viewer. +func NewContext(gvr client.GVR) ResourceViewer { + c := Context{ + ResourceViewer: NewBrowser(gvr), + } + c.GetTable().SetEnterFn(c.useCtx) + c.GetTable().SetColorerFn(render.Context{}.ColorerFunc()) + c.SetBindKeysFn(c.bindKeys) + + return &c +} + +func (c *Context) bindKeys(aa ui.KeyActions) { + aa.Delete(ui.KeyShiftA, tcell.KeyCtrlSpace, ui.KeySpace) +} + +func (c *Context) useCtx(app *App, _, res, path string) { + log.Debug().Msgf("SWITCH CTX %q--%q", res, path) + if err := c.useContext(path); err != nil { + app.Flash().Err(err) + return + } + if err := app.gotoResource("po", true); err != nil { + app.Flash().Err(err) + } +} + +func (c *Context) useContext(name string) error { + res, err := dao.AccessorFor(c.App().factory, client.GVR(c.GVR())) + if err != nil { + return nil + } + + switcher, ok := res.(dao.Switchable) + if !ok { + return errors.New("Expecting a switchable resource") + } + if err := switcher.Switch(name); err != nil { + return err + } + if err := c.App().switchCtx(name, false); err != nil { + return err + } + c.Refresh() + c.GetTable().Select(1, 0) + + return nil +} diff --git a/internal/view/context_test.go b/internal/view/context_test.go new file mode 100644 index 00000000..8c7da491 --- /dev/null +++ b/internal/view/context_test.go @@ -0,0 +1,17 @@ +package view_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/view" + "github.com/stretchr/testify/assert" +) + +func TestContext(t *testing.T) { + ctx := view.NewContext(client.GVR("contexts")) + + assert.Nil(t, ctx.Init(makeCtx())) + assert.Equal(t, "Contexts", ctx.Name()) + assert.Equal(t, 1, len(ctx.Hints())) +} diff --git a/internal/view/cronjob.go b/internal/view/cronjob.go new file mode 100644 index 00000000..ca6d9ae9 --- /dev/null +++ b/internal/view/cronjob.go @@ -0,0 +1,93 @@ +package view + +import ( + "context" + "fmt" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" + batchv1beta1 "k8s.io/api/batch/v1beta1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" +) + +// CronJob represents a cronjob viewer. +type CronJob struct { + ResourceViewer +} + +// NewCronJob returns a new viewer. +func NewCronJob(gvr client.GVR) ResourceViewer { + c := CronJob{ResourceViewer: NewBrowser(gvr)} + c.SetBindKeysFn(c.bindKeys) + c.GetTable().SetEnterFn(c.showJobs) + c.GetTable().SetColorerFn(render.CronJob{}.ColorerFunc()) + + return &c +} + +func (c *CronJob) showJobs(app *App, ns, gvr, path string) { + log.Debug().Msgf("Showing Jobs %q:%q -- %q", ns, gvr, path) + o, err := app.factory.Get(gvr, path, labels.Everything()) + if err != nil { + app.Flash().Err(err) + return + } + + var cj batchv1beta1.CronJob + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cj) + if err != nil { + app.Flash().Err(err) + return + } + + v := NewJob(client.GVR("batch/v1/jobs")) + v.SetContextFn(jobCtx(path, string(cj.UID))) + if err := app.inject(v); err != nil { + app.Flash().Err(err) + } +} + +func jobCtx(path, uid string) ContextFunc { + return func(ctx context.Context) context.Context { + ctx = context.WithValue(ctx, internal.KeyPath, path) + return context.WithValue(ctx, internal.KeyUID, uid) + } +} + +func (c *CronJob) bindKeys(aa ui.KeyActions) { + aa.Add(ui.KeyActions{ + tcell.KeyCtrlT: ui.NewKeyAction("Trigger", c.trigger, true), + }) +} + +func (c *CronJob) trigger(evt *tcell.EventKey) *tcell.EventKey { + sel := c.GetTable().GetSelectedItem() + if sel == "" { + return evt + } + + res, err := dao.AccessorFor(c.App().factory, client.GVR(c.GVR())) + if err != nil { + return nil + } + runner, ok := res.(dao.Runnable) + if !ok { + c.App().Flash().Err(fmt.Errorf("expecting a jobrunner resource for %q", c.GVR())) + return nil + } + + if err := runner.Run(sel); err != nil { + c.App().Flash().Errf("Cronjob trigger failed %v", err) + return evt + } + c.App().Flash().Infof("Triggering Job %s %s", c.GVR(), sel) + + return nil +} diff --git a/internal/view/details.go b/internal/view/details.go new file mode 100644 index 00000000..21efa517 --- /dev/null +++ b/internal/view/details.go @@ -0,0 +1,144 @@ +package view + +import ( + "context" + "fmt" + + "github.com/atotto/clipboard" + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/tview" + "github.com/gdamore/tcell" +) + +const detailsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-] " + +// Details represents a generic text viewer. +type Details struct { + *tview.TextView + + actions ui.KeyActions + app *App + title, subject string + buff string +} + +// NewDetails returns a details viewer. +func NewDetails(title string) *Details { + return &Details{ + TextView: tview.NewTextView(), + title: title, + actions: make(ui.KeyActions), + } +} + +// Init initializes the viewer. +func (d *Details) Init(ctx context.Context) error { + var err error + if d.app, err = extractApp(ctx); err != nil { + return err + } + + if d.title != "" { + d.SetBorder(true) + } + d.SetBackgroundColor(d.app.Styles.BgColor()) + d.SetTextColor(d.app.Styles.FgColor()) + d.SetScrollable(true) + d.SetWrap(true) + d.SetDynamicColors(true) + d.SetBorderFocusColor(config.AsColor(d.app.Styles.Frame().Border.FocusColor)) + d.SetHighlightColor(tcell.ColorOrange) + d.SetTitleColor(tcell.ColorAqua) + d.SetInputCapture(d.keyboard) + d.bindKeys() + d.SetChangedFunc(func() { + d.app.Draw() + }) + d.updateTitle() + d.app.Styles.AddListener(d) + + return nil +} + +func (d *Details) StylesChanged(s *config.Styles) { + d.SetBackgroundColor(d.app.Styles.BgColor()) + d.SetTextColor(d.app.Styles.FgColor()) + d.Update(d.buff) +} + +func (d *Details) Update(buff string) { + d.buff = buff + d.SetText(colorizeYAML(d.app.Styles.Views().Yaml, buff)) + d.ScrollToBeginning() +} + +func (d *Details) Actions() ui.KeyActions { + return d.actions +} + +// Name returns the component name. +func (d *Details) Name() string { return d.title } + +// Start starts the view updater. +func (d *Details) Start() {} + +// Stop terminates the updater. +func (d *Details) Stop() { + d.app.Styles.RemoveListener(d) +} + +// Hints returns menu hints. +func (d *Details) Hints() model.MenuHints { + return d.actions.Hints() +} + +func (d *Details) bindKeys() { + d.actions.Set(ui.KeyActions{ + tcell.KeyEscape: ui.NewKeyAction("Back", d.app.PrevCmd, false), + tcell.KeyCtrlS: ui.NewKeyAction("Save", d.saveCmd, false), + ui.KeyC: ui.NewKeyAction("Copy", d.cpCmd, true), + }) +} + +func (d *Details) keyboard(evt *tcell.EventKey) *tcell.EventKey { + key := evt.Key() + if key == tcell.KeyRune { + key = tcell.Key(evt.Rune()) + } + + if a, ok := d.actions[key]; ok { + return a.Action(evt) + } + return evt +} + +func (d *Details) saveCmd(evt *tcell.EventKey) *tcell.EventKey { + if path, err := saveYAML(d.app.Config.K9s.CurrentCluster, d.title, d.GetText(true)); err != nil { + d.app.Flash().Err(err) + } else { + d.app.Flash().Infof("Log %s saved successfully!", path) + } + return nil +} + +func (d *Details) cpCmd(evt *tcell.EventKey) *tcell.EventKey { + d.app.Flash().Info("Content copied to clipboard...") + if err := clipboard.WriteAll(d.GetText(true)); err != nil { + d.app.Flash().Err(err) + } + return nil +} + +func (d *Details) SetSubject(s string) { + d.subject = s +} + +func (d *Details) updateTitle() { + if d.title == "" { + return + } + title := ui.SkinTitle(fmt.Sprintf(detailsTitleFmt, d.title, d.subject), d.app.Styles.Frame()) + d.SetTitle(title) +} diff --git a/internal/view/dp.go b/internal/view/dp.go new file mode 100644 index 00000000..ec6f169f --- /dev/null +++ b/internal/view/dp.go @@ -0,0 +1,70 @@ +package view + +import ( + "strings" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" +) + +const scaleDialogKey = "scale" + +// Deploy represents a deployment view. +type Deploy struct { + ResourceViewer +} + +// NewDeploy returns a new deployment view. +func NewDeploy(gvr client.GVR) ResourceViewer { + d := Deploy{ + ResourceViewer: NewRestartExtender( + NewScaleExtender(NewLogsExtender(NewBrowser(gvr), nil)), + ), + } + d.SetBindKeysFn(d.bindKeys) + d.GetTable().SetEnterFn(d.showPods) + d.GetTable().SetColorerFn(render.Deployment{}.ColorerFunc()) + + return &d +} + +func (d *Deploy) bindKeys(aa ui.KeyActions) { + aa.Add(ui.KeyActions{ + ui.KeyShiftD: ui.NewKeyAction("Sort Desired", d.GetTable().SortColCmd(1, true), false), + ui.KeyShiftC: ui.NewKeyAction("Sort Current", d.GetTable().SortColCmd(2, true), false), + }) +} + +func (d *Deploy) showPods(app *App, _, _, path string) { + o, err := app.factory.Get(d.GVR(), path, labels.Everything()) + if err != nil { + app.Flash().Err(err) + return + } + + var dp appsv1.Deployment + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &dp) + if err != nil { + app.Flash().Err(err) + } + + showPodsFromSelector(app, strings.Replace(path, "/", "::", 1), dp.Spec.Selector) +} + +// Helpers... + +func showPodsFromSelector(app *App, path string, sel *metav1.LabelSelector) { + l, err := metav1.LabelSelectorAsSelector(sel) + if err != nil { + app.Flash().Err(err) + return + } + + showPods(app, path, l.String(), "") +} diff --git a/internal/view/dp_test.go b/internal/view/dp_test.go new file mode 100644 index 00000000..0ece1e38 --- /dev/null +++ b/internal/view/dp_test.go @@ -0,0 +1,18 @@ +package view_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/view" + "github.com/stretchr/testify/assert" +) + +func TestDeploy(t *testing.T) { + v := view.NewDeploy(client.GVR("apps/v1/deployments")) + + assert.Nil(t, v.Init(makeCtx())) + assert.Equal(t, "Deployments", v.Name()) + assert.Equal(t, 7, len(v.Hints())) + +} diff --git a/internal/view/ds.go b/internal/view/ds.go new file mode 100644 index 00000000..ad48890e --- /dev/null +++ b/internal/view/ds.go @@ -0,0 +1,53 @@ +package view + +import ( + "strings" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" +) + +type DaemonSet struct { + ResourceViewer +} + +func NewDaemonSet(gvr client.GVR) ResourceViewer { + d := DaemonSet{ + ResourceViewer: NewRestartExtender( + NewLogsExtender(NewBrowser(gvr), nil), + ), + } + d.SetBindKeysFn(d.bindKeys) + d.GetTable().SetEnterFn(d.showPods) + d.GetTable().SetColorerFn(render.DaemonSet{}.ColorerFunc()) + + return &d +} + +func (d *DaemonSet) bindKeys(aa ui.KeyActions) { + aa.Add(ui.KeyActions{ + ui.KeyShiftD: ui.NewKeyAction("Sort Desired", d.GetTable().SortColCmd(1, true), false), + ui.KeyShiftC: ui.NewKeyAction("Sort Current", d.GetTable().SortColCmd(2, true), false), + }) +} + +func (d *DaemonSet) showPods(app *App, _, _, path string) { + o, err := app.factory.Get(d.GVR(), path, labels.Everything()) + if err != nil { + d.App().Flash().Err(err) + return + } + + var ds appsv1.DaemonSet + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds) + if err != nil { + d.App().Flash().Err(err) + } + + showPodsFromSelector(app, strings.Replace(path, "/", "::", 1), ds.Spec.Selector) +} diff --git a/internal/view/ds_test.go b/internal/view/ds_test.go new file mode 100644 index 00000000..df5b67c9 --- /dev/null +++ b/internal/view/ds_test.go @@ -0,0 +1,17 @@ +package view_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/view" + "github.com/stretchr/testify/assert" +) + +func TestDaemonSet(t *testing.T) { + v := view.NewDaemonSet(client.GVR("apps/v1/daemonsets")) + + assert.Nil(t, v.Init(makeCtx())) + assert.Equal(t, "DaemonSets", v.Name()) + assert.Equal(t, 6, len(v.Hints())) +} diff --git a/internal/views/env.go b/internal/view/env.go similarity index 97% rename from internal/views/env.go rename to internal/view/env.go index 71ed834a..92988183 100644 --- a/internal/views/env.go +++ b/internal/view/env.go @@ -1,4 +1,4 @@ -package views +package view import ( "fmt" diff --git a/internal/views/env_test.go b/internal/view/env_test.go similarity index 92% rename from internal/views/env_test.go rename to internal/view/env_test.go index 10aadd76..cff67da4 100644 --- a/internal/views/env_test.go +++ b/internal/view/env_test.go @@ -1,4 +1,4 @@ -package views +package view import ( "errors" @@ -27,7 +27,8 @@ func TestK9sEnv(t *testing.T) { "COL0": "fred", } - for k, u := range uu { + for k := range uu { + u := uu[k] t.Run(k, func(t *testing.T) { a, err := e.envFor(u.q) assert.Equal(t, u.err, err) diff --git a/internal/view/event.go b/internal/view/event.go new file mode 100644 index 00000000..58ffa82b --- /dev/null +++ b/internal/view/event.go @@ -0,0 +1,28 @@ +package view + +import ( + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + "github.com/gdamore/tcell" +) + +// Event represents a command alias view. +type Event struct { + ResourceViewer +} + +// NewEvent returns a new alias view. +func NewEvent(gvr client.GVR) ResourceViewer { + e := Event{ + ResourceViewer: NewBrowser(gvr), + } + e.GetTable().SetColorerFn(render.Event{}.ColorerFunc()) + e.SetBindKeysFn(e.bindKeys) + + return &e +} + +func (e *Event) bindKeys(aa ui.KeyActions) { + aa.Delete(tcell.KeyCtrlD, ui.KeyE) +} diff --git a/internal/views/exec.go b/internal/view/exec.go similarity index 86% rename from internal/views/exec.go rename to internal/view/exec.go index e6fda318..dc5e39e2 100644 --- a/internal/views/exec.go +++ b/internal/view/exec.go @@ -1,4 +1,4 @@ -package views +package view import ( "context" @@ -13,7 +13,7 @@ import ( "github.com/rs/zerolog/log" ) -func runK(clear bool, app *appView, args ...string) bool { +func runK(clear bool, app *App, args ...string) bool { bin, err := exec.LookPath("kubectl") if err != nil { log.Error().Msgf("Unable to find kubectl command in path %v", err) @@ -23,7 +23,10 @@ func runK(clear bool, app *appView, args ...string) bool { return run(clear, app, bin, false, args...) } -func run(clear bool, app *appView, bin string, bg bool, args ...string) bool { +func run(clear bool, app *App, bin string, bg bool, args ...string) bool { + app.Halt() + defer app.Resume() + return app.Suspend(func() { if err := execute(clear, bin, bg, args...); err != nil { app.Flash().Errf("Command exited: %v", err) @@ -31,7 +34,7 @@ func run(clear bool, app *appView, bin string, bg bool, args ...string) bool { }) } -func edit(clear bool, app *appView, args ...string) bool { +func edit(clear bool, app *App, args ...string) bool { bin, err := exec.LookPath(os.Getenv("EDITOR")) if err != nil { log.Error().Msgf("Unable to find editor command in path %v", err) diff --git a/internal/view/group.go b/internal/view/group.go new file mode 100644 index 00000000..f04be749 --- /dev/null +++ b/internal/view/group.go @@ -0,0 +1,50 @@ +package view + +import ( + "context" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + "github.com/gdamore/tcell" +) + +// Group presents a RBAC group viewer. +type Group struct { + ResourceViewer +} + +// NewGroup returns a new subject viewer. +func NewGroup(gvr client.GVR) ResourceViewer { + g := Group{ResourceViewer: NewBrowser(gvr)} + g.GetTable().SetColorerFn(render.Subject{}.ColorerFunc()) + g.SetBindKeysFn(g.bindKeys) + g.SetContextFn(g.subjectCtx) + + return &g +} + +func (g *Group) bindKeys(aa ui.KeyActions) { + aa.Delete(ui.KeyShiftA, ui.KeyShiftP, tcell.KeyCtrlSpace, ui.KeySpace) + aa.Add(ui.KeyActions{ + tcell.KeyEnter: ui.NewKeyAction("Rules", g.policyCmd, true), + ui.KeyShiftK: ui.NewKeyAction("Sort Kind", g.GetTable().SortColCmd(1, true), false), + }) +} + +func (g *Group) subjectCtx(ctx context.Context) context.Context { + return context.WithValue(ctx, internal.KeySubjectKind, "Group") +} + +func (g *Group) policyCmd(evt *tcell.EventKey) *tcell.EventKey { + path := g.GetTable().GetSelectedItem() + if path == "" { + return evt + } + if err := g.App().inject(NewPolicy(g.App(), "Group", path)); err != nil { + g.App().Flash().Err(err) + } + + return nil +} diff --git a/internal/view/help.go b/internal/view/help.go new file mode 100644 index 00000000..768c789f --- /dev/null +++ b/internal/view/help.go @@ -0,0 +1,303 @@ +package view + +import ( + "context" + "fmt" + "runtime" + "sort" + "strconv" + "strings" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/tview" + "github.com/gdamore/tcell" +) + +const ( + helpTitle = "Help" + helpTitleFmt = " [aqua::b]%s " +) + +// Help presents a help viewer. +type Help struct { + *Table +} + +// NewHelp returns a new help viewer. +func NewHelp() *Help { + return &Help{ + Table: NewTable(helpTitle), + } +} + +// Init initializes the component. +func (v *Help) Init(ctx context.Context) error { + if err := v.Table.Init(ctx); err != nil { + return nil + } + v.SetSelectable(false, false) + v.resetTitle() + v.SetBorder(true) + v.SetBorderPadding(0, 0, 1, 1) + v.bindKeys() + v.build(v.app.Content.Top().Hints()) + v.SetBackgroundColor(v.App().Styles.BgColor()) + + return nil +} + +func (v *Help) bindKeys() { + v.Actions().Delete(ui.KeySpace, tcell.KeyCtrlSpace, tcell.KeyCtrlS) + v.Actions().Set(ui.KeyActions{ + tcell.KeyEsc: ui.NewKeyAction("Back", v.app.PrevCmd, false), + ui.KeyHelp: ui.NewKeyAction("Back", v.app.PrevCmd, false), + tcell.KeyEnter: ui.NewKeyAction("Back", v.app.PrevCmd, false), + }) +} + +func (v *Help) showHelp() model.MenuHints { + return model.MenuHints{ + { + Mnemonic: "?", + Description: "Help", + }, + { + Mnemonic: "Ctrl-a", + Description: "Aliases", + }, + } +} + +func (v *Help) showNav() model.MenuHints { + return model.MenuHints{ + { + Mnemonic: "g", + Description: "Goto Top", + }, + { + Mnemonic: "Shift-g", + Description: "Goto Bottom", + }, + { + Mnemonic: "Ctrl-b", + Description: "Page Down"}, + { + Mnemonic: "Ctrl-f", + Description: "Page Up", + }, + { + Mnemonic: "h", + Description: "Left", + }, + { + Mnemonic: "l", + Description: "Right", + }, + { + Mnemonic: "k", + Description: "Up", + }, + { + Mnemonic: "j", + Description: "Down", + }, + } +} + +func (v *Help) showHotKeys() (model.MenuHints, error) { + hh := config.NewHotKeys() + if err := hh.Load(); err != nil { + return nil, fmt.Errorf("no hotkey configuration found") + } + kk := make(sort.StringSlice, 0, len(hh.HotKey)) + for k := range hh.HotKey { + kk = append(kk, k) + } + kk.Sort() + mm := make(model.MenuHints, 0, len(hh.HotKey)) + for _, k := range kk { + mm = append(mm, model.MenuHint{ + Mnemonic: hh.HotKey[k].ShortCut, + Description: hh.HotKey[k].Description, + }) + } + + return mm, nil +} + +func (v *Help) showGeneral() model.MenuHints { + return model.MenuHints{ + { + Mnemonic: ":cmd", + Description: "Command mode", + }, + { + Mnemonic: "/term", + Description: "Filter mode", + }, + { + Mnemonic: "esc", + Description: "Back/Clear", + }, + { + Mnemonic: "tab", + Description: "Next Field", + }, + { + Mnemonic: "backtab", + Description: "Previous Field", + }, + { + Mnemonic: "Ctrl-r", + Description: "Refresh", + }, + { + Mnemonic: "Ctrl-u", + Description: "Clear command", + }, + { + Mnemonic: "h", + Description: "Toggle Header", + }, + { + Mnemonic: ":q", + Description: "Quit", + }, + { + Mnemonic: "space", + Description: "Mark", + }, + { + Mnemonic: "Ctrl-space", + Description: "Clear Marks", + }, + { + Mnemonic: "Ctrl-s", + Description: "Save", + }, + } +} + +func (v *Help) resetTitle() { + v.SetTitle(fmt.Sprintf(helpTitleFmt, helpTitle)) +} + +func (v *Help) build(hh model.MenuHints) { + v.Clear() + sort.Sort(hh) + + var col int + v.addSection(col, "RESOURCE", hh) + col += 2 + v.addSection(col, "GENERAL", v.showGeneral()) + col += 2 + v.addSection(col, "NAVIGATION", v.showNav()) + col += 2 + if h, err := v.showHotKeys(); err == nil { + v.addSection(col, "HOTKEYS", h) + col += 2 + } + v.addSection(col, "HELP", v.showHelp()) +} + +func (v *Help) addSpacer(c int) { + cell := tview.NewTableCell("") + cell.SetBackgroundColor(v.App().Styles.BgColor()) + cell.SetExpansion(1) + v.SetCell(0, c, cell) +} + +func (v *Help) addSection(c int, title string, hh model.MenuHints) { + row := 0 + v.addSpacer(c) + cell := tview.NewTableCell(title) + cell.SetTextColor(tcell.ColorGreen) + cell.SetAttributes(tcell.AttrBold) + cell.SetExpansion(1) + cell.SetAlign(tview.AlignLeft) + v.SetCell(row, c+1, cell) + row++ + + for _, h := range hh { + col := c + cell := tview.NewTableCell(toMnemonic(h.Mnemonic)) + if _, err := strconv.Atoi(h.Mnemonic); err != nil { + cell.SetTextColor(tcell.ColorDodgerBlue) + } else { + cell.SetTextColor(tcell.ColorFuchsia) + } + cell.SetAttributes(tcell.AttrBold) + cell.SetAlign(tview.AlignRight) + v.SetCell(row, col, cell) + col++ + cell = tview.NewTableCell(h.Description) + cell.SetTextColor(tcell.ColorWhite) + v.SetCell(row, col, cell) + row++ + } +} + +func toMnemonic(s string) string { + if len(s) == 0 { + return s + } + + return "<" + keyConv(strings.ToLower(s)) + ">" +} + +func keyConv(s string) string { + if !strings.Contains(s, "alt") { + return s + } + + if runtime.GOOS != "darwin" { + return s + } + + return strings.Replace(s, "alt", "opt", 1) +} + +func defaultK9sEnv(app *App, sel string, row render.Row) K9sEnv { + ns, n := client.Namespaced(sel) + ctx, err := app.Conn().Config().CurrentContextName() + if err != nil { + ctx = render.NAValue + } + cluster, err := app.Conn().Config().CurrentClusterName() + if err != nil { + cluster = render.NAValue + } + user, err := app.Conn().Config().CurrentUserName() + if err != nil { + user = render.NAValue + } + groups, err := app.Conn().Config().CurrentGroupNames() + if err != nil { + groups = []string{render.NAValue} + } + var cfg string + kcfg := app.Conn().Config().Flags().KubeConfig + if kcfg != nil && *kcfg != "" { + cfg = *kcfg + } + + env := K9sEnv{ + "NAMESPACE": ns, + "NAME": n, + "CONTEXT": ctx, + "CLUSTER": cluster, + "USER": user, + "GROUPS": strings.Join(groups, ","), + "KUBECONFIG": cfg, + } + + for i, r := range row.Fields { + env["COL"+strconv.Itoa(i)] = r + } + + return env +} diff --git a/internal/view/help_test.go b/internal/view/help_test.go new file mode 100644 index 00000000..692f6e75 --- /dev/null +++ b/internal/view/help_test.go @@ -0,0 +1,27 @@ +package view_test + +import ( + "testing" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/view" + "github.com/stretchr/testify/assert" +) + +func TestHelp(t *testing.T) { + ctx := makeCtx() + + app := ctx.Value(internal.KeyApp).(*view.App) + po := view.NewPod(client.GVR("v1/pods")) + po.Init(ctx) + app.Content.Push(po) + + v := view.NewHelp() + + assert.Nil(t, v.Init(ctx)) + assert.Equal(t, 16, v.GetRowCount()) + assert.Equal(t, 10, v.GetColumnCount()) + assert.Equal(t, "", v.GetCell(1, 0).Text) + assert.Equal(t, "Kill", v.GetCell(1, 1).Text) +} diff --git a/internal/view/helpers.go b/internal/view/helpers.go new file mode 100644 index 00000000..c5fd11c1 --- /dev/null +++ b/internal/view/helpers.go @@ -0,0 +1,108 @@ +package view + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/render" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" +) + +func showPodsWithLabels(app *App, path string, sel map[string]string) { + log.Debug().Msgf("SHOWING POD FOR %#v", sel) + var labels []string + for k, v := range sel { + labels = append(labels, fmt.Sprintf("%s=%s", k, v)) + } + showPods(app, path, strings.Join(labels, ","), "") +} + +func showPods(app *App, path, labelSel, fieldSel string) { + log.Debug().Msgf("SHOW PODS %q -- %q -- %q", path, labelSel, fieldSel) + app.switchNS("") + + v := NewPod(client.GVR("v1/pods")) + v.SetContextFn(podCtx(path, labelSel, fieldSel)) + v.GetTable().SetColorerFn(render.Pod{}.ColorerFunc()) + + ns, _ := client.Namespaced(path) + if err := app.Config.SetActiveNamespace(ns); err != nil { + log.Error().Err(err).Msg("Config NS set failed!") + } + if err := app.inject(v); err != nil { + app.Flash().Err(err) + } +} + +func podCtx(path, labelSel, fieldSel string) ContextFunc { + return func(ctx context.Context) context.Context { + ctx = context.WithValue(ctx, internal.KeyPath, path) + ctx = context.WithValue(ctx, internal.KeyLabels, labelSel) + return context.WithValue(ctx, internal.KeyFields, fieldSel) + } +} + +func extractApp(ctx context.Context) (*App, error) { + app, ok := ctx.Value(internal.KeyApp).(*App) + if !ok { + return nil, errors.New("No application found in context") + } + + return app, nil +} + +// AsKey maps a string representation of a key to a tcell key. +func asKey(key string) (tcell.Key, error) { + for k, v := range tcell.KeyNames { + if v == key { + return k, nil + } + } + + return 0, fmt.Errorf("No matching key found %s", key) +} + +// FwFQN returns a fully qualified ns/name:container id. +func fwFQN(po, co string) string { + return po + ":" + co +} + +func isTCPPort(p string) bool { + return !strings.Contains(p, "UDP") +} + +// ContainerID computes container ID based on ns/po/co. +func containerID(path, co string) string { + ns, n := client.Namespaced(path) + po := strings.Split(n, "-")[0] + + return ns + "/" + po + ":" + co +} + +// UrlFor computes fq url for a given benchmark configuration. +func urlFor(cfg config.BenchConfig, port string) string { + host := "localhost" + if cfg.HTTP.Host != "" { + host = cfg.HTTP.Host + } + + path := "/" + if cfg.HTTP.Path != "" { + path = cfg.HTTP.Path + } + + return "http://" + host + ":" + port + path +} + +func fqn(ns, n string) string { + if ns == "" { + return n + } + return ns + "/" + n +} diff --git a/internal/views/helpers_test.go b/internal/view/helpers_test.go similarity index 89% rename from internal/views/helpers_test.go rename to internal/view/helpers_test.go index 51eeb785..3b414f83 100644 --- a/internal/views/helpers_test.go +++ b/internal/view/helpers_test.go @@ -1,4 +1,4 @@ -package views +package view import ( "testing" @@ -21,7 +21,8 @@ func TestIsTCPPort(t *testing.T) { "udp": {"80╱UDP", false}, } - for k, u := range uu { + for k := range uu { + u := uu[k] t.Run(k, func(t *testing.T) { assert.Equal(t, u.e, isTCPPort(u.p)) }) @@ -36,7 +37,8 @@ func TestFQN(t *testing.T) { "allNS": {"", "fred", "fred"}, } - for k, u := range uu { + for k := range uu { + u := uu[k] t.Run(k, func(t *testing.T) { assert.Equal(t, u.e, fqn(u.ns, u.n)) }) @@ -75,9 +77,10 @@ func TestUrlFor(t *testing.T) { }, } - for k, u := range uu { + for k := range uu { + u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, urlFor(u.cfg, u.co, u.port)) + assert.Equal(t, u.e, urlFor(u.cfg, u.port)) }) } } @@ -98,7 +101,8 @@ func TestContainerID(t *testing.T) { }, } - for k, u := range uu { + for k := range uu { + u := uu[k] t.Run(k, func(t *testing.T) { assert.Equal(t, u.e, containerID(u.path, u.co)) }) diff --git a/internal/view/job.go b/internal/view/job.go new file mode 100644 index 00000000..ce4b2bff --- /dev/null +++ b/internal/view/job.go @@ -0,0 +1,41 @@ +package view + +import ( + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" + batchv1 "k8s.io/api/batch/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" +) + +// Job represents a job viewer. +type Job struct { + ResourceViewer +} + +// NewJob returns a new viewer. +func NewJob(gvr client.GVR) ResourceViewer { + j := Job{ResourceViewer: NewLogsExtender(NewBrowser(gvr), nil)} + j.GetTable().SetEnterFn(j.showPods) + j.GetTable().SetColorerFn(render.Job{}.ColorerFunc()) + + return &j +} + +func (*Job) showPods(app *App, _, gvr, path string) { + o, err := app.factory.Get(gvr, path, labels.Everything()) + if err != nil { + app.Flash().Err(err) + return + } + + var job batchv1.Job + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &job) + if err != nil { + app.Flash().Err(err) + return + } + + showPodsFromSelector(app, path, job.Spec.Selector) +} diff --git a/internal/view/log.go b/internal/view/log.go new file mode 100644 index 00000000..2074a9d9 --- /dev/null +++ b/internal/view/log.go @@ -0,0 +1,353 @@ +package view + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/tview" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" +) + +const ( + logTitle = "logs" + logBuffSize = 100 + FlushTimeout = 200 * time.Millisecond + + logCoFmt = " Logs([fg:bg:]%s:[hilite:bg:b]%s[-:bg:-]) " + logFmt = " Logs([fg:bg:]%s) " +) + +// Log represents a generic log viewer. +type Log struct { + *tview.Flex + + app *App + logs *Details + indicator *LogIndicator + ansiWriter io.Writer + path, container string + cancelFn context.CancelFunc + previous bool + gvr client.GVR +} + +var _ model.Component = &Log{} + +// NewLog returns a new viewer. +func NewLog(gvr client.GVR, path, co string, prev bool) *Log { + return &Log{ + gvr: gvr, + Flex: tview.NewFlex(), + path: path, + container: co, + previous: prev, + } +} + +// Init initialiazes the viewer. +func (l *Log) Init(ctx context.Context) (err error) { + log.Debug().Msgf(">>> Logs INIT") + if l.app, err = extractApp(ctx); err != nil { + return err + } + + l.SetBorder(true) + l.SetBackgroundColor(config.AsColor(l.app.Styles.Views().Log.BgColor)) + l.SetBorderPadding(0, 0, 1, 1) + l.SetDirection(tview.FlexRow) + + l.indicator = NewLogIndicator(l.app.Styles) + l.AddItem(l.indicator, 1, 1, false) + + l.logs = NewDetails("") + l.logs.SetBorder(false) + l.logs.SetDynamicColors(true) + l.logs.SetTextColor(config.AsColor(l.app.Styles.Views().Log.FgColor)) + l.logs.SetBackgroundColor(config.AsColor(l.app.Styles.Views().Log.BgColor)) + l.logs.SetWrap(true) + l.logs.SetMaxBuffer(l.app.Config.K9s.LogBufferSize) + if err = l.logs.Init(ctx); err != nil { + return err + } + l.ansiWriter = tview.ANSIWriter(l.logs, l.app.Styles.Views().Log.FgColor, l.app.Styles.Views().Log.BgColor) + l.AddItem(l.logs, 0, 1, true) + l.bindKeys() + l.logs.SetInputCapture(l.keyboard) + + return nil +} + +// Hints returns a collection of menu hints. +func (l *Log) Hints() model.MenuHints { + return l.logs.Actions().Hints() +} + +// Start runs the component. +func (l *Log) Start() { + l.Stop() + if err := l.doLoad(); err != nil { + l.app.Flash().Err(err) + l.log("😂 Doh! No logs are available at this time. Check again later on...") + return + } + l.app.SetFocus(l) +} + +// Stop terminates the component. +func (l *Log) Stop() { + if l.cancelFn != nil { + log.Debug().Msgf("<<<< Logger STOP!") + l.cancelFn() + l.cancelFn = nil + } +} + +func (l *Log) Name() string { return logTitle } + +func (l *Log) bindKeys() { + l.logs.Actions().Set(ui.KeyActions{ + tcell.KeyEscape: ui.NewKeyAction("Back", l.app.PrevCmd, true), + ui.KeyC: ui.NewKeyAction("Clear", l.clearCmd, true), + ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.toggleAutoScrollCmd, true), + ui.KeyG: ui.NewKeyAction("Top", l.topCmd, false), + ui.KeyShiftF: ui.NewKeyAction("FullScreen", l.fullScreenCmd, true), + ui.KeyW: ui.NewKeyAction("Toggle Wrap", l.textWrapCmd, true), + ui.KeyShiftG: ui.NewKeyAction("Bottom", l.bottomCmd, false), + ui.KeyF: ui.NewKeyAction("Up", l.pageUpCmd, false), + ui.KeyB: ui.NewKeyAction("Down", l.pageDownCmd, false), + tcell.KeyCtrlS: ui.NewKeyAction("Save", l.SaveCmd, true), + }) +} + +func (l *Log) doLoad() error { + l.logs.Clear() + l.setTitle(l.path, l.container) + + var ctx context.Context + ctx = context.WithValue(context.Background(), internal.KeyFactory, l.app.factory) + ctx, l.cancelFn = context.WithCancel(ctx) + + c := make(chan string, 10) + go l.updateLogs(ctx, c, logBuffSize) + + accessor, err := dao.AccessorFor(l.app.factory, l.gvr) + if err != nil { + return err + } + logger, ok := accessor.(dao.Loggable) + if !ok { + return fmt.Errorf("Resource %s is not tailable", l.gvr) + } + + if err := logger.TailLogs(ctx, c, l.logOpts(l.path, l.container, l.previous)); err != nil { + l.cancelFn() + close(c) + return err + } + + return nil +} + +func (l *Log) logOpts(path, co string, prevLogs bool) dao.LogOptions { + return dao.LogOptions{ + Path: path, + Container: co, + Lines: int64(l.app.Config.K9s.LogRequestSize), + Previous: prevLogs, + } +} + +func (l *Log) updateLogs(ctx context.Context, c <-chan string, buffSize int) { + defer func() { + log.Debug().Msgf("updateLogs view bailing out!") + }() + buff, index := make([]string, buffSize), 0 + for { + select { + case line, ok := <-c: + if !ok { + log.Debug().Msgf("Closed channel detected. Bailing out...") + l.Flush(index, buff) + return + } + if index < buffSize { + buff[index] = line + index++ + continue + } + l.Flush(index, buff) + index = 0 + buff[index] = line + index++ + case <-time.After(FlushTimeout): + l.Flush(index, buff) + index = 0 + case <-ctx.Done(): + return + } + } +} + +// ScrollIndicator returns the scroll mode viewer. +func (l *Log) Indicator() *LogIndicator { + return l.indicator +} + +func (l *Log) setTitle(path, co string) { + var fmat string + if co == "" { + fmat = ui.SkinTitle(fmt.Sprintf(logFmt, path), l.app.Styles.Frame()) + } else { + fmat = ui.SkinTitle(fmt.Sprintf(logCoFmt, path, co), l.app.Styles.Frame()) + } + l.path = path + l.SetTitle(fmat) +} + +func (l *Log) keyboard(evt *tcell.EventKey) *tcell.EventKey { + key := evt.Key() + if key == tcell.KeyRune { + key = tcell.Key(evt.Rune()) + } + if m, ok := l.logs.Actions()[key]; ok { + log.Debug().Msgf(">> LogView handled %s", tcell.KeyNames[key]) + return m.Action(evt) + } + + return evt +} + +func (l *Log) Logs() *Details { + return l.logs +} + +func (l *Log) log(lines string) { + fmt.Fprintln(l.ansiWriter, tview.Escape(lines)) + log.Debug().Msgf("LOG LINES %d", l.logs.GetLineCount()) +} + +// Flush write logs to viewer. +func (l *Log) Flush(index int, buff []string) { + if index == 0 || !l.indicator.AutoScroll() { + return + } + l.log(strings.Join(buff[:index], "\n")) + l.app.QueueUpdateDraw(func() { + l.indicator.Refresh() + l.logs.ScrollToEnd() + }) +} + +// ---------------------------------------------------------------------------- +// Actions()... + +// SaveCmd dumps the logs to file. +func (l *Log) SaveCmd(evt *tcell.EventKey) *tcell.EventKey { + if path, err := saveData(l.app.Config.K9s.CurrentCluster, l.path, l.logs.GetText(true)); err != nil { + l.app.Flash().Err(err) + } else { + l.app.Flash().Infof("Log %s saved successfully!", path) + } + return nil +} + +func ensureDir(dir string) error { + return os.MkdirAll(dir, 0744) +} + +func saveData(cluster, name, data string) (string, error) { + dir := filepath.Join(config.K9sDumpDir, cluster) + if err := ensureDir(dir); err != nil { + return "", err + } + + now := time.Now().UnixNano() + fName := fmt.Sprintf("%s-%d.log", strings.Replace(name, "/", "-", -1), now) + + path := filepath.Join(dir, fName) + mod := os.O_CREATE | os.O_WRONLY + file, err := os.OpenFile(path, mod, 0600) + if err != nil { + log.Error().Err(err).Msgf("LogFile create %s", path) + return "", nil + } + defer func() { + if err := file.Close(); err != nil { + log.Error().Err(err).Msg("Closing Log file") + } + }() + if _, err := file.Write([]byte(data)); err != nil { + return "", err + } + + return path, nil +} + +func (l *Log) topCmd(evt *tcell.EventKey) *tcell.EventKey { + l.app.Flash().Info("Top of logs...") + l.logs.ScrollToBeginning() + return nil +} + +func (l *Log) bottomCmd(*tcell.EventKey) *tcell.EventKey { + l.app.Flash().Info("Bottom of logs...") + l.logs.ScrollToEnd() + return nil +} + +func (l *Log) pageUpCmd(*tcell.EventKey) *tcell.EventKey { + if l.logs.PageUp() { + l.app.Flash().Info("Reached Top ...") + } + return nil +} + +func (l *Log) pageDownCmd(*tcell.EventKey) *tcell.EventKey { + if l.logs.PageDown() { + l.app.Flash().Info("Reached Bottom ...") + } + return nil +} + +func (l *Log) clearCmd(*tcell.EventKey) *tcell.EventKey { + l.app.Flash().Info("Clearing logs...") + l.logs.Clear() + l.logs.ScrollTo(0, 0) + return nil +} + +func (l *Log) textWrapCmd(*tcell.EventKey) *tcell.EventKey { + l.indicator.ToggleTextWrap() + l.logs.SetWrap(l.indicator.textWrap) + return nil +} + +func (l *Log) toggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey { + l.indicator.ToggleAutoScroll() + return nil +} + +func (l *Log) fullScreenCmd(*tcell.EventKey) *tcell.EventKey { + l.indicator.ToggleFullScreen() + sidePadding := 1 + if l.indicator.FullScreen() { + sidePadding = 0 + } + l.SetFullScreen(l.indicator.FullScreen()) + l.Box.SetBorder(!l.indicator.FullScreen()) + l.Flex.SetBorderPadding(0, 0, sidePadding, sidePadding) + + return nil +} diff --git a/internal/view/log_indicator.go b/internal/view/log_indicator.go new file mode 100644 index 00000000..3c806dcb --- /dev/null +++ b/internal/view/log_indicator.go @@ -0,0 +1,83 @@ +package view + +import ( + "fmt" + "sync/atomic" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/tview" +) + +// LogIndicator represents a log view indicator. +type LogIndicator struct { + *tview.TextView + + styles *config.Styles + scrollStatus int32 + fullScreen bool + textWrap bool +} + +// NewLogIndicator returns a new indicator. +func NewLogIndicator(styles *config.Styles) *LogIndicator { + l := LogIndicator{ + styles: styles, + TextView: tview.NewTextView(), + scrollStatus: 1, + } + l.SetBackgroundColor(config.AsColor(styles.Views().Log.BgColor)) + l.SetTextAlign(tview.AlignRight) + l.SetDynamicColors(true) + + return &l +} + +func (l *LogIndicator) AutoScroll() bool { + return atomic.LoadInt32(&l.scrollStatus) == 1 +} + +func (l *LogIndicator) TextWrap() bool { + return l.textWrap +} + +func (l *LogIndicator) FullScreen() bool { + return l.fullScreen +} + +func (l *LogIndicator) ToggleFullScreen() { + l.fullScreen = !l.fullScreen + l.Refresh() +} + +func (l *LogIndicator) ToggleTextWrap() { + l.textWrap = !l.textWrap + l.Refresh() +} + +func (l *LogIndicator) ToggleAutoScroll() { + var val int32 = 1 + if l.AutoScroll() { + val = 0 + } + atomic.StoreInt32(&l.scrollStatus, val) + l.Refresh() +} + +func (l *LogIndicator) Refresh() { + l.Clear() + l.update("Autoscroll: " + l.onOff(l.AutoScroll())) + l.update("FullScreen: " + l.onOff(l.fullScreen)) + l.update("Wrap: " + l.onOff(l.textWrap)) +} + +func (l *LogIndicator) onOff(b bool) string { + if b { + return "On" + } + return "Off" +} + +func (l *LogIndicator) update(status string) { + fg, bg := l.styles.Frame().Crumb.FgColor, l.styles.Frame().Crumb.ActiveColor + fmt.Fprintf(l, "[%s:%s:b] %-15s ", fg, bg, status) +} diff --git a/internal/view/log_indicator_test.go b/internal/view/log_indicator_test.go new file mode 100644 index 00000000..05f1caa7 --- /dev/null +++ b/internal/view/log_indicator_test.go @@ -0,0 +1,17 @@ +package view_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/view" + "github.com/stretchr/testify/assert" +) + +func TestLogIndicatorRefresh(t *testing.T) { + defaults := config.NewStyles() + v := view.NewLogIndicator(defaults) + v.Refresh() + + assert.Equal(t, "[black:orange:b] Autoscroll: On [black:orange:b] FullScreen: Off [black:orange:b] Wrap: Off \n", v.GetText(false)) +} diff --git a/internal/view/log_test.go b/internal/view/log_test.go new file mode 100644 index 00000000..a852a9ab --- /dev/null +++ b/internal/view/log_test.go @@ -0,0 +1,89 @@ +package view + +import ( + "bytes" + "fmt" + "io/ioutil" + "path/filepath" + "testing" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config" + "github.com/derailed/tview" + "github.com/stretchr/testify/assert" +) + +func TestLogAnsi(t *testing.T) { + buff := bytes.NewBufferString("") + w := tview.ANSIWriter(buff, "white", "black") + fmt.Fprintf(w, "[YELLOW] ok") + assert.Equal(t, "[YELLOW] ok", buff.String()) + + v := tview.NewTextView() + v.SetDynamicColors(true) + aw := tview.ANSIWriter(v, "white", "black") + s := "[2019-03-27T15:05:15,246][INFO ][o.e.c.r.a.AllocationService] [es-0] Cluster health status changed from [YELLOW] to [GREEN] (reason: [shards started [[.monitoring-es-6-2019.03.27][0]]" + fmt.Fprintf(aw, "%s", s) + assert.Equal(t, s+"\n", v.GetText(false)) +} + +func TestLogFlush(t *testing.T) { + v := NewLog(client.GVR("v1/pods"), "fred/p1", "blee", false) + v.Init(makeContext()) + v.Flush(2, []string{"blee", "bozo"}) + + v.toggleAutoScrollCmd(nil) + assert.Equal(t, "blee\nbozo\n", v.Logs().GetText(true)) + assert.Equal(t, " Autoscroll: Off FullScreen: Off Wrap: Off ", v.Indicator().GetText(true)) + v.toggleAutoScrollCmd(nil) + assert.Equal(t, " Autoscroll: On FullScreen: Off Wrap: Off ", v.Indicator().GetText(true)) + assert.Equal(t, 10, len(v.Hints())) +} + +func TestLogViewSave(t *testing.T) { + v := NewLog(client.GVR("v1/pods"), "fred/p1", "blee", false) + v.Init(makeContext()) + + app := makeApp() + v.Flush(2, []string{"blee", "bozo"}) + config.K9sDumpDir = "/tmp" + dir := filepath.Join(config.K9sDumpDir, app.Config.K9s.CurrentCluster) + c1, _ := ioutil.ReadDir(dir) + v.SaveCmd(nil) + c2, _ := ioutil.ReadDir(dir) + assert.Equal(t, len(c2), len(c1)+1) +} + +func TestLogViewNav(t *testing.T) { + v := NewLog(client.GVR("v1/pods"), "fred/p1", "blee", false) + v.Init(makeContext()) + + var buff []string + for i := 0; i < 100; i++ { + buff = append(buff, fmt.Sprintf("line-%d\n", i)) + } + v.Flush(100, buff) + v.toggleAutoScrollCmd(nil) + + r, _ := v.Logs().GetScrollOffset() + assert.Equal(t, -1, r) +} + +func TestLogViewClear(t *testing.T) { + v := NewLog(client.GVR("v1/pods"), "fred/p1", "blee", false) + v.Init(makeContext()) + + v.Flush(2, []string{"blee", "bozo"}) + + v.toggleAutoScrollCmd(nil) + assert.Equal(t, "blee\nbozo\n", v.Logs().GetText(true)) + v.Logs().Clear() + assert.Equal(t, "", v.Logs().GetText(true)) +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func makeApp() *App { + return NewApp(config.NewConfig(ks{})) +} diff --git a/internal/view/logs_extender.go b/internal/view/logs_extender.go new file mode 100644 index 00000000..5a4e39e6 --- /dev/null +++ b/internal/view/logs_extender.go @@ -0,0 +1,65 @@ +package view + +import ( + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/ui" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" +) + +// LogsExtender adds log actions to a given viewer. +type LogsExtender struct { + ResourceViewer + + containerFn ContainerFunc +} + +// NewLogsExtender returns a new extender. +func NewLogsExtender(v ResourceViewer, f ContainerFunc) ResourceViewer { + l := LogsExtender{ + ResourceViewer: v, + containerFn: f, + } + l.bindKeys(l.Actions()) + + return &l +} + +// BindKeys injects new menu actions. +func (l *LogsExtender) bindKeys(aa ui.KeyActions) { + aa.Add(ui.KeyActions{ + ui.KeyL: ui.NewKeyAction("Logs", l.logsCmd(false), true), + ui.KeyShiftL: ui.NewKeyAction("Logs Previous", l.logsCmd(true), true), + }) +} + +func (l *LogsExtender) logsCmd(prev bool) func(evt *tcell.EventKey) *tcell.EventKey { + return func(evt *tcell.EventKey) *tcell.EventKey { + path := l.GetTable().GetSelectedItem() + if path == "" { + return nil + } + if isResourcePath(l.GetTable().Path) { + path = l.GetTable().Path + } + l.showLogs(path, prev) + + return nil + } +} + +func isResourcePath(p string) bool { + ns, n := client.Namespaced(p) + return ns != "" && n != "" +} + +func (l *LogsExtender) showLogs(path string, prev bool) { + log.Debug().Msgf("SHOWING LOGS path %q", path) + co := "" + if l.containerFn != nil { + co = l.containerFn() + } + if err := l.App().inject(NewLog(client.GVR(l.GVR()), path, co, prev)); err != nil { + l.App().Flash().Err(err) + } +} diff --git a/internal/view/node.go b/internal/view/node.go new file mode 100644 index 00000000..0d6548b9 --- /dev/null +++ b/internal/view/node.go @@ -0,0 +1,72 @@ +package view + +import ( + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/ui" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Node represents a node view. +type Node struct { + ResourceViewer +} + +// NewNode returns a new node view. +func NewNode(gvr client.GVR) ResourceViewer { + n := Node{ + ResourceViewer: NewBrowser(gvr), + } + n.SetBindKeysFn(n.bindKeys) + n.GetTable().SetEnterFn(n.showPods) + + return &n +} + +func (n *Node) bindKeys(aa ui.KeyActions) { + aa.Delete(ui.KeySpace, tcell.KeyCtrlSpace, tcell.KeyCtrlD) + aa.Add(ui.KeyActions{ + ui.KeyY: ui.NewKeyAction("YAML", n.viewCmd, true), + ui.KeyShiftC: ui.NewKeyAction("Sort CPU", n.GetTable().SortColCmd(7, false), false), + ui.KeyShiftM: ui.NewKeyAction("Sort MEM", n.GetTable().SortColCmd(8, false), false), + ui.KeyShiftX: ui.NewKeyAction("Sort CPU%", n.GetTable().SortColCmd(9, false), false), + ui.KeyShiftZ: ui.NewKeyAction("Sort MEM%", n.GetTable().SortColCmd(10, false), false), + }) +} + +func (n *Node) showPods(app *App, ns, res, sel string) { + showPods(app, n.GetTable().GetSelectedItem(), "", "spec.nodeName="+sel) +} + +func (n *Node) viewCmd(evt *tcell.EventKey) *tcell.EventKey { + path := n.GetTable().GetSelectedItem() + if path == "" { + return evt + } + + sel := n.GetTable().GetSelectedItem() + log.Debug().Msgf("------ VIEW NODE %q", sel) + o, err := n.App().factory.Client().DynDialOrDie().Resource(client.GVR(n.GVR()).AsGVR()).Get(sel, metav1.GetOptions{}) + if err != nil { + n.App().Flash().Errf("Unable to get resource %q -- %s", n.GVR(), err) + return nil + } + + raw, err := toYAML(o) + if err != nil { + n.App().Flash().Errf("Unable to marshal resource %s", err) + return nil + } + + details := NewDetails("YAML") + details.SetSubject(sel) + details.SetTextColor(n.App().Styles.FgColor()) + details.SetText(colorizeYAML(n.App().Styles.Views().Yaml, raw)) + details.ScrollToBeginning() + if err := n.App().inject(details); err != nil { + n.App().Flash().Err(err) + } + + return nil +} diff --git a/internal/view/ns.go b/internal/view/ns.go new file mode 100644 index 00000000..d7caf7d8 --- /dev/null +++ b/internal/view/ns.go @@ -0,0 +1,101 @@ +package view + +import ( + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" +) + +const ( + favNSIndicator = "+" + defaultNSIndicator = "(*)" +) + +// Namespace represents a namespace viewer. +type Namespace struct { + ResourceViewer +} + +// NewNamespace returns a new viewer +func NewNamespace(gvr client.GVR) ResourceViewer { + n := Namespace{ + ResourceViewer: NewBrowser(gvr), + } + n.GetTable().SetDecorateFn(n.decorate) + n.GetTable().SetColorerFn(render.Namespace{}.ColorerFunc()) + n.GetTable().SetEnterFn(n.switchNs) + n.SetBindKeysFn(n.bindKeys) + + return &n +} + +func (n *Namespace) bindKeys(aa ui.KeyActions) { + aa.Add(ui.KeyActions{ + ui.KeyU: ui.NewKeyAction("Use", n.useNsCmd, true), + }) +} + +func (n *Namespace) switchNs(app *App, _, res, sel string) { + n.useNamespace(sel) + if err := app.gotoResource("pods", true); err != nil { + app.Flash().Err(err) + } +} + +func (n *Namespace) useNsCmd(evt *tcell.EventKey) *tcell.EventKey { + path := n.GetTable().GetSelectedItem() + if path == "" { + return nil + } + n.useNamespace(path) + + return nil +} + +func (n *Namespace) useNamespace(ns string) { + log.Debug().Msgf("SWITCHING NS %q", ns) + n.App().switchNS(ns) + if err := n.App().Config.SetActiveNamespace(ns); err != nil { + n.App().Flash().Err(err) + } else { + n.App().Flash().Infof("Namespace %s is now active!", ns) + } + if err := n.App().Config.Save(); err != nil { + log.Error().Err(err).Msg("Config file save failed!") + } +} + +func (n *Namespace) decorate(data render.TableData) render.TableData { + if n.App().Conn() == nil || len(data.RowEvents) == 0 { + return data + } + + // checks if all ns is in the list if not add it. + if _, ok := data.RowEvents.FindIndex(render.NamespaceAll); !ok { + data.RowEvents = append(data.RowEvents, + render.RowEvent{ + Kind: render.EventUnchanged, + Row: render.Row{ + ID: render.AllNamespaces, + Fields: render.Fields{render.NamespaceAll, "Active", "0"}, + }, + }, + ) + } + + for _, re := range data.RowEvents { + if config.InList(n.App().Config.FavNamespaces(), re.Row.ID) { + re.Row.Fields[0] += favNSIndicator + re.Kind = render.EventUnchanged + } + if n.App().Config.ActiveNamespace() == re.Row.ID { + re.Row.Fields[0] += defaultNSIndicator + re.Kind = render.EventUnchanged + } + } + + return data +} diff --git a/internal/view/ns_test.go b/internal/view/ns_test.go new file mode 100644 index 00000000..5ab9d368 --- /dev/null +++ b/internal/view/ns_test.go @@ -0,0 +1,17 @@ +package view_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/view" + "github.com/stretchr/testify/assert" +) + +func TestNSCleanser(t *testing.T) { + ns := view.NewNamespace(client.GVR("v1/namespaces")) + + assert.Nil(t, ns.Init(makeCtx())) + assert.Equal(t, "Namespaces", ns.Name()) + assert.Equal(t, 3, len(ns.Hints())) +} diff --git a/internal/view/page_stack.go b/internal/view/page_stack.go new file mode 100644 index 00000000..9a8d8c0e --- /dev/null +++ b/internal/view/page_stack.go @@ -0,0 +1,47 @@ +package view + +import ( + "context" + + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/ui" +) + +type PageStack struct { + *ui.Pages + + app *App +} + +func NewPageStack() *PageStack { + return &PageStack{ + Pages: ui.NewPages(), + } +} + +func (p *PageStack) Init(ctx context.Context) (err error) { + if p.app, err = extractApp(ctx); err != nil { + return err + } + p.Stack.AddListener(p) + + return nil +} + +func (p *PageStack) StackPushed(c model.Component) { + c.Start() + p.app.SetFocus(c) +} + +func (p *PageStack) StackPopped(o, top model.Component) { + o.Stop() + p.StackTop(top) +} + +func (p *PageStack) StackTop(top model.Component) { + if top == nil { + return + } + top.Start() + p.app.SetFocus(top) +} diff --git a/internal/view/picker.go b/internal/view/picker.go new file mode 100644 index 00000000..7330f1fb --- /dev/null +++ b/internal/view/picker.go @@ -0,0 +1,65 @@ +package view + +import ( + "context" + + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/tview" + "github.com/gdamore/tcell" +) + +// Picker represents a container picker. +type Picker struct { + *tview.List + + actions ui.KeyActions +} + +// NewPicker returns a new picker. +func NewPicker() *Picker { + return &Picker{ + List: tview.NewList(), + actions: ui.KeyActions{}, + } +} + +func (v *Picker) Init(ctx context.Context) error { + app, err := extractApp(ctx) + if err != nil { + return err + } + v.actions[tcell.KeyEscape] = ui.NewKeyAction("Back", app.PrevCmd, true) + + v.SetBorder(true) + v.SetMainTextColor(tcell.ColorWhite) + v.ShowSecondaryText(false) + v.SetShortcutColor(tcell.ColorAqua) + v.SetSelectedBackgroundColor(tcell.ColorAqua) + v.SetTitle(" [aqua::b]Containers Picker ") + v.SetInputCapture(func(evt *tcell.EventKey) *tcell.EventKey { + if a, ok := v.actions[evt.Key()]; ok { + a.Action(evt) + evt = nil + } + return evt + }) + + return nil +} +func (v *Picker) Start() {} +func (v *Picker) Stop() {} +func (v *Picker) Name() string { return "picker" } + +// Protocol... + +func (v *Picker) Hints() model.MenuHints { + return v.actions.Hints() +} + +func (v *Picker) populate(ss []string) { + v.Clear() + for i, s := range ss { + v.AddItem(s, "Select a container", rune('a'+i), nil) + } +} diff --git a/internal/view/pod.go b/internal/view/pod.go new file mode 100644 index 00000000..7929ceb5 --- /dev/null +++ b/internal/view/pod.go @@ -0,0 +1,189 @@ +package view + +import ( + "context" + "errors" + "fmt" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/k9s/internal/watch" + "github.com/gdamore/tcell" + "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" +) + +const shellCheck = "command -v bash >/dev/null && exec bash || exec sh" + +// Pod represents a pod viewer. +type Pod struct { + ResourceViewer +} + +// NewPod returns a new viewer. +func NewPod(gvr client.GVR) ResourceViewer { + p := Pod{ResourceViewer: NewLogsExtender(NewBrowser(gvr), nil)} + p.SetBindKeysFn(p.bindKeys) + p.GetTable().SetEnterFn(p.showContainers) + p.GetTable().SetColorerFn(render.Pod{}.ColorerFunc()) + + return &p +} + +func (p *Pod) bindKeys(aa ui.KeyActions) { + aa.Add(ui.KeyActions{ + tcell.KeyCtrlK: ui.NewKeyAction("Kill", p.killCmd, true), + ui.KeyS: ui.NewKeyAction("Shell", p.shellCmd, true), + ui.KeyShiftR: ui.NewKeyAction("Sort Ready", p.GetTable().SortColCmd(1, true), false), + ui.KeyShiftS: ui.NewKeyAction("Sort Status", p.GetTable().SortColCmd(2, true), false), + ui.KeyShiftT: ui.NewKeyAction("Sort Restart", p.GetTable().SortColCmd(3, false), false), + ui.KeyShiftC: ui.NewKeyAction("Sort CPU", p.GetTable().SortColCmd(4, false), false), + ui.KeyShiftM: ui.NewKeyAction("Sort MEM", p.GetTable().SortColCmd(5, false), false), + ui.KeyShiftX: ui.NewKeyAction("Sort CPU%", p.GetTable().SortColCmd(6, false), false), + ui.KeyShiftZ: ui.NewKeyAction("Sort MEM%", p.GetTable().SortColCmd(7, false), false), + ui.KeyShiftI: ui.NewKeyAction("Sort IP", p.GetTable().SortColCmd(8, true), false), + ui.KeyShiftO: ui.NewKeyAction("Sort Node", p.GetTable().SortColCmd(9, true), false), + }) +} + +func (p *Pod) showContainers(app *App, ns, gvr, path string) { + log.Debug().Msgf("SHOW CONTAINERS %q -- %q -- %q", gvr, ns, path) + co := NewContainer(client.GVR("containers")) + co.SetContextFn(p.podContext) + if err := app.inject(co); err != nil { + app.Flash().Err(err) + } +} + +func (p *Pod) podContext(ctx context.Context) context.Context { + return context.WithValue(ctx, internal.KeyPath, p.GetTable().GetSelectedItem()) +} + +// Commands... + +func (p *Pod) killCmd(evt *tcell.EventKey) *tcell.EventKey { + sels := p.GetTable().GetSelectedItems() + if len(sels) == 0 { + return evt + } + + res, err := dao.AccessorFor(p.App().factory, client.GVR(p.GVR())) + if err != nil { + p.App().Flash().Err(err) + return nil + } + nuker, ok := res.(dao.Nuker) + if !ok { + p.App().Flash().Err(fmt.Errorf("expecting a nuker for %q", p.GVR())) + return nil + } + p.GetTable().ShowDeleted() + for _, res := range sels { + p.App().Flash().Infof("Delete resource %s -- %s", p.GVR(), res) + if err := nuker.Delete(res, true, false); err != nil { + p.App().Flash().Errf("Delete failed with %s", err) + } else { + p.App().factory.DeleteForwarder(res) + } + } + p.Refresh() + + return nil +} + +func (p *Pod) shellCmd(evt *tcell.EventKey) *tcell.EventKey { + sel := p.GetTable().GetSelectedItem() + if sel == "" { + return evt + } + + row := p.GetTable().GetSelectedRowIndex() + status := ui.TrimCell(p.GetTable().SelectTable, row, p.GetTable().NameColIndex()+2) + if status != render.Running { + p.App().Flash().Errf("%s is not in a running state", sel) + return nil + } + cc, err := fetchContainers(p.App().factory, sel, false) + if err != nil { + p.App().Flash().Errf("Unable to retrieve containers %s", err) + return evt + } + if len(cc) == 1 { + p.shellIn(sel, "") + return nil + } + picker := NewPicker() + picker.populate(cc) + picker.SetSelectedFunc(func(i int, t, d string, r rune) { + p.shellIn(sel, t) + }) + if err := p.App().inject(picker); err != nil { + p.App().Flash().Err(err) + } + + return evt +} + +func (p *Pod) shellIn(path, co string) { + p.Stop() + shellIn(p.App(), path, co) + p.Start() +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func fetchContainers(f *watch.Factory, path string, includeInit bool) ([]string, error) { + o, err := f.Get("v1/pods", path, labels.Everything()) + if err != nil { + return nil, err + } + + var pod v1.Pod + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod) + if err != nil { + return nil, err + } + + nn := make([]string, 0, len(pod.Spec.Containers)+len(pod.Spec.InitContainers)) + for _, c := range pod.Spec.Containers { + nn = append(nn, c.Name) + } + if includeInit { + for _, c := range pod.Spec.InitContainers { + nn = append(nn, c.Name) + } + } + return nn, nil +} + +func shellIn(a *App, path, co string) { + args := computeShellArgs(path, co, a.Config.K9s.CurrentContext, a.Conn().Config().Flags().KubeConfig) + log.Debug().Msgf("Shell args %v", args) + if !runK(true, a, args...) { + a.Flash().Err(errors.New("Shell exec failed")) + } +} + +func computeShellArgs(path, co, context string, kcfg *string) []string { + args := make([]string, 0, 15) + args = append(args, "exec", "-it") + args = append(args, "--context", context) + ns, po := client.Namespaced(path) + args = append(args, "-n", ns) + args = append(args, po) + if kcfg != nil && *kcfg != "" { + args = append(args, "--kubeconfig", *kcfg) + } + if co != "" { + args = append(args, "-c", co) + } + + return append(args, "--", "sh", "-c", shellCheck) +} diff --git a/internal/views/pod_test.go b/internal/view/pod_int_test.go similarity index 95% rename from internal/views/pod_test.go rename to internal/view/pod_int_test.go index ec917ee5..2351bade 100644 --- a/internal/views/pod_test.go +++ b/internal/view/pod_int_test.go @@ -1,4 +1,4 @@ -package views +package view import ( "strings" @@ -44,7 +44,8 @@ func TestComputeShellArgs(t *testing.T) { }, } - for k, u := range uu { + for k := range uu { + u := uu[k] t.Run(k, func(t *testing.T) { args := computeShellArgs(u.path, u.co, u.context, u.cfg) diff --git a/internal/view/pod_test.go b/internal/view/pod_test.go new file mode 100644 index 00000000..7fcd5b74 --- /dev/null +++ b/internal/view/pod_test.go @@ -0,0 +1,27 @@ +package view_test + +import ( + "context" + "testing" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/view" + "github.com/stretchr/testify/assert" +) + +func TestPodNew(t *testing.T) { + po := view.NewPod(client.GVR("v1/pods")) + + assert.Nil(t, po.Init(makeCtx())) + assert.Equal(t, "Pods", po.Name()) + assert.Equal(t, 15, len(po.Hints())) +} + +// Helpers... + +func makeCtx() context.Context { + cfg := config.NewConfig(ks{}) + return context.WithValue(context.Background(), internal.KeyApp, view.NewApp(cfg)) +} diff --git a/internal/view/policy.go b/internal/view/policy.go new file mode 100644 index 00000000..cb2c22d8 --- /dev/null +++ b/internal/view/policy.go @@ -0,0 +1,68 @@ +package view + +import ( + "context" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + "github.com/gdamore/tcell" +) + +const ( + group = "Group" + user = "User" + sa = "ServiceAccount" +) + +// Policy presents a RBAC rules viewer based on what a given user/group or sa can do. +type Policy struct { + ResourceViewer + + subjectKind, subjectName string +} + +// NewPolicy returns a new viewer. +func NewPolicy(app *App, subject, name string) *Policy { + p := Policy{ + ResourceViewer: NewBrowser(client.GVR("policy")), + subjectKind: subject, + subjectName: name, + } + p.GetTable().SetColorerFn(render.Policy{}.ColorerFunc()) + p.SetBindKeysFn(p.bindKeys) + p.GetTable().SetSortCol(1, len(render.Policy{}.Header(render.AllNamespaces)), false) + p.SetContextFn(p.subjectCtx) + p.GetTable().SetEnterFn(blankEnterFn) + + return &p +} + +func (p *Policy) subjectCtx(ctx context.Context) context.Context { + ctx = context.WithValue(ctx, internal.KeySubjectKind, mapSubject(p.subjectKind)) + ctx = context.WithValue(ctx, internal.KeyPath, mapSubject(p.subjectKind)+":"+p.subjectName) + return context.WithValue(ctx, internal.KeySubjectName, p.subjectName) +} + +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(0, true), false), + ui.KeyShiftO: ui.NewKeyAction("Sort Group", p.GetTable().SortColCmd(1, true), false), + ui.KeyShiftB: ui.NewKeyAction("Sort Binding", p.GetTable().SortColCmd(2, true), false), + }) +} + +func mapSubject(subject string) string { + switch subject { + case "g": + return group + case "s": + return sa + case "u": + return user + default: + return subject + } +} diff --git a/internal/view/port_forward.go b/internal/view/port_forward.go new file mode 100644 index 00000000..4e16cdc8 --- /dev/null +++ b/internal/view/port_forward.go @@ -0,0 +1,183 @@ +package view + +import ( + "context" + "errors" + "fmt" + "time" + + "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/render" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/tview" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" +) + +const promptPage = "prompt" + +// PortForward presents active portforward viewer. +type PortForward struct { + ResourceViewer + + bench *perf.Benchmark +} + +// NewPortForward returns a new viewer. +func NewPortForward(gvr client.GVR) ResourceViewer { + p := PortForward{ + ResourceViewer: NewBrowser(gvr), + } + p.GetTable().SetBorderFocusColor(tcell.ColorDodgerBlue) + p.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorDodgerBlue, tcell.AttrNone) + p.GetTable().SetColorerFn(render.PortForward{}.ColorerFunc()) + p.GetTable().SetSortCol(p.GetTable().NameColIndex()+6, 0, true) + p.SetContextFn(p.portForwardContext) + p.SetBindKeysFn(p.bindKeys) + + return &p +} + +func (p *PortForward) portForwardContext(ctx context.Context) context.Context { + return context.WithValue(ctx, internal.KeyBenchCfg, p.App().Bench) +} + +func (p *PortForward) bindKeys(aa ui.KeyActions) { + aa.Add(ui.KeyActions{ + tcell.KeyEnter: ui.NewKeyAction("Benchmarks", p.showBenchCmd, true), + tcell.KeyCtrlB: ui.NewKeyAction("Bench", p.benchCmd, true), + tcell.KeyCtrlK: ui.NewKeyAction("Bench Stop", p.benchStopCmd, true), + tcell.KeyCtrlD: ui.NewKeyAction("Delete", p.deleteCmd, true), + ui.KeyShiftP: ui.NewKeyAction("Sort Ports", p.GetTable().SortColCmd(2, true), false), + ui.KeyShiftU: ui.NewKeyAction("Sort URL", p.GetTable().SortColCmd(4, true), false), + }) +} + +func (p *PortForward) showBenchCmd(evt *tcell.EventKey) *tcell.EventKey { + if err := p.App().inject(NewBenchmark("benchmarks")); err != nil { + p.App().Flash().Err(err) + } + + return nil +} + +func (p *PortForward) benchStopCmd(evt *tcell.EventKey) *tcell.EventKey { + if p.bench != nil { + log.Debug().Msg(">>> Benchmark cancelFned!!") + p.App().status(ui.FlashErr, "Benchmark Camceled!") + p.bench.Cancel() + } + p.App().StatusReset() + + return nil +} + +func (p *PortForward) benchCmd(evt *tcell.EventKey) *tcell.EventKey { + sel := p.GetTable().GetSelectedItem() + if sel == "" { + return nil + } + + if p.bench != nil { + p.App().Flash().Err(errors.New("Only one benchmark allowed at a time")) + return nil + } + + r, _ := p.GetTable().GetSelection() + cfg := defaultConfig() + if b, ok := p.App().Bench.Benchmarks.Containers[sel]; ok { + cfg = b + } + cfg.Name = sel + + base := ui.TrimCell(p.GetTable().SelectTable, r, 4) + var err error + if p.bench, err = perf.NewBenchmark(base, cfg); err != nil { + p.App().Flash().Errf("Bench failed %v", err) + p.App().StatusReset() + return nil + } + + p.App().status(ui.FlashWarn, "Benchmark in progress...") + log.Debug().Msg("Bench starting...") + go p.runBenchmark() + + return nil +} + +func (p *PortForward) runBenchmark() { + p.bench.Run(p.App().Config.K9s.CurrentCluster, func() { + log.Debug().Msg("Bench Completed!") + p.App().QueueUpdate(func() { + if p.bench.Canceled() { + p.App().status(ui.FlashInfo, "Benchmark cancelFned") + } else { + p.App().status(ui.FlashInfo, "Benchmark Completed!") + p.bench.Cancel() + } + p.bench = nil + go func() { + <-time.After(2 * time.Second) + p.App().QueueUpdate(func() { p.App().StatusReset() }) + }() + }) + }) +} + +func (p *PortForward) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { + if !p.GetTable().SearchBuff().Empty() { + p.GetTable().SearchBuff().Reset() + return nil + } + + sel := p.GetTable().GetSelectedItem() + if sel == "" { + return nil + } + log.Debug().Msgf("PF DELETE %q", sel) + + showModal(p.App().Content.Pages, fmt.Sprintf("Delete PortForward `%s?", sel), func() { + p.App().factory.DeleteForwarder(sel) + p.App().Flash().Infof("PortForward %s deleted!", sel) + p.GetTable().Refresh() + }) + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func defaultConfig() config.BenchConfig { + return config.BenchConfig{ + C: config.DefaultC, + N: config.DefaultN, + HTTP: config.HTTP{ + Method: config.DefaultMethod, + Path: "/", + }, + } +} + +func showModal(p *ui.Pages, msg string, ok func()) { + m := tview.NewModal(). + AddButtons([]string{"Cancel", "OK"}). + SetTextColor(tcell.ColorFuchsia). + SetText(msg). + SetDoneFunc(func(_ int, b string) { + if b == "OK" { + ok() + } + dismissModal(p) + }) + m.SetTitle("") + p.AddPage(promptPage, m, false, false) + p.ShowPage(promptPage) +} + +func dismissModal(p *ui.Pages) { + p.RemovePage(promptPage) +} diff --git a/internal/view/port_forward_test.go b/internal/view/port_forward_test.go new file mode 100644 index 00000000..69a0648c --- /dev/null +++ b/internal/view/port_forward_test.go @@ -0,0 +1,17 @@ +package view_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/view" + "github.com/stretchr/testify/assert" +) + +func TestPortForwardNew(t *testing.T) { + pf := view.NewPortForward(client.GVR("portforwards")) + + assert.Nil(t, pf.Init(makeCtx())) + assert.Equal(t, "PortForwards", pf.Name()) + assert.Equal(t, 8, len(pf.Hints())) +} diff --git a/internal/view/rbac.go b/internal/view/rbac.go new file mode 100644 index 00000000..e72002ea --- /dev/null +++ b/internal/view/rbac.go @@ -0,0 +1,54 @@ +package view + +import ( + "context" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + "github.com/gdamore/tcell" +) + +// Rbac presents an RBAC policy viewer. +type Rbac struct { + ResourceViewer +} + +// NewRbac returns a new viewer. +func NewRbac(gvr client.GVR) ResourceViewer { + r := Rbac{ + ResourceViewer: NewBrowser(gvr), + } + r.GetTable().SetColorerFn(render.Rbac{}.ColorerFunc()) + r.SetBindKeysFn(r.bindKeys) + r.GetTable().SetSortCol(1, len(render.Rbac{}.Header(render.ClusterScope)), true) + r.GetTable().SetEnterFn(blankEnterFn) + + return &r +} + +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(1, true), false), + }) +} + +func showRules(app *App, _, gvr, path string) { + v := NewRbac(client.GVR("rbac")) + v.SetContextFn(rbacCtxt(gvr, path)) + + if err := app.inject(v); err != nil { + app.Flash().Err(err) + } +} + +func rbacCtxt(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, _, _, _ string) {} diff --git a/internal/view/rbac_test.go b/internal/view/rbac_test.go new file mode 100644 index 00000000..463cb340 --- /dev/null +++ b/internal/view/rbac_test.go @@ -0,0 +1,17 @@ +package view_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/view" + "github.com/stretchr/testify/assert" +) + +func TestRbacNew(t *testing.T) { + v := view.NewRbac(client.GVR("rbac")) + + assert.Nil(t, v.Init(makeCtx())) + assert.Equal(t, "Rbac", v.Name()) + assert.Equal(t, 2, len(v.Hints())) +} diff --git a/internal/view/registrar.go b/internal/view/registrar.go new file mode 100644 index 00000000..a554ab20 --- /dev/null +++ b/internal/view/registrar.go @@ -0,0 +1,129 @@ +package view + +import ( + "strings" + + "github.com/derailed/k9s/internal/client" +) + +func loadCustomViewers() MetaViewers { + m := make(MetaViewers, 30) + coreRes(m) + miscRes(m) + appsRes(m) + rbacRes(m) + batchRes(m) + extRes(m) + + return m +} + +func coreRes(vv MetaViewers) { + vv["v1/namespaces"] = MetaViewer{ + viewerFn: NewNamespace, + } + vv["v1/events"] = MetaViewer{ + viewerFn: NewEvent, + } + vv["v1/pods"] = MetaViewer{ + viewerFn: NewPod, + } + vv["v1/services"] = MetaViewer{ + viewerFn: NewService, + } + vv["v1/nodes"] = MetaViewer{ + viewerFn: NewNode, + } + vv["v1/secrets"] = MetaViewer{ + viewerFn: NewSecret, + } +} + +func miscRes(vv MetaViewers) { + vv["contexts"] = MetaViewer{ + viewerFn: NewContext, + } + vv["containers"] = MetaViewer{ + viewerFn: NewContainer, + } + vv["portforwards"] = MetaViewer{ + viewerFn: NewPortForward, + } + vv["screendumps"] = MetaViewer{ + viewerFn: NewScreenDump, + } + vv["benchmarks"] = MetaViewer{ + viewerFn: NewBenchmark, + } + vv["aliases"] = MetaViewer{ + viewerFn: NewAlias, + } +} + +func appsRes(vv MetaViewers) { + vv["apps/v1/deployments"] = MetaViewer{ + viewerFn: NewDeploy, + } + vv["apps/v1/replicasets"] = MetaViewer{ + viewerFn: NewReplicaSet, + } + vv["apps/v1/statefulsets"] = MetaViewer{ + viewerFn: NewStatefulSet, + } + vv["apps/v1/daemonsets"] = MetaViewer{ + viewerFn: NewDaemonSet, + } + vv["extensions/v1beta1/daemonsets"] = MetaViewer{ + viewerFn: NewDaemonSet, + } +} + +func rbacRes(vv MetaViewers) { + vv["rbac"] = MetaViewer{ + enterFn: showRules, + } + vv["users"] = MetaViewer{ + viewerFn: NewUser, + } + vv["groups"] = MetaViewer{ + viewerFn: NewGroup, + } + vv["rbac.authorization.k8s.io/v1/clusterroles"] = MetaViewer{ + enterFn: showRules, + } + vv["rbac.authorization.k8s.io/v1/roles"] = MetaViewer{ + enterFn: showRules, + } + vv["rbac.authorization.k8s.io/v1/clusterrolebindings"] = MetaViewer{ + enterFn: showRules, + } + vv["rbac.authorization.k8s.io/v1/rolebindings"] = MetaViewer{ + enterFn: showRules, + } +} + +func batchRes(vv MetaViewers) { + vv["batch/v1beta1/cronjobs"] = MetaViewer{ + viewerFn: NewCronJob, + } + vv["batch/v1/jobs"] = MetaViewer{ + viewerFn: NewJob, + } +} + +func extRes(vv MetaViewers) { + vv["apiextensions.k8s.io/v1/customresourcedefinitions"] = MetaViewer{ + enterFn: showCRD, + } + vv["apiextensions.k8s.io/v1beta1/customresourcedefinitions"] = MetaViewer{ + enterFn: showCRD, + } +} + +func showCRD(app *App, ns, gvr, path string) { + _, crdGVR := client.Namespaced(path) + tokens := strings.Split(crdGVR, ".") + if err := app.gotoResource(tokens[0], false); err != nil { + app.Flash().Err(err) + } +} diff --git a/internal/view/restart_extender.go b/internal/view/restart_extender.go new file mode 100644 index 00000000..d60032b6 --- /dev/null +++ b/internal/view/restart_extender.go @@ -0,0 +1,65 @@ +package view + +import ( + "errors" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/k9s/internal/ui/dialog" + "github.com/gdamore/tcell" +) + +// RestartExtender represents a restartable resource. +type RestartExtender struct { + ResourceViewer +} + +// NewRestartExtender returns a new extender. +func NewRestartExtender(v ResourceViewer) ResourceViewer { + r := RestartExtender{ResourceViewer: v} + r.SetBindKeysFn(r.bindKeys) + + return &r +} + +// BindKeys creates additional menu actions. +func (r *RestartExtender) bindKeys(aa ui.KeyActions) { + r.Actions().Add(ui.KeyActions{ + tcell.KeyCtrlT: ui.NewKeyAction("Restart", r.restartCmd, true), + }) +} + +func (r *RestartExtender) restartCmd(evt *tcell.EventKey) *tcell.EventKey { + path := r.GetTable().GetSelectedItem() + if path == "" { + return nil + } + + r.Stop() + defer r.Start() + msg := "Please confirm rollout restart for " + path + dialog.ShowConfirm(r.App().Content.Pages, "", msg, func() { + if err := r.restartRollout(path); err != nil { + r.App().Flash().Err(err) + } else { + r.App().Flash().Infof("Rollout restart in progress for `%s...", path) + } + }, func() {}) + + return nil +} + +func (r *RestartExtender) restartRollout(path string) error { + res, err := dao.AccessorFor(r.App().factory, client.GVR(r.GVR())) + if err != nil { + return nil + } + + s, ok := res.(dao.Restartable) + if !ok { + return errors.New("resource is not restartable") + } + + return s.Restart(path) +} diff --git a/internal/view/rs.go b/internal/view/rs.go new file mode 100644 index 00000000..5273a659 --- /dev/null +++ b/internal/view/rs.go @@ -0,0 +1,192 @@ +package view + +import ( + "errors" + "fmt" + "strconv" + "strings" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/k9s/internal/watch" + "github.com/derailed/tview" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kubectl/pkg/polymorphichelpers" +) + +// ReplicaSet presents a replicaset viewer. +type ReplicaSet struct { + ResourceViewer +} + +// NewReplicaSet returns a new viewer. +func NewReplicaSet(gvr client.GVR) ResourceViewer { + r := ReplicaSet{ + ResourceViewer: NewBrowser(gvr), + } + r.bindKeys() + r.GetTable().SetEnterFn(r.showPods) + r.GetTable().SetColorerFn(render.ReplicaSet{}.ColorerFunc()) + + return &r +} + +func (r *ReplicaSet) bindKeys() { + r.Actions().Add(ui.KeyActions{ + ui.KeyShiftD: ui.NewKeyAction("Sort Desired", r.GetTable().SortColCmd(1, true), false), + ui.KeyShiftC: ui.NewKeyAction("Sort Current", r.GetTable().SortColCmd(2, true), false), + tcell.KeyCtrlB: ui.NewKeyAction("Rollback", r.rollbackCmd, true), + }) +} + +func (r *ReplicaSet) showPods(app *App, _, gvr, path string) { + o, err := app.factory.Get(r.GVR(), path, labels.Everything()) + if err != nil { + app.Flash().Err(err) + return + } + + var rs appsv1.ReplicaSet + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &rs) + if err != nil { + app.Flash().Err(err) + } + + showPodsFromSelector(app, path, rs.Spec.Selector) +} + +func (r *ReplicaSet) rollbackCmd(evt *tcell.EventKey) *tcell.EventKey { + sel := r.GetTable().GetSelectedItem() + if sel == "" { + return evt + } + + r.showModal(fmt.Sprintf("Rollback %s %s?", r.GVR(), sel), func(_ int, button string) { + if button == "OK" { + r.App().Flash().Infof("Rolling back %s %s", r.GVR(), sel) + if res, err := rollback(r.App().factory, sel); err != nil { + r.App().Flash().Err(err) + } else { + r.App().Flash().Info(res) + } + r.Refresh() + } + r.dismissModal() + }) + + return nil +} + +func (r *ReplicaSet) dismissModal() { + r.App().Content.RemovePage("confirm") +} + +func (r *ReplicaSet) showModal(msg string, done func(int, string)) { + confirm := tview.NewModal(). + AddButtons([]string{"Cancel", "OK"}). + SetTextColor(tcell.ColorFuchsia). + SetText(msg). + SetDoneFunc(done) + r.App().Content.AddPage("confirm", confirm, false, false) + r.App().Content.ShowPage("confirm") +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func findRS(f *watch.Factory, path string) (*v1.ReplicaSet, error) { + o, err := f.Get("apps/v1/replicasets", path, labels.Everything()) + if err != nil { + return nil, err + } + + var rs appsv1.ReplicaSet + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &rs) + if err != nil { + return nil, err + } + + return &rs, nil +} + +func findDP(f *watch.Factory, path string) (*appsv1.Deployment, error) { + o, err := f.Get("apps/v1/deployments", path, labels.Everything()) + if err != nil { + return nil, err + } + + var dp appsv1.Deployment + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &dp) + if err != nil { + return nil, err + } + + return &dp, nil +} + +func controllerInfo(rs *v1.ReplicaSet) (string, string, string, error) { + for _, ref := range rs.ObjectMeta.OwnerReferences { + if ref.Controller == nil { + continue + } + log.Debug().Msgf("Controller name %s", ref.Name) + tokens := strings.Split(ref.APIVersion, "/") + apiGroup := ref.APIVersion + if len(tokens) == 2 { + apiGroup = tokens[0] + } + return ref.Name, ref.Kind, apiGroup, nil + } + return "", "", "", fmt.Errorf("Unable to find controller for ReplicaSet %s", rs.ObjectMeta.Name) +} + +func getRevision(rs *v1.ReplicaSet) (int64, error) { + revision := rs.ObjectMeta.Annotations["deployment.kubernetes.io/revision"] + if rs.Status.Replicas != 0 { + return 0, errors.New("can not rollback current replica") + } + vers, err := strconv.Atoi(revision) + if err != nil { + return 0, errors.New("revision conversion failed") + } + + return int64(vers), nil +} + +func rollback(f *watch.Factory, path string) (string, error) { + rs, err := findRS(f, path) + if err != nil { + return "", err + } + version, err := getRevision(rs) + if err != nil { + return "", err + } + + name, kind, apiGroup, err := controllerInfo(rs) + if err != nil { + return "", err + } + rb, err := polymorphichelpers.RollbackerFor(schema.GroupKind{Group: apiGroup, Kind: kind}, f.Client().DialOrDie()) + if err != nil { + return "", err + } + dp, err := findDP(f, client.FQN(rs.Namespace, name)) + if err != nil { + return "", err + } + res, err := rb.Rollback(dp, map[string]string{}, version, false) + if err != nil { + return "", err + } + + return res, nil +} diff --git a/internal/view/scale_extender.go b/internal/view/scale_extender.go new file mode 100644 index 00000000..9726f17b --- /dev/null +++ b/internal/view/scale_extender.go @@ -0,0 +1,117 @@ +package view + +import ( + "fmt" + "strconv" + "strings" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/tview" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" +) + +type ScaleExtender struct { + ResourceViewer +} + +func NewScaleExtender(r ResourceViewer) ResourceViewer { + s := ScaleExtender{ResourceViewer: r} + s.bindKeys(s.Actions()) + + return &s +} + +func (s *ScaleExtender) bindKeys(aa ui.KeyActions) { + aa.Add(ui.KeyActions{ + ui.KeyS: ui.NewKeyAction("Scale", s.scaleCmd, true), + }) +} + +func (s *ScaleExtender) scaleCmd(evt *tcell.EventKey) *tcell.EventKey { + path := s.GetTable().GetSelectedItem() + if path == "" { + return nil + } + + s.Stop() + defer s.Start() + s.showScaleDialog(path) + + return nil +} + +func (s *ScaleExtender) showScaleDialog(path string) { + confirm := tview.NewModalForm("", s.makeScaleForm(path)) + confirm.SetText(fmt.Sprintf("Scale %s %s", s.GVR(), path)) + confirm.SetDoneFunc(func(int, string) { + s.dismissDialog() + }) + s.App().Content.AddPage(scaleDialogKey, confirm, false, false) + s.App().Content.ShowPage(scaleDialogKey) +} + +func (s *ScaleExtender) makeScaleForm(sel string) *tview.Form { + f := s.makeStyledForm() + replicas := strings.TrimSpace(s.GetTable().GetCell(s.GetTable().GetSelectedRowIndex(), s.GetTable().NameColIndex()+1).Text) + tokens := strings.Split(replicas, "/") + replicas = tokens[1] + f.AddInputField("Replicas:", replicas, 4, func(textToCheck string, lastChar rune) bool { + _, err := strconv.Atoi(textToCheck) + return err == nil + }, func(changed string) { + replicas = changed + }) + + f.AddButton("OK", func() { + defer s.dismissDialog() + count, err := strconv.Atoi(replicas) + if err != nil { + s.App().Flash().Err(err) + return + } + if err := s.scale(sel, count); err != nil { + log.Error().Err(err).Msgf("DP %s scaling failed", sel) + s.App().Flash().Err(err) + } else { + s.App().Flash().Infof("Resource %s:%s scaled successfully", s.GVR(), sel) + } + }) + + f.AddButton("Cancel", func() { + s.dismissDialog() + }) + + return f +} + +func (s *ScaleExtender) dismissDialog() { + s.App().Content.RemovePage(scaleDialogKey) +} + +func (s *ScaleExtender) makeStyledForm() *tview.Form { + f := tview.NewForm() + f.SetItemPadding(0) + f.SetButtonsAlign(tview.AlignCenter). + SetButtonBackgroundColor(tview.Styles.PrimitiveBackgroundColor). + SetButtonTextColor(tview.Styles.PrimaryTextColor). + SetLabelColor(tcell.ColorAqua). + SetFieldTextColor(tcell.ColorOrange) + + return f +} + +func (s *ScaleExtender) scale(path string, replicas int) error { + res, err := dao.AccessorFor(s.App().factory, client.GVR(s.GVR())) + if err != nil { + return nil + } + scaler, ok := res.(dao.Scalable) + if !ok { + return fmt.Errorf("expecting a scalable resource for %q", s.GVR()) + } + + return scaler.Scale(path, int32(replicas)) +} diff --git a/internal/view/screen_dump.go b/internal/view/screen_dump.go new file mode 100644 index 00000000..50c13cec --- /dev/null +++ b/internal/view/screen_dump.go @@ -0,0 +1,50 @@ +package view + +import ( + "context" + "errors" + "path/filepath" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/render" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" +) + +// ScreenDump presents a directory listing viewer. +type ScreenDump struct { + ResourceViewer +} + +// NewScreenDump returns a new viewer. +func NewScreenDump(gvr client.GVR) ResourceViewer { + s := ScreenDump{ + ResourceViewer: NewBrowser(gvr), + } + s.GetTable().SetBorderFocusColor(tcell.ColorSteelBlue) + s.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorRoyalBlue, tcell.AttrNone) + s.GetTable().SetColorerFn(render.ScreenDump{}.ColorerFunc()) + s.GetTable().SetSortCol(s.GetTable().NameColIndex(), 0, true) + s.GetTable().SelectRow(1, true) + s.GetTable().SetEnterFn(s.edit) + s.SetContextFn(s.dirContext) + + return &s +} + +func (s *ScreenDump) dirContext(ctx context.Context) context.Context { + dir := filepath.Join(config.K9sDumpDir, s.App().Config.K9s.CurrentCluster) + return context.WithValue(ctx, internal.KeyDir, dir) +} + +func (s *ScreenDump) edit(app *App, ns, resource, path string) { + log.Debug().Msgf("ScreenDump selection is %q", path) + + s.Stop() + defer s.Start() + if !edit(true, app, path) { + app.Flash().Err(errors.New("Failed to launch editor")) + } +} diff --git a/internal/view/screen_dump_test.go b/internal/view/screen_dump_test.go new file mode 100644 index 00000000..55770d41 --- /dev/null +++ b/internal/view/screen_dump_test.go @@ -0,0 +1,17 @@ +package view_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/view" + "github.com/stretchr/testify/assert" +) + +func TestScreenDumpNew(t *testing.T) { + po := view.NewScreenDump(client.GVR("screendumps")) + + assert.Nil(t, po.Init(makeCtx())) + assert.Equal(t, "ScreenDumps", po.Name()) + assert.Equal(t, 2, len(po.Hints())) +} diff --git a/internal/view/secret.go b/internal/view/secret.go new file mode 100644 index 00000000..6dfbaeaf --- /dev/null +++ b/internal/view/secret.go @@ -0,0 +1,75 @@ +package view + +import ( + "sigs.k8s.io/yaml" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/ui" + "github.com/gdamore/tcell" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" +) + +// Secret presents a secret viewer. +type Secret struct { + ResourceViewer +} + +// NewSecrets returns a new viewer. +func NewSecret(gvr client.GVR) ResourceViewer { + s := Secret{ + ResourceViewer: NewBrowser(gvr), + } + s.SetBindKeysFn(s.bindKeys) + + return &s +} + +func (s *Secret) bindKeys(aa ui.KeyActions) { + aa.Add(ui.KeyActions{ + tcell.KeyCtrlX: ui.NewKeyAction("Decode", s.decodeCmd, true), + }) +} + +func (s *Secret) decodeCmd(evt *tcell.EventKey) *tcell.EventKey { + path := s.GetTable().GetSelectedItem() + if path == "" { + return evt + } + + o, err := s.App().factory.Get("v1/secrets", path, labels.Everything()) + if err != nil { + s.App().Flash().Err(err) + return nil + } + + var secret v1.Secret + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &secret) + if err != nil { + s.App().Flash().Err(err) + return nil + } + + d := make(map[string]string, len(secret.Data)) + for k, val := range secret.Data { + d[k] = string(val) + } + raw, err := yaml.Marshal(d) + if err != nil { + s.App().Flash().Errf("Error decoding secret %s", err) + return nil + } + + details := NewDetails("Decoder") + details.SetSubject(path) + details.SetTextColor(s.App().Styles.FgColor()) + details.SetText(colorizeYAML(s.App().Styles.Views().Yaml, string(raw))) + details.ScrollToBeginning() + if err := s.App().inject(details); err != nil { + s.App().Flash().Err(err) + } + + return nil +} diff --git a/internal/view/secret_test.go b/internal/view/secret_test.go new file mode 100644 index 00000000..d7effb2c --- /dev/null +++ b/internal/view/secret_test.go @@ -0,0 +1,17 @@ +package view_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/view" + "github.com/stretchr/testify/assert" +) + +func TestSecretNew(t *testing.T) { + s := view.NewSecret(client.GVR("v1/secrets")) + + assert.Nil(t, s.Init(makeCtx())) + assert.Equal(t, "Secrets", s.Name()) + assert.Equal(t, 3, len(s.Hints())) +} diff --git a/internal/view/sts.go b/internal/view/sts.go new file mode 100644 index 00000000..13842ade --- /dev/null +++ b/internal/view/sts.go @@ -0,0 +1,57 @@ +package view + +import ( + "strings" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" +) + +// StatefulSet represents a statefulset viewer. +type StatefulSet struct { + ResourceViewer +} + +// NewStatefulSet returns a new viewer. +func NewStatefulSet(gvr client.GVR) ResourceViewer { + s := StatefulSet{ + ResourceViewer: NewRestartExtender( + NewScaleExtender( + NewLogsExtender(NewBrowser(gvr), nil), + ), + ), + } + s.SetBindKeysFn(s.bindKeys) + s.GetTable().SetEnterFn(s.showPods) + s.GetTable().SetColorerFn(render.StatefulSet{}.ColorerFunc()) + + return &s +} + +func (s *StatefulSet) bindKeys(aa ui.KeyActions) { + aa.Add(ui.KeyActions{ + ui.KeyShiftD: ui.NewKeyAction("Sort Desired", s.GetTable().SortColCmd(1, true), false), + ui.KeyShiftC: ui.NewKeyAction("Sort Current", s.GetTable().SortColCmd(2, true), false), + }) +} + +func (s *StatefulSet) showPods(app *App, _, gvr, path string) { + o, err := app.factory.Get(s.GVR(), path, labels.Everything()) + if err != nil { + app.Flash().Err(err) + return + } + + var sts appsv1.StatefulSet + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &sts) + if err != nil { + app.Flash().Err(err) + } + + showPodsFromSelector(app, strings.Replace(path, "/", "::", 1), sts.Spec.Selector) +} diff --git a/internal/view/sts_test.go b/internal/view/sts_test.go new file mode 100644 index 00000000..3ae4e794 --- /dev/null +++ b/internal/view/sts_test.go @@ -0,0 +1,17 @@ +package view_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/view" + "github.com/stretchr/testify/assert" +) + +func TestStatefulSetNew(t *testing.T) { + s := view.NewStatefulSet(client.GVR("apps/v1/statefulsets")) + + assert.Nil(t, s.Init(makeCtx())) + assert.Equal(t, "StatefulSets", s.Name()) + assert.Equal(t, 7, len(s.Hints())) +} diff --git a/internal/view/svc.go b/internal/view/svc.go new file mode 100644 index 00000000..0cd29f34 --- /dev/null +++ b/internal/view/svc.go @@ -0,0 +1,183 @@ +package view + +import ( + "errors" + "fmt" + "strings" + "time" + + "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/gdamore/tcell" + "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" +) + +// Service represents a service viewer. +type Service struct { + ResourceViewer + + bench *perf.Benchmark +} + +// NewService returns a new viewer. +func NewService(gvr client.GVR) ResourceViewer { + s := Service{ + ResourceViewer: NewLogsExtender(NewBrowser(gvr), nil), + } + s.SetBindKeysFn(s.bindKeys) + s.GetTable().SetEnterFn(s.showPods) + + return &s +} + +// Protocol... + +func (s *Service) bindKeys(aa ui.KeyActions) { + aa.Add(ui.KeyActions{ + tcell.KeyCtrlB: ui.NewKeyAction("Bench", s.benchCmd, true), + tcell.KeyCtrlK: ui.NewKeyAction("Bench Stop", s.benchStopCmd, true), + ui.KeyShiftT: ui.NewKeyAction("Sort Type", s.GetTable().SortColCmd(1, true), false), + }) +} + +func (s *Service) showPods(app *App, ns, gvr, path string) { + log.Debug().Msgf("SVC SHOW PODS %q", path) + o, err := app.factory.Get(gvr, path, labels.Everything()) + if err != nil { + app.Flash().Err(err) + return + } + + var svc v1.Service + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &svc) + if err != nil { + app.Flash().Err(err) + return + } + + showPodsWithLabels(app, path, svc.Spec.Selector) +} + +func (s *Service) benchStopCmd(evt *tcell.EventKey) *tcell.EventKey { + if s.bench != nil { + log.Debug().Msg(">>> Benchmark canceled!!") + s.App().status(ui.FlashErr, "Benchmark Canceled!") + s.bench.Cancel() + } + s.App().StatusReset() + + return nil +} + +func (s *Service) checkSvc(row int) error { + svcType := trimCellRelative(s.GetTable(), row, 1) + if svcType != "NodePort" && svcType != "LoadBalancer" { + return errors.New("You must select a reachable service") + } + return nil +} + +func (s *Service) getExternalPort(row int) (string, error) { + ports := trimCellRelative(s.GetTable(), row, 5) + + pp := strings.Split(ports, " ") + if len(pp) == 0 { + return "", errors.New("No ports found") + } + + // Grap the first port pair for now... + tokens := strings.Split(pp[0], "►") + if len(tokens) < 2 { + return "", errors.New("No ports pair found") + } + + return tokens[1], nil +} + +func (s *Service) reloadBenchCfg() error { + path := ui.BenchConfig(s.App().Config.K9s.CurrentCluster) + return s.App().Bench.Reload(path) +} + +func (s *Service) benchCmd(evt *tcell.EventKey) *tcell.EventKey { + sel := s.GetTable().GetSelectedItem() + if sel == "" || s.bench != nil { + return evt + } + + if err := s.reloadBenchCfg(); err != nil { + s.App().Flash().Err(err) + return nil + } + + cfg, ok := s.App().Bench.Benchmarks.Services[sel] + if !ok { + s.App().Flash().Errf("No bench config found for service %s", sel) + return nil + } + cfg.Name = sel + log.Debug().Msgf("Benchmark config %#v", cfg) + + row := s.GetTable().GetSelectedRowIndex() + if err := s.checkSvc(row); err != nil { + s.App().Flash().Err(err) + return nil + } + port, err := s.getExternalPort(row) + if err != nil { + s.App().Flash().Err(err) + return nil + } + if err := s.runBenchmark(port, cfg); err != nil { + s.App().Flash().Errf("Benchmark failed %v", err) + s.App().StatusReset() + s.bench = nil + } + + return nil +} + +func (s *Service) runBenchmark(port string, cfg config.BenchConfig) error { + if cfg.HTTP.Host == "" { + return fmt.Errorf("Invalid benchmark host %q", cfg.HTTP.Host) + } + + var err error + base := "http://" + cfg.HTTP.Host + ":" + port + cfg.HTTP.Path + if s.bench, err = perf.NewBenchmark(base, cfg); err != nil { + return err + } + + s.App().status(ui.FlashWarn, "Benchmark in progress...") + log.Debug().Msg("Bench starting...") + go s.bench.Run(s.App().Config.K9s.CurrentCluster, s.benchDone) + + return nil +} + +func (s *Service) benchDone() { + log.Debug().Msg("Bench Completed!") + s.App().QueueUpdate(func() { + if s.bench.Canceled() { + s.App().status(ui.FlashInfo, "Benchmark canceled") + } else { + s.App().status(ui.FlashInfo, "Benchmark Completed!") + s.bench.Cancel() + } + s.bench = nil + go benchTimedOut(s.App()) + }) +} + +func benchTimedOut(app *App) { + <-time.After(2 * time.Second) + app.QueueUpdate(func() { + app.StatusReset() + }) +} diff --git a/internal/view/svc_test.go b/internal/view/svc_test.go new file mode 100644 index 00000000..f9ebc3ac --- /dev/null +++ b/internal/view/svc_test.go @@ -0,0 +1,136 @@ +package view_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/view" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func init() { + dao.RegisterMeta("v1/pods", metav1.APIResource{ + Name: "pods", + SingularName: "pod", + Namespaced: true, + Kind: "Pods", + Verbs: []string{"get", "list", "watch", "delete"}, + Categories: []string{"k9s"}, + }) + dao.RegisterMeta("v1/namespaces", metav1.APIResource{ + Name: "namespaces", + SingularName: "namespace", + Namespaced: true, + Kind: "Namespaces", + Verbs: []string{"get", "list", "watch", "delete"}, + Categories: []string{"k9s"}, + }) + dao.RegisterMeta("v1/services", metav1.APIResource{ + Name: "services", + SingularName: "service", + Namespaced: true, + Kind: "Services", + Verbs: []string{"get", "list", "watch", "delete"}, + Categories: []string{"k9s"}, + }) + dao.RegisterMeta("v1/secrets", metav1.APIResource{ + Name: "secrets", + SingularName: "secret", + Namespaced: true, + Kind: "Secrets", + Verbs: []string{"get", "list", "watch", "delete"}, + Categories: []string{"k9s"}, + }) + + dao.RegisterMeta("aliases", metav1.APIResource{ + Name: "aliases", + SingularName: "alias", + Namespaced: true, + Kind: "Aliases", + Verbs: []string{"get", "list", "watch", "delete"}, + Categories: []string{"k9s"}, + }) + dao.RegisterMeta("containers", metav1.APIResource{ + Name: "containers", + SingularName: "container", + Namespaced: true, + Kind: "Containers", + Verbs: []string{"get", "list", "watch", "delete"}, + Categories: []string{"k9s"}, + }) + dao.RegisterMeta("contexts", metav1.APIResource{ + Name: "contexts", + SingularName: "context", + Namespaced: true, + Kind: "Contexts", + Verbs: []string{"get", "list", "watch", "delete"}, + Categories: []string{"k9s"}, + }) + dao.RegisterMeta("subjects", metav1.APIResource{ + Name: "subjects", + SingularName: "subject", + Namespaced: true, + Kind: "Subjects", + Verbs: []string{"get", "list", "watch", "delete"}, + Categories: []string{"k9s"}, + }) + dao.RegisterMeta("rbac", metav1.APIResource{ + Name: "rbacs", + SingularName: "rbac", + Namespaced: true, + Kind: "Rbac", + Verbs: []string{"get", "list", "watch", "delete"}, + Categories: []string{"k9s"}, + }) + dao.RegisterMeta("portforwards", metav1.APIResource{ + Name: "portforwards", + SingularName: "portforward", + Namespaced: true, + Kind: "PortForwards", + Verbs: []string{"get", "list", "watch", "delete"}, + Categories: []string{"k9s"}, + }) + + dao.RegisterMeta("screendumps", metav1.APIResource{ + Name: "screendumps", + SingularName: "screendump", + Namespaced: true, + Kind: "ScreenDumps", + Verbs: []string{"get", "list", "watch", "delete"}, + Categories: []string{"k9s"}, + }) + dao.RegisterMeta("apps/v1/statefulsets", metav1.APIResource{ + Name: "statefulsets", + SingularName: "statefulset", + Namespaced: true, + Kind: "StatefulSets", + Verbs: []string{"get", "list", "watch", "delete"}, + Categories: []string{"k9s"}, + }) + dao.RegisterMeta("apps/v1/daemonsets", metav1.APIResource{ + Name: "daemonsets", + SingularName: "daemonset", + Namespaced: true, + Kind: "DaemonSets", + Verbs: []string{"get", "list", "watch", "delete"}, + Categories: []string{"k9s"}, + }) + dao.RegisterMeta("apps/v1/deployments", metav1.APIResource{ + Name: "deployments", + SingularName: "deployment", + Namespaced: true, + Kind: "Deployments", + Verbs: []string{"get", "list", "watch", "delete"}, + Categories: []string{"k9s"}, + }) +} + +func TestServiceNew(t *testing.T) { + s := view.NewService(client.GVR("v1/services")) + + assert.Nil(t, s.Init(makeCtx())) + assert.Equal(t, "Services", s.Name()) + assert.Equal(t, 7, len(s.Hints())) +} diff --git a/internal/view/table.go b/internal/view/table.go new file mode 100644 index 00000000..e4ecd792 --- /dev/null +++ b/internal/view/table.go @@ -0,0 +1,150 @@ +package view + +import ( + "context" + "time" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/ui" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" +) + +type Table struct { + *ui.Table + + app *App + enterFn EnterFunc +} + +func NewTable(gvr string) *Table { + return &Table{ + Table: ui.NewTable(gvr), + } +} + +// Init initializes the component +func (t *Table) Init(ctx context.Context) (err error) { + if t.app, err = extractApp(ctx); err != nil { + return err + } + ctx = context.WithValue(ctx, internal.KeyStyles, t.app.Styles) + t.Table.Init(ctx) + t.bindKeys() + + t.GetModel().SetRefreshRate(time.Duration(t.app.Config.K9s.GetRefreshRate()) * time.Second) + + return nil +} + +// Name returns the table name. +func (t *Table) Name() string { return t.BaseTitle } + +// App returns the current app handle. +func (t *Table) App() *App { + return t.app +} + +// Start runs the component. +func (t *Table) Start() { + t.Stop() + t.SearchBuff().AddListener(t.app.Cmd()) + t.SearchBuff().AddListener(t) +} + +// Stop terminates the component. +func (t *Table) Stop() { + t.SearchBuff().RemoveListener(t.app.Cmd()) + t.SearchBuff().RemoveListener(t) +} + +// SetEnterFn specifies the default enter behavior. +func (t *Table) SetEnterFn(f EnterFunc) { + t.enterFn = f +} + +// SetExtraActionsFn specifies custom keyboard behavior. +func (t *Table) SetExtraActionsFn(BoostActionsFunc) {} + +// BufferChanged indicates the buffer was changed. +func (t *Table) BufferChanged(s string) {} + +// BufferActive indicates the buff activity changed. +func (t *Table) BufferActive(state bool, k ui.BufferKind) { + t.app.BufferActive(state, k) +} + +func (t *Table) saveCmd(evt *tcell.EventKey) *tcell.EventKey { + if path, err := saveTable(t.app.Config.K9s.CurrentCluster, t.BaseTitle, t.Path, t.GetFilteredData()); err != nil { + t.app.Flash().Err(err) + } else { + t.app.Flash().Infof("File %s saved successfully!", path) + } + + return nil +} + +func (t *Table) bindKeys() { + t.Actions().Add(ui.KeyActions{ + ui.KeySpace: ui.NewSharedKeyAction("Mark", t.markCmd, false), + tcell.KeyCtrlSpace: ui.NewSharedKeyAction("Marks Clear", t.clearMarksCmd, false), + tcell.KeyCtrlS: ui.NewSharedKeyAction("Save", t.saveCmd, false), + ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", t.activateCmd, false), + tcell.KeyCtrlU: ui.NewSharedKeyAction("Clear Filter", t.clearCmd, false), + tcell.KeyBackspace2: ui.NewSharedKeyAction("Erase", t.eraseCmd, false), + tcell.KeyBackspace: ui.NewSharedKeyAction("Erase", t.eraseCmd, false), + tcell.KeyDelete: ui.NewSharedKeyAction("Erase", t.eraseCmd, false), + ui.KeyShiftN: ui.NewKeyAction("Sort Name", t.SortColCmd(0, true), false), + ui.KeyShiftA: ui.NewKeyAction("Sort Age", t.SortColCmd(-1, true), false), + }) +} + +func (t *Table) markCmd(evt *tcell.EventKey) *tcell.EventKey { + path := t.GetSelectedItem() + if path == "" { + return evt + } + t.ToggleMark() + t.Refresh() + + return nil +} + +func (t *Table) clearMarksCmd(evt *tcell.EventKey) *tcell.EventKey { + path := t.GetSelectedItem() + if path == "" { + return evt + } + t.ClearMarks() + + return nil +} + +func (t *Table) clearCmd(evt *tcell.EventKey) *tcell.EventKey { + if !t.SearchBuff().IsActive() { + return evt + } + t.SearchBuff().Clear() + + return nil +} + +func (t *Table) eraseCmd(evt *tcell.EventKey) *tcell.EventKey { + if t.SearchBuff().IsActive() { + t.SearchBuff().Delete() + } + + return nil +} + +func (t *Table) activateCmd(evt *tcell.EventKey) *tcell.EventKey { + log.Debug().Msgf("Table filter activated!") + if t.app.InCmdMode() { + log.Debug().Msgf("App Is in Command mode!") + return evt + } + t.app.Flash().Info("Filter mode activated.") + t.SearchBuff().SetActive(true) + + return nil +} diff --git a/internal/view/table_helper.go b/internal/view/table_helper.go new file mode 100644 index 00000000..86993479 --- /dev/null +++ b/internal/view/table_helper.go @@ -0,0 +1,83 @@ +package view + +import ( + "encoding/csv" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + "github.com/rs/zerolog/log" +) + +func trimCellRelative(t *Table, row, col int) string { + return ui.TrimCell(t.SelectTable, row, t.NameColIndex()+col) +} + +func computeFilename(cluster, ns, title, path string) (string, error) { + now := time.Now().UnixNano() + + dir := filepath.Join(config.K9sDumpDir, cluster) + if err := ensureDir(dir); err != nil { + return "", err + } + + name := title + "-" + strings.Replace(path, "/", "-", -1) + if path == "" { + name = title + } + + var fName string + if ns == render.ClusterScope { + fName = fmt.Sprintf(ui.NoNSFmat, name, now) + } else { + fName = fmt.Sprintf(ui.FullFmat, name, ns, now) + } + + return strings.ToLower(filepath.Join(dir, fName)), nil +} + +func saveTable(cluster, title, path string, data render.TableData) (string, error) { + ns := data.Namespace + if ns == render.ClusterScope { + ns = render.NamespaceAll + } + + fPath, err := computeFilename(cluster, ns, title, path) + if err != nil { + return "", err + } + log.Debug().Msgf("Saving Table to %s", fPath) + + mod := os.O_CREATE | os.O_WRONLY + out, err := os.OpenFile(fPath, mod, 0600) + if err != nil { + return "", err + } + defer func() { + if err := out.Close(); err != nil { + log.Error().Err(err).Msg("Closing file") + } + }() + + w := csv.NewWriter(out) + if err := w.Write(data.Header.Columns()); err != nil { + return "", err + } + + for _, re := range data.RowEvents { + if err := w.Write(re.Row.Fields); err != nil { + return "", err + } + } + w.Flush() + if err := w.Error(); err != nil { + return "", err + } + + return fPath, nil +} diff --git a/internal/view/table_int_test.go b/internal/view/table_int_test.go new file mode 100644 index 00000000..ffdfa0e6 --- /dev/null +++ b/internal/view/table_int_test.go @@ -0,0 +1,155 @@ +package view + +import ( + "context" + "io/ioutil" + "path/filepath" + "testing" + "time" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/config" + "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" +) + +func TestTableSave(t *testing.T) { + v := NewTable("test") + v.Init(makeContext()) + v.SetTitle("k9s-test") + + dir := filepath.Join(config.K9sDumpDir, v.app.Config.K9s.CurrentCluster) + c1, _ := ioutil.ReadDir(dir) + v.saveCmd(nil) + + c2, _ := ioutil.ReadDir(dir) + assert.Equal(t, len(c2), len(c1)+1) +} + +func TestTableNew(t *testing.T) { + v := NewTable("test") + v.Init(makeContext()) + + data := render.NewTableData() + data.Header = render.HeaderRow{ + render.Header{Name: "NAMESPACE"}, + render.Header{Name: "NAME", Align: tview.AlignRight}, + render.Header{Name: "FRED"}, + render.Header{Name: "AGE", Decorator: render.AgeDecorator}, + } + data.RowEvents = render.RowEvents{ + render.RowEvent{ + Row: render.Row{ + Fields: render.Fields{"ns1", "a", "10", "3m"}, + }, + }, + render.RowEvent{ + Row: render.Row{ + Fields: render.Fields{"ns1", "b", "15", "1m"}, + }, + }, + } + data.Namespace = "" + + v.Update(*data) + assert.Equal(t, 3, v.GetRowCount()) +} + +func TestTableViewFilter(t *testing.T) { + v := NewTable("test") + v.Init(makeContext()) + v.SetModel(&testTableModel{}) + v.SearchBuff().SetActive(true) + v.SearchBuff().Set("blee") + v.Refresh() + assert.Equal(t, 2, v.GetRowCount()) +} + +func TestTableViewSort(t *testing.T) { + v := NewTable("test") + v.Init(makeContext()) + v.SetModel(&testTableModel{}) + v.SortColCmd(1, true)(nil) + assert.Equal(t, 3, v.GetRowCount()) + assert.Equal(t, "blee", v.GetCell(1, 1).Text) + + v.SortInvertCmd(nil) + assert.Equal(t, 3, v.GetRowCount()) + assert.Equal(t, "fred", v.GetCell(1, 1).Text) +} + +// ---------------------------------------------------------------------------- +// Helpers... + +type testTableModel struct{} + +var _ ui.Tabular = &testTableModel{} + +func (t *testTableModel) Empty() bool { return false } +func (t *testTableModel) Peek() render.TableData { return makeTableData() } +func (t *testTableModel) ClusterWide() bool { return false } +func (t *testTableModel) GetNamespace() string { return "blee" } +func (t *testTableModel) SetNamespace(string) {} +func (t *testTableModel) AddListener(model.TableListener) {} +func (t *testTableModel) Watch(context.Context) {} +func (t *testTableModel) InNamespace(string) bool { return true } +func (t *testTableModel) SetRefreshRate(time.Duration) {} + +func makeTableData() render.TableData { + t := render.NewTableData() + + t.Header = render.HeaderRow{ + render.Header{Name: "NAMESPACE"}, + render.Header{Name: "NAME", Align: tview.AlignRight}, + render.Header{Name: "FRED"}, + render.Header{Name: "AGE", Decorator: render.AgeDecorator}, + } + t.RowEvents = render.RowEvents{ + render.RowEvent{ + Row: render.Row{ + Fields: render.Fields{"ns1", "blee", "10", "3m"}, + }, + }, + render.RowEvent{ + Row: render.Row{ + Fields: render.Fields{"ns1", "fred", "15", "1m"}, + }, + Deltas: render.DeltaRow{"", "", "20", ""}, + }, + } + t.Namespace = "" + + return *t +} + +func makeContext() context.Context { + a := NewApp(config.NewConfig(ks{})) + 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() ([]string, error) { + return []string{"test"}, nil +} + +func (k ks) NamespaceNames(nn []v1.Namespace) []string { + return []string{"test"} +} diff --git a/internal/view/types.go b/internal/view/types.go new file mode 100644 index 00000000..c44ecc98 --- /dev/null +++ b/internal/view/types.go @@ -0,0 +1,105 @@ +package view + +import ( + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/ui" +) + +type ( + // EnvFunc represent the current view exposed environment. + EnvFunc func() K9sEnv + + // BoostActionFunc extends viewer keyboard actions. + BoostActionsFunc func(ui.KeyActions) + + // EnterFunc represents an enter key action. + EnterFunc func(app *App, ns, resource, selection string) + + // ContainerFunc returns the active container name. + ContainerFunc func() string +) + +// ActionExtender enhances a given viewer by adding new menu actions. +type ActionExtender interface { + // BindKeys injects new menu actions. + BindKeys(ResourceViewer) +} + +// Hinter represents a view that can produce menu hints. +type Hinter interface { + // Hints returns a collection of hints. + Hints() model.MenuHints +} + +// Viewer represents a component viewer. +type Viewer interface { + model.Component + + // Actions returns active menu bindings. + Actions() ui.KeyActions + + // App returns an app handle. + App() *App + + // Refresh updates the viewer + Refresh() +} + +// TableViewer represents a tabular viewer. +type TableViewer interface { + Viewer + + // Table returns a table component. + GetTable() *Table +} + +// ResourceViewer represents a generic resource viewer. +type ResourceViewer interface { + TableViewer + + // SetEnvFn sets a function to pull viewer env vars for plugins. + SetEnvFn(EnvFunc) + + // GVR returns a resource descriptor. + GVR() string + + // SetContextFn provision a custom context. + SetContextFn(ContextFunc) + + // SetBindKeys provision additional key bindings. + SetBindKeysFn(BindKeysFunc) +} + +type LogViewer interface { + ResourceViewer + + ShowLogs(prev bool) +} + +type RestartableViewer interface { + LogViewer +} + +type ScalableViewer interface { + LogViewer +} + +// SubjectViewer represents a policy viewer. +type SubjectViewer interface { + ResourceViewer + + // SetSubject sets the active subject. + SetSubject(s string) +} + +type ViewerFunc func(client.GVR) ResourceViewer + +// MetaViewer represents a registered meta viewer. +type MetaViewer struct { + viewerFn ViewerFunc + enterFn EnterFunc +} + +// MetaViewers represents a collection of meta viewers. +type MetaViewers map[client.GVR]MetaViewer diff --git a/internal/view/user.go b/internal/view/user.go new file mode 100644 index 00000000..6a363c2e --- /dev/null +++ b/internal/view/user.go @@ -0,0 +1,50 @@ +package view + +import ( + "context" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + "github.com/gdamore/tcell" +) + +// User presents a user viewer. +type User struct { + ResourceViewer +} + +// NewUser returns a new subject viewer. +func NewUser(gvr client.GVR) ResourceViewer { + u := User{ResourceViewer: NewBrowser(gvr)} + u.GetTable().SetColorerFn(render.Subject{}.ColorerFunc()) + u.SetBindKeysFn(u.bindKeys) + u.SetContextFn(u.subjectCtx) + + return &u +} + +func (u *User) bindKeys(aa ui.KeyActions) { + aa.Delete(ui.KeyShiftA, ui.KeyShiftP, tcell.KeyCtrlSpace, ui.KeySpace) + aa.Add(ui.KeyActions{ + tcell.KeyEnter: ui.NewKeyAction("Rules", u.policyCmd, true), + ui.KeyShiftK: ui.NewKeyAction("Sort Kind", u.GetTable().SortColCmd(1, true), false), + }) +} + +func (u *User) subjectCtx(ctx context.Context) context.Context { + return context.WithValue(ctx, internal.KeySubjectKind, "User") +} + +func (u *User) policyCmd(evt *tcell.EventKey) *tcell.EventKey { + path := u.GetTable().GetSelectedItem() + if path == "" { + return evt + } + if err := u.App().inject(NewPolicy(u.App(), "User", path)); err != nil { + u.App().Flash().Err(err) + } + + return nil +} diff --git a/internal/views/yaml.go b/internal/view/yaml.go similarity index 90% rename from internal/views/yaml.go rename to internal/view/yaml.go index 0795d5bf..a25580b9 100644 --- a/internal/views/yaml.go +++ b/internal/view/yaml.go @@ -1,4 +1,4 @@ -package views +package view import ( "fmt" @@ -68,17 +68,17 @@ func saveYAML(cluster, name, data string) (string, error) { path := filepath.Join(dir, fName) mod := os.O_CREATE | os.O_WRONLY - file, err := os.OpenFile(path, mod, 0644) - defer func() { - if file != nil { - file.Close() - } - }() + file, err := os.OpenFile(path, mod, 0600) if err != nil { log.Error().Err(err).Msgf("YAML create %s", path) return "", nil } - if _, err := fmt.Fprintf(file, data); err != nil { + defer func() { + if err := file.Close(); err != nil { + log.Error().Err(err).Msg("Closing yaml file") + } + }() + if _, err := file.Write([]byte(data)); err != nil { return "", err } diff --git a/internal/views/yaml_test.go b/internal/view/yaml_test.go similarity index 95% rename from internal/views/yaml_test.go rename to internal/view/yaml_test.go index 860fed2b..41451559 100644 --- a/internal/views/yaml_test.go +++ b/internal/view/yaml_test.go @@ -1,4 +1,4 @@ -package views +package view import ( "testing" @@ -49,7 +49,7 @@ func TestYaml(t *testing.T) { }, } - s, _ := config.NewStyles("skins/stock.yml") + s := config.NewStyles() for _, u := range uu { assert.Equal(t, u.e, colorizeYAML(s.Views().Yaml, u.s)) } diff --git a/internal/views/alias.go b/internal/views/alias.go deleted file mode 100644 index 7551e55e..00000000 --- a/internal/views/alias.go +++ /dev/null @@ -1,142 +0,0 @@ -package views - -import ( - "context" - "fmt" - "strings" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/gdamore/tcell" -) - -const ( - aliasTitle = "Aliases" - aliasTitleFmt = " [mediumseagreen::b]%s([fuchsia::b]%d[fuchsia::-][mediumseagreen::-]) " -) - -type aliasView struct { - *tableView - - app *appView - current ui.Igniter - cancel context.CancelFunc -} - -func newAliasView(app *appView, current ui.Igniter) *aliasView { - v := aliasView{ - tableView: newTableView(app, aliasTitle), - app: app, - } - v.SetBorderFocusColor(tcell.ColorMediumSpringGreen) - v.SetSelectedStyle(tcell.ColorWhite, tcell.ColorMediumSpringGreen, tcell.AttrNone) - v.SetColorerFn(aliasColorer) - v.current = current - v.SetActiveNS("") - v.registerActions() - - return &v -} - -// Init the view. -func (v *aliasView) Init(context.Context, string) { - v.Update(v.hydrate()) - v.app.SetFocus(v) - v.resetTitle() - v.app.SetHints(v.Hints()) -} - -func (v *aliasView) registerActions() { - v.RmAction(ui.KeyShiftA) - v.RmAction(ui.KeyShiftN) - v.RmAction(tcell.KeyCtrlS) - - v.SetActions(ui.KeyActions{ - tcell.KeyEnter: ui.NewKeyAction("Goto", v.gotoCmd, true), - tcell.KeyEscape: ui.NewKeyAction("Reset", v.resetCmd, false), - ui.KeySlash: ui.NewKeyAction("Filter", v.activateCmd, false), - ui.KeyShiftR: ui.NewKeyAction("Sort Resource", v.SortColCmd(0), false), - ui.KeyShiftC: ui.NewKeyAction("Sort Command", v.SortColCmd(1), false), - ui.KeyShiftA: ui.NewKeyAction("Sort ApiGroup", v.SortColCmd(2), false), - }) -} - -func (v *aliasView) getTitle() string { - return aliasTitle -} - -func (v *aliasView) resetCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.SearchBuff().Empty() { - v.SearchBuff().Reset() - return nil - } - - return v.backCmd(evt) -} - -func (v *aliasView) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { - r, _ := v.GetSelection() - if r != 0 { - s := ui.TrimCell(v.Table, r, 1) - tokens := strings.Split(s, ",") - v.app.gotoResource(tokens[0], true) - return nil - } - - if v.SearchBuff().IsActive() { - return v.activateCmd(evt) - } - return evt -} - -func (v *aliasView) backCmd(evt *tcell.EventKey) *tcell.EventKey { - if v.cancel != nil { - v.cancel() - } - - if v.SearchBuff().IsActive() { - v.SearchBuff().Reset() - } else { - v.app.inject(v.current) - } - - return nil -} - -func (v *aliasView) hydrate() resource.TableData { - data := resource.TableData{ - Header: resource.Row{"RESOURCE", "COMMAND", "APIGROUP"}, - Rows: make(resource.RowEvents, len(aliases.Alias)), - Namespace: resource.NotNamespaced, - } - - aa := make(map[string][]string, len(aliases.Alias)) - for alias, gvr := range aliases.Alias { - if _, ok := aa[gvr]; ok { - aa[gvr] = append(aa[gvr], alias) - } else { - aa[gvr] = []string{alias} - } - } - - for gvr, aliases := range aa { - g := k8s.GVR(gvr) - fields := resource.Row{ - ui.Pad(g.ToR(), 30), - ui.Pad(strings.Join(aliases, ","), 70), - ui.Pad(g.ToG(), 30), - } - data.Rows[string(gvr)] = &resource.RowEvent{ - Action: resource.New, - Fields: fields, - Deltas: fields, - } - } - - return data -} - -func (v *aliasView) resetTitle() { - v.SetTitle(fmt.Sprintf(aliasTitleFmt, aliasTitle, v.GetRowCount()-1)) -} diff --git a/internal/views/alias_test.go b/internal/views/alias_test.go deleted file mode 100644 index 88935fae..00000000 --- a/internal/views/alias_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package views - -import ( - "testing" - - "github.com/derailed/k9s/internal/config" - "github.com/stretchr/testify/assert" -) - -func TestAliasView(t *testing.T) { - v := newAliasView(NewApp(config.NewConfig(ks{})), nil) - td := v.hydrate() - v.Init(nil, "") - - assert.Equal(t, 3, len(td.Header)) - assert.Equal(t, 15, len(td.Rows)) - assert.Equal(t, "Aliases", v.getTitle()) -} diff --git a/internal/views/app.go b/internal/views/app.go deleted file mode 100644 index ea56e453..00000000 --- a/internal/views/app.go +++ /dev/null @@ -1,433 +0,0 @@ -package views - -import ( - "context" - "fmt" - "time" - - "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/derailed/k9s/internal/watch" - "github.com/derailed/tview" - "github.com/gdamore/tcell" - "github.com/rs/zerolog/log" - "k8s.io/client-go/tools/portforward" -) - -const ( - splashTime = 1 - devMode = "dev" - clusterRefresh = time.Duration(5 * time.Second) - indicatorFmt = "[orange::b]K9s [aqua::]%s [white::]%s:%s:%s [lawngreen::]%s%%[white::]::[darkturquoise::]%s%%" -) - -type ( - focusHandler func(tview.Primitive) - - forwarder interface { - Start(path, co string, ports []string) (*portforward.PortForwarder, error) - Stop() - Path() string - Container() string - Ports() []string - Active() bool - Age() string - } - - resourceViewer interface { - ui.Igniter - - setEnterFn(enterFn) - setColorerFn(ui.ColorerFunc) - setDecorateFn(decorateFn) - setExtraActionsFn(ui.ActionsFunc) - masterPage() *tableView - } - - appView struct { - *ui.App - - command *command - cancel context.CancelFunc - informer *watch.Informer - stopCh chan struct{} - forwarders map[string]forwarder - version string - showHeader bool - filter string - } -) - -// NewApp returns a K9s app instance. -func NewApp(cfg *config.Config) *appView { - v := appView{ - App: ui.NewApp(), - forwarders: make(map[string]forwarder), - } - v.Config = cfg - v.InitBench(cfg.K9s.CurrentCluster) - v.command = newCommand(&v) - - v.Views()["indicator"] = ui.NewIndicatorView(v.App, v.Styles) - v.Views()["flash"] = ui.NewFlashView(v.Application, "Initializing...") - v.Views()["clusterInfo"] = newClusterInfoView(&v, k8s.NewMetricsServer(cfg.GetConnection())) - - return &v -} - -func (a *appView) Init(version string, rate int) { - a.version = version - a.CmdBuff().AddListener(a) - a.App.Init() - - a.AddActions(ui.KeyActions{ - ui.KeyH: ui.NewKeyAction("ToggleHeader", a.toggleHeaderCmd, false), - ui.KeyHelp: ui.NewKeyAction("Help", a.helpCmd, false), - tcell.KeyCtrlA: ui.NewKeyAction("Aliases", a.aliasCmd, false), - tcell.KeyEnter: ui.NewKeyAction("Goto", a.gotoCmd, false), - }) - - if a.Conn() != nil { - ns, err := a.Conn().Config().CurrentNamespaceName() - if err != nil { - log.Info().Msg("No namespace specified using all namespaces") - } - a.startInformer(ns) - a.clusterInfo().init(version) - if a.Config.K9s.GetHeadless() { - a.refreshIndicator() - } - } - - main := tview.NewFlex() - main.SetDirection(tview.FlexRow) - a.Main().AddPage("main", main, true, false) - a.Main().AddPage("splash", ui.NewSplash(a.Styles, version), true, true) - - main.AddItem(a.indicator(), 1, 1, false) - main.AddItem(a.Frame(), 0, 10, true) - main.AddItem(a.Crumbs(), 2, 1, false) - main.AddItem(a.Flash(), 2, 1, false) - a.toggleHeader(!a.Config.K9s.GetHeadless()) -} - -// Changed indicates the buffer was changed. -func (a *appView) BufferChanged(s string) {} - -// Active indicates the buff activity changed. -func (a *appView) BufferActive(state bool, _ ui.BufferKind) { - flex, ok := a.Main().GetPrimitive("main").(*tview.Flex) - if !ok { - return - } - if state { - flex.AddItemAtIndex(1, a.Cmd(), 3, 1, false) - } else if flex.ItemAt(1) == a.Cmd() { - flex.RemoveItemAtIndex(1) - } - a.Draw() -} - -func (a *appView) toggleHeader(flag bool) { - a.showHeader = flag - flex := a.Main().GetPrimitive("main").(*tview.Flex) - if a.showHeader { - flex.RemoveItemAtIndex(0) - flex.AddItemAtIndex(0, a.buildHeader(), 7, 1, false) - } else { - flex.RemoveItemAtIndex(0) - flex.AddItemAtIndex(0, a.indicator(), 1, 1, false) - a.refreshIndicator() - } -} - -func (a *appView) buildHeader() tview.Primitive { - header := tview.NewFlex() - header.SetBorderPadding(0, 0, 1, 1) - header.SetDirection(tview.FlexColumn) - if !a.showHeader { - return header - } - header.AddItem(a.clusterInfo(), 35, 1, false) - header.AddItem(a.Menu(), 0, 1, false) - header.AddItem(a.Logo(), 26, 1, false) - - return header -} - -func (a *appView) clusterUpdater(ctx context.Context) { - for { - select { - case <-ctx.Done(): - log.Debug().Msg("Cluster updater canceled!") - return - case <-time.After(clusterRefresh): - a.QueueUpdateDraw(func() { - if !a.showHeader { - a.refreshIndicator() - } else { - a.clusterInfo().refresh() - } - }) - } - } -} - -func (a *appView) refreshIndicator() { - mx := k8s.NewMetricsServer(a.Conn()) - cluster := resource.NewCluster(a.Conn(), &log.Logger, mx) - var cmx k8s.ClusterMetrics - nos, nmx, err := fetchResources(a) - cpu, mem := "0", "0" - if err == nil { - cluster.Metrics(nos, nmx, &cmx) - cpu = resource.AsPerc(cmx.PercCPU) - if cpu == "0" { - cpu = resource.NAValue - } - mem = resource.AsPerc(cmx.PercMEM) - if mem == "0" { - mem = resource.NAValue - } - } - - info := fmt.Sprintf( - indicatorFmt, - a.version, - cluster.ClusterName(), - cluster.UserName(), - cluster.Version(), - cpu, - mem, - ) - a.indicator().SetPermanent(info) -} - -func (a *appView) switchNS(ns string) bool { - if ns == resource.AllNamespace { - ns = resource.AllNamespaces - } - if ns == a.Config.ActiveNamespace() { - log.Debug().Msgf("Namespace did not change %s", ns) - return true - } - a.Config.SetActiveNamespace(ns) - - return a.startInformer(ns) -} - -func (a *appView) switchCtx(ctx string, load bool) error { - l := resource.NewContext(a.Conn()) - if err := l.Switch(ctx); err != nil { - return err - } - - a.stopForwarders() - ns, err := a.Conn().Config().CurrentNamespaceName() - if err != nil { - log.Info().Err(err).Msg("No namespace specified using all namespaces") - } - a.startInformer(ns) - a.Config.Reset() - a.Config.Save() - a.Flash().Infof("Switching context to %s", ctx) - if load { - a.gotoResource("po", true) - } - - return nil -} - -func (a *appView) startInformer(ns string) bool { - if a.stopCh != nil { - close(a.stopCh) - a.stopCh = nil - } - - var err error - a.informer, err = watch.NewInformer(a.Conn(), ns) - if err != nil { - log.Error().Err(err).Msgf("%v", err) - a.Flash().Err(err) - return false - } - a.stopCh = make(chan struct{}) - a.informer.Run(a.stopCh) - if a.Config.K9s.GetHeadless() { - a.refreshIndicator() - } - - return true -} - -// BailOut exists the application. -func (a *appView) BailOut() { - if a.stopCh != nil { - log.Debug().Msg("<<<< Stopping Watcher") - close(a.stopCh) - a.stopCh = nil - } - - if a.cancel != nil { - a.cancel() - } - a.stopForwarders() - a.App.BailOut() -} - -func (a *appView) stopForwarders() { - for k, f := range a.forwarders { - log.Debug().Msgf("Deleting forwarder %s", f.Path()) - f.Stop() - delete(a.forwarders, k) - } -} - -// Run starts the application loop -func (a *appView) Run() { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - go a.clusterUpdater(ctx) - - // Only enable skin updater while in dev mode. - if a.HasSkins { - if err := a.StylesUpdater(ctx, a); err != nil { - log.Error().Err(err).Msg("Unable to track skin changes") - } - } - - go func() { - <-time.After(splashTime * time.Second) - a.QueueUpdateDraw(func() { - a.Main().SwitchToPage("main") - }) - }() - - a.command.defaultCmd() - if err := a.Application.Run(); err != nil { - panic(err) - } -} - -func (a *appView) status(l ui.FlashLevel, msg string) { - a.Flash().Info(msg) - if a.Config.K9s.GetHeadless() { - a.setIndicator(l, msg) - } else { - a.setLogo(l, msg) - } - a.Draw() -} - -func (a *appView) setLogo(l ui.FlashLevel, msg string) { - switch l { - case ui.FlashErr: - a.Logo().Err(msg) - case ui.FlashWarn: - a.Logo().Warn(msg) - case ui.FlashInfo: - a.Logo().Info(msg) - default: - a.Logo().Reset() - } - a.Draw() -} - -func (a *appView) setIndicator(l ui.FlashLevel, msg string) { - switch l { - case ui.FlashErr: - a.indicator().Err(msg) - case ui.FlashWarn: - a.indicator().Warn(msg) - case ui.FlashInfo: - a.indicator().Info(msg) - default: - a.indicator().Reset() - } - a.Draw() -} - -func (a *appView) toggleHeaderCmd(evt *tcell.EventKey) *tcell.EventKey { - if a.Cmd().InCmdMode() { - return evt - } - - a.showHeader = !a.showHeader - a.toggleHeader(a.showHeader) - a.Draw() - - return nil -} - -func (a *appView) prevCmd(evt *tcell.EventKey) *tcell.EventKey { - if top, ok := a.command.previousCmd(); ok { - log.Debug().Msgf("Previous command %s", top) - a.gotoResource(top, false) - return nil - } - return evt -} - -func (a *appView) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { - if a.CmdBuff().IsActive() && !a.CmdBuff().Empty() { - a.gotoResource(a.GetCmd(), true) - a.ResetCmd() - return nil - } - a.ActivateCmd(false) - - return evt -} - -func (a *appView) helpCmd(evt *tcell.EventKey) *tcell.EventKey { - if _, ok := a.Frame().GetPrimitive("main").(*helpView); ok { - return evt - } - h := newHelpView(a, a.ActiveView(), a.GetHints()) - a.inject(h) - - return nil -} - -func (a *appView) aliasCmd(evt *tcell.EventKey) *tcell.EventKey { - if _, ok := a.Frame().GetPrimitive("main").(*aliasView); ok { - return evt - } - a.inject(newAliasView(a, a.ActiveView())) - - return nil -} - -func (a *appView) gotoResource(res string, record bool) bool { - if a.cancel != nil { - a.cancel() - } - valid := a.command.run(res) - if valid && record { - a.command.pushCmd(res) - } - - return valid -} - -func (a *appView) inject(i ui.Igniter) { - if a.cancel != nil { - a.cancel() - } - a.Frame().RemovePage("main") - var ctx context.Context - ctx, a.cancel = context.WithCancel(context.Background()) - i.Init(ctx, a.Config.ActiveNamespace()) - a.Frame().AddPage("main", i, true, true) - a.SetFocus(i) -} - -func (a *appView) clusterInfo() *clusterInfoView { - return a.Views()["clusterInfo"].(*clusterInfoView) -} - -func (a *appView) indicator() *ui.IndicatorView { - return a.Views()["indicator"].(*ui.IndicatorView) -} diff --git a/internal/views/app_test.go b/internal/views/app_test.go deleted file mode 100644 index d7a7e02f..00000000 --- a/internal/views/app_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package views - -import ( - "testing" - - "github.com/derailed/k9s/internal/config" - "github.com/stretchr/testify/assert" -) - -func TestNewApp(t *testing.T) { - a := NewApp(config.NewConfig(ks{})) - a.Init("blee", 10) - - assert.Equal(t, 11, len(a.GetActions())) - assert.Equal(t, false, a.HasSkins) -} diff --git a/internal/views/bench.go b/internal/views/bench.go deleted file mode 100644 index 1e6f8d46..00000000 --- a/internal/views/bench.go +++ /dev/null @@ -1,316 +0,0 @@ -package views - -import ( - "context" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "regexp" - "strconv" - "strings" - "time" - - "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/perf" - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/fsnotify/fsnotify" - "github.com/gdamore/tcell" - "github.com/rs/zerolog/log" -) - -const ( - benchTitle = "Benchmarks" - benchTitleFmt = " [seagreen::b]%s([fuchsia::b]%d[fuchsia::-])[seagreen::-] " -) - -var ( - totalRx = regexp.MustCompile(`Total:\s+([0-9.]+)\ssecs`) - reqRx = regexp.MustCompile(`Requests/sec:\s+([0-9.]+)`) - okRx = regexp.MustCompile(`\[2\d{2}\]\s+(\d+)\s+responses`) - errRx = regexp.MustCompile(`\[[4-5]\d{2}\]\s+(\d+)\s+responses`) - toastRx = regexp.MustCompile(`Error distribution`) - benchHeader = resource.Row{"NAMESPACE", "NAME", "STATUS", "TIME", "REQ/S", "2XX", "4XX/5XX", "REPORT", "AGE"} -) - -type benchView struct { - *masterDetail - - app *appView -} - -func newBenchView(title, gvr string, app *appView, _ resource.List) resourceViewer { - v := benchView{app: app} - v.masterDetail = newMasterDetail(benchTitle, "", app, v.backCmd) - v.keyBindings() - - return &v -} - -// Init the view. -func (v *benchView) Init(ctx context.Context, ns string) { - v.masterDetail.init(ctx, ns) - - tv := v.masterPage() - tv.SetBorderFocusColor(tcell.ColorSeaGreen) - tv.SetSelectedStyle(tcell.ColorWhite, tcell.ColorSeaGreen, tcell.AttrNone) - tv.SetColorerFn(benchColorer) - - dv := v.detailsPage() - dv.setCategory("Bench") - dv.SetTextColor(tcell.ColorSeaGreen) - - if err := v.watchBenchDir(ctx); err != nil { - v.app.Flash().Errf("Unable to watch benchmarks directory %s", err) - } - - v.refresh() - tv.SetSortCol(tv.NameColIndex()+7, 0, true) - tv.Refresh() - tv.Select(1, 0) - v.app.SetFocus(tv) - v.app.SetHints(tv.Hints()) -} - -func (v *benchView) setEnterFn(enterFn) {} -func (v *benchView) setColorerFn(ui.ColorerFunc) {} -func (v *benchView) setDecorateFn(decorateFn) {} -func (v *benchView) setExtraActionsFn(ui.ActionsFunc) {} - -func (v *benchView) refresh() { - tv := v.masterPage() - tv.Update(v.hydrate()) - tv.UpdateTitle() -} - -func (v *benchView) keyBindings() { - aa := ui.KeyActions{ - ui.KeyP: ui.NewKeyAction("Previous", v.app.prevCmd, false), - tcell.KeyEnter: ui.NewKeyAction("Enter", v.enterCmd, false), - tcell.KeyCtrlD: ui.NewKeyAction("Delete", v.deleteCmd, false), - } - v.masterPage().SetActions(aa) -} - -func (v *benchView) getTitle() string { - return benchTitle -} - -func (v *benchView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { - return func(evt *tcell.EventKey) *tcell.EventKey { - tv := v.masterPage() - tv.SetSortCol(tv.NameColIndex()+col, 0, asc) - tv.Refresh() - - return nil - } -} - -func (v *benchView) enterCmd(evt *tcell.EventKey) *tcell.EventKey { - if v.masterPage().SearchBuff().IsActive() { - return v.masterPage().filterCmd(evt) - } - - if !v.masterPage().RowSelected() { - return nil - } - - data, err := readBenchFile(v.app.Config, v.benchFile()) - if err != nil { - v.app.Flash().Errf("Unable to load bench file %s", err) - return nil - } - vu := v.detailsPage() - vu.SetText(data) - vu.setTitle(v.masterPage().GetSelectedItem()) - v.showDetails() - - return nil -} - -func (v *benchView) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.masterPage().RowSelected() { - return nil - } - - sel, file := v.masterPage().GetSelectedItem(), v.benchFile() - dir := filepath.Join(perf.K9sBenchDir, v.app.Config.K9s.CurrentCluster) - showModal(v.Pages, fmt.Sprintf("Delete benchmark `%s?", file), "master", func() { - if err := os.Remove(filepath.Join(dir, file)); err != nil { - v.app.Flash().Errf("Unable to delete file %s", err) - return - } - v.app.Flash().Infof("Benchmark %s deleted!", sel) - }) - - return nil -} - -func (v *benchView) backCmd(evt *tcell.EventKey) *tcell.EventKey { - v.showMaster() - return nil -} - -func (v *benchView) benchFile() string { - r := v.masterPage().GetSelectedRow() - return ui.TrimCell(v.masterPage().Table, r, 7) -} - -func (v *benchView) hints() ui.Hints { - return v.CurrentPage().Item.(ui.Hinter).Hints() -} - -func (v *benchView) hydrate() resource.TableData { - ff, err := loadBenchDir(v.app.Config) - if err != nil { - v.app.Flash().Errf("Unable to read bench directory %s", err) - } - - data := initTable() - for _, f := range ff { - bench, err := readBenchFile(v.app.Config, f.Name()) - if err != nil { - log.Error().Err(err).Msgf("Unable to load bench file %s", f.Name()) - continue - } - fields := make(resource.Row, len(benchHeader)) - if err := initRow(fields, f); err != nil { - log.Error().Err(err).Msg("Load bench file") - continue - } - augmentRow(fields, bench) - data.Rows[f.Name()] = &resource.RowEvent{ - Action: resource.New, - Fields: fields, - Deltas: fields, - } - } - - return data -} - -func initRow(row resource.Row, f os.FileInfo) error { - tokens := strings.Split(f.Name(), "_") - if len(tokens) < 2 { - return fmt.Errorf("Invalid file name %s", f.Name()) - } - row[0] = tokens[0] - row[1] = tokens[1] - row[7] = f.Name() - row[8] = time.Since(f.ModTime()).String() - - return nil -} - -func (v *benchView) watchBenchDir(ctx context.Context) error { - w, err := fsnotify.NewWatcher() - if err != nil { - return err - } - - go func() { - for { - select { - case evt := <-w.Events: - log.Debug().Msgf("Bench event %#v", evt) - v.app.QueueUpdateDraw(func() { - v.refresh() - }) - case err := <-w.Errors: - log.Info().Err(err).Msg("Dir Watcher failed") - return - case <-ctx.Done(): - log.Debug().Msg("!!!! FS WATCHER DONE!!") - w.Close() - return - } - } - }() - - return w.Add(benchDir(v.app.Config)) -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func initTable() resource.TableData { - return resource.TableData{ - Header: benchHeader, - Rows: make(resource.RowEvents, 10), - NumCols: map[string]bool{ - benchHeader[3]: true, - benchHeader[4]: true, - benchHeader[5]: true, - benchHeader[6]: true, - }, - Namespace: resource.AllNamespaces, - } -} - -func augmentRow(fields resource.Row, data string) { - if len(data) == 0 { - return - } - - col := 2 - fields[col] = "pass" - mf := toastRx.FindAllStringSubmatch(data, 1) - if len(mf) > 0 { - fields[col] = "fail" - } - col++ - - mt := totalRx.FindAllStringSubmatch(data, 1) - if len(mt) > 0 { - fields[col] = mt[0][1] - } - col++ - - mr := reqRx.FindAllStringSubmatch(data, 1) - if len(mr) > 0 { - fields[col] = mr[0][1] - } - col++ - - ms := okRx.FindAllStringSubmatch(data, -1) - fields[col] = "0" - if len(ms) > 0 { - var sum int - for _, m := range ms { - if m, err := strconv.Atoi(string(m[1])); err == nil { - sum += m - } - } - fields[col] = asNum(sum) - } - col++ - - me := errRx.FindAllStringSubmatch(data, -1) - fields[col] = "0" - if len(me) > 0 { - var sum int - for _, m := range me { - if m, err := strconv.Atoi(string(m[1])); err == nil { - sum += m - } - } - fields[col] = asNum(sum) - } -} - -func benchDir(cfg *config.Config) string { - return filepath.Join(perf.K9sBenchDir, cfg.K9s.CurrentCluster) -} - -func loadBenchDir(cfg *config.Config) ([]os.FileInfo, error) { - return ioutil.ReadDir(benchDir(cfg)) -} - -func readBenchFile(cfg *config.Config, n string) (string, error) { - data, err := ioutil.ReadFile(filepath.Join(benchDir(cfg), n)) - if err != nil { - return "", err - } - return string(data), nil -} diff --git a/internal/views/bench_test.go b/internal/views/bench_test.go deleted file mode 100644 index 17109bc8..00000000 --- a/internal/views/bench_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package views - -import ( - "io/ioutil" - "testing" - - "github.com/derailed/k9s/internal/resource" - "github.com/stretchr/testify/assert" -) - -func TestAugmentRow(t *testing.T) { - uu := map[string]struct { - file string - e resource.Row - }{ - "cool": { - "test_assets/b1.txt", - resource.Row{"pass", "3.3544", "29.8116", "100", "0"}, - }, - "2XX": { - "test_assets/b4.txt", - resource.Row{"pass", "3.3544", "29.8116", "160", "0"}, - }, - "4XX/5XX": { - "test_assets/b2.txt", - resource.Row{"pass", "3.3544", "29.8116", "100", "12"}, - }, - "toast": { - "test_assets/b3.txt", - resource.Row{"fail", "2.3688", "35.4606", "0", "0"}, - }, - } - - for k, u := range uu { - t.Run(k, func(t *testing.T) { - data, err := ioutil.ReadFile(u.file) - - assert.Nil(t, err) - fields := make(resource.Row, 8) - augmentRow(fields, string(data)) - assert.Equal(t, u.e, fields[2:7]) - }) - } -} diff --git a/internal/views/cluster_info.go b/internal/views/cluster_info.go deleted file mode 100644 index 8538ee4e..00000000 --- a/internal/views/cluster_info.go +++ /dev/null @@ -1,171 +0,0 @@ -package views - -import ( - "strings" - - "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/derailed/k9s/internal/watch" - "github.com/derailed/tview" - "github.com/gdamore/tcell" - "github.com/rs/zerolog/log" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -type clusterInfoView struct { - *tview.Table - - app *appView - mxs resource.MetricsServer -} - -// ClusterInfo tracks Kubernetes cluster and K9s information. -type ClusterInfo interface { - ContextName() string - ClusterName() string - UserName() string - K9sVersion() string - K8sVersion() string - CurrentCPU() float64 - CurrentMEM() float64 -} - -func newClusterInfoView(app *appView, mx resource.MetricsServer) *clusterInfoView { - return &clusterInfoView{ - app: app, - Table: tview.NewTable(), - mxs: mx, - } -} - -func (v *clusterInfoView) init(version string) { - cluster := resource.NewCluster(v.app.Conn(), &log.Logger, v.mxs) - - row := v.initInfo(version, cluster) - row = v.initVersion(row, version, cluster) - - v.SetCell(row, 0, v.sectionCell("CPU")) - v.SetCell(row, 1, v.infoCell(resource.NAValue)) - row++ - v.SetCell(row, 0, v.sectionCell("MEM")) - v.SetCell(row, 1, v.infoCell(resource.NAValue)) - - v.refresh() -} - -func (v *clusterInfoView) initInfo(version string, cluster *resource.Cluster) int { - var row int - v.SetCell(row, 0, v.sectionCell("Context")) - v.SetCell(row, 1, v.infoCell(cluster.ContextName())) - row++ - - v.SetCell(row, 0, v.sectionCell("Cluster")) - v.SetCell(row, 1, v.infoCell(cluster.ClusterName())) - row++ - - v.SetCell(row, 0, v.sectionCell("User")) - v.SetCell(row, 1, v.infoCell(cluster.UserName())) - row++ - - return row -} - -func (v *clusterInfoView) initVersion(row int, version string, cluster *resource.Cluster) int { - v.SetCell(row, 0, v.sectionCell("K9s Rev")) - v.SetCell(row, 1, v.infoCell(version)) - row++ - - v.SetCell(row, 0, v.sectionCell("K8s Rev")) - v.SetCell(row, 1, v.infoCell(cluster.Version())) - row++ - - return row -} - -func (v *clusterInfoView) sectionCell(t string) *tview.TableCell { - c := tview.NewTableCell(t + ":") - c.SetAlign(tview.AlignLeft) - var s tcell.Style - c.SetStyle(s.Bold(true).Foreground(config.AsColor(v.app.Styles.K9s.Info.SectionColor))) - c.SetBackgroundColor(v.app.Styles.BgColor()) - - return c -} - -func (v *clusterInfoView) infoCell(t string) *tview.TableCell { - c := tview.NewTableCell(t) - c.SetExpansion(2) - c.SetTextColor(config.AsColor(v.app.Styles.K9s.Info.FgColor)) - c.SetBackgroundColor(v.app.Styles.BgColor()) - - return c -} - -func (v *clusterInfoView) refresh() { - var ( - cluster = resource.NewCluster(v.app.Conn(), &log.Logger, v.mxs) - row int - ) - v.GetCell(row, 1).SetText(cluster.ContextName()) - row++ - v.GetCell(row, 1).SetText(cluster.ClusterName()) - row++ - v.GetCell(row, 1).SetText(cluster.UserName()) - row += 2 - v.GetCell(row, 1).SetText(cluster.Version()) - row++ - - c := v.GetCell(row, 1) - c.SetText(resource.NAValue) - c = v.GetCell(row+1, 1) - c.SetText(resource.NAValue) - - v.refreshMetrics(cluster, row) -} - -func fetchResources(app *appView) (k8s.Collection, k8s.Collection, error) { - nos, err := app.informer.List(watch.NodeIndex, "", metav1.ListOptions{}) - if err != nil { - return nil, nil, err - } - - nmx, err := app.informer.List(watch.NodeMXIndex, "", metav1.ListOptions{}) - if err != nil { - return nil, nil, err - } - - return nos, nmx, nil -} - -func (v *clusterInfoView) refreshMetrics(cluster *resource.Cluster, row int) { - nos, nmx, err := fetchResources(v.app) - if err != nil { - log.Warn().Msgf("NodeMetrics %#v", err) - return - } - - var cmx k8s.ClusterMetrics - cluster.Metrics(nos, nmx, &cmx) - c := v.GetCell(row, 1) - cpu := resource.AsPerc(cmx.PercCPU) - if cpu == "0" { - cpu = resource.NAValue - } - c.SetText(cpu + "%" + ui.Deltas(strip(c.Text), cpu)) - row++ - - c = v.GetCell(row, 1) - mem := resource.AsPerc(cmx.PercMEM) - if mem == "0" { - mem = resource.NAValue - } - c.SetText(mem + "%" + ui.Deltas(strip(c.Text), mem)) -} - -func strip(s string) string { - t := strings.Replace(s, ui.PlusSign, "", 1) - t = strings.Replace(t, ui.MinusSign, "", 1) - return t -} diff --git a/internal/views/colorer.go b/internal/views/colorer.go deleted file mode 100644 index 48b89bb1..00000000 --- a/internal/views/colorer.go +++ /dev/null @@ -1,246 +0,0 @@ -package views - -import ( - "strings" - - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/gdamore/tcell" - "k8s.io/apimachinery/pkg/watch" -) - -func forwardColorer(string, *resource.RowEvent) tcell.Color { - return tcell.ColorSkyblue -} - -func dumpColorer(ns string, r *resource.RowEvent) tcell.Color { - return tcell.ColorNavajoWhite -} - -func benchColorer(ns string, r *resource.RowEvent) tcell.Color { - c := tcell.ColorPaleGreen - - statusCol := 2 - if strings.TrimSpace(r.Fields[statusCol]) != "pass" { - c = ui.ErrColor - } - - return c -} - -func aliasColorer(string, *resource.RowEvent) tcell.Color { - return tcell.ColorMediumSpringGreen -} - -func rbacColorer(ns string, r *resource.RowEvent) tcell.Color { - return ui.DefaultColorer(ns, r) -} - -func podColorer(ns string, r *resource.RowEvent) tcell.Color { - c := ui.DefaultColorer(ns, r) - - readyCol := 2 - if len(ns) != 0 { - readyCol = 1 - } - statusCol := readyCol + 1 - - tokens := strings.Split(strings.TrimSpace(r.Fields[readyCol]), "/") - if len(tokens) == 2 && (tokens[0] == "0" || tokens[0] != tokens[1]) { - if strings.TrimSpace(r.Fields[statusCol]) != "Completed" { - c = ui.ErrColor - } - } - - switch strings.TrimSpace(r.Fields[statusCol]) { - case "ContainerCreating", "PodInitializing": - return ui.AddColor - case "Initialized": - return ui.HighlightColor - case "Completed": - return ui.CompletedColor - case "Running": - default: - c = ui.ErrColor - } - - return c -} - -func containerColorer(ns string, r *resource.RowEvent) tcell.Color { - c := ui.DefaultColorer(ns, r) - - readyCol := 2 - if strings.TrimSpace(r.Fields[readyCol]) == "false" { - c = ui.ErrColor - } - - stateCol := readyCol + 1 - switch strings.TrimSpace(r.Fields[stateCol]) { - case "ContainerCreating", "PodInitializing": - return ui.AddColor - case "Terminating", "Initialized": - return ui.HighlightColor - case "Completed": - return ui.CompletedColor - case "Running": - default: - c = ui.ErrColor - } - - return c -} - -func ctxColorer(ns string, r *resource.RowEvent) tcell.Color { - c := ui.DefaultColorer(ns, r) - if r.Action == watch.Added || r.Action == watch.Modified { - return c - } - - if strings.Contains(strings.TrimSpace(r.Fields[0]), "*") { - c = ui.HighlightColor - } - - return c -} - -func pvColorer(ns string, r *resource.RowEvent) tcell.Color { - c := ui.DefaultColorer(ns, r) - if r.Action == watch.Added || r.Action == watch.Modified { - return c - } - - status := strings.TrimSpace(r.Fields[4]) - switch status { - case "Bound": - c = ui.StdColor - case "Available": - c = tcell.ColorYellow - default: - c = ui.ErrColor - } - - return c -} - -func pvcColorer(ns string, r *resource.RowEvent) tcell.Color { - c := ui.DefaultColorer(ns, r) - if r.Action == watch.Added || r.Action == watch.Modified { - return c - } - - markCol := 2 - if ns != resource.AllNamespaces { - markCol = 1 - } - - if strings.TrimSpace(r.Fields[markCol]) != "Bound" { - c = ui.ErrColor - } - - return c -} - -func pdbColorer(ns string, r *resource.RowEvent) tcell.Color { - c := ui.DefaultColorer(ns, r) - if r.Action == watch.Added || r.Action == watch.Modified { - return c - } - - markCol := 5 - if ns != resource.AllNamespaces { - markCol = 4 - } - if strings.TrimSpace(r.Fields[markCol]) != strings.TrimSpace(r.Fields[markCol+1]) { - return ui.ErrColor - } - - return ui.StdColor -} - -func dpColorer(ns string, r *resource.RowEvent) tcell.Color { - c := ui.DefaultColorer(ns, r) - if r.Action == watch.Added || r.Action == watch.Modified { - return c - } - - markCol := 2 - if ns != resource.AllNamespaces { - markCol = 1 - } - if strings.TrimSpace(r.Fields[markCol]) != strings.TrimSpace(r.Fields[markCol+1]) { - return ui.ErrColor - } - - return ui.StdColor -} - -func stsColorer(ns string, r *resource.RowEvent) tcell.Color { - c := ui.DefaultColorer(ns, r) - if r.Action == watch.Added || r.Action == watch.Modified { - return c - } - - markCol := 2 - if ns != resource.AllNamespaces { - markCol = 1 - } - if strings.TrimSpace(r.Fields[markCol]) != strings.TrimSpace(r.Fields[markCol+1]) { - return ui.ErrColor - } - - return ui.StdColor -} - -func rsColorer(ns string, r *resource.RowEvent) tcell.Color { - c := ui.DefaultColorer(ns, r) - if r.Action == watch.Added || r.Action == watch.Modified { - return c - } - - markCol := 2 - if ns != resource.AllNamespaces { - markCol = 1 - } - if strings.TrimSpace(r.Fields[markCol]) != strings.TrimSpace(r.Fields[markCol+1]) { - return ui.ErrColor - } - - return ui.StdColor -} - -func evColorer(ns string, r *resource.RowEvent) tcell.Color { - c := ui.DefaultColorer(ns, r) - - markCol := 3 - if ns != resource.AllNamespaces { - markCol = 2 - } - - switch strings.TrimSpace(r.Fields[markCol]) { - case "Failed": - c = ui.ErrColor - case "Killing": - c = ui.KillColor - } - - return c -} - -func nsColorer(ns string, r *resource.RowEvent) tcell.Color { - c := ui.DefaultColorer(ns, r) - if r.Action == watch.Added || r.Action == watch.Modified { - return c - } - - switch strings.TrimSpace(r.Fields[1]) { - case "Inactive", "Terminating": - c = ui.ErrColor - } - - if strings.Contains(strings.TrimSpace(r.Fields[0]), "*") { - c = ui.HighlightColor - } - - return c -} diff --git a/internal/views/colorer_test.go b/internal/views/colorer_test.go deleted file mode 100644 index d3df1d41..00000000 --- a/internal/views/colorer_test.go +++ /dev/null @@ -1,283 +0,0 @@ -package views - -import ( - "testing" - - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/gdamore/tcell" - "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/watch" -) - -type ( - colorerUC struct { - ns string - r *resource.RowEvent - e tcell.Color - } - colorerUCs []colorerUC -) - -func TestNSColorer(t *testing.T) { - var ( - ns = resource.Row{"blee", "Active"} - term = resource.Row{"blee", "Terminating"} - dead = resource.Row{"blee", "Inactive"} - ) - - uu := colorerUCs{ - // Add AllNS - {"", &resource.RowEvent{Action: watch.Added, Fields: ns}, ui.AddColor}, - // Mod AllNS - {"", &resource.RowEvent{Action: watch.Modified, Fields: ns}, ui.ModColor}, - // MoChange AllNS - {"", &resource.RowEvent{Action: resource.Unchanged, Fields: ns}, ui.StdColor}, - // Bust NS - {"", &resource.RowEvent{Action: resource.Unchanged, Fields: term}, ui.ErrColor}, - // Bust NS - {"", &resource.RowEvent{Action: resource.Unchanged, Fields: dead}, ui.ErrColor}, - } - for _, u := range uu { - assert.Equal(t, u.e, nsColorer(u.ns, u.r)) - } -} - -func TestEvColorer(t *testing.T) { - var ( - ns = resource.Row{"", "blee", "fred", "Normal"} - nonNS = resource.Row{"", "fred", "Normal"} - failNS = resource.Row{"", "blee", "fred", "Failed"} - failNoNS = resource.Row{"", "fred", "Failed"} - killNS = resource.Row{"", "blee", "fred", "Killing"} - killNoNS = resource.Row{"", "fred", "Killing"} - ) - - uu := colorerUCs{ - // Add AllNS - {"", &resource.RowEvent{Action: watch.Added, Fields: ns}, ui.AddColor}, - // Add NS - {"blee", &resource.RowEvent{Action: watch.Added, Fields: nonNS}, ui.AddColor}, - // Mod AllNS - {"", &resource.RowEvent{Action: watch.Modified, Fields: ns}, ui.ModColor}, - // Mod NS - {"blee", &resource.RowEvent{Action: watch.Modified, Fields: nonNS}, ui.ModColor}, - // Bust AllNS - {"", &resource.RowEvent{Action: resource.Unchanged, Fields: failNS}, ui.ErrColor}, - // Bust NS - {"blee", &resource.RowEvent{Action: resource.Unchanged, Fields: failNoNS}, ui.ErrColor}, - // Bust AllNS - {"", &resource.RowEvent{Action: resource.Unchanged, Fields: killNS}, ui.KillColor}, - // Bust NS - {"blee", &resource.RowEvent{Action: resource.Unchanged, Fields: killNoNS}, ui.KillColor}, - } - for _, u := range uu { - assert.Equal(t, u.e, evColorer(u.ns, u.r)) - } -} - -func TestRSColorer(t *testing.T) { - var ( - ns = resource.Row{"blee", "fred", "1", "1"} - noNs = ns[1:] - bustNS = resource.Row{"blee", "fred", "1", "0"} - bustNoNS = bustNS[1:] - ) - - uu := colorerUCs{ - // Add AllNS - {"", &resource.RowEvent{Action: watch.Added, Fields: ns}, ui.AddColor}, - // Add NS - {"blee", &resource.RowEvent{Action: watch.Added, Fields: noNs}, ui.AddColor}, - // Bust AllNS - {"", &resource.RowEvent{Action: resource.Unchanged, Fields: bustNS}, ui.ErrColor}, - // Bust NS - {"blee", &resource.RowEvent{Action: resource.Unchanged, Fields: bustNoNS}, ui.ErrColor}, - // Nochange AllNS - {"", &resource.RowEvent{Action: resource.Unchanged, Fields: ns}, ui.StdColor}, - // Nochange NS - {"blee", &resource.RowEvent{Action: resource.Unchanged, Fields: noNs}, ui.StdColor}, - } - for _, u := range uu { - assert.Equal(t, u.e, rsColorer(u.ns, u.r)) - } -} - -func TestStsColorer(t *testing.T) { - var ( - ns = resource.Row{"blee", "fred", "1", "1"} - nonNS = ns[1:] - bustNS = resource.Row{"blee", "fred", "2", "1"} - bustNoNS = bustNS[1:] - ) - - uu := colorerUCs{ - // Add AllNS - {"", &resource.RowEvent{Action: watch.Added, Fields: ns}, ui.AddColor}, - // Add NS - {"blee", &resource.RowEvent{Action: watch.Added, Fields: nonNS}, ui.AddColor}, - // Mod AllNS - {"", &resource.RowEvent{Action: watch.Modified, Fields: ns}, ui.ModColor}, - // Mod NS - {"blee", &resource.RowEvent{Action: watch.Modified, Fields: nonNS}, ui.ModColor}, - // Bust AllNS - {"", &resource.RowEvent{Action: resource.Unchanged, Fields: bustNS}, ui.ErrColor}, - // Bust NS - {"blee", &resource.RowEvent{Action: resource.Unchanged, Fields: bustNoNS}, ui.ErrColor}, - // Unchanged cool AllNS - {"", &resource.RowEvent{Action: resource.Unchanged, Fields: ns}, ui.StdColor}, - } - for _, u := range uu { - assert.Equal(t, u.e, stsColorer(u.ns, u.r)) - } -} - -func TestDpColorer(t *testing.T) { - var ( - ns = resource.Row{"blee", "fred", "1", "1"} - nonNS = ns[1:] - bustNS = resource.Row{"blee", "fred", "2", "1"} - bustNoNS = bustNS[1:] - ) - - uu := colorerUCs{ - // Add AllNS - {"", &resource.RowEvent{Action: watch.Added, Fields: ns}, ui.AddColor}, - // Add NS - {"blee", &resource.RowEvent{Action: watch.Added, Fields: nonNS}, ui.AddColor}, - // Mod AllNS - {"", &resource.RowEvent{Action: watch.Modified, Fields: ns}, ui.ModColor}, - // Mod NS - {"blee", &resource.RowEvent{Action: watch.Modified, Fields: nonNS}, ui.ModColor}, - // Unchanged cool - {"", &resource.RowEvent{Action: resource.Unchanged, Fields: ns}, ui.StdColor}, - // Bust AllNS - {"", &resource.RowEvent{Action: resource.Unchanged, Fields: bustNS}, ui.ErrColor}, - // Bust NS - {"blee", &resource.RowEvent{Action: resource.Unchanged, Fields: bustNoNS}, ui.ErrColor}, - } - for _, u := range uu { - assert.Equal(t, u.e, dpColorer(u.ns, u.r)) - } -} - -func TestPdbColorer(t *testing.T) { - var ( - ns = resource.Row{"blee", "fred", "1", "1", "1", "1", "1"} - nonNS = ns[1:] - bustNS = resource.Row{"blee", "fred", "1", "1", "1", "1", "2"} - bustNoNS = bustNS[1:] - ) - - uu := colorerUCs{ - // Add AllNS - {"", &resource.RowEvent{Action: watch.Added, Fields: ns}, ui.AddColor}, - // Add NS - {"blee", &resource.RowEvent{Action: watch.Added, Fields: nonNS}, ui.AddColor}, - // Mod AllNS - {"", &resource.RowEvent{Action: watch.Modified, Fields: ns}, ui.ModColor}, - // Mod NS - {"blee", &resource.RowEvent{Action: watch.Modified, Fields: nonNS}, ui.ModColor}, - // Unchanged cool - {"", &resource.RowEvent{Action: resource.Unchanged, Fields: ns}, ui.StdColor}, - // Bust AllNS - {"", &resource.RowEvent{Action: resource.Unchanged, Fields: bustNS}, ui.ErrColor}, - // Bust NS - {"blee", &resource.RowEvent{Action: resource.Unchanged, Fields: bustNoNS}, ui.ErrColor}, - } - for _, u := range uu { - assert.Equal(t, u.e, pdbColorer(u.ns, u.r)) - } -} - -func TestPVColorer(t *testing.T) { - var ( - pv = resource.Row{"blee", "1G", "RO", "Duh", "Bound"} - bustPv = resource.Row{"blee", "1G", "RO", "Duh", "UnBound"} - ) - - uu := colorerUCs{ - // Add Normal - {"", &resource.RowEvent{Action: watch.Added, Fields: pv}, ui.AddColor}, - // Unchanged Bound - {"", &resource.RowEvent{Action: resource.Unchanged, Fields: pv}, ui.StdColor}, - // Unchanged Bound - {"", &resource.RowEvent{Action: resource.Unchanged, Fields: bustPv}, ui.ErrColor}, - } - for _, u := range uu { - assert.Equal(t, u.e, pvColorer(u.ns, u.r)) - } -} - -func TestPVCColorer(t *testing.T) { - var ( - pvc = resource.Row{"blee", "fred", "Bound"} - bustPvc = resource.Row{"blee", "fred", "UnBound"} - ) - - uu := colorerUCs{ - // Add Normal - {"", &resource.RowEvent{Action: watch.Added, Fields: pvc}, ui.AddColor}, - // Add Bound - {"", &resource.RowEvent{Action: resource.Unchanged, Fields: bustPvc}, ui.ErrColor}, - } - for _, u := range uu { - assert.Equal(t, u.e, pvcColorer(u.ns, u.r)) - } -} - -func TestCtxColorer(t *testing.T) { - var ( - ctx = resource.Row{"blee"} - defCtx = resource.Row{"blee*"} - ) - - uu := colorerUCs{ - // Add Normal - {"", &resource.RowEvent{Action: watch.Added, Fields: ctx}, ui.AddColor}, - // Add Default - {"", &resource.RowEvent{Action: watch.Added, Fields: defCtx}, ui.AddColor}, - // Mod Normal - {"", &resource.RowEvent{Action: watch.Modified, Fields: ctx}, ui.ModColor}, - // Mod Default - {"", &resource.RowEvent{Action: watch.Modified, Fields: defCtx}, ui.ModColor}, - // Unchanged Normal - {"", &resource.RowEvent{Action: resource.Unchanged, Fields: ctx}, ui.StdColor}, - // Unchanged Default - {"", &resource.RowEvent{Action: resource.Unchanged, Fields: defCtx}, ui.HighlightColor}, - } - for _, u := range uu { - assert.Equal(t, u.e, ctxColorer(u.ns, u.r)) - } -} - -func TestPodColorer(t *testing.T) { - var ( - nsRow = resource.Row{"blee", "fred", "1/1", "Running"} - toastNS = resource.Row{"blee", "fred", "1/1", "Boom"} - notReadyNS = resource.Row{"blee", "fred", "0/1", "Boom"} - row, toast, notReady = nsRow[1:], toastNS[1:], notReadyNS[1:] - ) - - uu := colorerUCs{ - // Add allNS - {"", &resource.RowEvent{Action: watch.Added, Fields: nsRow}, ui.AddColor}, - // Add Namespaced - {"blee", &resource.RowEvent{Action: watch.Added, Fields: row}, ui.AddColor}, - // Mod AllNS - {"", &resource.RowEvent{Action: watch.Modified, Fields: nsRow}, ui.ModColor}, - // Mod Namespaced - {"blee", &resource.RowEvent{Action: watch.Modified, Fields: row}, ui.ModColor}, - // Mod Busted AllNS - {"", &resource.RowEvent{Action: watch.Modified, Fields: toastNS}, ui.ErrColor}, - // Mod Busted Namespaced - {"blee", &resource.RowEvent{Action: watch.Modified, Fields: toast}, ui.ErrColor}, - // NotReady AllNS - {"", &resource.RowEvent{Action: watch.Modified, Fields: notReadyNS}, ui.ErrColor}, - // NotReady Namespaced - {"blee", &resource.RowEvent{Action: watch.Modified, Fields: notReady}, ui.ErrColor}, - } - for _, u := range uu { - assert.Equal(t, u.e, podColorer(u.ns, u.r)) - } -} diff --git a/internal/views/command.go b/internal/views/command.go deleted file mode 100644 index 63f5cd06..00000000 --- a/internal/views/command.go +++ /dev/null @@ -1,180 +0,0 @@ -package views - -import ( - "fmt" - "regexp" - "strings" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/rs/zerolog/log" -) - -type subjectViewer interface { - resourceViewer - - setSubject(s string) -} - -type command struct { - app *appView - history *ui.CmdStack -} - -func newCommand(app *appView) *command { - return &command{app: app, history: ui.NewCmdStack()} -} - -func (c *command) lastCmd() bool { - return c.history.Last() -} - -func (c *command) pushCmd(cmd string) { - c.history.Push(cmd) - c.app.Crumbs().Refresh(c.history.Items()) -} - -func (c *command) previousCmd() (string, bool) { - c.history.Pop() - c.app.Crumbs().Refresh(c.history.Items()) - - return c.history.Top() -} - -// DefaultCmd reset default command ie show pods. -func (c *command) defaultCmd() { - cmd := c.app.Config.ActiveView() - c.pushCmd(cmd) - if !c.run(cmd) { - log.Error().Err(fmt.Errorf("Unable to load command %s", cmd)).Msg("Command failed") - } -} - -var authRX = regexp.MustCompile(`\Apol\s([u|g|s]):([\w-:]+)\b`) - -func (c *command) isK9sCmd(cmd string) bool { - cmds := strings.Split(cmd, " ") - switch cmds[0] { - case "q", "quit": - c.app.BailOut() - return true - case "?", "help": - c.app.helpCmd(nil) - return true - case "alias": - c.app.aliasCmd(nil) - return true - default: - if !authRX.MatchString(cmd) { - return false - } - tokens := authRX.FindAllStringSubmatch(cmd, -1) - if len(tokens) == 1 && len(tokens[0]) == 3 { - c.app.inject(newPolicyView(c.app, tokens[0][1], tokens[0][2])) - return true - } - } - return false -} - -// load scrape api for resources and populate aliases. -func (c *command) load() viewers { - vv := make(viewers, 100) - resourceViews(c.app.Conn(), vv) - allCRDs(c.app.Conn(), vv) - - return vv -} - -func (c *command) viewMetaFor(cmd string) (string, *viewer) { - vv := c.load() - gvr, ok := aliases.Get(cmd) - if !ok { - log.Error().Err(fmt.Errorf("Huh? `%s` command not found", cmd)).Msg("Command Failed") - c.app.Flash().Warnf("Huh? `%s` command not found", cmd) - return "", nil - } - v, ok := vv[gvr] - if !ok { - log.Error().Err(fmt.Errorf("Huh? `%s` viewer not found", gvr)).Msg("Viewer Failed") - c.app.Flash().Warnf("Huh? viewer for %s not found", cmd) - return "", nil - } - - return gvr, &v -} - -// Exec the command by showing associated display. -func (c *command) run(cmd string) bool { - if c.isK9sCmd(cmd) { - return true - } - - cmds := strings.Split(cmd, " ") - gvr, v := c.viewMetaFor(cmds[0]) - if v == nil { - return false - } - switch cmds[0] { - case "ctx", "context", "contexts": - if len(cmds) == 2 { - c.app.switchCtx(cmds[1], true) - return true - } - view := c.viewerFor(gvr, v) - return c.exec(gvr, "", view) - default: - ns := c.app.Config.ActiveNamespace() - if len(cmds) == 2 { - ns = cmds[1] - } - if !c.app.switchNS(ns) { - return false - } - return c.exec(gvr, ns, c.viewerFor(gvr, v)) - } - - return false -} - -func (c *command) viewerFor(gvr string, v *viewer) resourceViewer { - var r resource.List - if v.listFn != nil { - r = v.listFn(c.app.Conn(), resource.DefaultNamespace) - } - - var view resourceViewer - if v.viewFn != nil { - view = v.viewFn(v.kind, gvr, c.app, r) - } else { - view = newResourceView(v.kind, gvr, c.app, r) - } - if v.colorerFn != nil { - view.setColorerFn(v.colorerFn) - } - if v.enterFn != nil { - view.setEnterFn(v.enterFn) - } - if v.decorateFn != nil { - view.setDecorateFn(v.decorateFn) - } - - return view -} - -func (c *command) exec(gvr string, ns string, v ui.Igniter) bool { - if v == nil { - log.Error().Err(fmt.Errorf("No igniter given for %s", gvr)) - return false - } - - g := k8s.GVR(gvr) - c.app.Flash().Infof("Viewing %s resource...", g.ToR()) - log.Debug().Msgf("Running command %s", gvr) - c.app.Config.SetActiveView(g.ToR()) - c.app.Config.Save() - c.app.inject(v) - - return true -} diff --git a/internal/views/command_test.go b/internal/views/command_test.go deleted file mode 100644 index 864951c9..00000000 --- a/internal/views/command_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package views - -import ( - "testing" - - "github.com/derailed/k9s/internal/config" - "github.com/stretchr/testify/assert" -) - -func TestCommandPush(t *testing.T) { - c := newCommand(NewApp(config.NewConfig(ks{}))) - c.pushCmd("fred") - c.pushCmd("blee") - p, top := c.previousCmd() - - assert.Equal(t, "fred", p) - assert.True(t, top) - assert.True(t, c.lastCmd()) -} diff --git a/internal/views/container.go b/internal/views/container.go deleted file mode 100644 index d1af6de3..00000000 --- a/internal/views/container.go +++ /dev/null @@ -1,170 +0,0 @@ -package views - -import ( - "context" - "errors" - "fmt" - "strings" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/derailed/k9s/internal/ui/dialog" - "github.com/gdamore/tcell" - "github.com/rs/zerolog/log" - "k8s.io/client-go/tools/portforward" -) - -type containerView struct { - *logResourceView - - current ui.Igniter - exitFn func() -} - -func newContainerView(title string, app *appView, list resource.List, path string, exitFn func()) resourceViewer { - v := containerView{logResourceView: newLogResourceView(title, "", app, list)} - v.path = &path - v.envFn = v.k9sEnv - v.containerFn = v.selectedContainer - v.extraActionsFn = v.extraActions - v.enterFn = v.viewLogs - v.colorerFn = containerColorer - v.current = app.Frame().GetPrimitive("main").(ui.Igniter) - v.exitFn = exitFn - - return &v -} - -func (v *containerView) Init(ctx context.Context, ns string) { - v.resourceView.Init(ctx, ns) -} - -func (v *containerView) extraActions(aa ui.KeyActions) { - v.logResourceView.extraActions(aa) - - aa[ui.KeyShiftF] = ui.NewKeyAction("PortForward", v.portFwdCmd, true) - aa[ui.KeyShiftL] = ui.NewKeyAction("Logs Previous", v.prevLogsCmd, true) - aa[ui.KeyS] = ui.NewKeyAction("Shell", v.shellCmd, true) - aa[tcell.KeyEscape] = ui.NewKeyAction("Back", v.backCmd, false) - aa[ui.KeyP] = ui.NewKeyAction("Previous", v.backCmd, false) - aa[ui.KeyShiftC] = ui.NewKeyAction("Sort CPU", v.sortColCmd(6, false), false) - aa[ui.KeyShiftM] = ui.NewKeyAction("Sort MEM", v.sortColCmd(7, false), false) - aa[ui.KeyShiftX] = ui.NewKeyAction("Sort CPU%", v.sortColCmd(8, false), false) - aa[ui.KeyShiftZ] = ui.NewKeyAction("Sort MEM%", v.sortColCmd(9, false), false) -} - -func (v *containerView) k9sEnv() K9sEnv { - env := v.defaultK9sEnv() - - ns, n := namespaced(*v.path) - env["POD"] = n - env["NAMESPACE"] = ns - - return env -} - -func (v *containerView) selectedContainer() string { - return v.masterPage().GetSelectedItem() -} - -func (v *containerView) viewLogs(app *appView, _, res, sel string) { - status := v.masterPage().GetSelectedCell(3) - if status == "Running" || status == "Completed" { - v.showLogs(false) - return - } - v.app.Flash().Err(errors.New("No logs available")) -} - -// Handlers... - -func (v *containerView) shellCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.masterPage().RowSelected() { - return evt - } - - v.stopUpdates() - shellIn(v.app, *v.path, v.masterPage().GetSelectedItem()) - v.restartUpdates() - return nil -} - -func (v *containerView) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.masterPage().RowSelected() { - return evt - } - - sel := v.masterPage().GetSelectedItem() - if _, ok := v.app.forwarders[fwFQN(*v.path, sel)]; ok { - v.app.Flash().Err(fmt.Errorf("A PortForward already exist on container %s", *v.path)) - return nil - } - - state := v.masterPage().GetSelectedCell(3) - if state != "Running" { - v.app.Flash().Err(fmt.Errorf("Container %s is not running?", sel)) - return nil - } - - portC := v.masterPage().GetSelectedCell(10) - ports := strings.Split(portC, ",") - if len(ports) == 0 { - v.app.Flash().Err(errors.New("Container exposes no ports")) - return nil - } - - var port string - for _, p := range ports { - log.Debug().Msgf("Checking port %q", p) - if !isTCPPort(p) { - continue - } - port = strings.TrimSpace(p) - break - } - if port == "" { - v.app.Flash().Warn("No valid TCP port found on this container. User will specify...") - port = "MY_TCP_PORT!" - } - dialog.ShowPortForward(v.Pages, port, v.portForward) - - return nil -} - -func (v *containerView) portForward(lport, cport string) { - co := v.masterPage().GetSelectedCell(0) - pf := k8s.NewPortForward(v.app.Conn(), &log.Logger) - ports := []string{lport + ":" + cport} - fw, err := pf.Start(*v.path, co, ports) - if err != nil { - v.app.Flash().Err(err) - return - } - - log.Debug().Msgf(">>> Starting port forward %q %v", *v.path, ports) - go v.runForward(pf, fw) -} - -func (v *containerView) runForward(pf *k8s.PortForward, f *portforward.PortForwarder) { - v.app.QueueUpdateDraw(func() { - v.app.forwarders[pf.FQN()] = pf - v.app.Flash().Infof("PortForward activated %s:%s", pf.Path(), pf.Ports()[0]) - dialog.DismissPortForward(v.Pages) - }) - - pf.SetActive(true) - if err := f.ForwardPorts(); err != nil { - v.app.Flash().Err(err) - return - } - v.app.QueueUpdateDraw(func() { - delete(v.app.forwarders, pf.FQN()) - pf.SetActive(false) - }) -} - -func (v *containerView) backCmd(evt *tcell.EventKey) *tcell.EventKey { - v.exitFn() - return nil -} diff --git a/internal/views/context.go b/internal/views/context.go deleted file mode 100644 index 380de31c..00000000 --- a/internal/views/context.go +++ /dev/null @@ -1,68 +0,0 @@ -package views - -import ( - "strings" - - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" -) - -type contextView struct { - *resourceView -} - -func newContextView(title, gvr string, app *appView, list resource.List) resourceViewer { - v := contextView{newResourceView(title, gvr, app, list).(*resourceView)} - v.extraActionsFn = v.extraActions - v.enterFn = v.useCtx - v.masterPage().SetSelectedFn(v.cleanser) - - return &v -} - -func (v *contextView) extraActions(aa ui.KeyActions) { - v.masterPage().RmAction(ui.KeyShiftA) -} - -func (v *contextView) useCtx(app *appView, _, res, sel string) { - if err := v.useContext(sel); err != nil { - app.Flash().Err(err) - return - } - app.gotoResource("po", true) -} - -func (*contextView) cleanser(s string) string { - name := strings.TrimSpace(s) - if strings.HasSuffix(name, "*") { - name = strings.TrimRight(name, "*") - } - if strings.HasSuffix(name, "(𝜟)") { - name = strings.TrimRight(name, "(𝜟)") - } - return name -} - -func (v *contextView) useContext(name string) error { - ctx := v.cleanser(name) - if err := v.list.Resource().(*resource.Context).Switch(ctx); err != nil { - return err - } - - v.app.switchCtx(name, false) - // v.app.stopForwarders() - // ns, err := v.app.Conn().Config().CurrentNamespaceName() - // if err != nil { - // log.Info().Err(err).Msg("No namespace specified using all namespaces") - // } - // v.app.startInformer(ns) - // v.app.Config.Reset() - // v.app.Config.Save() - // v.app.Flash().Infof("Switching context to %s", ctx) - v.refresh() - if tv, ok := v.GetPrimitive("ctx").(*tableView); ok { - tv.Select(1, 0) - } - - return nil -} diff --git a/internal/views/context_test.go b/internal/views/context_test.go deleted file mode 100644 index 52e06813..00000000 --- a/internal/views/context_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package views - -import ( - "testing" - - "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/resource" - "github.com/stretchr/testify/assert" -) - -func TestContextView(t *testing.T) { - l := resource.NewContextList(nil, "fred") - v := newContextView("blee", "", NewApp(config.NewConfig(ks{})), l).(*contextView) - - assert.Equal(t, 10, len(v.hints())) -} - -func TestCleaner(t *testing.T) { - uu := map[string]struct { - s, e string - }{ - "normal": {"fred", "fred"}, - "default": {"fred*", "fred"}, - "delta": {"fred(𝜟)", "fred"}, - } - - v := contextView{} - for k, u := range uu { - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, v.cleanser(u.s)) - }) - } -} diff --git a/internal/views/cronjob.go b/internal/views/cronjob.go deleted file mode 100644 index aab5be93..00000000 --- a/internal/views/cronjob.go +++ /dev/null @@ -1,37 +0,0 @@ -package views - -import ( - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/gdamore/tcell" -) - -type cronJobView struct { - *resourceView -} - -func newCronJobView(title, gvr string, app *appView, list resource.List) resourceViewer { - v := cronJobView{resourceView: newResourceView(title, gvr, app, list).(*resourceView)} - v.extraActionsFn = v.extraActions - - return &v -} - -func (v *cronJobView) trigger(evt *tcell.EventKey) *tcell.EventKey { - if !v.masterPage().RowSelected() { - return evt - } - - sel := v.masterPage().GetSelectedItem() - if err := v.list.Resource().(resource.Runner).Run(sel); err != nil { - v.app.Flash().Errf("Cronjob trigger failed %v", err) - return evt - } - v.app.Flash().Infof("Triggering %s %s", v.list.GetName(), sel) - - return nil -} - -func (v *cronJobView) extraActions(aa ui.KeyActions) { - aa[tcell.KeyCtrlT] = ui.NewKeyAction("Trigger", v.trigger, true) -} diff --git a/internal/views/details.go b/internal/views/details.go deleted file mode 100644 index d6901609..00000000 --- a/internal/views/details.go +++ /dev/null @@ -1,263 +0,0 @@ -package views - -import ( - "fmt" - "regexp" - "strconv" - "strings" - - "github.com/atotto/clipboard" - "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/ui" - "github.com/derailed/tview" - "github.com/gdamore/tcell" - "github.com/rs/zerolog/log" -) - -const detailsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-] " - -type ( - textView struct { - *tview.TextView - - app *appView - actions ui.KeyActions - cmdBuff *ui.CmdBuff - title string - } - - detailsView struct { - *textView - - category string - backFn ui.ActionHandler - numSelections int - } -) - -func newTextView(app *appView) *textView { - return &textView{ - TextView: tview.NewTextView(), - app: app, - actions: make(ui.KeyActions), - } -} - -func newDetailsView(app *appView, backFn ui.ActionHandler) *detailsView { - v := detailsView{textView: newTextView(app)} - v.backFn = backFn - v.SetScrollable(true) - v.SetWrap(true) - v.SetDynamicColors(true) - v.SetRegions(true) - v.SetBorder(true) - v.SetBorderFocusColor(config.AsColor(v.app.Styles.Frame().Border.FocusColor)) - v.SetHighlightColor(tcell.ColorOrange) - v.SetTitleColor(tcell.ColorAqua) - v.SetInputCapture(v.keyboard) - - v.cmdBuff = ui.NewCmdBuff('/', ui.FilterBuff) - v.cmdBuff.AddListener(app.Cmd()) - v.cmdBuff.Reset() - - v.SetChangedFunc(func() { - app.Draw() - }) - - v.bindKeys() - - return &v -} - -func (v *detailsView) bindKeys() { - v.actions = ui.KeyActions{ - tcell.KeyBackspace2: ui.NewKeyAction("Erase", v.eraseCmd, false), - tcell.KeyBackspace: ui.NewKeyAction("Erase", v.eraseCmd, false), - tcell.KeyDelete: ui.NewKeyAction("Erase", v.eraseCmd, false), - tcell.KeyEscape: ui.NewKeyAction("Back", v.backCmd, true), - tcell.KeyTab: ui.NewKeyAction("Next Match", v.nextCmd, false), - tcell.KeyBacktab: ui.NewKeyAction("Previous Match", v.prevCmd, false), - tcell.KeyCtrlS: ui.NewKeyAction("Save", v.saveCmd, true), - ui.KeyC: ui.NewKeyAction("Copy", v.cpCmd, false), - } -} - -func (v *detailsView) setCategory(n string) { - v.category = n -} - -func (v *detailsView) keyboard(evt *tcell.EventKey) *tcell.EventKey { - key := evt.Key() - if key == tcell.KeyRune { - if v.cmdBuff.IsActive() { - v.cmdBuff.Add(evt.Rune()) - v.refreshTitle() - return nil - } - key = tcell.Key(evt.Rune()) - } - - if a, ok := v.actions[key]; ok { - log.Debug().Msgf(">> DetailsView handled %s", tcell.KeyNames[key]) - return a.Action(evt) - } - return evt -} - -func (v *detailsView) saveCmd(evt *tcell.EventKey) *tcell.EventKey { - if path, err := saveYAML(v.app.Config.K9s.CurrentCluster, v.title, v.GetText(true)); err != nil { - v.app.Flash().Err(err) - } else { - v.app.Flash().Infof("Log %s saved successfully!", path) - } - return nil -} - -func (v *detailsView) cpCmd(evt *tcell.EventKey) *tcell.EventKey { - v.app.Flash().Info("Content copied to clipboard...") - if err := clipboard.WriteAll(v.GetText(true)); err != nil { - v.app.Flash().Err(err) - } - return nil -} - -func (v *detailsView) backCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.cmdBuff.Empty() { - v.cmdBuff.Reset() - v.search(evt) - return nil - } - v.cmdBuff.Reset() - if v.backFn != nil { - return v.backFn(evt) - } - return evt -} - -func (v *detailsView) eraseCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.cmdBuff.IsActive() { - return evt - } - v.cmdBuff.Delete() - return nil -} - -func (v *detailsView) activateCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.app.InCmdMode() { - v.cmdBuff.SetActive(true) - v.cmdBuff.Clear() - return nil - } - return evt -} - -func (v *detailsView) searchCmd(evt *tcell.EventKey) *tcell.EventKey { - if v.cmdBuff.IsActive() && !v.cmdBuff.Empty() { - v.app.Flash().Infof("Searching for %s...", v.cmdBuff) - v.search(evt) - highlights := v.GetHighlights() - if len(highlights) > 0 { - v.Highlight() - } else { - v.Highlight("0").ScrollToHighlight() - } - } - v.cmdBuff.SetActive(false) - return evt -} - -func (v *detailsView) search(evt *tcell.EventKey) { - v.numSelections = 0 - log.Debug().Msgf("Searching... %s - %d", v.cmdBuff, v.numSelections) - v.Highlight("") - v.SetText(v.decorateLines(v.GetText(false), v.cmdBuff.String())) - - if v.cmdBuff.Empty() { - v.app.Flash().Info("Clearing out search query...") - v.refreshTitle() - return - } - if v.numSelections == 0 { - v.app.Flash().Warn("No matches found!") - return - } - v.app.Flash().Infof("Found <%d> matches! / for next/previous", v.numSelections) -} - -func (v *detailsView) nextCmd(evt *tcell.EventKey) *tcell.EventKey { - highlights := v.GetHighlights() - if len(highlights) == 0 || v.numSelections == 0 { - return evt - } - index, _ := strconv.Atoi(highlights[0]) - index = (index + 1) % v.numSelections - if index+1 == v.numSelections { - v.app.Flash().Info("Search hit BOTTOM, continuing at TOP") - } - v.Highlight(strconv.Itoa(index)).ScrollToHighlight() - return nil -} - -func (v *detailsView) prevCmd(evt *tcell.EventKey) *tcell.EventKey { - highlights := v.GetHighlights() - if len(highlights) == 0 || v.numSelections == 0 { - return evt - } - index, _ := strconv.Atoi(highlights[0]) - index = (index - 1 + v.numSelections) % v.numSelections - if index == 0 { - v.app.Flash().Info("Search hit TOP, continuing at BOTTOM") - } - v.Highlight(strconv.Itoa(index)).ScrollToHighlight() - return nil -} - -// SetActions to handle keyboard inputs -func (v *detailsView) setActions(aa ui.KeyActions) { - for k, a := range aa { - v.actions[k] = a - } -} - -// Hints fetch mmemonic and hints -func (v *detailsView) hints() ui.Hints { - if v.actions != nil { - return v.actions.Hints() - } - return nil -} - -func (v *detailsView) refreshTitle() { - v.setTitle(v.title) -} - -func (v *detailsView) setTitle(t string) { - v.title = t - - title := skinTitle(fmt.Sprintf(detailsTitleFmt, v.category, t), v.app.Styles.Frame()) - if !v.cmdBuff.Empty() { - title += skinTitle(fmt.Sprintf(searchFmt, v.cmdBuff.String()), v.app.Styles.Frame()) - } - v.SetTitle(title) -} - -var ( - regionRX = regexp.MustCompile(`\["([a-zA-Z0-9_,;: \-\.]*)"\]`) - escapeRX = regexp.MustCompile(`\[([a-zA-Z0-9_,;: \-\."#]+)\[(\[*)\]`) -) - -func (v *detailsView) decorateLines(buff, q string) string { - rx := regexp.MustCompile(`(?i)` + q) - lines := strings.Split(buff, "\n") - for i, l := range lines { - l = regionRX.ReplaceAllString(l, "") - l = escapeRX.ReplaceAllString(l, "") - if m := rx.FindString(l); len(m) > 0 { - lines[i] = rx.ReplaceAllString(l, fmt.Sprintf(`["%d"]%s[""]`, v.numSelections, m)) - v.numSelections++ - continue - } - lines[i] = l - } - return strings.Join(lines, "\n") -} diff --git a/internal/views/details_test.go b/internal/views/details_test.go deleted file mode 100644 index a7024f5e..00000000 --- a/internal/views/details_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package views - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestDetailsDecorateLines(t *testing.T) { - buff := ` - I love blee - blee is much [blue::]cooler [green::]than foo! - ` - exp := ` - I love ["0"]blee[""] - ["1"]blee[""] is much [blue::]cooler [green::]than foo! - ` - v := detailsView{} - assert.Equal(t, exp, v.decorateLines(buff, "blee")) -} diff --git a/internal/views/dp.go b/internal/views/dp.go deleted file mode 100644 index 51448c8d..00000000 --- a/internal/views/dp.go +++ /dev/null @@ -1,57 +0,0 @@ -package views - -import ( - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - v1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -type deployView struct { - *logResourceView - scalableResourceView *scalableResourceView - restartableResourceView *restartableResourceView -} - -const scaleDialogKey = "scale" - -func newDeployView(title, gvr string, app *appView, list resource.List) resourceViewer { - logResourceView := newLogResourceView(title, gvr, app, list) - v := deployView{ - logResourceView: logResourceView, - scalableResourceView: newScalableResourceViewForParent(logResourceView.resourceView), - restartableResourceView: newRestartableResourceViewForParent(logResourceView.resourceView), - } - v.extraActionsFn = v.extraActions - v.enterFn = v.showPods - - return &v -} - -func (v *deployView) extraActions(aa ui.KeyActions) { - v.logResourceView.extraActions(aa) - v.scalableResourceView.extraActions(aa) - v.restartableResourceView.extraActions(aa) - aa[ui.KeyShiftD] = ui.NewKeyAction("Sort Desired", v.sortColCmd(1, false), false) - aa[ui.KeyShiftC] = ui.NewKeyAction("Sort Current", v.sortColCmd(2, false), false) -} - -func (v *deployView) showPods(app *appView, _, res, sel string) { - ns, n := namespaced(sel) - d := k8s.NewDeployment(app.Conn()) - dep, err := d.Get(ns, n) - if err != nil { - app.Flash().Err(err) - return - } - - dp := dep.(*v1.Deployment) - l, err := metav1.LabelSelectorAsSelector(dp.Spec.Selector) - if err != nil { - app.Flash().Err(err) - return - } - - showPods(app, ns, l.String(), "", v.backCmd) -} diff --git a/internal/views/dp_test.go b/internal/views/dp_test.go deleted file mode 100644 index 0aa3f58e..00000000 --- a/internal/views/dp_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package views - -import ( - "testing" - - "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/resource" - "github.com/stretchr/testify/assert" -) - -func TestDeployView(t *testing.T) { - l := resource.NewDeploymentList(nil, "fred") - v := newDeployView("blee", "", NewApp(config.NewConfig(ks{})), l).(*deployView) - - assert.Equal(t, 10, len(v.hints())) -} diff --git a/internal/views/ds.go b/internal/views/ds.go deleted file mode 100644 index 76263250..00000000 --- a/internal/views/ds.go +++ /dev/null @@ -1,52 +0,0 @@ -package views - -import ( - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - appsv1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -type daemonSetView struct { - *logResourceView - restartableResourceView *restartableResourceView -} - -func newDaemonSetView(title, gvr string, app *appView, list resource.List) resourceViewer { - view := newLogResourceView(title, gvr, app, list) - v := daemonSetView{ - logResourceView: view, - restartableResourceView: newRestartableResourceViewForParent(view.resourceView), - } - v.extraActionsFn = v.extraActions - v.enterFn = v.showPods - - return &v -} - -func (v *daemonSetView) extraActions(aa ui.KeyActions) { - v.logResourceView.extraActions(aa) - v.restartableResourceView.extraActions(aa) - aa[ui.KeyShiftD] = ui.NewKeyAction("Sort Desired", v.sortColCmd(1, false), false) - aa[ui.KeyShiftC] = ui.NewKeyAction("Sort Current", v.sortColCmd(2, false), false) -} - -func (v *daemonSetView) showPods(app *appView, _, res, sel string) { - ns, n := namespaced(sel) - d := k8s.NewDaemonSet(app.Conn()) - dset, err := d.Get(ns, n) - if err != nil { - v.app.Flash().Err(err) - return - } - - ds := dset.(*appsv1.DaemonSet) - l, err := metav1.LabelSelectorAsSelector(ds.Spec.Selector) - if err != nil { - app.Flash().Err(err) - return - } - - showPods(app, ns, l.String(), "", v.backCmd) -} diff --git a/internal/views/ds_test.go b/internal/views/ds_test.go deleted file mode 100644 index 3503a903..00000000 --- a/internal/views/ds_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package views - -import ( - "testing" - - "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/resource" - "github.com/stretchr/testify/assert" -) - -func TestDaemonSetView(t *testing.T) { - l := resource.NewDaemonSetList(nil, "fred") - v := newDaemonSetView("blee", "", NewApp(config.NewConfig(ks{})), l).(*daemonSetView) - - assert.Equal(t, 10, len(v.hints())) -} diff --git a/internal/views/dump.go b/internal/views/dump.go deleted file mode 100644 index 9d663fa6..00000000 --- a/internal/views/dump.go +++ /dev/null @@ -1,235 +0,0 @@ -package views - -import ( - "context" - "errors" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "time" - - "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/derailed/tview" - "github.com/fsnotify/fsnotify" - "github.com/gdamore/tcell" - "github.com/rs/zerolog/log" -) - -const ( - dumpTitle = "Screen Dumps" - dumpTitleFmt = " [mediumvioletred::b]%s([fuchsia::b]%d[fuchsia::-])[mediumvioletred::-] " -) - -var ( - dumpHeader = resource.Row{"NAME", "AGE"} -) - -type dumpView struct { - *tview.Pages - - app *appView - cancel context.CancelFunc -} - -func newDumpView(_, _ string, app *appView, _ resource.List) resourceViewer { - v := dumpView{ - Pages: tview.NewPages(), - app: app, - } - - tv := newTableView(app, dumpTitle) - tv.SetBorderFocusColor(tcell.ColorSteelBlue) - tv.SetSelectedStyle(tcell.ColorWhite, tcell.ColorRoyalBlue, tcell.AttrNone) - tv.SetColorerFn(dumpColorer) - tv.SetActiveNS("") - v.AddPage("table", tv, true, true) - - details := newDetailsView(app, v.backCmd) - v.AddPage("details", details, true, false) - v.registerActions() - - return &v -} - -func (v *dumpView) masterPage() *tableView { - return v.GetPrimitive("table").(*tableView) -} - -func (v *dumpView) setEnterFn(enterFn) {} -func (v *dumpView) setColorerFn(ui.ColorerFunc) {} -func (v *dumpView) setDecorateFn(decorateFn) {} -func (v *dumpView) setExtraActionsFn(ui.ActionsFunc) {} - -// Init the view. -func (v *dumpView) Init(ctx context.Context, _ string) { - if err := v.watchDumpDir(ctx); err != nil { - v.app.Flash().Errf("Unable to watch dumpmarks directory %s", err) - } - - tv := v.getTV() - v.refresh() - tv.SetSortCol(tv.NameColIndex()+1, 0, true) - tv.Refresh() - tv.SelectRow(1, true) - v.app.SetFocus(tv) -} - -func (v *dumpView) refresh() { - tv := v.getTV() - tv.Update(v.hydrate()) - tv.UpdateTitle() -} - -func (v *dumpView) registerActions() { - aa := ui.KeyActions{ - ui.KeyP: ui.NewKeyAction("Previous", v.app.prevCmd, false), - tcell.KeyEnter: ui.NewKeyAction("Enter", v.enterCmd, true), - tcell.KeyCtrlD: ui.NewKeyAction("Delete", v.deleteCmd, true), - tcell.KeyCtrlS: ui.NewKeyAction("Save", noopCmd, false), - } - - tv := v.getTV() - tv.SetActions(aa) - v.app.SetHints(tv.Hints()) -} - -func (v *dumpView) getTitle() string { - return dumpTitle -} - -func (v *dumpView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { - return func(evt *tcell.EventKey) *tcell.EventKey { - tv := v.getTV() - tv.SetSortCol(tv.NameColIndex()+col, 0, asc) - tv.Refresh() - return nil - } -} - -func (v *dumpView) enterCmd(evt *tcell.EventKey) *tcell.EventKey { - log.Debug().Msg("Dump enter!") - tv := v.getTV() - if tv.SearchBuff().IsActive() { - return tv.filterCmd(evt) - } - sel := tv.GetSelectedItem() - if sel == "" { - return nil - } - - dir := filepath.Join(config.K9sDumpDir, v.app.Config.K9s.CurrentCluster) - if !edit(true, v.app, filepath.Join(dir, sel)) { - v.app.Flash().Err(errors.New("Failed to launch editor")) - } - - return nil -} - -func (v *dumpView) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { - sel := v.getTV().GetSelectedItem() - if sel == "" { - return nil - } - - dir := filepath.Join(config.K9sDumpDir, v.app.Config.K9s.CurrentCluster) - showModal(v.Pages, fmt.Sprintf("Delete screen dump `%s?", sel), "table", func() { - if err := os.Remove(filepath.Join(dir, sel)); err != nil { - v.app.Flash().Errf("Unable to delete file %s", err) - return - } - v.refresh() - v.app.Flash().Infof("ScreenDump file %s deleted!", sel) - }) - - return nil -} - -func (v *dumpView) backCmd(evt *tcell.EventKey) *tcell.EventKey { - if v.cancel != nil { - v.cancel() - } - v.SwitchToPage("table") - return nil -} - -func (v *dumpView) hints() ui.Hints { - return v.CurrentPage().Item.(ui.Hinter).Hints() -} - -func (v *dumpView) hydrate() resource.TableData { - data := resource.TableData{ - Header: dumpHeader, - Rows: make(resource.RowEvents, 10), - Namespace: resource.NotNamespaced, - } - - dir := filepath.Join(config.K9sDumpDir, v.app.Config.K9s.CurrentCluster) - ff, err := ioutil.ReadDir(dir) - if err != nil { - v.app.Flash().Errf("Unable to read dump directory %s", err) - } - - for _, f := range ff { - fields := resource.Row{f.Name(), time.Since(f.ModTime()).String()} - data.Rows[f.Name()] = &resource.RowEvent{ - Action: resource.New, - Fields: fields, - Deltas: fields, - } - } - - return data -} - -func (v *dumpView) resetTitle() { - v.SetTitle(fmt.Sprintf(dumpTitleFmt, dumpTitle, v.getTV().GetRowCount()-1)) -} - -func (v *dumpView) watchDumpDir(ctx context.Context) error { - w, err := fsnotify.NewWatcher() - if err != nil { - return err - } - - go func() { - for { - select { - case evt := <-w.Events: - log.Debug().Msgf("Dump event %#v", evt) - v.app.QueueUpdateDraw(func() { - v.refresh() - }) - case err := <-w.Errors: - log.Info().Err(err).Msg("Dir Watcher failed") - return - case <-ctx.Done(): - log.Debug().Msg("!!!! FS WATCHER DONE!!") - w.Close() - return - } - } - }() - - return w.Add(filepath.Join(config.K9sDumpDir, v.app.Config.K9s.CurrentCluster)) -} - -func (v *dumpView) getTV() *tableView { - if vu, ok := v.GetPrimitive("table").(*tableView); ok { - return vu - } - return nil -} - -func (v *dumpView) getDetails() *detailsView { - if vu, ok := v.GetPrimitive("details").(*detailsView); ok { - return vu - } - return nil -} - -func noopCmd(*tcell.EventKey) *tcell.EventKey { - return nil -} diff --git a/internal/views/forward.go b/internal/views/forward.go deleted file mode 100644 index e4a1d779..00000000 --- a/internal/views/forward.go +++ /dev/null @@ -1,381 +0,0 @@ -package views - -import ( - "context" - "errors" - "fmt" - "strings" - "time" - - "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/perf" - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/derailed/tview" - "github.com/fsnotify/fsnotify" - "github.com/gdamore/tcell" - "github.com/rs/zerolog/log" -) - -const ( - forwardTitle = "Port Forwards" - forwardTitleFmt = " [aqua::b]%s([fuchsia::b]%d[fuchsia::-])[aqua::-] " - promptPage = "prompt" -) - -type forwardView struct { - *tview.Pages - - app *appView - cancel context.CancelFunc - bench *perf.Benchmark -} - -var _ resourceViewer = &forwardView{} - -func newForwardView(title, gvr string, app *appView, list resource.List) resourceViewer { - v := forwardView{ - Pages: tview.NewPages(), - app: app, - } - - tv := newTableView(app, forwardTitle) - tv.SetBorderFocusColor(tcell.ColorDodgerBlue) - tv.SetSelectedStyle(tcell.ColorWhite, tcell.ColorDodgerBlue, tcell.AttrNone) - tv.SetColorerFn(forwardColorer) - tv.SetActiveNS("") - v.AddPage("table", tv, true, true) - v.registerActions() - - return &v -} - -func (v *forwardView) masterPage() *tableView { - return v.GetPrimitive("table").(*tableView) -} - -func (v *forwardView) setEnterFn(enterFn) {} -func (v *forwardView) setColorerFn(ui.ColorerFunc) {} -func (v *forwardView) setDecorateFn(decorateFn) {} -func (v *forwardView) setExtraActionsFn(ui.ActionsFunc) {} - -// Init the view. -func (v *forwardView) Init(ctx context.Context, _ string) { - path := ui.BenchConfig(v.app.Config.K9s.CurrentCluster) - if err := watchFS(ctx, v.app, config.K9sHome, path, v.reload); err != nil { - v.app.Flash().Errf("RuRoh! Unable to watch benchmarks directory %s : %s", config.K9sHome, err) - } - - tv := v.getTV() - v.refresh() - tv.SetSortCol(tv.NameColIndex()+6, 0, true) - tv.Refresh() - tv.Select(1, 0) - v.app.SetFocus(tv) - v.app.SetHints(v.hints()) -} - -func (v *forwardView) getTV() *tableView { - if vu, ok := v.GetPrimitive("table").(*tableView); ok { - return vu - } - return nil -} - -func (v *forwardView) reload() { - path := ui.BenchConfig(v.app.Config.K9s.CurrentCluster) - log.Debug().Msgf("Reloading Config %s", path) - if err := v.app.Bench.Reload(path); err != nil { - v.app.Flash().Err(err) - } - v.refresh() -} - -func (v *forwardView) refresh() { - tv := v.getTV() - tv.Update(v.hydrate()) - v.app.SetFocus(tv) - tv.UpdateTitle() -} - -func (v *forwardView) registerActions() { - tv := v.getTV() - tv.SetActions(ui.KeyActions{ - tcell.KeyEnter: ui.NewKeyAction("Goto", v.gotoBenchCmd, true), - tcell.KeyCtrlB: ui.NewKeyAction("Bench", v.benchCmd, true), - tcell.KeyCtrlK: ui.NewKeyAction("Bench Stop", v.benchStopCmd, true), - tcell.KeyCtrlD: ui.NewKeyAction("Delete", v.deleteCmd, true), - ui.KeySlash: ui.NewKeyAction("Filter", tv.activateCmd, false), - ui.KeyP: ui.NewKeyAction("Previous", v.app.prevCmd, false), - ui.KeyShiftP: ui.NewKeyAction("Sort Ports", v.sortColCmd(2, true), false), - ui.KeyShiftU: ui.NewKeyAction("Sort URL", v.sortColCmd(4, true), false), - }) -} - -func (v *forwardView) getTitle() string { - return forwardTitle -} - -func (v *forwardView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { - return func(evt *tcell.EventKey) *tcell.EventKey { - tv := v.getTV() - tv.SetSortCol(tv.NameColIndex()+col, 0, asc) - v.refresh() - - return nil - } -} - -func (v *forwardView) gotoBenchCmd(evt *tcell.EventKey) *tcell.EventKey { - v.app.gotoResource("be", true) - - return nil -} - -func (v *forwardView) benchStopCmd(evt *tcell.EventKey) *tcell.EventKey { - if v.bench != nil { - log.Debug().Msg(">>> Benchmark canceled!!") - v.app.status(ui.FlashErr, "Benchmark Camceled!") - v.bench.Cancel() - } - v.app.StatusReset() - - return nil -} - -func (v *forwardView) benchCmd(evt *tcell.EventKey) *tcell.EventKey { - sel := v.getSelectedItem() - if sel == "" { - return nil - } - - if v.bench != nil { - v.app.Flash().Err(errors.New("Only one benchmark allowed at a time")) - return nil - } - - tv := v.getTV() - r, _ := tv.GetSelection() - cfg, co := defaultConfig(), ui.TrimCell(tv.Table, r, 2) - if b, ok := v.app.Bench.Benchmarks.Containers[containerID(sel, co)]; ok { - cfg = b - } - cfg.Name = sel - - base := ui.TrimCell(tv.Table, r, 4) - var err error - if v.bench, err = perf.NewBenchmark(base, cfg); err != nil { - v.app.Flash().Errf("Bench failed %v", err) - v.app.StatusReset() - return nil - } - - v.app.status(ui.FlashWarn, "Benchmark in progress...") - log.Debug().Msg("Bench starting...") - go v.runBenchmark() - - return nil -} - -func (v *forwardView) runBenchmark() { - v.bench.Run(v.app.Config.K9s.CurrentCluster, func() { - log.Debug().Msg("Bench Completed!") - v.app.QueueUpdate(func() { - if v.bench.Canceled() { - v.app.status(ui.FlashInfo, "Benchmark canceled") - } else { - v.app.status(ui.FlashInfo, "Benchmark Completed!") - v.bench.Cancel() - } - v.bench = nil - go func() { - <-time.After(2 * time.Second) - v.app.QueueUpdate(func() { v.app.StatusReset() }) - }() - }) - }) -} - -func (v *forwardView) getSelectedItem() string { - tv := v.getTV() - r, _ := tv.GetSelection() - if r == 0 { - return "" - } - return fwFQN( - fqn(ui.TrimCell(tv.Table, r, 0), ui.TrimCell(tv.Table, r, 1)), - ui.TrimCell(tv.Table, r, 2), - ) -} - -func (v *forwardView) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { - tv := v.getTV() - if !tv.SearchBuff().Empty() { - tv.SearchBuff().Reset() - return nil - } - - sel := v.getSelectedItem() - if sel == "" { - return nil - } - - showModal(v.Pages, fmt.Sprintf("Delete PortForward `%s?", sel), "table", func() { - fw, ok := v.app.forwarders[sel] - if !ok { - log.Debug().Msgf("Unable to find forwarder %s", sel) - return - } - fw.Stop() - delete(v.app.forwarders, sel) - - log.Debug().Msgf("PortForwards after delete: %#v", v.app.forwarders) - v.getTV().Update(v.hydrate()) - v.app.Flash().Infof("PortForward %s deleted!", sel) - }) - - return nil -} - -func (v *forwardView) backCmd(evt *tcell.EventKey) *tcell.EventKey { - if v.cancel != nil { - v.cancel() - } - - tv := v.getTV() - if tv.SearchBuff().IsActive() { - tv.SearchBuff().Reset() - } else { - v.app.inject(v.app.Frame().GetPrimitive("main").(ui.Igniter)) - } - - return nil -} - -func (v *forwardView) hints() ui.Hints { - return v.getTV().Hints() -} - -func (v *forwardView) hydrate() resource.TableData { - data := initHeader(len(v.app.forwarders)) - dc, dn := v.app.Bench.Benchmarks.Defaults.C, v.app.Bench.Benchmarks.Defaults.N - for _, f := range v.app.forwarders { - c, n, cfg := loadConfig(dc, dn, containerID(f.Path(), f.Container()), v.app.Bench.Benchmarks.Containers) - - ports := strings.Split(f.Ports()[0], ":") - ns, na := namespaced(f.Path()) - fields := resource.Row{ - ns, - na, - f.Container(), - strings.Join(f.Ports(), ","), - urlFor(cfg, f.Container(), ports[0]), - asNum(c), - asNum(n), - f.Age(), - } - data.Rows[f.Path()] = &resource.RowEvent{ - Action: resource.New, - Fields: fields, - Deltas: fields, - } - } - - return data -} - -func (v *forwardView) resetTitle() { - v.SetTitle(fmt.Sprintf(forwardTitleFmt, forwardTitle, v.getTV().GetRowCount()-1)) -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func defaultConfig() config.BenchConfig { - return config.BenchConfig{ - C: config.DefaultC, - N: config.DefaultN, - HTTP: config.HTTP{ - Method: config.DefaultMethod, - Path: "/", - }, - } -} - -func initHeader(rows int) resource.TableData { - return resource.TableData{ - Header: resource.Row{"NAMESPACE", "NAME", "CONTAINER", "PORTS", "URL", "C", "N", "AGE"}, - NumCols: map[string]bool{"C": true, "N": true}, - Rows: make(resource.RowEvents, rows), - Namespace: resource.AllNamespaces, - } -} - -func loadConfig(dc, dn int, id string, cc map[string]config.BenchConfig) (int, int, config.BenchConfig) { - c, n := dc, dn - cfg, ok := cc[id] - if !ok { - return c, n, cfg - } - - if cfg.C != 0 { - c = cfg.C - } - if cfg.N != 0 { - n = cfg.N - } - - return c, n, cfg -} - -func showModal(pv *tview.Pages, msg, back string, ok func()) { - m := tview.NewModal(). - AddButtons([]string{"Cancel", "OK"}). - SetTextColor(tcell.ColorFuchsia). - SetText(msg). - SetDoneFunc(func(_ int, b string) { - if b == "OK" { - ok() - } - dismissModal(pv, back) - }) - m.SetTitle("") - pv.AddPage(promptPage, m, false, false) - pv.ShowPage(promptPage) -} - -func dismissModal(pv *tview.Pages, page string) { - pv.RemovePage(promptPage) - pv.SwitchToPage(page) -} - -func watchFS(ctx context.Context, app *appView, dir, file string, cb func()) error { - w, err := fsnotify.NewWatcher() - if err != nil { - return err - } - - go func() { - for { - select { - case evt := <-w.Events: - log.Debug().Msgf("FS %s event %v", file, evt.Name) - if file == "" || evt.Name == file { - log.Debug().Msgf("Capuring Event %#v", evt) - app.QueueUpdateDraw(func() { - cb() - }) - } - case err := <-w.Errors: - log.Info().Err(err).Msgf("FS %s watcher failed", dir) - return - case <-ctx.Done(): - log.Debug().Msgf("<>", dir) - w.Close() - return - } - } - }() - - return w.Add(dir) -} diff --git a/internal/views/help.go b/internal/views/help.go deleted file mode 100644 index 673c1da4..00000000 --- a/internal/views/help.go +++ /dev/null @@ -1,245 +0,0 @@ -package views - -import ( - "context" - "fmt" - "runtime" - "sort" - "strconv" - "strings" - - "github.com/derailed/k9s/internal/ui" - "github.com/derailed/tview" - "github.com/gdamore/tcell" - "github.com/rs/zerolog/log" -) - -const ( - helpTitle = "Help" - helpTitleFmt = " [aqua::b]%s " -) - -type ( - helpItem struct { - key, description string - } - - helpView struct { - *tview.Table - - app *appView - current ui.Igniter - actions ui.KeyActions - } -) - -func newHelpView(app *appView, current ui.Igniter, hh ui.Hints) *helpView { - v := helpView{ - Table: tview.NewTable(), - app: app, - actions: make(ui.KeyActions), - } - v.SetBorder(true) - v.SetBorderPadding(0, 0, 1, 1) - v.SetInputCapture(v.keyboard) - v.current = current - v.bindKeys() - v.build(hh) - - return &v -} - -func (v *helpView) bindKeys() { - v.actions = ui.KeyActions{ - ui.KeyHelp: ui.NewKeyAction("Back", v.backCmd, true), - tcell.KeyEsc: ui.NewKeyAction("Back", v.backCmd, false), - tcell.KeyEnter: ui.NewKeyAction("Back", v.backCmd, false), - } -} - -func (v *helpView) keyboard(evt *tcell.EventKey) *tcell.EventKey { - key := evt.Key() - if key == tcell.KeyRune { - key = tcell.Key(evt.Rune()) - } - - if a, ok := v.actions[key]; ok { - log.Debug().Msgf(">> TableView handled %s", tcell.KeyNames[key]) - return a.Action(evt) - } - return evt -} - -func (v *helpView) backCmd(evt *tcell.EventKey) *tcell.EventKey { - v.app.inject(v.current) - return nil -} - -func (v *helpView) Init(_ context.Context, _ string) { - v.resetTitle() - v.app.SetHints(v.Hints()) -} - -func (v *helpView) showHelp() ui.Hints { - return ui.Hints{ - { - Mnemonic: "?", - Description: "Help", - }, - { - Mnemonic: "Ctrl-a", - Description: "Aliases", - }, - } -} - -func (v *helpView) showNav() ui.Hints { - return ui.Hints{ - { - Mnemonic: "g", - Description: "Goto Top", - }, - { - Mnemonic: "Shift-g", - Description: "Goto Bottom", - }, - { - Mnemonic: "Ctrl-b", - Description: "Page Down"}, - { - Mnemonic: "Ctrl-f", - Description: "Page Up", - }, - { - Mnemonic: "h", - Description: "Left", - }, - { - Mnemonic: "l", - Description: "Right", - }, - { - Mnemonic: "k", - Description: "Up", - }, - { - Mnemonic: "j", - Description: "Down", - }, - } -} - -func (v *helpView) showGeneral() ui.Hints { - return ui.Hints{ - { - Mnemonic: ":cmd", - Description: "Command mode", - }, - { - Mnemonic: "/term", - Description: "Filter mode", - }, - { - Mnemonic: "esc", - Description: "Clear filter", - }, - { - Mnemonic: "tab", - Description: "Next Field", - }, - { - Mnemonic: "backtab", - Description: "Previous Field", - }, - { - Mnemonic: "Ctrl-r", - Description: "Refresh", - }, - { - Mnemonic: "h", - Description: "Toggle Header", - }, - { - Mnemonic: "Shift-i", - Description: "Invert Sort", - }, - { - Mnemonic: "p", - Description: "Previous View", - }, - { - Mnemonic: ":q", - Description: "Quit", - }, - } -} - -func (v *helpView) Hints() ui.Hints { - return v.actions.Hints() -} - -func (v *helpView) getTitle() string { - return helpTitle -} - -func (v *helpView) resetTitle() { - v.SetTitle(fmt.Sprintf(helpTitleFmt, helpTitle)) -} - -func (v *helpView) build(hh ui.Hints) { - v.Clear() - sort.Sort(hh) - v.addSection(0, 0, "RESOURCE", hh) - v.addSection(0, 4, "GENERAL", v.showGeneral()) - v.addSection(0, 6, "NAVIGATION", v.showNav()) - v.addSection(0, 8, "HELP", v.showHelp()) -} - -func (v *helpView) addSection(r, c int, title string, hh ui.Hints) { - row := r - cell := tview.NewTableCell(title) - cell.SetTextColor(tcell.ColorGreen) - cell.SetAttributes(tcell.AttrBold) - cell.SetExpansion(2) - cell.SetAlign(tview.AlignLeft) - v.SetCell(r, c+1, cell) - row++ - - for _, h := range hh { - col := c - cell := tview.NewTableCell(toMnemonic(h.Mnemonic)) - if _, err := strconv.Atoi(h.Mnemonic); err != nil { - cell.SetTextColor(tcell.ColorDodgerBlue) - } else { - cell.SetTextColor(tcell.ColorFuchsia) - } - cell.SetAttributes(tcell.AttrBold) - cell.SetAlign(tview.AlignRight) - v.SetCell(row, col, cell) - col++ - cell = tview.NewTableCell(h.Description) - cell.SetTextColor(tcell.ColorWhite) - v.SetCell(row, col, cell) - row++ - } -} - -func toMnemonic(s string) string { - if len(s) == 0 { - return s - } - - return "<" + keyConv(strings.ToLower(s)) + ">" -} - -func keyConv(s string) string { - if !strings.Contains(s, "alt") { - return s - } - - if runtime.GOOS != "darwin" { - return s - } - - return strings.Replace(s, "alt", "opt", 1) -} diff --git a/internal/views/help_test.go b/internal/views/help_test.go deleted file mode 100644 index f43c4832..00000000 --- a/internal/views/help_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package views - -import ( - "testing" - - "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/ui" - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -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() ([]string, error) { - return []string{"test"}, nil -} - -func (k ks) NamespaceNames(nn []v1.Namespace) []string { - return []string{"test"} -} - -func newNS(n string) v1.Namespace { - return v1.Namespace{ObjectMeta: metav1.ObjectMeta{ - Name: n, - }} -} - -func TestNewHelpView(t *testing.T) { - cfg := config.NewConfig(ks{}) - a := NewApp(cfg) - - v := newHelpView(a, nil, ui.Hints{{Mnemonic: "blee", Description: "duh"}}) - v.Init(nil, "") - - assert.Equal(t, "", v.GetCell(1, 0).Text) - assert.Equal(t, "duh", v.GetCell(1, 1).Text) -} diff --git a/internal/views/helpers.go b/internal/views/helpers.go deleted file mode 100644 index 570ffdbc..00000000 --- a/internal/views/helpers.go +++ /dev/null @@ -1,85 +0,0 @@ -package views - -import ( - "fmt" - "path" - "strings" - - "github.com/derailed/k9s/internal/config" - "github.com/gdamore/tcell" - "golang.org/x/text/language" - "golang.org/x/text/message" -) - -// In check if a string belongs to a set. -func in(ss []string, s string) bool { - for _, v := range ss { - if v == s { - return true - } - } - - return false -} - -// AsKey maps a string representation of a key to a tcell key. -func asKey(key string) (tcell.Key, error) { - for k, v := range tcell.KeyNames { - if v == key { - return k, nil - } - } - - return 0, fmt.Errorf("No matching key found %s", key) -} - -// FwFQN returns a fully qualified ns/name:container id. -func fwFQN(po, co string) string { - return po + ":" + co -} - -func isTCPPort(p string) bool { - return !strings.Contains(p, "UDP") -} - -// Namespaced converts an fqn resource name to ns and name. -func namespaced(n string) (string, string) { - ns, po := path.Split(n) - return strings.Trim(ns, "/"), po -} - -// ContainerID computes container ID based on ns/po/co. -func containerID(path, co string) string { - ns, n := namespaced(path) - po := strings.Split(n, "-")[0] - - return ns + "/" + po + ":" + co -} - -// UrlFor computes fq url for a given benchmark configuration. -func urlFor(cfg config.BenchConfig, co, port string) string { - host := "localhost" - if cfg.HTTP.Host != "" { - host = cfg.HTTP.Host - } - - path := "/" - if cfg.HTTP.Path != "" { - path = cfg.HTTP.Path - } - - return "http://" + host + ":" + port + path -} - -func fqn(ns, n string) string { - if ns == "" { - return n - } - return ns + "/" + n -} - -// AsNumb prints a number with thousand separator. -func asNum(n int) string { - p := message.NewPrinter(language.English) - return p.Sprintf("%d", n) -} diff --git a/internal/views/job.go b/internal/views/job.go deleted file mode 100644 index e1c715c0..00000000 --- a/internal/views/job.go +++ /dev/null @@ -1,44 +0,0 @@ -package views - -import ( - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - batchv1 "k8s.io/api/batch/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -type jobView struct { - *logResourceView -} - -func newJobView(title, gvr string, app *appView, list resource.List) resourceViewer { - v := jobView{newLogResourceView(title, gvr, app, list)} - v.extraActionsFn = v.extraActions - v.enterFn = v.showPods - - return &v -} - -func (v *jobView) extraActions(aa ui.KeyActions) { - v.logResourceView.extraActions(aa) -} - -func (v *jobView) showPods(app *appView, ns, res, sel string) { - ns, n := namespaced(sel) - j := k8s.NewJob(app.Conn()) - job, err := j.Get(ns, n) - if err != nil { - app.Flash().Err(err) - return - } - - jo := job.(*batchv1.Job) - l, err := metav1.LabelSelectorAsSelector(jo.Spec.Selector) - if err != nil { - app.Flash().Err(err) - return - } - - showPods(app, ns, l.String(), "", v.backCmd) -} diff --git a/internal/views/log.go b/internal/views/log.go deleted file mode 100644 index 77df56ca..00000000 --- a/internal/views/log.go +++ /dev/null @@ -1,248 +0,0 @@ -package views - -import ( - "fmt" - "io" - "os" - "path/filepath" - "strings" - "sync/atomic" - "time" - - "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/ui" - "github.com/derailed/tview" - "github.com/gdamore/tcell" - "github.com/rs/zerolog/log" -) - -type ( - logFrame struct { - *tview.Flex - - app *appView - actions ui.KeyActions - backFn ui.ActionHandler - } - - logView struct { - *logFrame - - logs *detailsView - status *statusView - ansiWriter io.Writer - autoScroll int32 - path string - } -) - -func newLogFrame(app *appView, backFn ui.ActionHandler) *logFrame { - f := logFrame{ - Flex: tview.NewFlex(), - app: app, - backFn: backFn, - actions: make(ui.KeyActions), - } - f.SetBorder(true) - f.SetBackgroundColor(config.AsColor(app.Styles.Views().Log.BgColor)) - f.SetBorderPadding(0, 0, 1, 1) - f.SetDirection(tview.FlexRow) - - return &f -} - -func newLogView(_ string, app *appView, backFn ui.ActionHandler) *logView { - v := logView{ - logFrame: newLogFrame(app, backFn), - autoScroll: 1, - } - - v.logs = newDetailsView(app, backFn) - { - v.logs.SetBorder(false) - v.logs.setCategory("Logs") - v.logs.SetDynamicColors(true) - v.logs.SetTextColor(config.AsColor(app.Styles.Views().Log.FgColor)) - v.logs.SetBackgroundColor(config.AsColor(app.Styles.Views().Log.BgColor)) - v.logs.SetWrap(true) - v.logs.SetMaxBuffer(app.Config.K9s.LogBufferSize) - } - v.ansiWriter = tview.ANSIWriter(v.logs, app.Styles.Views().Log.FgColor, app.Styles.Views().Log.BgColor) - v.status = newStatusView(app.Styles) - v.AddItem(v.status, 1, 1, false) - v.AddItem(v.logs, 0, 1, true) - - v.bindKeys() - v.logs.SetInputCapture(v.keyboard) - - return &v -} - -func (v *logView) bindKeys() { - v.actions = ui.KeyActions{ - tcell.KeyEscape: ui.NewKeyAction("Back", v.backCmd, true), - ui.KeyC: ui.NewKeyAction("Clear", v.clearCmd, true), - ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", v.toggleScrollCmd, true), - ui.KeyG: ui.NewKeyAction("Top", v.topCmd, false), - ui.KeyShiftG: ui.NewKeyAction("Bottom", v.bottomCmd, false), - ui.KeyF: ui.NewKeyAction("Up", v.pageUpCmd, false), - ui.KeyB: ui.NewKeyAction("Down", v.pageDownCmd, false), - tcell.KeyCtrlS: ui.NewKeyAction("Save", v.saveCmd, true), - } -} - -func (v *logView) setTitle(path, co string) { - var fmat string - if co == "" { - fmat = skinTitle(fmt.Sprintf(logFmt, path), v.app.Styles.Frame()) - } else { - fmat = skinTitle(fmt.Sprintf(logCoFmt, path, co), v.app.Styles.Frame()) - } - v.path = path - v.SetTitle(fmat) -} - -// Hints show action hints -func (v *logView) Hints() ui.Hints { - return v.actions.Hints() -} - -func (v *logView) keyboard(evt *tcell.EventKey) *tcell.EventKey { - key := evt.Key() - if key == tcell.KeyRune { - key = tcell.Key(evt.Rune()) - } - if m, ok := v.actions[key]; ok { - log.Debug().Msgf(">> LogView handled %s", tcell.KeyNames[key]) - return m.Action(evt) - } - - return evt -} - -func (v *logView) log(lines string) { - fmt.Fprintln(v.ansiWriter, tview.Escape(lines)) - log.Debug().Msgf("LOG LINES %d", v.logs.GetLineCount()) -} - -func (v *logView) flush(index int, buff []string) { - if index == 0 { - return - } - - if atomic.LoadInt32(&v.autoScroll) == 1 { - v.log(strings.Join(buff[:index], "\n")) - v.app.QueueUpdateDraw(func() { - v.updateIndicator() - v.logs.ScrollToEnd() - }) - } -} - -func (v *logView) updateIndicator() { - status := "Off" - if v.autoScroll == 1 { - status = "On" - } - v.status.update([]string{fmt.Sprintf("Autoscroll: %s", status)}) -} - -// ---------------------------------------------------------------------------- -// Actions... - -func (v *logView) saveCmd(evt *tcell.EventKey) *tcell.EventKey { - if path, err := saveData(v.app.Config.K9s.CurrentCluster, v.path, v.logs.GetText(true)); err != nil { - v.app.Flash().Err(err) - } else { - v.app.Flash().Infof("Log %s saved successfully!", path) - } - return nil -} - -func ensureDir(dir string) error { - return os.MkdirAll(dir, 0744) -} - -func saveData(cluster, name, data string) (string, error) { - dir := filepath.Join(config.K9sDumpDir, cluster) - if err := ensureDir(dir); err != nil { - return "", err - } - - now := time.Now().UnixNano() - fName := fmt.Sprintf("%s-%d.log", strings.Replace(name, "/", "-", -1), now) - - path := filepath.Join(dir, fName) - mod := os.O_CREATE | os.O_WRONLY - file, err := os.OpenFile(path, mod, 0644) - defer func() { - if file != nil { - file.Close() - } - }() - if err != nil { - log.Error().Err(err).Msgf("LogFile create %s", path) - return "", nil - } - if _, err := fmt.Fprintf(file, data); err != nil { - return "", err - } - - return path, nil -} - -func (v *logView) toggleScrollCmd(evt *tcell.EventKey) *tcell.EventKey { - if atomic.LoadInt32(&v.autoScroll) == 0 { - atomic.StoreInt32(&v.autoScroll, 1) - } else { - atomic.StoreInt32(&v.autoScroll, 0) - } - - if atomic.LoadInt32(&v.autoScroll) == 1 { - v.app.Flash().Info("Autoscroll is on.") - v.logs.ScrollToEnd() - } else { - v.logs.LineUp() - v.app.Flash().Info("Autoscroll is off.") - } - v.updateIndicator() - - return nil -} - -func (v *logView) backCmd(evt *tcell.EventKey) *tcell.EventKey { - return v.backFn(evt) -} - -func (v *logView) topCmd(evt *tcell.EventKey) *tcell.EventKey { - v.app.Flash().Info("Top of logs...") - v.logs.ScrollToBeginning() - return nil -} - -func (v *logView) bottomCmd(*tcell.EventKey) *tcell.EventKey { - v.app.Flash().Info("Bottom of logs...") - v.logs.ScrollToEnd() - return nil -} - -func (v *logView) pageUpCmd(*tcell.EventKey) *tcell.EventKey { - if v.logs.PageUp() { - v.app.Flash().Info("Reached Top ...") - } - return nil -} - -func (v *logView) pageDownCmd(*tcell.EventKey) *tcell.EventKey { - if v.logs.PageDown() { - v.app.Flash().Info("Reached Bottom ...") - } - return nil -} - -func (v *logView) clearCmd(*tcell.EventKey) *tcell.EventKey { - v.app.Flash().Info("Clearing logs...") - v.logs.Clear() - v.logs.ScrollTo(0, 0) - return nil -} diff --git a/internal/views/log_resource.go b/internal/views/log_resource.go deleted file mode 100644 index 825b03a6..00000000 --- a/internal/views/log_resource.go +++ /dev/null @@ -1,86 +0,0 @@ -package views - -import ( - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/gdamore/tcell" -) - -type ( - containerFn func() string - - logResourceView struct { - *resourceView - - containerFn containerFn - } -) - -func newLogResourceView(title, gvr string, app *appView, list resource.List) *logResourceView { - v := logResourceView{ - resourceView: newResourceView(title, gvr, app, list).(*resourceView), - } - v.AddPage("logs", newLogsView(list.GetName(), app, &v), true, false) - - return &v -} - -func (v *logResourceView) extraActions(aa ui.KeyActions) { - aa[ui.KeyL] = ui.NewKeyAction("Logs", v.logsCmd, true) - aa[ui.KeyShiftL] = ui.NewKeyAction("Logs Previous", v.prevLogsCmd, true) -} - -func (v *logResourceView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { - return func(evt *tcell.EventKey) *tcell.EventKey { - t := v.masterPage() - t.SetSortCol(t.NameColIndex()+col, 0, asc) - t.Refresh() - - return nil - } -} - -// Protocol... - -func (v *logResourceView) getList() resource.List { - return v.list -} - -func (v *logResourceView) getSelection() string { - if v.path != nil { - return *v.path - } - return v.masterPage().GetSelectedItem() -} - -func (v *logResourceView) prevLogsCmd(evt *tcell.EventKey) *tcell.EventKey { - v.showLogs(true) - return nil -} - -func (v *logResourceView) logsCmd(evt *tcell.EventKey) *tcell.EventKey { - v.showLogs(false) - return nil -} - -func (v *logResourceView) showLogs(prev bool) { - if !v.masterPage().RowSelected() { - return - } - - l := v.GetPrimitive("logs").(*logsView) - co := "" - if v.containerFn != nil { - co = v.containerFn() - } - l.reload(co, v, prev) - v.switchPage("logs") -} - -func (v *logResourceView) backCmd(evt *tcell.EventKey) *tcell.EventKey { - // Reset namespace to what it was - v.app.Config.SetActiveNamespace(v.list.GetNamespace()) - v.app.inject(v) - - return nil -} diff --git a/internal/views/log_test.go b/internal/views/log_test.go deleted file mode 100644 index af975567..00000000 --- a/internal/views/log_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package views - -import ( - "bytes" - "fmt" - "io/ioutil" - "path/filepath" - "testing" - - "github.com/derailed/k9s/internal/config" - "github.com/derailed/tview" - "github.com/stretchr/testify/assert" -) - -func TestAnsi(t *testing.T) { - buff := bytes.NewBufferString("") - w := tview.ANSIWriter(buff, "white", "black") - fmt.Fprintf(w, "[YELLOW] ok") - assert.Equal(t, "[YELLOW] ok", buff.String()) - - v := tview.NewTextView() - v.SetDynamicColors(true) - aw := tview.ANSIWriter(v, "white", "black") - s := "[2019-03-27T15:05:15,246][INFO ][o.e.c.r.a.AllocationService] [es-0] Cluster health status changed from [YELLOW] to [GREEN] (reason: [shards started [[.monitoring-es-6-2019.03.27][0]]" - fmt.Fprintf(aw, s) - assert.Equal(t, s+"\n", v.GetText(false)) -} - -func TestLogViewFlush(t *testing.T) { - v := newLogView("Logs", NewApp(config.NewConfig(ks{})), nil) - v.flush(2, []string{"blee", "bozo"}) - - v.toggleScrollCmd(nil) - assert.Equal(t, "blee\nbozo\n", v.logs.GetText(true)) - assert.Equal(t, " Autoscroll: Off ", v.status.GetText(true)) - v.toggleScrollCmd(nil) - assert.Equal(t, " Autoscroll: On ", v.status.GetText(true)) -} - -func TestLogViewSave(t *testing.T) { - v := newLogView("Logs", NewApp(config.NewConfig(ks{})), nil) - v.flush(2, []string{"blee", "bozo"}) - v.path = "k9s-test" - dir := filepath.Join(config.K9sDumpDir, v.app.Config.K9s.CurrentCluster) - c1, _ := ioutil.ReadDir(dir) - v.saveCmd(nil) - c2, _ := ioutil.ReadDir(dir) - assert.Equal(t, len(c2), len(c1)+1) -} - -func TestLogViewNav(t *testing.T) { - v := newLogView("Logs", NewApp(config.NewConfig(ks{})), nil) - var buff []string - v.autoScroll = 1 - for i := 0; i < 100; i++ { - buff = append(buff, fmt.Sprintf("line-%d\n", i)) - } - v.flush(100, buff) - - v.topCmd(nil) - r, _ := v.logs.GetScrollOffset() - assert.Equal(t, 0, r) - v.pageDownCmd(nil) - r, _ = v.logs.GetScrollOffset() - assert.Equal(t, 0, r) - v.pageUpCmd(nil) - r, _ = v.logs.GetScrollOffset() - assert.Equal(t, 0, r) - v.bottomCmd(nil) - r, _ = v.logs.GetScrollOffset() - assert.Equal(t, 0, r) -} - -func TestLogViewClear(t *testing.T) { - v := newLogView("Logs", NewApp(config.NewConfig(ks{})), nil) - v.flush(2, []string{"blee", "bozo"}) - - v.toggleScrollCmd(nil) - assert.Equal(t, "blee\nbozo\n", v.logs.GetText(true)) - v.clearCmd(nil) - assert.Equal(t, "", v.logs.GetText(true)) -} diff --git a/internal/views/logs.go b/internal/views/logs.go deleted file mode 100644 index 9fbe1936..00000000 --- a/internal/views/logs.go +++ /dev/null @@ -1,177 +0,0 @@ -package views - -import ( - "context" - "fmt" - "time" - - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/derailed/tview" - "github.com/gdamore/tcell" - "github.com/rs/zerolog/log" -) - -const ( - logBuffSize = 100 - flushTimeout = 200 * time.Millisecond - - logCoFmt = " Logs([fg:bg:]%s:[hilite:bg:b]%s[-:bg:-]) " - logFmt = " Logs([fg:bg:]%s) " -) - -type ( - masterView interface { - backFn() ui.ActionHandler - appView() *appView - } - - logsView struct { - *tview.Pages - - app *appView - parent loggable - actions ui.KeyActions - cancelFunc context.CancelFunc - } -) - -func newLogsView(title string, app *appView, parent loggable) *logsView { - v := logsView{ - app: app, - Pages: tview.NewPages(), - parent: parent, - } - - return &v -} - -// Protocol... - -func (v *logsView) reload(co string, parent loggable, prevLogs bool) { - v.parent = parent - v.deletePage() - v.AddPage("logs", newLogView(co, v.app, v.backCmd), true, true) - v.load(co, prevLogs) -} - -// SetActions to handle keyboard events. -func (v *logsView) setActions(aa ui.KeyActions) { - v.actions = aa -} - -// Hints show action hints -func (v *logsView) Hints() ui.Hints { - l := v.CurrentPage().Item.(*logView) - return l.actions.Hints() -} - -func (v *logsView) backFn() ui.ActionHandler { - return v.backCmd -} - -func (v *logsView) deletePage() { - v.RemovePage("logs") -} - -func (v *logsView) stop() { - if v.cancelFunc == nil { - return - } - v.cancelFunc() - log.Debug().Msgf("Canceling logs...") - v.cancelFunc = nil -} - -func (v *logsView) load(container string, prevLogs bool) { - if err := v.doLoad(v.parent.getSelection(), container, prevLogs); err != nil { - v.app.Flash().Err(err) - l := v.CurrentPage().Item.(*logView) - l.log("😂 Doh! No logs are available at this time. Check again later on...") - return - } - v.app.SetFocus(v) -} - -func (v *logsView) doLoad(path, co string, prevLogs bool) error { - v.stop() - - l := v.CurrentPage().Item.(*logView) - l.logs.Clear() - l.setTitle(path, co) - - var ctx context.Context - ctx = context.WithValue(context.Background(), resource.IKey("informer"), v.app.informer) - ctx, v.cancelFunc = context.WithCancel(ctx) - - c := make(chan string, 10) - go updateLogs(ctx, c, l, logBuffSize) - - res, ok := v.parent.getList().Resource().(resource.Tailable) - if !ok { - close(c) - return fmt.Errorf("Resource %T is not tailable", v.parent.getList().Resource()) - } - - if err := res.Logs(ctx, c, v.logOpts(path, co, prevLogs)); err != nil { - v.cancelFunc() - close(c) - return err - } - - return nil -} - -func (v *logsView) logOpts(path, co string, prevLogs bool) resource.LogOptions { - ns, po := namespaced(path) - return resource.LogOptions{ - Fqn: resource.Fqn{ - Namespace: ns, - Name: po, - Container: co, - }, - Lines: int64(v.app.Config.K9s.LogRequestSize), - Previous: prevLogs, - } -} - -func updateLogs(ctx context.Context, c <-chan string, l *logView, buffSize int) { - defer func() { - log.Debug().Msgf("updateLogs view bailing out!") - }() - buff, index := make([]string, buffSize), 0 - for { - select { - case line, ok := <-c: - if !ok { - log.Debug().Msgf("Closed channel detected. Bailing out...") - l.flush(index, buff) - return - } - if index < buffSize { - buff[index] = line - index++ - continue - } - l.flush(index, buff) - index = 0 - buff[index] = line - index++ - case <-time.After(flushTimeout): - l.flush(index, buff) - index = 0 - case <-ctx.Done(): - return - } - } -} - -// ---------------------------------------------------------------------------- -// Actions... - -func (v *logsView) backCmd(evt *tcell.EventKey) *tcell.EventKey { - v.stop() - v.parent.switchPage("master") - - return evt -} diff --git a/internal/views/logs_test.go b/internal/views/logs_test.go deleted file mode 100644 index 0a7397c5..00000000 --- a/internal/views/logs_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package views - -import ( - "context" - "fmt" - "sync" - "testing" - - "github.com/derailed/k9s/internal/config" - "github.com/stretchr/testify/assert" -) - -func TestUpdateLogs(t *testing.T) { - v := newLogView("test", NewApp(config.NewConfig(ks{})), nil) - - var wg sync.WaitGroup - wg.Add(1) - c := make(chan string, 10) - go func() { - defer wg.Done() - updateLogs(context.Background(), c, v, 10) - }() - - for i := 0; i < 500; i++ { - c <- fmt.Sprintf("log %d", i) - } - close(c) - wg.Wait() - - assert.Equal(t, 500, v.logs.GetLineCount()) -} diff --git a/internal/views/master_detail.go b/internal/views/master_detail.go deleted file mode 100644 index bd521a03..00000000 --- a/internal/views/master_detail.go +++ /dev/null @@ -1,94 +0,0 @@ -package views - -import ( - "context" - - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/derailed/tview" -) - -type ( - pageView struct { - *tview.Pages - - app *appView - } - - masterDetail struct { - *pageView - - currentNS string - title string - enterFn enterFn - extraActionsFn func(ui.KeyActions) - } -) - -func newPageView(app *appView) *pageView { - return &pageView{ - Pages: tview.NewPages(), - app: app, - } -} - -func newMasterDetail(title, ns string, app *appView, backCmd ui.ActionHandler) *masterDetail { - v := masterDetail{ - pageView: newPageView(app), - currentNS: ns, - title: title, - } - v.AddPage("master", newTableView(v.app, v.title), true, true) - v.AddPage("details", newDetailsView(v.app, backCmd), true, false) - - return &v -} - -func (v *masterDetail) init(ctx context.Context, ns string) { - if v.currentNS != resource.NotNamespaced { - v.currentNS = ns - } -} - -func (v *masterDetail) setExtraActionsFn(f ui.ActionsFunc) { - v.extraActionsFn = f -} - -// Protocol... - -// Hints fetch menu hints -func (v *masterDetail) hints() ui.Hints { - return v.CurrentPage().Item.(ui.Hinter).Hints() -} - -func (v *masterDetail) setEnterFn(f enterFn) { - v.enterFn = f -} - -func (v *masterDetail) showMaster() { - v.SwitchToPage("master") -} - -func (v *masterDetail) masterPage() *tableView { - return v.GetPrimitive("master").(*tableView) -} - -func (v *masterDetail) showDetails() { - v.SwitchToPage("details") -} - -func (v *masterDetail) detailsPage() *detailsView { - return v.GetPrimitive("details").(*detailsView) -} - -// ---------------------------------------------------------------------------- -// Actions... - -func (v *masterDetail) defaultActions(aa ui.KeyActions) { - aa[ui.KeyHelp] = ui.NewKeyAction("Help", noopCmd, false) - aa[ui.KeyP] = ui.NewKeyAction("Previous", v.app.prevCmd, false) - - if v.extraActionsFn != nil { - v.extraActionsFn(aa) - } -} diff --git a/internal/views/namespace_test.go b/internal/views/namespace_test.go deleted file mode 100644 index 1051bec4..00000000 --- a/internal/views/namespace_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package views - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestNSCleanser(t *testing.T) { - var v namespaceView - - uu := []struct { - s, e string - }{ - {"fred", "fred"}, - {"fred+", "fred"}, - {"fred(*)", "fred"}, - {"fred+(*)", "fred"}, - {"fred-blee+(*)", "fred-blee"}, - {"fred1-blee2+(*)", "fred1-blee2"}, - {"fred(𝜟)", "fred"}, - } - - for _, u := range uu { - assert.Equal(t, u.e, v.cleanser(u.s)) - } -} diff --git a/internal/views/no.go b/internal/views/no.go deleted file mode 100644 index fda110ed..00000000 --- a/internal/views/no.go +++ /dev/null @@ -1,63 +0,0 @@ -package views - -import ( - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/gdamore/tcell" -) - -type nodeView struct { - *resourceView -} - -func newNodeView(title, gvr string, app *appView, list resource.List) resourceViewer { - v := nodeView{newResourceView(title, gvr, app, list).(*resourceView)} - v.extraActionsFn = v.extraActions - v.enterFn = v.showPods - - return &v -} - -func (v *nodeView) extraActions(aa ui.KeyActions) { - aa[ui.KeyShiftC] = ui.NewKeyAction("Sort CPU", v.sortColCmd(7, false), false) - aa[ui.KeyShiftM] = ui.NewKeyAction("Sort MEM", v.sortColCmd(8, false), false) - aa[ui.KeyShiftX] = ui.NewKeyAction("Sort CPU%", v.sortColCmd(9, false), false) - aa[ui.KeyShiftZ] = ui.NewKeyAction("Sort MEM%", v.sortColCmd(10, false), false) -} - -func (v *nodeView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { - return func(evt *tcell.EventKey) *tcell.EventKey { - t := v.masterPage() - t.SetSortCol(t.NameColIndex()+col, 0, asc) - t.Refresh() - - return nil - } -} - -func (v *nodeView) showPods(app *appView, _, _, sel string) { - showPods(app, "", "", "spec.nodeName="+sel, v.backCmd) -} - -func (v *nodeView) backCmd(evt *tcell.EventKey) *tcell.EventKey { - v.app.inject(v) - - return nil -} - -func showPods(app *appView, ns, labelSel, fieldSel string, a ui.ActionHandler) { - app.switchNS(ns) - - list := resource.NewPodList(app.Conn(), ns) - list.SetLabelSelector(labelSel) - list.SetFieldSelector(fieldSel) - - pv := newPodView("Pod", "v1/pods", app, list) - pv.setColorerFn(podColorer) - pv.masterPage().SetActions(ui.KeyActions{ - tcell.KeyEsc: ui.NewKeyAction("Back", a, true), - }) - // Reset active namespace to ns. - app.Config.SetActiveNamespace(ns) - app.inject(pv) -} diff --git a/internal/views/ns.go b/internal/views/ns.go deleted file mode 100644 index 3335b9ba..00000000 --- a/internal/views/ns.go +++ /dev/null @@ -1,88 +0,0 @@ -package views - -import ( - "regexp" - - "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/gdamore/tcell" -) - -const ( - favNSIndicator = "+" - defaultNSIndicator = "(*)" - deltaNSIndicator = "(𝜟)" -) - -var nsCleanser = regexp.MustCompile(`(\w+)[+|(*)|(𝜟)]*`) - -type namespaceView struct { - *resourceView -} - -func newNamespaceView(title, gvr string, app *appView, list resource.List) resourceViewer { - v := namespaceView{newResourceView(title, gvr, app, list).(*resourceView)} - v.extraActionsFn = v.extraActions - v.masterPage().SetSelectedFn(v.cleanser) - v.decorateFn = v.decorate - v.enterFn = v.switchNs - - return &v -} - -func (v *namespaceView) extraActions(aa ui.KeyActions) { - aa[ui.KeyU] = ui.NewKeyAction("Use", v.useNsCmd, true) -} - -func (v *namespaceView) switchNs(app *appView, _, res, sel string) { - v.useNamespace(sel) - app.gotoResource("po", true) -} - -func (v *namespaceView) useNsCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.masterPage().RowSelected() { - return evt - } - v.useNamespace(v.masterPage().GetSelectedItem()) - - return nil -} - -func (v *namespaceView) useNamespace(ns string) { - if err := v.app.Config.SetActiveNamespace(ns); err != nil { - v.app.Flash().Err(err) - } else { - v.app.Flash().Infof("Namespace %s is now active!", ns) - } - v.app.Config.Save() - v.app.startInformer(ns) -} - -func (*namespaceView) cleanser(s string) string { - return nsCleanser.ReplaceAllString(s, `$1`) -} - -func (v *namespaceView) decorate(data resource.TableData) resource.TableData { - if _, ok := data.Rows[resource.AllNamespaces]; !ok { - if err := v.app.Conn().CheckNSAccess(""); err == nil { - data.Rows[resource.AllNamespace] = &resource.RowEvent{ - Action: resource.Unchanged, - Fields: resource.Row{resource.AllNamespace, "Active", "0"}, - Deltas: resource.Row{"", "", ""}, - } - } - } - for k, r := range data.Rows { - if config.InList(v.app.Config.FavNamespaces(), k) { - r.Fields[0] += "+" - r.Action = resource.Unchanged - } - if v.app.Config.ActiveNamespace() == k { - r.Fields[0] += "(*)" - r.Action = resource.Unchanged - } - } - - return data -} diff --git a/internal/views/pod.go b/internal/views/pod.go deleted file mode 100644 index abe9455c..00000000 --- a/internal/views/pod.go +++ /dev/null @@ -1,235 +0,0 @@ -package views - -import ( - "context" - "fmt" - - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/derailed/k9s/internal/watch" - "github.com/gdamore/tcell" - "github.com/rs/zerolog/log" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -const ( - containerFmt = "[fg:bg:b]%s([hilite:bg:b]%s[fg:bg:-])" - shellCheck = "command -v bash >/dev/null && exec bash || exec sh" -) - -type podView struct { - *resourceView - - childCancelFn context.CancelFunc -} - -var _ updatable = &podView{} - -type loggable interface { - getSelection() string - getList() resource.List - switchPage(n string) -} - -func newPodView(title, gvr string, app *appView, list resource.List) resourceViewer { - v := podView{resourceView: newResourceView(title, gvr, app, list).(*resourceView)} - v.extraActionsFn = v.extraActions - v.enterFn = v.listContainers - - picker := newSelectList(&v) - { - picker.setActions(ui.KeyActions{ - tcell.KeyEscape: {Description: "Back", Action: v.backCmd, Visible: true}, - }) - } - v.AddPage("picker", picker, true, false) - v.AddPage("logs", newLogsView(list.GetName(), app, &v), true, false) - - return &v -} - -func (v *podView) extraActions(aa ui.KeyActions) { - aa[tcell.KeyCtrlK] = ui.NewKeyAction("Kill", v.killCmd, true) - aa[ui.KeyS] = ui.NewKeyAction("Shell", v.shellCmd, true) - - aa[ui.KeyL] = ui.NewKeyAction("Logs", v.logsCmd, true) - aa[ui.KeyShiftL] = ui.NewKeyAction("Logs Previous", v.prevLogsCmd, true) - - aa[ui.KeyShiftR] = ui.NewKeyAction("Sort Ready", v.sortColCmd(1, false), false) - aa[ui.KeyShiftS] = ui.NewKeyAction("Sort Status", v.sortColCmd(2, true), false) - aa[ui.KeyShiftT] = ui.NewKeyAction("Sort Restart", v.sortColCmd(3, false), false) - aa[ui.KeyShiftC] = ui.NewKeyAction("Sort CPU", v.sortColCmd(4, false), false) - aa[ui.KeyShiftM] = ui.NewKeyAction("Sort MEM", v.sortColCmd(5, false), false) - aa[ui.KeyShiftX] = ui.NewKeyAction("Sort CPU%", v.sortColCmd(6, false), false) - aa[ui.KeyShiftZ] = ui.NewKeyAction("Sort MEM%", v.sortColCmd(7, false), false) - aa[ui.KeyShiftD] = ui.NewKeyAction("Sort IP", v.sortColCmd(8, true), false) - aa[ui.KeyShiftO] = ui.NewKeyAction("Sort Node", v.sortColCmd(9, true), false) -} - -func (v *podView) listContainers(app *appView, _, res, sel string) { - po, err := v.app.informer.Get(watch.PodIndex, sel, metav1.GetOptions{}) - if err != nil { - app.Flash().Errf("Unable to retrieve pods %s", err) - return - } - - pod := po.(*v1.Pod) - list := resource.NewContainerList(app.Conn(), pod) - title := skinTitle(fmt.Sprintf(containerFmt, "Container", sel), app.Styles.Frame()) - - // Stop my updater - if v.cancelFn != nil { - v.cancelFn() - } - - // Span child view - cv := newContainerView(title, app, list, fqn(pod.Namespace, pod.Name), v.exitFn) - v.AddPage("containers", cv, true, true) - var ctx context.Context - ctx, v.childCancelFn = context.WithCancel(v.parentCtx) - cv.Init(ctx, pod.Namespace) -} - -func (v *podView) exitFn() { - if v.childCancelFn != nil { - v.childCancelFn() - } - v.RemovePage("containers") - v.switchPage("master") - v.restartUpdates() -} - -// Protocol... - -func (v *podView) getList() resource.List { - return v.list -} - -func (v *podView) getSelection() string { - return v.masterPage().GetSelectedItem() -} - -func (v *podView) killCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.masterPage().RowSelected() { - return evt - } - - sel := v.masterPage().GetSelectedItems() - v.masterPage().ShowDeleted() - for _, res := range sel { - v.app.Flash().Infof("Delete resource %s %s", v.list.GetName(), res) - if err := v.list.Resource().Delete(res, true, false); err != nil { - v.app.Flash().Errf("Delete failed with %s", err) - } else { - deletePortForward(v.app.forwarders, res) - } - } - v.refresh() - return nil -} - -func (v *podView) logsCmd(evt *tcell.EventKey) *tcell.EventKey { - if v.viewLogs(false) { - return nil - } - - return evt -} - -func (v *podView) prevLogsCmd(evt *tcell.EventKey) *tcell.EventKey { - if v.viewLogs(true) { - return nil - } - - return evt -} - -func (v *podView) viewLogs(prev bool) bool { - if !v.masterPage().RowSelected() { - return false - } - v.showLogs(v.masterPage().GetSelectedItem(), "", v, prev) - - return true -} - -func (v *podView) showLogs(path, co string, parent loggable, prev bool) { - l := v.GetPrimitive("logs").(*logsView) - l.reload(co, parent, prev) - v.switchPage("logs") -} - -func (v *podView) shellCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.masterPage().RowSelected() { - return evt - } - - sel := v.masterPage().GetSelectedItem() - cc, err := fetchContainers(v.list, sel, false) - if err != nil { - v.app.Flash().Errf("Unable to retrieve containers %s", err) - return evt - } - if len(cc) == 1 { - v.shellIn(sel, "") - return nil - } - p := v.GetPrimitive("picker").(*selectList) - p.populate(cc) - p.SetSelectedFunc(func(i int, t, d string, r rune) { - v.shellIn(sel, t) - }) - v.switchPage("picker") - - return evt -} - -func (v *podView) shellIn(path, co string) { - v.stopUpdates() - shellIn(v.app, path, co) - v.restartUpdates() -} - -func (v *podView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { - return func(evt *tcell.EventKey) *tcell.EventKey { - t := v.masterPage() - t.SetSortCol(t.NameColIndex()+col, 0, asc) - t.Refresh() - - return nil - } -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func fetchContainers(l resource.List, po string, includeInit bool) ([]string, error) { - if len(po) == 0 { - return []string{}, nil - } - return l.Resource().(resource.Containers).Containers(po, includeInit) -} - -func shellIn(a *appView, path, co string) { - args := computeShellArgs(path, co, a.Config.K9s.CurrentContext, a.Conn().Config().Flags().KubeConfig) - log.Debug().Msgf("Shell args %v", args) - runK(true, a, args...) -} - -func computeShellArgs(path, co, context string, kcfg *string) []string { - args := make([]string, 0, 15) - args = append(args, "exec", "-it") - args = append(args, "--context", context) - ns, po := namespaced(path) - args = append(args, "-n", ns) - args = append(args, po) - if kcfg != nil && *kcfg != "" { - args = append(args, "--kubeconfig", *kcfg) - } - if co != "" { - args = append(args, "-c", co) - } - - return append(args, "--", "sh", "-c", shellCheck) -} diff --git a/internal/views/policy.go b/internal/views/policy.go deleted file mode 100644 index dfeff84b..00000000 --- a/internal/views/policy.go +++ /dev/null @@ -1,298 +0,0 @@ -package views - -import ( - "context" - "fmt" - "time" - - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/gdamore/tcell" - "github.com/rs/zerolog/log" - rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -const policyTitle = "Policy" - -var policyHeader = append(resource.Row{"NAMESPACE", "NAME", "API GROUP", "BINDING"}, rbacHeaderVerbs...) - -type ( - namespacedRole struct { - ns, role string - } - - policyView struct { - *tableView - - current ui.Igniter - cancel context.CancelFunc - subjectKind string - subjectName string - cache resource.RowEvents - } -) - -func newPolicyView(app *appView, subject, name string) *policyView { - v := policyView{} - { - v.subjectKind, v.subjectName = mapSubject(subject), name - v.tableView = newTableView(app, v.getTitle()) - v.SetColorerFn(rbacColorer) - v.current = app.Frame().GetPrimitive("main").(ui.Igniter) - v.bindKeys() - } - - return &v -} - -// Init the view. -func (v *policyView) Init(c context.Context, ns string) { - v.SetSortCol(1, len(rbacHeader), false) - - ctx, cancel := context.WithCancel(c) - v.cancel = cancel - go func(ctx context.Context) { - for { - select { - case <-ctx.Done(): - return - case <-time.After(time.Duration(v.app.Config.K9s.GetRefreshRate()) * time.Second): - v.refresh() - v.app.Draw() - } - } - }(ctx) - - v.refresh() - v.SelectRow(1, true) - v.app.SetFocus(v) -} - -func (v *policyView) bindKeys() { - v.RmAction(ui.KeyShiftA) - - v.SetActions(ui.KeyActions{ - tcell.KeyEscape: ui.NewKeyAction("Reset", v.resetCmd, false), - ui.KeySlash: ui.NewKeyAction("Filter", v.activateCmd, false), - ui.KeyP: ui.NewKeyAction("Previous", v.app.prevCmd, false), - ui.KeyShiftS: ui.NewKeyAction("Sort Namespace", v.SortColCmd(0), false), - ui.KeyShiftN: ui.NewKeyAction("Sort Name", v.SortColCmd(1), false), - ui.KeyShiftO: ui.NewKeyAction("Sort Group", v.SortColCmd(2), false), - ui.KeyShiftB: ui.NewKeyAction("Sort Binding", v.SortColCmd(3), false), - }) -} - -func (v *policyView) getTitle() string { - return fmt.Sprintf(rbacTitleFmt, policyTitle, v.subjectKind+":"+v.subjectName) -} - -func (v *policyView) refresh() { - data, err := v.reconcile() - if err != nil { - log.Error().Err(err).Msgf("Refresh for %s:%s", v.subjectKind, v.subjectName) - v.app.Flash().Err(err) - } - v.Update(data) -} - -func (v *policyView) resetCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.SearchBuff().Empty() { - v.SearchBuff().Reset() - return nil - } - - return v.backCmd(evt) -} - -func (v *policyView) backCmd(evt *tcell.EventKey) *tcell.EventKey { - if v.cancel != nil { - v.cancel() - } - - if v.SearchBuff().IsActive() { - v.SearchBuff().Reset() - return nil - } - - v.app.inject(v.current) - - return nil -} - -func (v *policyView) Hints() ui.Hints { - return v.Hints() -} - -func (v *policyView) reconcile() (resource.TableData, error) { - var table resource.TableData - - evts, errs := v.clusterPolicies() - if len(errs) > 0 { - for _, err := range errs { - log.Error().Err(err).Msg("Unable to find cluster policies") - } - return table, errs[0] - } - - nevts, errs := v.namespacedPolicies() - if len(errs) > 0 { - for _, err := range errs { - log.Error().Err(err).Msg("Unable to find cluster policies") - } - return table, errs[0] - } - - for k, v := range nevts { - evts[k] = v - } - - return buildTable(v, evts), nil -} - -// Protocol... - -func (v *policyView) header() resource.Row { - return policyHeader -} - -func (v *policyView) getCache() resource.RowEvents { - return v.cache -} - -func (v *policyView) setCache(evts resource.RowEvents) { - v.cache = evts -} - -func (v *policyView) clusterPolicies() (resource.RowEvents, []error) { - var errs []error - evts := make(resource.RowEvents) - - crbs, err := v.app.Conn().DialOrDie().RbacV1().ClusterRoleBindings().List(metav1.ListOptions{}) - if err != nil { - return evts, errs - } - - var rr []string - for _, crb := range crbs.Items { - for _, s := range crb.Subjects { - if s.Kind == v.subjectKind && s.Name == v.subjectName { - rr = append(rr, crb.RoleRef.Name) - } - } - } - - for _, r := range rr { - role, err := v.app.Conn().DialOrDie().RbacV1().ClusterRoles().Get(r, metav1.GetOptions{}) - if err != nil { - errs = append(errs, err) - } - for k, v := range v.parseRules("*", "CR:"+r, role.Rules) { - evts[k] = v - } - } - - return evts, errs -} - -func (v policyView) loadRoleBindings() ([]namespacedRole, error) { - var rr []namespacedRole - - dial := v.app.Conn().DialOrDie().RbacV1() - rbs, err := dial.RoleBindings("").List(metav1.ListOptions{}) - if err != nil { - return rr, err - } - - for _, rb := range rbs.Items { - for _, s := range rb.Subjects { - if s.Kind == v.subjectKind && s.Name == v.subjectName { - rr = append(rr, namespacedRole{rb.Namespace, rb.RoleRef.Name}) - } - } - } - - return rr, nil -} - -func (v *policyView) loadRoles(errs []error, rr []namespacedRole) (resource.RowEvents, []error) { - var ( - dial = v.app.Conn().DialOrDie().RbacV1() - evts = make(resource.RowEvents) - ) - for _, r := range rr { - if cr, err := dial.Roles(r.ns).Get(r.role, metav1.GetOptions{}); err != nil { - errs = append(errs, err) - } else { - for k, v := range v.parseRules(r.ns, "RO:"+r.role, cr.Rules) { - evts[k] = v - } - } - } - - return evts, errs -} - -func (v *policyView) namespacedPolicies() (resource.RowEvents, []error) { - var errs []error - rr, err := v.loadRoleBindings() - if err != nil { - errs = append(errs, err) - } - - evts, errs := v.loadRoles(errs, rr) - return evts, errs -} - -func (v *policyView) parseRules(ns, binding string, rules []rbacv1.PolicyRule) resource.RowEvents { - m := make(resource.RowEvents, len(rules)) - for _, r := range rules { - for _, grp := range r.APIGroups { - for _, res := range r.Resources { - k := res - if grp != "" { - k = res + "." + grp - } - for _, na := range r.ResourceNames { - n := fqn(k, na) - m[fqn(ns, n)] = &resource.RowEvent{ - Fields: append(policyRow(ns, n, grp, binding), asVerbs(r.Verbs...)...), - } - } - m[fqn(ns, k)] = &resource.RowEvent{ - Fields: append(policyRow(ns, k, grp, binding), asVerbs(r.Verbs...)...), - } - } - } - for _, nres := range r.NonResourceURLs { - if nres[0] != '/' { - nres = "/" + nres - } - m[fqn(ns, nres)] = &resource.RowEvent{ - Fields: append(policyRow(ns, nres, resource.NAValue, binding), asVerbs(r.Verbs...)...), - } - } - } - - return m -} - -func policyRow(ns, res, grp, binding string) resource.Row { - if grp != resource.NAValue { - grp = toGroup(grp) - } - - r := make(resource.Row, 0, len(policyHeader)) - return append(r, ns, res, grp, binding) -} - -func mapSubject(subject string) string { - switch subject { - case "g": - return "Group" - case "s": - return "ServiceAccount" - default: - return "User" - } -} diff --git a/internal/views/port_selector.go b/internal/views/port_selector.go deleted file mode 100644 index f22c0485..00000000 --- a/internal/views/port_selector.go +++ /dev/null @@ -1,43 +0,0 @@ -package views - -import ( - "github.com/derailed/tview" - "github.com/gdamore/tcell" -) - -type portSelector struct { - title, port string - ok, cancel func() -} - -func newSelector(title, port string, okFn, cancelFn func()) *portSelector { - return &portSelector{ - title: title, - port: port, - ok: okFn, - cancel: cancelFn, - } -} - -func (p *portSelector) show(app *appView) { - f := tview.NewForm() - f.SetItemPadding(0) - f.SetButtonsAlign(tview.AlignCenter). - SetButtonBackgroundColor(tview.Styles.PrimitiveBackgroundColor). - SetButtonTextColor(tview.Styles.PrimaryTextColor). - SetLabelColor(tcell.ColorAqua). - SetFieldTextColor(tcell.ColorOrange) - - f1 := p.port - f.AddInputField("Pod Port:", f1, 20, nil, func(changed string) { - f1 = changed - }) - - f.AddButton("OK", p.ok) - f.AddButton("Cancel", p.cancel) - - modal := tview.NewModalForm("<"+p.title+">", f) - modal.SetDoneFunc(func(_ int, b string) { - p.cancel() - }) -} diff --git a/internal/views/rbac.go b/internal/views/rbac.go deleted file mode 100644 index c2006875..00000000 --- a/internal/views/rbac.go +++ /dev/null @@ -1,341 +0,0 @@ -package views - -import ( - "context" - "fmt" - "strings" - "time" - - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/gdamore/tcell" - "github.com/rs/zerolog/log" - rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -const ( - clusterRole roleKind = iota - role - - all = "*" - rbacTitle = "RBAC" - rbacTitleFmt = " [fg:bg:b]%s([hilite:bg:b]%s[fg:bg:-])" -) - -var ( - rbacHeaderVerbs = resource.Row{ - "GET ", - "LIST ", - "DLIST ", - "WATCH ", - "CREATE", - "PATCH ", - "UPDATE", - "DELETE", - "EXTRAS", - } - - rbacHeader = append(resource.Row{"NAME", "API GROUP"}, rbacHeaderVerbs...) - - k8sVerbs = []string{ - "get", - "list", - "deletecollection", - "watch", - "create", - "patch", - "update", - "delete", - } - - httpVerbs = []string{ - "get", - "post", - "put", - "patch", - "delete", - "options", - } - - httpTok8sVerbs = map[string]string{ - "post": "create", - "put": "update", - } -) - -type ( - roleKind = int8 - - rbacView struct { - *tableView - - app *appView - current ui.Igniter - cancel context.CancelFunc - roleType roleKind - roleName string - cache resource.RowEvents - } -) - -func newRBACView(app *appView, ns, name string, kind roleKind) *rbacView { - v := rbacView{ - app: app, - roleName: name, - roleType: kind, - } - v.tableView = newTableView(app, v.getTitle()) - v.SetActiveNS(ns) - v.SetColorerFn(rbacColorer) - v.current = app.Frame().GetPrimitive("main").(ui.Igniter) - v.bindKeys() - - return &v -} - -// Init the view. -func (v *rbacView) Init(c context.Context, ns string) { - v.SetSortCol(1, len(rbacHeader), true) - - ctx, cancel := context.WithCancel(c) - v.cancel = cancel - go func(ctx context.Context) { - for { - select { - case <-ctx.Done(): - return - case <-time.After(time.Duration(v.app.Config.K9s.GetRefreshRate()) * time.Second): - v.app.QueueUpdateDraw(func() { - v.refresh() - }) - } - } - }(ctx) - - v.refresh() - v.app.SetHints(v.Hints()) - v.app.SetFocus(v) -} - -func (v *rbacView) bindKeys() { - v.RmAction(ui.KeyShiftA) - - v.SetActions(ui.KeyActions{ - tcell.KeyEscape: ui.NewKeyAction("Reset", v.resetCmd, false), - ui.KeySlash: ui.NewKeyAction("Filter", v.activateCmd, false), - ui.KeyP: ui.NewKeyAction("Previous", v.app.prevCmd, false), - ui.KeyShiftO: ui.NewKeyAction("Sort APIGroup", v.SortColCmd(1), false), - }) -} - -func (v *rbacView) getTitle() string { - return skinTitle(fmt.Sprintf(rbacTitleFmt, rbacTitle, v.roleName), v.app.Styles.Frame()) -} - -func (v *rbacView) refresh() { - data, err := v.reconcile(v.ActiveNS(), v.roleName, v.roleType) - if err != nil { - log.Error().Err(err).Msgf("Refresh for %s:%d", v.roleName, v.roleType) - v.app.Flash().Err(err) - } - v.Update(data) -} - -func (v *rbacView) resetCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.SearchBuff().Empty() { - v.SearchBuff().Reset() - return nil - } - - return v.backCmd(evt) -} - -func (v *rbacView) backCmd(evt *tcell.EventKey) *tcell.EventKey { - if v.cancel != nil { - v.cancel() - } - - if v.SearchBuff().IsActive() { - v.SearchBuff().Reset() - return nil - } - - v.app.inject(v.current) - - return nil -} - -func (v *rbacView) reconcile(ns, name string, kind roleKind) (resource.TableData, error) { - var table resource.TableData - - evts, err := v.rowEvents(ns, name, kind) - if err != nil { - return table, err - } - - return buildTable(v, evts), nil -} - -func (v *rbacView) header() resource.Row { - return rbacHeader -} - -func (v *rbacView) getCache() resource.RowEvents { - return v.cache -} - -func (v *rbacView) setCache(evts resource.RowEvents) { - v.cache = evts -} - -func (v *rbacView) rowEvents(ns, name string, kind roleKind) (resource.RowEvents, error) { - var ( - evts resource.RowEvents - err error - ) - - switch kind { - case clusterRole: - evts, err = v.clusterPolicies(name) - case role: - evts, err = v.namespacedPolicies(name) - default: - return evts, fmt.Errorf("Expecting clusterrole/role but found %d", kind) - } - if err != nil { - log.Error().Err(err).Msg("Unable to load CR") - return evts, err - } - - return evts, nil -} - -func (v *rbacView) clusterPolicies(name string) (resource.RowEvents, error) { - cr, err := v.app.Conn().DialOrDie().RbacV1().ClusterRoles().Get(name, metav1.GetOptions{}) - if err != nil { - return nil, err - } - - return v.parseRules(cr.Rules), nil -} - -func (v *rbacView) namespacedPolicies(path string) (resource.RowEvents, error) { - ns, na := namespaced(path) - cr, err := v.app.Conn().DialOrDie().RbacV1().Roles(ns).Get(na, metav1.GetOptions{}) - if err != nil { - return nil, err - } - - return v.parseRules(cr.Rules), nil -} - -func (v *rbacView) parseRules(rules []rbacv1.PolicyRule) resource.RowEvents { - m := make(resource.RowEvents, len(rules)) - for _, r := range rules { - for _, grp := range r.APIGroups { - for _, res := range r.Resources { - k := res - if grp != "" { - k = res + "." + grp - } - for _, na := range r.ResourceNames { - n := fqn(k, na) - m[n] = &resource.RowEvent{ - Fields: prepRow(n, grp, r.Verbs), - } - } - m[k] = &resource.RowEvent{ - Fields: prepRow(k, grp, r.Verbs), - } - } - } - for _, nres := range r.NonResourceURLs { - if nres[0] != '/' { - nres = "/" + nres - } - m[nres] = &resource.RowEvent{ - Fields: prepRow(nres, resource.NAValue, r.Verbs), - } - } - } - - return m -} - -func prepRow(res, grp string, verbs []string) resource.Row { - const ( - nameLen = 60 - groupLen = 30 - ) - - if grp != resource.NAValue { - grp = toGroup(grp) - } - - return makeRow(res, grp, asVerbs(verbs...)) -} - -func makeRow(res, group string, verbs []string) resource.Row { - r := make(resource.Row, 0, len(rbacHeader)) - r = append(r, res, group) - - return append(r, verbs...) -} - -func asVerbs(verbs ...string) resource.Row { - const ( - verbLen = 4 - unknownLen = 30 - ) - - r := make(resource.Row, 0, len(k8sVerbs)+1) - for _, v := range k8sVerbs { - r = append(r, toVerbIcon(hasVerb(verbs, v))) - } - - var unknowns []string - for _, v := range verbs { - if hv, ok := httpTok8sVerbs[v]; ok { - v = hv - } - if !hasVerb(k8sVerbs, v) && v != all { - unknowns = append(unknowns, v) - } - } - - return append(r, resource.Truncate(strings.Join(unknowns, ","), unknownLen)) -} - -func toVerbIcon(ok bool) string { - if ok { - return "[green::b] ✓ [::]" - } - return "[orangered::b] 𐄂 [::]" -} - -func hasVerb(verbs []string, verb string) bool { - if len(verbs) == 1 && verbs[0] == all { - return true - } - - for _, v := range verbs { - if hv, ok := httpTok8sVerbs[v]; ok { - if hv == verb { - return true - } - } - if v == verb { - return true - } - } - - return false -} - -func toGroup(g string) string { - if g == "" { - return "v1" - } - return g -} diff --git a/internal/views/rbac_test.go b/internal/views/rbac_test.go deleted file mode 100644 index bc9728ed..00000000 --- a/internal/views/rbac_test.go +++ /dev/null @@ -1,115 +0,0 @@ -package views - -import ( - "testing" - - "github.com/derailed/k9s/internal/resource" - "github.com/stretchr/testify/assert" - rbacv1 "k8s.io/api/rbac/v1" -) - -func TestHasVerb(t *testing.T) { - uu := []struct { - vv []string - v string - e bool - }{ - {[]string{"*"}, "get", true}, - {[]string{"get", "list", "watch"}, "watch", true}, - {[]string{"get", "dope", "list"}, "watch", false}, - {[]string{"get"}, "get", true}, - {[]string{"post"}, "create", true}, - {[]string{"put"}, "update", true}, - {[]string{"list", "deletecollection"}, "deletecollection", true}, - } - - for _, u := range uu { - assert.Equal(t, u.e, hasVerb(u.vv, u.v)) - } -} - -func TestAsVerbs(t *testing.T) { - ok, nok := toVerbIcon(true), toVerbIcon(false) - - uu := []struct { - vv []string - e resource.Row - }{ - {[]string{"*"}, resource.Row{ok, ok, ok, ok, ok, ok, ok, ok, ""}}, - {[]string{"get", "list", "patch"}, resource.Row{ok, ok, nok, nok, nok, ok, nok, nok, ""}}, - {[]string{"get", "list", "deletecollection", "post"}, resource.Row{ok, ok, ok, nok, ok, nok, nok, nok, ""}}, - {[]string{"get", "list", "blee"}, resource.Row{ok, ok, nok, nok, nok, nok, nok, nok, "blee"}}, - } - - for _, u := range uu { - assert.Equal(t, u.e, asVerbs(u.vv...)) - } -} - -func TestParseRules(t *testing.T) { - ok, nok := toVerbIcon(true), toVerbIcon(false) - _ = nok - - uu := []struct { - pp []rbacv1.PolicyRule - e map[string]resource.Row - }{ - { - []rbacv1.PolicyRule{ - {APIGroups: []string{"*"}, Resources: []string{"*"}, Verbs: []string{"*"}}, - }, - map[string]resource.Row{ - "*.*": {"*.*", "*", ok, ok, ok, ok, ok, ok, ok, ok, ""}, - }, - }, - { - []rbacv1.PolicyRule{ - {APIGroups: []string{"*"}, Resources: []string{"*"}, Verbs: []string{"get"}}, - }, - map[string]resource.Row{ - "*.*": {"*.*", "*", ok, nok, nok, nok, nok, nok, nok, nok, ""}, - }, - }, - { - []rbacv1.PolicyRule{ - {APIGroups: []string{""}, Resources: []string{"*"}, Verbs: []string{"list"}}, - }, - map[string]resource.Row{ - "*": {"*", "v1", nok, ok, nok, nok, nok, nok, nok, nok, ""}, - }, - }, - { - []rbacv1.PolicyRule{ - {APIGroups: []string{""}, Resources: []string{"pods"}, Verbs: []string{"list"}, ResourceNames: []string{"fred"}}, - }, - map[string]resource.Row{ - "pods": {"pods", "v1", nok, ok, nok, nok, nok, nok, nok, nok, ""}, - "pods/fred": {"pods/fred", "v1", nok, ok, nok, nok, nok, nok, nok, nok, ""}, - }, - }, - { - []rbacv1.PolicyRule{ - {APIGroups: []string{}, Resources: []string{}, Verbs: []string{"get"}, NonResourceURLs: []string{"/fred"}}, - }, - map[string]resource.Row{ - "/fred": {"/fred", resource.NAValue, ok, nok, nok, nok, nok, nok, nok, nok, ""}, - }, - }, - { - []rbacv1.PolicyRule{ - {APIGroups: []string{}, Resources: []string{}, Verbs: []string{"get"}, NonResourceURLs: []string{"fred"}}, - }, - map[string]resource.Row{ - "/fred": {"/fred", resource.NAValue, ok, nok, nok, nok, nok, nok, nok, nok, ""}, - }, - }, - } - - var v rbacView - for _, u := range uu { - evts := v.parseRules(u.pp) - for k, v := range u.e { - assert.Equal(t, v, evts[k].Fields) - } - } -} diff --git a/internal/views/registrar.go b/internal/views/registrar.go deleted file mode 100644 index 03b5e616..00000000 --- a/internal/views/registrar.go +++ /dev/null @@ -1,347 +0,0 @@ -package views - -import ( - "strings" - "time" - - "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/rs/zerolog/log" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -type ( - viewFn func(title, gvr string, app *appView, list resource.List) resourceViewer - listFn func(c resource.Connection, ns string) resource.List - enterFn func(app *appView, ns, resource, selection string) - decorateFn func(resource.TableData) resource.TableData - - viewer struct { - gvr string - kind string - namespaced bool - verbs metav1.Verbs - viewFn viewFn - listFn listFn - enterFn enterFn - colorerFn ui.ColorerFunc - decorateFn decorateFn - } - - viewers map[string]viewer -) - -func listFunc(l resource.List) viewFn { - return func(title, gvr string, app *appView, list resource.List) resourceViewer { - return newResourceView(title, gvr, app, l) - } -} - -var aliases = config.NewAliases() - -func allCRDs(c k8s.Connection, vv viewers) { - crds, err := resource.NewCustomResourceDefinitionList(c, resource.AllNamespaces). - Resource(). - List(resource.AllNamespaces, metav1.ListOptions{}) - if err != nil { - log.Error().Err(err).Msg("CRDs load fail") - return - } - - t := time.Now() - for _, crd := range crds { - meta, err := crd.ExtFields() - if err != nil { - log.Error().Err(err).Msgf("Error getting extended fields from %s", crd.Name()) - continue - } - - gvr := k8s.NewGVR(meta.Group, meta.Version, meta.Plural) - gvrs := gvr.String() - if meta.Plural != "" { - aliases.Define(gvrs, meta.Plural) - } - if meta.Singular != "" { - aliases.Define(gvrs, meta.Singular) - } - for _, a := range meta.ShortNames { - aliases.Define(gvrs, a) - } - - vv[gvrs] = viewer{ - gvr: gvrs, - kind: meta.Kind, - viewFn: listFunc(resource.NewCustomList(c, meta.Namespaced, "", gvrs)), - colorerFn: ui.DefaultColorer, - } - } - log.Debug().Msgf("Loading CRDS %v", time.Since(t)) -} - -func showRBAC(app *appView, ns, resource, selection string) { - kind := clusterRole - if resource == "role" { - kind = role - } - app.inject(newRBACView(app, ns, selection, kind)) -} - -func showCRD(app *appView, ns, resource, selection string) { - log.Debug().Msgf("Launching CRD %q -- %q -- %q", ns, resource, selection) - tokens := strings.Split(selection, ".") - app.gotoResource(tokens[0], true) - -} - -func showClusterRole(app *appView, ns, resource, selection string) { - crb, err := app.Conn().DialOrDie().RbacV1().ClusterRoleBindings().Get(selection, metav1.GetOptions{}) - if err != nil { - app.Flash().Errf("Unable to retrieve clusterrolebindings for %s", selection) - return - } - app.inject(newRBACView(app, ns, crb.RoleRef.Name, clusterRole)) -} - -func showRole(app *appView, _, resource, selection string) { - ns, n := namespaced(selection) - rb, err := app.Conn().DialOrDie().RbacV1().RoleBindings(ns).Get(n, metav1.GetOptions{}) - if err != nil { - app.Flash().Errf("Unable to retrieve rolebindings for %s", selection) - return - } - app.inject(newRBACView(app, ns, fqn(ns, rb.RoleRef.Name), role)) -} - -func showSAPolicy(app *appView, _, _, selection string) { - _, n := namespaced(selection) - app.inject(newPolicyView(app, mapFuSubject("ServiceAccount"), n)) -} - -func load(c k8s.Connection, vv viewers) { - if err := aliases.Load(); err != nil { - log.Error().Err(err).Msg("No custom aliases defined in config") - } - discovery, err := c.CachedDiscovery() - if err != nil { - log.Error().Err(err).Msgf("Error to get discovery client") - return - } - - rr, _ := discovery.ServerPreferredResources() - for _, r := range rr { - for _, res := range r.APIResources { - gvr := k8s.ToGVR(r.GroupVersion, res.Name) - cmd, ok := vv[gvr.String()] - if !ok { - // log.Debug().Msgf(fmt.Sprintf(">> No viewer defined for `%s`", gvr)) - continue - } - cmd.namespaced = res.Namespaced - cmd.kind = res.Kind - cmd.verbs = res.Verbs - cmd.gvr = gvr.String() - vv[gvr.String()] = cmd - gvrStr := gvr.String() - aliases.Define(gvrStr, strings.ToLower(res.Kind)) - aliases.Define(gvrStr, res.Name) - if len(res.SingularName) > 0 { - aliases.Define(gvrStr, res.SingularName) - } - for _, s := range res.ShortNames { - aliases.Define(gvrStr, s) - } - } - } -} - -func resourceViews(c k8s.Connection, m viewers) { - defer func(t time.Time) { - log.Debug().Msgf("Loading Views Elapsed %v", time.Since(t)) - }(time.Now()) - - coreRes(m) - miscRes(m) - appsRes(m) - authRes(m) - extRes(m) - netRes(m) - batchRes(m) - policyRes(m) - hpaRes(m) - - load(c, m) -} - -func coreRes(vv viewers) { - vv["v1/nodes"] = viewer{ - viewFn: newNodeView, - listFn: resource.NewNodeList, - colorerFn: nsColorer, - } - vv["v1/namespaces"] = viewer{ - viewFn: newNamespaceView, - listFn: resource.NewNamespaceList, - colorerFn: nsColorer, - } - vv["v1/pods"] = viewer{ - viewFn: newPodView, - listFn: resource.NewPodList, - colorerFn: podColorer, - } - vv["v1/serviceaccounts"] = viewer{ - listFn: resource.NewServiceAccountList, - enterFn: showSAPolicy, - } - vv["v1/services"] = viewer{ - viewFn: newSvcView, - listFn: resource.NewServiceList, - } - vv["v1/configmaps"] = viewer{ - listFn: resource.NewConfigMapList, - } - vv["v1/persistentvolumes"] = viewer{ - listFn: resource.NewPersistentVolumeList, - colorerFn: pvColorer, - } - vv["v1/persistentvolumeclaims"] = viewer{ - listFn: resource.NewPersistentVolumeClaimList, - colorerFn: pvcColorer, - } - vv["v1/secrets"] = viewer{ - viewFn: newSecretView, - listFn: resource.NewSecretList, - } - vv["v1/endpoints"] = viewer{ - listFn: resource.NewEndpointsList, - } - vv["v1/events"] = viewer{ - listFn: resource.NewEventList, - colorerFn: evColorer, - } - vv["v1/replicationcontrollers"] = viewer{ - viewFn: newScalableResourceView, - listFn: resource.NewReplicationControllerList, - colorerFn: rsColorer, - } -} - -func miscRes(vv viewers) { - vv["storage.k8s.io/v1/storageclasses"] = viewer{ - listFn: resource.NewStorageClassList, - } - vv["contexts"] = viewer{ - gvr: "contexts", - kind: "Contexts", - viewFn: newContextView, - listFn: resource.NewContextList, - colorerFn: ctxColorer, - } - vv["users"] = viewer{ - gvr: "users", - viewFn: newSubjectView, - } - vv["groups"] = viewer{ - gvr: "groups", - viewFn: newSubjectView, - } - vv["portforwards"] = viewer{ - gvr: "portforwards", - viewFn: newForwardView, - } - vv["benchmarks"] = viewer{ - gvr: "benchmarks", - viewFn: newBenchView, - } - vv["screendumps"] = viewer{ - gvr: "screendumps", - viewFn: newDumpView, - } -} - -func appsRes(vv viewers) { - vv["apps/v1/deployments"] = viewer{ - viewFn: newDeployView, - listFn: resource.NewDeploymentList, - colorerFn: dpColorer, - } - vv["apps/v1/replicasets"] = viewer{ - viewFn: newReplicaSetView, - listFn: resource.NewReplicaSetList, - colorerFn: rsColorer, - } - vv["apps/v1/statefulsets"] = viewer{ - viewFn: newStatefulSetView, - listFn: resource.NewStatefulSetList, - colorerFn: stsColorer, - } - vv["apps/v1/daemonsets"] = viewer{ - viewFn: newDaemonSetView, - listFn: resource.NewDaemonSetList, - colorerFn: dpColorer, - } -} - -func authRes(vv viewers) { - vv["rbac.authorization.k8s.io/v1/clusterroles"] = viewer{ - listFn: resource.NewClusterRoleList, - enterFn: showRBAC, - } - vv["rbac.authorization.k8s.io/v1/clusterrolebindings"] = viewer{ - listFn: resource.NewClusterRoleBindingList, - enterFn: showClusterRole, - } - vv["rbac.authorization.k8s.io/v1/rolebindings"] = viewer{ - listFn: resource.NewRoleBindingList, - enterFn: showRole, - } - vv["rbac.authorization.k8s.io/v1/roles"] = viewer{ - listFn: resource.NewRoleList, - enterFn: showRBAC, - } -} - -func extRes(vv viewers) { - vv["apiextensions.k8s.io/v1/customresourcedefinitions"] = viewer{ - listFn: resource.NewCustomResourceDefinitionList, - enterFn: showCRD, - } - vv["apiextensions.k8s.io/v1beta1/customresourcedefinitions"] = viewer{ - listFn: resource.NewCustomResourceDefinitionList, - enterFn: showCRD, - } -} - -func netRes(vv viewers) { - vv["networking.k8s.io/v1/networkpolicies"] = viewer{ - listFn: resource.NewNetworkPolicyList, - } - vv["extensions/v1beta1/ingresses"] = viewer{ - listFn: resource.NewIngressList, - } -} - -func batchRes(vv viewers) { - vv["batch/v1beta1/cronjobs"] = viewer{ - viewFn: newCronJobView, - listFn: resource.NewCronJobList, - } - vv["batch/v1/jobs"] = viewer{ - viewFn: newJobView, - listFn: resource.NewJobList, - } -} - -func policyRes(vv viewers) { - vv["policy/v1beta1/poddisruptionbudgets"] = viewer{ - listFn: resource.NewPDBList, - colorerFn: pdbColorer, - } -} - -func hpaRes(vv viewers) { - vv["autoscaling/v1/horizontalpodautoscalers"] = viewer{ - listFn: resource.NewHorizontalPodAutoscalerV1List, - } -} diff --git a/internal/views/resource.go b/internal/views/resource.go deleted file mode 100644 index d8d33cc5..00000000 --- a/internal/views/resource.go +++ /dev/null @@ -1,519 +0,0 @@ -package views - -import ( - "context" - "fmt" - "strconv" - "strings" - "time" - - "github.com/atotto/clipboard" - "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/derailed/k9s/internal/ui/dialog" - "github.com/gdamore/tcell" - "github.com/rs/zerolog/log" -) - -// EnvFn represent the current view exposed environment. -type envFn func() K9sEnv - -type ( - updatable interface { - restartUpdates() - stopUpdates() - update(context.Context) - } - - resourceView struct { - *masterDetail - - namespaces map[int]string - list resource.List - cancelFn context.CancelFunc - parentCtx context.Context - path *string - colorerFn ui.ColorerFunc - decorateFn decorateFn - envFn envFn - gvr string - } -) - -func newResourceView(title, gvr string, app *appView, list resource.List) resourceViewer { - v := resourceView{ - list: list, - gvr: gvr, - } - v.masterDetail = newMasterDetail(title, list.GetNamespace(), app, v.backCmd) - v.envFn = v.defaultK9sEnv - - return &v -} - -// Init watches all running pods in given namespace -func (v *resourceView) Init(ctx context.Context, ns string) { - v.masterDetail.init(ctx, ns) - v.masterPage().setFilterFn(v.filterResource) - if v.colorerFn != nil { - v.masterPage().SetColorerFn(v.colorerFn) - } - - v.parentCtx = ctx - var vctx context.Context - vctx, v.cancelFn = context.WithCancel(ctx) - - colorer := ui.DefaultColorer - if v.colorerFn != nil { - colorer = v.colorerFn - } - v.masterPage().SetColorerFn(colorer) - - v.update(vctx) - v.app.clusterInfo().refresh() - v.refresh() - - tv := v.masterPage() - r, _ := tv.GetSelection() - if r == 0 && tv.GetRowCount() > 0 { - tv.Select(1, 0) - } -} - -func (v *resourceView) setColorerFn(f ui.ColorerFunc) { - v.colorerFn = f -} - -func (v *resourceView) setDecorateFn(f decorateFn) { - v.decorateFn = f -} - -func (v *resourceView) filterResource(sel string) { - v.list.SetLabelSelector(sel) - v.refresh() -} - -func (v *resourceView) stopUpdates() { - if v.cancelFn != nil { - v.cancelFn() - } -} - -func (v *resourceView) restartUpdates() { - if v.cancelFn != nil { - v.cancelFn() - } - - var vctx context.Context - vctx, v.cancelFn = context.WithCancel(v.parentCtx) - v.update(vctx) -} - -func (v *resourceView) update(ctx context.Context) { - go func(ctx context.Context) { - for { - select { - case <-ctx.Done(): - log.Debug().Msgf("%s updater canceled!", v.list.GetName()) - return - case <-time.After(time.Duration(v.app.Config.K9s.GetRefreshRate()) * time.Second): - v.app.QueueUpdateDraw(func() { - v.refresh() - }) - } - } - }(ctx) -} - -func (v *resourceView) backCmd(*tcell.EventKey) *tcell.EventKey { - v.switchPage("master") - return nil -} - -func (v *resourceView) switchPage(p string) { - log.Debug().Msgf("Switching page to %s", p) - if _, ok := v.CurrentPage().Item.(*tableView); ok { - v.stopUpdates() - } - - v.SwitchToPage(p) - v.currentNS = v.list.GetNamespace() - if vu, ok := v.GetPrimitive(p).(ui.Hinter); ok { - v.app.SetHints(vu.Hints()) - } - - if _, ok := v.CurrentPage().Item.(*tableView); ok { - v.restartUpdates() - } -} - -// ---------------------------------------------------------------------------- -// Actions... - -func (v *resourceView) cpCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.masterPage().RowSelected() { - return evt - } - - _, n := namespaced(v.masterPage().GetSelectedItem()) - log.Debug().Msgf("Copied selection to clipboard %q", n) - v.app.Flash().Info("Current selection copied to clipboard...") - if err := clipboard.WriteAll(n); err != nil { - v.app.Flash().Err(err) - } - - return nil -} - -func (v *resourceView) enterCmd(evt *tcell.EventKey) *tcell.EventKey { - // If in command mode run filter otherwise enter function. - if v.masterPage().filterCmd(evt) == nil || !v.masterPage().RowSelected() { - return nil - } - - f := v.defaultEnter - if v.enterFn != nil { - f = v.enterFn - } - f(v.app, v.list.GetNamespace(), v.list.GetName(), v.masterPage().GetSelectedItem()) - - return nil -} - -func (v *resourceView) refreshCmd(*tcell.EventKey) *tcell.EventKey { - v.app.Flash().Info("Refreshing...") - v.refresh() - return nil -} - -func (v *resourceView) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.masterPage().RowSelected() { - return evt - } - - sel := v.masterPage().GetSelectedItems() - var msg string - if len(sel) > 1 { - msg = fmt.Sprintf("Delete %d selected %s?", len(sel), v.list.GetName()) - } else { - msg = fmt.Sprintf("Delete %s %s?", v.list.GetName(), sel[0]) - } - dialog.ShowDelete(v.Pages, msg, func(cascade, force bool) { - v.masterPage().ShowDeleted() - if len(sel) > 1 { - v.app.Flash().Infof("Delete %d selected %s", len(sel), v.list.GetName()) - } else { - v.app.Flash().Infof("Delete resource %s %s", v.list.GetName(), sel[0]) - } - for _, res := range sel { - if err := v.list.Resource().Delete(res, cascade, force); err != nil { - v.app.Flash().Errf("Delete failed with %s", err) - } else { - deletePortForward(v.app.forwarders, res) - } - } - v.refresh() - }, func() { - v.switchPage("master") - }) - return nil -} - -func (v *resourceView) markCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.masterPage().RowSelected() { - return evt - } - - v.masterPage().ToggleMark() - v.refresh() - v.app.Draw() - return nil -} - -func deletePortForward(ff map[string]forwarder, sel string) { - for k, v := range ff { - tokens := strings.Split(k, ":") - if tokens[0] == sel { - log.Debug().Msgf("Deleting associated portForward %s", k) - v.Stop() - } - } -} - -func (v *resourceView) defaultEnter(app *appView, ns, _, selection string) { - if !v.list.Access(resource.DescribeAccess) { - return - } - - log.Debug().Msgf("!!!!!! NAME %s", v.list.GetName()) - yaml, err := v.list.Resource().Describe(v.gvr, selection) - if err != nil { - v.app.Flash().Errf("Describe command failed: %s", err) - return - } - - details := v.detailsPage() - details.setCategory("Describe") - details.setTitle(selection) - details.SetTextColor(v.app.Styles.FgColor()) - details.SetText(colorizeYAML(v.app.Styles.Views().Yaml, yaml)) - details.ScrollToBeginning() - v.app.SetHints(details.hints()) - - v.switchPage("details") -} - -func (v *resourceView) describeCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.masterPage().RowSelected() { - return evt - } - v.defaultEnter(v.app, v.list.GetNamespace(), v.list.GetName(), v.masterPage().GetSelectedItem()) - - return nil -} - -func (v *resourceView) viewCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.masterPage().RowSelected() { - return evt - } - - sel := v.masterPage().GetSelectedItem() - raw, err := v.list.Resource().Marshal(sel) - if err != nil { - v.app.Flash().Errf("Unable to marshal resource %s", err) - return evt - } - details := v.detailsPage() - details.setCategory("YAML") - details.setTitle(sel) - details.SetTextColor(v.app.Styles.FgColor()) - details.SetText(colorizeYAML(v.app.Styles.Views().Yaml, raw)) - details.ScrollToBeginning() - v.app.SetHints(details.hints()) - - v.switchPage("details") - - return nil -} - -func (v *resourceView) editCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.masterPage().RowSelected() { - return evt - } - - v.stopUpdates() - { - ns, po := namespaced(v.masterPage().GetSelectedItem()) - args := make([]string, 0, 10) - args = append(args, "edit") - args = append(args, v.list.GetName()) - args = append(args, "-n", ns) - args = append(args, "--context", v.app.Config.K9s.CurrentContext) - if cfg := v.app.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" { - args = append(args, "--kubeconfig", *cfg) - } - runK(true, v.app, append(args, po)...) - } - v.restartUpdates() - - return evt -} - -func (v *resourceView) setNamespace(ns string) { - if v.list.Namespaced() { - v.currentNS = ns - v.list.SetNamespace(ns) - } -} - -func (v *resourceView) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey { - i, _ := strconv.Atoi(string(evt.Rune())) - ns := v.namespaces[i] - if ns == "" { - ns = resource.AllNamespace - } - if v.currentNS == ns { - return nil - } - - v.app.switchNS(ns) - v.setNamespace(ns) - v.app.Flash().Infof("Viewing namespace `%s`...", ns) - v.refresh() - v.masterPage().UpdateTitle() - v.masterPage().SelectRow(1, true) - v.app.CmdBuff().Reset() - v.app.Config.SetActiveNamespace(v.currentNS) - v.app.Config.Save() - - return nil -} - -func (v *resourceView) refresh() { - if _, ok := v.CurrentPage().Item.(*tableView); !ok { - return - } - - v.refreshActions() - if v.list.Namespaced() { - v.list.SetNamespace(v.currentNS) - } - if err := v.list.Reconcile(v.app.informer, v.path); err != nil { - v.app.Flash().Err(err) - } - data := v.list.Data() - if v.decorateFn != nil { - data = v.decorateFn(data) - } - v.masterPage().Update(data) -} - -func (v *resourceView) namespaceActions(aa ui.KeyActions) { - if !v.list.Access(resource.NamespaceAccess) { - return - } - v.namespaces = make(map[int]string, config.MaxFavoritesNS) - // User can't list namespace. Don't offer a choice. - if v.app.Conn().CheckListNSAccess() != nil { - return - } - aa[tcell.Key(ui.NumKeys[0])] = ui.NewKeyAction(resource.AllNamespace, v.switchNamespaceCmd, true) - v.namespaces[0] = resource.AllNamespace - index := 1 - for _, n := range v.app.Config.FavNamespaces() { - if n == resource.AllNamespace { - continue - } - aa[tcell.Key(ui.NumKeys[index])] = ui.NewKeyAction(n, v.switchNamespaceCmd, true) - v.namespaces[index] = n - index++ - } -} - -func (v *resourceView) refreshActions() { - aa := ui.KeyActions{ - ui.KeyC: ui.NewKeyAction("Copy", v.cpCmd, false), - tcell.KeyEnter: ui.NewKeyAction("Enter", v.enterCmd, false), - tcell.KeyCtrlR: ui.NewKeyAction("Refresh", v.refreshCmd, false), - } - aa[ui.KeySpace] = ui.NewKeyAction("Mark", v.markCmd, true) - v.namespaceActions(aa) - v.defaultActions(aa) - - if v.list.Access(resource.EditAccess) { - aa[ui.KeyE] = ui.NewKeyAction("Edit", v.editCmd, true) - } - if v.list.Access(resource.DeleteAccess) { - aa[tcell.KeyCtrlD] = ui.NewKeyAction("Delete", v.deleteCmd, true) - } - if v.list.Access(resource.ViewAccess) { - aa[ui.KeyY] = ui.NewKeyAction("YAML", v.viewCmd, true) - } - if v.list.Access(resource.DescribeAccess) { - aa[ui.KeyD] = ui.NewKeyAction("Describe", v.describeCmd, true) - } - v.customActions(aa) - - t := v.masterPage() - t.SetActions(aa) - v.app.SetHints(t.Hints()) -} - -func (v *resourceView) customActions(aa ui.KeyActions) { - pp := config.NewPlugins() - if err := pp.Load(); err != nil { - log.Warn().Msgf("No plugin configuration found") - return - } - - for k, plugin := range pp.Plugin { - if !in(plugin.Scopes, v.list.GetName()) { - continue - } - key, err := asKey(plugin.ShortCut) - if err != nil { - log.Error().Err(err).Msg("Unable to map shortcut to a key") - continue - } - _, ok := aa[key] - if ok { - log.Error().Err(fmt.Errorf("Doh! you are trying to overide an existing command `%s", k)).Msg("Invalid shortcut") - continue - } - aa[key] = ui.NewKeyAction( - plugin.Description, - v.execCmd(plugin.Command, plugin.Background, plugin.Args...), - true) - } -} - -func (v *resourceView) execCmd(bin string, bg bool, args ...string) ui.ActionHandler { - return func(evt *tcell.EventKey) *tcell.EventKey { - if !v.masterPage().RowSelected() { - return evt - } - - var ( - env = v.envFn() - aa = make([]string, len(args)) - err error - ) - for i, a := range args { - aa[i], err = env.envFor(a) - if err != nil { - log.Error().Err(err).Msg("Args match failed") - return nil - } - } - - if run(true, v.app, bin, bg, aa...) { - v.app.Flash().Info("Custom CMD launched!") - } else { - v.app.Flash().Info("Custom CMD failed!") - } - return nil - } -} - -func (v *resourceView) defaultK9sEnv() K9sEnv { - ns, n := namespaced(v.masterPage().GetSelectedItem()) - ctx, err := v.app.Conn().Config().CurrentContextName() - if err != nil { - ctx = "n/a" - } - cluster, err := v.app.Conn().Config().CurrentClusterName() - if err != nil { - cluster = "n/a" - } - user, err := v.app.Conn().Config().CurrentUserName() - if err != nil { - user = "n/a" - } - groups, err := v.app.Conn().Config().CurrentGroupNames() - if err != nil { - groups = []string{"n/a"} - } - var cfg string - kcfg := v.app.Conn().Config().Flags().KubeConfig - if kcfg != nil && *kcfg != "" { - cfg = *kcfg - } - - env := K9sEnv{ - "NAMESPACE": ns, - "NAME": n, - "CONTEXT": ctx, - "CLUSTER": cluster, - "USER": user, - "GROUPS": strings.Join(groups, ","), - "KUBECONFIG": cfg, - } - - row := v.masterPage().GetRow() - for i, r := range row { - env["COL"+strconv.Itoa(i)] = r - } - - return env -} diff --git a/internal/views/restartable_resource.go b/internal/views/restartable_resource.go deleted file mode 100644 index 10b7ebb2..00000000 --- a/internal/views/restartable_resource.go +++ /dev/null @@ -1,60 +0,0 @@ -package views - -import ( - "errors" - - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/derailed/k9s/internal/ui/dialog" - "github.com/gdamore/tcell" -) - -type ( - restartableResourceView struct { - *resourceView - } -) - -func newRestartableResourceViewForParent(parent *resourceView) *restartableResourceView { - v := restartableResourceView{ - parent, - } - parent.extraActionsFn = v.extraActions - return &v -} - -func (v *restartableResourceView) extraActions(aa ui.KeyActions) { - aa[tcell.KeyCtrlT] = ui.NewKeyAction("Restart Rollout", v.restartCmd, true) -} - -func (v *restartableResourceView) restartCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.masterPage().RowSelected() { - return evt - } - - sel := v.masterPage().GetSelectedItem() - v.stopUpdates() - defer v.restartUpdates() - msg := "Please confirm rollout restart for " + sel - dialog.ShowConfirm(v.Pages, "", msg, func() { - if err := v.restartRollout(sel); err != nil { - v.app.Flash().Err(err) - } else { - v.app.Flash().Infof("Rollout restart in progress for `%s...", sel) - } - }, func() { - v.showMaster() - }) - - return nil -} - -func (v *restartableResourceView) restartRollout(selection string) error { - r, ok := v.list.Resource().(resource.Restartable) - if !ok { - return errors.New("resource is not of type resource.Restartable") - } - ns, n := namespaced(selection) - - return r.Restart(ns, n) -} diff --git a/internal/views/rs.go b/internal/views/rs.go deleted file mode 100644 index fbca0e24..00000000 --- a/internal/views/rs.go +++ /dev/null @@ -1,189 +0,0 @@ -package views - -import ( - "errors" - "fmt" - "strconv" - "strings" - - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/derailed/tview" - "github.com/gdamore/tcell" - "github.com/rs/zerolog/log" - appsv1 "k8s.io/api/apps/v1" - v1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/kubectl/pkg/polymorphichelpers" -) - -type replicaSetView struct { - *resourceView -} - -func newReplicaSetView(title, gvr string, app *appView, list resource.List) resourceViewer { - v := replicaSetView{newResourceView(title, gvr, app, list).(*resourceView)} - v.extraActionsFn = v.extraActions - v.enterFn = v.showPods - - return &v -} - -func (v *replicaSetView) extraActions(aa ui.KeyActions) { - aa[ui.KeyShiftD] = ui.NewKeyAction("Sort Desired", v.sortColCmd(1, false), false) - aa[ui.KeyShiftC] = ui.NewKeyAction("Sort Current", v.sortColCmd(2, false), false) - aa[tcell.KeyCtrlB] = ui.NewKeyAction("Rollback", v.rollbackCmd, true) -} - -func (v *replicaSetView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { - return func(evt *tcell.EventKey) *tcell.EventKey { - t := v.masterPage() - t.SetSortCol(t.NameColIndex()+col, 0, asc) - t.Refresh() - - return nil - } -} - -func (v *replicaSetView) showPods(app *appView, ns, res, sel string) { - ns, n := namespaced(sel) - rset := k8s.NewReplicaSet(app.Conn()) - r, err := rset.Get(ns, n) - if err != nil { - app.Flash().Errf("Replicaset failed %s", err) - } - - rs := r.(*v1.ReplicaSet) - l, err := metav1.LabelSelectorAsSelector(rs.Spec.Selector) - if err != nil { - app.Flash().Errf("Selector failed %s", err) - return - } - - showPods(app, ns, l.String(), "", v.backCmd) -} - -func (v *replicaSetView) backCmd(evt *tcell.EventKey) *tcell.EventKey { - v.app.inject(v) - - return nil -} - -func (v *replicaSetView) rollbackCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.masterPage().RowSelected() { - return evt - } - - sel := v.masterPage().GetSelectedItem() - v.showModal(fmt.Sprintf("Rollback %s %s?", v.list.GetName(), sel), func(_ int, button string) { - if button == "OK" { - v.app.Flash().Infof("Rolling back %s %s", v.list.GetName(), sel) - if res, err := rollback(v.app.Conn(), sel); err != nil { - v.app.Flash().Err(err) - } else { - v.app.Flash().Info(res) - } - v.refresh() - } - v.dismissModal() - }) - - return nil -} - -func (v *replicaSetView) dismissModal() { - v.RemovePage("confirm") - v.switchPage("master") -} - -func (v *replicaSetView) showModal(msg string, done func(int, string)) { - confirm := tview.NewModal(). - AddButtons([]string{"Cancel", "OK"}). - SetTextColor(tcell.ColorFuchsia). - SetText(msg). - SetDoneFunc(done) - v.AddPage("confirm", confirm, false, false) - v.ShowPage("confirm") -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func findRS(Conn k8s.Connection, ns, n string) (*v1.ReplicaSet, error) { - rset := k8s.NewReplicaSet(Conn) - r, err := rset.Get(ns, n) - if err != nil { - return nil, err - } - return r.(*v1.ReplicaSet), nil -} - -func findDP(Conn k8s.Connection, ns, n string) (*appsv1.Deployment, error) { - dp, err := k8s.NewDeployment(Conn).Get(ns, n) - if err != nil { - return nil, err - } - return dp.(*appsv1.Deployment), nil -} - -func controllerInfo(rs *v1.ReplicaSet) (string, string, string, error) { - for _, ref := range rs.ObjectMeta.OwnerReferences { - if ref.Controller == nil { - continue - } - log.Debug().Msgf("Controller name %s", ref.Name) - tokens := strings.Split(ref.APIVersion, "/") - apiGroup := ref.APIVersion - if len(tokens) == 2 { - apiGroup = tokens[0] - } - return ref.Name, ref.Kind, apiGroup, nil - } - return "", "", "", fmt.Errorf("Unable to find controller for ReplicaSet %s", rs.ObjectMeta.Name) -} - -func getRevision(rs *v1.ReplicaSet) (int64, error) { - revision := rs.ObjectMeta.Annotations["deployment.kubernetes.io/revision"] - if rs.Status.Replicas != 0 { - return 0, errors.New("Can not rollback current replica") - } - vers, err := strconv.Atoi(revision) - if err != nil { - return 0, errors.New("Revision conversion failed") - } - return int64(vers), nil -} - -func rollback(Conn k8s.Connection, selectedItem string) (string, error) { - ns, n := namespaced(selectedItem) - - rs, err := findRS(Conn, ns, n) - if err != nil { - return "", err - } - version, err := getRevision(rs) - if err != nil { - return "", err - } - - name, kind, apiGroup, err := controllerInfo(rs) - if err != nil { - return "", err - } - rb, err := polymorphichelpers.RollbackerFor(schema.GroupKind{apiGroup, kind}, Conn.DialOrDie()) - if err != nil { - return "", err - } - dp, err := findDP(Conn, ns, name) - if err != nil { - return "", err - } - res, err := rb.Rollback(dp, map[string]string{}, version, false) - if err != nil { - return "", err - } - - return res, nil -} diff --git a/internal/views/scalable_resource.go b/internal/views/scalable_resource.go deleted file mode 100644 index be1f6325..00000000 --- a/internal/views/scalable_resource.go +++ /dev/null @@ -1,114 +0,0 @@ -package views - -import ( - "fmt" - "strconv" - "strings" - - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/derailed/tview" - "github.com/gdamore/tcell" -) - -type ( - scalableResourceView struct { - *resourceView - } -) - -func newScalableResourceView(title, gvr string, app *appView, list resource.List) resourceViewer { - return *newScalableResourceViewForParent(newResourceView(title, gvr, app, list).(*resourceView)) -} - -func newScalableResourceViewForParent(parent *resourceView) *scalableResourceView { - v := scalableResourceView{ - parent, - } - parent.extraActionsFn = v.extraActions - return &v -} - -func (v *scalableResourceView) extraActions(aa ui.KeyActions) { - aa[ui.KeyS] = ui.NewKeyAction("Scale", v.scaleCmd, true) -} - -func (v *scalableResourceView) scaleCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.masterPage().RowSelected() { - return evt - } - - v.showScaleDialog(v.list.GetName(), v.masterPage().GetSelectedItem()) - return nil -} - -func (v *scalableResourceView) scale(selection string, replicas int) { - ns, n := namespaced(selection) - - r := v.list.Resource().(resource.Scalable) - - err := r.Scale(ns, n, int32(replicas)) - if err != nil { - v.app.Flash().Err(err) - } -} - -func (v *scalableResourceView) showScaleDialog(resourceType string, resourceName string) { - f := v.createScaleForm() - - confirm := tview.NewModalForm("", f) - confirm.SetText(fmt.Sprintf("Scale %s %s", resourceType, resourceName)) - confirm.SetDoneFunc(func(int, string) { - v.dismissScaleDialog() - }) - v.AddPage(scaleDialogKey, confirm, false, false) - v.ShowPage(scaleDialogKey) -} - -func (v *scalableResourceView) createScaleForm() *tview.Form { - f := v.createStyledForm() - - tv := v.masterPage() - replicas := strings.TrimSpace(tv.GetCell(tv.GetSelectedRow(), tv.NameColIndex()+1).Text) - f.AddInputField("Replicas:", replicas, 4, func(textToCheck string, lastChar rune) bool { - _, err := strconv.Atoi(textToCheck) - return err == nil - }, func(changed string) { - replicas = changed - }) - - f.AddButton("OK", func() { - v.okSelected(replicas) - }) - - f.AddButton("Cancel", func() { - v.dismissScaleDialog() - }) - - return f -} - -func (v *scalableResourceView) createStyledForm() *tview.Form { - f := tview.NewForm() - f.SetItemPadding(0) - f.SetButtonsAlign(tview.AlignCenter). - SetButtonBackgroundColor(tview.Styles.PrimitiveBackgroundColor). - SetButtonTextColor(tview.Styles.PrimaryTextColor). - SetLabelColor(tcell.ColorAqua). - SetFieldTextColor(tcell.ColorOrange) - return f -} - -func (v *scalableResourceView) okSelected(replicas string) { - if val, err := strconv.Atoi(replicas); err == nil { - v.scale(v.masterPage().GetSelectedItem(), val) - } else { - v.app.Flash().Err(err) - } - - v.dismissScaleDialog() -} - -func (v *scalableResourceView) dismissScaleDialog() { - v.Pages.RemovePage(scaleDialogKey) -} diff --git a/internal/views/secret.go b/internal/views/secret.go deleted file mode 100644 index 617537b9..00000000 --- a/internal/views/secret.go +++ /dev/null @@ -1,59 +0,0 @@ -package views - -import ( - "sigs.k8s.io/yaml" - - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/gdamore/tcell" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -type secretView struct { - *resourceView -} - -func newSecretView(title, gvr string, app *appView, list resource.List) resourceViewer { - v := secretView{newResourceView(title, gvr, app, list).(*resourceView)} - v.extraActionsFn = v.extraActions - - return &v -} - -func (v *secretView) extraActions(aa ui.KeyActions) { - aa[tcell.KeyCtrlX] = ui.NewKeyAction("Decode", v.decodeCmd, true) -} - -func (v *secretView) decodeCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.masterPage().RowSelected() { - return evt - } - - sel := v.masterPage().GetSelectedItem() - ns, n := namespaced(sel) - sec, err := v.app.Conn().DialOrDie().CoreV1().Secrets(ns).Get(n, metav1.GetOptions{}) - if err != nil { - v.app.Flash().Errf("Unable to retrieve secret %s", err) - return evt - } - - d := make(map[string]string, len(sec.Data)) - for k, val := range sec.Data { - d[k] = string(val) - } - raw, err := yaml.Marshal(d) - if err != nil { - v.app.Flash().Errf("Error decoding secret %s", err) - return nil - } - - details := v.detailsPage() - details.setCategory("Decoder") - details.setTitle(sel) - details.SetTextColor(v.app.Styles.FgColor()) - details.SetText(colorizeYAML(v.app.Styles.Views().Yaml, string(raw))) - details.ScrollToBeginning() - v.switchPage("details") - - return nil -} diff --git a/internal/views/select_list.go b/internal/views/select_list.go deleted file mode 100644 index 6b077707..00000000 --- a/internal/views/select_list.go +++ /dev/null @@ -1,77 +0,0 @@ -package views - -import ( - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/derailed/tview" - "github.com/gdamore/tcell" -) - -type selectList struct { - *tview.List - - parent loggable - actions ui.KeyActions -} - -func newSelectList(parent loggable) *selectList { - v := selectList{List: tview.NewList(), actions: ui.KeyActions{}} - { - v.parent = parent - v.SetBorder(true) - v.SetMainTextColor(tcell.ColorWhite) - v.ShowSecondaryText(false) - v.SetShortcutColor(tcell.ColorAqua) - v.SetSelectedBackgroundColor(tcell.ColorAqua) - v.SetTitle(" [aqua::b]Container Selector ") - v.SetInputCapture(func(evt *tcell.EventKey) *tcell.EventKey { - if a, ok := v.actions[evt.Key()]; ok { - a.Action(evt) - evt = nil - } - return evt - }) - } - - return &v -} - -func (v *selectList) back(evt *tcell.EventKey) *tcell.EventKey { - v.parent.switchPage("master") - - return nil -} - -// Protocol... - -func (v *selectList) switchPage(p string) { - v.parent.switchPage(p) -} - -func (v *selectList) getList() resource.List { - return v.parent.getList() -} - -func (v *selectList) getSelection() string { - return v.parent.getSelection() -} - -// SetActions to handle keyboard events. -func (v *selectList) setActions(aa ui.KeyActions) { - v.actions = aa -} - -func (v *selectList) Hints() ui.Hints { - if v.actions != nil { - return v.actions.Hints() - } - - return nil -} - -func (v *selectList) populate(ss []string) { - v.Clear() - for i, s := range ss { - v.AddItem(s, "Select a container", rune('a'+i), nil) - } -} diff --git a/internal/views/status.go b/internal/views/status.go deleted file mode 100644 index 9adbea5b..00000000 --- a/internal/views/status.go +++ /dev/null @@ -1,35 +0,0 @@ -package views - -import ( - "fmt" - - "github.com/derailed/k9s/internal/config" - "github.com/derailed/tview" -) - -type statusView struct { - *tview.TextView - - styles *config.Styles -} - -func newStatusView(styles *config.Styles) *statusView { - v := statusView{styles: styles, TextView: tview.NewTextView()} - { - v.SetBackgroundColor(config.AsColor(styles.Views().Log.BgColor)) - v.SetTextAlign(tview.AlignRight) - v.SetDynamicColors(true) - } - return &v -} - -func (v *statusView) update(status []string) { - v.Clear() - last, bgColor := len(status)-1, v.styles.Frame().Crumb.BgColor - for i, c := range status { - if i == last { - bgColor = v.styles.Frame().Crumb.ActiveColor - } - fmt.Fprintf(v, "[%s:%s:b] %-15s ", v.styles.Frame().Crumb.FgColor, bgColor, c) - } -} diff --git a/internal/views/status_test.go b/internal/views/status_test.go deleted file mode 100644 index acdd0485..00000000 --- a/internal/views/status_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package views - -import ( - "testing" - - "github.com/derailed/k9s/internal/config" - "github.com/stretchr/testify/assert" -) - -func TestNewStatus(t *testing.T) { - defaults, _ := config.NewStyles("") - v := newStatusView(defaults) - v.update([]string{"blee", "duh"}) - - assert.Equal(t, "[black:aqua:b] blee [black:orange:b] duh \n", v.GetText(false)) -} diff --git a/internal/views/sts.go b/internal/views/sts.go deleted file mode 100644 index 42ac4952..00000000 --- a/internal/views/sts.go +++ /dev/null @@ -1,58 +0,0 @@ -package views - -import ( - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/rs/zerolog/log" - v1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -type statefulSetView struct { - *logResourceView - scalableResourceView *scalableResourceView - restartableResourceView *restartableResourceView -} - -func newStatefulSetView(title, gvr string, app *appView, list resource.List) resourceViewer { - logResourceView := newLogResourceView(title, gvr, app, list) - v := statefulSetView{ - logResourceView: logResourceView, - scalableResourceView: newScalableResourceViewForParent(logResourceView.resourceView), - restartableResourceView: newRestartableResourceViewForParent(logResourceView.resourceView), - } - v.extraActionsFn = v.extraActions - v.enterFn = v.showPods - - return &v -} - -func (v *statefulSetView) extraActions(aa ui.KeyActions) { - v.logResourceView.extraActions(aa) - v.scalableResourceView.extraActions(aa) - v.restartableResourceView.extraActions(aa) - aa[ui.KeyShiftD] = ui.NewKeyAction("Sort Desired", v.sortColCmd(1, false), false) - aa[ui.KeyShiftC] = ui.NewKeyAction("Sort Current", v.sortColCmd(2, false), false) -} - -func (v *statefulSetView) showPods(app *appView, ns, res, sel string) { - ns, n := namespaced(sel) - s := k8s.NewStatefulSet(app.Conn()) - st, err := s.Get(ns, n) - if err != nil { - log.Error().Err(err).Msgf("Fetching StatefulSet %s", sel) - app.Flash().Errf("Unable to fetch statefulset %s", err) - return - } - - sts := st.(*v1.StatefulSet) - l, err := metav1.LabelSelectorAsSelector(sts.Spec.Selector) - if err != nil { - log.Error().Err(err).Msgf("Converting selector for StatefulSet %s", sel) - app.Flash().Errf("Selector failed %s", err) - return - } - - showPods(app, ns, l.String(), "", v.backCmd) -} diff --git a/internal/views/styles.go b/internal/views/styles.go deleted file mode 100644 index 596472d9..00000000 --- a/internal/views/styles.go +++ /dev/null @@ -1,46 +0,0 @@ -package views - -import ( - "github.com/derailed/k9s/internal/ui" - "github.com/derailed/tview" - "github.com/gdamore/tcell" -) - -type styles struct { - color tcell.Color - attrs tcell.AttrMask - align int -} - -func stylesFor(app *appView, res string, col int) styles { - switch res { - case "pod": - return podStyles(app, col) - default: - return defaultStyles(app, col) - } -} - -func podStyles(app *appView, col int) styles { - st := styles{ - color: ui.StdColor, - attrs: tcell.AttrReverse, - align: tview.AlignLeft, - } - - switch col { - case 5, 6, 7, 8: - st.align = tview.AlignLeft - st.color = tcell.ColorGreen - } - - return st -} - -func defaultStyles(app *appView, col int) styles { - return styles{ - color: tcell.ColorRed, - attrs: tcell.AttrReverse, - align: tview.AlignLeft, - } -} diff --git a/internal/views/subject.go b/internal/views/subject.go deleted file mode 100644 index d5f32ec2..00000000 --- a/internal/views/subject.go +++ /dev/null @@ -1,309 +0,0 @@ -package views - -import ( - "context" - "fmt" - "reflect" - "time" - - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/gdamore/tcell" - "github.com/rs/zerolog/log" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/watch" -) - -var subjectHeader = resource.Row{"NAME", "KIND", "FIRST LOCATION"} - -type ( - cachedEventer interface { - header() resource.Row - getCache() resource.RowEvents - setCache(resource.RowEvents) - } - - subjectView struct { - *tableView - - current ui.Igniter - cancel context.CancelFunc - subjectKind string - cache resource.RowEvents - } -) - -func newSubjectView(title, gvr string, app *appView, list resource.List) resourceViewer { - v := subjectView{} - v.tableView = newTableView(app, "Subject") - v.SetActiveNS("*") - v.SetColorerFn(rbacColorer) - v.bindKeys() - - if current, ok := app.Frame().GetPrimitive("main").(ui.Igniter); ok { - v.current = current - } else { - v.current = &v - } - - return &v -} - -// Init the view. -func (v *subjectView) Init(c context.Context, _ string) { - if v.cancel != nil { - v.cancel() - } - - v.SetSortCol(1, len(rbacHeader), true) - v.subjectKind = mapCmdSubject(v.app.Config.K9s.ActiveCluster().View.Active) - v.SetBaseTitle(v.subjectKind) - - ctx, cancel := context.WithCancel(c) - v.cancel = cancel - go func(ctx context.Context) { - for { - select { - case <-ctx.Done(): - log.Debug().Msgf("Subject:%s Watch bailing out!", v.subjectKind) - return - case <-time.After(time.Duration(v.app.Config.K9s.GetRefreshRate()) * time.Second): - v.refresh() - v.app.Draw() - } - } - }(ctx) - - v.refresh() - v.SelectRow(1, true) - v.app.SetFocus(v) - v.app.SetHints(v.Hints()) - -} - -func (v *subjectView) masterPage() *tableView { - return v.tableView -} - -func (v *subjectView) bindKeys() { - // No time data or ns - v.RmAction(ui.KeyShiftA) - v.RmAction(ui.KeyShiftP) - - v.SetActions(ui.KeyActions{ - tcell.KeyEnter: ui.NewKeyAction("Policies", v.policyCmd, true), - tcell.KeyEscape: ui.NewKeyAction("Reset", v.resetCmd, false), - ui.KeySlash: ui.NewKeyAction("Filter", v.activateCmd, false), - ui.KeyP: ui.NewKeyAction("Previous", v.app.prevCmd, false), - ui.KeyShiftK: ui.NewKeyAction("Sort Kind", v.SortColCmd(1), false), - }) -} - -func (v *subjectView) setExtraActionsFn(f ui.ActionsFunc) {} -func (v *subjectView) setColorerFn(f ui.ColorerFunc) {} -func (v *subjectView) setEnterFn(f enterFn) {} -func (v *subjectView) setDecorateFn(f decorateFn) {} - -func (v *subjectView) getTitle() string { - return fmt.Sprintf(rbacTitleFmt, "Subject", v.subjectKind) -} - -func (v *subjectView) SetSubject(s string) { - v.subjectKind = mapSubject(s) -} - -func (v *subjectView) refresh() { - data, err := v.reconcile() - if err != nil { - log.Error().Err(err).Msgf("Refresh for %s", v.subjectKind) - v.app.Flash().Err(err) - } - v.Update(data) -} - -func (v *subjectView) policyCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.RowSelected() { - return evt - } - - if v.cancel != nil { - v.cancel() - } - - _, n := namespaced(v.GetSelectedItem()) - v.app.inject(newPolicyView(v.app, mapFuSubject(v.subjectKind), n)) - - return nil -} - -func (v *subjectView) resetCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.SearchBuff().Empty() { - v.SearchBuff().Reset() - return nil - } - - return v.backCmd(evt) -} - -func (v *subjectView) backCmd(evt *tcell.EventKey) *tcell.EventKey { - if v.cancel != nil { - v.cancel() - } - - if v.SearchBuff().IsActive() { - v.SearchBuff().Reset() - return nil - } - - v.app.inject(v.current) - - return nil -} - -func (v *subjectView) reconcile() (resource.TableData, error) { - var table resource.TableData - - evts, err := v.clusterSubjects() - if err != nil { - return table, err - } - - nevts, err := v.namespacedSubjects() - if err != nil { - return table, err - } - for k, v := range nevts { - evts[k] = v - } - - return buildTable(v, evts), nil -} - -func (v *subjectView) header() resource.Row { - return subjectHeader -} - -func (v *subjectView) getCache() resource.RowEvents { - return v.cache -} - -func (v *subjectView) setCache(evts resource.RowEvents) { - v.cache = evts -} - -func buildTable(c cachedEventer, evts resource.RowEvents) resource.TableData { - table := resource.TableData{ - Header: c.header(), - Rows: make(resource.RowEvents, len(evts)), - Namespace: "*", - } - - noDeltas := make(resource.Row, len(c.header())) - cache := c.getCache() - if len(cache) == 0 { - for k, ev := range evts { - ev.Action = resource.New - ev.Deltas = noDeltas - table.Rows[k] = ev - } - c.setCache(evts) - return table - } - - for k, ev := range evts { - table.Rows[k] = ev - - newr := ev.Fields - if _, ok := cache[k]; !ok { - ev.Action, ev.Deltas = watch.Added, noDeltas - continue - } - oldr := cache[k].Fields - deltas := make(resource.Row, len(newr)) - if !reflect.DeepEqual(oldr, newr) { - ev.Action = watch.Modified - for i, field := range oldr { - if field != newr[i] { - deltas[i] = field - } - } - ev.Deltas = deltas - } else { - ev.Action = resource.Unchanged - ev.Deltas = noDeltas - } - } - - for k := range evts { - if _, ok := table.Rows[k]; !ok { - delete(evts, k) - } - } - c.setCache(evts) - - return table -} - -func (v *subjectView) clusterSubjects() (resource.RowEvents, error) { - crbs, err := v.app.Conn().DialOrDie().RbacV1().ClusterRoleBindings().List(metav1.ListOptions{}) - if err != nil { - return nil, err - } - - evts := make(resource.RowEvents, len(crbs.Items)) - for _, crb := range crbs.Items { - for _, s := range crb.Subjects { - if s.Kind != v.subjectKind { - continue - } - evts[s.Name] = &resource.RowEvent{ - Fields: resource.Row{s.Name, "ClusterRoleBinding", crb.Name}, - } - } - } - - return evts, nil -} - -func (v *subjectView) namespacedSubjects() (resource.RowEvents, error) { - rbs, err := v.app.Conn().DialOrDie().RbacV1().RoleBindings("").List(metav1.ListOptions{}) - if err != nil { - return nil, err - } - - evts := make(resource.RowEvents, len(rbs.Items)) - for _, rb := range rbs.Items { - for _, s := range rb.Subjects { - if s.Kind == v.subjectKind { - evts[s.Name] = &resource.RowEvent{ - Fields: resource.Row{s.Name, "RoleBinding", rb.Name}, - } - } - } - } - - return evts, nil -} - -func mapCmdSubject(subject string) string { - log.Debug().Msgf("!!!!!!Subject %q", subject) - switch subject { - case "groups": - return "Group" - case "sas": - return "ServiceAccount" - default: - return "User" - } -} - -func mapFuSubject(subject string) string { - switch subject { - case "Group": - return "g" - case "ServiceAccount": - return "s" - default: - return "u" - } -} diff --git a/internal/views/svc.go b/internal/views/svc.go deleted file mode 100644 index 7b548c30..00000000 --- a/internal/views/svc.go +++ /dev/null @@ -1,218 +0,0 @@ -package views - -import ( - "errors" - "fmt" - "strings" - "time" - - "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/perf" - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" - "github.com/gdamore/tcell" - "github.com/rs/zerolog/log" - v1 "k8s.io/api/core/v1" -) - -type svcView struct { - *resourceView - - bench *perf.Benchmark -} - -func newSvcView(title, gvr string, app *appView, list resource.List) resourceViewer { - v := svcView{ - resourceView: newResourceView(title, gvr, app, list).(*resourceView), - } - v.extraActionsFn = v.extraActions - v.enterFn = v.showPods - v.AddPage("logs", newLogsView(list.GetName(), app, &v), true, false) - - return &v -} - -// Protocol... - -func (v *svcView) getList() resource.List { - return v.list -} - -func (v *svcView) getSelection() string { - return v.masterPage().GetSelectedItem() -} - -func (v *svcView) extraActions(aa ui.KeyActions) { - aa[ui.KeyL] = ui.NewKeyAction("Logs", v.logsCmd, true) - aa[tcell.KeyCtrlB] = ui.NewKeyAction("Bench", v.benchCmd, true) - aa[tcell.KeyCtrlK] = ui.NewKeyAction("Bench Stop", v.benchStopCmd, true) - aa[ui.KeyShiftT] = ui.NewKeyAction("Sort Type", v.sortColCmd(1, false), false) -} - -func (v *svcView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { - return func(evt *tcell.EventKey) *tcell.EventKey { - t := v.masterPage() - t.SetSortCol(t.NameColIndex()+col, 0, asc) - t.Refresh() - - return nil - } -} - -func (v *svcView) showPods(app *appView, ns, res, sel string) { - s := k8s.NewService(app.Conn()) - ns, n := namespaced(sel) - svc, err := s.Get(ns, n) - if err != nil { - app.Flash().Err(err) - return - } - - if s, ok := svc.(*v1.Service); ok { - v.showSvcPods(ns, s.Spec.Selector, v.backCmd) - } -} - -func (v *svcView) logsCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.masterPage().RowSelected() { - return evt - } - - l := v.GetPrimitive("logs").(*logsView) - l.reload("", v, false) - v.switchPage("logs") - - return nil -} - -func (v *svcView) backCmd(evt *tcell.EventKey) *tcell.EventKey { - // Reset namespace to what it was - v.app.Config.SetActiveNamespace(v.list.GetNamespace()) - v.app.inject(v) - - return nil -} - -func (v *svcView) benchStopCmd(evt *tcell.EventKey) *tcell.EventKey { - if v.bench != nil { - log.Debug().Msg(">>> Benchmark canceled!!") - v.app.status(ui.FlashErr, "Benchmark Camceled!") - v.bench.Cancel() - } - v.app.StatusReset() - - return nil -} - -func (v *svcView) checkSvc(row int) error { - svcType := trimCellRelative(v.masterPage(), row, 1) - if svcType != "NodePort" && svcType != "LoadBalancer" { - return errors.New("You must select a reachable service") - } - return nil -} - -func (v *svcView) getExternalPort(row int) (string, error) { - ports := trimCellRelative(v.masterPage(), row, 5) - - pp := strings.Split(ports, " ") - if len(pp) == 0 { - return "", errors.New("No ports found") - } - - // Grap the first port pair for now... - tokens := strings.Split(pp[0], "►") - if len(tokens) < 2 { - return "", errors.New("No ports pair found") - } - - return tokens[1], nil -} - -func (v *svcView) reloadBenchCfg() error { - // BOZO!! Poorman Reload bench to make sure we pick up updates if any. - path := ui.BenchConfig(v.app.Config.K9s.CurrentCluster) - return v.app.Bench.Reload(path) -} - -func (v *svcView) benchCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.masterPage().RowSelected() || v.bench != nil { - return evt - } - - if err := v.reloadBenchCfg(); err != nil { - v.app.Flash().Err(err) - return nil - } - - sel := v.getSelection() - cfg, ok := v.app.Bench.Benchmarks.Services[sel] - if !ok { - v.app.Flash().Errf("No bench config found for service %s", sel) - return nil - } - cfg.Name = sel - log.Debug().Msgf("Benchmark config %#v", cfg) - - row, _ := v.masterPage().GetSelection() - if err := v.checkSvc(row); err != nil { - v.app.Flash().Err(err) - return nil - } - port, err := v.getExternalPort(row) - if err != nil { - v.app.Flash().Err(err) - return nil - } - if err := v.runBenchmark(port, cfg); err != nil { - v.app.Flash().Errf("Benchmark failed %v", err) - v.app.StatusReset() - v.bench = nil - } - - return nil -} - -func (v *svcView) runBenchmark(port string, cfg config.BenchConfig) error { - var err error - base := "http://" + cfg.HTTP.Host + ":" + port + cfg.HTTP.Path - if v.bench, err = perf.NewBenchmark(base, cfg); err != nil { - return err - } - - v.app.status(ui.FlashWarn, "Benchmark in progress...") - log.Debug().Msg("Bench starting...") - go v.bench.Run(v.app.Config.K9s.CurrentCluster, v.benchDone) - - return nil -} - -func (v *svcView) benchDone() { - log.Debug().Msg("Bench Completed!") - v.app.QueueUpdate(func() { - if v.bench.Canceled() { - v.app.status(ui.FlashInfo, "Benchmark canceled") - } else { - v.app.status(ui.FlashInfo, "Benchmark Completed!") - v.bench.Cancel() - } - v.bench = nil - go benchTimedOut(v.app) - }) -} - -func benchTimedOut(app *appView) { - <-time.After(2 * time.Second) - app.QueueUpdate(func() { - app.StatusReset() - }) -} - -func (v *svcView) showSvcPods(ns string, sel map[string]string, a ui.ActionHandler) { - var s []string - for k, v := range sel { - s = append(s, fmt.Sprintf("%s=%s", k, v)) - } - showPods(v.app, ns, strings.Join(s, ","), "", a) -} diff --git a/internal/views/table.go b/internal/views/table.go deleted file mode 100644 index 01c82857..00000000 --- a/internal/views/table.go +++ /dev/null @@ -1,115 +0,0 @@ -package views - -import ( - "github.com/derailed/k9s/internal/ui" - "github.com/gdamore/tcell" -) - -type tableView struct { - *ui.Table - - app *appView - filterFn func(string) -} - -func newTableView(app *appView, title string) *tableView { - v := tableView{ - Table: ui.NewTable(title, app.Styles), - app: app, - } - v.SearchBuff().AddListener(app.Cmd()) - v.SearchBuff().AddListener(&v) - v.bindKeys() - - return &v -} - -// BufferChanged indicates the buffer was changed. -func (v *tableView) BufferChanged(s string) {} - -// BufferActive indicates the buff activity changed. -func (v *tableView) BufferActive(state bool, k ui.BufferKind) { - v.app.BufferActive(state, k) -} - -func (v *tableView) saveCmd(evt *tcell.EventKey) *tcell.EventKey { - if path, err := saveTable(v.app.Config.K9s.CurrentCluster, v.GetBaseTitle(), v.GetFilteredData()); err != nil { - v.app.Flash().Err(err) - } else { - v.app.Flash().Infof("File %s saved successfully!", path) - } - - return nil -} - -func (v *tableView) setFilterFn(fn func(string)) { - v.filterFn = fn - - cmd := v.SearchBuff().String() - if isLabelSelector(cmd) && v.filterFn != nil { - v.filterFn(trimLabelSelector(cmd)) - } -} - -func (v *tableView) bindKeys() { - v.SetActions(ui.KeyActions{ - tcell.KeyCtrlS: ui.NewKeyAction("Save", v.saveCmd, true), - ui.KeySlash: ui.NewKeyAction("Filter Mode", v.activateCmd, false), - tcell.KeyEscape: ui.NewKeyAction("Filter Reset", v.resetCmd, false), - tcell.KeyEnter: ui.NewKeyAction("Filter", v.filterCmd, false), - tcell.KeyBackspace2: ui.NewKeyAction("Erase", v.eraseCmd, false), - tcell.KeyBackspace: ui.NewKeyAction("Erase", v.eraseCmd, false), - tcell.KeyDelete: ui.NewKeyAction("Erase", v.eraseCmd, false), - ui.KeyShiftI: ui.NewKeyAction("Invert", v.SortInvertCmd, false), - ui.KeyShiftN: ui.NewKeyAction("Sort Name", v.SortColCmd(0), false), - ui.KeyShiftA: ui.NewKeyAction("Sort Age", v.SortColCmd(-1), false), - }) -} - -func (v *tableView) filterCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.SearchBuff().IsActive() { - return evt - } - - v.SearchBuff().SetActive(false) - cmd := v.SearchBuff().String() - if isLabelSelector(cmd) && v.filterFn != nil { - v.filterFn(trimLabelSelector(cmd)) - return nil - } - v.Refresh() - - return nil -} - -func (v *tableView) eraseCmd(evt *tcell.EventKey) *tcell.EventKey { - if v.SearchBuff().IsActive() { - v.SearchBuff().Delete() - } - - return nil -} - -func (v *tableView) resetCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.SearchBuff().Empty() { - v.app.Flash().Info("Clearing filter...") - } - if isLabelSelector(v.SearchBuff().String()) { - v.filterFn("") - } - v.SearchBuff().Reset() - v.Refresh() - - return nil -} - -func (v *tableView) activateCmd(evt *tcell.EventKey) *tcell.EventKey { - if v.app.InCmdMode() { - return evt - } - - v.app.Flash().Info("Filter mode activated.") - v.SearchBuff().SetActive(true) - - return nil -} diff --git a/internal/views/table_helper.go b/internal/views/table_helper.go deleted file mode 100644 index c44e91e9..00000000 --- a/internal/views/table_helper.go +++ /dev/null @@ -1,159 +0,0 @@ -package views - -import ( - "encoding/csv" - "fmt" - "os" - "path/filepath" - "regexp" - "strings" - "time" - - "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/resource" - "github.com/derailed/k9s/internal/ui" -) - -const ( - titleFmt = "[fg:bg:b] %s[fg:bg:-][[count:bg:b]%d[fg:bg:-]] " - searchFmt = "<[filter:bg:r]/%s[fg:bg:-]> " - nsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-][[count:bg:b]%d[fg:bg:-]][fg:bg:-] " - labelSelIndicator = "-l" - descIndicator = "↓" - ascIndicator = "↑" - fullFmat = "%s-%s-%d.csv" - noNSFmat = "%s-%d.csv" -) - -var ( - cpuRX = regexp.MustCompile(`\A.{0,1}CPU`) - memRX = regexp.MustCompile(`\A.{0,1}MEM`) - labelCmd = regexp.MustCompile(`\A\-l`) -) - -type cleanseFn func(string) string - -func trimCellRelative(tv *tableView, row, col int) string { - return ui.TrimCell(tv.Table, row, tv.NameColIndex()+col) -} - -// func trimCell(tv *ui.Table, row, col int) string { -// c := tv.GetCell(row, col) -// if c == nil { -// log.Error().Err(fmt.Errorf("No cell at location [%d:%d]", row, col)).Msg("Trim cell failed!") -// return "" -// } -// return strings.TrimSpace(c.Text) -// } - -func saveTable(cluster, name string, data resource.TableData) (string, error) { - dir := filepath.Join(config.K9sDumpDir, cluster) - if err := ensureDir(dir); err != nil { - return "", err - } - - ns, now := data.Namespace, time.Now().UnixNano() - if ns == resource.AllNamespaces { - ns = resource.AllNamespace - } - fName := fmt.Sprintf(fullFmat, name, ns, now) - if ns == resource.NotNamespaced { - fName = fmt.Sprintf(noNSFmat, name, now) - } - - path := filepath.Join(dir, fName) - mod := os.O_CREATE | os.O_WRONLY - file, err := os.OpenFile(path, mod, 0644) - defer func() { - if file != nil { - file.Close() - } - }() - if err != nil { - return "", err - } - - w := csv.NewWriter(file) - w.Write(data.Header) - for _, r := range data.Rows { - w.Write(r.Fields) - } - w.Flush() - if err := w.Error(); err != nil { - return "", err - } - - return path, nil -} - -func isLabelSelector(s string) bool { - if s == "" { - return false - } - return labelCmd.MatchString(s) -} - -func trimLabelSelector(s string) string { - return strings.TrimSpace(s[2:]) -} - -func skinTitle(fmat string, style config.Frame) string { - fmat = strings.Replace(fmat, "[fg:bg", "["+style.Title.FgColor+":"+style.Title.BgColor, -1) - fmat = strings.Replace(fmat, "[hilite", "["+style.Title.HighlightColor, 1) - fmat = strings.Replace(fmat, "[key", "["+style.Menu.NumKeyColor, 1) - fmat = strings.Replace(fmat, "[filter", "["+style.Title.FilterColor, 1) - fmat = strings.Replace(fmat, "[count", "["+style.Title.CounterColor, 1) - fmat = strings.Replace(fmat, ":bg:", ":"+style.Title.BgColor+":", -1) - - return fmat -} - -func sortRows(evts resource.RowEvents, sortFn ui.SortFn, sortCol ui.SortColumn, keys []string) { - rows := make(resource.Rows, 0, len(evts)) - for k, r := range evts { - rows = append(rows, append(r.Fields, k)) - } - sortFn(rows, sortCol) - - for i, r := range rows { - keys[i] = r[len(r)-1] - } -} - -// func defaultSort(rows resource.Rows, sortCol ui.SortColumn) { -// t := rowSorter{rows: rows, index: sortCol.index, asc: sortCol.asc} -// sort.Sort(t) -// } - -// func sortAllRows(col ui.SortColumn, rows resource.RowEvents, sortFn ui.SortFn) (resource.Row, map[string]resource.Row) { -// keys := make([]string, len(rows)) -// sortRows(rows, sortFn, col, keys) - -// sec := make(map[string]resource.Row, len(rows)) -// for _, k := range keys { -// grp := rows[k].Fields[col.index] -// sec[grp] = append(sec[grp], k) -// } - -// // Performs secondary to sort by name for each groups. -// prim := make(resource.Row, 0, len(sec)) -// for k, v := range sec { -// sort.Strings(v) -// prim = append(prim, k) -// } -// sort.Sort(groupSorter{prim, col.asc}) - -// return prim, sec -// } - -// func sortIndicator(col ui.SortColumn, style config.Table, index int, name string) string { -// if col.index != index { -// return name -// } - -// order := descIndicator -// if col.asc { -// order = ascIndicator -// } -// return fmt.Sprintf("%s[%s::]%s[::]", name, style.Header.SorterColor, order) -// } diff --git a/internal/views/table_test.go b/internal/views/table_test.go deleted file mode 100644 index d9bddf72..00000000 --- a/internal/views/table_test.go +++ /dev/null @@ -1,169 +0,0 @@ -package views - -import ( - "fmt" - "io/ioutil" - "path/filepath" - "strings" - "testing" - - "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/resource" - "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/watch" -) - -func TestTableViewSave(t *testing.T) { - v := newTableView(NewApp(config.NewConfig(ks{})), "test") - v.SetTitle("k9s-test") - dir := filepath.Join(config.K9sDumpDir, v.app.Config.K9s.CurrentCluster) - c1, _ := ioutil.ReadDir(dir) - v.saveCmd(nil) - c2, _ := ioutil.ReadDir(dir) - assert.Equal(t, len(c2), len(c1)+1) -} - -func TestTableViewNew(t *testing.T) { - v := newTableView(NewApp(config.NewConfig(ks{})), "test") - - data := resource.TableData{ - Header: resource.Row{"NAMESPACE", "NAME", "FRED", "AGE"}, - Rows: resource.RowEvents{ - "ns1/a": &resource.RowEvent{ - Action: watch.Added, - Fields: resource.Row{"ns1", "a", "10", "3m"}, - Deltas: resource.Row{"", "", "", ""}, - }, - "ns1/b": &resource.RowEvent{ - Action: watch.Added, - Fields: resource.Row{"ns1", "b", "15", "1m"}, - Deltas: resource.Row{"", "", "20", ""}, - }, - }, - NumCols: map[string]bool{ - "FRED": true, - }, - Namespace: "", - } - v.Update(data) - assert.Equal(t, 3, v.GetRowCount()) -} - -func TestTableViewFilter(t *testing.T) { - v := newTableView(NewApp(config.NewConfig(ks{})), "test") - - data := resource.TableData{ - Header: resource.Row{"NAMESPACE", "NAME", "FRED", "AGE"}, - Rows: resource.RowEvents{ - "ns1/blee": &resource.RowEvent{ - Action: watch.Added, - Fields: resource.Row{"ns1", "blee", "10", "3m"}, - Deltas: resource.Row{"", "", "", ""}, - }, - "ns1/fred": &resource.RowEvent{ - Action: watch.Added, - Fields: resource.Row{"ns1", "fred", "15", "1m"}, - Deltas: resource.Row{"", "", "20", ""}, - }, - }, - NumCols: map[string]bool{ - "FRED": true, - }, - Namespace: "", - } - v.Update(data) - v.SearchBuff().SetActive(true) - v.SearchBuff().Set("blee") - v.filterCmd(nil) - assert.Equal(t, 2, v.GetRowCount()) - v.resetCmd(nil) - assert.Equal(t, 3, v.GetRowCount()) -} - -func TestTableViewSort(t *testing.T) { - v := newTableView(NewApp(config.NewConfig(ks{})), "test") - - data := resource.TableData{ - Header: resource.Row{"NAMESPACE", "NAME", "FRED", "AGE"}, - Rows: resource.RowEvents{ - "ns1/blee": &resource.RowEvent{ - Action: watch.Added, - Fields: resource.Row{"ns1", "blee", "10", "3m"}, - Deltas: resource.Row{"", "", "", ""}, - }, - "ns1/fred": &resource.RowEvent{ - Action: watch.Added, - Fields: resource.Row{"ns1", "fred", "15", "1m"}, - Deltas: resource.Row{"", "", "20", ""}, - }, - }, - NumCols: map[string]bool{ - "FRED": true, - }, - Namespace: "", - } - v.Update(data) - v.SortColCmd(1)(nil) - assert.Equal(t, 3, v.GetRowCount()) - assert.Equal(t, "blee ", v.GetCell(1, 1).Text) - - v.SortInvertCmd(nil) - assert.Equal(t, 3, v.GetRowCount()) - assert.Equal(t, "fred ", v.GetCell(1, 1).Text) -} - -func TestIsSelector(t *testing.T) { - uu := map[string]struct { - sel string - e 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}, - } - - for k, u := range uu { - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, isLabelSelector(u.sel)) - }) - } -} - -func TestTrimLabelSelector(t *testing.T) { - uu := map[string]struct { - sel, e string - }{ - "cool": {"-l app=fred,env=blee", "app=fred,env=blee"}, - "noSpace": {"-lapp=fred,env=blee", "app=fred,env=blee"}, - } - - for k, u := range uu { - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, trimLabelSelector(u.sel)) - }) - } -} - -func BenchmarkTitleReplace(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - fmat := strings.Replace(nsTitleFmt, "[fg", "["+"red", -1) - fmat = strings.Replace(fmat, ":bg:", ":"+"blue"+":", -1) - fmat = strings.Replace(fmat, "[hilite", "["+"green", 1) - fmat = strings.Replace(fmat, "[count", "["+"yellow", 1) - _ = fmt.Sprintf(fmat, "Pods", "default", 10) - } -} - -func BenchmarkTitleReplace1(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - fmat := strings.Replace(nsTitleFmt, "fg:bg", "red"+":"+"blue", -1) - fmat = strings.Replace(fmat, "[hilite", "["+"green", 1) - fmat = strings.Replace(fmat, "[count", "["+"yellow", 1) - _ = fmt.Sprintf(fmat, "Pods", "default", 10) - } -} diff --git a/internal/watch/container.go b/internal/watch/container.go deleted file mode 100644 index 1781ec35..00000000 --- a/internal/watch/container.go +++ /dev/null @@ -1,81 +0,0 @@ -package watch - -import ( - "fmt" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -const ( - // ContainerIndex marker for stored containers. - ContainerIndex string = "co" - containerCols = 12 -) - -// Container tracks container activities. -type Container struct { - StoreInformer -} - -// NewContainer returns a new container. -func NewContainer(po StoreInformer) *Container { - return &Container{StoreInformer: po} -} - -// StartWatching registers container event listener. -func (c *Container) StartWatching(stopCh <-chan struct{}) {} - -// Run starts out the informer loop. -func (c *Container) Run(closeCh <-chan struct{}) {} - -// Get retrieves a given container from store. -func (c *Container) Get(fqn string, opts metav1.GetOptions) (interface{}, error) { - o, ok, err := c.GetStore().GetByKey(fqn) - if err != nil { - return nil, err - } - if !ok { - return nil, fmt.Errorf("Pod %s not found", fqn) - } - po := o.(*v1.Pod) - cc := make(k8s.Collection, len(po.Spec.InitContainers)+len(po.Spec.Containers)) - toContainers(po, cc) - - return cc, nil -} - -// List retrieves alist of containers for a given po from store. -func (c *Container) List(fqn string, opts metav1.ListOptions) k8s.Collection { - o, ok, err := c.GetStore().GetByKey(fqn) - if err != nil { - log.Error().Err(err).Msg("Pod") - return nil - } - if !ok { - log.Error().Err(fmt.Errorf("Pod %s not found", fqn)).Msg("Pod") - return nil - } - po := o.(*v1.Pod) - cc := make(k8s.Collection, len(po.Spec.InitContainers)+len(po.Spec.Containers)) - toContainers(po, cc) - - return cc -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func toContainers(po *v1.Pod, c k8s.Collection) { - var index int - for _, co := range po.Spec.InitContainers { - c[index] = co - index++ - } - for _, co := range po.Spec.Containers { - c[index] = co - index++ - } -} diff --git a/internal/watch/container_test.go b/internal/watch/container_test.go deleted file mode 100644 index ce5180c6..00000000 --- a/internal/watch/container_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package watch - -import ( - "testing" - - "github.com/derailed/k9s/internal/k8s" - "gotest.tools/assert" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - // "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" -) - -func TestContainerGet(t *testing.T) { - cmo := NewMockConnection() - c := NewContainer(NewPod(cmo, "")) - o, err := c.Get("fred", metav1.GetOptions{}) - - assert.ErrorContains(t, err, "not found") - assert.Assert(t, o == nil) -} - -func TestContainerList(t *testing.T) { - cmo := NewMockConnection() - c := NewContainer(NewPod(cmo, "")) - o := c.List("fred", metav1.ListOptions{}) - - assert.Assert(t, o == nil) -} - -func TestToContainer(t *testing.T) { - c := make(k8s.Collection, 2) - toContainers(makeCoPod("p1"), c) - - assert.Equal(t, 2, len(c)) -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func makePod(n string) *v1.Pod { - po := &v1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: n, - Namespace: "default", - }, - } - po.Status.Phase = v1.PodRunning - - return po -} - -func makeCoPod(n string) *v1.Pod { - po := makePod(n) - po.Spec = v1.PodSpec{ - InitContainers: []v1.Container{ - makeContainer("i1", "fred:0.0.1"), - }, - Containers: []v1.Container{ - makeContainer("c1", "blee:0.1.0"), - }, - } - - return po -} - -func makeContainer(n, img string) v1.Container { - co := v1.Container{ - Name: n, - Image: img, - } - - return co -} diff --git a/internal/watch/factory.go b/internal/watch/factory.go new file mode 100644 index 00000000..08538969 --- /dev/null +++ b/internal/watch/factory.go @@ -0,0 +1,252 @@ +package watch + +import ( + "fmt" + "path" + "strings" + "time" + + "github.com/derailed/k9s/internal/client" + "github.com/rs/zerolog/log" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + di "k8s.io/client-go/dynamic/dynamicinformer" + "k8s.io/client-go/informers" +) + +const ( + defaultResync = 10 * time.Minute + allNamespaces = "" + clusterScope = "-" +) + +// Factory tracks various resource informers. +type Factory struct { + factories map[string]di.DynamicSharedInformerFactory + client client.Connection + stopChan chan struct{} + activeNS string + forwarders Forwarders +} + +// NewFactory returns a new informers factory. +func NewFactory(client client.Connection) *Factory { + return &Factory{ + client: client, + stopChan: make(chan struct{}), + factories: make(map[string]di.DynamicSharedInformerFactory), + forwarders: NewForwarders(), + } +} + +func (f *Factory) String() string { + return fmt.Sprintf("Factory ActiveNS %s", f.activeNS) +} + +// List returns a resource collection. +func (f *Factory) List(gvr, ns string, sel labels.Selector) ([]runtime.Object, error) { + inf, err := f.CanForResource(ns, gvr, "list") + if err != nil { + return nil, err + } + if ns == clusterScope { + return inf.Lister().List(sel) + } + + return inf.Lister().ByNamespace(ns).List(sel) +} + +// Get retrieves a given resource. +func (f *Factory) Get(gvr, path string, sel labels.Selector) (runtime.Object, error) { + ns, n := namespaced(path) + inf, err := f.CanForResource(ns, gvr, "get") + if err != nil { + return nil, err + } + if ns == clusterScope { + return inf.Lister().Get(n) + } + + return inf.Lister().ByNamespace(ns).Get(n) +} + +// WaitForCachesync waits for all factories to update their cache. +func (f *Factory) WaitForCacheSync() { + for _, fac := range f.factories { + fac.WaitForCacheSync(f.stopChan) + } +} + +// Init starts a factory. +func (f *Factory) Init() { + f.Start(f.stopChan) +} + +// Terminate terminates all watchers and forwards. +func (f *Factory) Terminate() { + if f.stopChan != nil { + close(f.stopChan) + f.stopChan = nil + } + for k := range f.factories { + delete(f.factories, k) + } + f.forwarders.DeleteAll() +} + +// RegisterForwarder registers a new portforward for a given container. +func (f *Factory) AddForwarder(pf Forwarder) { + f.forwarders[pf.Path()] = pf + f.forwarders.Dump() +} + +// DeleteForwarder deletes portforward for a given container. +func (f *Factory) DeleteForwarder(path string) { + f.forwarders.Dump() + fwd, ok := f.forwarders[path] + if !ok { + log.Warn().Msgf("Unable to delete portForward %q", path) + return + } + fwd.Stop() + delete(f.forwarders, path) + f.forwarders.Dump() +} + +// Forwards returns all portforwards. +func (f *Factory) Forwarders() Forwarders { + return f.forwarders +} + +// ForwarderFor returns a portforward for a given container or nil if none exists. +func (f *Factory) ForwarderFor(path string) (Forwarder, bool) { + fwd, ok := f.forwarders[path] + return fwd, ok +} + +// Start initializes the informers until caller cancels the context. +func (f *Factory) Start(stopChan chan struct{}) { + for ns, fac := range f.factories { + log.Debug().Msgf("Starting factory in ns %q", ns) + fac.Start(stopChan) + } +} + +// BOZO!! Check ns access for resource?? +func (f *Factory) SetActive(ns string) { + if !f.isClusterWide() { + f.ensureFactory(ns) + } + f.activeNS = ns +} + +func (f *Factory) isClusterWide() bool { + _, ok := f.factories[allNamespaces] + return ok +} + +func (f *Factory) preload(ns string) { + // verbs := []string{"get", "list", "watch"} + // _, _ = f.CanForResource(ns, "v1/pods", verbs...) + // _, _ = f.CanForResource(allNamespaces, "apiextensions.k8s.io/v1beta1/customresourcedefinitions", verbs...) + // _, _ = f.CanForResource(clusterScope, "rbac.authorization.k8s.io/v1/clusterroles", verbs...) + // _, _ = f.CanForResource(allNamespaces, "rbac.authorization.k8s.io/v1/roles", verbs...) +} + +// CanForResource return an informer is user has access. +func (f *Factory) CanForResource(ns, gvr string, verbs ...string) (informers.GenericInformer, error) { + auth, err := f.Client().CanI(ns, gvr, verbs) + if err != nil { + return nil, err + } + if !auth { + return nil, fmt.Errorf("%v access denied on resource %q:%q", verbs, ns, gvr) + } + + return f.ForResource(ns, gvr), nil +} + +// FactoryFor returns a factory for a given namespace. +func (f *Factory) FactoryFor(ns string) di.DynamicSharedInformerFactory { + return f.factories[ns] +} + +// ForResource returns an informer for a given resource. +func (f *Factory) ForResource(ns, gvr string) informers.GenericInformer { + fact := f.ensureFactory(ns) + inf := fact.ForResource(toGVR(gvr)) + fact.Start(f.stopChan) + + return inf +} + +func (f *Factory) ensureFactory(ns string) di.DynamicSharedInformerFactory { + if f.isClusterWide() { + ns = allNamespaces + } + if fac, ok := f.factories[ns]; ok { + return fac + } + + f.factories[ns] = di.NewFilteredDynamicSharedInformerFactory( + f.client.DynDialOrDie(), + defaultResync, + ns, + nil, + ) + f.preload(ns) + + return f.factories[ns] +} + +func toGVR(gvr string) schema.GroupVersionResource { + tokens := strings.Split(gvr, "/") + if len(tokens) < 3 { + tokens = append([]string{""}, tokens...) + } + + return schema.GroupVersionResource{ + Group: tokens[0], + Version: tokens[1], + Resource: tokens[2], + } +} + +// Client return the factory connection. +func (f *Factory) Client() client.Connection { + return f.client +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func (f *Factory) Dump() { + log.Debug().Msgf("----------- FACTORIES -------------") + for ns := range f.factories { + log.Debug().Msgf(" Factory for NS %q", ns) + } + log.Debug().Msgf("-----------------------------------") +} + +func (f *Factory) Debug(gvr string) { + log.Debug().Msgf("----------- DEBUG FACTORY (%s) -------------", gvr) + inf := f.factories[allNamespaces].ForResource(toGVR(gvr)) + for i, k := range inf.Informer().GetStore().ListKeys() { + log.Debug().Msgf("%d -- %s", i, k) + } +} + +func (f *Factory) Show(ns, gvr string) { + log.Debug().Msgf("----------- SHOW FACTORIES %q -------------", ns) + inf := f.ForResource(ns, gvr) + for _, k := range inf.Informer().GetStore().ListKeys() { + log.Debug().Msgf(" Key: %s", k) + } +} + +func namespaced(n string) (string, string) { + ns, po := path.Split(n) + + return strings.Trim(ns, "/"), po +} diff --git a/internal/watch/forwarders.go b/internal/watch/forwarders.go new file mode 100644 index 00000000..762d0e70 --- /dev/null +++ b/internal/watch/forwarders.go @@ -0,0 +1,80 @@ +package watch + +import ( + "strings" + + "github.com/rs/zerolog/log" + "k8s.io/client-go/tools/portforward" +) + +// Forwarder represents a port forwarder. +type Forwarder interface { + // Start initializes a port forward. + Start(path, co, address string, ports []string) (*portforward.PortForwarder, error) + + // Stop terminates a port forward. + Stop() + + // Path returns a resource FQN. + Path() string + + // Container returns a container name. + Container() string + + // Ports returns container exposed ports. + Ports() []string + + // Active returns forwarder current state. + Active() bool + + // Age returns forwarder age. + Age() string +} + +// Forwarders tracks active port forwards. +type Forwarders map[string]Forwarder + +// NewForwarders returns new forwarders. +func NewForwarders() Forwarders { + return make(map[string]Forwarder) +} + +// KillAll stops and delete all port-forwards. +func (ff Forwarders) DeleteAll() { + ff.Dump() + for k, f := range ff { + log.Debug().Msgf("Deleting forwarder %s", f.Path()) + f.Stop() + delete(ff, k) + } +} + +// Kill stops and delete a port-forwards associated with pod. +func (ff Forwarders) Kill(path string) int { + ff.Dump() + + log.Debug().Msgf("Delete port-forward %q", path) + hasContainer := strings.Contains(path, ":") + var stats int + for k, f := range ff { + victim := k + if !hasContainer { + victim = strings.Split(k, ":")[0] + } + if victim == path { + stats++ + log.Debug().Msgf("Stopping and delete port-forward %s", k) + f.Stop() + delete(ff, k) + } + } + + return stats +} + +func (ff Forwarders) Dump() { + log.Debug().Msgf("----------- PORT-FORWARDS --------------") + for k, f := range ff { + log.Debug().Msgf(" %s -- %#v", k, f) + } +} diff --git a/internal/watch/helper_test.go b/internal/watch/helper_test.go deleted file mode 100644 index fc60c4c5..00000000 --- a/internal/watch/helper_test.go +++ /dev/null @@ -1,174 +0,0 @@ -package watch - -import ( - "strconv" - "testing" - - "gotest.tools/assert" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -func TestMetaFQN(t *testing.T) { - uu := map[string]struct { - m metav1.ObjectMeta - e string - }{ - "full": {metav1.ObjectMeta{Namespace: "fred", Name: "blee"}, "fred/blee"}, - "nons": {metav1.ObjectMeta{Name: "blee"}, "blee"}, - } - - for k, v := range uu { - t.Run(k, func(t *testing.T) { - assert.Equal(t, v.e, MetaFQN(v.m)) - }) - } -} - -func TestMxResourceDiff(t *testing.T) { - uu := map[string]struct { - r1, r2 v1.ResourceList - e bool - }{ - "same": {makeRes("0m", "0Mi"), makeRes("0m", "0Mi"), false}, - "omem": {makeRes("0m", "10Mi"), makeRes("0m", "1Mi"), true}, - "nmem": {makeRes("0m", "0Mi"), makeRes("0m", "1Mi"), true}, - "ocpu": {makeRes("1m", "0Mi"), makeRes("0m", "0Mi"), true}, - "ncpu": {makeRes("1m", "0Mi"), makeRes("2m", "0Mi"), true}, - } - - for k, v := range uu { - t.Run(k, func(t *testing.T) { - assert.Equal(t, v.e, resourceDiff(v.r1, v.r2)) - }) - } -} - -func TestToSelector(t *testing.T) { - uu := map[string]struct { - s string - e map[string]string - }{ - "cool": { - "app=fred,env=test", - map[string]string{"app": "fred", "env": "test"}, - }, - "empty": { - "", - map[string]string{}, - }, - "hosed": { - "app|blee", - map[string]string{}, - }, - "toast": { - "app,blee", - map[string]string{}, - }, - } - - for k, u := range uu { - t.Run(k, func(t *testing.T) { - m := toSelector(u.s) - for k, v := range m { - assert.Equal(t, u.e[k], v) - } - }) - } -} - -func TestMatchesNode(t *testing.T) { - uu := map[string]struct { - n string - s map[string]string - e bool - }{ - "cool": { - "n1", - map[string]string{"spec.nodeName": "n1"}, - true, - }, - "nomatch": { - "n2", - map[string]string{"spec.nodeName": "n1"}, - false, - }, - "matchAll": { - "n2", - map[string]string{}, - true, - }, - } - - for k, u := range uu { - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, matchesNode(u.n, u.s)) - }) - } -} - -func TestMatchesLabels(t *testing.T) { - uu := map[string]struct { - l, s map[string]string - e bool - }{ - "cool": { - map[string]string{"spec.nodeName": "n1"}, - map[string]string{"spec.nodeName": "n1"}, - true, - }, - "nomatch": { - map[string]string{"spec.nodeName": "n2"}, - map[string]string{"spec.nodeName": "n1"}, - false, - }, - "matchAll": { - map[string]string{"spec.nodeName": "n2"}, - map[string]string{}, - true, - }, - } - - for k, u := range uu { - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, matchesLabels(u.l, u.s)) - }) - } -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func makeRes(c, m string) v1.ResourceList { - cpu, _ := resource.ParseQuantity(c) - mem, _ := resource.ParseQuantity(m) - - return v1.ResourceList{ - v1.ResourceCPU: cpu, - v1.ResourceMemory: mem, - } -} - -func makePodMxCo(name, cpu, mem string, co int) *mv1beta1.PodMetrics { - mx := makePodMx(name) - for i := 0; i < co; i++ { - mx.Containers = append( - mx.Containers, - mv1beta1.ContainerMetrics{ - Name: "c" + strconv.Itoa(i), - Usage: makeRes(cpu, mem)}) - } - - return mx -} - -func makePodMx(name string) *mv1beta1.PodMetrics { - return &mv1beta1.PodMetrics{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: "default", - }, - } -} diff --git a/internal/watch/helpers.go b/internal/watch/helpers.go deleted file mode 100644 index b9e7c12b..00000000 --- a/internal/watch/helpers.go +++ /dev/null @@ -1,68 +0,0 @@ -package watch - -import ( - "github.com/rs/zerolog/log" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func resourceDiff(l1, l2 v1.ResourceList) bool { - if l1.Cpu().Cmp(*l2.Cpu()) != 0 { - return true - } - if l1.Memory().Cmp(*l2.Memory()) != 0 { - return true - } - - return false -} - -// MetaFQN computes unique resource id based on metadata. -func MetaFQN(m metav1.ObjectMeta) string { - if m.Namespace == "" { - return m.Name - } - - return m.Namespace + "/" + m.Name -} - -// ToSelector converts a string selector into a map. -func toSelector(s string) map[string]string { - var m map[string]string - ls, err := metav1.ParseToLabelSelector(s) - if err != nil { - log.Error().Err(err).Msg("StringToSel") - return m - } - mSel, err := metav1.LabelSelectorAsMap(ls) - if err != nil { - log.Error().Err(err).Msg("SelToMap") - return m - } - - return mSel -} - -// MatchesNode checks if pod selector matches node name. -func matchesNode(name string, selector map[string]string) bool { - if len(selector) == 0 { - return true - } - - return selector["spec.nodeName"] == name -} - -// MatchesLabels check if pod labels matches a given selector. -func matchesLabels(labels, selector map[string]string) bool { - if len(selector) == 0 { - return true - } - for k, v := range selector { - la, ok := labels[k] - if !ok || la != v { - return false - } - } - - return true -} diff --git a/internal/watch/informer.go b/internal/watch/informer.go deleted file mode 100644 index 8b64c6a7..00000000 --- a/internal/watch/informer.go +++ /dev/null @@ -1,151 +0,0 @@ -package watch - -import ( - "errors" - "fmt" - "sync" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/watch" - "k8s.io/client-go/tools/cache" -) - -const ( - // AllNamespaces designates all namespaces. - allNamespaces = "" - // AllNamespaces designate the special `all` namespace. - allNamespace = "all" -) - -type ( - // Row represents a collection of string fields. - Row []string - - // RowEvent represents a call for action after a resource reconciliation. - // Tracks whether a resource got added, deleted or updated. - RowEvent struct { - Action watch.EventType - Fields Row - Deltas Row - } - - // RowEvents tracks resource update events. - RowEvents map[string]*RowEvent - - // TableData tracks a K8s resource for tabular display. - TableData struct { - Header Row - Rows RowEvents - Namespace string - } -) - -// TableListenerFn represents a table data listener. -type TableListenerFn func(TableData) - -// StoreInformer an informer that allows listeners registration. -type StoreInformer interface { - cache.SharedIndexInformer - Get(fqn string, opts metav1.GetOptions) (interface{}, error) - List(ns string, opts metav1.ListOptions) k8s.Collection -} - -// Informer represents a collection of cluster wide watchers. -type Informer struct { - informers map[string]StoreInformer - client k8s.Connection - podInformer *Pod - listenerFn TableListenerFn - initOnce sync.Once -} - -// NewInformer creates a new cluster resource informer -func NewInformer(client k8s.Connection, ns string) (*Informer, error) { - i := Informer{ - client: client, - informers: map[string]StoreInformer{}, - } - if err := client.CheckNSAccess(ns); err != nil { - log.Error().Err(err).Msg("Checking NS Access") - return nil, err - } - i.init(ns) - - return &i, nil -} - -func (i *Informer) init(ns string) { - log.Debug().Msgf(">>>>> Starting Informer in namespace %q", ns) - - if ok, err := i.client.CanIAccess(ns, "pods.v1.", []string{"list", "watch"}); ok && err == nil { - log.Debug().Msgf("Pod access granted!") - } else { - log.Debug().Msgf("No pod access! %t -- %#v", ok, err) - } - - i.initOnce.Do(func() { - po := NewPod(i.client, ns) - i.informers = map[string]StoreInformer{ - PodIndex: po, - ContainerIndex: NewContainer(po), - } - if ok, err := i.client.CanIAccess(ns, "nodes.v1.", []string{"list", "watch"}); ok && err == nil { - log.Debug().Msgf("CanI access nodes %t -- %#v", ok, err) - i.informers[NodeIndex] = NewNode(i.client) - } else { - log.Debug().Msgf("No node access! %t -- %#v", ok, err) - } - - if !i.client.HasMetrics() { - return - } - - if ok, err := i.client.CanIAccess(ns, "nodes.v1beta1.metrics.k8s.io", []string{"list", "watch"}); ok && err == nil { - i.informers[NodeMXIndex] = NewNodeMetrics(i.client) - } else { - log.Debug().Msg("No node metrics access!") - } - if ok, err := i.client.CanIAccess(ns, "pods.v1beta1.metrics.k8s.io", []string{"list", "watch"}); ok && err == nil { - i.informers[PodMXIndex] = NewPodMetrics(i.client, ns) - } else { - log.Debug().Msgf("No pod metrics access! %t -- %#v", ok, err) - } - }) -} - -// List items from store. -func (i *Informer) List(res, ns string, opts metav1.ListOptions) (k8s.Collection, error) { - if i == nil { - return nil, errors.New("Invalid informer") - } - - if i, ok := i.informers[res]; ok { - return i.List(ns, opts), nil - } - - return nil, fmt.Errorf("No informer found for resource %s in namespace %q", res, ns) -} - -// Get a resource by name. -func (i *Informer) Get(res, fqn string, opts metav1.GetOptions) (interface{}, error) { - if i == nil { - return nil, errors.New("Invalid informer") - } - - if informer, ok := i.informers[res]; ok { - return informer.Get(fqn, opts) - } - - return nil, fmt.Errorf("No informer found for resource %s in namespace %q", res, fqn) -} - -// Run starts watching cluster resources. -func (i *Informer) Run(closeCh <-chan struct{}) { - for name := range i.informers { - go func(si StoreInformer, c <-chan struct{}) { - si.Run(c) - }(i.informers[name], closeCh) - } -} diff --git a/internal/watch/informer_test.go b/internal/watch/informer_test.go deleted file mode 100644 index 24ee53a0..00000000 --- a/internal/watch/informer_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package watch - -import ( - "errors" - "sync" - "testing" - - "github.com/derailed/k9s/internal/k8s" - m "github.com/petergtz/pegomock" - "gotest.tools/assert" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/cli-runtime/pkg/genericclioptions" -) - -func TestInformerAllNSNoAccess(t *testing.T) { - ns := "" - f := new(genericclioptions.ConfigFlags) - f.Namespace = &ns - cmo := NewMockConnection() - m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f)) - m.When(cmo.HasMetrics()).ThenReturn(true) - m.When(cmo.CheckListNSAccess()).ThenReturn(errors.New("denied")) - m.When(cmo.CheckNSAccess(ns)).ThenReturn(errors.New("denied")) - - _, err := NewInformer(cmo, ns) - assert.Error(t, err, "denied") -} - -func TestInformerNSNoAccess(t *testing.T) { - ns := "ns1" - f := new(genericclioptions.ConfigFlags) - f.Namespace = &ns - cmo := NewMockConnection() - m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f)) - m.When(cmo.HasMetrics()).ThenReturn(true) - m.When(cmo.CheckNSAccess(ns)).ThenReturn(errors.New("denied")) - m.When(cmo.CheckListNSAccess()).ThenReturn(errors.New("denied")) - - _, err := NewInformer(cmo, ns) - assert.Error(t, err, "denied") -} - -func TestInformerInitWithNS(t *testing.T) { - ns := "ns1" - f := new(genericclioptions.ConfigFlags) - f.Namespace = &ns - cmo := NewMockConnection() - m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f)) - m.When(cmo.HasMetrics()).ThenReturn(true) - m.When(cmo.CheckNSAccess(ns)).ThenReturn(nil) - m.When(cmo.CanIAccess(ns, "pods.v1", []string{"list", "watch"})).ThenReturn(true, nil) - i, err := NewInformer(cmo, ns) - assert.NilError(t, err) - - o, err := i.List(PodIndex, "fred", metav1.ListOptions{}) - assert.NilError(t, err) - assert.Assert(t, len(o) == 0) -} - -func TestInformerList(t *testing.T) { - f := new(genericclioptions.ConfigFlags) - cmo := NewMockConnection() - m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f)) - i, err := NewInformer(cmo, "") - assert.NilError(t, err) - - o, err := i.List(PodIndex, "fred", metav1.ListOptions{}) - assert.NilError(t, err) - assert.Assert(t, len(o) == 0) -} - -func TestInformerListNoRes(t *testing.T) { - f := new(genericclioptions.ConfigFlags) - cmo := NewMockConnection() - m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f)) - i, err := NewInformer(cmo, "") - assert.NilError(t, err) - - o, err := i.List("dp", "fred", metav1.ListOptions{}) - assert.ErrorContains(t, err, "No informer found") - assert.Assert(t, len(o) == 0) -} - -func TestInformerGet(t *testing.T) { - f := new(genericclioptions.ConfigFlags) - cmo := NewMockConnection() - m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f)) - i, err := NewInformer(cmo, "") - assert.NilError(t, err) - - o, err := i.Get(PodIndex, "fred", metav1.GetOptions{}) - assert.ErrorContains(t, err, "Pod fred not found") - assert.Assert(t, o == nil) -} - -func TestInformerGetNoRes(t *testing.T) { - f := new(genericclioptions.ConfigFlags) - cmo := NewMockConnection() - m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f)) - i, err := NewInformer(cmo, "") - assert.NilError(t, err) - - o, err := i.Get("rs", "fred", metav1.GetOptions{}) - assert.ErrorContains(t, err, "No informer found") - assert.Assert(t, o == nil) -} - -func TestInformerRun(t *testing.T) { - f := new(genericclioptions.ConfigFlags) - cmo := NewMockConnection() - m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f)) - i, err := NewInformer(cmo, "") - assert.NilError(t, err) - - var wg sync.WaitGroup - wg.Add(1) - c := make(chan struct{}) - go func() { - defer wg.Done() - i.Run(c) - }() - close(c) - wg.Wait() -} diff --git a/internal/watch/no.go b/internal/watch/no.go deleted file mode 100644 index 8c0bb153..00000000 --- a/internal/watch/no.go +++ /dev/null @@ -1,51 +0,0 @@ -package watch - -import ( - "fmt" - - "github.com/derailed/k9s/internal/k8s" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - wv1 "k8s.io/client-go/informers/core/v1" - "k8s.io/client-go/tools/cache" -) - -const ( - // NodeIndex marker for stored nodes. - NodeIndex string = "no" - nodeCols = 12 -) - -// Node tracks node activities. -type Node struct { - cache.SharedIndexInformer -} - -// NewNode returns a new node. -func NewNode(c k8s.Connection) *Node { - return &Node{ - SharedIndexInformer: wv1.NewNodeInformer(c.DialOrDie(), 0, cache.Indexers{}), - } -} - -// List all nodes. -func (n *Node) List(_ string, opts metav1.ListOptions) k8s.Collection { - var res k8s.Collection - for _, o := range n.GetStore().List() { - res = append(res, o) - } - - return res -} - -// Get retrieves a given node from store. -func (n *Node) Get(fqn string, opts metav1.GetOptions) (interface{}, error) { - o, ok, err := n.GetStore().GetByKey(fqn) - if err != nil { - return nil, err - } - if !ok { - return nil, fmt.Errorf("Node %s not found", fqn) - } - - return o, nil -} diff --git a/internal/watch/no_mx.go b/internal/watch/no_mx.go deleted file mode 100644 index 1e4972d7..00000000 --- a/internal/watch/no_mx.go +++ /dev/null @@ -1,179 +0,0 @@ -package watch - -import ( - "errors" - "fmt" - "time" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/watch" - "k8s.io/client-go/tools/cache" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -const ( - // NodeMXIndex track store indexer. - NodeMXIndex string = "nmx" - // NodeMXRefresh node metrics sync rate. - nodeMXRefresh = 30 * time.Second -) - -// NodeMetrics tracks node metrics. -type NodeMetrics struct { - cache.SharedIndexInformer - - client k8s.Connection -} - -// NewNodeMetrics returns a node metrics informer. -func NewNodeMetrics(c k8s.Connection) *NodeMetrics { - return &NodeMetrics{ - SharedIndexInformer: newNodeMetricsInformer(c, 0, cache.Indexers{}), - client: c, - } -} - -// List node metrics from store. -func (p *NodeMetrics) List(_ string, opts metav1.ListOptions) k8s.Collection { - return p.GetStore().List() -} - -// Get node metrics from store. -func (p *NodeMetrics) Get(MetaFQN string, opts metav1.GetOptions) (interface{}, error) { - o, ok, err := p.GetStore().GetByKey(MetaFQN) - if err != nil { - return nil, err - } - if !ok { - return nil, fmt.Errorf("No node metrics for %q", MetaFQN) - } - - return o, nil -} - -// NewNodeMetricsInformer return an informer to return node metrix. -func newNodeMetricsInformer(client k8s.Connection, sync time.Duration, idxs cache.Indexers) cache.SharedIndexInformer { - pw := newNodeMxWatcher(client) - c, err := client.MXDial() - if err != nil { - log.Error().Err(err).Msg("NodeMetrix dial") - return nil - } - - return cache.NewSharedIndexInformer( - &cache.ListWatch{ - ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) { - l, err := c.MetricsV1beta1().NodeMetricses().List(opts) - if err == nil { - pw.update(l, false) - } - return l, err - }, - WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) { - go pw.Run() - return pw, nil - }, - }, - &mv1beta1.NodeMetrics{}, - sync, - idxs, - ) -} - -// nodeMxWatcher tracks node metrics. -type nodeMxWatcher struct { - client k8s.Connection - cache map[string]runtime.Object - eventChan chan watch.Event - doneChan chan struct{} -} - -// NewnodeMxWatcher returns a new metrics watcher. -func newNodeMxWatcher(c k8s.Connection) *nodeMxWatcher { - return &nodeMxWatcher{ - client: c, - cache: map[string]runtime.Object{}, - eventChan: make(chan watch.Event), - doneChan: make(chan struct{}), - } -} - -// Run watcher to monitor node metrics. -func (n *nodeMxWatcher) Run() { - defer log.Debug().Msg("NodeMetrics informer canceled!") - c, err := n.client.MXDial() - if err != nil { - log.Error().Err(err).Msg("NodeMetrix Dial Failed!") - return - } - - for { - select { - case <-n.doneChan: - close(n.eventChan) - return - case <-time.After(nodeMXRefresh): - list, err := c.MetricsV1beta1().NodeMetricses().List(metav1.ListOptions{}) - if err != nil { - log.Error().Err(err).Msg("NodeMetrics List Failed!") - } - n.update(list, true) - } - } -} - -// Stop the metrics informer. -func (n *nodeMxWatcher) Stop() { - log.Debug().Msg("Stopping NodeMetrix informer!") - close(n.doneChan) -} - -// ResultChan retrieves event channel. -func (n *nodeMxWatcher) ResultChan() <-chan watch.Event { - return n.eventChan -} - -func (n *nodeMxWatcher) notify(event watch.Event) error { - select { - case n.eventChan <- event: - return nil - case <-n.doneChan: - return errors.New("watcher has ben closed.") - } -} - -func (n *nodeMxWatcher) update(list *mv1beta1.NodeMetricsList, notify bool) { - fqns := map[string]runtime.Object{} - for i := range list.Items { - fqn := MetaFQN(list.Items[i].ObjectMeta) - fqns[fqn] = &list.Items[i] - } - for k, v := range n.cache { - if _, ok := fqns[k]; !ok { - if notify { - if err := n.notify(watch.Event{Type: watch.Deleted, Object: v}); err != nil { - return - } - } - delete(n.cache, k) - } - } - for k, v := range fqns { - kind := watch.Added - if v1, ok := n.cache[k]; ok { - if !resourceDiff(v1.(*mv1beta1.NodeMetrics).Usage, v.(*mv1beta1.NodeMetrics).Usage) { - continue - } - kind = watch.Modified - } - if notify { - if err := n.notify(watch.Event{Type: kind, Object: v}); err != nil { - return - } - } - n.cache[k] = v - } -} diff --git a/internal/watch/no_mx_test.go b/internal/watch/no_mx_test.go deleted file mode 100644 index 88b9deac..00000000 --- a/internal/watch/no_mx_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package watch - -import ( - "sync" - "testing" - - "gotest.tools/assert" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" - v1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -func TestNodeMXList(t *testing.T) { - cmo := NewMockConnection() - no := NewNodeMetrics(cmo) - - o := no.List("", metav1.ListOptions{}) - assert.Assert(t, len(o) == 0) -} - -func TestNodeMXGet(t *testing.T) { - cmo := NewMockConnection() - no := NewNodeMetrics(cmo) - - o, err := no.Get("", metav1.GetOptions{}) - assert.ErrorContains(t, err, "No node metrics") - assert.Assert(t, o == nil) -} - -func TestNodeMXUpdate(t *testing.T) { - cmo := NewMockConnection() - no := newNodeMxWatcher(cmo) - no.cache = map[string]runtime.Object{ - "n1": makeNodeMX("n1", "11m", "11Mi"), - } - - mxx := &mv1beta1.NodeMetricsList{ - Items: []mv1beta1.NodeMetrics{ - *makeNodeMX("n1", "10m", "10Mi"), - }, - } - no.update(mxx, false) - - assert.Equal(t, toQty("10m"), *no.cache["n1"].(*mv1beta1.NodeMetrics).Usage.Cpu()) - assert.Equal(t, toQty("10Mi"), *no.cache["n1"].(*mv1beta1.NodeMetrics).Usage.Memory()) -} - -func TestNodeMXUpdateNoChange(t *testing.T) { - cmo := NewMockConnection() - no := newNodeMxWatcher(cmo) - no.cache = map[string]runtime.Object{ - "n1": makeNodeMX("n1", "10m", "10Mi"), - } - - mxx := &mv1beta1.NodeMetricsList{ - Items: []mv1beta1.NodeMetrics{ - *makeNodeMX("n1", "10m", "10Mi"), - }, - } - no.update(mxx, false) - - assert.Equal(t, toQty("10m"), *no.cache["n1"].(*mv1beta1.NodeMetrics).Usage.Cpu()) - assert.Equal(t, toQty("10Mi"), *no.cache["n1"].(*mv1beta1.NodeMetrics).Usage.Memory()) -} - -func TestNodeMXDelete(t *testing.T) { - cmo := NewMockConnection() - no := newNodeMxWatcher(cmo) - no.cache = map[string]runtime.Object{ - "n1": makeNodeMX("n1", "11m", "11Mi"), - } - - mxx := &mv1beta1.NodeMetricsList{} - no.update(mxx, false) - - assert.Equal(t, 0, len(no.cache)) -} - -func TestNodeMXRun(t *testing.T) { - cmo := NewMockConnection() - w := newNodeMxWatcher(cmo) - - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - w.Run() - }() - - w.Stop() - wg.Wait() -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func toQty(s string) resource.Quantity { - q, _ := resource.ParseQuantity(s) - - return q -} - -func makeNodeMX(n, cpu, mem string) *v1beta1.NodeMetrics { - return &v1beta1.NodeMetrics{ - ObjectMeta: metav1.ObjectMeta{ - Name: n, - }, - Usage: v1.ResourceList{ - v1.ResourceCPU: toQty(cpu), - v1.ResourceMemory: toQty(mem), - }, - } -} diff --git a/internal/watch/no_test.go b/internal/watch/no_test.go deleted file mode 100644 index e8ccffff..00000000 --- a/internal/watch/no_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package watch - -import ( - "testing" - - "gotest.tools/assert" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestNodeList(t *testing.T) { - cmo := NewMockConnection() - no := NewNode(cmo) - o := no.List("", metav1.ListOptions{}) - - assert.Assert(t, o == nil) -} - -func TestNodeGet(t *testing.T) { - cmo := NewMockConnection() - no := NewNode(cmo) - o, err := no.Get("", metav1.GetOptions{}) - - assert.ErrorContains(t, err, "not found") - assert.Assert(t, o == nil) -} diff --git a/internal/watch/pod.go b/internal/watch/pod.go deleted file mode 100644 index 8d6911d7..00000000 --- a/internal/watch/pod.go +++ /dev/null @@ -1,70 +0,0 @@ -package watch - -import ( - "fmt" - "strings" - - "github.com/derailed/k9s/internal/k8s" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - wv1 "k8s.io/client-go/informers/core/v1" - "k8s.io/client-go/tools/cache" -) - -const ( - // PodIndex marker for stored pods. - PodIndex string = "po" - podCols = 11 -) - -// Connection represents an client api server connection. -type Connection k8s.Connection - -// Pod tracks pod activities. -type Pod struct { - cache.SharedIndexInformer -} - -// NewPod returns a new pod. -func NewPod(c Connection, ns string) *Pod { - return &Pod{ - SharedIndexInformer: wv1.NewPodInformer(c.DialOrDie(), ns, 0, cache.Indexers{}), - } -} - -// List all pods from store in the given namespace. -func (p *Pod) List(ns string, opts metav1.ListOptions) k8s.Collection { - var res k8s.Collection - var nodeSelector bool - if strings.Contains(opts.FieldSelector, "spec.nodeName") { - nodeSelector = true - } - for _, o := range p.GetStore().List() { - pod := o.(*v1.Pod) - if ns != "" && pod.Namespace != ns { - continue - } - if nodeSelector { - if !matchesNode(pod.Spec.NodeName, toSelector(opts.FieldSelector)) { - continue - } - } else if !matchesLabels(pod.ObjectMeta.Labels, toSelector(opts.LabelSelector)) { - continue - } - res = append(res, pod) - } - return res -} - -// Get retrieves a given pod from store. -func (p *Pod) Get(fqn string, opts metav1.GetOptions) (interface{}, error) { - o, ok, err := p.GetStore().GetByKey(fqn) - if err != nil { - return nil, err - } - if !ok { - return nil, fmt.Errorf("Pod %s not found", fqn) - } - - return o, nil -} diff --git a/internal/watch/pod_mx.go b/internal/watch/pod_mx.go deleted file mode 100644 index 01c06202..00000000 --- a/internal/watch/pod_mx.go +++ /dev/null @@ -1,216 +0,0 @@ -package watch - -import ( - "errors" - "fmt" - "time" - - "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/watch" - "k8s.io/client-go/tools/cache" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -const ( - // PodMXIndex track store indexer. - PodMXIndex string = "pmx" - // PodMXRefresh pod metrics sync rate. - podMXRefresh = 15 * time.Second -) - -// PodMetrics tracks pod metrics. -type PodMetrics struct { - cache.SharedIndexInformer - client k8s.Connection - ns string -} - -// NewPodMetrics returns a pod metrics informer. -func NewPodMetrics(c k8s.Connection, ns string) *PodMetrics { - return &PodMetrics{ - SharedIndexInformer: newPodMetricsInformer(c, ns, 0, cache.Indexers{}), - ns: ns, - client: c, - } -} - -// List pod metrics from store. -func (p *PodMetrics) List(ns string, opts metav1.ListOptions) k8s.Collection { - var res k8s.Collection - for _, o := range p.GetStore().List() { - mx := o.(*mv1beta1.PodMetrics) - if ns == "" || mx.Namespace == ns { - res = append(res, mx) - } - } - - return res -} - -// Get pod metrics from store. -func (p *PodMetrics) Get(fqn string, opts metav1.GetOptions) (interface{}, error) { - o, ok, err := p.GetStore().GetByKey(fqn) - if err != nil { - return nil, err - } - if !ok { - return nil, fmt.Errorf("No pod metrics for %q found", fqn) - } - - return o, nil -} - -// NewPodMetricsInformer return an informer to return pod metrix. -func newPodMetricsInformer(client k8s.Connection, ns string, sync time.Duration, idxs cache.Indexers) cache.SharedIndexInformer { - pw := newPodMxWatcher(client, ns) - c, err := client.MXDial() - if err != nil { - log.Error().Err(err).Msg("PodMetrix dial") - return nil - } - - return cache.NewSharedIndexInformer( - &cache.ListWatch{ - ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) { - l, err := c.MetricsV1beta1().PodMetricses(ns).List(opts) - if err == nil { - pw.update(l, false) - } - return l, err - }, - WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) { - go pw.Run() - return pw, nil - }, - }, - &mv1beta1.PodMetrics{}, - sync, - idxs, - ) -} - -// PodMxWatcher tracks pod metrics. -type podMxWatcher struct { - client k8s.Connection - ns string - cache map[string]runtime.Object - eventChan chan watch.Event - doneChan chan struct{} -} - -// NewpodMxWatcher returns a new metrics watcher. -func newPodMxWatcher(c k8s.Connection, ns string) *podMxWatcher { - return &podMxWatcher{ - client: c, - ns: ns, - eventChan: make(chan watch.Event), - doneChan: make(chan struct{}), - cache: map[string]runtime.Object{}, - } -} - -// Run watcher to monitor pod metrics. -func (p *podMxWatcher) Run() { - defer log.Debug().Msg("PodMetrics informer stopped!") - c, err := p.client.MXDial() - if err != nil { - log.Error().Err(err).Msg("PodMetrix Dial Failed!") - return - } - - for { - select { - case <-p.doneChan: - close(p.eventChan) - return - case <-time.After(podMXRefresh): - list, err := c.MetricsV1beta1().PodMetricses(p.ns).List(metav1.ListOptions{}) - if err != nil { - log.Error().Err(err).Msg("PodMetrics List Failed!") - } - p.update(list, true) - } - } -} - -func (p *podMxWatcher) notify(event watch.Event) error { - select { - case p.eventChan <- event: - return nil - case <-p.doneChan: - return errors.New("watcher has ben closed.") - } -} - -func (p *podMxWatcher) update(list *mv1beta1.PodMetricsList, notify bool) { - fqns := map[string]runtime.Object{} - for i := range list.Items { - fqn := MetaFQN(list.Items[i].ObjectMeta) - fqns[fqn] = &list.Items[i] - } - - for k, v := range p.cache { - if _, ok := fqns[k]; !ok { - if notify { - if err := p.notify(watch.Event{Type: watch.Deleted, Object: v}); err != nil { - return - } - } - delete(p.cache, k) - } - } - - for k, v := range fqns { - kind := watch.Added - if v1, ok := p.cache[k]; ok { - if !p.deltas(v1.(*mv1beta1.PodMetrics), v.(*mv1beta1.PodMetrics)) { - continue - } - kind = watch.Modified - } - if notify { - if err := p.notify(watch.Event{Type: kind, Object: v}); err != nil { - return - } - } - p.cache[k] = v - } -} - -// Stop the metrics informer. -func (p *podMxWatcher) Stop() { - log.Debug().Msg("Stopping PodMetrix informer!!") - close(p.doneChan) -} - -// ResultChan retrieves event channel. -func (p *podMxWatcher) ResultChan() <-chan watch.Event { - return p.eventChan -} - -func (p *podMxWatcher) deltas(m1, m2 *mv1beta1.PodMetrics) bool { - mm1 := map[string]v1.ResourceList{} - for _, co := range m1.Containers { - mm1[co.Name] = co.Usage - } - mm2 := map[string]v1.ResourceList{} - for _, co := range m2.Containers { - mm2[co.Name] = co.Usage - } - - for k2, v2 := range mm2 { - v1, ok := mm1[k2] - if !ok { - return true - } - if resourceDiff(v1, v2) { - return true - } - } - - return false -} diff --git a/internal/watch/pod_mx_test.go b/internal/watch/pod_mx_test.go deleted file mode 100644 index bd88e9ee..00000000 --- a/internal/watch/pod_mx_test.go +++ /dev/null @@ -1,135 +0,0 @@ -package watch - -import ( - "sync" - "testing" - - "github.com/rs/zerolog" - "gotest.tools/assert" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" - v1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -func init() { - zerolog.SetGlobalLevel(zerolog.Disabled) -} - -func TestPodMXList(t *testing.T) { - cmo := NewMockConnection() - po := NewPodMetrics(cmo, "") - - o := po.List("", metav1.ListOptions{}) - assert.Assert(t, len(o) == 0) -} - -func TestPodMXGet(t *testing.T) { - cmo := NewMockConnection() - po := NewPodMetrics(cmo, "") - - o, err := po.Get("", metav1.GetOptions{}) - assert.ErrorContains(t, err, "No pod metrics") - assert.Assert(t, o == nil) -} - -func TestMxDeltas(t *testing.T) { - uu := map[string]struct { - m1, m2 *mv1beta1.PodMetrics - e bool - }{ - "same": {makePodMxCo("p1", "1m", "0Mi", 1), makePodMxCo("p1", "1m", "0Mi", 1), false}, - "dcpu": {makePodMxCo("p1", "10m", "0Mi", 1), makePodMxCo("p1", "0m", "0Mi", 1), true}, - "dmem": {makePodMxCo("p1", "0m", "10Mi", 1), makePodMxCo("p1", "0m", "0Mi", 1), true}, - "dco": {makePodMxCo("p1", "0m", "10Mi", 1), makePodMxCo("p1", "0m", "0Mi", 2), true}, - } - - var p podMxWatcher - for k, v := range uu { - t.Run(k, func(t *testing.T) { - assert.Equal(t, v.e, p.deltas(v.m1, v.m2)) - }) - } -} - -func TestPodMXRun(t *testing.T) { - cmo := NewMockConnection() - w := newPodMxWatcher(cmo, "") - - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - w.Run() - }() - - w.Stop() - wg.Wait() -} - -func TestPodMXUpdate(t *testing.T) { - cmo := NewMockConnection() - po := newPodMxWatcher(cmo, "default") - po.cache = map[string]runtime.Object{ - "default/p1": makePodMX("p1", "11m", "11Mi"), - } - - mxx := &mv1beta1.PodMetricsList{ - Items: []mv1beta1.PodMetrics{ - *makePodMX("p1", "10m", "10Mi"), - }, - } - po.update(mxx, false) - - pmx := po.cache["default/p1"].(*mv1beta1.PodMetrics) - assert.Equal(t, toQty("10m"), *pmx.Containers[0].Usage.Cpu()) - assert.Equal(t, toQty("10Mi"), *pmx.Containers[0].Usage.Memory()) -} - -func TestPodMXUpdateNoChange(t *testing.T) { - cmo := NewMockConnection() - po := newPodMxWatcher(cmo, "default") - po.cache = map[string]runtime.Object{ - "default/p1": makePodMX("p1", "10m", "10Mi"), - } - - mxx := &mv1beta1.PodMetricsList{ - Items: []mv1beta1.PodMetrics{ - *makePodMX("p1", "10m", "10Mi"), - }, - } - po.update(mxx, false) - - pmx := po.cache["default/p1"].(*mv1beta1.PodMetrics) - assert.Equal(t, toQty("10m"), *pmx.Containers[0].Usage.Cpu()) - assert.Equal(t, toQty("10Mi"), *pmx.Containers[0].Usage.Memory()) -} - -func TestPodMXDelete(t *testing.T) { - cmo := NewMockConnection() - po := newPodMxWatcher(cmo, "default") - po.cache = map[string]runtime.Object{ - "default/p1": makePodMX("p1", "11m", "11Mi"), - } - - mxx := &mv1beta1.PodMetricsList{} - po.update(mxx, false) - - assert.Equal(t, 0, len(po.cache)) -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func makePodMX(name, cpu, mem string) *v1beta1.PodMetrics { - return &v1beta1.PodMetrics{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: "default", - }, - Containers: []v1beta1.ContainerMetrics{ - {Name: "i1", Usage: makeRes(cpu, mem)}, - {Name: "c1", Usage: makeRes(cpu, mem)}, - }, - } -} diff --git a/internal/watch/pod_test.go b/internal/watch/pod_test.go deleted file mode 100644 index 5c878f7c..00000000 --- a/internal/watch/pod_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package watch - -import ( - "testing" - - "gotest.tools/assert" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestPodList(t *testing.T) { - cmo := NewMockConnection() - no := NewPod(cmo, "") - - o := no.List("", metav1.ListOptions{}) - assert.Assert(t, o == nil) -} - -func TestPodGet(t *testing.T) { - cmo := NewMockConnection() - no := NewPod(cmo, "") - o, err := no.Get("", metav1.GetOptions{}) - - assert.ErrorContains(t, err, "not found") - assert.Assert(t, o == nil) -} diff --git a/main.go b/main.go index 8e4f977c..a040a539 100644 --- a/main.go +++ b/main.go @@ -20,7 +20,6 @@ func main() { if err != nil { panic(err) } - log.Logger = log.Output(zerolog.ConsoleWriter{Out: file}) cmd.Execute() diff --git a/skins/snazzy.yml b/skins/snazzy.yml new file mode 100644 index 00000000..3e1e285e --- /dev/null +++ b/skins/snazzy.yml @@ -0,0 +1,51 @@ +k9s: + body: + fgColor: "#97979b" + bgColor: "#282a36" + logoColor: "#5af78e" + info: + fgColor: white + sectionColor: "#5af78e" + frame: + border: + fgColor: "#5af78e" + focusColor: "#5af78e" + menu: + fgColor: white + keyColor: "#57c7ff" + numKeyColor: "#ff6ac1" + crumbs: + fgColor: "#282a36" + bgColor: white + activeColor: "#f3f99d" + status: + newColor: "#eff0eb" + modifyColor: "#5af78e" + addColor: "#57c7ff" + errorColor: "#ff5c57" + highlightcolor: "#f3f99d" + killColor: mediumpurple + completedColor: gray + title: + fgColor: "#5af78e" + bgColor: "#282a36" + highlightColor: white + counterColor: white + filterColor: "#57c7ff" + table: + fgColor: "#57c7ff" + bgColor: "#282a36" + cursorColor: "#5af78e" + markColor: darkgoldenrod + header: + fgColor: white + bgColor: "#282a36" + sorterColor: orange + views: + yaml: + keyColor: "#ff5c57" + colonColor: white + valueColor: "#f3f99d" + logs: + fgColor: white + bgColor: "#282a36"