Skip to content

Commit

Permalink
Filter service plans by available
Browse files Browse the repository at this point in the history
A plan is `available` when its visbility is not `admin`.
See
https://github.com/cloudfoundry/cloud_controller_ng/blob/d543a6668838174d99fdf54945398055dab5bc79/app/models/services/service_plan.rb#L88-L103
for reference

fixes #3278

Co-authored-by: Georgi Sabev <[email protected]>
  • Loading branch information
danail-branekov and georgethebeatle committed Aug 8, 2024
1 parent 5687ba5 commit 3853006
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 45 deletions.
25 changes: 24 additions & 1 deletion api/payloads/service_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,34 @@ 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"
)

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 {
Expand All @@ -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"`
Expand Down
19 changes: 17 additions & 2 deletions api/payloads/service_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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),
}))
})
})
Expand Down
2 changes: 2 additions & 0 deletions api/presenter/service_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Expand All @@ -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{
Expand Down
2 changes: 2 additions & 0 deletions api/presenter/service_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ var _ = Describe("Service Plan", func() {
Type: "visibility-type",
},
ServiceOfferingGUID: "service-offering-guid",
Available: true,
}
})

Expand Down Expand Up @@ -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": {
Expand Down
10 changes: 9 additions & 1 deletion api/repositories/service_plan_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type ServicePlanRecord struct {
model.CFResource
Visibility PlanVisibility
ServiceOfferingGUID string
Available bool
}

type PlanVisibility struct {
Expand All @@ -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 {
Expand Down Expand Up @@ -193,6 +200,7 @@ func (r *ServicePlanRepo) planToRecord(ctx context.Context, authInfo authorizati
Organizations: organizations,
},
ServiceOfferingGUID: plan.Labels[korifiv1alpha1.RelServiceOfferingLabel],
Available: isAvailable(plan),
}, nil
}

Expand Down
111 changes: 70 additions & 41 deletions api/repositories/service_plan_repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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{}
})

Expand All @@ -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"}
})

Expand All @@ -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),
}),
})))
})
})
Expand Down
8 changes: 8 additions & 0 deletions tools/collections.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
9 changes: 9 additions & 0 deletions tools/collections_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
)
})

0 comments on commit 3853006

Please sign in to comment.