diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index e00d508c6..ff8f62f74 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -936,7 +936,7 @@ }, { "ImportPath": "k8s.io/client-go", - "Rev": "3147a30d7bb5" + "Rev": "93ce9718ffcd" }, { "ImportPath": "k8s.io/code-generator", diff --git a/go.mod b/go.mod index 0e2247489..103d647ee 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,7 @@ require ( k8s.io/api v0.0.0-20210202201024-9f65ac4826aa k8s.io/apimachinery v0.0.0-20210202200849-9e39a13d2cab k8s.io/cli-runtime v0.0.0-20210202202902-984374fbd3bd - k8s.io/client-go v0.0.0-20210202201239-3147a30d7bb5 + k8s.io/client-go v0.0.0-20210203041231-93ce9718ffcd k8s.io/component-base v0.0.0-20210202201701-81d9ea233619 k8s.io/component-helpers v0.0.0-20210202201756-e42f75bf9b21 k8s.io/klog/v2 v2.5.0 @@ -52,7 +52,7 @@ replace ( k8s.io/api => k8s.io/api v0.0.0-20210202201024-9f65ac4826aa k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20210202200849-9e39a13d2cab k8s.io/cli-runtime => k8s.io/cli-runtime v0.0.0-20210202202902-984374fbd3bd - k8s.io/client-go => k8s.io/client-go v0.0.0-20210202201239-3147a30d7bb5 + k8s.io/client-go => k8s.io/client-go v0.0.0-20210203041231-93ce9718ffcd k8s.io/code-generator => k8s.io/code-generator v0.0.0-20210202200712-b6eef682227f k8s.io/component-base => k8s.io/component-base v0.0.0-20210202201701-81d9ea233619 k8s.io/component-helpers => k8s.io/component-helpers v0.0.0-20210202201756-e42f75bf9b21 diff --git a/go.sum b/go.sum index 258c5c692..ce67a34b3 100644 --- a/go.sum +++ b/go.sum @@ -639,7 +639,7 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 k8s.io/api v0.0.0-20210202201024-9f65ac4826aa/go.mod h1:3jofhj44aajVJZcPa3+rvEFZRe4nr1NNQgw5HtNky0M= k8s.io/apimachinery v0.0.0-20210202200849-9e39a13d2cab/go.mod h1:usCLrfBNFPxV+npBFCgIy08RBKPAhZQyIzwcvPV2eh8= k8s.io/cli-runtime v0.0.0-20210202202902-984374fbd3bd/go.mod h1:vN4I0m+pP/HOTtiHkCpNvUcrRPPu0/FU8aUKMibl7d4= -k8s.io/client-go v0.0.0-20210202201239-3147a30d7bb5/go.mod h1:mJubEonhU7Puf25vxba4hDwWZKKp6ErQbXGPi9sLXhU= +k8s.io/client-go v0.0.0-20210203041231-93ce9718ffcd/go.mod h1:mJubEonhU7Puf25vxba4hDwWZKKp6ErQbXGPi9sLXhU= k8s.io/code-generator v0.0.0-20210202200712-b6eef682227f/go.mod h1:O7FXIFFMbeLstjVDD1gKtnexuIo2JF8jkudWpXyjVeo= k8s.io/component-base v0.0.0-20210202201701-81d9ea233619/go.mod h1:usKilGzmoexy3tmMYXYM2r6QFIS+drbtnot8qjqIXe8= k8s.io/component-helpers v0.0.0-20210202201756-e42f75bf9b21/go.mod h1:8OBU1/nmDh8pjPoisMrILN3oAmgF9879iToT+jg9vjU= diff --git a/pkg/cmd/wait/wait.go b/pkg/cmd/wait/wait.go index a9f366229..574d18cd3 100644 --- a/pkg/cmd/wait/wait.go +++ b/pkg/cmd/wait/wait.go @@ -445,6 +445,13 @@ func (w ConditionalWait) checkCondition(obj *unstructured.Unstructured) (bool, e if !found || err != nil { continue } + generation, found, _ := unstructured.NestedInt64(obj.Object, "metadata", "generation") + if found { + observedGeneration, found := getObservedGeneration(obj, condition) + if found && observedGeneration < generation { + return false, nil + } + } return strings.EqualFold(status, w.conditionStatus), nil } @@ -470,3 +477,12 @@ func (w ConditionalWait) isConditionMet(event watch.Event) (bool, error) { func extendErrWaitTimeout(err error, info *resource.Info) error { return fmt.Errorf("%s on %s/%s", err.Error(), info.Mapping.Resource.Resource, info.Name) } + +func getObservedGeneration(obj *unstructured.Unstructured, condition map[string]interface{}) (int64, bool) { + conditionObservedGeneration, found, _ := unstructured.NestedInt64(condition, "observedGeneration") + if found { + return conditionObservedGeneration, true + } + statusObservedGeneration, found, _ := unstructured.NestedInt64(obj.Object, "status", "observedGeneration") + return statusObservedGeneration, found +} diff --git a/pkg/cmd/wait/wait_test.go b/pkg/cmd/wait/wait_test.go index f347ea257..5eee26bea 100644 --- a/pkg/cmd/wait/wait_test.go +++ b/pkg/cmd/wait/wait_test.go @@ -60,6 +60,21 @@ func newUnstructured(apiVersion, kind, namespace, name string) *unstructured.Uns } } +func newUnstructuredWithGeneration(apiVersion, kind, namespace, name string, generation int64) *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": apiVersion, + "kind": kind, + "metadata": map[string]interface{}{ + "namespace": namespace, + "name": name, + "uid": "some-UID-value", + "generation": generation, + }, + }, + } +} + func newUnstructuredStatus(status *metav1.Status) runtime.Unstructured { obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(status) if err != nil { @@ -80,6 +95,17 @@ func addCondition(in *unstructured.Unstructured, name, status string) *unstructu return in } +func addConditionWithObservedGeneration(in *unstructured.Unstructured, name, status string, observedGeneration int64) *unstructured.Unstructured { + conditions, _, _ := unstructured.NestedSlice(in.Object, "status", "conditions") + conditions = append(conditions, map[string]interface{}{ + "type": name, + "status": status, + "observedGeneration": observedGeneration, + }) + unstructured.SetNestedSlice(in.Object, conditions, "status", "conditions") + return in +} + func TestWaitForDeletion(t *testing.T) { scheme := runtime.NewScheme() listMapping := map[schema.GroupVersionResource]string{ @@ -764,6 +790,164 @@ func TestWaitForCondition(t *testing.T) { } }, }, + { + name: "times out due to stale .status.conditions[0].observedGeneration", + infos: []*resource.Info{ + { + Mapping: &meta.RESTMapping{ + Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, + }, + Name: "name-foo", + Namespace: "ns-foo", + }, + }, + fakeClient: func() *dynamicfakeclient.FakeDynamicClient { + fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) + fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + return true, addConditionWithObservedGeneration( + newUnstructuredWithGeneration("group/version", "TheKind", "ns-foo", "name-foo", 2), + "the-condition", "status-value", 1, + ), nil + }) + return fakeClient + }, + timeout: 1 * time.Second, + + expectedErr: "timed out waiting for the condition on theresource/name-foo", + validateActions: func(t *testing.T, actions []clienttesting.Action) { + if len(actions) != 2 { + t.Fatal(spew.Sdump(actions)) + } + if !actions[0].Matches("list", "theresource") || actions[0].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=name-foo" { + t.Error(spew.Sdump(actions)) + } + if !actions[1].Matches("watch", "theresource") { + t.Error(spew.Sdump(actions)) + } + }, + }, + { + name: "handles watch .status.conditions[0].observedGeneration change", + infos: []*resource.Info{ + { + Mapping: &meta.RESTMapping{ + Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, + }, + Name: "name-foo", + Namespace: "ns-foo", + }, + }, + fakeClient: func() *dynamicfakeclient.FakeDynamicClient { + fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) + fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + return true, newUnstructuredList(addConditionWithObservedGeneration(newUnstructuredWithGeneration("group/version", "TheKind", "ns-foo", "name-foo", 2), "the-condition", "status-value", 1)), nil + }) + fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) { + fakeWatch := watch.NewRaceFreeFake() + fakeWatch.Action(watch.Modified, addConditionWithObservedGeneration( + newUnstructuredWithGeneration("group/version", "TheKind", "ns-foo", "name-foo", 2), + "the-condition", "status-value", 2, + )) + return true, fakeWatch, nil + }) + return fakeClient + }, + timeout: 10 * time.Second, + + validateActions: func(t *testing.T, actions []clienttesting.Action) { + if len(actions) != 2 { + t.Fatal(spew.Sdump(actions)) + } + if !actions[0].Matches("list", "theresource") || actions[0].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=name-foo" { + t.Error(spew.Sdump(actions)) + } + if !actions[1].Matches("watch", "theresource") { + t.Error(spew.Sdump(actions)) + } + }, + }, + { + name: "times out due to stale .status.observedGeneration", + infos: []*resource.Info{ + { + Mapping: &meta.RESTMapping{ + Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, + }, + Name: "name-foo", + Namespace: "ns-foo", + }, + }, + fakeClient: func() *dynamicfakeclient.FakeDynamicClient { + fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) + fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + instance := addCondition( + newUnstructuredWithGeneration("group/version", "TheKind", "ns-foo", "name-foo", 2), + "the-condition", "status-value") + unstructured.SetNestedField(instance.Object, int64(1), "status", "observedGeneration") + return true, instance, nil + }) + return fakeClient + }, + timeout: 1 * time.Second, + + expectedErr: "timed out waiting for the condition on theresource/name-foo", + validateActions: func(t *testing.T, actions []clienttesting.Action) { + if len(actions) != 2 { + t.Fatal(spew.Sdump(actions)) + } + if !actions[0].Matches("list", "theresource") || actions[0].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=name-foo" { + t.Error(spew.Sdump(actions)) + } + if !actions[1].Matches("watch", "theresource") { + t.Error(spew.Sdump(actions)) + } + }, + }, + { + name: "handles watch .status.observedGeneration change", + infos: []*resource.Info{ + { + Mapping: &meta.RESTMapping{ + Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, + }, + Name: "name-foo", + Namespace: "ns-foo", + }, + }, + fakeClient: func() *dynamicfakeclient.FakeDynamicClient { + fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) + fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + instance := addCondition( + newUnstructuredWithGeneration("group/version", "TheKind", "ns-foo", "name-foo", 2), + "the-condition", "status-value") + unstructured.SetNestedField(instance.Object, int64(1), "status", "observedGeneration") + return true, newUnstructuredList(instance), nil + }) + fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) { + instance := addCondition( + newUnstructuredWithGeneration("group/version", "TheKind", "ns-foo", "name-foo", 2), + "the-condition", "status-value") + unstructured.SetNestedField(instance.Object, int64(2), "status", "observedGeneration") + fakeWatch := watch.NewRaceFreeFake() + fakeWatch.Action(watch.Modified, instance) + return true, fakeWatch, nil + }) + return fakeClient + }, + timeout: 10 * time.Second, + + validateActions: func(t *testing.T, actions []clienttesting.Action) { + if len(actions) != 2 { + t.Fatal(spew.Sdump(actions)) + } + if !actions[0].Matches("list", "theresource") || actions[0].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=name-foo" { + t.Error(spew.Sdump(actions)) + } + if !actions[1].Matches("watch", "theresource") { + t.Error(spew.Sdump(actions)) + } + }, + }, } for _, test := range tests {