├── .github
└── workflows
│ ├── ci.yml
│ └── release.yml
├── .goreleaser.yml
├── .krew
├── ctx.yaml
└── ns.yaml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── cmd
├── kubectx
│ ├── current.go
│ ├── delete.go
│ ├── env.go
│ ├── flags.go
│ ├── flags_test.go
│ ├── fzf.go
│ ├── help.go
│ ├── help_test.go
│ ├── list.go
│ ├── main.go
│ ├── rename.go
│ ├── rename_test.go
│ ├── state.go
│ ├── state_test.go
│ ├── switch.go
│ ├── unset.go
│ └── version.go
└── kubens
│ ├── current.go
│ ├── flags.go
│ ├── flags_test.go
│ ├── fzf.go
│ ├── help.go
│ ├── list.go
│ ├── main.go
│ ├── statefile.go
│ ├── statefile_test.go
│ ├── switch.go
│ └── version.go
├── completion
├── _kubectx.zsh
├── _kubens.zsh
├── kubectx.bash
├── kubectx.fish
├── kubens.bash
└── kubens.fish
├── go.mod
├── go.sum
├── img
├── kubectx-demo.gif
├── kubectx-interactive.gif
└── kubens-demo.gif
├── internal
├── cmdutil
│ ├── deprecated.go
│ ├── deprecated_test.go
│ ├── interactive.go
│ ├── util.go
│ └── util_test.go
├── env
│ └── constants.go
├── kubeconfig
│ ├── contextmodify.go
│ ├── contextmodify_test.go
│ ├── contexts.go
│ ├── contexts_test.go
│ ├── currentcontext.go
│ ├── currentcontext_test.go
│ ├── helper_test.go
│ ├── kubeconfig.go
│ ├── kubeconfig_test.go
│ ├── kubeconfigloader.go
│ ├── kubeconfigloader_test.go
│ ├── namespace.go
│ └── namespace_test.go
├── printer
│ ├── color.go
│ ├── color_test.go
│ └── printer.go
└── testutil
│ ├── kubeconfigbuilder.go
│ ├── tempfile.go
│ └── testutil.go
├── kubectx
├── kubens
└── test
├── common.bash
├── kubectx.bats
├── kubens.bats
├── mock-kubectl
└── testdata
├── config1
└── config2
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | # Copyright 2021 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | name: Go implementation (CI)
16 | on:
17 | push:
18 | pull_request:
19 | jobs:
20 | ci:
21 | runs-on: ubuntu-latest
22 | steps:
23 | - name: Checkout
24 | uses: actions/checkout@master
25 | - name: Setup Go
26 | uses: actions/setup-go@v2
27 | with:
28 | go-version: '1.22'
29 | - id: go-cache-paths
30 | run: |
31 | echo "::set-output name=go-build::$(go env GOCACHE)"
32 | echo "::set-output name=go-mod::$(go env GOMODCACHE)"
33 | - name: Go Build Cache
34 | uses: actions/cache@v2
35 | with:
36 | path: ${{ steps.go-cache-paths.outputs.go-build }}
37 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }}
38 | - name: Go Mod Cache
39 | uses: actions/cache@v2
40 | with:
41 | path: ${{ steps.go-cache-paths.outputs.go-mod }}
42 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}
43 | - name: Ensure gofmt
44 | run: test -z "$(gofmt -s -d .)"
45 | - name: Ensure go.mod is already tidied
46 | run: go mod tidy && git diff --exit-code
47 | - name: Run unit tests
48 | run: go test ./...
49 | - name: Build with Goreleaser
50 | uses: goreleaser/goreleaser-action@v2
51 | with:
52 | version: latest
53 | args: release --snapshot --skip publish,snapcraft --clean
54 | - name: Setup BATS framework
55 | run: sudo npm install -g bats
56 | - name: kubectx (Go) integration tests
57 | run: COMMAND=./dist/kubectx_linux_amd64_v1/kubectx bats test/kubectx.bats
58 | - name: kubens (Go) integration tests
59 | run: COMMAND=./dist/kubens_linux_amd64_v1/kubens bats test/kubens.bats
60 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | # Copyright 2021 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | name: Release
16 | on:
17 | push:
18 | tags:
19 | - 'v*.*.*'
20 | jobs:
21 | goreleaser:
22 | permissions:
23 | contents: write
24 | runs-on: ubuntu-latest
25 | steps:
26 | - name: Checkout
27 | uses: actions/checkout@master
28 | - run: git fetch --tags
29 | - name: Setup Go
30 | uses: actions/setup-go@v2
31 | with:
32 | go-version: '1.22'
33 | - name: Install Snapcraft
34 | uses: samuelmeuli/action-snapcraft@v1
35 | - name: Setup Snapcraft
36 | run: |
37 | # https://github.com/goreleaser/goreleaser/issues/1715
38 | mkdir -p $HOME/.cache/snapcraft/download
39 | mkdir -p $HOME/.cache/snapcraft/stage-packages
40 | - name: GoReleaser
41 | uses: goreleaser/goreleaser-action@v2
42 | with:
43 | version: latest
44 | args: release --clean
45 | env:
46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
47 | - name: Update new version for plugin 'ctx' in krew-index
48 | uses: rajatjindal/krew-release-bot@v0.0.38
49 | with:
50 | krew_template_file: .krew/ctx.yaml
51 | - name: Update new version for plugin 'ns' in krew-index
52 | uses: rajatjindal/krew-release-bot@v0.0.38
53 | with:
54 | krew_template_file: .krew/ns.yaml
55 | - name: Publish Snaps to the Snap Store (stable channel)
56 | run: for snap in $(ls dist/*.snap); do snapcraft upload --release=stable $snap; done
57 | env:
58 | SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
59 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json
2 |
3 | # Copyright 2021 Google LLC
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | # This is an example goreleaser.yaml file with some sane defaults.
18 | # Make sure to check the documentation at http://goreleaser.com
19 |
20 | version: 2
21 | before:
22 | hooks:
23 | - go mod download
24 | builds:
25 | - id: kubectx
26 | main: ./cmd/kubectx
27 | binary: kubectx
28 | env:
29 | - CGO_ENABLED=0
30 | goos:
31 | - linux
32 | - darwin
33 | - windows
34 | goarch:
35 | - amd64
36 | - arm
37 | - arm64
38 | - ppc64le
39 | - s390x
40 | goarm: [6, 7]
41 | - id: kubens
42 | main: ./cmd/kubens
43 | binary: kubens
44 | env:
45 | - CGO_ENABLED=0
46 | goos:
47 | - linux
48 | - darwin
49 | - windows
50 | goarch:
51 | - amd64
52 | - arm
53 | - arm64
54 | - ppc64le
55 | - s390x
56 | goarm: [6, 7]
57 | archives:
58 | - id: kubectx-archive
59 | name_template: |-
60 | kubectx_{{ .Tag }}_{{ .Os }}_
61 | {{- with .Arch -}}
62 | {{- if (eq . "386") -}}i386
63 | {{- else if (eq . "amd64") -}}x86_64
64 | {{- else -}}{{- . -}}
65 | {{- end -}}
66 | {{ end }}
67 | {{- with .Arm -}}
68 | {{- if (eq . "6") -}}hf
69 | {{- else -}}v{{- . -}}
70 | {{- end -}}
71 | {{- end -}}
72 | builds:
73 | - kubectx
74 | format_overrides:
75 | - goos: windows
76 | format: zip
77 | files: ["LICENSE"]
78 | - id: kubens-archive
79 | name_template: |-
80 | kubens_{{ .Tag }}_{{ .Os }}_
81 | {{- with .Arch -}}
82 | {{- if (eq . "386") -}}i386
83 | {{- else if (eq . "amd64") -}}x86_64
84 | {{- else -}}{{- . -}}
85 | {{- end -}}
86 | {{ end }}
87 | {{- with .Arm -}}
88 | {{- if (eq . "6") -}}hf
89 | {{- else -}}v{{- . -}}
90 | {{- end -}}
91 | {{- end -}}
92 | builds:
93 | - kubens
94 | format_overrides:
95 | - goos: windows
96 | format: zip
97 | files: ["LICENSE"]
98 | checksum:
99 | name_template: "checksums.txt"
100 | algorithm: sha256
101 | release:
102 | extra_files:
103 | - glob: ./kubens
104 | - glob: ./kubectx
105 | snapcrafts:
106 | - id: kubectx
107 | name: kubectx
108 | summary: 'kubectx + kubens: Power tools for kubectl'
109 | description: |
110 | kubectx is a tool to switch between contexts (clusters) on kubectl faster.
111 | kubens is a tool to switch between Kubernetes namespaces (and configure them for kubectl) easily.
112 | grade: stable
113 | confinement: classic
114 | base: core20
115 | apps:
116 | kubectx:
117 | command: kubectx
118 | completer: completion/kubectx.bash
119 | kubens:
120 | command: kubens
121 | completer: completion/kubens.bash
122 |
--------------------------------------------------------------------------------
/.krew/ctx.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: krew.googlecontainertools.github.com/v1alpha2
2 | kind: Plugin
3 | metadata:
4 | name: ctx
5 | spec:
6 | homepage: https://github.com/ahmetb/kubectx
7 | shortDescription: Switch between contexts in your kubeconfig
8 | version: {{ .TagName }}
9 | description: |
10 | Also known as "kubectx", a utility to switch between context entries in
11 | your kubeconfig file efficiently.
12 | caveats: |
13 | If fzf is installed on your machine, you can interactively choose
14 | between the entries using the arrow keys, or by fuzzy searching
15 | as you type.
16 | See https://github.com/ahmetb/kubectx for customization and details.
17 | platforms:
18 | - selector:
19 | matchExpressions:
20 | - key: os
21 | operator: In
22 | values:
23 | - darwin
24 | - linux
25 | {{addURIAndSha "https://github.com/ahmetb/kubectx/archive/{{ .TagName }}.tar.gz" .TagName }}
26 | bin: kubectx
27 | files:
28 | - from: kubectx-*/kubectx
29 | to: .
30 | - from: kubectx-*/LICENSE
31 | to: .
32 |
--------------------------------------------------------------------------------
/.krew/ns.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: krew.googlecontainertools.github.com/v1alpha2
2 | kind: Plugin
3 | metadata:
4 | name: ns
5 | spec:
6 | homepage: https://github.com/ahmetb/kubectx
7 | shortDescription: Switch between Kubernetes namespaces
8 | version: {{ .TagName }}
9 | description: |
10 | Also known as "kubens", a utility to set your current namespace and switch
11 | between them.
12 | caveats: |
13 | If fzf is installed on your machine, you can interactively choose
14 | between the entries using the arrow keys, or by fuzzy searching
15 | as you type.
16 | platforms:
17 | - selector:
18 | matchExpressions:
19 | - key: os
20 | operator: In
21 | values:
22 | - darwin
23 | - linux
24 | {{addURIAndSha "https://github.com/ahmetb/kubectx/archive/{{ .TagName }}.tar.gz" .TagName }}
25 | bin: kubens
26 | files:
27 | - from: kubectx-*/kubens
28 | to: .
29 | - from: kubectx-*/LICENSE
30 | to: .
31 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to this project must be accompanied by a Contributor License
9 | Agreement. You (or your employer) retain the copyright to your contribution,
10 | this simply gives us permission to use and redistribute your contributions as
11 | part of the project. Head over to to see
12 | your current agreements on file or to sign a new one.
13 |
14 | You generally only need to submit a CLA once, so if you've already submitted one
15 | (even if it was for a different project), you probably don't need to do it
16 | again.
17 |
18 | ## Code reviews
19 |
20 | All submissions, including submissions by project members, require review. We
21 | use GitHub pull requests for this purpose. Consult
22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
23 | information on using pull requests.
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # `kubectx` + `kubens`: Power tools for kubectl
2 |
3 | 
4 | 
5 | 
6 | [/badge.svg)](https://github.com/ahmetb/kubectx/actions?query=workflow%3A"Go+implementation+(CI)")
7 | 
8 |
9 | This repository provides both `kubectx` and `kubens` tools.
10 | [Install →](#installation)
11 |
12 | ## What are `kubectx` and `kubens`?
13 |
14 | **kubectx** is a tool to switch between contexts (clusters) on kubectl
15 | faster.
16 | **kubens** is a tool to switch between Kubernetes namespaces (and
17 | configure them for kubectl) easily.
18 |
19 | Here's a **`kubectx`** demo:
20 | 
21 |
22 | ...and here's a **`kubens`** demo:
23 | 
24 |
25 | ### Examples
26 |
27 | ```sh
28 | # switch to another cluster that's in kubeconfig
29 | $ kubectx minikube
30 | Switched to context "minikube".
31 |
32 | # switch back to previous cluster
33 | $ kubectx -
34 | Switched to context "oregon".
35 |
36 | # rename context
37 | $ kubectx dublin=gke_ahmetb_europe-west1-b_dublin
38 | Context "gke_ahmetb_europe-west1-b_dublin" renamed to "dublin".
39 |
40 | # change the active namespace on kubectl
41 | $ kubens kube-system
42 | Context "test" set.
43 | Active namespace is "kube-system".
44 |
45 | # go back to the previous namespace
46 | $ kubens -
47 | Context "test" set.
48 | Active namespace is "default".
49 |
50 | # change the active namespace even if it doesn't exist
51 | $ kubens not-found-namespace --force
52 | Context "test" set.
53 | Active namespace is "not-found-namespace".
54 | ---
55 | $ kubens not-found-namespace -f
56 | Context "test" set.
57 | Active namespace is "not-found-namespace".
58 | ```
59 |
60 | If you have [`fzf`](https://github.com/junegunn/fzf) installed, you can also
61 | **interactively** select a context or cluster, or fuzzy-search by typing a few
62 | characters. To learn more, read [interactive mode →](#interactive-mode)
63 |
64 | Both `kubectx` and `kubens` support Tab completion on bash/zsh/fish
65 | shells to help with long context names. You don't have to remember full context
66 | names anymore.
67 |
68 | -----
69 |
70 | ## Installation
71 |
72 | Stable versions of `kubectx` and `kubens` are small bash scripts that you
73 | can find in this repository.
74 |
75 | Starting with v0.9.0, `kubectx` and `kubens` **are now rewritten in Go**. They
76 | should work the same way (and we'll keep the bash-based implementations around)
77 | but the new features will be added to the new Go programs. Please help us test
78 | this new Go implementation by downloading the binaries from the [**Releases page
79 | →**](https://github.com/ahmetb/kubectx/releases)
80 |
81 | **Installation options:**
82 |
83 | - [as kubectl plugins (macOS & Linux)](#kubectl-plugins-macos-and-linux)
84 | - [with Homebrew (macOS & Linux)](#homebrew-macos-and-linux)
85 | - [with MacPorts (macOS)](#macports-macos)
86 | - [with apt (Debian)](#apt-debian)
87 | - [with pacman (Arch Linux)](#pacman-arch-linux)
88 | - [with Chocolatey (Windows)](#windows-installation-using-chocolatey)
89 | - [Windows Installation (using Scoop)](#windows-installation-using-scoop)
90 | - [with winget (Windows)](#windows-installation-using-winget)
91 | - [manually (macOS & Linux)](#manual-installation-macos-and-linux)
92 |
93 | If you like to add context/namespace information to your shell prompt (`$PS1`),
94 | you can try out [kube-ps1].
95 |
96 | [kube-ps1]: https://github.com/jonmosco/kube-ps1
97 |
98 | ### Kubectl Plugins (macOS and Linux)
99 |
100 | You can install and use the [Krew](https://github.com/kubernetes-sigs/krew/) kubectl
101 | plugin manager to get `kubectx` and `kubens`.
102 |
103 | **Note:** This will not install the shell completion scripts. If you want them,
104 | *choose another installation method
105 | or install the scripts [manually](#manual-installation-macos-and-linux).
106 |
107 | ```sh
108 | kubectl krew install ctx
109 | kubectl krew install ns
110 | ```
111 |
112 | After installing, the tools will be available as `kubectl ctx` and `kubectl ns`.
113 |
114 | ### Homebrew (macOS and Linux)
115 |
116 | If you use [Homebrew](https://brew.sh/) you can install like this:
117 |
118 | ```sh
119 | brew install kubectx
120 | ```
121 |
122 | This command will set up bash/zsh/fish completion scripts automatically. Make sure you [configure your shell](https://docs.brew.sh/Shell-Completion) to load completions for installed Homebrew formulas.
123 |
124 |
125 | ### MacPorts (macOS)
126 |
127 | If you use [MacPorts](https://www.macports.org) you can install like this:
128 |
129 | ```sh
130 | sudo port install kubectx
131 | ```
132 |
133 | ### apt (Debian)
134 |
135 | ``` bash
136 | sudo apt install kubectx
137 | ```
138 | Newer versions might be available on repos like
139 | [Debian Buster (testing)](https://packages.debian.org/buster/kubectx),
140 | [Sid (unstable)](https://packages.debian.org/sid/kubectx)
141 | (_if you are unfamiliar with the Debian release process and how to enable
142 | testing/unstable repos, check out the
143 | [Debian Wiki](https://wiki.debian.org/DebianReleases)_):
144 |
145 |
146 | ### pacman (Arch Linux)
147 |
148 | Available as official Arch Linux package. Install it via:
149 |
150 | ```bash
151 | sudo pacman -S kubectx
152 | ```
153 |
154 | ### Windows Installation (using Chocolatey)
155 |
156 | Available as packages on [Chocolatey](https://chocolatey.org/why-chocolatey)
157 | ```pwsh
158 | choco install kubens kubectx
159 | ```
160 |
161 | ### Windows Installation (using Scoop)
162 |
163 | Available as packages on [Scoop](https://scoop.sh/)
164 | ```pwsh
165 | scoop bucket add main
166 | scoop install main/kubens main/kubectx
167 | ```
168 |
169 | ### Windows Installation (using winget)
170 |
171 | Available as packages on [winget](https://learn.microsoft.com/en-us/windows/package-manager/)
172 | ```pwsh
173 | winget install --id ahmetb.kubectx
174 | winget install --id ahmetb.kubens
175 | ```
176 |
177 | ### Manual Installation (macOS and Linux)
178 |
179 | Since `kubectx` and `kubens` are written in Bash, you should be able to install
180 | them to any POSIX environment that has Bash installed.
181 |
182 | - Download the `kubectx`, and `kubens` scripts.
183 | - Either:
184 | - save them all to somewhere in your `PATH`,
185 | - or save them to a directory, then create symlinks to `kubectx`/`kubens` from
186 | somewhere in your `PATH`, like `/usr/local/bin`
187 | - Make `kubectx` and `kubens` executable (`chmod +x ...`)
188 |
189 | Example installation steps:
190 |
191 | ``` bash
192 | sudo git clone https://github.com/ahmetb/kubectx /opt/kubectx
193 | sudo ln -s /opt/kubectx/kubectx /usr/local/bin/kubectx
194 | sudo ln -s /opt/kubectx/kubens /usr/local/bin/kubens
195 | ```
196 |
197 | If you also want to have shell completions, pick an installation method for the
198 | [completion scripts](completion/) that fits your system best: [`zsh` with
199 | `antibody`](#completion-scripts-for-zsh-with-antibody), [plain
200 | `zsh`](#completion-scripts-for-plain-zsh),
201 | [`bash`](#completion-scripts-for-bash) or
202 | [`fish`](#completion-scripts-for-fish).
203 |
204 | #### Completion scripts for `zsh` with [antibody](https://getantibody.github.io)
205 |
206 | Add this line to your [Plugins File](https://getantibody.github.io/usage/) (e.g.
207 | `~/.zsh_plugins.txt`):
208 |
209 | ```
210 | ahmetb/kubectx path:completion kind:fpath
211 | ```
212 |
213 | Depending on your setup, you might or might not need to call `compinit` or
214 | `autoload -U compinit && compinit` in your `~/.zshrc` after you load the Plugins
215 | file. If you use [oh-my-zsh](https://github.com/ohmyzsh/ohmyzsh), load the
216 | completions before you load `oh-my-zsh` because `oh-my-zsh` will call
217 | `compinit`.
218 |
219 | #### Completion scripts for plain `zsh`
220 |
221 | The completion scripts have to be in a path that belongs to `$fpath`. Either
222 | link or copy them to an existing folder.
223 |
224 | Example with [`oh-my-zsh`](https://github.com/ohmyzsh/ohmyzsh):
225 |
226 | ```bash
227 | mkdir -p ~/.oh-my-zsh/custom/completions
228 | chmod -R 755 ~/.oh-my-zsh/custom/completions
229 | ln -s /opt/kubectx/completion/_kubectx.zsh ~/.oh-my-zsh/custom/completions/_kubectx.zsh
230 | ln -s /opt/kubectx/completion/_kubens.zsh ~/.oh-my-zsh/custom/completions/_kubens.zsh
231 | echo "fpath=($ZSH/custom/completions $fpath)" >> ~/.zshrc
232 | ```
233 |
234 | If completion doesn't work, add `autoload -U compinit && compinit` to your
235 | `.zshrc` (similar to
236 | [`zsh-completions`](https://github.com/zsh-users/zsh-completions/blob/master/README.md#oh-my-zsh)).
237 |
238 | If you are not using [`oh-my-zsh`](https://github.com/ohmyzsh/ohmyzsh), you
239 | could link to `/usr/share/zsh/functions/Completion` (might require sudo),
240 | depending on the `$fpath` of your zsh installation.
241 |
242 | In case of errors, calling `compaudit` might help.
243 |
244 | #### Completion scripts for `bash`
245 |
246 | ```bash
247 | git clone https://github.com/ahmetb/kubectx.git ~/.kubectx
248 | COMPDIR=$(pkg-config --variable=completionsdir bash-completion)
249 | ln -sf ~/.kubectx/completion/kubens.bash $COMPDIR/kubens
250 | ln -sf ~/.kubectx/completion/kubectx.bash $COMPDIR/kubectx
251 | cat << EOF >> ~/.bashrc
252 |
253 |
254 | #kubectx and kubens
255 | export PATH=~/.kubectx:\$PATH
256 | EOF
257 | ```
258 |
259 | #### Completion scripts for `fish`
260 |
261 | ```fish
262 | mkdir -p ~/.config/fish/completions
263 | ln -s /opt/kubectx/completion/kubectx.fish ~/.config/fish/completions/
264 | ln -s /opt/kubectx/completion/kubens.fish ~/.config/fish/completions/
265 | ```
266 |
267 | -----
268 |
269 | ### Interactive mode
270 |
271 | If you want `kubectx` and `kubens` commands to present you an interactive menu
272 | with fuzzy searching, you just need to [install
273 | `fzf`](https://github.com/junegunn/fzf) in your `$PATH`.
274 |
275 | 
276 |
277 | If you have `fzf` installed, but want to opt out of using this feature, set the
278 | environment variable `KUBECTX_IGNORE_FZF=1`.
279 |
280 | If you want to keep `fzf` interactive mode but need the default behavior of the
281 | command, you can do it by piping the output to another command (e.g. `kubectx |
282 | cat `).
283 |
284 | -----
285 |
286 | ### Customizing colors
287 |
288 | If you like to customize the colors indicating the current namespace or context,
289 | set the environment variables `KUBECTX_CURRENT_FGCOLOR` and
290 | `KUBECTX_CURRENT_BGCOLOR` (refer color codes
291 | [here](https://linux.101hacks.com/ps1-examples/prompt-color-using-tput/)):
292 |
293 | ```sh
294 | export KUBECTX_CURRENT_FGCOLOR=$(tput setaf 6) # blue text
295 | export KUBECTX_CURRENT_BGCOLOR=$(tput setab 7) # white background
296 | ```
297 |
298 | Colors in the output can be disabled by setting the
299 | [`NO_COLOR`](https://no-color.org/) environment variable.
300 |
301 | -----
302 |
303 | If you liked `kubectx`, you may like my
304 | [`kubectl-aliases`](https://github.com/ahmetb/kubectl-aliases) project, too. I
305 | recommend pairing kubectx and kubens with [fzf](#interactive-mode) and
306 | [kube-ps1].
307 |
308 | #### Stargazers over time
309 |
310 | [](https://starchart.cc/ahmetb/kubectx)
311 | 
312 |
--------------------------------------------------------------------------------
/cmd/kubectx/current.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "fmt"
19 | "io"
20 |
21 | "github.com/pkg/errors"
22 |
23 | "github.com/ahmetb/kubectx/internal/kubeconfig"
24 | )
25 |
26 | // CurrentOp prints the current context
27 | type CurrentOp struct{}
28 |
29 | func (_op CurrentOp) Run(stdout, _ io.Writer) error {
30 | kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
31 | defer kc.Close()
32 | if err := kc.Parse(); err != nil {
33 | return errors.Wrap(err, "kubeconfig error")
34 | }
35 |
36 | v := kc.GetCurrentContext()
37 | if v == "" {
38 | return errors.New("current-context is not set")
39 | }
40 | _, err := fmt.Fprintln(stdout, v)
41 | return errors.Wrap(err, "write error")
42 | }
43 |
--------------------------------------------------------------------------------
/cmd/kubectx/delete.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "io"
19 |
20 | "github.com/pkg/errors"
21 |
22 | "github.com/ahmetb/kubectx/internal/kubeconfig"
23 | "github.com/ahmetb/kubectx/internal/printer"
24 | )
25 |
26 | // DeleteOp indicates intention to delete contexts.
27 | type DeleteOp struct {
28 | Contexts []string // NAME or '.' to indicate current-context.
29 | }
30 |
31 | // deleteContexts deletes context entries one by one.
32 | func (op DeleteOp) Run(_, stderr io.Writer) error {
33 | for _, ctx := range op.Contexts {
34 | // TODO inefficiency here. we open/write/close the same file many times.
35 | deletedName, wasActiveContext, err := deleteContext(ctx)
36 | if err != nil {
37 | return errors.Wrapf(err, "error deleting context \"%s\"", deletedName)
38 | }
39 | if wasActiveContext {
40 | printer.Warning(stderr, "You deleted the current context. Use \"%s\" to select a new context.",
41 | selfName())
42 | }
43 |
44 | printer.Success(stderr, `Deleted context %s.`, printer.SuccessColor.Sprint(deletedName))
45 | }
46 | return nil
47 | }
48 |
49 | // deleteContext deletes a context entry by NAME or current-context
50 | // indicated by ".".
51 | func deleteContext(name string) (deleteName string, wasActiveContext bool, err error) {
52 | kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
53 | defer kc.Close()
54 | if err := kc.Parse(); err != nil {
55 | return deleteName, false, errors.Wrap(err, "kubeconfig error")
56 | }
57 |
58 | cur := kc.GetCurrentContext()
59 | // resolve "." to a real name
60 | if name == "." {
61 | if cur == "" {
62 | return deleteName, false, errors.New("can't use '.' as the no active context is set")
63 | }
64 | wasActiveContext = true
65 | name = cur
66 | }
67 |
68 | if !kc.ContextExists(name) {
69 | return name, false, errors.New("context does not exist")
70 | }
71 |
72 | if err := kc.DeleteContextEntry(name); err != nil {
73 | return name, false, errors.Wrap(err, "failed to modify yaml doc")
74 | }
75 | return name, wasActiveContext, errors.Wrap(kc.Save(), "failed to save modified kubeconfig file")
76 | }
77 |
--------------------------------------------------------------------------------
/cmd/kubectx/env.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
--------------------------------------------------------------------------------
/cmd/kubectx/flags.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "fmt"
19 | "io"
20 | "os"
21 | "strings"
22 |
23 | "github.com/ahmetb/kubectx/internal/cmdutil"
24 | )
25 |
26 | // UnsupportedOp indicates an unsupported flag.
27 | type UnsupportedOp struct{ Err error }
28 |
29 | func (op UnsupportedOp) Run(_, _ io.Writer) error {
30 | return op.Err
31 | }
32 |
33 | // parseArgs looks at flags (excl. executable name, i.e. argv[0])
34 | // and decides which operation should be taken.
35 | func parseArgs(argv []string) Op {
36 | if len(argv) == 0 {
37 | if cmdutil.IsInteractiveMode(os.Stdout) {
38 | return InteractiveSwitchOp{SelfCmd: os.Args[0]}
39 | }
40 | return ListOp{}
41 | }
42 |
43 | if argv[0] == "-d" {
44 | if len(argv) == 1 {
45 | if cmdutil.IsInteractiveMode(os.Stdout) {
46 | return InteractiveDeleteOp{SelfCmd: os.Args[0]}
47 | } else {
48 | return UnsupportedOp{Err: fmt.Errorf("'-d' needs arguments")}
49 | }
50 | }
51 | return DeleteOp{Contexts: argv[1:]}
52 | }
53 |
54 | if len(argv) == 1 {
55 | v := argv[0]
56 | if v == "--help" || v == "-h" {
57 | return HelpOp{}
58 | }
59 | if v == "--version" || v == "-V" {
60 | return VersionOp{}
61 | }
62 | if v == "--current" || v == "-c" {
63 | return CurrentOp{}
64 | }
65 | if v == "--unset" || v == "-u" {
66 | return UnsetOp{}
67 | }
68 |
69 | if new, old, ok := parseRenameSyntax(v); ok {
70 | return RenameOp{New: new, Old: old}
71 | }
72 |
73 | if strings.HasPrefix(v, "-") && v != "-" {
74 | return UnsupportedOp{Err: fmt.Errorf("unsupported option '%s'", v)}
75 | }
76 | return SwitchOp{Target: argv[0]}
77 | }
78 | return UnsupportedOp{Err: fmt.Errorf("too many arguments")}
79 | }
80 |
--------------------------------------------------------------------------------
/cmd/kubectx/flags_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "fmt"
19 | "testing"
20 |
21 | "github.com/google/go-cmp/cmp"
22 | )
23 |
24 | func Test_parseArgs_new(t *testing.T) {
25 | tests := []struct {
26 | name string
27 | args []string
28 | want Op
29 | }{
30 | {name: "nil Args",
31 | args: nil,
32 | want: ListOp{}},
33 | {name: "empty Args",
34 | args: []string{},
35 | want: ListOp{}},
36 | {name: "help shorthand",
37 | args: []string{"-h"},
38 | want: HelpOp{}},
39 | {name: "help long form",
40 | args: []string{"--help"},
41 | want: HelpOp{}},
42 | {name: "current shorthand",
43 | args: []string{"-c"},
44 | want: CurrentOp{}},
45 | {name: "current long form",
46 | args: []string{"--current"},
47 | want: CurrentOp{}},
48 | {name: "unset shorthand",
49 | args: []string{"-u"},
50 | want: UnsetOp{}},
51 | {name: "unset long form",
52 | args: []string{"--unset"},
53 | want: UnsetOp{}},
54 | {name: "switch by name",
55 | args: []string{"foo"},
56 | want: SwitchOp{Target: "foo"}},
57 | {name: "switch by swap",
58 | args: []string{"-"},
59 | want: SwitchOp{Target: "-"}},
60 | {name: "delete - without contexts",
61 | args: []string{"-d"},
62 | want: UnsupportedOp{fmt.Errorf("'-d' needs arguments")}},
63 | {name: "delete - current context",
64 | args: []string{"-d", "."},
65 | want: DeleteOp{[]string{"."}}},
66 | {name: "delete - multiple contexts",
67 | args: []string{"-d", ".", "a", "b"},
68 | want: DeleteOp{[]string{".", "a", "b"}}},
69 | {name: "rename context",
70 | args: []string{"a=b"},
71 | want: RenameOp{"a", "b"}},
72 | {name: "rename context with old=current",
73 | args: []string{"a=."},
74 | want: RenameOp{"a", "."}},
75 | {name: "unrecognized flag",
76 | args: []string{"-x"},
77 | want: UnsupportedOp{Err: fmt.Errorf("unsupported option '-x'")}},
78 | {name: "too many args",
79 | args: []string{"a", "b", "c"},
80 | want: UnsupportedOp{Err: fmt.Errorf("too many arguments")}},
81 | }
82 | for _, tt := range tests {
83 | t.Run(tt.name, func(t *testing.T) {
84 | got := parseArgs(tt.args)
85 |
86 | var opts cmp.Options
87 | if _, ok := tt.want.(UnsupportedOp); ok {
88 | opts = append(opts, cmp.Comparer(func(x, y UnsupportedOp) bool {
89 | return (x.Err == nil && y.Err == nil) || (x.Err.Error() == y.Err.Error())
90 | }))
91 | }
92 |
93 | if diff := cmp.Diff(got, tt.want, opts...); diff != "" {
94 | t.Errorf("parseArgs(%#v) diff: %s", tt.args, diff)
95 | }
96 | })
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/cmd/kubectx/fzf.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "bytes"
19 | "fmt"
20 | "io"
21 | "os"
22 | "os/exec"
23 | "strings"
24 |
25 | "github.com/pkg/errors"
26 |
27 | "github.com/ahmetb/kubectx/internal/cmdutil"
28 | "github.com/ahmetb/kubectx/internal/env"
29 | "github.com/ahmetb/kubectx/internal/kubeconfig"
30 | "github.com/ahmetb/kubectx/internal/printer"
31 | )
32 |
33 | type InteractiveSwitchOp struct {
34 | SelfCmd string
35 | }
36 |
37 | type InteractiveDeleteOp struct {
38 | SelfCmd string
39 | }
40 |
41 | func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error {
42 | // parse kubeconfig just to see if it can be loaded
43 | kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
44 | if err := kc.Parse(); err != nil {
45 | if cmdutil.IsNotFoundErr(err) {
46 | printer.Warning(stderr, "kubeconfig file not found")
47 | return nil
48 | }
49 | return errors.Wrap(err, "kubeconfig error")
50 | }
51 | kc.Close()
52 |
53 | cmd := exec.Command("fzf", "--ansi", "--no-preview")
54 | var out bytes.Buffer
55 | cmd.Stdin = os.Stdin
56 | cmd.Stderr = stderr
57 | cmd.Stdout = &out
58 |
59 | cmd.Env = append(os.Environ(),
60 | fmt.Sprintf("FZF_DEFAULT_COMMAND=%s", op.SelfCmd),
61 | fmt.Sprintf("%s=1", env.EnvForceColor))
62 | if err := cmd.Run(); err != nil {
63 | if _, ok := err.(*exec.ExitError); !ok {
64 | return err
65 | }
66 | }
67 | choice := strings.TrimSpace(out.String())
68 | if choice == "" {
69 | return errors.New("you did not choose any of the options")
70 | }
71 | name, err := switchContext(choice)
72 | if err != nil {
73 | return errors.Wrap(err, "failed to switch context")
74 | }
75 | printer.Success(stderr, "Switched to context \"%s\".", printer.SuccessColor.Sprint(name))
76 | return nil
77 | }
78 |
79 | func (op InteractiveDeleteOp) Run(_, stderr io.Writer) error {
80 | // parse kubeconfig just to see if it can be loaded
81 | kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
82 | if err := kc.Parse(); err != nil {
83 | if cmdutil.IsNotFoundErr(err) {
84 | printer.Warning(stderr, "kubeconfig file not found")
85 | return nil
86 | }
87 | return errors.Wrap(err, "kubeconfig error")
88 | }
89 | kc.Close()
90 |
91 | if len(kc.ContextNames()) == 0 {
92 | return errors.New("no contexts found in config")
93 | }
94 |
95 | cmd := exec.Command("fzf", "--ansi", "--no-preview")
96 | var out bytes.Buffer
97 | cmd.Stdin = os.Stdin
98 | cmd.Stderr = stderr
99 | cmd.Stdout = &out
100 |
101 | cmd.Env = append(os.Environ(),
102 | fmt.Sprintf("FZF_DEFAULT_COMMAND=%s", op.SelfCmd),
103 | fmt.Sprintf("%s=1", env.EnvForceColor))
104 | if err := cmd.Run(); err != nil {
105 | if _, ok := err.(*exec.ExitError); !ok {
106 | return err
107 | }
108 | }
109 |
110 | choice := strings.TrimSpace(out.String())
111 | if choice == "" {
112 | return errors.New("you did not choose any of the options")
113 | }
114 |
115 | name, wasActiveContext, err := deleteContext(choice)
116 | if err != nil {
117 | return errors.Wrap(err, "failed to delete context")
118 | }
119 |
120 | if wasActiveContext {
121 | printer.Warning(stderr, "You deleted the current context. Use \"%s\" to select a new context.",
122 | selfName())
123 | }
124 |
125 | printer.Success(stderr, `Deleted context %s.`, printer.SuccessColor.Sprint(name))
126 |
127 | return nil
128 | }
129 |
--------------------------------------------------------------------------------
/cmd/kubectx/help.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "fmt"
19 | "io"
20 | "os"
21 | "path/filepath"
22 | "strings"
23 |
24 | "github.com/pkg/errors"
25 | )
26 |
27 | // HelpOp describes printing help.
28 | type HelpOp struct{}
29 |
30 | func (_ HelpOp) Run(stdout, _ io.Writer) error {
31 | return printUsage(stdout)
32 | }
33 |
34 | func printUsage(out io.Writer) error {
35 | help := `USAGE:
36 | %PROG% : list the contexts
37 | %PROG% : switch to context
38 | %PROG% - : switch to the previous context
39 | %PROG% -c, --current : show the current context name
40 | %PROG% = : rename context to
41 | %PROG% =. : rename current-context to
42 | %PROG% -u, --unset : unset the current context
43 | %PROG% -d [] : delete context ('.' for current-context)
44 | %SPAC% (this command won't delete the user/cluster entry
45 | %SPAC% referenced by the context entry)
46 | %PROG% -h,--help : show this message
47 | %PROG% -V,--version : show version`
48 | help = strings.ReplaceAll(help, "%PROG%", selfName())
49 | help = strings.ReplaceAll(help, "%SPAC%", strings.Repeat(" ", len(selfName())))
50 |
51 | _, err := fmt.Fprintf(out, "%s\n", help)
52 | return errors.Wrap(err, "write error")
53 | }
54 |
55 | // selfName guesses how the user invoked the program.
56 | func selfName() string {
57 | me := filepath.Base(os.Args[0])
58 | pluginPrefix := "kubectl-"
59 | if strings.HasPrefix(me, pluginPrefix) {
60 | return "kubectl " + strings.TrimPrefix(me, pluginPrefix)
61 | }
62 | return "kubectx"
63 | }
64 |
--------------------------------------------------------------------------------
/cmd/kubectx/help_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "bytes"
19 | "strings"
20 | "testing"
21 | )
22 |
23 | func TestPrintHelp(t *testing.T) {
24 | var buf bytes.Buffer
25 | if err := (&HelpOp{}).Run(&buf, &buf); err != nil {
26 | t.Fatal(err)
27 | }
28 |
29 | out := buf.String()
30 | if !strings.Contains(out, "USAGE:") {
31 | t.Errorf("help string doesn't contain USAGE: ; output=\"%s\"", out)
32 | }
33 |
34 | if !strings.HasSuffix(out, "\n") {
35 | t.Errorf("does not end with New line; output=\"%s\"", out)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/cmd/kubectx/list.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "fmt"
19 | "io"
20 |
21 | "facette.io/natsort"
22 | "github.com/pkg/errors"
23 |
24 | "github.com/ahmetb/kubectx/internal/cmdutil"
25 | "github.com/ahmetb/kubectx/internal/kubeconfig"
26 | "github.com/ahmetb/kubectx/internal/printer"
27 | )
28 |
29 | // ListOp describes listing contexts.
30 | type ListOp struct{}
31 |
32 | func (_ ListOp) Run(stdout, stderr io.Writer) error {
33 | kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
34 | defer kc.Close()
35 | if err := kc.Parse(); err != nil {
36 | if cmdutil.IsNotFoundErr(err) {
37 | printer.Warning(stderr, "kubeconfig file not found")
38 | return nil
39 | }
40 | return errors.Wrap(err, "kubeconfig error")
41 | }
42 |
43 | ctxs := kc.ContextNames()
44 | natsort.Sort(ctxs)
45 |
46 | cur := kc.GetCurrentContext()
47 | for _, c := range ctxs {
48 | s := c
49 | if c == cur {
50 | s = printer.ActiveItemColor.Sprint(c)
51 | }
52 | fmt.Fprintf(stdout, "%s\n", s)
53 | }
54 | return nil
55 | }
56 |
--------------------------------------------------------------------------------
/cmd/kubectx/main.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "fmt"
19 | "io"
20 | "os"
21 |
22 | "github.com/ahmetb/kubectx/internal/cmdutil"
23 | "github.com/ahmetb/kubectx/internal/env"
24 | "github.com/ahmetb/kubectx/internal/printer"
25 | "github.com/fatih/color"
26 | )
27 |
28 | type Op interface {
29 | Run(stdout, stderr io.Writer) error
30 | }
31 |
32 | func main() {
33 | cmdutil.PrintDeprecatedEnvWarnings(color.Error, os.Environ())
34 |
35 | op := parseArgs(os.Args[1:])
36 | if err := op.Run(color.Output, color.Error); err != nil {
37 | printer.Error(color.Error, err.Error())
38 |
39 | if _, ok := os.LookupEnv(env.EnvDebug); ok {
40 | // print stack trace in verbose mode
41 | fmt.Fprintf(color.Error, "[DEBUG] error: %+v\n", err)
42 | }
43 | defer os.Exit(1)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/cmd/kubectx/rename.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "io"
19 | "strings"
20 |
21 | "github.com/pkg/errors"
22 |
23 | "github.com/ahmetb/kubectx/internal/kubeconfig"
24 | "github.com/ahmetb/kubectx/internal/printer"
25 | )
26 |
27 | // RenameOp indicates intention to rename contexts.
28 | type RenameOp struct {
29 | New string // NAME of New context
30 | Old string // NAME of Old context (or '.' for current-context)
31 | }
32 |
33 | // parseRenameSyntax parses A=B form into [A,B] and returns
34 | // whether it is parsed correctly.
35 | func parseRenameSyntax(v string) (string, string, bool) {
36 | s := strings.Split(v, "=")
37 | if len(s) != 2 {
38 | return "", "", false
39 | }
40 | new, old := s[0], s[1]
41 | if new == "" || old == "" {
42 | return "", "", false
43 | }
44 | return new, old, true
45 | }
46 |
47 | // rename changes the old (NAME or '.' for current-context)
48 | // to the "new" value. If the old refers to the current-context,
49 | // current-context preference is also updated.
50 | func (op RenameOp) Run(_, stderr io.Writer) error {
51 | kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
52 | defer kc.Close()
53 | if err := kc.Parse(); err != nil {
54 | return errors.Wrap(err, "kubeconfig error")
55 | }
56 |
57 | cur := kc.GetCurrentContext()
58 | if op.Old == "." {
59 | op.Old = cur
60 | }
61 |
62 | if !kc.ContextExists(op.Old) {
63 | return errors.Errorf("context \"%s\" not found, can't rename it", op.Old)
64 | }
65 |
66 | if kc.ContextExists(op.New) {
67 | printer.Warning(stderr, "context \"%s\" exists, overwriting it.", op.New)
68 | if err := kc.DeleteContextEntry(op.New); err != nil {
69 | return errors.Wrap(err, "failed to delete new context to overwrite it")
70 | }
71 | }
72 |
73 | if err := kc.ModifyContextName(op.Old, op.New); err != nil {
74 | return errors.Wrap(err, "failed to change context name")
75 | }
76 | if op.Old == cur {
77 | if err := kc.ModifyCurrentContext(op.New); err != nil {
78 | return errors.Wrap(err, "failed to set current-context to new name")
79 | }
80 | }
81 | if err := kc.Save(); err != nil {
82 | return errors.Wrap(err, "failed to save modified kubeconfig")
83 | }
84 | printer.Success(stderr, "Context %s renamed to %s.",
85 | printer.SuccessColor.Sprint(op.Old),
86 | printer.SuccessColor.Sprint(op.New))
87 | return nil
88 | }
89 |
--------------------------------------------------------------------------------
/cmd/kubectx/rename_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "testing"
19 |
20 | "github.com/google/go-cmp/cmp"
21 | )
22 |
23 | func Test_parseRenameSyntax(t *testing.T) {
24 |
25 | type out struct {
26 | New string
27 | Old string
28 | OK bool
29 | }
30 | tests := []struct {
31 | name string
32 | in string
33 | want out
34 | }{
35 | {
36 | name: "no equals sign",
37 | in: "foo",
38 | want: out{OK: false},
39 | },
40 | {
41 | name: "no left side",
42 | in: "=a",
43 | want: out{OK: false},
44 | },
45 | {
46 | name: "no right side",
47 | in: "a=",
48 | want: out{OK: false},
49 | },
50 | {
51 | name: "correct format",
52 | in: "a=b",
53 | want: out{
54 | New: "a",
55 | Old: "b",
56 | OK: true,
57 | },
58 | },
59 | {
60 | name: "correct format with current context",
61 | in: "NEW_NAME=.",
62 | want: out{
63 | New: "NEW_NAME",
64 | Old: ".",
65 | OK: true,
66 | },
67 | },
68 | }
69 | for _, tt := range tests {
70 | t.Run(tt.name, func(t *testing.T) {
71 | new, old, ok := parseRenameSyntax(tt.in)
72 | got := out{
73 | New: new,
74 | Old: old,
75 | OK: ok,
76 | }
77 | diff := cmp.Diff(tt.want, got)
78 | if diff != "" {
79 | t.Errorf("parseRenameSyntax() diff=%s", diff)
80 | }
81 | })
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/cmd/kubectx/state.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "io/ioutil"
19 | "os"
20 | "path/filepath"
21 |
22 | "github.com/pkg/errors"
23 |
24 | "github.com/ahmetb/kubectx/internal/cmdutil"
25 | )
26 |
27 | func kubectxPrevCtxFile() (string, error) {
28 | home := cmdutil.HomeDir()
29 | if home == "" {
30 | return "", errors.New("HOME or USERPROFILE environment variable not set")
31 | }
32 | return filepath.Join(home, ".kube", "kubectx"), nil
33 | }
34 |
35 | // readLastContext returns the saved previous context
36 | // if the state file exists, otherwise returns "".
37 | func readLastContext(path string) (string, error) {
38 | b, err := ioutil.ReadFile(path)
39 | if os.IsNotExist(err) {
40 | return "", nil
41 | }
42 | return string(b), err
43 | }
44 |
45 | // writeLastContext saves the specified value to the state file.
46 | // It creates missing parent directories.
47 | func writeLastContext(path, value string) error {
48 | dir := filepath.Dir(path)
49 | if err := os.MkdirAll(dir, 0755); err != nil {
50 | return errors.Wrap(err, "failed to create parent directories")
51 | }
52 | return ioutil.WriteFile(path, []byte(value), 0644)
53 | }
54 |
--------------------------------------------------------------------------------
/cmd/kubectx/state_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "io/ioutil"
19 | "os"
20 | "path/filepath"
21 | "testing"
22 |
23 | "github.com/ahmetb/kubectx/internal/testutil"
24 | )
25 |
26 | func Test_readLastContext_nonExistingFile(t *testing.T) {
27 | s, err := readLastContext(filepath.FromSlash("/non/existing/file"))
28 | if err != nil {
29 | t.Fatal(err)
30 | }
31 | if s != "" {
32 | t.Fatalf("expected empty string; got=\"%s\"", s)
33 | }
34 | }
35 |
36 | func Test_readLastContext(t *testing.T) {
37 | path, cleanup := testutil.TempFile(t, "foo")
38 | defer cleanup()
39 |
40 | s, err := readLastContext(path)
41 | if err != nil {
42 | t.Fatal(err)
43 | }
44 | if expected := "foo"; s != expected {
45 | t.Fatalf("expected=\"%s\"; got=\"%s\"", expected, s)
46 | }
47 | }
48 |
49 | func Test_writeLastContext_err(t *testing.T) {
50 | path := filepath.Join(os.DevNull, "foo", "bar")
51 | err := writeLastContext(path, "foo")
52 | if err == nil {
53 | t.Fatal("got empty error")
54 | }
55 | }
56 |
57 | func Test_writeLastContext(t *testing.T) {
58 | dir, err := ioutil.TempDir(os.TempDir(), "state-file-test")
59 | if err != nil {
60 | t.Fatal(err)
61 | }
62 | path := filepath.Join(dir, "foo", "bar")
63 |
64 | if err := writeLastContext(path, "ctx1"); err != nil {
65 | t.Fatal(err)
66 | }
67 |
68 | v, err := readLastContext(path)
69 | if err != nil {
70 | t.Fatal(err)
71 | }
72 | if expected := "ctx1"; v != expected {
73 | t.Fatalf("read wrong value=\"%s\"; expected=\"%s\"", v, expected)
74 | }
75 | }
76 |
77 | func Test_kubectxFilePath(t *testing.T) {
78 | origHome := os.Getenv("HOME")
79 | os.Setenv("HOME", filepath.FromSlash("/foo/bar"))
80 | defer os.Setenv("HOME", origHome)
81 |
82 | expected := filepath.Join(filepath.FromSlash("/foo/bar"), ".kube", "kubectx")
83 | v, err := kubectxPrevCtxFile()
84 | if err != nil {
85 | t.Fatal(err)
86 | }
87 | if v != expected {
88 | t.Fatalf("expected=\"%s\" got=\"%s\"", expected, v)
89 | }
90 | }
91 |
92 | func Test_kubectxFilePath_error(t *testing.T) {
93 | origHome := os.Getenv("HOME")
94 | origUserprofile := os.Getenv("USERPROFILE")
95 | os.Unsetenv("HOME")
96 | os.Unsetenv("USERPROFILE")
97 | defer os.Setenv("HOME", origHome)
98 | defer os.Setenv("USERPROFILE", origUserprofile)
99 |
100 | _, err := kubectxPrevCtxFile()
101 | if err == nil {
102 | t.Fatal(err)
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/cmd/kubectx/switch.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "io"
19 |
20 | "github.com/pkg/errors"
21 |
22 | "github.com/ahmetb/kubectx/internal/kubeconfig"
23 | "github.com/ahmetb/kubectx/internal/printer"
24 | )
25 |
26 | // SwitchOp indicates intention to switch contexts.
27 | type SwitchOp struct {
28 | Target string // '-' for back and forth, or NAME
29 | }
30 |
31 | func (op SwitchOp) Run(_, stderr io.Writer) error {
32 | var newCtx string
33 | var err error
34 | if op.Target == "-" {
35 | newCtx, err = swapContext()
36 | } else {
37 | newCtx, err = switchContext(op.Target)
38 | }
39 | if err != nil {
40 | return errors.Wrap(err, "failed to switch context")
41 | }
42 | err = printer.Success(stderr, "Switched to context \"%s\".", printer.SuccessColor.Sprint(newCtx))
43 | return errors.Wrap(err, "print error")
44 | }
45 |
46 | // switchContext switches to specified context name.
47 | func switchContext(name string) (string, error) {
48 | prevCtxFile, err := kubectxPrevCtxFile()
49 | if err != nil {
50 | return "", errors.Wrap(err, "failed to determine state file")
51 | }
52 |
53 | kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
54 | defer kc.Close()
55 | if err := kc.Parse(); err != nil {
56 | return "", errors.Wrap(err, "kubeconfig error")
57 | }
58 |
59 | prev := kc.GetCurrentContext()
60 | if !kc.ContextExists(name) {
61 | return "", errors.Errorf("no context exists with the name: \"%s\"", name)
62 | }
63 | if err := kc.ModifyCurrentContext(name); err != nil {
64 | return "", err
65 | }
66 | if err := kc.Save(); err != nil {
67 | return "", errors.Wrap(err, "failed to save kubeconfig")
68 | }
69 |
70 | if prev != name {
71 | if err := writeLastContext(prevCtxFile, prev); err != nil {
72 | return "", errors.Wrap(err, "failed to save previous context name")
73 | }
74 | }
75 | return name, nil
76 | }
77 |
78 | // swapContext switches to previously switch context.
79 | func swapContext() (string, error) {
80 | prevCtxFile, err := kubectxPrevCtxFile()
81 | if err != nil {
82 | return "", errors.Wrap(err, "failed to determine state file")
83 | }
84 | prev, err := readLastContext(prevCtxFile)
85 | if err != nil {
86 | return "", errors.Wrap(err, "failed to read previous context file")
87 | }
88 | if prev == "" {
89 | return "", errors.New("no previous context found")
90 | }
91 | return switchContext(prev)
92 | }
93 |
--------------------------------------------------------------------------------
/cmd/kubectx/unset.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "io"
19 |
20 | "github.com/pkg/errors"
21 |
22 | "github.com/ahmetb/kubectx/internal/kubeconfig"
23 | "github.com/ahmetb/kubectx/internal/printer"
24 | )
25 |
26 | // UnsetOp indicates intention to remove current-context preference.
27 | type UnsetOp struct{}
28 |
29 | func (_ UnsetOp) Run(_, stderr io.Writer) error {
30 | kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
31 | defer kc.Close()
32 | if err := kc.Parse(); err != nil {
33 | return errors.Wrap(err, "kubeconfig error")
34 | }
35 |
36 | if err := kc.UnsetCurrentContext(); err != nil {
37 | return errors.Wrap(err, "error while modifying current-context")
38 | }
39 | if err := kc.Save(); err != nil {
40 | return errors.Wrap(err, "failed to save kubeconfig file after modification")
41 | }
42 |
43 | err := printer.Success(stderr, "Active context unset for kubectl.")
44 | return errors.Wrap(err, "write error")
45 | }
46 |
--------------------------------------------------------------------------------
/cmd/kubectx/version.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "io"
6 |
7 | "github.com/pkg/errors"
8 | )
9 |
10 | var (
11 | version = "v0.0.0+unknown" // populated by goreleaser
12 | )
13 |
14 | // VersionOp describes printing version string.
15 | type VersionOp struct{}
16 |
17 | func (_ VersionOp) Run(stdout, _ io.Writer) error {
18 | _, err := fmt.Fprintf(stdout, "%s\n", version)
19 | return errors.Wrap(err, "write error")
20 | }
21 |
--------------------------------------------------------------------------------
/cmd/kubens/current.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "fmt"
19 | "io"
20 |
21 | "github.com/pkg/errors"
22 |
23 | "github.com/ahmetb/kubectx/internal/kubeconfig"
24 | )
25 |
26 | type CurrentOp struct{}
27 |
28 | func (c CurrentOp) Run(stdout, _ io.Writer) error {
29 | kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
30 | defer kc.Close()
31 | if err := kc.Parse(); err != nil {
32 | return errors.Wrap(err, "kubeconfig error")
33 | }
34 |
35 | ctx := kc.GetCurrentContext()
36 | if ctx == "" {
37 | return errors.New("current-context is not set")
38 | }
39 | ns, err := kc.NamespaceOfContext(ctx)
40 | if err != nil {
41 | return errors.Wrapf(err, "failed to read namespace of \"%s\"", ctx)
42 | }
43 | _, err = fmt.Fprintln(stdout, ns)
44 | return errors.Wrap(err, "write error")
45 | }
46 |
--------------------------------------------------------------------------------
/cmd/kubens/flags.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "fmt"
19 | "io"
20 | "os"
21 | "slices"
22 | "strings"
23 |
24 | "github.com/ahmetb/kubectx/internal/cmdutil"
25 | )
26 |
27 | // UnsupportedOp indicates an unsupported flag.
28 | type UnsupportedOp struct{ Err error }
29 |
30 | func (op UnsupportedOp) Run(_, _ io.Writer) error {
31 | return op.Err
32 | }
33 |
34 | // parseArgs looks at flags (excl. executable name, i.e. argv[0])
35 | // and decides which operation should be taken.
36 | func parseArgs(argv []string) Op {
37 | n := len(argv)
38 |
39 | if n == 0 {
40 | if cmdutil.IsInteractiveMode(os.Stdout) {
41 | return InteractiveSwitchOp{SelfCmd: os.Args[0]}
42 | }
43 | return ListOp{}
44 | }
45 |
46 | if n == 1 {
47 | v := argv[0]
48 | switch v {
49 | case "--help", "-h":
50 | return HelpOp{}
51 | case "--version", "-V":
52 | return VersionOp{}
53 | case "--current", "-c":
54 | return CurrentOp{}
55 | default:
56 | return getSwitchOp(v, false)
57 | }
58 | } else if n == 2 {
59 | // {namespace} -f|--force
60 | name := argv[0]
61 | force := slices.Contains([]string{"-f", "--force"}, argv[1])
62 |
63 | if !force {
64 | if !slices.Contains([]string{"-f", "--force"}, argv[0]) {
65 | return UnsupportedOp{Err: fmt.Errorf("unsupported arguments %q", argv)}
66 | }
67 |
68 | // -f|--force {namespace}
69 | force = true
70 | name = argv[1]
71 | }
72 |
73 | return getSwitchOp(name, force)
74 | }
75 |
76 | return UnsupportedOp{Err: fmt.Errorf("too many arguments")}
77 | }
78 |
79 | func getSwitchOp(v string, force bool) Op {
80 | if strings.HasPrefix(v, "-") && v != "-" {
81 | return UnsupportedOp{Err: fmt.Errorf("unsupported option %q", v)}
82 | }
83 | return SwitchOp{Target: v, Force: force}
84 | }
85 |
--------------------------------------------------------------------------------
/cmd/kubens/flags_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "fmt"
19 | "testing"
20 |
21 | "github.com/google/go-cmp/cmp"
22 | )
23 |
24 | func Test_parseArgs_new(t *testing.T) {
25 | tests := []struct {
26 | name string
27 | args []string
28 | want Op
29 | }{
30 | {name: "nil Args",
31 | args: nil,
32 | want: ListOp{}},
33 | {name: "empty Args",
34 | args: []string{},
35 | want: ListOp{}},
36 | {name: "help shorthand",
37 | args: []string{"-h"},
38 | want: HelpOp{}},
39 | {name: "help long form",
40 | args: []string{"--help"},
41 | want: HelpOp{}},
42 | {name: "current shorthand",
43 | args: []string{"-c"},
44 | want: CurrentOp{}},
45 | {name: "current long form",
46 | args: []string{"--current"},
47 | want: CurrentOp{}},
48 | {name: "switch by name",
49 | args: []string{"foo"},
50 | want: SwitchOp{Target: "foo"}},
51 | {name: "switch by name force short flag",
52 | args: []string{"foo", "-f"},
53 | want: SwitchOp{Target: "foo", Force: true}},
54 | {name: "switch by name force long flag",
55 | args: []string{"foo", "--force"},
56 | want: SwitchOp{Target: "foo", Force: true}},
57 | {name: "switch by name force short flag before name",
58 | args: []string{"-f", "foo"},
59 | want: SwitchOp{Target: "foo", Force: true}},
60 | {name: "switch by name force long flag before name",
61 | args: []string{"--force", "foo"},
62 | want: SwitchOp{Target: "foo", Force: true}},
63 | {name: "switch by name unknown arguments",
64 | args: []string{"foo", "-x"},
65 | want: UnsupportedOp{Err: fmt.Errorf("unsupported arguments %q", []string{"foo", "-x"})}},
66 | {name: "switch by name unknown arguments",
67 | args: []string{"-x", "foo"},
68 | want: UnsupportedOp{Err: fmt.Errorf("unsupported arguments %q", []string{"-x", "foo"})}},
69 | {name: "switch by swap",
70 | args: []string{"-"},
71 | want: SwitchOp{Target: "-"}},
72 | {name: "unrecognized flag",
73 | args: []string{"-x"},
74 | want: UnsupportedOp{Err: fmt.Errorf("unsupported option %q", "-x")}},
75 | {name: "too many args",
76 | args: []string{"a", "b", "c"},
77 | want: UnsupportedOp{Err: fmt.Errorf("too many arguments")}},
78 | }
79 | for _, tt := range tests {
80 | t.Run(tt.name, func(t *testing.T) {
81 | got := parseArgs(tt.args)
82 |
83 | var opts cmp.Options
84 | if _, ok := tt.want.(UnsupportedOp); ok {
85 | opts = append(opts, cmp.Comparer(func(x, y UnsupportedOp) bool {
86 | return (x.Err == nil && y.Err == nil) || (x.Err.Error() == y.Err.Error())
87 | }))
88 | }
89 |
90 | if diff := cmp.Diff(got, tt.want, opts...); diff != "" {
91 | t.Errorf("parseArgs(%#v) diff: %s", tt.args, diff)
92 | }
93 | })
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/cmd/kubens/fzf.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "bytes"
19 | "fmt"
20 | "io"
21 | "os"
22 | "os/exec"
23 | "strings"
24 |
25 | "github.com/pkg/errors"
26 |
27 | "github.com/ahmetb/kubectx/internal/cmdutil"
28 | "github.com/ahmetb/kubectx/internal/env"
29 | "github.com/ahmetb/kubectx/internal/kubeconfig"
30 | "github.com/ahmetb/kubectx/internal/printer"
31 | )
32 |
33 | type InteractiveSwitchOp struct {
34 | SelfCmd string
35 | }
36 |
37 | // TODO(ahmetb) This method is heavily repetitive vs kubectx/fzf.go.
38 | func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error {
39 | // parse kubeconfig just to see if it can be loaded
40 | kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
41 | if err := kc.Parse(); err != nil {
42 | if cmdutil.IsNotFoundErr(err) {
43 | printer.Warning(stderr, "kubeconfig file not found")
44 | return nil
45 | }
46 | return errors.Wrap(err, "kubeconfig error")
47 | }
48 | defer kc.Close()
49 |
50 | cmd := exec.Command("fzf", "--ansi", "--no-preview")
51 | var out bytes.Buffer
52 | cmd.Stdin = os.Stdin
53 | cmd.Stderr = stderr
54 | cmd.Stdout = &out
55 |
56 | cmd.Env = append(os.Environ(),
57 | fmt.Sprintf("FZF_DEFAULT_COMMAND=%s", op.SelfCmd),
58 | fmt.Sprintf("%s=1", env.EnvForceColor))
59 | if err := cmd.Run(); err != nil {
60 | if _, ok := err.(*exec.ExitError); !ok {
61 | return err
62 | }
63 | }
64 | choice := strings.TrimSpace(out.String())
65 | if choice == "" {
66 | return errors.New("you did not choose any of the options")
67 | }
68 | name, err := switchNamespace(kc, choice, false)
69 | if err != nil {
70 | return errors.Wrap(err, "failed to switch namespace")
71 | }
72 | printer.Success(stderr, "Active namespace is \"%s\".", printer.SuccessColor.Sprint(name))
73 | return nil
74 | }
75 |
--------------------------------------------------------------------------------
/cmd/kubens/help.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "fmt"
19 | "io"
20 | "os"
21 | "path/filepath"
22 | "strings"
23 |
24 | "github.com/pkg/errors"
25 | )
26 |
27 | // HelpOp describes printing help.
28 | type HelpOp struct{}
29 |
30 | func (_ HelpOp) Run(stdout, _ io.Writer) error {
31 | return printUsage(stdout)
32 | }
33 |
34 | func printUsage(out io.Writer) error {
35 | help := `USAGE:
36 | %PROG% : list the namespaces in the current context
37 | %PROG% : change the active namespace of current context
38 | %PROG% --force/-f : force change the active namespace of current context (even if it doesn't exist)
39 | %PROG% - : switch to the previous namespace in this context
40 | %PROG% -c, --current : show the current namespace
41 | %PROG% -h,--help : show this message
42 | %PROG% -V,--version : show version`
43 |
44 | // TODO this replace logic is duplicated between this and kubectx
45 | help = strings.ReplaceAll(help, "%PROG%", selfName())
46 |
47 | _, err := fmt.Fprintf(out, "%s\n", help)
48 | return errors.Wrap(err, "write error")
49 | }
50 |
51 | // selfName guesses how the user invoked the program.
52 | func selfName() string {
53 | // TODO this method is duplicated between this and kubectx
54 | me := filepath.Base(os.Args[0])
55 | pluginPrefix := "kubectl-"
56 | if strings.HasPrefix(me, pluginPrefix) {
57 | return "kubectl " + strings.TrimPrefix(me, pluginPrefix)
58 | }
59 | return "kubens"
60 | }
61 |
--------------------------------------------------------------------------------
/cmd/kubens/list.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "context"
19 | "fmt"
20 | "io"
21 | "os"
22 |
23 | "github.com/pkg/errors"
24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25 | "k8s.io/client-go/kubernetes"
26 | _ "k8s.io/client-go/plugin/pkg/client/auth"
27 | "k8s.io/client-go/tools/clientcmd"
28 |
29 | "github.com/ahmetb/kubectx/internal/kubeconfig"
30 | "github.com/ahmetb/kubectx/internal/printer"
31 | )
32 |
33 | type ListOp struct{}
34 |
35 | func (op ListOp) Run(stdout, stderr io.Writer) error {
36 | kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
37 | defer kc.Close()
38 | if err := kc.Parse(); err != nil {
39 | return errors.Wrap(err, "kubeconfig error")
40 | }
41 |
42 | ctx := kc.GetCurrentContext()
43 | if ctx == "" {
44 | return errors.New("current-context is not set")
45 | }
46 | curNs, err := kc.NamespaceOfContext(ctx)
47 | if err != nil {
48 | return errors.Wrap(err, "cannot read current namespace")
49 | }
50 |
51 | ns, err := queryNamespaces(kc)
52 | if err != nil {
53 | return errors.Wrap(err, "could not list namespaces (is the cluster accessible?)")
54 | }
55 |
56 | for _, c := range ns {
57 | s := c
58 | if c == curNs {
59 | s = printer.ActiveItemColor.Sprint(c)
60 | }
61 | fmt.Fprintf(stdout, "%s\n", s)
62 | }
63 | return nil
64 | }
65 |
66 | func queryNamespaces(kc *kubeconfig.Kubeconfig) ([]string, error) {
67 | if os.Getenv("_MOCK_NAMESPACES") != "" {
68 | return []string{"ns1", "ns2"}, nil
69 | }
70 |
71 | clientset, err := newKubernetesClientSet(kc)
72 | if err != nil {
73 | return nil, errors.Wrap(err, "failed to initialize k8s REST client")
74 | }
75 |
76 | var out []string
77 | var next string
78 | for {
79 | list, err := clientset.CoreV1().Namespaces().List(
80 | context.Background(),
81 | metav1.ListOptions{
82 | Limit: 500,
83 | Continue: next,
84 | })
85 | if err != nil {
86 | return nil, errors.Wrap(err, "failed to list namespaces from k8s API")
87 | }
88 | next = list.Continue
89 | for _, it := range list.Items {
90 | out = append(out, it.Name)
91 | }
92 | if next == "" {
93 | break
94 | }
95 | }
96 | return out, nil
97 | }
98 |
99 | func newKubernetesClientSet(kc *kubeconfig.Kubeconfig) (*kubernetes.Clientset, error) {
100 | b, err := kc.Bytes()
101 | if err != nil {
102 | return nil, errors.Wrap(err, "failed to convert in-memory kubeconfig to yaml")
103 | }
104 | cfg, err := clientcmd.RESTConfigFromKubeConfig(b)
105 | if err != nil {
106 | return nil, errors.Wrap(err, "failed to initialize config")
107 | }
108 | return kubernetes.NewForConfig(cfg)
109 | }
110 |
--------------------------------------------------------------------------------
/cmd/kubens/main.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "fmt"
19 | "io"
20 | "os"
21 |
22 | "github.com/ahmetb/kubectx/internal/cmdutil"
23 | "github.com/ahmetb/kubectx/internal/env"
24 | "github.com/ahmetb/kubectx/internal/printer"
25 | "github.com/fatih/color"
26 | )
27 |
28 | type Op interface {
29 | Run(stdout, stderr io.Writer) error
30 | }
31 |
32 | func main() {
33 | cmdutil.PrintDeprecatedEnvWarnings(color.Error, os.Environ())
34 | op := parseArgs(os.Args[1:])
35 | if err := op.Run(color.Output, color.Error); err != nil {
36 | printer.Error(color.Error, err.Error())
37 |
38 | if _, ok := os.LookupEnv(env.EnvDebug); ok {
39 | // print stack trace in verbose mode
40 | fmt.Fprintf(color.Error, "[DEBUG] error: %+v\n", err)
41 | }
42 | defer os.Exit(1)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/cmd/kubens/statefile.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "bytes"
19 | "io/ioutil"
20 | "os"
21 | "path/filepath"
22 | "runtime"
23 | "strings"
24 |
25 | "github.com/ahmetb/kubectx/internal/cmdutil"
26 | )
27 |
28 | var defaultDir = filepath.Join(cmdutil.HomeDir(), ".kube", "kubens")
29 |
30 | type NSFile struct {
31 | dir string
32 | ctx string
33 | }
34 |
35 | func NewNSFile(ctx string) NSFile { return NSFile{dir: defaultDir, ctx: ctx} }
36 |
37 | func (f NSFile) path() string {
38 | fn := f.ctx
39 | if isWindows() {
40 | // bug 230: eks clusters contain ':' in ctx name, not a valid file name for win32
41 | fn = strings.ReplaceAll(fn, ":", "__")
42 | }
43 | return filepath.Join(f.dir, fn)
44 | }
45 |
46 | // Load reads the previous namespace setting, or returns empty if not exists.
47 | func (f NSFile) Load() (string, error) {
48 | b, err := ioutil.ReadFile(f.path())
49 | if err != nil {
50 | if os.IsNotExist(err) {
51 | return "", nil
52 | }
53 | return "", err
54 | }
55 | return string(bytes.TrimSpace(b)), nil
56 | }
57 |
58 | // Save stores the previous namespace information in the file.
59 | func (f NSFile) Save(value string) error {
60 | d := filepath.Dir(f.path())
61 | if err := os.MkdirAll(d, 0755); err != nil {
62 | return err
63 | }
64 | return ioutil.WriteFile(f.path(), []byte(value), 0644)
65 | }
66 |
67 | // isWindows determines if the process is running on windows OS.
68 | func isWindows() bool {
69 | if os.Getenv("_FORCE_GOOS") == "windows" { // for testing
70 | return true
71 | }
72 | return runtime.GOOS == "windows"
73 | }
74 |
--------------------------------------------------------------------------------
/cmd/kubens/statefile_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "io/ioutil"
19 | "os"
20 | "runtime"
21 | "strings"
22 | "testing"
23 |
24 | "github.com/ahmetb/kubectx/internal/testutil"
25 | )
26 |
27 | func TestNSFile(t *testing.T) {
28 | td, err := ioutil.TempDir(os.TempDir(), "")
29 | if err != nil {
30 | t.Fatal(err)
31 | }
32 | defer os.RemoveAll(td)
33 |
34 | f := NewNSFile("foo")
35 | f.dir = td
36 | v, err := f.Load()
37 | if err != nil {
38 | t.Fatal(err)
39 | }
40 | if v != "" {
41 | t.Fatalf("Load() expected empty; got=%v", err)
42 | }
43 |
44 | err = f.Save("bar")
45 | if err != nil {
46 | t.Fatalf("Save() err=%v", err)
47 | }
48 |
49 | v, err = f.Load()
50 | if err != nil {
51 | t.Fatal(err)
52 | }
53 | if expected := "bar"; v != expected {
54 | t.Fatalf("Load()=\"%s\"; expected=\"%s\"", v, expected)
55 | }
56 | }
57 |
58 | func TestNSFile_path_windows(t *testing.T) {
59 | defer testutil.WithEnvVar("_FORCE_GOOS", "windows")()
60 | fp := NewNSFile("a:b:c").path()
61 |
62 | if expected := "a__b__c"; !strings.HasSuffix(fp, expected) {
63 | t.Fatalf("file did not have expected ending %q: %s", expected, fp)
64 | }
65 | }
66 |
67 | func Test_isWindows(t *testing.T) {
68 | if runtime.GOOS == "windows" {
69 | t.Skip("won't test this case on windows")
70 | }
71 |
72 | got := isWindows()
73 | if got {
74 | t.Fatalf("isWindows() returned true for %s", runtime.GOOS)
75 | }
76 |
77 | defer testutil.WithEnvVar("_FORCE_GOOS", "windows")()
78 | if !isWindows() {
79 | t.Fatalf("isWindows() failed to detect windows with env override.")
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/cmd/kubens/switch.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "context"
19 | "io"
20 | "os"
21 |
22 | "github.com/pkg/errors"
23 | errors2 "k8s.io/apimachinery/pkg/api/errors"
24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25 |
26 | "github.com/ahmetb/kubectx/internal/kubeconfig"
27 | "github.com/ahmetb/kubectx/internal/printer"
28 | )
29 |
30 | type SwitchOp struct {
31 | Target string // '-' for back and forth, or NAME
32 | Force bool // force switch even if the namespace doesn't exist
33 | }
34 |
35 | func (s SwitchOp) Run(_, stderr io.Writer) error {
36 | kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
37 | defer kc.Close()
38 | if err := kc.Parse(); err != nil {
39 | return errors.Wrap(err, "kubeconfig error")
40 | }
41 |
42 | toNS, err := switchNamespace(kc, s.Target, s.Force)
43 | if err != nil {
44 | return err
45 | }
46 | err = printer.Success(stderr, "Active namespace is \"%s\"", printer.SuccessColor.Sprint(toNS))
47 | return err
48 | }
49 |
50 | func switchNamespace(kc *kubeconfig.Kubeconfig, ns string, force bool) (string, error) {
51 | ctx := kc.GetCurrentContext()
52 | if ctx == "" {
53 | return "", errors.New("current-context is not set")
54 | }
55 | curNS, err := kc.NamespaceOfContext(ctx)
56 | if err != nil {
57 | return "", errors.Wrap(err, "failed to get current namespace")
58 | }
59 |
60 | f := NewNSFile(ctx)
61 | prev, err := f.Load()
62 | if err != nil {
63 | return "", errors.Wrap(err, "failed to load previous namespace from file")
64 | }
65 |
66 | if ns == "-" {
67 | if prev == "" {
68 | return "", errors.Errorf("No previous namespace found for current context (%s)", ctx)
69 | }
70 | ns = prev
71 | }
72 |
73 | if !force {
74 | ok, err := namespaceExists(kc, ns)
75 | if err != nil {
76 | return "", errors.Wrap(err, "failed to query if namespace exists (is cluster accessible?)")
77 | }
78 | if !ok {
79 | return "", errors.Errorf("no namespace exists with name \"%s\"", ns)
80 | }
81 | }
82 |
83 | if err := kc.SetNamespace(ctx, ns); err != nil {
84 | return "", errors.Wrapf(err, "failed to change to namespace \"%s\"", ns)
85 | }
86 | if err := kc.Save(); err != nil {
87 | return "", errors.Wrap(err, "failed to save kubeconfig file")
88 | }
89 | if curNS != ns {
90 | if err := f.Save(curNS); err != nil {
91 | return "", errors.Wrap(err, "failed to save the previous namespace to file")
92 | }
93 | }
94 | return ns, nil
95 | }
96 |
97 | func namespaceExists(kc *kubeconfig.Kubeconfig, ns string) (bool, error) {
98 | // for tests
99 | if os.Getenv("_MOCK_NAMESPACES") != "" {
100 | return ns == "ns1" || ns == "ns2", nil
101 | }
102 |
103 | clientset, err := newKubernetesClientSet(kc)
104 | if err != nil {
105 | return false, errors.Wrap(err, "failed to initialize k8s REST client")
106 | }
107 |
108 | namespace, err := clientset.CoreV1().Namespaces().Get(context.Background(), ns, metav1.GetOptions{})
109 | if errors2.IsNotFound(err) {
110 | return false, nil
111 | }
112 | return namespace != nil, errors.Wrapf(err, "failed to query "+
113 | "namespace %q from k8s API", ns)
114 | }
115 |
--------------------------------------------------------------------------------
/cmd/kubens/version.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "io"
6 |
7 | "github.com/pkg/errors"
8 | )
9 |
10 | var (
11 | version = "v0.0.0+unknown" // populated by goreleaser
12 | )
13 |
14 | // VersionOp describes printing version string.
15 | type VersionOp struct{}
16 |
17 | func (_ VersionOp) Run(stdout, _ io.Writer) error {
18 | _, err := fmt.Fprintf(stdout, "%s\n", version)
19 | return errors.Wrap(err, "write error")
20 | }
21 |
--------------------------------------------------------------------------------
/completion/_kubectx.zsh:
--------------------------------------------------------------------------------
1 | #compdef kubectx kctx=kubectx
2 |
3 | local KUBECTX="${HOME}/.kube/kubectx"
4 | PREV=""
5 |
6 | local context_array=("${(@f)$(kubectl config get-contexts --output='name')}")
7 | local all_contexts=(\'${^context_array}\')
8 |
9 | if [ -f "$KUBECTX" ]; then
10 | # show '-' only if there's a saved previous context
11 | local PREV=$(cat "${KUBECTX}")
12 |
13 | _arguments \
14 | "-d:*: :(${all_contexts})" \
15 | "(- *): :(- ${all_contexts})"
16 | else
17 | _arguments \
18 | "-d:*: :(${all_contexts})" \
19 | "(- *): :(${all_contexts})"
20 | fi
21 |
--------------------------------------------------------------------------------
/completion/_kubens.zsh:
--------------------------------------------------------------------------------
1 | #compdef kubens kns=kubens
2 | _arguments "1: :(- $(kubectl get namespaces -o=jsonpath='{range .items[*].metadata.name}{@}{"\n"}{end}'))"
3 |
--------------------------------------------------------------------------------
/completion/kubectx.bash:
--------------------------------------------------------------------------------
1 | _kube_contexts()
2 | {
3 | local curr_arg;
4 | curr_arg=${COMP_WORDS[COMP_CWORD]}
5 | COMPREPLY=( $(compgen -W "- $(kubectl config get-contexts --output='name')" -- $curr_arg ) );
6 | }
7 |
8 | complete -F _kube_contexts kubectx kctx
9 |
--------------------------------------------------------------------------------
/completion/kubectx.fish:
--------------------------------------------------------------------------------
1 | # kubectx
2 |
3 | function __fish_kubectx_arg_number -a number
4 | set -l cmd (commandline -opc)
5 | test (count $cmd) -eq $number
6 | end
7 |
8 | complete -f -c kubectx
9 | complete -f -x -c kubectx -n '__fish_kubectx_arg_number 1' -a "(kubectl config get-contexts --output='name')"
10 | complete -f -x -c kubectx -n '__fish_kubectx_arg_number 1' -a "-" -d "switch to the previous namespace in this context"
11 |
--------------------------------------------------------------------------------
/completion/kubens.bash:
--------------------------------------------------------------------------------
1 | _kube_namespaces()
2 | {
3 | local curr_arg;
4 | curr_arg=${COMP_WORDS[COMP_CWORD]}
5 | COMPREPLY=( $(compgen -W "- $(kubectl get namespaces -o=jsonpath='{range .items[*].metadata.name}{@}{"\n"}{end}')" -- $curr_arg ) );
6 | }
7 |
8 | complete -F _kube_namespaces kubens kns
9 |
--------------------------------------------------------------------------------
/completion/kubens.fish:
--------------------------------------------------------------------------------
1 | # kubens
2 |
3 | function __fish_kubens_arg_number -a number
4 | set -l cmd (commandline -opc)
5 | test (count $cmd) -eq $number
6 | end
7 |
8 | complete -f -c kubens
9 | complete -f -x -c kubens -n '__fish_kubens_arg_number 1' -a "(kubectl get ns -o=custom-columns=NAME:.metadata.name --no-headers)"
10 | complete -f -x -c kubens -n '__fish_kubens_arg_number 1' -a "-" -d "switch to the previous namespace in this context"
11 | complete -f -x -c kubens -n '__fish_kubens_arg_number 1' -s c -l current -d "show the current namespace"
12 | complete -f -x -c kubens -n '__fish_kubens_arg_number 1' -s h -l help -d "show the help message"
13 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/ahmetb/kubectx
2 |
3 | go 1.22
4 |
5 | require (
6 | facette.io/natsort v0.0.0-20181210072756-2cd4dd1e2dcb
7 | github.com/fatih/color v1.9.0
8 | github.com/google/go-cmp v0.5.9
9 | github.com/mattn/go-isatty v0.0.14
10 | github.com/pkg/errors v0.9.1
11 | gopkg.in/yaml.v3 v3.0.1
12 | k8s.io/apimachinery v0.27.3
13 | k8s.io/client-go v0.27.3
14 | )
15 |
16 | require (
17 | github.com/davecgh/go-spew v1.1.1 // indirect
18 | github.com/emicklei/go-restful/v3 v3.9.0 // indirect
19 | github.com/go-logr/logr v1.2.3 // indirect
20 | github.com/go-openapi/jsonpointer v0.19.6 // indirect
21 | github.com/go-openapi/jsonreference v0.20.1 // indirect
22 | github.com/go-openapi/swag v0.22.3 // indirect
23 | github.com/gogo/protobuf v1.3.2 // indirect
24 | github.com/golang/protobuf v1.5.3 // indirect
25 | github.com/google/gnostic v0.5.7-v3refs // indirect
26 | github.com/google/gofuzz v1.1.0 // indirect
27 | github.com/google/uuid v1.3.0 // indirect
28 | github.com/imdario/mergo v0.3.9 // indirect
29 | github.com/josharian/intern v1.0.0 // indirect
30 | github.com/json-iterator/go v1.1.12 // indirect
31 | github.com/mailru/easyjson v0.7.7 // indirect
32 | github.com/mattn/go-colorable v0.1.4 // indirect
33 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
34 | github.com/modern-go/reflect2 v1.0.2 // indirect
35 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
36 | github.com/spf13/pflag v1.0.5 // indirect
37 | golang.org/x/net v0.8.0 // indirect
38 | golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect
39 | golang.org/x/sys v0.6.0 // indirect
40 | golang.org/x/term v0.6.0 // indirect
41 | golang.org/x/text v0.8.0 // indirect
42 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
43 | google.golang.org/appengine v1.6.7 // indirect
44 | google.golang.org/protobuf v1.28.1 // indirect
45 | gopkg.in/inf.v0 v0.9.1 // indirect
46 | gopkg.in/yaml.v2 v2.4.0 // indirect
47 | k8s.io/api v0.27.3 // indirect
48 | k8s.io/klog/v2 v2.90.1 // indirect
49 | k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect
50 | k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect
51 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
52 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
53 | sigs.k8s.io/yaml v1.3.0 // indirect
54 | )
55 |
--------------------------------------------------------------------------------
/img/kubectx-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ahmetb/kubectx/013b6bc252ea6bbe7c8372ed64c327ad8a52f003/img/kubectx-demo.gif
--------------------------------------------------------------------------------
/img/kubectx-interactive.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ahmetb/kubectx/013b6bc252ea6bbe7c8372ed64c327ad8a52f003/img/kubectx-interactive.gif
--------------------------------------------------------------------------------
/img/kubens-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ahmetb/kubectx/013b6bc252ea6bbe7c8372ed64c327ad8a52f003/img/kubens-demo.gif
--------------------------------------------------------------------------------
/internal/cmdutil/deprecated.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package cmdutil
16 |
17 | import (
18 | "io"
19 | "strings"
20 |
21 | "github.com/ahmetb/kubectx/internal/printer"
22 | )
23 |
24 | func PrintDeprecatedEnvWarnings(out io.Writer, vars []string) {
25 | for _, vv := range vars {
26 | parts := strings.SplitN(vv, "=", 2)
27 | if len(parts) != 2 {
28 | continue
29 | }
30 | key := parts[0]
31 |
32 | if key == `KUBECTX_CURRENT_FGCOLOR` || key == `KUBECTX_CURRENT_BGCOLOR` {
33 | printer.Warning(out, "%s environment variable is now deprecated", key)
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/internal/cmdutil/deprecated_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package cmdutil
16 |
17 | import (
18 | "bytes"
19 | "strings"
20 | "testing"
21 | )
22 |
23 | func TestPrintDeprecatedEnvWarnings_noDeprecatedVars(t *testing.T) {
24 | var out bytes.Buffer
25 | PrintDeprecatedEnvWarnings(&out, []string{
26 | "A=B",
27 | "PATH=/foo:/bar:/bin",
28 | })
29 | if v := out.String(); len(v) > 0 {
30 | t.Fatalf("something written to buf: %v", v)
31 | }
32 | }
33 |
34 | func TestPrintDeprecatedEnvWarnings_bgColors(t *testing.T) {
35 | var out bytes.Buffer
36 |
37 | PrintDeprecatedEnvWarnings(&out, []string{
38 | "KUBECTX_CURRENT_FGCOLOR=1",
39 | "KUBECTX_CURRENT_BGCOLOR=2",
40 | })
41 | v := out.String()
42 | if !strings.Contains(v, "KUBECTX_CURRENT_FGCOLOR") {
43 | t.Fatalf("output doesn't contain 'KUBECTX_CURRENT_FGCOLOR': \"%s\"", v)
44 | }
45 | if !strings.Contains(v, "KUBECTX_CURRENT_BGCOLOR") {
46 | t.Fatalf("output doesn't contain 'KUBECTX_CURRENT_BGCOLOR': \"%s\"", v)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/internal/cmdutil/interactive.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package cmdutil
16 |
17 | import (
18 | "os"
19 | "os/exec"
20 |
21 | "github.com/mattn/go-isatty"
22 |
23 | "github.com/ahmetb/kubectx/internal/env"
24 | )
25 |
26 | // isTerminal determines if given fd is a TTY.
27 | func isTerminal(fd *os.File) bool {
28 | return isatty.IsTerminal(fd.Fd())
29 | }
30 |
31 | // fzfInstalled determines if fzf(1) is in PATH.
32 | func fzfInstalled() bool {
33 | v, _ := exec.LookPath("fzf")
34 | if v != "" {
35 | return true
36 | }
37 | return false
38 | }
39 |
40 | // IsInteractiveMode determines if we can do choosing with fzf.
41 | func IsInteractiveMode(stdout *os.File) bool {
42 | v := os.Getenv(env.EnvFZFIgnore)
43 | return v == "" && isTerminal(stdout) && fzfInstalled()
44 | }
45 |
--------------------------------------------------------------------------------
/internal/cmdutil/util.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package cmdutil
16 |
17 | import (
18 | "os"
19 |
20 | "github.com/pkg/errors"
21 | )
22 |
23 | func HomeDir() string {
24 | home := os.Getenv("HOME")
25 | if home == "" {
26 | home = os.Getenv("USERPROFILE") // windows
27 | }
28 | return home
29 | }
30 |
31 | // IsNotFoundErr determines if the underlying error is os.IsNotExist. Right now
32 | // errors from github.com/pkg/errors doesn't work with os.IsNotExist.
33 | func IsNotFoundErr(err error) bool {
34 | for e := err; e != nil; e = errors.Unwrap(e) {
35 | if os.IsNotExist(e) {
36 | return true
37 | }
38 | }
39 | return false
40 | }
41 |
--------------------------------------------------------------------------------
/internal/cmdutil/util_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package cmdutil
16 |
17 | import (
18 | "testing"
19 |
20 | "github.com/ahmetb/kubectx/internal/testutil"
21 | )
22 |
23 | func Test_homeDir(t *testing.T) {
24 | type env struct{ k, v string }
25 | cases := []struct {
26 | name string
27 | envs []env
28 | want string
29 | }{
30 | {
31 | name: "don't use XDG_CACHE_HOME as homedir",
32 | envs: []env{
33 | {"XDG_CACHE_HOME", "xdg"},
34 | {"HOME", "home"},
35 | },
36 | want: "home",
37 | },
38 | {
39 | name: "HOME over USERPROFILE",
40 | envs: []env{
41 | {"HOME", "home"},
42 | {"USERPROFILE", "up"},
43 | },
44 | want: "home",
45 | },
46 | {
47 | name: "only USERPROFILE available",
48 | envs: []env{
49 | {"HOME", ""},
50 | {"USERPROFILE", "up"},
51 | },
52 | want: "up",
53 | },
54 | {
55 | name: "none available",
56 | envs: []env{
57 | {"HOME", ""},
58 | {"USERPROFILE", ""},
59 | },
60 | want: "",
61 | },
62 | }
63 |
64 | for _, c := range cases {
65 | t.Run(c.name, func(tt *testing.T) {
66 | var unsets []func()
67 | for _, e := range c.envs {
68 | unsets = append(unsets, testutil.WithEnvVar(e.k, e.v))
69 | }
70 |
71 | got := HomeDir()
72 | if got != c.want {
73 | t.Errorf("expected:%q got:%q", c.want, got)
74 | }
75 | for _, u := range unsets {
76 | u()
77 | }
78 | })
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/internal/env/constants.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package env
16 |
17 | const (
18 | // EnvFZFIgnore describes the environment variable to set to disable
19 | // interactive context selection when fzf is installed.
20 | EnvFZFIgnore = "KUBECTX_IGNORE_FZF"
21 |
22 | // EnvNoColor describes the environment variable to disable color usage
23 | // when printing current context in a list.
24 | EnvNoColor = `NO_COLOR`
25 |
26 | // EnvForceColor describes the "internal" environment variable to force
27 | // color usage to show current context in a list.
28 | EnvForceColor = `_KUBECTX_FORCE_COLOR`
29 |
30 | // EnvDebug describes the internal environment variable for more verbose logging.
31 | EnvDebug = `DEBUG`
32 | )
33 |
--------------------------------------------------------------------------------
/internal/kubeconfig/contextmodify.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package kubeconfig
16 |
17 | import (
18 | "github.com/pkg/errors"
19 | "gopkg.in/yaml.v3"
20 | )
21 |
22 | func (k *Kubeconfig) DeleteContextEntry(deleteName string) error {
23 | contexts, err := k.contextsNode()
24 | if err != nil {
25 | return err
26 | }
27 |
28 | i := -1
29 | for j, ctxNode := range contexts.Content {
30 | nameNode := valueOf(ctxNode, "name")
31 | if nameNode != nil && nameNode.Kind == yaml.ScalarNode && nameNode.Value == deleteName {
32 | i = j
33 | break
34 | }
35 | }
36 | if i >= 0 {
37 | copy(contexts.Content[i:], contexts.Content[i+1:])
38 | contexts.Content[len(contexts.Content)-1] = nil
39 | contexts.Content = contexts.Content[:len(contexts.Content)-1]
40 | }
41 | return nil
42 | }
43 |
44 | func (k *Kubeconfig) ModifyCurrentContext(name string) error {
45 | currentCtxNode := valueOf(k.rootNode, "current-context")
46 | if currentCtxNode != nil {
47 | currentCtxNode.Value = name
48 | return nil
49 | }
50 |
51 | // if current-context field doesn't exist, create new field
52 | keyNode := &yaml.Node{
53 | Kind: yaml.ScalarNode,
54 | Value: "current-context",
55 | Tag: "!!str"}
56 | valueNode := &yaml.Node{
57 | Kind: yaml.ScalarNode,
58 | Value: name,
59 | Tag: "!!str"}
60 | k.rootNode.Content = append(k.rootNode.Content, keyNode, valueNode)
61 | return nil
62 | }
63 |
64 | func (k *Kubeconfig) ModifyContextName(old, new string) error {
65 | contexts, err := k.contextsNode()
66 | if err != nil {
67 | return err
68 | }
69 |
70 | var changed bool
71 | for _, contextNode := range contexts.Content {
72 | nameNode := valueOf(contextNode, "name")
73 | if nameNode.Kind == yaml.ScalarNode && nameNode.Value == old {
74 | nameNode.Value = new
75 | changed = true
76 | break
77 | }
78 | }
79 | if !changed {
80 | return errors.New("no changes were made")
81 | }
82 | return nil
83 | }
84 |
--------------------------------------------------------------------------------
/internal/kubeconfig/contextmodify_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package kubeconfig
16 |
17 | import (
18 | "testing"
19 |
20 | "github.com/google/go-cmp/cmp"
21 |
22 | "github.com/ahmetb/kubectx/internal/testutil"
23 | )
24 |
25 | func TestKubeconfig_DeleteContextEntry_errors(t *testing.T) {
26 | kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`[1, 2, 3]`))
27 | _ = kc.Parse()
28 | err := kc.DeleteContextEntry("foo")
29 | if err == nil {
30 | t.Fatal("supposed to fail on non-mapping nodes")
31 | }
32 |
33 | kc = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`a: b`))
34 | _ = kc.Parse()
35 | err = kc.DeleteContextEntry("foo")
36 | if err == nil {
37 | t.Fatal("supposed to fail if contexts key does not exist")
38 | }
39 |
40 | kc = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`contexts: "some string"`))
41 | _ = kc.Parse()
42 | err = kc.DeleteContextEntry("foo")
43 | if err == nil {
44 | t.Fatal("supposed to fail if contexts key is not an array")
45 | }
46 | }
47 |
48 | func TestKubeconfig_DeleteContextEntry(t *testing.T) {
49 | test := WithMockKubeconfigLoader(
50 | testutil.KC().WithCtxs(
51 | testutil.Ctx("c1"),
52 | testutil.Ctx("c2"),
53 | testutil.Ctx("c3")).ToYAML(t))
54 | kc := new(Kubeconfig).WithLoader(test)
55 | if err := kc.Parse(); err != nil {
56 | t.Fatal(err)
57 | }
58 | if err := kc.DeleteContextEntry("c1"); err != nil {
59 | t.Fatal(err)
60 | }
61 | if err := kc.Save(); err != nil {
62 | t.Fatal(err)
63 | }
64 |
65 | expected := testutil.KC().WithCtxs(
66 | testutil.Ctx("c2"),
67 | testutil.Ctx("c3")).ToYAML(t)
68 | out := test.Output()
69 | if diff := cmp.Diff(expected, out); diff != "" {
70 | t.Fatalf("diff: %s", diff)
71 | }
72 | }
73 |
74 | func TestKubeconfig_ModifyCurrentContext_fieldExists(t *testing.T) {
75 | test := WithMockKubeconfigLoader(
76 | testutil.KC().WithCurrentCtx("abc").Set("field1", "value1").ToYAML(t))
77 | kc := new(Kubeconfig).WithLoader(test)
78 | if err := kc.Parse(); err != nil {
79 | t.Fatal(err)
80 | }
81 | if err := kc.ModifyCurrentContext("foo"); err != nil {
82 | t.Fatal(err)
83 | }
84 | if err := kc.Save(); err != nil {
85 | t.Fatal(err)
86 | }
87 |
88 | expected := testutil.KC().WithCurrentCtx("foo").Set("field1", "value1").ToYAML(t)
89 | out := test.Output()
90 | if diff := cmp.Diff(expected, out); diff != "" {
91 | t.Fatalf("diff: %s", diff)
92 | }
93 | }
94 |
95 | func TestKubeconfig_ModifyCurrentContext_fieldMissing(t *testing.T) {
96 | test := WithMockKubeconfigLoader(`f1: v1`)
97 | kc := new(Kubeconfig).WithLoader(test)
98 | if err := kc.Parse(); err != nil {
99 | t.Fatal(err)
100 | }
101 | if err := kc.ModifyCurrentContext("foo"); err != nil {
102 | t.Fatal(err)
103 | }
104 | if err := kc.Save(); err != nil {
105 | t.Fatal(err)
106 | }
107 |
108 | expected := `f1: v1
109 | current-context: foo
110 | `
111 | out := test.Output()
112 | if diff := cmp.Diff(expected, out); diff != "" {
113 | t.Fatalf("diff: %s", diff)
114 | }
115 | }
116 |
117 | func TestKubeconfig_ModifyContextName_noContextsEntryError(t *testing.T) {
118 | // no context entries
119 | test := WithMockKubeconfigLoader(`a: b`)
120 | kc := new(Kubeconfig).WithLoader(test)
121 | if err := kc.Parse(); err != nil {
122 | t.Fatal(err)
123 | }
124 | if err := kc.ModifyContextName("c1", "c2"); err == nil {
125 | t.Fatal("was expecting error for no 'contexts' entry; got nil")
126 | }
127 | }
128 |
129 | func TestKubeconfig_ModifyContextName_contextsEntryNotSequenceError(t *testing.T) {
130 | // no context entries
131 | test := WithMockKubeconfigLoader(
132 | `contexts: "hello"`)
133 | kc := new(Kubeconfig).WithLoader(test)
134 | if err := kc.Parse(); err != nil {
135 | t.Fatal(err)
136 | }
137 | if err := kc.ModifyContextName("c1", "c2"); err == nil {
138 | t.Fatal("was expecting error for 'context entry not a sequence'; got nil")
139 | }
140 | }
141 |
142 | func TestKubeconfig_ModifyContextName_noChange(t *testing.T) {
143 | test := WithMockKubeconfigLoader(testutil.KC().WithCtxs(
144 | testutil.Ctx("c1"),
145 | testutil.Ctx("c2"),
146 | testutil.Ctx("c3")).ToYAML(t))
147 | kc := new(Kubeconfig).WithLoader(test)
148 | if err := kc.Parse(); err != nil {
149 | t.Fatal(err)
150 | }
151 | if err := kc.ModifyContextName("c5", "c6"); err == nil {
152 | t.Fatal("was expecting error for 'no changes made'")
153 | }
154 | }
155 |
156 | func TestKubeconfig_ModifyContextName(t *testing.T) {
157 | test := WithMockKubeconfigLoader(testutil.KC().WithCtxs(
158 | testutil.Ctx("c1"),
159 | testutil.Ctx("c2"),
160 | testutil.Ctx("c3")).ToYAML(t))
161 | kc := new(Kubeconfig).WithLoader(test)
162 | if err := kc.Parse(); err != nil {
163 | t.Fatal(err)
164 | }
165 | if err := kc.ModifyContextName("c1", "ccc"); err != nil {
166 | t.Fatal(err)
167 | }
168 | if err := kc.Save(); err != nil {
169 | t.Fatal(err)
170 | }
171 |
172 | expected := testutil.KC().WithCtxs(
173 | testutil.Ctx("ccc"),
174 | testutil.Ctx("c2"),
175 | testutil.Ctx("c3")).ToYAML(t)
176 | out := test.Output()
177 | if diff := cmp.Diff(expected, out); diff != "" {
178 | t.Fatalf("diff: %s", diff)
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/internal/kubeconfig/contexts.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package kubeconfig
16 |
17 | import (
18 | "github.com/pkg/errors"
19 | "gopkg.in/yaml.v3"
20 | )
21 |
22 | func (k *Kubeconfig) contextsNode() (*yaml.Node, error) {
23 | contexts := valueOf(k.rootNode, "contexts")
24 | if contexts == nil {
25 | return nil, errors.New("\"contexts\" entry is nil")
26 | } else if contexts.Kind != yaml.SequenceNode {
27 | return nil, errors.New("\"contexts\" is not a sequence node")
28 | }
29 | return contexts, nil
30 | }
31 |
32 | func (k *Kubeconfig) contextNode(name string) (*yaml.Node, error) {
33 | contexts, err := k.contextsNode()
34 | if err != nil {
35 | return nil, err
36 | }
37 |
38 | for _, contextNode := range contexts.Content {
39 | nameNode := valueOf(contextNode, "name")
40 | if nameNode.Kind == yaml.ScalarNode && nameNode.Value == name {
41 | return contextNode, nil
42 | }
43 | }
44 | return nil, errors.Errorf("context with name \"%s\" not found", name)
45 | }
46 |
47 | func (k *Kubeconfig) ContextNames() []string {
48 | contexts := valueOf(k.rootNode, "contexts")
49 | if contexts == nil {
50 | return nil
51 | }
52 | if contexts.Kind != yaml.SequenceNode {
53 | return nil
54 | }
55 |
56 | var ctxNames []string
57 | for _, ctx := range contexts.Content {
58 | nameVal := valueOf(ctx, "name")
59 | if nameVal != nil {
60 | ctxNames = append(ctxNames, nameVal.Value)
61 | }
62 | }
63 | return ctxNames
64 | }
65 |
66 | func (k *Kubeconfig) ContextExists(name string) bool {
67 | ctxNames := k.ContextNames()
68 | for _, v := range ctxNames {
69 | if v == name {
70 | return true
71 | }
72 | }
73 | return false
74 | }
75 |
76 | func valueOf(mapNode *yaml.Node, key string) *yaml.Node {
77 | if mapNode.Kind != yaml.MappingNode {
78 | return nil
79 | }
80 | for i, ch := range mapNode.Content {
81 | if i%2 == 0 && ch.Kind == yaml.ScalarNode && ch.Value == key {
82 | return mapNode.Content[i+1]
83 | }
84 | }
85 | return nil
86 | }
87 |
--------------------------------------------------------------------------------
/internal/kubeconfig/contexts_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package kubeconfig
16 |
17 | import (
18 | "testing"
19 |
20 | "github.com/google/go-cmp/cmp"
21 |
22 | "github.com/ahmetb/kubectx/internal/testutil"
23 | )
24 |
25 | func TestKubeconfig_ContextNames(t *testing.T) {
26 | tl := WithMockKubeconfigLoader(
27 | testutil.KC().WithCtxs(
28 | testutil.Ctx("abc"),
29 | testutil.Ctx("def"),
30 | testutil.Ctx("ghi")).Set("field1", map[string]string{"bar": "zoo"}).ToYAML(t))
31 | kc := new(Kubeconfig).WithLoader(tl)
32 | if err := kc.Parse(); err != nil {
33 | t.Fatal(err)
34 | }
35 |
36 | ctx := kc.ContextNames()
37 | expected := []string{"abc", "def", "ghi"}
38 | if diff := cmp.Diff(expected, ctx); diff != "" {
39 | t.Fatalf("%s", diff)
40 | }
41 | }
42 |
43 | func TestKubeconfig_ContextNames_noContextsEntry(t *testing.T) {
44 | tl := WithMockKubeconfigLoader(`a: b`)
45 | kc := new(Kubeconfig).WithLoader(tl)
46 | if err := kc.Parse(); err != nil {
47 | t.Fatal(err)
48 | }
49 | ctx := kc.ContextNames()
50 | var expected []string = nil
51 | if diff := cmp.Diff(expected, ctx); diff != "" {
52 | t.Fatalf("%s", diff)
53 | }
54 | }
55 |
56 | func TestKubeconfig_ContextNames_nonArrayContextsEntry(t *testing.T) {
57 | tl := WithMockKubeconfigLoader(`contexts: "hello"`)
58 | kc := new(Kubeconfig).WithLoader(tl)
59 | if err := kc.Parse(); err != nil {
60 | t.Fatal(err)
61 | }
62 | ctx := kc.ContextNames()
63 | var expected []string = nil
64 | if diff := cmp.Diff(expected, ctx); diff != "" {
65 | t.Fatalf("%s", diff)
66 | }
67 | }
68 |
69 | func TestKubeconfig_CheckContextExists(t *testing.T) {
70 | tl := WithMockKubeconfigLoader(
71 | testutil.KC().WithCtxs(
72 | testutil.Ctx("c1"),
73 | testutil.Ctx("c2")).ToYAML(t))
74 |
75 | kc := new(Kubeconfig).WithLoader(tl)
76 | if err := kc.Parse(); err != nil {
77 | t.Fatal(err)
78 | }
79 |
80 | if !kc.ContextExists("c1") {
81 | t.Fatal("c1 actually exists; reported false")
82 | }
83 | if !kc.ContextExists("c2") {
84 | t.Fatal("c2 actually exists; reported false")
85 | }
86 | if kc.ContextExists("c3") {
87 | t.Fatal("c3 does not exist; but reported true")
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/internal/kubeconfig/currentcontext.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package kubeconfig
16 |
17 | // GetCurrentContext returns "current-context" value in given
18 | // kubeconfig object Node, or returns "" if not found.
19 | func (k *Kubeconfig) GetCurrentContext() string {
20 | v := valueOf(k.rootNode, "current-context")
21 | if v == nil {
22 | return ""
23 | }
24 | return v.Value
25 | }
26 |
27 | func (k *Kubeconfig) UnsetCurrentContext() error {
28 | curCtxValNode := valueOf(k.rootNode, "current-context")
29 | curCtxValNode.Value = ""
30 | return nil
31 | }
32 |
--------------------------------------------------------------------------------
/internal/kubeconfig/currentcontext_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package kubeconfig
16 |
17 | import (
18 | "testing"
19 |
20 | "github.com/ahmetb/kubectx/internal/testutil"
21 | )
22 |
23 | func TestKubeconfig_GetCurrentContext(t *testing.T) {
24 | tl := WithMockKubeconfigLoader(`current-context: foo`)
25 | kc := new(Kubeconfig).WithLoader(tl)
26 | if err := kc.Parse(); err != nil {
27 | t.Fatal(err)
28 | }
29 | v := kc.GetCurrentContext()
30 |
31 | expected := "foo"
32 | if v != expected {
33 | t.Fatalf("expected=\"%s\"; got=\"%s\"", expected, v)
34 | }
35 | }
36 |
37 | func TestKubeconfig_GetCurrentContext_missingField(t *testing.T) {
38 | tl := WithMockKubeconfigLoader(`abc: def`)
39 | kc := new(Kubeconfig).WithLoader(tl)
40 | if err := kc.Parse(); err != nil {
41 | t.Fatal(err)
42 | }
43 | v := kc.GetCurrentContext()
44 |
45 | expected := ""
46 | if v != expected {
47 | t.Fatalf("expected=\"%s\"; got=\"%s\"", expected, v)
48 | }
49 | }
50 |
51 | func TestKubeconfig_UnsetCurrentContext(t *testing.T) {
52 | tl := WithMockKubeconfigLoader(testutil.KC().WithCurrentCtx("foo").ToYAML(t))
53 | kc := new(Kubeconfig).WithLoader(tl)
54 | if err := kc.Parse(); err != nil {
55 | t.Fatal(err)
56 | }
57 | if err := kc.UnsetCurrentContext(); err != nil {
58 | t.Fatal(err)
59 | }
60 | if err := kc.Save(); err != nil {
61 | t.Fatal(err)
62 | }
63 |
64 | out := tl.Output()
65 | expected := testutil.KC().WithCurrentCtx("").ToYAML(t)
66 | if out != expected {
67 | t.Fatalf("expected=\"%s\"; got=\"%s\"", expected, out)
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/internal/kubeconfig/helper_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package kubeconfig
16 |
17 | import (
18 | "bytes"
19 | "io"
20 | "strings"
21 | )
22 |
23 | type MockKubeconfigLoader struct {
24 | in io.Reader
25 | out bytes.Buffer
26 | }
27 |
28 | func (t *MockKubeconfigLoader) Read(p []byte) (n int, err error) { return t.in.Read(p) }
29 | func (t *MockKubeconfigLoader) Write(p []byte) (n int, err error) { return t.out.Write(p) }
30 | func (t *MockKubeconfigLoader) Close() error { return nil }
31 | func (t *MockKubeconfigLoader) Reset() error { return nil }
32 | func (t *MockKubeconfigLoader) Load() ([]ReadWriteResetCloser, error) {
33 | return []ReadWriteResetCloser{ReadWriteResetCloser(t)}, nil
34 | }
35 | func (t *MockKubeconfigLoader) Output() string { return t.out.String() }
36 |
37 | func WithMockKubeconfigLoader(kubecfg string) *MockKubeconfigLoader {
38 | return &MockKubeconfigLoader{in: strings.NewReader(kubecfg)}
39 | }
40 |
--------------------------------------------------------------------------------
/internal/kubeconfig/kubeconfig.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package kubeconfig
16 |
17 | import (
18 | "io"
19 |
20 | "github.com/pkg/errors"
21 | "gopkg.in/yaml.v3"
22 | )
23 |
24 | type ReadWriteResetCloser interface {
25 | io.ReadWriteCloser
26 |
27 | // Reset truncates the file and seeks to the beginning of the file.
28 | Reset() error
29 | }
30 |
31 | type Loader interface {
32 | Load() ([]ReadWriteResetCloser, error)
33 | }
34 |
35 | type Kubeconfig struct {
36 | loader Loader
37 |
38 | f ReadWriteResetCloser
39 | rootNode *yaml.Node
40 | }
41 |
42 | func (k *Kubeconfig) WithLoader(l Loader) *Kubeconfig {
43 | k.loader = l
44 | return k
45 | }
46 |
47 | func (k *Kubeconfig) Close() error {
48 | if k.f == nil {
49 | return nil
50 | }
51 | return k.f.Close()
52 | }
53 |
54 | func (k *Kubeconfig) Parse() error {
55 | files, err := k.loader.Load()
56 | if err != nil {
57 | return errors.Wrap(err, "failed to load")
58 | }
59 |
60 | // TODO since we don't support multiple kubeconfig files at the moment, there's just 1 file
61 | f := files[0]
62 |
63 | k.f = f
64 | var v yaml.Node
65 | if err := yaml.NewDecoder(f).Decode(&v); err != nil {
66 | return errors.Wrap(err, "failed to decode")
67 | }
68 | k.rootNode = v.Content[0]
69 | if k.rootNode.Kind != yaml.MappingNode {
70 | return errors.New("kubeconfig file is not a map document")
71 | }
72 | return nil
73 | }
74 |
75 | func (k *Kubeconfig) Bytes() ([]byte, error) {
76 | return yaml.Marshal(k.rootNode)
77 | }
78 |
79 | func (k *Kubeconfig) Save() error {
80 | if err := k.f.Reset(); err != nil {
81 | return errors.Wrap(err, "failed to reset file")
82 | }
83 | enc := yaml.NewEncoder(k.f)
84 | enc.SetIndent(0)
85 | return enc.Encode(k.rootNode)
86 | }
87 |
--------------------------------------------------------------------------------
/internal/kubeconfig/kubeconfig_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package kubeconfig
16 |
17 | import (
18 | "testing"
19 |
20 | "github.com/google/go-cmp/cmp"
21 |
22 | "github.com/ahmetb/kubectx/internal/testutil"
23 | )
24 |
25 | func TestParse(t *testing.T) {
26 | err := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`a: [1, 2`)).Parse()
27 | if err == nil {
28 | t.Fatal("expected error from bad yaml")
29 | }
30 |
31 | err = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`[1, 2, 3]`)).Parse()
32 | if err == nil {
33 | t.Fatal("expected error from not-mapping root node")
34 | }
35 |
36 | err = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`current-context: foo`)).Parse()
37 | if err != nil {
38 | t.Fatal(err)
39 | }
40 |
41 | err = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC().
42 | WithCurrentCtx("foo").
43 | WithCtxs().ToYAML(t))).Parse()
44 | if err != nil {
45 | t.Fatal(err)
46 | }
47 | }
48 |
49 | func TestSave(t *testing.T) {
50 | in := "a: [1, 2, 3]\n"
51 | test := WithMockKubeconfigLoader(in)
52 | kc := new(Kubeconfig).WithLoader(test)
53 | defer kc.Close()
54 | if err := kc.Parse(); err != nil {
55 | t.Fatal(err)
56 | }
57 | if err := kc.ModifyCurrentContext("hello"); err != nil {
58 | t.Fatal(err)
59 | }
60 | if err := kc.Save(); err != nil {
61 | t.Fatal(err)
62 | }
63 | expected := "a: [1, 2, 3]\ncurrent-context: hello\n"
64 | if diff := cmp.Diff(expected, test.Output()); diff != "" {
65 | t.Fatal(diff)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/internal/kubeconfig/kubeconfigloader.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package kubeconfig
16 |
17 | import (
18 | "github.com/ahmetb/kubectx/internal/cmdutil"
19 | "os"
20 | "path/filepath"
21 |
22 | "github.com/pkg/errors"
23 | )
24 |
25 | var (
26 | DefaultLoader Loader = new(StandardKubeconfigLoader)
27 | )
28 |
29 | type StandardKubeconfigLoader struct{}
30 |
31 | type kubeconfigFile struct{ *os.File }
32 |
33 | func (*StandardKubeconfigLoader) Load() ([]ReadWriteResetCloser, error) {
34 | cfgPath, err := kubeconfigPath()
35 | if err != nil {
36 | return nil, errors.Wrap(err, "cannot determine kubeconfig path")
37 | }
38 |
39 | f, err := os.OpenFile(cfgPath, os.O_RDWR, 0)
40 | if err != nil {
41 | if os.IsNotExist(err) {
42 | return nil, errors.Wrap(err, "kubeconfig file not found")
43 | }
44 | return nil, errors.Wrap(err, "failed to open file")
45 | }
46 |
47 | // TODO we'll return all kubeconfig files when we start implementing multiple kubeconfig support
48 | return []ReadWriteResetCloser{ReadWriteResetCloser(&kubeconfigFile{f})}, nil
49 | }
50 |
51 | func (kf *kubeconfigFile) Reset() error {
52 | if err := kf.Truncate(0); err != nil {
53 | return errors.Wrap(err, "failed to truncate file")
54 | }
55 | _, err := kf.Seek(0, 0)
56 | return errors.Wrap(err, "failed to seek in file")
57 | }
58 |
59 | func kubeconfigPath() (string, error) {
60 | // KUBECONFIG env var
61 | if v := os.Getenv("KUBECONFIG"); v != "" {
62 | list := filepath.SplitList(v)
63 | if len(list) > 1 {
64 | // TODO KUBECONFIG=file1:file2 currently not supported
65 | return "", errors.New("multiple files in KUBECONFIG are currently not supported")
66 | }
67 | return v, nil
68 | }
69 |
70 | // default path
71 | home := cmdutil.HomeDir()
72 | if home == "" {
73 | return "", errors.New("HOME or USERPROFILE environment variable not set")
74 | }
75 | return filepath.Join(home, ".kube", "config"), nil
76 | }
77 |
--------------------------------------------------------------------------------
/internal/kubeconfig/kubeconfigloader_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package kubeconfig
16 |
17 | import (
18 | "github.com/ahmetb/kubectx/internal/cmdutil"
19 | "os"
20 | "path/filepath"
21 | "strings"
22 | "testing"
23 |
24 | "github.com/ahmetb/kubectx/internal/testutil"
25 | )
26 |
27 | func Test_kubeconfigPath(t *testing.T) {
28 | defer testutil.WithEnvVar("HOME", "/x/y/z")()
29 |
30 | expected := filepath.FromSlash("/x/y/z/.kube/config")
31 | got, err := kubeconfigPath()
32 | if err != nil {
33 | t.Fatal(err)
34 | }
35 | if got != expected {
36 | t.Fatalf("got=%q expected=%q", got, expected)
37 | }
38 | }
39 |
40 | func Test_kubeconfigPath_noEnvVars(t *testing.T) {
41 | defer testutil.WithEnvVar("XDG_CACHE_HOME", "")()
42 | defer testutil.WithEnvVar("HOME", "")()
43 | defer testutil.WithEnvVar("USERPROFILE", "")()
44 |
45 | _, err := kubeconfigPath()
46 | if err == nil {
47 | t.Fatalf("expected error")
48 | }
49 | }
50 |
51 | func Test_kubeconfigPath_envOvveride(t *testing.T) {
52 | defer testutil.WithEnvVar("KUBECONFIG", "foo")()
53 |
54 | v, err := kubeconfigPath()
55 | if err != nil {
56 | t.Fatal(err)
57 | }
58 | if expected := "foo"; v != expected {
59 | t.Fatalf("expected=%q, got=%q", expected, v)
60 | }
61 | }
62 |
63 | func Test_kubeconfigPath_envOvverideDoesNotSupportPathSeparator(t *testing.T) {
64 | path := strings.Join([]string{"file1", "file2"}, string(os.PathListSeparator))
65 | defer testutil.WithEnvVar("KUBECONFIG", path)()
66 |
67 | _, err := kubeconfigPath()
68 | if err == nil {
69 | t.Fatal("expected error")
70 | }
71 | }
72 |
73 | func TestStandardKubeconfigLoader_returnsNotFoundErr(t *testing.T) {
74 | defer testutil.WithEnvVar("KUBECONFIG", "foo")()
75 | kc := new(Kubeconfig).WithLoader(DefaultLoader)
76 | err := kc.Parse()
77 | if err == nil {
78 | t.Fatal("expected err")
79 | }
80 | if !cmdutil.IsNotFoundErr(err) {
81 | t.Fatalf("expected ENOENT error; got=%v", err)
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/internal/kubeconfig/namespace.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package kubeconfig
16 |
17 | import "gopkg.in/yaml.v3"
18 |
19 | const (
20 | defaultNamespace = "default"
21 | )
22 |
23 | func (k *Kubeconfig) NamespaceOfContext(contextName string) (string, error) {
24 | ctx, err := k.contextNode(contextName)
25 | if err != nil {
26 | return "", err
27 | }
28 | ctxBody := valueOf(ctx, "context")
29 | if ctxBody == nil {
30 | return defaultNamespace, nil
31 | }
32 | ns := valueOf(ctxBody, "namespace")
33 | if ns == nil || ns.Value == "" {
34 | return defaultNamespace, nil
35 | }
36 | return ns.Value, nil
37 | }
38 |
39 | func (k *Kubeconfig) SetNamespace(ctxName string, ns string) error {
40 | ctxNode, err := k.contextNode(ctxName)
41 | if err != nil {
42 | return err
43 | }
44 |
45 | var ctxBodyNodeWasEmpty bool // actual namespace value is in contexts[index].context.namespace, but .context might not exist
46 | ctxBodyNode := valueOf(ctxNode, "context")
47 | if ctxBodyNode == nil {
48 | ctxBodyNodeWasEmpty = true
49 | ctxBodyNode = &yaml.Node{
50 | Kind: yaml.MappingNode,
51 | }
52 | }
53 |
54 | nsNode := valueOf(ctxBodyNode, "namespace")
55 | if nsNode != nil {
56 | nsNode.Value = ns
57 | return nil
58 | }
59 |
60 | keyNode := &yaml.Node{
61 | Kind: yaml.ScalarNode,
62 | Value: "namespace",
63 | Tag: "!!str"}
64 | valueNode := &yaml.Node{
65 | Kind: yaml.ScalarNode,
66 | Value: ns,
67 | Tag: "!!str"}
68 | ctxBodyNode.Content = append(ctxBodyNode.Content, keyNode, valueNode)
69 | if ctxBodyNodeWasEmpty {
70 | ctxNode.Content = append(ctxNode.Content, &yaml.Node{
71 | Kind: yaml.ScalarNode,
72 | Value: "context",
73 | Tag: "!!str",
74 | }, ctxBodyNode)
75 | }
76 | return nil
77 | }
78 |
--------------------------------------------------------------------------------
/internal/kubeconfig/namespace_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package kubeconfig
16 |
17 | import (
18 | "testing"
19 |
20 | "github.com/google/go-cmp/cmp"
21 |
22 | "github.com/ahmetb/kubectx/internal/testutil"
23 | )
24 |
25 | func TestKubeconfig_NamespaceOfContext_ctxNotFound(t *testing.T) {
26 | kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC().
27 | WithCtxs(testutil.Ctx("c1")).ToYAML(t)))
28 | if err := kc.Parse(); err != nil {
29 | t.Fatal(err)
30 | }
31 |
32 | _, err := kc.NamespaceOfContext("c2")
33 | if err == nil {
34 | t.Fatal("expected err")
35 | }
36 | }
37 |
38 | func TestKubeconfig_NamespaceOfContext(t *testing.T) {
39 | kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC().
40 | WithCtxs(
41 | testutil.Ctx("c1"),
42 | testutil.Ctx("c2").Ns("c2n1")).ToYAML(t)))
43 | if err := kc.Parse(); err != nil {
44 | t.Fatal(err)
45 | }
46 |
47 | v1, err := kc.NamespaceOfContext("c1")
48 | if err != nil {
49 | t.Fatal("expected err")
50 | }
51 | if expected := `default`; v1 != expected {
52 | t.Fatalf("c1: expected=\"%s\" got=\"%s\"", expected, v1)
53 | }
54 |
55 | v2, err := kc.NamespaceOfContext("c2")
56 | if err != nil {
57 | t.Fatal("expected err")
58 | }
59 | if expected := `c2n1`; v2 != expected {
60 | t.Fatalf("c2: expected=\"%s\" got=\"%s\"", expected, v2)
61 | }
62 | }
63 |
64 | func TestKubeconfig_SetNamespace(t *testing.T) {
65 | l := WithMockKubeconfigLoader(testutil.KC().
66 | WithCtxs(
67 | testutil.Ctx("c1"),
68 | testutil.Ctx("c2").Ns("c2n1")).ToYAML(t))
69 | kc := new(Kubeconfig).WithLoader(l)
70 | if err := kc.Parse(); err != nil {
71 | t.Fatal(err)
72 | }
73 |
74 | if err := kc.SetNamespace("c3", "foo"); err == nil {
75 | t.Fatalf("expected error for non-existing ctx")
76 | }
77 |
78 | if err := kc.SetNamespace("c1", "c1n1"); err != nil {
79 | t.Fatal(err)
80 | }
81 | if err := kc.SetNamespace("c2", "c2n2"); err != nil {
82 | t.Fatal(err)
83 | }
84 | if err := kc.Save(); err != nil {
85 | t.Fatal(err)
86 | }
87 |
88 | expected := testutil.KC().WithCtxs(
89 | testutil.Ctx("c1").Ns("c1n1"),
90 | testutil.Ctx("c2").Ns("c2n2")).ToYAML(t)
91 | if diff := cmp.Diff(l.Output(), expected); diff != "" {
92 | t.Fatal(diff)
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/internal/printer/color.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package printer
16 |
17 | import (
18 | "os"
19 |
20 | "github.com/fatih/color"
21 |
22 | "github.com/ahmetb/kubectx/internal/env"
23 | )
24 |
25 | var (
26 | ActiveItemColor = color.New(color.FgGreen, color.Bold)
27 | )
28 |
29 | func init() {
30 | EnableOrDisableColor(ActiveItemColor)
31 | }
32 |
33 | // useColors returns true if colors are force-enabled,
34 | // false if colors are disabled, or nil for default behavior
35 | // which is determined based on factors like if stdout is tty.
36 | func useColors() *bool {
37 | tr, fa := true, false
38 | if os.Getenv(env.EnvForceColor) != "" {
39 | return &tr
40 | } else if os.Getenv(env.EnvNoColor) != "" {
41 | return &fa
42 | }
43 | return nil
44 | }
45 |
46 | // EnableOrDisableColor determines if color should be force-enabled or force-disabled
47 | // or left untouched based on environment configuration.
48 | func EnableOrDisableColor(c *color.Color) {
49 | if v := useColors(); v != nil && *v {
50 | c.EnableColor()
51 | } else if v != nil && !*v {
52 | c.DisableColor()
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/internal/printer/color_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package printer
16 |
17 | import (
18 | "testing"
19 |
20 | "github.com/google/go-cmp/cmp"
21 |
22 | "github.com/ahmetb/kubectx/internal/testutil"
23 | )
24 |
25 | var (
26 | tr, fa = true, false
27 | )
28 |
29 | func Test_useColors_forceColors(t *testing.T) {
30 | defer testutil.WithEnvVar("_KUBECTX_FORCE_COLOR", "1")()
31 | defer testutil.WithEnvVar("NO_COLOR", "1")()
32 |
33 | if v := useColors(); !cmp.Equal(v, &tr) {
34 | t.Fatalf("expected useColors() = true; got = %v", v)
35 | }
36 | }
37 |
38 | func Test_useColors_disableColors(t *testing.T) {
39 | defer testutil.WithEnvVar("NO_COLOR", "1")()
40 |
41 | if v := useColors(); !cmp.Equal(v, &fa) {
42 | t.Fatalf("expected useColors() = false; got = %v", v)
43 | }
44 | }
45 |
46 | func Test_useColors_default(t *testing.T) {
47 | defer testutil.WithEnvVar("NO_COLOR", "")()
48 | defer testutil.WithEnvVar("_KUBECTX_FORCE_COLOR", "")()
49 |
50 | if v := useColors(); v != nil {
51 | t.Fatalf("expected useColors() = nil; got=%v", *v)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/internal/printer/printer.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package printer
16 |
17 | import (
18 | "fmt"
19 | "io"
20 |
21 | "github.com/fatih/color"
22 | )
23 |
24 | var (
25 | ErrorColor = color.New(color.FgRed, color.Bold)
26 | WarningColor = color.New(color.FgYellow, color.Bold)
27 | SuccessColor = color.New(color.FgGreen)
28 | )
29 |
30 | func init() {
31 | colors := useColors()
32 | if colors == nil {
33 | return
34 | }
35 | if *colors {
36 | ErrorColor.EnableColor()
37 | WarningColor.EnableColor()
38 | SuccessColor.EnableColor()
39 | } else {
40 | ErrorColor.DisableColor()
41 | WarningColor.DisableColor()
42 | SuccessColor.DisableColor()
43 | }
44 | }
45 |
46 | func Error(w io.Writer, format string, args ...interface{}) error {
47 | _, err := fmt.Fprintf(w, ErrorColor.Sprint("error: ")+format+"\n", args...)
48 | return err
49 | }
50 |
51 | func Warning(w io.Writer, format string, args ...interface{}) error {
52 | _, err := fmt.Fprintf(w, WarningColor.Sprint("warning: ")+format+"\n", args...)
53 | return err
54 | }
55 |
56 | func Success(w io.Writer, format string, args ...interface{}) error {
57 | _, err := fmt.Fprintf(w, SuccessColor.Sprint("✔ ")+fmt.Sprintf(format+"\n", args...))
58 | return err
59 | }
60 |
--------------------------------------------------------------------------------
/internal/testutil/kubeconfigbuilder.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package testutil
16 |
17 | import (
18 | "strings"
19 | "testing"
20 |
21 | "gopkg.in/yaml.v3"
22 | )
23 |
24 | type Context struct {
25 | Name string `yaml:"name,omitempty"`
26 | Context struct {
27 | Namespace string `yaml:"namespace,omitempty"`
28 | } `yaml:"context,omitempty"`
29 | }
30 |
31 | func Ctx(name string) *Context { return &Context{Name: name} }
32 | func (c *Context) Ns(ns string) *Context { c.Context.Namespace = ns; return c }
33 |
34 | type Kubeconfig map[string]interface{}
35 |
36 | func KC() *Kubeconfig {
37 | return &Kubeconfig{
38 | "apiVersion": "v1",
39 | "kind": "Config"}
40 | }
41 |
42 | func (k *Kubeconfig) Set(key string, v interface{}) *Kubeconfig { (*k)[key] = v; return k }
43 | func (k *Kubeconfig) WithCurrentCtx(s string) *Kubeconfig { (*k)["current-context"] = s; return k }
44 | func (k *Kubeconfig) WithCtxs(c ...*Context) *Kubeconfig { (*k)["contexts"] = c; return k }
45 |
46 | func (k *Kubeconfig) ToYAML(t *testing.T) string {
47 | t.Helper()
48 | var v strings.Builder
49 | if err := yaml.NewEncoder(&v).Encode(*k); err != nil {
50 | t.Fatalf("failed to encode mock kubeconfig: %v", err)
51 | }
52 | return v.String()
53 | }
54 |
--------------------------------------------------------------------------------
/internal/testutil/tempfile.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package testutil
16 |
17 | import (
18 | "io/ioutil"
19 | "os"
20 | "testing"
21 | )
22 |
23 | func TempFile(t *testing.T, contents string) (path string, cleanup func()) {
24 | // TODO consider removing, used only in one place.
25 | t.Helper()
26 |
27 | f, err := ioutil.TempFile(os.TempDir(), "test-file")
28 | if err != nil {
29 | t.Fatalf("failed to create test file: %v", err)
30 | }
31 | path = f.Name()
32 | if _, err := f.Write([]byte(contents)); err != nil {
33 | t.Fatalf("failed to write to test file: %v", err)
34 | }
35 |
36 | return path, func() {
37 | f.Close()
38 | os.Remove(path)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/internal/testutil/testutil.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package testutil
16 |
17 | import "os"
18 |
19 | // WithEnvVar sets an env var temporarily. Call its return value
20 | // in defer to restore original value in env (if exists).
21 | func WithEnvVar(key, value string) func() {
22 | orig, ok := os.LookupEnv(key)
23 | os.Setenv(key, value)
24 | return func() {
25 | if ok {
26 | os.Setenv(key, orig)
27 | } else {
28 | os.Unsetenv(key)
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/kubectx:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | #
3 | # kubectx(1) is a utility to manage and switch between kubectl contexts.
4 |
5 | # Copyright 2017 Google Inc.
6 | #
7 | # Licensed under the Apache License, Version 2.0 (the "License");
8 | # you may not use this file except in compliance with the License.
9 | # You may obtain a copy of the License at
10 | #
11 | # http://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing, software
14 | # distributed under the License is distributed on an "AS IS" BASIS,
15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | # See the License for the specific language governing permissions and
17 | # limitations under the License.
18 |
19 | [[ -n $DEBUG ]] && set -x
20 |
21 | set -eou pipefail
22 | IFS=$'\n\t'
23 |
24 | SELF_CMD="$0"
25 |
26 | KUBECTX="${XDG_CACHE_HOME:-$HOME/.kube}/kubectx"
27 |
28 | usage() {
29 | local SELF
30 | SELF="kubectx"
31 | if [[ "$(basename "$0")" == kubectl-* ]]; then # invoked as plugin
32 | SELF="kubectl ctx"
33 | fi
34 |
35 | cat < : switch to context
39 | $SELF - : switch to the previous context
40 | $SELF -c, --current : show the current context name
41 | $SELF = : rename context to
42 | $SELF =. : rename current-context to
43 | $SELF -d [] : delete context ('.' for current-context)
44 | (this command won't delete the user/cluster entry
45 | that is used by the context)
46 | $SELF -u, --unset : unset the current context
47 |
48 | $SELF -h,--help : show this message
49 | EOF
50 | }
51 |
52 | exit_err() {
53 | echo >&2 "${1}"
54 | exit 1
55 | }
56 |
57 | current_context() {
58 | $KUBECTL config view -o=jsonpath='{.current-context}'
59 | }
60 |
61 | get_contexts() {
62 | $KUBECTL config get-contexts -o=name | sort -n
63 | }
64 |
65 | list_contexts() {
66 | set -u pipefail
67 | local cur ctx_list
68 | cur="$(current_context)" || exit_err "error getting current context"
69 | ctx_list=$(get_contexts) || exit_err "error getting context list"
70 |
71 | local yellow darkbg normal
72 | yellow=$(tput setaf 3 || true)
73 | darkbg=$(tput setab 0 || true)
74 | normal=$(tput sgr0 || true)
75 |
76 | local cur_ctx_fg cur_ctx_bg
77 | cur_ctx_fg=${KUBECTX_CURRENT_FGCOLOR:-$yellow}
78 | cur_ctx_bg=${KUBECTX_CURRENT_BGCOLOR:-$darkbg}
79 |
80 | for c in $ctx_list; do
81 | if [[ -n "${_KUBECTX_FORCE_COLOR:-}" || \
82 | -t 1 && -z "${NO_COLOR:-}" ]]; then
83 | # colored output mode
84 | if [[ "${c}" = "${cur}" ]]; then
85 | echo "${cur_ctx_bg}${cur_ctx_fg}${c}${normal}"
86 | else
87 | echo "${c}"
88 | fi
89 | else
90 | echo "${c}"
91 | fi
92 | done
93 | }
94 |
95 | read_context() {
96 | if [[ -f "${KUBECTX}" ]]; then
97 | cat "${KUBECTX}"
98 | fi
99 | }
100 |
101 | save_context() {
102 | local saved
103 | saved="$(read_context)"
104 |
105 | if [[ "${saved}" != "${1}" ]]; then
106 | printf %s "${1}" > "${KUBECTX}"
107 | fi
108 | }
109 |
110 | switch_context() {
111 | $KUBECTL config use-context "${1}"
112 | }
113 |
114 | choose_context_interactive() {
115 | local choice
116 | choice="$(_KUBECTX_FORCE_COLOR=1 \
117 | FZF_DEFAULT_COMMAND="${SELF_CMD}" \
118 | fzf --ansi --no-preview || true)"
119 | if [[ -z "${choice}" ]]; then
120 | echo 2>&1 "error: you did not choose any of the options"
121 | exit 1
122 | else
123 | set_context "${choice}"
124 | fi
125 | }
126 |
127 | set_context() {
128 | local prev
129 | prev="$(current_context)" || exit_err "error getting current context"
130 |
131 | switch_context "${1}"
132 |
133 | if [[ "${prev}" != "${1}" ]]; then
134 | save_context "${prev}"
135 | fi
136 | }
137 |
138 | swap_context() {
139 | local ctx
140 | ctx="$(read_context)"
141 | if [[ -z "${ctx}" ]]; then
142 | echo "error: No previous context found." >&2
143 | exit 1
144 | fi
145 | set_context "${ctx}"
146 | }
147 |
148 | context_exists() {
149 | grep -q ^"${1}"\$ <($KUBECTL config get-contexts -o=name)
150 | }
151 |
152 | rename_context() {
153 | local old_name="${1}"
154 | local new_name="${2}"
155 |
156 | if [[ "${old_name}" == "." ]]; then
157 | old_name="$(current_context)"
158 | fi
159 |
160 | if ! context_exists "${old_name}"; then
161 | echo "error: Context \"${old_name}\" not found, can't rename it." >&2
162 | exit 1
163 | fi
164 |
165 | if context_exists "${new_name}"; then
166 | echo "Context \"${new_name}\" exists, deleting..." >&2
167 | $KUBECTL config delete-context "${new_name}" 1>/dev/null 2>&1
168 | fi
169 |
170 | $KUBECTL config rename-context "${old_name}" "${new_name}"
171 | }
172 |
173 | delete_contexts() {
174 | for i in "${@}"; do
175 | delete_context "${i}"
176 | done
177 | }
178 |
179 | delete_context() {
180 | local ctx
181 | ctx="${1}"
182 | if [[ "${ctx}" == "." ]]; then
183 | ctx="$(current_context)" || exit_err "error getting current context"
184 | fi
185 | echo "Deleting context \"${ctx}\"..." >&2
186 | $KUBECTL config delete-context "${ctx}"
187 | }
188 |
189 | unset_context() {
190 | echo "Unsetting current context." >&2
191 | $KUBECTL config unset current-context
192 | }
193 |
194 | main() {
195 | if [[ -z "${KUBECTL:-}" ]]; then
196 | if hash kubectl 2>/dev/null; then
197 | KUBECTL=kubectl
198 | elif hash kubectl.exe 2>/dev/null; then
199 | KUBECTL=kubectl.exe
200 | else
201 | echo >&2 "kubectl is not installed"
202 | exit 1
203 | fi
204 | fi
205 |
206 | if [[ "$#" -eq 0 ]]; then
207 | if [[ -t 1 && -z "${KUBECTX_IGNORE_FZF:-}" && "$(type fzf &>/dev/null; echo $?)" -eq 0 ]]; then
208 | choose_context_interactive
209 | else
210 | list_contexts
211 | fi
212 | elif [[ "${1}" == "-d" ]]; then
213 | if [[ "$#" -lt 2 ]]; then
214 | echo "error: missing context NAME" >&2
215 | usage
216 | exit 1
217 | fi
218 | delete_contexts "${@:2}"
219 | elif [[ "$#" -gt 1 ]]; then
220 | echo "error: too many arguments" >&2
221 | usage
222 | exit 1
223 | elif [[ "$#" -eq 1 ]]; then
224 | if [[ "${1}" == "-" ]]; then
225 | swap_context
226 | elif [[ "${1}" == '-c' || "${1}" == '--current' ]]; then
227 | # we don't call current_context here for two reasons:
228 | # - it does not fail when current-context property is not set
229 | # - it does not return a trailing newline
230 | $KUBECTL config current-context
231 | elif [[ "${1}" == '-u' || "${1}" == '--unset' ]]; then
232 | unset_context
233 | elif [[ "${1}" == '-h' || "${1}" == '--help' ]]; then
234 | usage
235 | elif [[ "${1}" =~ ^-(.*) ]]; then
236 | echo "error: unrecognized flag \"${1}\"" >&2
237 | usage
238 | exit 1
239 | elif [[ "${1}" =~ (.+)=(.+) ]]; then
240 | rename_context "${BASH_REMATCH[2]}" "${BASH_REMATCH[1]}"
241 | else
242 | set_context "${1}"
243 | fi
244 | else
245 | usage
246 | exit 1
247 | fi
248 | }
249 |
250 | main "$@"
251 |
--------------------------------------------------------------------------------
/kubens:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | #
3 | # kubens(1) is a utility to switch between Kubernetes namespaces.
4 |
5 | # Copyright 2017 Google Inc.
6 | #
7 | # Licensed under the Apache License, Version 2.0 (the "License");
8 | # you may not use this file except in compliance with the License.
9 | # You may obtain a copy of the License at
10 | #
11 | # http://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing, software
14 | # distributed under the License is distributed on an "AS IS" BASIS,
15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | # See the License for the specific language governing permissions and
17 | # limitations under the License.
18 |
19 | [[ -n $DEBUG ]] && set -x
20 |
21 | set -eou pipefail
22 | IFS=$'\n\t'
23 |
24 | SELF_CMD="$0"
25 |
26 | KUBENS_DIR="${XDG_CACHE_HOME:-$HOME/.kube}/kubens"
27 |
28 | usage() {
29 | local SELF
30 | SELF="kubens"
31 | if [[ "$(basename "$0")" == kubectl-* ]]; then # invoked as plugin
32 | SELF="kubectl ns"
33 | fi
34 |
35 | cat < : change the active namespace of current context
39 | $SELF - : switch to the previous namespace in this context
40 | $SELF -c, --current : show the current namespace
41 | $SELF -h,--help : show this message
42 | EOF
43 | }
44 |
45 | exit_err() {
46 | echo >&2 "${1}"
47 | exit 1
48 | }
49 |
50 | current_namespace() {
51 | local cur_ctx
52 |
53 | cur_ctx="$(current_context)" || exit_err "error getting current context"
54 | ns="$($KUBECTL config view -o=jsonpath="{.contexts[?(@.name==\"${cur_ctx}\")].context.namespace}")" \
55 | || exit_err "error getting current namespace"
56 |
57 | if [[ -z "${ns}" ]]; then
58 | echo "default"
59 | else
60 | echo "${ns}"
61 | fi
62 | }
63 |
64 | current_context() {
65 | $KUBECTL config current-context
66 | }
67 |
68 | get_namespaces() {
69 | $KUBECTL get namespaces -o=jsonpath='{range .items[*].metadata.name}{@}{"\n"}{end}'
70 | }
71 |
72 | escape_context_name() {
73 | echo "${1//\//-}"
74 | }
75 |
76 | namespace_file() {
77 | local ctx
78 |
79 | ctx="$(escape_context_name "${1}")"
80 | echo "${KUBENS_DIR}/${ctx}"
81 | }
82 |
83 | read_namespace() {
84 | local f
85 | f="$(namespace_file "${1}")"
86 | [[ -f "${f}" ]] && cat "${f}"
87 | return 0
88 | }
89 |
90 | save_namespace() {
91 | mkdir -p "${KUBENS_DIR}"
92 | local f saved
93 | f="$(namespace_file "${1}")"
94 | saved="$(read_namespace "${1}")"
95 |
96 | if [[ "${saved}" != "${2}" ]]; then
97 | printf %s "${2}" > "${f}"
98 | fi
99 | }
100 |
101 | switch_namespace() {
102 | local ctx="${1}"
103 | $KUBECTL config set-context "${ctx}" --namespace="${2}"
104 | echo "Active namespace is \"${2}\".">&2
105 | }
106 |
107 | choose_namespace_interactive() {
108 | # directly calling kubens via fzf might fail with a cryptic error like
109 | # "$FZF_DEFAULT_COMMAND failed", so try to see if we can list namespaces
110 | # locally first
111 | if [[ -z "$(list_namespaces)" ]]; then
112 | echo >&2 "error: could not list namespaces (is the cluster accessible?)"
113 | exit 1
114 | fi
115 |
116 | local choice
117 | choice="$(_KUBECTX_FORCE_COLOR=1 \
118 | FZF_DEFAULT_COMMAND="${SELF_CMD}" \
119 | fzf --ansi --no-preview || true)"
120 | if [[ -z "${choice}" ]]; then
121 | echo 2>&1 "error: you did not choose any of the options"
122 | exit 1
123 | else
124 | set_namespace "${choice}"
125 | fi
126 | }
127 |
128 | set_namespace() {
129 | local ctx prev
130 | ctx="$(current_context)" || exit_err "error getting current context"
131 | prev="$(current_namespace)" || exit_error "error getting current namespace"
132 |
133 | if grep -q ^"${1}"\$ <(get_namespaces); then
134 | switch_namespace "${ctx}" "${1}"
135 |
136 | if [[ "${prev}" != "${1}" ]]; then
137 | save_namespace "${ctx}" "${prev}"
138 | fi
139 | else
140 | echo "error: no namespace exists with name \"${1}\".">&2
141 | exit 1
142 | fi
143 | }
144 |
145 | list_namespaces() {
146 | local yellow darkbg normal
147 | yellow=$(tput setaf 3 || true)
148 | darkbg=$(tput setab 0 || true)
149 | normal=$(tput sgr0 || true)
150 |
151 | local cur_ctx_fg cur_ctx_bg
152 | cur_ctx_fg=${KUBECTX_CURRENT_FGCOLOR:-$yellow}
153 | cur_ctx_bg=${KUBECTX_CURRENT_BGCOLOR:-$darkbg}
154 |
155 | local cur ns_list
156 | cur="$(current_namespace)" || exit_err "error getting current namespace"
157 | ns_list=$(get_namespaces) || exit_err "error getting namespace list"
158 |
159 | for c in $ns_list; do
160 | if [[ -n "${_KUBECTX_FORCE_COLOR:-}" || \
161 | -t 1 && -z "${NO_COLOR:-}" ]]; then
162 | # colored output mode
163 | if [[ "${c}" = "${cur}" ]]; then
164 | echo "${cur_ctx_bg}${cur_ctx_fg}${c}${normal}"
165 | else
166 | echo "${c}"
167 | fi
168 | else
169 | echo "${c}"
170 | fi
171 | done
172 | }
173 |
174 | swap_namespace() {
175 | local ctx ns
176 | ctx="$(current_context)" || exit_err "error getting current context"
177 | ns="$(read_namespace "${ctx}")"
178 | if [[ -z "${ns}" ]]; then
179 | echo "error: No previous namespace found for current context." >&2
180 | exit 1
181 | fi
182 | set_namespace "${ns}"
183 | }
184 |
185 | main() {
186 | if [[ -z "${KUBECTL:-}" ]]; then
187 | if hash kubectl 2>/dev/null; then
188 | KUBECTL=kubectl
189 | elif hash kubectl.exe 2>/dev/null; then
190 | KUBECTL=kubectl.exe
191 | else
192 | echo >&2 "kubectl is not installed"
193 | exit 1
194 | fi
195 | fi
196 |
197 | if [[ "$#" -eq 0 ]]; then
198 | if [[ -t 1 && -z ${KUBECTX_IGNORE_FZF:-} && "$(type fzf &>/dev/null; echo $?)" -eq 0 ]]; then
199 | choose_namespace_interactive
200 | else
201 | list_namespaces
202 | fi
203 | elif [[ "$#" -eq 1 ]]; then
204 | if [[ "${1}" == '-h' || "${1}" == '--help' ]]; then
205 | usage
206 | elif [[ "${1}" == "-" ]]; then
207 | swap_namespace
208 | elif [[ "${1}" == '-c' || "${1}" == '--current' ]]; then
209 | current_namespace
210 | elif [[ "${1}" =~ ^-(.*) ]]; then
211 | echo "error: unrecognized flag \"${1}\"" >&2
212 | usage
213 | exit 1
214 | elif [[ "${1}" =~ (.+)=(.+) ]]; then
215 | alias_context "${BASH_REMATCH[2]}" "${BASH_REMATCH[1]}"
216 | else
217 | set_namespace "${1}"
218 | fi
219 | else
220 | echo "error: too many flags" >&2
221 | usage
222 | exit 1
223 | fi
224 | }
225 |
226 | main "$@"
227 |
--------------------------------------------------------------------------------
/test/common.bash:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bats
2 |
3 | # bats setup function
4 | setup() {
5 | TEMP_HOME="$(mktemp -d)"
6 | export TEMP_HOME
7 | export HOME=$TEMP_HOME
8 | export KUBECONFIG="${TEMP_HOME}/config"
9 | }
10 |
11 | # bats teardown function
12 | teardown() {
13 | rm -rf "$TEMP_HOME"
14 | }
15 |
16 | use_config() {
17 | cp "$BATS_TEST_DIRNAME/testdata/$1" $KUBECONFIG
18 | }
19 |
20 | # wrappers around "kubectl config" command
21 |
22 | get_namespace() {
23 | kubectl config view -o=jsonpath="{.contexts[?(@.name==\"$(get_context)\")].context.namespace}"
24 | }
25 |
26 | get_context() {
27 | kubectl config current-context
28 | }
29 |
30 | switch_context() {
31 | kubectl config use-context "${1}"
32 | }
33 |
--------------------------------------------------------------------------------
/test/kubectx.bats:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bats
2 |
3 | COMMAND="${COMMAND:-$BATS_TEST_DIRNAME/../kubectx}"
4 |
5 | load common
6 |
7 | @test "--help should not fail" {
8 | run ${COMMAND} --help
9 | echo "$output"
10 | [ "$status" -eq 0 ]
11 | }
12 |
13 | @test "-h should not fail" {
14 | run ${COMMAND} -h
15 | echo "$output"
16 | [ "$status" -eq 0 ]
17 | }
18 |
19 | @test "switch to previous context when no one exists" {
20 | use_config config1
21 |
22 | run ${COMMAND} -
23 | echo "$output"
24 | [ "$status" -eq 1 ]
25 | [[ $output = *"no previous context found" ]]
26 | }
27 |
28 | @test "list contexts when no kubeconfig exists" {
29 | run ${COMMAND}
30 | echo "$output"
31 | [ "$status" -eq 0 ]
32 | [[ "$output" = "warning: kubeconfig file not found" ]]
33 | }
34 |
35 | @test "get one context and list contexts" {
36 | use_config config1
37 |
38 | run ${COMMAND}
39 | echo "$output"
40 | [ "$status" -eq 0 ]
41 | [[ "$output" = "user1@cluster1" ]]
42 | }
43 |
44 | @test "get two contexts and list contexts" {
45 | use_config config2
46 |
47 | run ${COMMAND}
48 | echo "$output"
49 | [ "$status" -eq 0 ]
50 | [[ "$output" = *"user1@cluster1"* ]]
51 | [[ "$output" = *"user2@cluster1"* ]]
52 | }
53 |
54 | @test "get two contexts and select contexts" {
55 | use_config config2
56 |
57 | run ${COMMAND} user1@cluster1
58 | echo "$output"
59 | [ "$status" -eq 0 ]
60 | echo "$(get_context)"
61 | [[ "$(get_context)" = "user1@cluster1" ]]
62 |
63 | run ${COMMAND} user2@cluster1
64 | echo "$output"
65 | [ "$status" -eq 0 ]
66 | echo "$(get_context)"
67 | [[ "$(get_context)" = "user2@cluster1" ]]
68 | }
69 |
70 | @test "get two contexts and switch between contexts" {
71 | use_config config2
72 |
73 | run ${COMMAND} user1@cluster1
74 | echo "$output"
75 | [ "$status" -eq 0 ]
76 | echo "$(get_context)"
77 | [[ "$(get_context)" = "user1@cluster1" ]]
78 |
79 | run ${COMMAND} user2@cluster1
80 | echo "$output"
81 | [ "$status" -eq 0 ]
82 | echo "$(get_context)"
83 | [[ "$(get_context)" = "user2@cluster1" ]]
84 |
85 | run ${COMMAND} -
86 | echo "$output"
87 | [ "$status" -eq 0 ]
88 | echo "$(get_context)"
89 | [[ "$(get_context)" = "user1@cluster1" ]]
90 |
91 | run ${COMMAND} -
92 | echo "$output"
93 | [ "$status" -eq 0 ]
94 | echo "$(get_context)"
95 | [[ "$(get_context)" = "user2@cluster1" ]]
96 | }
97 |
98 | @test "get one context and switch to non existent context" {
99 | use_config config1
100 |
101 | run ${COMMAND} "unknown-context"
102 | echo "$output"
103 | [ "$status" -eq 1 ]
104 | }
105 |
106 | @test "-c/--current fails when no context set" {
107 | use_config config1
108 |
109 | run "${COMMAND}" -c
110 | echo "$output"
111 | [ $status -eq 1 ]
112 | run "${COMMAND}" --current
113 | echo "$output"
114 | [ $status -eq 1 ]
115 | }
116 |
117 | @test "-c/--current prints the current context" {
118 | use_config config1
119 |
120 | run "${COMMAND}" user1@cluster1
121 | [ $status -eq 0 ]
122 |
123 | run "${COMMAND}" -c
124 | echo "$output"
125 | [ $status -eq 0 ]
126 | [[ "$output" = "user1@cluster1" ]]
127 | run "${COMMAND}" --current
128 | echo "$output"
129 | [ $status -eq 0 ]
130 | [[ "$output" = "user1@cluster1" ]]
131 | }
132 |
133 | @test "rename context" {
134 | use_config config2
135 |
136 | run ${COMMAND} "new-context=user1@cluster1"
137 | echo "$output"
138 | [ "$status" -eq 0 ]
139 |
140 | run ${COMMAND}
141 | echo "$output"
142 | [ "$status" -eq 0 ]
143 | [[ ! "$output" = *"user1@cluster1"* ]]
144 | [[ "$output" = *"new-context"* ]]
145 | [[ "$output" = *"user2@cluster1"* ]]
146 | }
147 |
148 | @test "rename current context" {
149 | use_config config2
150 |
151 | run ${COMMAND} user2@cluster1
152 | echo "$output"
153 | [ "$status" -eq 0 ]
154 |
155 | run ${COMMAND} new-context=.
156 | echo "$output"
157 | [ "$status" -eq 0 ]
158 |
159 | run ${COMMAND}
160 | echo "$output"
161 | [ "$status" -eq 0 ]
162 | [[ ! "$output" = *"user2@cluster1"* ]]
163 | [[ "$output" = *"user1@cluster1"* ]]
164 | [[ "$output" = *"new-context"* ]]
165 | }
166 |
167 | @test "delete context" {
168 | use_config config2
169 |
170 | run ${COMMAND} -d "user1@cluster1"
171 | echo "$output"
172 | [ "$status" -eq 0 ]
173 |
174 | run ${COMMAND}
175 | echo "$output"
176 | [ "$status" -eq 0 ]
177 | [[ ! "$output" = "user1@cluster1" ]]
178 | [[ "$output" = "user2@cluster1" ]]
179 | }
180 |
181 | @test "delete current context" {
182 | use_config config2
183 |
184 | run ${COMMAND} user2@cluster1
185 | echo "$output"
186 | [ "$status" -eq 0 ]
187 |
188 | run ${COMMAND} -d .
189 | echo "$output"
190 | [ "$status" -eq 0 ]
191 |
192 | run ${COMMAND}
193 | echo "$output"
194 | [ "$status" -eq 0 ]
195 | [[ ! "$output" = "user2@cluster1" ]]
196 | [[ "$output" = "user1@cluster1" ]]
197 | }
198 |
199 | @test "delete non existent context" {
200 | use_config config1
201 |
202 | run ${COMMAND} -d "unknown-context"
203 | echo "$output"
204 | [ "$status" -eq 1 ]
205 | }
206 |
207 | @test "delete several contexts" {
208 | use_config config2
209 |
210 | run ${COMMAND} -d "user1@cluster1" "user2@cluster1"
211 | echo "$output"
212 | [ "$status" -eq 0 ]
213 |
214 | run ${COMMAND}
215 | echo "$output"
216 | [ "$status" -eq 0 ]
217 | [[ "$output" = "" ]]
218 | }
219 |
220 | @test "delete several contexts including a non existent one" {
221 | use_config config2
222 |
223 | run ${COMMAND} -d "user1@cluster1" "non-existent" "user2@cluster1"
224 | echo "$output"
225 | [ "$status" -eq 1 ]
226 |
227 | run ${COMMAND}
228 | echo "$output"
229 | [ "$status" -eq 0 ]
230 | [[ "$output" = "user2@cluster1" ]]
231 | }
232 |
233 | @test "unset selected context" {
234 | use_config config2
235 |
236 | run ${COMMAND} user1@cluster1
237 | [ "$status" -eq 0 ]
238 |
239 | run ${COMMAND} -u
240 | [ "$status" -eq 0 ]
241 |
242 | run ${COMMAND} -c
243 | [ "$status" -ne 0 ]
244 | }
245 |
--------------------------------------------------------------------------------
/test/kubens.bats:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bats
2 |
3 | COMMAND="${COMMAND:-$BATS_TEST_DIRNAME/../kubens}"
4 |
5 | # TODO(ahmetb) remove this after bash implementations are deleted
6 | export KUBECTL="$BATS_TEST_DIRNAME/../test/mock-kubectl"
7 |
8 | # short-circuit namespace querying in kubens go implementation
9 | export _MOCK_NAMESPACES=1
10 |
11 | load common
12 |
13 | @test "--help should not fail" {
14 | run ${COMMAND} --help
15 | echo "$output">&2
16 | [[ "$status" -eq 0 ]]
17 | }
18 |
19 | @test "-h should not fail" {
20 | run ${COMMAND} -h
21 | echo "$output">&2
22 | [[ "$status" -eq 0 ]]
23 | }
24 |
25 | @test "list namespaces when no kubeconfig exists" {
26 | run ${COMMAND}
27 | echo "$output"
28 | [[ "$status" -eq 1 ]]
29 | }
30 |
31 | @test "list namespaces" {
32 | use_config config1
33 | switch_context user1@cluster1
34 |
35 | run ${COMMAND}
36 | echo "$output"
37 | [[ "$status" -eq 0 ]]
38 | [[ "$output" = *"ns1"* ]]
39 | [[ "$output" = *"ns2"* ]]
40 | }
41 |
42 | @test "switch to existing namespace" {
43 | use_config config1
44 | switch_context user1@cluster1
45 |
46 | run ${COMMAND} "ns1"
47 | echo "$output"
48 | [[ "$status" -eq 0 ]]
49 | [[ "$output" = *'Active namespace is "ns1"'* ]]
50 | }
51 |
52 | @test "switch to non-existing namespace" {
53 | use_config config1
54 | switch_context user1@cluster1
55 |
56 | run ${COMMAND} "unknown-namespace"
57 | echo "$output"
58 | [[ "$status" -eq 1 ]]
59 | [[ "$output" = *'no namespace exists with name "unknown-namespace"'* ]]
60 | }
61 |
62 | @test "switch between namespaces" {
63 | use_config config1
64 | switch_context user1@cluster1
65 |
66 | run ${COMMAND} ns1
67 | echo "$output"
68 | [[ "$status" -eq 0 ]]
69 | echo "$(get_namespace)"
70 | [[ "$(get_namespace)" = "ns1" ]]
71 |
72 | run ${COMMAND} ns2
73 | echo "$output"
74 | [[ "$status" -eq 0 ]]
75 | echo "$(get_namespace)"
76 | [[ "$(get_namespace)" = "ns2" ]]
77 |
78 | run ${COMMAND} -
79 | echo "$output"
80 | [[ "$status" -eq 0 ]]
81 | echo "$(get_namespace)"
82 | [[ "$(get_namespace)" = "ns1" ]]
83 |
84 | run ${COMMAND} -
85 | echo "$output"
86 | [[ "$status" -eq 0 ]]
87 | echo "$(get_namespace)"
88 | [[ "$(get_namespace)" = "ns2" ]]
89 | }
90 |
91 | @test "switch to previous namespace when none exists" {
92 | use_config config1
93 | switch_context user1@cluster1
94 |
95 | run ${COMMAND} -
96 | echo "$output"
97 | [[ "$status" -eq 1 ]]
98 | [[ "$output" = *"No previous namespace found for current context"* ]]
99 | }
100 |
101 | @test "switch to namespace when current context is empty" {
102 | use_config config1
103 |
104 | run ${COMMAND} -
105 | echo "$output"
106 | [[ "$status" -eq 1 ]]
107 | [[ "$output" = *"current-context is not set"* ]]
108 | }
109 |
110 | @test "-c/--current works when no namespace is set on context" {
111 | use_config config1
112 | switch_context user1@cluster1
113 |
114 | run ${COMMAND} "-c"
115 | echo "$output"
116 | [[ "$status" -eq 0 ]]
117 | [[ "$output" = "default" ]]
118 | run ${COMMAND} "--current"
119 | echo "$output"
120 | [[ "$status" -eq 0 ]]
121 | [[ "$output" = "default" ]]
122 | }
123 |
124 | @test "-c/--current prints the namespace after it is set" {
125 | use_config config1
126 | switch_context user1@cluster1
127 | ${COMMAND} ns1
128 |
129 | run ${COMMAND} "-c"
130 | echo "$output"
131 | [[ "$status" -eq 0 ]]
132 | [[ "$output" = "ns1" ]]
133 | run ${COMMAND} "--current"
134 | echo "$output"
135 | [[ "$status" -eq 0 ]]
136 | [[ "$output" = "ns1" ]]
137 | }
138 |
139 | @test "-c/--current fails when current context is not set" {
140 | use_config config1
141 | run ${COMMAND} -c
142 | echo "$output"
143 | [[ "$status" -eq 1 ]]
144 |
145 | run ${COMMAND} --current
146 | echo "$output"
147 | [[ "$status" -eq 1 ]]
148 | }
149 |
--------------------------------------------------------------------------------
/test/mock-kubectl:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | [[ -n $DEBUG ]] && set -x
4 |
5 | set -eou pipefail
6 |
7 | if [[ $@ == *'get namespaces'* ]]; then
8 | echo "ns1"
9 | echo "ns2"
10 | else
11 | kubectl $@
12 | fi
13 |
--------------------------------------------------------------------------------
/test/testdata/config1:
--------------------------------------------------------------------------------
1 | # config with one context
2 |
3 | apiVersion: v1
4 | clusters:
5 | - cluster:
6 | server: ""
7 | name: cluster1
8 | contexts:
9 | - context:
10 | cluster: cluster1
11 | user: user1
12 | name: user1@cluster1
13 | current-context: ""
14 | kind: Config
15 | preferences: {}
16 | users:
17 | - name: user1
18 | user: {}
19 |
--------------------------------------------------------------------------------
/test/testdata/config2:
--------------------------------------------------------------------------------
1 | # config with two contexts
2 |
3 | apiVersion: v1
4 | clusters:
5 | - cluster:
6 | server: ""
7 | name: cluster1
8 | contexts:
9 | - context:
10 | cluster: cluster1
11 | user: user1
12 | name: user1@cluster1
13 | - context:
14 | cluster: cluster1
15 | user: user2
16 | name: user2@cluster1
17 | current-context: ""
18 | kind: Config
19 | preferences: {}
20 | users:
21 | - name: user1
22 | user: {}
23 | - name: user2
24 | user: {}
25 |
--------------------------------------------------------------------------------