From a5e01b1a96267f62ea4c7f1647012cdf2fca4d41 Mon Sep 17 00:00:00 2001 From: Harry Metske Date: Thu, 7 May 2026 12:12:16 +0200 Subject: [PATCH 1/7] add route_policy resource and client --- client/route_policy.go | 96 ++++++++++++++++++++++++++++++++++++++++ resource/route_policy.go | 25 +++++++++++ 2 files changed, 121 insertions(+) create mode 100644 client/route_policy.go create mode 100644 resource/route_policy.go diff --git a/client/route_policy.go b/client/route_policy.go new file mode 100644 index 0000000..9e2420d --- /dev/null +++ b/client/route_policy.go @@ -0,0 +1,96 @@ +package client + +import ( + "context" + "net/url" + + "github.com/cloudfoundry/go-cfclient/v3/internal/path" + "github.com/cloudfoundry/go-cfclient/v3/resource" +) + +type RoutePolicyClient commonClient + +// RoutePolicyListOptions list filters +type RoutePolicyListOptions struct { + *ListOptions + + RouteGUIDs Filter `qs:"route_guids"` + SpaceGUIDs Filter `qs:"space_guids"` + SourceGUIDs Filter `qs:"source_guids"` + OrganizationGUIDs Filter `qs:"organization_guids"` +} + +// NewRoutePolicyListOptions creates new options to pass to list +func NewRoutePolicyListOptions() *RoutePolicyListOptions { + return &RoutePolicyListOptions{ + ListOptions: NewListOptions(), + } +} + +func (o RoutePolicyListOptions) ToQueryString() (url.Values, error) { + return o.ListOptions.ToQueryString(o) +} + +// Create a new route policy +func (c *RoutePolicyClient) Create(ctx context.Context, r *resource.RoutePolicyCreate) (*resource.RoutePolicy, error) { + var routePolicy resource.RoutePolicy + _, err := c.client.post(ctx, "/v3/route_policies", r, &routePolicy) + if err != nil { + return nil, err + } + return &routePolicy, nil +} + +// Delete the specified route policy asynchronously and return a jobGUID. +func (c *RoutePolicyClient) Delete(ctx context.Context, guid string) (string, error) { + return c.client.delete(ctx, path.Format("/v3/route_policies/%s", guid)) +} + +// First returns the first route policy matching the options or an error when less than 1 match +func (c *RoutePolicyClient) First(ctx context.Context, opts *RoutePolicyListOptions) (*resource.RoutePolicy, error) { + return First[*RoutePolicyListOptions, *resource.RoutePolicy](opts, func(opts *RoutePolicyListOptions) ([]*resource.RoutePolicy, *Pager, error) { + return c.List(ctx, opts) + }) +} + +// Get the specified route policy +func (c *RoutePolicyClient) Get(ctx context.Context, guid string) (*resource.RoutePolicy, error) { + var routePolicy resource.RoutePolicy + err := c.client.get(ctx, path.Format("/v3/route_policies/%s", guid), &routePolicy) + if err != nil { + return nil, err + } + return &routePolicy, nil +} + +// List pages all the route policies the user has access to +func (c *RoutePolicyClient) List(ctx context.Context, opts *RoutePolicyListOptions) ([]*resource.RoutePolicy, *Pager, error) { + if opts == nil { + opts = NewRoutePolicyListOptions() + } + + var res resource.RoutePolicyList + err := c.client.list(ctx, "/v3/route_policies", opts.ToQueryString, &res) + if err != nil { + return nil, nil, err + } + pager := NewPager(res.Pagination) + return res.Resources, pager, nil +} + +// ListAll retrieves all route policies the user has access to +func (c *RoutePolicyClient) ListAll(ctx context.Context, opts *RoutePolicyListOptions) ([]*resource.RoutePolicy, error) { + if opts == nil { + opts = NewRoutePolicyListOptions() + } + return AutoPage[*RoutePolicyListOptions, *resource.RoutePolicy](opts, func(opts *RoutePolicyListOptions) ([]*resource.RoutePolicy, *Pager, error) { + return c.List(ctx, opts) + }) +} + +// Single returns a single route policy matching the options or an error if not exactly 1 match +func (c *RoutePolicyClient) Single(ctx context.Context, opts *RoutePolicyListOptions) (*resource.RoutePolicy, error) { + return Single[*RoutePolicyListOptions, *resource.RoutePolicy](opts, func(opts *RoutePolicyListOptions) ([]*resource.RoutePolicy, *Pager, error) { + return c.List(ctx, opts) + }) +} diff --git a/resource/route_policy.go b/resource/route_policy.go new file mode 100644 index 0000000..a097d71 --- /dev/null +++ b/resource/route_policy.go @@ -0,0 +1,25 @@ +package resource + +type RoutePolicy struct { + Source string `json:"source"` + Metadata *Metadata `json:"metadata"` + Relationships RoutePolicyRelationships `json:"relationships"` + Resource `json:",inline"` +} + +type RoutePolicyCreate struct { + Relationships RoutePolicyRelationships `json:"relationships"` + Source string `json:"source"` +} + +type RoutePolicyList struct { + Pagination Pagination `json:"pagination"` + Resources []*RoutePolicy `json:"resources"` +} + +type RoutePolicyRelationships struct { + Route ToOneRelationship `json:"route"` + App ToOneRelationship `json:"app"` + Space ToOneRelationship `json:"space"` + Organization ToOneRelationship `json:"organization"` +} From 16685d955e582b87a3c5042376687866e637e2b3 Mon Sep 17 00:00:00 2001 From: Harry Metske Date: Thu, 7 May 2026 12:37:49 +0200 Subject: [PATCH 2/7] add RoutePolicyClient --- client/client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/client/client.go b/client/client.go index a3d8d3e..1d33b8f 100644 --- a/client/client.go +++ b/client/client.go @@ -46,6 +46,7 @@ type Client struct { Roles *RoleClient Root *RootClient Routes *RouteClient + RoutePolicies *RoutePolicyClient SecurityGroups *SecurityGroupClient ServiceBrokers *ServiceBrokerClient ServiceCredentialBindings *ServiceCredentialBindingClient From 836755297e200491ddc6b2d17e330f5c7b50f744 Mon Sep 17 00:00:00 2001 From: Harry Metske Date: Thu, 7 May 2026 13:35:46 +0200 Subject: [PATCH 3/7] add RoutePolicyClient in New() --- client/client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/client/client.go b/client/client.go index 1d33b8f..ac0a888 100644 --- a/client/client.go +++ b/client/client.go @@ -111,6 +111,7 @@ func New(config *config.Config) (*Client, error) { client.Roles = (*RoleClient)(&client.common) client.Root = (*RootClient)(&client.common) client.Routes = (*RouteClient)(&client.common) + client.RoutePolicies = (*RoutePolicyClient)(&client.common) client.SecurityGroups = (*SecurityGroupClient)(&client.common) client.ServiceBrokers = (*ServiceBrokerClient)(&client.common) client.ServiceCredentialBindings = (*ServiceCredentialBindingClient)(&client.common) From dbdb1c9f469fa3cab7389c9d969b41c6bb23140d Mon Sep 17 00:00:00 2001 From: Harry Metske Date: Thu, 7 May 2026 15:27:05 +0200 Subject: [PATCH 4/7] omitempty for RelationShips fields --- resource/route_policy.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resource/route_policy.go b/resource/route_policy.go index a097d71..cdbdd5f 100644 --- a/resource/route_policy.go +++ b/resource/route_policy.go @@ -18,8 +18,8 @@ type RoutePolicyList struct { } type RoutePolicyRelationships struct { - Route ToOneRelationship `json:"route"` - App ToOneRelationship `json:"app"` - Space ToOneRelationship `json:"space"` - Organization ToOneRelationship `json:"organization"` + Route ToOneRelationship `json:"route,omitempty"` + App ToOneRelationship `json:"app,omitempty"` + Space ToOneRelationship `json:"space,omitempty"` + Organization ToOneRelationship `json:"organization,omitempty"` } From 11174074ef0c38705f4509e6028237829436204a Mon Sep 17 00:00:00 2001 From: Harry Metske Date: Thu, 7 May 2026 15:31:56 +0200 Subject: [PATCH 5/7] omitempty for RelationShips fields, make them pointers --- resource/route_policy.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resource/route_policy.go b/resource/route_policy.go index cdbdd5f..93707c8 100644 --- a/resource/route_policy.go +++ b/resource/route_policy.go @@ -18,8 +18,8 @@ type RoutePolicyList struct { } type RoutePolicyRelationships struct { - Route ToOneRelationship `json:"route,omitempty"` - App ToOneRelationship `json:"app,omitempty"` - Space ToOneRelationship `json:"space,omitempty"` - Organization ToOneRelationship `json:"organization,omitempty"` + Route *ToOneRelationship `json:"route,omitempty"` + App *ToOneRelationship `json:"app,omitempty"` + Space *ToOneRelationship `json:"space,omitempty"` + Organization *ToOneRelationship `json:"organization,omitempty"` } From 19c7c55c4ed1c326ada5d217d15cc04143ed9ee8 Mon Sep 17 00:00:00 2001 From: Harry Metske Date: Fri, 8 May 2026 08:54:15 +0200 Subject: [PATCH 6/7] also provide the filter on Sources --- client/route_policy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/client/route_policy.go b/client/route_policy.go index 9e2420d..3f533b8 100644 --- a/client/route_policy.go +++ b/client/route_policy.go @@ -17,6 +17,7 @@ type RoutePolicyListOptions struct { RouteGUIDs Filter `qs:"route_guids"` SpaceGUIDs Filter `qs:"space_guids"` SourceGUIDs Filter `qs:"source_guids"` + Sources Filter `qs:"sources"` OrganizationGUIDs Filter `qs:"organization_guids"` } From b84314cbcfd82f25547102b026b4f4c484b65f9d Mon Sep 17 00:00:00 2001 From: Harry Metske Date: Fri, 8 May 2026 10:28:47 +0200 Subject: [PATCH 7/7] generated the test cases --- client/route_policy_test.go | 267 ++++++++++++++++++++++++++++ testutil/object_generator.go | 7 + testutil/template/route_policy.json | 38 ++++ 3 files changed, 312 insertions(+) create mode 100644 client/route_policy_test.go create mode 100644 testutil/template/route_policy.json diff --git a/client/route_policy_test.go b/client/route_policy_test.go new file mode 100644 index 0000000..bec9884 --- /dev/null +++ b/client/route_policy_test.go @@ -0,0 +1,267 @@ +package client + +import ( + "context" + "net/http" + "testing" + + "github.com/cloudfoundry/go-cfclient/v3/resource" + "github.com/cloudfoundry/go-cfclient/v3/testutil" +) + +func TestRoutePolicies(t *testing.T) { + g := testutil.NewObjectJSONGenerator() + routePolicy := g.RoutePolicy().JSON + routePolicy2 := g.RoutePolicy().JSON + + tests := []RouteTest{ + { + Description: "Create route policy", + Route: testutil.MockRoute{ + Method: "POST", + Endpoint: "/v3/route_policies", + Output: g.Single(routePolicy), + Status: http.StatusCreated, + PostForm: `{ + "source": "1cb006ee-fb05-47e1-b541-c34179ddc446", + "relationships": { + "route": { + "data": { "guid": "5a85c020-3e3d-42a5-a475-5084c5357e82" } + }, + "app": { + "data": { "guid": "1cb006ee-fb05-47e1-b541-c34179ddc446" } + } + } + }`, + }, + Expected: routePolicy, + Action: func(c *Client, t *testing.T) (any, error) { + r := &resource.RoutePolicyCreate{ + Source: "1cb006ee-fb05-47e1-b541-c34179ddc446", + Relationships: resource.RoutePolicyRelationships{ + Route: &resource.ToOneRelationship{ + Data: &resource.Relationship{ + GUID: "5a85c020-3e3d-42a5-a475-5084c5357e82", + }, + }, + App: &resource.ToOneRelationship{ + Data: &resource.Relationship{ + GUID: "1cb006ee-fb05-47e1-b541-c34179ddc446", + }, + }, + }, + } + return c.RoutePolicies.Create(context.Background(), r) + }, + }, + { + Description: "Delete route policy", + Route: testutil.MockRoute{ + Method: "DELETE", + Endpoint: "/v3/route_policies/c8dcf27f-39a3-466a-9cbf-1d3c31a43b93", + Status: http.StatusAccepted, + RedirectLocation: "https://api.example.org/api/v3/jobs/c33a5caf-77e0-4d6e-b587-5555d339bc9a", + }, + Expected: "c33a5caf-77e0-4d6e-b587-5555d339bc9a", + Action: func(c *Client, t *testing.T) (any, error) { + return c.RoutePolicies.Delete(context.Background(), "c8dcf27f-39a3-466a-9cbf-1d3c31a43b93") + }, + }, + { + Description: "Get route policy", + Route: testutil.MockRoute{ + Method: "GET", + Endpoint: "/v3/route_policies/c8dcf27f-39a3-466a-9cbf-1d3c31a43b93", + Output: g.Single(routePolicy), + Status: http.StatusOK, + }, + Expected: routePolicy, + Action: func(c *Client, t *testing.T) (any, error) { + return c.RoutePolicies.Get(context.Background(), "c8dcf27f-39a3-466a-9cbf-1d3c31a43b93") + }, + }, + { + Description: "List first page of route policies", + Route: testutil.MockRoute{ + Method: "GET", + Endpoint: "/v3/route_policies", + Output: g.SinglePaged(routePolicy), + Status: http.StatusOK, + }, + Expected: g.Array(routePolicy), + Action: func(c *Client, t *testing.T) (any, error) { + policies, _, err := c.RoutePolicies.List(context.Background(), NewRoutePolicyListOptions()) + return policies, err + }, + }, + { + Description: "List all route policies", + Route: testutil.MockRoute{ + Method: "GET", + Endpoint: "/v3/route_policies", + Output: g.Paged([]string{routePolicy}, []string{routePolicy2}), + Status: http.StatusOK, + }, + Expected: g.Array(routePolicy, routePolicy2), + Action: func(c *Client, t *testing.T) (any, error) { + return c.RoutePolicies.ListAll(context.Background(), nil) + }, + }, + { + Description: "First route policy with no matches", + Route: testutil.MockRoute{ + Method: "GET", + Endpoint: "/v3/route_policies", + Output: g.Paged([]string{}), + Status: http.StatusOK, + }, + Expected: "error", + Action: func(c *Client, t *testing.T) (any, error) { + _, err := c.RoutePolicies.First(context.Background(), NewRoutePolicyListOptions()) + if err != nil { + return "error", nil + } + return "no error", nil + }, + }, + { + Description: "First route policy", + Route: testutil.MockRoute{ + Method: "GET", + Endpoint: "/v3/route_policies", + Output: g.SinglePaged(routePolicy), + Status: http.StatusOK, + }, + Expected: routePolicy, + Action: func(c *Client, t *testing.T) (any, error) { + return c.RoutePolicies.First(context.Background(), NewRoutePolicyListOptions()) + }, + }, + { + Description: "Single route policy with no matches", + Route: testutil.MockRoute{ + Method: "GET", + Endpoint: "/v3/route_policies", + Output: g.Paged([]string{}), + Status: http.StatusOK, + }, + Expected: "error", + Action: func(c *Client, t *testing.T) (any, error) { + _, err := c.RoutePolicies.Single(context.Background(), NewRoutePolicyListOptions()) + if err != nil { + return "error", nil + } + return "no error", nil + }, + }, + { + Description: "Single route policy", + Route: testutil.MockRoute{ + Method: "GET", + Endpoint: "/v3/route_policies", + Output: g.SinglePaged(routePolicy), + Status: http.StatusOK, + }, + Expected: routePolicy, + Action: func(c *Client, t *testing.T) (any, error) { + return c.RoutePolicies.Single(context.Background(), NewRoutePolicyListOptions()) + }, + }, + { + Description: "List route policies with filter by route GUID", + Route: testutil.MockRoute{ + Method: "GET", + Endpoint: "/v3/route_policies", + QueryString: "page=1&per_page=50&route_guids=5a85c020-3e3d-42a5-a475-5084c5357e82", + Output: g.SinglePaged(routePolicy), + Status: http.StatusOK, + }, + Expected: g.Array(routePolicy), + Action: func(c *Client, t *testing.T) (any, error) { + opts := NewRoutePolicyListOptions() + opts.RouteGUIDs = Filter{ + Values: []string{"5a85c020-3e3d-42a5-a475-5084c5357e82"}, + } + policies, _, err := c.RoutePolicies.List(context.Background(), opts) + return policies, err + }, + }, + { + Description: "List route policies with filter by space GUID", + Route: testutil.MockRoute{ + Method: "GET", + Endpoint: "/v3/route_policies", + QueryString: "page=1&per_page=50&space_guids=33d27af8-788d-4de5-8f37-fb80d517f2ed", + Output: g.SinglePaged(routePolicy), + Status: http.StatusOK, + }, + Expected: g.Array(routePolicy), + Action: func(c *Client, t *testing.T) (any, error) { + opts := NewRoutePolicyListOptions() + opts.SpaceGUIDs = Filter{ + Values: []string{"33d27af8-788d-4de5-8f37-fb80d517f2ed"}, + } + policies, _, err := c.RoutePolicies.List(context.Background(), opts) + return policies, err + }, + }, + { + Description: "List route policies with filter by source GUID", + Route: testutil.MockRoute{ + Method: "GET", + Endpoint: "/v3/route_policies", + QueryString: "page=1&per_page=50&source_guids=1cb006ee-fb05-47e1-b541-c34179ddc446", + Output: g.SinglePaged(routePolicy), + Status: http.StatusOK, + }, + Expected: g.Array(routePolicy), + Action: func(c *Client, t *testing.T) (any, error) { + opts := NewRoutePolicyListOptions() + opts.SourceGUIDs = Filter{ + Values: []string{"1cb006ee-fb05-47e1-b541-c34179ddc446"}, + } + policies, _, err := c.RoutePolicies.List(context.Background(), opts) + return policies, err + }, + }, + { + Description: "List route policies with filter by source", + Route: testutil.MockRoute{ + Method: "GET", + Endpoint: "/v3/route_policies", + QueryString: "page=1&per_page=50&sources=network_policy", + Output: g.SinglePaged(routePolicy), + Status: http.StatusOK, + }, + Expected: g.Array(routePolicy), + Action: func(c *Client, t *testing.T) (any, error) { + opts := NewRoutePolicyListOptions() + opts.Sources = Filter{ + Values: []string{"network_policy"}, + } + policies, _, err := c.RoutePolicies.List(context.Background(), opts) + return policies, err + }, + }, + { + Description: "List route policies with filter by organization GUID", + Route: testutil.MockRoute{ + Method: "GET", + Endpoint: "/v3/route_policies", + QueryString: "organization_guids=3a5f687b-2ce8-4ade-be75-8eca99b0db8b&page=1&per_page=50", + Output: g.SinglePaged(routePolicy), + Status: http.StatusOK, + }, + Expected: g.Array(routePolicy), + Action: func(c *Client, t *testing.T) (any, error) { + opts := NewRoutePolicyListOptions() + opts.OrganizationGUIDs = Filter{ + Values: []string{"3a5f687b-2ce8-4ade-be75-8eca99b0db8b"}, + } + policies, _, err := c.RoutePolicies.List(context.Background(), opts) + return policies, err + }, + }, + } + ExecuteTests(tests, t) +} diff --git a/testutil/object_generator.go b/testutil/object_generator.go index 76baf9d..5836abe 100644 --- a/testutil/object_generator.go +++ b/testutil/object_generator.go @@ -343,6 +343,13 @@ func (o ObjectJSONGenerator) RouteDestinationWithLinks() *JSONResource { return o.renderTemplate(r, "route_destination_with_links.json") } +func (o ObjectJSONGenerator) RoutePolicy() *JSONResource { + r := &JSONResource{ + GUID: RandomGUID(), + } + return o.renderTemplate(r, "route_policy.json") +} + func (o ObjectJSONGenerator) ServiceBroker() *JSONResource { r := &JSONResource{ GUID: RandomGUID(), diff --git a/testutil/template/route_policy.json b/testutil/template/route_policy.json new file mode 100644 index 0000000..ae7ffb8 --- /dev/null +++ b/testutil/template/route_policy.json @@ -0,0 +1,38 @@ +{ + "guid": "{{.GUID}}", + "source": "1cb006ee-fb05-47e1-b541-c34179ddc446", + "created_at": "2023-01-01T00:00:00Z", + "updated_at": "2023-01-01T00:00:00Z", + "metadata": { + "labels": {}, + "annotations": {} + }, + "relationships": { + "route": { + "data": { + "guid": "5a85c020-3e3d-42a5-a475-5084c5357e82" + } + }, + "app": { + "data": { + "guid": "1cb006ee-fb05-47e1-b541-c34179ddc446" + } + }, + "space": { + "data": { + "guid": "33d27af8-788d-4de5-8f37-fb80d517f2ed" + } + }, + "organization": { + "data": { + "guid": "3a5f687b-2ce8-4ade-be75-8eca99b0db8b" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/route_policies/{{.GUID}}" + } + } +} +