Compare commits

..

30 Commits

Author SHA1 Message Date
Ahmet Alp Balkan
f500964e24 fix: skip fzf launch in kubens when no contexts exist in kubeconfig (#481)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 07:30:21 +00:00
Ahmet Alp Balkan
039f5ed1ef fix: skip fzf launch when no contexts exist in kubeconfig (#480)
Return an error early in InteractiveSwitchOp if the kubeconfig has no
contexts, instead of launching fzf with an empty list.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 06:54:27 +00:00
Ahmet Alp Balkan
81defc835f refactor: replace testutil.WithEnvVar with t.Setenv (#479)
t.Setenv is the modern standard Go testing utility (since Go 1.17) that automatically restores environment variables when the test completes. This replaces the custom testutil.WithEnvVar function which manually saved and restored env var state.

The testutil.go file is deleted as it only contained WithEnvVar. The testutil package remains for its other utilities like KubeconfigBuilder.
2026-03-08 20:26:40 -07:00
Ahmet Alp Balkan
d3576731a0 feat: add XDG_CACHE_HOME support to Go implementations (#478)
Adds a new CacheDir() function that respects the XDG_CACHE_HOME environment variable,
matching the bash scripts' behavior. kubectx and kubens now prefer XDG_CACHE_HOME when
set, falling back to $HOME/.kube otherwise. This aligns the Go implementations with
the bash scripts' cache directory logic.

Includes comprehensive tests covering all scenarios:
- XDG_CACHE_HOME set (returns the XDG value)
- XDG_CACHE_HOME unset (falls back to $HOME/.kube)
- Neither set (returns empty string)
2026-03-08 20:19:35 -07:00
Ahmet Alp Balkan
aa92c923c2 docs: simplify installation instructions into a single table (#477)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 18:01:03 -07:00
Ahmet Alp Balkan
bb9592d770 chore: update goreleaser config and release workflow (#475)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 17:44:31 -07:00
Ahmet Alp Balkan
0d800e1367 fix: improve error handling and resource management in kubeconfig (#476)
This change addresses eight key improvements to the kubectx/kubens codebase:

Resource Management Fixes:
- Fix use-after-close bugs where Kubeconfig was accessed after Close()
- Fix resource leaks on error paths by ensuring defer kc.Close() is called
- Fix YAML encoder not being closed after Encode(), causing buffered data loss

API Design Improvements:
- Change ContextNames() to return ([]string, error) instead of silently returning
  nil on error, making parse failures distinguishable from empty results
- Change GetCurrentContext() to return (string, error) instead of returning ""
  for both "not set" and parse error cases
- Update all 16 call sites across cmd/kubectx and cmd/kubens packages to handle
  the new error returns while preserving backward-compatible behavior

Error Handling:
- Add explicit error handling for printer.Success() calls in 5+ locations
  by prefixing unchecked calls with _ =

Performance:
- Add slice pre-allocation in namespace list pagination using slices.Grow()
  before append loops, reducing allocations when fetching 500+ item batches

All changes maintain backward compatibility for missing kubeconfig keys while
improving error transparency and resource safety.
2026-03-08 17:44:18 -07:00
Ahmet Alp Balkan
860e09775b refactor: modernize Go codebase for Go 1.25 (#473)
Modernize the codebase to use idiomatic Go 1.25 patterns, removing deprecated APIs and reducing external dependencies.

- Replace deprecated `io/ioutil` with `os.ReadFile`, `os.WriteFile`, `os.MkdirTemp`, `os.CreateTemp`
- Replace `interface{}` with `any` (Go 1.18+)
- Remove `github.com/pkg/errors` dependency entirely, using stdlib `fmt.Errorf` with `%w` and `errors.New`
- Use `errors.As()` instead of direct type assertions on error values
- Use `strings.Cut()` for delimiter parsing instead of `strings.Split` + length check
- Use `slices.Contains()` for linear search in `ContextExists()`
- Use `t.Setenv()` and `t.TempDir()` in tests instead of manual env save/restore and temp dir cleanup
- Delete unused `internal/testutil/tempfile.go` helper
- Update GitHub Actions CI and release workflows from Go 1.22 to 1.25

Net result: -70 lines, 1 fewer external dependency.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 17:07:59 -07:00
Ahmet Alp Balkan
189100f2b6 ci: add workflow to warn PRs modifying frozen bash scripts (#474)
Adds a GitHub Actions workflow that comments on PRs modifying the root kubectx or kubens bash scripts
The comment uses a [!WARNING] alert to inform contributors that bash implementations are frozen and to propose changes to the Go implementation instead
Skips the comment if the PR author is ahmetb

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 16:49:55 -07:00
Joffrey Mischler
c13bf5a18b feat(kubens): add unset option (#440)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Ahmet Alp Balkan <ahmet@linkedin.com>
2026-03-08 16:19:49 -07:00
github-actions[bot]
defbc123a4 Merge pull request #466 from ahmetb/dependabot/go_modules/kubernetes-a1707b12ae
chore(deps): bump the kubernetes group with 2 updates
2026-03-08 23:09:07 +00:00
dependabot[bot]
d770af960d chore(deps): bump the kubernetes group with 2 updates
Bumps the kubernetes group with 2 updates: [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery) and [k8s.io/client-go](https://github.com/kubernetes/client-go).


Updates `k8s.io/apimachinery` from 0.28.5 to 0.35.2
- [Commits](https://github.com/kubernetes/apimachinery/compare/v0.28.5...v0.35.2)

Updates `k8s.io/client-go` from 0.28.5 to 0.35.2
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.28.5...v0.35.2)

---
updated-dependencies:
- dependency-name: k8s.io/apimachinery
  dependency-version: 0.35.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: kubernetes
- dependency-name: k8s.io/client-go
  dependency-version: 0.35.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: kubernetes
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-08 23:08:58 +00:00
dependabot[bot]
6f020b98a5 chore(deps): bump samuelmeuli/action-snapcraft from 1 to 3 (#468)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-08 16:08:48 -07:00
dependabot[bot]
58ae4f7464 chore(deps): bump goreleaser/goreleaser-action from 2 to 7 (#467)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-08 16:08:44 -07:00
dependabot[bot]
6f89971cc4 chore(deps): bump actions/setup-go from 2 to 6 (#465)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-08 16:08:37 -07:00
github-actions[bot]
171ce81a99 Merge pull request #471 from ahmetb/dependabot/go_modules/github.com/fatih/color-1.18.0
chore(deps): bump github.com/fatih/color from 1.9.0 to 1.18.0
2026-03-08 23:08:16 +00:00
dependabot[bot]
b1dec7b4ae chore(deps): bump github.com/fatih/color from 1.9.0 to 1.18.0
Bumps [github.com/fatih/color](https://github.com/fatih/color) from 1.9.0 to 1.18.0.
- [Release notes](https://github.com/fatih/color/releases)
- [Commits](https://github.com/fatih/color/compare/v1.9.0...v1.18.0)

---
updated-dependencies:
- dependency-name: github.com/fatih/color
  dependency-version: 1.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-08 23:08:05 +00:00
github-actions[bot]
da8283523a Merge pull request #470 from ahmetb/dependabot/go_modules/github.com/mattn/go-isatty-0.0.20
chore(deps): bump github.com/mattn/go-isatty from 0.0.14 to 0.0.20
2026-03-08 23:07:31 +00:00
dependabot[bot]
830c34933a chore(deps): bump github.com/mattn/go-isatty from 0.0.14 to 0.0.20
Bumps [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) from 0.0.14 to 0.0.20.
- [Commits](https://github.com/mattn/go-isatty/compare/v0.0.14...v0.0.20)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-isatty
  dependency-version: 0.0.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-08 23:07:22 +00:00
github-actions[bot]
439f75c76b Merge pull request #469 from ahmetb/dependabot/go_modules/sigs.k8s.io/kustomize/kyaml-0.21.1
chore(deps): bump sigs.k8s.io/kustomize/kyaml from 0.16.0 to 0.21.1
2026-03-08 23:06:42 +00:00
dependabot[bot]
05b0aae499 chore(deps): bump sigs.k8s.io/kustomize/kyaml from 0.16.0 to 0.21.1
Bumps [sigs.k8s.io/kustomize/kyaml](https://github.com/kubernetes-sigs/kustomize) from 0.16.0 to 0.21.1.
- [Release notes](https://github.com/kubernetes-sigs/kustomize/releases)
- [Commits](https://github.com/kubernetes-sigs/kustomize/compare/api/v0.16.0...api/v0.21.1)

---
updated-dependencies:
- dependency-name: sigs.k8s.io/kustomize/kyaml
  dependency-version: 0.21.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-08 23:06:32 +00:00
github-actions[bot]
f0ef521d44 Merge pull request #472 from ahmetb/dependabot/go_modules/github.com/google/go-cmp-0.7.0
chore(deps): bump github.com/google/go-cmp from 0.5.9 to 0.7.0
2026-03-08 23:05:41 +00:00
dependabot[bot]
c22e1bce9c chore(deps): bump actions/cache from 4 to 5 (#463)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-08 16:04:59 -07:00
github-actions[bot]
052dfbf90a Merge pull request #464 from ahmetb/dependabot/github_actions/rajatjindal/krew-release-bot-0.0.51
chore(deps): bump rajatjindal/krew-release-bot from 0.0.38 to 0.0.51
2026-03-08 23:04:51 +00:00
dependabot[bot]
e9050880c7 chore(deps): bump github.com/google/go-cmp from 0.5.9 to 0.7.0
Bumps [github.com/google/go-cmp](https://github.com/google/go-cmp) from 0.5.9 to 0.7.0.
- [Release notes](https://github.com/google/go-cmp/releases)
- [Commits](https://github.com/google/go-cmp/compare/v0.5.9...v0.7.0)

---
updated-dependencies:
- dependency-name: github.com/google/go-cmp
  dependency-version: 0.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-08 22:58:13 +00:00
dependabot[bot]
430c1534d2 chore(deps): bump rajatjindal/krew-release-bot from 0.0.38 to 0.0.51
Bumps [rajatjindal/krew-release-bot](https://github.com/rajatjindal/krew-release-bot) from 0.0.38 to 0.0.51.
- [Release notes](https://github.com/rajatjindal/krew-release-bot/releases)
- [Commits](https://github.com/rajatjindal/krew-release-bot/compare/v0.0.38...v0.0.51)

---
updated-dependencies:
- dependency-name: rajatjindal/krew-release-bot
  dependency-version: 0.0.51
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-08 22:57:54 +00:00
Ahmet Alp Balkan
a6cf1728fe chore: add dependabot config and auto-merge workflow (#462)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:57:13 -07:00
ultram4rine
80bbe8306a Migrate to kyaml (#412)
Closes #327.
2026-03-08 15:20:45 -07:00
Ahmet Alp Balkan
4c9e8fb81e feat: add -s/--shell flag for scoped sub-shell (#461)
Spawns an isolated sub-shell with a minimal kubeconfig containing only the specified context.

This allows the user to launch a shell where they can only interact with a single cluster without having to worry about a command or an LLM agent interacting with other contexts.

Inside the isolated shell, most context switching/editing operations on kubectx are blocked. Nested shells not allowed.

Fixes #12.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 14:48:03 -07:00
Ahmet Alp Balkan
5a29645996 fix(ci): upgrade actions/cache from v2 to v4 (#460)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 13:46:29 -07:00
55 changed files with 1101 additions and 1099 deletions

21
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
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/*"

35
.github/workflows/bash-frozen.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: Bash scripts frozen
on:
pull_request:
paths:
- 'kubectx'
- 'kubens'
jobs:
comment:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Comment on PR if author is not ahmetb
if: github.event.pull_request.user.login != 'ahmetb'
uses: actions/github-script@v7
with:
script: |
const body = [
'> [!WARNING]',
'> **This PR will not be merged.**',
'>',
'> The bash implementation of `kubectx` and `kubens` is **frozen** and is provided only for convenience.',
'> We are not accepting any improvements to the bash scripts.',
'>',
'> Please propose your improvements to the **Go implementation** instead.',
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: body
});

View File

@@ -23,20 +23,20 @@ jobs:
- name: Checkout
uses: actions/checkout@master
- name: Setup Go
uses: actions/setup-go@v2
uses: actions/setup-go@v6
with:
go-version: '1.22'
go-version: '1.25'
- id: go-cache-paths
run: |
echo "::set-output name=go-build::$(go env GOCACHE)"
echo "::set-output name=go-mod::$(go env GOMODCACHE)"
- name: Go Build Cache
uses: actions/cache@v4
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@v4
uses: actions/cache@v5
with:
path: ${{ steps.go-cache-paths.outputs.go-mod }}
key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}
@@ -47,7 +47,7 @@ jobs:
- name: Run unit tests
run: go test ./...
- name: Build with Goreleaser
uses: goreleaser/goreleaser-action@v2
uses: goreleaser/goreleaser-action@v7
with:
version: latest
args: release --snapshot --skip publish,snapcraft --clean

24
.github/workflows/dependabot.yml vendored Normal file
View File

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

View File

@@ -24,32 +24,32 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@master
uses: actions/checkout@v4
- run: git fetch --tags
- name: Setup Go
uses: actions/setup-go@v2
uses: actions/setup-go@v6
with:
go-version: '1.22'
go-version: '1.25'
- name: Install Snapcraft
uses: samuelmeuli/action-snapcraft@v1
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@v2
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.38
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.38
uses: rajatjindal/krew-release-bot@v0.0.51
with:
krew_template_file: .krew/ns.yaml
- name: Publish Snaps to the Snap Store (stable channel)

View File

@@ -15,7 +15,7 @@
# limitations under the License.
# This is an example goreleaser.yaml file with some sane defaults.
# Make sure to check the documentation at http://goreleaser.com
# Make sure to check the documentation at https://goreleaser.com
version: 2
before:
@@ -69,11 +69,11 @@ archives:
{{- else -}}v{{- . -}}
{{- end -}}
{{- end -}}
builds:
ids:
- kubectx
format_overrides:
- goos: windows
format: zip
formats: [zip]
files: ["LICENSE"]
- id: kubens-archive
name_template: |-
@@ -89,11 +89,11 @@ archives:
{{- else -}}v{{- . -}}
{{- end -}}
{{- end -}}
builds:
ids:
- kubens
format_overrides:
- goos: windows
format: zip
formats: [zip]
files: ["LICENSE"]
checksum:
name_template: "checksums.txt"
@@ -111,7 +111,7 @@ snapcrafts:
kubens is a tool to switch between Kubernetes namespaces (and configure them for kubectl) easily.
grade: stable
confinement: classic
base: core20
base: core24
apps:
kubectx:
command: kubectx

175
README.md
View File

@@ -33,6 +33,9 @@ Switched to context "minikube".
$ kubectx -
Switched to context "oregon".
# start an "isolated shell" that only has a single context
$ kubectx -s minikube
# rename context
$ kubectx dublin=gke_ahmetb_europe-west1-b_dublin
Context "gke_ahmetb_europe-west1-b_dublin" renamed to "dublin".
@@ -69,139 +72,23 @@ names anymore.
## Installation
Stable versions of `kubectx` and `kubens` are small bash scripts that you
can find in this repository.
| 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` |
Starting with v0.9.0, `kubectx` and `kubens` **are now rewritten in Go**. They
should work the same way (and we'll keep the bash-based implementations around)
but the new features will be added to the new Go programs. Please help us test
this new Go implementation by downloading the binaries from the [**Releases page
&rarr;**](https://github.com/ahmetb/kubectx/releases)
Alternatively, download binaries from the [**Releases page &rarr;**](https://github.com/ahmetb/kubectx/releases) and add them to somewhere in your `PATH`.
**Installation options:**
<details>
<summary>Shell completion scripts</summary>
- [as kubectl plugins (macOS & Linux)](#kubectl-plugins-macos-and-linux)
- [with Homebrew (macOS & Linux)](#homebrew-macos-and-linux)
- [with MacPorts (macOS)](#macports-macos)
- [with apt (Debian)](#apt-debian)
- [with pacman (Arch Linux)](#pacman-arch-linux)
- [with Chocolatey (Windows)](#windows-installation-using-chocolatey)
- [Windows Installation (using Scoop)](#windows-installation-using-scoop)
- [with winget (Windows)](#windows-installation-using-winget)
- [manually (macOS & Linux)](#manual-installation-macos-and-linux)
If you like to add context/namespace information to your shell prompt (`$PS1`),
you can try out [kube-ps1].
[kube-ps1]: https://github.com/jonmosco/kube-ps1
### Kubectl Plugins (macOS and Linux)
You can install and use the [Krew](https://github.com/kubernetes-sigs/krew/) kubectl
plugin manager to get `kubectx` and `kubens`.
**Note:** This will not install the shell completion scripts. If you want them,
*choose another installation method
or install the scripts [manually](#manual-installation-macos-and-linux).
```sh
kubectl krew install ctx
kubectl krew install ns
```
After installing, the tools will be available as `kubectl ctx` and `kubectl ns`.
### Homebrew (macOS and Linux)
If you use [Homebrew](https://brew.sh/) you can install like this:
```sh
brew install kubectx
```
This command will set up bash/zsh/fish completion scripts automatically. Make sure you [configure your shell](https://docs.brew.sh/Shell-Completion) to load completions for installed Homebrew formulas.
### MacPorts (macOS)
If you use [MacPorts](https://www.macports.org) you can install like this:
```sh
sudo port install kubectx
```
### apt (Debian)
``` bash
sudo apt install kubectx
```
Newer versions might be available on repos like
[Debian Buster (testing)](https://packages.debian.org/buster/kubectx),
[Sid (unstable)](https://packages.debian.org/sid/kubectx)
(_if you are unfamiliar with the Debian release process and how to enable
testing/unstable repos, check out the
[Debian Wiki](https://wiki.debian.org/DebianReleases)_):
### pacman (Arch Linux)
Available as official Arch Linux package. Install it via:
```bash
sudo pacman -S kubectx
```
### Windows Installation (using Chocolatey)
Available as packages on [Chocolatey](https://chocolatey.org/why-chocolatey)
```pwsh
choco install kubens kubectx
```
### Windows Installation (using Scoop)
Available as packages on [Scoop](https://scoop.sh/)
```pwsh
scoop bucket add main
scoop install main/kubens main/kubectx
```
### Windows Installation (using winget)
Available as packages on [winget](https://learn.microsoft.com/en-us/windows/package-manager/)
```pwsh
winget install --id ahmetb.kubectx
winget install --id ahmetb.kubens
```
### Manual Installation (macOS and Linux)
Since `kubectx` and `kubens` are written in Bash, you should be able to install
them to any POSIX environment that has Bash installed.
- Download the `kubectx`, and `kubens` scripts.
- Either:
- save them all to somewhere in your `PATH`,
- or save them to a directory, then create symlinks to `kubectx`/`kubens` from
somewhere in your `PATH`, like `/usr/local/bin`
- Make `kubectx` and `kubens` executable (`chmod +x ...`)
Example installation steps:
``` bash
sudo git clone https://github.com/ahmetb/kubectx /opt/kubectx
sudo ln -s /opt/kubectx/kubectx /usr/local/bin/kubectx
sudo ln -s /opt/kubectx/kubens /usr/local/bin/kubens
```
If you also want to have shell completions, pick an installation method for the
[completion scripts](completion/) that fits your system best: [`zsh` with
`antibody`](#completion-scripts-for-zsh-with-antibody), [plain
`zsh`](#completion-scripts-for-plain-zsh),
[`bash`](#completion-scripts-for-bash) or
[`fish`](#completion-scripts-for-fish).
#### Completion scripts for `zsh` with [antibody](https://getantibody.github.io)
#### zsh (with [antibody](https://getantibody.github.io))
Add this line to your [Plugins File](https://getantibody.github.io/usage/) (e.g.
`~/.zsh_plugins.txt`):
@@ -214,9 +101,9 @@ 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`.
`compinit`.
#### Completion scripts for plain `zsh`
#### zsh (plain)
The completion scripts have to be in a path that belongs to `$fpath`. Either
link or copy them to an existing folder.
@@ -241,7 +128,7 @@ depending on the `$fpath` of your zsh installation.
In case of errors, calling `compaudit` might help.
#### Completion scripts for `bash`
#### bash
```bash
git clone https://github.com/ahmetb/kubectx.git ~/.kubectx
@@ -256,7 +143,7 @@ export PATH=~/.kubectx:\$PATH
EOF
```
#### Completion scripts for `fish`
#### fish
```fish
mkdir -p ~/.config/fish/completions
@@ -264,6 +151,12 @@ ln -s /opt/kubectx/completion/kubectx.fish ~/.config/fish/completions/
ln -s /opt/kubectx/completion/kubens.fish ~/.config/fish/completions/
```
</details>
> [!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
@@ -274,12 +167,12 @@ with fuzzy searching, you just need to [install
![kubectx interactive search with fzf](img/kubectx-interactive.gif)
If you have `fzf` installed, but want to opt out of using this feature, set the
environment variable `KUBECTX_IGNORE_FZF=1`.
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 `).
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 `).
-----
@@ -303,7 +196,7 @@ Colors in the output can be disabled by setting the
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].
[kube-ps1](https://github.com/jonmosco/kube-ps1).
#### Stargazers over time

View File

@@ -15,11 +15,10 @@
package main
import (
"errors"
"fmt"
"io"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/kubeconfig"
)
@@ -27,16 +26,24 @@ import (
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 errors.Wrap(err, "kubeconfig error")
return fmt.Errorf("kubeconfig error: %w", err)
}
v := kc.GetCurrentContext()
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")
}
_, err := fmt.Fprintln(stdout, v)
return errors.Wrap(err, "write error")
if _, err := fmt.Fprintln(stdout, v); err != nil {
return fmt.Errorf("write error: %w", err)
}
return nil
}

View File

@@ -15,10 +15,10 @@
package main
import (
"errors"
"fmt"
"io"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer"
)
@@ -30,18 +30,21 @@ type DeleteOp struct {
// 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 errors.Wrapf(err, "error deleting context \"%s\"", deletedName)
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))
_ = printer.Success(stderr, `Deleted context %s.`, printer.SuccessColor.Sprint(deletedName))
}
return nil
}
@@ -52,10 +55,13 @@ func deleteContext(name string) (deleteName string, wasActiveContext bool, err e
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
return deleteName, false, errors.Wrap(err, "kubeconfig error")
return deleteName, false, fmt.Errorf("kubeconfig error: %w", err)
}
cur := kc.GetCurrentContext()
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 == "" {
@@ -65,12 +71,19 @@ func deleteContext(name string) (deleteName string, wasActiveContext bool, err e
name = cur
}
if !kc.ContextExists(name) {
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, errors.Wrap(err, "failed to modify yaml doc")
return name, false, fmt.Errorf("failed to modify yaml doc: %w", err)
}
return name, wasActiveContext, errors.Wrap(kc.Save(), "failed to save modified kubeconfig file")
if err := kc.Save(); err != nil {
return name, wasActiveContext, fmt.Errorf("failed to save modified kubeconfig file: %w", err)
}
return name, wasActiveContext, nil
}

View File

@@ -40,6 +40,13 @@ func parseArgs(argv []string) Op {
return ListOp{}
}
if argv[0] == "--shell" || argv[0] == "-s" {
if len(argv) != 2 {
return UnsupportedOp{Err: fmt.Errorf("'%s' requires exactly one context name argument", argv[0])}
}
return ShellOp{Target: argv[1]}
}
if argv[0] == "-d" {
if len(argv) == 1 {
if cmdutil.IsInteractiveMode(os.Stdout) {

View File

@@ -72,6 +72,18 @@ func Test_parseArgs_new(t *testing.T) {
{name: "rename context with old=current",
args: []string{"a=."},
want: RenameOp{"a", "."}},
{name: "shell shorthand",
args: []string{"-s", "prod"},
want: ShellOp{Target: "prod"}},
{name: "shell long form",
args: []string{"--shell", "prod"},
want: ShellOp{Target: "prod"}},
{name: "shell without context name",
args: []string{"-s"},
want: UnsupportedOp{Err: fmt.Errorf("'-s' requires exactly one context name argument")}},
{name: "shell with too many args",
args: []string{"--shell", "a", "b"},
want: UnsupportedOp{Err: fmt.Errorf("'--shell' requires exactly one context name argument")}},
{name: "unrecognized flag",
args: []string{"-x"},
want: UnsupportedOp{Err: fmt.Errorf("unsupported option '-x'")}},

View File

@@ -16,14 +16,13 @@ package main
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"os/exec"
"strings"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/env"
"github.com/ahmetb/kubectx/internal/kubeconfig"
@@ -39,16 +38,27 @@ type InteractiveDeleteOp struct {
}
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 errors.Wrap(err, "kubeconfig error")
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")
}
kc.Close()
cmd := exec.Command("fzf", "--ansi", "--no-preview")
var out bytes.Buffer
@@ -60,7 +70,8 @@ func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error {
fmt.Sprintf("FZF_DEFAULT_COMMAND=%s", op.SelfCmd),
fmt.Sprintf("%s=1", env.EnvForceColor))
if err := cmd.Run(); err != nil {
if _, ok := err.(*exec.ExitError); !ok {
var exitErr *exec.ExitError
if !errors.As(err, &exitErr) {
return err
}
}
@@ -70,25 +81,32 @@ func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error {
}
name, err := switchContext(choice)
if err != nil {
return errors.Wrap(err, "failed to switch context")
return fmt.Errorf("failed to switch context: %w", err)
}
printer.Success(stderr, "Switched to context \"%s\".", printer.SuccessColor.Sprint(name))
_ = 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 errors.Wrap(err, "kubeconfig error")
return fmt.Errorf("kubeconfig error: %w", err)
}
kc.Close()
if len(kc.ContextNames()) == 0 {
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")
}
@@ -102,7 +120,8 @@ func (op InteractiveDeleteOp) Run(_, stderr io.Writer) error {
fmt.Sprintf("FZF_DEFAULT_COMMAND=%s", op.SelfCmd),
fmt.Sprintf("%s=1", env.EnvForceColor))
if err := cmd.Run(); err != nil {
if _, ok := err.(*exec.ExitError); !ok {
var exitErr *exec.ExitError
if !errors.As(err, &exitErr) {
return err
}
}
@@ -114,7 +133,7 @@ func (op InteractiveDeleteOp) Run(_, stderr io.Writer) error {
name, wasActiveContext, err := deleteContext(choice)
if err != nil {
return errors.Wrap(err, "failed to delete context")
return fmt.Errorf("failed to delete context: %w", err)
}
if wasActiveContext {
@@ -122,7 +141,7 @@ func (op InteractiveDeleteOp) Run(_, stderr io.Writer) error {
selfName())
}
printer.Success(stderr, `Deleted context %s.`, printer.SuccessColor.Sprint(name))
_ = printer.Success(stderr, `Deleted context %s.`, printer.SuccessColor.Sprint(name))
return nil
}

View File

@@ -20,8 +20,6 @@ import (
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
)
// HelpOp describes printing help.
@@ -43,13 +41,17 @@ func printUsage(out io.Writer) error {
%PROG% -d <NAME> [<NAME...>] : delete context <NAME> ('.' for current-context)
%SPAC% (this command won't delete the user/cluster entry
%SPAC% referenced by the context entry)
%PROG% -s, --shell <NAME> : start a shell scoped to context <NAME>
%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)
return errors.Wrap(err, "write error")
if err != nil {
return fmt.Errorf("write error: %w", err)
}
return nil
}
// selfName guesses how the user invoked the program.

View File

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

View File

@@ -19,7 +19,6 @@ import (
"io"
"facette.io/natsort"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/kubeconfig"
@@ -30,6 +29,9 @@ import (
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 {
@@ -37,13 +39,19 @@ func (_ ListOp) Run(stdout, stderr io.Writer) error {
printer.Warning(stderr, "kubeconfig file not found")
return nil
}
return errors.Wrap(err, "kubeconfig error")
return fmt.Errorf("kubeconfig error: %w", err)
}
ctxs := kc.ContextNames()
ctxs, err := kc.ContextNames()
if err != nil {
return fmt.Errorf("failed to get context names: %w", err)
}
natsort.Sort(ctxs)
cur := kc.GetCurrentContext()
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 {

View File

@@ -34,7 +34,7 @@ func main() {
op := parseArgs(os.Args[1:])
if err := op.Run(color.Output, color.Error); err != nil {
printer.Error(color.Error, err.Error())
printer.Error(color.Error, "%s", err)
if _, ok := os.LookupEnv(env.EnvDebug); ok {
// print stack trace in verbose mode

View File

@@ -15,11 +15,10 @@
package main
import (
"fmt"
"io"
"strings"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer"
)
@@ -33,12 +32,8 @@ type RenameOp struct {
// parseRenameSyntax parses A=B form into [A,B] and returns
// whether it is parsed correctly.
func parseRenameSyntax(v string) (string, string, bool) {
s := strings.Split(v, "=")
if len(s) != 2 {
return "", "", false
}
new, old := s[0], s[1]
if new == "" || old == "" {
new, old, ok := strings.Cut(v, "=")
if !ok || new == "" || old == "" {
return "", "", false
}
return new, old, true
@@ -48,40 +43,54 @@ func parseRenameSyntax(v string) (string, string, bool) {
// 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 errors.Wrap(err, "kubeconfig error")
return fmt.Errorf("kubeconfig error: %w", err)
}
cur := kc.GetCurrentContext()
cur, err := kc.GetCurrentContext()
if err != nil {
return fmt.Errorf("failed to get current context: %w", err)
}
if op.Old == "." {
op.Old = cur
}
if !kc.ContextExists(op.Old) {
return errors.Errorf("context \"%s\" not found, can't rename it", op.Old)
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)
}
if kc.ContextExists(op.New) {
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 errors.Wrap(err, "failed to delete new context to overwrite it")
return fmt.Errorf("failed to delete new context to overwrite it: %w", err)
}
}
if err := kc.ModifyContextName(op.Old, op.New); err != nil {
return errors.Wrap(err, "failed to change context name")
return fmt.Errorf("failed to change context name: %w", err)
}
if op.Old == cur {
if err := kc.ModifyCurrentContext(op.New); err != nil {
return errors.Wrap(err, "failed to set current-context to new name")
return fmt.Errorf("failed to set current-context to new name: %w", err)
}
}
if err := kc.Save(); err != nil {
return errors.Wrap(err, "failed to save modified kubeconfig")
return fmt.Errorf("failed to save modified kubeconfig: %w", err)
}
printer.Success(stderr, "Context %s renamed to %s.",
_ = printer.Success(stderr, "Context %s renamed to %s.",
printer.SuccessColor.Sprint(op.Old),
printer.SuccessColor.Sprint(op.New))
return nil

141
cmd/kubectx/shell.go Normal file
View File

@@ -0,0 +1,141 @@
package main
import (
"fmt"
"io"
"os"
"os/exec"
"runtime"
"github.com/fatih/color"
"github.com/ahmetb/kubectx/internal/env"
"github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer"
)
// ShellOp indicates intention to start a scoped sub-shell for a context.
type ShellOp struct {
Target string
}
func (op ShellOp) Run(_, stderr io.Writer) error {
if err := checkIsolatedMode(); err != nil {
return err
}
kubectlPath, err := resolveKubectl()
if err != nil {
return err
}
// Verify context exists and get current context for exit message
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
return fmt.Errorf("kubeconfig error: %w", err)
}
exists, err := kc.ContextExists(op.Target)
if err != nil {
return fmt.Errorf("failed to check context: %w", err)
}
if !exists {
return fmt.Errorf("no context exists with the name: \"%s\"", op.Target)
}
previousCtx, err := kc.GetCurrentContext()
if err != nil {
return fmt.Errorf("failed to get current context: %w", err)
}
// Extract minimal kubeconfig using kubectl
data, err := extractMinimalKubeconfig(kubectlPath, op.Target)
if err != nil {
return fmt.Errorf("failed to extract kubeconfig for context: %w", err)
}
// Write to temp file
tmpFile, err := os.CreateTemp("", "kubectx-shell-*.yaml")
if err != nil {
return fmt.Errorf("failed to create temp kubeconfig file: %w", err)
}
tmpPath := tmpFile.Name()
defer os.Remove(tmpPath)
if _, err := tmpFile.Write(data); err != nil {
tmpFile.Close()
return fmt.Errorf("failed to write temp kubeconfig: %w", err)
}
tmpFile.Close()
// Print entry message
badgeColor := color.New(color.BgRed, color.FgWhite, color.Bold)
printer.EnableOrDisableColor(badgeColor)
fmt.Fprintf(stderr, "%s kubectl context is %s in this shell — type 'exit' to leave.\n",
badgeColor.Sprint("[ISOLATED SHELL]"), printer.WarningColor.Sprint(op.Target))
// Detect and start shell
shellBin := detectShell()
cmd := exec.Command(shellBin)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = append(os.Environ(),
"KUBECONFIG="+tmpPath,
env.EnvIsolatedShell+"=1",
)
_ = cmd.Run()
// Print exit message
fmt.Fprintf(stderr, "%s kubectl context is now %s.\n",
badgeColor.Sprint("[ISOLATED SHELL EXITED]"), printer.WarningColor.Sprint(previousCtx))
return nil
}
func resolveKubectl() (string, error) {
if v := os.Getenv("KUBECTL"); v != "" {
return v, nil
}
path, err := exec.LookPath("kubectl")
if err != nil {
return "", fmt.Errorf("kubectl is required for --shell but was not found in PATH")
}
return path, nil
}
func extractMinimalKubeconfig(kubectlPath, contextName string) ([]byte, error) {
cmd := exec.Command(kubectlPath, "config", "view", "--minify", "--flatten",
"--context", contextName)
cmd.Env = os.Environ()
data, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("kubectl config view failed: %w", err)
}
return data, nil
}
func detectShell() string {
if runtime.GOOS == "windows" {
// cmd.exe always sets the PROMPT env var, so if it is present
// we can reliably assume we are running inside cmd.exe.
if os.Getenv("PROMPT") != "" {
return "cmd.exe"
}
// Otherwise assume PowerShell. PSModulePath is always set on
// Windows regardless of the shell, so it cannot be used as a
// discriminator; however the absence of PROMPT is a strong
// enough signal that we are in a PowerShell session.
if pwsh, err := exec.LookPath("pwsh"); err == nil {
return pwsh
}
if powershell, err := exec.LookPath("powershell"); err == nil {
return powershell
}
return "cmd.exe"
}
if v := os.Getenv("SHELL"); v != "" {
return v
}
return "/bin/sh"
}

113
cmd/kubectx/shell_test.go Normal file
View File

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

View File

@@ -15,27 +15,26 @@
package main
import (
"io/ioutil"
"errors"
"fmt"
"os"
"path/filepath"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
)
func kubectxPrevCtxFile() (string, error) {
home := cmdutil.HomeDir()
if home == "" {
dir := cmdutil.CacheDir()
if dir == "" {
return "", errors.New("HOME or USERPROFILE environment variable not set")
}
return filepath.Join(home, ".kube", "kubectx"), nil
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 := ioutil.ReadFile(path)
b, err := os.ReadFile(path)
if os.IsNotExist(err) {
return "", nil
}
@@ -47,7 +46,7 @@ func readLastContext(path string) (string, error) {
func writeLastContext(path, value string) error {
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
return errors.Wrap(err, "failed to create parent directories")
return fmt.Errorf("failed to create parent directories: %w", err)
}
return ioutil.WriteFile(path, []byte(value), 0644)
return os.WriteFile(path, []byte(value), 0644)
}

View File

@@ -15,12 +15,9 @@
package main
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/ahmetb/kubectx/internal/testutil"
)
func Test_readLastContext_nonExistingFile(t *testing.T) {
@@ -34,8 +31,11 @@ func Test_readLastContext_nonExistingFile(t *testing.T) {
}
func Test_readLastContext(t *testing.T) {
path, cleanup := testutil.TempFile(t, "foo")
defer cleanup()
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 {
@@ -55,10 +55,7 @@ func Test_writeLastContext_err(t *testing.T) {
}
func Test_writeLastContext(t *testing.T) {
dir, err := ioutil.TempDir(os.TempDir(), "state-file-test")
if err != nil {
t.Fatal(err)
}
dir := t.TempDir()
path := filepath.Join(dir, "foo", "bar")
if err := writeLastContext(path, "ctx1"); err != nil {
@@ -75,9 +72,8 @@ func Test_writeLastContext(t *testing.T) {
}
func Test_kubectxFilePath(t *testing.T) {
origHome := os.Getenv("HOME")
os.Setenv("HOME", filepath.FromSlash("/foo/bar"))
defer os.Setenv("HOME", origHome)
t.Setenv("HOME", filepath.FromSlash("/foo/bar"))
t.Setenv("XDG_CACHE_HOME", "")
expected := filepath.Join(filepath.FromSlash("/foo/bar"), ".kube", "kubectx")
v, err := kubectxPrevCtxFile()
@@ -89,13 +85,22 @@ func Test_kubectxFilePath(t *testing.T) {
}
}
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) {
origHome := os.Getenv("HOME")
origUserprofile := os.Getenv("USERPROFILE")
os.Unsetenv("HOME")
os.Unsetenv("USERPROFILE")
defer os.Setenv("HOME", origHome)
defer os.Setenv("USERPROFILE", origUserprofile)
t.Setenv("HOME", "")
t.Setenv("USERPROFILE", "")
_, err := kubectxPrevCtxFile()
if err == nil {

View File

@@ -15,10 +15,10 @@
package main
import (
"errors"
"fmt"
"io"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer"
)
@@ -29,6 +29,9 @@ type SwitchOp struct {
}
func (op SwitchOp) Run(_, stderr io.Writer) error {
if err := checkIsolatedMode(); err != nil {
return err
}
var newCtx string
var err error
if op.Target == "-" {
@@ -37,39 +40,48 @@ func (op SwitchOp) Run(_, stderr io.Writer) error {
newCtx, err = switchContext(op.Target)
}
if err != nil {
return errors.Wrap(err, "failed to switch context")
return fmt.Errorf("failed to switch context: %w", err)
}
err = printer.Success(stderr, "Switched to context \"%s\".", printer.SuccessColor.Sprint(newCtx))
return errors.Wrap(err, "print error")
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 "", errors.Wrap(err, "failed to determine state file")
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 "", errors.Wrap(err, "kubeconfig error")
return "", fmt.Errorf("kubeconfig error: %w", err)
}
prev := kc.GetCurrentContext()
if !kc.ContextExists(name) {
return "", errors.Errorf("no context exists with the name: \"%s\"", name)
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 "", errors.Wrap(err, "failed to save kubeconfig")
return "", fmt.Errorf("failed to save kubeconfig: %w", err)
}
if prev != name {
if err := writeLastContext(prevCtxFile, prev); err != nil {
return "", errors.Wrap(err, "failed to save previous context name")
return "", fmt.Errorf("failed to save previous context name: %w", err)
}
}
return name, nil
@@ -79,11 +91,11 @@ func switchContext(name string) (string, error) {
func swapContext() (string, error) {
prevCtxFile, err := kubectxPrevCtxFile()
if err != nil {
return "", errors.Wrap(err, "failed to determine state file")
return "", fmt.Errorf("failed to determine state file: %w", err)
}
prev, err := readLastContext(prevCtxFile)
if err != nil {
return "", errors.Wrap(err, "failed to read previous context file")
return "", fmt.Errorf("failed to read previous context file: %w", err)
}
if prev == "" {
return "", errors.New("no previous context found")

View File

@@ -15,10 +15,9 @@
package main
import (
"fmt"
"io"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer"
)
@@ -27,19 +26,25 @@ import (
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 errors.Wrap(err, "kubeconfig error")
return fmt.Errorf("kubeconfig error: %w", err)
}
if err := kc.UnsetCurrentContext(); err != nil {
return errors.Wrap(err, "error while modifying current-context")
return fmt.Errorf("error while modifying current-context: %w", err)
}
if err := kc.Save(); err != nil {
return errors.Wrap(err, "failed to save kubeconfig file after modification")
return fmt.Errorf("failed to save kubeconfig file after modification: %w", err)
}
err := printer.Success(stderr, "Active context unset for kubectl.")
return errors.Wrap(err, "write error")
if err != nil {
return fmt.Errorf("write error: %w", err)
}
return nil
}

View File

@@ -3,8 +3,6 @@ package main
import (
"fmt"
"io"
"github.com/pkg/errors"
)
var (
@@ -16,5 +14,8 @@ type VersionOp struct{}
func (_ VersionOp) Run(stdout, _ io.Writer) error {
_, err := fmt.Fprintf(stdout, "%s\n", version)
return errors.Wrap(err, "write error")
if err != nil {
return fmt.Errorf("write error: %w", err)
}
return nil
}

View File

@@ -15,11 +15,10 @@
package main
import (
"errors"
"fmt"
"io"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/kubeconfig"
)
@@ -29,17 +28,23 @@ 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 errors.Wrap(err, "kubeconfig error")
return fmt.Errorf("kubeconfig error: %w", err)
}
ctx := kc.GetCurrentContext()
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 errors.Wrapf(err, "failed to read namespace of \"%s\"", ctx)
return fmt.Errorf("failed to read namespace of \"%s\": %w", ctx, err)
}
_, err = fmt.Fprintln(stdout, ns)
return errors.Wrap(err, "write error")
if err != nil {
return fmt.Errorf("write error: %w", err)
}
return nil
}

View File

@@ -52,6 +52,8 @@ func parseArgs(argv []string) Op {
return VersionOp{}
case "--current", "-c":
return CurrentOp{}
case "--unset", "-u":
return UnsetOp{}
default:
return getSwitchOp(v, false)
}

View File

@@ -45,6 +45,12 @@ func Test_parseArgs_new(t *testing.T) {
{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"}},

View File

@@ -16,14 +16,13 @@ package main
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"os/exec"
"strings"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/env"
"github.com/ahmetb/kubectx/internal/kubeconfig"
@@ -38,14 +37,22 @@ type InteractiveSwitchOp struct {
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 errors.Wrap(err, "kubeconfig error")
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")
}
defer kc.Close()
cmd := exec.Command("fzf", "--ansi", "--no-preview")
var out bytes.Buffer
@@ -57,7 +64,8 @@ func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error {
fmt.Sprintf("FZF_DEFAULT_COMMAND=%s", op.SelfCmd),
fmt.Sprintf("%s=1", env.EnvForceColor))
if err := cmd.Run(); err != nil {
if _, ok := err.(*exec.ExitError); !ok {
var exitErr *exec.ExitError
if !errors.As(err, &exitErr) {
return err
}
}
@@ -67,8 +75,8 @@ func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error {
}
name, err := switchNamespace(kc, choice, false)
if err != nil {
return errors.Wrap(err, "failed to switch namespace")
return fmt.Errorf("failed to switch namespace: %w", err)
}
printer.Success(stderr, "Active namespace is \"%s\".", printer.SuccessColor.Sprint(name))
_ = printer.Success(stderr, "Active namespace is \"%s\".", printer.SuccessColor.Sprint(name))
return nil
}

View File

@@ -20,8 +20,6 @@ import (
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
)
// HelpOp describes printing help.
@@ -39,13 +37,17 @@ func printUsage(out io.Writer) error {
%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)
return errors.Wrap(err, "write error")
if err != nil {
return fmt.Errorf("write error: %w", err)
}
return nil
}
// selfName guesses how the user invoked the program.

View File

@@ -16,11 +16,12 @@ package main
import (
"context"
"errors"
"fmt"
"io"
"os"
"slices"
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
_ "k8s.io/client-go/plugin/pkg/client/auth"
@@ -36,21 +37,24 @@ 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 errors.Wrap(err, "kubeconfig error")
return fmt.Errorf("kubeconfig error: %w", err)
}
ctx := kc.GetCurrentContext()
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 errors.Wrap(err, "cannot read current namespace")
return fmt.Errorf("cannot read current namespace: %w", err)
}
ns, err := queryNamespaces(kc)
if err != nil {
return errors.Wrap(err, "could not list namespaces (is the cluster accessible?)")
return fmt.Errorf("could not list namespaces (is the cluster accessible?): %w", err)
}
for _, c := range ns {
@@ -70,7 +74,7 @@ func queryNamespaces(kc *kubeconfig.Kubeconfig) ([]string, error) {
clientset, err := newKubernetesClientSet(kc)
if err != nil {
return nil, errors.Wrap(err, "failed to initialize k8s REST client")
return nil, fmt.Errorf("failed to initialize k8s REST client: %w", err)
}
var out []string
@@ -83,9 +87,10 @@ func queryNamespaces(kc *kubeconfig.Kubeconfig) ([]string, error) {
Continue: next,
})
if err != nil {
return nil, errors.Wrap(err, "failed to list namespaces from k8s API")
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)
}
@@ -99,11 +104,11 @@ func queryNamespaces(kc *kubeconfig.Kubeconfig) ([]string, error) {
func newKubernetesClientSet(kc *kubeconfig.Kubeconfig) (*kubernetes.Clientset, error) {
b, err := kc.Bytes()
if err != nil {
return nil, errors.Wrap(err, "failed to convert in-memory kubeconfig to yaml")
return nil, fmt.Errorf("failed to convert in-memory kubeconfig to yaml: %w", err)
}
cfg, err := clientcmd.RESTConfigFromKubeConfig(b)
if err != nil {
return nil, errors.Wrap(err, "failed to initialize config")
return nil, fmt.Errorf("failed to initialize config: %w", err)
}
return kubernetes.NewForConfig(cfg)
}

View File

@@ -33,7 +33,7 @@ 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, err.Error())
printer.Error(color.Error, "%s", err)
if _, ok := os.LookupEnv(env.EnvDebug); ok {
// print stack trace in verbose mode

View File

@@ -16,7 +16,6 @@ package main
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"runtime"
@@ -25,7 +24,7 @@ import (
"github.com/ahmetb/kubectx/internal/cmdutil"
)
var defaultDir = filepath.Join(cmdutil.HomeDir(), ".kube", "kubens")
var defaultDir = filepath.Join(cmdutil.CacheDir(), "kubens")
type NSFile struct {
dir string
@@ -45,7 +44,7 @@ func (f NSFile) path() string {
// Load reads the previous namespace setting, or returns empty if not exists.
func (f NSFile) Load() (string, error) {
b, err := ioutil.ReadFile(f.path())
b, err := os.ReadFile(f.path())
if err != nil {
if os.IsNotExist(err) {
return "", nil
@@ -61,7 +60,7 @@ func (f NSFile) Save(value string) error {
if err := os.MkdirAll(d, 0755); err != nil {
return err
}
return ioutil.WriteFile(f.path(), []byte(value), 0644)
return os.WriteFile(f.path(), []byte(value), 0644)
}
// isWindows determines if the process is running on windows OS.

View File

@@ -15,21 +15,13 @@
package main
import (
"io/ioutil"
"os"
"runtime"
"strings"
"testing"
"github.com/ahmetb/kubectx/internal/testutil"
)
func TestNSFile(t *testing.T) {
td, err := ioutil.TempDir(os.TempDir(), "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(td)
td := t.TempDir()
f := NewNSFile("foo")
f.dir = td
@@ -56,7 +48,7 @@ func TestNSFile(t *testing.T) {
}
func TestNSFile_path_windows(t *testing.T) {
defer testutil.WithEnvVar("_FORCE_GOOS", "windows")()
t.Setenv("_FORCE_GOOS", "windows")
fp := NewNSFile("a:b:c").path()
if expected := "a__b__c"; !strings.HasSuffix(fp, expected) {
@@ -74,7 +66,7 @@ func Test_isWindows(t *testing.T) {
t.Fatalf("isWindows() returned true for %s", runtime.GOOS)
}
defer testutil.WithEnvVar("_FORCE_GOOS", "windows")()
t.Setenv("_FORCE_GOOS", "windows")
if !isWindows() {
t.Fatalf("isWindows() failed to detect windows with env override.")
}

View File

@@ -16,10 +16,11 @@ package main
import (
"context"
"errors"
"fmt"
"io"
"os"
"github.com/pkg/errors"
errors2 "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -36,7 +37,7 @@ 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 errors.Wrap(err, "kubeconfig error")
return fmt.Errorf("kubeconfig error: %w", err)
}
toNS, err := switchNamespace(kc, s.Target, s.Force)
@@ -48,24 +49,27 @@ func (s SwitchOp) Run(_, stderr io.Writer) error {
}
func switchNamespace(kc *kubeconfig.Kubeconfig, ns string, force bool) (string, error) {
ctx := kc.GetCurrentContext()
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 "", errors.Wrap(err, "failed to get current namespace")
return "", fmt.Errorf("failed to get current namespace: %w", err)
}
f := NewNSFile(ctx)
prev, err := f.Load()
if err != nil {
return "", errors.Wrap(err, "failed to load previous namespace from file")
return "", fmt.Errorf("failed to load previous namespace from file: %w", err)
}
if ns == "-" {
if prev == "" {
return "", errors.Errorf("No previous namespace found for current context (%s)", ctx)
return "", fmt.Errorf("No previous namespace found for current context (%s)", ctx)
}
ns = prev
}
@@ -73,22 +77,22 @@ func switchNamespace(kc *kubeconfig.Kubeconfig, ns string, force bool) (string,
if !force {
ok, err := namespaceExists(kc, ns)
if err != nil {
return "", errors.Wrap(err, "failed to query if namespace exists (is cluster accessible?)")
return "", fmt.Errorf("failed to query if namespace exists (is cluster accessible?): %w", err)
}
if !ok {
return "", errors.Errorf("no namespace exists with name \"%s\"", ns)
return "", fmt.Errorf("no namespace exists with name \"%s\"", ns)
}
}
if err := kc.SetNamespace(ctx, ns); err != nil {
return "", errors.Wrapf(err, "failed to change to namespace \"%s\"", ns)
return "", fmt.Errorf("failed to change to namespace \"%s\": %w", ns, err)
}
if err := kc.Save(); err != nil {
return "", errors.Wrap(err, "failed to save kubeconfig file")
return "", fmt.Errorf("failed to save kubeconfig file: %w", err)
}
if curNS != ns {
if err := f.Save(curNS); err != nil {
return "", errors.Wrap(err, "failed to save the previous namespace to file")
return "", fmt.Errorf("failed to save the previous namespace to file: %w", err)
}
}
return ns, nil
@@ -102,13 +106,15 @@ func namespaceExists(kc *kubeconfig.Kubeconfig, ns string) (bool, error) {
clientset, err := newKubernetesClientSet(kc)
if err != nil {
return false, errors.Wrap(err, "failed to initialize k8s REST client")
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
}
return namespace != nil, errors.Wrapf(err, "failed to query "+
"namespace %q from k8s API", ns)
if err != nil {
return false, fmt.Errorf("failed to query namespace %q from k8s API: %w", ns, err)
}
return namespace != nil, nil
}

61
cmd/kubens/unset.go Normal file
View File

@@ -0,0 +1,61 @@
// 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
}

View File

@@ -3,8 +3,6 @@ package main
import (
"fmt"
"io"
"github.com/pkg/errors"
)
var (
@@ -16,5 +14,8 @@ type VersionOp struct{}
func (_ VersionOp) Run(stdout, _ io.Writer) error {
_, err := fmt.Fprintf(stdout, "%s\n", version)
return errors.Wrap(err, "write error")
if err != nil {
return fmt.Errorf("write error: %w", err)
}
return nil
}

77
go.mod
View File

@@ -1,54 +1,55 @@
module github.com/ahmetb/kubectx
go 1.22
go 1.25.0
require (
facette.io/natsort v0.0.0-20181210072756-2cd4dd1e2dcb
github.com/fatih/color v1.9.0
github.com/google/go-cmp v0.5.9
github.com/mattn/go-isatty v0.0.14
github.com/pkg/errors v0.9.1
gopkg.in/yaml.v3 v3.0.1
k8s.io/apimachinery v0.27.3
k8s.io/client-go v0.27.3
github.com/fatih/color v1.18.0
github.com/google/go-cmp v0.7.0
github.com/mattn/go-isatty v0.0.20
k8s.io/apimachinery v0.35.2
k8s.io/client-go v0.35.2
sigs.k8s.io/kustomize/kyaml v0.21.1
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.1 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/imdario/mergo v0.3.9 // 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.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // 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.5 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.1 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.9.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/api v0.27.3 // indirect
k8s.io/klog/v2 v2.90.1 // indirect
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect
k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.35.2 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)

562
go.sum
View File

@@ -1,506 +1,144 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
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/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
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/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE=
github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8=
github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
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-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54=
github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
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.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
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.9.1 h1:zie5Ly042PD3bsCvsSOPvRnFwyo3rKe64TJlD6nu0mk=
github.com/onsi/ginkgo/v2 v2.9.1/go.mod h1:FEcmzVcCHl+4o9bQZVab+4dC9+j+91t2FHSzmGAPfuo=
github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E=
github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
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 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
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.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/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=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
k8s.io/api v0.27.3 h1:yR6oQXXnUEBWEWcvPWS0jQL575KoAboQPfJAuKNrw5Y=
k8s.io/api v0.27.3/go.mod h1:C4BNvZnQOF7JA/0Xed2S+aUyJSfTGkGFxLXz9MnpIpg=
k8s.io/apimachinery v0.27.3 h1:Ubye8oBufD04l9QnNtW05idcOe9Z3GQN8+7PqmuVcUM=
k8s.io/apimachinery v0.27.3/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E=
k8s.io/client-go v0.27.3 h1:7dnEGHZEJld3lYwxvLl7WoehK6lAq7GvgjxpA3nv1E8=
k8s.io/client-go v0.27.3/go.mod h1:2MBEKuTo6V1lbKy3z1euEGnhPfGZLKTS9tiJ2xodM48=
k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw=
k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg=
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg=
k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY=
k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw=
k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60=
k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8=
k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o=
k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7fI=
sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

View File

@@ -15,9 +15,9 @@
package cmdutil
import (
"errors"
"os"
"github.com/pkg/errors"
"path/filepath"
)
func HomeDir() string {
@@ -28,8 +28,20 @@ func HomeDir() string {
return home
}
// IsNotFoundErr determines if the underlying error is os.IsNotExist. Right now
// errors from github.com/pkg/errors doesn't work with os.IsNotExist.
// CacheDir returns XDG_CACHE_HOME if set, otherwise $HOME/.kube,
// matching the bash scripts' behavior: ${XDG_CACHE_HOME:-$HOME/.kube}.
func CacheDir() string {
if xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" {
return xdg
}
home := HomeDir()
if home == "" {
return ""
}
return filepath.Join(home, ".kube")
}
// IsNotFoundErr determines if the underlying error is os.IsNotExist.
func IsNotFoundErr(err error) bool {
for e := err; e != nil; e = errors.Unwrap(e) {
if os.IsNotExist(e) {

View File

@@ -15,9 +15,8 @@
package cmdutil
import (
"path/filepath"
"testing"
"github.com/ahmetb/kubectx/internal/testutil"
)
func Test_homeDir(t *testing.T) {
@@ -63,18 +62,40 @@ func Test_homeDir(t *testing.T) {
for _, c := range cases {
t.Run(c.name, func(tt *testing.T) {
var unsets []func()
for _, e := range c.envs {
unsets = append(unsets, testutil.WithEnvVar(e.k, e.v))
tt.Setenv(e.k, e.v)
}
got := HomeDir()
if got != c.want {
t.Errorf("expected:%q got:%q", c.want, got)
}
for _, u := range unsets {
u()
}
})
}
}
func TestCacheDir(t *testing.T) {
t.Run("XDG_CACHE_HOME set", func(t *testing.T) {
t.Setenv("XDG_CACHE_HOME", "/tmp/xdg-cache")
t.Setenv("HOME", "/home/user")
if got := CacheDir(); got != "/tmp/xdg-cache" {
t.Errorf("expected:%q got:%q", "/tmp/xdg-cache", got)
}
})
t.Run("XDG_CACHE_HOME unset, falls back to HOME/.kube", func(t *testing.T) {
t.Setenv("XDG_CACHE_HOME", "")
t.Setenv("HOME", "/home/user")
want := filepath.Join("/home/user", ".kube")
if got := CacheDir(); got != want {
t.Errorf("expected:%q got:%q", want, got)
}
})
t.Run("neither set", func(t *testing.T) {
t.Setenv("XDG_CACHE_HOME", "")
t.Setenv("HOME", "")
t.Setenv("USERPROFILE", "")
if got := CacheDir(); got != "" {
t.Errorf("expected:%q got:%q", "", got)
}
})
}

View File

@@ -29,4 +29,6 @@ const (
// EnvDebug describes the internal environment variable for more verbose logging.
EnvDebug = `DEBUG`
EnvIsolatedShell = "KUBECTX_ISOLATED_SHELL"
)

View File

@@ -15,8 +15,9 @@
package kubeconfig
import (
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
"errors"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
func (k *Kubeconfig) DeleteContextEntry(deleteName string) error {
@@ -24,60 +25,34 @@ func (k *Kubeconfig) DeleteContextEntry(deleteName string) error {
if err != nil {
return err
}
i := -1
for j, ctxNode := range contexts.Content {
nameNode := valueOf(ctxNode, "name")
if nameNode != nil && nameNode.Kind == yaml.ScalarNode && nameNode.Value == deleteName {
i = j
break
}
}
if i >= 0 {
copy(contexts.Content[i:], contexts.Content[i+1:])
contexts.Content[len(contexts.Content)-1] = nil
contexts.Content = contexts.Content[:len(contexts.Content)-1]
if err := contexts.PipeE(
yaml.ElementSetter{
Keys: []string{"name"},
Values: []string{deleteName},
},
); err != nil {
return err
}
return nil
}
func (k *Kubeconfig) ModifyCurrentContext(name string) error {
currentCtxNode := valueOf(k.rootNode, "current-context")
if currentCtxNode != nil {
currentCtxNode.Value = name
return nil
if err := k.config.PipeE(yaml.SetField("current-context", yaml.NewScalarRNode(name))); err != nil {
return err
}
// if current-context field doesn't exist, create new field
keyNode := &yaml.Node{
Kind: yaml.ScalarNode,
Value: "current-context",
Tag: "!!str"}
valueNode := &yaml.Node{
Kind: yaml.ScalarNode,
Value: name,
Tag: "!!str"}
k.rootNode.Content = append(k.rootNode.Content, keyNode, valueNode)
return nil
}
func (k *Kubeconfig) ModifyContextName(old, new string) error {
contexts, err := k.contextsNode()
context, err := k.config.Pipe(yaml.Lookup("contexts", "[name="+old+"]"))
if err != nil {
return err
}
var changed bool
for _, contextNode := range contexts.Content {
nameNode := valueOf(contextNode, "name")
if nameNode.Kind == yaml.ScalarNode && nameNode.Value == old {
nameNode.Value = new
changed = true
break
}
if context == nil {
return errors.New("\"contexts\" entry is nil")
}
if !changed {
return errors.New("no changes were made")
if err := context.PipeE(yaml.SetField("name", yaml.NewScalarRNode(new))); err != nil {
return err
}
return nil
}

View File

@@ -15,72 +15,60 @@
package kubeconfig
import (
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
"errors"
"fmt"
"slices"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
func (k *Kubeconfig) contextsNode() (*yaml.Node, error) {
contexts := valueOf(k.rootNode, "contexts")
func (k *Kubeconfig) contextsNode() (*yaml.RNode, error) {
contexts, err := k.config.Pipe(yaml.Get("contexts"))
if err != nil {
return nil, err
}
if contexts == nil {
return nil, errors.New("\"contexts\" entry is nil")
} else if contexts.Kind != yaml.SequenceNode {
} else if contexts.YNode().Kind != yaml.SequenceNode {
return nil, errors.New("\"contexts\" is not a sequence node")
}
return contexts, nil
}
func (k *Kubeconfig) contextNode(name string) (*yaml.Node, error) {
func (k *Kubeconfig) contextNode(name string) (*yaml.RNode, error) {
contexts, err := k.contextsNode()
if err != nil {
return nil, err
}
for _, contextNode := range contexts.Content {
nameNode := valueOf(contextNode, "name")
if nameNode.Kind == yaml.ScalarNode && nameNode.Value == name {
return contextNode, nil
}
context, err := contexts.Pipe(yaml.Lookup("[name=" + name + "]"))
if err != nil {
return nil, err
}
return nil, errors.Errorf("context with name \"%s\" not found", name)
if context == nil {
return nil, fmt.Errorf("context with name \"%s\" not found", name)
}
return context, nil
}
func (k *Kubeconfig) ContextNames() []string {
contexts := valueOf(k.rootNode, "contexts")
func (k *Kubeconfig) ContextNames() ([]string, error) {
contexts, err := k.config.Pipe(yaml.Get("contexts"))
if err != nil {
return nil, fmt.Errorf("failed to get contexts: %w", err)
}
if contexts == nil {
return nil
return nil, nil
}
if contexts.Kind != yaml.SequenceNode {
return nil
names, err := contexts.ElementValues("name")
if err != nil {
return nil, fmt.Errorf("failed to get context names: %w", err)
}
var ctxNames []string
for _, ctx := range contexts.Content {
nameVal := valueOf(ctx, "name")
if nameVal != nil {
ctxNames = append(ctxNames, nameVal.Value)
}
}
return ctxNames
return names, nil
}
func (k *Kubeconfig) ContextExists(name string) bool {
ctxNames := k.ContextNames()
for _, v := range ctxNames {
if v == name {
return true
}
func (k *Kubeconfig) ContextExists(name string) (bool, error) {
names, err := k.ContextNames()
if err != nil {
return false, err
}
return false
}
func valueOf(mapNode *yaml.Node, key string) *yaml.Node {
if mapNode.Kind != yaml.MappingNode {
return nil
}
for i, ch := range mapNode.Content {
if i%2 == 0 && ch.Kind == yaml.ScalarNode && ch.Value == key {
return mapNode.Content[i+1]
}
}
return nil
return slices.Contains(names, name), nil
}

View File

@@ -33,7 +33,10 @@ func TestKubeconfig_ContextNames(t *testing.T) {
t.Fatal(err)
}
ctx := kc.ContextNames()
ctx, err := kc.ContextNames()
if err != nil {
t.Fatal(err)
}
expected := []string{"abc", "def", "ghi"}
if diff := cmp.Diff(expected, ctx); diff != "" {
t.Fatalf("%s", diff)
@@ -46,7 +49,10 @@ func TestKubeconfig_ContextNames_noContextsEntry(t *testing.T) {
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
ctx := kc.ContextNames()
ctx, err := kc.ContextNames()
if err != nil {
t.Fatal(err)
}
var expected []string = nil
if diff := cmp.Diff(expected, ctx); diff != "" {
t.Fatalf("%s", diff)
@@ -59,10 +65,9 @@ func TestKubeconfig_ContextNames_nonArrayContextsEntry(t *testing.T) {
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
ctx := kc.ContextNames()
var expected []string = nil
if diff := cmp.Diff(expected, ctx); diff != "" {
t.Fatalf("%s", diff)
_, err := kc.ContextNames()
if err == nil {
t.Fatal("expected error for non-array contexts entry")
}
}
@@ -77,13 +82,15 @@ func TestKubeconfig_CheckContextExists(t *testing.T) {
t.Fatal(err)
}
if !kc.ContextExists("c1") {
if exists, err := kc.ContextExists("c1"); err != nil || !exists {
t.Fatal("c1 actually exists; reported false")
}
if !kc.ContextExists("c2") {
if exists, err := kc.ContextExists("c2"); err != nil || !exists {
t.Fatal("c2 actually exists; reported false")
}
if kc.ContextExists("c3") {
if exists, err := kc.ContextExists("c3"); err != nil {
t.Fatal(err)
} else if exists {
t.Fatal("c3 does not exist; but reported true")
}
}

View File

@@ -14,18 +14,22 @@
package kubeconfig
import (
"fmt"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// GetCurrentContext returns "current-context" value in given
// kubeconfig object Node, or returns "" if not found.
func (k *Kubeconfig) GetCurrentContext() string {
v := valueOf(k.rootNode, "current-context")
if v == nil {
return ""
// kubeconfig object Node, or returns ("", nil) if not found.
func (k *Kubeconfig) GetCurrentContext() (string, error) {
v, err := k.config.Pipe(yaml.Get("current-context"))
if err != nil {
return "", fmt.Errorf("failed to read current-context: %w", err)
}
return v.Value
return yaml.GetValue(v), nil
}
func (k *Kubeconfig) UnsetCurrentContext() error {
curCtxValNode := valueOf(k.rootNode, "current-context")
curCtxValNode.Value = ""
return nil
return k.config.PipeE(yaml.SetField("current-context", yaml.NewStringRNode("")))
}

View File

@@ -26,7 +26,10 @@ func TestKubeconfig_GetCurrentContext(t *testing.T) {
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
v := kc.GetCurrentContext()
v, err := kc.GetCurrentContext()
if err != nil {
t.Fatal(err)
}
expected := "foo"
if v != expected {
@@ -40,7 +43,10 @@ func TestKubeconfig_GetCurrentContext_missingField(t *testing.T) {
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
v := kc.GetCurrentContext()
v, err := kc.GetCurrentContext()
if err != nil {
t.Fatal(err)
}
expected := ""
if v != expected {

View File

@@ -15,10 +15,11 @@
package kubeconfig
import (
"errors"
"fmt"
"io"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
type ReadWriteResetCloser interface {
@@ -35,8 +36,8 @@ type Loader interface {
type Kubeconfig struct {
loader Loader
f ReadWriteResetCloser
rootNode *yaml.Node
f ReadWriteResetCloser
config *yaml.RNode
}
func (k *Kubeconfig) WithLoader(l Loader) *Kubeconfig {
@@ -54,7 +55,7 @@ func (k *Kubeconfig) Close() error {
func (k *Kubeconfig) Parse() error {
files, err := k.loader.Load()
if err != nil {
return errors.Wrap(err, "failed to load")
return fmt.Errorf("failed to load: %w", err)
}
// TODO since we don't support multiple kubeconfig files at the moment, there's just 1 file
@@ -63,24 +64,31 @@ func (k *Kubeconfig) Parse() error {
k.f = f
var v yaml.Node
if err := yaml.NewDecoder(f).Decode(&v); err != nil {
return errors.Wrap(err, "failed to decode")
return fmt.Errorf("failed to decode: %w", err)
}
k.rootNode = v.Content[0]
if k.rootNode.Kind != yaml.MappingNode {
k.config = yaml.NewRNode(&v)
if k.config.YNode().Kind != yaml.MappingNode {
return errors.New("kubeconfig file is not a map document")
}
return nil
}
func (k *Kubeconfig) Bytes() ([]byte, error) {
return yaml.Marshal(k.rootNode)
str, err := k.config.String()
if err != nil {
return nil, err
}
return []byte(str), nil
}
func (k *Kubeconfig) Save() error {
if err := k.f.Reset(); err != nil {
return errors.Wrap(err, "failed to reset file")
return fmt.Errorf("failed to reset file: %w", err)
}
enc := yaml.NewEncoder(k.f)
enc.SetIndent(0)
return enc.Encode(k.rootNode)
if err := enc.Encode(k.config.YNode()); err != nil {
return err
}
return enc.Close()
}

View File

@@ -15,11 +15,12 @@
package kubeconfig
import (
"github.com/ahmetb/kubectx/internal/cmdutil"
"errors"
"fmt"
"os"
"path/filepath"
"github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
)
var (
@@ -33,15 +34,15 @@ type kubeconfigFile struct{ *os.File }
func (*StandardKubeconfigLoader) Load() ([]ReadWriteResetCloser, error) {
cfgPath, err := kubeconfigPath()
if err != nil {
return nil, errors.Wrap(err, "cannot determine kubeconfig path")
return nil, fmt.Errorf("cannot determine kubeconfig path: %w", err)
}
f, err := os.OpenFile(cfgPath, os.O_RDWR, 0)
if err != nil {
if os.IsNotExist(err) {
return nil, errors.Wrap(err, "kubeconfig file not found")
return nil, fmt.Errorf("kubeconfig file not found: %w", err)
}
return nil, errors.Wrap(err, "failed to open file")
return nil, fmt.Errorf("failed to open file: %w", err)
}
// TODO we'll return all kubeconfig files when we start implementing multiple kubeconfig support
@@ -50,10 +51,12 @@ func (*StandardKubeconfigLoader) Load() ([]ReadWriteResetCloser, error) {
func (kf *kubeconfigFile) Reset() error {
if err := kf.Truncate(0); err != nil {
return errors.Wrap(err, "failed to truncate file")
return fmt.Errorf("failed to truncate file: %w", err)
}
_, err := kf.Seek(0, 0)
return errors.Wrap(err, "failed to seek in file")
if _, err := kf.Seek(0, 0); err != nil {
return fmt.Errorf("failed to seek in file: %w", err)
}
return nil
}
func kubeconfigPath() (string, error) {

View File

@@ -15,17 +15,16 @@
package kubeconfig
import (
"github.com/ahmetb/kubectx/internal/cmdutil"
"os"
"path/filepath"
"strings"
"testing"
"github.com/ahmetb/kubectx/internal/testutil"
"github.com/ahmetb/kubectx/internal/cmdutil"
)
func Test_kubeconfigPath(t *testing.T) {
defer testutil.WithEnvVar("HOME", "/x/y/z")()
t.Setenv("HOME", "/x/y/z")
expected := filepath.FromSlash("/x/y/z/.kube/config")
got, err := kubeconfigPath()
@@ -38,9 +37,9 @@ func Test_kubeconfigPath(t *testing.T) {
}
func Test_kubeconfigPath_noEnvVars(t *testing.T) {
defer testutil.WithEnvVar("XDG_CACHE_HOME", "")()
defer testutil.WithEnvVar("HOME", "")()
defer testutil.WithEnvVar("USERPROFILE", "")()
t.Setenv("XDG_CACHE_HOME", "")
t.Setenv("HOME", "")
t.Setenv("USERPROFILE", "")
_, err := kubeconfigPath()
if err == nil {
@@ -49,7 +48,7 @@ func Test_kubeconfigPath_noEnvVars(t *testing.T) {
}
func Test_kubeconfigPath_envOvveride(t *testing.T) {
defer testutil.WithEnvVar("KUBECONFIG", "foo")()
t.Setenv("KUBECONFIG", "foo")
v, err := kubeconfigPath()
if err != nil {
@@ -62,7 +61,7 @@ func Test_kubeconfigPath_envOvveride(t *testing.T) {
func Test_kubeconfigPath_envOvverideDoesNotSupportPathSeparator(t *testing.T) {
path := strings.Join([]string{"file1", "file2"}, string(os.PathListSeparator))
defer testutil.WithEnvVar("KUBECONFIG", path)()
t.Setenv("KUBECONFIG", path)
_, err := kubeconfigPath()
if err == nil {
@@ -71,7 +70,7 @@ func Test_kubeconfigPath_envOvverideDoesNotSupportPathSeparator(t *testing.T) {
}
func TestStandardKubeconfigLoader_returnsNotFoundErr(t *testing.T) {
defer testutil.WithEnvVar("KUBECONFIG", "foo")()
t.Setenv("KUBECONFIG", "foo")
kc := new(Kubeconfig).WithLoader(DefaultLoader)
err := kc.Parse()
if err == nil {

View File

@@ -14,7 +14,9 @@
package kubeconfig
import "gopkg.in/yaml.v3"
import (
"sigs.k8s.io/kustomize/kyaml/yaml"
)
const (
defaultNamespace = "default"
@@ -25,53 +27,23 @@ func (k *Kubeconfig) NamespaceOfContext(contextName string) (string, error) {
if err != nil {
return "", err
}
ctxBody := valueOf(ctx, "context")
if ctxBody == nil {
return defaultNamespace, nil
namespace, err := ctx.Pipe(yaml.Lookup("context", "namespace"))
if namespace == nil || err != nil {
return defaultNamespace, err
}
ns := valueOf(ctxBody, "namespace")
if ns == nil || ns.Value == "" {
return defaultNamespace, nil
}
return ns.Value, nil
return yaml.GetValue(namespace), nil
}
func (k *Kubeconfig) SetNamespace(ctxName string, ns string) error {
ctxNode, err := k.contextNode(ctxName)
ctx, err := k.contextNode(ctxName)
if err != nil {
return err
}
var ctxBodyNodeWasEmpty bool // actual namespace value is in contexts[index].context.namespace, but .context might not exist
ctxBodyNode := valueOf(ctxNode, "context")
if ctxBodyNode == nil {
ctxBodyNodeWasEmpty = true
ctxBodyNode = &yaml.Node{
Kind: yaml.MappingNode,
}
}
nsNode := valueOf(ctxBodyNode, "namespace")
if nsNode != nil {
nsNode.Value = ns
return nil
}
keyNode := &yaml.Node{
Kind: yaml.ScalarNode,
Value: "namespace",
Tag: "!!str"}
valueNode := &yaml.Node{
Kind: yaml.ScalarNode,
Value: ns,
Tag: "!!str"}
ctxBodyNode.Content = append(ctxBodyNode.Content, keyNode, valueNode)
if ctxBodyNodeWasEmpty {
ctxNode.Content = append(ctxNode.Content, &yaml.Node{
Kind: yaml.ScalarNode,
Value: "context",
Tag: "!!str",
}, ctxBodyNode)
if err := ctx.PipeE(
yaml.LookupCreate(yaml.MappingNode, "context"),
yaml.SetField("namespace", yaml.NewStringRNode(ns)),
); err != nil {
return err
}
return nil
}

View File

@@ -18,8 +18,6 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/ahmetb/kubectx/internal/testutil"
)
var (
@@ -27,8 +25,8 @@ var (
)
func Test_useColors_forceColors(t *testing.T) {
defer testutil.WithEnvVar("_KUBECTX_FORCE_COLOR", "1")()
defer testutil.WithEnvVar("NO_COLOR", "1")()
t.Setenv("_KUBECTX_FORCE_COLOR", "1")
t.Setenv("NO_COLOR", "1")
if v := useColors(); !cmp.Equal(v, &tr) {
t.Fatalf("expected useColors() = true; got = %v", v)
@@ -36,7 +34,7 @@ func Test_useColors_forceColors(t *testing.T) {
}
func Test_useColors_disableColors(t *testing.T) {
defer testutil.WithEnvVar("NO_COLOR", "1")()
t.Setenv("NO_COLOR", "1")
if v := useColors(); !cmp.Equal(v, &fa) {
t.Fatalf("expected useColors() = false; got = %v", v)
@@ -44,8 +42,8 @@ func Test_useColors_disableColors(t *testing.T) {
}
func Test_useColors_default(t *testing.T) {
defer testutil.WithEnvVar("NO_COLOR", "")()
defer testutil.WithEnvVar("_KUBECTX_FORCE_COLOR", "")()
t.Setenv("NO_COLOR", "")
t.Setenv("_KUBECTX_FORCE_COLOR", "")
if v := useColors(); v != nil {
t.Fatalf("expected useColors() = nil; got=%v", *v)

View File

@@ -43,17 +43,17 @@ func init() {
}
}
func Error(w io.Writer, format string, args ...interface{}) error {
_, err := fmt.Fprintf(w, ErrorColor.Sprint("error: ")+format+"\n", args...)
func Error(w io.Writer, format string, args ...any) error {
_, err := io.WriteString(w, ErrorColor.Sprint("error: ")+fmt.Sprintf(format, args...)+"\n")
return err
}
func Warning(w io.Writer, format string, args ...interface{}) error {
_, err := fmt.Fprintf(w, WarningColor.Sprint("warning: ")+format+"\n", args...)
func Warning(w io.Writer, format string, args ...any) error {
_, err := io.WriteString(w, WarningColor.Sprint("warning: ")+fmt.Sprintf(format, args...)+"\n")
return err
}
func Success(w io.Writer, format string, args ...interface{}) error {
_, err := fmt.Fprintf(w, SuccessColor.Sprint("✔ ")+fmt.Sprintf(format+"\n", args...))
func Success(w io.Writer, format string, args ...any) error {
_, err := io.WriteString(w, SuccessColor.Sprint("✔ ")+fmt.Sprintf(format, args...)+"\n")
return err
}

View File

@@ -18,7 +18,7 @@ import (
"strings"
"testing"
"gopkg.in/yaml.v3"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
type Context struct {
@@ -31,7 +31,7 @@ type Context struct {
func Ctx(name string) *Context { return &Context{Name: name} }
func (c *Context) Ns(ns string) *Context { c.Context.Namespace = ns; return c }
type Kubeconfig map[string]interface{}
type Kubeconfig map[string]any
func KC() *Kubeconfig {
return &Kubeconfig{
@@ -39,14 +39,16 @@ func KC() *Kubeconfig {
"kind": "Config"}
}
func (k *Kubeconfig) Set(key string, v interface{}) *Kubeconfig { (*k)[key] = v; return k }
func (k *Kubeconfig) WithCurrentCtx(s string) *Kubeconfig { (*k)["current-context"] = s; return k }
func (k *Kubeconfig) WithCtxs(c ...*Context) *Kubeconfig { (*k)["contexts"] = c; return k }
func (k *Kubeconfig) Set(key string, v any) *Kubeconfig { (*k)[key] = v; return k }
func (k *Kubeconfig) WithCurrentCtx(s string) *Kubeconfig { (*k)["current-context"] = s; return k }
func (k *Kubeconfig) WithCtxs(c ...*Context) *Kubeconfig { (*k)["contexts"] = c; return k }
func (k *Kubeconfig) ToYAML(t *testing.T) string {
t.Helper()
var v strings.Builder
if err := yaml.NewEncoder(&v).Encode(*k); err != nil {
enc := yaml.NewEncoder(&v)
enc.SetIndent(0)
if err := enc.Encode(*k); err != nil {
t.Fatalf("failed to encode mock kubeconfig: %v", err)
}
return v.String()

View File

@@ -1,40 +0,0 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package testutil
import (
"io/ioutil"
"os"
"testing"
)
func TempFile(t *testing.T, contents string) (path string, cleanup func()) {
// TODO consider removing, used only in one place.
t.Helper()
f, err := ioutil.TempFile(os.TempDir(), "test-file")
if err != nil {
t.Fatalf("failed to create test file: %v", err)
}
path = f.Name()
if _, err := f.Write([]byte(contents)); err != nil {
t.Fatalf("failed to write to test file: %v", err)
}
return path, func() {
f.Close()
os.Remove(path)
}
}

View File

@@ -1,31 +0,0 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package testutil
import "os"
// WithEnvVar sets an env var temporarily. Call its return value
// in defer to restore original value in env (if exists).
func WithEnvVar(key, value string) func() {
orig, ok := os.LookupEnv(key)
os.Setenv(key, value)
return func() {
if ok {
os.Setenv(key, orig)
} else {
os.Unsetenv(key)
}
}
}