diff --git a/README.md b/README.md index ffed2f36f4..617d1e1b9c 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,41 @@ The `cf-java-client` project is a Java language binding for interacting with a C ## Versions The Cloud Foundry Java Client has two active versions. The `5.x` line is compatible with Spring Boot `2.4.x - 2.6.x` just to manage its dependencies, while the `4.x` line uses Spring Boot `2.3.x`. +## Deprecations + +### `DopplerClient.recentLogs()` — Recent Logs via Doppler + +> [!WARNING] +> **Deprecated since cf-java-client `5.17.x`** +> +> The `DopplerClient.recentLogs()` endpoint (and the related `RecentLogsRequest` / `LogMessage` types from the `org.cloudfoundry.doppler` package) are **deprecated** and will be removed in a future release. +> +> This API relies on the [Loggregator][loggregator] Doppler/Traffic Controller endpoint `/apps/{id}/recentlogs`, which was removed in **Loggregator ≥ 107.0**. +> The affected platform versions are: +> +> | Platform | Last version with Doppler recent-logs support | +> | -------- | --------------------------------------------- | +> | CF Deployment (CFD) | `< 24.3` | +> | Tanzu Application Service (TAS) | `< 4.0` | +> +> **Migration:** Replace any call to `DopplerClient.recentLogs()` with [`LogCacheClient.read()`][log-cache-api] (available via `org.cloudfoundry.logcache.v1.LogCacheClient`). +> +> ```java +> // Before (deprecated) +> dopplerClient.recentLogs(RecentLogsRequest.builder() +> .applicationId(appId) +> .build()); +> +> // After +> logCacheClient.read(ReadRequest.builder() +> .sourceId(appId) +> .envelopeTypes(EnvelopeType.LOG) +> .build()); +> ``` + +[loggregator]: https://github.com/cloudfoundry/loggregator +[log-cache-api]: https://github.com/cloudfoundry/log-cache + ## Dependencies Most projects will need two dependencies; the Operations API and an implementation of the Client API. For Maven, the dependencies would be defined like this: @@ -76,6 +111,9 @@ Both the `cloudfoundry-operations` and `cloudfoundry-client` projects follow a [ ### `CloudFoundryClient`, `DopplerClient`, `UaaClient` Builders +> [!NOTE] +> **`DopplerClient` — partial deprecation:** The `recentLogs()` method on `DopplerClient` is deprecated and only works against Loggregator \< 107.0 (CFD \< 24.3 / TAS \< 4.0). See the [Deprecations](#deprecations) section above for the migration path to `LogCacheClient`. + The lowest-level building blocks of the API are `ConnectionContext` and `TokenProvider`. These types are intended to be shared between instances of the clients, and come with out of the box implementations. To instantiate them, you configure them with builders: ```java @@ -297,6 +335,7 @@ Name | Description `TEST_PROXY_PORT` | _(Optional)_ The port of a proxy to route all requests through. Defaults to `8080`. `TEST_PROXY_USERNAME` | _(Optional)_ The username for a proxy to route all requests through `TEST_SKIPSSLVALIDATION` | _(Optional)_ Whether to skip SSL validation when connecting to the Cloud Foundry instance. Defaults to `false`. +`UAA_API_REQUEST_LIMIT` | _(Optional)_ If your UAA server does rate limiting and returns 429 errors, set this variable to the smallest limit configured there. Whether your server limits UAA calls is shown in the log, together with the location of the configuration file on the server. Defaults to `0` (no limit). If you do not have access to a CloudFoundry instance with admin access, you can run one locally using [bosh-deployment](https://github.com/cloudfoundry/bosh-deployment) & [cf-deployment](https://github.com/cloudfoundry/cf-deployment/) and Virtualbox. diff --git a/cloudfoundry-client-reactor/pom.xml b/cloudfoundry-client-reactor/pom.xml index 10962f393a..2d0931852c 100644 --- a/cloudfoundry-client-reactor/pom.xml +++ b/cloudfoundry-client-reactor/pom.xml @@ -25,7 +25,7 @@ org.cloudfoundry cloudfoundry-java-client - 5.15.0.BUILD-SNAPSHOT + 6.0.0-SNAPSHOT cloudfoundry-client-reactor 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 d131ea8a49..80e5ef2614 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 @@ -76,6 +76,7 @@ import org.cloudfoundry.client.v3.spaces.SpacesV3; import org.cloudfoundry.client.v3.stacks.StacksV3; import org.cloudfoundry.client.v3.tasks.Tasks; +import org.cloudfoundry.client.v3.users.UsersV3; import org.cloudfoundry.reactor.ConnectionContext; import org.cloudfoundry.reactor.TokenProvider; import org.cloudfoundry.reactor.client.v2.applications.ReactorApplicationsV2; @@ -136,6 +137,7 @@ import org.cloudfoundry.reactor.client.v3.spaces.ReactorSpacesV3; import org.cloudfoundry.reactor.client.v3.stacks.ReactorStacksV3; import org.cloudfoundry.reactor.client.v3.tasks.ReactorTasks; +import org.cloudfoundry.reactor.client.v3.users.ReactorUsersV3; import org.immutables.value.Value; import reactor.core.publisher.Mono; @@ -509,6 +511,12 @@ public Users users() { return new ReactorUsers(getConnectionContext(), getRootV2(), getTokenProvider(), getRequestTags()); } + @Override + @Value.Derived + public UsersV3 usersV3() { + return new ReactorUsersV3(getConnectionContext(), getRootV3(), getTokenProvider(), getRequestTags()); + } + /** * The connection context */ diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/quotas/organizations/ReactorOrganizationQuotasV3.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/quotas/organizations/ReactorOrganizationQuotasV3.java index 2981f0b0f2..3f6c49d8ec 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/quotas/organizations/ReactorOrganizationQuotasV3.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/quotas/organizations/ReactorOrganizationQuotasV3.java @@ -94,4 +94,18 @@ public Mono delete(DeleteOrganizationQuotaRequest request) { "organization_quotas", request.getOrganizationQuotaId())) .checkpoint(); } + + @Override + public Mono apply(ApplyOrganizationQuotaRequest request) { + return post( + request, + ApplyOrganizationQuotaResponse.class, + builder -> + builder.pathSegment( + "organization_quotas", + request.getOrganizationQuotaId(), + "relationships", + "organizations")) + .checkpoint(); + } } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/quotas/spaces/ReactorSpaceQuotasV3.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/quotas/spaces/ReactorSpaceQuotasV3.java index 93e8182a00..1a6b512520 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/quotas/spaces/ReactorSpaceQuotasV3.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/quotas/spaces/ReactorSpaceQuotasV3.java @@ -87,4 +87,33 @@ public Mono delete(DeleteSpaceQuotaRequest request) { builder -> builder.pathSegment("space_quotas", request.getSpaceQuotaId())) .checkpoint(); } + + @Override + public Mono apply(ApplySpaceQuotaRequest request) { + return post( + request, + ApplySpaceQuotaResponse.class, + builder -> + builder.pathSegment( + "space_quotas", + request.getSpaceQuotaId(), + "relationships", + "spaces")) + .checkpoint(); + } + + @Override + public Mono remove(RemoveSpaceQuotaRequest request) { + return delete( + request, + Void.class, + builder -> + builder.pathSegment( + "space_quotas", + request.getSpaceQuotaId(), + "relationships", + "spaces", + request.getSpaceId())) + .checkpoint(); + } } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/users/ReactorUsersV3.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/users/ReactorUsersV3.java new file mode 100644 index 0000000000..69d48d9bae --- /dev/null +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/users/ReactorUsersV3.java @@ -0,0 +1,85 @@ +/* + * Copyright 2013-2025 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.users; + +import java.util.Map; +import org.cloudfoundry.client.v3.users.*; +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 UsersV3} + */ +public class ReactorUsersV3 extends AbstractClientV3Operations implements UsersV3 { + + /** + * 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.cloudfoundry.your.company.com}. + * @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 ReactorUsersV3( + ConnectionContext connectionContext, + Mono root, + TokenProvider tokenProvider, + Map requestTags) { + super(connectionContext, root, tokenProvider, requestTags); + } + + @Override + public Mono create(CreateUserRequest request) { + return post(request, CreateUserResponse.class, builder -> builder.pathSegment("users")) + .checkpoint(); + } + + @Override + public Mono get(GetUserRequest request) { + return get( + request, + GetUserResponse.class, + builder -> builder.pathSegment("users", request.getUserId())) + .checkpoint(); + } + + @Override + public Mono list(ListUsersRequest request) { + return get(request, ListUsersResponse.class, builder -> builder.pathSegment("users")) + .checkpoint(); + } + + @Override + public Mono update(UpdateUserRequest request) { + return patch( + request, + UpdateUserResponse.class, + builder -> builder.pathSegment("users", request.getUserId())) + .checkpoint(); + } + + @Override + public Mono delete(DeleteUserRequest request) { + return delete( + request, + Void.class, + builder -> builder.pathSegment("users", request.getUserId())) + .checkpoint(); + } +} diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/logcache/v1/ReactorLogCacheEndpoints.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/logcache/v1/ReactorLogCacheEndpoints.java index 2e68c52538..6df4093ed5 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/logcache/v1/ReactorLogCacheEndpoints.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/logcache/v1/ReactorLogCacheEndpoints.java @@ -16,15 +16,23 @@ package org.cloudfoundry.reactor.logcache.v1; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import org.cloudfoundry.logcache.v1.Envelope; +import org.cloudfoundry.logcache.v1.EnvelopeType; import org.cloudfoundry.logcache.v1.InfoRequest; import org.cloudfoundry.logcache.v1.InfoResponse; import org.cloudfoundry.logcache.v1.MetaRequest; import org.cloudfoundry.logcache.v1.MetaResponse; import org.cloudfoundry.logcache.v1.ReadRequest; import org.cloudfoundry.logcache.v1.ReadResponse; +import org.cloudfoundry.logcache.v1.TailLogsRequest; import org.cloudfoundry.reactor.ConnectionContext; import org.cloudfoundry.reactor.TokenProvider; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; final class ReactorLogCacheEndpoints extends AbstractLogCacheOperations { @@ -48,4 +56,111 @@ Mono meta(MetaRequest request) { Mono read(ReadRequest request) { return get(request, ReadResponse.class, "read", request.getSourceId()).checkpoint(); } + + Mono recentLogs(ReadRequest request) { + return read(request); + } + + /** + * Continuously polls Log Cache and emits new {@link Envelope}s as they arrive. + * + *

Mirrors the Go {@code logcache.Walk()} / {@code cf tail --follow} semantics: + *

    + *
  1. Start the cursor at {@code startTime} (defaults to now − 5 s in + * nanoseconds).
  2. + *
  3. Issue {@code GET /api/v1/read/{sourceId}?start_time=cursor}.
  4. + *
  5. Emit every returned envelope in ascending timestamp order and advance + * the cursor to {@code lastTimestamp + 1}.
  6. + *
  7. When the batch is empty, wait {@code pollInterval} before the next poll.
  8. + *
  9. Repeat forever – the caller cancels the subscription to stop.
  10. + *
+ * Fully non-blocking: no {@code Thread.sleep}. + */ + Flux logsTail(TailLogsRequest request) { + long defaultStartNanos = (System.currentTimeMillis() - 5_000L) * 1_000_000L; + AtomicLong cursor = + new AtomicLong( + request.getStartTime() != null + ? request.getStartTime() + : defaultStartNanos); + + List envelopeTypes = + request.getEnvelopeTypes() != null + ? request.getEnvelopeTypes() + : Collections.emptyList(); + String nameFilter = request.getNameFilter(); + + /* + * Strategy (mirrors Go's logcache.Walk): + * – Mono.defer builds a fresh ReadRequest from the mutable cursor on every repetition. + * – The Mono returns either the sorted batch (non-empty) or an empty list. + * – flatMapMany turns each batch into a stream of individual Envelope items. + * – repeat() subscribes again after each completion. + * – When the batch was empty we insert a delay via Mono.delay before the next + * repetition so we do not hammer the server. We signal "empty" by returning + * a sentinel Mono (false = was empty, true = had data) and use + * repeatWhen to conditionally delay. + */ + return Flux.defer( + () -> { + // Build the read request from the current cursor position. + ReadRequest.Builder builder = + ReadRequest.builder() + .sourceId(request.getSourceId()) + .startTime(cursor.get()); + if (!envelopeTypes.isEmpty()) { + builder.envelopeTypes(envelopeTypes); + } + if (nameFilter != null && !nameFilter.isEmpty()) { + builder.nameFilter(nameFilter); + } + + return read(builder.build()) + .onErrorReturn(ReadResponse.builder().build()) + .flatMapMany( + resp -> { + List raw = + resp.getEnvelopes() != null + ? resp.getEnvelopes().getBatch() + : Collections.emptyList(); + + if (raw.isEmpty()) { + // Signal "no data" so repeatWhen can insert the + // back-off delay. + return Flux.empty(); + } + + // Sort ascending by timestamp and advance the + // cursor. + List sorted = new ArrayList<>(raw); + sorted.sort( + (a, b) -> + Long.compare( + a.getTimestamp() != null + ? a.getTimestamp() + : 0L, + b.getTimestamp() != null + ? b.getTimestamp() + : 0L)); + + Envelope last = sorted.get(sorted.size() - 1); + cursor.set( + (last.getTimestamp() != null + ? last.getTimestamp() + : cursor.get()) + + 1); + + return Flux.fromIterable(sorted); + }); + }) + // repeatWhen receives a Flux where each element is the count of items + // emitted in the previous cycle (0 = empty batch → insert delay). + .repeatWhen( + companion -> + companion.flatMap( + count -> + count == 0 + ? Mono.delay(request.getPollInterval()) + : Mono.just(count))); + } } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/logcache/v1/_ReactorLogCacheClient.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/logcache/v1/_ReactorLogCacheClient.java index d9460476ea..0236da2188 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/logcache/v1/_ReactorLogCacheClient.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/logcache/v1/_ReactorLogCacheClient.java @@ -16,6 +16,7 @@ package org.cloudfoundry.reactor.logcache.v1; +import org.cloudfoundry.logcache.v1.Envelope; import org.cloudfoundry.logcache.v1.InfoRequest; import org.cloudfoundry.logcache.v1.InfoResponse; import org.cloudfoundry.logcache.v1.LogCacheClient; @@ -23,9 +24,11 @@ import org.cloudfoundry.logcache.v1.MetaResponse; import org.cloudfoundry.logcache.v1.ReadRequest; import org.cloudfoundry.logcache.v1.ReadResponse; +import org.cloudfoundry.logcache.v1.TailLogsRequest; import org.cloudfoundry.reactor.ConnectionContext; import org.cloudfoundry.reactor.TokenProvider; import org.immutables.value.Value; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.net.URI; @@ -53,6 +56,16 @@ public Mono read(ReadRequest request) { return getReactorLogCacheEndpoints().read(request); } + @Override + public Mono recentLogs(ReadRequest request) { + return getReactorLogCacheEndpoints().recentLogs(request); + } + + @Override + public Flux logsTail(TailLogsRequest request) { + return getReactorLogCacheEndpoints().logsTail(request); + } + /** * The connection context */ 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 6cfc85a486..10e34a4b0b 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 @@ -74,7 +74,7 @@ public abstract class AbstractUaaTokenProvider implements TokenProvider { private static final ZoneId UTC = ZoneId.of("UTC"); - private final ConcurrentMap> accessTokens = + protected final ConcurrentMap> accessTokens = new ConcurrentHashMap<>(1); private final ConcurrentMap refreshTokenStreams = diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/_ClientCredentialsGrantTokenProvider.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/_ClientCredentialsGrantTokenProvider.java index bd7a7061db..2bbbbaa101 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/_ClientCredentialsGrantTokenProvider.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/_ClientCredentialsGrantTokenProvider.java @@ -16,6 +16,7 @@ package org.cloudfoundry.reactor.tokenprovider; +import org.cloudfoundry.reactor.ConnectionContext; import org.cloudfoundry.reactor.TokenProvider; import org.immutables.value.Value; import reactor.netty.http.client.HttpClientForm; @@ -36,4 +37,9 @@ void tokenRequestTransformer(HttpClientRequest request, HttpClientForm form) { .attr("response_type", "token"); } + @Override + public void invalidate(ConnectionContext connectionContext) { + this.accessTokens.remove(connectionContext); + } + } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/ReactorRatelimit.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/ReactorRatelimit.java new file mode 100644 index 0000000000..c350367d61 --- /dev/null +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/ReactorRatelimit.java @@ -0,0 +1,53 @@ +/* + * Copyright 2013-2025 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.uaa; + +import java.util.Map; +import org.cloudfoundry.reactor.ConnectionContext; +import org.cloudfoundry.reactor.TokenProvider; +import org.cloudfoundry.uaa.ratelimit.Ratelimit; +import org.cloudfoundry.uaa.ratelimit.RatelimitRequest; +import org.cloudfoundry.uaa.ratelimit.RatelimitResponse; +import reactor.core.publisher.Mono; + +public final class ReactorRatelimit extends AbstractUaaOperations implements Ratelimit { + + /** + * 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://uaa.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 ReactorRatelimit( + ConnectionContext connectionContext, + Mono root, + TokenProvider tokenProvider, + Map requestTags) { + super(connectionContext, root, tokenProvider, requestTags); + } + + @Override + public Mono getRatelimit(RatelimitRequest request) { + return get( + request, + RatelimitResponse.class, + builder -> builder.pathSegment("RateLimitingStatus")) + .checkpoint(); + } +} diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/_ReactorUaaClient.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/_ReactorUaaClient.java index 1b2d613c4e..a1b28ca848 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/_ReactorUaaClient.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/_ReactorUaaClient.java @@ -33,6 +33,7 @@ import org.cloudfoundry.uaa.groups.Groups; import org.cloudfoundry.uaa.identityproviders.IdentityProviders; import org.cloudfoundry.uaa.identityzones.IdentityZones; +import org.cloudfoundry.uaa.ratelimit.Ratelimit; import org.cloudfoundry.uaa.serverinformation.ServerInformation; import org.cloudfoundry.uaa.tokens.Tokens; import org.cloudfoundry.uaa.users.Users; @@ -104,6 +105,12 @@ public Users users() { return new ReactorUsers(getConnectionContext(), getRoot(), getTokenProvider(), getRequestTags()); } + @Override + @Value.Derived + public Ratelimit rateLimit() { + return new ReactorRatelimit(getConnectionContext(), getRoot(), getTokenProvider(), getRequestTags()); + } + /** * The connection context */ diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/RequestLogger.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/RequestLogger.java index d0f3cc5ae3..4c94223c57 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/RequestLogger.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/RequestLogger.java @@ -34,7 +34,7 @@ public class RequestLogger { private long requestSentTime; public void request(HttpClientRequest request) { - request(String.format("%-6s {}", request.method()), request.uri()); + request(String.format("%-6s {}", request.method()), request.resourceUrl()); } public void response(HttpClientResponse response) { diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/quotas/organizations/ReactorOrganizationQuotasV3Test.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/quotas/organizations/ReactorOrganizationQuotasV3Test.java index 9d2485153e..8488b3271a 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/quotas/organizations/ReactorOrganizationQuotasV3Test.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/quotas/organizations/ReactorOrganizationQuotasV3Test.java @@ -22,11 +22,14 @@ import java.time.Duration; import java.util.Collections; +import java.util.Map; import org.cloudfoundry.client.v3.Link; import org.cloudfoundry.client.v3.Pagination; import org.cloudfoundry.client.v3.Relationship; import org.cloudfoundry.client.v3.ToManyRelationship; -import org.cloudfoundry.client.v3.quotas.*; +import org.cloudfoundry.client.v3.quotas.Apps; +import org.cloudfoundry.client.v3.quotas.Routes; +import org.cloudfoundry.client.v3.quotas.Services; import org.cloudfoundry.client.v3.quotas.organizations.*; import org.cloudfoundry.reactor.InteractionContext; import org.cloudfoundry.reactor.TestRequest; @@ -51,13 +54,13 @@ void create() { .method(POST) .path("/organization_quotas") .payload( - "fixtures/client/v3/organization_quotas/POST_request.json") + "fixtures/client/v3/quotas/organizations/POST_request.json") .build()) .response( TestResponse.builder() .status(OK) .payload( - "fixtures/client/v3/organization_quotas/POST_response.json") + "fixtures/client/v3/quotas/organizations/POST_response.json") .build()) .build()); @@ -115,7 +118,7 @@ void get() { TestResponse.builder() .status(OK) .payload( - "fixtures/client/v3/organization_quotas/GET_{id}_response.json") + "fixtures/client/v3/quotas/organizations/GET_{id}_response.json") .build()) .build()); @@ -146,7 +149,7 @@ void list() { TestResponse.builder() .status(OK) .payload( - "fixtures/client/v3/organization_quotas/GET_response.json") + "fixtures/client/v3/quotas/organizations/GET_response.json") .build()) .build()); @@ -193,13 +196,13 @@ void update() { .path( "/organization_quotas/24637893-3b77-489d-bb79-8466f0d88b52") .payload( - "fixtures/client/v3/organization_quotas/PATCH_{id}_request.json") + "fixtures/client/v3/quotas/organizations/PATCH_{id}_request.json") .build()) .response( TestResponse.builder() .status(OK) .payload( - "fixtures/client/v3/organization_quotas/PATCH_{id}_response.json") + "fixtures/client/v3/quotas/organizations/PATCH_{id}_response.json") .build()) .build()); @@ -217,6 +220,47 @@ void update() { .verify(Duration.ofSeconds(5)); } + @Test + void apply() { + mockRequest( + InteractionContext.builder() + .request( + TestRequest.builder() + .method(POST) + .path( + "/organization_quotas/24637893-3b77-489d-bb79-8466f0d88b52/relationships/organizations") + .payload( + "fixtures/client/v3/quotas/organizations/relationships/POST_{id}_request.json") + .build()) + .response( + TestResponse.builder() + .status(OK) + .payload( + "fixtures/client/v3/quotas/organizations/relationships/POST_{id}_response.json") + .build()) + .build()); + + Relationship org1 = Relationship.builder().id("org-guid1").build(); + Relationship org2 = Relationship.builder().id("org-guid2").build(); + + ToManyRelationship organizationRelationships = + ToManyRelationship.builder().data(org1, org2).build(); + + this.organizationQuotasV3 + .apply( + ApplyOrganizationQuotaRequest.builder() + .organizationQuotaId("24637893-3b77-489d-bb79-8466f0d88b52") + .organizationRelationships(organizationRelationships) + .build()) + .as(StepVerifier::create) + .expectNext( + ApplyOrganizationQuotaResponse.builder() + .from(expectedApplyOrganizationQuotaResponse()) + .build()) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + @NotNull private static OrganizationQuotaResource expectedOrganizationQuotaResource1() { return buildOrganizationQuotaResource( @@ -232,6 +276,28 @@ private static OrganizationQuotaResource expectedOrganizationQuotaResource2() { "144251f2-a202-4ffe-ab47-9046c4077e99"); } + @NotNull + private static ApplyOrganizationQuotaResponse expectedApplyOrganizationQuotaResponse() { + + Relationship org1 = Relationship.builder().id("org-guid1").build(); + Relationship org2 = Relationship.builder().id("org-guid2").build(); + Relationship existingOrg = Relationship.builder().id("previous-org-guid").build(); + + ToManyRelationship organizationRelationships = + ToManyRelationship.builder().data(org1, org2, existingOrg).build(); + Link selfLink = + Link.builder() + .href( + "https://api.example.org/v3/organization_quotas/24637893-3b77-489d-bb79-8466f0d88b52/relationships/organizations") + .build(); + Map links = Collections.singletonMap("self", selfLink); + + return ApplyOrganizationQuotaResponse.builder() + .organizationRelationships(organizationRelationships) + .links(links) + .build(); + } + @NotNull private static OrganizationQuotaResource buildOrganizationQuotaResource( String id, String name, String relatedOrganizationId) { diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/quotas/spaces/ReactorSpaceQuotasV3Test.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/quotas/spaces/ReactorSpaceQuotasV3Test.java index 34231e9426..7923efbb31 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/quotas/spaces/ReactorSpaceQuotasV3Test.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/quotas/spaces/ReactorSpaceQuotasV3Test.java @@ -16,21 +16,16 @@ package org.cloudfoundry.reactor.client.v3.quotas.spaces; -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.OK; +import static io.netty.handler.codec.http.HttpMethod.*; +import static io.netty.handler.codec.http.HttpResponseStatus.*; import java.time.Duration; import java.util.Collections; -import org.cloudfoundry.client.v3.Link; -import org.cloudfoundry.client.v3.Pagination; -import org.cloudfoundry.client.v3.Relationship; -import org.cloudfoundry.client.v3.ToManyRelationship; -import org.cloudfoundry.client.v3.ToOneRelationship; -import org.cloudfoundry.client.v3.quotas.*; +import java.util.Map; +import org.cloudfoundry.client.v3.*; +import org.cloudfoundry.client.v3.quotas.Apps; +import org.cloudfoundry.client.v3.quotas.Routes; +import org.cloudfoundry.client.v3.quotas.Services; import org.cloudfoundry.client.v3.quotas.spaces.*; import org.cloudfoundry.reactor.InteractionContext; import org.cloudfoundry.reactor.TestRequest; @@ -57,13 +52,13 @@ void create() { .method(POST) .path("/space_quotas") .payload( - "fixtures/client/v3/space_quotas/POST_request.json") + "fixtures/client/v3/quotas/spaces/POST_request.json") .build()) .response( TestResponse.builder() .status(OK) .payload( - "fixtures/client/v3/space_quotas/POST_response.json") + "fixtures/client/v3/quotas/spaces/POST_response.json") .build()) .build()); @@ -143,7 +138,7 @@ void get() { TestResponse.builder() .status(OK) .payload( - "fixtures/client/v3/space_quotas/GET_{id}_response.json") + "fixtures/client/v3/quotas/spaces/GET_{id}_response.json") .build()) .build()); @@ -165,7 +160,7 @@ void list() { TestResponse.builder() .status(OK) .payload( - "fixtures/client/v3/space_quotas/GET_response.json") + "fixtures/client/v3/quotas/spaces/GET_response.json") .build()) .build()); @@ -211,13 +206,13 @@ void update() { .method(PATCH) .path("/space_quotas/" + EXPECTED_SPACE_QUOTA_ID_1) .payload( - "fixtures/client/v3/space_quotas/PATCH_{id}_request.json") + "fixtures/client/v3/quotas/spaces/PATCH_{id}_request.json") .build()) .response( TestResponse.builder() .status(OK) .payload( - "fixtures/client/v3/space_quotas/PATCH_{id}_response.json") + "fixtures/client/v3/quotas/spaces/PATCH_{id}_response.json") .build()) .build()); @@ -235,6 +230,71 @@ void update() { .verify(Duration.ofSeconds(5)); } + @Test + void apply() { + mockRequest( + InteractionContext.builder() + .request( + TestRequest.builder() + .method(POST) + .path( + "/space_quotas/24637893-3b77-489d-bb79-8466f0d88b52/relationships/spaces") + .payload( + "fixtures/client/v3/quotas/spaces/relationships/POST_{id}_request.json") + .build()) + .response( + TestResponse.builder() + .status(OK) + .payload( + "fixtures/client/v3/quotas/spaces/relationships/POST_{id}_response.json") + .build()) + .build()); + + Relationship space1 = Relationship.builder().id("space-guid1").build(); + Relationship space2 = Relationship.builder().id("space-guid2").build(); + + ToManyRelationship organizationRelationships = + ToManyRelationship.builder().data(space1, space2).build(); + + this.spaceQuotasV3 + .apply( + ApplySpaceQuotaRequest.builder() + .spaceQuotaId("24637893-3b77-489d-bb79-8466f0d88b52") + .spaceRelationships(organizationRelationships) + .build()) + .as(StepVerifier::create) + .expectNext( + ApplySpaceQuotaResponse.builder() + .from(expectedApplySpaceQuotaResponse()) + .build()) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @Test + void remove() { + mockRequest( + InteractionContext.builder() + .request( + TestRequest.builder() + .method(DELETE) + .path( + "/space_quotas/test-space-quota-id/relationships/spaces/test-space-guid") + .build()) + .response(TestResponse.builder().status(NO_CONTENT).build()) + .build()); + + this.spaceQuotasV3 + .remove( + RemoveSpaceQuotaRequest.builder() + .spaceQuotaId("test-space-quota-id") + .spaceId("test-space-guid") + .build()) + .as(StepVerifier::create) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + @NotNull private static SpaceQuotaResource expectedSpaceQuotaResource1() { return buildSpaceQuotaResource( @@ -252,6 +312,28 @@ private static SpaceQuotaResource expectedSpaceQuotaResource2() { null); } + @NotNull + private static ApplySpaceQuotaResponse expectedApplySpaceQuotaResponse() { + + Relationship space1 = Relationship.builder().id("space-guid1").build(); + Relationship space2 = Relationship.builder().id("space-guid2").build(); + Relationship existingSpace = Relationship.builder().id("previous-space-guid").build(); + + ToManyRelationship spaceRelationships = + ToManyRelationship.builder().data(space1, space2, existingSpace).build(); + Link selfLink = + Link.builder() + .href( + "https://api.example.org/v3/space_quotas/24637893-3b77-489d-bb79-8466f0d88b52/relationships/spaces") + .build(); + Map links = Collections.singletonMap("self", selfLink); + + return ApplySpaceQuotaResponse.builder() + .spaceRelationships(spaceRelationships) + .links(links) + .build(); + } + @NotNull private static SpaceQuotaResource buildSpaceQuotaResource( String id, String name, String relatedOrganizationId, String relatedSpaceId) { diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/serviceInstances/ReactorServiceInstancesV3Test.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/serviceinstances/ReactorServiceInstancesV3Test.java similarity index 100% rename from cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/serviceInstances/ReactorServiceInstancesV3Test.java rename to cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/serviceinstances/ReactorServiceInstancesV3Test.java diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/users/ReactorUsersV3Test.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/users/ReactorUsersV3Test.java new file mode 100644 index 0000000000..b9d78e9a69 --- /dev/null +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/users/ReactorUsersV3Test.java @@ -0,0 +1,215 @@ +package org.cloudfoundry.reactor.client.v3.users; + +import static io.netty.handler.codec.http.HttpMethod.*; +import static io.netty.handler.codec.http.HttpResponseStatus.*; + +import java.time.Duration; +import java.util.Collections; +import java.util.Map; +import org.cloudfoundry.client.v3.Link; +import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.client.v3.Pagination; +import org.cloudfoundry.client.v3.users.*; +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 ReactorUsersV3Test extends AbstractClientApiTest { + private final ReactorUsersV3 users = + new ReactorUsersV3( + CONNECTION_CONTEXT, this.root, TOKEN_PROVIDER, Collections.emptyMap()); + + @Test + void create() { + mockRequest( + InteractionContext.builder() + .request( + TestRequest.builder() + .method(POST) + .path("/users") + .payload("fixtures/client/v3/users/POST_request.json") + .build()) + .response( + TestResponse.builder() + .status(CREATED) + .payload("fixtures/client/v3/users/POST_response.json") + .build()) + .build()); + + this.users + .create( + CreateUserRequest.builder() + .userId("3a5d3d89-3f89-4f05-8188-8a2b298c79d5") + .build()) + .as(StepVerifier::create) + .expectNext( + CreateUserResponse.builder() + .from( + expectedUserResource( + Collections.emptyMap(), Collections.emptyMap())) + .build()) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @Test + void get() { + mockRequest( + InteractionContext.builder() + .request( + TestRequest.builder() + .method(GET) + .path("/users/3a5d3d89-3f89-4f05-8188-8a2b298c79d5") + .build()) + .response( + TestResponse.builder() + .status(OK) + .payload("fixtures/client/v3/users/GET_{id}_response.json") + .build()) + .build()); + + this.users + .get( + GetUserRequest.builder() + .userId("3a5d3d89-3f89-4f05-8188-8a2b298c79d5") + .build()) + .as(StepVerifier::create) + .expectNext( + GetUserResponse.builder() + .from( + expectedUserResource( + Collections.emptyMap(), Collections.emptyMap())) + .build()) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @Test + void list() { + mockRequest( + InteractionContext.builder() + .request(TestRequest.builder().method(GET).path("/users").build()) + .response( + TestResponse.builder() + .status(OK) + .payload("fixtures/client/v3/users/GET_response.json") + .build()) + .build()); + + Link first = + Link.builder().href("https://api.example.org/v3/users?page=1&per_page=1").build(); + Link last = + Link.builder().href("https://api.example.org/v3/users?page=1&per_page=1").build(); + this.users + .list(ListUsersRequest.builder().build()) + .as(StepVerifier::create) + .expectNext( + ListUsersResponse.builder() + .pagination( + Pagination.builder() + .first(first) + .last(last) + .totalResults(1) + .totalPages(1) + .build()) + .resource( + expectedUserResource( + Collections.emptyMap(), Collections.emptyMap())) + .build()) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @Test + void update() { + mockRequest( + InteractionContext.builder() + .request( + TestRequest.builder() + .method(PATCH) + .path("/users/3a5d3d89-3f89-4f05-8188-8a2b298c79d5") + .payload("fixtures/client/v3/users/PATCH_{id}_request.json") + .build()) + .response( + TestResponse.builder() + .status(CREATED) + .payload( + "fixtures/client/v3/users/PATCH_{id}_response.json") + .build()) + .build()); + + this.users + .update( + UpdateUserRequest.builder() + .userId("3a5d3d89-3f89-4f05-8188-8a2b298c79d5") + .metadata( + Metadata.builder() + .putAllAnnotations( + Collections.singletonMap( + "note", "detailed information")) + .putAllLabels( + Collections.singletonMap( + "environment", "production")) + .build()) + .build()) + .as(StepVerifier::create) + .expectNext( + UpdateUserResponse.builder() + .from( + expectedUserResource( + Collections.singletonMap( + "note", "detailed information"), + Collections.singletonMap( + "environment", "production"))) + .build()) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @Test + void delete() { + mockRequest( + InteractionContext.builder() + .request( + TestRequest.builder() + .method(DELETE) + .path("/users/3a5d3d89-3f89-4f05-8188-8a2b298c79d5") + .build()) + .response(TestResponse.builder().status(ACCEPTED).build()) + .build()); + + this.users + .delete( + DeleteUserRequest.builder() + .userId("3a5d3d89-3f89-4f05-8188-8a2b298c79d5") + .build()) + .as(StepVerifier::create) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + UserResource expectedUserResource(Map labels, Map annotations) { + return UserResource.builder() + .id("3a5d3d89-3f89-4f05-8188-8a2b298c79d5") + .createdAt("2019-03-08T01:06:19Z") + .updatedAt("2019-03-08T01:06:19Z") + .username("some-name") + .presentationName("some-name") + .origin("uaa") + .metadata( + Metadata.builder() + .putAllAnnotations(labels) + .putAllLabels(annotations) + .build()) + .link( + "self", + Link.builder() + .href( + "https://api.example.org/v3/users/3a5d3d89-3f89-4f05-8188-8a2b298c79d5") + .build()) + .build(); + } +} diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/organization_quotas/GET_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/organizations/GET_response.json similarity index 100% rename from cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/organization_quotas/GET_response.json rename to cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/organizations/GET_response.json diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/organization_quotas/GET_{id}_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/organizations/GET_{id}_response.json similarity index 100% rename from cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/organization_quotas/GET_{id}_response.json rename to cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/organizations/GET_{id}_response.json diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/organization_quotas/PATCH_{id}_request.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/organizations/PATCH_{id}_request.json similarity index 100% rename from cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/organization_quotas/PATCH_{id}_request.json rename to cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/organizations/PATCH_{id}_request.json diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/organization_quotas/PATCH_{id}_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/organizations/PATCH_{id}_response.json similarity index 100% rename from cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/organization_quotas/PATCH_{id}_response.json rename to cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/organizations/PATCH_{id}_response.json diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/organization_quotas/POST_request.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/organizations/POST_request.json similarity index 100% rename from cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/organization_quotas/POST_request.json rename to cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/organizations/POST_request.json diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/organization_quotas/POST_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/organizations/POST_response.json similarity index 100% rename from cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/organization_quotas/POST_response.json rename to cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/organizations/POST_response.json diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/organizations/relationships/POST_{id}_request.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/organizations/relationships/POST_{id}_request.json new file mode 100644 index 0000000000..093842883d --- /dev/null +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/organizations/relationships/POST_{id}_request.json @@ -0,0 +1,10 @@ +{ + "data": [ + { + "guid": "org-guid1" + }, + { + "guid": "org-guid2" + } + ] +} \ No newline at end of file diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/organizations/relationships/POST_{id}_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/organizations/relationships/POST_{id}_response.json new file mode 100644 index 0000000000..e1abe6da19 --- /dev/null +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/organizations/relationships/POST_{id}_response.json @@ -0,0 +1,18 @@ +{ + "data": [ + { + "guid": "org-guid1" + }, + { + "guid": "org-guid2" + }, + { + "guid": "previous-org-guid" + } + ], + "links": { + "self": { + "href": "https://api.example.org/v3/organization_quotas/24637893-3b77-489d-bb79-8466f0d88b52/relationships/organizations" + } + } +} \ No newline at end of file diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/space_quotas/GET_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/spaces/GET_response.json similarity index 100% rename from cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/space_quotas/GET_response.json rename to cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/spaces/GET_response.json diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/space_quotas/GET_{id}_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/spaces/GET_{id}_response.json similarity index 100% rename from cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/space_quotas/GET_{id}_response.json rename to cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/spaces/GET_{id}_response.json diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/space_quotas/PATCH_{id}_request.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/spaces/PATCH_{id}_request.json similarity index 100% rename from cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/space_quotas/PATCH_{id}_request.json rename to cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/spaces/PATCH_{id}_request.json diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/space_quotas/PATCH_{id}_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/spaces/PATCH_{id}_response.json similarity index 100% rename from cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/space_quotas/PATCH_{id}_response.json rename to cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/spaces/PATCH_{id}_response.json diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/space_quotas/POST_request.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/spaces/POST_request.json similarity index 100% rename from cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/space_quotas/POST_request.json rename to cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/spaces/POST_request.json diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/space_quotas/POST_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/spaces/POST_response.json similarity index 100% rename from cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/space_quotas/POST_response.json rename to cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/spaces/POST_response.json diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/spaces/relationships/POST_{id}_request.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/spaces/relationships/POST_{id}_request.json new file mode 100644 index 0000000000..9cf906732a --- /dev/null +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/spaces/relationships/POST_{id}_request.json @@ -0,0 +1,10 @@ +{ + "data": [ + { + "guid": "space-guid1" + }, + { + "guid": "space-guid2" + } + ] +} \ No newline at end of file diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/spaces/relationships/POST_{id}_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/spaces/relationships/POST_{id}_response.json new file mode 100644 index 0000000000..7b316d68e3 --- /dev/null +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/quotas/spaces/relationships/POST_{id}_response.json @@ -0,0 +1,18 @@ +{ + "data": [ + { + "guid": "space-guid1" + }, + { + "guid": "space-guid2" + }, + { + "guid": "previous-space-guid" + } + ], + "links": { + "self": { + "href": "https://api.example.org/v3/space_quotas/24637893-3b77-489d-bb79-8466f0d88b52/relationships/spaces" + } + } +} \ No newline at end of file diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/users/GET_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/users/GET_response.json new file mode 100644 index 0000000000..c727d7dbe1 --- /dev/null +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/users/GET_response.json @@ -0,0 +1,33 @@ +{ + "pagination": { + "total_results": 1, + "total_pages": 1, + "first": { + "href": "https://api.example.org/v3/users?page=1&per_page=1" + }, + "last": { + "href": "https://api.example.org/v3/users?page=1&per_page=1" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "3a5d3d89-3f89-4f05-8188-8a2b298c79d5", + "created_at": "2019-03-08T01:06:19Z", + "updated_at": "2019-03-08T01:06:19Z", + "username": "some-name", + "presentation_name": "some-name", + "origin": "uaa", + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/users/3a5d3d89-3f89-4f05-8188-8a2b298c79d5" + } + } + } + ] +} \ No newline at end of file diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/users/GET_{id}_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/users/GET_{id}_response.json new file mode 100644 index 0000000000..b60ce5ab33 --- /dev/null +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/users/GET_{id}_response.json @@ -0,0 +1,17 @@ +{ + "guid": "3a5d3d89-3f89-4f05-8188-8a2b298c79d5", + "created_at": "2019-03-08T01:06:19Z", + "updated_at": "2019-03-08T01:06:19Z", + "username": "some-name", + "presentation_name": "some-name", + "origin": "uaa", + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/users/3a5d3d89-3f89-4f05-8188-8a2b298c79d5" + } + } +} \ No newline at end of file diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/users/PATCH_{id}_request.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/users/PATCH_{id}_request.json new file mode 100644 index 0000000000..52c4a1b610 --- /dev/null +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/users/PATCH_{id}_request.json @@ -0,0 +1,10 @@ +{ + "metadata": { + "labels": { + "environment": "production" + }, + "annotations": { + "note": "detailed information" + } + } +} \ No newline at end of file diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/users/PATCH_{id}_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/users/PATCH_{id}_response.json new file mode 100644 index 0000000000..f1729ecb51 --- /dev/null +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/users/PATCH_{id}_response.json @@ -0,0 +1,21 @@ +{ + "guid": "3a5d3d89-3f89-4f05-8188-8a2b298c79d5", + "created_at": "2019-03-08T01:06:19Z", + "updated_at": "2019-03-08T01:06:19Z", + "username": "some-name", + "presentation_name": "some-name", + "origin": "uaa", + "metadata": { + "labels": { + "environment": "production" + }, + "annotations": { + "note": "detailed information" + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/users/3a5d3d89-3f89-4f05-8188-8a2b298c79d5" + } + } +} \ No newline at end of file diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/users/POST_request.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/users/POST_request.json new file mode 100644 index 0000000000..e5463b4edb --- /dev/null +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/users/POST_request.json @@ -0,0 +1,3 @@ +{ + "guid": "3a5d3d89-3f89-4f05-8188-8a2b298c79d5" +} \ No newline at end of file diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/users/POST_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/users/POST_response.json new file mode 100644 index 0000000000..b60ce5ab33 --- /dev/null +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/users/POST_response.json @@ -0,0 +1,17 @@ +{ + "guid": "3a5d3d89-3f89-4f05-8188-8a2b298c79d5", + "created_at": "2019-03-08T01:06:19Z", + "updated_at": "2019-03-08T01:06:19Z", + "username": "some-name", + "presentation_name": "some-name", + "origin": "uaa", + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/users/3a5d3d89-3f89-4f05-8188-8a2b298c79d5" + } + } +} \ No newline at end of file diff --git a/cloudfoundry-client/pom.xml b/cloudfoundry-client/pom.xml index 5828f8c589..6b1bbd3dc8 100644 --- a/cloudfoundry-client/pom.xml +++ b/cloudfoundry-client/pom.xml @@ -25,7 +25,7 @@ org.cloudfoundry cloudfoundry-java-client - 5.15.0.BUILD-SNAPSHOT + 6.0.0-SNAPSHOT cloudfoundry-client 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 bfd4f1bd2a..0f85e973e5 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/CloudFoundryClient.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/CloudFoundryClient.java @@ -74,6 +74,7 @@ import org.cloudfoundry.client.v3.spaces.SpacesV3; import org.cloudfoundry.client.v3.stacks.StacksV3; import org.cloudfoundry.client.v3.tasks.Tasks; +import org.cloudfoundry.client.v3.users.UsersV3; /** * Main entry point to the Cloud Foundry Client API @@ -375,4 +376,9 @@ public interface CloudFoundryClient { * Main entry point to the Cloud Foundry Users Client API */ Users users(); + + /** + * Main entry point to the Cloud Foundry Users V3 Client API + */ + UsersV3 usersV3(); } diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/domains/_CreateDomainRequest.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/domains/_CreateDomainRequest.java index fe42ce4bc3..d452ebdc2f 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/domains/_CreateDomainRequest.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/domains/_CreateDomainRequest.java @@ -56,4 +56,11 @@ abstract class _CreateDomainRequest { @Nullable abstract DomainRelationships getRelationships(); + /** + * The router group + */ + @JsonProperty("router_group") + @Nullable + abstract RouterGroup getRouterGroup(); + } diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/quotas/organizations/OrganizationQuotasV3.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/quotas/organizations/OrganizationQuotasV3.java index cb6f9a4223..624e7374b2 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/quotas/organizations/OrganizationQuotasV3.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/quotas/organizations/OrganizationQuotasV3.java @@ -66,4 +66,13 @@ public interface OrganizationQuotasV3 { * @return the response from the Delete Organization Quota request */ Mono delete(DeleteOrganizationQuotaRequest request); + + /** + * Makes the Apply an Organization Quota to an Organization + * request + * + * @param request the Apply an Organization Quota to an Organization request + * @return the response from the Apply an Organization Quota to an Organization request + */ + Mono apply(ApplyOrganizationQuotaRequest request); } diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/quotas/organizations/_ApplyOrganizationQuotaRequest.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/quotas/organizations/_ApplyOrganizationQuotaRequest.java new file mode 100644 index 0000000000..84ca9423cf --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/quotas/organizations/_ApplyOrganizationQuotaRequest.java @@ -0,0 +1,28 @@ +package org.cloudfoundry.client.v3.quotas.organizations; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.cloudfoundry.client.v3.ToManyRelationship; +import org.immutables.value.Value; + +/** + * The request payload to apply an Organization Quota to an Organization + */ +@JsonSerialize +@Value.Immutable +abstract class _ApplyOrganizationQuotaRequest { + + /** + * The Organization Quota id + */ + @JsonIgnore + abstract String getOrganizationQuotaId(); + + /** + * Relationships to the organizations where the quota is applied + * Use of JsonUnwrapped to inline the organization relationships as per the API spec + */ + @JsonUnwrapped + abstract ToManyRelationship organizationRelationships(); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/quotas/organizations/_ApplyOrganizationQuotaResponse.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/quotas/organizations/_ApplyOrganizationQuotaResponse.java new file mode 100644 index 0000000000..eaf61f61a1 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/quotas/organizations/_ApplyOrganizationQuotaResponse.java @@ -0,0 +1,33 @@ +package org.cloudfoundry.client.v3.quotas.organizations; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.cloudfoundry.AllowNulls; +import org.cloudfoundry.client.v3.Link; +import org.cloudfoundry.client.v3.ToManyRelationship; +import org.immutables.value.Value; + +import java.util.Map; + +/** + * The response payload for applying an Organization Quota to an Organization + */ +@JsonDeserialize +@Value.Immutable +abstract class _ApplyOrganizationQuotaResponse { + + /** + * Relationships to the organizations where the quota is applied + * Use of JsonUnwrapped to inline the organization relationships as per the API spec + */ + @JsonUnwrapped + abstract ToManyRelationship organizationRelationships(); + + /** + * Links to related resources and actions for the resource + */ + @AllowNulls + @JsonProperty("links") + public abstract Map getLinks(); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/quotas/spaces/SpaceQuotasV3.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/quotas/spaces/SpaceQuotasV3.java index cf34330e8c..ae2b00143d 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/quotas/spaces/SpaceQuotasV3.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/quotas/spaces/SpaceQuotasV3.java @@ -33,7 +33,7 @@ public interface SpaceQuotasV3 { Mono create(CreateSpaceQuotaRequest request); /** - * Makes the Get Space Quota + * Makes the Get Space Quota * request * * @param request the Get Space Quota request @@ -42,7 +42,7 @@ public interface SpaceQuotasV3 { Mono get(GetSpaceQuotaRequest request); /** - * Makes the List all Space Quota + * Makes the List all Space Quota * request * * @param request the List all Space Quotas request @@ -50,7 +50,7 @@ public interface SpaceQuotasV3 { */ Mono list(ListSpaceQuotasRequest request); - /** Makes the Update Space Quota + /** Makes the Update Space Quota * request * * @param request the Update Space Quota request @@ -59,11 +59,29 @@ public interface SpaceQuotasV3 { Mono update(UpdateSpaceQuotaRequest request); /** - * Makes the Delete Space Quota + * Makes the Delete Space Quota * request * * @param request the Delete Space Quota request * @return the response from the Space Organization Quota request */ Mono delete(DeleteSpaceQuotaRequest request); + + /** + * Makes the Apply a Space Quota to a Space + * request + * + * @param request the Apply a Space Quota to a Space request + * @return the response from the Apply a Space Quota to a Space request + */ + Mono apply(ApplySpaceQuotaRequest request); + + /** + * Makes the Remove a Space Quota from a Space + * request + * + * @param request the Remove a Space Quota from a Space request + * @return the response from the Remove a Space Quota from a Space request + */ + Mono remove(RemoveSpaceQuotaRequest request); } diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/quotas/spaces/_ApplySpaceQuotaRequest.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/quotas/spaces/_ApplySpaceQuotaRequest.java new file mode 100644 index 0000000000..ddb1bb0d0e --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/quotas/spaces/_ApplySpaceQuotaRequest.java @@ -0,0 +1,28 @@ +package org.cloudfoundry.client.v3.quotas.spaces; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.cloudfoundry.client.v3.ToManyRelationship; +import org.immutables.value.Value; + +/** + * The request payload to apply an Space Quota to a Space + */ +@JsonSerialize +@Value.Immutable +abstract class _ApplySpaceQuotaRequest { + + /** + * The Space Quota id + */ + @JsonIgnore + abstract String getSpaceQuotaId(); + + /** + * Relationships to the spaces where the quota is applied + * Use of JsonUnwrapped to inline the space relationships as per the API spec + */ + @JsonUnwrapped + abstract ToManyRelationship spaceRelationships(); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/quotas/spaces/_ApplySpaceQuotaResponse.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/quotas/spaces/_ApplySpaceQuotaResponse.java new file mode 100644 index 0000000000..fe721048e7 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/quotas/spaces/_ApplySpaceQuotaResponse.java @@ -0,0 +1,33 @@ +package org.cloudfoundry.client.v3.quotas.spaces; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.cloudfoundry.AllowNulls; +import org.cloudfoundry.client.v3.Link; +import org.cloudfoundry.client.v3.ToManyRelationship; +import org.immutables.value.Value; + +import java.util.Map; + +/** + * The response payload for applying a Space Quota to a Space + */ +@JsonDeserialize +@Value.Immutable +abstract class _ApplySpaceQuotaResponse { + + /** + * Relationships to the spaces where the quota is applied + * Use of JsonUnwrapped to inline the space relationships as per the API spec + */ + @JsonUnwrapped + abstract ToManyRelationship spaceRelationships(); + + /** + * Links to related resources and actions for the resource + */ + @AllowNulls + @JsonProperty("links") + public abstract Map getLinks(); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/quotas/spaces/_RemoveSpaceQuotaRequest.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/quotas/spaces/_RemoveSpaceQuotaRequest.java new file mode 100644 index 0000000000..38f08bb525 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/quotas/spaces/_RemoveSpaceQuotaRequest.java @@ -0,0 +1,40 @@ +/* + * Copyright 2013-2025 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.quotas.spaces; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.immutables.value.Value; + +/** + * The request payload to Remove a space quota from a space + */ +@Value.Immutable +abstract class _RemoveSpaceQuotaRequest { + + /** + * The space quota id + */ + @JsonIgnore + abstract String getSpaceQuotaId(); + + /** + * The space id + */ + @JsonIgnore + abstract String getSpaceId(); + +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/User.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/User.java new file mode 100644 index 0000000000..cf6ffd4a89 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/User.java @@ -0,0 +1,56 @@ +/* + * Copyright 2013-2025 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.users; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.cloudfoundry.Nullable; +import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.client.v3.Resource; + +/** + * Base class for responses that are users + */ +public abstract class User extends Resource { + + /** + * The name registered in UAA; will be null for UAA clients and non-UAA users + */ + @JsonProperty("username") + @Nullable + public abstract String getUsername(); + + /** + * The name displayed for the user; for UAA users, this is the same as the username. For UAA clients, this is the UAA client ID + */ + @JsonProperty("presentation_name") + @Nullable + public abstract String getPresentationName(); + + /** + * The identity provider for the UAA user; will be null for UAA clients + */ + @JsonProperty("origin") + @Nullable + public abstract String getOrigin(); + + /** + * The metadata Labels and Annotations applied to the user + */ + @JsonProperty("metadata") + @Nullable + public abstract Metadata getMetadata(); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/UsersV3.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/UsersV3.java new file mode 100644 index 0000000000..906582ead3 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/UsersV3.java @@ -0,0 +1,65 @@ +/* + * Copyright 2013-2025 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.users; + +import reactor.core.publisher.Mono; + +/** + * Main entry point to the Cloud Foundry Users V3 Client API + */ +public interface UsersV3 { + + /** + * Makes the Create a User request + * + * @param request the Create User request + * @return the response from the Create User request + */ + Mono create(CreateUserRequest request); + + /** + * Makes the Get a User request + * + * @param request the Get User request + * @return the response from the Get User request + */ + Mono get(GetUserRequest request); + + /** + * Makes the List all Users request + * + * @param request the List Users request + * @return the response from the List Users request + */ + Mono list(ListUsersRequest request); + + /** + * Makes the Update a Stack request + * + * @param request the Update User request + * @return the response from the Update User request + */ + Mono update(UpdateUserRequest request); + + /** + * Makes the Delete a User request + * + * @param request the Delete User request + * @return void + */ + Mono delete(DeleteUserRequest request); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_CreateUserRequest.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_CreateUserRequest.java new file mode 100644 index 0000000000..71f6c4823f --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_CreateUserRequest.java @@ -0,0 +1,58 @@ +/* + * Copyright 2013-2025 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.users; + +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 Create User operation + */ +@JsonSerialize +@Value.Immutable +abstract class _CreateUserRequest { + + /** + * Unique identifier for the user. For UAA users this will match the UAA user ID; in the case of UAA clients, this will match the UAA client ID + */ + @JsonProperty("guid") + @Nullable + abstract String getUserId(); + + /** + * Username of the user to be created. This can only be provided together with origin. + */ + @JsonProperty("username") + @Nullable + abstract String getUsername(); + + /** + * Origin of the user to be created. This can only be provided together with username and cannot be uaa. + */ + @JsonProperty("origin") + @Nullable + abstract String getOrigin(); + + /** + * The metadata Labels and Annotations applied to the user + */ + @JsonProperty("metadata") + @Nullable + abstract Metadata getMetadata(); +} \ No newline at end of file diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_CreateUserResponse.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_CreateUserResponse.java new file mode 100644 index 0000000000..b1bdd0715d --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_CreateUserResponse.java @@ -0,0 +1,27 @@ +/* + * Copyright 2013-2025 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.users; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.immutables.value.Value; + +/** + * The response payload for the Create User operation + */ +@JsonDeserialize +@Value.Immutable +abstract class _CreateUserResponse extends User { +} \ No newline at end of file diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_DeleteUserRequest.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_DeleteUserRequest.java new file mode 100644 index 0000000000..5010b8193a --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_DeleteUserRequest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2025 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.users; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.immutables.value.Value; + +/** + * The request payload for the Delete User operation + * All roles associated with a user will be deleted if the user is deleted. + */ +@Value.Immutable +abstract class _DeleteUserRequest { + + /** + * The User id / guid + */ + @JsonIgnore + abstract String getUserId(); + +} \ No newline at end of file diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_GetUserRequest.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_GetUserRequest.java new file mode 100644 index 0000000000..1f2d070586 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_GetUserRequest.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2025 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.users; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.immutables.value.Value; + +/** + * The request payload for the Get User operation + */ +@Value.Immutable +abstract class _GetUserRequest { + + /** + * The User id / guid + */ + @JsonIgnore + abstract String getUserId(); +} \ No newline at end of file diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_GetUserResponse.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_GetUserResponse.java new file mode 100644 index 0000000000..76adda2fc0 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_GetUserResponse.java @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2025 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.users; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.immutables.value.Value; + +/** + * The response payload for the Get User operation + */ +@JsonDeserialize +@Value.Immutable +abstract class _GetUserResponse extends User { +} \ No newline at end of file diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_ListUsersRequest.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_ListUsersRequest.java new file mode 100644 index 0000000000..c8606ab59c --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_ListUsersRequest.java @@ -0,0 +1,49 @@ + + +package org.cloudfoundry.client.v3.users; + +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 Users operation. + */ +@Value.Immutable +abstract class _ListUsersRequest extends PaginatedRequest { + + /** + * Comma-delimited list of user guids to filter by + */ + @FilterParameter("guids") + @Nullable + abstract List getGuids(); + + /** + * Comma-delimited list of usernames to filter by. Mutually exclusive with partial_usernames + */ + @FilterParameter("usernames") + abstract List getUsernames(); + + /** + * Comma-delimited list of strings to search by. When using this query parameter, all the users that contain the string provided in their username will be returned. Mutually exclusive with usernames + */ + @FilterParameter("partial_usernames") + abstract List getPartialUsernames(); + + /** + * Comma-delimited list of user origins (user stores) to filter by, for example, users authenticated by UAA have the origin “uaa”; users authenticated by an LDAP provider have the origin “ldap”; when filtering by origins, usernames must be included + */ + @FilterParameter("origins") + abstract List getOrigins(); + + /** + * A query string containing a list of label selector requirements + */ + @FilterParameter("label_selector") + @Nullable + abstract String getLabelSelector(); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_ListUsersResponse.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_ListUsersResponse.java new file mode 100644 index 0000000000..1cf5a2e87e --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_ListUsersResponse.java @@ -0,0 +1,16 @@ +package org.cloudfoundry.client.v3.users; + + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.cloudfoundry.client.v3.PaginatedResponse; +import org.cloudfoundry.client.v3.users.UserResource; +import org.immutables.value.Value; + +/** + * The response payload for the List Users operation. + */ +@JsonDeserialize +@Value.Immutable +abstract class _ListUsersResponse extends PaginatedResponse { + +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_UpdateUserRequest.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_UpdateUserRequest.java new file mode 100644 index 0000000000..6258711533 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_UpdateUserRequest.java @@ -0,0 +1,46 @@ +/* + * Copyright 2013-2025 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.users; + +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 User operation + */ +@JsonSerialize +@Value.Immutable +abstract class _UpdateUserRequest { + + /** + * The User id / guid + */ + @JsonIgnore + abstract String getUserId(); + + /** + * The metadata Labels and Annotations applied to the user + */ + @JsonProperty("metadata") + @Nullable + abstract Metadata getMetadata(); + +} \ No newline at end of file diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_UpdateUserResponse.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_UpdateUserResponse.java new file mode 100644 index 0000000000..b264d5bcb1 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_UpdateUserResponse.java @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2025 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.users; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.immutables.value.Value; + +/** + * The response payload for the Update User operation + */ +@JsonDeserialize +@Value.Immutable +abstract class _UpdateUserResponse extends User { +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_UserResource.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_UserResource.java new file mode 100644 index 0000000000..65c08410c9 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/users/_UserResource.java @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2025 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.users; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.immutables.value.Value; + +/** + * The Resource response payload for the List User operation + */ +@JsonDeserialize +@Value.Immutable +abstract class _UserResource extends User{ +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/doppler/DopplerClient.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/doppler/DopplerClient.java index a9c03441cf..74d23b1488 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/doppler/DopplerClient.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/doppler/DopplerClient.java @@ -42,9 +42,15 @@ public interface DopplerClient { /** * Makes the Recent Logs request * + * @deprecated Use {@link org.cloudfoundry.logcache.v1.LogCacheClient#recentLogs(org.cloudfoundry.logcache.v1.ReadRequest)} instead. + * Only works with {@code Loggregator < 107.0}, shipped in {@code CFD < 24.3} and {@code TAS < 4.0}. * @param request the Recent Logs request - * @return the events from the recent logs + * @return a flux of events from the recent logs + * @see Loggregator + * @see Log Cache + * @see org.cloudfoundry.logcache.v1.LogCacheClient#recentLogs(org.cloudfoundry.logcache.v1.ReadRequest) */ + @Deprecated Flux recentLogs(RecentLogsRequest request); /** diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/logcache/v1/LogCacheClient.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/logcache/v1/LogCacheClient.java index e455db220a..6b91461fe3 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/logcache/v1/LogCacheClient.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/logcache/v1/LogCacheClient.java @@ -16,6 +16,7 @@ package org.cloudfoundry.logcache.v1; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; /** @@ -46,4 +47,25 @@ public interface LogCacheClient { * @return the read response */ Mono read(ReadRequest request); + + /** + * Makes the Log Cache RecentLogs /api/v1/read request + * + * @param request the Recent Logs request + * @return the events from the recent logs + */ + Mono recentLogs(ReadRequest request); + + /** + * Continuously polls the Log Cache /api/v1/read endpoint and streams new {@link Envelope}s + * as they appear. This is the Java equivalent of the Go {@code logcache.Walk()} API and + * {@code cf tail --follow}. + *

+ * The returned {@link Flux} will never complete on its own – unsubscribe (or cancel) it to + * stop streaming. + * + * @param request the tail request (source id, optional filters, poll interval) + * @return an infinite stream of envelopes + */ + Flux logsTail(TailLogsRequest request); } diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/logcache/v1/_TailLogsRequest.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/logcache/v1/_TailLogsRequest.java new file mode 100644 index 0000000000..1ca32d388a --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/logcache/v1/_TailLogsRequest.java @@ -0,0 +1,65 @@ +/* + * 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.logcache.v1; + +import org.cloudfoundry.Nullable; +import org.immutables.value.Value; + +import java.time.Duration; +import java.util.List; + +/** + * The request options for the Log Cache tail (streaming follow) operation. + * This continuously polls the Log Cache /api/v1/read endpoint, emitting new envelopes + * as they appear – equivalent to {@code cf tail --follow} or the Go {@code logcache.Walk()} API. + */ +@Value.Immutable +abstract class _TailLogsRequest { + + /** + * The source id (application guid or service guid) to stream logs for. + */ + abstract String getSourceId(); + + /** + * Optional start time (UNIX nanoseconds). Defaults to "now – 5 seconds" when not set. + */ + @Nullable + abstract Long getStartTime(); + + /** + * Optional envelope type filter. + */ + @Nullable + abstract List getEnvelopeTypes(); + + /** + * Optional regex name filter (requires Log Cache ≥ 2.1.0). + */ + @Nullable + abstract String getNameFilter(); + + /** + * How long to wait between successive polls when no new envelopes are available. + * Defaults to 250 ms (matching the Go client's {@code AlwaysRetryBackoff}). + */ + @Value.Default + Duration getPollInterval() { + return Duration.ofMillis(250); + } +} + diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/UaaClient.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/UaaClient.java index 9c88d37f59..665fb920ee 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/UaaClient.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/UaaClient.java @@ -21,6 +21,7 @@ import org.cloudfoundry.uaa.groups.Groups; import org.cloudfoundry.uaa.identityproviders.IdentityProviders; import org.cloudfoundry.uaa.identityzones.IdentityZones; +import org.cloudfoundry.uaa.ratelimit.Ratelimit; import org.cloudfoundry.uaa.serverinformation.ServerInformation; import org.cloudfoundry.uaa.tokens.Tokens; import org.cloudfoundry.uaa.users.Users; @@ -80,4 +81,9 @@ public interface UaaClient { * Main entry point to the UAA User Client API */ Users users(); + + /** + * Main entry point to the UAA Ratelimit API + */ + Ratelimit rateLimit(); } diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/Ratelimit.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/Ratelimit.java new file mode 100644 index 0000000000..c277fa7331 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/Ratelimit.java @@ -0,0 +1,27 @@ +/* + * Copyright 2013-2025 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.uaa.ratelimit; + +import reactor.core.publisher.Mono; + +/** + * Main entry point to the UAA Ratelimit Client API + */ +public interface Ratelimit { + + Mono getRatelimit(RatelimitRequest request); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/_Current.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/_Current.java new file mode 100644 index 0000000000..57811e9e59 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/_Current.java @@ -0,0 +1,63 @@ +/* + * Copyright 2013-2025 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.uaa.ratelimit; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import java.util.Date; + +import org.immutables.value.Value; + +/** + * The payload for the uaa ratelimiting + */ +@JsonDeserialize +@Value.Immutable +abstract class _Current { + + /** + * The number of configured limiter mappings + */ + @JsonProperty("limiterMappings") + abstract Integer getLimiterMappings(); + + /** + * Is ratelimit "ACTIVE" or not? Possible values are DISABLED, PENDING, ACTIVE + */ + @JsonProperty("status") + abstract String getStatus(); + + /** + * Timestamp, when this Current was created. + */ + @JsonProperty("asOf") + abstract Date getTimeOfCurrent(); + + /** + * The credentialIdExtractor + */ + @JsonProperty("credentialIdExtractor") + abstract String getCredentialIdExtractor(); + + /** + * The loggingLevel. Valid values include: "OnlyLimited", "AllCalls" and "AllCallsWithDetails" + */ + @JsonProperty("loggingLevel") + abstract String getLoggingLevel(); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/_RatelimitRequest.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/_RatelimitRequest.java new file mode 100644 index 0000000000..be75d58bfa --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/_RatelimitRequest.java @@ -0,0 +1,24 @@ +/* + * Copyright 2013-2025 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.uaa.ratelimit; + +import org.immutables.value.Value; + +@Value.Immutable +abstract class _RatelimitRequest { + +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/_RatelimitResponse.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/_RatelimitResponse.java new file mode 100644 index 0000000000..0fe927cbf6 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/ratelimit/_RatelimitResponse.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2025 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.uaa.ratelimit; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.cloudfoundry.Nullable; +import org.immutables.value.Value; + +@JsonDeserialize +@Value.Immutable +abstract class _RatelimitResponse { + + @JsonProperty("current") + @Nullable + abstract Current getCurrentData(); + + @JsonProperty("fromSource") + @Nullable + abstract String getFromSource(); + +} diff --git a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/quotas/organizations/ApplyOrganizationQuotaRequestTest.java b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/quotas/organizations/ApplyOrganizationQuotaRequestTest.java new file mode 100644 index 0000000000..6a572188c9 --- /dev/null +++ b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/quotas/organizations/ApplyOrganizationQuotaRequestTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2013-2025 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.quotas.organizations; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.cloudfoundry.client.v3.Relationship; +import org.cloudfoundry.client.v3.ToManyRelationship; +import org.junit.jupiter.api.Test; + +final class ApplyOrganizationQuotaRequestTest { + + @Test + void noOrganizationRelationships() { + assertThrows( + IllegalStateException.class, + () -> + ApplyOrganizationQuotaRequest.builder() + .organizationQuotaId("quota-id") + .build()); + } + + @Test + void noOrganizationQuotaId() { + Relationship organizationRelationship = + Relationship.builder().id("organization-id").build(); + ToManyRelationship organizationRelationships = + ToManyRelationship.builder().data(organizationRelationship).build(); + + assertThrows( + IllegalStateException.class, + () -> + ApplyOrganizationQuotaRequest.builder() + .organizationRelationships(organizationRelationships) + .build()); + } + + @Test + void valid() { + Relationship organizationRelationship = + Relationship.builder().id("organization-id").build(); + ToManyRelationship organizationRelationships = + ToManyRelationship.builder().data(organizationRelationship).build(); + ApplyOrganizationQuotaRequest.builder() + .organizationQuotaId("quota-id") + .organizationRelationships(organizationRelationships) + .build(); + } +} diff --git a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/quotas/spaces/ApplySpaceQuotaRequestTest.java b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/quotas/spaces/ApplySpaceQuotaRequestTest.java new file mode 100644 index 0000000000..63b8b8061d --- /dev/null +++ b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/quotas/spaces/ApplySpaceQuotaRequestTest.java @@ -0,0 +1,58 @@ +/* + * Copyright 2013-2025 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.quotas.spaces; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.cloudfoundry.client.v3.Relationship; +import org.cloudfoundry.client.v3.ToManyRelationship; +import org.junit.jupiter.api.Test; + +final class ApplySpaceQuotaRequestTest { + + @Test + void noSpaceRelationships() { + assertThrows( + IllegalStateException.class, + () -> ApplySpaceQuotaRequest.builder().spaceQuotaId("quota-id").build()); + } + + @Test + void noSpaceQuotaId() { + Relationship spaceRelationship = Relationship.builder().id("space-id").build(); + ToManyRelationship spaceRelationships = + ToManyRelationship.builder().data(spaceRelationship).build(); + + assertThrows( + IllegalStateException.class, + () -> + ApplySpaceQuotaRequest.builder() + .spaceRelationships(spaceRelationships) + .build()); + } + + @Test + void valid() { + Relationship spaceRelationship = Relationship.builder().id("space-id").build(); + ToManyRelationship spaceRelationships = + ToManyRelationship.builder().data(spaceRelationship).build(); + ApplySpaceQuotaRequest.builder() + .spaceQuotaId("quota-id") + .spaceRelationships(spaceRelationships) + .build(); + } +} diff --git a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/quotas/spaces/RemoveSpaceQuotaRequestTest.java b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/quotas/spaces/RemoveSpaceQuotaRequestTest.java new file mode 100644 index 0000000000..64f8490b07 --- /dev/null +++ b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/quotas/spaces/RemoveSpaceQuotaRequestTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2013-2025 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.quotas.spaces; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +final class RemoveSpaceQuotaRequestTest { + + @Test + void noSpaceId() { + assertThrows( + IllegalStateException.class, + () -> RemoveSpaceQuotaRequest.builder().spaceQuotaId("quota-id").build()); + } + + @Test + void noSpaceQuotaId() { + assertThrows( + IllegalStateException.class, + () -> RemoveSpaceQuotaRequest.builder().spaceId("space-guid").build()); + } + + @Test + void valid() { + RemoveSpaceQuotaRequest.builder().spaceQuotaId("quota-id").spaceId("space-guid").build(); + } +} diff --git a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/users/CreateUserRequestTest.java b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/users/CreateUserRequestTest.java new file mode 100644 index 0000000000..66ad42259b --- /dev/null +++ b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/users/CreateUserRequestTest.java @@ -0,0 +1,27 @@ +/* + * Copyright 2013-2025 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.users; + +import org.junit.jupiter.api.Test; + +class CreateUserRequestTest { + + @Test + void valid() { + CreateUserRequest.builder().build(); + } +} diff --git a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/users/DeleteUserRequestTest.java b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/users/DeleteUserRequestTest.java new file mode 100644 index 0000000000..6d65354fb3 --- /dev/null +++ b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/users/DeleteUserRequestTest.java @@ -0,0 +1,34 @@ +/* + * Copyright 2013-2025 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.users; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class DeleteUserRequestTest { + + @Test + void noUserId() { + assertThrows(IllegalStateException.class, () -> DeleteUserRequest.builder().build()); + } + + @Test + void valid() { + DeleteUserRequest.builder().userId("test-stack-id").build(); + } +} diff --git a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/users/GetUserRequestTest.java b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/users/GetUserRequestTest.java new file mode 100644 index 0000000000..83bb8541b9 --- /dev/null +++ b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/users/GetUserRequestTest.java @@ -0,0 +1,34 @@ +/* + * Copyright 2013-2025 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.users; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class GetUserRequestTest { + + @Test + void noUserId() { + assertThrows(IllegalStateException.class, () -> GetUserRequest.builder().build()); + } + + @Test + void valid() { + GetUserRequest.builder().userId("test-user-id").build(); + } +} diff --git a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/users/ListUsersRequestTest.java b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/users/ListUsersRequestTest.java new file mode 100644 index 0000000000..bf30fb14ff --- /dev/null +++ b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/users/ListUsersRequestTest.java @@ -0,0 +1,27 @@ +/* + * Copyright 2013-2025 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.users; + +import org.junit.jupiter.api.Test; + +public class ListUsersRequestTest { + + @Test + void valid() { + ListUsersRequest.builder().build(); + } +} diff --git a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/users/UpdateUserRequestTest.java b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/users/UpdateUserRequestTest.java new file mode 100644 index 0000000000..8a1c131cda --- /dev/null +++ b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/users/UpdateUserRequestTest.java @@ -0,0 +1,34 @@ +/* + * Copyright 2013-2025 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.users; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +final class UpdateUserRequestTest { + + @Test + void noOrganizationQuotaId() { + assertThrows(IllegalStateException.class, () -> UpdateUserRequest.builder().build()); + } + + @Test + void valid() { + UpdateUserRequest.builder().userId("test-id").build(); + } +} diff --git a/cloudfoundry-operations/pom.xml b/cloudfoundry-operations/pom.xml index 5ab004f136..1bc8c7cb1b 100644 --- a/cloudfoundry-operations/pom.xml +++ b/cloudfoundry-operations/pom.xml @@ -25,7 +25,7 @@ org.cloudfoundry cloudfoundry-java-client - 5.15.0.BUILD-SNAPSHOT + 6.0.0-SNAPSHOT cloudfoundry-operations diff --git a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/_DefaultCloudFoundryOperations.java b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/_DefaultCloudFoundryOperations.java index 299b4bf5e4..4db0bd8489 100644 --- a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/_DefaultCloudFoundryOperations.java +++ b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/_DefaultCloudFoundryOperations.java @@ -23,6 +23,7 @@ import org.cloudfoundry.client.v3.spaces.ListSpacesRequest; import org.cloudfoundry.client.v3.spaces.SpaceResource; import org.cloudfoundry.doppler.DopplerClient; +import org.cloudfoundry.logcache.v1.LogCacheClient; import org.cloudfoundry.networking.NetworkingClient; import org.cloudfoundry.operations.advanced.Advanced; import org.cloudfoundry.operations.advanced.DefaultAdvanced; @@ -79,19 +80,31 @@ public Advanced advanced() { @Override @Value.Derived public Applications applications() { - return new DefaultApplications(getCloudFoundryClientPublisher(), getDopplerClientPublisher(), getSpaceId()); + return new DefaultApplications(getCloudFoundryClientPublisher(), getDopplerClientPublisher(), getLogCacheClientPublisher(), getSpaceId()); } @Override @Value.Derived public Buildpacks buildpacks() { - return new DefaultBuildpacks(getCloudFoundryClientPublisher()); + CloudFoundryClient cloudFoundryClient = getCloudFoundryClient(); + if (cloudFoundryClient == null) { + throw new IllegalStateException("CloudFoundryClient must be set"); + } + return new DefaultBuildpacks(cloudFoundryClient); } @Override @Value.Derived public Domains domains() { - return new DefaultDomains(getCloudFoundryClientPublisher(), getRoutingClientPublisher()); + CloudFoundryClient cloudFoundryClient = getCloudFoundryClient(); + if (cloudFoundryClient == null) { + throw new IllegalStateException("CloudFoundryClient must be set"); + } + RoutingClient routingClient = getRoutingClient(); + if (routingClient == null) { + throw new IllegalStateException("RoutingClient must be set"); + } + return new DefaultDomains(cloudFoundryClient, routingClient); } @Override @@ -185,6 +198,19 @@ Mono getDopplerClientPublisher() { .orElse(Mono.error(new IllegalStateException("DopplerClient must be set"))); } + /** + * The {@link LogCacheClient} to use for operations functionality + */ + @Nullable + abstract LogCacheClient getLogCacheClient(); + + @Value.Derived + Mono getLogCacheClientPublisher() { + return Optional.ofNullable(getLogCacheClient()) + .map(Mono::just) + .orElse(Mono.error(new IllegalStateException("LogCacheClient must be set"))); + } + /** * The {@link NetworkingClient} to use for operations functionality */ diff --git a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/Applications.java b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/Applications.java index 5196fef6c8..5d95db5d01 100644 --- a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/Applications.java +++ b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/Applications.java @@ -17,6 +17,10 @@ package org.cloudfoundry.operations.applications; import org.cloudfoundry.doppler.LogMessage; +import org.cloudfoundry.logcache.v1.Envelope; +import org.cloudfoundry.logcache.v1.Log; +import org.cloudfoundry.logcache.v1.ReadRequest; +import org.cloudfoundry.logcache.v1.TailLogsRequest; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -126,6 +130,25 @@ public interface Applications { @Deprecated Flux logs(LogsRequest request); + /** + * List the applications logs from logCacheClient. + * If no messages are available, an empty Flux is returned. + * + * @param request the application logs request + * @return the applications logs + */ + Flux logsRecent(ReadRequest request); + + /** + * Continuously streams application log envelopes from Log Cache by repeatedly polling + * the {@code /api/v1/read} endpoint. The returned {@link Flux} is infinite – cancel it + * to stop streaming. This is the Java equivalent of {@code cf tail --follow}. + * + * @param request the tail request (source id, optional filters, poll interval) + * @return an infinite stream of envelopes + */ + Flux logsTail(TailLogsRequest request); + /** * List the applications logs. * Only works with {@code Loggregator < 107.0}, shipped in {@code CFD < 24.3} diff --git a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/DefaultApplications.java b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/DefaultApplications.java index e51ddbb472..23d1fb70cd 100644 --- a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/DefaultApplications.java +++ b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/DefaultApplications.java @@ -154,6 +154,11 @@ import org.cloudfoundry.doppler.LogMessage; import org.cloudfoundry.doppler.RecentLogsRequest; import org.cloudfoundry.doppler.StreamRequest; +import org.cloudfoundry.logcache.v1.EnvelopeBatch; +import org.cloudfoundry.logcache.v1.Log; +import org.cloudfoundry.logcache.v1.LogCacheClient; +import org.cloudfoundry.logcache.v1.ReadRequest; +import org.cloudfoundry.logcache.v1.TailLogsRequest; import org.cloudfoundry.operations.util.OperationsLogging; import org.cloudfoundry.util.DateUtils; import org.cloudfoundry.util.DelayTimeoutException; @@ -200,6 +205,10 @@ public final class DefaultApplications implements Applications { private static final Comparator LOG_MESSAGE_COMPARATOR = Comparator.comparing(LogMessage::getTimestamp); + private static final Comparator + LOG_MESSAGE_COMPARATOR_LOG_CACHE = + Comparator.comparing(org.cloudfoundry.logcache.v1.Envelope::getTimestamp); + private static final Duration LOG_MESSAGE_TIMESPAN = Duration.ofMillis(500); private static final int MAX_NUMBER_OF_RECENT_EVENTS = 50; @@ -214,6 +223,8 @@ public final class DefaultApplications implements Applications { private final Mono dopplerClient; + private final Mono logCacheClient; + private final RandomWords randomWords; private final Mono spaceId; @@ -221,17 +232,20 @@ public final class DefaultApplications implements Applications { public DefaultApplications( Mono cloudFoundryClient, Mono dopplerClient, + Mono logCacheClient, Mono spaceId) { - this(cloudFoundryClient, dopplerClient, new WordListRandomWords(), spaceId); + this(cloudFoundryClient, dopplerClient, logCacheClient, new WordListRandomWords(), spaceId); } DefaultApplications( Mono cloudFoundryClient, Mono dopplerClient, + Mono logCacheClient, RandomWords randomWords, Mono spaceId) { this.cloudFoundryClient = cloudFoundryClient; this.dopplerClient = dopplerClient; + this.logCacheClient = logCacheClient; this.randomWords = randomWords; this.spaceId = spaceId; } @@ -529,6 +543,7 @@ public Flux listTasks(ListApplicationTasksRequest request) { .checkpoint(); } + @Deprecated @Override public Flux logs(LogsRequest request) { return Mono.zip(this.cloudFoundryClient, this.spaceId) @@ -544,6 +559,21 @@ public Flux logs(LogsRequest request) { .checkpoint(); } + @Override + public Flux logsRecent(ReadRequest request) { + return getRecentLogsLogCache(this.logCacheClient, request) + .transform(OperationsLogging.log("Get Application Logs")) + .checkpoint(); + } + + @Override + public Flux logsTail(TailLogsRequest request) { + return this.logCacheClient + .flatMapMany(client -> client.logsTail(request)) + .transform(OperationsLogging.log("Tail Application Logs")) + .checkpoint(); + } + @Override public Flux logs(ApplicationLogsRequest request) { return logs(LogsRequest.builder() @@ -673,7 +703,6 @@ public Mono pushManifestV3(PushManifestV3Request request) { } catch (IOException e) { throw new RuntimeException("Could not serialize manifest", e); } - return Mono.zip(this.cloudFoundryClient, this.spaceId) .flatMap( function( @@ -1617,6 +1646,17 @@ private static Flux getLogs( } } + private static Flux getRecentLogsLogCache( + Mono logCacheClient, ReadRequest readRequest) { + return requestLogsRecentLogCache(logCacheClient, readRequest) + .map(EnvelopeBatch::getBatch) + .map(List::stream) + .flatMapIterable(envelopeStream -> envelopeStream.collect(Collectors.toList())) + .filter(e -> e.getLog() != null) + .sort(LOG_MESSAGE_COMPARATOR_LOG_CACHE) + .map(org.cloudfoundry.logcache.v1.Envelope::getLog); + } + @SuppressWarnings("unchecked") private static Map getMetadataRequest(EventEntity entity) { Map> metadata = @@ -2501,6 +2541,7 @@ private static Flux requestListTasks( .build())); } + @Deprecated private static Flux requestLogsRecent( Mono dopplerClient, String applicationId) { return dopplerClient.flatMapMany( @@ -2509,6 +2550,14 @@ private static Flux requestLogsRecent( RecentLogsRequest.builder().applicationId(applicationId).build())); } + private static Mono requestLogsRecentLogCache( + Mono logCacheClient, ReadRequest readRequest) { + return logCacheClient.flatMap( + client -> + client.recentLogs(readRequest) + .flatMap(response -> Mono.justOrEmpty(response.getEnvelopes()))); + } + private static Flux requestLogsStream( Mono dopplerClient, String applicationId) { return dopplerClient.flatMapMany( diff --git a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/buildpacks/DefaultBuildpacks.java b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/buildpacks/DefaultBuildpacks.java index ad2958886e..862992e1b1 100644 --- a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/buildpacks/DefaultBuildpacks.java +++ b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/buildpacks/DefaultBuildpacks.java @@ -16,104 +16,85 @@ package org.cloudfoundry.operations.buildpacks; -import static org.cloudfoundry.util.tuple.TupleUtils.function; - import java.nio.file.Path; import java.time.Duration; import java.util.Optional; import org.cloudfoundry.client.CloudFoundryClient; -import org.cloudfoundry.client.v2.buildpacks.BuildpackEntity; -import org.cloudfoundry.client.v2.buildpacks.BuildpackResource; -import org.cloudfoundry.client.v2.buildpacks.CreateBuildpackResponse; -import org.cloudfoundry.client.v2.buildpacks.DeleteBuildpackResponse; -import org.cloudfoundry.client.v2.buildpacks.ListBuildpacksRequest; -import org.cloudfoundry.client.v2.buildpacks.UpdateBuildpackResponse; -import org.cloudfoundry.client.v2.buildpacks.UploadBuildpackRequest; -import org.cloudfoundry.client.v2.buildpacks.UploadBuildpackResponse; +import org.cloudfoundry.client.v3.buildpacks.BuildpackResource; +import org.cloudfoundry.client.v3.buildpacks.BuildpackState; +import org.cloudfoundry.client.v3.buildpacks.CreateBuildpackResponse; +import org.cloudfoundry.client.v3.buildpacks.GetBuildpackRequest; +import org.cloudfoundry.client.v3.buildpacks.GetBuildpackResponse; +import org.cloudfoundry.client.v3.buildpacks.ListBuildpacksRequest; +import org.cloudfoundry.client.v3.buildpacks.UpdateBuildpackResponse; +import org.cloudfoundry.client.v3.buildpacks.UploadBuildpackRequest; +import org.cloudfoundry.client.v3.buildpacks.UploadBuildpackResponse; import org.cloudfoundry.operations.util.OperationsLogging; import org.cloudfoundry.util.ExceptionUtils; import org.cloudfoundry.util.JobUtils; import org.cloudfoundry.util.PaginationUtils; -import org.cloudfoundry.util.ResourceUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import reactor.util.retry.Retry; public final class DefaultBuildpacks implements Buildpacks { - private final Mono cloudFoundryClient; + private final CloudFoundryClient cloudFoundryClient; - public DefaultBuildpacks(Mono cloudFoundryClient) { + public DefaultBuildpacks(CloudFoundryClient cloudFoundryClient) { this.cloudFoundryClient = cloudFoundryClient; } + /** + * Create a new instance. + * + * @deprecated Use {@link DefaultBuildpacks(CloudFoundryClient)} instead. + */ + @Deprecated + public DefaultBuildpacks(Mono cloudFoundryClient) { + this.cloudFoundryClient = + cloudFoundryClient.subscribeOn(Schedulers.boundedElastic()).block(); + } + @Override public Mono create(CreateBuildpackRequest request) { - return this.cloudFoundryClient - .flatMap( - cloudFoundryClient -> - Mono.zip( - Mono.just(cloudFoundryClient), - requestCreateBuildpack( - cloudFoundryClient, - request.getName(), - request.getPosition(), - request.getEnable()))) + return requestCreateBuildpack( + this.cloudFoundryClient, + request.getName(), + request.getPosition(), + request.getEnable()) .flatMap( - function( - (cloudFoundryClient, response) -> - requestUploadBuildpackBits( - cloudFoundryClient, - ResourceUtils.getId(response), - request.getBuildpack()))) - .then() + response -> + requestUploadBuildpackBits( + response.getId(), request.getBuildpack())) + .map(UploadBuildpackResponse::getId) + .flatMap(this::waitForBuildpackReady) .transform(OperationsLogging.log("Create Buildpack")) .checkpoint(); } @Override public Mono delete(DeleteBuildpackRequest request) { - return this.cloudFoundryClient - .flatMap( - cloudFoundryClient -> - Mono.zip( - getBuildPackId(cloudFoundryClient, request.getName()), - Mono.just(cloudFoundryClient))) + return getBuildPackId(request.getName()) .flatMap( - function( - (buildpackId, cloudFoundryClient) -> - deleteBuildpack( - cloudFoundryClient, - buildpackId, - request.getCompletionTimeout()))) - .then() + buildpackId -> deleteBuildpack(buildpackId, request.getCompletionTimeout())) .transform(OperationsLogging.log("Delete Buildpack")) .checkpoint(); } @Override public Flux list() { - return this.cloudFoundryClient - .flatMapMany(DefaultBuildpacks::requestBuildpacks) - .map(DefaultBuildpacks::toBuildpackResource) + return requestBuildpacks(this.cloudFoundryClient) + .map(this::toBuildpackResource) .transform(OperationsLogging.log("List Buildpacks")) .checkpoint(); } @Override public Mono rename(RenameBuildpackRequest request) { - return this.cloudFoundryClient - .flatMap( - cloudFoundryClient -> - Mono.zip( - getBuildPackId(cloudFoundryClient, request.getName()), - Mono.just(cloudFoundryClient))) - .flatMap( - function( - (buildpackId, cloudFoundryClient) -> - requestUpdateBuildpack( - cloudFoundryClient, - buildpackId, - request.getNewName()))) + return getBuildPackId(request.getName()) + .flatMap(buildpackId -> requestUpdateBuildpack(buildpackId, request.getNewName())) .then() .transform(OperationsLogging.log("Rename Buildpack")) .checkpoint(); @@ -121,53 +102,44 @@ public Mono rename(RenameBuildpackRequest request) { @Override public Mono update(UpdateBuildpackRequest request) { - return this.cloudFoundryClient + return getBuildPackId(request.getName()) .flatMap( - cloudFoundryClient -> - Mono.zip( - getBuildPackId(cloudFoundryClient, request.getName()), - Mono.just(cloudFoundryClient))) - .flatMap( - function( - (buildpackId, cloudFoundryClient) -> - Mono.when( - requestUpdateBuildpack( - cloudFoundryClient, buildpackId, request), - uploadBuildpackBits( - cloudFoundryClient, buildpackId, request)))) + buildpackId -> + Mono.when( + requestUpdateBuildpack(buildpackId, request), + uploadBuildpackBits(buildpackId, request)) + .then(Mono.just(buildpackId))) + .flatMap(this::waitForBuildpackReady) .then() .transform(OperationsLogging.log("Update Buildpack")) .checkpoint(); } - private static Mono deleteBuildpack( - CloudFoundryClient cloudFoundryClient, String buildpackId, Duration timeout) { - return requestDeleteBuildpack(cloudFoundryClient, buildpackId) - .flatMap(job -> JobUtils.waitForCompletion(cloudFoundryClient, timeout, job)); + private Mono deleteBuildpack(String buildpackId, Duration timeout) { + return requestDeleteBuildpack(buildpackId) + .flatMap(job -> JobUtils.waitForCompletion(this.cloudFoundryClient, timeout, job)); } - private static Mono getBuildPackId(CloudFoundryClient cloudFoundryClient, String name) { - return requestBuildpacks(cloudFoundryClient, name) + private Mono getBuildPackId(String name) { + return requestBuildpacks(name) .singleOrEmpty() - .map(ResourceUtils::getId) + .map(BuildpackResource::getId) .switchIfEmpty(ExceptionUtils.illegalArgument("Buildpack %s not found", name)); } - private static Flux requestBuildpacks( - CloudFoundryClient cloudFoundryClient) { - return PaginationUtils.requestClientV2Resources( + private Flux requestBuildpacks(CloudFoundryClient cloudFoundryClient) { + return PaginationUtils.requestClientV3Resources( page -> cloudFoundryClient - .buildpacks() + .buildpacksV3() .list(ListBuildpacksRequest.builder().page(page).build())); } - private static Flux requestBuildpacks( - CloudFoundryClient cloudFoundryClient, String name) { - return PaginationUtils.requestClientV2Resources( + private Flux requestBuildpacks(String name) { + return PaginationUtils.requestClientV3Resources( page -> cloudFoundryClient - .buildpacks() + .buildpacksV3() .list( ListBuildpacksRequest.builder() .name(name) @@ -175,40 +147,36 @@ private static Flux requestBuildpacks( .build())); } - private static Mono requestCreateBuildpack( + private Mono requestCreateBuildpack( CloudFoundryClient cloudFoundryClient, String buildpackName, Integer position, Boolean enable) { return cloudFoundryClient - .buildpacks() + .buildpacksV3() .create( - org.cloudfoundry.client.v2.buildpacks.CreateBuildpackRequest.builder() + org.cloudfoundry.client.v3.buildpacks.CreateBuildpackRequest.builder() .name(buildpackName) .position(position) .enabled(Optional.ofNullable(enable).orElse(true)) .build()); } - private static Mono requestDeleteBuildpack( - CloudFoundryClient cloudFoundryClient, String buildpackId) { + private Mono requestDeleteBuildpack(String buildpackId) { return cloudFoundryClient - .buildpacks() + .buildpacksV3() .delete( - org.cloudfoundry.client.v2.buildpacks.DeleteBuildpackRequest.builder() - .async(true) + org.cloudfoundry.client.v3.buildpacks.DeleteBuildpackRequest.builder() .buildpackId(buildpackId) .build()); } - private static Mono requestUpdateBuildpack( - CloudFoundryClient cloudFoundryClient, - String buildpackId, - UpdateBuildpackRequest request) { + private Mono requestUpdateBuildpack( + String buildpackId, UpdateBuildpackRequest request) { return cloudFoundryClient - .buildpacks() + .buildpacksV3() .update( - org.cloudfoundry.client.v2.buildpacks.UpdateBuildpackRequest.builder() + org.cloudfoundry.client.v3.buildpacks.UpdateBuildpackRequest.builder() .buildpackId(buildpackId) .enabled(request.getEnable()) .locked(request.getLock()) @@ -216,36 +184,32 @@ private static Mono requestUpdateBuildpack( .build()); } - private static Mono requestUpdateBuildpack( - CloudFoundryClient cloudFoundryClient, String buildpackId, String name) { + private Mono requestUpdateBuildpack(String buildpackId, String name) { return cloudFoundryClient - .buildpacks() + .buildpacksV3() .update( - org.cloudfoundry.client.v2.buildpacks.UpdateBuildpackRequest.builder() + org.cloudfoundry.client.v3.buildpacks.UpdateBuildpackRequest.builder() .buildpackId(buildpackId) .name(name) .build()); } - private static Mono requestUploadBuildpackBits( - CloudFoundryClient cloudFoundryClient, String buildpackId, Path buildpack) { + private Mono requestUploadBuildpackBits( + String buildpackId, Path buildpack) { return cloudFoundryClient - .buildpacks() + .buildpacksV3() .upload( UploadBuildpackRequest.builder() .buildpackId(buildpackId) - .filename(buildpack.getFileName().toString()) - .buildpack(buildpack) + .bits(buildpack) .build()); } - private static Buildpack toBuildpackResource(BuildpackResource resource) { - BuildpackEntity entity = ResourceUtils.getEntity(resource); - + private Buildpack toBuildpackResource(BuildpackResource entity) { return Buildpack.builder() .enabled(entity.getEnabled()) .filename(entity.getFilename()) - .id(ResourceUtils.getId(resource)) + .id(entity.getId()) .locked(entity.getLocked()) .name(entity.getName()) .position(entity.getPosition()) @@ -253,16 +217,31 @@ private static Buildpack toBuildpackResource(BuildpackResource resource) { .build(); } - private static Mono uploadBuildpackBits( - CloudFoundryClient cloudFoundryClient, - String buildpackId, - UpdateBuildpackRequest request) { + private Mono uploadBuildpackBits(String buildpackId, UpdateBuildpackRequest request) { if (request.getBuildpack() != null) { - return requestUploadBuildpackBits( - cloudFoundryClient, buildpackId, request.getBuildpack()) + return requestUploadBuildpackBits(buildpackId, request.getBuildpack()) .then(Mono.empty()); } return Mono.empty(); } + + private Mono waitForBuildpackReady(String buildpackId) { + return requestBuildpack(buildpackId) + .flatMap( + buildpack -> { + if (!buildpack.getState().equals(BuildpackState.READY)) { + return Mono.error(new IllegalStateException("Not ready")); + } + return Mono.empty(); + }) + .retryWhen(Retry.backoff(3, Duration.ofSeconds(1))) + .then(); + } + + private Mono requestBuildpack(String buildpackId) { + return cloudFoundryClient + .buildpacksV3() + .get(GetBuildpackRequest.builder().buildpackId(buildpackId).build()); + } } diff --git a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/domains/DefaultDomains.java b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/domains/DefaultDomains.java index aa30f42811..3df9c0d4a0 100644 --- a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/domains/DefaultDomains.java +++ b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/domains/DefaultDomains.java @@ -18,22 +18,26 @@ import static org.cloudfoundry.util.tuple.TupleUtils.function; +import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.stream.Collectors; import org.cloudfoundry.client.CloudFoundryClient; -import org.cloudfoundry.client.v2.organizations.AssociateOrganizationPrivateDomainRequest; -import org.cloudfoundry.client.v2.organizations.AssociateOrganizationPrivateDomainResponse; -import org.cloudfoundry.client.v2.organizations.ListOrganizationsRequest; -import org.cloudfoundry.client.v2.organizations.OrganizationResource; -import org.cloudfoundry.client.v2.organizations.RemoveOrganizationPrivateDomainRequest; -import org.cloudfoundry.client.v2.privatedomains.CreatePrivateDomainRequest; -import org.cloudfoundry.client.v2.privatedomains.CreatePrivateDomainResponse; import org.cloudfoundry.client.v2.privatedomains.ListPrivateDomainsRequest; -import org.cloudfoundry.client.v2.privatedomains.PrivateDomainEntity; import org.cloudfoundry.client.v2.privatedomains.PrivateDomainResource; import org.cloudfoundry.client.v2.shareddomains.CreateSharedDomainResponse; import org.cloudfoundry.client.v2.shareddomains.ListSharedDomainsRequest; -import org.cloudfoundry.client.v2.shareddomains.SharedDomainEntity; import org.cloudfoundry.client.v2.shareddomains.SharedDomainResource; +import org.cloudfoundry.client.v3.Relationship; +import org.cloudfoundry.client.v3.ToOneRelationship; +import org.cloudfoundry.client.v3.domains.CreateDomainResponse; +import org.cloudfoundry.client.v3.domains.DomainRelationships; +import org.cloudfoundry.client.v3.domains.DomainResource; +import org.cloudfoundry.client.v3.domains.ListDomainsRequest; +import org.cloudfoundry.client.v3.domains.ShareDomainResponse; +import org.cloudfoundry.client.v3.organizations.ListOrganizationsRequest; +import org.cloudfoundry.client.v3.organizations.OrganizationResource; import org.cloudfoundry.operations.util.OperationsLogging; import org.cloudfoundry.routing.RoutingClient; import org.cloudfoundry.routing.v1.routergroups.ListRouterGroupsResponse; @@ -45,89 +49,82 @@ public final class DefaultDomains implements Domains { - private final Mono cloudFoundryClient; + private final CloudFoundryClient cloudFoundryClient; - private final Mono routingClient; + private final RoutingClient routingClient; - public DefaultDomains( - Mono cloudFoundryClient, Mono routingClient) { + public DefaultDomains(CloudFoundryClient cloudFoundryClient, RoutingClient routingClient) { this.cloudFoundryClient = cloudFoundryClient; this.routingClient = routingClient; } + /** + * @deprecated use {@link DefaultDomains(CloudFoundryClient, RoutingClient)} instead. + */ + @Deprecated + public DefaultDomains( + Mono cloudFoundryClient, Mono routingClient) { + this.cloudFoundryClient = cloudFoundryClient.block(); + this.routingClient = routingClient.block(); + } + @Override public Mono create(CreateDomainRequest request) { - return this.cloudFoundryClient - .flatMap( - cloudFoundryClient -> - Mono.zip( - Mono.just(cloudFoundryClient), - getOrganizationId( - cloudFoundryClient, request.getOrganization()))) + Mono> organizationId = + Mono.justOrEmpty(request.getOrganization()) + .flatMap(this::getOrganization) + .map(OrganizationResource::getId) + .map(Optional::of) + .defaultIfEmpty(Optional.empty()); + + Mono> groupId = + Mono.justOrEmpty(request.getRouterGroup()) + .flatMap(this::getRouterGroupId) + .map(Optional::of) + .defaultIfEmpty(Optional.empty()); + + return Mono.zip(organizationId, groupId) .flatMap( function( - (cloudFoundryClient, organizationId) -> + (oId, gId) -> requestCreateDomain( - cloudFoundryClient, request.getDomain(), - organizationId))) + oId.orElse(null), + gId.orElse(null)))) .then() .transform(OperationsLogging.log("Create Domain")) .checkpoint(); } @Override + @Deprecated public Mono createShared(CreateSharedDomainRequest request) { - if (request.getRouterGroup() == null) { - return this.cloudFoundryClient - .flatMap( - cloudFoundryClient -> - requestCreateSharedDomain( - cloudFoundryClient, request.getDomain(), null)) - .then() - .transform(OperationsLogging.log("Create Shared Domain")) - .checkpoint(); - } else { - return Mono.zip(this.cloudFoundryClient, this.routingClient) - .flatMap( - function( - (cloudFoundryClient, routingClient) -> - Mono.zip( - Mono.just(cloudFoundryClient), - getRouterGroupId( - routingClient, - request.getRouterGroup())))) - .flatMap( - function( - (cloudFoundryClient, routerGroupId) -> - requestCreateSharedDomain( - cloudFoundryClient, - request.getDomain(), - routerGroupId))) - .then() - .transform(OperationsLogging.log("Create Shared Domain")) - .checkpoint(); - } + return create( + CreateDomainRequest.builder() + .domain(request.getDomain()) + .routerGroup(request.getRouterGroup()) + .build()); } @Override public Flux list() { - return this.cloudFoundryClient + return requestListRouterGroups() + .map(ListRouterGroupsResponse::getRouterGroups) + .map(DefaultDomains::indexRouterGroupsById) .flatMapMany( - cloudFoundryClient -> - requestListPrivateDomains(cloudFoundryClient) - .map(DefaultDomains::toDomain) - .mergeWith( - requestListSharedDomains(cloudFoundryClient) - .map(DefaultDomains::toDomain))) + routerGroupsIndexedById -> + requestListDomains() + .map( + domain -> + DefaultDomains.toDomain( + domain, routerGroupsIndexedById))) .transform(OperationsLogging.log("List Domains")) .checkpoint(); } @Override public Flux listRouterGroups() { - return this.routingClient - .flatMapMany(DefaultDomains::requestListRouterGroups) + return requestListRouterGroups() .flatMapIterable(ListRouterGroupsResponse::getRouterGroups) .map(DefaultDomains::toRouterGroup) .transform(OperationsLogging.log("List Router Groups")) @@ -136,15 +133,10 @@ public Flux listRouterGroups() { @Override public Mono share(ShareDomainRequest request) { - return this.cloudFoundryClient - .flatMap( - cloudFoundryClient -> - Mono.zip( - Mono.just(cloudFoundryClient), - getPrivateDomainId(cloudFoundryClient, request.getDomain()), - getOrganizationId( - cloudFoundryClient, request.getOrganization()))) - .flatMap(function(DefaultDomains::requestAssociateOrganizationPrivateDomainRequest)) + return Mono.zip( + getDomainId(request.getDomain()), + getOrganizationId(request.getOrganization())) + .flatMap(function(this::requestShareDomain)) .then() .transform(OperationsLogging.log("Share Domain")) .checkpoint(); @@ -152,22 +144,16 @@ public Mono share(ShareDomainRequest request) { @Override public Mono unshare(UnshareDomainRequest request) { - return this.cloudFoundryClient - .flatMap( - cloudFoundryClient -> - Mono.zip( - Mono.just(cloudFoundryClient), - getPrivateDomainId(cloudFoundryClient, request.getDomain()), - getOrganizationId( - cloudFoundryClient, request.getOrganization()))) - .flatMap(function(DefaultDomains::requestRemoveOrganizationPrivateDomainRequest)) + return Mono.zip( + getDomainId(request.getDomain()), + getOrganizationId(request.getOrganization())) + .flatMap(function(this::requestUnshareDomain)) .transform(OperationsLogging.log("Unshare Domain")) .checkpoint(); } - private static Mono getOrganization( - CloudFoundryClient cloudFoundryClient, String organization) { - return requestOrganizations(cloudFoundryClient, organization) + private Mono getOrganization(String organization) { + return requestOrganizations(organization) .single() .onErrorResume( NoSuchElementException.class, @@ -176,14 +162,14 @@ private static Mono getOrganization( "Organization %s does not exist", organization)); } - private static Mono getOrganizationId( - CloudFoundryClient cloudFoundryClient, String organization) { - return getOrganization(cloudFoundryClient, organization).map(ResourceUtils::getId); + private Mono getOrganizationId(String organization) { + return getOrganization(organization).map(OrganizationResource::getId); } - private static Mono getPrivateDomain( - CloudFoundryClient cloudFoundryClient, String domain) { - return requestListPrivateDomains(cloudFoundryClient, domain) + private Mono getDomainId(String domain) { + return this.requestListDomains() + .filter(d -> d.getName().equals(domain)) + .map(DomainResource::getId) .single() .onErrorResume( NoSuchElementException.class, @@ -192,45 +178,63 @@ private static Mono getPrivateDomain( "Private domain %s does not exist", domain)); } - private static Mono getPrivateDomainId( - CloudFoundryClient cloudFoundryClient, String domain) { - return getPrivateDomain(cloudFoundryClient, domain).map(ResourceUtils::getId); + private Mono getPrivateDomain(String domain) { + return requestListPrivateDomains(domain) + .single() + .onErrorResume( + NoSuchElementException.class, + t -> + ExceptionUtils.illegalArgument( + "Private domain %s does not exist", domain)); } - private static Mono getRouterGroupId(RoutingClient routingClient, String routerGroup) { - return requestListRouterGroups(routingClient) + private Mono getPrivateDomainId(String domain) { + return getPrivateDomain(domain).map(ResourceUtils::getId); + } + + private Mono getRouterGroupId(String routerGroup) { + return requestListRouterGroups() .flatMapIterable(ListRouterGroupsResponse::getRouterGroups) .filter(group -> routerGroup.equals(group.getName())) .single() .map(org.cloudfoundry.routing.v1.routergroups.RouterGroup::getRouterGroupId); } - private static Mono - requestAssociateOrganizationPrivateDomainRequest( - CloudFoundryClient cloudFoundryClient, String domainId, String organizationId) { - return cloudFoundryClient - .organizations() - .associatePrivateDomain( - AssociateOrganizationPrivateDomainRequest.builder() - .organizationId(organizationId) - .privateDomainId(domainId) + private Mono requestShareDomain(String domainId, String organizationId) { + return this.cloudFoundryClient + .domainsV3() + .share( + org.cloudfoundry.client.v3.domains.ShareDomainRequest.builder() + .domainId(domainId) + .data(Relationship.builder().id(organizationId).build()) .build()); } - private static Mono requestCreateDomain( - CloudFoundryClient cloudFoundryClient, String domain, String organizationId) { - return cloudFoundryClient - .privateDomains() - .create( - CreatePrivateDomainRequest.builder() - .name(domain) - .owningOrganizationId(organizationId) - .build()); + private Mono requestCreateDomain( + String domain, String organizationId, String routerGroupId) { + org.cloudfoundry.client.v3.domains.CreateDomainRequest.Builder createDomainRequest = + org.cloudfoundry.client.v3.domains.CreateDomainRequest.builder().name(domain); + if (organizationId != null) { + createDomainRequest.relationships( + DomainRelationships.builder() + .organization( + ToOneRelationship.builder() + .data(Relationship.builder().id(organizationId).build()) + .build()) + .build()); + } + if (routerGroupId != null) { + createDomainRequest.routerGroup( + org.cloudfoundry.client.v3.domains.RouterGroup.builder() + .id(routerGroupId) + .build()); + } + return this.cloudFoundryClient.domainsV3().create(createDomainRequest.build()); } - private static Mono requestCreateSharedDomain( - CloudFoundryClient cloudFoundryClient, String domain, String routerGroupId) { - return cloudFoundryClient + private Mono requestCreateSharedDomain( + String domain, String routerGroupId) { + return this.cloudFoundryClient .sharedDomains() .create( org.cloudfoundry.client.v2.shareddomains.CreateSharedDomainRequest.builder() @@ -239,11 +243,10 @@ private static Mono requestCreateSharedDomain( .build()); } - private static Flux requestListPrivateDomains( - CloudFoundryClient cloudFoundryClient, String domain) { + private Flux requestListPrivateDomains(String domain) { return PaginationUtils.requestClientV2Resources( page -> - cloudFoundryClient + this.cloudFoundryClient .privateDomains() .list( ListPrivateDomainsRequest.builder() @@ -252,39 +255,35 @@ private static Flux requestListPrivateDomains( .build())); } - private static Flux requestListPrivateDomains( - CloudFoundryClient cloudFoundryClient) { - return PaginationUtils.requestClientV2Resources( + private Flux requestListDomains() { + return PaginationUtils.requestClientV3Resources( page -> - cloudFoundryClient - .privateDomains() - .list(ListPrivateDomainsRequest.builder().page(page).build())); + this.cloudFoundryClient + .domainsV3() + .list(ListDomainsRequest.builder().page(page).build())); } - private static Mono requestListRouterGroups( - RoutingClient routingClient) { - return routingClient + private Mono requestListRouterGroups() { + return this.routingClient .routerGroups() .list( org.cloudfoundry.routing.v1.routergroups.ListRouterGroupsRequest.builder() .build()); } - private static Flux requestListSharedDomains( - CloudFoundryClient cloudFoundryClient) { + private Flux requestListSharedDomains() { return PaginationUtils.requestClientV2Resources( page -> - cloudFoundryClient + this.cloudFoundryClient .sharedDomains() .list(ListSharedDomainsRequest.builder().page(page).build())); } - private static Flux requestOrganizations( - CloudFoundryClient cloudFoundryClient, String organization) { - return PaginationUtils.requestClientV2Resources( + private Flux requestOrganizations(String organization) { + return PaginationUtils.requestClientV3Resources( page -> - cloudFoundryClient - .organizations() + this.cloudFoundryClient + .organizationsV3() .list( ListOrganizationsRequest.builder() .name(organization) @@ -292,36 +291,39 @@ private static Flux requestOrganizations( .build())); } - private static Mono requestRemoveOrganizationPrivateDomainRequest( - CloudFoundryClient cloudFoundryClient, String domainId, String organizationId) { - return cloudFoundryClient - .organizations() - .removePrivateDomain( - RemoveOrganizationPrivateDomainRequest.builder() + private Mono requestUnshareDomain(String domainId, String organizationId) { + return this.cloudFoundryClient + .domainsV3() + .unshare( + org.cloudfoundry.client.v3.domains.UnshareDomainRequest.builder() .organizationId(organizationId) - .privateDomainId(domainId) + .domainId(domainId) .build()); } - private static Domain toDomain(PrivateDomainResource resource) { - PrivateDomainEntity entity = ResourceUtils.getEntity(resource); - + private static Domain toDomain(DomainResource entity, Map type) { return Domain.builder() - .id(ResourceUtils.getId(resource)) + .id(entity.getId()) .name(entity.getName()) - .status(Status.OWNED) + .status( + entity.getRelationships().getOrganization().getData() != null + ? Status.OWNED + : Status.SHARED) + .type( + entity.getRouterGroup() != null + ? type.get(entity.getRouterGroup().getId()) + : null) .build(); } - private static Domain toDomain(SharedDomainResource resource) { - SharedDomainEntity entity = ResourceUtils.getEntity(resource); - - return Domain.builder() - .id(ResourceUtils.getId(resource)) - .name(entity.getName()) - .status(Status.SHARED) - .type(entity.getRouterGroupType()) - .build(); + private static Map indexRouterGroupsById( + List routeGroups) { + return routeGroups.stream() + .collect( + Collectors.toMap( + org.cloudfoundry.routing.v1.routergroups.RouterGroup + ::getRouterGroupId, + org.cloudfoundry.routing.v1.routergroups.RouterGroup::getType)); } private static RouterGroup toRouterGroup( diff --git a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/domains/Domains.java b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/domains/Domains.java index 74ffa068b8..e32fc01ead 100644 --- a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/domains/Domains.java +++ b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/domains/Domains.java @@ -37,7 +37,9 @@ public interface Domains { * * @param request the Create Shared Domain request * @return a completion indicator + * @deprecated use {@link #create} instead */ + @Deprecated Mono createShared(CreateSharedDomainRequest request); /** diff --git a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/domains/_CreateDomainRequest.java b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/domains/_CreateDomainRequest.java index 924e47e2a8..04d4d2bcfc 100644 --- a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/domains/_CreateDomainRequest.java +++ b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/domains/_CreateDomainRequest.java @@ -16,6 +16,7 @@ package org.cloudfoundry.operations.domains; +import org.cloudfoundry.Nullable; import org.immutables.value.Value; /** @@ -32,6 +33,13 @@ abstract class _CreateDomainRequest { /** * The organization name of the domain */ + @Nullable abstract String getOrganization(); + /** + * The router group of the domain + */ + @Nullable + abstract String getRouterGroup(); + } diff --git a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/stacks/DefaultStacks.java b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/stacks/DefaultStacks.java index b57dbca983..5ba88cc57e 100644 --- a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/stacks/DefaultStacks.java +++ b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/stacks/DefaultStacks.java @@ -18,12 +18,11 @@ import java.util.NoSuchElementException; import org.cloudfoundry.client.CloudFoundryClient; -import org.cloudfoundry.client.v2.stacks.ListStacksRequest; -import org.cloudfoundry.client.v2.stacks.StackResource; +import org.cloudfoundry.client.v3.stacks.ListStacksRequest; +import org.cloudfoundry.client.v3.stacks.StackResource; import org.cloudfoundry.operations.util.OperationsLogging; import org.cloudfoundry.util.ExceptionUtils; import org.cloudfoundry.util.PaginationUtils; -import org.cloudfoundry.util.ResourceUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -64,26 +63,26 @@ private static Mono getStack( private static Flux requestStack( CloudFoundryClient cloudFoundryClient, String stack) { - return PaginationUtils.requestClientV2Resources( + return PaginationUtils.requestClientV3Resources( page -> cloudFoundryClient - .stacks() + .stacksV3() .list(ListStacksRequest.builder().name(stack).page(page).build())); } private static Flux requestStacks(CloudFoundryClient cloudFoundryClient) { - return PaginationUtils.requestClientV2Resources( + return PaginationUtils.requestClientV3Resources( page -> cloudFoundryClient - .stacks() + .stacksV3() .list(ListStacksRequest.builder().page(page).build())); } private Stack toStack(StackResource stackResource) { return Stack.builder() - .description(ResourceUtils.getEntity(stackResource).getDescription()) - .id(ResourceUtils.getId(stackResource)) - .name(ResourceUtils.getEntity(stackResource).getName()) + .description(stackResource.getDescription()) + .id(stackResource.getId()) + .name(stackResource.getName()) .build(); } } diff --git a/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/AbstractOperationsTest.java b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/AbstractOperationsTest.java index d864ed497e..9dd97126e8 100644 --- a/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/AbstractOperationsTest.java +++ b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/AbstractOperationsTest.java @@ -44,13 +44,16 @@ import org.cloudfoundry.client.v2.userprovidedserviceinstances.UserProvidedServiceInstances; import org.cloudfoundry.client.v2.users.Users; import org.cloudfoundry.client.v3.applications.ApplicationsV3; +import org.cloudfoundry.client.v3.buildpacks.BuildpacksV3; import org.cloudfoundry.client.v3.domains.DomainsV3; import org.cloudfoundry.client.v3.jobs.JobsV3; import org.cloudfoundry.client.v3.organizations.OrganizationsV3; import org.cloudfoundry.client.v3.routes.RoutesV3; import org.cloudfoundry.client.v3.spaces.SpacesV3; +import org.cloudfoundry.client.v3.stacks.StacksV3; import org.cloudfoundry.client.v3.tasks.Tasks; import org.cloudfoundry.doppler.DopplerClient; +import org.cloudfoundry.logcache.v1.LogCacheClient; import org.cloudfoundry.routing.RoutingClient; import org.cloudfoundry.routing.v1.routergroups.RouterGroups; import org.cloudfoundry.uaa.UaaClient; @@ -92,6 +95,7 @@ public abstract class AbstractOperationsTest { protected final Authorizations authorizations = mock(Authorizations.class, RETURNS_SMART_NULLS); protected final Buildpacks buildpacks = mock(Buildpacks.class, RETURNS_SMART_NULLS); + protected final BuildpacksV3 buildpacksV3 = mock(BuildpacksV3.class, RETURNS_SMART_NULLS); protected final CloudFoundryClient cloudFoundryClient = mock(CloudFoundryClient.class, RETURNS_SMART_NULLS); @@ -101,6 +105,8 @@ public abstract class AbstractOperationsTest { protected final DopplerClient dopplerClient = mock(DopplerClient.class, RETURNS_SMART_NULLS); + protected final LogCacheClient logCacheClient = mock(LogCacheClient.class, RETURNS_SMART_NULLS); + protected final Events events = mock(Events.class, RETURNS_SMART_NULLS); protected final FeatureFlags featureFlags = mock(FeatureFlags.class, RETURNS_SMART_NULLS); @@ -152,6 +158,7 @@ public abstract class AbstractOperationsTest { protected final SpacesV3 spacesV3 = mock(SpacesV3.class, RETURNS_SMART_NULLS); protected final Stacks stacks = mock(Stacks.class, RETURNS_SMART_NULLS); + protected final StacksV3 stacksV3 = mock(StacksV3.class, RETURNS_SMART_NULLS); protected final Tasks tasks = mock(Tasks.class, RETURNS_SMART_NULLS); @@ -172,6 +179,7 @@ public final void mockClient() { when(this.cloudFoundryClient.applicationsV2()).thenReturn(this.applications); when(this.cloudFoundryClient.applicationsV3()).thenReturn(this.applicationsV3); when(this.cloudFoundryClient.buildpacks()).thenReturn(this.buildpacks); + when(this.cloudFoundryClient.buildpacksV3()).thenReturn(this.buildpacksV3); when(this.cloudFoundryClient.domains()).thenReturn(this.domains); when(this.cloudFoundryClient.domainsV3()).thenReturn(this.domainsV3); when(this.cloudFoundryClient.events()).thenReturn(this.events); @@ -200,6 +208,7 @@ public final void mockClient() { when(this.cloudFoundryClient.spaces()).thenReturn(this.spaces); when(this.cloudFoundryClient.spacesV3()).thenReturn(this.spacesV3); when(this.cloudFoundryClient.stacks()).thenReturn(this.stacks); + when(this.cloudFoundryClient.stacksV3()).thenReturn(this.stacksV3); when(this.cloudFoundryClient.tasks()).thenReturn(this.tasks); when(this.cloudFoundryClient.userProvidedServiceInstances()) .thenReturn(this.userProvidedServiceInstances); diff --git a/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/DefaultCloudFoundryOperationsTest.java b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/DefaultCloudFoundryOperationsTest.java index fd1027e7f0..02e671a3e6 100644 --- a/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/DefaultCloudFoundryOperationsTest.java +++ b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/DefaultCloudFoundryOperationsTest.java @@ -26,6 +26,7 @@ final class DefaultCloudFoundryOperationsTest extends AbstractOperationsTest { DefaultCloudFoundryOperations.builder() .cloudFoundryClient(this.cloudFoundryClient) .dopplerClient(this.dopplerClient) + .routingClient(this.routingClient) .organization(TEST_ORGANIZATION_NAME) .space(TEST_SPACE_NAME) .uaaClient(this.uaaClient) diff --git a/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/applications/DefaultApplicationsTest.java b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/applications/DefaultApplicationsTest.java index cdc9619d2d..055407feca 100644 --- a/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/applications/DefaultApplicationsTest.java +++ b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/applications/DefaultApplicationsTest.java @@ -139,11 +139,18 @@ import org.cloudfoundry.client.v3.tasks.CreateTaskResponse; import org.cloudfoundry.client.v3.tasks.TaskResource; import org.cloudfoundry.doppler.DopplerClient; -import org.cloudfoundry.doppler.Envelope; import org.cloudfoundry.doppler.EventType; import org.cloudfoundry.doppler.LogMessage; import org.cloudfoundry.doppler.RecentLogsRequest; import org.cloudfoundry.doppler.StreamRequest; +import org.cloudfoundry.logcache.v1.Envelope; +import org.cloudfoundry.logcache.v1.EnvelopeBatch; +import org.cloudfoundry.logcache.v1.Log; +import org.cloudfoundry.logcache.v1.LogCacheClient; +import org.cloudfoundry.logcache.v1.LogType; +import org.cloudfoundry.logcache.v1.ReadRequest; +import org.cloudfoundry.logcache.v1.ReadResponse; +import org.cloudfoundry.logcache.v1.TailLogsRequest; import org.cloudfoundry.operations.AbstractOperationsTest; import org.cloudfoundry.util.DateUtils; import org.cloudfoundry.util.FluentMap; @@ -163,6 +170,7 @@ final class DefaultApplicationsTest extends AbstractOperationsTest { new DefaultApplications( Mono.just(this.cloudFoundryClient), Mono.just(this.dopplerClient), + Mono.just(this.logCacheClient), this.randomWords, Mono.just(TEST_SPACE_ID)); @@ -1306,8 +1314,9 @@ void listTasks() { .verify(Duration.ofSeconds(5)); } + @SuppressWarnings("deprecation") @Test - void logs() { + void logsDoppler() { requestApplications( this.cloudFoundryClient, "test-application-name", @@ -1318,13 +1327,14 @@ void logs() { this.applications .logs(LogsRequest.builder().name("test-application-name").recent(false).build()) .as(StepVerifier::create) - .expectNext(fill(LogMessage.builder(), "log-message-").build()) + .expectNextMatches(log -> log.getMessage().equals("test-log-message-message")) .expectComplete() .verify(Duration.ofSeconds(5)); } + @SuppressWarnings("deprecation") @Test - void logsNoApp() { + void logsNoAppDoppler() { requestApplicationsEmpty(this.cloudFoundryClient, "test-application-name", TEST_SPACE_ID); this.applications @@ -1339,8 +1349,9 @@ void logsNoApp() { .verify(Duration.ofSeconds(5)); } + @SuppressWarnings("deprecation") @Test - void logsRecent() { + void logsRecentDoppler() { requestApplications( this.cloudFoundryClient, "test-application-name", @@ -1351,13 +1362,96 @@ void logsRecent() { this.applications .logs(LogsRequest.builder().name("test-application-name").recent(true).build()) .as(StepVerifier::create) - .expectNext(fill(LogMessage.builder(), "log-message-").build()) + .expectNextMatches(log -> log.getMessage().equals("test-log-message-message")) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @Test + void logsRecentLogCache() { + requestApplications( + this.cloudFoundryClient, + "test-application-name", + TEST_SPACE_ID, + "test-metadata-id"); + requestLogsRecentLogCache(this.logCacheClient, "test-metadata-id", "test-payload"); + + this.applications + .logsRecent(ReadRequest.builder().sourceId("test-metadata-id").build()) + .as(StepVerifier::create) + .expectNext(fill(Log.builder()).type(LogType.OUT).build()) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @Test + void logsTailLogCache() { + TailLogsRequest tailRequest = TailLogsRequest.builder().sourceId("test-source-id").build(); + requestLogsTailLogCache(this.logCacheClient, tailRequest, "test-tail-payload"); + + this.applications + .logsTail(tailRequest) + .take(1) + .as(StepVerifier::create) + .expectNextMatches( + envelope -> + envelope.getLog() != null + && LogType.OUT.equals(envelope.getLog().getType())) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @Test + void logsTailLogCacheMultipleEnvelopes() { + TailLogsRequest tailRequest = TailLogsRequest.builder().sourceId("test-source-id").build(); + requestLogsTailLogCacheMultiple(this.logCacheClient, tailRequest); + + this.applications + .logsTail(tailRequest) + .take(3) + .map(e -> e.getLog().getType()) + .as(StepVerifier::create) + .expectNext(LogType.OUT) + .expectNext(LogType.ERR) + .expectNext(LogType.OUT) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @Test + void logsTailLogCacheError() { + TailLogsRequest tailRequest = TailLogsRequest.builder().sourceId("test-source-id").build(); + when(this.logCacheClient.logsTail(tailRequest)) + .thenReturn(Flux.error(new RuntimeException("log-cache unavailable"))); + + this.applications + .logsTail(tailRequest) + .as(StepVerifier::create) + .expectErrorMatches( + t -> + t instanceof RuntimeException + && "log-cache unavailable".equals(t.getMessage())) + .verify(Duration.ofSeconds(5)); + } + + @Test + void logsTailLogCacheOutAndErrEnvelopes() { + TailLogsRequest tailRequest = TailLogsRequest.builder().sourceId("test-source-id").build(); + requestLogsTailLogCacheOutAndErr(this.logCacheClient, tailRequest); + + this.applications + .logsTail(tailRequest) + .take(2) + .as(StepVerifier::create) + .expectNextMatches(e -> LogType.OUT.equals(e.getLog().getType())) + .expectNextMatches(e -> LogType.ERR.equals(e.getLog().getType())) .expectComplete() .verify(Duration.ofSeconds(5)); } + @SuppressWarnings("deprecation") @Test - void logsRecentNotSet() { + void logsRecentNotSetDoppler() { requestApplications( this.cloudFoundryClient, "test-application-name", @@ -5317,12 +5411,13 @@ private static void requestListTasksEmpty( .build())); } + @SuppressWarnings("deprecation") private static void requestLogsRecent(DopplerClient dopplerClient, String applicationId) { when(dopplerClient.recentLogs( RecentLogsRequest.builder().applicationId(applicationId).build())) .thenReturn( Flux.just( - Envelope.builder() + org.cloudfoundry.doppler.Envelope.builder() .eventType(EventType.LOG_MESSAGE) .logMessage( fill(LogMessage.builder(), "log-message-").build()) @@ -5330,11 +5425,120 @@ private static void requestLogsRecent(DopplerClient dopplerClient, String applic .build())); } + private static void requestLogsRecentLogCache( + LogCacheClient logCacheClient, String sourceId, String payload) { + when(logCacheClient.recentLogs(ReadRequest.builder().sourceId(sourceId).build())) + .thenReturn( + Mono.just( + fill(ReadResponse.builder()) + .envelopes( + fill(EnvelopeBatch.builder()) + .batch( + Arrays.asList( + fill(Envelope.builder()) + .log( + Log + .builder() + .payload( + payload) + .type( + LogType + .OUT) + .build()) + .build())) + .build()) + .build())); + } + + private static void requestLogsTailLogCache( + LogCacheClient logCacheClient, TailLogsRequest tailRequest, String payload) { + when(logCacheClient.logsTail(tailRequest)) + .thenReturn( + Flux.just( + Envelope.builder() + .sourceId(tailRequest.getSourceId()) + .timestamp(System.nanoTime()) + .log( + Log.builder() + .payload(payload) + .type(LogType.OUT) + .build()) + .build())); + } + + /** + * Three envelopes with types OUT, ERR, OUT and strictly ascending timestamps so ordering + * is deterministic. + */ + private static void requestLogsTailLogCacheMultiple( + LogCacheClient logCacheClient, TailLogsRequest tailRequest) { + long base = System.nanoTime(); + when(logCacheClient.logsTail(tailRequest)) + .thenReturn( + Flux.just( + Envelope.builder() + .sourceId(tailRequest.getSourceId()) + .timestamp(base) + .log( + Log.builder() + .payload("msg1") + .type(LogType.OUT) + .build()) + .build(), + Envelope.builder() + .sourceId(tailRequest.getSourceId()) + .timestamp(base + 1) + .log( + Log.builder() + .payload("msg2") + .type(LogType.ERR) + .build()) + .build(), + Envelope.builder() + .sourceId(tailRequest.getSourceId()) + .timestamp(base + 2) + .log( + Log.builder() + .payload("msg3") + .type(LogType.OUT) + .build()) + .build())); + } + + /** + * Two envelopes – one STDOUT, one STDERR – to verify both log types are forwarded. + */ + private static void requestLogsTailLogCacheOutAndErr( + LogCacheClient logCacheClient, TailLogsRequest tailRequest) { + long base = System.nanoTime(); + when(logCacheClient.logsTail(tailRequest)) + .thenReturn( + Flux.just( + Envelope.builder() + .sourceId(tailRequest.getSourceId()) + .timestamp(base) + .log( + Log.builder() + .payload("stdout") + .type(LogType.OUT) + .build()) + .build(), + Envelope.builder() + .sourceId(tailRequest.getSourceId()) + .timestamp(base + 1) + .log( + Log.builder() + .payload("stderr") + .type(LogType.ERR) + .build()) + .build())); + } + private static void requestLogsStream(DopplerClient dopplerClient, String applicationId) { when(dopplerClient.stream(StreamRequest.builder().applicationId(applicationId).build())) .thenReturn( Flux.just( - Envelope.builder() + org.cloudfoundry.doppler.Envelope.builder() .eventType(EventType.LOG_MESSAGE) .logMessage( fill(LogMessage.builder(), "log-message-").build()) diff --git a/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/buildpacks/DefaultBuildpacksTest.java b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/buildpacks/DefaultBuildpacksTest.java index fdfca5272b..1d4f928a63 100644 --- a/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/buildpacks/DefaultBuildpacksTest.java +++ b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/buildpacks/DefaultBuildpacksTest.java @@ -17,6 +17,7 @@ package org.cloudfoundry.operations.buildpacks; import static org.cloudfoundry.operations.TestObjects.fill; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import java.nio.file.Path; @@ -27,18 +28,18 @@ import java.util.Queue; import java.util.function.Supplier; import org.cloudfoundry.client.CloudFoundryClient; -import org.cloudfoundry.client.v2.Metadata; -import org.cloudfoundry.client.v2.buildpacks.BuildpackEntity; -import org.cloudfoundry.client.v2.buildpacks.BuildpackResource; -import org.cloudfoundry.client.v2.buildpacks.CreateBuildpackResponse; -import org.cloudfoundry.client.v2.buildpacks.DeleteBuildpackResponse; -import org.cloudfoundry.client.v2.buildpacks.ListBuildpacksRequest; -import org.cloudfoundry.client.v2.buildpacks.ListBuildpacksResponse; -import org.cloudfoundry.client.v2.buildpacks.UpdateBuildpackResponse; -import org.cloudfoundry.client.v2.buildpacks.UploadBuildpackResponse; -import org.cloudfoundry.client.v2.jobs.GetJobRequest; -import org.cloudfoundry.client.v2.jobs.GetJobResponse; -import org.cloudfoundry.client.v2.jobs.JobEntity; +import org.cloudfoundry.client.v3.buildpacks.BuildpackResource; +import org.cloudfoundry.client.v3.buildpacks.BuildpackState; +import org.cloudfoundry.client.v3.buildpacks.CreateBuildpackResponse; +import org.cloudfoundry.client.v3.buildpacks.GetBuildpackRequest; +import org.cloudfoundry.client.v3.buildpacks.GetBuildpackResponse; +import org.cloudfoundry.client.v3.buildpacks.ListBuildpacksRequest; +import org.cloudfoundry.client.v3.buildpacks.ListBuildpacksResponse; +import org.cloudfoundry.client.v3.buildpacks.UpdateBuildpackResponse; +import org.cloudfoundry.client.v3.buildpacks.UploadBuildpackResponse; +import org.cloudfoundry.client.v3.jobs.GetJobRequest; +import org.cloudfoundry.client.v3.jobs.GetJobResponse; +import org.cloudfoundry.client.v3.jobs.JobState; import org.cloudfoundry.operations.AbstractOperationsTest; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; @@ -47,17 +48,14 @@ final class DefaultBuildpacksTest extends AbstractOperationsTest { - private final DefaultBuildpacks buildpacks = - new DefaultBuildpacks(Mono.just(this.cloudFoundryClient)); + private final DefaultBuildpacks buildpacks = new DefaultBuildpacks(this.cloudFoundryClient); @Test void create() { + requestBuildpack(this.cloudFoundryClient); requestCreateBuildpack(this.cloudFoundryClient, "test-buildpack", 1, true); requestUploadBuildpack( - this.cloudFoundryClient, - "test-buildpack-id", - Paths.get("test-buildpack"), - "test-buildpack"); + this.cloudFoundryClient, "test-buildpack-id", Paths.get("test-buildpack")); this.buildpacks .create( @@ -128,6 +126,7 @@ void rename() { @Test void update() { + requestBuildpack(this.cloudFoundryClient); requestListBuildpacks(this.cloudFoundryClient, "test-buildpack"); requestUpdateBuildpack(this.cloudFoundryClient, "test-buildpack-id", true, true, 5); @@ -146,13 +145,11 @@ void update() { @Test void updateWithBits() { + requestBuildpack(this.cloudFoundryClient); requestListBuildpacks(this.cloudFoundryClient, "test-buildpack"); requestUpdateBuildpack(this.cloudFoundryClient, "test-buildpack-id", true, true, 5); requestUploadBuildpack( - this.cloudFoundryClient, - "test-buildpack-id", - Paths.get("test-buildpack"), - "test-buildpack"); + this.cloudFoundryClient, "test-buildpack-id", Paths.get("test-buildpack")); this.buildpacks .update( @@ -168,13 +165,25 @@ void updateWithBits() { .verify(Duration.ofSeconds(5)); } + private static void requestBuildpack(CloudFoundryClient cloudFoundryClient) { + when(cloudFoundryClient.buildpacksV3().get(any(GetBuildpackRequest.class))) + .thenReturn( + Mono.just( + fill(GetBuildpackResponse.builder(), "buildpack-") + .state(BuildpackState.READY) + .build())); + } + private static void requestBuildpacks(CloudFoundryClient cloudFoundryClient) { - when(cloudFoundryClient.buildpacks().list(ListBuildpacksRequest.builder().page(1).build())) + when(cloudFoundryClient + .buildpacksV3() + .list(ListBuildpacksRequest.builder().page(1).build())) .thenReturn( Mono.just( fill(ListBuildpacksResponse.builder()) .resource( fill(BuildpackResource.builder(), "buildpack-") + .state(BuildpackState.READY) .build()) .build())); } @@ -182,9 +191,9 @@ private static void requestBuildpacks(CloudFoundryClient cloudFoundryClient) { private static void requestCreateBuildpack( CloudFoundryClient cloudFoundryClient, String name, Integer position, Boolean enable) { when(cloudFoundryClient - .buildpacks() + .buildpacksV3() .create( - org.cloudfoundry.client.v2.buildpacks.CreateBuildpackRequest + org.cloudfoundry.client.v3.buildpacks.CreateBuildpackRequest .builder() .name(name) .position(position) @@ -197,22 +206,17 @@ private static void requestCreateBuildpack( private static void requestDeleteBuildpack( CloudFoundryClient cloudFoundryClient, String buildpackId) { when(cloudFoundryClient - .buildpacks() + .buildpacksV3() .delete( - org.cloudfoundry.client.v2.buildpacks.DeleteBuildpackRequest + org.cloudfoundry.client.v3.buildpacks.DeleteBuildpackRequest .builder() - .async(true) .buildpackId(buildpackId) .build())) - .thenReturn( - Mono.just( - fill(DeleteBuildpackResponse.builder()) - .entity(fill(JobEntity.builder()).id("test-job-id").build()) - .build())); + .thenReturn(Mono.just("test-job-id")); } private static void requestJobSuccess(CloudFoundryClient cloudFoundryClient, String jobId) { - when(cloudFoundryClient.jobs().get(GetJobRequest.builder().jobId(jobId).build())) + when(cloudFoundryClient.jobsV3().get(GetJobRequest.builder().jobId(jobId).build())) .thenReturn( Mono.defer( new Supplier>() { @@ -224,20 +228,10 @@ private static void requestJobSuccess(CloudFoundryClient cloudFoundryClient, Str GetJobResponse .builder(), "test-job-") - .entity( - fill(JobEntity - .builder()) - .status( - "running") - .build()) + .state(JobState.PROCESSING) .build(), fill(GetJobResponse.builder(), "job-") - .entity( - fill(JobEntity - .builder()) - .status( - "finished") - .build()) + .state(JobState.COMPLETE) .build())); @Override @@ -249,21 +243,15 @@ public Mono get() { private static void requestListBuildpacks(CloudFoundryClient cloudFoundryClient, String name) { when(cloudFoundryClient - .buildpacks() + .buildpacksV3() .list(ListBuildpacksRequest.builder().name(name).page(1).build())) .thenReturn( Mono.just( - ListBuildpacksResponse.builder() + fill(ListBuildpacksResponse.builder()) .resource( - BuildpackResource.builder() - .metadata( - Metadata.builder() - .id("test-buildpack-id") - .build()) - .entity( - BuildpackEntity.builder() - .name(name) - .build()) + fill(BuildpackResource.builder()) + .id("test-buildpack-id") + .name(name) .build()) .build())); } @@ -271,9 +259,9 @@ private static void requestListBuildpacks(CloudFoundryClient cloudFoundryClient, private static void requestUpdateBuildpack( CloudFoundryClient cloudFoundryClient, String buildpackId, String name) { when(cloudFoundryClient - .buildpacks() + .buildpacksV3() .update( - org.cloudfoundry.client.v2.buildpacks.UpdateBuildpackRequest + org.cloudfoundry.client.v3.buildpacks.UpdateBuildpackRequest .builder() .buildpackId(buildpackId) .name(name) @@ -288,9 +276,9 @@ private static void requestUpdateBuildpack( boolean locked, Integer position) { when(cloudFoundryClient - .buildpacks() + .buildpacksV3() .update( - org.cloudfoundry.client.v2.buildpacks.UpdateBuildpackRequest + org.cloudfoundry.client.v3.buildpacks.UpdateBuildpackRequest .builder() .buildpackId(buildpackId) .enabled(enabled) @@ -301,18 +289,14 @@ private static void requestUpdateBuildpack( } private static void requestUploadBuildpack( - CloudFoundryClient cloudFoundryClient, - String buildpackId, - Path buildpack, - String filename) { + CloudFoundryClient cloudFoundryClient, String buildpackId, Path buildpack) { when(cloudFoundryClient - .buildpacks() + .buildpacksV3() .upload( - org.cloudfoundry.client.v2.buildpacks.UploadBuildpackRequest + org.cloudfoundry.client.v3.buildpacks.UploadBuildpackRequest .builder() .buildpackId(buildpackId) - .buildpack(buildpack) - .filename(filename) + .bits(buildpack) .build())) .thenReturn(Mono.just(fill(UploadBuildpackResponse.builder()).build())); } diff --git a/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/domains/CreateDomainRequestTest.java b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/domains/CreateDomainRequestTest.java index bb41e2a15d..d974026dda 100644 --- a/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/domains/CreateDomainRequestTest.java +++ b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/domains/CreateDomainRequestTest.java @@ -31,15 +31,6 @@ void noDomain() { }); } - @Test - void noOrganization() { - assertThrows( - IllegalStateException.class, - () -> { - CreateDomainRequest.builder().domain("test-domain").build(); - }); - } - @Test void valid() { CreateDomainRequest.builder() diff --git a/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/domains/DefaultDomainsTest.java b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/domains/DefaultDomainsTest.java index 15d97267ad..6fdae8380f 100644 --- a/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/domains/DefaultDomainsTest.java +++ b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/domains/DefaultDomainsTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2021 the original author or authors. + * Copyright 2013-2025 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. @@ -18,29 +18,27 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.cloudfoundry.operations.TestObjects.fill; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import java.time.Duration; import org.cloudfoundry.client.CloudFoundryClient; -import org.cloudfoundry.client.v2.Metadata; -import org.cloudfoundry.client.v2.organizations.AssociateOrganizationPrivateDomainRequest; -import org.cloudfoundry.client.v2.organizations.ListOrganizationsRequest; -import org.cloudfoundry.client.v2.organizations.ListOrganizationsResponse; -import org.cloudfoundry.client.v2.organizations.OrganizationResource; -import org.cloudfoundry.client.v2.organizations.RemoveOrganizationPrivateDomainRequest; -import org.cloudfoundry.client.v2.privatedomains.CreatePrivateDomainRequest; -import org.cloudfoundry.client.v2.privatedomains.CreatePrivateDomainResponse; -import org.cloudfoundry.client.v2.privatedomains.ListPrivateDomainsRequest; -import org.cloudfoundry.client.v2.privatedomains.ListPrivateDomainsResponse; -import org.cloudfoundry.client.v2.privatedomains.PrivateDomainResource; -import org.cloudfoundry.client.v2.shareddomains.CreateSharedDomainResponse; -import org.cloudfoundry.client.v2.shareddomains.ListSharedDomainsRequest; -import org.cloudfoundry.client.v2.shareddomains.ListSharedDomainsResponse; -import org.cloudfoundry.client.v2.shareddomains.SharedDomainEntity; -import org.cloudfoundry.client.v2.shareddomains.SharedDomainResource; +import org.cloudfoundry.client.v3.Pagination; +import org.cloudfoundry.client.v3.Relationship; +import org.cloudfoundry.client.v3.ToOneRelationship; +import org.cloudfoundry.client.v3.domains.CreateDomainResponse; +import org.cloudfoundry.client.v3.domains.DomainRelationships; +import org.cloudfoundry.client.v3.domains.DomainResource; +import org.cloudfoundry.client.v3.domains.ListDomainsRequest; +import org.cloudfoundry.client.v3.domains.ListDomainsResponse; +import org.cloudfoundry.client.v3.organizations.ListOrganizationsRequest; +import org.cloudfoundry.client.v3.organizations.ListOrganizationsResponse; +import org.cloudfoundry.client.v3.organizations.OrganizationResource; import org.cloudfoundry.operations.AbstractOperationsTest; import org.cloudfoundry.routing.RoutingClient; -import org.cloudfoundry.routing.v1.routergroups.ListRouterGroupsRequest; import org.cloudfoundry.routing.v1.routergroups.ListRouterGroupsResponse; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; @@ -49,12 +47,12 @@ final class DefaultDomainsTest extends AbstractOperationsTest { private final DefaultDomains domains = - new DefaultDomains(Mono.just(this.cloudFoundryClient), Mono.just(this.routingClient)); + new DefaultDomains(this.cloudFoundryClient, this.routingClient); @Test void createDomain() { requestOrganizations(this.cloudFoundryClient, "test-organization"); - requestCreatePrivateDomain(this.cloudFoundryClient, "test-domain", "test-organization-id"); + requestCreateDomain(this.cloudFoundryClient, "test-domain", "test-organization-id"); this.domains .create( @@ -63,56 +61,89 @@ void createDomain() { .organization("test-organization") .build()) .as(StepVerifier::create) - .expectComplete() - .verify(Duration.ofSeconds(5)); + .verifyComplete(); + + verify(this.cloudFoundryClient.domainsV3()) + .create( + argThat( + a -> + a.getName().equals("test-domain") + && a.getRelationships() + .getOrganization() + .getData() + .getId() + .equals("test-organization-id"))); + verifyNoInteractions(this.routingClient.routerGroups().list(any())); } @Test void createSharedDomain() { - requestCreateSharedDomain(this.cloudFoundryClient, "test-domain"); + requestCreateDomain(this.cloudFoundryClient, "test-domain"); this.domains .createShared(CreateSharedDomainRequest.builder().domain("test-domain").build()) .as(StepVerifier::create) - .expectComplete() - .verify(Duration.ofSeconds(5)); + .verifyComplete(); + + verify(this.cloudFoundryClient.domainsV3()) + .create(argThat(a -> a.getName().equals("test-domain"))); + verifyNoInteractions(this.cloudFoundryClient.organizationsV3().list(any())); + verifyNoInteractions(this.routingClient.routerGroups().list(any())); } @Test - void listDomains() { - requestPrivateDomains(this.cloudFoundryClient); - requestSharedDomains(this.cloudFoundryClient); + void createDomainRouterGroup() { + requestCreateDomain(this.cloudFoundryClient, "test-domain"); + requestListRouterGroups(this.routingClient, "test-router-group"); this.domains - .list() - .as(StepVerifier::create) - .expectNext( - Domain.builder() - .id("test-private-domain-id") - .name("test-private-domain-name") - .status(Status.OWNED) - .build(), - Domain.builder() - .id("test-shared-domain-id") - .name("test-shared-domain-name") - .status(Status.SHARED) + .create( + CreateDomainRequest.builder() + .domain("test-domain") + .routerGroup("test-router-group") .build()) + .as(StepVerifier::create) .expectComplete() .verify(Duration.ofSeconds(5)); + + verifyNoInteractions(this.cloudFoundryClient.organizationsV3().list(any())); + verify(this.routingClient.routerGroups()).list(any()); + verify(this.cloudFoundryClient.domainsV3()) + .create(argThat(a -> a.getRouterGroup().getId().equals("test-routerGroupId"))); } @Test - void listDomainsOnlyPrivate() { - requestPrivateDomains(this.cloudFoundryClient); - requestSharedDomainsEmpty(this.cloudFoundryClient); + void createSharedDomainRouterGroup() { + requestCreateDomain(this.cloudFoundryClient, "test-domain"); + requestListRouterGroups(this.routingClient, "test-router-group"); + + this.domains + .createShared( + CreateSharedDomainRequest.builder() + .domain("test-domain") + .routerGroup("test-router-group") + .build()) + .as(StepVerifier::create) + .verifyComplete(); + + verifyNoInteractions(this.cloudFoundryClient.organizationsV3().list(any())); + verify(this.routingClient.routerGroups()).list(any()); + verify(this.cloudFoundryClient.domainsV3()) + .create(argThat(a -> a.getRouterGroup().getId().equals("test-routerGroupId"))); + } + + @Test + void listDomains() { + requestListRouterGroups(this.routingClient, "test-router-group"); + requestListDomains(this.cloudFoundryClient, "test-organization-id", null); this.domains .list() .as(StepVerifier::create) .expectNext( Domain.builder() - .id("test-private-domain-id") - .name("test-private-domain-name") + .id("test-domain-id") + .name("test-domain-name") .status(Status.OWNED) .build()) .expectComplete() @@ -120,17 +151,17 @@ void listDomainsOnlyPrivate() { } @Test - void listDomainsOnlyShared() { - requestSharedDomains(this.cloudFoundryClient); - requestPrivateDomainsEmpty(this.cloudFoundryClient); + void listDomainsShared() { + requestListRouterGroups(this.routingClient, "test-router-group"); + requestListDomains(this.cloudFoundryClient, null, null); this.domains .list() .as(StepVerifier::create) .expectNext( Domain.builder() - .id("test-shared-domain-id") - .name("test-shared-domain-name") + .id("test-domain-id") + .name("test-domain-name") .status(Status.SHARED) .build()) .expectComplete() @@ -139,23 +170,18 @@ void listDomainsOnlyShared() { @Test void listDomainsTcp() { - requestPrivateDomains(this.cloudFoundryClient); - requestSharedDomainsTcp(this.cloudFoundryClient); + requestListDomains(this.cloudFoundryClient, null, "test-routerGroupId"); + requestListRouterGroups(this.routingClient, "test-tcp-group"); this.domains .list() .as(StepVerifier::create) .expectNext( Domain.builder() - .id("test-private-domain-id") - .name("test-private-domain-name") - .status(Status.OWNED) - .build(), - Domain.builder() - .id("test-shared-domain-id") - .name("test-shared-domain-name") + .id("test-domain-id") + .name("test-domain-name") .status(Status.SHARED) - .type("test-shared-domain-type") + .type("tcp") .build()) .expectComplete() .verify(Duration.ofSeconds(5)); @@ -163,7 +189,7 @@ void listDomainsTcp() { @Test void listRouterGroups() { - requestListRouterGroups(this.routingClient); + requestListRouterGroups(this.routingClient, "test-router-group"); this.domains .listRouterGroups() @@ -171,8 +197,8 @@ void listRouterGroups() { .expectNext( RouterGroup.builder() .id("test-routerGroupId") - .name("test-name") - .type("test-type") + .name("test-router-group") + .type("tcp") .build()) .expectComplete() .verify(Duration.ofSeconds(5)); @@ -180,15 +206,14 @@ void listRouterGroups() { @Test void shareDomain() { - requestListPrivateDomains(this.cloudFoundryClient, "test-domain", "test-domain-id"); requestOrganizations(this.cloudFoundryClient, "test-organization"); - requestAssociateOrganizationPrivateDomain( - this.cloudFoundryClient, "test-domain-id", "test-organization-id"); + requestListDomains(this.cloudFoundryClient, "test-organization-id", null); + requestShareDomain(this.cloudFoundryClient, "test-domain-id", "test-organization-id"); this.domains .share( ShareDomainRequest.builder() - .domain("test-domain") + .domain("test-domain-name") .organization("test-organization") .build()) .as(StepVerifier::create) @@ -197,14 +222,15 @@ void shareDomain() { } @Test - void shareDomainSharedDomain() { - requestListPrivateDomainsEmpty(this.cloudFoundryClient, "test-domain"); + void shareDomainDoesNotExist() { requestOrganizations(this.cloudFoundryClient, "test-organization"); + requestListDomains(this.cloudFoundryClient, "test-organization-id", null); + requestShareDomain(this.cloudFoundryClient, "test-domain-id", "test-organization-id"); this.domains .share( ShareDomainRequest.builder() - .domain("test-domain") + .domain("invalid-domain-name") .organization("test-organization") .build()) .as(StepVerifier::create) @@ -212,21 +238,22 @@ void shareDomainSharedDomain() { t -> assertThat(t) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Private domain test-domain does not exist")) + .hasMessage( + "Private domain invalid-domain-name does not" + + " exist")) .verify(Duration.ofSeconds(5)); } @Test void unshareDomain() { - requestListPrivateDomains(this.cloudFoundryClient, "test-domain", "test-domain-id"); + requestListDomains(this.cloudFoundryClient, "test-organization-id", null); requestOrganizations(this.cloudFoundryClient, "test-organization"); - requestRemoveOrganizationPrivateDomain( - this.cloudFoundryClient, "test-domain-id", "test-organization-id"); + requestUnshareDomain(this.cloudFoundryClient, "test-domain-id", "test-organization-id"); this.domains .unshare( UnshareDomainRequest.builder() - .domain("test-domain") + .domain("test-domain-name") .organization("test-organization") .build()) .as(StepVerifier::create) @@ -234,85 +261,110 @@ void unshareDomain() { .verify(Duration.ofSeconds(5)); } - private static void requestAssociateOrganizationPrivateDomain( + private static void requestShareDomain( CloudFoundryClient cloudFoundryClient, String domainId, String organizationId) { when(cloudFoundryClient - .organizations() - .associatePrivateDomain( - AssociateOrganizationPrivateDomainRequest.builder() - .privateDomainId(domainId) - .organizationId(organizationId) + .domainsV3() + .share( + org.cloudfoundry.client.v3.domains.ShareDomainRequest.builder() + .domainId(domainId) + .data(Relationship.builder().id(organizationId).build()) .build())) .thenReturn(Mono.empty()); } - private static void requestCreatePrivateDomain( - CloudFoundryClient cloudFoundryClient, String domain, String organizationId) { + private static void requestUnshareDomain( + CloudFoundryClient cloudFoundryClient, String domainId, String organizationId) { when(cloudFoundryClient - .privateDomains() - .create( - CreatePrivateDomainRequest.builder() - .name(domain) - .owningOrganizationId(organizationId) + .domainsV3() + .unshare( + org.cloudfoundry.client.v3.domains.UnshareDomainRequest.builder() + .domainId(domainId) + .organizationId(organizationId) .build())) + .thenReturn(Mono.empty()); + } + + private static void requestCreateDomain(CloudFoundryClient cloudFoundryClient, String domain) { + when(cloudFoundryClient.domainsV3().create(any())) .thenReturn( Mono.just( - fill(CreatePrivateDomainResponse.builder(), "private-domain-") + fill(CreateDomainResponse.builder(), "domain-") + .isInternal(false) .build())); } - private static void requestCreateSharedDomain( - CloudFoundryClient cloudFoundryClient, String domain) { + private static void requestCreateDomain( + CloudFoundryClient cloudFoundryClient, String domain, String organizationId) { when(cloudFoundryClient - .sharedDomains() + .domainsV3() .create( - org.cloudfoundry.client.v2.shareddomains.CreateSharedDomainRequest - .builder() + org.cloudfoundry.client.v3.domains.CreateDomainRequest.builder() .name(domain) + .relationships( + DomainRelationships.builder() + .organization( + ToOneRelationship.builder() + .data( + Relationship + .builder() + .id( + organizationId) + .build()) + .build()) + .build()) .build())) .thenReturn( Mono.just( - fill(CreateSharedDomainResponse.builder(), "shared-domain-") + fill(CreateDomainResponse.builder(), "domain-") + .isInternal(false) .build())); } - private static void requestListPrivateDomains( - CloudFoundryClient cloudFoundryClient, String domain, String domainId) { - when(cloudFoundryClient - .privateDomains() - .list(ListPrivateDomainsRequest.builder().name(domain).page(1).build())) + private static void requestListDomains( + CloudFoundryClient cloudFoundryClient, String organizationId, String routerGroupId) { + ToOneRelationship organizationRelationShip = + organizationId != null + ? ToOneRelationship.builder() + .data(Relationship.builder().id(organizationId).build()) + .build() + : ToOneRelationship.builder().build(); + org.cloudfoundry.client.v3.domains.RouterGroup routerGroup = + routerGroupId != null + ? org.cloudfoundry.client.v3.domains.RouterGroup.builder() + .id(routerGroupId) + .build() + : null; + + when(cloudFoundryClient.domainsV3().list(ListDomainsRequest.builder().page(1).build())) .thenReturn( Mono.just( - fill(ListPrivateDomainsResponse.builder()) + fill(ListDomainsResponse.builder()) .resource( - fill(PrivateDomainResource.builder()) - .metadata( - fill( - Metadata.builder(), - "private-domain-") - .id(domainId) + fill(DomainResource.builder(), "domain-") + .isInternal(false) + .relationships( + DomainRelationships.builder() + .organization( + organizationRelationShip) .build()) + .routerGroup(routerGroup) .build()) - .totalPages(1) + .pagination(Pagination.builder().totalPages(1).build()) .build())); } - private static void requestListPrivateDomainsEmpty( - CloudFoundryClient cloudFoundryClient, String domain) { - when(cloudFoundryClient - .privateDomains() - .list(ListPrivateDomainsRequest.builder().name(domain).page(1).build())) - .thenReturn(Mono.just(fill(ListPrivateDomainsResponse.builder()).build())); - } - - private static void requestListRouterGroups(RoutingClient routingClient) { - when(routingClient.routerGroups().list(ListRouterGroupsRequest.builder().build())) + private static void requestListRouterGroups( + RoutingClient routingClient, String routerGroupName) { + when(routingClient.routerGroups().list(any())) .thenReturn( Mono.just( ListRouterGroupsResponse.builder() .routerGroup( fill(org.cloudfoundry.routing.v1.routergroups .RouterGroup.builder()) + .name(routerGroupName) + .type("tcp") .build()) .build())); } @@ -320,7 +372,7 @@ private static void requestListRouterGroups(RoutingClient routingClient) { private static void requestOrganizations( CloudFoundryClient cloudFoundryClient, String organization) { when(cloudFoundryClient - .organizations() + .organizationsV3() .list( ListOrganizationsRequest.builder() .name(organization) @@ -336,90 +388,4 @@ private static void requestOrganizations( .build()) .build())); } - - private static void requestPrivateDomains(CloudFoundryClient cloudFoundryClient) { - when(cloudFoundryClient - .privateDomains() - .list(ListPrivateDomainsRequest.builder().page(1).build())) - .thenReturn( - Mono.just( - fill(ListPrivateDomainsResponse.builder()) - .resource( - fill( - PrivateDomainResource.builder(), - "private-domain-") - .build()) - .build())); - } - - private static void requestPrivateDomainsEmpty(CloudFoundryClient cloudFoundryClient) { - when(cloudFoundryClient - .privateDomains() - .list(ListPrivateDomainsRequest.builder().page(1).build())) - .thenReturn(Mono.just(fill(ListPrivateDomainsResponse.builder()).build())); - } - - private static void requestRemoveOrganizationPrivateDomain( - CloudFoundryClient cloudFoundryClient, String domainId, String organizationId) { - when(cloudFoundryClient - .organizations() - .removePrivateDomain( - RemoveOrganizationPrivateDomainRequest.builder() - .privateDomainId(domainId) - .organizationId(organizationId) - .build())) - .thenReturn(Mono.empty()); - } - - private static void requestSharedDomains(CloudFoundryClient cloudFoundryClient) { - when(cloudFoundryClient - .sharedDomains() - .list(ListSharedDomainsRequest.builder().page(1).build())) - .thenReturn( - Mono.just( - fill(ListSharedDomainsResponse.builder()) - .resource( - fill( - SharedDomainResource.builder(), - "shared-domain-") - .entity( - fill( - SharedDomainEntity - .builder(), - "shared-domain-") - .routerGroupType(null) - .build()) - .build()) - .build())); - } - - private static void requestSharedDomainsEmpty(CloudFoundryClient cloudFoundryClient) { - when(cloudFoundryClient - .sharedDomains() - .list(ListSharedDomainsRequest.builder().page(1).build())) - .thenReturn(Mono.just(fill(ListSharedDomainsResponse.builder()).build())); - } - - private static void requestSharedDomainsTcp(CloudFoundryClient cloudFoundryClient) { - when(cloudFoundryClient - .sharedDomains() - .list(ListSharedDomainsRequest.builder().page(1).build())) - .thenReturn( - Mono.just( - fill(ListSharedDomainsResponse.builder()) - .resource( - fill( - SharedDomainResource.builder(), - "shared-domain-") - .entity( - fill( - SharedDomainEntity - .builder(), - "shared-domain-") - .routerGroupType( - "test-shared-domain-type") - .build()) - .build()) - .build())); - } } diff --git a/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/stacks/DefaultStacksTest.java b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/stacks/DefaultStacksTest.java index d64f055eef..1fb1635042 100644 --- a/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/stacks/DefaultStacksTest.java +++ b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/stacks/DefaultStacksTest.java @@ -21,9 +21,9 @@ import java.time.Duration; import org.cloudfoundry.client.CloudFoundryClient; -import org.cloudfoundry.client.v2.stacks.ListStacksRequest; -import org.cloudfoundry.client.v2.stacks.ListStacksResponse; -import org.cloudfoundry.client.v2.stacks.StackResource; +import org.cloudfoundry.client.v3.stacks.ListStacksRequest; +import org.cloudfoundry.client.v3.stacks.ListStacksResponse; +import org.cloudfoundry.client.v3.stacks.StackResource; import org.cloudfoundry.operations.AbstractOperationsTest; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; @@ -58,7 +58,7 @@ void listStacks() { } private static void requestStacks(CloudFoundryClient cloudFoundryClient) { - when(cloudFoundryClient.stacks().list(ListStacksRequest.builder().page(1).build())) + when(cloudFoundryClient.stacksV3().list(ListStacksRequest.builder().page(1).build())) .thenReturn( Mono.just( fill(ListStacksResponse.builder()) @@ -68,7 +68,7 @@ private static void requestStacks(CloudFoundryClient cloudFoundryClient) { private static void requestStacks(CloudFoundryClient cloudFoundryClient, String name) { when(cloudFoundryClient - .stacks() + .stacksV3() .list(ListStacksRequest.builder().name(name).page(1).build())) .thenReturn( Mono.just( diff --git a/cloudfoundry-util/pom.xml b/cloudfoundry-util/pom.xml index 749f52e746..0e5f8ae76d 100644 --- a/cloudfoundry-util/pom.xml +++ b/cloudfoundry-util/pom.xml @@ -25,7 +25,7 @@ org.cloudfoundry cloudfoundry-java-client - 5.15.0.BUILD-SNAPSHOT + 6.0.0-SNAPSHOT cloudfoundry-util diff --git a/integration-test/pom.xml b/integration-test/pom.xml index ca0b9009f1..fbad53c625 100644 --- a/integration-test/pom.xml +++ b/integration-test/pom.xml @@ -25,7 +25,7 @@ org.cloudfoundry cloudfoundry-java-client - 5.15.0.BUILD-SNAPSHOT + 6.0.0-SNAPSHOT integration-test @@ -70,6 +70,16 @@ ${project.version} test + + io.github.resilience4j + resilience4j-ratelimiter + ${resilience4j.version} + + + io.github.resilience4j + resilience4j-reactor + ${resilience4j.version} + org.cloudfoundry cloudfoundry-util diff --git a/integration-test/src/test/java/org/cloudfoundry/CloudFoundryCleaner.java b/integration-test/src/test/java/org/cloudfoundry/CloudFoundryCleaner.java index d515a86b09..582e7cfbe5 100644 --- a/integration-test/src/test/java/org/cloudfoundry/CloudFoundryCleaner.java +++ b/integration-test/src/test/java/org/cloudfoundry/CloudFoundryCleaner.java @@ -73,7 +73,6 @@ import org.cloudfoundry.client.v2.userprovidedserviceinstances.ListUserProvidedServiceInstancesRequest; import org.cloudfoundry.client.v2.userprovidedserviceinstances.RemoveUserProvidedServiceInstanceRouteRequest; import org.cloudfoundry.client.v2.userprovidedserviceinstances.UserProvidedServiceInstanceResource; -import org.cloudfoundry.client.v2.users.UserResource; import org.cloudfoundry.client.v3.Metadata; import org.cloudfoundry.client.v3.Relationship; import org.cloudfoundry.client.v3.applications.Application; @@ -86,6 +85,7 @@ import org.cloudfoundry.client.v3.spaces.GetSpaceResponse; import org.cloudfoundry.client.v3.spaces.UpdateSpaceRequest; import org.cloudfoundry.client.v3.spaces.UpdateSpaceResponse; +import org.cloudfoundry.client.v3.users.UserResource; import org.cloudfoundry.networking.NetworkingClient; import org.cloudfoundry.networking.v1.policies.DeletePoliciesRequest; import org.cloudfoundry.networking.v1.policies.Destination; @@ -213,7 +213,7 @@ void clean() { cleanSpaceQuotaDefinitions( this.cloudFoundryClient, this.nameFactory), cleanStacks(this.cloudFoundryClient, this.nameFactory), - cleanUsers(this.cloudFoundryClient, this.nameFactory))) + cleanUsersV3(this.cloudFoundryClient, this.nameFactory))) .thenMany( Mono.when( cleanApplicationsV3( @@ -1043,35 +1043,29 @@ private static Flux cleanUserProvidedServiceInstances( t))); } - private static Flux cleanUsers( + private static Flux cleanUsersV3( CloudFoundryClient cloudFoundryClient, NameFactory nameFactory) { - return PaginationUtils.requestClientV2Resources( + return PaginationUtils.requestClientV3Resources( page -> cloudFoundryClient - .users() + .usersV3() .list( - org.cloudfoundry.client.v2.users.ListUsersRequest + org.cloudfoundry.client.v3.users.ListUsersRequest .builder() .page(page) .build())) .filter(resource -> isCleanable(nameFactory, resource)) - .map(resource -> resource.getMetadata().getId()) + .map(UserResource::getId) .flatMap( userId -> cloudFoundryClient - .users() + .usersV3() .delete( - org.cloudfoundry.client.v2.users.DeleteUserRequest + org.cloudfoundry.client.v3.users.DeleteUserRequest .builder() - .async(true) .userId(userId) .build()) - .flatMapMany( - job -> - JobUtils.waitForCompletion( - cloudFoundryClient, - Duration.ofMinutes(5), - job)) + .then() .doOnError( t -> LOGGER.error( @@ -1150,12 +1144,14 @@ private static Flux ifCfVersion( CloudFoundryVersion expectedVersion, Version serverVersion, Supplier> supplier) { - return serverVersion.lessThan(expectedVersion.getVersion()) ? Flux.empty() : supplier.get(); + return serverVersion.isLowerThan(expectedVersion.getVersion()) + ? Flux.empty() + : supplier.get(); } private static boolean isCleanable(NameFactory nameFactory, UserResource resource) { - return nameFactory.isUserId(ResourceUtils.getId(resource)) - || nameFactory.isUserName(ResourceUtils.getEntity(resource).getUsername()); + return nameFactory.isUserId(resource.getId()) + || nameFactory.isUserName(resource.getUsername()); } private static Flux removeApplicationServiceBindings( diff --git a/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java b/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java index c0b3f44b30..58b01252f0 100644 --- a/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java +++ b/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java @@ -48,6 +48,7 @@ import org.cloudfoundry.client.v2.stacks.StackResource; import org.cloudfoundry.client.v2.userprovidedserviceinstances.CreateUserProvidedServiceInstanceRequest; import org.cloudfoundry.doppler.DopplerClient; +import org.cloudfoundry.logcache.v1.LogCacheClient; import org.cloudfoundry.logcache.v1.TestLogCacheEndpoints; import org.cloudfoundry.networking.NetworkingClient; import org.cloudfoundry.operations.DefaultCloudFoundryOperations; @@ -192,18 +193,25 @@ NetworkingClient adminNetworkingClient( @Bean @Qualifier("admin") - ReactorUaaClient adminUaaClient( + UaaClient adminUaaClient( ConnectionContext connectionContext, @Value("${test.admin.clientId}") String clientId, - @Value("${test.admin.clientSecret}") String clientSecret) { - return ReactorUaaClient.builder() - .connectionContext(connectionContext) - .tokenProvider( - ClientCredentialsGrantTokenProvider.builder() - .clientId(clientId) - .clientSecret(clientSecret) - .build()) - .build(); + @Value("${test.admin.clientSecret}") String clientSecret, + @Value("${uaa.api.request.limit:#{null}}") Integer environmentRequestLimit) { + ReactorUaaClient unthrottledClient = + ReactorUaaClient.builder() + .connectionContext(connectionContext) + .tokenProvider( + ClientCredentialsGrantTokenProvider.builder() + .clientId(clientId) + .clientSecret(clientSecret) + .build()) + .build(); + if (environmentRequestLimit == null) { + return unthrottledClient; + } else { + return new ThrottlingUaaClient(unthrottledClient, environmentRequestLimit); + } } @Bean(initMethod = "block") @@ -266,6 +274,7 @@ ReactorCloudFoundryClient cloudFoundryClient( DefaultCloudFoundryOperations cloudFoundryOperations( CloudFoundryClient cloudFoundryClient, DopplerClient dopplerClient, + LogCacheClient logCacheClient, NetworkingClient networkingClient, RoutingClient routingClient, UaaClient uaaClient, @@ -274,6 +283,7 @@ DefaultCloudFoundryOperations cloudFoundryOperations( return DefaultCloudFoundryOperations.builder() .cloudFoundryClient(cloudFoundryClient) .dopplerClient(dopplerClient) + .logCacheClient(logCacheClient) .networkingClient(networkingClient) .routingClient(routingClient) .uaaClient(uaaClient) @@ -643,11 +653,20 @@ PasswordGrantTokenProvider tokenProvider( } @Bean - ReactorUaaClient uaaClient(ConnectionContext connectionContext, TokenProvider tokenProvider) { - return ReactorUaaClient.builder() - .connectionContext(connectionContext) - .tokenProvider(tokenProvider) - .build(); + UaaClient uaaClient( + ConnectionContext connectionContext, + TokenProvider tokenProvider, + @Value("${uaa.api.request.limit:#{null}}") Integer environmentRequestLimit) { + ReactorUaaClient unthrottledClient = + ReactorUaaClient.builder() + .connectionContext(connectionContext) + .tokenProvider(tokenProvider) + .build(); + if (environmentRequestLimit == null) { + return unthrottledClient; + } else { + return new ThrottlingUaaClient(unthrottledClient, environmentRequestLimit); + } } @Bean(initMethod = "block") @@ -730,7 +749,7 @@ String username(NameFactory nameFactory) { return nameFactory.getUserName(); } - private static final class FailingDeserializationProblemHandler + public static final class FailingDeserializationProblemHandler extends DeserializationProblemHandler { @Override diff --git a/integration-test/src/test/java/org/cloudfoundry/ThrottlingUaaClient.java b/integration-test/src/test/java/org/cloudfoundry/ThrottlingUaaClient.java new file mode 100644 index 0000000000..a4a9d27a61 --- /dev/null +++ b/integration-test/src/test/java/org/cloudfoundry/ThrottlingUaaClient.java @@ -0,0 +1,348 @@ +package org.cloudfoundry; + +import io.github.resilience4j.ratelimiter.RateLimiter; +import io.github.resilience4j.ratelimiter.RateLimiterConfig; +import io.github.resilience4j.reactor.ratelimiter.operator.RateLimiterOperator; +import java.time.Duration; +import org.cloudfoundry.reactor.uaa.ReactorUaaClient; +import org.cloudfoundry.uaa.UaaClient; +import org.cloudfoundry.uaa.authorizations.Authorizations; +import org.cloudfoundry.uaa.clients.Clients; +import org.cloudfoundry.uaa.groups.AddMemberRequest; +import org.cloudfoundry.uaa.groups.AddMemberResponse; +import org.cloudfoundry.uaa.groups.CheckMembershipRequest; +import org.cloudfoundry.uaa.groups.CheckMembershipResponse; +import org.cloudfoundry.uaa.groups.CreateGroupRequest; +import org.cloudfoundry.uaa.groups.CreateGroupResponse; +import org.cloudfoundry.uaa.groups.DeleteGroupRequest; +import org.cloudfoundry.uaa.groups.DeleteGroupResponse; +import org.cloudfoundry.uaa.groups.GetGroupRequest; +import org.cloudfoundry.uaa.groups.GetGroupResponse; +import org.cloudfoundry.uaa.groups.Groups; +import org.cloudfoundry.uaa.groups.ListExternalGroupMappingsRequest; +import org.cloudfoundry.uaa.groups.ListExternalGroupMappingsResponse; +import org.cloudfoundry.uaa.groups.ListGroupsRequest; +import org.cloudfoundry.uaa.groups.ListGroupsResponse; +import org.cloudfoundry.uaa.groups.ListMembersRequest; +import org.cloudfoundry.uaa.groups.ListMembersResponse; +import org.cloudfoundry.uaa.groups.MapExternalGroupRequest; +import org.cloudfoundry.uaa.groups.MapExternalGroupResponse; +import org.cloudfoundry.uaa.groups.RemoveMemberRequest; +import org.cloudfoundry.uaa.groups.RemoveMemberResponse; +import org.cloudfoundry.uaa.groups.UnmapExternalGroupByGroupDisplayNameRequest; +import org.cloudfoundry.uaa.groups.UnmapExternalGroupByGroupDisplayNameResponse; +import org.cloudfoundry.uaa.groups.UnmapExternalGroupByGroupIdRequest; +import org.cloudfoundry.uaa.groups.UnmapExternalGroupByGroupIdResponse; +import org.cloudfoundry.uaa.groups.UpdateGroupRequest; +import org.cloudfoundry.uaa.groups.UpdateGroupResponse; +import org.cloudfoundry.uaa.identityproviders.IdentityProviders; +import org.cloudfoundry.uaa.identityzones.IdentityZones; +import org.cloudfoundry.uaa.ratelimit.Ratelimit; +import org.cloudfoundry.uaa.serverinformation.ServerInformation; +import org.cloudfoundry.uaa.tokens.Tokens; +import org.cloudfoundry.uaa.users.ChangeUserPasswordRequest; +import org.cloudfoundry.uaa.users.ChangeUserPasswordResponse; +import org.cloudfoundry.uaa.users.CreateUserRequest; +import org.cloudfoundry.uaa.users.CreateUserResponse; +import org.cloudfoundry.uaa.users.DeleteUserRequest; +import org.cloudfoundry.uaa.users.DeleteUserResponse; +import org.cloudfoundry.uaa.users.ExpirePasswordRequest; +import org.cloudfoundry.uaa.users.ExpirePasswordResponse; +import org.cloudfoundry.uaa.users.GetUserVerificationLinkRequest; +import org.cloudfoundry.uaa.users.GetUserVerificationLinkResponse; +import org.cloudfoundry.uaa.users.InviteUsersRequest; +import org.cloudfoundry.uaa.users.InviteUsersResponse; +import org.cloudfoundry.uaa.users.ListUsersRequest; +import org.cloudfoundry.uaa.users.ListUsersResponse; +import org.cloudfoundry.uaa.users.LookupUserIdsRequest; +import org.cloudfoundry.uaa.users.LookupUserIdsResponse; +import org.cloudfoundry.uaa.users.UpdateUserRequest; +import org.cloudfoundry.uaa.users.UpdateUserResponse; +import org.cloudfoundry.uaa.users.UserInfoRequest; +import org.cloudfoundry.uaa.users.UserInfoResponse; +import org.cloudfoundry.uaa.users.Users; +import org.cloudfoundry.uaa.users.VerifyUserRequest; +import org.cloudfoundry.uaa.users.VerifyUserResponse; +import org.immutables.value.Value; +import reactor.core.publisher.Mono; + +public class ThrottlingUaaClient implements UaaClient { + + private final UaaClient delegate; + private final int maxRequestsPerSecond; + private final RateLimiter rateLimiter; + private final ThrottledUsers users; + private Groups groups; + + /** + * An {@link UaaClient} implementation that throttles calls to the UAA + * {@code /Groups} and {@code /Users} endpoints. It uses a single "bucket" + * for throttling requests to both endpoints. + * + * @see resilience4j docs + */ + public ThrottlingUaaClient(ReactorUaaClient delegate, int maxRequestsPerSecond) { + // uaaLimit is calls per second. We need the milliseconds for one call because + // resilience4j uses sliced timeslots, while the uaa server uses a sliding window. + int clockSkewMillis = 20; // 20ms clock skew is a save value for ~5 requests per second. + int rateLimitRefreshPeriodMillis = (1000 / maxRequestsPerSecond) + clockSkewMillis; + this.delegate = delegate; + this.maxRequestsPerSecond = maxRequestsPerSecond; + RateLimiterConfig config = + RateLimiterConfig.custom() + .limitForPeriod(1) + .limitRefreshPeriod(Duration.ofMillis(rateLimitRefreshPeriodMillis)) + .timeoutDuration(Duration.ofSeconds(10)) + .build(); + this.rateLimiter = RateLimiter.of("uaa", config); + + this.users = new ThrottledUsers(); + this.groups = new ThrottledGroups(); + } + + @Override + public Authorizations authorizations() { + return this.delegate.authorizations(); + } + + @Override + public Clients clients() { + return this.delegate.clients(); + } + + @Override + public Mono getUsername() { + return this.delegate.getUsername(); + } + + @Override + public IdentityProviders identityProviders() { + return this.delegate.identityProviders(); + } + + @Override + public IdentityZones identityZones() { + return this.delegate.identityZones(); + } + + @Override + public ServerInformation serverInformation() { + return this.delegate.serverInformation(); + } + + @Override + public Tokens tokens() { + return this.delegate.tokens(); + } + + @Override + public Users users() { + return users; + } + + @Override + public Groups groups() { + return groups; + } + + @Override + @Value.Derived + public Ratelimit rateLimit() { + return this.delegate.rateLimit(); + } + + public int getMaxRequestsPerSecond() { + return maxRequestsPerSecond; + } + + public class ThrottledUsers implements Users { + + private final Users usersDelegate; + + public ThrottledUsers() { + this.usersDelegate = delegate.users(); + } + + @Override + public Mono changePassword(ChangeUserPasswordRequest request) { + return this.usersDelegate + .changePassword(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono create(CreateUserRequest request) { + return this.usersDelegate + .create(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono delete(DeleteUserRequest request) { + return this.usersDelegate + .delete(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono expirePassword(ExpirePasswordRequest request) { + return this.usersDelegate + .expirePassword(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono getVerificationLink( + GetUserVerificationLinkRequest request) { + return this.usersDelegate + .getVerificationLink(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono invite(InviteUsersRequest request) { + return this.usersDelegate + .invite(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono list(ListUsersRequest request) { + return this.usersDelegate + .list(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono lookup(LookupUserIdsRequest request) { + return this.usersDelegate + .lookup(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono update(UpdateUserRequest request) { + return this.usersDelegate + .update(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono userInfo(UserInfoRequest request) { + return this.usersDelegate + .userInfo(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono verify(VerifyUserRequest request) { + return this.usersDelegate + .verify(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + } + + public class ThrottledGroups implements Groups { + + public final Groups groupsDelegate; + + public ThrottledGroups() { + this.groupsDelegate = delegate.groups(); + } + + @Override + public Mono addMember(AddMemberRequest request) { + return this.groupsDelegate + .addMember(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono checkMembership(CheckMembershipRequest request) { + return this.groupsDelegate + .checkMembership(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono create(CreateGroupRequest request) { + return this.groupsDelegate + .create(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono delete(DeleteGroupRequest request) { + return this.groupsDelegate + .delete(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono get(GetGroupRequest request) { + return this.groupsDelegate + .get(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono list(ListGroupsRequest request) { + return this.groupsDelegate + .list(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono listExternalGroupMappings( + ListExternalGroupMappingsRequest request) { + return this.groupsDelegate + .listExternalGroupMappings(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono listMembers(ListMembersRequest request) { + return this.groupsDelegate + .listMembers(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono mapExternalGroup(MapExternalGroupRequest request) { + return this.groupsDelegate + .mapExternalGroup(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono removeMember(RemoveMemberRequest request) { + return this.groupsDelegate + .removeMember(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono + unmapExternalGroupByGroupDisplayName( + UnmapExternalGroupByGroupDisplayNameRequest request) { + return this.groupsDelegate + .unmapExternalGroupByGroupDisplayName(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono unmapExternalGroupByGroupId( + UnmapExternalGroupByGroupIdRequest request) { + return this.groupsDelegate + .unmapExternalGroupByGroupId(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + + @Override + public Mono update(UpdateGroupRequest request) { + return this.groupsDelegate + .update(request) + .transformDeferred(RateLimiterOperator.of(rateLimiter)); + } + } +} diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v3/OrganizationQuotasTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v3/OrganizationQuotasTest.java index 15b0239c96..e719fcc1b2 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v3/OrganizationQuotasTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v3/OrganizationQuotasTest.java @@ -19,11 +19,16 @@ import static org.assertj.core.api.Assertions.assertThat; import java.time.Duration; +import java.util.List; import org.cloudfoundry.AbstractIntegrationTest; import org.cloudfoundry.CloudFoundryVersion; import org.cloudfoundry.IfCloudFoundryVersion; import org.cloudfoundry.client.CloudFoundryClient; -import org.cloudfoundry.client.v3.quotas.*; +import org.cloudfoundry.client.v3.organizations.CreateOrganizationRequest; +import org.cloudfoundry.client.v3.organizations.Organization; +import org.cloudfoundry.client.v3.quotas.Apps; +import org.cloudfoundry.client.v3.quotas.Routes; +import org.cloudfoundry.client.v3.quotas.Services; import org.cloudfoundry.client.v3.quotas.organizations.*; import org.cloudfoundry.util.JobUtils; import org.cloudfoundry.util.PaginationUtils; @@ -197,6 +202,43 @@ public void update() { .verify(Duration.ofMinutes(5)); } + @Test + public void apply() { + String orgName = this.nameFactory.getOrganizationName(); + String organizationId = createOrganization(this.cloudFoundryClient, orgName).getId(); + Relationship organizationRelationship1 = Relationship.builder().id(organizationId).build(); + ToManyRelationship organizationRelationships = + ToManyRelationship.builder().data(organizationRelationship1).build(); + + String organizationQuotaName = this.nameFactory.getQuotaDefinitionName(); + + createOrganizationQuotaId(this.cloudFoundryClient, organizationQuotaName) + .flatMap( + organizationQuotaId -> { + ApplyOrganizationQuotaRequest applyOrganizationQuotaRequest = + ApplyOrganizationQuotaRequest.builder() + .organizationQuotaId(organizationQuotaId) + .organizationRelationships(organizationRelationships) + .build(); + return this.cloudFoundryClient + .organizationQuotasV3() + .apply(applyOrganizationQuotaRequest); + }) + .as(StepVerifier::create) + .consumeNextWith( + applyOrganizationQuotaResponse -> { + List organizationRelationshipsData = + applyOrganizationQuotaResponse + .organizationRelationships() + .getData(); + assertThat(organizationRelationshipsData.size()).isEqualTo(1); + assertThat(organizationRelationshipsData.get(0).getId()) + .isEqualTo(organizationId); + }) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + private static Mono createOrganizationQuotaId( CloudFoundryClient cloudFoundryClient, String organizationQuotaName) { return createOrganizationQuota(cloudFoundryClient, organizationQuotaName) @@ -225,4 +267,12 @@ private static Flux requestListOrganizationQuotas( .page(page) .build())); } + + private static Organization createOrganization( + CloudFoundryClient cloudFoundryClient, String orgName) { + return cloudFoundryClient + .organizationsV3() + .create(CreateOrganizationRequest.builder().name(orgName).build()) + .block(Duration.ofMinutes(5)); + } } diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v3/SpaceQuotasTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v3/SpaceQuotasTest.java index ede877a34c..a36fb49570 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v3/SpaceQuotasTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v3/SpaceQuotasTest.java @@ -19,13 +19,18 @@ import static org.assertj.core.api.Assertions.assertThat; import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; import org.cloudfoundry.AbstractIntegrationTest; import org.cloudfoundry.CloudFoundryVersion; import org.cloudfoundry.IfCloudFoundryVersion; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v3.organizations.CreateOrganizationRequest; import org.cloudfoundry.client.v3.organizations.Organization; -import org.cloudfoundry.client.v3.quotas.*; +import org.cloudfoundry.client.v3.quotas.Apps; +import org.cloudfoundry.client.v3.quotas.Routes; +import org.cloudfoundry.client.v3.quotas.Services; import org.cloudfoundry.client.v3.quotas.spaces.*; import org.cloudfoundry.client.v3.spaces.CreateSpaceRequest; import org.cloudfoundry.client.v3.spaces.Space; @@ -105,7 +110,7 @@ public void create() { public void createWithSpaceRelationship() { String spaceQuotaName = this.nameFactory.getQuotaDefinitionName(); SpaceQuotaRelationships spaceQuotaRelationships = - createSpaceQuotaRelationships(organizationId, spaceId); + createSpaceQuotaRelationships(organizationId, Collections.singletonList(spaceId)); Apps spaceQuotaAppLimits = Apps.builder() @@ -277,6 +282,72 @@ public void delete() { .verify(Duration.ofMinutes(5)); } + @Test + public void apply() { + String spaceQuotaName = this.nameFactory.getQuotaDefinitionName(); + + Relationship spaceRelationship1 = Relationship.builder().id(spaceId).build(); + ToManyRelationship spaceRelationships = + ToManyRelationship.builder().data(spaceRelationship1).build(); + + createSpaceQuotaId(this.cloudFoundryClient, spaceQuotaName, organizationId) + .flatMap( + spaceQuotaId -> { + ApplySpaceQuotaRequest applySpaceQuotaRequest = + ApplySpaceQuotaRequest.builder() + .spaceQuotaId(spaceQuotaId) + .spaceRelationships(spaceRelationships) + .build(); + return this.cloudFoundryClient + .spaceQuotasV3() + .apply(applySpaceQuotaRequest); + }) + .as(StepVerifier::create) + .consumeNextWith( + applySpaceQuotaResponse -> { + List spaceRelationshipsData = + applySpaceQuotaResponse.spaceRelationships().getData(); + assertThat(spaceRelationshipsData.size()).isEqualTo(1); + assertThat(spaceRelationshipsData.get(0).getId()).isEqualTo(spaceId); + }) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + + @Test + public void remove() { + String spaceQuotaName = this.nameFactory.getQuotaDefinitionName(); + + // create a space quota in org with "organizationId" that is pre-associated to the space + // with "spaceId" + createSpaceQuotaId( + this.cloudFoundryClient, + spaceQuotaName, + organizationId, + Collections.singletonList(spaceId)) + .flatMap( + spaceQuotaId -> { + RemoveSpaceQuotaRequest removeSpaceQuotaRequest = + RemoveSpaceQuotaRequest.builder() + .spaceQuotaId(spaceQuotaId) + .spaceId(spaceId) + .build(); + return this.cloudFoundryClient + .spaceQuotasV3() + .remove(removeSpaceQuotaRequest); + }) + .thenMany(requestListSpaceQuotas(this.cloudFoundryClient, spaceQuotaName)) + .as(StepVerifier::create) + .consumeNextWith( + spaceQuotaResource -> { + List spaceRelationshipsData = + spaceQuotaResource.getRelationships().getSpaces().getData(); + assertThat(spaceRelationshipsData.size()).isEqualTo(0); + }) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + private static Organization createOrganization( CloudFoundryClient cloudFoundryClient, String orgName) { return cloudFoundryClient @@ -314,15 +385,20 @@ private static SpaceQuotaRelationships createSpaceQuotaRelationships(String orgG @NotNull private static SpaceQuotaRelationships createSpaceQuotaRelationships( - String orgGuid, String spaceGuid) { + String orgGuid, List spaceGuids) { ToOneRelationship organizationRelationship = ToOneRelationship.builder() .data(Relationship.builder().id(orgGuid).build()) .build(); + + // iterate over spaceGuids to create Relationship objects + List spaceRelationshipsData = + spaceGuids.stream() + .map(spaceGuid -> Relationship.builder().id(spaceGuid).build()) + .collect(Collectors.toList()); + ToManyRelationship spaceRelationships = - ToManyRelationship.builder() - .data(Relationship.builder().id(spaceGuid).build()) - .build(); + ToManyRelationship.builder().data(spaceRelationshipsData).build(); return SpaceQuotaRelationships.builder() .organization(organizationRelationship) .spaces(spaceRelationships) @@ -347,6 +423,31 @@ private static Mono createSpaceQuota( .build()); } + private static Mono createSpaceQuotaId( + CloudFoundryClient cloudFoundryClient, + String spaceQuotaName, + String orgGuid, + List spaceGuids) { + return createSpaceQuota(cloudFoundryClient, spaceQuotaName, orgGuid, spaceGuids) + .map(CreateSpaceQuotaResponse::getId); + } + + private static Mono createSpaceQuota( + CloudFoundryClient cloudFoundryClient, + String spaceQuotaName, + String orgGuid, + List spaceGuids) { + SpaceQuotaRelationships spaceQuotaRelationships = + createSpaceQuotaRelationships(orgGuid, spaceGuids); + return cloudFoundryClient + .spaceQuotasV3() + .create( + CreateSpaceQuotaRequest.builder() + .name(spaceQuotaName) + .relationships(spaceQuotaRelationships) + .build()); + } + private static Flux requestListSpaceQuotas( CloudFoundryClient cloudFoundryClient, String spaceName) { return PaginationUtils.requestClientV3Resources( diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v3/UsersTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v3/UsersTest.java new file mode 100644 index 0000000000..e3dbab91cb --- /dev/null +++ b/integration-test/src/test/java/org/cloudfoundry/client/v3/UsersTest.java @@ -0,0 +1,155 @@ +/* + * Copyright 2013-2025 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.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.CloudFoundryVersion; +import org.cloudfoundry.IfCloudFoundryVersion; +import org.cloudfoundry.client.CloudFoundryClient; +import org.cloudfoundry.client.v3.users.*; +import org.cloudfoundry.util.PaginationUtils; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +@IfCloudFoundryVersion(greaterThanOrEqualTo = CloudFoundryVersion.PCF_4_v3) +public final class UsersTest extends AbstractIntegrationTest { + + @Autowired private CloudFoundryClient cloudFoundryClient; + + @Test + public void create() { + String userId = this.nameFactory.getUserId(); + + this.cloudFoundryClient + .usersV3() + .create(CreateUserRequest.builder().userId(userId).build()) + .single() + .as(StepVerifier::create) + .expectNextCount(1) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + + @Test + public void get() { + String userId = this.nameFactory.getUserId(); + + createUser(this.cloudFoundryClient, userId) + .flatMap( + createUserResponse -> + this.cloudFoundryClient + .usersV3() + .get(GetUserRequest.builder().userId(userId).build())) + .map(GetUserResponse::getId) + .as(StepVerifier::create) + .expectNext(userId) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + + @Test + public void list() { + String userId = this.nameFactory.getUserId(); + createUser(this.cloudFoundryClient, userId) + .thenMany( + PaginationUtils.requestClientV3Resources( + page -> + this.cloudFoundryClient + .usersV3() + .list( + ListUsersRequest.builder() + .page(page) + .build()))) + .filter(resource -> userId.equals(resource.getId())) + .map(UserResource::getId) + .as(StepVerifier::create) + .expectNext(userId) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + + @Test + public void update() { + String userId = this.nameFactory.getUserId(); + + createUser(this.cloudFoundryClient, userId) + .flatMap( + createUserResponse -> + this.cloudFoundryClient + .usersV3() + .update( + UpdateUserRequest.builder() + .userId(userId) + .metadata( + Metadata.builder() + .annotation( + "annotationKey", + "annotationValue") + .label( + "labelKey", + "labelValue") + .build()) + .build())) + .then(getUser(cloudFoundryClient, userId)) + .as(StepVerifier::create) + .consumeNextWith( + GetUserResponse -> { + Metadata metadata = GetUserResponse.getMetadata(); + assertThat(metadata.getAnnotations().get("annotationKey")) + .isEqualTo("annotationValue"); + assertThat(metadata.getLabels().get("labelKey")) + .isEqualTo("labelValue"); + }) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + + @Test + public void delete() { + String userId = this.nameFactory.getUserId(); + + createUser(this.cloudFoundryClient, userId) + .flatMap( + createUserResponse -> + this.cloudFoundryClient + .usersV3() + .delete( + DeleteUserRequest.builder() + .userId(createUserResponse.getId()) + .build())) + .as(StepVerifier::create) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + + private static Mono createUser( + CloudFoundryClient cloudFoundryClient, String userId) { + return cloudFoundryClient + .usersV3() + .create(CreateUserRequest.builder().userId(userId).build()); + } + + private static Mono getUser( + CloudFoundryClient cloudFoundryClient, String userId) { + return cloudFoundryClient.usersV3().get(GetUserRequest.builder().userId(userId).build()); + } +} 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 36e1bd9456..bc6d6826d8 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java @@ -25,6 +25,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.logging.Level; import org.cloudfoundry.AbstractIntegrationTest; import org.cloudfoundry.CleanupCloudFoundryAfterClass; import org.cloudfoundry.CloudFoundryVersion; @@ -38,6 +39,7 @@ import org.cloudfoundry.logcache.v1.LogType; import org.cloudfoundry.logcache.v1.ReadRequest; import org.cloudfoundry.logcache.v1.ReadResponse; +import org.cloudfoundry.logcache.v1.TailLogsRequest; import org.cloudfoundry.operations.applications.ApplicationDetail; import org.cloudfoundry.operations.applications.ApplicationEnvironments; import org.cloudfoundry.operations.applications.ApplicationEvent; @@ -86,6 +88,7 @@ import org.cloudfoundry.operations.services.CreateUserProvidedServiceInstanceRequest; import org.cloudfoundry.operations.services.GetServiceInstanceRequest; import org.cloudfoundry.operations.services.ServiceInstance; +import org.cloudfoundry.operations.util.OperationsLogging; import org.cloudfoundry.util.FluentMap; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -93,6 +96,7 @@ import org.springframework.core.io.ClassPathResource; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.publisher.SignalType; import reactor.test.StepVerifier; @CleanupCloudFoundryAfterClass @@ -511,6 +515,7 @@ public void listTasks() throws IOException { * Doppler was dropped in PCF 4.x in favor of logcache. This test does not work * on TAS 4.x. */ + @Deprecated @Test @IfCloudFoundryVersion(lessThan = CloudFoundryVersion.PCF_4_v2) public void logs() throws IOException { @@ -537,6 +542,32 @@ public void logs() throws IOException { .verify(Duration.ofMinutes(5)); } + @Test + public void logsRecent() throws IOException { + String applicationName = this.nameFactory.getApplicationName(); + Mono applicationGuid = + getAppGuidFromAppName(cloudFoundryOperations, applicationName); + createApplication( + this.cloudFoundryOperations, + new ClassPathResource("test-application.zip").getFile().toPath(), + applicationName, + false) + .then( + applicationGuid + .map(ApplicationsTest::getReadRequest) + .flatMapMany( + readRequest -> + callLogsRecent( + this.cloudFoundryOperations, + readRequest) + .log(null, Level.ALL, SignalType.ON_NEXT)) + .map(ApplicationsTest::checkOneLogEntry) + .then()) + .as(StepVerifier::create) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + /** * Exercise the LogCache client. Serves as a reference for using the logcache client, * and will help with the transition to the new @@ -576,6 +607,94 @@ public void logCacheLogs() throws IOException { .verify(Duration.ofMinutes(5)); } + /** + * Integration test for {@link org.cloudfoundry.operations.applications.Applications#logsTail}. + * Verifies that streaming a single LOG envelope from a running application succeeds. + */ + @Test + public void logsTail() throws IOException { + String applicationName = this.nameFactory.getApplicationName(); + + createApplication( + this.cloudFoundryOperations, + new ClassPathResource("test-application.zip").getFile().toPath(), + applicationName, + false) + .then( + this.cloudFoundryOperations + .applications() + .get( + GetApplicationRequest.builder() + .name(applicationName) + .build())) + .map(ApplicationDetail::getId) + .flatMapMany( + appGuid -> + this.cloudFoundryOperations + .applications() + .logsTail( + TailLogsRequest.builder() + .sourceId(appGuid) + .envelopeTypes( + Collections.singletonList( + EnvelopeType.LOG)) + .build()) + .take(1)) + .map(Envelope::getLog) + .map(Log::getType) + .as(StepVerifier::create) + .expectNextMatches( + logType -> LogType.OUT.equals(logType) || LogType.ERR.equals(logType)) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + + /** + * Integration test for {@link org.cloudfoundry.operations.applications.Applications#logsTail} + * verifying that multiple LOG envelopes can be streamed from a running application. + */ + @Test + public void logsTailMultipleEnvelopes() throws IOException { + String applicationName = this.nameFactory.getApplicationName(); + + createApplication( + this.cloudFoundryOperations, + new ClassPathResource("test-application.zip").getFile().toPath(), + applicationName, + false) + .then( + this.cloudFoundryOperations + .applications() + .get( + GetApplicationRequest.builder() + .name(applicationName) + .build())) + .map(ApplicationDetail::getId) + .flatMapMany( + appGuid -> + this.cloudFoundryOperations + .applications() + .logsTail( + TailLogsRequest.builder() + .sourceId(appGuid) + .envelopeTypes( + Collections.singletonList( + EnvelopeType.LOG)) + .build()) + .take(3)) + .map(Envelope::getLog) + .map(Log::getType) + .as(StepVerifier::create) + .expectNextMatches( + logType -> LogType.OUT.equals(logType) || LogType.ERR.equals(logType)) + .expectNextMatches( + logType -> LogType.OUT.equals(logType) || LogType.ERR.equals(logType)) + .expectNextMatches( + logType -> LogType.OUT.equals(logType) || LogType.ERR.equals(logType)) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + @Test public void pushBindServices() throws IOException { String applicationName = this.nameFactory.getApplicationName(); @@ -2156,4 +2275,27 @@ private static Mono requestSshEnabled( .applications() .sshEnabled(ApplicationSshEnabledRequest.builder().name(applicationName).build()); } + + private static ReadRequest getReadRequest(String applicationId) { + return ReadRequest.builder().sourceId(applicationId).build(); + } + + private static Flux callLogsRecent( + CloudFoundryOperations cloudFoundryOperations, ReadRequest readRequest) { + return cloudFoundryOperations.applications().logsRecent(readRequest); + } + + private static Mono getAppGuidFromAppName( + CloudFoundryOperations cloudFoundryOperations, String applicationName) { + return cloudFoundryOperations + .applications() + .get(GetApplicationRequest.builder().name(applicationName).build()) + .map(ApplicationDetail::getId); + } + + private static Log checkOneLogEntry(Log log) { + OperationsLogging.log("one log entry: " + log.getType() + " " + log.getPayloadAsText()); + assertThat(log.getType()).isIn(LogType.OUT, LogType.ERR); + return log; + } } 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 08b1dfcc24..62991cef2e 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/BuildpacksTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/BuildpacksTest.java @@ -75,7 +75,7 @@ public void createFromDirectory() throws IOException { .filter(buildpack -> buildpackName.equals(buildpack.getName())) .map(Buildpack::getFilename) .as(StepVerifier::create) - .expectNext("test-buildpack.zip") + .expectNextMatches(filename -> filename.matches(".*test-buildpack.*\\.zip")) .expectComplete() .verify(Duration.ofMinutes(5)); } 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 f56d7c777b..b75ca3f3a4 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/DomainsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/DomainsTest.java @@ -17,13 +17,15 @@ package org.cloudfoundry.operations; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.type; import static org.cloudfoundry.operations.domains.Status.OWNED; import static org.cloudfoundry.operations.domains.Status.SHARED; import java.time.Duration; import org.cloudfoundry.AbstractIntegrationTest; import org.cloudfoundry.client.CloudFoundryClient; -import org.cloudfoundry.client.v2.ClientV2Exception; +import org.cloudfoundry.client.v3.ClientV3Exception; +import org.cloudfoundry.client.v3.Error; import org.cloudfoundry.client.v3.domains.GetDomainRequest; import org.cloudfoundry.operations.domains.CreateDomainRequest; import org.cloudfoundry.operations.domains.CreateSharedDomainRequest; @@ -60,10 +62,20 @@ public void createInvalidDomain() { .consumeErrorWith( t -> assertThat(t) - .isInstanceOf(ClientV2Exception.class) - .hasMessageMatching( - "CF-DomainInvalid\\([0-9]+\\): The domain is" - + " invalid.*")) + .asInstanceOf(type(ClientV3Exception.class)) + .extracting(ClientV3Exception::getErrors) + .asList() + .hasSize(1) + .first() + .isEqualTo( + Error.builder() + .title("CF-UnprocessableEntity") + .code(10008) + .detail( + "Name does not comply with RFC 1035" + + " standards, Name must" + + " contain at least one \".\"") + .build())) .verify(Duration.ofMinutes(5)); } diff --git a/integration-test/src/test/java/org/cloudfoundry/operations/StacksTest.java b/integration-test/src/test/java/org/cloudfoundry/operations/StacksTest.java new file mode 100644 index 0000000000..7db823dd95 --- /dev/null +++ b/integration-test/src/test/java/org/cloudfoundry/operations/StacksTest.java @@ -0,0 +1,44 @@ +package org.cloudfoundry.operations; + +import java.time.Duration; +import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.operations.stacks.GetStackRequest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +class StacksTest extends AbstractIntegrationTest { + @Autowired private CloudFoundryOperations cloudFoundryOperations; + + @Autowired private Mono stackName; + + @Test + public void create() { + this.stackName + .flatMap( + name -> + this.cloudFoundryOperations + .stacks() + .get(GetStackRequest.builder().name(name).build())) + .as(StepVerifier::create) + .expectNextMatches( + s -> s.getDescription().contains("Cloud Foundry Linux-based filesystem")) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + + @Test + public void list() { + String stackName = this.stackName.block(); + this.cloudFoundryOperations + .stacks() + .list() + .filter(s -> s.getName().equals(stackName)) + .as(StepVerifier::create) + .expectNextMatches( + s -> s.getDescription().startsWith("Cloud Foundry Linux-based filesystem")) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } +} diff --git a/integration-test/src/test/java/org/cloudfoundry/uaa/RatelimitTestConfiguration.java b/integration-test/src/test/java/org/cloudfoundry/uaa/RatelimitTestConfiguration.java new file mode 100644 index 0000000000..e162955e4d --- /dev/null +++ b/integration-test/src/test/java/org/cloudfoundry/uaa/RatelimitTestConfiguration.java @@ -0,0 +1,73 @@ +package org.cloudfoundry.uaa; + +import java.time.Duration; +import org.cloudfoundry.IntegrationTestConfiguration.FailingDeserializationProblemHandler; +import org.cloudfoundry.ThrottlingUaaClient; +import org.cloudfoundry.reactor.ConnectionContext; +import org.cloudfoundry.reactor.DefaultConnectionContext; +import org.cloudfoundry.reactor.ProxyConfiguration; +import org.cloudfoundry.reactor.tokenprovider.ClientCredentialsGrantTokenProvider; +import org.cloudfoundry.reactor.uaa.ReactorUaaClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +@Configuration +@EnableAutoConfiguration +public class RatelimitTestConfiguration { + + @Bean + UaaClient adminUaaClient( + ConnectionContext connectionContext, + @Value("${test.admin.clientId}") String clientId, + @Value("${test.admin.clientSecret}") String clientSecret, + @Value("${uaa.api.request.limit:0}") int uaaLimit) { + ReactorUaaClient client = + ReactorUaaClient.builder() + .connectionContext(connectionContext) + .tokenProvider( + ClientCredentialsGrantTokenProvider.builder() + .clientId(clientId) + .clientSecret(clientSecret) + .build()) + .build(); + if (uaaLimit > 0) { + return new ThrottlingUaaClient(client, uaaLimit); + } + return client; + } + + @Bean + DefaultConnectionContext connectionContext( + @Value("${test.apiHost}") String apiHost, + @Value("${test.proxy.host:}") String proxyHost, + @Value("${test.proxy.password:}") String proxyPassword, + @Value("${test.proxy.port:8080}") Integer proxyPort, + @Value("${test.proxy.username:}") String proxyUsername, + @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 (StringUtils.hasText(proxyHost)) { + ProxyConfiguration.Builder proxyConfiguration = + ProxyConfiguration.builder().host(proxyHost).port(proxyPort); + + if (StringUtils.hasText(proxyUsername)) { + proxyConfiguration.password(proxyPassword).username(proxyUsername); + } + + connectionContext.proxyConfiguration(proxyConfiguration.build()); + } + + return connectionContext.build(); + } +} diff --git a/integration-test/src/test/java/org/cloudfoundry/uaa/UaaRatelimitTest.java b/integration-test/src/test/java/org/cloudfoundry/uaa/UaaRatelimitTest.java new file mode 100644 index 0000000000..116b20338d --- /dev/null +++ b/integration-test/src/test/java/org/cloudfoundry/uaa/UaaRatelimitTest.java @@ -0,0 +1,72 @@ +package org.cloudfoundry.uaa; + +import java.time.Duration; +import org.cloudfoundry.ThrottlingUaaClient; +import org.cloudfoundry.uaa.ratelimit.Current; +import org.cloudfoundry.uaa.ratelimit.RatelimitRequest; +import org.cloudfoundry.uaa.ratelimit.RatelimitResponse; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +@SpringJUnitConfig(classes = RatelimitTestConfiguration.class) +public class UaaRatelimitTest { + private static final Logger LOGGER = LoggerFactory.getLogger(UaaRatelimitTest.class); + + @Autowired private UaaClient adminUaaClient; + + @Test + public void getRatelimit() { + int envRatelimit; + if (adminUaaClient instanceof ThrottlingUaaClient) { + ThrottlingUaaClient throttlingClient = (ThrottlingUaaClient) adminUaaClient; + envRatelimit = throttlingClient.getMaxRequestsPerSecond(); + } else { + envRatelimit = 0; + } + Mono tmp = + adminUaaClient + .rateLimit() + .getRatelimit(RatelimitRequest.builder().build()) + .map(response -> getServerRatelimit(response, envRatelimit)) + .timeout(Duration.ofSeconds(5)) + .onErrorResume( + ex -> { + LOGGER.error( + "Warning: could not fetch UAA rate limit, using default" + + " " + + 0 + + ". Cause: " + + ex); + return Mono.just(false); + }); + StepVerifier.create(tmp.materialize()).expectNextCount(1).verifyComplete(); + } + + private Boolean getServerRatelimit(RatelimitResponse response, int maxRequestsPerSecond) { + Current curr = response.getCurrentData(); + if (!"ACTIVE".equals(curr.getStatus())) { + LOGGER.debug( + "UaaRatelimitInitializer server ratelimit is not 'ACTIVE', but " + + curr.getStatus() + + ". Ignoring server value for ratelimit."); + return false; + } + Integer result = curr.getLimiterMappings(); + LOGGER.info( + "Server uses uaa rate limiting. There are " + + result + + " mappings declared in file " + + response.getFromSource()); + if (maxRequestsPerSecond == 0) { + LOGGER.warn( + "Ratelimiting is not set locally, set variable 'UAA_API_REQUEST_LIMIT' to a" + + " save value or you might experience http 429 responses."); + } + return true; + } +} diff --git a/integration-test/src/test/resources/logback-test.xml b/integration-test/src/test/resources/logback-test.xml index 4d9869fc6e..3c774460d3 100644 --- a/integration-test/src/test/resources/logback-test.xml +++ b/integration-test/src/test/resources/logback-test.xml @@ -36,6 +36,7 @@ + diff --git a/pom.xml b/pom.xml index e8bd2cb83a..df2a45ed6e 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ cloudfoundry-java-client Cloud Foundry Java Client Parent A Java language binding for interacting with a Cloud Foundry instance - 5.15.0.BUILD-SNAPSHOT + 6.0.0-SNAPSHOT pom https://github.com/cloudfoundry/cf-java-client @@ -74,6 +74,7 @@ 3.0.2 2.44.4 + 1.7.0 diff --git a/test-log-cache/pom.xml b/test-log-cache/pom.xml index c872e9a072..65cc77ed82 100644 --- a/test-log-cache/pom.xml +++ b/test-log-cache/pom.xml @@ -25,7 +25,7 @@ org.cloudfoundry cloudfoundry-java-client - 5.15.0.BUILD-SNAPSHOT + 6.0.0-SNAPSHOT test-log-cache diff --git a/test-service-broker/pom.xml b/test-service-broker/pom.xml index 7eaf5c5940..2326a92d2c 100644 --- a/test-service-broker/pom.xml +++ b/test-service-broker/pom.xml @@ -25,7 +25,7 @@ org.cloudfoundry cloudfoundry-java-client - 5.15.0.BUILD-SNAPSHOT + 6.0.0-SNAPSHOT test-service-broker