Repository: ahmetb/kubectx Branch: master Commit: f500964e2415 Files: 82 Total size: 171.1 KB Directory structure: gitextract_5i38l93y/ ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── bash-frozen.yml │ ├── ci.yml │ ├── dependabot.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 │ │ ├── isolated_shell_guard.go │ │ ├── list.go │ │ ├── main.go │ │ ├── rename.go │ │ ├── rename_test.go │ │ ├── shell.go │ │ ├── shell_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 │ ├── unset.go │ └── version.go ├── completion/ │ ├── _kubectx.zsh │ ├── _kubens.zsh │ ├── kubectx.bash │ ├── kubectx.fish │ ├── kubens.bash │ └── kubens.fish ├── go.mod ├── go.sum ├── 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 ├── kubectx ├── kubens └── test/ ├── common.bash ├── kubectx.bats ├── kubens.bats ├── mock-kubectl └── testdata/ ├── config1 └── config2 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: github-actions directory: / schedule: interval: weekly commit-message: prefix: chore include: scope - package-ecosystem: gomod directory: / schedule: interval: weekly commit-message: prefix: chore include: scope groups: kubernetes: patterns: - "k8s.io/*" ================================================ FILE: .github/workflows/bash-frozen.yml ================================================ name: Bash scripts frozen on: pull_request: paths: - 'kubectx' - 'kubens' jobs: comment: runs-on: ubuntu-latest permissions: pull-requests: write steps: - name: Comment on PR if author is not ahmetb if: github.event.pull_request.user.login != 'ahmetb' uses: actions/github-script@v7 with: script: | const body = [ '> [!WARNING]', '> **This PR will not be merged.**', '>', '> The bash implementation of `kubectx` and `kubens` is **frozen** and is provided only for convenience.', '> We are not accepting any improvements to the bash scripts.', '>', '> Please propose your improvements to the **Go implementation** instead.', ].join('\n'); await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: body }); ================================================ FILE: .github/workflows/ci.yml ================================================ # Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. name: Go implementation (CI) on: push: pull_request: jobs: ci: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@master - name: Setup Go uses: actions/setup-go@v6 with: go-version: '1.25' - id: go-cache-paths run: | echo "::set-output name=go-build::$(go env GOCACHE)" echo "::set-output name=go-mod::$(go env GOMODCACHE)" - name: Go Build Cache uses: actions/cache@v5 with: path: ${{ steps.go-cache-paths.outputs.go-build }} key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} - name: Go Mod Cache uses: actions/cache@v5 with: path: ${{ steps.go-cache-paths.outputs.go-mod }} key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} - name: Ensure gofmt run: test -z "$(gofmt -s -d .)" - name: Ensure go.mod is already tidied run: go mod tidy && git diff --exit-code - name: Run unit tests run: go test ./... - name: Build with Goreleaser uses: goreleaser/goreleaser-action@v7 with: version: latest args: release --snapshot --skip publish,snapcraft --clean - name: Setup BATS framework run: sudo npm install -g bats - name: kubectx (Go) integration tests run: COMMAND=./dist/kubectx_linux_amd64_v1/kubectx bats test/kubectx.bats - name: kubens (Go) integration tests run: COMMAND=./dist/kubens_linux_amd64_v1/kubens bats test/kubens.bats ================================================ FILE: .github/workflows/dependabot.yml ================================================ name: Dependabot on: pull_request: permissions: contents: write pull-requests: write jobs: auto-merge: runs-on: ubuntu-latest if: ${{ github.actor == 'dependabot[bot]' }} steps: - name: Dependabot metadata id: metadata uses: dependabot/fetch-metadata@v2 - name: Enable auto-merge for Dependabot PRs if: ${{ steps.metadata.outputs.update-type != 'version-update:semver-major' }} run: gh pr merge --auto --merge "$PR_URL" env: PR_URL: ${{ github.event.pull_request.html_url }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/release.yml ================================================ # Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. name: Release on: push: tags: - 'v*.*.*' jobs: goreleaser: permissions: contents: write runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - run: git fetch --tags - name: Setup Go uses: actions/setup-go@v6 with: go-version: '1.25' - name: Install Snapcraft uses: samuelmeuli/action-snapcraft@v3 - name: Setup Snapcraft run: | # https://github.com/goreleaser/goreleaser/issues/1715 mkdir -p $HOME/.cache/snapcraft/download mkdir -p $HOME/.cache/snapcraft/stage-packages - name: GoReleaser uses: goreleaser/goreleaser-action@v7 with: version: latest args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Update new version for plugin 'ctx' in krew-index uses: rajatjindal/krew-release-bot@v0.0.51 with: krew_template_file: .krew/ctx.yaml - name: Update new version for plugin 'ns' in krew-index uses: rajatjindal/krew-release-bot@v0.0.51 with: krew_template_file: .krew/ns.yaml - name: Publish Snaps to the Snap Store (stable channel) run: for snap in $(ls dist/*.snap); do snapcraft upload --release=stable $snap; done env: SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }} ================================================ FILE: .goreleaser.yml ================================================ # yaml-language-server: $schema=https://goreleaser.com/static/schema.json # Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # This is an example goreleaser.yaml file with some sane defaults. # Make sure to check the documentation at https://goreleaser.com version: 2 before: hooks: - go mod download builds: - id: kubectx main: ./cmd/kubectx binary: kubectx env: - CGO_ENABLED=0 goos: - linux - darwin - windows goarch: - amd64 - arm - arm64 - ppc64le - s390x goarm: [6, 7] - id: kubens main: ./cmd/kubens binary: kubens env: - CGO_ENABLED=0 goos: - linux - darwin - windows goarch: - amd64 - arm - arm64 - ppc64le - s390x goarm: [6, 7] archives: - id: kubectx-archive name_template: |- kubectx_{{ .Tag }}_{{ .Os }}_ {{- with .Arch -}} {{- if (eq . "386") -}}i386 {{- else if (eq . "amd64") -}}x86_64 {{- else -}}{{- . -}} {{- end -}} {{ end }} {{- with .Arm -}} {{- if (eq . "6") -}}hf {{- else -}}v{{- . -}} {{- end -}} {{- end -}} ids: - kubectx format_overrides: - goos: windows formats: [zip] files: ["LICENSE"] - id: kubens-archive name_template: |- kubens_{{ .Tag }}_{{ .Os }}_ {{- with .Arch -}} {{- if (eq . "386") -}}i386 {{- else if (eq . "amd64") -}}x86_64 {{- else -}}{{- . -}} {{- end -}} {{ end }} {{- with .Arm -}} {{- if (eq . "6") -}}hf {{- else -}}v{{- . -}} {{- end -}} {{- end -}} ids: - kubens format_overrides: - goos: windows formats: [zip] files: ["LICENSE"] checksum: name_template: "checksums.txt" algorithm: sha256 release: extra_files: - glob: ./kubens - glob: ./kubectx snapcrafts: - id: kubectx name: kubectx summary: 'kubectx + kubens: Power tools for kubectl' description: | kubectx is a tool to switch between contexts (clusters) on kubectl faster. kubens is a tool to switch between Kubernetes namespaces (and configure them for kubectl) easily. grade: stable confinement: classic base: core24 apps: kubectx: command: kubectx completer: completion/kubectx.bash kubens: command: kubens completer: completion/kubens.bash ================================================ FILE: .krew/ctx.yaml ================================================ apiVersion: krew.googlecontainertools.github.com/v1alpha2 kind: Plugin metadata: name: ctx spec: homepage: https://github.com/ahmetb/kubectx shortDescription: Switch between contexts in your kubeconfig version: {{ .TagName }} description: | Also known as "kubectx", a utility to switch between context entries in your kubeconfig file efficiently. caveats: | If fzf is installed on your machine, you can interactively choose between the entries using the arrow keys, or by fuzzy searching as you type. See https://github.com/ahmetb/kubectx for customization and details. platforms: - selector: matchExpressions: - key: os operator: In values: - darwin - linux {{addURIAndSha "https://github.com/ahmetb/kubectx/archive/{{ .TagName }}.tar.gz" .TagName }} bin: kubectx files: - from: kubectx-*/kubectx to: . - from: kubectx-*/LICENSE to: . ================================================ FILE: .krew/ns.yaml ================================================ apiVersion: krew.googlecontainertools.github.com/v1alpha2 kind: Plugin metadata: name: ns spec: homepage: https://github.com/ahmetb/kubectx shortDescription: Switch between Kubernetes namespaces version: {{ .TagName }} description: | Also known as "kubens", a utility to set your current namespace and switch between them. caveats: | If fzf is installed on your machine, you can interactively choose between the entries using the arrow keys, or by fuzzy searching as you type. platforms: - selector: matchExpressions: - key: os operator: In values: - darwin - linux {{addURIAndSha "https://github.com/ahmetb/kubectx/archive/{{ .TagName }}.tar.gz" .TagName }} bin: kubens files: - from: kubectx-*/kubens to: . - from: kubectx-*/LICENSE to: . ================================================ FILE: CONTRIBUTING.md ================================================ # How to contribute We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow. ## Contributor License Agreement Contributions to this project must be accompanied by a Contributor License Agreement. You (or your employer) retain the copyright to your contribution, this simply gives us permission to use and redistribute your contributions as part of the project. Head over to to see your current agreements on file or to sign a new one. You generally only need to submit a CLA once, so if you've already submitted one (even if it was for a different project), you probably don't need to do it again. ## Code reviews All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # `kubectx` + `kubens`: Power tools for kubectl ![Latest GitHub release](https://img.shields.io/github/release/ahmetb/kubectx.svg) ![GitHub stars](https://img.shields.io/github/stars/ahmetb/kubectx.svg?label=github%20stars) ![Homebrew downloads](https://img.shields.io/homebrew/installs/dy/kubectx?label=macOS%20installs) [![Go implementation (CI)](https://github.com/ahmetb/kubectx/workflows/Go%20implementation%20(CI)/badge.svg)](https://github.com/ahmetb/kubectx/actions?query=workflow%3A"Go+implementation+(CI)") ![Proudly written in Bash](https://img.shields.io/badge/written%20in-bash-ff69b4.svg) This repository provides both `kubectx` and `kubens` tools. [Install →](#installation) ## What are `kubectx` and `kubens`? **kubectx** is a tool to switch between contexts (clusters) on kubectl faster.
**kubens** is a tool to switch between Kubernetes namespaces (and configure them for kubectl) easily. Here's a **`kubectx`** demo: ![kubectx demo GIF](img/kubectx-demo.gif) ...and here's a **`kubens`** demo: ![kubens demo GIF](img/kubens-demo.gif) ### Examples ```sh # switch to another cluster that's in kubeconfig $ kubectx minikube Switched to context "minikube". # switch back to previous cluster $ kubectx - Switched to context "oregon". # start an "isolated shell" that only has a single context $ kubectx -s minikube # rename context $ kubectx dublin=gke_ahmetb_europe-west1-b_dublin Context "gke_ahmetb_europe-west1-b_dublin" renamed to "dublin". # change the active namespace on kubectl $ kubens kube-system Context "test" set. Active namespace is "kube-system". # go back to the previous namespace $ kubens - Context "test" set. Active namespace is "default". # change the active namespace even if it doesn't exist $ kubens not-found-namespace --force Context "test" set. Active namespace is "not-found-namespace". --- $ kubens not-found-namespace -f Context "test" set. Active namespace is "not-found-namespace". ``` If you have [`fzf`](https://github.com/junegunn/fzf) installed, you can also **interactively** select a context or cluster, or fuzzy-search by typing a few characters. To learn more, read [interactive mode →](#interactive-mode) Both `kubectx` and `kubens` support Tab completion on bash/zsh/fish shells to help with long context names. You don't have to remember full context names anymore. ----- ## Installation | Package manager | Command | |---|---| | [Homebrew](https://brew.sh/) (macOS & Linux) | `brew install kubectx` | | [MacPorts](https://www.macports.org) (macOS) | `sudo port install kubectx` | | apt (Debian/Ubuntu) | `sudo apt install kubectx` | | pacman (Arch Linux) | `sudo pacman -S kubectx` | | [Chocolatey](https://chocolatey.org/) (Windows) | `choco install kubens kubectx` | | [Scoop](https://scoop.sh/) (Windows) | `scoop bucket add main && scoop install main/kubens main/kubectx` | | [winget](https://learn.microsoft.com/en-us/windows/package-manager/) (Windows) | `winget install --id ahmetb.kubectx && winget install --id ahmetb.kubens` | | [Krew](https://github.com/kubernetes-sigs/krew/) (kubectl plugin) | `kubectl krew install ctx && kubectl krew install ns` | Alternatively, download binaries from the [**Releases page →**](https://github.com/ahmetb/kubectx/releases) and add them to somewhere in your `PATH`.
Shell completion scripts #### zsh (with [antibody](https://getantibody.github.io)) Add this line to your [Plugins File](https://getantibody.github.io/usage/) (e.g. `~/.zsh_plugins.txt`): ``` ahmetb/kubectx path:completion kind:fpath ``` Depending on your setup, you might or might not need to call `compinit` or `autoload -U compinit && compinit` in your `~/.zshrc` after you load the Plugins file. If you use [oh-my-zsh](https://github.com/ohmyzsh/ohmyzsh), load the completions before you load `oh-my-zsh` because `oh-my-zsh` will call `compinit`. #### zsh (plain) The completion scripts have to be in a path that belongs to `$fpath`. Either link or copy them to an existing folder. Example with [`oh-my-zsh`](https://github.com/ohmyzsh/ohmyzsh): ```bash mkdir -p ~/.oh-my-zsh/custom/completions chmod -R 755 ~/.oh-my-zsh/custom/completions ln -s /opt/kubectx/completion/_kubectx.zsh ~/.oh-my-zsh/custom/completions/_kubectx.zsh ln -s /opt/kubectx/completion/_kubens.zsh ~/.oh-my-zsh/custom/completions/_kubens.zsh echo "fpath=($ZSH/custom/completions $fpath)" >> ~/.zshrc ``` If completion doesn't work, add `autoload -U compinit && compinit` to your `.zshrc` (similar to [`zsh-completions`](https://github.com/zsh-users/zsh-completions/blob/master/README.md#oh-my-zsh)). If you are not using [`oh-my-zsh`](https://github.com/ohmyzsh/ohmyzsh), you could link to `/usr/share/zsh/functions/Completion` (might require sudo), depending on the `$fpath` of your zsh installation. In case of errors, calling `compaudit` might help. #### bash ```bash git clone https://github.com/ahmetb/kubectx.git ~/.kubectx COMPDIR=$(pkg-config --variable=completionsdir bash-completion) ln -sf ~/.kubectx/completion/kubens.bash $COMPDIR/kubens ln -sf ~/.kubectx/completion/kubectx.bash $COMPDIR/kubectx cat << EOF >> ~/.bashrc #kubectx and kubens export PATH=~/.kubectx:\$PATH EOF ``` #### fish ```fish mkdir -p ~/.config/fish/completions ln -s /opt/kubectx/completion/kubectx.fish ~/.config/fish/completions/ ln -s /opt/kubectx/completion/kubens.fish ~/.config/fish/completions/ ```
> [!NOTE] > Tip: Show context/namespace in your shell prompt with [oh-my-posh](https://ohmyposh.dev/) or > simply with [kube-ps1](https://github.com/jonmosco/kube-ps1). ----- ### Interactive mode If you want `kubectx` and `kubens` commands to present you an interactive menu with fuzzy searching, you just need to [install `fzf`](https://github.com/junegunn/fzf) in your `$PATH`. ![kubectx interactive search with fzf](img/kubectx-interactive.gif) Caveats: - If you have `fzf` installed, but want to opt out of using this feature, set the environment variable `KUBECTX_IGNORE_FZF=1`. - If you want to keep `fzf` interactive mode but need the default behavior of the command, you can do it by piping the output to another command (e.g. `kubectx | cat `). ----- ### Customizing colors If you like to customize the colors indicating the current namespace or context, set the environment variables `KUBECTX_CURRENT_FGCOLOR` and `KUBECTX_CURRENT_BGCOLOR` (refer color codes [here](https://linux.101hacks.com/ps1-examples/prompt-color-using-tput/)): ```sh export KUBECTX_CURRENT_FGCOLOR=$(tput setaf 6) # blue text export KUBECTX_CURRENT_BGCOLOR=$(tput setab 7) # white background ``` Colors in the output can be disabled by setting the [`NO_COLOR`](https://no-color.org/) environment variable. ----- If you liked `kubectx`, you may like my [`kubectl-aliases`](https://github.com/ahmetb/kubectl-aliases) project, too. I recommend pairing kubectx and kubens with [fzf](#interactive-mode) and [kube-ps1](https://github.com/jonmosco/kube-ps1). #### Stargazers over time [![Stargazers over time](https://starchart.cc/ahmetb/kubectx.svg)](https://starchart.cc/ahmetb/kubectx) ![Google Analytics](https://ga-beacon.appspot.com/UA-2609286-17/kubectx/README?pixel) ================================================ FILE: cmd/kubectx/current.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "errors" "fmt" "io" "github.com/ahmetb/kubectx/internal/kubeconfig" ) // CurrentOp prints the current context type CurrentOp struct{} func (_op CurrentOp) Run(stdout, _ io.Writer) error { if err := checkIsolatedMode(); err != nil { return err } kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return fmt.Errorf("kubeconfig error: %w", err) } v, err := kc.GetCurrentContext() if err != nil { return fmt.Errorf("failed to get current context: %w", err) } if v == "" { return errors.New("current-context is not set") } if _, err := fmt.Fprintln(stdout, v); err != nil { return fmt.Errorf("write error: %w", err) } return nil } ================================================ FILE: cmd/kubectx/delete.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "errors" "fmt" "io" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) // DeleteOp indicates intention to delete contexts. type DeleteOp struct { Contexts []string // NAME or '.' to indicate current-context. } // deleteContexts deletes context entries one by one. func (op DeleteOp) Run(_, stderr io.Writer) error { if err := checkIsolatedMode(); err != nil { return err } for _, ctx := range op.Contexts { // TODO inefficiency here. we open/write/close the same file many times. deletedName, wasActiveContext, err := deleteContext(ctx) if err != nil { return fmt.Errorf("error deleting context \"%s\": %w", deletedName, err) } if wasActiveContext { printer.Warning(stderr, "You deleted the current context. Use \"%s\" to select a new context.", selfName()) } _ = printer.Success(stderr, `Deleted context %s.`, printer.SuccessColor.Sprint(deletedName)) } return nil } // deleteContext deletes a context entry by NAME or current-context // indicated by ".". func deleteContext(name string) (deleteName string, wasActiveContext bool, err error) { kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return deleteName, false, fmt.Errorf("kubeconfig error: %w", err) } cur, err := kc.GetCurrentContext() if err != nil { return deleteName, false, fmt.Errorf("failed to get current context: %w", err) } // resolve "." to a real name if name == "." { if cur == "" { return deleteName, false, errors.New("can't use '.' as the no active context is set") } wasActiveContext = true name = cur } exists, err := kc.ContextExists(name) if err != nil { return name, false, fmt.Errorf("failed to check context: %w", err) } if !exists { return name, false, errors.New("context does not exist") } if err := kc.DeleteContextEntry(name); err != nil { return name, false, fmt.Errorf("failed to modify yaml doc: %w", err) } if err := kc.Save(); err != nil { return name, wasActiveContext, fmt.Errorf("failed to save modified kubeconfig file: %w", err) } return name, wasActiveContext, nil } ================================================ FILE: cmd/kubectx/env.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main ================================================ FILE: cmd/kubectx/flags.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "io" "os" "strings" "github.com/ahmetb/kubectx/internal/cmdutil" ) // UnsupportedOp indicates an unsupported flag. type UnsupportedOp struct{ Err error } func (op UnsupportedOp) Run(_, _ io.Writer) error { return op.Err } // parseArgs looks at flags (excl. executable name, i.e. argv[0]) // and decides which operation should be taken. func parseArgs(argv []string) Op { if len(argv) == 0 { if cmdutil.IsInteractiveMode(os.Stdout) { return InteractiveSwitchOp{SelfCmd: os.Args[0]} } return ListOp{} } if argv[0] == "--shell" || argv[0] == "-s" { if len(argv) != 2 { return UnsupportedOp{Err: fmt.Errorf("'%s' requires exactly one context name argument", argv[0])} } return ShellOp{Target: argv[1]} } if argv[0] == "-d" { if len(argv) == 1 { if cmdutil.IsInteractiveMode(os.Stdout) { return InteractiveDeleteOp{SelfCmd: os.Args[0]} } else { return UnsupportedOp{Err: fmt.Errorf("'-d' needs arguments")} } } return DeleteOp{Contexts: argv[1:]} } if len(argv) == 1 { v := argv[0] if v == "--help" || v == "-h" { return HelpOp{} } if v == "--version" || v == "-V" { return VersionOp{} } if v == "--current" || v == "-c" { return CurrentOp{} } if v == "--unset" || v == "-u" { return UnsetOp{} } if new, old, ok := parseRenameSyntax(v); ok { return RenameOp{New: new, Old: old} } if strings.HasPrefix(v, "-") && v != "-" { return UnsupportedOp{Err: fmt.Errorf("unsupported option '%s'", v)} } return SwitchOp{Target: argv[0]} } return UnsupportedOp{Err: fmt.Errorf("too many arguments")} } ================================================ FILE: cmd/kubectx/flags_test.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "testing" "github.com/google/go-cmp/cmp" ) func Test_parseArgs_new(t *testing.T) { tests := []struct { name string args []string want Op }{ {name: "nil Args", args: nil, want: ListOp{}}, {name: "empty Args", args: []string{}, want: ListOp{}}, {name: "help shorthand", args: []string{"-h"}, want: HelpOp{}}, {name: "help long form", args: []string{"--help"}, want: HelpOp{}}, {name: "current shorthand", args: []string{"-c"}, want: CurrentOp{}}, {name: "current long form", args: []string{"--current"}, want: CurrentOp{}}, {name: "unset shorthand", args: []string{"-u"}, want: UnsetOp{}}, {name: "unset long form", args: []string{"--unset"}, want: UnsetOp{}}, {name: "switch by name", args: []string{"foo"}, want: SwitchOp{Target: "foo"}}, {name: "switch by swap", args: []string{"-"}, want: SwitchOp{Target: "-"}}, {name: "delete - without contexts", args: []string{"-d"}, want: UnsupportedOp{fmt.Errorf("'-d' needs arguments")}}, {name: "delete - current context", args: []string{"-d", "."}, want: DeleteOp{[]string{"."}}}, {name: "delete - multiple contexts", args: []string{"-d", ".", "a", "b"}, want: DeleteOp{[]string{".", "a", "b"}}}, {name: "rename context", args: []string{"a=b"}, want: RenameOp{"a", "b"}}, {name: "rename context with old=current", args: []string{"a=."}, want: RenameOp{"a", "."}}, {name: "shell shorthand", args: []string{"-s", "prod"}, want: ShellOp{Target: "prod"}}, {name: "shell long form", args: []string{"--shell", "prod"}, want: ShellOp{Target: "prod"}}, {name: "shell without context name", args: []string{"-s"}, want: UnsupportedOp{Err: fmt.Errorf("'-s' requires exactly one context name argument")}}, {name: "shell with too many args", args: []string{"--shell", "a", "b"}, want: UnsupportedOp{Err: fmt.Errorf("'--shell' requires exactly one context name argument")}}, {name: "unrecognized flag", args: []string{"-x"}, want: UnsupportedOp{Err: fmt.Errorf("unsupported option '-x'")}}, {name: "too many args", args: []string{"a", "b", "c"}, want: UnsupportedOp{Err: fmt.Errorf("too many arguments")}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := parseArgs(tt.args) var opts cmp.Options if _, ok := tt.want.(UnsupportedOp); ok { opts = append(opts, cmp.Comparer(func(x, y UnsupportedOp) bool { return (x.Err == nil && y.Err == nil) || (x.Err.Error() == y.Err.Error()) })) } if diff := cmp.Diff(got, tt.want, opts...); diff != "" { t.Errorf("parseArgs(%#v) diff: %s", tt.args, diff) } }) } } ================================================ FILE: cmd/kubectx/fzf.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "bytes" "errors" "fmt" "io" "os" "os/exec" "strings" "github.com/ahmetb/kubectx/internal/cmdutil" "github.com/ahmetb/kubectx/internal/env" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) type InteractiveSwitchOp struct { SelfCmd string } type InteractiveDeleteOp struct { SelfCmd string } func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error { if err := checkIsolatedMode(); err != nil { return err } // parse kubeconfig just to see if it can be loaded kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { if cmdutil.IsNotFoundErr(err) { printer.Warning(stderr, "kubeconfig file not found") return nil } return fmt.Errorf("kubeconfig error: %w", err) } ctxNames, err := kc.ContextNames() if err != nil { return fmt.Errorf("failed to get context names: %w", err) } if len(ctxNames) == 0 { return errors.New("no contexts found in the kubeconfig file") } cmd := exec.Command("fzf", "--ansi", "--no-preview") var out bytes.Buffer cmd.Stdin = os.Stdin cmd.Stderr = stderr cmd.Stdout = &out cmd.Env = append(os.Environ(), fmt.Sprintf("FZF_DEFAULT_COMMAND=%s", op.SelfCmd), fmt.Sprintf("%s=1", env.EnvForceColor)) if err := cmd.Run(); err != nil { var exitErr *exec.ExitError if !errors.As(err, &exitErr) { return err } } choice := strings.TrimSpace(out.String()) if choice == "" { return errors.New("you did not choose any of the options") } name, err := switchContext(choice) if err != nil { return fmt.Errorf("failed to switch context: %w", err) } _ = printer.Success(stderr, "Switched to context \"%s\".", printer.SuccessColor.Sprint(name)) return nil } func (op InteractiveDeleteOp) Run(_, stderr io.Writer) error { if err := checkIsolatedMode(); err != nil { return err } // parse kubeconfig just to see if it can be loaded kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { if cmdutil.IsNotFoundErr(err) { printer.Warning(stderr, "kubeconfig file not found") return nil } return fmt.Errorf("kubeconfig error: %w", err) } ctxNames, err := kc.ContextNames() if err != nil { return fmt.Errorf("failed to get context names: %w", err) } if len(ctxNames) == 0 { return errors.New("no contexts found in config") } cmd := exec.Command("fzf", "--ansi", "--no-preview") var out bytes.Buffer cmd.Stdin = os.Stdin cmd.Stderr = stderr cmd.Stdout = &out cmd.Env = append(os.Environ(), fmt.Sprintf("FZF_DEFAULT_COMMAND=%s", op.SelfCmd), fmt.Sprintf("%s=1", env.EnvForceColor)) if err := cmd.Run(); err != nil { var exitErr *exec.ExitError if !errors.As(err, &exitErr) { return err } } choice := strings.TrimSpace(out.String()) if choice == "" { return errors.New("you did not choose any of the options") } name, wasActiveContext, err := deleteContext(choice) if err != nil { return fmt.Errorf("failed to delete context: %w", err) } if wasActiveContext { printer.Warning(stderr, "You deleted the current context. Use \"%s\" to select a new context.", selfName()) } _ = printer.Success(stderr, `Deleted context %s.`, printer.SuccessColor.Sprint(name)) return nil } ================================================ FILE: cmd/kubectx/help.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "io" "os" "path/filepath" "strings" ) // HelpOp describes printing help. type HelpOp struct{} func (_ HelpOp) Run(stdout, _ io.Writer) error { return printUsage(stdout) } func printUsage(out io.Writer) error { help := `USAGE: %PROG% : list the contexts %PROG% : switch to context %PROG% - : switch to the previous context %PROG% -c, --current : show the current context name %PROG% = : rename context to %PROG% =. : rename current-context to %PROG% -u, --unset : unset the current context %PROG% -d [] : delete context ('.' for current-context) %SPAC% (this command won't delete the user/cluster entry %SPAC% referenced by the context entry) %PROG% -s, --shell : start a shell scoped to context %PROG% -h,--help : show this message %PROG% -V,--version : show version` help = strings.ReplaceAll(help, "%PROG%", selfName()) help = strings.ReplaceAll(help, "%SPAC%", strings.Repeat(" ", len(selfName()))) _, err := fmt.Fprintf(out, "%s\n", help) if err != nil { return fmt.Errorf("write error: %w", err) } return nil } // selfName guesses how the user invoked the program. func selfName() string { me := filepath.Base(os.Args[0]) pluginPrefix := "kubectl-" if strings.HasPrefix(me, pluginPrefix) { return "kubectl " + strings.TrimPrefix(me, pluginPrefix) } return "kubectx" } ================================================ FILE: cmd/kubectx/help_test.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "bytes" "strings" "testing" ) func TestPrintHelp(t *testing.T) { var buf bytes.Buffer if err := (&HelpOp{}).Run(&buf, &buf); err != nil { t.Fatal(err) } out := buf.String() if !strings.Contains(out, "USAGE:") { t.Errorf("help string doesn't contain USAGE: ; output=\"%s\"", out) } if !strings.HasSuffix(out, "\n") { t.Errorf("does not end with New line; output=\"%s\"", out) } } ================================================ FILE: cmd/kubectx/isolated_shell_guard.go ================================================ package main import ( "fmt" "os" "github.com/ahmetb/kubectx/internal/env" "github.com/ahmetb/kubectx/internal/kubeconfig" ) func checkIsolatedMode() error { if os.Getenv(env.EnvIsolatedShell) != "1" { return nil } kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return fmt.Errorf("you are in a locked single-context shell, use 'exit' to leave") } cur, _ := kc.GetCurrentContext() return fmt.Errorf("you are in a locked single-context shell (\"%s\"), use 'exit' to leave", cur) } ================================================ FILE: cmd/kubectx/list.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "io" "facette.io/natsort" "github.com/ahmetb/kubectx/internal/cmdutil" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) // ListOp describes listing contexts. type ListOp struct{} func (_ ListOp) Run(stdout, stderr io.Writer) error { if err := checkIsolatedMode(); err != nil { return err } kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { if cmdutil.IsNotFoundErr(err) { printer.Warning(stderr, "kubeconfig file not found") return nil } return fmt.Errorf("kubeconfig error: %w", err) } ctxs, err := kc.ContextNames() if err != nil { return fmt.Errorf("failed to get context names: %w", err) } natsort.Sort(ctxs) cur, err := kc.GetCurrentContext() if err != nil { return fmt.Errorf("failed to get current context: %w", err) } for _, c := range ctxs { s := c if c == cur { s = printer.ActiveItemColor.Sprint(c) } fmt.Fprintf(stdout, "%s\n", s) } return nil } ================================================ FILE: cmd/kubectx/main.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "io" "os" "github.com/ahmetb/kubectx/internal/cmdutil" "github.com/ahmetb/kubectx/internal/env" "github.com/ahmetb/kubectx/internal/printer" "github.com/fatih/color" ) type Op interface { Run(stdout, stderr io.Writer) error } func main() { cmdutil.PrintDeprecatedEnvWarnings(color.Error, os.Environ()) op := parseArgs(os.Args[1:]) if err := op.Run(color.Output, color.Error); err != nil { printer.Error(color.Error, "%s", err) if _, ok := os.LookupEnv(env.EnvDebug); ok { // print stack trace in verbose mode fmt.Fprintf(color.Error, "[DEBUG] error: %+v\n", err) } defer os.Exit(1) } } ================================================ FILE: cmd/kubectx/rename.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "io" "strings" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) // RenameOp indicates intention to rename contexts. type RenameOp struct { New string // NAME of New context Old string // NAME of Old context (or '.' for current-context) } // parseRenameSyntax parses A=B form into [A,B] and returns // whether it is parsed correctly. func parseRenameSyntax(v string) (string, string, bool) { new, old, ok := strings.Cut(v, "=") if !ok || new == "" || old == "" { return "", "", false } return new, old, true } // rename changes the old (NAME or '.' for current-context) // to the "new" value. If the old refers to the current-context, // current-context preference is also updated. func (op RenameOp) Run(_, stderr io.Writer) error { if err := checkIsolatedMode(); err != nil { return err } kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return fmt.Errorf("kubeconfig error: %w", err) } cur, err := kc.GetCurrentContext() if err != nil { return fmt.Errorf("failed to get current context: %w", err) } if op.Old == "." { op.Old = cur } oldExists, err := kc.ContextExists(op.Old) if err != nil { return fmt.Errorf("failed to check context: %w", err) } if !oldExists { return fmt.Errorf("context \"%s\" not found, can't rename it", op.Old) } newExists, err := kc.ContextExists(op.New) if err != nil { return fmt.Errorf("failed to check context: %w", err) } if newExists { printer.Warning(stderr, "context \"%s\" exists, overwriting it.", op.New) if err := kc.DeleteContextEntry(op.New); err != nil { return fmt.Errorf("failed to delete new context to overwrite it: %w", err) } } if err := kc.ModifyContextName(op.Old, op.New); err != nil { return fmt.Errorf("failed to change context name: %w", err) } if op.Old == cur { if err := kc.ModifyCurrentContext(op.New); err != nil { return fmt.Errorf("failed to set current-context to new name: %w", err) } } if err := kc.Save(); err != nil { return fmt.Errorf("failed to save modified kubeconfig: %w", err) } _ = printer.Success(stderr, "Context %s renamed to %s.", printer.SuccessColor.Sprint(op.Old), printer.SuccessColor.Sprint(op.New)) return nil } ================================================ FILE: cmd/kubectx/rename_test.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "testing" "github.com/google/go-cmp/cmp" ) func Test_parseRenameSyntax(t *testing.T) { type out struct { New string Old string OK bool } tests := []struct { name string in string want out }{ { name: "no equals sign", in: "foo", want: out{OK: false}, }, { name: "no left side", in: "=a", want: out{OK: false}, }, { name: "no right side", in: "a=", want: out{OK: false}, }, { name: "correct format", in: "a=b", want: out{ New: "a", Old: "b", OK: true, }, }, { name: "correct format with current context", in: "NEW_NAME=.", want: out{ New: "NEW_NAME", Old: ".", OK: true, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { new, old, ok := parseRenameSyntax(tt.in) got := out{ New: new, Old: old, OK: ok, } diff := cmp.Diff(tt.want, got) if diff != "" { t.Errorf("parseRenameSyntax() diff=%s", diff) } }) } } ================================================ FILE: cmd/kubectx/shell.go ================================================ package main import ( "fmt" "io" "os" "os/exec" "runtime" "github.com/fatih/color" "github.com/ahmetb/kubectx/internal/env" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) // ShellOp indicates intention to start a scoped sub-shell for a context. type ShellOp struct { Target string } func (op ShellOp) Run(_, stderr io.Writer) error { if err := checkIsolatedMode(); err != nil { return err } kubectlPath, err := resolveKubectl() if err != nil { return err } // Verify context exists and get current context for exit message kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return fmt.Errorf("kubeconfig error: %w", err) } exists, err := kc.ContextExists(op.Target) if err != nil { return fmt.Errorf("failed to check context: %w", err) } if !exists { return fmt.Errorf("no context exists with the name: \"%s\"", op.Target) } previousCtx, err := kc.GetCurrentContext() if err != nil { return fmt.Errorf("failed to get current context: %w", err) } // Extract minimal kubeconfig using kubectl data, err := extractMinimalKubeconfig(kubectlPath, op.Target) if err != nil { return fmt.Errorf("failed to extract kubeconfig for context: %w", err) } // Write to temp file tmpFile, err := os.CreateTemp("", "kubectx-shell-*.yaml") if err != nil { return fmt.Errorf("failed to create temp kubeconfig file: %w", err) } tmpPath := tmpFile.Name() defer os.Remove(tmpPath) if _, err := tmpFile.Write(data); err != nil { tmpFile.Close() return fmt.Errorf("failed to write temp kubeconfig: %w", err) } tmpFile.Close() // Print entry message badgeColor := color.New(color.BgRed, color.FgWhite, color.Bold) printer.EnableOrDisableColor(badgeColor) fmt.Fprintf(stderr, "%s kubectl context is %s in this shell — type 'exit' to leave.\n", badgeColor.Sprint("[ISOLATED SHELL]"), printer.WarningColor.Sprint(op.Target)) // Detect and start shell shellBin := detectShell() cmd := exec.Command(shellBin) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Env = append(os.Environ(), "KUBECONFIG="+tmpPath, env.EnvIsolatedShell+"=1", ) _ = cmd.Run() // Print exit message fmt.Fprintf(stderr, "%s kubectl context is now %s.\n", badgeColor.Sprint("[ISOLATED SHELL EXITED]"), printer.WarningColor.Sprint(previousCtx)) return nil } func resolveKubectl() (string, error) { if v := os.Getenv("KUBECTL"); v != "" { return v, nil } path, err := exec.LookPath("kubectl") if err != nil { return "", fmt.Errorf("kubectl is required for --shell but was not found in PATH") } return path, nil } func extractMinimalKubeconfig(kubectlPath, contextName string) ([]byte, error) { cmd := exec.Command(kubectlPath, "config", "view", "--minify", "--flatten", "--context", contextName) cmd.Env = os.Environ() data, err := cmd.Output() if err != nil { return nil, fmt.Errorf("kubectl config view failed: %w", err) } return data, nil } func detectShell() string { if runtime.GOOS == "windows" { // cmd.exe always sets the PROMPT env var, so if it is present // we can reliably assume we are running inside cmd.exe. if os.Getenv("PROMPT") != "" { return "cmd.exe" } // Otherwise assume PowerShell. PSModulePath is always set on // Windows regardless of the shell, so it cannot be used as a // discriminator; however the absence of PROMPT is a strong // enough signal that we are in a PowerShell session. if pwsh, err := exec.LookPath("pwsh"); err == nil { return pwsh } if powershell, err := exec.LookPath("powershell"); err == nil { return powershell } return "cmd.exe" } if v := os.Getenv("SHELL"); v != "" { return v } return "/bin/sh" } ================================================ FILE: cmd/kubectx/shell_test.go ================================================ package main import ( "bytes" "runtime" "testing" "github.com/ahmetb/kubectx/internal/env" ) func Test_detectShell_unix(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("skipping unix shell detection test on windows") } tests := []struct { name string shellEnv string want string }{ { name: "SHELL env set", shellEnv: "/bin/zsh", want: "/bin/zsh", }, { name: "SHELL env empty, falls back to /bin/sh", shellEnv: "", want: "/bin/sh", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Setenv("SHELL", tt.shellEnv) got := detectShell() if got != tt.want { t.Errorf("detectShell() = %q, want %q", got, tt.want) } }) } } func Test_ShellOp_blockedWhenNested(t *testing.T) { // Simulate being inside an isolated shell t.Setenv(env.EnvIsolatedShell, "1") op := ShellOp{Target: "some-context"} var stdout, stderr bytes.Buffer err := op.Run(&stdout, &stderr) if err == nil { t.Fatal("expected error when running ShellOp inside isolated shell, got nil") } want := "locked single-context shell to" if !bytes.Contains([]byte(err.Error()), []byte(want)) { // The error may not contain the context name if kubeconfig is not available, // but it should still be blocked want2 := "locked single-context shell" if !bytes.Contains([]byte(err.Error()), []byte(want2)) { t.Errorf("error message %q does not contain %q", err.Error(), want2) } } } func Test_resolveKubectl_envVar(t *testing.T) { t.Setenv("KUBECTL", "/custom/path/kubectl") got, err := resolveKubectl() if err != nil { t.Fatalf("unexpected error: %v", err) } if got != "/custom/path/kubectl" { t.Errorf("resolveKubectl() = %q, want %q", got, "/custom/path/kubectl") } } func Test_resolveKubectl_inPath(t *testing.T) { t.Setenv("KUBECTL", "") // kubectl should be findable in PATH on most dev machines got, err := resolveKubectl() if err != nil { t.Skip("kubectl not in PATH, skipping") } if got == "" { t.Error("resolveKubectl() returned empty string") } } func Test_checkIsolatedMode_notSet(t *testing.T) { t.Setenv(env.EnvIsolatedShell, "") err := checkIsolatedMode() if err != nil { t.Errorf("expected nil error when not in isolated mode, got: %v", err) } } func Test_checkIsolatedMode_set(t *testing.T) { t.Setenv(env.EnvIsolatedShell, "1") err := checkIsolatedMode() if err == nil { t.Fatal("expected error when in isolated mode, got nil") } want := "locked single-context shell" if !bytes.Contains([]byte(err.Error()), []byte(want)) { t.Errorf("error message %q does not contain %q", err.Error(), want) } } ================================================ FILE: cmd/kubectx/state.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "errors" "fmt" "os" "path/filepath" "github.com/ahmetb/kubectx/internal/cmdutil" ) func kubectxPrevCtxFile() (string, error) { dir := cmdutil.CacheDir() if dir == "" { return "", errors.New("HOME or USERPROFILE environment variable not set") } return filepath.Join(dir, "kubectx"), nil } // readLastContext returns the saved previous context // if the state file exists, otherwise returns "". func readLastContext(path string) (string, error) { b, err := os.ReadFile(path) if os.IsNotExist(err) { return "", nil } return string(b), err } // writeLastContext saves the specified value to the state file. // It creates missing parent directories. func writeLastContext(path, value string) error { dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("failed to create parent directories: %w", err) } return os.WriteFile(path, []byte(value), 0644) } ================================================ FILE: cmd/kubectx/state_test.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "os" "path/filepath" "testing" ) func Test_readLastContext_nonExistingFile(t *testing.T) { s, err := readLastContext(filepath.FromSlash("/non/existing/file")) if err != nil { t.Fatal(err) } if s != "" { t.Fatalf("expected empty string; got=\"%s\"", s) } } func Test_readLastContext(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "testfile") if err := os.WriteFile(path, []byte("foo"), 0644); err != nil { t.Fatal(err) } s, err := readLastContext(path) if err != nil { t.Fatal(err) } if expected := "foo"; s != expected { t.Fatalf("expected=\"%s\"; got=\"%s\"", expected, s) } } func Test_writeLastContext_err(t *testing.T) { path := filepath.Join(os.DevNull, "foo", "bar") err := writeLastContext(path, "foo") if err == nil { t.Fatal("got empty error") } } func Test_writeLastContext(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "foo", "bar") if err := writeLastContext(path, "ctx1"); err != nil { t.Fatal(err) } v, err := readLastContext(path) if err != nil { t.Fatal(err) } if expected := "ctx1"; v != expected { t.Fatalf("read wrong value=\"%s\"; expected=\"%s\"", v, expected) } } func Test_kubectxFilePath(t *testing.T) { t.Setenv("HOME", filepath.FromSlash("/foo/bar")) t.Setenv("XDG_CACHE_HOME", "") expected := filepath.Join(filepath.FromSlash("/foo/bar"), ".kube", "kubectx") v, err := kubectxPrevCtxFile() if err != nil { t.Fatal(err) } if v != expected { t.Fatalf("expected=\"%s\" got=\"%s\"", expected, v) } } func Test_kubectxFilePath_xdgCacheHome(t *testing.T) { t.Setenv("XDG_CACHE_HOME", filepath.FromSlash("/tmp/xdg-cache")) expected := filepath.Join(filepath.FromSlash("/tmp/xdg-cache"), "kubectx") v, err := kubectxPrevCtxFile() if err != nil { t.Fatal(err) } if v != expected { t.Fatalf("expected=\"%s\" got=\"%s\"", expected, v) } } func Test_kubectxFilePath_error(t *testing.T) { t.Setenv("HOME", "") t.Setenv("USERPROFILE", "") _, err := kubectxPrevCtxFile() if err == nil { t.Fatal(err) } } ================================================ FILE: cmd/kubectx/switch.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "errors" "fmt" "io" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) // SwitchOp indicates intention to switch contexts. type SwitchOp struct { Target string // '-' for back and forth, or NAME } func (op SwitchOp) Run(_, stderr io.Writer) error { if err := checkIsolatedMode(); err != nil { return err } var newCtx string var err error if op.Target == "-" { newCtx, err = swapContext() } else { newCtx, err = switchContext(op.Target) } if err != nil { return fmt.Errorf("failed to switch context: %w", err) } if err = printer.Success(stderr, "Switched to context \"%s\".", printer.SuccessColor.Sprint(newCtx)); err != nil { return fmt.Errorf("print error: %w", err) } return nil } // switchContext switches to specified context name. func switchContext(name string) (string, error) { prevCtxFile, err := kubectxPrevCtxFile() if err != nil { return "", fmt.Errorf("failed to determine state file: %w", err) } kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return "", fmt.Errorf("kubeconfig error: %w", err) } prev, err := kc.GetCurrentContext() if err != nil { return "", fmt.Errorf("failed to get current context: %w", err) } exists, err := kc.ContextExists(name) if err != nil { return "", fmt.Errorf("failed to check context: %w", err) } if !exists { return "", fmt.Errorf("no context exists with the name: \"%s\"", name) } if err := kc.ModifyCurrentContext(name); err != nil { return "", err } if err := kc.Save(); err != nil { return "", fmt.Errorf("failed to save kubeconfig: %w", err) } if prev != name { if err := writeLastContext(prevCtxFile, prev); err != nil { return "", fmt.Errorf("failed to save previous context name: %w", err) } } return name, nil } // swapContext switches to previously switch context. func swapContext() (string, error) { prevCtxFile, err := kubectxPrevCtxFile() if err != nil { return "", fmt.Errorf("failed to determine state file: %w", err) } prev, err := readLastContext(prevCtxFile) if err != nil { return "", fmt.Errorf("failed to read previous context file: %w", err) } if prev == "" { return "", errors.New("no previous context found") } return switchContext(prev) } ================================================ FILE: cmd/kubectx/unset.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "io" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) // UnsetOp indicates intention to remove current-context preference. type UnsetOp struct{} func (_ UnsetOp) Run(_, stderr io.Writer) error { if err := checkIsolatedMode(); err != nil { return err } kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return fmt.Errorf("kubeconfig error: %w", err) } if err := kc.UnsetCurrentContext(); err != nil { return fmt.Errorf("error while modifying current-context: %w", err) } if err := kc.Save(); err != nil { return fmt.Errorf("failed to save kubeconfig file after modification: %w", err) } err := printer.Success(stderr, "Active context unset for kubectl.") if err != nil { return fmt.Errorf("write error: %w", err) } return nil } ================================================ FILE: cmd/kubectx/version.go ================================================ package main import ( "fmt" "io" ) var ( version = "v0.0.0+unknown" // populated by goreleaser ) // VersionOp describes printing version string. type VersionOp struct{} func (_ VersionOp) Run(stdout, _ io.Writer) error { _, err := fmt.Fprintf(stdout, "%s\n", version) if err != nil { return fmt.Errorf("write error: %w", err) } return nil } ================================================ FILE: cmd/kubens/current.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "errors" "fmt" "io" "github.com/ahmetb/kubectx/internal/kubeconfig" ) type CurrentOp struct{} func (c CurrentOp) Run(stdout, _ io.Writer) error { kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return fmt.Errorf("kubeconfig error: %w", err) } ctx, err := kc.GetCurrentContext() if err != nil { return fmt.Errorf("failed to get current context: %w", err) } if ctx == "" { return errors.New("current-context is not set") } ns, err := kc.NamespaceOfContext(ctx) if err != nil { return fmt.Errorf("failed to read namespace of \"%s\": %w", ctx, err) } _, err = fmt.Fprintln(stdout, ns) if err != nil { return fmt.Errorf("write error: %w", err) } return nil } ================================================ FILE: cmd/kubens/flags.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "io" "os" "slices" "strings" "github.com/ahmetb/kubectx/internal/cmdutil" ) // UnsupportedOp indicates an unsupported flag. type UnsupportedOp struct{ Err error } func (op UnsupportedOp) Run(_, _ io.Writer) error { return op.Err } // parseArgs looks at flags (excl. executable name, i.e. argv[0]) // and decides which operation should be taken. func parseArgs(argv []string) Op { n := len(argv) if n == 0 { if cmdutil.IsInteractiveMode(os.Stdout) { return InteractiveSwitchOp{SelfCmd: os.Args[0]} } return ListOp{} } if n == 1 { v := argv[0] switch v { case "--help", "-h": return HelpOp{} case "--version", "-V": return VersionOp{} case "--current", "-c": return CurrentOp{} case "--unset", "-u": return UnsetOp{} default: return getSwitchOp(v, false) } } else if n == 2 { // {namespace} -f|--force name := argv[0] force := slices.Contains([]string{"-f", "--force"}, argv[1]) if !force { if !slices.Contains([]string{"-f", "--force"}, argv[0]) { return UnsupportedOp{Err: fmt.Errorf("unsupported arguments %q", argv)} } // -f|--force {namespace} force = true name = argv[1] } return getSwitchOp(name, force) } return UnsupportedOp{Err: fmt.Errorf("too many arguments")} } func getSwitchOp(v string, force bool) Op { if strings.HasPrefix(v, "-") && v != "-" { return UnsupportedOp{Err: fmt.Errorf("unsupported option %q", v)} } return SwitchOp{Target: v, Force: force} } ================================================ FILE: cmd/kubens/flags_test.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "testing" "github.com/google/go-cmp/cmp" ) func Test_parseArgs_new(t *testing.T) { tests := []struct { name string args []string want Op }{ {name: "nil Args", args: nil, want: ListOp{}}, {name: "empty Args", args: []string{}, want: ListOp{}}, {name: "help shorthand", args: []string{"-h"}, want: HelpOp{}}, {name: "help long form", args: []string{"--help"}, want: HelpOp{}}, {name: "current shorthand", args: []string{"-c"}, want: CurrentOp{}}, {name: "current long form", args: []string{"--current"}, want: CurrentOp{}}, {name: "unset shorthand", args: []string{"-u"}, want: UnsetOp{}}, {name: "unset long form", args: []string{"--unset"}, want: UnsetOp{}}, {name: "switch by name", args: []string{"foo"}, want: SwitchOp{Target: "foo"}}, {name: "switch by name force short flag", args: []string{"foo", "-f"}, want: SwitchOp{Target: "foo", Force: true}}, {name: "switch by name force long flag", args: []string{"foo", "--force"}, want: SwitchOp{Target: "foo", Force: true}}, {name: "switch by name force short flag before name", args: []string{"-f", "foo"}, want: SwitchOp{Target: "foo", Force: true}}, {name: "switch by name force long flag before name", args: []string{"--force", "foo"}, want: SwitchOp{Target: "foo", Force: true}}, {name: "switch by name unknown arguments", args: []string{"foo", "-x"}, want: UnsupportedOp{Err: fmt.Errorf("unsupported arguments %q", []string{"foo", "-x"})}}, {name: "switch by name unknown arguments", args: []string{"-x", "foo"}, want: UnsupportedOp{Err: fmt.Errorf("unsupported arguments %q", []string{"-x", "foo"})}}, {name: "switch by swap", args: []string{"-"}, want: SwitchOp{Target: "-"}}, {name: "unrecognized flag", args: []string{"-x"}, want: UnsupportedOp{Err: fmt.Errorf("unsupported option %q", "-x")}}, {name: "too many args", args: []string{"a", "b", "c"}, want: UnsupportedOp{Err: fmt.Errorf("too many arguments")}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := parseArgs(tt.args) var opts cmp.Options if _, ok := tt.want.(UnsupportedOp); ok { opts = append(opts, cmp.Comparer(func(x, y UnsupportedOp) bool { return (x.Err == nil && y.Err == nil) || (x.Err.Error() == y.Err.Error()) })) } if diff := cmp.Diff(got, tt.want, opts...); diff != "" { t.Errorf("parseArgs(%#v) diff: %s", tt.args, diff) } }) } } ================================================ FILE: cmd/kubens/fzf.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "bytes" "errors" "fmt" "io" "os" "os/exec" "strings" "github.com/ahmetb/kubectx/internal/cmdutil" "github.com/ahmetb/kubectx/internal/env" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) type InteractiveSwitchOp struct { SelfCmd string } // TODO(ahmetb) This method is heavily repetitive vs kubectx/fzf.go. func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error { // parse kubeconfig just to see if it can be loaded kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { if cmdutil.IsNotFoundErr(err) { printer.Warning(stderr, "kubeconfig file not found") return nil } return fmt.Errorf("kubeconfig error: %w", err) } ctxNames, err := kc.ContextNames() if err != nil { return fmt.Errorf("failed to get context names: %w", err) } if len(ctxNames) == 0 { return errors.New("no contexts found in the kubeconfig file") } cmd := exec.Command("fzf", "--ansi", "--no-preview") var out bytes.Buffer cmd.Stdin = os.Stdin cmd.Stderr = stderr cmd.Stdout = &out cmd.Env = append(os.Environ(), fmt.Sprintf("FZF_DEFAULT_COMMAND=%s", op.SelfCmd), fmt.Sprintf("%s=1", env.EnvForceColor)) if err := cmd.Run(); err != nil { var exitErr *exec.ExitError if !errors.As(err, &exitErr) { return err } } choice := strings.TrimSpace(out.String()) if choice == "" { return errors.New("you did not choose any of the options") } name, err := switchNamespace(kc, choice, false) if err != nil { return fmt.Errorf("failed to switch namespace: %w", err) } _ = printer.Success(stderr, "Active namespace is \"%s\".", printer.SuccessColor.Sprint(name)) return nil } ================================================ FILE: cmd/kubens/help.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "io" "os" "path/filepath" "strings" ) // HelpOp describes printing help. type HelpOp struct{} func (_ HelpOp) Run(stdout, _ io.Writer) error { return printUsage(stdout) } func printUsage(out io.Writer) error { help := `USAGE: %PROG% : list the namespaces in the current context %PROG% : change the active namespace of current context %PROG% --force/-f : force change the active namespace of current context (even if it doesn't exist) %PROG% - : switch to the previous namespace in this context %PROG% -c, --current : show the current namespace %PROG% -h,--help : show this message %PROG% -u,--unset : unset the namespace choice (set to 'default') %PROG% -V,--version : show version` // TODO this replace logic is duplicated between this and kubectx help = strings.ReplaceAll(help, "%PROG%", selfName()) _, err := fmt.Fprintf(out, "%s\n", help) if err != nil { return fmt.Errorf("write error: %w", err) } return nil } // selfName guesses how the user invoked the program. func selfName() string { // TODO this method is duplicated between this and kubectx me := filepath.Base(os.Args[0]) pluginPrefix := "kubectl-" if strings.HasPrefix(me, pluginPrefix) { return "kubectl " + strings.TrimPrefix(me, pluginPrefix) } return "kubens" } ================================================ FILE: cmd/kubens/list.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "context" "errors" "fmt" "io" "os" "slices" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/client-go/tools/clientcmd" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) type ListOp struct{} func (op ListOp) Run(stdout, stderr io.Writer) error { kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return fmt.Errorf("kubeconfig error: %w", err) } ctx, err := kc.GetCurrentContext() if err != nil { return fmt.Errorf("failed to get current context: %w", err) } if ctx == "" { return errors.New("current-context is not set") } curNs, err := kc.NamespaceOfContext(ctx) if err != nil { return fmt.Errorf("cannot read current namespace: %w", err) } ns, err := queryNamespaces(kc) if err != nil { return fmt.Errorf("could not list namespaces (is the cluster accessible?): %w", err) } for _, c := range ns { s := c if c == curNs { s = printer.ActiveItemColor.Sprint(c) } fmt.Fprintf(stdout, "%s\n", s) } return nil } func queryNamespaces(kc *kubeconfig.Kubeconfig) ([]string, error) { if os.Getenv("_MOCK_NAMESPACES") != "" { return []string{"ns1", "ns2"}, nil } clientset, err := newKubernetesClientSet(kc) if err != nil { return nil, fmt.Errorf("failed to initialize k8s REST client: %w", err) } var out []string var next string for { list, err := clientset.CoreV1().Namespaces().List( context.Background(), metav1.ListOptions{ Limit: 500, Continue: next, }) if err != nil { return nil, fmt.Errorf("failed to list namespaces from k8s API: %w", err) } next = list.Continue out = slices.Grow(out, len(list.Items)) for _, it := range list.Items { out = append(out, it.Name) } if next == "" { break } } return out, nil } func newKubernetesClientSet(kc *kubeconfig.Kubeconfig) (*kubernetes.Clientset, error) { b, err := kc.Bytes() if err != nil { return nil, fmt.Errorf("failed to convert in-memory kubeconfig to yaml: %w", err) } cfg, err := clientcmd.RESTConfigFromKubeConfig(b) if err != nil { return nil, fmt.Errorf("failed to initialize config: %w", err) } return kubernetes.NewForConfig(cfg) } ================================================ FILE: cmd/kubens/main.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "io" "os" "github.com/ahmetb/kubectx/internal/cmdutil" "github.com/ahmetb/kubectx/internal/env" "github.com/ahmetb/kubectx/internal/printer" "github.com/fatih/color" ) type Op interface { Run(stdout, stderr io.Writer) error } func main() { cmdutil.PrintDeprecatedEnvWarnings(color.Error, os.Environ()) op := parseArgs(os.Args[1:]) if err := op.Run(color.Output, color.Error); err != nil { printer.Error(color.Error, "%s", err) if _, ok := os.LookupEnv(env.EnvDebug); ok { // print stack trace in verbose mode fmt.Fprintf(color.Error, "[DEBUG] error: %+v\n", err) } defer os.Exit(1) } } ================================================ FILE: cmd/kubens/statefile.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "bytes" "os" "path/filepath" "runtime" "strings" "github.com/ahmetb/kubectx/internal/cmdutil" ) var defaultDir = filepath.Join(cmdutil.CacheDir(), "kubens") type NSFile struct { dir string ctx string } func NewNSFile(ctx string) NSFile { return NSFile{dir: defaultDir, ctx: ctx} } func (f NSFile) path() string { fn := f.ctx if isWindows() { // bug 230: eks clusters contain ':' in ctx name, not a valid file name for win32 fn = strings.ReplaceAll(fn, ":", "__") } return filepath.Join(f.dir, fn) } // Load reads the previous namespace setting, or returns empty if not exists. func (f NSFile) Load() (string, error) { b, err := os.ReadFile(f.path()) if err != nil { if os.IsNotExist(err) { return "", nil } return "", err } return string(bytes.TrimSpace(b)), nil } // Save stores the previous namespace information in the file. func (f NSFile) Save(value string) error { d := filepath.Dir(f.path()) if err := os.MkdirAll(d, 0755); err != nil { return err } return os.WriteFile(f.path(), []byte(value), 0644) } // isWindows determines if the process is running on windows OS. func isWindows() bool { if os.Getenv("_FORCE_GOOS") == "windows" { // for testing return true } return runtime.GOOS == "windows" } ================================================ FILE: cmd/kubens/statefile_test.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "runtime" "strings" "testing" ) func TestNSFile(t *testing.T) { td := t.TempDir() f := NewNSFile("foo") f.dir = td v, err := f.Load() if err != nil { t.Fatal(err) } if v != "" { t.Fatalf("Load() expected empty; got=%v", err) } err = f.Save("bar") if err != nil { t.Fatalf("Save() err=%v", err) } v, err = f.Load() if err != nil { t.Fatal(err) } if expected := "bar"; v != expected { t.Fatalf("Load()=\"%s\"; expected=\"%s\"", v, expected) } } func TestNSFile_path_windows(t *testing.T) { t.Setenv("_FORCE_GOOS", "windows") fp := NewNSFile("a:b:c").path() if expected := "a__b__c"; !strings.HasSuffix(fp, expected) { t.Fatalf("file did not have expected ending %q: %s", expected, fp) } } func Test_isWindows(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("won't test this case on windows") } got := isWindows() if got { t.Fatalf("isWindows() returned true for %s", runtime.GOOS) } t.Setenv("_FORCE_GOOS", "windows") if !isWindows() { t.Fatalf("isWindows() failed to detect windows with env override.") } } ================================================ FILE: cmd/kubens/switch.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "context" "errors" "fmt" "io" "os" errors2 "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) type SwitchOp struct { Target string // '-' for back and forth, or NAME Force bool // force switch even if the namespace doesn't exist } func (s SwitchOp) Run(_, stderr io.Writer) error { kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return fmt.Errorf("kubeconfig error: %w", err) } toNS, err := switchNamespace(kc, s.Target, s.Force) if err != nil { return err } err = printer.Success(stderr, "Active namespace is \"%s\"", printer.SuccessColor.Sprint(toNS)) return err } func switchNamespace(kc *kubeconfig.Kubeconfig, ns string, force bool) (string, error) { ctx, err := kc.GetCurrentContext() if err != nil { return "", fmt.Errorf("failed to get current context: %w", err) } if ctx == "" { return "", errors.New("current-context is not set") } curNS, err := kc.NamespaceOfContext(ctx) if err != nil { return "", fmt.Errorf("failed to get current namespace: %w", err) } f := NewNSFile(ctx) prev, err := f.Load() if err != nil { return "", fmt.Errorf("failed to load previous namespace from file: %w", err) } if ns == "-" { if prev == "" { return "", fmt.Errorf("No previous namespace found for current context (%s)", ctx) } ns = prev } if !force { ok, err := namespaceExists(kc, ns) if err != nil { return "", fmt.Errorf("failed to query if namespace exists (is cluster accessible?): %w", err) } if !ok { return "", fmt.Errorf("no namespace exists with name \"%s\"", ns) } } if err := kc.SetNamespace(ctx, ns); err != nil { return "", fmt.Errorf("failed to change to namespace \"%s\": %w", ns, err) } if err := kc.Save(); err != nil { return "", fmt.Errorf("failed to save kubeconfig file: %w", err) } if curNS != ns { if err := f.Save(curNS); err != nil { return "", fmt.Errorf("failed to save the previous namespace to file: %w", err) } } return ns, nil } func namespaceExists(kc *kubeconfig.Kubeconfig, ns string) (bool, error) { // for tests if os.Getenv("_MOCK_NAMESPACES") != "" { return ns == "ns1" || ns == "ns2", nil } clientset, err := newKubernetesClientSet(kc) if err != nil { return false, fmt.Errorf("failed to initialize k8s REST client: %w", err) } namespace, err := clientset.CoreV1().Namespaces().Get(context.Background(), ns, metav1.GetOptions{}) if errors2.IsNotFound(err) { return false, nil } if err != nil { return false, fmt.Errorf("failed to query namespace %q from k8s API: %w", ns, err) } return namespace != nil, nil } ================================================ FILE: cmd/kubens/unset.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "errors" "fmt" "io" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) // UnsetOp indicates intention to remove current namespace preference. type UnsetOp struct{} func (_ UnsetOp) Run(_, stderr io.Writer) error { kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return fmt.Errorf("kubeconfig error: %w", err) } ns, err := clearNamespace(kc) if err != nil { return err } err = printer.Success(stderr, "Active namespace is \"%s\".", printer.SuccessColor.Sprint(ns)) return err } func clearNamespace(kc *kubeconfig.Kubeconfig) (string, error) { ctx, err := kc.GetCurrentContext() if err != nil { return "", fmt.Errorf("failed to get current context: %w", err) } ns := "default" if ctx == "" { return "", errors.New("current-context is not set") } if err := kc.SetNamespace(ctx, ns); err != nil { return "", fmt.Errorf("failed to clear namespace: %w", err) } if err := kc.Save(); err != nil { return "", fmt.Errorf("failed to save kubeconfig file: %w", err) } return ns, nil } ================================================ FILE: cmd/kubens/version.go ================================================ package main import ( "fmt" "io" ) var ( version = "v0.0.0+unknown" // populated by goreleaser ) // VersionOp describes printing version string. type VersionOp struct{} func (_ VersionOp) Run(stdout, _ io.Writer) error { _, err := fmt.Fprintf(stdout, "%s\n", version) if err != nil { return fmt.Errorf("write error: %w", err) } return nil } ================================================ FILE: completion/_kubectx.zsh ================================================ #compdef kubectx kctx=kubectx local KUBECTX="${HOME}/.kube/kubectx" PREV="" local context_array=("${(@f)$(kubectl config get-contexts --output='name')}") local all_contexts=(\'${^context_array}\') if [ -f "$KUBECTX" ]; then # show '-' only if there's a saved previous context local PREV=$(cat "${KUBECTX}") _arguments \ "-d:*: :(${all_contexts})" \ "(- *): :(- ${all_contexts})" else _arguments \ "-d:*: :(${all_contexts})" \ "(- *): :(${all_contexts})" fi ================================================ FILE: completion/_kubens.zsh ================================================ #compdef kubens kns=kubens _arguments "1: :(- $(kubectl get namespaces -o=jsonpath='{range .items[*].metadata.name}{@}{"\n"}{end}'))" ================================================ FILE: completion/kubectx.bash ================================================ _kube_contexts() { local curr_arg; curr_arg=${COMP_WORDS[COMP_CWORD]} COMPREPLY=( $(compgen -W "- $(kubectl config get-contexts --output='name')" -- $curr_arg ) ); } complete -F _kube_contexts kubectx kctx ================================================ FILE: completion/kubectx.fish ================================================ # kubectx function __fish_kubectx_arg_number -a number set -l cmd (commandline -opc) test (count $cmd) -eq $number end complete -f -c kubectx complete -f -x -c kubectx -n '__fish_kubectx_arg_number 1' -a "(kubectl config get-contexts --output='name')" complete -f -x -c kubectx -n '__fish_kubectx_arg_number 1' -a "-" -d "switch to the previous namespace in this context" ================================================ FILE: completion/kubens.bash ================================================ _kube_namespaces() { local curr_arg; curr_arg=${COMP_WORDS[COMP_CWORD]} COMPREPLY=( $(compgen -W "- $(kubectl get namespaces -o=jsonpath='{range .items[*].metadata.name}{@}{"\n"}{end}')" -- $curr_arg ) ); } complete -F _kube_namespaces kubens kns ================================================ FILE: completion/kubens.fish ================================================ # kubens function __fish_kubens_arg_number -a number set -l cmd (commandline -opc) test (count $cmd) -eq $number end complete -f -c kubens complete -f -x -c kubens -n '__fish_kubens_arg_number 1' -a "(kubectl get ns -o=custom-columns=NAME:.metadata.name --no-headers)" complete -f -x -c kubens -n '__fish_kubens_arg_number 1' -a "-" -d "switch to the previous namespace in this context" complete -f -x -c kubens -n '__fish_kubens_arg_number 1' -s c -l current -d "show the current namespace" complete -f -x -c kubens -n '__fish_kubens_arg_number 1' -s h -l help -d "show the help message" ================================================ FILE: go.mod ================================================ module github.com/ahmetb/kubectx go 1.25.0 require ( facette.io/natsort v0.0.0-20181210072756-2cd4dd1e2dcb github.com/fatih/color v1.18.0 github.com/google/go-cmp v0.7.0 github.com/mattn/go-isatty v0.0.20 k8s.io/apimachinery v0.35.2 k8s.io/client-go v0.35.2 sigs.k8s.io/kustomize/kyaml v0.21.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/spf13/pflag v1.0.9 // indirect github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.9.0 // indirect google.golang.org/protobuf v1.36.8 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.35.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) ================================================ FILE: go.sum ================================================ facette.io/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:1pSweJFeR3Pqx7uoelppkzeegfUBXL6I2FFAbfXw570= facette.io/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:npRYmtaITVom7rcSo+pRURltHSG2r4TQM1cdqJ2dUB0= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw= k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60= k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8= k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o= k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7fI= sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= ================================================ FILE: internal/cmdutil/deprecated.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package cmdutil import ( "io" "strings" "github.com/ahmetb/kubectx/internal/printer" ) func PrintDeprecatedEnvWarnings(out io.Writer, vars []string) { for _, vv := range vars { parts := strings.SplitN(vv, "=", 2) if len(parts) != 2 { continue } key := parts[0] if key == `KUBECTX_CURRENT_FGCOLOR` || key == `KUBECTX_CURRENT_BGCOLOR` { printer.Warning(out, "%s environment variable is now deprecated", key) } } } ================================================ FILE: internal/cmdutil/deprecated_test.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package cmdutil import ( "bytes" "strings" "testing" ) func TestPrintDeprecatedEnvWarnings_noDeprecatedVars(t *testing.T) { var out bytes.Buffer PrintDeprecatedEnvWarnings(&out, []string{ "A=B", "PATH=/foo:/bar:/bin", }) if v := out.String(); len(v) > 0 { t.Fatalf("something written to buf: %v", v) } } func TestPrintDeprecatedEnvWarnings_bgColors(t *testing.T) { var out bytes.Buffer PrintDeprecatedEnvWarnings(&out, []string{ "KUBECTX_CURRENT_FGCOLOR=1", "KUBECTX_CURRENT_BGCOLOR=2", }) v := out.String() if !strings.Contains(v, "KUBECTX_CURRENT_FGCOLOR") { t.Fatalf("output doesn't contain 'KUBECTX_CURRENT_FGCOLOR': \"%s\"", v) } if !strings.Contains(v, "KUBECTX_CURRENT_BGCOLOR") { t.Fatalf("output doesn't contain 'KUBECTX_CURRENT_BGCOLOR': \"%s\"", v) } } ================================================ FILE: internal/cmdutil/interactive.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package cmdutil import ( "os" "os/exec" "github.com/mattn/go-isatty" "github.com/ahmetb/kubectx/internal/env" ) // isTerminal determines if given fd is a TTY. func isTerminal(fd *os.File) bool { return isatty.IsTerminal(fd.Fd()) } // fzfInstalled determines if fzf(1) is in PATH. func fzfInstalled() bool { v, _ := exec.LookPath("fzf") if v != "" { return true } return false } // IsInteractiveMode determines if we can do choosing with fzf. func IsInteractiveMode(stdout *os.File) bool { v := os.Getenv(env.EnvFZFIgnore) return v == "" && isTerminal(stdout) && fzfInstalled() } ================================================ FILE: internal/cmdutil/util.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package cmdutil import ( "errors" "os" "path/filepath" ) func HomeDir() string { home := os.Getenv("HOME") if home == "" { home = os.Getenv("USERPROFILE") // windows } return home } // CacheDir returns XDG_CACHE_HOME if set, otherwise $HOME/.kube, // matching the bash scripts' behavior: ${XDG_CACHE_HOME:-$HOME/.kube}. func CacheDir() string { if xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" { return xdg } home := HomeDir() if home == "" { return "" } return filepath.Join(home, ".kube") } // IsNotFoundErr determines if the underlying error is os.IsNotExist. func IsNotFoundErr(err error) bool { for e := err; e != nil; e = errors.Unwrap(e) { if os.IsNotExist(e) { return true } } return false } ================================================ FILE: internal/cmdutil/util_test.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package cmdutil import ( "path/filepath" "testing" ) func Test_homeDir(t *testing.T) { type env struct{ k, v string } cases := []struct { name string envs []env want string }{ { name: "don't use XDG_CACHE_HOME as homedir", envs: []env{ {"XDG_CACHE_HOME", "xdg"}, {"HOME", "home"}, }, want: "home", }, { name: "HOME over USERPROFILE", envs: []env{ {"HOME", "home"}, {"USERPROFILE", "up"}, }, want: "home", }, { name: "only USERPROFILE available", envs: []env{ {"HOME", ""}, {"USERPROFILE", "up"}, }, want: "up", }, { name: "none available", envs: []env{ {"HOME", ""}, {"USERPROFILE", ""}, }, want: "", }, } for _, c := range cases { t.Run(c.name, func(tt *testing.T) { for _, e := range c.envs { tt.Setenv(e.k, e.v) } got := HomeDir() if got != c.want { t.Errorf("expected:%q got:%q", c.want, got) } }) } } func TestCacheDir(t *testing.T) { t.Run("XDG_CACHE_HOME set", func(t *testing.T) { t.Setenv("XDG_CACHE_HOME", "/tmp/xdg-cache") t.Setenv("HOME", "/home/user") if got := CacheDir(); got != "/tmp/xdg-cache" { t.Errorf("expected:%q got:%q", "/tmp/xdg-cache", got) } }) t.Run("XDG_CACHE_HOME unset, falls back to HOME/.kube", func(t *testing.T) { t.Setenv("XDG_CACHE_HOME", "") t.Setenv("HOME", "/home/user") want := filepath.Join("/home/user", ".kube") if got := CacheDir(); got != want { t.Errorf("expected:%q got:%q", want, got) } }) t.Run("neither set", func(t *testing.T) { t.Setenv("XDG_CACHE_HOME", "") t.Setenv("HOME", "") t.Setenv("USERPROFILE", "") if got := CacheDir(); got != "" { t.Errorf("expected:%q got:%q", "", got) } }) } ================================================ FILE: internal/env/constants.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package env const ( // EnvFZFIgnore describes the environment variable to set to disable // interactive context selection when fzf is installed. EnvFZFIgnore = "KUBECTX_IGNORE_FZF" // EnvNoColor describes the environment variable to disable color usage // when printing current context in a list. EnvNoColor = `NO_COLOR` // EnvForceColor describes the "internal" environment variable to force // color usage to show current context in a list. EnvForceColor = `_KUBECTX_FORCE_COLOR` // EnvDebug describes the internal environment variable for more verbose logging. EnvDebug = `DEBUG` EnvIsolatedShell = "KUBECTX_ISOLATED_SHELL" ) ================================================ FILE: internal/kubeconfig/contextmodify.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubeconfig import ( "errors" "sigs.k8s.io/kustomize/kyaml/yaml" ) func (k *Kubeconfig) DeleteContextEntry(deleteName string) error { contexts, err := k.contextsNode() if err != nil { return err } if err := contexts.PipeE( yaml.ElementSetter{ Keys: []string{"name"}, Values: []string{deleteName}, }, ); err != nil { return err } return nil } func (k *Kubeconfig) ModifyCurrentContext(name string) error { if err := k.config.PipeE(yaml.SetField("current-context", yaml.NewScalarRNode(name))); err != nil { return err } return nil } func (k *Kubeconfig) ModifyContextName(old, new string) error { context, err := k.config.Pipe(yaml.Lookup("contexts", "[name="+old+"]")) if err != nil { return err } if context == nil { return errors.New("\"contexts\" entry is nil") } if err := context.PipeE(yaml.SetField("name", yaml.NewScalarRNode(new))); err != nil { return err } return nil } ================================================ FILE: internal/kubeconfig/contextmodify_test.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubeconfig import ( "testing" "github.com/google/go-cmp/cmp" "github.com/ahmetb/kubectx/internal/testutil" ) func TestKubeconfig_DeleteContextEntry_errors(t *testing.T) { kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`[1, 2, 3]`)) _ = kc.Parse() err := kc.DeleteContextEntry("foo") if err == nil { t.Fatal("supposed to fail on non-mapping nodes") } kc = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`a: b`)) _ = kc.Parse() err = kc.DeleteContextEntry("foo") if err == nil { t.Fatal("supposed to fail if contexts key does not exist") } kc = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`contexts: "some string"`)) _ = kc.Parse() err = kc.DeleteContextEntry("foo") if err == nil { t.Fatal("supposed to fail if contexts key is not an array") } } func TestKubeconfig_DeleteContextEntry(t *testing.T) { test := WithMockKubeconfigLoader( testutil.KC().WithCtxs( testutil.Ctx("c1"), testutil.Ctx("c2"), testutil.Ctx("c3")).ToYAML(t)) kc := new(Kubeconfig).WithLoader(test) if err := kc.Parse(); err != nil { t.Fatal(err) } if err := kc.DeleteContextEntry("c1"); err != nil { t.Fatal(err) } if err := kc.Save(); err != nil { t.Fatal(err) } expected := testutil.KC().WithCtxs( testutil.Ctx("c2"), testutil.Ctx("c3")).ToYAML(t) out := test.Output() if diff := cmp.Diff(expected, out); diff != "" { t.Fatalf("diff: %s", diff) } } func TestKubeconfig_ModifyCurrentContext_fieldExists(t *testing.T) { test := WithMockKubeconfigLoader( testutil.KC().WithCurrentCtx("abc").Set("field1", "value1").ToYAML(t)) kc := new(Kubeconfig).WithLoader(test) if err := kc.Parse(); err != nil { t.Fatal(err) } if err := kc.ModifyCurrentContext("foo"); err != nil { t.Fatal(err) } if err := kc.Save(); err != nil { t.Fatal(err) } expected := testutil.KC().WithCurrentCtx("foo").Set("field1", "value1").ToYAML(t) out := test.Output() if diff := cmp.Diff(expected, out); diff != "" { t.Fatalf("diff: %s", diff) } } func TestKubeconfig_ModifyCurrentContext_fieldMissing(t *testing.T) { test := WithMockKubeconfigLoader(`f1: v1`) kc := new(Kubeconfig).WithLoader(test) if err := kc.Parse(); err != nil { t.Fatal(err) } if err := kc.ModifyCurrentContext("foo"); err != nil { t.Fatal(err) } if err := kc.Save(); err != nil { t.Fatal(err) } expected := `f1: v1 current-context: foo ` out := test.Output() if diff := cmp.Diff(expected, out); diff != "" { t.Fatalf("diff: %s", diff) } } func TestKubeconfig_ModifyContextName_noContextsEntryError(t *testing.T) { // no context entries test := WithMockKubeconfigLoader(`a: b`) kc := new(Kubeconfig).WithLoader(test) if err := kc.Parse(); err != nil { t.Fatal(err) } if err := kc.ModifyContextName("c1", "c2"); err == nil { t.Fatal("was expecting error for no 'contexts' entry; got nil") } } func TestKubeconfig_ModifyContextName_contextsEntryNotSequenceError(t *testing.T) { // no context entries test := WithMockKubeconfigLoader( `contexts: "hello"`) kc := new(Kubeconfig).WithLoader(test) if err := kc.Parse(); err != nil { t.Fatal(err) } if err := kc.ModifyContextName("c1", "c2"); err == nil { t.Fatal("was expecting error for 'context entry not a sequence'; got nil") } } func TestKubeconfig_ModifyContextName_noChange(t *testing.T) { test := WithMockKubeconfigLoader(testutil.KC().WithCtxs( testutil.Ctx("c1"), testutil.Ctx("c2"), testutil.Ctx("c3")).ToYAML(t)) kc := new(Kubeconfig).WithLoader(test) if err := kc.Parse(); err != nil { t.Fatal(err) } if err := kc.ModifyContextName("c5", "c6"); err == nil { t.Fatal("was expecting error for 'no changes made'") } } func TestKubeconfig_ModifyContextName(t *testing.T) { test := WithMockKubeconfigLoader(testutil.KC().WithCtxs( testutil.Ctx("c1"), testutil.Ctx("c2"), testutil.Ctx("c3")).ToYAML(t)) kc := new(Kubeconfig).WithLoader(test) if err := kc.Parse(); err != nil { t.Fatal(err) } if err := kc.ModifyContextName("c1", "ccc"); err != nil { t.Fatal(err) } if err := kc.Save(); err != nil { t.Fatal(err) } expected := testutil.KC().WithCtxs( testutil.Ctx("ccc"), testutil.Ctx("c2"), testutil.Ctx("c3")).ToYAML(t) out := test.Output() if diff := cmp.Diff(expected, out); diff != "" { t.Fatalf("diff: %s", diff) } } ================================================ FILE: internal/kubeconfig/contexts.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubeconfig import ( "errors" "fmt" "slices" "sigs.k8s.io/kustomize/kyaml/yaml" ) func (k *Kubeconfig) contextsNode() (*yaml.RNode, error) { contexts, err := k.config.Pipe(yaml.Get("contexts")) if err != nil { return nil, err } if contexts == nil { return nil, errors.New("\"contexts\" entry is nil") } else if contexts.YNode().Kind != yaml.SequenceNode { return nil, errors.New("\"contexts\" is not a sequence node") } return contexts, nil } func (k *Kubeconfig) contextNode(name string) (*yaml.RNode, error) { contexts, err := k.contextsNode() if err != nil { return nil, err } context, err := contexts.Pipe(yaml.Lookup("[name=" + name + "]")) if err != nil { return nil, err } if context == nil { return nil, fmt.Errorf("context with name \"%s\" not found", name) } return context, nil } func (k *Kubeconfig) ContextNames() ([]string, error) { contexts, err := k.config.Pipe(yaml.Get("contexts")) if err != nil { return nil, fmt.Errorf("failed to get contexts: %w", err) } if contexts == nil { return nil, nil } names, err := contexts.ElementValues("name") if err != nil { return nil, fmt.Errorf("failed to get context names: %w", err) } return names, nil } func (k *Kubeconfig) ContextExists(name string) (bool, error) { names, err := k.ContextNames() if err != nil { return false, err } return slices.Contains(names, name), nil } ================================================ FILE: internal/kubeconfig/contexts_test.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubeconfig import ( "testing" "github.com/google/go-cmp/cmp" "github.com/ahmetb/kubectx/internal/testutil" ) func TestKubeconfig_ContextNames(t *testing.T) { tl := WithMockKubeconfigLoader( testutil.KC().WithCtxs( testutil.Ctx("abc"), testutil.Ctx("def"), testutil.Ctx("ghi")).Set("field1", map[string]string{"bar": "zoo"}).ToYAML(t)) kc := new(Kubeconfig).WithLoader(tl) if err := kc.Parse(); err != nil { t.Fatal(err) } ctx, err := kc.ContextNames() if err != nil { t.Fatal(err) } expected := []string{"abc", "def", "ghi"} if diff := cmp.Diff(expected, ctx); diff != "" { t.Fatalf("%s", diff) } } func TestKubeconfig_ContextNames_noContextsEntry(t *testing.T) { tl := WithMockKubeconfigLoader(`a: b`) kc := new(Kubeconfig).WithLoader(tl) if err := kc.Parse(); err != nil { t.Fatal(err) } ctx, err := kc.ContextNames() if err != nil { t.Fatal(err) } var expected []string = nil if diff := cmp.Diff(expected, ctx); diff != "" { t.Fatalf("%s", diff) } } func TestKubeconfig_ContextNames_nonArrayContextsEntry(t *testing.T) { tl := WithMockKubeconfigLoader(`contexts: "hello"`) kc := new(Kubeconfig).WithLoader(tl) if err := kc.Parse(); err != nil { t.Fatal(err) } _, err := kc.ContextNames() if err == nil { t.Fatal("expected error for non-array contexts entry") } } func TestKubeconfig_CheckContextExists(t *testing.T) { tl := WithMockKubeconfigLoader( testutil.KC().WithCtxs( testutil.Ctx("c1"), testutil.Ctx("c2")).ToYAML(t)) kc := new(Kubeconfig).WithLoader(tl) if err := kc.Parse(); err != nil { t.Fatal(err) } if exists, err := kc.ContextExists("c1"); err != nil || !exists { t.Fatal("c1 actually exists; reported false") } if exists, err := kc.ContextExists("c2"); err != nil || !exists { t.Fatal("c2 actually exists; reported false") } if exists, err := kc.ContextExists("c3"); err != nil { t.Fatal(err) } else if exists { t.Fatal("c3 does not exist; but reported true") } } ================================================ FILE: internal/kubeconfig/currentcontext.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubeconfig import ( "fmt" "sigs.k8s.io/kustomize/kyaml/yaml" ) // GetCurrentContext returns "current-context" value in given // kubeconfig object Node, or returns ("", nil) if not found. func (k *Kubeconfig) GetCurrentContext() (string, error) { v, err := k.config.Pipe(yaml.Get("current-context")) if err != nil { return "", fmt.Errorf("failed to read current-context: %w", err) } return yaml.GetValue(v), nil } func (k *Kubeconfig) UnsetCurrentContext() error { return k.config.PipeE(yaml.SetField("current-context", yaml.NewStringRNode(""))) } ================================================ FILE: internal/kubeconfig/currentcontext_test.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubeconfig import ( "testing" "github.com/ahmetb/kubectx/internal/testutil" ) func TestKubeconfig_GetCurrentContext(t *testing.T) { tl := WithMockKubeconfigLoader(`current-context: foo`) kc := new(Kubeconfig).WithLoader(tl) if err := kc.Parse(); err != nil { t.Fatal(err) } v, err := kc.GetCurrentContext() if err != nil { t.Fatal(err) } expected := "foo" if v != expected { t.Fatalf("expected=\"%s\"; got=\"%s\"", expected, v) } } func TestKubeconfig_GetCurrentContext_missingField(t *testing.T) { tl := WithMockKubeconfigLoader(`abc: def`) kc := new(Kubeconfig).WithLoader(tl) if err := kc.Parse(); err != nil { t.Fatal(err) } v, err := kc.GetCurrentContext() if err != nil { t.Fatal(err) } expected := "" if v != expected { t.Fatalf("expected=\"%s\"; got=\"%s\"", expected, v) } } func TestKubeconfig_UnsetCurrentContext(t *testing.T) { tl := WithMockKubeconfigLoader(testutil.KC().WithCurrentCtx("foo").ToYAML(t)) kc := new(Kubeconfig).WithLoader(tl) if err := kc.Parse(); err != nil { t.Fatal(err) } if err := kc.UnsetCurrentContext(); err != nil { t.Fatal(err) } if err := kc.Save(); err != nil { t.Fatal(err) } out := tl.Output() expected := testutil.KC().WithCurrentCtx("").ToYAML(t) if out != expected { t.Fatalf("expected=\"%s\"; got=\"%s\"", expected, out) } } ================================================ FILE: internal/kubeconfig/helper_test.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubeconfig import ( "bytes" "io" "strings" ) type MockKubeconfigLoader struct { in io.Reader out bytes.Buffer } func (t *MockKubeconfigLoader) Read(p []byte) (n int, err error) { return t.in.Read(p) } func (t *MockKubeconfigLoader) Write(p []byte) (n int, err error) { return t.out.Write(p) } func (t *MockKubeconfigLoader) Close() error { return nil } func (t *MockKubeconfigLoader) Reset() error { return nil } func (t *MockKubeconfigLoader) Load() ([]ReadWriteResetCloser, error) { return []ReadWriteResetCloser{ReadWriteResetCloser(t)}, nil } func (t *MockKubeconfigLoader) Output() string { return t.out.String() } func WithMockKubeconfigLoader(kubecfg string) *MockKubeconfigLoader { return &MockKubeconfigLoader{in: strings.NewReader(kubecfg)} } ================================================ FILE: internal/kubeconfig/kubeconfig.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubeconfig import ( "errors" "fmt" "io" "sigs.k8s.io/kustomize/kyaml/yaml" ) type ReadWriteResetCloser interface { io.ReadWriteCloser // Reset truncates the file and seeks to the beginning of the file. Reset() error } type Loader interface { Load() ([]ReadWriteResetCloser, error) } type Kubeconfig struct { loader Loader f ReadWriteResetCloser config *yaml.RNode } func (k *Kubeconfig) WithLoader(l Loader) *Kubeconfig { k.loader = l return k } func (k *Kubeconfig) Close() error { if k.f == nil { return nil } return k.f.Close() } func (k *Kubeconfig) Parse() error { files, err := k.loader.Load() if err != nil { return fmt.Errorf("failed to load: %w", err) } // TODO since we don't support multiple kubeconfig files at the moment, there's just 1 file f := files[0] k.f = f var v yaml.Node if err := yaml.NewDecoder(f).Decode(&v); err != nil { return fmt.Errorf("failed to decode: %w", err) } k.config = yaml.NewRNode(&v) if k.config.YNode().Kind != yaml.MappingNode { return errors.New("kubeconfig file is not a map document") } return nil } func (k *Kubeconfig) Bytes() ([]byte, error) { str, err := k.config.String() if err != nil { return nil, err } return []byte(str), nil } func (k *Kubeconfig) Save() error { if err := k.f.Reset(); err != nil { return fmt.Errorf("failed to reset file: %w", err) } enc := yaml.NewEncoder(k.f) enc.SetIndent(0) if err := enc.Encode(k.config.YNode()); err != nil { return err } return enc.Close() } ================================================ FILE: internal/kubeconfig/kubeconfig_test.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubeconfig import ( "testing" "github.com/google/go-cmp/cmp" "github.com/ahmetb/kubectx/internal/testutil" ) func TestParse(t *testing.T) { err := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`a: [1, 2`)).Parse() if err == nil { t.Fatal("expected error from bad yaml") } err = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`[1, 2, 3]`)).Parse() if err == nil { t.Fatal("expected error from not-mapping root node") } err = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`current-context: foo`)).Parse() if err != nil { t.Fatal(err) } err = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC(). WithCurrentCtx("foo"). WithCtxs().ToYAML(t))).Parse() if err != nil { t.Fatal(err) } } func TestSave(t *testing.T) { in := "a: [1, 2, 3]\n" test := WithMockKubeconfigLoader(in) kc := new(Kubeconfig).WithLoader(test) defer kc.Close() if err := kc.Parse(); err != nil { t.Fatal(err) } if err := kc.ModifyCurrentContext("hello"); err != nil { t.Fatal(err) } if err := kc.Save(); err != nil { t.Fatal(err) } expected := "a: [1, 2, 3]\ncurrent-context: hello\n" if diff := cmp.Diff(expected, test.Output()); diff != "" { t.Fatal(diff) } } ================================================ FILE: internal/kubeconfig/kubeconfigloader.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubeconfig import ( "errors" "fmt" "os" "path/filepath" "github.com/ahmetb/kubectx/internal/cmdutil" ) var ( DefaultLoader Loader = new(StandardKubeconfigLoader) ) type StandardKubeconfigLoader struct{} type kubeconfigFile struct{ *os.File } func (*StandardKubeconfigLoader) Load() ([]ReadWriteResetCloser, error) { cfgPath, err := kubeconfigPath() if err != nil { return nil, fmt.Errorf("cannot determine kubeconfig path: %w", err) } f, err := os.OpenFile(cfgPath, os.O_RDWR, 0) if err != nil { if os.IsNotExist(err) { return nil, fmt.Errorf("kubeconfig file not found: %w", err) } return nil, fmt.Errorf("failed to open file: %w", err) } // TODO we'll return all kubeconfig files when we start implementing multiple kubeconfig support return []ReadWriteResetCloser{ReadWriteResetCloser(&kubeconfigFile{f})}, nil } func (kf *kubeconfigFile) Reset() error { if err := kf.Truncate(0); err != nil { return fmt.Errorf("failed to truncate file: %w", err) } if _, err := kf.Seek(0, 0); err != nil { return fmt.Errorf("failed to seek in file: %w", err) } return nil } func kubeconfigPath() (string, error) { // KUBECONFIG env var if v := os.Getenv("KUBECONFIG"); v != "" { list := filepath.SplitList(v) if len(list) > 1 { // TODO KUBECONFIG=file1:file2 currently not supported return "", errors.New("multiple files in KUBECONFIG are currently not supported") } return v, nil } // default path home := cmdutil.HomeDir() if home == "" { return "", errors.New("HOME or USERPROFILE environment variable not set") } return filepath.Join(home, ".kube", "config"), nil } ================================================ FILE: internal/kubeconfig/kubeconfigloader_test.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubeconfig import ( "os" "path/filepath" "strings" "testing" "github.com/ahmetb/kubectx/internal/cmdutil" ) func Test_kubeconfigPath(t *testing.T) { t.Setenv("HOME", "/x/y/z") expected := filepath.FromSlash("/x/y/z/.kube/config") got, err := kubeconfigPath() if err != nil { t.Fatal(err) } if got != expected { t.Fatalf("got=%q expected=%q", got, expected) } } func Test_kubeconfigPath_noEnvVars(t *testing.T) { t.Setenv("XDG_CACHE_HOME", "") t.Setenv("HOME", "") t.Setenv("USERPROFILE", "") _, err := kubeconfigPath() if err == nil { t.Fatalf("expected error") } } func Test_kubeconfigPath_envOvveride(t *testing.T) { t.Setenv("KUBECONFIG", "foo") v, err := kubeconfigPath() if err != nil { t.Fatal(err) } if expected := "foo"; v != expected { t.Fatalf("expected=%q, got=%q", expected, v) } } func Test_kubeconfigPath_envOvverideDoesNotSupportPathSeparator(t *testing.T) { path := strings.Join([]string{"file1", "file2"}, string(os.PathListSeparator)) t.Setenv("KUBECONFIG", path) _, err := kubeconfigPath() if err == nil { t.Fatal("expected error") } } func TestStandardKubeconfigLoader_returnsNotFoundErr(t *testing.T) { t.Setenv("KUBECONFIG", "foo") kc := new(Kubeconfig).WithLoader(DefaultLoader) err := kc.Parse() if err == nil { t.Fatal("expected err") } if !cmdutil.IsNotFoundErr(err) { t.Fatalf("expected ENOENT error; got=%v", err) } } ================================================ FILE: internal/kubeconfig/namespace.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubeconfig import ( "sigs.k8s.io/kustomize/kyaml/yaml" ) const ( defaultNamespace = "default" ) func (k *Kubeconfig) NamespaceOfContext(contextName string) (string, error) { ctx, err := k.contextNode(contextName) if err != nil { return "", err } namespace, err := ctx.Pipe(yaml.Lookup("context", "namespace")) if namespace == nil || err != nil { return defaultNamespace, err } return yaml.GetValue(namespace), nil } func (k *Kubeconfig) SetNamespace(ctxName string, ns string) error { ctx, err := k.contextNode(ctxName) if err != nil { return err } if err := ctx.PipeE( yaml.LookupCreate(yaml.MappingNode, "context"), yaml.SetField("namespace", yaml.NewStringRNode(ns)), ); err != nil { return err } return nil } ================================================ FILE: internal/kubeconfig/namespace_test.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubeconfig import ( "testing" "github.com/google/go-cmp/cmp" "github.com/ahmetb/kubectx/internal/testutil" ) func TestKubeconfig_NamespaceOfContext_ctxNotFound(t *testing.T) { kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC(). WithCtxs(testutil.Ctx("c1")).ToYAML(t))) if err := kc.Parse(); err != nil { t.Fatal(err) } _, err := kc.NamespaceOfContext("c2") if err == nil { t.Fatal("expected err") } } func TestKubeconfig_NamespaceOfContext(t *testing.T) { kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC(). WithCtxs( testutil.Ctx("c1"), testutil.Ctx("c2").Ns("c2n1")).ToYAML(t))) if err := kc.Parse(); err != nil { t.Fatal(err) } v1, err := kc.NamespaceOfContext("c1") if err != nil { t.Fatal("expected err") } if expected := `default`; v1 != expected { t.Fatalf("c1: expected=\"%s\" got=\"%s\"", expected, v1) } v2, err := kc.NamespaceOfContext("c2") if err != nil { t.Fatal("expected err") } if expected := `c2n1`; v2 != expected { t.Fatalf("c2: expected=\"%s\" got=\"%s\"", expected, v2) } } func TestKubeconfig_SetNamespace(t *testing.T) { l := WithMockKubeconfigLoader(testutil.KC(). WithCtxs( testutil.Ctx("c1"), testutil.Ctx("c2").Ns("c2n1")).ToYAML(t)) kc := new(Kubeconfig).WithLoader(l) if err := kc.Parse(); err != nil { t.Fatal(err) } if err := kc.SetNamespace("c3", "foo"); err == nil { t.Fatalf("expected error for non-existing ctx") } if err := kc.SetNamespace("c1", "c1n1"); err != nil { t.Fatal(err) } if err := kc.SetNamespace("c2", "c2n2"); err != nil { t.Fatal(err) } if err := kc.Save(); err != nil { t.Fatal(err) } expected := testutil.KC().WithCtxs( testutil.Ctx("c1").Ns("c1n1"), testutil.Ctx("c2").Ns("c2n2")).ToYAML(t) if diff := cmp.Diff(l.Output(), expected); diff != "" { t.Fatal(diff) } } ================================================ FILE: internal/printer/color.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package printer import ( "os" "github.com/fatih/color" "github.com/ahmetb/kubectx/internal/env" ) var ( ActiveItemColor = color.New(color.FgGreen, color.Bold) ) func init() { EnableOrDisableColor(ActiveItemColor) } // useColors returns true if colors are force-enabled, // false if colors are disabled, or nil for default behavior // which is determined based on factors like if stdout is tty. func useColors() *bool { tr, fa := true, false if os.Getenv(env.EnvForceColor) != "" { return &tr } else if os.Getenv(env.EnvNoColor) != "" { return &fa } return nil } // EnableOrDisableColor determines if color should be force-enabled or force-disabled // or left untouched based on environment configuration. func EnableOrDisableColor(c *color.Color) { if v := useColors(); v != nil && *v { c.EnableColor() } else if v != nil && !*v { c.DisableColor() } } ================================================ FILE: internal/printer/color_test.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package printer import ( "testing" "github.com/google/go-cmp/cmp" ) var ( tr, fa = true, false ) func Test_useColors_forceColors(t *testing.T) { t.Setenv("_KUBECTX_FORCE_COLOR", "1") t.Setenv("NO_COLOR", "1") if v := useColors(); !cmp.Equal(v, &tr) { t.Fatalf("expected useColors() = true; got = %v", v) } } func Test_useColors_disableColors(t *testing.T) { t.Setenv("NO_COLOR", "1") if v := useColors(); !cmp.Equal(v, &fa) { t.Fatalf("expected useColors() = false; got = %v", v) } } func Test_useColors_default(t *testing.T) { t.Setenv("NO_COLOR", "") t.Setenv("_KUBECTX_FORCE_COLOR", "") if v := useColors(); v != nil { t.Fatalf("expected useColors() = nil; got=%v", *v) } } ================================================ FILE: internal/printer/printer.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package printer import ( "fmt" "io" "github.com/fatih/color" ) var ( ErrorColor = color.New(color.FgRed, color.Bold) WarningColor = color.New(color.FgYellow, color.Bold) SuccessColor = color.New(color.FgGreen) ) func init() { colors := useColors() if colors == nil { return } if *colors { ErrorColor.EnableColor() WarningColor.EnableColor() SuccessColor.EnableColor() } else { ErrorColor.DisableColor() WarningColor.DisableColor() SuccessColor.DisableColor() } } func Error(w io.Writer, format string, args ...any) error { _, err := io.WriteString(w, ErrorColor.Sprint("error: ")+fmt.Sprintf(format, args...)+"\n") return err } func Warning(w io.Writer, format string, args ...any) error { _, err := io.WriteString(w, WarningColor.Sprint("warning: ")+fmt.Sprintf(format, args...)+"\n") return err } func Success(w io.Writer, format string, args ...any) error { _, err := io.WriteString(w, SuccessColor.Sprint("✔ ")+fmt.Sprintf(format, args...)+"\n") return err } ================================================ FILE: internal/testutil/kubeconfigbuilder.go ================================================ // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package testutil import ( "strings" "testing" "sigs.k8s.io/kustomize/kyaml/yaml" ) type Context struct { Name string `yaml:"name,omitempty"` Context struct { Namespace string `yaml:"namespace,omitempty"` } `yaml:"context,omitempty"` } func Ctx(name string) *Context { return &Context{Name: name} } func (c *Context) Ns(ns string) *Context { c.Context.Namespace = ns; return c } type Kubeconfig map[string]any func KC() *Kubeconfig { return &Kubeconfig{ "apiVersion": "v1", "kind": "Config"} } func (k *Kubeconfig) Set(key string, v any) *Kubeconfig { (*k)[key] = v; return k } func (k *Kubeconfig) WithCurrentCtx(s string) *Kubeconfig { (*k)["current-context"] = s; return k } func (k *Kubeconfig) WithCtxs(c ...*Context) *Kubeconfig { (*k)["contexts"] = c; return k } func (k *Kubeconfig) ToYAML(t *testing.T) string { t.Helper() var v strings.Builder enc := yaml.NewEncoder(&v) enc.SetIndent(0) if err := enc.Encode(*k); err != nil { t.Fatalf("failed to encode mock kubeconfig: %v", err) } return v.String() } ================================================ FILE: kubectx ================================================ #!/usr/bin/env bash # # kubectx(1) is a utility to manage and switch between kubectl contexts. # Copyright 2017 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. [[ -n $DEBUG ]] && set -x set -eou pipefail IFS=$'\n\t' SELF_CMD="$0" KUBECTX="${XDG_CACHE_HOME:-$HOME/.kube}/kubectx" usage() { local SELF SELF="kubectx" if [[ "$(basename "$0")" == kubectl-* ]]; then # invoked as plugin SELF="kubectl ctx" fi cat < : switch to context $SELF - : switch to the previous context $SELF -c, --current : show the current context name $SELF = : rename context to $SELF =. : rename current-context to $SELF -d [] : delete context ('.' for current-context) (this command won't delete the user/cluster entry that is used by the context) $SELF -u, --unset : unset the current context $SELF -h,--help : show this message (This executable is the legacy bash-based implementation, consider upgrading to Go-based implementation.) EOF } exit_err() { echo >&2 "${1}" exit 1 } current_context() { $KUBECTL config view -o=jsonpath='{.current-context}' } get_contexts() { $KUBECTL config get-contexts -o=name | sort -n } list_contexts() { set -u pipefail local cur ctx_list cur="$(current_context)" || exit_err "error getting current context" ctx_list=$(get_contexts) || exit_err "error getting context list" local yellow darkbg normal yellow=$(tput setaf 3 || true) darkbg=$(tput setab 0 || true) normal=$(tput sgr0 || true) local cur_ctx_fg cur_ctx_bg cur_ctx_fg=${KUBECTX_CURRENT_FGCOLOR:-$yellow} cur_ctx_bg=${KUBECTX_CURRENT_BGCOLOR:-$darkbg} for c in $ctx_list; do if [[ -n "${_KUBECTX_FORCE_COLOR:-}" || \ -t 1 && -z "${NO_COLOR:-}" ]]; then # colored output mode if [[ "${c}" = "${cur}" ]]; then echo "${cur_ctx_bg}${cur_ctx_fg}${c}${normal}" else echo "${c}" fi else echo "${c}" fi done } read_context() { if [[ -f "${KUBECTX}" ]]; then cat "${KUBECTX}" fi } save_context() { local saved saved="$(read_context)" if [[ "${saved}" != "${1}" ]]; then printf %s "${1}" > "${KUBECTX}" fi } switch_context() { $KUBECTL config use-context "${1}" } choose_context_interactive() { local choice choice="$(_KUBECTX_FORCE_COLOR=1 \ FZF_DEFAULT_COMMAND="${SELF_CMD}" \ fzf --ansi --no-preview || true)" if [[ -z "${choice}" ]]; then echo 2>&1 "error: you did not choose any of the options" exit 1 else set_context "${choice}" fi } set_context() { local prev prev="$(current_context)" || exit_err "error getting current context" switch_context "${1}" if [[ "${prev}" != "${1}" ]]; then save_context "${prev}" fi } swap_context() { local ctx ctx="$(read_context)" if [[ -z "${ctx}" ]]; then echo "error: No previous context found." >&2 exit 1 fi set_context "${ctx}" } context_exists() { grep -q ^"${1}"\$ <($KUBECTL config get-contexts -o=name) } rename_context() { local old_name="${1}" local new_name="${2}" if [[ "${old_name}" == "." ]]; then old_name="$(current_context)" fi if ! context_exists "${old_name}"; then echo "error: Context \"${old_name}\" not found, can't rename it." >&2 exit 1 fi if context_exists "${new_name}"; then echo "Context \"${new_name}\" exists, deleting..." >&2 $KUBECTL config delete-context "${new_name}" 1>/dev/null 2>&1 fi $KUBECTL config rename-context "${old_name}" "${new_name}" } delete_contexts() { for i in "${@}"; do delete_context "${i}" done } delete_context() { local ctx ctx="${1}" if [[ "${ctx}" == "." ]]; then ctx="$(current_context)" || exit_err "error getting current context" fi echo "Deleting context \"${ctx}\"..." >&2 $KUBECTL config delete-context "${ctx}" } unset_context() { echo "Unsetting current context." >&2 $KUBECTL config unset current-context } main() { if [[ -z "${KUBECTL:-}" ]]; then if hash kubectl 2>/dev/null; then KUBECTL=kubectl elif hash kubectl.exe 2>/dev/null; then KUBECTL=kubectl.exe else echo >&2 "kubectl is not installed" exit 1 fi fi if [[ "$#" -eq 0 ]]; then if [[ -t 1 && -z "${KUBECTX_IGNORE_FZF:-}" && "$(type fzf &>/dev/null; echo $?)" -eq 0 ]]; then choose_context_interactive else list_contexts fi elif [[ "${1}" == "-d" ]]; then if [[ "$#" -lt 2 ]]; then echo "error: missing context NAME" >&2 usage exit 1 fi delete_contexts "${@:2}" elif [[ "$#" -gt 1 ]]; then echo "error: too many arguments" >&2 usage exit 1 elif [[ "$#" -eq 1 ]]; then if [[ "${1}" == "-" ]]; then swap_context elif [[ "${1}" == '-c' || "${1}" == '--current' ]]; then # we don't call current_context here for two reasons: # - it does not fail when current-context property is not set # - it does not return a trailing newline $KUBECTL config current-context elif [[ "${1}" == '-u' || "${1}" == '--unset' ]]; then unset_context elif [[ "${1}" == '-h' || "${1}" == '--help' ]]; then usage elif [[ "${1}" =~ ^-(.*) ]]; then echo "error: unrecognized flag \"${1}\"" >&2 usage exit 1 elif [[ "${1}" =~ (.+)=(.+) ]]; then rename_context "${BASH_REMATCH[2]}" "${BASH_REMATCH[1]}" else set_context "${1}" fi else usage exit 1 fi } main "$@" ================================================ FILE: kubens ================================================ #!/usr/bin/env bash # # kubens(1) is a utility to switch between Kubernetes namespaces. # Copyright 2017 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. [[ -n $DEBUG ]] && set -x set -eou pipefail IFS=$'\n\t' SELF_CMD="$0" KUBENS_DIR="${XDG_CACHE_HOME:-$HOME/.kube}/kubens" usage() { local SELF SELF="kubens" if [[ "$(basename "$0")" == kubectl-* ]]; then # invoked as plugin SELF="kubectl ns" fi cat < : change the active namespace of current context $SELF - : switch to the previous namespace in this context $SELF -c, --current : show the current namespace $SELF -h,--help : show this message (This executable is the legacy bash-based implementation, consider upgrading to Go-based implementation.) EOF } exit_err() { echo >&2 "${1}" exit 1 } current_namespace() { local cur_ctx cur_ctx="$(current_context)" || exit_err "error getting current context" ns="$($KUBECTL config view -o=jsonpath="{.contexts[?(@.name==\"${cur_ctx}\")].context.namespace}")" \ || exit_err "error getting current namespace" if [[ -z "${ns}" ]]; then echo "default" else echo "${ns}" fi } current_context() { $KUBECTL config current-context } get_namespaces() { $KUBECTL get namespaces -o=jsonpath='{range .items[*].metadata.name}{@}{"\n"}{end}' } escape_context_name() { echo "${1//\//-}" } namespace_file() { local ctx ctx="$(escape_context_name "${1}")" echo "${KUBENS_DIR}/${ctx}" } read_namespace() { local f f="$(namespace_file "${1}")" [[ -f "${f}" ]] && cat "${f}" return 0 } save_namespace() { mkdir -p "${KUBENS_DIR}" local f saved f="$(namespace_file "${1}")" saved="$(read_namespace "${1}")" if [[ "${saved}" != "${2}" ]]; then printf %s "${2}" > "${f}" fi } switch_namespace() { local ctx="${1}" $KUBECTL config set-context "${ctx}" --namespace="${2}" echo "Active namespace is \"${2}\".">&2 } choose_namespace_interactive() { # directly calling kubens via fzf might fail with a cryptic error like # "$FZF_DEFAULT_COMMAND failed", so try to see if we can list namespaces # locally first if [[ -z "$(list_namespaces)" ]]; then echo >&2 "error: could not list namespaces (is the cluster accessible?)" exit 1 fi local choice choice="$(_KUBECTX_FORCE_COLOR=1 \ FZF_DEFAULT_COMMAND="${SELF_CMD}" \ fzf --ansi --no-preview || true)" if [[ -z "${choice}" ]]; then echo 2>&1 "error: you did not choose any of the options" exit 1 else set_namespace "${choice}" fi } set_namespace() { local ctx prev ctx="$(current_context)" || exit_err "error getting current context" prev="$(current_namespace)" || exit_error "error getting current namespace" if grep -q ^"${1}"\$ <(get_namespaces); then switch_namespace "${ctx}" "${1}" if [[ "${prev}" != "${1}" ]]; then save_namespace "${ctx}" "${prev}" fi else echo "error: no namespace exists with name \"${1}\".">&2 exit 1 fi } list_namespaces() { local yellow darkbg normal yellow=$(tput setaf 3 || true) darkbg=$(tput setab 0 || true) normal=$(tput sgr0 || true) local cur_ctx_fg cur_ctx_bg cur_ctx_fg=${KUBECTX_CURRENT_FGCOLOR:-$yellow} cur_ctx_bg=${KUBECTX_CURRENT_BGCOLOR:-$darkbg} local cur ns_list cur="$(current_namespace)" || exit_err "error getting current namespace" ns_list=$(get_namespaces) || exit_err "error getting namespace list" for c in $ns_list; do if [[ -n "${_KUBECTX_FORCE_COLOR:-}" || \ -t 1 && -z "${NO_COLOR:-}" ]]; then # colored output mode if [[ "${c}" = "${cur}" ]]; then echo "${cur_ctx_bg}${cur_ctx_fg}${c}${normal}" else echo "${c}" fi else echo "${c}" fi done } swap_namespace() { local ctx ns ctx="$(current_context)" || exit_err "error getting current context" ns="$(read_namespace "${ctx}")" if [[ -z "${ns}" ]]; then echo "error: No previous namespace found for current context." >&2 exit 1 fi set_namespace "${ns}" } main() { if [[ -z "${KUBECTL:-}" ]]; then if hash kubectl 2>/dev/null; then KUBECTL=kubectl elif hash kubectl.exe 2>/dev/null; then KUBECTL=kubectl.exe else echo >&2 "kubectl is not installed" exit 1 fi fi if [[ "$#" -eq 0 ]]; then if [[ -t 1 && -z ${KUBECTX_IGNORE_FZF:-} && "$(type fzf &>/dev/null; echo $?)" -eq 0 ]]; then choose_namespace_interactive else list_namespaces fi elif [[ "$#" -eq 1 ]]; then if [[ "${1}" == '-h' || "${1}" == '--help' ]]; then usage elif [[ "${1}" == "-" ]]; then swap_namespace elif [[ "${1}" == '-c' || "${1}" == '--current' ]]; then current_namespace elif [[ "${1}" =~ ^-(.*) ]]; then echo "error: unrecognized flag \"${1}\"" >&2 usage exit 1 elif [[ "${1}" =~ (.+)=(.+) ]]; then alias_context "${BASH_REMATCH[2]}" "${BASH_REMATCH[1]}" else set_namespace "${1}" fi else echo "error: too many flags" >&2 usage exit 1 fi } main "$@" ================================================ FILE: test/common.bash ================================================ #!/usr/bin/env bats # bats setup function setup() { TEMP_HOME="$(mktemp -d)" export TEMP_HOME export HOME=$TEMP_HOME export KUBECONFIG="${TEMP_HOME}/config" } # bats teardown function teardown() { rm -rf "$TEMP_HOME" } use_config() { cp "$BATS_TEST_DIRNAME/testdata/$1" $KUBECONFIG } # wrappers around "kubectl config" command get_namespace() { kubectl config view -o=jsonpath="{.contexts[?(@.name==\"$(get_context)\")].context.namespace}" } get_context() { kubectl config current-context } switch_context() { kubectl config use-context "${1}" } ================================================ FILE: test/kubectx.bats ================================================ #!/usr/bin/env bats COMMAND="${COMMAND:-$BATS_TEST_DIRNAME/../kubectx}" load common @test "--help should not fail" { run ${COMMAND} --help echo "$output" [ "$status" -eq 0 ] } @test "-h should not fail" { run ${COMMAND} -h echo "$output" [ "$status" -eq 0 ] } @test "switch to previous context when no one exists" { use_config config1 run ${COMMAND} - echo "$output" [ "$status" -eq 1 ] [[ $output = *"no previous context found" ]] } @test "list contexts when no kubeconfig exists" { run ${COMMAND} echo "$output" [ "$status" -eq 0 ] [[ "$output" = "warning: kubeconfig file not found" ]] } @test "get one context and list contexts" { use_config config1 run ${COMMAND} echo "$output" [ "$status" -eq 0 ] [[ "$output" = "user1@cluster1" ]] } @test "get two contexts and list contexts" { use_config config2 run ${COMMAND} echo "$output" [ "$status" -eq 0 ] [[ "$output" = *"user1@cluster1"* ]] [[ "$output" = *"user2@cluster1"* ]] } @test "get two contexts and select contexts" { use_config config2 run ${COMMAND} user1@cluster1 echo "$output" [ "$status" -eq 0 ] echo "$(get_context)" [[ "$(get_context)" = "user1@cluster1" ]] run ${COMMAND} user2@cluster1 echo "$output" [ "$status" -eq 0 ] echo "$(get_context)" [[ "$(get_context)" = "user2@cluster1" ]] } @test "get two contexts and switch between contexts" { use_config config2 run ${COMMAND} user1@cluster1 echo "$output" [ "$status" -eq 0 ] echo "$(get_context)" [[ "$(get_context)" = "user1@cluster1" ]] run ${COMMAND} user2@cluster1 echo "$output" [ "$status" -eq 0 ] echo "$(get_context)" [[ "$(get_context)" = "user2@cluster1" ]] run ${COMMAND} - echo "$output" [ "$status" -eq 0 ] echo "$(get_context)" [[ "$(get_context)" = "user1@cluster1" ]] run ${COMMAND} - echo "$output" [ "$status" -eq 0 ] echo "$(get_context)" [[ "$(get_context)" = "user2@cluster1" ]] } @test "get one context and switch to non existent context" { use_config config1 run ${COMMAND} "unknown-context" echo "$output" [ "$status" -eq 1 ] } @test "-c/--current fails when no context set" { use_config config1 run "${COMMAND}" -c echo "$output" [ $status -eq 1 ] run "${COMMAND}" --current echo "$output" [ $status -eq 1 ] } @test "-c/--current prints the current context" { use_config config1 run "${COMMAND}" user1@cluster1 [ $status -eq 0 ] run "${COMMAND}" -c echo "$output" [ $status -eq 0 ] [[ "$output" = "user1@cluster1" ]] run "${COMMAND}" --current echo "$output" [ $status -eq 0 ] [[ "$output" = "user1@cluster1" ]] } @test "rename context" { use_config config2 run ${COMMAND} "new-context=user1@cluster1" echo "$output" [ "$status" -eq 0 ] run ${COMMAND} echo "$output" [ "$status" -eq 0 ] [[ ! "$output" = *"user1@cluster1"* ]] [[ "$output" = *"new-context"* ]] [[ "$output" = *"user2@cluster1"* ]] } @test "rename current context" { use_config config2 run ${COMMAND} user2@cluster1 echo "$output" [ "$status" -eq 0 ] run ${COMMAND} new-context=. echo "$output" [ "$status" -eq 0 ] run ${COMMAND} echo "$output" [ "$status" -eq 0 ] [[ ! "$output" = *"user2@cluster1"* ]] [[ "$output" = *"user1@cluster1"* ]] [[ "$output" = *"new-context"* ]] } @test "delete context" { use_config config2 run ${COMMAND} -d "user1@cluster1" echo "$output" [ "$status" -eq 0 ] run ${COMMAND} echo "$output" [ "$status" -eq 0 ] [[ ! "$output" = "user1@cluster1" ]] [[ "$output" = "user2@cluster1" ]] } @test "delete current context" { use_config config2 run ${COMMAND} user2@cluster1 echo "$output" [ "$status" -eq 0 ] run ${COMMAND} -d . echo "$output" [ "$status" -eq 0 ] run ${COMMAND} echo "$output" [ "$status" -eq 0 ] [[ ! "$output" = "user2@cluster1" ]] [[ "$output" = "user1@cluster1" ]] } @test "delete non existent context" { use_config config1 run ${COMMAND} -d "unknown-context" echo "$output" [ "$status" -eq 1 ] } @test "delete several contexts" { use_config config2 run ${COMMAND} -d "user1@cluster1" "user2@cluster1" echo "$output" [ "$status" -eq 0 ] run ${COMMAND} echo "$output" [ "$status" -eq 0 ] [[ "$output" = "" ]] } @test "delete several contexts including a non existent one" { use_config config2 run ${COMMAND} -d "user1@cluster1" "non-existent" "user2@cluster1" echo "$output" [ "$status" -eq 1 ] run ${COMMAND} echo "$output" [ "$status" -eq 0 ] [[ "$output" = "user2@cluster1" ]] } @test "unset selected context" { use_config config2 run ${COMMAND} user1@cluster1 [ "$status" -eq 0 ] run ${COMMAND} -u [ "$status" -eq 0 ] run ${COMMAND} -c [ "$status" -ne 0 ] } ================================================ FILE: test/kubens.bats ================================================ #!/usr/bin/env bats COMMAND="${COMMAND:-$BATS_TEST_DIRNAME/../kubens}" # TODO(ahmetb) remove this after bash implementations are deleted export KUBECTL="$BATS_TEST_DIRNAME/../test/mock-kubectl" # short-circuit namespace querying in kubens go implementation export _MOCK_NAMESPACES=1 load common @test "--help should not fail" { run ${COMMAND} --help echo "$output">&2 [[ "$status" -eq 0 ]] } @test "-h should not fail" { run ${COMMAND} -h echo "$output">&2 [[ "$status" -eq 0 ]] } @test "list namespaces when no kubeconfig exists" { run ${COMMAND} echo "$output" [[ "$status" -eq 1 ]] } @test "list namespaces" { use_config config1 switch_context user1@cluster1 run ${COMMAND} echo "$output" [[ "$status" -eq 0 ]] [[ "$output" = *"ns1"* ]] [[ "$output" = *"ns2"* ]] } @test "switch to existing namespace" { use_config config1 switch_context user1@cluster1 run ${COMMAND} "ns1" echo "$output" [[ "$status" -eq 0 ]] [[ "$output" = *'Active namespace is "ns1"'* ]] } @test "switch to non-existing namespace" { use_config config1 switch_context user1@cluster1 run ${COMMAND} "unknown-namespace" echo "$output" [[ "$status" -eq 1 ]] [[ "$output" = *'no namespace exists with name "unknown-namespace"'* ]] } @test "switch between namespaces" { use_config config1 switch_context user1@cluster1 run ${COMMAND} ns1 echo "$output" [[ "$status" -eq 0 ]] echo "$(get_namespace)" [[ "$(get_namespace)" = "ns1" ]] run ${COMMAND} ns2 echo "$output" [[ "$status" -eq 0 ]] echo "$(get_namespace)" [[ "$(get_namespace)" = "ns2" ]] run ${COMMAND} - echo "$output" [[ "$status" -eq 0 ]] echo "$(get_namespace)" [[ "$(get_namespace)" = "ns1" ]] run ${COMMAND} - echo "$output" [[ "$status" -eq 0 ]] echo "$(get_namespace)" [[ "$(get_namespace)" = "ns2" ]] } @test "switch to previous namespace when none exists" { use_config config1 switch_context user1@cluster1 run ${COMMAND} - echo "$output" [[ "$status" -eq 1 ]] [[ "$output" = *"No previous namespace found for current context"* ]] } @test "switch to namespace when current context is empty" { use_config config1 run ${COMMAND} - echo "$output" [[ "$status" -eq 1 ]] [[ "$output" = *"current-context is not set"* ]] } @test "-c/--current works when no namespace is set on context" { use_config config1 switch_context user1@cluster1 run ${COMMAND} "-c" echo "$output" [[ "$status" -eq 0 ]] [[ "$output" = "default" ]] run ${COMMAND} "--current" echo "$output" [[ "$status" -eq 0 ]] [[ "$output" = "default" ]] } @test "-c/--current prints the namespace after it is set" { use_config config1 switch_context user1@cluster1 ${COMMAND} ns1 run ${COMMAND} "-c" echo "$output" [[ "$status" -eq 0 ]] [[ "$output" = "ns1" ]] run ${COMMAND} "--current" echo "$output" [[ "$status" -eq 0 ]] [[ "$output" = "ns1" ]] } @test "-c/--current fails when current context is not set" { use_config config1 run ${COMMAND} -c echo "$output" [[ "$status" -eq 1 ]] run ${COMMAND} --current echo "$output" [[ "$status" -eq 1 ]] } ================================================ FILE: test/mock-kubectl ================================================ #!/usr/bin/env bash [[ -n $DEBUG ]] && set -x set -eou pipefail if [[ $@ == *'get namespaces'* ]]; then echo "ns1" echo "ns2" else kubectl $@ fi ================================================ FILE: test/testdata/config1 ================================================ # config with one context apiVersion: v1 clusters: - cluster: server: "" name: cluster1 contexts: - context: cluster: cluster1 user: user1 name: user1@cluster1 current-context: "" kind: Config preferences: {} users: - name: user1 user: {} ================================================ FILE: test/testdata/config2 ================================================ # config with two contexts apiVersion: v1 clusters: - cluster: server: "" name: cluster1 contexts: - context: cluster: cluster1 user: user1 name: user1@cluster1 - context: cluster: cluster1 user: user2 name: user2@cluster1 current-context: "" kind: Config preferences: {} users: - name: user1 user: {} - name: user2 user: {}