npx skills add ...
npx skills add grafana/skills --skill admission-control
Use when the user asks to "write a validator", "add validation", "implement admission control", "write a mutating webhook", "add a mutation handler", "validate incoming resources", "implement admission logic", "add admission webhooks", "write ingress validation", or asks how to validate or mutate resources before they are persisted in a grafana-app-sdk app. Provides guidance on implementing validation and mutation admission handlers for grafana-app-sdk apps.
npx skills add grafana/skills --skill admission-control
Admission control intercepts resource create/update requests before they are persisted. In grafana-app-sdk there are two types:
The app business logic for admission is identical whether the app runs as a standalone operator or inside grafana/apps. The only difference is the runtime: standalone apps stand up their own webhook server; grafana/apps apps have admission auto-registered as a Kubernetes plugin.
For standalone apps, if pkg/app/app.go does not yet exist, a stub App can be generated with:
This creates scaffolded simple.App which admission handlers can be added to for each kind in ManagedKinds.
nil to admit the requestapp.AdmissionRequest provides access to the incoming object and operation typek8s.NewAdmissionError(err error, statusCode int, reason string) (from "github.com/grafana/grafana-app-sdk/k8s") to better control the returned error informationMutatingResponse containing the (optionally modified) objectRegister validators and mutators when building the app in pkg/app/app.go:
Note that mutation and validation must also be enabled in the kind's CUE definition (mutation.operations and validation.operations fields) — see the cue-kind-definition skill for details.
Key fields available on app.AdmissionRequest:
| Field | Type | Description |
|---|---|---|
Object | resource.Object | The incoming resource (after decoding) |
OldObject | resource.Object | Previous state (only on UPDATE operations) |
Action | resource.AdmissionAction | AdmissionActionCreate, AdmissionActionUpdate, AdmissionActionDelete, AdmissionActionConnect |
UserInfo | resource.AdmissionUserInfo | The user making the request |
Kind | string | The Object kind |
Group | string | The Object API Group |
Version | string | The Object API Version |
Common patterns to implement:
| Mode | Admission runtime |
|---|---|
| Standalone operator | App starts a webhook server; Kubernetes routes admission requests to it |
grafana/apps | Admission handlers are auto-registered as a Kubernetes in-process plugin — no separate server required |
The handler code itself is identical in both cases.
type MyKindValidator struct{}
func (v *MyKindValidator) Validate(ctx context.Context, req *app.AdmissionRequest) error {
obj, ok := req.Object.(*v1.MyKind)
if !ok {
return fmt.Errorf("admission request object was of invalid type %T (expected *v1.MyKind)", req.Object)
}
// Validate spec fields
if obj.Spec.Title == "" {
return fmt.Errorf("spec.title is required")
}
if obj.Spec.Count < 0 {
return fmt.Errorf("spec.count must be non-negative, got %d", obj.Spec.Count)
}
// Distinguish create vs update
if req.Action == resource.AdmissionActionUpdate && req.OldObject != nil {
old, ok := req.OldObject.(*v1.MyKind)
if !ok {
return fmt.Errorf("admission request old object was of invalid type %T (expected *v1.MyKind)", req.OldObject)
}
if old.Spec.Title != obj.Spec.Title {
return fmt.Errorf("spec.title is immutable after creation")
}
}
return nil
}// Implement this interface to mutate resources before persistence
type Mutator interface {
Mutate(ctx context.Context, request *app.AdmissionRequest) (*app.MutatingResponse, error)
}type MyKindMutator struct{}
func (m *MyKindMutator) Mutate(
ctx context.Context,
req *app.AdmissionRequest,
) (*app.MutatingResponse, error) {
obj, ok := req.Object.(*v1.MyKind)
if !ok {
return nil, fmt.Errorf("admission request object was of invalid type %T (expected *v1.MyKind)", req.Object)
}
// Set defaults on create
if req.Action == resource.AdmissionActionCreate {
if obj.Spec.Description == "" {
obj.Spec.Description = "No description provided"
}
}
return &app.MutatingResponse{UpdatedObject: obj}, nil
}func New(cfg app.Config) (app.App, error) {
cfg.KubeConfig.APIPath = "/apis"
a, err := simple.NewApp(simple.AppConfig{
ManagedKinds: []simple.AppManagedKind{
{
Kind: v1.MyKindKind(),
Validator: &MyKindValidator{},
Mutator: &MyKindMutator{},
},
},
})
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
}// Immutability check
if req.Action == resource.AdmissionActionUpdate && old.Spec.ImmutableField != obj.Spec.ImmutableField {
return fmt.Errorf("spec.immutableField cannot be changed after creation")
}
// Cross-field validation
if obj.Spec.StartTime.After(obj.Spec.EndTime) {
return fmt.Errorf("spec.startTime must be before spec.endTime")
}
// Referential validation (e.g. check referenced resource exists)
if _, err := v.client.Get(ctx, resource.Identifier{Name: obj.Spec.RefName, Namespace: obj.Namespace}); err != nil {
return fmt.Errorf("referenced resource %q not found", obj.Spec.RefName)
}