From cbca74726ac62962256ca7fa3a07a83c4821507b Mon Sep 17 00:00:00 2001 From: Derek Carr Date: Wed, 24 May 2017 19:43:23 -0400 Subject: [PATCH] instance never provisioned should just delete --- Makefile | 3 + pkg/controller/controller_instance.go | 384 ++++++++++++--------- pkg/controller/controller_instance_test.go | 51 +++ test/e2e/framework/util.go | 6 +- test/e2e/instance.go | 90 +++++ 5 files changed, 364 insertions(+), 170 deletions(-) create mode 100644 test/e2e/instance.go diff --git a/Makefile b/Makefile index 6916a306479..85f33628a42 100644 --- a/Makefile +++ b/Makefile @@ -249,6 +249,9 @@ test-integration: .init $(scBuildImageTarget) build # golang integration tests $(DOCKER_CMD) test/integration.sh +clean-e2e: + rm -f $(BINDIR)/e2e.test + test-e2e: .generate_files $(BINDIR)/e2e.test $(BINDIR)/e2e.test diff --git a/pkg/controller/controller_instance.go b/pkg/controller/controller_instance.go index e3a64789888..8fdc4d43ecb 100644 --- a/pkg/controller/controller_instance.go +++ b/pkg/controller/controller_instance.go @@ -70,6 +70,127 @@ func (c *controller) instanceUpdate(oldObj, newObj interface{}) { c.instanceAdd(newObj) } +// reconcileInstanceDelete is responsible for handling any instance whose deletion timestamp is set. +func (c *controller) reconcileInstanceDelete(instance *v1alpha1.Instance) error { + // nothing to do... + if instance.DeletionTimestamp == nil { + return nil + } + + finalizerToken := v1alpha1.FinalizerServiceCatalog + finalizers := sets.NewString(instance.Finalizers...) + if !finalizers.Has(finalizerToken) { + return nil + } + + // if there is no op in progress, and the instance was never provisioned, we can just delete. + // this can happen if the service class name referenced never existed. + if !instance.Status.AsyncOpInProgress && instance.Status.Checksum == nil { + finalizers.Delete(finalizerToken) + // Clear the finalizer + return c.updateInstanceFinalizers(instance, finalizers.List()) + } + + // All updates not having a DeletingTimestamp will have been handled above + // and returned early. If we reach this point, we're dealing with an update + // that's actually a soft delete-- i.e. we have some finalization to do. + // Since the potential exists for an instance to have multiple finalizers and + // since those most be cleared in order, we proceed with the soft delete + // only if it's "our turn--" i.e. only if the finalizer we care about is at + // the head of the finalizers list. + serviceClass, servicePlan, brokerName, brokerClient, err := c.getServiceClassPlanAndBroker(instance) + if err != nil { + return err + } + + glog.V(4).Infof("Finalizing Instance %v/%v", instance.Namespace, instance.Name) + + request := &brokerapi.DeleteServiceInstanceRequest{ + ServiceID: serviceClass.ExternalID, + PlanID: servicePlan.ExternalID, + AcceptsIncomplete: true, + } + + glog.V(4).Infof("Deprovisioning Instance %v/%v of ServiceClass %v at Broker %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName) + response, respCode, err := brokerClient.DeleteServiceInstance(instance.Spec.ExternalID, request) + + if err != nil { + s := fmt.Sprintf( + "Error deprovisioning Instance \"%s/%s\" of ServiceClass %q at Broker %q with status code %d: %s", + instance.Namespace, + instance.Name, + serviceClass.Name, + brokerName, + respCode, + err, + ) + glog.Warning(s) + c.updateInstanceCondition( + instance, + v1alpha1.InstanceConditionReady, + v1alpha1.ConditionUnknown, + errorDeprovisionCalledReason, + "Deprovision call failed. "+s) + c.recorder.Event(instance, api.EventTypeWarning, errorDeprovisionCalledReason, s) + return err + } + + if respCode == http.StatusAccepted { + glog.V(5).Infof("Received asynchronous de-provisioning response for Instance %v/%v of ServiceClass %v at Broker %v: response: %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName, response) + if response.Operation != "" { + instance.Status.LastOperation = &response.Operation + } + + // Tag this instance as having an ongoing async operation so we can enforce + // no other operations against it can start. + instance.Status.AsyncOpInProgress = true + + err := c.updateInstanceCondition( + instance, + v1alpha1.InstanceConditionReady, + v1alpha1.ConditionFalse, + asyncDeprovisioningReason, + asyncDeprovisioningMessage, + ) + if err != nil { + return err + } + } else if respCode == http.StatusOK { + err := c.updateInstanceCondition( + instance, + v1alpha1.InstanceConditionReady, + v1alpha1.ConditionFalse, + successDeprovisionReason, + successDeprovisionMessage, + ) + if err != nil { + return err + } + // Clear the finalizer + finalizers.Delete(finalizerToken) + if err = c.updateInstanceFinalizers(instance, finalizers.List()); err != nil { + return err + } + c.recorder.Event(instance, api.EventTypeNormal, successDeprovisionReason, successDeprovisionMessage) + glog.V(5).Infof("Successfully deprovisioned Instance %v/%v of ServiceClass %v at Broker %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName) + } else { + // the broker returned a failure response + errorDeprovisionCalledMessage := fmt.Sprintf("deprovision call failed") + err := c.updateInstanceCondition( + instance, + v1alpha1.InstanceConditionReady, + v1alpha1.ConditionFalse, + errorDeprovisionCalledReason, + errorDeprovisionCalledMessage, + ) + if err != nil { + return err + } + c.recorder.Eventf(instance, api.EventTypeWarning, errorDeprovisionCalledReason, errorDeprovisionCalledMessage) + } + return nil +} + // reconcileInstance is the control-loop for reconciling Instances. func (c *controller) reconcileInstance(instance *v1alpha1.Instance) error { @@ -98,6 +219,12 @@ func (c *controller) reconcileInstance(instance *v1alpha1.Instance) error { glog.V(4).Infof("Processing Instance %v/%v", instance.Namespace, instance.Name) + // if the instance is marked for deletion, handle that first. + if instance.ObjectMeta.DeletionTimestamp != nil { + glog.V(4).Infof("Soft-deleting Instance %v/%v", instance.Namespace, instance.Name) + return c.reconcileInstanceDelete(instance) + } + serviceClass, servicePlan, brokerName, brokerClient, err := c.getServiceClassPlanAndBroker(instance) if err != nil { return err @@ -107,196 +234,119 @@ func (c *controller) reconcileInstance(instance *v1alpha1.Instance) error { return c.pollInstance(serviceClass, servicePlan, brokerName, brokerClient, instance) } - if instance.DeletionTimestamp == nil { // Add or update - glog.V(4).Infof("Adding/Updating Instance %v/%v", instance.Namespace, instance.Name) - - var parameters map[string]interface{} - if instance.Spec.Parameters != nil { - parameters, err = unmarshalParameters(instance.Spec.Parameters.Raw) - if err != nil { - s := fmt.Sprintf("Failed to unmarshal Instance parameters\n%s\n %s", instance.Spec.Parameters, err) - glog.Warning(s) - c.updateInstanceCondition( - instance, - v1alpha1.InstanceConditionReady, - v1alpha1.ConditionFalse, - errorWithParameters, - "Error unmarshaling instance parameters. "+s, - ) - c.recorder.Event(instance, api.EventTypeWarning, errorWithParameters, s) - return err - } - } + glog.V(4).Infof("Adding/Updating Instance %v/%v", instance.Namespace, instance.Name) - ns, err := c.kubeClient.Core().Namespaces().Get(instance.Namespace, metav1.GetOptions{}) + var parameters map[string]interface{} + if instance.Spec.Parameters != nil { + parameters, err = unmarshalParameters(instance.Spec.Parameters.Raw) if err != nil { - s := fmt.Sprintf("Failed to get namespace %q during instance create: %s", instance.Namespace, err) - glog.Info(s) + s := fmt.Sprintf("Failed to unmarshal Instance parameters\n%s\n %s", instance.Spec.Parameters, err) + glog.Warning(s) c.updateInstanceCondition( instance, v1alpha1.InstanceConditionReady, v1alpha1.ConditionFalse, - errorFindingNamespaceInstanceReason, - "Error finding namespace for instance. "+s, + errorWithParameters, + "Error unmarshaling instance parameters. "+s, ) - c.recorder.Event(instance, api.EventTypeWarning, errorFindingNamespaceInstanceReason, s) + c.recorder.Event(instance, api.EventTypeWarning, errorWithParameters, s) return err } + } - request := &brokerapi.CreateServiceInstanceRequest{ - ServiceID: serviceClass.ExternalID, - PlanID: servicePlan.ExternalID, - Parameters: parameters, - OrgID: string(ns.UID), - SpaceID: string(ns.UID), - AcceptsIncomplete: true, - } - if c.enableOSBAPIContextProfle { - request.ContextProfile = brokerapi.ContextProfile{ - Platform: brokerapi.ContextProfilePlatformKubernetes, - Namespace: instance.Namespace, - } - } - - glog.V(4).Infof("Provisioning a new Instance %v/%v of ServiceClass %v at Broker %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName) - response, respCode, err := brokerClient.CreateServiceInstance(instance.Spec.ExternalID, request) - if err != nil { - s := fmt.Sprintf("Error provisioning Instance \"%s/%s\" of ServiceClass %q at Broker %q: %s", instance.Namespace, instance.Name, serviceClass.Name, brokerName, err) - glog.Warning(s) - c.updateInstanceCondition( - instance, - v1alpha1.InstanceConditionReady, - v1alpha1.ConditionFalse, - errorProvisionCalledReason, - "Provision call failed. "+s) - c.recorder.Event(instance, api.EventTypeWarning, errorProvisionCalledReason, s) - return err - } + ns, err := c.kubeClient.Core().Namespaces().Get(instance.Namespace, metav1.GetOptions{}) + if err != nil { + s := fmt.Sprintf("Failed to get namespace %q during instance create: %s", instance.Namespace, err) + glog.Info(s) + c.updateInstanceCondition( + instance, + v1alpha1.InstanceConditionReady, + v1alpha1.ConditionFalse, + errorFindingNamespaceInstanceReason, + "Error finding namespace for instance. "+s, + ) + c.recorder.Event(instance, api.EventTypeWarning, errorFindingNamespaceInstanceReason, s) + return err + } - if response.DashboardURL != "" { - instance.Status.DashboardURL = &response.DashboardURL + request := &brokerapi.CreateServiceInstanceRequest{ + ServiceID: serviceClass.ExternalID, + PlanID: servicePlan.ExternalID, + Parameters: parameters, + OrgID: string(ns.UID), + SpaceID: string(ns.UID), + AcceptsIncomplete: true, + } + if c.enableOSBAPIContextProfle { + request.ContextProfile = brokerapi.ContextProfile{ + Platform: brokerapi.ContextProfilePlatformKubernetes, + Namespace: instance.Namespace, } + } - // Broker can return either a synchronous or asynchronous - // response, if the response is StatusAccepted it's an async - // and we need to add it to the polling queue. Broker can - // optionally return 'Operation' that will then need to be - // passed back to the broker during polling of last_operation. - if respCode == http.StatusAccepted { - glog.V(5).Infof("Received asynchronous provisioning response for Instance %v/%v of ServiceClass %v at Broker %v: response: %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName, response) - if response.Operation != "" { - instance.Status.LastOperation = &response.Operation - } - - // Tag this instance as having an ongoing async operation so we can enforce - // no other operations against it can start. - instance.Status.AsyncOpInProgress = true - - c.updateInstanceCondition( - instance, - v1alpha1.InstanceConditionReady, - v1alpha1.ConditionFalse, - asyncProvisioningReason, - asyncProvisioningMessage, - ) - c.recorder.Eventf(instance, api.EventTypeNormal, asyncProvisioningReason, asyncProvisioningMessage) - - // Actually, start polling this Service Instance by adding it into the polling queue - key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(instance) - if err != nil { - glog.Errorf("Couldn't create a key for object %+v: %v", instance, err) - return fmt.Errorf("Couldn't create a key for object %+v: %v", instance, err) - } - c.pollingQueue.Add(key) - } else { - glog.V(5).Infof("Successfully provisioned Instance %v/%v of ServiceClass %v at Broker %v: response: %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName, response) - - // TODO: process response - c.updateInstanceCondition( - instance, - v1alpha1.InstanceConditionReady, - v1alpha1.ConditionTrue, - successProvisionReason, - successProvisionMessage, - ) - c.recorder.Eventf(instance, api.EventTypeNormal, successProvisionReason, successProvisionMessage) - } - return nil + glog.V(4).Infof("Provisioning a new Instance %v/%v of ServiceClass %v at Broker %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName) + response, respCode, err := brokerClient.CreateServiceInstance(instance.Spec.ExternalID, request) + if err != nil { + s := fmt.Sprintf("Error provisioning Instance \"%s/%s\" of ServiceClass %q at Broker %q: %s", instance.Namespace, instance.Name, serviceClass.Name, brokerName, err) + glog.Warning(s) + c.updateInstanceCondition( + instance, + v1alpha1.InstanceConditionReady, + v1alpha1.ConditionFalse, + errorProvisionCalledReason, + "Provision call failed. "+s) + c.recorder.Event(instance, api.EventTypeWarning, errorProvisionCalledReason, s) + return err } - // All updates not having a DeletingTimestamp will have been handled above - // and returned early. If we reach this point, we're dealing with an update - // that's actually a soft delete-- i.e. we have some finalization to do. - // Since the potential exists for an instance to have multiple finalizers and - // since those most be cleared in order, we proceed with the soft delete - // only if it's "our turn--" i.e. only if the finalizer we care about is at - // the head of the finalizers list. - if finalizers := sets.NewString(instance.Finalizers...); finalizers.Has(v1alpha1.FinalizerServiceCatalog) { - glog.V(4).Infof("Finalizing Instance %v/%v", instance.Namespace, instance.Name) + if response.DashboardURL != "" { + instance.Status.DashboardURL = &response.DashboardURL + } - request := &brokerapi.DeleteServiceInstanceRequest{ - ServiceID: serviceClass.ExternalID, - PlanID: servicePlan.ExternalID, - AcceptsIncomplete: true, + // Broker can return either a synchronous or asynchronous + // response, if the response is StatusAccepted it's an async + // and we need to add it to the polling queue. Broker can + // optionally return 'Operation' that will then need to be + // passed back to the broker during polling of last_operation. + if respCode == http.StatusAccepted { + glog.V(5).Infof("Received asynchronous provisioning response for Instance %v/%v of ServiceClass %v at Broker %v: response: %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName, response) + if response.Operation != "" { + instance.Status.LastOperation = &response.Operation } - glog.V(4).Infof("Deprovisioning Instance %v/%v of ServiceClass %v at Broker %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName) - response, respCode, err := brokerClient.DeleteServiceInstance(instance.Spec.ExternalID, request) + // Tag this instance as having an ongoing async operation so we can enforce + // no other operations against it can start. + instance.Status.AsyncOpInProgress = true + c.updateInstanceCondition( + instance, + v1alpha1.InstanceConditionReady, + v1alpha1.ConditionFalse, + asyncProvisioningReason, + asyncProvisioningMessage, + ) + c.recorder.Eventf(instance, api.EventTypeNormal, asyncProvisioningReason, asyncProvisioningMessage) + + // Actually, start polling this Service Instance by adding it into the polling queue + key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(instance) if err != nil { - s := fmt.Sprintf( - "Error deprovisioning Instance \"%s/%s\" of ServiceClass %q at Broker %q with status code %d: %s", - instance.Namespace, - instance.Name, - serviceClass.Name, - brokerName, - respCode, - err, - ) - glog.Warning(s) - c.updateInstanceCondition( - instance, - v1alpha1.InstanceConditionReady, - v1alpha1.ConditionUnknown, - errorDeprovisionCalledReason, - "Deprovision call failed. "+s) - c.recorder.Event(instance, api.EventTypeWarning, errorDeprovisionCalledReason, s) - return err + glog.Errorf("Couldn't create a key for object %+v: %v", instance, err) + return fmt.Errorf("Couldn't create a key for object %+v: %v", instance, err) } + c.pollingQueue.Add(key) + } else { + glog.V(5).Infof("Successfully provisioned Instance %v/%v of ServiceClass %v at Broker %v: response: %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName, response) - if respCode == http.StatusAccepted { - glog.V(5).Infof("Received asynchronous de-provisioning response for Instance %v/%v of ServiceClass %v at Broker %v: response: %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName, response) - if response.Operation != "" { - instance.Status.LastOperation = &response.Operation - } - - // Tag this instance as having an ongoing async operation so we can enforce - // no other operations against it can start. - instance.Status.AsyncOpInProgress = true - - c.updateInstanceCondition( - instance, - v1alpha1.InstanceConditionReady, - v1alpha1.ConditionFalse, - asyncDeprovisioningReason, - asyncDeprovisioningMessage, - ) - } else { - c.updateInstanceCondition( - instance, - v1alpha1.InstanceConditionReady, - v1alpha1.ConditionFalse, - successDeprovisionReason, - successDeprovisionMessage, - ) - // Clear the finalizer - finalizers.Delete(v1alpha1.FinalizerServiceCatalog) - c.updateInstanceFinalizers(instance, finalizers.List()) - c.recorder.Event(instance, api.EventTypeNormal, successDeprovisionReason, successDeprovisionMessage) - glog.V(5).Infof("Successfully deprovisioned Instance %v/%v of ServiceClass %v at Broker %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName) - } + // TODO: process response + c.updateInstanceCondition( + instance, + v1alpha1.InstanceConditionReady, + v1alpha1.ConditionTrue, + successProvisionReason, + successProvisionMessage, + ) + c.recorder.Eventf(instance, api.EventTypeNormal, successProvisionReason, successProvisionMessage) } - return nil } diff --git a/pkg/controller/controller_instance_test.go b/pkg/controller/controller_instance_test.go index 00a7d7dedb7..1d812375667 100644 --- a/pkg/controller/controller_instance_test.go +++ b/pkg/controller/controller_instance_test.go @@ -26,6 +26,7 @@ import ( "testing" "time" + checksum "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/checksum/versioned/v1alpha1" "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1" "github.com/kubernetes-incubator/service-catalog/pkg/brokerapi" @@ -605,6 +606,10 @@ func TestReconcileInstanceDelete(t *testing.T) { instance := getTestInstance() instance.ObjectMeta.DeletionTimestamp = &metav1.Time{} instance.ObjectMeta.Finalizers = []string{v1alpha1.FinalizerServiceCatalog} + // we only invoke the broker client to deprovision if we have a checksum set + // as that implies a previous success. + checksum := checksum.InstanceSpecChecksum(instance.Spec) + instance.Status.Checksum = &checksum fakeCatalogClient.AddReactor("get", "instances", func(action clientgotesting.Action) (bool, runtime.Object, error) { return true, instance, nil @@ -643,6 +648,52 @@ func TestReconcileInstanceDelete(t *testing.T) { } } +// TestReconcileInstanceDeleteDoesNotInvokeBroker verfies that if an instance is created that is never +// actually provisioned the instance is able to be deleted and is not blocked by any interaction with +// a broker (since its very likely that a broker never actually existed). +func TestReconcileInstanceDeleteDoesNotInvokeBroker(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + fakeBrokerClient.InstanceClient.Instances = map[string]*brokerapi.ServiceInstance{ + instanceGUID: {}, + } + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + + instance := getTestInstance() + instance.ObjectMeta.DeletionTimestamp = &metav1.Time{} + instance.ObjectMeta.Finalizers = []string{v1alpha1.FinalizerServiceCatalog} + + fakeCatalogClient.AddReactor("get", "instances", func(action clientgotesting.Action) (bool, runtime.Object, error) { + return true, instance, nil + }) + + testController.reconcileInstance(instance) + + // Verify no core kube actions occurred + kubeActions := fakeKubeClient.Actions() + assertNumberOfActions(t, kubeActions, 0) + + actions := fakeCatalogClient.Actions() + // The three actions should be: + // 0. Get against the instance + // 1. Removing the finalizer + assertNumberOfActions(t, actions, 2) + + assertGet(t, actions[0], instance) + updatedInstance := assertUpdateStatus(t, actions[1], instance) + assertEmptyFinalizers(t, updatedInstance) + + if _, ok := fakeBrokerClient.InstanceClient.Instances[instanceGUID]; !ok { + t.Fatalf("The broker should never have been invoked as service was never provisioned prior.") + } + + // no events because no external deprovision was needed + events := getRecordedEvents(testController) + assertNumEvents(t, events, 0) +} + func TestPollServiceInstanceInProgressProvisioningWithOperation(t *testing.T) { fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) diff --git a/test/e2e/framework/util.go b/test/e2e/framework/util.go index aee7cf2c0c1..ed1819bdf95 100644 --- a/test/e2e/framework/util.go +++ b/test/e2e/framework/util.go @@ -36,7 +36,7 @@ import ( const ( // How often to poll for conditions - defaultPoll = 2 * time.Second + Poll = 2 * time.Second // Default time to wait for operations to complete defaultTimeout = 30 * time.Second @@ -104,7 +104,7 @@ func CreateKubeNamespace(baseName string, c kubernetes.Interface) (*v1.Namespace Logf("namespace: %v", ns) // Be robust about making the namespace creation call. var got *v1.Namespace - err := wait.PollImmediate(defaultPoll, defaultTimeout, func() (bool, error) { + err := wait.PollImmediate(Poll, defaultTimeout, func() (bool, error) { var err error got, err = c.Core().Namespaces().Create(ns) if err != nil { @@ -140,7 +140,7 @@ func WaitForPodRunningInNamespace(c kubernetes.Interface, pod *v1.Pod) error { } func waitTimeoutForPodRunningInNamespace(c kubernetes.Interface, podName, namespace string, timeout time.Duration) error { - return wait.PollImmediate(defaultPoll, defaultTimeout, podRunning(c, podName, namespace)) + return wait.PollImmediate(Poll, defaultTimeout, podRunning(c, podName, namespace)) } func podRunning(c kubernetes.Interface, podName, namespace string) wait.ConditionFunc { diff --git a/test/e2e/instance.go b/test/e2e/instance.go new file mode 100644 index 00000000000..7a75e6e5047 --- /dev/null +++ b/test/e2e/instance.go @@ -0,0 +1,90 @@ +/* +Copyright 2017 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 e2e + +import ( + "time" + + v1alpha1 "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1" + "github.com/kubernetes-incubator/service-catalog/pkg/client/clientset_generated/clientset" + "github.com/kubernetes-incubator/service-catalog/test/e2e/framework" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +const ( + // how long to wait for an instance to be deleted. + instanceDeleteTimeout = 30 * time.Second +) + +func newTestInstance(name, serviceClassName, planName string) *v1alpha1.Instance { + return &v1alpha1.Instance{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: v1alpha1.InstanceSpec{ + PlanName: planName, + ServiceClassName: serviceClassName, + }, + } +} + +// createInstance in the specified namespace +func createInstance(c clientset.Interface, namespace string, instance *v1alpha1.Instance) (*v1alpha1.Instance, error) { + return c.ServicecatalogV1alpha1().Instances(namespace).Create(instance) +} + +// deleteInstance with the specified namespace and name +func deleteInstance(c clientset.Interface, namespace, name string) error { + return c.ServicecatalogV1alpha1().Instances(namespace).Delete(name, nil) +} + +// waitForInstanceToBeDeleted waits for the instance to be removed. +func waitForInstanceToBeDeleted(c clientset.Interface, namespace, name string) error { + return wait.Poll(framework.Poll, instanceDeleteTimeout, func() (bool, error) { + _, err := c.ServicecatalogV1alpha1().Instances(namespace).Get(name, metav1.GetOptions{}) + if err == nil { + framework.Logf("waiting for instance %s to be deleted", name) + return false, nil + } + if errors.IsNotFound(err) { + framework.Logf("verified instance %s is deleted", name) + return true, nil + } + return false, err + }) +} + +var _ = framework.ServiceCatalogDescribe("Instance", func() { + f := framework.NewDefaultFramework("instance") + + It("should verify an Instance can be deleted if referenced service class does not exist.", func() { + By("Creating an Instance") + instance := newTestInstance("test-instance", "no-service-class", "no-plan") + instance, err := createInstance(f.ServiceCatalogClientSet, f.Namespace.Name, instance) + Expect(err).NotTo(HaveOccurred()) + By("Deleting the Instance") + err = deleteInstance(f.ServiceCatalogClientSet, f.Namespace.Name, instance.Name) + Expect(err).NotTo(HaveOccurred()) + err = waitForInstanceToBeDeleted(f.ServiceCatalogClientSet, f.Namespace.Name, instance.Name) + Expect(err).NotTo(HaveOccurred()) + }) +})