Custom Controllers

Custom Controllers and a Preview of the Operator Pattern

Any program that watches the API and reconciles is a controller. This one, in Node.js 24 2,131 with no dependencies, ensures that every namespace labeled booknest=true holds a booknest-settings ConfigMap:

settings-controller.mjs: a minimal controller built on a raw watchJavaScript
// Ensures every namespace labeled booknest=true has a "booknest-settings" ConfigMap.
const API = 'http://127.0.0.1:33801'; // kubectl proxy adds the credentials
async function reconcile(ns) {
  const url = `${API}/api/v1/namespaces/${ns}/configmaps`;
  if ((await fetch(`${url}/booknest-settings`)).status !== 404) return; // already as desired
  const body = { metadata: { name: 'booknest-settings' }, data: { currency: 'USD' } };
  const res = await fetch(url, { method: 'POST', body: JSON.stringify(body),
    headers: { 'content-type': 'application/json' } });
  console.log(`reconcile ${ns}: created booknest-settings (HTTP ${res.status})`);
}
const res = await fetch(`${API}/api/v1/namespaces?watch=1&labelSelector=booknest%3Dtrue`);
let buf = '';
for await (const chunk of res.body) { // one JSON event per line
  buf += Buffer.from(chunk).toString();
  for (let nl; (nl = buf.indexOf('\n')) >= 0; buf = buf.slice(nl + 1)) {
    const { type, object } = JSON.parse(buf.slice(0, nl));
    console.log(`${type} namespace/${object.metadata.name}`);
    if (type === 'DELETED' || object.metadata.deletionTimestamp) continue; // going away
    await reconcile(object.metadata.name);
  }
}
Running the controller against the clusterShell
kubectl proxy --port=33801 >/dev/null & PROXY=$!
sleep 1; node settings-controller.mjs > controller.log & CTRL=$!
sleep 2; kubectl create namespace shop-a
kubectl label namespace shop-a booknest=true
sleep 2; kubectl get configmap booknest-settings -n shop-a -o jsonpath='{.data}'; echo
kubectl delete namespace shop-a
sleep 3; kill $CTRL $PROXY; cat controller.log
Output
namespace/shop-a created
namespace/shop-a labeled
{"currency":"USD"}
namespace "shop-a" deleted
ADDED namespace/shop-a
reconcile shop-a: created booknest-settings (HTTP 201)
MODIFIED namespace/shop-a
MODIFIED namespace/shop-a
DELETED namespace/shop-a

The deletionTimestamp check fixes a real bug in the first version: during namespace deletion it tried to recreate the removed ConfigMap and got HTTP 403. Production controllers add a cache, a retrying queue and leader election, from frameworks such as Kubebuilder (github.com/kubernetes-sigs/kubebuilder (https://github.com/kubernetes-sigs/kubebuilder 9,324 )) or Kopf 2,641 . Swap the label for a custom resource such as BookNestShop and the controller becomes an operator (Operators and CRDs).