Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support managed binding parameters #3698

Merged
merged 1 commit into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/payloads/service_binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type ServiceBindingCreate struct {
Relationships *ServiceBindingRelationships `json:"relationships"`
Type string `json:"type"`
Name *string `json:"name"`
Parameters map[string]any `json:"parameters"`
}

func (p ServiceBindingCreate) ToMessage(spaceGUID string) repositories.CreateServiceBindingMessage {
Expand All @@ -21,6 +22,7 @@ func (p ServiceBindingCreate) ToMessage(spaceGUID string) repositories.CreateSer
ServiceInstanceGUID: p.Relationships.ServiceInstance.Data.GUID,
AppGUID: p.Relationships.App.Data.GUID,
SpaceGUID: spaceGUID,
Parameters: p.Parameters,
}
}

Expand Down
147 changes: 89 additions & 58 deletions api/payloads/service_binding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import (
"code.cloudfoundry.org/korifi/api/payloads"
"code.cloudfoundry.org/korifi/api/repositories"
"code.cloudfoundry.org/korifi/tools"
"github.com/google/uuid"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gstruct"
. "github.com/onsi/gomega/gstruct"
)

var _ = Describe("ServiceBindingList", func() {
Expand Down Expand Up @@ -57,16 +58,11 @@ var _ = Describe("ServiceBindingList", func() {
})

var _ = Describe("ServiceBindingCreate", func() {
var (
createPayload payloads.ServiceBindingCreate
serviceBindingCreate *payloads.ServiceBindingCreate
validatorErr error
apiError errors.ApiError
)
var createPayload payloads.ServiceBindingCreate

BeforeEach(func() {
serviceBindingCreate = new(payloads.ServiceBindingCreate)
createPayload = payloads.ServiceBindingCreate{
Name: tools.PtrTo(uuid.NewString()),
Relationships: &payloads.ServiceBindingRelationships{
App: &payloads.Relationship{
Data: &payloads.RelationshipData{
Expand All @@ -80,82 +76,117 @@ var _ = Describe("ServiceBindingCreate", func() {
},
},
Type: "app",
Parameters: map[string]any{
"p1": "p1-value",
},
}
})

JustBeforeEach(func() {
validatorErr = validator.DecodeAndValidateJSONPayload(createJSONRequest(createPayload), serviceBindingCreate)
apiError, _ = validatorErr.(errors.ApiError)
})

It("succeeds", func() {
Expect(validatorErr).NotTo(HaveOccurred())
Expect(serviceBindingCreate).To(gstruct.PointTo(Equal(createPayload)))
})
Describe("Validation", func() {
var (
serviceBindingCreate *payloads.ServiceBindingCreate
validatorErr error
apiError errors.ApiError
)

When(`the type is "key"`, func() {
BeforeEach(func() {
createPayload.Type = "key"
serviceBindingCreate = new(payloads.ServiceBindingCreate)
})

It("fails", func() {
Expect(apiError).To(HaveOccurred())
Expect(apiError.Detail()).To(ContainSubstring("type value must be one of: app"))
JustBeforeEach(func() {
validatorErr = validator.DecodeAndValidateJSONPayload(createJSONRequest(createPayload), serviceBindingCreate)
apiError, _ = validatorErr.(errors.ApiError)
})
})

When("all relationships are missing", func() {
BeforeEach(func() {
createPayload.Relationships = nil
It("succeeds", func() {
Expect(validatorErr).NotTo(HaveOccurred())
Expect(serviceBindingCreate).To(PointTo(Equal(createPayload)))
})

It("fails", func() {
Expect(apiError).To(HaveOccurred())
Expect(apiError.Detail()).To(ContainSubstring("relationships is required"))
})
})
When(`the type is "key"`, func() {
BeforeEach(func() {
createPayload.Type = "key"
})

When("app relationship is missing", func() {
BeforeEach(func() {
createPayload.Relationships.App = nil
It("fails", func() {
Expect(apiError).To(HaveOccurred())
Expect(apiError.Detail()).To(ContainSubstring("type value must be one of: app"))
})
})

It("fails", func() {
Expect(apiError).To(HaveOccurred())
Expect(apiError.Detail()).To(ContainSubstring("relationships.app is required"))
When("all relationships are missing", func() {
BeforeEach(func() {
createPayload.Relationships = nil
})

It("fails", func() {
Expect(apiError).To(HaveOccurred())
Expect(apiError.Detail()).To(ContainSubstring("relationships is required"))
})
})
})

When("the app GUID is blank", func() {
BeforeEach(func() {
createPayload.Relationships.App.Data.GUID = ""
When("app relationship is missing", func() {
BeforeEach(func() {
createPayload.Relationships.App = nil
})

It("fails", func() {
Expect(apiError).To(HaveOccurred())
Expect(apiError.Detail()).To(ContainSubstring("relationships.app is required"))
})
})

It("fails", func() {
Expect(apiError).To(HaveOccurred())
Expect(apiError.Detail()).To(ContainSubstring("app.data.guid cannot be blank"))
When("the app GUID is blank", func() {
BeforeEach(func() {
createPayload.Relationships.App.Data.GUID = ""
})

It("fails", func() {
Expect(apiError).To(HaveOccurred())
Expect(apiError.Detail()).To(ContainSubstring("app.data.guid cannot be blank"))
})
})
})

When("service instance relationship is missing", func() {
BeforeEach(func() {
createPayload.Relationships.ServiceInstance = nil
When("service instance relationship is missing", func() {
BeforeEach(func() {
createPayload.Relationships.ServiceInstance = nil
})

It("fails", func() {
Expect(apiError).To(HaveOccurred())
Expect(apiError.Detail()).To(ContainSubstring("relationships.service_instance is required"))
})
})

It("fails", func() {
Expect(apiError).To(HaveOccurred())
Expect(apiError.Detail()).To(ContainSubstring("relationships.service_instance is required"))
When("the service instance GUID is blank", func() {
BeforeEach(func() {
createPayload.Relationships.ServiceInstance.Data.GUID = ""
})

It("fails", func() {
Expect(apiError).To(HaveOccurred())
Expect(apiError.Detail()).To(ContainSubstring("relationships.service_instance.data.guid cannot be blank"))
})
})
})

When("the service instance GUID is blank", func() {
BeforeEach(func() {
createPayload.Relationships.ServiceInstance.Data.GUID = ""
Describe("ToMessage", func() {
var createMessage repositories.CreateServiceBindingMessage

JustBeforeEach(func() {
createMessage = createPayload.ToMessage("space-guid")
})

It("fails", func() {
Expect(apiError).To(HaveOccurred())
Expect(apiError.Detail()).To(ContainSubstring("relationships.service_instance.data.guid cannot be blank"))
It("creates the message", func() {
Expect(createMessage).To(Equal(repositories.CreateServiceBindingMessage{
Name: tools.PtrTo(*createPayload.Name),
ServiceInstanceGUID: createPayload.Relationships.ServiceInstance.Data.GUID,
AppGUID: createPayload.Relationships.App.Data.GUID,
SpaceGUID: "space-guid",
Parameters: map[string]any{
"p1": "p1-value",
},
}))
})
})
})
Expand Down Expand Up @@ -185,7 +216,7 @@ var _ = Describe("ServiceBindingUpdate", func() {

It("succeeds", func() {
Expect(validatorErr).NotTo(HaveOccurred())
Expect(serviceBindingPatch).To(gstruct.PointTo(Equal(patchPayload)))
Expect(serviceBindingPatch).To(PointTo(Equal(patchPayload)))
})

When("metadata uses the cloudfoundry domain", func() {
Expand Down
56 changes: 48 additions & 8 deletions api/repositories/service_binding_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/scheme"

"code.cloudfoundry.org/korifi/api/authorization"
apierrors "code.cloudfoundry.org/korifi/api/errors"
Expand All @@ -25,6 +26,7 @@ import (
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

const (
Expand Down Expand Up @@ -87,6 +89,7 @@ type CreateServiceBindingMessage struct {
ServiceInstanceGUID string
AppGUID string
SpaceGUID string
Parameters map[string]any
}

type DeleteServiceBindingMessage struct {
Expand All @@ -106,8 +109,8 @@ func (m *ListServiceBindingsMessage) matches(serviceBinding korifiv1alpha1.CFSer
tools.EmptyOrContains(m.PlanGUIDs, serviceBinding.Labels[korifiv1alpha1.PlanGUIDLabelKey])
}

func (m CreateServiceBindingMessage) toCFServiceBinding() *korifiv1alpha1.CFServiceBinding {
return &korifiv1alpha1.CFServiceBinding{
func (m CreateServiceBindingMessage) toCFServiceBinding(instanceType korifiv1alpha1.InstanceType) *korifiv1alpha1.CFServiceBinding {
binding := &korifiv1alpha1.CFServiceBinding{
ObjectMeta: metav1.ObjectMeta{
Name: uuid.NewString(),
Namespace: m.SpaceGUID,
Expand All @@ -123,6 +126,12 @@ func (m CreateServiceBindingMessage) toCFServiceBinding() *korifiv1alpha1.CFServ
AppRef: corev1.LocalObjectReference{Name: m.AppGUID},
},
}

if instanceType == korifiv1alpha1.ManagedType {
binding.Spec.Parameters.Name = uuid.NewString()
}

return binding
}

type UpdateServiceBindingMessage struct {
Expand All @@ -136,10 +145,20 @@ func (r *ServiceBindingRepo) CreateServiceBinding(ctx context.Context, authInfo
return ServiceBindingRecord{}, fmt.Errorf("failed to build user client: %w", err)
}

cfServiceBinding := message.toCFServiceBinding()
cfServiceInstance := new(korifiv1alpha1.CFServiceInstance)
err = userClient.Get(ctx, types.NamespacedName{Name: message.ServiceInstanceGUID, Namespace: message.SpaceGUID}, cfServiceInstance)
if err != nil {
return ServiceBindingRecord{},
apierrors.AsUnprocessableEntity(
apierrors.FromK8sError(err, ServiceBindingResourceType),
"Unable to bind to instance. Ensure that the instance exists and you have access to it.",
apierrors.ForbiddenError{},
apierrors.NotFoundError{},
)
}

cfApp := new(korifiv1alpha1.CFApp)
err = userClient.Get(ctx, types.NamespacedName{Name: cfServiceBinding.Spec.AppRef.Name, Namespace: cfServiceBinding.Namespace}, cfApp)
err = userClient.Get(ctx, types.NamespacedName{Name: message.AppGUID, Namespace: message.SpaceGUID}, cfApp)
if err != nil {
return ServiceBindingRecord{},
apierrors.AsUnprocessableEntity(
Expand All @@ -150,6 +169,7 @@ func (r *ServiceBindingRepo) CreateServiceBinding(ctx context.Context, authInfo
)
}

cfServiceBinding := message.toCFServiceBinding(cfServiceInstance.Spec.Type)
err = userClient.Create(ctx, cfServiceBinding)
if err != nil {
if validationError, ok := validation.WebhookErrorToValidationError(err); ok {
Expand All @@ -161,10 +181,11 @@ func (r *ServiceBindingRepo) CreateServiceBinding(ctx context.Context, authInfo
return ServiceBindingRecord{}, apierrors.FromK8sError(err, ServiceBindingResourceType)
}

cfServiceInstance := new(korifiv1alpha1.CFServiceInstance)
err = userClient.Get(ctx, types.NamespacedName{Name: cfServiceBinding.Spec.Service.Name, Namespace: cfServiceBinding.Namespace}, cfServiceInstance)
if err != nil {
return ServiceBindingRecord{}, fmt.Errorf("failed to get service instance: %w", err)
if cfServiceInstance.Spec.Type == korifiv1alpha1.ManagedType {
err = r.createParametersSecret(ctx, userClient, cfServiceBinding, message.Parameters)
if err != nil {
return ServiceBindingRecord{}, apierrors.FromK8sError(err, ServiceBindingResourceType)
}
}

if cfServiceInstance.Spec.Type == korifiv1alpha1.UserProvidedType {
Expand All @@ -177,6 +198,25 @@ func (r *ServiceBindingRepo) CreateServiceBinding(ctx context.Context, authInfo
return serviceBindingToRecord(*cfServiceBinding), nil
}

func (r *ServiceBindingRepo) createParametersSecret(ctx context.Context, userClient client.Client, cfServiceBinding *korifiv1alpha1.CFServiceBinding, parameters map[string]any) error {
parametersData, err := tools.ToParametersSecretData(parameters)
if err != nil {
return err
}

paramsSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: cfServiceBinding.Namespace,
Name: cfServiceBinding.Spec.Parameters.Name,
},
Data: parametersData,
}

_ = controllerutil.SetOwnerReference(cfServiceBinding, paramsSecret, scheme.Scheme)

return userClient.Create(ctx, paramsSecret)
}

func (r *ServiceBindingRepo) DeleteServiceBinding(ctx context.Context, authInfo authorization.Info, guid string) error {
userClient, err := r.userClientFactory.BuildClient(authInfo)
if err != nil {
Expand Down
Loading
Loading