diff --git a/api/payloads/service_plan.go b/api/payloads/service_plan.go index 26eead826..5d6edc013 100644 --- a/api/payloads/service_plan.go +++ b/api/payloads/service_plan.go @@ -4,12 +4,14 @@ import ( "fmt" "net/url" "regexp" + "strconv" "code.cloudfoundry.org/korifi/api/payloads/parse" "code.cloudfoundry.org/korifi/api/payloads/validation" "code.cloudfoundry.org/korifi/api/repositories" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/model/services" + "code.cloudfoundry.org/korifi/tools" "github.com/BooleanCat/go-functional/iter" jellidation "github.com/jellydator/validation" ) @@ -17,17 +19,19 @@ import ( type ServicePlanList struct { ServiceOfferingGUIDs string Names string + Available *bool } func (l *ServicePlanList) ToMessage() repositories.ListServicePlanMessage { return repositories.ListServicePlanMessage{ ServiceOfferingGUIDs: parse.ArrayParam(l.ServiceOfferingGUIDs), Names: parse.ArrayParam(l.Names), + Available: l.Available, } } func (l *ServicePlanList) SupportedKeys() []string { - return []string{"service_offering_guids", "names", "page", "per_page", "include"} + return []string{"service_offering_guids", "names", "available", "page", "per_page", "include"} } func (l *ServicePlanList) IgnoredKeys() []*regexp.Regexp { @@ -37,9 +41,28 @@ func (l *ServicePlanList) IgnoredKeys() []*regexp.Regexp { func (l *ServicePlanList) DecodeFromURLValues(values url.Values) error { l.ServiceOfferingGUIDs = values.Get("service_offering_guids") l.Names = values.Get("names") + + available, err := parseBool(values.Get("available")) + if err != nil { + return fmt.Errorf("failed to parse 'available' query parameter: %w", err) + } + l.Available = available + return nil } +func parseBool(valueStr string) (*bool, error) { + if valueStr == "" { + return nil, nil + } + + valueBool, err := strconv.ParseBool(valueStr) + if err != nil { + return nil, err + } + return tools.PtrTo(valueBool), nil +} + type ServicePlanVisibility struct { Type string `json:"type"` Organizations []services.VisibilityOrganization `json:"organizations"` diff --git a/api/payloads/service_plan_test.go b/api/payloads/service_plan_test.go index dbf3e9ae1..855ef3016 100644 --- a/api/payloads/service_plan_test.go +++ b/api/payloads/service_plan_test.go @@ -5,6 +5,7 @@ import ( "code.cloudfoundry.org/korifi/api/repositories" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/model/services" + "code.cloudfoundry.org/korifi/tools" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/onsi/gomega/types" @@ -21,15 +22,29 @@ var _ = Describe("ServicePlan", func() { }, Entry("service_offering_guids", "service_offering_guids=b1,b2", payloads.ServicePlanList{ServiceOfferingGUIDs: "b1,b2"}), 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)}), + ) + + DescribeTable("invalid query", + func(query string, errMatcher types.GomegaMatcher) { + _, decodeErr := decodeQuery[payloads.ServicePlanList](query) + Expect(decodeErr).To(errMatcher) + }, + Entry("invalid available", "available=invalid", MatchError(ContainSubstring("failed to parse"))), ) Describe("ToMessage", func() { It("converts payload to repository message", func() { - payload := &payloads.ServicePlanList{ServiceOfferingGUIDs: "b1,b2", Names: "n1,n2"} - + payload := payloads.ServicePlanList{ + ServiceOfferingGUIDs: "b1,b2", + Names: "n1,n2", + Available: tools.PtrTo(true), + } Expect(payload.ToMessage()).To(Equal(repositories.ListServicePlanMessage{ ServiceOfferingGUIDs: []string{"b1", "b2"}, Names: []string{"n1", "n2"}, + Available: tools.PtrTo(true), })) }) }) diff --git a/api/presenter/service_plan.go b/api/presenter/service_plan.go index 8973fa011..dda3491c0 100644 --- a/api/presenter/service_plan.go +++ b/api/presenter/service_plan.go @@ -22,6 +22,7 @@ type ServicePlanResponse struct { services.ServicePlan model.CFResource VisibilityType string `json:"visibility_type"` + Available bool `json:"available"` Relationships ServicePlanRelationships `json:"relationships"` Links ServicePlanLinks `json:"links"` } @@ -31,6 +32,7 @@ func ForServicePlan(servicePlan repositories.ServicePlanRecord, baseURL url.URL) ServicePlan: servicePlan.ServicePlan, CFResource: servicePlan.CFResource, VisibilityType: servicePlan.Visibility.Type, + Available: servicePlan.Available, Relationships: ServicePlanRelationships{ ServiceOffering: model.ToOneRelationship{ Data: model.Relationship{ diff --git a/api/presenter/service_plan_test.go b/api/presenter/service_plan_test.go index 6a527ba85..8540227c4 100644 --- a/api/presenter/service_plan_test.go +++ b/api/presenter/service_plan_test.go @@ -85,6 +85,7 @@ var _ = Describe("Service Plan", func() { Type: "visibility-type", }, ServiceOfferingGUID: "service-offering-guid", + Available: true, } }) @@ -133,6 +134,7 @@ var _ = Describe("Service Plan", func() { }, "guid": "resource-guid", "visibility_type": "visibility-type", + "available": true, "created_at": "1970-01-01T00:00:01Z", "updated_at": "1970-01-01T00:00:02Z", "metadata": { diff --git a/api/repositories/service_plan_repository.go b/api/repositories/service_plan_repository.go index 076bf133a..c6d08c490 100644 --- a/api/repositories/service_plan_repository.go +++ b/api/repositories/service_plan_repository.go @@ -25,6 +25,7 @@ type ServicePlanRecord struct { model.CFResource Visibility PlanVisibility ServiceOfferingGUID string + Available bool } type PlanVisibility struct { @@ -41,11 +42,17 @@ type ServicePlanRepo struct { type ListServicePlanMessage struct { ServiceOfferingGUIDs []string Names []string + Available *bool } func (m *ListServicePlanMessage) matches(cfServicePlan korifiv1alpha1.CFServicePlan) bool { return tools.EmptyOrContains(m.ServiceOfferingGUIDs, cfServicePlan.Labels[korifiv1alpha1.RelServiceOfferingLabel]) && - tools.EmptyOrContains(m.Names, cfServicePlan.Spec.Name) + tools.EmptyOrContains(m.Names, cfServicePlan.Spec.Name) && + tools.NilOrEquals(m.Available, isAvailable(cfServicePlan)) +} + +func isAvailable(cfServicePlan korifiv1alpha1.CFServicePlan) bool { + return cfServicePlan.Spec.Visibility.Type != korifiv1alpha1.AdminServicePlanVisibilityType } type ApplyServicePlanVisibilityMessage struct { @@ -193,6 +200,7 @@ func (r *ServicePlanRepo) planToRecord(ctx context.Context, authInfo authorizati Organizations: organizations, }, ServiceOfferingGUID: plan.Labels[korifiv1alpha1.RelServiceOfferingLabel], + Available: isAvailable(plan), }, nil } diff --git a/api/repositories/service_plan_repository_test.go b/api/repositories/service_plan_repository_test.go index a5929c9f9..22781f6e5 100644 --- a/api/repositories/service_plan_repository_test.go +++ b/api/repositories/service_plan_repository_test.go @@ -7,6 +7,7 @@ import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/model/services" "code.cloudfoundry.org/korifi/tests/matchers" + "code.cloudfoundry.org/korifi/tools" "code.cloudfoundry.org/korifi/tools/k8s" . "github.com/onsi/gomega/gstruct" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -149,19 +150,57 @@ var _ = Describe("ServicePlanRepo", func() { "Type": Equal(korifiv1alpha1.AdminServicePlanVisibilityType), "Organizations": BeEmpty(), }), + "Available": BeFalse(), "ServiceOfferingGUID": Equal("offering-guid"), })) }) + + When("the visibility type is not admin", func() { + BeforeEach(func() { + cfServicePlan := &korifiv1alpha1.CFServicePlan{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: rootNamespace, + Name: planGUID, + }, + } + Expect(k8s.PatchResource(ctx, k8sClient, cfServicePlan, func() { + cfServicePlan.Spec.Visibility.Type = korifiv1alpha1.PublicServicePlanVisibilityType + })).To(Succeed()) + }) + + It("returns an available plan", func() { + Expect(plan.Available).To(BeTrue()) + }) + }) }) Describe("List", func() { var ( - listedPlans []repositories.ServicePlanRecord - message repositories.ListServicePlanMessage - listErr error + otherPlanGUID string + listedPlans []repositories.ServicePlanRecord + message repositories.ListServicePlanMessage + listErr error ) BeforeEach(func() { + otherPlanGUID = uuid.NewString() + Expect(k8sClient.Create(ctx, &korifiv1alpha1.CFServicePlan{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: rootNamespace, + Name: otherPlanGUID, + Labels: map[string]string{ + korifiv1alpha1.RelServiceOfferingLabel: "other-offering-guid", + }, + }, + Spec: korifiv1alpha1.CFServicePlanSpec{ + Visibility: korifiv1alpha1.ServicePlanVisibility{ + Type: korifiv1alpha1.PublicServicePlanVisibilityType, + }, + ServicePlan: services.ServicePlan{ + Name: "other-plan", + }, + }, + })).To(Succeed()) message = repositories.ListServicePlanMessage{} }) @@ -171,30 +210,21 @@ var _ = Describe("ServicePlanRepo", func() { It("lists service offerings", func() { Expect(listErr).NotTo(HaveOccurred()) - Expect(listedPlans).To(ConsistOf(MatchFields(IgnoreExtras, Fields{ - "CFResource": MatchFields(IgnoreExtras, Fields{ - "GUID": Equal(planGUID), + Expect(listedPlans).To(ConsistOf( + MatchFields(IgnoreExtras, Fields{ + "CFResource": MatchFields(IgnoreExtras, Fields{ + "GUID": Equal(planGUID), + }), + }), MatchFields(IgnoreExtras, Fields{ + "CFResource": MatchFields(IgnoreExtras, Fields{ + "GUID": Equal(otherPlanGUID), + }), }), - }))) + )) }) When("filtering by service_offering_guid", func() { BeforeEach(func() { - Expect(k8sClient.Create(ctx, &korifiv1alpha1.CFServicePlan{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: rootNamespace, - Name: uuid.NewString(), - Labels: map[string]string{ - korifiv1alpha1.RelServiceOfferingLabel: "other-offering-guid", - }, - }, - Spec: korifiv1alpha1.CFServicePlanSpec{ - Visibility: korifiv1alpha1.ServicePlanVisibility{ - Type: korifiv1alpha1.AdminServicePlanVisibilityType, - }, - }, - })).To(Succeed()) - message.ServiceOfferingGUIDs = []string{"other-offering-guid"} }) @@ -208,31 +238,30 @@ var _ = Describe("ServicePlanRepo", func() { When("filtering by names", func() { BeforeEach(func() { - Expect(k8sClient.Create(ctx, &korifiv1alpha1.CFServicePlan{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: rootNamespace, - Name: uuid.NewString(), - Labels: map[string]string{ - korifiv1alpha1.RelServiceOfferingLabel: "other-offering-guid", - }, - }, - Spec: korifiv1alpha1.CFServicePlanSpec{ - Visibility: korifiv1alpha1.ServicePlanVisibility{ - Type: korifiv1alpha1.AdminServicePlanVisibilityType, - }, - ServicePlan: services.ServicePlan{ - Name: "other-plan", - }, - }, - })).To(Succeed()) - message.Names = []string{"other-plan"} }) It("returns matching service plans", func() { Expect(listErr).NotTo(HaveOccurred()) Expect(listedPlans).To(ConsistOf(MatchFields(IgnoreExtras, Fields{ - "ServiceOfferingGUID": Equal("other-offering-guid"), + "ServicePlan": MatchFields(IgnoreExtras, Fields{ + "Name": Equal("other-plan"), + }), + }))) + }) + }) + + When("filtering by availability", func() { + BeforeEach(func() { + message.Available = tools.PtrTo(true) + }) + + It("returns matching service plans", func() { + Expect(listErr).NotTo(HaveOccurred()) + Expect(listedPlans).To(ConsistOf(MatchFields(IgnoreExtras, Fields{ + "CFResource": MatchFields(IgnoreExtras, Fields{ + "GUID": Equal(otherPlanGUID), + }), }))) }) }) diff --git a/tools/collections.go b/tools/collections.go index 42924960f..76161b080 100644 --- a/tools/collections.go +++ b/tools/collections.go @@ -17,3 +17,11 @@ func EmptyOrContains[S ~[]E, E comparable](elements S, e E) bool { return slices.Contains(elements, e) } + +func NilOrEquals[E comparable](value *E, expectedValue E) bool { + if value == nil { + return true + } + + return expectedValue == *value +} diff --git a/tools/collections_test.go b/tools/collections_test.go index 66d45dc89..0383cc112 100644 --- a/tools/collections_test.go +++ b/tools/collections_test.go @@ -23,4 +23,13 @@ var _ = Describe("Collections", func() { Entry("contains element", []string{"nail", "needle", "pin"}, BeTrue()), Entry("does not contain element", []string{"nail", "pin"}, BeFalse()), ) + + DescribeTable("NilOrEquals", + func(value *string, match types.GomegaMatcher) { + Expect(tools.NilOrEquals(value, "needle")).To(match) + }, + Entry("nil", nil, BeTrue()), + Entry("equal", tools.PtrTo("needle"), BeTrue()), + Entry("not-equal", tools.PtrTo("not-needle"), BeFalse()), + ) })