Commit 76794643 authored by David Eads's avatar David Eads

add wait

parent e85b81bb
......@@ -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",
],
)
......
......@@ -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
......
......@@ -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 #
......
......@@ -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 = [
......
......@@ -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()
......
......@@ -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) {
......
......@@ -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),
},
},
......
......@@ -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 {
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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,
......
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",
],
)
/*
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
}
/*
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
})
}
......@@ -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
......@@ -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 {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment