diff --git a/.github/scripts/resolve-capi-version.sh b/.github/scripts/resolve-capi-version.sh new file mode 100755 index 0000000000..5ff2dfec33 --- /dev/null +++ b/.github/scripts/resolve-capi-version.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Prints the newest capi-release version whose CC API (v2) version equals SUPPORTED_API_VERSION +# from CloudFoundryClient.java. Needs only git and curl, no GitHub API and so no token. +# +# A capi-release tag pins cloud_controller_ng as a submodule, and that repo records its v2 API +# version in config/version_v2. The release notes aren't reliable for this. The v2 version only +# grows with the tags, so a binary search over the sorted tags is enough. +set -euo pipefail + +source_file="$(dirname "$0")/../../cloudfoundry-client/src/main/java/org/cloudfoundry/client/CloudFoundryClient.java" +target="$(sed -n 's/.*String SUPPORTED_API_VERSION = "\([0-9.]*\)";.*/\1/p' "$source_file")" +[ -n "$target" ] || { echo "SUPPORTED_API_VERSION not found in $source_file" >&2; exit 1; } + +workdir="$(mktemp -d)" +trap 'rm -rf "$workdir"' EXIT +# commits and trees only, which is all it takes to read the submodule commit of a tag +git clone --quiet --bare --filter=blob:none https://github.com/cloudfoundry/capi-release.git "$workdir/capi-release" + +mapfile -t tags < <(git -C "$workdir/capi-release" tag -l | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V) + +v2_version() { + local sha + sha="$(git -C "$workdir/capi-release" ls-tree "$1" src/cloud_controller_ng | awk '{print $3}')" + curl -fsSL --retry 3 "https://raw.githubusercontent.com/cloudfoundry/cloud_controller_ng/$sha/config/version_v2" | tr -d '[:space:]' +} + +# highest index whose v2 version is <= target +lo=0 +hi=$((${#tags[@]} - 1)) +found=-1 +while [ "$lo" -le "$hi" ]; do + mid=$(((lo + hi) / 2)) + v="$(v2_version "${tags[$mid]}")" + if [ "$(printf '%s\n%s\n' "$v" "$target" | sort -V | tail -1)" = "$target" ]; then + found=$mid + lo=$((mid + 1)) + else + hi=$((mid - 1)) + fi +done + +if [ "$found" -lt 0 ] || [ "$(v2_version "${tags[$found]}")" != "$target" ]; then + echo "No capi-release found for CC API version $target" >&2 + exit 1 +fi + +echo "CC API $target -> capi-release ${tags[$found]}" >&2 +echo "${tags[$found]}" diff --git a/.github/workflows/ci-java.yml b/.github/workflows/ci-java.yml index b1cf12b2a4..3d05984149 100644 --- a/.github/workflows/ci-java.yml +++ b/.github/workflows/ci-java.yml @@ -41,3 +41,9 @@ jobs: name: Check style with Spotless run: ./mvnw spotless:check -Pintegration-test + integration-test: + needs: build + uses: ./.github/workflows/integration-test.yml + with: + java-versions: '[21]' + diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml new file mode 100644 index 0000000000..b703c62b45 --- /dev/null +++ b/.github/workflows/integration-test.yml @@ -0,0 +1,125 @@ +name: Integration Tests + +on: + workflow_dispatch: + inputs: + java-versions: + description: 'JSON array of Java versions to test' + type: string + required: false + default: '[8, 11, 17, 21]' + preview: + description: 'Deploy the latest cf-deployment versions instead of the default ones' + type: boolean + required: false + default: false + schedule: + - cron: '0 3 * * 1-5' + workflow_call: + inputs: + java-versions: + description: 'JSON array of Java versions to test' + type: string + required: false + default: '[8, 11, 17, 21]' + preview: + description: 'Deploy the latest cf-deployment versions instead of the default ones' + type: boolean + required: false + default: false + +permissions: + contents: read + +# A new push supersedes the running test. The group name differs from the caller's workflow name on +# purpose: a called workflow shares github.workflow with its caller, which would cancel the caller. +concurrency: + group: integration-test-${{ github.event_name == 'schedule' && 'schedule' || github.ref }} + cancel-in-progress: true + +env: + CF_API_HOST: api.cf.127-0-0-1.nip.io + CF_UAA_HOST: uaa.cf.127-0-0-1.nip.io + +jobs: + integration-test: + if: ${{ github.event_name != 'schedule' || github.repository == 'cloudfoundry/cf-java-client' }} + runs-on: ubuntu-latest + timeout-minutes: 120 + strategy: + fail-fast: false + matrix: + java: ${{ fromJSON(inputs.java-versions || (github.event_name == 'schedule' && '[21]' || '[8, 11, 17, 21]')) }} + name: Java ${{ matrix.java }} integration test${{ (inputs.preview || github.event_name == 'schedule') && ' (preview)' || '' }} + steps: + - uses: actions/checkout@v7 + with: + submodules: recursive + # Validation runs deploy the capi-release that matches SUPPORTED_API_VERSION. Preview runs + # (scheduled, or started by hand with `preview`) keep the latest versions. + - name: Resolve CAPI version + id: capi + if: ${{ !(inputs.preview || github.event_name == 'schedule') }} + run: | + # separate assignment, so that a failing resolver fails the step + version="$(.github/scripts/resolve-capi-version.sh)" + echo "version=$version" >> "$GITHUB_OUTPUT" + # All components share one Postgres, which runs out of its default 100 connections during a full + # test run. Written first, the CAPI override below appends to the same file. + - name: Write PostgreSQL values override + run: | + cat > "$RUNNER_TEMP/cf-values.yaml" <> "$RUNNER_TEMP/cf-values.yaml" < Version.valueOf(response.getApiVersion())) - .zipWith(Mono.just(Version.valueOf(CloudFoundryClient.SUPPORTED_API_VERSION))) + Mono> v2 = checkV2(); + v2.switchIfEmpty(checkV3()) .subscribe( consumer( (server, supported) -> @@ -51,11 +54,41 @@ void check() { t)); } + private Mono> checkV2() { + return this.info + .get(GetInfoRequest.builder().build()) + .flatMap( + response -> { + String version = response.getApiVersion(); + if (version == null || version.isEmpty()) { + if ("CF API v2 is disabled".equals(response.getSupport())) { + this.logger.warn( + "calling v2 info endpoint but CF API v2 is disabled", + response); + } + return Mono.empty(); + } else { + return Mono.just(Version.valueOf(version)); + } + }) + .zipWith(Mono.just(Version.valueOf(CloudFoundryClient.SUPPORTED_API_VERSION))); + } + + private Mono> checkV3() { + return this.root + .get(GetRootRequest.builder().build()) + .map( + response -> { + String versionV3 = response.getApiVersionV3(); + return Version.valueOf(versionV3); + }) + .zipWith(Mono.just(Version.valueOf(CloudFoundryClient.SUPPORTED_API_VERSION_V3))); + } + private static void logCompatibility(Version server, Version supported, Logger logger) { String message = "Client supports API version {} and is connected to server with API version {}." + " Things may not work as expected."; - if (server.greaterThan(supported)) { logger.info(message, supported, server); } else if (server.lessThan(supported)) { diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/ReactorRoot.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/ReactorRoot.java new file mode 100644 index 0000000000..7cf690307a --- /dev/null +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/ReactorRoot.java @@ -0,0 +1,53 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.reactor.client; + +import java.util.Map; +import org.cloudfoundry.client.GetRootRequest; +import org.cloudfoundry.client.GetRootResponse; +import org.cloudfoundry.client.Root; +import org.cloudfoundry.reactor.ConnectionContext; +import org.cloudfoundry.reactor.TokenProvider; +import org.cloudfoundry.reactor.client.v3.AbstractClientV3Operations; +import reactor.core.publisher.Mono; + +/** + * The Reactor-based implementation of {@link Root} + */ +public class ReactorRoot extends AbstractClientV3Operations implements Root { + + /** + * Creates an instance + * + * @param connectionContext the {@link ConnectionContext} to use when communicating with the server + * @param root the root URI of the server. Typically something like {@code https://api.run.pivotal.io}. + * @param tokenProvider the {@link TokenProvider} to use when communicating with the server + * @param requestTags map with custom http headers which will be added to web request + */ + public ReactorRoot( + ConnectionContext connectionContext, + Mono root, + TokenProvider tokenProvider, + Map requestTags) { + super(connectionContext, root, tokenProvider, requestTags); + } + + @Override + public Mono get(GetRootRequest request) { + return get(request, GetRootResponse.class, builder -> builder.pathSegment("")).checkpoint(); + } +} diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/_ReactorCloudFoundryClient.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/_ReactorCloudFoundryClient.java index 80e5ef2614..6fe80b2d96 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/_ReactorCloudFoundryClient.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/_ReactorCloudFoundryClient.java @@ -18,6 +18,7 @@ import jakarta.annotation.PostConstruct; import org.cloudfoundry.client.CloudFoundryClient; +import org.cloudfoundry.client.Root; import org.cloudfoundry.client.v2.applications.ApplicationsV2; import org.cloudfoundry.client.v2.applicationusageevents.ApplicationUsageEvents; import org.cloudfoundry.client.v2.blobstores.Blobstores; @@ -72,6 +73,7 @@ import org.cloudfoundry.client.v3.serviceinstances.ServiceInstancesV3; import org.cloudfoundry.client.v3.serviceofferings.ServiceOfferingsV3; import org.cloudfoundry.client.v3.serviceplans.ServicePlansV3; +import org.cloudfoundry.client.v3.serviceroutebindings.ServiceRouteBindingsV3; import org.cloudfoundry.client.v3.quotas.spaces.SpaceQuotasV3; import org.cloudfoundry.client.v3.spaces.SpacesV3; import org.cloudfoundry.client.v3.stacks.StacksV3; @@ -133,6 +135,7 @@ import org.cloudfoundry.reactor.client.v3.serviceinstances.ReactorServiceInstancesV3; import org.cloudfoundry.reactor.client.v3.serviceofferings.ReactorServiceOfferingsV3; import org.cloudfoundry.reactor.client.v3.serviceplans.ReactorServicePlansV3; +import org.cloudfoundry.reactor.client.v3.serviceroutebindings.ReactorServiceRouteBindingsV3; import org.cloudfoundry.reactor.client.v3.quotas.spaces.ReactorSpaceQuotasV3; import org.cloudfoundry.reactor.client.v3.spaces.ReactorSpacesV3; import org.cloudfoundry.reactor.client.v3.stacks.ReactorStacksV3; @@ -207,7 +210,7 @@ public Builds builds() { @PostConstruct public void checkCompatibility() { - new CloudFoundryClientCompatibilityChecker(info()).check(); + new CloudFoundryClientCompatibilityChecker(info(), rootEndpoint()).check(); } @Override @@ -259,6 +262,13 @@ public Info info() { return new ReactorInfo(getConnectionContext(), getRootV2(), getTokenProvider(), getRequestTags()); } + @Override + @Value.Derived + public Root rootEndpoint() { + Mono root = getConnectionContext().getRootProvider().getRoot(getConnectionContext()); + return new ReactorRoot(getConnectionContext(), root, getTokenProvider(), getRequestTags()); + } + @Override @Value.Derived public IsolationSegments isolationSegments() { @@ -546,4 +556,9 @@ Mono getRootV3() { */ abstract TokenProvider getTokenProvider(); + @Override + @Value.Derived + public ServiceRouteBindingsV3 serviceRouteBindingsV3() { + return new ReactorServiceRouteBindingsV3(getConnectionContext(), getRootV3(), getTokenProvider(), getRequestTags()); + } } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/applications/ReactorApplicationsV2.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/applications/ReactorApplicationsV2.java index 0e87bba174..36292061de 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/applications/ReactorApplicationsV2.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/applications/ReactorApplicationsV2.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collections; import java.util.Map; import org.cloudfoundry.client.v2.applications.ApplicationEnvironmentRequest; import org.cloudfoundry.client.v2.applications.ApplicationEnvironmentResponse; @@ -66,6 +67,7 @@ import org.cloudfoundry.reactor.client.v2.AbstractClientV2Operations; import org.cloudfoundry.reactor.util.MultipartHttpClientRequest; import org.cloudfoundry.util.FileUtils; +import org.springframework.web.util.UriComponentsBuilder; import reactor.core.Exceptions; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -356,13 +358,25 @@ private Mono upload( return put( request, UploadApplicationResponse.class, - builder -> builder.pathSegment("apps", request.getApplicationId(), "bits"), + builder -> uploadUri(builder, request), multipartRequest -> upload(request.getApplication(), multipartRequest, request), onTerminate) .checkpoint(); } + // The nginx upload module of CAPI can drop the "resources" form field ("missing :resources"). + // Cloud Controller also takes it as query parameter, but only an empty list fits into a URL. + private static UriComponentsBuilder uploadUri( + UriComponentsBuilder builder, UploadApplicationRequest request) { + builder.pathSegment("apps", request.getApplicationId(), "bits"); + if (request.getResources().isEmpty()) { + builder.queryParam("resources", "{resources}") + .uriVariables(Collections.singletonMap("resources", "[]")); + } + return builder; + } + private void upload( Path application, MultipartHttpClientRequest multipartRequest, diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/packages/ReactorPackages.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/packages/ReactorPackages.java index c28ea0d1a4..01279e990b 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/packages/ReactorPackages.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/packages/ReactorPackages.java @@ -16,9 +16,12 @@ package org.cloudfoundry.reactor.client.v3.packages; +import static io.netty.handler.codec.http.HttpHeaderValues.APPLICATION_JSON; + import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collections; import java.util.List; import java.util.Map; import org.cloudfoundry.client.v3.packages.CopyPackageRequest; @@ -169,9 +172,12 @@ private void upload(Path bits, List resources, MultipartHttpCli r.addPart(part -> part.setName("bits").setContentType(APPLICATION_ZIP).sendFile(bits)); } - if (resources != null && !resources.isEmpty()) { - r.addPart(part -> part.setName("resources").send(resources)); - } + // CAPI rejects bits uploads without a "resources" part (CF-AppBitsUploadInvalid) + r.addPart( + part -> + part.setName("resources") + .setContentType(APPLICATION_JSON.toString()) + .send(resources == null ? Collections.emptyList() : resources)); r.done(); } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/serviceroutebindings/ReactorServiceRouteBindingsV3.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/serviceroutebindings/ReactorServiceRouteBindingsV3.java new file mode 100644 index 0000000000..38a2a15296 --- /dev/null +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/serviceroutebindings/ReactorServiceRouteBindingsV3.java @@ -0,0 +1,142 @@ +/* + * Copyright 2013-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.reactor.client.v3.serviceroutebindings; + +import java.util.Map; +import java.util.Optional; +import org.cloudfoundry.client.v3.serviceroutebindings.CreateServiceRouteBindingRequest; +import org.cloudfoundry.client.v3.serviceroutebindings.CreateServiceRouteBindingResponse; +import org.cloudfoundry.client.v3.serviceroutebindings.DeleteServiceRouteBindingRequest; +import org.cloudfoundry.client.v3.serviceroutebindings.DeleteServiceRouteBindingResponse; +import org.cloudfoundry.client.v3.serviceroutebindings.GetServiceRouteBindingParametersRequest; +import org.cloudfoundry.client.v3.serviceroutebindings.GetServiceRouteBindingParametersResponse; +import org.cloudfoundry.client.v3.serviceroutebindings.GetServiceRouteBindingRequest; +import org.cloudfoundry.client.v3.serviceroutebindings.GetServiceRouteBindingResponse; +import org.cloudfoundry.client.v3.serviceroutebindings.ListServiceRouteBindingsRequest; +import org.cloudfoundry.client.v3.serviceroutebindings.ListServiceRouteBindingsResponse; +import org.cloudfoundry.client.v3.serviceroutebindings.ServiceRouteBindingResource; +import org.cloudfoundry.client.v3.serviceroutebindings.ServiceRouteBindingsV3; +import org.cloudfoundry.client.v3.serviceroutebindings.UpdateServiceRouteBindingRequest; +import org.cloudfoundry.client.v3.serviceroutebindings.UpdateServiceRouteBindingResponse; +import org.cloudfoundry.reactor.ConnectionContext; +import org.cloudfoundry.reactor.TokenProvider; +import org.cloudfoundry.reactor.client.v3.AbstractClientV3Operations; +import reactor.core.publisher.Mono; + +/** + * The Reactor-based implementation of {@link ServiceRouteBindingsV3} + */ +public final class ReactorServiceRouteBindingsV3 extends AbstractClientV3Operations + implements ServiceRouteBindingsV3 { + + public ReactorServiceRouteBindingsV3( + ConnectionContext connectionContext, + Mono root, + TokenProvider tokenProvider, + Map requestTags) { + super(connectionContext, root, tokenProvider, requestTags); + } + + @Override + public Mono create( + CreateServiceRouteBindingRequest request) { + return postWithResponse( + request, + ServiceRouteBindingResource.class, + builder -> builder.pathSegment("service_route_bindings")) + .map( + responseTuple -> + CreateServiceRouteBindingResponse.builder() + .serviceRouteBinding(responseTuple.getBody()) + .jobId( + Optional.ofNullable( + extractJobId(responseTuple.getResponse()))) + .build()) + .checkpoint(); + } + + @Override + public Mono delete( + DeleteServiceRouteBindingRequest request) { + return createOperator() + .flatMap( + operator -> + operator.delete() + .uri( + builder -> + builder.pathSegment( + "service_route_bindings", + request.getServiceRouteBindingId())) + .send(request) + .response() + .get()) + .map( + response -> + DeleteServiceRouteBindingResponse.builder() + .jobId(Optional.ofNullable(extractJobId(response))) + .build()) + .checkpoint(); + } + + @Override + public Mono get(GetServiceRouteBindingRequest request) { + return get( + request, + GetServiceRouteBindingResponse.class, + builder -> + builder.pathSegment( + "service_route_bindings", + request.getServiceRouteBindingId())) + .checkpoint(); + } + + @Override + public Mono getParameters( + GetServiceRouteBindingParametersRequest request) { + return get( + request, + GetServiceRouteBindingParametersResponse.class, + builder -> + builder.pathSegment( + "service_route_bindings", + request.getServiceRouteBindingId(), + "parameters")) + .checkpoint(); + } + + @Override + public Mono list(ListServiceRouteBindingsRequest request) { + return get( + request, + ListServiceRouteBindingsResponse.class, + builder -> builder.pathSegment("service_route_bindings")) + .checkpoint(); + } + + @Override + public Mono update( + UpdateServiceRouteBindingRequest request) { + return patch( + request, + UpdateServiceRouteBindingResponse.class, + builder -> + builder.pathSegment( + "service_route_bindings", + request.getServiceRouteBindingId())) + .checkpoint(); + } +} diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/authorizations/ReactorAuthorizations.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/authorizations/ReactorAuthorizations.java index f929b35dda..833248a430 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/authorizations/ReactorAuthorizations.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/authorizations/ReactorAuthorizations.java @@ -18,6 +18,8 @@ import static io.netty.handler.codec.http.HttpHeaderNames.AUTHORIZATION; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpHeaders; import io.netty.util.AsciiString; import java.util.Map; @@ -99,7 +101,10 @@ public Mono authorizationCodeGrantBrowser( builder -> builder.pathSegment("oauth", "authorize") .queryParam("response_type", ResponseType.CODE), - outbound -> {}, + outbound -> + outbound.set( + HttpHeaderNames.ACCEPT, + HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED), ReactorAuthorizations::removeAuthorization) .map(inbound -> inbound.responseHeaders().get(LOCATION)) .checkpoint(); @@ -114,7 +119,10 @@ public Mono authorizationCodeGrantHybrid( builder.pathSegment("oauth", "authorize") .queryParam( "response_type", ResponseType.CODE_AND_ID_TOKEN), - outbound -> {}, + outbound -> + outbound.set( + HttpHeaderNames.ACCEPT, + HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED), ReactorAuthorizations::removeAuthorization) .map(inbound -> inbound.responseHeaders().get(LOCATION)) .checkpoint(); @@ -126,7 +134,8 @@ public Mono getOpenIdProviderConfigurati return get( request, GetOpenIdProviderConfigurationResponse.class, - builder -> builder.pathSegment(".well-known", "openid-configuration")) + builder -> builder.pathSegment(".well-known", "openid-configuration"), + ReactorAuthorizations::removeAuthorization) .checkpoint(); } @@ -137,7 +146,10 @@ public Mono implicitGrantBrowser(AuthorizeByImplicitGrantBrowserRequest builder -> builder.pathSegment("oauth", "authorize") .queryParam("response_type", ResponseType.TOKEN), - outbound -> {}, + outbound -> + outbound.set( + HttpHeaderNames.ACCEPT, + HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED), ReactorAuthorizations::removeAuthorization) .map(inbound -> inbound.responseHeaders().get(LOCATION)) .checkpoint(); @@ -152,7 +164,10 @@ public Mono openIdWithAuthorizationCodeAndIdToken( builder.pathSegment("oauth", "authorize") .queryParam( "response_type", ResponseType.CODE_AND_ID_TOKEN), - outbound -> {}, + outbound -> + outbound.set( + HttpHeaderNames.ACCEPT, + HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED), ReactorAuthorizations::removeAuthorization) .map(inbound -> inbound.responseHeaders().get(LOCATION)) .checkpoint(); @@ -164,7 +179,12 @@ public Mono openIdWithIdToken(AuthorizeByOpenIdWithIdTokenRequest reques request, builder -> builder.pathSegment("oauth", "authorize") - .queryParam("response_type", ResponseType.ID_TOKEN)) + .queryParam("response_type", ResponseType.ID_TOKEN), + outbound -> + outbound.set( + HttpHeaderNames.ACCEPT, + HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED), + ReactorAuthorizations::removeAuthorization) .map(inbound -> inbound.responseHeaders().get(LOCATION)) .checkpoint(); } @@ -177,7 +197,11 @@ public Mono openIdWithTokenAndIdToken( builder -> builder.pathSegment("oauth", "authorize") .queryParam( - "response_type", ResponseType.TOKEN_AND_ID_TOKEN)) + "response_type", ResponseType.TOKEN_AND_ID_TOKEN), + outbound -> + outbound.set( + HttpHeaderNames.ACCEPT, + HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED)) .map(inbound -> inbound.responseHeaders().get(LOCATION)) .checkpoint(); } diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/applications/ReactorApplicationsV2Test.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/applications/ReactorApplicationsV2Test.java index c7730693f2..70c8875600 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/applications/ReactorApplicationsV2Test.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/applications/ReactorApplicationsV2Test.java @@ -1362,6 +1362,16 @@ void upload() throws IOException { .verify(Duration.ofSeconds(5)); } + @Test + void uploadWithoutResourcesSendsResourcesQueryParameter() throws IOException { + uploadWithoutResources("/apps/test-application-id/bits?resources=%5B%5D", null); + } + + @Test + void uploadAsyncWithoutResourcesSendsResourcesQueryParameter() throws IOException { + uploadWithoutResources("/apps/test-application-id/bits?async=true&resources=%5B%5D", true); + } + @Test void uploadDroplet() throws IOException { mockRequest( @@ -1438,4 +1448,49 @@ void uploadDroplet() throws IOException { .expectComplete() .verify(Duration.ofSeconds(5)); } + + private void uploadWithoutResources(String path, Boolean async) throws IOException { + mockRequest( + InteractionContext.builder() + .request( + TestRequest.builder() + .method(PUT) + .path(path) + .contents( + consumer( + (headers, body) -> + assertThat( + body.readString( + Charset + .defaultCharset())) + .contains( + "name=\"application\"") + .contains( + "name=\"resources\"") + .contains("[]"))) + .build()) + .response( + TestResponse.builder() + .status(CREATED) + .payload( + "fixtures/client/v2/apps/PUT_{id}_bits_response.json") + .build()) + .build()); + + this.applications + .upload( + UploadApplicationRequest.builder() + .application( + new ClassPathResource( + "fixtures/client/v2/apps/test-application.zip") + .getFile() + .toPath()) + .applicationId("test-application-id") + .async(async) + .build()) + .as(StepVerifier::create) + .expectNextCount(1) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } } diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/packages/ReactorPackagesTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/packages/ReactorPackagesTest.java index 6ac5f2792c..cb3bf411c1 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/packages/ReactorPackagesTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/packages/ReactorPackagesTest.java @@ -764,6 +764,20 @@ void upload() throws IOException { + "test-content\r\n" + "--" + boundary + + "\r\n" + + "content-disposition:" + + " form-data;" + + " name=\"resources\"\r\n" + + "content-length:" + + " 2\r\n" + + "content-type:" + + " application/json\r\n" + + "content-transfer-encoding:" + + " binary\r\n" + + "\r\n" + + "[]\r\n" + + "--" + + boundary + "--\r\n"); })) .build()) diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/serviceroutebindings/ReactorServiceRouteBindingsV3Test.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/serviceroutebindings/ReactorServiceRouteBindingsV3Test.java new file mode 100644 index 0000000000..5e507c2aeb --- /dev/null +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/serviceroutebindings/ReactorServiceRouteBindingsV3Test.java @@ -0,0 +1,371 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.reactor.client.v3.serviceroutebindings; + +import static io.netty.handler.codec.http.HttpMethod.DELETE; +import static io.netty.handler.codec.http.HttpMethod.GET; +import static io.netty.handler.codec.http.HttpMethod.PATCH; +import static io.netty.handler.codec.http.HttpMethod.POST; +import static io.netty.handler.codec.http.HttpResponseStatus.ACCEPTED; +import static io.netty.handler.codec.http.HttpResponseStatus.NO_CONTENT; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; + +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.cloudfoundry.client.v3.LastOperation; +import org.cloudfoundry.client.v3.Link; +import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.client.v3.Pagination; +import org.cloudfoundry.client.v3.Relationship; +import org.cloudfoundry.client.v3.ToOneRelationship; +import org.cloudfoundry.client.v3.serviceroutebindings.CreateServiceRouteBindingRequest; +import org.cloudfoundry.client.v3.serviceroutebindings.CreateServiceRouteBindingResponse; +import org.cloudfoundry.client.v3.serviceroutebindings.DeleteServiceRouteBindingRequest; +import org.cloudfoundry.client.v3.serviceroutebindings.DeleteServiceRouteBindingResponse; +import org.cloudfoundry.client.v3.serviceroutebindings.GetServiceRouteBindingParametersRequest; +import org.cloudfoundry.client.v3.serviceroutebindings.GetServiceRouteBindingParametersResponse; +import org.cloudfoundry.client.v3.serviceroutebindings.GetServiceRouteBindingRequest; +import org.cloudfoundry.client.v3.serviceroutebindings.GetServiceRouteBindingResponse; +import org.cloudfoundry.client.v3.serviceroutebindings.ListServiceRouteBindingsRequest; +import org.cloudfoundry.client.v3.serviceroutebindings.ListServiceRouteBindingsResponse; +import org.cloudfoundry.client.v3.serviceroutebindings.ServiceRouteBindingRelationships; +import org.cloudfoundry.client.v3.serviceroutebindings.ServiceRouteBindingResource; +import org.cloudfoundry.client.v3.serviceroutebindings.UpdateServiceRouteBindingRequest; +import org.cloudfoundry.client.v3.serviceroutebindings.UpdateServiceRouteBindingResponse; +import org.cloudfoundry.reactor.InteractionContext; +import org.cloudfoundry.reactor.TestRequest; +import org.cloudfoundry.reactor.TestResponse; +import org.cloudfoundry.reactor.client.AbstractClientApiTest; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +final class ReactorServiceRouteBindingsV3Test extends AbstractClientApiTest { + + private static final String ROUTE_ID = "7304bc3c-7010-11ea-8840-48bf6bec2d78"; + private static final String SERVICE_INSTANCE_ID_1 = "e0e4417c-74ee-11ea-a604-48bf6bec2d78"; + private static final String SERVICE_INSTANCE_ID_2 = "f0a63e60-74ee-11ea-a604-48bf6bec2d78"; + private static final String BINDING_ID_1 = "dde5ad2a-d8f4-44dc-a56f-0452d744f1c3"; + private static final String BINDING_ID_2 = "7aa37bad-6ccb-4ef9-ba48-9ce3a91b2b62"; + private static final String CREATED_AT = "2020-03-10T15:49:29Z"; + private static final String UPDATED_AT = "2020-03-10T15:49:32Z"; + + private final ReactorServiceRouteBindingsV3 serviceRouteBindings = + new ReactorServiceRouteBindingsV3( + CONNECTION_CONTEXT, this.root, TOKEN_PROVIDER, Collections.emptyMap()); + + @Test + void create() { + mockRequest( + InteractionContext.builder() + .request( + TestRequest.builder() + .method(POST) + .path("/service_route_bindings") + .payload( + "fixtures/client/v3/serviceroutebindings/POST_request.json") + .build()) + .response( + TestResponse.builder() + .status(ACCEPTED) + .header( + "Location", + "https://api.example.org/v3/jobs/af5c57f6-8769-41fa-a499-2c84ed896788") + .build()) + .build()); + + this.serviceRouteBindings + .create( + CreateServiceRouteBindingRequest.builder() + .relationships(relationships(SERVICE_INSTANCE_ID_1)) + .build()) + .as(StepVerifier::create) + .expectNext( + CreateServiceRouteBindingResponse.builder() + .jobId("af5c57f6-8769-41fa-a499-2c84ed896788") + .build()) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @Test + void delete() { + mockRequest( + InteractionContext.builder() + .request( + TestRequest.builder() + .method(DELETE) + .path( + "/service_route_bindings/test-service-route-binding-id") + .build()) + .response(TestResponse.builder().status(NO_CONTENT).build()) + .build()); + + this.serviceRouteBindings + .delete( + DeleteServiceRouteBindingRequest.builder() + .serviceRouteBindingId("test-service-route-binding-id") + .build()) + .as(StepVerifier::create) + .expectNext(DeleteServiceRouteBindingResponse.builder().build()) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @Test + void get() { + mockRequest( + InteractionContext.builder() + .request( + TestRequest.builder() + .method(GET) + .path( + "/service_route_bindings/test-service-route-binding-id") + .build()) + .response( + TestResponse.builder() + .status(OK) + .payload( + "fixtures/client/v3/serviceroutebindings/GET_{id}_response.json") + .build()) + .build()); + + this.serviceRouteBindings + .get( + GetServiceRouteBindingRequest.builder() + .serviceRouteBindingId("test-service-route-binding-id") + .build()) + .as(StepVerifier::create) + .expectNext( + GetServiceRouteBindingResponse.builder() + .id(BINDING_ID_1) + .createdAt(CREATED_AT) + .updatedAt(UPDATED_AT) + .lastOperation(lastOperation("create")) + .relationships(relationships(SERVICE_INSTANCE_ID_1)) + .link("self", link(bindingHref(BINDING_ID_1))) + .link( + "service_instance", + link(serviceInstanceHref(SERVICE_INSTANCE_ID_1))) + .link("route", link(routeHref())) + .link("parameters", link(parametersHref(BINDING_ID_1))) + .metadata(emptyMetadata()) + .build()) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @Test + void getParameters() { + Map parameters = new HashMap<>(); + parameters.put("key1", "value1"); + parameters.put("key2", "value2"); + + mockRequest( + InteractionContext.builder() + .request( + TestRequest.builder() + .method(GET) + .path( + "/service_route_bindings/test-service-route-binding-id/parameters") + .build()) + .response( + TestResponse.builder() + .status(OK) + .payload( + "fixtures/client/v3/serviceroutebindings/GET_{id}_parameters_response.json") + .build()) + .build()); + + this.serviceRouteBindings + .getParameters( + GetServiceRouteBindingParametersRequest.builder() + .serviceRouteBindingId("test-service-route-binding-id") + .build()) + .as(StepVerifier::create) + .expectNext( + GetServiceRouteBindingParametersResponse.builder() + .parameters(parameters) + .build()) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @Test + void list() { + mockRequest( + InteractionContext.builder() + .request( + TestRequest.builder() + .method(GET) + .path( + "/service_route_bindings?route_guids=7304bc3c-7010-11ea-8840-48bf6bec2d78&order_by=%2Bcreated_at&page=1") + .build()) + .response( + TestResponse.builder() + .status(OK) + .payload( + "fixtures/client/v3/serviceroutebindings/GET_response.json") + .build()) + .build()); + + this.serviceRouteBindings + .list( + ListServiceRouteBindingsRequest.builder() + .page(1) + .orderBy("+created_at") + .routeId(ROUTE_ID) + .build()) + .as(StepVerifier::create) + .expectNext( + ListServiceRouteBindingsResponse.builder() + .pagination( + Pagination.builder() + .totalResults(2) + .first( + link( + "https://api.example.org/v3/service_route_bindings?page=1&per_page=50")) + .last( + link( + "https://api.example.org/v3/service_route_bindings?page=1&per_page=50")) + .build()) + .resource( + bindingResource( + BINDING_ID_1, SERVICE_INSTANCE_ID_1, "create")) + .resource( + bindingResource( + BINDING_ID_2, SERVICE_INSTANCE_ID_2, "create")) + .build()) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @Test + void update() { + mockRequest( + InteractionContext.builder() + .request( + TestRequest.builder() + .method(PATCH) + .path( + "/service_route_bindings/test-service-route-binding-id") + .payload( + "fixtures/client/v3/serviceroutebindings/PATCH_{id}_request.json") + .build()) + .response( + TestResponse.builder() + .status(OK) + .payload( + "fixtures/client/v3/serviceroutebindings/PATCH_{id}_response.json") + .build()) + .build()); + + this.serviceRouteBindings + .update( + UpdateServiceRouteBindingRequest.builder() + .serviceRouteBindingId("test-service-route-binding-id") + .metadata(metadataWithLabel("env", "production")) + .build()) + .as(StepVerifier::create) + .expectNext( + UpdateServiceRouteBindingResponse.builder() + .id(BINDING_ID_1) + .createdAt(CREATED_AT) + .updatedAt(UPDATED_AT) + .lastOperation(lastOperation("update")) + .relationships(relationships(SERVICE_INSTANCE_ID_1)) + .link("self", link(bindingHref(BINDING_ID_1))) + .link( + "service_instance", + link(serviceInstanceHref(SERVICE_INSTANCE_ID_1))) + .link("route", link(routeHref())) + .link("parameters", link(parametersHref(BINDING_ID_1))) + .metadata(metadataWithLabel("env", "production")) + .build()) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + private static LastOperation lastOperation(String type) { + return LastOperation.builder() + .type(type) + .state("succeeded") + .description("Operation succeeded") + .createdAt(CREATED_AT) + .updatedAt(UPDATED_AT) + .build(); + } + + private static ToOneRelationship toOneRelationship(String id) { + return ToOneRelationship.builder().data(Relationship.builder().id(id).build()).build(); + } + + private static ServiceRouteBindingRelationships relationships(String serviceInstanceId) { + return ServiceRouteBindingRelationships.builder() + .route(toOneRelationship(ROUTE_ID)) + .serviceInstance(toOneRelationship(serviceInstanceId)) + .build(); + } + + private static Link link(String href) { + return Link.builder().href(href).build(); + } + + private static String bindingHref(String bindingId) { + return "https://api.example.org/v3/service_route_bindings/" + bindingId; + } + + private static String serviceInstanceHref(String serviceInstanceId) { + return "https://api.example.org/v3/service_instances/" + serviceInstanceId; + } + + private static String routeHref() { + return "https://api.example.org/v3/routes/" + ROUTE_ID; + } + + private static String parametersHref(String bindingId) { + return "https://api.example.org/v3/service_route_bindings/" + bindingId + "/parameters"; + } + + private static Metadata emptyMetadata() { + return Metadata.builder() + .labels(Collections.emptyMap()) + .annotations(Collections.emptyMap()) + .build(); + } + + private static Metadata metadataWithLabel(String key, String value) { + return Metadata.builder().label(key, value).annotations(Collections.emptyMap()).build(); + } + + private static ServiceRouteBindingResource bindingResource( + String bindingId, String serviceInstanceId, String lastOperationType) { + return ServiceRouteBindingResource.builder() + .id(bindingId) + .createdAt(CREATED_AT) + .updatedAt(UPDATED_AT) + .lastOperation(lastOperation(lastOperationType)) + .relationships(relationships(serviceInstanceId)) + .link("self", link(bindingHref(bindingId))) + .link("service_instance", link(serviceInstanceHref(serviceInstanceId))) + .link("route", link(routeHref())) + .link("parameters", link(parametersHref(bindingId))) + .metadata(emptyMetadata()) + .build(); + } +} diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/clients/ReactorClientsTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/clients/ReactorClientsTest.java index a9d730008a..ce8d31b5a2 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/clients/ReactorClientsTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/clients/ReactorClientsTest.java @@ -133,6 +133,7 @@ void batchChangeSecret() { .resourceId("none") .scopes("clients.read", "clients.write") .tokenSalt("uHICvG") + .allowPublic(true) .build()) .client( Client.builder() @@ -155,6 +156,7 @@ void batchChangeSecret() { .resourceId("none") .scopes("clients.read", "clients.write") .tokenSalt("WjlWvu") + .allowPublic(true) .build()) .build()) .expectComplete() @@ -279,6 +281,7 @@ void batchDelete() { .client( Client.builder() .approvalsDeleted(true) + .allowPublic(true) .allowedProviders("uaa", "ldap", "my-saml-provider") .authorities("clients.read", "clients.write") .authorizedGrantType(CLIENT_CREDENTIALS) @@ -296,6 +299,7 @@ void batchDelete() { .client( Client.builder() .approvalsDeleted(true) + .allowPublic(true) .allowedProviders("uaa", "ldap", "my-saml-provider") .authorities("clients.read", "clients.write") .authorizedGrantType(CLIENT_CREDENTIALS) @@ -469,6 +473,7 @@ void create() { .authorities("clients.read", "clients.write") .authorizedGrantType(CLIENT_CREDENTIALS) .autoApprove("true") + .allowPublic(true) .clientId("aPq3I1") .clientSecret("secret") .name("My Client Name") @@ -494,6 +499,7 @@ void create() { .resourceId("none") .scopes("clients.read", "clients.write") .tokenSalt("hRZ21X") + .allowPublic(true) .build()) .expectComplete() .verify(Duration.ofSeconds(5)); @@ -819,6 +825,7 @@ void mixedActions() { .refreshTokenValidity(7000L) .scopes("clients.read", "clients.write") .tokenSalt("UpzrHR") + .allowPublic(true) .build()) .build()) .as(StepVerifier::create) @@ -852,6 +859,7 @@ void mixedActions() { .resourceId("none") .scopes("clients.read", "clients.write") .tokenSalt("WjlWvu") + .allowPublic(true) .build()) .client( ActionClient.builder() @@ -871,6 +879,7 @@ void mixedActions() { .resourceId("none") .scopes("clients.read", "clients.write") .tokenSalt("UpzrHR") + .allowPublic(true) .build()) .build()) .expectComplete() @@ -901,6 +910,7 @@ void update() { .autoApprove("clients.autoapprove") .clientId("55pTMX") .scopes("clients.new", "clients.autoapprove") + .allowPublic(true) .build()) .as(StepVerifier::create) .expectNext( @@ -918,6 +928,7 @@ void update() { .resourceId("none") .scopes("clients.new", "clients.autoapprove") .tokenSalt("8mwCEy") + .allowPublic(true) .build()) .expectComplete() .verify(Duration.ofSeconds(5)); diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/serviceroutebindings/GET_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/serviceroutebindings/GET_response.json new file mode 100644 index 0000000000..b1e91ee93e --- /dev/null +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/serviceroutebindings/GET_response.json @@ -0,0 +1,99 @@ +{ + "pagination": { + "total_results": 2, + "first": { + "href": "https://api.example.org/v3/service_route_bindings?page=1&per_page=50" + }, + "last": { + "href": "https://api.example.org/v3/service_route_bindings?page=1&per_page=50" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "dde5ad2a-d8f4-44dc-a56f-0452d744f1c3", + "created_at": "2020-03-10T15:49:29Z", + "updated_at": "2020-03-10T15:49:32Z", + "last_operation": { + "type": "create", + "state": "succeeded", + "description": "Operation succeeded", + "created_at": "2020-03-10T15:49:29Z", + "updated_at": "2020-03-10T15:49:32Z" + }, + "relationships": { + "route": { + "data": { + "guid": "7304bc3c-7010-11ea-8840-48bf6bec2d78" + } + }, + "service_instance": { + "data": { + "guid": "e0e4417c-74ee-11ea-a604-48bf6bec2d78" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/service_route_bindings/dde5ad2a-d8f4-44dc-a56f-0452d744f1c3" + }, + "service_instance": { + "href": "https://api.example.org/v3/service_instances/e0e4417c-74ee-11ea-a604-48bf6bec2d78" + }, + "route": { + "href": "https://api.example.org/v3/routes/7304bc3c-7010-11ea-8840-48bf6bec2d78" + }, + "parameters": { + "href": "https://api.example.org/v3/service_route_bindings/dde5ad2a-d8f4-44dc-a56f-0452d744f1c3/parameters" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } + }, + { + "guid": "7aa37bad-6ccb-4ef9-ba48-9ce3a91b2b62", + "created_at": "2020-03-10T15:49:29Z", + "updated_at": "2020-03-10T15:49:32Z", + "last_operation": { + "type": "create", + "state": "succeeded", + "description": "Operation succeeded", + "created_at": "2020-03-10T15:49:29Z", + "updated_at": "2020-03-10T15:49:32Z" + }, + "relationships": { + "route": { + "data": { + "guid": "7304bc3c-7010-11ea-8840-48bf6bec2d78" + } + }, + "service_instance": { + "data": { + "guid": "f0a63e60-74ee-11ea-a604-48bf6bec2d78" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/service_route_bindings/7aa37bad-6ccb-4ef9-ba48-9ce3a91b2b62" + }, + "service_instance": { + "href": "https://api.example.org/v3/service_instances/f0a63e60-74ee-11ea-a604-48bf6bec2d78" + }, + "route": { + "href": "https://api.example.org/v3/routes/7304bc3c-7010-11ea-8840-48bf6bec2d78" + }, + "parameters": { + "href": "https://api.example.org/v3/service_route_bindings/7aa37bad-6ccb-4ef9-ba48-9ce3a91b2b62/parameters" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } + } + ] +} diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/serviceroutebindings/GET_{id}_parameters_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/serviceroutebindings/GET_{id}_parameters_response.json new file mode 100644 index 0000000000..7afc33d536 --- /dev/null +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/serviceroutebindings/GET_{id}_parameters_response.json @@ -0,0 +1,4 @@ +{ + "key1": "value1", + "key2": "value2" +} diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/serviceroutebindings/GET_{id}_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/serviceroutebindings/GET_{id}_response.json new file mode 100644 index 0000000000..82ddbb78b0 --- /dev/null +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/serviceroutebindings/GET_{id}_response.json @@ -0,0 +1,42 @@ +{ + "guid": "dde5ad2a-d8f4-44dc-a56f-0452d744f1c3", + "created_at": "2020-03-10T15:49:29Z", + "updated_at": "2020-03-10T15:49:32Z", + "last_operation": { + "type": "create", + "state": "succeeded", + "description": "Operation succeeded", + "created_at": "2020-03-10T15:49:29Z", + "updated_at": "2020-03-10T15:49:32Z" + }, + "relationships": { + "route": { + "data": { + "guid": "7304bc3c-7010-11ea-8840-48bf6bec2d78" + } + }, + "service_instance": { + "data": { + "guid": "e0e4417c-74ee-11ea-a604-48bf6bec2d78" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/service_route_bindings/dde5ad2a-d8f4-44dc-a56f-0452d744f1c3" + }, + "service_instance": { + "href": "https://api.example.org/v3/service_instances/e0e4417c-74ee-11ea-a604-48bf6bec2d78" + }, + "route": { + "href": "https://api.example.org/v3/routes/7304bc3c-7010-11ea-8840-48bf6bec2d78" + }, + "parameters": { + "href": "https://api.example.org/v3/service_route_bindings/dde5ad2a-d8f4-44dc-a56f-0452d744f1c3/parameters" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } +} diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/serviceroutebindings/PATCH_{id}_request.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/serviceroutebindings/PATCH_{id}_request.json new file mode 100644 index 0000000000..229ea92c51 --- /dev/null +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/serviceroutebindings/PATCH_{id}_request.json @@ -0,0 +1,6 @@ +{ + "metadata": { + "labels": {"env": "production"}, + "annotations": {} + } +} diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/serviceroutebindings/PATCH_{id}_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/serviceroutebindings/PATCH_{id}_response.json new file mode 100644 index 0000000000..b8f4266b7c --- /dev/null +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/serviceroutebindings/PATCH_{id}_response.json @@ -0,0 +1,44 @@ +{ + "guid": "dde5ad2a-d8f4-44dc-a56f-0452d744f1c3", + "created_at": "2020-03-10T15:49:29Z", + "updated_at": "2020-03-10T15:49:32Z", + "last_operation": { + "type": "update", + "state": "succeeded", + "description": "Operation succeeded", + "created_at": "2020-03-10T15:49:29Z", + "updated_at": "2020-03-10T15:49:32Z" + }, + "relationships": { + "route": { + "data": { + "guid": "7304bc3c-7010-11ea-8840-48bf6bec2d78" + } + }, + "service_instance": { + "data": { + "guid": "e0e4417c-74ee-11ea-a604-48bf6bec2d78" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/service_route_bindings/dde5ad2a-d8f4-44dc-a56f-0452d744f1c3" + }, + "service_instance": { + "href": "https://api.example.org/v3/service_instances/e0e4417c-74ee-11ea-a604-48bf6bec2d78" + }, + "route": { + "href": "https://api.example.org/v3/routes/7304bc3c-7010-11ea-8840-48bf6bec2d78" + }, + "parameters": { + "href": "https://api.example.org/v3/service_route_bindings/dde5ad2a-d8f4-44dc-a56f-0452d744f1c3/parameters" + } + }, + "metadata": { + "labels": { + "env": "production" + }, + "annotations": {} + } +} diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/serviceroutebindings/POST_request.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/serviceroutebindings/POST_request.json new file mode 100644 index 0000000000..9796cc3138 --- /dev/null +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/serviceroutebindings/POST_request.json @@ -0,0 +1,14 @@ +{ + "relationships": { + "route": { + "data": { + "guid": "7304bc3c-7010-11ea-8840-48bf6bec2d78" + } + }, + "service_instance": { + "data": { + "guid": "e0e4417c-74ee-11ea-a604-48bf6bec2d78" + } + } + } +} diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/POST_request.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/POST_request.json index cd24223fe7..ff9cb074b9 100644 --- a/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/POST_request.json +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/POST_request.json @@ -25,5 +25,6 @@ "clients.read", "clients.write" ], - "token_salt": "hRZ21X" + "token_salt": "hRZ21X", + "allowpublic": true } \ No newline at end of file diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/POST_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/POST_response.json index bc91157c3b..d61bedfc92 100644 --- a/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/POST_response.json +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/POST_response.json @@ -28,5 +28,6 @@ "my-saml-provider" ], "name": "My Client Name", + "allowpublic": true, "lastModified": 1468364445109 } \ No newline at end of file diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/POST_tx_delete_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/POST_tx_delete_response.json index 4b113ed45a..84fa404dd1 100644 --- a/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/POST_tx_delete_response.json +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/POST_tx_delete_response.json @@ -30,7 +30,8 @@ ], "name": "My Client Name", "lastModified": 1468364444461, - "approvals_deleted": true + "approvals_deleted": true, + "allowpublic": true }, { "scope": [ @@ -63,6 +64,7 @@ ], "name": "My Client Name", "lastModified": 1468364444868, - "approvals_deleted": true + "approvals_deleted": true, + "allowpublic": true } ] \ No newline at end of file diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/POST_tx_modify_request.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/POST_tx_modify_request.json index c25d438d83..976b638b7f 100644 --- a/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/POST_tx_modify_request.json +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/POST_tx_modify_request.json @@ -39,6 +39,7 @@ "redirect_uri": [ "http://test1.com", "http*://ant.path.wildcard/**/passback/*" - ] + ], + "allowpublic": true } ] diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/POST_tx_modify_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/POST_tx_modify_response.json index a0bed0ad73..a3fe946d72 100644 --- a/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/POST_tx_modify_response.json +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/POST_tx_modify_response.json @@ -43,7 +43,8 @@ ], "name": "My Client Name", "lastModified": 1474923482302, - "approvals_deleted": true + "approvals_deleted": true, + "allowpublic": true }, { "scope": [ @@ -78,6 +79,7 @@ "my-saml-provider" ], "name": "My Client Name", + "allowpublic": true, "lastModified": 1474923482727 } ] \ No newline at end of file diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/POST_tx_secret_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/POST_tx_secret_response.json index dcfd19a32a..66cdbf957a 100644 --- a/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/POST_tx_secret_response.json +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/POST_tx_secret_response.json @@ -32,7 +32,8 @@ ], "name": "My Client Name", "lastModified": 1474923482301, - "approvals_deleted": true + "approvals_deleted": true, + "allowpublic": true }, { "scope": [ @@ -68,6 +69,7 @@ ], "name": "My Client Name", "lastModified": 1474923482302, - "approvals_deleted": true + "approvals_deleted": true, + "allowpublic": true } ] \ No newline at end of file diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/PUT_{id}_request.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/PUT_{id}_request.json index d3f59d11cc..6d48b51404 100644 --- a/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/PUT_{id}_request.json +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/PUT_{id}_request.json @@ -9,5 +9,6 @@ ], "autoapprove": [ "clients.autoapprove" - ] + ], + "allowpublic": true } diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/PUT_{id}_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/PUT_{id}_response.json index 64704ce173..3f5e88a93f 100644 --- a/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/PUT_{id}_response.json +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/PUT_{id}_response.json @@ -28,5 +28,6 @@ "my-saml-provider" ], "name": "My Client Name", + "allowpublic": true, "lastModified": 1468364443857 } diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/CloudFoundryClient.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/CloudFoundryClient.java index 0f85e973e5..ad6d1f0df5 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/CloudFoundryClient.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/CloudFoundryClient.java @@ -71,6 +71,7 @@ import org.cloudfoundry.client.v3.serviceinstances.ServiceInstancesV3; import org.cloudfoundry.client.v3.serviceofferings.ServiceOfferingsV3; import org.cloudfoundry.client.v3.serviceplans.ServicePlansV3; +import org.cloudfoundry.client.v3.serviceroutebindings.ServiceRouteBindingsV3; import org.cloudfoundry.client.v3.spaces.SpacesV3; import org.cloudfoundry.client.v3.stacks.StacksV3; import org.cloudfoundry.client.v3.tasks.Tasks; @@ -84,7 +85,9 @@ public interface CloudFoundryClient { /** * The currently supported Cloud Controller API version */ - String SUPPORTED_API_VERSION = "2.272.0"; + String SUPPORTED_API_VERSION = "2.291.0"; + + String SUPPORTED_API_VERSION_V3 = "3.225.0"; /** * Main entry point to the Cloud Foundry Application Usage Events Client API @@ -171,6 +174,11 @@ public interface CloudFoundryClient { */ Info info(); + /** + * Main entry point to the Cloud Foundry root endpoint + */ + Root rootEndpoint(); + /** * Main entry point to the Cloud Foundry Isolation Segments API */ @@ -381,4 +389,10 @@ public interface CloudFoundryClient { * Main entry point to the Cloud Foundry Users V3 Client API */ UsersV3 usersV3(); + + /** + * Main entry point to the Cloud Foundry Service Route Bindings V3 Client API + * See https://v3-apidocs.cloudfoundry.org/index.html#service-route-binding + */ + ServiceRouteBindingsV3 serviceRouteBindingsV3(); } diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/Root.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/Root.java new file mode 100644 index 0000000000..37c59a4d7e --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/Root.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client; + +import reactor.core.publisher.Mono; + +/** + * Main entry point to the Cloud Foundry RootV3 Client API + */ +public interface Root { + + /** + * Makes the Get root request + * + * @param request the Get Root request + * @return the response from the Get Root request + */ + Mono get(GetRootRequest request); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/_GetRootRequest.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/_GetRootRequest.java new file mode 100644 index 0000000000..a0aab3e5f1 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/_GetRootRequest.java @@ -0,0 +1,27 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client; + +import org.immutables.value.Value; + +/** + * The request payload for the Get root "/" operation + */ +@Value.Immutable +abstract class _GetRootRequest { + +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/_GetRootResponse.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/_GetRootResponse.java new file mode 100644 index 0000000000..f56a890a5c --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/_GetRootResponse.java @@ -0,0 +1,218 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client; + +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +import java.io.IOException; + +import org.cloudfoundry.Nullable; +import org.immutables.value.Value; + +/** + * The response payload for the get root operation. See V3 API Root + */ +@JsonDeserialize(using = org.cloudfoundry.client._GetRootResponse.RootDeserializer.class) +@Value.Immutable() +public interface _GetRootResponse { + + /** + * The root of the cloud controller Api version 3 + */ + @Value.Parameter + @Nullable + public abstract String getApiV3(); + + /** + * The version of the cloud controller Api version 3 + */ + @Value.Parameter + @Nullable + public abstract String getApiVersionV3(); + + /** + * The root of the cloud controller Api version 2 + */ + @Value.Parameter + @Nullable + public abstract String getApiV2(); + + /** + * The version of the cloud controller Api version 2 (if available) + */ + @Value.Parameter + @Nullable + public abstract String getApiVersion(); + + /** + * The network policy v0 endpoint + */ + @Value.Parameter + @Nullable + public abstract String getNetworkPolicyV0Endpoint(); + + /** + * The network policy v1 endpoint + */ + @Value.Parameter + @Nullable + public abstract String getNetworkPolicyV1Endpoint(); + + /** + * The login endpoint + */ + @Value.Parameter + @Nullable + public abstract String getLoginEndpoint(); + + /** + * The uaa endpoint + */ + @Value.Parameter + @Nullable + public abstract String getUaaEndpoint(); + + /** + * The credhub endpoint + */ + @Value.Parameter + @Nullable + public abstract String getCredhubEndpoint(); + + /** + * The routing endpoint + */ + @Value.Parameter + @Nullable + public abstract String getRoutingEndpoint(); + + /** + * The loggin encpoint + */ + @Value.Parameter + @Nullable + public abstract String getLoggingEndpoint(); + + /** + * The log cache url + */ + @Value.Parameter + @Nullable + public abstract String getLogCacheEndpoint(); + + /** + * The log stream url + */ + @Value.Parameter + @Nullable + public abstract String getLogStreamEndpoint(); + + /** + * The ssh endpoint for apps. + */ + @Value.Parameter + @Nullable + public abstract String getAppSshEndpoint(); + + /** + * The ssh host key fingerprint for apps. + */ + @Value.Parameter + @Nullable + public abstract String getAppSshHostKeyFingerprint(); + + /** + * The ssh oauth client for apps. + */ + @Value.Parameter + @Nullable + public abstract String getAppSshOauthClient(); + + /** + * The self url + */ + @Value.Parameter + @Nullable + public abstract String getSelf(); + + public class RootDeserializer extends StdDeserializer<_GetRootResponse>{ + private static final long serialVersionUID = 1L; + + protected RootDeserializer() { + super(GetRootResponse.class); + } + @Override + public _GetRootResponse deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException, JacksonException { + JsonNode productNode = jp.getCodec().readTree(jp); + String apiV3Endpoint = getEndpoint("cloud_controller_v3",productNode); + + JsonNode tmp = productNode.get("links").get("cloud_controller_v3"); + String apiVersionV3 = null; + if(tmp!=null) { + apiVersionV3 = tmp.get("meta").get("version").textValue(); + } + + String apiV2Endpoint = getEndpoint("cloud_controller_v2",productNode); + tmp = productNode.get("links").get("cloud_controller_v2"); + String apiVersion = null; + if(tmp!=null) { + apiVersion = tmp.get("meta").get("version").textValue(); + } + String networkPolicyV0Endpoint = getEndpoint("network_policy_v0",productNode); + String networkPolicyV1Endpoint = getEndpoint("network_policy_v1",productNode); + String loginEndpoint = getEndpoint("login",productNode); + String uaaEndpoint = getEndpoint("uaa",productNode); + String credhubEndpoint = getEndpoint("credhub",productNode); + String routingEndpoint = getEndpoint("routing",productNode); + String loggingEndpoint = getEndpoint("logging",productNode); + String logCacheEndpoint = getEndpoint("log_cache",productNode); + String logStreamEndpoint = getEndpoint("log_stream",productNode); + String appSshEndpoint = getEndpoint("app_ssh",productNode); + tmp = productNode.get("links").get("app_ssh"); + String appSshHostKeyFingerprint = null; + if(tmp!=null) { + appSshHostKeyFingerprint = tmp.get("meta").get("host_key_fingerprint").textValue(); + } + tmp = productNode.get("links").get("app_ssh"); + String appSshOauthClient = null; + if(tmp!=null) { + appSshOauthClient = tmp.get("meta").get("oauth_client").textValue(); + } + + String self = getEndpoint("self",productNode); + return GetRootResponse.of(apiV3Endpoint,apiVersionV3,apiV2Endpoint, + apiVersion,networkPolicyV0Endpoint, networkPolicyV1Endpoint, loginEndpoint,uaaEndpoint,credhubEndpoint, + routingEndpoint,loggingEndpoint, logCacheEndpoint, logStreamEndpoint,appSshEndpoint, appSshHostKeyFingerprint, appSshOauthClient,self); + } + + // null safe access to href-endpoints + private String getEndpoint(String name,JsonNode productNode) { + String result = null; + JsonNode tmp = productNode.get("links").get(name); + if(tmp!=null&& !tmp.isNull()) { + result = tmp.get("href").textValue(); + } + return result; + } + } +} 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 7f66e5d9cf..a0848dba77 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 0c8bc8b15c..772e34b06f 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,5 +1,6 @@ package org.cloudfoundry.client.v3.routes; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import org.cloudfoundry.Nullable; @@ -13,10 +14,23 @@ abstract class _RouteOptions { /** - * The loadbalancing + * The loadbalancing algorithm */ @JsonProperty("loadbalancing") @Nullable public abstract String getLoadbalancing(); + /** + * The hash header + */ + @JsonProperty("hash_header") + @Nullable + public abstract String getHashHeader(); + + /** + * The hash balance + */ + @JsonProperty("hash_balance") + @Nullable + public abstract String getHashBalance(); } 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 e375cdeac1..42a9f97e95 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/main/java/org/cloudfoundry/client/v3/serviceroutebindings/ServiceRouteBinding.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/ServiceRouteBinding.java new file mode 100644 index 0000000000..01f1ac35e1 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/ServiceRouteBinding.java @@ -0,0 +1,58 @@ +/* + * Copyright 2013-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client.v3.serviceroutebindings; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.cloudfoundry.Nullable; +import org.cloudfoundry.client.v3.LastOperation; +import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.client.v3.Resource; + +/** + * Base class for responses that are service route bindings + * + * see https://v3-apidocs.cloudfoundry.org/index.html#service-route-binding + */ +public abstract class ServiceRouteBinding extends Resource { + + /** + * The last operation + */ + @JsonProperty("last_operation") + @Nullable + public abstract LastOperation getLastOperation(); + + /** + * The metadata + */ + @JsonProperty("metadata") + @Nullable + public abstract Metadata getMetadata(); + + /** + * The relationships + */ + @JsonProperty("relationships") + public abstract ServiceRouteBindingRelationships getRelationships(); + + /** + * The URL of the service that will intercept traffic to the route + */ + @JsonProperty("route_service_url") + @Nullable + public abstract String getRouteServiceUrl(); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/ServiceRouteBindingsV3.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/ServiceRouteBindingsV3.java new file mode 100644 index 0000000000..cd1c1055ea --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/ServiceRouteBindingsV3.java @@ -0,0 +1,76 @@ +/* + * Copyright 2013-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client.v3.serviceroutebindings; + +import reactor.core.publisher.Mono; + +/** + * Main entry point to the Cloud Foundry Service Route Bindings V3 Client API + * + * see https://v3-apidocs.cloudfoundry.org/index.html#service-route-binding + */ +public interface ServiceRouteBindingsV3 { + + /** + * Makes the Create a service route binding request + * + * @param request the Create Service Route Binding request + * @return the response from the Create Service Route Binding request + */ + Mono create(CreateServiceRouteBindingRequest request); + + /** + * Makes the Delete a service route binding request + * + * @param request the Delete Service Route Binding request + * @return the response from the Delete Service Route Binding request + */ + Mono delete(DeleteServiceRouteBindingRequest request); + + /** + * Makes the Get a service route binding request + * + * @param request the Get Service Route Binding request + * @return the response from the Get Service Route Binding request + */ + Mono get(GetServiceRouteBindingRequest request); + + /** + * Makes the Get parameters for a service route binding request + * + * @param request the Get Service Route Binding Parameters request + * @return the response from the Get Service Route Binding Parameters request + */ + Mono getParameters( + GetServiceRouteBindingParametersRequest request); + + /** + * Makes the List service route bindings request + * + * @param request the List Service Route Bindings request + * @return the response from the List Service Route Bindings request + */ + Mono list(ListServiceRouteBindingsRequest request); + + /** + * Makes the Update a service route binding request + * + * @param request the Update Service Route Binding request + * @return the response from the Update Service Route Binding request + */ + Mono update(UpdateServiceRouteBindingRequest request); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_CreateServiceRouteBindingRequest.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_CreateServiceRouteBindingRequest.java new file mode 100644 index 0000000000..d77297d2a2 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_CreateServiceRouteBindingRequest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client.v3.serviceroutebindings; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.cloudfoundry.AllowNulls; +import org.cloudfoundry.Nullable; +import org.cloudfoundry.client.v3.Metadata; +import org.immutables.value.Value; + +import java.util.Map; + +/** + * The request payload for the Create Service Route Binding operation. + * see https://v3-apidocs.cloudfoundry.org/index.html#create-a-service-route-binding + */ +@JsonSerialize +@Value.Immutable +abstract class _CreateServiceRouteBindingRequest { + + /** + * The metadata + */ + @JsonProperty("metadata") + @Nullable + abstract Metadata getMetadata(); + + /** + * The parameters + */ + @JsonProperty("parameters") + @Nullable + @AllowNulls + abstract Map getParameters(); + + /** + * The relationships + */ + @JsonProperty("relationships") + abstract ServiceRouteBindingRelationships getRelationships(); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_CreateServiceRouteBindingResponse.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_CreateServiceRouteBindingResponse.java new file mode 100644 index 0000000000..d4cf5a98d4 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_CreateServiceRouteBindingResponse.java @@ -0,0 +1,39 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client.v3.serviceroutebindings; + +import org.immutables.value.Value; + +import java.util.Optional; + +/** + * The response payload for the Create Service Route Binding operation. + * see https://v3-apidocs.cloudfoundry.org/#create-a-service-credential-binding + */ +@Value.Immutable +abstract class _CreateServiceRouteBindingResponse { + + /** + * The job id (present when the binding is created asynchronously, managed service instance) + */ + public abstract Optional getJobId(); + + /** + * The service route binding (present when the binding is created synchronously, user provided service instance) + */ + public abstract Optional getServiceRouteBinding(); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_DeleteServiceRouteBindingRequest.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_DeleteServiceRouteBindingRequest.java new file mode 100644 index 0000000000..cafb901573 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_DeleteServiceRouteBindingRequest.java @@ -0,0 +1,32 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client.v3.serviceroutebindings; + +import org.immutables.value.Value; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * The request payload for the Delete Service Route Binding operation. + * see https://v3-apidocs.cloudfoundry.org/index.html#delete-a-service-route-binding + */ +@Value.Immutable +abstract class _DeleteServiceRouteBindingRequest { + + @JsonIgnore + abstract String getServiceRouteBindingId(); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_DeleteServiceRouteBindingResponse.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_DeleteServiceRouteBindingResponse.java new file mode 100644 index 0000000000..809cc27c3a --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_DeleteServiceRouteBindingResponse.java @@ -0,0 +1,34 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client.v3.serviceroutebindings; + +import org.immutables.value.Value; + +import java.util.Optional; + +/** + * The response payload for the Delete Service Route Binding operation. + * see https://v3-apidocs.cloudfoundry.org/index.html#delete-a-service-route-binding + */ +@Value.Immutable +abstract class _DeleteServiceRouteBindingResponse { + + /** + * The job id (present when the binding is deleted asynchronously, managed service instance) + */ + public abstract Optional getJobId(); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_GetServiceRouteBindingParametersRequest.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_GetServiceRouteBindingParametersRequest.java new file mode 100644 index 0000000000..e23402562f --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_GetServiceRouteBindingParametersRequest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client.v3.serviceroutebindings; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.immutables.value.Value; + +/** + * The request payload for the Get Service Route Binding Parameters operation + * + * see https://v3-apidocs.cloudfoundry.org/index.html#get-parameters-for-a-route-binding + */ +@Value.Immutable +abstract class _GetServiceRouteBindingParametersRequest { + + /** + * The service route binding id + */ + @JsonIgnore + abstract String getServiceRouteBindingId(); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_GetServiceRouteBindingParametersResponse.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_GetServiceRouteBindingParametersResponse.java new file mode 100644 index 0000000000..2832398a8f --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_GetServiceRouteBindingParametersResponse.java @@ -0,0 +1,65 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client.v3.serviceroutebindings; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import org.cloudfoundry.AllowNulls; +import org.immutables.value.Value; + +import java.io.IOException; +import java.util.Map; + +/** + * The response payload for the Get Service Route Binding Parameters operation + * + * see https://v3-apidocs.cloudfoundry.org/index.html#get-parameters-for-a-route-binding + */ +@JsonDeserialize( + using = + _GetServiceRouteBindingParametersResponse + .ServiceRouteBindingParametersResponseDeserializer.class) +@Value.Immutable +abstract class _GetServiceRouteBindingParametersResponse { + + /** + * The parameters + */ + @AllowNulls + abstract Map getParameters(); + + static final class ServiceRouteBindingParametersResponseDeserializer + extends StdDeserializer { + + private static final long serialVersionUID = 1L; + + ServiceRouteBindingParametersResponseDeserializer() { + super(GetServiceRouteBindingParametersResponse.class); + } + + @Override + public GetServiceRouteBindingParametersResponse deserialize( + JsonParser p, DeserializationContext ctxt) throws IOException { + return GetServiceRouteBindingParametersResponse.builder() + .parameters(p.readValueAs(new TypeReference>() {})) + .build(); + } + } +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_GetServiceRouteBindingRequest.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_GetServiceRouteBindingRequest.java new file mode 100644 index 0000000000..166c33bb91 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_GetServiceRouteBindingRequest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client.v3.serviceroutebindings; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.immutables.value.Value; + +/** + * The request payload for the Get Service Route Binding operation. + * + * see https://v3-apidocs.cloudfoundry.org/index.html#get-a-service-route-binding + */ +@Value.Immutable +abstract class _GetServiceRouteBindingRequest { + + /** + * The service route binding id + */ + @JsonIgnore + abstract String getServiceRouteBindingId(); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_GetServiceRouteBindingResponse.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_GetServiceRouteBindingResponse.java new file mode 100644 index 0000000000..8465e2fb60 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_GetServiceRouteBindingResponse.java @@ -0,0 +1,31 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client.v3.serviceroutebindings; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.immutables.value.Value; + +/** + * The response payload for the Get Service Route Binding operation + * + * see https://v3-apidocs.cloudfoundry.org/index.html#get-a-service-route-binding + */ +@JsonDeserialize +@Value.Immutable +abstract class _GetServiceRouteBindingResponse extends ServiceRouteBinding { + +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_ListServiceRouteBindingsRequest.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_ListServiceRouteBindingsRequest.java new file mode 100644 index 0000000000..bafe9e1196 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_ListServiceRouteBindingsRequest.java @@ -0,0 +1,64 @@ +/* + * Copyright 2013-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client.v3.serviceroutebindings; + +import org.cloudfoundry.Nullable; +import org.cloudfoundry.client.v3.FilterParameter; +import org.cloudfoundry.client.v3.PaginatedRequest; +import org.immutables.value.Value; + +import java.util.List; + +/** + * The request payload for the List Service Route Bindings operation + * + * see https://v3-apidocs.cloudfoundry.org/index.html#list-service-route-bindings + */ +@Value.Immutable +abstract class _ListServiceRouteBindingsRequest extends PaginatedRequest { + + /** + * The ids + */ + @FilterParameter("guids") + abstract List getIds(); + + /** + * A query string containing a list of label selector requirements + */ + @FilterParameter("label_selector") + @Nullable + abstract String getLabelSelector(); + + /** + * The route ids + */ + @FilterParameter("route_guids") + abstract List getRouteIds(); + + /** + * The service instance ids + */ + @FilterParameter("service_instance_guids") + abstract List getServiceInstanceIds(); + + /** + * The service instance names + */ + @FilterParameter("service_instance_names") + abstract List getServiceInstanceNames(); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_ListServiceRouteBindingsResponse.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_ListServiceRouteBindingsResponse.java new file mode 100644 index 0000000000..0b3181b853 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_ListServiceRouteBindingsResponse.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client.v3.serviceroutebindings; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.cloudfoundry.client.v3.PaginatedResponse; +import org.immutables.value.Value; + +/** + * The response payload for the List Service Route Bindings operation + * + * see https://v3-apidocs.cloudfoundry.org/index.html#list-service-route-bindings + */ +@JsonDeserialize +@Value.Immutable +abstract class _ListServiceRouteBindingsResponse + extends PaginatedResponse { + +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_ServiceRouteBindingRelationships.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_ServiceRouteBindingRelationships.java new file mode 100644 index 0000000000..373a5508af --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_ServiceRouteBindingRelationships.java @@ -0,0 +1,42 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client.v3.serviceroutebindings; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.cloudfoundry.client.v3.ToOneRelationship; +import org.immutables.value.Value; + +/** + * The relationships for a service route binding + */ +@Value.Immutable +@JsonDeserialize +abstract class _ServiceRouteBindingRelationships { + + /** + * The route relationship + */ + @JsonProperty("route") + abstract ToOneRelationship getRoute(); + + /** + * The service instance relationship + */ + @JsonProperty("service_instance") + abstract ToOneRelationship getServiceInstance(); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_ServiceRouteBindingResource.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_ServiceRouteBindingResource.java new file mode 100644 index 0000000000..f0ba3553b0 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_ServiceRouteBindingResource.java @@ -0,0 +1,27 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client.v3.serviceroutebindings; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.immutables.value.Value; + +/** + * The Resource response payload for the List Service Route Bindings operation + */ +@JsonDeserialize +@Value.Immutable +abstract class _ServiceRouteBindingResource extends ServiceRouteBinding {} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_UpdateServiceRouteBindingRequest.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_UpdateServiceRouteBindingRequest.java new file mode 100644 index 0000000000..bafdc7ba6e --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_UpdateServiceRouteBindingRequest.java @@ -0,0 +1,47 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client.v3.serviceroutebindings; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.cloudfoundry.Nullable; +import org.cloudfoundry.client.v3.Metadata; +import org.immutables.value.Value; + +/** + * The request payload for the Update Service Route Binding operation + * + * see https://v3-apidocs.cloudfoundry.org/index.html#update-a-service-route-binding + */ +@JsonSerialize +@Value.Immutable +abstract class _UpdateServiceRouteBindingRequest { + + /** + * The service route binding id + */ + @JsonIgnore + abstract String getServiceRouteBindingId(); + + /** + * The metadata + */ + @JsonProperty("metadata") + @Nullable + abstract Metadata getMetadata(); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_UpdateServiceRouteBindingResponse.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_UpdateServiceRouteBindingResponse.java new file mode 100644 index 0000000000..76d03d5796 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/serviceroutebindings/_UpdateServiceRouteBindingResponse.java @@ -0,0 +1,31 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client.v3.serviceroutebindings; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.immutables.value.Value; + +/** + * The response payload for the Update Service Route Binding operation + * + * see https://v3-apidocs.cloudfoundry.org/index.html#update-a-service-route-binding + */ +@JsonDeserialize +@Value.Immutable +abstract class _UpdateServiceRouteBindingResponse extends ServiceRouteBinding { + +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/spaces/Space.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/spaces/Space.java index 0ea0fc9d09..7d69999a2d 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/spaces/Space.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/spaces/Space.java @@ -45,4 +45,12 @@ public abstract class Space extends Resource { @JsonProperty("relationships") @Nullable public abstract SpaceRelationships getRelationships(); + + /** + * True if the space is suspended and no changes are allowed. + * See: https://v3-apidocs.cloudfoundry.org/index.html#spaces + */ + @JsonProperty("suspended") + @Nullable + public abstract Boolean getSuspended(); } diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/stacks/Stack.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/stacks/Stack.java index 0e90cc053d..6e6cca0410 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/stacks/Stack.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/stacks/Stack.java @@ -66,4 +66,18 @@ public abstract class Stack extends Resource { @JsonProperty("default") @Nullable abstract Boolean geDefault(); + + /** + * The state of the stack (e.g. ready) + */ + @JsonProperty("state") + @Nullable + abstract String getState(); + + /** + * The reason for the stack's current state + */ + @JsonProperty("state_reason") + @Nullable + abstract String getStateReason(); } diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/clients/AbstractClient.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/clients/AbstractClient.java index a761952b0c..6fa73e06c0 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/clients/AbstractClient.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/clients/AbstractClient.java @@ -54,6 +54,13 @@ abstract class AbstractClient { @Nullable abstract Boolean getApprovalsDeleted(); + /** + * If true, allow to omit client_secret for authorization_code flow in combination with PKCE + */ + @JsonProperty("allowpublic") + @Nullable + abstract Boolean getAllowPublic(); + /** * Scopes that the client is able to grant when creating a client */ diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/clients/AbstractCreateClient.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/clients/AbstractCreateClient.java index f5d10479c1..92e4a952bd 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/clients/AbstractCreateClient.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/clients/AbstractCreateClient.java @@ -57,6 +57,13 @@ void checkAuthorizedGrantTypes() { @Nullable abstract Boolean getApprovalsDeleted(); + /** + * If client allows authentication_code flow with PKCE w/o client_secret + */ + @JsonProperty("allowpublic") + @Nullable + abstract Boolean getAllowPublic(); + /** * Scopes that the client is able to grant when creating a client */ diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/clients/AbstractUpdateClient.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/clients/AbstractUpdateClient.java index de8b2ff317..bf3466f32e 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/clients/AbstractUpdateClient.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/clients/AbstractUpdateClient.java @@ -50,6 +50,13 @@ void checkAuthorizedGrantType() { @Nullable abstract Boolean getApprovalsDeleted(); + /** + * If client allows authentication_code flow with PKCE w/o client_secret + */ + @JsonProperty("allowpublic") + @Nullable + abstract Boolean getAllowPublic(); + /** * Scopes that the client is able to grant when creating a client */ diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/identityproviders/_LdapConfiguration.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/identityproviders/_LdapConfiguration.java index 75b658b695..42345e795f 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/identityproviders/_LdapConfiguration.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/identityproviders/_LdapConfiguration.java @@ -21,6 +21,8 @@ import org.cloudfoundry.Nullable; import org.immutables.value.Value; +import java.util.List; + /** * The payload for the ldap identity provider configuration */ @@ -63,6 +65,13 @@ abstract class _LdapConfiguration extends AbstractExternalIdentityProviderConfig @Nullable abstract String getBindUserDistinguishedName(); + /** + * The PEM encoded CA certificates used to validate the connection + */ + @JsonProperty("caCertificates") + @Nullable + abstract List getCaCertificates(); + /** * The group role attribute */ diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/identityproviders/_SamlConfiguration.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/identityproviders/_SamlConfiguration.java index 4e56d9204e..6a2e895b1e 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/identityproviders/_SamlConfiguration.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/identityproviders/_SamlConfiguration.java @@ -51,6 +51,13 @@ abstract class _SamlConfiguration extends AbstractExternalIdentityProviderConfig @Nullable abstract List getAuthnContext(); + /** + * The PEM encoded CA certificates used to validate the connection + */ + @JsonProperty("caCertificates") + @Nullable + abstract List getCaCertificates(); + /** * Either EXPLICITLY_MAPPED in order to map external groups to OAuth scopes using the group mappings, or AS_SCOPES to use SAML group names as scopes. */ 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 602ec57f84..9c925b2adc 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,38 @@ 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() + .loadbalancing("hash") + .hashHeader("X-Hash") + .hashBalance("90") + .build()) + .build(); + + 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 10e0f1eb0a..0ca13db01e 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,23 @@ 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() + .loadbalancing("hash") + .hashHeader("X-Hash") + .hashBalance("90") + .build()) + .routeId("test-route-id") + .build(); + + 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/serviceroutebindings/CreateServiceRouteBindingRequestTest.java b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/serviceroutebindings/CreateServiceRouteBindingRequestTest.java new file mode 100644 index 0000000000..bb320e107b --- /dev/null +++ b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/serviceroutebindings/CreateServiceRouteBindingRequestTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client.v3.serviceroutebindings; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.cloudfoundry.client.v3.Relationship; +import org.cloudfoundry.client.v3.ToOneRelationship; +import org.junit.jupiter.api.Test; + +final class CreateServiceRouteBindingRequestTest { + + @Test + void noRelationships() { + assertThrows( + IllegalStateException.class, + () -> CreateServiceRouteBindingRequest.builder().build()); + } + + @Test + void valid() { + CreateServiceRouteBindingRequest.builder() + .relationships( + ServiceRouteBindingRelationships.builder() + .route( + ToOneRelationship.builder() + .data( + Relationship.builder() + .id("test-route-id") + .build()) + .build()) + .serviceInstance( + ToOneRelationship.builder() + .data( + Relationship.builder() + .id("test-service-instance-id") + .build()) + .build()) + .build()) + .build(); + } +} diff --git a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/serviceroutebindings/DeleteServiceRouteBindingRequestTest.java b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/serviceroutebindings/DeleteServiceRouteBindingRequestTest.java new file mode 100644 index 0000000000..63ad678fb9 --- /dev/null +++ b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/serviceroutebindings/DeleteServiceRouteBindingRequestTest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client.v3.serviceroutebindings; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +final class DeleteServiceRouteBindingRequestTest { + + @Test + void noServiceRouteBindingId() { + assertThrows( + IllegalStateException.class, + () -> DeleteServiceRouteBindingRequest.builder().build()); + } + + @Test + void valid() { + DeleteServiceRouteBindingRequest.builder() + .serviceRouteBindingId("test-service-route-binding-id") + .build(); + } +} diff --git a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/serviceroutebindings/GetServiceRouteBindingParametersRequestTest.java b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/serviceroutebindings/GetServiceRouteBindingParametersRequestTest.java new file mode 100644 index 0000000000..0b1880a830 --- /dev/null +++ b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/serviceroutebindings/GetServiceRouteBindingParametersRequestTest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client.v3.serviceroutebindings; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +final class GetServiceRouteBindingParametersRequestTest { + + @Test + void noServiceRouteBindingId() { + assertThrows( + IllegalStateException.class, + () -> GetServiceRouteBindingParametersRequest.builder().build()); + } + + @Test + void valid() { + GetServiceRouteBindingParametersRequest.builder() + .serviceRouteBindingId("test-service-route-binding-id") + .build(); + } +} diff --git a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/serviceroutebindings/GetServiceRouteBindingRequestTest.java b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/serviceroutebindings/GetServiceRouteBindingRequestTest.java new file mode 100644 index 0000000000..e18d6e76cb --- /dev/null +++ b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/serviceroutebindings/GetServiceRouteBindingRequestTest.java @@ -0,0 +1,37 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client.v3.serviceroutebindings; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +final class GetServiceRouteBindingRequestTest { + + @Test + void noServiceRouteBindingId() { + assertThrows( + IllegalStateException.class, () -> GetServiceRouteBindingRequest.builder().build()); + } + + @Test + void valid() { + GetServiceRouteBindingRequest.builder() + .serviceRouteBindingId("test-service-route-binding-id") + .build(); + } +} diff --git a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/serviceroutebindings/ListServiceRouteBindingsRequestTest.java b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/serviceroutebindings/ListServiceRouteBindingsRequestTest.java new file mode 100644 index 0000000000..f6056cb11a --- /dev/null +++ b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/serviceroutebindings/ListServiceRouteBindingsRequestTest.java @@ -0,0 +1,27 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client.v3.serviceroutebindings; + +import org.junit.jupiter.api.Test; + +final class ListServiceRouteBindingsRequestTest { + + @Test + void valid() { + ListServiceRouteBindingsRequest.builder().build(); + } +} diff --git a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/serviceroutebindings/RelationshipsTest.java b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/serviceroutebindings/RelationshipsTest.java new file mode 100644 index 0000000000..da5cfce0d6 --- /dev/null +++ b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/serviceroutebindings/RelationshipsTest.java @@ -0,0 +1,66 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client.v3.serviceroutebindings; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.cloudfoundry.client.v3.Relationship; +import org.cloudfoundry.client.v3.ToOneRelationship; +import org.junit.jupiter.api.Test; + +final class RelationshipsTest { + + @Test + void noRoute() { + assertThrows( + IllegalStateException.class, + () -> + ServiceRouteBindingRelationships.builder() + .serviceInstance( + ToOneRelationship.builder() + .data(Relationship.builder().id("test-id").build()) + .build()) + .build()); + } + + @Test + void noServiceInstance() { + assertThrows( + IllegalStateException.class, + () -> + ServiceRouteBindingRelationships.builder() + .route( + ToOneRelationship.builder() + .data(Relationship.builder().id("test-id").build()) + .build()) + .build()); + } + + @Test + void valid() { + ServiceRouteBindingRelationships.builder() + .route( + ToOneRelationship.builder() + .data(Relationship.builder().id("test-route-id").build()) + .build()) + .serviceInstance( + ToOneRelationship.builder() + .data(Relationship.builder().id("test-service-instance-id").build()) + .build()) + .build(); + } +} diff --git a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/serviceroutebindings/UpdateServiceRouteBindingRequestTest.java b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/serviceroutebindings/UpdateServiceRouteBindingRequestTest.java new file mode 100644 index 0000000000..dab5942935 --- /dev/null +++ b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/serviceroutebindings/UpdateServiceRouteBindingRequestTest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client.v3.serviceroutebindings; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +final class UpdateServiceRouteBindingRequestTest { + + @Test + void noServiceRouteBindingId() { + assertThrows( + IllegalStateException.class, + () -> UpdateServiceRouteBindingRequest.builder().build()); + } + + @Test + void valid() { + UpdateServiceRouteBindingRequest.builder() + .serviceRouteBindingId("test-service-route-binding-id") + .build(); + } +} diff --git a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/ApplicationManifestUtilsV3.java b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/ApplicationManifestUtilsV3.java index da335ae704..5b585b810c 100644 --- a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/ApplicationManifestUtilsV3.java +++ b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/ApplicationManifestUtilsV3.java @@ -156,6 +156,12 @@ private static ManifestV3Application.Builder toApplicationManifest( Path root) { toApplicationManifestCommon(application, variables, builder, root); + asMap( + application, + "features", + variables, + String::valueOf, + (k, v) -> builder.feature(k, Boolean.valueOf(v))); asList( application, "processes", @@ -299,6 +305,7 @@ private static Map toYaml(ManifestV3 manifest) { private static Map toApplicationYaml(ManifestV3Application application) { Map yaml = ApplicationManifestUtilsCommon.toApplicationYaml(application); + putIfPresent(yaml, "features", application.getFeatures()); putIfPresent( yaml, "processes", diff --git a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/_ManifestV3Application.java b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/_ManifestV3Application.java index b235a1eb5d..fe0ac3412e 100644 --- a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/_ManifestV3Application.java +++ b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/_ManifestV3Application.java @@ -16,9 +16,10 @@ package org.cloudfoundry.operations.applications; +import org.cloudfoundry.AllowNulls; import org.cloudfoundry.Nullable; -import org.immutables.value.Value; import org.cloudfoundry.client.v3.Metadata; +import org.immutables.value.Value; import java.util.List; import java.util.Map; @@ -36,6 +37,13 @@ abstract class _ManifestV3Application extends _ApplicationManifestCommon { @Nullable abstract Boolean getDefaultRoute(); + /** + * Manage whether optional capabilities are enabled + */ + @AllowNulls + @Nullable + abstract Map getFeatures(); + /** * The metadata for this application */ @@ -61,6 +69,9 @@ abstract class _ManifestV3Application extends _ApplicationManifestCommon { abstract List getSidecars(); public abstract static class Builder implements _ApplicationManifestCommon.Builder { - + abstract Builder feature(String key, Object value); + abstract Builder feature(Map.Entry entry); + abstract Builder features(@Nullable Map entries); + abstract Builder putAllFeatures(Map entries); } } diff --git a/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/applications/ApplicationManifestUtilsV3Test.java b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/applications/ApplicationManifestUtilsV3Test.java index cf54b8120b..3e05995f57 100644 --- a/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/applications/ApplicationManifestUtilsV3Test.java +++ b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/applications/ApplicationManifestUtilsV3Test.java @@ -1,15 +1,60 @@ package org.cloudfoundry.operations.applications; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Map; import org.cloudfoundry.client.v3.Metadata; import org.cloudfoundry.client.v3.processes.ReadinessHealthCheckType; import org.junit.jupiter.api.Test; class ApplicationManifestUtilsV3Test { + @Test + void testWithDockerApp() throws IOException { + ManifestV3 manifest = + ManifestV3.builder() + .application( + ManifestV3Application.builder() + .name("test-app") + .docker(Docker.builder().image("test-image").build()) + .build()) + .build(); + + assertSerializeDeserialize(manifest); + } + + @Test + void testWithFeature() throws IOException { + ManifestV3 manifest = + ManifestV3.builder() + .application( + ManifestV3Application.builder() + .name("test-app") + .feature("file-based-vcap-services", true) + .build()) + .build(); + + assertSerializeDeserialize(manifest); + } + + @Test + void testWithFeatureAsMap() throws IOException { + Map features = new java.util.HashMap<>(); + features.put("file-based-vcap-services", true); + ManifestV3 manifest = + ManifestV3.builder() + .application( + ManifestV3Application.builder() + .name("test-app") + .features(features) + .build()) + .build(); + + assertSerializeDeserialize(manifest); + } + @Test void testGenericApplication() throws IOException { ManifestV3 manifest = @@ -47,20 +92,6 @@ void testGenericApplication() throws IOException { assertSerializeDeserialize(manifest); } - @Test - void testWithDockerApp() throws IOException { - ManifestV3 manifest = - ManifestV3.builder() - .application( - ManifestV3Application.builder() - .name("test-app") - .docker(Docker.builder().image("test-image").build()) - .build()) - .build(); - - assertSerializeDeserialize(manifest); - } - @Test void testWithMetadata() throws IOException { ManifestV3 manifest = diff --git a/integration-test/src/test/java/org/cloudfoundry/CloudFoundryCleaner.java b/integration-test/src/test/java/org/cloudfoundry/CloudFoundryCleaner.java index 582e7cfbe5..54455f7f4d 100644 --- a/integration-test/src/test/java/org/cloudfoundry/CloudFoundryCleaner.java +++ b/integration-test/src/test/java/org/cloudfoundry/CloudFoundryCleaner.java @@ -28,7 +28,6 @@ import java.util.function.Supplier; import javax.net.ssl.SSLException; import org.cloudfoundry.client.CloudFoundryClient; -import org.cloudfoundry.client.v2.applications.ListApplicationServiceBindingsRequest; import org.cloudfoundry.client.v2.applications.RemoveApplicationServiceBindingRequest; import org.cloudfoundry.client.v2.buildpacks.DeleteBuildpackRequest; import org.cloudfoundry.client.v2.buildpacks.ListBuildpacksRequest; @@ -78,6 +77,8 @@ import org.cloudfoundry.client.v3.applications.Application; import org.cloudfoundry.client.v3.applications.DeleteApplicationRequest; import org.cloudfoundry.client.v3.applications.ListApplicationsRequest; +import org.cloudfoundry.client.v3.servicebindings.DeleteServiceBindingRequest; +import org.cloudfoundry.client.v3.servicebindings.ListServiceBindingsRequest; import org.cloudfoundry.client.v3.serviceinstances.ListSharedSpacesRelationshipRequest; import org.cloudfoundry.client.v3.serviceinstances.ListSharedSpacesRelationshipResponse; import org.cloudfoundry.client.v3.serviceinstances.UnshareServiceInstanceRequest; @@ -129,6 +130,8 @@ final class CloudFoundryCleaner implements InitializingBean, DisposableBean { private static final Logger LOGGER = LoggerFactory.getLogger("cloudfoundry-client.test"); + private static final boolean RUN_V2_CLEANUP = RequiresV2Api.V2ApiCondition.isEnabled(); + private static final Map STANDARD_FEATURE_FLAGS = FluentMap.builder() .entry("app_bits_upload", true) @@ -186,6 +189,28 @@ public void destroy() { } void clean() { + if (!RUN_V2_CLEANUP) { + LOGGER.info("Skipping V2 API cleanup operations (SKIP_V2_TESTS=true)"); + // Only run V3 and UAA cleanup operations + Flux.empty() + .thenMany( + Mono.when( // No prerequisites - V3/UAA only + cleanClients(this.uaaClient, this.nameFactory), + cleanGroups(this.uaaClient, this.nameFactory), + cleanIdentityProviders(this.uaaClient, this.nameFactory), + cleanIdentityZones(this.uaaClient, this.nameFactory))) + .thenMany( + Mono.when( + cleanApplicationsV3(this.cloudFoundryClient, this.nameFactory), + cleanUsers(this.uaaClient, this.nameFactory))) + .retryWhen(Retry.max(5).filter(SSLException.class::isInstance)) + .doOnSubscribe(s -> LOGGER.debug(">> CLEANUP (V3 only) <<")) + .doOnComplete(() -> LOGGER.debug("<< CLEANUP (V3 only) >>")) + .then() + .block(Duration.ofMinutes(30)); + return; + } + Flux.empty() .thenMany( Mono.when( // Before Routes @@ -216,10 +241,7 @@ void clean() { cleanUsersV3(this.cloudFoundryClient, this.nameFactory))) .thenMany( Mono.when( - cleanApplicationsV3( - this.cloudFoundryClient, - this.nameFactory), // After Routes, cannot run with - // other cleanApps + cleanApplicationsV3(this.cloudFoundryClient, this.nameFactory), cleanUsers(this.uaaClient, this.nameFactory) // After CF Users )) .thenMany( @@ -1156,21 +1178,24 @@ private static boolean isCleanable(NameFactory nameFactory, UserResource resourc private static Flux removeApplicationServiceBindings( CloudFoundryClient cloudFoundryClient, Application application) { - return PaginationUtils.requestClientV2Resources( + return PaginationUtils.requestClientV3Resources( page -> cloudFoundryClient - .applicationsV2() - .listServiceBindings( - ListApplicationServiceBindingsRequest.builder() + .serviceBindingsV3() + .list( + ListServiceBindingsRequest.builder() .page(page) .applicationId(application.getId()) .build())) .flatMap( serviceBinding -> - requestRemoveServiceBinding( - cloudFoundryClient, - application.getId(), - ResourceUtils.getId(serviceBinding)) + cloudFoundryClient + .serviceBindingsV3() + .delete( + DeleteServiceBindingRequest.builder() + .serviceBindingId(serviceBinding.getId()) + .build()) + .then() .doOnError( t -> LOGGER.error( diff --git a/integration-test/src/test/java/org/cloudfoundry/CloudFoundryVersion.java b/integration-test/src/test/java/org/cloudfoundry/CloudFoundryVersion.java index 65d7a8e38c..bb18987810 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)), + CAPI_2_248(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/IntegrationTestConfiguration.java b/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java index 58b01252f0..3edc347192 100644 --- a/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java +++ b/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java @@ -38,15 +38,24 @@ import java.util.HashMap; import java.util.List; import org.cloudfoundry.client.CloudFoundryClient; +import org.cloudfoundry.client.GetRootRequest; import org.cloudfoundry.client.v2.info.GetInfoRequest; -import org.cloudfoundry.client.v2.organizationquotadefinitions.CreateOrganizationQuotaDefinitionRequest; -import org.cloudfoundry.client.v2.organizations.AssociateOrganizationManagerRequest; -import org.cloudfoundry.client.v2.organizations.CreateOrganizationRequest; -import org.cloudfoundry.client.v2.spaces.CreateSpaceRequest; -import org.cloudfoundry.client.v2.stacks.ListStacksRequest; -import org.cloudfoundry.client.v2.stacks.StackEntity; -import org.cloudfoundry.client.v2.stacks.StackResource; import org.cloudfoundry.client.v2.userprovidedserviceinstances.CreateUserProvidedServiceInstanceRequest; +import org.cloudfoundry.client.v3.Relationship; +import org.cloudfoundry.client.v3.ToOneRelationship; +import org.cloudfoundry.client.v3.organizations.CreateOrganizationRequest; +import org.cloudfoundry.client.v3.organizations.CreateOrganizationResponse; +import org.cloudfoundry.client.v3.quotas.Routes; +import org.cloudfoundry.client.v3.quotas.organizations.ListOrganizationQuotasRequest; +import org.cloudfoundry.client.v3.quotas.organizations.ListOrganizationQuotasResponse; +import org.cloudfoundry.client.v3.quotas.organizations.OrganizationQuotaResource; +import org.cloudfoundry.client.v3.quotas.organizations.UpdateOrganizationQuotaRequest; +import org.cloudfoundry.client.v3.roles.CreateRoleRequest; +import org.cloudfoundry.client.v3.roles.RoleRelationships; +import org.cloudfoundry.client.v3.roles.RoleType; +import org.cloudfoundry.client.v3.spaces.CreateSpaceRequest; +import org.cloudfoundry.client.v3.spaces.CreateSpaceResponse; +import org.cloudfoundry.client.v3.spaces.SpaceRelationships; import org.cloudfoundry.doppler.DopplerClient; import org.cloudfoundry.logcache.v1.LogCacheClient; import org.cloudfoundry.logcache.v1.TestLogCacheEndpoints; @@ -65,6 +74,9 @@ import org.cloudfoundry.reactor.tokenprovider.PasswordGrantTokenProvider; import org.cloudfoundry.reactor.uaa.ReactorUaaClient; import org.cloudfoundry.routing.RoutingClient; +import org.cloudfoundry.routing.v1.routergroups.ListRouterGroupsRequest; +import org.cloudfoundry.routing.v1.routergroups.ListRouterGroupsResponse; +import org.cloudfoundry.routing.v1.routergroups.UpdateRouterGroupRequest; import org.cloudfoundry.uaa.UaaClient; import org.cloudfoundry.uaa.clients.CreateClientRequest; import org.cloudfoundry.uaa.groups.AddMemberRequest; @@ -86,6 +98,7 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; @@ -251,6 +264,7 @@ String clientSecret(NameFactory nameFactory) { } @Bean + @DependsOn("routerGroupPorts") CloudFoundryCleaner cloudFoundryCleaner( @Qualifier("admin") CloudFoundryClient cloudFoundryClient, NameFactory nameFactory, @@ -304,17 +318,18 @@ DefaultConnectionContext connectionContext( @Value("${test.proxy.password:}") String proxyPassword, @Value("${test.proxy.port:8080}") Integer proxyPort, @Value("${test.proxy.username:}") String proxyUsername, + @Value("${test.fail.on.unknown.properties:true}") Boolean failOnUnknownProperties, @Value("${test.skipSslValidation:false}") Boolean skipSslValidation) { DefaultConnectionContext.Builder connectionContext = DefaultConnectionContext.builder() .apiHost(apiHost) - .problemHandler( - new FailingDeserializationProblemHandler()) // Test-only problem - // handler .skipSslValidation(skipSslValidation) .sslHandshakeTimeout(Duration.ofSeconds(30)); - + if (failOnUnknownProperties) { + connectionContext.problemHandler( + new FailingDeserializationProblemHandler()); // Test-only problem handler + } if (StringUtils.hasText(proxyHost)) { ProxyConfiguration.Builder proxyConfiguration = ProxyConfiguration.builder().host(proxyHost).port(proxyPort); @@ -353,6 +368,10 @@ RandomNameFactory nameFactory() { @Bean(initMethod = "block") @DependsOn("cloudFoundryCleaner") + @ConditionalOnProperty( + name = RequiresV2Api.SKIP_V2_TESTS_ENV, + havingValue = "false", + matchIfMissing = true) Mono metricRegistrarServiceInstance( CloudFoundryClient cloudFoundryClient, Mono spaceId, NameFactory nameFactory) { return spaceId.flatMap( @@ -381,52 +400,48 @@ NetworkingClient networkingClient( @Bean(initMethod = "block") @DependsOn("cloudFoundryCleaner") Mono organizationId( - CloudFoundryClient cloudFoundryClient, - String organizationName, - String organizationQuotaName, - Mono userId) { + CloudFoundryClient cloudFoundryClient, String organizationName, Mono userId) { return userId.flatMap( userId1 -> cloudFoundryClient - .organizationQuotaDefinitions() + .organizationsV3() .create( - CreateOrganizationQuotaDefinitionRequest.builder() - .applicationInstanceLimit(-1) - .applicationTaskLimit(-1) - .instanceMemoryLimit(-1) - .memoryLimit(16384) - .name(organizationQuotaName) - .nonBasicServicesAllowed(true) - .totalPrivateDomains(-1) - .totalReservedRoutePorts(-1) - .totalRoutes(-1) - .totalServiceKeys(-1) - .totalServices(-1) + CreateOrganizationRequest.builder() + .name(organizationName) .build()) - .map(ResourceUtils::getId) + .map(CreateOrganizationResponse::getId) .zipWith(Mono.just(userId1))) - .flatMap( - function( - (quotaId, userId1) -> - cloudFoundryClient - .organizations() - .create( - CreateOrganizationRequest.builder() - .name(organizationName) - .quotaDefinitionId(quotaId) - .build()) - .map(ResourceUtils::getId) - .zipWith(Mono.just(userId1)))) .flatMap( function( (organizationId, userId1) -> cloudFoundryClient - .organizations() - .associateManager( - AssociateOrganizationManagerRequest - .builder() - .organizationId(organizationId) - .managerId(userId1) + .rolesV3() + .create( + CreateRoleRequest.builder() + .type(RoleType.ORGANIZATION_MANAGER) + .relationships( + RoleRelationships.builder() + .user( + ToOneRelationship + .builder() + .data( + Relationship + .builder() + .id( + userId1) + .build()) + .build()) + .organization( + ToOneRelationship + .builder() + .data( + Relationship + .builder() + .id( + organizationId) + .build()) + .build()) + .build()) .build()) .thenReturn(organizationId))) .doOnSubscribe(s -> this.logger.debug(">> ORGANIZATION ({}) <<", organizationName)) @@ -440,11 +455,6 @@ String organizationName(NameFactory nameFactory) { return nameFactory.getOrganizationName(); } - @Bean - String organizationQuotaName(NameFactory nameFactory) { - return nameFactory.getQuotaDefinitionName(); - } - @Bean String password(NameFactory nameFactory) { return nameFactory.getPassword(); @@ -455,6 +465,55 @@ String planName(NameFactory nameFactory) { return nameFactory.getPlanName(); } + // Tests allocate TCP ports from NameFactory, so the default router group has to reserve them + // regardless of the order in which the tests run (or of the deployment's default range). + @Bean(initMethod = "block") + Mono routerGroupPorts( + ConnectionContext connectionContext, + @Value("${test.admin.password}") String password, + @Value("${test.admin.username}") String username, + @Value("${skip.tcp.routing.tests:false}") boolean skipTcpRouting) { + if (skipTcpRouting) { + return Mono.empty(); + } + + // Not the shared routingClient: its token provider depends on the cleaner, which depends on + // this bean + RoutingClient routingClient = + ReactorRoutingClient.builder() + .connectionContext(connectionContext) + .tokenProvider( + PasswordGrantTokenProvider.builder() + .password(password) + .username(username) + .build()) + .build(); + + return routingClient + .routerGroups() + .list(ListRouterGroupsRequest.builder().build()) + .flatMapIterable(ListRouterGroupsResponse::getRouterGroups) + .filter(group -> "default-tcp".equals(group.getName())) + .next() + .switchIfEmpty( + Mono.fromRunnable( + () -> + this.logger.warn( + "No default-tcp router group, TCP route tests will" + + " fail"))) + .flatMap( + group -> + routingClient + .routerGroups() + .update( + UpdateRouterGroupRequest.builder() + .reservablePorts( + RandomNameFactory.PORT_RANGE) + .routerGroupId(group.getRouterGroupId()) + .build())) + .then(); + } + @Bean RoutingClient routingClient(ConnectionContext connectionContext, TokenProvider tokenProvider) { return ReactorRoutingClient.builder() @@ -465,18 +524,41 @@ RoutingClient routingClient(ConnectionContext connectionContext, TokenProvider t @Bean Version serverVersion(@Qualifier("admin") CloudFoundryClient cloudFoundryClient) { - return cloudFoundryClient - .info() - .get(GetInfoRequest.builder().build()) - .map(response -> Version.valueOf(response.getApiVersion())) + return serverVersionV2(cloudFoundryClient) + .switchIfEmpty(serverVersionV3(cloudFoundryClient)) .doOnSubscribe(s -> this.logger.debug(">> CLOUD FOUNDRY VERSION <<")) .doOnSuccess(r -> this.logger.debug("<< CLOUD FOUNDRY VERSION >>")) .block(); } + private static Mono serverVersionV2(CloudFoundryClient cloudFoundryClient) { + return cloudFoundryClient + .info() + .get(GetInfoRequest.builder().build()) + .flatMap( + response -> { + String version = response.getApiVersion(); + if (version == null || version.isEmpty()) { + return Mono.empty(); + } + return Mono.just(Version.valueOf(version)); + }); + } + + private static Mono serverVersionV3(CloudFoundryClient cloudFoundryClient) { + return cloudFoundryClient + .rootEndpoint() + .get(GetRootRequest.builder().build()) + .map(response -> Version.valueOf(response.getApiVersionV3())); + } + @Lazy @Bean(initMethod = "block") @DependsOn("cloudFoundryCleaner") + @ConditionalOnProperty( + name = RequiresV2Api.SKIP_V2_TESTS_ENV, + havingValue = "false", + matchIfMissing = true) Mono serviceBrokerId( CloudFoundryClient cloudFoundryClient, NameFactory nameFactory, @@ -525,13 +607,25 @@ Mono spaceId( .flatMap( orgId -> cloudFoundryClient - .spaces() + .spacesV3() .create( CreateSpaceRequest.builder() .name(spaceName) - .organizationId(orgId) + .relationships( + SpaceRelationships.builder() + .organization( + ToOneRelationship + .builder() + .data( + Relationship + .builder() + .id( + orgId) + .build()) + .build()) + .build()) .build())) - .map(ResourceUtils::getId) + .map(CreateSpaceResponse::getId) .doOnSubscribe(s -> this.logger.debug(">> SPACE ({}) <<", spaceName)) .doOnError(Throwable::printStackTrace) .doOnSuccess(id -> this.logger.debug("<< SPACE ({}) >>", id)) @@ -547,20 +641,20 @@ String spaceName(NameFactory nameFactory) { @DependsOn("cloudFoundryCleaner") Mono stackId(CloudFoundryClient cloudFoundryClient, Mono stackName) { return stackName - .flux() - .flatMap( + .flatMapMany( name -> - PaginationUtils.requestClientV2Resources( + PaginationUtils.requestClientV3Resources( page -> cloudFoundryClient - .stacks() + .stacksV3() .list( - ListStacksRequest.builder() + org.cloudfoundry.client.v3.stacks + .ListStacksRequest.builder() .name(name) .page(page) .build()))) .single() - .map(ResourceUtils::getId) + .map(org.cloudfoundry.client.v3.stacks.StackResource::getId) .doOnSubscribe(s -> this.logger.debug(">> STACK ({}) <<", stackName)) .doOnError(Throwable::printStackTrace) .doOnSuccess(id -> this.logger.debug("<< STACK ({})>>", id)) @@ -574,13 +668,16 @@ Mono stackId(CloudFoundryClient cloudFoundryClient, Mono stackNa @Bean(initMethod = "block") @DependsOn("cloudFoundryCleaner") Mono stackName(CloudFoundryClient cloudFoundryClient) { - return PaginationUtils.requestClientV2Resources( + return PaginationUtils.requestClientV3Resources( page -> cloudFoundryClient - .stacks() - .list(ListStacksRequest.builder().page(page).build())) - .map(StackResource::getEntity) - .map(StackEntity::getName) + .stacksV3() + .list( + org.cloudfoundry.client.v3.stacks.ListStacksRequest + .builder() + .page(page) + .build())) + .map(org.cloudfoundry.client.v3.stacks.StackResource::getName) .filter(s -> s.matches("^cflinuxfs\\d$")) .sort(Comparator.reverseOrder()) .next() @@ -590,6 +687,10 @@ Mono stackName(CloudFoundryClient cloudFoundryClient) { @Lazy @Bean(initMethod = "block") @DependsOn("cloudFoundryCleaner") + @ConditionalOnProperty( + name = RequiresV2Api.SKIP_V2_TESTS_ENV, + havingValue = "false", + matchIfMissing = true) Mono testLogCacheApp( CloudFoundryClient cloudFoundryClient, Mono spaceId, @@ -626,6 +727,10 @@ String testLogCacheAppName(NameFactory nameFactory) { @Lazy @Bean + @ConditionalOnProperty( + name = RequiresV2Api.SKIP_V2_TESTS_ENV, + havingValue = "false", + matchIfMissing = true) TestLogCacheEndpoints testLogCacheEndpoints( ConnectionContext connectionContext, TokenProvider tokenProvider, @@ -749,6 +854,35 @@ String username(NameFactory nameFactory) { return nameFactory.getUserName(); } + @Bean + @ConditionalOnProperty(value = "test.quotas.routes.reserved-ports", matchIfMissing = false) + Integer tcpRouteQuota( + @Value("${test.quotas.routes.reserved-ports}") Integer portsQuota, + @Qualifier("admin") CloudFoundryClient cloudFoundryClient) { + cloudFoundryClient + .organizationQuotasV3() + .list(ListOrganizationQuotasRequest.builder().name("default").build()) + .flatMapIterable(ListOrganizationQuotasResponse::getResources) + .map(OrganizationQuotaResource::getId) + .last() + .flatMap( + orgId -> + cloudFoundryClient + .organizationQuotasV3() + .update( + UpdateOrganizationQuotaRequest.builder() + .organizationQuotaId(orgId) + .routes( + Routes.builder() + .totalReservedPorts( + portsQuota) + .build()) + .build())) + .block(); + this.logger.debug("APPLIED ORG QUOTA, reserved-route-ports={}", portsQuota); + return portsQuota; + } + public static final class FailingDeserializationProblemHandler extends DeserializationProblemHandler { diff --git a/integration-test/src/test/java/org/cloudfoundry/RandomNameFactory.java b/integration-test/src/test/java/org/cloudfoundry/RandomNameFactory.java index 5443f15780..6f9b92859c 100644 --- a/integration-test/src/test/java/org/cloudfoundry/RandomNameFactory.java +++ b/integration-test/src/test/java/org/cloudfoundry/RandomNameFactory.java @@ -29,6 +29,11 @@ final class RandomNameFactory implements NameFactory { private static final int PORT_MINIMUM = 1025; + /** + * The router group port range that covers every port handed out by {@link #getPort()} + */ + static final String PORT_RANGE = PORT_MINIMUM + "-" + PORT_MAXIMUM; + private final Random random; private AtomicInteger port = new AtomicInteger(PORT_MINIMUM); diff --git a/integration-test/src/test/java/org/cloudfoundry/RequiresBrowserAuth.java b/integration-test/src/test/java/org/cloudfoundry/RequiresBrowserAuth.java new file mode 100644 index 0000000000..d675dd6324 --- /dev/null +++ b/integration-test/src/test/java/org/cloudfoundry/RequiresBrowserAuth.java @@ -0,0 +1,74 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; + +/** + * Annotation to mark tests that require browser-based authentication (e.g., OAuth + * authorization code grants, implicit grants). Tests annotated with this will be skipped + * if the environment variable {@code SKIP_BROWSER_AUTH_TESTS} is set to "true". + * + *

Usage: + *

+ * @RequiresBrowserAuth
+ * public class MyAuthTest extends AbstractIntegrationTest {
+ *     // ...
+ * }
+ * 
+ * + *

To skip browser auth tests, set the environment variable: + *

+ * export SKIP_BROWSER_AUTH_TESTS=true
+ * 
+ */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ExtendWith(RequiresBrowserAuth.BrowserAuthCondition.class) +public @interface RequiresBrowserAuth { + + /** + * JUnit 5 ExecutionCondition that checks if Browser Auth tests should be skipped. + */ + class BrowserAuthCondition implements ExecutionCondition { + + private static final String SKIP_BROWSER_AUTH_ENV = "SKIP_BROWSER_AUTH_TESTS"; + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + if ("true".equalsIgnoreCase(System.getenv(SKIP_BROWSER_AUTH_ENV))) { + return ConditionEvaluationResult.disabled( + "Tests requiring Browser Authentication are disabled via " + + SKIP_BROWSER_AUTH_ENV + + " environment variable"); + } + + return ConditionEvaluationResult.enabled("Browser authentication tests are enabled"); + } + } +} diff --git a/integration-test/src/test/java/org/cloudfoundry/RequiresMetricRegistrar.java b/integration-test/src/test/java/org/cloudfoundry/RequiresMetricRegistrar.java new file mode 100644 index 0000000000..c70d21dfe2 --- /dev/null +++ b/integration-test/src/test/java/org/cloudfoundry/RequiresMetricRegistrar.java @@ -0,0 +1,74 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; + +/** + * Annotation to mark tests that require the Metric Registrar service to be available. + * Tests annotated with this will be skipped if the environment variable + * {@code SKIP_METRIC_REGISTRAR_TESTS} is set to "true". + * + *

Usage: + *

+ * @RequiresMetricRegistrar
+ * public class MetricTest extends AbstractIntegrationTest {
+ *     // ...
+ * }
+ * 
+ * + *

To skip metric registrar tests, set the environment variable: + *

+ * export SKIP_METRIC_REGISTRAR_TESTS=true
+ * 
+ */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ExtendWith(RequiresMetricRegistrar.RegistrarCondition.class) +public @interface RequiresMetricRegistrar { + + /** + * JUnit 5 ExecutionCondition that checks if Metric Registrar tests should be skipped. + */ + class RegistrarCondition implements ExecutionCondition { + + private static final String SKIP_REGISTRAR_ENV = "SKIP_METRIC_REGISTRAR_TESTS"; + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + if ("true".equalsIgnoreCase(System.getenv(SKIP_REGISTRAR_ENV))) { + return ConditionEvaluationResult.disabled( + "Tests requiring Metric Registrar Service are disabled via " + + SKIP_REGISTRAR_ENV + + " environment variable."); + } + + return ConditionEvaluationResult.enabled("Metric Registrar tests are enabled"); + } + } +} diff --git a/integration-test/src/test/java/org/cloudfoundry/RequiresTcpRouting.java b/integration-test/src/test/java/org/cloudfoundry/RequiresTcpRouting.java new file mode 100644 index 0000000000..0ee45aee62 --- /dev/null +++ b/integration-test/src/test/java/org/cloudfoundry/RequiresTcpRouting.java @@ -0,0 +1,77 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; + +/** + * Annotation to mark tests that require TCP routing to be configured. + * + *

Tests annotated with this (or test classes containing this annotation) + * will be skipped if the {@code SKIP_TCP_ROUTING_TESTS} environment variable + * is set to "true". + * + *

Use this when your Cloud Foundry instance does not have TCP routing + * configured (i.e., when the info payload does not contain a 'routing_endpoint' key). + * + *

Example usage: + *

+ * @RequiresTcpRouting
+ * public class TcpRoutesTest extends AbstractIntegrationTest {
+ *     // tests that require TCP routing
+ * }
+ * 
+ */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ExtendWith(RequiresTcpRouting.SkipTcpRoutingCondition.class) +public @interface RequiresTcpRouting { + + /** + * JUnit 5 ExecutionCondition that checks if tcp routing tests should be skipped. + */ + class SkipTcpRoutingCondition implements ExecutionCondition { + + private static final String ENV_VAR = "SKIP_TCP_ROUTING_TESTS"; + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + String envValue = System.getenv(ENV_VAR); + + if ("true".equalsIgnoreCase(envValue)) { + return ConditionEvaluationResult.disabled( + "TCP routing tests are disabled via " + + ENV_VAR + + " environment variable" + + ". TCP routing may not be configured on the target CF instance."); + } + + return ConditionEvaluationResult.enabled("TCP routing tests are enabled"); + } + } +} diff --git a/integration-test/src/test/java/org/cloudfoundry/RequiresV2Api.java b/integration-test/src/test/java/org/cloudfoundry/RequiresV2Api.java new file mode 100644 index 0000000000..b11fe8ca06 --- /dev/null +++ b/integration-test/src/test/java/org/cloudfoundry/RequiresV2Api.java @@ -0,0 +1,78 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; + +/** + * Annotation to mark tests that require the V2 API. Tests annotated with this + * will be skipped if the environment variable {@code SKIP_V2_TESTS} is set to "true". + * + *

Usage: + *

+ * @RequiresV2Api
+ * public class MyV2Test extends AbstractIntegrationTest {
+ *     // ...
+ * }
+ * 
+ * + *

To skip V2 tests, set the environment variable: + *

+ * export SKIP_V2_TESTS=true
+ * 
+ */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ExtendWith(RequiresV2Api.V2ApiCondition.class) +public @interface RequiresV2Api { + + /** Environment variable name used to skip V2 API tests. */ + String SKIP_V2_TESTS_ENV = "SKIP_V2_TESTS"; + + /** + * JUnit 5 ExecutionCondition that checks if V2 tests should be skipped. + */ + class V2ApiCondition implements ExecutionCondition { + + /** Returns {@code true} if V2 API tests should run (i.e., not skipped). */ + public static boolean isEnabled() { + return !"true".equalsIgnoreCase(System.getenv(SKIP_V2_TESTS_ENV)); + } + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + if (!isEnabled()) { + return ConditionEvaluationResult.disabled( + "V2 API tests are disabled via " + + SKIP_V2_TESTS_ENV + + " environment variable"); + } + return ConditionEvaluationResult.enabled("V2 API tests are enabled"); + } + } +} diff --git a/integration-test/src/test/java/org/cloudfoundry/client/RootEndpointTest.java b/integration-test/src/test/java/org/cloudfoundry/client/RootEndpointTest.java new file mode 100644 index 0000000000..84b0479420 --- /dev/null +++ b/integration-test/src/test/java/org/cloudfoundry/client/RootEndpointTest.java @@ -0,0 +1,28 @@ +package org.cloudfoundry.client; + +import com.github.zafarkhaja.semver.Version; +import java.time.Duration; +import org.cloudfoundry.AbstractIntegrationTest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import reactor.test.StepVerifier; + +class RootEndpointTest extends AbstractIntegrationTest { + + @Qualifier("cloudFoundryClient") + @Autowired + CloudFoundryClient client; + + @Test + void rootVersion() { + client.rootEndpoint() + .get(GetRootRequest.builder().build()) + .map(GetRootResponse::getApiVersionV3) + .map(Version::parse) + .as(StepVerifier::create) + .consumeNextWith(v -> v.isHigherThan(Version.of(3))) + .expectComplete() + .verify(Duration.ofMinutes(1)); + } +} diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/ApplicationsTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/ApplicationsTest.java index 9774568bd0..df19fe4c8a 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/ApplicationsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/ApplicationsTest.java @@ -39,6 +39,7 @@ import org.cloudfoundry.CleanupCloudFoundryAfterClass; import org.cloudfoundry.CloudFoundryVersion; import org.cloudfoundry.IfCloudFoundryVersion; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.applications.AbstractApplicationResource; import org.cloudfoundry.client.v2.applications.ApplicationEnvironmentRequest; @@ -98,6 +99,7 @@ import reactor.util.function.Tuple2; @CleanupCloudFoundryAfterClass +@RequiresV2Api public final class ApplicationsTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/BlobstoresTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/BlobstoresTest.java index f9368a0730..283ac47f41 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/BlobstoresTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/BlobstoresTest.java @@ -19,6 +19,7 @@ import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; import org.cloudfoundry.ApplicationUtils; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.blobstores.DeleteBlobstoreBuildpackCachesRequest; import org.cloudfoundry.util.JobUtils; @@ -27,6 +28,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +@RequiresV2Api public final class BlobstoresTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; @@ -47,6 +49,6 @@ public void delete() { this.cloudFoundryClient, Duration.ofMinutes(5), job)) .as(StepVerifier::create) .expectComplete() - .verify(Duration.ofMinutes(5)); + .verify(Duration.ofMinutes(10)); } } diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/BuildpacksTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/BuildpacksTest.java index 03b7d3f0e9..00afd83bcb 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/BuildpacksTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/BuildpacksTest.java @@ -20,6 +20,7 @@ import java.nio.file.Path; import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.buildpacks.BuildpackEntity; import org.cloudfoundry.client.v2.buildpacks.BuildpackResource; @@ -40,6 +41,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +@RequiresV2Api public final class BuildpacksTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/DomainsTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/DomainsTest.java index 676d130484..8d4841222d 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/DomainsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/DomainsTest.java @@ -23,6 +23,7 @@ import java.time.Duration; import java.util.function.Consumer; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.applications.CreateApplicationRequest; import org.cloudfoundry.client.v2.applications.CreateApplicationResponse; @@ -59,6 +60,7 @@ import reactor.util.function.Tuple2; @SuppressWarnings("deprecation") +@RequiresV2Api public final class DomainsTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/EventsTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/EventsTest.java index 76dcea594d..d52358d9db 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/EventsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/EventsTest.java @@ -18,6 +18,7 @@ import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.events.EventResource; import org.cloudfoundry.client.v2.events.GetEventRequest; @@ -29,6 +30,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +@RequiresV2Api public final class EventsTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/FeatureFlagsTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/FeatureFlagsTest.java index d47962b5aa..217bdd155a 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/FeatureFlagsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/FeatureFlagsTest.java @@ -26,6 +26,7 @@ import java.util.Set; import java.util.stream.Collectors; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.featureflags.FeatureFlagEntity; import org.cloudfoundry.client.v2.featureflags.GetFeatureFlagRequest; @@ -38,6 +39,7 @@ import reactor.test.StepVerifier; import reactor.util.function.Tuples; +@RequiresV2Api public final class FeatureFlagsTest extends AbstractIntegrationTest { private static final List coreFeatureFlagNameList = diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/InfoTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/InfoTest.java index 471cb284e8..b94c87a187 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/InfoTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/InfoTest.java @@ -22,12 +22,14 @@ import com.github.zafarkhaja.semver.Version; import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.info.GetInfoRequest; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import reactor.test.StepVerifier; +@RequiresV2Api public final class InfoTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; @@ -41,8 +43,15 @@ public void info() { .consumeNextWith( response -> { Version expected = Version.valueOf(SUPPORTED_API_VERSION); - Version actual = Version.valueOf(response.getApiVersion()); - + Version actual; + String version = response.getApiVersion(); + if (version == null || version.isEmpty()) { + assertThat("CF API v2 is disabled") + .isEqualTo(response.getSupport()); + actual = Version.of(0, 0, 0); + } else { + actual = Version.valueOf(version); + } assertThat(actual).isLessThanOrEqualTo(expected); }) .expectComplete() diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/JobsTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/JobsTest.java index 0727b062d2..950ae4e35a 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/JobsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/JobsTest.java @@ -20,6 +20,7 @@ import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.jobs.GetJobRequest; import org.cloudfoundry.client.v2.organizations.CreateOrganizationRequest; @@ -31,6 +32,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +@RequiresV2Api public final class JobsTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/OrganizationQuotaDefinitionsTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/OrganizationQuotaDefinitionsTest.java index 8a435cfa81..b99f91e307 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/OrganizationQuotaDefinitionsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/OrganizationQuotaDefinitionsTest.java @@ -20,6 +20,7 @@ import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.organizationquotadefinitions.CreateOrganizationQuotaDefinitionRequest; import org.cloudfoundry.client.v2.organizationquotadefinitions.CreateOrganizationQuotaDefinitionResponse; @@ -37,6 +38,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +@RequiresV2Api public final class OrganizationQuotaDefinitionsTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/OrganizationsTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/OrganizationsTest.java index e1d1762e64..3c8740d0fb 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/OrganizationsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/OrganizationsTest.java @@ -22,6 +22,7 @@ import java.time.Duration; import java.util.function.UnaryOperator; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.applications.CreateApplicationRequest; import org.cloudfoundry.client.v2.applications.CreateApplicationResponse; @@ -98,6 +99,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +@RequiresV2Api public final class OrganizationsTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/PrivateDomainsTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/PrivateDomainsTest.java index 587cfa35ae..4dde9496a3 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/PrivateDomainsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/PrivateDomainsTest.java @@ -23,6 +23,7 @@ import java.time.Duration; import java.util.function.Consumer; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.organizations.AssociateOrganizationAuditorRequest; import org.cloudfoundry.client.v2.organizations.AssociateOrganizationAuditorResponse; @@ -57,6 +58,7 @@ import reactor.test.StepVerifier; import reactor.util.function.Tuple2; +@RequiresV2Api public final class PrivateDomainsTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/RouteMappingsTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/RouteMappingsTest.java index 1056e98304..9b31ddf78a 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/RouteMappingsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/RouteMappingsTest.java @@ -22,6 +22,7 @@ import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.applications.CreateApplicationRequest; import org.cloudfoundry.client.v2.applications.CreateApplicationResponse; @@ -43,6 +44,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +@RequiresV2Api public final class RouteMappingsTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/RoutesTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/RoutesTest.java index 3ee5650893..cb974f35e6 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/RoutesTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/RoutesTest.java @@ -23,6 +23,7 @@ import java.time.Duration; import java.util.function.Consumer; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.applications.CreateApplicationRequest; import org.cloudfoundry.client.v2.applications.CreateApplicationResponse; @@ -53,6 +54,7 @@ import reactor.test.StepVerifier; import reactor.util.function.Tuple3; +@RequiresV2Api public final class RoutesTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/SecurityGroupsTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/SecurityGroupsTest.java index 64ee5df01a..ba249ceff2 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/SecurityGroupsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/SecurityGroupsTest.java @@ -21,6 +21,7 @@ import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.securitygroups.AssociateSecurityGroupSpaceRequest; import org.cloudfoundry.client.v2.securitygroups.AssociateSecurityGroupSpaceResponse; @@ -55,6 +56,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +@RequiresV2Api public final class SecurityGroupsTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/ServiceBindingsTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/ServiceBindingsTest.java index 668976376d..f596238704 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/ServiceBindingsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/ServiceBindingsTest.java @@ -24,6 +24,7 @@ import java.util.Optional; import java.util.function.Consumer; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.applications.CreateApplicationRequest; import org.cloudfoundry.client.v2.applications.CreateApplicationResponse; @@ -49,6 +50,7 @@ import reactor.util.function.Tuple2; import reactor.util.function.Tuple3; +@RequiresV2Api public final class ServiceBindingsTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/ServiceBrokersTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/ServiceBrokersTest.java index 214577e4a9..61320ccd50 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/ServiceBrokersTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/ServiceBrokersTest.java @@ -26,6 +26,7 @@ import org.cloudfoundry.AbstractIntegrationTest; import org.cloudfoundry.ApplicationUtils; import org.cloudfoundry.CleanupCloudFoundryAfterClass; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.ServiceBrokerUtils; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.servicebrokers.CreateServiceBrokerRequest; @@ -45,6 +46,7 @@ import reactor.test.StepVerifier; @CleanupCloudFoundryAfterClass +@RequiresV2Api public final class ServiceBrokersTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/ServiceInstancesTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/ServiceInstancesTest.java index ed152f87a3..c3620f4957 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/ServiceInstancesTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/ServiceInstancesTest.java @@ -24,6 +24,7 @@ import org.cloudfoundry.AbstractIntegrationTest; import org.cloudfoundry.CloudFoundryVersion; import org.cloudfoundry.IfCloudFoundryVersion; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.applications.CreateApplicationRequest; import org.cloudfoundry.client.v2.applications.CreateApplicationResponse; @@ -65,6 +66,7 @@ import reactor.test.StepVerifier; import reactor.util.function.Tuples; +@RequiresV2Api public final class ServiceInstancesTest extends AbstractIntegrationTest { private static final String DEFAULT_ROUTER_GROUP = "default-tcp"; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/ServiceKeysTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/ServiceKeysTest.java index 58067e0d31..3ec01e0d95 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/ServiceKeysTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/ServiceKeysTest.java @@ -20,6 +20,7 @@ import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.serviceinstances.CreateServiceInstanceRequest; import org.cloudfoundry.client.v2.serviceinstances.CreateServiceInstanceResponse; @@ -42,6 +43,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +@RequiresV2Api public final class ServiceKeysTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/ServicePlanVisibilitiesTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/ServicePlanVisibilitiesTest.java index 910294d42a..6b0ea71e72 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/ServicePlanVisibilitiesTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/ServicePlanVisibilitiesTest.java @@ -22,6 +22,7 @@ import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.ServiceBrokerUtils; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.organizations.CreateOrganizationRequest; @@ -47,6 +48,7 @@ import reactor.test.StepVerifier; import reactor.util.function.Tuples; +@RequiresV2Api public final class ServicePlanVisibilitiesTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; @@ -76,7 +78,7 @@ public void create() { serviceName, spaceId, false)) - .block(Duration.ofMinutes(5)); + .block(Duration.ofMinutes(10)); Mono.zip( this.organizationId, @@ -141,7 +143,7 @@ public void delete() { serviceName, spaceId, false)) - .block(Duration.ofMinutes(5)); + .block(Duration.ofMinutes(10)); Mono.zip( this.organizationId, @@ -275,7 +277,7 @@ public void get() { serviceName, spaceId, false)) - .block(Duration.ofMinutes(5)); + .block(Duration.ofMinutes(10)); Mono.zip( this.organizationId, @@ -498,7 +500,7 @@ public void listFilterByServicePlanId() { serviceName, spaceId, false)) - .block(Duration.ofMinutes(5)); + .block(Duration.ofMinutes(10)); Mono.zip( this.organizationId, @@ -571,7 +573,7 @@ public void update() { serviceName, spaceId, false)) - .block(Duration.ofMinutes(5)); + .block(Duration.ofMinutes(10)); Mono.zip( this.organizationId, diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/ServicePlansTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/ServicePlansTest.java index c8f4db7e14..d0cc34c36b 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/ServicePlansTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/ServicePlansTest.java @@ -22,6 +22,7 @@ import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.ServiceBrokerUtils; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.applications.CreateApplicationRequest; @@ -53,6 +54,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +@RequiresV2Api public final class ServicePlansTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/ServiceUsageEventsTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/ServiceUsageEventsTest.java index 987de74b28..56ddd5bbfc 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/ServiceUsageEventsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/ServiceUsageEventsTest.java @@ -21,6 +21,7 @@ import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; import org.cloudfoundry.NameFactory; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.serviceinstances.CreateServiceInstanceRequest; import org.cloudfoundry.client.v2.serviceinstances.CreateServiceInstanceResponse; @@ -40,6 +41,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +@RequiresV2Api public final class ServiceUsageEventsTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/ServicesTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/ServicesTest.java index 9af396d315..5410c1a951 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/ServicesTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/ServicesTest.java @@ -22,6 +22,7 @@ import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.ServiceBrokerUtils; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.serviceinstances.CreateServiceInstanceRequest; @@ -46,6 +47,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +@RequiresV2Api public final class ServicesTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/SharedDomainsTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/SharedDomainsTest.java index 063992a657..bf60654252 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/SharedDomainsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/SharedDomainsTest.java @@ -19,6 +19,7 @@ import java.time.Duration; import java.util.Optional; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.shareddomains.CreateSharedDomainRequest; import org.cloudfoundry.client.v2.shareddomains.CreateSharedDomainResponse; @@ -37,6 +38,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +@RequiresV2Api public final class SharedDomainsTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/SpaceQuotaDefinitionsTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/SpaceQuotaDefinitionsTest.java index 47c9ae0f5f..ebe8ca1b61 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/SpaceQuotaDefinitionsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/SpaceQuotaDefinitionsTest.java @@ -20,6 +20,7 @@ import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.applications.CreateApplicationRequest; import org.cloudfoundry.client.v2.applications.CreateApplicationResponse; @@ -48,6 +49,7 @@ import reactor.test.StepVerifier; import reactor.util.function.Tuples; +@RequiresV2Api public final class SpaceQuotaDefinitionsTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/SpacesTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/SpacesTest.java index 4193ec53d6..4561f496e3 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/SpacesTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/SpacesTest.java @@ -22,11 +22,14 @@ import java.time.Duration; import java.time.Instant; +import java.util.Arrays; import java.util.Collections; import java.util.Date; +import java.util.List; import java.util.function.Function; import java.util.function.UnaryOperator; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.applications.ApplicationResource; import org.cloudfoundry.client.v2.applications.CreateApplicationRequest; @@ -108,7 +111,6 @@ import org.cloudfoundry.util.JobUtils; import org.cloudfoundry.util.PaginationUtils; import org.cloudfoundry.util.ResourceUtils; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import reactor.core.publisher.Flux; @@ -116,6 +118,7 @@ import reactor.test.StepVerifier; import reactor.util.function.Tuple2; +@RequiresV2Api public final class SpacesTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; @@ -1883,13 +1886,10 @@ public void listServicesFilterByLabel() { .verify(Duration.ofMinutes(5)); } - // TODO: Await https://github.com/cloudfoundry/cloud_controller_ng/issues/856 for this test to - // work - @Disabled( - "Await https://github.com/cloudfoundry/cloud_controller_ng/issues/856 for this test to" - + " work") @Test public void listServicesFilterByServiceBrokerId() { + List expectedValues = + Arrays.asList(this.serviceName, this.serviceName + "-shareable"); Mono.zip(this.serviceBrokerId, this.spaceId) .flatMapMany( function( @@ -1901,9 +1901,10 @@ public void listServicesFilterByServiceBrokerId() { builder.serviceBrokerId(serviceBrokerId)))) .map(response -> response.getEntity().getLabel()) .as(StepVerifier::create) - .expectNext(this.serviceName) + .consumeNextWith(element -> assertThat(element).isIn(expectedValues)) + .consumeNextWith(element -> assertThat(element).isIn(expectedValues)) .expectComplete() - .verify(Duration.ofMinutes(5)); + .verify(Duration.ofMinutes(10)); } @Test diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/StacksTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/StacksTest.java index cbd3ac2d29..3360a86a3f 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/StacksTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/StacksTest.java @@ -20,6 +20,7 @@ import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.stacks.CreateStackRequest; import org.cloudfoundry.client.v2.stacks.CreateStackResponse; @@ -38,6 +39,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +@RequiresV2Api public final class StacksTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/UserProvidedServicesTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/UserProvidedServicesTest.java index 555741d634..a850b86141 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/UserProvidedServicesTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/UserProvidedServicesTest.java @@ -24,6 +24,7 @@ import java.util.Collections; import java.util.function.Consumer; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.applications.CreateApplicationRequest; import org.cloudfoundry.client.v2.applications.CreateApplicationResponse; @@ -56,6 +57,7 @@ import reactor.util.function.Tuple3; import reactor.util.function.Tuples; +@RequiresV2Api public final class UserProvidedServicesTest extends AbstractIntegrationTest { private static final String DEFAULT_ROUTER_GROUP = "default-tcp"; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/UsersTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/UsersTest.java index 27f1232c3d..08efb1965e 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/UsersTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/UsersTest.java @@ -20,6 +20,7 @@ import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.applications.CreateApplicationRequest; import org.cloudfoundry.client.v2.applications.CreateApplicationResponse; @@ -80,6 +81,7 @@ import reactor.test.StepVerifier; import reactor.util.function.Tuples; +@RequiresV2Api public final class UsersTest extends AbstractIntegrationTest { private static final String STATUS_FILTER = "active"; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v3/AdminTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v3/AdminTest.java index d185f15dea..bdbc6778c8 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v3/AdminTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v3/AdminTest.java @@ -21,6 +21,7 @@ import org.cloudfoundry.ApplicationUtils; import org.cloudfoundry.CloudFoundryVersion; import org.cloudfoundry.IfCloudFoundryVersion; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v3.admin.ClearBuildpackCacheRequest; import org.cloudfoundry.util.JobUtils; @@ -29,6 +30,8 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +@RequiresV2Api +// TODO Does it really require V2? public final class AdminTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v3/DeploymentsTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v3/DeploymentsTest.java index d823ad7b1c..bcec497842 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v3/DeploymentsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v3/DeploymentsTest.java @@ -25,6 +25,7 @@ import org.cloudfoundry.CleanupCloudFoundryAfterClass; import org.cloudfoundry.CloudFoundryVersion; import org.cloudfoundry.IfCloudFoundryVersion; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v3.applications.GetApplicationCurrentDropletRequest; import org.cloudfoundry.client.v3.applications.GetApplicationCurrentDropletResponse; @@ -52,6 +53,7 @@ import reactor.test.StepVerifier; @IfCloudFoundryVersion(greaterThanOrEqualTo = CloudFoundryVersion.PCF_2_4) +@RequiresV2Api // Due to ApplicationUtils @CleanupCloudFoundryAfterClass public final class DeploymentsTest extends AbstractIntegrationTest { diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v3/IsolationSegmentsTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v3/IsolationSegmentsTest.java index 0f0a56fc1c..580fb30214 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v3/IsolationSegmentsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v3/IsolationSegmentsTest.java @@ -22,6 +22,7 @@ import org.cloudfoundry.AbstractIntegrationTest; import org.cloudfoundry.CloudFoundryVersion; import org.cloudfoundry.IfCloudFoundryVersion; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.organizations.CreateOrganizationRequest; import org.cloudfoundry.client.v2.organizations.CreateOrganizationResponse; @@ -53,6 +54,7 @@ import reactor.test.StepVerifier; @IfCloudFoundryVersion(greaterThanOrEqualTo = CloudFoundryVersion.PCF_1_11) +@RequiresV2Api public final class IsolationSegmentsTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v3/OrganizationsTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v3/OrganizationsTest.java index a1fbd52f3d..6843271134 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v3/OrganizationsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v3/OrganizationsTest.java @@ -24,6 +24,7 @@ import org.cloudfoundry.ApplicationUtils; import org.cloudfoundry.CloudFoundryVersion; import org.cloudfoundry.IfCloudFoundryVersion; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v3.domains.CreateDomainRequest; import org.cloudfoundry.client.v3.domains.CreateDomainResponse; @@ -57,6 +58,7 @@ import reactor.test.StepVerifier; @IfCloudFoundryVersion(greaterThanOrEqualTo = CloudFoundryVersion.PCF_1_12) +@RequiresV2Api public final class OrganizationsTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; @@ -166,9 +168,12 @@ public void get() { @IfCloudFoundryVersion(greaterThanOrEqualTo = CloudFoundryVersion.PCF_2_7) @Test + /* the default domain name is configured by the admin during deployment of the cf landscape. + * There is no way to guarantee that it matches the APIHOST or any substring of it. + * Therefore we only check if the endpoint can be called and returns something. + */ public void getDefaultDomain() { String organizationName = this.nameFactory.getOrganizationName(); - createOrganizationId(this.cloudFoundryClient, organizationName) .flatMap( organizationId -> @@ -180,8 +185,7 @@ public void getDefaultDomain() { .build())) .map(GetOrganizationDefaultDomainResponse::getName) .as(StepVerifier::create) - .consumeNextWith( - name -> assertThat(name).contains("apps.", ".shepherd.tanzu.broadcom.net")) + .consumeNextWith(name -> assertThat(name).isNotEmpty()) .expectComplete() .verify(Duration.ofMinutes(5)); } diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v3/PackagesTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v3/PackagesTest.java index 967923af84..e35bf3a026 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v3/PackagesTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v3/PackagesTest.java @@ -30,9 +30,9 @@ import org.cloudfoundry.client.v3.packages.CreatePackageRequest; import org.cloudfoundry.client.v3.packages.GetPackageRequest; import org.cloudfoundry.client.v3.packages.Package; +import org.cloudfoundry.client.v3.packages.PackageRelationships; import org.cloudfoundry.client.v3.packages.PackageType; import org.cloudfoundry.client.v3.packages.UploadPackageRequest; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; @@ -40,7 +40,6 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -@Disabled("Until Packages are no longer experimental") public final class PackagesTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; @@ -81,6 +80,19 @@ public void upload() { .create( CreatePackageRequest.builder() .type(PackageType.BITS) + .relationships( + PackageRelationships.builder() + .application( + ToOneRelationship + .builder() + .data( + Relationship + .builder() + .id( + applicationId) + .build()) + .build()) + .build()) .build())) .map(Package::getId) .flatMap( diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v3/ProcessesTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v3/ProcessesTest.java index e4a4184e85..8e3cbba3cb 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v3/ProcessesTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v3/ProcessesTest.java @@ -25,6 +25,7 @@ import org.cloudfoundry.CleanupCloudFoundryAfterClass; import org.cloudfoundry.CloudFoundryVersion; import org.cloudfoundry.IfCloudFoundryVersion; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v3.applications.GetApplicationProcessRequest; import org.cloudfoundry.client.v3.applications.GetApplicationProcessResponse; @@ -43,6 +44,7 @@ import reactor.test.StepVerifier; @IfCloudFoundryVersion(greaterThanOrEqualTo = CloudFoundryVersion.PCF_2_0) +@RequiresV2Api // Due to ApplicationUtils @CleanupCloudFoundryAfterClass public final class ProcessesTest extends AbstractIntegrationTest { 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 d3f5bf4ff6..05017449a7 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.CAPI_2_248) + @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.CAPI_2_248) + @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) diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v3/ServiceBindingsTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v3/ServiceBindingsTest.java index 81477aef52..95d107a7ca 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v3/ServiceBindingsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v3/ServiceBindingsTest.java @@ -25,6 +25,7 @@ import org.cloudfoundry.AbstractIntegrationTest; import org.cloudfoundry.CloudFoundryVersion; import org.cloudfoundry.IfCloudFoundryVersion; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v3.servicebindings.CreateServiceBindingRequest; import org.cloudfoundry.client.v3.servicebindings.DeleteServiceBindingRequest; @@ -54,6 +55,7 @@ import reactor.test.StepVerifier; @IfCloudFoundryVersion(greaterThanOrEqualTo = CloudFoundryVersion.PCF_2_11) +@RequiresV2Api public class ServiceBindingsTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; @@ -366,7 +368,7 @@ public void getDetails() { .as(StepVerifier::create) .expectNext(Collections.singletonMap("foo", "bar")) .expectComplete() - .verify(Duration.ofMinutes(5)); + .verify(Duration.ofMinutes(10)); } private static Mono createManagedServiceInstanceId( diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v3/ServiceBrokersTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v3/ServiceBrokersTest.java index 656a0b0efe..4a13ea744c 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v3/ServiceBrokersTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v3/ServiceBrokersTest.java @@ -28,6 +28,7 @@ import org.cloudfoundry.CleanupCloudFoundryAfterClass; import org.cloudfoundry.CloudFoundryVersion; import org.cloudfoundry.IfCloudFoundryVersion; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.ServiceBrokerUtils; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v3.servicebrokers.BasicAuthentication; @@ -50,6 +51,7 @@ import reactor.test.StepVerifier; @IfCloudFoundryVersion(greaterThanOrEqualTo = CloudFoundryVersion.PCF_2_10) +@RequiresV2Api @CleanupCloudFoundryAfterClass public final class ServiceBrokersTest extends AbstractIntegrationTest { diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v3/ServiceInstancesTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v3/ServiceInstancesTest.java index 507b288a65..1af91bbdf8 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v3/ServiceInstancesTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v3/ServiceInstancesTest.java @@ -25,6 +25,7 @@ import org.cloudfoundry.AbstractIntegrationTest; import org.cloudfoundry.CloudFoundryVersion; import org.cloudfoundry.IfCloudFoundryVersion; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v3.serviceinstances.CreateServiceInstanceRequest; import org.cloudfoundry.client.v3.serviceinstances.CreateServiceInstanceResponse; @@ -57,6 +58,7 @@ import reactor.test.StepVerifier; @IfCloudFoundryVersion(greaterThanOrEqualTo = CloudFoundryVersion.PCF_2_1) +@RequiresV2Api public final class ServiceInstancesTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v3/ServiceOfferingsTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v3/ServiceOfferingsTest.java index f0b67a37b4..fec36409fe 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v3/ServiceOfferingsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v3/ServiceOfferingsTest.java @@ -24,6 +24,7 @@ import org.cloudfoundry.AbstractIntegrationTest; import org.cloudfoundry.CloudFoundryVersion; import org.cloudfoundry.IfCloudFoundryVersion; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.ServiceBrokerUtils; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v3.serviceofferings.DeleteServiceOfferingRequest; @@ -43,6 +44,7 @@ import reactor.test.StepVerifier; @IfCloudFoundryVersion(greaterThanOrEqualTo = CloudFoundryVersion.PCF_2_10) +@RequiresV2Api public final class ServiceOfferingsTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v3/ServicePlansTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v3/ServicePlansTest.java index 5e37753b66..fd820af7f3 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v3/ServicePlansTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v3/ServicePlansTest.java @@ -24,6 +24,7 @@ import org.cloudfoundry.AbstractIntegrationTest; import org.cloudfoundry.CloudFoundryVersion; import org.cloudfoundry.IfCloudFoundryVersion; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.ServiceBrokerUtils; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v3.serviceplans.DeleteServicePlanRequest; @@ -45,6 +46,7 @@ import reactor.test.StepVerifier; @IfCloudFoundryVersion(greaterThanOrEqualTo = CloudFoundryVersion.PCF_2_10) +@RequiresV2Api public final class ServicePlansTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; @@ -78,7 +80,7 @@ public void delete() { serviceName, spaceId, true)) - .block(Duration.ofMinutes(5)); + .block(Duration.ofMinutes(10)); getServicePlanId(this.cloudFoundryClient, serviceBrokerMetadata.serviceBrokerId) .flatMap( @@ -287,7 +289,7 @@ public void update() { serviceName, spaceId, true)) - .block(Duration.ofMinutes(5)); + .block(Duration.ofMinutes(10)); getServicePlanId(this.cloudFoundryClient, serviceBrokerMetadata.serviceBrokerId) .flatMap( diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v3/ServiceRouteBindingsTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v3/ServiceRouteBindingsTest.java new file mode 100644 index 0000000000..d00b79db9a --- /dev/null +++ b/integration-test/src/test/java/org/cloudfoundry/client/v3/ServiceRouteBindingsTest.java @@ -0,0 +1,505 @@ +/* + * Copyright 2013-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.client.v3; + +import static org.cloudfoundry.util.tuple.TupleUtils.function; + +import java.time.Duration; +import java.util.Collections; +import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.CloudFoundryVersion; +import org.cloudfoundry.IfCloudFoundryVersion; +import org.cloudfoundry.Nullable; +import org.cloudfoundry.client.CloudFoundryClient; +import org.cloudfoundry.client.v3.domains.CreateDomainRequest; +import org.cloudfoundry.client.v3.domains.CreateDomainResponse; +import org.cloudfoundry.client.v3.domains.DomainRelationships; +import org.cloudfoundry.client.v3.routes.CreateRouteRequest; +import org.cloudfoundry.client.v3.routes.CreateRouteResponse; +import org.cloudfoundry.client.v3.routes.RouteRelationships; +import org.cloudfoundry.client.v3.serviceinstances.CreateServiceInstanceRequest; +import org.cloudfoundry.client.v3.serviceinstances.CreateServiceInstanceResponse; +import org.cloudfoundry.client.v3.serviceinstances.ServiceInstance; +import org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceRelationships; +import org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceType; +import org.cloudfoundry.client.v3.serviceroutebindings.CreateServiceRouteBindingRequest; +import org.cloudfoundry.client.v3.serviceroutebindings.CreateServiceRouteBindingResponse; +import org.cloudfoundry.client.v3.serviceroutebindings.DeleteServiceRouteBindingRequest; +import org.cloudfoundry.client.v3.serviceroutebindings.GetServiceRouteBindingRequest; +import org.cloudfoundry.client.v3.serviceroutebindings.ListServiceRouteBindingsRequest; +import org.cloudfoundry.client.v3.serviceroutebindings.ServiceRouteBinding; +import org.cloudfoundry.client.v3.serviceroutebindings.ServiceRouteBindingRelationships; +import org.cloudfoundry.client.v3.serviceroutebindings.ServiceRouteBindingResource; +import org.cloudfoundry.client.v3.serviceroutebindings.UpdateServiceRouteBindingRequest; +import org.cloudfoundry.util.JobUtils; +import org.cloudfoundry.util.PaginationUtils; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +@IfCloudFoundryVersion(greaterThanOrEqualTo = CloudFoundryVersion.PCF_2_11) +public class ServiceRouteBindingsTest extends AbstractIntegrationTest { + + @Autowired private CloudFoundryClient cloudFoundryClient; + + @Autowired private Mono organizationId; + + @Autowired private Mono spaceId; + + @Test + public void create() { + String domainName = this.nameFactory.getDomainName(); + String serviceInstanceName = this.nameFactory.getServiceInstanceName(); + + this.organizationId + .flatMap( + organizationId -> + Mono.zip( + createDomainId( + this.cloudFoundryClient, + domainName, + organizationId), + this.spaceId)) + .flatMap( + function( + (domainId, spaceId) -> + Mono.zip( + createRouteId( + this.cloudFoundryClient, domainId, spaceId), + createUserProvidedServiceInstanceId( + this.cloudFoundryClient, + serviceInstanceName, + spaceId)))) + .flatMap( + function( + (routeId, serviceInstanceId) -> + this.cloudFoundryClient + .serviceRouteBindingsV3() + .create( + CreateServiceRouteBindingRequest.builder() + .relationships( + ServiceRouteBindingRelationships + .builder() + .route( + ToOneRelationship + .builder() + .data( + Relationship + .builder() + .id( + routeId) + .build()) + .build()) + .serviceInstance( + ToOneRelationship + .builder() + .data( + Relationship + .builder() + .id( + serviceInstanceId) + .build()) + .build()) + .build()) + .build()))) + .map(response -> response.getServiceRouteBinding().isPresent()) + .as(StepVerifier::create) + .expectNext(true) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + + @Test + public void delete() { + String domainName = this.nameFactory.getDomainName(); + String serviceInstanceName = this.nameFactory.getServiceInstanceName(); + + this.organizationId + .flatMap( + organizationId -> + Mono.zip( + createDomainId( + this.cloudFoundryClient, + domainName, + organizationId), + this.spaceId)) + .flatMap( + function( + (domainId, spaceId) -> + Mono.zip( + createRouteId( + this.cloudFoundryClient, domainId, spaceId), + createUserProvidedServiceInstanceId( + this.cloudFoundryClient, + serviceInstanceName, + spaceId)))) + .flatMap( + function( + (routeId, serviceInstanceId) -> + createServiceRouteBinding( + this.cloudFoundryClient, + routeId, + serviceInstanceId) + .map(ServiceRouteBinding::getId) + .flatMap( + bindingId -> + this.cloudFoundryClient + .serviceRouteBindingsV3() + .delete( + DeleteServiceRouteBindingRequest + .builder() + .serviceRouteBindingId( + bindingId) + .build())) + .thenMany( + requestListServiceRouteBindings( + this.cloudFoundryClient, routeId)) + .hasElements())) + .as(StepVerifier::create) + .expectNext(false) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + + @Test + public void get() { + String domainName = this.nameFactory.getDomainName(); + String serviceInstanceName = this.nameFactory.getServiceInstanceName(); + + this.organizationId + .flatMap( + organizationId -> + Mono.zip( + createDomainId( + this.cloudFoundryClient, + domainName, + organizationId), + this.spaceId)) + .flatMap( + function( + (domainId, spaceId) -> + Mono.zip( + createRouteId( + this.cloudFoundryClient, domainId, spaceId), + createUserProvidedServiceInstanceId( + this.cloudFoundryClient, + serviceInstanceName, + spaceId)))) + .flatMap( + function( + (routeId, serviceInstanceId) -> + createServiceRouteBinding( + this.cloudFoundryClient, + routeId, + serviceInstanceId) + .map(ServiceRouteBinding::getId) + .flatMap( + bindingId -> + this.cloudFoundryClient + .serviceRouteBindingsV3() + .get( + GetServiceRouteBindingRequest + .builder() + .serviceRouteBindingId( + bindingId) + .build())) + .map( + binding -> + binding.getRelationships() + .getServiceInstance() + .getData() + .getId()) + .zipWith(Mono.just(serviceInstanceId)))) + .as(StepVerifier::create) + .consumeNextWith(tupleEquality()) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + + @Test + public void list() { + String domainName = this.nameFactory.getDomainName(); + String serviceInstanceName = this.nameFactory.getServiceInstanceName(); + + this.organizationId + .flatMap( + organizationId -> + Mono.zip( + createDomainId( + this.cloudFoundryClient, + domainName, + organizationId), + this.spaceId)) + .flatMap( + function( + (domainId, spaceId) -> + Mono.zip( + createRouteId( + this.cloudFoundryClient, domainId, spaceId), + createUserProvidedServiceInstanceId( + this.cloudFoundryClient, + serviceInstanceName, + spaceId)))) + .flatMap( + function( + (routeId, serviceInstanceId) -> + createServiceRouteBinding( + this.cloudFoundryClient, + routeId, + serviceInstanceId) + .thenMany( + requestListServiceRouteBindings( + this.cloudFoundryClient, routeId)) + .map(ServiceRouteBindingResource::getRelationships) + .map( + relationships -> + relationships + .getServiceInstance() + .getData() + .getId()) + .single() + .zipWith(Mono.just(serviceInstanceId)))) + .as(StepVerifier::create) + .consumeNextWith(tupleEquality()) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + + @Test + public void update() { + String domainName = this.nameFactory.getDomainName(); + String serviceInstanceName = this.nameFactory.getServiceInstanceName(); + + this.organizationId + .flatMap( + organizationId -> + Mono.zip( + createDomainId( + this.cloudFoundryClient, + domainName, + organizationId), + this.spaceId)) + .flatMap( + function( + (domainId, spaceId) -> + Mono.zip( + createRouteId( + this.cloudFoundryClient, domainId, spaceId), + createUserProvidedServiceInstanceId( + this.cloudFoundryClient, + serviceInstanceName, + spaceId)))) + .flatMap( + function( + (routeId, serviceInstanceId) -> + createServiceRouteBinding( + this.cloudFoundryClient, + routeId, + serviceInstanceId))) + .map(ServiceRouteBinding::getId) + .flatMap( + bindingId -> + this.cloudFoundryClient + .serviceRouteBindingsV3() + .update( + UpdateServiceRouteBindingRequest.builder() + .serviceRouteBindingId(bindingId) + .metadata( + Metadata.builder() + .label( + "test-label", + "test-label-value") + .build()) + .build())) + .map(binding -> binding.getMetadata().getLabels()) + .as(StepVerifier::create) + .expectNext(Collections.singletonMap("test-label", "test-label-value")) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + + private static Mono createDomainId( + CloudFoundryClient cloudFoundryClient, String domainName, String organizationId) { + return requestCreateDomain(cloudFoundryClient, domainName, organizationId) + .map(CreateDomainResponse::getId); + } + + private static Mono createRouteId( + CloudFoundryClient cloudFoundryClient, String domainId, String spaceId) { + return requestCreateRoute(cloudFoundryClient, domainId, spaceId) + .map(CreateRouteResponse::getId); + } + + private static Mono createUserProvidedServiceInstanceId( + CloudFoundryClient cloudFoundryClient, String serviceInstanceName, String spaceId) { + return requestCreateUserProvidedServiceInstance( + cloudFoundryClient, serviceInstanceName, spaceId) + .map( + response -> + response.getServiceInstance() + .map(ServiceInstance::getId) + .orElseThrow( + () -> + new IllegalStateException( + "Service instance not returned" + + " synchronously"))); + } + + private static Mono createServiceRouteBinding( + CloudFoundryClient cloudFoundryClient, String routeId, String serviceInstanceId) { + return requestCreateServiceRouteBinding(cloudFoundryClient, routeId, serviceInstanceId) + .flatMap( + response -> { + if (response.getJobId().isPresent()) { + return JobUtils.waitForCompletion( + cloudFoundryClient, + Duration.ofMinutes(5), + response.getJobId().get()) + .thenMany( + requestListServiceRouteBindingsByServiceInstance( + cloudFoundryClient, serviceInstanceId)) + .single(); + } + return Mono.just(response.getServiceRouteBinding().get()); + }); + } + + private static Mono requestCreateDomain( + CloudFoundryClient cloudFoundryClient, String domainName, String organizationId) { + return cloudFoundryClient + .domainsV3() + .create( + CreateDomainRequest.builder() + .internal(false) + .name(domainName) + .relationships( + DomainRelationships.builder() + .organization( + ToOneRelationship.builder() + .data( + Relationship.builder() + .id(organizationId) + .build()) + .build()) + .build()) + .build()); + } + + private static Mono requestCreateRoute( + CloudFoundryClient cloudFoundryClient, String domainId, String spaceId) { + return cloudFoundryClient + .routesV3() + .create( + CreateRouteRequest.builder() + .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()); + } + + private static Mono requestCreateUserProvidedServiceInstance( + CloudFoundryClient cloudFoundryClient, String serviceInstanceName, String spaceId) { + return cloudFoundryClient + .serviceInstancesV3() + .create( + CreateServiceInstanceRequest.builder() + .type(ServiceInstanceType.USER_PROVIDED) + .name(serviceInstanceName) + .routeServiceUrl("https://route-service.example.com") + .relationships( + ServiceInstanceRelationships.builder() + .space( + ToOneRelationship.builder() + .data( + Relationship.builder() + .id(spaceId) + .build()) + .build()) + .build()) + .build()); + } + + private static Mono requestCreateServiceRouteBinding( + CloudFoundryClient cloudFoundryClient, String routeId, String serviceInstanceId) { + return cloudFoundryClient + .serviceRouteBindingsV3() + .create( + CreateServiceRouteBindingRequest.builder() + .relationships( + ServiceRouteBindingRelationships.builder() + .route( + ToOneRelationship.builder() + .data( + Relationship.builder() + .id(routeId) + .build()) + .build()) + .serviceInstance( + ToOneRelationship.builder() + .data( + Relationship.builder() + .id( + serviceInstanceId) + .build()) + .build()) + .build()) + .build()); + } + + private static Flux requestListServiceRouteBindings( + CloudFoundryClient cloudFoundryClient, @Nullable String routeId) { + return PaginationUtils.requestClientV3Resources( + page -> { + ListServiceRouteBindingsRequest.Builder builder = + ListServiceRouteBindingsRequest.builder().page(page); + if (routeId != null) { + builder.routeId(routeId); + } + return cloudFoundryClient.serviceRouteBindingsV3().list(builder.build()); + }); + } + + private static Flux + requestListServiceRouteBindingsByServiceInstance( + CloudFoundryClient cloudFoundryClient, String serviceInstanceId) { + return PaginationUtils.requestClientV3Resources( + page -> + cloudFoundryClient + .serviceRouteBindingsV3() + .list( + ListServiceRouteBindingsRequest.builder() + .page(page) + .serviceInstanceId(serviceInstanceId) + .build())); + } +} diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v3/TasksTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v3/TasksTest.java index 0e3fdada8c..ec5970a08b 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v3/TasksTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v3/TasksTest.java @@ -25,6 +25,7 @@ import org.cloudfoundry.CleanupCloudFoundryAfterClass; import org.cloudfoundry.CloudFoundryVersion; import org.cloudfoundry.IfCloudFoundryVersion; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v3.applications.ApplicationResource; import org.cloudfoundry.client.v3.applications.GetApplicationCurrentDropletRequest; @@ -52,6 +53,7 @@ @IfCloudFoundryVersion(greaterThanOrEqualTo = CloudFoundryVersion.PCF_1_12) @CleanupCloudFoundryAfterClass +@RequiresV2Api // Due to ApplicationUtils public final class TasksTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/logcache/v1/LogCacheTest.java b/integration-test/src/test/java/org/cloudfoundry/logcache/v1/LogCacheTest.java index 414af210bd..26e5143229 100644 --- a/integration-test/src/test/java/org/cloudfoundry/logcache/v1/LogCacheTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/logcache/v1/LogCacheTest.java @@ -29,6 +29,8 @@ import org.cloudfoundry.ApplicationUtils; import org.cloudfoundry.CloudFoundryVersion; import org.cloudfoundry.IfCloudFoundryVersion; +import org.cloudfoundry.RequiresMetricRegistrar; +import org.cloudfoundry.RequiresV2Api; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -37,6 +39,7 @@ import reactor.test.StepVerifier; @IfCloudFoundryVersion(greaterThanOrEqualTo = CloudFoundryVersion.PCF_2_9) +@RequiresV2Api public class LogCacheTest extends AbstractIntegrationTest { @Autowired LogCacheClient logCacheClient; @@ -83,6 +86,7 @@ public void meta() { } @Test + @RequiresMetricRegistrar public void readCounter() { final String name = this.nameFactory.getName("counter-"); final int delta = this.random.nextInt(1000); @@ -102,6 +106,7 @@ public void readCounter() { } @Test + @RequiresMetricRegistrar public void readEvent() { final String title = this.nameFactory.getName("event-"); final String body = "This is the body. " + new BigInteger(1024, this.random).toString(32); diff --git a/integration-test/src/test/java/org/cloudfoundry/operations/AdvancedTest.java b/integration-test/src/test/java/org/cloudfoundry/operations/AdvancedTest.java index 733e95d6bc..201b9a4988 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/AdvancedTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/AdvancedTest.java @@ -20,10 +20,12 @@ import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import reactor.test.StepVerifier; +@RequiresV2Api public final class AdvancedTest extends AbstractIntegrationTest { @Autowired private CloudFoundryOperations cloudFoundryOperations; diff --git a/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java b/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java index 37c701dbbe..ed8319bafc 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java @@ -29,7 +29,11 @@ import org.cloudfoundry.CleanupCloudFoundryAfterClass; import org.cloudfoundry.CloudFoundryVersion; import org.cloudfoundry.IfCloudFoundryVersion; +import org.cloudfoundry.RequiresTcpRouting; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; +import org.cloudfoundry.client.v3.applications.ApplicationFeatureResource; +import org.cloudfoundry.client.v3.applications.ListApplicationFeaturesRequest; import org.cloudfoundry.operations.applications.ApplicationDetail; import org.cloudfoundry.operations.applications.ApplicationEnvironments; import org.cloudfoundry.operations.applications.ApplicationEvent; @@ -79,6 +83,7 @@ import org.cloudfoundry.operations.services.GetServiceInstanceRequest; import org.cloudfoundry.operations.services.ServiceInstance; import org.cloudfoundry.util.FluentMap; +import org.cloudfoundry.util.PaginationUtils; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -88,6 +93,7 @@ import reactor.test.StepVerifier; @CleanupCloudFoundryAfterClass +@RequiresV2Api public final class ApplicationsTest extends AbstractIntegrationTest { private static final String DEFAULT_ROUTER_GROUP = "default-tcp"; @@ -350,6 +356,7 @@ public void getManifest() throws IOException { } @Test + @RequiresTcpRouting public void getManifestForTcpRoute() throws IOException { String applicationName = this.nameFactory.getApplicationName(); @@ -436,6 +443,7 @@ public void getStopped() throws IOException { } @Test + @RequiresTcpRouting public void getTcp() throws IOException { String applicationName = this.nameFactory.getApplicationName(); String domainName = this.nameFactory.getDomainName(); @@ -745,6 +753,59 @@ public void pushManifestV3() throws IOException { .verify(Duration.ofMinutes(5)); } + @Test + @IfCloudFoundryVersion(greaterThanOrEqualTo = CloudFoundryVersion.PCF_4_v2) + public void pushManifestV3WithFeature() throws IOException { + String applicationName = this.nameFactory.getApplicationName(); + + final String featureKey = "ssh"; + final boolean featureValue = false; + ManifestV3 manifest = + ManifestV3.builder() + .application( + ManifestV3Application.builder() + .buildpack("staticfile_buildpack") + .disk(512) + .healthCheckType(ApplicationHealthCheck.PORT) + .memory(64) + .name(applicationName) + .feature(featureKey, false) + .path( + new ClassPathResource("test-application.zip") + .getFile() + .toPath()) + .build()) + .build(); + + this.cloudFoundryOperations + .applications() + .pushManifestV3(PushManifestV3Request.builder().manifest(manifest).build()) + .then( + this.cloudFoundryOperations + .applications() + .get(GetApplicationRequest.builder().name(applicationName).build())) + .map(ApplicationDetail::getId) + .flatMapMany( + applicationId -> + PaginationUtils.requestClientV3Resources( + page -> + this.cloudFoundryClient + .applicationsV3() + .listFeatures( + ListApplicationFeaturesRequest + .builder() + .applicationId( + applicationId) + .page(page) + .build()))) + .filter(feature -> featureKey.equals(feature.getName())) + .map(ApplicationFeatureResource::getEnabled) + .as(StepVerifier::create) + .expectNext(featureValue) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + @Test @IfCloudFoundryVersion(greaterThanOrEqualTo = CloudFoundryVersion.PCF_4_v2) public void pushManifestV3WithMetadata() throws IOException { @@ -1140,6 +1201,7 @@ public void pushRoutePath() throws IOException { } @Test + @RequiresTcpRouting public void pushTcpRoute() throws IOException { String applicationName = this.nameFactory.getApplicationName(); String domainName = this.nameFactory.getDomainName(); @@ -1263,6 +1325,7 @@ public void pushUpdateRoute() throws IOException { } @Test + @RequiresTcpRouting public void pushUpdateTcpRoute() throws IOException { String applicationName = this.nameFactory.getApplicationName(); String domainName = this.nameFactory.getDomainName(); @@ -1735,10 +1798,33 @@ public void terminateTask() throws IOException { .sequenceId(sequenceId) .build()) .thenReturn(sequenceId)) - .flatMapMany( + .flatMap( sequenceId -> - requestListTasks(this.cloudFoundryOperations, applicationName) - .filter(task -> sequenceId.equals(task.getSequenceId()))) + Mono.defer( + () -> + requestListTasks( + this.cloudFoundryOperations, + applicationName) + .filter( + task -> + sequenceId.equals( + task + .getSequenceId())) + .next() + .flatMap( + task -> + TaskState.CANCELING + .equals( + task + .getState()) + ? Mono + .empty() + : Mono.just( + task))) + .repeatWhenEmpty( + repeat -> + repeat.delayElements( + Duration.ofSeconds(2)))) .map(Task::getState) .as(StepVerifier::create) .expectNext(TaskState.FAILED) @@ -1957,7 +2043,7 @@ private static Mono createDockerApplication( .push( PushApplicationRequest.builder() .diskQuota(512) - .dockerImage("cloudfoundry/lattice-app") + .dockerImage("cloudfoundry/grace") .healthCheckType(ApplicationHealthCheck.PORT) .memory(64) .name(name) diff --git a/integration-test/src/test/java/org/cloudfoundry/operations/BuildpacksTest.java b/integration-test/src/test/java/org/cloudfoundry/operations/BuildpacksTest.java index 62991cef2e..7dde64ffee 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/BuildpacksTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/BuildpacksTest.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.operations.buildpacks.Buildpack; import org.cloudfoundry.operations.buildpacks.CreateBuildpackRequest; import org.cloudfoundry.operations.buildpacks.DeleteBuildpackRequest; @@ -31,6 +32,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +@RequiresV2Api public final class BuildpacksTest extends AbstractIntegrationTest { @Autowired private CloudFoundryOperations cloudFoundryOperations; diff --git a/integration-test/src/test/java/org/cloudfoundry/operations/DomainsTest.java b/integration-test/src/test/java/org/cloudfoundry/operations/DomainsTest.java index b75ca3f3a4..b7b16566ae 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/DomainsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/DomainsTest.java @@ -23,6 +23,7 @@ import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v3.ClientV3Exception; import org.cloudfoundry.client.v3.Error; @@ -40,6 +41,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +@RequiresV2Api public final class DomainsTest extends AbstractIntegrationTest { private static final String DEFAULT_ROUTER_GROUP = "default-tcp"; diff --git a/integration-test/src/test/java/org/cloudfoundry/operations/NetworkPoliciesTest.java b/integration-test/src/test/java/org/cloudfoundry/operations/NetworkPoliciesTest.java index 1643a3ef4f..1d32a910a2 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/NetworkPoliciesTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/NetworkPoliciesTest.java @@ -20,6 +20,7 @@ import org.cloudfoundry.AbstractIntegrationTest; import org.cloudfoundry.CloudFoundryVersion; import org.cloudfoundry.IfCloudFoundryVersion; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.applications.CreateApplicationRequest; import org.cloudfoundry.client.v2.applications.CreateApplicationResponse; @@ -34,6 +35,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +@RequiresV2Api public final class NetworkPoliciesTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/operations/OrganizationAdminTest.java b/integration-test/src/test/java/org/cloudfoundry/operations/OrganizationAdminTest.java index 3518683680..4a4325d191 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/OrganizationAdminTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/OrganizationAdminTest.java @@ -20,6 +20,7 @@ import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.operations.organizationadmin.CreateQuotaRequest; import org.cloudfoundry.operations.organizationadmin.DeleteQuotaRequest; import org.cloudfoundry.operations.organizationadmin.GetQuotaRequest; @@ -33,6 +34,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +@RequiresV2Api public final class OrganizationAdminTest extends AbstractIntegrationTest { @Autowired private CloudFoundryOperations cloudFoundryOperations; diff --git a/integration-test/src/test/java/org/cloudfoundry/operations/OrganizationsTest.java b/integration-test/src/test/java/org/cloudfoundry/operations/OrganizationsTest.java index 3e4e2b57a6..ebe3f90ebe 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/OrganizationsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/OrganizationsTest.java @@ -18,12 +18,14 @@ import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.operations.organizations.CreateOrganizationRequest; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +@RequiresV2Api public final class OrganizationsTest extends AbstractIntegrationTest { @Autowired private CloudFoundryOperations cloudFoundryOperations; diff --git a/integration-test/src/test/java/org/cloudfoundry/operations/RoutesTest.java b/integration-test/src/test/java/org/cloudfoundry/operations/RoutesTest.java index 19db1a53b1..76c16e9691 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/RoutesTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/RoutesTest.java @@ -28,6 +28,7 @@ import java.util.function.Predicate; import org.cloudfoundry.AbstractIntegrationTest; import org.cloudfoundry.CleanupCloudFoundryAfterClass; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.operations.applications.ApplicationHealthCheck; import org.cloudfoundry.operations.applications.PushApplicationRequest; import org.cloudfoundry.operations.domains.CreateDomainRequest; @@ -42,7 +43,6 @@ import org.cloudfoundry.operations.routes.UnmapRouteRequest; import org.cloudfoundry.operations.services.BindRouteServiceInstanceRequest; import org.cloudfoundry.operations.services.CreateUserProvidedServiceInstanceRequest; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; @@ -51,6 +51,7 @@ import reactor.test.StepVerifier; @CleanupCloudFoundryAfterClass +@RequiresV2Api public final class RoutesTest extends AbstractIntegrationTest { private static final String DEFAULT_ROUTER_GROUP = "default-tcp"; @@ -272,7 +273,6 @@ public void deleteInvalidDomain() { } @Test - @Disabled("fails often for no reasons") public void deleteOrphanedRoutes() { String domainName = this.nameFactory.getDomainName(); String hostName = this.nameFactory.getHostName(); diff --git a/integration-test/src/test/java/org/cloudfoundry/operations/ServiceAdminTest.java b/integration-test/src/test/java/org/cloudfoundry/operations/ServiceAdminTest.java index 7a8f1ec378..ebd89c92e3 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/ServiceAdminTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/ServiceAdminTest.java @@ -22,6 +22,7 @@ import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; import org.cloudfoundry.CleanupCloudFoundryAfterClass; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.ServiceBrokerUtils; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.spaces.CreateSpaceRequest; @@ -40,6 +41,7 @@ import reactor.test.StepVerifier; @CleanupCloudFoundryAfterClass +@RequiresV2Api public final class ServiceAdminTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/operations/ServicesTest.java b/integration-test/src/test/java/org/cloudfoundry/operations/ServicesTest.java index 520027128b..f81ddb1a67 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/ServicesTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/ServicesTest.java @@ -24,6 +24,7 @@ import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; import org.cloudfoundry.CleanupCloudFoundryAfterClass; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.servicebindings.ListServiceBindingsRequest; import org.cloudfoundry.client.v2.servicebindings.ServiceBindingResource; @@ -66,6 +67,7 @@ import reactor.test.StepVerifier; @CleanupCloudFoundryAfterClass +@RequiresV2Api public final class ServicesTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/operations/SpacesTest.java b/integration-test/src/test/java/org/cloudfoundry/operations/SpacesTest.java index fbb8dc765b..7f862e0478 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/SpacesTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/SpacesTest.java @@ -20,6 +20,7 @@ import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.operations.spaces.CreateSpaceRequest; import org.cloudfoundry.operations.spaces.GetSpaceRequest; import org.cloudfoundry.operations.spaces.SpaceDetail; @@ -27,6 +28,7 @@ import org.springframework.beans.factory.annotation.Autowired; import reactor.test.StepVerifier; +@RequiresV2Api public final class SpacesTest extends AbstractIntegrationTest { @Autowired private CloudFoundryOperations cloudFoundryOperations; diff --git a/integration-test/src/test/java/org/cloudfoundry/operations/UserAdminTest.java b/integration-test/src/test/java/org/cloudfoundry/operations/UserAdminTest.java index b0c12b8ea3..af534acda3 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/UserAdminTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/UserAdminTest.java @@ -20,6 +20,7 @@ import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresV2Api; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.organizations.ListOrganizationAuditorsRequest; import org.cloudfoundry.client.v2.organizations.ListOrganizationsRequest; @@ -47,6 +48,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +@RequiresV2Api public final class UserAdminTest extends AbstractIntegrationTest { @Autowired private CloudFoundryClient cloudFoundryClient; diff --git a/integration-test/src/test/java/org/cloudfoundry/routing/v1/RouterGroupsTest.java b/integration-test/src/test/java/org/cloudfoundry/routing/v1/RouterGroupsTest.java index 8253f29ad5..7a812c7e26 100644 --- a/integration-test/src/test/java/org/cloudfoundry/routing/v1/RouterGroupsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/routing/v1/RouterGroupsTest.java @@ -18,6 +18,7 @@ import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresTcpRouting; import org.cloudfoundry.routing.RoutingClient; import org.cloudfoundry.routing.v1.routergroups.ListRouterGroupsRequest; import org.cloudfoundry.routing.v1.routergroups.ListRouterGroupsResponse; @@ -29,6 +30,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +@RequiresTcpRouting public final class RouterGroupsTest extends AbstractIntegrationTest { private static final String DEFAULT_ROUTER_GROUP = "default-tcp"; diff --git a/integration-test/src/test/java/org/cloudfoundry/routing/v1/TcpRoutesTest.java b/integration-test/src/test/java/org/cloudfoundry/routing/v1/TcpRoutesTest.java index a562fa4223..6fb7af061b 100644 --- a/integration-test/src/test/java/org/cloudfoundry/routing/v1/TcpRoutesTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/routing/v1/TcpRoutesTest.java @@ -19,6 +19,7 @@ import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; import org.cloudfoundry.NameFactory; +import org.cloudfoundry.RequiresTcpRouting; import org.cloudfoundry.routing.RoutingClient; import org.cloudfoundry.routing.v1.routergroups.ListRouterGroupsRequest; import org.cloudfoundry.routing.v1.routergroups.ListRouterGroupsResponse; @@ -39,6 +40,7 @@ import reactor.test.StepVerifier; import reactor.util.retry.Retry; +@RequiresTcpRouting public final class TcpRoutesTest extends AbstractIntegrationTest { private static final String DEFAULT_ROUTER_GROUP = "default-tcp"; diff --git a/integration-test/src/test/java/org/cloudfoundry/uaa/AuthorizationsTest.java b/integration-test/src/test/java/org/cloudfoundry/uaa/AuthorizationsTest.java index 6480aef0b9..e0eac2d214 100644 --- a/integration-test/src/test/java/org/cloudfoundry/uaa/AuthorizationsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/uaa/AuthorizationsTest.java @@ -21,6 +21,7 @@ import java.time.Duration; import java.util.function.Consumer; import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.RequiresBrowserAuth; import org.cloudfoundry.uaa.authorizations.AuthorizeByAuthorizationCodeGrantApiRequest; import org.cloudfoundry.uaa.authorizations.AuthorizeByAuthorizationCodeGrantBrowserRequest; import org.cloudfoundry.uaa.authorizations.AuthorizeByAuthorizationCodeGrantHybridRequest; @@ -55,6 +56,7 @@ public void authorizeByAuthorizationCodeGrantApi() { } @Test + @RequiresBrowserAuth public void authorizeByAuthorizationCodeGrantBrowser() { this.uaaClient .authorizations() @@ -70,6 +72,7 @@ public void authorizeByAuthorizationCodeGrantBrowser() { } @Test + @RequiresBrowserAuth public void authorizeByAuthorizationCodeGrantHybrid() { this.uaaClient .authorizations() @@ -85,6 +88,7 @@ public void authorizeByAuthorizationCodeGrantHybrid() { } @Test + @RequiresBrowserAuth public void authorizeByImplicitGrantBrowser() { this.uaaClient .authorizations() @@ -100,6 +104,7 @@ public void authorizeByImplicitGrantBrowser() { } @Test + @RequiresBrowserAuth public void authorizeByOpenIdWithAuthorizationCodeGrant() { this.uaaClient .authorizations() @@ -116,6 +121,7 @@ public void authorizeByOpenIdWithAuthorizationCodeGrant() { } @Test + @RequiresBrowserAuth public void authorizeByOpenIdWithIdToken() { this.uaaClient .authorizations() @@ -132,6 +138,7 @@ public void authorizeByOpenIdWithIdToken() { } @Test + @RequiresBrowserAuth public void authorizeByOpenIdWithImplicitGrant() { this.uaaClient .authorizations() diff --git a/integration-test/src/test/java/org/cloudfoundry/uaa/ClientsTest.java b/integration-test/src/test/java/org/cloudfoundry/uaa/ClientsTest.java index 4b52e9e1a6..87a2cf8d9e 100644 --- a/integration-test/src/test/java/org/cloudfoundry/uaa/ClientsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/uaa/ClientsTest.java @@ -121,6 +121,7 @@ public void batchCreate() { .authorizedGrantType(PASSWORD) .clientId(clientId1) .clientSecret(clientSecret) + .allowPublic(false) .scopes("client.read", "client.write") .tokenSalt("test-token-salt") .build()) @@ -130,21 +131,33 @@ public void batchCreate() { .authorizedGrantTypes(PASSWORD, REFRESH_TOKEN) .clientId(clientId2) .clientSecret(clientSecret) + .allowPublic(true) .scope("client.write") .tokenSalt("filtered-test-token-salt") .build()) .build()) .flatMapIterable(BatchCreateClientsResponse::getClients) - .filter(client -> clientId1.equals(client.getClientId())) + .collectMap(Client::getClientId) .as(StepVerifier::create) .consumeNextWith( - response -> { - assertThat(response.getAuthorizedGrantTypes()) + clients -> { + Client client1 = clients.get(clientId1); + assertThat(client1).isNotNull(); + assertThat(client1.getAuthorizedGrantTypes()) .containsExactly(PASSWORD, REFRESH_TOKEN); - assertThat(response.getClientId()).isEqualTo(clientId1); - assertThat(response.getScopes()) + assertThat(client1.getScopes()) .containsExactly("client.read", "client.write"); - assertThat(response.getTokenSalt()).isEqualTo("test-token-salt"); + assertThat(client1.getTokenSalt()).isEqualTo("test-token-salt"); + assertThat(client1.getAllowPublic()).isFalse(); + + Client client2 = clients.get(clientId2); + assertThat(client2).isNotNull(); + assertThat(client2.getAuthorizedGrantTypes()) + .containsExactlyInAnyOrder(PASSWORD, REFRESH_TOKEN); + assertThat(client2.getScopes()).containsExactly("client.write"); + assertThat(client2.getTokenSalt()) + .isEqualTo("filtered-test-token-salt"); + assertThat(client2.getAllowPublic()).isTrue(); }) .expectComplete() .verify(Duration.ofMinutes(5)); diff --git a/integration-test/src/test/java/org/cloudfoundry/uaa/TokensTest.java b/integration-test/src/test/java/org/cloudfoundry/uaa/TokensTest.java index 39d956f801..0c9e107053 100644 --- a/integration-test/src/test/java/org/cloudfoundry/uaa/TokensTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/uaa/TokensTest.java @@ -125,11 +125,13 @@ public void getTokenByClientCredentials() { .verify(Duration.ofMinutes(5)); } - // TODO: Ready to Implement - Await https://github.com/cloudfoundry/cf-java-client/issues/862 to - // get passcode - @Disabled( - "Ready to Implement - Await https://github.com/cloudfoundry/cf-java-client/issues/862" - + " to get passcode") + // When implemented, will fix issue: https://github.com/cloudfoundry/cf-java-client/issues/910. + // Could be done similarly to "getTokenByAuthorizationCode". + // First user_token grant type must be implented. + // See: https://docs.cloudfoundry.org/api/uaa/index.html#user-token-grant + // Then this endpoint must be called first, to create the token. + // See: https://docs.cloudfoundry.org/api/uaa/index.html#saml2-bearer-grant + @Disabled("Ready to Implement") @Test public void getTokenByOneTimePasscode() { this.uaaClient diff --git a/pom.xml b/pom.xml index df2a45ed6e..96dd501d44 100644 --- a/pom.xml +++ b/pom.xml @@ -58,18 +58,18 @@ 1.8 UTF-8 - 2.7.17 + 2.7.18 - 1.27.1 - 3.13.0 + 1.28.0 + 3.20.0 1.3 - 2.10.1 - 2.16.1 + 2.12.2 + 2.19.0 0.10.2 - 0.12.6 - 5.10.1 + 0.12.7 + 5.14.4 5.8.0 - 2.2 + 2.4 3.7.1 3.0.2 @@ -124,6 +124,11 @@ commons-compress ${commons-compress.version} + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + org.atteo evo-inflector @@ -134,7 +139,22 @@ value ${immutables.version} - + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + +