Loading...
Loading...
Use when the user asks to "write a reconciler", "implement a reconciler", "add business logic", "handle resource changes", "process resource events", "implement the reconcile loop", "add async processing", "write a controller", "handle create/update/delete events", "use TypedReconciler", "use a Watcher", or asks how to respond to resource state changes in a grafana-app-sdk app. Provides guidance on implementing reconciler and watcher business logic for grafana-app-sdk apps.
npx skill4agent add grafana/skills reconciler-logicgrafana-app-sdk project component add operatoroperator.TypedReconcilerReconcileFunctype 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))
// Handle deletion
if req.Action == operator.ReconcileActionDeleted {
return operator.ReconcileResult{}, nil
}
// ... business logic ...
// Atomic status update with conflict resolution
_, 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
}operator.ReconcileActionReconcileActionCreatedReconcileActionUpdatedReconcileActionDeletedReconcileActionResyncedRequeueAfterreturn operator.ReconcileResult{RequeueAfter: 10 * time.Second}, nilresource.UpdateObjectresource.UpdateObject409 Conflict_, 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"},
)client.UpdateLastObservedGenerationif obj.GetGeneration() == obj.Status.LastObservedGeneration {
return operator.ReconcileResult{}, nil
}BasicReconcileOptionsAppManagedKind{
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)
// true = use reconciler directly, no finalizer management
},
},UsePlain: falseOpinionatedReconcilerWatcherAddUpdateDeletetype MyKindWatcher struct {
client resource.Client
}
func (w *MyKindWatcher) Add(ctx context.Context, obj resource.Object) error {
typed := obj.(*v1alpha1.MyKind)
// handle create
return nil
}
func (w *MyKindWatcher) Update(ctx context.Context, obj, old resource.Object) error {
typed := obj.(*v1alpha1.MyKind)
// handle update
return nil
}
func (w *MyKindWatcher) Delete(ctx context.Context, obj resource.Object) error {
// handle delete
return nil
}
func (w *MyKindWatcher) Sync(ctx context.Context, obj resource.Object) error {
// called on resync; handle like Add if needed
return nil
}WatcherReconcilerAppManagedKindUnmanagedKindsAppConfigUnmanagedKinds: []simple.AppUnmanagedKind{
{
Kind: corev1.ConfigMapKind(),
Reconciler: &ConfigMapReconciler{},
ReconcileOptions: simple.UnmanagedKindReconcileOptions{
Namespace: "my-namespace",
LabelFilters: []string{"app=my-app"},
UseOpinionated: false, // don't add finalizers to unmanaged resources
},
},
},func New(cfg app.Config) (app.App, error) {
cfg.KubeConfig.APIPath = "/apis"
client, err := k8s.NewClientRegistry(cfg.KubeConfig, k8s.DefaultClientConfig()).
ClientFor(mykindv1alpha2.MyKindKind())
if err != nil {
return nil, fmt.Errorf("creating client: %w", err)
}
a, err := simple.NewApp(simple.AppConfig{
Name: "my-app",
KubeConfig: cfg.KubeConfig,
ManagedKinds: []simple.AppManagedKind{
{
Kind: mykindv1alpha1.MyKindKind(),
Validator: NewValidator(),
Mutator: NewMutator(),
},
{
// Attach reconciler to latest version only
Kind: mykindv1alpha2.MyKindKind(),
Reconciler: NewMyKindReconciler(client),
Validator: NewValidator(),
Mutator: NewMutator(),
},
},
})
if err != nil {
return nil, fmt.Errorf("error creating app: %w", err)
}
if err = a.ValidateManifest(cfg.ManifestData); err != nil {
return nil, fmt.Errorf("app manifest validation failed: %w", err)
}
return a, nil
}