diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/CloudFoundryClientCompatibilityChecker.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/CloudFoundryClientCompatibilityChecker.java index e2aa39459b..d540e1f70c 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/CloudFoundryClientCompatibilityChecker.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/CloudFoundryClientCompatibilityChecker.java @@ -20,27 +20,30 @@ import com.github.zafarkhaja.semver.Version; import org.cloudfoundry.client.CloudFoundryClient; +import org.cloudfoundry.client.GetRootRequest; +import org.cloudfoundry.client.Root; import org.cloudfoundry.client.v2.info.GetInfoRequest; import org.cloudfoundry.client.v2.info.Info; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; final class CloudFoundryClientCompatibilityChecker { private final Logger logger = LoggerFactory.getLogger("cloudfoundry-client.compatibility"); private final Info info; + private final Root root; - CloudFoundryClientCompatibilityChecker(Info info) { + CloudFoundryClientCompatibilityChecker(Info info, Root root) { this.info = info; + this.root = root; } void check() { - this.info - .get(GetInfoRequest.builder().build()) - .map(response -> 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..7a26cd13d2 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; @@ -207,7 +208,7 @@ public Builds builds() { @PostConstruct public void checkCompatibility() { - new CloudFoundryClientCompatibilityChecker(info()).check(); + new CloudFoundryClientCompatibilityChecker(info(), rootEndpoint()).check(); } @Override @@ -259,6 +260,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() { 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..6181dfff7a 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/CloudFoundryClient.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/CloudFoundryClient.java @@ -86,6 +86,8 @@ public interface CloudFoundryClient { */ String SUPPORTED_API_VERSION = "2.272.0"; + String SUPPORTED_API_VERSION_V3 = "3.216.0"; + /** * Main entry point to the Cloud Foundry Application Usage Events Client API */ @@ -171,6 +173,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 */ 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/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java b/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java index 58b01252f0..603c468207 100644 --- a/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java +++ b/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java @@ -38,6 +38,7 @@ 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; @@ -465,15 +466,38 @@ 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 Mono serverVersionV2( + @Qualifier("admin") CloudFoundryClient cloudFoundryClient) { + return cloudFoundryClient + .info() + .get(GetInfoRequest.builder().build()) + .flatMap( + response -> { + String version = response.getApiVersion(); + if (version == null || version.isEmpty()) { + this.logger.warn( + "calling v2 info endpoint but CF API v2 is disabled"); + return Mono.empty(); + } + return Mono.just(Version.valueOf(version)); + }); + } + + private Mono serverVersionV3( + @Qualifier("admin") CloudFoundryClient cloudFoundryClient) { + return cloudFoundryClient + .rootEndpoint() + .get(GetRootRequest.builder().build()) + .map(response -> Version.valueOf(response.getApiVersionV3())); + } + @Lazy @Bean(initMethod = "block") @DependsOn("cloudFoundryCleaner") 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..f12d833f2e 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 @@ -41,7 +41,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); })