Skip to content

Commit 3c0ffcc

Browse files
committed
fix: Adapt the client and the integration tests to the KinD based CAPI
Make the integration tests pass against a CAPI deployed on KinD. Client: - Send the "resources" field with every bits upload (v3 packages) and also as query parameter for v2 uploads without matched resources, as CAPI otherwise rejects them (CF-AppBitsUploadInvalid / missing :resources). - Add the CA certificates of the LDAP and SAML identity provider configurations. Integration tests: - Reserve the TCP ports handed out by the NameFactory on the default-tcp router group at startup, so the TCP route tests don't depend on the test order or the deployment's default range. Skipped together with the TCP routing tests (SKIP_TCP_ROUTING_TESTS).
1 parent 9579cd0 commit 3c0ffcc

8 files changed

Lines changed: 167 additions & 4 deletions

File tree

‎cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/applications/ReactorApplicationsV2.java‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.io.IOException;
2222
import java.nio.file.Files;
2323
import java.nio.file.Path;
24+
import java.util.Collections;
2425
import java.util.Map;
2526
import org.cloudfoundry.client.v2.applications.ApplicationEnvironmentRequest;
2627
import org.cloudfoundry.client.v2.applications.ApplicationEnvironmentResponse;
@@ -66,6 +67,7 @@
6667
import org.cloudfoundry.reactor.client.v2.AbstractClientV2Operations;
6768
import org.cloudfoundry.reactor.util.MultipartHttpClientRequest;
6869
import org.cloudfoundry.util.FileUtils;
70+
import org.springframework.web.util.UriComponentsBuilder;
6971
import reactor.core.Exceptions;
7072
import reactor.core.publisher.Flux;
7173
import reactor.core.publisher.Mono;
@@ -356,13 +358,25 @@ private Mono<UploadApplicationResponse> upload(
356358
return put(
357359
request,
358360
UploadApplicationResponse.class,
359-
builder -> builder.pathSegment("apps", request.getApplicationId(), "bits"),
361+
builder -> uploadUri(builder, request),
360362
multipartRequest ->
361363
upload(request.getApplication(), multipartRequest, request),
362364
onTerminate)
363365
.checkpoint();
364366
}
365367

368+
// The nginx upload module of CAPI can drop the "resources" form field ("missing :resources").
369+
// Cloud Controller also takes it as query parameter, but only an empty list fits into a URL.
370+
private static UriComponentsBuilder uploadUri(
371+
UriComponentsBuilder builder, UploadApplicationRequest request) {
372+
builder.pathSegment("apps", request.getApplicationId(), "bits");
373+
if (request.getResources().isEmpty()) {
374+
builder.queryParam("resources", "{resources}")
375+
.uriVariables(Collections.singletonMap("resources", "[]"));
376+
}
377+
return builder;
378+
}
379+
366380
private void upload(
367381
Path application,
368382
MultipartHttpClientRequest multipartRequest,

‎cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/packages/ReactorPackages.java‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@
1616

1717
package org.cloudfoundry.reactor.client.v3.packages;
1818

19+
import static io.netty.handler.codec.http.HttpHeaderValues.APPLICATION_JSON;
20+
1921
import java.io.IOException;
2022
import java.nio.file.Files;
2123
import java.nio.file.Path;
24+
import java.util.Collections;
2225
import java.util.List;
2326
import java.util.Map;
2427
import org.cloudfoundry.client.v3.packages.CopyPackageRequest;
@@ -169,9 +172,12 @@ private void upload(Path bits, List<MatchedResource> resources, MultipartHttpCli
169172
r.addPart(part -> part.setName("bits").setContentType(APPLICATION_ZIP).sendFile(bits));
170173
}
171174

172-
if (resources != null && !resources.isEmpty()) {
173-
r.addPart(part -> part.setName("resources").send(resources));
174-
}
175+
// CAPI rejects bits uploads without a "resources" part (CF-AppBitsUploadInvalid)
176+
r.addPart(
177+
part ->
178+
part.setName("resources")
179+
.setContentType(APPLICATION_JSON.toString())
180+
.send(resources == null ? Collections.emptyList() : resources));
175181

176182
r.done();
177183
}

‎cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/applications/ReactorApplicationsV2Test.java‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1362,6 +1362,16 @@ void upload() throws IOException {
13621362
.verify(Duration.ofSeconds(5));
13631363
}
13641364

1365+
@Test
1366+
void uploadWithoutResourcesSendsResourcesQueryParameter() throws IOException {
1367+
uploadWithoutResources("/apps/test-application-id/bits?resources=%5B%5D", null);
1368+
}
1369+
1370+
@Test
1371+
void uploadAsyncWithoutResourcesSendsResourcesQueryParameter() throws IOException {
1372+
uploadWithoutResources("/apps/test-application-id/bits?async=true&resources=%5B%5D", true);
1373+
}
1374+
13651375
@Test
13661376
void uploadDroplet() throws IOException {
13671377
mockRequest(
@@ -1438,4 +1448,49 @@ void uploadDroplet() throws IOException {
14381448
.expectComplete()
14391449
.verify(Duration.ofSeconds(5));
14401450
}
1451+
1452+
private void uploadWithoutResources(String path, Boolean async) throws IOException {
1453+
mockRequest(
1454+
InteractionContext.builder()
1455+
.request(
1456+
TestRequest.builder()
1457+
.method(PUT)
1458+
.path(path)
1459+
.contents(
1460+
consumer(
1461+
(headers, body) ->
1462+
assertThat(
1463+
body.readString(
1464+
Charset
1465+
.defaultCharset()))
1466+
.contains(
1467+
"name=\"application\"")
1468+
.contains(
1469+
"name=\"resources\"")
1470+
.contains("[]")))
1471+
.build())
1472+
.response(
1473+
TestResponse.builder()
1474+
.status(CREATED)
1475+
.payload(
1476+
"fixtures/client/v2/apps/PUT_{id}_bits_response.json")
1477+
.build())
1478+
.build());
1479+
1480+
this.applications
1481+
.upload(
1482+
UploadApplicationRequest.builder()
1483+
.application(
1484+
new ClassPathResource(
1485+
"fixtures/client/v2/apps/test-application.zip")
1486+
.getFile()
1487+
.toPath())
1488+
.applicationId("test-application-id")
1489+
.async(async)
1490+
.build())
1491+
.as(StepVerifier::create)
1492+
.expectNextCount(1)
1493+
.expectComplete()
1494+
.verify(Duration.ofSeconds(5));
1495+
}
14411496
}

‎cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/packages/ReactorPackagesTest.java‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,20 @@ void upload() throws IOException {
764764
+ "test-content\r\n"
765765
+ "--"
766766
+ boundary
767+
+ "\r\n"
768+
+ "content-disposition:"
769+
+ " form-data;"
770+
+ " name=\"resources\"\r\n"
771+
+ "content-length:"
772+
+ " 2\r\n"
773+
+ "content-type:"
774+
+ " application/json\r\n"
775+
+ "content-transfer-encoding:"
776+
+ " binary\r\n"
777+
+ "\r\n"
778+
+ "[]\r\n"
779+
+ "--"
780+
+ boundary
767781
+ "--\r\n");
768782
}))
769783
.build())

‎cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/identityproviders/_LdapConfiguration.java‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import org.cloudfoundry.Nullable;
2222
import org.immutables.value.Value;
2323

24+
import java.util.List;
25+
2426
/**
2527
* The payload for the ldap identity provider configuration
2628
*/
@@ -63,6 +65,13 @@ abstract class _LdapConfiguration extends AbstractExternalIdentityProviderConfig
6365
@Nullable
6466
abstract String getBindUserDistinguishedName();
6567

68+
/**
69+
* The PEM encoded CA certificates used to validate the connection
70+
*/
71+
@JsonProperty("caCertificates")
72+
@Nullable
73+
abstract List<String> getCaCertificates();
74+
6675
/**
6776
* The group role attribute
6877
*/

‎cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/identityproviders/_SamlConfiguration.java‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ abstract class _SamlConfiguration extends AbstractExternalIdentityProviderConfig
5151
@Nullable
5252
abstract List<String> getAuthnContext();
5353

54+
/**
55+
* The PEM encoded CA certificates used to validate the connection
56+
*/
57+
@JsonProperty("caCertificates")
58+
@Nullable
59+
abstract List<String> getCaCertificates();
60+
5461
/**
5562
* Either EXPLICITLY_MAPPED in order to map external groups to OAuth scopes using the group mappings, or AS_SCOPES to use SAML group names as scopes.
5663
*/

‎integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@
6565
import org.cloudfoundry.reactor.tokenprovider.PasswordGrantTokenProvider;
6666
import org.cloudfoundry.reactor.uaa.ReactorUaaClient;
6767
import org.cloudfoundry.routing.RoutingClient;
68+
import org.cloudfoundry.routing.v1.routergroups.ListRouterGroupsRequest;
69+
import org.cloudfoundry.routing.v1.routergroups.ListRouterGroupsResponse;
70+
import org.cloudfoundry.routing.v1.routergroups.UpdateRouterGroupRequest;
6871
import org.cloudfoundry.uaa.UaaClient;
6972
import org.cloudfoundry.uaa.clients.CreateClientRequest;
7073
import org.cloudfoundry.uaa.groups.AddMemberRequest;
@@ -251,6 +254,7 @@ String clientSecret(NameFactory nameFactory) {
251254
}
252255

253256
@Bean
257+
@DependsOn("routerGroupPorts")
254258
CloudFoundryCleaner cloudFoundryCleaner(
255259
@Qualifier("admin") CloudFoundryClient cloudFoundryClient,
256260
NameFactory nameFactory,
@@ -455,6 +459,55 @@ String planName(NameFactory nameFactory) {
455459
return nameFactory.getPlanName();
456460
}
457461

462+
// Tests allocate TCP ports from NameFactory, so the default router group has to reserve them
463+
// regardless of the order in which the tests run (or of the deployment's default range).
464+
@Bean(initMethod = "block")
465+
Mono<Void> routerGroupPorts(
466+
ConnectionContext connectionContext,
467+
@Value("${test.admin.password}") String password,
468+
@Value("${test.admin.username}") String username,
469+
@Value("${skip.tcp.routing.tests:false}") boolean skipTcpRouting) {
470+
if (skipTcpRouting) {
471+
return Mono.empty();
472+
}
473+
474+
// Not the shared routingClient: its token provider depends on the cleaner, which depends on
475+
// this bean
476+
RoutingClient routingClient =
477+
ReactorRoutingClient.builder()
478+
.connectionContext(connectionContext)
479+
.tokenProvider(
480+
PasswordGrantTokenProvider.builder()
481+
.password(password)
482+
.username(username)
483+
.build())
484+
.build();
485+
486+
return routingClient
487+
.routerGroups()
488+
.list(ListRouterGroupsRequest.builder().build())
489+
.flatMapIterable(ListRouterGroupsResponse::getRouterGroups)
490+
.filter(group -> "default-tcp".equals(group.getName()))
491+
.next()
492+
.switchIfEmpty(
493+
Mono.fromRunnable(
494+
() ->
495+
this.logger.warn(
496+
"No default-tcp router group, TCP route tests will"
497+
+ " fail")))
498+
.flatMap(
499+
group ->
500+
routingClient
501+
.routerGroups()
502+
.update(
503+
UpdateRouterGroupRequest.builder()
504+
.reservablePorts(
505+
RandomNameFactory.PORT_RANGE)
506+
.routerGroupId(group.getRouterGroupId())
507+
.build()))
508+
.then();
509+
}
510+
458511
@Bean
459512
RoutingClient routingClient(ConnectionContext connectionContext, TokenProvider tokenProvider) {
460513
return ReactorRoutingClient.builder()

‎integration-test/src/test/java/org/cloudfoundry/RandomNameFactory.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ final class RandomNameFactory implements NameFactory {
2929

3030
private static final int PORT_MINIMUM = 1025;
3131

32+
/**
33+
* The router group port range that covers every port handed out by {@link #getPort()}
34+
*/
35+
static final String PORT_RANGE = PORT_MINIMUM + "-" + PORT_MAXIMUM;
36+
3237
private final Random random;
3338

3439
private AtomicInteger port = new AtomicInteger(PORT_MINIMUM);

0 commit comments

Comments
 (0)