100 Commits

Author SHA1 Message Date
Ahmet Alp Balkan
b51befee82 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:08 -07:00
Ahmet Alp Balkan
be3e5b2d61 ns list: increase page size to 500
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:43:07 -07:00
Ahmet Alp Balkan
cf41febf16 Load namespaces using client-go
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:35:27 -07:00
Ahmet Alp Balkan
27a902174f fix compile error
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:16:26 -07:00
Ahmet Alp Balkan
84676b7062 deprecation msgs for KUBECTX_CURRENT_{BG,FG}COLOR
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 12:11:59 -07:00
Ahmet Alp Balkan
64e5a0ed13 Add interactive switching to kubens
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-29 11:55:28 -07:00
Ahmet Alp Balkan
ebfd724d08 Fix bug about where cur ns was stored in yaml
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-18 16:17:47 -07:00
Ahmet Alp Balkan
25833eaa29 kubens: implement namespace switching
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-18 16:10:34 -07:00
Ahmet Alp Balkan
99b593be90 kubens: Add facility to store state file
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-18 14:26:38 -07:00
Ahmet Alp Balkan
d0c352c5bf Implement list (via exec kubectl), clearer color settings
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-18 14:05:32 -07:00
Ahmet Alp Balkan
3e34177cb9 Move kubeconfig loader utils to cmdutil pkg
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-18 13:32:53 -07:00
Ahmet Alp Balkan
d4112ce088 kubens: Start implementing stubs
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-18 13:09:59 -07:00
Ahmet Alp Balkan
56f3370d36 Create test utils for crafting kubeconfig strings
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-16 21:37:29 -07:00
Ahmet Alp Balkan
7b96a338a3 extract kubeconfig test utils to a type
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-16 21:00:00 -07:00
Ahmet Alp Balkan
49539fbcb3 do not fail on non-existing kubeconfig files
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-16 19:55:34 -07:00
Ahmet Alp Balkan
10f53bb15b Better success msgs, handle -d without args
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-12 23:06:17 -07:00
Ahmet Alp Balkan
0ebccceeab Tidy up colors, help msgs, TODOs
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-12 23:05:22 -07:00
Ahmet Alp Balkan
57f2bb1eb4 Create printer pkg, fix color force enable/disable
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-12 22:30:10 -07:00
Ahmet Alp Balkan
0ab135af99 Move kubeconfig utility to a shared pkg
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-12 18:19:38 -07:00
Ahmet Alp Balkan
73c1f268ee Extend test coverage
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-12 18:17:03 -07:00
Ahmet Alp Balkan
562631ad2b Fix UnsupportedOp tests through custom comparer
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-12 18:03:06 -07:00
Ahmet Alp Balkan
077d8a829d Re-introduce DEBUG env var stack traces
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-12 17:57:57 -07:00
Ahmet Alp Balkan
195e6315da Update tests for homeDir and kubeconfigPath()
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-12 17:53:09 -07:00
Ahmet Alp Balkan
e5a09017d0 Unify errors from kubeconfig.Parse
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-12 16:20:34 -07:00
Ahmet Alp Balkan
37ba52f357 Extract env vars to a file + test
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-12 16:15:17 -07:00
Ahmet Alp Balkan
91e00f9867 Support for fzf, color ignore/force knobs
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-12 15:59:48 -07:00
Ahmet Alp Balkan
17f6ffe73b Move all yaml logic to pkg/kubeconfig
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-12 14:22:52 -07:00
Ahmet Alp Balkan
fb5e8bc904 Move ctx-related YAML parse methods to pkg
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-12 13:46:14 -07:00
Ahmet Alp Balkan
1313d98f57 Use kubeconfig pkg for parsing utils
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-12 13:17:08 -07:00
Ahmet Alp Balkan
94664bcaf9 kubeconfig pkg for loading/parsing
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-12 12:59:20 -07:00
Ahmet Alp Balkan
21d0a6aeeb add printSuccess, pass writers to print funcs
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-12 12:37:04 -07:00
Ahmet Alp Balkan
7c2cf62cf0 define Run(stdout,stderr) method on **Ops
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-12 12:29:08 -07:00
Ahmet Alp Balkan
68ea776826 add some TODOs
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-10 16:41:21 -07:00
Ahmet Alp Balkan
37441b648f Fix bugs for test pass, update tests
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-10 16:23:23 -07:00
Ahmet Alp Balkan
8ce95d4a00 Add support for renaming contexts
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-10 16:11:38 -07:00
Ahmet Alp Balkan
5ec2f4f032 Support for -d (deleting contexts)
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-10 15:38:50 -07:00
Ahmet Alp Balkan
32d65fc527 Add support for -u/--unset
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-10 15:04:31 -07:00
Ahmet Alp Balkan
c5696a46b7 Add support for -c/--current
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-10 14:50:14 -07:00
Ahmet Alp Balkan
5f40b12a4e Integrate ctx swap, check for wrong ctx names
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-10 14:41:57 -07:00
Ahmet Alp Balkan
74a30a60e0 Save last context name in state file
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-10 14:27:52 -07:00
Ahmet Alp Balkan
7a40a5ed07 Add utils for r/w ~/.kube/kubectx file
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-10 14:25:00 -07:00
Ahmet Alp Balkan
a9476f3215 Implement switch via editing yaml in-place
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-10 14:07:12 -07:00
Ahmet Alp Balkan
04e963c02c Implement context listing
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-10 13:21:52 -07:00
Ahmet Alp Balkan
da08491f0b Implement facilities to parse kubeconfig file
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-10 13:02:00 -07:00
Ahmet Alp Balkan
7c2f8ffa75 Add logic to determine kubeconfig path
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-10 12:34:31 -07:00
Ahmet Alp Balkan
d2267aa60c Support help op, add color to error
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-10 12:07:58 -07:00
Ahmet Alp Balkan
1b2fc5961a Handle supported operation in main
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-10 12:00:12 -07:00
Ahmet Alp Balkan
68a8276146 Start porting to Go: parse flags
Parse help/list/swap command line flags.

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2020-04-10 11:54:01 -07: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
66 changed files with 3780 additions and 92 deletions

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

@@ -0,0 +1,29 @@
name: release
on:
push:
tags:
- 'v*.*.*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@master
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
- name: Update new version for plugin 'ctx' in krew-index
uses: rajatjindal/krew-release-bot@v0.0.31
with:
krew_template_file: .krew/ctx.yaml
- name: Update new version for plugin 'ns' in krew-index
uses: rajatjindal/krew-release-bot@v0.0.31
with:
krew_template_file: .krew/ns.yaml

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: .

31
.krew/ns.yaml Normal file
View File

@@ -0,0 +1,31 @@
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.
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: kubens
files:
- from: kubectx-*/kubens
to: .
- from: kubectx-*/LICENSE
to: .

12
.travis.yml Normal file
View File

@@ -0,0 +1,12 @@
before_install:
- sudo add-apt-repository ppa:duggan/bats --yes
- sudo apt-get update -qq
- sudo apt-get install -qq bats
- sudo curl -fsSL -o /usr/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/v1.13.1/bin/linux/amd64/kubectl
- sudo chmod +x /usr/bin/kubectl
script:
- basename /usr/bin
- bats test/kubectx.bats
- bats test/kubens.bats
- shellcheck kubectx
- shellcheck kubens

122
README.md
View File

@@ -1,4 +1,13 @@
# `kubectx` + `kubens`: Power tools for kubectl
![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)
![Travis (.org) branch](https://img.shields.io/travis/ahmetb/kubectx/master.svg)
![Proudly written in Bash](https://img.shields.io/badge/written%20in-bash-ff69b4.svg)
This repository provides both `kubectx` and `kubens` tools.
[Install &rarr;](#installation)
**`kubectx`** helps you switch between clusters back and forth:
@@ -9,18 +18,20 @@ This repository provides both `kubectx` and `kubens` tools.
# kubectx(1)
kubectx is an utility to manage and switch between kubectl(1) contexts.
kubectx is a utility to manage and switch between kubectl(1) contexts.
```
USAGE:
kubectx : list the contexts
kubectx <NAME> : switch to context <NAME>
kubectx - : switch to the previous context
kubectx -c, --current : show the current context name
kubectx <NEW_NAME>=<NAME> : rename context <NAME> to <NEW_NAME>
kubectx <NEW_NAME>=. : rename current-context to <NEW_NAME>
kubectx -d <NAME> : delete context <NAME> ('.' for current-context)
(this command won't delete the user/cluster entry
that is used by the context)
kubectx -u, --unset : unset the current context
```
### Usage
@@ -47,13 +58,14 @@ long context names. You don't have to remember full context names anymore.
# kubens(1)
kubens is an utility to switch between Kubernetes namespaces.
kubens is a utility to switch between Kubernetes namespaces.
```
USAGE:
kubens : list the namespaces
kubens <NAME> : change the active namespace
kubens - : switch to the previous namespace
kubens -c, --current : show the current namespace
```
@@ -75,24 +87,53 @@ Active namespace is "default".
## Installation
There are several installation options:
- As kubectl plugins (macOS/Linux)
- macOS
- Homebrew (recommended)
- MacPorts
- Linux
- manual installation/upgrades
- Arch Linux
- Debian/Ubuntu
### Kubectl Plugins (macOS and Linux)
You can install and use [Krew](https://github.com/kubernetes-sigs/krew/) kubectl
plugin manager to get `kubectx` and `kubens`. **NOTE:** This will not install
shell completion scripts, if you want those, choose another installation method
below.
```sh
kubectl krew install ctx
kubectl krew install ns
```
After installing, the tools will be available as `kubectl ctx` and `kubectl ns`.
### macOS
:confetti_ball: Use the [Homebrew](https://brew.sh/) package manager:
#### Homebrew
:confetti_ball: If you use [Homebrew](https://brew.sh/) you can install like this:
brew install kubectx
This command will set up bash/zsh/fish completion scripts automatically.
- Running `brew install` with `--with-short-names` will install tools with names
`kctx` and `kns` to prevent prefix collision with `kubectl` name.
- If you like to add context/namespace info to your shell prompt (`$PS1`),
I recommend trying out [kube-ps1](https://github.com/jonmosco/kube-ps1).
#### MacPorts
If you use [MacPorts](https://www.macports.org) you can install like this:
sudo port install kubectx
### Linux
Since `kubectx`/`kubens` are written in Bash, you should be able to instal
Since `kubectx`/`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.
@@ -101,7 +142,38 @@ them to any POSIX environment that has Bash installed.
- 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 ...`)
- Figure out how to install bash/zsh/fish [completion scripts](completion/).
- Install bash/zsh/fish [completion scripts](completion/).
- For zsh:
The completion scripts have to be in a path that belongs to `$fpath`. Either link or copy them to an existing folder.
If using oh-my-zsh you can do as follows:
```bash
mkdir -p ~/.oh-my-zsh/completions
chmod -R 755 ~/.oh-my-zsh/completions
ln -s /opt/kubectx/completion/kubectx.zsh ~/.oh-my-zsh/completions/_kubectx.zsh
ln -s /opt/kubectx/completion/kubens.zsh ~/.oh-my-zsh/completions/_kubens.zsh
```
Note that the leading underscore seems to be a convention. 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 not using oh-my-zsh, you could link to `/usr/share/zsh/functions/Completion` (might require sudo), depending on the `$fpath` of your zsh installation.
In case of error, calling `compaudit` might help.
- 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 << FOE >> ~/.bashrc
#kubectx and kubens
export PATH=~/.kubectx:\$PATH
FOE
```
- 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/
```
Example installation steps:
@@ -113,9 +185,11 @@ sudo ln -s /opt/kubectx/kubens /usr/local/bin/kubens
#### Arch Linux
An unofficial [AUR package](https://aur.archlinux.org/packages/kubectx) `kubectx`
is available. Install instructions can be found on the [Arch
wiki](https://wiki.archlinux.org/index.php/Arch_User_Repository#Installing_packages).
Available as official Arch Linux package. Install it via:
```bash
sudo pacman -S kubectx
```
#### Debian/Ubuntu
@@ -127,16 +201,29 @@ sudo apt install kubectx
-----
### Customizing current context colors
### 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`.
-----
### 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/)):
```
export KUBECTX_CURRENT_FGCOLOR=$(tput setaf 6) # blue text
export KUBECTX_CURRENT_BGCOLOR=$(tput setaf 7) # white background
export KUBECTX_CURRENT_BGCOLOR=$(tput setab 7) # white background
```
Colors in the output can be disabled by setting the
Colors in the output can be disabled by setting the
[`NO_COLOR`](http://no-color.org/) environment variable.
-----
@@ -151,7 +238,8 @@ Colors in the output can be disabled by setting the
| _“Also using it on a daily basis. This and my zsh config that shows me the current k8s context 😉”_ [@puja108](https://twitter.com/puja108/status/928742521139810305) |
| _“Lately I've found myself using the kubens command more than kubectx. Both very useful though :-)”_ [@stuartleeks](https://twitter.com/stuartleeks/status/928562850464907264) |
| _“yeah kubens rocks!”_ [@embano1](https://twitter.com/embano1/status/928698440732815360) |
| _“Special thanks to Ahmet Alp Balkan for creating kubectx, kubens, and kubectl aliases, as these tools made my life better.”_ [@strebeld](https://medium.com/@strebeld/5-ways-to-enhance-kubectl-ux-97c8893227a)
| _“Special thanks to Ahmet Alp Balkan for creating kubectx, kubens, and kubectl aliases, as these tools made my life better.”_ [@strebeld](https://medium.com/@strebeld/5-ways-to-enhance-kubectl-ux-97c8893227a) |
| _“❤ this shell script @ahmetb wrote to help make switching between kubectl config contexts a breeze.”_ [@briandanowski](https://twitter.com/briandanowski/status/1085409568165896193) |
> If you liked `kubectx`, you may like my [`kubectl-aliases`](https://github.com/ahmetb/kubectl-aliases) project, too.
@@ -163,4 +251,4 @@ Disclaimer: This is not an official Google product.
#### Stargazers over time
[![Stargazers over time](https://starcharts.herokuapp.com/ahmetb/kubectx.svg)](https://starcharts.herokuapp.com/ahmetb/kubectx)
![Google Analytics](https://ga-beacon.appspot.com/UA-2609286-17/kubectx/README?pixel)

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

@@ -0,0 +1,29 @@
package main
import (
"fmt"
"io"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"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(cmdutil.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")
}

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

@@ -0,0 +1,63 @@
package main
import (
"io"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"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 inefficency 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 %q", 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(cmdutil.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")
}

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

@@ -0,0 +1 @@
package main

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

@@ -0,0 +1,58 @@
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 {
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 == "--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")}
}

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

@@ -0,0 +1,84 @@
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)
}
})
}
}

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

@@ -0,0 +1,59 @@
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
}
func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error {
// parse kubeconfig just to see if it can be loaded
kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.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
}

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

@@ -0,0 +1,48 @@
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`
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"
}

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

@@ -0,0 +1,23 @@
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=%q", out)
}
if !strings.HasSuffix(out, "\n") {
t.Errorf("does not end with New line; output=%q", out)
}
}

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

@@ -0,0 +1,41 @@
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(cmdutil.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
}

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

@@ -0,0 +1,30 @@
package main
import (
"fmt"
"io"
"os"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/env"
"github.com/ahmetb/kubectx/internal/printer"
)
type Op interface {
Run(stdout, stderr io.Writer) error
}
func main() {
cmdutil.PrintDeprecatedEnvWarnings(os.Stderr, os.Environ())
op := parseArgs(os.Args[1:])
if err := op.Run(os.Stdout, os.Stderr); err != nil {
printer.Error(os.Stderr, err.Error())
if _, ok := os.LookupEnv(env.EnvDebug); ok {
// print stack trace in verbose mode
fmt.Fprintf(os.Stderr, "[DEBUG] error: %+v\n", err)
}
defer os.Exit(1)
}
}

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

@@ -0,0 +1,75 @@
package main
import (
"io"
"strings"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"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(cmdutil.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 %q not found, can't rename it", op.Old)
}
if kc.ContextExists(op.New) {
printer.Warning(stderr, "context %q 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,69 @@
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)
}
})
}
}

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

@@ -0,0 +1,39 @@
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)
}

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

@@ -0,0 +1,91 @@
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=%q", 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=%q; got=%q", 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=%q; expected=%q", 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=%q got=%q", 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)
}
}

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

@@ -0,0 +1,79 @@
package main
import (
"io"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"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 %q.", 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(cmdutil.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: %q", 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)
}

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

@@ -0,0 +1,32 @@
package main
import (
"io"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"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(cmdutil.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")
}

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

@@ -0,0 +1,32 @@
package main
import (
"fmt"
"io"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/kubeconfig"
)
type CurrentOp struct{}
func (c CurrentOp) Run(stdout, _ io.Writer) error {
kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.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 %q", ctx)
}
_, err = fmt.Fprintln(stdout, ns)
return errors.Wrap(err, "write error")
}

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

@@ -0,0 +1,43 @@
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 len(argv) == 1 {
v := argv[0]
if v == "--help" || v == "-h" {
return HelpOp{}
}
if v == "--current" || v == "-c" {
return CurrentOp{}
}
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")}
}

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

@@ -0,0 +1,63 @@
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 swap",
args: []string{"-"},
want: SwitchOp{Target: "-"}},
{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)
}
})
}
}

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

@@ -0,0 +1,60 @@
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(cmdutil.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)
if err != nil {
return errors.Wrap(err, "failed to switch context")
}
printer.Success(stderr, "Switched to context %s.", printer.SuccessColor.Sprint(name))
return nil
}

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

@@ -0,0 +1,44 @@
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% - : switch to the previous namespace in this context
%PROG% -c, --current : show the current namespace
%PROG% -h,--help : show this message
`
// 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 "kubectx"
}

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

@@ -0,0 +1,89 @@
package main
import (
"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/cmdutil"
"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(cmdutil.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
}
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")
}
clientset, err := kubernetes.NewForConfig(cfg)
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(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
}

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

@@ -0,0 +1,29 @@
package main
import (
"fmt"
"io"
"os"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/env"
"github.com/ahmetb/kubectx/internal/printer"
)
type Op interface {
Run(stdout, stderr io.Writer) error
}
func main() {
cmdutil.PrintDeprecatedEnvWarnings(os.Stderr, os.Environ())
op := parseArgs(os.Args[1:])
if err := op.Run(os.Stdout, os.Stderr); err != nil {
printer.Error(os.Stderr, err.Error())
if _, ok := os.LookupEnv(env.EnvDebug); ok {
// print stack trace in verbose mode
fmt.Fprintf(os.Stderr, "[DEBUG] error: %+v\n", err)
}
defer os.Exit(1)
}
}

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

@@ -0,0 +1,42 @@
package main
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"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 { return filepath.Join(f.dir, f.ctx) }
// 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)
}

View File

@@ -0,0 +1,38 @@
package main
import (
"io/ioutil"
"os"
"testing"
)
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()=%q; expected=%q", v, expected)
}
}

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

@@ -0,0 +1,89 @@
package main
import (
"io"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer"
)
type SwitchOp struct {
Target string // '-' for back and forth, or NAME
}
func (s SwitchOp) Run(_, stderr io.Writer) error {
kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
return errors.Wrap(err, "kubeconfig error")
}
toNS, err := switchNamespace(kc, s.Target)
if err != nil {
return err
}
err = printer.Success(stderr, "Active namespace is %q", toNS)
return err
}
func switchNamespace(kc *kubeconfig.Kubeconfig, ns string) (string, error) {
ctx := kc.GetCurrentContext()
if ctx == "" {
return "", errors.New("current-context is not set")
}
curNS, err := kc.NamespaceOfContext(ctx)
if ctx == "" {
return "", errors.New("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
}
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 %q", ns)
}
if err := kc.SetNamespace(ctx, ns); err != nil {
return "", errors.Wrapf(err, "failed to change to namespace %q", 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) {
nses, err := queryNamespaces(kc)
if err != nil {
return false, err
}
for _, v := range nses {
if v == ns {
return true, nil
}
}
return false, nil
}

View File

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

26
go.mod Normal file
View File

@@ -0,0 +1,26 @@
module github.com/ahmetb/kubectx
go 1.14
require (
facette.io/natsort v0.0.0-20181210072756-2cd4dd1e2dcb
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 // indirect
github.com/fatih/color v1.9.0
github.com/gogo/protobuf v1.3.1 // indirect
github.com/google/go-cmp v0.4.0
github.com/google/gofuzz v1.1.0 // indirect
github.com/googleapis/gnostic v0.1.0 // indirect
github.com/imdario/mergo v0.3.9 // indirect
github.com/mattn/go-isatty v0.0.12
github.com/onsi/ginkgo v1.11.0 // indirect
github.com/pkg/errors v0.9.1
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
k8s.io/apimachinery v0.17.0
k8s.io/client-go v0.17.0
k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c // indirect
k8s.io/utils v0.0.0-20200414100711-2df71ebbae66 // indirect
sigs.k8s.io/structured-merge-diff/v3 v3.0.0 // indirect
sigs.k8s.io/yaml v1.2.0 // indirect
)

263
go.sum Normal file
View File

@@ -0,0 +1,263 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
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/Azure/go-autorest/autorest v0.9.0 h1:MRvx8gncNaXJqOoLmhNjUAKh33JJF8LyxPhomEtOsjs=
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
github.com/Azure/go-autorest/autorest/adal v0.5.0 h1:q2gDruN08/guU9vAjuPWff0+QIrpH6ediguzdAzXAUU=
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
github.com/Azure/go-autorest/autorest/date v0.1.0 h1:YGrhWfrgtFs84+h0o46rJrlmsZtyZRg470CqAXTZaGM=
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY=
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k=
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/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/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
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/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.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
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/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/googleapis/gnostic v0.1.0 h1:rVsPeBmXbYv4If/cumu1AzZPwV58q433hvONV1UEZoI=
github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/gophercloud/gophercloud v0.1.0 h1:P/nh25+rzXouhytV2pUHBb65fnds26Ghl8/391+sT5o=
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
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/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
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 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
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 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
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 v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
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/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-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-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI=
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
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-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
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/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/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 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/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-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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=
k8s.io/api v0.17.0 h1:H9d/lw+VkZKEVIUc8F3wgiQ+FUXTTr21M87jXLU7yqM=
k8s.io/api v0.17.0/go.mod h1:npsyOePkeP0CPwyGfXDHxvypiYMJxBWAMpQxCaJ4ZxI=
k8s.io/api v0.17.1-beta.0/go.mod h1:t+ColJ7ZemYM/LXgLo4sVuO86DluzcnNHVKfZK4irZM=
k8s.io/api v0.17.1/go.mod h1:zxiAc5y8Ngn4fmhWUtSxuUlkfz1ixT7j9wESokELzOg=
k8s.io/api v0.17.2/go.mod h1:BS9fjjLc4CMuqfSO8vgbHPKMt5+SF0ET6u/RVDihTo4=
k8s.io/api v0.17.3-beta.0/go.mod h1:II7E2nD74NziEP/I5++IpJ/E4xAnLSVSxsWjEY7nTJc=
k8s.io/api v0.17.3/go.mod h1:YZ0OTkuw7ipbe305fMpIdf3GLXZKRigjtZaV5gzC2J0=
k8s.io/api v0.17.4-beta.0/go.mod h1:GvrPCgJXMjQy7jNXQyJTEtLb97iKYOPIRHf12YpPgsg=
k8s.io/api v0.17.4/go.mod h1:5qxx6vjmwUVG2nHQTKGlLts8Tbok8PzHl4vHtVFuZCA=
k8s.io/api v0.17.5-beta.0/go.mod h1:KZb7OowZyrErfJIgFiNbvk8Mz27wiFdZJzgoOg3Ij3k=
k8s.io/api v0.17.5/go.mod h1:0zV5/ungglgy2Rlm3QK8fbxkXVs+BSJWpJP/+8gUVLY=
k8s.io/api v0.17.6-beta.0/go.mod h1:VidxcyvUtKF2+Hul10U4/nUiefO2ZPcMffUpPx0a6F4=
k8s.io/api v0.18.0-alpha.1/go.mod h1:X82bXHlVEfxpVA9rO5PMaSOdQ+VdlSjT9A2Tl/CWL4A=
k8s.io/api v0.18.0-alpha.2/go.mod h1:jmDzGjASmjc+X3sojto6zy8iHsZEpgdnqHz0aWfRTEg=
k8s.io/api v0.18.0-alpha.4/go.mod h1:SvC64HywGrMEvfBPw7FcGXhoKTBfkFrpbuCoS2/gdiA=
k8s.io/api v0.18.0-alpha.5/go.mod h1:4R9YKKdSnQmR4J01O1TXy0QMouQ6r46A+9kwfhV7rZk=
k8s.io/api v0.18.0-beta.0/go.mod h1:VrRplS6LnRDM5Iq8CeqbtMAaAGU2iZDEoO3qNUe32FQ=
k8s.io/api v0.18.0-beta.1/go.mod h1:NcLIcCLuI/dH9R6reQzXe8l3GZMBqYyV7IpCg8ELWNw=
k8s.io/api v0.18.0-beta.2/go.mod h1:2oeNnWEqcSmaM/ibSh3t7xcIqbkGXhzZdn4ezV9T4m0=
k8s.io/api v0.18.0-rc.1/go.mod h1:ZOh6SbHjOYyaMLlWmB2+UOQKEWDpCnVEVpEyt7S2J9s=
k8s.io/api v0.18.0/go.mod h1:q2HRQkfDzHMBZL9l/y9rH63PkQl4vae0xRT+8prbrK8=
k8s.io/api v0.18.1-beta.0/go.mod h1:xZ5sMb6uqkZxXjcN6D2st2inR6dSqMs935Na0rw/1h4=
k8s.io/api v0.18.1/go.mod h1:3My4jorQWzSs5a+l7Ge6JBbIxChLnY8HnuT58ZWolss=
k8s.io/api v0.18.2-beta.0/go.mod h1:OdO5IPycJkPS91wMkDJJd/yfJpgZukVaxs0MA20Wn6g=
k8s.io/api v0.18.2 h1:wG5g5ZmSVgm5B+eHMIbI9EGATS2L8Z72rda19RIEgY8=
k8s.io/api v0.18.2/go.mod h1:SJCWI7OLzhZSvbY7U8zwNl9UA4o1fizoug34OV/2r78=
k8s.io/apimachinery v0.17.0 h1:xRBnuie9rXcPxUkDizUsGvPf1cnlZCFu210op7J7LJo=
k8s.io/apimachinery v0.17.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg=
k8s.io/apimachinery v0.18.2 h1:44CmtbmkzVDAhCpRVSiP2R5PPrC2RtlIv/MoB8xpdRA=
k8s.io/apimachinery v0.18.2/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA=
k8s.io/client-go v0.17.0 h1:8QOGvUGdqDMFrm9sD6IUFl256BcffynGoe80sxgTEDg=
k8s.io/client-go v0.17.0/go.mod h1:TYgR6EUHs6k45hb6KWjVD6jFZvJV4gHDikv/It0xz+k=
k8s.io/client-go v1.5.1 h1:XaX/lo2/u3/pmFau8HN+sB5C/b4dc4Dmm2eXjBH4p1E=
k8s.io/client-go v11.0.0+incompatible h1:LBbX2+lOwY9flffWlJM7f1Ct8V2SRNiMRDFeiwnJo9o=
k8s.io/client-go v11.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
k8s.io/utils v0.0.0-20200414100711-2df71ebbae66 h1:Ly1Oxdu5p5ZFmiVT71LFgeZETvMfZ1iBIGeOenT2JeM=
k8s.io/utils v0.0.0-20200414100711-2df71ebbae66/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e h1:4Z09Hglb792X0kfOBBJUPFEyvVfQWrYT/l8h5EKA6JQ=
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
sigs.k8s.io/structured-merge-diff/v3 v3.0.0 h1:dOmIZBMfhcHS09XZkMyUgkq5trg3/jRyJYFZUiaOp8E=
sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=

BIN
img/kubectx-interactive.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -0,0 +1,22 @@
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,35 @@
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': %q", v)
}
if !strings.Contains(v, "KUBECTX_CURRENT_BGCOLOR"){
t.Fatalf("output doesn't contain 'KUBECTX_CURRENT_BGCOLOR': %q", v)
}
}

View File

@@ -0,0 +1,30 @@
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()
}

View File

@@ -0,0 +1,82 @@
package cmdutil
import (
"os"
"path/filepath"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/kubeconfig"
)
var (
DefaultLoader kubeconfig.Loader = new(StandardKubeconfigLoader)
)
type StandardKubeconfigLoader struct{}
type kubeconfigFile struct{ *os.File }
func (*StandardKubeconfigLoader) Load() (kubeconfig.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")
}
return &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 := HomeDir()
if home == "" {
return "", errors.New("HOME or USERPROFILE environment variable not set")
}
return filepath.Join(home, ".kube", "config"), nil
}
func HomeDir() string {
if v := os.Getenv("XDG_CACHE_HOME"); v != "" {
return v
}
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,130 @@
package cmdutil
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/ahmetb/kubectx/internal/kubeconfig"
"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: "XDG_CACHE_HOME precedence",
envs: []env{
{"XDG_CACHE_HOME", "xdg"},
{"HOME", "home"},
},
want: "xdg",
},
{
name: "HOME over USERPROFILE",
envs: []env{
{"HOME", "home"},
{"USERPROFILE", "up"},
},
want: "home",
},
{
name: "only USERPROFILE available",
envs: []env{
{"XDG_CACHE_HOME", ""},
{"HOME", ""},
{"USERPROFILE", "up"},
},
want: "up",
},
{
name: "none available",
envs: []env{
{"XDG_CACHE_HOME", ""},
{"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()
}
})
}
}
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.Kubeconfig).WithLoader(DefaultLoader)
err := kc.Parse()
if err == nil {
t.Fatal("expected err")
}
if !IsNotFoundErr(err) {
t.Fatalf("expected ENOENT error; got=%v", err)
}
}

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

@@ -0,0 +1,18 @@
package env
const (
// EnvFZFIgnore describes the environment variable to set to disable
// interactive context selection when fzf is installed.
EnvFZFIgnore = "KUBECTX_IGNORE_FZF"
// EnvForceColor 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,69 @@
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,166 @@
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,72 @@
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 %q 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,75 @@
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,17 @@
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,55 @@
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=%q; got=%q", 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=%q; got=%q", 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=%q; got=%q", expected, out)
}
}

View File

@@ -0,0 +1,23 @@
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 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,69 @@
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 {
f, err := k.loader.Load()
if err != nil {
return errors.Wrap(err, "failed to load")
}
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,53 @@
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,63 @@
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,80 @@
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=%q got=%q", expected, v1)
}
v2, err := kc.NamespaceOfContext("c2")
if err != nil {
t.Fatal("expected err")
}
if expected := `c2n1`; v2 != expected {
t.Fatalf("c2: expected=%q got=%q", 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)
}
}

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

@@ -0,0 +1,40 @@
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,39 @@
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,45 @@
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,39 @@
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,26 @@
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,17 @@
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)
}
}
}

142
kubectx
View File

@@ -21,50 +21,71 @@
set -eou pipefail
IFS=$'\n\t'
KUBECTX="${HOME}/.kube/kubectx"
SELF_CMD="$0"
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 <NAME>
kubectx - : switch to the previous context
kubectx <NEW_NAME>=<NAME> : rename context <NAME> to <NEW_NAME>
kubectx <NEW_NAME>=. : rename current-context to <NEW_NAME>
kubectx -d <NAME> [<NAME...>] : delete context <NAME> ('.' for current-context)
$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
kubectx -h,--help : show this message
$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}'
$KUBECTL config view -o=jsonpath='{.current-context}'
}
get_contexts() {
kubectl config get-contexts -o=name | sort -n
$KUBECTL config get-contexts -o=name | sort -n
}
list_contexts() {
set -u pipefail
local cur
cur="$(current_context)"
local cur ctx_list
cur="$(current_context)" || exit_err "error getting current context"
ctx_list=$(get_contexts) || exit_err "error getting context list"
local yellow darkbg normal
yellow=$(tput setaf 3)
darkbg=$(tput setab 0)
normal=$(tput sgr0)
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 $(get_contexts); do
if [[ -t 1 && -z "${NO_COLOR:-}" && "${c}" = "${cur}" ]]; then
echo "${cur_ctx_bg}${cur_ctx_fg}${c}${normal}"
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
@@ -87,12 +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
prev="$(current_context)"
prev="$(current_context)" || exit_err "error getting current context"
switch_context "${1}"
@@ -111,20 +145,8 @@ swap_context() {
set_context "${ctx}"
}
user_of_context() {
# TODO(ahmetb) no longer used, consider deleting
kubectl config view \
-o=jsonpath="{.contexts[?(@.name==\"${1}\")].context.user}"
}
cluster_of_context() {
# TODO(ahmetb) no longer used, consider deleting
kubectl config view \
-o=jsonpath="{.contexts[?(@.name==\"${1}\")].context.cluster}"
}
context_exists() {
grep -q ^"${1}"\$ <(kubectl config get-contexts -o=name)
grep -q ^"${1}"\$ <($KUBECTL config get-contexts -o=name)
}
rename_context() {
@@ -135,26 +157,21 @@ rename_context() {
old_name="$(current_context)"
fi
# TODO(ahmetb) old_user and old_cluster are no longer used, clean up
local old_user old_cluster
old_user="$(user_of_context "${old_name}")"
old_cluster="$(cluster_of_context "${old_name}")"
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
if context_exists "${new_name}"; then
echo "Context \"${new_name}\" exists, deleting..." >&2
kubectl config delete-context "${new_name}" 1>/dev/null 2>&1
$KUBECTL config delete-context "${new_name}" 1>/dev/null 2>&1
fi
kubectl config rename-context "${old_name}" "${new_name}"
$KUBECTL config rename-context "${old_name}" "${new_name}"
}
delete_contexts() {
IFS=' ' read -ra CTXS <<< "${1}"
for i in "${CTXS[@]}"; do
for i in "${@}"; do
delete_context "${i}"
done
}
@@ -163,32 +180,60 @@ delete_context() {
local ctx
ctx="${1}"
if [[ "${ctx}" == "." ]]; then
ctx="$(current_context)"
ctx="$(current_context)" || exit_err "error getting current context"
fi
echo "Deleting context \"${ctx}\"..." >&2
kubectl config delete-context "${ctx}"
$KUBECTL config delete-context "${ctx}"
}
unset_context() {
echo "Unsetting current context." >&2
$KUBECTL config unset current-context
}
main() {
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
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 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
rename_context "${BASH_REMATCH[2]}" "${BASH_REMATCH[1]}"
else
@@ -196,6 +241,7 @@ main() {
fi
else
usage
exit 1
fi
}

113
kubens
View File

@@ -1,6 +1,6 @@
#!/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,23 +21,39 @@
set -eou pipefail
IFS=$'\n\t'
KUBENS_DIR="${HOME}/.kube/kubens"
SELF_CMD="$0"
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
cur_ctx="$(current_context)"
ns="$(kubectl config view -o=jsonpath="{.contexts[?(@.name==\"${cur_ctx}\")].context.namespace}")"
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
@@ -46,11 +62,11 @@ current_namespace() {
}
current_context() {
kubectl config view -o=jsonpath='{.current-context}'
$KUBECTL config current-context
}
get_namespaces() {
kubectl get namespaces -o=jsonpath='{range .items[*].metadata.name}{@}{"\n"}{end}'
$KUBECTL get namespaces -o=jsonpath='{range .items[*].metadata.name}{@}{"\n"}{end}'
}
escape_context_name() {
@@ -58,7 +74,9 @@ escape_context_name() {
}
namespace_file() {
local ctx="$(escape_context_name "${1}")"
local ctx
ctx="$(escape_context_name "${1}")"
echo "${KUBENS_DIR}/${ctx}"
}
@@ -82,14 +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 prev
ctx="$(current_context)"
prev="$(current_namespace)"
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}"
@@ -105,29 +144,36 @@ set_namespace() {
list_namespaces() {
local yellow darkbg normal
yellow=$(tput setaf 3)
darkbg=$(tput setab 0)
normal=$(tput sgr0)
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}
local cur ns_list
cur="$(current_namespace)"
ns_list=$(get_namespaces)
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 [[ -t 1 && -z "${NO_COLOR:-}" && "${c}" = "${cur}" ]]; then
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
done
}
swap_namespace() {
local ctx ns
ctx="$(current_context)"
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
@@ -137,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
@@ -155,6 +219,7 @@ main() {
else
echo "error: too many flags" >&2
usage
exit 1
fi
}

30
test/common.bash Normal file
View File

@@ -0,0 +1,30 @@
#!/usr/bin/env bats
# bats setup function
setup() {
export XDG_CACHE_HOME="$(mktemp -d)"
export KUBECONFIG="${XDG_CACHE_HOME}/config"
}
# bats teardown function
teardown() {
rm -rf "$XDG_CACHE_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: {}