This example installs a real controller into a Kubernetes cluster (a Kind
cluster works out of the box) and reacts to live changes. It watches a
leaderworkerset.x-k8s.io LeaderWorkerSet and, on every change, uses Karta to
inspect and mutate it through a generic reconcile loop, with no CRD-specific
branching.
LeaderWorkerSet is a good showcase because its shape is non-trivial: it has no
single pod template on the root. Pods live in two child components, a leader and
a worker, each with its own template under spec.leaderWorkerTemplate, and the
worker replica count is a computed expression. The reconciler does not know or
care, because Karta resolves where everything lives from the LeaderWorkerSet's
Karta definition.
It complements the quickstart, which runs offline against
embedded YAML. Here the workload structure is not embedded in the binary: it
lives in the cluster as a Karta custom resource, and the controller reads it at
runtime. The controller is not hard-wired to LeaderWorkerSet either: the watched
types come from the --watch-gvk flag, so managing another workload type needs
no code change (see
Manage another workload type).
The reconcile loop in controller.go does the same work for any workload Karta can describe:
- Discover the workload structure from the cluster by GVK: the controller lists Karta objects and selects the one whose root component matches the watched workload's GVK. It never hard-codes a Karta name, so it resolves a definition the same way a real consumer does for an arbitrary workload type.
- Read a unified status (
Initializing,Running,Failed, and so on) without parsing CRD-specific conditions. - Aggregate replica counts and container resource requests across every component (root and children), so the totals are right even though the leader and worker pods live in separate child components.
- Write the results back as
karta/*annotations, and emit aStatusChangedKubernetes Event on each real status transition (a single canonical phase, so no churn through transient or simultaneous states). - Inject a pod-template label into every pod-bearing component (leader and
worker) through Karta
UpdatePodTemplateSpec.
There is no switch on workload kind anywhere in the reconciler.
A LeaderWorkerSet's pod templates are mutable while it runs (an update triggers a normal rolling update), and it has no suspend field. So unlike a Job or JobSet, there is no immutable window to work around: the controller injects the managed-by label directly into the leader and worker templates, and Karta routes each update to the right path in the workload.
The bundled lws Karta definition maps the leader and
worker templates and the computed worker replica count, so the controller reads
and writes them with plain GetPodTemplateSpec / UpdatePodTemplateSpec calls.
- kind,
kubectl,docker - Go (only to build the image)
From the repository root.
Create a cluster:
kind create clusterInstall the LeaderWorkerSet operator (its CRD and controller are not built into Kubernetes):
VERSION=v0.8.0
kubectl apply --server-side -f https://github.com/kubernetes-sigs/lws/releases/download/$VERSION/manifests.yaml
kubectl -n lws-system rollout status deploy/lws-controller-managerInstall the Karta CRD, then apply the LeaderWorkerSet Karta definition. Order matters: the CRD must exist before the object that uses it.
kubectl apply -f charts/karta/crds/run.ai_kartas.yaml
kubectl apply -f docs/catalog/leaderworkerset-x-k8s-io-leaderworkerset-v1.yamlBuild the controller image and side-load it into the cluster:
# Derive the Go version from go.mod so the build image stays aligned with it.
GO_VERSION=$(awk '/^go /{print $2}' docs/examples/controller-runtime/go.mod)
docker build --build-arg GO_VERSION=$GO_VERSION \
-f docs/examples/controller-runtime/Dockerfile -t generic-controller-example:latest .
kind load docker-image generic-controller-example:latestDeploy the controller:
kubectl apply -f docs/examples/controller-runtime/manifests/
kubectl -n karta-system rollout status deploy/generic-controllerApply the LeaderWorkerSet and inspect what the controller wrote:
kubectl apply -f docs/examples/controller-runtime/samples/leaderworkerset.yaml
# Karta-derived annotations on the LeaderWorkerSet metadata
kubectl get leaderworkerset karta-demo-lws -o jsonpath='{.metadata.annotations}' | tr ',' '\n'
# The label is injected into both the leader and worker templates
kubectl get leaderworkerset karta-demo-lws \
-o jsonpath='leader={.spec.leaderWorkerTemplate.leaderTemplate.metadata.labels}{"\n"}worker={.spec.leaderWorkerTemplate.workerTemplate.metadata.labels}{"\n"}'
# The injected label also reaches the running pods
kubectl get pods -l app.kubernetes.io/managed-by=karta
# Events recorded by the controller
kubectl describe leaderworkerset karta-demo-lws | sed -n '/Events:/,$p'Expected annotations once the group is ready (replicas sums the group, leader and worker components; cpu/memory sum the leader and worker requests):
karta/status: Running
karta/replicas: 4
karta/cpu-request: 200m
karta/memory-request: 128Mi
karta/gpu-request: 0
The label injection is recorded as a single event:
Normal PodTemplateLabeled Injected pod-template label app.kubernetes.io/managed-by=karta via Karta
The first observed status is recorded silently, so a brand-new workload does not
emit a StatusChanged event. To see one, drive a transition, for example a
rolling update that the controller observes going back through Initializing and
into Running:
kubectl patch leaderworkerset karta-demo-lws --type=json \
-p '[{"op":"replace","path":"/spec/leaderWorkerTemplate/workerTemplate/spec/containers/0/image","value":"busybox:1.38"}]'
kubectl describe leaderworkerset karta-demo-lws | sed -n '/Events:/,$p'Normal StatusChanged Workload status changed: Running -> Initializing (replicas=4 cpu=200m memory=128Mi gpu=0)
The controller also watches Karta objects. Editing the one whose root component
is leaderworkerset.x-k8s.io/v1 LeaderWorkerSet re-reconciles every governed
LeaderWorkerSet with the new structure, with no redeploy:
kubectl edit karta leaderworkerset-x-k8s-io-leaderworkerset-v1
# change something the controller reports, then saveThis is the deeper layer of the example: the workload structure is cluster data, not code. See docs/catalog/ for ready-made definitions covering PyTorchJob, RayCluster, JobSet, and more.
The watched types are configured at runtime through the --watch-gvk flag, so
adding one never touches the Go code. To also manage, say, JobSet:
- Install the JobSet operator (the controller can only watch types the API
server knows):
VERSION=v0.12.0 kubectl apply --server-side -f https://github.com/kubernetes-sigs/jobset/releases/download/$VERSION/manifests.yaml - Apply its Karta definition:
kubectl apply -f docs/catalog/jobset-x-k8s-io-jobset-v1alpha2.yaml
- Add the GVK to the flag in
manifests/01-deployment.yaml(formatgroup/version/kind, core group empty):args: - --watch-gvk=leaderworkerset.x-k8s.io/v1/LeaderWorkerSet,jobset.x-k8s.io/v1alpha2/JobSet
- Grant RBAC for the new resource in
manifests/00-rbac.yaml:- apiGroups: ["jobset.x-k8s.io"] resources: ["jobsets"] verbs: ["get", "list", "watch", "update", "patch"]
- Re-apply the manifests:
kubectl apply -f docs/examples/controller-runtime/manifests/
- Resume the Job and watch the injected label reach the pods:
kubectl patch jobset karta-demo-jobset-suspended --type=merge -p '{"spec":{"suspend":false}}' kubectl get pods -l app.kubernetes.io/managed-by=karta
The controller creates one watch/reconciler per configured GVK at startup, so a flag change takes effect on the next rollout. The reconcile code is untouched; Karta absorbs the new workload's structure.
| File | Purpose |
|---|---|
main.go |
Manager setup: scheme, one reconciler per watched GVK, start |
controller.go |
WorkloadReconciler inspect + mutate loop (no per-CRD branching) |
Dockerfile |
Builds the controller image from the repository root |
manifests/00-rbac.yaml |
Namespace, ServiceAccount, ClusterRole, ClusterRoleBinding (applied first) |
manifests/01-deployment.yaml |
Controller Deployment in the karta-system namespace |
samples/leaderworkerset.yaml |
LeaderWorkerSet (leader + worker child templates, computed worker count) |
kubectl delete -f docs/examples/controller-runtime/samples/ --ignore-not-found
kubectl delete -f docs/examples/controller-runtime/manifests/ --ignore-not-found
kind delete cluster