Skip to content

Commit a6f51da

Browse files
authored
Merge pull request kubernetes#80572 from knight42/fix/scale-cr
Fix missing resource version when updating the scale subresource of custom resource
2 parents 72bcec4 + da24601 commit a6f51da

File tree

2 files changed

+242
-40
lines changed

2 files changed

+242
-40
lines changed

staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/etcd.go

Lines changed: 62 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -237,48 +237,17 @@ func (r *ScaleREST) Get(ctx context.Context, name string, options *metav1.GetOpt
237237
}
238238

239239
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) {
240-
obj, err := r.store.Get(ctx, name, &metav1.GetOptions{})
241-
if err != nil {
242-
return nil, false, err
243-
}
244-
cr := obj.(*unstructured.Unstructured)
245-
246-
const invalidSpecReplicas = -2147483648 // smallest int32
247-
oldScale, replicasFound, err := scaleFromCustomResource(cr, r.specReplicasPath, r.statusReplicasPath, r.labelSelectorPath)
248-
if err != nil {
249-
return nil, false, err
250-
}
251-
if !replicasFound {
252-
oldScale.Spec.Replicas = invalidSpecReplicas // signal that this was not set before
253-
}
254-
255-
obj, err = objInfo.UpdatedObject(ctx, oldScale)
256-
if err != nil {
257-
return nil, false, err
258-
}
259-
if obj == nil {
260-
return nil, false, apierrors.NewBadRequest(fmt.Sprintf("nil update passed to Scale"))
261-
}
262-
263-
scale, ok := obj.(*autoscalingv1.Scale)
264-
if !ok {
265-
return nil, false, apierrors.NewBadRequest(fmt.Sprintf("wrong object passed to Scale update: %v", obj))
266-
}
267-
268-
if scale.Spec.Replicas == invalidSpecReplicas {
269-
return nil, false, apierrors.NewBadRequest(fmt.Sprintf("the spec replicas field %q cannot be empty", r.specReplicasPath))
240+
scaleObjInfo := &scaleUpdatedObjectInfo{
241+
reqObjInfo: objInfo,
242+
specReplicasPath: r.specReplicasPath,
243+
labelSelectorPath: r.labelSelectorPath,
244+
statusReplicasPath: r.statusReplicasPath,
270245
}
271246

272-
specReplicasPath := strings.TrimPrefix(r.specReplicasPath, ".") // ignore leading period
273-
if err = unstructured.SetNestedField(cr.Object, int64(scale.Spec.Replicas), strings.Split(specReplicasPath, ".")...); err != nil {
274-
return nil, false, err
275-
}
276-
cr.SetResourceVersion(scale.ResourceVersion)
277-
278-
obj, _, err = r.store.Update(
247+
obj, _, err := r.store.Update(
279248
ctx,
280-
cr.GetName(),
281-
rest.DefaultUpdatedObjectInfo(cr),
249+
name,
250+
scaleObjInfo,
282251
toScaleCreateValidation(createValidation, r.specReplicasPath, r.statusReplicasPath, r.labelSelectorPath),
283252
toScaleUpdateValidation(updateValidation, r.specReplicasPath, r.statusReplicasPath, r.labelSelectorPath),
284253
false,
@@ -287,12 +256,13 @@ func (r *ScaleREST) Update(ctx context.Context, name string, objInfo rest.Update
287256
if err != nil {
288257
return nil, false, err
289258
}
290-
cr = obj.(*unstructured.Unstructured)
259+
cr := obj.(*unstructured.Unstructured)
291260

292261
newScale, _, err := scaleFromCustomResource(cr, r.specReplicasPath, r.statusReplicasPath, r.labelSelectorPath)
293262
if err != nil {
294263
return nil, false, apierrors.NewBadRequest(err.Error())
295264
}
265+
296266
return newScale, false, err
297267
}
298268

@@ -372,3 +342,55 @@ func scaleFromCustomResource(cr *unstructured.Unstructured, specReplicasPath, st
372342

373343
return scale, foundSpecReplicas, nil
374344
}
345+
346+
type scaleUpdatedObjectInfo struct {
347+
reqObjInfo rest.UpdatedObjectInfo
348+
specReplicasPath string
349+
statusReplicasPath string
350+
labelSelectorPath string
351+
}
352+
353+
func (i *scaleUpdatedObjectInfo) Preconditions() *metav1.Preconditions {
354+
return i.reqObjInfo.Preconditions()
355+
}
356+
357+
func (i *scaleUpdatedObjectInfo) UpdatedObject(ctx context.Context, oldObj runtime.Object) (runtime.Object, error) {
358+
cr := oldObj.DeepCopyObject().(*unstructured.Unstructured)
359+
const invalidSpecReplicas = -2147483648 // smallest int32
360+
oldScale, replicasFound, err := scaleFromCustomResource(cr, i.specReplicasPath, i.statusReplicasPath, i.labelSelectorPath)
361+
if err != nil {
362+
return nil, err
363+
}
364+
if !replicasFound {
365+
oldScale.Spec.Replicas = invalidSpecReplicas // signal that this was not set before
366+
}
367+
368+
obj, err := i.reqObjInfo.UpdatedObject(ctx, oldScale)
369+
if err != nil {
370+
return nil, err
371+
}
372+
if obj == nil {
373+
return nil, apierrors.NewBadRequest(fmt.Sprintf("nil update passed to Scale"))
374+
}
375+
376+
scale, ok := obj.(*autoscalingv1.Scale)
377+
if !ok {
378+
return nil, apierrors.NewBadRequest(fmt.Sprintf("wrong object passed to Scale update: %v", obj))
379+
}
380+
381+
if scale.Spec.Replicas == invalidSpecReplicas {
382+
return nil, apierrors.NewBadRequest(fmt.Sprintf("the spec replicas field %q cannot be empty", i.specReplicasPath))
383+
}
384+
385+
specReplicasPath := strings.TrimPrefix(i.specReplicasPath, ".") // ignore leading period
386+
387+
if err := unstructured.SetNestedField(cr.Object, int64(scale.Spec.Replicas), strings.Split(specReplicasPath, ".")...); err != nil {
388+
return nil, err
389+
}
390+
if len(scale.ResourceVersion) != 0 {
391+
// The client provided a resourceVersion precondition.
392+
// Set that precondition and return any conflict errors to the client.
393+
cr.SetResourceVersion(scale.ResourceVersion)
394+
}
395+
return cr, nil
396+
}

staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/etcd_test.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ limitations under the License.
1717
package customresource_test
1818

1919
import (
20+
"context"
21+
"fmt"
2022
"reflect"
2123
"strings"
2224
"testing"
@@ -526,6 +528,184 @@ func TestScaleUpdateWithoutSpecReplicas(t *testing.T) {
526528
}
527529
}
528530

531+
func TestScaleUpdateWithoutResourceVersion(t *testing.T) {
532+
storage, server := newStorage(t)
533+
defer server.Terminate(t)
534+
defer storage.CustomResource.Store.DestroyFunc()
535+
536+
name := "foo"
537+
538+
var cr unstructured.Unstructured
539+
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
540+
key := "/noxus/" + metav1.NamespaceDefault + "/" + name
541+
if err := storage.CustomResource.Storage.Create(ctx, key, &validCustomResource, &cr, 0, false); err != nil {
542+
t.Fatalf("error setting new custom resource (key: %s) %v: %v", key, validCustomResource, err)
543+
}
544+
545+
replicas := int32(8)
546+
update := autoscalingv1.Scale{
547+
ObjectMeta: metav1.ObjectMeta{
548+
Name: name,
549+
},
550+
Spec: autoscalingv1.ScaleSpec{
551+
Replicas: replicas,
552+
},
553+
}
554+
555+
if _, _, err := storage.Scale.Update(ctx, update.Name, rest.DefaultUpdatedObjectInfo(&update), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil {
556+
t.Fatalf("error updating scale %v: %v", update, err)
557+
}
558+
559+
obj, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{})
560+
if err != nil {
561+
t.Fatalf("error fetching scale for %s: %v", name, err)
562+
}
563+
scale := obj.(*autoscalingv1.Scale)
564+
if scale.Spec.Replicas != replicas {
565+
t.Errorf("wrong replicas count: expected: %d got: %d", replicas, scale.Spec.Replicas)
566+
}
567+
}
568+
569+
func TestScaleUpdateWithoutResourceVersionWithConflicts(t *testing.T) {
570+
storage, server := newStorage(t)
571+
defer server.Terminate(t)
572+
defer storage.CustomResource.Store.DestroyFunc()
573+
574+
name := "foo"
575+
576+
var cr unstructured.Unstructured
577+
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
578+
key := "/noxus/" + metav1.NamespaceDefault + "/" + name
579+
if err := storage.CustomResource.Storage.Create(ctx, key, &validCustomResource, &cr, 0, false); err != nil {
580+
t.Fatalf("error setting new custom resource (key: %s) %v: %v", key, validCustomResource, err)
581+
}
582+
583+
fetchObject := func(name string) (*unstructured.Unstructured, error) {
584+
gotObj, err := storage.CustomResource.Get(ctx, name, &metav1.GetOptions{})
585+
if err != nil {
586+
return nil, fmt.Errorf("error fetching custom resource %s: %v", name, err)
587+
}
588+
return gotObj.(*unstructured.Unstructured), nil
589+
}
590+
591+
applyPatch := func(labelName, labelValue string) rest.TransformFunc {
592+
return func(_ context.Context, _, currentObject runtime.Object) (objToUpdate runtime.Object, patchErr error) {
593+
o := currentObject.(metav1.Object)
594+
o.SetLabels(map[string]string{
595+
labelName: labelValue,
596+
})
597+
return currentObject, nil
598+
}
599+
}
600+
601+
errs := make(chan error, 1)
602+
rounds := 100
603+
go func() {
604+
// continuously submits a patch that updates a label and verifies the label update was effective
605+
labelName := "timestamp"
606+
for i := 0; i < rounds; i++ {
607+
expectedLabelValue := fmt.Sprint(i)
608+
update, err := fetchObject(name)
609+
if err != nil {
610+
errs <- err
611+
return
612+
}
613+
setNestedField(update, expectedLabelValue, "metadata", "labels", labelName)
614+
if _, _, err := storage.CustomResource.Update(ctx, name, rest.DefaultUpdatedObjectInfo(nil, applyPatch(labelName, fmt.Sprint(i))), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil {
615+
616+
errs <- fmt.Errorf("error updating custom resource label: %v", err)
617+
return
618+
}
619+
620+
gotObj, err := fetchObject(name)
621+
if err != nil {
622+
errs <- err
623+
return
624+
}
625+
gotLabelValue, _, err := unstructured.NestedString(gotObj.Object, "metadata", "labels", labelName)
626+
if err != nil {
627+
errs <- fmt.Errorf("error getting label %s of custom resource %s: %v", labelName, name, err)
628+
return
629+
}
630+
if gotLabelValue != expectedLabelValue {
631+
errs <- fmt.Errorf("wrong label value: expected: %s, got: %s", expectedLabelValue, gotLabelValue)
632+
return
633+
}
634+
}
635+
}()
636+
637+
replicas := int32(0)
638+
update := autoscalingv1.Scale{
639+
ObjectMeta: metav1.ObjectMeta{
640+
Name: name,
641+
},
642+
}
643+
// continuously submits a scale update without a resourceVersion for a monotonically increasing replica value
644+
// and verifies the scale update was effective
645+
for i := 0; i < rounds; i++ {
646+
select {
647+
case err := <-errs:
648+
t.Fatal(err)
649+
default:
650+
replicas++
651+
update.Spec.Replicas = replicas
652+
if _, _, err := storage.Scale.Update(ctx, update.Name, rest.DefaultUpdatedObjectInfo(&update), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil {
653+
t.Fatalf("error updating scale %v: %v", update, err)
654+
}
655+
656+
obj, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{})
657+
if err != nil {
658+
t.Fatalf("error fetching scale for %s: %v", name, err)
659+
}
660+
scale := obj.(*autoscalingv1.Scale)
661+
if scale.Spec.Replicas != replicas {
662+
t.Errorf("wrong replicas count: expected: %d got: %d", replicas, scale.Spec.Replicas)
663+
}
664+
}
665+
}
666+
}
667+
668+
func TestScaleUpdateWithResourceVersionWithConflicts(t *testing.T) {
669+
storage, server := newStorage(t)
670+
defer server.Terminate(t)
671+
defer storage.CustomResource.Store.DestroyFunc()
672+
673+
name := "foo"
674+
675+
var cr unstructured.Unstructured
676+
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
677+
key := "/noxus/" + metav1.NamespaceDefault + "/" + name
678+
if err := storage.CustomResource.Storage.Create(ctx, key, &validCustomResource, &cr, 0, false); err != nil {
679+
t.Fatalf("error setting new custom resource (key: %s) %v: %v", key, validCustomResource, err)
680+
}
681+
682+
obj, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{})
683+
if err != nil {
684+
t.Fatalf("error fetching scale for %s: %v", name, err)
685+
}
686+
scale, ok := obj.(*autoscalingv1.Scale)
687+
if !ok {
688+
t.Fatalf("%v is not of the type autoscalingv1.Scale", scale)
689+
}
690+
691+
replicas := int32(12)
692+
update := autoscalingv1.Scale{
693+
ObjectMeta: scale.ObjectMeta,
694+
Spec: autoscalingv1.ScaleSpec{
695+
Replicas: replicas,
696+
},
697+
}
698+
update.ResourceVersion = "1"
699+
700+
_, _, err = storage.Scale.Update(ctx, update.Name, rest.DefaultUpdatedObjectInfo(&update), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{})
701+
if err == nil {
702+
t.Fatal("expecting an update conflict error")
703+
}
704+
if !errors.IsConflict(err) {
705+
t.Fatalf("unexpected error, expecting an update conflict but got %v", err)
706+
}
707+
}
708+
529709
func setSpecReplicas(u *unstructured.Unstructured, replicas int64) {
530710
setNestedField(u, replicas, "spec", "replicas")
531711
}

0 commit comments

Comments
 (0)