Skip to content

Commit

Permalink
Implement POST /v3/service_plans/{guid}/visibility
Browse files Browse the repository at this point in the history
- For now `public` is the only allowd value other than `admin`, which is
  the default
- Bonus: Introduce the `visibility_type` field in the service plan
  resource. This makes `cf service-access` display the plan visibility
  correctly

fixes #3275

Co-authored-by: Georgi Sabev <[email protected]>
  • Loading branch information
danail-branekov and georgethebeatle committed Aug 2, 2024
1 parent b6bd5dc commit 5eccff9
Show file tree
Hide file tree
Showing 18 changed files with 464 additions and 70 deletions.
83 changes: 83 additions & 0 deletions api/handlers/fake/cfservice_plan_repository.go

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

22 changes: 22 additions & 0 deletions api/handlers/service_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const (
type CFServicePlanRepository interface {
ListPlans(context.Context, authorization.Info, repositories.ListServicePlanMessage) ([]repositories.ServicePlanRecord, error)
GetPlanVisibility(context.Context, authorization.Info, string) (repositories.ServicePlanVisibilityRecord, error)
ApplyPlanVisibility(context.Context, authorization.Info, repositories.ApplyServicePlanVisibilityMessage) (repositories.ServicePlanVisibilityRecord, error)
}

type ServicePlan struct {
Expand Down Expand Up @@ -76,6 +77,26 @@ func (h *ServicePlan) getPlanVisibility(r *http.Request) (*routing.Response, err
return routing.NewResponse(http.StatusOK).WithBody(presenter.ForServicePlanVisibility(visibility, h.serverURL)), nil
}

func (h *ServicePlan) applyPlanVisibility(r *http.Request) (*routing.Response, error) {
authInfo, _ := authorization.InfoFromContext(r.Context())
logger := logr.FromContextOrDiscard(r.Context()).WithName("handlers.service-plan.get-visibility")

planGUID := routing.URLParam(r, "guid")
logger = logger.WithValues("guid", planGUID)

payload := payloads.ServicePlanVisibility{}
if err := h.requestValidator.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return nil, apierrors.LogAndReturn(logger, err, "failed to decode json payload")
}

visibility, err := h.servicePlanRepo.ApplyPlanVisibility(r.Context(), authInfo, payload.ToMessage(planGUID))
if err != nil {
return nil, apierrors.LogAndReturn(logger, err, "failed to apply plan visibility")
}

return routing.NewResponse(http.StatusOK).WithBody(presenter.ForServicePlanVisibility(visibility, h.serverURL)), nil
}

func (h *ServicePlan) UnauthenticatedRoutes() []routing.Route {
return nil
}
Expand All @@ -84,5 +105,6 @@ func (h *ServicePlan) AuthenticatedRoutes() []routing.Route {
return []routing.Route{
{Method: "GET", Pattern: ServicePlansPath, Handler: h.list},
{Method: "GET", Pattern: ServicePlanVisivilityPath, Handler: h.getPlanVisibility},
{Method: "POST", Pattern: ServicePlanVisivilityPath, Handler: h.applyPlanVisibility},
}
}
63 changes: 63 additions & 0 deletions api/handlers/service_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package handlers_test
import (
"errors"
"net/http"
"strings"

. "code.cloudfoundry.org/korifi/api/handlers"
"code.cloudfoundry.org/korifi/api/handlers/fake"
Expand Down Expand Up @@ -146,4 +147,66 @@ var _ = Describe("ServicePlan", func() {
})
})
})

Describe("POST /v3/service_plans/{guid}/visibility", func() {
BeforeEach(func() {
requestValidator.DecodeAndValidateJSONPayloadStub = decodeAndValidatePayloadStub(&payloads.ServicePlanVisibility{
Type: korifiv1alpha1.PublicServicePlanVisibilityType,
})

servicePlanRepo.ApplyPlanVisibilityReturns(repositories.ServicePlanVisibilityRecord{
Type: korifiv1alpha1.PublicServicePlanVisibilityType,
}, nil)
})

JustBeforeEach(func() {
req, err := http.NewRequestWithContext(ctx, "POST", "/v3/service_plans/my-service-plan/visibility", strings.NewReader("the-payload"))
Expect(err).NotTo(HaveOccurred())

routerBuilder.Build().ServeHTTP(rr, req)
})

It("validates the payload", func() {
Expect(requestValidator.DecodeAndValidateJSONPayloadCallCount()).To(Equal(1))
actualReq, _ := requestValidator.DecodeAndValidateJSONPayloadArgsForCall(0)
Expect(bodyString(actualReq)).To(Equal("the-payload"))
})

It("updates the plan visibility", func() {
Expect(servicePlanRepo.ApplyPlanVisibilityCallCount()).To(Equal(1))
_, actualAuthInfo, actualMessage := servicePlanRepo.ApplyPlanVisibilityArgsForCall(0)
Expect(actualAuthInfo).To(Equal(authInfo))
Expect(actualMessage).To(Equal(repositories.ApplyServicePlanVisibilityMessage{
PlanGUID: "my-service-plan",
Type: korifiv1alpha1.PublicServicePlanVisibilityType,
}))

Expect(rr).To(HaveHTTPStatus(http.StatusOK))
Expect(rr).To(HaveHTTPHeaderWithValue("Content-Type", "application/json"))

Expect(rr).To(HaveHTTPBody(SatisfyAll(
MatchJSONPath("$.type", korifiv1alpha1.PublicServicePlanVisibilityType),
)))
})

When("the payload is invalid", func() {
BeforeEach(func() {
requestValidator.DecodeAndValidateJSONPayloadReturns(errors.New("invalid-payload"))
})

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

When("updating the visibility fails", func() {
BeforeEach(func() {
servicePlanRepo.ApplyPlanVisibilityReturns(repositories.ServicePlanVisibilityRecord{}, errors.New("visibility-err"))
})

It("returns an error", func() {
expectUnknownError()
})
})
})
})
11 changes: 11 additions & 0 deletions api/payloads/service_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,14 @@ func (l *ServicePlanList) DecodeFromURLValues(values url.Values) error {
l.ServiceOfferingGUIDs = values.Get("service_offering_guids")
return nil
}

type ServicePlanVisibility struct {
Type string `json:"type"`
}

func (p *ServicePlanVisibility) ToMessage(planGUID string) repositories.ApplyServicePlanVisibilityMessage {
return repositories.ApplyServicePlanVisibilityMessage{
PlanGUID: planGUID,
Type: p.Type,
}
}
46 changes: 32 additions & 14 deletions api/payloads/service_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,46 @@ package payloads_test
import (
"code.cloudfoundry.org/korifi/api/payloads"
"code.cloudfoundry.org/korifi/api/repositories"
korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("ServicePlan", func() {
DescribeTable("valid query",
func(query string, expectedServicePlanList payloads.ServicePlanList) {
actualServicePlanList, decodeErr := decodeQuery[payloads.ServicePlanList](query)
Describe("List", func() {
DescribeTable("valid query",
func(query string, expectedServicePlanList payloads.ServicePlanList) {
actualServicePlanList, decodeErr := decodeQuery[payloads.ServicePlanList](query)

Expect(decodeErr).NotTo(HaveOccurred())
Expect(*actualServicePlanList).To(Equal(expectedServicePlanList))
},
Entry("service_offering_guids", "service_offering_guids=b1,b2", payloads.ServicePlanList{ServiceOfferingGUIDs: "b1,b2"}),
)
Expect(decodeErr).NotTo(HaveOccurred())
Expect(*actualServicePlanList).To(Equal(expectedServicePlanList))
},
Entry("service_offering_guids", "service_offering_guids=b1,b2", payloads.ServicePlanList{ServiceOfferingGUIDs: "b1,b2"}),
)

Describe("ToMessage", func() {
It("converts payload to repository message", func() {
payload := &payloads.ServicePlanList{ServiceOfferingGUIDs: "b1,b2"}
Describe("ToMessage", func() {
It("converts payload to repository message", func() {
payload := &payloads.ServicePlanList{ServiceOfferingGUIDs: "b1,b2"}

Expect(payload.ToMessage()).To(Equal(repositories.ListServicePlanMessage{
ServiceOfferingGUIDs: []string{"b1", "b2"},
}))
Expect(payload.ToMessage()).To(Equal(repositories.ListServicePlanMessage{
ServiceOfferingGUIDs: []string{"b1", "b2"},
}))
})
})
})

Describe("Visibility", func() {
Describe("ToMessage", func() {
It("converts payload to repository message", func() {
payload := &payloads.ServicePlanVisibility{
Type: korifiv1alpha1.PublicServicePlanVisibilityType,
}

Expect(payload.ToMessage("plan-guid")).To(Equal(repositories.ApplyServicePlanVisibilityMessage{
PlanGUID: "plan-guid",
Type: korifiv1alpha1.PublicServicePlanVisibilityType,
}))
})
})
})
})
4 changes: 4 additions & 0 deletions api/presenter/service_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
type ServicePlanLinks struct {
Self Link `json:"self"`
ServiceOffering Link `json:"service_offering"`
Visibility Link `json:"visibility"`
}

type ServicePlanResponse struct {
Expand All @@ -27,6 +28,9 @@ func ForServicePlan(servicePlan repositories.ServicePlanRecord, baseURL url.URL)
ServiceOffering: Link{
HRef: buildURL(baseURL).appendPath(serviceOfferingsBase, servicePlan.Relationships.ServiceOffering.Data.GUID).build(),
},
Visibility: Link{
HRef: buildURL(baseURL).appendPath(servicePlansBase, servicePlan.GUID, "visibility").build(),
},
},
}
}
Expand Down
5 changes: 5 additions & 0 deletions api/presenter/service_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ var _ = Describe("Service Plan", func() {
},
},
},
VisibilityType: "visibility-type",
Relationships: repositories.ServicePlanRelationships{
ServiceOffering: model.ToOneRelationship{
Data: model.Relationship{
Expand Down Expand Up @@ -135,6 +136,7 @@ var _ = Describe("Service Plan", func() {
}
},
"guid": "resource-guid",
"visibility_type": "visibility-type",
"created_at": "1970-01-01T00:00:01Z",
"updated_at": "1970-01-01T00:00:02Z",
"metadata": {
Expand All @@ -158,6 +160,9 @@ var _ = Describe("Service Plan", func() {
},
"service_offering": {
"href": "https://api.example.org/v3/service_offerings/service-offering-guid"
},
"visibility": {
"href": "https://api.example.org/v3/service_plans/resource-guid/visibility"
}
}
}`))
Expand Down
Loading

0 comments on commit 5eccff9

Please sign in to comment.