Self-hosted service
lwolf/kube-cleanup-operator avatar
lwolf/kube-cleanup-operator

kube-cleanup-operator deletes the Jobs your CI left behind

Kubernetes Operator to automatically delete completed Jobs and their Pods

524 stars107 forksGoMIT

At a glance

What is it?
The kube-cleanup-operator is a MIT licensed Go controller that watches Pods and Jobs and deletes the ones you no longer want, on configurable timers for successful, failed, pending, evicted and orphaned resources. It ships as a Helm chart, a Quay image and a static binary, and its newest release is v0.8.4 from May 2024.
Who is it for?
Adopt kube-cleanup-operator if your cluster accumulates finished Jobs and their Pods faster than anything prunes them. Skip it if your platform already runs a TTL controller or a policy engine that covers the same ground, since this would duplicate it.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 113 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the controller watches and what it removes

The project description calls it a Kubernetes Operator to automatically delete completed Jobs and their Pods, and the README describes it more precisely as a Kubernetes Controller that listens for changes in Pods and Jobs and acts accordingly with config arguments. The use cases it lists are deleting Jobs and their pods after completion, deleting Pods stuck in a Pending state, deleting Pods in Evicted state, and deleting orphaned Pods, meaning pods without an owner in a non-running state. The pattern is a reconciler rather than a cron sweep. It watches the API server for events and applies the policy as objects change state, so a Job that succeeds is handled by its own transition rather than by a periodic scan of the whole namespace. The scope is deliberately narrow, which is the right call for a controller with delete permissions on Pods.

The timer flags, and what each one covers

The flag table in the README is the operational contract, and it distinguishes what applies to Pods from what applies to Jobs.

text
  -delete-evicted-pods-after duration
        Delete pods in evicted state (golang duration format, e.g 5m), 0 - never delete (default 15m0s)
  -delete-failed-after duration
        Delete jobs and pods in failed state after X duration (golang duration format, e.g 5m), 0 - never delete
  -delete-orphaned-pods-after duration
        Delete orphaned pods. Pods without an owner in non-running state (golang duration format, e.g 5m), 0 - never delete (default 1h0m0s)
  -delete-pending-pods-after duration
        Delete pods in pending state after X duration (golang duration format, e.g 5m), 0 - never delete
  -delete-successful-after duration
        Delete jobs and pods in successful state after X duration (golang duration format, e.g 5m), 0 - never delete (default 15m0s)

So successful and failed Jobs are both covered with a duration, orphaned and pending Pods have their own timers, and Evicted Pods are removed on discovery rather than after a delay. Every duration uses the Go duration format, and zero means never delete, which is how you disable a category without removing the flag.

Legacy mode, and why the two flag sets coexist

The flag list carries a legacy-mode switch, and it is the single most confusing thing about the interface. The description reads: legacy mode, true to use the old keep-* flags, false to enable the new delete-*-after flags, with the default being true. Before v0.7.0 the flags were keep-successful, keep-failures and keep-pending, expressed as a number of hours, where -1 means forever, 0 means never and a positive number is a count of hours. After v0.7.0 the durations replaced them, and the two families coexist because the project kept the old ones for compatibility. The default is the old behaviour, which means an upgrade does not change what gets deleted until you opt in. There is also a -keep-failures int flag still listed with a default of -1, so the old semantics survive in the new build as well. If you are deploying this fresh, the sensible choice is legacy-mode false with the new duration flags, since a duration of 0 to never delete reads more clearly than an hour count of -1.

Scoping, metrics and the other flags that matter

Three more flags decide how broadly the controller acts. The -namespace flag limits the scope to a single namespace, and the older help text for it says watch only this namespace, omit to operate clusterwide. The -label-selector flag deletes only jobs and pods that meet label selector requirements, which is how you keep a controller from touching workloads it has no business touching. The -run-outside-cluster flag exists for running the binary outside the cluster during development. There is an experimental flag, -ignore-owned-by-cronjobs, that stops the controller cleaning pods and jobs created by cronjobs, and it is marked experimental in the help output. Metrics are exposed on an address set with -listen-addr, defaulting to 0.0.0.0:7000, and the dependency list shows the VictoriaMetrics metrics library, so the controller is built to be scraped. The README also documents a set of optional parameters that come from the underlying logging libraries, with an explicit disclaimer that they are not supported by this project and that any malfunction with them is not covered here.

Installing it, and the RBAC step you must not skip

The usage section gives a four-step sequence, and the first step carries a comment worth reading.

bash
# remember to change namespace in RBAC manifests for monitoring namespaces other than "default"

kubectl create -f https://raw.githubusercontent.com/lwolf/kube-cleanup-operator/master/deploy/deployment/rbac.yaml

# create deployment
kubectl create -f https://raw.githubusercontent.com/lwolf/kube-cleanup-operator/master/deploy/deployment/deployment.yaml


kubectl logs -f $(kubectl get pods --namespace default -l "run=cleanup-operator" -o jsonpath="{.items[0].metadata.name}")

# Use simple job to test it
kubectl create -f https://k8s.io/examples/controllers/job.yaml

The RBAC manifest comes first because the controller needs permission to list and delete before it can do anything. The comment tells you that if you intend to watch namespaces other than default, you need to edit the namespace in the RBAC manifest, which is the point where most first installations go wrong: the operator deploys fine and then silently does nothing because its role is scoped to the wrong namespace. After the deployment you follow the logs.

bash
kubectl logs -f $(kubectl get pods --namespace default -l "run=cleanup-operator" -o jsonpath="{.items[0].metadata.name}")

And you create a simple Job to test the behaviour.

Helm chart, container image and building from source

There are three ways to get it running. The Helm chart is served from charts.lwolf.org, hosted in the lwolf/kube-charts repository, and adding the repo is a single command.

bash
$ helm repo add lwolf-charts http://charts.lwolf.org
"lwolf-charts" has been added to your repositories
$ helm search kube-cleanup

The published container image lives on Quay as quay.io/lwolf/kube-cleanup-operator. Or you build it yourself, which the README offers as a console command.

bash
$ docker build .

For local development the Makefile has the three targets you need.

bash
$ make install_deps
$ make build
$ ./bin/kube-cleanup-operator -run-outside-cluster -dry-run=true

The dry-run flag is the one to remember. It prints what it would do and deletes nothing, which makes it the right way to see the effect of a new configuration on a live cluster. The Dockerfile is a two-stage build on golang:1.21.6-alpine3.19 that runs the same make targets, then copies the static binary into a plain alpine image and runs it as a non-root user named kube-operator.

Where kube-cleanup-operator is the wrong choice

Three limits. The first is that it only handles Jobs and Pods, and only the states listed in the flag table. It will not prune Deployments, StatefulSets, ConfigMaps or Services, and it does not touch ReplicaSets that are scaling normally. If your problem is stale Services or orphaned volumes, a different tool is needed, and the ecosystem has plenty. The second is that the default release cadence is slow. The listed versions are v0.8.4 from 2024-05-29, v0.8.3 from 2024-02-16 and v0.8.2 from 2021-07-09, and the last push to the repository was on 2026-07-02. That gap between the last tag and the last commit means fixes land on master without a release, so if you pin a version you pin code from 2024, and if you track master you are running unreleased code against a Kubernetes API that has moved on since. The go.mod pins client-go at 0.30.1 and requires Go 1.22, so the operator is a few Kubernetes minors behind current. The third is the delete permission itself. This controller has the ability to remove Pods and Jobs across the namespaces its RBAC allows, with the RBAC manifest defaulting to a namespace that the README warns you to change. Run it with -dry-run first, in a namespace you own, and treat the RBAC manifest as the most security-sensitive file in the repository.

Editorial conclusion

Adopt kube-cleanup-operator if your cluster accumulates finished Jobs and their Pods faster than anything prunes them. Skip it if your platform already runs a TTL controller or a policy engine that covers the same ground, since this would duplicate it. Before you deploy, read the RBAC manifest note about namespaces other than default, and set the legacy-mode flag deliberately, because the old keep-* flags and the new delete-*-after flags mean different things.

Frequently asked questions

What does the kube-cleanup-operator delete?

It deletes completed Jobs and their Pods, Pods stuck in Pending, Pods in Evicted state, and orphaned Pods, meaning Pods without an owner in a non-running state. The controller listens for changes in Pods and Jobs and acts on them according to its config arguments.

How do I install the kube-cleanup-operator?

Create the RBAC manifest first with kubectl create -f pointing at deploy/deployment/rbac.yaml, then create the deployment from deploy/deployment/deployment.yaml. Remember to change the namespace in the RBAC manifest if you intend to monitor namespaces other than default. A Helm chart is also available at charts.lwolf.org.

What is legacy mode in the kube-cleanup-operator?

The legacy-mode flag is true to use the old keep-* flags and false to enable the new delete-*-after flags, with the default being true. The old flags express retention as a number of hours where -1 means forever and 0 means never, while the new ones use Go durations where zero means never delete.

Can the kube-cleanup-operator run in dry-run mode?

Yes. The -dry-run flag prints only and does not delete anything, and the README's development instructions run the binary with -run-outside-cluster and -dry-run=true to see what it would do.

Official sources

  1. Issues
  2. License: MIT
  3. lwolf/kube-cleanup-operator on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/lwolf-kube-cleanup-operator.svg)](https://hysenlabs.com/projects/lwolf-kube-cleanup-operator)

Was this useful?