├── docs └── images │ ├── demo.gif │ └── usage.png ├── .gitignore ├── Makefile ├── main.go ├── internal ├── kube │ ├── resource.go │ ├── namespace.go │ ├── seviceaccount.go │ ├── secret.go │ ├── client.go │ ├── kubeconfig_test.go │ └── kubeconfig.go ├── util │ └── util.go ├── output │ └── output.go ├── types │ └── context.go ├── completion │ └── completion.go └── prompt │ └── prompt.go ├── .github └── workflows │ └── release.yml ├── .goreleaser.yaml ├── cmd ├── version.go ├── root.go ├── switch.go ├── generate.go ├── set_server.go ├── remove.go ├── export.go ├── set_namespace.go ├── rename.go ├── list.go └── add.go ├── install.sh ├── README_zh-CN.md ├── README.md ├── go.mod ├── LICENSE └── go.sum /docs/images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ketches/ktx/HEAD/docs/images/demo.gif -------------------------------------------------------------------------------- /docs/images/usage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ketches/ktx/HEAD/docs/images/usage.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | 23 | .vscode/ 24 | .idea/ 25 | test/ 26 | dist/ 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install 2 | install: 3 | @echo "» installing ktx..." 4 | go install -ldflags="-s -w" 5 | 6 | .PHONY: release 7 | release: 8 | @if [ -z "${VERSION}" ]; then \ 9 | echo "VERSION is not set"; \ 10 | exit 1; \ 11 | fi 12 | @if git rev-parse "refs/tags/${VERSION}" >/dev/null 2>&1; then \ 13 | echo "Git tag ${VERSION} already exists, please use a new version."; \ 14 | exit 1; \ 15 | fi 16 | @sed -E -i '' 's/(const VERSION = ")[^"]+(")/\1${VERSION}\2/' cmd/version.go 17 | @git add cmd/version.go 18 | @git commit -m "Release ${VERSION}" 19 | @git push 20 | @git tag -a "${VERSION}" -m "release ${VERSION}" 21 | @git push origin "${VERSION}" 22 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Ketches Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import "github.com/ketches/ktx/cmd" 20 | 21 | func main() { 22 | cmd.Execute() 23 | } 24 | -------------------------------------------------------------------------------- /internal/kube/resource.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Ketches Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package kube 18 | 19 | import "k8s.io/client-go/discovery" 20 | 21 | // ServerVersion returns the Kubernetes server version. 22 | func ServerVersion(discoveryClient discovery.DiscoveryInterface) (string, error) { 23 | sv, err := discoveryClient.ServerVersion() 24 | if err != nil { 25 | return "", err 26 | } 27 | return sv.String(), nil 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/release.yml 2 | name: release 3 | 4 | on: 5 | pull_request: 6 | push: 7 | tags: 8 | - "*" 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | release: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | - name: Set up Go 22 | uses: actions/setup-go@v5 23 | - name: Run GoReleaser 24 | uses: goreleaser/goreleaser-action@v6 25 | with: 26 | # either 'goreleaser' (default) or 'goreleaser-pro' 27 | distribution: goreleaser 28 | # 'latest', 'nightly', or a semver 29 | version: "~> v2" 30 | args: release --clean 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' distribution 34 | # GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} 35 | -------------------------------------------------------------------------------- /internal/util/util.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Ketches Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "os" 21 | ) 22 | 23 | // IsFileExist checks if a file exists. 24 | func IsFileExist(filename string) bool { 25 | _, err := os.Stat(filename) 26 | return !os.IsNotExist(err) 27 | } 28 | 29 | // If returns then if cond is true, otherwise els. 30 | func If[T any](cond bool, then T, els T) T { 31 | if cond { 32 | return then 33 | } 34 | return els 35 | } 36 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | 4 | # The lines below are called `modelines`. See `:help modeline` 5 | # Feel free to remove those if you don't want/need to use them. 6 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 7 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 8 | 9 | version: 2 10 | 11 | before: 12 | hooks: 13 | # You may remove this if you don't use go modules. 14 | - go mod tidy 15 | # you may remove this if you don't need go generate 16 | # - go generate ./... 17 | 18 | builds: 19 | - env: 20 | - CGO_ENABLED=0 21 | goos: 22 | - linux 23 | - windows 24 | - darwin 25 | goarch: 26 | - amd64 27 | - arm64 28 | 29 | archives: 30 | - formats: ["binary"] 31 | # this name template makes the OS and Arch compatible with the results of `uname`. 32 | name_template: >- 33 | {{ .ProjectName }}_ 34 | {{- .Version }}_ 35 | {{- .Os }}_ 36 | {{- .Arch }} 37 | 38 | changelog: 39 | sort: asc 40 | filters: 41 | exclude: 42 | - "^docs:" 43 | - "^test:" 44 | -------------------------------------------------------------------------------- /internal/kube/namespace.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Ketches Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package kube 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/ketches/ktx/internal/output" 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | "k8s.io/client-go/kubernetes" 25 | ) 26 | 27 | // ListNamespaces returns a list of namespaces 28 | func ListNamespaces(kubeClientset kubernetes.Interface) []string { 29 | namespaces, err := kubeClientset.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{}) 30 | if err != nil { 31 | output.Fatal("Failed to list namespaces: %s", err) 32 | } 33 | var ns []string 34 | for _, namespace := range namespaces.Items { 35 | ns = append(ns, namespace.Name) 36 | } 37 | return ns 38 | } 39 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 The Ketches Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "github.com/ketches/ktx/internal/completion" 21 | "github.com/ketches/ktx/internal/output" 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | const VERSION = "v0.3.2" 26 | 27 | // versionCmd represents the version command 28 | var versionCmd = &cobra.Command{ 29 | Use: "version", 30 | Aliases: []string{"v"}, 31 | Short: "Print the version number of ktx", 32 | Long: `Print the version number of ktx`, 33 | Run: func(cmd *cobra.Command, args []string) { 34 | output.Done("Version: %s", VERSION) 35 | }, 36 | ValidArgsFunction: completion.None, 37 | } 38 | 39 | func init() { 40 | rootCmd.AddCommand(versionCmd) 41 | } 42 | -------------------------------------------------------------------------------- /internal/output/output.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Ketches Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package output 18 | 19 | import ( 20 | "os" 21 | 22 | "github.com/fatih/color" 23 | ) 24 | 25 | // Fatal prints a fatal error message and exits the program. 26 | func Fatal(format string, a ...any) { 27 | color.Red("😾 "+format, a...) 28 | os.Exit(1) 29 | } 30 | 31 | // Note prints a note message. 32 | func Note(format string, a ...interface{}) { 33 | color.New(color.Faint).Printf("😼 "+format+"\n", a...) 34 | } 35 | 36 | // Done prints a done message. 37 | func Done(format string, a ...interface{}) { 38 | color.Green("😺 "+format, a...) 39 | } 40 | 41 | // Fail prints a fail message. 42 | func Fail(format string, a ...interface{}) { 43 | color.Red("😾 "+format, a...) 44 | } 45 | -------------------------------------------------------------------------------- /internal/types/context.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Ketches Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | import "github.com/fatih/color" 20 | 21 | // ContextProfile represents a context profile 22 | type ContextProfile struct { 23 | Current bool 24 | Name string 25 | Cluster string 26 | User string 27 | Server string 28 | Namespace string 29 | Emoji string 30 | ClusterStatus ClusterStatus 31 | ClusterVersion string 32 | } 33 | 34 | type ClusterStatus string 35 | 36 | const ( 37 | ClusterStatusAvailable ClusterStatus = "✓ Available" 38 | ClusterStatusTimeout ClusterStatus = "✗ Timeout" 39 | ClusterStatusUnavailable ClusterStatus = "✗ Unavailable" 40 | ) 41 | 42 | func (cs ClusterStatus) ColorString() string { 43 | switch cs { 44 | case ClusterStatusAvailable: 45 | return color.GreenString(string(cs)) 46 | case ClusterStatusUnavailable: 47 | return color.RedString(string(cs)) 48 | default: 49 | return string(cs) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | REPO_OWNER="ketches" 4 | REPO_NAME="ktx" 5 | BINARY_NAME="ktx" 6 | INSTALL_DIR="/usr/local/bin" 7 | 8 | OS=$(uname -s | tr '[:upper:]' '[:lower:]') 9 | ARCH=$(uname -m) 10 | 11 | case $ARCH in 12 | x86_64) ARCH="amd64" ;; 13 | arm64) ARCH="arm64" ;; 14 | i386) ARCH="386" ;; 15 | *) echo "Unsupported architecture: $ARCH"; exit 1 ;; 16 | esac 17 | 18 | LATEST_VERSION=$(basename $(curl -s -w %{redirect_url} https://github.com/$REPO_OWNER/$REPO_NAME/releases/latest) | sed 's/^v//') 19 | 20 | if [ -z "$LATEST_VERSION" ]; then 21 | echo "Failed to get latest version" 22 | exit 1 23 | fi 24 | 25 | DOWNLOAD_URL="https://github.com/$REPO_OWNER/$REPO_NAME/releases/download/v$LATEST_VERSION/${BINARY_NAME}_${LATEST_VERSION}_${OS}_${ARCH}" 26 | 27 | TMP_DIR=$(mktemp -d) 28 | 29 | echo "Installing $BINARY_NAME v$LATEST_VERSION ($OS/$ARCH)" 30 | echo "Downloading from: $DOWNLOAD_URL" 31 | 32 | curl -L $DOWNLOAD_URL -o "$TMP_DIR/$BINARY_NAME" || { 33 | echo "Download failed" 34 | rm -rf "$TMP_DIR" 35 | exit 1 36 | } 37 | 38 | sudo install -d "$INSTALL_DIR" 39 | sudo install -m 755 "$TMP_DIR/$BINARY_NAME" "$INSTALL_DIR/$BINARY_NAME" || { 40 | echo "Installation failed" 41 | rm -rf "$TMP_DIR" 42 | exit 1 43 | } 44 | 45 | rm -rf "$TMP_DIR" 46 | 47 | if command -v $BINARY_NAME >/dev/null; then 48 | echo "Successfully installed $BINARY_NAME to $INSTALL_DIR" 49 | echo "$($BINARY_NAME version 2>/dev/null || echo 'version check not supported')" 50 | else 51 | echo "Installation completed but binary not found in PATH" 52 | exit 1 53 | fi -------------------------------------------------------------------------------- /README_zh-CN.md: -------------------------------------------------------------------------------- 1 | # ktx 2 | 3 | [English](README.md) | 简体中文 4 | 5 | ktx 是一个针对 Kubernetes 多集群上下文管理的命令行工具,简单易用。 6 | 7 | ![demo](docs/images/demo.gif) 8 | 9 | ## 安装 10 | 11 | - 通过 `go install` 安装 12 | 13 | ```bash 14 | go install -u github.com/ketches/ktx@latest 15 | ``` 16 | 17 | - 通过二进制文件安装 18 | 19 | 对于 MacOS 或 Linux: 20 | 21 | ```bash 22 | curl -sSL https://github.com/ketches/ktx/raw/master/install.sh | sh 23 | ``` 24 | 25 | 对于 Windows: 26 | 27 | 从 [Releases](https://github.com/ketches/ktx/releases/latest) 下载最新版本可执行文件并将其添加到 PATH。 28 | 29 | ## 使用 30 | 31 | ![usage](docs/images/usage.png) 32 | 33 | 1. 添加集群上下文 34 | 35 | ```bash 36 | ktx add -f .kube/kind-cluster-01 37 | ktx add -f .kube/kind-cluster-02 38 | ``` 39 | 40 | 2. 列出集群上下文 41 | 42 | ```bash 43 | ktx list 44 | ``` 45 | 46 | 命令别名:`ktx ls` 47 | 48 | 3. 切换集群上下文 49 | 50 | ```bash 51 | # 切换上下文 52 | ktx 53 | 54 | # 切换到指定集群上下文 55 | ktx switch kind-cluster-01 56 | 57 | # 交互式切换 58 | ktx switch 59 | ``` 60 | 61 | 命令别名:`ktx s` 62 | 63 | 4. 重命名集群上下文 64 | 65 | ```bash 66 | # 重命名指定集群上下文 67 | ktx rename kind-cluster-01 68 | 69 | # 交互式重命名 70 | ktx rename 71 | ``` 72 | 73 | 5. 删除集群上下文 74 | 75 | ```bash 76 | # 删除指定集群上下文 77 | ktx remove kind-cluster-01 78 | 79 | # 交互式删除 80 | ktx remove 81 | ``` 82 | 83 | 命令别名:`ktx rm` 84 | 85 | 6. 导出集群上下文 86 | 87 | ```bash 88 | ktx export kind-cluster-01 -f .kube/export-01 89 | ``` 90 | 91 | 7. 从 ServiceAccount 生成 kubeconfig 92 | 93 | ```bash 94 | ktx generate --service-account default -f .kube/my-gen-config 95 | ``` 96 | 97 | 命令别名:`ktx gen` 98 | 99 | 8. 设置命名空间 100 | 101 | ```bash 102 | ktx set-namespace --namespace default 103 | ``` 104 | 105 | 9. 设置 Server 地址 106 | 107 | ```bash 108 | ktx set-server --server https://api.k8s.local:6443 109 | ``` 110 | -------------------------------------------------------------------------------- /internal/kube/seviceaccount.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Ketches Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package kube 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/ketches/ktx/internal/output" 23 | v1 "k8s.io/api/core/v1" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | "k8s.io/client-go/kubernetes" 26 | ) 27 | 28 | // GetServiceAccount returns a service account 29 | func GetServiceAccount(kubeClientset kubernetes.Interface, serviceAccountName, namespace string) *v1.ServiceAccount { 30 | serviceAccount, err := kubeClientset.CoreV1().ServiceAccounts(namespace).Get(context.Background(), serviceAccountName, metav1.GetOptions{}) 31 | if err != nil { 32 | output.Fatal("Failed to get service account %s.", serviceAccountName) 33 | } 34 | return serviceAccount 35 | } 36 | 37 | // ListServiceAccounts returns a list of service accounts 38 | func ListServiceAccounts(kubeClientset kubernetes.Interface, namespace string) []string { 39 | serviceAccounts, err := kubeClientset.CoreV1().ServiceAccounts(namespace).List(context.Background(), metav1.ListOptions{}) 40 | if err != nil { 41 | output.Fatal("Failed to list service accounts: %s", err) 42 | } 43 | var sa []string 44 | for _, serviceAccount := range serviceAccounts.Items { 45 | sa = append(sa, serviceAccount.Name) 46 | } 47 | return sa 48 | } 49 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 The Ketches Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "os" 21 | 22 | "github.com/ketches/ktx/internal/kube" 23 | "github.com/spf13/cobra" 24 | ) 25 | 26 | type rootFlags struct { 27 | kubeconfig string 28 | } 29 | 30 | var rootFlag rootFlags 31 | 32 | // rootCmd represents the base command when called without any subcommands 33 | var rootCmd = &cobra.Command{ 34 | Use: "ktx", 35 | Short: "ktx is a tool to manage kubernetes contexts.", 36 | Long: `ktx is a tool to manage kubernetes contexts.`, 37 | // Uncomment the following line if your bare application 38 | // has an action associated with it: 39 | Run: func(cmd *cobra.Command, args []string) { 40 | // 默认运行 switch 子命令 41 | runSwitch(args) 42 | }, 43 | } 44 | 45 | // Execute adds all child commands to the root command and sets flags appropriately. 46 | // This is called by main.main(). It only needs to happen once to the rootCmd. 47 | func Execute() { 48 | // 如果 kubeconfig 为默认值,则检查或初始化 kubeconfig 49 | if rootFlag.kubeconfig == kube.DefaultConfigFile { 50 | kube.CheckOrInitConfig() 51 | } 52 | 53 | err := rootCmd.Execute() 54 | if err != nil { 55 | os.Exit(1) 56 | } 57 | } 58 | 59 | func init() { 60 | rootCmd.PersistentFlags().StringVar(&rootFlag.kubeconfig, "kubeconfig", kube.DefaultConfigFile, "kubeconfig file") 61 | } 62 | -------------------------------------------------------------------------------- /cmd/switch.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 The Ketches Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "github.com/ketches/ktx/internal/completion" 21 | "github.com/ketches/ktx/internal/kube" 22 | "github.com/ketches/ktx/internal/output" 23 | "github.com/ketches/ktx/internal/prompt" 24 | "github.com/spf13/cobra" 25 | clientcmdapi "k8s.io/client-go/tools/clientcmd/api" 26 | ) 27 | 28 | // switchCmd represents the switch command 29 | var switchCmd = &cobra.Command{ 30 | Use: "switch", 31 | Aliases: []string{"s"}, 32 | Short: "Switch context in specified kubeconfig(~/.kube/config by default)", 33 | Long: `Switch context in specified kubeconfig(~/.kube/config by default)`, 34 | Args: cobra.MaximumNArgs(1), 35 | Run: func(cmd *cobra.Command, args []string) { 36 | runSwitch(args) 37 | }, 38 | ValidArgsFunction: completion.Context, 39 | } 40 | 41 | func init() { 42 | rootCmd.AddCommand(switchCmd) 43 | } 44 | 45 | func runSwitch(args []string) { 46 | config := kube.LoadConfigFromFile(rootFlag.kubeconfig) 47 | 48 | var dst string 49 | if len(args) == 0 { 50 | dst = prompt.ContextSelection("Switch to context", config) 51 | } else { 52 | dst = args[0] 53 | } 54 | 55 | switchContext(config, dst) 56 | } 57 | 58 | func switchContext(config *clientcmdapi.Config, dst string) { 59 | _, ok := config.Contexts[dst] 60 | if !ok { 61 | output.Fatal("Context <%s> not found.", dst) 62 | } 63 | 64 | config.CurrentContext = dst 65 | kube.SaveConfigToFile(config, rootFlag.kubeconfig) 66 | output.Done("Switched to context <%s>.", dst) 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ktx 2 | 3 | English | [简体中文](README_zh-CN.md) 4 | 5 | ktx is an easy-to-use command line tool for kubernetes multi-cluster context management. 6 | 7 | ![demo](docs/images/demo.gif) 8 | 9 | ## Installation 10 | 11 | - Install with `go install` 12 | 13 | ```bash 14 | go install -u github.com/ketches/ktx@latest 15 | ``` 16 | 17 | - Install from binary 18 | 19 | For MacOS or Linux: 20 | 21 | ```bash 22 | curl -sSL https://github.com/ketches/ktx/raw/master/install.sh | sh 23 | ``` 24 | 25 | For Windows: 26 | 27 | Download the lastest executable from [Releases](https://github.com/ketches/ktx/releases/latest) and add it to the PATH. 28 | 29 | ## Usage 30 | 31 | ![usage](docs/images/usage.png) 32 | 33 | 1. Add cluster context 34 | 35 | ```bash 36 | ktx add -f .kube/kind-cluster-01 37 | ktx add -f .kube/kind-cluster-02 38 | ``` 39 | 40 | 2. List cluster contexts 41 | 42 | ```bash 43 | ktx list 44 | ``` 45 | 46 | Alias: `ktx ls` 47 | 48 | 3. Switch cluster context 49 | 50 | ```bash 51 | # Switch contexts 52 | ktx 53 | 54 | # Switch to specified cluster context 55 | ktx switch kind-cluster-01 56 | 57 | # Interactive switch 58 | ktx switch 59 | ``` 60 | 61 | Alias: `ktx s` 62 | 63 | 4. Rename cluster context 64 | 65 | ```bash 66 | # Rename specified cluster context 67 | ktx rename kind-cluster-01 68 | 69 | # Interactive rename 70 | ktx rename 71 | ``` 72 | 73 | 5. Remove cluster context 74 | 75 | ```bash 76 | # Remove specified cluster context 77 | ktx remove kind-cluster-01 78 | 79 | # Interactive remove 80 | ktx remove 81 | ``` 82 | 83 | Alias: `ktx rm` 84 | 85 | 6. Export cluster context 86 | 87 | ```bash 88 | ktx export kind-cluster-01 -f .kube/export-01 89 | ``` 90 | 91 | 7. Generate kubeconfig from ServiceAccount 92 | 93 | ```bash 94 | ktx generate --service-account default -f .kube/my-gen-config 95 | ``` 96 | 97 | Alias: `ktx gen` 98 | 99 | 8. Set namespace 100 | 101 | ```bash 102 | ktx set-namespace --namespace default 103 | ``` 104 | 105 | 9. Set Server address 106 | 107 | ```bash 108 | ktx set-server --server https://api.k8s.local:6443 109 | ``` 110 | -------------------------------------------------------------------------------- /internal/kube/secret.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Ketches Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package kube 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/ketches/ktx/internal/output" 23 | v1 "k8s.io/api/core/v1" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | "k8s.io/apimachinery/pkg/util/rand" 26 | "k8s.io/client-go/kubernetes" 27 | ) 28 | 29 | // GetSecret returns a secret 30 | func GetSecret(kubeClientset kubernetes.Interface, secretName, namespace string) *v1.Secret { 31 | secret, err := kubeClientset.CoreV1().Secrets(namespace).Get(context.Background(), secretName, metav1.GetOptions{}) 32 | if err != nil { 33 | output.Fatal("Failed to get secret: %s", err) 34 | } 35 | return secret 36 | } 37 | 38 | // CreateServiceAccountTokenSecret creates a secret for a service account 39 | func CreateServiceAccountTokenSecret(kubeClientset kubernetes.Interface, serviceAccountName, namespace string) *v1.Secret { 40 | secret, err := kubeClientset.CoreV1().Secrets(namespace).Create(context.Background(), &v1.Secret{ 41 | ObjectMeta: metav1.ObjectMeta{ 42 | Name: serviceAccountName + "-token-" + rand.String(5), 43 | Namespace: namespace, 44 | Annotations: map[string]string{ 45 | "kubernetes.io/service-account.name": serviceAccountName, 46 | }, 47 | }, 48 | Type: v1.SecretTypeServiceAccountToken, 49 | }, metav1.CreateOptions{}) 50 | if err != nil { 51 | output.Fatal("Failed to create secret for service account %s from namespace %s", serviceAccountName, namespace) 52 | } 53 | 54 | return secret 55 | } 56 | 57 | // CreateSecret creates a secret 58 | func CreateSecret(kubeClientset kubernetes.Interface, secret *v1.Secret, namespace string) *v1.Secret { 59 | secret, err := kubeClientset.CoreV1().Secrets(namespace).Create(context.Background(), secret, metav1.CreateOptions{}) 60 | if err != nil { 61 | output.Fatal("Failed to create secret %s from namespace %s", secret.Name, namespace) 62 | } 63 | 64 | return secret 65 | } 66 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ketches/ktx 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/fatih/color v1.18.0 7 | github.com/jedib0t/go-pretty/v6 v6.6.7 8 | github.com/manifoldco/promptui v0.9.0 9 | github.com/spf13/cobra v1.9.1 10 | k8s.io/api v0.33.1 11 | k8s.io/apimachinery v0.33.1 12 | k8s.io/client-go v0.33.1 13 | ) 14 | 15 | require ( 16 | github.com/chzyer/readline v1.5.1 // indirect 17 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 18 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect 19 | github.com/fxamacker/cbor/v2 v2.8.0 // indirect 20 | github.com/go-logr/logr v1.4.2 // indirect 21 | github.com/go-openapi/jsonpointer v0.21.1 // indirect 22 | github.com/go-openapi/jsonreference v0.21.0 // indirect 23 | github.com/go-openapi/swag v0.23.1 // indirect 24 | github.com/gogo/protobuf v1.3.2 // indirect 25 | github.com/google/gnostic-models v0.6.9 // indirect 26 | github.com/google/go-cmp v0.7.0 // indirect 27 | github.com/google/uuid v1.6.0 // indirect 28 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 29 | github.com/josharian/intern v1.0.0 // indirect 30 | github.com/json-iterator/go v1.1.12 // indirect 31 | github.com/mailru/easyjson v0.9.0 // indirect 32 | github.com/mattn/go-colorable v0.1.14 // indirect 33 | github.com/mattn/go-isatty v0.0.20 // indirect 34 | github.com/mattn/go-runewidth v0.0.16 // indirect 35 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 36 | github.com/modern-go/reflect2 v1.0.2 // indirect 37 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 38 | github.com/pkg/errors v0.9.1 // indirect 39 | github.com/rivo/uniseg v0.4.7 // indirect 40 | github.com/spf13/pflag v1.0.6 // indirect 41 | github.com/x448/float16 v0.8.4 // indirect 42 | golang.org/x/net v0.40.0 // indirect 43 | golang.org/x/oauth2 v0.30.0 // indirect 44 | golang.org/x/sys v0.33.0 // indirect 45 | golang.org/x/term v0.32.0 // indirect 46 | golang.org/x/text v0.25.0 // indirect 47 | golang.org/x/time v0.11.0 // indirect 48 | google.golang.org/protobuf v1.36.6 // indirect 49 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 50 | gopkg.in/inf.v0 v0.9.1 // indirect 51 | gopkg.in/yaml.v3 v3.0.1 // indirect 52 | k8s.io/klog/v2 v2.130.1 // indirect 53 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 54 | k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 // indirect 55 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 56 | sigs.k8s.io/randfill v1.0.0 // indirect 57 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect 58 | sigs.k8s.io/yaml v1.4.0 // indirect 59 | ) 60 | -------------------------------------------------------------------------------- /cmd/generate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 The Ketches Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "github.com/ketches/ktx/internal/completion" 21 | "github.com/ketches/ktx/internal/kube" 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | type generateFlags struct { 26 | context string 27 | namespace string 28 | serviceAccount string 29 | output string 30 | } 31 | 32 | var generateFlag generateFlags 33 | 34 | // generateCmd represents the gen command 35 | var generateCmd = &cobra.Command{ 36 | Use: "generate", 37 | Aliases: []string{"gen"}, 38 | Short: "Generate a new context from ServiceAccount", 39 | Long: `Generate a new context from ServiceAccount.`, 40 | Run: func(cmd *cobra.Command, args []string) { 41 | runGenerate() 42 | }, 43 | ValidArgsFunction: completion.None, 44 | } 45 | 46 | func init() { 47 | rootCmd.AddCommand(generateCmd) 48 | 49 | generateCmd.Flags().StringVarP(&generateFlag.context, "context", "c", "", "Context") 50 | generateCmd.Flags().StringVarP(&generateFlag.namespace, "namespace", "n", kube.DefaultNamespace, "Namespace") 51 | generateCmd.Flags().StringVar(&generateFlag.serviceAccount, "service-account", "", "ServiceAccount") 52 | generateCmd.Flags().StringVarP(&generateFlag.output, "output", "o", "", "Output kube config file") 53 | 54 | generateCmd.RegisterFlagCompletionFunc("context", completion.Context) 55 | generateCmd.RegisterFlagCompletionFunc("namespace", completion.Namespace) 56 | generateCmd.RegisterFlagCompletionFunc("service-account", completion.ServiceAccount) 57 | 58 | generateCmd.MarkFlagRequired("service-account") 59 | } 60 | 61 | func runGenerate() { 62 | generateContext(rootFlag.kubeconfig, generateFlag.context, generateFlag.namespace, generateFlag.serviceAccount) 63 | } 64 | 65 | func generateContext(kubeconfig, context, namespace, serviceAccount string) { 66 | config := kube.GenerateConfigForServiceAccount(kubeconfig, context, namespace, serviceAccount) 67 | if len(generateFlag.output) == 0 { 68 | kube.PrintConfig(config) 69 | } else { 70 | kube.SaveConfigToFile(config, generateFlag.output) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /cmd/set_server.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 The Ketches Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | completion "github.com/ketches/ktx/internal/completion" 21 | "github.com/ketches/ktx/internal/kube" 22 | "github.com/ketches/ktx/internal/output" 23 | "github.com/spf13/cobra" 24 | clientcmdapi "k8s.io/client-go/tools/clientcmd/api" 25 | ) 26 | 27 | type setServerFlags struct { 28 | context string 29 | server string 30 | } 31 | 32 | var setServerFlag setServerFlags 33 | 34 | // setServerCmd represents the set-server command 35 | var setServerCmd = &cobra.Command{ 36 | Use: "set-server", 37 | Short: "Set context server host in specified kubeconfig(~/.kube/config by default)", 38 | Long: `Set context server host in specified kubeconfig(~/.kube/config by default)`, 39 | Args: cobra.MaximumNArgs(0), 40 | Run: func(cmd *cobra.Command, args []string) { 41 | runSetServer() 42 | }, 43 | ValidArgsFunction: completion.None, 44 | } 45 | 46 | func init() { 47 | rootCmd.AddCommand(setServerCmd) 48 | 49 | setServerCmd.Flags().StringVarP(&setServerFlag.context, "context", "c", "", "Context") 50 | setServerCmd.Flags().StringVarP(&setServerFlag.server, "server", "s", "", "Server host") 51 | 52 | setServerCmd.RegisterFlagCompletionFunc("context", completion.Context) 53 | setServerCmd.RegisterFlagCompletionFunc("server", completion.Server) 54 | setServerCmd.MarkFlagRequired("server") 55 | } 56 | 57 | func runSetServer() { 58 | config := kube.LoadConfigFromFile(rootFlag.kubeconfig) 59 | 60 | ctxName := setServerFlag.context 61 | if len(ctxName) == 0 { 62 | ctxName = config.CurrentContext 63 | } 64 | 65 | setContextServer(config, ctxName, setServerFlag.server) 66 | } 67 | 68 | func setContextServer(config *clientcmdapi.Config, ctx, server string) { 69 | dstCtx, ok := config.Contexts[ctx] 70 | if !ok { 71 | output.Fatal("Context <%s> not found.", ctx) 72 | } 73 | 74 | oldServer := config.Clusters[dstCtx.Cluster].Server 75 | 76 | if oldServer == server { 77 | output.Done("Context <%s> server not changed.", ctx) 78 | return 79 | } 80 | 81 | config.Clusters[dstCtx.Cluster].Server = server 82 | 83 | kube.SaveConfigToFile(config, rootFlag.kubeconfig) 84 | output.Done("Context <%s> set server from <%s> to <%s>.", ctx, oldServer, server) 85 | } 86 | -------------------------------------------------------------------------------- /cmd/remove.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 The Ketches Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/ketches/ktx/internal/completion" 23 | "github.com/ketches/ktx/internal/kube" 24 | "github.com/ketches/ktx/internal/output" 25 | "github.com/ketches/ktx/internal/prompt" 26 | "github.com/spf13/cobra" 27 | "k8s.io/client-go/tools/clientcmd/api" 28 | ) 29 | 30 | // removeCmd represents the remove command 31 | var removeCmd = &cobra.Command{ 32 | Use: "remove", 33 | Aliases: []string{"rm"}, 34 | Short: "Remove context(s) from specified kubeconfig(~/.kube/config by default)", 35 | Long: `Remove context(s) from specified kubeconfig(~/.kube/config by default)`, 36 | Run: func(cmd *cobra.Command, args []string) { 37 | runRemove(args) 38 | }, 39 | ValidArgsFunction: completion.ContextArray, 40 | } 41 | 42 | func init() { 43 | rootCmd.AddCommand(removeCmd) 44 | } 45 | 46 | func runRemove(args []string) { 47 | config := kube.LoadConfigFromFile(rootFlag.kubeconfig) 48 | 49 | dsts := args 50 | if len(dsts) == 0 { 51 | dsts = []string{prompt.ContextSelection("Select context to remove", config)} 52 | } 53 | 54 | for _, dst := range dsts { 55 | removeContext(config, dst) 56 | } 57 | } 58 | 59 | func removeContext(config *api.Config, dst string) { 60 | dstCtx, ok := config.Contexts[dst] 61 | if !ok { 62 | output.Fatal("Context <%s> not found.", dst) 63 | } 64 | 65 | if !prompt.YesNo(fmt.Sprintf("Are you sure you want to remove context %s", dst)) { 66 | return 67 | } 68 | 69 | delete(config.Clusters, dstCtx.Cluster) 70 | delete(config.AuthInfos, dstCtx.AuthInfo) 71 | delete(config.Contexts, dst) 72 | 73 | // 如果删除的是 current context,那么清空 current context 74 | if config.CurrentContext == dst { 75 | config.CurrentContext = "" 76 | } 77 | 78 | kube.SaveConfigToFile(config, rootFlag.kubeconfig) 79 | output.Done("Context <%s> removed.", dst) 80 | 81 | // 如果当前没有 context,那么提示用户选择一个 context 82 | if len(config.CurrentContext) == 0 && len(config.Contexts) > 0 { 83 | if len(config.Contexts) > 0 { 84 | new := prompt.ContextSelection("Select a context as current", config) 85 | config.CurrentContext = new 86 | kube.SaveConfigToFile(config, rootFlag.kubeconfig) 87 | output.Done("Switched to context <%s>.", new) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /cmd/export.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 The Ketches Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | completion "github.com/ketches/ktx/internal/completion" 21 | "github.com/ketches/ktx/internal/kube" 22 | "github.com/ketches/ktx/internal/output" 23 | "github.com/spf13/cobra" 24 | clientcmdapi "k8s.io/client-go/tools/clientcmd/api" 25 | ) 26 | 27 | type exportFlags struct { 28 | output string 29 | } 30 | 31 | var exportFlag exportFlags 32 | 33 | // exportCmd represents the export command 34 | var exportCmd = &cobra.Command{ 35 | Use: "export", 36 | Short: "Export context(s) from specified kubeconfig(~/.kube/config by default)", 37 | Long: `Export context(s) from specified kubeconfig(~/.kube/config by default)`, 38 | Args: cobra.MinimumNArgs(1), 39 | Run: func(cmd *cobra.Command, args []string) { 40 | runExport(args) 41 | }, 42 | ValidArgsFunction: completion.ContextArray, 43 | } 44 | 45 | func init() { 46 | rootCmd.AddCommand(exportCmd) 47 | 48 | exportCmd.Flags().StringVarP(&exportFlag.output, "output", "o", "", "Output kube config file") 49 | } 50 | 51 | func runExport(args []string) { 52 | config := kube.LoadConfigFromFile(rootFlag.kubeconfig) 53 | 54 | exportContext(config, args) 55 | } 56 | 57 | func exportContext(config *clientcmdapi.Config, dsts []string) { 58 | dstConfig := clientcmdapi.NewConfig() 59 | for _, dst := range dsts { 60 | dstCtx, ok := config.Contexts[dst] 61 | if !ok { 62 | output.Fatal("Context <%s> not found.", dst) 63 | } 64 | 65 | dstCluster, ok := config.Clusters[dstCtx.Cluster] 66 | if !ok { 67 | output.Fatal("Cluster not found for context <%s>.", dstCtx.Cluster, dst) 68 | } 69 | 70 | dstUser, ok := config.AuthInfos[dstCtx.AuthInfo] 71 | if !ok { 72 | output.Fatal("User not found for context <%s>.", dstCtx.AuthInfo, dst) 73 | } 74 | dstConfig.Contexts[dst] = dstCtx 75 | dstConfig.Clusters[dstCtx.Cluster] = dstCluster 76 | dstConfig.AuthInfos[dstCtx.AuthInfo] = dstUser 77 | } 78 | 79 | // 设置当前上下文,默认第一个 80 | if len(dstConfig.Contexts) > 0 { 81 | dstConfig.CurrentContext = dsts[0] 82 | } 83 | 84 | if len(exportFlag.output) == 0 { 85 | kube.PrintConfig(dstConfig) 86 | } else { 87 | kube.SaveConfigToFile(dstConfig, exportFlag.output) 88 | output.Done("Context exported to %s.", exportFlag.output) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /cmd/set_namespace.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 The Ketches Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | completion "github.com/ketches/ktx/internal/completion" 21 | "github.com/ketches/ktx/internal/kube" 22 | "github.com/ketches/ktx/internal/output" 23 | "github.com/spf13/cobra" 24 | clientcmdapi "k8s.io/client-go/tools/clientcmd/api" 25 | ) 26 | 27 | type setNamespaceFlags struct { 28 | context string 29 | namespace string 30 | } 31 | 32 | var setNamespaceFlag setNamespaceFlags 33 | 34 | // setNamespaceCmd represents the set-namespace command 35 | var setNamespaceCmd = &cobra.Command{ 36 | Use: "set-namespace", 37 | Short: "Set context namespace in specified kubeconfig(~/.kube/config by default)", 38 | Long: `Set context namespace in specified kubeconfig(~/.kube/config by default)`, 39 | Args: cobra.MaximumNArgs(0), 40 | Run: func(cmd *cobra.Command, args []string) { 41 | runSetNamespace() 42 | }, 43 | ValidArgsFunction: completion.None, 44 | } 45 | 46 | func init() { 47 | rootCmd.AddCommand(setNamespaceCmd) 48 | 49 | setNamespaceCmd.Flags().StringVarP(&setNamespaceFlag.context, "context", "c", "", "Context") 50 | setNamespaceCmd.Flags().StringVarP(&setNamespaceFlag.namespace, "namespace", "n", kube.DefaultNamespace, "Namespace") 51 | 52 | setNamespaceCmd.RegisterFlagCompletionFunc("context", completion.Context) 53 | setNamespaceCmd.RegisterFlagCompletionFunc("namespace", completion.Namespace) 54 | 55 | setNamespaceCmd.MarkFlagRequired("namespace") 56 | } 57 | 58 | func runSetNamespace() { 59 | config := kube.LoadConfigFromFile(rootFlag.kubeconfig) 60 | 61 | ctxName := setNamespaceFlag.context 62 | if len(ctxName) == 0 { 63 | ctxName = config.CurrentContext 64 | } 65 | 66 | setContextNamespace(config, ctxName, setNamespaceFlag.namespace) 67 | } 68 | 69 | func setContextNamespace(config *clientcmdapi.Config, ctx, namespace string) { 70 | dstCtx, ok := config.Contexts[ctx] 71 | if !ok { 72 | output.Fatal("Context <%s> not found.", ctx) 73 | } 74 | 75 | oldNamespace := dstCtx.Namespace 76 | 77 | if oldNamespace == namespace { 78 | output.Done("Context <%s> namespace not changed.", ctx) 79 | return 80 | } 81 | 82 | config.Contexts[ctx].Namespace = namespace 83 | 84 | kube.SaveConfigToFile(config, rootFlag.kubeconfig) 85 | output.Done("Context <%s> set namespace from <%s> to <%s>.", ctx, oldNamespace, namespace) 86 | } 87 | -------------------------------------------------------------------------------- /internal/kube/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Ketches Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package kube 18 | 19 | import ( 20 | "github.com/ketches/ktx/internal/output" 21 | "k8s.io/client-go/discovery" 22 | "k8s.io/client-go/kubernetes" 23 | "k8s.io/client-go/rest" 24 | "k8s.io/client-go/tools/clientcmd" 25 | ) 26 | 27 | // ClientOrDie creates a new kubernetes client from the given 28 | // kubeconfig file and context, and panics if it fails. 29 | func ClientOrDie(kubeConfigFile, ctx string) kubernetes.Interface { 30 | clientset, err := Client(kubeConfigFile, ctx) 31 | if err != nil { 32 | output.Fatal("Failed to create kubernetes client: %s from file %s", err, kubeConfigFile) 33 | } 34 | return clientset 35 | } 36 | 37 | // Client creates a new kubernetes client from the given 38 | // kubeconfig file and context. 39 | func Client(kubeConfigFile, ctx string) (kubernetes.Interface, error) { 40 | client, err := kubernetes.NewForConfig(configOrDie(kubeConfigFile, ctx)) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return client, nil 45 | } 46 | 47 | // DiscoveryClient creates a new kubernetes discovery client 48 | // from the given kubeconfig file and context. 49 | func DiscoveryClient(kubeConfigFile, ctx string) (discovery.DiscoveryInterface, error) { 50 | client, err := Client(kubeConfigFile, ctx) 51 | if err != nil { 52 | return nil, err 53 | } 54 | return client.Discovery(), nil 55 | } 56 | 57 | // configOrDie creates a new kubernetes rest configOrDie from the 58 | // given kubeconfig file, and panics if it fails. 59 | func configOrDie(kubeConfigFile, ctx string) *rest.Config { 60 | config, err := config(kubeConfigFile, ctx) 61 | if err != nil { 62 | output.Fatal("Failed to build kubernetes rest config: %s from file %s", err, kubeConfigFile) 63 | } 64 | return config 65 | } 66 | 67 | // config creates a new kubernetes rest config from the given 68 | // kubeconfig file and context. 69 | func config(kubeConfigFile, ctx string) (*rest.Config, error) { 70 | loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() 71 | loadingRules.ExplicitPath = kubeConfigFile 72 | 73 | configOverrides := &clientcmd.ConfigOverrides{} 74 | if len(ctx) > 0 { 75 | configOverrides.CurrentContext = ctx 76 | } 77 | 78 | clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) 79 | 80 | config, err := clientConfig.ClientConfig() 81 | if err != nil { 82 | return nil, err 83 | } 84 | return config, nil 85 | } 86 | -------------------------------------------------------------------------------- /internal/kube/kubeconfig_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Ketches Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package kube 18 | 19 | import ( 20 | "testing" 21 | 22 | clientcmdapi "k8s.io/client-go/tools/clientcmd/api" 23 | ) 24 | 25 | func TestStandardizeConfig(t *testing.T) { 26 | testdata := []struct { 27 | config *clientcmdapi.Config 28 | expected *clientcmdapi.Config 29 | }{ 30 | { 31 | config: &clientcmdapi.Config{ 32 | Clusters: map[string]*clientcmdapi.Cluster{"cluster1": {}, "cluster2": {}}, 33 | AuthInfos: map[string]*clientcmdapi.AuthInfo{"user1": {}, "user2": {}}, 34 | Contexts: map[string]*clientcmdapi.Context{ 35 | "context1": {Cluster: "cluster1", AuthInfo: "user1"}, 36 | "context2": {Cluster: "cluster2", AuthInfo: "user2"}, 37 | }, 38 | }, 39 | expected: &clientcmdapi.Config{ 40 | Clusters: map[string]*clientcmdapi.Cluster{"cluster-context1": {}, "cluster-context2": {}}, 41 | AuthInfos: map[string]*clientcmdapi.AuthInfo{"user-context1": {}, "user-context2": {}}, 42 | Contexts: map[string]*clientcmdapi.Context{ 43 | "context1": {Cluster: "cluster-context1", AuthInfo: "user-context1"}, 44 | "context2": {Cluster: "cluster-context2", AuthInfo: "user-context2"}, 45 | }, 46 | }, 47 | }, 48 | { 49 | config: &clientcmdapi.Config{ 50 | Clusters: map[string]*clientcmdapi.Cluster{"cluster1": {}}, 51 | AuthInfos: map[string]*clientcmdapi.AuthInfo{"user1": {}}, 52 | Contexts: map[string]*clientcmdapi.Context{ 53 | "context1": {Cluster: "cluster1", AuthInfo: "user1"}, 54 | "context2": {Cluster: "cluster1", AuthInfo: "user1"}, 55 | }, 56 | }, 57 | expected: &clientcmdapi.Config{ 58 | Clusters: map[string]*clientcmdapi.Cluster{"cluster-context1": {}, "cluster-context2": {}}, 59 | AuthInfos: map[string]*clientcmdapi.AuthInfo{"user-context1": {}, "user-context2": {}}, 60 | Contexts: map[string]*clientcmdapi.Context{ 61 | "context1": {Cluster: "cluster-context1", AuthInfo: "user-context1"}, 62 | "context2": {Cluster: "cluster-context2", AuthInfo: "user-context2"}, 63 | }, 64 | }, 65 | }, 66 | } 67 | 68 | for _, test := range testdata { 69 | StandardizeConfig(test.config) 70 | for ctxName, ctx := range test.config.Contexts { 71 | if ctx.Cluster != "cluster-"+ctxName { 72 | t.Errorf("StandardizeConfig() failed, context: %s, expected cluster name: %s, got: %s", ctxName, "cluster-"+ctxName, ctx.Cluster) 73 | } 74 | if ctx.AuthInfo != "user-"+ctxName { 75 | t.Errorf("StandardizeConfig() failed, context: %s, expected user name: %s, got: %s", ctxName, "user-"+ctxName, ctx.AuthInfo) 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /cmd/rename.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 The Ketches Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/ketches/ktx/internal/completion" 23 | "github.com/ketches/ktx/internal/kube" 24 | "github.com/ketches/ktx/internal/output" 25 | "github.com/ketches/ktx/internal/prompt" 26 | "github.com/spf13/cobra" 27 | clientcmdapi "k8s.io/client-go/tools/clientcmd/api" 28 | ) 29 | 30 | // renameCmd represents the rename command 31 | var renameCmd = &cobra.Command{ 32 | Use: "rename", 33 | Short: "Rename context in specified kubeconfig(~/.kube/config by default)", 34 | Long: `Rename context in specified kubeconfig(~/.kube/config by default)`, 35 | Args: cobra.MaximumNArgs(1), 36 | Run: func(cmd *cobra.Command, args []string) { 37 | runRename(args) 38 | }, 39 | ValidArgsFunction: completion.Context, 40 | } 41 | 42 | func init() { 43 | rootCmd.AddCommand(renameCmd) 44 | } 45 | 46 | func runRename(args []string) { 47 | config := kube.LoadConfigFromFile(rootFlag.kubeconfig) 48 | 49 | var dst string 50 | if len(args) == 0 { 51 | dst = prompt.ContextSelection("Select context to rename", config) 52 | } else { 53 | dst = args[0] 54 | } 55 | 56 | renameContext(dst, config) 57 | } 58 | 59 | func renameContext(oldCtxName string, config *clientcmdapi.Config) { 60 | dstCtx, ok := config.Contexts[oldCtxName] 61 | if !ok { 62 | output.Fatal("Context <%s> not found.", oldCtxName) 63 | } 64 | 65 | var ( 66 | oldCluster = dstCtx.Cluster 67 | oldUser = dstCtx.AuthInfo 68 | cluster = config.Clusters[dstCtx.Cluster] 69 | user = config.AuthInfos[dstCtx.AuthInfo] 70 | ) 71 | 72 | newCtxName := prompt.TextInput("Enter a new name", oldCtxName) 73 | for contextNameConflict(newCtxName, config) { 74 | if newCtxName == oldCtxName { 75 | output.Done("Context <%s> not changed.", oldCtxName) 76 | return 77 | } 78 | 79 | newCtxName = prompt.TextInput(fmt.Sprintf("Context name <%s> already exists, enter a new name", newCtxName), newCtxName) 80 | } 81 | 82 | dstCtx.Cluster = "cluster-" + newCtxName 83 | dstCtx.AuthInfo = "user-" + newCtxName 84 | if dstCtx.Cluster != oldCluster { 85 | config.Clusters[dstCtx.Cluster] = cluster 86 | delete(config.Clusters, oldCluster) 87 | } 88 | 89 | if dstCtx.AuthInfo != oldUser { 90 | config.AuthInfos[dstCtx.AuthInfo] = user 91 | delete(config.AuthInfos, oldUser) 92 | } 93 | config.Contexts[newCtxName] = dstCtx 94 | if config.CurrentContext == oldCtxName { 95 | config.CurrentContext = newCtxName 96 | } 97 | delete(config.Contexts, oldCtxName) 98 | 99 | kube.SaveConfigToFile(config, rootFlag.kubeconfig) 100 | output.Done("Context <%s> renamed to <%s>.", oldCtxName, newCtxName) 101 | } 102 | -------------------------------------------------------------------------------- /internal/completion/completion.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Ketches Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package completion 18 | 19 | import ( 20 | "fmt" 21 | "slices" 22 | 23 | "github.com/ketches/ktx/internal/kube" 24 | "github.com/spf13/cobra" 25 | ) 26 | 27 | // None is a shell completion function that does nothing. 28 | func None(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 29 | return nil, cobra.ShellCompDirectiveNoFileComp 30 | } 31 | 32 | // Context is a shell completion function that completes context names, just one completion. 33 | func Context(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 34 | if len(args) > 0 { 35 | return nil, cobra.ShellCompDirectiveNoFileComp 36 | } 37 | 38 | current := kube.ListContexts(kube.LoadConfigFromFile(cmd.Flag("kubeconfig").Value.String())) 39 | var completions []string 40 | for _, context := range current { 41 | completions = append(completions, fmt.Sprintf("%s\t[%s] %s - %s", context.Name, context.Emoji, context.Namespace, context.Server)) 42 | } 43 | 44 | return completions, cobra.ShellCompDirectiveNoFileComp 45 | } 46 | 47 | // ContextArray is a shell completion function that completes context names, allow multiple completion. 48 | func ContextArray(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 49 | current := kube.ListContexts(kube.LoadConfigFromFile(cmd.Flag("kubeconfig").Value.String())) 50 | 51 | var completions []string 52 | for _, context := range current { 53 | if slices.Contains(args, context.Name) { 54 | continue 55 | } 56 | completions = append(completions, fmt.Sprintf("%s\t[%s] %s - %s", context.Name, context.Emoji, context.Namespace, context.Server)) 57 | } 58 | 59 | return completions, cobra.ShellCompDirectiveNoFileComp 60 | } 61 | 62 | // Server is a shell completion function that completes server names, just one completion. 63 | func Server(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 64 | if len(args) > 0 { 65 | return nil, cobra.ShellCompDirectiveNoFileComp 66 | } 67 | 68 | config := kube.LoadConfigFromFile(cmd.Flag("kubeconfig").Value.String()) 69 | ctxName := cmd.Flag("context").Value.String() 70 | if len(ctxName) == 0 { 71 | ctxName = config.CurrentContext 72 | } 73 | ctx := config.Contexts[ctxName] 74 | 75 | completions := []string{config.Clusters[ctx.Cluster].Server} 76 | return completions, cobra.ShellCompDirectiveNoFileComp 77 | } 78 | 79 | // Namespace is a shell completion function that completes namespace names, just one completion. 80 | func Namespace(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 81 | if len(args) > 0 { 82 | return nil, cobra.ShellCompDirectiveNoFileComp 83 | } 84 | 85 | kubeClientset := kube.ClientOrDie(cmd.Flag("kubeconfig").Value.String(), cmd.Flag("context").Value.String()) 86 | 87 | return kube.ListNamespaces(kubeClientset), cobra.ShellCompDirectiveNoFileComp 88 | } 89 | 90 | // ServiceAccount is a shell completion function that completes service account names, just one completion. 91 | func ServiceAccount(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 92 | if len(args) > 0 { 93 | return nil, cobra.ShellCompDirectiveNoFileComp 94 | } 95 | 96 | kubeClientset := kube.ClientOrDie(cmd.Flag("kubeconfig").Value.String(), cmd.Flag("context").Value.String()) 97 | 98 | return kube.ListServiceAccounts(kubeClientset, cmd.Flag("namespace").Value.String()), cobra.ShellCompDirectiveNoFileComp 99 | } 100 | -------------------------------------------------------------------------------- /internal/prompt/prompt.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Ketches Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package prompt 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "strings" 23 | 24 | "github.com/ketches/ktx/internal/kube" 25 | "github.com/ketches/ktx/internal/output" 26 | "github.com/ketches/ktx/internal/types" 27 | "github.com/manifoldco/promptui" 28 | clientcmdapi "k8s.io/client-go/tools/clientcmd/api" 29 | ) 30 | 31 | // YesNo prompts the user to select Yes or No 32 | func YesNo(label string) bool { 33 | templates := &promptui.SelectTemplates{ 34 | Label: promptui.Styler(promptui.FGYellow)("❖ {{ . }}?"), 35 | Active: promptui.Styler(promptui.FGCyan, promptui.FGUnderline)("➤ {{ . }}"), 36 | Inactive: promptui.Styler(promptui.FGFaint)(" {{ . }}"), 37 | } 38 | prompt := promptui.Select{ 39 | Label: label, 40 | Items: []string{"No", "Yes"}, 41 | Templates: templates, 42 | Size: 4, 43 | HideSelected: true, 44 | } 45 | _, obj, err := prompt.Run() 46 | if err != nil { 47 | output.Fatal("Prompt failed %v", err) 48 | } 49 | 50 | return obj == "Yes" 51 | } 52 | 53 | // TextInput prompts the user to input a value 54 | func TextInput(label, def string) string { 55 | prompt := promptui.Prompt{ 56 | Label: promptui.Styler(promptui.FGYellow)(label), 57 | Validate: func(input string) error { 58 | if len(strings.TrimSpace(input)) == 0 { 59 | return fmt.Errorf("please input a valid value") 60 | } 61 | return nil 62 | }, 63 | Default: def, 64 | Templates: &promptui.PromptTemplates{ 65 | Prompt: promptui.Styler(promptui.FGCyan)("➤ {{ . }} "), 66 | ValidationError: promptui.Styler(promptui.FGRed)("✗ {{ . }}"), 67 | }, 68 | HideEntered: true, 69 | } 70 | result, err := prompt.Run() 71 | if err != nil { 72 | output.Fatal("Prompt failed %v", err) 73 | } 74 | result = strings.TrimSpace(result) 75 | 76 | return result 77 | } 78 | 79 | // ContextSelection prompts the user to select a context 80 | func ContextSelection(label string, config *clientcmdapi.Config) string { 81 | ctxs := kube.ListContexts(config) 82 | ctxs = append(ctxs, &types.ContextProfile{ 83 | Name: "Exit", 84 | Emoji: "✗", 85 | }) 86 | cursorPos := 0 87 | for i, ctx := range ctxs { 88 | if ctx.Current { 89 | cursorPos = i 90 | } 91 | } 92 | 93 | templates := &promptui.SelectTemplates{ 94 | Label: promptui.Styler(promptui.FGYellow)("❖ {{ . }}:"), 95 | Active: promptui.Styler(promptui.FGCyan, promptui.FGUnderline)("➤ {{ .Emoji }} {{ .Name }}"), 96 | Inactive: promptui.Styler(promptui.FGFaint)(" {{ .Emoji }} {{ .Name }}"), 97 | Details: `{{if .Cluster}} 98 | ---------- Context ---------- 99 | {{ "Name:" | faint }} {{ .Name }} 100 | {{ "Namespace:" | faint }} {{ .Namespace }} 101 | {{ "Cluster:" | faint }} {{ .Cluster }} 102 | {{ "User:" | faint }} {{ .User }} 103 | {{ "Server:" | faint }} {{ .Server }}{{end}}`, 104 | } 105 | 106 | prompt := promptui.Select{ 107 | Label: label, 108 | Items: ctxs, 109 | Searcher: func(input string, index int) bool { 110 | if index < 0 || index >= len(ctxs) { 111 | return false 112 | } 113 | 114 | current := ctxs[index] 115 | if strings.Contains(strings.ToLower(current.Name), strings.ToLower(input)) || 116 | strings.Contains(strings.ToLower(current.Server), strings.ToLower(input)) { 117 | return true 118 | } 119 | 120 | return false 121 | }, 122 | HideSelected: true, 123 | CursorPos: cursorPos, 124 | Templates: templates, 125 | } 126 | 127 | index, _, err := prompt.Run() 128 | if err != nil { 129 | output.Fatal("Prompt failed %v", err) 130 | } 131 | 132 | if index == len(ctxs)-1 { 133 | os.Exit(0) 134 | } 135 | 136 | return ctxs[index].Name 137 | } 138 | -------------------------------------------------------------------------------- /cmd/list.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 The Ketches Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "os" 21 | "sync" 22 | "time" 23 | 24 | "github.com/fatih/color" 25 | "github.com/jedib0t/go-pretty/v6/table" 26 | completion "github.com/ketches/ktx/internal/completion" 27 | "github.com/ketches/ktx/internal/kube" 28 | "github.com/ketches/ktx/internal/output" 29 | "github.com/ketches/ktx/internal/prompt" 30 | "github.com/ketches/ktx/internal/types" 31 | "github.com/ketches/ktx/internal/util" 32 | "github.com/spf13/cobra" 33 | ) 34 | 35 | type listFlags struct { 36 | clusterInfo bool 37 | } 38 | 39 | var listFlag listFlags 40 | 41 | // listCmd represents the list command 42 | var listCmd = &cobra.Command{ 43 | Use: "list", 44 | Aliases: []string{"ls"}, 45 | Short: "List contexts in ~/.kube/config", 46 | Long: `List contexts in ~/.kube/config`, 47 | Run: func(cmd *cobra.Command, args []string) { 48 | runList() 49 | }, 50 | ValidArgsFunction: completion.None, 51 | } 52 | 53 | func init() { 54 | rootCmd.AddCommand(listCmd) 55 | 56 | listCmd.Flags().BoolVar(&listFlag.clusterInfo, "cluster-info", false, "Show cluster info eg. status, version, and more.") 57 | } 58 | 59 | func runList() { 60 | config := kube.LoadConfigFromFile(rootFlag.kubeconfig) 61 | ctxs := kube.ListContexts(config) 62 | 63 | if len(ctxs) == 0 { 64 | output.Note("No context found.") 65 | return 66 | } 67 | 68 | listContexts(ctxs) 69 | 70 | // 如果当前没有 context,那么提示用户选择一个 context 71 | if len(config.CurrentContext) == 0 { 72 | config.CurrentContext = prompt.ContextSelection("Select a context as current", config) 73 | kube.SaveConfigToFile(config, rootFlag.kubeconfig) 74 | } 75 | } 76 | 77 | var tableStyle = table.Style{ 78 | Name: "KrbTableStyle", 79 | Box: table.StyleBoxDefault, 80 | Color: table.ColorOptionsDefault, 81 | Format: table.FormatOptionsDefault, 82 | HTML: table.DefaultHTMLOptions, 83 | Options: table.OptionsNoBordersAndSeparators, 84 | Size: table.SizeOptionsDefault, 85 | Title: table.TitleOptionsDefault, 86 | } 87 | 88 | func listContexts(ctxs []*types.ContextProfile) { 89 | t := table.NewWriter() 90 | t.SetOutputMirror(os.Stdout) 91 | row := table.Row{"", "name", "namespace", "server"} 92 | if listFlag.clusterInfo { 93 | row = append(row, "status", "version") 94 | 95 | var wg sync.WaitGroup 96 | for i, ctx := range ctxs { 97 | wg.Add(1) 98 | go func(i int, ctx *types.ContextProfile) { 99 | defer wg.Done() 100 | 101 | var ( 102 | clusterStatus = types.ClusterStatusUnavailable 103 | clusterVersion string 104 | ) 105 | done := make(chan struct{}) 106 | go func() { 107 | defer close(done) 108 | dc, _ := kube.DiscoveryClient(rootFlag.kubeconfig, ctx.Name) 109 | if dc != nil { 110 | cv, _ := kube.ServerVersion(dc) 111 | if cv != "" { 112 | clusterStatus = types.ClusterStatusAvailable 113 | clusterVersion = cv 114 | } 115 | } 116 | }() 117 | 118 | timer := time.NewTimer(time.Second * 2) 119 | defer timer.Stop() 120 | 121 | select { 122 | case <-timer.C: 123 | clusterStatus = types.ClusterStatusTimeout 124 | case <-done: 125 | } 126 | 127 | ctx.ClusterStatus = clusterStatus 128 | ctx.ClusterVersion = clusterVersion 129 | }(i, ctx) 130 | } 131 | wg.Wait() 132 | } 133 | 134 | t.AppendHeader(row) 135 | 136 | for _, ctx := range ctxs { 137 | appendRow(t, ctx) 138 | } 139 | t.SetStyle(tableStyle) 140 | t.Render() 141 | } 142 | 143 | func appendRow(t table.Writer, ctx *types.ContextProfile) { 144 | if ctx.Current { 145 | ctx.Name = color.CyanString(ctx.Name) 146 | ctx.Namespace = color.CyanString(ctx.Namespace) 147 | ctx.Server = color.CyanString(ctx.Server) 148 | ctx.Emoji = color.CyanString(ctx.Emoji) 149 | } 150 | row := table.Row{ctx.Emoji, ctx.Name, ctx.Namespace, ctx.Server} 151 | if listFlag.clusterInfo { 152 | row = append(row, string(ctx.ClusterStatus.ColorString()), util.If(ctx.ClusterVersion == "", "-", color.CyanString(ctx.ClusterVersion))) 153 | } 154 | t.AppendRow(row, table.RowConfig{ 155 | AutoMerge: true, 156 | }) 157 | } 158 | -------------------------------------------------------------------------------- /cmd/add.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 The Ketches Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "fmt" 21 | 22 | completion "github.com/ketches/ktx/internal/completion" 23 | "github.com/ketches/ktx/internal/kube" 24 | "github.com/ketches/ktx/internal/output" 25 | "github.com/ketches/ktx/internal/prompt" 26 | "github.com/ketches/ktx/internal/util" 27 | "github.com/spf13/cobra" 28 | clientcmdapi "k8s.io/client-go/tools/clientcmd/api" 29 | ) 30 | 31 | var ( 32 | addFile string 33 | ) 34 | 35 | // addCmd represents the add command 36 | var addCmd = &cobra.Command{ 37 | Use: "add", 38 | Short: "Add context from kubeconfig file to ~/.kube/config", 39 | Long: `Add context from kubeconfig file to ~/.kube/config`, 40 | Run: func(cmd *cobra.Command, args []string) { 41 | runAdd() 42 | }, 43 | ValidArgsFunction: completion.None, 44 | } 45 | 46 | func init() { 47 | rootCmd.AddCommand(addCmd) 48 | 49 | addCmd.Flags().StringVarP(&addFile, "file", "f", "", "kubeconfig file") 50 | 51 | addCmd.MarkFlagRequired("file") 52 | } 53 | 54 | func runAdd() { 55 | if !util.IsFileExist(addFile) { 56 | output.Fatal("File %s not found.", addFile) 57 | } 58 | 59 | config := kube.LoadConfigFromFile(rootFlag.kubeconfig) 60 | kube.StandardizeConfig(config) 61 | 62 | new := kube.LoadConfigFromFile(addFile) 63 | kube.StandardizeConfig(new) 64 | 65 | merge(config, new) 66 | } 67 | 68 | func merge(config, new *clientcmdapi.Config) { 69 | for newCtxName, newCtx := range new.Contexts { 70 | newCluster, ok := new.Clusters[newCtx.Cluster] 71 | if !ok { 72 | output.Note("Cluster not found for context <%s> in file %s, skipped.", newCtxName, addFile) 73 | continue 74 | } 75 | 76 | newUser, ok := new.AuthInfos[newCtx.AuthInfo] 77 | if !ok { 78 | output.Note("User not found for context <%s> in file %s, skipped.", newCtxName, addFile) 79 | continue 80 | } 81 | 82 | // 如果 context 名称已经存在,要求用户输入新的 context 名称 83 | var quitWithConflict bool 84 | for contextNameConflict(newCtxName, config) { 85 | if prompt.YesNo(fmt.Sprintf("Context name <%s> already exists, rename it", newCtxName)) { 86 | newCtxName = prompt.TextInput("Enter a new context name", newCtxName) 87 | newCtx.Cluster = "cluster-" + newCtxName 88 | newCtx.AuthInfo = "user-" + newCtxName 89 | } else { 90 | quitWithConflict = true 91 | break 92 | } 93 | } 94 | 95 | if quitWithConflict { 96 | continue 97 | } 98 | 99 | mf := &mergeFrom{ 100 | contextName: newCtxName, 101 | clusterName: newCtx.Cluster, 102 | userName: newCtx.AuthInfo, 103 | context: newCtx, 104 | cluster: newCluster, 105 | user: newUser, 106 | } 107 | 108 | handleMerge(config, mf) 109 | } 110 | 111 | kube.SaveConfigToFile(config, rootFlag.kubeconfig) 112 | 113 | // 如果当前没有 context,那么提示用户选择一个 context 114 | if len(config.CurrentContext) == 0 && len(config.Contexts) > 0 { 115 | if len(config.Contexts) == 1 { 116 | for ctxName := range config.Contexts { 117 | config.CurrentContext = ctxName 118 | break 119 | } 120 | } else { 121 | config.CurrentContext = prompt.ContextSelection("Select a context to as current", config) 122 | } 123 | kube.SaveConfigToFile(config, rootFlag.kubeconfig) 124 | } 125 | } 126 | 127 | type mergeFrom struct { 128 | contextName, clusterName, userName string 129 | context *clientcmdapi.Context 130 | cluster *clientcmdapi.Cluster 131 | user *clientcmdapi.AuthInfo 132 | } 133 | 134 | func handleMerge(config *clientcmdapi.Config, mf *mergeFrom) { 135 | for contextNameConflict(mf.contextName, config) { 136 | mf.contextName = prompt.TextInput(fmt.Sprintf("Context name <%s> already exists, enter a new name", mf.contextName), mf.contextName) 137 | mf.clusterName = "cluster-" + mf.contextName 138 | mf.userName = "user-" + mf.contextName 139 | } 140 | 141 | config.Clusters[mf.clusterName] = mf.cluster 142 | config.AuthInfos[mf.userName] = mf.user 143 | config.Contexts[mf.contextName] = mf.context 144 | 145 | output.Done("Context <%s> added.", mf.contextName) 146 | } 147 | 148 | func contextNameConflict(name string, config *clientcmdapi.Config) bool { 149 | _, ok := config.Contexts[name] 150 | return ok 151 | } 152 | -------------------------------------------------------------------------------- /internal/kube/kubeconfig.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Ketches Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package kube 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "path/filepath" 23 | "sort" 24 | "time" 25 | 26 | "github.com/ketches/ktx/internal/output" 27 | "github.com/ketches/ktx/internal/types" 28 | "github.com/ketches/ktx/internal/util" 29 | v1 "k8s.io/api/core/v1" 30 | "k8s.io/client-go/tools/clientcmd" 31 | clientcmdapi "k8s.io/client-go/tools/clientcmd/api" 32 | "k8s.io/client-go/util/homedir" 33 | ) 34 | 35 | var ( 36 | DefaultConfigDir = filepath.Join(homedir.HomeDir(), ".kube") 37 | DefaultConfigFile = filepath.Join(DefaultConfigDir, "config") 38 | DefaultNamespace = "default" 39 | ) 40 | 41 | // NewConfig returns a new kubeconfig 42 | func NewConfig() *clientcmdapi.Config { 43 | return &clientcmdapi.Config{ 44 | APIVersion: "v1", 45 | Kind: "Config", 46 | Contexts: make(map[string]*clientcmdapi.Context), 47 | Clusters: make(map[string]*clientcmdapi.Cluster), 48 | AuthInfos: make(map[string]*clientcmdapi.AuthInfo), 49 | } 50 | } 51 | 52 | // StandardizeConfig standardizes the cluster and user names in the kubeconfig 53 | func StandardizeConfig(config *clientcmdapi.Config) { 54 | new := NewConfig() 55 | 56 | for ctxName, ctx := range config.Contexts { 57 | if _, ok := config.Clusters[ctx.Cluster]; !ok { 58 | continue 59 | } 60 | if _, ok := config.AuthInfos[ctx.AuthInfo]; !ok { 61 | continue 62 | } 63 | 64 | new.Clusters["cluster-"+ctxName] = config.Clusters[ctx.Cluster] 65 | new.AuthInfos["user-"+ctxName] = config.AuthInfos[ctx.AuthInfo] 66 | 67 | new.Contexts[ctxName] = ctx 68 | new.Contexts[ctxName].Cluster = "cluster-" + ctxName 69 | new.Contexts[ctxName].AuthInfo = "user-" + ctxName 70 | } 71 | 72 | if _, ok := new.Contexts[config.CurrentContext]; ok { 73 | new.CurrentContext = config.CurrentContext 74 | } 75 | *config = *new 76 | } 77 | 78 | // LoadConfigFromFile loads the kubeconfig from the file 79 | func LoadConfigFromFile(file string) *clientcmdapi.Config { 80 | config, err := clientcmd.LoadFromFile(file) 81 | if err != nil { 82 | output.Fatal("Failed to load kubeconfig from file: %s", err) 83 | } 84 | 85 | return config 86 | } 87 | 88 | // SaveConfigToFile saves the kubeconfig to the file 89 | func SaveConfigToFile(config *clientcmdapi.Config, file string) { 90 | if err := clientcmd.WriteToFile(*config, file); err != nil { 91 | output.Fatal("Failed to save kubeconfig to file: %s", err) 92 | } 93 | } 94 | 95 | // PrintConfig prints the kubeconfig 96 | func PrintConfig(config *clientcmdapi.Config) { 97 | v, er := clientcmd.Write(*config) 98 | if er != nil { 99 | output.Fatal("Failed to write kubeconfig: %s", er) 100 | } 101 | fmt.Print(string(v)) 102 | } 103 | 104 | // CheckOrInitConfig checks if the kubeconfig exists, if not, create it 105 | func CheckOrInitConfig() { 106 | kubeconfigDir := DefaultConfigDir 107 | kubeconfigFile := DefaultConfigFile 108 | 109 | if _, err := os.Stat(kubeconfigFile); os.IsNotExist(err) { 110 | if err := os.MkdirAll(kubeconfigDir, 0700); err != nil { 111 | output.Fatal("Failed to create %s directory: %s", kubeconfigDir, err) 112 | } 113 | 114 | f, err := os.OpenFile(kubeconfigFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) 115 | if err != nil { 116 | output.Fatal("Failed to create %s: %s", kubeconfigFile, err) 117 | } 118 | defer f.Close() 119 | } 120 | } 121 | 122 | // ListContexts lists all contexts in the kubeconfig 123 | func ListContexts(config *clientcmdapi.Config) []*types.ContextProfile { 124 | var contexts []*types.ContextProfile 125 | for contextName, context := range config.Contexts { 126 | item := &types.ContextProfile{ 127 | Current: contextName == config.CurrentContext, 128 | Name: contextName, 129 | Cluster: context.Cluster, 130 | User: context.AuthInfo, 131 | Namespace: util.If(len(context.Namespace) > 0, context.Namespace, DefaultNamespace), 132 | Server: config.Clusters[context.Cluster].Server, 133 | } 134 | item.Emoji = util.If(item.Current, "✲", " ") 135 | contexts = append(contexts, item) 136 | } 137 | 138 | sort.Slice(contexts, func(i, j int) bool { 139 | return contexts[i].Name < contexts[j].Name 140 | }) 141 | 142 | return contexts 143 | } 144 | 145 | // GenerateConfigForServiceAccount generates kubeconfig for service account 146 | func GenerateConfigForServiceAccount(kubeconfig, context, namespace, serviceAccount string) *clientcmdapi.Config { 147 | restConfig := configOrDie(kubeconfig, context) 148 | 149 | kubeClientset := ClientOrDie(kubeconfig, context) 150 | sa := GetServiceAccount(kubeClientset, serviceAccount, namespace) 151 | 152 | var secret *v1.Secret 153 | if len(sa.Secrets) == 0 { 154 | // Create a new secret for the service account 155 | secret = CreateServiceAccountTokenSecret(kubeClientset, serviceAccount, namespace) 156 | 157 | for i := 0; i < 3; i++ { 158 | secret = GetSecret(kubeClientset, secret.Name, namespace) 159 | if len(secret.Data) > 0 { 160 | break 161 | } 162 | 163 | time.Sleep(1 * time.Millisecond) 164 | } 165 | } else { 166 | secret = GetSecret(kubeClientset, sa.Secrets[0].Name, namespace) 167 | } 168 | 169 | var ( 170 | cfgContext = serviceAccount 171 | cfgCluster = "cluster-" + serviceAccount 172 | cfgUser = "user-" + serviceAccount 173 | ) 174 | 175 | return &clientcmdapi.Config{ 176 | APIVersion: "v1", 177 | Kind: "Config", 178 | CurrentContext: cfgContext, 179 | Contexts: map[string]*clientcmdapi.Context{ 180 | cfgContext: { 181 | Cluster: cfgCluster, 182 | AuthInfo: cfgUser, 183 | Namespace: string(secret.Data["namespace"]), 184 | }, 185 | }, 186 | Clusters: map[string]*clientcmdapi.Cluster{ 187 | cfgCluster: { 188 | Server: restConfig.Host, 189 | CertificateAuthorityData: secret.Data["ca.crt"], 190 | }, 191 | }, 192 | AuthInfos: map[string]*clientcmdapi.AuthInfo{ 193 | cfgUser: { 194 | Token: string(secret.Data["token"]), 195 | }, 196 | }, 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2025 The Ketches Authors. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 2 | github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= 3 | github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 4 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 5 | github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= 6 | github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= 7 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 8 | github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= 9 | github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 10 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 14 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= 16 | github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 17 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 18 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 19 | github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= 20 | github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 21 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 22 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 23 | github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= 24 | github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= 25 | github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= 26 | github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 27 | github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= 28 | github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= 29 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 30 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 31 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 32 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 33 | github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= 34 | github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= 35 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 36 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 37 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 38 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 39 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 40 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 41 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 42 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 43 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 44 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 45 | github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= 46 | github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= 47 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 48 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 49 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 50 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 51 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 52 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 53 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 54 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 55 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 56 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 57 | github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= 58 | github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= 59 | github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= 60 | github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 61 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 62 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 63 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 64 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 65 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 66 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 67 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 68 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 69 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 70 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 71 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 72 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 73 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 74 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= 75 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 76 | github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= 77 | github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 78 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 79 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 80 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 81 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 82 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 83 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 84 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 85 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 86 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 87 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 88 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 89 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 90 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 91 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 92 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 93 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 94 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 95 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 96 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 97 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 98 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 99 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 100 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 101 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 102 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 103 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 104 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 105 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 106 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 107 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 108 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 109 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 110 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 111 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 112 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 113 | golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= 114 | golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 115 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 116 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 117 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 118 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 119 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 120 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 121 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 122 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 123 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 124 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 125 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 126 | golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= 127 | golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 128 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 129 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 130 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 131 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 132 | golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= 133 | golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 134 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 135 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 136 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 137 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 138 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 139 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 140 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 141 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 142 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 143 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 144 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 145 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 146 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 147 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 148 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 149 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 150 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 151 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 152 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 153 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 154 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 155 | k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= 156 | k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= 157 | k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= 158 | k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= 159 | k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= 160 | k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= 161 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 162 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 163 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= 164 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= 165 | k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 h1:jgJW5IePPXLGB8e/1wvd0Ich9QE97RvvF3a8J3fP/Lg= 166 | k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 167 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= 168 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 169 | sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 170 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 171 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 172 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= 173 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= 174 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 175 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 176 | --------------------------------------------------------------------------------