Skip to content

Commit

Permalink
Merge pull request kubernetes#1367 from frodopwns/1302-require-confir…
Browse files Browse the repository at this point in the history
…m-on-delete

Require a confirmation when deleting resources kubernetes#1302
  • Loading branch information
justinsb authored Jan 19, 2017
2 parents 59ea88b + 4496f9d commit 8946091
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 2 deletions.
38 changes: 38 additions & 0 deletions cmd/kops/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
14 changes: 12 additions & 2 deletions cmd/kops/delete_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand All @@ -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")

Expand Down
86 changes: 86 additions & 0 deletions cmd/kops/delete_confirm_test.go
Original file line number Diff line number Diff line change
@@ -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.")
}

}
1 change: 1 addition & 0 deletions hack/.packages
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 101 additions & 0 deletions util/pkg/ui/user.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 8946091

Please sign in to comment.