diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/AbstractRootProvider.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/AbstractRootProvider.java index d718cd3f05..cac9c48de7 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/AbstractRootProvider.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/AbstractRootProvider.java @@ -18,6 +18,7 @@ import io.netty.handler.codec.http.HttpHeaders; import java.util.Optional; +import java.util.Queue; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.cloudfoundry.reactor.util.JsonCodec; @@ -77,6 +78,12 @@ public final Mono getRoot(String key, ConnectionContext connectionContex return connectionContext.getCacheDuration().map(cached::cache).orElseGet(cached::cache); } + @Override + public final Mono getRootKey(Queue key, ConnectionContext connectionContext) { + Mono cached = doGetRootKey(key, connectionContext); + return connectionContext.getCacheDuration().map(cached::cache).orElseGet(cached::cache); + } + @Override public final Mono getRoot(ConnectionContext connectionContext) { Mono cached = @@ -92,6 +99,9 @@ public final Mono getRoot(ConnectionContext connectionContext) { protected abstract Mono doGetRoot( String key, ConnectionContext connectionContext); + protected abstract Mono doGetRootKey( + Queue key, ConnectionContext connectionContext); + protected final UriComponents getRoot() { UriComponentsBuilder builder = UriComponentsBuilder.newInstance().scheme("https").host(getApiHost()); diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/RootProvider.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/RootProvider.java index 05ce06ad3c..969612a10a 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/RootProvider.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/RootProvider.java @@ -16,6 +16,7 @@ package org.cloudfoundry.reactor; +import java.util.Queue; import reactor.core.publisher.Mono; /** @@ -32,11 +33,24 @@ public interface RootProvider { Mono getRoot(ConnectionContext connectionContext); /** - * The normalized root for a given key + * The normalized root for a given key. + * The "href" entry for the given key is returned from the root endpoint. + * If the endpoint does not provide a port, it will be added at nomalisation. * - * @param key the key to look up root from - * @param connectionContext a {@link ConnectionContext} to be used if the roo needs to be retrieved via a network request + * @param key the key to look up from root + * @param connectionContext a {@link ConnectionContext} to be used if the root needs to be retrieved via a network request * @return the normalized API root */ Mono getRoot(String key, ConnectionContext connectionContext); + + /** + * The literal String value for a given key. May also access structured fields from the root endpoint, + * like "links.cloud_controller_v2.meta.version". + * Null values from the endpoint are translated to an empty String. + * + * @param keyList the key(s) to look up from root. Nested keys are added at the end of the queue. + * @param connectionContext a {@link ConnectionContext} to be used if the root needs to be retrieved via a network request + * @return the plain value for the given key + */ + Mono getRootKey(Queue keyList, ConnectionContext connectionContext); } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_DelegatingRootProvider.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_DelegatingRootProvider.java index 554eecfa1e..1459bd99f1 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_DelegatingRootProvider.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_DelegatingRootProvider.java @@ -17,65 +17,79 @@ package org.cloudfoundry.reactor; import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.LinkedList; +import java.util.Queue; + import org.immutables.value.Value; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; + import reactor.core.publisher.Mono; /** - * A {@link RootProvider} that returns endpoints by delegating to an {@link RootPayloadRootProvider} and then an {@link InfoPayloadRootProvider}. + * A {@link RootProvider} that returns endpoints by delegating to an + * {@link RootPayloadRootProvider} and then an {@link InfoPayloadRootProvider}. */ @Value.Immutable abstract class _DelegatingRootProvider extends AbstractRootProvider { - @Override - protected Mono doGetRoot(ConnectionContext connectionContext) { - return getRootPayloadRootProvider().doGetRoot(connectionContext) - .onErrorResume(t -> getInfoPayloadRootProvider().doGetRoot(connectionContext)); - } + @Override + protected Mono doGetRoot(ConnectionContext connectionContext) { + return getRootPayloadRootProvider().doGetRoot(connectionContext) + .onErrorResume(t -> getInfoPayloadRootProvider().doGetRoot(connectionContext)); + } + + @Override + protected Mono doGetRoot(String key, ConnectionContext connectionContext) { + return getRootPayloadRootProvider().doGetRoot(key, connectionContext).onErrorResume(t -> { + // if root does not return a value, try with info object + if ("cloud_controller_v2".equals(key)) { + return getInfoPayloadRootProvider().doGetRoot(connectionContext) + .map(uri -> UriComponentsBuilder.newInstance().uriComponents(uri).pathSegment("v2").build()); + } else if ("cloud_controller_v3".equals(key)) { + return getInfoPayloadRootProvider().doGetRoot(connectionContext) + .map(uri -> UriComponentsBuilder.newInstance().uriComponents(uri).pathSegment("v3").build()); + } else if ("logging".equals(key)) { + return getInfoPayloadRootProvider().doGetRoot("doppler_logging_endpoint", connectionContext); + } else if ("routing".equals(key)) { + return getInfoPayloadRootProvider().doGetRoot("routing_endpoint", connectionContext); + } else if ("uaa".equals(key)) { + return getInfoPayloadRootProvider().doGetRoot("token_endpoint", connectionContext); + } else if ("login".equals(key)) { + return getInfoPayloadRootProvider().doGetRoot("authorization_endpoint", connectionContext); + } else { + return getInfoPayloadRootProvider().doGetRoot(key, connectionContext); + } + }); + } + + @Override + protected Mono doGetRootKey(Queue keyList, ConnectionContext connectionContext) { + Queue keyCopy = new LinkedList<>(keyList); + return getRootPayloadRootProvider().doGetRootKey(keyList, connectionContext).onErrorResume(t -> { + return getInfoV3PayloadRootProvider().doGetRootKey(keyCopy, connectionContext); + }); + } - @Override - protected Mono doGetRoot(String key, ConnectionContext connectionContext) { - return getRootPayloadRootProvider().doGetRoot(key, connectionContext) - .onErrorResume(t -> { - if ("cloud_controller_v2".equals(key)) { - return getInfoPayloadRootProvider().doGetRoot(connectionContext) - .map(uri -> UriComponentsBuilder.newInstance().uriComponents(uri).pathSegment("v2").build()); - } else if ("cloud_controller_v3".equals(key)) { - return getInfoPayloadRootProvider().doGetRoot(connectionContext) - .map(uri -> UriComponentsBuilder.newInstance().uriComponents(uri).pathSegment("v3").build()); - } else if ("logging".equals(key)) { - return getInfoPayloadRootProvider().doGetRoot("doppler_logging_endpoint", connectionContext); - } else if ("routing".equals(key)) { - return getInfoPayloadRootProvider().doGetRoot("routing_endpoint", connectionContext); - } else if ("uaa".equals(key)) { - return getInfoPayloadRootProvider().doGetRoot("token_endpoint", connectionContext); - } else { - return getInfoPayloadRootProvider().doGetRoot(key, connectionContext); - } - }); - } + @Value.Derived + InfoPayloadRootProvider getInfoPayloadRootProvider() { + return InfoPayloadRootProvider.builder().apiHost(getApiHost()).objectMapper(getObjectMapper()).port(getPort()) + .secure(getSecure()).build(); + } - @Value.Derived - InfoPayloadRootProvider getInfoPayloadRootProvider() { - return InfoPayloadRootProvider.builder() - .apiHost(getApiHost()) - .objectMapper(getObjectMapper()) - .port(getPort()) - .secure(getSecure()) - .build(); - } + @Value.Derived + InfoV3PayloadRootProvider getInfoV3PayloadRootProvider() { + return InfoV3PayloadRootProvider.builder().apiHost(getApiHost()).objectMapper(getObjectMapper()).port(getPort()) + .secure(getSecure()).build(); + } - abstract ObjectMapper getObjectMapper(); + abstract ObjectMapper getObjectMapper(); - @Value.Derived - RootPayloadRootProvider getRootPayloadRootProvider() { - return RootPayloadRootProvider.builder() - .apiHost(getApiHost()) - .objectMapper(getObjectMapper()) - .port(getPort()) - .secure(getSecure()) - .build(); - } + @Value.Derived + RootPayloadRootProvider getRootPayloadRootProvider() { + return RootPayloadRootProvider.builder().apiHost(getApiHost()).objectMapper(getObjectMapper()).port(getPort()) + .secure(getSecure()).build(); + } } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_InfoPayloadRootProvider.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_InfoPayloadRootProvider.java index 27847909d4..d2aef1f9e5 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_InfoPayloadRootProvider.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_InfoPayloadRootProvider.java @@ -23,6 +23,7 @@ import reactor.core.publisher.Mono; import java.util.Map; +import java.util.Queue; /** * A {@link RootProvider} that returns endpoints extracted from the `/v2/info` API for the configured endpoint. @@ -45,6 +46,17 @@ protected Mono doGetRoot(String key, ConnectionContext connection }); } + protected Mono doGetRootKey(Queue keyList, ConnectionContext connectionContext) { + String key = keyList.poll(); + return getInfo(connectionContext) + .map(info -> { + if (!info.containsKey(key)) { + throw new IllegalArgumentException(String.format("Info payload does not contain key '%s'", key)); + } + return info.get(key); + }); + } + abstract ObjectMapper getObjectMapper(); private UriComponentsBuilder buildInfoUri(UriComponentsBuilder root) { diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_InfoV3PayloadRootProvider.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_InfoV3PayloadRootProvider.java new file mode 100644 index 0000000000..d44ace55a9 --- /dev/null +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_InfoV3PayloadRootProvider.java @@ -0,0 +1,101 @@ +/* + * 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; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.immutables.value.Value; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; +import reactor.core.publisher.Mono; + +import java.util.Map; +import java.util.Queue; + +/** + * A {@link RootProvider} that returns endpoints extracted from the `/v3/info` API for the configured endpoint. + */ +@Value.Immutable +abstract class _InfoV3PayloadRootProvider extends AbstractRootProvider { + + protected Mono doGetRoot(ConnectionContext connectionContext) { + return Mono.just(getRoot()); + } + + protected Mono doGetRoot(String key, ConnectionContext connectionContext) { + return getInfo(connectionContext) + .map(info -> { + if (!info.containsKey(key)) { + throw new IllegalArgumentException(String.format("InfoV3 payload does not contain key '%s'", key)); + } + + return normalize(UriComponentsBuilder.fromUriString((String) info.get(key))); + }); + } + + protected Mono doGetRootKey(Queue keyList, ConnectionContext connectionContext) { + String firstKey = keyList.poll(); + + @SuppressWarnings("rawtypes") + Mono payload = getInfo(connectionContext); + return payload + .map(info -> { + if (!info.containsKey(firstKey)) { + throw new IllegalArgumentException(String.format("InfoV3 payload does not contain key '%s'", firstKey)); + } + return handleEntry(keyList,info.get(firstKey)); + }); + + } + + private String handleEntry(Queue keyList, Object entry) { + if(entry==null) { + return ""; + } else if(entry instanceof String) { + if(keyList.isEmpty()) { + return (String) entry; + }else { + throw new IllegalArgumentException(String.format("InfoV3 payload does not contain key '%s'", keyList.peek())); + } + }else if(entry instanceof Map) { + @SuppressWarnings("unchecked") + Map entryMap = (Map) entry; + String key = keyList.poll(); + return handleEntry(keyList, entryMap.get(key)); + } else{ + throw new IllegalArgumentException(String.format("InfoV3 payload does contain unknown type '%s'", entry.getClass().getName())); + } + } + + abstract ObjectMapper getObjectMapper(); + + private UriComponentsBuilder buildInfoUri(UriComponentsBuilder root) { + return root.pathSegment("v3", "info"); + } + + @SuppressWarnings("rawtypes") + @Value.Derived + private Mono getInfo(ConnectionContext connectionContext) { + return createOperator(connectionContext) + .flatMap(operator -> operator.get() + .uri(this::buildInfoUri) + .response() + .parseBody(Map.class)) + .switchIfEmpty(Mono.error(new IllegalArgumentException("InfoV3 endpoint does not contain a payload"))) + .checkpoint(); + } + +} diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_RootPayloadRootProvider.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_RootPayloadRootProvider.java index 12f394c457..5bc09e792f 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_RootPayloadRootProvider.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_RootPayloadRootProvider.java @@ -23,6 +23,7 @@ import reactor.core.publisher.Mono; import java.util.Map; +import java.util.Queue; import java.util.function.Function; import java.util.stream.Collectors; @@ -39,21 +40,53 @@ protected Mono doGetRoot(ConnectionContext connectionContext) { @Override protected Mono doGetRoot(String key, ConnectionContext connectionContext) { - return getPayload(connectionContext) + return getHrefPayload(connectionContext) .map(payload -> { if (!payload.containsKey(key)) { throw new IllegalArgumentException(String.format("Root payload does not contain key '%s'", key)); } - - return normalize(UriComponentsBuilder.fromUriString(payload.get(key))); + return normalize(UriComponentsBuilder.fromUriString(payload.get(key))); }); } + @Override + protected Mono doGetRootKey(Queue keyList, ConnectionContext connectionContext) { + String key = keyList.poll(); + @SuppressWarnings("rawtypes") + Mono payload = getPayload(connectionContext); + return payload + .map( root -> { + if (!root.containsKey(key)) { + throw new IllegalArgumentException(String.format("Root payload does not contain key '%s'", key)); + } + return handleEntry(keyList,root.get(key)); + }); + } + + @SuppressWarnings("unchecked") + private String handleEntry(Queue keyList, Object entry) { + if(entry==null) { + return ""; + } else if(entry instanceof String) { + if(keyList.isEmpty()) { + return (String) entry; + }else { + throw new IllegalArgumentException(String.format("root payload does not contain key '%s'", keyList.peek())); + } + }else if(entry instanceof Map) { + Map entryMap = (Map) entry; + String key = keyList.poll(); + return handleEntry(keyList, entryMap.get(key)); + } else{ + throw new IllegalArgumentException(String.format("root payload does contain unhandled type '%s'", entry.getClass().getName())); + } + } + abstract ObjectMapper getObjectMapper(); @SuppressWarnings("unchecked") @Value.Derived - private Mono> getPayload(ConnectionContext connectionContext) { + private Mono> getHrefPayload(ConnectionContext connectionContext) { return createOperator(connectionContext) .flatMap(operator -> operator.get() .uri(Function.identity()) @@ -65,6 +98,19 @@ private Mono> getPayload(ConnectionContext connectionContext .checkpoint(); } + @SuppressWarnings("rawtypes") + @Value.Derived + private Mono getPayload(ConnectionContext connectionContext) { + return createOperator(connectionContext) + .flatMap(operator -> operator.get() + .uri(Function.identity()) + .response() + .parseBody(Map.class)) + .switchIfEmpty(Mono.error(new IllegalArgumentException("Root endpoint does not contain a payload"))) + .checkpoint(); + } + + // Convert json payload into Map, keeping only "href" entries. private Map processPayload(Map>> payload) { return payload.get("links").entrySet().stream() .filter(item -> null != item.getValue()) diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_SingleEndpointRootProvider.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_SingleEndpointRootProvider.java index 018a45c6be..66459761f5 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_SingleEndpointRootProvider.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_SingleEndpointRootProvider.java @@ -16,6 +16,8 @@ package org.cloudfoundry.reactor; +import java.util.Queue; + import org.immutables.value.Value; import org.springframework.web.util.UriComponents; import reactor.core.publisher.Mono; @@ -36,4 +38,9 @@ protected Mono doGetRoot(String key, ConnectionContext connection return Mono.just(getRoot()); } + @Override + protected Mono doGetRootKey(Queue key, ConnectionContext connectionContext) { + return Mono.just(getRoot().toString()); + } + } 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..dc1b9f646a 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 @@ -19,9 +19,11 @@ import static org.cloudfoundry.util.tuple.TupleUtils.consumer; import com.github.zafarkhaja.semver.Version; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.Queue; import org.cloudfoundry.client.CloudFoundryClient; -import org.cloudfoundry.client.v2.info.GetInfoRequest; -import org.cloudfoundry.client.v2.info.Info; +import org.cloudfoundry.reactor.ConnectionContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Mono; @@ -30,16 +32,21 @@ final class CloudFoundryClientCompatibilityChecker { private final Logger logger = LoggerFactory.getLogger("cloudfoundry-client.compatibility"); - private final Info info; + private final ConnectionContext connectionContext; - CloudFoundryClientCompatibilityChecker(Info info) { - this.info = info; + CloudFoundryClientCompatibilityChecker(ConnectionContext connectionContext) { + this.connectionContext = connectionContext; } void check() { - this.info - .get(GetInfoRequest.builder().build()) - .map(response -> Version.valueOf(response.getApiVersion())) + Queue keyList = + new LinkedList( + Arrays.asList("links", "cloud_controller_v2", "meta", "version")); + + connectionContext + .getRootProvider() + .getRootKey(keyList, connectionContext) + .map(response -> Version.valueOf(response)) .zipWith(Mono.just(Version.valueOf(CloudFoundryClient.SUPPORTED_API_VERSION))) .subscribe( consumer( 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 319a1bcfd6..388984c10e 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 @@ -57,6 +57,7 @@ import org.cloudfoundry.client.v3.deployments.DeploymentsV3; import org.cloudfoundry.client.v3.domains.DomainsV3; import org.cloudfoundry.client.v3.droplets.Droplets; +import org.cloudfoundry.client.v3.info.InfoV3; import org.cloudfoundry.client.v3.isolationsegments.IsolationSegments; import org.cloudfoundry.client.v3.jobs.JobsV3; import org.cloudfoundry.client.v3.organizations.OrganizationsV3; @@ -115,6 +116,7 @@ import org.cloudfoundry.reactor.client.v3.deployments.ReactorDeploymentsV3; import org.cloudfoundry.reactor.client.v3.domains.ReactorDomainsV3; import org.cloudfoundry.reactor.client.v3.droplets.ReactorDroplets; +import org.cloudfoundry.reactor.client.v3.info.ReactorInfoV3; import org.cloudfoundry.reactor.client.v3.isolationsegments.ReactorIsolationSegments; import org.cloudfoundry.reactor.client.v3.jobs.ReactorJobsV3; import org.cloudfoundry.reactor.client.v3.organizations.ReactorOrganizationsV3; @@ -201,7 +203,7 @@ public Builds builds() { @PostConstruct public void checkCompatibility() { - new CloudFoundryClientCompatibilityChecker(info()).check(); + new CloudFoundryClientCompatibilityChecker(getConnectionContext()).check(); } @Override @@ -249,10 +251,17 @@ public FeatureFlags featureFlags() { @Override @Value.Derived + @Deprecated public Info info() { return new ReactorInfo(getConnectionContext(), getRootV2(), getTokenProvider(), getRequestTags()); } + @Override + @Value.Derived + public InfoV3 infoV3() { + return new ReactorInfoV3(getConnectionContext(), getRootV3(), getTokenProvider(), getRequestTags()); + } + @Override @Value.Derived public IsolationSegments isolationSegments() { diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/info/ReactorInfo.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/info/ReactorInfo.java index 36cc4ab567..d6e552d778 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/info/ReactorInfo.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/info/ReactorInfo.java @@ -46,6 +46,7 @@ public ReactorInfo( super(connectionContext, root, tokenProvider, requestTags); } + @Deprecated @Override public Mono get(GetInfoRequest request) { return get(request, GetInfoResponse.class, builder -> builder.pathSegment("info")) diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/info/ReactorInfoV3.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/info/ReactorInfoV3.java new file mode 100644 index 0000000000..e10690bdcf --- /dev/null +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/info/ReactorInfoV3.java @@ -0,0 +1,54 @@ +/* + * 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.v3.info; + +import java.util.Map; +import org.cloudfoundry.client.v3.info.GetInfoRequestV3; +import org.cloudfoundry.client.v3.info.GetInfoResponseV3; +import org.cloudfoundry.client.v3.info.InfoV3; +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 InfoV3} + */ +public class ReactorInfoV3 extends AbstractClientV3Operations implements InfoV3 { + + /** + * 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 ReactorInfoV3( + ConnectionContext connectionContext, + Mono root, + TokenProvider tokenProvider, + Map requestTags) { + super(connectionContext, root, tokenProvider, requestTags); + } + + @Override + public Mono get(GetInfoRequestV3 request) { + return get(request, GetInfoResponseV3.class, builder -> builder.pathSegment("info")) + .checkpoint(); + } +} diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/AbstractUaaTokenProvider.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/AbstractUaaTokenProvider.java index 731cd1ab99..d898a09f99 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/AbstractUaaTokenProvider.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/AbstractUaaTokenProvider.java @@ -68,7 +68,7 @@ public abstract class AbstractUaaTokenProvider implements TokenProvider { private static final String ACCESS_TOKEN = "access_token"; - private static final String AUTHORIZATION_ENDPOINT = "authorization_endpoint"; + private static final String AUTHORIZATION_ENDPOINT = "login"; private static final String REFRESH_TOKEN = "refresh_token"; diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/DefaultConnectionContextTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/DefaultConnectionContextTest.java index 4aba4f36d3..2dd253158a 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/DefaultConnectionContextTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/DefaultConnectionContextTest.java @@ -24,7 +24,10 @@ import io.netty.handler.logging.LogLevel; import java.net.InetSocketAddress; import java.time.Duration; +import java.util.Arrays; +import java.util.LinkedList; import java.util.Optional; +import java.util.Queue; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import reactor.netty.http.client.HttpClient; @@ -76,6 +79,38 @@ void getInfo() { .verify(Duration.ofSeconds(5)); } + @Test + void getInfoV3() { + mockRequest( + InteractionContext.builder() + .request(TestRequest.builder().method(GET).path("/").build()) + .response( + TestResponse.builder() + .status(OK) + .payload("fixtures/GET_response.json") + .build()) + .build()); + mockRequest( + InteractionContext.builder() + .request(TestRequest.builder().method(GET).path("/v3/info").build()) + .response( + TestResponse.builder() + .status(OK) + .payload("fixtures/client/v3/info/GET_response.json") + .build()) + .build()); + + Queue keyList = new LinkedList(Arrays.asList("cli_version", "minimum")); + + this.connectionContext + .getRootProvider() + .getRootKey(keyList, this.connectionContext) + .as(StepVerifier::create) + .expectNext("8.0.0") + .expectComplete() + .verify(Duration.ofSeconds(5000)); + } + @Test void multipleInstances() { DefaultConnectionContext first = diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/RootPayloadRootProviderTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/RootPayloadRootProviderTest.java index d9fbc35887..46001805bc 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/RootPayloadRootProviderTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/RootPayloadRootProviderTest.java @@ -18,9 +18,15 @@ import static io.netty.handler.codec.http.HttpMethod.GET; import static io.netty.handler.codec.http.HttpResponseStatus.OK; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.time.Duration; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.Queue; import org.junit.jupiter.api.Test; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; import reactor.test.StepVerifier; final class RootPayloadRootProviderTest extends AbstractRestTest { @@ -43,6 +49,55 @@ void getRoot() { .verify(Duration.ofSeconds(5)); } + @Test + void getLoggingKey() { + mockRequest( + InteractionContext.builder() + .request(TestRequest.builder().method(GET).path("/").build()) + .response( + TestResponse.builder() + .status(OK) + .payload("fixtures/GET_response.json") + .build()) + .build()); + + this.rootProvider + .getRoot("logging", CONNECTION_CONTEXT) + .as(StepVerifier::create) + .expectNext("http://doppler.run.pivotal.io:443") + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @Test + void secureNormalizeUrlHttps() { + RootPayloadRootProvider secureRootProvider = + RootPayloadRootProvider.builder() + .apiHost("localhost") + .secure(true) + .objectMapper(CONNECTION_CONTEXT.getObjectMapper()) + .build(); + UriComponents normalized = + secureRootProvider.normalize( + UriComponentsBuilder.fromUriString("https://api.run.pivotal.io/v2")); + assertEquals("https://api.run.pivotal.io:443/v2", normalized.toString()); + } + + @Test + void secureNormalizeUrlNonHttps() { + RootPayloadRootProvider secureRootProvider = + RootPayloadRootProvider.builder() + .apiHost("localhost") + .secure(true) + .objectMapper(CONNECTION_CONTEXT.getObjectMapper()) + .build(); + UriComponents normalized = + secureRootProvider.normalize( + UriComponentsBuilder.fromUriString("wss://doppler.run.pivotal.io:8080")); + // TODO: Is this expected behavior? Replacing "wss" (or any other protocol) with "https". + assertEquals("https://doppler.run.pivotal.io:8080", normalized.toString()); + } + @Test void getRootKey() { mockRequest( @@ -65,6 +120,50 @@ void getRootKey() { .verify(Duration.ofSeconds(5)); } + @Test + void getEmptyKey() { + mockRequest( + InteractionContext.builder() + .request(TestRequest.builder().method(GET).path("/").build()) + .response( + TestResponse.builder() + .status(OK) + .payload("fixtures/GET_response.json") + .build()) + .build()); + + Queue keyList = new LinkedList(Arrays.asList("links", "empty_value")); + this.rootProvider + .getRootKey(keyList, CONNECTION_CONTEXT) + .as(StepVerifier::create) + .expectNext("") + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @Test + void getStructuredKey() { + mockRequest( + InteractionContext.builder() + .request(TestRequest.builder().method(GET).path("/").build()) + .response( + TestResponse.builder() + .status(OK) + .payload("fixtures/GET_response.json") + .build()) + .build()); + Queue keyList = + new LinkedList( + Arrays.asList("links", "cloud_controller_v2", "meta", "version")); + + this.rootProvider + .getRootKey(keyList, CONNECTION_CONTEXT) + .as(StepVerifier::create) + .expectNext("2.93.0") + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + @Test void getRootKeyNoKey() { mockRequest( diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/info/ReactorInfoV3Test.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/info/ReactorInfoV3Test.java new file mode 100644 index 0000000000..8553920cd5 --- /dev/null +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/info/ReactorInfoV3Test.java @@ -0,0 +1,68 @@ +/* + * 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.v3.info; + +import static io.netty.handler.codec.http.HttpMethod.GET; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; + +import java.time.Duration; +import java.util.Collections; +import org.cloudfoundry.client.v3.info.GetInfoRequestV3; +import org.cloudfoundry.client.v3.info.GetInfoResponseV3; +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 ReactorInfoV3Test extends AbstractClientApiTest { + + private final ReactorInfoV3 info = + new ReactorInfoV3( + CONNECTION_CONTEXT, this.root, TOKEN_PROVIDER, Collections.emptyMap()); + + @Test + void get() { + mockRequest( + InteractionContext.builder() + .request(TestRequest.builder().method(GET).path("/info").build()) + .response( + TestResponse.builder() + .status(OK) + .payload("fixtures/client/v3/info/GET_response.json") + .build()) + .build()); + + this.info + .get(GetInfoRequestV3.builder().build()) + .as(StepVerifier::create) + .expectNext( + GetInfoResponseV3.builder() + .name("cf-deployment") + .buildNumber("v40.18.0") + .support("http://support.cloudfoundry.com") + .version(40) + .description("SAP BTP Cloud Foundry environment") + .minCliVersion("8.0.0") + .minRecommendedCliVersion("") + .self("https://api.cf.lod-cfcli3.cfrt-sof.sapcloud.io/v3/info") + .build()) + .expectComplete() + .verify(Duration.ofSeconds(5000)); + } +} diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/info/GET_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/info/GET_response.json new file mode 100644 index 0000000000..be60b1a273 --- /dev/null +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/info/GET_response.json @@ -0,0 +1,19 @@ +{ + "build": "v40.18.0", + "cli_version": { + "minimum": "8.0.0", + "recommended": "" + }, + "custom": {}, + "description": "SAP BTP Cloud Foundry environment", + "name": "cf-deployment", + "version": 40, + "links": { + "self": { + "href": "https://api.cf.lod-cfcli3.cfrt-sof.sapcloud.io/v3/info" + }, + "support": { + "href": "http://support.cloudfoundry.com" + } + } +} \ No newline at end of file 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 75186dbd8c..2f80b454d3 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/CloudFoundryClient.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/CloudFoundryClient.java @@ -55,6 +55,7 @@ import org.cloudfoundry.client.v3.deployments.DeploymentsV3; import org.cloudfoundry.client.v3.domains.DomainsV3; import org.cloudfoundry.client.v3.droplets.Droplets; +import org.cloudfoundry.client.v3.info.InfoV3; import org.cloudfoundry.client.v3.isolationsegments.IsolationSegments; import org.cloudfoundry.client.v3.jobs.JobsV3; import org.cloudfoundry.client.v3.organizations.OrganizationsV3; @@ -166,8 +167,14 @@ public interface CloudFoundryClient { /** * Main entry point to the Cloud Foundry Info Client API */ + @Deprecated Info info(); + /** + * Main entry point to the Cloud Foundry Info Client API + */ + InfoV3 infoV3(); + /** * Main entry point to the Cloud Foundry Isolation Segments API */ diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v2/info/Info.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v2/info/Info.java index 1b8c854c91..4aee77c3d5 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v2/info/Info.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v2/info/Info.java @@ -20,6 +20,7 @@ /** * Main entry point to the Cloud Foundry Info Client API + * @deprecated use the v3 API. */ public interface Info { diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v2/info/_GetInfoResponse.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v2/info/_GetInfoResponse.java index ea0470553a..1b7144b596 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v2/info/_GetInfoResponse.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v2/info/_GetInfoResponse.java @@ -18,6 +18,10 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import java.util.Arrays; +import java.util.LinkedList; + import org.cloudfoundry.Nullable; import org.immutables.value.Value; @@ -26,10 +30,12 @@ */ @JsonDeserialize @Value.Immutable -abstract class _GetInfoResponse { +abstract class _GetInfoResponse{ /** * The API version + * @deprecated: use connectionContext.getRootProvider().getRootKey(new LinkedList(Arrays.asList("links","cloud_controller_v3","meta","version")), connectionContext) + * or connectionContext.getRootProvider().getRootKey(new LinkedList(Arrays.asList("links","cloud_controller_v2","meta","version")), connectionContext) */ @JsonProperty("api_version") @Nullable @@ -37,6 +43,7 @@ abstract class _GetInfoResponse { /** * The application SSH endpoint + * @deprecated: use connectionContext.getRootProvider().getRoot("app_ssh", connectionContext) */ @JsonProperty("app_ssh_endpoint") @Nullable @@ -44,6 +51,7 @@ abstract class _GetInfoResponse { /** * The application SSH host key fingerprint + * @deprecated: use connectionContext.getRootProvider().getRootKey("app_ssh.meta.host_key_fingerprint", connectionContext) */ @JsonProperty("app_ssh_host_key_fingerprint") @Nullable @@ -51,6 +59,7 @@ abstract class _GetInfoResponse { /** * The application SSH OAuth client + * @deprecated: use connectionContext.getRootProvider().getRootKey("app_ssh.meta.oauth_client", connectionContext) */ @JsonProperty("app_ssh_oauth_client") @Nullable @@ -58,6 +67,7 @@ abstract class _GetInfoResponse { /** * The authorization endpoint + * @deprecated: use connectionContext.getRootProvider().getRoot("login", connectionContext) */ @JsonProperty("authorization_endpoint") @Nullable @@ -65,6 +75,7 @@ abstract class _GetInfoResponse { /** * The build number + * @deprecated: use corresponding method in V3 api. */ @JsonProperty("build") @Nullable @@ -72,6 +83,7 @@ abstract class _GetInfoResponse { /** * The description + * @deprecated: use corresponding method in V3 api. */ @JsonProperty("description") @Nullable @@ -79,6 +91,7 @@ abstract class _GetInfoResponse { /** * The doppler logging endpoint + * @deprecated: use connectionContext.getRootProvider().getRoot("logging", connectionContext) */ @JsonProperty("doppler_logging_endpoint") @Nullable @@ -93,6 +106,7 @@ abstract class _GetInfoResponse { /** * The minimum CLI version + * @deprecated: use corresponding method in V3 api. */ @JsonProperty("min_cli_version") @Nullable @@ -100,6 +114,7 @@ abstract class _GetInfoResponse { /** * The minimum recommended CLI version + * @deprecated: use corresponding method in V3 api. */ @JsonProperty("min_recommended_cli_version") @Nullable @@ -107,6 +122,7 @@ abstract class _GetInfoResponse { /** * The name + * @deprecated: use corresponding method in V3 api. */ @JsonProperty("name") @Nullable @@ -121,6 +137,7 @@ abstract class _GetInfoResponse { /** * The routing endpoint + * @deprecated: use connectionContext.getRootProvider().getRoot("routing", connectionContext) */ @JsonProperty("routing_endpoint") @Nullable @@ -128,6 +145,7 @@ abstract class _GetInfoResponse { /** * The support url + * @deprecated: use corresponding method in V3 api. */ @JsonProperty("support") @Nullable @@ -135,6 +153,7 @@ abstract class _GetInfoResponse { /** * The token endpoint + * @deprecated: use connectionContext.getRootProvider().getRoot("uaa", connectionContext) */ @JsonProperty("token_endpoint") @Nullable @@ -149,6 +168,7 @@ abstract class _GetInfoResponse { /** * The version + * @deprecated: use corresponding method in V3 api. */ @JsonProperty("version") @Nullable diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/info/InfoV3.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/info/InfoV3.java new file mode 100644 index 0000000000..fdb61fc32a --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/info/InfoV3.java @@ -0,0 +1,33 @@ +/* + * 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.v3.info; + +import reactor.core.publisher.Mono; + +/** + * Main entry point to the Cloud Foundry InfoV3 Client API + */ +public interface InfoV3 { + + /** + * Makes the Get Info request + * + * @param request the Get Info request + * @return the response from the Get Info request + */ + Mono get(GetInfoRequestV3 request); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/info/_GetInfoRequestV3.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/info/_GetInfoRequestV3.java new file mode 100644 index 0000000000..e7a1bf98f2 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/info/_GetInfoRequestV3.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.v3.info; + +import org.immutables.value.Value; + +/** + * The request payload for the Get Info operation + */ +@Value.Immutable +abstract class _GetInfoRequestV3 { + +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/info/_GetInfoResponseV3.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/info/_GetInfoResponseV3.java new file mode 100644 index 0000000000..2d46db094f --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/info/_GetInfoResponseV3.java @@ -0,0 +1,115 @@ +/* + * 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.v3.info; + +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 Info operation + */ +@JsonDeserialize(using = org.cloudfoundry.client.v3.info._GetInfoResponseV3.InfoV3Deserializer.class) +@Value.Immutable() +public interface _GetInfoResponseV3 { + + /** + * The build number + */ + @Value.Parameter + @Nullable + public abstract String getBuildNumber(); + + /** + * The description + */ + @Value.Parameter + @Nullable + public abstract String getDescription(); + + /** + * The minimum CLI version + */ + @Value.Parameter + @Nullable + public abstract String getMinCliVersion(); + + /** + * The minimum recommended CLI version + */ + @Value.Parameter + @Nullable + public abstract String getMinRecommendedCliVersion(); + + /** + * The name + */ + @Value.Parameter + @Nullable + public abstract String getName(); + + /** + * The support url + */ + @Value.Parameter + @Nullable + public abstract String getSupport(); + + /** + * The self url + */ + @Value.Parameter + @Nullable + public abstract String getSelf(); + + /** + * The version + */ + @Value.Parameter + @Nullable + public abstract Integer getVersion(); + + public class InfoV3Deserializer extends StdDeserializer<_GetInfoResponseV3>{ + private static final long serialVersionUID = 1L; + + protected InfoV3Deserializer() { + super(GetInfoResponseV3.class); + } + @Override + public _GetInfoResponseV3 deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException, JacksonException { + JsonNode productNode = jp.getCodec().readTree(jp); + String buildNumber = productNode.get("build").textValue(); + String description = productNode.get("description").textValue(); + String minCliVersion = productNode.get("cli_version").get("minimum").textValue(); + String minRecommendedCliVersion = productNode.get("cli_version").get("recommended").textValue(); + String name = productNode.get("name").textValue(); + String support = productNode.get("links").get("support").get("href").textValue(); + String self = productNode.get("links").get("self").get("href").textValue(); + Integer version = productNode.get("version").asInt(); + return GetInfoResponseV3.of(buildNumber,description,minCliVersion, minRecommendedCliVersion,name,support,self,version); + } + } +} diff --git a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/info/GetInfoRequestV3Test.java b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/info/GetInfoRequestV3Test.java new file mode 100644 index 0000000000..2e818f8681 --- /dev/null +++ b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/info/GetInfoRequestV3Test.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.v3.info; + +import org.junit.jupiter.api.Test; + +final class GetInfoRequestV3Test { + + @Test + void valid() { + GetInfoRequestV3.builder().build(); + } +} diff --git a/integration-test/src/test/java/org/cloudfoundry/AbstractIntegrationTest.java b/integration-test/src/test/java/org/cloudfoundry/AbstractIntegrationTest.java index c677804e71..fc2eafa164 100644 --- a/integration-test/src/test/java/org/cloudfoundry/AbstractIntegrationTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/AbstractIntegrationTest.java @@ -25,8 +25,13 @@ import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; +import java.time.Duration; +import java.util.Arrays; +import java.util.LinkedList; import java.util.Optional; +import java.util.Queue; import java.util.function.Consumer; +import org.cloudfoundry.reactor.ConnectionContext; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestInfo; @@ -45,11 +50,15 @@ public abstract class AbstractIntegrationTest { public String testName; + private static Boolean serverUsesRouting = null; + @Autowired protected NameFactory nameFactory; @Autowired @RegisterExtension public CloudFoundryVersionConditionalRule cloudFoundryVersionConditionalRule; + @Autowired private ConnectionContext connectionContext; + @BeforeEach public void testEntry(TestInfo testInfo) { Optional testMethod = testInfo.getTestMethod(); @@ -57,6 +66,15 @@ public void testEntry(TestInfo testInfo) { this.testName = testMethod.get().getName(); } this.logger.debug(">> {} <<", getTestName()); + if (serverUsesRouting == null) { + Queue keyList = new LinkedList(Arrays.asList("links", "routing")); + String routing = + connectionContext + .getRootProvider() + .getRootKey(keyList, connectionContext) + .block(Duration.ofMinutes(5)); + serverUsesRouting = Boolean.valueOf(!routing.isBlank()); + } } @AfterEach @@ -84,6 +102,10 @@ protected static Consumer> tupleEquality() { return consumer((expected, actual) -> assertThat(actual).isEqualTo(expected)); } + protected boolean serverUsesRouting() { + return serverUsesRouting; + } + private String getTestName() { return String.format("%s.%s", this.getClass().getSimpleName(), this.testName); } diff --git a/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java b/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java index 5aed533645..f9cdfe8742 100644 --- a/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java +++ b/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java @@ -35,10 +35,11 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; +import java.util.Queue; import java.util.Random; import org.cloudfoundry.client.CloudFoundryClient; -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; @@ -447,11 +448,15 @@ 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())) + Version serverVersion(ConnectionContext connectionContext) { + Queue keyList = + new LinkedList( + Arrays.asList("links", "cloud_controller_v2", "meta", "version")); + + return connectionContext + .getRootProvider() + .getRootKey(keyList, connectionContext) + .map(response -> Version.valueOf(response)) .doOnSubscribe(s -> this.logger.debug(">> CLOUD FOUNDRY VERSION <<")) .doOnSuccess(r -> this.logger.debug("<< CLOUD FOUNDRY VERSION >>")) .block(); 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 2a87f6c588..77f30472a7 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java @@ -77,6 +77,7 @@ import org.cloudfoundry.operations.services.GetServiceInstanceRequest; import org.cloudfoundry.operations.services.ServiceInstance; import org.cloudfoundry.util.FluentMap; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; @@ -425,6 +426,7 @@ public void getStopped() throws IOException { @Test public void getTcp() throws IOException { + Assumptions.assumeTrue(super.serverUsesRouting()); String applicationName = this.nameFactory.getApplicationName(); String domainName = this.nameFactory.getDomainName(); @@ -1066,6 +1068,7 @@ public void pushRoutePath() throws IOException { @Test public void pushTcpRoute() throws IOException { + Assumptions.assumeTrue(super.serverUsesRouting()); String applicationName = this.nameFactory.getApplicationName(); String domainName = this.nameFactory.getDomainName(); @@ -1189,6 +1192,7 @@ public void pushUpdateRoute() throws IOException { @Test public void pushUpdateTcpRoute() throws IOException { + Assumptions.assumeTrue(super.serverUsesRouting()); String applicationName = this.nameFactory.getApplicationName(); String domainName = this.nameFactory.getDomainName(); 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 8fe5c90cb7..d2c1c56859 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/DomainsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/DomainsTest.java @@ -29,6 +29,7 @@ import org.cloudfoundry.operations.domains.ShareDomainRequest; import org.cloudfoundry.operations.domains.UnshareDomainRequest; import org.cloudfoundry.operations.organizations.CreateOrganizationRequest; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import reactor.core.publisher.Flux; @@ -101,6 +102,7 @@ public void createShared() { @Test public void createSharedTcp() { + Assumptions.assumeTrue(super.serverUsesRouting()); String domainName = this.nameFactory.getDomainName(); this.cloudFoundryOperations @@ -138,6 +140,7 @@ public void list() { @Test public void listRouterGroups() { + Assumptions.assumeTrue(super.serverUsesRouting()); this.cloudFoundryOperations .domains() .listRouterGroups() @@ -150,6 +153,7 @@ public void listRouterGroups() { @Test public void listTcp() { + Assumptions.assumeTrue(super.serverUsesRouting()); String domainName = this.nameFactory.getDomainName(); requestCreateTcpDomain(this.cloudFoundryOperations, domainName) 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 5b86191b22..a2ccf77c67 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/RoutesTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/RoutesTest.java @@ -41,6 +41,7 @@ import org.cloudfoundry.operations.routes.UnmapRouteRequest; import org.cloudfoundry.operations.services.BindRouteServiceInstanceRequest; import org.cloudfoundry.operations.services.CreateUserProvidedServiceInstanceRequest; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -154,6 +155,7 @@ public void create() { @Test public void createRouteTcpAssignedPort() { + Assumptions.assumeTrue(super.serverUsesRouting()); String domainName = this.nameFactory.getDomainName(); Integer port = this.nameFactory.getPort(); @@ -171,6 +173,7 @@ public void createRouteTcpAssignedPort() { @Test public void createRouteTcpRandomPort() { + Assumptions.assumeTrue(super.serverUsesRouting()); String domainName = this.nameFactory.getDomainName(); requestCreateSharedDomain(this.cloudFoundryOperations, domainName, DEFAULT_ROUTER_GROUP) @@ -305,6 +308,7 @@ public void deleteOrphanedRoutes() { @Test public void deleteTcpRoute() { + Assumptions.assumeTrue(super.serverUsesRouting()); String domainName = this.nameFactory.getDomainName(); requestCreateSharedDomain(this.cloudFoundryOperations, domainName, DEFAULT_ROUTER_GROUP) @@ -522,6 +526,7 @@ public void mapNoPath() throws IOException { @Test public void mapTcpRoute() throws IOException { + Assumptions.assumeTrue(super.serverUsesRouting()); String applicationName = this.nameFactory.getApplicationName(); String domainName = this.nameFactory.getDomainName(); @@ -553,6 +558,7 @@ public void mapTcpRoute() throws IOException { @Test public void mapTcpRouteTwice() throws IOException { + Assumptions.assumeTrue(super.serverUsesRouting()); String applicationName = this.nameFactory.getApplicationName(); String domainName = this.nameFactory.getDomainName(); @@ -680,6 +686,7 @@ public void unmapNoPath() throws IOException { @Test public void unmapTcpRoute() throws IOException { + Assumptions.assumeTrue(super.serverUsesRouting()); String applicationName = this.nameFactory.getApplicationName(); String domainName = this.nameFactory.getDomainName(); 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..b13bc3b138 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 @@ -24,6 +24,7 @@ import org.cloudfoundry.routing.v1.routergroups.RouterGroup; import org.cloudfoundry.routing.v1.routergroups.UpdateRouterGroupRequest; import org.cloudfoundry.routing.v1.routergroups.UpdateRouterGroupResponse; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import reactor.core.publisher.Mono; @@ -37,6 +38,7 @@ public final class RouterGroupsTest extends AbstractIntegrationTest { @Test public void list() { + Assumptions.assumeTrue(super.serverUsesRouting()); this.routingClient .routerGroups() .list(ListRouterGroupsRequest.builder().build()) @@ -51,6 +53,7 @@ public void list() { @Test public void update() { + Assumptions.assumeTrue(super.serverUsesRouting()); getRouterGroupId(this.routingClient, DEFAULT_ROUTER_GROUP) .flatMap( routerGroupId -> 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 27852f13fc..baa725c84d 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 @@ -32,6 +32,7 @@ import org.cloudfoundry.routing.v1.tcproutes.TcpRoute; import org.cloudfoundry.routing.v1.tcproutes.TcpRouteConfiguration; import org.cloudfoundry.routing.v1.tcproutes.TcpRouteDeletion; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import reactor.core.publisher.Flux; @@ -48,6 +49,7 @@ public final class TcpRoutesTest extends AbstractIntegrationTest { @Test public void create() { + Assumptions.assumeTrue(super.serverUsesRouting()); String backendIp = this.nameFactory.getIpAddress(); Integer backendPort = this.nameFactory.getPort(); Integer port = this.nameFactory.getPort(); @@ -82,6 +84,7 @@ public void create() { @Test public void delete() { + Assumptions.assumeTrue(super.serverUsesRouting()); String backendIp = this.nameFactory.getIpAddress(); Integer backendPort = this.nameFactory.getPort(); Integer port = this.nameFactory.getPort(); @@ -122,6 +125,7 @@ public void delete() { @Test public void events() { + Assumptions.assumeTrue(super.serverUsesRouting()); String backendIp = this.nameFactory.getIpAddress(); Integer backendPort = this.nameFactory.getPort(); Integer port = this.nameFactory.getPort(); @@ -158,6 +162,7 @@ public void events() { @Test public void list() { + Assumptions.assumeTrue(super.serverUsesRouting()); String backendIp = this.nameFactory.getIpAddress(); Integer backendPort = this.nameFactory.getPort(); Integer port = this.nameFactory.getPort();