【kubernetes v1.21】(kube-apiserver 3)kube-apiserver Admission Control 与 CRD 超深度分析
Part 3: kube-apiserver Admission Control 与 CRD 超深度分析
一、模块定位
1.1 Admission Control 业务职责
Admission Control(准入控制)是 kube-apiserver 请求处理管线中的核心守门人,位于认证(Authentication)与授权(Authorization)之后、持久化存储之前。其业务职责可归纳为:
| 职责维度 | 描述 |
|---|---|
| 变更(Mutation) | 在对象持久化前对其进行自动修改——注入默认值、补全标签、设置初始值 |
| 验证(Validation) | 在对象持久化前对其进行策略校验——拒绝不符合集群策略的请求 |
| 审计(Audit) | 在准入过程中记录关键决策信息,供后续审计追踪 |
| 副作用协调 | 如 ResourceQuota 的配额扣减、NamespaceLifecycle 的终止保护 |
Admission Control 的核心设计原则是双阶段执行:先 Mutate 再 Validate,且二者严格分离——Validation 阶段不得修改对象。这保证了验证逻辑的确定性,避免了变更与校验交织导致的不可预测行为。
1.2 CRD(CustomResourceDefinition)业务职责
CRD 是 Kubernetes 扩展 API 的核心机制,允许用户在不修改 apiserver 源码的前提下定义新的资源类型。其业务职责包括:
| 职责维度 | 描述 |
|---|---|
| 类型定义 | 声明自定义资源的 Group/Version/Kind、Scope(命名空间级/集群级) |
| Schema 校验 | 基于 OpenAPI v3 Schema 对 CR 实例进行结构验证 |
| 多版本管理 | 支持 CRD 的多版本定义及版本间转换(通过 Webhook 或 None 策略) |
| 子资源支持 | 为 CR 提供 /status 和 /scale 子资源端点 |
| OpenAPI 发布 | 将 CRD 的 Schema 动态注册到 OpenAPI / Discovery 端点 |
| 存储编排 | 为每个 CRD 动态创建 ETCD 存储层,处理 Unstructured 对象的序列化/反序列化 |
CRD 与 Admission Control 的交汇点在于:CRD 定义的自定义资源同样需要经过完整的 Admission Chain,包括内置插件和动态 Webhook 的变更与验证。
二、模块整体结构
2.1 Admission Plugin 接口层次
从源码 interfaces.go 可见,Admission 的接口设计呈三层结构:
Interface(基础接口)
└── Handles(operation Operation) bool —— 声明本插件处理的操作类型
MutationInterface(变更接口)
└── Interface + Admit(ctx, Attributes, ObjectInterfaces) error
ValidationInterface(验证接口)
└── Interface + Validate(ctx, Attributes, ObjectInterfaces) error
一个 Plugin 可以同时实现 MutationInterface 和 ValidationInterface,也可以只实现其中之一。例如:
NamespaceLifecycle只实现MutationInterface(其 Admit 方法既做变更也做验证,但在接口语义上归类为 Admit)MutatingAdmissionWebhook只实现MutationInterfaceValidatingAdmissionWebhook只实现ValidationInterfaceResourceQuota同时实现两个接口
Attributes 接口是 Admission 系统的"请求上下文",承载了全部准入决策所需信息:
type Attributes interface {
GetName() string // 对象名称
GetNamespace() string // 命名空间
GetResource() schema.GroupVersionResource // 资源类型(GVR)
GetSubresource() string // 子资源(status/scale等)
GetOperation() Operation // 操作类型(CREATE/UPDATE/DELETE/CONNECT)
GetOperationOptions() runtime.Object // 操作选项
IsDryRun() bool // 是否为 DryRun
GetObject() runtime.Object // 新对象
GetOldObject() runtime.Object // 旧对象(仅UPDATE)
GetKind() schema.GroupVersionKind // 类型(GVK)
GetUserInfo() user.Info // 请求用户信息
AddAnnotation(key, value string) error // 审计标注
GetReinvocationContext() ReinvocationContext // 重调用上下文
}
ObjectInterfaces 接口提供与对象交互的基础设施:
type ObjectInterfaces interface {
GetObjectCreater() runtime.ObjectCreater
GetObjectTyper() runtime.ObjectTyper
GetObjectDefaulter() runtime.ObjectDefaulter
GetObjectConvertor() runtime.ObjectConvertor
GetEquivalentResourceMapper() runtime.EquivalentResourceMapper
}
2.2 AdmissionChain 执行顺序
从源码 chain.go 可见,chainAdmissionHandler 本质是一个 []Interface 切片,Admit 和 Validate 分别顺序遍历:
func (admissionHandler chainAdmissionHandler) Admit(ctx context.Context, a Attributes, o ObjectInterfaces) error {
for _, handler := range admissionHandler {
if !handler.Handles(a.GetOperation()) {
continue // 跳过不处理该操作的插件
}
if mutator, ok := handler.(MutationInterface); ok {
err := mutator.Admit(ctx, a, o)
if err != nil {
return err // 首个错误即返回
}
}
}
return nil
}
kube-apiserver 默认启用的 Admission Plugin 及其推荐执行顺序如下(在 plugin/pkg/admission/order.go 中定义):
1. NamespaceLifecycle — 保护 immortal 命名空间、阻止在终止命名空间中创建对象
2. LimitRanger — 为 Pod/Container 注入默认 limit/request
3. ServiceAccount — 自动注入 ServiceAccount 和 ImagePullSecrets
4. NodeRestriction — 限制 kubelet 只能修改自身 Node 和绑定到自身的 Pod
5. DefaultTolerationSeconds — 为 Pod 注入默认容忍度
6. PodSecurity — Pod 安全标准 enforcement(替代原 PodSecurityPolicy)
7. DefaultStorageClass — 为 PVC 注入默认 StorageClass
8. MutatingAdmissionWebhook — 调用外部变更 Webhook(按 webhook 配置顺序串行)
9. ValidatingAdmissionPolicy — 基于CEL的策略验证(1.28+)
10. ValidatingAdmissionWebhook — 调用外部验证 Webhook(所有 webhook 并行调用)
11. ResourceQuota — 配额检查与扣减
12. DenyEscalatingExec — 阻止特权容器的 exec/attach
13. OwnerReferencesPermissionEnforcement — 检查 OwnerReference 修改权限
关键顺序约束:
- Mutating 必须在 Validating 之前执行,否则验证的是未变更的对象
- MutatingAdmissionWebhook 放在内置 Mutate 插件之后,使其能修改已被默认值填充的对象
- ValidatingAdmissionWebhook 放在 ResourceQuota 之前,使得被拒绝的请求不会浪费配额评估
- ResourceQuota 放在链末端,确保配额计算基于最终(已变更)的对象状态
2.3 CRD APIExtensions Server 类结构
从源码 apiserver.go 可见,CRD 子系统的核心类结构为:
type ExtraConfig struct {
CRDRESTOptionsGetter genericregistry.RESTOptionsGetter // ETCD 存储选项
MasterCount int // HA Master 数量
ServiceResolver webhook.ServiceResolver // Webhook 服务名解析
AuthResolverWrapper webhook.AuthenticationInfoResolverWrapper // Webhook 认证
}
type Config struct {
GenericConfig *genericapiserver.RecommendedConfig // 通用服务器配置
ExtraConfig ExtraConfig // CRD 专有配置
}
type completedConfig struct {
GenericConfig genericapiserver.CompletedConfig
ExtraConfig *ExtraConfig
}
type CustomResourceDefinitions struct {
GenericAPIServer *genericapiserver.GenericAPIServer // 内嵌通用 API Server
Informers externalinformers.SharedInformerFactory // CRD Informer 工厂
}
CRD APIExtensions Server 作为独立 API Server 运行,通过 delegation 机制委托给 kube-apiserver 的主 handler。其在 New() 方法中的初始化流程为:
- 创建
GenericAPIServer实例 - 注册 CRD 自身的 API(
customresourcedefinitions+customresourcedefinitions/status) - 创建
CustomResourceDefinitionHandler,拦截/apis/路径下的所有 CR 请求 - 启动多个控制器:Establishing、Naming、NonStructuralSchema、APIApproval、Finalizer、OpenAPI、Discovery
2.4 CustomResourceDefinition 类型体系
CRD 的类型定义涉及多个层次:
apiextensions.GroupName(apiextensions.k8s.io)
└── CustomResourceDefinition
├── Spec
│ ├── Group string // API Group
│ ├── Versions []CustomResourceDefinitionVersion
│ │ ├── Name string // 版本名(v1, v1beta1等)
│ │ ├── Served bool // 是否提供服务
│ │ ├── Storage bool // 是否为存储版本(仅一个)
│ │ ├── Schema *CustomResourceValidation // OpenAPI v3 Schema
│ │ ├── Subresources *CustomResourceSubresources
│ │ │ ├── Status *bool // /status 子资源
│ │ │ └── Scale *CustomResourceSubresourceScale // /scale 子资源
│ │ └── AdditionalPrinterColumns []CustomResourceColumnDefinition
│ ├── Scope ResourceScope // Namespaced 或 Cluster
│ ├── Conversion *CustomResourceConversion
│ │ ├── Strategy None/Webhook // 转换策略
│ │ └── Webhook *WebhookConversion
│ │ ├── ClientConfig *WebhookClientConfig
│ │ └── ConversionReviewVersions []string
│ └── PreserveUnknownFields *bool
└── Status
├── Conditions []CustomResourceDefinitionCondition
│ ├── Type Established/NamesAccepted/NonStructuralSchema/...
│ ├── Status True/False/Unknown
│ └── Reason/Message
└── AcceptedNames CustomResourceDefinitionNames // 可被 API Approval 修改的名称
三、核心业务逻辑深度解析
3.1 Admission Chain 执行流程(Validate/Mutate 双阶段)
kube-apiserver 的请求处理管线中,Admission 在 Create/Update/Delete/Connect 操作时被触发。完整流程如下:
Mutating Webhook 重调用机制:MutatingAdmissionWebhook 支持重调用策略(ReinvocationPolicy)。当一个 Webhook 的变更可能影响后续 Webhook 的匹配条件时,设置 IfNeeded 策略,系统会在所有 Webhook 执行完毕后重新调用之前被标记为可重调用的 Webhook。从源码 reinvocationcontext.go 可见:
webhookReinvokeContext.previouslyInvokedReinvocableWebhooks:记录已执行且支持重调用的 Webhook UID 集合webhookReinvokeContext.reinvokeWebhooks:需要重调用的 Webhook UID 集合- 当某个 Webhook 修改了对象(
changed=true),则触发RequireReinvokingPreviouslyInvokedPlugins() - 重调用时只调用被标记的 Webhook,不会首次调用之前被跳过的 Webhook
3.2 内置 Admission Plugin 逐个解析
3.2.1 NamespaceLifecycle
源码路径:pkg/admission/plugin/namespace/lifecycle/admission.go
核心职责:阻止在不合法的命名空间中进行操作。
从源码分析,Admit() 方法的执行逻辑如下:
关键设计细节:
- Immortal 命名空间:
default、kube-system、kube-public三个命名空间永远不可删除 - ForceLiveLookupCache:LRU 缓存(容量100),TTL 30s。当命名空间被 Delete 时加入此缓存,后续对该命名空间内对象的操作会跳过本地缓存,直接查询 ETCD,防止缓存的延迟导致在正在终止的命名空间中创建对象
- missingNamespaceWait:50ms 等待。创建对象时如果缓存中找不到命名空间,先等待 50ms 再查一次,为 Informer 的 Watch 事件传播留出时间
- AccessReview 放行:
localsubjectaccessreviews始终放行,避免通过返回 “namespace not found” 泄露命名空间存在性信息
3.2.2 ResourceQuota
核心职责:在 Mutate 阶段计算配额使用量,在 Validate 阶段检查配额是否超限。
执行流程:
- Admit 阶段:枚举请求对象消耗的所有资源类型(CPU/Memory/Storage/Pods/Services 等),通过
ListResourceQuotasByNamespace获取所有匹配的 Quota 对象,计算配额使用量 - Validate 阶段:检查配额使用量是否超出硬性限制,如果超出则返回 Forbidden 错误
- DryRun 处理:DryRun 请求不实际扣减配额
3.2.3 LimitRanger
核心职责:为未设置 limit/request 的 Pod/Container 注入默认值。
执行逻辑:
- 遍历命名空间中的所有 LimitRange 对象
- 对请求对象中的每个 Container,检查其资源声明
- 如果未设置 request,从 LimitRange 的默认值注入
- 如果未设置 limit,从 LimitRange 的默认值注入
- 如果 limit/request 比例不符合 LimitRange 的比例约束,则注入调整后的值
3.2.4 DefaultStorageClass
核心职责:为未指定 StorageClassName 的 PVC 注入默认 StorageClass。
执行逻辑:
- 仅处理 CREATE 操作且对象类型为 PersistentVolumeClaim
- 如果 PVC 已指定 StorageClassName,放行
- 查找集群中被标记为默认的 StorageClass(注解
storageclass.kubernetes.io/is-default-class=true) - 如果找到恰好一个默认 StorageClass,将其名称注入到 PVC 的 StorageClassName 字段
- 如果有多个默认 StorageClass,不注入(避免歧义)
3.2.5 DefaultTolerationSeconds
核心职责:为未设置容忍度的 Pod 自动注入 node.kubernetes.io/not-ready 和 node.kubernetes.io/unreachable 的默认容忍度(300s),确保 Pod 在节点故障时有合理的优雅驱逐时间。
3.2.6 MutatingAdmissionWebhook
详见 3.3 节。
3.2.7 ValidatingAdmissionWebhook
详见 3.4 节。
3.2.8 OwnerReferencesPermissionEnforcement
核心职责:阻止用户创建/修改 OwnerReference 到其没有权限操作的对象类型。
执行逻辑:
- 仅处理 CREATE/UPDATE 操作
- 比较新旧对象的 OwnerReferences 列表
- 对于新增或修改的 OwnerReference,检查用户是否对其
apiVersion/kind和namespace有 GET 权限 - 如果无权限,返回 Forbidden
3.2.9 PodNodeSelector
核心职责:通过 Namespace 的注解 scheduler.alpha.kubernetes.io/default-node-selector 为 Pod 注入默认的 Node Selector,限制 Pod 可调度的节点范围。
3.2.10 Priority(PrioritySort)
核心职责:在创建 Pod 时根据 PriorityClass 设置 Pod 的优先级值,并处理抢占逻辑。在 Admission 阶段主要是注入 Priority 值到 Pod 对象。
3.3 Webhook Admission — Mutating 远程调用机制
从源码分析,MutatingAdmissionWebhook 的架构层次为:
Plugin(实现 MutationInterface)
└── generic.Webhook(基础设施:匹配、分发、客户端管理)
└── mutatingDispatcher(串行调用 + JSON Patch 应用 + 重调用)
核心源码解析:
// plugin.go - Plugin 只做两件事:注册 + Admit 委托
type Plugin struct {
*generic.Webhook
}
func (a *Plugin) Admit(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error {
return a.Webhook.Dispatch(ctx, attr, o) // 完全委托给 generic.Webhook.Dispatch
}
// generic/webhook.go - Dispatch 做三件事
func (a *Webhook) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error {
// 1. 跳过 webhook 配置资源自身的请求(避免递归)
if rules.IsWebhookConfigurationResource(attr) { return nil }
// 2. 等待 Informer 同步
if !a.WaitForReady() { return Forbidden }
// 3. 获取所有 webhook 配置,委托给 dispatcher
hooks := a.hookSource.Webhooks()
return a.dispatcher.Dispatch(ctx, attr, o, hooks)
}
ShouldCallHook 匹配逻辑(决定某个 Webhook 是否应该被调用):
mutatingDispatcher.Dispatch 串行调用流程:
callAttrMutatingHook 的核心逻辑:
- DryRun 检查:如果请求是 DryRun,Webhook 必须声明
SideEffects=None或SideEffects=NoneOnDryRun,否则返回DryRunUnsupportedErr - 创建 AdmissionReview:通过
CreateAdmissionObjects()构造请求对象,包含 UID、Kind、Resource、UserInfo、Object、OldObject、DryRun 等完整信息 - HTTP 调用:通过 RESTClient POST 到 Webhook 端点,支持自定义超时(
TimeoutSeconds) - 验证响应:
VerifyAdmissionResponse(uid, true, response)严格校验:- Response 不能为空
- UID 必须匹配请求 UID
- v1 版本还要校验 GVK
- Mutating 模式:patch 和 patchType 必须同时提供或同时缺失
- 应用 Patch:仅支持
PatchTypeJSONPatchpatchObj, _ := jsonpatch.DecodePatch(result.Patch) objJS, _ := runtime.Encode(jsonSerializer, attr.VersionedObject) patchedJS, _ := patchObj.Apply(objJS) newVersionedObject, _ = jsonSerializer.Decode(patchedJS, nil, newVersionedObject) - 变更检测:使用
apiequality.Semantic.DeepEqual比较变更前后对象,只有真正变化才标记changed=true - 审计标注:记录 Mutation 和 Patch 的审计信息
- Default 应用:对变更后的对象调用
Default()方法
3.4 Webhook Admission — Validating 远程调用机制
从源码分析,ValidatingAdmissionWebhook 的关键区别在于并行调用所有匹配的 Webhook:
// validating/dispatcher.go - 核心并行调度逻辑
func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attributes,
o admission.ObjectInterfaces, hooks []webhook.WebhookAccessor) error {
// 1. 预筛选:收集所有匹配的 hook 及其 VersionedAttributes
var relevantHooks []*generic.WebhookInvocation
versionedAttrs := map[schema.GroupVersionKind]*generic.VersionedAttributes{}
for _, hook := range hooks {
invocation, statusError := d.plugin.ShouldCallHook(hook, attr, o)
if statusError != nil { return statusError }
if invocation == nil { continue }
relevantHooks = append(relevantHooks, invocation)
// 缓存 VersionedAttributes,相同 GVK 只创建一次
if _, ok := versionedAttrs[invocation.Kind]; !ok {
versionedAttr, _ := generic.NewVersionedAttributes(attr, invocation.Kind, o)
versionedAttrs[invocation.Kind] = versionedAttr
}
}
if len(relevantHooks) == 0 { return nil }
// 2. 检查上下文是否已超时
select {
case <-ctx.Done():
return apierrors.NewTimeoutError(...)
default:
}
// 3. 并行调用所有 webhook
wg := sync.WaitGroup{}
errCh := make(chan error, len(relevantHooks))
wg.Add(len(relevantHooks))
for i := range relevantHooks {
go func(invocation *generic.WebhookInvocation) {
defer wg.Done()
// 调用单个 webhook
err := d.callHook(ctx, hook, invocation, versionedAttr)
// ... 错误处理与指标记录 ...
if err != nil { errCh <- err }
}(relevantHooks[i])
}
wg.Wait()
close(errCh)
// 4. 汇总错误
var errs []error
for e := range errCh { errs = append(errs, e) }
if len(errs) == 0 { return nil }
// 目前只返回第一个错误
return errs[0]
}
Validating vs Mutating 的核心差异:
| 维度 | Mutating | Validating |
|---|---|---|
| 调用方式 | 串行(顺序调用,前一个的输出是后一个的输入) | 并行(goroutine 并发调用) |
| 对象修改 | 允许返回 JSON Patch | 不允许返回 Patch/PatchType |
| 响应校验 | VerifyAdmissionResponse(uid, true, ...) |
VerifyAdmissionResponse(uid, false, ...) |
| 重调用 | 支持(ReinvocationPolicy) | 不支持 |
| 版本转换 | 每次调用可能需要转换到不同版本 | 预计算所有版本,缓存复用 |
| 审计标注 | Mutation + Patch 双重标注 | 仅 AuditAnnotations |
| 失败策略 | FailurePolicy=Ignore 时继续下一个 | 同样支持,但并行中各自独立处理 |
Validating Webhook 的并行调用时序图:
3.5 AdmissionReview 请求/响应构造
从源码 request/admissionreview.go 分析,AdmissionReview 的构造支持 v1 和 v1beta1 两个版本:
CreateAdmissionObjects 根据Webhook声明的 admissionReviewVersions 选择版本:
func CreateAdmissionObjects(versionedAttributes *generic.VersionedAttributes,
invocation *generic.WebhookInvocation) (uid types.UID, request, response runtime.Object, err error) {
for _, version := range invocation.Webhook.GetAdmissionReviewVersions() {
switch version {
case admissionv1.SchemeGroupVersion.Version: // "v1"
uid := types.UID(uuid.NewUUID())
request := CreateV1AdmissionReview(uid, versionedAttributes, invocation)
response := &admissionv1.AdmissionReview{}
return uid, request, response, nil
case admissionv1beta1.SchemeGroupVersion.Version: // "v1beta1"
// ...
}
}
return "", nil, nil, fmt.Errorf("webhook does not accept known AdmissionReview versions")
}
v1 AdmissionReview Request 包含的字段:
type AdmissionRequest struct {
UID types.UID // 唯一请求ID
Kind metav1.GroupVersionKind // Webhook 匹配的 GVK
Resource metav1.GroupVersionResource // Webhook 匹配的 GVR
SubResource string // 子资源
RequestKind *metav1.GroupVersionKind // 原始请求的 GVK
RequestResource *metav1.GroupVersionResource // 原始请求的 GVR
RequestSubResource string // 原始子资源
Name string // 对象名
Namespace string // 命名空间
Operation Operation // CREATE/UPDATE/DELETE/CONNECT
UserInfo UserInfo // 请求用户
Object runtime.RawExtension // 新对象(版本化)
OldObject runtime.RawExtension // 旧对象(仅UPDATE)
DryRun *bool // DryRun 标志
Options runtime.RawExtension // 操作选项
}
VerifyAdmissionResponse 响应校验的关键差异:
| 校验项 | v1 Mutating | v1 Validating | v1beta1 |
|---|---|---|---|
| Response 非空 | ✅ | ✅ | ✅ |
| UID 匹配 | ✅ | ✅ | ❌ 不校验 |
| GVK 匹配 | ✅ | ✅ | ❌ 不校验 |
| Patch+PatchType 一致性 | ✅ 同时提供/缺失 | ❌ 都不允许 | ✅ 有Patch时自动Pin为JSONPatch |
| PatchType 为空检查 | ✅ | N/A | ❌ |
3.6 CRD 创建 → Schema 验证 → ETCD 存储完整流程
3.6.1 CRD APIExtensions 架构
从源码 apiserver.go 的 New() 方法可见,CRD 系统的初始化流程为:
3.6.2 CRD 创建流程
当用户通过 kubectl apply 创建一个 CRD 时:
3.6.3 CR 实例的 Schema 验证
从源码 validation/validation.go 分析,CR 的 Schema 验证流程为:
ConvertJSONSchemaPropsWithPostProcess 的核心转换逻辑:
这个函数将 apiextensions.JSONSchemaProps(CRD 内部类型)递归转换为 spec.Schema(kube-openapi 类型),转换过程包括:
- 基础属性映射:type, format, title, description, nullable, default, example, enum
- 数值约束:maximum, minimum, exclusiveMaximum, exclusiveMinimum, multipleOf
- 字符串约束:maxLength, minLength, pattern
- 数组约束:maxItems, minItems, uniqueItems, items
- 对象约束:maxProperties, minProperties, required, properties, additionalProperties, patternProperties
- 组合约束:allOf, oneOf, anyOf, not
- Kubernetes 扩展:
x-kubernetes-int-or-string:XIntOrString=true→ type=[“integer”,“string”]x-kubernetes-preserve-unknown-fieldsx-kubernetes-embedded-resourcex-kubernetes-list-map-keysx-kubernetes-list-typex-kubernetes-map-type
StripUnsupportedFormatsPostProcess 作为后处理步骤,去除 kube-openapi 验证器不支持的 format 值(如 date-time 以外的某些自定义 format),避免验证失败。
ValidateCustomResource 的错误映射:
| OpenAPI 错误码 | Kubernetes 字段错误 |
|---|---|
RequiredFailCode |
field.Required(errPath, "") |
EnumFailCode |
field.NotSupported(errPath, err.Value, values) |
| 其他 Validation 错误 | field.Invalid(errPath, value, err.Error()) |
| 非 Validation 类型 | field.Invalid(fldPath, "", err.Error()) |
3.6.4 CR 的 ETCD 存储
从源码 customresource/etcd.go 分析,CR 的存储架构为:
type CustomResourceStorage struct {
CustomResource *REST // 主资源存储
Status *StatusREST // /status 子资源存储
Scale *ScaleREST // /scale 子资源存储
}
REST 存储的创建:
func newREST(resource schema.GroupResource, kind, listKind schema.GroupVersionKind,
strategy customResourceStrategy, optsGetter generic.RESTOptionsGetter, ...) (*REST, *StatusREST) {
store := &genericregistry.Store{
NewFunc: func() runtime.Object {
ret := &unstructured.Unstructured{}
ret.SetGroupVersionKind(kind) // 设置 GVK 作为版本信号
return ret
},
NewListFunc: func() runtime.Object {
ret := &unstructured.UnstructuredList{}
ret.SetGroupVersionKind(listKind)
return ret
},
CreateStrategy: strategy,
UpdateStrategy: strategy,
DeleteStrategy: strategy,
ResetFieldsStrategy: strategy,
TableConvertor: tableConvertor,
}
// 通过 CompleteWithOptions 完成 ETCD 存储配置
store.CompleteWithOptions(&generic.StoreOptions{RESTOptions: optsGetter})
// Status 子资源共享底层 Store,但使用独立的 UpdateStrategy
statusStore := *store
statusStore.UpdateStrategy = NewStatusStrategy(strategy)
statusStore.ResetFieldsStrategy = statusStrategy
return &REST{store, categories}, &StatusREST{store: &statusStore}
}
关键设计点:
- Unstructured 对象:CR 在存储层使用
unstructured.Unstructured,而非编译时的 Go struct。这使得 CRD 可以在运行时定义新类型 - GVK 信号:
NewFunc创建的对象预设了 GVK,帮助版本化解码器正确识别对象类型 - Strategy 模式:Create/Update/Delete 各自有独立的 Strategy,处理验证、默认值、允许的更新字段等
- Status 子资源:使用共享的 Store 实例,但 Override 了 UpdateStrategy,限制只能修改 status 字段。
forceAllowCreate=false确保不能通过 Update 创建 Status - Scale 子资源:通过 JSON Path 从 Unstructured 对象中提取
specReplicas/statusReplicas/labelSelector字段,映射为autoscaling/v1.Scale类型
Scale 子资源的数据提取:
func scaleFromCustomResource(cr *unstructured.Unstructured, specReplicasPath, statusReplicasPath,
labelSelectorPath string) (*autoscalingv1.Scale, bool, error) {
specReplicas, foundSpecReplicas, _ := unstructured.NestedInt64(cr.UnstructuredContent(),
strings.Split(specReplicasPath, ".")...)
statusReplicas, _, _ := unstructured.NestedInt64(cr.UnstructuredContent(),
strings.Split(statusReplicasPath, ".")...)
// labelSelectorPath 可选
scale := &autoscalingv1.Scale{
ObjectMeta: metav1.ObjectMeta{...}, // 从 CR 的 metadata 复制
Spec: ScaleSpec{Replicas: int32(specReplicas)},
Status: ScaleStatus{Replicas: int32(statusReplicas), Selector: labelSelector},
}
return scale, foundSpecReplicas, nil
}
shallowCopyObjectMeta:CR 的 List/Get 操作返回对象时会浅拷贝 ObjectMeta,因为 generic store 会设置 self-link,避免修改缓存中的原始对象。
3.7 CRD 版本转换(Convert)机制
从源码 conversion/converter.go 和 conversion/webhook_converter.go 分析,CRD 的版本转换体系如下:
3.7.1 转换器工厂
type CRConverterFactory struct {
webhookConverterFactory *webhookConverterFactory
}
func (m *CRConverterFactory) NewConverter(crd *apiextensionsv1.CustomResourceDefinition)
(safe, unsafe runtime.ObjectConvertor, err error) {
switch crd.Spec.Conversion.Strategy {
case apiextensionsv1.NoneConverter:
converter = &nopConverter{} // 无转换
case apiextensionsv1.WebhookConverter:
converter, err = m.webhookConverterFactory.NewWebhookConverter(crd)
}
unsafe = &crConverter{
convertScale: convertScale, // 是否需要转换 Scale 子资源
validVersions: validVersions, // CRD 声明的合法版本集合
clusterScoped: crd.Spec.Scope == ClusterScoped,
converter: converter, // 底层转换器
}
return &safeConverterWrapper{unsafe}, unsafe, nil
}
3.7.2 crConverter 的 Convert 逻辑
crConverter 封装了通用的 CR 转换行为:
- 如果输入对象已经是目标 GVK 版本,无需转换
- 检查目标 GVK 是否在 CRD 的合法版本列表中
- 如果是 Scale 类型,使用 Scale 转换路径
- 调用底层 converter(nopConverter 或 webhookConverter)
- 转换完成后设置目标的 apiVersion/kind
3.7.3 Webhook 转换器
Webhook 转换的核心验证步骤(v1 版本):
- GVK 校验:
response.GroupVersionKind() != v1GVK→ 错误 - UID 校验:
response.Response.UID != expectedUID→ 错误 - 状态校验:
response.Response.Result.Status != "Success"→ 错误 - 数量校验:
len(convertedObjects) != len(objectsToConvert)→ 错误 - GVK 匹配:每个转换后的对象必须为目标 GroupVersion 和原始 Kind
- ObjectMeta 一致性:
validateConvertedObject检查 name/namespace/uid 必须匹配 - 元数据恢复:
restoreObjectMeta深拷贝原始 metadata,但保留 Webhook 返回的 labels/annotations
ConversionReview 数据结构:
// Request
type ConversionRequest struct {
UID types.UID // 请求唯一ID
Objects []runtime.RawExtension // 待转换对象列表
DesiredAPIVersion string // 目标 apiVersion(如 "example.com/v1")
}
// Response
type ConversionResponse struct {
UID types.UID // 必须匹配 Request.UID
ConvertedObjects []runtime.RawExtension // 转换后的对象列表
Result metav1.Status // 必须为 StatusSuccess
}
性能优化:Webhook 转换器的 trace 阈值动态计算:50ms + 8ms × objCount,基于 SLO(~4ms/对象 + 序列化开销 ~4ms/对象)。
3.8 CustomResource OpenAPI 发布
从源码 apiserver.go 中的 OpenAPI 控制器注册可见:
openapiController := openapicontroller.NewController(
s.Informers.Apiextensions().V1().CustomResourceDefinitions())
// 在 PostStartHook 中运行
if s.GenericAPIServer.OpenAPIVersionedService != nil &&
s.GenericAPIServer.StaticOpenAPISpec != nil {
go openapiController.Run(
s.GenericAPIServer.StaticOpenAPISpec, // 静态 OpenAPI Spec 的更新接口
s.GenericAPIServer.OpenAPIVersionedService, // /openapi/v2 端点服务
context.StopCh)
}
OpenAPI 发布流程:
NonStructuralSchema 控制器:当 CRD 的 Schema 不满足结构化要求时(如使用 x-kubernetes-preserve-unknown-fields 但未在顶层设置 type: object),该控制器会在 CRD Status 中设置 NonStructuralSchema Condition 为 False,提示用户修复 Schema。
四、Mermaid 图集合
图1:Admission Chain 整体架构图
图2:Admission 双阶段执行图
图3:NamespaceLifecycle 过滤流程图
图4:MutatingAdmissionWebhook 时序图
图5:ValidatingAdmissionWebhook 时序图
图6:CRD APIExtensions 架构图
图7:CRD 创建流程图
图8:CRD Schema 验证图
图9:CRD ETCD 存储图
图10:CRD 版本转换图
图11:OpenAPI 发布流程图
图12:Admission Plugin 注册图
图13:Webhook 配置图
图14:CRD Status 子资源图
图15:Admission 拒绝流程图
五、总结
5.1 Admission Control 设计哲学
- 关注点分离:Mutation 和 Validation 严格分阶段执行,避免交叉依赖
- 插件化:所有准入逻辑通过插件接口实现,可独立启用/禁用
- 渐进增强:内置插件处理核心策略,Webhook 提供用户可扩展的动态准入能力
- 串行 Mutate vs 并行 Validate:Mutate 因为有数据依赖(前一个的输出是后一个的输入)必须串行;Validate 因为无副作用可以并行提升性能
- 重调用保障:Mutating Webhook 的 ReinvocationPolicy 解决了多个 Webhook 之间的交互问题
- DryRun 安全:DryRun 请求跳过有副作用的操作,Webhook 必须声明 SideEffects=None
5.2 CRD 设计哲学
- Unstructured 优先:CR 不编译为 Go struct,使用 Unstructured 保持运行时灵活性
- 声明式 Schema:通过 OpenAPI v3 JSONSchema 声明 CR 结构,支持自动验证和 OpenAPI 发布
- 存储版本唯一性:ETCD 只存储一个版本(storage: true),其他版本通过 Convert 转换
- Webhook 转换解耦:版本间转换逻辑委托给外部 Webhook,CRD 本身只声明策略
- 子资源复用:Status 和 Scale 子资源共享底层 Store 实例,通过 Strategy Override 限制可修改字段
- 控制器协同:Establishing、Discovery、Naming、NonStructuralSchema、APIApproval、Finalizer、OpenAPI 七个控制器协同确保 CRD 的完整生命周期管理
5.3 关键交互点
Admission Control 与 CRD 的关键交互点在于:
- CR 实例经过完整 Admission Chain:所有 CR 的 Create/Update/Delete 操作都会经过内置插件和动态 Webhook 的变更与验证
- CRD 自身也是 API 资源:CRD 的创建/更新也经过 Admission Chain,可以被 Webhook 拦截和修改
- Webhook 的 CRD 匹配:Webhook 的 Rules 可以匹配 CRD 定义的 GVR,等效资源匹配(MatchPolicy=Equivalent)支持跨版本匹配
- CRD Webhook 的服务发现:Admission Webhook 和 CRD Conversion Webhook 共享 ServiceResolver 和 AuthResolverWrapper 基础设施
- 版本转换与准入的时序:Admission 操作基于请求版本的 Schema 进行验证,版本转换发生在存储层和读取层
本文基于 Kubernetes 源码(staging/src/k8s.io/apiserver、staging/src/k8s.io/apiextensions-apiserver)深度分析,覆盖了 Admission Control 的接口体系、双阶段执行、内置插件、Webhook 远程调用机制,以及 CRD 的创建流程、Schema 验证、ETCD 存储、版本转换和 OpenAPI 发布等核心业务逻辑。
更多推荐


所有评论(0)