From 87064a83c4f6c6ff1fccc29ecdeb8d0c224384d0 Mon Sep 17 00:00:00 2001 From: theghost5800 Date: Thu, 12 Mar 2026 18:03:52 +0200 Subject: [PATCH 1/4] Create/Update route options Refactor _RouteOptions class to handle added options in future --- .../client/v3/routes/_CreateRouteRequest.java | 7 +++ .../client/v3/routes/_RouteOptions.java | 46 +++++++++++++++++-- .../client/v3/routes/_UpdateRouteRequest.java | 7 +++ .../v3/routes/CreateRouteRequestTest.java | 34 ++++++++++++++ .../v3/routes/UpdateRouteRequestTest.java | 19 ++++++++ 5 files changed, 108 insertions(+), 5 deletions(-) diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/routes/_CreateRouteRequest.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/routes/_CreateRouteRequest.java index 7f66e5d9cf0..a0848dba77c 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/routes/_CreateRouteRequest.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/routes/_CreateRouteRequest.java @@ -27,6 +27,13 @@ abstract class _CreateRouteRequest { @JsonProperty("metadata") abstract Metadata getMetadata(); + /** + * Route options + */ + @Nullable + @JsonProperty("options") + abstract RouteOptions getOptions(); + /** * The path */ diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/routes/_RouteOptions.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/routes/_RouteOptions.java index 0c8bc8b15c3..46d00a3f87e 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/routes/_RouteOptions.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/routes/_RouteOptions.java @@ -1,8 +1,14 @@ package org.cloudfoundry.client.v3.routes; +import java.util.Map; +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import org.cloudfoundry.Nullable; +import org.cloudfoundry.AllowNulls; import org.immutables.value.Value; /** @@ -13,10 +19,40 @@ abstract class _RouteOptions { /** - * The loadbalancing + * All route options, including unknown future keys. */ - @JsonProperty("loadbalancing") - @Nullable - public abstract String getLoadbalancing(); + @JsonAnyGetter + @JsonProperty("options") + @JsonInclude(value = JsonInclude.Include.ALWAYS) + @AllowNulls + public abstract Map getValues(); + + @JsonIgnore + @Value.Derived + public Optional getLoadbalancing() { + return getString("loadbalancing"); + } + + @JsonIgnore + @Value.Derived + public Optional getHashHeader() { + return getString("hash_header"); + } + + @JsonIgnore + @Value.Derived + public Optional getHashBalance() { + return getString("hash_balance"); + } + + @JsonIgnore + public Optional get(String key) { + return Optional.ofNullable(getValues().get(key)); + } + + private Optional getString(String key) { + Object value = getValues().get(key); + return value instanceof String ? Optional.of((String) value) : Optional.empty(); + } } diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/routes/_UpdateRouteRequest.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/routes/_UpdateRouteRequest.java index e375cdeac1f..42a9f97e95a 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/routes/_UpdateRouteRequest.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/routes/_UpdateRouteRequest.java @@ -21,6 +21,13 @@ abstract class _UpdateRouteRequest { @Nullable abstract Metadata getMetadata(); + /** + * Route options + */ + @JsonProperty("options") + @Nullable + abstract RouteOptions getOptions(); + /** * The route id */ diff --git a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/routes/CreateRouteRequestTest.java b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/routes/CreateRouteRequestTest.java index 602ec57f849..dfbd5020db4 100644 --- a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/routes/CreateRouteRequestTest.java +++ b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/routes/CreateRouteRequestTest.java @@ -16,6 +16,7 @@ package org.cloudfoundry.client.v3.routes; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import org.cloudfoundry.client.v3.Relationship; @@ -94,4 +95,37 @@ void valid() { .build()) .build()); } + + @Test + void validWithRouteOptions() { + CreateRouteRequest request = CreateRouteRequest.builder() + .relationships( + RouteRelationships.builder() + .domain( + ToOneRelationship.builder() + .data( + Relationship.builder() + .id("test-domain-id") + .build()) + .build()) + .space( + ToOneRelationship.builder() + .data( + Relationship.builder() + .id("test-space-id") + .build()) + .build()) + .build()) + .options( + RouteOptions.builder() + .value("loadbalancing", "hash") + .value("hash_header", "X-Hash") + .value("hash_balance", "90") + .build()) + .build(); + + assertEquals("hash", request.getOptions().getLoadbalancing().get()); + assertEquals("X-Hash", request.getOptions().getHashHeader().get()); + assertEquals("90", request.getOptions().getHashBalance().get()); + } } diff --git a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/routes/UpdateRouteRequestTest.java b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/routes/UpdateRouteRequestTest.java index 10e0f1eb0a0..f873840b3af 100644 --- a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/routes/UpdateRouteRequestTest.java +++ b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/routes/UpdateRouteRequestTest.java @@ -16,6 +16,7 @@ package org.cloudfoundry.client.v3.routes; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import org.cloudfoundry.client.v3.Metadata; @@ -39,4 +40,22 @@ void valid() { .routeId("test-route-id") .build(); } + + @Test + void validWithRouteOptions() { + UpdateRouteRequest request = UpdateRouteRequest.builder() + .metadata(Metadata.builder().label("test-key", "test-value").build()) + .options( + RouteOptions.builder() + .value("loadbalancing", "hash") + .value("hash_header", "X-Hash") + .value("hash_balance", "90") + .build()) + .routeId("test-route-id") + .build(); + + assertEquals("hash", request.getOptions().getLoadbalancing().get()); + assertEquals("X-Hash", request.getOptions().getHashHeader().get()); + assertEquals("90", request.getOptions().getHashBalance().get()); + } } From 901e4ed2dbb90f6238700be220e75f4288a5d071 Mon Sep 17 00:00:00 2001 From: theghost5800 Date: Thu, 19 Mar 2026 10:50:58 +0200 Subject: [PATCH 2/4] Fix tests formatting --- .../v3/routes/CreateRouteRequestTest.java | 51 ++++++++++--------- .../v3/routes/UpdateRouteRequestTest.java | 21 ++++---- 2 files changed, 37 insertions(+), 35 deletions(-) diff --git a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/routes/CreateRouteRequestTest.java b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/routes/CreateRouteRequestTest.java index dfbd5020db4..0afd65f6dfd 100644 --- a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/routes/CreateRouteRequestTest.java +++ b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/routes/CreateRouteRequestTest.java @@ -98,31 +98,32 @@ void valid() { @Test void validWithRouteOptions() { - CreateRouteRequest request = CreateRouteRequest.builder() - .relationships( - RouteRelationships.builder() - .domain( - ToOneRelationship.builder() - .data( - Relationship.builder() - .id("test-domain-id") - .build()) - .build()) - .space( - ToOneRelationship.builder() - .data( - Relationship.builder() - .id("test-space-id") - .build()) - .build()) - .build()) - .options( - RouteOptions.builder() - .value("loadbalancing", "hash") - .value("hash_header", "X-Hash") - .value("hash_balance", "90") - .build()) - .build(); + CreateRouteRequest request = + CreateRouteRequest.builder() + .relationships( + RouteRelationships.builder() + .domain( + ToOneRelationship.builder() + .data( + Relationship.builder() + .id("test-domain-id") + .build()) + .build()) + .space( + ToOneRelationship.builder() + .data( + Relationship.builder() + .id("test-space-id") + .build()) + .build()) + .build()) + .options( + RouteOptions.builder() + .value("loadbalancing", "hash") + .value("hash_header", "X-Hash") + .value("hash_balance", "90") + .build()) + .build(); assertEquals("hash", request.getOptions().getLoadbalancing().get()); assertEquals("X-Hash", request.getOptions().getHashHeader().get()); diff --git a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/routes/UpdateRouteRequestTest.java b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/routes/UpdateRouteRequestTest.java index f873840b3af..c9d5a65d64b 100644 --- a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/routes/UpdateRouteRequestTest.java +++ b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/routes/UpdateRouteRequestTest.java @@ -43,16 +43,17 @@ void valid() { @Test void validWithRouteOptions() { - UpdateRouteRequest request = UpdateRouteRequest.builder() - .metadata(Metadata.builder().label("test-key", "test-value").build()) - .options( - RouteOptions.builder() - .value("loadbalancing", "hash") - .value("hash_header", "X-Hash") - .value("hash_balance", "90") - .build()) - .routeId("test-route-id") - .build(); + UpdateRouteRequest request = + UpdateRouteRequest.builder() + .metadata(Metadata.builder().label("test-key", "test-value").build()) + .options( + RouteOptions.builder() + .value("loadbalancing", "hash") + .value("hash_header", "X-Hash") + .value("hash_balance", "90") + .build()) + .routeId("test-route-id") + .build(); assertEquals("hash", request.getOptions().getLoadbalancing().get()); assertEquals("X-Hash", request.getOptions().getHashHeader().get()); From c4298c9fd698fb4945ad1205b614935c969bef9b Mon Sep 17 00:00:00 2001 From: theghost5800 Date: Thu, 21 May 2026 15:19:32 +0300 Subject: [PATCH 3/4] Switch RouteOptions to use specific fields for every parameter --- .../client/v3/routes/_RouteOptions.java | 59 +++++++------------ .../v3/routes/CreateRouteRequestTest.java | 12 ++-- .../v3/routes/UpdateRouteRequestTest.java | 12 ++-- 3 files changed, 32 insertions(+), 51 deletions(-) diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/routes/_RouteOptions.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/routes/_RouteOptions.java index 46d00a3f87e..ceeedc2a518 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/routes/_RouteOptions.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/routes/_RouteOptions.java @@ -1,14 +1,9 @@ package org.cloudfoundry.client.v3.routes; -import java.util.Map; -import java.util.Optional; - -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import org.cloudfoundry.AllowNulls; +import org.cloudfoundry.Nullable; import org.immutables.value.Value; /** @@ -19,40 +14,26 @@ abstract class _RouteOptions { /** - * All route options, including unknown future keys. + * The loadbalancing algorithm */ - @JsonAnyGetter - @JsonProperty("options") - @JsonInclude(value = JsonInclude.Include.ALWAYS) - @AllowNulls - public abstract Map getValues(); - - @JsonIgnore - @Value.Derived - public Optional getLoadbalancing() { - return getString("loadbalancing"); - } - - @JsonIgnore - @Value.Derived - public Optional getHashHeader() { - return getString("hash_header"); - } - - @JsonIgnore - @Value.Derived - public Optional getHashBalance() { - return getString("hash_balance"); - } - - @JsonIgnore - public Optional get(String key) { - return Optional.ofNullable(getValues().get(key)); - } + @JsonInclude + @JsonProperty("loadbalancing") + @Nullable + public abstract String getLoadbalancing(); - private Optional getString(String key) { - Object value = getValues().get(key); - return value instanceof String ? Optional.of((String) value) : Optional.empty(); - } + /** + * The hash header + */ + @JsonInclude + @JsonProperty("hash_header") + @Nullable + public abstract String getHashHeader(); + /** + * The hash balance + */ + @JsonInclude + @JsonProperty("hash_balance") + @Nullable + public abstract String getHashBalance(); } diff --git a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/routes/CreateRouteRequestTest.java b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/routes/CreateRouteRequestTest.java index 0afd65f6dfd..9c925b2adcd 100644 --- a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/routes/CreateRouteRequestTest.java +++ b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/routes/CreateRouteRequestTest.java @@ -119,14 +119,14 @@ void validWithRouteOptions() { .build()) .options( RouteOptions.builder() - .value("loadbalancing", "hash") - .value("hash_header", "X-Hash") - .value("hash_balance", "90") + .loadbalancing("hash") + .hashHeader("X-Hash") + .hashBalance("90") .build()) .build(); - assertEquals("hash", request.getOptions().getLoadbalancing().get()); - assertEquals("X-Hash", request.getOptions().getHashHeader().get()); - assertEquals("90", request.getOptions().getHashBalance().get()); + assertEquals("hash", request.getOptions().getLoadbalancing()); + assertEquals("X-Hash", request.getOptions().getHashHeader()); + assertEquals("90", request.getOptions().getHashBalance()); } } diff --git a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/routes/UpdateRouteRequestTest.java b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/routes/UpdateRouteRequestTest.java index c9d5a65d64b..0ca13db01e2 100644 --- a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/routes/UpdateRouteRequestTest.java +++ b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/routes/UpdateRouteRequestTest.java @@ -48,15 +48,15 @@ void validWithRouteOptions() { .metadata(Metadata.builder().label("test-key", "test-value").build()) .options( RouteOptions.builder() - .value("loadbalancing", "hash") - .value("hash_header", "X-Hash") - .value("hash_balance", "90") + .loadbalancing("hash") + .hashHeader("X-Hash") + .hashBalance("90") .build()) .routeId("test-route-id") .build(); - assertEquals("hash", request.getOptions().getLoadbalancing().get()); - assertEquals("X-Hash", request.getOptions().getHashHeader().get()); - assertEquals("90", request.getOptions().getHashBalance().get()); + assertEquals("hash", request.getOptions().getLoadbalancing()); + assertEquals("X-Hash", request.getOptions().getHashHeader()); + assertEquals("90", request.getOptions().getHashBalance()); } } From 319024f8ad6cc9bc7f4bf0e3420b6ce21f60866c Mon Sep 17 00:00:00 2001 From: theghost5800 Date: Thu, 21 May 2026 17:27:37 +0300 Subject: [PATCH 4/4] Add integration tests for route options --- .../org/cloudfoundry/CloudFoundryVersion.java | 2 + .../cloudfoundry/client/v3/RoutesTest.java | 117 ++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/integration-test/src/test/java/org/cloudfoundry/CloudFoundryVersion.java b/integration-test/src/test/java/org/cloudfoundry/CloudFoundryVersion.java index 65d7a8e38ce..dffe0465f66 100644 --- a/integration-test/src/test/java/org/cloudfoundry/CloudFoundryVersion.java +++ b/integration-test/src/test/java/org/cloudfoundry/CloudFoundryVersion.java @@ -55,6 +55,8 @@ public enum CloudFoundryVersion { PCF_2_13(Version.forIntegers(2, 186, 0)), + PCF_2_14(Version.forIntegers(2, 248, 0)), + PCF_4_v2(Version.forIntegers(2, 209, 0)), PCF_4_v3(Version.forIntegers(3, 138, 0)), diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v3/RoutesTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v3/RoutesTest.java index d3f5bf4ff64..74a2b2a2e07 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v3/RoutesTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v3/RoutesTest.java @@ -46,6 +46,7 @@ import org.cloudfoundry.client.v3.routes.RemoveRouteDestinationsRequest; import org.cloudfoundry.client.v3.routes.ReplaceRouteDestinationsRequest; import org.cloudfoundry.client.v3.routes.ReplaceRouteDestinationsResponse; +import org.cloudfoundry.client.v3.routes.RouteOptions; import org.cloudfoundry.client.v3.routes.RouteRelationships; import org.cloudfoundry.client.v3.routes.RouteResource; import org.cloudfoundry.client.v3.routes.UpdateRouteRequest; @@ -128,6 +129,72 @@ public void create() { .verify(Duration.ofMinutes(5)); } + @IfCloudFoundryVersion(greaterThanOrEqualTo = CloudFoundryVersion.PCF_2_14) + @Test + public void createWithOptions() { + String domainName = this.nameFactory.getDomainName(); + + this.organizationId + .flatMap( + organizationId -> + Mono.zip( + createDomainId( + this.cloudFoundryClient, + domainName, + organizationId), + this.spaceId)) + .flatMap( + function( + (domainId, spaceId) -> + this.cloudFoundryClient + .routesV3() + .create( + CreateRouteRequest.builder() + .metadata( + Metadata.builder() + .label( + "test-createWithOptions-key", + "test-createWithOptions-value") + .build()) + .options( + RouteOptions.builder() + .loadbalancing( + "round-robin") + .build()) + .relationships( + RouteRelationships.builder() + .domain( + ToOneRelationship + .builder() + .data( + Relationship + .builder() + .id( + domainId) + .build()) + .build()) + .space( + ToOneRelationship + .builder() + .data( + Relationship + .builder() + .id( + spaceId) + .build()) + .build()) + .build()) + .build()) + .thenReturn(domainId))) + .flatMapMany(domainId -> requestListRoutes(this.cloudFoundryClient, domainId)) + .map(RouteResource::getOptions) + .map(RouteOptions::getLoadbalancing) + .as(StepVerifier::create) + .expectNext("round-robin") + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + @IfCloudFoundryVersion(greaterThanOrEqualTo = CloudFoundryVersion.PCF_2_9) @Test public void get() { @@ -966,6 +1033,56 @@ public void update() { .verify(Duration.ofMinutes(5)); } + @IfCloudFoundryVersion(greaterThanOrEqualTo = CloudFoundryVersion.PCF_2_14) + @Test + public void updateOptions() { + String domainName = this.nameFactory.getDomainName(); + + this.organizationId + .flatMap( + organizationId -> + Mono.zip( + createDomainId( + this.cloudFoundryClient, + domainName, + organizationId), + this.spaceId)) + .flatMap( + function( + (domainId, spaceId) -> + Mono.zip( + Mono.just(domainId), + createRouteId( + this.cloudFoundryClient, + domainId, + "updateOptions", + spaceId)))) + .delayUntil( + function( + (domainId, routeId) -> + this.cloudFoundryClient + .routesV3() + .update( + UpdateRouteRequest.builder() + .routeId(routeId) + .options( + RouteOptions.builder() + .loadbalancing( + "least-connection") + .build()) + .build()))) + .flatMapMany( + function( + (domainId, ignore) -> + requestListRoutes(this.cloudFoundryClient, domainId))) + .map(RouteResource::getOptions) + .map(RouteOptions::getLoadbalancing) + .as(StepVerifier::create) + .expectNext("least-connection") + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + private static Mono createApplicationId( CloudFoundryClient cloudFoundryClient, String applicationName, String spaceId) { return requestCreateApplication(cloudFoundryClient, applicationName, spaceId)