Merge pull request #87776 from brianpursley/kubectl-813

Default grace period to 0 when --force is used to delete an object
This commit is contained in:
Kubernetes Prow Robot 2020-02-27 11:33:28 -08:00 committed by GitHub
commit 882b6f8440
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 149 additions and 41 deletions

View File

@ -52,8 +52,7 @@ var (
the --grace-period flag, or pass --now to set a grace-period of 1. Because these resources often the --grace-period flag, or pass --now to set a grace-period of 1. Because these resources often
represent entities in the cluster, deletion may not be acknowledged immediately. If the node represent entities in the cluster, deletion may not be acknowledged immediately. If the node
hosting a pod is down or cannot reach the API server, termination may take significantly longer hosting a pod is down or cannot reach the API server, termination may take significantly longer
than the grace period. To force delete a resource, you must pass a grace period of 0 and specify than the grace period. To force delete a resource, you must specify the --force flag.
the --force flag.
Note: only a subset of resources support graceful deletion. In absence of the support, --grace-period is ignored. Note: only a subset of resources support graceful deletion. In absence of the support, --grace-period is ignored.
IMPORTANT: Force deleting pods does not wait for confirmation that the pod's processes have been IMPORTANT: Force deleting pods does not wait for confirmation that the pod's processes have been
@ -90,7 +89,7 @@ var (
kubectl delete pod foo --now kubectl delete pod foo --now
# Force delete a pod on a dead node # Force delete a pod on a dead node
kubectl delete pod foo --grace-period=0 --force kubectl delete pod foo --force
# Delete all pods # Delete all pods
kubectl delete pods --all`)) kubectl delete pods --all`))
@ -176,6 +175,9 @@ func (o *DeleteOptions) Complete(f cmdutil.Factory, args []string, cmd *cobra.Co
// into --grace-period=1. Users may provide --force to bypass this conversion. // into --grace-period=1. Users may provide --force to bypass this conversion.
o.GracePeriod = 1 o.GracePeriod = 1
} }
if o.ForceDeletion && o.GracePeriod < 0 {
o.GracePeriod = 0
}
o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
if err != nil { if err != nil {
@ -239,8 +241,8 @@ func (o *DeleteOptions) Validate() error {
switch { switch {
case o.GracePeriod == 0 && o.ForceDeletion: case o.GracePeriod == 0 && o.ForceDeletion:
fmt.Fprintf(o.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") fmt.Fprintf(o.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")
case o.ForceDeletion: case o.GracePeriod > 0 && o.ForceDeletion:
fmt.Fprintf(o.ErrOut, "warning: --force is ignored because --grace-period is not 0.\n") return fmt.Errorf("--force and --grace-period greater than 0 cannot be specified together")
} }
if len(o.Raw) > 0 { if len(o.Raw) > 0 {

View File

@ -116,7 +116,7 @@ func (f *DeleteFlags) AddFlags(cmd *cobra.Command) {
cmd.Flags().BoolVarP(f.AllNamespaces, "all-namespaces", "A", *f.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.") cmd.Flags().BoolVarP(f.AllNamespaces, "all-namespaces", "A", *f.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.")
} }
if f.Force != nil { if f.Force != nil {
cmd.Flags().BoolVar(f.Force, "force", *f.Force, "Only used when grace-period=0. If true, immediately remove resources from API and bypass graceful deletion. Note that immediate deletion of some resources may result in inconsistency or data loss and requires confirmation.") cmd.Flags().BoolVar(f.Force, "force", *f.Force, "If true, immediately remove resources from API and bypass graceful deletion. Note that immediate deletion of some resources may result in inconsistency or data loss and requires confirmation.")
} }
if f.Cascade != nil { if f.Cascade != nil {
cmd.Flags().BoolVar(f.Cascade, "cascade", *f.Cascade, "If true, cascade the deletion of the resources managed by this resource (e.g. Pods created by a ReplicationController). Default true.") cmd.Flags().BoolVar(f.Cascade, "cascade", *f.Cascade, "If true, cascade the deletion of the resources managed by this resource (e.g. Pods created by a ReplicationController). Default true.")

View File

@ -21,6 +21,7 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"strconv"
"strings" "strings"
"testing" "testing"
@ -250,52 +251,157 @@ func TestDeleteObject(t *testing.T) {
} }
} }
func TestDeleteObjectGraceZero(t *testing.T) { func TestGracePeriodScenarios(t *testing.T) {
cmdtesting.InitTestErrorHandler(t)
pods, _, _ := cmdtesting.TestData() pods, _, _ := cmdtesting.TestData()
count := 0
tf := cmdtesting.NewTestFactory().WithNamespace("test") tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup() defer tf.Cleanup()
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
tf.UnstructuredClient = &fake.RESTClient{ tc := []struct {
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, name string
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { cmdArgs []string
t.Logf("got request %s %s", req.Method, req.URL.Path) forceFlag bool
switch p, m := req.URL.Path, req.Method; { nowFlag bool
case p == "/namespaces/test/pods/nginx" && m == "GET": gracePeriodFlag string
count++ expectedGracePeriod string
switch count { expectedOut string
case 1, 2, 3: expectedErrOut string
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, nil expectedDeleteRequestPath string
default: expectedExitCode int
return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &metav1.Status{})}, nil }{
{
name: "Deleting an object with --force should use grace period = 0",
cmdArgs: []string{"pods/foo"},
forceFlag: true,
expectedGracePeriod: "0",
expectedOut: "pod/foo\n",
expectedErrOut: "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",
expectedDeleteRequestPath: "/namespaces/test/pods/foo",
},
{
name: "Deleting an object with --force and --grace-period 0 should use grade period = 0",
cmdArgs: []string{"pods/foo"},
forceFlag: true,
gracePeriodFlag: "0",
expectedGracePeriod: "0",
expectedOut: "pod/foo\n",
expectedErrOut: "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",
expectedDeleteRequestPath: "/namespaces/test/pods/foo",
},
{
name: "Deleting an object with --force and --grace-period > 0 should fail",
cmdArgs: []string{"pods/foo"},
forceFlag: true,
gracePeriodFlag: "10",
expectedErrOut: "error: --force and --grace-period greater than 0 cannot be specified together",
expectedExitCode: 1,
},
{
name: "Deleting an object with --grace-period 0 should use a grace period of 1",
cmdArgs: []string{"pods/foo"},
gracePeriodFlag: "0",
expectedGracePeriod: "1",
expectedOut: "pod/foo\n",
expectedDeleteRequestPath: "/namespaces/test/pods/foo",
},
{
name: "Deleting an object with --grace-period > 0 should use the specified grace period",
cmdArgs: []string{"pods/foo"},
gracePeriodFlag: "10",
expectedGracePeriod: "10",
expectedOut: "pod/foo\n",
expectedDeleteRequestPath: "/namespaces/test/pods/foo",
},
{
name: "Deleting an object with the --now flag should use grace period = 1",
cmdArgs: []string{"pods/foo"},
nowFlag: true,
expectedGracePeriod: "1",
expectedOut: "pod/foo\n",
expectedDeleteRequestPath: "/namespaces/test/pods/foo",
},
{
name: "Deleting an object with --now and --grace-period should fail",
cmdArgs: []string{"pods/foo"},
nowFlag: true,
gracePeriodFlag: "10",
expectedErrOut: "error: --now and --grace-period cannot be specified together",
expectedExitCode: 1,
},
}
for _, test := range tc {
t.Run(test.name, func(t *testing.T) {
// Use a custom fatal behavior with panic/recover so that we can test failure scenarios where
// os.Exit() would normally be called
cmdutil.BehaviorOnFatal(func(actualErrOut string, actualExitCode int) {
if test.expectedExitCode != actualExitCode {
t.Errorf("unexpected exit code:\n\tExpected: %d\n\tActual: %d\n", test.expectedExitCode, actualExitCode)
} }
case p == "/api/v1/namespaces/test" && m == "GET": if test.expectedErrOut != actualErrOut {
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Namespace{})}, nil t.Errorf("unexpected error:\n\tExpected: %s\n\tActual: %s\n", test.expectedErrOut, actualErrOut)
case p == "/namespaces/test/pods/nginx" && m == "DELETE": }
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, nil panic(nil)
default: })
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) defer func() {
return nil, nil if test.expectedExitCode != 0 {
recover()
}
}()
// Setup a fake HTTP Client to capture whether a delete request was made or not and if so,
// the actual grace period that was used.
actualGracePeriod := ""
deleteOccurred := false
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case m == "DELETE" && p == test.expectedDeleteRequestPath:
data := make(map[string]interface{})
_ = json.NewDecoder(req.Body).Decode(&data)
actualGracePeriod = strconv.FormatFloat(data["gracePeriodSeconds"].(float64), 'f', 0, 64)
deleteOccurred = true
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
} }
}),
}
streams, _, buf, errBuf := genericclioptions.NewTestIOStreams() // Test the command using the flags specified in the test case
cmd := NewCmdDelete(tf, streams) streams, _, out, errOut := genericclioptions.NewTestIOStreams()
cmd.Flags().Set("output", "name") cmd := NewCmdDelete(tf, streams)
cmd.Flags().Set("grace-period", "0") cmd.Flags().Set("output", "name")
cmd.Run(cmd, []string{"pods/nginx"}) if test.forceFlag {
cmd.Flags().Set("force", "true")
}
if test.nowFlag {
cmd.Flags().Set("now", "true")
}
if len(test.gracePeriodFlag) > 0 {
cmd.Flags().Set("grace-period", test.gracePeriodFlag)
}
cmd.Run(cmd, test.cmdArgs)
// uses the name from the file, not the response // Check actual vs expected conditions
if buf.String() != "pod/nginx\n" { if len(test.expectedDeleteRequestPath) > 0 && !deleteOccurred {
t.Errorf("unexpected output: %s\n---\n%s", buf.String(), errBuf.String()) t.Errorf("expected http delete request to %s but it did not occur", test.expectedDeleteRequestPath)
} }
if count != 0 { if test.expectedGracePeriod != actualGracePeriod {
t.Errorf("unexpected calls to GET: %d", count) t.Errorf("unexpected grace period:\n\tExpected: %s\n\tActual: %s\n", test.expectedGracePeriod, actualGracePeriod)
}
if out.String() != test.expectedOut {
t.Errorf("unexpected output:\n\tExpected: %s\n\tActual: %s\n", test.expectedOut, out.String())
}
if errOut.String() != test.expectedErrOut {
t.Errorf("unexpected error output:\n\tExpected: %s\n\tActual: %s\n", test.expectedErrOut, errOut.String())
}
})
} }
} }