diff --git a/api/handlers/fake/cfservice_offering_repository.go b/api/handlers/fake/cfservice_offering_repository.go index e8d66fdc7..1ca8d9123 100644 --- a/api/handlers/fake/cfservice_offering_repository.go +++ b/api/handlers/fake/cfservice_offering_repository.go @@ -11,6 +11,19 @@ import ( ) type CFServiceOfferingRepository struct { + DeleteOfferingStub func(context.Context, authorization.Info, repositories.DeleteServiceOfferingMessage) error + deleteOfferingMutex sync.RWMutex + deleteOfferingArgsForCall []struct { + arg1 context.Context + arg2 authorization.Info + arg3 repositories.DeleteServiceOfferingMessage + } + deleteOfferingReturns struct { + result1 error + } + deleteOfferingReturnsOnCall map[int]struct { + result1 error + } GetServiceOfferingStub func(context.Context, authorization.Info, string) (repositories.ServiceOfferingRecord, error) getServiceOfferingMutex sync.RWMutex getServiceOfferingArgsForCall []struct { @@ -45,6 +58,69 @@ type CFServiceOfferingRepository struct { invocationsMutex sync.RWMutex } +func (fake *CFServiceOfferingRepository) DeleteOffering(arg1 context.Context, arg2 authorization.Info, arg3 repositories.DeleteServiceOfferingMessage) error { + fake.deleteOfferingMutex.Lock() + ret, specificReturn := fake.deleteOfferingReturnsOnCall[len(fake.deleteOfferingArgsForCall)] + fake.deleteOfferingArgsForCall = append(fake.deleteOfferingArgsForCall, struct { + arg1 context.Context + arg2 authorization.Info + arg3 repositories.DeleteServiceOfferingMessage + }{arg1, arg2, arg3}) + stub := fake.DeleteOfferingStub + fakeReturns := fake.deleteOfferingReturns + fake.recordInvocation("DeleteOffering", []interface{}{arg1, arg2, arg3}) + fake.deleteOfferingMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *CFServiceOfferingRepository) DeleteOfferingCallCount() int { + fake.deleteOfferingMutex.RLock() + defer fake.deleteOfferingMutex.RUnlock() + return len(fake.deleteOfferingArgsForCall) +} + +func (fake *CFServiceOfferingRepository) DeleteOfferingCalls(stub func(context.Context, authorization.Info, repositories.DeleteServiceOfferingMessage) error) { + fake.deleteOfferingMutex.Lock() + defer fake.deleteOfferingMutex.Unlock() + fake.DeleteOfferingStub = stub +} + +func (fake *CFServiceOfferingRepository) DeleteOfferingArgsForCall(i int) (context.Context, authorization.Info, repositories.DeleteServiceOfferingMessage) { + fake.deleteOfferingMutex.RLock() + defer fake.deleteOfferingMutex.RUnlock() + argsForCall := fake.deleteOfferingArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *CFServiceOfferingRepository) DeleteOfferingReturns(result1 error) { + fake.deleteOfferingMutex.Lock() + defer fake.deleteOfferingMutex.Unlock() + fake.DeleteOfferingStub = nil + fake.deleteOfferingReturns = struct { + result1 error + }{result1} +} + +func (fake *CFServiceOfferingRepository) DeleteOfferingReturnsOnCall(i int, result1 error) { + fake.deleteOfferingMutex.Lock() + defer fake.deleteOfferingMutex.Unlock() + fake.DeleteOfferingStub = nil + if fake.deleteOfferingReturnsOnCall == nil { + fake.deleteOfferingReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deleteOfferingReturnsOnCall[i] = struct { + result1 error + }{result1} +} + func (fake *CFServiceOfferingRepository) GetServiceOffering(arg1 context.Context, arg2 authorization.Info, arg3 string) (repositories.ServiceOfferingRecord, error) { fake.getServiceOfferingMutex.Lock() ret, specificReturn := fake.getServiceOfferingReturnsOnCall[len(fake.getServiceOfferingArgsForCall)] @@ -180,6 +256,8 @@ func (fake *CFServiceOfferingRepository) ListOfferingsReturnsOnCall(i int, resul func (fake *CFServiceOfferingRepository) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() + fake.deleteOfferingMutex.RLock() + defer fake.deleteOfferingMutex.RUnlock() fake.getServiceOfferingMutex.RLock() defer fake.getServiceOfferingMutex.RUnlock() fake.listOfferingsMutex.RLock() diff --git a/api/handlers/fake/cfservice_plan_repository.go b/api/handlers/fake/cfservice_plan_repository.go index 6e8320c8e..d6f53363c 100644 --- a/api/handlers/fake/cfservice_plan_repository.go +++ b/api/handlers/fake/cfservice_plan_repository.go @@ -26,6 +26,7 @@ type CFServicePlanRepository struct { result1 repositories.ServicePlanRecord result2 error } + DeletePlanVisibilityStub func(context.Context, authorization.Info, repositories.DeleteServicePlanVisibilityMessage) error deletePlanVisibilityMutex sync.RWMutex deletePlanVisibilityArgsForCall []struct { @@ -37,6 +38,19 @@ type CFServicePlanRepository struct { result1 error } deletePlanVisibilityReturnsOnCall map[int]struct { + + DeletePlanStub func(context.Context, authorization.Info, string) error + deletePlanMutex sync.RWMutex + deletePlanArgsForCall []struct { + arg1 context.Context + arg2 authorization.Info + arg3 string + } + deletePlanReturns struct { + result1 error + } + deletePlanReturnsOnCall map[int]struct { + result1 error } GetPlanStub func(context.Context, authorization.Info, string) (repositories.ServicePlanRecord, error) @@ -154,6 +168,7 @@ func (fake *CFServicePlanRepository) ApplyPlanVisibilityReturnsOnCall(i int, res }{result1, result2} } + func (fake *CFServicePlanRepository) DeletePlanVisibility(arg1 context.Context, arg2 authorization.Info, arg3 repositories.DeleteServicePlanVisibilityMessage) error { fake.deletePlanVisibilityMutex.Lock() ret, specificReturn := fake.deletePlanVisibilityReturnsOnCall[len(fake.deletePlanVisibilityArgsForCall)] @@ -166,6 +181,20 @@ func (fake *CFServicePlanRepository) DeletePlanVisibility(arg1 context.Context, fakeReturns := fake.deletePlanVisibilityReturns fake.recordInvocation("DeletePlanVisibility", []interface{}{arg1, arg2, arg3}) fake.deletePlanVisibilityMutex.Unlock() + +func (fake *CFServicePlanRepository) DeletePlan(arg1 context.Context, arg2 authorization.Info, arg3 string) error { + fake.deletePlanMutex.Lock() + ret, specificReturn := fake.deletePlanReturnsOnCall[len(fake.deletePlanArgsForCall)] + fake.deletePlanArgsForCall = append(fake.deletePlanArgsForCall, struct { + arg1 context.Context + arg2 authorization.Info + arg3 string + }{arg1, arg2, arg3}) + stub := fake.DeletePlanStub + fakeReturns := fake.deletePlanReturns + fake.recordInvocation("DeletePlan", []interface{}{arg1, arg2, arg3}) + fake.deletePlanMutex.Unlock() + if stub != nil { return stub(arg1, arg2, arg3) } @@ -175,6 +204,7 @@ func (fake *CFServicePlanRepository) DeletePlanVisibility(arg1 context.Context, return fakeReturns.result1 } + func (fake *CFServicePlanRepository) DeletePlanVisibilityCallCount() int { fake.deletePlanVisibilityMutex.RLock() defer fake.deletePlanVisibilityMutex.RUnlock() @@ -199,10 +229,37 @@ func (fake *CFServicePlanRepository) DeletePlanVisibilityReturns(result1 error) defer fake.deletePlanVisibilityMutex.Unlock() fake.DeletePlanVisibilityStub = nil fake.deletePlanVisibilityReturns = struct { + +func (fake *CFServicePlanRepository) DeletePlanCallCount() int { + fake.deletePlanMutex.RLock() + defer fake.deletePlanMutex.RUnlock() + return len(fake.deletePlanArgsForCall) +} + +func (fake *CFServicePlanRepository) DeletePlanCalls(stub func(context.Context, authorization.Info, string) error) { + fake.deletePlanMutex.Lock() + defer fake.deletePlanMutex.Unlock() + fake.DeletePlanStub = stub +} + +func (fake *CFServicePlanRepository) DeletePlanArgsForCall(i int) (context.Context, authorization.Info, string) { + fake.deletePlanMutex.RLock() + defer fake.deletePlanMutex.RUnlock() + argsForCall := fake.deletePlanArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *CFServicePlanRepository) DeletePlanReturns(result1 error) { + fake.deletePlanMutex.Lock() + defer fake.deletePlanMutex.Unlock() + fake.DeletePlanStub = nil + fake.deletePlanReturns = struct { + result1 error }{result1} } + func (fake *CFServicePlanRepository) DeletePlanVisibilityReturnsOnCall(i int, result1 error) { fake.deletePlanVisibilityMutex.Lock() defer fake.deletePlanVisibilityMutex.Unlock() @@ -213,6 +270,18 @@ func (fake *CFServicePlanRepository) DeletePlanVisibilityReturnsOnCall(i int, re }) } fake.deletePlanVisibilityReturnsOnCall[i] = struct { + +func (fake *CFServicePlanRepository) DeletePlanReturnsOnCall(i int, result1 error) { + fake.deletePlanMutex.Lock() + defer fake.deletePlanMutex.Unlock() + fake.DeletePlanStub = nil + if fake.deletePlanReturnsOnCall == nil { + fake.deletePlanReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deletePlanReturnsOnCall[i] = struct { + result1 error }{result1} } @@ -420,8 +489,13 @@ func (fake *CFServicePlanRepository) Invocations() map[string][][]interface{} { defer fake.invocationsMutex.RUnlock() fake.applyPlanVisibilityMutex.RLock() defer fake.applyPlanVisibilityMutex.RUnlock() + fake.deletePlanVisibilityMutex.RLock() defer fake.deletePlanVisibilityMutex.RUnlock() + + fake.deletePlanMutex.RLock() + defer fake.deletePlanMutex.RUnlock() + fake.getPlanMutex.RLock() defer fake.getPlanMutex.RUnlock() fake.listPlansMutex.RLock() diff --git a/api/handlers/service_broker.go b/api/handlers/service_broker.go index a124520f7..09917fab5 100644 --- a/api/handlers/service_broker.go +++ b/api/handlers/service_broker.go @@ -80,6 +80,19 @@ func (h *ServiceBroker) list(r *http.Request) (*routing.Response, error) { return routing.NewResponse(http.StatusOK).WithBody(presenter.ForList(presenter.ForServiceBroker, brokers, h.serverURL, *r.URL)), nil } +func (h *ServiceBroker) get(r *http.Request) (*routing.Response, error) { + authInfo, _ := authorization.InfoFromContext(r.Context()) + logger := logr.FromContextOrDiscard(r.Context()).WithName("handlers.service-broker.list") + + guid := routing.URLParam(r, "guid") + broker, err := h.serviceBrokerRepo.GetServiceBroker(r.Context(), authInfo, guid) + if err != nil { + return nil, apierrors.LogAndReturn(logger, apierrors.ForbiddenAsNotFound(err), "failed to get service broker") + } + + return routing.NewResponse(http.StatusOK).WithBody(presenter.ForServiceBroker(broker, h.serverURL)), nil +} + func (h *ServiceBroker) delete(r *http.Request) (*routing.Response, error) { authInfo, _ := authorization.InfoFromContext(r.Context()) logger := logr.FromContextOrDiscard(r.Context()).WithName("handlers.service-broker.delete") @@ -136,6 +149,7 @@ func (h *ServiceBroker) AuthenticatedRoutes() []routing.Route { return []routing.Route{ {Method: "POST", Pattern: ServiceBrokersPath, Handler: h.create}, {Method: "GET", Pattern: ServiceBrokersPath, Handler: h.list}, + {Method: "GET", Pattern: ServiceBrokerPath, Handler: h.get}, {Method: "DELETE", Pattern: ServiceBrokerPath, Handler: h.delete}, {Method: "PATCH", Pattern: ServiceBrokerPath, Handler: h.update}, } diff --git a/api/handlers/service_broker_test.go b/api/handlers/service_broker_test.go index 2b49a3058..03f62fe94 100644 --- a/api/handlers/service_broker_test.go +++ b/api/handlers/service_broker_test.go @@ -167,6 +167,52 @@ var _ = Describe("ServiceBroker", func() { }) }) + Describe("GET /v3/service_brokers/guid", func() { + BeforeEach(func() { + serviceBrokerRepo.GetServiceBrokerReturns(repositories.ServiceBrokerRecord{ + CFResource: model.CFResource{ + GUID: "broker-guid", + }, + }, nil) + + var err error + req, err = http.NewRequestWithContext(ctx, "GET", "/v3/service_brokers/broker-guid", nil) + Expect(err).NotTo(HaveOccurred()) + }) + + It("gets the service broker", func() { + Expect(serviceBrokerRepo.GetServiceBrokerCallCount()).To(Equal(1)) + _, actualAuthInfo, actualBrokerGUID := serviceBrokerRepo.GetServiceBrokerArgsForCall(0) + Expect(actualAuthInfo).To(Equal(authInfo)) + Expect(actualBrokerGUID).To(Equal("broker-guid")) + + Expect(rr).To(HaveHTTPStatus(http.StatusOK)) + Expect(rr).To(HaveHTTPBody(SatisfyAll( + MatchJSONPath("$.guid", "broker-guid"), + ))) + }) + + When("getting the service broker is not allowed", func() { + BeforeEach(func() { + serviceBrokerRepo.GetServiceBrokerReturns(repositories.ServiceBrokerRecord{}, apierrors.NewForbiddenError(nil, repositories.ServiceBrokerResourceType)) + }) + + It("returns a not found error", func() { + expectNotFoundError(repositories.ServiceBrokerResourceType) + }) + }) + + When("getting the service broker fails with an error", func() { + BeforeEach(func() { + serviceBrokerRepo.GetServiceBrokerReturns(repositories.ServiceBrokerRecord{}, errors.New("get-broker-err")) + }) + + It("returns an error", func() { + expectUnknownError() + }) + }) + }) + Describe("DELETE /v3/service_brokers/guid", func() { BeforeEach(func() { serviceBrokerRepo.GetServiceBrokerReturns(repositories.ServiceBrokerRecord{ diff --git a/api/handlers/service_offering.go b/api/handlers/service_offering.go index d7e603c3f..c5f49ccd7 100644 --- a/api/handlers/service_offering.go +++ b/api/handlers/service_offering.go @@ -26,6 +26,7 @@ const ( type CFServiceOfferingRepository interface { GetServiceOffering(context.Context, authorization.Info, string) (repositories.ServiceOfferingRecord, error) ListOfferings(context.Context, authorization.Info, repositories.ListServiceOfferingMessage) ([]repositories.ServiceOfferingRecord, error) + DeleteOffering(context.Context, authorization.Info, repositories.DeleteServiceOfferingMessage) error } type ServiceOffering struct { @@ -103,6 +104,23 @@ func (h *ServiceOffering) list(r *http.Request) (*routing.Response, error) { return routing.NewResponse(http.StatusOK).WithBody(presenter.ForList(presenter.ForServiceOffering, serviceOfferingList, h.serverURL, *r.URL, includedResources...)), nil } +func (h *ServiceOffering) delete(r *http.Request) (*routing.Response, error) { + authInfo, _ := authorization.InfoFromContext(r.Context()) + logger := logr.FromContextOrDiscard(r.Context()).WithName("handlers.service-offering.delete") + + payload := new(payloads.ServiceOfferingDelete) + if err := h.requestValidator.DecodeAndValidateURLValues(r, payload); err != nil { + return nil, apierrors.LogAndReturn(logger, err, "Unable to decode request query parameters") + } + + serviceOfferingGUID := routing.URLParam(r, "guid") + if err := h.serviceOfferingRepo.DeleteOffering(r.Context(), authInfo, payload.ToMessage(serviceOfferingGUID)); err != nil { + return nil, apierrors.LogAndReturn(logger, err, "failed to delete service offering: %s", serviceOfferingGUID) + } + + return routing.NewResponse(http.StatusNoContent), nil +} + func (h *ServiceOffering) UnauthenticatedRoutes() []routing.Route { return nil } @@ -111,5 +129,6 @@ func (h *ServiceOffering) AuthenticatedRoutes() []routing.Route { return []routing.Route{ {Method: "GET", Pattern: ServiceOfferingPath, Handler: h.get}, {Method: "GET", Pattern: ServiceOfferingsPath, Handler: h.list}, + {Method: "DELETE", Pattern: ServiceOfferingPath, Handler: h.delete}, } } diff --git a/api/handlers/service_offering_test.go b/api/handlers/service_offering_test.go index 12a373958..2877256ef 100644 --- a/api/handlers/service_offering_test.go +++ b/api/handlers/service_offering_test.go @@ -5,6 +5,7 @@ import ( "log" "net/http" + apierrors "code.cloudfoundry.org/korifi/api/errors" . "code.cloudfoundry.org/korifi/api/handlers" "code.cloudfoundry.org/korifi/api/handlers/fake" "code.cloudfoundry.org/korifi/api/payloads" @@ -271,4 +272,61 @@ var _ = Describe("ServiceOffering", func() { }) }) }) + + Describe("DELETE /v3/service_offerings/:guid", func() { + JustBeforeEach(func() { + req, err := http.NewRequestWithContext(ctx, "DELETE", "/v3/service_offerings/offering-guid", nil) + Expect(err).NotTo(HaveOccurred()) + + routerBuilder.Build().ServeHTTP(rr, req) + }) + + It("deletes the service offering", func() { + Expect(serviceOfferingRepo.DeleteOfferingCallCount()).To(Equal(1)) + _, actualAuthInfo, actualDeleteMessage := serviceOfferingRepo.DeleteOfferingArgsForCall(0) + Expect(actualAuthInfo).To(Equal(authInfo)) + Expect(actualDeleteMessage.GUID).To(Equal("offering-guid")) + Expect(actualDeleteMessage.Purge).To(BeFalse()) + + Expect(rr).To(HaveHTTPStatus(http.StatusNoContent)) + }) + + When("deleting the service offering fails with not found", func() { + BeforeEach(func() { + serviceOfferingRepo.DeleteOfferingReturns(apierrors.NewNotFoundError(nil, repositories.ServiceOfferingResourceType)) + }) + + It("returns 404 Not Found", func() { + expectNotFoundError("Service Offering") + }) + }) + + When("deleting the service offering fails", func() { + BeforeEach(func() { + serviceOfferingRepo.DeleteOfferingReturns(errors.New("boom")) + }) + + It("returns 500 Internal Server Error", func() { + expectUnknownError() + }) + }) + + When("purging is set to true", func() { + BeforeEach(func() { + requestValidator.DecodeAndValidateURLValuesStub = decodeAndValidateURLValuesStub(&payloads.ServiceOfferingDelete{ + Purge: true, + }) + }) + + It("purges the service offering", func() { + Expect(serviceOfferingRepo.DeleteOfferingCallCount()).To(Equal(1)) + _, actualAuthInfo, actualDeleteMessage := serviceOfferingRepo.DeleteOfferingArgsForCall(0) + Expect(actualAuthInfo).To(Equal(authInfo)) + Expect(actualDeleteMessage.GUID).To(Equal("offering-guid")) + Expect(actualDeleteMessage.Purge).To(BeTrue()) + + Expect(rr).To(HaveHTTPStatus(http.StatusNoContent)) + }) + }) + }) }) diff --git a/api/handlers/service_plan.go b/api/handlers/service_plan.go index f4f7c0947..7aff5e89e 100644 --- a/api/handlers/service_plan.go +++ b/api/handlers/service_plan.go @@ -29,6 +29,7 @@ type CFServicePlanRepository interface { ApplyPlanVisibility(context.Context, authorization.Info, repositories.ApplyServicePlanVisibilityMessage) (repositories.ServicePlanRecord, error) UpdatePlanVisibility(context.Context, authorization.Info, repositories.UpdateServicePlanVisibilityMessage) (repositories.ServicePlanRecord, error) DeletePlanVisibility(context.Context, authorization.Info, repositories.DeleteServicePlanVisibilityMessage) error + DeletePlan(context.Context, authorization.Info, string) error } type ServicePlan struct { @@ -145,6 +146,14 @@ func (h *ServicePlan) deletePlanVisibility(r *http.Request) (*routing.Response, OrgGUID: orgGUID, }); err != nil { return nil, apierrors.LogAndReturn(logger, err, "failed to delete org: %s for plan visibility", orgGUID) + +func (h *ServicePlan) delete(r *http.Request) (*routing.Response, error) { + authInfo, _ := authorization.InfoFromContext(r.Context()) + logger := logr.FromContextOrDiscard(r.Context()).WithName("handlers.service-plan.delete") + + planGUID := routing.URLParam(r, "guid") + if err := h.servicePlanRepo.DeletePlan(r.Context(), authInfo, planGUID); err != nil { + return nil, apierrors.LogAndReturn(logger, err, "failed to delete plan: %s", planGUID) } return routing.NewResponse(http.StatusNoContent), nil @@ -161,5 +170,6 @@ func (h *ServicePlan) AuthenticatedRoutes() []routing.Route { {Method: "POST", Pattern: ServicePlanVisibilityPath, Handler: h.applyPlanVisibility}, {Method: "PATCH", Pattern: ServicePlanVisibilityPath, Handler: h.updatePlanVisibility}, {Method: "DELETE", Pattern: ServicePlanVisibilityOrgPath, Handler: h.deletePlanVisibility}, + {Method: "DELETE", Pattern: ServicePlanPath, Handler: h.delete}, } } diff --git a/api/handlers/service_plan_test.go b/api/handlers/service_plan_test.go index 72d1a6e3a..fbc343e32 100644 --- a/api/handlers/service_plan_test.go +++ b/api/handlers/service_plan_test.go @@ -5,6 +5,7 @@ import ( "net/http" "strings" + apierrors "code.cloudfoundry.org/korifi/api/errors" . "code.cloudfoundry.org/korifi/api/handlers" "code.cloudfoundry.org/korifi/api/handlers/fake" "code.cloudfoundry.org/korifi/api/payloads" @@ -351,6 +352,7 @@ var _ = Describe("ServicePlan", func() { }) }) + Describe("DELETE /v3/service_plans/{guid}/visibility/{org-guid}", func() { JustBeforeEach(func() { req, err := http.NewRequestWithContext(ctx, "DELETE", "/v3/service_plans/my-service-plan/visibility/org-guid", nil) @@ -358,8 +360,8 @@ var _ = Describe("ServicePlan", func() { routerBuilder.Build().ServeHTTP(rr, req) }) - - It("deletes the service plan visibility", func() { + + It("deletes the service plan visibility", func() { Expect(servicePlanRepo.DeletePlanVisibilityCallCount()).To(Equal(1)) _, actualAuthInfo, actualMessage := servicePlanRepo.DeletePlanVisibilityArgsForCall(0) Expect(actualAuthInfo).To(Equal(authInfo)) @@ -368,12 +370,56 @@ var _ = Describe("ServicePlan", func() { Expect(rr).To(HaveHTTPStatus(http.StatusNoContent)) }) + + Describe("DELETE /v3/service_plans/:guid", func() { + BeforeEach(func() { + servicePlanRepo.GetPlanReturns(repositories.ServicePlanRecord{ + CFResource: model.CFResource{ + GUID: "plan-guid", + }, + ServiceOfferingGUID: "service-offering-guid", + }, nil) + }) + + JustBeforeEach(func() { + req, err := http.NewRequestWithContext(ctx, "DELETE", "/v3/service_plans/plan-guid", nil) + + When("deleting the visibility fails with an error", func() { BeforeEach(func() { servicePlanRepo.DeletePlanVisibilityReturns(errors.New("visibility-err")) }) It("returns an error", func() { + + It("deletes the service plan", func() { + Expect(servicePlanRepo.DeletePlanCallCount()).To(Equal(1)) + _, actualAuthInfo, actualPlanGUID := servicePlanRepo.DeletePlanArgsForCall(0) + Expect(actualAuthInfo).To(Equal(authInfo)) + Expect(actualPlanGUID).To(Equal("plan-guid")) + + Expect(rr).Should(HaveHTTPStatus(http.StatusNoContent)) + }) + + When("deleting the service plan fails with not found", func() { + BeforeEach(func() { + servicePlanRepo.DeletePlanReturns( + apierrors.NewNotFoundError(errors.New("not found"), repositories.ServicePlanResourceType), + ) + }) + + It("returns 404 Not Found", func() { + expectNotFoundError("Service Plan") + }) + }) + + When("deleting the service plan fails", func() { + BeforeEach(func() { + servicePlanRepo.DeletePlanReturns(errors.New("boom")) + }) + + It("returns 500 Internal Server Error", func() { + expectUnknownError() }) }) diff --git a/api/main.go b/api/main.go index 79c70ccb5..b3aaa1ae6 100644 --- a/api/main.go +++ b/api/main.go @@ -244,7 +244,7 @@ func main() { ) metricsRepo := repositories.NewMetricsRepo(userClientFactory) serviceBrokerRepo := repositories.NewServiceBrokerRepo(userClientFactory, cfg.RootNamespace) - serviceOfferingRepo := repositories.NewServiceOfferingRepo(userClientFactory, cfg.RootNamespace, serviceBrokerRepo) + serviceOfferingRepo := repositories.NewServiceOfferingRepo(userClientFactory, cfg.RootNamespace, serviceBrokerRepo, nsPermissions) servicePlanRepo := repositories.NewServicePlanRepo(userClientFactory, cfg.RootNamespace, orgRepo) processStats := actions.NewProcessStats(processRepo, appRepo, metricsRepo) diff --git a/api/payloads/service_offering.go b/api/payloads/service_offering.go index 9570249bc..d0da4aa26 100644 --- a/api/payloads/service_offering.go +++ b/api/payloads/service_offering.go @@ -95,3 +95,27 @@ func (l *ServiceOfferingList) DecodeFromURLValues(values url.Values) error { l.IncludeResourceRules = append(l.IncludeResourceRules, params.ParseFields(values)...) return nil } + +type ServiceOfferingDelete struct { + Purge bool +} + +func (d *ServiceOfferingDelete) SupportedKeys() []string { + return []string{"purge"} +} + +func (d *ServiceOfferingDelete) DecodeFromURLValues(values url.Values) error { + var err error + if d.Purge, err = getBool(values, "purge"); err != nil { + return err + } + + return nil +} + +func (d *ServiceOfferingDelete) ToMessage(guid string) repositories.DeleteServiceOfferingMessage { + return repositories.DeleteServiceOfferingMessage{ + GUID: guid, + Purge: d.Purge, + } +} diff --git a/api/payloads/service_offering_test.go b/api/payloads/service_offering_test.go index 6f8e6fbfd..3f2a307e8 100644 --- a/api/payloads/service_offering_test.go +++ b/api/payloads/service_offering_test.go @@ -72,3 +72,24 @@ var _ = Describe("ServiceOfferingList", func() { }) }) }) + +var _ = Describe("ServiceOfferingDelete", func() { + DescribeTable("valid query", + func(query string, expectedServiceOfferingDelete payloads.ServiceOfferingDelete) { + actualServiceOfferingDelete, decodeErr := decodeQuery[payloads.ServiceOfferingDelete](query) + + Expect(decodeErr).NotTo(HaveOccurred()) + Expect(*actualServiceOfferingDelete).To(Equal(expectedServiceOfferingDelete)) + }, + Entry("purge", "purge=true", payloads.ServiceOfferingDelete{Purge: true}), + ) + + DescribeTable("invalid query", + func(query string, expectedErrMsg string) { + _, decodeErr := decodeQuery[payloads.ServiceOfferingDelete](query) + Expect(decodeErr).To(HaveOccurred()) + }, + Entry("unsuported param", "foo=bar", "unsupported query parameter: foo"), + Entry("invalid value for purge", "purge=foo", "invalid syntax"), + ) +}) diff --git a/api/repositories/service_instance_repository.go b/api/repositories/service_instance_repository.go index 12b08c405..e5e0ac42b 100644 --- a/api/repositories/service_instance_repository.go +++ b/api/repositories/service_instance_repository.go @@ -481,7 +481,7 @@ func (r *ServiceInstanceRepo) DeleteServiceInstance(ctx context.Context, authInf if message.Purge { if err = k8s.PatchResource(ctx, userClient, serviceInstance, func() { - controllerutil.RemoveFinalizer(serviceInstance, korifiv1alpha1.CFManagedServiceInstanceFinalizerName) + controllerutil.RemoveFinalizer(serviceInstance, korifiv1alpha1.CFServiceInstanceFinalizerName) }); err != nil { return ServiceInstanceRecord{}, fmt.Errorf("failed to remove finalizer for service instance: %s, %w", message.GUID, apierrors.FromK8sError(err, ServiceInstanceResourceType)) } diff --git a/api/repositories/service_instance_repository_test.go b/api/repositories/service_instance_repository_test.go index 3ae36c2c4..4a58d9967 100644 --- a/api/repositories/service_instance_repository_test.go +++ b/api/repositories/service_instance_repository_test.go @@ -1081,7 +1081,7 @@ var _ = Describe("ServiceInstanceRepository", func() { BeforeEach(func() { serviceInstance = createServiceInstanceCR(ctx, k8sClient, prefixedGUID("service-instance"), space.Name, "the-service-instance", prefixedGUID("secret")) - serviceInstance.Finalizers = append(serviceInstance.Finalizers, korifiv1alpha1.CFManagedServiceInstanceFinalizerName) + serviceInstance.Finalizers = append(serviceInstance.Finalizers, korifiv1alpha1.CFServiceInstanceFinalizerName) Expect(k8sClient.Update(ctx, serviceInstance)).To(Succeed()) serviceBinding = &korifiv1alpha1.CFServiceBinding{ diff --git a/api/repositories/service_offering_repository.go b/api/repositories/service_offering_repository.go index c54766958..ba8ab4627 100644 --- a/api/repositories/service_offering_repository.go +++ b/api/repositories/service_offering_repository.go @@ -11,11 +11,13 @@ import ( "code.cloudfoundry.org/korifi/model" "code.cloudfoundry.org/korifi/model/services" "code.cloudfoundry.org/korifi/tools" + "code.cloudfoundry.org/korifi/tools/k8s" "github.com/BooleanCat/go-functional/v2/it" "github.com/BooleanCat/go-functional/v2/it/itx" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) const ServiceOfferingResourceType = "Service Offering" @@ -33,9 +35,10 @@ func (r ServiceOfferingRecord) Relationships() map[string]string { } type ServiceOfferingRepo struct { - userClientFactory authorization.UserK8sClientFactory - rootNamespace string - brokerRepo *ServiceBrokerRepo + userClientFactory authorization.UserK8sClientFactory + rootNamespace string + brokerRepo *ServiceBrokerRepo + namespacePermissions *authorization.NamespacePermissions } type ListServiceOfferingMessage struct { @@ -44,6 +47,11 @@ type ListServiceOfferingMessage struct { BrokerNames []string } +type DeleteServiceOfferingMessage struct { + GUID string + Purge bool +} + func (m *ListServiceOfferingMessage) matches(cfServiceOffering korifiv1alpha1.CFServiceOffering) bool { return tools.EmptyOrContains(m.Names, cfServiceOffering.Spec.Name) && tools.EmptyOrContains(m.GUIDs, cfServiceOffering.Name) && @@ -54,11 +62,13 @@ func NewServiceOfferingRepo( userClientFactory authorization.UserK8sClientFactory, rootNamespace string, brokerRepo *ServiceBrokerRepo, + namespacePermissions *authorization.NamespacePermissions, ) *ServiceOfferingRepo { return &ServiceOfferingRepo{ - userClientFactory: userClientFactory, - rootNamespace: rootNamespace, - brokerRepo: brokerRepo, + userClientFactory: userClientFactory, + rootNamespace: rootNamespace, + brokerRepo: brokerRepo, + namespacePermissions: namespacePermissions, } } @@ -103,6 +113,36 @@ func (r *ServiceOfferingRepo) ListOfferings(ctx context.Context, authInfo author return slices.Collect(it.Map(itx.FromSlice(offeringsList.Items).Filter(message.matches), offeringToRecord)), nil } +func (r *ServiceOfferingRepo) DeleteOffering(ctx context.Context, authInfo authorization.Info, message DeleteServiceOfferingMessage) error { + userClient, err := r.userClientFactory.BuildClient(authInfo) + if err != nil { + return fmt.Errorf("failed to build user client: %w", err) + } + + offering := &korifiv1alpha1.CFServiceOffering{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: r.rootNamespace, + Name: message.GUID, + }, + } + + if err = userClient.Get(ctx, client.ObjectKeyFromObject(offering), offering); err != nil { + return fmt.Errorf("failed to get service offering: %w", apierrors.FromK8sError(err, ServiceOfferingResourceType)) + } + + if message.Purge { + if err = r.purgeRelatedResources(ctx, authInfo, userClient, message.GUID); err != nil { + return fmt.Errorf("failed to purge service offering resources: %w", apierrors.FromK8sError(err, ServiceOfferingResourceType)) + } + } + + if err = userClient.Delete(ctx, offering); err != nil { + return fmt.Errorf("failed to delete service offering: %w", apierrors.FromK8sError(err, ServiceOfferingResourceType)) + } + + return nil +} + func offeringToRecord(offering korifiv1alpha1.CFServiceOffering) ServiceOfferingRecord { return ServiceOfferingRecord{ ServiceOffering: offering.Spec.ServiceOffering, @@ -117,3 +157,112 @@ func offeringToRecord(offering korifiv1alpha1.CFServiceOffering) ServiceOffering ServiceBrokerGUID: offering.Labels[korifiv1alpha1.RelServiceBrokerGUIDLabel], } } + +func (r *ServiceOfferingRepo) purgeRelatedResources(ctx context.Context, authInfo authorization.Info, userClient client.WithWatch, offeringGUID string) error { + planGUIDs, err := r.deleteServicePlans(ctx, userClient, offeringGUID) + if err != nil { + return fmt.Errorf("failed to delete service plans: %w", apierrors.FromK8sError(err, ServicePlanResourceType)) + } + + authorizedSpaceNamespacesIter, err := authorizedSpaceNamespaces(ctx, authInfo, r.namespacePermissions) + if err != nil { + return fmt.Errorf("failed to list namespaces: %w", err) + } + + serviceInstances, err := r.fetchServiceInstances(ctx, userClient, authorizedSpaceNamespacesIter, planGUIDs) + if err != nil { + return fmt.Errorf("failed to list service instances: %w", err) + } + + for _, instance := range serviceInstances { + err = k8s.PatchResource(ctx, userClient, &instance, func() { + controllerutil.RemoveFinalizer(&instance, korifiv1alpha1.CFServiceInstanceFinalizerName) + }) + if err != nil { + return fmt.Errorf("failed to remove finalizer for service instance: %s, %w", instance.Name, apierrors.FromK8sError(err, ServiceInstanceResourceType)) + } + + if err = userClient.Delete(ctx, &instance); err != nil { + return fmt.Errorf("failed to delete service instance: %w", apierrors.FromK8sError(err, ServiceInstanceResourceType)) + } + + } + + serviceBindings, err := r.fetchServiceBindings(ctx, userClient, authorizedSpaceNamespacesIter, planGUIDs) + if err != nil { + return fmt.Errorf("failed to list service bindings: %w", err) + } + + for _, binding := range serviceBindings { + err = k8s.PatchResource(ctx, userClient, &binding, func() { + controllerutil.RemoveFinalizer(&binding, korifiv1alpha1.CFServiceBindingFinalizerName) + }) + if err != nil { + return fmt.Errorf("failed to remove finalizer for service binding: %s, %w", binding.Name, apierrors.FromK8sError(err, ServiceBindingResourceType)) + } + } + + return nil +} + +func (r *ServiceOfferingRepo) deleteServicePlans(ctx context.Context, userClient client.WithWatch, offeringGUID string) ([]string, error) { + var planGUIDs []string + plans := &korifiv1alpha1.CFServicePlanList{} + + if err := userClient.List(ctx, plans, client.InNamespace(r.rootNamespace), client.MatchingLabels{ + korifiv1alpha1.RelServiceOfferingGUIDLabel: offeringGUID, + }); err != nil { + return []string{}, fmt.Errorf("failed to list service plans: %w", err) + } + + for _, plan := range plans.Items { + planGUIDs = append(planGUIDs, plan.Name) + if err := userClient.Delete(ctx, &plan); err != nil { + return []string{}, apierrors.FromK8sError(err, ServicePlanResourceType) + } + } + + return planGUIDs, nil +} + +func (r *ServiceOfferingRepo) fetchServiceInstances(ctx context.Context, userClient client.WithWatch, authorizedNamespaces itx.Iterator[string], planGUIDs []string) ([]korifiv1alpha1.CFServiceInstance, error) { + var serviceInstances []korifiv1alpha1.CFServiceInstance + + for _, ns := range authorizedNamespaces.Collect() { + instances := new(korifiv1alpha1.CFServiceInstanceList) + + err := userClient.List(ctx, instances, client.InNamespace(ns)) + if err != nil { + return []korifiv1alpha1.CFServiceInstance{}, fmt.Errorf("failed to list service instances: %w", err) + } + + filtered := itx.FromSlice(instances.Items).Filter(func(serviceInstance korifiv1alpha1.CFServiceInstance) bool { + return tools.EmptyOrContains(planGUIDs, serviceInstance.Spec.PlanGUID) + }).Collect() + + serviceInstances = append(serviceInstances, filtered...) + } + + return serviceInstances, nil +} + +func (r *ServiceOfferingRepo) fetchServiceBindings(ctx context.Context, userClient client.WithWatch, authorizedNamespaces itx.Iterator[string], planGUIDs []string) ([]korifiv1alpha1.CFServiceBinding, error) { + var serviceBindings []korifiv1alpha1.CFServiceBinding + + for _, ns := range authorizedNamespaces.Collect() { + bindings := new(korifiv1alpha1.CFServiceBindingList) + + err := userClient.List(ctx, bindings, client.InNamespace(ns)) + if err != nil { + return []korifiv1alpha1.CFServiceBinding{}, fmt.Errorf("failed to list service bindings: %w", err) + } + + filtered := itx.FromSlice(bindings.Items).Filter(func(serviceBinding korifiv1alpha1.CFServiceBinding) bool { + return tools.EmptyOrContains(planGUIDs, serviceBinding.Labels[korifiv1alpha1.PlanGUIDLabelKey]) + }).Collect() + + serviceBindings = append(serviceBindings, filtered...) + } + + return serviceBindings, nil +} diff --git a/api/repositories/service_offering_repository_test.go b/api/repositories/service_offering_repository_test.go index 3128e63f1..b461b4acd 100644 --- a/api/repositories/service_offering_repository_test.go +++ b/api/repositories/service_offering_repository_test.go @@ -1,7 +1,9 @@ package repositories_test import ( + "context" "errors" + "fmt" apierrors "code.cloudfoundry.org/korifi/api/errors" "code.cloudfoundry.org/korifi/api/repositories" @@ -9,8 +11,11 @@ import ( "code.cloudfoundry.org/korifi/model/services" "code.cloudfoundry.org/korifi/tools" . "github.com/onsi/gomega/gstruct" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" @@ -18,7 +23,11 @@ import ( ) var _ = Describe("ServiceOfferingRepo", func() { - var repo *repositories.ServiceOfferingRepo + var ( + repo *repositories.ServiceOfferingRepo + org *korifiv1alpha1.CFOrg + space *korifiv1alpha1.CFSpace + ) BeforeEach(func() { repo = repositories.NewServiceOfferingRepo( @@ -28,7 +37,11 @@ var _ = Describe("ServiceOfferingRepo", func() { userClientFactory, rootNamespace, ), + nsPerms, ) + + org = createOrgWithCleanup(ctx, uuid.NewString()) + space = createSpaceWithCleanup(ctx, org.Name, uuid.NewString()) }) Describe("Get", func() { @@ -317,4 +330,148 @@ var _ = Describe("ServiceOfferingRepo", func() { }) }) }) + + Describe("Delete-off", func() { + var ( + plan *korifiv1alpha1.CFServicePlan + offering *korifiv1alpha1.CFServiceOffering + instance *korifiv1alpha1.CFServiceInstance + binding *korifiv1alpha1.CFServiceBinding + message repositories.DeleteServiceOfferingMessage + deleteErr error + ) + + BeforeEach(func() { + offering = &korifiv1alpha1.CFServiceOffering{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: rootNamespace, + Name: uuid.NewString(), + }, + Spec: korifiv1alpha1.CFServiceOfferingSpec{ + ServiceOffering: services.ServiceOffering{ + Name: "my-offering", + Description: "my offering description", + Tags: []string{"t1"}, + Requires: []string{"r1"}, + }, + }, + } + Expect(k8sClient.Create(ctx, offering)).To(Succeed()) + + plan = &korifiv1alpha1.CFServicePlan{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: rootNamespace, + Name: uuid.NewString(), + Labels: map[string]string{ + korifiv1alpha1.RelServiceOfferingGUIDLabel: offering.Name, + }, + }, + Spec: korifiv1alpha1.CFServicePlanSpec{ + ServicePlan: services.ServicePlan{ + Name: "my-service-plan", + Free: true, + Description: "service plan description", + }, + Visibility: korifiv1alpha1.ServicePlanVisibility{ + Type: korifiv1alpha1.PublicServicePlanVisibilityType, + }, + }, + } + Expect(k8sClient.Create(ctx, plan)).To(Succeed()) + + instance = &korifiv1alpha1.CFServiceInstance{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: space.Name, + Name: uuid.NewString(), + Finalizers: []string{ + korifiv1alpha1.CFServiceInstanceFinalizerName, + }, + }, + Spec: korifiv1alpha1.CFServiceInstanceSpec{ + PlanGUID: plan.Name, + Type: "user-provided", + }, + } + Expect(k8sClient.Create(ctx, instance)).To(Succeed()) + + binding = &korifiv1alpha1.CFServiceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: space.Name, + Labels: map[string]string{ + korifiv1alpha1.PlanGUIDLabelKey: plan.Name, + }, + Finalizers: []string{ + korifiv1alpha1.CFServiceBindingFinalizerName, + }, + }, + Spec: korifiv1alpha1.CFServiceBindingSpec{ + Service: corev1.ObjectReference{ + Kind: "CFServiceInstance", + APIVersion: korifiv1alpha1.SchemeGroupVersion.Identifier(), + Name: instance.Name, + }, + AppRef: corev1.LocalObjectReference{ + Name: "some-app-guid", + }, + }, + } + Expect(k8sClient.Create(ctx, binding)).To(Succeed()) + + createRoleBinding(ctx, userName, spaceDeveloperRole.Name, space.Name) + createRoleBinding(ctx, userName, adminRole.Name, rootNamespace) + + message = repositories.DeleteServiceOfferingMessage{GUID: offering.Name} + }) + + JustBeforeEach(func() { + deleteErr = repo.DeleteOffering(ctx, authInfo, message) + }) + + It("successfully deletes the offering", func() { + Expect(deleteErr).ToNot(HaveOccurred()) + + namespacedName := types.NamespacedName{ + Name: offering.Name, + Namespace: rootNamespace, + } + + err := k8sClient.Get(context.Background(), namespacedName, &korifiv1alpha1.CFServiceOffering{}) + Expect(k8serrors.IsNotFound(err)).To(BeTrue(), fmt.Sprintf("error: %+v", err)) + }) + + When("the service offering does not exist", func() { + BeforeEach(func() { + message.GUID = "does-not-exist" + }) + + It("returns a error", func() { + Expect(errors.As(deleteErr, &apierrors.NotFoundError{})).To(BeTrue()) + }) + }) + + When("Purge is set to true", func() { + BeforeEach(func() { + message.Purge = true + }) + It("successfully deletes the offering and all related resources", func() { + Expect(deleteErr).ToNot(HaveOccurred()) + + err := k8sClient.Get(context.Background(), types.NamespacedName{Name: offering.Name, Namespace: rootNamespace}, &korifiv1alpha1.CFServiceOffering{}) + Expect(k8serrors.IsNotFound(err)).To(BeTrue(), fmt.Sprintf("error: %+v", err)) + + err = k8sClient.Get(context.Background(), types.NamespacedName{Name: plan.Name, Namespace: rootNamespace}, &korifiv1alpha1.CFServicePlan{}) + Expect(k8serrors.IsNotFound(err)).To(BeTrue(), fmt.Sprintf("error: %+v", err)) + + err = k8sClient.Get(context.Background(), types.NamespacedName{Name: instance.Name, Namespace: space.Name}, &korifiv1alpha1.CFServiceInstance{}) + Expect(k8serrors.IsNotFound(err)).To(BeTrue(), fmt.Sprintf("error: %+v", err)) + + serviceBinding := new(korifiv1alpha1.CFServiceBinding) + err = k8sClient.Get(context.Background(), types.NamespacedName{Name: binding.Name, Namespace: space.Name}, serviceBinding) + + Expect(err).ToNot(HaveOccurred()) + Expect(serviceBinding.Finalizers).To(BeEmpty()) + }) + }) + }) }) diff --git a/api/repositories/service_plan_repository.go b/api/repositories/service_plan_repository.go index cffa0a04f..02244aa0a 100644 --- a/api/repositories/service_plan_repository.go +++ b/api/repositories/service_plan_repository.go @@ -171,6 +171,22 @@ func (r *ServicePlanRepo) UpdatePlanVisibility(ctx context.Context, authInfo aut func (r *ServicePlanRepo) DeletePlanVisibility(ctx context.Context, authInfo authorization.Info, message DeleteServicePlanVisibilityMessage) error { if _, err := r.patchServicePlan(ctx, authInfo, message.PlanGUID, message.apply); err != nil { return err + +func (r *ServicePlanRepo) DeletePlan(ctx context.Context, authInfo authorization.Info, planGUID string) error { + userClient, err := r.userClientFactory.BuildClient(authInfo) + if err != nil { + return fmt.Errorf("failed to build user client: %w", err) + } + + cfServicePlan := &korifiv1alpha1.CFServicePlan{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: r.rootNamespace, + Name: planGUID, + }, + } + + if err := userClient.Delete(ctx, cfServicePlan); err != nil { + return apierrors.FromK8sError(err, ServicePlanResourceType) } return nil diff --git a/api/repositories/service_plan_repository_test.go b/api/repositories/service_plan_repository_test.go index 9ad16a53d..6dc8bb316 100644 --- a/api/repositories/service_plan_repository_test.go +++ b/api/repositories/service_plan_repository_test.go @@ -1,7 +1,9 @@ package repositories_test import ( + "context" "errors" + "fmt" apierrors "code.cloudfoundry.org/korifi/api/errors" "code.cloudfoundry.org/korifi/api/repositories" @@ -12,8 +14,10 @@ import ( "code.cloudfoundry.org/korifi/tools" "code.cloudfoundry.org/korifi/tools/k8s" . "github.com/onsi/gomega/gstruct" + k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/google/uuid" @@ -687,4 +691,35 @@ var _ = Describe("ServicePlanRepo", func() { }) }) }) + + Describe("Delete", func() { + var deleteErr error + + JustBeforeEach(func() { + createRoleBinding(ctx, userName, adminRole.Name, rootNamespace) + deleteErr = repo.DeletePlan(ctx, authInfo, planGUID) + }) + + It("deletes the service plan", func() { + Expect(deleteErr).ToNot(HaveOccurred()) + + namespacedName := types.NamespacedName{ + Name: planGUID, + Namespace: rootNamespace, + } + + err := k8sClient.Get(context.Background(), namespacedName, &korifiv1alpha1.CFServicePlan{}) + Expect(k8serrors.IsNotFound(err)).To(BeTrue(), fmt.Sprintf("error: %+v", err)) + }) + + When("the service plan does not exist", func() { + BeforeEach(func() { + planGUID = "does-not-exist" + }) + + It("returns a not found error", func() { + Expect(errors.As(deleteErr, &apierrors.NotFoundError{})).To(BeTrue()) + }) + }) + }) }) diff --git a/controllers/api/v1alpha1/cfserviceinstance_types.go b/controllers/api/v1alpha1/cfserviceinstance_types.go index 56269457c..858962cfe 100644 --- a/controllers/api/v1alpha1/cfserviceinstance_types.go +++ b/controllers/api/v1alpha1/cfserviceinstance_types.go @@ -29,7 +29,7 @@ const ( UserProvidedType = "user-provided" ManagedType = "managed" - CFManagedServiceInstanceFinalizerName = "managed.cfServiceInstance.korifi.cloudfoundry.org" + CFServiceInstanceFinalizerName = "cfServiceInstance.korifi.cloudfoundry.org" ProvisioningFailedCondition = "ProvisioningFailed" ) diff --git a/controllers/controllers/services/brokers/controller.go b/controllers/controllers/services/brokers/controller.go index 3712fd333..9181600eb 100644 --- a/controllers/controllers/services/brokers/controller.go +++ b/controllers/controllers/services/brokers/controller.go @@ -102,8 +102,8 @@ func (r *Reconciler) secretToServiceBroker(ctx context.Context, o client.Object) //+kubebuilder:rbac:groups=korifi.cloudfoundry.org,resources=cfservicebrokers,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=korifi.cloudfoundry.org,resources=cfservicebrokers/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=korifi.cloudfoundry.org,resources=cfserviceofferings,verbs=get;list;watch;create;update;patch -//+kubebuilder:rbac:groups=korifi.cloudfoundry.org,resources=cfserviceplans,verbs=get;list;watch;create;update;patch +//+kubebuilder:rbac:groups=korifi.cloudfoundry.org,resources=cfserviceofferings,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=korifi.cloudfoundry.org,resources=cfserviceplans,verbs=get;list;watch;create;update;patch;delete func (r *Reconciler) ReconcileResource(ctx context.Context, cfServiceBroker *korifiv1alpha1.CFServiceBroker) (ctrl.Result, error) { log := logr.FromContextOrDiscard(ctx).WithValues("broker-id", cfServiceBroker.Name) diff --git a/controllers/controllers/services/instances/managed/controller.go b/controllers/controllers/services/instances/managed/controller.go index a83194c83..12b563ec5 100644 --- a/controllers/controllers/services/instances/managed/controller.go +++ b/controllers/controllers/services/instances/managed/controller.go @@ -329,7 +329,7 @@ func (r *Reconciler) finalizeCFServiceInstance( ) (ctrl.Result, error) { log := logr.FromContextOrDiscard(ctx).WithName("finalizeCFServiceInstance") - if !controllerutil.ContainsFinalizer(serviceInstance, korifiv1alpha1.CFManagedServiceInstanceFinalizerName) { + if !controllerutil.ContainsFinalizer(serviceInstance, korifiv1alpha1.CFServiceInstanceFinalizerName) { return ctrl.Result{}, nil } @@ -338,7 +338,7 @@ func (r *Reconciler) finalizeCFServiceInstance( log.Error(err, "failed to deprovision service instance with broker") } - controllerutil.RemoveFinalizer(serviceInstance, korifiv1alpha1.CFManagedServiceInstanceFinalizerName) + controllerutil.RemoveFinalizer(serviceInstance, korifiv1alpha1.CFServiceInstanceFinalizerName) log.V(1).Info("finalizer removed") return ctrl.Result{}, nil diff --git a/controllers/controllers/services/instances/managed/controller_test.go b/controllers/controllers/services/instances/managed/controller_test.go index a7d4d6be6..dc41c5c5c 100644 --- a/controllers/controllers/services/instances/managed/controller_test.go +++ b/controllers/controllers/services/instances/managed/controller_test.go @@ -126,7 +126,7 @@ var _ = Describe("CFServiceInstance", func() { Name: uuid.NewString(), Namespace: namespace.Name, Finalizers: []string{ - korifiv1alpha1.CFManagedServiceInstanceFinalizerName, + korifiv1alpha1.CFServiceInstanceFinalizerName, }, }, Spec: korifiv1alpha1.CFServiceInstanceSpec{ @@ -797,7 +797,7 @@ var _ = Describe("CFServiceInstance", func() { When("the service instance is purged", func() { BeforeEach(func() { Expect(k8s.PatchResource(ctx, adminClient, instance, func() { - controllerutil.RemoveFinalizer(instance, korifiv1alpha1.CFManagedServiceInstanceFinalizerName) + controllerutil.RemoveFinalizer(instance, korifiv1alpha1.CFServiceInstanceFinalizerName) })).To(Succeed()) }) diff --git a/controllers/controllers/services/instances/upsi/controller.go b/controllers/controllers/services/instances/upsi/controller.go index 774ba1dbd..4f1943029 100644 --- a/controllers/controllers/services/instances/upsi/controller.go +++ b/controllers/controllers/services/instances/upsi/controller.go @@ -37,6 +37,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -110,6 +111,13 @@ func (r *Reconciler) ReconcileResource(ctx context.Context, cfServiceInstance *k cfServiceInstance.Status.ObservedGeneration = cfServiceInstance.Generation log.V(1).Info("set observed generation", "generation", cfServiceInstance.Status.ObservedGeneration) + if !cfServiceInstance.GetDeletionTimestamp().IsZero() { + controllerutil.RemoveFinalizer(cfServiceInstance, korifiv1alpha1.CFServiceInstanceFinalizerName) + log.V(1).Info("finalizer removed") + + return ctrl.Result{}, nil + } + credentialsSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: cfServiceInstance.Namespace, diff --git a/controllers/controllers/services/instances/upsi/controller_test.go b/controllers/controllers/services/instances/upsi/controller_test.go index 36a830107..f9a98f775 100644 --- a/controllers/controllers/services/instances/upsi/controller_test.go +++ b/controllers/controllers/services/instances/upsi/controller_test.go @@ -12,6 +12,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -36,6 +37,9 @@ var _ = Describe("CFServiceInstance", func() { ObjectMeta: metav1.ObjectMeta{ Name: uuid.NewString(), Namespace: testNamespace, + Finalizers: []string{ + korifiv1alpha1.CFServiceInstanceFinalizerName, + }, }, Spec: korifiv1alpha1.CFServiceInstanceSpec{ DisplayName: "service-instance-name", @@ -209,6 +213,19 @@ var _ = Describe("CFServiceInstance", func() { }).Should(Succeed()) }) }) + + When("the instance is deleted", func() { + JustBeforeEach(func() { + Expect(adminClient.Delete(ctx, instance)).To(Succeed()) + }) + + It("is deleted", func() { + Eventually(func(g Gomega) { + err := adminClient.Get(ctx, client.ObjectKeyFromObject(instance), instance) + g.Expect(k8serrors.IsNotFound(err)).To(BeTrue()) + }).Should(Succeed()) + }) + }) }) When("the service instance is managed", func() { diff --git a/controllers/webhooks/finalizer/webhook.go b/controllers/webhooks/finalizer/webhook.go index af486a31b..8ebac5da0 100644 --- a/controllers/webhooks/finalizer/webhook.go +++ b/controllers/webhooks/finalizer/webhook.go @@ -7,8 +7,6 @@ import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/tools/k8s" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) @@ -25,7 +23,7 @@ func NewControllersFinalizerWebhook() *ControllersFinalizerWebhook { "CFPackage": {FinalizerName: korifiv1alpha1.CFPackageFinalizerName, SetPolicy: k8s.Always}, "CFOrg": {FinalizerName: korifiv1alpha1.CFOrgFinalizerName, SetPolicy: k8s.Always}, "CFDomain": {FinalizerName: korifiv1alpha1.CFDomainFinalizerName, SetPolicy: k8s.Always}, - "CFServiceInstance": {FinalizerName: korifiv1alpha1.CFManagedServiceInstanceFinalizerName, SetPolicy: isManagedServiceInstance}, + "CFServiceInstance": {FinalizerName: korifiv1alpha1.CFServiceInstanceFinalizerName, SetPolicy: k8s.Always}, "CFServiceBinding": {FinalizerName: korifiv1alpha1.CFServiceBindingFinalizerName, SetPolicy: k8s.Always}, }), } @@ -41,16 +39,3 @@ func (r *ControllersFinalizerWebhook) SetupWebhookWithManager(mgr ctrl.Manager) func (r *ControllersFinalizerWebhook) Handle(ctx context.Context, req admission.Request) admission.Response { return r.delegate.Handle(ctx, req) } - -func isManagedServiceInstance(object unstructured.Unstructured) bool { - l := ctrl.Log.WithName("isManagedServiceInstance") - cfServiceInstance := &korifiv1alpha1.CFServiceInstance{} - err := runtime.DefaultUnstructuredConverter.FromUnstructured(object.Object, cfServiceInstance) - if err != nil { - l.Error(err, "failed to convert to CFServiceInstnace from unstructured", "unstructured", object.Object) - return true - } - - l.Info("CFServiceInstance converted", "cfserviceinstance", cfServiceInstance) - return cfServiceInstance.Spec.Type == korifiv1alpha1.ManagedType -} diff --git a/controllers/webhooks/finalizer/webhook_test.go b/controllers/webhooks/finalizer/webhook_test.go index 55a7483b6..e743f20f8 100644 --- a/controllers/webhooks/finalizer/webhook_test.go +++ b/controllers/webhooks/finalizer/webhook_test.go @@ -119,7 +119,7 @@ var _ = Describe("Controllers Finalizers Webhook", func() { Type: korifiv1alpha1.ManagedType, }, }, - korifiv1alpha1.CFManagedServiceInstanceFinalizerName, + korifiv1alpha1.CFServiceInstanceFinalizerName, ), Entry("user-provided CF service instance", &korifiv1alpha1.CFServiceInstance{ @@ -131,6 +131,7 @@ var _ = Describe("Controllers Finalizers Webhook", func() { Type: korifiv1alpha1.UserProvidedType, }, }, + korifiv1alpha1.CFServiceInstanceFinalizerName, ), Entry("cfservicebinding", &korifiv1alpha1.CFServiceBinding{ diff --git a/go.mod b/go.mod index fb41beb1e..e33ca47f1 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,14 @@ module code.cloudfoundry.org/korifi go 1.23.0 require ( - code.cloudfoundry.org/bytefmt v0.19.0 + code.cloudfoundry.org/bytefmt v0.20.0 code.cloudfoundry.org/go-loggregator/v8 v8.0.5 github.com/BooleanCat/go-functional/v2 v2.3.0 github.com/Masterminds/semver v1.5.0 github.com/PaesslerAG/jsonpath v0.1.1 github.com/SermoDigital/jose v0.9.2-0.20161205224733-f6df55f235c2 - github.com/aws/aws-sdk-go-v2/config v1.28.5 - github.com/aws/aws-sdk-go-v2/service/ecr v1.36.6 + github.com/aws/aws-sdk-go-v2/config v1.28.6 + github.com/aws/aws-sdk-go-v2/service/ecr v1.36.7 github.com/blendle/zapdriver v1.3.1 github.com/buildpacks/pack v0.36.0 github.com/cloudfoundry/cf-test-helpers v1.0.1-0.20220603211108-d498b915ef74 @@ -31,7 +31,7 @@ require ( github.com/pivotal/kpack v0.15.0 github.com/satori/go.uuid v1.2.0 github.com/servicebinding/runtime v1.0.0 - golang.org/x/text v0.20.0 + golang.org/x/text v0.21.0 gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.31.3 @@ -40,7 +40,7 @@ require ( k8s.io/metrics v0.31.3 k8s.io/pod-security-admission v0.31.3 k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 - sigs.k8s.io/controller-runtime v0.19.2 + sigs.k8s.io/controller-runtime v0.19.3 sigs.k8s.io/controller-tools v0.16.5 sigs.k8s.io/gateway-api v1.2.1 ) @@ -100,17 +100,17 @@ require ( github.com/GehirnInc/crypt v0.0.0-20190301055215-6c0105aabd46 // indirect github.com/Masterminds/semver/v3 v3.3.1 github.com/PaesslerAG/gval v1.0.0 // indirect - github.com/aws/aws-sdk-go-v2 v1.32.5 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.46 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 // indirect + github.com/aws/aws-sdk-go-v2 v1.32.6 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.47 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.23.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.7 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.2 // indirect github.com/aws/smithy-go v1.22.1 // indirect github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20240419161514-af205d85bb44 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -144,7 +144,7 @@ require ( github.com/google/go-containerregistry/pkg/authn/k8schain v0.0.0-20230822174451-190ad0e4d556 github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-20230516205744-dbecb1de8cfa // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/pprof v0.0.0-20241122213907-cbe949e5a41b // indirect + github.com/google/pprof v0.0.0-20241128161848-dc51965c6481 // indirect github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect @@ -179,7 +179,7 @@ require ( golang.org/x/mod v0.22.0 // indirect golang.org/x/net v0.31.0 // indirect golang.org/x/oauth2 v0.24.0 // indirect - golang.org/x/sync v0.9.0 // indirect + golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.27.0 // indirect golang.org/x/term v0.26.0 // indirect golang.org/x/time v0.6.0 // indirect diff --git a/go.sum b/go.sum index 903e572b9..f24c1d426 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go/compute/metadata v0.5.1 h1:NM6oZeZNlYjiwYje+sYFjEpP0Q0zCan1bmQW/KmIrGs= cloud.google.com/go/compute/metadata v0.5.1/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= -code.cloudfoundry.org/bytefmt v0.19.0 h1:kXnXpABxS5LZtW6aFcpEtNr9Zyr6Zg3iDYGmYqZ+wxc= -code.cloudfoundry.org/bytefmt v0.19.0/go.mod h1:HGXyTS2gCcbWp5Yd38YyqUUuUh6HegIuxiFyHysccTw= +code.cloudfoundry.org/bytefmt v0.20.0 h1:8wrzADAdZTffE5CaMCeP41UKbrVgdvsvPIVBjWujGBU= +code.cloudfoundry.org/bytefmt v0.20.0/go.mod h1:QHF0NUOkvyEeSqzPW6Ec6EiI5yVdypyACA18ESJC3+4= code.cloudfoundry.org/go-diodes v0.0.0-20180905200951-72629b5276e3/go.mod h1:Jzi+ccHgo/V/PLQUaQ6hnZcC1c4BS790gx21LRRui4g= code.cloudfoundry.org/go-loggregator/v8 v8.0.5 h1:p1rrGxTwUqLjlUVtbjTAvKOSGNmPuBja8LeQOQgRrBc= code.cloudfoundry.org/go-loggregator/v8 v8.0.5/go.mod h1:mLlJ1ZyG6gVvBEtYypvbztRvFeCtBsTxE9tt+85tS6Y= @@ -68,34 +68,34 @@ github.com/apoydence/eachers v0.0.0-20181020210610-23942921fe77/go.mod h1:bXvGk6 github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/aws/aws-sdk-go-v2 v1.32.5 h1:U8vdWJuY7ruAkzaOdD7guwJjD06YSKmnKCJs7s3IkIo= -github.com/aws/aws-sdk-go-v2 v1.32.5/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= -github.com/aws/aws-sdk-go-v2/config v1.28.5 h1:Za41twdCXbuyyWv9LndXxZZv3QhTG1DinqlFsSuvtI0= -github.com/aws/aws-sdk-go-v2/config v1.28.5/go.mod h1:4VsPbHP8JdcdUDmbTVgNL/8w9SqOkM5jyY8ljIxLO3o= -github.com/aws/aws-sdk-go-v2/credentials v1.17.46 h1:AU7RcriIo2lXjUfHFnFKYsLCwgbz1E7Mm95ieIRDNUg= -github.com/aws/aws-sdk-go-v2/credentials v1.17.46/go.mod h1:1FmYyLGL08KQXQ6mcTlifyFXfJVCNJTVGuQP4m0d/UA= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 h1:sDSXIrlsFSFJtWKLQS4PUWRvrT580rrnuLydJrCQ/yA= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20/go.mod h1:WZ/c+w0ofps+/OUqMwWgnfrgzZH1DZO1RIkktICsqnY= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 h1:4usbeaes3yJnCFC7kfeyhkdkPtoRYPa/hTmCqMpKpLI= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24/go.mod h1:5CI1JemjVwde8m2WG3cz23qHKPOxbpkq0HaoreEgLIY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 h1:N1zsICrQglfzaBnrfM0Ys00860C+QFwu6u/5+LomP+o= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24/go.mod h1:dCn9HbJ8+K31i8IQ8EWmWj0EiIk0+vKiHNMxTTYveAg= +github.com/aws/aws-sdk-go-v2 v1.32.6 h1:7BokKRgRPuGmKkFMhEg/jSul+tB9VvXhcViILtfG8b4= +github.com/aws/aws-sdk-go-v2 v1.32.6/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= +github.com/aws/aws-sdk-go-v2/config v1.28.6 h1:D89IKtGrs/I3QXOLNTH93NJYtDhm8SYa9Q5CsPShmyo= +github.com/aws/aws-sdk-go-v2/config v1.28.6/go.mod h1:GDzxJ5wyyFSCoLkS+UhGB0dArhb9mI+Co4dHtoTxbko= +github.com/aws/aws-sdk-go-v2/credentials v1.17.47 h1:48bA+3/fCdi2yAwVt+3COvmatZ6jUDNkDTIsqDiMUdw= +github.com/aws/aws-sdk-go-v2/credentials v1.17.47/go.mod h1:+KdckOejLW3Ks3b0E3b5rHsr2f9yuORBum0WPnE5o5w= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.21 h1:AmoU1pziydclFT/xRV+xXE/Vb8fttJCLRPv8oAkprc0= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.21/go.mod h1:AjUdLYe4Tgs6kpH4Bv7uMZo7pottoyHMn4eTcIcneaY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25 h1:s/fF4+yDQDoElYhfIVvSNyeCydfbuTKzhxSXDXCPasU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25/go.mod h1:IgPfDv5jqFIzQSNbUEMoitNooSMXjRSDkhXv8jiROvU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25 h1:ZntTCl5EsYnhN/IygQEUugpdwbhdkom9uHcbCftiGgA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25/go.mod h1:DBdPrgeocww+CSl1C8cEV8PN1mHMBhuCDLpXezyvWkE= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= -github.com/aws/aws-sdk-go-v2/service/ecr v1.36.6 h1:zg+3FGHA0PBs0KM25qE/rOf2o5zsjNa1g/Qq83+SDI0= -github.com/aws/aws-sdk-go-v2/service/ecr v1.36.6/go.mod h1:ZSq54Z9SIsOTf1Efwgw1msilSs4XVEfVQiP9nYVnKpM= +github.com/aws/aws-sdk-go-v2/service/ecr v1.36.7 h1:R+5XKIJga2K9Dkj0/iQ6fD/MBGo02oxGGFTc512lK/Q= +github.com/aws/aws-sdk-go-v2/service/ecr v1.36.7/go.mod h1:fDPQV/6ONOQOjvtKhtypIy1wcGLcKYtoK/lvZ9fyDGQ= github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.23.4 h1:aNuiieMaS2IHxqAsTdM/pjHyY1aoaDLBGLqpNnFMMqk= github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.23.4/go.mod h1:8pvvNAklmq+hKmqyvFoMRg0bwg9sdGOvdwximmKiKP0= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 h1:wtpJ4zcwrSbwhECWQoI/g6WM9zqCcSpHDJIWSbMLOu4= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5/go.mod h1:qu/W9HXQbbQ4+1+JcZp0ZNPV31ym537ZJN+fiS7Ti8E= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 h1:3zu537oLmsPfDMyjnUS2g+F2vITgy5pB74tHI+JBNoM= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.6/go.mod h1:WJSZH2ZvepM6t6jwu4w/Z45Eoi75lPN7DcydSRtJg6Y= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 h1:K0OQAsDywb0ltlFrZm0JHPY3yZp/S9OaoLU33S7vPS8= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5/go.mod h1:ORITg+fyuMoeiQFiVGoqB3OydVTLkClw/ljbblMq6Cc= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 h1:6SZUVRQNvExYlMLbHdlKB48x0fLbc2iVROyaNEwBHbU= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.1/go.mod h1:GqWyYCwLXnlUB1lOAXQyNSPqPLQJvmo8J0DWBzp9mtg= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.6 h1:50+XsN70RS7dwJ2CkVNXzj7U2L1HKP8nqTd3XWEXBN4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.6/go.mod h1:WqgLmwY7so32kG01zD8CPTJWVWM+TzJoOVHwTg4aPug= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.7 h1:rLnYAfXQ3YAccocshIH5mzNNwZBkBo+bP6EhIxak6Hw= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.7/go.mod h1:ZHtuQJ6t9A/+YDuxOLnbryAmITtr8UysSny3qcyvJTc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.6 h1:JnhTZR3PiYDNKlXy50/pNeix9aGMo6lLpXwJ1mw8MD4= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.6/go.mod h1:URronUEGfXZN1VpdktPSD1EkAL9mfrV+2F4sjH38qOY= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.2 h1:s4074ZO1Hk8qv65GqNXqDjmkf4HSQqJukaLuuW0TpDA= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.2/go.mod h1:mVggCnIWoM09jP71Wh+ea7+5gAp53q+49wDFs1SW5z8= github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20240419161514-af205d85bb44 h1:oNDkocd5/+6jUuxyz07jQWnKhgpNtKQoZSXKMb7emqQ= @@ -256,8 +256,8 @@ github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-2023051620574 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20241122213907-cbe949e5a41b h1:SXO0REt4iu865upYCk8aKBBJQ4BqoE0ReP23ClMu60s= -github.com/google/pprof v0.0.0-20241122213907-cbe949e5a41b/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20241128161848-dc51965c6481 h1:yudKIrXagAOl99WQzrP1gbz5HLB9UjhcOFnPzdd6Qec= +github.com/google/pprof v0.0.0-20241128161848-dc51965c6481/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= @@ -549,8 +549,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -582,8 +582,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -665,8 +665,8 @@ knative.dev/pkg v0.0.0-20230821102121-81e4ee140363/go.mod h1:dA3TdhFTRm4Kmmpvfkn launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM= reconciler.io/runtime v0.20.0 h1:b2RQTYRrnEDTZQHH6h57SIR373vzKPRyfTKtRyO2cpw= reconciler.io/runtime v0.20.0/go.mod h1:rDD6qZcijjw+7JIkfOOnLM9uMOH+Robq24fbihD5ZRc= -sigs.k8s.io/controller-runtime v0.19.2 h1:3sPrF58XQEPzbE8T81TN6selQIMGbtYwuaJ6eDssDF8= -sigs.k8s.io/controller-runtime v0.19.2/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= +sigs.k8s.io/controller-runtime v0.19.3 h1:XO2GvC9OPftRst6xWCpTgBZO04S2cbp0Qqkj8bX1sPw= +sigs.k8s.io/controller-runtime v0.19.3/go.mod h1:j4j87DqtsThvwTv5/Tc5NFRyyF/RF0ip4+62tbTSIUM= sigs.k8s.io/controller-tools v0.16.5 h1:5k9FNRqziBPwqr17AMEPPV/En39ZBplLAdOwwQHruP4= sigs.k8s.io/controller-tools v0.16.5/go.mod h1:8vztuRVzs8IuuJqKqbXCSlXcw+lkAv/M2sTpg55qjMY= sigs.k8s.io/gateway-api v1.2.1 h1:fZZ/+RyRb+Y5tGkwxFKuYuSRQHu9dZtbjenblleOLHM= diff --git a/helm/korifi/controllers/cf_roles/cf_admin.yaml b/helm/korifi/controllers/cf_roles/cf_admin.yaml index 6eaf7c7e7..18b16e020 100644 --- a/helm/korifi/controllers/cf_roles/cf_admin.yaml +++ b/helm/korifi/controllers/cf_roles/cf_admin.yaml @@ -207,6 +207,7 @@ rules: - list - get - patch + - delete - apiGroups: - rbac.authorization.k8s.io diff --git a/helm/korifi/controllers/cf_roles/cf_root_namespace_user.yaml b/helm/korifi/controllers/cf_roles/cf_root_namespace_user.yaml index 5f0e97639..dbdae539f 100644 --- a/helm/korifi/controllers/cf_roles/cf_root_namespace_user.yaml +++ b/helm/korifi/controllers/cf_roles/cf_root_namespace_user.yaml @@ -61,3 +61,4 @@ rules: verbs: - get - list + - delete diff --git a/helm/korifi/controllers/role.yaml b/helm/korifi/controllers/role.yaml index 2f53e64e2..45d93023b 100644 --- a/helm/korifi/controllers/role.yaml +++ b/helm/korifi/controllers/role.yaml @@ -110,6 +110,8 @@ rules: - cfservicebindings - cfservicebrokers - cfserviceinstances + - cfserviceofferings + - cfserviceplans - cfspaces - cftasks verbs: @@ -159,6 +161,7 @@ rules: - cfservicebindings/status - cfservicebrokers/status - cfserviceinstances/status + - cfserviceinstances/status - cfspaces/status - cftasks/status verbs: @@ -196,18 +199,6 @@ rules: - list - patch - watch -- apiGroups: - - korifi.cloudfoundry.org - resources: - - cfserviceofferings - - cfserviceplans - verbs: - - create - - get - - list - - patch - - update - - watch - apiGroups: - kpack.io resources: diff --git a/tests/e2e/service_brokers_test.go b/tests/e2e/service_brokers_test.go index 2db084a1c..e1a1a045e 100644 --- a/tests/e2e/service_brokers_test.go +++ b/tests/e2e/service_brokers_test.go @@ -83,6 +83,31 @@ var _ = Describe("Service Brokers", func() { }) }) + Describe("Get", func() { + var ( + result responseResource + brokerGUID string + ) + + BeforeEach(func() { + brokerGUID = createBroker(serviceBrokerURL) + }) + + AfterEach(func() { + broker.NewCatalogDeleter(rootNamespace).ForBrokerGUID(brokerGUID).Delete() + }) + + JustBeforeEach(func() { + resp, err = adminClient.R().SetResult(&result).Get("/v3/service_brokers/" + brokerGUID) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns the service broker", func() { + Expect(resp).To(HaveRestyStatusCode(http.StatusOK)) + Expect(result.GUID).To(Equal(brokerGUID)) + }) + }) + Describe("Update", func() { var brokerGUID string diff --git a/tests/e2e/service_plans_test.go b/tests/e2e/service_plans_test.go index 6541426b0..45e282aca 100644 --- a/tests/e2e/service_plans_test.go +++ b/tests/e2e/service_plans_test.go @@ -18,6 +18,7 @@ var _ = Describe("Service Plans", func() { var ( brokerGUID string resp *resty.Response + err error ) BeforeEach(func() { @@ -32,7 +33,6 @@ var _ = Describe("Service Plans", func() { var result resourceList[resource] JustBeforeEach(func() { - var err error resp, err = adminClient.R().SetResult(&result).Get("/v3/service_plans") Expect(err).NotTo(HaveOccurred()) }) @@ -55,9 +55,9 @@ var _ = Describe("Service Plans", func() { BeforeEach(func() { plans := resourceList[resource]{} - listResp, err := adminClient.R().SetResult(&plans).Get("/v3/service_plans") + resp, err = adminClient.R().SetResult(&plans).Get("/v3/service_plans") Expect(err).NotTo(HaveOccurred()) - Expect(listResp).To(HaveRestyStatusCode(http.StatusOK)) + Expect(resp).To(HaveRestyStatusCode(http.StatusOK)) brokerPlans := itx.FromSlice(plans.Resources).Filter(func(r resource) bool { return r.Metadata.Labels[korifiv1alpha1.RelServiceBrokerGUIDLabel] == brokerGUID @@ -69,7 +69,6 @@ var _ = Describe("Service Plans", func() { Describe("Get Visibility", func() { JustBeforeEach(func() { - var err error resp, err = adminClient.R().SetResult(&result).Get(fmt.Sprintf("/v3/service_plans/%s/visibility", planGUID)) Expect(err).NotTo(HaveOccurred()) }) @@ -84,7 +83,6 @@ var _ = Describe("Service Plans", func() { Describe("Apply Visibility", func() { JustBeforeEach(func() { - var err error resp, err = adminClient.R(). SetResult(&result). SetBody(planVisibilityResource{ @@ -106,7 +104,6 @@ var _ = Describe("Service Plans", func() { Describe("Update Visibility", func() { JustBeforeEach(func() { - var err error resp, err = adminClient.R(). SetResult(&result). SetBody(planVisibilityResource{ @@ -156,4 +153,26 @@ var _ = Describe("Service Plans", func() { }) }) }) + + Describe("Delete", func() { + var servicePlanGUID string + + BeforeEach(func() { + plans := resourceList[resource]{} + resp, err = adminClient.R().SetResult(&plans).Get("/v3/service_plans?service_broker_guids=" + brokerGUID) + Expect(err).ToNot(HaveOccurred()) + + servicePlanGUID = plans.Resources[0].GUID + resp, err = adminClient.R().Delete("/v3/service_plans/" + servicePlanGUID) + }) + + It("deletes the service plan", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(resp).To(HaveRestyStatusCode(http.StatusNoContent)) + + resp, err = adminClient.R().Get("/v3/service_plans/" + servicePlanGUID) + Expect(err).ToNot(HaveOccurred()) + Expect(resp).To(HaveRestyStatusCode(http.StatusNotFound)) + }) + }) })