mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-17 15:50:10 +00:00
Fix status subresource ratcheting
This commit is contained in:
parent
f1b3fdf7e6
commit
091fa29390
@ -22,11 +22,17 @@ import (
|
|||||||
|
|
||||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||||
|
|
||||||
|
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel"
|
||||||
|
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
|
||||||
structurallisttype "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/listtype"
|
structurallisttype "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/listtype"
|
||||||
|
"k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
|
||||||
|
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||||
|
"k8s.io/apiserver/pkg/cel/common"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
)
|
)
|
||||||
|
|
||||||
type statusStrategy struct {
|
type statusStrategy struct {
|
||||||
@ -94,8 +100,17 @@ func (a statusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Obj
|
|||||||
return field.ErrorList{field.Invalid(field.NewPath(""), old, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", old))}
|
return field.ErrorList{field.Invalid(field.NewPath(""), old, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", old))}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var options []validation.ValidationOption
|
||||||
|
var celOptions []cel.Option
|
||||||
|
var correlatedObject *common.CorrelatedObject
|
||||||
|
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CRDValidationRatcheting) {
|
||||||
|
correlatedObject = common.NewCorrelatedObject(uNew.Object, uOld.Object, &model.Structural{Structural: a.structuralSchema})
|
||||||
|
options = append(options, validation.WithRatcheting(correlatedObject))
|
||||||
|
celOptions = append(celOptions, cel.WithRatcheting(correlatedObject))
|
||||||
|
}
|
||||||
|
|
||||||
var errs field.ErrorList
|
var errs field.ErrorList
|
||||||
errs = append(errs, a.customResourceStrategy.validator.ValidateStatusUpdate(ctx, uNew, uOld, a.scale)...)
|
errs = append(errs, a.customResourceStrategy.validator.ValidateStatusUpdate(ctx, uNew, uOld, a.scale, options...)...)
|
||||||
|
|
||||||
// ratcheting validation of x-kubernetes-list-type value map and set
|
// ratcheting validation of x-kubernetes-list-type value map and set
|
||||||
if newErrs := structurallisttype.ValidateListSetsAndMaps(nil, a.structuralSchema, uNew.Object); len(newErrs) > 0 {
|
if newErrs := structurallisttype.ValidateListSetsAndMaps(nil, a.structuralSchema, uNew.Object); len(newErrs) > 0 {
|
||||||
@ -109,10 +124,15 @@ func (a statusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Obj
|
|||||||
if has, err := hasBlockingErr(errs); has {
|
if has, err := hasBlockingErr(errs); has {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
} else {
|
} else {
|
||||||
err, _ := celValidator.Validate(ctx, nil, a.customResourceStrategy.structuralSchema, uNew.Object, uOld.Object, celconfig.RuntimeCELCostBudget)
|
err, _ := celValidator.Validate(ctx, nil, a.customResourceStrategy.structuralSchema, uNew.Object, uOld.Object, celconfig.RuntimeCELCostBudget, celOptions...)
|
||||||
errs = append(errs, err...)
|
errs = append(errs, err...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No-op if not attached to context
|
||||||
|
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CRDValidationRatcheting) {
|
||||||
|
validation.Metrics.ObserveRatchetingTime(*correlatedObject.Duration)
|
||||||
|
}
|
||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ func validateKubeFinalizerName(stringValue string, fldPath *field.Path) []string
|
|||||||
return allWarnings
|
return allWarnings
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a customResourceValidator) ValidateStatusUpdate(ctx context.Context, obj, old *unstructured.Unstructured, scale *apiextensions.CustomResourceSubresourceScale) field.ErrorList {
|
func (a customResourceValidator) ValidateStatusUpdate(ctx context.Context, obj, old *unstructured.Unstructured, scale *apiextensions.CustomResourceSubresourceScale, options ...apiextensionsvalidation.ValidationOption) field.ErrorList {
|
||||||
if errs := a.ValidateTypeMeta(ctx, obj); len(errs) > 0 {
|
if errs := a.ValidateTypeMeta(ctx, obj); len(errs) > 0 {
|
||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
@ -105,7 +105,7 @@ func (a customResourceValidator) ValidateStatusUpdate(ctx context.Context, obj,
|
|||||||
|
|
||||||
allErrs = append(allErrs, validation.ValidateObjectMetaAccessorUpdate(obj, old, field.NewPath("metadata"))...)
|
allErrs = append(allErrs, validation.ValidateObjectMetaAccessorUpdate(obj, old, field.NewPath("metadata"))...)
|
||||||
if status, hasStatus := obj.UnstructuredContent()["status"]; hasStatus {
|
if status, hasStatus := obj.UnstructuredContent()["status"]; hasStatus {
|
||||||
allErrs = append(allErrs, apiextensionsvalidation.ValidateCustomResourceUpdate(field.NewPath("status"), status, old.UnstructuredContent()["status"], a.statusSchemaValidator)...)
|
allErrs = append(allErrs, apiextensionsvalidation.ValidateCustomResourceUpdate(field.NewPath("status"), status, old.UnstructuredContent()["status"], a.statusSchemaValidator, options...)...)
|
||||||
}
|
}
|
||||||
allErrs = append(allErrs, a.ValidateScaleStatus(ctx, obj, scale)...)
|
allErrs = append(allErrs, a.ValidateScaleStatus(ctx, obj, scale)...)
|
||||||
|
|
||||||
|
@ -45,6 +45,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||||
"k8s.io/apimachinery/pkg/util/uuid"
|
"k8s.io/apimachinery/pkg/util/uuid"
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
utilyaml "k8s.io/apimachinery/pkg/util/yaml"
|
utilyaml "k8s.io/apimachinery/pkg/util/yaml"
|
||||||
@ -81,6 +82,7 @@ type ratchetingTestContext struct {
|
|||||||
*testing.T
|
*testing.T
|
||||||
DynamicClient dynamic.Interface
|
DynamicClient dynamic.Interface
|
||||||
APIExtensionsClient clientset.Interface
|
APIExtensionsClient clientset.Interface
|
||||||
|
SkipStatus bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type ratchetingTestOperation interface {
|
type ratchetingTestOperation interface {
|
||||||
@ -95,6 +97,12 @@ type expectError struct {
|
|||||||
func (e expectError) Do(ctx *ratchetingTestContext) error {
|
func (e expectError) Do(ctx *ratchetingTestContext) error {
|
||||||
err := e.op.Do(ctx)
|
err := e.op.Do(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// If there is an error, it should happen when updating the CR
|
||||||
|
// and also when updating the CR status subresource.
|
||||||
|
if agg, ok := err.(utilerrors.Aggregate); ok && len(agg.Errors()) != 2 {
|
||||||
|
return fmt.Errorf("expected 2 errors, got %v", len(agg.Errors()))
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return errors.New("expected error")
|
return errors.New("expected error")
|
||||||
@ -190,8 +198,51 @@ func (a applyPatchOperation) Do(ctx *ratchetingTestContext) error {
|
|||||||
FieldManager: "manager",
|
FieldManager: "manager",
|
||||||
})
|
})
|
||||||
|
|
||||||
return err
|
if ctx.SkipStatus {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
errs := []error{}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
statusPatch := &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"status": patch.Object,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
statusPatch.SetKind(kind)
|
||||||
|
statusPatch.SetAPIVersion(a.gvr.GroupVersion().String())
|
||||||
|
statusPatch.SetName(a.name)
|
||||||
|
statusPatch.SetNamespace("default")
|
||||||
|
|
||||||
|
delete(patch.Object, "metadata")
|
||||||
|
delete(patch.Object, "apiVersion")
|
||||||
|
delete(patch.Object, "kind")
|
||||||
|
|
||||||
|
_, err = ctx.DynamicClient.
|
||||||
|
Resource(a.gvr).
|
||||||
|
Namespace(statusPatch.GetNamespace()).
|
||||||
|
ApplyStatus(
|
||||||
|
context.TODO(),
|
||||||
|
statusPatch.GetName(),
|
||||||
|
statusPatch,
|
||||||
|
metav1.ApplyOptions{
|
||||||
|
FieldManager: "manager",
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return utilerrors.NewAggregate(errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a applyPatchOperation) Description() string {
|
func (a applyPatchOperation) Description() string {
|
||||||
@ -228,10 +279,21 @@ func (u updateMyCRDV1Beta1Schema) Do(ctx *ratchetingTestContext) error {
|
|||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !ctx.SkipStatus {
|
||||||
|
// Duplicate the schema as a status schema
|
||||||
|
statusSchema := sch.DeepCopy()
|
||||||
|
sch.Properties["status"] = *statusSchema
|
||||||
|
}
|
||||||
|
|
||||||
for _, v := range myCRD.Spec.Versions {
|
for _, v := range myCRD.Spec.Versions {
|
||||||
if v.Name != myCRDV1Beta1.Version {
|
if v.Name != myCRDV1Beta1.Version {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*v.Subresources = apiextensionsv1.CustomResourceSubresources{
|
||||||
|
Status: &apiextensionsv1.CustomResourceSubresourceStatus{},
|
||||||
|
}
|
||||||
|
|
||||||
v.Schema.OpenAPIV3Schema = sch
|
v.Schema.OpenAPIV3Schema = sch
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -288,6 +350,16 @@ func (p patchMyCRDV1Beta1Schema) Do(ctx *ratchetingTestContext) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
statusPatch := map[string]interface{}{
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"status": p.patch,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
statusPatchJSON, err := json.Marshal(statusPatch)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
myCRD, err := ctx.APIExtensionsClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), myCRDV1Beta1.Resource+"."+myCRDV1Beta1.Group, metav1.GetOptions{})
|
myCRD, err := ctx.APIExtensionsClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), myCRDV1Beta1.Resource+"."+myCRDV1Beta1.Group, metav1.GetOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -308,8 +380,13 @@ func (p patchMyCRDV1Beta1Schema) Do(ctx *ratchetingTestContext) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mergedStatus, err := jsonpatch.MergePatch(merged, statusPatchJSON)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
var parsed apiextensionsv1.JSONSchemaProps
|
var parsed apiextensionsv1.JSONSchemaProps
|
||||||
if err := json.Unmarshal(merged, &parsed); err != nil {
|
if err := json.Unmarshal(mergedStatus, &parsed); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,6 +406,7 @@ type ratchetingTestCase struct {
|
|||||||
Name string
|
Name string
|
||||||
Disabled bool
|
Disabled bool
|
||||||
Operations []ratchetingTestOperation
|
Operations []ratchetingTestOperation
|
||||||
|
SkipStatus bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func runTests(t *testing.T, cases []ratchetingTestCase) {
|
func runTests(t *testing.T, cases []ratchetingTestCase) {
|
||||||
@ -372,9 +450,15 @@ func runTests(t *testing.T, cases []ratchetingTestCase) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"status": {
|
||||||
|
Type: "object",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Subresources: &apiextensionsv1.CustomResourceSubresources{
|
||||||
|
Status: &apiextensionsv1.CustomResourceSubresourceStatus{},
|
||||||
|
},
|
||||||
}},
|
}},
|
||||||
Names: apiextensionsv1.CustomResourceDefinitionNames{
|
Names: apiextensionsv1.CustomResourceDefinitionNames{
|
||||||
Plural: resource,
|
Plural: resource,
|
||||||
@ -399,6 +483,7 @@ func runTests(t *testing.T, cases []ratchetingTestCase) {
|
|||||||
T: t,
|
T: t,
|
||||||
DynamicClient: dynamicClient,
|
DynamicClient: dynamicClient,
|
||||||
APIExtensionsClient: apiExtensionClient,
|
APIExtensionsClient: apiExtensionClient,
|
||||||
|
SkipStatus: c.SkipStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, op := range c.Operations {
|
for i, op := range c.Operations {
|
||||||
@ -1330,7 +1415,8 @@ func TestRatchetingFunctionality(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "CEL Optional OldSelf",
|
Name: "CEL Optional OldSelf",
|
||||||
|
SkipStatus: true, // oldSelf can never be null for a status update.
|
||||||
Operations: []ratchetingTestOperation{
|
Operations: []ratchetingTestOperation{
|
||||||
updateMyCRDV1Beta1Schema{&apiextensionsv1.JSONSchemaProps{
|
updateMyCRDV1Beta1Schema{&apiextensionsv1.JSONSchemaProps{
|
||||||
Type: "object",
|
Type: "object",
|
||||||
@ -1387,7 +1473,8 @@ func TestRatchetingFunctionality(t *testing.T) {
|
|||||||
Name: "Not_should_not_ratchet",
|
Name: "Not_should_not_ratchet",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "CEL_transition_rules_should_not_ratchet",
|
Name: "CEL_transition_rules_should_not_ratchet",
|
||||||
|
SkipStatus: true, // Adding a broken status transition rule prevents the object from being updated.
|
||||||
Operations: []ratchetingTestOperation{
|
Operations: []ratchetingTestOperation{
|
||||||
updateMyCRDV1Beta1Schema{&apiextensionsv1.JSONSchemaProps{
|
updateMyCRDV1Beta1Schema{&apiextensionsv1.JSONSchemaProps{
|
||||||
Type: "object",
|
Type: "object",
|
||||||
|
Loading…
Reference in New Issue
Block a user