Add BeginCreate and BeginUpdate REST hooks

These hooks return a "cleanup" func which is called when the top-level
operation completes, with an indicator of which result.

This is to enable much simpler handling of allocations in Service's REST
implementation, in particular.

Some discussion in https://github.com/kubernetes/kubernetes/pull/95967

This also adds tests for the almost totally untested Decorator,
AfterCreate, and AfterUpdate hooks.

Kubernetes-commit: 67c9761623052d147a58807caced1c89262fe30d
This commit is contained in:
Tim Hockin 2020-11-10 08:40:25 -08:00 committed by Kubernetes Publisher
parent f343cfc213
commit 9a5725b94a
2 changed files with 828 additions and 10 deletions

View File

@ -55,6 +55,9 @@ import (
// object. // object.
type ObjectFunc func(obj runtime.Object) error type ObjectFunc func(obj runtime.Object) error
// FinishFunc is a function returned by Begin hooks to complete an operation.
type FinishFunc func(ctx context.Context, success bool)
// GenericStore interface can be used for type assertions when we need to access the underlying strategies. // GenericStore interface can be used for type assertions when we need to access the underlying strategies.
type GenericStore interface { type GenericStore interface {
GetCreateStrategy() rest.RESTCreateStrategy GetCreateStrategy() rest.RESTCreateStrategy
@ -146,14 +149,27 @@ type Store struct {
// specific cases where storage of the value is not appropriate, since // specific cases where storage of the value is not appropriate, since
// they cannot be watched. // they cannot be watched.
Decorator ObjectFunc Decorator ObjectFunc
// CreateStrategy implements resource-specific behavior during creation. // CreateStrategy implements resource-specific behavior during creation.
CreateStrategy rest.RESTCreateStrategy CreateStrategy rest.RESTCreateStrategy
// BeginCreate is an optional hook that returns a "transaction-like"
// commit/revert function which will be called at the end of the operation,
// but before AfterCreate and Decorator, indicating via the argument
// whether the operation succeeded. If this returns an error, the function
// is not called. Almost nobody should use this hook.
BeginCreate func(ctx context.Context, obj runtime.Object, options *metav1.CreateOptions) (FinishFunc, error)
// AfterCreate implements a further operation to run after a resource is // AfterCreate implements a further operation to run after a resource is
// created and before it is decorated, optional. // created and before it is decorated, optional.
AfterCreate ObjectFunc AfterCreate ObjectFunc
// UpdateStrategy implements resource-specific behavior during updates. // UpdateStrategy implements resource-specific behavior during updates.
UpdateStrategy rest.RESTUpdateStrategy UpdateStrategy rest.RESTUpdateStrategy
// BeginUpdate is an optional hook that returns a "transaction-like"
// commit/revert function which will be called at the end of the operation,
// but before AfterUpdate and Decorator, indicating via the argument
// whether the operation succeeded. If this returns an error, the function
// is not called. Almost nobody should use this hook.
BeginUpdate func(ctx context.Context, obj, old runtime.Object, options *metav1.UpdateOptions) (FinishFunc, error)
// AfterUpdate implements a further operation to run after a resource is // AfterUpdate implements a further operation to run after a resource is
// updated and before it is decorated, optional. // updated and before it is decorated, optional.
AfterUpdate ObjectFunc AfterUpdate ObjectFunc
@ -171,9 +187,11 @@ type Store struct {
// If specified, this is checked in addition to standard finalizer, // If specified, this is checked in addition to standard finalizer,
// deletionTimestamp, and deletionGracePeriodSeconds checks. // deletionTimestamp, and deletionGracePeriodSeconds checks.
ShouldDeleteDuringUpdate func(ctx context.Context, key string, obj, existing runtime.Object) bool ShouldDeleteDuringUpdate func(ctx context.Context, key string, obj, existing runtime.Object) bool
// ExportStrategy implements resource-specific behavior during export, // ExportStrategy implements resource-specific behavior during export,
// optional. Exported objects are not decorated. // optional. Exported objects are not decorated.
ExportStrategy rest.RESTExportStrategy ExportStrategy rest.RESTExportStrategy
// TableConvertor is an optional interface for transforming items or lists // TableConvertor is an optional interface for transforming items or lists
// of items into tabular output. If unset, the default will be used. // of items into tabular output. If unset, the default will be used.
TableConvertor rest.TableConvertor TableConvertor rest.TableConvertor
@ -335,8 +353,24 @@ func (e *Store) ListPredicate(ctx context.Context, p storage.SelectionPredicate,
return list, storeerr.InterpretListError(err, qualifiedResource) return list, storeerr.InterpretListError(err, qualifiedResource)
} }
// finishNothing is a do-nothing FinishFunc.
func finishNothing(context.Context, bool) {}
// Create inserts a new item according to the unique key from the object. // Create inserts a new item according to the unique key from the object.
func (e *Store) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { func (e *Store) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
var finishCreate FinishFunc = finishNothing
if e.BeginCreate != nil {
fn, err := e.BeginCreate(ctx, obj, options)
if err != nil {
return nil, err
}
finishCreate = fn
defer func() {
finishCreate(ctx, false)
}()
}
if err := rest.BeforeCreate(e.CreateStrategy, ctx, obj); err != nil { if err := rest.BeforeCreate(e.CreateStrategy, ctx, obj); err != nil {
return nil, err return nil, err
} }
@ -381,6 +415,12 @@ func (e *Store) Create(ctx context.Context, obj runtime.Object, createValidation
} }
return nil, err return nil, err
} }
// The operation has succeeded. Call the finish function if there is one,
// and then make sure the defer doesn't call it again.
fn := finishCreate
finishCreate = finishNothing
fn(ctx, true)
if e.AfterCreate != nil { if e.AfterCreate != nil {
if err := e.AfterCreate(out); err != nil { if err := e.AfterCreate(out); err != nil {
return nil, err return nil, err
@ -500,6 +540,19 @@ func (e *Store) Update(ctx context.Context, name string, objInfo rest.UpdatedObj
doUnconditionalUpdate := newResourceVersion == 0 && e.UpdateStrategy.AllowUnconditionalUpdate() doUnconditionalUpdate := newResourceVersion == 0 && e.UpdateStrategy.AllowUnconditionalUpdate()
if existingResourceVersion == 0 { if existingResourceVersion == 0 {
var finishCreate FinishFunc = finishNothing
if e.BeginCreate != nil {
fn, err := e.BeginCreate(ctx, obj, newCreateOptionsFromUpdateOptions(options))
if err != nil {
return nil, nil, err
}
finishCreate = fn
defer func() {
finishCreate(ctx, false)
}()
}
creating = true creating = true
creatingObj = obj creatingObj = obj
if err := rest.BeforeCreate(e.CreateStrategy, ctx, obj); err != nil { if err := rest.BeforeCreate(e.CreateStrategy, ctx, obj); err != nil {
@ -517,6 +570,12 @@ func (e *Store) Update(ctx context.Context, name string, objInfo rest.UpdatedObj
return nil, nil, err return nil, nil, err
} }
// The operation has succeeded. Call the finish function if there is one,
// and then make sure the defer doesn't call it again.
fn := finishCreate
finishCreate = finishNothing
fn(ctx, true)
return obj, &ttl, nil return obj, &ttl, nil
} }
@ -544,6 +603,20 @@ func (e *Store) Update(ctx context.Context, name string, objInfo rest.UpdatedObj
return nil, nil, apierrors.NewConflict(qualifiedResource, name, fmt.Errorf(OptimisticLockErrorMsg)) return nil, nil, apierrors.NewConflict(qualifiedResource, name, fmt.Errorf(OptimisticLockErrorMsg))
} }
} }
var finishUpdate FinishFunc = finishNothing
if e.BeginUpdate != nil {
fn, err := e.BeginUpdate(ctx, obj, existing, options)
if err != nil {
return nil, nil, err
}
finishUpdate = fn
defer func() {
finishUpdate(ctx, false)
}()
}
if err := rest.BeforeUpdate(e.UpdateStrategy, ctx, obj, existing); err != nil { if err := rest.BeforeUpdate(e.UpdateStrategy, ctx, obj, existing); err != nil {
return nil, nil, err return nil, nil, err
} }
@ -564,6 +637,13 @@ func (e *Store) Update(ctx context.Context, name string, objInfo rest.UpdatedObj
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
// The operation has succeeded. Call the finish function if there is one,
// and then make sure the defer doesn't call it again.
fn := finishUpdate
finishUpdate = finishNothing
fn(ctx, true)
if int64(ttl) != res.TTL { if int64(ttl) != res.TTL {
return obj, &ttl, nil return obj, &ttl, nil
} }
@ -605,6 +685,17 @@ func (e *Store) Update(ctx context.Context, name string, objInfo rest.UpdatedObj
return out, creating, nil return out, creating, nil
} }
// This is a helper to convert UpdateOptions to CreateOptions for the
// create-on-update path.
func newCreateOptionsFromUpdateOptions(in *metav1.UpdateOptions) *metav1.CreateOptions {
co := &metav1.CreateOptions{
DryRun: in.DryRun,
FieldManager: in.FieldManager,
}
co.TypeMeta.SetGroupVersionKind(metav1.SchemeGroupVersion.WithKind("CreateOptions"))
return co
}
// Get retrieves the item from storage. // Get retrieves the item from storage.
func (e *Store) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { func (e *Store) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
obj := e.NewFunc() obj := e.NewFunc()

View File

@ -18,6 +18,7 @@ package registry
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"path" "path"
"reflect" "reflect"
@ -27,6 +28,7 @@ import (
"testing" "testing"
"time" "time"
fuzz "github.com/google/gofuzz"
apitesting "k8s.io/apimachinery/pkg/api/apitesting" apitesting "k8s.io/apimachinery/pkg/api/apitesting"
apiequality "k8s.io/apimachinery/pkg/api/equality" apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
@ -310,11 +312,6 @@ func TestStoreCreate(t *testing.T) {
// re-define delete strategy to have graceful delete capability // re-define delete strategy to have graceful delete capability
defaultDeleteStrategy := testRESTStrategy{scheme, names.SimpleNameGenerator, true, false, true} defaultDeleteStrategy := testRESTStrategy{scheme, names.SimpleNameGenerator, true, false, true}
registry.DeleteStrategy = testGracefulStrategy{defaultDeleteStrategy} registry.DeleteStrategy = testGracefulStrategy{defaultDeleteStrategy}
registry.Decorator = func(obj runtime.Object) error {
pod := obj.(*example.Pod)
pod.Status.Phase = example.PodPhase("Testing")
return nil
}
// create the object with denying admission // create the object with denying admission
_, err := registry.Create(testContext, podA, denyCreateValidation, &metav1.CreateOptions{}) _, err := registry.Create(testContext, podA, denyCreateValidation, &metav1.CreateOptions{})
@ -328,11 +325,6 @@ func TestStoreCreate(t *testing.T) {
t.Errorf("Unexpected error: %v", err) t.Errorf("Unexpected error: %v", err)
} }
// verify the decorator was called
if objA.(*example.Pod).Status.Phase != example.PodPhase("Testing") {
t.Errorf("Decorator was not called: %#v", objA)
}
// get the object // get the object
checkobj, err := registry.Get(testContext, podA.Name, &metav1.GetOptions{}) checkobj, err := registry.Get(testContext, podA.Name, &metav1.GetOptions{})
if err != nil { if err != nil {
@ -376,6 +368,245 @@ func TestStoreCreate(t *testing.T) {
} }
} }
func TestNewCreateOptionsFromUpdateOptions(t *testing.T) {
f := fuzz.New().NilChance(0.0).NumElements(1, 1)
// The goal here is to trigger when any changes are made to either
// CreateOptions or UpdateOptions types, so we can update the converter.
for i := 0; i < 20; i++ {
in := &metav1.UpdateOptions{}
f.Fuzz(in)
in.TypeMeta.SetGroupVersionKind(metav1.SchemeGroupVersion.WithKind("CreateOptions"))
out := newCreateOptionsFromUpdateOptions(in)
// This sequence is intending to elide type information, but produce an
// intermediate structure (map) that can be manually patched up to make
// the comparison work as needed.
// Convert both structs to maps of primitives.
inBytes, err := json.Marshal(in)
if err != nil {
t.Fatalf("failed to json.Marshal(in): %v", err)
}
outBytes, err := json.Marshal(out)
if err != nil {
t.Fatalf("failed to json.Marshal(out): %v", err)
}
inMap := map[string]interface{}{}
if err := json.Unmarshal(inBytes, &inMap); err != nil {
t.Fatalf("failed to json.Unmarshal(in): %v", err)
}
outMap := map[string]interface{}{}
if err := json.Unmarshal(outBytes, &outMap); err != nil {
t.Fatalf("failed to json.Unmarshal(out): %v", err)
}
// Patch the maps to handle any expected differences before we compare
// - none for now.
// Compare the results.
inBytes, err = json.Marshal(inMap)
if err != nil {
t.Fatalf("failed to json.Marshal(in): %v", err)
}
outBytes, err = json.Marshal(outMap)
if err != nil {
t.Fatalf("failed to json.Marshal(out): %v", err)
}
if i, o := string(inBytes), string(outBytes); i != o {
t.Fatalf("output != input:\n want: %s\n got: %s", i, o)
}
}
}
func TestStoreCreateHooks(t *testing.T) {
// To track which hooks were called in what order. Not all hooks can
// mutate the object.
var milestones []string
setAnn := func(obj runtime.Object, key string) {
pod := obj.(*example.Pod)
if pod.Annotations == nil {
pod.Annotations = make(map[string]string)
}
pod.Annotations[key] = "true"
}
mile := func(s string) {
milestones = append(milestones, s)
}
testCases := []struct {
name string
decorator ObjectFunc
beginCreate func(context.Context, runtime.Object, *metav1.CreateOptions) (FinishFunc, error)
afterCreate ObjectFunc
// the TTLFunc is an easy hook to force a failure
ttl func(obj runtime.Object, existing uint64, update bool) (uint64, error)
expectError bool
expectAnnotation string // to test object mutations
expectMilestones []string // to test sequence
}{{
name: "no hooks",
}, {
name: "Decorator mutation",
decorator: func(obj runtime.Object) error {
setAnn(obj, "DecoratorWasCalled")
return nil
},
expectAnnotation: "DecoratorWasCalled",
}, {
name: "AfterCreate mutation",
afterCreate: func(obj runtime.Object) error {
setAnn(obj, "AfterCreateWasCalled")
return nil
},
expectAnnotation: "AfterCreateWasCalled",
}, {
name: "BeginCreate mutation",
beginCreate: func(_ context.Context, obj runtime.Object, _ *metav1.CreateOptions) (FinishFunc, error) {
setAnn(obj, "BeginCreateWasCalled")
return func(context.Context, bool) {}, nil
},
expectAnnotation: "BeginCreateWasCalled",
}, {
name: "success ordering",
decorator: func(obj runtime.Object) error {
mile("Decorator")
return nil
},
afterCreate: func(obj runtime.Object) error {
mile("AfterCreate")
return nil
},
beginCreate: func(_ context.Context, obj runtime.Object, _ *metav1.CreateOptions) (FinishFunc, error) {
mile("BeginCreate")
return func(_ context.Context, success bool) {
mile(fmt.Sprintf("FinishCreate(%v)", success))
}, nil
},
expectMilestones: []string{"BeginCreate", "FinishCreate(true)", "AfterCreate", "Decorator"},
}, {
name: "fail ordering",
decorator: func(obj runtime.Object) error {
mile("Decorator")
return nil
},
afterCreate: func(obj runtime.Object) error {
mile("AfterCreate")
return nil
},
beginCreate: func(_ context.Context, obj runtime.Object, _ *metav1.CreateOptions) (FinishFunc, error) {
mile("BeginCreate")
return func(_ context.Context, success bool) {
mile(fmt.Sprintf("FinishCreate(%v)", success))
}, nil
},
ttl: func(_ runtime.Object, existing uint64, _ bool) (uint64, error) {
mile("TTLError")
return existing, fmt.Errorf("TTL fail")
},
expectMilestones: []string{"BeginCreate", "TTLError", "FinishCreate(false)"},
expectError: true,
}, {
name: "fail Decorator ordering",
expectError: true,
decorator: func(obj runtime.Object) error {
mile("Decorator")
return fmt.Errorf("decorator")
},
afterCreate: func(obj runtime.Object) error {
mile("AfterCreate")
return nil
},
beginCreate: func(_ context.Context, obj runtime.Object, _ *metav1.CreateOptions) (FinishFunc, error) {
mile("BeginCreate")
return func(_ context.Context, success bool) {
mile(fmt.Sprintf("FinishCreate(%v)", success))
}, nil
},
expectMilestones: []string{"BeginCreate", "FinishCreate(true)", "AfterCreate", "Decorator"},
}, {
name: "fail AfterCreate ordering",
expectError: true,
decorator: func(obj runtime.Object) error {
mile("Decorator")
return nil
},
afterCreate: func(obj runtime.Object) error {
mile("AfterCreate")
return fmt.Errorf("after")
},
beginCreate: func(_ context.Context, obj runtime.Object, _ *metav1.CreateOptions) (FinishFunc, error) {
mile("BeginCreate")
return func(_ context.Context, success bool) {
mile(fmt.Sprintf("FinishCreate(%v)", success))
}, nil
},
expectMilestones: []string{"BeginCreate", "FinishCreate(true)", "AfterCreate"},
}, {
name: "fail BeginCreate ordering",
expectError: true,
decorator: func(obj runtime.Object) error {
mile("Decorator")
return nil
},
afterCreate: func(obj runtime.Object) error {
mile("AfterCreate")
return nil
},
beginCreate: func(_ context.Context, obj runtime.Object, _ *metav1.CreateOptions) (FinishFunc, error) {
mile("BeginCreate")
return func(_ context.Context, success bool) {
mile(fmt.Sprintf("FinishCreate(%v)", success))
}, fmt.Errorf("begin")
},
expectMilestones: []string{"BeginCreate"},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
pod := &example.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "test"},
Spec: example.PodSpec{NodeName: "machine"},
}
testContext := genericapirequest.WithNamespace(genericapirequest.NewContext(), "test")
destroyFunc, registry := NewTestGenericStoreRegistry(t)
defer destroyFunc()
registry.Decorator = tc.decorator
registry.BeginCreate = tc.beginCreate
registry.AfterCreate = tc.afterCreate
registry.TTLFunc = tc.ttl
// create the object
milestones = nil
obj, err := registry.Create(testContext, pod, rest.ValidateAllObjectFunc, &metav1.CreateOptions{})
if err != nil && !tc.expectError {
t.Fatalf("Unexpected error: %v", err)
}
if err == nil && tc.expectError {
t.Fatalf("Unexpected success")
}
// verify the results
if tc.expectAnnotation != "" {
out := obj.(*example.Pod)
if v, found := out.Annotations[tc.expectAnnotation]; !found {
t.Errorf("Expected annotation %q not found", tc.expectAnnotation)
} else if v != "true" {
t.Errorf("Expected annotation %q has wrong value: %q", tc.expectAnnotation, v)
}
}
if tc.expectMilestones != nil {
if !reflect.DeepEqual(milestones, tc.expectMilestones) {
t.Errorf("Unexpected milestones: wanted %v, got %v", tc.expectMilestones, milestones)
}
}
})
}
}
func isQualifiedResource(err error, kind, group string) bool { func isQualifiedResource(err error, kind, group string) bool {
if err.(errors.APIStatus).Status().Details.Kind != kind || err.(errors.APIStatus).Status().Details.Group != group { if err.(errors.APIStatus).Status().Details.Kind != kind || err.(errors.APIStatus).Status().Details.Group != group {
return false return false
@ -531,6 +762,502 @@ func TestNoOpUpdates(t *testing.T) {
} }
} }
func TestStoreUpdateHooks(t *testing.T) {
// To track which hooks were called in what order. Not all hooks can
// mutate the object.
var milestones []string
setAnn := func(obj runtime.Object, key string) {
pod := obj.(*example.Pod)
if pod.Annotations == nil {
pod.Annotations = make(map[string]string)
}
pod.Annotations[key] = "true"
}
mile := func(s string) {
milestones = append(milestones, s)
}
testCases := []struct {
name string
decorator ObjectFunc
// create-on-update is tested elsewhere, but this proves non-use here
beginCreate func(context.Context, runtime.Object, *metav1.CreateOptions) (FinishFunc, error)
afterCreate ObjectFunc
beginUpdate func(context.Context, runtime.Object, runtime.Object, *metav1.UpdateOptions) (FinishFunc, error)
afterUpdate ObjectFunc
expectError bool
expectAnnotation string // to test object mutations
expectMilestones []string // to test sequence
}{{
name: "no hooks",
}, {
name: "Decorator mutation",
decorator: func(obj runtime.Object) error {
setAnn(obj, "DecoratorWasCalled")
return nil
},
expectAnnotation: "DecoratorWasCalled",
}, {
name: "AfterUpdate mutation",
afterUpdate: func(obj runtime.Object) error {
setAnn(obj, "AfterUpdateWasCalled")
return nil
},
expectAnnotation: "AfterUpdateWasCalled",
}, {
name: "BeginUpdate mutation",
beginUpdate: func(_ context.Context, obj, _ runtime.Object, _ *metav1.UpdateOptions) (FinishFunc, error) {
setAnn(obj, "BeginUpdateWasCalled")
return func(context.Context, bool) {}, nil
},
expectAnnotation: "BeginUpdateWasCalled",
}, {
name: "success ordering",
decorator: func(obj runtime.Object) error {
mile("Decorator")
return nil
},
afterCreate: func(obj runtime.Object) error {
mile("AfterCreate")
return nil
},
beginCreate: func(_ context.Context, obj runtime.Object, _ *metav1.CreateOptions) (FinishFunc, error) {
mile("BeginCreate")
return func(_ context.Context, success bool) {
mile(fmt.Sprintf("FinishCreate(%v)", success))
}, nil
},
afterUpdate: func(obj runtime.Object) error {
mile("AfterUpdate")
return nil
},
beginUpdate: func(_ context.Context, obj, _ runtime.Object, _ *metav1.UpdateOptions) (FinishFunc, error) {
mile("BeginUpdate")
return func(_ context.Context, success bool) {
mile(fmt.Sprintf("FinishUpdate(%v)", success))
}, nil
},
expectMilestones: []string{"BeginUpdate", "FinishUpdate(true)", "AfterUpdate", "Decorator"},
}, /* fail ordering is covered in TestStoreUpdateHooksInnerRetry */ {
name: "fail Decorator ordering",
expectError: true,
decorator: func(obj runtime.Object) error {
mile("Decorator")
return fmt.Errorf("decorator")
},
afterUpdate: func(obj runtime.Object) error {
mile("AfterUpdate")
return nil
},
beginUpdate: func(_ context.Context, obj, _ runtime.Object, _ *metav1.UpdateOptions) (FinishFunc, error) {
mile("BeginUpdate")
return func(_ context.Context, success bool) {
mile(fmt.Sprintf("FinishUpdate(%v)", success))
}, nil
},
expectMilestones: []string{"BeginUpdate", "FinishUpdate(true)", "AfterUpdate", "Decorator"},
}, {
name: "fail AfterUpdate ordering",
expectError: true,
decorator: func(obj runtime.Object) error {
mile("Decorator")
return nil
},
afterUpdate: func(obj runtime.Object) error {
mile("AfterUpdate")
return fmt.Errorf("after")
},
beginUpdate: func(_ context.Context, obj, _ runtime.Object, _ *metav1.UpdateOptions) (FinishFunc, error) {
mile("BeginUpdate")
return func(_ context.Context, success bool) {
mile(fmt.Sprintf("FinishUpdate(%v)", success))
}, nil
},
expectMilestones: []string{"BeginUpdate", "FinishUpdate(true)", "AfterUpdate"},
}, {
name: "fail BeginUpdate ordering",
expectError: true,
decorator: func(obj runtime.Object) error {
mile("Decorator")
return nil
},
afterUpdate: func(obj runtime.Object) error {
mile("AfterUpdate")
return nil
},
beginUpdate: func(_ context.Context, obj, _ runtime.Object, _ *metav1.UpdateOptions) (FinishFunc, error) {
mile("BeginUpdate")
return func(_ context.Context, success bool) {
mile(fmt.Sprintf("FinishUpdate(%v)", success))
}, fmt.Errorf("begin")
},
expectMilestones: []string{"BeginUpdate"},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
pod := &example.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "test"},
Spec: example.PodSpec{NodeName: "machine"},
}
testContext := genericapirequest.WithNamespace(genericapirequest.NewContext(), "test")
destroyFunc, registry := NewTestGenericStoreRegistry(t)
defer destroyFunc()
registry.BeginUpdate = tc.beginUpdate
registry.AfterUpdate = tc.afterUpdate
registry.BeginCreate = tc.beginCreate
registry.AfterCreate = tc.afterCreate
_, err := registry.Create(testContext, pod, rest.ValidateAllObjectFunc, &metav1.CreateOptions{})
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
milestones = nil
registry.Decorator = tc.decorator
obj, _, err := registry.Update(testContext, pod.Name, rest.DefaultUpdatedObjectInfo(pod), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{})
if err != nil && !tc.expectError {
t.Fatalf("Unexpected error: %v", err)
}
if err == nil && tc.expectError {
t.Fatalf("Unexpected success")
}
// verify the results
if tc.expectAnnotation != "" {
out := obj.(*example.Pod)
if v, found := out.Annotations[tc.expectAnnotation]; !found {
t.Errorf("Expected annotation %q not found", tc.expectAnnotation)
} else if v != "true" {
t.Errorf("Expected annotation %q has wrong value: %q", tc.expectAnnotation, v)
}
}
if tc.expectMilestones != nil {
if !reflect.DeepEqual(milestones, tc.expectMilestones) {
t.Errorf("Unexpected milestones: wanted %v, got %v", tc.expectMilestones, milestones)
}
}
})
}
}
func TestStoreCreateOnUpdateHooks(t *testing.T) {
// To track which hooks were called in what order. Not all hooks can
// mutate the object.
var milestones []string
mile := func(s string) {
milestones = append(milestones, s)
}
testCases := []struct {
name string
decorator ObjectFunc
beginCreate func(context.Context, runtime.Object, *metav1.CreateOptions) (FinishFunc, error)
afterCreate ObjectFunc
beginUpdate func(context.Context, runtime.Object, runtime.Object, *metav1.UpdateOptions) (FinishFunc, error)
afterUpdate ObjectFunc
// the TTLFunc is an easy hook to force a failure
ttl func(obj runtime.Object, existing uint64, update bool) (uint64, error)
expectError bool
expectMilestones []string // to test sequence
}{{
name: "no hooks",
}, {
name: "success ordering",
decorator: func(obj runtime.Object) error {
mile("Decorator")
return nil
},
afterCreate: func(obj runtime.Object) error {
mile("AfterCreate")
return nil
},
beginCreate: func(_ context.Context, obj runtime.Object, _ *metav1.CreateOptions) (FinishFunc, error) {
mile("BeginCreate")
return func(_ context.Context, success bool) {
mile(fmt.Sprintf("FinishCreate(%v)", success))
}, nil
},
afterUpdate: func(obj runtime.Object) error {
mile("AfterUpdate")
return nil
},
beginUpdate: func(_ context.Context, obj, _ runtime.Object, _ *metav1.UpdateOptions) (FinishFunc, error) {
mile("BeginUpdate")
return func(_ context.Context, success bool) {
mile(fmt.Sprintf("FinishUpdate(%v)", success))
}, nil
},
expectMilestones: []string{"BeginCreate", "FinishCreate(true)", "AfterCreate", "Decorator"},
}, {
name: "fail ordering",
decorator: func(obj runtime.Object) error {
mile("Decorator")
return nil
},
afterCreate: func(obj runtime.Object) error {
mile("AfterCreate")
return nil
},
beginCreate: func(_ context.Context, obj runtime.Object, _ *metav1.CreateOptions) (FinishFunc, error) {
mile("BeginCreate")
return func(_ context.Context, success bool) {
mile(fmt.Sprintf("FinishCreate(%v)", success))
}, nil
},
afterUpdate: func(obj runtime.Object) error {
mile("AfterUpdate")
return nil
},
beginUpdate: func(_ context.Context, obj, _ runtime.Object, _ *metav1.UpdateOptions) (FinishFunc, error) {
mile("BeginUpdate")
return func(_ context.Context, success bool) {
mile(fmt.Sprintf("FinishUpdate(%v)", success))
}, nil
},
ttl: func(_ runtime.Object, existing uint64, _ bool) (uint64, error) {
mile("TTLError")
return existing, fmt.Errorf("TTL fail")
},
expectMilestones: []string{"BeginCreate", "TTLError", "FinishCreate(false)"},
expectError: true,
}, {
name: "fail Decorator ordering",
expectError: true,
decorator: func(obj runtime.Object) error {
mile("Decorator")
return fmt.Errorf("decorator")
},
afterCreate: func(obj runtime.Object) error {
mile("AfterCreate")
return nil
},
beginCreate: func(_ context.Context, obj runtime.Object, _ *metav1.CreateOptions) (FinishFunc, error) {
mile("BeginCreate")
return func(_ context.Context, success bool) {
mile(fmt.Sprintf("FinishCreate(%v)", success))
}, nil
},
afterUpdate: func(obj runtime.Object) error {
mile("AfterUpdate")
return nil
},
beginUpdate: func(_ context.Context, obj, _ runtime.Object, _ *metav1.UpdateOptions) (FinishFunc, error) {
mile("BeginUpdate")
return func(_ context.Context, success bool) {
mile(fmt.Sprintf("FinishUpdate(%v)", success))
}, nil
},
expectMilestones: []string{"BeginCreate", "FinishCreate(true)", "AfterCreate", "Decorator"},
}, {
name: "fail AfterCreate ordering",
expectError: true,
decorator: func(obj runtime.Object) error {
mile("Decorator")
return nil
},
afterCreate: func(obj runtime.Object) error {
mile("AfterCreate")
return fmt.Errorf("after")
},
beginCreate: func(_ context.Context, obj runtime.Object, _ *metav1.CreateOptions) (FinishFunc, error) {
mile("BeginCreate")
return func(_ context.Context, success bool) {
mile(fmt.Sprintf("FinishCreate(%v)", success))
}, nil
},
afterUpdate: func(obj runtime.Object) error {
mile("AfterUpdate")
return nil
},
beginUpdate: func(_ context.Context, obj, _ runtime.Object, _ *metav1.UpdateOptions) (FinishFunc, error) {
mile("BeginUpdate")
return func(_ context.Context, success bool) {
mile(fmt.Sprintf("FinishUpdate(%v)", success))
}, nil
},
expectMilestones: []string{"BeginCreate", "FinishCreate(true)", "AfterCreate"},
}, {
name: "fail BeginCreate ordering",
expectError: true,
decorator: func(obj runtime.Object) error {
mile("Decorator")
return nil
},
afterCreate: func(obj runtime.Object) error {
mile("AfterCreate")
return nil
},
beginCreate: func(_ context.Context, obj runtime.Object, _ *metav1.CreateOptions) (FinishFunc, error) {
mile("BeginCreate")
return func(_ context.Context, success bool) {
mile(fmt.Sprintf("FinishCreate(%v)", success))
}, fmt.Errorf("begin")
},
afterUpdate: func(obj runtime.Object) error {
mile("AfterUpdate")
return nil
},
beginUpdate: func(_ context.Context, obj, _ runtime.Object, _ *metav1.UpdateOptions) (FinishFunc, error) {
mile("BeginUpdate")
return func(_ context.Context, success bool) {
mile(fmt.Sprintf("FinishUpdate(%v)", success))
}, nil
},
expectMilestones: []string{"BeginCreate"},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
pod := &example.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "test"},
Spec: example.PodSpec{NodeName: "machine"},
}
testContext := genericapirequest.WithNamespace(genericapirequest.NewContext(), "test")
destroyFunc, registry := NewTestGenericStoreRegistry(t)
defer destroyFunc()
registry.Decorator = tc.decorator
registry.UpdateStrategy.(*testRESTStrategy).allowCreateOnUpdate = true
registry.BeginUpdate = tc.beginUpdate
registry.AfterUpdate = tc.afterUpdate
registry.BeginCreate = tc.beginCreate
registry.AfterCreate = tc.afterCreate
registry.TTLFunc = tc.ttl
// NB: did not create it first.
milestones = nil
_, _, err := registry.Update(testContext, pod.Name, rest.DefaultUpdatedObjectInfo(pod), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{})
if err != nil && !tc.expectError {
t.Fatalf("Unexpected error: %v", err)
}
if err == nil && tc.expectError {
t.Fatalf("Unexpected success")
}
// verify the results
if tc.expectMilestones != nil {
if !reflect.DeepEqual(milestones, tc.expectMilestones) {
t.Errorf("Unexpected milestones: wanted %v, got %v", tc.expectMilestones, milestones)
}
}
})
}
}
func TestStoreUpdateHooksInnerRetry(t *testing.T) {
// To track which hooks were called in what order. Not all hooks can
// mutate the object.
var milestones []string
mile := func(s string) {
milestones = append(milestones, s)
}
ttlFailDone := false
ttlFailOnce := func(_ runtime.Object, existing uint64, _ bool) (uint64, error) {
if ttlFailDone {
mile("TTL")
return existing, nil
}
ttlFailDone = true
mile("TTLError")
return existing, fmt.Errorf("TTL fail")
}
ttlFailAlways := func(_ runtime.Object, existing uint64, _ bool) (uint64, error) {
mile("TTLError")
return existing, fmt.Errorf("TTL fail")
}
testCases := []struct {
name string
decorator func(runtime.Object) error
beginUpdate func(context.Context, runtime.Object, runtime.Object, *metav1.UpdateOptions) (FinishFunc, error)
afterUpdate func(runtime.Object) error
// the TTLFunc is an easy hook to force an inner-loop retry
ttl func(obj runtime.Object, existing uint64, update bool) (uint64, error)
expectError bool
expectMilestones []string // to test sequence
}{{
name: "inner retry success",
decorator: func(obj runtime.Object) error {
mile("Decorator")
return nil
},
afterUpdate: func(obj runtime.Object) error {
mile("AfterUpdate")
return nil
},
beginUpdate: func(_ context.Context, obj, _ runtime.Object, _ *metav1.UpdateOptions) (FinishFunc, error) {
mile("BeginUpdate")
return func(_ context.Context, success bool) {
mile(fmt.Sprintf("FinishUpdate(%v)", success))
}, nil
},
ttl: ttlFailOnce,
expectMilestones: []string{"BeginUpdate", "TTLError", "FinishUpdate(false)", "BeginUpdate", "TTL", "FinishUpdate(true)", "AfterUpdate", "Decorator"},
}, {
name: "inner retry fail",
decorator: func(obj runtime.Object) error {
mile("Decorator")
return nil
},
afterUpdate: func(obj runtime.Object) error {
mile("AfterUpdate")
return nil
},
beginUpdate: func(_ context.Context, obj, _ runtime.Object, _ *metav1.UpdateOptions) (FinishFunc, error) {
mile("BeginUpdate")
return func(_ context.Context, success bool) {
mile(fmt.Sprintf("FinishUpdate(%v)", success))
}, nil
},
ttl: ttlFailAlways,
expectError: true,
expectMilestones: []string{"BeginUpdate", "TTLError", "FinishUpdate(false)", "BeginUpdate", "TTLError", "FinishUpdate(false)"},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
pod := &example.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "test"},
Spec: example.PodSpec{NodeName: "machine"},
}
testContext := genericapirequest.WithNamespace(genericapirequest.NewContext(), "test")
destroyFunc, registry := NewTestGenericStoreRegistry(t)
defer destroyFunc()
registry.BeginUpdate = tc.beginUpdate
registry.AfterUpdate = tc.afterUpdate
created, err := registry.Create(testContext, pod, rest.ValidateAllObjectFunc, &metav1.CreateOptions{})
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
milestones = nil
registry.Decorator = tc.decorator
ttlFailDone = false
registry.TTLFunc = tc.ttl
registry.Storage.Storage = &staleGuaranteedUpdateStorage{Interface: registry.Storage.Storage, cachedObj: created}
_, _, err = registry.Update(testContext, pod.Name, rest.DefaultUpdatedObjectInfo(pod), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{})
if err != nil && !tc.expectError {
t.Fatalf("Unexpected error: %v", err)
}
if err == nil && tc.expectError {
t.Fatalf("Unexpected success")
}
// verify the results
if tc.expectMilestones != nil {
if !reflect.DeepEqual(milestones, tc.expectMilestones) {
t.Errorf("Unexpected milestones: wanted %v, got %v", tc.expectMilestones, milestones)
}
}
})
}
}
// TODO: Add a test to check no-op update if we have object with ResourceVersion // TODO: Add a test to check no-op update if we have object with ResourceVersion
// already stored in etcd. Currently there is no easy way to store object with // already stored in etcd. Currently there is no easy way to store object with
// ResourceVersion in etcd. // ResourceVersion in etcd.