func TestGuaranteedUpdateNoChange(t *testing.T) { fakeClient := tools.NewFakeEtcdClient(t) fakeClient.TestIndex = true helper := newEtcdHelper(fakeClient, codec, etcdtest.PathPrefix()) key := etcdtest.AddPrefix("/some/key") // Create a new node. fakeClient.ExpectNotFoundGet(key) obj := &TestResource{ObjectMeta: api.ObjectMeta{Name: "foo"}, Value: 1} err := helper.GuaranteedUpdate("/some/key", &TestResource{}, true, storage.SimpleUpdate(func(in runtime.Object) (runtime.Object, error) { return obj, nil })) if err != nil { t.Errorf("Unexpected error %#v", err) } // Update an existing node with the same data callbackCalled := false objUpdate := &TestResource{ObjectMeta: api.ObjectMeta{Name: "foo"}, Value: 1} err = helper.GuaranteedUpdate("/some/key", &TestResource{}, true, storage.SimpleUpdate(func(in runtime.Object) (runtime.Object, error) { fakeClient.Err = errors.New("should not be called") callbackCalled = true return objUpdate, nil })) if err != nil { t.Fatalf("Unexpected error %#v", err) } if !callbackCalled { t.Errorf("tryUpdate callback should have been called.") } }
// TestWatchFromZero tests that // - watch from 0 should sync up and grab the object added before // - watch from 0 is able to return events for objects whose previous version has been compacted // - watch from non-0 should just watch changes after given version func TestWatchFromZero(t *testing.T) { ctx, store, cluster := testSetup(t) defer cluster.Terminate(t) key, storedObj := testPropogateStore(ctx, t, store, &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "ns"}}) w, err := store.Watch(ctx, key, "0", storage.Everything) if err != nil { t.Fatalf("Watch failed: %v", err) } testCheckResult(t, 0, watch.Added, w, storedObj) w.Stop() // Update out := &api.Pod{} err = store.GuaranteedUpdate(ctx, key, out, true, nil, storage.SimpleUpdate( func(runtime.Object) (runtime.Object, error) { return &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "ns", Annotations: map[string]string{"a": "1"}}}, nil })) if err != nil { t.Fatalf("GuaranteedUpdate failed: %v", err) } // Make sure when we watch from 0 we receive an ADDED event w, err = store.Watch(ctx, key, "0", storage.Everything) if err != nil { t.Fatalf("Watch failed: %v", err) } testCheckResult(t, 1, watch.Added, w, out) w.Stop() // Update again out = &api.Pod{} err = store.GuaranteedUpdate(ctx, key, out, true, nil, storage.SimpleUpdate( func(runtime.Object) (runtime.Object, error) { return &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "ns"}}, nil })) if err != nil { t.Fatalf("GuaranteedUpdate failed: %v", err) } // Compact previous versions revToCompact, err := strconv.Atoi(out.ResourceVersion) if err != nil { t.Fatalf("Error converting %q to an int: %v", storedObj.ResourceVersion, err) } _, err = cluster.RandClient().Compact(ctx, int64(revToCompact), clientv3.WithCompactPhysical()) if err != nil { t.Fatalf("Error compacting: %v", err) } // Make sure we can still watch from 0 and receive an ADDED event w, err = store.Watch(ctx, key, "0", storage.Everything) if err != nil { t.Fatalf("Watch failed: %v", err) } testCheckResult(t, 2, watch.Added, w, out) }
func TestGuaranteedUpdate(t *testing.T) { fakeClient := tools.NewFakeEtcdClient(t) fakeClient.TestIndex = true helper := newEtcdHelper(fakeClient, codec, etcdtest.PathPrefix()) key := etcdtest.AddPrefix("/some/key") // Create a new node. fakeClient.ExpectNotFoundGet(key) obj := &TestResource{ObjectMeta: api.ObjectMeta{Name: "foo"}, Value: 1} err := helper.GuaranteedUpdate("/some/key", &TestResource{}, true, storage.SimpleUpdate(func(in runtime.Object) (runtime.Object, error) { return obj, nil })) if err != nil { t.Errorf("Unexpected error %#v", err) } data, err := codec.Encode(obj) if err != nil { t.Errorf("Unexpected error %#v", err) } expect := string(data) got := fakeClient.Data[key].R.Node.Value if expect != got { t.Errorf("Wanted %v, got %v", expect, got) } // Update an existing node. callbackCalled := false objUpdate := &TestResource{ObjectMeta: api.ObjectMeta{Name: "foo"}, Value: 2} err = helper.GuaranteedUpdate("/some/key", &TestResource{}, true, storage.SimpleUpdate(func(in runtime.Object) (runtime.Object, error) { callbackCalled = true if in.(*TestResource).Value != 1 { t.Errorf("Callback input was not current set value") } return objUpdate, nil })) if err != nil { t.Errorf("Unexpected error %#v", err) } data, err = codec.Encode(objUpdate) if err != nil { t.Errorf("Unexpected error %#v", err) } expect = string(data) got = fakeClient.Data[key].R.Node.Value if expect != got { t.Errorf("Wanted %v, got %v", expect, got) } if !callbackCalled { t.Errorf("tryUpdate callback should have been called.") } }
func TestGuaranteedUpdateKeyNotFound(t *testing.T) { _, codec := testScheme(t) server := etcdtesting.NewEtcdTestClientServer(t) defer server.Terminate(t) key := etcdtest.AddPrefix("/some/key") helper := newEtcdHelper(server.Client, codec, key) // Create a new node. obj := &storagetesting.TestResource{ObjectMeta: api.ObjectMeta{Name: "foo"}, Value: 1} f := storage.SimpleUpdate(func(in runtime.Object) (runtime.Object, error) { return obj, nil }) ignoreNotFound := false err := helper.GuaranteedUpdate(context.TODO(), key, &storagetesting.TestResource{}, ignoreNotFound, nil, f) if err == nil { t.Errorf("Expected error for key not found.") } ignoreNotFound = true err = helper.GuaranteedUpdate(context.TODO(), key, &storagetesting.TestResource{}, ignoreNotFound, nil, f) if err != nil { t.Errorf("Unexpected error %v.", err) } }
func TestGuaranteedUpdateKeyNotFound(t *testing.T) { fakeClient := tools.NewFakeEtcdClient(t) fakeClient.TestIndex = true helper := newEtcdHelper(fakeClient, codec, etcdtest.PathPrefix()) key := etcdtest.AddPrefix("/some/key") // Create a new node. fakeClient.ExpectNotFoundGet(key) obj := &storagetesting.TestResource{ObjectMeta: api.ObjectMeta{Name: "foo"}, Value: 1} f := storage.SimpleUpdate(func(in runtime.Object) (runtime.Object, error) { return obj, nil }) ignoreNotFound := false err := helper.GuaranteedUpdate(context.TODO(), "/some/key", &storagetesting.TestResource{}, ignoreNotFound, f) if err == nil { t.Errorf("Expected error for key not found.") } ignoreNotFound = true err = helper.GuaranteedUpdate(context.TODO(), "/some/key", &storagetesting.TestResource{}, ignoreNotFound, f) if err != nil { t.Errorf("Unexpected error %v.", err) } }
// setPodHostAndAnnotations sets the given pod's host to 'machine' iff it was previously 'oldMachine' and merges // the provided annotations with those of the pod. // Returns the current state of the pod, or an error. func (r *BindingREST) setPodHostAndAnnotations(ctx api.Context, podID, oldMachine, machine string, annotations map[string]string) (finalPod *api.Pod, err error) { podKey, err := r.store.KeyFunc(ctx, podID) if err != nil { return nil, err } err = r.store.Storage.GuaranteedUpdate(podKey, &api.Pod{}, false, storage.SimpleUpdate(func(obj runtime.Object) (runtime.Object, error) { pod, ok := obj.(*api.Pod) if !ok { return nil, fmt.Errorf("unexpected object: %#v", obj) } if pod.DeletionTimestamp != nil { return nil, fmt.Errorf("pod %s is being deleted, cannot be assigned to a host", pod.Name) } if pod.Spec.NodeName != oldMachine { return nil, fmt.Errorf("pod %v is already assigned to node %q", pod.Name, pod.Spec.NodeName) } pod.Spec.NodeName = machine if pod.Annotations == nil { pod.Annotations = make(map[string]string) } for k, v := range annotations { pod.Annotations[k] = v } finalPod = pod return pod, nil })) return finalPod, err }
// CreateOrUpdate attempts to update the current etcd state with the provided // allocation. func (e *Etcd) CreateOrUpdate(snapshot *api.RangeAllocation) error { e.lock.Lock() defer e.lock.Unlock() last := "" err := e.storage.GuaranteedUpdate(context.TODO(), e.baseKey, &api.RangeAllocation{}, true, nil, storage.SimpleUpdate(func(input runtime.Object) (output runtime.Object, err error) { existing := input.(*api.RangeAllocation) switch { case len(snapshot.ResourceVersion) != 0 && len(existing.ResourceVersion) != 0: if snapshot.ResourceVersion != existing.ResourceVersion { return nil, k8serr.NewConflict(e.resource, "", fmt.Errorf("the provided resource version does not match")) } case len(existing.ResourceVersion) != 0: return nil, k8serr.NewConflict(e.resource, "", fmt.Errorf("another caller has already initialized the resource")) } last = snapshot.ResourceVersion return snapshot, nil }), ) if err != nil { return storeerr.InterpretUpdateError(err, e.resource, "") } err = e.alloc.Restore(snapshot.Range, snapshot.Data) if err == nil { e.last = last } return err }
func TestGuaranteedUpdateWithConflict(t *testing.T) { ctx, store, cluster := testSetup(t) defer cluster.Terminate(t) key, _ := testPropogateStore(t, store, ctx, &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}) errChan := make(chan error, 1) var firstToFinish sync.WaitGroup var secondToEnter sync.WaitGroup firstToFinish.Add(1) secondToEnter.Add(1) go func() { err := store.GuaranteedUpdate(ctx, key, &api.Pod{}, false, nil, storage.SimpleUpdate(func(obj runtime.Object) (runtime.Object, error) { pod := obj.(*api.Pod) pod.Name = "foo-1" secondToEnter.Wait() return pod, nil })) firstToFinish.Done() errChan <- err }() updateCount := 0 err := store.GuaranteedUpdate(ctx, key, &api.Pod{}, false, nil, storage.SimpleUpdate(func(obj runtime.Object) (runtime.Object, error) { if updateCount == 0 { secondToEnter.Done() firstToFinish.Wait() } updateCount++ pod := obj.(*api.Pod) pod.Name = "foo-2" return pod, nil })) if err != nil { t.Fatalf("Second GuaranteedUpdate error %#v", err) } if err := <-errChan; err != nil { t.Fatalf("First GuaranteedUpdate error %#v", err) } if updateCount != 2 { t.Errorf("Should have conflict and called update func twice") } }
func TestGuaranteedUpdate_CreateCollision(t *testing.T) { fakeClient := tools.NewFakeEtcdClient(t) fakeClient.TestIndex = true helper := newEtcdHelper(fakeClient, codec, etcdtest.PathPrefix()) key := etcdtest.AddPrefix("/some/key") fakeClient.ExpectNotFoundGet(key) const concurrency = 10 var wgDone sync.WaitGroup var wgForceCollision sync.WaitGroup wgDone.Add(concurrency) wgForceCollision.Add(concurrency) for i := 0; i < concurrency; i++ { // Increment TestResource.Value by 1 go func() { defer wgDone.Done() firstCall := true err := helper.GuaranteedUpdate("/some/key", &TestResource{}, true, storage.SimpleUpdate(func(in runtime.Object) (runtime.Object, error) { defer func() { firstCall = false }() if firstCall { // Force collision by joining all concurrent GuaranteedUpdate operations here. wgForceCollision.Done() wgForceCollision.Wait() } currValue := in.(*TestResource).Value obj := &TestResource{ObjectMeta: api.ObjectMeta{Name: "foo"}, Value: currValue + 1} return obj, nil })) if err != nil { t.Errorf("Unexpected error %#v", err) } }() } wgDone.Wait() // Check that stored TestResource has received all updates. body := fakeClient.Data[key].R.Node.Value stored := &TestResource{} if err := codec.DecodeInto([]byte(body), stored); err != nil { t.Errorf("Error decoding stored value: %v", body) } if stored.Value != concurrency { t.Errorf("Some of the writes were lost. Stored value: %d", stored.Value) } }
func TestWatchError(t *testing.T) { cluster := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1}) defer cluster.Terminate(t) invalidStore := newStore(cluster.RandClient(), false, &testCodec{testapi.Default.Codec()}, "") ctx := context.Background() w, err := invalidStore.Watch(ctx, "/abc", "0", storage.Everything) if err != nil { t.Fatalf("Watch failed: %v", err) } validStore := newStore(cluster.RandClient(), false, testapi.Default.Codec(), "") validStore.GuaranteedUpdate(ctx, "/abc", &api.Pod{}, true, nil, storage.SimpleUpdate( func(runtime.Object) (runtime.Object, error) { return &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}, nil })) testCheckEventType(t, watch.Error, w) }
// TestWatchFromNoneZero tests that // - watch from non-0 should just watch changes after given version func TestWatchFromNoneZero(t *testing.T) { ctx, store, cluster := testSetup(t) defer cluster.Terminate(t) key, storedObj := testPropogateStore(ctx, t, store, &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}) w, err := store.Watch(ctx, key, storedObj.ResourceVersion, storage.Everything) if err != nil { t.Fatalf("Watch failed: %v", err) } out := &api.Pod{} store.GuaranteedUpdate(ctx, key, out, true, nil, storage.SimpleUpdate( func(runtime.Object) (runtime.Object, error) { return &api.Pod{ObjectMeta: api.ObjectMeta{Name: "bar"}}, err })) testCheckResult(t, 0, watch.Modified, w, out) }
// this functions need to be kept synced with updateForGracefulDeletionAndFinalizers. func (e *Store) updateForGracefulDeletion(ctx api.Context, name, key string, options *api.DeleteOptions, preconditions storage.Preconditions, in runtime.Object) (err error, ignoreNotFound, deleteImmediately bool, out, lastExisting runtime.Object) { lastGraceful := int64(0) out = e.NewFunc() err = e.Storage.GuaranteedUpdate( ctx, key, out, false, &preconditions, storage.SimpleUpdate(func(existing runtime.Object) (runtime.Object, error) { graceful, pendingGraceful, err := rest.BeforeDelete(e.DeleteStrategy, ctx, existing, options) if err != nil { return nil, err } if pendingGraceful { return nil, errAlreadyDeleting } if !graceful { return nil, errDeleteNow } lastGraceful = *options.GracePeriodSeconds lastExisting = existing return existing, nil }), ) switch err { case nil: if lastGraceful > 0 { return nil, false, false, out, lastExisting } // If we are here, the registry supports grace period mechanism and // we are intentionally delete gracelessly. In this case, we may // enter a race with other k8s components. If other component wins // the race, the object will not be found, and we should tolerate // the NotFound error. See // https://github.com/kubernetes/kubernetes/issues/19403 for // details. return nil, true, true, out, lastExisting case errDeleteNow: // we've updated the object to have a zero grace period, or it's already at 0, so // we should fall through and truly delete the object. return nil, false, true, out, lastExisting case errAlreadyDeleting: out, err = e.finalizeDelete(in, true) return err, false, false, out, lastExisting default: return storeerr.InterpretUpdateError(err, e.QualifiedResource, name), false, false, out, lastExisting } }
func (r *RollbackREST) setDeploymentRollback(ctx api.Context, deploymentID string, config *extensions.RollbackConfig, annotations map[string]string) (finalDeployment *extensions.Deployment, err error) { dKey, err := r.store.KeyFunc(ctx, deploymentID) if err != nil { return nil, err } err = r.store.Storage.GuaranteedUpdate(ctx, dKey, &extensions.Deployment{}, false, storage.SimpleUpdate(func(obj runtime.Object) (runtime.Object, error) { d, ok := obj.(*extensions.Deployment) if !ok { return nil, fmt.Errorf("unexpected object: %#v", obj) } if d.Annotations == nil { d.Annotations = make(map[string]string) } for k, v := range annotations { d.Annotations[k] = v } d.Spec.RollbackTo = config finalDeployment = d return d, nil })) return finalDeployment, err }
// tryUpdate performs a read-update to persist the latest snapshot state of allocation. func (e *Etcd) tryUpdate(fn func() error) error { err := e.storage.GuaranteedUpdate(context.TODO(), e.baseKey, &api.RangeAllocation{}, true, nil, storage.SimpleUpdate(func(input runtime.Object) (output runtime.Object, err error) { existing := input.(*api.RangeAllocation) if len(existing.ResourceVersion) == 0 { return nil, fmt.Errorf("cannot allocate resources of type %s at this time", e.resource.String()) } if existing.ResourceVersion != e.last { if err := e.alloc.Restore(existing.Range, existing.Data); err != nil { return nil, err } if err := fn(); err != nil { return nil, err } } e.last = existing.ResourceVersion rangeSpec, data := e.alloc.Snapshot() existing.Range = rangeSpec existing.Data = data return existing, nil }), ) return storeerr.InterpretUpdateError(err, e.resource, "") }
func TestGuaranteedUpdate_CreateCollision(t *testing.T) { _, codec := testScheme(t) server := etcdtesting.NewEtcdTestClientServer(t) defer server.Terminate(t) key := etcdtest.AddPrefix("/some/key") helper := newEtcdHelper(server.Client, codec, etcdtest.PathPrefix()) const concurrency = 10 var wgDone sync.WaitGroup var wgForceCollision sync.WaitGroup wgDone.Add(concurrency) wgForceCollision.Add(concurrency) for i := 0; i < concurrency; i++ { // Increment storagetesting.TestResource.Value by 1 go func() { defer wgDone.Done() firstCall := true err := helper.GuaranteedUpdate(context.TODO(), key, &storagetesting.TestResource{}, true, nil, storage.SimpleUpdate(func(in runtime.Object) (runtime.Object, error) { defer func() { firstCall = false }() if firstCall { // Force collision by joining all concurrent GuaranteedUpdate operations here. wgForceCollision.Done() wgForceCollision.Wait() } currValue := in.(*storagetesting.TestResource).Value obj := &storagetesting.TestResource{ObjectMeta: api.ObjectMeta{Name: "foo"}, Value: currValue + 1} return obj, nil })) if err != nil { t.Errorf("Unexpected error %#v", err) } }() } wgDone.Wait() stored := &storagetesting.TestResource{} err := helper.Get(context.TODO(), key, stored, false) if err != nil { t.Errorf("Unexpected error %#v", stored) } if stored.Value != concurrency { t.Errorf("Some of the writes were lost. Stored value: %d", stored.Value) } }
func TestGuaranteedUpdate(t *testing.T) { ctx, store, cluster := testSetup(t) defer cluster.Terminate(t) key, storeObj := testPropogateStore(t, store, ctx, &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", UID: "A"}}) tests := []struct { key string name string ignoreNotFound bool precondition *storage.Preconditions expectNotFoundErr bool expectInvalidObjErr bool expectNoUpdate bool }{{ // GuaranteedUpdate on non-existing key with ignoreNotFound=false key: "/non-existing", ignoreNotFound: false, precondition: nil, expectNotFoundErr: true, expectInvalidObjErr: false, expectNoUpdate: false, }, { // GuaranteedUpdate on non-existing key with ignoreNotFound=true key: "/non-existing", ignoreNotFound: true, precondition: nil, expectNotFoundErr: false, expectInvalidObjErr: false, expectNoUpdate: false, }, { // GuaranteedUpdate on existing key key: key, ignoreNotFound: false, precondition: nil, expectNotFoundErr: false, expectInvalidObjErr: false, expectNoUpdate: false, }, { // GuaranteedUpdate with same data key: key, ignoreNotFound: false, precondition: nil, expectNotFoundErr: false, expectInvalidObjErr: false, expectNoUpdate: true, }, { // GuaranteedUpdate with UID match key: key, ignoreNotFound: false, precondition: storage.NewUIDPreconditions("A"), expectNotFoundErr: false, expectInvalidObjErr: false, expectNoUpdate: true, }, { // GuaranteedUpdate with UID mismatch key: key, ignoreNotFound: false, precondition: storage.NewUIDPreconditions("B"), expectNotFoundErr: false, expectInvalidObjErr: true, expectNoUpdate: true, }} for i, tt := range tests { out := &api.Pod{} name := fmt.Sprintf("foo-%d", i) if tt.expectNoUpdate { name = storeObj.Name } version := storeObj.ResourceVersion err := store.GuaranteedUpdate(ctx, tt.key, out, tt.ignoreNotFound, tt.precondition, storage.SimpleUpdate(func(obj runtime.Object) (runtime.Object, error) { if tt.expectNotFoundErr && tt.ignoreNotFound { if pod := obj.(*api.Pod); pod.Name != "" { t.Errorf("#%d: expecting zero value, but get=%#v", i, pod) } } pod := *storeObj pod.Name = name return &pod, nil })) if tt.expectNotFoundErr { if err == nil || !storage.IsNotFound(err) { t.Errorf("#%d: expecting not found error, but get: %v", i, err) } continue } if tt.expectInvalidObjErr { if err == nil || !storage.IsInvalidObj(err) { t.Errorf("#%d: expecting invalid UID error, but get: %s", i, err) } continue } if err != nil { t.Fatalf("GuaranteedUpdate failed: %v", err) } if out.ObjectMeta.Name != name { t.Errorf("#%d: pod name want=%s, get=%s", i, name, out.ObjectMeta.Name) } switch tt.expectNoUpdate { case true: if version != out.ResourceVersion { t.Errorf("#%d: expect no version change, before=%s, after=%s", i, version, out.ResourceVersion) } case false: if version == out.ResourceVersion { t.Errorf("#%d: expect version change, but get the same version=%s", i, version) } } storeObj = out } }
// It tests that // - first occurrence of objects should notify Add event // - update should trigger Modified event // - update that gets filtered should trigger Deleted event func testWatch(t *testing.T, recursive bool) { ctx, store, cluster := testSetup(t) defer cluster.Terminate(t) podFoo := &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}} podBar := &api.Pod{ObjectMeta: api.ObjectMeta{Name: "bar"}} tests := []struct { key string pred storage.SelectionPredicate watchTests []*testWatchStruct }{{ // create a key key: "/somekey-1", watchTests: []*testWatchStruct{{podFoo, true, watch.Added}}, pred: storage.Everything, }, { // create a key but obj gets filtered. Then update it with unfiltered obj key: "/somekey-3", watchTests: []*testWatchStruct{{podFoo, false, ""}, {podBar, true, watch.Added}}, pred: storage.SelectionPredicate{ Label: labels.Everything(), Field: fields.ParseSelectorOrDie("metadata.name=bar"), GetAttrs: func(obj runtime.Object) (labels.Set, fields.Set, error) { pod := obj.(*api.Pod) return nil, fields.Set{"metadata.name": pod.Name}, nil }, }, }, { // update key: "/somekey-4", watchTests: []*testWatchStruct{{podFoo, true, watch.Added}, {podBar, true, watch.Modified}}, pred: storage.Everything, }, { // delete because of being filtered key: "/somekey-5", watchTests: []*testWatchStruct{{podFoo, true, watch.Added}, {podBar, true, watch.Deleted}}, pred: storage.SelectionPredicate{ Label: labels.Everything(), Field: fields.ParseSelectorOrDie("metadata.name!=bar"), GetAttrs: func(obj runtime.Object) (labels.Set, fields.Set, error) { pod := obj.(*api.Pod) return nil, fields.Set{"metadata.name": pod.Name}, nil }, }, }} for i, tt := range tests { w, err := store.watch(ctx, tt.key, "0", tt.pred, recursive) if err != nil { t.Fatalf("Watch failed: %v", err) } var prevObj *api.Pod for _, watchTest := range tt.watchTests { out := &api.Pod{} key := tt.key if recursive { key = key + "/item" } err := store.GuaranteedUpdate(ctx, key, out, true, nil, storage.SimpleUpdate( func(runtime.Object) (runtime.Object, error) { return watchTest.obj, nil })) if err != nil { t.Fatalf("GuaranteedUpdate failed: %v", err) } if watchTest.expectEvent { expectObj := out if watchTest.watchType == watch.Deleted { expectObj = prevObj expectObj.ResourceVersion = out.ResourceVersion } testCheckResult(t, i, watchTest.watchType, w, expectObj) } prevObj = out } w.Stop() testCheckStop(t, i, w) } }
// It tests that // - first occurrence of objects should notify Add event // - update should trigger Modified event // - update that gets filtered should trigger Deleted event func testWatch(t *testing.T, recursive bool) { podFoo := &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}} podBar := &api.Pod{ObjectMeta: api.ObjectMeta{Name: "bar"}} tests := []struct { key string filter func(runtime.Object) bool trigger func() []storage.MatchValue watchTests []*testWatchStruct }{{ // create a key key: "/somekey-1", watchTests: []*testWatchStruct{{podFoo, true, watch.Added}}, filter: storage.EverythingFunc, trigger: storage.NoTriggerFunc, }, { // create a key but obj gets filtered key: "/somekey-2", watchTests: []*testWatchStruct{{podFoo, false, ""}}, filter: func(runtime.Object) bool { return false }, trigger: storage.NoTriggerFunc, }, { // create a key but obj gets filtered. Then update it with unfiltered obj key: "/somekey-3", watchTests: []*testWatchStruct{{podFoo, false, ""}, {podBar, true, watch.Added}}, filter: func(obj runtime.Object) bool { pod := obj.(*api.Pod) return pod.Name == "bar" }, trigger: storage.NoTriggerFunc, }, { // update key: "/somekey-4", watchTests: []*testWatchStruct{{podFoo, true, watch.Added}, {podBar, true, watch.Modified}}, filter: storage.EverythingFunc, trigger: storage.NoTriggerFunc, }, { // delete because of being filtered key: "/somekey-5", watchTests: []*testWatchStruct{{podFoo, true, watch.Added}, {podBar, true, watch.Deleted}}, filter: func(obj runtime.Object) bool { pod := obj.(*api.Pod) return pod.Name != "bar" }, trigger: storage.NoTriggerFunc, }} for i, tt := range tests { ctx, store, cluster := testSetup(t) filter := storage.NewSimpleFilter(tt.filter, tt.trigger) w, err := store.watch(ctx, tt.key, "0", filter, recursive) if err != nil { t.Fatalf("Watch failed: %v", err) } var prevObj *api.Pod for _, watchTest := range tt.watchTests { out := &api.Pod{} key := tt.key if recursive { key = key + "/item" } err := store.GuaranteedUpdate(ctx, key, out, true, nil, storage.SimpleUpdate( func(runtime.Object) (runtime.Object, error) { return watchTest.obj, nil })) if err != nil { t.Fatalf("GuaranteedUpdate failed: %v", err) } if watchTest.expectEvent { expectObj := out if watchTest.watchType == watch.Deleted { expectObj = prevObj expectObj.ResourceVersion = out.ResourceVersion } testCheckResult(t, i, watchTest.watchType, w, expectObj) } prevObj = out } w.Stop() testCheckStop(t, i, w) cluster.Terminate(t) } }
func TestGuaranteedUpdateNoChange(t *testing.T) { _, codec := testScheme(t) server := etcdtesting.NewEtcdTestClientServer(t) defer server.Terminate(t) key := etcdtest.AddPrefix("/some/key") helper := newEtcdHelper(server.Client, codec, key) obj := &storagetesting.TestResource{ObjectMeta: api.ObjectMeta{Name: "foo"}, Value: 1} err := helper.GuaranteedUpdate(context.TODO(), key, &storagetesting.TestResource{}, true, nil, storage.SimpleUpdate(func(in runtime.Object) (runtime.Object, error) { return obj, nil })) if err != nil { t.Errorf("Unexpected error %#v", err) } // Update an existing node with the same data callbackCalled := false objUpdate := &storagetesting.TestResource{ObjectMeta: api.ObjectMeta{Name: "foo"}, Value: 1} err = helper.GuaranteedUpdate(context.TODO(), key, &storagetesting.TestResource{}, true, nil, storage.SimpleUpdate(func(in runtime.Object) (runtime.Object, error) { callbackCalled = true return objUpdate, nil })) if err != nil { t.Fatalf("Unexpected error %#v", err) } if !callbackCalled { t.Errorf("tryUpdate callback should have been called.") } }
func TestGuaranteedUpdate(t *testing.T) { _, codec := testScheme(t) server := etcdtesting.NewEtcdTestClientServer(t) defer server.Terminate(t) key := etcdtest.AddPrefix("/some/key") helper := newEtcdHelper(server.Client, codec, key) obj := &storagetesting.TestResource{ObjectMeta: api.ObjectMeta{Name: "foo"}, Value: 1} err := helper.GuaranteedUpdate(context.TODO(), key, &storagetesting.TestResource{}, true, nil, storage.SimpleUpdate(func(in runtime.Object) (runtime.Object, error) { return obj, nil })) if err != nil { t.Errorf("Unexpected error %#v", err) } // Update an existing node. callbackCalled := false objUpdate := &storagetesting.TestResource{ObjectMeta: api.ObjectMeta{Name: "foo"}, Value: 2} err = helper.GuaranteedUpdate(context.TODO(), key, &storagetesting.TestResource{}, true, nil, storage.SimpleUpdate(func(in runtime.Object) (runtime.Object, error) { callbackCalled = true if in.(*storagetesting.TestResource).Value != 1 { t.Errorf("Callback input was not current set value") } return objUpdate, nil })) if err != nil { t.Errorf("unexpected error: %v", err) } objCheck := &storagetesting.TestResource{} err = helper.Get(context.TODO(), key, objCheck, false) if err != nil { t.Errorf("Unexpected error %#v", err) } if objCheck.Value != 2 { t.Errorf("Value should have been 2 but got %v", objCheck.Value) } if !callbackCalled { t.Errorf("tryUpdate callback should have been called.") } }
// Delete enforces life-cycle rules for namespace termination func (r *REST) Delete(ctx api.Context, name string, options *api.DeleteOptions) (runtime.Object, error) { nsObj, err := r.Get(ctx, name) if err != nil { return nil, err } namespace := nsObj.(*api.Namespace) // Ensure we have a UID precondition if options == nil { options = api.NewDeleteOptions(0) } if options.Preconditions == nil { options.Preconditions = &api.Preconditions{} } if options.Preconditions.UID == nil { options.Preconditions.UID = &namespace.UID } else if *options.Preconditions.UID != namespace.UID { err = apierrors.NewConflict( api.Resource("namespaces"), name, fmt.Errorf("Precondition failed: UID in precondition: %v, UID in object meta: %v", *options.Preconditions.UID, namespace.UID), ) return nil, err } // upon first request to delete, we switch the phase to start namespace termination // TODO: enhance graceful deletion's calls to DeleteStrategy to allow phase change and finalizer patterns if namespace.DeletionTimestamp.IsZero() { key, err := r.Store.KeyFunc(ctx, name) if err != nil { return nil, err } preconditions := storage.Preconditions{UID: options.Preconditions.UID} out := r.Store.NewFunc() err = r.Store.Storage.GuaranteedUpdate( ctx, key, out, false, &preconditions, storage.SimpleUpdate(func(existing runtime.Object) (runtime.Object, error) { existingNamespace, ok := existing.(*api.Namespace) if !ok { // wrong type return nil, fmt.Errorf("expected *api.Namespace, got %v", existing) } // Set the deletion timestamp if needed if existingNamespace.DeletionTimestamp.IsZero() { now := unversioned.Now() existingNamespace.DeletionTimestamp = &now } // Set the namespace phase to terminating, if needed if existingNamespace.Status.Phase != api.NamespaceTerminating { existingNamespace.Status.Phase = api.NamespaceTerminating } return existingNamespace, nil }), ) if err != nil { err = storageerr.InterpretGetError(err, api.Resource("namespaces"), name) err = storageerr.InterpretUpdateError(err, api.Resource("namespaces"), name) if _, ok := err.(*apierrors.StatusError); !ok { err = apierrors.NewInternalError(err) } return nil, err } return out, nil } // prior to final deletion, we must ensure that finalizers is empty if len(namespace.Spec.Finalizers) != 0 { err = apierrors.NewConflict(api.Resource("namespaces"), namespace.Name, fmt.Errorf("The system is ensuring all content is removed from this namespace. Upon completion, this namespace will automatically be purged by the system.")) return nil, err } return r.Store.Delete(ctx, name, options) }
// Delete removes the item from etcd. func (e *Etcd) Delete(ctx api.Context, name string, options *api.DeleteOptions) (runtime.Object, error) { key, err := e.KeyFunc(ctx, name) if err != nil { return nil, err } obj := e.NewFunc() trace := util.NewTrace("Delete " + reflect.TypeOf(obj).String()) defer trace.LogIfLong(time.Second) trace.Step("About to read object") if err := e.Storage.Get(ctx, key, obj, false); err != nil { return nil, etcderr.InterpretDeleteError(err, e.EndpointName, name) } // support older consumers of delete by treating "nil" as delete immediately if options == nil { options = api.NewDeleteOptions(0) } graceful, pendingGraceful, err := rest.BeforeDelete(e.DeleteStrategy, ctx, obj, options) if err != nil { return nil, err } if pendingGraceful { return e.finalizeDelete(obj, false) } if graceful { trace.Step("Graceful deletion") out := e.NewFunc() lastGraceful := int64(0) err := e.Storage.GuaranteedUpdate( ctx, key, out, false, storage.SimpleUpdate(func(existing runtime.Object) (runtime.Object, error) { graceful, pendingGraceful, err := rest.BeforeDelete(e.DeleteStrategy, ctx, existing, options) if err != nil { return nil, err } if pendingGraceful { return nil, errAlreadyDeleting } if !graceful { return nil, errDeleteNow } lastGraceful = *options.GracePeriodSeconds return existing, nil }), ) switch err { case nil: if lastGraceful > 0 { return out, nil } // fall through and delete immediately case errDeleteNow: // we've updated the object to have a zero grace period, or it's already at 0, so // we should fall through and truly delete the object. case errAlreadyDeleting: return e.finalizeDelete(obj, true) default: return nil, etcderr.InterpretUpdateError(err, e.EndpointName, name) } } // delete immediately, or no graceful deletion supported out := e.NewFunc() trace.Step("About to delete object") if err := e.Storage.Delete(ctx, key, out); err != nil { return nil, etcderr.InterpretDeleteError(err, e.EndpointName, name) } return e.finalizeDelete(out, true) }
func TestGuaranteedUpdateUIDMismatch(t *testing.T) { server := etcdtesting.NewEtcdTestClientServer(t) defer server.Terminate(t) prefix := path.Join("/", etcdtest.PathPrefix()) helper := newEtcdHelper(server.Client, testapi.Default.Codec(), prefix) obj := &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", UID: "A"}} podPtr := &api.Pod{} err := helper.Create(context.TODO(), "/some/key", obj, podPtr, 0) if err != nil { t.Fatalf("Unexpected error %#v", err) } err = helper.GuaranteedUpdate(context.TODO(), "/some/key", podPtr, true, storage.NewUIDPreconditions("B"), storage.SimpleUpdate(func(in runtime.Object) (runtime.Object, error) { return obj, nil })) if !storage.IsTestFailed(err) { t.Fatalf("Expect a Test Failed (write conflict) error, got: %v", err) } }
// this functions need to be kept synced with updateForGracefulDeletion. func (e *Store) updateForGracefulDeletionAndFinalizers(ctx api.Context, name, key string, options *api.DeleteOptions, preconditions storage.Preconditions, in runtime.Object) (err error, ignoreNotFound, deleteImmediately bool, out, lastExisting runtime.Object) { lastGraceful := int64(0) var pendingFinalizers bool out = e.NewFunc() err = e.Storage.GuaranteedUpdate( ctx, key, out, false, &preconditions, storage.SimpleUpdate(func(existing runtime.Object) (runtime.Object, error) { graceful, pendingGraceful, err := rest.BeforeDelete(e.DeleteStrategy, ctx, existing, options) if err != nil { return nil, err } if pendingGraceful { return nil, errAlreadyDeleting } // Add/remove the orphan finalizer as the options dictates. // Note that this occurs after checking pendingGraceufl, so // finalizers cannot be updated via DeleteOptions if deletion has // started. existingAccessor, err := meta.Accessor(existing) if err != nil { return nil, err } shouldUpdate, newFinalizers := shouldUpdateFinalizers(existingAccessor, options) if shouldUpdate { existingAccessor.SetFinalizers(newFinalizers) } if !graceful { // set the DeleteGracePeriods to 0 if the object has pendingFinalizers but not supporting graceful deletion pendingFinalizers = len(existingAccessor.GetFinalizers()) != 0 if pendingFinalizers { glog.V(6).Infof("update the DeletionTimestamp to \"now\" and GracePeriodSeconds to 0 for object %s, because it has pending finalizers", name) err = markAsDeleting(existing) if err != nil { return nil, err } return existing, nil } return nil, errDeleteNow } lastGraceful = *options.GracePeriodSeconds lastExisting = existing return existing, nil }), ) switch err { case nil: // If there are pending finalizers, we never delete the object immediately. if pendingFinalizers { return nil, false, false, out, lastExisting } if lastGraceful > 0 { return nil, false, false, out, lastExisting } // If we are here, the registry supports grace period mechanism and // we are intentionally delete gracelessly. In this case, we may // enter a race with other k8s components. If other component wins // the race, the object will not be found, and we should tolerate // the NotFound error. See // https://github.com/kubernetes/kubernetes/issues/19403 for // details. return nil, true, true, out, lastExisting case errDeleteNow: // we've updated the object to have a zero grace period, or it's already at 0, so // we should fall through and truly delete the object. return nil, false, true, out, lastExisting case errAlreadyDeleting: out, err = e.finalizeDelete(in, true) return err, false, false, out, lastExisting default: return storeerr.InterpretUpdateError(err, e.QualifiedResource, name), false, false, out, lastExisting } }
// Delete removes the item from storage. func (e *Store) Delete(ctx api.Context, name string, options *api.DeleteOptions) (runtime.Object, error) { key, err := e.KeyFunc(ctx, name) if err != nil { return nil, err } obj := e.NewFunc() if err := e.Storage.Get(ctx, key, obj, false); err != nil { return nil, storeerr.InterpretDeleteError(err, e.QualifiedResource, name) } // support older consumers of delete by treating "nil" as delete immediately if options == nil { options = api.NewDeleteOptions(0) } var preconditions storage.Preconditions if options.Preconditions != nil { preconditions.UID = options.Preconditions.UID } graceful, pendingGraceful, err := rest.BeforeDelete(e.DeleteStrategy, ctx, obj, options) if err != nil { return nil, err } if pendingGraceful { return e.finalizeDelete(obj, false) } var ignoreNotFound bool = false var lastExisting runtime.Object = nil if graceful { out := e.NewFunc() lastGraceful := int64(0) err := e.Storage.GuaranteedUpdate( ctx, key, out, false, &preconditions, storage.SimpleUpdate(func(existing runtime.Object) (runtime.Object, error) { graceful, pendingGraceful, err := rest.BeforeDelete(e.DeleteStrategy, ctx, existing, options) if err != nil { return nil, err } if pendingGraceful { return nil, errAlreadyDeleting } if !graceful { return nil, errDeleteNow } lastGraceful = *options.GracePeriodSeconds lastExisting = existing return existing, nil }), ) switch err { case nil: if lastGraceful > 0 { return out, nil } // If we are here, the registry supports grace period mechanism and // we are intentionally delete gracelessly. In this case, we may // enter a race with other k8s components. If other component wins // the race, the object will not be found, and we should tolerate // the NotFound error. See // https://github.com/kubernetes/kubernetes/issues/19403 for // details. ignoreNotFound = true // exit the switch and delete immediately case errDeleteNow: // we've updated the object to have a zero grace period, or it's already at 0, so // we should fall through and truly delete the object. case errAlreadyDeleting: return e.finalizeDelete(obj, true) default: return nil, storeerr.InterpretUpdateError(err, e.QualifiedResource, name) } } // delete immediately, or no graceful deletion supported out := e.NewFunc() if err := e.Storage.Delete(ctx, key, out, &preconditions); err != nil { // Please refer to the place where we set ignoreNotFound for the reason // why we ignore the NotFound error . if storage.IsNotFound(err) && ignoreNotFound && lastExisting != nil { // The lastExisting object may not be the last state of the object // before its deletion, but it's the best approximation. return e.finalizeDelete(lastExisting, true) } return nil, storeerr.InterpretDeleteError(err, e.QualifiedResource, name) } return e.finalizeDelete(out, true) }