From 2a7905889e82b309198c582616b9a36d69bd4852 Mon Sep 17 00:00:00 2001 From: Danail Branekov Date: Wed, 31 Jul 2024 13:38:28 +0000 Subject: [PATCH] Implement `GET /v3/service_plans/:guid/visibility` fixes #3274 Co-authored-by: Georgi Sabev --- .../fake/cfservice_plan_repository.go | 83 ++++++++ api/handlers/service_plan.go | 22 +- api/handlers/service_plan_test.go | 40 ++++ api/payloads/service_plan.go | 2 +- api/presenter/service_plan.go | 9 + api/presenter/service_plan_test.go | 197 ++++++++++-------- api/repositories/service_plan_repository.go | 28 ++- .../service_plan_repository_test.go | 158 ++++++++------ .../api/v1alpha1/cfservice_plan_types.go | 8 + .../api/v1alpha1/zz_generated.deepcopy.go | 16 ++ .../services/brokers/controller.go | 29 +-- .../services/brokers/controller_test.go | 63 +++--- .../korifi/controllers/cf_roles/cf_admin.yaml | 1 + .../cf_roles/cf_root_namespace_user.yaml | 7 + ...orifi.cloudfoundry.org_cfserviceplans.yaml | 10 + model/services/plans.go | 18 +- model/services/zz_generated.deepcopy.go | 20 +- tests/e2e/e2e_suite_test.go | 4 + tests/e2e/service_plans_test.go | 37 ++++ 19 files changed, 523 insertions(+), 229 deletions(-) diff --git a/api/handlers/fake/cfservice_plan_repository.go b/api/handlers/fake/cfservice_plan_repository.go index 9e94d6d44..e1faa8a20 100644 --- a/api/handlers/fake/cfservice_plan_repository.go +++ b/api/handlers/fake/cfservice_plan_repository.go @@ -11,6 +11,21 @@ import ( ) type CFServicePlanRepository struct { + GetPlanVisibilityStub func(context.Context, authorization.Info, string) (repositories.ServicePlanVisibilityRecord, error) + getPlanVisibilityMutex sync.RWMutex + getPlanVisibilityArgsForCall []struct { + arg1 context.Context + arg2 authorization.Info + arg3 string + } + getPlanVisibilityReturns struct { + result1 repositories.ServicePlanVisibilityRecord + result2 error + } + getPlanVisibilityReturnsOnCall map[int]struct { + result1 repositories.ServicePlanVisibilityRecord + result2 error + } ListPlansStub func(context.Context, authorization.Info, repositories.ListServicePlanMessage) ([]repositories.ServicePlanRecord, error) listPlansMutex sync.RWMutex listPlansArgsForCall []struct { @@ -30,6 +45,72 @@ type CFServicePlanRepository struct { invocationsMutex sync.RWMutex } +func (fake *CFServicePlanRepository) GetPlanVisibility(arg1 context.Context, arg2 authorization.Info, arg3 string) (repositories.ServicePlanVisibilityRecord, error) { + fake.getPlanVisibilityMutex.Lock() + ret, specificReturn := fake.getPlanVisibilityReturnsOnCall[len(fake.getPlanVisibilityArgsForCall)] + fake.getPlanVisibilityArgsForCall = append(fake.getPlanVisibilityArgsForCall, struct { + arg1 context.Context + arg2 authorization.Info + arg3 string + }{arg1, arg2, arg3}) + stub := fake.GetPlanVisibilityStub + fakeReturns := fake.getPlanVisibilityReturns + fake.recordInvocation("GetPlanVisibility", []interface{}{arg1, arg2, arg3}) + fake.getPlanVisibilityMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *CFServicePlanRepository) GetPlanVisibilityCallCount() int { + fake.getPlanVisibilityMutex.RLock() + defer fake.getPlanVisibilityMutex.RUnlock() + return len(fake.getPlanVisibilityArgsForCall) +} + +func (fake *CFServicePlanRepository) GetPlanVisibilityCalls(stub func(context.Context, authorization.Info, string) (repositories.ServicePlanVisibilityRecord, error)) { + fake.getPlanVisibilityMutex.Lock() + defer fake.getPlanVisibilityMutex.Unlock() + fake.GetPlanVisibilityStub = stub +} + +func (fake *CFServicePlanRepository) GetPlanVisibilityArgsForCall(i int) (context.Context, authorization.Info, string) { + fake.getPlanVisibilityMutex.RLock() + defer fake.getPlanVisibilityMutex.RUnlock() + argsForCall := fake.getPlanVisibilityArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *CFServicePlanRepository) GetPlanVisibilityReturns(result1 repositories.ServicePlanVisibilityRecord, result2 error) { + fake.getPlanVisibilityMutex.Lock() + defer fake.getPlanVisibilityMutex.Unlock() + fake.GetPlanVisibilityStub = nil + fake.getPlanVisibilityReturns = struct { + result1 repositories.ServicePlanVisibilityRecord + result2 error + }{result1, result2} +} + +func (fake *CFServicePlanRepository) GetPlanVisibilityReturnsOnCall(i int, result1 repositories.ServicePlanVisibilityRecord, result2 error) { + fake.getPlanVisibilityMutex.Lock() + defer fake.getPlanVisibilityMutex.Unlock() + fake.GetPlanVisibilityStub = nil + if fake.getPlanVisibilityReturnsOnCall == nil { + fake.getPlanVisibilityReturnsOnCall = make(map[int]struct { + result1 repositories.ServicePlanVisibilityRecord + result2 error + }) + } + fake.getPlanVisibilityReturnsOnCall[i] = struct { + result1 repositories.ServicePlanVisibilityRecord + result2 error + }{result1, result2} +} + func (fake *CFServicePlanRepository) ListPlans(arg1 context.Context, arg2 authorization.Info, arg3 repositories.ListServicePlanMessage) ([]repositories.ServicePlanRecord, error) { fake.listPlansMutex.Lock() ret, specificReturn := fake.listPlansReturnsOnCall[len(fake.listPlansArgsForCall)] @@ -99,6 +180,8 @@ func (fake *CFServicePlanRepository) ListPlansReturnsOnCall(i int, result1 []rep func (fake *CFServicePlanRepository) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() + fake.getPlanVisibilityMutex.RLock() + defer fake.getPlanVisibilityMutex.RUnlock() fake.listPlansMutex.RLock() defer fake.listPlansMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} diff --git a/api/handlers/service_plan.go b/api/handlers/service_plan.go index 297b9b62c..a3542d39f 100644 --- a/api/handlers/service_plan.go +++ b/api/handlers/service_plan.go @@ -16,12 +16,14 @@ import ( ) const ( - ServicePlansPath = "/v3/service_plans" + ServicePlansPath = "/v3/service_plans" + ServicePlanVisivilityPath = "/v3/service_plans/{guid}/visibility" ) //counterfeiter:generate -o fake -fake-name CFServicePlanRepository . CFServicePlanRepository type CFServicePlanRepository interface { ListPlans(context.Context, authorization.Info, repositories.ListServicePlanMessage) ([]repositories.ServicePlanRecord, error) + GetPlanVisibility(context.Context, authorization.Info, string) (repositories.ServicePlanVisibilityRecord, error) } type ServicePlan struct { @@ -53,12 +55,27 @@ func (h *ServicePlan) list(r *http.Request) (*routing.Response, error) { servicePlanList, err := h.servicePlanRepo.ListPlans(r.Context(), authInfo, payload.ToMessage()) if err != nil { - return nil, apierrors.LogAndReturn(logger, err, "Failed to list service plans") + 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 } +func (h *ServicePlan) getPlanVisibility(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) + + visibility, err := h.servicePlanRepo.GetPlanVisibility(r.Context(), authInfo, planGUID) + if err != nil { + return nil, apierrors.LogAndReturn(logger, err, "failed to get plan visibility") + } + + return routing.NewResponse(http.StatusOK).WithBody(presenter.ForServicePlanVisibility(visibility, h.serverURL)), nil +} + func (h *ServicePlan) UnauthenticatedRoutes() []routing.Route { return nil } @@ -66,5 +83,6 @@ func (h *ServicePlan) UnauthenticatedRoutes() []routing.Route { func (h *ServicePlan) AuthenticatedRoutes() []routing.Route { return []routing.Route{ {Method: "GET", Pattern: ServicePlansPath, Handler: h.list}, + {Method: "GET", Pattern: ServicePlanVisivilityPath, Handler: h.getPlanVisibility}, } } diff --git a/api/handlers/service_plan_test.go b/api/handlers/service_plan_test.go index 28591e5b4..da8bf4ac1 100644 --- a/api/handlers/service_plan_test.go +++ b/api/handlers/service_plan_test.go @@ -11,6 +11,7 @@ import ( "code.cloudfoundry.org/korifi/model" . "code.cloudfoundry.org/korifi/tests/matchers" + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -106,4 +107,43 @@ var _ = Describe("ServicePlan", func() { }) }) }) + + Describe("GET /v3/service_plans/{guid}/visibility", func() { + BeforeEach(func() { + servicePlanRepo.GetPlanVisibilityReturns(repositories.ServicePlanVisibilityRecord{ + Type: korifiv1alpha1.AdminServicePlanVisibilityType, + }, nil) + }) + + JustBeforeEach(func() { + req, err := http.NewRequestWithContext(ctx, "GET", "/v3/service_plans/my-service-plan/visibility", nil) + Expect(err).NotTo(HaveOccurred()) + + routerBuilder.Build().ServeHTTP(rr, req) + }) + + It("returns the plan visibility", func() { + Expect(servicePlanRepo.GetPlanVisibilityCallCount()).To(Equal(1)) + _, actualAuthInfo, actualPlanID := servicePlanRepo.GetPlanVisibilityArgsForCall(0) + Expect(actualPlanID).To(Equal("my-service-plan")) + Expect(actualAuthInfo).To(Equal(authInfo)) + + Expect(rr).To(HaveHTTPStatus(http.StatusOK)) + Expect(rr).To(HaveHTTPHeaderWithValue("Content-Type", "application/json")) + + Expect(rr).To(HaveHTTPBody(SatisfyAll( + MatchJSONPath("$.type", korifiv1alpha1.AdminServicePlanVisibilityType), + ))) + }) + + When("getting the visibility fails", func() { + BeforeEach(func() { + servicePlanRepo.GetPlanVisibilityReturns(repositories.ServicePlanVisibilityRecord{}, errors.New("visibility-err")) + }) + + It("returns an error", func() { + expectUnknownError() + }) + }) + }) }) diff --git a/api/payloads/service_plan.go b/api/payloads/service_plan.go index 4672063d2..e19039fbe 100644 --- a/api/payloads/service_plan.go +++ b/api/payloads/service_plan.go @@ -19,7 +19,7 @@ func (l *ServicePlanList) ToMessage() repositories.ListServicePlanMessage { } func (l *ServicePlanList) SupportedKeys() []string { - return []string{"service_offering_guids", "page", "per_page"} + return []string{"service_offering_guids", "page", "per_page", "include"} } func (l *ServicePlanList) IgnoredKeys() []*regexp.Regexp { diff --git a/api/presenter/service_plan.go b/api/presenter/service_plan.go index 4120c7ee4..e154e577f 100644 --- a/api/presenter/service_plan.go +++ b/api/presenter/service_plan.go @@ -4,6 +4,7 @@ import ( "net/url" "code.cloudfoundry.org/korifi/api/repositories" + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" ) type ServicePlanLinks struct { @@ -29,3 +30,11 @@ func ForServicePlan(servicePlan repositories.ServicePlanRecord, baseURL url.URL) }, } } + +type ServicePlanVisibilityResponse korifiv1alpha1.ServicePlanVisibility + +func ForServicePlanVisibility(visibility repositories.ServicePlanVisibilityRecord, _ url.URL) ServicePlanVisibilityResponse { + return ServicePlanVisibilityResponse{ + Type: visibility.Type, + } +} diff --git a/api/presenter/service_plan_test.go b/api/presenter/service_plan_test.go index 5621862df..ddc2c6a63 100644 --- a/api/presenter/service_plan_test.go +++ b/api/presenter/service_plan_test.go @@ -19,16 +19,20 @@ var _ = Describe("Service Plan", func() { var ( baseURL *url.URL output []byte - record repositories.ServicePlanRecord ) BeforeEach(func() { var err error baseURL, err = url.Parse("https://api.example.org") Expect(err).NotTo(HaveOccurred()) - record = repositories.ServicePlanRecord{ - ServicePlan: services.ServicePlan{ - BrokerServicePlan: services.BrokerServicePlan{ + }) + + Describe("ForServicePlan", func() { + var record repositories.ServicePlanRecord + + BeforeEach(func() { + record = repositories.ServicePlanRecord{ + ServicePlan: services.ServicePlan{ Name: "my-service-plan", Free: true, Description: "service plan description", @@ -64,99 +68,122 @@ var _ = Describe("Service Plan", func() { }, }, }, - }, - CFResource: model.CFResource{ - GUID: "resource-guid", - CreatedAt: time.UnixMilli(1000), - UpdatedAt: tools.PtrTo(time.UnixMilli(2000)), - Metadata: model.Metadata{ - Labels: map[string]string{ - "label": "label-foo", - }, - Annotations: map[string]string{ - "annotation": "annotation-bar", + CFResource: model.CFResource{ + GUID: "resource-guid", + CreatedAt: time.UnixMilli(1000), + UpdatedAt: tools.PtrTo(time.UnixMilli(2000)), + Metadata: model.Metadata{ + Labels: map[string]string{ + "label": "label-foo", + }, + Annotations: map[string]string{ + "annotation": "annotation-bar", + }, }, }, - }, - Relationships: repositories.ServicePlanRelationships{ - ServiceOffering: model.ToOneRelationship{ - Data: model.Relationship{ - GUID: "service-offering-guid", + Relationships: repositories.ServicePlanRelationships{ + ServiceOffering: model.ToOneRelationship{ + Data: model.Relationship{ + GUID: "service-offering-guid", + }, }, }, - }, - } - }) + } + }) - JustBeforeEach(func() { - response := presenter.ForServicePlan(record, *baseURL) - var err error - output, err = json.Marshal(response) - Expect(err).NotTo(HaveOccurred()) - }) + JustBeforeEach(func() { + response := presenter.ForServicePlan(record, *baseURL) + var err error + output, err = json.Marshal(response) + Expect(err).NotTo(HaveOccurred()) + }) - It("returns the expected JSON", func() { - Expect(output).To(MatchJSON(`{ - "name": "my-service-plan", - "free": true, - "description": "service plan description", - "broker_catalog": { - "id": "broker-catalog-plan-guid", - "metadata": { - "foo": "bar" - }, - "features": { - "plan_updateable": true, - "bindable": true - } - }, - "schemas": { - "service_instance": { - "create": { - "parameters": { - "create-param": "create-value" + It("returns the expected JSON", func() { + Expect(output).To(MatchJSON(`{ + "name": "my-service-plan", + "free": true, + "description": "service plan description", + "broker_catalog": { + "id": "broker-catalog-plan-guid", + "metadata": { + "foo": "bar" + }, + "features": { + "plan_updateable": true, + "bindable": true } }, - "update": { - "parameters": { - "update-param": "update-value" + "schemas": { + "service_instance": { + "create": { + "parameters": { + "create-param": "create-value" + } + }, + "update": { + "parameters": { + "update-param": "update-value" + } + } + }, + "service_binding": { + "create": { + "parameters": { + "binding-create-param": "binding-create-value" + } + } } - } - }, - "service_binding": { - "create": { - "parameters": { - "binding-create-param": "binding-create-value" + }, + "guid": "resource-guid", + "created_at": "1970-01-01T00:00:01Z", + "updated_at": "1970-01-01T00:00:02Z", + "metadata": { + "labels": { + "label": "label-foo" + }, + "annotations": { + "annotation": "annotation-bar" + } + }, + "relationships": { + "service_offering": { + "data": { + "guid": "service-offering-guid" + } } - } - } - }, - "guid": "resource-guid", - "created_at": "1970-01-01T00:00:01Z", - "updated_at": "1970-01-01T00:00:02Z", - "metadata": { - "labels": { - "label": "label-foo" }, - "annotations": { - "annotation": "annotation-bar" - } - }, - "relationships": { - "service_offering": { - "data": { - "guid": "service-offering-guid" + "links": { + "self": { + "href": "https://api.example.org/v3/service_plans/resource-guid" + }, + "service_offering": { + "href": "https://api.example.org/v3/service_offerings/service-offering-guid" + } } - } - }, - "links": { - "self": { - "href": "https://api.example.org/v3/service_plans/resource-guid" - }, - "service_offering": { - "href": "https://api.example.org/v3/service_offerings/service-offering-guid" - } + }`)) + }) + }) + + Describe("ForServicePlanVisibility", func() { + var record repositories.ServicePlanVisibilityRecord + + BeforeEach(func() { + record = repositories.ServicePlanVisibilityRecord{ + Type: "admin", } - }`)) + }) + + JustBeforeEach(func() { + response := presenter.ForServicePlanVisibility(record, url.URL{}) + var err error + output, err = json.Marshal(response) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns the expected JSON", func() { + Expect(output).To(MatchJSON(`{ + "type": "admin" + }`)) + }) }) }) diff --git a/api/repositories/service_plan_repository.go b/api/repositories/service_plan_repository.go index 9db1f28ce..529779513 100644 --- a/api/repositories/service_plan_repository.go +++ b/api/repositories/service_plan_repository.go @@ -10,10 +10,14 @@ import ( "code.cloudfoundry.org/korifi/model" "code.cloudfoundry.org/korifi/model/services" "github.com/BooleanCat/go-functional/iter" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" ) -const ServicePlanResourceType = "Service Plan" +const ( + ServicePlanResourceType = "Service Plan" + ServicePlanVisibilityResourceType = "Service Plan Visibility" +) type ServicePlanRecord struct { services.ServicePlan @@ -21,6 +25,8 @@ type ServicePlanRecord struct { Relationships ServicePlanRelationships `json:"relationships"` } +type ServicePlanVisibilityRecord korifiv1alpha1.ServicePlanVisibility + type ServicePlanRelationships struct { ServiceOffering model.ToOneRelationship `json:"service_offering"` } @@ -62,6 +68,26 @@ func (r *ServicePlanRepo) ListPlans(ctx context.Context, authInfo authorization. return iter.Map(iter.Lift(cfServicePlans.Items).Filter(message.matches), planToRecord).Collect(), nil } +func (r *ServicePlanRepo) GetPlanVisibility(ctx context.Context, authInfo authorization.Info, planGUID string) (ServicePlanVisibilityRecord, error) { + userClient, err := r.userClientFactory.BuildClient(authInfo) + if err != nil { + return ServicePlanVisibilityRecord{}, fmt.Errorf("failed to build user client: %w", err) + } + + cfServicePlan := &korifiv1alpha1.CFServicePlan{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: r.rootNamespace, + Name: planGUID, + }, + } + + err = userClient.Get(ctx, client.ObjectKeyFromObject(cfServicePlan), cfServicePlan) + if err != nil { + return ServicePlanVisibilityRecord{}, apierrors.FromK8sError(err, ServicePlanVisibilityResourceType) + } + return ServicePlanVisibilityRecord(cfServicePlan.Spec.Visibility), nil +} + func planToRecord(plan korifiv1alpha1.CFServicePlan) ServicePlanRecord { return ServicePlanRecord{ ServicePlan: plan.Spec.ServicePlan, diff --git a/api/repositories/service_plan_repository_test.go b/api/repositories/service_plan_repository_test.go index 6c0b33d1f..497d78f87 100644 --- a/api/repositories/service_plan_repository_test.go +++ b/api/repositories/service_plan_repository_test.go @@ -44,43 +44,44 @@ var _ = Describe("ServicePlanRepo", func() { }, Spec: korifiv1alpha1.CFServicePlanSpec{ ServicePlan: services.ServicePlan{ - BrokerServicePlan: services.BrokerServicePlan{ - Name: "my-service-plan", - Free: true, - Description: "service plan description", - BrokerCatalog: services.ServicePlanBrokerCatalog{ - ID: "broker-plan-guid", - Metadata: &runtime.RawExtension{ - Raw: []byte(`{"foo":"bar"}`), - }, - Features: services.ServicePlanFeatures{ - PlanUpdateable: true, - Bindable: true, - }, + Name: "my-service-plan", + Free: true, + Description: "service plan description", + BrokerCatalog: services.ServicePlanBrokerCatalog{ + ID: "broker-plan-guid", + Metadata: &runtime.RawExtension{ + Raw: []byte(`{"foo":"bar"}`), + }, + Features: services.ServicePlanFeatures{ + PlanUpdateable: true, + Bindable: true, }, - Schemas: services.ServicePlanSchemas{ - ServiceInstance: services.ServiceInstanceSchema{ - Create: services.InputParameterSchema{ - Parameters: &runtime.RawExtension{ - Raw: []byte(`{"create-param":"create-value"}`), - }, + }, + Schemas: services.ServicePlanSchemas{ + ServiceInstance: services.ServiceInstanceSchema{ + Create: services.InputParameterSchema{ + Parameters: &runtime.RawExtension{ + Raw: []byte(`{"create-param":"create-value"}`), }, - Update: services.InputParameterSchema{ - Parameters: &runtime.RawExtension{ - Raw: []byte(`{"update-param":"update-value"}`), - }, + }, + Update: services.InputParameterSchema{ + Parameters: &runtime.RawExtension{ + Raw: []byte(`{"update-param":"update-value"}`), }, }, - ServiceBinding: services.ServiceBindingSchema{ - Create: services.InputParameterSchema{ - Parameters: &runtime.RawExtension{ - Raw: []byte(`{"binding-create-param":"binding-create-value"}`), - }, + }, + ServiceBinding: services.ServiceBindingSchema{ + Create: services.InputParameterSchema{ + Parameters: &runtime.RawExtension{ + Raw: []byte(`{"binding-create-param":"binding-create-value"}`), }, }, }, }, }, + Visibility: korifiv1alpha1.ServicePlanVisibility{ + Type: korifiv1alpha1.AdminServicePlanVisibilityType, + }, }, })).To(Succeed()) @@ -95,40 +96,38 @@ var _ = Describe("ServicePlanRepo", func() { Expect(listErr).NotTo(HaveOccurred()) Expect(listedPlans).To(ConsistOf(MatchFields(IgnoreExtras, Fields{ "ServicePlan": MatchFields(IgnoreExtras, Fields{ - "BrokerServicePlan": MatchFields(IgnoreExtras, Fields{ - "Name": Equal("my-service-plan"), - "Description": Equal("service plan description"), - "Free": BeTrue(), - "BrokerCatalog": MatchFields(IgnoreExtras, Fields{ - "ID": Equal("broker-plan-guid"), - "Metadata": PointTo(MatchFields(IgnoreExtras, Fields{ - "Raw": MatchJSON(`{"foo": "bar"}`), - })), - - "Features": MatchFields(IgnoreExtras, Fields{ - "PlanUpdateable": BeTrue(), - "Bindable": BeTrue(), - }), + "Name": Equal("my-service-plan"), + "Description": Equal("service plan description"), + "Free": BeTrue(), + "BrokerCatalog": MatchFields(IgnoreExtras, Fields{ + "ID": Equal("broker-plan-guid"), + "Metadata": PointTo(MatchFields(IgnoreExtras, Fields{ + "Raw": MatchJSON(`{"foo": "bar"}`), + })), + + "Features": MatchFields(IgnoreExtras, Fields{ + "PlanUpdateable": BeTrue(), + "Bindable": BeTrue(), }), - "Schemas": MatchFields(IgnoreExtras, Fields{ - "ServiceInstance": MatchFields(IgnoreExtras, Fields{ - "Create": MatchFields(IgnoreExtras, Fields{ - "Parameters": PointTo(MatchFields(IgnoreExtras, Fields{ - "Raw": MatchJSON(`{"create-param":"create-value"}`), - })), - }), - "Update": MatchFields(IgnoreExtras, Fields{ - "Parameters": PointTo(MatchFields(IgnoreExtras, Fields{ - "Raw": MatchJSON(`{"update-param":"update-value"}`), - })), - }), + }), + "Schemas": MatchFields(IgnoreExtras, Fields{ + "ServiceInstance": MatchFields(IgnoreExtras, Fields{ + "Create": MatchFields(IgnoreExtras, Fields{ + "Parameters": PointTo(MatchFields(IgnoreExtras, Fields{ + "Raw": MatchJSON(`{"create-param":"create-value"}`), + })), + }), + "Update": MatchFields(IgnoreExtras, Fields{ + "Parameters": PointTo(MatchFields(IgnoreExtras, Fields{ + "Raw": MatchJSON(`{"update-param":"update-value"}`), + })), }), - "ServiceBinding": MatchFields(IgnoreExtras, Fields{ - "Create": MatchFields(IgnoreExtras, Fields{ - "Parameters": PointTo(MatchFields(IgnoreExtras, Fields{ - "Raw": MatchJSON(`{"binding-create-param": "binding-create-value"}`), - })), - }), + }), + "ServiceBinding": MatchFields(IgnoreExtras, Fields{ + "Create": MatchFields(IgnoreExtras, Fields{ + "Parameters": PointTo(MatchFields(IgnoreExtras, Fields{ + "Raw": MatchJSON(`{"binding-create-param": "binding-create-value"}`), + })), }), }), }), @@ -162,6 +161,11 @@ var _ = Describe("ServicePlanRepo", func() { korifiv1alpha1.RelServiceOfferingLabel: "other-offering-guid", }, }, + Spec: korifiv1alpha1.CFServicePlanSpec{ + Visibility: korifiv1alpha1.ServicePlanVisibility{ + Type: korifiv1alpha1.AdminServicePlanVisibilityType, + }, + }, })).To(Succeed()) message.ServiceOfferingGUIDs = []string{"other-offering-guid"} @@ -181,4 +185,38 @@ var _ = Describe("ServicePlanRepo", func() { }) }) }) + + Describe("GetPlanVisibility", func() { + var ( + planGUID string + visibility repositories.ServicePlanVisibilityRecord + ) + + BeforeEach(func() { + planGUID = uuid.NewString() + Expect(k8sClient.Create(ctx, &korifiv1alpha1.CFServicePlan{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: rootNamespace, + Name: planGUID, + }, + Spec: korifiv1alpha1.CFServicePlanSpec{ + Visibility: korifiv1alpha1.ServicePlanVisibility{ + Type: korifiv1alpha1.AdminServicePlanVisibilityType, + }, + }, + })).To(Succeed()) + }) + + JustBeforeEach(func() { + var err error + visibility, err = repo.GetPlanVisibility(ctx, authInfo, planGUID) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns the plan visibility", func() { + Expect(visibility).To(Equal(repositories.ServicePlanVisibilityRecord{ + Type: korifiv1alpha1.AdminServicePlanVisibilityType, + })) + }) + }) }) diff --git a/controllers/api/v1alpha1/cfservice_plan_types.go b/controllers/api/v1alpha1/cfservice_plan_types.go index 48e1db9aa..eb64771ee 100644 --- a/controllers/api/v1alpha1/cfservice_plan_types.go +++ b/controllers/api/v1alpha1/cfservice_plan_types.go @@ -7,6 +7,14 @@ import ( type CFServicePlanSpec struct { services.ServicePlan `json:",inline"` + Visibility ServicePlanVisibility `json:"visibility"` +} + +const AdminServicePlanVisibilityType = "admin" + +type ServicePlanVisibility struct { + // +kubebuilder:validation:Enum=admin + Type string `json:"type"` } // +kubebuilder:object:root=true diff --git a/controllers/api/v1alpha1/zz_generated.deepcopy.go b/controllers/api/v1alpha1/zz_generated.deepcopy.go index b6bc549ff..20a6ea487 100644 --- a/controllers/api/v1alpha1/zz_generated.deepcopy.go +++ b/controllers/api/v1alpha1/zz_generated.deepcopy.go @@ -1622,6 +1622,7 @@ func (in *CFServicePlanList) DeepCopyObject() runtime.Object { func (in *CFServicePlanSpec) DeepCopyInto(out *CFServicePlanSpec) { *out = *in in.ServicePlan.DeepCopyInto(&out.ServicePlan) + out.Visibility = in.Visibility } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CFServicePlanSpec. @@ -2099,6 +2100,21 @@ func (in *RunnerInfoStatus) DeepCopy() *RunnerInfoStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServicePlanVisibility) DeepCopyInto(out *ServicePlanVisibility) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServicePlanVisibility. +func (in *ServicePlanVisibility) DeepCopy() *ServicePlanVisibility { + if in == nil { + return nil + } + out := new(ServicePlanVisibility) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TaskWorkload) DeepCopyInto(out *TaskWorkload) { *out = *in diff --git a/controllers/controllers/services/brokers/controller.go b/controllers/controllers/services/brokers/controller.go index c460aeeb5..1e86b7ab0 100644 --- a/controllers/controllers/services/brokers/controller.go +++ b/controllers/controllers/services/brokers/controller.go @@ -235,22 +235,23 @@ func (r *Reconciler) reconcileCatalogPlan(ctx context.Context, serviceOffering * servicePlan.Spec = korifiv1alpha1.CFServicePlanSpec{ ServicePlan: services.ServicePlan{ - BrokerServicePlan: services.BrokerServicePlan{ - Name: catalogPlan.Name, - Free: catalogPlan.Free, - Description: catalogPlan.Description, - BrokerCatalog: services.ServicePlanBrokerCatalog{ - ID: catalogPlan.ID, - Metadata: &runtime.RawExtension{ - Raw: rawMetadata, - }, - Features: services.ServicePlanFeatures{ - PlanUpdateable: catalogPlan.PlanUpdateable, - Bindable: catalogPlan.Bindable, - }, + Name: catalogPlan.Name, + Free: catalogPlan.Free, + Description: catalogPlan.Description, + BrokerCatalog: services.ServicePlanBrokerCatalog{ + ID: catalogPlan.ID, + Metadata: &runtime.RawExtension{ + Raw: rawMetadata, + }, + Features: services.ServicePlanFeatures{ + PlanUpdateable: catalogPlan.PlanUpdateable, + Bindable: catalogPlan.Bindable, }, - Schemas: catalogPlan.Schemas, }, + Schemas: catalogPlan.Schemas, + }, + Visibility: korifiv1alpha1.ServicePlanVisibility{ + Type: korifiv1alpha1.AdminServicePlanVisibilityType, }, } diff --git a/controllers/controllers/services/brokers/controller_test.go b/controllers/controllers/services/brokers/controller_test.go index bd392ecb1..04f3fb3f6 100644 --- a/controllers/controllers/services/brokers/controller_test.go +++ b/controllers/controllers/services/brokers/controller_test.go @@ -191,43 +191,44 @@ var _ = Describe("CFServiceBroker", func() { )) g.Expect(plan.Spec).To(MatchAllFields(Fields{ "ServicePlan": MatchAllFields(Fields{ - "BrokerServicePlan": MatchAllFields(Fields{ - "Name": Equal("plan-name"), - "Free": BeTrue(), - "Description": Equal("plan description"), - "BrokerCatalog": MatchAllFields(Fields{ - "ID": Equal("plan-id"), - "Metadata": PointTo(MatchFields(IgnoreExtras, Fields{ - "Raw": MatchJSON(`{"plan-md": "plan-md-value"}`), - })), - "Features": Equal(services.ServicePlanFeatures{ - PlanUpdateable: true, - Bindable: true, - }), + "Name": Equal("plan-name"), + "Free": BeTrue(), + "Description": Equal("plan description"), + "BrokerCatalog": MatchAllFields(Fields{ + "ID": Equal("plan-id"), + "Metadata": PointTo(MatchFields(IgnoreExtras, Fields{ + "Raw": MatchJSON(`{"plan-md": "plan-md-value"}`), + })), + "Features": Equal(services.ServicePlanFeatures{ + PlanUpdateable: true, + Bindable: true, }), - "Schemas": MatchFields(IgnoreExtras, Fields{ - "ServiceInstance": MatchFields(IgnoreExtras, Fields{ - "Create": MatchFields(IgnoreExtras, Fields{ - "Parameters": PointTo(MatchFields(IgnoreExtras, Fields{ - "Raw": MatchJSON(`{"create-param":"create-value"}`), - })), - }), - "Update": MatchFields(IgnoreExtras, Fields{ - "Parameters": PointTo(MatchFields(IgnoreExtras, Fields{ - "Raw": MatchJSON(`{"update-param":"update-value"}`), - })), - }), + }), + "Schemas": MatchFields(IgnoreExtras, Fields{ + "ServiceInstance": MatchFields(IgnoreExtras, Fields{ + "Create": MatchFields(IgnoreExtras, Fields{ + "Parameters": PointTo(MatchFields(IgnoreExtras, Fields{ + "Raw": MatchJSON(`{"create-param":"create-value"}`), + })), + }), + "Update": MatchFields(IgnoreExtras, Fields{ + "Parameters": PointTo(MatchFields(IgnoreExtras, Fields{ + "Raw": MatchJSON(`{"update-param":"update-value"}`), + })), }), - "ServiceBinding": MatchFields(IgnoreExtras, Fields{ - "Create": MatchFields(IgnoreExtras, Fields{ - "Parameters": PointTo(MatchFields(IgnoreExtras, Fields{ - "Raw": MatchJSON(`{"binding-create-param": "binding-create-value"}`), - })), - }), + }), + "ServiceBinding": MatchFields(IgnoreExtras, Fields{ + "Create": MatchFields(IgnoreExtras, Fields{ + "Parameters": PointTo(MatchFields(IgnoreExtras, Fields{ + "Raw": MatchJSON(`{"binding-create-param": "binding-create-value"}`), + })), }), }), }), }), + "Visibility": MatchAllFields(Fields{ + "Type": Equal(korifiv1alpha1.AdminServicePlanVisibilityType), + }), })) }).Should(Succeed()) }) diff --git a/helm/korifi/controllers/cf_roles/cf_admin.yaml b/helm/korifi/controllers/cf_roles/cf_admin.yaml index 2fd587276..a70c2013a 100644 --- a/helm/korifi/controllers/cf_roles/cf_admin.yaml +++ b/helm/korifi/controllers/cf_roles/cf_admin.yaml @@ -205,6 +205,7 @@ rules: - cfserviceplans verbs: - list + - get - apiGroups: - rbac.authorization.k8s.io diff --git a/helm/korifi/controllers/cf_roles/cf_root_namespace_user.yaml b/helm/korifi/controllers/cf_roles/cf_root_namespace_user.yaml index 255f3dc43..0b2b00658 100644 --- a/helm/korifi/controllers/cf_roles/cf_root_namespace_user.yaml +++ b/helm/korifi/controllers/cf_roles/cf_root_namespace_user.yaml @@ -47,3 +47,10 @@ rules: - cfservicebrokers verbs: - list + +- apiGroups: + - korifi.cloudfoundry.org + resources: + - cfserviceplans + verbs: + - get diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfserviceplans.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfserviceplans.yaml index 1cdb733a1..f2a94ceee 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfserviceplans.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfserviceplans.yaml @@ -111,11 +111,21 @@ spec: - service_binding - service_instance type: object + visibility: + properties: + type: + enum: + - admin + type: string + required: + - type + type: object required: - broker_catalog - free - name - schemas + - visibility type: object type: object served: true diff --git a/model/services/plans.go b/model/services/plans.go index 7e539dee3..334d47859 100644 --- a/model/services/plans.go +++ b/model/services/plans.go @@ -6,11 +6,6 @@ import ( // +kubebuilder:object:generate=true type ServicePlan struct { - BrokerServicePlan `json:",inline"` -} - -// +kubebuilder:object:generate=true -type BrokerServicePlan struct { Name string `json:"name"` Free bool `json:"free"` Description string `json:"description,omitempty"` @@ -18,22 +13,11 @@ type BrokerServicePlan struct { Schemas ServicePlanSchemas `json:"schemas"` } -type ServicePlanCost struct { - Amount string `json:"amount"` - Currency string `json:"currency"` - Unit string `json:"unit"` -} - -type ServicePlanMaintenanceInfo struct { - Version string `json:"version"` - Description string `json:"description"` -} - // +kubebuilder:object:generate=true type ServicePlanBrokerCatalog struct { ID string `json:"id"` // +kubebuilder:validation:Optional - Metadata *runtime.RawExtension `json:"metadata"` + Metadata *runtime.RawExtension `json:"metadata,omitempty"` // +kubebuilder:validation:Optional Features ServicePlanFeatures `json:"features"` } diff --git a/model/services/zz_generated.deepcopy.go b/model/services/zz_generated.deepcopy.go index 4931d1e3f..bdacf711f 100644 --- a/model/services/zz_generated.deepcopy.go +++ b/model/services/zz_generated.deepcopy.go @@ -24,23 +24,6 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BrokerServicePlan) DeepCopyInto(out *BrokerServicePlan) { - *out = *in - in.BrokerCatalog.DeepCopyInto(&out.BrokerCatalog) - in.Schemas.DeepCopyInto(&out.Schemas) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BrokerServicePlan. -func (in *BrokerServicePlan) DeepCopy() *BrokerServicePlan { - if in == nil { - return nil - } - out := new(BrokerServicePlan) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InputParameterSchema) DeepCopyInto(out *InputParameterSchema) { *out = *in @@ -164,7 +147,8 @@ func (in *ServiceOffering) DeepCopy() *ServiceOffering { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServicePlan) DeepCopyInto(out *ServicePlan) { *out = *in - in.BrokerServicePlan.DeepCopyInto(&out.BrokerServicePlan) + in.BrokerCatalog.DeepCopyInto(&out.BrokerCatalog) + in.Schemas.DeepCopyInto(&out.Schemas) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServicePlan. diff --git a/tests/e2e/e2e_suite_test.go b/tests/e2e/e2e_suite_test.go index 4dd11b7c9..c397f5642 100644 --- a/tests/e2e/e2e_suite_test.go +++ b/tests/e2e/e2e_suite_test.go @@ -295,6 +295,10 @@ type cfErr struct { Code int `json:"code"` } +type planVisibilityResource struct { + Type string `json:"type"` +} + func TestE2E(t *testing.T) { RegisterFailHandler(fail_handler.New("E2E Tests", fail_handler.Hook{ diff --git a/tests/e2e/service_plans_test.go b/tests/e2e/service_plans_test.go index 7ac20f827..8cc8334e9 100644 --- a/tests/e2e/service_plans_test.go +++ b/tests/e2e/service_plans_test.go @@ -1,9 +1,11 @@ package e2e_test import ( + "fmt" "net/http" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "github.com/BooleanCat/go-functional/iter" "github.com/go-resty/resty/v2" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -31,4 +33,39 @@ var _ = Describe("Service Plans", func() { }))) }) }) + + Describe("Get Visibility", func() { + var ( + result planVisibilityResource + planGUID string + ) + + BeforeEach(func() { + plans := resourceList[resource]{} + + listResp, err := adminClient.R().SetResult(&plans).Get("/v3/service_plans") + Expect(err).NotTo(HaveOccurred()) + Expect(listResp).To(HaveRestyStatusCode(http.StatusOK)) + + brokerPlans := iter.Lift(plans.Resources).Filter(func(r resource) bool { + return r.Metadata.Labels[korifiv1alpha1.RelServiceBrokerLabel] == serviceBrokerGUID + }).Collect() + + Expect(brokerPlans).NotTo(BeEmpty()) + planGUID = brokerPlans[0].GUID + }) + + JustBeforeEach(func() { + var err error + resp, err = adminClient.R().SetResult(&result).Get(fmt.Sprintf("/v3/service_plans/%s/visibility", planGUID)) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns the service plan visibility", func() { + Expect(resp).To(HaveRestyStatusCode(http.StatusOK)) + Expect(result).To(Equal(planVisibilityResource{ + Type: korifiv1alpha1.AdminServicePlanVisibilityType, + })) + }) + }) })