From 0927ac87ced34c13cdebc2f93fec5f66c94225c3 Mon Sep 17 00:00:00 2001 From: zoetrope Date: Sun, 12 Feb 2023 20:12:39 +0900 Subject: [PATCH 1/6] README --- .goreleaser.yml | 2 +- README.md | 125 +++++++++++------- charts/kubbernecker/templates/deployment.yaml | 6 +- charts/kubbernecker/values.yaml | 44 +++++- internal/controller/metrics.go | 2 +- 5 files changed, 118 insertions(+), 61 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 166bd44..a7f4432 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -32,7 +32,7 @@ builds: archives: - builds: - kubectl-kubbernecker - name_template: "kubectl-{{ .ProjectName }}_{{ .Tag }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" + name_template: "kubectl-{{ .ProjectName }}_{{ .Os }}-{{ .Arch }}" wrap_in_directory: false format: tar.gz files: diff --git a/README.md b/README.md index 8f501f1..7790109 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,107 @@ -# kubbernecker -// TODO(user): Add simple overview of use/purpose +[![GitHub release](https://img.shields.io/github/release/zoetrope/kubbernecker.svg?maxAge=60)](https://github.com/zoetrope/kubbernecker/releases) +[![CI](https://github.com/zoetrope/kubbernecker/actions/workflows/ci.yaml/badge.svg)](https://github.com/zoetrope/kubbernecker/actions/workflows/ci.yaml) +[![PkgGoDev](https://pkg.go.dev/badge/github.com/zoetrope/kubbernecker?tab=overview)](https://pkg.go.dev/github.com/zoetrope/kubbernecker?tab=overview) -## Description -// TODO(user): An in-depth paragraph about your project and overview of use +# Kubbernecker -## Getting Started -You’ll need a Kubernetes cluster to run against. You can use [KIND](https://sigs.k8s.io/kind) to get a local cluster for testing, or run against a remote cluster. -**Note:** Your controller will automatically use the current context in your kubeconfig file (i.e. whatever cluster `kubectl cluster-info` shows). +**Project Status**: Alpha -### Running on the cluster -1. Install Instances of Custom Resources: +Kubbernecker is tools that helps to check the number of changes made to Kubernetes resources. +It provides two tools: `kubbernecker-metrics` and `kubectl-kubbernecker`. -```sh -kubectl apply -f config/samples/ -``` +`kubbernecker-metrics` is an exporter that exposes the number of changes made to Kubernetes resources as Prometheus +metrics. +It helps to monitor the changes made to resources within a Kubernetes cluster. -2. Build and push your image to the location specified by `IMG`: +`kubectl-kubbernecker` is a kubectl plugin that shows the number of changes made to Kubernetes resources and the manager +who made the changes. +It helps to quickly check the changes made to resources within a Kubernetes cluster. -```sh -make docker-build docker-push IMG=/kubbernecker:tag -``` +## Motivation + +In a Kubernetes cluster, different controllers may continuously edit the same resource, leading to a race condition. +It can cause increased loads on kube-apiserver and performance issues. +Kubbernecker helps to solve these problems by checking the number of changes made to Kubernetes resources. + +## Installation -3. Deploy the controller to the cluster with the image specified by `IMG`: +### kubbernecker-metrics -```sh -make deploy IMG=/kubbernecker:tag +You need to add this repository to your Helm repositories: + +```console +$ helm repo add kubbernecker https://zoetrope.github.io/kubbernecker/ +$ helm repo update ``` -### Uninstall CRDs -To delete the CRDs from the cluster: +To install the chart with the release name `kubbernecker` using a dedicated namespace(recommended): -```sh -make uninstall +``` +$ helm install --create-namespace --namespace kubbernecker kubbernecker kubbernecker/kubbernecker ``` -### Undeploy controller -UnDeploy the controller from the cluster: +Specify parameters using `--set key=value[,key=value]` argument to `helm install`. +Alternatively a YAML file that specifies the values for the parameters can be provided like this: -```sh -make undeploy +```console +$ helm install --create-namespace --namespace kubbernecker kubbernecker -f values.yaml kubbernecker/kubbernecker ``` -## Contributing -// TODO(user): Add detailed information on how you would like others to contribute to this project +Values: -### How it works -This project aims to follow the Kubernetes [Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/). +| Key | Type | Default | Description | +|-------------------------------|--------|-----------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------| +| image.repository | string | `"ghcr.io/zoetrope/kubbernecker"` | Kubbernecker image repository to use. | +| image.tag | string | `{{ .Chart.AppVersion }}` | Kubbernecker image tag to use. | +| image.imagePullPolicy | string | `IfNotPresent` | imagePullPolicy applied to Kubbernecker image. | +| resources | object | `{"requests":{"cpu":"100m","memory":"20Mi"}}` | Specify resources. | +| config.targetResources | list | `[]` (See [values.yaml]) | Target Resources. If this is empty, all resources will be the target. | +| config.namespaceSelector | list | `{}` (See [values.yaml]) | Selector of the namespace to which the target resource belongs. If this is empty, all namespaces will be the target. | +| config.enableClusterResources | bool | `false` | If `targetResources` is empty, whether to include cluster-scope resources in the target. If `targetResources` is not empty, this field will be ignored. | -It uses [Controllers](https://kubernetes.io/docs/concepts/architecture/controller/), -which provide a reconcile function responsible for synchronizing resources until the desired state is reached on the cluster. +### kubectl-kubbernecker -### Test It Out -1. Install the CRDs into the cluster: +Download the binary and put it in a directory of your `PATH`. +The following is an example to install the plugin in `/usr/local/bin`. -```sh -make install +```console +$ OS=$(go env GOOS) +$ ARCH=$(go env GOARCH) +$ curl -L -sS https://github.com/zoetrope/kubbernecker/releases/latest/download/kubectl-kubbernecker_${OS}-${ARCH}.tar.gz \ + | tar xz -C /usr/local/bin kubectl-kubbernecker ``` -2. Run your controller (this will run in the foreground, so switch to a new terminal if you want to leave it running): +NOTE: In the future, this tool will be able to be installed by [krew](https://krew.sigs.k8s.io). -```sh -make run -``` +## Usage -**NOTE:** You can also run this in one step by running: `make install run` +### kubbernecker-metrics -### Modifying the API definitions -If you are editing the API definitions, generate the manifests such as CRs or CRDs using: +| Name | Type | Description | Labels | +|---------------------------------------|---------|-------------|---------------------------------------------------------------------------------------------------------------| +| `kubbernecker_resource_updates_total` | counter | | `group`=group
`version`=version
`kind`=kind
`namespace`=namespace
`resource`=resource | +| `kubbernecker_kind_updates_total` | counter | | `group`=group
`version`=version
`kind`=kind
`namespace`=namespace
`type`=type | -```sh -make manifests +### kubectl-kubbernecker + +- watch sub-command + +```console +$ kubectl kubbernecker watch --all-resources --all-namespaces +``` + + +- blame sub-command + +```console +$ kubectl kubbernecker blame -n default pod nginx ``` -**NOTE:** Run `make --help` for more information on all potential `make` targets +## Development -More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) +```console +$ make start-dev +$ tilt up +``` +[values.yaml]: ./charts/kubbernecker/values.yaml diff --git a/charts/kubbernecker/templates/deployment.yaml b/charts/kubbernecker/templates/deployment.yaml index f814504..6592f0f 100644 --- a/charts/kubbernecker/templates/deployment.yaml +++ b/charts/kubbernecker/templates/deployment.yaml @@ -19,7 +19,7 @@ metadata: control-plane: manager {{- include "kubbernecker.labels" . | nindent 4 }} spec: - replicas: {{ .Values.replicas }} + replicas: 1 selector: matchLabels: control-plane: manager @@ -42,10 +42,8 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace - - name: KUBERNETES_CLUSTER_DOMAIN - value: {{ .Values.kubernetesClusterDomain }} image: {{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }} - imagePullPolicy: IfNotPresent + imagePullPolicy: {{ .Values.image.imagePullPolicy }} livenessProbe: httpGet: path: /healthz diff --git a/charts/kubbernecker/values.yaml b/charts/kubbernecker/values.yaml index fd83878..f78506a 100644 --- a/charts/kubbernecker/values.yaml +++ b/charts/kubbernecker/values.yaml @@ -1,16 +1,46 @@ +# Default image image: + # Kubbernecker image repository to use. repository: ghcr.io/zoetrope/kubbernecker + # Kubbernecker image tag to use. tag: app-version-placeholder + # imagePullPolicy applied to Kubbernecker image. + imagePullPolicy: IfNotPresent +# Resource limits and requests for Kubbernecker resources: - limits: - cpu: 500m - memory: 128Mi requests: - cpu: 10m - memory: 64Mi -replicas: 1 -kubernetesClusterDomain: cluster.local + cpu: 100m + memory: 256Mi +# Kubbernecker configuration config: + # Target Resources. If this is empty, all resources will be the target. + # Specify the resource type to be monitored by `group`, `version` and `kind`. + # `namespaceSelector` can select the namespaces to which the resource belongs. + # `resourceSelector` can select the target resources by its labels. targetResources: [] + # Example: + # - group: "" + # version: "v1" + # kind: "Pod" + # - group: "apps" + # version: "v1" + # kind: "Deployment" + # namespaceSelector: + # matchLabels: + # app: "frontend" + # - group: "storage.k8s.io" + # version: "v1" + # kind: "StorageClass" + # resourceSelector: + # matchLabels: + # team: "myteam" + + # Selector of the namespace to which the target resource belongs. If this is empty, all namespaces will be the target. namespaceSelector: {} + # Example: + # namespaceSelector: + # matchLabels: + # role: admin + + # If `targetResources` is empty, whether to include cluster-scope resources in the target. If `targetResources` is not empty, this field will be ignored. enableClusterResources: false diff --git a/internal/controller/metrics.go b/internal/controller/metrics.go index d11f947..63d3c31 100644 --- a/internal/controller/metrics.go +++ b/internal/controller/metrics.go @@ -9,7 +9,7 @@ var ( resourceUpdateCountDesc = prometheus.NewDesc( "kubbernecker_resource_updates_total", "", - []string{"group", "version", "kind", "namespace", "resource", "type"}, nil) + []string{"group", "version", "kind", "namespace", "resource"}, nil) kindUpdateCountDesc = prometheus.NewDesc( "kubbernecker_kind_updates_total", "", From 348ba0f4239a73c61d808eac533fae7f204721f9 Mon Sep 17 00:00:00 2001 From: zoetrope Date: Wed, 15 Feb 2023 23:21:10 +0900 Subject: [PATCH 2/6] Change metrics name --- README.md | 14 +++++++------ internal/controller/metrics.go | 37 +++++++++++++++++++++------------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 7790109..a595bc1 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,9 @@ It helps to monitor the changes made to resources within a Kubernetes cluster. who made the changes. It helps to quickly check the changes made to resources within a Kubernetes cluster. +The name of Kubbernecker comes from rubbernecker. +It is like staring at a fight between Kubernetes controllers. + ## Motivation In a Kubernetes cluster, different controllers may continuously edit the same resource, leading to a race condition. @@ -77,10 +80,10 @@ NOTE: In the future, this tool will be able to be installed by [krew](https://kr ### kubbernecker-metrics -| Name | Type | Description | Labels | -|---------------------------------------|---------|-------------|---------------------------------------------------------------------------------------------------------------| -| `kubbernecker_resource_updates_total` | counter | | `group`=group
`version`=version
`kind`=kind
`namespace`=namespace
`resource`=resource | -| `kubbernecker_kind_updates_total` | counter | | `group`=group
`version`=version
`kind`=kind
`namespace`=namespace
`type`=type | +| Name | Type | Description | Labels | +|---------------------------------------|---------|-------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------| +| `kubbernecker_resource_events_total` | counter | Total number of Kubernetes events by resource type. | `resource_type`: resource type
`namespace`: namespace
`event_type`: event type ("add", "update" or "delete") | +| `kubbernecker_resource_updates_total` | counter | Total number of updates for a Kubernetes resource instance. | `resource_type`: resource type
`namespace`: namespace
`resource_name`: resource name | ### kubectl-kubbernecker @@ -90,9 +93,8 @@ NOTE: In the future, this tool will be able to be installed by [krew](https://kr $ kubectl kubbernecker watch --all-resources --all-namespaces ``` - - blame sub-command - + ```console $ kubectl kubbernecker blame -n default pod nginx ``` diff --git a/internal/controller/metrics.go b/internal/controller/metrics.go index 63d3c31..76db80e 100644 --- a/internal/controller/metrics.go +++ b/internal/controller/metrics.go @@ -2,23 +2,32 @@ package controller import ( "github.com/prometheus/client_golang/prometheus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/metrics" ) var ( + resourceEventsCountDesc = prometheus.NewDesc( + "kubbernecker_resource_events_total", + "Total number of Kubernetes events by resource type.", + []string{"resource_type", "namespace", "event_type"}, nil) resourceUpdateCountDesc = prometheus.NewDesc( "kubbernecker_resource_updates_total", - "", - []string{"group", "version", "kind", "namespace", "resource"}, nil) - kindUpdateCountDesc = prometheus.NewDesc( - "kubbernecker_kind_updates_total", - "", - []string{"group", "version", "kind", "namespace", "type"}, nil) + "Total number of updates for a Kubernetes resource instance.", + []string{"resource_type", "namespace", "resource_name"}, nil) ) func (m *WatcherManager) Describe(ch chan<- *prometheus.Desc) { + ch <- resourceEventsCountDesc ch <- resourceUpdateCountDesc - ch <- kindUpdateCountDesc +} + +func resourceType(gvk metav1.GroupVersionKind) string { + if gvk.Group == "" { + return "core." + gvk.Version + "." + gvk.Kind + } + + return gvk.Group + "." + gvk.Version + "." + gvk.Kind } func (m *WatcherManager) Collect(ch chan<- prometheus.Metric) { @@ -26,29 +35,29 @@ func (m *WatcherManager) Collect(ch chan<- prometheus.Metric) { statistics := watcher.Statistics() for ns, nsStatistics := range statistics.Namespaces { ch <- prometheus.MustNewConstMetric( - kindUpdateCountDesc, + resourceEventsCountDesc, prometheus.CounterValue, float64(nsStatistics.AddCount), - statistics.GroupVersionKind.Group, statistics.GroupVersionKind.Version, statistics.GroupVersionKind.Kind, ns, "add", + resourceType(statistics.GroupVersionKind), ns, "add", ) ch <- prometheus.MustNewConstMetric( - kindUpdateCountDesc, + resourceEventsCountDesc, prometheus.CounterValue, float64(nsStatistics.UpdateCount), - statistics.GroupVersionKind.Group, statistics.GroupVersionKind.Version, statistics.GroupVersionKind.Kind, ns, "update", + resourceType(statistics.GroupVersionKind), ns, "update", ) ch <- prometheus.MustNewConstMetric( - kindUpdateCountDesc, + resourceEventsCountDesc, prometheus.CounterValue, float64(nsStatistics.DeleteCount), - statistics.GroupVersionKind.Group, statistics.GroupVersionKind.Version, statistics.GroupVersionKind.Kind, ns, "delete", + resourceType(statistics.GroupVersionKind), ns, "delete", ) for res, resStatistics := range nsStatistics.Resources { ch <- prometheus.MustNewConstMetric( resourceUpdateCountDesc, prometheus.CounterValue, float64(resStatistics.UpdateCount), - statistics.GroupVersionKind.Group, statistics.GroupVersionKind.Version, statistics.GroupVersionKind.Kind, ns, res, "update", + resourceType(statistics.GroupVersionKind), ns, res, ) } } From 1b74246978a157f239f827271462169c179cbc30 Mon Sep 17 00:00:00 2001 From: zoetrope Date: Thu, 16 Feb 2023 22:37:29 +0900 Subject: [PATCH 3/6] Revise metrics design --- README.md | 7 ++--- internal/controller/metrics.go | 52 +++++++++++----------------------- pkg/watch/statistics.go | 14 ++++----- pkg/watch/watcher.go | 15 +++++----- 4 files changed, 31 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index a595bc1..c0b26e4 100644 --- a/README.md +++ b/README.md @@ -80,10 +80,9 @@ NOTE: In the future, this tool will be able to be installed by [krew](https://kr ### kubbernecker-metrics -| Name | Type | Description | Labels | -|---------------------------------------|---------|-------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------| -| `kubbernecker_resource_events_total` | counter | Total number of Kubernetes events by resource type. | `resource_type`: resource type
`namespace`: namespace
`event_type`: event type ("add", "update" or "delete") | -| `kubbernecker_resource_updates_total` | counter | Total number of updates for a Kubernetes resource instance. | `resource_type`: resource type
`namespace`: namespace
`resource_name`: resource name | +| Name | Type | Description | Labels | +|--------------------------------------|---------|--------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `kubbernecker_resource_events_total` | counter | Total number of events for Kubernetes resources. | `group`: group
`version`: version
`kind`: kind
`namespace`: namespace
`event_type`: event type ("add", "update" or "delete")
`resource_name`: resource name | ### kubectl-kubbernecker diff --git a/internal/controller/metrics.go b/internal/controller/metrics.go index 76db80e..afd1cd7 100644 --- a/internal/controller/metrics.go +++ b/internal/controller/metrics.go @@ -2,62 +2,42 @@ package controller import ( "github.com/prometheus/client_golang/prometheus" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/metrics" ) var ( resourceEventsCountDesc = prometheus.NewDesc( "kubbernecker_resource_events_total", - "Total number of Kubernetes events by resource type.", - []string{"resource_type", "namespace", "event_type"}, nil) - resourceUpdateCountDesc = prometheus.NewDesc( - "kubbernecker_resource_updates_total", - "Total number of updates for a Kubernetes resource instance.", - []string{"resource_type", "namespace", "resource_name"}, nil) + "Total number of events for Kubernetes resources", + []string{"group", "version", "kind", "namespace", "event_type", "resource_name"}, nil) ) func (m *WatcherManager) Describe(ch chan<- *prometheus.Desc) { ch <- resourceEventsCountDesc - ch <- resourceUpdateCountDesc -} - -func resourceType(gvk metav1.GroupVersionKind) string { - if gvk.Group == "" { - return "core." + gvk.Version + "." + gvk.Kind - } - - return gvk.Group + "." + gvk.Version + "." + gvk.Kind } func (m *WatcherManager) Collect(ch chan<- prometheus.Metric) { for _, watcher := range m.watchers { statistics := watcher.Statistics() for ns, nsStatistics := range statistics.Namespaces { - ch <- prometheus.MustNewConstMetric( - resourceEventsCountDesc, - prometheus.CounterValue, - float64(nsStatistics.AddCount), - resourceType(statistics.GroupVersionKind), ns, "add", - ) - ch <- prometheus.MustNewConstMetric( - resourceEventsCountDesc, - prometheus.CounterValue, - float64(nsStatistics.UpdateCount), - resourceType(statistics.GroupVersionKind), ns, "update", - ) - ch <- prometheus.MustNewConstMetric( - resourceEventsCountDesc, - prometheus.CounterValue, - float64(nsStatistics.DeleteCount), - resourceType(statistics.GroupVersionKind), ns, "delete", - ) for res, resStatistics := range nsStatistics.Resources { ch <- prometheus.MustNewConstMetric( - resourceUpdateCountDesc, + resourceEventsCountDesc, prometheus.CounterValue, float64(resStatistics.UpdateCount), - resourceType(statistics.GroupVersionKind), ns, res, + statistics.GroupVersionKind.Group, statistics.GroupVersionKind.Version, statistics.GroupVersionKind.Kind, ns, "update", res, + ) + ch <- prometheus.MustNewConstMetric( + resourceEventsCountDesc, + prometheus.CounterValue, + float64(resStatistics.AddCount), + statistics.GroupVersionKind.Group, statistics.GroupVersionKind.Version, statistics.GroupVersionKind.Kind, ns, "add", res, + ) + ch <- prometheus.MustNewConstMetric( + resourceEventsCountDesc, + prometheus.CounterValue, + float64(resStatistics.DeleteCount), + statistics.GroupVersionKind.Group, statistics.GroupVersionKind.Version, statistics.GroupVersionKind.Kind, ns, "delete", res, ) } } diff --git a/pkg/watch/statistics.go b/pkg/watch/statistics.go index 7e1f37a..89a5baa 100644 --- a/pkg/watch/statistics.go +++ b/pkg/watch/statistics.go @@ -10,13 +10,12 @@ type Statistics struct { } type NamespaceStatistics struct { - Resources map[string]*ResourceStatistics `json:"resources"` - AddCount int `json:"add"` - DeleteCount int `json:"delete"` - UpdateCount int `json:"update"` + Resources map[string]*ResourceStatistics `json:"resources"` } type ResourceStatistics struct { + AddCount int `json:"add"` + DeleteCount int `json:"delete"` UpdateCount int `json:"update"` } @@ -31,7 +30,7 @@ func (in *Statistics) DeepCopy() *Statistics { func (in *Statistics) DeepCopyInto(out *Statistics) { *out = *in - + out.GroupVersionKind = in.GroupVersionKind if in.Namespaces != nil { in, out := &in.Namespaces, &out.Namespaces *out = make(map[string]*NamespaceStatistics, len(*in)) @@ -47,7 +46,6 @@ func (in *Statistics) DeepCopyInto(out *Statistics) { (*out)[key] = outVal } } - return } func (in *NamespaceStatistics) DeepCopy() *NamespaceStatistics { @@ -61,7 +59,6 @@ func (in *NamespaceStatistics) DeepCopy() *NamespaceStatistics { func (in *NamespaceStatistics) DeepCopyInto(out *NamespaceStatistics) { *out = *in - if in.Resources != nil { in, out := &in.Resources, &out.Resources *out = make(map[string]*ResourceStatistics, len(*in)) @@ -72,10 +69,9 @@ func (in *NamespaceStatistics) DeepCopyInto(out *NamespaceStatistics) { } else { in, out := &val, &outVal *out = new(ResourceStatistics) - *out = *in + **out = **in } (*out)[key] = outVal } } - return } diff --git a/pkg/watch/watcher.go b/pkg/watch/watcher.go index cdaa81e..8f12fc2 100644 --- a/pkg/watch/watcher.go +++ b/pkg/watch/watcher.go @@ -23,9 +23,9 @@ type Watcher struct { nsSelector labels.Selector resSelector labels.Selector - startTime time.Time - informer cache.Informer - regstration toolscache.ResourceEventHandlerRegistration + startTime time.Time + informer cache.Informer + registration toolscache.ResourceEventHandlerRegistration mu sync.RWMutex statistics Statistics @@ -94,12 +94,11 @@ func (w *Watcher) handle(obj interface{}, event string) { switch event { case "add": - info.AddCount += 1 + resInfo.AddCount += 1 case "update": - info.UpdateCount += 1 resInfo.UpdateCount += 1 case "delete": - info.DeleteCount += 1 + resInfo.DeleteCount += 1 } } @@ -133,11 +132,11 @@ func (w *Watcher) Start(ctx context.Context) error { w.handle(obj, "delete") }, }) - w.regstration = reg + w.registration = reg return err } func (w *Watcher) Stop() error { - return w.informer.RemoveEventHandler(w.regstration) + return w.informer.RemoveEventHandler(w.registration) } From e682c86b2cf0a30a528f1ad01025d9029ce8a19b Mon Sep 17 00:00:00 2001 From: zoetrope Date: Fri, 17 Feb 2023 20:06:44 +0900 Subject: [PATCH 4/6] Fix test --- pkg/watch/watcher_test.go | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/pkg/watch/watcher_test.go b/pkg/watch/watcher_test.go index 049a9b4..32ccddf 100644 --- a/pkg/watch/watcher_test.go +++ b/pkg/watch/watcher_test.go @@ -41,6 +41,15 @@ var _ = Describe("Test Watcher", func() { err = cli.DeleteAllOf(ctx, &corev1.ConfigMap{}, ctrlclient.InNamespace("user-ns")) Expect(err).ShouldNot(HaveOccurred()) + Eventually(func(g Gomega) { + for _, ns := range []string{"default", "admin-ns", "user-ns"} { + cms := &corev1.ConfigMapList{} + err = cli.List(ctx, cms, ctrlclient.InNamespace(ns)) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(cms.Items).Should(HaveLen(0)) + } + }).Should(Succeed()) + err = watcher.Stop() Expect(err).ShouldNot(HaveOccurred()) }) @@ -69,12 +78,11 @@ var _ = Describe("Test Watcher", func() { "default": PointTo(MatchAllFields(Fields{ "Resources": MatchAllKeys(Keys{ "test": PointTo(MatchAllFields(Fields{ + "AddCount": Equal(1), "UpdateCount": Equal(0), + "DeleteCount": Equal(0), })), }), - "AddCount": Equal(1), - "UpdateCount": Equal(0), - "DeleteCount": Equal(0), })), })) }).Should(Succeed()) @@ -117,12 +125,11 @@ var _ = Describe("Test Watcher", func() { "admin-ns": PointTo(MatchAllFields(Fields{ "Resources": MatchAllKeys(Keys{ "test1": PointTo(MatchAllFields(Fields{ + "AddCount": Equal(1), "UpdateCount": Equal(0), + "DeleteCount": Equal(0), })), }), - "AddCount": Equal(1), - "UpdateCount": Equal(0), - "DeleteCount": Equal(0), })), // user-ns should not appear })) @@ -171,12 +178,11 @@ var _ = Describe("Test Watcher", func() { "user-ns": PointTo(MatchAllFields(Fields{ "Resources": MatchAllKeys(Keys{ "test2": PointTo(MatchAllFields(Fields{ + "AddCount": Equal(1), "UpdateCount": Equal(0), + "DeleteCount": Equal(0), })), }), - "AddCount": Equal(1), - "UpdateCount": Equal(0), - "DeleteCount": Equal(0), })), // admin-ns should not appear })) From 7d3291b7cc46a1232b2229b23b087db9e5a009bc Mon Sep 17 00:00:00 2001 From: zoetrope Date: Fri, 17 Feb 2023 22:31:37 +0900 Subject: [PATCH 5/6] Update README --- README.md | 55 +++++++++++++++++++++++++-- cmd/kubectl-kubbernecker/sub/blame.go | 11 ++++-- cmd/kubectl-kubbernecker/sub/watch.go | 21 +++++++--- pkg/watch/blame_watcher.go | 38 ------------------ pkg/watch/statistics.go | 41 ++++++++++++++++++++ scripts/update-annotate.sh | 11 ++++++ 6 files changed, 127 insertions(+), 50 deletions(-) create mode 100755 scripts/update-annotate.sh diff --git a/README.md b/README.md index c0b26e4..cbfe3e5 100644 --- a/README.md +++ b/README.md @@ -80,26 +80,73 @@ NOTE: In the future, this tool will be able to be installed by [krew](https://kr ### kubbernecker-metrics +`kubbernecker-metrics` exposes the following metrics: + | Name | Type | Description | Labels | |--------------------------------------|---------|--------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `kubbernecker_resource_events_total` | counter | Total number of events for Kubernetes resources. | `group`: group
`version`: version
`kind`: kind
`namespace`: namespace
`event_type`: event type ("add", "update" or "delete")
`resource_name`: resource name | ### kubectl-kubbernecker -- watch sub-command +`kubectl-kubbernecker` has two subcommands: + +`watch` sub-command prints the number of times a resource is updated. ```console -$ kubectl kubbernecker watch --all-resources --all-namespaces +$ kubectl kubbernecker watch -n default configmap +{ + "gvk": { + "group": "", + "version": "v1", + "kind": "ConfigMap" + }, + "namespaces": { + "default": { + "resources": { + "test-cm": { + "add": 0, + "delete": 0, + "update": 9 + } + } + } + } +} ``` -- blame sub-command +`blame` sub-command prints the name of managers that updated the given resource. ```console -$ kubectl kubbernecker blame -n default pod nginx +$ kubectl kubbernecker blame -n default configmap test-cm +{ + "managers": { + "manager1": { + "update": 4 + }, + "manager2": { + "update": 4 + } + }, + "lastUpdate": "2023-02-17T22:25:20+09:00" +} ``` ## Development +Tools for developing kubbernecker are managed by aqua. +Please install aqua as described in the following page: + +https://aquaproj.github.io/docs/reference/install + +Then install the tools. + +```console +$ cd /path/to/kubbernecker +$ aqua i -l +``` + +You can start development with tilt. + ```console $ make start-dev $ tilt up diff --git a/cmd/kubectl-kubbernecker/sub/blame.go b/cmd/kubectl-kubbernecker/sub/blame.go index 1832951..c81f261 100644 --- a/cmd/kubectl-kubbernecker/sub/blame.go +++ b/cmd/kubectl-kubbernecker/sub/blame.go @@ -23,9 +23,14 @@ func newBlameCmd() *cobwrap.Command[*blameOptions] { cmd := &cobwrap.Command[*blameOptions]{ Command: &cobra.Command{ Use: "blame TYPE[.VERSION][.GROUP] NAME", - Short: "", - Long: ``, - Args: cobra.ExactArgs(2), + Short: "Print the name of managers that updated the given resource", + Long: `Print the name of managers that updated the given resource. + +Examples: + # Print managers that updated "test" ConfigMap resource + kubectl kubbernecker blame configmap test +`, + Args: cobra.ExactArgs(2), }, Options: &blameOptions{}, } diff --git a/cmd/kubectl-kubbernecker/sub/watch.go b/cmd/kubectl-kubbernecker/sub/watch.go index 32e1b18..e4c69db 100644 --- a/cmd/kubectl-kubbernecker/sub/watch.go +++ b/cmd/kubectl-kubbernecker/sub/watch.go @@ -30,9 +30,20 @@ func newWatchCmd() *cobwrap.Command[*watchOptions] { cmd := &cobwrap.Command[*watchOptions]{ Command: &cobra.Command{ - Use: "watch", - Short: "", - Long: ``, + Use: "watch (TYPE[.VERSION][.GROUP]...)", + Short: "Print the number of times a resource is updated", + Long: `Print the number of times a resource is updated. + +Examples: + # Watch Pod resources in "default" namespace + kubectl kubbernecker watch pods -n default + + # Watch Pod resources in all namespaces + kubectl kubbernecker watch pods --all-namespaces + + # Watch all resources in all namespaces + kubectl kubbernecker watch --all-resources --all-namespaces +`, }, Options: &watchOptions{}, } @@ -55,10 +66,10 @@ func (o *watchOptions) Fill(cmd *cobra.Command, args []string) error { o.resources = args if len(o.resources) > 0 && o.allResources { - return errors.New("resources and --all-resources cannot be used together") + return errors.New("the type of resource and `--all-namespaces` flag cannot be used together") } if len(o.resources) == 0 && !o.allResources { - return errors.New("resources or --all-resources is required but not provided") + return errors.New("you must specify the type of resource to get or `--all-namespaces` flag") } return nil diff --git a/pkg/watch/blame_watcher.go b/pkg/watch/blame_watcher.go index 6fa390e..c06ce9b 100644 --- a/pkg/watch/blame_watcher.go +++ b/pkg/watch/blame_watcher.go @@ -22,44 +22,6 @@ type BlameWatcher struct { statistics BlameStatistics } -type ManagerStatistics struct { - UpdateCount int -} - -type BlameStatistics struct { - Managers map[string]*ManagerStatistics - LatestUpdate time.Time -} - -func (in *BlameStatistics) DeepCopy() *BlameStatistics { - if in == nil { - return nil - } - out := new(BlameStatistics) - in.DeepCopyInto(out) - return out -} - -func (in *BlameStatistics) DeepCopyInto(out *BlameStatistics) { - *out = *in - - if in.Managers != nil { - in, out := &in.Managers, &out.Managers - *out = make(map[string]*ManagerStatistics, len(*in)) - for key, val := range *in { - var outVal *ManagerStatistics - if val == nil { - (*out)[key] = nil - } else { - in, out := &val, &outVal - *out = new(ManagerStatistics) - *out = *in - } - (*out)[key] = outVal - } - } - return -} func NewBlameWatcher(logger logr.Logger, kube *client.KubeClient, gvk schema.GroupVersionKind, resource string) *BlameWatcher { statistics := BlameStatistics{} statistics.Managers = make(map[string]*ManagerStatistics) diff --git a/pkg/watch/statistics.go b/pkg/watch/statistics.go index 89a5baa..2498143 100644 --- a/pkg/watch/statistics.go +++ b/pkg/watch/statistics.go @@ -1,6 +1,8 @@ package watch import ( + "time" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -75,3 +77,42 @@ func (in *NamespaceStatistics) DeepCopyInto(out *NamespaceStatistics) { } } } + +type ManagerStatistics struct { + UpdateCount int `json:"update"` +} + +type BlameStatistics struct { + Managers map[string]*ManagerStatistics `json:"managers"` + LatestUpdate time.Time `json:"lastUpdate"` +} + +func (in *BlameStatistics) DeepCopy() *BlameStatistics { + if in == nil { + return nil + } + out := new(BlameStatistics) + in.DeepCopyInto(out) + return out +} + +func (in *BlameStatistics) DeepCopyInto(out *BlameStatistics) { + *out = *in + + if in.Managers != nil { + in, out := &in.Managers, &out.Managers + *out = make(map[string]*ManagerStatistics, len(*in)) + for key, val := range *in { + var outVal *ManagerStatistics + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = new(ManagerStatistics) + *out = *in + } + (*out)[key] = outVal + } + } + return +} diff --git a/scripts/update-annotate.sh b/scripts/update-annotate.sh new file mode 100755 index 0000000..476b6c9 --- /dev/null +++ b/scripts/update-annotate.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +kubectl create -n default configmap test-cm --from-literal=sample="test" + +while : +do + kubectl patch --field-manager=manager1 -n default configmap test-cm --type json --patch '[{"op": "add", "path": "/data/sample", "value": "hoge"}]' + sleep 1 + kubectl patch --field-manager=manager2 -n default configmap test-cm --type json --patch '[{"op": "add", "path": "/data/sample", "value": "fuga"}]' + sleep 1 +done From 87056b352079d90b550218418ab6d76094fd02c0 Mon Sep 17 00:00:00 2001 From: zoetrope Date: Fri, 17 Feb 2023 22:50:59 +0900 Subject: [PATCH 6/6] Stabilize test --- pkg/watch/watcher_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/watch/watcher_test.go b/pkg/watch/watcher_test.go index 32ccddf..f8eb00c 100644 --- a/pkg/watch/watcher_test.go +++ b/pkg/watch/watcher_test.go @@ -72,7 +72,7 @@ var _ = Describe("Test Watcher", func() { err := kubeClient.Cluster.GetClient().Create(ctx, cm) Expect(err).NotTo(HaveOccurred()) - Consistently(func(g Gomega) { + Eventually(func(g Gomega) { statistics := watcher.Statistics() g.Expect(statistics.Namespaces).Should(MatchAllKeys(Keys{ "default": PointTo(MatchAllFields(Fields{ @@ -95,10 +95,11 @@ var _ = Describe("Test Watcher", func() { }) It("should only count configmap in admin-ns", func() { + cm1 := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Namespace: "admin-ns", - Name: "test1", + Namespace: "user-ns", + Name: "test2", }, Data: map[string]string{ "sample": "data", @@ -106,11 +107,10 @@ var _ = Describe("Test Watcher", func() { } err := kubeClient.Cluster.GetClient().Create(ctx, cm1) Expect(err).NotTo(HaveOccurred()) - cm2 := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Namespace: "user-ns", - Name: "test2", + Namespace: "admin-ns", + Name: "test1", }, Data: map[string]string{ "sample": "data", @@ -119,7 +119,7 @@ var _ = Describe("Test Watcher", func() { err = kubeClient.Cluster.GetClient().Create(ctx, cm2) Expect(err).NotTo(HaveOccurred()) - Consistently(func(g Gomega) { + Eventually(func(g Gomega) { statistics := watcher.Statistics() g.Expect(statistics.Namespaces).Should(MatchAllKeys(Keys{ "admin-ns": PointTo(MatchAllFields(Fields{ @@ -172,7 +172,7 @@ var _ = Describe("Test Watcher", func() { err = kubeClient.Cluster.GetClient().Create(ctx, cm2) Expect(err).NotTo(HaveOccurred()) - Consistently(func(g Gomega) { + Eventually(func(g Gomega) { statistics := watcher.Statistics() g.Expect(statistics.Namespaces).Should(MatchAllKeys(Keys{ "user-ns": PointTo(MatchAllFields(Fields{