diff --git a/cmd/purge.go b/cmd/purge.go index dee2dc777..bc4132046 100644 --- a/cmd/purge.go +++ b/cmd/purge.go @@ -32,7 +32,7 @@ If using Cloud Foundry, the steps are: case 0: log.Fatal("missing service instance GUID") case 1: - purge(args[0]) + purgeServiceInstance(args[0]) default: log.Fatal("too many arguments") } @@ -42,8 +42,8 @@ If using Cloud Foundry, the steps are: rootCmd.AddCommand(purgeCmd) } -func purge(serviceInstanceGUID string) { - logger := utils.NewLogger("purge") +func purgeServiceInstance(serviceInstanceGUID string) { + logger := utils.NewLogger("purge-service-instance") db := dbservice.New(logger) encryptor := setupDBEncryption(db, logger) store := storage.New(db, encryptor) @@ -53,14 +53,8 @@ func purge(serviceInstanceGUID string) { log.Fatalf("error listing bindings: %s", err) } for _, bindingGUID := range bindings { - if err := store.DeleteServiceBindingCredentials(bindingGUID, serviceInstanceGUID); err != nil { - log.Fatalf("error deleting binding credentials for %q: %s", bindingGUID, err) - } - if err := store.DeleteBindRequestDetails(bindingGUID, serviceInstanceGUID); err != nil { - log.Fatalf("error deleting binding request details for %q: %s", bindingGUID, err) - } - if err := store.DeleteTerraformDeployment(fmt.Sprintf("tf:%s:%s", serviceInstanceGUID, bindingGUID)); err != nil { - log.Fatalf("error deleting binding terraform deployment for %q: %s", bindingGUID, err) + if err := deleteServiceBindingFromStore(store, serviceInstanceGUID, bindingGUID); err != nil { + log.Fatalf("error deleting binding %q for service instance %q: %s", bindingGUID, serviceInstanceGUID, err) } } if err := store.DeleteProvisionRequestDetails(serviceInstanceGUID); err != nil { diff --git a/cmd/purge_binding.go b/cmd/purge_binding.go new file mode 100644 index 000000000..0ed8de613 --- /dev/null +++ b/cmd/purge_binding.go @@ -0,0 +1,99 @@ +package cmd + +import ( + "fmt" + "log" + + "github.com/spf13/cobra" + + "github.com/cloudfoundry/cloud-service-broker/dbservice" + "github.com/cloudfoundry/cloud-service-broker/internal/storage" + "github.com/cloudfoundry/cloud-service-broker/utils" +) + +func init() { + purgeCmd := &cobra.Command{ + Use: "purge-binding", + Short: "purge a service binding from the database", + Long: `Lets you remove a service binding (or service key) from the Cloud Service Broker database. + +It does not actually delete the service binding, it just removes all references from the database. +This can be used to remove references to a service binding that has been manually removed, +or to clean up a service binding that fails to delete. + +If using Cloud Foundry, identify the GUID of the service instance: + + cf service --guid # Prints the service instance guid + +Then identify the GUID of the service binding, or service key that you want to remove. +You can see the service keys and bindings for a service instance by running: + + cf curl /v3/service_credential_bindings?service_instance_guids= + +Remove the binding from Cloud Service broker: + + cloud-service-broker purge + +Then you can delete the binding from Cloud Foundry. Cloud Service Broker will confirm +to Cloud Foundry that the service binding or key no longer exists, and it will be removed +from the Cloud Foundry database + + cf unbind-service + +Or + + cf delete-service-key +`, + Run: func(cmd *cobra.Command, args []string) { + switch len(args) { + case 0: + log.Fatal("missing service instance GUID and service binding GUID") + case 1: + log.Fatal("missing service binding GUID") + case 2: + purgeServiceBinding(args[0], args[1]) + default: + log.Fatal("too many arguments") + } + }, + } + + rootCmd.AddCommand(purgeCmd) +} + +func purgeServiceBinding(serviceInstanceGUID, serviceBindingGUID string) { + logger := utils.NewLogger("purge-service-binding") + db := dbservice.New(logger) + encryptor := setupDBEncryption(db, logger) + store := storage.New(db, encryptor) + + bindings, err := store.GetServiceBindingIDsForServiceInstance(serviceInstanceGUID) + if err != nil { + log.Fatalf("error listing bindings: %s", err) + } + for _, bindingGUID := range bindings { + if bindingGUID == serviceBindingGUID { + if err := deleteServiceBindingFromStore(store, serviceInstanceGUID, serviceBindingGUID); err != nil { + log.Fatalf("error deleting binding %q for service instance %q: %s", serviceBindingGUID, serviceInstanceGUID, err) + } + log.Printf("deleted binding %q for service instance %q from the Cloud Service Broker database", serviceBindingGUID, serviceInstanceGUID) + return + } + } + + log.Fatalf("could not find service binding %q for service instance %q", serviceBindingGUID, serviceInstanceGUID) +} + +func deleteServiceBindingFromStore(store *storage.Storage, serviceInstanceGUID, serviceBindingGUID string) error { + if err := store.DeleteServiceBindingCredentials(serviceBindingGUID, serviceInstanceGUID); err != nil { + return fmt.Errorf("error deleting binding credentials for %q: %w", serviceBindingGUID, err) + } + if err := store.DeleteBindRequestDetails(serviceBindingGUID, serviceInstanceGUID); err != nil { + return fmt.Errorf("error deleting binding request details for %q: %w", serviceBindingGUID, err) + } + if err := store.DeleteTerraformDeployment(fmt.Sprintf("tf:%s:%s", serviceInstanceGUID, serviceBindingGUID)); err != nil { + return fmt.Errorf("error deleting binding terraform deployment for %q: %s", serviceBindingGUID, err) + } + + return nil +} diff --git a/integrationtest/fixtures/purge-service-binding/fake-bind.tf b/integrationtest/fixtures/purge-service-binding/fake-bind.tf new file mode 100644 index 000000000..e69de29bb diff --git a/integrationtest/fixtures/purge-service-binding/fake-provision.tf b/integrationtest/fixtures/purge-service-binding/fake-provision.tf new file mode 100644 index 000000000..e69de29bb diff --git a/integrationtest/fixtures/purge-service-binding/fake-service.yml b/integrationtest/fixtures/purge-service-binding/fake-service.yml new file mode 100644 index 000000000..57d2de8b7 --- /dev/null +++ b/integrationtest/fixtures/purge-service-binding/fake-service.yml @@ -0,0 +1,22 @@ +version: 1 +name: fake-service +id: 2f36d5c6-ccc3-11ee-a3be-cb7c74dcfe9a +description: description +display_name: Fake +image_url: https://example.com/icon.jpg +documentation_url: https://example.com +support_url: https://example.com/support.html +plans: +- name: standard + id: 21a3e6c4-ccc3-11ee-a9dd-d74726b3c0d2 + description: Standard plan + display_name: Standard +provision: + template_ref: fake-provision.tf +bind: + template_refs: + main: fake-bind.tf + user_inputs: + - field_name: foo + type: string + details: needed so that BindRequestDetails gets stored diff --git a/integrationtest/fixtures/purge-service-binding/manifest.yml b/integrationtest/fixtures/purge-service-binding/manifest.yml new file mode 100644 index 000000000..2c8c3ed0e --- /dev/null +++ b/integrationtest/fixtures/purge-service-binding/manifest.yml @@ -0,0 +1,16 @@ +packversion: 1 +name: fake-brokerpak +version: 0.1.0 +metadata: + author: noone@nowhere.com +platforms: +- os: linux + arch: amd64 +- os: darwin + arch: amd64 +terraform_binaries: +- name: terraform + version: 1.5.7 + source: https://github.com/hashicorp/terraform/archive/v1.5.7.zip +service_definitions: +- fake-service.yml diff --git a/integrationtest/purge_service_binding_test.go b/integrationtest/purge_service_binding_test.go new file mode 100644 index 000000000..992de3278 --- /dev/null +++ b/integrationtest/purge_service_binding_test.go @@ -0,0 +1,63 @@ +package integrationtest_test + +import ( + "fmt" + "os" + "os/exec" + "time" + + "github.com/cloudfoundry/cloud-service-broker/integrationtest/packer" + "github.com/cloudfoundry/cloud-service-broker/internal/testdrive" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Purge Service Binding", func() { + const ( + serviceOfferingGUID = "2f36d5c6-ccc3-11ee-a3be-cb7c74dcfe9a" + servicePlanGUID = "21a3e6c4-ccc3-11ee-a9dd-d74726b3c0d2" + bindParams = `{"foo":"bar"}` + ) + + It("purges the correct service binding and no others", func() { + By("creating a broker with brokerpak") + brokerpak := must(packer.BuildBrokerpak(csb, fixtures("purge-service-binding"))) + broker := must(testdrive.StartBroker(csb, brokerpak, database)) + DeferCleanup(func() { + broker.Stop() + cleanup(brokerpak) + }) + + By("creating a service with bindings to purge") + instance := must(broker.Provision(serviceOfferingGUID, servicePlanGUID)) + keepBinding1 := must(broker.CreateBinding(instance, testdrive.WithBindingParams(bindParams))) + purgeBinding := must(broker.CreateBinding(instance, testdrive.WithBindingParams(bindParams))) + keepBinding2 := must(broker.CreateBinding(instance, testdrive.WithBindingParams(bindParams))) + + By("stopping the broker") + broker.Stop() + + By("purging the binding") + purgeServiceBinding(database, instance.GUID, purgeBinding.GUID) + + By("checking that we purged the service binding") + expectServiceBindingStatus(instance.GUID, purgeBinding.GUID, BeFalse()) + + By("checking that the other service bindings still exists") + expectServiceBindingStatus(instance.GUID, keepBinding1.GUID, BeTrue()) + expectServiceBindingStatus(instance.GUID, keepBinding2.GUID, BeTrue()) + }) +}) + +func purgeServiceBinding(database, serviceInstanceGUID, serviceBindingGUID string) { + cmd := exec.Command(csb, "purge-binding", serviceInstanceGUID, serviceBindingGUID) + cmd.Env = append( + os.Environ(), + "DB_TYPE=sqlite3", + fmt.Sprintf("DB_PATH=%s", database), + ) + purgeSession, err := Start(cmd, GinkgoWriter, GinkgoWriter) + Expect(err).WithOffset(1).NotTo(HaveOccurred()) + Eventually(purgeSession).WithTimeout(time.Minute).WithOffset(1).Should(Exit(0)) +}