mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-07 11:13:48 +00:00
refactor: add ValidateCustomResourceUpdate to support future validators for CRD Updates
This commit is contained in:
parent
4da418aba7
commit
9ee6d97fc0
@ -79,8 +79,6 @@ import (
|
|||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
"k8s.io/kube-openapi/pkg/spec3"
|
"k8s.io/kube-openapi/pkg/spec3"
|
||||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||||
"k8s.io/kube-openapi/pkg/validation/strfmt"
|
|
||||||
"k8s.io/kube-openapi/pkg/validation/validate"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// crdHandler serves the `/apis` endpoint.
|
// crdHandler serves the `/apis` endpoint.
|
||||||
@ -740,8 +738,9 @@ func (r *crdHandler) getOrCreateServingInfoFor(uid types.UID, name string) (*crd
|
|||||||
return nil, fmt.Errorf("the server could not properly serve the CR schema")
|
return nil, fmt.Errorf("the server could not properly serve the CR schema")
|
||||||
}
|
}
|
||||||
var internalSchemaProps *apiextensionsinternal.JSONSchemaProps
|
var internalSchemaProps *apiextensionsinternal.JSONSchemaProps
|
||||||
|
var internalValidationSchema *apiextensionsinternal.CustomResourceValidation
|
||||||
if validationSchema != nil {
|
if validationSchema != nil {
|
||||||
internalValidationSchema := &apiextensionsinternal.CustomResourceValidation{}
|
internalValidationSchema = &apiextensionsinternal.CustomResourceValidation{}
|
||||||
if err := apiextensionsv1.Convert_v1_CustomResourceValidation_To_apiextensions_CustomResourceValidation(validationSchema, internalValidationSchema, nil); err != nil {
|
if err := apiextensionsv1.Convert_v1_CustomResourceValidation_To_apiextensions_CustomResourceValidation(validationSchema, internalValidationSchema, nil); err != nil {
|
||||||
return nil, fmt.Errorf("failed to convert CRD validation to internal version: %v", err)
|
return nil, fmt.Errorf("failed to convert CRD validation to internal version: %v", err)
|
||||||
}
|
}
|
||||||
@ -753,7 +752,7 @@ func (r *crdHandler) getOrCreateServingInfoFor(uid types.UID, name string) (*crd
|
|||||||
}
|
}
|
||||||
|
|
||||||
var statusSpec *apiextensionsinternal.CustomResourceSubresourceStatus
|
var statusSpec *apiextensionsinternal.CustomResourceSubresourceStatus
|
||||||
var statusValidator *validate.SchemaValidator
|
var statusValidator apiservervalidation.SchemaValidator
|
||||||
subresources, err := apiextensionshelpers.GetSubresourcesForVersion(crd, v.Name)
|
subresources, err := apiextensionshelpers.GetSubresourcesForVersion(crd, v.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utilruntime.HandleError(err)
|
utilruntime.HandleError(err)
|
||||||
@ -768,11 +767,10 @@ func (r *crdHandler) getOrCreateServingInfoFor(uid types.UID, name string) (*crd
|
|||||||
// for the status subresource, validate only against the status schema
|
// for the status subresource, validate only against the status schema
|
||||||
if internalValidationSchema != nil && internalValidationSchema.OpenAPIV3Schema != nil && internalValidationSchema.OpenAPIV3Schema.Properties != nil {
|
if internalValidationSchema != nil && internalValidationSchema.OpenAPIV3Schema != nil && internalValidationSchema.OpenAPIV3Schema.Properties != nil {
|
||||||
if statusSchema, ok := internalValidationSchema.OpenAPIV3Schema.Properties["status"]; ok {
|
if statusSchema, ok := internalValidationSchema.OpenAPIV3Schema.Properties["status"]; ok {
|
||||||
openapiSchema := &spec.Schema{}
|
statusValidator, _, err = apiservervalidation.NewSchemaValidator(&statusSchema)
|
||||||
if err := apiservervalidation.ConvertJSONSchemaPropsWithPostProcess(&statusSchema, openapiSchema, apiservervalidation.StripUnsupportedFormatsPostProcess); err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
statusValidator = validate.NewSchemaValidator(openapiSchema, nil, "", strfmt.Default)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,8 +29,34 @@ import (
|
|||||||
"k8s.io/kube-openapi/pkg/validation/validate"
|
"k8s.io/kube-openapi/pkg/validation/validate"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type SchemaValidator interface {
|
||||||
|
SchemaCreateValidator
|
||||||
|
ValidateUpdate(new, old interface{}) *validate.Result
|
||||||
|
}
|
||||||
|
|
||||||
|
type SchemaCreateValidator interface {
|
||||||
|
Validate(value interface{}) *validate.Result
|
||||||
|
}
|
||||||
|
|
||||||
|
// basicSchemaValidator wraps a kube-openapi SchemaCreateValidator to
|
||||||
|
// support ValidateUpdate. It implements ValidateUpdate by simply validating
|
||||||
|
// the new value via kube-openapi, ignoring the old value.
|
||||||
|
type basicSchemaValidator struct {
|
||||||
|
*validate.SchemaValidator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s basicSchemaValidator) ValidateUpdate(new, old interface{}) *validate.Result {
|
||||||
|
return s.Validate(new)
|
||||||
|
}
|
||||||
|
|
||||||
// NewSchemaValidator creates an openapi schema validator for the given CRD validation.
|
// NewSchemaValidator creates an openapi schema validator for the given CRD validation.
|
||||||
func NewSchemaValidator(customResourceValidation *apiextensions.JSONSchemaProps) (*validate.SchemaValidator, *spec.Schema, error) {
|
//
|
||||||
|
// If feature `CRDValidationRatcheting` is disabled, this returns validator which
|
||||||
|
// validates all `Update`s and `Create`s as a `Create` - without considering old value.
|
||||||
|
//
|
||||||
|
// If feature `CRDValidationRatcheting` is enabled - the validator returned
|
||||||
|
// will support ratcheting unchanged correlatable fields across an update.
|
||||||
|
func NewSchemaValidator(customResourceValidation *apiextensions.JSONSchemaProps) (SchemaValidator, *spec.Schema, error) {
|
||||||
// Convert CRD schema to openapi schema
|
// Convert CRD schema to openapi schema
|
||||||
openapiSchema := &spec.Schema{}
|
openapiSchema := &spec.Schema{}
|
||||||
if customResourceValidation != nil {
|
if customResourceValidation != nil {
|
||||||
@ -39,12 +65,35 @@ func NewSchemaValidator(customResourceValidation *apiextensions.JSONSchemaProps)
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return validate.NewSchemaValidator(openapiSchema, nil, "", strfmt.Default), openapiSchema, nil
|
|
||||||
|
return basicSchemaValidator{validate.NewSchemaValidator(openapiSchema, nil, "", strfmt.Default)}, openapiSchema, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateCustomResourceUpdate validates the transition of Custom Resource from
|
||||||
|
// `old` to `new` against the schema in the CustomResourceDefinition.
|
||||||
|
// Both customResource and old represent a JSON data structures.
|
||||||
|
//
|
||||||
|
// If feature `CRDValidationRatcheting` is disabled, this behaves identically to
|
||||||
|
// ValidateCustomResource(customResource).
|
||||||
|
func ValidateCustomResourceUpdate(fldPath *field.Path, customResource, old interface{}, validator SchemaValidator) field.ErrorList {
|
||||||
|
// Additional feature gate check for sanity
|
||||||
|
if !utilfeature.DefaultFeatureGate.Enabled(features.CRDValidationRatcheting) {
|
||||||
|
return ValidateCustomResource(nil, customResource, validator)
|
||||||
|
} else if validator == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := validator.ValidateUpdate(customResource, old)
|
||||||
|
if result.IsValid() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return kubeOpenAPIResultToFieldErrors(fldPath, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateCustomResource validates the Custom Resource against the schema in the CustomResourceDefinition.
|
// ValidateCustomResource validates the Custom Resource against the schema in the CustomResourceDefinition.
|
||||||
// CustomResource is a JSON data structure.
|
// CustomResource is a JSON data structure.
|
||||||
func ValidateCustomResource(fldPath *field.Path, customResource interface{}, validator *validate.SchemaValidator) field.ErrorList {
|
func ValidateCustomResource(fldPath *field.Path, customResource interface{}, validator SchemaCreateValidator) field.ErrorList {
|
||||||
if validator == nil {
|
if validator == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -53,6 +102,11 @@ func ValidateCustomResource(fldPath *field.Path, customResource interface{}, val
|
|||||||
if result.IsValid() {
|
if result.IsValid() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return kubeOpenAPIResultToFieldErrors(fldPath, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func kubeOpenAPIResultToFieldErrors(fldPath *field.Path, result *validate.Result) field.ErrorList {
|
||||||
var allErrs field.ErrorList
|
var allErrs field.ErrorList
|
||||||
for _, err := range result.Errors {
|
for _, err := range result.Errors {
|
||||||
switch err := err.(type) {
|
switch err := err.(type) {
|
||||||
|
@ -19,13 +19,12 @@ package customresource
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"k8s.io/kube-openapi/pkg/validation/validate"
|
|
||||||
|
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||||
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel"
|
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel"
|
||||||
structurallisttype "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/listtype"
|
structurallisttype "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/listtype"
|
||||||
schemaobjectmeta "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/objectmeta"
|
schemaobjectmeta "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/objectmeta"
|
||||||
|
"k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
|
||||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
@ -58,7 +57,7 @@ type customResourceStrategy struct {
|
|||||||
kind schema.GroupVersionKind
|
kind schema.GroupVersionKind
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStrategy(typer runtime.ObjectTyper, namespaceScoped bool, kind schema.GroupVersionKind, schemaValidator, statusSchemaValidator *validate.SchemaValidator, structuralSchemas map[string]*structuralschema.Structural, status *apiextensions.CustomResourceSubresourceStatus, scale *apiextensions.CustomResourceSubresourceScale) customResourceStrategy {
|
func NewStrategy(typer runtime.ObjectTyper, namespaceScoped bool, kind schema.GroupVersionKind, schemaValidator, statusSchemaValidator validation.SchemaValidator, structuralSchemas map[string]*structuralschema.Structural, status *apiextensions.CustomResourceSubresourceStatus, scale *apiextensions.CustomResourceSubresourceScale) customResourceStrategy {
|
||||||
celValidators := map[string]*cel.Validator{}
|
celValidators := map[string]*cel.Validator{}
|
||||||
if utilfeature.DefaultFeatureGate.Enabled(features.CustomResourceValidationExpressions) {
|
if utilfeature.DefaultFeatureGate.Enabled(features.CustomResourceValidationExpressions) {
|
||||||
for name, s := range structuralSchemas {
|
for name, s := range structuralSchemas {
|
||||||
|
@ -28,7 +28,6 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
"k8s.io/kube-openapi/pkg/validation/validate"
|
|
||||||
|
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||||
apiextensionsvalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
|
apiextensionsvalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
|
||||||
@ -37,8 +36,8 @@ import (
|
|||||||
type customResourceValidator struct {
|
type customResourceValidator struct {
|
||||||
namespaceScoped bool
|
namespaceScoped bool
|
||||||
kind schema.GroupVersionKind
|
kind schema.GroupVersionKind
|
||||||
schemaValidator *validate.SchemaValidator
|
schemaValidator apiextensionsvalidation.SchemaValidator
|
||||||
statusSchemaValidator *validate.SchemaValidator
|
statusSchemaValidator apiextensionsvalidation.SchemaValidator
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a customResourceValidator) Validate(ctx context.Context, obj runtime.Object, scale *apiextensions.CustomResourceSubresourceScale) field.ErrorList {
|
func (a customResourceValidator) Validate(ctx context.Context, obj runtime.Object, scale *apiextensions.CustomResourceSubresourceScale) field.ErrorList {
|
||||||
@ -70,6 +69,10 @@ func (a customResourceValidator) ValidateUpdate(ctx context.Context, obj, old ru
|
|||||||
if !ok {
|
if !ok {
|
||||||
return field.ErrorList{field.Invalid(field.NewPath(""), u, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", u))}
|
return field.ErrorList{field.Invalid(field.NewPath(""), u, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", u))}
|
||||||
}
|
}
|
||||||
|
oldU, ok := old.(*unstructured.Unstructured)
|
||||||
|
if !ok {
|
||||||
|
return field.ErrorList{field.Invalid(field.NewPath(""), old, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", u))}
|
||||||
|
}
|
||||||
objAccessor, err := meta.Accessor(obj)
|
objAccessor, err := meta.Accessor(obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
|
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
|
||||||
@ -86,7 +89,7 @@ func (a customResourceValidator) ValidateUpdate(ctx context.Context, obj, old ru
|
|||||||
var allErrs field.ErrorList
|
var allErrs field.ErrorList
|
||||||
|
|
||||||
allErrs = append(allErrs, validation.ValidateObjectMetaAccessorUpdate(objAccessor, oldAccessor, field.NewPath("metadata"))...)
|
allErrs = append(allErrs, validation.ValidateObjectMetaAccessorUpdate(objAccessor, oldAccessor, field.NewPath("metadata"))...)
|
||||||
allErrs = append(allErrs, apiextensionsvalidation.ValidateCustomResource(nil, u.UnstructuredContent(), a.schemaValidator)...)
|
allErrs = append(allErrs, apiextensionsvalidation.ValidateCustomResourceUpdate(nil, u.UnstructuredContent(), oldU.UnstructuredContent(), a.schemaValidator)...)
|
||||||
allErrs = append(allErrs, a.ValidateScaleSpec(ctx, u, scale)...)
|
allErrs = append(allErrs, a.ValidateScaleSpec(ctx, u, scale)...)
|
||||||
allErrs = append(allErrs, a.ValidateScaleStatus(ctx, u, scale)...)
|
allErrs = append(allErrs, a.ValidateScaleStatus(ctx, u, scale)...)
|
||||||
|
|
||||||
@ -103,6 +106,10 @@ func (a customResourceValidator) ValidateStatusUpdate(ctx context.Context, obj,
|
|||||||
if !ok {
|
if !ok {
|
||||||
return field.ErrorList{field.Invalid(field.NewPath(""), u, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", u))}
|
return field.ErrorList{field.Invalid(field.NewPath(""), u, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", u))}
|
||||||
}
|
}
|
||||||
|
oldU, ok := old.(*unstructured.Unstructured)
|
||||||
|
if !ok {
|
||||||
|
return field.ErrorList{field.Invalid(field.NewPath(""), old, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", u))}
|
||||||
|
}
|
||||||
objAccessor, err := meta.Accessor(obj)
|
objAccessor, err := meta.Accessor(obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
|
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
|
||||||
@ -119,7 +126,7 @@ func (a customResourceValidator) ValidateStatusUpdate(ctx context.Context, obj,
|
|||||||
var allErrs field.ErrorList
|
var allErrs field.ErrorList
|
||||||
|
|
||||||
allErrs = append(allErrs, validation.ValidateObjectMetaAccessorUpdate(objAccessor, oldAccessor, field.NewPath("metadata"))...)
|
allErrs = append(allErrs, validation.ValidateObjectMetaAccessorUpdate(objAccessor, oldAccessor, field.NewPath("metadata"))...)
|
||||||
allErrs = append(allErrs, apiextensionsvalidation.ValidateCustomResource(nil, u.UnstructuredContent(), a.schemaValidator)...)
|
allErrs = append(allErrs, apiextensionsvalidation.ValidateCustomResourceUpdate(nil, u.UnstructuredContent(), oldU, a.schemaValidator)...)
|
||||||
allErrs = append(allErrs, a.ValidateScaleStatus(ctx, u, scale)...)
|
allErrs = append(allErrs, a.ValidateScaleStatus(ctx, u, scale)...)
|
||||||
|
|
||||||
return allErrs
|
return allErrs
|
||||||
|
Loading…
Reference in New Issue
Block a user