Linear and explicit
One sentence per thing the operator does. No Reconcile method to write, no SetupWithManager to wire. What runs, and when, is what you read top to bottom.
A reconcile is a single observable transaction. Every other decision falls out of that one sentence.
Same reconcile, minus the noise that buries the intent.
The interesting logic, buried in plumbing
The intent, on the page
There is no Reconcile method to write and no SetupWithManager to wire up. You describe the reconcile as a linear, observable sequence of named steps, and prose handles the boilerplate around them.
prose.For[*v1alpha1.Foo](mgr).
Owns(&appsv1.Deployment{}).
WithObservability(
prose.Otel(tracer),
prose.WideEvents(logger),
prose.Recorder(mgr.GetEventRecorderFor("foo")),
).
When("paused", isPaused).Skip().
Describe("dependencies", func(g *prose.Group[*v1alpha1.Foo]) {
g.Step("configmap", upsertConfigMap)
g.Step("deployment", upsertDeployment)
}).
Step("status", syncStatus).
Complete()A step holds only business logic. It contributes fields with rctx.Set and returns an outcome; it never logs, traces, or counts.
func upsertDeployment(rctx *prose.Context[*v1alpha1.Foo]) (prose.Outcome, error) {
foo := rctx.Object() // already fetched, typed, no Get, no cast
desired := buildDeployment(foo)
rctx.Set("deployment.image", desired.Spec.Template.Spec.Containers[0].Image)
if err := rctx.Apply(desired); err != nil {
return prose.Requeue, humane.Wrap(err, "apply deployment",
"check that the controller has RBAC to create Deployments in this namespace")
}
return prose.Continue, nil
}When the reconcile returns, the framework emits exactly one structured record describing everything that happened, flattened into dotted keys that mirror your group nesting:
controller=foo namespace=team-a name=widget generation=7 result=requeue requeue_after=30s duration=412ms
dependencies.configmap.duration=8ms dependencies.configmap.outcome=continue
dependencies.deployment.duration=190ms dependencies.deployment.outcome=continue dependencies.deployment.image=ghcr.io/...:v2
status.duration=14ms status.outcome=requeueOne reconcile, one queryable row. The same rctx.Set call also lands on the OpenTelemetry span, so you write the field once and it shows up in both your logs and your traces.
Ready to build one?
The five-minute guide gets you from prose.For to a running reconciler. If you want the why first, start with the mental model.
Early and moving
prose is early and the API surface is still settling. The concepts are stable; signatures may change before a tagged release. Signatures live on pkg.go.dev; these docs are the narrative around them.