mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-09 03:57:41 +00:00
kubectl delete: Introduce new interactive flag for interactive deletion (#114530)
This commit is contained in:
parent
86038ae590
commit
3267dd9d52
@ -121,6 +121,7 @@ type DeleteOptions struct {
|
|||||||
Quiet bool
|
Quiet bool
|
||||||
WarnClusterScope bool
|
WarnClusterScope bool
|
||||||
Raw string
|
Raw string
|
||||||
|
Interactive bool
|
||||||
|
|
||||||
GracePeriod int
|
GracePeriod int
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
@ -129,9 +130,11 @@ type DeleteOptions struct {
|
|||||||
|
|
||||||
Output string
|
Output string
|
||||||
|
|
||||||
DynamicClient dynamic.Interface
|
DynamicClient dynamic.Interface
|
||||||
Mapper meta.RESTMapper
|
Mapper meta.RESTMapper
|
||||||
Result *resource.Result
|
Result *resource.Result
|
||||||
|
PreviewResult *resource.Result
|
||||||
|
previewResourceMap map[cmdwait.ResourceLocation]struct{}
|
||||||
|
|
||||||
genericiooptions.IOStreams
|
genericiooptions.IOStreams
|
||||||
WarningPrinter *printers.WarningPrinter
|
WarningPrinter *printers.WarningPrinter
|
||||||
@ -197,8 +200,38 @@ func (o *DeleteOptions) Complete(f cmdutil.Factory, args []string, cmd *cobra.Co
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(o.Raw) == 0 {
|
// Set default WarningPrinter if not already set.
|
||||||
r := f.NewBuilder().
|
if o.WarningPrinter == nil {
|
||||||
|
o.WarningPrinter = printers.NewWarningPrinter(o.ErrOut, printers.WarningPrinterOptions{Color: term.AllowsColorOutput(o.ErrOut)})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(o.Raw) != 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r := f.NewBuilder().
|
||||||
|
Unstructured().
|
||||||
|
ContinueOnError().
|
||||||
|
NamespaceParam(cmdNamespace).DefaultNamespace().
|
||||||
|
FilenameParam(enforceNamespace, &o.FilenameOptions).
|
||||||
|
LabelSelectorParam(o.LabelSelector).
|
||||||
|
FieldSelectorParam(o.FieldSelector).
|
||||||
|
SelectAllParam(o.DeleteAll).
|
||||||
|
AllNamespaces(o.DeleteAllNamespaces).
|
||||||
|
ResourceTypeOrNameArgs(false, args...).RequireObject(false).
|
||||||
|
Flatten().
|
||||||
|
Do()
|
||||||
|
err = r.Err()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.Result = r
|
||||||
|
|
||||||
|
if o.Interactive {
|
||||||
|
// preview result will be used to list resources for confirmation prior to actual delete.
|
||||||
|
// We can not use r as result object because it can only be used once. But we need to traverse
|
||||||
|
// twice. Parameters in preview result must be equal to genuine result.
|
||||||
|
previewr := f.NewBuilder().
|
||||||
Unstructured().
|
Unstructured().
|
||||||
ContinueOnError().
|
ContinueOnError().
|
||||||
NamespaceParam(cmdNamespace).DefaultNamespace().
|
NamespaceParam(cmdNamespace).DefaultNamespace().
|
||||||
@ -210,26 +243,22 @@ func (o *DeleteOptions) Complete(f cmdutil.Factory, args []string, cmd *cobra.Co
|
|||||||
ResourceTypeOrNameArgs(false, args...).RequireObject(false).
|
ResourceTypeOrNameArgs(false, args...).RequireObject(false).
|
||||||
Flatten().
|
Flatten().
|
||||||
Do()
|
Do()
|
||||||
err = r.Err()
|
err = previewr.Err()
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
o.Result = r
|
|
||||||
|
|
||||||
o.Mapper, err = f.ToRESTMapper()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
o.DynamicClient, err = f.DynamicClient()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
o.PreviewResult = previewr
|
||||||
|
o.previewResourceMap = make(map[cmdwait.ResourceLocation]struct{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set default WarningPrinter if not already set.
|
o.Mapper, err = f.ToRESTMapper()
|
||||||
if o.WarningPrinter == nil {
|
if err != nil {
|
||||||
o.WarningPrinter = printers.NewWarningPrinter(o.ErrOut, printers.WarningPrinterOptions{Color: term.AllowsColorOutput(o.ErrOut)})
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.DynamicClient, err = f.DynamicClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -257,26 +286,31 @@ func (o *DeleteOptions) Validate() error {
|
|||||||
return fmt.Errorf("--force and --grace-period greater than 0 cannot be specified together")
|
return fmt.Errorf("--force and --grace-period greater than 0 cannot be specified together")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(o.Raw) > 0 {
|
if len(o.Raw) == 0 {
|
||||||
if len(o.FilenameOptions.Filenames) > 1 {
|
return nil
|
||||||
return fmt.Errorf("--raw can only use a single local file or stdin")
|
}
|
||||||
} else if len(o.FilenameOptions.Filenames) == 1 {
|
|
||||||
if strings.Index(o.FilenameOptions.Filenames[0], "http://") == 0 || strings.Index(o.FilenameOptions.Filenames[0], "https://") == 0 {
|
|
||||||
return fmt.Errorf("--raw cannot read from a url")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if o.FilenameOptions.Recursive {
|
if o.Interactive {
|
||||||
return fmt.Errorf("--raw and --recursive are mutually exclusive")
|
return fmt.Errorf("--interactive can not be used with --raw")
|
||||||
}
|
}
|
||||||
if len(o.Output) > 0 {
|
if len(o.FilenameOptions.Filenames) > 1 {
|
||||||
return fmt.Errorf("--raw and --output are mutually exclusive")
|
return fmt.Errorf("--raw can only use a single local file or stdin")
|
||||||
}
|
} else if len(o.FilenameOptions.Filenames) == 1 {
|
||||||
if _, err := url.ParseRequestURI(o.Raw); err != nil {
|
if strings.Index(o.FilenameOptions.Filenames[0], "http://") == 0 || strings.Index(o.FilenameOptions.Filenames[0], "https://") == 0 {
|
||||||
return fmt.Errorf("--raw must be a valid URL path: %v", err)
|
return fmt.Errorf("--raw cannot read from a url")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if o.FilenameOptions.Recursive {
|
||||||
|
return fmt.Errorf("--raw and --recursive are mutually exclusive")
|
||||||
|
}
|
||||||
|
if len(o.Output) > 0 {
|
||||||
|
return fmt.Errorf("--raw and --output are mutually exclusive")
|
||||||
|
}
|
||||||
|
if _, err := url.ParseRequestURI(o.Raw); err != nil {
|
||||||
|
return fmt.Errorf("--raw must be a valid URL path: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -291,6 +325,39 @@ func (o *DeleteOptions) RunDelete(f cmdutil.Factory) error {
|
|||||||
}
|
}
|
||||||
return rawhttp.RawDelete(restClient, o.IOStreams, o.Raw, o.Filenames[0])
|
return rawhttp.RawDelete(restClient, o.IOStreams, o.Raw, o.Filenames[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if o.Interactive {
|
||||||
|
previewInfos := []*resource.Info{}
|
||||||
|
if o.IgnoreNotFound {
|
||||||
|
o.PreviewResult = o.PreviewResult.IgnoreErrors(errors.IsNotFound)
|
||||||
|
}
|
||||||
|
err := o.PreviewResult.Visit(func(info *resource.Info, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
previewInfos = append(previewInfos, info)
|
||||||
|
o.previewResourceMap[cmdwait.ResourceLocation{
|
||||||
|
GroupResource: info.Mapping.Resource.GroupResource(),
|
||||||
|
Namespace: info.Namespace,
|
||||||
|
Name: info.Name,
|
||||||
|
}] = struct{}{}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(previewInfos) == 0 {
|
||||||
|
fmt.Fprintf(o.Out, "No resources found\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !o.confirmation(previewInfos) {
|
||||||
|
fmt.Fprintf(o.Out, "deletion is cancelled\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return o.DeleteResult(o.Result)
|
return o.DeleteResult(o.Result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -306,6 +373,18 @@ func (o *DeleteOptions) DeleteResult(r *resource.Result) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if o.Interactive {
|
||||||
|
if _, ok := o.previewResourceMap[cmdwait.ResourceLocation{
|
||||||
|
GroupResource: info.Mapping.Resource.GroupResource(),
|
||||||
|
Namespace: info.Namespace,
|
||||||
|
Name: info.Name,
|
||||||
|
}]; !ok {
|
||||||
|
// resource not in the list of previewed resources based on resourceLocation
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
deletedInfos = append(deletedInfos, info)
|
deletedInfos = append(deletedInfos, info)
|
||||||
found++
|
found++
|
||||||
|
|
||||||
@ -440,3 +519,24 @@ func (o *DeleteOptions) PrintObj(info *resource.Info) {
|
|||||||
// understandable output by default
|
// understandable output by default
|
||||||
fmt.Fprintf(o.Out, "%s \"%s\" %s\n", kindString, info.Name, operation)
|
fmt.Fprintf(o.Out, "%s \"%s\" %s\n", kindString, info.Name, operation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *DeleteOptions) confirmation(infos []*resource.Info) bool {
|
||||||
|
fmt.Fprintf(o.Out, i18n.T("You are about to delete the following %d resource(s):\n"), len(infos))
|
||||||
|
for _, info := range infos {
|
||||||
|
groupKind := info.Mapping.GroupVersionKind
|
||||||
|
kindString := fmt.Sprintf("%s.%s", strings.ToLower(groupKind.Kind), groupKind.Group)
|
||||||
|
if len(groupKind.Group) == 0 {
|
||||||
|
kindString = strings.ToLower(groupKind.Kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(o.Out, "%s/%s\n", kindString, info.Name)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(o.Out, i18n.T("Do you want to continue?")+" (y/n): ")
|
||||||
|
var input string
|
||||||
|
_, err := fmt.Fscan(o.In, &input)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.EqualFold(input, "y")
|
||||||
|
}
|
||||||
|
@ -48,6 +48,7 @@ type DeleteFlags struct {
|
|||||||
Wait *bool
|
Wait *bool
|
||||||
Output *string
|
Output *string
|
||||||
Raw *string
|
Raw *string
|
||||||
|
Interactive *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *DeleteFlags) ToOptions(dynamicClient dynamic.Interface, streams genericiooptions.IOStreams) (*DeleteOptions, error) {
|
func (f *DeleteFlags) ToOptions(dynamicClient dynamic.Interface, streams genericiooptions.IOStreams) (*DeleteOptions, error) {
|
||||||
@ -106,6 +107,9 @@ func (f *DeleteFlags) ToOptions(dynamicClient dynamic.Interface, streams generic
|
|||||||
if f.Raw != nil {
|
if f.Raw != nil {
|
||||||
options.Raw = *f.Raw
|
options.Raw = *f.Raw
|
||||||
}
|
}
|
||||||
|
if f.Interactive != nil {
|
||||||
|
options.Interactive = *f.Interactive
|
||||||
|
}
|
||||||
|
|
||||||
return options, nil
|
return options, nil
|
||||||
}
|
}
|
||||||
@ -156,6 +160,11 @@ func (f *DeleteFlags) AddFlags(cmd *cobra.Command) {
|
|||||||
if f.Raw != nil {
|
if f.Raw != nil {
|
||||||
cmd.Flags().StringVar(f.Raw, "raw", *f.Raw, "Raw URI to DELETE to the server. Uses the transport specified by the kubeconfig file.")
|
cmd.Flags().StringVar(f.Raw, "raw", *f.Raw, "Raw URI to DELETE to the server. Uses the transport specified by the kubeconfig file.")
|
||||||
}
|
}
|
||||||
|
if cmdutil.InteractiveDelete.IsEnabled() {
|
||||||
|
if f.Interactive != nil {
|
||||||
|
cmd.Flags().BoolVarP(f.Interactive, "interactive", "i", *f.Interactive, "If true, delete resource only when user confirms. This flag is in Alpha.")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDeleteCommandFlags provides default flags and values for use with the "delete" command
|
// NewDeleteCommandFlags provides default flags and values for use with the "delete" command
|
||||||
@ -175,6 +184,7 @@ func NewDeleteCommandFlags(usage string) *DeleteFlags {
|
|||||||
timeout := time.Duration(0)
|
timeout := time.Duration(0)
|
||||||
wait := true
|
wait := true
|
||||||
raw := ""
|
raw := ""
|
||||||
|
interactive := false
|
||||||
|
|
||||||
filenames := []string{}
|
filenames := []string{}
|
||||||
recursive := false
|
recursive := false
|
||||||
@ -198,6 +208,7 @@ func NewDeleteCommandFlags(usage string) *DeleteFlags {
|
|||||||
Wait: &wait,
|
Wait: &wait,
|
||||||
Output: &output,
|
Output: &output,
|
||||||
Raw: &raw,
|
Raw: &raw,
|
||||||
|
Interactive: &interactive,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ package delete
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -35,6 +36,7 @@ import (
|
|||||||
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
||||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
"k8s.io/kubectl/pkg/scheme"
|
"k8s.io/kubectl/pkg/scheme"
|
||||||
|
"k8s.io/utils/pointer"
|
||||||
)
|
)
|
||||||
|
|
||||||
func fakecmd() *cobra.Command {
|
func fakecmd() *cobra.Command {
|
||||||
@ -47,6 +49,49 @@ func fakecmd() *cobra.Command {
|
|||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeleteFlagValidation(t *testing.T) {
|
||||||
|
f := cmdtesting.NewTestFactory()
|
||||||
|
defer f.Cleanup()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
flags DeleteFlags
|
||||||
|
enableAlphas []cmdutil.FeatureGate
|
||||||
|
args [][]string
|
||||||
|
expectedErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
flags: DeleteFlags{
|
||||||
|
Raw: pointer.String("test"),
|
||||||
|
Interactive: pointer.Bool(true),
|
||||||
|
},
|
||||||
|
enableAlphas: []cmdutil.FeatureGate{cmdutil.InteractiveDelete},
|
||||||
|
expectedErr: "--interactive can not be used with --raw",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
cmd := fakecmd()
|
||||||
|
cmdtesting.WithAlphaEnvs(test.enableAlphas, t, func(t *testing.T) {
|
||||||
|
deleteOptions, err := test.flags.ToOptions(nil, genericiooptions.NewTestIOStreamsDiscard())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error creating delete options: %s", err)
|
||||||
|
}
|
||||||
|
deleteOptions.Filenames = []string{"../../../testdata/redis-master-controller.yaml"}
|
||||||
|
err = deleteOptions.Complete(f, nil, cmd)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error creating delete options: %s", err)
|
||||||
|
}
|
||||||
|
err = deleteOptions.Validate()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("missing expected error")
|
||||||
|
}
|
||||||
|
if test.expectedErr != err.Error() {
|
||||||
|
t.Errorf("expected error %s, got %s", test.expectedErr, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDeleteObjectByTuple(t *testing.T) {
|
func TestDeleteObjectByTuple(t *testing.T) {
|
||||||
cmdtesting.InitTestErrorHandler(t)
|
cmdtesting.InitTestErrorHandler(t)
|
||||||
_, _, rc := cmdtesting.TestData()
|
_, _, rc := cmdtesting.TestData()
|
||||||
@ -263,6 +308,90 @@ func TestDeleteObject(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPreviewResultEqualToResult(t *testing.T) {
|
||||||
|
deleteFlags := NewDeleteCommandFlags("")
|
||||||
|
deleteFlags.Interactive = pointer.Bool(true)
|
||||||
|
|
||||||
|
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||||
|
defer tf.Cleanup()
|
||||||
|
|
||||||
|
streams, _, _, _ := genericiooptions.NewTestIOStreams()
|
||||||
|
|
||||||
|
deleteOptions, err := deleteFlags.ToOptions(nil, streams)
|
||||||
|
deleteOptions.Filenames = []string{"../../../testdata/redis-master-controller.yaml"}
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error %v", err)
|
||||||
|
}
|
||||||
|
err = deleteOptions.Complete(tf, nil, fakecmd())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
infos, err := deleteOptions.Result.Infos()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error %v", err)
|
||||||
|
}
|
||||||
|
previewInfos, err := deleteOptions.PreviewResult.Infos()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error %v", err)
|
||||||
|
}
|
||||||
|
if len(infos) != len(previewInfos) {
|
||||||
|
t.Errorf("result and previewResult must match")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteObjectWithInteractive(t *testing.T) {
|
||||||
|
cmdtesting.InitTestErrorHandler(t)
|
||||||
|
_, _, rc := cmdtesting.TestData()
|
||||||
|
|
||||||
|
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||||
|
defer tf.Cleanup()
|
||||||
|
|
||||||
|
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||||
|
|
||||||
|
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 p == "/namespaces/test/replicationcontrollers/redis-master" && m == "DELETE":
|
||||||
|
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil
|
||||||
|
default:
|
||||||
|
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.InteractiveDelete}, t, func(t *testing.T) {
|
||||||
|
streams, in, buf, _ := genericiooptions.NewTestIOStreams()
|
||||||
|
fmt.Fprint(in, "y")
|
||||||
|
cmd := NewCmdDelete(tf, streams)
|
||||||
|
cmd.Flags().Set("filename", "../../../testdata/redis-master-controller.yaml")
|
||||||
|
cmd.Flags().Set("output", "name")
|
||||||
|
cmd.Flags().Set("interactive", "true")
|
||||||
|
cmd.Run(cmd, []string{})
|
||||||
|
|
||||||
|
if buf.String() != "You are about to delete the following 1 resource(s):\nreplicationcontroller/redis-master\nDo you want to continue? (y/n): replicationcontroller/redis-master\n" {
|
||||||
|
t.Errorf("unexpected output: %s", buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
streams, in, buf, _ = genericiooptions.NewTestIOStreams()
|
||||||
|
fmt.Fprint(in, "n")
|
||||||
|
cmd = NewCmdDelete(tf, streams)
|
||||||
|
cmd.Flags().Set("filename", "../../../testdata/redis-master-controller.yaml")
|
||||||
|
cmd.Flags().Set("output", "name")
|
||||||
|
cmd.Flags().Set("interactive", "true")
|
||||||
|
cmd.Run(cmd, []string{})
|
||||||
|
|
||||||
|
if buf.String() != "You are about to delete the following 1 resource(s):\nreplicationcontroller/redis-master\nDo you want to continue? (y/n): deletion is cancelled\n" {
|
||||||
|
t.Errorf("unexpected output: %s", buf.String())
|
||||||
|
}
|
||||||
|
if buf.String() == ": replicationcontroller/redis-master\n" {
|
||||||
|
t.Errorf("unexpected output: %s", buf.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestGracePeriodScenarios(t *testing.T) {
|
func TestGracePeriodScenarios(t *testing.T) {
|
||||||
pods, _, _ := cmdtesting.TestData()
|
pods, _, _ := cmdtesting.TestData()
|
||||||
|
|
||||||
|
@ -428,6 +428,7 @@ const (
|
|||||||
ApplySet FeatureGate = "KUBECTL_APPLYSET"
|
ApplySet FeatureGate = "KUBECTL_APPLYSET"
|
||||||
ExplainOpenapiV3 FeatureGate = "KUBECTL_EXPLAIN_OPENAPIV3"
|
ExplainOpenapiV3 FeatureGate = "KUBECTL_EXPLAIN_OPENAPIV3"
|
||||||
CmdPluginAsSubcommand FeatureGate = "KUBECTL_ENABLE_CMD_SHADOW"
|
CmdPluginAsSubcommand FeatureGate = "KUBECTL_ENABLE_CMD_SHADOW"
|
||||||
|
InteractiveDelete FeatureGate = "KUBECTL_INTERACTIVE_DELETE"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (f FeatureGate) IsEnabled() bool {
|
func (f FeatureGate) IsEnabled() bool {
|
||||||
|
@ -52,3 +52,75 @@ run_kubectl_delete_allnamespaces_tests() {
|
|||||||
set +o nounset
|
set +o nounset
|
||||||
set +o errexit
|
set +o errexit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Runs tests related to kubectl delete --confirm
|
||||||
|
run_kubectl_delete_interactive_tests() {
|
||||||
|
set -o nounset
|
||||||
|
set -o errexit
|
||||||
|
|
||||||
|
# enable interactivity flag feature environment variable
|
||||||
|
export KUBECTL_INTERACTIVE_DELETE=true
|
||||||
|
|
||||||
|
ns_one="namespace-$(date +%s)-${RANDOM}"
|
||||||
|
ns_two="namespace-$(date +%s)-${RANDOM}"
|
||||||
|
kubectl create namespace "${ns_one}"
|
||||||
|
kubectl create namespace "${ns_two}"
|
||||||
|
|
||||||
|
# create configmaps
|
||||||
|
kubectl create configmap "one" --namespace="${ns_one}"
|
||||||
|
kubectl create configmap "two" --namespace="${ns_two}"
|
||||||
|
kubectl label configmap "one" --namespace="${ns_one}" deletetest=true
|
||||||
|
kubectl label configmap "two" --namespace="${ns_two}" deletetest=true
|
||||||
|
|
||||||
|
# not confirm dry-run=server deletions
|
||||||
|
output_message=$(kubectl delete configmap --dry-run=server --interactive -l deletetest=true --all-namespaces <<< $'n\n')
|
||||||
|
kube::test::if_has_string "${output_message}" 'configmap/two' 'configmap/one'
|
||||||
|
# confirm dry-run=server deletions
|
||||||
|
kubectl delete configmap --dry-run=server --interactive -l deletetest=true --all-namespaces <<< $'y\n'
|
||||||
|
# not confirm resource deletions
|
||||||
|
output_message=$(kubectl delete configmap --interactive -l deletetest=true --all-namespaces <<< $'n\n')
|
||||||
|
kube::test::if_has_string "${output_message}" 'configmap/two' 'configmap/one'
|
||||||
|
kubectl config set-context "${CONTEXT}" --namespace="${ns_one}"
|
||||||
|
kube::test::get_object_assert 'configmap -l deletetest' "{{range.items}}{{${id_field:?}}}:{{end}}" 'one:'
|
||||||
|
kubectl config set-context "${CONTEXT}" --namespace="${ns_two}"
|
||||||
|
kube::test::get_object_assert 'configmap -l deletetest' "{{range.items}}{{${id_field:?}}}:{{end}}" 'two:'
|
||||||
|
|
||||||
|
# clean configmaps with label deletetest=true
|
||||||
|
kubectl delete configmap -l deletetest=true --all-namespaces
|
||||||
|
|
||||||
|
# create new configmaps
|
||||||
|
kubectl create configmap "third" --namespace="${ns_one}"
|
||||||
|
kubectl create configmap "fourth" --namespace="${ns_two}"
|
||||||
|
kubectl label configmap "third" --namespace="${ns_one}" deletetest2=true
|
||||||
|
kubectl label configmap "fourth" --namespace="${ns_two}" deletetest2=true
|
||||||
|
|
||||||
|
# confirm all resource deletions with waiting
|
||||||
|
kubectl delete configmaps --interactive -l deletetest2=true --all-namespaces --wait <<< $'y\n'
|
||||||
|
|
||||||
|
# no configmaps should be in either of those namespaces with label deletetest2
|
||||||
|
kubectl config set-context "${CONTEXT}" --namespace="${ns_one}"
|
||||||
|
kube::test::get_object_assert 'configmap -l deletetest2' "{{range.items}}{{${id_field:?}}}:{{end}}" ''
|
||||||
|
kubectl config set-context "${CONTEXT}" --namespace="${ns_two}"
|
||||||
|
kube::test::get_object_assert 'configmap -l deletetest2' "{{range.items}}{{${id_field:?}}}:{{end}}" ''
|
||||||
|
|
||||||
|
# clean configmaps with label deletetest2=true
|
||||||
|
kubectl delete configmap -l deletetest2=true --all-namespaces
|
||||||
|
|
||||||
|
# create new configmaps in one namespace
|
||||||
|
kubectl create configmap "fifth" --namespace="${ns_one}"
|
||||||
|
kubectl create configmap "sixth" --namespace="${ns_one}"
|
||||||
|
kubectl label configmap "fifth" --namespace="${ns_one}" deletetest3=true
|
||||||
|
kubectl label configmap "sixth" --namespace="${ns_one}" deletetest3=true
|
||||||
|
|
||||||
|
# confirm all resource deletions with forcing and waiting
|
||||||
|
kubectl delete configmaps -l deletetest3=true --force --interactive --namespace="${ns_one}" --wait <<< $'y\n'
|
||||||
|
|
||||||
|
# no configmaps should be in either of those namespaces with label deletetest3
|
||||||
|
kubectl config set-context "${CONTEXT}" --namespace="${ns_one}"
|
||||||
|
kube::test::get_object_assert 'configmap -l deletetest3' "{{range.items}}{{${id_field:?}}}:{{end}}" ''
|
||||||
|
|
||||||
|
unset KUBECTL_INTERACTIVE_DELETE
|
||||||
|
|
||||||
|
set +o nounset
|
||||||
|
set +o errexit
|
||||||
|
}
|
||||||
|
@ -618,6 +618,13 @@ runTests() {
|
|||||||
record_command run_kubectl_delete_allnamespaces_tests
|
record_command run_kubectl_delete_allnamespaces_tests
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
######################
|
||||||
|
# Delete --interactive #
|
||||||
|
######################
|
||||||
|
if kube::test::if_supports_resource "${configmaps}" ; then
|
||||||
|
record_command run_kubectl_delete_interactive_tests
|
||||||
|
fi
|
||||||
|
|
||||||
##################
|
##################
|
||||||
# Global timeout #
|
# Global timeout #
|
||||||
##################
|
##################
|
||||||
|
Loading…
Reference in New Issue
Block a user