pax_global_header 0000666 0000000 0000000 00000000064 15161372462 0014521 g ustar 00root root 0000000 0000000 52 comment=7605ca50c42eab0937bd3d4de825e0bcd120ba1f
kubectx-0.11.0/ 0000775 0000000 0000000 00000000000 15161372462 0013245 5 ustar 00root root 0000000 0000000 kubectx-0.11.0/.github/ 0000775 0000000 0000000 00000000000 15161372462 0014605 5 ustar 00root root 0000000 0000000 kubectx-0.11.0/.github/dependabot.yml 0000664 0000000 0000000 00000000541 15161372462 0017435 0 ustar 00root root 0000000 0000000 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/*"
kubectx-0.11.0/.github/workflows/ 0000775 0000000 0000000 00000000000 15161372462 0016642 5 ustar 00root root 0000000 0000000 kubectx-0.11.0/.github/workflows/bash-frozen.yml 0000664 0000000 0000000 00000002051 15161372462 0021601 0 ustar 00root root 0000000 0000000 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@v8
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
});
kubectx-0.11.0/.github/workflows/ci.yml 0000664 0000000 0000000 00000004036 15161372462 0017763 0 ustar 00root root 0000000 0000000 # 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 "go-build=$(go env GOCACHE)" >> "$GITHUB_OUTPUT"
echo "go-mod=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT"
- 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
kubectx-0.11.0/.github/workflows/dependabot.yml 0000664 0000000 0000000 00000001117 15161372462 0021472 0 ustar 00root root 0000000 0000000 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 }}
kubectx-0.11.0/.github/workflows/release.yml 0000664 0000000 0000000 00000003625 15161372462 0021013 0 ustar 00root root 0000000 0000000 # 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@v6
with:
fetch-depth: 0
- 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 }}
kubectx-0.11.0/.goreleaser.yml 0000664 0000000 0000000 00000005441 15161372462 0016202 0 ustar 00root root 0000000 0000000 # 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
kubectx-0.11.0/.krew/ 0000775 0000000 0000000 00000000000 15161372462 0014273 5 ustar 00root root 0000000 0000000 kubectx-0.11.0/.krew/ctx.yaml 0000664 0000000 0000000 00000001670 15161372462 0015761 0 ustar 00root root 0000000 0000000 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: .
kubectx-0.11.0/.krew/ns.yaml 0000664 0000000 0000000 00000001525 15161372462 0015602 0 ustar 00root root 0000000 0000000 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: .
kubectx-0.11.0/CONTRIBUTING.md 0000664 0000000 0000000 00000001710 15161372462 0015475 0 ustar 00root root 0000000 0000000 # 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. kubectx-0.11.0/LICENSE 0000664 0000000 0000000 00000026135 15161372462 0014261 0 ustar 00root root 0000000 0000000 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.
kubectx-0.11.0/README.md 0000664 0000000 0000000 00000016204 15161372462 0014527 0 ustar 00root root 0000000 0000000 # `kubectx` + `kubens`: Power tools for kubectl



[/badge.svg)](https://github.com/ahmetb/kubectx/actions?query=workflow%3A"Go+implementation+(CI)")
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, and launch readonly shells for each context.
**kubens** is a tool to switch between Kubernetes namespaces (and
configure them for kubectl) easily.
Here's a **`kubectx`** demo:

...and here's a **`kubens`** demo:

### Usage
#### kubectx
Switch to another cluster that's in kubeconfig:
```sh
$ kubectx minikube
Switched to context "minikube".
```
Switch back to previous cluster:
```sh
$ kubectx -
Switched to context "oregon".
```
Start an isolated shell that only has a single context:
```sh
$ kubectx -s minikube
```
Start a read-only shell where write operations are blocked:
```sh
$ kubectx -r minikube
```
Rename context:
```sh
$ kubectx dublin=gke_ahmetb_europe-west1-b_dublin
Context "gke_ahmetb_europe-west1-b_dublin" renamed to "dublin".
```
#### kubens
Change the active namespace on kubectl:
```sh
$ kubens kube-system
Context "test" set.
Active namespace is "kube-system".
```
Go back to the previous namespace:
```sh
$ kubens -
Context "test" set.
Active namespace is "default".
```
Change the active namespace even if it doesn't exist:
```sh
$ kubens namespace-404 -f
Context "test" set.
Active namespace is "namespace-404".
```
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`.

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
[](https://starchart.cc/ahmetb/kubectx)

kubectx-0.11.0/cmd/ 0000775 0000000 0000000 00000000000 15161372462 0014010 5 ustar 00root root 0000000 0000000 kubectx-0.11.0/cmd/kubectx/ 0000775 0000000 0000000 00000000000 15161372462 0015455 5 ustar 00root root 0000000 0000000 kubectx-0.11.0/cmd/kubectx/current.go 0000664 0000000 0000000 00000002476 15161372462 0017477 0 ustar 00root root 0000000 0000000 // 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
}
kubectx-0.11.0/cmd/kubectx/delete.go 0000664 0000000 0000000 00000005337 15161372462 0017256 0 ustar 00root root 0000000 0000000 // 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
}
kubectx-0.11.0/cmd/kubectx/env.go 0000664 0000000 0000000 00000001132 15161372462 0016571 0 ustar 00root root 0000000 0000000 // 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
kubectx-0.11.0/cmd/kubectx/flags.go 0000664 0000000 0000000 00000005572 15161372462 0017111 0 ustar 00root root 0000000 0000000 // 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] == "--readonly" || argv[0] == "-r" {
if len(argv) == 1 {
if cmdutil.IsInteractiveMode(os.Stdout) {
return InteractiveReadonlyShellOp{SelfCmd: os.Args[0]}
}
return UnsupportedOp{Err: fmt.Errorf("'%s' requires a context name argument (or fzf for interactive mode)", argv[0])}
}
if len(argv) == 2 {
return ReadonlyShellOp{Target: argv[1]}
}
return UnsupportedOp{Err: fmt.Errorf("'%s' accepts at most one context name argument", argv[0])}
}
if argv[0] == "--shell" || argv[0] == "-s" {
if len(argv) == 1 {
if cmdutil.IsInteractiveMode(os.Stdout) {
return InteractiveShellOp{SelfCmd: os.Args[0]}
}
return UnsupportedOp{Err: fmt.Errorf("'%s' requires a context name argument (or fzf for interactive mode)", argv[0])}
}
if len(argv) == 2 {
return ShellOp{Target: argv[1]}
}
return UnsupportedOp{Err: fmt.Errorf("'%s' accepts at most one context name argument", argv[0])}
}
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")}
}
kubectx-0.11.0/cmd/kubectx/flags_test.go 0000664 0000000 0000000 00000007474 15161372462 0020153 0 ustar 00root root 0000000 0000000 // 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 a context name argument (or fzf for interactive mode)")}},
{name: "shell with too many args",
args: []string{"--shell", "a", "b"},
want: UnsupportedOp{Err: fmt.Errorf("'--shell' accepts at most one context name argument")}},
{name: "readonly shorthand",
args: []string{"-r", "prod"},
want: ReadonlyShellOp{Target: "prod"}},
{name: "readonly long form",
args: []string{"--readonly", "prod"},
want: ReadonlyShellOp{Target: "prod"}},
{name: "readonly without context name",
args: []string{"-r"},
want: UnsupportedOp{Err: fmt.Errorf("'-r' requires a context name argument (or fzf for interactive mode)")}},
{name: "readonly with too many args",
args: []string{"--readonly", "a", "b"},
want: UnsupportedOp{Err: fmt.Errorf("'--readonly' accepts at most 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)
}
})
}
}
kubectx-0.11.0/cmd/kubectx/fzf.go 0000664 0000000 0000000 00000007515 15161372462 0016601 0 ustar 00root root 0000000 0000000 // 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
}
kubectx-0.11.0/cmd/kubectx/help.go 0000664 0000000 0000000 00000004701 15161372462 0016736 0 ustar 00root root 0000000 0000000 // 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 := `Manage and switch between kubectl contexts.
USAGE:
%PROG% : list the contexts
%PROG% : switch to context
%PROG% - : switch to the previous context
%PROG% -s, --shell : start a shell scoped to context
%PROG% -s, --shell : interactively select a context to start a shell
%PROG% -r, --readonly : start a read-only shell for context
%PROG% -r, --readonly : interactively select a context for read-only shell
%PROG% -c, --current : show the current context name
%PROG% -u, --unset : unset the current context
%PROG% = : rename context to
%PROG% =. : rename current-context to
%PROG% -d [] : delete context ('.' for current-context)
%SPAC% (this command won't delete the user/cluster entry
%SPAC% referenced by the context entry)
%PROG% -h, --help : show this message
%PROG% -V, --version : show version`
help = strings.ReplaceAll(help, "%PROG%", selfName())
help = strings.ReplaceAll(help, "%SPAC%", strings.Repeat(" ", len(selfName())))
_, err := fmt.Fprintf(out, "%s\n", help)
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"
}
kubectx-0.11.0/cmd/kubectx/help_test.go 0000664 0000000 0000000 00000001763 15161372462 0020002 0 ustar 00root root 0000000 0000000 // 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)
}
}
kubectx-0.11.0/cmd/kubectx/isolated_shell_guard.go 0000664 0000000 0000000 00000001076 15161372462 0022165 0 ustar 00root root 0000000 0000000 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)
}
kubectx-0.11.0/cmd/kubectx/list.go 0000664 0000000 0000000 00000003163 15161372462 0016762 0 ustar 00root root 0000000 0000000 // 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
}
kubectx-0.11.0/cmd/kubectx/main.go 0000664 0000000 0000000 00000002436 15161372462 0016735 0 ustar 00root root 0000000 0000000 // kubectx(1) is a utility to manage and switch between kubectl contexts.
// 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)
}
}
kubectx-0.11.0/cmd/kubectx/readonly_shell.go 0000664 0000000 0000000 00000005166 15161372462 0021020 0 ustar 00root root 0000000 0000000 package main
import (
"context"
"fmt"
"io"
"os"
"time"
"github.com/fatih/color"
"github.com/ahmetb/kubectx/internal/env"
"github.com/ahmetb/kubectx/internal/printer"
"github.com/ahmetb/kubectx/internal/proxy"
)
// InteractiveReadonlyShellOp launches fzf to pick a context, then starts a readonly shell.
type InteractiveReadonlyShellOp struct {
SelfCmd string
}
// ReadonlyShellOp starts a read-only sub-shell for a context.
type ReadonlyShellOp struct {
Target string
}
func (op InteractiveReadonlyShellOp) Run(_, stderr io.Writer) error {
choice, err := fzfPickContext(op.SelfCmd, stderr)
if err != nil || choice == "" {
return err
}
return ReadonlyShellOp{Target: choice}.Run(nil, stderr)
}
func (op ReadonlyShellOp) Run(_, stderr io.Writer) error {
badgeColor := color.New(color.BgYellow, color.FgBlack, color.Bold)
printer.EnableOrDisableColor(badgeColor)
s := &shellSession{
target: op.Target,
extraEnv: []string{env.EnvReadonlyShell + "=1"},
printEntry: func(w io.Writer, ctxName string) {
fmt.Fprintf(w, "%s kubectl context is %s in READ-ONLY mode — type 'exit' to leave.\n",
badgeColor.Sprint("[READONLY SHELL]"), printer.WarningColor.Sprint(ctxName))
},
printExit: func(w io.Writer, prevCtx string) {
fmt.Fprintf(w, "%s kubectl context is now %s.\n",
badgeColor.Sprint("[READONLY SHELL EXITED]"), printer.WarningColor.Sprint(prevCtx))
},
transformKubeconfig: func(data []byte) ([]byte, func(), error) {
// Write original kubeconfig to temp file for the proxy to load TLS/auth.
origFile, err := os.CreateTemp("", "kubectx-readonly-orig-*.yaml")
if err != nil {
return nil, nil, fmt.Errorf("failed to create temp kubeconfig file: %w", err)
}
origPath := origFile.Name()
if _, err := origFile.Write(data); err != nil {
origFile.Close()
os.Remove(origPath)
return nil, nil, fmt.Errorf("failed to write temp kubeconfig: %w", err)
}
origFile.Close()
// Start the readonly proxy.
p, err := proxy.Start(proxy.Config{
KubeconfigPath: origPath,
ContextName: op.Target,
})
if err != nil {
os.Remove(origPath)
return nil, nil, fmt.Errorf("failed to start readonly proxy: %w", err)
}
// Rewrite kubeconfig to point to the proxy.
rewritten, err := proxy.RewriteKubeconfig(data, p.Addr())
if err != nil {
p.Shutdown(context.Background())
os.Remove(origPath)
return nil, nil, fmt.Errorf("failed to rewrite kubeconfig: %w", err)
}
time.Sleep(10 * time.Millisecond)
cleanup := func() {
p.Shutdown(context.Background())
os.Remove(origPath)
}
return rewritten, cleanup, nil
},
}
return s.run(stderr)
}
kubectx-0.11.0/cmd/kubectx/rename.go 0000664 0000000 0000000 00000005550 15161372462 0017260 0 ustar 00root root 0000000 0000000 // 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
}
kubectx-0.11.0/cmd/kubectx/rename_test.go 0000664 0000000 0000000 00000003112 15161372462 0020307 0 ustar 00root root 0000000 0000000 // 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)
}
})
}
}
kubectx-0.11.0/cmd/kubectx/shell.go 0000664 0000000 0000000 00000005057 15161372462 0017122 0 ustar 00root root 0000000 0000000 package main
import (
"fmt"
"io"
"os"
"os/exec"
"runtime"
"github.com/fatih/color"
"github.com/ahmetb/kubectx/internal/printer"
)
// InteractiveShellOp launches fzf to pick a context, then starts an isolated shell.
type InteractiveShellOp struct {
SelfCmd string
}
// ShellOp indicates intention to start a scoped sub-shell for a context.
type ShellOp struct {
Target string
}
func (op InteractiveShellOp) Run(_, stderr io.Writer) error {
choice, err := fzfPickContext(op.SelfCmd, stderr)
if err != nil || choice == "" {
return err
}
return ShellOp{Target: choice}.Run(nil, stderr)
}
func (op ShellOp) Run(_, stderr io.Writer) error {
badgeColor := color.New(color.BgRed, color.FgWhite, color.Bold)
printer.EnableOrDisableColor(badgeColor)
s := &shellSession{
target: op.Target,
printEntry: func(w io.Writer, ctxName string) {
fmt.Fprintf(w, "%s kubectl context is %s in this shell — type 'exit' to leave.\n",
badgeColor.Sprint("[ISOLATED SHELL]"), printer.WarningColor.Sprint(ctxName))
},
printExit: func(w io.Writer, prevCtx string) {
fmt.Fprintf(w, "%s kubectl context is now %s.\n",
badgeColor.Sprint("[ISOLATED SHELL EXITED]"), printer.WarningColor.Sprint(prevCtx))
},
}
return s.run(stderr)
}
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"
}
kubectx-0.11.0/cmd/kubectx/shell_session.go 0000664 0000000 0000000 00000010232 15161372462 0020654 0 ustar 00root root 0000000 0000000 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"
)
// shellSession holds the configuration for spawning an isolated sub-shell.
type shellSession struct {
target string
extraEnv []string // additional env vars beyond KUBECONFIG + KUBECTX_ISOLATED_SHELL
printEntry func(stderr io.Writer, ctxName string)
printExit func(stderr io.Writer, prevCtx string)
// transformKubeconfig optionally transforms the minified kubeconfig bytes
// before writing them to the shell's temp file. The returned cleanup func
// is called after the shell exits (e.g. to shut down a proxy).
// If nil, the kubeconfig is used as-is.
transformKubeconfig func(data []byte) (newData []byte, cleanup func(), err error)
}
func (s *shellSession) 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(s.target)
if err != nil {
return fmt.Errorf("failed to check context: %w", err)
}
if !exists {
return fmt.Errorf("no context exists with the name: %q", s.target)
}
previousCtx, err := kc.GetCurrentContext()
if err != nil {
return fmt.Errorf("failed to get current context: %w", err)
}
// Extract minimal kubeconfig for the target context.
data, err := extractMinimalKubeconfig(kubectlPath, s.target)
if err != nil {
return fmt.Errorf("failed to extract kubeconfig for context: %w", err)
}
// Optionally transform the kubeconfig (e.g. rewrite for readonly proxy).
var cleanup func()
if s.transformKubeconfig != nil {
data, cleanup, err = s.transformKubeconfig(data)
if err != nil {
return err
}
if cleanup != nil {
defer cleanup()
}
}
// Write kubeconfig 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.
s.printEntry(stderr, s.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(),
append([]string{
"KUBECONFIG=" + tmpPath,
env.EnvIsolatedShell + "=1",
}, s.extraEnv...)...,
)
_ = cmd.Run()
// Print exit message.
s.printExit(stderr, previousCtx)
return nil
}
// fzfPickContext launches fzf for interactive context selection.
func fzfPickContext(selfCmd string, stderr io.Writer) (string, 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)
}
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", 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")
}
return choice, nil
}
kubectx-0.11.0/cmd/kubectx/shell_test.go 0000664 0000000 0000000 00000005132 15161372462 0020153 0 ustar 00root root 0000000 0000000 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)
}
}
kubectx-0.11.0/cmd/kubectx/state.go 0000664 0000000 0000000 00000002764 15161372462 0017135 0 ustar 00root root 0000000 0000000 // 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)
}
kubectx-0.11.0/cmd/kubectx/state_test.go 0000664 0000000 0000000 00000005132 15161372462 0020164 0 ustar 00root root 0000000 0000000 // 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)
}
}
kubectx-0.11.0/cmd/kubectx/switch.go 0000664 0000000 0000000 00000005570 15161372462 0017314 0 ustar 00root root 0000000 0000000 // 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)
}
kubectx-0.11.0/cmd/kubectx/unset.go 0000664 0000000 0000000 00000002730 15161372462 0017144 0 ustar 00root root 0000000 0000000 // 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
}
kubectx-0.11.0/cmd/kubectx/version.go 0000664 0000000 0000000 00000000542 15161372462 0017472 0 ustar 00root root 0000000 0000000 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
}
kubectx-0.11.0/cmd/kubens/ 0000775 0000000 0000000 00000000000 15161372462 0015277 5 ustar 00root root 0000000 0000000 kubectx-0.11.0/cmd/kubens/current.go 0000664 0000000 0000000 00000002536 15161372462 0017316 0 ustar 00root root 0000000 0000000 // 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
}
kubectx-0.11.0/cmd/kubens/flags.go 0000664 0000000 0000000 00000004052 15161372462 0016723 0 ustar 00root root 0000000 0000000 // 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}
}
kubectx-0.11.0/cmd/kubens/flags_test.go 0000664 0000000 0000000 00000006042 15161372462 0017763 0 ustar 00root root 0000000 0000000 // 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)
}
})
}
}
kubectx-0.11.0/cmd/kubens/fzf.go 0000664 0000000 0000000 00000004444 15161372462 0016421 0 ustar 00root root 0000000 0000000 // 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
}
kubectx-0.11.0/cmd/kubens/help.go 0000664 0000000 0000000 00000003751 15161372462 0016564 0 ustar 00root root 0000000 0000000 // 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 := `Switch between Kubernetes namespaces.
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"
}
kubectx-0.11.0/cmd/kubens/list.go 0000664 0000000 0000000 00000007360 15161372462 0016607 0 ustar 00root root 0000000 0000000 // 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/rest"
"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) {
var cfg *rest.Config
var err error
if paths := kc.ConfigPaths(); len(paths) > 0 {
// Load from file paths so that client-go resolves relative paths
// (e.g. in exec credential plugins) relative to the kubeconfig directory.
//
// TODO: This re-reads and re-parses the kubeconfig files from disk via
// client-go, duplicating work already done by our kyaml-based loader.
// A better approach would be to extract the current context/cluster/user
// entries from the already-parsed multi-file kubeconfig and normalize
// relative paths in memory based on which file each entry was read from.
cfg, err = clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
&clientcmd.ClientConfigLoadingRules{Precedence: paths},
&clientcmd.ConfigOverrides{},
).ClientConfig()
} else {
// Fallback for in-memory configs (e.g. tests).
var b []byte
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)
}
kubectx-0.11.0/cmd/kubens/main.go 0000664 0000000 0000000 00000002426 15161372462 0016556 0 ustar 00root root 0000000 0000000 // kubens(1) is a utility to switch between Kubernetes namespaces.
// 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)
}
}
kubectx-0.11.0/cmd/kubens/statefile.go 0000664 0000000 0000000 00000003512 15161372462 0017607 0 ustar 00root root 0000000 0000000 // 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"
}
kubectx-0.11.0/cmd/kubens/statefile_test.go 0000664 0000000 0000000 00000003222 15161372462 0020644 0 ustar 00root root 0000000 0000000 // 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.")
}
}
kubectx-0.11.0/cmd/kubens/switch.go 0000664 0000000 0000000 00000006471 15161372462 0017137 0 ustar 00root root 0000000 0000000 // 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
}
kubectx-0.11.0/cmd/kubens/unset.go 0000664 0000000 0000000 00000003322 15161372462 0016764 0 ustar 00root root 0000000 0000000 // 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
}
kubectx-0.11.0/cmd/kubens/version.go 0000664 0000000 0000000 00000000542 15161372462 0017314 0 ustar 00root root 0000000 0000000 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
}
kubectx-0.11.0/completion/ 0000775 0000000 0000000 00000000000 15161372462 0015416 5 ustar 00root root 0000000 0000000 kubectx-0.11.0/completion/_kubectx.zsh 0000664 0000000 0000000 00000000765 15161372462 0017760 0 ustar 00root root 0000000 0000000 #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
kubectx-0.11.0/completion/_kubens.zsh 0000664 0000000 0000000 00000000206 15161372462 0017570 0 ustar 00root root 0000000 0000000 #compdef kubens kns=kubens
_arguments "1: :(- $(kubectl get namespaces -o=jsonpath='{range .items[*].metadata.name}{@}{"\n"}{end}'))"
kubectx-0.11.0/completion/kubectx.bash 0000664 0000000 0000000 00000000325 15161372462 0017722 0 ustar 00root root 0000000 0000000 _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
kubectx-0.11.0/completion/kubectx.fish 0000664 0000000 0000000 00000000576 15161372462 0017746 0 ustar 00root root 0000000 0000000 # 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"
kubectx-0.11.0/completion/kubens.bash 0000664 0000000 0000000 00000000376 15161372462 0017552 0 ustar 00root root 0000000 0000000 _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
kubectx-0.11.0/completion/kubens.fish 0000664 0000000 0000000 00000001127 15161372462 0017561 0 ustar 00root root 0000000 0000000 # 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"
kubectx-0.11.0/go.mod 0000664 0000000 0000000 00000004252 15161372462 0014356 0 ustar 00root root 0000000 0000000 module github.com/ahmetb/kubectx
go 1.25.0
require (
facette.io/natsort v0.0.0-20181210072756-2cd4dd1e2dcb
github.com/fatih/color v1.19.0
github.com/google/go-cmp v0.7.0
github.com/mattn/go-isatty v0.0.20
k8s.io/apimachinery v0.35.3
k8s.io/client-go v0.35.3
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.14 // 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.42.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.3 // 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
)
kubectx-0.11.0/go.sum 0000664 0000000 0000000 00000030227 15161372462 0014404 0 ustar 00root root 0000000 0000000 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.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
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.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
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.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ=
k8s.io/api v0.35.3/go.mod h1:9Y9tkBcFwKNq2sxwZTQh1Njh9qHl81D0As56tu42GA4=
k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8=
k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg=
k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c=
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=
kubectx-0.11.0/img/ 0000775 0000000 0000000 00000000000 15161372462 0014021 5 ustar 00root root 0000000 0000000 kubectx-0.11.0/img/kubectx-demo.gif 0000664 0000000 0000000 00000505464 15161372462 0017115 0 ustar 00root root 0000000 0000000 GIF89aW "#$CCD;<