Return an error if there is no resources matching

This makes `kubectl wait` print useful message when
there is no resources matching a query. Also it will now
exit with the exit status 1.
This commit is contained in:
Mikalai Radchuk 2018-07-26 22:59:42 +01:00
parent 259e0743f1
commit d3445d71d0
2 changed files with 121 additions and 58 deletions

View File

@ -17,13 +17,14 @@ limitations under the License.
package wait package wait
import ( import (
"errors"
"fmt" "fmt"
"strings" "strings"
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
@ -61,6 +62,9 @@ var (
kubectl wait --for=delete pod/busybox1 --timeout=60s`) kubectl wait --for=delete pod/busybox1 --timeout=60s`)
) )
// errNoMatchingResources is returned when there is no resources matching a query.
var errNoMatchingResources = errors.New("no matching resources found")
// WaitFlags directly reflect the information that CLI is gathering via flags. They will be converted to Options, which // WaitFlags directly reflect the information that CLI is gathering via flags. They will be converted to Options, which
// reflect the runtime requirements for the command. This structure reduces the transformation to wiring and makes // reflect the runtime requirements for the command. This structure reduces the transformation to wiring and makes
// the logic itself easy to unit test // the logic itself easy to unit test
@ -206,11 +210,13 @@ type ConditionFunc func(info *resource.Info, o *WaitOptions) (finalObject runtim
// RunWait runs the waiting logic // RunWait runs the waiting logic
func (o *WaitOptions) RunWait() error { func (o *WaitOptions) RunWait() error {
return o.ResourceFinder.Do().Visit(func(info *resource.Info, err error) error { visitCount := 0
err := o.ResourceFinder.Do().Visit(func(info *resource.Info, err error) error {
if err != nil { if err != nil {
return err return err
} }
visitCount++
finalObject, success, err := o.ConditionFn(info, o) finalObject, success, err := o.ConditionFn(info, o)
if success { if success {
o.Printer.PrintObj(finalObject, o.Out) o.Printer.PrintObj(finalObject, o.Out)
@ -221,6 +227,13 @@ func (o *WaitOptions) RunWait() error {
} }
return err return err
}) })
if err != nil {
return err
}
if visitCount == 0 {
return errNoMatchingResources
}
return err
} }
// IsDeleted is a condition func for waiting for something to be deleted // IsDeleted is a condition func for waiting for something to be deleted
@ -228,7 +241,7 @@ func IsDeleted(info *resource.Info, o *WaitOptions) (runtime.Object, bool, error
endTime := time.Now().Add(o.Timeout) endTime := time.Now().Add(o.Timeout)
for { for {
gottenObj, err := o.DynamicClient.Resource(info.Mapping.Resource).Namespace(info.Namespace).Get(info.Name, metav1.GetOptions{}) gottenObj, err := o.DynamicClient.Resource(info.Mapping.Resource).Namespace(info.Namespace).Get(info.Name, metav1.GetOptions{})
if errors.IsNotFound(err) { if apierrors.IsNotFound(err) {
return info.Object, true, nil return info.Object, true, nil
} }
if err != nil { if err != nil {
@ -293,7 +306,7 @@ func (w ConditionalWait) IsConditionMet(info *resource.Info, o *WaitOptions) (ru
resourceVersion := "" resourceVersion := ""
gottenObj, err := o.DynamicClient.Resource(info.Mapping.Resource).Namespace(info.Namespace).Get(info.Name, metav1.GetOptions{}) gottenObj, err := o.DynamicClient.Resource(info.Mapping.Resource).Namespace(info.Namespace).Get(info.Name, metav1.GetOptions{})
switch { switch {
case errors.IsNotFound(err): case apierrors.IsNotFound(err):
resourceVersion = "0" resourceVersion = "0"
case err != nil: case err != nil:
return info.Object, false, err return info.Object, false, err

View File

@ -68,7 +68,7 @@ func TestWaitForDeletion(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
info *resource.Info infos []*resource.Info
fakeClient func() *dynamicfakeclient.FakeDynamicClient fakeClient func() *dynamicfakeclient.FakeDynamicClient
timeout time.Duration timeout time.Duration
uidMap UIDMap uidMap UIDMap
@ -78,12 +78,14 @@ func TestWaitForDeletion(t *testing.T) {
}{ }{
{ {
name: "missing on get", name: "missing on get",
info: &resource.Info{ infos: []*resource.Info{
Mapping: &meta.RESTMapping{ {
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, Mapping: &meta.RESTMapping{
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
},
Name: "name-foo",
Namespace: "ns-foo",
}, },
Name: "name-foo",
Namespace: "ns-foo",
}, },
fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
return dynamicfakeclient.NewSimpleDynamicClient(scheme) return dynamicfakeclient.NewSimpleDynamicClient(scheme)
@ -99,14 +101,31 @@ func TestWaitForDeletion(t *testing.T) {
} }
}, },
}, },
{
name: "handles no infos",
infos: []*resource.Info{},
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
return dynamicfakeclient.NewSimpleDynamicClient(scheme)
},
timeout: 10 * time.Second,
expectedErr: errNoMatchingResources.Error(),
validateActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 0 {
t.Fatal(spew.Sdump(actions))
}
},
},
{ {
name: "uid conflict on get", name: "uid conflict on get",
info: &resource.Info{ infos: []*resource.Info{
Mapping: &meta.RESTMapping{ {
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, Mapping: &meta.RESTMapping{
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
},
Name: "name-foo",
Namespace: "ns-foo",
}, },
Name: "name-foo",
Namespace: "ns-foo",
}, },
fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme) fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
@ -146,12 +165,14 @@ func TestWaitForDeletion(t *testing.T) {
}, },
{ {
name: "times out", name: "times out",
info: &resource.Info{ infos: []*resource.Info{
Mapping: &meta.RESTMapping{ {
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, Mapping: &meta.RESTMapping{
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
},
Name: "name-foo",
Namespace: "ns-foo",
}, },
Name: "name-foo",
Namespace: "ns-foo",
}, },
fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme) fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
@ -177,12 +198,14 @@ func TestWaitForDeletion(t *testing.T) {
}, },
{ {
name: "handles watch close out", name: "handles watch close out",
info: &resource.Info{ infos: []*resource.Info{
Mapping: &meta.RESTMapping{ {
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, Mapping: &meta.RESTMapping{
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
},
Name: "name-foo",
Namespace: "ns-foo",
}, },
Name: "name-foo",
Namespace: "ns-foo",
}, },
fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme) fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
@ -228,12 +251,14 @@ func TestWaitForDeletion(t *testing.T) {
}, },
{ {
name: "handles watch delete", name: "handles watch delete",
info: &resource.Info{ infos: []*resource.Info{
Mapping: &meta.RESTMapping{ {
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, Mapping: &meta.RESTMapping{
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
},
Name: "name-foo",
Namespace: "ns-foo",
}, },
Name: "name-foo",
Namespace: "ns-foo",
}, },
fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme) fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
@ -267,7 +292,7 @@ func TestWaitForDeletion(t *testing.T) {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
fakeClient := test.fakeClient() fakeClient := test.fakeClient()
o := &WaitOptions{ o := &WaitOptions{
ResourceFinder: genericclioptions.NewSimpleFakeResourceFinder(test.info), ResourceFinder: genericclioptions.NewSimpleFakeResourceFinder(test.infos...),
UIDMap: test.uidMap, UIDMap: test.uidMap,
DynamicClient: fakeClient, DynamicClient: fakeClient,
Timeout: test.timeout, Timeout: test.timeout,
@ -299,7 +324,7 @@ func TestWaitForCondition(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
info *resource.Info infos []*resource.Info
fakeClient func() *dynamicfakeclient.FakeDynamicClient fakeClient func() *dynamicfakeclient.FakeDynamicClient
timeout time.Duration timeout time.Duration
@ -308,12 +333,14 @@ func TestWaitForCondition(t *testing.T) {
}{ }{
{ {
name: "present on get", name: "present on get",
info: &resource.Info{ infos: []*resource.Info{
Mapping: &meta.RESTMapping{ {
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, Mapping: &meta.RESTMapping{
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
},
Name: "name-foo",
Namespace: "ns-foo",
}, },
Name: "name-foo",
Namespace: "ns-foo",
}, },
fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme) fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
@ -336,14 +363,31 @@ func TestWaitForCondition(t *testing.T) {
} }
}, },
}, },
{
name: "handles no infos",
infos: []*resource.Info{},
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
return dynamicfakeclient.NewSimpleDynamicClient(scheme)
},
timeout: 10 * time.Second,
expectedErr: errNoMatchingResources.Error(),
validateActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 0 {
t.Fatal(spew.Sdump(actions))
}
},
},
{ {
name: "times out", name: "times out",
info: &resource.Info{ infos: []*resource.Info{
Mapping: &meta.RESTMapping{ {
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, Mapping: &meta.RESTMapping{
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
},
Name: "name-foo",
Namespace: "ns-foo",
}, },
Name: "name-foo",
Namespace: "ns-foo",
}, },
fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme) fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
@ -372,12 +416,14 @@ func TestWaitForCondition(t *testing.T) {
}, },
{ {
name: "handles watch close out", name: "handles watch close out",
info: &resource.Info{ infos: []*resource.Info{
Mapping: &meta.RESTMapping{ {
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, Mapping: &meta.RESTMapping{
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
},
Name: "name-foo",
Namespace: "ns-foo",
}, },
Name: "name-foo",
Namespace: "ns-foo",
}, },
fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme) fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
@ -423,12 +469,14 @@ func TestWaitForCondition(t *testing.T) {
}, },
{ {
name: "handles watch condition change", name: "handles watch condition change",
info: &resource.Info{ infos: []*resource.Info{
Mapping: &meta.RESTMapping{ {
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, Mapping: &meta.RESTMapping{
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
},
Name: "name-foo",
Namespace: "ns-foo",
}, },
Name: "name-foo",
Namespace: "ns-foo",
}, },
fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme) fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
@ -461,12 +509,14 @@ func TestWaitForCondition(t *testing.T) {
}, },
{ {
name: "handles watch created", name: "handles watch created",
info: &resource.Info{ infos: []*resource.Info{
Mapping: &meta.RESTMapping{ {
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, Mapping: &meta.RESTMapping{
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
},
Name: "name-foo",
Namespace: "ns-foo",
}, },
Name: "name-foo",
Namespace: "ns-foo",
}, },
fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme) fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
@ -500,7 +550,7 @@ func TestWaitForCondition(t *testing.T) {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
fakeClient := test.fakeClient() fakeClient := test.fakeClient()
o := &WaitOptions{ o := &WaitOptions{
ResourceFinder: genericclioptions.NewSimpleFakeResourceFinder(test.info), ResourceFinder: genericclioptions.NewSimpleFakeResourceFinder(test.infos...),
DynamicClient: fakeClient, DynamicClient: fakeClient,
Timeout: test.timeout, Timeout: test.timeout,