Merge pull request #97408 from KnicKnic/kubectl_wait

kubectl wait ensures observedGeneration >= generation

Kubernetes-commit: af8594ff998c17a59ce74ee2be8e02b0ea153df4
This commit is contained in:
Kubernetes Publisher 2021-02-03 12:42:28 -08:00
commit b50ba6012a
5 changed files with 204 additions and 4 deletions

2
Godeps/Godeps.json generated
View File

@ -936,7 +936,7 @@
},
{
"ImportPath": "k8s.io/client-go",
"Rev": "3147a30d7bb5"
"Rev": "93ce9718ffcd"
},
{
"ImportPath": "k8s.io/code-generator",

4
go.mod
View File

@ -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

2
go.sum
View File

@ -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=

View File

@ -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
}

View File

@ -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 {