From 1a3163e3bfa54e832c4f47e9c1ec09ca13164ebf Mon Sep 17 00:00:00 2001 From: dongdong Date: Wed, 31 May 2023 23:26:56 +0800 Subject: [PATCH] feature: display the size of images (#25) * feature: display the size of images * docs update * minor: upgrade gomod version --------- Co-authored-by: mandochen --- README.md | 2 +- cmd/main.go | 4 +- go.mod | 9 ++++- go.sum | 2 + kubectl_images.go | 96 ++++++++++++++++++++++++++++++++++++++--------- 5 files changed, 92 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 5662508..6dec45e 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Examples: Flags: -A, --all-namespaces if present, list images in all namespaces. - -c, --columns string specify the columns to display, separated by comma. [0:Namespace, 1:PodName, 2:ContainerName, 3:ContainerImage, 4:ImagePullPolicy] (default "1,2,3") + -c, --columns string specify the columns to display, separated by comma. [0:Namespace, 1:PodName, 2:ContainerName, 3:ContainerImage, 4:ImagePullPolicy, 5:ImageSize] (default "1,2,3") -C, --context string The name of the kubeconfig context to use. -h, --help help for kubectl-images -k, --kubeconfig string path to the kubeconfig file to use for CLI requests. diff --git a/cmd/main.go b/cmd/main.go index f8ffdbc..a4f3603 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -9,7 +9,7 @@ import ( "github.com/spf13/cobra" ) -const version = "0.5.2" +const version = "0.6.0" var rootCmd *cobra.Command @@ -55,7 +55,7 @@ func init() { } rootCmd.Flags().BoolP("all-namespaces", "A", false, "if present, list images in all namespaces.") rootCmd.Flags().StringP("namespace", "n", "", "if present, list images in the specified namespace only. Use current namespace as fallback.") - rootCmd.Flags().StringP("columns", "c", "1,2,3", "specify the columns to display, separated by comma. [0:Namespace, 1:PodName, 2:ContainerName, 3:ContainerImage, 4:ImagePullPolicy]") + rootCmd.Flags().StringP("columns", "c", "1,2,3", "specify the columns to display, separated by comma. [0:Namespace, 1:PodName, 2:ContainerName, 3:ContainerImage, 4:ImagePullPolicy, 5:ImageSize]") rootCmd.Flags().StringP("kubeconfig", "k", "", "path to the kubeconfig file to use for CLI requests.") rootCmd.Flags().StringP("output-format", "o", "table", "output format. [json(j)|table(t)|yaml(y)]") rootCmd.Flags().StringP("context", "C", "", "The name of the kubeconfig context to use.") diff --git a/go.mod b/go.mod index d7abe8c..48d0867 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,16 @@ module github.com/chenjiandongx/kubectl-images -go 1.13 +go 1.18 require ( + github.com/dustin/go-humanize v1.0.1 github.com/olekukonko/tablewriter v0.0.4 github.com/spf13/cobra v0.0.5 gopkg.in/yaml.v2 v2.2.8 ) + +require ( + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/mattn/go-runewidth v0.0.7 // indirect + github.com/spf13/pflag v1.0.3 // indirect +) diff --git a/go.sum b/go.sum index 9bbe51c..6ee956e 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8Nz github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= diff --git a/kubectl_images.go b/kubectl_images.go index b7a6784..cff0da8 100644 --- a/kubectl_images.go +++ b/kubectl_images.go @@ -6,20 +6,24 @@ import ( "os" "os/exec" "regexp" + "strconv" "strings" + "github.com/dustin/go-humanize" "github.com/olekukonko/tablewriter" "gopkg.in/yaml.v2" ) const ( - gotemplate = `go-template={{range .items}} {{.metadata.namespace}} {{","}} {{.metadata.name}} {{","}} {{range .spec.containers}} {{.name}} {{","}} {{.image}} {{","}} {{.imagePullPolicy}} {{"\n"}} {{end}} {{range .spec.initContainers}} {{"(init)"}} {{.name}} {{","}} {{.image}} {{","}} {{.imagePullPolicy}} {{"\n"}} {{end}} {{end}}` + podTemplate = `go-template={{range .items}} {{.metadata.namespace}} {{","}} {{.metadata.name}} {{","}} {{range .spec.containers}} {{.name}} {{","}} {{.image}} {{","}} {{.imagePullPolicy}} {{"\n"}} {{end}} {{range .spec.initContainers}} {{"(init)"}} {{.name}} {{","}} {{.image}} {{","}} {{.imagePullPolicy}} {{"\n"}} {{end}} {{end}}` + nodeTemplate = `go-template={{range .items}} {{range .status.images}} {{range .names}} {{.}} {{","}} {{end}} {{.sizeBytes}} {{"\n"}} {{end}} {{end}}` labelNamespace = "Namespace" labelPod = "Pod" labelContainer = "Container" labelImage = "Image" labelImagePullPolicy = "ImagePullPolicy" + labelImageSize = "ImageSize" ) type Parameters struct { @@ -33,14 +37,17 @@ type Parameters struct { // KubeImage is the representation of a container image used in the cluster. type KubeImage struct { - entities []*ImageEntity - columns []string - regx *regexp.Regexp - params Parameters + entities []*ImageEntity + columns []string + regx *regexp.Regexp + params Parameters + imageSize map[string]int + needNodeInfo bool } // NewKubeImage creates a new KubeImage instance. func NewKubeImage(regx *regexp.Regexp, params Parameters) *KubeImage { + var needNodeInfo bool names := make([]string, 0) for _, c := range stringSplit(params.Columns, ",") { switch c { @@ -54,13 +61,18 @@ func NewKubeImage(regx *regexp.Regexp, params Parameters) *KubeImage { names = append(names, labelImage) case "4": names = append(names, labelImagePullPolicy) + case "5": + names = append(names, labelImageSize) + needNodeInfo = true } } return &KubeImage{ - columns: names, - params: params, - regx: regx, + columns: names, + params: params, + regx: regx, + imageSize: make(map[string]int), + needNodeInfo: needNodeInfo, } } @@ -71,6 +83,7 @@ type ImageEntity struct { Container string `json:"container,omitempty" yaml:"container,omitempty"` Image string `json:"image,omitempty" yaml:"image,omitempty"` ImagePullPolicy string `json:"imagePullPolicy,omitempty" yaml:"imagePullPolicy,omitempty"` + ImageSize string `json:"imageSize,omitempty" yaml:"imageSize,omitempty"` } func (ie *ImageEntity) selectBy(columns []string) []string { @@ -87,6 +100,8 @@ func (ie *ImageEntity) selectBy(columns []string) []string { result = append(result, ie.Image) case labelImagePullPolicy: result = append(result, ie.ImagePullPolicy) + case labelImageSize: + result = append(result, ie.ImageSize) } } return result @@ -106,6 +121,8 @@ func (ie *ImageEntity) filterBy(columns []string) ImageEntity { entity.Image = ie.Image case labelImagePullPolicy: entity.ImagePullPolicy = ie.ImagePullPolicy + case labelImageSize: + entity.ImageSize = ie.ImageSize } } return entity @@ -142,8 +159,8 @@ func stringSplit(in, sep string) []string { return out } -// Commands builds the command to be executed based on user input. -func (ki *KubeImage) Commands() []string { +// podCommands builds the command to be executed based on user input. +func (ki *KubeImage) podCommands() []string { kubecfg := make([]string, 0) if ki.params.KubeConfig != "" { kubecfg = append(kubecfg, "--kubeconfig", ki.params.KubeConfig) @@ -154,18 +171,60 @@ func (ki *KubeImage) Commands() []string { } if ki.params.AllNamespace { - return append([]string{"get", "pods", "--all-namespaces", "-o", gotemplate}, kubecfg...) + return append([]string{"get", "pods", "--all-namespaces", "-o", podTemplate}, kubecfg...) } else if ki.params.Namespace != "" { - return append([]string{"get", "pods", "-n", ki.params.Namespace, "-o", gotemplate}, kubecfg...) + return append([]string{"get", "pods", "-n", ki.params.Namespace, "-o", podTemplate}, kubecfg...) } - return append([]string{"get", "pods", "-o", gotemplate}, kubecfg...) + return append([]string{"get", "pods", "-o", podTemplate}, kubecfg...) } -func (ki *KubeImage) exec() { - process := exec.Command("kubectl", ki.Commands()...) +func (ki *KubeImage) nodeCommands() []string { + kubecfg := make([]string, 0) + if ki.params.KubeConfig != "" { + kubecfg = append(kubecfg, "--kubeconfig", ki.params.KubeConfig) + } + + if ki.params.Context != "" { + kubecfg = append(kubecfg, "--context", ki.params.Context) + } + + return append([]string{"get", "nodes", "-o", nodeTemplate}, kubecfg...) +} + +func (ki *KubeImage) execNodeCommand() { + process := exec.Command("kubectl", ki.nodeCommands()...) + bs, err := process.CombinedOutput() + if err != nil { + fmt.Fprintf(os.Stderr, "[Oh...] Execute nodes command error: %v, %s", err, string(bs)) + os.Exit(1) + } + + for _, line := range stringSplit(string(bs), "\n") { + items := stringSplit(line, ",") + switch len(items) { + case 3: + size, err := strconv.Atoi(items[2]) + if err != nil { + continue + } + ki.imageSize[items[0]] = size + ki.imageSize[items[1]] = size + } + } + + for _, entity := range ki.entities { + size, ok := ki.imageSize[entity.Image] + if ok { + entity.ImageSize = humanize.IBytes(uint64(size)) + } + } +} + +func (ki *KubeImage) execPodCommand() { + process := exec.Command("kubectl", ki.podCommands()...) bs, err := process.CombinedOutput() if err != nil { - fmt.Fprintf(os.Stderr, "[Oh...] Execute command error: %v, %s", err, string(bs)) + fmt.Fprintf(os.Stderr, "[Oh...] Execute pods command error: %v, %s", err, string(bs)) os.Exit(1) } @@ -293,7 +352,10 @@ func (ki *KubeImage) yamlRender() { // Render renders and displays the table output. func (ki *KubeImage) Render(format string) { - ki.exec() + ki.execPodCommand() + if ki.needNodeInfo { + ki.execNodeCommand() + } if len(ki.entities) == 0 { fmt.Fprintln(os.Stdout, "[Oh...] No images matched!")