The Operator
The lok8s operator runs on a management cluster and watches lok8s custom resources. It uses shell-operator; the hooks are the lo binary itself (lo operator <hook>, package internal/operator) behind two-line shims under operator/hooks/, so the operator shares the exact Go packages the CLI runs (drivers, bootstrap, gitops, deploy).
Alpha
The Lo lifecycle is complete: creation, drift detection, kubeconfig publication, and finalizer-guarded teardown. Capi covers creation and status sync only: deleting a Capi resource does not tear down the cluster (#6). Don't point the Capi path at production credentials yet.
Architecture
CLI mode (lo) Operator mode (shell-operator)
synchronous event-driven
runs from disk watches CRDs
one-shot reconciliation loop
\ /
\ /
the same lo binary
internal/driver/* internal/bootstrap
internal/operator (the hook bodies)The operator container ships the lo binary and the framework tree. shell-operator discovers hooks by path, so operator/hooks/<name>.sh keeps its name and simply runs exec lo operator <name> "$@" — --config and the binding context pass straight through. The original bash hook bodies are frozen at .archive/legacy/operator/hooks/ and are the oracle hack/parity-operator.sh diffs the Go hooks against (binding configuration bytes and the recorded kubectl/clusterctl argv).
Custom Resource Definitions
The operator defines its CRDs in the cluster.lok8s.dev API group:
Lo (cluster.lok8s.dev/v1beta1)
For local/CI clusters:
# Minimal form — the framework derives everything from the domain
# (slot 125) and domain-independent defaults. See the Specs reference
# for the full defaulting table.
apiVersion: cluster.lok8s.dev/v1beta1
kind: Lo
metadata:
name: local
namespace: default
spec:
cluster:
domain: lok8s.dev
bootstrap:
- cilium
- metallb # opt in; default is [cilium] onlykubectl get lo
# NAME PHASE READY DOMAIN AGE
# local Provisioned true lok8s.dev 5mCapi (cluster.lok8s.dev/v1beta1)
For production clusters via Cluster API:
apiVersion: cluster.lok8s.dev/v1beta1
kind: Capi
metadata:
name: prod
spec:
kubernetes:
version: "v1.31.12"
cluster:
domain: prod.example.com
managementCluster:
domain: prod-mgmt.example.com
local: true
provider:
name: hetzner
config:
region: fsn1
sshKeyName: my-key
credentials:
envVars: [HCLOUD_TOKEN]
controlPlane:
replicas: 3
type: cpx22
workers:
general:
replicas: 3
type: cpx22
bootstrap:
- cilium
- ccmkubectl get capi
# NAME PHASE READY DOMAIN PROVIDER AGE
# prod Provisioned true prod.example.com hetzner 10mDeploy (cluster.lok8s.dev/v1beta1)
For deployment domains that target an existing cluster:
apiVersion: cluster.lok8s.dev/v1beta1
kind: Deploy
metadata:
name: api
spec:
clusterRef:
domain: prod.example.comNote: Deploy CRD workload selection is under rework: target selection will land alongside the
services.yamltargets-map redesign. For now, a Deploy spec only carriesclusterRef.
kubectl get deploys.cluster.lok8s.dev
# NAME PHASE CLUSTER AGE
# api Deployed prod.example.com 3mHooks
lo-reconcile.sh
Full lifecycle for Lo resources, with the same driver contract as the CLI:
- Create/Modify: adds a
lok8s.dev/lo-teardownfinalizer, checksdriver::statusfirst (idempotent: aRunningcluster never gets re-provisioned), thendriver::provision+ framework bootstrap (spec.bootstrapaddons), publishes the kubeconfig as Secret<name>-kubeconfig, and setsstatus.kubeconfig.secretRef. - Delete: the finalizer holds the object while
driver::destroytears the cluster down; on success the hook removes the finalizer and deletes the kubeconfig Secret. A failed teardown keeps the finalizer and gets retried. - Drift: a
*/3schedule re-lists everyLoand converges. A cluster deleted out-of-band gets re-provisioned; a finished manual teardown gets detected.
capi-reconcile.sh
Watches Capi resources. On Added/Modified events:
- Updates status to
Provisioning - Detects the CAPI provider from the spec
- Generates CAPI resources from templates
- Applies resources to the cluster
- Status sync is handled by
capi-status-sync.sh
capi-status-sync.sh
Watches cluster.x-k8s.io/v1beta1 Cluster resources with the lok8s.dev/managed: "true" label. When a CAPI Cluster's status changes:
- Maps CAPI phase to lok8s phase
- Updates the corresponding Capi CR status
- On
Provisioned: extracts kubeconfig, bootstraps GitOps or runs direct deploy
Installation
# Apply CRDs
kubectl apply -f operator/crds/
# Deploy the operator (Capi reconciliation only)
kubectl apply -k operator/deploy/
# OR: deploy with the Lo driver enabled (kind clusters)
kubectl apply -k operator/deploy/lo/The operator runs in the lok8s-system namespace with a dedicated service account and RBAC rules.
The lo/ overlay mounts the node's Docker socket, enables host networking (kind kubeconfigs point at host ports), and runs as root: a deliberate trade-off for CI and single-node management hosts. Do not apply it on multi-tenant clusters.
Container Image
The operator image is built from the project root:
docker build -t ghcr.io/kernpilot/lok8s-operator:0.1.0 -f operator/Dockerfile .The Dockerfile:
- Starts from
ghcr.io/flant/shell-operator:v1.14.0 - Installs kubectl, kustomize, yq, jq, docker-cli, kind, clusterctl, flux, git, openssh-client, gettext (for
envsubst), and the khelm kustomize plugin (bootstrap addon charts) - Copies hooks, driver contracts, libraries, bootstrap addons, CAPI templates, and CRDs
- Strips exec bits from the library trees (shell-operator treats every executable under
/hooksas a hook)