228 Commits

Author SHA1 Message Date
justin0u0
013b6bc252 fix(kubectx): use the env variable for kubectl (#438) 2025-01-22 09:07:57 -08:00
Antoine
0bcd0d5dd5 fix some ci enhancement (#435)
* fix(release): customize goreleaser config file, by adding json schema and fixing configuration version

* fix(go): rename invalid comment format

* fix(ci): made release workflow work again

replace goreleaser --rm-dist flag by --clean
increment go version for release pipeline
fetch previous tags use by goreleaser
give release workflow content write permissions to publish release
2025-01-07 12:25:01 -08:00
Suleiman Dibirov
561793c356 chore: upgrade Go version to 1.22 (#425) 2024-07-10 14:25:37 -07:00
Suleiman Dibirov
b5daf2cef7 feat(kubens): added force flag to switch namespaces even if it doesn'… (#416)
* feat(kubens): added force flag to switch namespaces even if it doesn't exist

* Merged lines

* fixed README.md and flags.go

* updated flags.go, flags_test.go
2024-07-09 21:38:40 -07:00
pullmerge
4997a261dc Fix some comments (#418) 2024-04-15 09:31:07 -07:00
Marcos Alano
8fb8c9f2f2 Add support to publish Snaps using goreleaser (#353)
Closes #351.
2023-12-25 00:28:51 -08:00
Nabil
11c19c0fb7 Add Scoop command (#396) 2023-08-04 15:04:50 -07:00
Ahmet Alp Balkan
92e5b5f43b Release v0.9.5 2023-07-13 20:12:27 -07:00
Ahmet Alp Balkan
33c27c03b2 k8s.io & go version bump (#393) 2023-07-13 20:10:47 -07:00
niko2
7560b8f04f Modify oh-my-zsh example (#346) 2023-03-15 16:30:08 -07:00
Ahmet Alp Balkan
d8ff2847ba fix go.sum tidy
Signed-off-by: Ahmet Alp Balkan <ahmetalpbalkan@gmail.com>
2023-02-24 12:03:11 -08:00
Gastón Haro
da454d8a0c Fix README.md "alias" to "rename" (#375) 2023-02-24 09:20:16 -08:00
Nikolas Grottendieck
021c1bc736 Windows installation instructions (winget) (#365) (#381) 2023-02-24 09:14:12 -08:00
Vincent Victoria
29850e1a75 Add prerequisite for completion to work after brew install (#368) 2022-10-24 12:06:54 -07:00
Sandro
7d6b179aed Bump golang.org/x/sys to fix compilation on M1 macs (#360) 2022-08-04 08:41:17 -07:00
Apoorv Verma [AP]
e5e7f53336 Windows installation instructions (Chocolatey) (#350) 2022-03-16 00:08:53 -07:00
Gábor Lipták
e6de7ba0a2 Run Go 1.17 mod tiny (#336) 2022-01-10 21:00:11 -08:00
SADIK KUZU
b6b364685a Fix typo on README.md (#341) 2022-01-09 09:39:34 -08:00
Ahmet Alp Balkan
617e4f0562 add build cache to actions (#339) 2021-12-26 21:50:17 -08:00
Kai
60523045a5 README.md: HTTP => HTTPS (#337) 2021-11-28 08:23:09 -08:00
Ahmet Alp Balkan
38117be348 go mod tidy
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2021-11-27 21:34:14 -08:00
Gábor Lipták
f123e3864e Bump Go to 1.17 in GHA (#335) 2021-11-27 10:47:19 -08:00
Ahmet Alp Balkan
207dd606bb overhaul the readme
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2021-11-12 12:53:40 -08:00
Joshua Gleitze
bdb1ea9e9d Make Completions Installable with Antibody (#263) 2021-11-04 08:34:23 -07:00
Ahmet Alp Balkan
a509657288 Update README.md 2021-10-21 11:15:52 -07:00
Johan Dewe
e449e739f8 Wrap context names in single quotes to prevent completion script to fail (#316) 2021-08-19 10:04:48 -07:00
Carlos Alexandro Becker
33212062fb docs: fix starchart url (#306) 2021-07-18 18:33:51 -07:00
Yaakov Selkowitz
13695147d1 Add s390x build (#270) 2021-07-08 17:21:05 -07:00
Yankee
58a5c4693e Add interactive delete op (#304) 2021-07-07 11:48:32 -07:00
Ahmet Alp Balkan
979012e094 Release v0.9.4 2021-07-06 15:24:06 -07:00
peter woodman
ff2f9661a2 stop using XDG_CACHE_HOME as home directory (#299)
* stop using XDG_CACHE_HOME as home directory

XDG_CACHE_HOME is not a substitute for $HOME, see [1].

[1]: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html

* fix bats testing setup/teardown

since cmdutil.Homedir() would treat $XDG_CACHE_HOME as $HOME, deleting
$XDG_CACHE_HOME would wipe out previous kubens state. now that we're not
doing that, we need to make a real synthetic $HOME and clear it out so
that $HOME/.kube/kubens doesn't persist between runs.
2021-05-28 16:09:32 -07:00
Ahmet Alp Balkan
34e9024835 add -V/--version flag to go implementations (#295)
Uses goreleaser to pass ldflags.

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2021-04-09 09:43:48 -07:00
Ahmet Alp Balkan
3504e66edb Provide binary release for darwin/arm64
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2021-02-26 11:34:03 -08:00
Ahmet Alp Balkan
55548e15ed Release v0.9.2 2021-02-24 11:21:15 -08:00
Andrey Viktorov
9b4aea3b59 Bump up go-client to fix auth plugin panic (#281) 2021-02-02 15:38:27 -08:00
Jeff MAURY
767218a9a6 Check correct variable after current namespace retrieval (#287) 2021-01-31 17:48:35 -08:00
Ahmet Alp Balkan
438ba19fb0 Update debian installation instructions
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-12-29 13:40:14 -08:00
Ahmet Alp Balkan
1e49c336fc Add license headers for 2021
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-12-29 13:38:42 -08:00
Jason Harmon
8c323c5653 Fix color in kubens success message when using fzf (#228) 2020-11-16 09:01:28 -08:00
Chris Stefano
9527e308e5 make kubectx consistent with kubens wrt. KUBECTL environment variable (#274) 2020-11-13 16:09:28 -08:00
Carlos Alexandro Becker
8241576f28 fix: improve kubectx completion on fish (#269) 2020-11-01 21:45:40 -08:00
Justin Garrison
5aba9fa311 Add Homebrew install for Linux (#261)
* Add Homebrew install for Linux

Is there a reason why `brew` install instructions isn't included for Linux? Because it's bash I'm able to install it just fine with brew on Ubuntu.

* Rearange Linux install sections
2020-09-25 14:37:59 -07:00
Gábor Lipták
a8a63da51c Bump CI to Go 1.15 (#266) 2020-09-25 13:25:03 -07:00
Gábor Lipták
8c8aeada3d Correct build badge in README (#265) 2020-09-25 13:24:51 -07:00
Gábor Lipták
ab50545ecd Bump CI release to Go 1.15 (#267) 2020-09-25 13:24:25 -07:00
Ahmet Alp Balkan
51c61b862d Add ppc64le build
Not tagging a release for now. This only causes linux/ppc64le binaries to be
created for now.

Closes #252.

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-07-08 11:37:10 -07:00
Ahmet Alp Balkan
09f31d96e4 Release v0.9.1 2020-06-30 13:02:11 -07:00
Rajat Jindal
0813c314c6 Add krew release bot back (#245) 2020-06-22 10:37:07 -07:00
Sedat Gökcen
1db00a20d9 Structural refactoring for multiple kubeconfig support (#219) 2020-06-02 13:04:13 -07:00
Ahmet Alp Balkan
170233bffd query namespace exists with GET Namespace (#236)
More efficient ns switches with kubens by querying only the namespace (instead
of listing all namespaces).

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-05-31 15:32:44 -07:00
Ahmet Alp Balkan
0141ee19d2 handle ctxs with ':' in the name for windows (#235)
For windows, if the ctx name for kubens state file
contains a colon ':' (EKS), we replace it with __ (both while reading/writing).

It doesn't break existing users (as it didn't work in the first place).

Other characters remain unhandled, hoping it won't happen again.

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-05-31 14:24:01 -07:00
Jason Harmon
401188fefd Fix color output on Windows (#220) 2020-05-15 15:26:00 -07:00
Ahmet Alp Balkan
01bd237baa Release for arm64 (armv8) and armhf (armv6), armv7 (#217) 2020-05-04 14:56:20 -07:00
Ahmet Alp Balkan
956d5953c2 kubens: fix interactive switch messages
Fixes #209.

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-30 07:36:21 -07:00
Ahmet Alp Balkan
4425628f91 Remove changelog
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-30 07:36:21 -07:00
Curro Rodriguez
f2021bb08b changed help in kubens, return had a typo (#210) 2020-04-30 07:34:06 -07:00
Ahmet Alp Balkan
d603c7dada Release v0.9.0 2020-04-29 13:52:03 -07:00
Ahmet Alp Balkan
ba79bdb0f8 Update readme to talk about Go rewrite
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 13:51:19 -07:00
Ahmet Alp Balkan
fc21b8c522 Update readme, update bats setup
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 13:45:40 -07:00
Ahmet Alp Balkan
e5024778a9 Add integration tests to CI workflow
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 13:40:11 -07:00
Ahmet Alp Balkan
d669862436 Update CI workflow name
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 13:30:55 -07:00
Ahmet Alp Balkan
7f3f0699b3 Extend CI workflow with gofmt
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 13:28:12 -07:00
Ahmet Alp Balkan
3c9c44842c Extend CI workflow by adding tests
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 13:22:42 -07:00
Ahmet Alp Balkan
0491ac552e Add goreleaser and github workflows
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 13:18:14 -07:00
Ahmet Alp Balkan
5348d7aa7e kubens add a short-circuit to bypass API call for tests
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:56 -07:00
Ahmet Alp Balkan
04689f571e ns list: increase page size to 500
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:56 -07:00
Ahmet Alp Balkan
1881107d55 Load namespaces using client-go
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:56 -07:00
Ahmet Alp Balkan
52bbf5c786 fix compile error
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:56 -07:00
Ahmet Alp Balkan
4bbe0fad79 deprecation msgs for KUBECTX_CURRENT_{BG,FG}COLOR
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:56 -07:00
Ahmet Alp Balkan
5b745727c3 Add interactive switching to kubens
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:56 -07:00
Ahmet Alp Balkan
fc2e1c6b08 Fix bug about where cur ns was stored in yaml
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:56 -07:00
Ahmet Alp Balkan
2915103e3d kubens: implement namespace switching
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:56 -07:00
Ahmet Alp Balkan
6c3977d574 kubens: Add facility to store state file
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:56 -07:00
Ahmet Alp Balkan
de2867a622 Implement list (via exec kubectl), clearer color settings
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:56 -07:00
Ahmet Alp Balkan
c4252b5795 Move kubeconfig loader utils to cmdutil pkg
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:56 -07:00
Ahmet Alp Balkan
1982becb15 kubens: Start implementing stubs
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:56 -07:00
Ahmet Alp Balkan
342d21683b Create test utils for crafting kubeconfig strings
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:56 -07:00
Ahmet Alp Balkan
62d8dad7d5 extract kubeconfig test utils to a type
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:56 -07:00
Ahmet Alp Balkan
68b842f39b do not fail on non-existing kubeconfig files
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:56 -07:00
Ahmet Alp Balkan
0e50f15393 Better success msgs, handle -d without args
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:56 -07:00
Ahmet Alp Balkan
f4f558004a Tidy up colors, help msgs, TODOs
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:56 -07:00
Ahmet Alp Balkan
7598c4d4dd Create printer pkg, fix color force enable/disable
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:56 -07:00
Ahmet Alp Balkan
bef0a4cca7 Move kubeconfig utility to a shared pkg
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:56 -07:00
Ahmet Alp Balkan
d5546f062d Extend test coverage
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:55 -07:00
Ahmet Alp Balkan
bf5b715798 Fix UnsupportedOp tests through custom comparer
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:55 -07:00
Ahmet Alp Balkan
a01db6ecde Re-introduce DEBUG env var stack traces
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:55 -07:00
Ahmet Alp Balkan
280fcec765 Update tests for homeDir and kubeconfigPath()
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:55 -07:00
Ahmet Alp Balkan
c82e299daa Unify errors from kubeconfig.Parse
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:55 -07:00
Ahmet Alp Balkan
5aaccdf801 Extract env vars to a file + test
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:55 -07:00
Ahmet Alp Balkan
936964dde5 Support for fzf, color ignore/force knobs
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:55 -07:00
Ahmet Alp Balkan
1f4eed962a Move all yaml logic to pkg/kubeconfig
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:55 -07:00
Ahmet Alp Balkan
492e3e7053 Move ctx-related YAML parse methods to pkg
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:55 -07:00
Ahmet Alp Balkan
7013899503 Use kubeconfig pkg for parsing utils
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:55 -07:00
Ahmet Alp Balkan
7f3e441ff2 kubeconfig pkg for loading/parsing
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:55 -07:00
Ahmet Alp Balkan
bb95141fc5 add printSuccess, pass writers to print funcs
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:55 -07:00
Ahmet Alp Balkan
28051b1fd7 define Run(stdout,stderr) method on **Ops
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:55 -07:00
Ahmet Alp Balkan
5b3796ba1c add some TODOs
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:55 -07:00
Ahmet Alp Balkan
1284b822a5 Fix bugs for test pass, update tests
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:55 -07:00
Ahmet Alp Balkan
cb103701ac Add support for renaming contexts
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:55 -07:00
Ahmet Alp Balkan
5eabeab47e Support for -d (deleting contexts)
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:55 -07:00
Ahmet Alp Balkan
c5f17b83e7 Add support for -u/--unset
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:55 -07:00
Ahmet Alp Balkan
94e8d3b4c7 Add support for -c/--current
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:55 -07:00
Ahmet Alp Balkan
ff6326c122 Integrate ctx swap, check for wrong ctx names
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:54 -07:00
Ahmet Alp Balkan
8aaefb8a94 Save last context name in state file
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:54 -07:00
Ahmet Alp Balkan
f51f8be7f9 Add utils for r/w ~/.kube/kubectx file
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:54 -07:00
Ahmet Alp Balkan
4fdd2898b7 Implement switch via editing yaml in-place
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:54 -07:00
Ahmet Alp Balkan
3c6fa48260 Implement context listing
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:54 -07:00
Ahmet Alp Balkan
b1afdbf375 Implement facilities to parse kubeconfig file
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:54 -07:00
Ahmet Alp Balkan
3fdc1855c0 Add logic to determine kubeconfig path
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:54 -07:00
Ahmet Alp Balkan
df8957403c Support help op, add color to error
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:54 -07:00
Ahmet Alp Balkan
eee1c23654 Handle supported operation in main
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:54 -07:00
Ahmet Alp Balkan
6c9273e582 Start porting to Go: parse flags
Parse help/list/swap command line flags.

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:52:54 -07:00
drorlevywork
37c765684f feat(completion): Improve kubens fish completions (#204)
This change covers all of the supported arguments and switches for the kubens command

Co-authored-by: Dror Levy <Dror@tablecheck.com>
2020-04-17 08:30:17 -07:00
Ahmet Alp Balkan
d0c9679d85 Update README.md 2020-04-07 14:30:29 -07:00
Ahmet Alp Balkan
e388bfa616 Remove ubuntu indication from install guide 2020-04-07 14:28:24 -07:00
Sébastien Maintrot
db8b706612 Update README about fzf and Unix composability (#197) 2020-03-08 10:24:24 -07:00
Rajat Jindal
06289683dd use v0.0.37 of krew-release-bot (#196)
use v0.0.37 of krew-release-bot
2020-02-29 08:51:38 -08:00
Ahmet Alp Balkan
d3295e5b7a Release v0.8.0 2020-02-20 15:12:06 -08:00
rob salmond
3369d42e2d add unset flag (#187)
* add unset flag

* test unsetting selected context

* update readme with new unset flag

* testdata notes

* set a current context

* cleanup

* omit fixture changes
2020-02-04 09:39:47 -08:00
Rajat Jindal
f48c4198e7 Add krew-release-bot support (#189)
* try github actions

* create release

* open pr using krew-release-bot
2020-01-22 11:30:36 -08:00
Ahmet Alp Balkan
26d3422917 Install option as a kubectl plugin (#182)
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2019-11-11 11:19:34 -08:00
Ahmet Alp Balkan
56e30d2b43 Detect invocation style only in usage() (#183)
- removes global SELF variable
- fixes #181

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2019-11-11 11:15:48 -08:00
Ahmet Alp Balkan
dcb43fdf1b Release v0.7.1 2019-11-09 16:46:53 -08:00
Ahmet Alp Balkan
e2f7dc0de2 Print plugin-friendly usage string
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2019-11-09 16:46:27 -08:00
Pedro Rodrigues
9645e5c62c Add zsh completion for kubectx subcommand (-d) (#178)
- Add basic completion for subcommand -d.
  Note: Kubectx will suggest all available contexts.

- References:
  - http://zsh.sourceforge.net/Doc/Release/Completion-System.html#Completion-System
  - https://github.com/zsh-users/zsh-completions/blob/master/zsh-completions-howto.org
2019-10-24 15:25:31 -07:00
Oliver Ford
00a1e12bfb Disable preview when fuzzy-finding (#163)
The user may have global settings that enable the preview pane in fzf.

Whatever the preview command is set as, it probably doesn't render
anything meaningful for kubens - I can't think what would be.

For kubectx, the context yaml itself would _maybe_ be helpful, but it
likely contains secrets, so I don't personally think I'd find it useful
enough to get into.

This commit thus disables the preview, so that if the user did have it
enabled, there's now no pane where there would previously have probably
been an error, such as:

    [bat error]: '<namespace>': No such file or directory (os error 2)
2019-10-11 12:14:32 -04:00
Eugene Aseev
c3dd1e5deb Add missing instruction for zsh on Linux (#173)
* Add missing instruction for zsh on Linux

* Add completion reloading to .zshrc
2019-09-09 08:13:33 -07:00
Ahmet Alp Balkan
a21638226f Release v0.7.0 2019-08-30 11:50:15 -07:00
Ahmet Alp Balkan
28e7c12f51 Introduce -c/--current options for kubectx/kubens (#171)
Per #127 the user community wants to have this feature, primarily as
-c and --current flags.

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2019-08-30 11:49:49 -07:00
ferhat elmas
1652420a15 Enable shellcheck and fix issues (#170) 2019-08-25 12:05:01 -07:00
Ed Vinyard
543e035090 fix typo in "Customizing colors" example (#166) 2019-08-10 11:28:51 -07:00
Nils Breunese
a5e810b837 Add install instructions for MacPorts users (#159) 2019-07-15 10:29:52 -07:00
Christian Rebischke
62f3f27889 changed instructions for arch linux (#152)
I have pushed kubectx to the official repositories.
Users can install it via `pacman` now :)

Signed-off-by: Christian Rebischke <chris@nullday.de>
2019-05-22 17:01:08 +02:00
Ahmet Alp Balkan
4258f03446 Add shields.io badges 2019-05-08 19:39:34 -07:00
Ahmet Alp Balkan
b9614bd2e0 kubectx rename check if old_name is a valid ctx (#139)
Without this safeguard, when user runs `kubectx NEW_NAME=OLD_NAME` where
NEW_NAME is an existing context but OLD_NAME isn't, we end up deleting NEW_NAME
and not doing any renames (because OLD_NAME is not found).

Fixes #136.

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2019-04-04 08:59:51 -07:00
Ahmet Alp Balkan
b3732b309e Update README.md 2019-02-06 10:20:55 -08:00
Tariq Ibrahim
a1bce92cc8 Fix alignment of kubectx help in USAGE text. (#129) 2019-02-04 14:21:45 -08:00
Tariq Ibrahim
1356c37cc0 Fix typos in readme doc. (#126)
* Fix typos in readme doc.

* fix typos
2019-01-30 12:20:37 -08:00
Ahmet Alp Balkan
10c9bd58ca v0.6.3
- FIX: Show current context/ns color in interactive (fzf) mode. (#109)
- TEST: Add integration tests for kubectx (#111, #113) and kubens (#105, #117)

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2019-01-28 10:11:52 -08:00
Jonathan Liuti
b6e918b084 Remove --with-short-names from doc (#120)
First measure to avoid confusing people.
see #112
2019-01-13 14:56:24 -08:00
Kumbirai Tanekha
402cc2c4b9 add cli tests for kubens (#117)
* split bats test invocation by executable

* add more cli tests for kubens

* clean up kubens tests

* small cleanup to kubens tests
2019-01-03 10:06:13 -08:00
Philippe MARTIN
df557e4fa7 Add more cli tests for kubectx (#113) 2019-01-02 09:47:01 -08:00
Ahmet Alp Balkan
b584d14f90 Show color in interactive mode (#109)
This patch introduces an internal _KUBECTX_FORCE_COLOR environment variable
that overrides color output decision.

With this, fzf output shows the color indicators for ctx/ns and choosing the
option with the color works without any extra handling.

Fixes #89.
Fixes #98.
2018-12-29 11:05:00 -08:00
Philippe MARTIN
acbf324464 test: Add more kubectx tests (#111) 2018-12-25 11:38:45 -08:00
Ahmet Alp Balkan
845f3b690b test: enable travis-ci with bats (#108)
- add .travis.yml.
- move bats fixtures to .bats extension, since it allows detection of test
files automatically by file extension.
- use BATS_TEST_DIRNAME variable to compute location of COMMAND.
- IMPORTANT: use `echo "$output">&2` before final check so that we can debug
  the test cases by their output

Ref #2.

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2018-12-22 13:22:08 -08:00
Philippe MARTIN
2b5bf4e429 Add simple tests for kubectx/kubens -h/--help (#105) 2018-12-22 13:02:20 -08:00
Eric Bailey
4a7d7cf025 README.md: add fish completion installation hint (#106)
This should work for most users anyway.
2018-12-21 20:30:46 -08:00
Ahmet Alp Balkan
dfeb7df363 Release v0.6.2 2018-11-26 12:23:47 -08:00
Rafael Bodill
407a84ce9e Support XDG_CACHE_HOME environment variable (#93) 2018-11-26 12:21:43 -08:00
Chad Metcalf
ec994aff89 Check for kubectl.exe on Windows (#96)
On the various flavors of bash for Windows kubectl won't resolve as the binary is kubectl.exe.
Simple aliasing doesn't seem to work. So test for both otherwise fail with a not found error.
2018-11-26 12:21:21 -08:00
Robert James Hernandez
3aeb4e76d2 fix subshell error handling (#95)
fixes #5
2018-11-07 09:27:34 -08:00
Ahmet Alp Balkan
517dae9fc8 Adding dependency checker for kubectx and kubens (#92)
Ensure kubectl in PATH for kubectx and kubens.
2018-10-22 10:10:08 -07:00
Robert James Hernandez
083e56f221 Ensure kubectl in PATH for kubens 2018-10-19 22:20:16 -07:00
Robert James Hernandez
6c94248e98 Ensure kubectl in PATH for kubectx 2018-10-19 22:20:03 -07:00
Gianpaolo Macario
244dd5b8a5 kubens: Fix typo in comment (#90) 2018-10-17 09:04:46 -07:00
Ahmet Alp Balkan
121f15d1d3 Update README.md 2018-10-16 20:15:36 -07:00
Ahmet Alp Balkan
21a1e1e963 Add ga-beacon 2018-10-16 20:15:01 -07:00
Mitchell Turner
6811a5f03c Added example of using bash completion (#86)
Added example using bash completion.

Example clones into `~/.kubectx` for people who don't want to make modifications outside of `$HOME`.
2018-09-16 23:31:35 -07:00
Ahmet Alp Balkan
41296a5fcf README: fix typo in ln cmd for zsh comp
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2018-09-05 15:26:17 -07:00
schnatterer
34a9e100c8 README: Install completion for zsh & Linux (#80) 2018-09-04 15:59:34 -07:00
Ahmet Alp Balkan
365fa23d87 zsh: fix kubectx completion for 2+ contexts (#81)
fixes #68

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2018-08-31 15:40:26 -07:00
Oliver
ccc077b6c5 allow disabling interactive mode with fzf (#82)
Introduce KUBECTX_IGNORE_FZF for both kubectx/kubens to force-disable
attempt to enable interactive mode and lookup for fzf(1).
2018-08-31 15:39:43 -07:00
Ahmet Alp Balkan
d931779c0c Release v0.6.1
- FIX: fix crash when kubectx/kubens is installed --with-short-names and fzf(1)
  is in PATH, but calling the binaries with the wrong name. (#78)
2018-08-24 09:28:10 -07:00
Ahmet Alp Balkan
f01719a5a6 fix: --with-short-names not compatible with fzf (#79)
When kubectx is installed as kctx, FZF_DEFAULT_COMMAND=kubectx won't work.

Fixes #78.

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2018-08-24 09:28:00 -07:00
Ahmet Alp Balkan
46d593305a Release v0.6.0
- FEATURE: interactive search mode when kubectx and kubens are ran without any
  arguments and fzf(1) is detected in PATH. (#71, #74)
- FIX: kubectx -d now doesn't ignore arguments after the first argument. (#75)
- FIX: empty output bug when TERM=vt100 even though NO_COLOR is set. (#57, #73)
- FIX: --help exits with code 0 now. (#69, #72)
2018-08-23 10:19:12 -07:00
Ahmet Alp Balkan
595c27ada7 fix: ignored args while deleting multiple clusters (#76)
Fixes #75.
2018-08-23 10:17:42 -07:00
Ahmet Alp Balkan
8df92316d6 add support for interactive selection with fzf (#74)
Present a fuzzy search choice in "kubectx" and "kubens" commands without
arguments.

![demo2](https://user-images.githubusercontent.com/159209/44478683-40f16d00-a5f3-11e8-99e2-f32f2a3539c1.gif)

Fixes #71.
2018-08-22 10:08:13 -07:00
Ahmet Alp Balkan
7b23263fc2 ignore errors from tput (to fix TERM=vt100) (#73)
Currently TERM=vt100 is causing kubectx failure since tput is returning
exitcode=1. vt100 does not have colors. Ignoring tput exit code.

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2018-08-22 09:50:34 -07:00
Vít Listík
e368d13eea help exit status 0 (#72) 2018-08-19 16:40:47 -07:00
Ahmet Alp Balkan
7bc9a1277c Release v0.5.1 2018-08-06 12:21:18 -07:00
Ahmet Alp Balkan
bc46739ab5 add note about NO_COLOR 2018-08-06 12:20:51 -07:00
Ahmet Alp Balkan
5a1366b7c9 Merge pull request #64 from idvoretskyi/patch-1
Debian package installation information added
2018-07-19 20:04:13 -07:00
Ihor Dvoretskyi
5144c0f958 Debian releases information added
Signed-off-by: Ihor Dvoretskyi <ihor@linux.com>
2018-07-19 23:03:25 +00:00
Ihor Dvoretskyi
11336883cb Debian releases information added
Signed-off-by: Ihor Dvoretskyi <ihor@linux.com>
2018-07-19 23:00:17 +00:00
Ihor Dvoretskyi
b1324570ad Debian releases information added
Signed-off-by: Ihor Dvoretskyi <ihor@linux.com>
2018-07-19 22:58:08 +00:00
Ihor Dvoretskyi
5f4160766d Debian package installation information added
As the package is currently available for Debian Buster and Sid, the brief installation information added.
2018-07-18 22:53:09 +03:00
Ahmet Alp Balkan
c606382a62 Respect $NO_COLOR
Fixes #54.

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2018-06-10 14:16:05 -07:00
Ahmet Alp Balkan
428fb0045c Merge pull request #53 from prabhu43/master
Configure highlight color for current context & namespace
2018-06-07 12:27:45 -07:00
Prabhu Jayakumar
a97ab8367d Update Readme 2018-06-08 00:35:45 +05:30
Prabhu Jayakumar
9beb1a1587 Configure highlight color for current context & namespace using environment variable 2018-06-05 23:29:08 +05:30
Prabhu Jayakumar
c23c2a9e29 Revert "Use * to indicate current context & current namespace"
This reverts commit dad48e5397.
2018-05-30 00:32:23 +05:30
Prabhu Jayakumar
dad48e5397 Use * to indicate current context & current namespace 2018-05-28 23:03:03 +05:30
Ahmet Alp Balkan
e9fbafc923 Merge pull request #46 from uesteibar/delete-multiple-contexts
Allow deleting multiple contexts at once
2018-05-25 11:06:09 -07:00
Ahmet Alp Balkan
b7af607a91 Merge pull request #48 from xnaveira/portable
Make the scripts portable
2018-05-23 09:26:23 -07:00
Xavier Naveira
bcb89389c6 Make the scripts portable 2018-05-23 17:09:54 +02:00
uesteibar
4a1d73d5fe Allow deleting multiple contexts at once
fixes #39

Example: `kubectx -d ctx1 ctx2`
2018-05-18 23:20:49 +02:00
Ahmet Alp Balkan
f986c148b2 Merge pull request #44 from fiws/patch-1
add Arch AUR note to the readme
2018-04-28 16:35:13 -07:00
Ahmet Alp Balkan
2799a9e331 Fix indentation to 80 chars, make title Arch Linux 2018-04-28 16:34:57 -07:00
Filip Weiss
d61654cb39 add Arch AUR note to the readme 2018-04-28 00:34:00 +02:00
Ahmet Alp Balkan
d1b69a995a Merge pull request #41 from ahmetb/tty_check
Do not print colors if stdout isn't tty
2018-04-17 13:17:10 -07:00
Ahmet Alp Balkan
4520d3f54b Do not print colors if stdout isn't tty
This is previously offered in #18, #22, and #27 and I previously rejected
these PRs. My rationale was to prevent scripting around kubectx as it is
meant to be used as an interactive tool.

But clearly that will be a problem, when you're doing operations like:

    kubectx -d $(kubectx)

which I proposed in #39. Plus this change is harmless. I think this
implementation does a better check than the previous ones.

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2018-04-10 21:53:14 -07:00
Ahmet Alp Balkan
d906013970 Fix readme.md
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2018-04-10 09:45:57 -07:00
Ahmet Alp Balkan
c4867bc1f2 Merge pull request #38 from ahmetb/delete-context
Prototype context deletion (kubectx -d)
2018-04-09 16:06:51 -07:00
Ahmet Alp Balkan
7b6528a4ae Prototype context deletion (kubectx -d)
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2018-04-04 12:40:23 -07:00
Ahmet Alp Balkan
07583efe3d Update -h message in README.md
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2018-04-04 12:32:37 -07:00
Ahmet Alp Balkan
cce04a3279 Merge pull request #37 from jvassev/fix-kubens
Allow for '/' in context name
2018-04-04 12:12:43 -07:00
Julian Vassev
d6f706a28e Allow for '/' in context name
Also, fix `kubens -` which fails to save previous namespace
after 6610d70
2018-04-04 11:42:04 -07:00
Ahmet Alp Balkan
0141d66224 Support "kubectx NAME=." to rename current-context
Fixes #24.

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2018-04-02 23:52:18 -07:00
Ahmet Alp Balkan
fbce3de6b9 Add overwriting to rename
rename now deletes the context (soft-delete, as in does not delete user or
cluster of the context) if the specified name exists.

Fixes #25.

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2018-04-02 23:27:51 -07:00
Ahmet Alp Balkan
6610d70ca8 separate local declarations and function calls
Suggested by SC2155 (https://github.com/koalaman/shellcheck/wiki/SC2155).

It captures some of the errors from kubectl that were previously described
in #5. However, this doesn't completely address that.

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2018-04-02 22:20:35 -07:00
Ahmet Alp Balkan
80336137bd Merge pull request #32 from shiroyasha/installation-steps-linux
Example installation steps on Linux
2018-03-30 10:26:18 -07:00
Ahmet Alp Balkan
1c9b5c54e8 Merge pull request #33 from jonmosco/utils_bash
Incorporate utils.bash into each executable to help simplify packaging for Linux and other platforms.  This adds to PR #32
2018-03-15 10:17:48 -07:00
Jon Mosco
b188f4da88 Incorporate utils.bash into each executable to help simplify packaging
for Linux and other platforms.  This adds to PR #32

remove unused functions
2018-03-15 12:34:18 -04:00
Igor Šarčević
57893cb668 Example installation steps on Linux 2018-03-10 13:17:09 +01:00
Ahmet Alp Balkan
8da629d98e Update README.md 2018-03-01 10:27:55 -08:00
Ahmet Alp Balkan
b2992aa0df Update readme to add fish completion support 2018-01-18 10:18:50 -08:00
Ahmet Alp Balkan
2f33693466 Merge pull request #26 from ichekrygin/master
Add Fish Shell Completions
2018-01-18 10:18:04 -08:00
Illya Chekrygin
c813642dc3 Add fish completions for kubectx and kubens 2018-01-17 23:23:53 -08:00
Ahmet Alp Balkan
12575f8ce4 Update README.md 2018-01-12 15:30:38 -08:00
Ahmet Alp Balkan
46d4236f1a Update README.md 2018-01-12 15:29:57 -08:00
Ahmet Alp Balkan
7ae68ada83 Update README.md 2018-01-02 14:58:33 -08:00
Ahmet Alp Balkan
146b67d2b0 Merge pull request #20 from knil-sama/knil-sama-readme-typo
Update README.md
2017-12-20 09:04:05 -08:00
Clement Demonchy
e3ceb14db5 Update README.md
Correct typo soemwhere -> somewhere
2017-12-20 14:16:28 +01:00
Ahmet Alp Balkan
6e250aecb6 Remove Formula/*, it's now in homebrew-core
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2017-12-14 15:21:10 -08:00
Ahmet Alp Balkan
d613c37c16 Update README.md 2017-12-12 10:34:58 -08:00
Ahmet Alp Balkan
61f1aa8fd7 Update README.md 2017-12-12 10:34:44 -08:00
Ahmet Alp Balkan
8390860474 Update README.md 2017-11-17 10:15:31 -08:00
Ahmet Alp Balkan
394288edc0 Update README.md 2017-11-15 12:49:33 -08:00
Ahmet Alp Balkan
dc6d2e57b3 README.md: Add user testimonials 2017-11-15 12:48:05 -08:00
Ahmet Alp Balkan
aa215b9eeb Update installation instructions for non-macOS 2017-11-15 12:41:03 -08:00
Ahmet Alp Balkan
e82879a185 Update README.md 2017-11-09 14:15:52 -08:00
Ahmet Alp Balkan
8c17834c36 Merge pull request #15 from bnookala/use-rename-context
Use rename-context in place of set-context
2017-11-09 14:15:23 -08:00
Bhargav Nookala
9ed6690978 🔧 use rename-context in place of set-context 2017-11-03 12:49:03 -07:00
Ahmet Alp Balkan
ed2afa7572 Add GIFs to README.md
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2017-10-24 12:32:22 -07:00
Ahmet Alp Balkan
5d79a0663d Remove message for '-' in kubectx.zsh
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2017-10-23 23:56:21 -07:00
Ahmet Alp Balkan
e7131add7a Add Stargazers chart
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2017-10-22 19:22:23 -07:00
Ahmet Alp Balkan
fd91e53d1f 🎉 kubectx is now in homebrew-core 🎊
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2017-06-29 14:12:46 -07:00
Ahmet Alp Balkan
b06fcaf054 Update Homebrew Formula with v0.3.1
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2017-05-17 10:09:10 -07:00
79 changed files with 5466 additions and 188 deletions

59
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,59 @@
# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Go implementation (CI)
on:
push:
pull_request:
jobs:
ci:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@master
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: '1.22'
- id: go-cache-paths
run: |
echo "::set-output name=go-build::$(go env GOCACHE)"
echo "::set-output name=go-mod::$(go env GOMODCACHE)"
- name: Go Build Cache
uses: actions/cache@v2
with:
path: ${{ steps.go-cache-paths.outputs.go-build }}
key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }}
- name: Go Mod Cache
uses: actions/cache@v2
with:
path: ${{ steps.go-cache-paths.outputs.go-mod }}
key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}
- name: Ensure gofmt
run: test -z "$(gofmt -s -d .)"
- name: Ensure go.mod is already tidied
run: go mod tidy && git diff --exit-code
- name: Run unit tests
run: go test ./...
- name: Build with Goreleaser
uses: goreleaser/goreleaser-action@v2
with:
version: latest
args: release --snapshot --skip publish,snapcraft --clean
- name: Setup BATS framework
run: sudo npm install -g bats
- name: kubectx (Go) integration tests
run: COMMAND=./dist/kubectx_linux_amd64_v1/kubectx bats test/kubectx.bats
- name: kubens (Go) integration tests
run: COMMAND=./dist/kubens_linux_amd64_v1/kubens bats test/kubens.bats

58
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Release
on:
push:
tags:
- 'v*.*.*'
jobs:
goreleaser:
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@master
- run: git fetch --tags
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: '1.22'
- name: Install Snapcraft
uses: samuelmeuli/action-snapcraft@v1
- name: Setup Snapcraft
run: |
# https://github.com/goreleaser/goreleaser/issues/1715
mkdir -p $HOME/.cache/snapcraft/download
mkdir -p $HOME/.cache/snapcraft/stage-packages
- name: GoReleaser
uses: goreleaser/goreleaser-action@v2
with:
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Update new version for plugin 'ctx' in krew-index
uses: rajatjindal/krew-release-bot@v0.0.38
with:
krew_template_file: .krew/ctx.yaml
- name: Update new version for plugin 'ns' in krew-index
uses: rajatjindal/krew-release-bot@v0.0.38
with:
krew_template_file: .krew/ns.yaml
- name: Publish Snaps to the Snap Store (stable channel)
run: for snap in $(ls dist/*.snap); do snapcraft upload --release=stable $snap; done
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}

121
.goreleaser.yml Normal file
View File

@@ -0,0 +1,121 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This is an example goreleaser.yaml file with some sane defaults.
# Make sure to check the documentation at http://goreleaser.com
version: 2
before:
hooks:
- go mod download
builds:
- id: kubectx
main: ./cmd/kubectx
binary: kubectx
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm
- arm64
- ppc64le
- s390x
goarm: [6, 7]
- id: kubens
main: ./cmd/kubens
binary: kubens
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm
- arm64
- ppc64le
- s390x
goarm: [6, 7]
archives:
- id: kubectx-archive
name_template: |-
kubectx_{{ .Tag }}_{{ .Os }}_
{{- with .Arch -}}
{{- if (eq . "386") -}}i386
{{- else if (eq . "amd64") -}}x86_64
{{- else -}}{{- . -}}
{{- end -}}
{{ end }}
{{- with .Arm -}}
{{- if (eq . "6") -}}hf
{{- else -}}v{{- . -}}
{{- end -}}
{{- end -}}
builds:
- kubectx
format_overrides:
- goos: windows
format: zip
files: ["LICENSE"]
- id: kubens-archive
name_template: |-
kubens_{{ .Tag }}_{{ .Os }}_
{{- with .Arch -}}
{{- if (eq . "386") -}}i386
{{- else if (eq . "amd64") -}}x86_64
{{- else -}}{{- . -}}
{{- end -}}
{{ end }}
{{- with .Arm -}}
{{- if (eq . "6") -}}hf
{{- else -}}v{{- . -}}
{{- end -}}
{{- end -}}
builds:
- kubens
format_overrides:
- goos: windows
format: zip
files: ["LICENSE"]
checksum:
name_template: "checksums.txt"
algorithm: sha256
release:
extra_files:
- glob: ./kubens
- glob: ./kubectx
snapcrafts:
- id: kubectx
name: kubectx
summary: 'kubectx + kubens: Power tools for kubectl'
description: |
kubectx is a tool to switch between contexts (clusters) on kubectl faster.
kubens is a tool to switch between Kubernetes namespaces (and configure them for kubectl) easily.
grade: stable
confinement: classic
base: core20
apps:
kubectx:
command: kubectx
completer: completion/kubectx.bash
kubens:
command: kubens
completer: completion/kubens.bash

31
.krew/ctx.yaml Normal file
View File

@@ -0,0 +1,31 @@
apiVersion: krew.googlecontainertools.github.com/v1alpha2
kind: Plugin
metadata:
name: ctx
spec:
homepage: https://github.com/ahmetb/kubectx
shortDescription: Switch between contexts in your kubeconfig
version: {{ .TagName }}
description: |
Also known as "kubectx", a utility to switch between context entries in
your kubeconfig file efficiently.
caveats: |
If fzf is installed on your machine, you can interactively choose
between the entries using the arrow keys, or by fuzzy searching
as you type.
See https://github.com/ahmetb/kubectx for customization and details.
platforms:
- selector:
matchExpressions:
- key: os
operator: In
values:
- darwin
- linux
{{addURIAndSha "https://github.com/ahmetb/kubectx/archive/{{ .TagName }}.tar.gz" .TagName }}
bin: kubectx
files:
- from: kubectx-*/kubectx
to: .
- from: kubectx-*/LICENSE
to: .

30
.krew/ns.yaml Normal file
View File

@@ -0,0 +1,30 @@
apiVersion: krew.googlecontainertools.github.com/v1alpha2
kind: Plugin
metadata:
name: ns
spec:
homepage: https://github.com/ahmetb/kubectx
shortDescription: Switch between Kubernetes namespaces
version: {{ .TagName }}
description: |
Also known as "kubens", a utility to set your current namespace and switch
between them.
caveats: |
If fzf is installed on your machine, you can interactively choose
between the entries using the arrow keys, or by fuzzy searching
as you type.
platforms:
- selector:
matchExpressions:
- key: os
operator: In
values:
- darwin
- linux
{{addURIAndSha "https://github.com/ahmetb/kubectx/archive/{{ .TagName }}.tar.gz" .TagName }}
bin: kubens
files:
- from: kubectx-*/kubens
to: .
- from: kubectx-*/LICENSE
to: .

View File

@@ -1,26 +0,0 @@
class Kubectx < Formula
desc "Tool that can switch between kubectl contexts easily and create aliases"
homepage "https://github.com/ahmetb/kubectx"
url "https://github.com/ahmetb/kubectx/archive/v0.3.0.tar.gz"
sha256 "720b6d9a960a7583983ac84c90d2ff89f4494584cf82a3abb5d4e66e72444621"
head "https://github.com/ahmetb/kubectx.git", :branch => "short-names"
bottle :unneeded
option "with-short-names", "link as \"kctx\" and \"kns\" instead"
def install
bin.install "kubectx" => build.with?("short-names") ? "kctx" : "kubectx"
bin.install "kubens" => build.with?("short-names") ? "kns" : "kubens"
include.install "utils.bash"
bash_completion.install "completion/kubectx.bash" => "kubectx"
bash_completion.install "completion/kubens.bash" => "kubens"
zsh_completion.install "completion/kubectx.zsh" => "_kubectx"
zsh_completion.install "completion/kubens.zsh" => "_kubens"
end
test do
system "which", build.with?("short-names") ? "kctx" : "kubectx"
system "which", build.with?("short-names") ? "kns" : "kubens"
end
end

331
README.md
View File

@@ -1,92 +1,311 @@
This repository provides both `kubectx` and `kubens` tools. Purpose of this
project is to provide an utility and facilitate discussion about how `kubectl`
can manage contexts better.
# `kubectx` + `kubens`: Power tools for kubectl
# kubectx(1)
![Latest GitHub release](https://img.shields.io/github/release/ahmetb/kubectx.svg)
![GitHub stars](https://img.shields.io/github/stars/ahmetb/kubectx.svg?label=github%20stars)
![Homebrew downloads](https://img.shields.io/homebrew/installs/dy/kubectx?label=macOS%20installs)
[![Go implementation (CI)](https://github.com/ahmetb/kubectx/workflows/Go%20implementation%20(CI)/badge.svg)](https://github.com/ahmetb/kubectx/actions?query=workflow%3A"Go+implementation+(CI)")
![Proudly written in Bash](https://img.shields.io/badge/written%20in-bash-ff69b4.svg)
kubectx is an utility to manage and switch between kubectl(1) contexts.
This repository provides both `kubectx` and `kubens` tools.
[Install &rarr;](#installation)
```
USAGE:
kubectx : list the contexts
kubectx <NAME> : switch to context
kubectx - : switch to the previous context
kubectx <NEW_NAME>=<NAME> : create alias for context
kubectx -h,--help : show this message
```
## What are `kubectx` and `kubens`?
Purpose of this project is to provide an utility and facilitate discussion
about how `kubectl` can manage contexts better.
**kubectx** is a tool to switch between contexts (clusters) on kubectl
faster.<br/>
**kubens** is a tool to switch between Kubernetes namespaces (and
configure them for kubectl) easily.
### Usage
Here's a **`kubectx`** demo:
![kubectx demo GIF](img/kubectx-demo.gif)
...and here's a **`kubens`** demo:
![kubens demo GIF](img/kubens-demo.gif)
### Examples
```sh
# switch to another cluster that's in kubeconfig
$ kubectx minikube
Switched to context "minikube".
# switch back to previous cluster
$ kubectx -
Switched to context "oregon".
$ kubectx -
Switched to context "minikube".
# rename context
$ kubectx dublin=gke_ahmetb_europe-west1-b_dublin
Context "dublin" set.
Aliased "gke_ahmetb_europe-west1-b_dublin" as "dublin".
```
Context "gke_ahmetb_europe-west1-b_dublin" renamed to "dublin".
`kubectx` supports <kbd>Tab</kbd> completion on bash/zsh shells to help with
long context names. You don't have to remember full context names anymore.
-----
# kubens(1)
kubens is an utility to switch between Kubernetes namespaces.
```
USAGE:
kubens : list the namespaces
kubens <NAME> : change the active namespace
kubens - : switch to the previous namespace
kubens -h,--help : show this message
```
### Usage
```sh
# change the active namespace on kubectl
$ kubens kube-system
Context "test" set.
Active namespace is "kube-system".
# go back to the previous namespace
$ kubens -
Context "test" set.
Active namespace is "default".
# change the active namespace even if it doesn't exist
$ kubens not-found-namespace --force
Context "test" set.
Active namespace is "not-found-namespace".
---
$ kubens not-found-namespace -f
Context "test" set.
Active namespace is "not-found-namespace".
```
`kubens` also supports <kbd>Tab</kbd> completion on bash/zsh shells.
If you have [`fzf`](https://github.com/junegunn/fzf) installed, you can also
**interactively** select a context or cluster, or fuzzy-search by typing a few
characters. To learn more, read [interactive mode &rarr;](#interactive-mode)
Both `kubectx` and `kubens` support <kbd>Tab</kbd> completion on bash/zsh/fish
shells to help with long context names. You don't have to remember full context
names anymore.
-----
## Installation
**For macOS:**
Stable versions of `kubectx` and `kubens` are small bash scripts that you
can find in this repository.
> Use [Homebrew](https://brew.sh/) package manager:
>
> brew tap ahmetb/kubectx https://github.com/ahmetb/kubectx.git
> brew install kubectx
> this will also set up bash/zsh completion scripts automatically.
Starting with v0.9.0, `kubectx` and `kubens` **are now rewritten in Go**. They
should work the same way (and we'll keep the bash-based implementations around)
but the new features will be added to the new Go programs. Please help us test
this new Go implementation by downloading the binaries from the [**Releases page
&rarr;**](https://github.com/ahmetb/kubectx/releases)
Running `brew install` with `--with-short-names` will install tools with names
`kctx` and `kns` to prevent prefix collision with `kubectl` name.
**Installation options:**
**Other platforms:**
- [as kubectl plugins (macOS & Linux)](#kubectl-plugins-macos-and-linux)
- [with Homebrew (macOS & Linux)](#homebrew-macos-and-linux)
- [with MacPorts (macOS)](#macports-macos)
- [with apt (Debian)](#apt-debian)
- [with pacman (Arch Linux)](#pacman-arch-linux)
- [with Chocolatey (Windows)](#windows-installation-using-chocolatey)
- [Windows Installation (using Scoop)](#windows-installation-using-scoop)
- [with winget (Windows)](#windows-installation-using-winget)
- [manually (macOS & Linux)](#manual-installation-macos-and-linux)
> Download the `kubectx` script, make it executable and add it to your PATH. You
> can also install bash/zsh [completion scripts](completion/) manually.
If you like to add context/namespace information to your shell prompt (`$PS1`),
you can try out [kube-ps1].
[kube-ps1]: https://github.com/jonmosco/kube-ps1
### Kubectl Plugins (macOS and Linux)
You can install and use the [Krew](https://github.com/kubernetes-sigs/krew/) kubectl
plugin manager to get `kubectx` and `kubens`.
**Note:** This will not install the shell completion scripts. If you want them,
*choose another installation method
or install the scripts [manually](#manual-installation-macos-and-linux).
```sh
kubectl krew install ctx
kubectl krew install ns
```
After installing, the tools will be available as `kubectl ctx` and `kubectl ns`.
### Homebrew (macOS and Linux)
If you use [Homebrew](https://brew.sh/) you can install like this:
```sh
brew install kubectx
```
This command will set up bash/zsh/fish completion scripts automatically. Make sure you [configure your shell](https://docs.brew.sh/Shell-Completion) to load completions for installed Homebrew formulas.
### MacPorts (macOS)
If you use [MacPorts](https://www.macports.org) you can install like this:
```sh
sudo port install kubectx
```
### apt (Debian)
``` bash
sudo apt install kubectx
```
Newer versions might be available on repos like
[Debian Buster (testing)](https://packages.debian.org/buster/kubectx),
[Sid (unstable)](https://packages.debian.org/sid/kubectx)
(_if you are unfamiliar with the Debian release process and how to enable
testing/unstable repos, check out the
[Debian Wiki](https://wiki.debian.org/DebianReleases)_):
### pacman (Arch Linux)
Available as official Arch Linux package. Install it via:
```bash
sudo pacman -S kubectx
```
### Windows Installation (using Chocolatey)
Available as packages on [Chocolatey](https://chocolatey.org/why-chocolatey)
```pwsh
choco install kubens kubectx
```
### Windows Installation (using Scoop)
Available as packages on [Scoop](https://scoop.sh/)
```pwsh
scoop bucket add main
scoop install main/kubens main/kubectx
```
### Windows Installation (using winget)
Available as packages on [winget](https://learn.microsoft.com/en-us/windows/package-manager/)
```pwsh
winget install --id ahmetb.kubectx
winget install --id ahmetb.kubens
```
### Manual Installation (macOS and Linux)
Since `kubectx` and `kubens` are written in Bash, you should be able to install
them to any POSIX environment that has Bash installed.
- Download the `kubectx`, and `kubens` scripts.
- Either:
- save them all to somewhere in your `PATH`,
- or save them to a directory, then create symlinks to `kubectx`/`kubens` from
somewhere in your `PATH`, like `/usr/local/bin`
- Make `kubectx` and `kubens` executable (`chmod +x ...`)
Example installation steps:
``` bash
sudo git clone https://github.com/ahmetb/kubectx /opt/kubectx
sudo ln -s /opt/kubectx/kubectx /usr/local/bin/kubectx
sudo ln -s /opt/kubectx/kubens /usr/local/bin/kubens
```
If you also want to have shell completions, pick an installation method for the
[completion scripts](completion/) that fits your system best: [`zsh` with
`antibody`](#completion-scripts-for-zsh-with-antibody), [plain
`zsh`](#completion-scripts-for-plain-zsh),
[`bash`](#completion-scripts-for-bash) or
[`fish`](#completion-scripts-for-fish).
#### Completion scripts for `zsh` with [antibody](https://getantibody.github.io)
Add this line to your [Plugins File](https://getantibody.github.io/usage/) (e.g.
`~/.zsh_plugins.txt`):
```
ahmetb/kubectx path:completion kind:fpath
```
Depending on your setup, you might or might not need to call `compinit` or
`autoload -U compinit && compinit` in your `~/.zshrc` after you load the Plugins
file. If you use [oh-my-zsh](https://github.com/ohmyzsh/ohmyzsh), load the
completions before you load `oh-my-zsh` because `oh-my-zsh` will call
`compinit`.
#### Completion scripts for plain `zsh`
The completion scripts have to be in a path that belongs to `$fpath`. Either
link or copy them to an existing folder.
Example with [`oh-my-zsh`](https://github.com/ohmyzsh/ohmyzsh):
```bash
mkdir -p ~/.oh-my-zsh/custom/completions
chmod -R 755 ~/.oh-my-zsh/custom/completions
ln -s /opt/kubectx/completion/_kubectx.zsh ~/.oh-my-zsh/custom/completions/_kubectx.zsh
ln -s /opt/kubectx/completion/_kubens.zsh ~/.oh-my-zsh/custom/completions/_kubens.zsh
echo "fpath=($ZSH/custom/completions $fpath)" >> ~/.zshrc
```
If completion doesn't work, add `autoload -U compinit && compinit` to your
`.zshrc` (similar to
[`zsh-completions`](https://github.com/zsh-users/zsh-completions/blob/master/README.md#oh-my-zsh)).
If you are not using [`oh-my-zsh`](https://github.com/ohmyzsh/ohmyzsh), you
could link to `/usr/share/zsh/functions/Completion` (might require sudo),
depending on the `$fpath` of your zsh installation.
In case of errors, calling `compaudit` might help.
#### Completion scripts for `bash`
```bash
git clone https://github.com/ahmetb/kubectx.git ~/.kubectx
COMPDIR=$(pkg-config --variable=completionsdir bash-completion)
ln -sf ~/.kubectx/completion/kubens.bash $COMPDIR/kubens
ln -sf ~/.kubectx/completion/kubectx.bash $COMPDIR/kubectx
cat << EOF >> ~/.bashrc
#kubectx and kubens
export PATH=~/.kubectx:\$PATH
EOF
```
#### Completion scripts for `fish`
```fish
mkdir -p ~/.config/fish/completions
ln -s /opt/kubectx/completion/kubectx.fish ~/.config/fish/completions/
ln -s /opt/kubectx/completion/kubens.fish ~/.config/fish/completions/
```
-----
Disclaimer: This is not an official Google product.
### Interactive mode
If you want `kubectx` and `kubens` commands to present you an interactive menu
with fuzzy searching, you just need to [install
`fzf`](https://github.com/junegunn/fzf) in your `$PATH`.
![kubectx interactive search with fzf](img/kubectx-interactive.gif)
If you have `fzf` installed, but want to opt out of using this feature, set the
environment variable `KUBECTX_IGNORE_FZF=1`.
If you want to keep `fzf` interactive mode but need the default behavior of the
command, you can do it by piping the output to another command (e.g. `kubectx |
cat `).
-----
### Customizing colors
If you like to customize the colors indicating the current namespace or context,
set the environment variables `KUBECTX_CURRENT_FGCOLOR` and
`KUBECTX_CURRENT_BGCOLOR` (refer color codes
[here](https://linux.101hacks.com/ps1-examples/prompt-color-using-tput/)):
```sh
export KUBECTX_CURRENT_FGCOLOR=$(tput setaf 6) # blue text
export KUBECTX_CURRENT_BGCOLOR=$(tput setab 7) # white background
```
Colors in the output can be disabled by setting the
[`NO_COLOR`](https://no-color.org/) environment variable.
-----
If you liked `kubectx`, you may like my
[`kubectl-aliases`](https://github.com/ahmetb/kubectl-aliases) project, too. I
recommend pairing kubectx and kubens with [fzf](#interactive-mode) and
[kube-ps1].
#### Stargazers over time
[![Stargazers over time](https://starchart.cc/ahmetb/kubectx.svg)](https://starchart.cc/ahmetb/kubectx)
![Google Analytics](https://ga-beacon.appspot.com/UA-2609286-17/kubectx/README?pixel) <!-- TODO broken since Aug 2021 as igrigorik left Google -->

42
cmd/kubectx/current.go Normal file
View File

@@ -0,0 +1,42 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"fmt"
"io"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/kubeconfig"
)
// CurrentOp prints the current context
type CurrentOp struct{}
func (_op CurrentOp) Run(stdout, _ io.Writer) error {
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
return errors.Wrap(err, "kubeconfig error")
}
v := kc.GetCurrentContext()
if v == "" {
return errors.New("current-context is not set")
}
_, err := fmt.Fprintln(stdout, v)
return errors.Wrap(err, "write error")
}

76
cmd/kubectx/delete.go Normal file
View File

@@ -0,0 +1,76 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"io"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer"
)
// DeleteOp indicates intention to delete contexts.
type DeleteOp struct {
Contexts []string // NAME or '.' to indicate current-context.
}
// deleteContexts deletes context entries one by one.
func (op DeleteOp) Run(_, stderr io.Writer) error {
for _, ctx := range op.Contexts {
// TODO inefficiency here. we open/write/close the same file many times.
deletedName, wasActiveContext, err := deleteContext(ctx)
if err != nil {
return errors.Wrapf(err, "error deleting context \"%s\"", deletedName)
}
if wasActiveContext {
printer.Warning(stderr, "You deleted the current context. Use \"%s\" to select a new context.",
selfName())
}
printer.Success(stderr, `Deleted context %s.`, printer.SuccessColor.Sprint(deletedName))
}
return nil
}
// deleteContext deletes a context entry by NAME or current-context
// indicated by ".".
func deleteContext(name string) (deleteName string, wasActiveContext bool, err error) {
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
return deleteName, false, errors.Wrap(err, "kubeconfig error")
}
cur := kc.GetCurrentContext()
// resolve "." to a real name
if name == "." {
if cur == "" {
return deleteName, false, errors.New("can't use '.' as the no active context is set")
}
wasActiveContext = true
name = cur
}
if !kc.ContextExists(name) {
return name, false, errors.New("context does not exist")
}
if err := kc.DeleteContextEntry(name); err != nil {
return name, false, errors.Wrap(err, "failed to modify yaml doc")
}
return name, wasActiveContext, errors.Wrap(kc.Save(), "failed to save modified kubeconfig file")
}

15
cmd/kubectx/env.go Normal file
View File

@@ -0,0 +1,15 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main

79
cmd/kubectx/flags.go Normal file
View File

@@ -0,0 +1,79 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"fmt"
"io"
"os"
"strings"
"github.com/ahmetb/kubectx/internal/cmdutil"
)
// UnsupportedOp indicates an unsupported flag.
type UnsupportedOp struct{ Err error }
func (op UnsupportedOp) Run(_, _ io.Writer) error {
return op.Err
}
// parseArgs looks at flags (excl. executable name, i.e. argv[0])
// and decides which operation should be taken.
func parseArgs(argv []string) Op {
if len(argv) == 0 {
if cmdutil.IsInteractiveMode(os.Stdout) {
return InteractiveSwitchOp{SelfCmd: os.Args[0]}
}
return ListOp{}
}
if argv[0] == "-d" {
if len(argv) == 1 {
if cmdutil.IsInteractiveMode(os.Stdout) {
return InteractiveDeleteOp{SelfCmd: os.Args[0]}
} else {
return UnsupportedOp{Err: fmt.Errorf("'-d' needs arguments")}
}
}
return DeleteOp{Contexts: argv[1:]}
}
if len(argv) == 1 {
v := argv[0]
if v == "--help" || v == "-h" {
return HelpOp{}
}
if v == "--version" || v == "-V" {
return VersionOp{}
}
if v == "--current" || v == "-c" {
return CurrentOp{}
}
if v == "--unset" || v == "-u" {
return UnsetOp{}
}
if new, old, ok := parseRenameSyntax(v); ok {
return RenameOp{New: new, Old: old}
}
if strings.HasPrefix(v, "-") && v != "-" {
return UnsupportedOp{Err: fmt.Errorf("unsupported option '%s'", v)}
}
return SwitchOp{Target: argv[0]}
}
return UnsupportedOp{Err: fmt.Errorf("too many arguments")}
}

98
cmd/kubectx/flags_test.go Normal file
View File

@@ -0,0 +1,98 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
)
func Test_parseArgs_new(t *testing.T) {
tests := []struct {
name string
args []string
want Op
}{
{name: "nil Args",
args: nil,
want: ListOp{}},
{name: "empty Args",
args: []string{},
want: ListOp{}},
{name: "help shorthand",
args: []string{"-h"},
want: HelpOp{}},
{name: "help long form",
args: []string{"--help"},
want: HelpOp{}},
{name: "current shorthand",
args: []string{"-c"},
want: CurrentOp{}},
{name: "current long form",
args: []string{"--current"},
want: CurrentOp{}},
{name: "unset shorthand",
args: []string{"-u"},
want: UnsetOp{}},
{name: "unset long form",
args: []string{"--unset"},
want: UnsetOp{}},
{name: "switch by name",
args: []string{"foo"},
want: SwitchOp{Target: "foo"}},
{name: "switch by swap",
args: []string{"-"},
want: SwitchOp{Target: "-"}},
{name: "delete - without contexts",
args: []string{"-d"},
want: UnsupportedOp{fmt.Errorf("'-d' needs arguments")}},
{name: "delete - current context",
args: []string{"-d", "."},
want: DeleteOp{[]string{"."}}},
{name: "delete - multiple contexts",
args: []string{"-d", ".", "a", "b"},
want: DeleteOp{[]string{".", "a", "b"}}},
{name: "rename context",
args: []string{"a=b"},
want: RenameOp{"a", "b"}},
{name: "rename context with old=current",
args: []string{"a=."},
want: RenameOp{"a", "."}},
{name: "unrecognized flag",
args: []string{"-x"},
want: UnsupportedOp{Err: fmt.Errorf("unsupported option '-x'")}},
{name: "too many args",
args: []string{"a", "b", "c"},
want: UnsupportedOp{Err: fmt.Errorf("too many arguments")}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseArgs(tt.args)
var opts cmp.Options
if _, ok := tt.want.(UnsupportedOp); ok {
opts = append(opts, cmp.Comparer(func(x, y UnsupportedOp) bool {
return (x.Err == nil && y.Err == nil) || (x.Err.Error() == y.Err.Error())
}))
}
if diff := cmp.Diff(got, tt.want, opts...); diff != "" {
t.Errorf("parseArgs(%#v) diff: %s", tt.args, diff)
}
})
}
}

128
cmd/kubectx/fzf.go Normal file
View File

@@ -0,0 +1,128 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"strings"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/env"
"github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer"
)
type InteractiveSwitchOp struct {
SelfCmd string
}
type InteractiveDeleteOp struct {
SelfCmd string
}
func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error {
// parse kubeconfig just to see if it can be loaded
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
if err := kc.Parse(); err != nil {
if cmdutil.IsNotFoundErr(err) {
printer.Warning(stderr, "kubeconfig file not found")
return nil
}
return errors.Wrap(err, "kubeconfig error")
}
kc.Close()
cmd := exec.Command("fzf", "--ansi", "--no-preview")
var out bytes.Buffer
cmd.Stdin = os.Stdin
cmd.Stderr = stderr
cmd.Stdout = &out
cmd.Env = append(os.Environ(),
fmt.Sprintf("FZF_DEFAULT_COMMAND=%s", op.SelfCmd),
fmt.Sprintf("%s=1", env.EnvForceColor))
if err := cmd.Run(); err != nil {
if _, ok := err.(*exec.ExitError); !ok {
return err
}
}
choice := strings.TrimSpace(out.String())
if choice == "" {
return errors.New("you did not choose any of the options")
}
name, err := switchContext(choice)
if err != nil {
return errors.Wrap(err, "failed to switch context")
}
printer.Success(stderr, "Switched to context \"%s\".", printer.SuccessColor.Sprint(name))
return nil
}
func (op InteractiveDeleteOp) Run(_, stderr io.Writer) error {
// parse kubeconfig just to see if it can be loaded
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
if err := kc.Parse(); err != nil {
if cmdutil.IsNotFoundErr(err) {
printer.Warning(stderr, "kubeconfig file not found")
return nil
}
return errors.Wrap(err, "kubeconfig error")
}
kc.Close()
if len(kc.ContextNames()) == 0 {
return errors.New("no contexts found in config")
}
cmd := exec.Command("fzf", "--ansi", "--no-preview")
var out bytes.Buffer
cmd.Stdin = os.Stdin
cmd.Stderr = stderr
cmd.Stdout = &out
cmd.Env = append(os.Environ(),
fmt.Sprintf("FZF_DEFAULT_COMMAND=%s", op.SelfCmd),
fmt.Sprintf("%s=1", env.EnvForceColor))
if err := cmd.Run(); err != nil {
if _, ok := err.(*exec.ExitError); !ok {
return err
}
}
choice := strings.TrimSpace(out.String())
if choice == "" {
return errors.New("you did not choose any of the options")
}
name, wasActiveContext, err := deleteContext(choice)
if err != nil {
return errors.Wrap(err, "failed to delete context")
}
if wasActiveContext {
printer.Warning(stderr, "You deleted the current context. Use \"%s\" to select a new context.",
selfName())
}
printer.Success(stderr, `Deleted context %s.`, printer.SuccessColor.Sprint(name))
return nil
}

63
cmd/kubectx/help.go Normal file
View File

@@ -0,0 +1,63 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
)
// HelpOp describes printing help.
type HelpOp struct{}
func (_ HelpOp) Run(stdout, _ io.Writer) error {
return printUsage(stdout)
}
func printUsage(out io.Writer) error {
help := `USAGE:
%PROG% : list the contexts
%PROG% <NAME> : switch to context <NAME>
%PROG% - : switch to the previous context
%PROG% -c, --current : show the current context name
%PROG% <NEW_NAME>=<NAME> : rename context <NAME> to <NEW_NAME>
%PROG% <NEW_NAME>=. : rename current-context to <NEW_NAME>
%PROG% -u, --unset : unset the current context
%PROG% -d <NAME> [<NAME...>] : delete context <NAME> ('.' for current-context)
%SPAC% (this command won't delete the user/cluster entry
%SPAC% referenced by the context entry)
%PROG% -h,--help : show this message
%PROG% -V,--version : show version`
help = strings.ReplaceAll(help, "%PROG%", selfName())
help = strings.ReplaceAll(help, "%SPAC%", strings.Repeat(" ", len(selfName())))
_, err := fmt.Fprintf(out, "%s\n", help)
return errors.Wrap(err, "write error")
}
// selfName guesses how the user invoked the program.
func selfName() string {
me := filepath.Base(os.Args[0])
pluginPrefix := "kubectl-"
if strings.HasPrefix(me, pluginPrefix) {
return "kubectl " + strings.TrimPrefix(me, pluginPrefix)
}
return "kubectx"
}

37
cmd/kubectx/help_test.go Normal file
View File

@@ -0,0 +1,37 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"bytes"
"strings"
"testing"
)
func TestPrintHelp(t *testing.T) {
var buf bytes.Buffer
if err := (&HelpOp{}).Run(&buf, &buf); err != nil {
t.Fatal(err)
}
out := buf.String()
if !strings.Contains(out, "USAGE:") {
t.Errorf("help string doesn't contain USAGE: ; output=\"%s\"", out)
}
if !strings.HasSuffix(out, "\n") {
t.Errorf("does not end with New line; output=\"%s\"", out)
}
}

55
cmd/kubectx/list.go Normal file
View File

@@ -0,0 +1,55 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"fmt"
"io"
"facette.io/natsort"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer"
)
// ListOp describes listing contexts.
type ListOp struct{}
func (_ ListOp) Run(stdout, stderr io.Writer) error {
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
if cmdutil.IsNotFoundErr(err) {
printer.Warning(stderr, "kubeconfig file not found")
return nil
}
return errors.Wrap(err, "kubeconfig error")
}
ctxs := kc.ContextNames()
natsort.Sort(ctxs)
cur := kc.GetCurrentContext()
for _, c := range ctxs {
s := c
if c == cur {
s = printer.ActiveItemColor.Sprint(c)
}
fmt.Fprintf(stdout, "%s\n", s)
}
return nil
}

45
cmd/kubectx/main.go Normal file
View File

@@ -0,0 +1,45 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"fmt"
"io"
"os"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/env"
"github.com/ahmetb/kubectx/internal/printer"
"github.com/fatih/color"
)
type Op interface {
Run(stdout, stderr io.Writer) error
}
func main() {
cmdutil.PrintDeprecatedEnvWarnings(color.Error, os.Environ())
op := parseArgs(os.Args[1:])
if err := op.Run(color.Output, color.Error); err != nil {
printer.Error(color.Error, err.Error())
if _, ok := os.LookupEnv(env.EnvDebug); ok {
// print stack trace in verbose mode
fmt.Fprintf(color.Error, "[DEBUG] error: %+v\n", err)
}
defer os.Exit(1)
}
}

88
cmd/kubectx/rename.go Normal file
View File

@@ -0,0 +1,88 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"io"
"strings"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer"
)
// RenameOp indicates intention to rename contexts.
type RenameOp struct {
New string // NAME of New context
Old string // NAME of Old context (or '.' for current-context)
}
// parseRenameSyntax parses A=B form into [A,B] and returns
// whether it is parsed correctly.
func parseRenameSyntax(v string) (string, string, bool) {
s := strings.Split(v, "=")
if len(s) != 2 {
return "", "", false
}
new, old := s[0], s[1]
if new == "" || old == "" {
return "", "", false
}
return new, old, true
}
// rename changes the old (NAME or '.' for current-context)
// to the "new" value. If the old refers to the current-context,
// current-context preference is also updated.
func (op RenameOp) Run(_, stderr io.Writer) error {
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
return errors.Wrap(err, "kubeconfig error")
}
cur := kc.GetCurrentContext()
if op.Old == "." {
op.Old = cur
}
if !kc.ContextExists(op.Old) {
return errors.Errorf("context \"%s\" not found, can't rename it", op.Old)
}
if kc.ContextExists(op.New) {
printer.Warning(stderr, "context \"%s\" exists, overwriting it.", op.New)
if err := kc.DeleteContextEntry(op.New); err != nil {
return errors.Wrap(err, "failed to delete new context to overwrite it")
}
}
if err := kc.ModifyContextName(op.Old, op.New); err != nil {
return errors.Wrap(err, "failed to change context name")
}
if op.Old == cur {
if err := kc.ModifyCurrentContext(op.New); err != nil {
return errors.Wrap(err, "failed to set current-context to new name")
}
}
if err := kc.Save(); err != nil {
return errors.Wrap(err, "failed to save modified kubeconfig")
}
printer.Success(stderr, "Context %s renamed to %s.",
printer.SuccessColor.Sprint(op.Old),
printer.SuccessColor.Sprint(op.New))
return nil
}

View File

@@ -0,0 +1,83 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"testing"
"github.com/google/go-cmp/cmp"
)
func Test_parseRenameSyntax(t *testing.T) {
type out struct {
New string
Old string
OK bool
}
tests := []struct {
name string
in string
want out
}{
{
name: "no equals sign",
in: "foo",
want: out{OK: false},
},
{
name: "no left side",
in: "=a",
want: out{OK: false},
},
{
name: "no right side",
in: "a=",
want: out{OK: false},
},
{
name: "correct format",
in: "a=b",
want: out{
New: "a",
Old: "b",
OK: true,
},
},
{
name: "correct format with current context",
in: "NEW_NAME=.",
want: out{
New: "NEW_NAME",
Old: ".",
OK: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
new, old, ok := parseRenameSyntax(tt.in)
got := out{
New: new,
Old: old,
OK: ok,
}
diff := cmp.Diff(tt.want, got)
if diff != "" {
t.Errorf("parseRenameSyntax() diff=%s", diff)
}
})
}
}

53
cmd/kubectx/state.go Normal file
View File

@@ -0,0 +1,53 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"io/ioutil"
"os"
"path/filepath"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
)
func kubectxPrevCtxFile() (string, error) {
home := cmdutil.HomeDir()
if home == "" {
return "", errors.New("HOME or USERPROFILE environment variable not set")
}
return filepath.Join(home, ".kube", "kubectx"), nil
}
// readLastContext returns the saved previous context
// if the state file exists, otherwise returns "".
func readLastContext(path string) (string, error) {
b, err := ioutil.ReadFile(path)
if os.IsNotExist(err) {
return "", nil
}
return string(b), err
}
// writeLastContext saves the specified value to the state file.
// It creates missing parent directories.
func writeLastContext(path, value string) error {
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
return errors.Wrap(err, "failed to create parent directories")
}
return ioutil.WriteFile(path, []byte(value), 0644)
}

104
cmd/kubectx/state_test.go Normal file
View File

@@ -0,0 +1,104 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/ahmetb/kubectx/internal/testutil"
)
func Test_readLastContext_nonExistingFile(t *testing.T) {
s, err := readLastContext(filepath.FromSlash("/non/existing/file"))
if err != nil {
t.Fatal(err)
}
if s != "" {
t.Fatalf("expected empty string; got=\"%s\"", s)
}
}
func Test_readLastContext(t *testing.T) {
path, cleanup := testutil.TempFile(t, "foo")
defer cleanup()
s, err := readLastContext(path)
if err != nil {
t.Fatal(err)
}
if expected := "foo"; s != expected {
t.Fatalf("expected=\"%s\"; got=\"%s\"", expected, s)
}
}
func Test_writeLastContext_err(t *testing.T) {
path := filepath.Join(os.DevNull, "foo", "bar")
err := writeLastContext(path, "foo")
if err == nil {
t.Fatal("got empty error")
}
}
func Test_writeLastContext(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "state-file-test")
if err != nil {
t.Fatal(err)
}
path := filepath.Join(dir, "foo", "bar")
if err := writeLastContext(path, "ctx1"); err != nil {
t.Fatal(err)
}
v, err := readLastContext(path)
if err != nil {
t.Fatal(err)
}
if expected := "ctx1"; v != expected {
t.Fatalf("read wrong value=\"%s\"; expected=\"%s\"", v, expected)
}
}
func Test_kubectxFilePath(t *testing.T) {
origHome := os.Getenv("HOME")
os.Setenv("HOME", filepath.FromSlash("/foo/bar"))
defer os.Setenv("HOME", origHome)
expected := filepath.Join(filepath.FromSlash("/foo/bar"), ".kube", "kubectx")
v, err := kubectxPrevCtxFile()
if err != nil {
t.Fatal(err)
}
if v != expected {
t.Fatalf("expected=\"%s\" got=\"%s\"", expected, v)
}
}
func Test_kubectxFilePath_error(t *testing.T) {
origHome := os.Getenv("HOME")
origUserprofile := os.Getenv("USERPROFILE")
os.Unsetenv("HOME")
os.Unsetenv("USERPROFILE")
defer os.Setenv("HOME", origHome)
defer os.Setenv("USERPROFILE", origUserprofile)
_, err := kubectxPrevCtxFile()
if err == nil {
t.Fatal(err)
}
}

92
cmd/kubectx/switch.go Normal file
View File

@@ -0,0 +1,92 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"io"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer"
)
// SwitchOp indicates intention to switch contexts.
type SwitchOp struct {
Target string // '-' for back and forth, or NAME
}
func (op SwitchOp) Run(_, stderr io.Writer) error {
var newCtx string
var err error
if op.Target == "-" {
newCtx, err = swapContext()
} else {
newCtx, err = switchContext(op.Target)
}
if err != nil {
return errors.Wrap(err, "failed to switch context")
}
err = printer.Success(stderr, "Switched to context \"%s\".", printer.SuccessColor.Sprint(newCtx))
return errors.Wrap(err, "print error")
}
// switchContext switches to specified context name.
func switchContext(name string) (string, error) {
prevCtxFile, err := kubectxPrevCtxFile()
if err != nil {
return "", errors.Wrap(err, "failed to determine state file")
}
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
return "", errors.Wrap(err, "kubeconfig error")
}
prev := kc.GetCurrentContext()
if !kc.ContextExists(name) {
return "", errors.Errorf("no context exists with the name: \"%s\"", name)
}
if err := kc.ModifyCurrentContext(name); err != nil {
return "", err
}
if err := kc.Save(); err != nil {
return "", errors.Wrap(err, "failed to save kubeconfig")
}
if prev != name {
if err := writeLastContext(prevCtxFile, prev); err != nil {
return "", errors.Wrap(err, "failed to save previous context name")
}
}
return name, nil
}
// swapContext switches to previously switch context.
func swapContext() (string, error) {
prevCtxFile, err := kubectxPrevCtxFile()
if err != nil {
return "", errors.Wrap(err, "failed to determine state file")
}
prev, err := readLastContext(prevCtxFile)
if err != nil {
return "", errors.Wrap(err, "failed to read previous context file")
}
if prev == "" {
return "", errors.New("no previous context found")
}
return switchContext(prev)
}

45
cmd/kubectx/unset.go Normal file
View File

@@ -0,0 +1,45 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"io"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer"
)
// UnsetOp indicates intention to remove current-context preference.
type UnsetOp struct{}
func (_ UnsetOp) Run(_, stderr io.Writer) error {
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
return errors.Wrap(err, "kubeconfig error")
}
if err := kc.UnsetCurrentContext(); err != nil {
return errors.Wrap(err, "error while modifying current-context")
}
if err := kc.Save(); err != nil {
return errors.Wrap(err, "failed to save kubeconfig file after modification")
}
err := printer.Success(stderr, "Active context unset for kubectl.")
return errors.Wrap(err, "write error")
}

20
cmd/kubectx/version.go Normal file
View File

@@ -0,0 +1,20 @@
package main
import (
"fmt"
"io"
"github.com/pkg/errors"
)
var (
version = "v0.0.0+unknown" // populated by goreleaser
)
// VersionOp describes printing version string.
type VersionOp struct{}
func (_ VersionOp) Run(stdout, _ io.Writer) error {
_, err := fmt.Fprintf(stdout, "%s\n", version)
return errors.Wrap(err, "write error")
}

45
cmd/kubens/current.go Normal file
View File

@@ -0,0 +1,45 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"fmt"
"io"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/kubeconfig"
)
type CurrentOp struct{}
func (c CurrentOp) Run(stdout, _ io.Writer) error {
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
return errors.Wrap(err, "kubeconfig error")
}
ctx := kc.GetCurrentContext()
if ctx == "" {
return errors.New("current-context is not set")
}
ns, err := kc.NamespaceOfContext(ctx)
if err != nil {
return errors.Wrapf(err, "failed to read namespace of \"%s\"", ctx)
}
_, err = fmt.Fprintln(stdout, ns)
return errors.Wrap(err, "write error")
}

84
cmd/kubens/flags.go Normal file
View File

@@ -0,0 +1,84 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"fmt"
"io"
"os"
"slices"
"strings"
"github.com/ahmetb/kubectx/internal/cmdutil"
)
// UnsupportedOp indicates an unsupported flag.
type UnsupportedOp struct{ Err error }
func (op UnsupportedOp) Run(_, _ io.Writer) error {
return op.Err
}
// parseArgs looks at flags (excl. executable name, i.e. argv[0])
// and decides which operation should be taken.
func parseArgs(argv []string) Op {
n := len(argv)
if n == 0 {
if cmdutil.IsInteractiveMode(os.Stdout) {
return InteractiveSwitchOp{SelfCmd: os.Args[0]}
}
return ListOp{}
}
if n == 1 {
v := argv[0]
switch v {
case "--help", "-h":
return HelpOp{}
case "--version", "-V":
return VersionOp{}
case "--current", "-c":
return CurrentOp{}
default:
return getSwitchOp(v, false)
}
} else if n == 2 {
// {namespace} -f|--force
name := argv[0]
force := slices.Contains([]string{"-f", "--force"}, argv[1])
if !force {
if !slices.Contains([]string{"-f", "--force"}, argv[0]) {
return UnsupportedOp{Err: fmt.Errorf("unsupported arguments %q", argv)}
}
// -f|--force {namespace}
force = true
name = argv[1]
}
return getSwitchOp(name, force)
}
return UnsupportedOp{Err: fmt.Errorf("too many arguments")}
}
func getSwitchOp(v string, force bool) Op {
if strings.HasPrefix(v, "-") && v != "-" {
return UnsupportedOp{Err: fmt.Errorf("unsupported option %q", v)}
}
return SwitchOp{Target: v, Force: force}
}

95
cmd/kubens/flags_test.go Normal file
View File

@@ -0,0 +1,95 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
)
func Test_parseArgs_new(t *testing.T) {
tests := []struct {
name string
args []string
want Op
}{
{name: "nil Args",
args: nil,
want: ListOp{}},
{name: "empty Args",
args: []string{},
want: ListOp{}},
{name: "help shorthand",
args: []string{"-h"},
want: HelpOp{}},
{name: "help long form",
args: []string{"--help"},
want: HelpOp{}},
{name: "current shorthand",
args: []string{"-c"},
want: CurrentOp{}},
{name: "current long form",
args: []string{"--current"},
want: CurrentOp{}},
{name: "switch by name",
args: []string{"foo"},
want: SwitchOp{Target: "foo"}},
{name: "switch by name force short flag",
args: []string{"foo", "-f"},
want: SwitchOp{Target: "foo", Force: true}},
{name: "switch by name force long flag",
args: []string{"foo", "--force"},
want: SwitchOp{Target: "foo", Force: true}},
{name: "switch by name force short flag before name",
args: []string{"-f", "foo"},
want: SwitchOp{Target: "foo", Force: true}},
{name: "switch by name force long flag before name",
args: []string{"--force", "foo"},
want: SwitchOp{Target: "foo", Force: true}},
{name: "switch by name unknown arguments",
args: []string{"foo", "-x"},
want: UnsupportedOp{Err: fmt.Errorf("unsupported arguments %q", []string{"foo", "-x"})}},
{name: "switch by name unknown arguments",
args: []string{"-x", "foo"},
want: UnsupportedOp{Err: fmt.Errorf("unsupported arguments %q", []string{"-x", "foo"})}},
{name: "switch by swap",
args: []string{"-"},
want: SwitchOp{Target: "-"}},
{name: "unrecognized flag",
args: []string{"-x"},
want: UnsupportedOp{Err: fmt.Errorf("unsupported option %q", "-x")}},
{name: "too many args",
args: []string{"a", "b", "c"},
want: UnsupportedOp{Err: fmt.Errorf("too many arguments")}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseArgs(tt.args)
var opts cmp.Options
if _, ok := tt.want.(UnsupportedOp); ok {
opts = append(opts, cmp.Comparer(func(x, y UnsupportedOp) bool {
return (x.Err == nil && y.Err == nil) || (x.Err.Error() == y.Err.Error())
}))
}
if diff := cmp.Diff(got, tt.want, opts...); diff != "" {
t.Errorf("parseArgs(%#v) diff: %s", tt.args, diff)
}
})
}
}

74
cmd/kubens/fzf.go Normal file
View File

@@ -0,0 +1,74 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"strings"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/env"
"github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer"
)
type InteractiveSwitchOp struct {
SelfCmd string
}
// TODO(ahmetb) This method is heavily repetitive vs kubectx/fzf.go.
func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error {
// parse kubeconfig just to see if it can be loaded
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
if err := kc.Parse(); err != nil {
if cmdutil.IsNotFoundErr(err) {
printer.Warning(stderr, "kubeconfig file not found")
return nil
}
return errors.Wrap(err, "kubeconfig error")
}
defer kc.Close()
cmd := exec.Command("fzf", "--ansi", "--no-preview")
var out bytes.Buffer
cmd.Stdin = os.Stdin
cmd.Stderr = stderr
cmd.Stdout = &out
cmd.Env = append(os.Environ(),
fmt.Sprintf("FZF_DEFAULT_COMMAND=%s", op.SelfCmd),
fmt.Sprintf("%s=1", env.EnvForceColor))
if err := cmd.Run(); err != nil {
if _, ok := err.(*exec.ExitError); !ok {
return err
}
}
choice := strings.TrimSpace(out.String())
if choice == "" {
return errors.New("you did not choose any of the options")
}
name, err := switchNamespace(kc, choice, false)
if err != nil {
return errors.Wrap(err, "failed to switch namespace")
}
printer.Success(stderr, "Active namespace is \"%s\".", printer.SuccessColor.Sprint(name))
return nil
}

60
cmd/kubens/help.go Normal file
View File

@@ -0,0 +1,60 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
)
// HelpOp describes printing help.
type HelpOp struct{}
func (_ HelpOp) Run(stdout, _ io.Writer) error {
return printUsage(stdout)
}
func printUsage(out io.Writer) error {
help := `USAGE:
%PROG% : list the namespaces in the current context
%PROG% <NAME> : change the active namespace of current context
%PROG% <NAME> --force/-f : force change the active namespace of current context (even if it doesn't exist)
%PROG% - : switch to the previous namespace in this context
%PROG% -c, --current : show the current namespace
%PROG% -h,--help : show this message
%PROG% -V,--version : show version`
// TODO this replace logic is duplicated between this and kubectx
help = strings.ReplaceAll(help, "%PROG%", selfName())
_, err := fmt.Fprintf(out, "%s\n", help)
return errors.Wrap(err, "write error")
}
// selfName guesses how the user invoked the program.
func selfName() string {
// TODO this method is duplicated between this and kubectx
me := filepath.Base(os.Args[0])
pluginPrefix := "kubectl-"
if strings.HasPrefix(me, pluginPrefix) {
return "kubectl " + strings.TrimPrefix(me, pluginPrefix)
}
return "kubens"
}

109
cmd/kubens/list.go Normal file
View File

@@ -0,0 +1,109 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"context"
"fmt"
"io"
"os"
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
_ "k8s.io/client-go/plugin/pkg/client/auth"
"k8s.io/client-go/tools/clientcmd"
"github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer"
)
type ListOp struct{}
func (op ListOp) Run(stdout, stderr io.Writer) error {
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
return errors.Wrap(err, "kubeconfig error")
}
ctx := kc.GetCurrentContext()
if ctx == "" {
return errors.New("current-context is not set")
}
curNs, err := kc.NamespaceOfContext(ctx)
if err != nil {
return errors.Wrap(err, "cannot read current namespace")
}
ns, err := queryNamespaces(kc)
if err != nil {
return errors.Wrap(err, "could not list namespaces (is the cluster accessible?)")
}
for _, c := range ns {
s := c
if c == curNs {
s = printer.ActiveItemColor.Sprint(c)
}
fmt.Fprintf(stdout, "%s\n", s)
}
return nil
}
func queryNamespaces(kc *kubeconfig.Kubeconfig) ([]string, error) {
if os.Getenv("_MOCK_NAMESPACES") != "" {
return []string{"ns1", "ns2"}, nil
}
clientset, err := newKubernetesClientSet(kc)
if err != nil {
return nil, errors.Wrap(err, "failed to initialize k8s REST client")
}
var out []string
var next string
for {
list, err := clientset.CoreV1().Namespaces().List(
context.Background(),
metav1.ListOptions{
Limit: 500,
Continue: next,
})
if err != nil {
return nil, errors.Wrap(err, "failed to list namespaces from k8s API")
}
next = list.Continue
for _, it := range list.Items {
out = append(out, it.Name)
}
if next == "" {
break
}
}
return out, nil
}
func newKubernetesClientSet(kc *kubeconfig.Kubeconfig) (*kubernetes.Clientset, error) {
b, err := kc.Bytes()
if err != nil {
return nil, errors.Wrap(err, "failed to convert in-memory kubeconfig to yaml")
}
cfg, err := clientcmd.RESTConfigFromKubeConfig(b)
if err != nil {
return nil, errors.Wrap(err, "failed to initialize config")
}
return kubernetes.NewForConfig(cfg)
}

44
cmd/kubens/main.go Normal file
View File

@@ -0,0 +1,44 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"fmt"
"io"
"os"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/env"
"github.com/ahmetb/kubectx/internal/printer"
"github.com/fatih/color"
)
type Op interface {
Run(stdout, stderr io.Writer) error
}
func main() {
cmdutil.PrintDeprecatedEnvWarnings(color.Error, os.Environ())
op := parseArgs(os.Args[1:])
if err := op.Run(color.Output, color.Error); err != nil {
printer.Error(color.Error, err.Error())
if _, ok := os.LookupEnv(env.EnvDebug); ok {
// print stack trace in verbose mode
fmt.Fprintf(color.Error, "[DEBUG] error: %+v\n", err)
}
defer os.Exit(1)
}
}

73
cmd/kubens/statefile.go Normal file
View File

@@ -0,0 +1,73 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/ahmetb/kubectx/internal/cmdutil"
)
var defaultDir = filepath.Join(cmdutil.HomeDir(), ".kube", "kubens")
type NSFile struct {
dir string
ctx string
}
func NewNSFile(ctx string) NSFile { return NSFile{dir: defaultDir, ctx: ctx} }
func (f NSFile) path() string {
fn := f.ctx
if isWindows() {
// bug 230: eks clusters contain ':' in ctx name, not a valid file name for win32
fn = strings.ReplaceAll(fn, ":", "__")
}
return filepath.Join(f.dir, fn)
}
// Load reads the previous namespace setting, or returns empty if not exists.
func (f NSFile) Load() (string, error) {
b, err := ioutil.ReadFile(f.path())
if err != nil {
if os.IsNotExist(err) {
return "", nil
}
return "", err
}
return string(bytes.TrimSpace(b)), nil
}
// Save stores the previous namespace information in the file.
func (f NSFile) Save(value string) error {
d := filepath.Dir(f.path())
if err := os.MkdirAll(d, 0755); err != nil {
return err
}
return ioutil.WriteFile(f.path(), []byte(value), 0644)
}
// isWindows determines if the process is running on windows OS.
func isWindows() bool {
if os.Getenv("_FORCE_GOOS") == "windows" { // for testing
return true
}
return runtime.GOOS == "windows"
}

View File

@@ -0,0 +1,81 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"io/ioutil"
"os"
"runtime"
"strings"
"testing"
"github.com/ahmetb/kubectx/internal/testutil"
)
func TestNSFile(t *testing.T) {
td, err := ioutil.TempDir(os.TempDir(), "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(td)
f := NewNSFile("foo")
f.dir = td
v, err := f.Load()
if err != nil {
t.Fatal(err)
}
if v != "" {
t.Fatalf("Load() expected empty; got=%v", err)
}
err = f.Save("bar")
if err != nil {
t.Fatalf("Save() err=%v", err)
}
v, err = f.Load()
if err != nil {
t.Fatal(err)
}
if expected := "bar"; v != expected {
t.Fatalf("Load()=\"%s\"; expected=\"%s\"", v, expected)
}
}
func TestNSFile_path_windows(t *testing.T) {
defer testutil.WithEnvVar("_FORCE_GOOS", "windows")()
fp := NewNSFile("a:b:c").path()
if expected := "a__b__c"; !strings.HasSuffix(fp, expected) {
t.Fatalf("file did not have expected ending %q: %s", expected, fp)
}
}
func Test_isWindows(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("won't test this case on windows")
}
got := isWindows()
if got {
t.Fatalf("isWindows() returned true for %s", runtime.GOOS)
}
defer testutil.WithEnvVar("_FORCE_GOOS", "windows")()
if !isWindows() {
t.Fatalf("isWindows() failed to detect windows with env override.")
}
}

114
cmd/kubens/switch.go Normal file
View File

@@ -0,0 +1,114 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"context"
"io"
"os"
"github.com/pkg/errors"
errors2 "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer"
)
type SwitchOp struct {
Target string // '-' for back and forth, or NAME
Force bool // force switch even if the namespace doesn't exist
}
func (s SwitchOp) Run(_, stderr io.Writer) error {
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
return errors.Wrap(err, "kubeconfig error")
}
toNS, err := switchNamespace(kc, s.Target, s.Force)
if err != nil {
return err
}
err = printer.Success(stderr, "Active namespace is \"%s\"", printer.SuccessColor.Sprint(toNS))
return err
}
func switchNamespace(kc *kubeconfig.Kubeconfig, ns string, force bool) (string, error) {
ctx := kc.GetCurrentContext()
if ctx == "" {
return "", errors.New("current-context is not set")
}
curNS, err := kc.NamespaceOfContext(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to get current namespace")
}
f := NewNSFile(ctx)
prev, err := f.Load()
if err != nil {
return "", errors.Wrap(err, "failed to load previous namespace from file")
}
if ns == "-" {
if prev == "" {
return "", errors.Errorf("No previous namespace found for current context (%s)", ctx)
}
ns = prev
}
if !force {
ok, err := namespaceExists(kc, ns)
if err != nil {
return "", errors.Wrap(err, "failed to query if namespace exists (is cluster accessible?)")
}
if !ok {
return "", errors.Errorf("no namespace exists with name \"%s\"", ns)
}
}
if err := kc.SetNamespace(ctx, ns); err != nil {
return "", errors.Wrapf(err, "failed to change to namespace \"%s\"", ns)
}
if err := kc.Save(); err != nil {
return "", errors.Wrap(err, "failed to save kubeconfig file")
}
if curNS != ns {
if err := f.Save(curNS); err != nil {
return "", errors.Wrap(err, "failed to save the previous namespace to file")
}
}
return ns, nil
}
func namespaceExists(kc *kubeconfig.Kubeconfig, ns string) (bool, error) {
// for tests
if os.Getenv("_MOCK_NAMESPACES") != "" {
return ns == "ns1" || ns == "ns2", nil
}
clientset, err := newKubernetesClientSet(kc)
if err != nil {
return false, errors.Wrap(err, "failed to initialize k8s REST client")
}
namespace, err := clientset.CoreV1().Namespaces().Get(context.Background(), ns, metav1.GetOptions{})
if errors2.IsNotFound(err) {
return false, nil
}
return namespace != nil, errors.Wrapf(err, "failed to query "+
"namespace %q from k8s API", ns)
}

20
cmd/kubens/version.go Normal file
View File

@@ -0,0 +1,20 @@
package main
import (
"fmt"
"io"
"github.com/pkg/errors"
)
var (
version = "v0.0.0+unknown" // populated by goreleaser
)
// VersionOp describes printing version string.
type VersionOp struct{}
func (_ VersionOp) Run(stdout, _ io.Writer) error {
_, err := fmt.Fprintf(stdout, "%s\n", version)
return errors.Wrap(err, "write error")
}

20
completion/_kubectx.zsh Normal file
View File

@@ -0,0 +1,20 @@
#compdef kubectx kctx=kubectx
local KUBECTX="${HOME}/.kube/kubectx"
PREV=""
local context_array=("${(@f)$(kubectl config get-contexts --output='name')}")
local all_contexts=(\'${^context_array}\')
if [ -f "$KUBECTX" ]; then
# show '-' only if there's a saved previous context
local PREV=$(cat "${KUBECTX}")
_arguments \
"-d:*: :(${all_contexts})" \
"(- *): :(- ${all_contexts})"
else
_arguments \
"-d:*: :(${all_contexts})" \
"(- *): :(${all_contexts})"
fi

10
completion/kubectx.fish Normal file
View File

@@ -0,0 +1,10 @@
# kubectx
function __fish_kubectx_arg_number -a number
set -l cmd (commandline -opc)
test (count $cmd) -eq $number
end
complete -f -c kubectx
complete -f -x -c kubectx -n '__fish_kubectx_arg_number 1' -a "(kubectl config get-contexts --output='name')"
complete -f -x -c kubectx -n '__fish_kubectx_arg_number 1' -a "-" -d "switch to the previous namespace in this context"

View File

@@ -1,12 +0,0 @@
#compdef kubectx kctx=kubectx
local KUBECTX="${HOME}/.kube/kubectx"
PREV=""
if [ -f "$KUBECTX" ]; then
# show '-' only if there's a saved previous context
local PREV=$(cat "${KUBECTX}")
_arguments "1: :((-\:Back\ to\ ${PREV} \
$(kubectl config get-contexts --output='name')))"
else
_arguments "1: :($(kubectl config get-contexts --output='name'))"
fi

12
completion/kubens.fish Normal file
View File

@@ -0,0 +1,12 @@
# kubens
function __fish_kubens_arg_number -a number
set -l cmd (commandline -opc)
test (count $cmd) -eq $number
end
complete -f -c kubens
complete -f -x -c kubens -n '__fish_kubens_arg_number 1' -a "(kubectl get ns -o=custom-columns=NAME:.metadata.name --no-headers)"
complete -f -x -c kubens -n '__fish_kubens_arg_number 1' -a "-" -d "switch to the previous namespace in this context"
complete -f -x -c kubens -n '__fish_kubens_arg_number 1' -s c -l current -d "show the current namespace"
complete -f -x -c kubens -n '__fish_kubens_arg_number 1' -s h -l help -d "show the help message"

54
go.mod Normal file
View File

@@ -0,0 +1,54 @@
module github.com/ahmetb/kubectx
go 1.22
require (
facette.io/natsort v0.0.0-20181210072756-2cd4dd1e2dcb
github.com/fatih/color v1.9.0
github.com/google/go-cmp v0.5.9
github.com/mattn/go-isatty v0.0.14
github.com/pkg/errors v0.9.1
gopkg.in/yaml.v3 v3.0.1
k8s.io/apimachinery v0.27.3
k8s.io/client-go v0.27.3
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.1 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/imdario/mergo v0.3.9 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.4 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/api v0.27.3 // indirect
k8s.io/klog/v2 v2.90.1 // indirect
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect
k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)

506
go.sum Normal file
View File

@@ -0,0 +1,506 @@
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/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
facette.io/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:1pSweJFeR3Pqx7uoelppkzeegfUBXL6I2FFAbfXw570=
facette.io/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:npRYmtaITVom7rcSo+pRURltHSG2r4TQM1cdqJ2dUB0=
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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE=
github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8=
github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/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/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54=
github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/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/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
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=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.9.1 h1:zie5Ly042PD3bsCvsSOPvRnFwyo3rKe64TJlD6nu0mk=
github.com/onsi/ginkgo/v2 v2.9.1/go.mod h1:FEcmzVcCHl+4o9bQZVab+4dC9+j+91t2FHSzmGAPfuo=
github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E=
github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/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-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
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/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
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-20190108225652-1e06a53dbb7e/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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190222072716-a9d3bda3a223/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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-20190425150028-36563e24a262/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-20190606124116-d0a3d012864b/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-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
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/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
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-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
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.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
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/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
k8s.io/api v0.27.3 h1:yR6oQXXnUEBWEWcvPWS0jQL575KoAboQPfJAuKNrw5Y=
k8s.io/api v0.27.3/go.mod h1:C4BNvZnQOF7JA/0Xed2S+aUyJSfTGkGFxLXz9MnpIpg=
k8s.io/apimachinery v0.27.3 h1:Ubye8oBufD04l9QnNtW05idcOe9Z3GQN8+7PqmuVcUM=
k8s.io/apimachinery v0.27.3/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E=
k8s.io/client-go v0.27.3 h1:7dnEGHZEJld3lYwxvLl7WoehK6lAq7GvgjxpA3nv1E8=
k8s.io/client-go v0.27.3/go.mod h1:2MBEKuTo6V1lbKy3z1euEGnhPfGZLKTS9tiJ2xodM48=
k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw=
k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg=
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg=
k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY=
k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

BIN
img/kubectx-demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

BIN
img/kubectx-interactive.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

BIN
img/kubens-demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

View File

@@ -0,0 +1,36 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmdutil
import (
"io"
"strings"
"github.com/ahmetb/kubectx/internal/printer"
)
func PrintDeprecatedEnvWarnings(out io.Writer, vars []string) {
for _, vv := range vars {
parts := strings.SplitN(vv, "=", 2)
if len(parts) != 2 {
continue
}
key := parts[0]
if key == `KUBECTX_CURRENT_FGCOLOR` || key == `KUBECTX_CURRENT_BGCOLOR` {
printer.Warning(out, "%s environment variable is now deprecated", key)
}
}
}

View File

@@ -0,0 +1,48 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmdutil
import (
"bytes"
"strings"
"testing"
)
func TestPrintDeprecatedEnvWarnings_noDeprecatedVars(t *testing.T) {
var out bytes.Buffer
PrintDeprecatedEnvWarnings(&out, []string{
"A=B",
"PATH=/foo:/bar:/bin",
})
if v := out.String(); len(v) > 0 {
t.Fatalf("something written to buf: %v", v)
}
}
func TestPrintDeprecatedEnvWarnings_bgColors(t *testing.T) {
var out bytes.Buffer
PrintDeprecatedEnvWarnings(&out, []string{
"KUBECTX_CURRENT_FGCOLOR=1",
"KUBECTX_CURRENT_BGCOLOR=2",
})
v := out.String()
if !strings.Contains(v, "KUBECTX_CURRENT_FGCOLOR") {
t.Fatalf("output doesn't contain 'KUBECTX_CURRENT_FGCOLOR': \"%s\"", v)
}
if !strings.Contains(v, "KUBECTX_CURRENT_BGCOLOR") {
t.Fatalf("output doesn't contain 'KUBECTX_CURRENT_BGCOLOR': \"%s\"", v)
}
}

View File

@@ -0,0 +1,44 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmdutil
import (
"os"
"os/exec"
"github.com/mattn/go-isatty"
"github.com/ahmetb/kubectx/internal/env"
)
// isTerminal determines if given fd is a TTY.
func isTerminal(fd *os.File) bool {
return isatty.IsTerminal(fd.Fd())
}
// fzfInstalled determines if fzf(1) is in PATH.
func fzfInstalled() bool {
v, _ := exec.LookPath("fzf")
if v != "" {
return true
}
return false
}
// IsInteractiveMode determines if we can do choosing with fzf.
func IsInteractiveMode(stdout *os.File) bool {
v := os.Getenv(env.EnvFZFIgnore)
return v == "" && isTerminal(stdout) && fzfInstalled()
}

40
internal/cmdutil/util.go Normal file
View File

@@ -0,0 +1,40 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmdutil
import (
"os"
"github.com/pkg/errors"
)
func HomeDir() string {
home := os.Getenv("HOME")
if home == "" {
home = os.Getenv("USERPROFILE") // windows
}
return home
}
// IsNotFoundErr determines if the underlying error is os.IsNotExist. Right now
// errors from github.com/pkg/errors doesn't work with os.IsNotExist.
func IsNotFoundErr(err error) bool {
for e := err; e != nil; e = errors.Unwrap(e) {
if os.IsNotExist(e) {
return true
}
}
return false
}

View File

@@ -0,0 +1,80 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmdutil
import (
"testing"
"github.com/ahmetb/kubectx/internal/testutil"
)
func Test_homeDir(t *testing.T) {
type env struct{ k, v string }
cases := []struct {
name string
envs []env
want string
}{
{
name: "don't use XDG_CACHE_HOME as homedir",
envs: []env{
{"XDG_CACHE_HOME", "xdg"},
{"HOME", "home"},
},
want: "home",
},
{
name: "HOME over USERPROFILE",
envs: []env{
{"HOME", "home"},
{"USERPROFILE", "up"},
},
want: "home",
},
{
name: "only USERPROFILE available",
envs: []env{
{"HOME", ""},
{"USERPROFILE", "up"},
},
want: "up",
},
{
name: "none available",
envs: []env{
{"HOME", ""},
{"USERPROFILE", ""},
},
want: "",
},
}
for _, c := range cases {
t.Run(c.name, func(tt *testing.T) {
var unsets []func()
for _, e := range c.envs {
unsets = append(unsets, testutil.WithEnvVar(e.k, e.v))
}
got := HomeDir()
if got != c.want {
t.Errorf("expected:%q got:%q", c.want, got)
}
for _, u := range unsets {
u()
}
})
}
}

32
internal/env/constants.go vendored Normal file
View File

@@ -0,0 +1,32 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package env
const (
// EnvFZFIgnore describes the environment variable to set to disable
// interactive context selection when fzf is installed.
EnvFZFIgnore = "KUBECTX_IGNORE_FZF"
// EnvNoColor describes the environment variable to disable color usage
// when printing current context in a list.
EnvNoColor = `NO_COLOR`
// EnvForceColor describes the "internal" environment variable to force
// color usage to show current context in a list.
EnvForceColor = `_KUBECTX_FORCE_COLOR`
// EnvDebug describes the internal environment variable for more verbose logging.
EnvDebug = `DEBUG`
)

View File

@@ -0,0 +1,83 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package kubeconfig
import (
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
)
func (k *Kubeconfig) DeleteContextEntry(deleteName string) error {
contexts, err := k.contextsNode()
if err != nil {
return err
}
i := -1
for j, ctxNode := range contexts.Content {
nameNode := valueOf(ctxNode, "name")
if nameNode != nil && nameNode.Kind == yaml.ScalarNode && nameNode.Value == deleteName {
i = j
break
}
}
if i >= 0 {
copy(contexts.Content[i:], contexts.Content[i+1:])
contexts.Content[len(contexts.Content)-1] = nil
contexts.Content = contexts.Content[:len(contexts.Content)-1]
}
return nil
}
func (k *Kubeconfig) ModifyCurrentContext(name string) error {
currentCtxNode := valueOf(k.rootNode, "current-context")
if currentCtxNode != nil {
currentCtxNode.Value = name
return nil
}
// if current-context field doesn't exist, create new field
keyNode := &yaml.Node{
Kind: yaml.ScalarNode,
Value: "current-context",
Tag: "!!str"}
valueNode := &yaml.Node{
Kind: yaml.ScalarNode,
Value: name,
Tag: "!!str"}
k.rootNode.Content = append(k.rootNode.Content, keyNode, valueNode)
return nil
}
func (k *Kubeconfig) ModifyContextName(old, new string) error {
contexts, err := k.contextsNode()
if err != nil {
return err
}
var changed bool
for _, contextNode := range contexts.Content {
nameNode := valueOf(contextNode, "name")
if nameNode.Kind == yaml.ScalarNode && nameNode.Value == old {
nameNode.Value = new
changed = true
break
}
}
if !changed {
return errors.New("no changes were made")
}
return nil
}

View File

@@ -0,0 +1,180 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package kubeconfig
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/ahmetb/kubectx/internal/testutil"
)
func TestKubeconfig_DeleteContextEntry_errors(t *testing.T) {
kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`[1, 2, 3]`))
_ = kc.Parse()
err := kc.DeleteContextEntry("foo")
if err == nil {
t.Fatal("supposed to fail on non-mapping nodes")
}
kc = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`a: b`))
_ = kc.Parse()
err = kc.DeleteContextEntry("foo")
if err == nil {
t.Fatal("supposed to fail if contexts key does not exist")
}
kc = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`contexts: "some string"`))
_ = kc.Parse()
err = kc.DeleteContextEntry("foo")
if err == nil {
t.Fatal("supposed to fail if contexts key is not an array")
}
}
func TestKubeconfig_DeleteContextEntry(t *testing.T) {
test := WithMockKubeconfigLoader(
testutil.KC().WithCtxs(
testutil.Ctx("c1"),
testutil.Ctx("c2"),
testutil.Ctx("c3")).ToYAML(t))
kc := new(Kubeconfig).WithLoader(test)
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
if err := kc.DeleteContextEntry("c1"); err != nil {
t.Fatal(err)
}
if err := kc.Save(); err != nil {
t.Fatal(err)
}
expected := testutil.KC().WithCtxs(
testutil.Ctx("c2"),
testutil.Ctx("c3")).ToYAML(t)
out := test.Output()
if diff := cmp.Diff(expected, out); diff != "" {
t.Fatalf("diff: %s", diff)
}
}
func TestKubeconfig_ModifyCurrentContext_fieldExists(t *testing.T) {
test := WithMockKubeconfigLoader(
testutil.KC().WithCurrentCtx("abc").Set("field1", "value1").ToYAML(t))
kc := new(Kubeconfig).WithLoader(test)
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
if err := kc.ModifyCurrentContext("foo"); err != nil {
t.Fatal(err)
}
if err := kc.Save(); err != nil {
t.Fatal(err)
}
expected := testutil.KC().WithCurrentCtx("foo").Set("field1", "value1").ToYAML(t)
out := test.Output()
if diff := cmp.Diff(expected, out); diff != "" {
t.Fatalf("diff: %s", diff)
}
}
func TestKubeconfig_ModifyCurrentContext_fieldMissing(t *testing.T) {
test := WithMockKubeconfigLoader(`f1: v1`)
kc := new(Kubeconfig).WithLoader(test)
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
if err := kc.ModifyCurrentContext("foo"); err != nil {
t.Fatal(err)
}
if err := kc.Save(); err != nil {
t.Fatal(err)
}
expected := `f1: v1
current-context: foo
`
out := test.Output()
if diff := cmp.Diff(expected, out); diff != "" {
t.Fatalf("diff: %s", diff)
}
}
func TestKubeconfig_ModifyContextName_noContextsEntryError(t *testing.T) {
// no context entries
test := WithMockKubeconfigLoader(`a: b`)
kc := new(Kubeconfig).WithLoader(test)
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
if err := kc.ModifyContextName("c1", "c2"); err == nil {
t.Fatal("was expecting error for no 'contexts' entry; got nil")
}
}
func TestKubeconfig_ModifyContextName_contextsEntryNotSequenceError(t *testing.T) {
// no context entries
test := WithMockKubeconfigLoader(
`contexts: "hello"`)
kc := new(Kubeconfig).WithLoader(test)
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
if err := kc.ModifyContextName("c1", "c2"); err == nil {
t.Fatal("was expecting error for 'context entry not a sequence'; got nil")
}
}
func TestKubeconfig_ModifyContextName_noChange(t *testing.T) {
test := WithMockKubeconfigLoader(testutil.KC().WithCtxs(
testutil.Ctx("c1"),
testutil.Ctx("c2"),
testutil.Ctx("c3")).ToYAML(t))
kc := new(Kubeconfig).WithLoader(test)
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
if err := kc.ModifyContextName("c5", "c6"); err == nil {
t.Fatal("was expecting error for 'no changes made'")
}
}
func TestKubeconfig_ModifyContextName(t *testing.T) {
test := WithMockKubeconfigLoader(testutil.KC().WithCtxs(
testutil.Ctx("c1"),
testutil.Ctx("c2"),
testutil.Ctx("c3")).ToYAML(t))
kc := new(Kubeconfig).WithLoader(test)
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
if err := kc.ModifyContextName("c1", "ccc"); err != nil {
t.Fatal(err)
}
if err := kc.Save(); err != nil {
t.Fatal(err)
}
expected := testutil.KC().WithCtxs(
testutil.Ctx("ccc"),
testutil.Ctx("c2"),
testutil.Ctx("c3")).ToYAML(t)
out := test.Output()
if diff := cmp.Diff(expected, out); diff != "" {
t.Fatalf("diff: %s", diff)
}
}

View File

@@ -0,0 +1,86 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package kubeconfig
import (
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
)
func (k *Kubeconfig) contextsNode() (*yaml.Node, error) {
contexts := valueOf(k.rootNode, "contexts")
if contexts == nil {
return nil, errors.New("\"contexts\" entry is nil")
} else if contexts.Kind != yaml.SequenceNode {
return nil, errors.New("\"contexts\" is not a sequence node")
}
return contexts, nil
}
func (k *Kubeconfig) contextNode(name string) (*yaml.Node, error) {
contexts, err := k.contextsNode()
if err != nil {
return nil, err
}
for _, contextNode := range contexts.Content {
nameNode := valueOf(contextNode, "name")
if nameNode.Kind == yaml.ScalarNode && nameNode.Value == name {
return contextNode, nil
}
}
return nil, errors.Errorf("context with name \"%s\" not found", name)
}
func (k *Kubeconfig) ContextNames() []string {
contexts := valueOf(k.rootNode, "contexts")
if contexts == nil {
return nil
}
if contexts.Kind != yaml.SequenceNode {
return nil
}
var ctxNames []string
for _, ctx := range contexts.Content {
nameVal := valueOf(ctx, "name")
if nameVal != nil {
ctxNames = append(ctxNames, nameVal.Value)
}
}
return ctxNames
}
func (k *Kubeconfig) ContextExists(name string) bool {
ctxNames := k.ContextNames()
for _, v := range ctxNames {
if v == name {
return true
}
}
return false
}
func valueOf(mapNode *yaml.Node, key string) *yaml.Node {
if mapNode.Kind != yaml.MappingNode {
return nil
}
for i, ch := range mapNode.Content {
if i%2 == 0 && ch.Kind == yaml.ScalarNode && ch.Value == key {
return mapNode.Content[i+1]
}
}
return nil
}

View File

@@ -0,0 +1,89 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package kubeconfig
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/ahmetb/kubectx/internal/testutil"
)
func TestKubeconfig_ContextNames(t *testing.T) {
tl := WithMockKubeconfigLoader(
testutil.KC().WithCtxs(
testutil.Ctx("abc"),
testutil.Ctx("def"),
testutil.Ctx("ghi")).Set("field1", map[string]string{"bar": "zoo"}).ToYAML(t))
kc := new(Kubeconfig).WithLoader(tl)
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
ctx := kc.ContextNames()
expected := []string{"abc", "def", "ghi"}
if diff := cmp.Diff(expected, ctx); diff != "" {
t.Fatalf("%s", diff)
}
}
func TestKubeconfig_ContextNames_noContextsEntry(t *testing.T) {
tl := WithMockKubeconfigLoader(`a: b`)
kc := new(Kubeconfig).WithLoader(tl)
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
ctx := kc.ContextNames()
var expected []string = nil
if diff := cmp.Diff(expected, ctx); diff != "" {
t.Fatalf("%s", diff)
}
}
func TestKubeconfig_ContextNames_nonArrayContextsEntry(t *testing.T) {
tl := WithMockKubeconfigLoader(`contexts: "hello"`)
kc := new(Kubeconfig).WithLoader(tl)
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
ctx := kc.ContextNames()
var expected []string = nil
if diff := cmp.Diff(expected, ctx); diff != "" {
t.Fatalf("%s", diff)
}
}
func TestKubeconfig_CheckContextExists(t *testing.T) {
tl := WithMockKubeconfigLoader(
testutil.KC().WithCtxs(
testutil.Ctx("c1"),
testutil.Ctx("c2")).ToYAML(t))
kc := new(Kubeconfig).WithLoader(tl)
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
if !kc.ContextExists("c1") {
t.Fatal("c1 actually exists; reported false")
}
if !kc.ContextExists("c2") {
t.Fatal("c2 actually exists; reported false")
}
if kc.ContextExists("c3") {
t.Fatal("c3 does not exist; but reported true")
}
}

View File

@@ -0,0 +1,31 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package kubeconfig
// GetCurrentContext returns "current-context" value in given
// kubeconfig object Node, or returns "" if not found.
func (k *Kubeconfig) GetCurrentContext() string {
v := valueOf(k.rootNode, "current-context")
if v == nil {
return ""
}
return v.Value
}
func (k *Kubeconfig) UnsetCurrentContext() error {
curCtxValNode := valueOf(k.rootNode, "current-context")
curCtxValNode.Value = ""
return nil
}

View File

@@ -0,0 +1,69 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package kubeconfig
import (
"testing"
"github.com/ahmetb/kubectx/internal/testutil"
)
func TestKubeconfig_GetCurrentContext(t *testing.T) {
tl := WithMockKubeconfigLoader(`current-context: foo`)
kc := new(Kubeconfig).WithLoader(tl)
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
v := kc.GetCurrentContext()
expected := "foo"
if v != expected {
t.Fatalf("expected=\"%s\"; got=\"%s\"", expected, v)
}
}
func TestKubeconfig_GetCurrentContext_missingField(t *testing.T) {
tl := WithMockKubeconfigLoader(`abc: def`)
kc := new(Kubeconfig).WithLoader(tl)
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
v := kc.GetCurrentContext()
expected := ""
if v != expected {
t.Fatalf("expected=\"%s\"; got=\"%s\"", expected, v)
}
}
func TestKubeconfig_UnsetCurrentContext(t *testing.T) {
tl := WithMockKubeconfigLoader(testutil.KC().WithCurrentCtx("foo").ToYAML(t))
kc := new(Kubeconfig).WithLoader(tl)
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
if err := kc.UnsetCurrentContext(); err != nil {
t.Fatal(err)
}
if err := kc.Save(); err != nil {
t.Fatal(err)
}
out := tl.Output()
expected := testutil.KC().WithCurrentCtx("").ToYAML(t)
if out != expected {
t.Fatalf("expected=\"%s\"; got=\"%s\"", expected, out)
}
}

View File

@@ -0,0 +1,39 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package kubeconfig
import (
"bytes"
"io"
"strings"
)
type MockKubeconfigLoader struct {
in io.Reader
out bytes.Buffer
}
func (t *MockKubeconfigLoader) Read(p []byte) (n int, err error) { return t.in.Read(p) }
func (t *MockKubeconfigLoader) Write(p []byte) (n int, err error) { return t.out.Write(p) }
func (t *MockKubeconfigLoader) Close() error { return nil }
func (t *MockKubeconfigLoader) Reset() error { return nil }
func (t *MockKubeconfigLoader) Load() ([]ReadWriteResetCloser, error) {
return []ReadWriteResetCloser{ReadWriteResetCloser(t)}, nil
}
func (t *MockKubeconfigLoader) Output() string { return t.out.String() }
func WithMockKubeconfigLoader(kubecfg string) *MockKubeconfigLoader {
return &MockKubeconfigLoader{in: strings.NewReader(kubecfg)}
}

View File

@@ -0,0 +1,86 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package kubeconfig
import (
"io"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
)
type ReadWriteResetCloser interface {
io.ReadWriteCloser
// Reset truncates the file and seeks to the beginning of the file.
Reset() error
}
type Loader interface {
Load() ([]ReadWriteResetCloser, error)
}
type Kubeconfig struct {
loader Loader
f ReadWriteResetCloser
rootNode *yaml.Node
}
func (k *Kubeconfig) WithLoader(l Loader) *Kubeconfig {
k.loader = l
return k
}
func (k *Kubeconfig) Close() error {
if k.f == nil {
return nil
}
return k.f.Close()
}
func (k *Kubeconfig) Parse() error {
files, err := k.loader.Load()
if err != nil {
return errors.Wrap(err, "failed to load")
}
// TODO since we don't support multiple kubeconfig files at the moment, there's just 1 file
f := files[0]
k.f = f
var v yaml.Node
if err := yaml.NewDecoder(f).Decode(&v); err != nil {
return errors.Wrap(err, "failed to decode")
}
k.rootNode = v.Content[0]
if k.rootNode.Kind != yaml.MappingNode {
return errors.New("kubeconfig file is not a map document")
}
return nil
}
func (k *Kubeconfig) Bytes() ([]byte, error) {
return yaml.Marshal(k.rootNode)
}
func (k *Kubeconfig) Save() error {
if err := k.f.Reset(); err != nil {
return errors.Wrap(err, "failed to reset file")
}
enc := yaml.NewEncoder(k.f)
enc.SetIndent(0)
return enc.Encode(k.rootNode)
}

View File

@@ -0,0 +1,67 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package kubeconfig
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/ahmetb/kubectx/internal/testutil"
)
func TestParse(t *testing.T) {
err := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`a: [1, 2`)).Parse()
if err == nil {
t.Fatal("expected error from bad yaml")
}
err = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`[1, 2, 3]`)).Parse()
if err == nil {
t.Fatal("expected error from not-mapping root node")
}
err = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`current-context: foo`)).Parse()
if err != nil {
t.Fatal(err)
}
err = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC().
WithCurrentCtx("foo").
WithCtxs().ToYAML(t))).Parse()
if err != nil {
t.Fatal(err)
}
}
func TestSave(t *testing.T) {
in := "a: [1, 2, 3]\n"
test := WithMockKubeconfigLoader(in)
kc := new(Kubeconfig).WithLoader(test)
defer kc.Close()
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
if err := kc.ModifyCurrentContext("hello"); err != nil {
t.Fatal(err)
}
if err := kc.Save(); err != nil {
t.Fatal(err)
}
expected := "a: [1, 2, 3]\ncurrent-context: hello\n"
if diff := cmp.Diff(expected, test.Output()); diff != "" {
t.Fatal(diff)
}
}

View File

@@ -0,0 +1,76 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package kubeconfig
import (
"github.com/ahmetb/kubectx/internal/cmdutil"
"os"
"path/filepath"
"github.com/pkg/errors"
)
var (
DefaultLoader Loader = new(StandardKubeconfigLoader)
)
type StandardKubeconfigLoader struct{}
type kubeconfigFile struct{ *os.File }
func (*StandardKubeconfigLoader) Load() ([]ReadWriteResetCloser, error) {
cfgPath, err := kubeconfigPath()
if err != nil {
return nil, errors.Wrap(err, "cannot determine kubeconfig path")
}
f, err := os.OpenFile(cfgPath, os.O_RDWR, 0)
if err != nil {
if os.IsNotExist(err) {
return nil, errors.Wrap(err, "kubeconfig file not found")
}
return nil, errors.Wrap(err, "failed to open file")
}
// TODO we'll return all kubeconfig files when we start implementing multiple kubeconfig support
return []ReadWriteResetCloser{ReadWriteResetCloser(&kubeconfigFile{f})}, nil
}
func (kf *kubeconfigFile) Reset() error {
if err := kf.Truncate(0); err != nil {
return errors.Wrap(err, "failed to truncate file")
}
_, err := kf.Seek(0, 0)
return errors.Wrap(err, "failed to seek in file")
}
func kubeconfigPath() (string, error) {
// KUBECONFIG env var
if v := os.Getenv("KUBECONFIG"); v != "" {
list := filepath.SplitList(v)
if len(list) > 1 {
// TODO KUBECONFIG=file1:file2 currently not supported
return "", errors.New("multiple files in KUBECONFIG are currently not supported")
}
return v, nil
}
// default path
home := cmdutil.HomeDir()
if home == "" {
return "", errors.New("HOME or USERPROFILE environment variable not set")
}
return filepath.Join(home, ".kube", "config"), nil
}

View File

@@ -0,0 +1,83 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package kubeconfig
import (
"github.com/ahmetb/kubectx/internal/cmdutil"
"os"
"path/filepath"
"strings"
"testing"
"github.com/ahmetb/kubectx/internal/testutil"
)
func Test_kubeconfigPath(t *testing.T) {
defer testutil.WithEnvVar("HOME", "/x/y/z")()
expected := filepath.FromSlash("/x/y/z/.kube/config")
got, err := kubeconfigPath()
if err != nil {
t.Fatal(err)
}
if got != expected {
t.Fatalf("got=%q expected=%q", got, expected)
}
}
func Test_kubeconfigPath_noEnvVars(t *testing.T) {
defer testutil.WithEnvVar("XDG_CACHE_HOME", "")()
defer testutil.WithEnvVar("HOME", "")()
defer testutil.WithEnvVar("USERPROFILE", "")()
_, err := kubeconfigPath()
if err == nil {
t.Fatalf("expected error")
}
}
func Test_kubeconfigPath_envOvveride(t *testing.T) {
defer testutil.WithEnvVar("KUBECONFIG", "foo")()
v, err := kubeconfigPath()
if err != nil {
t.Fatal(err)
}
if expected := "foo"; v != expected {
t.Fatalf("expected=%q, got=%q", expected, v)
}
}
func Test_kubeconfigPath_envOvverideDoesNotSupportPathSeparator(t *testing.T) {
path := strings.Join([]string{"file1", "file2"}, string(os.PathListSeparator))
defer testutil.WithEnvVar("KUBECONFIG", path)()
_, err := kubeconfigPath()
if err == nil {
t.Fatal("expected error")
}
}
func TestStandardKubeconfigLoader_returnsNotFoundErr(t *testing.T) {
defer testutil.WithEnvVar("KUBECONFIG", "foo")()
kc := new(Kubeconfig).WithLoader(DefaultLoader)
err := kc.Parse()
if err == nil {
t.Fatal("expected err")
}
if !cmdutil.IsNotFoundErr(err) {
t.Fatalf("expected ENOENT error; got=%v", err)
}
}

View File

@@ -0,0 +1,77 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package kubeconfig
import "gopkg.in/yaml.v3"
const (
defaultNamespace = "default"
)
func (k *Kubeconfig) NamespaceOfContext(contextName string) (string, error) {
ctx, err := k.contextNode(contextName)
if err != nil {
return "", err
}
ctxBody := valueOf(ctx, "context")
if ctxBody == nil {
return defaultNamespace, nil
}
ns := valueOf(ctxBody, "namespace")
if ns == nil || ns.Value == "" {
return defaultNamespace, nil
}
return ns.Value, nil
}
func (k *Kubeconfig) SetNamespace(ctxName string, ns string) error {
ctxNode, err := k.contextNode(ctxName)
if err != nil {
return err
}
var ctxBodyNodeWasEmpty bool // actual namespace value is in contexts[index].context.namespace, but .context might not exist
ctxBodyNode := valueOf(ctxNode, "context")
if ctxBodyNode == nil {
ctxBodyNodeWasEmpty = true
ctxBodyNode = &yaml.Node{
Kind: yaml.MappingNode,
}
}
nsNode := valueOf(ctxBodyNode, "namespace")
if nsNode != nil {
nsNode.Value = ns
return nil
}
keyNode := &yaml.Node{
Kind: yaml.ScalarNode,
Value: "namespace",
Tag: "!!str"}
valueNode := &yaml.Node{
Kind: yaml.ScalarNode,
Value: ns,
Tag: "!!str"}
ctxBodyNode.Content = append(ctxBodyNode.Content, keyNode, valueNode)
if ctxBodyNodeWasEmpty {
ctxNode.Content = append(ctxNode.Content, &yaml.Node{
Kind: yaml.ScalarNode,
Value: "context",
Tag: "!!str",
}, ctxBodyNode)
}
return nil
}

View File

@@ -0,0 +1,94 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package kubeconfig
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/ahmetb/kubectx/internal/testutil"
)
func TestKubeconfig_NamespaceOfContext_ctxNotFound(t *testing.T) {
kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC().
WithCtxs(testutil.Ctx("c1")).ToYAML(t)))
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
_, err := kc.NamespaceOfContext("c2")
if err == nil {
t.Fatal("expected err")
}
}
func TestKubeconfig_NamespaceOfContext(t *testing.T) {
kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC().
WithCtxs(
testutil.Ctx("c1"),
testutil.Ctx("c2").Ns("c2n1")).ToYAML(t)))
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
v1, err := kc.NamespaceOfContext("c1")
if err != nil {
t.Fatal("expected err")
}
if expected := `default`; v1 != expected {
t.Fatalf("c1: expected=\"%s\" got=\"%s\"", expected, v1)
}
v2, err := kc.NamespaceOfContext("c2")
if err != nil {
t.Fatal("expected err")
}
if expected := `c2n1`; v2 != expected {
t.Fatalf("c2: expected=\"%s\" got=\"%s\"", expected, v2)
}
}
func TestKubeconfig_SetNamespace(t *testing.T) {
l := WithMockKubeconfigLoader(testutil.KC().
WithCtxs(
testutil.Ctx("c1"),
testutil.Ctx("c2").Ns("c2n1")).ToYAML(t))
kc := new(Kubeconfig).WithLoader(l)
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
if err := kc.SetNamespace("c3", "foo"); err == nil {
t.Fatalf("expected error for non-existing ctx")
}
if err := kc.SetNamespace("c1", "c1n1"); err != nil {
t.Fatal(err)
}
if err := kc.SetNamespace("c2", "c2n2"); err != nil {
t.Fatal(err)
}
if err := kc.Save(); err != nil {
t.Fatal(err)
}
expected := testutil.KC().WithCtxs(
testutil.Ctx("c1").Ns("c1n1"),
testutil.Ctx("c2").Ns("c2n2")).ToYAML(t)
if diff := cmp.Diff(l.Output(), expected); diff != "" {
t.Fatal(diff)
}
}

54
internal/printer/color.go Normal file
View File

@@ -0,0 +1,54 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package printer
import (
"os"
"github.com/fatih/color"
"github.com/ahmetb/kubectx/internal/env"
)
var (
ActiveItemColor = color.New(color.FgGreen, color.Bold)
)
func init() {
EnableOrDisableColor(ActiveItemColor)
}
// useColors returns true if colors are force-enabled,
// false if colors are disabled, or nil for default behavior
// which is determined based on factors like if stdout is tty.
func useColors() *bool {
tr, fa := true, false
if os.Getenv(env.EnvForceColor) != "" {
return &tr
} else if os.Getenv(env.EnvNoColor) != "" {
return &fa
}
return nil
}
// EnableOrDisableColor determines if color should be force-enabled or force-disabled
// or left untouched based on environment configuration.
func EnableOrDisableColor(c *color.Color) {
if v := useColors(); v != nil && *v {
c.EnableColor()
} else if v != nil && !*v {
c.DisableColor()
}
}

View File

@@ -0,0 +1,53 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package printer
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/ahmetb/kubectx/internal/testutil"
)
var (
tr, fa = true, false
)
func Test_useColors_forceColors(t *testing.T) {
defer testutil.WithEnvVar("_KUBECTX_FORCE_COLOR", "1")()
defer testutil.WithEnvVar("NO_COLOR", "1")()
if v := useColors(); !cmp.Equal(v, &tr) {
t.Fatalf("expected useColors() = true; got = %v", v)
}
}
func Test_useColors_disableColors(t *testing.T) {
defer testutil.WithEnvVar("NO_COLOR", "1")()
if v := useColors(); !cmp.Equal(v, &fa) {
t.Fatalf("expected useColors() = false; got = %v", v)
}
}
func Test_useColors_default(t *testing.T) {
defer testutil.WithEnvVar("NO_COLOR", "")()
defer testutil.WithEnvVar("_KUBECTX_FORCE_COLOR", "")()
if v := useColors(); v != nil {
t.Fatalf("expected useColors() = nil; got=%v", *v)
}
}

View File

@@ -0,0 +1,59 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package printer
import (
"fmt"
"io"
"github.com/fatih/color"
)
var (
ErrorColor = color.New(color.FgRed, color.Bold)
WarningColor = color.New(color.FgYellow, color.Bold)
SuccessColor = color.New(color.FgGreen)
)
func init() {
colors := useColors()
if colors == nil {
return
}
if *colors {
ErrorColor.EnableColor()
WarningColor.EnableColor()
SuccessColor.EnableColor()
} else {
ErrorColor.DisableColor()
WarningColor.DisableColor()
SuccessColor.DisableColor()
}
}
func Error(w io.Writer, format string, args ...interface{}) error {
_, err := fmt.Fprintf(w, ErrorColor.Sprint("error: ")+format+"\n", args...)
return err
}
func Warning(w io.Writer, format string, args ...interface{}) error {
_, err := fmt.Fprintf(w, WarningColor.Sprint("warning: ")+format+"\n", args...)
return err
}
func Success(w io.Writer, format string, args ...interface{}) error {
_, err := fmt.Fprintf(w, SuccessColor.Sprint("✔ ")+fmt.Sprintf(format+"\n", args...))
return err
}

View File

@@ -0,0 +1,53 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package testutil
import (
"strings"
"testing"
"gopkg.in/yaml.v3"
)
type Context struct {
Name string `yaml:"name,omitempty"`
Context struct {
Namespace string `yaml:"namespace,omitempty"`
} `yaml:"context,omitempty"`
}
func Ctx(name string) *Context { return &Context{Name: name} }
func (c *Context) Ns(ns string) *Context { c.Context.Namespace = ns; return c }
type Kubeconfig map[string]interface{}
func KC() *Kubeconfig {
return &Kubeconfig{
"apiVersion": "v1",
"kind": "Config"}
}
func (k *Kubeconfig) Set(key string, v interface{}) *Kubeconfig { (*k)[key] = v; return k }
func (k *Kubeconfig) WithCurrentCtx(s string) *Kubeconfig { (*k)["current-context"] = s; return k }
func (k *Kubeconfig) WithCtxs(c ...*Context) *Kubeconfig { (*k)["contexts"] = c; return k }
func (k *Kubeconfig) ToYAML(t *testing.T) string {
t.Helper()
var v strings.Builder
if err := yaml.NewEncoder(&v).Encode(*k); err != nil {
t.Fatalf("failed to encode mock kubeconfig: %v", err)
}
return v.String()
}

View File

@@ -0,0 +1,40 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package testutil
import (
"io/ioutil"
"os"
"testing"
)
func TempFile(t *testing.T, contents string) (path string, cleanup func()) {
// TODO consider removing, used only in one place.
t.Helper()
f, err := ioutil.TempFile(os.TempDir(), "test-file")
if err != nil {
t.Fatalf("failed to create test file: %v", err)
}
path = f.Name()
if _, err := f.Write([]byte(contents)); err != nil {
t.Fatalf("failed to write to test file: %v", err)
}
return path, func() {
f.Close()
os.Remove(path)
}
}

View File

@@ -0,0 +1,31 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package testutil
import "os"
// WithEnvVar sets an env var temporarily. Call its return value
// in defer to restore original value in env (if exists).
func WithEnvVar(key, value string) func() {
orig, ok := os.LookupEnv(key)
os.Setenv(key, value)
return func() {
if ok {
os.Setenv(key, orig)
} else {
os.Unsetenv(key)
}
}
}

193
kubectx
View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
#
# kubectx(1) is a utility to manage and switch between kubectl contexts.
@@ -21,39 +21,71 @@
set -eou pipefail
IFS=$'\n\t'
SCRIPT_DIR="$(dirname "$( readlink -f "${0}" 2>/dev/null || \
python -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "${0}" )")"
SELF_CMD="$0"
if [[ -f "${SCRIPT_DIR}/utils.bash" ]]; then
source "${SCRIPT_DIR}/utils.bash"
else
source "${SCRIPT_DIR}/../include/utils.bash"
fi
KUBECTX="${HOME}/.kube/kubectx"
KUBECTX="${XDG_CACHE_HOME:-$HOME/.kube}/kubectx"
usage() {
cat <<"EOF"
local SELF
SELF="kubectx"
if [[ "$(basename "$0")" == kubectl-* ]]; then # invoked as plugin
SELF="kubectl ctx"
fi
cat <<EOF
USAGE:
kubectx : list the contexts
kubectx <NAME> : switch to context
kubectx - : switch to the previous context
kubectx <NEW_NAME>=<NAME> : create alias for context
kubectx -h,--help : show this message
$SELF : list the contexts
$SELF <NAME> : switch to context <NAME>
$SELF - : switch to the previous context
$SELF -c, --current : show the current context name
$SELF <NEW_NAME>=<NAME> : rename context <NAME> to <NEW_NAME>
$SELF <NEW_NAME>=. : rename current-context to <NEW_NAME>
$SELF -d <NAME> [<NAME...>] : delete context <NAME> ('.' for current-context)
(this command won't delete the user/cluster entry
that is used by the context)
$SELF -u, --unset : unset the current context
$SELF -h,--help : show this message
EOF
exit 1
}
exit_err() {
echo >&2 "${1}"
exit 1
}
current_context() {
$KUBECTL config view -o=jsonpath='{.current-context}'
}
get_contexts() {
$KUBECTL config get-contexts -o=name | sort -n
}
list_contexts() {
set -u pipefail
local cur="$(current_context)"
local yellow=$(tput setaf 3)
local darkbg=$(tput setab 0)
local normal=$(tput sgr0)
local cur ctx_list
cur="$(current_context)" || exit_err "error getting current context"
ctx_list=$(get_contexts) || exit_err "error getting context list"
for c in $(get_contexts); do
if [[ "${c}" = "${cur}" ]]; then
echo "${darkbg}${yellow}${c}${normal}"
local yellow darkbg normal
yellow=$(tput setaf 3 || true)
darkbg=$(tput setab 0 || true)
normal=$(tput sgr0 || true)
local cur_ctx_fg cur_ctx_bg
cur_ctx_fg=${KUBECTX_CURRENT_FGCOLOR:-$yellow}
cur_ctx_bg=${KUBECTX_CURRENT_BGCOLOR:-$darkbg}
for c in $ctx_list; do
if [[ -n "${_KUBECTX_FORCE_COLOR:-}" || \
-t 1 && -z "${NO_COLOR:-}" ]]; then
# colored output mode
if [[ "${c}" = "${cur}" ]]; then
echo "${cur_ctx_bg}${cur_ctx_fg}${c}${normal}"
else
echo "${c}"
fi
else
echo "${c}"
fi
@@ -67,7 +99,8 @@ read_context() {
}
save_context() {
local saved="$(read_context)"
local saved
saved="$(read_context)"
if [[ "${saved}" != "${1}" ]]; then
printf %s "${1}" > "${KUBECTX}"
@@ -75,11 +108,25 @@ save_context() {
}
switch_context() {
kubectl config use-context "${1}"
$KUBECTL config use-context "${1}"
}
choose_context_interactive() {
local choice
choice="$(_KUBECTX_FORCE_COLOR=1 \
FZF_DEFAULT_COMMAND="${SELF_CMD}" \
fzf --ansi --no-preview || true)"
if [[ -z "${choice}" ]]; then
echo 2>&1 "error: you did not choose any of the options"
exit 1
else
set_context "${choice}"
fi
}
set_context() {
local prev="$(current_context)"
local prev
prev="$(current_context)" || exit_err "error getting current context"
switch_context "${1}"
@@ -89,7 +136,8 @@ set_context() {
}
swap_context() {
local ctx="$(read_context)"
local ctx
ctx="$(read_context)"
if [[ -z "${ctx}" ]]; then
echo "error: No previous context found." >&2
exit 1
@@ -97,56 +145,105 @@ swap_context() {
set_context "${ctx}"
}
user_of_context() {
kubectl config view \
-o=jsonpath="{.contexts[?(@.name==\"${1}\")].context.user}"
context_exists() {
grep -q ^"${1}"\$ <($KUBECTL config get-contexts -o=name)
}
cluster_of_context() {
kubectl config view \
-o=jsonpath="{.contexts[?(@.name==\"${1}\")].context.cluster}"
}
alias_context() {
rename_context() {
local old_name="${1}"
local new_name="${2}"
local old_user="$(user_of_context "${old_name}")"
local old_cluster="$(cluster_of_context "${old_name}")"
if [[ "${old_name}" == "." ]]; then
old_name="$(current_context)"
fi
if [[ -z "$old_user" || -z "$old_cluster" ]]; then
echo "error: Cannot retrieve context ${old_name}." >&2
if ! context_exists "${old_name}"; then
echo "error: Context \"${old_name}\" not found, can't rename it." >&2
exit 1
fi
kubectl config set-context "${new_name}" \
--cluster="${old_cluster}" \
--user="${old_user}" \
if context_exists "${new_name}"; then
echo "Context \"${new_name}\" exists, deleting..." >&2
$KUBECTL config delete-context "${new_name}" 1>/dev/null 2>&1
fi
echo "Aliased \"${old_name}\" as \"${new_name}\"." >&2
$KUBECTL config rename-context "${old_name}" "${new_name}"
}
delete_contexts() {
for i in "${@}"; do
delete_context "${i}"
done
}
delete_context() {
local ctx
ctx="${1}"
if [[ "${ctx}" == "." ]]; then
ctx="$(current_context)" || exit_err "error getting current context"
fi
echo "Deleting context \"${ctx}\"..." >&2
$KUBECTL config delete-context "${ctx}"
}
unset_context() {
echo "Unsetting current context." >&2
$KUBECTL config unset current-context
}
main() {
if [[ -z "${KUBECTL:-}" ]]; then
if hash kubectl 2>/dev/null; then
KUBECTL=kubectl
elif hash kubectl.exe 2>/dev/null; then
KUBECTL=kubectl.exe
else
echo >&2 "kubectl is not installed"
exit 1
fi
fi
if [[ "$#" -eq 0 ]]; then
list_contexts
if [[ -t 1 && -z "${KUBECTX_IGNORE_FZF:-}" && "$(type fzf &>/dev/null; echo $?)" -eq 0 ]]; then
choose_context_interactive
else
list_contexts
fi
elif [[ "${1}" == "-d" ]]; then
if [[ "$#" -lt 2 ]]; then
echo "error: missing context NAME" >&2
usage
exit 1
fi
delete_contexts "${@:2}"
elif [[ "$#" -gt 1 ]]; then
echo "error: too many flags" >&2
echo "error: too many arguments" >&2
usage
exit 1
elif [[ "$#" -eq 1 ]]; then
if [[ "${1}" == "-" ]]; then
swap_context
elif [[ "${1}" == '-c' || "${1}" == '--current' ]]; then
# we don't call current_context here for two reasons:
# - it does not fail when current-context property is not set
# - it does not return a trailing newline
$KUBECTL config current-context
elif [[ "${1}" == '-u' || "${1}" == '--unset' ]]; then
unset_context
elif [[ "${1}" == '-h' || "${1}" == '--help' ]]; then
usage
elif [[ "${1}" =~ ^-(.*) ]]; then
echo "error: unrecognized flag \"${1}\"" >&2
usage
exit 1
elif [[ "${1}" =~ (.+)=(.+) ]]; then
alias_context "${BASH_REMATCH[2]}" "${BASH_REMATCH[1]}"
rename_context "${BASH_REMATCH[2]}" "${BASH_REMATCH[1]}"
else
set_context "${1}"
fi
else
usage
exit 1
fi
}

150
kubens
View File

@@ -1,6 +1,6 @@
#!/bin/bash
#!/usr/bin/env bash
#
# kubenx(1) is a utility to switch between Kubernetes namespaces.
# kubens(1) is a utility to switch between Kubernetes namespaces.
# Copyright 2017 Google Inc.
#
@@ -21,31 +21,39 @@
set -eou pipefail
IFS=$'\n\t'
SCRIPT_DIR="$(dirname "$( readlink -f "${0}" 2>/dev/null || \
python -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "${0}" )")"
SELF_CMD="$0"
if [[ -f "${SCRIPT_DIR}/utils.bash" ]]; then
source "${SCRIPT_DIR}/utils.bash"
else
source "${SCRIPT_DIR}/../include/utils.bash"
fi
KUBENS_DIR="${HOME}/.kube/kubens"
KUBENS_DIR="${XDG_CACHE_HOME:-$HOME/.kube}/kubens"
usage() {
cat <<"EOF"
local SELF
SELF="kubens"
if [[ "$(basename "$0")" == kubectl-* ]]; then # invoked as plugin
SELF="kubectl ns"
fi
cat <<EOF
USAGE:
kubens : list the namespaces in the current context
kubens <NAME> : change the active namespace of current context
kubens - : switch to the previous namespace in this context
kubens -h,--help : show this message
$SELF : list the namespaces in the current context
$SELF <NAME> : change the active namespace of current context
$SELF - : switch to the previous namespace in this context
$SELF -c, --current : show the current namespace
$SELF -h,--help : show this message
EOF
exit 1
}
exit_err() {
echo >&2 "${1}"
exit 1
}
current_namespace() {
local cur_ctx=$(current_context)
ns="$(kubectl config view -o=jsonpath="{.contexts[?(@.name==\"${cur_ctx}\")].context.namespace}")"
local cur_ctx
cur_ctx="$(current_context)" || exit_err "error getting current context"
ns="$($KUBECTL config view -o=jsonpath="{.contexts[?(@.name==\"${cur_ctx}\")].context.namespace}")" \
|| exit_err "error getting current namespace"
if [[ -z "${ns}" ]]; then
echo "default"
else
@@ -53,20 +61,37 @@ current_namespace() {
fi
}
current_context() {
$KUBECTL config current-context
}
get_namespaces() {
$KUBECTL get namespaces -o=jsonpath='{range .items[*].metadata.name}{@}{"\n"}{end}'
}
escape_context_name() {
echo "${1//\//-}"
}
namespace_file() {
local ctx="${1}"
local ctx
ctx="$(escape_context_name "${1}")"
echo "${KUBENS_DIR}/${ctx}"
}
read_namespace() {
local f="$(namespace_file "${1}")"
local f
f="$(namespace_file "${1}")"
[[ -f "${f}" ]] && cat "${f}"
return 0
}
save_namespace() {
mkdir -p "${KUBENS_DIR}"
local f="$(namespace_file "${1}")"
local saved="$(read_namespace "${1}")"
local f saved
f="$(namespace_file "${1}")"
saved="$(read_namespace "${1}")"
if [[ "${saved}" != "${2}" ]]; then
printf %s "${2}" > "${f}"
@@ -75,13 +100,35 @@ save_namespace() {
switch_namespace() {
local ctx="${1}"
kubectl config set-context "${ctx}" --namespace="${2}"
$KUBECTL config set-context "${ctx}" --namespace="${2}"
echo "Active namespace is \"${2}\".">&2
}
choose_namespace_interactive() {
# directly calling kubens via fzf might fail with a cryptic error like
# "$FZF_DEFAULT_COMMAND failed", so try to see if we can list namespaces
# locally first
if [[ -z "$(list_namespaces)" ]]; then
echo >&2 "error: could not list namespaces (is the cluster accessible?)"
exit 1
fi
local choice
choice="$(_KUBECTX_FORCE_COLOR=1 \
FZF_DEFAULT_COMMAND="${SELF_CMD}" \
fzf --ansi --no-preview || true)"
if [[ -z "${choice}" ]]; then
echo 2>&1 "error: you did not choose any of the options"
exit 1
else
set_namespace "${choice}"
fi
}
set_namespace() {
local ctx="$(current_context)"
local prev="$(current_namespace)"
local ctx prev
ctx="$(current_context)" || exit_err "error getting current context"
prev="$(current_namespace)" || exit_error "error getting current namespace"
if grep -q ^"${1}"\$ <(get_namespaces); then
switch_namespace "${ctx}" "${1}"
@@ -96,24 +143,38 @@ set_namespace() {
}
list_namespaces() {
local cur="$(current_namespace)"
local yellow darkbg normal
yellow=$(tput setaf 3 || true)
darkbg=$(tput setab 0 || true)
normal=$(tput sgr0 || true)
local yellow=$(tput setaf 3)
local darkbg=$(tput setab 0)
local normal=$(tput sgr0)
local cur_ctx_fg cur_ctx_bg
cur_ctx_fg=${KUBECTX_CURRENT_FGCOLOR:-$yellow}
cur_ctx_bg=${KUBECTX_CURRENT_BGCOLOR:-$darkbg}
for c in $(get_namespaces); do
local cur ns_list
cur="$(current_namespace)" || exit_err "error getting current namespace"
ns_list=$(get_namespaces) || exit_err "error getting namespace list"
for c in $ns_list; do
if [[ -n "${_KUBECTX_FORCE_COLOR:-}" || \
-t 1 && -z "${NO_COLOR:-}" ]]; then
# colored output mode
if [[ "${c}" = "${cur}" ]]; then
echo "${darkbg}${yellow}${c}${normal}"
echo "${cur_ctx_bg}${cur_ctx_fg}${c}${normal}"
else
echo "${c}"
fi
else
echo "${c}"
fi
done
}
swap_namespace() {
local ctx="$(current_context)"
local ns="$(read_namespace "${ctx}")"
local ctx ns
ctx="$(current_context)" || exit_err "error getting current context"
ns="$(read_namespace "${ctx}")"
if [[ -z "${ns}" ]]; then
echo "error: No previous namespace found for current context." >&2
exit 1
@@ -122,16 +183,34 @@ swap_namespace() {
}
main() {
if [[ -z "${KUBECTL:-}" ]]; then
if hash kubectl 2>/dev/null; then
KUBECTL=kubectl
elif hash kubectl.exe 2>/dev/null; then
KUBECTL=kubectl.exe
else
echo >&2 "kubectl is not installed"
exit 1
fi
fi
if [[ "$#" -eq 0 ]]; then
list_namespaces
if [[ -t 1 && -z ${KUBECTX_IGNORE_FZF:-} && "$(type fzf &>/dev/null; echo $?)" -eq 0 ]]; then
choose_namespace_interactive
else
list_namespaces
fi
elif [[ "$#" -eq 1 ]]; then
if [[ "${1}" == '-h' || "${1}" == '--help' ]]; then
usage
elif [[ "${1}" == "-" ]]; then
swap_namespace
elif [[ "${1}" == '-c' || "${1}" == '--current' ]]; then
current_namespace
elif [[ "${1}" =~ ^-(.*) ]]; then
echo "error: unrecognized flag \"${1}\"" >&2
usage
exit 1
elif [[ "${1}" =~ (.+)=(.+) ]]; then
alias_context "${BASH_REMATCH[2]}" "${BASH_REMATCH[1]}"
else
@@ -140,6 +219,7 @@ main() {
else
echo "error: too many flags" >&2
usage
exit 1
fi
}

32
test/common.bash Normal file
View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bats
# bats setup function
setup() {
TEMP_HOME="$(mktemp -d)"
export TEMP_HOME
export HOME=$TEMP_HOME
export KUBECONFIG="${TEMP_HOME}/config"
}
# bats teardown function
teardown() {
rm -rf "$TEMP_HOME"
}
use_config() {
cp "$BATS_TEST_DIRNAME/testdata/$1" $KUBECONFIG
}
# wrappers around "kubectl config" command
get_namespace() {
kubectl config view -o=jsonpath="{.contexts[?(@.name==\"$(get_context)\")].context.namespace}"
}
get_context() {
kubectl config current-context
}
switch_context() {
kubectl config use-context "${1}"
}

244
test/kubectx.bats Normal file
View File

@@ -0,0 +1,244 @@
#!/usr/bin/env bats
COMMAND="${COMMAND:-$BATS_TEST_DIRNAME/../kubectx}"
load common
@test "--help should not fail" {
run ${COMMAND} --help
echo "$output"
[ "$status" -eq 0 ]
}
@test "-h should not fail" {
run ${COMMAND} -h
echo "$output"
[ "$status" -eq 0 ]
}
@test "switch to previous context when no one exists" {
use_config config1
run ${COMMAND} -
echo "$output"
[ "$status" -eq 1 ]
[[ $output = *"no previous context found" ]]
}
@test "list contexts when no kubeconfig exists" {
run ${COMMAND}
echo "$output"
[ "$status" -eq 0 ]
[[ "$output" = "warning: kubeconfig file not found" ]]
}
@test "get one context and list contexts" {
use_config config1
run ${COMMAND}
echo "$output"
[ "$status" -eq 0 ]
[[ "$output" = "user1@cluster1" ]]
}
@test "get two contexts and list contexts" {
use_config config2
run ${COMMAND}
echo "$output"
[ "$status" -eq 0 ]
[[ "$output" = *"user1@cluster1"* ]]
[[ "$output" = *"user2@cluster1"* ]]
}
@test "get two contexts and select contexts" {
use_config config2
run ${COMMAND} user1@cluster1
echo "$output"
[ "$status" -eq 0 ]
echo "$(get_context)"
[[ "$(get_context)" = "user1@cluster1" ]]
run ${COMMAND} user2@cluster1
echo "$output"
[ "$status" -eq 0 ]
echo "$(get_context)"
[[ "$(get_context)" = "user2@cluster1" ]]
}
@test "get two contexts and switch between contexts" {
use_config config2
run ${COMMAND} user1@cluster1
echo "$output"
[ "$status" -eq 0 ]
echo "$(get_context)"
[[ "$(get_context)" = "user1@cluster1" ]]
run ${COMMAND} user2@cluster1
echo "$output"
[ "$status" -eq 0 ]
echo "$(get_context)"
[[ "$(get_context)" = "user2@cluster1" ]]
run ${COMMAND} -
echo "$output"
[ "$status" -eq 0 ]
echo "$(get_context)"
[[ "$(get_context)" = "user1@cluster1" ]]
run ${COMMAND} -
echo "$output"
[ "$status" -eq 0 ]
echo "$(get_context)"
[[ "$(get_context)" = "user2@cluster1" ]]
}
@test "get one context and switch to non existent context" {
use_config config1
run ${COMMAND} "unknown-context"
echo "$output"
[ "$status" -eq 1 ]
}
@test "-c/--current fails when no context set" {
use_config config1
run "${COMMAND}" -c
echo "$output"
[ $status -eq 1 ]
run "${COMMAND}" --current
echo "$output"
[ $status -eq 1 ]
}
@test "-c/--current prints the current context" {
use_config config1
run "${COMMAND}" user1@cluster1
[ $status -eq 0 ]
run "${COMMAND}" -c
echo "$output"
[ $status -eq 0 ]
[[ "$output" = "user1@cluster1" ]]
run "${COMMAND}" --current
echo "$output"
[ $status -eq 0 ]
[[ "$output" = "user1@cluster1" ]]
}
@test "rename context" {
use_config config2
run ${COMMAND} "new-context=user1@cluster1"
echo "$output"
[ "$status" -eq 0 ]
run ${COMMAND}
echo "$output"
[ "$status" -eq 0 ]
[[ ! "$output" = *"user1@cluster1"* ]]
[[ "$output" = *"new-context"* ]]
[[ "$output" = *"user2@cluster1"* ]]
}
@test "rename current context" {
use_config config2
run ${COMMAND} user2@cluster1
echo "$output"
[ "$status" -eq 0 ]
run ${COMMAND} new-context=.
echo "$output"
[ "$status" -eq 0 ]
run ${COMMAND}
echo "$output"
[ "$status" -eq 0 ]
[[ ! "$output" = *"user2@cluster1"* ]]
[[ "$output" = *"user1@cluster1"* ]]
[[ "$output" = *"new-context"* ]]
}
@test "delete context" {
use_config config2
run ${COMMAND} -d "user1@cluster1"
echo "$output"
[ "$status" -eq 0 ]
run ${COMMAND}
echo "$output"
[ "$status" -eq 0 ]
[[ ! "$output" = "user1@cluster1" ]]
[[ "$output" = "user2@cluster1" ]]
}
@test "delete current context" {
use_config config2
run ${COMMAND} user2@cluster1
echo "$output"
[ "$status" -eq 0 ]
run ${COMMAND} -d .
echo "$output"
[ "$status" -eq 0 ]
run ${COMMAND}
echo "$output"
[ "$status" -eq 0 ]
[[ ! "$output" = "user2@cluster1" ]]
[[ "$output" = "user1@cluster1" ]]
}
@test "delete non existent context" {
use_config config1
run ${COMMAND} -d "unknown-context"
echo "$output"
[ "$status" -eq 1 ]
}
@test "delete several contexts" {
use_config config2
run ${COMMAND} -d "user1@cluster1" "user2@cluster1"
echo "$output"
[ "$status" -eq 0 ]
run ${COMMAND}
echo "$output"
[ "$status" -eq 0 ]
[[ "$output" = "" ]]
}
@test "delete several contexts including a non existent one" {
use_config config2
run ${COMMAND} -d "user1@cluster1" "non-existent" "user2@cluster1"
echo "$output"
[ "$status" -eq 1 ]
run ${COMMAND}
echo "$output"
[ "$status" -eq 0 ]
[[ "$output" = "user2@cluster1" ]]
}
@test "unset selected context" {
use_config config2
run ${COMMAND} user1@cluster1
[ "$status" -eq 0 ]
run ${COMMAND} -u
[ "$status" -eq 0 ]
run ${COMMAND} -c
[ "$status" -ne 0 ]
}

148
test/kubens.bats Normal file
View File

@@ -0,0 +1,148 @@
#!/usr/bin/env bats
COMMAND="${COMMAND:-$BATS_TEST_DIRNAME/../kubens}"
# TODO(ahmetb) remove this after bash implementations are deleted
export KUBECTL="$BATS_TEST_DIRNAME/../test/mock-kubectl"
# short-circuit namespace querying in kubens go implementation
export _MOCK_NAMESPACES=1
load common
@test "--help should not fail" {
run ${COMMAND} --help
echo "$output">&2
[[ "$status" -eq 0 ]]
}
@test "-h should not fail" {
run ${COMMAND} -h
echo "$output">&2
[[ "$status" -eq 0 ]]
}
@test "list namespaces when no kubeconfig exists" {
run ${COMMAND}
echo "$output"
[[ "$status" -eq 1 ]]
}
@test "list namespaces" {
use_config config1
switch_context user1@cluster1
run ${COMMAND}
echo "$output"
[[ "$status" -eq 0 ]]
[[ "$output" = *"ns1"* ]]
[[ "$output" = *"ns2"* ]]
}
@test "switch to existing namespace" {
use_config config1
switch_context user1@cluster1
run ${COMMAND} "ns1"
echo "$output"
[[ "$status" -eq 0 ]]
[[ "$output" = *'Active namespace is "ns1"'* ]]
}
@test "switch to non-existing namespace" {
use_config config1
switch_context user1@cluster1
run ${COMMAND} "unknown-namespace"
echo "$output"
[[ "$status" -eq 1 ]]
[[ "$output" = *'no namespace exists with name "unknown-namespace"'* ]]
}
@test "switch between namespaces" {
use_config config1
switch_context user1@cluster1
run ${COMMAND} ns1
echo "$output"
[[ "$status" -eq 0 ]]
echo "$(get_namespace)"
[[ "$(get_namespace)" = "ns1" ]]
run ${COMMAND} ns2
echo "$output"
[[ "$status" -eq 0 ]]
echo "$(get_namespace)"
[[ "$(get_namespace)" = "ns2" ]]
run ${COMMAND} -
echo "$output"
[[ "$status" -eq 0 ]]
echo "$(get_namespace)"
[[ "$(get_namespace)" = "ns1" ]]
run ${COMMAND} -
echo "$output"
[[ "$status" -eq 0 ]]
echo "$(get_namespace)"
[[ "$(get_namespace)" = "ns2" ]]
}
@test "switch to previous namespace when none exists" {
use_config config1
switch_context user1@cluster1
run ${COMMAND} -
echo "$output"
[[ "$status" -eq 1 ]]
[[ "$output" = *"No previous namespace found for current context"* ]]
}
@test "switch to namespace when current context is empty" {
use_config config1
run ${COMMAND} -
echo "$output"
[[ "$status" -eq 1 ]]
[[ "$output" = *"current-context is not set"* ]]
}
@test "-c/--current works when no namespace is set on context" {
use_config config1
switch_context user1@cluster1
run ${COMMAND} "-c"
echo "$output"
[[ "$status" -eq 0 ]]
[[ "$output" = "default" ]]
run ${COMMAND} "--current"
echo "$output"
[[ "$status" -eq 0 ]]
[[ "$output" = "default" ]]
}
@test "-c/--current prints the namespace after it is set" {
use_config config1
switch_context user1@cluster1
${COMMAND} ns1
run ${COMMAND} "-c"
echo "$output"
[[ "$status" -eq 0 ]]
[[ "$output" = "ns1" ]]
run ${COMMAND} "--current"
echo "$output"
[[ "$status" -eq 0 ]]
[[ "$output" = "ns1" ]]
}
@test "-c/--current fails when current context is not set" {
use_config config1
run ${COMMAND} -c
echo "$output"
[[ "$status" -eq 1 ]]
run ${COMMAND} --current
echo "$output"
[[ "$status" -eq 1 ]]
}

12
test/mock-kubectl Executable file
View File

@@ -0,0 +1,12 @@
#!/usr/bin/env bash
[[ -n $DEBUG ]] && set -x
set -eou pipefail
if [[ $@ == *'get namespaces'* ]]; then
echo "ns1"
echo "ns2"
else
kubectl $@
fi

18
test/testdata/config1 vendored Normal file
View File

@@ -0,0 +1,18 @@
# config with one context
apiVersion: v1
clusters:
- cluster:
server: ""
name: cluster1
contexts:
- context:
cluster: cluster1
user: user1
name: user1@cluster1
current-context: ""
kind: Config
preferences: {}
users:
- name: user1
user: {}

24
test/testdata/config2 vendored Normal file
View File

@@ -0,0 +1,24 @@
# config with two contexts
apiVersion: v1
clusters:
- cluster:
server: ""
name: cluster1
contexts:
- context:
cluster: cluster1
user: user1
name: user1@cluster1
- context:
cluster: cluster1
user: user2
name: user2@cluster1
current-context: ""
kind: Config
preferences: {}
users:
- name: user1
user: {}
- name: user2
user: {}

View File

@@ -1,11 +0,0 @@
current_context() {
kubectl config view -o=jsonpath='{.current-context}'
}
get_contexts() {
kubectl config get-contexts -o=name | sort -n
}
get_namespaces() {
kubectl get namespaces -o=jsonpath='{range .items[*].metadata.name}{@}{"\n"}{end}'
}