diff --git a/spring-cloud-app-broker-acceptance-tests/README.adoc b/spring-cloud-app-broker-acceptance-tests/README.adoc index 29d0370..b3f3934 100644 --- a/spring-cloud-app-broker-acceptance-tests/README.adoc +++ b/spring-cloud-app-broker-acceptance-tests/README.adoc @@ -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. diff --git a/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/CloudFoundryAcceptanceTest.java b/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/CloudFoundryAcceptanceTest.java index c7dcd52..a728759 100644 --- a/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/CloudFoundryAcceptanceTest.java +++ b/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/CloudFoundryAcceptanceTest.java @@ -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 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 cleanup() { + private Mono 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) { diff --git a/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/fixtures/cf/CloudFoundryClientConfiguration.java b/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/fixtures/cf/CloudFoundryClientConfiguration.java index db846da..ff8ec25 100644 --- a/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/fixtures/cf/CloudFoundryClientConfiguration.java +++ b/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/fixtures/cf/CloudFoundryClientConfiguration.java @@ -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(); - } - } diff --git a/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/fixtures/cf/CloudFoundryService.java b/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/fixtures/cf/CloudFoundryService.java index dbc203d..a317c4f 100644 --- a/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/fixtures/cf/CloudFoundryService.java +++ b/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/fixtures/cf/CloudFoundryService.java @@ -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 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 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 associateOrgUser(String orgId, String userId) { + return cloudFoundryClient.organizations().associateUser(AssociateOrganizationUserRequest.builder() + .organizationId(orgId) + .userId(userId) + .build()); + } + + private Mono associateOrgManager(String orgId, String userId) { + return cloudFoundryClient.organizations().associateManager(AssociateOrganizationManagerRequest.builder() + .organizationId(orgId) + .managerId(userId) + .build()); + } + + private Mono associateSpaceDeveloper(String spaceId, String userId) { + return cloudFoundryClient.spaces().associateDeveloper(AssociateSpaceDeveloperRequest.builder() + .spaceId(spaceId) + .developerId(userId) + .build()); + } + + private Mono removeOrgUser(String orgId, String userId) { + return cloudFoundryClient.organizations().removeUser(RemoveOrganizationUserRequest.builder() + .organizationId(orgId) + .userId(userId) + .build()); + } + + private Mono removeOrgManager(String orgId, String userId) { + return cloudFoundryClient.organizations().removeManager(RemoveOrganizationManagerRequest.builder() + .organizationId(orgId) + .managerId(userId) + .build()); + } + + private Mono 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; } diff --git a/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/fixtures/uaa/UaaService.java b/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/fixtures/uaa/UaaService.java index 6e76370..e33a392 100644 --- a/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/fixtures/uaa/UaaService.java +++ b/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/fixtures/uaa/UaaService.java @@ -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 getUaaClient(String clientId) { - return uaaClient.clients().get(GetClientRequest.builder() + return uaaClient.clients().get(GetClientRequest + .builder() .clientId(clientId) .build()) .onErrorResume(e -> Mono.empty()); } + + public Mono 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(); + } + } diff --git a/spring-cloud-app-broker-deployer-cloudfoundry/src/main/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployer.java b/spring-cloud-app-broker-deployer-cloudfoundry/src/main/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployer.java index 1f4481d..faa127c 100644 --- a/spring-cloud-app-broker-deployer-cloudfoundry/src/main/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployer.java +++ b/spring-cloud-app-broker-deployer-cloudfoundry/src/main/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployer.java @@ -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 createSpace(String spaceName) { - Mono 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 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 getDefaultOrganizationId() { - return this.operations.organizations() - .get(OrganizationInfoRequest.builder() - .name(targetProperties.getDefaultOrg()) - .build()) + private Mono 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 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 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 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 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 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 getEnvironmentVariables(Map properties, diff --git a/spring-cloud-app-broker-deployer-cloudfoundry/src/test/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployerTest.java b/spring-cloud-app-broker-deployer-cloudfoundry/src/test/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployerTest.java index 77bbdb8..d827d27 100644 --- a/spring-cloud-app-broker-deployer-cloudfoundry/src/test/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployerTest.java +++ b/spring-cloud-app-broker-deployer-cloudfoundry/src/test/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployerTest.java @@ -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 parameters = Collections.singletonMap("param1", "value"); + Map 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); diff --git a/spring-cloud-app-broker-integration-tests/src/test/java/org.springframework.cloud.appbroker/integration/CreateInstanceWithSpacePerServiceInstanceTargetComponentTest.java b/spring-cloud-app-broker-integration-tests/src/test/java/org.springframework.cloud.appbroker/integration/CreateInstanceWithSpacePerServiceInstanceTargetComponentTest.java index 838b634..095a3ce 100644 --- a/spring-cloud-app-broker-integration-tests/src/test/java/org.springframework.cloud.appbroker/integration/CreateInstanceWithSpacePerServiceInstanceTargetComponentTest.java +++ b/spring-cloud-app-broker-integration-tests/src/test/java/org.springframework.cloud.appbroker/integration/CreateInstanceWithSpacePerServiceInstanceTargetComponentTest.java @@ -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 diff --git a/spring-cloud-app-broker-integration-tests/src/test/java/org.springframework.cloud.appbroker/integration/fixtures/CloudControllerStubFixture.java b/spring-cloud-app-broker-integration-tests/src/test/java/org.springframework.cloud.appbroker/integration/fixtures/CloudControllerStubFixture.java index 0a9c8ee..e9e9714 100644 --- a/spring-cloud-app-broker-integration-tests/src/test/java/org.springframework.cloud.appbroker/integration/fixtures/CloudControllerStubFixture.java +++ b/spring-cloud-app-broker-integration-tests/src/test/java/org.springframework.cloud.appbroker/integration/fixtures/CloudControllerStubFixture.java @@ -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) { diff --git a/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/get-feature-flag-roles.json b/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/get-feature-flag-roles.json new file mode 100644 index 0000000..d01fede --- /dev/null +++ b/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/get-feature-flag-roles.json @@ -0,0 +1,6 @@ +{ + "name": "set_roles_by_username", + "enabled": true, + "error_message": null, + "url": "/v2/config/feature_flags/set_roles_by_username" +} \ No newline at end of file