From f45b42c2f4d67f302dded849e40567f91952531b Mon Sep 17 00:00:00 2001 From: klapkov <91314044+klapkov@users.noreply.github.com> Date: Thu, 7 Nov 2024 16:14:29 +0200 Subject: [PATCH 1/4] Add Service Credential Bindings of type Key * Added Service Credential Bindings of type Key * Added type label to CFServiceBinding CRD * Tweaked list for bindings so you can filter by type --------- Co-authored-by: Dimitar Draganov --- api/handlers/service_binding.go | 35 +- api/handlers/service_binding_test.go | 775 ++++++++++-------- api/payloads/service_binding.go | 48 +- api/payloads/service_binding_test.go | 53 +- .../service_binding_repository.go | 48 +- .../service_binding_repository_test.go | 168 +++- .../api/v1alpha1/cfservicebinding_types.go | 3 + controllers/api/v1alpha1/shared_types.go | 2 + .../services/bindings/controller.go | 20 +- .../services/bindings/controller_test.go | 7 +- .../webhooks/services/bindings/validator.go | 4 + tests/assets/sample-broker-golang/main.go | 3 +- 12 files changed, 741 insertions(+), 425 deletions(-) diff --git a/api/handlers/service_binding.go b/api/handlers/service_binding.go index 8f61bec28..06eef6e68 100644 --- a/api/handlers/service_binding.go +++ b/api/handlers/service_binding.go @@ -52,33 +52,42 @@ func (h *ServiceBinding) create(r *http.Request) (*routing.Response, error) { authInfo, _ := authorization.InfoFromContext(r.Context()) logger := logr.FromContextOrDiscard(r.Context()).WithName("handlers.service-binding.create") - var payload payloads.ServiceBindingCreate - if err := h.requestValidator.DecodeAndValidateJSONPayload(r, &payload); err != nil { + payload := new(payloads.ServiceBindingCreate) + if err := h.requestValidator.DecodeAndValidateJSONPayload(r, payload); err != nil { return nil, apierrors.LogAndReturn(logger, err, "failed to decode payload") } - app, err := h.appRepo.GetApp(r.Context(), authInfo, payload.Relationships.App.Data.GUID) - if err != nil { - return nil, apierrors.LogAndReturn(logger, apierrors.ForbiddenAsNotFound(err), "failed to get "+repositories.AppResourceType) - } - serviceInstance, err := h.serviceInstanceRepo.GetServiceInstance(r.Context(), authInfo, payload.Relationships.ServiceInstance.Data.GUID) if err != nil { return nil, apierrors.LogAndReturn(logger, apierrors.ForbiddenAsNotFound(err), "failed to get "+repositories.ServiceInstanceResourceType) } - if app.SpaceGUID != serviceInstance.SpaceGUID { + if payload.Type == korifiv1alpha1.CFServiceBindingTypeKey && serviceInstance.Type != korifiv1alpha1.ManagedType { return nil, apierrors.LogAndReturn( logger, - apierrors.NewUnprocessableEntityError(nil, "The service instance and the app are in different spaces"), - "App and ServiceInstance in different spaces", "App GUID", app.GUID, - "ServiceInstance GUID", serviceInstance.GUID, + apierrors.NewUnprocessableEntityError(nil, "Service credential bindings of type 'key' are not supported for user-provided service instances."), + "", ) } - ctx := logr.NewContext(r.Context(), logger.WithValues("app", app.GUID, "service-instance", serviceInstance.GUID)) + if payload.Type == korifiv1alpha1.CFServiceBindingTypeApp { + var app repositories.AppRecord + if app, err = h.appRepo.GetApp(r.Context(), authInfo, payload.Relationships.App.Data.GUID); err != nil { + return nil, apierrors.LogAndReturn(logger, apierrors.ForbiddenAsNotFound(err), "failed to get "+repositories.AppResourceType) + } + + if app.SpaceGUID != serviceInstance.SpaceGUID { + return nil, apierrors.LogAndReturn( + logger, + apierrors.NewUnprocessableEntityError(nil, "The service instance and the app are in different spaces"), + "App and ServiceInstance in different spaces", "App GUID", app.GUID, + "ServiceInstance GUID", serviceInstance.GUID, + ) + } + } - serviceBinding, err := h.serviceBindingRepo.CreateServiceBinding(ctx, authInfo, payload.ToMessage(app.SpaceGUID)) + ctx := logr.NewContext(r.Context(), logger.WithValues("service-instance", serviceInstance.GUID)) + serviceBinding, err := h.serviceBindingRepo.CreateServiceBinding(ctx, authInfo, payload.ToMessage(serviceInstance.SpaceGUID)) if err != nil { return nil, apierrors.LogAndReturn(logr.FromContextOrDiscard(ctx), err, "failed to create ServiceBinding") } diff --git a/api/handlers/service_binding_test.go b/api/handlers/service_binding_test.go index 72bc4df8a..8dae932ac 100644 --- a/api/handlers/service_binding_test.go +++ b/api/handlers/service_binding_test.go @@ -72,476 +72,555 @@ var _ = Describe("ServiceBinding", func() { Describe("POST /v3/service_credential_bindings", func() { var payload payloads.ServiceBindingCreate - BeforeEach(func() { - requestMethod = http.MethodPost - requestPath = "/v3/service_credential_bindings" - requestBody = "the-json-body" - - payload = payloads.ServiceBindingCreate{ - Relationships: &payloads.ServiceBindingRelationships{ - App: &payloads.Relationship{ - Data: &payloads.RelationshipData{ - GUID: "app-guid", - }, - }, - ServiceInstance: &payloads.Relationship{ - Data: &payloads.RelationshipData{ - GUID: "service-instance-guid", + When("creating a service binding of type key", func() { + BeforeEach(func() { + requestMethod = http.MethodPost + requestPath = "/v3/service_credential_bindings" + requestBody = "the-json-body" + + payload = payloads.ServiceBindingCreate{ + Relationships: &payloads.ServiceBindingRelationships{ + ServiceInstance: &payloads.Relationship{ + Data: &payloads.RelationshipData{ + GUID: "service-instance-guid", + }, }, }, - }, - Type: "app", - } - requestValidator.DecodeAndValidateJSONPayloadStub = decodeAndValidatePayloadStub(&payload) - }) - - It("validates the payload", func() { - Expect(requestValidator.DecodeAndValidateJSONPayloadCallCount()).To(Equal(1)) - actualReq, _ := requestValidator.DecodeAndValidateJSONPayloadArgsForCall(0) - Expect(bodyString(actualReq)).To(Equal("the-json-body")) - }) - - When("the request body is invalid json", func() { - BeforeEach(func() { - requestValidator.DecodeAndValidateJSONPayloadReturns(errors.New("boom")) + Type: korifiv1alpha1.CFServiceBindingTypeKey, + } + requestValidator.DecodeAndValidateJSONPayloadStub = decodeAndValidatePayloadStub(&payload) }) - It("returns an error", func() { - expectUnknownError() - }) - }) + When("binding to a managed service instance", func() { + BeforeEach(func() { + serviceInstanceRepo.GetServiceInstanceReturns(repositories.ServiceInstanceRecord{ + GUID: "service-instance-guid", + SpaceGUID: "space-guid", + Type: korifiv1alpha1.ManagedType, + }, nil) + + serviceBindingRepo.CreateServiceBindingReturns(repositories.ServiceBindingRecord{ + GUID: "service-binding-guid", + Type: korifiv1alpha1.CFServiceBindingTypeKey, + }, nil) + }) - It("gets the app", func() { - Expect(serviceInstanceRepo.GetServiceInstanceCallCount()).To(Equal(1)) - _, actualAuthInfo, actualServiceInstanceGUID := serviceInstanceRepo.GetServiceInstanceArgsForCall(0) - Expect(actualAuthInfo).To(Equal(authInfo)) - Expect(actualServiceInstanceGUID).To(Equal("service-instance-guid")) - }) + It("creates a binding", func() { + Expect(serviceInstanceRepo.GetServiceInstanceCallCount()).To(Equal(1)) + Expect(serviceBindingRepo.CreateServiceBindingCallCount()).To(Equal(1)) + _, actualAuthInfo, createServiceBindingMessage := serviceBindingRepo.CreateServiceBindingArgsForCall(0) + Expect(actualAuthInfo).To(Equal(authInfo)) + Expect(createServiceBindingMessage.ServiceInstanceGUID).To(Equal("service-instance-guid")) + Expect(createServiceBindingMessage.SpaceGUID).To(Equal("space-guid")) + Expect(createServiceBindingMessage.Type).To(Equal(korifiv1alpha1.CFServiceBindingTypeKey)) + Expect(rr).To(HaveHTTPStatus(http.StatusAccepted)) + Expect(rr).To(HaveHTTPHeaderWithValue("Location", + ContainSubstring("/v3/jobs/managed_service_binding.create~service-binding-guid"))) + }) - When("getting the app is forbidden", func() { - BeforeEach(func() { - appRepo.GetAppReturns(repositories.AppRecord{}, apierrors.NewForbiddenError(nil, repositories.ServiceBindingResourceType)) - }) + When("creating the ServiceBinding errors", func() { + BeforeEach(func() { + serviceBindingRepo.CreateServiceBindingReturns(repositories.ServiceBindingRecord{}, errors.New("boom")) + }) - It("returns a not found error", func() { - expectNotFoundError(repositories.ServiceBindingResourceType) + It("returns an error", func() { + expectUnknownError() + }) + }) }) - }) - When("getting the App errors", func() { - BeforeEach(func() { - appRepo.GetAppReturns(repositories.AppRecord{}, errors.New("boom")) + When("binding to a user provided service instance", func() { + BeforeEach(func() { + serviceInstanceRepo.GetServiceInstanceReturns(repositories.ServiceInstanceRecord{ + GUID: "service-instance-guid", + SpaceGUID: "space-guid", + Type: korifiv1alpha1.UserProvidedType, + }, nil) + }) + + It("returns an error", func() { + Expect(serviceBindingRepo.CreateServiceBindingCallCount()).To(Equal(0)) + expectUnprocessableEntityError("Service credential bindings of type 'key' are not supported for user-provided service instances.") + }) }) - It("returns an error and doesn't create the ServiceBinding", func() { - expectUnknownError() - Expect(serviceBindingRepo.CreateServiceBindingCallCount()).To(Equal(0)) + It("validates the payload", func() { + Expect(requestValidator.DecodeAndValidateJSONPayloadCallCount()).To(Equal(1)) + actualReq, _ := requestValidator.DecodeAndValidateJSONPayloadArgsForCall(0) + Expect(bodyString(actualReq)).To(Equal("the-json-body")) }) - }) - It("gets the service instance", func() { - Expect(serviceInstanceRepo.GetServiceInstanceCallCount()).To(Equal(1)) - _, actualAuthInfo, actualServiceInstanceGUID := serviceInstanceRepo.GetServiceInstanceArgsForCall(0) - Expect(actualAuthInfo).To(Equal(authInfo)) - Expect(actualServiceInstanceGUID).To(Equal("service-instance-guid")) - }) + When("the request body is invalid json", func() { + BeforeEach(func() { + requestValidator.DecodeAndValidateJSONPayloadReturns(errors.New("boom")) + }) - When("getting the service instance is forbidden", func() { - BeforeEach(func() { - serviceInstanceRepo.GetServiceInstanceReturns(repositories.ServiceInstanceRecord{}, apierrors.NewForbiddenError(nil, repositories.ServiceInstanceResourceType)) + It("returns an error", func() { + expectUnknownError() + }) }) - It("returns a not found error", func() { - expectNotFoundError(repositories.ServiceInstanceResourceType) + It("gets the service instance", func() { + Expect(serviceInstanceRepo.GetServiceInstanceCallCount()).To(Equal(1)) + _, actualAuthInfo, actualServiceInstanceGUID := serviceInstanceRepo.GetServiceInstanceArgsForCall(0) + Expect(actualAuthInfo).To(Equal(authInfo)) + Expect(actualServiceInstanceGUID).To(Equal("service-instance-guid")) }) - }) - When("getting the ServiceInstance errors", func() { - BeforeEach(func() { - serviceInstanceRepo.GetServiceInstanceReturns(repositories.ServiceInstanceRecord{}, errors.New("boom")) - }) + When("getting the service instance is forbidden", func() { + BeforeEach(func() { + serviceInstanceRepo.GetServiceInstanceReturns(repositories.ServiceInstanceRecord{}, apierrors.NewForbiddenError(nil, repositories.ServiceInstanceResourceType)) + }) - It("returns an error and doesn't create the ServiceBinding", func() { - expectUnknownError() - Expect(serviceBindingRepo.CreateServiceBindingCallCount()).To(Equal(0)) + It("returns a not found error", func() { + expectNotFoundError(repositories.ServiceInstanceResourceType) + }) }) - }) - When("the App and the ServiceInstance are in different spaces", func() { - BeforeEach(func() { - appRepo.GetAppReturns(repositories.AppRecord{SpaceGUID: spaceGUID}, nil) - serviceInstanceRepo.GetServiceInstanceReturns(repositories.ServiceInstanceRecord{SpaceGUID: "another-space-guid"}, nil) - }) + When("getting the ServiceInstance errors", func() { + BeforeEach(func() { + serviceInstanceRepo.GetServiceInstanceReturns(repositories.ServiceInstanceRecord{}, errors.New("boom")) + }) - It("returns an error and doesn't create the ServiceBinding", func() { - expectUnprocessableEntityError("The service instance and the app are in different spaces") - Expect(serviceBindingRepo.CreateServiceBindingCallCount()).To(Equal(0)) + It("returns an error and doesn't create the ServiceBinding", func() { + expectUnknownError() + Expect(serviceBindingRepo.CreateServiceBindingCallCount()).To(Equal(0)) + }) }) }) - When("binding to a user provided service instance", func() { + When("creating a service binding of type app", func() { BeforeEach(func() { - serviceInstanceRepo.GetServiceInstanceReturns(repositories.ServiceInstanceRecord{ - GUID: "service-instance-guid", - SpaceGUID: "space-guid", - Type: korifiv1alpha1.UserProvidedType, - }, nil) - - serviceBindingRepo.CreateServiceBindingReturns(repositories.ServiceBindingRecord{ - GUID: "service-binding-guid", - }, nil) + requestMethod = http.MethodPost + requestPath = "/v3/service_credential_bindings" + requestBody = "the-json-body" + + payload = payloads.ServiceBindingCreate{ + Relationships: &payloads.ServiceBindingRelationships{ + App: &payloads.Relationship{ + Data: &payloads.RelationshipData{ + GUID: "app-guid", + }, + }, + ServiceInstance: &payloads.Relationship{ + Data: &payloads.RelationshipData{ + GUID: "service-instance-guid", + }, + }, + }, + Type: korifiv1alpha1.CFServiceBindingTypeApp, + } + requestValidator.DecodeAndValidateJSONPayloadStub = decodeAndValidatePayloadStub(&payload) }) - It("creates a service binding", func() { - Expect(serviceBindingRepo.CreateServiceBindingCallCount()).To(Equal(1)) - _, actualAuthInfo, createServiceBindingMessage := serviceBindingRepo.CreateServiceBindingArgsForCall(0) - Expect(actualAuthInfo).To(Equal(authInfo)) - Expect(createServiceBindingMessage.AppGUID).To(Equal("app-guid")) - Expect(createServiceBindingMessage.ServiceInstanceGUID).To(Equal("service-instance-guid")) - Expect(createServiceBindingMessage.SpaceGUID).To(Equal("space-guid")) + When("binding to a user provided service instance", func() { + BeforeEach(func() { + serviceInstanceRepo.GetServiceInstanceReturns(repositories.ServiceInstanceRecord{ + GUID: "service-instance-guid", + SpaceGUID: "space-guid", + Type: korifiv1alpha1.UserProvidedType, + }, nil) + + serviceBindingRepo.CreateServiceBindingReturns(repositories.ServiceBindingRecord{ + GUID: "service-binding-guid", + Type: korifiv1alpha1.CFServiceBindingTypeApp, + }, nil) + }) - Expect(rr).To(HaveHTTPStatus(http.StatusCreated)) - Expect(rr).To(HaveHTTPHeaderWithValue("Content-Type", "application/json")) - Expect(rr).To(HaveHTTPBody(SatisfyAll( - MatchJSONPath("$.guid", "service-binding-guid"), - MatchJSONPath("$.links.self.href", "https://api.example.org/v3/service_credential_bindings/service-binding-guid"), - ))) + It("creates a service binding", func() { + Expect(serviceBindingRepo.CreateServiceBindingCallCount()).To(Equal(1)) + _, actualAuthInfo, createServiceBindingMessage := serviceBindingRepo.CreateServiceBindingArgsForCall(0) + Expect(actualAuthInfo).To(Equal(authInfo)) + Expect(createServiceBindingMessage.AppGUID).To(Equal("app-guid")) + Expect(createServiceBindingMessage.ServiceInstanceGUID).To(Equal("service-instance-guid")) + Expect(createServiceBindingMessage.SpaceGUID).To(Equal("space-guid")) + Expect(createServiceBindingMessage.Type).To(Equal(korifiv1alpha1.CFServiceBindingTypeApp)) + + Expect(rr).To(HaveHTTPStatus(http.StatusCreated)) + Expect(rr).To(HaveHTTPHeaderWithValue("Content-Type", "application/json")) + Expect(rr).To(HaveHTTPBody(SatisfyAll( + MatchJSONPath("$.guid", "service-binding-guid"), + MatchJSONPath("$.links.self.href", "https://api.example.org/v3/service_credential_bindings/service-binding-guid"), + ))) + }) + + When("creating the ServiceBinding errors", func() { + BeforeEach(func() { + serviceBindingRepo.CreateServiceBindingReturns(repositories.ServiceBindingRecord{}, errors.New("boom")) + }) + + It("returns an error", func() { + expectUnknownError() + }) + }) }) - When("creating the ServiceBinding errors", func() { + When("binding to a managed service instance", func() { BeforeEach(func() { - serviceBindingRepo.CreateServiceBindingReturns(repositories.ServiceBindingRecord{}, errors.New("boom")) + serviceInstanceRepo.GetServiceInstanceReturns(repositories.ServiceInstanceRecord{ + GUID: "service-instance-guid", + SpaceGUID: "space-guid", + Type: korifiv1alpha1.ManagedType, + }, nil) + + serviceBindingRepo.CreateServiceBindingReturns(repositories.ServiceBindingRecord{ + GUID: "service-binding-guid", + Type: korifiv1alpha1.CFServiceBindingTypeApp, + }, nil) }) - It("returns an error", func() { - expectUnknownError() + It("creates a binding", func() { + Expect(serviceBindingRepo.CreateServiceBindingCallCount()).To(Equal(1)) + _, actualAuthInfo, createServiceBindingMessage := serviceBindingRepo.CreateServiceBindingArgsForCall(0) + Expect(actualAuthInfo).To(Equal(authInfo)) + Expect(createServiceBindingMessage.AppGUID).To(Equal("app-guid")) + Expect(createServiceBindingMessage.ServiceInstanceGUID).To(Equal("service-instance-guid")) + Expect(createServiceBindingMessage.SpaceGUID).To(Equal("space-guid")) + Expect(createServiceBindingMessage.Type).To(Equal(korifiv1alpha1.CFServiceBindingTypeApp)) + Expect(rr).To(HaveHTTPStatus(http.StatusAccepted)) + Expect(rr).To(HaveHTTPHeaderWithValue("Location", + ContainSubstring("/v3/jobs/managed_service_binding.create~service-binding-guid"))) }) - }) - }) - When("binding to a managed service instance", func() { - BeforeEach(func() { - serviceInstanceRepo.GetServiceInstanceReturns(repositories.ServiceInstanceRecord{ - GUID: "service-instance-guid", - SpaceGUID: "space-guid", - Type: korifiv1alpha1.ManagedType, - }, nil) + When("creating the ServiceBinding errors", func() { + BeforeEach(func() { + serviceBindingRepo.CreateServiceBindingReturns(repositories.ServiceBindingRecord{}, errors.New("boom")) + }) - serviceBindingRepo.CreateServiceBindingReturns(repositories.ServiceBindingRecord{ - GUID: "service-binding-guid", - }, nil) + It("returns an error", func() { + expectUnknownError() + }) + }) }) - It("creates a binding", func() { - Expect(serviceBindingRepo.CreateServiceBindingCallCount()).To(Equal(1)) - _, actualAuthInfo, createServiceBindingMessage := serviceBindingRepo.CreateServiceBindingArgsForCall(0) + It("gets the app", func() { + Expect(appRepo.GetAppCallCount()).To(Equal(1)) + _, actualAuthInfo, actualAppGUID := appRepo.GetAppArgsForCall(0) Expect(actualAuthInfo).To(Equal(authInfo)) - Expect(createServiceBindingMessage.AppGUID).To(Equal("app-guid")) - Expect(createServiceBindingMessage.ServiceInstanceGUID).To(Equal("service-instance-guid")) - Expect(createServiceBindingMessage.SpaceGUID).To(Equal("space-guid")) - Expect(rr).To(HaveHTTPStatus(http.StatusAccepted)) - Expect(rr).To(HaveHTTPHeaderWithValue("Location", - ContainSubstring("/v3/jobs/managed_service_binding.create~service-binding-guid"))) + Expect(actualAppGUID).To(Equal("app-guid")) }) - When("creating the ServiceBinding errors", func() { + When("getting the app is forbidden", func() { BeforeEach(func() { - serviceBindingRepo.CreateServiceBindingReturns(repositories.ServiceBindingRecord{}, errors.New("boom")) + appRepo.GetAppReturns(repositories.AppRecord{}, apierrors.NewForbiddenError(nil, repositories.ServiceBindingResourceType)) }) - It("returns an error", func() { - expectUnknownError() + It("returns a not found error", func() { + expectNotFoundError(repositories.ServiceBindingResourceType) }) }) - }) - }) - Describe("GET /v3/service_credential_bindings/{guid}", func() { - BeforeEach(func() { - requestMethod = http.MethodGet - requestPath = "/v3/service_credential_bindings/service-binding-guid" - requestBody = "" - }) - - It("returns the service binding", func() { - Expect(rr).To(HaveHTTPStatus(http.StatusOK)) - Expect(rr).To(HaveHTTPHeaderWithValue("Content-Type", "application/json")) - Expect(rr).To(HaveHTTPBody(SatisfyAll( - MatchJSONPath("$.guid", "service-binding-guid"), - MatchJSONPath("$.links.self.href", "https://api.example.org/v3/service_credential_bindings/service-binding-guid"), - ))) - }) + When("getting the App errors", func() { + BeforeEach(func() { + appRepo.GetAppReturns(repositories.AppRecord{}, errors.New("boom")) + }) - When("the service bindding repo returns an error", func() { - BeforeEach(func() { - serviceBindingRepo.GetServiceBindingReturns(repositories.ServiceBindingRecord{}, errors.New("get-service-binding-error")) + It("returns an error and doesn't create the ServiceBinding", func() { + expectUnknownError() + Expect(serviceBindingRepo.CreateServiceBindingCallCount()).To(Equal(0)) + }) }) - It("returns an error", func() { - expectUnknownError() + When("the App and the ServiceInstance are in different spaces", func() { + BeforeEach(func() { + appRepo.GetAppReturns(repositories.AppRecord{SpaceGUID: spaceGUID}, nil) + serviceInstanceRepo.GetServiceInstanceReturns(repositories.ServiceInstanceRecord{SpaceGUID: "another-space-guid"}, nil) + }) + + It("returns an error and doesn't create the ServiceBinding", func() { + expectUnprocessableEntityError("The service instance and the app are in different spaces") + Expect(serviceBindingRepo.CreateServiceBindingCallCount()).To(Equal(0)) + }) }) }) - When("the user is not authorized", func() { + Describe("GET /v3/service_credential_bindings/{guid}", func() { BeforeEach(func() { - serviceBindingRepo.GetServiceBindingReturns(repositories.ServiceBindingRecord{}, apierrors.NewForbiddenError(nil, "CFServiceBinding")) + requestMethod = http.MethodGet + requestPath = "/v3/service_credential_bindings/service-binding-guid" + requestBody = "" }) - It("returns 404 NotFound", func() { - expectNotFoundError("CFServiceBinding") + It("returns the service binding", func() { + Expect(rr).To(HaveHTTPStatus(http.StatusOK)) + Expect(rr).To(HaveHTTPHeaderWithValue("Content-Type", "application/json")) + Expect(rr).To(HaveHTTPBody(SatisfyAll( + MatchJSONPath("$.guid", "service-binding-guid"), + MatchJSONPath("$.links.self.href", "https://api.example.org/v3/service_credential_bindings/service-binding-guid"), + ))) }) - }) - }) - - Describe("GET /v3/service_credential_bindings", func() { - BeforeEach(func() { - requestMethod = http.MethodGet - requestBody = "" - requestPath = "/v3/service_credential_bindings?foo=bar" - - serviceBindingRepo.ListServiceBindingsReturns([]repositories.ServiceBindingRecord{ - {GUID: "service-binding-guid", AppGUID: "app-guid"}, - }, nil) - appRepo.ListAppsReturns([]repositories.AppRecord{{Name: "some-app-name"}}, nil) - - payload := payloads.ServiceBindingList{ - AppGUIDs: "a1,a2", - ServiceInstanceGUIDs: "s1,s2", - LabelSelector: "label=value", - PlanGUIDs: "p1,p2", - } - requestValidator.DecodeAndValidateURLValuesStub = decodeAndValidateURLValuesStub(&payload) - }) - It("returns the list of ServiceBindings", func() { - Expect(requestValidator.DecodeAndValidateURLValuesCallCount()).To(Equal(1)) - actualReq, _ := requestValidator.DecodeAndValidateURLValuesArgsForCall(0) - Expect(actualReq.URL.String()).To(HaveSuffix(requestPath)) - - Expect(serviceBindingRepo.ListServiceBindingsCallCount()).To(Equal(1)) - _, _, message := serviceBindingRepo.ListServiceBindingsArgsForCall(0) - Expect(message.AppGUIDs).To(ConsistOf([]string{"a1", "a2"})) - Expect(message.ServiceInstanceGUIDs).To(ConsistOf([]string{"s1", "s2"})) - Expect(message.LabelSelector).To(Equal("label=value")) - Expect(message.PlanGUIDs).To(ConsistOf("p1", "p2")) - - Expect(rr).To(HaveHTTPStatus(http.StatusOK)) - Expect(rr).To(HaveHTTPHeaderWithValue("Content-Type", "application/json")) - Expect(rr).To(HaveHTTPBody(SatisfyAll( - MatchJSONPath("$.pagination.total_results", BeEquivalentTo(1)), - MatchJSONPath("$.pagination.first.href", "https://api.example.org/v3/service_credential_bindings?foo=bar"), - MatchJSONPath("$.resources[0].guid", "service-binding-guid"), - ))) - }) + When("the service bindding repo returns an error", func() { + BeforeEach(func() { + serviceBindingRepo.GetServiceBindingReturns(repositories.ServiceBindingRecord{}, errors.New("get-service-binding-error")) + }) - When("there is an error fetching service binding", func() { - BeforeEach(func() { - serviceBindingRepo.ListServiceBindingsReturns([]repositories.ServiceBindingRecord{}, errors.New("unknown")) + It("returns an error", func() { + expectUnknownError() + }) }) - It("returns an error", func() { - expectUnknownError() + When("the user is not authorized", func() { + BeforeEach(func() { + serviceBindingRepo.GetServiceBindingReturns(repositories.ServiceBindingRecord{}, apierrors.NewForbiddenError(nil, "CFServiceBinding")) + }) + + It("returns 404 NotFound", func() { + expectNotFoundError("CFServiceBinding") + }) }) }) - When("an include=app query parameter is specified", func() { + Describe("GET /v3/service_credential_bindings", func() { BeforeEach(func() { + requestMethod = http.MethodGet + requestBody = "" + requestPath = "/v3/service_credential_bindings?foo=bar" + + serviceBindingRepo.ListServiceBindingsReturns([]repositories.ServiceBindingRecord{ + {GUID: "service-binding-guid", AppGUID: "app-guid"}, + }, nil) + appRepo.ListAppsReturns([]repositories.AppRecord{{Name: "some-app-name"}}, nil) + payload := payloads.ServiceBindingList{ - Include: "app", + AppGUIDs: "a1,a2", + ServiceInstanceGUIDs: "s1,s2", + LabelSelector: "label=value", + PlanGUIDs: "p1,p2", } requestValidator.DecodeAndValidateURLValuesStub = decodeAndValidateURLValuesStub(&payload) }) - It("includes app data in the response", func() { - Expect(appRepo.ListAppsCallCount()).To(Equal(1)) - _, _, listAppsMessage := appRepo.ListAppsArgsForCall(0) - Expect(listAppsMessage.Guids).To(ContainElements("app-guid")) + It("returns the list of ServiceBindings", func() { + Expect(requestValidator.DecodeAndValidateURLValuesCallCount()).To(Equal(1)) + actualReq, _ := requestValidator.DecodeAndValidateURLValuesArgsForCall(0) + Expect(actualReq.URL.String()).To(HaveSuffix(requestPath)) - Expect(rr).To(HaveHTTPBody(MatchJSONPath("$.included.apps[0].name", "some-app-name"))) - }) - }) + Expect(serviceBindingRepo.ListServiceBindingsCallCount()).To(Equal(1)) + _, _, message := serviceBindingRepo.ListServiceBindingsArgsForCall(0) + Expect(message.AppGUIDs).To(ConsistOf([]string{"a1", "a2"})) + Expect(message.ServiceInstanceGUIDs).To(ConsistOf([]string{"s1", "s2"})) + Expect(message.LabelSelector).To(Equal("label=value")) + Expect(message.PlanGUIDs).To(ConsistOf("p1", "p2")) - When("decoding URL params fails", func() { - BeforeEach(func() { - requestValidator.DecodeAndValidateURLValuesReturns(errors.New("boom")) + Expect(rr).To(HaveHTTPStatus(http.StatusOK)) + Expect(rr).To(HaveHTTPHeaderWithValue("Content-Type", "application/json")) + Expect(rr).To(HaveHTTPBody(SatisfyAll( + MatchJSONPath("$.pagination.total_results", BeEquivalentTo(1)), + MatchJSONPath("$.pagination.first.href", "https://api.example.org/v3/service_credential_bindings?foo=bar"), + MatchJSONPath("$.resources[0].guid", "service-binding-guid"), + ))) }) - It("returns an error", func() { - expectUnknownError() + When("there is an error fetching service binding", func() { + BeforeEach(func() { + serviceBindingRepo.ListServiceBindingsReturns([]repositories.ServiceBindingRecord{}, errors.New("unknown")) + }) + + It("returns an error", func() { + expectUnknownError() + }) }) - }) - }) - Describe("DELETE /v3/service_credential_bindings/:guid", func() { - BeforeEach(func() { - requestMethod = "DELETE" - requestPath = "/v3/service_credential_bindings/service-binding-guid" - }) + When("an include=app query parameter is specified", func() { + BeforeEach(func() { + payload := payloads.ServiceBindingList{ + Include: "app", + } + requestValidator.DecodeAndValidateURLValuesStub = decodeAndValidateURLValuesStub(&payload) + }) - It("gets the service binding", func() { - Expect(serviceBindingRepo.GetServiceBindingCallCount()).To(Equal(1)) - _, actualAuthInfo, actualBindingGUID := serviceBindingRepo.GetServiceBindingArgsForCall(0) - Expect(actualAuthInfo).To(Equal(authInfo)) - Expect(actualBindingGUID).To(Equal("service-binding-guid")) - }) + It("includes app data in the response", func() { + Expect(appRepo.ListAppsCallCount()).To(Equal(1)) + _, _, listAppsMessage := appRepo.ListAppsArgsForCall(0) + Expect(listAppsMessage.Guids).To(ContainElements("app-guid")) - When("getting the service binding is forbidden", func() { - BeforeEach(func() { - serviceBindingRepo.GetServiceBindingReturns(repositories.ServiceBindingRecord{}, apierrors.NewForbiddenError(nil, repositories.ServiceBindingResourceType)) + Expect(rr).To(HaveHTTPBody(MatchJSONPath("$.included.apps[0].name", "some-app-name"))) + }) }) - It("returns a not found error", func() { - expectNotFoundError(repositories.ServiceBindingResourceType) + When("decoding URL params fails", func() { + BeforeEach(func() { + requestValidator.DecodeAndValidateURLValuesReturns(errors.New("boom")) + }) + + It("returns an error", func() { + expectUnknownError() + }) }) }) - When("getting the service binding fails", func() { + Describe("DELETE /v3/service_credential_bindings/:guid", func() { BeforeEach(func() { - serviceBindingRepo.GetServiceBindingReturns(repositories.ServiceBindingRecord{}, errors.New("getting-binding-failed")) + requestMethod = "DELETE" + requestPath = "/v3/service_credential_bindings/service-binding-guid" }) - It("returns unknown error", func() { - expectUnknownError() + It("gets the service binding", func() { + Expect(serviceBindingRepo.GetServiceBindingCallCount()).To(Equal(1)) + _, actualAuthInfo, actualBindingGUID := serviceBindingRepo.GetServiceBindingArgsForCall(0) + Expect(actualAuthInfo).To(Equal(authInfo)) + Expect(actualBindingGUID).To(Equal("service-binding-guid")) }) - }) - It("gets the service instance", func() { - Expect(serviceInstanceRepo.GetServiceInstanceCallCount()).To(Equal(1)) - _, actualAuthInfo, actualInstanceGUID := serviceInstanceRepo.GetServiceInstanceArgsForCall(0) - Expect(actualAuthInfo).To(Equal(authInfo)) - Expect(actualInstanceGUID).To(Equal("service-instance-guid")) - }) + When("getting the service binding is forbidden", func() { + BeforeEach(func() { + serviceBindingRepo.GetServiceBindingReturns(repositories.ServiceBindingRecord{}, apierrors.NewForbiddenError(nil, repositories.ServiceBindingResourceType)) + }) - When("getting the service instance fails", func() { - BeforeEach(func() { - serviceInstanceRepo.GetServiceInstanceReturns(repositories.ServiceInstanceRecord{}, errors.New("getting-instance-failed")) + It("returns a not found error", func() { + expectNotFoundError(repositories.ServiceBindingResourceType) + }) }) - It("returns error", func() { - expectUnprocessableEntityError("failed to get service instance") + When("getting the service binding fails", func() { + BeforeEach(func() { + serviceBindingRepo.GetServiceBindingReturns(repositories.ServiceBindingRecord{}, errors.New("getting-binding-failed")) + }) + + It("returns unknown error", func() { + expectUnknownError() + }) }) - }) - It("deletes the service binding", func() { - Expect(rr).To(HaveHTTPStatus(http.StatusNoContent)) - Expect(rr).To(HaveHTTPBody(BeEmpty())) + It("gets the service instance", func() { + Expect(serviceInstanceRepo.GetServiceInstanceCallCount()).To(Equal(1)) + _, actualAuthInfo, actualInstanceGUID := serviceInstanceRepo.GetServiceInstanceArgsForCall(0) + Expect(actualAuthInfo).To(Equal(authInfo)) + Expect(actualInstanceGUID).To(Equal("service-instance-guid")) + }) - Expect(serviceBindingRepo.DeleteServiceBindingCallCount()).To(Equal(1)) - _, _, guid := serviceBindingRepo.DeleteServiceBindingArgsForCall(0) - Expect(guid).To(Equal("service-binding-guid")) - }) + When("getting the service instance fails", func() { + BeforeEach(func() { + serviceInstanceRepo.GetServiceInstanceReturns(repositories.ServiceInstanceRecord{}, errors.New("getting-instance-failed")) + }) - When("the service instance is managed", func() { - BeforeEach(func() { - serviceInstanceRepo.GetServiceInstanceReturns(repositories.ServiceInstanceRecord{ - GUID: "service-instance-guid", - SpaceGUID: "space-guid", - Type: korifiv1alpha1.ManagedType, - }, nil) + It("returns error", func() { + expectUnprocessableEntityError("failed to get service instance") + }) }) - It("deletes the binding in a job", func() { + It("deletes the service binding", func() { + Expect(rr).To(HaveHTTPStatus(http.StatusNoContent)) + Expect(rr).To(HaveHTTPBody(BeEmpty())) + Expect(serviceBindingRepo.DeleteServiceBindingCallCount()).To(Equal(1)) _, _, guid := serviceBindingRepo.DeleteServiceBindingArgsForCall(0) Expect(guid).To(Equal("service-binding-guid")) - - Expect(rr).To(HaveHTTPStatus(http.StatusAccepted)) - Expect(rr).To(HaveHTTPHeaderWithValue("Location", - ContainSubstring("/v3/jobs/managed_service_binding.delete~service-binding-guid"))) }) - }) - When("deleting the service binding fails", func() { - BeforeEach(func() { - serviceBindingRepo.DeleteServiceBindingReturns(errors.New("delete-binding-failed")) - }) + When("the service instance is managed", func() { + BeforeEach(func() { + serviceInstanceRepo.GetServiceInstanceReturns(repositories.ServiceInstanceRecord{ + GUID: "service-instance-guid", + SpaceGUID: "space-guid", + Type: korifiv1alpha1.ManagedType, + }, nil) + }) + + It("deletes the binding in a job", func() { + Expect(serviceBindingRepo.DeleteServiceBindingCallCount()).To(Equal(1)) + _, _, guid := serviceBindingRepo.DeleteServiceBindingArgsForCall(0) + Expect(guid).To(Equal("service-binding-guid")) - It("returns unknown error", func() { - expectUnknownError() + Expect(rr).To(HaveHTTPStatus(http.StatusAccepted)) + Expect(rr).To(HaveHTTPHeaderWithValue("Location", + ContainSubstring("/v3/jobs/managed_service_binding.delete~service-binding-guid"))) + }) }) - }) - }) - Describe("PATCH /v3/service_credential_bindings/:guid", func() { - BeforeEach(func() { - requestMethod = "PATCH" - requestPath = "/v3/service_credential_bindings/service-binding-guid" - requestBody = "the-json-body" - - serviceBindingRepo.UpdateServiceBindingReturns(repositories.ServiceBindingRecord{ - GUID: "service-binding-guid", - }, nil) - - payload := payloads.ServiceBindingUpdate{ - Metadata: payloads.MetadataPatch{ - Labels: map[string]*string{"foo": tools.PtrTo("bar")}, - Annotations: map[string]*string{"bar": tools.PtrTo("baz")}, - }, - } - requestValidator.DecodeAndValidateJSONPayloadStub = decodeAndValidatePayloadStub(&payload) - }) + When("deleting the service binding fails", func() { + BeforeEach(func() { + serviceBindingRepo.DeleteServiceBindingReturns(errors.New("delete-binding-failed")) + }) - It("updates the service binding", func() { - Expect(requestValidator.DecodeAndValidateJSONPayloadCallCount()).To(Equal(1)) - actualReq, _ := requestValidator.DecodeAndValidateJSONPayloadArgsForCall(0) - Expect(bodyString(actualReq)).To(Equal("the-json-body")) - - Expect(rr).To(HaveHTTPStatus(http.StatusOK)) - Expect(rr).To(HaveHTTPHeaderWithValue("Content-Type", "application/json")) - Expect(rr).To(HaveHTTPBody(SatisfyAll( - MatchJSONPath("$.guid", "service-binding-guid"), - MatchJSONPath("$.links.self.href", "https://api.example.org/v3/service_credential_bindings/service-binding-guid"), - ))) - - Expect(serviceBindingRepo.UpdateServiceBindingCallCount()).To(Equal(1)) - _, actualAuthInfo, updateMessage := serviceBindingRepo.UpdateServiceBindingArgsForCall(0) - Expect(actualAuthInfo).To(Equal(authInfo)) - Expect(updateMessage).To(Equal(repositories.UpdateServiceBindingMessage{ - GUID: "service-binding-guid", - MetadataPatch: repositories.MetadataPatch{ - Labels: map[string]*string{"foo": tools.PtrTo("bar")}, - Annotations: map[string]*string{"bar": tools.PtrTo("baz")}, - }, - })) + It("returns unknown error", func() { + expectUnknownError() + }) + }) }) - When("the payload cannot be decoded", func() { + Describe("PATCH /v3/service_credential_bindings/:guid", func() { BeforeEach(func() { - requestValidator.DecodeAndValidateJSONPayloadReturns(errors.New("boom")) - }) + requestMethod = "PATCH" + requestPath = "/v3/service_credential_bindings/service-binding-guid" + requestBody = "the-json-body" - It("returns an error", func() { - expectUnknownError() - }) - }) + serviceBindingRepo.UpdateServiceBindingReturns(repositories.ServiceBindingRecord{ + GUID: "service-binding-guid", + }, nil) - When("getting the service binding is forbidden", func() { - BeforeEach(func() { - serviceBindingRepo.GetServiceBindingReturns(repositories.ServiceBindingRecord{}, apierrors.NewForbiddenError(nil, repositories.ServiceBindingResourceType)) + payload := payloads.ServiceBindingUpdate{ + Metadata: payloads.MetadataPatch{ + Labels: map[string]*string{"foo": tools.PtrTo("bar")}, + Annotations: map[string]*string{"bar": tools.PtrTo("baz")}, + }, + } + requestValidator.DecodeAndValidateJSONPayloadStub = decodeAndValidatePayloadStub(&payload) }) - It("returns a not found error", func() { - expectNotFoundError(repositories.ServiceBindingResourceType) + It("updates the service binding", func() { + Expect(requestValidator.DecodeAndValidateJSONPayloadCallCount()).To(Equal(1)) + actualReq, _ := requestValidator.DecodeAndValidateJSONPayloadArgsForCall(0) + Expect(bodyString(actualReq)).To(Equal("the-json-body")) + + Expect(rr).To(HaveHTTPStatus(http.StatusOK)) + Expect(rr).To(HaveHTTPHeaderWithValue("Content-Type", "application/json")) + Expect(rr).To(HaveHTTPBody(SatisfyAll( + MatchJSONPath("$.guid", "service-binding-guid"), + MatchJSONPath("$.links.self.href", "https://api.example.org/v3/service_credential_bindings/service-binding-guid"), + ))) + + Expect(serviceBindingRepo.UpdateServiceBindingCallCount()).To(Equal(1)) + _, actualAuthInfo, updateMessage := serviceBindingRepo.UpdateServiceBindingArgsForCall(0) + Expect(actualAuthInfo).To(Equal(authInfo)) + Expect(updateMessage).To(Equal(repositories.UpdateServiceBindingMessage{ + GUID: "service-binding-guid", + MetadataPatch: repositories.MetadataPatch{ + Labels: map[string]*string{"foo": tools.PtrTo("bar")}, + Annotations: map[string]*string{"bar": tools.PtrTo("baz")}, + }, + })) }) - }) - When("the service binding repo returns an error", func() { - BeforeEach(func() { - serviceBindingRepo.UpdateServiceBindingReturns(repositories.ServiceBindingRecord{}, errors.New("update-sb-error")) + When("the payload cannot be decoded", func() { + BeforeEach(func() { + requestValidator.DecodeAndValidateJSONPayloadReturns(errors.New("boom")) + }) + + It("returns an error", func() { + expectUnknownError() + }) }) - It("returns an error", func() { - expectUnknownError() + When("getting the service binding is forbidden", func() { + BeforeEach(func() { + serviceBindingRepo.GetServiceBindingReturns(repositories.ServiceBindingRecord{}, apierrors.NewForbiddenError(nil, repositories.ServiceBindingResourceType)) + }) + + It("returns a not found error", func() { + expectNotFoundError(repositories.ServiceBindingResourceType) + }) }) - }) - When("the user is not authorized to get service bindings", func() { - BeforeEach(func() { - serviceBindingRepo.GetServiceBindingReturns(repositories.ServiceBindingRecord{}, apierrors.NewForbiddenError(nil, "CFServiceBinding")) + When("the service binding repo returns an error", func() { + BeforeEach(func() { + serviceBindingRepo.UpdateServiceBindingReturns(repositories.ServiceBindingRecord{}, errors.New("update-sb-error")) + }) + + It("returns an error", func() { + expectUnknownError() + }) }) - It("returns 404 NotFound", func() { - expectNotFoundError("CFServiceBinding") + When("the user is not authorized to get service bindings", func() { + BeforeEach(func() { + serviceBindingRepo.GetServiceBindingReturns(repositories.ServiceBindingRecord{}, apierrors.NewForbiddenError(nil, "CFServiceBinding")) + }) + + It("returns 404 NotFound", func() { + expectNotFoundError("CFServiceBinding") + }) }) }) }) diff --git a/api/payloads/service_binding.go b/api/payloads/service_binding.go index 85b42abe7..263580893 100644 --- a/api/payloads/service_binding.go +++ b/api/payloads/service_binding.go @@ -1,11 +1,13 @@ package payloads import ( + "fmt" "net/url" "code.cloudfoundry.org/korifi/api/payloads/parse" "code.cloudfoundry.org/korifi/api/payloads/validation" "code.cloudfoundry.org/korifi/api/repositories" + "code.cloudfoundry.org/korifi/tools" jellidation "github.com/jellydator/validation" ) @@ -16,18 +18,40 @@ type ServiceBindingCreate struct { } func (p ServiceBindingCreate) ToMessage(spaceGUID string) repositories.CreateServiceBindingMessage { + var appGUID string + if p.Relationships.App != nil { + appGUID = p.Relationships.App.Data.GUID + } + return repositories.CreateServiceBindingMessage{ Name: p.Name, ServiceInstanceGUID: p.Relationships.ServiceInstance.Data.GUID, - AppGUID: p.Relationships.App.Data.GUID, + AppGUID: appGUID, SpaceGUID: spaceGUID, + Type: p.Type, } } func (p ServiceBindingCreate) Validate() error { return jellidation.ValidateStruct(&p, - jellidation.Field(&p.Type, validation.OneOf("app")), - jellidation.Field(&p.Relationships, jellidation.NotNil), + jellidation.Field(&p.Type, validation.OneOf("app", "key")), + jellidation.Field(&p.Relationships, jellidation.By(func(value any) error { + relationships, ok := value.(*ServiceBindingRelationships) + if !ok || relationships == nil { + return fmt.Errorf("relationships is required") + } + + if p.Type == "app" { + if relationships.App == nil { + return jellidation.NewError("when type is app", "relationships.app is required") + } + if relationships.App.Data.GUID == "" { + return fmt.Errorf("relationships.app.data.guid cannot be blank") + } + } + + return nil + })), ) } @@ -38,12 +62,12 @@ type ServiceBindingRelationships struct { func (r ServiceBindingRelationships) Validate() error { return jellidation.ValidateStruct(&r, - jellidation.Field(&r.App, jellidation.NotNil), jellidation.Field(&r.ServiceInstance, jellidation.NotNil), ) } type ServiceBindingList struct { + Type string AppGUIDs string ServiceInstanceGUIDs string Include string @@ -51,13 +75,26 @@ type ServiceBindingList struct { PlanGUIDs string } +func (l ServiceBindingList) Validate() error { + return jellidation.ValidateStruct(&l, + jellidation.Field(&l.Type, validation.OneOf("app", "key")), + jellidation.Field(&l.Include, validation.OneOf("app", "service_instance")), + ) +} + func (l *ServiceBindingList) ToMessage() repositories.ListServiceBindingsMessage { - return repositories.ListServiceBindingsMessage{ + message := repositories.ListServiceBindingsMessage{ ServiceInstanceGUIDs: parse.ArrayParam(l.ServiceInstanceGUIDs), AppGUIDs: parse.ArrayParam(l.AppGUIDs), LabelSelector: l.LabelSelector, PlanGUIDs: parse.ArrayParam(l.PlanGUIDs), } + + if l.Type != "" { + message.Type = tools.PtrTo(l.Type) + } + + return message } func (l *ServiceBindingList) SupportedKeys() []string { @@ -65,6 +102,7 @@ func (l *ServiceBindingList) SupportedKeys() []string { } func (l *ServiceBindingList) DecodeFromURLValues(values url.Values) error { + l.Type = values.Get("type") l.AppGUIDs = values.Get("app_guids") l.ServiceInstanceGUIDs = values.Get("service_instance_guids") l.Include = values.Get("include") diff --git a/api/payloads/service_binding_test.go b/api/payloads/service_binding_test.go index 593eaf3ac..f1a126d99 100644 --- a/api/payloads/service_binding_test.go +++ b/api/payloads/service_binding_test.go @@ -4,10 +4,12 @@ import ( "code.cloudfoundry.org/korifi/api/errors" "code.cloudfoundry.org/korifi/api/payloads" "code.cloudfoundry.org/korifi/api/repositories" + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/tools" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/onsi/gomega/gstruct" + "github.com/onsi/gomega/types" ) var _ = Describe("ServiceBindingList", func() { @@ -18,13 +20,23 @@ var _ = Describe("ServiceBindingList", func() { Expect(decodeErr).NotTo(HaveOccurred()) Expect(*actualServiceBindingList).To(Equal(expectedServiceBindingList)) }, + Entry("type", "type=key", payloads.ServiceBindingList{Type: korifiv1alpha1.CFServiceBindingTypeKey}), Entry("app_guids", "app_guids=app_guid", payloads.ServiceBindingList{AppGUIDs: "app_guid"}), Entry("service_instance_guids", "service_instance_guids=si_guid", payloads.ServiceBindingList{ServiceInstanceGUIDs: "si_guid"}), - Entry("include", "include=include", payloads.ServiceBindingList{Include: "include"}), + Entry("include", "include=app", payloads.ServiceBindingList{Include: "app"}), Entry("label_selector=foo", "label_selector=foo", payloads.ServiceBindingList{LabelSelector: "foo"}), Entry("service_plan_guids=plan-guid", "service_plan_guids=plan-guid", payloads.ServiceBindingList{PlanGUIDs: "plan-guid"}), ) + DescribeTable("invalid query", + func(query string, errMatcher types.GomegaMatcher) { + _, decodeErr := decodeQuery[payloads.ServiceBindingList](query) + Expect(decodeErr).To(errMatcher) + }, + Entry("invalid type", "type=foo", MatchError(ContainSubstring("value must be one of"))), + Entry("invalid include type", "include=foo", MatchError(ContainSubstring("value must be one of"))), + ) + Describe("ToMessage", func() { var ( payload payloads.ServiceBindingList @@ -33,6 +45,7 @@ var _ = Describe("ServiceBindingList", func() { BeforeEach(func() { payload = payloads.ServiceBindingList{ + Type: korifiv1alpha1.CFServiceBindingTypeApp, AppGUIDs: "app1,app2", ServiceInstanceGUIDs: "s1,s2", Include: "include", @@ -47,6 +60,7 @@ var _ = Describe("ServiceBindingList", func() { It("returns a list service bindings message", func() { Expect(message).To(Equal(repositories.ListServiceBindingsMessage{ + Type: tools.PtrTo(korifiv1alpha1.CFServiceBindingTypeApp), AppGUIDs: []string{"app1", "app2"}, ServiceInstanceGUIDs: []string{"s1", "s2"}, LabelSelector: "foo=bar", @@ -64,6 +78,32 @@ var _ = Describe("ServiceBindingCreate", func() { apiError errors.ApiError ) + When("binding is of type key", func() { + BeforeEach(func() { + serviceBindingCreate = new(payloads.ServiceBindingCreate) + createPayload = payloads.ServiceBindingCreate{ + Relationships: &payloads.ServiceBindingRelationships{ + ServiceInstance: &payloads.Relationship{ + Data: &payloads.RelationshipData{ + GUID: "service-instance-guid", + }, + }, + }, + Type: "key", + } + }) + + 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))) + }) + }) + BeforeEach(func() { serviceBindingCreate = new(payloads.ServiceBindingCreate) createPayload = payloads.ServiceBindingCreate{ @@ -93,17 +133,6 @@ var _ = Describe("ServiceBindingCreate", func() { Expect(serviceBindingCreate).To(gstruct.PointTo(Equal(createPayload))) }) - When(`the type is "key"`, func() { - BeforeEach(func() { - createPayload.Type = "key" - }) - - It("fails", func() { - Expect(apiError).To(HaveOccurred()) - Expect(apiError.Detail()).To(ContainSubstring("type value must be one of: app")) - }) - }) - When("all relationships are missing", func() { BeforeEach(func() { createPayload.Relationships = nil diff --git a/api/repositories/service_binding_repository.go b/api/repositories/service_binding_repository.go index bdf516ab4..73acbb13f 100644 --- a/api/repositories/service_binding_repository.go +++ b/api/repositories/service_binding_repository.go @@ -3,6 +3,7 @@ package repositories import ( "context" "fmt" + "log" "slices" "time" @@ -31,7 +32,6 @@ import ( const ( LabelServiceBindingProvisionedService = "servicebinding.io/provisioned-service" ServiceBindingResourceType = "Service Binding" - ServiceBindingTypeApp = "app" ) type ServiceBindingRepo struct { @@ -87,6 +87,7 @@ type ServiceBindingLastOperation struct { } type CreateServiceBindingMessage struct { + Type string Name *string ServiceInstanceGUID string AppGUID string @@ -101,22 +102,28 @@ type ListServiceBindingsMessage struct { AppGUIDs []string ServiceInstanceGUIDs []string LabelSelector string + Type *string PlanGUIDs []string } func (m *ListServiceBindingsMessage) matches(serviceBinding korifiv1alpha1.CFServiceBinding) bool { return tools.EmptyOrContains(m.ServiceInstanceGUIDs, serviceBinding.Spec.Service.Name) && tools.EmptyOrContains(m.AppGUIDs, serviceBinding.Spec.AppRef.Name) && - tools.EmptyOrContains(m.PlanGUIDs, serviceBinding.Labels[korifiv1alpha1.PlanGUIDLabelKey]) + tools.EmptyOrContains(m.PlanGUIDs, serviceBinding.Labels[korifiv1alpha1.PlanGUIDLabelKey]) && + tools.NilOrEquals(m.Type, serviceBinding.Labels[korifiv1alpha1.ServiceCredentialBindingTypeLabel]) } func (m CreateServiceBindingMessage) toCFServiceBinding() *korifiv1alpha1.CFServiceBinding { guid := uuid.NewString() - return &korifiv1alpha1.CFServiceBinding{ + + binding := &korifiv1alpha1.CFServiceBinding{ ObjectMeta: metav1.ObjectMeta{ Name: guid, Namespace: m.SpaceGUID, - Labels: map[string]string{LabelServiceBindingProvisionedService: "true"}, + Labels: map[string]string{ + LabelServiceBindingProvisionedService: "true", + korifiv1alpha1.ServiceCredentialBindingTypeLabel: m.Type, + }, }, Spec: korifiv1alpha1.CFServiceBindingSpec{ DisplayName: m.Name, @@ -125,9 +132,14 @@ func (m CreateServiceBindingMessage) toCFServiceBinding() *korifiv1alpha1.CFServ APIVersion: korifiv1alpha1.GroupVersion.Identifier(), Name: m.ServiceInstanceGUID, }, - AppRef: corev1.LocalObjectReference{Name: m.AppGUID}, }, } + + if m.AppGUID != "" { + binding.Spec.AppRef = corev1.LocalObjectReference{Name: m.AppGUID} + } + + return binding } type UpdateServiceBindingMessage struct { @@ -143,16 +155,18 @@ func (r *ServiceBindingRepo) CreateServiceBinding(ctx context.Context, authInfo cfServiceBinding := message.toCFServiceBinding() - cfApp := new(korifiv1alpha1.CFApp) - err = userClient.Get(ctx, types.NamespacedName{Name: cfServiceBinding.Spec.AppRef.Name, Namespace: cfServiceBinding.Namespace}, cfApp) - if err != nil { - return ServiceBindingRecord{}, - apierrors.AsUnprocessableEntity( - apierrors.FromK8sError(err, ServiceBindingResourceType), - "Unable to use app. Ensure that the app exists and you have access to it.", - apierrors.ForbiddenError{}, - apierrors.NotFoundError{}, - ) + if message.Type == korifiv1alpha1.CFServiceBindingTypeApp { + cfApp := new(korifiv1alpha1.CFApp) + err = userClient.Get(ctx, types.NamespacedName{Name: cfServiceBinding.Spec.AppRef.Name, Namespace: cfServiceBinding.Namespace}, cfApp) + if err != nil { + return ServiceBindingRecord{}, + apierrors.AsUnprocessableEntity( + apierrors.FromK8sError(err, ServiceBindingResourceType), + "Unable to use app. Ensure that the app exists and you have access to it.", + apierrors.ForbiddenError{}, + apierrors.NotFoundError{}, + ) + } } err = userClient.Create(ctx, cfServiceBinding) @@ -179,6 +193,8 @@ func (r *ServiceBindingRepo) CreateServiceBinding(ctx context.Context, authInfo } } + log.Printf("aaa: %+v", cfServiceBinding) + return serviceBindingToRecord(*cfServiceBinding), nil } @@ -230,7 +246,7 @@ func (r *ServiceBindingRepo) GetServiceBinding(ctx context.Context, authInfo aut func serviceBindingToRecord(binding korifiv1alpha1.CFServiceBinding) ServiceBindingRecord { return ServiceBindingRecord{ GUID: binding.Name, - Type: ServiceBindingTypeApp, + Type: binding.Labels[korifiv1alpha1.ServiceCredentialBindingTypeLabel], Name: binding.Spec.DisplayName, AppGUID: binding.Spec.AppRef.Name, ServiceInstanceGUID: binding.Spec.Service.Name, diff --git a/api/repositories/service_binding_repository_test.go b/api/repositories/service_binding_repository_test.go index 4fb23ffcd..4299f1acb 100644 --- a/api/repositories/service_binding_repository_test.go +++ b/api/repositories/service_binding_repository_test.go @@ -254,6 +254,7 @@ var _ = Describe("ServiceBindingRepo", func() { JustBeforeEach(func() { serviceBindingRecord, createErr = repo.CreateServiceBinding(ctx, authInfo, repositories.CreateServiceBindingMessage{ + Type: korifiv1alpha1.CFServiceBindingTypeApp, Name: bindingName, ServiceInstanceGUID: cfServiceInstance.Name, AppGUID: appGUID, @@ -274,7 +275,7 @@ var _ = Describe("ServiceBindingRepo", func() { Expect(createErr).NotTo(HaveOccurred()) Expect(serviceBindingRecord.GUID).NotTo(BeEmpty()) - Expect(serviceBindingRecord.Type).To(Equal("app")) + Expect(serviceBindingRecord.Type).To(Equal(korifiv1alpha1.CFServiceBindingTypeApp)) Expect(serviceBindingRecord.Name).To(BeNil()) Expect(serviceBindingRecord.AppGUID).To(Equal(appGUID)) Expect(serviceBindingRecord.ServiceInstanceGUID).To(Equal(cfServiceInstance.Name)) @@ -478,7 +479,7 @@ var _ = Describe("ServiceBindingRepo", func() { }) }) - Describe("CreateManagedServiceBinding", func() { + Describe("CreateManagedServiceBinding of type key", func() { var ( cfServiceInstance *korifiv1alpha1.CFServiceInstance serviceBindingRecord repositories.ServiceBindingRecord @@ -504,6 +505,98 @@ var _ = Describe("ServiceBindingRepo", func() { JustBeforeEach(func() { serviceBindingRecord, createErr = repo.CreateServiceBinding(ctx, authInfo, repositories.CreateServiceBindingMessage{ + Type: korifiv1alpha1.CFServiceBindingTypeKey, + Name: bindingName, + ServiceInstanceGUID: cfServiceInstance.Name, + SpaceGUID: space.Name, + }) + }) + + It("returns a forbidden error", func() { + Expect(createErr).To(BeAssignableToTypeOf(apierrors.ForbiddenError{})) + }) + + When("the user can create CFServiceBindings of type key in the Space", func() { + BeforeEach(func() { + createRoleBinding(ctx, userName, spaceDeveloperRole.Name, space.Name) + }) + + It("creates a new CFServiceBinding resource and returns a record", func() { + Expect(createErr).NotTo(HaveOccurred()) + + Expect(serviceBindingRecord.GUID).NotTo(BeEmpty()) + Expect(serviceBindingRecord.Type).To(Equal(korifiv1alpha1.CFServiceBindingTypeKey)) + Expect(serviceBindingRecord.Name).To(BeNil()) + Expect(serviceBindingRecord.AppGUID).To(Equal("")) + Expect(serviceBindingRecord.ServiceInstanceGUID).To(Equal(cfServiceInstance.Name)) + Expect(serviceBindingRecord.SpaceGUID).To(Equal(space.Name)) + Expect(serviceBindingRecord.CreatedAt).NotTo(BeZero()) + Expect(serviceBindingRecord.UpdatedAt).NotTo(BeNil()) + + Expect(serviceBindingRecord.Relationships()).To(Equal(map[string]string{ + "app": "", + "service_instance": cfServiceInstance.Name, + })) + + serviceBinding := new(korifiv1alpha1.CFServiceBinding) + Expect( + k8sClient.Get(ctx, types.NamespacedName{Name: serviceBindingRecord.GUID, Namespace: space.Name}, serviceBinding), + ).To(Succeed()) + + Expect(serviceBinding.Labels).To(HaveKeyWithValue("servicebinding.io/provisioned-service", "true")) + Expect(serviceBinding.Labels).To(HaveKeyWithValue(korifiv1alpha1.ServiceCredentialBindingTypeLabel, korifiv1alpha1.CFServiceBindingTypeKey)) + Expect(serviceBinding.Spec).To(Equal( + korifiv1alpha1.CFServiceBindingSpec{ + DisplayName: nil, + Service: corev1.ObjectReference{ + Kind: "CFServiceInstance", + APIVersion: korifiv1alpha1.GroupVersion.Identifier(), + Name: cfServiceInstance.Name, + }, + }, + )) + }) + + When("The service binding has a name", func() { + BeforeEach(func() { + tempName := "some-name-for-a-binding" + bindingName = &tempName + }) + + It("creates the binding with the specified name", func() { + Expect(serviceBindingRecord.Name).To(Equal(bindingName)) + }) + }) + }) + }) + + Describe("CreateManagedServiceBinding of type app", func() { + var ( + cfServiceInstance *korifiv1alpha1.CFServiceInstance + serviceBindingRecord repositories.ServiceBindingRecord + createErr error + ) + + BeforeEach(func() { + cfServiceInstance = &korifiv1alpha1.CFServiceInstance{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: space.Name, + Name: uuid.NewString(), + }, + Spec: korifiv1alpha1.CFServiceInstanceSpec{ + Type: korifiv1alpha1.ManagedType, + }, + } + Expect( + k8sClient.Create(ctx, cfServiceInstance), + ).To(Succeed()) + + bindingName = nil + }) + + JustBeforeEach(func() { + serviceBindingRecord, createErr = repo.CreateServiceBinding(ctx, authInfo, repositories.CreateServiceBindingMessage{ + Type: korifiv1alpha1.CFServiceBindingTypeApp, Name: bindingName, ServiceInstanceGUID: cfServiceInstance.Name, AppGUID: appGUID, @@ -524,7 +617,7 @@ var _ = Describe("ServiceBindingRepo", func() { Expect(createErr).NotTo(HaveOccurred()) Expect(serviceBindingRecord.GUID).NotTo(BeEmpty()) - Expect(serviceBindingRecord.Type).To(Equal("app")) + Expect(serviceBindingRecord.Type).To(Equal(korifiv1alpha1.CFServiceBindingTypeApp)) Expect(serviceBindingRecord.Name).To(BeNil()) Expect(serviceBindingRecord.AppGUID).To(Equal(appGUID)) Expect(serviceBindingRecord.ServiceInstanceGUID).To(Equal(cfServiceInstance.Name)) @@ -543,6 +636,7 @@ var _ = Describe("ServiceBindingRepo", func() { ).To(Succeed()) Expect(serviceBinding.Labels).To(HaveKeyWithValue("servicebinding.io/provisioned-service", "true")) + Expect(serviceBinding.Labels).To(HaveKeyWithValue(korifiv1alpha1.ServiceCredentialBindingTypeLabel, korifiv1alpha1.CFServiceBindingTypeApp)) Expect(serviceBinding.Spec).To(Equal( korifiv1alpha1.CFServiceBindingSpec{ DisplayName: nil, @@ -656,10 +750,10 @@ var _ = Describe("ServiceBindingRepo", func() { Describe("ListServiceBindings", func() { var ( - serviceBinding1, serviceBinding2, serviceBinding3 *korifiv1alpha1.CFServiceBinding - space2 *korifiv1alpha1.CFSpace - cfApp1, cfApp2, cfApp3 *korifiv1alpha1.CFApp - serviceInstance1GUID, serviceInstance2GUID, serviceInstance3GUID string + serviceBinding1, serviceBinding2, serviceBinding3, serviceBinding4 *korifiv1alpha1.CFServiceBinding + space2 *korifiv1alpha1.CFSpace + cfApp1, cfApp2, cfApp3 *korifiv1alpha1.CFApp + serviceInstance1GUID, serviceInstance2GUID, serviceInstance3GUID string requestMessage repositories.ListServiceBindingsMessage responseServiceBindings []repositories.ServiceBindingRecord @@ -675,7 +769,8 @@ var _ = Describe("ServiceBindingRepo", func() { Name: prefixedGUID("binding-1"), Namespace: space.Name, Labels: map[string]string{ - korifiv1alpha1.PlanGUIDLabelKey: "plan-1", + korifiv1alpha1.PlanGUIDLabelKey: "plan-1", + korifiv1alpha1.ServiceCredentialBindingTypeLabel: korifiv1alpha1.CFServiceBindingTypeApp, }, }, Spec: korifiv1alpha1.CFServiceBindingSpec{ @@ -700,7 +795,8 @@ var _ = Describe("ServiceBindingRepo", func() { Name: prefixedGUID("binding-2"), Namespace: space2.Name, Labels: map[string]string{ - korifiv1alpha1.PlanGUIDLabelKey: "plan-2", + korifiv1alpha1.PlanGUIDLabelKey: "plan-2", + korifiv1alpha1.ServiceCredentialBindingTypeLabel: korifiv1alpha1.CFServiceBindingTypeApp, }, }, Spec: korifiv1alpha1.CFServiceBindingSpec{ @@ -724,7 +820,8 @@ var _ = Describe("ServiceBindingRepo", func() { Name: prefixedGUID("binding-3"), Namespace: space2.Name, Labels: map[string]string{ - korifiv1alpha1.PlanGUIDLabelKey: "plan-3", + korifiv1alpha1.PlanGUIDLabelKey: "plan-3", + korifiv1alpha1.ServiceCredentialBindingTypeLabel: korifiv1alpha1.CFServiceBindingTypeApp, }, }, Spec: korifiv1alpha1.CFServiceBindingSpec{ @@ -740,6 +837,25 @@ var _ = Describe("ServiceBindingRepo", func() { } Expect(k8sClient.Create(ctx, serviceBinding3)).To(Succeed()) + serviceBinding4 = &korifiv1alpha1.CFServiceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: prefixedGUID("binding-4"), + Namespace: space2.Name, + Labels: map[string]string{ + korifiv1alpha1.PlanGUIDLabelKey: "plan-4", + korifiv1alpha1.ServiceCredentialBindingTypeLabel: korifiv1alpha1.CFServiceBindingTypeKey, + }, + }, + Spec: korifiv1alpha1.CFServiceBindingSpec{ + Service: corev1.ObjectReference{ + Kind: "ServiceInstance", + Name: cfServiceInstance3.Name, + APIVersion: "korifi.cloudfoundry.org/v1alpha1", + }, + }, + } + Expect(k8sClient.Create(ctx, serviceBinding4)).To(Succeed()) + requestMessage = repositories.ListServiceBindingsMessage{} }) @@ -762,7 +878,7 @@ var _ = Describe("ServiceBindingRepo", func() { Expect(responseServiceBindings).To(ConsistOf( MatchFields(IgnoreExtras, Fields{ "GUID": Equal(serviceBinding1.Name), - "Type": Equal("app"), + "Type": Equal(korifiv1alpha1.CFServiceBindingTypeApp), "Name": Equal(serviceBinding1.Spec.DisplayName), "AppGUID": Equal(serviceBinding1.Spec.AppRef.Name), "ServiceInstanceGUID": Equal(serviceBinding1.Spec.Service.Name), @@ -770,7 +886,7 @@ var _ = Describe("ServiceBindingRepo", func() { }), MatchFields(IgnoreExtras, Fields{ "GUID": Equal(serviceBinding2.Name), - "Type": Equal("app"), + "Type": Equal(korifiv1alpha1.CFServiceBindingTypeApp), "Name": Equal(serviceBinding2.Spec.DisplayName), "AppGUID": Equal(serviceBinding2.Spec.AppRef.Name), "ServiceInstanceGUID": Equal(serviceBinding2.Spec.Service.Name), @@ -778,12 +894,19 @@ var _ = Describe("ServiceBindingRepo", func() { }), MatchFields(IgnoreExtras, Fields{ "GUID": Equal(serviceBinding3.Name), - "Type": Equal("app"), + "Type": Equal(korifiv1alpha1.CFServiceBindingTypeApp), "Name": Equal(serviceBinding3.Spec.DisplayName), "AppGUID": Equal(serviceBinding3.Spec.AppRef.Name), "ServiceInstanceGUID": Equal(serviceBinding3.Spec.Service.Name), "SpaceGUID": Equal(serviceBinding3.Namespace), }), + MatchFields(IgnoreExtras, Fields{ + "GUID": Equal(serviceBinding4.Name), + "Type": Equal(korifiv1alpha1.CFServiceBindingTypeKey), + "Name": Equal(serviceBinding4.Spec.DisplayName), + "ServiceInstanceGUID": Equal(serviceBinding4.Spec.Service.Name), + "SpaceGUID": Equal(serviceBinding4.Namespace), + }), )) }) }) @@ -791,19 +914,19 @@ var _ = Describe("ServiceBindingRepo", func() { When("filtered by service instance GUID", func() { BeforeEach(func() { requestMessage = repositories.ListServiceBindingsMessage{ - ServiceInstanceGUIDs: []string{serviceInstance2GUID, serviceInstance3GUID}, + ServiceInstanceGUIDs: []string{serviceInstance1GUID, serviceInstance2GUID}, } }) It("returns only the ServiceBindings that match the provided service instance guids", func() { Expect(responseServiceBindings).To(ConsistOf( MatchFields(IgnoreExtras, Fields{ - "GUID": Equal(serviceBinding2.Name), - "ServiceInstanceGUID": Equal(serviceInstance2GUID), + "GUID": Equal(serviceBinding1.Name), + "ServiceInstanceGUID": Equal(serviceInstance1GUID), }), MatchFields(IgnoreExtras, Fields{ - "GUID": Equal(serviceBinding3.Name), - "ServiceInstanceGUID": Equal(serviceInstance3GUID), + "GUID": Equal(serviceBinding2.Name), + "ServiceInstanceGUID": Equal(serviceInstance2GUID), }), )) }) @@ -840,6 +963,9 @@ var _ = Describe("ServiceBindingRepo", func() { Expect(k8s.PatchResource(ctx, k8sClient, serviceBinding3, func() { serviceBinding3.Labels = map[string]string{"not_foo": "NOT_FOO"} })).To(Succeed()) + Expect(k8s.PatchResource(ctx, k8sClient, serviceBinding4, func() { + serviceBinding4.Labels = map[string]string{"not_foo": "NOT_FOO"} + })).To(Succeed()) }) DescribeTable("valid label selectors", @@ -857,12 +983,12 @@ var _ = Describe("ServiceBindingRepo", func() { Expect(serviceBindings).To(ConsistOf(matchers...)) }, Entry("key", "foo", "binding-1", "binding-2"), - Entry("!key", "!foo", "binding-3"), + Entry("!key", "!foo", "binding-3", "binding-4"), Entry("key=value", "foo=FOO1", "binding-1"), Entry("key==value", "foo==FOO2", "binding-2"), - Entry("key!=value", "foo!=FOO1", "binding-2", "binding-3"), + Entry("key!=value", "foo!=FOO1", "binding-2", "binding-3", "binding-4"), Entry("key in (value1,value2)", "foo in (FOO1,FOO2)", "binding-1", "binding-2"), - Entry("key notin (value1,value2)", "foo notin (FOO2)", "binding-1", "binding-3"), + Entry("key notin (value1,value2)", "foo notin (FOO2)", "binding-1", "binding-3", "binding-4"), ) When("the label selector is invalid", func() { diff --git a/controllers/api/v1alpha1/cfservicebinding_types.go b/controllers/api/v1alpha1/cfservicebinding_types.go index af2b8144d..051d3ffe9 100644 --- a/controllers/api/v1alpha1/cfservicebinding_types.go +++ b/controllers/api/v1alpha1/cfservicebinding_types.go @@ -28,6 +28,9 @@ const ( BindingRequestedCondition = "BindingRequested" UnbindingRequestedCondition = "UnbindingRequested" + CFServiceBindingTypeKey = "key" + CFServiceBindingTypeApp = "app" + ServiceInstanceTypeAnnotationKey = "korifi.cloudfoundry.org/service-instance-type" PlanGUIDLabelKey = "korifi.cloudfoundry.org/plan-guid" diff --git a/controllers/api/v1alpha1/shared_types.go b/controllers/api/v1alpha1/shared_types.go index 4745d0041..728ef9c07 100644 --- a/controllers/api/v1alpha1/shared_types.go +++ b/controllers/api/v1alpha1/shared_types.go @@ -19,6 +19,8 @@ const ( CFRouteGUIDLabelKey = "korifi.cloudfoundry.org/route-guid" CFTaskGUIDLabelKey = "korifi.cloudfoundry.org/task-guid" + ServiceCredentialBindingTypeLabel = "korifi.cloudfoundry.org/service-credential-binding-type" + PodIndexLabelKey = "apps.kubernetes.io/pod-index" StagingConditionType = "Staging" diff --git a/controllers/controllers/services/bindings/controller.go b/controllers/controllers/services/bindings/controller.go index 1127c1785..39273c823 100644 --- a/controllers/controllers/services/bindings/controller.go +++ b/controllers/controllers/services/bindings/controller.go @@ -40,9 +40,8 @@ import ( ) const ( - ServiceBindingGUIDLabel = "korifi.cloudfoundry.org/service-binding-guid" - ServiceCredentialBindingTypeLabel = "korifi.cloudfoundry.org/service-credential-binding-type" - ServiceBindingSecretTypePrefix = "servicebinding.io/" + ServiceBindingGUIDLabel = "korifi.cloudfoundry.org/service-binding-guid" + ServiceBindingSecretTypePrefix = "servicebinding.io/" ) type CredentialsReconciler interface { @@ -145,6 +144,15 @@ func (r *Reconciler) ReconcileResource(ctx context.Context, cfServiceBinding *ko return res, err } + if cfServiceBinding.Labels[korifiv1alpha1.ServiceCredentialBindingTypeLabel] == korifiv1alpha1.CFServiceBindingTypeApp { + cfApp := new(korifiv1alpha1.CFApp) + err = r.k8sClient.Get(ctx, types.NamespacedName{Name: cfServiceBinding.Spec.AppRef.Name, Namespace: cfServiceBinding.Namespace}, cfApp) + if err != nil { + log.Info("error when fetching CFApp", "reason", err) + return ctrl.Result{}, err + } + } + sbServiceBinding, err := r.reconcileSBServiceBinding(ctx, cfServiceBinding) if err != nil { log.Info("error creating/updating servicebinding.io servicebinding", "reason", err) @@ -235,9 +243,9 @@ func (r *Reconciler) toSBServiceBinding(cfServiceBinding *korifiv1alpha1.CFServi Name: fmt.Sprintf("cf-binding-%s", cfServiceBinding.Name), Namespace: cfServiceBinding.Namespace, Labels: map[string]string{ - ServiceBindingGUIDLabel: cfServiceBinding.Name, - korifiv1alpha1.CFAppGUIDLabelKey: cfServiceBinding.Spec.AppRef.Name, - ServiceCredentialBindingTypeLabel: "app", + ServiceBindingGUIDLabel: cfServiceBinding.Name, + korifiv1alpha1.CFAppGUIDLabelKey: cfServiceBinding.Spec.AppRef.Name, + korifiv1alpha1.ServiceCredentialBindingTypeLabel: cfServiceBinding.Labels[korifiv1alpha1.ServiceCredentialBindingTypeLabel], }, }, Spec: servicebindingv1beta1.ServiceBindingSpec{ diff --git a/controllers/controllers/services/bindings/controller_test.go b/controllers/controllers/services/bindings/controller_test.go index 4fb69d3bd..79684204e 100644 --- a/controllers/controllers/services/bindings/controller_test.go +++ b/controllers/controllers/services/bindings/controller_test.go @@ -53,6 +53,9 @@ var _ = Describe("CFServiceBinding", func() { Finalizers: []string{ korifiv1alpha1.CFServiceBindingFinalizerName, }, + Labels: map[string]string{ + korifiv1alpha1.ServiceCredentialBindingTypeLabel: korifiv1alpha1.CFServiceBindingTypeApp, + }, }, Spec: korifiv1alpha1.CFServiceBindingSpec{ Service: corev1.ObjectReference{ @@ -199,8 +202,8 @@ var _ = Describe("CFServiceBinding", func() { g.Expect(sbServiceBinding.Labels).To(SatisfyAll( HaveKeyWithValue(bindings.ServiceBindingGUIDLabel, binding.Name), - HaveKeyWithValue(korifiv1alpha1.CFAppGUIDLabelKey, cfAppGUID), - HaveKeyWithValue(bindings.ServiceCredentialBindingTypeLabel, "app"), + HaveKeyWithValue(korifiv1alpha1.CFAppGUIDLabelKey, cfApp.Name), + HaveKeyWithValue(korifiv1alpha1.ServiceCredentialBindingTypeLabel, korifiv1alpha1.CFServiceBindingTypeApp) )) g.Expect(sbServiceBinding.OwnerReferences).To(ConsistOf(MatchFields(IgnoreExtras, Fields{ diff --git a/controllers/webhooks/services/bindings/validator.go b/controllers/webhooks/services/bindings/validator.go index 29991a180..ea651d140 100644 --- a/controllers/webhooks/services/bindings/validator.go +++ b/controllers/webhooks/services/bindings/validator.go @@ -47,6 +47,10 @@ func NewCFServiceBindingValidator(duplicateValidator webhooks.NameValidator) *CF func (v *CFServiceBindingValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { serviceBinding, ok := obj.(*korifiv1alpha1.CFServiceBinding) + if serviceBinding.Labels[korifiv1alpha1.ServiceCredentialBindingTypeLabel] == korifiv1alpha1.CFServiceBindingTypeKey { + return nil, nil + } + if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a CFServiceBinding but got a %T", obj)) } diff --git a/tests/assets/sample-broker-golang/main.go b/tests/assets/sample-broker-golang/main.go index 10b0aad0d..9799e0031 100644 --- a/tests/assets/sample-broker-golang/main.go +++ b/tests/assets/sample-broker-golang/main.go @@ -7,9 +7,8 @@ import ( "fmt" "net/http" "os" - "strings" - "sample-broker/osbapi" + "strings" ) const ( From e49c85b2f70fc76a3055e19acb4e570c025a187d Mon Sep 17 00:00:00 2001 From: Dimitar Draganov Date: Tue, 26 Nov 2024 09:12:10 +0200 Subject: [PATCH 2/4] Added discussed changes --- api/handlers/service_binding.go | 45 +++- api/payloads/service_binding.go | 15 +- api/payloads/service_binding_test.go | 64 +++-- .../service_binding_repository.go | 15 +- .../service_binding_repository_test.go | 254 +++++++++--------- .../api/v1alpha1/cfservicebinding_types.go | 4 + controllers/api/v1alpha1/shared_types.go | 2 +- .../services/bindings/controller.go | 11 +- .../services/bindings/controller_test.go | 8 +- .../webhooks/services/bindings/validator.go | 4 - ...fi.cloudfoundry.org_cfservicebindings.yaml | 8 + 11 files changed, 228 insertions(+), 202 deletions(-) diff --git a/api/handlers/service_binding.go b/api/handlers/service_binding.go index 06eef6e68..ad6857e18 100644 --- a/api/handlers/service_binding.go +++ b/api/handlers/service_binding.go @@ -52,8 +52,8 @@ func (h *ServiceBinding) create(r *http.Request) (*routing.Response, error) { authInfo, _ := authorization.InfoFromContext(r.Context()) logger := logr.FromContextOrDiscard(r.Context()).WithName("handlers.service-binding.create") - payload := new(payloads.ServiceBindingCreate) - if err := h.requestValidator.DecodeAndValidateJSONPayload(r, payload); err != nil { + var payload payloads.ServiceBindingCreate + if err := h.requestValidator.DecodeAndValidateJSONPayload(r, &payload); err != nil { return nil, apierrors.LogAndReturn(logger, err, "failed to decode payload") } @@ -70,9 +70,11 @@ func (h *ServiceBinding) create(r *http.Request) (*routing.Response, error) { ) } + ctx := logr.NewContext(r.Context(), logger.WithValues("service-instance", serviceInstance.GUID)) + if payload.Type == korifiv1alpha1.CFServiceBindingTypeApp { var app repositories.AppRecord - if app, err = h.appRepo.GetApp(r.Context(), authInfo, payload.Relationships.App.Data.GUID); err != nil { + if app, err = h.appRepo.GetApp(ctx, authInfo, payload.Relationships.App.Data.GUID); err != nil { return nil, apierrors.LogAndReturn(logger, apierrors.ForbiddenAsNotFound(err), "failed to get "+repositories.AppResourceType) } @@ -86,20 +88,45 @@ func (h *ServiceBinding) create(r *http.Request) (*routing.Response, error) { } } - ctx := logr.NewContext(r.Context(), logger.WithValues("service-instance", serviceInstance.GUID)) + if serviceInstance.Type == korifiv1alpha1.UserProvidedType { + return h.createUserProvided(ctx, &payload, serviceInstance) + } + + return h.createManaged(ctx, &payload, serviceInstance) +} + +func (h *ServiceBinding) createUserProvided(ctx context.Context, payload *payloads.ServiceBindingCreate, serviceInstance repositories.ServiceInstanceRecord) (*routing.Response, error) { + authInfo, _ := authorization.InfoFromContext(ctx) + logger := logr.FromContextOrDiscard(ctx).WithName("handlers.service-binding.create-user-provided") + + if payload.Type == korifiv1alpha1.CFServiceBindingTypeKey { + return nil, apierrors.LogAndReturn( + logger, + apierrors.NewUnprocessableEntityError(nil, "Service credential bindings of type 'key' are not supported for user-provided service instances."), + "", + ) + } + serviceBinding, err := h.serviceBindingRepo.CreateServiceBinding(ctx, authInfo, payload.ToMessage(serviceInstance.SpaceGUID)) if err != nil { return nil, apierrors.LogAndReturn(logr.FromContextOrDiscard(ctx), err, "failed to create ServiceBinding") } - if serviceInstance.Type == korifiv1alpha1.ManagedType { - return routing.NewResponse(http.StatusAccepted). - WithHeader("Location", presenter.JobURLForRedirects(serviceBinding.GUID, presenter.ManagedServiceBindingCreateOperation, h.serverURL)), nil - } - return routing.NewResponse(http.StatusCreated).WithBody(presenter.ForServiceBinding(serviceBinding, h.serverURL)), nil } +func (h *ServiceBinding) createManaged(ctx context.Context, payload *payloads.ServiceBindingCreate, serviceInstance repositories.ServiceInstanceRecord) (*routing.Response, error) { + authInfo, _ := authorization.InfoFromContext(ctx) + logger := logr.FromContextOrDiscard(ctx).WithName("handlers.service-binding.create-managed") + + serviceBinding, err := h.serviceBindingRepo.CreateServiceBinding(ctx, authInfo, payload.ToMessage(serviceInstance.SpaceGUID)) + if err != nil { + return nil, apierrors.LogAndReturn(logger, err, "failed to create ServiceBinding") + } + return routing.NewResponse(http.StatusAccepted). + WithHeader("Location", presenter.JobURLForRedirects(serviceBinding.GUID, presenter.ManagedServiceBindingCreateOperation, h.serverURL)), nil +} + func (h *ServiceBinding) delete(r *http.Request) (*routing.Response, error) { authInfo, _ := authorization.InfoFromContext(r.Context()) logger := logr.FromContextOrDiscard(r.Context()).WithName("handlers.service-binding.delete") diff --git a/api/payloads/service_binding.go b/api/payloads/service_binding.go index 263580893..e6e59172d 100644 --- a/api/payloads/service_binding.go +++ b/api/payloads/service_binding.go @@ -1,7 +1,7 @@ package payloads import ( - "fmt" + "errors" "net/url" "code.cloudfoundry.org/korifi/api/payloads/parse" @@ -14,7 +14,7 @@ import ( type ServiceBindingCreate struct { Relationships *ServiceBindingRelationships `json:"relationships"` Type string `json:"type"` - Name *string `json:"name"` + Name string `json:"name"` } func (p ServiceBindingCreate) ToMessage(spaceGUID string) repositories.CreateServiceBindingMessage { @@ -24,7 +24,7 @@ func (p ServiceBindingCreate) ToMessage(spaceGUID string) repositories.CreateSer } return repositories.CreateServiceBindingMessage{ - Name: p.Name, + Name: &p.Name, ServiceInstanceGUID: p.Relationships.ServiceInstance.Data.GUID, AppGUID: appGUID, SpaceGUID: spaceGUID, @@ -35,18 +35,21 @@ func (p ServiceBindingCreate) ToMessage(spaceGUID string) repositories.CreateSer func (p ServiceBindingCreate) Validate() error { return jellidation.ValidateStruct(&p, jellidation.Field(&p.Type, validation.OneOf("app", "key")), + jellidation.Field(&p.Name, jellidation.Required.When(p.Type == "key")), + jellidation.Field(&p.Relationships, jellidation.Required), + jellidation.Field(&p.Relationships, jellidation.By(func(value any) error { relationships, ok := value.(*ServiceBindingRelationships) if !ok || relationships == nil { - return fmt.Errorf("relationships is required") + return errors.New("relationships cannot be blank") } if p.Type == "app" { if relationships.App == nil { - return jellidation.NewError("when type is app", "relationships.app is required") + return jellidation.NewError("validation_required", "relationships.app cannot be blank") } if relationships.App.Data.GUID == "" { - return fmt.Errorf("relationships.app.data.guid cannot be blank") + return jellidation.NewError("validation_required", "relationships.app.data.guid cannot be blank") } } diff --git a/api/payloads/service_binding_test.go b/api/payloads/service_binding_test.go index f1a126d99..81bcfa593 100644 --- a/api/payloads/service_binding_test.go +++ b/api/payloads/service_binding_test.go @@ -78,32 +78,6 @@ var _ = Describe("ServiceBindingCreate", func() { apiError errors.ApiError ) - When("binding is of type key", func() { - BeforeEach(func() { - serviceBindingCreate = new(payloads.ServiceBindingCreate) - createPayload = payloads.ServiceBindingCreate{ - Relationships: &payloads.ServiceBindingRelationships{ - ServiceInstance: &payloads.Relationship{ - Data: &payloads.RelationshipData{ - GUID: "service-instance-guid", - }, - }, - }, - Type: "key", - } - }) - - 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))) - }) - }) - BeforeEach(func() { serviceBindingCreate = new(payloads.ServiceBindingCreate) createPayload = payloads.ServiceBindingCreate{ @@ -120,6 +94,7 @@ var _ = Describe("ServiceBindingCreate", func() { }, }, Type: "app", + Name: "service-binding-name", } }) @@ -133,6 +108,39 @@ var _ = Describe("ServiceBindingCreate", func() { Expect(serviceBindingCreate).To(gstruct.PointTo(Equal(createPayload))) }) + When("binding is key", func() { + BeforeEach(func() { + createPayload.Type = "key" + }) + + It("succeeds", func() { + Expect(validatorErr).NotTo(HaveOccurred()) + Expect(serviceBindingCreate).To(gstruct.PointTo(Equal(createPayload))) + }) + }) + + When("binding is key and name field is omitted", func() { + BeforeEach(func() { + createPayload.Name = "" + createPayload.Type = "key" + }) + + It("fails validation", func() { + Expect(apiError).To(HaveOccurred()) + Expect(apiError.Detail()).To(ContainSubstring("name cannot be blank")) + }) + }) + + When("binding is app and name field is omitted", func() { + BeforeEach(func() { + createPayload.Name = "" + }) + + It("fails validation", func() { + Expect(apiError).NotTo(HaveOccurred()) + }) + }) + When("all relationships are missing", func() { BeforeEach(func() { createPayload.Relationships = nil @@ -140,7 +148,7 @@ var _ = Describe("ServiceBindingCreate", func() { It("fails", func() { Expect(apiError).To(HaveOccurred()) - Expect(apiError.Detail()).To(ContainSubstring("relationships is required")) + Expect(apiError.Detail()).To(ContainSubstring("relationships cannot be blank")) }) }) @@ -151,7 +159,7 @@ var _ = Describe("ServiceBindingCreate", func() { It("fails", func() { Expect(apiError).To(HaveOccurred()) - Expect(apiError.Detail()).To(ContainSubstring("relationships.app is required")) + Expect(apiError.Detail()).To(ContainSubstring("relationships.app cannot be blank")) }) }) diff --git a/api/repositories/service_binding_repository.go b/api/repositories/service_binding_repository.go index 73acbb13f..add8c6242 100644 --- a/api/repositories/service_binding_repository.go +++ b/api/repositories/service_binding_repository.go @@ -3,7 +3,6 @@ package repositories import ( "context" "fmt" - "log" "slices" "time" @@ -110,7 +109,7 @@ func (m *ListServiceBindingsMessage) matches(serviceBinding korifiv1alpha1.CFSer return tools.EmptyOrContains(m.ServiceInstanceGUIDs, serviceBinding.Spec.Service.Name) && tools.EmptyOrContains(m.AppGUIDs, serviceBinding.Spec.AppRef.Name) && tools.EmptyOrContains(m.PlanGUIDs, serviceBinding.Labels[korifiv1alpha1.PlanGUIDLabelKey]) && - tools.NilOrEquals(m.Type, serviceBinding.Labels[korifiv1alpha1.ServiceCredentialBindingTypeLabel]) + tools.NilOrEquals(m.Type, serviceBinding.Spec.Type) } func (m CreateServiceBindingMessage) toCFServiceBinding() *korifiv1alpha1.CFServiceBinding { @@ -121,8 +120,7 @@ func (m CreateServiceBindingMessage) toCFServiceBinding() *korifiv1alpha1.CFServ Name: guid, Namespace: m.SpaceGUID, Labels: map[string]string{ - LabelServiceBindingProvisionedService: "true", - korifiv1alpha1.ServiceCredentialBindingTypeLabel: m.Type, + LabelServiceBindingProvisionedService: "true", }, }, Spec: korifiv1alpha1.CFServiceBindingSpec{ @@ -132,6 +130,7 @@ func (m CreateServiceBindingMessage) toCFServiceBinding() *korifiv1alpha1.CFServ APIVersion: korifiv1alpha1.GroupVersion.Identifier(), Name: m.ServiceInstanceGUID, }, + Type: m.Type, }, } @@ -180,8 +179,8 @@ 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) + var cfServiceInstance 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) } @@ -193,8 +192,6 @@ func (r *ServiceBindingRepo) CreateServiceBinding(ctx context.Context, authInfo } } - log.Printf("aaa: %+v", cfServiceBinding) - return serviceBindingToRecord(*cfServiceBinding), nil } @@ -246,7 +243,7 @@ func (r *ServiceBindingRepo) GetServiceBinding(ctx context.Context, authInfo aut func serviceBindingToRecord(binding korifiv1alpha1.CFServiceBinding) ServiceBindingRecord { return ServiceBindingRecord{ GUID: binding.Name, - Type: binding.Labels[korifiv1alpha1.ServiceCredentialBindingTypeLabel], + Type: binding.Spec.Type, Name: binding.Spec.DisplayName, AppGUID: binding.Spec.AppRef.Name, ServiceInstanceGUID: binding.Spec.Service.Name, diff --git a/api/repositories/service_binding_repository_test.go b/api/repositories/service_binding_repository_test.go index 4299f1acb..bc806b481 100644 --- a/api/repositories/service_binding_repository_test.go +++ b/api/repositories/service_binding_repository_test.go @@ -479,11 +479,12 @@ var _ = Describe("ServiceBindingRepo", func() { }) }) - Describe("CreateManagedServiceBinding of type key", func() { + Describe("CreateManagedServiceBinding", func() { var ( cfServiceInstance *korifiv1alpha1.CFServiceInstance serviceBindingRecord repositories.ServiceBindingRecord createErr error + createMsg repositories.CreateServiceBindingMessage ) BeforeEach(func() { @@ -504,152 +505,141 @@ var _ = Describe("ServiceBindingRepo", func() { }) JustBeforeEach(func() { - serviceBindingRecord, createErr = repo.CreateServiceBinding(ctx, authInfo, repositories.CreateServiceBindingMessage{ - Type: korifiv1alpha1.CFServiceBindingTypeKey, - Name: bindingName, - ServiceInstanceGUID: cfServiceInstance.Name, - SpaceGUID: space.Name, - }) + serviceBindingRecord, createErr = repo.CreateServiceBinding(ctx, authInfo, createMsg) }) - It("returns a forbidden error", func() { - Expect(createErr).To(BeAssignableToTypeOf(apierrors.ForbiddenError{})) - }) - - When("the user can create CFServiceBindings of type key in the Space", func() { + Describe("type key", func() { BeforeEach(func() { - createRoleBinding(ctx, userName, spaceDeveloperRole.Name, space.Name) + createMsg = repositories.CreateServiceBindingMessage{ + Type: korifiv1alpha1.CFServiceBindingTypeApp, + Name: bindingName, + ServiceInstanceGUID: cfServiceInstance.Name, + SpaceGUID: space.Name, + } }) - It("creates a new CFServiceBinding resource and returns a record", func() { - Expect(createErr).NotTo(HaveOccurred()) - - Expect(serviceBindingRecord.GUID).NotTo(BeEmpty()) - Expect(serviceBindingRecord.Type).To(Equal(korifiv1alpha1.CFServiceBindingTypeKey)) - Expect(serviceBindingRecord.Name).To(BeNil()) - Expect(serviceBindingRecord.AppGUID).To(Equal("")) - Expect(serviceBindingRecord.ServiceInstanceGUID).To(Equal(cfServiceInstance.Name)) - Expect(serviceBindingRecord.SpaceGUID).To(Equal(space.Name)) - Expect(serviceBindingRecord.CreatedAt).NotTo(BeZero()) - Expect(serviceBindingRecord.UpdatedAt).NotTo(BeNil()) - - Expect(serviceBindingRecord.Relationships()).To(Equal(map[string]string{ - "app": "", - "service_instance": cfServiceInstance.Name, - })) - - serviceBinding := new(korifiv1alpha1.CFServiceBinding) - Expect( - k8sClient.Get(ctx, types.NamespacedName{Name: serviceBindingRecord.GUID, Namespace: space.Name}, serviceBinding), - ).To(Succeed()) - - Expect(serviceBinding.Labels).To(HaveKeyWithValue("servicebinding.io/provisioned-service", "true")) - Expect(serviceBinding.Labels).To(HaveKeyWithValue(korifiv1alpha1.ServiceCredentialBindingTypeLabel, korifiv1alpha1.CFServiceBindingTypeKey)) - Expect(serviceBinding.Spec).To(Equal( - korifiv1alpha1.CFServiceBindingSpec{ - DisplayName: nil, - Service: corev1.ObjectReference{ - Kind: "CFServiceInstance", - APIVersion: korifiv1alpha1.GroupVersion.Identifier(), - Name: cfServiceInstance.Name, - }, - }, - )) + When("the user is not allowed to create CFServiceBindings", func() { + It("returns a forbidden error", func() { + Expect(createErr).To(BeAssignableToTypeOf(apierrors.ForbiddenError{})) + }) }) - When("The service binding has a name", func() { + When("the user is allowed to create CFServiceBindings", func() { BeforeEach(func() { - tempName := "some-name-for-a-binding" - bindingName = &tempName + createRoleBinding(ctx, userName, spaceDeveloperRole.Name, space.Name) }) - It("creates the binding with the specified name", func() { - Expect(serviceBindingRecord.Name).To(Equal(bindingName)) + It("creates a new CFServiceBinding resource and returns a record", func() { + Expect(createErr).NotTo(HaveOccurred()) + + Expect(serviceBindingRecord.GUID).NotTo(BeEmpty()) + Expect(serviceBindingRecord.Type).To(Equal(korifiv1alpha1.CFServiceBindingTypeKey)) + Expect(serviceBindingRecord.Name).To(BeNil()) + Expect(serviceBindingRecord.AppGUID).To(Equal("")) + Expect(serviceBindingRecord.ServiceInstanceGUID).To(Equal(cfServiceInstance.Name)) + Expect(serviceBindingRecord.SpaceGUID).To(Equal(space.Name)) + Expect(serviceBindingRecord.CreatedAt).NotTo(BeZero()) + Expect(serviceBindingRecord.UpdatedAt).NotTo(BeNil()) + + Expect(serviceBindingRecord.Relationships()).To(Equal(map[string]string{ + "app": "", + "service_instance": cfServiceInstance.Name, + })) + + serviceBinding := new(korifiv1alpha1.CFServiceBinding) + Expect( + k8sClient.Get(ctx, types.NamespacedName{Name: serviceBindingRecord.GUID, Namespace: space.Name}, serviceBinding), + ).To(Succeed()) + + Expect(serviceBinding.Labels).To(HaveKeyWithValue("servicebinding.io/provisioned-service", "true")) + Expect(serviceBinding.Spec.Type).To(Equal(korifiv1alpha1.CFServiceBindingTypeKey)) + Expect(serviceBinding.Spec).To(Equal( + korifiv1alpha1.CFServiceBindingSpec{ + DisplayName: nil, + Service: corev1.ObjectReference{ + Kind: "CFServiceInstance", + APIVersion: korifiv1alpha1.GroupVersion.Identifier(), + Name: cfServiceInstance.Name, + }, + }, + )) }) }) - }) - }) - Describe("CreateManagedServiceBinding of type app", func() { - var ( - cfServiceInstance *korifiv1alpha1.CFServiceInstance - serviceBindingRecord repositories.ServiceBindingRecord - createErr error - ) + When("the binding has no name", func() { + BeforeEach(func() { + cfServiceInstance.Name = "" + }) - BeforeEach(func() { - cfServiceInstance = &korifiv1alpha1.CFServiceInstance{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: space.Name, - Name: uuid.NewString(), - }, - Spec: korifiv1alpha1.CFServiceInstanceSpec{ - Type: korifiv1alpha1.ManagedType, - }, - } - Expect( - k8sClient.Create(ctx, cfServiceInstance), - ).To(Succeed()) + It("fails the validation", func() { - bindingName = nil - }) + }) - JustBeforeEach(func() { - serviceBindingRecord, createErr = repo.CreateServiceBinding(ctx, authInfo, repositories.CreateServiceBindingMessage{ - Type: korifiv1alpha1.CFServiceBindingTypeApp, - Name: bindingName, - ServiceInstanceGUID: cfServiceInstance.Name, - AppGUID: appGUID, - SpaceGUID: space.Name, }) }) - It("returns a forbidden error", func() { - Expect(createErr).To(BeAssignableToTypeOf(apierrors.UnprocessableEntityError{})) - }) + Describe("type app", func() { - When("the user can create CFServiceBindings in the Space", func() { - BeforeEach(func() { - createRoleBinding(ctx, userName, spaceDeveloperRole.Name, space.Name) - }) - - It("creates a new CFServiceBinding resource and returns a record", func() { - Expect(createErr).NotTo(HaveOccurred()) - - Expect(serviceBindingRecord.GUID).NotTo(BeEmpty()) - Expect(serviceBindingRecord.Type).To(Equal(korifiv1alpha1.CFServiceBindingTypeApp)) - Expect(serviceBindingRecord.Name).To(BeNil()) - Expect(serviceBindingRecord.AppGUID).To(Equal(appGUID)) - Expect(serviceBindingRecord.ServiceInstanceGUID).To(Equal(cfServiceInstance.Name)) - Expect(serviceBindingRecord.SpaceGUID).To(Equal(space.Name)) - Expect(serviceBindingRecord.CreatedAt).NotTo(BeZero()) - Expect(serviceBindingRecord.UpdatedAt).NotTo(BeNil()) + When("the user is not allowed to create CFServiceBindings", func() { + BeforeEach(func() { + createMsg = repositories.CreateServiceBindingMessage{ + Type: korifiv1alpha1.CFServiceBindingTypeApp, + Name: bindingName, + ServiceInstanceGUID: cfServiceInstance.Name, + AppGUID: appGUID, + SpaceGUID: space.Name, + } + }) - Expect(serviceBindingRecord.Relationships()).To(Equal(map[string]string{ - "app": appGUID, - "service_instance": cfServiceInstance.Name, - })) + It("returns a forbidden error", func() { + Expect(createErr).To(BeAssignableToTypeOf(apierrors.ForbiddenError{})) + }) + }) - serviceBinding := new(korifiv1alpha1.CFServiceBinding) - Expect( - k8sClient.Get(ctx, types.NamespacedName{Name: serviceBindingRecord.GUID, Namespace: space.Name}, serviceBinding), - ).To(Succeed()) + When("the user is allowed to create CFServiceBindings", func() { + BeforeEach(func() { + createRoleBinding(ctx, userName, spaceDeveloperRole.Name, space.Name) + }) - Expect(serviceBinding.Labels).To(HaveKeyWithValue("servicebinding.io/provisioned-service", "true")) - Expect(serviceBinding.Labels).To(HaveKeyWithValue(korifiv1alpha1.ServiceCredentialBindingTypeLabel, korifiv1alpha1.CFServiceBindingTypeApp)) - Expect(serviceBinding.Spec).To(Equal( - korifiv1alpha1.CFServiceBindingSpec{ - DisplayName: nil, - Service: corev1.ObjectReference{ - Kind: "CFServiceInstance", - APIVersion: korifiv1alpha1.GroupVersion.Identifier(), - Name: cfServiceInstance.Name, - }, - AppRef: corev1.LocalObjectReference{ - Name: appGUID, + It("creates a new CFServiceBinding resource and returns a record", func() { + Expect(createErr).NotTo(HaveOccurred()) + + Expect(serviceBindingRecord.GUID).NotTo(BeEmpty()) + Expect(serviceBindingRecord.Type).To(Equal(korifiv1alpha1.CFServiceBindingTypeApp)) + Expect(serviceBindingRecord.Name).To(BeNil()) + Expect(serviceBindingRecord.AppGUID).To(Equal(appGUID)) + Expect(serviceBindingRecord.ServiceInstanceGUID).To(Equal(cfServiceInstance.Name)) + Expect(serviceBindingRecord.SpaceGUID).To(Equal(space.Name)) + Expect(serviceBindingRecord.CreatedAt).NotTo(BeZero()) + Expect(serviceBindingRecord.UpdatedAt).NotTo(BeNil()) + + Expect(serviceBindingRecord.Relationships()).To(Equal(map[string]string{ + "app": appGUID, + "service_instance": cfServiceInstance.Name, + })) + + serviceBinding := new(korifiv1alpha1.CFServiceBinding) + Expect( + k8sClient.Get(ctx, types.NamespacedName{Name: serviceBindingRecord.GUID, Namespace: space.Name}, serviceBinding), + ).To(Succeed()) + + Expect(serviceBinding.Labels).To(HaveKeyWithValue("servicebinding.io/provisioned-service", "true")) + Expect(serviceBinding.Spec.Type).To(Equal(korifiv1alpha1.CFServiceBindingTypeKey)) + Expect(serviceBinding.Spec).To(Equal( + korifiv1alpha1.CFServiceBindingSpec{ + DisplayName: nil, + Service: corev1.ObjectReference{ + Kind: "CFServiceInstance", + APIVersion: korifiv1alpha1.GroupVersion.Identifier(), + Name: cfServiceInstance.Name, + }, + AppRef: corev1.LocalObjectReference{ + Name: appGUID, + }, }, - }, - )) + )) + }) + }) When("the app does not exist", func() { @@ -657,7 +647,7 @@ var _ = Describe("ServiceBindingRepo", func() { appGUID = "i-do-not-exits" }) - It("reuturns an UnprocessableEntity error", func() { + It("returns an UnprocessableEntity error", func() { Expect(createErr).To(BeAssignableToTypeOf(apierrors.UnprocessableEntityError{})) }) }) @@ -769,8 +759,7 @@ var _ = Describe("ServiceBindingRepo", func() { Name: prefixedGUID("binding-1"), Namespace: space.Name, Labels: map[string]string{ - korifiv1alpha1.PlanGUIDLabelKey: "plan-1", - korifiv1alpha1.ServiceCredentialBindingTypeLabel: korifiv1alpha1.CFServiceBindingTypeApp, + korifiv1alpha1.PlanGUIDLabelKey: "plan-1", }, }, Spec: korifiv1alpha1.CFServiceBindingSpec{ @@ -782,6 +771,7 @@ var _ = Describe("ServiceBindingRepo", func() { AppRef: corev1.LocalObjectReference{ Name: cfApp1.Name, }, + Type: korifiv1alpha1.CFServiceBindingTypeApp, }, } Expect(k8sClient.Create(ctx, serviceBinding1)).To(Succeed()) @@ -795,8 +785,7 @@ var _ = Describe("ServiceBindingRepo", func() { Name: prefixedGUID("binding-2"), Namespace: space2.Name, Labels: map[string]string{ - korifiv1alpha1.PlanGUIDLabelKey: "plan-2", - korifiv1alpha1.ServiceCredentialBindingTypeLabel: korifiv1alpha1.CFServiceBindingTypeApp, + korifiv1alpha1.PlanGUIDLabelKey: "plan-2", }, }, Spec: korifiv1alpha1.CFServiceBindingSpec{ @@ -808,6 +797,7 @@ var _ = Describe("ServiceBindingRepo", func() { AppRef: corev1.LocalObjectReference{ Name: cfApp2.Name, }, + Type: korifiv1alpha1.CFServiceBindingTypeApp, }, } Expect(k8sClient.Create(ctx, serviceBinding2)).To(Succeed()) @@ -820,8 +810,7 @@ var _ = Describe("ServiceBindingRepo", func() { Name: prefixedGUID("binding-3"), Namespace: space2.Name, Labels: map[string]string{ - korifiv1alpha1.PlanGUIDLabelKey: "plan-3", - korifiv1alpha1.ServiceCredentialBindingTypeLabel: korifiv1alpha1.CFServiceBindingTypeApp, + korifiv1alpha1.PlanGUIDLabelKey: "plan-3", }, }, Spec: korifiv1alpha1.CFServiceBindingSpec{ @@ -833,6 +822,7 @@ var _ = Describe("ServiceBindingRepo", func() { AppRef: corev1.LocalObjectReference{ Name: cfApp3.Name, }, + Type: korifiv1alpha1.CFServiceBindingTypeApp, }, } Expect(k8sClient.Create(ctx, serviceBinding3)).To(Succeed()) @@ -842,8 +832,7 @@ var _ = Describe("ServiceBindingRepo", func() { Name: prefixedGUID("binding-4"), Namespace: space2.Name, Labels: map[string]string{ - korifiv1alpha1.PlanGUIDLabelKey: "plan-4", - korifiv1alpha1.ServiceCredentialBindingTypeLabel: korifiv1alpha1.CFServiceBindingTypeKey, + korifiv1alpha1.PlanGUIDLabelKey: "plan-4", }, }, Spec: korifiv1alpha1.CFServiceBindingSpec{ @@ -852,6 +841,7 @@ var _ = Describe("ServiceBindingRepo", func() { Name: cfServiceInstance3.Name, APIVersion: "korifi.cloudfoundry.org/v1alpha1", }, + Type: korifiv1alpha1.CFServiceBindingTypeKey, }, } Expect(k8sClient.Create(ctx, serviceBinding4)).To(Succeed()) diff --git a/controllers/api/v1alpha1/cfservicebinding_types.go b/controllers/api/v1alpha1/cfservicebinding_types.go index 051d3ffe9..4b611b94e 100644 --- a/controllers/api/v1alpha1/cfservicebinding_types.go +++ b/controllers/api/v1alpha1/cfservicebinding_types.go @@ -47,6 +47,10 @@ type CFServiceBindingSpec struct { // A reference to the CFApp that owns this service binding. The CFApp must be in the same namespace AppRef v1.LocalObjectReference `json:"appRef"` + + // The type of the binding. There are two possible values - "key" or "app" + // +kubebuilder:validation:Enum=app;key + Type string `json:"type"` } // CFServiceBindingStatus defines the observed state of CFServiceBinding diff --git a/controllers/api/v1alpha1/shared_types.go b/controllers/api/v1alpha1/shared_types.go index 728ef9c07..710cf430e 100644 --- a/controllers/api/v1alpha1/shared_types.go +++ b/controllers/api/v1alpha1/shared_types.go @@ -19,7 +19,7 @@ const ( CFRouteGUIDLabelKey = "korifi.cloudfoundry.org/route-guid" CFTaskGUIDLabelKey = "korifi.cloudfoundry.org/task-guid" - ServiceCredentialBindingTypeLabel = "korifi.cloudfoundry.org/service-credential-binding-type" + ServiceCredentialBindingTypeLabel = "korifi.cloudfoundry.org/service-binding-type" PodIndexLabelKey = "apps.kubernetes.io/pod-index" diff --git a/controllers/controllers/services/bindings/controller.go b/controllers/controllers/services/bindings/controller.go index 39273c823..5ea8273be 100644 --- a/controllers/controllers/services/bindings/controller.go +++ b/controllers/controllers/services/bindings/controller.go @@ -144,13 +144,8 @@ func (r *Reconciler) ReconcileResource(ctx context.Context, cfServiceBinding *ko return res, err } - if cfServiceBinding.Labels[korifiv1alpha1.ServiceCredentialBindingTypeLabel] == korifiv1alpha1.CFServiceBindingTypeApp { - cfApp := new(korifiv1alpha1.CFApp) - err = r.k8sClient.Get(ctx, types.NamespacedName{Name: cfServiceBinding.Spec.AppRef.Name, Namespace: cfServiceBinding.Namespace}, cfApp) - if err != nil { - log.Info("error when fetching CFApp", "reason", err) - return ctrl.Result{}, err - } + if cfServiceBinding.Spec.Type == korifiv1alpha1.CFServiceBindingTypeKey { + return ctrl.Result{}, nil } sbServiceBinding, err := r.reconcileSBServiceBinding(ctx, cfServiceBinding) @@ -245,7 +240,7 @@ func (r *Reconciler) toSBServiceBinding(cfServiceBinding *korifiv1alpha1.CFServi Labels: map[string]string{ ServiceBindingGUIDLabel: cfServiceBinding.Name, korifiv1alpha1.CFAppGUIDLabelKey: cfServiceBinding.Spec.AppRef.Name, - korifiv1alpha1.ServiceCredentialBindingTypeLabel: cfServiceBinding.Labels[korifiv1alpha1.ServiceCredentialBindingTypeLabel], + korifiv1alpha1.ServiceCredentialBindingTypeLabel: cfServiceBinding.Spec.Type, }, }, Spec: servicebindingv1beta1.ServiceBindingSpec{ diff --git a/controllers/controllers/services/bindings/controller_test.go b/controllers/controllers/services/bindings/controller_test.go index 79684204e..ddcc9e12f 100644 --- a/controllers/controllers/services/bindings/controller_test.go +++ b/controllers/controllers/services/bindings/controller_test.go @@ -53,9 +53,6 @@ var _ = Describe("CFServiceBinding", func() { Finalizers: []string{ korifiv1alpha1.CFServiceBindingFinalizerName, }, - Labels: map[string]string{ - korifiv1alpha1.ServiceCredentialBindingTypeLabel: korifiv1alpha1.CFServiceBindingTypeApp, - }, }, Spec: korifiv1alpha1.CFServiceBindingSpec{ Service: corev1.ObjectReference{ @@ -66,6 +63,7 @@ var _ = Describe("CFServiceBinding", func() { AppRef: corev1.LocalObjectReference{ Name: cfAppGUID, }, + Type: korifiv1alpha1.CFServiceBindingTypeApp, }, } Expect(adminClient.Create(ctx, binding)).To(Succeed()) @@ -199,11 +197,11 @@ var _ = Describe("CFServiceBinding", func() { g.Expect(sbServiceBinding.Spec.Name).To(Equal(binding.Name)) g.Expect(sbServiceBinding.Spec.Type).To(Equal("user-provided")) g.Expect(sbServiceBinding.Spec.Provider).To(BeEmpty()) + g.Expect(sbServiceBinding.Spec.Type).To(Equal(korifiv1alpha1.CFServiceBindingTypeApp)) g.Expect(sbServiceBinding.Labels).To(SatisfyAll( HaveKeyWithValue(bindings.ServiceBindingGUIDLabel, binding.Name), - HaveKeyWithValue(korifiv1alpha1.CFAppGUIDLabelKey, cfApp.Name), - HaveKeyWithValue(korifiv1alpha1.ServiceCredentialBindingTypeLabel, korifiv1alpha1.CFServiceBindingTypeApp) + HaveKeyWithValue(korifiv1alpha1.CFAppGUIDLabelKey, cfAppGUID), )) g.Expect(sbServiceBinding.OwnerReferences).To(ConsistOf(MatchFields(IgnoreExtras, Fields{ diff --git a/controllers/webhooks/services/bindings/validator.go b/controllers/webhooks/services/bindings/validator.go index ea651d140..29991a180 100644 --- a/controllers/webhooks/services/bindings/validator.go +++ b/controllers/webhooks/services/bindings/validator.go @@ -47,10 +47,6 @@ func NewCFServiceBindingValidator(duplicateValidator webhooks.NameValidator) *CF func (v *CFServiceBindingValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { serviceBinding, ok := obj.(*korifiv1alpha1.CFServiceBinding) - if serviceBinding.Labels[korifiv1alpha1.ServiceCredentialBindingTypeLabel] == korifiv1alpha1.CFServiceBindingTypeKey { - return nil, nil - } - if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a CFServiceBinding but got a %T", obj)) } diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfservicebindings.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfservicebindings.yaml index 3282636eb..38a927036 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfservicebindings.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfservicebindings.yaml @@ -109,9 +109,17 @@ spec: type: string type: object x-kubernetes-map-type: atomic + type: + description: The type of the binding. There are two possible values + - "key" or "app" + enum: + - app + - key + type: string required: - appRef - service + - type type: object status: description: CFServiceBindingStatus defines the observed state of CFServiceBinding From 8ff4258929c306497e6117b14ebed09bd36f1f0f Mon Sep 17 00:00:00 2001 From: Dimitar Draganov Date: Tue, 26 Nov 2024 10:24:49 +0200 Subject: [PATCH 3/4] Fixed build errors related to a recent name change --- api/repositories/service_binding_repository_test.go | 6 +++--- controllers/api/v1alpha1/groupversion_info.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/repositories/service_binding_repository_test.go b/api/repositories/service_binding_repository_test.go index f18a59a78..8b3f80c88 100644 --- a/api/repositories/service_binding_repository_test.go +++ b/api/repositories/service_binding_repository_test.go @@ -551,7 +551,7 @@ var _ = Describe("ServiceBindingRepo", func() { Expect( k8sClient.Get(ctx, types.NamespacedName{Name: serviceBindingRecord.GUID, Namespace: space.Name}, serviceBinding), ).To(Succeed()) - + korifiv1alpha1.SchemeGroupVersion.Identifier() Expect(serviceBinding.Labels).To(HaveKeyWithValue("servicebinding.io/provisioned-service", "true")) Expect(serviceBinding.Spec.Type).To(Equal(korifiv1alpha1.CFServiceBindingTypeKey)) Expect(serviceBinding.Spec).To(Equal( @@ -559,7 +559,7 @@ var _ = Describe("ServiceBindingRepo", func() { DisplayName: nil, Service: corev1.ObjectReference{ Kind: "CFServiceInstance", - APIVersion: korifiv1alpha1.GroupVersion.Identifier(), + APIVersion: korifiv1alpha1.SchemeGroupVersion.Identifier(), Name: cfServiceInstance.Name, }, }, @@ -631,7 +631,7 @@ var _ = Describe("ServiceBindingRepo", func() { DisplayName: nil, Service: corev1.ObjectReference{ Kind: "CFServiceInstance", - APIVersion: korifiv1alpha1.GroupVersion.Identifier(), + APIVersion: korifiv1alpha1.SchemeGroupVersion.Identifier(), Name: cfServiceInstance.Name, }, AppRef: corev1.LocalObjectReference{ diff --git a/controllers/api/v1alpha1/groupversion_info.go b/controllers/api/v1alpha1/groupversion_info.go index 531823776..713759e8e 100644 --- a/controllers/api/v1alpha1/groupversion_info.go +++ b/controllers/api/v1alpha1/groupversion_info.go @@ -6,7 +6,7 @@ import ( ) var ( - // GroupVersion is group version used to register these objects + // SchemeGroupVersion is group version used to register these objects SchemeGroupVersion = schema.GroupVersion{Group: "korifi.cloudfoundry.org", Version: "v1alpha1"} // SchemeBuilder is used to add go types to the GroupVersionKind scheme From 15f51138cad5c7516222d6acf52b17ed1bfe4e5b Mon Sep 17 00:00:00 2001 From: Dimitar Draganov Date: Tue, 3 Dec 2024 17:10:21 +0200 Subject: [PATCH 4/4] Fixed the bugs --- api/handlers/service_binding.go | 8 -- .../service_binding_repository_test.go | 75 +++++++------------ .../services/bindings/controller_test.go | 2 +- 3 files changed, 26 insertions(+), 59 deletions(-) diff --git a/api/handlers/service_binding.go b/api/handlers/service_binding.go index ad6857e18..ab9e053c4 100644 --- a/api/handlers/service_binding.go +++ b/api/handlers/service_binding.go @@ -62,14 +62,6 @@ func (h *ServiceBinding) create(r *http.Request) (*routing.Response, error) { return nil, apierrors.LogAndReturn(logger, apierrors.ForbiddenAsNotFound(err), "failed to get "+repositories.ServiceInstanceResourceType) } - if payload.Type == korifiv1alpha1.CFServiceBindingTypeKey && serviceInstance.Type != korifiv1alpha1.ManagedType { - return nil, apierrors.LogAndReturn( - logger, - apierrors.NewUnprocessableEntityError(nil, "Service credential bindings of type 'key' are not supported for user-provided service instances."), - "", - ) - } - ctx := logr.NewContext(r.Context(), logger.WithValues("service-instance", serviceInstance.GUID)) if payload.Type == korifiv1alpha1.CFServiceBindingTypeApp { diff --git a/api/repositories/service_binding_repository_test.go b/api/repositories/service_binding_repository_test.go index 8b3f80c88..cc7c3f7f5 100644 --- a/api/repositories/service_binding_repository_test.go +++ b/api/repositories/service_binding_repository_test.go @@ -486,6 +486,7 @@ var _ = Describe("ServiceBindingRepo", func() { serviceBindingRecord repositories.ServiceBindingRecord createErr error createMsg repositories.CreateServiceBindingMessage + serviceBindingName string = "service-binding-name" ) BeforeEach(func() { @@ -502,7 +503,13 @@ var _ = Describe("ServiceBindingRepo", func() { k8sClient.Create(ctx, cfServiceInstance), ).To(Succeed()) - bindingName = nil + createMsg = repositories.CreateServiceBindingMessage{ + Type: korifiv1alpha1.CFServiceBindingTypeApp, + Name: &serviceBindingName, + ServiceInstanceGUID: cfServiceInstance.Name, + AppGUID: appGUID, + SpaceGUID: space.Name, + } }) JustBeforeEach(func() { @@ -511,12 +518,7 @@ var _ = Describe("ServiceBindingRepo", func() { Describe("type key", func() { BeforeEach(func() { - createMsg = repositories.CreateServiceBindingMessage{ - Type: korifiv1alpha1.CFServiceBindingTypeApp, - Name: bindingName, - ServiceInstanceGUID: cfServiceInstance.Name, - SpaceGUID: space.Name, - } + createMsg.Type = korifiv1alpha1.CFServiceBindingTypeKey }) When("the user is not allowed to create CFServiceBindings", func() { @@ -532,16 +534,14 @@ var _ = Describe("ServiceBindingRepo", func() { It("creates a new CFServiceBinding resource and returns a record", func() { Expect(createErr).NotTo(HaveOccurred()) - Expect(serviceBindingRecord.GUID).NotTo(BeEmpty()) Expect(serviceBindingRecord.Type).To(Equal(korifiv1alpha1.CFServiceBindingTypeKey)) - Expect(serviceBindingRecord.Name).To(BeNil()) + Expect(*(serviceBindingRecord.Name)).To(Equal(serviceBindingName)) Expect(serviceBindingRecord.AppGUID).To(Equal("")) Expect(serviceBindingRecord.ServiceInstanceGUID).To(Equal(cfServiceInstance.Name)) Expect(serviceBindingRecord.SpaceGUID).To(Equal(space.Name)) Expect(serviceBindingRecord.CreatedAt).NotTo(BeZero()) Expect(serviceBindingRecord.UpdatedAt).NotTo(BeNil()) - Expect(serviceBindingRecord.Relationships()).To(Equal(map[string]string{ "app": "", "service_instance": cfServiceInstance.Name, @@ -551,49 +551,24 @@ var _ = Describe("ServiceBindingRepo", func() { Expect( k8sClient.Get(ctx, types.NamespacedName{Name: serviceBindingRecord.GUID, Namespace: space.Name}, serviceBinding), ).To(Succeed()) - korifiv1alpha1.SchemeGroupVersion.Identifier() Expect(serviceBinding.Labels).To(HaveKeyWithValue("servicebinding.io/provisioned-service", "true")) Expect(serviceBinding.Spec.Type).To(Equal(korifiv1alpha1.CFServiceBindingTypeKey)) - Expect(serviceBinding.Spec).To(Equal( - korifiv1alpha1.CFServiceBindingSpec{ - DisplayName: nil, - Service: corev1.ObjectReference{ - Kind: "CFServiceInstance", - APIVersion: korifiv1alpha1.SchemeGroupVersion.Identifier(), - Name: cfServiceInstance.Name, - }, - }, - )) + Expect(*(serviceBinding.Spec.DisplayName)).To(Equal(serviceBindingName)) + Expect(serviceBinding.Spec.Service.Kind).To(Equal("CFServiceInstance")) + Expect(serviceBinding.Spec.Service.Name).To(Equal(cfServiceInstance.Name)) + Expect(serviceBinding.Spec.Service.APIVersion).To(Equal(korifiv1alpha1.SchemeGroupVersion.Identifier())) }) }) - - When("the binding has no name", func() { - BeforeEach(func() { - cfServiceInstance.Name = "" - }) - - It("fails the validation", func() { - - }) - - }) }) Describe("type app", func() { + BeforeEach(func() { + createMsg.Type = korifiv1alpha1.CFServiceBindingTypeApp + }) When("the user is not allowed to create CFServiceBindings", func() { - BeforeEach(func() { - createMsg = repositories.CreateServiceBindingMessage{ - Type: korifiv1alpha1.CFServiceBindingTypeApp, - Name: bindingName, - ServiceInstanceGUID: cfServiceInstance.Name, - AppGUID: appGUID, - SpaceGUID: space.Name, - } - }) - It("returns a forbidden error", func() { - Expect(createErr).To(BeAssignableToTypeOf(apierrors.ForbiddenError{})) + Expect(createErr).To(BeAssignableToTypeOf(apierrors.UnprocessableEntityError{})) }) }) @@ -613,7 +588,6 @@ var _ = Describe("ServiceBindingRepo", func() { Expect(serviceBindingRecord.SpaceGUID).To(Equal(space.Name)) Expect(serviceBindingRecord.CreatedAt).NotTo(BeZero()) Expect(serviceBindingRecord.UpdatedAt).NotTo(BeNil()) - Expect(serviceBindingRecord.Relationships()).To(Equal(map[string]string{ "app": appGUID, "service_instance": cfServiceInstance.Name, @@ -625,7 +599,7 @@ var _ = Describe("ServiceBindingRepo", func() { ).To(Succeed()) Expect(serviceBinding.Labels).To(HaveKeyWithValue("servicebinding.io/provisioned-service", "true")) - Expect(serviceBinding.Spec.Type).To(Equal(korifiv1alpha1.CFServiceBindingTypeKey)) + Expect(serviceBinding.Spec.Type).To(Equal(korifiv1alpha1.CFServiceBindingTypeApp)) Expect(serviceBinding.Spec).To(Equal( korifiv1alpha1.CFServiceBindingSpec{ DisplayName: nil, @@ -637,10 +611,10 @@ var _ = Describe("ServiceBindingRepo", func() { AppRef: corev1.LocalObjectReference{ Name: appGUID, }, + Type: korifiv1alpha1.CFServiceBindingTypeApp, }, )) }) - }) When("the app does not exist", func() { @@ -653,14 +627,14 @@ var _ = Describe("ServiceBindingRepo", func() { }) }) - When("The service binding has a name", func() { + When("the service binding has a name", func() { BeforeEach(func() { - tempName := "some-name-for-a-binding" - bindingName = &tempName + createRoleBinding(ctx, userName, spaceDeveloperRole.Name, space.Name) + createMsg.Name = &serviceBindingName }) It("creates the binding with the specified name", func() { - Expect(serviceBindingRecord.Name).To(Equal(bindingName)) + Expect(*(serviceBindingRecord.Name)).To(Equal(serviceBindingName)) }) }) }) @@ -1057,6 +1031,7 @@ var _ = Describe("ServiceBindingRepo", func() { APIVersion: korifiv1alpha1.SchemeGroupVersion.Identifier(), Name: uuid.NewString(), }, + Type: "app", AppRef: corev1.LocalObjectReference{ Name: appGUID, }, diff --git a/controllers/controllers/services/bindings/controller_test.go b/controllers/controllers/services/bindings/controller_test.go index a08259bfe..f19cfdc9a 100644 --- a/controllers/controllers/services/bindings/controller_test.go +++ b/controllers/controllers/services/bindings/controller_test.go @@ -197,11 +197,11 @@ var _ = Describe("CFServiceBinding", func() { g.Expect(sbServiceBinding.Spec.Name).To(Equal(binding.Name)) g.Expect(sbServiceBinding.Spec.Type).To(Equal("user-provided")) g.Expect(sbServiceBinding.Spec.Provider).To(BeEmpty()) - g.Expect(sbServiceBinding.Spec.Type).To(Equal(korifiv1alpha1.CFServiceBindingTypeApp)) g.Expect(sbServiceBinding.Labels).To(SatisfyAll( HaveKeyWithValue(bindings.ServiceBindingGUIDLabel, binding.Name), HaveKeyWithValue(korifiv1alpha1.CFAppGUIDLabelKey, cfAppGUID), + HaveKeyWithValue(korifiv1alpha1.ServiceCredentialBindingTypeLabel, "app"), )) g.Expect(sbServiceBinding.OwnerReferences).To(ConsistOf(MatchFields(IgnoreExtras, Fields{