├── main.go ├── .gitignore ├── .github ├── workflows │ ├── go-dependency-submission.yml │ ├── stale.yml │ └── go.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── pkg ├── options │ └── options.go ├── utils │ ├── networking.go │ ├── autoscaling.go │ ├── batch.go │ ├── storage.go │ ├── apps.go │ ├── rbac.go │ ├── common.go │ └── core.go ├── client │ └── client.go ├── resources │ ├── clusterroles.go │ ├── clusterrolebindings.go │ ├── roles.go │ ├── secrets.go │ ├── configmaps.go │ ├── rolebindings.go │ ├── serviceaccounts.go │ ├── cronjobs.go │ ├── jobs.go │ ├── statefulsets.go │ ├── pods.go │ ├── ingresses.go │ ├── replicasets.go │ ├── deployments.go │ ├── nodes.go │ ├── services.go │ ├── hpas.go │ ├── daemonsets.go │ └── storages.go └── constants │ └── constants.go ├── cmd ├── version.go ├── root.go └── resources.go ├── RESOURCE_TYPES.md ├── .krew.yaml ├── go.mod ├── README.md ├── Makefile ├── CHANGELOG.md ├── LICENSE └── go.sum /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/guessi/kubectl-grep/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Ignore vendor files 15 | vendor/* 16 | 17 | # Ignore binary releases 18 | releases/* 19 | 20 | # Ignore garbage 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /.github/workflows/go-dependency-submission.yml: -------------------------------------------------------------------------------- 1 | name: Go Dependency Submission 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | go-action-detection: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v6 15 | - uses: actions/setup-go@v6 16 | with: 17 | go-version-file: 'go.mod' 18 | 19 | - uses: actions/go-dependency-submission@v2 20 | with: 21 | go-mod-path: go.mod 22 | -------------------------------------------------------------------------------- /pkg/options/options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import "time" 4 | 5 | // TODO: integrate with "k8s.io/cli-runtime/pkg/genericclioptions" 6 | 7 | type SearchOptions struct { 8 | AllNamespaces bool 9 | Namespace string 10 | Selector string 11 | FieldSelector string 12 | InvertMatch bool 13 | ExcludePattern string 14 | Timeout time.Duration 15 | } 16 | 17 | // NewSearchOptions - genericclioptions wrapper for searchOptions 18 | func NewSearchOptions() *SearchOptions { 19 | return &SearchOptions{} 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. 2 | # 3 | # You can adjust the behavior by modifying this file. 4 | # For more information, see: 5 | # https://github.com/actions/stale 6 | name: Mark stale issues and pull requests 7 | 8 | on: 9 | schedule: 10 | - cron: '30 2 * * 0' 11 | 12 | jobs: 13 | stale: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | issues: write 17 | pull-requests: write 18 | 19 | steps: 20 | - uses: actions/stale@v10 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature] Description of Feature Request" 5 | labels: '' 6 | assignees: '' 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 | -------------------------------------------------------------------------------- /pkg/utils/networking.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | log "github.com/sirupsen/logrus" 8 | networkingv1 "k8s.io/api/networking/v1" 9 | 10 | "github.com/guessi/kubectl-grep/pkg/client" 11 | "github.com/guessi/kubectl-grep/pkg/options" 12 | ) 13 | 14 | // IngressList - return a list of Ingress(es) 15 | func IngressList(ctx context.Context, opt *options.SearchOptions) (*networkingv1.IngressList, error) { 16 | clientset := client.InitClient() 17 | ns, o := setOptions(opt) 18 | list, err := clientset.NetworkingV1().Ingresses(ns).List(ctx, *o) 19 | if err != nil { 20 | log.WithFields(log.Fields{ 21 | "err": err.Error(), 22 | }) 23 | return nil, fmt.Errorf("failed to list Ingresses: %w", err) 24 | } 25 | return list, nil 26 | } 27 | -------------------------------------------------------------------------------- /pkg/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "k8s.io/client-go/kubernetes" 8 | "k8s.io/client-go/tools/clientcmd" 9 | 10 | // utilities for kubernetes integration 11 | _ "k8s.io/client-go/plugin/pkg/client/auth" 12 | ) 13 | 14 | // ClientConfig 15 | func ClientConfig() clientcmd.ClientConfig { 16 | return clientcmd.NewNonInteractiveDeferredLoadingClientConfig( 17 | clientcmd.NewDefaultClientConfigLoadingRules(), 18 | &clientcmd.ConfigOverrides{}) 19 | } 20 | 21 | // InitClient - Kubernetes Client 22 | func InitClient() *kubernetes.Clientset { 23 | clientConfig := ClientConfig() 24 | config, err := clientConfig.ClientConfig() 25 | if err != nil { 26 | fmt.Fprintln(os.Stderr, err.Error()) 27 | os.Exit(1) 28 | } 29 | 30 | return kubernetes.NewForConfigOrDie(config) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/utils/autoscaling.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | log "github.com/sirupsen/logrus" 8 | autoscalingv2 "k8s.io/api/autoscaling/v2" 9 | 10 | "github.com/guessi/kubectl-grep/pkg/client" 11 | "github.com/guessi/kubectl-grep/pkg/options" 12 | ) 13 | 14 | // HpaList - return a list of HorizontalPodAutoscaler(s) 15 | func HpaList(ctx context.Context, opt *options.SearchOptions) (*autoscalingv2.HorizontalPodAutoscalerList, error) { 16 | clientset := client.InitClient() 17 | ns, o := setOptions(opt) 18 | list, err := clientset.AutoscalingV2().HorizontalPodAutoscalers(ns).List(ctx, *o) 19 | if err != nil { 20 | log.WithFields(log.Fields{ 21 | "err": err.Error(), 22 | }) 23 | return nil, fmt.Errorf("failed to list HorizontalPodAutoscalers: %w", err) 24 | } 25 | return list, nil 26 | } 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Bug] A Clear Bug Description" 5 | labels: '' 6 | assignees: '' 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. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment (please complete the following information):** 27 | - Operation System: [e.g. Ubuntu, CentOS, macOS] 28 | - Kubernetes Version [e.g. v1.10.5, v1.11.7, v1.13.4] 29 | - Kubectl Version [e.g. v1.10.5, v1.11.7, v1.13.4] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var ( 11 | gitVersion = "v0.0.0" 12 | goVersion = "v0.0.0" 13 | buildTime = "undefined" 14 | shortVersion bool 15 | ) 16 | 17 | var versionCmd = &cobra.Command{ 18 | Use: "version", 19 | Short: "Print version number", 20 | Run: func(cmd *cobra.Command, args []string) { 21 | showVersion() 22 | }, 23 | } 24 | 25 | func init() { 26 | rootCmd.AddCommand(versionCmd) 27 | versionCmd.Flags().BoolVarP(&shortVersion, "short", "s", false, "short version output") 28 | } 29 | 30 | func showVersion() { 31 | r, _ := regexp.Compile(`v[0-9]\.[0-9]+\.[0-9]+`) 32 | versionInfo := r.FindString(gitVersion) 33 | if shortVersion { 34 | fmt.Println(versionInfo) 35 | } else { 36 | fmt.Println("kubectl-grep", versionInfo) 37 | fmt.Println(" Git Commit:", gitVersion) 38 | fmt.Println(" Build with:", goVersion) 39 | fmt.Println(" Build time:", buildTime) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /RESOURCE_TYPES.md: -------------------------------------------------------------------------------- 1 | # Supported Resource Types (Sort alphabetically) 2 | 3 | ### apps/v1 4 | 5 | - [X] DaemonSet 6 | - [X] Deployment 7 | - [X] ReplicaSet 8 | - [X] StatefulSet 9 | 10 | ### autoscaling/v2 11 | 12 | - [X] HPA 13 | 14 | ### batch/v1 15 | 16 | - [X] CronJob 17 | - [X] Job 18 | 19 | ### discovery.k8s.io/v1 20 | 21 | - [ ] EndpointSlice 22 | 23 | ### networking.k8s.io/v1 24 | 25 | - [X] Ingress 26 | - [ ] IngressClass 27 | - [ ] NetworkPolicy 28 | 29 | ### policy/v1 30 | 31 | - [ ] PodDisruptionBudget 32 | 33 | ### rbac.authorization.k8s.io/v1 34 | 35 | - [X] ClusterRole 36 | - [X] ClusterRoleBinding 37 | - [X] Role 38 | - [X] RoleBinding 39 | 40 | ### scheduling.k8s.io/v1 41 | 42 | - [ ] PriorityClass 43 | 44 | ### storage.k8s.io/v1 45 | 46 | - [X] CSIDriver 47 | - [X] StorageClass 48 | 49 | ### v1 50 | 51 | - [X] ConfigMap 52 | - [ ] Endpoints 53 | - [ ] LimitRange 54 | - [ ] Namespace 55 | - [X] Node 56 | - [ ] PersistentVolume 57 | - [ ] PersistentVolumeClaim 58 | - [X] Pod 59 | - [ ] ReplicationController 60 | - [ ] ResourceQuota 61 | - [X] Secret 62 | - [X] Service 63 | - [X] ServiceAccount 64 | -------------------------------------------------------------------------------- /pkg/utils/batch.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | log "github.com/sirupsen/logrus" 8 | batchv1 "k8s.io/api/batch/v1" 9 | 10 | "github.com/guessi/kubectl-grep/pkg/client" 11 | "github.com/guessi/kubectl-grep/pkg/options" 12 | ) 13 | 14 | // CronJobList - return a list of CronJob(s) 15 | func CronJobList(ctx context.Context, opt *options.SearchOptions) (*batchv1.CronJobList, error) { 16 | clientset := client.InitClient() 17 | ns, o := setOptions(opt) 18 | list, err := clientset.BatchV1().CronJobs(ns).List(ctx, *o) 19 | if err != nil { 20 | log.WithFields(log.Fields{ 21 | "err": err.Error(), 22 | }) 23 | return nil, fmt.Errorf("failed to list CronJobs: %w", err) 24 | } 25 | return list, nil 26 | } 27 | 28 | // JobList - return a list of Job(s) 29 | func JobList(ctx context.Context, opt *options.SearchOptions) (*batchv1.JobList, error) { 30 | clientset := client.InitClient() 31 | ns, o := setOptions(opt) 32 | list, err := clientset.BatchV1().Jobs(ns).List(ctx, *o) 33 | if err != nil { 34 | log.WithFields(log.Fields{ 35 | "err": err.Error(), 36 | }) 37 | return nil, fmt.Errorf("failed to list Jobs: %w", err) 38 | } 39 | return list, nil 40 | } 41 | -------------------------------------------------------------------------------- /pkg/utils/storage.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | log "github.com/sirupsen/logrus" 8 | storagev1 "k8s.io/api/storage/v1" 9 | 10 | "github.com/guessi/kubectl-grep/pkg/client" 11 | "github.com/guessi/kubectl-grep/pkg/options" 12 | ) 13 | 14 | // CsiDriverList - return a list of CSIDriver(s) 15 | func CsiDriverList(ctx context.Context, opt *options.SearchOptions) (*storagev1.CSIDriverList, error) { 16 | clientset := client.InitClient() 17 | _, o := setOptions(opt) 18 | list, err := clientset.StorageV1().CSIDrivers().List(ctx, *o) 19 | if err != nil { 20 | log.WithFields(log.Fields{ 21 | "err": err.Error(), 22 | }) 23 | return nil, fmt.Errorf("failed to list CSIDrivers: %w", err) 24 | } 25 | return list, nil 26 | } 27 | 28 | // StorageClassList - return a list of StorageClass(es) 29 | func StorageClassList(ctx context.Context, opt *options.SearchOptions) (*storagev1.StorageClassList, error) { 30 | clientset := client.InitClient() 31 | _, o := setOptions(opt) 32 | list, err := clientset.StorageV1().StorageClasses().List(ctx, *o) 33 | if err != nil { 34 | log.WithFields(log.Fields{ 35 | "err": err.Error(), 36 | }) 37 | return nil, fmt.Errorf("failed to list StorageClasses: %w", err) 38 | } 39 | return list, nil 40 | } 41 | -------------------------------------------------------------------------------- /pkg/resources/clusterroles.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "text/tabwriter" 8 | "time" 9 | 10 | "github.com/guessi/kubectl-grep/pkg/constants" 11 | "github.com/guessi/kubectl-grep/pkg/options" 12 | "github.com/guessi/kubectl-grep/pkg/utils" 13 | ) 14 | 15 | // ClusterRoles - a public function for searching clusterroles with keyword 16 | func ClusterRoles(ctx context.Context, opt *options.SearchOptions, keyword string) error { 17 | var clusterRoleInfo string 18 | 19 | clusterRoleList, err := utils.ClusterRoleList(ctx, opt) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | if len(clusterRoleList.Items) <= 0 { 25 | fmt.Printf("No resources found.\n") 26 | return nil 27 | } 28 | 29 | buf := bytes.NewBuffer(nil) 30 | w := tabwriter.NewWriter(buf, 0, 0, 3, ' ', 0) 31 | 32 | fmt.Fprintln(w, constants.ClusterRolesHeader) 33 | 34 | for _, c := range clusterRoleList.Items { 35 | if !utils.MatchesKeyword(c.Name, keyword, opt.InvertMatch) { 36 | continue 37 | } 38 | 39 | if utils.ShouldExcludeResource(c.Name, opt.ExcludePattern) { 40 | continue 41 | } 42 | 43 | createdAt := c.CreationTimestamp.Time 44 | 45 | clusterRoleInfo = fmt.Sprintf(constants.ClusterRolesRowTemplate, 46 | c.Name, 47 | createdAt.UTC().Format(time.RFC3339), 48 | ) 49 | 50 | fmt.Fprintln(w, clusterRoleInfo) 51 | } 52 | w.Flush() 53 | 54 | fmt.Printf("%s", buf.String()) 55 | 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /pkg/resources/clusterrolebindings.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "text/tabwriter" 8 | "time" 9 | 10 | "github.com/guessi/kubectl-grep/pkg/constants" 11 | "github.com/guessi/kubectl-grep/pkg/options" 12 | "github.com/guessi/kubectl-grep/pkg/utils" 13 | ) 14 | 15 | // ClusterRoleBindings - a public function for searching clusterrolebindings with keyword 16 | func ClusterRoleBindings(ctx context.Context, opt *options.SearchOptions, keyword string) error { 17 | var clusterRoleBindingInfo string 18 | 19 | clusterRoleBindingList, err := utils.ClusterRoleBindingList(ctx, opt) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | if len(clusterRoleBindingList.Items) <= 0 { 25 | fmt.Printf("No resources found.\n") 26 | return nil 27 | } 28 | 29 | buf := bytes.NewBuffer(nil) 30 | w := tabwriter.NewWriter(buf, 0, 0, 3, ' ', 0) 31 | 32 | fmt.Fprintln(w, constants.ClusterRoleBindingsHeader) 33 | 34 | for _, c := range clusterRoleBindingList.Items { 35 | if !utils.MatchesKeyword(c.Name, keyword, opt.InvertMatch) { 36 | continue 37 | } 38 | 39 | if utils.ShouldExcludeResource(c.Name, opt.ExcludePattern) { 40 | continue 41 | } 42 | 43 | age := utils.GetAge(time.Since(c.CreationTimestamp.Time)) 44 | 45 | clusterRoleBindingInfo = fmt.Sprintf(constants.ClusterRoleBindingsRowTemplate, 46 | c.Name, 47 | "ClusterRole/"+c.RoleRef.Name, 48 | age, 49 | ) 50 | 51 | fmt.Fprintln(w, clusterRoleBindingInfo) 52 | } 53 | w.Flush() 54 | 55 | fmt.Printf("%s", buf.String()) 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /pkg/resources/roles.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "text/tabwriter" 8 | "time" 9 | 10 | "github.com/guessi/kubectl-grep/pkg/constants" 11 | "github.com/guessi/kubectl-grep/pkg/options" 12 | "github.com/guessi/kubectl-grep/pkg/utils" 13 | ) 14 | 15 | // Roles - a public function for searching roles with keyword 16 | func Roles(ctx context.Context, opt *options.SearchOptions, keyword string) error { 17 | var roleInfo string 18 | 19 | roleList, err := utils.RoleList(ctx, opt) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | if len(roleList.Items) == 0 { 25 | ns := opt.Namespace 26 | if opt.AllNamespaces { 27 | fmt.Println("No resources found.") 28 | } else { 29 | if ns == "" { 30 | ns = "default" 31 | } 32 | fmt.Printf("No resources found in %s namespace.\n", ns) 33 | } 34 | return nil 35 | } 36 | 37 | buf := bytes.NewBuffer(nil) 38 | w := tabwriter.NewWriter(buf, 0, 0, 3, ' ', 0) 39 | 40 | fmt.Fprintln(w, constants.RolesHeader) 41 | 42 | for _, r := range roleList.Items { 43 | if !utils.MatchesKeyword(r.Name, keyword, opt.InvertMatch) { 44 | continue 45 | } 46 | 47 | if utils.ShouldExcludeResource(r.Name, opt.ExcludePattern) { 48 | continue 49 | } 50 | 51 | createdAt := r.CreationTimestamp.Time 52 | 53 | roleInfo = fmt.Sprintf(constants.RolesRowTemplate, 54 | r.Namespace, 55 | r.Name, 56 | createdAt.UTC().Format(time.RFC3339), 57 | ) 58 | 59 | fmt.Fprintln(w, roleInfo) 60 | } 61 | w.Flush() 62 | 63 | fmt.Printf("%s", buf.String()) 64 | 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /pkg/resources/secrets.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "text/tabwriter" 8 | "time" 9 | 10 | "github.com/guessi/kubectl-grep/pkg/constants" 11 | "github.com/guessi/kubectl-grep/pkg/options" 12 | "github.com/guessi/kubectl-grep/pkg/utils" 13 | ) 14 | 15 | // Secrets - a public function for searching secrets with keyword 16 | func Secrets(ctx context.Context, opt *options.SearchOptions, keyword string) error { 17 | var secretInfo string 18 | 19 | secretList, err := utils.SecretList(ctx, opt) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | if len(secretList.Items) == 0 { 25 | ns := opt.Namespace 26 | if opt.AllNamespaces { 27 | fmt.Println("No resources found.") 28 | } else { 29 | if ns == "" { 30 | ns = "default" 31 | } 32 | fmt.Printf("No resources found in %s namespace.\n", ns) 33 | } 34 | return nil 35 | } 36 | 37 | buf := bytes.NewBuffer(nil) 38 | w := tabwriter.NewWriter(buf, 0, 0, 3, ' ', 0) 39 | 40 | fmt.Fprintln(w, constants.SecretHeader) 41 | 42 | for _, s := range secretList.Items { 43 | if !utils.MatchesKeyword(s.Name, keyword, opt.InvertMatch) { 44 | continue 45 | } 46 | 47 | if utils.ShouldExcludeResource(s.Name, opt.ExcludePattern) { 48 | continue 49 | } 50 | 51 | age := utils.GetAge(time.Since(s.CreationTimestamp.Time)) 52 | 53 | secretInfo = fmt.Sprintf(constants.SecretRowTemplate, 54 | s.Namespace, 55 | s.Name, 56 | s.Type, 57 | len(s.Data), 58 | age, 59 | ) 60 | 61 | fmt.Fprintln(w, secretInfo) 62 | } 63 | w.Flush() 64 | 65 | fmt.Printf("%s", buf.String()) 66 | 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /pkg/resources/configmaps.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "text/tabwriter" 8 | "time" 9 | 10 | "github.com/guessi/kubectl-grep/pkg/constants" 11 | "github.com/guessi/kubectl-grep/pkg/options" 12 | "github.com/guessi/kubectl-grep/pkg/utils" 13 | ) 14 | 15 | // ConfigMaps - a public function for searching configmaps with keyword 16 | func ConfigMaps(ctx context.Context, opt *options.SearchOptions, keyword string) error { 17 | var configMapInfo string 18 | 19 | configMapList, err := utils.ConfigMapList(ctx, opt) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | if len(configMapList.Items) == 0 { 25 | ns := opt.Namespace 26 | if opt.AllNamespaces { 27 | fmt.Println("No resources found.") 28 | } else { 29 | if ns == "" { 30 | ns = "default" 31 | } 32 | fmt.Printf("No resources found in %s namespace.\n", ns) 33 | } 34 | return nil 35 | } 36 | 37 | buf := bytes.NewBuffer(nil) 38 | w := tabwriter.NewWriter(buf, 0, 0, 3, ' ', 0) 39 | 40 | fmt.Fprintln(w, constants.ConfigMapHeader) 41 | 42 | for _, cm := range configMapList.Items { 43 | if !utils.MatchesKeyword(cm.Name, keyword, opt.InvertMatch) { 44 | continue 45 | } 46 | 47 | if utils.ShouldExcludeResource(cm.Name, opt.ExcludePattern) { 48 | continue 49 | } 50 | 51 | age := utils.GetAge(time.Since(cm.CreationTimestamp.Time)) 52 | 53 | configMapInfo = fmt.Sprintf(constants.ConfigMapRowTemplate, 54 | cm.Namespace, 55 | cm.Name, 56 | len(cm.Data), 57 | age, 58 | ) 59 | 60 | fmt.Fprintln(w, configMapInfo) 61 | } 62 | w.Flush() 63 | 64 | fmt.Printf("%s", buf.String()) 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /pkg/resources/rolebindings.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "text/tabwriter" 8 | "time" 9 | 10 | "github.com/guessi/kubectl-grep/pkg/constants" 11 | "github.com/guessi/kubectl-grep/pkg/options" 12 | "github.com/guessi/kubectl-grep/pkg/utils" 13 | ) 14 | 15 | // RoleBindings - a public function for searching rolebindings with keyword 16 | func RoleBindings(ctx context.Context, opt *options.SearchOptions, keyword string) error { 17 | var roleBindingInfo string 18 | 19 | roleBindingList, err := utils.RoleBindingList(ctx, opt) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | if len(roleBindingList.Items) == 0 { 25 | ns := opt.Namespace 26 | if opt.AllNamespaces { 27 | fmt.Println("No resources found.") 28 | } else { 29 | if ns == "" { 30 | ns = "default" 31 | } 32 | fmt.Printf("No resources found in %s namespace.\n", ns) 33 | } 34 | return nil 35 | } 36 | 37 | buf := bytes.NewBuffer(nil) 38 | w := tabwriter.NewWriter(buf, 0, 0, 3, ' ', 0) 39 | 40 | fmt.Fprintln(w, constants.RoleBindingsHeader) 41 | 42 | for _, r := range roleBindingList.Items { 43 | if !utils.MatchesKeyword(r.Name, keyword, opt.InvertMatch) { 44 | continue 45 | } 46 | 47 | if utils.ShouldExcludeResource(r.Name, opt.ExcludePattern) { 48 | continue 49 | } 50 | 51 | age := utils.GetAge(time.Since(r.CreationTimestamp.Time)) 52 | 53 | roleBindingInfo = fmt.Sprintf(constants.RoleBindingsRowTemplate, 54 | r.Namespace, 55 | r.Name, 56 | "Role/"+r.RoleRef.Name, 57 | age, 58 | ) 59 | 60 | fmt.Fprintln(w, roleBindingInfo) 61 | } 62 | w.Flush() 63 | 64 | fmt.Printf("%s", buf.String()) 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /pkg/resources/serviceaccounts.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "text/tabwriter" 8 | "time" 9 | 10 | "github.com/guessi/kubectl-grep/pkg/constants" 11 | "github.com/guessi/kubectl-grep/pkg/options" 12 | "github.com/guessi/kubectl-grep/pkg/utils" 13 | ) 14 | 15 | // ServiceAccounts - a public function for searching serviceaccounts with keyword 16 | func ServiceAccounts(ctx context.Context, opt *options.SearchOptions, keyword string) error { 17 | var serviceAccountInfo string 18 | 19 | serviceAccountList, err := utils.ServiceAccountList(ctx, opt) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | if len(serviceAccountList.Items) == 0 { 25 | ns := opt.Namespace 26 | if opt.AllNamespaces { 27 | fmt.Println("No resources found.") 28 | } else { 29 | if ns == "" { 30 | ns = "default" 31 | } 32 | fmt.Printf("No resources found in %s namespace.\n", ns) 33 | } 34 | return nil 35 | } 36 | 37 | buf := bytes.NewBuffer(nil) 38 | w := tabwriter.NewWriter(buf, 0, 0, 3, ' ', 0) 39 | 40 | fmt.Fprintln(w, constants.ServiceAccountsHeader) 41 | 42 | for _, s := range serviceAccountList.Items { 43 | if !utils.MatchesKeyword(s.Name, keyword, opt.InvertMatch) { 44 | continue 45 | } 46 | 47 | if utils.ShouldExcludeResource(s.Name, opt.ExcludePattern) { 48 | continue 49 | } 50 | 51 | age := utils.GetAge(time.Since(s.CreationTimestamp.Time)) 52 | 53 | serviceAccountInfo = fmt.Sprintf(constants.ServiceAccountsRowTemplate, 54 | s.Namespace, 55 | s.Name, 56 | len(s.Secrets), 57 | age, 58 | ) 59 | 60 | fmt.Fprintln(w, serviceAccountInfo) 61 | } 62 | w.Flush() 63 | 64 | fmt.Printf("%s", buf.String()) 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /pkg/resources/cronjobs.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "text/tabwriter" 8 | "time" 9 | 10 | "github.com/guessi/kubectl-grep/pkg/constants" 11 | "github.com/guessi/kubectl-grep/pkg/options" 12 | "github.com/guessi/kubectl-grep/pkg/utils" 13 | ) 14 | 15 | // CronJobs - a public function for searching cronjobs with keyword 16 | func CronJobs(ctx context.Context, opt *options.SearchOptions, keyword string) error { 17 | var cronjobInfo string 18 | 19 | cronjobList, err := utils.CronJobList(ctx, opt) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | if len(cronjobList.Items) == 0 { 25 | ns := opt.Namespace 26 | if opt.AllNamespaces { 27 | fmt.Println("No resources found.") 28 | } else { 29 | if ns == "" { 30 | ns = "default" 31 | } 32 | fmt.Printf("No resources found in %s namespace.\n", ns) 33 | } 34 | return nil 35 | } 36 | 37 | buf := bytes.NewBuffer(nil) 38 | w := tabwriter.NewWriter(buf, 0, 0, 3, ' ', 0) 39 | 40 | fmt.Fprintln(w, constants.CronJobsHeader) 41 | 42 | for _, j := range cronjobList.Items { 43 | if !utils.MatchesKeyword(j.Name, keyword, opt.InvertMatch) { 44 | continue 45 | } 46 | 47 | if utils.ShouldExcludeResource(j.Name, opt.ExcludePattern) { 48 | continue 49 | } 50 | 51 | var lastScheduleTime string = "" 52 | if j.Status.LastScheduleTime != nil { 53 | lastScheduleTime = utils.GetAge(time.Since(j.Status.LastScheduleTime.Time)) 54 | } 55 | 56 | cronjobInfo = fmt.Sprintf(constants.CronJobsRowTemplate, 57 | j.Namespace, 58 | j.Name, 59 | j.Spec.Schedule, 60 | utils.BoolString(j.Spec.Suspend), 61 | len(j.Status.Active), 62 | lastScheduleTime, 63 | utils.GetAge(time.Since(j.CreationTimestamp.Time)), 64 | ) 65 | fmt.Fprintln(w, cronjobInfo) 66 | } 67 | w.Flush() 68 | 69 | fmt.Printf("%s", buf.String()) 70 | 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /pkg/resources/jobs.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "text/tabwriter" 8 | "time" 9 | 10 | "github.com/guessi/kubectl-grep/pkg/constants" 11 | "github.com/guessi/kubectl-grep/pkg/options" 12 | "github.com/guessi/kubectl-grep/pkg/utils" 13 | "k8s.io/apimachinery/pkg/util/duration" 14 | ) 15 | 16 | // Jobs - a public function for searching jobs with keyword 17 | func Jobs(ctx context.Context, opt *options.SearchOptions, keyword string) error { 18 | var jobInfo string 19 | 20 | jobList, err := utils.JobList(ctx, opt) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | if len(jobList.Items) == 0 { 26 | ns := opt.Namespace 27 | if opt.AllNamespaces { 28 | fmt.Println("No resources found.") 29 | } else { 30 | if ns == "" { 31 | ns = "default" 32 | } 33 | fmt.Printf("No resources found in %s namespace.\n", ns) 34 | } 35 | return nil 36 | } 37 | 38 | buf := bytes.NewBuffer(nil) 39 | w := tabwriter.NewWriter(buf, 0, 0, 3, ' ', 0) 40 | 41 | fmt.Fprintln(w, constants.JobsHeader) 42 | 43 | for _, j := range jobList.Items { 44 | var jobDuration string 45 | 46 | if !utils.MatchesKeyword(j.Name, keyword, opt.InvertMatch) { 47 | continue 48 | } 49 | 50 | if utils.ShouldExcludeResource(j.Name, opt.ExcludePattern) { 51 | continue 52 | } 53 | 54 | age := utils.GetAge(time.Since(j.CreationTimestamp.Time)) 55 | 56 | completions := j.Spec.Completions 57 | succeeded := j.Status.Succeeded 58 | 59 | if succeeded > 0 && j.Status.StartTime != nil && j.Status.CompletionTime != nil { 60 | start := j.Status.StartTime.Time 61 | end := j.Status.CompletionTime.Time 62 | jobDuration = duration.HumanDuration(end.Sub(start)) 63 | } else { 64 | jobDuration = age 65 | } 66 | 67 | var completionsValue int32 = 0 68 | if completions != nil { 69 | completionsValue = *completions 70 | } 71 | 72 | jobInfo = fmt.Sprintf(constants.JobsRowTemplate, 73 | j.Namespace, 74 | j.Name, 75 | succeeded, completionsValue, 76 | jobDuration, 77 | age, 78 | ) 79 | fmt.Fprintln(w, jobInfo) 80 | } 81 | w.Flush() 82 | 83 | fmt.Printf("%s", buf.String()) 84 | 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /pkg/utils/apps.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | log "github.com/sirupsen/logrus" 8 | appsv1 "k8s.io/api/apps/v1" 9 | 10 | "github.com/guessi/kubectl-grep/pkg/client" 11 | "github.com/guessi/kubectl-grep/pkg/options" 12 | ) 13 | 14 | // DaemonSetList - return a list of DaemonSet(s) 15 | func DaemonSetList(ctx context.Context, opt *options.SearchOptions) (*appsv1.DaemonSetList, error) { 16 | clientset := client.InitClient() 17 | ns, o := setOptions(opt) 18 | list, err := clientset.AppsV1().DaemonSets(ns).List(ctx, *o) 19 | if err != nil { 20 | log.WithFields(log.Fields{ 21 | "err": err.Error(), 22 | }) 23 | return nil, fmt.Errorf("failed to list DaemonSets: %w", err) 24 | } 25 | return list, nil 26 | } 27 | 28 | // DeploymentList - return a list of Deployment(s) 29 | func DeploymentList(ctx context.Context, opt *options.SearchOptions) (*appsv1.DeploymentList, error) { 30 | clientset := client.InitClient() 31 | ns, o := setOptions(opt) 32 | list, err := clientset.AppsV1().Deployments(ns).List(ctx, *o) 33 | if err != nil { 34 | log.WithFields(log.Fields{ 35 | "err": err.Error(), 36 | }) 37 | return nil, fmt.Errorf("failed to list Deployments: %w", err) 38 | } 39 | return list, nil 40 | } 41 | 42 | // ReplicaSetList - return a list of ReplicaSet(s) 43 | func ReplicaSetList(ctx context.Context, opt *options.SearchOptions) (*appsv1.ReplicaSetList, error) { 44 | clientset := client.InitClient() 45 | ns, o := setOptions(opt) 46 | list, err := clientset.AppsV1().ReplicaSets(ns).List(ctx, *o) 47 | if err != nil { 48 | log.WithFields(log.Fields{ 49 | "err": err.Error(), 50 | }) 51 | return nil, fmt.Errorf("failed to list ReplicaSets: %w", err) 52 | } 53 | return list, nil 54 | } 55 | 56 | // StatefulSetList - return a list of StatefulSet(s) 57 | func StatefulSetList(ctx context.Context, opt *options.SearchOptions) (*appsv1.StatefulSetList, error) { 58 | clientset := client.InitClient() 59 | ns, o := setOptions(opt) 60 | list, err := clientset.AppsV1().StatefulSets(ns).List(ctx, *o) 61 | if err != nil { 62 | log.WithFields(log.Fields{ 63 | "err": err.Error(), 64 | }) 65 | return nil, fmt.Errorf("failed to list StatefulSets: %w", err) 66 | } 67 | return list, nil 68 | } 69 | -------------------------------------------------------------------------------- /pkg/utils/rbac.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | log "github.com/sirupsen/logrus" 8 | rbacv1 "k8s.io/api/rbac/v1" 9 | 10 | "github.com/guessi/kubectl-grep/pkg/client" 11 | "github.com/guessi/kubectl-grep/pkg/options" 12 | ) 13 | 14 | // RoleList - return a list of Role(s) 15 | func RoleList(ctx context.Context, opt *options.SearchOptions) (*rbacv1.RoleList, error) { 16 | clientset := client.InitClient() 17 | ns, o := setOptions(opt) 18 | list, err := clientset.RbacV1().Roles(ns).List(ctx, *o) 19 | if err != nil { 20 | log.WithFields(log.Fields{ 21 | "err": err.Error(), 22 | }) 23 | return nil, fmt.Errorf("failed to list Roles: %w", err) 24 | } 25 | return list, nil 26 | } 27 | 28 | // RoleBindingList - return a list of RoleBinding(s) 29 | func RoleBindingList(ctx context.Context, opt *options.SearchOptions) (*rbacv1.RoleBindingList, error) { 30 | clientset := client.InitClient() 31 | ns, o := setOptions(opt) 32 | list, err := clientset.RbacV1().RoleBindings(ns).List(ctx, *o) 33 | if err != nil { 34 | log.WithFields(log.Fields{ 35 | "err": err.Error(), 36 | }) 37 | return nil, fmt.Errorf("failed to list RoleBindings: %w", err) 38 | } 39 | return list, nil 40 | } 41 | 42 | // ClusterRoleList - return a list of ClusterRole(s) 43 | func ClusterRoleList(ctx context.Context, opt *options.SearchOptions) (*rbacv1.ClusterRoleList, error) { 44 | clientset := client.InitClient() 45 | _, o := setOptions(opt) 46 | list, err := clientset.RbacV1().ClusterRoles().List(ctx, *o) 47 | if err != nil { 48 | log.WithFields(log.Fields{ 49 | "err": err.Error(), 50 | }) 51 | return nil, fmt.Errorf("failed to list ClusterRoles: %w", err) 52 | } 53 | return list, nil 54 | } 55 | 56 | // ClusterRoleBindingList - return a list of ClusterRoleBinding(s) 57 | func ClusterRoleBindingList(ctx context.Context, opt *options.SearchOptions) (*rbacv1.ClusterRoleBindingList, error) { 58 | clientset := client.InitClient() 59 | _, o := setOptions(opt) 60 | list, err := clientset.RbacV1().ClusterRoleBindings().List(ctx, *o) 61 | if err != nil { 62 | log.WithFields(log.Fields{ 63 | "err": err.Error(), 64 | }) 65 | return nil, fmt.Errorf("failed to list ClusterRoleBindings: %w", err) 66 | } 67 | return list, nil 68 | } 69 | -------------------------------------------------------------------------------- /.krew.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: krew.googlecontainertools.github.com/v1alpha2 2 | kind: Plugin 3 | metadata: 4 | name: grep 5 | spec: 6 | platforms: 7 | - {{addURIAndSha "https://github.com/guessi/kubectl-grep/releases/download/{{ .TagName }}/kubectl-grep-Darwin-x86_64.tar.gz" .TagName }} 8 | bin: kubectl-grep 9 | files: 10 | - from: kubectl-grep 11 | to: . 12 | - from: LICENSE 13 | to: . 14 | selector: 15 | matchLabels: 16 | os: darwin 17 | arch: amd64 18 | - {{addURIAndSha "https://github.com/guessi/kubectl-grep/releases/download/{{ .TagName }}/kubectl-grep-Darwin-arm64.tar.gz" .TagName }} 19 | bin: kubectl-grep 20 | files: 21 | - from: kubectl-grep 22 | to: . 23 | - from: LICENSE 24 | to: . 25 | selector: 26 | matchLabels: 27 | os: darwin 28 | arch: arm64 29 | - {{addURIAndSha "https://github.com/guessi/kubectl-grep/releases/download/{{ .TagName }}/kubectl-grep-Linux-x86_64.tar.gz" .TagName }} 30 | bin: kubectl-grep 31 | files: 32 | - from: kubectl-grep 33 | to: . 34 | - from: LICENSE 35 | to: . 36 | selector: 37 | matchLabels: 38 | os: linux 39 | arch: amd64 40 | - {{addURIAndSha "https://github.com/guessi/kubectl-grep/releases/download/{{ .TagName }}/kubectl-grep-Linux-arm64.tar.gz" .TagName }} 41 | bin: kubectl-grep 42 | files: 43 | - from: kubectl-grep 44 | to: . 45 | - from: LICENSE 46 | to: . 47 | selector: 48 | matchLabels: 49 | os: linux 50 | arch: arm64 51 | - {{addURIAndSha "https://github.com/guessi/kubectl-grep/releases/download/{{ .TagName }}/kubectl-grep-Windows-x86_64.tar.gz" .TagName }} 52 | bin: kubectl-grep.exe 53 | files: 54 | - from: kubectl-grep.exe 55 | to: . 56 | - from: LICENSE.txt 57 | to: . 58 | selector: 59 | matchLabels: 60 | os: windows 61 | arch: amd64 62 | version: {{ .TagName }} 63 | homepage: https://github.com/guessi/kubectl-grep 64 | shortDescription: Filter Kubernetes resources by matching their names 65 | description: | 66 | Filter Kubernetes resources by matching their names 67 | 68 | Examples: 69 | 70 | List all pods in all namespaces 71 | $ kubectl grep pods --all-namespaces 72 | 73 | List all pods in namespace "star-lab" which contain the keyword "flash" 74 | $ kubectl grep pods -n star-lab flash 75 | 76 | No more pipe, built-in grep :-) 77 | -------------------------------------------------------------------------------- /pkg/resources/statefulsets.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "strings" 8 | "text/tabwriter" 9 | "time" 10 | 11 | "github.com/guessi/kubectl-grep/pkg/constants" 12 | "github.com/guessi/kubectl-grep/pkg/options" 13 | "github.com/guessi/kubectl-grep/pkg/utils" 14 | ) 15 | 16 | // Statefulsets - a public function for searching Statefulsets with keyword 17 | func Statefulsets(ctx context.Context, opt *options.SearchOptions, keyword string, wide bool) error { 18 | var statefulsetInfo string 19 | 20 | statefulsetList, err := utils.StatefulSetList(ctx, opt) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | if len(statefulsetList.Items) == 0 { 26 | ns := opt.Namespace 27 | if opt.AllNamespaces { 28 | fmt.Println("No resources found.") 29 | } else { 30 | if ns == "" { 31 | ns = "default" 32 | } 33 | fmt.Printf("No resources found in %s namespace.\n", ns) 34 | } 35 | return nil 36 | } 37 | 38 | buf := bytes.NewBuffer(nil) 39 | w := tabwriter.NewWriter(buf, 0, 0, 3, ' ', 0) 40 | 41 | if wide { 42 | fmt.Fprintln(w, constants.StatefulsetHeaderWide) 43 | } else { 44 | fmt.Fprintln(w, constants.StatefulsetHeader) 45 | } 46 | 47 | for _, s := range statefulsetList.Items { 48 | if !utils.MatchesKeyword(s.Name, keyword, opt.InvertMatch) { 49 | continue 50 | } 51 | 52 | if utils.ShouldExcludeResource(s.Name, opt.ExcludePattern) { 53 | continue 54 | } 55 | 56 | age := utils.GetAge(time.Since(s.CreationTimestamp.Time)) 57 | containers := s.Spec.Template.Spec.Containers 58 | 59 | var replicas int32 = 0 60 | if s.Spec.Replicas != nil { 61 | replicas = *s.Spec.Replicas 62 | } 63 | 64 | if wide { 65 | names := []string{} 66 | images := []string{} 67 | 68 | for _, n := range containers { 69 | names = append(names, n.Name) 70 | images = append(images, n.Image) 71 | } 72 | 73 | statefulsetInfo = fmt.Sprintf(constants.StatefulsetRowTemplateWide, 74 | s.Namespace, 75 | s.Name, 76 | s.Status.ReadyReplicas, 77 | replicas, 78 | age, 79 | strings.Join(names, ","), 80 | strings.Join(images, ","), 81 | ) 82 | } else { 83 | statefulsetInfo = fmt.Sprintf(constants.StatefulsetRowTemplate, 84 | s.Namespace, 85 | s.Name, 86 | s.Status.ReadyReplicas, 87 | replicas, 88 | age, 89 | ) 90 | } 91 | fmt.Fprintln(w, statefulsetInfo) 92 | } 93 | w.Flush() 94 | 95 | fmt.Printf("%s", buf.String()) 96 | 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /pkg/resources/pods.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "text/tabwriter" 8 | "time" 9 | 10 | "github.com/guessi/kubectl-grep/pkg/constants" 11 | "github.com/guessi/kubectl-grep/pkg/options" 12 | "github.com/guessi/kubectl-grep/pkg/utils" 13 | ) 14 | 15 | // Pods - a public function for searching pods with keyword 16 | func Pods(ctx context.Context, opt *options.SearchOptions, keyword string, wide bool) error { 17 | var podInfo string 18 | 19 | podList, err := utils.PodList(ctx, opt) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | if len(podList.Items) == 0 { 25 | ns := opt.Namespace 26 | if opt.AllNamespaces { 27 | fmt.Println("No resources found.") 28 | } else { 29 | if ns == "" { 30 | ns = "default" 31 | } 32 | fmt.Printf("No resources found in %s namespace.\n", ns) 33 | } 34 | return nil 35 | } 36 | 37 | buf := bytes.NewBuffer(nil) 38 | w := tabwriter.NewWriter(buf, 0, 0, 3, ' ', 0) 39 | 40 | if wide { 41 | fmt.Fprintln(w, constants.PodHeaderWide) 42 | } else { 43 | fmt.Fprintln(w, constants.PodHeader) 44 | } 45 | for _, p := range podList.Items { 46 | if !utils.MatchesKeyword(p.Name, keyword, opt.InvertMatch) { 47 | continue 48 | } 49 | 50 | if utils.ShouldExcludeResource(p.Name, opt.ExcludePattern) { 51 | continue 52 | } 53 | 54 | var containerCount int = len(p.Spec.Containers) 55 | var readyCount int32 56 | var restartCount int32 57 | 58 | for _, cs := range p.Status.ContainerStatuses { 59 | restartCount += cs.RestartCount 60 | if cs.Ready { 61 | readyCount++ 62 | } 63 | } 64 | 65 | var podIP string = "" 66 | if len(p.Status.PodIP) > 0 { 67 | podIP = p.Status.PodIP 68 | } 69 | 70 | var nodeName string = "" 71 | if len(p.Spec.NodeName) > 0 { 72 | nodeName = p.Spec.NodeName 73 | } 74 | 75 | age := utils.GetAge(time.Since(p.CreationTimestamp.Time)) 76 | 77 | if wide { 78 | podInfo = fmt.Sprintf(constants.PodRowTemplateWide, 79 | p.Namespace, 80 | p.Name, 81 | readyCount, containerCount, 82 | p.Status.Phase, 83 | restartCount, 84 | age, 85 | podIP, 86 | nodeName, 87 | ) 88 | } else { 89 | podInfo = fmt.Sprintf(constants.PodRowTemplate, 90 | p.Namespace, 91 | p.Name, 92 | readyCount, containerCount, 93 | p.Status.Phase, 94 | restartCount, 95 | age, 96 | ) 97 | } 98 | fmt.Fprintln(w, podInfo) 99 | } 100 | w.Flush() 101 | 102 | fmt.Printf("%s", buf.String()) 103 | 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /pkg/resources/ingresses.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | "text/tabwriter" 10 | "time" 11 | 12 | "github.com/guessi/kubectl-grep/pkg/constants" 13 | "github.com/guessi/kubectl-grep/pkg/options" 14 | "github.com/guessi/kubectl-grep/pkg/utils" 15 | ) 16 | 17 | // Ingresses - a public function for searching ingresses with keyword 18 | func Ingresses(ctx context.Context, opt *options.SearchOptions, keyword string) error { 19 | var ingressInfo string 20 | 21 | ingressList, err := utils.IngressList(ctx, opt) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | if len(ingressList.Items) == 0 { 27 | ns := opt.Namespace 28 | if opt.AllNamespaces { 29 | fmt.Println("No resources found.") 30 | } else { 31 | if ns == "" { 32 | ns = "default" 33 | } 34 | fmt.Printf("No resources found in %s namespace.\n", ns) 35 | } 36 | return nil 37 | } 38 | 39 | buf := bytes.NewBuffer(nil) 40 | w := tabwriter.NewWriter(buf, 0, 0, 3, ' ', 0) 41 | 42 | fmt.Fprintln(w, constants.IngressHeader) 43 | 44 | for _, i := range ingressList.Items { 45 | var ingressClassName string 46 | var hosts, ports, addresses []string 47 | 48 | if !utils.MatchesKeyword(i.Name, keyword, opt.InvertMatch) { 49 | continue 50 | } 51 | 52 | if utils.ShouldExcludeResource(i.Name, opt.ExcludePattern) { 53 | continue 54 | } 55 | 56 | age := utils.GetAge(time.Since(i.CreationTimestamp.Time)) 57 | 58 | if i.Spec.IngressClassName == nil { 59 | ingressClassName = "" 60 | } else { 61 | ingressClassName = *i.Spec.IngressClassName 62 | } 63 | 64 | for _, irs := range i.Spec.Rules { 65 | if len(irs.Host) > 0 { 66 | hosts = append(hosts, irs.Host) 67 | } 68 | 69 | if irs.IngressRuleValue.HTTP != nil { 70 | for _, ips := range irs.IngressRuleValue.HTTP.Paths { 71 | if ips.Backend.Service != nil && ips.Backend.Service.Port.Number > 0 { 72 | ports = append(ports, strconv.Itoa(int(ips.Backend.Service.Port.Number))) 73 | } 74 | } 75 | } 76 | } 77 | 78 | for _, lbi := range i.Status.LoadBalancer.Ingress { 79 | addresses = append(addresses, lbi.IP) 80 | } 81 | 82 | ingressInfo = fmt.Sprintf(constants.IngressRowTemplate, 83 | i.Namespace, 84 | i.Name, 85 | ingressClassName, 86 | strings.Join(hosts, ","), 87 | strings.Join(addresses, ","), 88 | strings.Join(ports, ","), 89 | age, 90 | ) 91 | 92 | fmt.Fprintln(w, ingressInfo) 93 | } 94 | w.Flush() 95 | 96 | fmt.Printf("%s", buf.String()) 97 | 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | schedule: 8 | - cron: '0 3 * * *' 9 | tags: 10 | - "v1.[0-9]+.[0-9]+" 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 5 16 | steps: 17 | - uses: actions/checkout@v6 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v6 21 | with: 22 | go-version-file: "go.mod" 23 | 24 | - name: Run make test 25 | run: make test 26 | 27 | - name: Run make staticcheck 28 | run: make staticcheck 29 | 30 | build: 31 | needs: 32 | - lint 33 | runs-on: ubuntu-latest 34 | timeout-minutes: 10 35 | strategy: 36 | matrix: 37 | platform: ['darwin', 'linux', 'windows'] 38 | steps: 39 | - uses: actions/checkout@v6 40 | 41 | - name: Set up Go 42 | uses: actions/setup-go@v6 43 | with: 44 | go-version-file: "go.mod" 45 | 46 | - name: Run make build-${{ matrix.platform }} 47 | run: make build-${{ matrix.platform }} 48 | 49 | - name: Check outputs 50 | run: find ./releases -type f 51 | 52 | - name: Cache builds 53 | uses: actions/cache@v4 54 | with: 55 | path: ./releases 56 | key: ${{ runner.os }}-go-${{ matrix.platform }}-${{ hashFiles('**/go.sum') }}-${{ github.ref_name }} 57 | 58 | release: 59 | # only work when it is a tagged release 60 | # ref: https://docs.github.com/en/actions/learn-github-actions/expressions 61 | if: ${{ github.ref_type == 'tag' && contains(github.ref, 'v1.') }} 62 | needs: 63 | - build 64 | runs-on: ubuntu-latest 65 | timeout-minutes: 10 66 | steps: 67 | - uses: actions/checkout@v6 68 | 69 | - name: Restore cache-darwin 70 | uses: actions/cache@v4 71 | with: 72 | path: ./releases 73 | key: ${{ runner.os }}-go-darwin-${{ hashFiles('**/go.sum') }}-${{ github.ref_name }} 74 | 75 | - name: Restore cache-linux 76 | uses: actions/cache@v4 77 | with: 78 | path: ./releases 79 | key: ${{ runner.os }}-go-linux-${{ hashFiles('**/go.sum') }}-${{ github.ref_name }} 80 | 81 | - name: Restore cache-windows 82 | uses: actions/cache@v4 83 | with: 84 | path: ./releases 85 | key: ${{ runner.os }}-go-windows-${{ hashFiles('**/go.sum') }}-${{ github.ref_name }} 86 | 87 | - name: Check outputs 88 | run: find ./releases -type f 89 | 90 | - name: Run make release 91 | run: make release 92 | env: 93 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 94 | 95 | - name: Update new version in krew-index 96 | uses: rajatjindal/krew-release-bot@v0.0.46 97 | -------------------------------------------------------------------------------- /pkg/resources/replicasets.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "strings" 8 | "text/tabwriter" 9 | "time" 10 | 11 | "github.com/guessi/kubectl-grep/pkg/constants" 12 | "github.com/guessi/kubectl-grep/pkg/options" 13 | "github.com/guessi/kubectl-grep/pkg/utils" 14 | ) 15 | 16 | // Replicasets - a public function for searching Replicasets with keyword 17 | func Replicasets(ctx context.Context, opt *options.SearchOptions, keyword string, wide bool) error { 18 | var replicasetInfo string 19 | 20 | replicasetList, err := utils.ReplicaSetList(ctx, opt) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | if len(replicasetList.Items) == 0 { 26 | ns := opt.Namespace 27 | if opt.AllNamespaces { 28 | fmt.Println("No resources found.") 29 | } else { 30 | if ns == "" { 31 | ns = "default" 32 | } 33 | fmt.Printf("No resources found in %s namespace.\n", ns) 34 | } 35 | return nil 36 | } 37 | 38 | buf := bytes.NewBuffer(nil) 39 | w := tabwriter.NewWriter(buf, 0, 0, 3, ' ', 0) 40 | 41 | if wide { 42 | fmt.Fprintln(w, constants.ReplicasetHeaderWide) 43 | } else { 44 | fmt.Fprintln(w, constants.ReplicasetHeader) 45 | } 46 | 47 | for _, s := range replicasetList.Items { 48 | if !utils.MatchesKeyword(s.Name, keyword, opt.InvertMatch) { 49 | continue 50 | } 51 | 52 | if utils.ShouldExcludeResource(s.Name, opt.ExcludePattern) { 53 | continue 54 | } 55 | 56 | age := utils.GetAge(time.Since(s.CreationTimestamp.Time)) 57 | 58 | var replicas int32 = 0 59 | if s.Spec.Replicas != nil { 60 | replicas = *s.Spec.Replicas 61 | } 62 | 63 | if wide { 64 | names := []string{} 65 | images := []string{} 66 | 67 | for _, n := range s.Spec.Template.Spec.Containers { 68 | names = append(names, n.Name) 69 | images = append(images, n.Image) 70 | } 71 | 72 | selectors := []string{} 73 | for k, v := range s.Spec.Selector.MatchLabels { 74 | selectors = append(selectors, fmt.Sprintf("%s=%s", k, v)) 75 | } 76 | 77 | replicasetInfo = fmt.Sprintf(constants.ReplicasetRowTemplateWide, 78 | s.Namespace, 79 | s.Name, 80 | replicas, 81 | s.Status.Replicas, 82 | s.Status.ReadyReplicas, 83 | age, 84 | strings.Join(names, ","), 85 | strings.Join(images, ","), 86 | strings.Join(selectors, ","), 87 | ) 88 | } else { 89 | replicasetInfo = fmt.Sprintf(constants.ReplicasetRowTemplate, 90 | s.Namespace, 91 | s.Name, 92 | replicas, 93 | s.Status.Replicas, 94 | s.Status.ReadyReplicas, 95 | age, 96 | ) 97 | } 98 | fmt.Fprintln(w, replicasetInfo) 99 | } 100 | w.Flush() 101 | 102 | fmt.Printf("%s", buf.String()) 103 | 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/guessi/kubectl-grep 2 | 3 | go 1.25.0 4 | 5 | require ( 6 | github.com/sirupsen/logrus v1.9.3 7 | github.com/spf13/cobra v1.10.2 8 | k8s.io/api v0.35.0 9 | k8s.io/apimachinery v0.35.0 10 | k8s.io/client-go v0.35.0 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 15 | github.com/emicklei/go-restful/v3 v3.13.0 // indirect 16 | github.com/fxamacker/cbor/v2 v2.9.0 // indirect 17 | github.com/go-logr/logr v1.4.3 // indirect 18 | github.com/go-openapi/jsonpointer v0.22.4 // indirect 19 | github.com/go-openapi/jsonreference v0.21.4 // indirect 20 | github.com/go-openapi/swag v0.25.4 // indirect 21 | github.com/go-openapi/swag/cmdutils v0.25.4 // indirect 22 | github.com/go-openapi/swag/conv v0.25.4 // indirect 23 | github.com/go-openapi/swag/fileutils v0.25.4 // indirect 24 | github.com/go-openapi/swag/jsonname v0.25.4 // indirect 25 | github.com/go-openapi/swag/jsonutils v0.25.4 // indirect 26 | github.com/go-openapi/swag/loading v0.25.4 // indirect 27 | github.com/go-openapi/swag/mangling v0.25.4 // indirect 28 | github.com/go-openapi/swag/netutils v0.25.4 // indirect 29 | github.com/go-openapi/swag/stringutils v0.25.4 // indirect 30 | github.com/go-openapi/swag/typeutils v0.25.4 // indirect 31 | github.com/go-openapi/swag/yamlutils v0.25.4 // indirect 32 | github.com/google/gnostic-models v0.7.1 // indirect 33 | github.com/google/uuid v1.6.0 // indirect 34 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 35 | github.com/json-iterator/go v1.1.12 // indirect 36 | github.com/kr/text v0.2.0 // indirect 37 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 38 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect 39 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 40 | github.com/spf13/pflag v1.0.10 // indirect 41 | github.com/x448/float16 v0.8.4 // indirect 42 | go.yaml.in/yaml/v2 v2.4.3 // indirect 43 | go.yaml.in/yaml/v3 v3.0.4 // indirect 44 | golang.org/x/net v0.48.0 // indirect 45 | golang.org/x/oauth2 v0.34.0 // indirect 46 | golang.org/x/sys v0.39.0 // indirect 47 | golang.org/x/term v0.38.0 // indirect 48 | golang.org/x/text v0.32.0 // indirect 49 | golang.org/x/time v0.14.0 // indirect 50 | google.golang.org/protobuf v1.36.11 // indirect 51 | gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect 52 | gopkg.in/inf.v0 v0.9.1 // indirect 53 | k8s.io/klog/v2 v2.130.1 // indirect 54 | k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e // indirect 55 | k8s.io/utils v0.0.0-20251219084037-98d557b7f1e7 // indirect 56 | sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect 57 | sigs.k8s.io/randfill v1.0.0 // indirect 58 | sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect 59 | sigs.k8s.io/yaml v1.6.0 // indirect 60 | ) 61 | -------------------------------------------------------------------------------- /pkg/resources/deployments.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "strings" 8 | "text/tabwriter" 9 | "time" 10 | 11 | "github.com/guessi/kubectl-grep/pkg/constants" 12 | "github.com/guessi/kubectl-grep/pkg/options" 13 | "github.com/guessi/kubectl-grep/pkg/utils" 14 | ) 15 | 16 | // Deployments - a public function for searching deployments with keyword 17 | func Deployments(ctx context.Context, opt *options.SearchOptions, keyword string, wide bool) error { 18 | var deploymentInfo string 19 | 20 | deploymentList, err := utils.DeploymentList(ctx, opt) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | if len(deploymentList.Items) == 0 { 26 | ns := opt.Namespace 27 | if opt.AllNamespaces { 28 | fmt.Println("No resources found.") 29 | } else { 30 | if ns == "" { 31 | ns = "default" 32 | } 33 | fmt.Printf("No resources found in %s namespace.\n", ns) 34 | } 35 | return nil 36 | } 37 | 38 | buf := bytes.NewBuffer(nil) 39 | w := tabwriter.NewWriter(buf, 0, 0, 3, ' ', 0) 40 | 41 | if wide { 42 | fmt.Fprintln(w, constants.DeploymentHeaderWide) 43 | } else { 44 | fmt.Fprintln(w, constants.DeploymentHeader) 45 | } 46 | for _, d := range deploymentList.Items { 47 | if !utils.MatchesKeyword(d.Name, keyword, opt.InvertMatch) { 48 | continue 49 | } 50 | 51 | if utils.ShouldExcludeResource(d.Name, opt.ExcludePattern) { 52 | continue 53 | } 54 | 55 | age := utils.GetAge(time.Since(d.CreationTimestamp.Time)) 56 | containers := d.Spec.Template.Spec.Containers 57 | 58 | var replicas int32 = 0 59 | if d.Spec.Replicas != nil { 60 | replicas = *d.Spec.Replicas 61 | } 62 | 63 | if wide { 64 | var names []string 65 | var images []string 66 | var selectors []string 67 | 68 | for _, n := range containers { 69 | names = append(names, n.Name) 70 | images = append(images, n.Image) 71 | } 72 | 73 | for k, v := range d.Spec.Selector.MatchLabels { 74 | selectors = append(selectors, fmt.Sprintf("%s=%s", k, v)) 75 | } 76 | 77 | deploymentInfo = fmt.Sprintf(constants.DeploymentRowTemplateWide, 78 | d.Namespace, 79 | d.Name, 80 | d.Status.ReadyReplicas, 81 | replicas, 82 | d.Status.UpdatedReplicas, 83 | d.Status.AvailableReplicas, 84 | age, 85 | strings.Join(names, ","), 86 | strings.Join(images, ","), 87 | strings.Join(selectors, ","), 88 | ) 89 | } else { 90 | deploymentInfo = fmt.Sprintf(constants.DeploymentRowTemplate, 91 | d.Namespace, 92 | d.Name, 93 | d.Status.ReadyReplicas, 94 | replicas, 95 | d.Status.UpdatedReplicas, 96 | d.Status.AvailableReplicas, 97 | age, 98 | ) 99 | } 100 | fmt.Fprintln(w, deploymentInfo) 101 | } 102 | w.Flush() 103 | 104 | fmt.Printf("%s", buf.String()) 105 | 106 | return nil 107 | } 108 | -------------------------------------------------------------------------------- /pkg/utils/common.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | log "github.com/sirupsen/logrus" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/util/duration" 12 | 13 | "github.com/guessi/kubectl-grep/pkg/client" 14 | "github.com/guessi/kubectl-grep/pkg/constants" 15 | "github.com/guessi/kubectl-grep/pkg/options" 16 | ) 17 | 18 | // setOptions - set common options for clientset 19 | func setOptions(opt *options.SearchOptions) (string, *metav1.ListOptions) { 20 | // set default namespace as "default" 21 | namespace := "default" 22 | 23 | // override `namespace` if `--all-namespaces` exist 24 | if opt.AllNamespaces { 25 | namespace = "" 26 | } else { 27 | if len(opt.Namespace) > 0 { 28 | namespace = opt.Namespace 29 | } else { 30 | ns, _, err := client.ClientConfig().Namespace() 31 | if err != nil { 32 | log.WithFields(log.Fields{ 33 | "err": err.Error(), 34 | }) 35 | } else { 36 | namespace = ns 37 | } 38 | } 39 | } 40 | 41 | // retrieve listOptions from meta 42 | listOptions := &metav1.ListOptions{ 43 | LabelSelector: opt.Selector, 44 | FieldSelector: opt.FieldSelector, 45 | } 46 | return namespace, listOptions 47 | } 48 | 49 | // TrimQuoteAndSpace - remove Spaces, Tabs, SingleQuotes, DoubleQuites 50 | func TrimQuoteAndSpace(input string) string { 51 | if len(input) >= 2 { 52 | if input[0] == '"' && input[len(input)-1] == '"' { 53 | return input[1 : len(input)-1] 54 | } 55 | if input[0] == '\'' && input[len(input)-1] == '\'' { 56 | return input[1 : len(input)-1] 57 | } 58 | } 59 | return strings.TrimSpace(input) 60 | } 61 | 62 | // GetAge - return human readable time expression 63 | func GetAge(d time.Duration) string { 64 | return duration.HumanDuration(d) 65 | } 66 | 67 | // BoolValue 68 | func BoolValue(b *bool) bool { 69 | if b != nil { 70 | return *b 71 | } 72 | return false 73 | } 74 | 75 | // BoolString 76 | func BoolString(b *bool) string { 77 | return strconv.FormatBool(BoolValue(b)) 78 | } 79 | 80 | func MatchesKeyword(target string, keyword string, invertMatch bool) bool { 81 | if len(keyword) == 0 { 82 | return true 83 | } 84 | match := strings.Contains(target, keyword) 85 | return match != invertMatch 86 | } 87 | 88 | func ShouldExcludeResource(target string, excludePattern string) bool { 89 | excludePattern = strings.TrimSpace(excludePattern) 90 | if excludePattern == "" { 91 | return false 92 | } 93 | 94 | for _, exclude := range strings.Split(excludePattern, ",") { 95 | exclude = strings.TrimSpace(exclude) 96 | if exclude != "" && strings.Contains(target, exclude) { 97 | return true 98 | } 99 | } 100 | return false 101 | } 102 | 103 | func FormatUtilization(utilization *int32) string { 104 | if utilization != nil { 105 | return fmt.Sprintf("%d%%", *utilization) 106 | } 107 | return constants.UNKNOWN 108 | } 109 | -------------------------------------------------------------------------------- /pkg/utils/core.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | log "github.com/sirupsen/logrus" 8 | corev1 "k8s.io/api/core/v1" 9 | 10 | "github.com/guessi/kubectl-grep/pkg/client" 11 | "github.com/guessi/kubectl-grep/pkg/options" 12 | ) 13 | 14 | // ConfigMapList - return a list of ConfigMap(s) 15 | func ConfigMapList(ctx context.Context, opt *options.SearchOptions) (*corev1.ConfigMapList, error) { 16 | clientset := client.InitClient() 17 | ns, o := setOptions(opt) 18 | list, err := clientset.CoreV1().ConfigMaps(ns).List(ctx, *o) 19 | if err != nil { 20 | log.WithFields(log.Fields{ 21 | "err": err.Error(), 22 | }) 23 | return nil, fmt.Errorf("failed to list ConfigMaps: %w", err) 24 | } 25 | return list, nil 26 | } 27 | 28 | // NodeList - return a list of Node(s) 29 | func NodeList(ctx context.Context, opt *options.SearchOptions) (*corev1.NodeList, error) { 30 | clientset := client.InitClient() 31 | _, o := setOptions(opt) 32 | list, err := clientset.CoreV1().Nodes().List(ctx, *o) 33 | if err != nil { 34 | log.WithFields(log.Fields{ 35 | "err": err.Error(), 36 | }) 37 | return nil, fmt.Errorf("failed to list Nodes: %w", err) 38 | } 39 | return list, nil 40 | } 41 | 42 | // PodList - return a list of Pod(s) 43 | func PodList(ctx context.Context, opt *options.SearchOptions) (*corev1.PodList, error) { 44 | clientset := client.InitClient() 45 | ns, o := setOptions(opt) 46 | list, err := clientset.CoreV1().Pods(ns).List(ctx, *o) 47 | if err != nil { 48 | log.WithFields(log.Fields{ 49 | "err": err.Error(), 50 | }) 51 | return nil, fmt.Errorf("failed to list Pods: %w", err) 52 | } 53 | return list, nil 54 | } 55 | 56 | // SecretList - return a list of Secret(s) 57 | func SecretList(ctx context.Context, opt *options.SearchOptions) (*corev1.SecretList, error) { 58 | clientset := client.InitClient() 59 | ns, o := setOptions(opt) 60 | list, err := clientset.CoreV1().Secrets(ns).List(ctx, *o) 61 | if err != nil { 62 | log.WithFields(log.Fields{ 63 | "err": err.Error(), 64 | }) 65 | return nil, fmt.Errorf("failed to list Secrets: %w", err) 66 | } 67 | return list, nil 68 | } 69 | 70 | // ServiceAccountList - return a list of ServiceAccount(s) 71 | func ServiceAccountList(ctx context.Context, opt *options.SearchOptions) (*corev1.ServiceAccountList, error) { 72 | clientset := client.InitClient() 73 | ns, o := setOptions(opt) 74 | list, err := clientset.CoreV1().ServiceAccounts(ns).List(ctx, *o) 75 | if err != nil { 76 | log.WithFields(log.Fields{ 77 | "err": err.Error(), 78 | }) 79 | return nil, fmt.Errorf("failed to list ServiceAccounts: %w", err) 80 | } 81 | return list, nil 82 | } 83 | 84 | // ServiceList - return a list of Service(s) 85 | func ServiceList(ctx context.Context, opt *options.SearchOptions) (*corev1.ServiceList, error) { 86 | clientset := client.InitClient() 87 | ns, o := setOptions(opt) 88 | list, err := clientset.CoreV1().Services(ns).List(ctx, *o) 89 | if err != nil { 90 | log.WithFields(log.Fields{ 91 | "err": err.Error(), 92 | }) 93 | return nil, fmt.Errorf("failed to list Services: %w", err) 94 | } 95 | return list, nil 96 | } 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kubectl Grep 2 | 3 | [![GitHub Actions](https://github.com/guessi/kubectl-grep/actions/workflows/go.yml/badge.svg?branch=main)](https://github.com/guessi/kubectl-grep/actions/workflows/go.yml) 4 | [![GoDoc](https://godoc.org/github.com/guessi/kubectl-grep?status.svg)](https://godoc.org/github.com/guessi/kubectl-grep) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/guessi/kubectl-grep)](https://goreportcard.com/report/github.com/guessi/kubectl-grep) 6 | [![GitHub release](https://img.shields.io/github/release/guessi/kubectl-grep.svg)](https://github.com/guessi/kubectl-grep/releases/latest) 7 | [![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/guessi/kubectl-grep)](https://github.com/guessi/kubectl-grep/blob/main/go.mod) 8 | 9 | Filter Kubernetes resources by matching their names 10 | 11 | ## 🤔 Why we need this? what it is trying to resolve? 12 | 13 | Playing with Kubernetes in our daily job, we normally search pods by `pipe`, `grep`, `--label`, `--field-selector`, etc. while hunting abnormal pods, but typing such long commands is quite annoying. With plugin installed, it could be easily be done by `kubectl grep`. 14 | 15 | ## 🔢 Prerequisites 16 | 17 | * An existing Kubernetes cluster. 18 | * Have [kubectl](https://kubernetes.io/docs/tasks/tools/) installed. 19 | * Have [krew](https://krew.sigs.k8s.io/docs/user-guide/setup/install/) installed. 20 | 21 | ## 🚀 Quick start 22 | 23 | ```bash 24 | # Have krew plugin installed 25 | kubectl krew install grep 26 | ``` 27 | 28 | ```bash 29 | # Before change, we usually filter pods by the following commands, 30 | kubectl get pods -n star-lab | grep "flash" 31 | ``` 32 | 33 | ```bash 34 | # With this plugin installed, you can filter pod easily 35 | kubectl grep pods -n star-lab flash 36 | ``` 37 | 38 | ## :accessibility: FAQ 39 | 40 | How do I check the version installed? 41 | 42 | * `kubectl grep version` 43 | 44 | How do I know the version installed is compatible with my cluster version? 45 | 46 | * Ideally, it should be compatible with [supported Kubernetes versions](https://kubernetes.io/releases/). 47 | 48 | What kind of resource(s) `kubectl-grep` support? 49 | 50 | * Please refer to [Resource Types](RESOURCE_TYPES.md) 51 | 52 | ## 👷 Install 53 | 54 | ### Recommended way 55 | 56 | Brand new install 57 | 58 | ```bash 59 | kubectl krew update && kubectl krew install grep 60 | ``` 61 | 62 | To upgrade version 63 | 64 | ```bash 65 | kubectl krew update && kubectl krew upgrade grep 66 | ``` 67 | 68 | ### Manual Installation 69 | 70 |
71 | Click to expand! 72 | 73 | ```bash 74 | curl -fsSL -O https://github.com/guessi/kubectl-grep/releases/latest/download/kubectl-grep-$(uname -s)-$(uname -m).tar.gz 75 | tar zxvf kubectl-grep-$(uname -s)-$(uname -m).tar.gz 76 | mv kubectl-grep /usr/local/bin 77 | ``` 78 | 79 |
80 | 81 | ### Developer build 82 | 83 |
84 | Click to expand! 85 | 86 | ```bash 87 | go get -u github.com/guessi/kubectl-grep 88 | cd ${GOPATH}/src/github.com/guessi/kubectl-grep 89 | make all 90 | ``` 91 | 92 |
93 | 94 | ## ⚖️ License 95 | 96 | [Apache-2.0](LICENSE) 97 | -------------------------------------------------------------------------------- /pkg/resources/nodes.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "strings" 8 | "text/tabwriter" 9 | "time" 10 | 11 | "github.com/guessi/kubectl-grep/pkg/constants" 12 | "github.com/guessi/kubectl-grep/pkg/options" 13 | "github.com/guessi/kubectl-grep/pkg/utils" 14 | v1 "k8s.io/api/core/v1" 15 | ) 16 | 17 | // Nodes - a public function for searching nodes with keyword 18 | func Nodes(ctx context.Context, opt *options.SearchOptions, keyword string, wide bool) error { 19 | var nodeInfo string 20 | 21 | nodeList, err := utils.NodeList(ctx, opt) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | if len(nodeList.Items) == 0 { 27 | ns := opt.Namespace 28 | if opt.AllNamespaces { 29 | fmt.Println("No resources found.") 30 | } else { 31 | if ns == "" { 32 | ns = "default" 33 | } 34 | fmt.Printf("No resources found in %s namespace.\n", ns) 35 | } 36 | return nil 37 | } 38 | 39 | buf := bytes.NewBuffer(nil) 40 | w := tabwriter.NewWriter(buf, 0, 0, 3, ' ', 0) 41 | 42 | if wide { 43 | fmt.Fprintln(w, constants.NodeHeaderWide) 44 | } else { 45 | fmt.Fprintln(w, constants.NodeHeader) 46 | } 47 | for _, n := range nodeList.Items { 48 | if !utils.MatchesKeyword(n.Name, keyword, opt.InvertMatch) { 49 | continue 50 | } 51 | 52 | if utils.ShouldExcludeResource(n.Name, opt.ExcludePattern) { 53 | continue 54 | } 55 | 56 | var roles []string 57 | 58 | for label := range n.Labels { 59 | if strings.HasPrefix(label, "node-role.kubernetes.io") { 60 | roles = append(roles, strings.SplitN(label, "/", 2)[1]) 61 | } 62 | } 63 | if len(roles) <= 0 { 64 | roles = append(roles, "") 65 | } 66 | 67 | var nodeStatus string = "Unknown" 68 | for _, condition := range n.Status.Conditions { 69 | if condition.Type == v1.NodeReady { 70 | if condition.Status == v1.ConditionTrue { 71 | nodeStatus = "Ready" 72 | } else { 73 | nodeStatus = "NotReady" 74 | } 75 | } 76 | } 77 | 78 | if n.Spec.Unschedulable { 79 | nodeStatus = nodeStatus + ",SchedulingDisabled" 80 | } 81 | 82 | age := utils.GetAge(time.Since(n.CreationTimestamp.Time)) 83 | 84 | if wide { 85 | var extAddr string = "" 86 | var intAddr string = "" 87 | 88 | for _, addr := range n.Status.Addresses { 89 | if addr.Type == "ExternalIP" { 90 | extAddr = addr.Address 91 | } 92 | if addr.Type == "InternalIP" { 93 | intAddr = addr.Address 94 | } 95 | } 96 | 97 | nodeInfo = fmt.Sprintf(constants.NodeRowTemplateWide, 98 | n.Name, 99 | nodeStatus, 100 | strings.Join(roles, ","), 101 | age, 102 | n.Status.NodeInfo.KubeletVersion, 103 | intAddr, 104 | extAddr, 105 | n.Status.NodeInfo.OSImage, 106 | n.Status.NodeInfo.KernelVersion, 107 | n.Status.NodeInfo.ContainerRuntimeVersion, 108 | ) 109 | } else { 110 | nodeInfo = fmt.Sprintf(constants.NodeRowTemplate, 111 | n.Name, 112 | nodeStatus, 113 | strings.Join(roles, ","), 114 | age, 115 | n.Status.NodeInfo.KubeletVersion, 116 | ) 117 | } 118 | fmt.Fprintln(w, nodeInfo) 119 | } 120 | w.Flush() 121 | 122 | fmt.Printf("%s", buf.String()) 123 | 124 | return nil 125 | } 126 | -------------------------------------------------------------------------------- /pkg/resources/services.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "strings" 8 | "text/tabwriter" 9 | "time" 10 | 11 | "github.com/guessi/kubectl-grep/pkg/constants" 12 | "github.com/guessi/kubectl-grep/pkg/options" 13 | "github.com/guessi/kubectl-grep/pkg/utils" 14 | ) 15 | 16 | // Services - a public function for searching services with keyword 17 | func Services(ctx context.Context, opt *options.SearchOptions, keyword string, wide bool) error { 18 | var serviceInfo string 19 | 20 | serviceList, err := utils.ServiceList(ctx, opt) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | if len(serviceList.Items) == 0 { 26 | ns := opt.Namespace 27 | if opt.AllNamespaces { 28 | fmt.Println("No resources found.") 29 | } else { 30 | if ns == "" { 31 | ns = "default" 32 | } 33 | fmt.Printf("No resources found in %s namespace.\n", ns) 34 | } 35 | return nil 36 | } 37 | 38 | buf := bytes.NewBuffer(nil) 39 | w := tabwriter.NewWriter(buf, 0, 0, 3, ' ', 0) 40 | 41 | if wide { 42 | fmt.Fprintln(w, constants.ServicesHeaderWide) 43 | } else { 44 | fmt.Fprintln(w, constants.ServicesHeader) 45 | } 46 | for _, s := range serviceList.Items { 47 | var ports []string 48 | 49 | if !utils.MatchesKeyword(s.Name, keyword, opt.InvertMatch) { 50 | continue 51 | } 52 | 53 | if utils.ShouldExcludeResource(s.Name, opt.ExcludePattern) { 54 | continue 55 | } 56 | 57 | age := utils.GetAge(time.Since(s.CreationTimestamp.Time)) 58 | 59 | for _, p := range s.Spec.Ports { 60 | var concatenated string 61 | if p.NodePort != 0 { 62 | concatenated = fmt.Sprintf("%d:%d/%s", p.Port, p.NodePort, p.Protocol) 63 | } else { 64 | concatenated = fmt.Sprintf("%d/%s", p.Port, p.Protocol) 65 | } 66 | ports = append(ports, concatenated) 67 | } 68 | 69 | var selectors []string 70 | var selector string 71 | if s.Spec.Selector != nil { 72 | for k, v := range s.Spec.Selector { 73 | selector = fmt.Sprintf("%s=%s", k, v) 74 | selectors = append(selectors, selector) 75 | } 76 | } 77 | selectorOutput := "" 78 | if len(selectors) > 0 { 79 | selectorOutput = strings.Join(selectors, ",") 80 | } 81 | 82 | var externalIPs []string 83 | if s.Spec.ExternalIPs == nil { 84 | for _, i := range s.Status.LoadBalancer.Ingress { 85 | if i.Hostname != "" { 86 | externalIPs = append(externalIPs, i.Hostname) 87 | } else if i.IP != "" { 88 | externalIPs = append(externalIPs, i.IP) 89 | } 90 | } 91 | } else { 92 | externalIPs = s.Spec.ExternalIPs 93 | } 94 | 95 | var externalIPsDisplay string = "" 96 | if len(externalIPs) > 0 { 97 | externalIPsDisplay = strings.Join(externalIPs, ",") 98 | } 99 | 100 | if wide { 101 | serviceInfo = fmt.Sprintf(constants.ServicesRowTemplateWide, 102 | s.Namespace, 103 | s.Name, 104 | s.Spec.Type, 105 | s.Spec.ClusterIP, 106 | externalIPsDisplay, 107 | strings.Join(ports, ","), 108 | age, 109 | selectorOutput, 110 | ) 111 | } else { 112 | serviceInfo = fmt.Sprintf(constants.ServicesRowTemplate, 113 | s.Namespace, 114 | s.Name, 115 | s.Spec.Type, 116 | s.Spec.ClusterIP, 117 | externalIPsDisplay, 118 | strings.Join(ports, ","), 119 | age, 120 | ) 121 | } 122 | 123 | fmt.Fprintln(w, serviceInfo) 124 | } 125 | w.Flush() 126 | 127 | fmt.Printf("%s", buf.String()) 128 | 129 | return nil 130 | } 131 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/spf13/cobra" 12 | 13 | "github.com/guessi/kubectl-grep/pkg/options" 14 | ) 15 | 16 | var ( 17 | output string 18 | rootCmdDescriptionShort = "Filter Kubernetes resources by matching their names" 19 | rootCmdDescriptionLong = `Filter Kubernetes resources by matching their names 20 | 21 | More info: https://github.com/guessi/kubectl-grep 22 | ` 23 | 24 | rootCmdExamples = ` 25 | List all pods in default namespace 26 | $ kubectl grep pods 27 | 28 | List all pods in all namespaces 29 | $ kubectl grep pods -A 30 | 31 | List all pods in namespace "start-lab" which contains keyword "flash" 32 | $ kubectl grep pods -n star-lab flash 33 | ` 34 | ) 35 | 36 | // generic search options handler 37 | var searchOptions = options.NewSearchOptions() 38 | 39 | // rootCmd represents the base command when called without any subcommands 40 | var rootCmd = &cobra.Command{ 41 | Use: "kubectl-grep", 42 | Short: rootCmdDescriptionShort, 43 | Long: rootCmdDescriptionLong, 44 | Example: rootCmdExamples, 45 | } 46 | 47 | func init() { 48 | // Global Flags 49 | rootCmd.PersistentFlags().StringVarP( 50 | &searchOptions.Namespace, "namespace", "n", "", 51 | "Namespace for search. (default: \"default\")") 52 | rootCmd.PersistentFlags().BoolVarP( 53 | &searchOptions.AllNamespaces, "all-namespaces", "A", false, 54 | "If present, list the requested object(s) across all namespaces.") 55 | rootCmd.PersistentFlags().StringVarP( 56 | &searchOptions.Selector, "selector", "l", "", 57 | "Selector (label query) to filter on. (e.g. -l key1=value1,key2=value2)") 58 | rootCmd.PersistentFlags().StringVar( 59 | &searchOptions.FieldSelector, "field-selector", "", 60 | "Selector (field query) to filter on. (e.g. --field-selector key1=value1,key2=value2)") 61 | rootCmd.PersistentFlags().BoolVarP( 62 | &searchOptions.InvertMatch, "invert-match", "v", false, 63 | "If present, filter out those not matching the specified patterns") 64 | rootCmd.PersistentFlags().StringVarP( 65 | &searchOptions.ExcludePattern, "exclude", "x", "", 66 | "If present, exclude those with specified pattern (comma-separated string)") 67 | rootCmd.PersistentFlags().DurationVarP( 68 | &searchOptions.Timeout, "timeout", "t", 30*time.Second, 69 | "Timeout for Kubernetes API calls (default: 30s)") 70 | } 71 | 72 | // createContextWithTimeout creates a context with timeout and cancellation support 73 | func createContextWithTimeout() (context.Context, context.CancelFunc) { 74 | // Create context with timeout 75 | ctx, cancel := context.WithTimeout(context.Background(), searchOptions.Timeout) 76 | 77 | // Handle interrupt signals for graceful cancellation 78 | c := make(chan os.Signal, 1) 79 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 80 | 81 | go func() { 82 | select { 83 | case <-c: 84 | fmt.Fprintln(os.Stderr, "\nOperation cancelled by user") 85 | cancel() 86 | case <-ctx.Done(): 87 | // Context already done, cleanup signal handler 88 | } 89 | signal.Stop(c) 90 | }() 91 | 92 | return ctx, cancel 93 | } 94 | 95 | // Execute adds all child commands to the root command and sets flags appropriately. 96 | // This is called by main.main(). It only needs to happen once to the rootCmd. 97 | func Execute() { 98 | if err := rootCmd.Execute(); err != nil { 99 | fmt.Println(err) 100 | os.Exit(1) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /pkg/resources/hpas.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "strings" 8 | "text/tabwriter" 9 | "time" 10 | 11 | "github.com/guessi/kubectl-grep/pkg/constants" 12 | "github.com/guessi/kubectl-grep/pkg/options" 13 | "github.com/guessi/kubectl-grep/pkg/utils" 14 | autoscalingv2 "k8s.io/api/autoscaling/v2" 15 | ) 16 | 17 | // Hpas - a public function for searching hpas with keyword 18 | func Hpas(ctx context.Context, opt *options.SearchOptions, keyword string) error { 19 | hpaList, err := utils.HpaList(ctx, opt) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | if len(hpaList.Items) == 0 { 25 | ns := opt.Namespace 26 | if opt.AllNamespaces { 27 | fmt.Println("No resources found.") 28 | } else { 29 | if ns == "" { 30 | ns = "default" 31 | } 32 | fmt.Printf("No resources found in %s namespace.\n", ns) 33 | } 34 | return nil 35 | } 36 | 37 | buf := bytes.NewBuffer(nil) 38 | w := tabwriter.NewWriter(buf, 0, 0, 3, ' ', 0) 39 | 40 | fmt.Fprintln(w, constants.HpaHeader) 41 | for _, h := range hpaList.Items { 42 | if !utils.MatchesKeyword(h.Name, keyword, opt.InvertMatch) { 43 | continue 44 | } 45 | 46 | if utils.ShouldExcludeResource(h.Name, opt.ExcludePattern) { 47 | continue 48 | } 49 | 50 | var age string = utils.GetAge(time.Since(h.CreationTimestamp.Time)) 51 | 52 | var targetCPUUtilization string = constants.UNKNOWN 53 | var targetMemoryUtilization string = constants.UNKNOWN 54 | 55 | var currentCPUUtilization string = constants.UNKNOWN 56 | var currentMemoryUtilization string = constants.UNKNOWN 57 | 58 | // Process target metrics 59 | for _, metric := range h.Spec.Metrics { 60 | if metric.Type == autoscalingv2.ResourceMetricSourceType { 61 | switch metric.Resource.Name { 62 | case "cpu": 63 | targetCPUUtilization = utils.FormatUtilization(metric.Resource.Target.AverageUtilization) 64 | case "memory": 65 | targetMemoryUtilization = utils.FormatUtilization(metric.Resource.Target.AverageUtilization) 66 | } 67 | } 68 | } 69 | 70 | // Process current metrics 71 | for _, metric := range h.Status.CurrentMetrics { 72 | if metric.Type == autoscalingv2.ResourceMetricSourceType { 73 | switch metric.Resource.Name { 74 | case "cpu": 75 | currentCPUUtilization = utils.FormatUtilization(metric.Resource.Current.AverageUtilization) 76 | case "memory": 77 | currentMemoryUtilization = utils.FormatUtilization(metric.Resource.Current.AverageUtilization) 78 | } 79 | } 80 | } 81 | 82 | var targetsFieldInfo string 83 | var metrics []string 84 | 85 | if targetCPUUtilization != constants.UNKNOWN { 86 | metrics = append(metrics, fmt.Sprintf("cpu: %s/%s", currentCPUUtilization, targetCPUUtilization)) 87 | } 88 | if targetMemoryUtilization != constants.UNKNOWN { 89 | metrics = append(metrics, fmt.Sprintf("memory: %s/%s", currentMemoryUtilization, targetMemoryUtilization)) 90 | } 91 | if len(metrics) > 0 { 92 | targetsFieldInfo = fmt.Sprintf("%s", strings.Join(metrics, ", ")) 93 | } 94 | 95 | hpaInfo := fmt.Sprintf(constants.HpaRowTemplate, 96 | h.Namespace, 97 | h.Name, 98 | h.Spec.ScaleTargetRef.Kind, 99 | h.Spec.ScaleTargetRef.Name, 100 | targetsFieldInfo, 101 | *h.Spec.MinReplicas, 102 | h.Spec.MaxReplicas, 103 | h.Status.CurrentReplicas, 104 | age, 105 | ) 106 | fmt.Fprintln(w, hpaInfo) 107 | } 108 | w.Flush() 109 | 110 | fmt.Printf("%s", buf.String()) 111 | 112 | return nil 113 | } 114 | -------------------------------------------------------------------------------- /pkg/resources/daemonsets.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "strings" 8 | "text/tabwriter" 9 | "time" 10 | 11 | "github.com/guessi/kubectl-grep/pkg/constants" 12 | "github.com/guessi/kubectl-grep/pkg/options" 13 | "github.com/guessi/kubectl-grep/pkg/utils" 14 | ) 15 | 16 | // Daemonsets - a public function for searching daemonsets with keyword 17 | func Daemonsets(ctx context.Context, opt *options.SearchOptions, keyword string, wide bool) error { 18 | var daemonsetInfo string 19 | 20 | daemonsetList, err := utils.DaemonSetList(ctx, opt) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | if len(daemonsetList.Items) == 0 { 26 | ns := opt.Namespace 27 | if opt.AllNamespaces { 28 | fmt.Println("No resources found.") 29 | } else { 30 | if ns == "" { 31 | ns = "default" 32 | } 33 | fmt.Printf("No resources found in %s namespace.\n", ns) 34 | } 35 | return nil 36 | } 37 | 38 | buf := bytes.NewBuffer(nil) 39 | w := tabwriter.NewWriter(buf, 0, 0, 3, ' ', 0) 40 | 41 | if wide { 42 | fmt.Fprintln(w, constants.DaemonsetHeaderWide) 43 | } else { 44 | fmt.Fprintln(w, constants.DaemonsetHeader) 45 | } 46 | 47 | for _, d := range daemonsetList.Items { 48 | if !utils.MatchesKeyword(d.Name, keyword, opt.InvertMatch) { 49 | continue 50 | } 51 | 52 | if utils.ShouldExcludeResource(d.Name, opt.ExcludePattern) { 53 | continue 54 | } 55 | 56 | age := utils.GetAge(time.Since(d.CreationTimestamp.Time)) 57 | containers := d.Spec.Template.Spec.Containers 58 | 59 | var nodeSelectors []string 60 | var nodeSelector string 61 | if d.Spec.Template.Spec.NodeSelector != nil { 62 | for k, v := range d.Spec.Template.Spec.NodeSelector { 63 | nodeSelector = fmt.Sprintf("%s=%s", k, v) 64 | nodeSelectors = append(nodeSelectors, nodeSelector) 65 | } 66 | } 67 | nodeSelectorOutput := "" 68 | if len(nodeSelectors) > 0 { 69 | nodeSelectorOutput = strings.Join(nodeSelectors, ",") 70 | } 71 | 72 | var selectors []string 73 | var selector string 74 | if d.Spec.Selector.MatchLabels != nil { 75 | for k, v := range d.Spec.Selector.MatchLabels { 76 | selector = fmt.Sprintf("%s=%s", k, v) 77 | selectors = append(selectors, selector) 78 | } 79 | } 80 | selectorOutput := "" 81 | if len(selectors) > 0 { 82 | selectorOutput = strings.Join(selectors, ",") 83 | } 84 | 85 | if wide { 86 | names := []string{} 87 | images := []string{} 88 | 89 | for _, n := range containers { 90 | names = append(names, n.Name) 91 | images = append(images, n.Image) 92 | } 93 | 94 | daemonsetInfo = fmt.Sprintf(constants.DaemonsetRowTemplateWide, 95 | d.Namespace, 96 | d.Name, 97 | d.Status.DesiredNumberScheduled, 98 | d.Status.CurrentNumberScheduled, 99 | d.Status.NumberReady, 100 | d.Status.UpdatedNumberScheduled, 101 | d.Status.NumberAvailable, 102 | nodeSelectorOutput, 103 | age, 104 | strings.Join(names, ","), 105 | strings.Join(images, ","), 106 | selectorOutput, 107 | ) 108 | } else { 109 | daemonsetInfo = fmt.Sprintf(constants.DaemonsetRowTemplate, 110 | d.Namespace, 111 | d.Name, 112 | d.Status.DesiredNumberScheduled, 113 | d.Status.CurrentNumberScheduled, 114 | d.Status.NumberReady, 115 | d.Status.UpdatedNumberScheduled, 116 | d.Status.NumberAvailable, 117 | nodeSelectorOutput, 118 | age, 119 | ) 120 | } 121 | fmt.Fprintln(w, daemonsetInfo) 122 | } 123 | w.Flush() 124 | 125 | fmt.Printf("%s", buf.String()) 126 | 127 | return nil 128 | } 129 | -------------------------------------------------------------------------------- /pkg/resources/storages.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "strings" 8 | "text/tabwriter" 9 | "time" 10 | 11 | "github.com/guessi/kubectl-grep/pkg/constants" 12 | "github.com/guessi/kubectl-grep/pkg/options" 13 | "github.com/guessi/kubectl-grep/pkg/utils" 14 | ) 15 | 16 | // CsiDrivers - a public function for searching csidrivers with keyword 17 | func CsiDrivers(ctx context.Context, opt *options.SearchOptions, keyword string) error { 18 | var csiDriverInfo string 19 | 20 | csiDriverList, err := utils.CsiDriverList(ctx, opt) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | if len(csiDriverList.Items) == 0 { 26 | ns := opt.Namespace 27 | if opt.AllNamespaces { 28 | fmt.Println("No resources found.") 29 | } else { 30 | if ns == "" { 31 | ns = "default" 32 | } 33 | fmt.Printf("No resources found in %s namespace.\n", ns) 34 | } 35 | return nil 36 | } 37 | 38 | buf := bytes.NewBuffer(nil) 39 | w := tabwriter.NewWriter(buf, 0, 0, 3, ' ', 0) 40 | 41 | fmt.Fprintln(w, constants.CsiDriversHeader) 42 | 43 | for _, s := range csiDriverList.Items { 44 | if !utils.MatchesKeyword(s.Name, keyword, opt.InvertMatch) { 45 | continue 46 | } 47 | 48 | if utils.ShouldExcludeResource(s.Name, opt.ExcludePattern) { 49 | continue 50 | } 51 | 52 | age := utils.GetAge(time.Since(s.CreationTimestamp.Time)) 53 | 54 | var tokenRequest string 55 | if len(s.Spec.TokenRequests) <= 0 { 56 | tokenRequest = "" 57 | } 58 | 59 | var modes []string 60 | for _, m := range s.Spec.VolumeLifecycleModes { 61 | modes = append(modes, string(m)) 62 | } 63 | 64 | csiDriverInfo = fmt.Sprintf(constants.CsiDriversRowTemplate, 65 | s.Name, 66 | utils.BoolString(s.Spec.AttachRequired), 67 | utils.BoolString(s.Spec.PodInfoOnMount), 68 | utils.BoolString(s.Spec.StorageCapacity), 69 | tokenRequest, 70 | utils.BoolString(s.Spec.RequiresRepublish), 71 | strings.Join(modes, ","), 72 | age, 73 | ) 74 | 75 | fmt.Fprintln(w, csiDriverInfo) 76 | } 77 | w.Flush() 78 | 79 | fmt.Printf("%s", buf.String()) 80 | 81 | return nil 82 | } 83 | 84 | // StorageClasses - a public function for searching storageclasses with keyword 85 | func StorageClasses(ctx context.Context, opt *options.SearchOptions, keyword string) error { 86 | var storageClassInfo string 87 | 88 | storageClassList, err := utils.StorageClassList(ctx, opt) 89 | if err != nil { 90 | return err 91 | } 92 | 93 | buf := bytes.NewBuffer(nil) 94 | w := tabwriter.NewWriter(buf, 0, 0, 3, ' ', 0) 95 | 96 | fmt.Fprintln(w, constants.StorageClassesHeader) 97 | 98 | for _, s := range storageClassList.Items { 99 | // return all storages under namespace if no keyword specific 100 | if len(keyword) > 0 { 101 | match := strings.Contains(s.Name, keyword) 102 | if match == opt.InvertMatch { 103 | continue 104 | } 105 | } 106 | 107 | age := utils.GetAge(time.Since(s.CreationTimestamp.Time)) 108 | 109 | var isDefaultClass string 110 | for k, v := range s.Annotations { 111 | if k == "storageclass.kubernetes.io/is-default-class" && v == "true" { 112 | isDefaultClass = "(default)" 113 | break 114 | } 115 | } 116 | 117 | storageClassInfo = fmt.Sprintf(constants.StorageClassesRowTemplate, 118 | s.Name, 119 | isDefaultClass, 120 | s.Provisioner, 121 | *(s.ReclaimPolicy), 122 | *(s.VolumeBindingMode), 123 | utils.BoolString(s.AllowVolumeExpansion), 124 | age, 125 | ) 126 | 127 | fmt.Fprintln(w, storageClassInfo) 128 | } 129 | w.Flush() 130 | 131 | fmt.Printf("%s", buf.String()) 132 | 133 | return nil 134 | } 135 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: staticcheck dependency clean build release all 2 | 3 | PKGS := $(shell go list ./...) 4 | REPO := github.com/guessi/kubectl-grep 5 | BUILDTIME := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") 6 | GITVERSION := $(shell git describe --tags --abbrev=8) 7 | GOVERSION := $(shell go version | cut -d' ' -f3) 8 | LDFLAGS := -s -w -X "$(REPO)/cmd.gitVersion=$(GITVERSION)" -X "$(REPO)/cmd.goVersion=$(GOVERSION)" -X "$(REPO)/cmd.buildTime=$(BUILDTIME)" 9 | 10 | default: build 11 | 12 | staticcheck: 13 | @echo "Setup staticcheck..." 14 | @go install honnef.co/go/tools/cmd/staticcheck@2025.1.1 # https://github.com/dominikh/go-tools/releases/tag/2025.1.1 15 | @echo "Check staticcheck version..." 16 | staticcheck --version 17 | @echo "Run staticcheck..." 18 | @for i in $(PKGS); do echo $${i}; staticcheck $${i}; done 19 | 20 | test: 21 | go version 22 | go fmt ./... 23 | go vet ./... 24 | # go test -v ./... 25 | 26 | dependency: 27 | go mod download 28 | 29 | build-linux-x86_64: 30 | @echo "Creating Build for Linux (x86_64)..." 31 | @CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o ./releases/$(GITVERSION)/Linux-x86_64/kubectl-grep 32 | @cp ./LICENSE ./releases/$(GITVERSION)/Linux-x86_64/LICENSE 33 | @tar zcf ./releases/$(GITVERSION)/kubectl-grep-Linux-x86_64.tar.gz -C releases/$(GITVERSION)/Linux-x86_64 kubectl-grep LICENSE 34 | 35 | build-linux-arm64: 36 | @echo "Creating Build for Linux (arm64)..." 37 | @CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o ./releases/$(GITVERSION)/Linux-arm64/kubectl-grep 38 | @cp ./LICENSE ./releases/$(GITVERSION)/Linux-arm64/LICENSE 39 | @tar zcf ./releases/$(GITVERSION)/kubectl-grep-Linux-arm64.tar.gz -C releases/$(GITVERSION)/Linux-arm64 kubectl-grep LICENSE 40 | 41 | build-darwin-x86_64: 42 | @echo "Creating Build for macOS (x86_64)..." 43 | @CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o ./releases/$(GITVERSION)/Darwin-x86_64/kubectl-grep 44 | @cp ./LICENSE ./releases/$(GITVERSION)/Darwin-x86_64/LICENSE 45 | @tar zcf ./releases/$(GITVERSION)/kubectl-grep-Darwin-x86_64.tar.gz -C releases/$(GITVERSION)/Darwin-x86_64 kubectl-grep LICENSE 46 | 47 | build-darwin-arm64: 48 | @echo "Creating Build for macOS (arm64)..." 49 | @CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o ./releases/$(GITVERSION)/Darwin-arm64/kubectl-grep 50 | @cp ./LICENSE ./releases/$(GITVERSION)/Darwin-arm64/LICENSE 51 | @tar zcf ./releases/$(GITVERSION)/kubectl-grep-Darwin-arm64.tar.gz -C releases/$(GITVERSION)/Darwin-arm64 kubectl-grep LICENSE 52 | 53 | build-windows-x86_64: 54 | @echo "Creating Build for Windows (x86_64)..." 55 | @CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o ./releases/$(GITVERSION)/Windows-x86_64/kubectl-grep.exe 56 | @cp ./LICENSE ./releases/$(GITVERSION)/Windows-x86_64/LICENSE.txt 57 | @tar zcf ./releases/$(GITVERSION)/kubectl-grep-Windows-x86_64.tar.gz -C releases/$(GITVERSION)/Windows-x86_64 kubectl-grep.exe LICENSE.txt 58 | 59 | build-linux: build-linux-x86_64 build-linux-arm64 60 | build-darwin: build-darwin-x86_64 build-darwin-arm64 61 | build-windows: build-windows-x86_64 62 | 63 | build: build-linux build-darwin build-windows 64 | 65 | clean: 66 | @echo "Cleanup Releases..." 67 | rm -rvf ./releases/* 68 | 69 | release: 70 | @echo "Creating Releases..." 71 | @curl -LO https://github.com/tcnksm/ghr/releases/download/v0.17.0/ghr_v0.17.0_linux_amd64.tar.gz 72 | @tar --strip-components=1 -xvf ghr_v0.17.0_linux_amd64.tar.gz ghr_v0.17.0_linux_amd64/ghr 73 | ./ghr -version 74 | ./ghr -replace -recreate -token ${GITHUB_TOKEN} $(GITVERSION) releases/$(GITVERSION)/ 75 | sha1sum releases/$(GITVERSION)/*.tar.gz > releases/$(GITVERSION)/SHA1SUM 76 | 77 | all: staticcheck dependency clean build 78 | -------------------------------------------------------------------------------- /pkg/constants/constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | // common 5 | UNKNOWN = "" 6 | 7 | // apps/v1 8 | DaemonsetHeader = "NAMESPACE\tNAME\tDESIRED\tCURRENT\tREADY\tUP-TO-DATE\tAVAILABLE\tNODE SELECTOR\tAGE" 9 | DaemonsetRowTemplate = "%s\t%s\t%d\t%d\t%d\t%d\t%d\t%s\t%s" 10 | 11 | DaemonsetHeaderWide = "NAMESPACE\tNAME\tDESIRED\tCURRENT\tREADY\tUP-TO-DATE\tAVAILABLE\tNODE SELECTOR\tAGE\tCONTAINERS\tIMAGES\tSELECTOR" 12 | DaemonsetRowTemplateWide = "%s\t%s\t%d\t%d\t%d\t%d\t%d\t%s\t%s\t%s\t%s\t%s" 13 | 14 | DeploymentHeader = "NAMESPACE\tNAME\tREADY\tUP-TO-DATE\tAVAILABLE\tAGE" 15 | DeploymentRowTemplate = "%s\t%s\t%d/%d\t%d\t%d\t%s" 16 | 17 | DeploymentHeaderWide = "NAMESPACE\tNAME\tREADY\tUP-TO-DATE\tAVAILABLE\tAGE\tCONTAINERS\tIMAGES\tSELECTOR" 18 | DeploymentRowTemplateWide = "%s\t%s\t%d/%d\t%d\t%d\t%s\t%s\t%s\t%s" 19 | 20 | ReplicasetHeader = "NAMESPACE\tNAME\tDESIRED\tCURRENT\tREADY\tAGE" 21 | ReplicasetRowTemplate = "%s\t%s\t%d\t%d\t%d\t%s" 22 | 23 | ReplicasetHeaderWide = "NAMESPACE\tNAME\tDESIRED\tCURRENT\tREADY\tAGE\tCONTAINERS\tIMAGES\tSELECTOR" 24 | ReplicasetRowTemplateWide = "%s\t%s\t%d\t%d\t%d\t%s\t%s\t%s\t%s" 25 | 26 | StatefulsetHeader = "NAMESPACE\tNAME\tREADY\tAGE" 27 | StatefulsetRowTemplate = "%s\t%s\t%d/%d\t%s" 28 | 29 | StatefulsetHeaderWide = "NAMESPACE\tNAME\tREADY\tAGE\tCONTAINERS\tIMAGES" 30 | StatefulsetRowTemplateWide = "%s\t%s\t%d/%d\t%s\t%s\t%s" 31 | 32 | // autoscaling/v1 33 | HpaHeader = "NAMESPACE\tNAME\tREFERENCE\tTARGETS\tMINPODS\tMAXPODS\tREPLICAS\tAGE" 34 | HpaRowTemplate = "%s\t%s\t%s/%s\t%s\t%d\t%d\t%d\t%s" 35 | 36 | // batch/v1 37 | CronJobsHeader = "NAMESPACE\tNAME\tSCHEDULE\tSUSPEND\tACTIVE\tLAST SCHEDULE\tAGE" 38 | CronJobsRowTemplate = "%s\t%s\t%s\t%s\t%d\t%s\t%s" 39 | 40 | JobsHeader = "NAMESPACE\tNAME\tCOMPLETIONS\tDURATION\tAGE" 41 | JobsRowTemplate = "%s\t%s\t%d/%d\t%s\t%s" 42 | 43 | // networking.k8s.io/v1 44 | IngressHeader = "NAMESPACE\tNAME\tCLASS\tHOSTS\tADDRESS\tPORTS\tAGE" 45 | IngressRowTemplate = "%s\t%s\t%s\t%s\t%s\t%s\t%s" 46 | 47 | // rbac.authorization.k8s.io/v1 48 | RolesHeader = "NAMESPACE\tNAME\tCREATED AT" 49 | RolesRowTemplate = "%s\t%s\t%s" 50 | 51 | RoleBindingsHeader = "NAMESPACE\tNAME\tROLE\tAGE" 52 | RoleBindingsRowTemplate = "%s\t%s\t%s\t%s" 53 | 54 | ClusterRolesHeader = "NAME\tCREATED AT" 55 | ClusterRolesRowTemplate = "%s\t%s" 56 | 57 | ClusterRoleBindingsHeader = "NAME\tROLE\tAGE" 58 | ClusterRoleBindingsRowTemplate = "%s\t%s\t%s" 59 | 60 | // storage.k8s.io/v1 61 | CsiDriversHeader = "NAME\tATTACHREQUIRED\tPODINFOONMOUNT\tSTORAGECAPACITY\tTOKENREQUESTS\tREQUIRESREPUBLISH\tMODES\tAGE" 62 | CsiDriversRowTemplate = "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s" 63 | 64 | StorageClassesRowTemplate = "%s %s\t%s\t%s\t%s\t%s\t%s" 65 | StorageClassesHeader = "NAME\tPROVISIONER\tRECLAIMPOLICY\tVOLUMEBINDINGMODE\tALLOWVOLUMEEXPANSION\tAGE" 66 | 67 | // v1 68 | ConfigMapHeader = "NAMESPACE\tNAME\tDATA\tAGE" 69 | ConfigMapRowTemplate = "%s\t%s\t%d\t%s" 70 | 71 | NodeHeader = "NAME\tSTATUS\tROLES\tAGE\tVERSION" 72 | NodeRowTemplate = "%s\t%s\t%s\t%s\t%s" 73 | 74 | NodeHeaderWide = "NAME\tSTATUS\tROLES\tAGE\tVERSION\tINTERNAL-IP\tEXTERNAL-IP\tOS-IMAGE\tKERNEL-VERSION\tCONTAINER-RUNTIME" 75 | NodeRowTemplateWide = "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s" 76 | 77 | PodHeader = "NAMESPACE\tNAME\tREADY\tSTATUS\tRESTARTS\tAGE" 78 | PodRowTemplate = "%s\t%s\t%d/%d\t%s\t%d\t%s" 79 | 80 | PodHeaderWide = "NAMESPACE\tNAME\tREADY\tSTATUS\tRESTARTS\tAGE\tIP\tNODENAME" 81 | PodRowTemplateWide = "%s\t%s\t%d/%d\t%s\t%d\t%s\t%s\t%s" 82 | 83 | SecretHeader = "NAMESPACE\tNAME\tTYPE\tDATA\tAGE" 84 | SecretRowTemplate = "%s\t%s\t%s\t%d\t%s" 85 | 86 | ServiceAccountsHeader = "NAMESPACE\tNAME\tSECRETS\tAGE" 87 | ServiceAccountsRowTemplate = "%s\t%s\t%d\t%s" 88 | 89 | ServicesHeaderWide = "NAMESPACE\tNAME\tTYPE\tCLUSTER-IP\tEXTERNAL-IP\tPORT(S)\tAGE\tSELECTOR" 90 | ServicesRowTemplateWide = "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s" 91 | 92 | ServicesHeader = "NAMESPACE\tNAME\tTYPE\tCLUSTER-IP\tEXTERNAL-IP\tPORT(S)\tAGE" 93 | ServicesRowTemplate = "%s\t%s\t%s\t%s\t%s\t%s\t%s" 94 | ) 95 | -------------------------------------------------------------------------------- /cmd/resources.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/guessi/kubectl-grep/pkg/resources" 9 | "github.com/guessi/kubectl-grep/pkg/utils" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var ( 15 | // apps/v1 16 | daemonsetsCmd = &cobra.Command{ 17 | Use: "daemonsets", 18 | Aliases: []string{"ds", "daemonset"}, 19 | Short: "Search Daemonsets by keyword, by namespace", 20 | Run: func(cmd *cobra.Command, args []string) { 21 | resourceSearch(args, "daemonsets") 22 | }, 23 | } 24 | deploymentsCmd = &cobra.Command{ 25 | Use: "deployments", 26 | Aliases: []string{"deploy", "deployment"}, 27 | Short: "Search Deployments by keyword, by namespace", 28 | Run: func(cmd *cobra.Command, args []string) { 29 | resourceSearch(args, "deployments") 30 | }, 31 | } 32 | replicasetsCmd = &cobra.Command{ 33 | Use: "replicasets", 34 | Aliases: []string{"rs", "replicaset"}, 35 | Short: "Search Replicasets by keyword, by namespace", 36 | Run: func(cmd *cobra.Command, args []string) { 37 | resourceSearch(args, "replicasets") 38 | }, 39 | } 40 | statefulsetsCmd = &cobra.Command{ 41 | Use: "statefulsets", 42 | Aliases: []string{"sts", "statefulset"}, 43 | Short: "Search Statefulsets by keyword, by namespace", 44 | Run: func(cmd *cobra.Command, args []string) { 45 | resourceSearch(args, "statefulsets") 46 | }, 47 | } 48 | 49 | // autoscaling/v1 50 | hpasCmd = &cobra.Command{ 51 | Use: "hpas", 52 | Aliases: []string{"hpa"}, 53 | Short: "Search HPAs by keyword, by namespace", 54 | Run: func(cmd *cobra.Command, args []string) { 55 | resourceSearch(args, "hpas") 56 | }, 57 | } 58 | 59 | // batch/v1 60 | cronjobsCmd = &cobra.Command{ 61 | Use: "cronjobs", 62 | Aliases: []string{"cj", "cronjob"}, 63 | Short: "Search CronJobs by keyword, by namespace", 64 | Run: func(cmd *cobra.Command, args []string) { 65 | resourceSearch(args, "cronjobs") 66 | }, 67 | } 68 | jobsCmd = &cobra.Command{ 69 | Use: "jobs", 70 | Aliases: []string{"job"}, 71 | Short: "Search Jobs by keyword, by namespace", 72 | Run: func(cmd *cobra.Command, args []string) { 73 | resourceSearch(args, "jobs") 74 | }, 75 | } 76 | 77 | // networking.k8s.io/v1 78 | ingressesCmd = &cobra.Command{ 79 | Use: "ingresses", 80 | Aliases: []string{"ing", "ingress"}, 81 | Short: "Search Ingresses by keyword, by namespace", 82 | Run: func(cmd *cobra.Command, args []string) { 83 | resourceSearch(args, "ingresses") 84 | }, 85 | } 86 | 87 | // rbac.authorization.k8s.io/v1 88 | rolesCmd = &cobra.Command{ 89 | Use: "roles", 90 | Aliases: []string{"role"}, 91 | Short: "Search Roles by keyword, by namespace", 92 | Run: func(cmd *cobra.Command, args []string) { 93 | resourceSearch(args, "roles") 94 | }, 95 | } 96 | 97 | roleBindingsCmd = &cobra.Command{ 98 | Use: "rolebindings", 99 | Aliases: []string{"rolebinding"}, 100 | Short: "Search RoleBindings by keyword, by namespace", 101 | Run: func(cmd *cobra.Command, args []string) { 102 | resourceSearch(args, "rolebindings") 103 | }, 104 | } 105 | 106 | clusterRolesCmd = &cobra.Command{ 107 | Use: "clusterroles", 108 | Aliases: []string{"clusterrole"}, 109 | Short: "Search ClusterRoles by keyword", 110 | Run: func(cmd *cobra.Command, args []string) { 111 | resourceSearch(args, "clusterroles") 112 | }, 113 | } 114 | 115 | clusterRoleBindingsCmd = &cobra.Command{ 116 | Use: "clusterrolebindings", 117 | Aliases: []string{"clusterrolebinding"}, 118 | Short: "Search ClusterRoleBindings by keyword", 119 | Run: func(cmd *cobra.Command, args []string) { 120 | resourceSearch(args, "clusterrolebindings") 121 | }, 122 | } 123 | 124 | // storage.k8s.io/v1 125 | csiDriversCmd = &cobra.Command{ 126 | Use: "csidrivers", 127 | Aliases: []string{"csidrivers"}, 128 | Short: "Search csidrivers by keyword", 129 | Run: func(cmd *cobra.Command, args []string) { 130 | resourceSearch(args, "csidrivers") 131 | }, 132 | } 133 | storageClassesCmd = &cobra.Command{ 134 | Use: "storageclasses", 135 | Aliases: []string{"storageclasses", "storageclasse", "sc"}, 136 | Short: "Search storageclasses by keyword", 137 | Run: func(cmd *cobra.Command, args []string) { 138 | resourceSearch(args, "storageclasses") 139 | }, 140 | } 141 | 142 | // v1 143 | configmapsCmd = &cobra.Command{ 144 | Use: "configmaps", 145 | Aliases: []string{"cm", "configmap"}, 146 | Short: "Search ConfigMaps by keyword, by namespace", 147 | Run: func(cmd *cobra.Command, args []string) { 148 | resourceSearch(args, "configmaps") 149 | }, 150 | } 151 | nodesCmd = &cobra.Command{ 152 | Use: "nodes", 153 | Aliases: []string{"no", "node", "nodes"}, 154 | Short: "Search Nodes by keyword", 155 | Run: func(cmd *cobra.Command, args []string) { 156 | resourceSearch(args, "nodes") 157 | }, 158 | } 159 | podsCmd = &cobra.Command{ 160 | Use: "pods", 161 | Aliases: []string{"po", "pod"}, 162 | Short: "Search Pods by keyword, by namespace", 163 | Run: func(cmd *cobra.Command, args []string) { 164 | resourceSearch(args, "pods") 165 | }, 166 | } 167 | secretsCmd = &cobra.Command{ 168 | Use: "secrets", 169 | Aliases: []string{"secret"}, 170 | Short: "Search Secrets by keyword, by namespace", 171 | Run: func(cmd *cobra.Command, args []string) { 172 | resourceSearch(args, "secrets") 173 | }, 174 | } 175 | serviceAccountsCmd = &cobra.Command{ 176 | Use: "serviceaccounts", 177 | Aliases: []string{"sa", "serviceaccount"}, 178 | Short: "Search ServiceAccounts by keyword, by namespace", 179 | Run: func(cmd *cobra.Command, args []string) { 180 | resourceSearch(args, "serviceaccounts") 181 | }, 182 | } 183 | servicesCmd = &cobra.Command{ 184 | Use: "services", 185 | Aliases: []string{"svc", "service"}, 186 | Short: "Search Services by keyword, by namespace", 187 | Run: func(cmd *cobra.Command, args []string) { 188 | resourceSearch(args, "services") 189 | }, 190 | } 191 | ) 192 | 193 | func init() { 194 | // apps/v1 195 | rootCmd.AddCommand(daemonsetsCmd) 196 | daemonsetsCmd.Flags().StringVarP(&output, "output", "o", "", "Output format.") 197 | 198 | rootCmd.AddCommand(deploymentsCmd) 199 | deploymentsCmd.Flags().StringVarP(&output, "output", "o", "", "Output format.") 200 | 201 | rootCmd.AddCommand(replicasetsCmd) 202 | replicasetsCmd.Flags().StringVarP(&output, "output", "o", "", "Output format.") 203 | 204 | rootCmd.AddCommand(statefulsetsCmd) 205 | statefulsetsCmd.Flags().StringVarP(&output, "output", "o", "", "Output format.") 206 | 207 | // autoscaling/v1 208 | rootCmd.AddCommand(hpasCmd) 209 | 210 | // batch/v1 211 | rootCmd.AddCommand(cronjobsCmd) 212 | rootCmd.AddCommand(jobsCmd) 213 | 214 | // networking.k8s.io/v1 215 | rootCmd.AddCommand(ingressesCmd) 216 | 217 | // rbac.authorization.k8s.io/v1 218 | rootCmd.AddCommand(rolesCmd) 219 | rootCmd.AddCommand(roleBindingsCmd) 220 | rootCmd.AddCommand(clusterRolesCmd) 221 | rootCmd.AddCommand(clusterRoleBindingsCmd) 222 | 223 | // storage.k8s.io/v1 224 | rootCmd.AddCommand(csiDriversCmd) 225 | 226 | rootCmd.AddCommand(storageClassesCmd) 227 | 228 | // v1 229 | rootCmd.AddCommand(configmapsCmd) 230 | 231 | rootCmd.AddCommand(nodesCmd) 232 | nodesCmd.Flags().StringVarP(&output, "output", "o", "", "Output format.") 233 | 234 | rootCmd.AddCommand(podsCmd) 235 | podsCmd.Flags().StringVarP(&output, "output", "o", "", "Output format.") 236 | 237 | rootCmd.AddCommand(secretsCmd) 238 | 239 | rootCmd.AddCommand(serviceAccountsCmd) 240 | 241 | rootCmd.AddCommand(servicesCmd) 242 | servicesCmd.Flags().StringVarP(&output, "output", "o", "", "Output format.") 243 | 244 | } 245 | 246 | func resourceSearch(args []string, resourceType string) { 247 | var keyword string 248 | 249 | if len(args) >= 1 && args[0] != "" { 250 | keyword = utils.TrimQuoteAndSpace(args[0]) 251 | } 252 | 253 | ctx, cancel := createContextWithTimeout() 254 | defer cancel() 255 | 256 | handleContextError := func(err error) { 257 | if err != nil { 258 | if ctx.Err() == context.DeadlineExceeded { 259 | fmt.Fprintf(os.Stderr, "Error: Operation timed out after %v\n", searchOptions.Timeout) 260 | os.Exit(1) 261 | } 262 | if ctx.Err() == context.Canceled { 263 | fmt.Fprintln(os.Stderr, "Error: Operation was cancelled") 264 | os.Exit(1) 265 | } 266 | } 267 | } 268 | 269 | var err error 270 | switch resourceType { 271 | // apps/v1 272 | case "daemonsets": 273 | err = resources.Daemonsets(ctx, searchOptions, keyword, output == "wide") 274 | case "deployments": 275 | err = resources.Deployments(ctx, searchOptions, keyword, output == "wide") 276 | case "replicasets": 277 | err = resources.Replicasets(ctx, searchOptions, keyword, output == "wide") 278 | case "statefulsets": 279 | err = resources.Statefulsets(ctx, searchOptions, keyword, output == "wide") 280 | 281 | // autoscaling/v1 282 | case "hpas": 283 | err = resources.Hpas(ctx, searchOptions, keyword) 284 | 285 | // batch/v1 286 | case "cronjobs": 287 | err = resources.CronJobs(ctx, searchOptions, keyword) 288 | case "jobs": 289 | err = resources.Jobs(ctx, searchOptions, keyword) 290 | 291 | // networking.k8s.io/v1 292 | case "ingresses": 293 | err = resources.Ingresses(ctx, searchOptions, keyword) 294 | 295 | // rbac.authorization.k8s.io/v1 296 | case "roles": 297 | err = resources.Roles(ctx, searchOptions, keyword) 298 | case "rolebindings": 299 | err = resources.RoleBindings(ctx, searchOptions, keyword) 300 | case "clusterroles": 301 | err = resources.ClusterRoles(ctx, searchOptions, keyword) 302 | case "clusterrolebindings": 303 | err = resources.ClusterRoleBindings(ctx, searchOptions, keyword) 304 | 305 | // storage.k8s.io/v1 306 | case "csidrivers": 307 | err = resources.CsiDrivers(ctx, searchOptions, keyword) 308 | case "storageclasses": 309 | err = resources.StorageClasses(ctx, searchOptions, keyword) 310 | 311 | // v1 312 | case "configmaps": 313 | err = resources.ConfigMaps(ctx, searchOptions, keyword) 314 | case "nodes": 315 | err = resources.Nodes(ctx, searchOptions, keyword, output == "wide") 316 | case "pods": 317 | err = resources.Pods(ctx, searchOptions, keyword, output == "wide") 318 | case "secrets": 319 | err = resources.Secrets(ctx, searchOptions, keyword) 320 | case "serviceaccounts": 321 | err = resources.ServiceAccounts(ctx, searchOptions, keyword) 322 | case "services": 323 | err = resources.Services(ctx, searchOptions, keyword, output == "wide") 324 | 325 | // default 326 | default: 327 | break 328 | } 329 | 330 | handleContextError(err) 331 | } 332 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v1.27.0 / 2025-12-11 2 | 3 | * Build with Kubernetes Client SDK v1.34.3 4 | * Bump dependencies 5 | 6 | # v1.26.0 / 2025-10-08 7 | 8 | * Build with Kubernetes Client SDK v1.34.1 9 | * Build with go1.25 10 | * Introduce Go Dependency Submission 11 | * Bump dependencies 12 | 13 | # v1.25.0 / 2025-08-10 14 | 15 | * Build with Kubernetes Client SDK v1.33.3 16 | * Switch to autoscaling/v2 17 | * Bump dependencies 18 | 19 | # v1.24.0 / 2025-06-28 20 | 21 | * Build with Kubernetes Client SDK v1.33.2 22 | * Add context handling support 23 | * Bump dependencies 24 | 25 | # v1.23.0 / 2025-06-01 26 | 27 | * Build with Kubernetes Client SDK v1.33.1 28 | * Bump dependencies 29 | 30 | # v1.22.0 / 2025-05-22 31 | 32 | * Build with go1.24.3 33 | * Prevent nil pointer dereferences 34 | 35 | # v1.21.0 / 2025-04-15 36 | 37 | * Implement --exclude/-x support 38 | * Build with go 1.24 39 | * Bump dependencies 40 | 41 | # v1.20.1 / 2025-03-13 42 | 43 | * Build with Kubernetes Client SDK v1.32.3 44 | * Bump dependencies 45 | 46 | # v1.20.0 / 2025-01-25 47 | 48 | * Build with Kubernetes Client SDK v1.32.1 49 | * Build with go 1.23 50 | * Bump dependencies 51 | 52 | # v1.19.2 / 2024-12-16 53 | 54 | * Build with Kubernetes Client SDK v1.31.3 55 | * Bump dependencies 56 | 57 | # v1.19.1 / 2024-11-12 58 | 59 | * Implement --invert-match/-v support 60 | * Bump dependencies 61 | 62 | # v1.19.0 / 2024-11-03 63 | 64 | * Build with Kubernetes Client SDK v1.31.2 65 | * Bump dependencies 66 | 67 | # v1.18.3 / 2024-09-30 68 | 69 | * Build with Kubernetes Client SDK v1.30.5 70 | * Bump dependencies 71 | 72 | # v1.18.2 / 2024-08-15 73 | 74 | * Build with Kubernetes Client SDK v1.30.4 75 | * Bump dependencies 76 | 77 | # v1.18.1 / 2024-08-02 78 | 79 | * Build with Kubernetes Client SDK v1.30.3 80 | * Bump dependencies 81 | 82 | # v1.18.0 / 2024-06-15 83 | 84 | * Build with go 1.22 85 | * Build with Kubernetes Client SDK v1.30.2 86 | * Bump github.com/spf13/cobra v1.8.1 87 | 88 | # v1.17.1 / 2024-04-26 89 | 90 | * Build with Kubernetes Client SDK v1.29.4 91 | * Bump dependencies 92 | 93 | # v1.17.0 / 2024-03-24 94 | 95 | * Build with Kubernetes Client SDK v1.29.3 96 | * Bump dependencies 97 | 98 | # v1.16.1 / 2024-02-16 99 | 100 | * Build with Kubernetes Client SDK v1.28.7 101 | * Bump actions/cache@v4 102 | 103 | # v1.16.0 / 2024-01-07 104 | 105 | * Build with Kubernetes Client SDK v1.28.5 106 | * Bump actions/steup-go@v5 107 | * Bump actions/stale@v9 108 | 109 | # v1.15.3 / 2023-11-19 110 | 111 | * Build with Kubernetes Client SDK v1.27.8 112 | * Bump github.com/spf13/cobra v1.8.0 113 | 114 | # v1.15.2 / 2023-11-04 115 | 116 | * Build with Kubernetes Client SDK v1.27.7 117 | * Bump actions/checkout@v4 118 | 119 | # v1.15.1 / 2023-09-29 120 | 121 | * Build with Kubernetes Client SDK v1.27.6 122 | 123 | # v1.15.0 / 2023-08-27 124 | 125 | * Upstream k8s 1.27 built with go 1.20 126 | * Build with Kubernetes Client SDK v1.27.5 127 | * More detail for staticcheck 128 | * Bump github.com/sirupsen/logrus v1.9.3 129 | * Bump github.com/spf13/cobra v1.7.0 130 | 131 | # v1.14.1 / 2023-08-18 132 | 133 | * Build with Kubernetes Client SDK v1.26.7 134 | 135 | # v1.14.0 / 2023-07-23 136 | 137 | * Build with Kubernetes Client SDK v1.26.6 138 | 139 | # v1.13.2 / 2023-06-15 140 | 141 | * Build with Kubernetes Client SDK v1.25.11 142 | 143 | # v1.13.1 / 2023-05-26 144 | 145 | * Build with Kubernetes Client SDK v1.25.10 146 | 147 | # v1.13.0 / 2023-04-19 148 | 149 | * Release with krew-release-bot v0.0.46 150 | * Build with Kubernetes Client SDK v1.25.9 151 | * Keep output aligned with kubectl and bug fixes 152 | * Trigger actions/stale once per week 153 | * Bump actions/stale@v8 154 | 155 | # v1.12.3 / 2023-03-26 156 | 157 | * Base: build with Kubernetes Client SDK v1.24.12 158 | * Misc: release with krew-release-bot v0.0.44 159 | * Misc: bump actions/setup-go@v4 160 | 161 | # v1.12.2 / 2023-03-12 162 | 163 | * Base: build with Kubernetes Client SDK v1.24.11 164 | * Address vulnerabilities fixes 165 | 166 | # v1.12.1 / 2023-02-18 167 | 168 | * Address vulnerabilities 169 | * https://pkg.go.dev/vuln/GO-2022-0619 170 | * https://pkg.go.dev/vuln/GO-2023-1571 171 | * https://pkg.go.dev/vuln/GO-2023-1570 172 | * https://groups.google.com/g/golang-announce/c/V0aBFqaFs_E 173 | 174 | # v1.12.0 / 2023-01-25 175 | 176 | * CI: upgrade actions/stale@v7 177 | * Base: build with Kubernetes Client SDK v1.24.10 and Golang 1.19 178 | * https://github.com/kubernetes/kubernetes/pull/113956 179 | * https://github.com/kubernetes/kubernetes/pull/115012 180 | 181 | # v1.11.0 / 2023-01-03 182 | 183 | * Release with GitHub Actions 184 | 185 | # v1.10.4 / 2022-12-11 186 | 187 | * Base: bump Kubernetes Client SDK v1.24.9 188 | * Mitigated CVEs: CVE-2022-41717, CVE-2022-27664 189 | 190 | # v1.10.3 / 2022-11-14 191 | 192 | * Base: bump Kubernetes Client SDK v1.24.8 193 | * Mitigated CVEs: [CVE-2022-3162](https://discuss.kubernetes.io/t/security-advisory-cve-2022-3162-unauthorized-read-of-custom-resources/21902), [CVE-2022-3294](https://discuss.kubernetes.io/t/security-advisory-cve-2022-3294-node-address-isnt-always-verified-when-proxying/21903) 194 | * Upgrade dependencies: 195 | - spf13/cobra v1.6.1 196 | 197 | # v1.10.2 / 2022-10-16 198 | 199 | * Bump Kubernetes Client SDK v1.24.7 200 | 201 | # v1.10.1 / 2022-09-17 202 | 203 | * Build with golang 1.18.x (CI) 204 | * Replace deprecated golint with Staticcheck 205 | * Download ghr binary directly 206 | 207 | # v1.10.0 / 2022-09-17 208 | 209 | * Bump Kubernetes Client SDK v1.24.5 210 | * Build with golang 1.18.x 211 | * Include more info for "version" command 212 | * Introduce stable bot 213 | 214 | **Known issue:** it is actually build with golang 1.17, please use v1.10.1 instead. 215 | 216 | # v1.9.0 / 2022-09-11 217 | 218 | * Added support for the following resources 219 | - ClusterRoleBindings 220 | - ClusterRoles 221 | - RoleBindings 222 | - Roles 223 | - ServiceAccounts 224 | 225 | # v1.8.0 / 2022-09-06 226 | 227 | * Initial support for arm-based Linux 228 | * Initial support for arm-based macOS (M1/M2-series) 229 | * Upgrade dependencies: 230 | - spf13/cobra v1.5.0 231 | - sirupsen/logrus v1.9.0 232 | 233 | # v1.7.2 / 2022-08-29 234 | 235 | * Bump Kubernetes Client SDK: kubernetes-1.23.10 236 | * Bump krew-release-bot v0.0.43 237 | 238 | # v1.7.1 / 2022-05-28 239 | 240 | * CVE-2022-28948 241 | 242 | # v1.7.0 / 2022-05-28 243 | 244 | * Bump Kubernetes Client SDK: kubernetes-1.23.7 245 | * Release with krew-release-bot v0.0.42 246 | * Keep output aligned when no resource found 247 | 248 | # v1.6.0 / 2022-03-22 249 | 250 | * Build with github.com/spf13/cobra v1.4.0 251 | * Build with next-gen convenience image: cimg/go:1.16 252 | * Added support for the following resources 253 | - CronJobs 254 | - ReplicaSets 255 | 256 | # v1.5.1 / 2022-03-19 257 | 258 | * Bump Kubernetes Client SDK: kubernetes-1.22.8 259 | * Cleanup go.mod 260 | 261 | # v1.5.0 / 2022-01-23 262 | 263 | * Bump Kubernetes Client SDK: kubernetes-1.22.6 264 | * Cleanup go.mod 265 | * Added support for the following resources 266 | - CSIDrivers 267 | - StorageClasses 268 | 269 | # v1.4.4 / 2022-01-05 270 | 271 | * Upgrade krew-release-bot to v0.0.40 272 | * Bump Kubernetes Client SDK: kubernetes-1.21.8 273 | * Upgrade dependencies: 274 | - spf13/cobra v1.3.0 275 | * Cleanup CircleCI configuration 276 | 277 | # v1.4.3 / 2021-11-21 278 | 279 | * Bump Kubernetes Client SDK: kubernetes-1.21.7 280 | 281 | # v1.4.2 / 2021-10-17 282 | 283 | * Fix incorrect node role display 284 | 285 | # v1.4.1 / 2021-10-16 286 | 287 | * Added support for the following resources 288 | - Services, thanks to @wshihadeh 289 | * Bump Kubernetes Client SDK: kubernetes-1.21.5 290 | 291 | # v1.4.0 / 2021-09-04 292 | 293 | * Bump Kubernetes Client SDK: kubernetes-1.21.4 294 | * Build with go 1.16 295 | 296 | # v1.3.3 / 2021-06-25 297 | 298 | * Bump Kubernetes Client SDK: kubernetes-1.20.8 299 | * Bump golang.org/x/crypto for CVE-2020-29652 300 | 301 | # v1.3.2 / 2021-03-26 302 | 303 | * NO CHANGE, to fix incorrect sha256sum on krew-index 304 | 305 | # v1.3.1 / 2021-03-24 306 | 307 | * Introduce krew-release-bot for release automation 308 | 309 | # v1.3.0 / 2021-03-15 310 | 311 | * Drop support for Kubernetes 1.16 or earlier 312 | * Bump Kubernetes Client SDK: kubernetes-1.20.4 313 | * Upgrade dependencies: 314 | - spf13/cobra v1.1.3 315 | - sirupsen/logrus v1.8.1 316 | * Build with go 1.15 317 | 318 | # v1.2.7 / 2020-10-25 319 | 320 | * Added support for the following resources 321 | - Jobs 322 | - Ingresses 323 | * Bump Kubernetes Client SDK: kubernetes-1.18.10 324 | * Upgrade dependencies: 325 | - spf13/cobra v1.1.1 326 | - sirupsen/logrus v1.7.0 327 | 328 | # v1.2.6 / 2020-08-08 329 | 330 | * Upgrade to Go 1.14 331 | * Bump Kubernetes Client SDK: kubernetes-1.18.6 332 | * Upgrade dependencies: 333 | - spf13/cobra v1.0.0 334 | - sirupsen/logrus v1.6.0 335 | 336 | # v1.2.5 / 2020-05-11 337 | 338 | * Bump Kubernetes Client SDK: kubernetes-1.18.2 339 | * Fix logic error for client init process 340 | 341 | # v1.2.4 / 2020-03-25 342 | 343 | * Bump Kubernetes Client SDK: kubernetes-1.16.8 344 | 345 | # v1.2.3 / 2020-01-13 346 | 347 | * Bump Kubernetes Client SDK: kubernetes-1.16.4 348 | * Added support for ConfigMaps search, with command aliases: 349 | - configmaps, configmap, cm 350 | * Added support for Secrets search, with command aliases: 351 | - secrets, secret 352 | 353 | # v1.2.2 / 2019-12-09 354 | 355 | * Release with LICENSE, resolved [#16](https://github.com/guessi/kubectl-grep/issues/16) 356 | 357 | # v1.2.1 / 2019-11-15 358 | 359 | * Bump Kubernetes Client SDK: kubernetes-1.15.6 360 | * Fix KUBECONFIG not working issue, @tsaarni thanks ([#14](https://github.com/guessi/kubectl-grep/pull/14)) 361 | * Fix namespace not respect the setting from KUBECONFIG, reported by @fredleger ([#13](https://github.com/guessi/kubectl-grep/issues/13), [#15](https://github.com/guessi/kubectl-grep/pull/15)) 362 | 363 | # v1.2.0 / 2019-09-15 364 | 365 | * Bump Kubernetes Client SDK: kubernetes-1.15.3 366 | * Upgrade to Go 1.13.0 367 | * Added support for StatefulSets search 368 | * Added support for `-A` as the shortcut of `--all-namespaces` 369 | * Added support for install via `kubectl krew install grep` 370 | * Fixed `--help` not work for the root command 371 | * Support for command aliases: 372 | - daemonsets, daemonset, ds 373 | - deployments, deployment, deploy 374 | - hpas, hpa 375 | - nodes, node, no 376 | - pods, pod, po 377 | - statefulsets, stateful, sts 378 | 379 | # v1.1.0 / 2019-07-19 380 | 381 | * Bump Kubernetes Client SDK: kubernetes-1.15.1 382 | * Cleanup go.mod / go.sum 383 | * *BREAKING CHANGE*: Renamed as `kubectl-grep` 384 | 385 | # v1.0.5 / 2019-05-12 386 | 387 | * Exit if `.kube/config` not found 388 | * Code refactoring 389 | 390 | # v1.0.4 / 2019-04-20 391 | 392 | * Added Support for Node Search 393 | * Bump Kubernetes Client SDK: kubernetes-1.14.1 394 | 395 | # v1.0.3 / 2019-04-07 396 | 397 | * Security Fixes for CVE-2019-1002101 and CVE-2019-9946 398 | 399 | # v1.0.2 / 2019-03-22 400 | 401 | * Supported NodeName Display for Pods 402 | * Supported Multi-Selector Display for DaemonSets 403 | * Don't Panic if Cluster Unreachable 404 | 405 | # v1.0.1 / 2019-03-17 406 | 407 | * Added Support for DaemonSets 408 | * Added Support for "-o wide" Option 409 | * Added Support for GO Modules 410 | 411 | # v1.0.0 / 2019-03-09 412 | 413 | * Initial Release 414 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= 2 | github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 4 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 8 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= 10 | github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 11 | github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= 12 | github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 13 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 14 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 15 | github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= 16 | github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= 17 | github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= 18 | github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= 19 | github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= 20 | github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= 21 | github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= 22 | github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= 23 | github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= 24 | github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= 25 | github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= 26 | github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= 27 | github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= 28 | github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= 29 | github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= 30 | github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= 31 | github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= 32 | github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= 33 | github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= 34 | github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= 35 | github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= 36 | github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= 37 | github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= 38 | github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= 39 | github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= 40 | github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= 41 | github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= 42 | github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= 43 | github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= 44 | github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= 45 | github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= 46 | github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= 47 | github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= 48 | github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= 49 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 50 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 51 | github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= 52 | github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= 53 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 54 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 55 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 56 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= 57 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 58 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 59 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 60 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 61 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 62 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 63 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 64 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 65 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 66 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 67 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 68 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 69 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 70 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 71 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 72 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= 73 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 74 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 75 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 76 | github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= 77 | github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= 78 | github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= 79 | github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= 80 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 81 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 82 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 83 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 84 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 85 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 86 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 87 | github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= 88 | github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= 89 | github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 90 | github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= 91 | github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 92 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 93 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 94 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 95 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 96 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 97 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 98 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 99 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 100 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 101 | go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= 102 | go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= 103 | go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= 104 | go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 105 | golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= 106 | golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= 107 | golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= 108 | golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= 109 | golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= 110 | golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 111 | golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= 112 | golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 113 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 114 | golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= 115 | golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 116 | golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= 117 | golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 118 | golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= 119 | golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 120 | golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= 121 | golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 122 | golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= 123 | golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= 124 | google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= 125 | google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 126 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 127 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 128 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 129 | gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= 130 | gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 131 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 132 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 133 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 134 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 135 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 136 | k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY= 137 | k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA= 138 | k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= 139 | k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= 140 | k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE= 141 | k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o= 142 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 143 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 144 | k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= 145 | k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= 146 | k8s.io/utils v0.0.0-20251219084037-98d557b7f1e7 h1:H6xtwB5tC+KFSHoEhA1o7DnOtHDEo+n9OBSHjlajVKc= 147 | k8s.io/utils v0.0.0-20251219084037-98d557b7f1e7/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= 148 | sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= 149 | sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 150 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 151 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 152 | sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= 153 | sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= 154 | sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= 155 | sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= 156 | --------------------------------------------------------------------------------