27|Kubernetes如何进行请求参数校验?

你好,我是孔令飞。

绝大部分企业应用,都是通过 API 接口的形式对外提供功能,API 接口的通信协议通常是 HTTP 或者 RPC。使用 RPC 作为通信协议的 API 接口又称作 RPC 接口,使用 HTTP 作为通信协议的 API 接口又称作 HTTP 接口。不管哪种类型的 API 接口,在请求到来时,都要进行请求参数校验。

Kubernetes 中有大量的 RESTful API 接口,在请求这些接口的时候,同样要进行请求参数校验。

本节课,我们就来看下 Kuberentes 是如何进行请求参数校验的(请求参数,其实就是资源定义,所以也叫资源定义校验)。希望通过本节课的学习,你可以掌握 Kubernetes 请求参数校验的实现方式。更重要的是,通过学习 Kubernetes 请求参数校验的设计和实现,你可以掌握优秀开源项目的参数校验设计和实现,并迁移到未来的业务开发中。

Kubernetes 参数校验流程

kube-apiserver 真正开始处理 HTTP 请求的函数是 createHandler 函数,跟验证相关的逻辑如下:

func createHandler(r rest.NamedCreater, scope *RequestScope, admit admission.Interface, includeName bool) http.HandlerFunc {
    return func(w http.ResponseWriter, req *http.Request) {
        ctx := req.Context()
        // For performance tracking purposes.
        ...
        namespace, name, err := scope.Namer.Name(req)
        ...
        options := &metav1.CreateOptions{}
        values := req.URL.Query()
        // 将 HTTP 请求的 Query 参数解析到 metav1.CreateOptions 类型的变量 options 中
        if err := metainternalversionscheme.ParameterCodec.DecodeParameters(values, scope.MetaGroupVersion, options); err != nil {
            ...
            return
        }
        // 验证创建资源的CreateOptions是否合法
        if errs := validation.ValidateCreateOptions(options); len(errs) > 0 {
            ...
            return
        }

        // WithAudit 封装了 admit,做了更多的逻辑处理,例如记录Annotations
        admit = admission.WithAudit(admit)
        audit.LogRequestObject(req.Context(), obj, objGV, scope.Resource, scope.Subresource, scope.Serializer)

        if objectMeta, err := meta.Accessor(obj); err == nil {
            // 这里会校验资源定义中的 namespace 和请求的 namespace 是一致的
            if err := rest.EnsureObjectNamespaceMatchesRequestNamespace(rest.ExpectedNamespaceForResource(namespace, scope.Resource), objectMeta); err != nil {
                scope.err(err, w, req)
                return
            }
        }

        requestFunc := func() (runtime.Object, error) {
            return r.Create(
                ctx,
                name,
                obj,
                rest.AdmissionToValidateObjectFunc(admit, admissionAttributes, scope),
                options,
            )
        }
        ...
        result, err := finisher.FinishRequest(ctx, func() (runtime.Object, error) {
            ...
            // 用来校验ManagedFields,其实是在mutation webhook中校验的
            admit = fieldmanager.NewManagedFieldsValidatingAdmissionController(admit)

            // 执行mutation webhook
            if mutatingAdmission, ok := admit.(admission.MutationInterface); ok && mutatingAdmission.Handles(admission.Create) {
                if err := mutatingAdmission.Admit(ctx, admissionAttributes, scope); err != nil {
                    return nil, err
                }
            }
            ...
            // 请求流程转到 r.Create 方法中
            result, err := requestFunc()
            ...
            return result, err
        })
        ...
    }
}

createHandler 函数有 4 个入参:

  • r rest.NamedCreater:这个其实就是 Deployment 资源的存储层实现 RESTREST 结构体中的 CreateStrategy 字段包含了 Validate 方法,用来对资源定义进行参数校验。

  • scope *RequestScopeRequestScope 封装了所有 RESTful 处理方法中的常见字段。

  • admit admission.Interface:Kubernetes Admission Controller 链,里面包含了多个 Admission Webhook 插件。在执行时,会按初始化时的先后顺序串行执行这些插件。admit 准入控制链是在 kube-apiserver 启动时被初始化的,具体代码如下:

func (a *AdmissionOptions) ApplyTo(
    c *server.Config,
    informers informers.SharedInformerFactory,
    kubeClient kubernetes.Interface,
    dynamicClient dynamic.Interface,
    features featuregate.FeatureGate,
    pluginInitializers ...admission.PluginInitializer,
) error {
    ...
    admissionChain, err := a.Plugins.NewFromPlugins(pluginNames, pluginsConfigProvider, initializersChain, a.Decorators)
    if err != nil {
        return err
    }
    ...
}
  • includeName boolinclusterName 用来判断请求是否是指定 Name 来请求的。如果是指定 Name,Request 中又没有 Name,则会报错返回。如果不是指定 Name 来请求,则会从资源的 MetaData 中获取 Name。指定 Name 和不指定 Name 请求的接口区别如下:

    • 指定名字:Create(ctx context.Context, name string, obj runtime.Object, createValidation ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error);

    • 不指定名字:Create(ctx context.Context, obj runtime.Object, createValidation ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error)

    Kubernetes 绝大部分资源请求接口都是不指定 Name 来请求的。

createHandler 方法中,会使用方法入参完成整个 HTTP 请求链路,包括参数校验。下面,我们来详细看下。

CreateOptions 校验

createHandler 方法中,通过以下代码来完成 CreateOptions 的校验(代码点为create.go#L108)。

        options := &metav1.CreateOptions{}
        values := req.URL.Query()
        if err := metainternalversionscheme.ParameterCodec.DecodeParameters(values, scope.MetaGroupVersion, options); err != nil {
            err = errors.NewBadRequest(err.Error())
            scope.err(err, w, req)
            return
        }
        if errs := validation.ValidateCreateOptions(options); len(errs) > 0 {
            err := errors.NewInvalid(schema.GroupKind{Group: metav1.GroupName, Kind: "CreateOptions"}, "", errs)
            scope.err(err, w, req)
            return
        }

上述代码,首先将 HTTP 请求的 Query 参数解析到 metav1.CreateOptions 类型的变量 options。之后调用 validation.ValidateCreateOptions 函数,对 options 变量进行参数校验。

validation.ValidateCreateOptions 代码实现如下:

func ValidateCreateOptions(options *metav1.CreateOptions) field.ErrorList {
    allErrs := field.ErrorList{}
    allErrs = append(allErrs, ValidateFieldManager(options.FieldManager, field.NewPath("fieldManager"))...)
    allErrs = append(allErrs, ValidateDryRun(field.NewPath("dryRun"), options.DryRun)...)
    allErrs = append(allErrs, ValidateFieldValidation(field.NewPath("fieldValidation"), options.FieldValidation)...)
    return allErrs
}    

ValidateCreateOptions 分别校验了 metav1.CreateOptions 结构体中的 fieldManagerdryRunfieldValidation 字段。

k8s.io/apimachinery/pkg/apis/meta/v1/validation 包封装了多个通用的资源校验函数,这些函数被 kube-apiserver 代码大量用于各类资源的校验逻辑中。在我们的 Kubernetes 编程开发中,也可以复用 validation 包提供的校验方法,OneX 项目就使用了该包中的大量校验函数。这些校验方法列表如下:

LabelSelectorHasInvalidLabelValue(ps *metav1.LabelSelector) bool
ValidateLabelSelector(ps *metav1.LabelSelector, opts LabelSelectorValidationOptions, fldPath *field.Path) field.ErrorList
ValidateLabelSelectorRequirement(sr metav1.LabelSelectorRequirement, opts LabelSelectorValidationOptions, fldPath *field.Path) field.ErrorList
ValidateLabelName(labelName string, fldPath *field.Path) field.ErrorList
ValidateLabels(labels map[string]string, fldPath *field.Path) field.ErrorList
ValidateDeleteOptions(options *metav1.DeleteOptions) field.ErrorList
ValidateCreateOptions(options *metav1.CreateOptions) field.ErrorList
ValidateUpdateOptions(options *metav1.UpdateOptions) field.ErrorList
ValidatePatchOptions(options *metav1.PatchOptions, patchType types.PatchType) field.ErrorList
ValidateFieldManager(fieldManager string, fldPath *field.Path) field.ErrorList
ValidateDryRun(fldPath *field.Path, dryRun []string) field.ErrorList
ValidateFieldValidation(fldPath *field.Path, fieldValidation string) field.ErrorList
ValidateTableOptions(opts *metav1.TableOptions) field.ErrorList
ValidateManagedFields(fieldsList []metav1.ManagedFieldsEntry, fldPath *field.Path) field.ErrorList
ValidateConditions(conditions []metav1.Condition, fldPath *field.Path) field.ErrorList
ValidateCondition(condition metav1.Condition, fldPath *field.Path) field.ErrorList

ManagedFieldsValidating

createHandler 中,在校验完 CreateOptions 之后,会继续处理 HTTP 请求。当执行到以下代码段时,会对资源 ObjectMeta 字段中的 ManagedFields 字段进行校验(代码点为create.go#L199)。

            admit = fieldmanager.NewManagedFieldsValidatingAdmissionController(admit)
        
            if mutatingAdmission, ok := admit.(admission.MutationInterface); ok && mutatingAdmission.Handles(admission.Create) {
                if err := mutatingAdmission.Admit(ctx, admissionAttributes, scope); err != nil {
                    return nil, err
                }
            }

通过阅读上述代码,我们可以知道在校验资源定义中的 ObjectMeta.ManagedFields 字段时,其实是走的 Mutating Webhook,这是因为 Kubernetes 需要在 ObjectMeta.ManagedFields 校验失败时,给 ObjectMeta.ManagedFields 设置默认值。

Validating Webhook

createHandler 方法中,HTTP 处理流程继续前行,通过调用 r.Create 方法,HTTP 请求流程会进入到存储层的 create 方法中。

// 位于文件 staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go 中
func createHandler(r rest.NamedCreater, scope *RequestScope, admit admission.Interface, includeName bool) http.HandlerFunc {
    ...
        requestFunc := func() (runtime.Object, error) {
            return r.Create(
                ctx,
                name,
                obj,
                rest.AdmissionToValidateObjectFunc(admit, admissionAttributes, scope),
                options,
            )   
        }   
        result, err := finisher.FinishRequest(ctx, func() (runtime.Object, error) {
            ...
            result, err := requestFunc()
            ...
            }
            return result, err
        })
        ...
}

// 位于文件 staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go 中
func (e *Store) create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
    ...
    // 执行创建前处理,执行与创建相关的前置验证或处理。
    if err := rest.BeforeCreate(e.CreateStrategy, ctx, obj); err != nil {
        return nil, err
    }
    // at this point we have a fully formed object.  It is time to call the validators that the apiserver
    // handling chain wants to enforce.
    // 资源参数校验
    if createValidation != nil {
        if err := createValidation(ctx, obj.DeepCopyObject()); err != nil {
            return nil, err
        }
    }
    ...
}

// 位于文件 staging/src/k8s.io/apiserver/pkg/registry/rest/create.go 中
func BeforeCreate(strategy RESTCreateStrategy, ctx context.Context, obj runtime.Object) error {
    ...
    if errs := strategy.Validate(ctx, obj); len(errs) > 0 {
        return errors.NewInvalid(kind.GroupKind(), objectMeta.GetName(), errs)
    }
    ...
    if errs := genericvalidation.ValidateObjectMetaAccessor(objectMeta, strategy.NamespaceScoped(), path.ValidatePathSegmentName, field.NewPath("metadata")); len(errs) > 0 {
        return errors.NewInvalid(kind.GroupKind(), objectMeta.GetName(), errs)
    }
    ...
}

rest.BeforeCreate 函数中,会先调用 strategy.Validate 方法来进行资源校验,strategy.Validate 方法实现如下:

// 位于文件 pkg/registry/apps/deployment/strategy.go 中
func (deploymentStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
    deployment := obj.(*apps.Deployment)
    opts := pod.GetValidationOptionsFromPodTemplate(&deployment.Spec.Template, nil)
    return appsvalidation.ValidateDeployment(deployment, opts)
}

可以看到,deploymentStrategyValidate 方法会调用 k8s.io/kubernetes/pkg/apis/apps/validation 包中的 ValidateXXX 方法来校验资源的参数是否合法。基于此,请求流程,我们分别可以在 deploymentStrategyValidate 方法中和 appsvalidation.ValidateDeployment 方法中添加自定义资源参数校验逻辑。在添加校验逻辑的时候,你可以参考现有的校验代码实现,编写你自己的资源校验逻辑。

rest.BeforeCreate 函数中接着会调用 ValidateObjectMetaAccessor 函数,来校验资源的 ObjectMeta 字段。

rest.BeforeCreate 方法执行成功后,如果 createValidation 不为 nil 则会调用 createValidation 方法,createValidation 方法实现为 AdmissionToValidateObjectFunc

// AdmissionToValidateObjectFunc converts validating admission to a rest validate object func
func AdmissionToValidateObjectFunc(admit admission.Interface, staticAttributes admission.Attributes, o admission.ObjectInterfaces) ValidateObjectFunc {
    validatingAdmission, ok := admit.(admission.ValidationInterface)
    if !ok {    
        return func(ctx context.Context, obj runtime.Object) error { return nil }
    }           
    return func(ctx context.Context, obj runtime.Object) error {
        name := staticAttributes.GetName()
        // in case the generated name is populated
        if len(name) == 0 {
            if metadata, err := meta.Accessor(obj); err == nil {
                name = metadata.GetName()
            }
        }   
            
        finalAttributes := admission.NewAttributesRecord(
            obj,
            staticAttributes.GetOldObject(),
            staticAttributes.GetKind(),
            staticAttributes.GetNamespace(),
            name,
            staticAttributes.GetResource(),
            staticAttributes.GetSubresource(),
            staticAttributes.GetOperation(),
            staticAttributes.GetOperationOptions(),
            staticAttributes.IsDryRun(),
            staticAttributes.GetUserInfo(),
        )
        if !validatingAdmission.Handles(finalAttributes.GetOperation()) {
            return nil
        }
        return validatingAdmission.Validate(ctx, finalAttributes, o)
    }
}

可以看到,其实 createValidation 用来执行 Validating Webhook 列表。也就是说,如果 kube-apiserver 在启动时,加载了 Validating Webhook,则会执行 Validating Webhook 链。在执行之前会调用 validatingAdmission.Handles 方法来判断,是否需要对请求资源执行 Validate 逻辑,如果需要,则对资源进行校验。

课程总结

通过本节课的学习,你应该已经了解了 Kubernetes 中资源校验的流程、校验点和具体的实现方式。这里我用一张图来带着你一起回顾下。

下图是 Kubernetes 中创建资源的资源定义校验逻辑:

图片

当 POST 类型的 HTTP 请求到来时,通过路由匹配将请求转到到路由函数中,路由函数的核心逻辑从 createHandler 方法开始。之后 HTTP 的校验流程如下(按校验顺序):

  1. 校验 CreateOptions:校验请求 Query 参数是否合法。

  2. 校验资源的 ObjectMeta.ManagedFields 是否合法,这个校验逻辑是在 Mutating Webhook 中校验的。

  3. 资源校验策略:在资源策略结构体(例如deploymentStrategy)的 Validate 方法中,可以执行你的自定义校验逻辑,并调用 k8s.io/kubernetes/pkg/apis/xxx/validation 包中的自定义校验逻辑。

  4. 校验资源的 ObjectMeta 字段是否合法。

  5. 执行 Validating Webhook 校验资源。

上图中,绿色部分是我们在 Kubernetes 开发中经常需要开发或修改的地方。

希望通过本节课的学习,你能够反哺业务,用更好的方式实现业务请求参数校验。

课后练习

  1. 请你通过 AdmissionOptionsApplyTo 方法中的 NewFromPlugins 调用,查看准入控制链具体是如何被初始化的。

  2. 阅读 kube-apiserver 代码,学习更新资源的校验流程和实现。

欢迎你在留言区与我交流讨论,如果今天的内容让你有所收获,也欢迎转发给有需要的朋友,我们下节课再见!

精选留言