Skip to content

Commit

Permalink
Implement asynchronous binding to managed services
Browse files Browse the repository at this point in the history
fixes #3293

Co-authored-by: Danail Branekov <danailster@gmail.com>
  • Loading branch information
zabanov-lab and danail-branekov committed Oct 24, 2024
1 parent 4c93f17 commit dfc0b45
Show file tree
Hide file tree
Showing 26 changed files with 920 additions and 653 deletions.
1 change: 0 additions & 1 deletion api/payloads/role_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,5 +216,4 @@ var _ = Describe("role list", func() {
},
Entry("created_at", payloads.RoleList{OrderBy: "created_at"}, repositories.ListRolesMessage{OrderBy: "created_at"}),
)

})
10 changes: 9 additions & 1 deletion controllers/api/v1alpha1/cfservicebinding_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import (
)

const (
BindingFailedCondition = "BindingFailed"
BindingFailedCondition = "BindingFailed"
BindingRequestedCondition = "BindingRequested"
)

// CFServiceBindingSpec defines the desired state of CFServiceBinding
Expand All @@ -48,6 +49,13 @@ type CFServiceBindingStatus struct {
// +optional
Binding v1.LocalObjectReference `json:"binding"`

// The
// [operation](https://github.com/openservicebrokerapi/servicebroker/blob/master/spec.md#binding)
// of the bind request to the the OSBAPI broker. Only makes sense for
// bindings to managed service instances
// +optional
BindingOperation string `json:"bindingOperation"`

// A reference to the Secret containing the binding Credentials object. For
// bindings to user-provided services this refers to the credentials secret
// from the service instance. For managed services the secret contains the
Expand Down
9 changes: 8 additions & 1 deletion controllers/api/v1alpha1/cfserviceinstance_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,14 @@ type CFServiceInstanceStatus struct {
//+kubebuilder:validation:Optional
CredentialsObservedVersion string `json:"credentialsObservedVersion,omitempty"`

ProvisionOperation string `json:"provisionOperation,omitempty"`
// The operation returned by the OSBAPI broker when instance provisioning
// is requested. Only makes sense for managed service instances
//+kubebuilder:validation:Optional
ProvisionOperation string `json:"provisionOperation,omitempty"`

// The operation returned by the OSBAPI broker when instance deprovisioning
// is requested. Only makes sense for managed service instances
//+kubebuilder:validation:Optional
DeprovisionOperation string `json:"deprovisionOperation,omitempty"`
}

Expand Down
1 change: 1 addition & 0 deletions controllers/controllers/services/bindings/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ func (r *Reconciler) ReconcileResource(ctx context.Context, cfServiceBinding *ko

res, err := r.reconcileCredentialsSecrets(ctx, cfServiceInstance.Spec.Type, cfServiceBinding)
if needsRequeue(res, err) {
log.Error(err, "failed to reconcile binding credentials")
return res, err
}

Expand Down
234 changes: 232 additions & 2 deletions controllers/controllers/services/bindings/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import (

korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1"
"code.cloudfoundry.org/korifi/controllers/controllers/services/bindings"
"code.cloudfoundry.org/korifi/controllers/controllers/services/brokers/fake"
"code.cloudfoundry.org/korifi/controllers/controllers/services/osbapi"
"code.cloudfoundry.org/korifi/controllers/controllers/services/osbapi/fake"
"code.cloudfoundry.org/korifi/model/services"
"code.cloudfoundry.org/korifi/tools"
"code.cloudfoundry.org/korifi/tools/k8s"
Expand Down Expand Up @@ -593,6 +593,7 @@ var _ = Describe("CFServiceBinding", func() {
Credentials: map[string]any{
"foo": "bar",
},
Complete: true,
}, nil)

instance = &korifiv1alpha1.CFServiceInstance{
Expand Down Expand Up @@ -640,12 +641,23 @@ var _ = Describe("CFServiceBinding", func() {
BindResource: osbapi.BindResource{
AppGUID: cfApp.Name,
},
Parameters: map[string]any{},
},
}))
}).Should(Succeed())
})

It("does not check for binding last operation", func() {
Consistently(func(g Gomega) {
g.Expect(brokerClient.GetServiceBindingLastOperationCallCount()).To(BeZero())
}).Should(Succeed())
})

It("does not get the binding", func() {
Consistently(func(g Gomega) {
g.Expect(brokerClient.GetServiceBindingCallCount()).To(BeZero())
}).Should(Succeed())
})

It("creates the credentials secret", func() {
Eventually(func(g Gomega) {
g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(binding), binding)).To(Succeed())
Expand Down Expand Up @@ -691,10 +703,228 @@ var _ = Describe("CFServiceBinding", func() {
}).Should(Succeed())
})

When("the binding credentials have been reconciled", func() {
BeforeEach(func() {
Expect(k8s.Patch(ctx, adminClient, binding, func() {
binding.Status.Credentials.Name = uuid.NewString()
binding.Status.Binding.Name = uuid.NewString()
})).To(Succeed())
})

It("does not request bind", func() {
Consistently(func(g Gomega) {
g.Expect(brokerClient.BindCallCount()).To(BeZero())
g.Expect(brokerClient.GetServiceBindingLastOperationCallCount()).To(BeZero())
g.Expect(brokerClient.GetServiceBindingCallCount()).To(BeZero())
}).Should(Succeed())
})
})

When("the binding has failed", func() {
BeforeEach(func() {
Expect(k8s.Patch(ctx, adminClient, binding, func() {
meta.SetStatusCondition(&binding.Status.Conditions, metav1.Condition{
Type: korifiv1alpha1.BindingFailedCondition,
Status: metav1.ConditionTrue,
Reason: "BindingFailed",
})
})).To(Succeed())
})

It("does not request bind", func() {
Consistently(func(g Gomega) {
g.Expect(brokerClient.BindCallCount()).To(BeZero())
g.Expect(brokerClient.GetServiceBindingLastOperationCallCount()).To(BeZero())
g.Expect(brokerClient.GetServiceBindingCallCount()).To(BeZero())
}).Should(Succeed())
})
})

When("binding is asynchronous", func() {
BeforeEach(func() {
brokerClient.BindReturns(osbapi.BindResponse{
Operation: "operation-1",
Complete: false,
}, nil)

brokerClient.GetServiceBindingLastOperationReturns(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(binding), binding)).To(Succeed())

g.Expect(binding.Status.Conditions).To(ContainElement(SatisfyAll(
HasType(Equal(korifiv1alpha1.StatusConditionReady)),
HasStatus(Equal(metav1.ConditionFalse)),
)))
}).Should(Succeed())
})

It("sets the BindRequested condition", 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.BindingRequestedCondition)),
HasStatus(Equal(metav1.ConditionTrue)),
)))
}).Should(Succeed())

Consistently(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.BindingRequestedCondition)),
HasStatus(Equal(metav1.ConditionTrue)),
)))
}).Should(Succeed())
})

It("keeps checking last operation", func() {
Eventually(func(g Gomega) {
g.Expect(brokerClient.GetServiceBindingLastOperationCallCount()).To(BeNumerically(">", 1))
_, actualLastOpPayload := brokerClient.GetServiceBindingLastOperationArgsForCall(1)
g.Expect(actualLastOpPayload).To(Equal(osbapi.GetServiceBindingLastOperationRequest{
InstanceID: instance.Name,
BindingID: binding.Name,
GetLastOperationRequestParameters: osbapi.GetLastOperationRequestParameters{
ServiceId: "service-offering-id",
PlanID: "service-plan-id",
Operation: "operation-1",
},
}))
}).Should(Succeed())
})

When("getting binding last operation fails", func() {
BeforeEach(func() {
brokerClient.GetServiceBindingLastOperationReturns(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(binding), binding)).To(Succeed())

g.Expect(binding.Status.Conditions).To(ContainElement(SatisfyAll(
HasType(Equal(korifiv1alpha1.StatusConditionReady)),
HasStatus(Equal(metav1.ConditionFalse)),
HasMessage(ContainSubstring("get-last-op-failed")),
)))
}).Should(Succeed())
})
})

When("the last operation is failed", func() {
BeforeEach(func() {
brokerClient.GetServiceBindingLastOperationReturns(osbapi.LastOperationResponse{
State: "failed",
Description: "last-operation-failed",
}, nil)
})

It("fails the binding", func() {
Eventually(func(g Gomega) {
g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(binding), binding)).To(Succeed())
g.Expect(binding.Status.Conditions).To(ContainElements(
SatisfyAll(
HasType(Equal(korifiv1alpha1.StatusConditionReady)),
HasStatus(Equal(metav1.ConditionFalse)),
),
SatisfyAll(
HasType(Equal(korifiv1alpha1.BindingFailedCondition)),
HasStatus(Equal(metav1.ConditionTrue)),
HasReason(Equal("BindingFailed")),
HasMessage(ContainSubstring("last-operation-failed")),
),
))
}).Should(Succeed())
})
})

When("last operation has succeeded", func() {
BeforeEach(func() {
brokerClient.GetServiceBindingLastOperationReturns(osbapi.LastOperationResponse{
State: "succeeded",
}, nil)

brokerClient.GetServiceBindingReturns(osbapi.GetBindingResponse{
Credentials: map[string]any{
"foo": "bar",
},
}, nil)
})

It("creates the credentials secret", func() {
Eventually(func(g Gomega) {
g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(binding), binding)).To(Succeed())
g.Expect(binding.Status.Credentials.Name).To(Equal(binding.Name))

credentialsSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: binding.Namespace,
Name: binding.Status.Credentials.Name,
},
}
g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(credentialsSecret), credentialsSecret)).To(Succeed())
g.Expect(credentialsSecret.Type).To(BeEquivalentTo("Opaque"))
g.Expect(credentialsSecret.Data).To(MatchKeys(IgnoreExtras, Keys{
tools.CredentialsSecretKey: BeEquivalentTo(`{"foo":"bar"}`),
}))
g.Expect(credentialsSecret.OwnerReferences).To(ConsistOf(MatchFields(IgnoreExtras, Fields{
"Name": Equal(binding.Name),
})))
}).Should(Succeed())
})

It("creates the servicebinding.io secret", func() {
Eventually(func(g Gomega) {
g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(binding), binding)).To(Succeed())
g.Expect(binding.Status.Binding.Name).NotTo(BeEmpty())

bindingSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: binding.Namespace,
Name: binding.Status.Binding.Name,
},
}
g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(bindingSecret), bindingSecret)).To(Succeed())
g.Expect(bindingSecret.Type).To(BeEquivalentTo("servicebinding.io/managed"))
g.Expect(bindingSecret.Data).To(MatchKeys(IgnoreExtras, Keys{
"foo": BeEquivalentTo("bar"),
}))

g.Expect(bindingSecret.OwnerReferences).To(ConsistOf(MatchFields(IgnoreExtras, Fields{
"Name": Equal(binding.Name),
})))
}).Should(Succeed())
})

When("getting the binding fails", func() {
BeforeEach(func() {
brokerClient.GetServiceBindingReturns(osbapi.GetBindingResponse{}, errors.New("get-binding-err"))
})

It("sets the ready condition to false", 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.ConditionFalse)),
HasMessage(ContainSubstring("get-binding-err")),
)))
}).Should(Succeed())
})
})
})
})

When("binding fails with the broker", func() {
BeforeEach(func() {
brokerClient.BindReturns(osbapi.BindResponse{}, errors.New("binding-failed"))
})

It("fails the binding", func() {
Eventually(func(g Gomega) {
g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(binding), binding)).To(Succeed())
Expand Down
Loading

0 comments on commit dfc0b45

Please sign in to comment.