/*
* Copyright 2013-2017 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;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler;
import com.github.zafarkhaja.semver.Version;
import org.cloudfoundry.client.CloudFoundryClient;
import org.cloudfoundry.client.v2.applications.ApplicationInstanceInfo;
import org.cloudfoundry.client.v2.applications.ApplicationInstancesRequest;
import org.cloudfoundry.client.v2.applications.AssociateApplicationRouteRequest;
import org.cloudfoundry.client.v2.applications.CreateApplicationRequest;
import org.cloudfoundry.client.v2.applications.GetApplicationRequest;
import org.cloudfoundry.client.v2.applications.UpdateApplicationRequest;
import org.cloudfoundry.client.v2.applications.UploadApplicationRequest;
import org.cloudfoundry.client.v2.info.GetInfoRequest;
import org.cloudfoundry.client.v2.organizationquotadefinitions.CreateOrganizationQuotaDefinitionRequest;
import org.cloudfoundry.client.v2.organizations.AssociateOrganizationManagerRequest;
import org.cloudfoundry.client.v2.organizations.CreateOrganizationRequest;
import org.cloudfoundry.client.v2.routes.CreateRouteRequest;
import org.cloudfoundry.client.v2.servicebrokers.CreateServiceBrokerRequest;
import org.cloudfoundry.client.v2.serviceplans.ListServicePlansRequest;
import org.cloudfoundry.client.v2.serviceplans.UpdateServicePlanRequest;
import org.cloudfoundry.client.v2.services.ListServicesRequest;
import org.cloudfoundry.client.v2.shareddomains.ListSharedDomainsRequest;
import org.cloudfoundry.client.v2.spaces.CreateSpaceRequest;
import org.cloudfoundry.client.v2.stacks.ListStacksRequest;
import org.cloudfoundry.doppler.DopplerClient;
import org.cloudfoundry.operations.DefaultCloudFoundryOperations;
import org.cloudfoundry.reactor.ConnectionContext;
import org.cloudfoundry.reactor.DefaultConnectionContext;
import org.cloudfoundry.reactor.ProxyConfiguration;
import org.cloudfoundry.reactor.TokenProvider;
import org.cloudfoundry.reactor.client.ReactorCloudFoundryClient;
import org.cloudfoundry.reactor.doppler.ReactorDopplerClient;
import org.cloudfoundry.reactor.routing.ReactorRoutingClient;
import org.cloudfoundry.reactor.tokenprovider.ClientCredentialsGrantTokenProvider;
import org.cloudfoundry.reactor.tokenprovider.PasswordGrantTokenProvider;
import org.cloudfoundry.reactor.uaa.ReactorUaaClient;
import org.cloudfoundry.routing.RoutingClient;
import org.cloudfoundry.uaa.UaaClient;
import org.cloudfoundry.uaa.clients.CreateClientRequest;
import org.cloudfoundry.uaa.groups.AddMemberRequest;
import org.cloudfoundry.uaa.groups.CreateGroupRequest;
import org.cloudfoundry.uaa.groups.CreateGroupResponse;
import org.cloudfoundry.uaa.groups.Group;
import org.cloudfoundry.uaa.groups.ListGroupsRequest;
import org.cloudfoundry.uaa.groups.ListGroupsResponse;
import org.cloudfoundry.uaa.groups.MemberType;
import org.cloudfoundry.uaa.users.CreateUserRequest;
import org.cloudfoundry.uaa.users.CreateUserResponse;
import org.cloudfoundry.uaa.users.Email;
import org.cloudfoundry.uaa.users.Name;
import org.cloudfoundry.util.JobUtils;
import org.cloudfoundry.util.PaginationUtils;
import org.cloudfoundry.util.ResourceUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
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.context.annotation.DependsOn;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;
import java.io.IOException;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import static org.assertj.core.api.Assertions.fail;
import static org.cloudfoundry.uaa.tokens.GrantType.AUTHORIZATION_CODE;
import static org.cloudfoundry.uaa.tokens.GrantType.CLIENT_CREDENTIALS;
import static org.cloudfoundry.uaa.tokens.GrantType.PASSWORD;
import static org.cloudfoundry.uaa.tokens.GrantType.REFRESH_TOKEN;
import static org.cloudfoundry.util.DelayUtils.exponentialBackOff;
import static org.cloudfoundry.util.tuple.TupleUtils.function;
@Configuration
@EnableAutoConfiguration
public class IntegrationTestConfiguration {
private static final List<String> GROUPS = Arrays.asList(
"clients.admin",
"clients.secret",
"cloud_controller.admin",
"idps.write",
"routing.router_groups.read",
"routing.router_groups.write",
"routing.routes.read",
"routing.routes.write",
"scim.create",
"scim.invite",
"scim.read",
"scim.userids",
"scim.write",
"scim.zones",
"uaa.admin",
"zones.read",
"zones.write");
private static final List<String> SCOPES = Arrays.asList(
"clients.admin",
"clients.secret",
"cloud_controller.admin",
"cloud_controller.read",
"cloud_controller.write",
"idps.write",
"password.write",
"routing.router_groups.read",
"routing.router_groups.write",
"routing.routes.read",
"routing.routes.write",
"scim.create",
"scim.invite",
"scim.read",
"scim.userids",
"scim.write",
"scim.zones",
"uaa.admin",
"uaa.user",
"zones.read",
"zones.write");
private final Logger logger = LoggerFactory.getLogger("cloudfoundry-client.test");
@Bean
@Qualifier("admin")
ReactorCloudFoundryClient adminCloudFoundryClient(ConnectionContext connectionContext,
@Value("${test.admin.password}") String password,
@Value("${test.admin.username}") String username) {
return ReactorCloudFoundryClient.builder()
.connectionContext(connectionContext)
.tokenProvider(PasswordGrantTokenProvider.builder()
.password(password)
.username(username)
.build())
.build();
}
@Bean
@Qualifier("admin")
ReactorUaaClient 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();
}
@Bean(initMethod = "block")
@DependsOn("cloudFoundryCleaner")
Mono<Tuple2<String, String>> client(@Qualifier("admin") UaaClient uaaClient, String clientId, String clientSecret) {
return uaaClient.clients()
.create(CreateClientRequest.builder()
.authorizedGrantType(AUTHORIZATION_CODE, CLIENT_CREDENTIALS, PASSWORD, REFRESH_TOKEN)
.autoApprove(String.valueOf(true))
.clientId(clientId)
.clientSecret(clientSecret)
.redirectUriPattern("/login")
.scopes(SCOPES)
.build())
.then(Mono.just(Tuples.of(clientId, clientSecret)))
.doOnSubscribe(s -> this.logger.debug(">> CLIENT ({}/{}) <<", clientId, clientSecret))
.doOnError(Throwable::printStackTrace)
.doOnSuccess(r -> this.logger.debug("<< CLIENT ({})>>", clientId));
}
@Bean
String clientId(NameFactory nameFactory) {
return nameFactory.getClientId();
}
@Bean
String clientSecret(NameFactory nameFactory) {
return nameFactory.getClientSecret();
}
@Bean(initMethod = "clean", destroyMethod = "clean")
CloudFoundryCleaner cloudFoundryCleaner(@Qualifier("admin") CloudFoundryClient cloudFoundryClient, NameFactory nameFactory, @Qualifier("admin") UaaClient uaaClient) {
return new CloudFoundryCleaner(cloudFoundryClient, nameFactory, uaaClient);
}
@Bean
ReactorCloudFoundryClient cloudFoundryClient(ConnectionContext connectionContext, TokenProvider tokenProvider) {
return ReactorCloudFoundryClient.builder()
.connectionContext(connectionContext)
.tokenProvider(tokenProvider)
.build();
}
@Bean
DefaultCloudFoundryOperations cloudFoundryOperations(CloudFoundryClient cloudFoundryClient, DopplerClient dopplerClient, RoutingClient routingClient, UaaClient uaaClient, String organizationName,
String spaceName) {
return DefaultCloudFoundryOperations.builder()
.cloudFoundryClient(cloudFoundryClient)
.dopplerClient(dopplerClient)
.routingClient(routingClient)
.uaaClient(uaaClient)
.organization(organizationName)
.space(spaceName)
.build();
}
@Bean
CloudFoundryVersionConditionalRule cloudFoundryVersionConditionalRule(CloudFoundryClient cloudFoundryClient) {
return cloudFoundryClient.info()
.get(GetInfoRequest.builder()
.build())
.map(response -> Version.valueOf(response.getApiVersion()))
.map(CloudFoundryVersionConditionalRule::new)
.doOnSubscribe(s -> this.logger.debug(">> CLOUD FOUNDRY VERSION <<"))
.doOnSuccess(r -> this.logger.debug("<< CLOUD FOUNDRY VERSION >>"))
.block();
}
@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();
}
@Bean
DopplerClient dopplerClient(ConnectionContext connectionContext, TokenProvider tokenProvider) {
return ReactorDopplerClient.builder()
.connectionContext(connectionContext)
.tokenProvider(tokenProvider)
.build();
}
@Bean
RandomNameFactory nameFactory(Random random) {
return new RandomNameFactory(random);
}
@Bean(initMethod = "block")
@DependsOn("cloudFoundryCleaner")
Mono<String> organizationId(CloudFoundryClient cloudFoundryClient, String organizationName, String organizationQuotaName, Mono<String> userId) throws InterruptedException {
return userId
.then(userId1 -> cloudFoundryClient.organizationQuotaDefinitions()
.create(CreateOrganizationQuotaDefinitionRequest.builder()
.applicationInstanceLimit(-1)
.applicationTaskLimit(-1)
.instanceMemoryLimit(-1)
.memoryLimit(8192)
.name(organizationQuotaName)
.nonBasicServicesAllowed(true)
.totalPrivateDomains(-1)
.totalReservedRoutePorts(-1)
.totalRoutes(-1)
.totalServiceKeys(-1)
.totalServices(-1)
.build())
.map(ResourceUtils::getId)
.and(Mono.just(userId1)))
.then(function((quotaId, userId1) -> cloudFoundryClient.organizations()
.create(CreateOrganizationRequest.builder()
.name(organizationName)
.quotaDefinitionId(quotaId)
.build())
.map(ResourceUtils::getId)
.and(Mono.just(userId1))))
.then(function((organizationId, userId1) -> cloudFoundryClient.organizations()
.associateManager(AssociateOrganizationManagerRequest.builder()
.organizationId(organizationId)
.managerId(userId1)
.build())
.then(Mono.just(organizationId))))
.doOnSubscribe(s -> this.logger.debug(">> ORGANIZATION ({}) <<", organizationName))
.doOnError(Throwable::printStackTrace)
.doOnSuccess(id -> this.logger.debug("<< ORGANIZATION ({}) >>", id))
.cache();
}
@Bean
String organizationName(NameFactory nameFactory) {
return nameFactory.getOrganizationName();
}
@Bean
String organizationQuotaName(NameFactory nameFactory) {
return nameFactory.getQuotaDefinitionName();
}
@Bean
String password(NameFactory nameFactory) {
return nameFactory.getPassword();
}
@Bean
String planName(NameFactory nameFactory) {
return nameFactory.getPlanName();
}
@Bean
SecureRandom random() {
return new SecureRandom();
}
@Bean
RoutingClient routingClient(ConnectionContext connectionContext, TokenProvider tokenProvider) {
return ReactorRoutingClient.builder()
.connectionContext(connectionContext)
.tokenProvider(tokenProvider)
.build();
}
@Bean(initMethod = "block")
@DependsOn("cloudFoundryCleaner")
Mono<String> serviceBrokerId(CloudFoundryClient cloudFoundryClient, NameFactory nameFactory, Mono<String> spaceId, String serviceBrokerName, String serviceName, String planName)
throws IOException {
Path application = new ClassPathResource("test-service-broker.jar").getFile().toPath();
String hostName = nameFactory.getHostName();
return Mono
.when(
spaceId,
PaginationUtils
.requestClientV2Resources(page -> cloudFoundryClient.sharedDomains()
.list(ListSharedDomainsRequest.builder()
.page(page)
.build()))
.next()
)
.then(function((space, domain) -> Mono
.when(
cloudFoundryClient.applicationsV2()
.create(CreateApplicationRequest.builder()
.buildpack("http://github.com/cloudfoundry/java-buildpack.git")
.memory(768)
.name(nameFactory.getApplicationName())
.spaceId(space)
.build())
.map(ResourceUtils::getId),
cloudFoundryClient.routes()
.create(CreateRouteRequest.builder()
.domainId(ResourceUtils.getId(domain))
.host(hostName)
.spaceId(space)
.build())
.map(ResourceUtils::getId)
)
.then(function((applicationId, routeId) -> cloudFoundryClient.applicationsV2()
.associateRoute(AssociateApplicationRouteRequest.builder()
.applicationId(applicationId)
.routeId(routeId)
.build())
.then(Mono.just(applicationId))))
.then(applicationId -> cloudFoundryClient.applicationsV2()
.upload(UploadApplicationRequest.builder()
.application(application)
.applicationId(applicationId)
.async(true)
.build())
.then(job -> JobUtils.waitForCompletion(cloudFoundryClient, Duration.ofMinutes(5), job))
.then(Mono.just(applicationId)))
.then(applicationId -> cloudFoundryClient.applicationsV2()
.update(UpdateApplicationRequest.builder()
.applicationId(applicationId)
.environmentJson("SERVICE_NAME", serviceName)
.environmentJson("PLAN_NAME", planName)
.state("STARTED")
.build())
.then(Mono.just(applicationId)))
.then(applicationId -> cloudFoundryClient.applicationsV2()
.get(GetApplicationRequest.builder()
.applicationId(applicationId)
.build())
.map(response -> ResourceUtils.getEntity(response).getPackageState())
.filter(state -> "STAGED".equals(state) || "FAILED".equals(state))
.repeatWhenEmpty(exponentialBackOff(Duration.ofSeconds(1), Duration.ofSeconds(5), Duration.ofMinutes(5)))
.then(Mono.just(applicationId)))
.then(applicationId -> cloudFoundryClient.applicationsV2()
.instances(ApplicationInstancesRequest.builder()
.applicationId(applicationId)
.build())
.flatMapMany(response -> Flux.fromIterable(response.getInstances().values()))
.single()
.map(ApplicationInstanceInfo::getState)
.filter("RUNNING"::equals)
.repeatWhenEmpty(exponentialBackOff(Duration.ofSeconds(1), Duration.ofSeconds(5), Duration.ofMinutes(5))))
.then(Mono.just(String.format("https://%s.%s", hostName, ResourceUtils.getEntity(domain).getName())))
))
.then(url -> cloudFoundryClient.serviceBrokers()
.create(CreateServiceBrokerRequest.builder()
.authenticationPassword("test-authentication-password")
.authenticationUsername("test-authentication-username")
.brokerUrl(url)
.name(serviceBrokerName)
.build())
.map(ResourceUtils::getId))
.then(serviceBrokerId -> PaginationUtils
.requestClientV2Resources(page -> cloudFoundryClient.services()
.list(ListServicesRequest.builder()
.label(serviceName)
.build()))
.single()
.map(ResourceUtils::getId)
.then(serviceId -> PaginationUtils
.requestClientV2Resources(page -> cloudFoundryClient.servicePlans()
.list(ListServicePlansRequest.builder()
.serviceId(serviceId)
.page(page)
.build()))
.single()
.map(ResourceUtils::getId))
.then(planId -> cloudFoundryClient.servicePlans()
.update(UpdateServicePlanRequest.builder()
.servicePlanId(planId)
.publiclyVisible(true)
.build())
.then(Mono.just(serviceBrokerId))))
.doOnSubscribe(s -> this.logger.debug(">> SERVICE BROKER ({} {}/{}) <<", serviceBrokerName, serviceName, planName))
.doOnError(Throwable::printStackTrace)
.doOnSuccess(id -> this.logger.debug("<< SERVICE_BROKER ({})>>", id))
.cache();
}
@Bean
String serviceBrokerName(NameFactory nameFactory) {
return nameFactory.getServiceBrokerName();
}
@Bean
String serviceName(NameFactory nameFactory) {
return nameFactory.getServiceName();
}
@Bean(initMethod = "block")
@DependsOn("cloudFoundryCleaner")
Mono<String> spaceId(CloudFoundryClient cloudFoundryClient, Mono<String> organizationId, String spaceName) throws InterruptedException {
return organizationId
.then(orgId -> cloudFoundryClient.spaces()
.create(CreateSpaceRequest.builder()
.name(spaceName)
.organizationId(orgId)
.build()))
.map(ResourceUtils::getId)
.doOnSubscribe(s -> this.logger.debug(">> SPACE ({}) <<", spaceName))
.doOnError(Throwable::printStackTrace)
.doOnSuccess(id -> this.logger.debug("<< SPACE ({}) >>", id))
.cache();
}
@Bean
String spaceName(NameFactory nameFactory) {
return nameFactory.getSpaceName();
}
@Bean(initMethod = "block")
@DependsOn("cloudFoundryCleaner")
Mono<String> stackId(CloudFoundryClient cloudFoundryClient, String stackName) throws InterruptedException {
return PaginationUtils
.requestClientV2Resources(page -> cloudFoundryClient.stacks()
.list(ListStacksRequest.builder()
.name(stackName)
.page(page)
.build()))
.single()
.map(ResourceUtils::getId)
.doOnSubscribe(s -> this.logger.debug(">> STACK ({}) <<", stackName))
.doOnError(Throwable::printStackTrace)
.doOnSuccess(id -> this.logger.debug("<< STACK ({})>>", id))
.cache();
}
@Bean
String stackName() {
return "cflinuxfs2";
}
@Bean
@DependsOn({"client", "userId"})
PasswordGrantTokenProvider tokenProvider(String clientId, String clientSecret, String password, String username) {
return PasswordGrantTokenProvider.builder()
.clientId(clientId)
.clientSecret(clientSecret)
.password(password)
.username(username)
.build();
}
@Bean
ReactorUaaClient uaaClient(ConnectionContext connectionContext, TokenProvider tokenProvider) {
return ReactorUaaClient.builder()
.connectionContext(connectionContext)
.tokenProvider(tokenProvider)
.build();
}
@Bean(initMethod = "block")
@DependsOn("cloudFoundryCleaner")
Mono<String> userId(@Qualifier("admin") UaaClient uaaClient, String password, String username) {
return uaaClient.users()
.create(CreateUserRequest.builder()
.email(Email.builder()
.primary(true)
.value(String.format("%s@%s.com", username, username))
.build())
.name(Name.builder()
.givenName("Test")
.familyName("User")
.build())
.password(password)
.userName(username)
.build())
.map(CreateUserResponse::getId)
.then(userId -> Flux.fromIterable(GROUPS)
.flatMap(group -> uaaClient.groups()
.list(ListGroupsRequest.builder()
.filter(String.format("displayName eq \"%s\"", group))
.build())
.flatMapIterable(ListGroupsResponse::getResources)
.singleOrEmpty()
.map(Group::getId)
.switchIfEmpty(uaaClient.groups()
.create(CreateGroupRequest.builder()
.displayName(group)
.build())
.map(CreateGroupResponse::getId))
.then(groupId -> uaaClient.groups()
.addMember(AddMemberRequest.builder()
.groupId(groupId)
.memberId(userId)
.origin("uaa")
.type(MemberType.USER)
.build())))
.then()
.then(Mono.just(userId)))
.doOnSubscribe(s -> this.logger.debug(">> USER ({}/{}) <<", username, password))
.doOnError(Throwable::printStackTrace)
.doOnSuccess(id -> this.logger.debug("<< USER ({})>>", id))
.cache();
}
@Bean
String username(NameFactory nameFactory) {
return nameFactory.getUserName();
}
private static final class FailingDeserializationProblemHandler extends DeserializationProblemHandler {
@Override
public boolean handleUnknownProperty(DeserializationContext ctxt, JsonParser jp, JsonDeserializer<?> deserializer, Object beanOrClass, String propertyName) {
fail(String.format("Found unexpected property %s in payload for %s", propertyName, beanOrClass.getClass().getName()));
return false;
}
}
}