
























读完本篇,你应该能回答:
Kubernetes API 访问控制的完整链路是什么?
MutatingWebhook 和 ValidatingWebhook 在请求处理流程中处于什么位置?
AdmissionReview 的请求体和响应体各包含哪些字段?
MutatingWebhook 如何通过 JSON Patch 实现对象修改?
ValidatingWebhook 的并行调用和短路逻辑是什么?
Webhook 如何处理 fail-open 和 fail-closed?
如何在 Kubernetes 中部署和配置一个生产可用的 Admission Webhook?
如何编写单元测试覆盖 Webhook 行为?
Kubernetes Go Operator AdmissionWebhook MutatingWebhook ValidatingWebhook k8s v1.36.1
学习重点提示 — 建议先通读全文,再重点回顾标注内容
重点掌握(必须)
- API Server 请求处理链:Authentication → Authorization → MutatingAdmissionWebhook → ValidatingAdmissionWebhook → Storage
- AdmissionReview 协议:Request/Response 结构、UID 机制、PatchType=JSONPatch
- MutatingWebhook 实现:dispatcher.go 中 callAttrMutatingHook 的完整调用链路和 Patch 应用流程
- ValidatingWebhook 实现:并行调用、FailOpen/FailClosed、短路逻辑
次重点(了解即可)
- ReinvocationPolicy 的二次调用机制
- MatchConditions(CEL 表达式匹配条件)
- AuditAnnotations 和 Warnings 机制
文章目录
思考记忆提示 — 本节是全篇的"入口"——理解 Admission Webhook 解决什么问题,才能理解后面的每一个设计决策
Kubernetes API Server 是整个集群的"中央大脑"——所有操作(创建 Pod、修改 Deployment、删除 Service)都必须经过 API Server。但 API Server 默认只做两件事:验证请求的语法合法性(字段类型对不对)和检查基本约束(字段值是否在范围内)。它不关心业务语义——比如:"这个 Pod 使用的镜像是否在白名单中?""这个 Deployment 的副本数是否超过了资源配额?"
Admission Webhook 就是来解决这个问题的:它允许管理员在不修改 API Server 源码的情况下,注入自定义的准入逻辑。API Server 在处理请求时,会向外部的 Webhook 服务发送 HTTP 请求,Webhook 返回允许或拒绝(或带修改的响应)。
我的理解的意思是说
可以把 Admission Webhook 想象成一个机场安检系统:
Mutating 必须在 Validating 之前执行——因为只有先"脱掉外套",才能检查你有没有藏在口袋里。
思考记忆提示 — 这是全篇最重要的"地图"——理解完整链路中每个环节的职责和顺序
Kubernetes API Server 处理请求的完整链路分为 6 个阶段,每个阶段都有明确的职责:
┌──────────────────────────────────────────────────────────────────────────────────┐
│ Kubernetes API Server 请求处理完整链路 │
│ │
│ ┌─────────────────────────┐ │
│ │ 1. Authentication │ 用户身份验证 │
│ │ (认证) │ ├─ X509 证书 │
│ │ pkg/authentication/ │ ├─ Bearer Token │
│ │ │ └─ OIDC Token │
│ │ │ 输出:UserInfo(username, uid, groups) │
│ └────────────┬────────────┘ │
│ │ 验证通过 │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ 2. Authorization │ 权限检查(RBAC) │
│ │ (授权) │ ├─ Can I create this resource in this namespace? │
│ │ pkg/authorization/ │ └─ RBAC / ABAC / Webhook │
│ │ │ 输出:Decision (Allow / Deny) │
│ └────────────┬────────────┘ │
│ │ 允许 │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ 3. MutatingAdmission │ 变形准入(可修改对象) │
│ │ Webhook │ ├─ 内置:MutatingAdmissionWebhook │
│ │ (在请求体写入 etcd 前 │ ├─ 内置:DefaultStorageVersion │
│ │ 修改对象) │ ├─ 内置:NamespaceLifecycle │
│ │ │ └─ 自定义:MutatingWebhookConfiguration │
│ │ │ 特点:可以修改对象、多次调用(Reinvocation) │
│ └────────────┬────────────┘ │
│ │ 对象被修改(如果有) │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ 4. ValidatingAdmission │ 验证准入(只检查,不修改) │
│ │ Webhook │ ├─ 内置:ValidatingAdmissionWebhook │
│ │ (检查对象是否符合要求, │ ├─ 内置:ResourceQuota │
│ │ 拒绝或允许) │ ├─ 内置:PodSecurity │
│ │ │ └─ 自定义:ValidatingWebhookConfiguration │
│ │ │ 特点:只验证不修改,并行调用,无 Reinvocation │
│ └────────────┬────────────┘ │
│ │ 允许 │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ 5. Persistence │ 持久化到 etcd │
│ │ (etcd 写入) │ ├─ 一致性保证(RAFT 协议) │
│ │ pkg/storage/ │ └─ Watch 通知所有 Watcher │
│ └────────────┬────────────┘ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ 6. Watch Notification │ 通知所有 Watcher(Informer 等) │
│ │ (etcd → API Server → │ │
│ │ Informer Reflector) │ │
│ └─────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────┘
设计精髓
MutatingWebhook 必须在 ValidatingWebhook 之前执行,这是一个关键的有序性保证。因为 MutatingWebhook 可能修改对象(比如添加默认标签),修改后的对象才是 ValidatingWebhook 要验证的最终形态。如果 ValidatingWebhook 先执行,它验证的是原始对象,而不是最终会被写入 etcd 的对象——这会导致验证结果不准确。
思考记忆提示 — 本节展示 Webhook 在 kube-apiserver 内部是如何被调用的
Webhook 插件在 API Server 的 handler chain 中处于这样的位置:
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ kube-apiserver Handler Chain │
│ │
│ ┌───────────┐ ┌──────────────┐ ┌────────────────┐ ┌────────────────────┐ │
│ │ Filters │───▶│ Audit │───▶│ Authentication │───▶│ Authorization │ │
│ │ (基础过滤) │ │ (审计日志) │ │ (认证) │ │ (授权) │ │
│ └───────────┘ └──────────────┘ └───────┬────────┘ └─────────┬────────┘ │
│ │ │ │
│ ┌──────┴─────────────────────────┴──────┐ │
│ │ Admission Chain │ │
│ │ │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ Built-in Mutating Plugins │ │ │
│ │ │ (NamespaceLifecycle, etc.) │ │ │
│ │ └─────────────┬───────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ MutatingAdmissionWebhook │ │ │
│ │ │ (调用外部 Webhook 服务) │ │ │
│ │ │ 可能在内部多次调用 │ │ │
│ │ └─────────────┬───────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ Built-in Validating Plugins │ │ │
│ │ │ (ResourceQuota, PodSecurity) │ │ │
│ │ └─────────────┬───────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ ValidatingAdmissionWebhook │ │ │
│ │ │ (调用外部 Webhook 服务) │ │ │
│ │ │ 并行调用,短路逻辑 │ │ │
│ │ └─────────────────────────────────┘ │ │
│ └───────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Storage (etcd) │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────────┘
关键点:Webhook 配置是通过 MutatingWebhookConfiguration 和 ValidatingWebhookConfiguration 这两个 Kubernetes API 对象来管理的——API Server 通过 Informer 监听这两个资源的变化,动态更新内存中的 Webhook 调用列表。
思考记忆提示 — 这两个接口是整个 Admission 体系的抽象核心,理解它们就知道如何扩展 Admission
Kubernetes 的 Admission 体系基于一组简洁的接口构建(staging/src/k8s.io/apiserver/pkg/admission/interfaces.go):
// staging/src/k8s.io/apiserver/pkg/admission/interfaces.go(行 122~144)
// Interface 是所有 Admission 插件的基接口
type Interface interface {
// Handles 返回该插件能处理的操作类型(CREATE/UPDATE/DELETE/CONNECT)
Handles(operation Operation) bool
}
// MutationInterface:变形(可变)准入
type MutationInterface interface {
Interface
// Admit 可以修改请求中的对象(通过 attr.GetObject() 获取并修改)
// Context 用于超时、取消和追踪
Admit(ctx context.Context, a Attributes, o ObjectInterfaces) (err error)
}
// ValidationInterface:验证(只读)准入
type ValidationInterface interface {
Interface
// Validate 只能检查对象是否符合规则,不能修改对象
// 如果拒绝,返回 error(包含 Status 或自定义信息)
Validate(ctx context.Context, a Attributes, o ObjectInterfaces) (err error)
}
Attributes 接口封装了请求的完整上下文:
// staging/src/k8s.io/apiserver/pkg/admission/interfaces.go(行 29~77)
type Attributes interface {
GetName() string // 对象名称
GetNamespace() string // 命名空间
GetResource() schema.GroupVersionResource // 资源类型
GetSubresource() string // 子资源(如 status、scale)
GetOperation() Operation // 操作类型:CREATE/UPDATE/DELETE/CONNECT
GetOperationOptions() runtime.Object // 操作选项(如 DeleteOptions)
IsDryRun() bool // 是否是 DryRun 请求
GetObject() runtime.Object // 请求中的对象(可修改,用于 Mutation)
GetOldObject() runtime.Object // 旧对象(仅用于 UPDATE/DELETE)
GetKind() schema.GroupVersionKind // 对象类型
GetUserInfo() user.Info // 请求用户信息
// 添加审计注解(供 Audit 系统记录)
AddAnnotation(key, value string) error
AddAnnotationWithLevel(key string, value string, level auditinternal.Level) error
// Reinvocation 上下文(MutatingWebhook 多轮调用相关)
GetReinvocationContext() ReinvocationContext
}
我的理解的意思是说
MutationInterface 和 ValidationInterface 的根本区别在于"是否允许修改对象"。
MutationInterface 的 Admit() 接收一个 Attributes 对象,其中 GetObject() 返回的是可修改的对象引用。MutatingWebhook 可以直接修改这个对象(比如添加标签、设置默认值),修改后的对象会被序列化回请求体,写入 etcd。
ValidationInterface 的 Validate() 同样接收 Attributes,但不能修改对象——只能读取对象、检查条件,然后返回 error 或 nil。这是一种"只读"约束,它确保 ValidatingWebhook 的行为是确定性的(同一个对象,无论何时验证,结果都相同)。
思考记忆提示 — AdmissionReview 是 Webhook 和 API Server 之间的"通信协议"——理解它就知道 Webhook 服务应该怎么写
AdmissionReview 是 Kubernetes 定义的标准 HTTP 请求/响应格式,Webhook 服务通过它与 API Server 通信(staging/src/k8s.io/api/admission/v1/types.go):
// staging/src/k8s.io/api/admission/v1/types.go(行 30~178)
// AdmissionReview 是请求/响应的外层包装
type AdmissionReview struct {
metav1.TypeMeta `json:",inline"`
Request *AdmissionRequest `json:"request,omitempty"` // API Server → Webhook
Response *AdmissionResponse `json:"response,omitempty"` // Webhook → API Server
}
// ========== Request(API Server → Webhook)==========
type AdmissionRequest struct {
UID types.UID // 唯一标识,Response 必须原样返回
Kind metav1.GroupVersionKind // 对象的完整类型(如 v1.Pod)
Resource metav1.GroupVersionResource // 资源类型(如 v1.pods)
SubResource string // 子资源(如 "status"、"scale")
// 请求的对象(CREATE 时)或新对象(UPDATE 时)
Object runtime.RawExtension // JSON 序列化后的字节流
// 旧对象(仅 UPDATE/DELETE 时,用于对比变化)
OldObject runtime.RawExtension
Operation Operation // CREATE | UPDATE | DELETE | CONNECT
UserInfo authenticationv1.UserInfo // 请求用户信息
// DryRun 表示这只是试运行,不实际持久化
DryRun *bool
// Options 包含操作选项(如 CreateOptions/DeleteOptions)
Options runtime.RawExtension
}
// ========== Response(Webhook → API Server)==========
type AdmissionResponse struct {
UID types.UID // 必须与 Request.UID 完全一致
// 允许或拒绝
Allowed bool
// 拒绝原因(仅在 Allowed=false 时使用)
Result *metav1.Status
// JSON Patch 内容(仅 MutatingWebhook 使用)
// 格式:RFC 6902 JSON Patch([{op, path, value}, ...])
Patch []byte
// Patch 类型,目前只支持 JSONPatch
PatchType *PatchType // PatchTypeJSONPatch = "JSONPatch"
// 审计注解(键值对,会被记录到审计日志)
AuditAnnotations map[string]string
// 警告信息(返回给客户端,v1.36 新增)
Warnings []string
}
// 支持的操作类型
const (
Create Operation = "CREATE"
Update Operation = "UPDATE"
Delete Operation = "DELETE"
Connect Operation = "CONNECT"
)
// ========== HTTP Request (API Server → Webhook) ==========
POST /admissionreviews HTTP/1.1
Content-Type: application/json
{
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"request": {
"uid": "e9a23bb0-1234-5678-abcd-123456789abc",
"kind": {"group":"","version":"v1","kind":"Pod"},
"resource": {"group":"","version":"v1","resource":"pods"},
"namespace": "default",
"operation": "CREATE",
"userInfo": {
"username": "admin",
"uid": "admin-uid",
"groups": ["system:masters"]
},
"object": {
"apiVersion": "v1",
"kind": "Pod",
"metadata": {"name":"my-pod","namespace":"default"},
"spec": {
"containers": [{
"name": "nginx",
"image": "nginx:1.25"
}]
}
},
"dryRun": false
}
}
// ========== HTTP Response (Webhook → API Server, Mutating) ==========
HTTP/1.1 200 OK
Content-Type: application/json
{
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"response": {
"uid": "e9a23bb0-1234-5678-abcd-123456789abc",
"allowed": true,
"patchType": "JSONPatch",
"patch": "W3sgIm9wIjoiYWRkIiwicGF0aCI6Ii9tZXRhZGF0YS9sYWJlbHMiLCJ2YWx1ZSI6eyJhZGQtbGFiZWwiOiJ5ZXMifV19"
// Base64([{"op":"add","path":"/metadata/labels","value":{"add-label":"yes"}}])
}
}
小贴士
Request.Object 和 Request.OldObject 是 runtime.RawExtension 类型(JSON 序列化后的字节流),Webhook 需要用 json.Unmarshal 解析。如果对象是自定义资源(CRD),需要用 unstructured.Unstructured 来解析,因为没有对应的 Go 结构体。
思考记忆提示 — 这是全篇最重要的源码分析——MutatingDispatcher 是 API Server 调用外部 Webhook 的核心引擎
MutatingDispatcher 是 Kubernetes API Server 调用外部 MutatingWebhook 的核心引擎(staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/dispatcher.go)。
// staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/dispatcher.go(行 105~240)
// Dispatch 是 MutatingWebhook 的入口方法
func (a *mutatingDispatcher) Dispatch(
ctx context.Context,
attr admission.Attributes, // 请求属性(包含对象)
o admission.ObjectInterfaces, // 对象操作接口(创建器、转换器等)
hooks []webhook.WebhookAccessor, // 所有注册的 Webhook(从配置中获取)
) error {
// ========== 1. 初始化 Reinvocation 上下文(用于多次调用)==========
reinvokeCtx := attr.GetReinvocationContext()
// ...
// ========== 2. 遍历所有注册的 hooks ==========
for i, hook := range hooks {
// ShouldCallHook:根据 Rules、Selectors、MatchConditions 判断是否需要调用
invocation, statusErr := a.plugin.ShouldCallHook(ctx, hook, attrForCheck, o, v)
if statusErr != nil { return statusErr }
if invocation == nil { continue } // 不匹配,跳过
// ========== 3. 调用单个 Webhook ==========
changed, err := a.callAttrMutatingHook(ctx, hook, invocation, ...)
if err != nil {
// 错误处理:FailOpen / FailClosed
return err
}
// ========== 4. 如果对象被修改,标记需要重新调用 ==========
if changed {
webhookReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins()
reinvokeCtx.SetShouldReinvoke()
}
// ========== 5. 注册可重新调用的 Webhook ==========
// ReinvocationPolicy=IfNeeded 时生效
if hook.ReinvocationPolicy != nil && *hook.ReinvocationPolicy == IfNeededReinvocationPolicy {
webhookReinvokeCtx.AddReinvocableWebhookToPreviouslyInvoked(invocation.Webhook.GetUID())
}
}
// ========== 6. 将修改后的 VersionedObject 转换回内部版本 ==========
if v.versionedAttr.VersionedObject != nil && v.versionedAttr.Dirty {
return o.GetObjectConvertor().Convert(
v.versionedAttr.VersionedObject,
v.versionedAttr.Attributes.GetObject(), nil)
}
return nil
}
// staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/dispatcher.go(行 244~392)
// callAttrMutatingHook 负责:发送请求 → 接收响应 → 验证 → 应用 Patch
func (a *mutatingDispatcher) callAttrMutatingHook(
ctx context.Context,
h *admissionregistrationv1.MutatingWebhook,
invocation *generic.WebhookInvocation,
attr *admission.VersionedAttributes,
// ...
) (changed bool, err error) {
// ========== 1. DryRun 检查:不能有副作用 ==========
if attr.Attributes.IsDryRun() {
if *h.SideEffects == SideEffectClassSome || *h.SideEffects == SideEffectClassNone {
// 只有 SideEffectClassNone 或 SideEffectClassNoneOnDryRun 才允许 DryRun
return false, ErrDryRunUnsupported
}
}
// ========== 2. 构建 AdmissionReview 请求体 ==========
uid, request, response, err := webhookrequest.CreateAdmissionObjects(attr, invocation)
// request 是序列化后的 v1.AdmissionReview 请求体
// ========== 3. 获取 Webhook 的 HTTP 客户端 ==========
client, err := invocation.Webhook.GetRESTClient(a.cm)
if err != nil { return false, ErrCallingWebhook }
// ========== 4. 发送 HTTP POST 请求到 Webhook 服务 ==========
// 注意:使用 Webhook 配置的超时时间
ctx, cancel := context.WithTimeout(ctx, time.Duration(*h.TimeoutSeconds)*time.Second)
defer cancel()
err = client.Post().Body(request).Do(ctx).Into(response)
// ========== 5. 验证响应 ==========
result, err := webhookrequest.VerifyAdmissionResponse(uid, true, response)
// 验证 UID 是否匹配、响应是否有效
// ========== 6. 处理审计注解和警告 ==========
for k, v := range result.AuditAnnotations {
attr.Attributes.AddAnnotation(h.Name + "/" + k, v)
}
for _, w := range result.Warnings {
warning.AddWarning(ctx, "", w)
}
// ========== 7. 如果拒绝,返回错误 ==========
if !result.Allowed {
return false, ErrWebhookRejection{Status: ToStatusErr(h.Name, result.Result)}
}
// ========== 8. 应用 JSON Patch 到对象 ==========
if len(result.Patch) > 0 {
patchObj, _ := jsonpatch.DecodePatch(result.Patch)
objJS, _ := runtime.Encode(jsonSerializer, attr.VersionedObject)
patchedJS, _ := patchObj.Apply(objJS) // 应用 Patch
// 解码回对象
newVersionedObject, _, _ := jsonSerializer.Decode(patchedJS, nil, newVersionedObject)
// 标记对象已被修改
attr.Dirty = true
attr.VersionedObject = newVersionedObject
// 重新应用默认值(Mutating 修改后可能需要重新 Default)
o.GetObjectDefaulter().Default(attr.VersionedObject)
return true, nil // changed=true
}
return false, nil
}
设计精髓
MutatingDispatcher 的 Patch 应用流程采用了累积 Patch模式:每个 Webhook 返回的 Patch 都会被应用到对象上,然后下一个 Webhook 看到的是前一个 Webhook 修改后的对象。这要求每个 Webhook 的 Patch 都基于同一个基础对象(原始请求)来编写,而不是基于前一个 Patch 的结果。Kubernetes 使用的是 RFC 6902 JSON Patch,每个 patch 操作是绝对路径(以 / 开头),因此不会出现"累积误差"问题。
思考记忆提示 — JSON Patch 是 MutatingWebhook 修改对象的标准方式——理解它就知道如何写一个 MutatingWebhook
Kubernetes MutatingWebhook 使用 RFC 6902 JSON Patch 作为标准修改格式。来看 Kubernetes 官方测试镜像 agnhost 中的实现(test/images/agnhost/webhook/addlabel.go):
// test/images/agnhost/webhook/addlabel.go(行 27~71)
// 演示三种场景下的 JSON Patch 写法
const (
// 场景1:对象还没有 labels map,需要先创建 labels 再添加键值对
addFirstLabelPatch = `[
{ "op": "add", "path": "/metadata/labels", "value": {"added-label": "yes"}}
]`
// 场景2:对象已有 labels map,只需添加特定键
addAdditionalLabelPatch = `[
{ "op": "add", "path": "/metadata/labels/added-label", "value": "yes" }
]`
// 场景3:标签已存在但值不对,需要替换
updateLabelPatch = `[
{ "op": "replace", "path": "/metadata/labels/added-label", "value": "yes" }
]`
)
func addLabel(ar v1.AdmissionReview) *v1.AdmissionResponse {
reviewResponse := v1.AdmissionResponse{}
reviewResponse.Allowed = true
pt := v1.PatchTypeJSONPatch // 声明 Patch 类型
// 解析请求体中的对象
obj := struct {
metav1.ObjectMeta `json:"metadata,omitempty"`
}{}
json.Unmarshal(ar.Request.Object.Raw, &obj)
labelValue, hasLabel := obj.ObjectMeta.Labels["added-label"]
switch {
case len(obj.ObjectMeta.Labels) == 0:
// 标签 Map 不存在:add 操作创建 /metadata/labels
reviewResponse.Patch = []byte(addFirstLabelPatch)
reviewResponse.PatchType = &pt
case !hasLabel:
// 标签 Map 存在但没有这个键:add 操作在已有 map 中添加键
reviewResponse.Patch = []byte(addAdditionalLabelPatch)
reviewResponse.PatchType = &pt
case labelValue != "yes":
// 标签值不正确:replace 操作替换值
reviewResponse.Patch = []byte(updateLabelPatch)
reviewResponse.PatchType = &pt
default:
// 已经是 "yes":不返回 Patch(对象不需要修改)
}
return &reviewResponse
}
小贴士
JSON Patch 的三个核心操作:add 在指定路径添加值(路径不存在则创建,存在则替换);replace 替换已存在的值;remove 删除已存在的值。需要注意的是:JSON Patch 的路径使用 RFC 6901 规范——/metadata/labels/foo 表示 labels 下的 foo 键,/metadata/labels/-(减号)在 JSON Patch 中是特殊操作,表示在数组末尾添加。
思考记忆提示 — ValidatingWebhook 的核心特点是并行调用和短路逻辑——一旦拒绝,后续 Webhook 不再调用
ValidatingDispatcher 的实现(staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/dispatcher.go)与 MutatingDispatcher 有几个关键区别:
// staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/dispatcher.go(行 88~175)
func (d *validatingDispatcher) Dispatch(ctx context.Context,
attr admission.Attributes, o admission.ObjectInterfaces,
hooks []webhook.WebhookAccessor) error {
// ========== 1. 筛选出匹配的 hooks ==========
var relevantHooks []*generic.WebhookInvocation
versionedAttrAccessor := &versionedAttributeAccessor{...}
for _, hook := range hooks {
invocation, err := d.plugin.ShouldCallHook(ctx, hook, attr, o, versionedAttrAccessor)
if err != nil { return err }
if invocation == nil { continue }
relevantHooks = append(relevantHooks, invocation)
}
if len(relevantHooks) == 0 { return nil } // 无匹配,直接通过
// ========== 2. 并行调用所有匹配的 hooks ==========
wg := sync.WaitGroup{}
errCh := make(chan error, 2*len(relevantHooks))
wg.Add(len(relevantHooks))
for i, invocation := range relevantHooks {
go func(invocation *generic.WebhookInvocation, idx int) {
defer wg.Done()
// 调用单个 Webhook
err := d.callHook(ctx, invocation, versionedAttrAccessor.versionedAttrs[invocation.Kind])
if err != nil {
errCh
我的理解的意思是说
ValidatingWebhook 选择并行调用而不是串行,有两个原因:
第一,ValidatingWebhook 不修改对象,所以不需要等前一个的结果。每个 Webhook 验证的是同一个原始对象,互不干扰。串行调用不会带来额外价值,只会增加延迟。
第二,短路逻辑是关键:一旦任何一个 Webhook 返回拒绝,后续的 Webhook 就不再被调用。这类似于"与"运算——任何一个条件不满足,整体失败。这大大减少了不必要的 HTTP 调用(如果前置条件已经失败,后面的检查就是浪费)。
但这也带来一个权衡:多个 ValidatingWebhook 之间无法共享验证结果。如果两个 Webhook 都要做类似的检查(比如都检查镜像白名单),它们会各自调用一次,增加 API Server 的调用负载。
// test/images/agnhost/webhook/alwaysdeny.go(行 25~32)
// 最简单的 ValidatingWebhook:拒绝所有请求
func alwaysDeny(ar v1.AdmissionReview) *v1.AdmissionResponse {
reviewResponse := v1.AdmissionResponse{}
reviewResponse.Allowed = false // 拒绝
reviewResponse.Result = &metav1.Status{
Message: "this webhook denies all requests",
}
return &reviewResponse
}
思考记忆提示 — Webhook 配置决定了 Webhook 的作用范围和行为策略——理解它就能正确部署一个 Webhook
Webhook 通过 Kubernetes API 对象注册到 API Server(staging/src/k8s.io/api/admissionregistration/v1/types.go):
# MutatingWebhookConfiguration 示例
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: my-mutating-webhook
webhooks:
# ========== 核心配置 ==========
- name: mutating-webhook.example.com # Webhook 名称(必须唯一)
sideEffects: None # 或 NoneOnDryRun(不允许有副作用必须声明)
# ========== 调用目标配置 ==========
clientConfig:
service:
namespace: webhook-system
name: webhook-service # Service 名称
path: /mutating-pods # 可选路径(默认 /)
port: 443 # Service 端口
# 或使用 URL(用于集群外服务)
# caBundle: LS0tLS1... # Base64 编码的 CA 证书(用于验证服务端 TLS)
# ========== 触发条件:请求匹配规则 ==========
rules:
- operations: ["CREATE", "UPDATE"] # 监听哪些操作
apiGroups: [""] # API Group(空字符串=核心 API)
apiVersions: ["v1"] # API 版本
resources: ["pods"] # 资源类型
scope: "Namespaced" # 作用域:Namespaced/Cluster/*
# ========== 行为策略 ==========
failurePolicy: Ignore # Fail = 拒绝请求,Ignore = 忽略错误继续
matchPolicy: Equivalent # Exact = 精确匹配,Equivalent = 等价资源匹配
# ========== 选择器(额外过滤)==========
namespaceSelector: # 只作用于匹配的命名空间
matchLabels:
environment: production
objectSelector: # 只作用于匹配的对象
matchLabels:
webhook-enabled: "true"
# ========== 超时时间(默认 10s)==========
timeoutSeconds: 30
# ========== 仅 MutatingWebhook ==========
reinvocationPolicy: IfNeeded # Never / IfNeeded
# ========== 匹配条件(CEL 表达式,v1.27+)==========
matchConditions:
- name: exclude-leader-election
expression: "request.operation != 'CREATE' || !request.object.metadata.labels.containsKey('control-plane')"
---
# ValidatingWebhookConfiguration 示例
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: my-validating-webhook
webhooks:
- name: validating-webhook.example.com
sideEffects: None
clientConfig:
service:
namespace: webhook-system
name: webhook-service
path: /pods
caBundle: LS0tLS1...
rules:
- operations: ["CREATE"]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
scope: "Namespaced"
failurePolicy: Fail
matchPolicy: Equivalent
注意
sideEffects 是必填字段,必须为以下四个值之一:None(无副作用)、NoneOnDryRun(仅在 DryRun 时无副作用)、Some(有副作用,必须在独立沙箱中运行)。这是 Kubernetes 为了防止生产事故而设计的强制声明——如果 Webhook 有副作用(修改集群状态),它必须在隔离的沙箱环境中运行,不能与 API Server 共享资源。
思考记忆提示 — 这三个配置决定了 Webhook 的安全边界和性能特性——配置错误可能导致集群故障
| 配置项 | 选项 | 含义 | 推荐场景 |
|---|---|---|---|
| FailurePolicy | Fail | Webhook 调用失败时,拒绝请求 | 生产环境(安全优先) |
| Ignore | Webhook 调用失败时,忽略错误继续处理 | 开发测试 / 非关键 Webhook | |
| MatchPolicy | Equivalent | 请求修改等效资源时也调用(推荐) | 生产环境(大多数场景) |
| Exact | 只精确匹配规则中声明的资源 | 需要严格控制范围时 | |
| SideEffects | None | Webhook 永无副作用 | 生产环境(必须配合 DryRun) |
| NoneOnDryRun | 仅在 DryRun 时无副作用 | MutatingWebhook(允许在 DryRun 时调用) | |
| Some | 有副作用 | 必须在隔离沙箱中运行 | |
| Unknown | 未知(不推荐) | — | |
| ReinvocationPolicy (仅 Mutating) |
Never | 只调用一次,不重新调用 | 默认 |
| IfNeeded | 树内插件修改对象后,重新调用 | 需要和内置插件协同工作时 |
小贴士
MatchPolicy: Equivalent 是推荐配置。例如,Deployment 在 apps/v1beta2 和 apps/v1 两个版本都有定义。如果 Webhook 注册的规则是 apiVersions: ["apps/v1"], resources: ["deployments"],Equivalent 模式下,对 apps/v1beta2 Deployment 的请求也会被发送到 Webhook(因为它们是"等价"的)。Exact 模式下则不会。
思考记忆提示 — 这是全篇的"工程落地"部分——从 HTTP 服务器到完整请求处理链路
// 基于 test/images/agnhost/webhook/main.go 整理
package webhook
import (
"encoding/json"
"io"
"net/http"
v1 "k8s.io/api/admission/v1"
"k8s.io/api/admission/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/klog/v2"
)
// ========== 1. 定义 Handler 类型 ==========
type admitv1Func func(v1.AdmissionReview) *v1.AdmissionResponse
type admitv1beta1Func func(v1beta1.AdmissionReview) *v1beta1.AdmissionResponse
type admitHandler struct {
v1beta1 admitv1beta1Func // v1beta1 版本处理函数
v1 admitv1Func // v1 版本处理函数
}
func newDelegateToV1AdmitHandler(f admitv1Func) admitHandler {
return admitHandler{
v1beta1: delegateV1beta1AdmitToV1(f), // v1beta1 → v1 转换
v1: f,
}
}
// ========== 2. 核心 HTTP Handler ==========
func serve(w http.ResponseWriter, r *http.Request, admit admitHandler) {
// 读取请求体
var body []byte
if r.Body != nil {
data, _ := io.ReadAll(r.Body)
body = data
}
// 验证 Content-Type
contentType := r.Header.Get("Content-Type")
if contentType != "application/json" {
http.Error(w, "expect application/json", http.StatusBadRequest)
return
}
// 反序列化:根据 GVK 判断版本
deserializer := codecs.UniversalDeserializer()
obj, gvk, err := deserializer.Decode(body, nil, nil)
if err != nil {
http.Error(w, "cannot decode: "+err.Error(), http.StatusBadRequest)
return
}
var responseObj runtime.Object
switch *gvk {
// ========== v1beta1 版本处理 ==========
case v1beta1.SchemeGroupVersion.WithKind("AdmissionReview"):
review := obj.(*v1beta1.AdmissionReview)
resp := &v1beta1.AdmissionReview{}
resp.SetGroupVersionKind(*gvk)
resp.Response = admit.v1beta1(*review)
resp.Response.UID = review.Request.UID // 必须原样返回 UID
responseObj = resp
// ========== v1 版本处理 ==========
case v1.SchemeGroupVersion.WithKind("AdmissionReview"):
review := obj.(*v1.AdmissionReview)
resp := &v1.AdmissionReview{}
resp.SetGroupVersionKind(*gvk)
resp.Response = admit.v1(*review)
resp.Response.UID = review.Request.UID
responseObj = resp
default:
http.Error(w, "unsupported GVK: "+gvk.String(), http.StatusBadRequest)
return
}
// 序列化响应并返回
respBytes, _ := json.Marshal(responseObj)
w.Header().Set("Content-Type", "application/json")
w.Write(respBytes)
}
// ========== 3. 业务逻辑:添加标签的 MutatingWebhook ==========
func addLabel(ar v1.AdmissionReview) *v1.AdmissionResponse {
obj := struct {
metav1.ObjectMeta `json:"metadata,omitempty"`
}{}
json.Unmarshal(ar.Request.Object.Raw, &obj)
reviewResponse := v1.AdmissionResponse{Allowed: true}
pt := v1.PatchTypeJSONPatch
switch {
case len(obj.ObjectMeta.Labels) == 0:
patch := `[{"op":"add","path":"/metadata/labels","value":{"injected":"true"}}]`
reviewResponse.Patch = []byte(patch)
reviewResponse.PatchType = &pt
case obj.ObjectMeta.Labels["injected"] != "true":
patch := `[{"op":"add","path":"/metadata/labels/injected","value":"true"}]`
reviewResponse.Patch = []byte(patch)
reviewResponse.PatchType = &pt
}
return &reviewResponse
}
// ========== 4. 主函数:路由注册 ==========
func main() {
http.HandleFunc("/mutating-pods", func(w http.ResponseWriter, r *http.Request) {
serve(w, r, newDelegateToV1AdmitHandler(addLabel))
})
http.HandleFunc("/always-deny", func(w http.ResponseWriter, r *http.Request) {
serve(w, r, newDelegateToV1AdmitHandler(alwaysDeny))
})
http.HandleFunc("/readyz", func(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("ok"))
})
server := &http.Server{Addr: ":443", TLSConfig: configTLS()}
server.ListenAndServeTLS("", "")
}
我的理解的意思是说
这个 Webhook 实现有四个关键设计点:
思考记忆提示 — 这一节解决"如何把 Webhook 跑起来"的问题——证书生成是最容易出错的地方
# ========== 1. 生成 CA(自签名)==========
openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \
-out ca.crt -subj "/CN=webhook-ca"
# ========== 2. 生成 Webhook 服务器证书(由 CA 签名)==========
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr \
-subj "/CN=webhook-service.webhook-system.svc" \
-addext "subjectAltName=DNS:webhook-service.webhook-system.svc"
# ========== 3. 使用 CA 签署服务器证书 ===========
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out server.crt -days 365 -sha256 \
-extfile <(printf "subjectAltName=DNS:webhook-service.webhook-system.svc")
# ========== 4. 验证证书 ==========
openssl x509 -in server.crt -text -noout | grep -A1 "Subject Alternative Name"
# ========== Deployment + Service ==========
apiVersion: v1
kind: Namespace
metadata:
name: webhook-system
---
apiVersion: v1
kind: Service
metadata:
name: webhook-service
namespace: webhook-system
spec:
ports:
- port: 443
targetPort: 443
selector:
app: webhook
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: webhook
namespace: webhook-system
spec:
replicas: 2
selector:
matchLabels:
app: webhook
template:
metadata:
labels:
app: webhook
spec:
containers:
- name: webhook
image: my-webhook:v1.0.0
ports:
- containerPort: 443
volumeMounts:
- name: tls-certs
mountPath: /etc/webhook/certs
readOnly: true
readinessProbe:
httpGet:
path: /readyz
port: 443
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: tls-certs
secret:
secretName: webhook-tls
---
# ========== ValidatingWebhookConfiguration ==========
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: my-validating-webhook
# Webhook 通常需要持久化配置,不能依赖 webhook 自身存储
# 使用 Opaque secret 或其他外部存储管理
webhooks:
- name: my-webhook.example.com
sideEffects: None
clientConfig:
service:
namespace: webhook-system
name: webhook-service
path: "/pods"
# caBundle:API Server 用于验证 Webhook 服务端证书的 CA
# 如果 Webhook 使用 kubernetes.io/legacy-unknown CA(k8s 自动注入),可以留空
# caBundle: LS0tLS1... # Base64(ca.crt)
rules:
- operations: ["CREATE", "UPDATE"]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
scope: "Namespaced"
failurePolicy: Fail
timeoutSeconds: 30
注意
生产环境中,Webhook 的 caBundle 不能硬编码(否则证书轮换时需要更新 Config 并重启 API Server)。推荐使用 MutatingWebhookConfiguration 或 ValidatingWebhookConfiguration 中的 webhooks.clientConfig.service 引用 Service,让 API Server 通过服务发现自动获取 CA 证书。Kubernetes 1.27+ 还支持通过 service.name 和 service.path 引用,API Server 会自动注入正确的 caBundle。
思考记忆提示 — 测试是 Webhook 开发中最容易被忽视但最重要的环节
Kubernetes 提供了完整的 Webhook 测试框架(staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testing/):
// ========== 1. 准备测试场景 ==========
// staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testing/testcase.go
// ValidatingWebhook 测试用例
validatingTest := []TestCase{
{
Name: "allow pods with allowed label",
Webhook: ®istrationv1.ValidatingWebhook{
Name: "test-webhook.example.com",
Rules: []registrationv1.RuleWithOperations{{
Operations: []registrationv1.OperationType{registrationv1.Create},
Rule: registrationv1.Rule{
APIGroups: []string{""},
APIVersions: []string{"v1"},
Resources: []string{"pods"},
},
}},
},
// Fake Webhook Server:返回 Allowed=true
WebhookReactor: func() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
review := v1.AdmissionReview{}
json.NewDecoder(r.Body).Decode(&review)
resp := v1.AdmissionReview{
Response: &v1.AdmissionResponse{
UID: review.Request.UID,
Allowed: true,
},
}
json.NewEncoder(w).Encode(resp)
}
},
Object: &v1.Pod{...},
ExpectedDecision: TestDecisionAllow,
},
{
Name: "deny pods with disallowed label",
// Fake Webhook 返回拒绝
ExpectedDecision: TestDecisionDeny,
ExpectedReason: "webhook rejected the request",
},
}
// ========== 2. 运行测试 ==========
// TestWebhookSuite 运行完整的 Webhook 测试套件
func TestWebhookSuite(t *testing.T) {
suite := webhook_testing.NewWebhookTestSuite("my-webhook-tests")
suite.Register(validatingTest, mutatingTest)
suite.Run(t)
}
// ========== 3. MutatingWebhook Patch 验证 ==========
mutatingTest := []TestCase{
{
Name: "adds injected label to pod",
Webhook: ®istrationv1.MutatingWebhook{...},
WebhookReactor: func() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
review := v1.AdmissionReview{}
json.NewDecoder(r.Body).Decode(&review)
patch := `[{"op":"add","path":"/metadata/labels","value":{"injected":"true"}}]`
resp := v1.AdmissionReview{
Response: &v1.AdmissionResponse{
UID: review.Request.UID,
Allowed: true,
Patch: []byte(patch),
PatchType: func() *v1.PatchType { pt := v1.PatchTypeJSONPatch; return &pt }(),
},
}
json.NewEncoder(w).Encode(resp)
}
},
Object: &v1.Pod{...},
// ExpectedPatch:验证返回的 Patch 内容
ExpectedPatch: []byte(`[{"op":"add","path":"/metadata/labels","value":{"injected":"true"}}]`),
ExpectedPatchType: v1.PatchTypeJSONPatch,
},
}
// ========== 4. Fake HTTPS Server(测试用)==========
// webhook_server.go 提供了开箱即用的 Fake HTTPS Server
ts := webhook_testing.NewTestServer("testdata/tls.crt", "testdata/tls.key")
defer ts.Close()
// 注册自定义处理函数
ts.RegisterHandler("/mutating-pods", func(w http.ResponseWriter, r *http.Request) {
// 处理请求...
})
设计精髓
Kubernetes 的 Webhook 测试框架通过Fake HTTPS Server实现了端到端的测试。测试服务器使用真实的 TLS 证书(可以是自签名),完全模拟 API Server 与 Webhook 之间的 HTTPS 通信。这确保了测试环境与生产环境高度一致——TLS 握手、HTTP 请求格式、响应序列化全部真实模拟。
思考记忆提示 — Webhook 是 Operator 开发的三大支柱之一,另外两个是 Controller 和 CRD
全篇必记总纲
Admission Webhook 的完整链路是:API Server(Authentication → Authorization)→ MutatingAdmissionWebhook(修改对象)→ ValidatingAdmissionWebhook(验证对象)→ Storage。MutatingWebhook 通过 JSON Patch 修改对象(多个 Webhook 串联 Patch),ValidatingWebhook 并行验证(短路逻辑)。两者通过 AdmissionReview 协议与外部服务通信,配置通过 MutatingWebhookConfiguration 和 ValidatingWebhookConfiguration 资源动态管理。
思考记忆提示 — FAQ 是全篇的"临考前速背"模块,20 组覆盖全部关键知识点
Authentication 验证"你是谁",Authorization 验证"你能做什么",Admission 验证"你的请求是否合规"。Authentication(认证)发生在最前面,通过 X509 证书、Bearer Token 或 OIDC Token 验证请求者的身份,输出 UserInfo(username, uid, groups)。Authorization(授权)接在认证后面,通过 RBAC/ABAC/Webhook 检查该用户是否有权限执行这个操作。Admission 位于两者之后,验证请求本身是否符合集群的策略规范(与身份无关)。三者的关系是:先确认身份,再检查权限,最后检查合规性——任何一步失败,请求都不会到达 etcd。
MutatingWebhook 必须在 ValidatingWebhook 之前执行,这个顺序不能颠倒。因为 MutatingWebhook 可以修改对象(比如添加标签、设置默认值),修改后的对象才是最终会被持久化的状态。ValidatingWebhook 需要验证的是最终状态,不是原始状态。如果 ValidatingWebhook 先执行,它验证的是用户提交的对象,而不是实际会被写入 etcd 的对象——这会导致验证结果不准确。
MutatingWebhook 多次调用是为了与树内内置插件(如 DefaultStorageVersion)配合;ValidatingWebhook 一次就足够,因为每个 Webhook 验证的是同一个原始对象。树内内置的 Mutating 插件在外部 Webhook 之后执行(称为"树内插件")。如果树内插件修改了对象(如设置默认值),MutatingWebhook 需要重新看到修改后的对象,以便做进一步的修改或验证。ReinvocationPolicy=IfNeeded 可以实现这个效果。ValidatingWebhook 不修改对象,每个 Webhook 验证的是同一个原始对象,多次调用没有意义,反而增加延迟。
UID 是防重放和请求匹配的唯一标识,确保 API Server 能正确匹配请求和响应。API Server 可能同时向多个 Webhook 发送请求,多个 Webhook 的响应可能以任意顺序返回(ValidatingWebhook 并行调用)。通过 UID,API Server 能准确地将每个响应与对应的请求匹配。如果 UID 不匹配或缺失,VerifyAdmissionResponse 会返回错误,请求被拒绝。
add 在指定路径添加值(路径不存在则创建,存在则替换),replace 只替换已存在的值。add 用于两种场景:1)路径本身不存在(如对象还没有 labels map),需要先创建父路径;2)在已有数组或对象中添加键。replace 用于路径已存在的情况——如果路径不存在,replace 会报错。编写 MutatingWebhook 的 Patch 时,通常先用 add 尝试,如果 add 报错再用 replace。
空 Patch 表示该 Webhook 不修改对象,API Server 跳过 Patch 应用,继续处理下一个 Webhook。在 dispatcher.go:333~334 的逻辑中,如果 len(result.Patch)==0,直接返回 changed=false,对象保持不变。但需要注意:如果 Webhook 返回 Allowed=false(拒绝请求),即使有 Patch 也不会被应用(请求直接被拒绝)。
Fail 在 Webhook 调用失败时拒绝请求(安全优先),Ignore 忽略失败继续处理(可用性优先)。生产环境推荐 Fail——如果 Webhook 是安全策略的一部分(如镜像白名单),调用失败时拒绝请求比放行更安全。对于非关键性 Webhook(如仅用于添加可选标签),可以设为 Ignore,避免 Webhook 服务临时不可用时阻塞所有资源创建。
sideEffects 是 Kubernetes 的强制安全声明,防止有副作用的 Webhook 污染 API Server。四个合法值:None(永无副作用)、NoneOnDryRun(仅 DryRun 时无副作用)、Some(有副作用,必须在隔离沙箱中运行)、Unknown(不推荐)。如果 Webhook 每次请求都会修改集群状态(如写数据库、发消息),必须声明为 Some。声明不实可能导致 API Server 拒绝加载该 Webhook 配置。
NamespaceSelector 根据请求的命名空间标签过滤,ObjectSelector 根据请求对象的标签过滤。NamespaceSelector 是在请求进入 API Server 时就能判断的(已知目标命名空间),不需要解析对象内容,适合做"整个命名空间的开关"。ObjectSelector 需要先解析对象标签,适合做"按资源标签精细控制"。两者可以同时使用,只有同时满足才调用 Webhook。
Equivalent 会匹配所有与规则等价的 API 请求,Exact 只匹配精确声明的资源。例如,Deployment 同时在 apps/v1 和 apps/v1beta1 中定义(等价资源)。如果 Webhook 规则声明 apiVersions: ["apps/v1"],Equivalent 模式下对 apps/v1beta2 Deployment 的请求也会发送到 Webhook(因为是等价资源)。Exact 模式则不会。建议使用 Equivalent(默认行为)。
并行调用可以减少总延迟(所有 Webhook 同时执行,取最长耗时);串行调用没有意义,因为 ValidatingWebhook 不修改对象,验证的是同一个原始对象。权衡是:多个 Webhook 如果都做类似的验证(如镜像白名单检查),会重复调用,浪费 API Server 的调用资源。但对于不同维度的验证(如镜像白名单 + 资源配额 + 安全上下文),并行调用的延迟优势明显。
MatchConditions 用 CEL 表达式实现比 Rules 更灵活的匹配条件,基于请求元数据和对象内容做判断。Rules 按 apiGroups/apiVersions/resources/operations 做粗粒度过滤(只能判断"是否是 Pod")。MatchConditions 可以基于请求的任何信息做判断,例如:"request.userInfo.username != 'system:kube-proxy'" 或 "request.object.spec.priority != nil"。MatchConditions 在 Rules 之后执行,作为更细粒度的二次过滤。
timeoutSeconds 默认 10 秒,超时后按 FailurePolicy 处理。如果 FailurePolicy=Fail,超时会拒绝请求;如果 FailurePolicy=Ignore,超时会忽略错误继续。建议将 timeoutSeconds 设置为 Webhook 实际处理时间的 1.5~2 倍,留足 buffer 应对网络抖动和 GC 暂停。
每个 Webhook 的 Patch 都是基于原始对象的绝对路径修改,API Server 依次应用所有 Patch,形成累积效果。例如:Webhook A 添加 labels.webhook-a,Webhook B 添加 labels.webhook-b,最终对象会同时有两个标签。Webhoo k 的 Patch 是基于请求 JSON 序列化后的字节流来编写的(RFC 6902 JSON Patch),每个操作是绝对路径,不会依赖前一个 Patch 的结果。
通过 webhook_testing.TestCase 的 ExpectedPatch 和 ExpectedPatchType 字段验证。Kubernetes 提供了完整的 Webhook 测试框架,通过 NewTestServer 创建 Fake HTTPS Server,注册返回特定 Patch 的处理函数,然后通过 webhook_testing.TestCase 定义期望的 Patch 内容(Base64 编码或 JSON 字符串),框架会自动验证返回的 Patch 是否与期望一致。
API Server 要求与 Webhook 的通信必须是加密的,以防止中间人攻击和数据篡改。自签名证书的流程:1)生成 CA;2)用 CA 签署 Webhook 服务器证书(CN 必须匹配 Service DNS 名);3)将 CA 证书的 Base64 编码放入 webhook.clientConfig.caBundle;4)服务器加载服务器证书和私钥。如果使用 Kubernetes 1.27+,可以省略 caBundle(API Server 会自动从 Service 发现中获取正确的 CA)。
通过 MutatingWebhookConfiguration/ValidatingWebhookConfiguration 中的 clientConfig.service 引用 Kubernetes Service。API Server 在启动时和配置变更时(通过 Informer)从配置对象中读取 Service 引用(namespace + name),通过 Kubernetes DNS 解析到 Pod IP。API Server 验证 TLS 证书时,会用 caBundle 中的 CA 验证服务端证书的 CN/SAN 是否与服务 DNS 匹配。
在 serve() 函数中根据 GVK(GroupVersionKind)判断走哪个分支,v1beta1 路由到 v1 处理函数(版本转换)。实现模式:case v1beta1.SchemeGroupVersion.WithKind("AdmissionReview"): ... admit.v1beta1(*review),在 v1beta1 处理函数中调用 convertAdmissionRequestToV1() 将请求转换为 v1 格式,调用 v1 业务逻辑,再通过 convertAdmissionResponseToV1beta1() 转换回 v1beta1 响应。这样业务逻辑只需实现一份。
DryRun 时,只有 sideEffects=None 或 NoneOnDryRun 的 MutatingWebhook 才会被调用。如果 Webhook 声明了 sideEffects=Some(可能有副作用),API Server 不会在 DryRun 请求中调用它,返回 DryRunUnsupported 错误。这是为了防止在 DryRun 场景下产生意外的集群状态变更。
AuditAnnotations 记录到审计日志(需要开启审计策略),Warnings 返回给 API 客户端。AuditAnnotations 是键值对(如 "imagepolicy.example.com/reason": "image blacklisted"),由 API Server 根据审计策略决定是否记录到审计日志。Warnings 是字符串数组(v1.36 新增),直接返回在 HTTP 响应头 Warning: 299 ... 中,kubectl 客户端会显示给用户。两者的用途不同:审计用于事后追溯,警告用于实时通知。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。