From 7cdb6b169d88f592a0fa6880d10549750a346473 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Mon, 21 Nov 2016 22:22:24 -0500 Subject: [PATCH] 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. --- hack/make-rules/test-cmd.sh | 10 +-- pkg/kubectl/cmd/cmd.go | 2 +- pkg/kubectl/cmd/delete.go | 51 ++++++++++-- pkg/kubectl/cmd/delete_test.go | 141 ++++++++++++++++++++++++++------- pkg/kubectl/cmd/replace.go | 10 ++- pkg/kubectl/cmd/run.go | 2 +- pkg/kubectl/cmd/stop.go | 10 ++- 7 files changed, 179 insertions(+), 47 deletions(-) diff --git a/hack/make-rules/test-cmd.sh b/hack/make-rules/test-cmd.sh index 893225e38e4..f50bd76ded0 100755 --- a/hack/make-rules/test-cmd.sh +++ b/hack/make-rules/test-cmd.sh @@ -490,10 +490,8 @@ runTests() { # Pre-condition: valid-pod POD exists 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:' - # Command fails without --force - ! 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 + # Command succeeds without --force by waiting + kubectl delete pod valid-pod "${kube_flags[@]}" --grace-period=0 # Post-condition: valid-pod POD doesn't exist kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" '' @@ -2226,7 +2224,7 @@ __EOF__ ## Set resource limits/request of a deployment # Pre-condition: no deployment exists kube::test::get_object_assert deployment "{{range.items}}{{$id_field}}:{{end}}" '' - # Set resources of a local file without talking to the server + # Set resources of a local file without talking to the server kubectl set resources -f hack/testdata/deployment-multicontainer-resources.yaml -c=perl --limits=cpu=300m --requests=cpu=300m --local -o yaml "${kube_flags[@]}" ! kubectl set resources -f hack/testdata/deployment-multicontainer-resources.yaml -c=perl --limits=cpu=300m --requests=cpu=300m --dry-run -o yaml "${kube_flags[@]}" # Create a deployment @@ -2249,7 +2247,7 @@ __EOF__ kube::test::get_object_assert deployment "{{range.items}}{{(index .spec.template.spec.containers 0).resources.limits.cpu}}:{{end}}" "200m:" kube::test::get_object_assert deployment "{{range.items}}{{(index .spec.template.spec.containers 1).resources.limits.cpu}}:{{end}}" "300m:" kube::test::get_object_assert deployment "{{range.items}}{{(index .spec.template.spec.containers 1).resources.requests.cpu}}:{{end}}" "300m:" - # Show dry-run works on running deployments + # Show dry-run works on running deployments kubectl set resources deployment nginx-deployment-resources -c=perl --limits=cpu=400m --requests=cpu=400m --dry-run -o yaml "${kube_flags[@]}" ! kubectl set resources deployment nginx-deployment-resources -c=perl --limits=cpu=400m --requests=cpu=400m --local -o yaml "${kube_flags[@]}" kube::test::get_object_assert deployment "{{range.items}}{{(index .spec.template.spec.containers 0).resources.limits.cpu}}:{{end}}" "200m:" diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index 7a4914f51ea..739678210c8 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -237,7 +237,7 @@ func NewKubectlCommand(f cmdutil.Factory, in io.Reader, out, err io.Writer) *cob NewCmdGet(f, out, err), NewCmdExplain(f, out, err), NewCmdEdit(f, out, err), - NewCmdDelete(f, out), + NewCmdDelete(f, out, err), }, }, { diff --git a/pkg/kubectl/cmd/delete.go b/pkg/kubectl/cmd/delete.go index 8414ce0f080..bd65d50e034 100644 --- a/pkg/kubectl/cmd/delete.go +++ b/pkg/kubectl/cmd/delete.go @@ -31,6 +31,7 @@ import ( cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" "k8s.io/kubernetes/pkg/kubectl/resource" "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/util/wait" ) var ( @@ -88,7 +89,7 @@ var ( 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{} // 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, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(cmdutil.ValidateOutputArgs(cmd)) - err := RunDelete(f, out, cmd, args, options) + err := RunDelete(f, out, errOut, cmd, args, options) cmdutil.CheckErr(err) }, SuggestFor: []string{"rm"}, @@ -131,7 +132,7 @@ func NewCmdDelete(f cmdutil.Factory, out io.Writer) *cobra.Command { 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() if err != nil { 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") + force := cmdutil.GetFlagBool(cmd, "force") if cmdutil.GetFlagBool(cmd, "now") { if gracePeriod != -1 { return fmt.Errorf("--now and --grace-period cannot be specified together") } gracePeriod = 1 } - if gracePeriod == 0 && !cmdutil.GetFlagBool(cmd, "force") { - 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.") + wait := false + 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" // By default use a reaper to delete all related resources. 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) } -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 if ignoreNotFound { 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 { 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 { 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") 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 + } + }) +} diff --git a/pkg/kubectl/cmd/delete_test.go b/pkg/kubectl/cmd/delete_test.go index 5104c6b0134..29663565e54 100644 --- a/pkg/kubectl/cmd/delete_test.go +++ b/pkg/kubectl/cmd/delete_test.go @@ -21,15 +21,19 @@ import ( "net/http" "strings" "testing" + "time" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/errors" + "k8s.io/kubernetes/pkg/api/meta" "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/apimachinery/registered" "k8s.io/kubernetes/pkg/client/restclient" "k8s.io/kubernetes/pkg/client/restclient/fake" "k8s.io/kubernetes/pkg/client/typed/dynamic" + "k8s.io/kubernetes/pkg/kubectl" cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" "k8s.io/kubernetes/pkg/kubectl/resource" ) @@ -54,9 +58,9 @@ func TestDeleteObjectByTuple(t *testing.T) { }), } 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("cascade", "false") cmd.Flags().Set("output", "name") @@ -86,9 +90,9 @@ func TestDeleteNamedObject(t *testing.T) { }), } 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("cascade", "false") cmd.Flags().Set("output", "name") @@ -117,9 +121,9 @@ func TestDeleteObject(t *testing.T) { }), } 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("cascade", "false") 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) { f, tf, _, _ := cmdtesting.NewAPIFactory() tf.Printer = &testPrinter{} @@ -147,14 +226,14 @@ func TestDeleteObjectNotFound(t *testing.T) { }), } 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.Filenames = []string{"../../../examples/guestbook/legacy/redis-master-controller.yaml"} cmd.Flags().Set("cascade", "false") 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) { t.Errorf("unexpected error: expected NotFound, got %v", err) } @@ -176,9 +255,9 @@ func TestDeleteObjectIgnoreNotFound(t *testing.T) { }), } 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("cascade", "false") cmd.Flags().Set("ignore-not-found", "true") @@ -216,16 +295,16 @@ func TestDeleteAllNotFound(t *testing.T) { }), } 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("cascade", "false") // Make sure we can explicitly choose to fail on NotFound errors, even with --all cmd.Flags().Set("ignore-not-found", "false") 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) { t.Errorf("unexpected error: expected NotFound, got %v", err) } @@ -258,9 +337,9 @@ func TestDeleteAllIgnoreNotFound(t *testing.T) { }), } 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("cascade", "false") cmd.Flags().Set("output", "name") @@ -291,9 +370,9 @@ func TestDeleteMultipleObject(t *testing.T) { }), } 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/frontend-service.yaml") cmd.Flags().Set("cascade", "false") @@ -325,14 +404,14 @@ func TestDeleteMultipleObjectContinueOnMissing(t *testing.T) { }), } 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.Filenames = []string{"../../../examples/guestbook/legacy/redis-master-controller.yaml", "../../../examples/guestbook/frontend-service.yaml"} cmd.Flags().Set("cascade", "false") 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) { t.Errorf("unexpected error: expected NotFound, got %v", err) } @@ -366,8 +445,9 @@ func TestDeleteMultipleResourcesWithTheSameName(t *testing.T) { }), } tf.Namespace = "test" - buf := bytes.NewBuffer([]byte{}) - cmd := NewCmdDelete(f, buf) + buf, errBuf := bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{}) + + cmd := NewCmdDelete(f, buf, errBuf) cmd.Flags().Set("namespace", "test") cmd.Flags().Set("cascade", "false") cmd.Flags().Set("output", "name") @@ -395,9 +475,9 @@ func TestDeleteDirectory(t *testing.T) { }), } 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("cascade", "false") cmd.Flags().Set("output", "name") @@ -438,9 +518,9 @@ func TestDeleteMultipleSelector(t *testing.T) { }), } 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("cascade", "false") cmd.Flags().Set("output", "name") @@ -481,14 +561,15 @@ func TestResourceErrors(t *testing.T) { tf.Namespace = "test" tf.ClientConfig = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: ®istered.GroupOrDie(api.GroupName).GroupVersion}} - buf := bytes.NewBuffer([]byte{}) - cmd := NewCmdDelete(f, buf) + buf, errBuf := bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{}) + + cmd := NewCmdDelete(f, buf, errBuf) cmd.SetOutput(buf) for k, v := range testCase.flags { 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) { t.Errorf("%s: unexpected error: %v", k, err) continue diff --git a/pkg/kubectl/cmd/replace.go b/pkg/kubectl/cmd/replace.go index 653a690b7b5..30ffbc1889c 100644 --- a/pkg/kubectl/cmd/replace.go +++ b/pkg/kubectl/cmd/replace.go @@ -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 ignoreNotFound := true 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. 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.") - 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 { err = DeleteResult(r, out, ignoreNotFound, shortOutput, mapper) } diff --git a/pkg/kubectl/cmd/run.go b/pkg/kubectl/cmd/run.go index 1a7b6378d62..8c7a3a486ab 100644 --- a/pkg/kubectl/cmd/run.go +++ b/pkg/kubectl/cmd/run.go @@ -324,7 +324,7 @@ func Run(f cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cobr ResourceNames(mapping.Resource, name). Flatten(). 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 { return err } diff --git a/pkg/kubectl/cmd/stop.go b/pkg/kubectl/cmd/stop.go index 968de529a26..fbf989692d5 100644 --- a/pkg/kubectl/cmd/stop.go +++ b/pkg/kubectl/cmd/stop.go @@ -96,5 +96,13 @@ func RunStop(f cmdutil.Factory, cmd *cobra.Command, args []string, out io.Writer return r.Err() } 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) }