Kubernetes operator that continuously detects configuration drift between your desired-state manifests and the live cluster, on a schedule. The in-cluster, GitOps-grade counterpart to the
kube-diffCLI.
Status: early development (v0.5.1 — WIP). The
DriftCheckCRD, the operator, and drift detection forConfigMap,Git,Helm, andKustomizesources are implemented, along with Prometheus metrics and Slack/webhook notifications: the controller loads (and, for Helm/Kustomize, renders) the desired manifests, compares them against the live cluster via thekube-diffengine, and records the result intostatus. Git sources can now clone private repositories viasource.git.auth(Basic/Bearer/SSH), and broader read access for comparing arbitrary kinds is available opt-in via the chart'srbac.viewRole/rbac.extraRulesknobs. See Roadmap before depending on this.
kube-drift runs the same manifest-vs-cluster comparison that the kube-diff CLI performs on demand, but does it continuously and in-cluster as a Kubernetes operator. You declare a DriftCheck resource that points at a set of desired-state manifests (from a ConfigMap or a Git repository) and, on a configurable interval, the controller compares them against the live objects in the cluster and reports what has drifted — resources that changed, that are missing, or that exist but were never declared.
Where kube-diff answers "does the cluster match this directory of YAML right now?" from a laptop or CI job, kube-drift answers "has the cluster drifted from its declared state since I last looked?" and keeps that answer fresh in the resource's status, making it a natural fit for GitOps and audit workflows.
DriftCheckCRD (drift.somaz.io/v1alpha1) — declarative drift checks, one per desired-state source- Pluggable sources — desired manifests come from a
ConfigMap, aGitrepository, or aHelmchart /Kustomizeoverlay rendered in-process (nohelm/kustomizebinary, no shell-out) - Scoped comparison — compare only the desired manifests matching
target.namespacesandtarget.labelSelector - Scheduled re-evaluation — configurable
interval(default5m) for continuous drift detection - Structured status — per-resource drift entries plus a rolled-up summary (changed / new / deleted / unchanged),
lastCheckedAt,observedGeneration, and standard conditions - Webhook notifications — Slack or generic-JSON webhooks fire when the drift state changes (detected or resolved), deduplicated so they don't repeat on every re-check; the URL can come from a
Secret - Opt-in broader read RBAC — the controller's own role reads only
configmapsandsecrets; the Helm chart addsrbac.viewRole.enabled(bind the built-inviewClusterRole) andrbac.extraRules(a custom read-only ClusterRole) knobs for comparing arbitrary kinds — both off by default - Shared engine — reuses the comparison engine extracted from
kube-diff, so CLI and operator produce consistent results
Source-backend maturity:
ConfigMap,Git,Helm, andKustomizesources are implemented.Helm/Kustomizeare rendered in-process from a Git checkout. Git clones anonymously by default, and can authenticate to private repositories viasource.git.auth(Basic / Bearer / SSH) when set. Helm charts are expected to be self-contained (dependencies vendored undercharts/); charts with external dependencies can fetch them at render time withsource.helm.dependencyBuild: true— see the Usage Guide.
Each DriftCheck drives the following loop:
- Load desired state — fetch plain-YAML manifests from the configured
source: ConfigMap key(s), or a Git repository cloned atrefwith manifests read frompath. Only manifests matchingtarget.namespaces/target.labelSelectorare kept. - Read live state — fetch each desired object from the live cluster by group/kind/namespace/name.
- Compare — hand both sides to the
kube-diffengine (engine.Run(...)), producing a[]*diff.Result. - Map to status — classify each result as
changed,new,deleted, orunchanged, write the drifted entries + summary into.status, and stamplastCheckedAt. - Requeue — re-run after
spec.interval.
- Kubernetes v1.25+ (the CRD ships CEL validation rules, which require
x-kubernetes-validations) kubectlconfigured against the target cluster- Helm 3 (for the chart install path)
A consolidated manifest — CRD, RBAC, and controller Deployment — is published at
dist/install.yaml:
kubectl apply -f https://raw.githubusercontent.com/somaz94/kube-drift/main/dist/install.yamlmake install # Install the DriftCheck CRD into the cluster
make deploy # Deploy the controller (config/default overlay)The chart lives at helm/kube-drift/:
helm install kube-drift ./helm/kube-drift \
--namespace kube-drift-system --create-namespaceUse the counterpart of whichever install path you took:
# kubectl
kubectl delete -f https://raw.githubusercontent.com/somaz94/kube-drift/main/dist/install.yaml
# Kustomize
make undeploy
make uninstall
# Helm
helm uninstall kube-drift --namespace kube-drift-systemFull walkthrough — install, every source type, reading results, metrics, RBAC, and troubleshooting — in the Usage Guide.
Create a DriftCheck that compares a set of desired manifests stored in a ConfigMap against the default namespace, re-checking every 5 minutes (config/samples/driftcheck_v1alpha1_sample.yaml):
apiVersion: drift.somaz.io/v1alpha1
kind: DriftCheck
metadata:
name: driftcheck-sample
namespace: default
spec:
# Desired-state manifests to compare against the live cluster.
source:
type: ConfigMap
configMap:
name: desired-manifests
# namespace: default # defaults to the DriftCheck's namespace
# key: manifests.yaml # omit to concatenate every key as a YAML stream
# Optionally compare only the desired manifests in these namespaces.
target:
namespaces:
- default
# How often to re-evaluate drift.
interval: 5mkubectl apply -f config/samples/driftcheck_v1alpha1_sample.yaml
kubectl get driftchecksOr point at a Git repository instead of a ConfigMap — cloned at ref, with plain YAML read from path:
spec:
source:
type: Git
git:
url: https://github.com/somaz94/kube-drift.git # anonymous by default; private repos via auth (see USAGE)
ref: main # branch, tag, or commit; omit for the default branch
path: config/samples # sub-directory holding the manifests; omit for the root
interval: 10m| Field | Type | Description |
|---|---|---|
source.type |
enum | Git, ConfigMap, Helm, or Kustomize — where desired manifests come from |
source.git.url |
string | Clone URL of the repository (required when type: Git) |
source.git.ref |
string | Branch, tag, or commit; defaults to the repo's default branch |
source.git.path |
string | Directory within the repo holding the manifests; defaults to root |
source.git.auth.type |
enum | Basic, Bearer, or SSH — omit for an anonymous clone (see Usage Guide) |
source.git.auth.secretRef.name |
string | Secret (in the DriftCheck's namespace) holding the credentials for auth.type |
source.configMap.name |
string | ConfigMap name (required when type: ConfigMap) |
source.configMap.namespace |
string | ConfigMap namespace; defaults to the DriftCheck's namespace |
source.configMap.key |
string | Single data key; omit to concatenate every key as a YAML stream |
source.helm.git |
object | Chart location (same fields as source.git); path is the chart directory. Required when type: Helm |
source.helm.releaseName |
string | .Release.Name used for rendering; defaults to the DriftCheck's name |
source.helm.namespace |
string | .Release.Namespace used for rendering; defaults to the DriftCheck's namespace |
source.helm.valuesFiles |
list | Values files relative to the chart directory, merged in order |
source.helm.values |
object | Inline values, applied after valuesFiles |
source.helm.dependencyBuild |
bool | Fetch unvendored chart dependencies at render time (default false) |
source.kustomize.git |
object | Overlay location (same fields as source.git); path holds kustomization.yaml. Required when type: Kustomize |
target.namespaces |
list | Compare only desired manifests in these namespaces; manifests without a namespace are skipped |
target.labelSelector |
selector | Compare only desired manifests whose labels match |
interval |
duration | How often to re-evaluate drift (default 5m) |
notify.webhooks[].type |
enum | Slack or Generic (default Generic) |
notify.webhooks[].url |
string | Webhook endpoint |
notify.webhooks[].urlSecretRef |
object | name / key of a Secret entry holding the URL; takes precedence over url |
| Field | Type | Description |
|---|---|---|
lastCheckedAt |
time | When the drift check last completed |
driftedResources[] |
list | Resources whose live state differs from desired (apiVersion, kind, name, namespace, status) |
summary |
object | Tally across all compared resources: changed, new, deleted, unchanged |
observedGeneration |
int | .metadata.generation last reconciled |
lastNotifiedHash |
string | Fingerprint of the drift set last sent to webhooks; suppresses repeat notifications |
conditions |
list | Standard condition array for the latest observations |
Per-resource status values: unchanged, changed, new, deleted.
-
DriftCheckCRD (drift.somaz.io/v1alpha1) — spec, status, printer columns - Operator skeleton — manager, RBAC, Kustomize overlays, Helm chart, CI/CD
- Reconcile loop wired to the
kube-diffengine (engine.Run) — drift recorded intostatus -
ConfigMapsource backend -
Gitsource backend — clone a repo atref, load plain YAML frompath(anonymous clone; go-git, no shell-out) -
Helm/Kustomizesource backends — render a chart / build an overlay from a Git checkout in-process (Helm SDK + Kustomize API, no shell-out) - Webhook notifications — Slack / generic-JSON, deduplicated on drift-state change, URL sourced from a
Secret - End-to-end test suite +
test-e2e.ymlon push/PR viakind-e2e-test-action - Git credential support for private repositories —
source.git.auth(Basic / Bearer / SSH), also onsource.helm.git.authandsource.kustomize.git.auth - Read-RBAC story for comparing arbitrary resource kinds — chart knobs
rbac.viewRole.enabled(bind the built-inviewClusterRole) andrbac.extraRules(custom read-only ClusterRole), off by default - Opt-in Helm chart dependency build —
source.helm.dependencyBuildfetches declared-but-unvendored dependencies at render time (default off; vendoredcharts/remains the reproducible default) - Scoped comparison —
spec.target.namespaces/spec.target.labelSelectorfilter the desired manifests before comparison - Honest fetch errors — missing read RBAC surfaces as a
FetchErrorcondition instead of falsenewdrift, and kinds resolve through API discovery (irregular plurals, CRDs installed after startup)
kube-drift deliberately does not reimplement manifest comparison. The comparison engine was extracted into reusable packages in kube-diff, and this operator consumes them directly:
github.com/somaz94/kube-diff/pkg/engine— the top-levelCompare(...)entry pointgithub.com/somaz94/kube-diff/pkg/diff— theResulttype and diff classificationgithub.com/somaz94/kube-diff/pkg/source— loading desired-state manifestsgithub.com/somaz94/kube-diff/pkg/cluster— reading live cluster objects
This keeps the CLI and the operator behaviorally consistent: the same comparison that kube-diff prints to a terminal is the one kube-drift records into DriftCheck status.
.
├── cmd/
│ └── main.go # Controller-runtime manager entry point
├── api/
│ └── v1alpha1/
│ ├── types.go # DriftCheck spec/status definitions
│ ├── groupversion_info.go # GroupVersion registration
│ └── zz_generated.deepcopy.go # Generated DeepCopy methods
├── internal/
│ ├── controller/
│ │ ├── driftcheck_controller.go # Reconciler — load source, run kube-diff engine, write status
│ │ └── driftcheck_controller_test.go
│ ├── source/ # Git desired-state source (go-git clone → FileSource)
│ ├── notify/ # Slack / generic webhook delivery
│ └── metrics/ # kube_drift_resources drift gauge
├── config/
│ ├── crd/bases/ # Generated CRD YAML
│ ├── default/ # Main Kustomize overlay
│ ├── manager/ # Deployment manifest
│ ├── rbac/ # ClusterRole, bindings, service account
│ └── samples/
│ └── driftcheck_v1alpha1_sample.yaml # Example DriftCheck
├── helm/
│ └── kube-drift/ # Helm chart (CRDs synced from config/crd/bases/)
├── hack/ # boilerplate header, version bump
├── scripts/ # PR auto-generator
├── docs/
│ ├── USAGE.md # Install, source types, results, metrics, RBAC
│ └── DEVELOPMENT.md
├── Dockerfile # Multi-stage (golang → distroless:nonroot)
├── Makefile
├── PROJECT # Kubebuilder v4 project metadata
├── go.mod
└── README.md
Kubebuilder v4 project. Common targets:
make manifests generate # Regenerate CRD YAML, RBAC, and DeepCopy after editing api/v1alpha1/types.go
make build # Build the manager binary → ./bin/manager
make test # Run unit tests with envtest
make lint # Run golangci-lint
make docker-build # Build the container image
make install / uninstall # Install / remove the CRD in the cluster
make deploy / undeploy # Deploy / remove the controller
make help # List all targetsAfter changing
api/v1alpha1/types.go, always runmake manifests generateand keephelm/kube-drift/crds/in sync withconfig/crd/bases/.
The end-to-end workflow (test-e2e.yml) runs the kind-based e2e suite on push / PR / manual dispatch via kind-e2e-test-action: it builds the image, loads it into a kind cluster, deploys the controller, and asserts a DriftCheck detects drift and exposes the kube_drift_resources metric.
- Commits: Conventional Commits (
feat:,fix:,docs:,refactor:,test:,ci:,chore:) - Documentation & code comments: English only
- CRD changes: always run
make manifests generateafter editingapi/v1alpha1/types.go - Branches:
feat/name,fix/name
- kube-diff: Diff local manifests (plain YAML, Helm, Kustomize) against the live cluster.
- kube-diff-action: Run kube-diff in GitHub Actions and post the drift as a pull request comment.
- helios-lb: A bare-metal LoadBalancer for Kubernetes, a lightweight MetalLB alternative.
- network-policy-generator: An operator that learns pod traffic and generates NetworkPolicies from it.
- k8s-namespace-sync: An operator that syncs Secrets and ConfigMaps across namespaces.
See LICENSE — Apache 2.0.
