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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"k8s.io/kops/util/pkg/ui"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var confirmDelete bool
|
||||||
|
|
||||||
// deleteCmd represents the delete command
|
// deleteCmd represents the delete command
|
||||||
var deleteCmd = &cobra.Command{
|
var deleteCmd = &cobra.Command{
|
||||||
Use: "delete",
|
Use: "delete",
|
||||||
Short: "delete clusters",
|
Short: "delete clusters",
|
||||||
Long: `Delete clusters`,
|
Long: `Delete clusters`,
|
||||||
SuggestFor: []string{"rm"},
|
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() {
|
func init() {
|
||||||
|
deleteCmd.PersistentFlags().BoolVarP(&confirmDelete, "yes", "y", false, "Auto confirm deletetion.")
|
||||||
rootCommand.AddCommand(deleteCmd)
|
rootCommand.AddCommand(deleteCmd)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,9 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
api "k8s.io/kops/pkg/apis/kops"
|
api "k8s.io/kops/pkg/apis/kops"
|
||||||
"k8s.io/kops/pkg/apis/kops/registry"
|
"k8s.io/kops/pkg/apis/kops/registry"
|
||||||
|
@ -28,7 +31,6 @@ import (
|
||||||
"k8s.io/kops/upup/pkg/kutil"
|
"k8s.io/kops/upup/pkg/kutil"
|
||||||
"k8s.io/kops/util/pkg/tables"
|
"k8s.io/kops/util/pkg/tables"
|
||||||
"k8s.io/kops/util/pkg/vfs"
|
"k8s.io/kops/util/pkg/vfs"
|
||||||
"os"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type DeleteClusterCmd struct {
|
type DeleteClusterCmd struct {
|
||||||
|
@ -55,7 +57,15 @@ func init() {
|
||||||
|
|
||||||
deleteCmd.AddCommand(cmd)
|
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.Unregister, "unregister", false, "Don't delete cloud resources, just unregister the cluster")
|
||||||
cmd.Flags().BoolVar(&deleteCluster.External, "external", false, "Delete an external 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/upup/tools/generators/pkg/codegen
|
||||||
k8s.io/kops/util/pkg/hashing
|
k8s.io/kops/util/pkg/hashing
|
||||||
k8s.io/kops/util/pkg/tables
|
k8s.io/kops/util/pkg/tables
|
||||||
|
k8s.io/kops/util/pkg/ui
|
||||||
k8s.io/kops/util/pkg/vfs
|
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