Skip to content

Commit 0d187ba

Browse files
authored
Implement API PATCH /v3/service_offerings/:guid (#4053)
1 parent 9090f8b commit 0d187ba

File tree

7 files changed

+378
-0
lines changed

7 files changed

+378
-0
lines changed

api/handlers/fake/cfservice_offering_repository.go

Lines changed: 83 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/handlers/service_offering.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const (
2424
//counterfeiter:generate -o fake -fake-name CFServiceOfferingRepository . CFServiceOfferingRepository
2525
type CFServiceOfferingRepository interface {
2626
GetServiceOffering(context.Context, authorization.Info, string) (repositories.ServiceOfferingRecord, error)
27+
UpdateServiceOffering(context.Context, authorization.Info, repositories.UpdateServiceOfferingMessage) (repositories.ServiceOfferingRecord, error)
2728
ListOfferings(context.Context, authorization.Info, repositories.ListServiceOfferingMessage) ([]repositories.ServiceOfferingRecord, error)
2829
DeleteOffering(context.Context, authorization.Info, repositories.DeleteServiceOfferingMessage) error
2930
}
@@ -79,6 +80,30 @@ func (h *ServiceOffering) get(r *http.Request) (*routing.Response, error) {
7980
return routing.NewResponse(http.StatusOK).WithBody(presenter.ForServiceOffering(serviceOffering, h.serverURL, includedResources...)), nil
8081
}
8182

83+
func (h *ServiceOffering) update(r *http.Request) (*routing.Response, error) {
84+
authInfo, _ := authorization.InfoFromContext(r.Context())
85+
logger := logr.FromContextOrDiscard(r.Context()).WithName("handlers.service-offering.update")
86+
87+
serviceOfferingGUID := routing.URLParam(r, "guid")
88+
89+
var payload payloads.ServiceOfferingUpdate
90+
if err := h.requestValidator.DecodeAndValidateJSONPayload(r, &payload); err != nil {
91+
return nil, apierrors.LogAndReturn(logger, err, "failed to decode payload")
92+
}
93+
94+
_, err := h.serviceOfferingRepo.GetServiceOffering(r.Context(), authInfo, serviceOfferingGUID)
95+
if err != nil {
96+
return nil, apierrors.LogAndReturn(logger, apierrors.ForbiddenAsNotFound(err), "Error getting service offering in repository")
97+
}
98+
99+
serviceOffering, err := h.serviceOfferingRepo.UpdateServiceOffering(r.Context(), authInfo, payload.ToMessage(serviceOfferingGUID))
100+
if err != nil {
101+
return nil, apierrors.LogAndReturn(logger, err, "Error updating service offering in repository")
102+
}
103+
104+
return routing.NewResponse(http.StatusOK).WithBody(presenter.ForServiceOffering(serviceOffering, h.serverURL)), nil
105+
}
106+
82107
func (h *ServiceOffering) list(r *http.Request) (*routing.Response, error) {
83108
authInfo, _ := authorization.InfoFromContext(r.Context())
84109
logger := logr.FromContextOrDiscard(r.Context()).WithName("handlers.service-offering.list")
@@ -125,6 +150,7 @@ func (h *ServiceOffering) UnauthenticatedRoutes() []routing.Route {
125150
func (h *ServiceOffering) AuthenticatedRoutes() []routing.Route {
126151
return []routing.Route{
127152
{Method: "GET", Pattern: ServiceOfferingPath, Handler: h.get},
153+
{Method: "PATCH", Pattern: ServiceOfferingPath, Handler: h.update},
128154
{Method: "GET", Pattern: ServiceOfferingsPath, Handler: h.list},
129155
{Method: "DELETE", Pattern: ServiceOfferingPath, Handler: h.delete},
130156
}

api/handlers/service_offering_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"code.cloudfoundry.org/korifi/api/repositories/k8sklient/descriptors"
1414
"code.cloudfoundry.org/korifi/api/repositories/relationships"
1515
. "code.cloudfoundry.org/korifi/tests/matchers"
16+
"code.cloudfoundry.org/korifi/tools"
1617

1718
. "github.com/onsi/ginkgo/v2"
1819
. "github.com/onsi/gomega"
@@ -325,4 +326,78 @@ var _ = Describe("ServiceOffering", func() {
325326
})
326327
})
327328
})
329+
330+
Describe("PATCH /v3/service_offering/:guid", func() {
331+
BeforeEach(func() {
332+
serviceOfferingRepo.UpdateServiceOfferingReturns(repositories.ServiceOfferingRecord{
333+
GUID: "offering-guid",
334+
}, nil)
335+
336+
payload := payloads.ServiceOfferingUpdate{
337+
Metadata: payloads.MetadataPatch{
338+
Labels: map[string]*string{"foo": tools.PtrTo("bar")},
339+
Annotations: map[string]*string{"bar": tools.PtrTo("baz")},
340+
},
341+
}
342+
343+
requestValidator.DecodeAndValidateJSONPayloadStub = decodeAndValidatePayloadStub(&payload)
344+
})
345+
346+
JustBeforeEach(func() {
347+
req, err := http.NewRequestWithContext(ctx, "PATCH", "/v3/service_offerings/offering-guid", nil)
348+
Expect(err).NotTo(HaveOccurred())
349+
350+
routerBuilder.Build().ServeHTTP(rr, req)
351+
})
352+
353+
It("updates the service offering", func() {
354+
Expect(rr).Should(HaveHTTPStatus(http.StatusOK))
355+
Expect(rr).To(HaveHTTPHeaderWithValue("Content-Type", "application/json"))
356+
Expect(rr).To(HaveHTTPBody(SatisfyAll(
357+
MatchJSONPath("$.guid", "offering-guid"),
358+
MatchJSONPath("$.links.self.href", "https://api.example.org/v3/service_offerings/offering-guid"),
359+
)))
360+
361+
Expect(serviceOfferingRepo.UpdateServiceOfferingCallCount()).To(Equal(1))
362+
_, actualAuthInfo, updateMessage := serviceOfferingRepo.UpdateServiceOfferingArgsForCall(0)
363+
Expect(actualAuthInfo).To(Equal(authInfo))
364+
Expect(updateMessage).To(Equal(repositories.UpdateServiceOfferingMessage{
365+
GUID: "offering-guid",
366+
MetadataPatch: repositories.MetadataPatch{
367+
Labels: map[string]*string{"foo": tools.PtrTo("bar")},
368+
Annotations: map[string]*string{"bar": tools.PtrTo("baz")},
369+
},
370+
}))
371+
})
372+
373+
When("the request is invalid", func() {
374+
BeforeEach(func() {
375+
requestValidator.DecodeAndValidateJSONPayloadReturns(errors.New("invalid-request"))
376+
})
377+
378+
It("returns an error", func() {
379+
expectUnknownError()
380+
})
381+
})
382+
383+
When("user is not allowed to get a process", func() {
384+
BeforeEach(func() {
385+
serviceOfferingRepo.GetServiceOfferingReturns(repositories.ServiceOfferingRecord{}, apierrors.NewForbiddenError(errors.New("Forbidden"), repositories.ServiceOfferingResourceType))
386+
})
387+
388+
It("returns a not found error", func() {
389+
expectNotFoundError(repositories.ServiceOfferingResourceType)
390+
})
391+
})
392+
393+
When("the service offering repo returns an error", func() {
394+
BeforeEach(func() {
395+
serviceOfferingRepo.UpdateServiceOfferingReturns(repositories.ServiceOfferingRecord{}, errors.New("update-so-error"))
396+
})
397+
398+
It("returns an error", func() {
399+
expectUnknownError()
400+
})
401+
})
402+
})
328403
})

api/payloads/service_offering.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,26 @@ func (l *ServiceOfferingGet) DecodeFromURLValues(values url.Values) error {
4747
return nil
4848
}
4949

50+
type ServiceOfferingUpdate struct {
51+
Metadata MetadataPatch `json:"metadata"`
52+
}
53+
54+
func (g ServiceOfferingUpdate) Validate() error {
55+
return jellidation.ValidateStruct(&g,
56+
jellidation.Field(&g.Metadata),
57+
)
58+
}
59+
60+
func (c *ServiceOfferingUpdate) ToMessage(serviceOfferingGUID string) repositories.UpdateServiceOfferingMessage {
61+
return repositories.UpdateServiceOfferingMessage{
62+
GUID: serviceOfferingGUID,
63+
MetadataPatch: repositories.MetadataPatch{
64+
Labels: c.Metadata.Labels,
65+
Annotations: c.Metadata.Annotations,
66+
},
67+
}
68+
}
69+
5070
type ServiceOfferingList struct {
5171
Names string
5272
BrokerNames string

api/repositories/service_offering_repository.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,20 @@ type ListServiceOfferingMessage struct {
6565
BrokerNames []string
6666
}
6767

68+
type UpdateServiceOfferingMessage struct {
69+
GUID string
70+
MetadataPatch MetadataPatch
71+
}
72+
6873
type DeleteServiceOfferingMessage struct {
6974
GUID string
7075
Purge bool
7176
}
7277

78+
func (m UpdateServiceOfferingMessage) apply(offering *korifiv1alpha1.CFServiceOffering) {
79+
m.MetadataPatch.Apply(offering)
80+
}
81+
7382
func (m *ListServiceOfferingMessage) toListOptions() []ListOption {
7483
return []ListOption{
7584
WithLabelIn(korifiv1alpha1.CFServiceOfferingNameKey, tools.EncodeValuesToSha224(m.Names...)),
@@ -258,3 +267,20 @@ func (r *ServiceOfferingRepo) fetchServiceBindings(ctx context.Context, serviceI
258267

259268
return bindings.Items, nil
260269
}
270+
271+
func (r *ServiceOfferingRepo) UpdateServiceOffering(ctx context.Context, authInfo authorization.Info, message UpdateServiceOfferingMessage) (ServiceOfferingRecord, error) {
272+
offering := &korifiv1alpha1.CFServiceOffering{
273+
ObjectMeta: metav1.ObjectMeta{
274+
Namespace: r.rootNamespace,
275+
Name: message.GUID,
276+
},
277+
}
278+
if err := GetAndPatch(ctx, r.rootNSKlient, offering, func() error {
279+
message.apply(offering)
280+
return nil
281+
}); err != nil {
282+
return ServiceOfferingRecord{}, fmt.Errorf("failed to patch service offering metadata: %w", apierrors.FromK8sError(err, ServiceOfferingResourceType))
283+
}
284+
285+
return offeringToRecord(*offering)
286+
}

0 commit comments

Comments
 (0)