From 9db404aa867f1e96743388630b163c89b1ead677 Mon Sep 17 00:00:00 2001 From: Tara Gu Date: Sun, 10 Mar 2019 11:43:11 -0400 Subject: [PATCH] Unbind abandon --- cmd/svcat/binding/unbind_cmd.go | 70 +++++++++++++++++-- cmd/svcat/binding/unbind_cmd_test.go | 62 ++++++++++++++-- cmd/svcat/testdata/output/completion-bash.txt | 5 ++ cmd/svcat/testdata/output/completion-zsh.txt | 5 ++ cmd/svcat/testdata/plugin.yaml | 9 +++ 5 files changed, 142 insertions(+), 9 deletions(-) diff --git a/cmd/svcat/binding/unbind_cmd.go b/cmd/svcat/binding/unbind_cmd.go index af1d8fed49b..d3cc6ee33cc 100644 --- a/cmd/svcat/binding/unbind_cmd.go +++ b/cmd/svcat/binding/unbind_cmd.go @@ -17,16 +17,22 @@ limitations under the License. package binding import ( + "bufio" "fmt" + "io" + "strings" "sync" "github.com/pkg/errors" "github.com/kubernetes-incubator/service-catalog/cmd/svcat/command" "github.com/kubernetes-incubator/service-catalog/cmd/svcat/output" + "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1beta1" "github.com/spf13/cobra" apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" ) type unbindCmd struct { @@ -35,6 +41,9 @@ type unbindCmd struct { instanceName string bindingNames []string + abandon bool + stdin io.Reader + skipPrompt bool } // NewUnbindCmd builds a "svcat unbind" command @@ -49,6 +58,7 @@ func NewUnbindCmd(cxt *command.Context) *cobra.Command { Example: command.NormalizeExamples(` svcat unbind wordpress-mysql-instance svcat unbind --name wordpress-mysql-binding + svcat unbind --abandon wordpress-mysql-instance `), PreRunE: command.PreRunE(unbindCmd), RunE: command.RunE(unbindCmd), @@ -60,6 +70,20 @@ func NewUnbindCmd(cxt *command.Context) *cobra.Command { []string{}, "The name of the binding to remove", ) + cmd.Flags().BoolVar( + &unbindCmd.abandon, + "abandon", + false, + "Forcefully and immediately delete the resource from Service Catalog ONLY, potentially abandoning any broker resources that you may continue to be charged for.", + ) + cmd.Flags().BoolVarP( + &unbindCmd.skipPrompt, + "yes", + "y", + false, + `Automatic yes to prompts. Assume "yes" as answer to all prompts and run non-interactively.`, + ) + unbindCmd.AddWaitFlags(cmd) return cmd @@ -80,13 +104,49 @@ func (c *unbindCmd) Validate(args []string) error { func (c *unbindCmd) Run() error { // Indicates an error occurred and that a non-zero exit code should be used var hasErrors bool - var bindings []types.NamespacedName + var bindingNames []types.NamespacedName var err error + if c.abandon { + fmt.Fprintln(c.Output, "This action is not reversible and may cause you to be charged for the broker resources that are abandoned.") + if !c.skipPrompt { + fmt.Fprintln(c.Output, "Are you sure? [y|N]: ") + s := bufio.NewScanner(c.stdin) + s.Scan() + + err = s.Err() + fmt.Fprintln(c.Output, err) + + if strings.ToLower(s.Text()) != "y" { + err = fmt.Errorf("aborted abandon operation") + return err + } + } + + if c.instanceName == "" { + err = fmt.Errorf("Please provide instance name; aborted abandon operation") + return err + } + + si := &v1beta1.ServiceInstance{ObjectMeta: metav1.ObjectMeta{Name: c.instanceName, Namespace: c.Namespace}} + bindings, err := c.App.RetrieveBindingsByInstance(si) + if err != nil { + fmt.Fprintln(c.Output, err) + return err + } + for _, binding := range bindings { + finalizers := sets.NewString(binding.Finalizers...) + finalizers.Delete(v1beta1.FinalizerServiceCatalog) + fmt.Fprintln(c.Output, fmt.Sprintf("deleted %s\n", binding.Name)) + } + fmt.Fprintln(c.Output, fmt.Sprintf("deleted %s\n", c.instanceName)) + return nil + } + if c.instanceName != "" { - bindings, err = c.App.Unbind(c.Namespace, c.instanceName) + bindingNames, err = c.App.Unbind(c.Namespace, c.instanceName) } else { - bindings, err = c.App.DeleteBindings(c.getBindingsToDelete()) + bindingNames, err = c.App.DeleteBindings(c.getBindingsToDelete()) } if err != nil { @@ -96,9 +156,9 @@ func (c *unbindCmd) Run() error { } if c.Wait { - hasErrors = c.waitForBindingDeletes("waiting for the binding(s) to be deleted...", bindings...) || hasErrors + hasErrors = c.waitForBindingDeletes("waiting for the binding(s) to be deleted...", bindingNames...) || hasErrors } else { - for _, binding := range bindings { + for _, binding := range bindingNames { output.WriteDeletedResourceName(c.Output, binding.Name) } } diff --git a/cmd/svcat/binding/unbind_cmd_test.go b/cmd/svcat/binding/unbind_cmd_test.go index 51474b49593..c33b0b49603 100644 --- a/cmd/svcat/binding/unbind_cmd_test.go +++ b/cmd/svcat/binding/unbind_cmd_test.go @@ -19,20 +19,19 @@ package binding import ( "bytes" "errors" + "fmt" "strings" "testing" "github.com/kubernetes-incubator/service-catalog/cmd/svcat/command" - "github.com/kubernetes-incubator/service-catalog/cmd/svcat/test" + svcattest "github.com/kubernetes-incubator/service-catalog/cmd/svcat/test" "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1beta1" svcatfake "github.com/kubernetes-incubator/service-catalog/pkg/client/clientset_generated/clientset/fake" "github.com/kubernetes-incubator/service-catalog/pkg/svcat" - "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" k8sfake "k8s.io/client-go/kubernetes/fake" testing2 "k8s.io/client-go/testing" - - _ "github.com/kubernetes-incubator/service-catalog/internal/test" ) func TestUnbindCommand(t *testing.T) { @@ -47,6 +46,9 @@ func TestUnbindCommand(t *testing.T) { wantOutput string wantError bool allowDiffOrder bool // whether the order of lines in the output can be different from the one in wantOutput + abandon bool // delete all finalizers from the service instance so that it is deleted immediately + userResponse string + skipPrompt bool }{ { name: "delete binding", @@ -154,6 +156,50 @@ func TestUnbindCommand(t *testing.T) { wantOutput: "error:\n remove binding default/badbinding failed: sabotaged\ncould not remove all bindings", wantError: true, }, + { + name: "delete all finalizers with user answering yes to interactive prompt", + fakeBindings: []string{"mybinding"}, + instanceName: "myinstance", + abandon: true, + userResponse: "y", + wantOutput: "This action is not reversible and may cause you to be charged for the broker resources that are abandoned.\nAre you sure? [y|N]: \n\ndeleted myinstance", + }, + { + name: "delete all finalizers with user skip prompt flag", + fakeBindings: []string{"mybinding"}, + instanceName: "myinstance", + abandon: true, + skipPrompt: true, + userResponse: "y", + wantOutput: "This action is not reversible and may cause you to be charged for the broker resources that are abandoned.\ndeleted myinstance", + }, + { + name: "delete all finalizers with user answering no to interactive prompt", + fakeBindings: []string{"mybinding"}, + instanceName: "myinstance", + abandon: true, + userResponse: "n", + wantOutput: "This action is not reversible and may cause you to be charged for the broker resources that are abandoned.\nAre you sure? [y|N]: \n\naborted abandon operation", + wantError: true, + }, + { + name: "delete all finalizers with user providing random response", + fakeBindings: []string{"mybinding"}, + instanceName: "myinstance", + abandon: true, + userResponse: "foo", + wantOutput: "This action is not reversible and may cause you to be charged for the broker resources that are abandoned.\nAre you sure? [y|N]: \n\naborted abandon operation", + wantError: true, + }, + { + name: "delete all finalizers - instance name not provided", + fakeBindings: []string{"mybinding"}, + instanceName: "", + abandon: true, + userResponse: "y", + wantOutput: "This action is not reversible and may cause you to be charged for the broker resources that are abandoned.\nAre you sure? [y|N]: \n\nPlease provide instance name; aborted abandon operation", + wantError: true, + }, } for _, tc := range testcases { @@ -203,6 +249,14 @@ func TestUnbindCommand(t *testing.T) { cmd.bindingNames = tc.bindingNames cmd.instanceName = tc.instanceName cmd.Wait = tc.wait + cmd.abandon = tc.abandon + cmd.skipPrompt = tc.skipPrompt + + if tc.userResponse != "" { + var stdin bytes.Buffer + stdin.Write([]byte(fmt.Sprintf("%s\n", tc.userResponse))) + cmd.stdin = &stdin + } err := cmd.Run() diff --git a/cmd/svcat/testdata/output/completion-bash.txt b/cmd/svcat/testdata/output/completion-bash.txt index 5996cfc72e9..06d7d9309c3 100644 --- a/cmd/svcat/testdata/output/completion-bash.txt +++ b/cmd/svcat/testdata/output/completion-bash.txt @@ -1068,6 +1068,8 @@ _svcat_unbind() flags_with_completion=() flags_completion=() + flags+=("--abandon") + local_nonpersistent_flags+=("--abandon") flags+=("--interval=") local_nonpersistent_flags+=("--interval=") flags+=("--name=") @@ -1079,6 +1081,9 @@ _svcat_unbind() local_nonpersistent_flags+=("--timeout=") flags+=("--wait") local_nonpersistent_flags+=("--wait") + flags+=("--yes") + flags+=("-y") + local_nonpersistent_flags+=("--yes") flags+=("--context=") flags+=("--kubeconfig=") flags+=("--logtostderr") diff --git a/cmd/svcat/testdata/output/completion-zsh.txt b/cmd/svcat/testdata/output/completion-zsh.txt index f62dd796da7..8a0d3a4cba8 100644 --- a/cmd/svcat/testdata/output/completion-zsh.txt +++ b/cmd/svcat/testdata/output/completion-zsh.txt @@ -1202,6 +1202,8 @@ _svcat_unbind() flags_with_completion=() flags_completion=() + flags+=("--abandon") + local_nonpersistent_flags+=("--abandon") flags+=("--interval=") local_nonpersistent_flags+=("--interval=") flags+=("--name=") @@ -1213,6 +1215,9 @@ _svcat_unbind() local_nonpersistent_flags+=("--timeout=") flags+=("--wait") local_nonpersistent_flags+=("--wait") + flags+=("--yes") + flags+=("-y") + local_nonpersistent_flags+=("--yes") flags+=("--context=") flags+=("--kubeconfig=") flags+=("--logtostderr") diff --git a/cmd/svcat/testdata/plugin.yaml b/cmd/svcat/testdata/plugin.yaml index cdfca7917b5..4ab6bdf3221 100644 --- a/cmd/svcat/testdata/plugin.yaml +++ b/cmd/svcat/testdata/plugin.yaml @@ -413,7 +413,12 @@ tree: example: |2- svcat unbind wordpress-mysql-instance svcat unbind --name wordpress-mysql-binding + svcat unbind --abandon wordpress-mysql-instance flags: + - desc: Forcefully and immediately delete the resource from Service Catalog ONLY, + potentially abandoning any broker resources that you may continue to be charged + for. + name: abandon - desc: 'Poll interval for --wait, specified in human readable format: 30s, 1m, 1h' name: interval @@ -424,6 +429,10 @@ tree: name: timeout - desc: Wait until the operation completes. name: wait + - desc: Automatic yes to prompts. Assume "yes" as answer to all prompts and run + non-interactively. + name: "yes" + shorthand: "y" name: unbind shortDesc: Unbinds an instance. When an instance name is specified, all of its bindings are removed, otherwise use --name to remove a specific binding