Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include service broker fields when listing plans #3421

Merged
merged 1 commit into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 83 additions & 16 deletions api/handlers/service_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package handlers

import (
"context"
"fmt"
"net/http"
"net/url"
"slices"
Expand Down Expand Up @@ -37,19 +38,22 @@ type ServicePlan struct {
requestValidator RequestValidator
servicePlanRepo CFServicePlanRepository
serviceOfferingRepo CFServiceOfferingRepository
serviceBrokerRepo CFServiceBrokerRepository
}

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

Expand All @@ -62,47 +66,110 @@ func (h *ServicePlan) list(r *http.Request) (*routing.Response, error) {
return nil, apierrors.LogAndReturn(logger, err, "failed to decode json payload")
}

servicePlanList, err := h.servicePlanRepo.ListPlans(r.Context(), authInfo, payload.ToMessage())
servicePlans, err := h.servicePlanRepo.ListPlans(r.Context(), authInfo, payload.ToMessage())
if err != nil {
return nil, apierrors.LogAndReturn(logger, err, "failed to list service plans")
}

includedResources, err := h.getIncludedResources(r.Context(), authInfo, payload, servicePlans)
if err != nil {
return nil, apierrors.LogAndReturn(logger, err, "failed to build included resources")
}

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

func (h *ServicePlan) getIncludedResources(
ctx context.Context,
authInfo authorization.Info,
payload payloads.ServicePlanList,
servicePlans []repositories.ServicePlanRecord,
) ([]model.IncludedResource, error) {
if len(payload.IncludeResources) == 0 && len(payload.IncludeBrokerFields) == 0 {
return nil, nil
}

includedResources := []model.IncludedResource{}

offerings, err := h.listOfferings(ctx, authInfo, servicePlans)
if err != nil {
return nil, fmt.Errorf("failed to list offerings for plans: %w", err)
}

if slices.Contains(payload.IncludeResources, "service_offering") {
includedOfferings, err := h.getIncludedServiceOfferings(r.Context(), authInfo, servicePlanList, h.serverURL)
includedResources = append(includedResources, iter.Map(iter.Lift(offerings), func(o repositories.ServiceOfferingRecord) model.IncludedResource {
return model.IncludedResource{
Type: "service_offerings",
Resource: presenter.ForServiceOffering(o, h.serverURL),
}
}).Collect()...)
}

if len(payload.IncludeBrokerFields) != 0 {
brokers, err := h.listBrokers(ctx, authInfo, offerings)
if err != nil {
return nil, fmt.Errorf("failed to list brokers for offerings of plans: %w", err)
}

includedBrokerFields, err := h.getIncludedBrokerFields(brokers, payload.IncludeBrokerFields)
if err != nil {
return nil, apierrors.LogAndReturn(logger, err, "failed to get included service offerings")
return nil, fmt.Errorf("failed to get included broker fields: %w", err)
}
includedResources = append(includedResources, includedOfferings...)
includedResources = append(includedResources, includedBrokerFields...)
}

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

func (h *ServicePlan) getIncludedServiceOfferings(
func (h *ServicePlan) listOfferings(
ctx context.Context,
authInfo authorization.Info,
servicePlans []repositories.ServicePlanRecord,
baseUrl url.URL,
) ([]model.IncludedResource, error) {
) ([]repositories.ServiceOfferingRecord, 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{
return h.serviceOfferingRepo.ListOfferings(ctx, authInfo, repositories.ListServiceOfferingMessage{
GUIDs: tools.Uniq(offeringGUIDs),
})
if err != nil {
return nil, err
}
}

func (h *ServicePlan) listBrokers(
ctx context.Context,
authInfo authorization.Info,
offerings []repositories.ServiceOfferingRecord,
) ([]repositories.ServiceBrokerRecord, error) {
brokerGUIDs := iter.Map(iter.Lift(offerings), func(o repositories.ServiceOfferingRecord) string {
return o.ServiceBrokerGUID
}).Collect()

return h.serviceBrokerRepo.ListServiceBrokers(ctx, authInfo, repositories.ListServiceBrokerMessage{
GUIDs: brokerGUIDs,
})
}

return iter.Map(iter.Lift(offerings), func(o repositories.ServiceOfferingRecord) model.IncludedResource {
func (h *ServicePlan) getIncludedBrokerFields(
brokers []repositories.ServiceBrokerRecord,
brokerFields []string,
) ([]model.IncludedResource, error) {
brokerIncludes := iter.Map(iter.Lift(brokers), func(b repositories.ServiceBrokerRecord) model.IncludedResource {
return model.IncludedResource{
Type: "service_offerings",
Resource: presenter.ForServiceOffering(o, baseUrl),
Type: "service_brokers",
Resource: presenter.ForServiceBroker(b, h.serverURL),
}
}).Collect(), nil
}).Collect()

brokerIncludesFields := []model.IncludedResource{}
for _, brokerInclude := range brokerIncludes {
fields, err := brokerInclude.SelectJSONFields(brokerFields...)
if err != nil {
return nil, err
}
brokerIncludesFields = append(brokerIncludesFields, fields)
}

return brokerIncludesFields, nil
}

func (h *ServicePlan) getPlanVisibility(r *http.Request) (*routing.Response, error) {
Expand Down
58 changes: 57 additions & 1 deletion api/handlers/service_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,22 @@ var _ = Describe("ServicePlan", func() {
var (
servicePlanRepo *fake.CFServicePlanRepository
serviceOfferingRepo *fake.CFServiceOfferingRepository
serviceBrokerRepo *fake.CFServiceBrokerRepository
requestValidator *fake.RequestValidator
)

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

apiHandler := NewServicePlan(
*serverURL,
requestValidator,
servicePlanRepo,
serviceOfferingRepo,
serviceBrokerRepo,
)
routerBuilder.LoadRoutes(apiHandler)
})
Expand Down Expand Up @@ -121,7 +124,7 @@ var _ = Describe("ServicePlan", func() {
})
})

It("includes broker fields in the response", func() {
It("includes service offering in the response", func() {
Expect(rr).Should(HaveHTTPStatus(http.StatusOK))
Expect(rr).To(HaveHTTPBody(SatisfyAll(
MatchJSONPath("$.included.service_offerings[0].guid", "service-offering-guid"),
Expand All @@ -130,6 +133,59 @@ var _ = Describe("ServicePlan", func() {
})
})

Describe("fields service_offering.service_broker", func() {
BeforeEach(func() {
serviceBrokerRepo.ListServiceBrokersReturns([]repositories.ServiceBrokerRecord{{
ServiceBroker: services.ServiceBroker{
Name: "service-broker-name",
},
CFResource: model.CFResource{
GUID: "service-broker-guid",
},
}}, nil)

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

requestValidator.DecodeAndValidateURLValuesStub = decodeAndValidateURLValuesStub(&payloads.ServicePlanList{
IncludeBrokerFields: []string{"guid", "name"},
})
})

It("lists the brokers", func() {
Expect(serviceBrokerRepo.ListServiceBrokersCallCount()).To(Equal(1))
_, _, actualListMessage := serviceBrokerRepo.ListServiceBrokersArgsForCall(0)
Expect(actualListMessage).To(Equal(repositories.ListServiceBrokerMessage{
GUIDs: []string{"service-broker-guid"},
}))
})

When("listing brokers fails", func() {
BeforeEach(func() {
serviceBrokerRepo.ListServiceBrokersReturns([]repositories.ServiceBrokerRecord{}, errors.New("list-broker-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_brokers[0].guid", "service-broker-guid"),
MatchJSONPath("$.included.service_brokers[0].name", "service-broker-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 @@ -432,6 +432,7 @@ func main() {
requestValidator,
servicePlanRepo,
serviceOfferingRepo,
serviceBrokerRepo,
),
}
for _, handler := range apiHandlers {
Expand Down
7 changes: 5 additions & 2 deletions api/payloads/service_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ type ServicePlanList struct {
Names string
Available *bool
IncludeResources []string
IncludeBrokerFields []string
}

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

Expand All @@ -38,11 +40,11 @@ func (l *ServicePlanList) ToMessage() repositories.ListServicePlanMessage {
}

func (l *ServicePlanList) SupportedKeys() []string {
return []string{"service_offering_guids", "names", "available", "page", "per_page", "include"}
return []string{"service_offering_guids", "names", "available", "fields[service_offering.service_broker]", "page", "per_page", "include"}
}

func (l *ServicePlanList) IgnoredKeys() []*regexp.Regexp {
return []*regexp.Regexp{regexp.MustCompile(`fields\[.+\]`)}
return nil
}

func (l *ServicePlanList) DecodeFromURLValues(values url.Values) error {
Expand All @@ -55,6 +57,7 @@ func (l *ServicePlanList) DecodeFromURLValues(values url.Values) error {
}
l.Available = available
l.IncludeResources = parse.ArrayParam(values.Get("include"))
l.IncludeBrokerFields = parse.ArrayParam(values.Get("fields[service_offering.service_broker]"))

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 @@ -25,6 +25,7 @@ var _ = Describe("ServicePlan", func() {
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"}}),
Entry("service broker fields", "fields[service_offering.service_broker]=guid,name", payloads.ServicePlanList{IncludeBrokerFields: []string{"guid", "name"}}),
)

DescribeTable("invalid query",
Expand All @@ -34,6 +35,7 @@ var _ = Describe("ServicePlan", func() {
},
Entry("invalid available", "available=invalid", MatchError(ContainSubstring("failed to parse"))),
Entry("invalid include", "include=foo", MatchError(ContainSubstring("value must be one of: service_offering"))),
Entry("invalid service broker fields", "fields[service_offering.service_broker]=foo", MatchError(ContainSubstring("value must be one of"))),
)

Describe("ToMessage", func() {
Expand Down
Loading