Skip to content

Commit

Permalink
Include service offerings when listing plans
Browse files Browse the repository at this point in the history
fixes #3279

Co-authored-by: Georgi Sabev <georgethebeatle@gmail.com>
  • Loading branch information
danail-branekov and georgethebeatle committed Aug 9, 2024
1 parent 3853006 commit 2a5725f
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 67 deletions.
56 changes: 49 additions & 7 deletions api/handlers/service_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import (
"context"
"net/http"
"net/url"
"slices"

"code.cloudfoundry.org/korifi/api/authorization"
apierrors "code.cloudfoundry.org/korifi/api/errors"
"code.cloudfoundry.org/korifi/api/payloads"
"code.cloudfoundry.org/korifi/api/presenter"
"code.cloudfoundry.org/korifi/api/repositories"
"code.cloudfoundry.org/korifi/api/routing"
"code.cloudfoundry.org/korifi/model"
"code.cloudfoundry.org/korifi/tools"
"github.com/BooleanCat/go-functional/iter"
"github.com/go-logr/logr"
)

Expand All @@ -29,20 +33,23 @@ type CFServicePlanRepository interface {
}

type ServicePlan struct {
serverURL url.URL
requestValidator RequestValidator
servicePlanRepo CFServicePlanRepository
serverURL url.URL
requestValidator RequestValidator
servicePlanRepo CFServicePlanRepository
serviceOfferingRepo CFServiceOfferingRepository
}

func NewServicePlan(
serverURL url.URL,
requestValidator RequestValidator,
servicePlanRepo CFServicePlanRepository,
serviceOfferingRepo CFServiceOfferingRepository,
) *ServicePlan {
return &ServicePlan{
serverURL: serverURL,
requestValidator: requestValidator,
servicePlanRepo: servicePlanRepo,
serverURL: serverURL,
requestValidator: requestValidator,
servicePlanRepo: servicePlanRepo,
serviceOfferingRepo: serviceOfferingRepo,
}
}

Expand All @@ -60,7 +67,42 @@ func (h *ServicePlan) list(r *http.Request) (*routing.Response, error) {
return nil, apierrors.LogAndReturn(logger, err, "failed to list service plans")
}

return routing.NewResponse(http.StatusOK).WithBody(presenter.ForList(presenter.ForServicePlan, servicePlanList, h.serverURL, *r.URL)), nil
includedResources := []model.IncludedResource{}

if slices.Contains(payload.IncludeResources, "service_offering") {
includedOfferings, err := h.getIncludedServiceOfferings(r.Context(), authInfo, servicePlanList, h.serverURL)
if err != nil {
return nil, apierrors.LogAndReturn(logger, err, "failed to get included service offerings")
}
includedResources = append(includedResources, includedOfferings...)
}

return routing.NewResponse(http.StatusOK).WithBody(presenter.ForList(presenter.ForServicePlan, servicePlanList, h.serverURL, *r.URL, includedResources...)), nil
}

func (h *ServicePlan) getIncludedServiceOfferings(
ctx context.Context,
authInfo authorization.Info,
servicePlans []repositories.ServicePlanRecord,
baseUrl url.URL,
) ([]model.IncludedResource, error) {
offeringGUIDs := iter.Map(iter.Lift(servicePlans), func(o repositories.ServicePlanRecord) string {
return o.ServiceOfferingGUID
}).Collect()

offerings, err := h.serviceOfferingRepo.ListOfferings(ctx, authInfo, repositories.ListServiceOfferingMessage{
GUIDs: tools.Uniq(offeringGUIDs),
})
if err != nil {
return nil, err
}

return iter.Map(iter.Lift(offerings), func(o repositories.ServiceOfferingRecord) model.IncludedResource {
return model.IncludedResource{
Type: "service_offerings",
Resource: presenter.ForServiceOffering(o, baseUrl),
}
}).Collect(), nil
}

func (h *ServicePlan) getPlanVisibility(r *http.Request) (*routing.Response, error) {
Expand Down
52 changes: 50 additions & 2 deletions api/handlers/service_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"code.cloudfoundry.org/korifi/api/payloads"
"code.cloudfoundry.org/korifi/api/repositories"
"code.cloudfoundry.org/korifi/model"
"code.cloudfoundry.org/korifi/model/services"
. "code.cloudfoundry.org/korifi/tests/matchers"

korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1"
Expand All @@ -19,18 +20,21 @@ import (

var _ = Describe("ServicePlan", func() {
var (
servicePlanRepo *fake.CFServicePlanRepository
requestValidator *fake.RequestValidator
servicePlanRepo *fake.CFServicePlanRepository
serviceOfferingRepo *fake.CFServiceOfferingRepository
requestValidator *fake.RequestValidator
)

BeforeEach(func() {
requestValidator = new(fake.RequestValidator)
servicePlanRepo = new(fake.CFServicePlanRepository)
serviceOfferingRepo = new(fake.CFServiceOfferingRepository)

apiHandler := NewServicePlan(
*serverURL,
requestValidator,
servicePlanRepo,
serviceOfferingRepo,
)
routerBuilder.LoadRoutes(apiHandler)
})
Expand All @@ -43,6 +47,15 @@ var _ = Describe("ServicePlan", func() {
},
ServiceOfferingGUID: "service-offering-guid",
}}, nil)

serviceOfferingRepo.ListOfferingsReturns([]repositories.ServiceOfferingRecord{{
ServiceOffering: services.ServiceOffering{
Name: "service-offering-name",
},
CFResource: model.CFResource{
GUID: "service-offering-guid",
},
}}, nil)
})

JustBeforeEach(func() {
Expand All @@ -65,6 +78,7 @@ var _ = Describe("ServicePlan", func() {
MatchJSONPath("$.resources[0].guid", "plan-guid"),
MatchJSONPath("$.resources[0].links.self.href", "https://api.example.org/v3/service_plans/plan-guid"),
MatchJSONPath("$.resources[0].links.service_offering.href", "https://api.example.org/v3/service_offerings/service-offering-guid"),
Not(ContainSubstring("included")),
)))
})

Expand All @@ -82,6 +96,40 @@ var _ = Describe("ServicePlan", func() {
})
})

Describe("include service_offering", func() {
BeforeEach(func() {
requestValidator.DecodeAndValidateURLValuesStub = decodeAndValidateURLValuesStub(&payloads.ServicePlanList{
IncludeResources: []string{"service_offering"},
})
})

It("lists the offerings", func() {
Expect(serviceOfferingRepo.ListOfferingsCallCount()).To(Equal(1))
_, _, actualListMessage := serviceOfferingRepo.ListOfferingsArgsForCall(0)
Expect(actualListMessage).To(Equal(repositories.ListServiceOfferingMessage{
GUIDs: []string{"service-offering-guid"},
}))
})

When("listing offerings fails", func() {
BeforeEach(func() {
serviceOfferingRepo.ListOfferingsReturns([]repositories.ServiceOfferingRecord{}, errors.New("list-offering-err"))
})

It("returns an error", func() {
expectUnknownError()
})
})

It("includes broker fields in the response", func() {
Expect(rr).Should(HaveHTTPStatus(http.StatusOK))
Expect(rr).To(HaveHTTPBody(SatisfyAll(
MatchJSONPath("$.included.service_offerings[0].guid", "service-offering-guid"),
MatchJSONPath("$.included.service_offerings[0].name", "service-offering-name"),
)))
})
})

When("the request is invalid", func() {
BeforeEach(func() {
requestValidator.DecodeAndValidateURLValuesReturns(errors.New("invalid-request"))
Expand Down
1 change: 1 addition & 0 deletions api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ func main() {
*serverURL,
requestValidator,
servicePlanRepo,
serviceOfferingRepo,
),
}
for _, handler := range apiHandlers {
Expand Down
8 changes: 8 additions & 0 deletions api/payloads/service_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ type ServicePlanList struct {
ServiceOfferingGUIDs string
Names string
Available *bool
IncludeResources []string
}

func (l ServicePlanList) Validate() error {
return jellidation.ValidateStruct(&l,
jellidation.Field(&l.IncludeResources, jellidation.Each(validation.OneOf("service_offering"))),
)
}

func (l *ServicePlanList) ToMessage() repositories.ListServicePlanMessage {
Expand Down Expand Up @@ -47,6 +54,7 @@ func (l *ServicePlanList) DecodeFromURLValues(values url.Values) error {
return fmt.Errorf("failed to parse 'available' query parameter: %w", err)
}
l.Available = available
l.IncludeResources = parse.ArrayParam(values.Get("include"))

return nil
}
Expand Down
2 changes: 2 additions & 0 deletions api/payloads/service_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ var _ = Describe("ServicePlan", func() {
Entry("names", "names=b1,b2", payloads.ServicePlanList{Names: "b1,b2"}),
Entry("available", "available=true", payloads.ServicePlanList{Available: tools.PtrTo(true)}),
Entry("not available", "available=false", payloads.ServicePlanList{Available: tools.PtrTo(false)}),
Entry("include", "include=service_offering", payloads.ServicePlanList{IncludeResources: []string{"service_offering"}}),
)

DescribeTable("invalid query",
Expand All @@ -32,6 +33,7 @@ var _ = Describe("ServicePlan", func() {
Expect(decodeErr).To(errMatcher)
},
Entry("invalid available", "available=invalid", MatchError(ContainSubstring("failed to parse"))),
Entry("invalid include", "include=foo", MatchError(ContainSubstring("value must be one of: service_offering"))),
)

Describe("ToMessage", func() {
Expand Down
4 changes: 3 additions & 1 deletion api/repositories/service_offering_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@ type ServiceOfferingRepo struct {

type ListServiceOfferingMessage struct {
Names []string
GUIDs []string
BrokerNames []string
}

func (m *ListServiceOfferingMessage) matchesName(cfServiceOffering korifiv1alpha1.CFServiceOffering) bool {
return tools.EmptyOrContains(m.Names, cfServiceOffering.Spec.Name)
return tools.EmptyOrContains(m.Names, cfServiceOffering.Spec.Name) &&
tools.EmptyOrContains(m.GUIDs, cfServiceOffering.Name)
}

func NewServiceOfferingRepo(
Expand Down
Loading

0 comments on commit 2a5725f

Please sign in to comment.