Dynamically add space developer role for non-admin clients

When using the space per service instance strategy, currently the OAuth
client requires `cloud_controller.admin` privileges to deploy apps and
services to a new space. This commit adds support for dynamically granting
the space developer role to a client which has the org manager role but not
admin privileges.

Resolves #203

Requiring client credentials to run ATs
This commit is contained in:
Roy Clarkson
2019-04-25 00:07:09 -04:00
committed by Roy Clarkson
parent bc75e248c2
commit 29be9d77be
10 changed files with 359 additions and 129 deletions

View File

@@ -16,14 +16,8 @@ The tests require the following properties to be set:
* `spring.cloud.appbroker.acceptance-test.cloudfoundry.default-org` - The CF organization where the tests are going to run.
* `spring.cloud.appbroker.acceptance-test.cloudfoundry.default-space` - The CF space where the tests are going to run.
* `spring.cloud.appbroker.acceptance-test.cloudfoundry.skip-ssl-validation` - If SSL validation should be skipped.
To authenticate to the target CF with a user account, set the following properties:
* `spring.cloud.appbroker.acceptance-test.cloudfoundry.username` - The CF API username.
* `spring.cloud.appbroker.acceptance-test.cloudfoundry.password` - The CF API password.
To authenticate to the target CF with an OAuth2 client in UAA, set the following properties:
* `spring.cloud.appbroker.acceptance-test.cloudfoundry.client-id` - The CF API OAuth2 client ID.
* `spring.cloud.appbroker.acceptance-test.cloudfoundry.client-secret` - The CF API OAuth2 client secret.

View File

@@ -36,7 +36,9 @@ import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
import com.jayway.jsonpath.spi.mapper.MappingProvider;
import org.cloudfoundry.operations.applications.ApplicationEnvironments;
import org.cloudfoundry.operations.applications.ApplicationSummary;
import org.cloudfoundry.operations.organizations.OrganizationSummary;
import org.cloudfoundry.operations.services.ServiceInstance;
import org.cloudfoundry.operations.spaces.SpaceSummary;
import org.cloudfoundry.uaa.clients.GetClientResponse;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -54,6 +56,12 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.cloud.appbroker.acceptance.fixtures.cf.CloudFoundryClientConfiguration.ACCEPTANCE_TEST_OAUTH_CLIENT_AUTHORITIES;
import static org.springframework.cloud.appbroker.acceptance.fixtures.cf.CloudFoundryClientConfiguration.ACCEPTANCE_TEST_OAUTH_CLIENT_ID;
import static org.springframework.cloud.appbroker.acceptance.fixtures.cf.CloudFoundryClientConfiguration.ACCEPTANCE_TEST_OAUTH_CLIENT_SECRET;
import static org.springframework.cloud.appbroker.acceptance.fixtures.cf.CloudFoundryClientConfiguration.APP_BROKER_CLIENT_AUTHORITIES;
import static org.springframework.cloud.appbroker.acceptance.fixtures.cf.CloudFoundryClientConfiguration.APP_BROKER_CLIENT_ID;
import static org.springframework.cloud.appbroker.acceptance.fixtures.cf.CloudFoundryClientConfiguration.APP_BROKER_CLIENT_SECRET;
@SpringBootTest(classes = {
CloudFoundryClientConfiguration.class,
@@ -114,24 +122,41 @@ class CloudFoundryAcceptanceTest {
@AfterEach
void tearDown() {
blockingSubscribe(cleanup());
blockingSubscribe(cloudFoundryService.getOrCreateDefaultOrganization()
.map(OrganizationSummary::getId)
.flatMap(orgId -> cloudFoundryService.getOrCreateDefaultSpace()
.map(SpaceSummary::getId)
.flatMap(spaceId -> cleanup(orgId, spaceId))));
}
private Mono<Void> initializeBroker(String... appBrokerProperties) {
return cleanup()
.then(
cloudFoundryService
.getOrCreateDefaultOrganization()
.then(cloudFoundryService.getOrCreateDefaultSpace())
return cloudFoundryService
.getOrCreateDefaultOrganization()
.map(OrganizationSummary::getId)
.flatMap(orgId -> cloudFoundryService
.getOrCreateDefaultSpace()
.map(SpaceSummary::getId)
.flatMap(spaceId -> cleanup(orgId, spaceId)
.then(uaaService.createClient(
ACCEPTANCE_TEST_OAUTH_CLIENT_ID,
ACCEPTANCE_TEST_OAUTH_CLIENT_SECRET,
ACCEPTANCE_TEST_OAUTH_CLIENT_AUTHORITIES))
.then(uaaService.createClient(
APP_BROKER_CLIENT_ID,
APP_BROKER_CLIENT_SECRET,
APP_BROKER_CLIENT_AUTHORITIES))
.then(cloudFoundryService.associateAppBrokerClientWithOrgAndSpace(orgId, spaceId))
.then(cloudFoundryService.pushBrokerApp(TEST_BROKER_APP_NAME, getTestBrokerAppPath(), appBrokerProperties))
.then(cloudFoundryService.createServiceBroker(SERVICE_BROKER_NAME, TEST_BROKER_APP_NAME))
.then(cloudFoundryService.enableServiceBrokerAccess(APP_SERVICE_NAME))
.then(cloudFoundryService.enableServiceBrokerAccess(BACKING_SERVICE_NAME)));
.then(cloudFoundryService.enableServiceBrokerAccess(BACKING_SERVICE_NAME))));
}
private Mono<Void> cleanup() {
private Mono<Void> cleanup(String orgId, String spaceId) {
return cloudFoundryService.deleteServiceBroker(SERVICE_BROKER_NAME)
.then(cloudFoundryService.deleteApp(TEST_BROKER_APP_NAME));
.then(cloudFoundryService.deleteApp(TEST_BROKER_APP_NAME))
.then(cloudFoundryService.removeAppBrokerClientFromOrgAndSpace(orgId, spaceId))
.onErrorResume(e -> Mono.empty());
}
void createServiceInstance(String serviceInstanceName) {

View File

@@ -18,7 +18,6 @@ package org.springframework.cloud.appbroker.acceptance.fixtures.cf;
import java.util.Optional;
import org.cloudfoundry.UnknownCloudFoundryException;
import org.cloudfoundry.client.CloudFoundryClient;
import org.cloudfoundry.doppler.DopplerClient;
import org.cloudfoundry.operations.CloudFoundryOperations;
@@ -32,31 +31,39 @@ import org.cloudfoundry.reactor.tokenprovider.ClientCredentialsGrantTokenProvide
import org.cloudfoundry.reactor.tokenprovider.PasswordGrantTokenProvider;
import org.cloudfoundry.reactor.uaa.ReactorUaaClient;
import org.cloudfoundry.uaa.UaaClient;
import org.cloudfoundry.uaa.UaaException;
import org.cloudfoundry.uaa.clients.Clients;
import org.cloudfoundry.uaa.clients.CreateClientRequest;
import org.cloudfoundry.uaa.clients.DeleteClientRequest;
import org.cloudfoundry.uaa.tokens.GrantType;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
@Configuration
@EnableConfigurationProperties(CloudFoundryProperties.class)
public class CloudFoundryClientConfiguration {
static final String ACCEPTANCE_TEST_OAUTH_CLIENT_ID = "acceptance-test-client";
static final String ACCEPTANCE_TEST_OAUTH_CLIENT_SECRET = "acceptance-test-client-secret";
private static final String[] ACCEPTANCE_TEST_OAUTH_CLIENT_AUTHORITIES = {
"openid", "cloud_controller.admin", "cloud_controller.read", "cloud_controller.write",
"clients.read", "clients.write"
public static final String ACCEPTANCE_TEST_OAUTH_CLIENT_ID = "acceptance-test-client";
public static final String ACCEPTANCE_TEST_OAUTH_CLIENT_SECRET = "acceptance-test-client-secret";
public static final String[] ACCEPTANCE_TEST_OAUTH_CLIENT_AUTHORITIES = {
"openid",
"cloud_controller.admin",
"cloud_controller.read",
"cloud_controller.write",
"clients.read",
"clients.write"
};
public static final String APP_BROKER_CLIENT_ID = "app-broker-client";
public static final String APP_BROKER_CLIENT_SECRET = "app-broker-client-secret";
public static final String[] APP_BROKER_CLIENT_AUTHORITIES = {
"cloud_controller.read", "cloud_controller.write"
};
@Bean
CloudFoundryOperations cloudFoundryOperations(CloudFoundryProperties properties, CloudFoundryClient client,
DopplerClient dopplerClient, UaaClient uaaClient) {
CloudFoundryOperations cloudFoundryOperations(CloudFoundryProperties properties,
CloudFoundryClient client,
DopplerClient dopplerClient,
UaaClient uaaClient) {
return DefaultCloudFoundryOperations.builder()
.cloudFoundryClient(client)
.dopplerClient(dopplerClient)
@@ -67,7 +74,8 @@ public class CloudFoundryClientConfiguration {
}
@Bean
CloudFoundryClient cloudFoundryClient(ConnectionContext connectionContext, TokenProvider tokenProvider) {
CloudFoundryClient cloudFoundryClient(ConnectionContext connectionContext,
@Qualifier("userCredentials") TokenProvider tokenProvider) {
return ReactorCloudFoundryClient.builder()
.connectionContext(connectionContext)
.tokenProvider(tokenProvider)
@@ -85,7 +93,8 @@ public class CloudFoundryClientConfiguration {
}
@Bean
DopplerClient dopplerClient(ConnectionContext connectionContext, TokenProvider tokenProvider) {
DopplerClient dopplerClient(ConnectionContext connectionContext,
@Qualifier("userCredentials") TokenProvider tokenProvider) {
return ReactorDopplerClient.builder()
.connectionContext(connectionContext)
.tokenProvider(tokenProvider)
@@ -93,7 +102,8 @@ public class CloudFoundryClientConfiguration {
}
@Bean
UaaClient uaaClient(ConnectionContext connectionContext, TokenProvider tokenProvider) {
UaaClient uaaClient(ConnectionContext connectionContext,
@Qualifier("clientCredentials") TokenProvider tokenProvider) {
return ReactorUaaClient.builder()
.connectionContext(connectionContext)
.tokenProvider(tokenProvider)
@@ -101,8 +111,11 @@ public class CloudFoundryClientConfiguration {
}
@Bean
@ConditionalOnProperty({CloudFoundryProperties.PROPERTY_PREFIX + ".username",
CloudFoundryProperties.PROPERTY_PREFIX + ".password"})
@Qualifier("userCredentials")
@ConditionalOnProperty({
CloudFoundryProperties.PROPERTY_PREFIX + ".username",
CloudFoundryProperties.PROPERTY_PREFIX + ".password"
})
PasswordGrantTokenProvider passwordTokenProvider(CloudFoundryProperties properties) {
return PasswordGrantTokenProvider.builder()
.password(properties.getPassword())
@@ -111,42 +124,17 @@ public class CloudFoundryClientConfiguration {
}
@Bean
@ConditionalOnProperty({CloudFoundryProperties.PROPERTY_PREFIX + ".client-id",
CloudFoundryProperties.PROPERTY_PREFIX + ".client-secret"})
ClientCredentialsGrantTokenProvider clientTokenProvider(ConnectionContext connectionContext,
CloudFoundryProperties properties) {
Clients uaaClients = buildTempUaaClient(connectionContext, properties).clients();
uaaClients.delete(DeleteClientRequest.builder()
.clientId(ACCEPTANCE_TEST_OAUTH_CLIENT_ID)
.build())
.onErrorResume(UaaException.class, e -> Mono.empty())
.onErrorResume(UnknownCloudFoundryException.class, e -> Mono.empty())
.then(uaaClients.create(CreateClientRequest.builder()
.clientId(ACCEPTANCE_TEST_OAUTH_CLIENT_ID)
.clientSecret(ACCEPTANCE_TEST_OAUTH_CLIENT_SECRET)
.authorizedGrantType(GrantType.CLIENT_CREDENTIALS)
.authorities(ACCEPTANCE_TEST_OAUTH_CLIENT_AUTHORITIES)
.build()))
.block();
@Qualifier("clientCredentials")
@ConditionalOnProperty({
CloudFoundryProperties.PROPERTY_PREFIX + ".client-id",
CloudFoundryProperties.PROPERTY_PREFIX + ".client-secret"
})
ClientCredentialsGrantTokenProvider clientTokenProvider(CloudFoundryProperties properties) {
return ClientCredentialsGrantTokenProvider.builder()
.clientId(ACCEPTANCE_TEST_OAUTH_CLIENT_ID)
.clientSecret(ACCEPTANCE_TEST_OAUTH_CLIENT_SECRET)
.clientId(properties.getClientId())
.clientSecret(properties.getClientSecret())
.identityZoneSubdomain(properties.getIdentityZoneSubdomain())
.build();
}
private UaaClient buildTempUaaClient(ConnectionContext connectionContext, CloudFoundryProperties properties) {
return ReactorUaaClient.builder()
.connectionContext(connectionContext)
.tokenProvider(ClientCredentialsGrantTokenProvider.builder()
.clientId(properties.getClientId())
.clientSecret(properties.getClientSecret())
.identityZoneSubdomain(properties.getIdentityZoneSubdomain())
.build())
.build();
}
}

View File

@@ -22,6 +22,16 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.cloudfoundry.client.CloudFoundryClient;
import org.cloudfoundry.client.v2.organizations.AssociateOrganizationManagerRequest;
import org.cloudfoundry.client.v2.organizations.AssociateOrganizationManagerResponse;
import org.cloudfoundry.client.v2.organizations.AssociateOrganizationUserRequest;
import org.cloudfoundry.client.v2.organizations.AssociateOrganizationUserResponse;
import org.cloudfoundry.client.v2.organizations.RemoveOrganizationManagerRequest;
import org.cloudfoundry.client.v2.organizations.RemoveOrganizationUserRequest;
import org.cloudfoundry.client.v2.spaces.AssociateSpaceDeveloperRequest;
import org.cloudfoundry.client.v2.spaces.AssociateSpaceDeveloperResponse;
import org.cloudfoundry.client.v2.spaces.RemoveSpaceDeveloperRequest;
import org.cloudfoundry.operations.CloudFoundryOperations;
import org.cloudfoundry.operations.DefaultCloudFoundryOperations;
import org.cloudfoundry.operations.applications.ApplicationDetail;
@@ -63,11 +73,16 @@ public class CloudFoundryService {
private static final String DEPLOYER_PROPERTY_PREFIX = "spring.cloud.appbroker.deployer.cloudfoundry.";
private final CloudFoundryClient cloudFoundryClient;
private final CloudFoundryOperations cloudFoundryOperations;
private final CloudFoundryProperties cloudFoundryProperties;
public CloudFoundryService(CloudFoundryOperations cloudFoundryOperations,
public CloudFoundryService(CloudFoundryClient cloudFoundryClient,
CloudFoundryOperations cloudFoundryOperations,
CloudFoundryProperties cloudFoundryProperties) {
this.cloudFoundryClient = cloudFoundryClient;
this.cloudFoundryOperations = cloudFoundryOperations;
this.cloudFoundryProperties = cloudFoundryProperties;
}
@@ -129,7 +144,7 @@ public class CloudFoundryService {
.deleteRoutes(true)
.build())
.doOnSuccess(item -> LOGGER.info("Deleted app " + appName))
.doOnError(error -> LOGGER.error("Error deleting app " + appName + ": " + error))
.doOnError(error -> LOGGER.warn("Error deleting app " + appName + ": " + error))
.onErrorResume(e -> Mono.empty());
}
@@ -139,7 +154,7 @@ public class CloudFoundryService {
.name(brokerName)
.build())
.doOnSuccess(item -> LOGGER.info("Deleted service broker " + brokerName))
.doOnError(error -> LOGGER.error("Error deleting service broker " + brokerName + ": " + error))
.doOnError(error -> LOGGER.warn("Error deleting service broker " + brokerName + ": " + error))
.onErrorResume(e -> Mono.empty());
}
@@ -152,7 +167,7 @@ public class CloudFoundryService {
.doOnSuccess(item -> LOGGER.info("Deleted service instance " + serviceInstanceName))
.doOnError(error -> LOGGER.error("Error deleting service instance " + serviceInstanceName + ": " + error))
.onErrorResume(e -> Mono.empty()))
.doOnError(error -> LOGGER.error("Error getting service instance " + serviceInstanceName + ": " + error))
.doOnError(error -> LOGGER.warn("Error getting service instance " + serviceInstanceName + ": " + error))
.onErrorResume(e -> Mono.empty());
}
@@ -303,6 +318,63 @@ public class CloudFoundryService {
.next();
}
public Mono<Void> associateAppBrokerClientWithOrgAndSpace(String orgId, String spaceId) {
return Mono.justOrEmpty(CloudFoundryClientConfiguration.APP_BROKER_CLIENT_ID)
.flatMap(userId -> associateOrgUser(orgId, userId)
.then(associateOrgManager(orgId, userId))
.then(associateSpaceDeveloper(spaceId, userId)))
.then();
}
public Mono<Void> removeAppBrokerClientFromOrgAndSpace(String orgId, String spaceId) {
return Mono.justOrEmpty(CloudFoundryClientConfiguration.APP_BROKER_CLIENT_ID)
.flatMap(userId -> removeSpaceDeveloper(spaceId, userId)
.then(removeOrgManager(orgId, userId))
.then(removeOrgUser(orgId, userId)));
}
private Mono<AssociateOrganizationUserResponse> associateOrgUser(String orgId, String userId) {
return cloudFoundryClient.organizations().associateUser(AssociateOrganizationUserRequest.builder()
.organizationId(orgId)
.userId(userId)
.build());
}
private Mono<AssociateOrganizationManagerResponse> associateOrgManager(String orgId, String userId) {
return cloudFoundryClient.organizations().associateManager(AssociateOrganizationManagerRequest.builder()
.organizationId(orgId)
.managerId(userId)
.build());
}
private Mono<AssociateSpaceDeveloperResponse> associateSpaceDeveloper(String spaceId, String userId) {
return cloudFoundryClient.spaces().associateDeveloper(AssociateSpaceDeveloperRequest.builder()
.spaceId(spaceId)
.developerId(userId)
.build());
}
private Mono<Void> removeOrgUser(String orgId, String userId) {
return cloudFoundryClient.organizations().removeUser(RemoveOrganizationUserRequest.builder()
.organizationId(orgId)
.userId(userId)
.build());
}
private Mono<Void> removeOrgManager(String orgId, String userId) {
return cloudFoundryClient.organizations().removeManager(RemoveOrganizationManagerRequest.builder()
.organizationId(orgId)
.managerId(userId)
.build());
}
private Mono<Void> removeSpaceDeveloper(String spaceId, String userId) {
return cloudFoundryClient.spaces().removeDeveloper(RemoveSpaceDeveloperRequest.builder()
.spaceId(spaceId)
.developerId(userId)
.build());
}
private CloudFoundryOperations createOperationsForSpace(String space) {
final String defaultOrg = cloudFoundryProperties.getDefaultOrg();
return DefaultCloudFoundryOperations.builder()
@@ -325,19 +397,10 @@ public class CloudFoundryService {
deployerVariables.put(DEPLOYER_PROPERTY_PREFIX + "skip-ssl-validation",
String.valueOf(cloudFoundryProperties.isSkipSslValidation()));
deployerVariables.put(DEPLOYER_PROPERTY_PREFIX + "properties.memory", "1024M");
if (cloudFoundryProperties.getUsername() == null || cloudFoundryProperties.getPassword() == null) {
deployerVariables.put(DEPLOYER_PROPERTY_PREFIX + "client-id",
CloudFoundryClientConfiguration.ACCEPTANCE_TEST_OAUTH_CLIENT_ID);
deployerVariables.put(DEPLOYER_PROPERTY_PREFIX + "client-secret",
CloudFoundryClientConfiguration.ACCEPTANCE_TEST_OAUTH_CLIENT_SECRET);
} else {
deployerVariables.put(DEPLOYER_PROPERTY_PREFIX + "username",
cloudFoundryProperties.getUsername());
deployerVariables.put(DEPLOYER_PROPERTY_PREFIX + "password",
cloudFoundryProperties.getPassword());
}
deployerVariables.put(DEPLOYER_PROPERTY_PREFIX + "client-id",
CloudFoundryClientConfiguration.APP_BROKER_CLIENT_ID);
deployerVariables.put(DEPLOYER_PROPERTY_PREFIX + "client-secret",
CloudFoundryClientConfiguration.APP_BROKER_CLIENT_SECRET);
return deployerVariables;
}

View File

@@ -17,13 +17,22 @@
package org.springframework.cloud.appbroker.acceptance.fixtures.uaa;
import org.cloudfoundry.uaa.UaaClient;
import org.cloudfoundry.uaa.clients.CreateClientRequest;
import org.cloudfoundry.uaa.clients.DeleteClientRequest;
import org.cloudfoundry.uaa.clients.GetClientRequest;
import org.cloudfoundry.uaa.clients.GetClientResponse;
import org.springframework.stereotype.Service;
import org.cloudfoundry.uaa.tokens.GrantType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import org.springframework.stereotype.Service;
@Service
public class UaaService {
private static final Logger LOGGER = LoggerFactory.getLogger(UaaService.class);
private final UaaClient uaaClient;
public UaaService(UaaClient uaaClient) {
@@ -31,9 +40,30 @@ public class UaaService {
}
public Mono<GetClientResponse> getUaaClient(String clientId) {
return uaaClient.clients().get(GetClientRequest.builder()
return uaaClient.clients().get(GetClientRequest
.builder()
.clientId(clientId)
.build())
.onErrorResume(e -> Mono.empty());
}
public Mono<Void> createClient(String clientId, String clientSecret, String... authorities) {
return uaaClient.clients().delete(DeleteClientRequest
.builder()
.clientId(clientId)
.build())
.doOnError(error -> LOGGER.warn("Error deleting client: " + clientId + " with error: " + error))
.onErrorResume(e -> Mono.empty())
.then(uaaClient.clients().create(CreateClientRequest
.builder()
.clientId(clientId)
.clientSecret(clientSecret)
.authorizedGrantType(GrantType.CLIENT_CREDENTIALS)
.authorities(authorities)
.build())
.doOnError(error -> LOGGER.error("Error creating client: " + clientId + " with error: " + error))
.onErrorResume(e -> Mono.empty()))
.then();
}
}

View File

@@ -40,6 +40,7 @@ import org.cloudfoundry.client.v2.organizations.GetOrganizationResponse;
import org.cloudfoundry.client.v2.organizations.ListOrganizationSpacesRequest;
import org.cloudfoundry.client.v2.organizations.OrganizationEntity;
import org.cloudfoundry.client.v2.serviceinstances.ServiceInstanceEntity;
import org.cloudfoundry.client.v2.spaces.AssociateSpaceDeveloperRequest;
import org.cloudfoundry.client.v2.spaces.CreateSpaceRequest;
import org.cloudfoundry.client.v2.spaces.DeleteSpaceRequest;
import org.cloudfoundry.client.v2.spaces.SpaceEntity;
@@ -80,6 +81,8 @@ import org.cloudfoundry.operations.organizations.OrganizationInfoRequest;
import org.cloudfoundry.operations.services.BindServiceInstanceRequest;
import org.cloudfoundry.operations.services.ServiceInstance;
import org.cloudfoundry.operations.services.UnbindServiceInstanceRequest;
import org.cloudfoundry.operations.useradmin.SetSpaceRoleRequest;
import org.cloudfoundry.operations.useradmin.SpaceRole;
import org.cloudfoundry.util.DelayUtils;
import org.cloudfoundry.util.PaginationUtils;
import org.cloudfoundry.util.ResourceUtils;
@@ -435,26 +438,50 @@ public class CloudFoundryAppDeployer implements AppDeployer, ResourceLoaderAware
.flatMap(cfOperations -> cfOperations.applications().pushManifest(request));
}
private Mono<Void> createSpace(String spaceName) {
Mono<String> createSpacePublisher = getDefaultOrganizationId()
.flatMap(orgId -> this.client.spaces()
.create(CreateSpaceRequest.builder()
.organizationId(orgId)
.name(spaceName)
.build())
.doOnSuccess(response -> logger.info("Created space {}", spaceName))
.doOnError(e -> logger.warn(String.format("Error creating space %s: %s", spaceName, e.getMessage())))
.onErrorResume(e -> Mono.empty())
.then(Mono.empty()));
return getSpaceIdFromName(spaceName)
.switchIfEmpty(createSpacePublisher).then();
private Mono<String> createSpace(String spaceName) {
return getSpaceId(spaceName)
.switchIfEmpty(Mono.justOrEmpty(targetProperties.getDefaultOrg())
.flatMap(orgName -> getOrganizationId(orgName)
.flatMap(orgId -> client.spaces().create(CreateSpaceRequest.builder()
.organizationId(orgId)
.name(spaceName)
.build())
.doOnSuccess(response -> logger.info("Created space {}", spaceName))
.doOnError(e -> logger.warn(String.format("Error creating space %s: %s", spaceName, e.getMessage())))
.map(response -> response.getMetadata().getId())
.flatMap(spaceId -> setSpaceDeveloperRoleForCurrentUser(orgName, spaceName, spaceId)
.thenReturn(spaceId)))));
}
private Mono<String> getDefaultOrganizationId() {
return this.operations.organizations()
.get(OrganizationInfoRequest.builder()
.name(targetProperties.getDefaultOrg())
.build())
private Mono<Void> setSpaceDeveloperRoleForCurrentUser(String orgName, String spaceName, String spaceId) {
return Mono.defer(() -> {
if (StringUtils.hasText(targetProperties.getClientId())){
return client.spaces().associateDeveloper(AssociateSpaceDeveloperRequest.builder()
.spaceId(spaceId)
.developerId(targetProperties.getClientId())
.build())
.doOnSuccess(v -> logger.info("Set space developer role for space {}", spaceName))
.doOnError(e -> logger.warn(String.format("Error setting space developer role for space %s: %s", spaceName, e.getMessage())))
.then();
}
else if (StringUtils.hasText(targetProperties.getUsername())) {
return operations.userAdmin().setSpaceRole(SetSpaceRoleRequest.builder()
.spaceRole(SpaceRole.DEVELOPER)
.organizationName(orgName)
.spaceName(spaceName)
.username(targetProperties.getUsername())
.build())
.doOnSuccess(v -> logger.info("Set space developer role for space {}", spaceName))
.doOnError(e -> logger.warn(String.format("Error setting space developer role for space %s: %s", spaceName, e.getMessage())));
}
return Mono.empty();
});
}
private Mono<String> getOrganizationId(String orgName) {
return operations.organizations().get(OrganizationInfoRequest.builder()
.name(orgName)
.build())
.map(OrganizationDetail::getId);
}
@@ -492,7 +519,7 @@ public class CloudFoundryAppDeployer implements AppDeployer, ResourceLoaderAware
}
private Mono<Void> deleteApplicationInSpace(String name, String spaceName) {
return getSpaceIdFromName(spaceName)
return getSpaceId(spaceName)
.doOnError(error -> logger.warn("Unable to get space name: {} ", spaceName))
.then(operationsUtils.getOperationsForSpace(spaceName))
.flatMap(cfOperations -> cfOperations.applications().delete(DeleteApplicationRequest.builder()
@@ -504,7 +531,7 @@ public class CloudFoundryAppDeployer implements AppDeployer, ResourceLoaderAware
}
private Mono<Void> deleteSpace(String spaceName) {
return getSpaceIdFromName(spaceName)
return getSpaceId(spaceName)
.doOnError(error -> logger.warn("Unable to get space name: {} ", spaceName))
.flatMap(spaceId -> this.client.spaces()
.delete(DeleteSpaceRequest.builder()
@@ -513,17 +540,18 @@ public class CloudFoundryAppDeployer implements AppDeployer, ResourceLoaderAware
.then(Mono.empty()));
}
private Mono<String> getSpaceIdFromName(String spaceName) {
return getDefaultOrganizationId()
.flatMap(orgId -> PaginationUtils.requestClientV2Resources(page -> client.organizations()
.listSpaces(ListOrganizationSpacesRequest.builder()
.name(spaceName)
.organizationId(orgId)
.page(page)
.build()))
.filter(resource -> resource.getEntity().getName().equals(spaceName))
.map(resource -> resource.getMetadata().getId())
.next());
private Mono<String> getSpaceId(String spaceName) {
return Mono.justOrEmpty(targetProperties.getDefaultOrg())
.flatMap(orgName -> getOrganizationId(orgName)
.flatMap(orgId -> PaginationUtils.requestClientV2Resources(page -> client.organizations()
.listSpaces(ListOrganizationSpacesRequest.builder()
.name(spaceName)
.organizationId(orgId)
.page(page)
.build()))
.filter(resource -> resource.getEntity().getName().equals(spaceName))
.map(resource -> resource.getMetadata().getId())
.next()));
}
private Map<String, Object> getEnvironmentVariables(Map<String, String> properties,

View File

@@ -18,16 +18,17 @@ package org.springframework.cloud.appbroker.deployer.cloudfoundry;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
import org.cloudfoundry.client.CloudFoundryClient;
import org.cloudfoundry.client.v2.organizations.GetOrganizationRequest;
import org.cloudfoundry.client.v2.organizations.GetOrganizationResponse;
import org.cloudfoundry.client.v2.organizations.ListOrganizationSpacesRequest;
import org.cloudfoundry.client.v2.organizations.OrganizationEntity;
import org.cloudfoundry.client.v2.serviceinstances.GetServiceInstanceResponse;
import org.cloudfoundry.client.v2.serviceinstances.ServiceInstanceEntity;
import org.cloudfoundry.client.v2.serviceinstances.ServiceInstances;
import org.cloudfoundry.client.v2.spaces.CreateSpaceRequest;
import org.cloudfoundry.client.v2.spaces.GetSpaceRequest;
import org.cloudfoundry.client.v2.spaces.GetSpaceResponse;
import org.cloudfoundry.client.v2.spaces.SpaceEntity;
@@ -36,6 +37,10 @@ import org.cloudfoundry.operations.applications.ApplicationHealthCheck;
import org.cloudfoundry.operations.applications.ApplicationManifest;
import org.cloudfoundry.operations.applications.Applications;
import org.cloudfoundry.operations.applications.PushApplicationManifestRequest;
import org.cloudfoundry.operations.organizations.OrganizationDetail;
import org.cloudfoundry.operations.organizations.OrganizationInfoRequest;
import org.cloudfoundry.operations.organizations.OrganizationQuota;
import org.cloudfoundry.operations.organizations.Organizations;
import org.cloudfoundry.operations.services.BindServiceInstanceRequest;
import org.cloudfoundry.operations.services.GetServiceInstanceRequest;
import org.cloudfoundry.operations.services.ServiceInstance;
@@ -65,6 +70,7 @@ import org.springframework.core.io.ResourceLoader;
import org.springframework.util.CollectionUtils;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyMap;
@@ -74,6 +80,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.cloud.appbroker.deployer.DeploymentProperties.TARGET_PROPERTY_KEY;
@SuppressWarnings("UnassignedFluxMonoInstance")
@ExtendWith(MockitoExtension.class)
@@ -92,6 +99,9 @@ class CloudFoundryAppDeployerTest {
@Mock
private Services operationsServices;
@Mock
private Organizations operationsOrganizations;
@Mock
private Spaces operationsSpaces;
@@ -122,6 +132,8 @@ class CloudFoundryAppDeployerTest {
void setUp() {
deploymentProperties = new CloudFoundryDeploymentProperties();
CloudFoundryTargetProperties targetProperties = new CloudFoundryTargetProperties();
targetProperties.setDefaultOrg("default-org");
targetProperties.setDefaultSpace("default-space");
when(operationsApplications.pushManifest(any())).thenReturn(Mono.empty());
when(resourceLoader.getResource(APP_PATH)).thenReturn(new FileSystemResource(APP_PATH));
@@ -129,6 +141,7 @@ class CloudFoundryAppDeployerTest {
when(cloudFoundryOperations.spaces()).thenReturn(operationsSpaces);
when(cloudFoundryOperations.applications()).thenReturn(operationsApplications);
when(cloudFoundryOperations.services()).thenReturn(operationsServices);
when(cloudFoundryOperations.organizations()).thenReturn(operationsOrganizations);
when(cloudFoundryClient.serviceInstances()).thenReturn(clientServiceInstances);
when(cloudFoundryClient.spaces()).thenReturn(clientSpaces);
when(cloudFoundryClient.organizations()).thenReturn(clientOrganizations);
@@ -434,9 +447,79 @@ class CloudFoundryAppDeployerTest {
.verifyComplete();
}
@Test
void createServiceInstanceWithTarget() {
when(operationsOrganizations
.get(
OrganizationInfoRequest
.builder()
.name("default-org")
.build()))
.thenReturn(Mono.just(
OrganizationDetail
.builder()
.id("default-org-id")
.name("default-org")
.quota(OrganizationQuota
.builder()
.id("quota-id")
.instanceMemoryLimit(0)
.organizationId("default-org-id")
.name("quota")
.paidServicePlans(false)
.totalMemoryLimit(0)
.totalRoutes(0)
.totalServiceInstances(0)
.build())
.build()));
when(clientOrganizations
.listSpaces(ListOrganizationSpacesRequest
.builder()
.name("service-instance-id")
.organizationId("default-org-id")
.page(1)
.build()))
.thenReturn(Mono.empty());
when(clientSpaces
.create(CreateSpaceRequest
.builder()
.organizationId("default-org-id")
.name("service-instance-id")
.build()))
.thenReturn(Mono.empty());
when(operationsServices
.createInstance(
org.cloudfoundry.operations.services.CreateServiceInstanceRequest
.builder()
.serviceInstanceName("service-instance-name")
.serviceName("db-service")
.planName("standard")
.parameters(emptyMap())
.build()))
.thenReturn(Mono.empty());
CreateServiceInstanceRequest request =
CreateServiceInstanceRequest
.builder()
.serviceInstanceName("service-instance-name")
.name("db-service")
.plan("standard")
.parameters(emptyMap())
.properties(singletonMap(TARGET_PROPERTY_KEY, "service-instance-id"))
.build();
StepVerifier.create(
appDeployer.createServiceInstance(request))
.assertNext(response -> assertThat(response.getName()).isEqualTo("service-instance-name"))
.verifyComplete();
}
@Test
void updateServiceInstanceUpdatesWithParameters() {
Map<String, Object> parameters = Collections.singletonMap("param1", "value");
Map<String, Object> parameters = singletonMap("param1", "value");
when(operationsServices.updateInstance(
org.cloudfoundry.operations.services.UpdateServiceInstanceRequest.builder()
@@ -643,7 +726,7 @@ class CloudFoundryAppDeployerTest {
org.springframework.cloud.appbroker.deployer.GetServiceInstanceRequest request = org.springframework.cloud.appbroker.deployer.GetServiceInstanceRequest
.builder()
.name("my-foo-service")
.properties(Collections.singletonMap(DeploymentProperties.TARGET_PROPERTY_KEY, "foo-space"))
.properties(singletonMap(TARGET_PROPERTY_KEY, "foo-space"))
.build();
StepVerifier.create(appDeployer.getServiceInstance(request))
@@ -655,7 +738,7 @@ class CloudFoundryAppDeployerTest {
.verifyComplete();
verify(operationsUtils).getOperations(
argThat(argument -> "foo-space".equals(argument.get(DeploymentProperties.TARGET_PROPERTY_KEY))));
argThat(argument -> "foo-space".equals(argument.get(TARGET_PROPERTY_KEY))));
verify(cloudFoundryOperations).services();
verify(operationsServices).getInstance(argThat(req -> "my-foo-service".equals(req.getName())));
verifyZeroInteractions(cloudFoundryClient);

View File

@@ -63,8 +63,8 @@ class CreateInstanceWithSpacePerServiceInstanceTargetComponentTest extends Wirem
void pushAppWithServicesInSpace() {
String serviceInstanceId = "instance-id";
cloudControllerFixture.stubSpaceDoesNotExist(serviceInstanceId);
cloudControllerFixture.stubCreateSpace(serviceInstanceId);
cloudControllerFixture.stubAssociatePermissions(serviceInstanceId);
cloudControllerFixture.stubPushAppWithHost(APP_NAME, APP_NAME + "-" + serviceInstanceId);
// given services are available in the marketplace

View File

@@ -142,13 +142,6 @@ public class CloudControllerStubFixture extends WiremockStubFixture {
replace("@org-guid", TEST_ORG_GUID)))));
}
public void stubSpaceDoesNotExist(final String spaceName) {
stubFor(get(urlPathEqualTo("/v2/organizations/" + TEST_ORG_GUID + "/spaces"))
.withQueryParam("q", equalTo("name:" + spaceName))
.willReturn(ok()
.withBody(cc("empty-query-results"))));
}
public void stubCreateSpace(final String spaceName) {
stubFor(post(urlPathEqualTo("/v2/spaces"))
.withRequestBody(matchingJsonPath("$.[?(@.name == '" + spaceName + "')]"))
@@ -512,6 +505,26 @@ public class CloudControllerStubFixture extends WiremockStubFixture {
.withBody(cc("empty-query-results"))));
}
public void stubAssociatePermissions(final String spaceName) {
stubFor(get(urlPathEqualTo("/v2/config/feature_flags/set_roles_by_username"))
.willReturn(ok()
.withBody(cc("get-feature-flag-roles"))));
stubSpaceExists(spaceName);
stubFor(put(urlPathEqualTo("/v2/organizations/" + TEST_ORG_GUID + "/users"))
.willReturn(ok()));
}
private void stubSpaceExists(final String spaceName) {
stubFor(get(urlPathEqualTo("/v2/organizations/" + TEST_ORG_GUID + "/spaces"))
.withQueryParam("q", equalTo("name:" + spaceName))
.willReturn(ok()
.withBody(cc("list-spaces",
replace("@org-guid", TEST_ORG_GUID),
replace("@space-guid", TEST_SPACE_GUID),
replace("@name", spaceName)))));
}
private String cc(String fileRoot, StringReplacementPair... replacements) {
String response = readResponseFromFile(fileRoot, "cloudcontroller");
for (StringReplacementPair pair : replacements) {

View File

@@ -0,0 +1,6 @@
{
"name": "set_roles_by_username",
"enabled": true,
"error_message": null,
"url": "/v2/config/feature_flags/set_roles_by_username"
}