Concepts
This section is aimed at contributors, operators running Palena in production, and anyone debugging a tricky reconciliation. It explains how the operator is built — not just what the CRDs look like.
Four layers
Palena is structured as four cooperating layers:
Each layer has one job:
1. CRD types (api/v1alpha1/)
Go structs with kubebuilder markers. These are the source of truth for the CRD schema — make manifests regenerates YAML into config/crd/bases/ and the OLM bundle. Every field, default, and enum constraint lives here.
Read: CRD design
2. Controllers (internal/controller/)
One *_controller.go per CRD. Each implements the controller-runtime Reconciler interface and owns the reconcile loop for its type: finalizer handling, prerequisite checks, dispatching to the wiring + resource layers, and status updates.
Read: Controllers
3. Wiring builders (internal/wiring/)
Pure functions that translate a Palena CR into an unstructured upstream CR. Used for resources Palena doesn't own the Go types for — CloudNativePG Cluster, LiteLLM LiteLLMInstance / LiteLLMModel, Langfuse LangfuseInstance. No I/O; no state.
Read: Wiring upstream CRs
4. Resource builders (internal/resources/)
Pure functions that build native Kubernetes resources Palena deploys directly — Redis StatefulSet, LibreChat Deployment, MongoDB, MeiliSearch, and the websearch MCP sidecars. Again: no I/O, just builders.
Read: Managed resources
Why wiring and resources are separate
The split is deliberate:
- Wiring targets CRDs owned by other operators. We import them as
unstructured.Unstructuredto avoid pulling in the upstream operator's Go module (which would explode our dependency graph and create version pinning hell). - Resources targets native
k8s.io/apitypes. We import them directly because they're stable.
This also means: if a new upstream operator comes along, the only things that need to change are a few wiring builders + a prerequisite check. Controllers stay identical.
Common patterns
- Every controller uses the same finalizer pattern, the same condition naming scheme, and the same standard labels — see
internal/resources/common.go. - Status updates happen once at the end of every reconciliation, never mid-loop.
- Owner references are set on every created resource so
kubectl deletecascades correctly. - Config hashes are computed over every generated ConfigMap and attached to the consuming Deployment's pod template annotations — changing the CR triggers a rolling restart automatically.
What's next
- CRD design — fields, enums, defaults, reference graph
- Controllers — reconciliation logic per CRD
- Wiring upstream CRs — how unstructured CRs are built
- Managed resources — direct Kubernetes resource generation