...

Source file src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/strategy.go

Documentation: k8s.io/apiextensions-apiserver/pkg/registry/customresource

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package customresource
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strings"
    23  
    24  	"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
    25  
    26  	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
    27  	v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    28  	structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
    29  	"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel"
    30  	"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
    31  	structurallisttype "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/listtype"
    32  	schemaobjectmeta "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/objectmeta"
    33  	"k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
    34  	apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
    35  	apiequality "k8s.io/apimachinery/pkg/api/equality"
    36  	"k8s.io/apimachinery/pkg/api/meta"
    37  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    38  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    39  	"k8s.io/apimachinery/pkg/fields"
    40  	"k8s.io/apimachinery/pkg/labels"
    41  	"k8s.io/apimachinery/pkg/runtime"
    42  	"k8s.io/apimachinery/pkg/runtime/schema"
    43  	"k8s.io/apimachinery/pkg/util/sets"
    44  	"k8s.io/apimachinery/pkg/util/validation/field"
    45  	celconfig "k8s.io/apiserver/pkg/apis/cel"
    46  	"k8s.io/apiserver/pkg/cel/common"
    47  	"k8s.io/apiserver/pkg/features"
    48  	"k8s.io/apiserver/pkg/registry/generic"
    49  	apiserverstorage "k8s.io/apiserver/pkg/storage"
    50  	"k8s.io/apiserver/pkg/storage/names"
    51  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    52  	"k8s.io/client-go/util/jsonpath"
    53  )
    54  
    55  // customResourceStrategy implements behavior for CustomResources for a single
    56  // version
    57  type customResourceStrategy struct {
    58  	runtime.ObjectTyper
    59  	names.NameGenerator
    60  
    61  	namespaceScoped    bool
    62  	validator          customResourceValidator
    63  	structuralSchema   *structuralschema.Structural
    64  	celValidator       *cel.Validator
    65  	status             *apiextensions.CustomResourceSubresourceStatus
    66  	scale              *apiextensions.CustomResourceSubresourceScale
    67  	kind               schema.GroupVersionKind
    68  	selectableFieldSet []selectableField
    69  }
    70  
    71  type selectableField struct {
    72  	name      string
    73  	fieldPath *jsonpath.JSONPath
    74  	err       error
    75  }
    76  
    77  func NewStrategy(typer runtime.ObjectTyper, namespaceScoped bool, kind schema.GroupVersionKind, schemaValidator, statusSchemaValidator validation.SchemaValidator, structuralSchema *structuralschema.Structural, status *apiextensions.CustomResourceSubresourceStatus, scale *apiextensions.CustomResourceSubresourceScale, selectableFields []v1.SelectableField) customResourceStrategy {
    78  	var celValidator *cel.Validator
    79  	if utilfeature.DefaultFeatureGate.Enabled(features.CustomResourceValidationExpressions) {
    80  		celValidator = cel.NewValidator(structuralSchema, true, celconfig.PerCallLimit) // CEL programs are compiled and cached here
    81  	}
    82  
    83  	strategy := customResourceStrategy{
    84  		ObjectTyper:     typer,
    85  		NameGenerator:   names.SimpleNameGenerator,
    86  		namespaceScoped: namespaceScoped,
    87  		status:          status,
    88  		scale:           scale,
    89  		validator: customResourceValidator{
    90  			namespaceScoped:       namespaceScoped,
    91  			kind:                  kind,
    92  			schemaValidator:       schemaValidator,
    93  			statusSchemaValidator: statusSchemaValidator,
    94  		},
    95  		structuralSchema: structuralSchema,
    96  		celValidator:     celValidator,
    97  		kind:             kind,
    98  	}
    99  	if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceFieldSelectors) {
   100  		strategy.selectableFieldSet = prepareSelectableFields(selectableFields)
   101  	}
   102  	return strategy
   103  }
   104  
   105  func prepareSelectableFields(selectableFields []v1.SelectableField) []selectableField {
   106  	result := make([]selectableField, len(selectableFields))
   107  	for i, sf := range selectableFields {
   108  		name := strings.TrimPrefix(sf.JSONPath, ".")
   109  
   110  		parser := jsonpath.New("selectableField")
   111  		parser.AllowMissingKeys(true)
   112  		err := parser.Parse("{" + sf.JSONPath + "}")
   113  		if err == nil {
   114  			result[i] = selectableField{
   115  				name:      name,
   116  				fieldPath: parser,
   117  			}
   118  		} else {
   119  			result[i] = selectableField{
   120  				name: name,
   121  				err:  err,
   122  			}
   123  		}
   124  	}
   125  
   126  	return result
   127  }
   128  
   129  func (a customResourceStrategy) NamespaceScoped() bool {
   130  	return a.namespaceScoped
   131  }
   132  
   133  // GetResetFields returns the set of fields that get reset by the strategy
   134  // and should not be modified by the user.
   135  func (a customResourceStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set {
   136  	fields := map[fieldpath.APIVersion]*fieldpath.Set{}
   137  
   138  	if a.status != nil {
   139  		fields[fieldpath.APIVersion(a.kind.GroupVersion().String())] = fieldpath.NewSet(
   140  			fieldpath.MakePathOrDie("status"),
   141  		)
   142  	}
   143  
   144  	return fields
   145  }
   146  
   147  // PrepareForCreate clears the status of a CustomResource before creation.
   148  func (a customResourceStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
   149  	if a.status != nil {
   150  		customResourceObject := obj.(*unstructured.Unstructured)
   151  		customResource := customResourceObject.UnstructuredContent()
   152  
   153  		// create cannot set status
   154  		delete(customResource, "status")
   155  	}
   156  
   157  	accessor, _ := meta.Accessor(obj)
   158  	accessor.SetGeneration(1)
   159  }
   160  
   161  // PrepareForUpdate clears fields that are not allowed to be set by end users on update.
   162  func (a customResourceStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
   163  	newCustomResourceObject := obj.(*unstructured.Unstructured)
   164  	oldCustomResourceObject := old.(*unstructured.Unstructured)
   165  
   166  	newCustomResource := newCustomResourceObject.UnstructuredContent()
   167  	oldCustomResource := oldCustomResourceObject.UnstructuredContent()
   168  
   169  	// If the /status subresource endpoint is installed, update is not allowed to set status.
   170  	if a.status != nil {
   171  		_, ok1 := newCustomResource["status"]
   172  		_, ok2 := oldCustomResource["status"]
   173  		switch {
   174  		case ok2:
   175  			newCustomResource["status"] = oldCustomResource["status"]
   176  		case ok1:
   177  			delete(newCustomResource, "status")
   178  		}
   179  	}
   180  
   181  	// except for the changes to `metadata`, any other changes
   182  	// cause the generation to increment.
   183  	newCopyContent := copyNonMetadata(newCustomResource)
   184  	oldCopyContent := copyNonMetadata(oldCustomResource)
   185  	if !apiequality.Semantic.DeepEqual(newCopyContent, oldCopyContent) {
   186  		oldAccessor, _ := meta.Accessor(oldCustomResourceObject)
   187  		newAccessor, _ := meta.Accessor(newCustomResourceObject)
   188  		newAccessor.SetGeneration(oldAccessor.GetGeneration() + 1)
   189  	}
   190  }
   191  
   192  func copyNonMetadata(original map[string]interface{}) map[string]interface{} {
   193  	ret := make(map[string]interface{})
   194  	for key, val := range original {
   195  		if key == "metadata" {
   196  			continue
   197  		}
   198  		ret[key] = val
   199  	}
   200  	return ret
   201  }
   202  
   203  // Validate validates a new CustomResource.
   204  func (a customResourceStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
   205  	u, ok := obj.(*unstructured.Unstructured)
   206  	if !ok {
   207  		return field.ErrorList{field.Invalid(field.NewPath(""), u, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", obj))}
   208  	}
   209  
   210  	var errs field.ErrorList
   211  	errs = append(errs, a.validator.Validate(ctx, u, a.scale)...)
   212  
   213  	// validate embedded resources
   214  	errs = append(errs, schemaobjectmeta.Validate(nil, u.Object, a.structuralSchema, false)...)
   215  
   216  	// validate x-kubernetes-list-type "map" and "set" invariant
   217  	errs = append(errs, structurallisttype.ValidateListSetsAndMaps(nil, a.structuralSchema, u.Object)...)
   218  
   219  	// validate x-kubernetes-validations rules
   220  	if celValidator := a.celValidator; celValidator != nil {
   221  		if has, err := hasBlockingErr(errs); has {
   222  			errs = append(errs, err)
   223  		} else {
   224  			err, _ := celValidator.Validate(ctx, nil, a.structuralSchema, u.Object, nil, celconfig.RuntimeCELCostBudget)
   225  			errs = append(errs, err...)
   226  		}
   227  	}
   228  
   229  	return errs
   230  }
   231  
   232  // WarningsOnCreate returns warnings for the creation of the given object.
   233  func (a customResourceStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
   234  	return generateWarningsFromObj(obj, nil)
   235  }
   236  
   237  func generateWarningsFromObj(obj, old runtime.Object) []string {
   238  	var allWarnings []string
   239  	fldPath := field.NewPath("metadata", "finalizers")
   240  	newObjAccessor, err := meta.Accessor(obj)
   241  	if err != nil {
   242  		return allWarnings
   243  	}
   244  
   245  	newAdded := sets.NewString(newObjAccessor.GetFinalizers()...)
   246  	if old != nil {
   247  		oldObjAccessor, err := meta.Accessor(old)
   248  		if err != nil {
   249  			return allWarnings
   250  		}
   251  		newAdded = newAdded.Difference(sets.NewString(oldObjAccessor.GetFinalizers()...))
   252  	}
   253  
   254  	for _, finalizer := range newAdded.List() {
   255  		allWarnings = append(allWarnings, validateKubeFinalizerName(finalizer, fldPath)...)
   256  	}
   257  
   258  	return allWarnings
   259  }
   260  
   261  // Canonicalize normalizes the object after validation.
   262  func (customResourceStrategy) Canonicalize(obj runtime.Object) {
   263  }
   264  
   265  // AllowCreateOnUpdate is false for CustomResources; this means a POST is
   266  // needed to create one.
   267  func (customResourceStrategy) AllowCreateOnUpdate() bool {
   268  	return false
   269  }
   270  
   271  // AllowUnconditionalUpdate is the default update policy for CustomResource objects.
   272  func (customResourceStrategy) AllowUnconditionalUpdate() bool {
   273  	return false
   274  }
   275  
   276  // ValidateUpdate is the default update validation for an end user updating status.
   277  func (a customResourceStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
   278  	uNew, ok := obj.(*unstructured.Unstructured)
   279  	if !ok {
   280  		return field.ErrorList{field.Invalid(field.NewPath(""), obj, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", obj))}
   281  	}
   282  	uOld, ok := old.(*unstructured.Unstructured)
   283  	if !ok {
   284  		return field.ErrorList{field.Invalid(field.NewPath(""), old, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", old))}
   285  	}
   286  
   287  	var options []validation.ValidationOption
   288  	var celOptions []cel.Option
   289  	var correlatedObject *common.CorrelatedObject
   290  	if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CRDValidationRatcheting) {
   291  		correlatedObject = common.NewCorrelatedObject(uNew.Object, uOld.Object, &model.Structural{Structural: a.structuralSchema})
   292  		options = append(options, validation.WithRatcheting(correlatedObject))
   293  		celOptions = append(celOptions, cel.WithRatcheting(correlatedObject))
   294  	}
   295  
   296  	var errs field.ErrorList
   297  	errs = append(errs, a.validator.ValidateUpdate(ctx, uNew, uOld, a.scale, options...)...)
   298  
   299  	// Checks the embedded objects. We don't make a difference between update and create for those.
   300  	errs = append(errs, schemaobjectmeta.Validate(nil, uNew.Object, a.structuralSchema, false)...)
   301  
   302  	// ratcheting validation of x-kubernetes-list-type value map and set
   303  	if oldErrs := structurallisttype.ValidateListSetsAndMaps(nil, a.structuralSchema, uOld.Object); len(oldErrs) == 0 {
   304  		errs = append(errs, structurallisttype.ValidateListSetsAndMaps(nil, a.structuralSchema, uNew.Object)...)
   305  	}
   306  
   307  	// validate x-kubernetes-validations rules
   308  	if celValidator := a.celValidator; celValidator != nil {
   309  		if has, err := hasBlockingErr(errs); has {
   310  			errs = append(errs, err)
   311  		} else {
   312  			err, _ := celValidator.Validate(ctx, nil, a.structuralSchema, uNew.Object, uOld.Object, celconfig.RuntimeCELCostBudget, celOptions...)
   313  			errs = append(errs, err...)
   314  		}
   315  	}
   316  
   317  	// No-op if not attached to context
   318  	if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CRDValidationRatcheting) {
   319  		validation.Metrics.ObserveRatchetingTime(*correlatedObject.Duration)
   320  	}
   321  	return errs
   322  }
   323  
   324  // WarningsOnUpdate returns warnings for the given update.
   325  func (a customResourceStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
   326  	return generateWarningsFromObj(obj, old)
   327  }
   328  
   329  // GetAttrs returns labels and fields of a given object for filtering purposes.
   330  func (a customResourceStrategy) GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) {
   331  	accessor, err := meta.Accessor(obj)
   332  	if err != nil {
   333  		return nil, nil, err
   334  	}
   335  	sFields, err := a.selectableFields(obj, accessor)
   336  	if err != nil {
   337  		return nil, nil, err
   338  	}
   339  	return accessor.GetLabels(), sFields, nil
   340  }
   341  
   342  // selectableFields returns a field set that can be used for filter selection.
   343  // This includes metadata.name, metadata.namespace and all custom selectable fields.
   344  func (a customResourceStrategy) selectableFields(obj runtime.Object, objectMeta metav1.Object) (fields.Set, error) {
   345  	objectMetaFields := objectMetaFieldsSet(objectMeta, a.namespaceScoped)
   346  	var selectableFieldsSet fields.Set
   347  
   348  	if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceFieldSelectors) && len(a.selectableFieldSet) > 0 {
   349  		us, ok := obj.(runtime.Unstructured)
   350  		if !ok {
   351  			return nil, fmt.Errorf("unexpected error casting a custom resource to unstructured")
   352  		}
   353  		uc := us.UnstructuredContent()
   354  
   355  		selectableFieldsSet = fields.Set{}
   356  		for _, sf := range a.selectableFieldSet {
   357  			if sf.err != nil {
   358  				return nil, fmt.Errorf("unexpected error parsing jsonPath: %w", sf.err)
   359  			}
   360  			results, err := sf.fieldPath.FindResults(uc)
   361  			if err != nil {
   362  				return nil, fmt.Errorf("unexpected error finding value with jsonPath: %w", err)
   363  			}
   364  			var value any
   365  
   366  			if len(results) > 0 && len(results[0]) > 0 {
   367  				if len(results) > 1 || len(results[0]) > 1 {
   368  					return nil, fmt.Errorf("unexpectedly received more than one JSON path result")
   369  				}
   370  				value = results[0][0].Interface()
   371  			}
   372  
   373  			if value != nil {
   374  				selectableFieldsSet[sf.name] = fmt.Sprint(value)
   375  			} else {
   376  				selectableFieldsSet[sf.name] = ""
   377  			}
   378  		}
   379  	}
   380  	return generic.MergeFieldsSets(objectMetaFields, selectableFieldsSet), nil
   381  }
   382  
   383  // objectMetaFieldsSet returns a fields that represent the ObjectMeta.
   384  func objectMetaFieldsSet(objectMeta metav1.Object, namespaceScoped bool) fields.Set {
   385  	if namespaceScoped {
   386  		return fields.Set{
   387  			"metadata.name":      objectMeta.GetName(),
   388  			"metadata.namespace": objectMeta.GetNamespace(),
   389  		}
   390  	}
   391  	return fields.Set{
   392  		"metadata.name": objectMeta.GetName(),
   393  	}
   394  }
   395  
   396  // MatchCustomResourceDefinitionStorage is the filter used by the generic etcd backend to route
   397  // watch events from etcd to clients of the apiserver only interested in specific
   398  // labels/fields.
   399  func (a customResourceStrategy) MatchCustomResourceDefinitionStorage(label labels.Selector, field fields.Selector) apiserverstorage.SelectionPredicate {
   400  	return apiserverstorage.SelectionPredicate{
   401  		Label:    label,
   402  		Field:    field,
   403  		GetAttrs: a.GetAttrs,
   404  	}
   405  }
   406  
   407  // OpenAPIv3 type/maxLength/maxItems/MaxProperties/required/enum violation/wrong type field validation failures are viewed as blocking err for CEL validation
   408  func hasBlockingErr(errs field.ErrorList) (bool, *field.Error) {
   409  	for _, err := range errs {
   410  		if err.Type == field.ErrorTypeNotSupported || err.Type == field.ErrorTypeRequired || err.Type == field.ErrorTypeTooLong || err.Type == field.ErrorTypeTooMany || err.Type == field.ErrorTypeTypeInvalid {
   411  			return true, field.Invalid(nil, nil, "some validation rules were not checked because the object was invalid; correct the existing errors to complete validation")
   412  		}
   413  	}
   414  	return false, nil
   415  }
   416  

View as plain text