diff --git a/.github/workflows/build.yml b/.github/workflows/build.yaml similarity index 66% rename from .github/workflows/build.yml rename to .github/workflows/build.yaml index ce1ab99..400bc82 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yaml @@ -1,15 +1,14 @@ -name: Build Binary +name: Build Binary + Unit Tests on: [push] jobs: build: name: Build runs-on: ubuntu-latest - #strategy: - # matrix: - # build and publish in parallel: linux/386, linux/amd64, windows/386, windows/amd64, darwin/386, darwin/amd64 - # goos: [linux, windows, darwin] - # goarch: ["386", amd64] + strategy: + matrix: + goos: [linux, windows, darwin] + goarch: ["386", amd64] steps: - name: Set up Go 1.x uses: actions/setup-go@v2 @@ -23,14 +22,18 @@ jobs: - name: Get dependencies run: | go get -v -t -d ./... - if [ -f Gopkg.toml ]; then - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh - dep ensure - fi - name: Build run: go build -v . - + env: + GOARCH: ${{ matrix.goarch }} + GOOS: ${{ matrix.goos }} + tests: + name: Unit Tests + runs-on: ubuntu-latest + steps: + - name: Check out code into the Go module directory + uses: actions/checkout@v2 - uses: engineerd/setup-kind@v0.4.0 - name: Testing run: | diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..9736783 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,80 @@ +on: release +name: Build Release +jobs: + release-linux-386: + name: release linux/386 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: compile and release + uses: ngs/go-release.action@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GOARCH: "386" + GOOS: linux + release-linux-amd64: + name: release linux/amd64 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: compile and release + uses: ngs/go-release.action@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GOARCH: amd64 + GOOS: linux + release-linux-arm: + name: release linux/386 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: compile and release + uses: ngs/go-release.action@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GOARCH: "arm" + GOOS: linux + release-linux-arm64: + name: release linux/amd64 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: compile and release + uses: ngs/go-release.action@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GOARCH: arm64 + GOOS: linux + release-darwin-amd64: + name: release darwin/amd64 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: compile and release + uses: ngs/go-release.action@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GOARCH: amd64 + GOOS: darwin + release-windows-386: + name: release windows/386 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: compile and release + uses: ngs/go-release.action@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GOARCH: "386" + GOOS: windows + release-windows-amd64: + name: release windows/amd64 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: compile and release + uses: ngs/go-release.action@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GOARCH: amd64 + GOOS: windows \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3783cf8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:alpine AS build-env +RUN apk --no-cache add build-base git gcc +ADD . /src +RUN cd /src && go build -o kubevol + + +FROM alpine +WORKDIR /app +COPY --from=build-env /src/kubevol /app/ + +EXPOSE 8080 + +RUN mkdir -p /app/mocks + +CMD ["/app/kubevol", "watch"] \ No newline at end of file diff --git a/README.md b/README.md index d3ce655..f82bb60 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,27 @@ # kubevol -This is a simple application that queries all pods for an attached volume or see all the volumes attached to each pod by specific type (eg: ConfigMap, Secret). +Kubevol allows you to audit all your Kubernetes pods for an attached volume or see all the volumes attached to each pod by a specific type (eg: ConfigMap, Secret). Features: - Query for ConfigMaps and Secrets (future support coming for other types of volumes) +- Kubernetes controller to watch and record changes to ConfigMaps and Secrets - Filter by namespace - Filter by a specific object name -- See if attached volume is outdated - - Limited support, can only detect if configmap was deleted after pod was created +- See if attached volume has a stale version attached -## Install +## Installation -Currently you need to build the binary yourself which you can accomplish with the following steps: +You can download the latest release from [Releases](https://github.com/bmaynard/kubevol/releases). -``` -git clone git@github.com:bmaynard/kubevol.git -cd kubevol -go build -./kubevol --help +## Watch And Record Changes + +Since Kubernetes doesn't keep track of when a `Secret` or `Configmap` was updated, `kubevol` has a 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 @@ -34,14 +37,14 @@ 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 +------------------+----------+-----------------------+-----------------------+-------------+ | NAMESPACE | POD NAME | SECRET NAME | VOLUME NAME | OUT OF DATE | +------------------+----------+-----------------------+-----------------------+-------------+ -| kubevol-test-run | redis | redis-secret | redis-secret | Unknown | +| kubevol-test-run | redis | redis-secret | redis-secret | No | | 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/build-publish-docker.sh b/build-publish-docker.sh new file mode 100755 index 0000000..18367c0 --- /dev/null +++ b/build-publish-docker.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +if [ -z "$TAG" ] +then + echo "\$TAG has not been supplied" + exit 1 +fi + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +cd $DIR + +docker build -t bmaynard/kubevol-watch:$TAG . +docker push bmaynard/kubevol-watch:$TAG \ No newline at end of file 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 4bbcc82..867ca48 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -28,16 +28,17 @@ func NewKubevolApp() *cobra.Command { initConfig() factory := core.NewDepsFactory() - coreClient, err := factory.CoreClient(viper.GetString("kubeconfig")) + coreClient, err := factory.CoreClient() if err != nil { - panic(err.Error()) + factory.Logger.Fatal(err) } kubeData := core.NewKubeData(coreClient) - rootCmd.AddCommand(NewConfigMapCommand(*kubeData)) - rootCmd.AddCommand(NewSecretCommand(*kubeData)) + 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 new file mode 100644 index 0000000..04266e6 --- /dev/null +++ b/cmd/watch.go @@ -0,0 +1,97 @@ +package cmd + +import ( + "sync" + + "github.com/bmaynard/kubevol/pkg/core" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/cache" + + w "github.com/bmaynard/kubevol/pkg/watch" + + apiv1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func NewWatchCommand(f *core.Factory) *cobra.Command { + var cmd = &cobra.Command{ + Use: "watch", + Short: "Watch for updates to ConfigMaps and Secrets", + RunE: func(cmd *cobra.Command, args []string) error { + clientset, err := f.CoreClient() + + if err != nil { + f.Logger.Fatal(err) + } + + var wg sync.WaitGroup + wg.Add(2) + + go watchConfigmap(&wg, f, clientset) + go watchSecret(&wg, f, clientset) + wg.Wait() + return nil + }, + } + + return cmd +} + +func watchConfigmap(wg *sync.WaitGroup, f *core.Factory, clientset kubernetes.Interface) { + defer wg.Done() + informer := cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { + return clientset.CoreV1().ConfigMaps("").List(options) + }, + WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { + return clientset.CoreV1().ConfigMaps("").Watch(options) + }, + }, + &apiv1.ConfigMap{}, + 0, //Skip resync + cache.Indexers{}, + ) + + watcher := w.NewWatch(f) + + informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + UpdateFunc: watcher.UpateConfigMapTracker, + DeleteFunc: watcher.DeleteConfigMapTracker, + }) + + stopCh := make(chan struct{}) + defer close(stopCh) + informer.Run(stopCh) +} + +func watchSecret(wg *sync.WaitGroup, f *core.Factory, clientset kubernetes.Interface) { + defer wg.Done() + informer := cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { + return clientset.CoreV1().Secrets("").List(options) + }, + WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { + return clientset.CoreV1().Secrets("").Watch(options) + }, + }, + &apiv1.Secret{}, + 0, //Skip resync + cache.Indexers{}, + ) + + watcher := w.NewWatch(f) + + informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + UpdateFunc: watcher.UpateSecretTracker, + DeleteFunc: watcher.DeleteSecretTracker, + }) + + stopCh := make(chan struct{}) + defer close(stopCh) + informer.Run(stopCh) +} diff --git a/deployment/manifest.yaml b/deployment/manifest.yaml new file mode 100644 index 0000000..6022f2b --- /dev/null +++ b/deployment/manifest.yaml @@ -0,0 +1,78 @@ +apiVersion: v1 +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: + name: kubevol-watch + namespace: "kubevol" + labels: + app: kubevol +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: kubevol + app.kubernetes.io/instance: kubevol-watch + app.kubernetes.io/component: "controller" + template: + metadata: + labels: + app: kubevol + app.kubernetes.io/name: kubevol + 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" + imagePullPolicy: IfNotPresent \ No newline at end of file diff --git a/go.mod b/go.mod index f360da9..1aef4b1 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +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/smartystreets/goconvey v1.6.4 + github.com/sirupsen/logrus v1.6.0 github.com/spf13/cobra v1.0.0 github.com/spf13/viper v1.7.0 k8s.io/api v0.17.0 diff --git a/pkg/core/factory.go b/pkg/core/factory.go index 76628f0..ee0895d 100644 --- a/pkg/core/factory.go +++ b/pkg/core/factory.go @@ -5,18 +5,53 @@ import ( "os" "path/filepath" + "github.com/spf13/viper" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" ) type Factory struct { + Logger *Logger + kubeclient kubernetes.Interface } func NewDepsFactory() *Factory { - return &Factory{} + return &Factory{ + Logger: NewLogger(), + } +} + +func (f *Factory) CoreClient() (kubernetes.Interface, error) { + if f.kubeclient == nil { + clientset, err := f.getKubernetesConfigClient(viper.GetString("kubeconfig")) + + if err != nil { + return nil, err + } + + f.kubeclient = clientset + } + + return f.kubeclient, nil +} + +func (f *Factory) getKubeconfigRESTClient(kubeconfig string) (kubernetes.Interface, error) { + config, err := rest.InClusterConfig() + if err != nil { + return nil, fmt.Errorf("Building Core clientset: %s", err) + + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, fmt.Errorf("Building Core clientset: %s", err) + } + + return clientset, nil } -func (f *Factory) CoreClient(kubeconfig string) (kubernetes.Interface, error) { +func (f *Factory) getKubernetesConfigClient(kubeconfig string) (kubernetes.Interface, error) { if kubeconfig == "" { kubeconfig = filepath.Join(homeDir(), ".kube", "config") @@ -25,13 +60,13 @@ func (f *Factory) CoreClient(kubeconfig string) (kubernetes.Interface, error) { config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) if err != nil { - return nil, fmt.Errorf("Building Core clientset: %s", err) + return f.getKubeconfigRESTClient("") } clientset, err := kubernetes.NewForConfig(config) if err != nil { - return nil, fmt.Errorf("Building Core clientset: %s", err) + return f.getKubeconfigRESTClient("") } return clientset, nil diff --git a/pkg/core/logger.go b/pkg/core/logger.go new file mode 100644 index 0000000..6262c33 --- /dev/null +++ b/pkg/core/logger.go @@ -0,0 +1,72 @@ +package core + +import ( + "os" + + "github.com/sirupsen/logrus" +) + +type Logger struct { + logger *logrus.Logger +} + +func NewLogger() *Logger { + return &Logger{ + logger: &logrus.Logger{ + Out: os.Stdout, + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + DisableLevelTruncation: true, + }, + Level: logrus.DebugLevel, + }, + } +} + +func (l *Logger) Debugf(format string, args ...interface{}) { + l.logger.Debugf(format, args...) +} + +func (l *Logger) Infof(format string, args ...interface{}) { + l.logger.Infof(format, args...) +} + +func (l *Logger) Warnf(format string, args ...interface{}) { + l.logger.Warnf(format, args...) +} + +func (l *Logger) Errorf(format string, args ...interface{}) { + l.logger.Errorf(format, args...) +} + +func (l *Logger) Fatalf(format string, args ...interface{}) { + l.logger.Fatalf(format, args...) +} + +func (l *Logger) Panicf(format string, args ...interface{}) { + l.logger.Fatalf(format, args...) +} + +func (l *Logger) Debug(args ...interface{}) { + l.logger.Debug(args...) +} + +func (l *Logger) Info(args ...interface{}) { + l.logger.Info(args...) +} + +func (l *Logger) Warn(args ...interface{}) { + l.logger.Warn(args...) +} + +func (l *Logger) Error(args ...interface{}) { + l.logger.Error(args...) +} + +func (l *Logger) Fatal(args ...interface{}) { + l.logger.Fatal(args...) +} + +func (l *Logger) Panic(args ...interface{}) { + l.logger.Fatal(args...) +} diff --git a/pkg/watch/configmap.go b/pkg/watch/configmap.go new file mode 100644 index 0000000..dde9cfe --- /dev/null +++ b/pkg/watch/configmap.go @@ -0,0 +1,87 @@ +package watch + +import ( + "fmt" + "time" + + apiv1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (w Watch) UpateConfigMapTracker(old, new interface{}) { + cm := new.(*apiv1.ConfigMap) + + if cm.Name == WatchConfigMapTrackerName || cm.Name == WatchSecretTrackerName { + return + } + + mutex.Lock() + 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()) + + if err != nil { + record := &apiv1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: WatchConfigMapTrackerName, + }, + Data: map[string]string{ + trackerName: currentTime, + }, + } + + _, err := w.clientset.CoreV1().ConfigMaps(WatchNamespace).Create(record) + + if err != nil { + w.f.Logger.Errorf("Error creating tracker configmap: %s", err) + } else { + w.f.Logger.Infof("Created tracker configmap and added configmap: \"%s\"", cm.Name) + } + + } else { + if cmTracker.Data == nil { + cmTracker.Data = make(map[string]string) + } + + cmTracker.Data[trackerName] = currentTime + _, err := w.clientset.CoreV1().ConfigMaps(WatchNamespace).Update(cmTracker) + + if err != nil { + w.f.Logger.Errorf("Unable to update tracker configmap: %v", err) + } else { + w.f.Logger.Infof("Updated tracker for configmap: \"%s\"", cm.Name) + } + } +} + +func (w Watch) DeleteConfigMapTracker(obj interface{}) { + cm := obj.(*apiv1.ConfigMap) + + if cm.Name == WatchConfigMapTrackerName || cm.Name == WatchSecretTrackerName { + return + } + + mutex.Lock() + 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, trackerName) + _, dErr := w.clientset.CoreV1().ConfigMaps(WatchNamespace).Update(cmTracker) + + if dErr != nil { + 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 new file mode 100644 index 0000000..50966ad --- /dev/null +++ b/pkg/watch/secret.go @@ -0,0 +1,88 @@ +package watch + +import ( + "fmt" + "time" + + apiv1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (w Watch) UpateSecretTracker(old, new interface{}) { + cm := new.(*apiv1.Secret) + + if cm.Name == WatchSecretTrackerName { + return + } + + mutex.Lock() + 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()) + + if err != nil { + record := &apiv1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: WatchSecretTrackerName, + }, + Data: map[string]string{ + trackerName: currentTime, + }, + } + + _, err := w.clientset.CoreV1().ConfigMaps(WatchNamespace).Create(record) + + if err != nil { + w.f.Logger.Errorf("Error creating tracker configmap: %s", err) + } else { + w.f.Logger.Infof("Created tracker configmap and added secret: \"%s\"", cm.Name) + } + + } else { + if cmTracker.Data == nil { + cmTracker.Data = make(map[string]string) + } + + cmTracker.Data[trackerName] = currentTime + _, err := w.clientset.CoreV1().ConfigMaps(WatchNamespace).Update(cmTracker) + + if err != nil { + w.f.Logger.Errorf("Unable to update tracker secret: %w", err) + } else { + w.f.Logger.Infof("Updated tracker for secret: \"%s\"", cm.Name) + } + } +} + +func (w Watch) DeleteSecretTracker(obj interface{}) { + cm := obj.(*apiv1.Secret) + + if cm.Name == WatchSecretTrackerName { + return + } + + mutex.Lock() + 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, trackerName) + _, dErr := w.clientset.CoreV1().ConfigMaps(WatchNamespace).Update(cmTracker) + + if dErr != nil { + w.f.Logger.Errorf("Unable to delete secret from tracker; Error: %w", err) + } else { + w.f.Logger.Infof("Deleted secret: \"%s\" from tracker", cm.Name) + } + +} diff --git a/pkg/watch/watch.go b/pkg/watch/watch.go new file mode 100644 index 0000000..c8baf5e --- /dev/null +++ b/pkg/watch/watch.go @@ -0,0 +1,45 @@ +package watch + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "sync" + + "github.com/bmaynard/kubevol/pkg/core" + + "k8s.io/client-go/kubernetes" +) + +var ( + WatchNamespace = "kubevol" + WatchConfigMapTrackerName = "kubevol-configmap-tracker" + WatchSecretTrackerName = "kubevol-secret-tracker" + mutex = &sync.Mutex{} +) + +type Watch struct { + kubeData *core.KubeData + f *core.Factory + clientset kubernetes.Interface +} + +func NewWatch(f *core.Factory) *Watch { + clientset, err := f.CoreClient() + kubeData := core.NewKubeData(clientset) + + if err != nil { + f.Logger.Fatal(err) + } + + return &Watch{ + kubeData: kubeData, + f: f, + clientset: clientset, + } +} + +func GetConfigMapKey(namespace string, name string) string { + hash := md5.Sum([]byte(fmt.Sprintf("%s|%s", namespace, name))) + return hex.EncodeToString(hash[:]) +} diff --git a/tests/yaml/redis-configmap.yml b/tests/yaml/redis-configmap.yml index 2e0117f..9230369 100644 --- a/tests/yaml/redis-configmap.yml +++ b/tests/yaml/redis-configmap.yml @@ -4,5 +4,5 @@ metadata: name: redis-config data: redis-config: | - maxmemory 4mb - maxmemory-policy allkeys-lru \ No newline at end of file + maxmemory 6mb + maxmemory-policy allkeys-lru diff --git a/tests/yaml/service.yml b/tests/yaml/service.yml new file mode 100644 index 0000000..a6b8b76 --- /dev/null +++ b/tests/yaml/service.yml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Service +metadata: + name: redis +spec: + ports: + - port: 6379 + targetPort: 6379 + selector: + app: redis \ No newline at end of file