├── .github └── workflows │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── .krew.yaml ├── LICENSE ├── README.md ├── cmd └── main.go ├── go.mod ├── go.sum └── kubectl_images.go /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - 'v*.*.*' 6 | jobs: 7 | goreleaser: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@master 12 | - name: Setup Go 13 | uses: actions/setup-go@v1 14 | with: 15 | go-version: 1.16 16 | - name: GoReleaser 17 | uses: goreleaser/goreleaser-action@v1 18 | with: 19 | version: latest 20 | args: release --rm-dist 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | - name: Update new version in krew-index 24 | uses: rajatjindal/krew-release-bot@v0.0.39 25 | -------------------------------------------------------------------------------- /.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 | # IDE 15 | .idea/ 16 | .vscode/ 17 | .DS_Store 18 | 19 | releases/ 20 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - id: kubectl-images 3 | main: ./cmd 4 | binary: kubectl-images 5 | env: 6 | - CGO_ENABLED=0 7 | goos: 8 | - darwin 9 | - linux 10 | - windows 11 | goarch: 12 | - amd64 13 | - arm64 14 | - arm 15 | ignore: 16 | - goos: windows 17 | goarch: arm 18 | - goos: windows 19 | goarch: arm64 20 | - goos: darwin 21 | goarch: arm 22 | 23 | archives: 24 | - builds: 25 | - kubectl-images 26 | name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}" 27 | wrap_in_directory: false 28 | format: tar.gz 29 | files: 30 | - LICENSE 31 | -------------------------------------------------------------------------------- /.krew.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: krew.googlecontainertools.github.com/v1alpha2 2 | kind: Plugin 3 | metadata: 4 | name: images 5 | spec: 6 | version: {{ .TagName }} 7 | homepage: https://github.com/chenjiandongx/kubectl-images 8 | shortDescription: Show container images used in the cluster. 9 | description: | 10 | This plugin shows container images used in the Kubernetes cluster in a 11 | table view. You can show all images or show images used in a specified 12 | namespace. 13 | platforms: 14 | - selector: 15 | matchLabels: 16 | os: darwin 17 | arch: amd64 18 | files: 19 | - from: kubectl-images 20 | to: . 21 | - from: LICENSE 22 | to: . 23 | {{ addURIAndSha "https://github.com/chenjiandongx/kubectl-images/releases/download/{{ .TagName }}/kubectl-images_darwin_amd64.tar.gz" .TagName }} 24 | bin: kubectl-images 25 | - selector: 26 | matchLabels: 27 | os: darwin 28 | arch: arm64 29 | files: 30 | - from: kubectl-images 31 | to: . 32 | - from: LICENSE 33 | to: . 34 | {{ addURIAndSha "https://github.com/chenjiandongx/kubectl-images/releases/download/{{ .TagName }}/kubectl-images_darwin_arm64.tar.gz" .TagName }} 35 | bin: kubectl-images 36 | - selector: 37 | matchLabels: 38 | os: linux 39 | arch: amd64 40 | files: 41 | - from: kubectl-images 42 | to: . 43 | - from: LICENSE 44 | to: . 45 | {{ addURIAndSha "https://github.com/chenjiandongx/kubectl-images/releases/download/{{ .TagName }}/kubectl-images_linux_amd64.tar.gz" .TagName }} 46 | bin: kubectl-images 47 | - selector: 48 | matchLabels: 49 | os: linux 50 | arch: arm64 51 | files: 52 | - from: kubectl-images 53 | to: . 54 | - from: LICENSE 55 | to: . 56 | {{ addURIAndSha "https://github.com/chenjiandongx/kubectl-images/releases/download/{{ .TagName }}/kubectl-images_linux_arm64.tar.gz" .TagName }} 57 | bin: kubectl-images 58 | - selector: 59 | matchLabels: 60 | os: linux 61 | arch: arm 62 | files: 63 | - from: kubectl-images 64 | to: . 65 | - from: LICENSE 66 | to: . 67 | {{ addURIAndSha "https://github.com/chenjiandongx/kubectl-images/releases/download/{{ .TagName }}/kubectl-images_linux_arm.tar.gz" .TagName }} 68 | bin: kubectl-images 69 | - selector: 70 | matchLabels: 71 | os: windows 72 | arch: amd64 73 | files: 74 | - from: kubectl-images.exe 75 | to: . 76 | - from: LICENSE 77 | to: . 78 | {{ addURIAndSha "https://github.com/chenjiandongx/kubectl-images/releases/download/{{ .TagName }}/kubectl-images_windows_amd64.tar.gz" .TagName }} 79 | bin: kubectl-images.exe 80 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020~now chenjiandongx 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

kubectl-images

2 |

3 | 🕸 Show container images used in the cluster. 4 |

5 | 6 | kubectl-images makes use of the `kubectl` command. It first calls `kubectl get pods` to retrieve pods details and 7 | filters out the container image information of each pod, then prints out the final result in a table/json/yaml view. 8 | 9 | ### 🔰 Installation 10 | 11 | Krew 12 | 13 | ```shell 14 | $ kubectl krew install images 15 | Updated the local copy of plugin index. 16 | Installing plugin: images 17 | Installed plugin: images 18 | \ 19 | | Use this plugin: 20 | | kubectl images 21 | | Documentation: 22 | | https://github.com/chenjiandongx/kubectl-images 23 | / 24 | ``` 25 | 26 | Build from source code 27 | 28 | ```shell 29 | $ git clone https://github.com/chenjiandongx/kubectl-images.git 30 | $ cd kubectl-images && go build -ldflags="-s -w" -o kubectl-images . && mv ./kubectl-images /usr/local/bin 31 | $ kubectl images --help 32 | ``` 33 | 34 | Download the binary 35 | 36 | ```shell 37 | # Refer to the link: https://github.com/chenjiandongx/kubectl-images/releases 38 | # Download the binary and then... 39 | $ chmod +x kubectl-images && mv kubectl-images /usr/local/bin/ 40 | $ kubectl images --help 41 | ``` 42 | 43 | ### 📝 Usage 44 | 45 | ```shell 46 | ~ 🐶 kubectl images --help 47 | Show container images used in the cluster. 48 | 49 | Usage: 50 | kubectl-images [podname-regex] [flags] 51 | 52 | Examples: 53 | # display a table of all images in current namespace using podName/containerName/containerImage as columns. 54 | kubectl images 55 | 56 | # display images info in yaml format 57 | kubectl images -oy 58 | 59 | # display a table of images that match 'nginx' podname regex in 'dev' namespace using podName/containerImage as columns. 60 | kubectl images -n dev nginx -c 1,2 61 | 62 | Flags: 63 | -A, --all-namespaces if present, list images in all namespaces. 64 | -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") 65 | -C, --context string The name of the kubeconfig context to use. 66 | -h, --help help for kubectl-images 67 | -k, --kubeconfig string path to the kubeconfig file to use for CLI requests. 68 | -n, --namespace string if present, list images in the specified namespace only. Use current namespace as fallback. 69 | -o, --output-format string output format. [json(j)|table(t)|yaml(y)] (default "table") 70 | -u, --unique Unique images group by namespace/container/images/pullPolicy. 71 | --version version for kubectl-images 72 | ``` 73 | 74 | ### 🔖 Glances 75 | 76 | ```shell 77 | ~ 🐶 kubectl images -n kube-system -oy dns 78 | - pod: coredns-78fcd69978-9pbjh 79 | container: coredns 80 | image: k8s.gcr.io/coredns/coredns:v1.8.4 81 | - pod: coredns-78fcd69978-jh7m2 82 | container: coredns 83 | image: k8s.gcr.io/coredns/coredns:v1.8.4 84 | 85 | ~ 🐶 kubectl images -A -c 0,1,3 86 | [Summary]: 2 namespaces, 11 pods, 11 containers and 9 different images 87 | +-------------+----------------------------------------+--------------------------------------------+ 88 | | Namespace | Pod | Image | 89 | +-------------+----------------------------------------+--------------------------------------------+ 90 | | kube-system | coredns-78fcd69978-9pbjh | k8s.gcr.io/coredns/coredns:v1.8.4 | 91 | + +----------------------------------------+ + 92 | | | coredns-78fcd69978-jh7m2 | | 93 | + +----------------------------------------+--------------------------------------------+ 94 | | | etcd-docker-desktop | k8s.gcr.io/etcd:3.5.0-0 | 95 | + +----------------------------------------+--------------------------------------------+ 96 | | | kube-apiserver-docker-desktop | k8s.gcr.io/kube-apiserver:v1.22.5 | 97 | + +----------------------------------------+--------------------------------------------+ 98 | | | kube-controller-manager-docker-desktop | k8s.gcr.io/kube-controller-manager:v1.22.5 | 99 | + +----------------------------------------+--------------------------------------------+ 100 | | | kube-proxy-vc7fv | k8s.gcr.io/kube-proxy:v1.22.5 | 101 | + +----------------------------------------+--------------------------------------------+ 102 | | | kube-scheduler-docker-desktop | k8s.gcr.io/kube-scheduler:v1.22.5 | 103 | + +----------------------------------------+--------------------------------------------+ 104 | | | storage-provisioner | docker/desktop-storage-provisioner:v2.0 | 105 | + +----------------------------------------+--------------------------------------------+ 106 | | | vpnkit-controller | docker/desktop-vpnkit-controller:v2.0 | 107 | +-------------+----------------------------------------+--------------------------------------------+ 108 | | nginx | nginx-deployment-66b6c48dd5-s9wv5 | nginx:1.14.2 | 109 | + +----------------------------------------+ + 110 | | | nginx-deployment-66b6c48dd5-wmn9x | | 111 | +-------------+----------------------------------------+--------------------------------------------+ 112 | 113 | ~ 🐶 kubectl images -A -c 0,1,3 -u 114 | [Summary]: 2 namespaces, 11 pods, 11 containers and 9 different images 115 | +-------------+----------------------------------------+--------------------------------------------+ 116 | | Namespace | Pod | Image | 117 | +-------------+----------------------------------------+--------------------------------------------+ 118 | | kube-system | coredns-78fcd69978-9pbjh | k8s.gcr.io/coredns/coredns:v1.8.4 | + 119 | + +----------------------------------------+--------------------------------------------+ 120 | | | etcd-docker-desktop | k8s.gcr.io/etcd:3.5.0-0 | 121 | + +----------------------------------------+--------------------------------------------+ 122 | | | kube-apiserver-docker-desktop | k8s.gcr.io/kube-apiserver:v1.22.5 | 123 | + +----------------------------------------+--------------------------------------------+ 124 | | | kube-controller-manager-docker-desktop | k8s.gcr.io/kube-controller-manager:v1.22.5 | 125 | + +----------------------------------------+--------------------------------------------+ 126 | | | kube-proxy-vc7fv | k8s.gcr.io/kube-proxy:v1.22.5 | 127 | + +----------------------------------------+--------------------------------------------+ 128 | | | kube-scheduler-docker-desktop | k8s.gcr.io/kube-scheduler:v1.22.5 | 129 | + +----------------------------------------+--------------------------------------------+ 130 | | | storage-provisioner | docker/desktop-storage-provisioner:v2.0 | 131 | + +----------------------------------------+--------------------------------------------+ 132 | | | vpnkit-controller | docker/desktop-vpnkit-controller:v2.0 | 133 | +-------------+----------------------------------------+--------------------------------------------+ 134 | | nginx | nginx-deployment-66b6c48dd5-s9wv5 | nginx:1.14.2 | 135 | +-------------+----------------------------------------+--------------------------------------------+ 136 | 137 | ~ 🐶 kubectl images -c 0,1,2,3,4 -n nginx -oj 138 | [ 139 | { 140 | "namespace": "nginx", 141 | "pod": "nginx-deployment-66b6c48dd5-s9wv5", 142 | "container": "nginx", 143 | "image": "nginx:latest", 144 | "imagePullPolicy": "IfNotPresent" 145 | }, 146 | { 147 | "namespace": "nginx", 148 | "pod": "nginx-deployment-66b6c48dd5-wmn9x", 149 | "container": "nginx", 150 | "image": "nginx:latest", 151 | "imagePullPolicy": "IfNotPresent" 152 | } 153 | ] 154 | ``` 155 | 156 | ### 📃 License 157 | 158 | MIT [©chenjiandongx](https://github.com/chenjiandongx) 159 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "regexp" 7 | 8 | kubeimages "github.com/chenjiandongx/kubectl-images" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | const version = "0.6.3" 13 | 14 | var rootCmd *cobra.Command 15 | 16 | func init() { 17 | rootCmd = &cobra.Command{ 18 | Use: "kubectl-images [podname-regex]", 19 | Short: "Show container images used in the cluster.", 20 | Example: ` # display a table of all images in current namespace using podName/containerName/containerImage as columns. 21 | kubectl images 22 | 23 | # display images info in yaml format 24 | kubectl images -oy 25 | 26 | # display a table of images that match 'nginx' podname regex in 'dev' namespace using podName/containerImage as columns. 27 | kubectl images -n dev nginx -c 1,2`, 28 | Version: version, 29 | Run: func(cmd *cobra.Command, args []string) { 30 | var regx *regexp.Regexp 31 | var err error 32 | if len(args) > 0 { 33 | if regx, err = regexp.Compile(args[0]); err != nil { 34 | fmt.Fprintf(os.Stderr, "[Oh...] Invalid regex pattern (%q)", args[0]) 35 | return 36 | } 37 | } 38 | namespace, _ := cmd.Flags().GetString("namespace") 39 | columns, _ := cmd.Flags().GetString("columns") 40 | format, _ := cmd.Flags().GetString("output-format") 41 | allNamespace, _ := cmd.Flags().GetBool("all-namespaces") 42 | kubeConfig, _ := cmd.Flags().GetString("kubeConfig") 43 | context, _ := cmd.Flags().GetString("context") 44 | unique, _ := cmd.Flags().GetBool("unique") 45 | kubeImage := kubeimages.NewKubeImage(regx, kubeimages.Parameters{ 46 | AllNamespace: allNamespace, 47 | Namespace: namespace, 48 | Columns: columns, 49 | KubeConfig: kubeConfig, 50 | Context: context, 51 | Unique: unique, 52 | }) 53 | kubeImage.Render(format) 54 | }, 55 | } 56 | rootCmd.Flags().BoolP("all-namespaces", "A", false, "if present, list images in all namespaces.") 57 | rootCmd.Flags().StringP("namespace", "n", "", "if present, list images in the specified namespace only. Use current namespace as fallback.") 58 | 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]") 59 | rootCmd.Flags().StringP("kubeconfig", "k", "", "path to the kubeconfig file to use for CLI requests.") 60 | rootCmd.Flags().StringP("output-format", "o", "table", "output format. [json(j)|table(t)|yaml(y)]") 61 | rootCmd.Flags().StringP("context", "C", "", "The name of the kubeconfig context to use.") 62 | rootCmd.Flags().BoolP("unique", "u", false, "Unique images group by namespace/container/images/pullPolicy.") 63 | } 64 | 65 | func main() { 66 | if err := rootCmd.Execute(); err != nil { 67 | fmt.Fprintf(os.Stderr, "[Oh...] Failed to exec command: %v", err) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/chenjiandongx/kubectl-images 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/dustin/go-humanize v1.0.1 7 | github.com/olekukonko/tablewriter v0.0.4 8 | github.com/spf13/cobra v0.0.5 9 | gopkg.in/yaml.v2 v2.2.8 10 | ) 11 | 12 | require ( 13 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 14 | github.com/mattn/go-runewidth v0.0.7 // indirect 15 | github.com/spf13/pflag v1.0.3 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 3 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 4 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 5 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 6 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 9 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 10 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 11 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 12 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 13 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 14 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 15 | github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= 16 | github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 17 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 18 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 19 | github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= 20 | github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= 21 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 22 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 23 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 24 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 25 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 26 | github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= 27 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 28 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 29 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 30 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 31 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 32 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 33 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 34 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 35 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 36 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 37 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 38 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 39 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 40 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 41 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 42 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 43 | -------------------------------------------------------------------------------- /kubectl_images.go: -------------------------------------------------------------------------------- 1 | package kubeimage 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path" 9 | "regexp" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/dustin/go-humanize" 14 | "github.com/olekukonko/tablewriter" 15 | "gopkg.in/yaml.v2" 16 | ) 17 | 18 | const ( 19 | 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}}` 20 | nodeTemplate = `go-template={{range .items}} {{range .status.images}} {{range .names}} {{.}} {{","}} {{end}} {{.sizeBytes}} {{"\n"}} {{end}} {{end}}` 21 | 22 | labelNamespace = "Namespace" 23 | labelPod = "Pod" 24 | labelContainer = "Container" 25 | labelImage = "Image" 26 | labelImagePullPolicy = "ImagePullPolicy" 27 | labelImageSize = "ImageSize" 28 | ) 29 | 30 | type Parameters struct { 31 | AllNamespace bool 32 | Namespace string 33 | Columns string 34 | KubeConfig string 35 | Context string 36 | Unique bool 37 | } 38 | 39 | // KubeImage is the representation of a container image used in the cluster. 40 | type KubeImage struct { 41 | entities []*ImageEntity 42 | columns []string 43 | regx *regexp.Regexp 44 | params Parameters 45 | imageSize map[string]int 46 | needNodeInfo bool 47 | } 48 | 49 | // NewKubeImage creates a new KubeImage instance. 50 | func NewKubeImage(regx *regexp.Regexp, params Parameters) *KubeImage { 51 | var needNodeInfo bool 52 | names := make([]string, 0) 53 | for _, c := range stringSplit(params.Columns, ",") { 54 | switch c { 55 | case "0": 56 | names = append(names, labelNamespace) 57 | case "1": 58 | names = append(names, labelPod) 59 | case "2": 60 | names = append(names, labelContainer) 61 | case "3": 62 | names = append(names, labelImage) 63 | case "4": 64 | names = append(names, labelImagePullPolicy) 65 | case "5": 66 | names = append(names, labelImageSize) 67 | needNodeInfo = true 68 | } 69 | } 70 | 71 | return &KubeImage{ 72 | columns: names, 73 | params: params, 74 | regx: regx, 75 | imageSize: make(map[string]int), 76 | needNodeInfo: needNodeInfo, 77 | } 78 | } 79 | 80 | // ImageEntity is the representation of an entity to be displayed. 81 | type ImageEntity struct { 82 | Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` 83 | Pod string `json:"pod,omitempty" yaml:"pod,omitempty"` 84 | Container string `json:"container,omitempty" yaml:"container,omitempty"` 85 | Image string `json:"image,omitempty" yaml:"image,omitempty"` 86 | ImagePullPolicy string `json:"imagePullPolicy,omitempty" yaml:"imagePullPolicy,omitempty"` 87 | ImageSize string `json:"imageSize,omitempty" yaml:"imageSize,omitempty"` 88 | } 89 | 90 | func (ie *ImageEntity) selectBy(columns []string) []string { 91 | result := make([]string, 0) 92 | for _, c := range columns { 93 | switch c { 94 | case labelNamespace: 95 | result = append(result, ie.Namespace) 96 | case labelPod: 97 | result = append(result, ie.Pod) 98 | case labelContainer: 99 | result = append(result, ie.Container) 100 | case labelImage: 101 | result = append(result, ie.Image) 102 | case labelImagePullPolicy: 103 | result = append(result, ie.ImagePullPolicy) 104 | case labelImageSize: 105 | result = append(result, ie.ImageSize) 106 | } 107 | } 108 | return result 109 | } 110 | 111 | func (ie *ImageEntity) filterBy(columns []string) ImageEntity { 112 | var entity ImageEntity 113 | for _, c := range columns { 114 | switch c { 115 | case labelNamespace: 116 | entity.Namespace = ie.Namespace 117 | case labelPod: 118 | entity.Pod = ie.Pod 119 | case labelContainer: 120 | entity.Container = ie.Container 121 | case labelImage: 122 | entity.Image = ie.Image 123 | case labelImagePullPolicy: 124 | entity.ImagePullPolicy = ie.ImagePullPolicy 125 | case labelImageSize: 126 | entity.ImageSize = ie.ImageSize 127 | } 128 | } 129 | return entity 130 | } 131 | 132 | // Counter is a simple counter. 133 | type Counter struct { 134 | cnt int 135 | items map[string]bool 136 | } 137 | 138 | // NewCounter creates a new Counter instance. 139 | func NewCounter() *Counter { 140 | return &Counter{items: make(map[string]bool)} 141 | } 142 | 143 | func (c *Counter) add(obj string) { 144 | if !c.items[obj] { 145 | c.cnt += 1 146 | c.items[obj] = true 147 | } 148 | } 149 | 150 | // Count returns current counter reading. 151 | func (c *Counter) Count() int { 152 | return c.cnt 153 | } 154 | 155 | func stringSplit(in, sep string) []string { 156 | out := make([]string, 0) 157 | for _, s := range strings.Split(in, sep) { 158 | out = append(out, strings.TrimSpace(s)) 159 | } 160 | return out 161 | } 162 | 163 | // podCommands builds the command to be executed based on user input. 164 | func (ki *KubeImage) podCommands() []string { 165 | kubecfg := make([]string, 0) 166 | if ki.params.KubeConfig != "" { 167 | kubecfg = append(kubecfg, "--kubeconfig", ki.params.KubeConfig) 168 | } 169 | 170 | if ki.params.Context != "" { 171 | kubecfg = append(kubecfg, "--context", ki.params.Context) 172 | } 173 | 174 | if ki.params.AllNamespace { 175 | return append([]string{"get", "pods", "--all-namespaces", "-o", podTemplate}, kubecfg...) 176 | } else if ki.params.Namespace != "" { 177 | return append([]string{"get", "pods", "-n", ki.params.Namespace, "-o", podTemplate}, kubecfg...) 178 | } 179 | return append([]string{"get", "pods", "-o", podTemplate}, kubecfg...) 180 | } 181 | 182 | func (ki *KubeImage) nodeCommands() []string { 183 | kubecfg := make([]string, 0) 184 | if ki.params.KubeConfig != "" { 185 | kubecfg = append(kubecfg, "--kubeconfig", ki.params.KubeConfig) 186 | } 187 | 188 | if ki.params.Context != "" { 189 | kubecfg = append(kubecfg, "--context", ki.params.Context) 190 | } 191 | 192 | return append([]string{"get", "nodes", "-o", nodeTemplate}, kubecfg...) 193 | } 194 | 195 | func (ki *KubeImage) recordImageSize(image string, size int) { 196 | ki.imageSize[image] = size 197 | ki.imageSize[path.Base(image)] = size 198 | } 199 | 200 | func (ki *KubeImage) execNodeCommand() { 201 | process := exec.Command("kubectl", ki.nodeCommands()...) 202 | bs, err := process.CombinedOutput() 203 | if err != nil { 204 | fmt.Fprintf(os.Stderr, "[Oh...] Execute nodes command error: %v, %s", err, string(bs)) 205 | os.Exit(1) 206 | } 207 | 208 | for _, line := range stringSplit(string(bs), "\n") { 209 | items := stringSplit(line, ",") 210 | switch len(items) { 211 | case 2: 212 | size, err := strconv.Atoi(items[1]) 213 | if err != nil { 214 | continue 215 | } 216 | 217 | ki.recordImageSize(items[0], size) 218 | parts := strings.Split(items[0], ":") 219 | if len(parts) == 2 && parts[1] == "latest" { 220 | ki.recordImageSize(parts[0], size) 221 | } 222 | 223 | case 3: 224 | size, err := strconv.Atoi(items[2]) 225 | if err != nil { 226 | continue 227 | } 228 | 229 | ki.recordImageSize(items[0], size) 230 | ki.recordImageSize(items[1], size) 231 | parts := strings.Split(items[1], ":") 232 | if len(parts) == 2 && parts[1] == "latest" { 233 | ki.recordImageSize(parts[0], size) 234 | } 235 | } 236 | } 237 | 238 | for _, entity := range ki.entities { 239 | size, ok := ki.imageSize[entity.Image] 240 | if ok { 241 | entity.ImageSize = humanize.IBytes(uint64(size)) 242 | } 243 | } 244 | } 245 | 246 | func (ki *KubeImage) execPodCommand() { 247 | process := exec.Command("kubectl", ki.podCommands()...) 248 | bs, err := process.CombinedOutput() 249 | if err != nil { 250 | fmt.Fprintf(os.Stderr, "[Oh...] Execute pods command error: %v, %s", err, string(bs)) 251 | os.Exit(1) 252 | } 253 | 254 | entities := make([]*ImageEntity, 0) 255 | for _, line := range stringSplit(string(bs), "\n") { 256 | items := stringSplit(line, ",") 257 | entity := &ImageEntity{} 258 | 259 | switch len(items) { 260 | case 1: 261 | continue 262 | case 3: 263 | entity.Container = items[0] 264 | entity.Image = items[1] 265 | entity.ImagePullPolicy = items[2] 266 | case 5: 267 | entity.Namespace = items[0] 268 | entity.Pod = items[1] 269 | entity.Container = items[2] 270 | entity.Image = items[3] 271 | entity.ImagePullPolicy = items[4] 272 | } 273 | entities = append(entities, entity) 274 | } 275 | 276 | for i := 0; i < len(entities); i++ { 277 | if entities[i].Pod == "" && i > 0 { 278 | entities[i].Namespace = entities[i-1].Namespace 279 | entities[i].Pod = entities[i-1].Pod 280 | } 281 | } 282 | 283 | for i := 0; i < len(entities); i++ { 284 | if ki.regx == nil { 285 | ki.entities = append(ki.entities, entities[i]) 286 | continue 287 | } 288 | if ki.regx.Match([]byte(entities[i].Pod)) { 289 | ki.entities = append(ki.entities, entities[i]) 290 | } 291 | } 292 | } 293 | 294 | func (ki *KubeImage) groupBy() []*ImageEntity { 295 | if !ki.params.Unique { 296 | return ki.entities 297 | } 298 | 299 | set := make(map[string]struct{}) 300 | entities := make([]*ImageEntity, 0) 301 | 302 | for i, entity := range ki.entities { 303 | k := fmt.Sprintf("%s/%s/%s/%s", entity.Namespace, entity.Container, entity.Image, entity.ImagePullPolicy) 304 | if _, ok := set[k]; ok { 305 | continue 306 | } 307 | 308 | set[k] = struct{}{} 309 | entities = append(entities, ki.entities[i]) 310 | } 311 | return entities 312 | } 313 | 314 | func (ki *KubeImage) summary() { 315 | namespaceCnt := NewCounter() 316 | podCnt := NewCounter() 317 | imageCnt := NewCounter() 318 | containerCnt := 0 319 | 320 | for i := 0; i < len(ki.entities); i++ { 321 | namespaceCnt.add(ki.entities[i].Namespace) 322 | podCnt.add(ki.entities[i].Pod) 323 | imageCnt.add(ki.entities[i].Image) 324 | containerCnt += 1 325 | } 326 | 327 | fmt.Fprintf(os.Stdout, "[Summary]: %d namespaces, %d pods, %d containers and %d different images\n", 328 | namespaceCnt.Count(), podCnt.Count(), containerCnt, imageCnt.Count(), 329 | ) 330 | } 331 | 332 | func (ki *KubeImage) tableRender() { 333 | table := tablewriter.NewWriter(os.Stdout) 334 | table.SetHeader(ki.columns) 335 | table.SetAutoFormatHeaders(false) 336 | table.SetAutoMergeCells(true) 337 | table.SetRowLine(true) 338 | 339 | entities := ki.getGroupByEntities() 340 | for _, entity := range entities { 341 | table.Append(entity) 342 | } 343 | table.Render() 344 | } 345 | 346 | func (ki *KubeImage) getGroupByEntities() [][]string { 347 | set := make(map[string]struct{}) 348 | dst := make([][]string, 0) 349 | for _, entity := range ki.groupBy() { 350 | line := strings.Join(entity.selectBy(ki.columns), "||") 351 | _, ok := set[line] 352 | if !ok { 353 | set[line] = struct{}{} 354 | dst = append(dst, strings.Split(line, "||")) 355 | } 356 | } 357 | return dst 358 | } 359 | 360 | func (ki *KubeImage) jsonRender() { 361 | entities := ki.groupBy() 362 | records := make([]ImageEntity, 0, len(entities)) 363 | for _, entity := range entities { 364 | records = append(records, entity.filterBy(ki.columns)) 365 | } 366 | 367 | output, err := json.MarshalIndent(records, "", " ") 368 | if err != nil { 369 | fmt.Fprintf(os.Stderr, "[Oh...] Failed to marshal JSON data, error: %v", err) 370 | os.Exit(1) 371 | } 372 | fmt.Fprintln(os.Stdout, string(output)) 373 | } 374 | 375 | func (ki *KubeImage) yamlRender() { 376 | entities := ki.groupBy() 377 | records := make([]ImageEntity, 0, len(entities)) 378 | for _, entity := range entities { 379 | records = append(records, entity.filterBy(ki.columns)) 380 | } 381 | 382 | output, err := yaml.Marshal(records) 383 | if err != nil { 384 | fmt.Fprintf(os.Stderr, "[Oh...] Failed to marshal YAML data, error: %v", err) 385 | os.Exit(1) 386 | } 387 | fmt.Fprintln(os.Stdout, string(output)) 388 | } 389 | 390 | // Render renders and displays the table output. 391 | func (ki *KubeImage) Render(format string) { 392 | ki.execPodCommand() 393 | if ki.needNodeInfo { 394 | ki.execNodeCommand() 395 | } 396 | 397 | if len(ki.entities) == 0 { 398 | fmt.Fprintln(os.Stdout, "[Oh...] No images matched!") 399 | return 400 | } 401 | 402 | switch format { 403 | case "json", "j": 404 | ki.jsonRender() 405 | case "yaml", "y": 406 | ki.yamlRender() 407 | default: // table 408 | ki.summary() 409 | ki.tableRender() 410 | } 411 | } 412 | --------------------------------------------------------------------------------