Merge pull request #37263 from smarterclayton/wait_on_immediate

Automatic merge from submit-queue

When --grace-period=0 is provided, wait for deletion

The grace-period is automatically set to 1 unless --force is provided, and the client waits until the object is deleted.

This preserves backwards compatibility with 1.4 and earlier. It does not handle scenarios where the object is deleted and a new object is created with the same name because we don't have the initial object loaded (and that's a larger change for 1.5).

Fixes #37117 by relaxing the guarantees provided.

```release-note
When deleting an object with `--grace-period=0`, the client will begin a graceful deletion and wait until the resource is fully deleted.  To force deletion, use the `--force` flag.
```
pull/6/head
Kubernetes Submit Queue 2016-11-30 11:15:17 -08:00 committed by GitHub
commit 9ccc291e8a
7 changed files with 179 additions and 47 deletions

View File

@ -490,10 +490,8 @@ runTests() {
# Pre-condition: valid-pod POD exists # Pre-condition: valid-pod POD exists
kubectl create "${kube_flags[@]}" -f test/fixtures/doc-yaml/admin/limitrange/valid-pod.yaml kubectl create "${kube_flags[@]}" -f test/fixtures/doc-yaml/admin/limitrange/valid-pod.yaml
kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" 'valid-pod:' kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" 'valid-pod:'
# Command fails without --force # Command succeeds without --force by waiting
! kubectl delete pod valid-pod "${kube_flags[@]}" --grace-period=0 kubectl delete pod valid-pod "${kube_flags[@]}" --grace-period=0
# Command succeds with --force
kubectl delete pod valid-pod "${kube_flags[@]}" --grace-period=0 --force
# Post-condition: valid-pod POD doesn't exist # Post-condition: valid-pod POD doesn't exist
kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" '' kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" ''

View File

@ -237,7 +237,7 @@ func NewKubectlCommand(f cmdutil.Factory, in io.Reader, out, err io.Writer) *cob
NewCmdGet(f, out, err), NewCmdGet(f, out, err),
NewCmdExplain(f, out, err), NewCmdExplain(f, out, err),
NewCmdEdit(f, out, err), NewCmdEdit(f, out, err),
NewCmdDelete(f, out), NewCmdDelete(f, out, err),
}, },
}, },
{ {

View File

@ -31,6 +31,7 @@ import (
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/resource" "k8s.io/kubernetes/pkg/kubectl/resource"
"k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/util/wait"
) )
var ( var (
@ -88,7 +89,7 @@ var (
kubectl delete pods --all`) kubectl delete pods --all`)
) )
func NewCmdDelete(f cmdutil.Factory, out io.Writer) *cobra.Command { func NewCmdDelete(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command {
options := &resource.FilenameOptions{} options := &resource.FilenameOptions{}
// retrieve a list of handled resources from printer as valid args // retrieve a list of handled resources from printer as valid args
@ -109,7 +110,7 @@ func NewCmdDelete(f cmdutil.Factory, out io.Writer) *cobra.Command {
Example: delete_example, Example: delete_example,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(cmdutil.ValidateOutputArgs(cmd)) cmdutil.CheckErr(cmdutil.ValidateOutputArgs(cmd))
err := RunDelete(f, out, cmd, args, options) err := RunDelete(f, out, errOut, cmd, args, options)
cmdutil.CheckErr(err) cmdutil.CheckErr(err)
}, },
SuggestFor: []string{"rm"}, SuggestFor: []string{"rm"},
@ -131,7 +132,7 @@ func NewCmdDelete(f cmdutil.Factory, out io.Writer) *cobra.Command {
return cmd return cmd
} }
func RunDelete(f cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []string, options *resource.FilenameOptions) error { func RunDelete(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args []string, options *resource.FilenameOptions) error {
cmdNamespace, enforceNamespace, err := f.DefaultNamespace() cmdNamespace, enforceNamespace, err := f.DefaultNamespace()
if err != nil { if err != nil {
return err return err
@ -169,25 +170,35 @@ func RunDelete(f cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []stri
} }
gracePeriod := cmdutil.GetFlagInt(cmd, "grace-period") gracePeriod := cmdutil.GetFlagInt(cmd, "grace-period")
force := cmdutil.GetFlagBool(cmd, "force")
if cmdutil.GetFlagBool(cmd, "now") { if cmdutil.GetFlagBool(cmd, "now") {
if gracePeriod != -1 { if gracePeriod != -1 {
return fmt.Errorf("--now and --grace-period cannot be specified together") return fmt.Errorf("--now and --grace-period cannot be specified together")
} }
gracePeriod = 1 gracePeriod = 1
} }
if gracePeriod == 0 && !cmdutil.GetFlagBool(cmd, "force") { wait := false
return fmt.Errorf("Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely. You must pass --force to delete with grace period 0.") if gracePeriod == 0 {
if force {
fmt.Fprintf(errOut, "warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.\n")
} else {
// To preserve backwards compatibility, but prevent accidental data loss, we convert --grace-period=0
// into --grace-period=1 and wait until the object is successfully deleted. Users may provide --force
// to bypass this wait.
wait = true
gracePeriod = 1
}
} }
shortOutput := cmdutil.GetFlagString(cmd, "output") == "name" shortOutput := cmdutil.GetFlagString(cmd, "output") == "name"
// By default use a reaper to delete all related resources. // By default use a reaper to delete all related resources.
if cmdutil.GetFlagBool(cmd, "cascade") { if cmdutil.GetFlagBool(cmd, "cascade") {
return ReapResult(r, f, out, cmdutil.GetFlagBool(cmd, "cascade"), ignoreNotFound, cmdutil.GetFlagDuration(cmd, "timeout"), gracePeriod, shortOutput, mapper, false) return ReapResult(r, f, out, cmdutil.GetFlagBool(cmd, "cascade"), ignoreNotFound, cmdutil.GetFlagDuration(cmd, "timeout"), gracePeriod, wait, shortOutput, mapper, false)
} }
return DeleteResult(r, out, ignoreNotFound, shortOutput, mapper) return DeleteResult(r, out, ignoreNotFound, shortOutput, mapper)
} }
func ReapResult(r *resource.Result, f cmdutil.Factory, out io.Writer, isDefaultDelete, ignoreNotFound bool, timeout time.Duration, gracePeriod int, shortOutput bool, mapper meta.RESTMapper, quiet bool) error { func ReapResult(r *resource.Result, f cmdutil.Factory, out io.Writer, isDefaultDelete, ignoreNotFound bool, timeout time.Duration, gracePeriod int, waitForDeletion, shortOutput bool, mapper meta.RESTMapper, quiet bool) error {
found := 0 found := 0
if ignoreNotFound { if ignoreNotFound {
r = r.IgnoreErrors(errors.IsNotFound) r = r.IgnoreErrors(errors.IsNotFound)
@ -212,6 +223,11 @@ func ReapResult(r *resource.Result, f cmdutil.Factory, out io.Writer, isDefaultD
if err := reaper.Stop(info.Namespace, info.Name, timeout, options); err != nil { if err := reaper.Stop(info.Namespace, info.Name, timeout, options); err != nil {
return cmdutil.AddSourceToErr("stopping", info.Source, err) return cmdutil.AddSourceToErr("stopping", info.Source, err)
} }
if waitForDeletion {
if err := waitForObjectDeletion(info, timeout); err != nil {
return cmdutil.AddSourceToErr("stopping", info.Source, err)
}
}
if !quiet { if !quiet {
cmdutil.PrintSuccess(mapper, shortOutput, out, info.Mapping.Resource, info.Name, false, "deleted") cmdutil.PrintSuccess(mapper, shortOutput, out, info.Mapping.Resource, info.Name, false, "deleted")
} }
@ -254,3 +270,24 @@ func deleteResource(info *resource.Info, out io.Writer, shortOutput bool, mapper
cmdutil.PrintSuccess(mapper, shortOutput, out, info.Mapping.Resource, info.Name, false, "deleted") cmdutil.PrintSuccess(mapper, shortOutput, out, info.Mapping.Resource, info.Name, false, "deleted")
return nil return nil
} }
// objectDeletionWaitInterval is the interval to wait between checks for deletion. Exposed for testing.
var objectDeletionWaitInterval = time.Second
// waitForObjectDeletion refreshes the object, waiting until it is deleted, a timeout is reached, or
// an error is encountered. It checks once a second.
func waitForObjectDeletion(info *resource.Info, timeout time.Duration) error {
copied := *info
info = &copied
// TODO: refactor Reaper so that we can pass the "wait" option into it, and then check for UID change.
return wait.PollImmediate(objectDeletionWaitInterval, timeout, func() (bool, error) {
switch err := info.Get(); {
case err == nil:
return false, nil
case errors.IsNotFound(err):
return true, nil
default:
return false, err
}
})
}

View File

@ -21,15 +21,19 @@ import (
"net/http" "net/http"
"strings" "strings"
"testing" "testing"
"time"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/errors" "k8s.io/kubernetes/pkg/api/errors"
"k8s.io/kubernetes/pkg/api/meta"
"k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/apimachinery/registered" "k8s.io/kubernetes/pkg/apimachinery/registered"
"k8s.io/kubernetes/pkg/client/restclient" "k8s.io/kubernetes/pkg/client/restclient"
"k8s.io/kubernetes/pkg/client/restclient/fake" "k8s.io/kubernetes/pkg/client/restclient/fake"
"k8s.io/kubernetes/pkg/client/typed/dynamic" "k8s.io/kubernetes/pkg/client/typed/dynamic"
"k8s.io/kubernetes/pkg/kubectl"
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing" cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/resource" "k8s.io/kubernetes/pkg/kubectl/resource"
) )
@ -54,9 +58,9 @@ func TestDeleteObjectByTuple(t *testing.T) {
}), }),
} }
tf.Namespace = "test" tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{}) buf, errBuf := bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{})
cmd := NewCmdDelete(f, buf) cmd := NewCmdDelete(f, buf, errBuf)
cmd.Flags().Set("namespace", "test") cmd.Flags().Set("namespace", "test")
cmd.Flags().Set("cascade", "false") cmd.Flags().Set("cascade", "false")
cmd.Flags().Set("output", "name") cmd.Flags().Set("output", "name")
@ -86,9 +90,9 @@ func TestDeleteNamedObject(t *testing.T) {
}), }),
} }
tf.Namespace = "test" tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{}) buf, errBuf := bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{})
cmd := NewCmdDelete(f, buf) cmd := NewCmdDelete(f, buf, errBuf)
cmd.Flags().Set("namespace", "test") cmd.Flags().Set("namespace", "test")
cmd.Flags().Set("cascade", "false") cmd.Flags().Set("cascade", "false")
cmd.Flags().Set("output", "name") cmd.Flags().Set("output", "name")
@ -117,9 +121,9 @@ func TestDeleteObject(t *testing.T) {
}), }),
} }
tf.Namespace = "test" tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{}) buf, errBuf := bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{})
cmd := NewCmdDelete(f, buf) cmd := NewCmdDelete(f, buf, errBuf)
cmd.Flags().Set("filename", "../../../examples/guestbook/legacy/redis-master-controller.yaml") cmd.Flags().Set("filename", "../../../examples/guestbook/legacy/redis-master-controller.yaml")
cmd.Flags().Set("cascade", "false") cmd.Flags().Set("cascade", "false")
cmd.Flags().Set("output", "name") cmd.Flags().Set("output", "name")
@ -131,6 +135,81 @@ func TestDeleteObject(t *testing.T) {
} }
} }
type fakeReaper struct {
namespace, name string
timeout time.Duration
deleteOptions *api.DeleteOptions
err error
}
func (r *fakeReaper) Stop(namespace, name string, timeout time.Duration, gracePeriod *api.DeleteOptions) error {
r.namespace, r.name = namespace, name
r.timeout = timeout
r.deleteOptions = gracePeriod
return r.err
}
type fakeReaperFactory struct {
cmdutil.Factory
reaper kubectl.Reaper
}
func (f *fakeReaperFactory) Reaper(mapping *meta.RESTMapping) (kubectl.Reaper, error) {
return f.reaper, nil
}
func TestDeleteObjectGraceZero(t *testing.T) {
pods, _, _ := testData()
objectDeletionWaitInterval = time.Millisecond
count := 0
f, tf, codec, _ := cmdtesting.NewAPIFactory()
tf.Printer = &testPrinter{}
tf.Client = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
t.Logf("got request %s %s", req.Method, req.URL.Path)
switch p, m := req.URL.Path, req.Method; {
case p == "/namespaces/test/pods/nginx" && m == "GET":
count++
switch count {
case 1, 2, 3:
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, &pods.Items[0])}, nil
default:
return &http.Response{StatusCode: 404, Header: defaultHeader(), Body: objBody(codec, &unversioned.Status{})}, nil
}
case p == "/api/v1/namespaces/test" && m == "GET":
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, &api.Namespace{})}, nil
case p == "/namespaces/test/pods/nginx" && m == "DELETE":
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, &pods.Items[0])}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
}
tf.Namespace = "test"
buf, errBuf := bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{})
reaper := &fakeReaper{}
fake := &fakeReaperFactory{Factory: f, reaper: reaper}
cmd := NewCmdDelete(fake, buf, errBuf)
cmd.Flags().Set("output", "name")
cmd.Flags().Set("grace-period", "0")
cmd.Run(cmd, []string{"pod/nginx"})
// uses the name from the file, not the response
if buf.String() != "pod/nginx\n" {
t.Errorf("unexpected output: %s\n---\n%s", buf.String(), errBuf.String())
}
if reaper.deleteOptions == nil || reaper.deleteOptions.GracePeriodSeconds == nil || *reaper.deleteOptions.GracePeriodSeconds != 1 {
t.Errorf("unexpected reaper options: %#v", reaper)
}
if count != 4 {
t.Errorf("unexpected calls to GET: %d", count)
}
}
func TestDeleteObjectNotFound(t *testing.T) { func TestDeleteObjectNotFound(t *testing.T) {
f, tf, _, _ := cmdtesting.NewAPIFactory() f, tf, _, _ := cmdtesting.NewAPIFactory()
tf.Printer = &testPrinter{} tf.Printer = &testPrinter{}
@ -147,14 +226,14 @@ func TestDeleteObjectNotFound(t *testing.T) {
}), }),
} }
tf.Namespace = "test" tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{}) buf, errBuf := bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{})
cmd := NewCmdDelete(f, buf) cmd := NewCmdDelete(f, buf, errBuf)
options := &resource.FilenameOptions{} options := &resource.FilenameOptions{}
options.Filenames = []string{"../../../examples/guestbook/legacy/redis-master-controller.yaml"} options.Filenames = []string{"../../../examples/guestbook/legacy/redis-master-controller.yaml"}
cmd.Flags().Set("cascade", "false") cmd.Flags().Set("cascade", "false")
cmd.Flags().Set("output", "name") cmd.Flags().Set("output", "name")
err := RunDelete(f, buf, cmd, []string{}, options) err := RunDelete(f, buf, errBuf, cmd, []string{}, options)
if err == nil || !errors.IsNotFound(err) { if err == nil || !errors.IsNotFound(err) {
t.Errorf("unexpected error: expected NotFound, got %v", err) t.Errorf("unexpected error: expected NotFound, got %v", err)
} }
@ -176,9 +255,9 @@ func TestDeleteObjectIgnoreNotFound(t *testing.T) {
}), }),
} }
tf.Namespace = "test" tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{}) buf, errBuf := bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{})
cmd := NewCmdDelete(f, buf) cmd := NewCmdDelete(f, buf, errBuf)
cmd.Flags().Set("filename", "../../../examples/guestbook/legacy/redis-master-controller.yaml") cmd.Flags().Set("filename", "../../../examples/guestbook/legacy/redis-master-controller.yaml")
cmd.Flags().Set("cascade", "false") cmd.Flags().Set("cascade", "false")
cmd.Flags().Set("ignore-not-found", "true") cmd.Flags().Set("ignore-not-found", "true")
@ -216,16 +295,16 @@ func TestDeleteAllNotFound(t *testing.T) {
}), }),
} }
tf.Namespace = "test" tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{}) buf, errBuf := bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{})
cmd := NewCmdDelete(f, buf) cmd := NewCmdDelete(f, buf, errBuf)
cmd.Flags().Set("all", "true") cmd.Flags().Set("all", "true")
cmd.Flags().Set("cascade", "false") cmd.Flags().Set("cascade", "false")
// Make sure we can explicitly choose to fail on NotFound errors, even with --all // Make sure we can explicitly choose to fail on NotFound errors, even with --all
cmd.Flags().Set("ignore-not-found", "false") cmd.Flags().Set("ignore-not-found", "false")
cmd.Flags().Set("output", "name") cmd.Flags().Set("output", "name")
err := RunDelete(f, buf, cmd, []string{"services"}, &resource.FilenameOptions{}) err := RunDelete(f, buf, errBuf, cmd, []string{"services"}, &resource.FilenameOptions{})
if err == nil || !errors.IsNotFound(err) { if err == nil || !errors.IsNotFound(err) {
t.Errorf("unexpected error: expected NotFound, got %v", err) t.Errorf("unexpected error: expected NotFound, got %v", err)
} }
@ -258,9 +337,9 @@ func TestDeleteAllIgnoreNotFound(t *testing.T) {
}), }),
} }
tf.Namespace = "test" tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{}) buf, errBuf := bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{})
cmd := NewCmdDelete(f, buf) cmd := NewCmdDelete(f, buf, errBuf)
cmd.Flags().Set("all", "true") cmd.Flags().Set("all", "true")
cmd.Flags().Set("cascade", "false") cmd.Flags().Set("cascade", "false")
cmd.Flags().Set("output", "name") cmd.Flags().Set("output", "name")
@ -291,9 +370,9 @@ func TestDeleteMultipleObject(t *testing.T) {
}), }),
} }
tf.Namespace = "test" tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{}) buf, errBuf := bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{})
cmd := NewCmdDelete(f, buf) cmd := NewCmdDelete(f, buf, errBuf)
cmd.Flags().Set("filename", "../../../examples/guestbook/legacy/redis-master-controller.yaml") cmd.Flags().Set("filename", "../../../examples/guestbook/legacy/redis-master-controller.yaml")
cmd.Flags().Set("filename", "../../../examples/guestbook/frontend-service.yaml") cmd.Flags().Set("filename", "../../../examples/guestbook/frontend-service.yaml")
cmd.Flags().Set("cascade", "false") cmd.Flags().Set("cascade", "false")
@ -325,14 +404,14 @@ func TestDeleteMultipleObjectContinueOnMissing(t *testing.T) {
}), }),
} }
tf.Namespace = "test" tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{}) buf, errBuf := bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{})
cmd := NewCmdDelete(f, buf) cmd := NewCmdDelete(f, buf, errBuf)
options := &resource.FilenameOptions{} options := &resource.FilenameOptions{}
options.Filenames = []string{"../../../examples/guestbook/legacy/redis-master-controller.yaml", "../../../examples/guestbook/frontend-service.yaml"} options.Filenames = []string{"../../../examples/guestbook/legacy/redis-master-controller.yaml", "../../../examples/guestbook/frontend-service.yaml"}
cmd.Flags().Set("cascade", "false") cmd.Flags().Set("cascade", "false")
cmd.Flags().Set("output", "name") cmd.Flags().Set("output", "name")
err := RunDelete(f, buf, cmd, []string{}, options) err := RunDelete(f, buf, errBuf, cmd, []string{}, options)
if err == nil || !errors.IsNotFound(err) { if err == nil || !errors.IsNotFound(err) {
t.Errorf("unexpected error: expected NotFound, got %v", err) t.Errorf("unexpected error: expected NotFound, got %v", err)
} }
@ -366,8 +445,9 @@ func TestDeleteMultipleResourcesWithTheSameName(t *testing.T) {
}), }),
} }
tf.Namespace = "test" tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{}) buf, errBuf := bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{})
cmd := NewCmdDelete(f, buf)
cmd := NewCmdDelete(f, buf, errBuf)
cmd.Flags().Set("namespace", "test") cmd.Flags().Set("namespace", "test")
cmd.Flags().Set("cascade", "false") cmd.Flags().Set("cascade", "false")
cmd.Flags().Set("output", "name") cmd.Flags().Set("output", "name")
@ -395,9 +475,9 @@ func TestDeleteDirectory(t *testing.T) {
}), }),
} }
tf.Namespace = "test" tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{}) buf, errBuf := bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{})
cmd := NewCmdDelete(f, buf) cmd := NewCmdDelete(f, buf, errBuf)
cmd.Flags().Set("filename", "../../../examples/guestbook/legacy") cmd.Flags().Set("filename", "../../../examples/guestbook/legacy")
cmd.Flags().Set("cascade", "false") cmd.Flags().Set("cascade", "false")
cmd.Flags().Set("output", "name") cmd.Flags().Set("output", "name")
@ -438,9 +518,9 @@ func TestDeleteMultipleSelector(t *testing.T) {
}), }),
} }
tf.Namespace = "test" tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{}) buf, errBuf := bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{})
cmd := NewCmdDelete(f, buf) cmd := NewCmdDelete(f, buf, errBuf)
cmd.Flags().Set("selector", "a=b") cmd.Flags().Set("selector", "a=b")
cmd.Flags().Set("cascade", "false") cmd.Flags().Set("cascade", "false")
cmd.Flags().Set("output", "name") cmd.Flags().Set("output", "name")
@ -481,14 +561,15 @@ func TestResourceErrors(t *testing.T) {
tf.Namespace = "test" tf.Namespace = "test"
tf.ClientConfig = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &registered.GroupOrDie(api.GroupName).GroupVersion}} tf.ClientConfig = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &registered.GroupOrDie(api.GroupName).GroupVersion}}
buf := bytes.NewBuffer([]byte{}) buf, errBuf := bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{})
cmd := NewCmdDelete(f, buf)
cmd := NewCmdDelete(f, buf, errBuf)
cmd.SetOutput(buf) cmd.SetOutput(buf)
for k, v := range testCase.flags { for k, v := range testCase.flags {
cmd.Flags().Set(k, v) cmd.Flags().Set(k, v)
} }
err := RunDelete(f, buf, cmd, testCase.args, &resource.FilenameOptions{}) err := RunDelete(f, buf, errBuf, cmd, testCase.args, &resource.FilenameOptions{})
if !testCase.errFn(err) { if !testCase.errFn(err) {
t.Errorf("%s: unexpected error: %v", k, err) t.Errorf("%s: unexpected error: %v", k, err)
continue continue

View File

@ -213,10 +213,18 @@ func forceReplace(f cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []s
//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
ignoreNotFound := true ignoreNotFound := true
timeout := cmdutil.GetFlagDuration(cmd, "timeout") timeout := cmdutil.GetFlagDuration(cmd, "timeout")
gracePeriod := cmdutil.GetFlagInt(cmd, "grace-period")
waitForDeletion := false
if gracePeriod == 0 {
// To preserve backwards compatibility, but prevent accidental data loss, we convert --grace-period=0
// into --grace-period=1 and wait until the object is successfully deleted.
gracePeriod = 1
waitForDeletion = true
}
// By default use a reaper to delete all related resources. // By default use a reaper to delete all related resources.
if cmdutil.GetFlagBool(cmd, "cascade") { if cmdutil.GetFlagBool(cmd, "cascade") {
glog.Warningf("\"cascade\" is set, kubectl will delete and re-create all resources managed by this resource (e.g. Pods created by a ReplicationController). Consider using \"kubectl rolling-update\" if you want to update a ReplicationController together with its Pods.") glog.Warningf("\"cascade\" is set, kubectl will delete and re-create all resources managed by this resource (e.g. Pods created by a ReplicationController). Consider using \"kubectl rolling-update\" if you want to update a ReplicationController together with its Pods.")
err = ReapResult(r, f, out, cmdutil.GetFlagBool(cmd, "cascade"), ignoreNotFound, timeout, cmdutil.GetFlagInt(cmd, "grace-period"), shortOutput, mapper, false) err = ReapResult(r, f, out, cmdutil.GetFlagBool(cmd, "cascade"), ignoreNotFound, timeout, gracePeriod, waitForDeletion, shortOutput, mapper, false)
} else { } else {
err = DeleteResult(r, out, ignoreNotFound, shortOutput, mapper) err = DeleteResult(r, out, ignoreNotFound, shortOutput, mapper)
} }

View File

@ -325,7 +325,7 @@ func Run(f cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cobr
ResourceNames(mapping.Resource, name). ResourceNames(mapping.Resource, name).
Flatten(). Flatten().
Do() Do()
err = ReapResult(r, f, cmdOut, true, true, 0, -1, false, mapper, quiet) err = ReapResult(r, f, cmdOut, true, true, 0, -1, false, false, mapper, quiet)
if err != nil { if err != nil {
return err return err
} }

View File

@ -96,5 +96,13 @@ func RunStop(f cmdutil.Factory, cmd *cobra.Command, args []string, out io.Writer
return r.Err() return r.Err()
} }
shortOutput := cmdutil.GetFlagString(cmd, "output") == "name" shortOutput := cmdutil.GetFlagString(cmd, "output") == "name"
return ReapResult(r, f, out, false, cmdutil.GetFlagBool(cmd, "ignore-not-found"), cmdutil.GetFlagDuration(cmd, "timeout"), cmdutil.GetFlagInt(cmd, "grace-period"), shortOutput, mapper, false) gracePeriod := cmdutil.GetFlagInt(cmd, "grace-period")
waitForDeletion := false
if gracePeriod == 0 {
// To preserve backwards compatibility, but prevent accidental data loss, we convert --grace-period=0
// into --grace-period=1 and wait until the object is successfully deleted.
gracePeriod = 1
waitForDeletion = true
}
return ReapResult(r, f, out, false, cmdutil.GetFlagBool(cmd, "ignore-not-found"), cmdutil.GetFlagDuration(cmd, "timeout"), gracePeriod, waitForDeletion, shortOutput, mapper, false)
} }