Kubernetes Controllers¶
A Kubernetes controller is an active reconciliation process that continually aligns the actual state of cluster resources with your declared configuration. Starkite implements this pattern through a background reconcile loop registered via the k8s.control() API. The loop runs continuously in the background, watching for API events—such as resource creation, modification, or deletion—and executing Starlark handler functions to correct any detected configuration drift.
How Reconcile Loops Work¶
The k8s.control() function is a blocking call. It parks the Starlark script and runs a watch-based reconcile loop. The Starkite runtime manages the lower-level controller details, including:
* Reestablishing watch connections during network interruptions.
* Enforcing exponential backoff and rate-limiting for retries.
* Managing concurrent worker threads.
* Handling clean shutdowns on SIGTERM or SIGINT.
Example: Pod Event Logger¶
This controller watches a resource and logs lifecycle events. The script registers a reconcile loop that watches all pods in the default namespace and prints their status changes:
# pod-logger-controller.star
# Logs lifecycle events for all pods in the default namespace
def reconcile(event, obj):
# event is a string: ADDED, MODIFIED, or DELETED
# obj is an AttrDict representing the pod resource
print("Event:", event, "| Pod Name:", obj.metadata.name, "| Status:", obj.status.phase)
print("Starting pod logger controller...")
k8s.control(
kind = "pods",
reconcile = reconcile,
namespace = "default",
)
Under the Hood¶
- Registration: Calling
k8s.control(kind="pods", ...)registers thereconcilehandler for the Pod resource. - The Callback: Whenever a pod is created, updated, or deleted, Kubernetes emits an API event. Starkite intercepts this event and invokes the
reconcile(event, obj)function. - The Resource Object: The
objargument is passed as anAttrDict, allowing you to read nested fields using dot notation (e.g.,obj.status.phase).
Advanced Example: Enforcing Maximum Replicas¶
Advanced Example: Enforcing Maximum Replicas¶
Controllers actively correct configuration drift. The following controller watches deployments labeled with enforce-max-replicas=true. If a deployment's replica count exceeds a configured maximum, the controller scales it back down via merge patch and emits a Kubernetes event. Built-in self-echo suppression ensures the controller's patch does not trigger a feedback loop.
# max-replicas-controller.star
# Enforces a maximum replica limit on labeled deployments
max_replicas = var_int("max_replicas", 3)
def reconcile(deploy):
name = deploy.metadata.name
ns = deploy.metadata.namespace
replicas = deploy.spec.get("replicas", 1)
if replicas > max_replicas:
print("[DRIFT DETECTED] %s/%s replicas=%d exceeds max=%d" % (ns, name, replicas, max_replicas))
# Patch replicas to allowed maximum
k8s.patch("deployment", name, {
"spec": {"replicas": max_replicas}
}, namespace=ns)
# Emit an event attached to the deployment
k8s.event(
deploy,
reason = "DriftCorrected",
message = "Scaled down replicas from %d to %d" % (replicas, max_replicas),
type = "Normal",
namespace = ns,
)
else:
print("[IN POLICY] %s/%s replicas=%d within limit (<= %d)" % (ns, name, replicas, max_replicas))
print("Starting max-replicas controller. Max allowed:", max_replicas)
k8s.control(
"deployments",
reconcile = reconcile,
labels = "enforce-max-replicas=true",
poll = "30s",
health_port = 8081,
workers = 2,
)
Running the Controller Locally¶
Start the controller by passing the script to kite run:
# Start with default max_replicas=3
kite run ./max-replicas-controller.star --allow-all
# Override variables at startup
kite run ./max-replicas-controller.star --allow-all --var max_replicas=5
Test drift correction and health probes in another terminal:
# Create a deployment with 10 replicas and label it
kubectl create deployment alice-web --image=nginx:alpine --replicas=10
kubectl label deployment alice-web enforce-max-replicas=true
# Inspect the auto-corrected replica count and emitted event
kubectl get deployment alice-web
kubectl get events --field-selector involvedObject.name=alice-web
# Probe the embedded health server
curl -i http://localhost:8081/healthz
Handler Signatures¶
k8s.control() supports functional child reconciliation, declarative finalization, or specific lifecycle hooks:
| Handler | Signature | Trigger Event / Lifecycle Role |
|---|---|---|
reconcile |
fn(obj) or fn(event, obj) |
Main reconciliation loop. Can return None, requeue duration string (e.g., "10s"), or list[KubeResource] of desired child resources |
finalize |
fn(obj) |
Declarative teardown hook executed when metadata.deletionTimestamp is set before the finalizer is removed |
on_create |
fn(obj) |
Triggered on ADDED events when resources are created |
on_update |
fn(old, new) |
Triggered on MODIFIED events when resources are changed |
on_delete |
fn(obj) |
Triggered on DELETED events when resources are removed |
Configuration Parameters¶
You can customize controller execution using keyword arguments in k8s.control():
labels: Filters watched resources using a standard label selector (e.g.,labels="app=web").field_selector: Filters watched resources using field expressions (e.g.,field_selector="metadata.name=web").poll: A duration string (e.g.,"30s") that periodically requeues all resources for background reconciliation.resync: Informer full cache resync interval (e.g.,"10m").health_port: An integer port number (e.g.,8081) running an embedded HTTP server exposing/healthzand/readyz.finalizer: Custom finalizer name. Automatically added to active resources and removed oncefinalize()returns cleanly.generation_changed: Boolean (defaultTrue). Suppresses reconciliation when updates only modify status or metadata without changing.metadata.generation.watch_owned: A list of child resource kinds (e.g.,["deployments", "services"]) whoseownerReferencespoint to the primary resource.watch_related: A list of secondary resource mappings:[{"kind": "secrets", "map_func": map_fn}]or[("secrets", map_fn)].workers: Integer count of concurrent workqueue workers.leader_election: Boolean enabling distributedcoordination.k8s.io/v1Leaselocking. Standby replicas maintain passive informers;/readyzreturns 200 on the leader and 503 on standbys.identity: Identifier string for the replica (defaults to<hostname>_<pid>).
Operator Pattern: Child Reconciliation & Finalizers¶
When writing Custom Resource Definition (CRD) operators, reconcile(obj) can return a list of desired child resources. The runtime automates child ownership, Server-Side Apply, and orphan pruning:
def reconcile(site):
name = site.metadata.name
ns = site.metadata.namespace
replicas = site.spec.get("replicas", 1)
child_dep = k8s.obj.deployment(
name = name,
namespace = ns,
replicas = replicas,
containers = [k8s.obj.container(name="web", image="nginx:alpine")],
)
child_svc = k8s.obj.service(
name = name,
namespace = ns,
ports = [k8s.obj.service_port(port=80, target_port=80)],
)
# Returning desired child resources triggers:
# 1. Automatic ownerReference injection pointing to site
# 2. Dynamic auto-watching on deployment and service kinds
# 3. Server-Side Apply with fieldManager="starkite"
# 4. Orphan pruning when items are removed from this list
# 5. Inferred Ready condition in status.conditions
return [child_dep, child_svc]
def finalize(site):
# Runs before finalizer is stripped and resource is removed
print("Performing pre-deletion cleanup for", site.metadata.name)
return None
k8s.control(
"staticsites",
reconcile = reconcile,
finalize = finalize,
finalizer = "tutorial.starkite.io/finalizer",
health_port = 8081,
workers = 2,
)
Deploying Controllers to a Cluster¶
Once you have validated your controller script locally, you must deploy it to run continuously inside your Kubernetes cluster. Rather than requiring you to author complex deployment manifests and role-based access control (RBAC) configurations by hand, the Starkite CLI automates this process.
Generating Manifests¶
The kite kube gen-controller-artifacts command inspects your Starlark script and generates a complete, self-contained Kubernetes manifest stream containing:
1. Namespace: A dedicated namespace to isolate your controller.
2. ServiceAccount: The identity your controller runs under.
3. ClusterRole and Binding: The RBAC permissions required by the controller to watch resources and apply modifications.
4. Deployment: A single-replica workload that executes your Starlark script within the cluster.
Run the generator and save the output to a file:
# Generate all deployment and RBAC manifests
kite kube gen-controller-artifacts \
--controller ./max-replicas-controller.star \
--image myregistry/max-replicas-controller:v1 \
--namespace alice-system > deploy.yaml
Applying to the Cluster¶
Deploy the generated manifests directly to your cluster using kubectl:
The controller will start running in the alice-system namespace, automatically mounting the required cluster credentials and executing the Starlark reconcile loop as a background daemon.