mirror of https://github.com/k3s-io/k3s
add wait
parent
e85b81bbee
commit
76794643c5
|
@ -177,6 +177,7 @@ package_group(
|
||||||
"//pkg/kubectl/cmd/templates",
|
"//pkg/kubectl/cmd/templates",
|
||||||
"//pkg/kubectl/cmd/util",
|
"//pkg/kubectl/cmd/util",
|
||||||
"//pkg/kubectl/cmd/util/sanity",
|
"//pkg/kubectl/cmd/util/sanity",
|
||||||
|
"//pkg/kubectl/cmd/wait",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -196,6 +197,7 @@ package_group(
|
||||||
"//pkg/kubectl/cmd/get",
|
"//pkg/kubectl/cmd/get",
|
||||||
"//pkg/kubectl/cmd/rollout",
|
"//pkg/kubectl/cmd/rollout",
|
||||||
"//pkg/kubectl/cmd/set",
|
"//pkg/kubectl/cmd/set",
|
||||||
|
"//pkg/kubectl/cmd/wait",
|
||||||
"//pkg/kubectl/explain",
|
"//pkg/kubectl/explain",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -230,6 +232,7 @@ package_group(
|
||||||
"//pkg/kubectl/cmd/testing",
|
"//pkg/kubectl/cmd/testing",
|
||||||
"//pkg/kubectl/cmd/util",
|
"//pkg/kubectl/cmd/util",
|
||||||
"//pkg/kubectl/cmd/util/editor",
|
"//pkg/kubectl/cmd/util/editor",
|
||||||
|
"//pkg/kubectl/cmd/wait",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -150,6 +150,7 @@ pkg/kubectl/cmd/util
|
||||||
pkg/kubectl/cmd/util/editor
|
pkg/kubectl/cmd/util/editor
|
||||||
pkg/kubectl/cmd/util/jsonmerge
|
pkg/kubectl/cmd/util/jsonmerge
|
||||||
pkg/kubectl/cmd/util/sanity
|
pkg/kubectl/cmd/util/sanity
|
||||||
|
pkg/kubectl/cmd/wait
|
||||||
pkg/kubectl/genericclioptions
|
pkg/kubectl/genericclioptions
|
||||||
pkg/kubectl/genericclioptions/printers
|
pkg/kubectl/genericclioptions/printers
|
||||||
pkg/kubectl/genericclioptions/resource
|
pkg/kubectl/genericclioptions/resource
|
||||||
|
|
|
@ -2382,7 +2382,11 @@ run_namespace_tests() {
|
||||||
# Post-condition: namespace 'my-namespace' is created.
|
# Post-condition: namespace 'my-namespace' is created.
|
||||||
kube::test::get_object_assert 'namespaces/my-namespace' "{{$id_field}}" 'my-namespace'
|
kube::test::get_object_assert 'namespaces/my-namespace' "{{$id_field}}" 'my-namespace'
|
||||||
# Clean up
|
# 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 #
|
# Pods in Namespaces #
|
||||||
|
|
|
@ -77,6 +77,7 @@ go_library(
|
||||||
"//pkg/kubectl/cmd/util:go_default_library",
|
"//pkg/kubectl/cmd/util:go_default_library",
|
||||||
"//pkg/kubectl/cmd/util/editor:go_default_library",
|
"//pkg/kubectl/cmd/util/editor:go_default_library",
|
||||||
"//pkg/kubectl/cmd/util/openapi: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/explain:go_default_library",
|
||||||
"//pkg/kubectl/genericclioptions:go_default_library",
|
"//pkg/kubectl/genericclioptions:go_default_library",
|
||||||
"//pkg/kubectl/genericclioptions/printers: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/testdata/edit:all-srcs",
|
||||||
"//pkg/kubectl/cmd/testing:all-srcs",
|
"//pkg/kubectl/cmd/testing:all-srcs",
|
||||||
"//pkg/kubectl/cmd/util:all-srcs",
|
"//pkg/kubectl/cmd/util:all-srcs",
|
||||||
|
"//pkg/kubectl/cmd/wait:all-srcs",
|
||||||
],
|
],
|
||||||
tags = ["automanaged"],
|
tags = ["automanaged"],
|
||||||
visibility = [
|
visibility = [
|
||||||
|
|
|
@ -207,7 +207,11 @@ func (o *ApplyOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error {
|
||||||
return err
|
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.ShouldIncludeUninitialized = cmdutil.ShouldIncludeUninitialized(cmd, o.Prune)
|
||||||
|
|
||||||
o.OpenAPISchema, _ = f.OpenAPISchema()
|
o.OpenAPISchema, _ = f.OpenAPISchema()
|
||||||
|
|
|
@ -523,6 +523,7 @@ func TestApplyObject(t *testing.T) {
|
||||||
}
|
}
|
||||||
tf.OpenAPISchemaFunc = fn
|
tf.OpenAPISchemaFunc = fn
|
||||||
tf.Namespace = "test"
|
tf.Namespace = "test"
|
||||||
|
tf.ClientConfigVal = defaultClientConfig()
|
||||||
|
|
||||||
ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
|
ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
|
||||||
cmd := NewCmdApply("kubectl", tf, ioStreams)
|
cmd := NewCmdApply("kubectl", tf, ioStreams)
|
||||||
|
@ -587,6 +588,7 @@ func TestApplyObjectOutput(t *testing.T) {
|
||||||
}
|
}
|
||||||
tf.OpenAPISchemaFunc = fn
|
tf.OpenAPISchemaFunc = fn
|
||||||
tf.Namespace = "test"
|
tf.Namespace = "test"
|
||||||
|
tf.ClientConfigVal = defaultClientConfig()
|
||||||
|
|
||||||
ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
|
ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
|
||||||
cmd := NewCmdApply("kubectl", tf, ioStreams)
|
cmd := NewCmdApply("kubectl", tf, ioStreams)
|
||||||
|
@ -648,6 +650,7 @@ func TestApplyRetry(t *testing.T) {
|
||||||
}
|
}
|
||||||
tf.OpenAPISchemaFunc = fn
|
tf.OpenAPISchemaFunc = fn
|
||||||
tf.Namespace = "test"
|
tf.Namespace = "test"
|
||||||
|
tf.ClientConfigVal = defaultClientConfig()
|
||||||
|
|
||||||
ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
|
ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
|
||||||
cmd := NewCmdApply("kubectl", tf, ioStreams)
|
cmd := NewCmdApply("kubectl", tf, ioStreams)
|
||||||
|
@ -697,6 +700,7 @@ func TestApplyNonExistObject(t *testing.T) {
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
tf.Namespace = "test"
|
tf.Namespace = "test"
|
||||||
|
tf.ClientConfigVal = defaultClientConfig()
|
||||||
|
|
||||||
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||||
cmd := NewCmdApply("kubectl", tf, ioStreams)
|
cmd := NewCmdApply("kubectl", tf, ioStreams)
|
||||||
|
@ -749,6 +753,7 @@ func TestApplyEmptyPatch(t *testing.T) {
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
tf.Namespace = "test"
|
tf.Namespace = "test"
|
||||||
|
tf.ClientConfigVal = defaultClientConfig()
|
||||||
|
|
||||||
// 1. apply non exist object
|
// 1. apply non exist object
|
||||||
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||||
|
@ -823,6 +828,7 @@ func testApplyMultipleObjects(t *testing.T, asList bool) {
|
||||||
}
|
}
|
||||||
tf.OpenAPISchemaFunc = fn
|
tf.OpenAPISchemaFunc = fn
|
||||||
tf.Namespace = "test"
|
tf.Namespace = "test"
|
||||||
|
tf.ClientConfigVal = defaultClientConfig()
|
||||||
|
|
||||||
ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
|
ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
|
||||||
cmd := NewCmdApply("kubectl", tf, ioStreams)
|
cmd := NewCmdApply("kubectl", tf, ioStreams)
|
||||||
|
@ -923,6 +929,7 @@ func TestApplyNULLPreservation(t *testing.T) {
|
||||||
}
|
}
|
||||||
tf.OpenAPISchemaFunc = fn
|
tf.OpenAPISchemaFunc = fn
|
||||||
tf.Namespace = "test"
|
tf.Namespace = "test"
|
||||||
|
tf.ClientConfigVal = defaultClientConfig()
|
||||||
|
|
||||||
ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
|
ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
|
||||||
cmd := NewCmdApply("kubectl", tf, ioStreams)
|
cmd := NewCmdApply("kubectl", tf, ioStreams)
|
||||||
|
@ -989,6 +996,7 @@ func TestUnstructuredApply(t *testing.T) {
|
||||||
}
|
}
|
||||||
tf.OpenAPISchemaFunc = fn
|
tf.OpenAPISchemaFunc = fn
|
||||||
tf.Namespace = "test"
|
tf.Namespace = "test"
|
||||||
|
tf.ClientConfigVal = defaultClientConfig()
|
||||||
|
|
||||||
ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
|
ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
|
||||||
cmd := NewCmdApply("kubectl", tf, ioStreams)
|
cmd := NewCmdApply("kubectl", tf, ioStreams)
|
||||||
|
@ -1054,6 +1062,7 @@ func TestUnstructuredIdempotentApply(t *testing.T) {
|
||||||
}
|
}
|
||||||
tf.OpenAPISchemaFunc = fn
|
tf.OpenAPISchemaFunc = fn
|
||||||
tf.Namespace = "test"
|
tf.Namespace = "test"
|
||||||
|
tf.ClientConfigVal = defaultClientConfig()
|
||||||
|
|
||||||
ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
|
ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
|
||||||
cmd := NewCmdApply("kubectl", tf, ioStreams)
|
cmd := NewCmdApply("kubectl", tf, ioStreams)
|
||||||
|
@ -1223,6 +1232,7 @@ func TestForceApply(t *testing.T) {
|
||||||
tf := cmdtesting.NewTestFactory()
|
tf := cmdtesting.NewTestFactory()
|
||||||
defer tf.Cleanup()
|
defer tf.Cleanup()
|
||||||
|
|
||||||
|
tf.ClientConfigVal = defaultClientConfig()
|
||||||
tf.UnstructuredClient = &fake.RESTClient{
|
tf.UnstructuredClient = &fake.RESTClient{
|
||||||
NegotiatedSerializer: unstructuredSerializer,
|
NegotiatedSerializer: unstructuredSerializer,
|
||||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||||
|
|
|
@ -33,6 +33,7 @@ import (
|
||||||
"k8s.io/kubernetes/pkg/kubectl/cmd/set"
|
"k8s.io/kubernetes/pkg/kubectl/cmd/set"
|
||||||
"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
|
"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
|
||||||
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
||||||
|
"k8s.io/kubernetes/pkg/kubectl/cmd/wait"
|
||||||
"k8s.io/kubernetes/pkg/kubectl/util/i18n"
|
"k8s.io/kubernetes/pkg/kubectl/util/i18n"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
@ -362,6 +363,7 @@ func NewKubectlCommand(in io.Reader, out, err io.Writer) *cobra.Command {
|
||||||
NewCmdApply("kubectl", f, ioStreams),
|
NewCmdApply("kubectl", f, ioStreams),
|
||||||
NewCmdPatch(f, ioStreams),
|
NewCmdPatch(f, ioStreams),
|
||||||
NewCmdReplace(f, ioStreams),
|
NewCmdReplace(f, ioStreams),
|
||||||
|
wait.NewCmdWait(f, ioStreams),
|
||||||
NewCmdConvert(f, ioStreams),
|
NewCmdConvert(f, ioStreams),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -21,15 +21,18 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang/glog"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/api/errors"
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
"k8s.io/kubernetes/pkg/kubectl"
|
"k8s.io/kubernetes/pkg/kubectl"
|
||||||
"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
|
"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
|
||||||
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
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"
|
||||||
"k8s.io/kubernetes/pkg/kubectl/genericclioptions/resource"
|
"k8s.io/kubernetes/pkg/kubectl/genericclioptions/resource"
|
||||||
"k8s.io/kubernetes/pkg/kubectl/util/i18n"
|
"k8s.io/kubernetes/pkg/kubectl/util/i18n"
|
||||||
|
@ -106,8 +109,9 @@ type DeleteOptions struct {
|
||||||
|
|
||||||
Output string
|
Output string
|
||||||
|
|
||||||
Mapper meta.RESTMapper
|
DynamicClient dynamic.Interface
|
||||||
Result *resource.Result
|
Mapper meta.RESTMapper
|
||||||
|
Result *resource.Result
|
||||||
|
|
||||||
genericclioptions.IOStreams
|
genericclioptions.IOStreams
|
||||||
}
|
}
|
||||||
|
@ -122,7 +126,7 @@ func NewCmdDelete(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra
|
||||||
Long: delete_long,
|
Long: delete_long,
|
||||||
Example: delete_example,
|
Example: delete_example,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
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 {
|
if err := o.Complete(f, args, cmd); err != nil {
|
||||||
cmdutil.CheckErr(err)
|
cmdutil.CheckErr(err)
|
||||||
}
|
}
|
||||||
|
@ -138,6 +142,8 @@ func NewCmdDelete(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra
|
||||||
|
|
||||||
deleteFlags.AddFlags(cmd)
|
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)
|
cmdutil.AddIncludeUninitializedFlag(cmd)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
@ -167,6 +173,9 @@ func (o *DeleteOptions) Complete(f cmdutil.Factory, args []string, cmd *cobra.Co
|
||||||
o.WaitForDeletion = true
|
o.WaitForDeletion = true
|
||||||
o.GracePeriod = 1
|
o.GracePeriod = 1
|
||||||
}
|
}
|
||||||
|
if b, err := cmd.Flags().GetBool("wait"); err == nil {
|
||||||
|
o.WaitForDeletion = b
|
||||||
|
}
|
||||||
|
|
||||||
o.Reaper = f.Reaper
|
o.Reaper = f.Reaper
|
||||||
|
|
||||||
|
@ -194,6 +203,11 @@ func (o *DeleteOptions) Complete(f cmdutil.Factory, args []string, cmd *cobra.Co
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
o.DynamicClient, err = f.DynamicClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -300,8 +314,38 @@ func (o *DeleteOptions) DeleteResult(r *resource.Result) error {
|
||||||
}
|
}
|
||||||
if found == 0 {
|
if found == 0 {
|
||||||
fmt.Fprintf(o.Out, "No resources found\n")
|
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 {
|
func (o *DeleteOptions) cascadingDeleteResource(info *resource.Info) error {
|
||||||
|
|
|
@ -21,6 +21,7 @@ import (
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
"k8s.io/kubernetes/pkg/kubectl"
|
"k8s.io/kubernetes/pkg/kubectl"
|
||||||
"k8s.io/kubernetes/pkg/kubectl/genericclioptions"
|
"k8s.io/kubernetes/pkg/kubectl/genericclioptions"
|
||||||
"k8s.io/kubernetes/pkg/kubectl/genericclioptions/resource"
|
"k8s.io/kubernetes/pkg/kubectl/genericclioptions/resource"
|
||||||
|
@ -72,9 +73,10 @@ type DeleteFlags struct {
|
||||||
Output *string
|
Output *string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *DeleteFlags) ToOptions(streams genericclioptions.IOStreams) *DeleteOptions {
|
func (f *DeleteFlags) ToOptions(dynamicClient dynamic.Interface, streams genericclioptions.IOStreams) *DeleteOptions {
|
||||||
options := &DeleteOptions{
|
options := &DeleteOptions{
|
||||||
IOStreams: streams,
|
DynamicClient: dynamicClient,
|
||||||
|
IOStreams: streams,
|
||||||
}
|
}
|
||||||
|
|
||||||
// add filename options
|
// add filename options
|
||||||
|
|
|
@ -150,7 +150,11 @@ func (o *ReplaceOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []
|
||||||
return printer.PrintObj(obj, o.Out)
|
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
|
//Replace will create a resource if it doesn't exist already, so ignore not found error
|
||||||
deleteOpts.IgnoreNotFound = true
|
deleteOpts.IgnoreNotFound = true
|
||||||
|
|
|
@ -223,7 +223,7 @@ func (o *RunOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error {
|
||||||
return printer.PrintObj(obj, o.Out)
|
return printer.PrintObj(obj, o.Out)
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteOpts := o.DeleteFlags.ToOptions(o.IOStreams)
|
deleteOpts := o.DeleteFlags.ToOptions(o.DynamicClient, o.IOStreams)
|
||||||
deleteOpts.IgnoreNotFound = true
|
deleteOpts.IgnoreNotFound = true
|
||||||
deleteOpts.WaitForDeletion = false
|
deleteOpts.WaitForDeletion = false
|
||||||
deleteOpts.GracePeriod = -1
|
deleteOpts.GracePeriod = -1
|
||||||
|
|
|
@ -207,7 +207,7 @@ func TestRunArgsFollowDashRules(t *testing.T) {
|
||||||
deleteFlags := NewDeleteFlags("to use to replace the resource.")
|
deleteFlags := NewDeleteFlags("to use to replace the resource.")
|
||||||
opts := &RunOptions{
|
opts := &RunOptions{
|
||||||
PrintFlags: printFlags,
|
PrintFlags: printFlags,
|
||||||
DeleteOptions: deleteFlags.ToOptions(genericclioptions.NewTestIOStreamsDiscard()),
|
DeleteOptions: deleteFlags.ToOptions(nil, genericclioptions.NewTestIOStreamsDiscard()),
|
||||||
|
|
||||||
IOStreams: genericclioptions.NewTestIOStreamsDiscard(),
|
IOStreams: genericclioptions.NewTestIOStreamsDiscard(),
|
||||||
|
|
||||||
|
@ -376,7 +376,7 @@ func TestGenerateService(t *testing.T) {
|
||||||
deleteFlags := NewDeleteFlags("to use to replace the resource.")
|
deleteFlags := NewDeleteFlags("to use to replace the resource.")
|
||||||
opts := &RunOptions{
|
opts := &RunOptions{
|
||||||
PrintFlags: printFlags,
|
PrintFlags: printFlags,
|
||||||
DeleteOptions: deleteFlags.ToOptions(genericclioptions.NewTestIOStreamsDiscard()),
|
DeleteOptions: deleteFlags.ToOptions(nil, genericclioptions.NewTestIOStreamsDiscard()),
|
||||||
|
|
||||||
IOStreams: ioStreams,
|
IOStreams: ioStreams,
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
],
|
||||||
|
)
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
|
@ -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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -83,3 +83,15 @@ func (c *clientOptions) Put() *rest.Request {
|
||||||
type ContentValidator interface {
|
type ContentValidator interface {
|
||||||
ValidateBytes(data []byte) error
|
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
|
||||||
|
|
|
@ -45,18 +45,6 @@ const (
|
||||||
stopValidateMessage = "if you choose to ignore these errors, turn validation off with --validate=false"
|
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,
|
// Watchable describes a resource that can be watched for changes that occur on the server,
|
||||||
// beginning after the provided resource version.
|
// beginning after the provided resource version.
|
||||||
type Watchable interface {
|
type Watchable interface {
|
||||||
|
|
Loading…
Reference in New Issue