Change:
What happened: Using a command such as: fails with the following error: This bug happens on each run and started occurring as of v1.15.0. With v1.14 or older, I don't have such problem, i.e. it works flawlessly. What you expected to happen: The command succeeds: How to reproduce it (as minimally and precisely as possible): Create a CRD that has the scale subresource and try to scale a CR using . Example of such CRD can be found . Anything else we need to know?: Regarding the bug, it seems that it started appearing as of . Before the PR, we always fetched the resource and it worked because it had the resource version, while now we are only fetching it in case preconditions are provided (preconditions are not provided by default/when using ). Here is the output of a command: If I manually craft the request that has the resource version (or use the flag), it works. Is it as expected that the Scale resource requires the resource version in order for update to succeed? Shouldn't it always pick the latest resource version in case it isn't provided? On that note, I also noticed that if you want to update a CR by crafting the request manually and using , you also need to specify the resource version or you will get the same error. I'm not sure if this is expected and related, but I'm writing it down as it might be related. As far as I know, this is not required for some native resources at all, but I'm not sure is this the case for CRDs. Environment: Kubernetes version (use ):
/sig api-machinery cc
/sub
could you try to see if it fix the problem?
I've created a test cluster based on and I can confirm that I'm able to scale CustomResources using as usual. I used an to test this, such as: Thanks a lot for your time spent fixing this!
Bug triage for 1.17 here with a gentle reminder that code freeze for this release is on November 14. Is this issue still intended for 1.17? I see you move it to 1.17 so I just wanted to be sure.
I guess could be merged now?
It is in the review queue and should still make 1.17
a kind reminder that code freeze for this release is on November 14.
I wanted to circle back around on this. We are 4 days out from code freeze. If this is still good based on just note here. Judging from the looks of it we are still good
Yes this should merge early in the week
Code freeze is tomorrow, this still going to make it?
yes, is still planned

Discussion:
} func (r *ScaleREST) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { <del> obj, err := r.store.Get(ctx, name, &metav1.GetOptions{}) if err != nil { return nil, false, err } cr := obj.(*unstructured.Unstructured) const invalidSpecReplicas = -2147483648 // smallest int32 oldScale, replicasFound, err := scaleFromCustomResource(cr, r.specReplicasPath, r.statusReplicasPath, r.labelSelectorPath) if err != nil { return nil, false, err } if !replicasFound { oldScale.Spec.Replicas = invalidSpecReplicas // signal that this was not set before } obj, err = objInfo.UpdatedObject(ctx, oldScale) if err != nil { return nil, false, err } if obj == nil { return nil, false, apierrors.NewBadRequest(fmt.Sprintf("nil update passed to Scale")) } scale, ok := obj.(*autoscalingv1.Scale) if !ok { return nil, false, apierrors.NewBadRequest(fmt.Sprintf("wrong object passed to Scale update: %v", obj)) } if scale.Spec.Replicas == invalidSpecReplicas { return nil, false, apierrors.NewBadRequest(fmt.Sprintf("the spec replicas field %q cannot be empty", r.specReplicasPath)) </del> <ins> scaleObjInfo := &scaleUpdatedObjectInfo{ reqObjInfo:         objInfo, specReplicasPath:   r.specReplicasPath, labelSelectorPath:  r.labelSelectorPath, statusReplicasPath: r.statusReplicasPath, </ins> } <del> specReplicasPath := strings.TrimPrefix(r.specReplicasPath, ".") // ignore leading period if err = unstructured.SetNestedField(cr.Object, int64(scale.Spec.Replicas), strings.Split(specReplicasPath, ".")...); err != nil { return nil, false, err } cr.SetResourceVersion(scale.ResourceVersion) obj, _, err = r.store.Update( </del> <ins> obj, _, err := r.store.Update( </ins> ctx, <del> cr.GetName(), rest.DefaultUpdatedObjectInfo(cr), </del> <ins> name, scaleObjInfo, </ins> toScaleCreateValidation(createValidation, r.specReplicasPath, r.statusReplicasPath, r.labelSelectorPath), toScaleUpdateValidation(updateValidation, r.specReplicasPath, r.statusReplicasPath, r.labelSelectorPath), false,

if err != nil { return nil, false, err } <del> cr = obj.(*unstructured.Unstructured) </del> <ins> cr := obj.(*unstructured.Unstructured) </ins> newScale, _, err := scaleFromCustomResource(cr, r.specReplicasPath, r.statusReplicasPath, r.labelSelectorPath) if err != nil { return nil, false, apierrors.NewBadRequest(err.Error()) } return newScale, false, err }

return scale, foundSpecReplicas, nil } <ins> type scaleUpdatedObjectInfo struct { reqObjInfo         rest.UpdatedObjectInfo specReplicasPath   string statusReplicasPath string labelSelectorPath  string } func (i *scaleUpdatedObjectInfo) Preconditions() *metav1.Preconditions { return i.reqObjInfo.Preconditions() } func (i *scaleUpdatedObjectInfo) UpdatedObject(ctx context.Context, oldObj runtime.Object) (runtime.Object, error) { cr := oldObj.DeepCopyObject().(*unstructured.Unstructured) const invalidSpecReplicas = -2147483648 // smallest int32 oldScale, replicasFound, err := scaleFromCustomResource(cr, i.specReplicasPath, i.statusReplicasPath, i.labelSelectorPath) if err != nil { return nil, err } if !replicasFound { oldScale.Spec.Replicas = invalidSpecReplicas // signal that this was not set before } obj, err := i.reqObjInfo.UpdatedObject(ctx, oldScale) if err != nil { return nil, err } if obj == nil { return nil, apierrors.NewBadRequest(fmt.Sprintf("nil update passed to Scale")) } scale, ok := obj.(*autoscalingv1.Scale) if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("wrong object passed to Scale update: %v", obj)) } if scale.Spec.Replicas == invalidSpecReplicas { return nil, apierrors.NewBadRequest(fmt.Sprintf("the spec replicas field %q cannot be empty", i.specReplicasPath)) } specReplicasPath := strings.TrimPrefix(i.specReplicasPath, ".") // ignore leading period if err := unstructured.SetNestedField(cr.Object, int64(scale.Spec.Replicas), strings.Split(specReplicasPath, ".")...); err != nil { return nil, err } if len(scale.ResourceVersion) != 0 { // The client provided a resourceVersion precondition. // Set that precondition and return any conflict errors to the client. cr.SetResourceVersion(scale.ResourceVersion) } return cr, nil } </ins>

package customresource_test import ( <ins> "context" "fmt" </ins> "reflect" "strings" "testing"

} } <ins> func TestScaleUpdateWithoutResourceVersion(t *testing.T) { storage, server := newStorage(t) defer server.Terminate(t) defer storage.CustomResource.Store.DestroyFunc() name := "foo" var cr unstructured.Unstructured ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault) key := "/noxus/" + metav1.NamespaceDefault + "/" + name if err := storage.CustomResource.Storage.Create(ctx, key, &validCustomResource, &cr, 0, false); err != nil { t.Fatalf("error setting new custom resource (key: %s) %v: %v", key, validCustomResource, err) } replicas := int32(8) update := autoscalingv1.Scale{ ObjectMeta: metav1.ObjectMeta{ Name: name, }, Spec: autoscalingv1.ScaleSpec{ Replicas: replicas, }, } if _, _, err := storage.Scale.Update(ctx, update.Name, rest.DefaultUpdatedObjectInfo(&update), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil { t.Fatalf("error updating scale %v: %v", update, err) } obj, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{}) if err != nil { t.Fatalf("error fetching scale for %s: %v", name, err) } scale := obj.(*autoscalingv1.Scale) if scale.Spec.Replicas != replicas { t.Errorf("wrong replicas count: expected: %d got: %d", replicas, scale.Spec.Replicas) } } func TestScaleUpdateWithoutResourceVersionWithConflicts(t *testing.T) { storage, server := newStorage(t) defer server.Terminate(t) defer storage.CustomResource.Store.DestroyFunc() name := "foo" var cr unstructured.Unstructured ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault) key := "/noxus/" + metav1.NamespaceDefault + "/" + name if err := storage.CustomResource.Storage.Create(ctx, key, &validCustomResource, &cr, 0, false); err != nil { t.Fatalf("error setting new custom resource (key: %s) %v: %v", key, validCustomResource, err) } fetchObject := func(name string) (*unstructured.Unstructured, error) { gotObj, err := storage.CustomResource.Get(ctx, name, &metav1.GetOptions{}) if err != nil { return nil, fmt.Errorf("error fetching custom resource %s: %v", name, err) } return gotObj.(*unstructured.Unstructured), nil } applyPatch := func(labelName, labelValue string) rest.TransformFunc { return func(_ context.Context, _, currentObject runtime.Object) (objToUpdate runtime.Object, patchErr error) { o := currentObject.(metav1.Object) o.SetLabels(map[string]string{ labelName: labelValue, }) return currentObject, nil } } errs := make(chan error, 1) rounds := 100 go func() { // continuously submits a patch that updates a label and verifies the label update was effective labelName := "timestamp" for i := 0; i < rounds; i++ { expectedLabelValue := fmt.Sprint(i) update, err := fetchObject(name) if err != nil { errs <- err return } setNestedField(update, expectedLabelValue, "metadata", "labels", labelName) if _, _, err := storage.CustomResource.Update(ctx, name, rest.DefaultUpdatedObjectInfo(nil, applyPatch(labelName, fmt.Sprint(i))), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil { errs <- fmt.Errorf("error updating custom resource label: %v", err) return } gotObj, err := fetchObject(name) if err != nil { errs <- err return } gotLabelValue, _, err := unstructured.NestedString(gotObj.Object, "metadata", "labels", labelName) if err != nil { errs <- fmt.Errorf("error getting label %s of custom resource %s: %v", labelName, name, err) return } if gotLabelValue != expectedLabelValue { errs <- fmt.Errorf("wrong label value: expected: %s, got: %s", expectedLabelValue, gotLabelValue) return } } }() replicas := int32(0) update := autoscalingv1.Scale{ ObjectMeta: metav1.ObjectMeta{ Name: name, }, } // continuously submits a scale update without a resourceVersion for a monotonically increasing replica value // and verifies the scale update was effective for i := 0; i < rounds; i++ { select { case err := <-errs: t.Fatal(err) default: replicas++ update.Spec.Replicas = replicas if _, _, err := storage.Scale.Update(ctx, update.Name, rest.DefaultUpdatedObjectInfo(&update), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil { t.Fatalf("error updating scale %v: %v", update, err) } obj, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{}) if err != nil { t.Fatalf("error fetching scale for %s: %v", name, err) } scale := obj.(*autoscalingv1.Scale) if scale.Spec.Replicas != replicas { t.Errorf("wrong replicas count: expected: %d got: %d", replicas, scale.Spec.Replicas) } } } } func TestScaleUpdateWithResourceVersionWithConflicts(t *testing.T) { storage, server := newStorage(t) defer server.Terminate(t) defer storage.CustomResource.Store.DestroyFunc() name := "foo" var cr unstructured.Unstructured ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault) key := "/noxus/" + metav1.NamespaceDefault + "/" + name if err := storage.CustomResource.Storage.Create(ctx, key, &validCustomResource, &cr, 0, false); err != nil { t.Fatalf("error setting new custom resource (key: %s) %v: %v", key, validCustomResource, err) } obj, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{}) if err != nil { t.Fatalf("error fetching scale for %s: %v", name, err) } scale, ok := obj.(*autoscalingv1.Scale) if !ok { t.Fatalf("%v is not of the type autoscalingv1.Scale", scale) } replicas := int32(12) update := autoscalingv1.Scale{ ObjectMeta: scale.ObjectMeta, Spec: autoscalingv1.ScaleSpec{ Replicas: replicas, }, } update.ResourceVersion = "1" _, _, err = storage.Scale.Update(ctx, update.Name, rest.DefaultUpdatedObjectInfo(&update), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}) if err == nil { t.Fatal("expecting an update conflict error") } if !errors.IsConflict(err) { t.Fatalf("unexpected error, expecting an update conflict but got %v", err) } } </ins> func setSpecReplicas(u *unstructured.Unstructured, replicas int64) { setNestedField(u, replicas, "spec", "replicas") }