├── .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 | ![Latest GitHub release](https://img.shields.io/github/release/ahmetb/kubectx.svg) 4 | ![GitHub stars](https://img.shields.io/github/stars/ahmetb/kubectx.svg?label=github%20stars) 5 | ![Homebrew downloads](https://img.shields.io/homebrew/installs/dy/kubectx?label=macOS%20installs) 6 | [![Go implementation (CI)](https://github.com/ahmetb/kubectx/workflows/Go%20implementation%20(CI)/badge.svg)](https://github.com/ahmetb/kubectx/actions?query=workflow%3A"Go+implementation+(CI)") 7 | ![Proudly written in Bash](https://img.shields.io/badge/written%20in-bash-ff69b4.svg) 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 | ![kubectx demo GIF](img/kubectx-demo.gif) 21 | 22 | ...and here's a **`kubens`** demo: 23 | ![kubens demo GIF](img/kubens-demo.gif) 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 | ![kubectx interactive search with fzf](img/kubectx-interactive.gif) 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 | [![Stargazers over time](https://starchart.cc/ahmetb/kubectx.svg)](https://starchart.cc/ahmetb/kubectx) 311 | ![Google Analytics](https://ga-beacon.appspot.com/UA-2609286-17/kubectx/README?pixel) 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 | --------------------------------------------------------------------------------