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