From 596428569588c9e761b7f06ba81d39c24a538697 Mon Sep 17 00:00:00 2001 From: Danail Branekov Date: Thu, 9 Jan 2025 11:51:44 +0000 Subject: [PATCH 1/2] Ensure credentials secret is created on async bind * When the bind operation state is `succeeded`, requeue the reconcile request. Thus we ensure that the controller would try to `bind` again, and as the bind operation has already succeeded, it would get a response containing the binding credentials * When checking the state of the last operation in asynchronous provision/bind, instance/binding controllers consider the operation to have completed when its state is either `succeeded`, or `failed`. Anything else is considered as if the operation is ongoing. Thus we protect ourselves from brokers returning nonsense operation state * The test for the instance controller is refactored so that the synchronous provisioning is the "default" case and there is a dedicated context for asynchronous provisioning. This aligns test style of instance/binding controller tests --- .../services/bindings/controller_test.go | 17 +- .../services/bindings/managed/controller.go | 8 +- .../services/instances/managed/controller.go | 8 +- .../instances/managed/controller_test.go | 257 +++++++++--------- .../controllers/services/osbapi/types.go | 14 +- 5 files changed, 160 insertions(+), 144 deletions(-) diff --git a/controllers/controllers/services/bindings/controller_test.go b/controllers/controllers/services/bindings/controller_test.go index ae8772074..0144202da 100644 --- a/controllers/controllers/services/bindings/controller_test.go +++ b/controllers/controllers/services/bindings/controller_test.go @@ -798,7 +798,7 @@ var _ = Describe("CFServiceBinding", func() { }, nil) brokerClient.GetServiceBindingLastOperationReturns(osbapi.LastOperationResponse{ - State: "in progress", + State: "in-progress-or-whatever", }, nil) }) @@ -809,6 +809,7 @@ var _ = Describe("CFServiceBinding", func() { g.Expect(binding.Status.Conditions).To(ContainElement(SatisfyAll( HasType(Equal(korifiv1alpha1.StatusConditionReady)), HasStatus(Equal(metav1.ConditionFalse)), + HasReason(Equal("BindingInProgress")), ))) }).Should(Succeed()) }) @@ -841,6 +842,7 @@ var _ = Describe("CFServiceBinding", func() { g.Expect(binding.Status.Conditions).To(ContainElement(SatisfyAll( HasType(Equal(korifiv1alpha1.StatusConditionReady)), HasStatus(Equal(metav1.ConditionFalse)), + HasReason(Equal("GetLastOperationFailed")), HasMessage(ContainSubstring("get-last-op-failed")), ))) }).Should(Succeed()) @@ -880,13 +882,24 @@ var _ = Describe("CFServiceBinding", func() { State: "succeeded", }, nil) - brokerClient.BindReturns(osbapi.BindResponse{ + brokerClient.BindReturnsOnCall(3, osbapi.BindResponse{ Credentials: map[string]any{ "foo": "bar", }, }, nil) }) + It("sets the ready condition to true", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(binding), binding)).To(Succeed()) + + g.Expect(binding.Status.Conditions).To(ContainElement(SatisfyAll( + HasType(Equal(korifiv1alpha1.StatusConditionReady)), + HasStatus(Equal(metav1.ConditionTrue)), + ))) + }).Should(Succeed()) + }) + It("creates the credentials secret", func() { Eventually(func(g Gomega) { g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(binding), binding)).To(Succeed()) diff --git a/controllers/controllers/services/bindings/managed/controller.go b/controllers/controllers/services/bindings/managed/controller.go index 1300dec35..f77354ae3 100644 --- a/controllers/controllers/services/bindings/managed/controller.go +++ b/controllers/controllers/services/bindings/managed/controller.go @@ -147,8 +147,8 @@ func (r *ManagedBindingsReconciler) processBindOperation( cfServiceBinding *korifiv1alpha1.CFServiceBinding, lastOperation osbapi.LastOperationResponse, ) (ctrl.Result, error) { - if lastOperation.State == "in progress" { - return ctrl.Result{}, k8s.NewNotReadyError().WithReason("BindingInProgress").WithRequeue() + if lastOperation.State == "succeeded" { + return ctrl.Result{Requeue: true}, nil } if lastOperation.State == "failed" { @@ -160,10 +160,10 @@ func (r *ManagedBindingsReconciler) processBindOperation( Reason: "BindingFailed", Message: lastOperation.Description, }) - return ctrl.Result{}, k8s.NewNotReadyError().WithReason("BindingFailed") + return ctrl.Result{}, k8s.NewNotReadyError().WithReason("BindingFailed").WithMessage(lastOperation.Description) } - return ctrl.Result{}, nil + return ctrl.Result{}, k8s.NewNotReadyError().WithReason("BindingInProgress").WithRequeue() } func (r *ManagedBindingsReconciler) reconcileCredentials(ctx context.Context, cfServiceBinding *korifiv1alpha1.CFServiceBinding, creds map[string]any) error { diff --git a/controllers/controllers/services/instances/managed/controller.go b/controllers/controllers/services/instances/managed/controller.go index a97860cc7..671f5dbd4 100644 --- a/controllers/controllers/services/instances/managed/controller.go +++ b/controllers/controllers/services/instances/managed/controller.go @@ -247,8 +247,8 @@ func (r *Reconciler) processProvisionOperation( serviceInstance *korifiv1alpha1.CFServiceInstance, lastOpResponse osbapi.LastOperationResponse, ) (ctrl.Result, error) { - if lastOpResponse.State == "in progress" { - return ctrl.Result{}, k8s.NewNotReadyError().WithReason("ProvisionInProgress").WithRequeue() + if lastOpResponse.State == "succeeded" { + return ctrl.Result{}, nil } if lastOpResponse.State == "failed" { @@ -263,7 +263,7 @@ func (r *Reconciler) processProvisionOperation( return ctrl.Result{}, k8s.NewNotReadyError().WithReason("ProvisionFailed") } - return ctrl.Result{}, nil + return ctrl.Result{}, k8s.NewNotReadyError().WithReason("ProvisionInProgress").WithRequeue() } func (r *Reconciler) finalizeCFServiceInstance( @@ -381,7 +381,7 @@ func (r *Reconciler) pollLastOperation( return osbapi.LastOperationResponse{}, k8s.NewNotReadyError().WithCause(err).WithReason("GetLastOperationFailed") } - serviceInstance.Status.LastOperation.State = lastOpResponse.State + serviceInstance.Status.LastOperation.State = lastOpResponse.State.Value() serviceInstance.Status.LastOperation.Description = lastOpResponse.Description return lastOpResponse, nil } diff --git a/controllers/controllers/services/instances/managed/controller_test.go b/controllers/controllers/services/instances/managed/controller_test.go index 9f71d1676..ce92d8e0b 100644 --- a/controllers/controllers/services/instances/managed/controller_test.go +++ b/controllers/controllers/services/instances/managed/controller_test.go @@ -38,14 +38,7 @@ var _ = Describe("CFServiceInstance", func() { brokerClient = new(fake.BrokerClient) brokerClientFactory.CreateClientReturns(brokerClient, nil) - brokerClient.ProvisionReturns(osbapi.ProvisionResponse{ - IsAsync: true, - Operation: "operation-1", - }, nil) - - brokerClient.GetServiceInstanceLastOperationReturns(osbapi.LastOperationResponse{ - State: "succeeded", - }, nil) + brokerClient.ProvisionReturns(osbapi.ProvisionResponse{}, nil) serviceBroker = &korifiv1alpha1.CFServiceBroker{ ObjectMeta: metav1.ObjectMeta{ @@ -181,6 +174,12 @@ var _ = Describe("CFServiceInstance", func() { }) }) + It("does not check last operation", func() { + Consistently(func(g Gomega) { + g.Expect(brokerClient.GetServiceInstanceLastOperationCallCount()).To(Equal(0)) + }).Should(Succeed()) + }) + It("provisions the service", func() { Eventually(func(g Gomega) { g.Expect(brokerClient.ProvisionCallCount()).NotTo(BeZero()) @@ -197,18 +196,6 @@ var _ = Describe("CFServiceInstance", func() { }, }, })) - - g.Expect(brokerClient.GetServiceInstanceLastOperationCallCount()).To(BeNumerically(">", 0)) - _, lastOp := brokerClient.GetServiceInstanceLastOperationArgsForCall(brokerClient.GetServiceInstanceLastOperationCallCount() - 1) - g.Expect(lastOp).To(Equal(osbapi.GetInstanceLastOperationRequest{ - InstanceID: instance.Name, - GetLastOperationRequestParameters: osbapi.GetLastOperationRequestParameters{ - ServiceId: "service-offering-id", - PlanID: "service-plan-id", - Operation: "operation-1", - }, - })) - g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(instance), instance)).To(Succeed()) }).Should(Succeed()) }) @@ -296,27 +283,136 @@ var _ = Describe("CFServiceInstance", func() { }) }) - When("the provisioning is synchronous", func() { + When("the provisioning is asynchronous", func() { BeforeEach(func() { - brokerClient.ProvisionReturns(osbapi.ProvisionResponse{}, nil) - }) + brokerClient.GetServiceInstanceLastOperationReturns(osbapi.LastOperationResponse{ + State: "in-progress-or-whatever", + }, nil) - It("does not check last operation", func() { - Consistently(func(g Gomega) { - g.Expect(brokerClient.GetServiceInstanceLastOperationCallCount()).To(Equal(0)) - }).Should(Succeed()) + brokerClient.ProvisionReturns(osbapi.ProvisionResponse{ + IsAsync: true, + Operation: "operation-1", + }, nil) }) - It("set sets ready condition to true", func() { + It("set sets ready condition to false", func() { Eventually(func(g Gomega) { g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(instance), instance)).To(Succeed()) g.Expect(instance.Status.Conditions).To(ContainElement(SatisfyAll( HasType(Equal(korifiv1alpha1.StatusConditionReady)), - HasStatus(Equal(metav1.ConditionTrue)), + HasStatus(Equal(metav1.ConditionFalse)), + HasReason(Equal("ProvisionInProgress")), ))) }).Should(Succeed()) }) + + It("sets in progress state in instance last operation", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(instance), instance)).To(Succeed()) + g.Expect(brokerClient.ProvisionCallCount()).To(BeNumerically(">=", 1)) + g.Expect(instance.Status.LastOperation).To(Equal(services.LastOperation{ + Type: "create", + State: "in progress", + })) + }).Should(Succeed()) + }) + + It("continuously checks the last operation", func() { + Eventually(func(g Gomega) { + g.Expect(brokerClient.GetServiceInstanceLastOperationCallCount()).To(BeNumerically(">", 1)) + _, lastOp := brokerClient.GetServiceInstanceLastOperationArgsForCall(brokerClient.GetServiceInstanceLastOperationCallCount() - 1) + g.Expect(lastOp).To(Equal(osbapi.GetInstanceLastOperationRequest{ + InstanceID: instance.Name, + GetLastOperationRequestParameters: osbapi.GetLastOperationRequestParameters{ + ServiceId: "service-offering-id", + PlanID: "service-plan-id", + Operation: "operation-1", + }, + })) + }).Should(Succeed()) + }) + + When("getting service last operation fails", func() { + BeforeEach(func() { + brokerClient.GetServiceInstanceLastOperationReturns(osbapi.LastOperationResponse{}, errors.New("get-last-op-failed")) + }) + + It("sets the ready condition to false", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(instance), instance)).To(Succeed()) + + g.Expect(instance.Status.Conditions).To(ContainElement(SatisfyAll( + HasType(Equal(korifiv1alpha1.StatusConditionReady)), + HasStatus(Equal(metav1.ConditionFalse)), + ))) + }).Should(Succeed()) + }) + }) + + When("the last operation is succeeded", func() { + BeforeEach(func() { + brokerClient.GetServiceInstanceLastOperationReturns(osbapi.LastOperationResponse{ + State: "succeeded", + }, nil) + }) + + It("sets the ready condition to true", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(instance), instance)).To(Succeed()) + + g.Expect(instance.Status.Conditions).To(ContainElement(SatisfyAll( + HasType(Equal(korifiv1alpha1.StatusConditionReady)), + HasStatus(Equal(metav1.ConditionTrue)), + ))) + }).Should(Succeed()) + }) + }) + + When("the last operation is failed", func() { + BeforeEach(func() { + brokerClient.GetServiceInstanceLastOperationReturns(osbapi.LastOperationResponse{ + State: "failed", + Description: "provision-failed", + }, nil) + }) + + It("sets the ready condition to false", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(instance), instance)).To(Succeed()) + + g.Expect(instance.Status.Conditions).To(ContainElement(SatisfyAll( + HasType(Equal(korifiv1alpha1.StatusConditionReady)), + HasStatus(Equal(metav1.ConditionFalse)), + ))) + }).Should(Succeed()) + }) + + It("sets the failed condition", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(instance), instance)).To(Succeed()) + + g.Expect(instance.Status.Conditions).To(ContainElement(SatisfyAll( + HasType(Equal(korifiv1alpha1.ProvisioningFailedCondition)), + HasStatus(Equal(metav1.ConditionTrue)), + HasReason(Equal("ProvisionFailed")), + HasMessage(Equal("provision-failed")), + ))) + }).Should(Succeed()) + }) + + It("sets failed state in instance last operation", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(instance), instance)).To(Succeed()) + g.Expect(brokerClient.ProvisionCallCount()).To(BeNumerically(">=", 1)) + g.Expect(instance.Status.LastOperation).To(Equal(services.LastOperation{ + Type: "create", + State: "failed", + Description: "provision-failed", + })) + }).Should(Succeed()) + }) + }) }) When("service provisioning fails with recoverable error", func() { @@ -391,109 +487,6 @@ var _ = Describe("CFServiceInstance", func() { }) }) - When("getting service last operation fails", func() { - BeforeEach(func() { - brokerClient.GetServiceInstanceLastOperationReturns(osbapi.LastOperationResponse{}, errors.New("get-last-op-failed")) - }) - - It("sets the ready condition to false", func() { - Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(instance), instance)).To(Succeed()) - - g.Expect(instance.Status.Conditions).To(ContainElement(SatisfyAll( - HasType(Equal(korifiv1alpha1.StatusConditionReady)), - HasStatus(Equal(metav1.ConditionFalse)), - ))) - }).Should(Succeed()) - }) - }) - - When("the last operation is in progress", func() { - BeforeEach(func() { - brokerClient.GetServiceInstanceLastOperationReturns(osbapi.LastOperationResponse{ - State: "in progress", - }, nil) - }) - - It("sets the ready condition to false", func() { - Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(instance), instance)).To(Succeed()) - - g.Expect(instance.Status.Conditions).To(ContainElement(SatisfyAll( - HasType(Equal(korifiv1alpha1.StatusConditionReady)), - HasStatus(Equal(metav1.ConditionFalse)), - ))) - }).Should(Succeed()) - }) - - It("sets in progress state in instance last operation", func() { - Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(instance), instance)).To(Succeed()) - g.Expect(brokerClient.ProvisionCallCount()).To(BeNumerically(">=", 1)) - g.Expect(instance.Status.LastOperation).To(Equal(services.LastOperation{ - Type: "create", - State: "in progress", - })) - }).Should(Succeed()) - }) - - It("keeps checking last operation", func() { - Eventually(func(g Gomega) { - g.Expect(brokerClient.GetServiceInstanceLastOperationCallCount()).To(BeNumerically(">", 1)) - _, actualLastOpPayload := brokerClient.GetServiceInstanceLastOperationArgsForCall(1) - g.Expect(actualLastOpPayload).To(Equal(osbapi.GetInstanceLastOperationRequest{ - InstanceID: instance.Name, - GetLastOperationRequestParameters: osbapi.GetLastOperationRequestParameters{ - ServiceId: "service-offering-id", - PlanID: "service-plan-id", - Operation: "operation-1", - }, - })) - }).Should(Succeed()) - }) - }) - - When("the last operation is failed", func() { - BeforeEach(func() { - brokerClient.GetServiceInstanceLastOperationReturns(osbapi.LastOperationResponse{ - State: "failed", - }, nil) - }) - - It("sets the ready condition to false", func() { - Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(instance), instance)).To(Succeed()) - - g.Expect(instance.Status.Conditions).To(ContainElement(SatisfyAll( - HasType(Equal(korifiv1alpha1.StatusConditionReady)), - HasStatus(Equal(metav1.ConditionFalse)), - ))) - }).Should(Succeed()) - }) - - It("sets the failed condition", func() { - Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(instance), instance)).To(Succeed()) - - g.Expect(instance.Status.Conditions).To(ContainElement(SatisfyAll( - HasType(Equal(korifiv1alpha1.ProvisioningFailedCondition)), - HasStatus(Equal(metav1.ConditionTrue)), - ))) - }).Should(Succeed()) - }) - - It("sets failed state in instance last operation", func() { - Eventually(func(g Gomega) { - g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(instance), instance)).To(Succeed()) - g.Expect(brokerClient.ProvisionCallCount()).To(BeNumerically(">=", 1)) - g.Expect(instance.Status.LastOperation).To(Equal(services.LastOperation{ - Type: "create", - State: "failed", - })) - }).Should(Succeed()) - }) - }) - When("the instance has become ready", func() { BeforeEach(func() { Expect(k8s.Patch(ctx, adminClient, instance, func() { diff --git a/controllers/controllers/services/osbapi/types.go b/controllers/controllers/services/osbapi/types.go index e3cae1343..908652238 100644 --- a/controllers/controllers/services/osbapi/types.go +++ b/controllers/controllers/services/osbapi/types.go @@ -142,6 +142,16 @@ func (r UnbindResponse) IsComplete() bool { } type LastOperationResponse struct { - State string `json:"state"` - Description string `json:"description"` + State LastOperationResponseState `json:"state"` + Description string `json:"description"` +} + +type LastOperationResponseState string + +func (s LastOperationResponseState) Value() string { + if s == "succeeded" || s == "failed" { + return string(s) + } + + return "in progress" } From 651ff429c9d2a0dc8328bc3b24688d6fb316d42c Mon Sep 17 00:00:00 2001 From: Danail Branekov Date: Thu, 9 Jan 2025 14:39:37 +0000 Subject: [PATCH 2/2] Sample broker implements async instances/bindings flows properly * `Provision`/`Deprovision`/`Bind`/`Unbind` operations - return `HTTP 202 Accepted` on first invocation for a resource and return operation id - store the operation in a map and flags it as `in-progress` * `GetLastOperation` - returns `in-progress` state upon first invocation for given operation and flags the operation as `done` in the map above - on subsequent invocation, `succeeded` state is returned * Once the last operation succeeds (and is flagged as succeeded in the map), `Provision`/`Deprovision`/`Bind`/`Unbind` return `HTTP 200 OK` * Misc: - last operation handler is common for instances and bindings - helper functions to implement async responses in a common manner - improved logging --- tests/assets/sample-broker-golang/main.go | 135 +++++++++++++++------- 1 file changed, 91 insertions(+), 44 deletions(-) diff --git a/tests/assets/sample-broker-golang/main.go b/tests/assets/sample-broker-golang/main.go index 10b0aad0d..822be41fa 100644 --- a/tests/assets/sample-broker-golang/main.go +++ b/tests/assets/sample-broker-golang/main.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "strings" + "sync" "sample-broker/osbapi" ) @@ -17,33 +18,39 @@ const ( hardcodedPassword = "broker-password" ) +var inProgressOperations sync.Map + func main() { http.HandleFunc("GET /", helloWorldHandler) http.HandleFunc("GET /v2/catalog", getCatalogHandler) + http.HandleFunc("PUT /v2/service_instances/{id}", provisionServiceInstanceHandler) http.HandleFunc("DELETE /v2/service_instances/{id}", deprovisionServiceInstanceHandler) - http.HandleFunc("GET /v2/service_instances/{id}/last_operation", serviceInstanceLastOperationHandler) + http.HandleFunc("GET /v2/service_instances/{id}/last_operation", getLastOperationHandler) + http.HandleFunc("PUT /v2/service_instances/{instance_id}/service_bindings/{binding_id}", bindHandler) - http.HandleFunc("GET /v2/service_instances/{instance_id}/service_bindings/{binding_id}/last_operation", serviceBindingLastOperationHandler) - http.HandleFunc("GET /v2/service_instances/{instance_id}/service_bindings/{binding_id}", getServiceBindingHandler) http.HandleFunc("DELETE /v2/service_instances/{instance_id}/service_bindings/{binding_id}", unbindHandler) + http.HandleFunc("GET /v2/service_instances/{instance_id}/service_bindings/{binding_id}/last_operation", getLastOperationHandler) port := os.Getenv("PORT") if port == "" { port = "8080" } - fmt.Printf("Listening on port %s\n", port) + log(fmt.Sprintf("Listening on port %s", port)) http.ListenAndServe(fmt.Sprintf(":%s", port), nil) } -func helloWorldHandler(w http.ResponseWriter, _ *http.Request) { - fmt.Fprintln(w, "Hi, I'm the sample broker!") +func helloWorldHandler(w http.ResponseWriter, r *http.Request) { + logRequest(r) + + respond(w, http.StatusOK, "Hi, I'm the sample broker!") } func getCatalogHandler(w http.ResponseWriter, r *http.Request) { + logRequest(r) + if status, err := checkCredentials(w, r); err != nil { - w.WriteHeader(status) - fmt.Fprintf(w, "Credentials check failed: %v", err) + respond(w, status, fmt.Sprintf("Credentials check failed: %v", err)) return } @@ -64,88 +71,91 @@ func getCatalogHandler(w http.ResponseWriter, r *http.Request) { catalogBytes, err := json.Marshal(catalog) if err != nil { - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, "Failed to marshal catalog: %v", err) + log(fmt.Sprintf("failed to marshal catalog: %v", err)) + respond(w, http.StatusInternalServerError, fmt.Sprintf("failed to marshal catalog: %v", err)) return } - fmt.Fprintln(w, string(catalogBytes)) + respond(w, http.StatusOK, string(catalogBytes)) } func provisionServiceInstanceHandler(w http.ResponseWriter, r *http.Request) { + logRequest(r) + if status, err := checkCredentials(w, r); err != nil { - w.WriteHeader(status) - fmt.Fprintf(w, "Credentials check failed: %v", err) + respond(w, status, fmt.Sprintf("Credentials check failed: %v", err)) return } - fmt.Fprintf(w, `{"operation":"provision-%s"}`, r.PathValue("id")) + asyncOperation(w, fmt.Sprintf("provision-%s", r.PathValue("id")), "{}") } func deprovisionServiceInstanceHandler(w http.ResponseWriter, r *http.Request) { - if status, err := checkCredentials(w, r); err != nil { - w.WriteHeader(status) - fmt.Fprintf(w, "Credentials check failed: %v", err) - return - } + logRequest(r) - fmt.Fprintf(w, `{"operation":"deprovision-%s"}`, r.PathValue("id")) -} - -func serviceInstanceLastOperationHandler(w http.ResponseWriter, r *http.Request) { if status, err := checkCredentials(w, r); err != nil { w.WriteHeader(status) - fmt.Fprintf(w, "Credentials check failed: %v", err) + fmt.Fprintf(w, "Credentials check failed: %v\n", err) return } - fmt.Fprint(w, `{"state":"succeeded"}`) + asyncOperation(w, fmt.Sprintf("deprovision-%s", r.PathValue("id")), "{}") } func bindHandler(w http.ResponseWriter, r *http.Request) { + logRequest(r) + if status, err := checkCredentials(w, r); err != nil { w.WriteHeader(status) - fmt.Fprintf(w, "Credentials check failed: %v", err) + fmt.Fprintf(w, "Credentials check failed: %v\n", err) return } - w.WriteHeader(http.StatusAccepted) - fmt.Fprint(w, `{ - "operation":"bind-operation" + asyncOperation(w, fmt.Sprintf("bind-%s-%s", r.PathValue("instance_id"), r.PathValue("binding_id")), `{ + "credentials": { + "username": "binding-user", + "password": "binding-password" + } }`) } -func serviceBindingLastOperationHandler(w http.ResponseWriter, r *http.Request) { +func unbindHandler(w http.ResponseWriter, r *http.Request) { + logRequest(r) + if status, err := checkCredentials(w, r); err != nil { w.WriteHeader(status) - fmt.Fprintf(w, "Credentials check failed: %v", err) + fmt.Fprintf(w, "Credentials check failed: %v\n", err) return } - fmt.Fprint(w, `{"state":"succeeded"}`) + asyncOperation(w, fmt.Sprintf("unbind-%s-%s", r.PathValue("instance_id"), r.PathValue("binding_id")), "{}") } -func getServiceBindingHandler(w http.ResponseWriter, r *http.Request) { +func getLastOperationHandler(w http.ResponseWriter, r *http.Request) { + logRequest(r) + if status, err := checkCredentials(w, r); err != nil { w.WriteHeader(status) - fmt.Fprintf(w, "Credentials check failed: %v", err) + fmt.Fprintf(w, "Credentials check failed: %v\n", err) return } - fmt.Fprint(w, `{"credentials":{ - "user":"my-user", - "password":"my-password" - }}`) -} + operation, err := getOperation(r) + if err != nil { + log(fmt.Sprintf("failed to get operation: %v\n", err)) + respond(w, http.StatusInternalServerError, fmt.Sprintf("failed to get operation: %v\n", err)) + return + } -func unbindHandler(w http.ResponseWriter, r *http.Request) { - if status, err := checkCredentials(w, r); err != nil { - w.WriteHeader(status) - fmt.Fprintf(w, "Credentials check failed: %v", err) + isDone := inProgressOperations.CompareAndSwap(operation, true, false) + if isDone { + log(fmt.Sprintf("operation %q succeeds", operation)) + respond(w, http.StatusOK, `{"state":"succeeded"}`) return } - fmt.Fprintf(w, `{"operation":"unbind-%s"}`, r.PathValue("binding_id")) + log(fmt.Sprintf("operation %q is in progress", operation)) + respond(w, http.StatusOK, `{"state":"in progress"}`) } func checkCredentials(_ http.ResponseWriter, r *http.Request) (int, error) { @@ -182,3 +192,40 @@ func checkCredentials(_ http.ResponseWriter, r *http.Request) (int, error) { return -1, nil } + +func asyncOperation(w http.ResponseWriter, operationName string, asyncResultBody string) { + inProgress, _ := inProgressOperations.LoadOrStore(operationName, true) + if inProgress.(bool) { + log(fmt.Sprintf("operation %q in progress", operationName)) + respond(w, http.StatusAccepted, fmt.Sprintf(`{ + "operation":"%s" + }`, operationName)) + return + } + + inProgressOperations.Delete(operationName) + log(fmt.Sprintf("operation %q is done", operationName)) + respond(w, http.StatusOK, asyncResultBody) +} + +func getOperation(r *http.Request) (string, error) { + operation := r.URL.Query().Get("operation") + if operation == "" { + return "", fmt.Errorf("last operation request %q body does not contain operation query parameter", r.URL) + } + + return operation, nil +} + +func logRequest(r *http.Request) { + log(fmt.Sprintf("%s %v", r.Method, r.URL)) +} + +func log(s string) { + fmt.Printf("%s\n", s) +} + +func respond(w http.ResponseWriter, statusCode int, responseContent string) { + w.WriteHeader(statusCode) + fmt.Fprintf(w, "%s\n", responseContent) +}