admission-control

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Admission Control

Admission Control

Admission control intercepts resource create/update requests before they are persisted. In grafana-app-sdk there are two types:
  • Validation — accept or reject a request; cannot modify the resource
  • Mutation — modify the resource before it is persisted (e.g. set defaults, normalize fields)
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.
Admission Control会在资源持久化之前拦截资源的创建/更新请求。在grafana-app-sdk中有两种类型:
  • Validation(验证) — 接受或拒绝请求;无法修改资源
  • Mutation(变异) — 在资源持久化之前修改资源(例如设置默认值、标准化字段)
无论应用作为独立operator运行还是在
grafana/apps
内部运行,准入控制的业务逻辑都是相同的。唯一的区别在于运行时:独立应用会启动自己的Webhook服务器;
grafana/apps
中的应用会将准入控制自动注册为Kubernetes插件。

Getting Stubs

获取代码模板

For standalone apps, if
pkg/app/app.go
does not yet exist, a stub App can be generated with:
bash
grafana-app-sdk project component add operator
This creates scaffolded
simple.App
which admission handlers can be added to for each kind in
ManagedKinds
.
对于独立应用,如果
pkg/app/app.go
尚未存在,可以通过以下命令生成一个Stub App:
bash
grafana-app-sdk project component add operator
这会创建一个包含脚手架的
simple.App
,可以为
ManagedKinds
中的每种类型添加准入处理器。

Validator Interface

Validator接口

go
// Implement this interface for each kind you want to validate
type Validator interface {
    Validate(ctx context.Context, request *app.AdmissionRequest) error
}
  • Return
    nil
    to admit the request
  • Return an error to reject it (the error message is returned to the API caller)
  • app.AdmissionRequest
    provides access to the incoming object and operation type
  • You can use
    k8s.NewAdmissionError(err error, statusCode int, reason string)
    (from
    "github.com/grafana/grafana-app-sdk/k8s"
    ) to better control the returned error information
go
// 为每个需要验证的类型实现此接口
type Validator interface {
    Validate(ctx context.Context, request *app.AdmissionRequest) error
}
  • 返回
    nil
    表示允许请求
  • 返回错误表示拒绝请求(错误信息会返回给API调用者)
  • app.AdmissionRequest
    提供对传入对象和操作类型的访问权限
  • 你可以使用
    k8s.NewAdmissionError(err error, statusCode int, reason string)
    (来自
    "github.com/grafana/grafana-app-sdk/k8s"
    )来更好地控制返回的错误信息

Validator Example

Validator示例

go
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
}
go
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)
    }

    // 验证spec字段
    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)
    }

    // 区分创建与更新操作
    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
}

Mutating Admission (Mutator)

变异准入控制(Mutator)

go
// Implement this interface to mutate resources before persistence
type Mutator interface {
    Mutate(ctx context.Context, request *app.AdmissionRequest) (*app.MutatingResponse, error)
}
  • Return a
    MutatingResponse
    containing the (optionally modified) object
  • Return an error to reject the request entirely
  • Best practice is to reject requests from validators, not mutators
go
// 实现此接口以在持久化前修改资源
type Mutator interface {
    Mutate(ctx context.Context, request *app.AdmissionRequest) (*app.MutatingResponse, error)
}
  • 返回包含(可选修改后的)对象的
    MutatingResponse
  • 返回错误表示完全拒绝请求
  • 最佳实践是通过验证器而非变异器拒绝请求

Mutating Handler Example

变异处理器示例

go
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
}
go
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)
    }

    // 在创建时设置默认值
    if req.Action == resource.AdmissionActionCreate {
        if obj.Spec.Description == "" {
            obj.Spec.Description = "No description provided"
        }
    }

    return &app.MutatingResponse{UpdatedObject: obj}, nil
}

Registering Admission Handlers

注册准入处理器

Register validators and mutators when building the app in
pkg/app/app.go
:
go
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
}
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.
pkg/app/app.go
中构建应用时注册验证器和变异器:
go
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
}
注意,还必须在类型的CUE定义中启用变异和验证功能(
mutation.operations
validation.operations
字段)——详情请查看
cue-kind-definition
技能文档。

Admission Request Fields

Admission Request字段

Key fields available on
app.AdmissionRequest
:
FieldTypeDescription
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
app.AdmissionRequest
上可用的关键字段:
字段类型描述
Object
resource.Object
传入的资源(解码后)
OldObject
resource.Object
之前的状态(仅在UPDATE操作中可用)
Action
resource.AdmissionAction
AdmissionActionCreate
AdmissionActionUpdate
AdmissionActionDelete
AdmissionActionConnect
UserInfo
resource.AdmissionUserInfo
发起请求的用户信息
Kind
string
Object
的类型
Group
string
Object
的API组
Version
string
Object
的API版本

Validation Patterns

验证模式

Common patterns to implement:
go
// 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)
}
常见的实现模式:
go
// 不可变性检查
if req.Action == resource.AdmissionActionUpdate && old.Spec.ImmutableField != obj.Spec.ImmutableField {
    return fmt.Errorf("spec.immutableField cannot be changed after creation")
}

// 跨字段验证
if obj.Spec.StartTime.After(obj.Spec.EndTime) {
    return fmt.Errorf("spec.startTime must be before spec.endTime")
}

// 引用验证(例如检查引用的资源是否存在)
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)
}

Deployment Difference

部署差异

ModeAdmission runtime
Standalone operatorApp 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.
模式准入控制运行时
独立operator应用启动Webhook服务器;Kubernetes将准入请求路由到该服务器
grafana/apps
准入处理器自动注册为Kubernetes进程内插件 — 无需单独服务器
两种模式下的处理器代码本身完全相同。

Resources

参考资源