mirror of https://github.com/kubernetes/kops.git
Merge pull request #1367 from frodopwns/1302-require-confirm-on-delete
Require a confirmation when deleting resources #1302
This commit is contained in:
commit
89460916c6
|
@ -17,17 +17,55 @@ limitations under the License.
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"k8s.io/kops/util/pkg/ui"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var confirmDelete bool
|
||||
|
||||
// deleteCmd represents the delete command
|
||||
var deleteCmd = &cobra.Command{
|
||||
Use: "delete",
|
||||
Short: "delete clusters",
|
||||
Long: `Delete clusters`,
|
||||
SuggestFor: []string{"rm"},
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
// cobra doesn't give you the full arg list even though it should.
|
||||
args = os.Args
|
||||
|
||||
// args should be [delete, resource, resource name]
|
||||
// if there are less args than 3 confirming isnt necessary as the child command will fail
|
||||
if !confirmDelete && len(args) >= 3 {
|
||||
message := fmt.Sprintf(
|
||||
"Do you really want to %s? This action cannot be undone.",
|
||||
strings.Join(args[1:], " "),
|
||||
)
|
||||
|
||||
c := &ui.ConfirmArgs{
|
||||
Out: os.Stdout,
|
||||
Message: message,
|
||||
Default: "no",
|
||||
Retries: 2,
|
||||
}
|
||||
|
||||
confirmed, err := ui.GetConfirm(c)
|
||||
if err != nil {
|
||||
exitWithError(err)
|
||||
}
|
||||
if !confirmed {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
deleteCmd.PersistentFlags().BoolVarP(&confirmDelete, "yes", "y", false, "Auto confirm deletetion.")
|
||||
rootCommand.AddCommand(deleteCmd)
|
||||
}
|
||||
|
|
|
@ -18,6 +18,9 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
api "k8s.io/kops/pkg/apis/kops"
|
||||
"k8s.io/kops/pkg/apis/kops/registry"
|
||||
|
@ -28,7 +31,6 @@ import (
|
|||
"k8s.io/kops/upup/pkg/kutil"
|
||||
"k8s.io/kops/util/pkg/tables"
|
||||
"k8s.io/kops/util/pkg/vfs"
|
||||
"os"
|
||||
)
|
||||
|
||||
type DeleteClusterCmd struct {
|
||||
|
@ -55,7 +57,15 @@ func init() {
|
|||
|
||||
deleteCmd.AddCommand(cmd)
|
||||
|
||||
cmd.Flags().BoolVar(&deleteCluster.Yes, "yes", false, "Delete without confirmation")
|
||||
// had to do this because this init function is running before the flag is set
|
||||
for _, arg := range os.Args {
|
||||
arg = strings.ToLower(arg)
|
||||
if arg == "-y" || arg == "--yes" {
|
||||
deleteCluster.Yes = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&deleteCluster.Unregister, "unregister", false, "Don't delete cloud resources, just unregister the cluster")
|
||||
cmd.Flags().BoolVar(&deleteCluster.External, "external", false, "Delete an external cluster")
|
||||
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"k8s.io/kops/util/pkg/ui"
|
||||
)
|
||||
|
||||
// TestContainsString tests the ContainsString() function
|
||||
func TestContainsString(t *testing.T) {
|
||||
testString := "my test string"
|
||||
answer := ui.ContainsString(strings.Split(testString, " "), "my")
|
||||
if !answer {
|
||||
t.Fatal("Failed to find string using ui.ContainsString()")
|
||||
}
|
||||
answer = ui.ContainsString(strings.Split(testString, " "), "string")
|
||||
if !answer {
|
||||
t.Fatal("Failed to find string using ui.ContainsString()")
|
||||
}
|
||||
answer = ui.ContainsString(strings.Split(testString, " "), "random")
|
||||
if answer {
|
||||
t.Fatal("Found string that does not exist using ui.ContainsString()")
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfirmation attempts to test the majority of the ui.GetConfirm function used in the 'kogs delete' commands
|
||||
func TestConfirmation(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
c := &ui.ConfirmArgs{
|
||||
Message: "Are you sure you want to remove?",
|
||||
Out: &out,
|
||||
TestVal: "no",
|
||||
Default: "no",
|
||||
}
|
||||
|
||||
answer, err := ui.GetConfirm(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(out.String(), "Are you sure") {
|
||||
t.Fatal("Confirmation not in output")
|
||||
}
|
||||
if !strings.Contains(out.String(), "y/N") {
|
||||
t.Fatal("Default 'No' was not set")
|
||||
}
|
||||
if answer == true {
|
||||
t.Fatal("Confirmation should have been denied.")
|
||||
}
|
||||
|
||||
c.Default = "yes"
|
||||
answer, err = ui.GetConfirm(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(out.String(), "Y/n") {
|
||||
t.Fatal("Default 'Yes' was not set")
|
||||
}
|
||||
|
||||
c.TestVal = "yes"
|
||||
answer, err = ui.GetConfirm(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if answer != true {
|
||||
t.Fatal("Confirmation should have been approved.")
|
||||
}
|
||||
|
||||
}
|
|
@ -62,4 +62,5 @@ k8s.io/kops/upup/tools/generators/fitask
|
|||
k8s.io/kops/upup/tools/generators/pkg/codegen
|
||||
k8s.io/kops/util/pkg/hashing
|
||||
k8s.io/kops/util/pkg/tables
|
||||
k8s.io/kops/util/pkg/ui
|
||||
k8s.io/kops/util/pkg/vfs
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ConfirmArgs encapsulates the arguments that can he passed to GetConfirm
|
||||
type ConfirmArgs struct {
|
||||
Out io.Writer // os.Stdout or &bytes.Buffer used to putput the message above the confirmation
|
||||
Message string // what you want to say to the user before confirming
|
||||
Default string // if you hit enter instead of yes or no shoudl it approve or deny
|
||||
TestVal string // if you need to test without the interactive prompt then set the user response here
|
||||
Retries int // how many tines to ask for a valid confirmation before giving up
|
||||
RetryCount int // how many attempts have been made
|
||||
}
|
||||
|
||||
// GetConfirm prompts a user for a yes or no answer.
|
||||
// In order to test this function som extra parameters are reqired:
|
||||
//
|
||||
// out: an io.Writer that allows you to direct prints to stdout or another location
|
||||
// message: the string that will be printed just before prompting for a yes or no.
|
||||
// answer: "", "yes", or "no" - this allows for easier testing
|
||||
func GetConfirm(c *ConfirmArgs) (bool, error) {
|
||||
if c.Default != "" {
|
||||
c.Default = strings.ToLower(c.Default)
|
||||
}
|
||||
answerTemplate := "(%s/%s)"
|
||||
switch c.Default {
|
||||
case "yes", "y":
|
||||
c.Message = c.Message + fmt.Sprintf(answerTemplate, "Y", "n")
|
||||
case "no", "n":
|
||||
c.Message = c.Message + fmt.Sprintf(answerTemplate, "y", "N")
|
||||
default:
|
||||
c.Message = c.Message + fmt.Sprintf(answerTemplate, "y", "n")
|
||||
}
|
||||
fmt.Fprintln(c.Out, c.Message)
|
||||
|
||||
// these are the acceptable answers
|
||||
okayResponses := []string{"y", "yes"}
|
||||
nokayResponses := []string{"n", "no"}
|
||||
response := c.TestVal
|
||||
|
||||
// only prompt user if you predefined answer was passed in
|
||||
if response == "" {
|
||||
_, err := fmt.Scanln(&response)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
responseLower := strings.ToLower(response)
|
||||
// make sure the response is valid
|
||||
if ContainsString(okayResponses, responseLower) {
|
||||
return true, nil
|
||||
} else if ContainsString(nokayResponses, responseLower) {
|
||||
return false, nil
|
||||
} else if c.Default != "" && response == "" {
|
||||
if string(c.Default[0]) == "y" {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
fmt.Printf("invalid response: %s\n\n", response)
|
||||
|
||||
// if c.RetryCount exceeds the requested number of retries then give up
|
||||
if c.RetryCount >= c.Retries {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
c.RetryCount++
|
||||
return GetConfirm(c)
|
||||
}
|
||||
|
||||
// ContainsString returns true if slice contains the element
|
||||
func ContainsString(slice []string, element string) bool {
|
||||
for _, arg := range slice {
|
||||
if arg == element {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
Loading…
Reference in New Issue