Compare commits

..

60 Commits

Author SHA1 Message Date
Ahmet Alp Balkan
711beb5fc5 fix: skip fzf launch in kubens when no contexts exist in kubeconfig
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 00:28:28 -07: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
Ahmet Alp Balkan
c52b598c2c docs: add description and legacy note to bash --help output (#459)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 13:28:53 -07:00
justin0u0
013b6bc252 fix(kubectx): use the env variable for kubectl (#438) 2025-01-22 09:07:57 -08:00
Antoine
0bcd0d5dd5 fix some ci enhancement (#435)
* fix(release): customize goreleaser config file, by adding json schema and fixing configuration version

* fix(go): rename invalid comment format

* fix(ci): made release workflow work again

replace goreleaser --rm-dist flag by --clean
increment go version for release pipeline
fetch previous tags use by goreleaser
give release workflow content write permissions to publish release
2025-01-07 12:25:01 -08:00
Suleiman Dibirov
561793c356 chore: upgrade Go version to 1.22 (#425) 2024-07-10 14:25:37 -07:00
Suleiman Dibirov
b5daf2cef7 feat(kubens): added force flag to switch namespaces even if it doesn'… (#416)
* feat(kubens): added force flag to switch namespaces even if it doesn't exist

* Merged lines

* fixed README.md and flags.go

* updated flags.go, flags_test.go
2024-07-09 21:38:40 -07:00
pullmerge
4997a261dc Fix some comments (#418) 2024-04-15 09:31:07 -07:00
Marcos Alano
8fb8c9f2f2 Add support to publish Snaps using goreleaser (#353)
Closes #351.
2023-12-25 00:28:51 -08:00
Nabil
11c19c0fb7 Add Scoop command (#396) 2023-08-04 15:04:50 -07:00
Ahmet Alp Balkan
92e5b5f43b Release v0.9.5 2023-07-13 20:12:27 -07:00
Ahmet Alp Balkan
33c27c03b2 k8s.io & go version bump (#393) 2023-07-13 20:10:47 -07:00
niko2
7560b8f04f Modify oh-my-zsh example (#346) 2023-03-15 16:30:08 -07:00
Ahmet Alp Balkan
d8ff2847ba fix go.sum tidy
Signed-off-by: Ahmet Alp Balkan <ahmetalpbalkan@gmail.com>
2023-02-24 12:03:11 -08:00
Gastón Haro
da454d8a0c Fix README.md "alias" to "rename" (#375) 2023-02-24 09:20:16 -08:00
Nikolas Grottendieck
021c1bc736 Windows installation instructions (winget) (#365) (#381) 2023-02-24 09:14:12 -08:00
Vincent Victoria
29850e1a75 Add prerequisite for completion to work after brew install (#368) 2022-10-24 12:06:54 -07:00
Sandro
7d6b179aed Bump golang.org/x/sys to fix compilation on M1 macs (#360) 2022-08-04 08:41:17 -07:00
Apoorv Verma [AP]
e5e7f53336 Windows installation instructions (Chocolatey) (#350) 2022-03-16 00:08:53 -07:00
Gábor Lipták
e6de7ba0a2 Run Go 1.17 mod tiny (#336) 2022-01-10 21:00:11 -08:00
SADIK KUZU
b6b364685a Fix typo on README.md (#341) 2022-01-09 09:39:34 -08:00
Ahmet Alp Balkan
617e4f0562 add build cache to actions (#339) 2021-12-26 21:50:17 -08:00
Kai
60523045a5 README.md: HTTP => HTTPS (#337) 2021-11-28 08:23:09 -08:00
Ahmet Alp Balkan
38117be348 go mod tidy
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2021-11-27 21:34:14 -08:00
Gábor Lipták
f123e3864e Bump Go to 1.17 in GHA (#335) 2021-11-27 10:47:19 -08:00
Ahmet Alp Balkan
207dd606bb overhaul the readme
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
2021-11-12 12:53:40 -08:00
Joshua Gleitze
bdb1ea9e9d Make Completions Installable with Antibody (#263) 2021-11-04 08:34:23 -07:00
Ahmet Alp Balkan
a509657288 Update README.md 2021-10-21 11:15:52 -07:00
Johan Dewe
e449e739f8 Wrap context names in single quotes to prevent completion script to fail (#316) 2021-08-19 10:04:48 -07:00
Carlos Alexandro Becker
33212062fb docs: fix starchart url (#306) 2021-07-18 18:33:51 -07:00
Yaakov Selkowitz
13695147d1 Add s390x build (#270) 2021-07-08 17:21:05 -07:00
Yankee
58a5c4693e Add interactive delete op (#304) 2021-07-07 11:48:32 -07:00
59 changed files with 1418 additions and 1112 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,23 +23,37 @@ jobs:
- name: Checkout
uses: actions/checkout@master
- name: Setup Go
uses: actions/setup-go@v1
uses: actions/setup-go@v6
with:
go-version: 1.16
go-version: '1.25'
- id: go-cache-paths
run: |
echo "::set-output name=go-build::$(go env GOCACHE)"
echo "::set-output name=go-mod::$(go env GOMODCACHE)"
- name: Go Build Cache
uses: actions/cache@v5
with:
path: ${{ steps.go-cache-paths.outputs.go-build }}
key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }}
- name: Go Mod Cache
uses: actions/cache@v5
with:
path: ${{ steps.go-cache-paths.outputs.go-mod }}
key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}
- name: Ensure gofmt
run: test -z "$(gofmt -s -d .)"
- name: Ensure go.mod is already tidied
run: go mod tidy && git diff --no-patch --exit-code
run: go mod tidy && git diff --exit-code
- name: Run unit tests
run: go test ./...
- name: Build with Goreleaser
uses: goreleaser/goreleaser-action@v1
uses: goreleaser/goreleaser-action@v7
with:
version: latest
args: release --snapshot --skip-publish --rm-dist
args: release --snapshot --skip publish,snapcraft --clean
- name: Setup BATS framework
run: sudo npm install -g bats
- name: kubectx (Go) integration tests
run: COMMAND=./dist/kubectx_linux_amd64/kubectx bats test/kubectx.bats
run: COMMAND=./dist/kubectx_linux_amd64_v1/kubectx bats test/kubectx.bats
- name: kubens (Go) integration tests
run: COMMAND=./dist/kubens_linux_amd64/kubens bats test/kubens.bats
run: COMMAND=./dist/kubens_linux_amd64_v1/kubens bats test/kubens.bats

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

@@ -19,26 +19,40 @@ on:
- 'v*.*.*'
jobs:
goreleaser:
permissions:
contents: write
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@v1
uses: actions/setup-go@v6
with:
go-version: 1.16
go-version: '1.25'
- name: Install Snapcraft
uses: samuelmeuli/action-snapcraft@v3
- name: Setup Snapcraft
run: |
# https://github.com/goreleaser/goreleaser/issues/1715
mkdir -p $HOME/.cache/snapcraft/download
mkdir -p $HOME/.cache/snapcraft/stage-packages
- name: GoReleaser
uses: goreleaser/goreleaser-action@v2
uses: goreleaser/goreleaser-action@v7
with:
version: latest
args: release --rm-dist
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)
run: for snap in $(ls dist/*.snap); do snapcraft upload --release=stable $snap; done
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}

View File

@@ -1,3 +1,5 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,7 +15,9 @@
# 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:
hooks:
- go mod download
@@ -32,6 +36,7 @@ builds:
- arm
- arm64
- ppc64le
- s390x
goarm: [6, 7]
- id: kubens
main: ./cmd/kubens
@@ -47,41 +52,48 @@ builds:
- arm
- arm64
- ppc64le
- s390x
goarm: [6, 7]
archives:
- id: kubectx-archive
name_template: |-
kubectx_{{ .Tag }}_{{ .Os }}_{{ .Arch -}}
kubectx_{{ .Tag }}_{{ .Os }}_
{{- with .Arch -}}
{{- if (eq . "386") -}}i386
{{- else if (eq . "amd64") -}}x86_64
{{- else -}}{{- . -}}
{{- end -}}
{{ end }}
{{- with .Arm -}}
{{- if (eq . "6") -}}hf
{{- else -}}v{{- . -}}
{{- end -}}
{{- end -}}
builds:
ids:
- kubectx
replacements:
386: i386
amd64: x86_64
format_overrides:
- goos: windows
format: zip
formats: [zip]
files: ["LICENSE"]
- id: kubens-archive
name_template: |-
kubens_{{ .Tag }}_{{ .Os }}_{{ .Arch -}}
kubens_{{ .Tag }}_{{ .Os }}_
{{- with .Arch -}}
{{- if (eq . "386") -}}i386
{{- else if (eq . "amd64") -}}x86_64
{{- else -}}{{- . -}}
{{- end -}}
{{ end }}
{{- with .Arm -}}
{{- if (eq . "6") -}}hf
{{- else -}}v{{- . -}}
{{- end -}}
{{- end -}}
builds:
ids:
- kubens
replacements:
386: i386
amd64: x86_64
format_overrides:
- goos: windows
format: zip
formats: [zip]
files: ["LICENSE"]
checksum:
name_template: "checksums.txt"
@@ -90,3 +102,20 @@ release:
extra_files:
- glob: ./kubens
- glob: ./kubectx
snapcrafts:
- id: kubectx
name: kubectx
summary: 'kubectx + kubens: Power tools for kubectl'
description: |
kubectx is a tool to switch between contexts (clusters) on kubectl faster.
kubens is a tool to switch between Kubernetes namespaces (and configure them for kubectl) easily.
grade: stable
confinement: classic
base: core24
apps:
kubectx:
command: kubectx
completer: completion/kubectx.bash
kubens:
command: kubens
completer: completion/kubens.bash

314
README.md
View File

@@ -2,285 +2,203 @@
![Latest GitHub release](https://img.shields.io/github/release/ahmetb/kubectx.svg)
![GitHub stars](https://img.shields.io/github/stars/ahmetb/kubectx.svg?label=github%20stars)
![Homebrew downloads](https://img.shields.io/homebrew/installs/dy/kubectx?label=macOS%20installs)
[![Go implementation (CI)](https://github.com/ahmetb/kubectx/workflows/Go%20implementation%20(CI)/badge.svg)](https://github.com/ahmetb/kubectx/actions?query=workflow%3A"Go+implementation+(CI)")
![Proudly written in Bash](https://img.shields.io/badge/written%20in-bash-ff69b4.svg)
This repository provides both `kubectx` and `kubens` tools.
[Install &rarr;](#installation)
> **🥳📰 NEWS:** With v0.9.0 `kubectx` and `kubens` **are now rewritten in Go**.
> (Don't worry, our lovely **bash** versions are still available!) Please test
> this new Go binaries by downloading them from
> [**Releases &rarr;**](https://github.com/ahmetb/kubectx/releases)
## What are `kubectx` and `kubens`?
**`kubectx`** helps you switch between clusters back and forth:
**kubectx** is a tool to switch between contexts (clusters) on kubectl
faster.<br/>
**kubens** is a tool to switch between Kubernetes namespaces (and
configure them for kubectl) easily.
Here's a **`kubectx`** demo:
![kubectx demo GIF](img/kubectx-demo.gif)
**`kubens`** helps you switch between Kubernetes namespaces smoothly:
...and here's a **`kubens`** demo:
![kubens demo GIF](img/kubens-demo.gif)
# kubectx(1)
kubectx is a utility to manage and switch between kubectl(1) contexts.
```
USAGE:
kubectx : list the contexts
kubectx <NAME> : switch to context <NAME>
kubectx - : switch to the previous context
kubectx -c, --current : show the current context name
kubectx <NEW_NAME>=<NAME> : rename context <NAME> to <NEW_NAME>
kubectx <NEW_NAME>=. : rename current-context to <NEW_NAME>
kubectx -d <NAME> : delete context <NAME> ('.' for current-context)
(this command won't delete the user/cluster entry
that is used by the context)
kubectx -u, --unset : unset the current context
```
### Usage
### Examples
```sh
# switch to another cluster that's in kubeconfig
$ kubectx minikube
Switched to context "minikube".
# switch back to previous cluster
$ kubectx -
Switched to context "oregon".
$ kubectx -
Switched to context "minikube".
# start an "isolated shell" that only has a single context
$ kubectx -s minikube
# rename context
$ kubectx dublin=gke_ahmetb_europe-west1-b_dublin
Context "dublin" set.
Aliased "gke_ahmetb_europe-west1-b_dublin" as "dublin".
```
Context "gke_ahmetb_europe-west1-b_dublin" renamed to "dublin".
`kubectx` supports <kbd>Tab</kbd> completion on bash/zsh/fish shells to help with
long context names. You don't have to remember full context names anymore.
-----
# kubens(1)
kubens is a utility to switch between Kubernetes namespaces.
```
USAGE:
kubens : list the namespaces
kubens <NAME> : change the active namespace
kubens - : switch to the previous namespace
kubens -c, --current : show the current namespace
```
### Usage
```sh
# change the active namespace on kubectl
$ kubens kube-system
Context "test" set.
Active namespace is "kube-system".
# go back to the previous namespace
$ kubens -
Context "test" set.
Active namespace is "default".
# change the active namespace even if it doesn't exist
$ kubens not-found-namespace --force
Context "test" set.
Active namespace is "not-found-namespace".
---
$ kubens not-found-namespace -f
Context "test" set.
Active namespace is "not-found-namespace".
```
`kubens` also supports <kbd>Tab</kbd> completion on bash/zsh/fish shells.
If you have [`fzf`](https://github.com/junegunn/fzf) installed, you can also
**interactively** select a context or cluster, or fuzzy-search by typing a few
characters. To learn more, read [interactive mode &rarr;](#interactive-mode)
Both `kubectx` and `kubens` support <kbd>Tab</kbd> completion on bash/zsh/fish
shells to help with long context names. You don't have to remember full context
names anymore.
-----
## Installation
> **🥳📰 NEWS:** With v0.9.0 `kubectx` and `kubens` **are now rewritten in Go**.
> (Don't worry, our lovely **bash** versions are still available!) Please test
> this new Go binaries by downloading them from
> [**Releases &rarr;**](https://github.com/ahmetb/kubectx/releases)
| 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` |
There are several installation options:
Alternatively, download binaries from the [**Releases page &rarr;**](https://github.com/ahmetb/kubectx/releases) and add them to somewhere in your `PATH`.
- As kubectl plugins (macOS/Linux)
- macOS
- Homebrew (recommended)
- MacPorts
- Linux
- Debian
- Arch Linux
- Homebrew
- Manual installation
<details>
<summary>Shell completion scripts</summary>
### Kubectl Plugins (macOS and Linux)
#### zsh (with [antibody](https://getantibody.github.io))
You can install and use [Krew](https://github.com/kubernetes-sigs/krew/) kubectl
plugin manager to get `kubectx` and `kubens`. **NOTE:** This will not install
shell completion scripts, if you want those, choose another installation method
below.
Add this line to your [Plugins File](https://getantibody.github.io/usage/) (e.g.
`~/.zsh_plugins.txt`):
```sh
kubectl krew install ctx
kubectl krew install ns
```
ahmetb/kubectx path:completion kind:fpath
```
After installing, the tools will be available as `kubectl ctx` and `kubectl ns`.
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`.
### macOS
#### zsh (plain)
#### Homebrew
The completion scripts have to be in a path that belongs to `$fpath`. Either
link or copy them to an existing folder.
:confetti_ball: If you use [Homebrew](https://brew.sh/) you can install like this:
brew install kubectx
This command will set up bash/zsh/fish completion scripts automatically.
- If you like to add context/namespace info to your shell prompt (`$PS1`),
I recommend trying out [kube-ps1](https://github.com/jonmosco/kube-ps1).
#### MacPorts
If you use [MacPorts](https://www.macports.org) you can install like this:
sudo port install kubectx
### Linux
#### 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)_):
#### Arch Linux
Available as official Arch Linux package. Install it via:
Example with [`oh-my-zsh`](https://github.com/ohmyzsh/ohmyzsh):
```bash
sudo pacman -S kubectx
mkdir -p ~/.oh-my-zsh/custom/completions
chmod -R 755 ~/.oh-my-zsh/custom/completions
ln -s /opt/kubectx/completion/_kubectx.zsh ~/.oh-my-zsh/custom/completions/_kubectx.zsh
ln -s /opt/kubectx/completion/_kubens.zsh ~/.oh-my-zsh/custom/completions/_kubens.zsh
echo "fpath=($ZSH/custom/completions $fpath)" >> ~/.zshrc
```
#### Homebrew
If completion doesn't work, add `autoload -U compinit && compinit` to your
`.zshrc` (similar to
[`zsh-completions`](https://github.com/zsh-users/zsh-completions/blob/master/README.md#oh-my-zsh)).
:confetti_ball: If you use [Homebrew](https://brew.sh/) you can install like this:
If you are not using [`oh-my-zsh`](https://github.com/ohmyzsh/ohmyzsh), you
could link to `/usr/share/zsh/functions/Completion` (might require sudo),
depending on the `$fpath` of your zsh installation.
brew install kubectx
In case of errors, calling `compaudit` might help.
This command will set up bash/zsh/fish completion scripts automatically.
#### bash
- If you like to add context/namespace info to your shell prompt (`$PS1`),
I recommend trying out [kube-ps1](https://github.com/jonmosco/kube-ps1).
#### Manual
Since `kubectx`/`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 ...`)
- Install bash/zsh/fish [completion scripts](completion/).
- For zsh:
The completion scripts have to be in a path that belongs to `$fpath`. Either link or copy them to an existing folder.
If using oh-my-zsh you can do as follows:
```bash
mkdir -p ~/.oh-my-zsh/completions
chmod -R 755 ~/.oh-my-zsh/completions
ln -s /opt/kubectx/completion/kubectx.zsh ~/.oh-my-zsh/completions/_kubectx.zsh
ln -s /opt/kubectx/completion/kubens.zsh ~/.oh-my-zsh/completions/_kubens.zsh
```
Note that the leading underscore seems to be a convention. If completion doesn't work, add `autoload -U compinit && compinit` to your `.zshrc` (similar to [`zsh-completions`](https://github.com/zsh-users/zsh-completions/blob/master/README.md#oh-my-zsh)).
If not using oh-my-zsh, you could link to `/usr/share/zsh/functions/Completion` (might require sudo), depending on the `$fpath` of your zsh installation.
In case of error, calling `compaudit` might help.
- For bash:
```bash
git clone https://github.com/ahmetb/kubectx.git ~/.kubectx
COMPDIR=$(pkg-config --variable=completionsdir bash-completion)
ln -sf ~/.kubectx/completion/kubens.bash $COMPDIR/kubens
ln -sf ~/.kubectx/completion/kubectx.bash $COMPDIR/kubectx
cat << FOE >> ~/.bashrc
```bash
git clone https://github.com/ahmetb/kubectx.git ~/.kubectx
COMPDIR=$(pkg-config --variable=completionsdir bash-completion)
ln -sf ~/.kubectx/completion/kubens.bash $COMPDIR/kubens
ln -sf ~/.kubectx/completion/kubectx.bash $COMPDIR/kubectx
cat << EOF >> ~/.bashrc
#kubectx and kubens
export PATH=~/.kubectx:\$PATH
FOE
```
- For fish:
```fish
mkdir -p ~/.config/fish/completions
ln -s /opt/kubectx/completion/kubectx.fish ~/.config/fish/completions/
ln -s /opt/kubectx/completion/kubens.fish ~/.config/fish/completions/
```
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
#kubectx and kubens
export PATH=~/.kubectx:\$PATH
EOF
```
#### fish
```fish
mkdir -p ~/.config/fish/completions
ln -s /opt/kubectx/completion/kubectx.fish ~/.config/fish/completions/
ln -s /opt/kubectx/completion/kubens.fish ~/.config/fish/completions/
```
</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
If you want `kubectx` and `kubens` commands to present you an interactive menu
with fuzzy searching, you just need to [install
`fzf`](https://github.com/junegunn/fzf) in your PATH.
`fzf`](https://github.com/junegunn/fzf) in your `$PATH`.
![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 using Unix composability:
```
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 `).
-----
### Customizing colors
If you like to customize the colors indicating the current namespace or context, set the environment variables `KUBECTX_CURRENT_FGCOLOR` and `KUBECTX_CURRENT_BGCOLOR` (refer color codes [here](https://linux.101hacks.com/ps1-examples/prompt-color-using-tput/)):
If you like to customize the colors indicating the current namespace or context,
set the environment variables `KUBECTX_CURRENT_FGCOLOR` and
`KUBECTX_CURRENT_BGCOLOR` (refer color codes
[here](https://linux.101hacks.com/ps1-examples/prompt-color-using-tput/)):
```
```sh
export KUBECTX_CURRENT_FGCOLOR=$(tput setaf 6) # blue text
export KUBECTX_CURRENT_BGCOLOR=$(tput setab 7) # white background
```
Colors in the output can be disabled by setting the
[`NO_COLOR`](http://no-color.org/) environment variable.
[`NO_COLOR`](https://no-color.org/) environment variable.
-----
#### Users
| What are others saying about kubectx? |
| ---- |
| _“Thank you for kubectx & kubens - I use them all the time & have them in my k8s toolset to maintain happiness :) ”_ [@pbouwer](https://twitter.com/pbouwer/status/925896377929949184) |
| _“I can't imagine working without kubectx and especially kubens anymore. It's pure gold.”_ [@timoreimann](https://twitter.com/timoreimann/status/925801946757419008) |
| _“I'm liking kubectx from @ahmetb, makes it super-easy to switch #Kubernetes contexts [...]”_ &mdash; [@lizrice](https://twitter.com/lizrice/status/928556415517589505) |
| _“Also using it on a daily basis. This and my zsh config that shows me the current k8s context 😉”_ [@puja108](https://twitter.com/puja108/status/928742521139810305) |
| _“Lately I've found myself using the kubens command more than kubectx. Both very useful though :-)”_ [@stuartleeks](https://twitter.com/stuartleeks/status/928562850464907264) |
| _“yeah kubens rocks!”_ [@embano1](https://twitter.com/embano1/status/928698440732815360) |
| _“Special thanks to Ahmet Alp Balkan for creating kubectx, kubens, and kubectl aliases, as these tools made my life better.”_ [@strebeld](https://medium.com/@strebeld/5-ways-to-enhance-kubectl-ux-97c8893227a) |
| _“❤ this shell script @ahmetb wrote to help make switching between kubectl config contexts a breeze.”_ [@briandanowski](https://twitter.com/briandanowski/status/1085409568165896193) |
> If you liked `kubectx`, you may like my [`kubectl-aliases`](https://github.com/ahmetb/kubectl-aliases) project, too.
-----
Disclaimer: This is not an official Google product.
If you liked `kubectx`, you may like my
[`kubectl-aliases`](https://github.com/ahmetb/kubectl-aliases) project, too. I
recommend pairing kubectx and kubens with [fzf](#interactive-mode) and
[kube-ps1](https://github.com/jonmosco/kube-ps1).
#### Stargazers over time
[![Stargazers over time](https://starcharts.herokuapp.com/ahmetb/kubectx.svg)](https://starcharts.herokuapp.com/ahmetb/kubectx)
![Google Analytics](https://ga-beacon.appspot.com/UA-2609286-17/kubectx/README?pixel)
[![Stargazers over time](https://starchart.cc/ahmetb/kubectx.svg)](https://starchart.cc/ahmetb/kubectx)
![Google Analytics](https://ga-beacon.appspot.com/UA-2609286-17/kubectx/README?pixel) <!-- TODO broken since Aug 2021 as igrigorik left Google -->

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 inefficency here. we open/write/close the same file many times.
// 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,9 +40,20 @@ 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 {
return UnsupportedOp{Err: fmt.Errorf("'-d' needs arguments")}
if cmdutil.IsInteractiveMode(os.Stdout) {
return InteractiveDeleteOp{SelfCmd: os.Args[0]}
} else {
return UnsupportedOp{Err: fmt.Errorf("'-d' needs arguments")}
}
}
return DeleteOp{Contexts: argv[1:]}
}

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"
@@ -34,17 +33,32 @@ type InteractiveSwitchOp struct {
SelfCmd string
}
type InteractiveDeleteOp struct {
SelfCmd string
}
func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error {
if err := checkIsolatedMode(); err != nil {
return err
}
// parse kubeconfig just to see if it can be loaded
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
if cmdutil.IsNotFoundErr(err) {
printer.Warning(stderr, "kubeconfig file not found")
return nil
}
return 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
@@ -56,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
}
}
@@ -66,8 +81,67 @@ 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 fmt.Errorf("kubeconfig error: %w", err)
}
ctxNames, err := kc.ContextNames()
if err != nil {
return fmt.Errorf("failed to get context names: %w", err)
}
if len(ctxNames) == 0 {
return errors.New("no contexts found in config")
}
cmd := exec.Command("fzf", "--ansi", "--no-preview")
var out bytes.Buffer
cmd.Stdin = os.Stdin
cmd.Stderr = stderr
cmd.Stdout = &out
cmd.Env = append(os.Environ(),
fmt.Sprintf("FZF_DEFAULT_COMMAND=%s", op.SelfCmd),
fmt.Sprintf("%s=1", env.EnvForceColor))
if err := cmd.Run(); err != nil {
var exitErr *exec.ExitError
if !errors.As(err, &exitErr) {
return err
}
}
choice := strings.TrimSpace(out.String())
if choice == "" {
return errors.New("you did not choose any of the options")
}
name, wasActiveContext, err := deleteContext(choice)
if err != nil {
return fmt.Errorf("failed to delete context: %w", err)
}
if wasActiveContext {
printer.Warning(stderr, "You deleted the current context. Use \"%s\" to select a new context.",
selfName())
}
_ = printer.Success(stderr, `Deleted context %s.`, printer.SuccessColor.Sprint(name))
return nil
}

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,18 +3,19 @@ package main
import (
"fmt"
"io"
"github.com/pkg/errors"
)
var (
version = "v0.0.0+unknown" // populated by goreleaser
)
// VersionOps describes printing version string.
// VersionOp describes printing version string.
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

@@ -18,6 +18,7 @@ import (
"fmt"
"io"
"os"
"slices"
"strings"
"github.com/ahmetb/kubectx/internal/cmdutil"
@@ -33,28 +34,53 @@ func (op UnsupportedOp) Run(_, _ io.Writer) error {
// parseArgs looks at flags (excl. executable name, i.e. argv[0])
// and decides which operation should be taken.
func parseArgs(argv []string) Op {
if len(argv) == 0 {
n := len(argv)
if n == 0 {
if cmdutil.IsInteractiveMode(os.Stdout) {
return InteractiveSwitchOp{SelfCmd: os.Args[0]}
}
return ListOp{}
}
if len(argv) == 1 {
if n == 1 {
v := argv[0]
if v == "--help" || v == "-h" {
switch v {
case "--help", "-h":
return HelpOp{}
}
if v == "--version" || v == "-V" {
case "--version", "-V":
return VersionOp{}
}
if v == "--current" || v == "-c" {
case "--current", "-c":
return CurrentOp{}
case "--unset", "-u":
return UnsetOp{}
default:
return getSwitchOp(v, false)
}
if strings.HasPrefix(v, "-") && v != "-" {
return UnsupportedOp{Err: fmt.Errorf("unsupported option '%s'", v)}
} else if n == 2 {
// {namespace} -f|--force
name := argv[0]
force := slices.Contains([]string{"-f", "--force"}, argv[1])
if !force {
if !slices.Contains([]string{"-f", "--force"}, argv[0]) {
return UnsupportedOp{Err: fmt.Errorf("unsupported arguments %q", argv)}
}
// -f|--force {namespace}
force = true
name = argv[1]
}
return SwitchOp{Target: argv[0]}
return getSwitchOp(name, force)
}
return UnsupportedOp{Err: fmt.Errorf("too many arguments")}
}
func getSwitchOp(v string, force bool) Op {
if strings.HasPrefix(v, "-") && v != "-" {
return UnsupportedOp{Err: fmt.Errorf("unsupported option %q", v)}
}
return SwitchOp{Target: v, Force: force}
}

View File

@@ -45,15 +45,39 @@ 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"}},
{name: "switch by name force short flag",
args: []string{"foo", "-f"},
want: SwitchOp{Target: "foo", Force: true}},
{name: "switch by name force long flag",
args: []string{"foo", "--force"},
want: SwitchOp{Target: "foo", Force: true}},
{name: "switch by name force short flag before name",
args: []string{"-f", "foo"},
want: SwitchOp{Target: "foo", Force: true}},
{name: "switch by name force long flag before name",
args: []string{"--force", "foo"},
want: SwitchOp{Target: "foo", Force: true}},
{name: "switch by name unknown arguments",
args: []string{"foo", "-x"},
want: UnsupportedOp{Err: fmt.Errorf("unsupported arguments %q", []string{"foo", "-x"})}},
{name: "switch by name unknown arguments",
args: []string{"-x", "foo"},
want: UnsupportedOp{Err: fmt.Errorf("unsupported arguments %q", []string{"-x", "foo"})}},
{name: "switch by swap",
args: []string{"-"},
want: SwitchOp{Target: "-"}},
{name: "unrecognized flag",
args: []string{"-x"},
want: UnsupportedOp{Err: fmt.Errorf("unsupported option '-x'")}},
want: UnsupportedOp{Err: fmt.Errorf("unsupported option %q", "-x")}},
{name: "too many args",
args: []string{"a", "b", "c"},
want: UnsupportedOp{Err: fmt.Errorf("too many arguments")}},

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
}
}
@@ -65,10 +73,10 @@ func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error {
if choice == "" {
return errors.New("you did not choose any of the options")
}
name, err := switchNamespace(kc, choice)
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.
@@ -35,16 +33,21 @@ func printUsage(out io.Writer) error {
help := `USAGE:
%PROG% : list the namespaces in the current context
%PROG% <NAME> : change the active namespace of current context
%PROG% <NAME> --force/-f : force change the active namespace of current context (even if it doesn't exist)
%PROG% - : switch to the previous namespace in this context
%PROG% -c, --current : show the current namespace
%PROG% -h,--help : show this message
%PROG% -u,--unset : unset the namespace choice (set to 'default')
%PROG% -V,--version : show version`
// TODO this replace logic is duplicated between this and kubectx
help = strings.ReplaceAll(help, "%PROG%", selfName())
_, err := fmt.Fprintf(out, "%s\n", help)
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"
@@ -29,16 +30,17 @@ import (
type SwitchOp struct {
Target string // '-' for back and forth, or NAME
Force bool // force switch even if the namespace doesn't exist
}
func (s SwitchOp) Run(_, stderr io.Writer) error {
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
return errors.Wrap(err, "kubeconfig error")
return fmt.Errorf("kubeconfig error: %w", err)
}
toNS, err := switchNamespace(kc, s.Target)
toNS, err := switchNamespace(kc, s.Target, s.Force)
if err != nil {
return err
}
@@ -46,46 +48,51 @@ func (s SwitchOp) Run(_, stderr io.Writer) error {
return err
}
func switchNamespace(kc *kubeconfig.Kubeconfig, ns string) (string, error) {
ctx := kc.GetCurrentContext()
func switchNamespace(kc *kubeconfig.Kubeconfig, ns string, force bool) (string, error) {
ctx, err := kc.GetCurrentContext()
if err != nil {
return "", fmt.Errorf("failed to get current context: %w", err)
}
if ctx == "" {
return "", errors.New("current-context is not set")
}
curNS, err := kc.NamespaceOfContext(ctx)
if err != nil {
return "", 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
}
ok, err := namespaceExists(kc, ns)
if err != nil {
return "", errors.Wrap(err, "failed to query if namespace exists (is cluster accessible?)")
}
if !ok {
return "", errors.Errorf("no namespace exists with name \"%s\"", ns)
if !force {
ok, err := namespaceExists(kc, ns)
if err != nil {
return "", fmt.Errorf("failed to query if namespace exists (is cluster accessible?): %w", err)
}
if !ok {
return "", fmt.Errorf("no namespace exists with name \"%s\"", ns)
}
}
if err := kc.SetNamespace(ctx, ns); err != nil {
return "", 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
@@ -99,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,18 +3,19 @@ package main
import (
"fmt"
"io"
"github.com/pkg/errors"
)
var (
version = "v0.0.0+unknown" // populated by goreleaser
)
// VersionOps describes printing version string.
// VersionOp describes printing version string.
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

@@ -3,7 +3,9 @@
local KUBECTX="${HOME}/.kube/kubectx"
PREV=""
local all_contexts="$(kubectl config get-contexts --output='name')"
local context_array=("${(@f)$(kubectl config get-contexts --output='name')}")
local all_contexts=(\'${^context_array}\')
if [ -f "$KUBECTX" ]; then
# show '-' only if there's a saved previous context
local PREV=$(cat "${KUBECTX}")

58
go.mod
View File

@@ -1,15 +1,55 @@
module github.com/ahmetb/kubectx
go 1.14
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.2
github.com/imdario/mergo v0.3.9 // indirect
github.com/mattn/go-isatty v0.0.12
github.com/pkg/errors v0.9.1
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
k8s.io/apimachinery v0.21.0-alpha.1
k8s.io/client-go v0.21.0-alpha.1
github.com/fatih/color v1.18.0
github.com/google/go-cmp v0.7.0
github.com/mattn/go-isatty v0.0.20
k8s.io/apimachinery v0.35.2
k8s.io/client-go v0.35.2
sigs.k8s.io/kustomize/kyaml v0.21.1
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.9.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.35.2 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)

544
go.sum
View File

@@ -1,438 +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 h1:3ithwDMr7/3vpAMXiH+ZQnYbuIsh+OPhUPMFC9enmn0=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
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/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/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=
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/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest/autorest v0.11.12 h1:gI8ytXbxMfI+IVbI9mP2JGCTXIuhHLgRlvQ9X4PsnHE=
github.com/Azure/go-autorest/autorest v0.11.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw=
github.com/Azure/go-autorest/autorest/adal v0.9.5 h1:Y3bBUV4rTuxenJJs41HU3qmqsb+auo+a3Lz+PlJPpL0=
github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A=
github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk=
github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
github.com/Azure/go-autorest/logger v0.2.0 h1:e4RVHVZKC5p6UANLJHkM4OfR1UKZPj8Wt8Pcx+3oqrE=
github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
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/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
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/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/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
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/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
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 v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY=
github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
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/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.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.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
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/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.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/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/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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/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/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I=
github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
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/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
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.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
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 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
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.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/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/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
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/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.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
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.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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=
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-20190611184440-5c40567a22f8/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/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/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/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-20180906233101-161cd47e91fd/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-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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-20190827160401-ba9fcec4b297/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-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
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 h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
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/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/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-20190616124812-15dcb6c0061f/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-20191005200804-aed5e4c7ecf9/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-20200116001909-b77594299b42/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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd h1:5CtCZbICpIOFdgO940moixOPjc0178IU44m4EjOO5IY=
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
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-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s=
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/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-20181030221726-6c7e314b6563/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-20190614205625-5aca471b1d59/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-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
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.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
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 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
google.golang.org/appengine v1.6.5/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-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
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.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/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.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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=
k8s.io/api v0.21.0-alpha.1 h1:yh89vKdwWmBeM8yFgalsQcI5uQL8rJ4Qw0+sIJ1REC4=
k8s.io/api v0.21.0-alpha.1/go.mod h1:wc25iu2fBpPp8cMHJFJUB61Td8+KPtSzLp5086BvemI=
k8s.io/apimachinery v0.21.0-alpha.1 h1:cfEusYxbpabEAVueoFt2LOlicnj7OATyInRMbWtXkbs=
k8s.io/apimachinery v0.21.0-alpha.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
k8s.io/client-go v0.21.0-alpha.1 h1:YeIFnYcI0xQYYDmz7e7pqs0nkdMyKELya6oZVFe7rT8=
k8s.io/client-go v0.21.0-alpha.1/go.mod h1:oBYLLHfqHIzd6+EdOr5634Ajx7RVp5v2JgEMjOa3LYE=
k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ=
k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM=
k8s.io/utils v0.0.0-20201110183641-67b214c5f920 h1:CbnUZsM497iRC5QMVkHwyl8s2tB3g7yaSHkYPkpgelw=
k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
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/structured-merge-diff/v4 v4.0.2 h1:YHQV7Dajm86OuqnIR6zAelnDWBRjo+YhYV9PmGrh1s8=
sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw=
k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60=
k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8=
k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o=
k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7fI=
sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

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

@@ -19,7 +19,7 @@ const (
// interactive context selection when fzf is installed.
EnvFZFIgnore = "KUBECTX_IGNORE_FZF"
// EnvForceColor describes the environment variable to disable color usage
// EnvNoColor describes the environment variable to disable color usage
// when printing current context in a list.
EnvNoColor = `NO_COLOR`
@@ -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)
}
}
}

View File

@@ -33,6 +33,8 @@ usage() {
fi
cat <<EOF
Manage and switch between kubectl contexts.
USAGE:
$SELF : list the contexts
$SELF <NAME> : switch to context <NAME>
@@ -46,6 +48,8 @@ USAGE:
$SELF -u, --unset : unset the current context
$SELF -h,--help : show this message
(This executable is the legacy bash-based implementation, consider upgrading to Go-based implementation.)
EOF
}
@@ -227,7 +231,7 @@ main() {
# we don't call current_context here for two reasons:
# - it does not fail when current-context property is not set
# - it does not return a trailing newline
kubectl config current-context
$KUBECTL config current-context
elif [[ "${1}" == '-u' || "${1}" == '--unset' ]]; then
unset_context
elif [[ "${1}" == '-h' || "${1}" == '--help' ]]; then

4
kubens
View File

@@ -33,12 +33,16 @@ usage() {
fi
cat <<EOF
Switch between Kubernetes namespaces.
USAGE:
$SELF : list the namespaces in the current context
$SELF <NAME> : change the active namespace of current context
$SELF - : switch to the previous namespace in this context
$SELF -c, --current : show the current namespace
$SELF -h,--help : show this message
(This executable is the legacy bash-based implementation, consider upgrading to Go-based implementation.)
EOF
}