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