Skip to content

Commit

Permalink
Add DELETE /v3/service_offerings/:guid
Browse files Browse the repository at this point in the history
  * Implement delete handler and DeleteOffering in repo
  * Implement purging for Service Offerings - is purge is set to true,
    delete all related service plans, instances and bindings without
    contacting the service broker
  • Loading branch information
klapkov committed Nov 18, 2024
1 parent 3dbb283 commit 1ea629b
Show file tree
Hide file tree
Showing 9 changed files with 504 additions and 8 deletions.
78 changes: 78 additions & 0 deletions api/handlers/fake/cfservice_offering_repository.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions api/handlers/service_offering.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand All @@ -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},
}
}
58 changes: 58 additions & 0 deletions api/handlers/service_offering_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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("getting 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))
})
})
})
})
2 changes: 1 addition & 1 deletion api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
24 changes: 24 additions & 0 deletions api/payloads/service_offering.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
21 changes: 21 additions & 0 deletions api/payloads/service_offering_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
)
})
Loading

0 comments on commit 1ea629b

Please sign in to comment.