From eb2359605320711f4109ced25fbceb7427788fda Mon Sep 17 00:00:00 2001 From: Dimitar Draganov Date: Thu, 12 Dec 2024 19:43:19 +0200 Subject: [PATCH 1/2] Added service binding of type key --- api/handlers/service_binding.go | 62 +- api/handlers/service_binding_test.go | 775 ++++++++++-------- api/payloads/service_binding.go | 47 +- api/payloads/service_binding_test.go | 48 +- .../service_binding_repository.go | 40 +- .../service_binding_repository_test.go | 139 +++- .../service_instance_repository_test.go | 1 + .../service_offering_repository_test.go | 1 + .../api/v1alpha1/cfservicebinding_types.go | 7 + controllers/api/v1alpha1/groupversion_info.go | 2 +- controllers/api/v1alpha1/shared_types.go | 3 +- .../services/bindings/controller.go | 5 +- .../services/bindings/controller_test.go | 3 +- .../workloads/apps/controller_test.go | 2 + .../build/buildpack/controller_test.go | 1 + .../env/vcap_services_builder_test.go | 2 + .../webhooks/finalizer/webhook_test.go | 3 + controllers/webhooks/version/version_test.go | 3 + ...fi.cloudfoundry.org_cfservicebindings.yaml | 8 + 19 files changed, 726 insertions(+), 426 deletions(-) diff --git a/api/handlers/service_binding.go b/api/handlers/service_binding.go index 8f61bec28..ab9e053c4 100644 --- a/api/handlers/service_binding.go +++ b/api/handlers/service_binding.go @@ -57,40 +57,68 @@ func (h *ServiceBinding) create(r *http.Request) (*routing.Response, error) { 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 { + 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(ctx, 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, + ) + } + } + + 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, "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)) - - serviceBinding, err := h.serviceBindingRepo.CreateServiceBinding(ctx, authInfo, payload.ToMessage(app.SpaceGUID)) + 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/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 9989b490d..0e1901e4c 100644 --- a/api/payloads/service_binding.go +++ b/api/payloads/service_binding.go @@ -1,6 +1,7 @@ package payloads import ( + "errors" "net/url" "code.cloudfoundry.org/korifi/api/payloads/parse" @@ -12,24 +13,49 @@ import ( type ServiceBindingCreate struct { Relationships *ServiceBindingRelationships `json:"relationships"` Type string `json:"type"` - Name *string `json:"name"` Parameters map[string]any `json:"parameters"` + Name string `json:"name"` } 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, + Name: &p.Name, ServiceInstanceGUID: p.Relationships.ServiceInstance.Data.GUID, - AppGUID: p.Relationships.App.Data.GUID, + AppGUID: appGUID, SpaceGUID: spaceGUID, Parameters: p.Parameters, + 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.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 errors.New("relationships cannot be blank") + } + + if p.Type == "app" { + if relationships.App == nil { + return jellidation.NewError("validation_required", "relationships.app cannot be blank") + } + if relationships.App.Data.GUID == "" { + return jellidation.NewError("validation_required", "relationships.app.data.guid cannot be blank") + } + } + + return nil + })), ) } @@ -40,12 +66,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 @@ -53,12 +79,20 @@ 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{ ServiceInstanceGUIDs: parse.ArrayParam(l.ServiceInstanceGUIDs), AppGUIDs: parse.ArrayParam(l.AppGUIDs), LabelSelector: l.LabelSelector, PlanGUIDs: parse.ArrayParam(l.PlanGUIDs), + Type: &l.Type, } } @@ -67,6 +101,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 67ed98b14..3d9816391 100644 --- a/api/payloads/service_binding_test.go +++ b/api/payloads/service_binding_test.go @@ -4,11 +4,13 @@ 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/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/onsi/gomega/gstruct" + "github.com/onsi/gomega/types" ) var _ = Describe("ServiceBindingList", func() { @@ -19,13 +21,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 @@ -34,6 +46,7 @@ var _ = Describe("ServiceBindingList", func() { BeforeEach(func() { payload = payloads.ServiceBindingList{ + Type: korifiv1alpha1.CFServiceBindingTypeApp, AppGUIDs: "app1,app2", ServiceInstanceGUIDs: "s1,s2", Include: "include", @@ -48,6 +61,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", @@ -62,7 +76,7 @@ var _ = Describe("ServiceBindingCreate", func() { BeforeEach(func() { createPayload = payloads.ServiceBindingCreate{ - Name: tools.PtrTo(uuid.NewString()), + Name: uuid.NewString(), Relationships: &payloads.ServiceBindingRelationships{ App: &payloads.Relationship{ Data: &payloads.RelationshipData{ @@ -103,15 +117,37 @@ var _ = Describe("ServiceBindingCreate", func() { Expect(serviceBindingCreate).To(PointTo(Equal(createPayload))) }) + When("name is omitted", func() { + BeforeEach(func() { + createPayload.Name = "" + }) + + It("fails validation", func() { + Expect(apiError).NotTo(HaveOccurred()) + }) + }) + 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")) + It("succeeds", func() { + Expect(validatorErr).NotTo(HaveOccurred()) + Expect(serviceBindingCreate).To(PointTo(Equal(createPayload))) }) + + When("name field is omitted", func() { + BeforeEach(func() { + createPayload.Name = "" + }) + + It("fails validation", func() { + Expect(apiError).To(HaveOccurred()) + Expect(apiError.Detail()).To(ContainSubstring("name cannot be blank")) + }) + }) + }) When("all relationships are missing", func() { @@ -179,7 +215,7 @@ var _ = Describe("ServiceBindingCreate", func() { It("creates the message", func() { Expect(createMessage).To(Equal(repositories.CreateServiceBindingMessage{ - Name: tools.PtrTo(*createPayload.Name), + Name: tools.PtrTo(createPayload.Name), ServiceInstanceGUID: createPayload.Relationships.ServiceInstance.Data.GUID, AppGUID: createPayload.Relationships.App.Data.GUID, SpaceGUID: "space-guid", diff --git a/api/repositories/service_binding_repository.go b/api/repositories/service_binding_repository.go index 6732c0a1b..7d2298bc6 100644 --- a/api/repositories/service_binding_repository.go +++ b/api/repositories/service_binding_repository.go @@ -32,7 +32,6 @@ import ( const ( LabelServiceBindingProvisionedService = "servicebinding.io/provisioned-service" ServiceBindingResourceType = "Service Binding" - ServiceBindingTypeApp = "app" ) type ServiceBindingRepo struct { @@ -85,6 +84,7 @@ type ServiceBindingLastOperation struct { } type CreateServiceBindingMessage struct { + Type string Name *string ServiceInstanceGUID string AppGUID string @@ -100,13 +100,15 @@ 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.Spec.Type) } func (m CreateServiceBindingMessage) toCFServiceBinding(instanceType korifiv1alpha1.InstanceType) *korifiv1alpha1.CFServiceBinding { @@ -114,7 +116,9 @@ func (m CreateServiceBindingMessage) toCFServiceBinding(instanceType korifiv1alp ObjectMeta: metav1.ObjectMeta{ Name: uuid.NewString(), Namespace: m.SpaceGUID, - Labels: map[string]string{LabelServiceBindingProvisionedService: "true"}, + Labels: map[string]string{ + LabelServiceBindingProvisionedService: "true", + }, }, Spec: korifiv1alpha1.CFServiceBindingSpec{ DisplayName: m.Name, @@ -123,7 +127,7 @@ func (m CreateServiceBindingMessage) toCFServiceBinding(instanceType korifiv1alp APIVersion: korifiv1alpha1.SchemeGroupVersion.Identifier(), Name: m.ServiceInstanceGUID, }, - AppRef: corev1.LocalObjectReference{Name: m.AppGUID}, + Type: m.Type, }, } @@ -131,6 +135,10 @@ func (m CreateServiceBindingMessage) toCFServiceBinding(instanceType korifiv1alp binding.Spec.Parameters.Name = uuid.NewString() } + if m.Type == "app" { + binding.Spec.AppRef = corev1.LocalObjectReference{Name: m.AppGUID} + } + return binding } @@ -157,16 +165,18 @@ func (r *ServiceBindingRepo) CreateServiceBinding(ctx context.Context, authInfo ) } - cfApp := new(korifiv1alpha1.CFApp) - err = userClient.Get(ctx, types.NamespacedName{Name: message.AppGUID, Namespace: message.SpaceGUID}, 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: message.AppGUID, Namespace: message.SpaceGUID}, 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{}, + ) + } } cfServiceBinding := message.toCFServiceBinding(cfServiceInstance.Spec.Type) @@ -265,7 +275,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.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 604224244..7f9cc9a42 100644 --- a/api/repositories/service_binding_repository_test.go +++ b/api/repositories/service_binding_repository_test.go @@ -107,6 +107,7 @@ var _ = Describe("ServiceBindingRepo", func() { AppRef: corev1.LocalObjectReference{ Name: appGUID, }, + Type: korifiv1alpha1.CFServiceBindingTypeApp, }, } Expect( @@ -182,6 +183,9 @@ var _ = Describe("ServiceBindingRepo", func() { korifiv1alpha1.SpaceGUIDKey: space.Name, }, }, + Spec: korifiv1alpha1.CFServiceBindingSpec{ + Type: korifiv1alpha1.CFServiceBindingTypeApp, + }, } Expect(k8sClient.Create(ctx, cfServiceBinding)).To(Succeed()) @@ -266,6 +270,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, @@ -284,9 +289,9 @@ 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.CFServiceBindingTypeApp)) Expect(serviceBindingRecord.GUID).To(matchers.BeValidUUID()) - Expect(serviceBindingRecord.Type).To(Equal("app")) Expect(serviceBindingRecord.Name).To(BeNil()) Expect(serviceBindingRecord.AppGUID).To(Equal(appGUID)) Expect(serviceBindingRecord.ServiceInstanceGUID).To(Equal(cfServiceInstance.Name)) @@ -316,6 +321,7 @@ var _ = Describe("ServiceBindingRepo", func() { AppRef: corev1.LocalObjectReference{ Name: appGUID, }, + Type: korifiv1alpha1.CFServiceBindingTypeApp, }, )) }) @@ -391,6 +397,7 @@ var _ = Describe("ServiceBindingRepo", func() { AppRef: corev1.LocalObjectReference{ Name: appGUID, }, + Type: korifiv1alpha1.CFServiceBindingTypeApp, }, } Expect( @@ -498,6 +505,8 @@ var _ = Describe("ServiceBindingRepo", func() { cfServiceInstance *korifiv1alpha1.CFServiceInstance serviceBindingRecord repositories.ServiceBindingRecord createErr error + createMsg repositories.CreateServiceBindingMessage + serviceBindingName string = "service-binding-name" ) BeforeEach(func() { @@ -514,26 +523,29 @@ var _ = Describe("ServiceBindingRepo", func() { k8sClient.Create(ctx, cfServiceInstance), ).To(Succeed()) - bindingName = nil - }) - - JustBeforeEach(func() { - serviceBindingRecord, createErr = repo.CreateServiceBinding(ctx, authInfo, repositories.CreateServiceBindingMessage{ - Name: bindingName, + createMsg = repositories.CreateServiceBindingMessage{ + Type: korifiv1alpha1.CFServiceBindingTypeApp, + Name: &serviceBindingName, ServiceInstanceGUID: cfServiceInstance.Name, AppGUID: appGUID, SpaceGUID: space.Name, Parameters: map[string]any{ "p1": "p1-value", }, - }) + } }) - It("returns a forbidden error", func() { - Expect(createErr).To(BeAssignableToTypeOf(apierrors.UnprocessableEntityError{})) + JustBeforeEach(func() { + serviceBindingRecord, createErr = repo.CreateServiceBinding(ctx, authInfo, createMsg) }) - When("the user can create CFServiceBindings in the Space", func() { + When("the user is not allowed to create CFServiceBindings", func() { + It("returns a forbidden error", func() { + Expect(createErr).To(BeAssignableToTypeOf(apierrors.UnprocessableEntityError{})) + }) + }) + + When("the user is allowed to create CFServiceBindings in the Space", func() { BeforeEach(func() { createRoleBinding(ctx, userName, spaceDeveloperRole.Name, space.Name) }) @@ -607,7 +619,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{})) }) }) @@ -623,6 +635,42 @@ var _ = Describe("ServiceBindingRepo", func() { }) }) }) + + When("binding type is key", func() { + BeforeEach(func() { + createMsg.Type = korifiv1alpha1.CFServiceBindingTypeKey + createMsg.AppGUID = "" + createRoleBinding(ctx, userName, spaceDeveloperRole.Name, space.Name) + }) + + It("creates a key binding", func() { + Expect(serviceBindingRecord.AppGUID).To(Equal("")) + Expect(serviceBindingRecord.Type).To(Equal(korifiv1alpha1.CFServiceBindingTypeKey)) + Expect(serviceBindingRecord.Relationships()).To(HaveKeyWithValue("app", "")) + Expect(*(serviceBindingRecord.Name)).To(Equal(serviceBindingName)) + Expect(createErr).NotTo(HaveOccurred()) + + serviceBinding := new(korifiv1alpha1.CFServiceBinding) + Expect( + k8sClient.Get(ctx, types.NamespacedName{Name: serviceBindingRecord.GUID, Namespace: space.Name}, serviceBinding), + ).To(Succeed()) + + Expect(*serviceBinding).To(MatchFields(IgnoreExtras, Fields{ + "ObjectMeta": MatchFields(IgnoreExtras, Fields{ + "Labels": HaveKeyWithValue("servicebinding.io/provisioned-service", "true"), + }), + "Spec": MatchFields(IgnoreExtras, Fields{ + "Type": Equal(korifiv1alpha1.CFServiceBindingTypeKey), + "DisplayName": PointTo(Equal(serviceBindingName)), + "Service": Equal(corev1.ObjectReference{ + Kind: "CFServiceInstance", + APIVersion: korifiv1alpha1.SchemeGroupVersion.Identifier(), + Name: cfServiceInstance.Name, + }), + }), + })) + }) + }) }) Describe("DeleteServiceBinding", func() { @@ -655,6 +703,7 @@ var _ = Describe("ServiceBindingRepo", func() { AppRef: corev1.LocalObjectReference{ Name: appGUID, }, + Type: korifiv1alpha1.CFServiceBindingTypeApp, }, } Expect( @@ -703,10 +752,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 @@ -735,6 +784,7 @@ var _ = Describe("ServiceBindingRepo", func() { AppRef: corev1.LocalObjectReference{ Name: cfApp1.Name, }, + Type: korifiv1alpha1.CFServiceBindingTypeApp, }, } Expect(k8sClient.Create(ctx, serviceBinding1)).To(Succeed()) @@ -761,6 +811,7 @@ var _ = Describe("ServiceBindingRepo", func() { AppRef: corev1.LocalObjectReference{ Name: cfApp2.Name, }, + Type: korifiv1alpha1.CFServiceBindingTypeApp, }, } Expect(k8sClient.Create(ctx, serviceBinding2)).To(Succeed()) @@ -786,10 +837,30 @@ var _ = Describe("ServiceBindingRepo", func() { AppRef: corev1.LocalObjectReference{ Name: cfApp3.Name, }, + Type: korifiv1alpha1.CFServiceBindingTypeApp, }, } 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", + }, + }, + Spec: korifiv1alpha1.CFServiceBindingSpec{ + Service: corev1.ObjectReference{ + Kind: "ServiceInstance", + Name: cfServiceInstance3.Name, + APIVersion: "korifi.cloudfoundry.org/v1alpha1", + }, + Type: korifiv1alpha1.CFServiceBindingTypeKey, + }, + } + Expect(k8sClient.Create(ctx, serviceBinding4)).To(Succeed()) + requestMessage = repositories.ListServiceBindingsMessage{} }) @@ -812,7 +883,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), @@ -820,7 +891,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), @@ -828,12 +899,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), + }), )) }) }) @@ -841,19 +919,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), }), )) }) @@ -890,6 +968,9 @@ var _ = Describe("ServiceBindingRepo", func() { Expect(k8s.PatchResource(ctx, k8sClient, serviceBinding3, func() { serviceBinding3.Labels["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", @@ -907,12 +988,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() { @@ -993,6 +1074,7 @@ var _ = Describe("ServiceBindingRepo", func() { APIVersion: korifiv1alpha1.SchemeGroupVersion.Identifier(), Name: uuid.NewString(), }, + Type: korifiv1alpha1.CFServiceBindingTypeApp, AppRef: corev1.LocalObjectReference{ Name: appGUID, }, @@ -1056,6 +1138,7 @@ var _ = Describe("ServiceBindingRepo", func() { }, }, Spec: korifiv1alpha1.CFServiceBindingSpec{ + Type: korifiv1alpha1.CFServiceBindingTypeApp, Service: corev1.ObjectReference{ Kind: "CFServiceInstance", APIVersion: korifiv1alpha1.SchemeGroupVersion.Identifier(), diff --git a/api/repositories/service_instance_repository_test.go b/api/repositories/service_instance_repository_test.go index a5480f7a5..8b0178ae2 100644 --- a/api/repositories/service_instance_repository_test.go +++ b/api/repositories/service_instance_repository_test.go @@ -1204,6 +1204,7 @@ var _ = Describe("ServiceInstanceRepository", func() { AppRef: corev1.LocalObjectReference{ Name: "some-app-guid", }, + Type: korifiv1alpha1.CFServiceBindingTypeApp, }, } Expect(k8sClient.Create(ctx, serviceBinding)).To(Succeed()) diff --git a/api/repositories/service_offering_repository_test.go b/api/repositories/service_offering_repository_test.go index b461b4acd..ff276d0ef 100644 --- a/api/repositories/service_offering_repository_test.go +++ b/api/repositories/service_offering_repository_test.go @@ -414,6 +414,7 @@ var _ = Describe("ServiceOfferingRepo", func() { AppRef: corev1.LocalObjectReference{ Name: "some-app-guid", }, + Type: korifiv1alpha1.CFServiceBindingTypeApp, }, } Expect(k8sClient.Create(ctx, binding)).To(Succeed()) diff --git a/controllers/api/v1alpha1/cfservicebinding_types.go b/controllers/api/v1alpha1/cfservicebinding_types.go index d2d21532b..52321a82a 100644 --- a/controllers/api/v1alpha1/cfservicebinding_types.go +++ b/controllers/api/v1alpha1/cfservicebinding_types.go @@ -28,6 +28,9 @@ const ( UnbindingFailedCondition = "UnbindingFailed" + CFServiceBindingTypeKey = "key" + CFServiceBindingTypeApp = "app" + ServiceInstanceTypeAnnotationKey = "korifi.cloudfoundry.org/service-instance-type" PlanGUIDLabelKey = "korifi.cloudfoundry.org/plan-guid" @@ -50,6 +53,10 @@ type CFServiceBindingSpec struct { // A reference to the secret that contains the service binding parameters. // Only makes sense for bindings to managed service instances Parameters v1.LocalObjectReference `json:"parameters"` + + // 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/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 diff --git a/controllers/api/v1alpha1/shared_types.go b/controllers/api/v1alpha1/shared_types.go index 99b9bf6b1..24e207a5d 100644 --- a/controllers/api/v1alpha1/shared_types.go +++ b/controllers/api/v1alpha1/shared_types.go @@ -19,7 +19,8 @@ const ( CFRouteGUIDLabelKey = "korifi.cloudfoundry.org/route-guid" CFTaskGUIDLabelKey = "korifi.cloudfoundry.org/task-guid" - SpaceGUIDKey = "korifi.cloudfoundry.org/space-guid" + SpaceGUIDKey = "korifi.cloudfoundry.org/space-guid" + ServiceBindingTypeLabel = "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 6ed4c96c1..751bfb7c9 100644 --- a/controllers/controllers/services/bindings/controller.go +++ b/controllers/controllers/services/bindings/controller.go @@ -38,9 +38,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 DelegateReconciler interface { diff --git a/controllers/controllers/services/bindings/controller_test.go b/controllers/controllers/services/bindings/controller_test.go index e05e52a89..dd13c75d1 100644 --- a/controllers/controllers/services/bindings/controller_test.go +++ b/controllers/controllers/services/bindings/controller_test.go @@ -63,6 +63,7 @@ var _ = Describe("CFServiceBinding", func() { AppRef: corev1.LocalObjectReference{ Name: cfAppGUID, }, + Type: korifiv1alpha1.CFServiceBindingTypeApp, }, } Expect(adminClient.Create(ctx, binding)).To(Succeed()) @@ -208,7 +209,7 @@ var _ = Describe("CFServiceBinding", func() { g.Expect(sbServiceBinding.Labels).To(SatisfyAll( HaveKeyWithValue(korifiv1alpha1.ServiceBindingGUIDLabel, binding.Name), HaveKeyWithValue(korifiv1alpha1.CFAppGUIDLabelKey, cfAppGUID), - HaveKeyWithValue(korifiv1alpha1.ServiceCredentialBindingTypeLabel, "app"), + HaveKeyWithValue(korifiv1alpha1.ServiceBindingTypeLabel, "app"), )) g.Expect(sbServiceBinding.OwnerReferences).To(ConsistOf(MatchFields(IgnoreExtras, Fields{ diff --git a/controllers/controllers/workloads/apps/controller_test.go b/controllers/controllers/workloads/apps/controller_test.go index 8cba1b4e2..66b87076b 100644 --- a/controllers/controllers/workloads/apps/controller_test.go +++ b/controllers/controllers/workloads/apps/controller_test.go @@ -383,6 +383,7 @@ var _ = Describe("CFAppReconciler Integration Tests", func() { Name: instance.Name, }, AppRef: corev1.LocalObjectReference{Name: cfApp.Name}, + Type: korifiv1alpha1.CFServiceBindingTypeApp, }, } Expect(adminClient.Create(ctx, binding)).To(Succeed()) @@ -481,6 +482,7 @@ var _ = Describe("CFAppReconciler Integration Tests", func() { AppRef: corev1.LocalObjectReference{ Name: cfApp.Name, }, + Type: korifiv1alpha1.CFServiceBindingTypeApp, }, } Expect(adminClient.Create(ctx, &cfServiceBinding)).To(Succeed()) diff --git a/controllers/controllers/workloads/build/buildpack/controller_test.go b/controllers/controllers/workloads/build/buildpack/controller_test.go index 147f33faf..e20b1270f 100644 --- a/controllers/controllers/workloads/build/buildpack/controller_test.go +++ b/controllers/controllers/workloads/build/buildpack/controller_test.go @@ -226,6 +226,7 @@ var _ = Describe("CFBuildpackBuildReconciler Integration Tests", func() { AppRef: corev1.LocalObjectReference{ Name: cfApp.Name, }, + Type: korifiv1alpha1.CFServiceBindingTypeApp, }, } Expect(adminClient.Create(ctx, serviceBinding)).To(Succeed()) diff --git a/controllers/controllers/workloads/env/vcap_services_builder_test.go b/controllers/controllers/workloads/env/vcap_services_builder_test.go index f7d88a817..83abf53c4 100644 --- a/controllers/controllers/workloads/env/vcap_services_builder_test.go +++ b/controllers/controllers/workloads/env/vcap_services_builder_test.go @@ -70,6 +70,7 @@ var _ = Describe("Builder", func() { }, }, Spec: korifiv1alpha1.CFServiceBindingSpec{ + Type: "app", DisplayName: &serviceBindingName, Service: corev1.ObjectReference{ Name: "my-service-instance-guid", @@ -111,6 +112,7 @@ var _ = Describe("Builder", func() { }, }, Spec: korifiv1alpha1.CFServiceBindingSpec{ + Type: "app", DisplayName: &serviceBindingName2, Service: corev1.ObjectReference{ Name: "my-service-instance-guid-2", diff --git a/controllers/webhooks/finalizer/webhook_test.go b/controllers/webhooks/finalizer/webhook_test.go index e743f20f8..74e05a12e 100644 --- a/controllers/webhooks/finalizer/webhook_test.go +++ b/controllers/webhooks/finalizer/webhook_test.go @@ -139,6 +139,9 @@ var _ = Describe("Controllers Finalizers Webhook", func() { Namespace: "test-org-" + uuid.NewString(), Name: uuid.NewString(), }, + Spec: korifiv1alpha1.CFServiceBindingSpec{ + Type: korifiv1alpha1.CFServiceBindingTypeApp, + }, }, korifiv1alpha1.CFServiceBindingFinalizerName, ), diff --git a/controllers/webhooks/version/version_test.go b/controllers/webhooks/version/version_test.go index 6ac787efd..79db04d5e 100644 --- a/controllers/webhooks/version/version_test.go +++ b/controllers/webhooks/version/version_test.go @@ -145,6 +145,9 @@ var _ = Describe("Setting the version annotation", func() { Namespace: orgNamespace, Name: uuid.NewString(), }, + Spec: korifiv1alpha1.CFServiceBindingSpec{ + Type: korifiv1alpha1.CFServiceBindingTypeApp, + }, }), createObject(&korifiv1alpha1.TaskWorkload{ ObjectMeta: metav1.ObjectMeta{ diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfservicebindings.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfservicebindings.yaml index 6c7130f88..e3e90b661 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfservicebindings.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfservicebindings.yaml @@ -125,10 +125,18 @@ 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 - parameters - service + - type type: object status: description: CFServiceBindingStatus defines the observed state of CFServiceBinding From 7d37bb3865063ea2bb6d5ae35392354ab617fa67 Mon Sep 17 00:00:00 2001 From: Dimitar Draganov Date: Fri, 13 Dec 2024 02:45:47 +0200 Subject: [PATCH 2/2] Added key type filter in managed controller --- api/handlers/service_binding_test.go | 526 ++++++++---------- api/payloads/service_binding.go | 4 +- api/payloads/service_binding_test.go | 26 +- .../service_binding_repository_test.go | 151 +++-- .../api/v1alpha1/cfservicebinding_types.go | 5 +- .../services/bindings/controller_test.go | 17 + .../services/bindings/managed/controller.go | 4 + .../services/bindings/sbio/servicebinding.go | 6 +- .../bindings/sbio/servicebinding_test.go | 6 +- .../webhooks/relationships/webhook_test.go | 3 + 10 files changed, 386 insertions(+), 362 deletions(-) diff --git a/api/handlers/service_binding_test.go b/api/handlers/service_binding_test.go index 8dae932ac..e3879bf93 100644 --- a/api/handlers/service_binding_test.go +++ b/api/handlers/service_binding_test.go @@ -91,7 +91,22 @@ var _ = Describe("ServiceBinding", func() { requestValidator.DecodeAndValidateJSONPayloadStub = decodeAndValidatePayloadStub(&payload) }) - When("binding to a managed service instance", func() { + 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 unprocessable entity error", func() { + Expect(serviceBindingRepo.CreateServiceBindingCallCount()).To(Equal(0)) + expectUnprocessableEntityError("Service credential bindings of type 'key' are not supported for user-provided service instances.") + }) + }) + + When("binding to a managed service", func() { BeforeEach(func() { serviceInstanceRepo.GetServiceInstanceReturns(repositories.ServiceInstanceRecord{ GUID: "service-instance-guid", @@ -106,85 +121,14 @@ var _ = Describe("ServiceBinding", func() { }) It("creates a binding", func() { - Expect(serviceInstanceRepo.GetServiceInstanceCallCount()).To(Equal(1)) Expect(serviceBindingRepo.CreateServiceBindingCallCount()).To(Equal(1)) + Expect(rr).To(HaveHTTPStatus(http.StatusAccepted)) + _, 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("creating the ServiceBinding errors", func() { - BeforeEach(func() { - serviceBindingRepo.CreateServiceBindingReturns(repositories.ServiceBindingRecord{}, errors.New("boom")) - }) - - It("returns an error", func() { - expectUnknownError() - }) - }) - }) - - 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("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")) - }) - - It("returns an error", func() { - expectUnknownError() - }) - }) - - 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 service instance is forbidden", func() { - BeforeEach(func() { - serviceInstanceRepo.GetServiceInstanceReturns(repositories.ServiceInstanceRecord{}, apierrors.NewForbiddenError(nil, repositories.ServiceInstanceResourceType)) - }) - - It("returns a not found error", func() { - expectNotFoundError(repositories.ServiceInstanceResourceType) - }) - }) - - 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() { - expectUnknownError() - Expect(serviceBindingRepo.CreateServiceBindingCallCount()).To(Equal(0)) }) }) }) @@ -333,294 +277,294 @@ var _ = Describe("ServiceBinding", func() { }) }) }) + }) + + 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"), + ))) + }) - Describe("GET /v3/service_credential_bindings/{guid}", func() { + When("the service bindding repo returns an error", func() { BeforeEach(func() { - requestMethod = http.MethodGet - requestPath = "/v3/service_credential_bindings/service-binding-guid" - requestBody = "" + serviceBindingRepo.GetServiceBindingReturns(repositories.ServiceBindingRecord{}, errors.New("get-service-binding-error")) }) - 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"), - ))) + It("returns an error", func() { + expectUnknownError() }) + }) - When("the service bindding repo returns an error", func() { - BeforeEach(func() { - serviceBindingRepo.GetServiceBindingReturns(repositories.ServiceBindingRecord{}, errors.New("get-service-binding-error")) - }) + When("the user is not authorized", func() { + BeforeEach(func() { + serviceBindingRepo.GetServiceBindingReturns(repositories.ServiceBindingRecord{}, apierrors.NewForbiddenError(nil, "CFServiceBinding")) + }) - It("returns an error", func() { - expectUnknownError() - }) + It("returns 404 NotFound", func() { + expectNotFoundError("CFServiceBinding") }) + }) + }) - When("the user is not authorized", func() { - BeforeEach(func() { - serviceBindingRepo.GetServiceBindingReturns(repositories.ServiceBindingRecord{}, apierrors.NewForbiddenError(nil, "CFServiceBinding")) - }) + 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 404 NotFound", func() { - expectNotFoundError("CFServiceBinding") - }) - }) + 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"), + ))) }) - Describe("GET /v3/service_credential_bindings", func() { + When("there is an error fetching service binding", func() { BeforeEach(func() { - requestMethod = http.MethodGet - requestBody = "" - requestPath = "/v3/service_credential_bindings?foo=bar" + serviceBindingRepo.ListServiceBindingsReturns([]repositories.ServiceBindingRecord{}, errors.New("unknown")) + }) - serviceBindingRepo.ListServiceBindingsReturns([]repositories.ServiceBindingRecord{ - {GUID: "service-binding-guid", AppGUID: "app-guid"}, - }, nil) - appRepo.ListAppsReturns([]repositories.AppRecord{{Name: "some-app-name"}}, nil) + It("returns an error", func() { + expectUnknownError() + }) + }) + When("an include=app query parameter is specified", func() { + BeforeEach(func() { payload := payloads.ServiceBindingList{ - AppGUIDs: "a1,a2", - ServiceInstanceGUIDs: "s1,s2", - LabelSelector: "label=value", - PlanGUIDs: "p1,p2", + Include: "app", } 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")) + 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")) - 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"), - ))) + Expect(rr).To(HaveHTTPBody(MatchJSONPath("$.included.apps[0].name", "some-app-name"))) }) + }) - When("there is an error fetching service binding", func() { - BeforeEach(func() { - serviceBindingRepo.ListServiceBindingsReturns([]repositories.ServiceBindingRecord{}, errors.New("unknown")) - }) + When("decoding URL params fails", func() { + BeforeEach(func() { + requestValidator.DecodeAndValidateURLValuesReturns(errors.New("boom")) + }) - It("returns an error", func() { - expectUnknownError() - }) + It("returns an error", func() { + expectUnknownError() }) + }) + }) - When("an include=app query parameter is specified", func() { - BeforeEach(func() { - payload := payloads.ServiceBindingList{ - Include: "app", - } - requestValidator.DecodeAndValidateURLValuesStub = decodeAndValidateURLValuesStub(&payload) - }) + Describe("DELETE /v3/service_credential_bindings/:guid", func() { + BeforeEach(func() { + requestMethod = "DELETE" + requestPath = "/v3/service_credential_bindings/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")) + 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")) + }) - Expect(rr).To(HaveHTTPBody(MatchJSONPath("$.included.apps[0].name", "some-app-name"))) - }) + When("getting the service binding is forbidden", func() { + BeforeEach(func() { + serviceBindingRepo.GetServiceBindingReturns(repositories.ServiceBindingRecord{}, apierrors.NewForbiddenError(nil, repositories.ServiceBindingResourceType)) }) - When("decoding URL params fails", func() { - BeforeEach(func() { - requestValidator.DecodeAndValidateURLValuesReturns(errors.New("boom")) - }) - - It("returns an error", func() { - expectUnknownError() - }) + It("returns a not found error", func() { + expectNotFoundError(repositories.ServiceBindingResourceType) }) }) - Describe("DELETE /v3/service_credential_bindings/:guid", func() { + When("getting the service binding fails", func() { BeforeEach(func() { - requestMethod = "DELETE" - requestPath = "/v3/service_credential_bindings/service-binding-guid" + serviceBindingRepo.GetServiceBindingReturns(repositories.ServiceBindingRecord{}, errors.New("getting-binding-failed")) }) - 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("returns unknown error", func() { + expectUnknownError() }) + }) - When("getting the service binding is forbidden", func() { - BeforeEach(func() { - serviceBindingRepo.GetServiceBindingReturns(repositories.ServiceBindingRecord{}, apierrors.NewForbiddenError(nil, repositories.ServiceBindingResourceType)) - }) + 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")) + }) - It("returns a not found error", func() { - expectNotFoundError(repositories.ServiceBindingResourceType) - }) + When("getting the service instance fails", func() { + BeforeEach(func() { + serviceInstanceRepo.GetServiceInstanceReturns(repositories.ServiceInstanceRecord{}, errors.New("getting-instance-failed")) }) - 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("returns error", func() { + expectUnprocessableEntityError("failed to get service instance") }) + }) - 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")) - }) + It("deletes the service binding", func() { + Expect(rr).To(HaveHTTPStatus(http.StatusNoContent)) + Expect(rr).To(HaveHTTPBody(BeEmpty())) - When("getting the service instance fails", func() { - BeforeEach(func() { - serviceInstanceRepo.GetServiceInstanceReturns(repositories.ServiceInstanceRecord{}, errors.New("getting-instance-failed")) - }) + Expect(serviceBindingRepo.DeleteServiceBindingCallCount()).To(Equal(1)) + _, _, guid := serviceBindingRepo.DeleteServiceBindingArgsForCall(0) + Expect(guid).To(Equal("service-binding-guid")) + }) - It("returns error", func() { - expectUnprocessableEntityError("failed to get service instance") - }) + 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 service binding", func() { - Expect(rr).To(HaveHTTPStatus(http.StatusNoContent)) - Expect(rr).To(HaveHTTPBody(BeEmpty())) - + 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")) - }) - - 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")) - 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")) - }) - - 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() { + When("deleting the service binding fails", 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) + serviceBindingRepo.DeleteServiceBindingReturns(errors.New("delete-binding-failed")) + }) - 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 unknown error", func() { + expectUnknownError() }) + }) + }) - It("updates the service binding", func() { - Expect(requestValidator.DecodeAndValidateJSONPayloadCallCount()).To(Equal(1)) - actualReq, _ := requestValidator.DecodeAndValidateJSONPayloadArgsForCall(0) - Expect(bodyString(actualReq)).To(Equal("the-json-body")) + 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) + }) - 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"), - ))) + 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")}, + }, + })) + }) - 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 payload cannot be decoded", func() { + BeforeEach(func() { + requestValidator.DecodeAndValidateJSONPayloadReturns(errors.New("boom")) }) - 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)) - }) + 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) - }) + It("returns a not found error", func() { + expectNotFoundError(repositories.ServiceBindingResourceType) }) + }) - When("the service binding repo returns an error", func() { - BeforeEach(func() { - serviceBindingRepo.UpdateServiceBindingReturns(repositories.ServiceBindingRecord{}, errors.New("update-sb-error")) - }) + 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 an error", func() { + expectUnknownError() }) + }) - When("the user is not authorized to get service bindings", func() { - BeforeEach(func() { - serviceBindingRepo.GetServiceBindingReturns(repositories.ServiceBindingRecord{}, apierrors.NewForbiddenError(nil, "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") - }) + It("returns 404 NotFound", func() { + expectNotFoundError("CFServiceBinding") }) }) }) diff --git a/api/payloads/service_binding.go b/api/payloads/service_binding.go index 0e1901e4c..58b07c00a 100644 --- a/api/payloads/service_binding.go +++ b/api/payloads/service_binding.go @@ -42,12 +42,12 @@ func (p ServiceBindingCreate) Validate() error { jellidation.Field(&p.Relationships, jellidation.By(func(value any) error { relationships, ok := value.(*ServiceBindingRelationships) if !ok || relationships == nil { - return errors.New("relationships cannot be blank") + return errors.New("relationships is required") } if p.Type == "app" { if relationships.App == nil { - return jellidation.NewError("validation_required", "relationships.app cannot be blank") + return jellidation.NewError("validation_required", "relationships.app is required") } if relationships.App.Data.GUID == "" { 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 3d9816391..c233d08fd 100644 --- a/api/payloads/service_binding_test.go +++ b/api/payloads/service_binding_test.go @@ -22,9 +22,11 @@ var _ = Describe("ServiceBindingList", func() { Expect(*actualServiceBindingList).To(Equal(expectedServiceBindingList)) }, Entry("type", "type=key", payloads.ServiceBindingList{Type: korifiv1alpha1.CFServiceBindingTypeKey}), + Entry("type", "type=app", payloads.ServiceBindingList{Type: korifiv1alpha1.CFServiceBindingTypeApp}), 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=app", payloads.ServiceBindingList{Include: "app"}), + Entry("include", "include=service_instance", payloads.ServiceBindingList{Include: "service_instance"}), 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"}), ) @@ -147,7 +149,6 @@ var _ = Describe("ServiceBindingCreate", func() { Expect(apiError.Detail()).To(ContainSubstring("name cannot be blank")) }) }) - }) When("all relationships are missing", func() { @@ -204,6 +205,28 @@ var _ = Describe("ServiceBindingCreate", func() { Expect(apiError.Detail()).To(ContainSubstring("relationships.service_instance.data.guid cannot be blank")) }) }) + + When("binding is key", func() { + BeforeEach(func() { + createPayload.Type = "key" + }) + + It("succeeds", func() { + Expect(validatorErr).NotTo(HaveOccurred()) + Expect(serviceBindingCreate).To(PointTo(Equal(createPayload))) + }) + + When("name field is omitted", func() { + BeforeEach(func() { + createPayload.Name = "" + }) + + It("fails validation", func() { + Expect(apiError).To(HaveOccurred()) + Expect(apiError.Detail()).To(ContainSubstring("name cannot be blank")) + }) + }) + }) }) Describe("ToMessage", func() { @@ -219,6 +242,7 @@ var _ = Describe("ServiceBindingCreate", func() { ServiceInstanceGUID: createPayload.Relationships.ServiceInstance.Data.GUID, AppGUID: createPayload.Relationships.App.Data.GUID, SpaceGUID: "space-guid", + Type: "app", Parameters: map[string]any{ "p1": "p1-value", }, diff --git a/api/repositories/service_binding_repository_test.go b/api/repositories/service_binding_repository_test.go index 7f9cc9a42..82c3c24ff 100644 --- a/api/repositories/service_binding_repository_test.go +++ b/api/repositories/service_binding_repository_test.go @@ -271,10 +271,10 @@ var _ = Describe("ServiceBindingRepo", func() { JustBeforeEach(func() { serviceBindingRecord, createErr = repo.CreateServiceBinding(ctx, authInfo, repositories.CreateServiceBindingMessage{ Type: korifiv1alpha1.CFServiceBindingTypeApp, - Name: bindingName, ServiceInstanceGUID: cfServiceInstance.Name, AppGUID: appGUID, SpaceGUID: space.Name, + Name: bindingName, }) }) @@ -289,7 +289,6 @@ 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.CFServiceBindingTypeApp)) Expect(serviceBindingRecord.GUID).To(matchers.BeValidUUID()) Expect(serviceBindingRecord.Name).To(BeNil()) @@ -525,7 +524,7 @@ var _ = Describe("ServiceBindingRepo", func() { createMsg = repositories.CreateServiceBindingMessage{ Type: korifiv1alpha1.CFServiceBindingTypeApp, - Name: &serviceBindingName, + Name: nil, ServiceInstanceGUID: cfServiceInstance.Name, AppGUID: appGUID, SpaceGUID: space.Name, @@ -572,18 +571,24 @@ var _ = Describe("ServiceBindingRepo", func() { 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).To(MatchFields(IgnoreExtras, Fields{ - "DisplayName": BeNil(), - "Service": MatchFields(IgnoreExtras, Fields{ - "Kind": Equal("CFServiceInstance"), - "Name": Equal(cfServiceInstance.Name), - }), - "AppRef": Equal(corev1.LocalObjectReference{ - Name: appGUID, + Expect(*serviceBinding).To(MatchFields(IgnoreExtras, Fields{ + "ObjectMeta": MatchFields(IgnoreExtras, Fields{ + "Labels": HaveKeyWithValue("servicebinding.io/provisioned-service", "true"), }), - "Parameters": MatchAllFields(Fields{ - "Name": Not(BeEmpty()), + "Spec": MatchFields(IgnoreExtras, Fields{ + "Type": Equal(korifiv1alpha1.CFServiceBindingTypeApp), + "DisplayName": BeNil(), + "Service": Equal(corev1.ObjectReference{ + Kind: "CFServiceInstance", + APIVersion: korifiv1alpha1.SchemeGroupVersion.Identifier(), + Name: cfServiceInstance.Name, + }), + "AppRef": Equal(corev1.LocalObjectReference{ + Name: appGUID, + }), + "Parameters": MatchAllFields(Fields{ + "Name": Not(BeEmpty()), + }), }), })) }) @@ -616,7 +621,7 @@ var _ = Describe("ServiceBindingRepo", func() { When("the app does not exist", func() { BeforeEach(func() { - appGUID = "i-do-not-exits" + createMsg.AppGUID = "i-do-not-exits" }) It("returns an UnprocessableEntity error", func() { @@ -624,51 +629,63 @@ 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 + createMsg.Name = tools.PtrTo("some-name-for-a-binding") }) It("creates the binding with the specified name", func() { - Expect(serviceBindingRecord.Name).To(Equal(bindingName)) + Expect(serviceBindingRecord.Name).To(Equal(tools.PtrTo("some-name-for-a-binding"))) }) }) - }) - - When("binding type is key", func() { - BeforeEach(func() { - createMsg.Type = korifiv1alpha1.CFServiceBindingTypeKey - createMsg.AppGUID = "" - createRoleBinding(ctx, userName, spaceDeveloperRole.Name, space.Name) - }) - It("creates a key binding", func() { - Expect(serviceBindingRecord.AppGUID).To(Equal("")) - Expect(serviceBindingRecord.Type).To(Equal(korifiv1alpha1.CFServiceBindingTypeKey)) - Expect(serviceBindingRecord.Relationships()).To(HaveKeyWithValue("app", "")) - Expect(*(serviceBindingRecord.Name)).To(Equal(serviceBindingName)) - Expect(createErr).NotTo(HaveOccurred()) - - serviceBinding := new(korifiv1alpha1.CFServiceBinding) - Expect( - k8sClient.Get(ctx, types.NamespacedName{Name: serviceBindingRecord.GUID, Namespace: space.Name}, serviceBinding), - ).To(Succeed()) + When("binding type is key", func() { + BeforeEach(func() { + createMsg.Type = korifiv1alpha1.CFServiceBindingTypeKey + createMsg.AppGUID = "" + createMsg.Name = tools.PtrTo(serviceBindingName) + createRoleBinding(ctx, userName, spaceDeveloperRole.Name, space.Name) + }) - Expect(*serviceBinding).To(MatchFields(IgnoreExtras, Fields{ - "ObjectMeta": MatchFields(IgnoreExtras, Fields{ - "Labels": HaveKeyWithValue("servicebinding.io/provisioned-service", "true"), - }), - "Spec": MatchFields(IgnoreExtras, Fields{ - "Type": Equal(korifiv1alpha1.CFServiceBindingTypeKey), - "DisplayName": PointTo(Equal(serviceBindingName)), - "Service": Equal(corev1.ObjectReference{ - Kind: "CFServiceInstance", - APIVersion: korifiv1alpha1.SchemeGroupVersion.Identifier(), - Name: cfServiceInstance.Name, + It("creates a key binding", func() { + Expect(serviceBindingRecord.GUID).To(matchers.BeValidUUID()) + Expect(serviceBindingRecord.AppGUID).To(Equal("")) + Expect(serviceBindingRecord.Type).To(Equal(korifiv1alpha1.CFServiceBindingTypeKey)) + Expect(*(serviceBindingRecord.Name)).To(Equal(serviceBindingName)) + 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, + })) + + Expect(createErr).NotTo(HaveOccurred()) + + serviceBinding := new(korifiv1alpha1.CFServiceBinding) + Expect( + k8sClient.Get(ctx, types.NamespacedName{Name: serviceBindingRecord.GUID, Namespace: space.Name}, serviceBinding), + ).To(Succeed()) + + Expect(*serviceBinding).To(MatchFields(IgnoreExtras, Fields{ + "ObjectMeta": MatchFields(IgnoreExtras, Fields{ + "Labels": HaveKeyWithValue("servicebinding.io/provisioned-service", "true"), }), - }), - })) + "Spec": MatchFields(IgnoreExtras, Fields{ + "Type": Equal(korifiv1alpha1.CFServiceBindingTypeKey), + "DisplayName": PointTo(Equal(serviceBindingName)), + "Service": Equal(corev1.ObjectReference{ + Kind: "CFServiceInstance", + APIVersion: korifiv1alpha1.SchemeGroupVersion.Identifier(), + Name: cfServiceInstance.Name, + }), + "Parameters": MatchAllFields(Fields{ + "Name": Not(BeEmpty()), + }), + }), + })) + }) }) }) }) @@ -848,6 +865,7 @@ var _ = Describe("ServiceBindingRepo", func() { Namespace: space2.Name, Labels: map[string]string{ korifiv1alpha1.PlanGUIDLabelKey: "plan-4", + korifiv1alpha1.SpaceGUIDKey: space.Name, }, }, Spec: korifiv1alpha1.CFServiceBindingSpec{ @@ -919,20 +937,24 @@ var _ = Describe("ServiceBindingRepo", func() { When("filtered by service instance GUID", func() { BeforeEach(func() { requestMessage = repositories.ListServiceBindingsMessage{ - ServiceInstanceGUIDs: []string{serviceInstance1GUID, serviceInstance2GUID}, + ServiceInstanceGUIDs: []string{serviceInstance2GUID, serviceInstance3GUID}, } }) It("returns only the ServiceBindings that match the provided service instance guids", func() { Expect(responseServiceBindings).To(ConsistOf( - MatchFields(IgnoreExtras, Fields{ - "GUID": Equal(serviceBinding1.Name), - "ServiceInstanceGUID": Equal(serviceInstance1GUID), - }), MatchFields(IgnoreExtras, Fields{ "GUID": Equal(serviceBinding2.Name), "ServiceInstanceGUID": Equal(serviceInstance2GUID), }), + MatchFields(IgnoreExtras, Fields{ + "GUID": Equal(serviceBinding3.Name), + "ServiceInstanceGUID": Equal(serviceInstance3GUID), + }), + MatchFields(IgnoreExtras, Fields{ + "GUID": Equal(serviceBinding4.Name), + "ServiceInstanceGUID": Equal(serviceInstance3GUID), + }), )) }) }) @@ -968,9 +990,6 @@ var _ = Describe("ServiceBindingRepo", func() { Expect(k8s.PatchResource(ctx, k8sClient, serviceBinding3, func() { serviceBinding3.Labels["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", @@ -1038,6 +1057,20 @@ var _ = Describe("ServiceBindingRepo", func() { )) }) }) + + When("filtered by binding type", func() { + BeforeEach(func() { + requestMessage = repositories.ListServiceBindingsMessage{ + Type: tools.PtrTo("key"), + } + }) + + It("returns only the ServiceBindings that match the provided type", func() { + Expect(responseServiceBindings).To(ConsistOf( + MatchFields(IgnoreExtras, Fields{"GUID": Equal(serviceBinding4.Name)}), + )) + }) + }) }) When("the user does not have access to any namespaces", func() { diff --git a/controllers/api/v1alpha1/cfservicebinding_types.go b/controllers/api/v1alpha1/cfservicebinding_types.go index 52321a82a..064ce3b80 100644 --- a/controllers/api/v1alpha1/cfservicebinding_types.go +++ b/controllers/api/v1alpha1/cfservicebinding_types.go @@ -34,9 +34,8 @@ const ( ServiceInstanceTypeAnnotationKey = "korifi.cloudfoundry.org/service-instance-type" PlanGUIDLabelKey = "korifi.cloudfoundry.org/plan-guid" - ServiceBindingGUIDLabel = "korifi.cloudfoundry.org/service-binding-guid" - ServiceCredentialBindingTypeLabel = "korifi.cloudfoundry.org/service-credential-binding-type" - CFServiceBindingFinalizerName = "cfServiceBinding.korifi.cloudfoundry.org" + ServiceBindingGUIDLabel = "korifi.cloudfoundry.org/service-binding-guid" + CFServiceBindingFinalizerName = "cfServiceBinding.korifi.cloudfoundry.org" ) // CFServiceBindingSpec defines the desired state of CFServiceBinding diff --git a/controllers/controllers/services/bindings/controller_test.go b/controllers/controllers/services/bindings/controller_test.go index dd13c75d1..dc68dbbc7 100644 --- a/controllers/controllers/services/bindings/controller_test.go +++ b/controllers/controllers/services/bindings/controller_test.go @@ -750,6 +750,23 @@ var _ = Describe("CFServiceBinding", func() { }).Should(Succeed()) }) + When("binding is of type key", func() { + BeforeEach(func() { + Expect(k8s.Patch(ctx, adminClient, binding, func() { + binding.Spec.Type = korifiv1alpha1.CFServiceBindingTypeKey + })).To(Succeed()) + }) + + It("does not create servicebinding.io", func() { + Consistently(func(g Gomega) { + sbList := &servicebindingv1beta1.ServiceBindingList{} + err := adminClient.List(ctx, sbList, client.InNamespace(testNamespace)) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(sbList.Items).To(BeEmpty()) + }).Should(Succeed()) + }) + }) + When("the credentials contain type key", func() { BeforeEach(func() { brokerClient.BindReturns(osbapi.BindResponse{ diff --git a/controllers/controllers/services/bindings/managed/controller.go b/controllers/controllers/services/bindings/managed/controller.go index 6f426342b..b389bcb56 100644 --- a/controllers/controllers/services/bindings/managed/controller.go +++ b/controllers/controllers/services/bindings/managed/controller.go @@ -89,6 +89,10 @@ func (r *ManagedBindingsReconciler) ReconcileResource(ctx context.Context, cfSer return ctrl.Result{}, err } + if cfServiceBinding.Spec.Type == korifiv1alpha1.CFServiceBindingTypeKey { + return ctrl.Result{}, nil + } + sbServiceBinding, err := r.reconcileSBServiceBinding(ctx, cfServiceBinding) if err != nil { log.Info("error creating/updating servicebinding.io servicebinding", "reason", err) diff --git a/controllers/controllers/services/bindings/sbio/servicebinding.go b/controllers/controllers/services/bindings/sbio/servicebinding.go index cdff4a669..dc373abdb 100644 --- a/controllers/controllers/services/bindings/sbio/servicebinding.go +++ b/controllers/controllers/services/bindings/sbio/servicebinding.go @@ -16,9 +16,9 @@ func ToSBServiceBinding(cfServiceBinding *korifiv1alpha1.CFServiceBinding, bindi Name: fmt.Sprintf("cf-binding-%s", cfServiceBinding.Name), Namespace: cfServiceBinding.Namespace, Labels: map[string]string{ - korifiv1alpha1.ServiceBindingGUIDLabel: cfServiceBinding.Name, - korifiv1alpha1.CFAppGUIDLabelKey: cfServiceBinding.Spec.AppRef.Name, - korifiv1alpha1.ServiceCredentialBindingTypeLabel: "app", + korifiv1alpha1.ServiceBindingGUIDLabel: cfServiceBinding.Name, + korifiv1alpha1.CFAppGUIDLabelKey: cfServiceBinding.Spec.AppRef.Name, + korifiv1alpha1.ServiceBindingTypeLabel: "app", }, }, Spec: servicebindingv1beta1.ServiceBindingSpec{ diff --git a/controllers/controllers/services/bindings/sbio/servicebinding_test.go b/controllers/controllers/services/bindings/sbio/servicebinding_test.go index edaf15d8c..58325b0ed 100644 --- a/controllers/controllers/services/bindings/sbio/servicebinding_test.go +++ b/controllers/controllers/services/bindings/sbio/servicebinding_test.go @@ -62,9 +62,9 @@ var _ = Describe("SBIO", func() { Name: "cf-binding-cf-binding", Namespace: cfServiceBinding.Namespace, Labels: map[string]string{ - korifiv1alpha1.ServiceBindingGUIDLabel: bindingName, - korifiv1alpha1.CFAppGUIDLabelKey: cfServiceBinding.Spec.AppRef.Name, - korifiv1alpha1.ServiceCredentialBindingTypeLabel: "app", + korifiv1alpha1.ServiceBindingGUIDLabel: bindingName, + korifiv1alpha1.CFAppGUIDLabelKey: cfServiceBinding.Spec.AppRef.Name, + korifiv1alpha1.ServiceBindingTypeLabel: "app", }, }, Spec: servicebindingv1beta1.ServiceBindingSpec{ diff --git a/controllers/webhooks/relationships/webhook_test.go b/controllers/webhooks/relationships/webhook_test.go index 1b71fbeef..f6bcb86ba 100644 --- a/controllers/webhooks/relationships/webhook_test.go +++ b/controllers/webhooks/relationships/webhook_test.go @@ -83,6 +83,9 @@ var _ = Describe("Setting the space-guid label", func() { Namespace: spaceNamespace, Name: uuid.NewString(), }, + Spec: korifiv1alpha1.CFServiceBindingSpec{ + Type: "app", + }, }, &korifiv1alpha1.CFServiceInstance{ ObjectMeta: metav1.ObjectMeta{