Merge pull request #1367 from frodopwns/1302-require-confirm-on-delete

Require a confirmation when deleting resources #1302
This commit is contained in:
Justin Santa Barbara 2017-01-19 10:21:51 -05:00 committed by GitHub
commit 89460916c6
5 changed files with 238 additions and 2 deletions

View File

@ -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)
}

View File

@ -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")

View File

@ -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.")
}
}

View File

@ -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

101
util/pkg/ui/user.go Normal file
View File

@ -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
}