From a5b9d6cd15e159246eb505803e3fe2d7b339a026 Mon Sep 17 00:00:00 2001 From: Ben Maynard Date: Wed, 8 Jul 2020 16:15:10 -0400 Subject: [PATCH] Lookup status of ConfigMap/Secret in watch tracker --- README.md | 14 ++++++++++-- cmd/configmap.go | 30 ++++++++++++++++++++++--- cmd/root.go | 6 ++--- cmd/secret.go | 28 ++++++++++++++++++++++-- cmd/watch.go | 6 ++--- deployment/manifest.yaml | 47 ++++++++++++++++++++++++++++++++++++++++ go.mod | 5 ----- pkg/watch/configmap.go | 12 +++++----- pkg/watch/secret.go | 8 ++++--- pkg/watch/watch.go | 8 +++++++ 10 files changed, 138 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 27f69a9..9672322 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,16 @@ Features: You can download the latest release from [Releases](https://github.com/bmaynard/kubevol/releases). +## Watch And Record Changes + +Since Kubernetes doesn't keep track of when a `Secret` or `Configmap` was updated, `kubevol` has Kubernetes controller that will watch for all changes and will record the last modified date. This then gives `kubevol` the ability to detect if an attached `Secret` or `Configmap` is outdated. + +To install the watch controller, run: + +```bash +$ kubectl apply -f https://raw.githubusercontent.com/bmaynard/kubevol/master/deployment/manifest.yaml +``` + ### Configuration If your kubeconfig is not in the default location in your home directory, you can specify a custom kubeconfig file by creating the following file: @@ -27,7 +37,8 @@ kubeconfig: /path/to/kube/config ## Sample Output ``` -There are 1 pods in the cluster +$ kubevol secret +There are 12 pods in the cluster Searching for pods that have a Secret attached +------------------+----------+-----------------------+-----------------------+-------------+ @@ -35,6 +46,5 @@ Searching for pods that have a Secret attached +------------------+----------+-----------------------+-----------------------+-------------+ | kubevol-test-run | redis | redis-secret | redis-secret | Unknown | | kubevol-test-run | redis | redis-secret-outdated | redis-secret-outdated | Yes | -| kubevol-test-run | redis | default-token-nd4wr | default-token-nd4wr | Unknown | +------------------+----------+-----------------------+-----------------------+-------------+ ``` diff --git a/cmd/configmap.go b/cmd/configmap.go index 31ae619..7b2fa75 100644 --- a/cmd/configmap.go +++ b/cmd/configmap.go @@ -2,15 +2,18 @@ package cmd import ( "fmt" + "strconv" + "time" "github.com/bmaynard/kubevol/pkg/core" + "github.com/bmaynard/kubevol/pkg/watch" "github.com/fatih/color" "github.com/spf13/cobra" "github.com/jedib0t/go-pretty/v6/table" ) -func NewConfigMapCommand(k core.KubeData) *cobra.Command { +func NewConfigMapCommand(f *core.Factory, k *core.KubeData) *cobra.Command { var cmd = &cobra.Command{ Use: "configmap", Short: "Find all pods that have a specific ConfigMap attached", @@ -25,6 +28,11 @@ func NewConfigMapCommand(k core.KubeData) *cobra.Command { } ui := core.SetupTable(table.Row{"Namespace", "Pod Name", "ConfigMap Name", "Volume Name", "Out of Date"}, cmd.OutOrStdout()) + configmapTracker, err := k.GetConfigMap(watch.WatchConfigMapTrackerName, watch.WatchNamespace) + + if err != nil { + f.Logger.Error(err) + } for _, pod := range pods.Items { podName := pod.ObjectMeta.Name @@ -32,7 +40,7 @@ func NewConfigMapCommand(k core.KubeData) *cobra.Command { _, err := k.GetPod(podName, namespace) if err != nil { - panic(err.Error()) + f.Logger.Error(err) } podCreationTime := pod.ObjectMeta.CreationTimestamp.Time @@ -41,12 +49,28 @@ func NewConfigMapCommand(k core.KubeData) *cobra.Command { if volume.ConfigMap != nil { if objectName == "" || (volume.ConfigMap != nil && volume.ConfigMap.LocalObjectReference.Name == objectName) { configMap, err := k.GetConfigMap(volume.ConfigMap.LocalObjectReference.Name, namespace) - outOfDate := color.YellowString("Unknown") + trackerName := watch.GetConfigMapKey(namespace, volume.ConfigMap.LocalObjectReference.Name) + var outOfDate string + + if configmapTracker.CreationTimestamp.Time.Before(configMap.ObjectMeta.CreationTimestamp.Time) { + outOfDate = color.GreenString("No") + } else { + outOfDate = color.YellowString("Unknown") + } if err != nil || configMap.ObjectMeta.CreationTimestamp.Time.After(podCreationTime) { outOfDate = color.RedString("Yes") } + if updatedTime, ok := configmapTracker.Data[trackerName]; ok { + parsedTime, err := strconv.ParseInt(updatedTime, 10, 64) + if err == nil && configMap.ObjectMeta.CreationTimestamp.Time.Before(time.Unix(parsedTime, 0)) { + outOfDate = color.RedString("Yes") + } else { + outOfDate = color.RedString("No") + } + } + ui.AppendRow([]table.Row{ {color.BlueString(namespace), podName, volume.ConfigMap.LocalObjectReference.Name, volume.Name, outOfDate}, }) diff --git a/cmd/root.go b/cmd/root.go index 6b81781..867ca48 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -36,9 +36,9 @@ func NewKubevolApp() *cobra.Command { kubeData := core.NewKubeData(coreClient) - rootCmd.AddCommand(NewConfigMapCommand(*kubeData)) - rootCmd.AddCommand(NewSecretCommand(*kubeData)) - rootCmd.AddCommand(NewWatchCommand(*factory)) + rootCmd.AddCommand(NewConfigMapCommand(factory, kubeData)) + rootCmd.AddCommand(NewSecretCommand(factory, kubeData)) + rootCmd.AddCommand(NewWatchCommand(factory)) return rootCmd } diff --git a/cmd/secret.go b/cmd/secret.go index 296fc6a..b12f38c 100644 --- a/cmd/secret.go +++ b/cmd/secret.go @@ -2,15 +2,18 @@ package cmd import ( "fmt" + "strconv" + "time" "github.com/bmaynard/kubevol/pkg/core" + "github.com/bmaynard/kubevol/pkg/watch" "github.com/fatih/color" "github.com/spf13/cobra" "github.com/jedib0t/go-pretty/v6/table" ) -func NewSecretCommand(k core.KubeData) *cobra.Command { +func NewSecretCommand(f *core.Factory, k *core.KubeData) *cobra.Command { var cmd = &cobra.Command{ Use: "secret", Short: "Find all pods that have a specific Secret attached", @@ -25,6 +28,11 @@ func NewSecretCommand(k core.KubeData) *cobra.Command { } ui := core.SetupTable(table.Row{"Namespace", "Pod Name", "Secret Name", "Volume Name", "Out of Date"}, cmd.OutOrStdout()) + secretTracker, err := k.GetConfigMap(watch.WatchSecretTrackerName, watch.WatchNamespace) + + if err != nil { + f.Logger.Error(err) + } for _, pod := range pods.Items { podName := pod.ObjectMeta.Name @@ -41,12 +49,28 @@ func NewSecretCommand(k core.KubeData) *cobra.Command { if volume.Secret != nil { if objectName == "" || (volume.Secret != nil && volume.Secret.SecretName == objectName) { secret, err := k.GetSecret(volume.Secret.SecretName, namespace) - outOfDate := color.YellowString("Unknown") + trackerName := watch.GetConfigMapKey(namespace, volume.Secret.SecretName) + var outOfDate string + + if secretTracker.CreationTimestamp.Time.Before(secret.ObjectMeta.CreationTimestamp.Time) { + outOfDate = color.GreenString("No") + } else { + outOfDate = color.YellowString("Unknown") + } if err != nil || secret.ObjectMeta.CreationTimestamp.Time.After(podCreationTime) { outOfDate = color.RedString("Yes") } + if updatedTime, ok := secretTracker.Data[trackerName]; ok { + parsedTime, err := strconv.ParseInt(updatedTime, 10, 64) + if err == nil && secret.ObjectMeta.CreationTimestamp.Time.Before(time.Unix(parsedTime, 0)) { + outOfDate = color.RedString("Yes") + } else { + outOfDate = color.RedString("No") + } + } + ui.AppendRow([]table.Row{ {color.BlueString(namespace), podName, volume.Secret.SecretName, volume.Name, outOfDate}, }) diff --git a/cmd/watch.go b/cmd/watch.go index 62fb9bf..04266e6 100644 --- a/cmd/watch.go +++ b/cmd/watch.go @@ -16,7 +16,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func NewWatchCommand(f core.Factory) *cobra.Command { +func NewWatchCommand(f *core.Factory) *cobra.Command { var cmd = &cobra.Command{ Use: "watch", Short: "Watch for updates to ConfigMaps and Secrets", @@ -30,8 +30,8 @@ func NewWatchCommand(f core.Factory) *cobra.Command { var wg sync.WaitGroup wg.Add(2) - go watchConfigmap(&wg, &f, clientset) - go watchSecret(&wg, &f, clientset) + go watchConfigmap(&wg, f, clientset) + go watchSecret(&wg, f, clientset) wg.Wait() return nil }, diff --git a/deployment/manifest.yaml b/deployment/manifest.yaml index afcc0cb..6022f2b 100644 --- a/deployment/manifest.yaml +++ b/deployment/manifest.yaml @@ -3,6 +3,52 @@ kind: Namespace metadata: name: kubevol --- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: kubevol + namespace: "kubevol" + labels: + app: kubevol + app.kubernetes.io/name: kubevol + app.kubernetes.io/instance: kubevol-watcher + app.kubernetes.io/component: "controller" +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRole +metadata: + name: kubevol + labels: + app: kubevol + app.kubernetes.io/name: kubevol + app.kubernetes.io/instance: kubevol-watcher + app.kubernetes.io/component: "controller" +rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["configmap"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRoleBinding +metadata: + name: kubevol + labels: + app: kubevol + app.kubernetes.io/name: kubevol + app.kubernetes.io/instance: kubevol-watcher + app.kubernetes.io/component: "controller" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kubevol +subjects: + - name: kubevol + namespace: "kubevol" + kind: ServiceAccount +--- apiVersion: apps/v1 kind: Deployment metadata: @@ -25,6 +71,7 @@ spec: app.kubernetes.io/instance: kubevol-watch app.kubernetes.io/component: "controller" spec: + serviceAccountName: kubevol containers: - name: kubevol-watch image: "bmaynard/kubevol-watch:v0.5.0" diff --git a/go.mod b/go.mod index 1fba99a..1aef4b1 100644 --- a/go.mod +++ b/go.mod @@ -4,14 +4,9 @@ go 1.14 require ( github.com/fatih/color v1.9.0 - github.com/go-delve/delve v1.4.1 // indirect - github.com/go-openapi/strfmt v0.19.5 // indirect - github.com/jedib0t/go-pretty v4.3.0+incompatible github.com/jedib0t/go-pretty/v6 v6.0.3 github.com/mitchellh/go-homedir v1.1.0 - github.com/pkg/errors v0.8.1 github.com/sirupsen/logrus v1.6.0 - github.com/smartystreets/goconvey v1.6.4 github.com/spf13/cobra v1.0.0 github.com/spf13/viper v1.7.0 k8s.io/api v0.17.0 diff --git a/pkg/watch/configmap.go b/pkg/watch/configmap.go index 5b29e31..dde9cfe 100644 --- a/pkg/watch/configmap.go +++ b/pkg/watch/configmap.go @@ -19,6 +19,7 @@ func (w Watch) UpateConfigMapTracker(old, new interface{}) { defer mutex.Unlock() cmTracker, err := w.kubeData.GetConfigMap(WatchConfigMapTrackerName, WatchNamespace) + trackerName := GetConfigMapKey(cm.Namespace, cm.Name) now := time.Now() currentTime := fmt.Sprintf("%d", now.Unix()) @@ -29,7 +30,7 @@ func (w Watch) UpateConfigMapTracker(old, new interface{}) { Name: WatchConfigMapTrackerName, }, Data: map[string]string{ - cm.Name: currentTime, + trackerName: currentTime, }, } @@ -46,11 +47,11 @@ func (w Watch) UpateConfigMapTracker(old, new interface{}) { cmTracker.Data = make(map[string]string) } - cmTracker.Data[cm.Name] = currentTime + cmTracker.Data[trackerName] = currentTime _, err := w.clientset.CoreV1().ConfigMaps(WatchNamespace).Update(cmTracker) if err != nil { - w.f.Logger.Errorf("Unable to update tracker configmap: %w", err) + w.f.Logger.Errorf("Unable to update tracker configmap: %v", err) } else { w.f.Logger.Infof("Updated tracker for configmap: \"%s\"", cm.Name) } @@ -68,17 +69,18 @@ func (w Watch) DeleteConfigMapTracker(obj interface{}) { defer mutex.Unlock() cmTracker, err := w.kubeData.GetConfigMap(WatchConfigMapTrackerName, WatchNamespace) + trackerName := GetConfigMapKey(cm.Namespace, cm.Name) if err != nil { w.f.Logger.Info("Unable find tracker configmap") return } - delete(cmTracker.Data, cm.Name) + delete(cmTracker.Data, trackerName) _, dErr := w.clientset.CoreV1().ConfigMaps(WatchNamespace).Update(cmTracker) if dErr != nil { - w.f.Logger.Errorf("Unable to delete configmap from tracker; Error: %w", err) + w.f.Logger.Errorf("Unable to delete configmap from tracker; Error: %v", err) } else { w.f.Logger.Infof("Deleted configmap: \"%s\" from tracker", cm.Name) } diff --git a/pkg/watch/secret.go b/pkg/watch/secret.go index 3f77bca..50966ad 100644 --- a/pkg/watch/secret.go +++ b/pkg/watch/secret.go @@ -19,6 +19,7 @@ func (w Watch) UpateSecretTracker(old, new interface{}) { defer mutex.Unlock() cmTracker, err := w.kubeData.GetConfigMap(WatchSecretTrackerName, WatchNamespace) + trackerName := GetConfigMapKey(cm.Namespace, cm.Name) now := time.Now() currentTime := fmt.Sprintf("%d", now.Unix()) @@ -29,7 +30,7 @@ func (w Watch) UpateSecretTracker(old, new interface{}) { Name: WatchSecretTrackerName, }, Data: map[string]string{ - cm.Name: currentTime, + trackerName: currentTime, }, } @@ -46,7 +47,7 @@ func (w Watch) UpateSecretTracker(old, new interface{}) { cmTracker.Data = make(map[string]string) } - cmTracker.Data[cm.Name] = currentTime + cmTracker.Data[trackerName] = currentTime _, err := w.clientset.CoreV1().ConfigMaps(WatchNamespace).Update(cmTracker) if err != nil { @@ -68,13 +69,14 @@ func (w Watch) DeleteSecretTracker(obj interface{}) { defer mutex.Unlock() cmTracker, err := w.kubeData.GetConfigMap(WatchSecretTrackerName, WatchNamespace) + trackerName := GetConfigMapKey(cm.Namespace, cm.Name) if err != nil { w.f.Logger.Info("Unable find tracker configmap") return } - delete(cmTracker.Data, cm.Name) + delete(cmTracker.Data, trackerName) _, dErr := w.clientset.CoreV1().ConfigMaps(WatchNamespace).Update(cmTracker) if dErr != nil { diff --git a/pkg/watch/watch.go b/pkg/watch/watch.go index 0aa9ea5..c8baf5e 100644 --- a/pkg/watch/watch.go +++ b/pkg/watch/watch.go @@ -1,6 +1,9 @@ package watch import ( + "crypto/md5" + "encoding/hex" + "fmt" "sync" "github.com/bmaynard/kubevol/pkg/core" @@ -35,3 +38,8 @@ func NewWatch(f *core.Factory) *Watch { clientset: clientset, } } + +func GetConfigMapKey(namespace string, name string) string { + hash := md5.Sum([]byte(fmt.Sprintf("%s|%s", namespace, name))) + return hex.EncodeToString(hash[:]) +}