Reconciler Logic
Grafana LabsOfficialPublished by Grafana Labs
- Skill
- Tool integration
- Cloud and DevOps
- Apache-2.0
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides the patterns and procedures for implementing the asynchronous business-logic layer in grafana-app-sdk applications. It covers the creation of TypedReconciler functions, managing resource state changes, handling event-driven updates, and configuring informer options. It emphasizes safe status updates using resource.UpdateObject to prevent race conditions and provides guidance on managing finalizers and periodic resyncs to ensure system consistency.
What it does
- Defines TypedReconciler patterns for strongly-typed resource processing.
- Implements conflict-safe status updates using resource.UpdateObject.
- Configures BasicReconcileOptions for namespace, label, and field filtering.
- Manages resource finalizers for clean deletion of external dependencies.
- Schedules periodic resyncs and async polling using RequeueAfter.
- Wires controllers into the application registration process.
When to use it
- When building the core business logic for a custom Kubernetes operator.
- When you need to reconcile resources that your application does not own.
- When handling complex create, update, or delete event cycles.
- When you need to poll external systems asynchronously and update resource status.
- When you need to filter which resources trigger reconciliation events.
Advantages
- Prevents 409 conflict errors during status updates.
- Reduces unnecessary processing with generation-based skip patterns.
- Provides a structured, type-safe approach to operator development.
- Simplifies finalizer management through opinionated defaults.
Usage examples
- “How do I implement a reconciler for my custom resource MyKind?”
- You define a struct embedding operator.TypedReconciler[*v1alpha1.MyKind], wire a ReconcileFunc to handle the logic, and register it in app.go using AppManagedKind.
- “How can I avoid 409 conflicts when updating status?”
- Use the resource.UpdateObject function, which fetches the latest object version before applying your update, ensuring atomic status changes.
- “How do I make my reconciler poll an external system every 30 seconds?”
- Return an operator.ReconcileResult with the RequeueAfter field set to 30 * time.Second within your reconcile function.
- “How do I filter events so my reconciler only processes resources in the prod namespace?”
- Configure the BasicReconcileOptions with the Namespace field set to 'prod' when registering the reconciler in your app configuration.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/grafana/reconciler-logic/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip reconciler-logic.zip -d .agents/skills
Documentation by Grafana Labs
Reconciler Logic
Reconcilers are the async business-logic layer of a grafana-app-sdk app. The SDK enqueues a reconcile event when a resource is created, updated, or deleted; the reconciler observes the current state and drives the system toward the desired state.
Common Workflows
Implementing a new reconciler end-to-end
# 1. Generate operator stubs for a standalone app
grafana-app-sdk project component add operator
# 2. Implement the ReconcileFunc — see § TypedReconciler below for the pattern
# 3. Register the reconciler in app.go (see references/registration.md)
# 4. Generate, build, and verify it runs
grafana-app-sdk generate
go build ./...
go run ./cmd/operator # tail logs — reconcile entries should appear when you kubectl-apply a resourceIf the operator starts but no reconcile events fire when you create a resource:
Check
BasicReconcileOptions.Namespacematches the resource's namespaceCheck
BasicReconcileOptions.LabelFilters/FieldSelectors— most "no events" issues are filter mismatches (kubectl get <resource> -o yamlto see labels)Confirm the reconciler was attached to the right (latest) version of the kind
TypedReconciler — preferred pattern
operator.TypedReconciler handles type assertion and provides a strongly-typed ReconcileFunc:
type MyKindReconciler struct {
operator.TypedReconciler[*v1alpha1.MyKind]
client resource.Client
}
func NewMyKindReconciler(client resource.Client) *MyKindReconciler {
r := &MyKindReconciler{client: client}
r.ReconcileFunc = r.reconcile // wire the typed func
return r
}
func (r *MyKindReconciler) reconcile(
ctx context.Context,
req operator.TypedReconcileRequest[*v1alpha1.MyKind],
) (operator.ReconcileResult, error) {
obj := req.Object
// Skip if already reconciled this generation
if obj.GetGeneration() == obj.Status.LastObservedGeneration &&
req.Action != operator.ReconcileActionDeleted {
return operator.ReconcileResult{}, nil
}
log := logging.FromContext(ctx).With("name", obj.GetName(), "namespace", obj.GetNamespace())
log.Info("reconciling", "action", operator.ResourceActionFromReconcileAction(req.Action))
if req.Action == operator.ReconcileActionDeleted {
return operator.ReconcileResult{}, nil
}
// ... business logic ...
// Atomic status update — see § Status updates below
_, err := resource.UpdateObject(ctx, r.client, obj.GetStaticMetadata().Identifier(),
func(obj *v1alpha1.MyKind, _ bool) (*v1alpha1.MyKind, error) {
obj.Status.LastObservedGeneration = obj.GetGeneration()
obj.Status.State = "Ready"
return obj, nil
},
resource.UpdateOptions{Subresource: "status"},
)
return operator.ReconcileResult{}, err
}ReconcileAction values: ReconcileActionCreated, ReconcileActionUpdated, ReconcileActionDeleted, ReconcileActionResynced.
To requeue after a delay (e.g. polling an external system):
return operator.ReconcileResult{RequeueAfter: 10 * time.Second}, nilStatus updates with resource.UpdateObject
Always use resource.UpdateObject for status writes — it fetches the latest version before applying your update function, avoiding 409 Conflict errors when multiple reconcile events race:
_, err := resource.UpdateObject(ctx, r.client, identifier,
func(obj *v1alpha1.MyKind, exists bool) (*v1alpha1.MyKind, error) {
obj.Status.LastObservedGeneration = obj.GetGeneration()
obj.Status.State = "Ready"
obj.Status.Message = ""
return obj, nil
},
resource.UpdateOptions{Subresource: "status"},
)Do not use client.Update for status — it sends the full object and races with spec changes made by users.
Generation-based skip
Check LastObservedGeneration at the top of the reconcile function to avoid re-processing unchanged resources:
if obj.GetGeneration() == obj.Status.LastObservedGeneration {
return operator.ReconcileResult{}, nil
}ReconcileOptions
Control informer behavior via BasicReconcileOptions on the AppManagedKind entry:
{
Kind: mykindv1alpha1.MyKindKind(),
Reconciler: reconciler,
ReconcileOptions: simple.BasicReconcileOptions{
Namespace: "my-namespace", // watch one namespace; default is all
LabelFilters: []string{"env=prod"}, // only reconcile matching resources
FieldSelectors: []string{"status.phase=Running"},
UsePlain: false, // false = wrap in OpinionatedReconciler (default; manages finalizers)
},
},UsePlain: false (the default) wraps your reconciler in OpinionatedReconciler, which manages finalizers automatically so the SDK can guarantee clean deletion.
References
references/watchers.md—Watcheralternative (event-style Add/Update/Delete callbacks) + decision matrix for watcher vs reconcilerreferences/unmanaged-kinds.md—UnmanagedKindsfor reconciling resources your app doesn't own, withUseOpinionated: falseguidance and common failure modesreferences/registration.md— fullapp.gowiring (client setup, multi-version registration,ValidateManifest) + common failure modes
External resources
Files
- ATTRIBUTION.mdAdded by AutoGineer855 B
- LICENSELicence file added by AutoGineer11.1 KiB
- SKILL.md6.4 KiB
- references/informer-configuration.md12.0 KiB
- references/reconciler-patterns.md9.7 KiB
- references/registration.md2.5 KiB
- references/unmanaged-kinds.md1.7 KiB
- references/watchers.md1.5 KiB
references/registration.md
Registration in app.go
How to wire your reconciler into the app's New function.
func New(cfg app.Config) (app.App, error) {
cfg.KubeConfig.APIPath = "/apis"
// 1. Build a client for the kind your reconciler operates on
client, err := k8s.NewClientRegistry(cfg.KubeConfig, k8s.DefaultClientConfig()).
ClientFor(mykindv1alpha2.MyKindKind())
if err != nil {
return nil, fmt.Errorf("creating client: %w", err)
}
// 2. Register the kind with reconciler attached to the latest version only
a, err := simple.NewApp(simple.AppConfig{
Name: "my-app",
KubeConfig: cfg.KubeConfig,
ManagedKinds: []simple.AppManagedKind{
{
// Older version: validation + mutation only
Kind: mykindv1alpha1.MyKindKind(),
Validator: NewValidator(),
Mutator: NewMutator(),
},
{
// Latest version: validation + mutation + reconciler
Kind: mykindv1alpha2.MyKindKind(),
Reconciler: NewMyKindReconciler(client),
Validator: NewValidator(),
Mutator: NewMutator(),
},
},
})
if err != nil {
return nil, fmt.Errorf("error creating app: %w", err)
}
// 3. Validate the AppManifest matches what's registered (catches missing route handlers etc.)
if err = a.ValidateManifest(cfg.ManifestData); err != nil {
return nil, fmt.Errorf("app manifest validation failed: %w", err)
}
return a, nil
}Why attach the reconciler only to the latest version
With multi-version kinds, the reconciler should only fire on the canonical/latest version. The SDK auto-converts incoming objects via the conversion webhook, so reconciling v1alpha1 and v1alpha2 separately would double-reconcile the same resource.
Common failure modes
| Symptom | Likely cause |
|---|---|
app manifest validation failed: route handler missing | A routes: entry in CUE has no Go handler registered. Add app.RegisterCustomRouteHandler("<routeName>", handler) before ValidateManifest. |
| Reconciler doesn't fire on resource creates | The reconciler is attached to the wrong version, or UsePlain: true was set on BasicReconcileOptions and finalizers aren't being managed. |
error creating client: ... | Wrong API path or kube config — check cfg.KubeConfig.APIPath = "/apis" is set before building the client. |
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- Grafana Labs
- Publisher
- Grafana Labs (Official)
- Source
- grafana/skills · skills/grafana-app-sdk/reconciler-logic · commit 8fb72a5
- Licence
- Apache License 2.0 (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Grafana Labs or Grafana Labs.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download reconciler-logic.zip