pull/8/head
David Eads 2018-05-18 08:12:55 -04:00
parent e85b81bbee
commit 76794643c5
19 changed files with 1135 additions and 24 deletions

View File

@ -177,6 +177,7 @@ package_group(
"//pkg/kubectl/cmd/templates",
"//pkg/kubectl/cmd/util",
"//pkg/kubectl/cmd/util/sanity",
"//pkg/kubectl/cmd/wait",
],
)
@ -196,6 +197,7 @@ package_group(
"//pkg/kubectl/cmd/get",
"//pkg/kubectl/cmd/rollout",
"//pkg/kubectl/cmd/set",
"//pkg/kubectl/cmd/wait",
"//pkg/kubectl/explain",
],
)
@ -230,6 +232,7 @@ package_group(
"//pkg/kubectl/cmd/testing",
"//pkg/kubectl/cmd/util",
"//pkg/kubectl/cmd/util/editor",
"//pkg/kubectl/cmd/wait",
],
)

View File

@ -150,6 +150,7 @@ pkg/kubectl/cmd/util
pkg/kubectl/cmd/util/editor
pkg/kubectl/cmd/util/jsonmerge
pkg/kubectl/cmd/util/sanity
pkg/kubectl/cmd/wait
pkg/kubectl/genericclioptions
pkg/kubectl/genericclioptions/printers
pkg/kubectl/genericclioptions/resource

View File

@ -2382,7 +2382,11 @@ run_namespace_tests() {
# Post-condition: namespace 'my-namespace' is created.
kube::test::get_object_assert 'namespaces/my-namespace' "{{$id_field}}" 'my-namespace'
# Clean up
kubectl delete namespace my-namespace
kubectl delete namespace my-namespace --wait=false
# make sure that wait properly waits for finalization
kubectl wait --for=delete ns/my-namespace
output_message=$(! kubectl get ns/my-namespace 2>&1 "${kube_flags[@]}")
kube::test::if_has_string "${output_message}" ' not found'
######################
# Pods in Namespaces #

View File

@ -77,6 +77,7 @@ go_library(
"//pkg/kubectl/cmd/util:go_default_library",
"//pkg/kubectl/cmd/util/editor:go_default_library",
"//pkg/kubectl/cmd/util/openapi:go_default_library",
"//pkg/kubectl/cmd/wait:go_default_library",
"//pkg/kubectl/explain:go_default_library",
"//pkg/kubectl/genericclioptions:go_default_library",
"//pkg/kubectl/genericclioptions/printers:go_default_library",
@ -264,6 +265,7 @@ filegroup(
"//pkg/kubectl/cmd/testdata/edit:all-srcs",
"//pkg/kubectl/cmd/testing:all-srcs",
"//pkg/kubectl/cmd/util:all-srcs",
"//pkg/kubectl/cmd/wait:all-srcs",
],
tags = ["automanaged"],
visibility = [

View File

@ -207,7 +207,11 @@ func (o *ApplyOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error {
return err
}
o.DeleteOptions = o.DeleteFlags.ToOptions(o.IOStreams)
dynamicClient, err := f.DynamicClient()
if err != nil {
return err
}
o.DeleteOptions = o.DeleteFlags.ToOptions(dynamicClient, o.IOStreams)
o.ShouldIncludeUninitialized = cmdutil.ShouldIncludeUninitialized(cmd, o.Prune)
o.OpenAPISchema, _ = f.OpenAPISchema()

View File

@ -523,6 +523,7 @@ func TestApplyObject(t *testing.T) {
}
tf.OpenAPISchemaFunc = fn
tf.Namespace = "test"
tf.ClientConfigVal = defaultClientConfig()
ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
cmd := NewCmdApply("kubectl", tf, ioStreams)
@ -587,6 +588,7 @@ func TestApplyObjectOutput(t *testing.T) {
}
tf.OpenAPISchemaFunc = fn
tf.Namespace = "test"
tf.ClientConfigVal = defaultClientConfig()
ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
cmd := NewCmdApply("kubectl", tf, ioStreams)
@ -648,6 +650,7 @@ func TestApplyRetry(t *testing.T) {
}
tf.OpenAPISchemaFunc = fn
tf.Namespace = "test"
tf.ClientConfigVal = defaultClientConfig()
ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
cmd := NewCmdApply("kubectl", tf, ioStreams)
@ -697,6 +700,7 @@ func TestApplyNonExistObject(t *testing.T) {
}),
}
tf.Namespace = "test"
tf.ClientConfigVal = defaultClientConfig()
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
cmd := NewCmdApply("kubectl", tf, ioStreams)
@ -749,6 +753,7 @@ func TestApplyEmptyPatch(t *testing.T) {
}),
}
tf.Namespace = "test"
tf.ClientConfigVal = defaultClientConfig()
// 1. apply non exist object
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
@ -823,6 +828,7 @@ func testApplyMultipleObjects(t *testing.T, asList bool) {
}
tf.OpenAPISchemaFunc = fn
tf.Namespace = "test"
tf.ClientConfigVal = defaultClientConfig()
ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
cmd := NewCmdApply("kubectl", tf, ioStreams)
@ -923,6 +929,7 @@ func TestApplyNULLPreservation(t *testing.T) {
}
tf.OpenAPISchemaFunc = fn
tf.Namespace = "test"
tf.ClientConfigVal = defaultClientConfig()
ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
cmd := NewCmdApply("kubectl", tf, ioStreams)
@ -989,6 +996,7 @@ func TestUnstructuredApply(t *testing.T) {
}
tf.OpenAPISchemaFunc = fn
tf.Namespace = "test"
tf.ClientConfigVal = defaultClientConfig()
ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
cmd := NewCmdApply("kubectl", tf, ioStreams)
@ -1054,6 +1062,7 @@ func TestUnstructuredIdempotentApply(t *testing.T) {
}
tf.OpenAPISchemaFunc = fn
tf.Namespace = "test"
tf.ClientConfigVal = defaultClientConfig()
ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
cmd := NewCmdApply("kubectl", tf, ioStreams)
@ -1223,6 +1232,7 @@ func TestForceApply(t *testing.T) {
tf := cmdtesting.NewTestFactory()
defer tf.Cleanup()
tf.ClientConfigVal = defaultClientConfig()
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {

View File

@ -33,6 +33,7 @@ import (
"k8s.io/kubernetes/pkg/kubectl/cmd/set"
"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/cmd/wait"
"k8s.io/kubernetes/pkg/kubectl/util/i18n"
"github.com/spf13/cobra"
@ -362,6 +363,7 @@ func NewKubectlCommand(in io.Reader, out, err io.Writer) *cobra.Command {
NewCmdApply("kubectl", f, ioStreams),
NewCmdPatch(f, ioStreams),
NewCmdReplace(f, ioStreams),
wait.NewCmdWait(f, ioStreams),
NewCmdConvert(f, ioStreams),
},
},

View File

@ -21,15 +21,18 @@ import (
"strings"
"time"
"github.com/golang/glog"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/dynamic"
"k8s.io/kubernetes/pkg/kubectl"
"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
kubectlwait "k8s.io/kubernetes/pkg/kubectl/cmd/wait"
"k8s.io/kubernetes/pkg/kubectl/genericclioptions"
"k8s.io/kubernetes/pkg/kubectl/genericclioptions/resource"
"k8s.io/kubernetes/pkg/kubectl/util/i18n"
@ -106,8 +109,9 @@ type DeleteOptions struct {
Output string
Mapper meta.RESTMapper
Result *resource.Result
DynamicClient dynamic.Interface
Mapper meta.RESTMapper
Result *resource.Result
genericclioptions.IOStreams
}
@ -122,7 +126,7 @@ func NewCmdDelete(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra
Long: delete_long,
Example: delete_example,
Run: func(cmd *cobra.Command, args []string) {
o := deleteFlags.ToOptions(streams)
o := deleteFlags.ToOptions(nil, streams)
if err := o.Complete(f, args, cmd); err != nil {
cmdutil.CheckErr(err)
}
@ -138,6 +142,8 @@ func NewCmdDelete(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra
deleteFlags.AddFlags(cmd)
cmd.Flags().Bool("wait", true, `If true, wait for resources to be gone before returning. This waits for finalizers.`)
cmdutil.AddIncludeUninitializedFlag(cmd)
return cmd
}
@ -167,6 +173,9 @@ func (o *DeleteOptions) Complete(f cmdutil.Factory, args []string, cmd *cobra.Co
o.WaitForDeletion = true
o.GracePeriod = 1
}
if b, err := cmd.Flags().GetBool("wait"); err == nil {
o.WaitForDeletion = b
}
o.Reaper = f.Reaper
@ -194,6 +203,11 @@ func (o *DeleteOptions) Complete(f cmdutil.Factory, args []string, cmd *cobra.Co
return err
}
o.DynamicClient, err = f.DynamicClient()
if err != nil {
return err
}
return nil
}
@ -300,8 +314,38 @@ func (o *DeleteOptions) DeleteResult(r *resource.Result) error {
}
if found == 0 {
fmt.Fprintf(o.Out, "No resources found\n")
return nil
}
return nil
if !o.WaitForDeletion {
return nil
}
// if we don't have a dynamic client, we don't want to wait. Eventually when delete is cleaned up, this will likely
// drop out.
if o.DynamicClient == nil {
return nil
}
effectiveTimeout := o.Timeout
if effectiveTimeout == 0 {
// if we requested to wait forever, set it to a week.
effectiveTimeout = 168 * time.Hour
}
waitOptions := kubectlwait.WaitOptions{
ResourceFinder: kubectlwait.ResourceFinderForResult(o.Result),
DynamicClient: o.DynamicClient,
Timeout: effectiveTimeout,
Printer: kubectlwait.NewDiscardingPrinter(),
ConditionFn: kubectlwait.IsDeleted,
IOStreams: o.IOStreams,
}
err = waitOptions.RunWait()
if errors.IsForbidden(err) {
// if we're forbidden from waiting, we shouldn't fail.
glog.V(1).Info(err)
return nil
}
return err
}
func (o *DeleteOptions) cascadingDeleteResource(info *resource.Info) error {

View File

@ -21,6 +21,7 @@ import (
"github.com/spf13/cobra"
"k8s.io/client-go/dynamic"
"k8s.io/kubernetes/pkg/kubectl"
"k8s.io/kubernetes/pkg/kubectl/genericclioptions"
"k8s.io/kubernetes/pkg/kubectl/genericclioptions/resource"
@ -72,9 +73,10 @@ type DeleteFlags struct {
Output *string
}
func (f *DeleteFlags) ToOptions(streams genericclioptions.IOStreams) *DeleteOptions {
func (f *DeleteFlags) ToOptions(dynamicClient dynamic.Interface, streams genericclioptions.IOStreams) *DeleteOptions {
options := &DeleteOptions{
IOStreams: streams,
DynamicClient: dynamicClient,
IOStreams: streams,
}
// add filename options

View File

@ -150,7 +150,11 @@ func (o *ReplaceOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []
return printer.PrintObj(obj, o.Out)
}
deleteOpts := o.DeleteFlags.ToOptions(o.IOStreams)
dynamicClient, err := f.DynamicClient()
if err != nil {
return err
}
deleteOpts := o.DeleteFlags.ToOptions(dynamicClient, o.IOStreams)
//Replace will create a resource if it doesn't exist already, so ignore not found error
deleteOpts.IgnoreNotFound = true

View File

@ -223,7 +223,7 @@ func (o *RunOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error {
return printer.PrintObj(obj, o.Out)
}
deleteOpts := o.DeleteFlags.ToOptions(o.IOStreams)
deleteOpts := o.DeleteFlags.ToOptions(o.DynamicClient, o.IOStreams)
deleteOpts.IgnoreNotFound = true
deleteOpts.WaitForDeletion = false
deleteOpts.GracePeriod = -1

View File

@ -207,7 +207,7 @@ func TestRunArgsFollowDashRules(t *testing.T) {
deleteFlags := NewDeleteFlags("to use to replace the resource.")
opts := &RunOptions{
PrintFlags: printFlags,
DeleteOptions: deleteFlags.ToOptions(genericclioptions.NewTestIOStreamsDiscard()),
DeleteOptions: deleteFlags.ToOptions(nil, genericclioptions.NewTestIOStreamsDiscard()),
IOStreams: genericclioptions.NewTestIOStreamsDiscard(),
@ -376,7 +376,7 @@ func TestGenerateService(t *testing.T) {
deleteFlags := NewDeleteFlags("to use to replace the resource.")
opts := &RunOptions{
PrintFlags: printFlags,
DeleteOptions: deleteFlags.ToOptions(genericclioptions.NewTestIOStreamsDiscard()),
DeleteOptions: deleteFlags.ToOptions(nil, genericclioptions.NewTestIOStreamsDiscard()),
IOStreams: ioStreams,

View File

@ -0,0 +1,60 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"fakeresourcefinder.go",
"flags.go",
"wait.go",
],
importpath = "k8s.io/kubernetes/pkg/kubectl/cmd/wait",
visibility = ["//visibility:public"],
deps = [
"//pkg/kubectl/cmd/util:go_default_library",
"//pkg/kubectl/genericclioptions:go_default_library",
"//pkg/kubectl/genericclioptions/printers:go_default_library",
"//pkg/kubectl/genericclioptions/resource:go_default_library",
"//vendor/github.com/spf13/cobra:go_default_library",
"//vendor/github.com/spf13/pflag:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
"//vendor/k8s.io/client-go/dynamic:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = ["wait_test.go"],
embed = [":go_default_library"],
deps = [
"//pkg/kubectl/genericclioptions:go_default_library",
"//pkg/kubectl/genericclioptions/resource:go_default_library",
"//vendor/github.com/davecgh/go-spew/spew:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
"//vendor/k8s.io/client-go/dynamic/fake:go_default_library",
"//vendor/k8s.io/client-go/testing:go_default_library",
],
)

View File

@ -0,0 +1,54 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package wait
import (
"k8s.io/kubernetes/pkg/kubectl/genericclioptions/resource"
)
// NewSimpleResourceFinder builds a super simple ResourceFinder that just iterates over the objects you provided
func NewSimpleResourceFinder(infos ...*resource.Info) ResourceFinder {
return &fakeResourceFinder{
Infos: infos,
}
}
type fakeResourceFinder struct {
Infos []*resource.Info
}
// Do implements the interface
func (f *fakeResourceFinder) Do() resource.Visitor {
return &fakeResourceResult{
Infos: f.Infos,
}
}
type fakeResourceResult struct {
Infos []*resource.Info
}
// Visit just iterates over info
func (r *fakeResourceResult) Visit(fn resource.VisitorFunc) error {
for _, info := range r.Infos {
err := fn(info, nil)
if err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,114 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package wait
import (
"strings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"k8s.io/kubernetes/pkg/kubectl/genericclioptions"
"k8s.io/kubernetes/pkg/kubectl/genericclioptions/resource"
)
// ResourceBuilderFlags are flags for finding resources
type ResourceBuilderFlags struct {
FilenameOptions resource.FilenameOptions
LabelSelector string
FieldSelector string
AllNamespaces bool
Namespace string
ExplicitNamespace bool
// TODO add conditional support. These are false for now.
All bool
Local bool
}
// NewResourceBuilderFlags returns a default ResourceBuilderFlags
func NewResourceBuilderFlags() *ResourceBuilderFlags {
return &ResourceBuilderFlags{
FilenameOptions: resource.FilenameOptions{
Recursive: true,
},
}
}
// AddFlags registers flags for finding resources
func (o *ResourceBuilderFlags) AddFlags(flagset *pflag.FlagSet) {
flagset.StringSliceVarP(&o.FilenameOptions.Filenames, "filename", "f", o.FilenameOptions.Filenames, "Filename, directory, or URL to files identifying the resource.")
annotations := make([]string, 0, len(resource.FileExtensions))
for _, ext := range resource.FileExtensions {
annotations = append(annotations, strings.TrimLeft(ext, "."))
}
flagset.SetAnnotation("filename", cobra.BashCompFilenameExt, annotations)
flagset.BoolVar(&o.FilenameOptions.Recursive, "recursive", o.FilenameOptions.Recursive, "Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory.")
flagset.StringVarP(&o.LabelSelector, "selector", "l", o.LabelSelector, "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)")
flagset.StringVar(&o.FieldSelector, "field-selector", o.FieldSelector, "Selector (field query) to filter on, supports '=', '==', and '!='.(e.g. --field-selector key1=value1,key2=value2). The server only supports a limited number of field queries per type.")
flagset.BoolVar(&o.AllNamespaces, "all-namespaces", o.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.")
}
// ToBuilder gives you back a resource finder to visit resources that are located
func (o *ResourceBuilderFlags) ToBuilder(restClientGetter genericclioptions.RESTClientGetter, resources []string) ResourceFinder {
namespace, enforceNamespace, namespaceErr := restClientGetter.ToRawKubeConfigLoader().Namespace()
return &ResourceFindBuilderWrapper{
builder: resource.NewBuilder(restClientGetter).
Unstructured().
NamespaceParam(namespace).DefaultNamespace().
FilenameParam(enforceNamespace, &o.FilenameOptions).
LabelSelectorParam(o.LabelSelector).
FieldSelectorParam(o.FieldSelector).
ResourceTypeOrNameArgs(o.All, resources...).
Latest().
Flatten().
AddError(namespaceErr),
}
}
// ResourceFindBuilderWrapper wraps a builder in an interface
type ResourceFindBuilderWrapper struct {
builder *resource.Builder
}
// Do finds you resources to check
func (b *ResourceFindBuilderWrapper) Do() resource.Visitor {
return b.builder.Do()
}
// ResourceFinder allows mocking the resource builder
// TODO resource builders needs to become more interfacey
type ResourceFinder interface {
Do() resource.Visitor
}
// ResourceFinderFunc is a handy way to make a ResourceFinder
type ResourceFinderFunc func() resource.Visitor
// Do implements ResourceFinder
func (fn ResourceFinderFunc) Do() resource.Visitor {
return fn()
}
// ResourceFinderForResult skins a visitor for re-use as a ResourceFinder
func ResourceFinderForResult(result resource.Visitor) ResourceFinder {
return ResourceFinderFunc(func() resource.Visitor {
return result
})
}

View File

@ -0,0 +1,330 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package wait
import (
"fmt"
"io"
"strings"
"time"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/dynamic"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/genericclioptions"
"k8s.io/kubernetes/pkg/kubectl/genericclioptions/printers"
"k8s.io/kubernetes/pkg/kubectl/genericclioptions/resource"
)
// 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
// the logic itself easy to unit test
type WaitFlags struct {
RESTClientGetter genericclioptions.RESTClientGetter
PrintFlags *genericclioptions.PrintFlags
ResourceBuilderFlags *ResourceBuilderFlags
Timeout time.Duration
ForCondition string
genericclioptions.IOStreams
}
// NewWaitFlags returns a default WaitFlags
func NewWaitFlags(restClientGetter genericclioptions.RESTClientGetter, streams genericclioptions.IOStreams) *WaitFlags {
return &WaitFlags{
RESTClientGetter: restClientGetter,
PrintFlags: genericclioptions.NewPrintFlags("condition met"),
ResourceBuilderFlags: NewResourceBuilderFlags(),
Timeout: 30 * time.Second,
IOStreams: streams,
}
}
// NewCmdWait returns a cobra command for waiting
func NewCmdWait(restClientGetter genericclioptions.RESTClientGetter, streams genericclioptions.IOStreams) *cobra.Command {
flags := NewWaitFlags(restClientGetter, streams)
cmd := &cobra.Command{
Use: "wait resource.group/name [--for=delete|--for condition=available]",
DisableFlagsInUseLine: true,
Short: "Wait for one condition on one or many resources",
Run: func(cmd *cobra.Command, args []string) {
o, err := flags.ToOptions(args)
cmdutil.CheckErr(err)
err = o.RunWait()
cmdutil.CheckErr(err)
},
SuggestFor: []string{"list", "ps"},
}
flags.AddFlags(cmd)
return cmd
}
// AddFlags registers flags for a cli
func (flags *WaitFlags) AddFlags(cmd *cobra.Command) {
flags.PrintFlags.AddFlags(cmd)
flags.ResourceBuilderFlags.AddFlags(cmd.Flags())
cmd.Flags().DurationVar(&flags.Timeout, "timeout", flags.Timeout, "The length of time to wait before giving up. Zero means check once and don't wait, negative means wait for a week.")
cmd.Flags().StringVar(&flags.ForCondition, "for", flags.ForCondition, "The condition to wait on: [delete|condition=condition-name].")
}
// ToOptions converts from CLI inputs to runtime inputs
func (flags *WaitFlags) ToOptions(args []string) (*WaitOptions, error) {
printer, err := flags.PrintFlags.ToPrinter()
if err != nil {
return nil, err
}
builder := flags.ResourceBuilderFlags.ToBuilder(flags.RESTClientGetter, args)
clientConfig, err := flags.RESTClientGetter.ToRESTConfig()
if err != nil {
return nil, err
}
dynamicClient, err := dynamic.NewForConfig(clientConfig)
if err != nil {
return nil, err
}
conditionFn, err := conditionFuncFor(flags.ForCondition)
if err != nil {
return nil, err
}
effectiveTimeout := flags.Timeout
if effectiveTimeout < 0 {
effectiveTimeout = 168 * time.Hour
}
o := &WaitOptions{
ResourceFinder: builder,
DynamicClient: dynamicClient,
Timeout: effectiveTimeout,
Printer: printer,
ConditionFn: conditionFn,
IOStreams: flags.IOStreams,
}
return o, nil
}
func conditionFuncFor(condition string) (ConditionFunc, error) {
if strings.ToLower(condition) == "delete" {
return IsDeleted, nil
}
if strings.HasPrefix(condition, "condition=") {
conditionName := condition[len("condition="):]
return ConditionalWait{
conditionName: conditionName,
// TODO allow specifying a false
conditionStatus: "true",
}.IsConditionMet, nil
}
return nil, fmt.Errorf("unrecognized condition: %q", condition)
}
// WaitOptions is a set of options that allows you to wait. This is the object reflects the runtime needs of a wait
// command, making the logic itself easy to unit test with our existing mocks.
type WaitOptions struct {
ResourceFinder ResourceFinder
DynamicClient dynamic.Interface
Timeout time.Duration
Printer printers.ResourcePrinter
ConditionFn ConditionFunc
genericclioptions.IOStreams
}
// ConditionFunc is the interface for providing condition checks
type ConditionFunc func(info *resource.Info, o *WaitOptions) (finalObject runtime.Object, done bool, err error)
// RunWait runs the waiting logic
func (o *WaitOptions) RunWait() error {
return o.ResourceFinder.Do().Visit(func(info *resource.Info, err error) error {
if err != nil {
return err
}
finalObject, success, err := o.ConditionFn(info, o)
if success {
o.Printer.PrintObj(finalObject, o.Out)
return nil
}
if err == nil {
return fmt.Errorf("%v unsatisified for unknown reason", finalObject)
}
return err
})
}
// IsDeleted is a condition func for waiting for something to be deleted
func IsDeleted(info *resource.Info, o *WaitOptions) (runtime.Object, bool, error) {
endTime := time.Now().Add(o.Timeout)
for {
gottenObj, err := o.DynamicClient.Resource(info.Mapping.Resource).Namespace(info.Namespace).Get(info.Name, metav1.GetOptions{})
if errors.IsNotFound(err) {
return info.Object, true, nil
}
if err != nil {
// TODO this could do something slightly fancier if we wish
return info.Object, false, err
}
watchOptions := metav1.ListOptions{}
watchOptions.FieldSelector = "metadata.name=" + info.Name
watchOptions.ResourceVersion = gottenObj.GetResourceVersion()
objWatch, err := o.DynamicClient.Resource(info.Mapping.Resource).Namespace(info.Namespace).Watch(watchOptions)
if err != nil {
return gottenObj, false, err
}
timeout := endTime.Sub(time.Now())
if timeout < 0 {
// we're out of time
return gottenObj, false, wait.ErrWaitTimeout
}
watchEvent, err := watch.Until(o.Timeout, objWatch, isDeleted)
switch {
case err == nil:
return watchEvent.Object, true, nil
case err == watch.ErrWatchClosed:
continue
case err == wait.ErrWaitTimeout:
if watchEvent != nil {
return watchEvent.Object, false, wait.ErrWaitTimeout
}
return gottenObj, false, wait.ErrWaitTimeout
default:
return gottenObj, false, err
}
}
}
func isDeleted(event watch.Event) (bool, error) {
return event.Type == watch.Deleted, nil
}
// ConditionalWait hold information to check an API status condition
type ConditionalWait struct {
conditionName string
conditionStatus string
}
// IsConditionMet is a conditionfunc for waiting on an API condition to be met
func (w ConditionalWait) IsConditionMet(info *resource.Info, o *WaitOptions) (runtime.Object, bool, error) {
endTime := time.Now().Add(o.Timeout)
for {
resourceVersion := ""
gottenObj, err := o.DynamicClient.Resource(info.Mapping.Resource).Namespace(info.Namespace).Get(info.Name, metav1.GetOptions{})
switch {
case errors.IsNotFound(err):
resourceVersion = "0"
case err != nil:
return info.Object, false, err
default:
conditionMet, err := w.checkCondition(gottenObj)
if conditionMet {
return gottenObj, true, nil
}
if err != nil {
return gottenObj, false, err
}
resourceVersion = gottenObj.GetResourceVersion()
}
watchOptions := metav1.ListOptions{}
watchOptions.FieldSelector = "metadata.name=" + info.Name
watchOptions.ResourceVersion = resourceVersion
objWatch, err := o.DynamicClient.Resource(info.Mapping.Resource).Namespace(info.Namespace).Watch(watchOptions)
if err != nil {
return gottenObj, false, err
}
timeout := endTime.Sub(time.Now())
if timeout < 0 {
// we're out of time
return gottenObj, false, wait.ErrWaitTimeout
}
watchEvent, err := watch.Until(o.Timeout, objWatch, w.isConditionMet)
switch {
case err == nil:
return watchEvent.Object, true, nil
case err == watch.ErrWatchClosed:
continue
case err == wait.ErrWaitTimeout:
if watchEvent != nil {
return watchEvent.Object, false, wait.ErrWaitTimeout
}
return gottenObj, false, wait.ErrWaitTimeout
default:
return gottenObj, false, err
}
}
}
func (w ConditionalWait) checkCondition(obj *unstructured.Unstructured) (bool, error) {
conditions, found, err := unstructured.NestedSlice(obj.Object, "status", "conditions")
if err != nil {
return false, err
}
if !found {
return false, nil
}
for _, conditionUncast := range conditions {
condition := conditionUncast.(map[string]interface{})
name, found, err := unstructured.NestedString(condition, "type")
if !found || err != nil || strings.ToLower(name) != strings.ToLower(w.conditionName) {
continue
}
status, found, err := unstructured.NestedString(condition, "status")
if !found || err != nil {
continue
}
return strings.ToLower(status) == strings.ToLower(w.conditionStatus), nil
}
return false, nil
}
func (w ConditionalWait) isConditionMet(event watch.Event) (bool, error) {
if event.Type == watch.Deleted {
// this will chain back out, result in another get and an return false back up the chain
return false, nil
}
obj := event.Object.(*unstructured.Unstructured)
return w.checkCondition(obj)
}
// NewDiscardingPrinter is a printer that discards all objects
// TODO use the real discarding printer from a different pull I just opened.
func NewDiscardingPrinter() printers.ResourcePrinterFunc {
return printers.ResourcePrinterFunc(func(runtime.Object, io.Writer) error {
return nil
})
}

View File

@ -0,0 +1,477 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package wait
import (
"testing"
"time"
"strings"
"github.com/davecgh/go-spew/spew"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch"
dynamicfakeclient "k8s.io/client-go/dynamic/fake"
clienttesting "k8s.io/client-go/testing"
"k8s.io/kubernetes/pkg/kubectl/genericclioptions"
"k8s.io/kubernetes/pkg/kubectl/genericclioptions/resource"
)
func newUnstructured(apiVersion, kind, namespace, name string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": apiVersion,
"kind": kind,
"metadata": map[string]interface{}{
"namespace": namespace,
"name": name,
},
},
}
}
func addCondition(in *unstructured.Unstructured, name, status string) *unstructured.Unstructured {
conditions, _, _ := unstructured.NestedSlice(in.Object, "status", "conditions")
conditions = append(conditions, map[string]interface{}{
"type": name,
"status": status,
})
unstructured.SetNestedSlice(in.Object, conditions, "status", "conditions")
return in
}
func TestWaitForDeletion(t *testing.T) {
scheme := runtime.NewScheme()
tests := []struct {
name string
info *resource.Info
fakeClient func() *dynamicfakeclient.FakeDynamicClient
timeout time.Duration
expectedErr string
validateActions func(t *testing.T, actions []clienttesting.Action)
}{
{
name: "missing on get",
info: &resource.Info{
Mapping: &meta.RESTMapping{
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
},
Name: "name-foo",
Namespace: "ns-foo",
},
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
return dynamicfakeclient.NewSimpleDynamicClient(scheme)
},
timeout: 10 * time.Second,
validateActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 1 {
t.Fatal(spew.Sdump(actions))
}
if !actions[0].Matches("get", "theresource") || actions[0].(clienttesting.GetAction).GetName() != "name-foo" {
t.Error(spew.Sdump(actions))
}
},
},
{
name: "times out",
info: &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.NewSimpleDynamicClient(scheme)
fakeClient.PrependReactor("get", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
return true, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"), nil
})
return fakeClient
},
timeout: 1 * time.Second,
expectedErr: wait.ErrWaitTimeout.Error(),
validateActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 2 {
t.Fatal(spew.Sdump(actions))
}
if !actions[0].Matches("get", "theresource") || actions[0].(clienttesting.GetAction).GetName() != "name-foo" {
t.Error(spew.Sdump(actions))
}
if !actions[1].Matches("watch", "theresource") {
t.Error(spew.Sdump(actions))
}
},
},
{
name: "handles watch close out",
info: &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.NewSimpleDynamicClient(scheme)
fakeClient.PrependReactor("get", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
return true, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"), nil
})
count := 0
fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
if count == 0 {
count++
fakeWatch := watch.NewRaceFreeFake()
go func() {
time.Sleep(100 * time.Millisecond)
fakeWatch.Stop()
}()
return true, fakeWatch, nil
}
fakeWatch := watch.NewRaceFreeFake()
return true, fakeWatch, nil
})
return fakeClient
},
timeout: 3 * time.Second,
expectedErr: wait.ErrWaitTimeout.Error(),
validateActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 4 {
t.Fatal(spew.Sdump(actions))
}
if !actions[0].Matches("get", "theresource") || actions[0].(clienttesting.GetAction).GetName() != "name-foo" {
t.Error(spew.Sdump(actions))
}
if !actions[1].Matches("watch", "theresource") {
t.Error(spew.Sdump(actions))
}
if !actions[2].Matches("get", "theresource") || actions[2].(clienttesting.GetAction).GetName() != "name-foo" {
t.Error(spew.Sdump(actions))
}
if !actions[3].Matches("watch", "theresource") {
t.Error(spew.Sdump(actions))
}
},
},
{
name: "handles watch delete",
info: &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.NewSimpleDynamicClient(scheme)
fakeClient.PrependReactor("get", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
return true, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"), nil
})
fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
fakeWatch := watch.NewRaceFreeFake()
fakeWatch.Action(watch.Deleted, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"))
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("get", "theresource") || actions[0].(clienttesting.GetAction).GetName() != "name-foo" {
t.Error(spew.Sdump(actions))
}
if !actions[1].Matches("watch", "theresource") {
t.Error(spew.Sdump(actions))
}
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
fakeClient := test.fakeClient()
o := &WaitOptions{
ResourceFinder: NewSimpleResourceFinder(test.info),
DynamicClient: fakeClient,
Timeout: test.timeout,
Printer: NewDiscardingPrinter(),
ConditionFn: IsDeleted,
IOStreams: genericclioptions.NewTestIOStreamsDiscard(),
}
err := o.RunWait()
switch {
case err == nil && len(test.expectedErr) == 0:
case err != nil && len(test.expectedErr) == 0:
t.Fatal(err)
case err == nil && len(test.expectedErr) != 0:
t.Fatalf("missing: %q", test.expectedErr)
case err != nil && len(test.expectedErr) != 0:
if !strings.Contains(err.Error(), test.expectedErr) {
t.Fatalf("expected %q, got %q", test.expectedErr, err.Error())
}
}
test.validateActions(t, fakeClient.Actions())
})
}
}
func TestWaitForCondition(t *testing.T) {
scheme := runtime.NewScheme()
tests := []struct {
name string
info *resource.Info
fakeClient func() *dynamicfakeclient.FakeDynamicClient
timeout time.Duration
expectedErr string
validateActions func(t *testing.T, actions []clienttesting.Action)
}{
{
name: "present on get",
info: &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.NewSimpleDynamicClient(scheme)
fakeClient.PrependReactor("get", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
return true, addCondition(
newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
"the-condition", "status-value",
), nil
})
return fakeClient
},
timeout: 10 * time.Second,
validateActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 1 {
t.Fatal(spew.Sdump(actions))
}
if !actions[0].Matches("get", "theresource") || actions[0].(clienttesting.GetAction).GetName() != "name-foo" {
t.Error(spew.Sdump(actions))
}
},
},
{
name: "times out",
info: &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.NewSimpleDynamicClient(scheme)
fakeClient.PrependReactor("get", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
return true, addCondition(
newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
"some-other-condition", "status-value",
), nil
})
return fakeClient
},
timeout: 1 * time.Second,
expectedErr: wait.ErrWaitTimeout.Error(),
validateActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 2 {
t.Fatal(spew.Sdump(actions))
}
if !actions[0].Matches("get", "theresource") || actions[0].(clienttesting.GetAction).GetName() != "name-foo" {
t.Error(spew.Sdump(actions))
}
if !actions[1].Matches("watch", "theresource") {
t.Error(spew.Sdump(actions))
}
},
},
{
name: "handles watch close out",
info: &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.NewSimpleDynamicClient(scheme)
fakeClient.PrependReactor("get", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
return true, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"), nil
})
count := 0
fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
if count == 0 {
count++
fakeWatch := watch.NewRaceFreeFake()
go func() {
time.Sleep(100 * time.Millisecond)
fakeWatch.Stop()
}()
return true, fakeWatch, nil
}
fakeWatch := watch.NewRaceFreeFake()
return true, fakeWatch, nil
})
return fakeClient
},
timeout: 3 * time.Second,
expectedErr: wait.ErrWaitTimeout.Error(),
validateActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 4 {
t.Fatal(spew.Sdump(actions))
}
if !actions[0].Matches("get", "theresource") || actions[0].(clienttesting.GetAction).GetName() != "name-foo" {
t.Error(spew.Sdump(actions))
}
if !actions[1].Matches("watch", "theresource") {
t.Error(spew.Sdump(actions))
}
if !actions[2].Matches("get", "theresource") || actions[2].(clienttesting.GetAction).GetName() != "name-foo" {
t.Error(spew.Sdump(actions))
}
if !actions[3].Matches("watch", "theresource") {
t.Error(spew.Sdump(actions))
}
},
},
{
name: "handles watch condition change",
info: &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.NewSimpleDynamicClient(scheme)
fakeClient.PrependReactor("get", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
return true, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"), nil
})
fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
fakeWatch := watch.NewRaceFreeFake()
fakeWatch.Action(watch.Modified, addCondition(
newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
"the-condition", "status-value",
))
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("get", "theresource") || actions[0].(clienttesting.GetAction).GetName() != "name-foo" {
t.Error(spew.Sdump(actions))
}
if !actions[1].Matches("watch", "theresource") {
t.Error(spew.Sdump(actions))
}
},
},
{
name: "handles watch created",
info: &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.NewSimpleDynamicClient(scheme)
fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
fakeWatch := watch.NewRaceFreeFake()
fakeWatch.Action(watch.Added, addCondition(
newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
"the-condition", "status-value",
))
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("get", "theresource") || actions[0].(clienttesting.GetAction).GetName() != "name-foo" {
t.Error(spew.Sdump(actions))
}
if !actions[1].Matches("watch", "theresource") {
t.Error(spew.Sdump(actions))
}
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
fakeClient := test.fakeClient()
o := &WaitOptions{
ResourceFinder: NewSimpleResourceFinder(test.info),
DynamicClient: fakeClient,
Timeout: test.timeout,
Printer: NewDiscardingPrinter(),
ConditionFn: ConditionalWait{conditionName: "the-condition", conditionStatus: "status-value"}.IsConditionMet,
IOStreams: genericclioptions.NewTestIOStreamsDiscard(),
}
err := o.RunWait()
switch {
case err == nil && len(test.expectedErr) == 0:
case err != nil && len(test.expectedErr) == 0:
t.Fatal(err)
case err == nil && len(test.expectedErr) != 0:
t.Fatalf("missing: %q", test.expectedErr)
case err != nil && len(test.expectedErr) != 0:
if !strings.Contains(err.Error(), test.expectedErr) {
t.Fatalf("expected %q, got %q", test.expectedErr, err.Error())
}
}
test.validateActions(t, fakeClient.Actions())
})
}
}

View File

@ -83,3 +83,15 @@ func (c *clientOptions) Put() *rest.Request {
type ContentValidator interface {
ValidateBytes(data []byte) error
}
// Visitor lets clients walk a list of resources.
type Visitor interface {
Visit(VisitorFunc) error
}
// VisitorFunc implements the Visitor interface for a matching function.
// If there was a problem walking a list of resources, the incoming error
// will describe the problem and the function can decide how to handle that error.
// A nil returned indicates to accept an error to continue loops even when errors happen.
// This is useful for ignoring certain kinds of errors or aggregating errors in some way.
type VisitorFunc func(*Info, error) error

View File

@ -45,18 +45,6 @@ const (
stopValidateMessage = "if you choose to ignore these errors, turn validation off with --validate=false"
)
// Visitor lets clients walk a list of resources.
type Visitor interface {
Visit(VisitorFunc) error
}
// VisitorFunc implements the Visitor interface for a matching function.
// If there was a problem walking a list of resources, the incoming error
// will describe the problem and the function can decide how to handle that error.
// A nil returned indicates to accept an error to continue loops even when errors happen.
// This is useful for ignoring certain kinds of errors or aggregating errors in some way.
type VisitorFunc func(*Info, error) error
// Watchable describes a resource that can be watched for changes that occur on the server,
// beginning after the provided resource version.
type Watchable interface {