├── CONTRIBUTING.md ├── main.go ├── .gitignore ├── cmd ├── constants.go ├── auth.go ├── root.go ├── collect_test.go ├── commands.go ├── collect.go ├── printer_test.go └── printer.go ├── tests ├── test.yml └── e2e.sh ├── .github ├── dependabot.yml ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── go-releaser.yml │ ├── ci.yml │ ├── e2e.yml │ └── codeql-analysis.yml ├── .goreleaser.yml ├── internal └── version │ └── version.go ├── LICENSE ├── .krew.yaml ├── go.mod ├── README.md ├── CODE_OF_CONDUCT.md └── go.sum /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Create an Issue or Create a PR 4 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "kurt/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | .vscode/ 8 | **/__debug_bin 9 | kurt 10 | !kurt/ 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out -------------------------------------------------------------------------------- /cmd/constants.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | var namespaceTracker = make(map[string]int32) 4 | var nodeTracker = make(map[string]int32) 5 | var podTracker = make(map[string]int32) 6 | var labelTracker = make(map[string]int32) 7 | var containerTracker = make(map[string]map[string]int32) 8 | 9 | var printAll bool 10 | var printNS bool 11 | var printNode bool 12 | var printPods bool 13 | var printLabel bool 14 | -------------------------------------------------------------------------------- /tests/test.yml: -------------------------------------------------------------------------------- 1 | kind: Namespace 2 | apiVersion: v1 3 | metadata: 4 | name: test1 5 | --- 6 | kind: Namespace 7 | apiVersion: v1 8 | metadata: 9 | name: test2 10 | --- 11 | apiVersion: v1 12 | kind: Pod 13 | metadata: 14 | name: nginx 15 | namespace: test1 16 | spec: 17 | containers: 18 | - name: nginx 19 | image: nginx:latest 20 | --- 21 | apiVersion: v1 22 | kind: Pod 23 | metadata: 24 | name: apache 25 | namespace: test2 26 | spec: 27 | containers: 28 | - name: apache 29 | image: httpd:latest -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | #### What this PR does / why we need it: 8 | 9 | #### Which issue(s) this PR fixes: 10 | 15 | Fixes # 16 | 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: kwsorensen 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - id: kurt 3 | goos: 4 | - darwin 5 | - linux 6 | - windows 7 | goarch: 8 | - amd64 9 | - arm64 10 | ldflags: -s -w 11 | -X kurt/internal/version.version={{.Version}} 12 | -X kurt/internal/version.gitSHA={{.Commit}} 13 | -X kurt/internal/version.buildTime={{.Date}} 14 | -extldflags "-static" 15 | checksum: 16 | name_template: "{{ .ProjectName }}_checksums.txt" 17 | sboms: # https://goreleaser.com/customization/sbom/ 18 | - artifacts: archive 19 | archives: 20 | - id: kurt 21 | builds: 22 | - kurt 23 | format: tar.gz 24 | format_overrides: 25 | - goos: windows 26 | format: zip 27 | name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}" -------------------------------------------------------------------------------- /cmd/auth.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "k8s.io/client-go/kubernetes" 5 | _ "k8s.io/client-go/plugin/pkg/client/auth" 6 | "k8s.io/client-go/tools/clientcmd" 7 | ) 8 | 9 | // Handle setting up cluster auth and return clientset 10 | func auth() *kubernetes.Clientset { 11 | 12 | loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() 13 | configOverrides := &clientcmd.ConfigOverrides{} 14 | 15 | kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) 16 | config, err := kubeConfig.ClientConfig() 17 | if err != nil { 18 | panic(err.Error()) 19 | } 20 | 21 | clientset, err := kubernetes.NewForConfig(config) 22 | if err != nil { 23 | panic(err.Error()) 24 | } 25 | 26 | return clientset 27 | 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: kwsorensen 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Command run [e.g. ./kurt all] 16 | 2. Information about run environment 17 | Including 18 | - OS: [e.g. Ubuntu] 19 | - kurt cli version [e.g. v0.1.0] 20 | - Confirmation of kubectl configuration/permissions 21 | 22 | **Expected behavior** 23 | A clear and concise description of what you expected to happen. 24 | 25 | **Screenshots** 26 | If applicable, add screenshots to help explain your problem 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | ) 7 | 8 | // NOTE: these variables are injected at build time 9 | var ( 10 | version string = "development" 11 | gitSHA, buildTime string 12 | build Build 13 | ) 14 | 15 | type Build struct { 16 | Version string `json:"version,omitempty"` 17 | GitSHA string `json:"git,omitempty"` 18 | BuildTime string `json:"buildTime,omitempty"` 19 | GoVersion string `json:"goversion,omitempty"` 20 | } 21 | 22 | func initBuild() { 23 | build.Version = version 24 | if len(gitSHA) >= 7 { 25 | build.GitSHA = gitSHA[:7] 26 | } 27 | build.BuildTime = buildTime 28 | build.GoVersion = runtime.Version() 29 | } 30 | 31 | func Version() string { 32 | initBuild() 33 | return fmt.Sprintf("%s\n%s", build.Version, build) 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/go-releaser.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - 13 | name: Checkout 14 | uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | - 18 | name: Set up Go 19 | uses: actions/setup-go@v2 20 | with: 21 | go-version: 1.21 22 | 23 | - name: Anchore SBOM action 24 | uses: anchore/sbom-action@v0 25 | 26 | - 27 | name: Run GoReleaser 28 | uses: goreleaser/goreleaser-action@v2 29 | with: 30 | distribution: goreleaser 31 | version: latest 32 | args: release 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - 37 | name: Update new version in krew-index 38 | uses: rajatjindal/krew-release-bot@v0.0.40 39 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | branches: 4 | - main 5 | push: 6 | branches: 7 | - "main" 8 | tags: 9 | - "v*.*.*" 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-20.04 14 | steps: 15 | - uses: actions/setup-go@v2 16 | with: 17 | go-version: "1.21" 18 | - uses: actions/checkout@v2 19 | - run: go build 20 | 21 | test: 22 | runs-on: ubuntu-20.04 23 | steps: 24 | - uses: actions/setup-go@v2 25 | with: 26 | go-version: "1.21" 27 | - uses: actions/checkout@v2 28 | - run: go test ./cmd -v 29 | 30 | lint: 31 | runs-on: ubuntu-20.04 32 | steps: 33 | - uses: actions/setup-go@v2 34 | with: 35 | go-version: "1.21" 36 | - uses: actions/checkout@v2 37 | - run: | 38 | if 39 | test -z $(gofmt -l .); then 40 | echo "All golang files formatted correctly 👍️"; 41 | else 42 | echo "❗️ Golang formatting issues:"; gofmt -l .; exit 1 43 | fi -------------------------------------------------------------------------------- /tests/e2e.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eou pipefail 3 | 4 | kubectl apply -f tests/test.yml --wait 5 | kubectl wait --for=condition=Ready pod/nginx -n test1 6 | kubectl wait --for=condition=Ready pod/apache -n test2 7 | 8 | # Generate some pod restarts 9 | kubectl exec nginx -n test1 -- bash -c "kill 1" 10 | 11 | kubectl exec apache -n test2 -- bash -c "kill 1" 12 | 13 | sleep 5 14 | kubectl exec nginx -n test1 -- bash -c "kill 1" 15 | 16 | echo "[!] wait for pods to finish restarting..." 17 | sleep 30 18 | 19 | NGINX_RESTARTS=$(./kurt pods -n test1 -o json | jq '.pods[0].count') 20 | if [ $NGINX_RESTARTS -eq 2 ]; then 21 | echo "[+] Correct number of restarts for nginx 👍" 22 | else 23 | echo "[!] Incorrect number of restarts for nginx: $NGINX_RESTARTS" 24 | exit 1 25 | fi 26 | 27 | APACHE_RESTARTS=$(./kurt pods -n test2 -o json | jq '.pods[0].count') 28 | if [ $APACHE_RESTARTS -eq 1 ]; then 29 | echo "[+] Correct number of restarts for apache 👍" 30 | else 31 | echo "[!] Incorrect number of restarts for apache: $APACHE_RESTARTS" 32 | exit 1 33 | fi 34 | 35 | echo "[+] Cleaning up..." 36 | kubectl delete -f tests/test.yml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Kyle Sorensen, Aaron Stults 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yml: -------------------------------------------------------------------------------- 1 | name: e2e tests 2 | on: 3 | pull_request: 4 | workflow_dispatch: 5 | jobs: 6 | run-e2e-tests: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | include: 11 | # get these here: https://github.com/kubernetes-sigs/kind/releases 12 | - version: "1.32" 13 | image: kindest/node:v1.32.0@sha256:c48c62eac5da28cdadcf560d1d8616cfa6783b58f0d94cf63ad1bf49600cb027 14 | - version: "1.31" 15 | image: kindest/node:v1.31.4@sha256:2cb39f7295fe7eafee0842b1052a599a4fb0f8bcf3f83d96c7f4864c357c6c30 16 | - version: "1.30" 17 | image: kindest/node:v1.30.8@sha256:17cd608b3971338d9180b00776cb766c50d0a0b6b904ab4ff52fd3fc5c6369bf 18 | - version: "1.29" 19 | image: kindest/node:v1.29.2@sha256:51a1434a5397193442f0be2a297b488b6c919ce8a3931be0ce822606ea5ca245 20 | - version: "1.28" 21 | image: kindest/node:v1.28.7@sha256:9bc6c451a289cf96ad0bbaf33d416901de6fd632415b076ab05f5fa7e4f65c58 22 | - version: "1.27" 23 | image: kindest/node:v1.27.11@sha256:681253009e68069b8e01aad36a1e0fa8cf18bb0ab3e5c4069b2e65cafdd70843 24 | - version: "1.26" 25 | image: kindest/node:v1.26.14@sha256:5d548739ddef37b9318c70cb977f57bf3e5015e4552be4e27e57280a8cbb8e4f 26 | steps: 27 | - name: Create k8s Kind Cluster - ${{ matrix.version }} 28 | uses: helm/kind-action@v1.5.0 29 | with: 30 | node_image: ${{ matrix.image }} 31 | - name: Show cluster version 32 | run: kubectl version 33 | - uses: actions/setup-go@v2 34 | with: 35 | go-version: "1.21" 36 | - uses: actions/checkout@v2 37 | - run: go build 38 | - name: e2e test 39 | run: tests/e2e.sh -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var inamespace []string 12 | var ilabels []string 13 | var ishowContainers bool 14 | var limitFlag int 15 | var output string 16 | 17 | var rootCmd = &cobra.Command{ 18 | Use: "kurt", 19 | Short: "KUbernetes Restart Tracker", 20 | Long: `kurt: KUbernetes Restart Tracker 21 | 22 | A restart tracker that gives context to what is restarting in your cluster 23 | `, 24 | } 25 | 26 | func init() { 27 | rootCmd.PersistentFlags().StringSliceVarP(&inamespace, "namespace", "n", []string{""}, "Specify namespace for kurt to collect restart metrics.\nLeave blank to collect in all namespaces.") 28 | rootCmd.PersistentFlags().StringSliceVarP(&ilabels, "label", "l", []string{""}, "Specify multiple times for the label keys you want to see.\nFor example: \"kurt all -l app\"") 29 | rootCmd.PersistentFlags().IntVarP(&limitFlag, "limit", "c", 5, "Limit the number of resources you want to see. Set limit to 0 for no limits. Must be positive.\nFor example: \"kurt all -c=10\"") 30 | rootCmd.PersistentFlags().StringVarP(&output, "output", "o", "standard", "Specify output type. Options are: json, yaml, standard\nFor example: \"kurt all -o json\"") 31 | 32 | // command specific flags 33 | cmdPods.PersistentFlags().BoolVarP(&ishowContainers, "show-containers", "", false, "Show specific container restart counts for pods\nFor example: \"kurt pods --show-containers\"") 34 | cmdAll.PersistentFlags().BoolVarP(&ishowContainers, "show-containers", "", false, "Show specific container restart counts for pods\nFor example: \"kurt pods --show-containers\"") 35 | 36 | if strings.HasPrefix(filepath.Base(os.Args[0]), "kubectl-") { 37 | rootCmd.SetUsageTemplate(strings.NewReplacer( 38 | "{{.UseLine}}", "kubectl {{.UseLine}}", 39 | "{{.CommandPath}}", "kubectl {{.CommandPath}}").Replace(rootCmd.UsageTemplate())) 40 | } 41 | 42 | } 43 | 44 | func Execute() { 45 | cobra.CheckErr(rootCmd.Execute()) 46 | } 47 | -------------------------------------------------------------------------------- /cmd/collect_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestTrackNamespaces(t *testing.T) { 8 | trackNamespaces("ns1", 5) 9 | trackNamespaces("ns1", 2) 10 | 11 | if namespaceTracker["ns1"] != int32(7) { 12 | t.Errorf("ns1 namespace expected to have a count of 7 but instead shows: %v", namespaceTracker["ns1"]) 13 | } 14 | } 15 | 16 | func TestTrackNodes(t *testing.T) { 17 | trackNodes("node01", 5) 18 | trackNodes("node01", 2) 19 | trackNodes("node02", 2) 20 | 21 | if nodeTracker["node01"] != int32(7) { 22 | t.Errorf("node01 node expected to have a count of 7 but instead shows: %v", nodeTracker["node01"]) 23 | } 24 | } 25 | 26 | func TestTrackPods(t *testing.T) { 27 | // Test that a pod with the same name in a different namespace is held uniquely in the map 28 | trackPods("pod1", "default", 3) 29 | trackPods("pod1", "other", 2) 30 | if podTracker["default:pod1"] != 3 { 31 | t.Errorf("pod1 pod expected to have a count of 3 but instead shows: %v", podTracker["pod1"]) 32 | } 33 | } 34 | 35 | func TestTrackContainers(t *testing.T) { 36 | initializeContainerMap("pod1", "default") 37 | trackContainers("pod1", "default", "container1", 5) 38 | if containerTracker["default:pod1"]["container1"] != 5 { 39 | t.Errorf("pod1/container1 expected to have a count of 5 but instead shows: %v", containerTracker) 40 | } 41 | } 42 | 43 | func TestTrackLabels(t *testing.T) { 44 | tlabels := []string{"app", "k8s-app"} 45 | plabelsA := map[string]string{ 46 | "app": "app1", 47 | "other": "label", 48 | } 49 | plabelsB := map[string]string{ 50 | "k8s-app": "app2", 51 | } 52 | 53 | trackLabels(tlabels, plabelsA, 3) 54 | trackLabels(tlabels, plabelsB, 5) 55 | 56 | // other:label should not exist because it is not defined in tlabels 57 | if labelTracker["other:label"] != int32(0) { 58 | t.Errorf("other:label should not exist because it was not defined in user-defined tlabels") 59 | } 60 | 61 | if labelTracker["app:app1"] != int32(3) { 62 | t.Errorf("app:app1 should be equal to 3 since it is defined in the tlabels but instead shows: %v", labelTracker["app:app1"]) 63 | } 64 | 65 | if labelTracker["k8s-app:app2"] != int32(5) { 66 | t.Errorf("ks-app:app2 should be equal to 5 since it is defined in the tlabels but instead shows: %v", labelTracker["k8s-app:app2"]) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.krew.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: krew.googlecontainertools.github.com/v1alpha2 2 | kind: Plugin 3 | metadata: 4 | name: kurt 5 | spec: 6 | version: {{ .TagName }} 7 | platforms: 8 | - selector: 9 | matchLabels: 10 | os: linux 11 | arch: amd64 12 | {{addURIAndSha "https://github.com/soraro/kurt/releases/download/{{ .TagName }}/kurt_linux_amd64.tar.gz" .TagName }} 13 | files: 14 | - from: kurt 15 | to: . 16 | - from: LICENSE 17 | to: . 18 | bin: kurt 19 | - selector: 20 | matchLabels: 21 | os: darwin 22 | arch: amd64 23 | {{addURIAndSha "https://github.com/soraro/kurt/releases/download/{{ .TagName }}/kurt_darwin_amd64.tar.gz" .TagName }} 24 | files: 25 | - from: kurt 26 | to: . 27 | - from: LICENSE 28 | to: . 29 | bin: kurt 30 | - selector: 31 | matchLabels: 32 | os: darwin 33 | arch: arm64 34 | {{addURIAndSha "https://github.com/soraro/kurt/releases/download/{{ .TagName }}/kurt_darwin_arm64.tar.gz" .TagName }} 35 | files: 36 | - from: kurt 37 | to: . 38 | - from: LICENSE 39 | to: . 40 | bin: kurt 41 | - selector: 42 | matchLabels: 43 | os: linux 44 | arch: arm64 45 | {{addURIAndSha "https://github.com/soraro/kurt/releases/download/{{ .TagName }}/kurt_linux_arm64.tar.gz" .TagName }} 46 | files: 47 | - from: kurt 48 | to: . 49 | - from: LICENSE 50 | to: . 51 | bin: kurt 52 | - selector: 53 | matchLabels: 54 | os: windows 55 | arch: amd64 56 | {{addURIAndSha "https://github.com/soraro/kurt/releases/download/{{ .TagName }}/kurt_windows_amd64.zip" .TagName }} 57 | files: 58 | - from: kurt.exe 59 | to: . 60 | - from: LICENSE 61 | to: . 62 | bin: kurt.exe 63 | shortDescription: Find what's restarting and why 64 | homepage: https://github.com/soraro/kurt 65 | description: | 66 | Use kurt to see pods that are restarting in your cluster and get further context to issues by grouping the results. 67 | Top 5 results from all groupings: 68 | kubectl kurt all 69 | 70 | Top 5 nodes with restarting pods: 71 | kubectl kurt nodes 72 | 73 | All restarting pods in the test namespace: 74 | kubectl kurt pods -c 0 -n test 75 | 76 | Help: 77 | kubectl kurt -h 78 | 79 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module kurt 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.4 6 | 7 | require ( 8 | github.com/spf13/cobra v1.8.1 9 | gopkg.in/yaml.v2 v2.4.0 10 | k8s.io/apimachinery v0.32.1 11 | k8s.io/client-go v0.32.1 12 | ) 13 | 14 | require ( 15 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 16 | github.com/emicklei/go-restful/v3 v3.12.1 // indirect 17 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 18 | github.com/go-logr/logr v1.4.2 // indirect 19 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 20 | github.com/go-openapi/jsonreference v0.21.0 // indirect 21 | github.com/go-openapi/swag v0.23.0 // indirect 22 | github.com/gogo/protobuf v1.3.2 // indirect 23 | github.com/golang/protobuf v1.5.4 // indirect 24 | github.com/google/gnostic-models v0.6.9 // indirect 25 | github.com/google/go-cmp v0.6.0 // indirect 26 | github.com/google/gofuzz v1.2.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/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 33 | github.com/modern-go/reflect2 v1.0.2 // indirect 34 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 35 | github.com/pkg/errors v0.9.1 // indirect 36 | github.com/spf13/pflag v1.0.5 // indirect 37 | github.com/x448/float16 v0.8.4 // indirect 38 | golang.org/x/net v0.36.0 // indirect 39 | golang.org/x/oauth2 v0.24.0 // indirect 40 | golang.org/x/sys v0.30.0 // indirect 41 | golang.org/x/term v0.29.0 // indirect 42 | golang.org/x/text v0.22.0 // indirect 43 | golang.org/x/time v0.8.0 // indirect 44 | google.golang.org/protobuf v1.36.0 // indirect 45 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 46 | gopkg.in/inf.v0 v0.9.1 // indirect 47 | gopkg.in/yaml.v3 v3.0.1 // indirect 48 | k8s.io/api v0.32.1 // indirect 49 | k8s.io/klog/v2 v2.130.1 // indirect 50 | k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect 51 | k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect 52 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 53 | sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect 54 | sigs.k8s.io/yaml v1.4.0 // indirect 55 | ) 56 | -------------------------------------------------------------------------------- /cmd/commands.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/cobra" 6 | "kurt/internal/version" 7 | ) 8 | 9 | var cmdNamespaces = &cobra.Command{ 10 | Use: "namespaces", 11 | Short: "Only print namespace-wide restart counts", 12 | Long: "Only print namespace-wide restart counts", 13 | Aliases: []string{"ns"}, 14 | Run: func(cmd *cobra.Command, args []string) { 15 | printNS = true 16 | printAll = false 17 | clientset := auth() 18 | collect(clientset, inamespace, ilabels) 19 | }, 20 | } 21 | 22 | var cmdNodes = &cobra.Command{ 23 | Use: "nodes", 24 | Short: "Only print node restart counts", 25 | Long: "Only print node restart counts", 26 | Aliases: []string{"no", "node"}, 27 | Run: func(cmd *cobra.Command, args []string) { 28 | printNode = true 29 | printAll = false 30 | clientset := auth() 31 | collect(clientset, inamespace, ilabels) 32 | }, 33 | } 34 | 35 | var cmdPods = &cobra.Command{ 36 | Use: "pods", 37 | Short: "Only print pod restart counts", 38 | Long: "Only print pod restart counts", 39 | Aliases: []string{"po"}, 40 | Run: func(cmd *cobra.Command, args []string) { 41 | printPods = true 42 | printAll = false 43 | clientset := auth() 44 | collect(clientset, inamespace, ilabels) 45 | }, 46 | } 47 | 48 | var cmdLabels = &cobra.Command{ 49 | Use: "labels", 50 | Short: "Only print restart counts grouped by labels", 51 | Long: "Only print restart counts grouped by labels", 52 | Run: func(cmd *cobra.Command, args []string) { 53 | printLabel = true 54 | printAll = false 55 | clientset := auth() 56 | collect(clientset, inamespace, ilabels) 57 | }, 58 | } 59 | 60 | var cmdAll = &cobra.Command{ 61 | Use: "all", 62 | Short: "Print all groupings collected by kurt!", 63 | Long: "Print all groupings collected by kurt!", 64 | Run: func(cmd *cobra.Command, args []string) { 65 | printAll = true 66 | clientset := auth() 67 | collect(clientset, inamespace, ilabels) 68 | }, 69 | } 70 | 71 | var cmdVersion = &cobra.Command{ 72 | Use: "version", 73 | Short: "Print the current version and exit", 74 | Long: `Print the current version and exit`, 75 | Run: func(cmd *cobra.Command, args []string) { 76 | fmt.Printf("kurt: %s\n", version.Version()) 77 | }, 78 | } 79 | 80 | func init() { 81 | rootCmd.AddCommand(cmdNamespaces) 82 | rootCmd.AddCommand(cmdNodes) 83 | rootCmd.AddCommand(cmdPods) 84 | rootCmd.AddCommand(cmdLabels) 85 | rootCmd.AddCommand(cmdAll) 86 | rootCmd.AddCommand(cmdVersion) 87 | } 88 | -------------------------------------------------------------------------------- /cmd/collect.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/client-go/kubernetes" 9 | ) 10 | 11 | func collect(clientset *kubernetes.Clientset, namespace []string, labels []string) { 12 | 13 | if limitFlag < 0 { 14 | log.Fatal("FATAL CONFIGURATION: --limit flag value must not be negative.") 15 | } 16 | 17 | if !(output == "standard" || output == "yaml" || output == "json") { 18 | log.Fatal("FATAL CONFIGURATION: --output flag can only be: standard, json, yaml") 19 | } 20 | 21 | for _, ns := range namespace { 22 | for _, lb := range labels { 23 | pods, err := clientset.CoreV1().Pods(ns).List(context.TODO(), metav1.ListOptions{LabelSelector: lb}) 24 | if err != nil { 25 | log.Fatal(err.Error()) 26 | } 27 | 28 | for _, v := range pods.Items { 29 | initializeContainerMap(v.ObjectMeta.Name, v.ObjectMeta.Namespace) 30 | restarts := int32(0) 31 | for _, vv := range v.Status.ContainerStatuses { 32 | restarts += vv.RestartCount 33 | trackContainers(v.ObjectMeta.Name, v.ObjectMeta.Namespace, vv.Name, vv.RestartCount) 34 | } 35 | trackPods(v.ObjectMeta.Name, v.ObjectMeta.Namespace, restarts) 36 | trackNamespaces(v.ObjectMeta.Namespace, restarts) 37 | trackLabels(labels, v.ObjectMeta.Labels, restarts) 38 | trackNodes(v.Spec.NodeName, restarts) 39 | } 40 | } 41 | } 42 | showResults() 43 | 44 | } 45 | 46 | func trackNamespaces(namespace string, count int32) { 47 | namespaceTracker[namespace] += count 48 | } 49 | 50 | func trackNodes(node string, count int32) { 51 | nodeTracker[node] += count 52 | } 53 | 54 | func trackPods(pod, namespace string, count int32) { 55 | podTracker[namespace+":"+pod] = count 56 | } 57 | 58 | func trackContainers(pod, namespace, container string, count int32) { 59 | containerTracker[namespace+":"+pod][container] = count 60 | } 61 | 62 | func initializeContainerMap(pod, namespace string) { 63 | containerTracker[namespace+":"+pod] = make(map[string]int32) 64 | } 65 | 66 | // plabels = Pod Labels 67 | // tlabels = (User-defined) tracking labels 68 | func trackLabels(tlabels []string, plabels map[string]string, count int32) { 69 | // range through all the labels specified in the -l CLI flag 70 | for _, l := range tlabels { 71 | // range through plabels to see if any match the user specified labels. If so, add it to the map 72 | // the default value "*" will match everything 73 | for k, v := range plabels { 74 | if l == k || l == "" { 75 | labelTracker[k+":"+v] += count 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '44 7 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /cmd/printer_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func Test_returnSortedLimit(t *testing.T) { 9 | type args struct { 10 | data map[string]int32 11 | limit int 12 | parseNS bool 13 | containers map[string]map[string]int32 14 | } 15 | tests := []struct { 16 | name string 17 | args args 18 | want ItemList 19 | }{ 20 | { 21 | name: "test1", 22 | args: args{ 23 | data: map[string]int32{ 24 | "test1": 5, 25 | "test2": 7, 26 | "test3": 8, 27 | "test4": 9, 28 | "test5": 0, 29 | "test6": 4, 30 | }, 31 | limit: 5, 32 | parseNS: false, 33 | containers: nil, 34 | }, 35 | want: ItemList{ 36 | Item{ 37 | Name: "test4", 38 | Count: 9, 39 | Namespace: "", 40 | Containers: nil, 41 | }, 42 | Item{ 43 | Name: "test3", 44 | Count: 8, 45 | Namespace: "", 46 | Containers: nil, 47 | }, 48 | Item{ 49 | Name: "test2", 50 | Count: 7, 51 | Namespace: "", 52 | Containers: nil, 53 | }, 54 | Item{ 55 | Name: "test1", 56 | Count: 5, 57 | Namespace: "", 58 | Containers: nil, 59 | }, 60 | Item{ 61 | Name: "test6", 62 | Count: 4, 63 | Namespace: "", 64 | Containers: nil, 65 | }, 66 | }, 67 | }, 68 | { 69 | name: "test2", 70 | args: args{ 71 | data: map[string]int32{ 72 | "test1:pod1": 5, 73 | "test2:pod2": 7, 74 | "test2:pod3": 8, 75 | "test1:pod4": 9, 76 | "test2:pod5": 0, 77 | "test1:pod6": 4, 78 | }, 79 | limit: 5, 80 | parseNS: true, 81 | containers: map[string]map[string]int32{ 82 | "test1:pod1": {"container1": 5}, 83 | "test2:pod2": {"container1": 7}, 84 | "test2:pod3": {"container1": 8}, 85 | "test1:pod4": {"container3": 2, "container1": 4, "container2": 3}, 86 | "test2:pod5": {"container1": 0}, 87 | "test1:pod6": {"container1": 4}, 88 | }, 89 | }, 90 | want: ItemList{ 91 | Item{ 92 | Name: "pod4", 93 | Count: 9, 94 | Namespace: "test1", 95 | Containers: Containers{Container{"container1", 4}, Container{"container2", 3}, Container{"container3", 2}}, 96 | }, 97 | Item{ 98 | Name: "pod3", 99 | Count: 8, 100 | Namespace: "test2", 101 | Containers: Containers{Container{"container1", 8}}, 102 | }, 103 | Item{ 104 | Name: "pod2", 105 | Count: 7, 106 | Namespace: "test2", 107 | Containers: Containers{Container{"container1", 7}}, 108 | }, 109 | Item{ 110 | Name: "pod1", 111 | Count: 5, 112 | Namespace: "test1", 113 | Containers: Containers{Container{"container1", 5}}, 114 | }, 115 | Item{ 116 | Name: "pod6", 117 | Count: 4, 118 | Namespace: "test1", 119 | Containers: Containers{Container{"container1", 4}}, 120 | }, 121 | }, 122 | }, 123 | } 124 | 125 | for _, tt := range tests { 126 | t.Run(tt.name, func(t *testing.T) { 127 | if got := returnSortedLimit(tt.args.data, tt.args.limit, tt.args.parseNS, tt.args.containers); !reflect.DeepEqual(got, tt.want) { 128 | t.Errorf("returnSortedLimit() = %v, want %v", got, tt.want) 129 | } 130 | }) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kurt 2 | ``` 3 | kurt: KUbernetes Restart Tracker 4 | 5 | A restart tracker that gives context to what is restarting in your cluster 6 | 7 | Usage: 8 | kurt [command] 9 | 10 | Available Commands: 11 | all Print all groupings collected by kurt! 12 | completion generate the autocompletion script for the specified shell 13 | help Help about any command 14 | labels Only print restart counts grouped by labels 15 | namespaces Only print namespace-wide restart counts 16 | nodes Only print node restart counts 17 | pods Only print pod restart counts 18 | version Print the current version and exit 19 | 20 | Flags: 21 | -h, --help help for kurt 22 | -l, --label strings Specify multiple times for the label keys you want to see. 23 | For example: "kurt all -l app" 24 | -c, --limit int Limit the number of resources you want to see. Set limit to 0 for no limits. Must be positive. 25 | For example: "kurt all -c=10" (default 5) 26 | -n, --namespace strings Specify namespace for kurt to collect restart metrics. 27 | Leave blank to collect in all namespaces. 28 | -o, --output string Specify output type. Options are: json, yaml, standard 29 | For example: "kurt all -o json" (default "standard") 30 | 31 | Use "kurt [command] --help" for more information about a command. 32 | ``` 33 | 34 | # Install 35 | Head over to our [releases page](https://github.com/soraro/kurt/releases/latest) or run as a `kubectl` plugin with [krew](https://krew.sigs.k8s.io/) 36 | ``` 37 | kubectl krew install kurt 38 | ``` 39 | 40 | Easily install krew and kurt with the following: 41 | ``` 42 | curl https://krew.sh/kurt | bash 43 | ``` 44 | 45 | # Examples 46 | Show the top 5 highest restart counts grouped by `Namespace`, `Node`, `Label`, and `Pod`: 47 | ``` 48 | $ kurt all 49 | 50 | kurt: KUbernetes Restart Tracker 51 | 52 | ========== 53 | 54 | Namespace Restarts 55 | 56 | default 2 57 | test 1 58 | kube-system 0 59 | 60 | ========== 61 | 62 | Node Restarts 63 | 64 | minikube-m02 2 65 | minikube-m03 1 66 | minikube 0 67 | 68 | ========== 69 | 70 | Label Restarts 71 | 72 | run:nginx 3 73 | component:etcd 0 74 | k8s-app:kube-proxy 0 75 | addonmanager.kubernetes.io/mode:Reconcile 0 76 | integration-test:storage-provisioner 0 77 | 78 | ========== 79 | 80 | Pod Namespace Restarts 81 | 82 | nginx default 2 83 | nginx test 1 84 | kube-apiserver-minikube kube-system 0 85 | storage-provisioner kube-system 0 86 | etcd-minikube kube-system 0 87 | ``` 88 | 89 | Show more results: 90 | ``` 91 | kurt all -c 10 92 | 93 | # use -c 0 if you want to show all results 94 | ``` 95 | 96 | Show which node has the most restarted pods: 97 | ``` 98 | kurt no 99 | ``` 100 | 101 | Show top 20 pod restart counts in the `default` namespace which also have the `app` label key: 102 | ``` 103 | kurt po -n default -l app -c 20 104 | ``` 105 | 106 | Get help: 107 | ``` 108 | kurt -h 109 | ``` 110 | 111 | Structured output: 112 | ``` 113 | # With structured output you could use a script like this to delete the top rebooting pod 114 | 115 | JSON=$(kurt pods -o json) 116 | POD=$(echo $JSON | jq -r .pods[0].name) 117 | NS=$(echo $JSON | jq -r .pods[0].namespace) 118 | kubectl delete pod $POD -n $NS 119 | ``` 120 | 121 | # Permissions 122 | As seen in the [`cmd/collect.go` file](https://github.com/soraro/kurt/blob/main/cmd/collect.go) the only permission required for kurt is `pods/list`. 123 | 124 | # Requirements 125 | Go Version 1.21 126 | 127 | # Building 128 | ``` 129 | go build . 130 | ``` 131 | Outputs a `kurt` binary 132 | 133 | # Testing 134 | ``` 135 | go test ./cmd -v 136 | ``` 137 | -------------------------------------------------------------------------------- /cmd/printer.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "sort" 8 | "strings" 9 | "text/tabwriter" 10 | 11 | "gopkg.in/yaml.v2" 12 | ) 13 | 14 | type StructuredOutput struct { 15 | Namespaces ItemList `yaml:"namespaces,omitempty" json:"namespaces,omitempty"` 16 | Nodes ItemList `yaml:"nodes,omitempty" json:"nodes,omitempty"` 17 | Labels ItemList `yaml:"labels,omitempty" json:"labels,omitempty"` 18 | Pods ItemList `yaml:"pods,omitempty" json:"pods,omitempty"` 19 | } 20 | 21 | func showResults() { 22 | var so StructuredOutput 23 | w := new(tabwriter.Writer) 24 | // minwidth, tabwidth, padding, padchar, flags 25 | w.Init(os.Stdout, 8, 8, 1, '\t', 0) 26 | 27 | if output == "standard" { 28 | fmt.Printf("kurt: KUbernetes Restart Tracker") 29 | } 30 | 31 | if printNS || printAll { 32 | so.Namespaces = returnSortedLimit(namespaceTracker, limitFlag, false, nil) 33 | if output == "standard" { 34 | fmt.Println("\n\n==========") 35 | fmt.Fprintf(w, "\n Namespace\tRestarts\t") 36 | fmt.Fprintf(w, "\n \t\t") 37 | for _, v := range so.Namespaces { 38 | fmt.Fprintf(w, "\n %v\t%v\t", v.Name, v.Count) 39 | } 40 | w.Flush() 41 | } 42 | } 43 | 44 | if printNode || printAll { 45 | so.Nodes = returnSortedLimit(nodeTracker, limitFlag, false, nil) 46 | if output == "standard" { 47 | fmt.Println("\n\n==========") 48 | fmt.Fprintf(w, "\n Node\tRestarts\t") 49 | fmt.Fprintf(w, "\n \t\t") 50 | for _, v := range so.Nodes { 51 | fmt.Fprintf(w, "\n %v\t%v\t", v.Name, v.Count) 52 | } 53 | w.Flush() 54 | } 55 | } 56 | 57 | if printLabel || printAll { 58 | if len(labelTracker) > 0 { 59 | so.Labels = returnSortedLimit(labelTracker, limitFlag, false, nil) 60 | if output == "standard" { 61 | fmt.Println("\n\n==========") 62 | fmt.Fprintf(w, "\n Label\tRestarts\t") 63 | fmt.Fprintf(w, "\n \t\t") 64 | for _, v := range so.Labels { 65 | fmt.Fprintf(w, "\n %v\t%v\t", v.Name, v.Count) 66 | } 67 | w.Flush() 68 | } 69 | } 70 | } 71 | 72 | if printPods || printAll { 73 | so.Pods = returnSortedLimit(podTracker, limitFlag, true, containerTracker) 74 | if output == "standard" { 75 | fmt.Println("\n\n==========") 76 | fmt.Fprintf(w, "\n Pod\tNamespace\tRestarts\t") 77 | fmt.Fprintf(w, "\n \t\t\t") 78 | for _, v := range so.Pods { 79 | fmt.Fprintf(w, "\n %v\t%v\t%v\t", v.Name, v.Namespace, v.Count) 80 | if v.Containers != nil && ishowContainers { 81 | for _, vv := range v.Containers { 82 | fmt.Fprintf(w, "\n └%v: %v\t\t\t", vv.Name, vv.Count) 83 | } 84 | } 85 | } 86 | w.Flush() 87 | } 88 | } 89 | switch output { 90 | case "json": 91 | j, _ := json.MarshalIndent(so, "", " ") 92 | fmt.Println(string(j)) 93 | case "yaml": 94 | y, _ := yaml.Marshal(so) 95 | fmt.Println(string(y)) 96 | default: 97 | fmt.Printf("\n") 98 | } 99 | 100 | } 101 | 102 | // sorting results 103 | // https://stackoverflow.com/a/18695740 104 | type Container struct { 105 | Name string `yaml:"name" json:"name"` 106 | Count int32 `yaml:"count" json:"count"` 107 | } 108 | 109 | type Containers []Container 110 | 111 | func (p Containers) Len() int { return len(p) } 112 | func (p Containers) Less(i, j int) bool { return p[i].Count < p[j].Count } 113 | func (p Containers) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 114 | 115 | type Item struct { 116 | Name string `yaml:"name" json:"name"` 117 | Count int32 `yaml:"count" json:"count"` 118 | Namespace string `yaml:"namespace,omitempty" json:"namespace,omitempty"` 119 | Containers Containers `yaml:"containers,omitempty" json:"containers,omitempty"` 120 | } 121 | 122 | type ItemList []Item 123 | 124 | func (p ItemList) Len() int { return len(p) } 125 | func (p ItemList) Less(i, j int) bool { return p[i].Count < p[j].Count } 126 | func (p ItemList) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 127 | 128 | func returnSortedLimit(data map[string]int32, limit int, parseNS bool, containers map[string]map[string]int32) ItemList { 129 | il := make(ItemList, len(data)) 130 | i := 0 131 | for k, v := range data { 132 | if parseNS { 133 | // split the Name so we can display the pod an namespace separately 134 | // only used for pod items 135 | s := strings.Split(k, ":") 136 | il[i] = Item{s[1], v, s[0], createContainerSlice(containers[k])} 137 | } else { 138 | il[i] = Item{k, v, "", nil} 139 | } 140 | i++ 141 | } 142 | sort.Sort(sort.Reverse(il)) 143 | if len(il) <= limit || limit == 0 { 144 | return il 145 | } else { 146 | return il[0:limit] 147 | } 148 | } 149 | 150 | func createContainerSlice(containers map[string]int32) []Container { 151 | if containers != nil { 152 | c := make(Containers, len(containers)) 153 | i := 0 154 | for k, v := range containers { 155 | c[i] = Container{k, v} 156 | i++ 157 | } 158 | sort.Sort(sort.Reverse(c)) 159 | return c 160 | } 161 | return nil 162 | } 163 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | [@kwsorensen](https://github.com/kwsorensen) or [@aro5000](https://github.com/aro5000). 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 5 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= 7 | github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 8 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 9 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 10 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 11 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 12 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 13 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 14 | github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= 15 | github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 16 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 17 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 18 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 19 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 20 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 21 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 22 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 23 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 24 | github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= 25 | github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= 26 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 27 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 28 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 29 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 30 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 31 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 32 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 33 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 34 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 35 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 36 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 37 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 38 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 39 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 40 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 41 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 42 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 43 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 44 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 45 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 46 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 47 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 48 | github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= 49 | github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= 50 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 51 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 52 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 53 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 54 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 55 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 56 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 57 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= 58 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 59 | github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= 60 | github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 61 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 62 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 63 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 64 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 65 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 66 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 67 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 68 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 69 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 70 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 71 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 72 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 73 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 74 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 75 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 76 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 77 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 78 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 79 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 80 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 81 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 82 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 83 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 84 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 85 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 86 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 87 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 88 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 89 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 90 | golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= 91 | golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= 92 | golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= 93 | golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 94 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 95 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 96 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 97 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 98 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 99 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 100 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 101 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 102 | golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= 103 | golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 104 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 105 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 106 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 107 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 108 | golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= 109 | golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 110 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 111 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 112 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 113 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 114 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 115 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 116 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 117 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 118 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 119 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 120 | google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= 121 | google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 122 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 123 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 124 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 125 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 126 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 127 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 128 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 129 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 130 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 131 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 132 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 133 | k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc= 134 | k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k= 135 | k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs= 136 | k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= 137 | k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU= 138 | k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg= 139 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 140 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 141 | k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 h1:hcha5B1kVACrLujCKLbr8XWMxCxzQx42DY8QKYJrDLg= 142 | k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7/go.mod h1:GewRfANuJ70iYzvn+i4lezLDAFzvjxZYK1gn1lWcfas= 143 | k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= 144 | k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 145 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= 146 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 147 | sigs.k8s.io/structured-merge-diff/v4 v4.5.0 h1:nbCitCK2hfnhyiKo6uf2HxUPTCodY6Qaf85SbDIaMBk= 148 | sigs.k8s.io/structured-merge-diff/v4 v4.5.0/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= 149 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 150 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 151 | --------------------------------------------------------------------------------