diff --git a/hack/make-rules/test-cmd.sh b/hack/make-rules/test-cmd.sh index b3d0decd2f..9f5aac8dc2 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 7a4914f51e..739678210c 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 8414ce0f08..bd65d50e03 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 5104c6b013..29663565e5 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 653a690b7b..30ffbc1889 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 7b2e860e74..bb0da510c0 100644 --- a/pkg/kubectl/cmd/run.go +++ b/pkg/kubectl/cmd/run.go @@ -325,7 +325,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 968de529a2..fbf989692d 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) }