├── .gitignore ├── LISENCE ├── Makefile ├── README.md ├── VERSION.txt ├── agent ├── Dockerfile ├── krawler.go ├── krawler_test.go └── type.go ├── cmd ├── check_cert.go └── types.go ├── fixture └── krawler.yaml ├── go.mod ├── go.sum └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | kubectl-check-cert 2 | kubectl-check_cert 3 | cross 4 | -------------------------------------------------------------------------------- /LISENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Eohyung Lee 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. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # borrow from https://github.com/genuinetools/udict/blob/master/basic.mk 2 | NAME := kubectl-check_cert 3 | PKG := github.com/leoh0/$(NAME) 4 | GO := go 5 | CGO_ENABLED := 1 6 | BUILDTAGS := seccomp 7 | 8 | GOOSARCHES = linux/amd64 9 | 10 | # Set an output prefix, which is the local directory if not specified 11 | PREFIX?=$(shell pwd) 12 | 13 | # Set the build dir, where built cross-compiled binaries will be output 14 | BUILDDIR := ${PREFIX}/cross 15 | 16 | # Populate version variables 17 | # Add to compile time flags 18 | VERSION := $(shell cat VERSION.txt) 19 | GITCOMMIT := $(shell git rev-parse --short HEAD) 20 | GITUNTRACKEDCHANGES := $(shell git status --porcelain --untracked-files=no) 21 | ifneq ($(GITUNTRACKEDCHANGES),) 22 | GITCOMMIT := $(GITCOMMIT)-dirty 23 | endif 24 | ifeq ($(GITCOMMIT),) 25 | GITCOMMIT := ${GITHUB_SHA} 26 | endif 27 | CTIMEVAR=-X $(PKG)/version.GITCOMMIT=$(GITCOMMIT) -X $(PKG)/version.VERSION=$(VERSION) 28 | GO_LDFLAGS=-ldflags "-w $(CTIMEVAR)" 29 | GO_LDFLAGS_STATIC=-ldflags "-w $(CTIMEVAR) -extldflags -static" 30 | 31 | .PHONY: build 32 | build: $(NAME) ## Builds a dynamic executable or package. 33 | 34 | $(NAME): $(wildcard *.go) VERSION.txt 35 | @echo "+ $@" 36 | $(GO) build -tags "$(BUILDTAGS)" ${GO_LDFLAGS} -o $(NAME) . 37 | 38 | .PHONY: static 39 | static: ## Builds a static executable. 40 | @echo "+ $@" 41 | CGO_ENABLED=$(CGO_ENABLED) $(GO) build \ 42 | -tags "$(BUILDTAGS) static_build" \ 43 | ${GO_LDFLAGS_STATIC} -o $(NAME) . 44 | 45 | define buildpretty 46 | mkdir -p $(BUILDDIR)/$(1)/$(2); 47 | GOOS=$(1) GOARCH=$(2) CGO_ENABLED=$(CGO_ENABLED) $(GO) build \ 48 | -o $(BUILDDIR)/$(1)/$(2)/$(NAME) \ 49 | -a -tags "$(BUILDTAGS) static_build netgo" \ 50 | -installsuffix netgo ${GO_LDFLAGS_STATIC} .; 51 | md5sum $(BUILDDIR)/$(1)/$(2)/$(NAME) > $(BUILDDIR)/$(1)/$(2)/$(NAME).md5; 52 | sha256sum $(BUILDDIR)/$(1)/$(2)/$(NAME) > $(BUILDDIR)/$(1)/$(2)/$(NAME).sha256; 53 | endef 54 | 55 | .PHONY: cross 56 | cross: *.go VERSION.txt ## Builds the cross-compiled binaries, creating a clean directory structure (eg. GOOS/GOARCH/binary). 57 | @echo "+ $@" 58 | $(foreach GOOSARCH,$(GOOSARCHES), $(call buildpretty,$(subst /,,$(dir $(GOOSARCH))),$(notdir $(GOOSARCH)))) 59 | 60 | define buildrelease 61 | mkdir -p $(BUILDDIR); 62 | GOOS=$(1) GOARCH=$(2) CGO_ENABLED=$(CGO_ENABLED) $(GO) build \ 63 | -o $(BUILDDIR)/$(NAME)-$(1)-$(2) \ 64 | -a -tags "$(BUILDTAGS) static_build netgo" \ 65 | -installsuffix netgo ${GO_LDFLAGS_STATIC} .; 66 | md5sum $(BUILDDIR)/$(NAME)-$(1)-$(2) > $(BUILDDIR)/$(NAME)-$(1)-$(2).md5; 67 | sha256sum $(BUILDDIR)/$(NAME)-$(1)-$(2) > $(BUILDDIR)/$(NAME)-$(1)-$(2).sha256; 68 | endef 69 | 70 | .PHONY: release 71 | release: *.go VERSION.txt ## Builds the cross-compiled binaries, naming them in such a way for release (eg. binary-GOOS-GOARCH). 72 | @echo "+ $@" 73 | $(foreach GOOSARCH,$(GOOSARCHES), $(call buildrelease,$(subst /,,$(dir $(GOOSARCH))),$(notdir $(GOOSARCH)))) 74 | 75 | DOCKERHUB := leoh0 76 | AGENTNAME := krawler 77 | .PHONY: agent-image 78 | agent-image: ## Create the docker image from the Dockerfile. 79 | @docker build --rm --force-rm -t $(DOCKERHUB)/$(AGENTNAME) -f agent/Dockerfile . 80 | 81 | .PHONY: agent-image-push 82 | agent-image-push: ## Create the docker image from the Dockerfile. 83 | @docker push $(DOCKERHUB)/$(AGENTNAME) 84 | 85 | .PHONY: krawler-recreate 86 | krawler-recreate: 87 | @kubectl delete ds krawler || true 2>/dev/null 88 | @kubectl create -f fixture/krawler.yaml 89 | 90 | .PHONY: clean 91 | clean: ## Cleanup any build binaries or packages. 92 | @echo "+ $@" 93 | $(RM) $(NAME) 94 | $(RM) -r $(BUILDDIR) 95 | 96 | .PHONY: help 97 | help: 98 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | sed 's/^[^:]*://g' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kubectl check-cert 2 | 3 | `kubectl-check-cert` will help you find the kubernetes certificates that can be expired and check the remaining time. 4 | 5 | ## How to use 6 | 7 | after k8s 1.12 version 8 | 9 | $ kubectl check-cert 10 | 11 | or just use below (check name carefully `check_cert` not `check-cert`. See [Names with dashes and underscores](https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/#names-with-dashes-and-underscores)) 12 | 13 | $ kubectl-check_cert 14 | 15 | and you can also check kubelet certification 16 | 17 | $ kubectl-check_cert --also-check-kubelet 18 | 19 | ## Example 20 | 21 | $ kubectl-check_cert --also-check-kubelet 22 | 4 / 4 [============================================================] 100.00% 5s 23 | +--------------------+----------+----------------------------+------+-------------------------------+------------------------------------------------------+----------------------+ 24 | | TYPE | NODE | NAME | DAYS | DUE | PATH | WARNING | 25 | +--------------------+----------+----------------------------+------+-------------------------------+------------------------------------------------------+----------------------+ 26 | | apiserver | minikube | etcd-certfile | 354 | 2020-01-10 15:52:33 +0000 UTC | /var/lib/minikube/certs/apiserver-etcd-client.crt | | 27 | | apiserver | minikube | kubelet-client-certificate | 354 | 2020-01-10 15:52:31 +0000 UTC | /var/lib/minikube/certs/apiserver-kubelet-client.crt | | 28 | | apiserver | minikube | proxy-client-cert-file | 354 | 2020-01-10 15:52:31 +0000 UTC | /var/lib/minikube/certs/front-proxy-client.crt | | 29 | | apiserver | minikube | tls-cert-file | 362 | 2020-01-18 07:29:07 +0000 UTC | /var/lib/minikube/certs/apiserver.crt | | 30 | | controller-manager | minikube | client-cert | 362 | 2020-01-18 07:29:10 +0000 UTC | /etc/kubernetes/controller-manager.conf | | 31 | | scheduler | minikube | client-cert | 362 | 2020-01-18 07:29:10 +0000 UTC | /etc/kubernetes/scheduler.conf | | 32 | | kubelet | minikube | client-cert | 364 | 2020-01-18 07:29:09 +0000 UTC | /etc/kubernetes/kubelet.conf | | 33 | | kubelet | minikube | server-cert | 356 | 2020-01-10 14:51:39 +0000 UTC | /var/lib/kubelet/pki/kubelet.crt | Can be ignored this. | 34 | +--------------------+----------+----------------------------+------+-------------------------------+------------------------------------------------------+----------------------+ 35 | 36 | ## Install 37 | 38 | MacOS 39 | 40 | curl -L https://github.com/leoh0/kubectl-check-cert/releases/download/v0.0.2/kubectl-check_cert_0.0.2_darwin_amd64.tar.gz | tar zxvf - > kubectl-check_cert 41 | chmod +x kubectl-check_cert 42 | sudo mv ./kubectl-check_cert /usr/local/bin/kubectl-check_cert 43 | 44 | Linux 45 | 46 | curl -L https://github.com/leoh0/kubectl-check-cert/releases/download/v0.0.2/kubectl-check_cert_0.0.2_linux_amd64.tar.gz | tar zxvf - > kubectl-check_cert 47 | chmod +x kubectl-check_cert 48 | sudo mv ./kubectl-check_cert /usr/local/bin/kubectl-check_cert 49 | 50 | ## Explain certification types 51 | 52 | ### Apiserver 53 | 54 | |Type|Name|Explain| 55 | |---------|---|---| 56 | |apiserver|etcd-certfile|apiserver -> etcd client certification| 57 | |apiserver|kubelet-client-certificate|apiserver -> kubelet client certification| 58 | |apiserver|proxy-client-cert-file|front-proxy-client| 59 | |apiserver|tls-cert-file|client -> apiserver server certification| 60 | 61 | ### Controller manager 62 | 63 | |Type|Name|Explain| 64 | |---------|---|---| 65 | |controller-manager|client-cert|controller-manager -> apiserver client certification| 66 | 67 | ### Scheduler 68 | 69 | |Type|Name|Explain| 70 | |---------|---|---| 71 | |scheduler|client-cert| scheduler -> apiserver client certification| 72 | 73 | ### Kubelet 74 | 75 | |Type|Name|Explain| 76 | |---------|---|---| 77 | |kubelet|client-cert| kubelet -> apiserver client certification| 78 | |kubelet|server-cert| apiserver -> kubelet server certification| 79 | 80 | ## develop 81 | 82 | make normal build 83 | 84 | $ make build 85 | 86 | 87 | make static build 88 | 89 | $ make static 90 | 91 | make static linux/amd build 92 | 93 | $ docker run --rm -it -v "$GOPATH":/go -v "$PWD":/app -w /app golang:1.11.5 sh -c 'make release' 94 | 95 | ## Note 96 | 97 | * If you use `--also-check-kubelet` option, then it'll install daemon-set for gathering kubelet information. 98 | * You can safely ignore kubelet's server-cert unless you use the `--kubelet-certificate-authority` option in apiserver. This will appear as a message like `Can be ignored this.` 99 | -------------------------------------------------------------------------------- /VERSION.txt: -------------------------------------------------------------------------------- 1 | v0.0.1 2 | -------------------------------------------------------------------------------- /agent/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.11-alpine AS gobuild-base 2 | RUN apk add --no-cache \ 3 | bash \ 4 | build-base \ 5 | gcc \ 6 | git \ 7 | libseccomp-dev \ 8 | linux-headers \ 9 | make 10 | 11 | FROM gobuild-base AS krawler 12 | WORKDIR /app 13 | COPY go.mod go.sum /app/ 14 | RUN go mod tidy 15 | 16 | WORKDIR /app/agent 17 | COPY agent/* /app/agent/ 18 | RUN CGO_ENABLED=1 go build \ 19 | -tags "seccomp static_build" \ 20 | -ldflags " -extldflags -static" -o krawler . && mv krawler /usr/bin/krawler 21 | 22 | FROM alpine:3.8 AS base 23 | COPY --from=krawler /usr/bin/krawler /usr/bin/krawler 24 | 25 | CMD [ "sleep", "3600" ] 26 | -------------------------------------------------------------------------------- /agent/krawler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/x509" 6 | "encoding/json" 7 | "encoding/pem" 8 | "fmt" 9 | "io/ioutil" 10 | "os" 11 | "os/exec" 12 | "path" 13 | "strings" 14 | "time" 15 | 16 | "github.com/spf13/cast" 17 | yaml "gopkg.in/yaml.v2" 18 | 19 | "k8s.io/client-go/tools/clientcmd" 20 | 21 | // ref:https://github.com/kubernetes/client-go/issues/242 22 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 23 | ) 24 | 25 | func main() { 26 | var ( 27 | result string 28 | err error 29 | ) 30 | 31 | hostName := os.Getenv("NODENAME") 32 | if hostName == "" { 33 | ReadorDie("/etc/hostname") 34 | } 35 | 36 | result, err = Execute([]string{"sh", "-c", "grep -rw kubelet /tmp/proc/*/comm | cut -d'/' -f4"}) 37 | if err != nil { 38 | fmt.Println(err) 39 | os.Exit(1) 40 | } 41 | 42 | commands := GetCommandsFromCmdline( 43 | ReadorDie(fmt.Sprintf("/tmp/proc/%s/cmdline", result))) 44 | 45 | var ( 46 | tempCertPath = "" 47 | tempKeyPath = "" 48 | // A and B 여야함 49 | isServerRotateCertA = false 50 | isServerRotateCertB = false 51 | 52 | isClientRotateCert = false 53 | isServerRotateCert = false 54 | kubeletServerCertPath = path.Join(defaultKubeletServerCertPath, "kubelet.crt") 55 | ) 56 | 57 | configData := map[string]interface{}{} 58 | if configPath, ok := commands[configFlag]; ok { 59 | err = yaml.Unmarshal( 60 | []byte(ReadorDie(configPath)), &configData) 61 | 62 | if err != nil { 63 | fmt.Println(err) 64 | os.Exit(1) 65 | } 66 | } 67 | 68 | if data, ok := commands[tlsCertFlag]; ok { 69 | tempCertPath = data 70 | } else if data, ok := configData[tlsCertOption]; ok { 71 | tempCertPath = cast.ToString(data) 72 | } 73 | 74 | if data, ok := commands[tlsKeyFlag]; ok { 75 | tempKeyPath = data 76 | } else if data, ok := configData[tlsKeyOption]; ok { 77 | tempKeyPath = cast.ToString(data) 78 | } 79 | 80 | if data, ok := commands[rotateServerCertFlag]; ok { 81 | isServerRotateCertA = cast.ToBool(data) 82 | } else if data, ok := configData[rotateServerCertOption]; ok { 83 | isServerRotateCertA = cast.ToBool(data) 84 | } 85 | 86 | for _, v := range strings.Split(commands[featureGatesFlag], ",") { 87 | value := strings.Split(v, "=") 88 | if value[0] == rotateKubeletServerCertFeature { 89 | isServerRotateCertB = cast.ToBool(value[1]) 90 | } 91 | } 92 | if features, ok := configData[featureGatesOption].(map[interface{}]interface{}); ok { 93 | if data, ok := features[rotateKubeletServerCertFeature]; ok { 94 | isServerRotateCertB = cast.ToBool(data) 95 | } 96 | } 97 | 98 | isServerRotateCert = isServerRotateCertA && isServerRotateCertB 99 | 100 | if tempCertPath != "" && tempKeyPath != "" { 101 | kubeletServerCertPath = tempCertPath 102 | } else if isServerRotateCert { 103 | kubeletServerCertPath = path.Join(defaultKubeletServerCertPath, "kubelet-server-current.pem") 104 | } else if data, ok := commands[certDirFlag]; ok { 105 | kubeletServerCertPath = path.Join(data, "kubelet.crt") 106 | } 107 | 108 | date, days := GetDateAndDaysFromCert( 109 | ReadorDie(kubeletServerCertPath)) 110 | 111 | e := []Entry{} 112 | e = append(e, *NewEntry(hostName, "server-cert", days, date, kubeletServerCertPath)) 113 | 114 | if data, ok := commands[rotateCertFlag]; ok { 115 | isClientRotateCert = cast.ToBool(data) 116 | } else if data, ok := configData[rotateCertOption]; ok { 117 | isClientRotateCert = cast.ToBool(data) 118 | } 119 | 120 | // TODO: also return this value 121 | _ = isClientRotateCert 122 | 123 | if kubeConfigPath, ok := commands[kubeConfigFlag]; ok { 124 | cfg, err := clientcmd.NewClientConfigFromBytes( 125 | []byte(ReadorDie(kubeConfigPath))) 126 | if err != nil { 127 | fmt.Println(err) 128 | os.Exit(1) 129 | } 130 | rawConfig, err := cfg.RawConfig() 131 | if err != nil { 132 | fmt.Println(err) 133 | os.Exit(1) 134 | } 135 | 136 | var ( 137 | kubeletClientCertFile string 138 | kubeletClientCertPath string 139 | ) 140 | currentContext := rawConfig.CurrentContext 141 | user := rawConfig.Contexts[currentContext].AuthInfo 142 | u := rawConfig.AuthInfos[user] 143 | 144 | if string(u.ClientCertificateData) != "" { 145 | kubeletClientCertFile = string(u.ClientCertificateData) 146 | kubeletClientCertPath = kubeConfigPath 147 | } else if string(u.ClientCertificate) != "" { 148 | kubeletClientCertFile = ReadorDie(strings.Trim(string(u.ClientCertificate), "\n")) 149 | kubeletClientCertPath = string(u.ClientCertificate) 150 | } else { 151 | fmt.Println(err) 152 | os.Exit(1) 153 | } 154 | 155 | date, days := GetDateAndDaysFromCert(kubeletClientCertFile) 156 | 157 | e = append(e, *NewEntry(hostName, "client-cert", days, date, kubeletClientCertPath)) 158 | 159 | } else { 160 | fmt.Println(err) 161 | os.Exit(1) 162 | } 163 | 164 | o := Output{ 165 | Entries: e, 166 | } 167 | 168 | buffer := &bytes.Buffer{} 169 | if err := json.NewEncoder(buffer).Encode(o); err != nil { 170 | fmt.Println(err) 171 | os.Exit(1) 172 | } 173 | 174 | // print to stdout 175 | fmt.Print(buffer.String()) 176 | } 177 | 178 | // Execute run command and return it's output 179 | func Execute(cmd []string) (string, error) { 180 | var ( 181 | out []byte 182 | err error 183 | ) 184 | if len(cmd) > 1 { 185 | out, err = exec.Command(cmd[0], cmd[1:]...).Output() 186 | } else { 187 | out, err = exec.Command(cmd[0]).Output() 188 | } 189 | if err != nil { 190 | fmt.Printf("Failed to execute command: %s\n", strings.Join(cmd, " ")) 191 | return "", err 192 | } 193 | 194 | return strings.Trim(string(out), "\n"), nil 195 | } 196 | 197 | // GetDateAndDaysFromCert takes a cert and extract Date and Days 198 | func GetDateAndDaysFromCert(cert string) (time.Time, int) { 199 | timeNow := time.Now() 200 | block, _ := pem.Decode([]byte(cert)) 201 | if block == nil { 202 | fmt.Println("failed to parse certificate PEM") 203 | os.Exit(1) 204 | } 205 | c, err := x509.ParseCertificate(block.Bytes) 206 | if err != nil { 207 | fmt.Println("failed to parse certificate: " + err.Error()) 208 | os.Exit(1) 209 | } 210 | expiresIn := int(c.NotAfter.Sub(timeNow).Hours() / 24) 211 | 212 | return c.NotAfter, expiresIn 213 | } 214 | 215 | // GetCommandsFromCmdline takes a cmdline and parse it to several commands 216 | func GetCommandsFromCmdline(cmdline string) map[string]string { 217 | var ( 218 | cmds = map[string]string{} 219 | value string 220 | ) 221 | 222 | for _, c := range strings.Split(cmdline, "--") { 223 | c = string(bytes.Trim([]byte(c), "\x00")) 224 | s := strings.Split(c, "=") 225 | 226 | if len(s) == 1 { 227 | value = "true" 228 | } else { 229 | value = strings.Join(s[1:], "=") 230 | } 231 | 232 | cmds[s[0]] = strings.Trim(value, "\n") 233 | } 234 | 235 | return cmds 236 | } 237 | 238 | // ReadorDie read fileName file and return it's content 239 | func ReadorDie(fileName string) string { 240 | body, err := ioutil.ReadFile(fileName) 241 | if err != nil { 242 | fmt.Println("Read file fail: " + fileName + err.Error()) 243 | os.Exit(1) 244 | } 245 | 246 | return strings.Trim(string(body), "\n") 247 | } 248 | -------------------------------------------------------------------------------- /agent/krawler_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | "github.com/spf13/cast" 13 | 14 | "github.com/stretchr/testify/assert" 15 | yaml "gopkg.in/yaml.v2" 16 | ) 17 | 18 | func TestParsingYaml(t *testing.T) { 19 | kubeletConfigFile := ` 20 | address: 0.0.0.0 21 | apiVersion: kubelet.config.k8s.io/v1beta1 22 | authentication: 23 | anonymous: 24 | enabled: false 25 | webhook: 26 | cacheTTL: 2m0s 27 | enabled: true 28 | x509: 29 | clientCAFile: /etc/kubernetes/pki/ca.crt 30 | authorization: 31 | mode: Webhook 32 | webhook: 33 | cacheAuthorizedTTL: 5m0s 34 | cacheUnauthorizedTTL: 30s 35 | cgroupDriver: cgroupfs 36 | cgroupsPerQOS: true 37 | clusterDNS: 38 | - 10.96.0.10 39 | clusterDomain: cluster.local 40 | containerLogMaxFiles: 5 41 | containerLogMaxSize: 10Mi 42 | contentType: application/vnd.kubernetes.protobuf 43 | cpuCFSQuota: true 44 | cpuManagerPolicy: none 45 | cpuManagerReconcilePeriod: 10s 46 | enableControllerAttachDetach: true 47 | enableDebuggingHandlers: true 48 | enforceNodeAllocatable: 49 | - pods 50 | eventBurst: 10 51 | eventRecordQPS: 5 52 | evictionHard: 53 | imagefs.available: 15% 54 | memory.available: 100Mi 55 | nodefs.available: 10% 56 | nodefs.inodesFree: 5% 57 | evictionPressureTransitionPeriod: 5m0s 58 | failSwapOn: true 59 | fileCheckFrequency: 20s 60 | hairpinMode: promiscuous-bridge 61 | healthzBindAddress: 127.0.0.1 62 | healthzPort: 10248 63 | httpCheckFrequency: 20s 64 | imageGCHighThresholdPercent: 85 65 | imageGCLowThresholdPercent: 80 66 | imageMinimumGCAge: 2m0s 67 | iptablesDropBit: 15 68 | iptablesMasqueradeBit: 14 69 | kind: KubeletConfiguration 70 | kubeAPIBurst: 10 71 | kubeAPIQPS: 5 72 | makeIPTablesUtilChains: true 73 | maxOpenFiles: 1000000 74 | maxPods: 110 75 | nodeStatusUpdateFrequency: 10s 76 | oomScoreAdj: -999 77 | podPidsLimit: -1 78 | port: 10250 79 | registryBurst: 10 80 | registryPullQPS: 5 81 | resolvConf: /etc/resolv.conf 82 | rotateCertificates: true 83 | runtimeRequestTimeout: 2m0s 84 | serializeImagePulls: true 85 | staticPodPath: /etc/kubernetes/manifests 86 | streamingConnectionIdleTimeout: 4h0m0s 87 | syncFrequency: 1m0s 88 | volumeStatsAggPeriod: 1m0s 89 | featureGates: 90 | RotateKubeletServerCertificate: true 91 | ` 92 | 93 | config := map[string]interface{}{} 94 | _ = yaml.Unmarshal([]byte(kubeletConfigFile), &config) 95 | 96 | assert.Equal(t, true, config[rotateCertOption]) 97 | 98 | if features, ok := config[featureGatesOption].(map[interface{}]interface{}); ok { 99 | if data, ok := features[rotateKubeletServerCertFeature]; ok { 100 | assert.Equal(t, true, data) 101 | } 102 | } 103 | 104 | } 105 | 106 | func TestParsingFlag(t *testing.T) { 107 | s := "/usr/bin/kubelet--bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf--kubeconfig=/etc/kubernetes/kubelet.conf--config=/var/lib/kubelet/config.yaml--cgroup-driver=systemd--cni-bin-dir=/opt/cni/bin--cni-conf-dir=/etc/cni/net.d--network-plugin=cni--feature-gates=RotateKubeletServerCertificate=true--allowed-unsafe-sysctls=net.*" 108 | commands := GetCommandsFromCmdline(s) 109 | 110 | assert.Equal(t, "/var/lib/kubelet/config.yaml", commands[configFlag]) 111 | assert.Equal(t, "", commands[certDirFlag]) 112 | assert.Equal(t, "/etc/kubernetes/kubelet.conf", commands[kubeConfigFlag]) 113 | assert.Equal(t, "", commands[rotateCertFlag]) 114 | assert.Equal(t, "", commands[rotateServerCertFlag]) 115 | assert.Equal(t, "", commands[tlsCertFlag]) 116 | assert.Equal(t, "", commands[tlsKeyFlag]) 117 | assert.Equal(t, "RotateKubeletServerCertificate=true", commands[featureGatesFlag]) 118 | for _, v := range strings.Split(commands[featureGatesFlag], ",") { 119 | value := strings.Split(v, "=") 120 | if value[0] == rotateKubeletServerCertFeature { 121 | assert.Equal(t, true, cast.ToBool(value[1])) 122 | } 123 | } 124 | } 125 | 126 | func TestJsonEncode(t *testing.T) { 127 | o := Output{} 128 | o.Entries = append(o.Entries, *NewEntry("node", "name", 1, time.Date(2019, time.January, 1, 0, 0, 0, 0, time.UTC), "path")) 129 | 130 | buffer := &bytes.Buffer{} 131 | if err := json.NewEncoder(buffer).Encode(o); err != nil { 132 | fmt.Println(err) 133 | os.Exit(1) 134 | } 135 | 136 | t.Log(buffer.String()) 137 | assert.Equal(t, "{\"entry\":[{\"type\":\"kubelet\",\"node\":\"node\",\"name\":\"name\",\"days\":1,\"due\":\"2019-01-01T00:00:00Z\",\"path\":\"path\"}]}\n", buffer.String()) 138 | } 139 | -------------------------------------------------------------------------------- /agent/type.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "time" 4 | 5 | const ( 6 | // only in flag 7 | configFlag = "config" 8 | certDirFlag = "cert-dir" 9 | 10 | // flag and options 11 | kubeConfigFlag = "kubeconfig" 12 | rotateCertFlag = "rotate-certificates" 13 | rotateServerCertFlag = "rotate-server-certificates" 14 | tlsCertFlag = "tls-cert-file" 15 | tlsKeyFlag = "tls-key-file" 16 | featureGatesFlag = "feature-gates" 17 | 18 | kubeConfigOption = "kubeconfig" 19 | rotateCertOption = "rotateCertificates" 20 | rotateServerCertOption = "serverTLSBootstrap" 21 | tlsCertOption = "tlsCertFile" 22 | tlsKeyOption = "tlsKeyFile" 23 | featureGatesOption = "featureGates" 24 | 25 | rotateKubeletServerCertFeature = "RotateKubeletServerCertificate" 26 | 27 | defaultKubeletServerCertPath = "/var/lib/kubelet/pki/" 28 | 29 | entryType = "kubelet" 30 | ) 31 | 32 | // Output is for json stdout 33 | type Output struct { 34 | Entries []Entry `json:"entry"` 35 | } 36 | 37 | // Entry is for cert entries 38 | type Entry struct { 39 | Type string `json:"type"` 40 | Node string `json:"node"` 41 | Name string `json:"name"` 42 | Days int `json:"days"` 43 | Due time.Time `json:"due"` 44 | Path string `json:"path"` 45 | } 46 | 47 | // NewEntry make entry 48 | func NewEntry(node string, name string, days int, due time.Time, path string) *Entry { 49 | return &Entry{ 50 | Type: entryType, 51 | Node: node, 52 | Name: name, 53 | Days: days, 54 | Due: due, 55 | Path: path, 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /cmd/check_cert.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "crypto/x509" 6 | "encoding/json" 7 | "encoding/pem" 8 | "fmt" 9 | "os" 10 | "sort" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "github.com/prometheus/common/log" 16 | "github.com/spf13/cast" 17 | "github.com/spf13/cobra" 18 | pb "gopkg.in/cheggaaa/pb.v1" 19 | 20 | "k8s.io/client-go/kubernetes/scheme" 21 | appsV1Client "k8s.io/client-go/kubernetes/typed/apps/v1" 22 | coreV1Client "k8s.io/client-go/kubernetes/typed/core/v1" 23 | "k8s.io/client-go/rest" 24 | "k8s.io/client-go/tools/clientcmd" 25 | "k8s.io/client-go/tools/remotecommand" 26 | 27 | // ref:https://github.com/kubernetes/client-go/issues/242 28 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 29 | 30 | appv1 "k8s.io/api/apps/v1" 31 | corev1 "k8s.io/api/core/v1" 32 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 | "k8s.io/apimachinery/pkg/util/intstr" 34 | "k8s.io/cli-runtime/pkg/genericclioptions" 35 | 36 | "github.com/olekukonko/tablewriter" 37 | ) 38 | 39 | const ( 40 | kubesystemNamespace = "kube-system" 41 | defaultNamespace = "default" 42 | 43 | kubeletCAFlag = "kubelet-certificate-authority" 44 | 45 | name = "krawler" 46 | imageName = "leoh0/krawler" 47 | etcKubernetesPath = "/etc/kubernetes/" 48 | etcKubernetesName = "etc-kubernetes" 49 | varLibKubeletPath = "/var/lib/kubelet/" 50 | varLibKubeletName = "var-lib-kubelet" 51 | realProcPath = "/proc/" 52 | tmpProcPath = "/tmp/proc/" 53 | tmpProcName = "tmp-proc" 54 | ) 55 | 56 | var ( 57 | expirationExample = ` 58 | # view expiration days of certifications about control plane (e.g. apiserver, controller-manager, scheduler) 59 | %[1]s check-cert 60 | 61 | # view expiration days of certifications about control plane and also kubelets by installing crawling daemon-set 62 | %[1]s check-cert --also-check-kubelet 63 | ` 64 | 65 | certOptions = []string{"etcd-certfile", "tls-cert-file", "kubelet-client-certificate", "proxy-client-cert-file"} 66 | 67 | matchLabel = map[string]string{"app": name} 68 | max = intstr.FromString("100%") 69 | 70 | hostPathType = corev1.HostPathDirectory 71 | hostPathFileType = corev1.HostPathFile 72 | 73 | checkKubeletWithCA = false 74 | clientConfig *rest.Config 75 | ) 76 | 77 | type serverCertification struct { 78 | Entry Entry 79 | Warning string 80 | } 81 | 82 | // ExpirationOptions provides information 83 | type ExpirationOptions struct { 84 | configFlags *genericclioptions.ConfigFlags 85 | genericclioptions.IOStreams 86 | 87 | checkKubelet bool 88 | } 89 | 90 | // NewExpirationOptions provides an instance of ExpirationOptions with default values 91 | func NewExpirationOptions(streams genericclioptions.IOStreams) *ExpirationOptions { 92 | return &ExpirationOptions{ 93 | configFlags: genericclioptions.NewConfigFlags(true), 94 | checkKubelet: false, 95 | IOStreams: streams, 96 | } 97 | } 98 | 99 | // NewCmdExpiration provides a cobra command wrapping ExpirationOptions 100 | func NewCmdExpiration(streams genericclioptions.IOStreams) *cobra.Command { 101 | o := NewExpirationOptions(streams) 102 | 103 | cmd := &cobra.Command{ 104 | Use: "check-cert [flags]", 105 | Short: "View expiration days of certifications in kubernetes cluster", 106 | Example: fmt.Sprintf(expirationExample, "kubectl"), 107 | SilenceUsage: true, 108 | RunE: func(c *cobra.Command, args []string) error { 109 | if err := o.Run(c); err != nil { 110 | return err 111 | } 112 | return nil 113 | }, 114 | } 115 | 116 | cmd.Flags().BoolVar(&o.checkKubelet, "also-check-kubelet", false, "if true, also check kubelet certification") 117 | o.configFlags.AddFlags(cmd.Flags()) 118 | 119 | return cmd 120 | } 121 | 122 | func getPods(coreclient *coreV1Client.CoreV1Client, namespace string, label string) (*corev1.PodList, error) { 123 | listOption := meta_v1.ListOptions{ 124 | LabelSelector: label, 125 | } 126 | 127 | pods, err := coreclient.Pods(namespace).List(listOption) 128 | if len(pods.Items) == 0 || err != nil { 129 | return &corev1.PodList{}, err 130 | } 131 | 132 | return pods, nil 133 | } 134 | 135 | func isJSON(s string) (*Output, bool) { 136 | var e Output 137 | val := json.Unmarshal([]byte(s), &e) 138 | return &e, val == nil 139 | } 140 | 141 | // Run gather all information 142 | func (o *ExpirationOptions) Run(cmd *cobra.Command) error { 143 | var err error 144 | clientConfig, err = o.configFlags.ToRESTConfig() 145 | if err != nil { 146 | return err 147 | } 148 | coreclient, err := coreV1Client.NewForConfig(clientConfig) 149 | if err != nil { 150 | return err 151 | } 152 | 153 | if o.checkKubelet { 154 | daemonSet := &appv1.DaemonSet{ 155 | ObjectMeta: meta_v1.ObjectMeta{ 156 | Name: name, 157 | }, 158 | Spec: appv1.DaemonSetSpec{ 159 | UpdateStrategy: appv1.DaemonSetUpdateStrategy{ 160 | Type: appv1.RollingUpdateDaemonSetStrategyType, 161 | RollingUpdate: &appv1.RollingUpdateDaemonSet{ 162 | MaxUnavailable: &max, 163 | }, 164 | }, 165 | Selector: &meta_v1.LabelSelector{ 166 | MatchLabels: matchLabel, 167 | }, 168 | Template: corev1.PodTemplateSpec{ 169 | ObjectMeta: meta_v1.ObjectMeta{ 170 | Labels: matchLabel, 171 | }, 172 | Spec: corev1.PodSpec{ 173 | HostPID: true, 174 | HostNetwork: true, 175 | Containers: []corev1.Container{{ 176 | Name: name, 177 | Env: []corev1.EnvVar{ 178 | { 179 | Name: "NODENAME", 180 | ValueFrom: &corev1.EnvVarSource{ 181 | FieldRef: &corev1.ObjectFieldSelector{ 182 | FieldPath: "spec.nodeName", 183 | }, 184 | }, 185 | }, 186 | }, 187 | Image: imageName, 188 | ImagePullPolicy: corev1.PullAlways, 189 | VolumeMounts: []corev1.VolumeMount{ 190 | { 191 | Name: etcKubernetesName, 192 | MountPath: etcKubernetesPath, 193 | }, { 194 | Name: varLibKubeletName, 195 | MountPath: varLibKubeletPath, 196 | }, { 197 | Name: tmpProcName, 198 | MountPath: tmpProcPath, 199 | }, 200 | }, 201 | }}, 202 | RestartPolicy: corev1.RestartPolicyAlways, 203 | Tolerations: []corev1.Toleration{{ 204 | Operator: corev1.TolerationOpExists, 205 | }}, 206 | Volumes: []corev1.Volume{ 207 | { 208 | Name: etcKubernetesName, 209 | VolumeSource: corev1.VolumeSource{ 210 | HostPath: &corev1.HostPathVolumeSource{ 211 | Type: &hostPathType, 212 | Path: etcKubernetesPath, 213 | }, 214 | }, 215 | }, { 216 | Name: varLibKubeletName, 217 | VolumeSource: corev1.VolumeSource{ 218 | HostPath: &corev1.HostPathVolumeSource{ 219 | Type: &hostPathType, 220 | Path: varLibKubeletPath, 221 | }, 222 | }, 223 | }, { 224 | Name: tmpProcName, 225 | VolumeSource: corev1.VolumeSource{ 226 | HostPath: &corev1.HostPathVolumeSource{ 227 | Type: &hostPathType, 228 | Path: realProcPath, 229 | }, 230 | }, 231 | }, 232 | }, 233 | }, 234 | }, 235 | }, 236 | } 237 | 238 | appClient, err := appsV1Client.NewForConfig(clientConfig) 239 | if err != nil { 240 | return err 241 | } 242 | 243 | _, err = appClient.DaemonSets(defaultNamespace).Create(daemonSet) 244 | 245 | defer appClient.DaemonSets(defaultNamespace).Delete(name, nil) 246 | } 247 | 248 | apiServerPods, err := getPods( 249 | coreclient, kubesystemNamespace, "component=kube-apiserver,tier=control-plane") 250 | if err != nil { 251 | fmt.Println("Apiserver is not exists. Skip.") 252 | } 253 | 254 | controllerManagerPods, err := getPods( 255 | coreclient, kubesystemNamespace, "component=kube-controller-manager,tier=control-plane") 256 | if err != nil { 257 | fmt.Println("ControllerManager is not exists. Skip.") 258 | } 259 | 260 | schedulerManagerPods, err := getPods( 261 | coreclient, kubesystemNamespace, "component=kube-scheduler,tier=control-plane") 262 | if err != nil { 263 | fmt.Println("Scheduler is not exists. Skip.") 264 | } 265 | 266 | dsPodCount := 0 267 | if o.checkKubelet { 268 | appClient, err := appsV1Client.NewForConfig(clientConfig) 269 | if err != nil { 270 | return err 271 | } 272 | 273 | dsClient := appClient.DaemonSets(defaultNamespace) 274 | if err != nil { 275 | fmt.Println("Exist already") 276 | } else { 277 | for { 278 | time.Sleep(time.Second / 2) 279 | 280 | ds, err := dsClient.Get(name, meta_v1.GetOptions{}) 281 | if err != nil { 282 | return err 283 | } 284 | if ds.Status.DesiredNumberScheduled > 0 { 285 | dsPodCount = int(ds.Status.DesiredNumberScheduled) 286 | break 287 | } 288 | } 289 | } 290 | } 291 | 292 | bar := pb.New(len(apiServerPods.Items) + len(controllerManagerPods.Items) + len(schedulerManagerPods.Items) + dsPodCount) 293 | bar.SetWidth(80) 294 | bar.SetMaxWidth(80) 295 | bar.Start() 296 | 297 | var wg sync.WaitGroup 298 | var mutex = &sync.Mutex{} 299 | 300 | channel := make(chan interface{}) 301 | 302 | serverCertifications := make([]serverCertification, 0) 303 | for _, apiServerPod := range apiServerPods.Items { 304 | wg.Add(1) 305 | go func(p corev1.Pod) { 306 | defer wg.Done() 307 | for _, c := range p.Spec.Containers[0].Command { 308 | // only for options 309 | if strings.HasPrefix(c, "--") { 310 | s := strings.Split(c[2:], "=") 311 | if s[0] == kubeletCAFlag && s[1] != "" { 312 | mutex.Lock() 313 | checkKubeletWithCA = true 314 | mutex.Unlock() 315 | } 316 | for _, co := range certOptions { 317 | if co == s[0] { 318 | cert, err := ExecPod(coreclient, kubesystemNamespace, &p, []string{"cat", s[1]}) 319 | errorStr := "" 320 | if err != nil { 321 | errorStr = err.Error() 322 | } else { 323 | date, days, err := GetDateAndDaysFromCert(cert) 324 | if err != nil { 325 | errorStr = err.Error() 326 | } else { 327 | channel <- serverCertification{ 328 | Entry: Entry{ 329 | Type: "apiserver", 330 | Node: p.Spec.NodeName, 331 | Name: s[0], 332 | Path: s[1], 333 | Days: days, 334 | Due: date, 335 | }, 336 | Warning: "", 337 | } 338 | } 339 | } 340 | if errorStr != "" { 341 | channel <- serverCertification{ 342 | Entry: Entry{ 343 | Type: "apiserver", 344 | Node: p.Spec.NodeName, 345 | Name: s[0], 346 | Path: s[1], 347 | Days: 0, 348 | Due: time.Time{}, 349 | }, 350 | Warning: err.Error(), 351 | } 352 | } 353 | } 354 | } 355 | } 356 | } 357 | mutex.Lock() 358 | bar.Increment() 359 | mutex.Unlock() 360 | }(apiServerPod) 361 | } 362 | 363 | for _, controllerManagerPod := range controllerManagerPods.Items { 364 | wg.Add(1) 365 | go func(p corev1.Pod) { 366 | defer wg.Done() 367 | for _, c := range p.Spec.Containers[0].Command { 368 | // only for options 369 | if strings.HasPrefix(c, "--") { 370 | s := strings.Split(c[2:], "=") 371 | if s[0] == kubeConfigFlag { 372 | errorResult := func(node string, errstr string) { 373 | channel <- serverCertification{ 374 | Entry: Entry{ 375 | Type: "controller-manager", 376 | Node: node, 377 | Name: "client-cert", 378 | Path: "-", 379 | Days: 0, 380 | Due: time.Time{}, 381 | }, 382 | Warning: errstr, 383 | } 384 | } 385 | kubeconfig, err := ExecPod(coreclient, kubesystemNamespace, &p, []string{"cat", s[1]}) 386 | if err != nil { 387 | errorResult(p.Spec.NodeName, err.Error()) 388 | } 389 | cfg, err := clientcmd.NewClientConfigFromBytes([]byte(kubeconfig)) 390 | if err != nil { 391 | errorResult(p.Spec.NodeName, err.Error()) 392 | } 393 | rawConfig, err := cfg.RawConfig() 394 | if err != nil { 395 | errorResult(p.Spec.NodeName, err.Error()) 396 | } 397 | 398 | var cert string 399 | var path string 400 | currentContext := rawConfig.CurrentContext 401 | user := rawConfig.Contexts[currentContext].AuthInfo 402 | u := rawConfig.AuthInfos[user] 403 | 404 | if string(u.ClientCertificateData) != "" { 405 | cert = string(u.ClientCertificateData) 406 | path = s[1] 407 | } else if string(u.ClientCertificate) != "" { 408 | cert, err = ExecPod(coreclient, kubesystemNamespace, &p, []string{"cat", string(u.ClientCertificate)}) 409 | if err != nil { 410 | errorResult(p.Spec.NodeName, err.Error()) 411 | } 412 | path = string(u.ClientCertificate) 413 | } else { 414 | errorResult(p.Spec.NodeName, err.Error()) 415 | } 416 | 417 | date, days, err := GetDateAndDaysFromCert(cert) 418 | channel <- serverCertification{ 419 | Entry: Entry{ 420 | Type: "controller-manager", 421 | Node: p.Spec.NodeName, 422 | Name: "client-cert", 423 | Path: path, 424 | Days: days, 425 | Due: date, 426 | }, 427 | Warning: "", 428 | } 429 | break 430 | } 431 | } 432 | } 433 | mutex.Lock() 434 | bar.Increment() 435 | mutex.Unlock() 436 | }(controllerManagerPod) 437 | } 438 | 439 | for _, schedulerManagerPod := range schedulerManagerPods.Items { 440 | wg.Add(1) 441 | go func(p corev1.Pod) { 442 | defer wg.Done() 443 | for _, c := range p.Spec.Containers[0].Command { 444 | // only for options 445 | if strings.HasPrefix(c, "--") { 446 | s := strings.Split(c[2:], "=") 447 | if s[0] == kubeConfigFlag { 448 | errorResult := func(node string, errstr string) { 449 | channel <- serverCertification{ 450 | Entry: Entry{ 451 | Type: "scheduler", 452 | Node: node, 453 | Name: "client-cert", 454 | Path: "-", 455 | Days: 0, 456 | Due: time.Time{}, 457 | }, 458 | Warning: errstr, 459 | } 460 | } 461 | kubeconfig, err := ExecPod(coreclient, kubesystemNamespace, &p, []string{"cat", s[1]}) 462 | if err != nil { 463 | errorResult(p.Spec.NodeName, err.Error()) 464 | } 465 | cfg, err := clientcmd.NewClientConfigFromBytes([]byte(kubeconfig)) 466 | if err != nil { 467 | errorResult(p.Spec.NodeName, err.Error()) 468 | } 469 | rawConfig, err := cfg.RawConfig() 470 | if err != nil { 471 | errorResult(p.Spec.NodeName, err.Error()) 472 | } 473 | 474 | var cert string 475 | var path string 476 | currentContext := rawConfig.CurrentContext 477 | user := rawConfig.Contexts[currentContext].AuthInfo 478 | u := rawConfig.AuthInfos[user] 479 | 480 | if string(u.ClientCertificateData) != "" { 481 | cert = string(u.ClientCertificateData) 482 | path = s[1] 483 | } else if string(u.ClientCertificate) != "" { 484 | cert, err = ExecPod(coreclient, kubesystemNamespace, &p, []string{"cat", string(u.ClientCertificate)}) 485 | if err != nil { 486 | errorResult(p.Spec.NodeName, err.Error()) 487 | } 488 | path = string(u.ClientCertificate) 489 | } else { 490 | errorResult(p.Spec.NodeName, err.Error()) 491 | } 492 | 493 | date, days, err := GetDateAndDaysFromCert(cert) 494 | channel <- serverCertification{ 495 | Entry: Entry{ 496 | Type: "scheduler", 497 | Node: p.Spec.NodeName, 498 | Name: "client-cert", 499 | Path: path, 500 | Days: days, 501 | Due: date, 502 | }, 503 | Warning: "", 504 | } 505 | break 506 | } 507 | } 508 | } 509 | mutex.Lock() 510 | bar.Increment() 511 | mutex.Unlock() 512 | }(schedulerManagerPod) 513 | } 514 | 515 | if o.checkKubelet { 516 | appClient, err := appsV1Client.NewForConfig(clientConfig) 517 | if err != nil { 518 | return err 519 | } 520 | 521 | dsClient := appClient.DaemonSets(defaultNamespace) 522 | if err != nil { 523 | fmt.Println("Exist already") 524 | } else { 525 | for { 526 | time.Sleep(time.Second / 2) 527 | 528 | ds, err := dsClient.Get(name, meta_v1.GetOptions{}) 529 | if err != nil { 530 | return err 531 | } 532 | if ds.Status.NumberAvailable == ds.Status.DesiredNumberScheduled && ds.Status.DesiredNumberScheduled > 0 { 533 | time.Sleep(time.Second) 534 | break 535 | } 536 | } 537 | } 538 | 539 | krawlerPods, err := getPods( 540 | coreclient, defaultNamespace, fmt.Sprintf("app=%s", name)) 541 | if err != nil { 542 | return err 543 | } 544 | 545 | for _, p := range krawlerPods.Items { 546 | wg.Add(1) 547 | go func(p corev1.Pod) { 548 | defer wg.Done() 549 | for i := 1; i <= 10; i++ { 550 | if p.Status.Phase != corev1.PodRunning { 551 | time.Sleep(time.Second / 2) 552 | } 553 | } 554 | // p.Spec.NodeName 555 | var command string 556 | for i := 1; i <= 5; i++ { 557 | command, err = ExecPod( 558 | coreclient, 559 | defaultNamespace, 560 | &p, 561 | []string{"krawler"}, 562 | ) 563 | if err == nil { 564 | break 565 | } 566 | time.Sleep(time.Second / 2) 567 | } 568 | 569 | if value, ok := isJSON(command); ok { 570 | for _, v := range value.Entries { 571 | warn := "" 572 | if v.Name == "server-cert" && checkKubeletWithCA == false { 573 | warn = "Can be ignored this." 574 | } 575 | channel <- serverCertification{ 576 | Entry: v, 577 | Warning: warn, 578 | } 579 | } 580 | } else { 581 | channel <- serverCertification{ 582 | Entry: Entry{ 583 | Type: "kubelet", 584 | Node: p.Spec.NodeName, 585 | Name: "Error", 586 | Path: "", 587 | Days: 0, 588 | Due: time.Now(), 589 | }, 590 | Warning: command, 591 | } 592 | } 593 | mutex.Lock() 594 | bar.Increment() 595 | mutex.Unlock() 596 | 597 | }(p) 598 | } 599 | } 600 | 601 | go func() { 602 | defer close(channel) 603 | wg.Wait() 604 | }() 605 | 606 | for result := range channel { 607 | switch v := result.(type) { 608 | case serverCertification: 609 | serverCertifications = append(serverCertifications, v) 610 | default: 611 | err := fmt.Errorf("Unknown type %T, %+v", result, result) 612 | log.Error(err) 613 | return nil 614 | } 615 | } 616 | 617 | bar.Finish() 618 | sort.Slice(serverCertifications, func(i, j int) bool { 619 | if serverCertifications[i].Entry.Type == "scheduler" && serverCertifications[j].Entry.Type == "kubelet" { 620 | return true 621 | } 622 | if serverCertifications[i].Entry.Type == "kubelet" && serverCertifications[j].Entry.Type == "scheduler" { 623 | return false 624 | } 625 | if serverCertifications[i].Entry.Type < serverCertifications[j].Entry.Type { 626 | return true 627 | } 628 | if serverCertifications[i].Entry.Type > serverCertifications[j].Entry.Type { 629 | return false 630 | } 631 | if serverCertifications[i].Entry.Node < serverCertifications[j].Entry.Node { 632 | return true 633 | } 634 | if serverCertifications[i].Entry.Node > serverCertifications[j].Entry.Node { 635 | return false 636 | } 637 | return serverCertifications[i].Entry.Name < serverCertifications[j].Entry.Name 638 | }) 639 | 640 | table := tablewriter.NewWriter(os.Stdout) 641 | table.SetHeader([]string{"Type", "Node", "Name", "Days", "Due", "Path", "Warning"}) 642 | 643 | for _, v := range serverCertifications { 644 | m := []string{v.Entry.Type, v.Entry.Node, v.Entry.Name, cast.ToString(v.Entry.Days), v.Entry.Due.String(), v.Entry.Path, v.Warning} 645 | table.Append(m) 646 | } 647 | table.Render() // Send output 648 | 649 | return nil 650 | } 651 | 652 | // ExecPod sets all information required for updating the current context 653 | func ExecPod(coreclient *coreV1Client.CoreV1Client, namespace string, pod *corev1.Pod, command []string) (string, error) { 654 | req := coreclient.RESTClient(). 655 | Post(). 656 | Namespace(namespace). 657 | Resource("pods"). 658 | Name(pod.Name). 659 | SubResource("exec"). 660 | VersionedParams(&corev1.PodExecOptions{ 661 | Container: pod.Spec.Containers[0].Name, 662 | Command: command, 663 | Stdin: false, 664 | Stdout: true, 665 | Stderr: true, 666 | TTY: false, 667 | }, scheme.ParameterCodec) 668 | 669 | exec, err := remotecommand.NewSPDYExecutor(clientConfig, "POST", req.URL()) 670 | if err != nil { 671 | return "", fmt.Errorf("%s: %s", req.URL().String(), err.Error()) 672 | } 673 | 674 | var stdout, stderr bytes.Buffer 675 | err = exec.Stream(remotecommand.StreamOptions{ 676 | Stdin: nil, 677 | Stdout: &stdout, 678 | Stderr: &stderr, 679 | Tty: false, 680 | }) 681 | 682 | if err != nil { 683 | return "", fmt.Errorf("%s: %s", strings.Join(command, " "), err.Error()) 684 | } 685 | 686 | if stderr.String() != "" { 687 | fmt.Println("Error : " + stderr.String()) 688 | } 689 | 690 | return stdout.String(), nil 691 | } 692 | 693 | // GetDateAndDaysFromCert takes a cert and extract Date and Days 694 | func GetDateAndDaysFromCert(cert string) (time.Time, int, error) { 695 | var err error 696 | timeNow := time.Now() 697 | block, _ := pem.Decode([]byte(cert)) 698 | if block == nil { 699 | return time.Time{}, 0, fmt.Errorf("failed to parse certificate PEM") 700 | } 701 | c, err := x509.ParseCertificate(block.Bytes) 702 | if err != nil { 703 | return time.Time{}, 0, fmt.Errorf("failed to parse certificate: " + err.Error()) 704 | } 705 | expiresIn := int(c.NotAfter.Sub(timeNow).Hours() / 24) 706 | 707 | return c.NotAfter, expiresIn, err 708 | } 709 | -------------------------------------------------------------------------------- /cmd/types.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "time" 4 | 5 | const ( 6 | // only in flag 7 | configFlag = "config" 8 | certDirFlag = "cert-dir" 9 | 10 | // flag and options 11 | kubeConfigFlag = "kubeconfig" 12 | rotateCertFlag = "rotate-certificates" 13 | rotateServerCertFlag = "rotate-server-certificates" 14 | tlsCertFlag = "tls-cert-file" 15 | tlsKeyFlag = "tls-key-file" 16 | featureGatesFlag = "feature-gates" 17 | 18 | kubeConfigOption = "kubeconfig" 19 | rotateCertOption = "rotateCertificates" 20 | rotateServerCertOption = "serverTLSBootstrap" 21 | tlsCertOption = "tlsCertFile" 22 | tlsKeyOption = "tlsKeyFile" 23 | featureGatesOption = "featureGates" 24 | 25 | rotateKubeletServerCertFeature = "RotateKubeletServerCertificate" 26 | 27 | defaultKubeletServerCertPath = "/var/lib/kubelet/pki/" 28 | 29 | entryType = "kubelet" 30 | ) 31 | 32 | // Output is for json stdout 33 | type Output struct { 34 | Entries []Entry `json:"entry"` 35 | } 36 | 37 | // Entry is for cert entries 38 | type Entry struct { 39 | Type string `json:"type"` 40 | Node string `json:"node"` 41 | Name string `json:"name"` 42 | Days int `json:"days"` 43 | Due time.Time `json:"due"` 44 | Path string `json:"path"` 45 | } 46 | 47 | // NewEntry make entry 48 | func NewEntry(node string, name string, days int, due time.Time, path string) *Entry { 49 | return &Entry{ 50 | Type: entryType, 51 | Node: node, 52 | Name: name, 53 | Days: days, 54 | Due: due, 55 | Path: path, 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /fixture/krawler.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: DaemonSet 3 | metadata: 4 | labels: 5 | app: krawler 6 | name: krawler 7 | namespace: default 8 | spec: 9 | updateStrategy: 10 | type: RollingUpdate 11 | rollingUpdate: 12 | maxUnavailable: 100% 13 | selector: 14 | matchLabels: 15 | app: krawler 16 | template: 17 | metadata: 18 | labels: 19 | app: krawler 20 | spec: 21 | containers: 22 | - image: leoh0/krawler 23 | imagePullPolicy: Always 24 | name: krawler 25 | volumeMounts: 26 | - mountPath: /etc/kubernetes/ 27 | name: host-etc 28 | readOnly: True 29 | - mountPath: /var/lib/kubelet/ 30 | name: host-lib 31 | readOnly: True 32 | - mountPath: /tmp/proc 33 | name: tmp-proc 34 | readOnly: True 35 | env: 36 | - name: NODENAME 37 | valueFrom: 38 | fieldRef: 39 | fieldPath: spec.nodeName 40 | restartPolicy: Always 41 | tolerations: 42 | - operator: "Exists" 43 | volumes: 44 | - hostPath: 45 | path: /etc/kubernetes/ 46 | name: host-etc 47 | - hostPath: 48 | path: /var/lib/kubelet/ 49 | name: host-lib 50 | - hostPath: 51 | path: /proc 52 | name: tmp-proc 53 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/leoh0/kubectl-check-cert 2 | 3 | require ( 4 | github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect 5 | github.com/elazarl/goproxy v0.0.0-20181111060418-2ce16c963a8a // indirect 6 | github.com/evanphx/json-patch v4.1.0+incompatible // indirect 7 | github.com/fatih/color v1.7.0 // indirect 8 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect 9 | github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect 10 | github.com/googleapis/gnostic v0.2.0 // indirect 11 | github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f // indirect 12 | github.com/imdario/mergo v0.3.6 // indirect 13 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 14 | github.com/json-iterator/go v1.1.5 // indirect 15 | github.com/mattn/go-colorable v0.0.9 // indirect 16 | github.com/mattn/go-isatty v0.0.4 // indirect 17 | github.com/mattn/go-runewidth v0.0.4 // indirect 18 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 19 | github.com/modern-go/reflect2 v1.0.1 // indirect 20 | github.com/olekukonko/tablewriter v0.0.1 21 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 22 | github.com/prometheus/common v0.0.0-20190107103113-2998b132700a 23 | github.com/spf13/cast v1.3.0 24 | github.com/spf13/cobra v0.0.3 25 | github.com/spf13/pflag v1.0.3 26 | github.com/stretchr/testify v1.3.0 27 | golang.org/x/oauth2 v0.0.0-20190115181402-5dab4167f31c // indirect 28 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect 29 | gopkg.in/cheggaaa/pb.v1 v1.0.27 30 | gopkg.in/inf.v0 v0.9.1 // indirect 31 | gopkg.in/yaml.v2 v2.2.2 32 | k8s.io/api v0.0.0-20181221193117-173ce66c1e39 33 | k8s.io/apimachinery v0.0.0-20190119020841-d41becfba9ee 34 | k8s.io/cli-runtime v0.0.0-20190119125336-e15d12b9962e 35 | k8s.io/client-go v10.0.0+incompatible 36 | k8s.io/klog v0.1.0 // indirect 37 | sigs.k8s.io/yaml v1.1.0 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= 4 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 5 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= 6 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 7 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c h1:ZfSZ3P3BedhKGUhzj7BQlPSU4OvT6tfOKe3DVHzOA7s= 12 | github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= 13 | github.com/elazarl/goproxy v0.0.0-20181111060418-2ce16c963a8a h1:A4wNiqeKqU56ZhtnzJCTyPZ1+cyu8jKtIchQ3TtxHgw= 14 | github.com/elazarl/goproxy v0.0.0-20181111060418-2ce16c963a8a/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= 15 | github.com/evanphx/json-patch v4.1.0+incompatible h1:K1MDoo4AZ4wU0GIU/fPmtZg7VpzLjCxu+UwBD1FvwOc= 16 | github.com/evanphx/json-patch v4.1.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 17 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 18 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 19 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 20 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 21 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 22 | github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= 23 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 24 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 25 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 26 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw= 27 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 28 | github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck= 29 | github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= 30 | github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g= 31 | github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= 32 | github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f h1:ShTPMJQes6tubcjzGMODIVG5hlrCeImaBnZzKF2N8SM= 33 | github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 34 | github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= 35 | github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 36 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 37 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 38 | github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE= 39 | github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 40 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 41 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 42 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 43 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 44 | github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= 45 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 46 | github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= 47 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 48 | github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= 49 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 50 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 51 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 52 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 53 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 54 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 55 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 56 | github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88= 57 | github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= 58 | github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= 59 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 60 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 61 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 62 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 63 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 64 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 65 | github.com/prometheus/common v0.0.0-20190107103113-2998b132700a h1:bLKgQQEViHvsdgCwCGyyga8npETKygQ8b7c/28mJ8tw= 66 | github.com/prometheus/common v0.0.0-20190107103113-2998b132700a/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 67 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 68 | github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= 69 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 70 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 71 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 72 | github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= 73 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 74 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 75 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 76 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 77 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 78 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 79 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 80 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 81 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I= 82 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 83 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 84 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a h1:gOpx8G595UYyvj8UK4+OFyY4rx037g3fmfhe5SasG3U= 85 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 86 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= 87 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 88 | golang.org/x/oauth2 v0.0.0-20190115181402-5dab4167f31c h1:pcBdqVcrlT+A3i+tWsOROFONQyey9tisIQHI4xqVGLg= 89 | golang.org/x/oauth2 v0.0.0-20190115181402-5dab4167f31c/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 90 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ= 91 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 92 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= 93 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 94 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 95 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5 h1:mzjBh+S5frKOsOBobWIMAbXavqjmgO17k/2puhcFR94= 96 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 97 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 98 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 99 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg= 100 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 101 | google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= 102 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 103 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= 104 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 105 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 106 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 107 | gopkg.in/cheggaaa/pb.v1 v1.0.27 h1:kJdccidYzt3CaHD1crCFTS1hxyhSi059NhOFUf03YFo= 108 | gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= 109 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 110 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 111 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 112 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 113 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 114 | k8s.io/api v0.0.0-20181221193117-173ce66c1e39 h1:iGq7zEPXFb0IeXAQK5RiYT1SVKX/af9F9Wv0M+yudPY= 115 | k8s.io/api v0.0.0-20181221193117-173ce66c1e39/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA= 116 | k8s.io/apimachinery v0.0.0-20190119020841-d41becfba9ee h1:3MH/wGFP+9PjyLIMnPN2GYatdJosd+5TnSO2BzQqqo4= 117 | k8s.io/apimachinery v0.0.0-20190119020841-d41becfba9ee/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0= 118 | k8s.io/cli-runtime v0.0.0-20190119125336-e15d12b9962e h1:/EkI93kLZQ6cpIrxt9r/H4i2tD4gFED5x7QIsNVeKTM= 119 | k8s.io/cli-runtime v0.0.0-20190119125336-e15d12b9962e/go.mod h1:qWnH3/b8sp/l7EvlDh7ulDU3UWA4P4N1NFbEEP791tM= 120 | k8s.io/client-go v10.0.0+incompatible h1:F1IqCqw7oMBzDkqlcBymRq1450wD0eNqLE9jzUrIi34= 121 | k8s.io/client-go v10.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= 122 | k8s.io/klog v0.1.0 h1:I5HMfc/DtuVaGR1KPwUrTc476K8NCqNBldC7H4dYEzk= 123 | k8s.io/klog v0.1.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 124 | sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= 125 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 126 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/pflag" 7 | 8 | "github.com/leoh0/kubectl-check-cert/cmd" 9 | "k8s.io/cli-runtime/pkg/genericclioptions" 10 | ) 11 | 12 | func main() { 13 | flags := pflag.NewFlagSet("kubectl-check-cert", pflag.ExitOnError) 14 | pflag.CommandLine = flags 15 | 16 | root := cmd.NewCmdExpiration( 17 | genericclioptions.IOStreams{ 18 | In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}) 19 | if err := root.Execute(); err != nil { 20 | os.Exit(1) 21 | } 22 | } 23 | --------------------------------------------------------------------------------