diff --git a/build.gradle b/build.gradle index 92e48fea..22190ac2 100644 --- a/build.gradle +++ b/build.gradle @@ -42,10 +42,10 @@ configure(allprojects) { apply plugin: "maven-publish" ext { - springBootVersion = project.findProperty("springBootVersion") ?: "2.1.1.RELEASE" - springVersion = project.findProperty("springVersion") ?: "5.1.3.RELEASE" - reactorVersion = project.findProperty("reactorVersion") ?: "Californium-SR3" - openServiceBrokerVersion = "3.0.0.M4" + springBootVersion = project.findProperty("springBootVersion") ?: "2.1.3.RELEASE" + springVersion = project.findProperty("springVersion") ?: "5.1.5.RELEASE" + reactorVersion = project.findProperty("reactorVersion") ?: "Californium-SR5" + openServiceBrokerVersion = "3.0.0.BUILD-SNAPSHOT" springCredhubVersion = "2.0.0.BUILD-SNAPSHOT" cfJavaClientVersion = "3.15.0.RELEASE" mockitoVersion = "2.23.4" diff --git a/spring-cloud-app-broker-acceptance-tests/README.adoc b/spring-cloud-app-broker-acceptance-tests/README.adoc index 293b467f..29d03703 100644 --- a/spring-cloud-app-broker-acceptance-tests/README.adoc +++ b/spring-cloud-app-broker-acceptance-tests/README.adoc @@ -12,6 +12,7 @@ The tests require the following properties to be set: * `spring.cloud.appbroker.acceptance-test.cloudfoundry.api-host` - The CF API host where the tests are going to run. * `spring.cloud.appbroker.acceptance-test.cloudfoundry.api-port` - The CF API port where the tests are going to run. +* `spring.cloud.appbroker.acceptance-test.cloudfoundry.apps-domain` - The CF apps domain where the tests are going to run. * `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. diff --git a/spring-cloud-app-broker-acceptance-tests/build.gradle b/spring-cloud-app-broker-acceptance-tests/build.gradle index a99cc1c0..6b083f41 100644 --- a/spring-cloud-app-broker-acceptance-tests/build.gradle +++ b/spring-cloud-app-broker-acceptance-tests/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright 2016-2018. the original author or authors. + * Copyright 2016-2019. 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. @@ -43,6 +43,7 @@ dependencies { testImplementation("com.revinate:assertj-json:1.2.0") testImplementation("org.springframework.boot:spring-boot-starter-test") + testCompile("io.projectreactor:reactor-test") } // build the test broker from /src into a jar that the tests can deploy diff --git a/spring-cloud-app-broker-acceptance-tests/src/main/java/org/springframework/cloud/appbroker/acceptance/ManagementController.java b/spring-cloud-app-broker-acceptance-tests/src/main/java/org/springframework/cloud/appbroker/acceptance/ManagementController.java new file mode 100644 index 00000000..65054e89 --- /dev/null +++ b/spring-cloud-app-broker-acceptance-tests/src/main/java/org/springframework/cloud/appbroker/acceptance/ManagementController.java @@ -0,0 +1,58 @@ +/* + * Copyright 2016-2019 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.springframework.cloud.appbroker.acceptance; + +import reactor.core.publisher.Mono; + +import org.springframework.cloud.appbroker.manager.BackingAppManagementService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class ManagementController { + + private final BackingAppManagementService service; + + public ManagementController(BackingAppManagementService service) { + this.service = service; + } + + @GetMapping("/start/{serviceInstanceId}") + public Mono startApplications(@PathVariable String serviceInstanceId) { + return service.start(serviceInstanceId) + .thenReturn("starting " + serviceInstanceId); + } + + @GetMapping("/stop/{serviceInstanceId}") + public Mono stopApplications(@PathVariable String serviceInstanceId) { + return service.stop(serviceInstanceId) + .thenReturn("stopping " + serviceInstanceId); + } + + @GetMapping("/restart/{serviceInstanceId}") + public Mono restartApplications(@PathVariable String serviceInstanceId) { + return service.restart(serviceInstanceId) + .thenReturn("restarting " + serviceInstanceId); + } + + @GetMapping("/restage/{serviceInstanceId}") + public Mono restageApplications(@PathVariable String serviceInstanceId) { + return service.restage(serviceInstanceId) + .thenReturn("restaging " + serviceInstanceId); + } +} diff --git a/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/AppManagementAcceptanceTest.java b/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/AppManagementAcceptanceTest.java new file mode 100644 index 00000000..940a8d5c --- /dev/null +++ b/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/AppManagementAcceptanceTest.java @@ -0,0 +1,211 @@ +/* + * Copyright 2016-2019 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.springframework.cloud.appbroker.acceptance; + +import javax.net.ssl.SSLException; +import java.net.URI; +import java.util.Date; +import java.util.List; + +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import org.cloudfoundry.operations.applications.ApplicationDetail; +import org.cloudfoundry.operations.services.ServiceInstance; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import reactor.netty.http.client.HttpClient; +import reactor.test.StepVerifier; + +import org.springframework.http.HttpEntity; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.assertj.core.api.Assertions.assertThat; + +class AppManagementAcceptanceTest extends CloudFoundryAcceptanceTest { + + private static final String APP_1 = "app-1"; + + private static final String APP_2 = "app-2"; + + private static final String SI_NAME = "si-managed"; + + private final WebClient webClient = getSslIgnoringWebClient(); + + @BeforeEach + void setUp() { + StepVerifier.create(cloudFoundryService.deleteServiceInstance(SI_NAME)) + .verifyComplete(); + + StepVerifier.create(cloudFoundryService.createServiceInstance(PLAN_NAME, APP_SERVICE_NAME, SI_NAME, null)) + .verifyComplete(); + + StepVerifier.create(cloudFoundryService.getServiceInstance(SI_NAME)) + .assertNext(serviceInstance -> assertThat(serviceInstance.getStatus()).isEqualTo("succeeded")) + .verifyComplete(); + } + + @AfterEach + void cleanUp() { + StepVerifier.create(cloudFoundryService.deleteServiceInstance(SI_NAME)) + .verifyComplete(); + + StepVerifier.create(getApplications()) + .verifyError(); + } + + @Test + @AppBrokerTestProperties({ + "spring.cloud.appbroker.services[0].service-name=" + APP_SERVICE_NAME, + "spring.cloud.appbroker.services[0].plan-name=" + PLAN_NAME, + "spring.cloud.appbroker.services[0].apps[0].name=" + APP_1, + "spring.cloud.appbroker.services[0].apps[0].path=" + BACKING_APP_PATH, + "spring.cloud.appbroker.services[0].apps[1].name=" + APP_2, + "spring.cloud.appbroker.services[0].apps[1].path=" + BACKING_APP_PATH + }) + void stopApps() { + StepVerifier.create(manageApps("stop")) + .assertNext(result -> assertThat(result).contains("stopping")) + .verifyComplete(); + + StepVerifier.create(getApplications()) + .assertNext(apps -> assertThat(apps).extracting("runningInstances").containsOnly(0)) + .verifyComplete(); + } + + @Test + @AppBrokerTestProperties({ + "spring.cloud.appbroker.services[0].service-name=" + APP_SERVICE_NAME, + "spring.cloud.appbroker.services[0].plan-name=" + PLAN_NAME, + "spring.cloud.appbroker.services[0].apps[0].name=" + APP_1, + "spring.cloud.appbroker.services[0].apps[0].path=" + BACKING_APP_PATH, + "spring.cloud.appbroker.services[0].apps[1].name=" + APP_2, + "spring.cloud.appbroker.services[0].apps[1].path=" + BACKING_APP_PATH + }) + void startApps() { + StepVerifier.create(cloudFoundryService.stopApplication(APP_1) + .then(cloudFoundryService.stopApplication(APP_2))) + .verifyComplete(); + + StepVerifier.create(getApplications()) + .assertNext(apps -> assertThat(apps).extracting("runningInstances").containsOnly(0)) + .verifyComplete(); + + StepVerifier.create(manageApps("start")) + .assertNext(result -> assertThat(result).contains("starting")) + .verifyComplete(); + + StepVerifier.create(getApplications()) + .assertNext(apps -> assertThat(apps).extracting("runningInstances").containsOnly(1)) + .verifyComplete(); + } + + @Test + @AppBrokerTestProperties({ + "spring.cloud.appbroker.services[0].service-name=" + APP_SERVICE_NAME, + "spring.cloud.appbroker.services[0].plan-name=" + PLAN_NAME, + "spring.cloud.appbroker.services[0].apps[0].name=" + APP_1, + "spring.cloud.appbroker.services[0].apps[0].path=" + BACKING_APP_PATH, + "spring.cloud.appbroker.services[0].apps[1].name=" + APP_2, + "spring.cloud.appbroker.services[0].apps[1].path=" + BACKING_APP_PATH + }) + void restartApps() { + List apps = getApplications().block(); + Date originallySince1 = apps.get(0).getInstanceDetails().get(0).getSince(); + Date originallySince2 = apps.get(1).getInstanceDetails().get(0).getSince(); + + StepVerifier.create(manageApps("restart")) + .assertNext(result -> assertThat(result).contains("restarting")) + .verifyComplete(); + + List restagedApps = getApplications().block(); + Date since1 = restagedApps.get(0).getInstanceDetails().get(0).getSince(); + Date since2 = restagedApps.get(1).getInstanceDetails().get(0).getSince(); + assertThat(restagedApps).extracting("runningInstances").containsOnly(1); + assertThat(since1).isAfter(originallySince1); + assertThat(since2).isAfter(originallySince2); + } + + @Test + @AppBrokerTestProperties({ + "spring.cloud.appbroker.services[0].service-name=" + APP_SERVICE_NAME, + "spring.cloud.appbroker.services[0].plan-name=" + PLAN_NAME, + "spring.cloud.appbroker.services[0].apps[0].name=" + APP_1, + "spring.cloud.appbroker.services[0].apps[0].path=" + BACKING_APP_PATH, + "spring.cloud.appbroker.services[0].apps[1].name=" + APP_2, + "spring.cloud.appbroker.services[0].apps[1].path=" + BACKING_APP_PATH + }) + void restageApps() throws Exception { + List apps = getApplications().block(); + Date originallySince1 = apps.get(0).getInstanceDetails().get(0).getSince(); + Date originallySince2 = apps.get(1).getInstanceDetails().get(0).getSince(); + assertThat(apps).extracting("runningInstances").containsOnly(1); + + StepVerifier.create(manageApps("restage")) + .assertNext(result -> assertThat(result).contains("restaging")) + .verifyComplete(); + + List restagedApps = getApplications().block(); + Date since1 = restagedApps.get(0).getInstanceDetails().get(0).getSince(); + Date since2 = restagedApps.get(1).getInstanceDetails().get(0).getSince(); + assertThat(restagedApps).extracting("runningInstances").containsOnly(1); + assertThat(since1).isAfter(originallySince1); + assertThat(since2).isAfter(originallySince2); + } + + private Mono> getApplications() { + return Flux.merge(cloudFoundryService.getApplication(APP_1), + cloudFoundryService.getApplication(APP_2)) + .parallel() + .runOn(Schedulers.parallel()) + .sequential() + .collectList(); + } + + private Mono manageApps(String operation) { + return cloudFoundryService.getServiceInstance(SI_NAME) + .map(ServiceInstance::getId) + .flatMap(serviceInstanceId -> cloudFoundryService.getApplicationRoute(TEST_BROKER_APP_NAME) + .flatMap(appRoute -> webClient.get() + .uri(URI.create(appRoute + "/" + operation + "/" + serviceInstanceId)) + .exchange() + .flatMap(clientResponse -> clientResponse.toEntity(String.class)) + .map(HttpEntity::getBody))); + } + + private WebClient getSslIgnoringWebClient() { + return WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(HttpClient + .create() + .secure(t -> { + try { + t.sslContext(SslContextBuilder + .forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .build()); + } + catch (SSLException e) { + e.printStackTrace(); + } + }))) + .build(); + } +} \ No newline at end of file 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 a0f6a95d..3a75fafa 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 @@ -67,7 +67,7 @@ @EnableConfigurationProperties(AcceptanceTestProperties.class) class CloudFoundryAcceptanceTest { - private static final String TEST_BROKER_APP_NAME = "test-broker-app"; + static final String TEST_BROKER_APP_NAME = "test-broker-app"; private static final String SERVICE_BROKER_NAME = "test-broker"; static final String APP_SERVICE_NAME = "app-service"; @@ -76,7 +76,7 @@ class CloudFoundryAcceptanceTest { static final String BACKING_APP_PATH = "classpath:backing-app.jar"; @Autowired - private CloudFoundryService cloudFoundryService; + protected CloudFoundryService cloudFoundryService; @Autowired private UaaService uaaService; @@ -201,7 +201,7 @@ Optional getApplicationSummary(String appName) { } Optional getApplicationSummary(String appName, String space) { - return cloudFoundryService.getApplicationSummary(appName, space).blockOptional(); + return cloudFoundryService.getApplication(appName, space).blockOptional(); } ApplicationEnvironments getApplicationEnvironment(String appName) { @@ -212,6 +212,10 @@ ApplicationEnvironments getApplicationEnvironment(String appName, String space) return cloudFoundryService.getApplicationEnvironment(appName, space).block(); } + String getTestBrokerAppRoute() { + return cloudFoundryService.getApplicationRoute(TEST_BROKER_APP_NAME).block(); + } + DocumentContext getSpringAppJson(String appName) { ApplicationEnvironments env = getApplicationEnvironment(appName); String saj = (String) env.getUserProvided().get("SPRING_APPLICATION_JSON"); 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 de0bd729..f8b485a4 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 @@ -32,6 +32,7 @@ import org.cloudfoundry.operations.applications.GetApplicationEnvironmentsRequest; import org.cloudfoundry.operations.applications.GetApplicationRequest; import org.cloudfoundry.operations.applications.PushApplicationManifestRequest; +import org.cloudfoundry.operations.applications.StopApplicationRequest; import org.cloudfoundry.operations.organizations.CreateOrganizationRequest; import org.cloudfoundry.operations.organizations.OrganizationSummary; import org.cloudfoundry.operations.organizations.Organizations; @@ -93,7 +94,7 @@ public Mono createServiceBroker(String brokerName, String testBrokerAppNam .doOnError(error -> LOGGER.error("Error creating service broker " + brokerName + ": " + error))); } - private Mono getApplicationRoute(String appName) { + public Mono getApplicationRoute(String appName) { return cloudFoundryOperations.applications() .get(GetApplicationRequest.builder() .name(appName) @@ -203,7 +204,13 @@ public Mono> getApplications() { .collectList(); } - public Mono getApplicationSummary(String appName, String space) { + public Mono getApplication(String appName) { + return cloudFoundryOperations.applications().get(GetApplicationRequest.builder() + .name(appName) + .build()); + } + + public Mono getApplication(String appName, String space) { return listApplications(createOperationsForSpace(space)) .filter(applicationSummary -> applicationSummary.getName().equals(appName)) .single(); @@ -233,6 +240,12 @@ private Mono getApplicationEnvironment(CloudFoundryOper .doOnError(error -> LOGGER.error("Error getting environment for application " + appName + ": " + error)); } + public Mono stopApplication(String appName) { + return cloudFoundryOperations.applications().stop(StopApplicationRequest.builder() + .name(appName) + .build()); + } + public Mono> getSpaces() { return cloudFoundryOperations.spaces() .list() diff --git a/spring-cloud-app-broker-autoconfigure/src/main/java/org/springframework/cloud/appbroker/autoconfigure/AppBrokerAutoConfiguration.java b/spring-cloud-app-broker-autoconfigure/src/main/java/org/springframework/cloud/appbroker/autoconfigure/AppBrokerAutoConfiguration.java index a1455821..739cf298 100644 --- a/spring-cloud-app-broker-autoconfigure/src/main/java/org/springframework/cloud/appbroker/autoconfigure/AppBrokerAutoConfiguration.java +++ b/spring-cloud-app-broker-autoconfigure/src/main/java/org/springframework/cloud/appbroker/autoconfigure/AppBrokerAutoConfiguration.java @@ -46,6 +46,9 @@ import org.springframework.cloud.appbroker.extensions.targets.SpacePerServiceInstance; import org.springframework.cloud.appbroker.extensions.targets.TargetFactory; import org.springframework.cloud.appbroker.extensions.targets.TargetService; +import org.springframework.cloud.appbroker.manager.AppManager; +import org.springframework.cloud.appbroker.manager.BackingAppManagementService; +import org.springframework.cloud.appbroker.manager.ManagementClient; import org.springframework.cloud.appbroker.oauth2.OAuth2Client; import org.springframework.cloud.appbroker.service.CreateServiceInstanceAppBindingWorkflow; import org.springframework.cloud.appbroker.service.CreateServiceInstanceRouteBindingWorkflow; @@ -73,14 +76,25 @@ public class AppBrokerAutoConfiguration { private static final String PROPERTY_PREFIX = "spring.cloud.appbroker"; + @Bean + public DeployerClient deployerClient(AppDeployer appDeployer) { + return new DeployerClient(appDeployer); + } + @Bean public BackingAppDeploymentService backingAppDeploymentService(DeployerClient deployerClient) { return new BackingAppDeploymentService(deployerClient); } @Bean - public DeployerClient deployerClient(AppDeployer appDeployer) { - return new DeployerClient(appDeployer); + public ManagementClient managementClient(AppManager appManager) { + return new ManagementClient(appManager); + } + + @Bean + public BackingAppManagementService backingAppManagementService(ManagementClient managementClient, + AppDeployer appDeployer, BrokeredServices brokeredServices, TargetService targetService) { + return new BackingAppManagementService(managementClient, appDeployer, brokeredServices, targetService); } @Bean diff --git a/spring-cloud-app-broker-autoconfigure/src/main/java/org/springframework/cloud/appbroker/autoconfigure/CloudFoundryAppDeployerAutoConfiguration.java b/spring-cloud-app-broker-autoconfigure/src/main/java/org/springframework/cloud/appbroker/autoconfigure/CloudFoundryAppDeployerAutoConfiguration.java index b375b752..2807b121 100644 --- a/spring-cloud-app-broker-autoconfigure/src/main/java/org/springframework/cloud/appbroker/autoconfigure/CloudFoundryAppDeployerAutoConfiguration.java +++ b/spring-cloud-app-broker-autoconfigure/src/main/java/org/springframework/cloud/appbroker/autoconfigure/CloudFoundryAppDeployerAutoConfiguration.java @@ -16,9 +16,15 @@ package org.springframework.cloud.appbroker.autoconfigure; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Optional; +import java.util.stream.Stream; + import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.doppler.DopplerClient; - import org.cloudfoundry.operations.CloudFoundryOperations; import org.cloudfoundry.operations.DefaultCloudFoundryOperations; import org.cloudfoundry.reactor.ConnectionContext; @@ -36,23 +42,19 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.appbroker.deployer.AppDeployer; -import org.springframework.cloud.appbroker.oauth2.OAuth2Client; +import org.springframework.cloud.appbroker.deployer.cloudfoundry.CloudFoundryAppManager; +import org.springframework.cloud.appbroker.deployer.cloudfoundry.CloudFoundryOperationsUtils; import org.springframework.cloud.appbroker.deployer.cloudfoundry.CloudFoundryAppDeployer; import org.springframework.cloud.appbroker.deployer.cloudfoundry.CloudFoundryDeploymentProperties; import org.springframework.cloud.appbroker.deployer.cloudfoundry.CloudFoundryOAuth2Client; import org.springframework.cloud.appbroker.deployer.cloudfoundry.CloudFoundryTargetProperties; +import org.springframework.cloud.appbroker.manager.AppManager; +import org.springframework.cloud.appbroker.oauth2.OAuth2Client; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ResourceLoader; import org.springframework.util.StringUtils; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import java.util.Optional; -import java.util.stream.Stream; - @Configuration @ConditionalOnProperty(CloudFoundryAppDeployerAutoConfiguration.PROPERTY_PREFIX + ".api-host") @EnableConfigurationProperties @@ -75,10 +77,16 @@ CloudFoundryTargetProperties cloudFoundryTargetProperties() { AppDeployer cloudFoundryAppDeployer(CloudFoundryDeploymentProperties deploymentProperties, CloudFoundryOperations cloudFoundryOperations, CloudFoundryClient cloudFoundryClient, + CloudFoundryOperationsUtils operationsUtils, CloudFoundryTargetProperties targetProperties, ResourceLoader resourceLoader) { return new CloudFoundryAppDeployer(deploymentProperties, cloudFoundryOperations, cloudFoundryClient, - targetProperties, resourceLoader); + operationsUtils, targetProperties, resourceLoader); + } + + @Bean + AppManager cloudFoundryAppManager(CloudFoundryOperationsUtils cloudFoundryOperationsUtils) { + return new CloudFoundryAppManager(cloudFoundryOperationsUtils); } @Bean @@ -109,6 +117,11 @@ CloudFoundryOperations cloudFoundryOperations(CloudFoundryTargetProperties prope .build(); } + @Bean + CloudFoundryOperationsUtils cloudFoundryOperationsUtils(CloudFoundryOperations operations) { + return new CloudFoundryOperationsUtils(operations); + } + @Bean DefaultConnectionContext connectionContext(CloudFoundryTargetProperties properties) { return DefaultConnectionContext.builder() diff --git a/spring-cloud-app-broker-autoconfigure/src/test/java/org/springframework/cloud/appbroker/autoconfigure/AppBrokerAutoConfigurationTest.java b/spring-cloud-app-broker-autoconfigure/src/test/java/org/springframework/cloud/appbroker/autoconfigure/AppBrokerAutoConfigurationTest.java index 8ded22d1..d23c4a24 100644 --- a/spring-cloud-app-broker-autoconfigure/src/test/java/org/springframework/cloud/appbroker/autoconfigure/AppBrokerAutoConfigurationTest.java +++ b/spring-cloud-app-broker-autoconfigure/src/test/java/org/springframework/cloud/appbroker/autoconfigure/AppBrokerAutoConfigurationTest.java @@ -41,6 +41,8 @@ import org.springframework.cloud.appbroker.extensions.targets.ServiceInstanceGuidSuffix; import org.springframework.cloud.appbroker.extensions.targets.SpacePerServiceInstance; import org.springframework.cloud.appbroker.extensions.targets.TargetService; +import org.springframework.cloud.appbroker.manager.BackingAppManagementService; +import org.springframework.cloud.appbroker.manager.ManagementClient; import org.springframework.cloud.appbroker.service.CreateServiceInstanceAppBindingWorkflow; import org.springframework.cloud.appbroker.service.CreateServiceInstanceRouteBindingWorkflow; import org.springframework.cloud.appbroker.service.DeleteServiceInstanceBindingWorkflow; @@ -164,12 +166,14 @@ private ApplicationContextRunner configuredContext() { private void assertBeansCreated(AssertableApplicationContext context) { assertThat(context).hasSingleBean(DeployerClient.class); + assertThat(context).hasSingleBean(ManagementClient.class); assertThat(context).hasSingleBean(BrokeredServices.class); assertThat(context).hasSingleBean(ServiceInstanceStateRepository.class); assertThat(context).hasSingleBean(ServiceInstanceBindingStateRepository.class); assertThat(context).hasSingleBean(BackingAppDeploymentService.class); + assertThat(context).hasSingleBean(BackingAppManagementService.class); assertThat(context).hasSingleBean(BackingServicesProvisionService.class); assertThat(context).hasSingleBean(BackingApplicationsParametersTransformationService.class); diff --git a/spring-cloud-app-broker-autoconfigure/src/test/java/org/springframework/cloud/appbroker/autoconfigure/CloudFoundryAppDeployerAutoConfigurationTest.java b/spring-cloud-app-broker-autoconfigure/src/test/java/org/springframework/cloud/appbroker/autoconfigure/CloudFoundryAppDeployerAutoConfigurationTest.java index 0a9a76fa..274c5b8b 100644 --- a/spring-cloud-app-broker-autoconfigure/src/test/java/org/springframework/cloud/appbroker/autoconfigure/CloudFoundryAppDeployerAutoConfigurationTest.java +++ b/spring-cloud-app-broker-autoconfigure/src/test/java/org/springframework/cloud/appbroker/autoconfigure/CloudFoundryAppDeployerAutoConfigurationTest.java @@ -31,7 +31,9 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.appbroker.deployer.AppDeployer; import org.springframework.cloud.appbroker.deployer.cloudfoundry.CloudFoundryDeploymentProperties; +import org.springframework.cloud.appbroker.deployer.cloudfoundry.CloudFoundryOperationsUtils; import org.springframework.cloud.appbroker.deployer.cloudfoundry.CloudFoundryTargetProperties; +import org.springframework.cloud.appbroker.manager.AppManager; import org.springframework.cloud.appbroker.oauth2.OAuth2Client; import static org.assertj.core.api.Assertions.assertThat; @@ -74,12 +76,14 @@ void clientIsCreatedWithPasswordGrantConfiguration() { assertThat(deploymentProperties.getDomain()).isEqualTo("example.com"); assertThat(context).hasSingleBean(AppDeployer.class); + assertThat(context).hasSingleBean(AppManager.class); assertThat(context).hasSingleBean(OAuth2Client.class); assertThat(context).hasSingleBean(ReactorCloudFoundryClient.class); assertThat(context).hasSingleBean(ReactorDopplerClient.class); assertThat(context).hasSingleBean(ReactorUaaClient.class); assertThat(context).hasSingleBean(CloudFoundryOperations.class); + assertThat(context).hasSingleBean(CloudFoundryOperationsUtils.class); assertThat(context).hasSingleBean(DefaultConnectionContext.class); assertThat(context).hasSingleBean(PasswordGrantTokenProvider.class); }); @@ -107,11 +111,13 @@ void clientIsCreatedWithCredentialsGrantConfiguration() { assertThat(targetProperties.getClientSecret()).isEqualTo("secret"); assertThat(context).hasSingleBean(AppDeployer.class); + assertThat(context).hasSingleBean(AppManager.class); assertThat(context).hasSingleBean(ReactorCloudFoundryClient.class); assertThat(context).hasSingleBean(ReactorDopplerClient.class); assertThat(context).hasSingleBean(ReactorUaaClient.class); assertThat(context).hasSingleBean(CloudFoundryOperations.class); + assertThat(context).hasSingleBean(CloudFoundryOperationsUtils.class); assertThat(context).hasSingleBean(DefaultConnectionContext.class); assertThat(context).hasSingleBean(ClientCredentialsGrantTokenProvider.class); }); @@ -127,6 +133,7 @@ void clientIsNotCreatedWithoutConfiguration() { assertThat(context).doesNotHaveBean(ReactorDopplerClient.class); assertThat(context).doesNotHaveBean(ReactorUaaClient.class); assertThat(context).doesNotHaveBean(CloudFoundryOperations.class); + assertThat(context).doesNotHaveBean(CloudFoundryOperationsUtils.class); assertThat(context).doesNotHaveBean(ConnectionContext.class); assertThat(context).doesNotHaveBean(TokenProvider.class); }); diff --git a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/BackingApplications.java b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/BackingApplications.java index fd033a67..c17f75e3 100644 --- a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/BackingApplications.java +++ b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/BackingApplications.java @@ -19,6 +19,8 @@ import java.util.ArrayList; import java.util.List; +import org.springframework.util.CollectionUtils; + public class BackingApplications extends ArrayList { private static final long serialVersionUID = 159473836238657105L; @@ -46,6 +48,13 @@ public BackingApplicationsBuilder backingApplication(BackingApplication backingA return this; } + public BackingApplicationsBuilder backingApplications(List backingApplications) { + if (!CollectionUtils.isEmpty(backingApplications)) { + this.backingApplications.addAll(backingApplications); + } + return this; + } + public BackingApplications build() { return new BackingApplications(backingApplications); } diff --git a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/manager/BackingAppManagementService.java b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/manager/BackingAppManagementService.java new file mode 100644 index 00000000..e40e347c --- /dev/null +++ b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/manager/BackingAppManagementService.java @@ -0,0 +1,135 @@ +/* + * Copyright 2016-2019 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.springframework.cloud.appbroker.manager; + +import java.util.List; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import reactor.util.Logger; +import reactor.util.Loggers; + +import org.springframework.cloud.appbroker.deployer.AppDeployer; +import org.springframework.cloud.appbroker.deployer.BackingApplication; +import org.springframework.cloud.appbroker.deployer.BackingApplications; +import org.springframework.cloud.appbroker.deployer.BrokeredService; +import org.springframework.cloud.appbroker.deployer.BrokeredServices; +import org.springframework.cloud.appbroker.deployer.GetServiceInstanceRequest; +import org.springframework.cloud.appbroker.extensions.targets.TargetService; + +public class BackingAppManagementService { + + private final Logger log = Loggers.getLogger(BackingAppManagementService.class); + + private final ManagementClient managementClient; + + private final AppDeployer appDeployer; + + private final BrokeredServices brokeredServices; + + private final TargetService targetService; + + public BackingAppManagementService(ManagementClient managementClient, AppDeployer appDeployer, + BrokeredServices brokeredServices, TargetService targetService) { + this.managementClient = managementClient; + this.appDeployer = appDeployer; + this.brokeredServices = brokeredServices; + this.targetService = targetService; + } + + public Mono stop(String serviceInstanceId) { + return getBackingApplicationsForService(serviceInstanceId) + .flatMapMany(backingApps -> Flux.fromIterable(backingApps) + .parallel() + .runOn(Schedulers.parallel()) + .flatMap(managementClient::stop) + .doOnRequest(l -> log.debug("Stopping applications {}", backingApps)) + .doOnEach(response -> log.debug("Finished stopping application {}", response)) + .doOnComplete(() -> log.debug("Finished stopping application {}", backingApps)) + .doOnError(exception -> log.error("Error stopping applications {} with error '{}'", + backingApps, exception.getMessage()))) + .then(); + } + + public Mono start(String serviceInstanceId) { + return getBackingApplicationsForService(serviceInstanceId) + .flatMapMany(backingApps -> Flux.fromIterable(backingApps) + .parallel() + .runOn(Schedulers.parallel()) + .flatMap(managementClient::start) + .doOnRequest(l -> log.debug("Starting applications {}", backingApps)) + .doOnEach(response -> log.debug("Finished starting application {}", response)) + .doOnComplete(() -> log.debug("Finished starting application {}", backingApps)) + .doOnError(exception -> log.error("Error starting applications {} with error '{}'", + backingApps, exception.getMessage()))) + .then(); + } + + public Mono restart(String serviceInstanceId) { + return getBackingApplicationsForService(serviceInstanceId) + .flatMapMany(backingApps -> Flux.fromIterable(backingApps) + .parallel() + .runOn(Schedulers.parallel()) + .flatMap(managementClient::restart) + .doOnRequest(l -> log.debug("Restarting applications {}", backingApps)) + .doOnEach(response -> log.debug("Finished restarting application {}", response)) + .doOnComplete(() -> log.debug("Finished restarting application {}", backingApps)) + .doOnError(exception -> log.error("Error restarting applications {} with error '{}'", + backingApps, exception.getMessage()))) + .then(); + } + + public Mono restage(String serviceInstanceId) { + return getBackingApplicationsForService(serviceInstanceId) + .flatMapMany(backingApps -> Flux.fromIterable(backingApps) + .parallel() + .runOn(Schedulers.parallel()) + .flatMap(managementClient::restage) + .doOnRequest(l -> log.debug("Restaging applications {}", backingApps)) + .doOnEach(response -> log.debug("Finished restaging application {}", response)) + .doOnComplete(() -> log.debug("Finished restaging application {}", backingApps)) + .doOnError(exception -> log.error("Error restaging applications {} with error '{}'", + backingApps, exception.getMessage()))) + .then(); + } + + private Mono> getBackingApplicationsForService(String serviceInstanceId) { + return appDeployer.getServiceInstance(GetServiceInstanceRequest.builder() + .serviceInstanceId(serviceInstanceId) + .build()) + .flatMap(response -> findBrokeredService(response.getService(), response.getPlan())) + .flatMap(brokeredService -> updateBackingApps(brokeredService, serviceInstanceId)); + } + + private Mono findBrokeredService(String serviceName, String planName) { + return Flux.fromIterable(brokeredServices) + .filter(brokeredService -> brokeredService.getServiceName().equals(serviceName) + && brokeredService.getPlanName().equals(planName)) + .singleOrEmpty(); + } + + private Mono> updateBackingApps(BrokeredService brokeredService, + String serviceInstanceId) { + return Mono.just(BackingApplications.builder() + .backingApplications(brokeredService.getApps()) + .build()) + .flatMap(backingApps -> targetService.addToBackingApplications(backingApps, + brokeredService.getTarget(), serviceInstanceId)); + } + +} diff --git a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/manager/ManagementClient.java b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/manager/ManagementClient.java new file mode 100644 index 00000000..4220875e --- /dev/null +++ b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/manager/ManagementClient.java @@ -0,0 +1,82 @@ +/* + * Copyright 2016-2019 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.springframework.cloud.appbroker.manager; + +import reactor.core.publisher.Mono; +import reactor.util.Logger; +import reactor.util.Loggers; + +import org.springframework.cloud.appbroker.deployer.BackingApplication; + +public class ManagementClient { + + private final Logger log = Loggers.getLogger(ManagementClient.class); + + private final AppManager appManager; + + public ManagementClient(AppManager appManager) { + this.appManager = appManager; + } + + Mono start(BackingApplication backingApplication) { + return Mono.justOrEmpty(backingApplication) + .flatMap(backingApp -> appManager.start(StartApplicationRequest.builder() + .name(backingApp.getName()) + .properties(backingApp.getProperties()) + .build()) + .doOnRequest(l -> log.debug("Starting application {}", backingApp)) + .doOnSuccess(response -> log.debug("Finished starting application {}", backingApp)) + .doOnError(exception -> log.error("Error starting application {} with error '{}'", + backingApp, exception.getMessage()))); + } + + Mono stop(BackingApplication backingApplication) { + return Mono.justOrEmpty(backingApplication) + .flatMap(backingApp -> appManager.stop(StopApplicationRequest.builder() + .name(backingApp.getName()) + .properties(backingApp.getProperties()) + .build()) + .doOnRequest(l -> log.debug("Stopping application {}", backingApp)) + .doOnSuccess(response -> log.debug("Finished stopping application {}", backingApp)) + .doOnError(exception -> log.error("Error stopping application {} with error '{}'", + backingApp, exception.getMessage()))); + } + + Mono restart(BackingApplication backingApplication) { + return Mono.justOrEmpty(backingApplication) + .flatMap(backingApp -> appManager.restart(RestartApplicationRequest.builder() + .name(backingApp.getName()) + .properties(backingApp.getProperties()) + .build()) + .doOnRequest(l -> log.debug("Restarting application {}", backingApp)) + .doOnSuccess(response -> log.debug("Finished restarting application {}", backingApp)) + .doOnError(exception -> log.error("Error restarting application {} with error '{}'", + backingApp, exception.getMessage()))); + } + + Mono restage(BackingApplication backingApplication) { + return Mono.justOrEmpty(backingApplication) + .flatMap(backingApp -> appManager.restage(RestageApplicationRequest.builder() + .name(backingApp.getName()) + .properties(backingApp.getProperties()) + .build()) + .doOnRequest(l -> log.debug("Restaging application {}", backingApp)) + .doOnSuccess(response -> log.debug("Finished restaging application {}", backingApp)) + .doOnError(exception -> log.error("Error restaging application {} with error '{}'", + backingApp, exception.getMessage()))); + } +} diff --git a/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/manager/BackingAppManagementServiceTest.java b/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/manager/BackingAppManagementServiceTest.java new file mode 100644 index 00000000..7313f873 --- /dev/null +++ b/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/manager/BackingAppManagementServiceTest.java @@ -0,0 +1,389 @@ +/* + * Copyright 2016-2019 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.springframework.cloud.appbroker.manager; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.cloud.appbroker.deployer.AppDeployer; +import org.springframework.cloud.appbroker.deployer.BackingApplication; +import org.springframework.cloud.appbroker.deployer.BackingApplications; +import org.springframework.cloud.appbroker.deployer.BrokeredService; +import org.springframework.cloud.appbroker.deployer.BrokeredServices; +import org.springframework.cloud.appbroker.deployer.GetServiceInstanceRequest; +import org.springframework.cloud.appbroker.deployer.GetServiceInstanceResponse; +import org.springframework.cloud.appbroker.extensions.targets.TargetService; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; + +@ExtendWith(MockitoExtension.class) +class BackingAppManagementServiceTest { + + private BackingAppManagementService backingAppManagementService; + + private BackingApplications backingApps; + + @Mock + private ManagementClient managementClient; + + @Mock + private AppDeployer appDeployer; + + @Mock + private TargetService targetService; + + @BeforeEach + void setUp() { + this.backingApps = BackingApplications.builder() + .backingApplication(BackingApplication.builder() + .name("testApp1") + .path("http://myfiles/app1.jar") + .build()) + .backingApplication(BackingApplication.builder() + .name("testApp2") + .path("http://myfiles/app2.jar") + .build()) + .build(); + + BrokeredServices brokeredServices = BrokeredServices + .builder() + .service(BrokeredService + .builder() + .serviceName("service1") + .planName("plan1") + .apps(backingApps) + .build()) + .build(); + + this.backingAppManagementService = new BackingAppManagementService(managementClient, appDeployer, + brokeredServices, + targetService); + } + + @Test + void stopApplications() { + doReturn(Mono.empty()).when(managementClient).stop(backingApps.get(0)); + doReturn(Mono.empty()).when(managementClient).stop(backingApps.get(1)); + + given(appDeployer.getServiceInstance(any(GetServiceInstanceRequest.class))) + .willReturn(Mono.just(GetServiceInstanceResponse.builder() + .name("foo-service") + .plan("plan1") + .service("service1") + .build())); + + given(targetService.addToBackingApplications(eq(backingApps), any(), eq("foo-service-id"))) + .willReturn(Mono.just(backingApps)); + + StepVerifier.create(backingAppManagementService.stop("foo-service-id")) + .expectNext() + .expectNext() + .verifyComplete(); + + verify(appDeployer).getServiceInstance(any(GetServiceInstanceRequest.class)); + verify(targetService).addToBackingApplications(eq(backingApps), any(), eq("foo-service-id")); + verify(managementClient, times(2)).stop(any(BackingApplication.class)); + verifyNoMoreInteractions(appDeployer, targetService, managementClient); + } + + @Test + void stopApplicationsWithEmptyApplications() { + BackingApplications emptyBackingApps = BackingApplications.builder().build(); + + BrokeredServices brokeredServicesNoApps = BrokeredServices + .builder() + .service(BrokeredService + .builder() + .serviceName("service1") + .planName("plan1") + .apps(emptyBackingApps) + .build()) + .build(); + + this.backingAppManagementService = new BackingAppManagementService(managementClient, appDeployer, + brokeredServicesNoApps, + targetService); + + given(appDeployer.getServiceInstance(any(GetServiceInstanceRequest.class))) + .willReturn(Mono.just(GetServiceInstanceResponse.builder() + .name("foo-service") + .plan("plan1") + .service("service1") + .build())); + + given(targetService.addToBackingApplications(eq(emptyBackingApps), any(), eq("foo-service-id"))) + .willReturn(Mono.just(emptyBackingApps)); + + StepVerifier.create(backingAppManagementService.stop("foo-service-id")) + .verifyComplete(); + + verify(appDeployer).getServiceInstance(any(GetServiceInstanceRequest.class)); + verify(targetService).addToBackingApplications(eq(emptyBackingApps), any(), eq("foo-service-id")); + verifyZeroInteractions(managementClient); + verifyNoMoreInteractions(appDeployer, targetService, managementClient); + } + + @Test + void stopApplicationsServiceNotFound() { + given(appDeployer.getServiceInstance(any(GetServiceInstanceRequest.class))) + .willReturn(Mono.just(GetServiceInstanceResponse.builder() + .build())); + + StepVerifier.create(backingAppManagementService.stop("unknown-service-id")) + .verifyComplete(); + + verifyNoMoreInteractions(appDeployer, targetService, managementClient); + } + + @Test + void startApplications() { + given(appDeployer.getServiceInstance(any(GetServiceInstanceRequest.class))) + .willReturn(Mono.just(GetServiceInstanceResponse.builder() + .name("foo-service") + .plan("plan1") + .service("service1") + .build())); + + given(targetService.addToBackingApplications(eq(backingApps), any(), eq("foo-service-id"))) + .willReturn(Mono.just(backingApps)); + + doReturn(Mono.empty()).when(managementClient).start(backingApps.get(0)); + doReturn(Mono.empty()).when(managementClient).start(backingApps.get(1)); + + StepVerifier.create(backingAppManagementService.start("foo-service-id")) + .expectNext() + .expectNext() + .verifyComplete(); + + verify(appDeployer).getServiceInstance(any(GetServiceInstanceRequest.class)); + verify(targetService).addToBackingApplications(eq(backingApps), any(), eq("foo-service-id")); + verify(managementClient, times(2)).start(any(BackingApplication.class)); + verifyNoMoreInteractions(appDeployer, targetService, managementClient); + } + + @Test + void startApplicationsServiceNotFound() { + given(appDeployer.getServiceInstance(any(GetServiceInstanceRequest.class))) + .willReturn(Mono.just(GetServiceInstanceResponse.builder() + .build())); + + StepVerifier.create(backingAppManagementService.start("unknown-service-id")) + .verifyComplete(); + + verifyNoMoreInteractions(appDeployer, targetService, managementClient); + } + + @Test + void startApplicationsWithEmptyApplications() { + BackingApplications emptyBackingApps = BackingApplications.builder().build(); + + BrokeredServices brokeredServicesNoApps = BrokeredServices + .builder() + .service(BrokeredService + .builder() + .serviceName("service1") + .planName("plan1") + .apps(emptyBackingApps) + .build()) + .build(); + + this.backingAppManagementService = new BackingAppManagementService(managementClient, appDeployer, + brokeredServicesNoApps, + targetService); + + given(appDeployer.getServiceInstance(any(GetServiceInstanceRequest.class))) + .willReturn(Mono.just(GetServiceInstanceResponse.builder() + .name("foo-service") + .plan("plan1") + .service("service1") + .build())); + + given(targetService.addToBackingApplications(eq(emptyBackingApps), any(), eq("foo-service-id"))) + .willReturn(Mono.just(emptyBackingApps)); + + StepVerifier.create(backingAppManagementService.start("foo-service-id")) + .verifyComplete(); + + verify(appDeployer).getServiceInstance(any(GetServiceInstanceRequest.class)); + verify(targetService).addToBackingApplications(eq(emptyBackingApps), any(), eq("foo-service-id")); + verifyZeroInteractions(managementClient); + verifyNoMoreInteractions(appDeployer, targetService, managementClient); + } + + @Test + void restartApplications() { + given(appDeployer.getServiceInstance(any(GetServiceInstanceRequest.class))) + .willReturn(Mono.just(GetServiceInstanceResponse.builder() + .name("foo-service") + .plan("plan1") + .service("service1") + .build())); + + given(targetService.addToBackingApplications(eq(backingApps), any(), eq("foo-service-id"))) + .willReturn(Mono.just(backingApps)); + + doReturn(Mono.empty()).when(managementClient).restart(backingApps.get(0)); + doReturn(Mono.empty()).when(managementClient).restart(backingApps.get(1)); + + StepVerifier.create(backingAppManagementService.restart("foo-service-id")) + .expectNext() + .expectNext() + .verifyComplete(); + + verify(appDeployer).getServiceInstance(any(GetServiceInstanceRequest.class)); + verify(targetService).addToBackingApplications(eq(backingApps), any(), eq("foo-service-id")); + verify(managementClient, times(2)).restart(any(BackingApplication.class)); + verifyNoMoreInteractions(appDeployer, targetService, managementClient); + } + + @Test + void restartApplicationsServiceNotFound() { + given(appDeployer.getServiceInstance(any(GetServiceInstanceRequest.class))) + .willReturn(Mono.just(GetServiceInstanceResponse.builder() + .build())); + + StepVerifier.create(backingAppManagementService.restart("unknown-service-id")) + .verifyComplete(); + + verifyNoMoreInteractions(appDeployer, targetService, managementClient); + } + + @Test + void restartApplicationsWithEmptyApplications() { + BackingApplications emptyBackingApps = BackingApplications.builder().build(); + + BrokeredServices brokeredServicesNoApps = BrokeredServices + .builder() + .service(BrokeredService + .builder() + .serviceName("service1") + .planName("plan1") + .apps(emptyBackingApps) + .build()) + .build(); + + this.backingAppManagementService = new BackingAppManagementService(managementClient, appDeployer, + brokeredServicesNoApps, + targetService); + + given(appDeployer.getServiceInstance(any(GetServiceInstanceRequest.class))) + .willReturn(Mono.just(GetServiceInstanceResponse.builder() + .name("foo-service") + .plan("plan1") + .service("service1") + .build())); + + given(targetService.addToBackingApplications(eq(emptyBackingApps), any(), eq("foo-service-id"))) + .willReturn(Mono.just(emptyBackingApps)); + + StepVerifier.create(backingAppManagementService.restart("foo-service-id")) + .verifyComplete(); + + verify(appDeployer).getServiceInstance(any(GetServiceInstanceRequest.class)); + verify(targetService).addToBackingApplications(eq(emptyBackingApps), any(), eq("foo-service-id")); + verifyZeroInteractions(managementClient); + verifyNoMoreInteractions(appDeployer, targetService, managementClient); + } + + @Test + void restageApplications() { + given(appDeployer.getServiceInstance(any(GetServiceInstanceRequest.class))) + .willReturn(Mono.just(GetServiceInstanceResponse.builder() + .name("foo-service") + .plan("plan1") + .service("service1") + .build())); + + given(targetService.addToBackingApplications(eq(backingApps), any(), eq("foo-service-id"))) + .willReturn(Mono.just(backingApps)); + + doReturn(Mono.empty()).when(managementClient).restage(backingApps.get(0)); + doReturn(Mono.empty()).when(managementClient).restage(backingApps.get(1)); + + StepVerifier.create(backingAppManagementService.restage("foo-service-id")) + .expectNext() + .expectNext() + .verifyComplete(); + + verify(appDeployer).getServiceInstance(any(GetServiceInstanceRequest.class)); + verify(targetService).addToBackingApplications(eq(backingApps), any(), eq("foo-service-id")); + verify(managementClient, times(2)).restage(any(BackingApplication.class)); + verifyNoMoreInteractions(appDeployer, targetService, managementClient); + } + + @Test + void restageApplicationsServiceNotFound() { + given(appDeployer.getServiceInstance(any(GetServiceInstanceRequest.class))) + .willReturn(Mono.just(GetServiceInstanceResponse.builder() + .build())); + + StepVerifier.create(backingAppManagementService.restage("unknown-service-id")) + .verifyComplete(); + + verifyNoMoreInteractions(appDeployer, targetService, managementClient); + } + + @Test + void restageApplicationsWithEmptyApplications() { + BackingApplications emptyBackingApps = BackingApplications.builder().build(); + + BrokeredServices brokeredServicesNoApps = BrokeredServices + .builder() + .service(BrokeredService + .builder() + .serviceName("service1") + .planName("plan1") + .apps(emptyBackingApps) + .build()) + .build(); + + this.backingAppManagementService = new BackingAppManagementService(managementClient, appDeployer, + brokeredServicesNoApps, + targetService); + + given(appDeployer.getServiceInstance(any(GetServiceInstanceRequest.class))) + .willReturn(Mono.just(GetServiceInstanceResponse.builder() + .name("foo-service") + .plan("plan1") + .service("service1") + .build())); + + given(targetService.addToBackingApplications(eq(emptyBackingApps), any(), eq("foo-service-id"))) + .willReturn(Mono.just(emptyBackingApps)); + + StepVerifier.create(backingAppManagementService.restage("foo-service-id")) + .verifyComplete(); + + verify(appDeployer).getServiceInstance(any(GetServiceInstanceRequest.class)); + verify(targetService).addToBackingApplications(eq(emptyBackingApps), any(), eq("foo-service-id")); + verifyZeroInteractions(managementClient); + verifyNoMoreInteractions(appDeployer, targetService, managementClient); + } + +} \ No newline at end of file diff --git a/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/manager/ManagementClientTest.java b/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/manager/ManagementClientTest.java new file mode 100644 index 00000000..2f79084f --- /dev/null +++ b/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/manager/ManagementClientTest.java @@ -0,0 +1,141 @@ +/* + * Copyright 2016-2019 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.springframework.cloud.appbroker.manager; + +import java.util.Collections; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.cloud.appbroker.deployer.BackingApplication; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; + +@ExtendWith(MockitoExtension.class) +class ManagementClientTest { + + private ManagementClient managementClient; + + @Mock + private AppManager appManager; + + private BackingApplication backingApplication; + + @BeforeEach + void setUp() { + this.managementClient = new ManagementClient(appManager); + + this.backingApplication = BackingApplication.builder() + .name("foo-app") + .property("foo", "bar") + .build(); + } + + @Test + void startApplication() { + given(appManager.start(any(StartApplicationRequest.class))) + .willReturn(Mono.empty()); + + StepVerifier.create(managementClient.start(backingApplication)) + .verifyComplete(); + + verify(appManager).start(argThat(request -> "foo-app".equals(request.getName()) && + Collections.singletonMap("foo", "bar").equals(request.getProperties()))); + verifyNoMoreInteractions(appManager); + } + + @Test + void startNullApplication() { + StepVerifier.create(managementClient.start(null)) + .verifyComplete(); + + verifyZeroInteractions(appManager); + } + + @Test + void stopApplication() { + given(appManager.stop(any(StopApplicationRequest.class))) + .willReturn(Mono.empty()); + + StepVerifier.create(managementClient.stop(backingApplication)) + .verifyComplete(); + + verify(appManager).stop(argThat(request -> "foo-app".equals(request.getName()) && + Collections.singletonMap("foo", "bar").equals(request.getProperties()))); + verifyNoMoreInteractions(appManager); + } + + @Test + void stopNullApplication() { + StepVerifier.create(managementClient.stop(null)) + .verifyComplete(); + + verifyZeroInteractions(appManager); + } + + @Test + void restartApplication() { + given(appManager.restart(any(RestartApplicationRequest.class))) + .willReturn(Mono.empty()); + + StepVerifier.create(managementClient.restart(backingApplication)) + .verifyComplete(); + + verify(appManager).restart(argThat(request -> "foo-app".equals(request.getName()) && + Collections.singletonMap("foo", "bar").equals(request.getProperties()))); + verifyNoMoreInteractions(appManager); + } + + @Test + void restartNullApplication() { + StepVerifier.create(managementClient.restart(null)) + .verifyComplete(); + + verifyZeroInteractions(appManager); + } + + @Test + void restageApplication() { + given(appManager.restage(any(RestageApplicationRequest.class))) + .willReturn(Mono.empty()); + + StepVerifier.create(managementClient.restage(backingApplication)) + .verifyComplete(); + + verify(appManager).restage(argThat(request -> "foo-app".equals(request.getName()) && + Collections.singletonMap("foo", "bar").equals(request.getProperties()))); + verifyNoMoreInteractions(appManager); + } + + @Test + void restageNullApplication() { + StepVerifier.create(managementClient.restage(null)) + .verifyComplete(); + + verifyZeroInteractions(appManager); + } +} \ No newline at end of file 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 77a8bed0..153dd57b 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 @@ -36,8 +36,10 @@ import org.cloudfoundry.UnknownCloudFoundryException; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.organizations.ListOrganizationSpacesRequest; +import org.cloudfoundry.client.v2.serviceinstances.ServiceInstanceEntity; import org.cloudfoundry.client.v2.spaces.CreateSpaceRequest; import org.cloudfoundry.client.v2.spaces.DeleteSpaceRequest; +import org.cloudfoundry.client.v2.spaces.SpaceEntity; import org.cloudfoundry.client.v3.Relationship; import org.cloudfoundry.client.v3.ToOneRelationship; import org.cloudfoundry.client.v3.builds.BuildState; @@ -62,7 +64,6 @@ import org.cloudfoundry.client.v3.packages.UploadPackageRequest; import org.cloudfoundry.client.v3.packages.UploadPackageResponse; import org.cloudfoundry.operations.CloudFoundryOperations; -import org.cloudfoundry.operations.DefaultCloudFoundryOperations; import org.cloudfoundry.operations.applications.ApplicationDetail; import org.cloudfoundry.operations.applications.ApplicationHealthCheck; import org.cloudfoundry.operations.applications.ApplicationManifest; @@ -74,11 +75,11 @@ import org.cloudfoundry.operations.organizations.OrganizationDetail; import org.cloudfoundry.operations.organizations.OrganizationInfoRequest; import org.cloudfoundry.operations.services.BindServiceInstanceRequest; -import org.cloudfoundry.operations.services.GetServiceInstanceRequest; import org.cloudfoundry.operations.services.ServiceInstance; import org.cloudfoundry.operations.services.UnbindServiceInstanceRequest; import org.cloudfoundry.util.DelayUtils; import org.cloudfoundry.util.PaginationUtils; +import org.cloudfoundry.util.ResourceUtils; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -94,6 +95,8 @@ import org.springframework.cloud.appbroker.deployer.DeployApplicationRequest; import org.springframework.cloud.appbroker.deployer.DeployApplicationResponse; import org.springframework.cloud.appbroker.deployer.DeploymentProperties; +import org.springframework.cloud.appbroker.deployer.GetServiceInstanceRequest; +import org.springframework.cloud.appbroker.deployer.GetServiceInstanceResponse; import org.springframework.cloud.appbroker.deployer.UndeployApplicationRequest; import org.springframework.cloud.appbroker.deployer.UndeployApplicationResponse; import org.springframework.cloud.appbroker.deployer.UpdateApplicationRequest; @@ -117,7 +120,11 @@ public class CloudFoundryAppDeployer implements AppDeployer, ResourceLoaderAware private final CloudFoundryDeploymentProperties defaultDeploymentProperties; private final CloudFoundryOperations operations; + private final CloudFoundryClient client; + + private final CloudFoundryOperationsUtils operationsUtils; + private final CloudFoundryTargetProperties targetProperties; private ResourceLoader resourceLoader; @@ -125,11 +132,13 @@ public class CloudFoundryAppDeployer implements AppDeployer, ResourceLoaderAware public CloudFoundryAppDeployer(CloudFoundryDeploymentProperties deploymentProperties, CloudFoundryOperations operations, CloudFoundryClient client, + CloudFoundryOperationsUtils operationsUtils, CloudFoundryTargetProperties targetProperties, ResourceLoader resourceLoader) { this.defaultDeploymentProperties = deploymentProperties; this.operations = operations; this.client = client; + this.operationsUtils = operationsUtils; this.targetProperties = targetProperties; this.resourceLoader = resourceLoader; } @@ -170,63 +179,52 @@ public Mono update(UpdateApplicationRequest request) final Map environmentVariables = getApplicationEnvironment(request.getProperties(), request.getEnvironment(), request.getServiceInstanceId()); - Map deploymentProperties = request.getProperties(); - CloudFoundryOperations operations; - - if (deploymentProperties.containsKey(DeploymentProperties.TARGET_PROPERTY_KEY)) { - String space = deploymentProperties.get(DeploymentProperties.TARGET_PROPERTY_KEY); - operations = createCloudFoundryOperationsForSpace(space); - } - else { - operations = this.operations; - } + return operationsUtils.getOperations(request.getProperties()) + .flatMap(cfOperations -> cfOperations.applications() + .get(GetApplicationRequest.builder().name(name).build()) + .doOnRequest(l -> logger.debug("Getting application {}", name)) + .doOnSuccess(response -> logger.info("Success getting application {}", name)) + .doOnError(e -> logger.warn(String.format("Error getting application %s: %s", name, e.getMessage()))) + .map(ApplicationDetail::getId) + .flatMap(applicationId -> + updateApplicationEnvironment(applicationId, environmentVariables).thenReturn(applicationId) + ) + .flatMap(applicationId -> Mono.zip(Mono.just(applicationId), + createPackageForApplication(applicationId))) + .map(tuple2 -> tuple2.mapT2(CreatePackageResponse::getId)) + .flatMap(tuple2 -> { + String packageId = tuple2.getT2(); + return Mono.zip(Mono.just(tuple2.getT1()), uploadPackage(request, packageId)); + }) - return operations - .applications() - .get(GetApplicationRequest.builder().name(name).build()) - .doOnRequest(l -> logger.debug("Getting application {}", name)) - .doOnSuccess(response -> logger.info("Success getting application {}", name)) - .doOnError(e -> logger.warn(String.format("Error getting application %s: %s", name, e.getMessage()))) - .map(ApplicationDetail::getId) - .flatMap(applicationId -> - updateApplicationEnvironment(applicationId, environmentVariables).thenReturn(applicationId) - ) - .flatMap(applicationId -> Mono.zip(Mono.just(applicationId), - createPackageForApplication(applicationId))) - .map(tuple2 -> tuple2.mapT2(CreatePackageResponse::getId)) - .flatMap(tuple2 -> { - String packageId = tuple2.getT2(); - return Mono.zip(Mono.just(tuple2.getT1()), uploadPackage(request, packageId)); - }) - - .map(tuple2 -> tuple2.mapT2(Package::getId)) - .flatMap(tuple2 -> { - String packageId1 = tuple2.getT2(); - return Mono.zip(Mono.just(tuple2.getT1()), waitForPackageReady(packageId1)); - } - ) - .map(tuple2 -> tuple2.mapT2(Package::getId)) - .flatMap(tuple2 -> { - String packageId = tuple2.getT2(); - return Mono.zip(Mono.just(tuple2.getT1()), createBuildForPackage(packageId)); - }) - .flatMap(tuple2 -> { - String buildId = tuple2.getT2(); - return Mono.zip(Mono.just(tuple2.getT1()), waitForBuildStaged(buildId)); - } - ) - .map(tuple2 -> tuple2.mapT2((t2) -> t2.getDroplet().getId())) - .flatMap(tuple2 -> { - String dropletId = tuple2.getT2(); - String applicationId = tuple2.getT1(); - return createDeployment(dropletId, applicationId); - }) - .map(CreateDeploymentResponse::getId) - .flatMap(this::waitForDeploymentDeployed) - .doOnRequest(l -> logger.debug("Updating application {}", name)) - .doOnSuccess(item -> logger.info("Successfully updated application {}", name)) - .doOnError(error -> logger.error("Failed to update application {}", name)) - .thenReturn(UpdateApplicationResponse.builder().name(name).build()); + .map(tuple2 -> tuple2.mapT2(Package::getId)) + .flatMap(tuple2 -> { + String packageId1 = tuple2.getT2(); + return Mono.zip(Mono.just(tuple2.getT1()), waitForPackageReady(packageId1)); + } + ) + .map(tuple2 -> tuple2.mapT2(Package::getId)) + .flatMap(tuple2 -> { + String packageId = tuple2.getT2(); + return Mono.zip(Mono.just(tuple2.getT1()), createBuildForPackage(packageId)); + }) + .flatMap(tuple2 -> { + String buildId = tuple2.getT2(); + return Mono.zip(Mono.just(tuple2.getT1()), waitForBuildStaged(buildId)); + } + ) + .map(tuple2 -> tuple2.mapT2((t2) -> t2.getDroplet().getId())) + .flatMap(tuple2 -> { + String dropletId = tuple2.getT2(); + String applicationId = tuple2.getT1(); + return createDeployment(dropletId, applicationId); + }) + .map(CreateDeploymentResponse::getId) + .flatMap(this::waitForDeploymentDeployed) + .doOnRequest(l -> logger.debug("Updating application {}", name)) + .doOnSuccess(item -> logger.info("Successfully updated application {}", name)) + .doOnError(error -> logger.error("Failed to update application {}", name)) + .thenReturn(UpdateApplicationResponse.builder().name(name).build())); } private Mono waitForDeploymentDeployed(String deploymentId) { @@ -429,9 +427,8 @@ private Mono pushManifest(PushApplicationManifestRequest request) { private Mono pushManifestInSpace(PushApplicationManifestRequest request, String spaceName) { return createSpace(spaceName) - .then(createCloudFoundryOperationsForSpace(spaceName) - .applications() - .pushManifest(request)); + .then(operationsUtils.getOperationsForSpace(spaceName)) + .flatMap(cfOperations -> cfOperations.applications().pushManifest(request)); } private Mono createSpace(String spaceName) { @@ -491,21 +488,20 @@ private Mono deleteApplication(String name) { private Mono deleteApplicationInSpace(String name, String spaceName) { return getSpaceIdFromName(spaceName) - .doOnError(error -> logger.warn("Unable get space name: {} ", spaceName)) - .then(createCloudFoundryOperationsForSpace(spaceName) - .applications() - .delete(DeleteApplicationRequest.builder() - .deleteRoutes(this.defaultDeploymentProperties.isDeleteRoutes()) - .name(name) - .build()) - .doOnError(error -> logger.warn("Unable delete application: {} ", name)) + .doOnError(error -> logger.warn("Unable to get space name: {} ", spaceName)) + .then(operationsUtils.getOperationsForSpace(spaceName)) + .flatMap(cfOperations -> cfOperations.applications().delete(DeleteApplicationRequest.builder() + .deleteRoutes(this.defaultDeploymentProperties.isDeleteRoutes()) + .name(name) + .build()) + .doOnError(error -> logger.warn("Unable to delete application: {} ", name)) .then(deleteSpace(spaceName))) .onErrorResume(e -> Mono.empty()); } private Mono deleteSpace(String spaceName) { return getSpaceIdFromName(spaceName) - .doOnError(error -> logger.warn("Unable get space name: {} ", spaceName)) + .doOnError(error -> logger.warn("Unable to get space name: {} ", spaceName)) .flatMap(spaceId -> this.client.spaces() .delete(DeleteSpaceRequest.builder() .spaceId(spaceId) @@ -526,13 +522,6 @@ private Mono getSpaceIdFromName(String spaceName) { .next()); } - private CloudFoundryOperations createCloudFoundryOperationsForSpace(String space) { - return DefaultCloudFoundryOperations.builder() - .from((DefaultCloudFoundryOperations) this.operations) - .space(space) - .build(); - } - private Map getEnvironmentVariables(Map properties, Map environment, String serviceInstanceId) { @@ -721,6 +710,51 @@ private Path getApplication(Resource resource) { } } + @Override + public Mono getServiceInstance(GetServiceInstanceRequest request) { + return Mono.defer(() -> { + if (StringUtils.hasText(request.getServiceInstanceId())) { + return getServiceInstance(request.getServiceInstanceId()) + .flatMap(serviceInstanceEntity -> getSpace(serviceInstanceEntity.getSpaceId()) + .flatMap(spaceEntity -> getServiceInstance(serviceInstanceEntity.getName(), spaceEntity.getName()))); + } + else { + return operationsUtils.getOperations(request.getProperties()) + .flatMap(cfOperations -> cfOperations.services() + .getInstance(org.cloudfoundry.operations.services.GetServiceInstanceRequest.builder() + .name(request.getName()) + .build())); + } + }) + .map(serviceInstance -> GetServiceInstanceResponse.builder() + .name(serviceInstance.getName()) + .service(serviceInstance.getService()) + .plan(serviceInstance.getPlan()) + .build()); + } + + private Mono getServiceInstance(String name, String space) { + return operationsUtils.getOperationsForSpace(space) + .flatMap(cfOperations -> cfOperations.services() + .getInstance(org.cloudfoundry.operations.services.GetServiceInstanceRequest.builder() + .name(name) + .build())); + } + + private Mono getServiceInstance(String serviceInstanceId) { + return client.serviceInstances().get(org.cloudfoundry.client.v2.serviceinstances.GetServiceInstanceRequest.builder() + .serviceInstanceId(serviceInstanceId) + .build()) + .map(ResourceUtils::getEntity); + } + + private Mono getSpace(String spaceId) { + return client.spaces().get(org.cloudfoundry.client.v2.spaces.GetSpaceRequest.builder() + .spaceId(spaceId) + .build()) + .map(ResourceUtils::getEntity); + } + @Override public Mono createServiceInstance(CreateServiceInstanceRequest request) { org.cloudfoundry.operations.services.CreateServiceInstanceRequest createServiceInstanceRequest = @@ -740,11 +774,12 @@ public Mono createServiceInstance(CreateServiceIn if (request.getProperties().containsKey(DeploymentProperties.TARGET_PROPERTY_KEY)) { return createSpace(request.getProperties().get(DeploymentProperties.TARGET_PROPERTY_KEY)) .then( - createCloudFoundryOperationsForSpace(request.getProperties().get(DeploymentProperties.TARGET_PROPERTY_KEY)) - .services() - .createInstance(createServiceInstanceRequest) - .then(createServiceInstanceResponseMono)); - } else { + operationsUtils.getOperations(request.getProperties()) + .flatMap(cfOperations -> cfOperations.services() + .createInstance(createServiceInstanceRequest) + .then(createServiceInstanceResponseMono))); + } + else { return operations .services() .createInstance(createServiceInstanceRequest) @@ -754,22 +789,20 @@ public Mono createServiceInstance(CreateServiceIn @Override public Mono updateServiceInstance(UpdateServiceInstanceRequest request) { - CloudFoundryOperations cloudFoundryOperations = getOperations(request.getProperties()); - return rebindServiceInstanceIfNecessary(request, cloudFoundryOperations) - .then(updateServiceInstanceIfNecessary(request, cloudFoundryOperations)); + return operationsUtils.getOperations(request.getProperties()) + .flatMap(cfOperations -> rebindServiceInstanceIfNecessary(request, cfOperations) + .then(updateServiceInstanceIfNecessary(request, cfOperations))); } - @Override public Mono deleteServiceInstance(DeleteServiceInstanceRequest request) { - final String serviceInstanceName = request.getServiceInstanceName(); - - CloudFoundryOperations cloudFoundryOperations = getOperations(request.getProperties()); - return unbindServiceInstance(serviceInstanceName, cloudFoundryOperations) - .then(deleteServiceInstance(serviceInstanceName, cloudFoundryOperations) - .then(Mono.just(DeleteServiceInstanceResponse.builder() - .name(serviceInstanceName) - .build()))); + return operationsUtils.getOperations(request.getProperties()) + .flatMap(cfOperations -> Mono.just(request.getServiceInstanceName()) + .flatMap(serviceInstanceName -> unbindServiceInstance(serviceInstanceName, cfOperations) + .then(deleteServiceInstance(serviceInstanceName, cfOperations) + .thenReturn(DeleteServiceInstanceResponse.builder() + .name(serviceInstanceName) + .build())))); } private Mono deleteServiceInstance(String serviceInstanceName, CloudFoundryOperations cloudFoundryOperations) { @@ -782,7 +815,7 @@ private Mono deleteServiceInstance(String serviceInstanceName, CloudFoundr private Mono unbindServiceInstance(String serviceInstanceName, CloudFoundryOperations cloudFoundryOperations) { - return cloudFoundryOperations.services().getInstance(GetServiceInstanceRequest.builder() + return cloudFoundryOperations.services().getInstance(org.cloudfoundry.operations.services.GetServiceInstanceRequest.builder() .name(serviceInstanceName) .build()) .map(ServiceInstance::getApplications) @@ -798,7 +831,7 @@ private Mono unbindServiceInstance(String serviceInstanceName, private Mono rebindServiceInstance(String serviceInstanceName, CloudFoundryOperations cloudFoundryOperations) { - return cloudFoundryOperations.services().getInstance(GetServiceInstanceRequest.builder() + return cloudFoundryOperations.services().getInstance(org.cloudfoundry.operations.services.GetServiceInstanceRequest.builder() .name(serviceInstanceName) .build()) .map(ServiceInstance::getApplications) @@ -846,14 +879,6 @@ private Mono updateServiceInstanceIfNecessary(Upd .build())); } - private CloudFoundryOperations getOperations(Map properties) { - if (properties.containsKey(DeploymentProperties.TARGET_PROPERTY_KEY)) { - return createCloudFoundryOperationsForSpace(properties.get(DeploymentProperties.TARGET_PROPERTY_KEY)); - } else { - return this.operations; - } - } - /** * Return a function usable in {@literal doOnError} constructs that will unwrap unrecognized Cloud Foundry Exceptions * and log the text payload. diff --git a/spring-cloud-app-broker-deployer-cloudfoundry/src/main/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppManager.java b/spring-cloud-app-broker-deployer-cloudfoundry/src/main/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppManager.java new file mode 100644 index 00000000..946e3fc5 --- /dev/null +++ b/spring-cloud-app-broker-deployer-cloudfoundry/src/main/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppManager.java @@ -0,0 +1,94 @@ +/* + * Copyright 2016-2019 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.springframework.cloud.appbroker.deployer.cloudfoundry; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +import org.springframework.cloud.appbroker.manager.AppManager; +import org.springframework.cloud.appbroker.manager.RestageApplicationRequest; +import org.springframework.cloud.appbroker.manager.RestartApplicationRequest; +import org.springframework.cloud.appbroker.manager.StartApplicationRequest; +import org.springframework.cloud.appbroker.manager.StopApplicationRequest; + +public class CloudFoundryAppManager implements AppManager { + + private final Logger logger = LoggerFactory.getLogger(CloudFoundryAppManager.class); + + private final CloudFoundryOperationsUtils operationsUtils; + + public CloudFoundryAppManager(CloudFoundryOperationsUtils operationsUtils) { + this.operationsUtils = operationsUtils; + } + + @Override + public Mono start(StartApplicationRequest request) { + return Mono.justOrEmpty(request) + .flatMap(req -> operationsUtils.getOperations(req.getProperties()) + .flatMap(cfOperations -> Mono.justOrEmpty(req.getName()) + .flatMap(appName -> cfOperations.applications().start( + org.cloudfoundry.operations.applications.StartApplicationRequest.builder() + .name(appName) + .build()) + .doOnRequest(l -> logger.debug("Starting application {}", appName)) + .doOnSuccess(item -> logger.info("Successfully started application {}", appName)) + .doOnError(error -> logger.error("Failed to start application {}", appName))))); + } + + @Override + public Mono stop(StopApplicationRequest request) { + return Mono.justOrEmpty(request) + .flatMap(req -> operationsUtils.getOperations(req.getProperties()) + .flatMap(cfOperations -> Mono.justOrEmpty(req.getName()) + .flatMap(appName -> cfOperations.applications().stop( + org.cloudfoundry.operations.applications.StopApplicationRequest.builder() + .name(appName) + .build()) + .doOnRequest(l -> logger.debug("Stopping application {}", appName)) + .doOnSuccess(item -> logger.info("Successfully stopped application {}", appName)) + .doOnError(error -> logger.error("Failed to stop application {}", appName))))); + } + + @Override + public Mono restart(RestartApplicationRequest request) { + return Mono.justOrEmpty(request) + .flatMap(req -> operationsUtils.getOperations(req.getProperties()) + .flatMap(cfOperations -> Mono.justOrEmpty(req.getName()) + .flatMap(appName -> cfOperations.applications().restart( + org.cloudfoundry.operations.applications.RestartApplicationRequest.builder() + .name(appName) + .build()) + .doOnRequest(l -> logger.debug("Restarting application {}", appName)) + .doOnSuccess(item -> logger.info("Successfully restarted application {}", appName)) + .doOnError(error -> logger.error("Failed to restart application {}", appName))))); + } + + @Override + public Mono restage(RestageApplicationRequest request) { + return Mono.justOrEmpty(request) + .flatMap(req -> operationsUtils.getOperations(req.getProperties()) + .flatMap(cfOperations -> Mono.justOrEmpty(req.getName()) + .flatMap(appName -> cfOperations.applications().restage( + org.cloudfoundry.operations.applications.RestageApplicationRequest.builder() + .name(appName) + .build()) + .doOnRequest(l -> logger.debug("Restaging application {}", appName)) + .doOnSuccess(item -> logger.info("Successfully restaged application {}", appName)) + .doOnError(error -> logger.error("Failed to restage application {}", appName))))); + } +} diff --git a/spring-cloud-app-broker-deployer-cloudfoundry/src/main/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryOperationsUtils.java b/spring-cloud-app-broker-deployer-cloudfoundry/src/main/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryOperationsUtils.java new file mode 100644 index 00000000..e3d09498 --- /dev/null +++ b/spring-cloud-app-broker-deployer-cloudfoundry/src/main/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryOperationsUtils.java @@ -0,0 +1,54 @@ +/* + * Copyright 2016-2019 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.springframework.cloud.appbroker.deployer.cloudfoundry; + +import java.util.Map; + +import org.cloudfoundry.operations.CloudFoundryOperations; +import org.cloudfoundry.operations.DefaultCloudFoundryOperations; +import reactor.core.publisher.Mono; + +import org.springframework.cloud.appbroker.deployer.DeploymentProperties; +import org.springframework.util.CollectionUtils; + +public class CloudFoundryOperationsUtils { + + private final CloudFoundryOperations operations; + + public CloudFoundryOperationsUtils(CloudFoundryOperations operations) { + this.operations = operations; + } + + public Mono getOperations(Map properties) { + return Mono.defer(() -> { + if (!CollectionUtils.isEmpty(properties) && properties.containsKey( + DeploymentProperties.TARGET_PROPERTY_KEY)) { + return getOperationsForSpace(properties.get(DeploymentProperties.TARGET_PROPERTY_KEY)); + } + return Mono.just(this.operations); + }); + } + + public Mono getOperationsForSpace(String space) { + return Mono.just(this.operations) + .cast(DefaultCloudFoundryOperations.class) + .map(cfOperations -> DefaultCloudFoundryOperations.builder() + .from(cfOperations) + .space(space) + .build()); + } +} 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 3f622277..dc4f5e8a 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 @@ -22,7 +22,12 @@ import java.util.Map; import org.cloudfoundry.client.CloudFoundryClient; +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.GetSpaceRequest; +import org.cloudfoundry.client.v2.spaces.GetSpaceResponse; +import org.cloudfoundry.client.v2.spaces.SpaceEntity; import org.cloudfoundry.operations.CloudFoundryOperations; import org.cloudfoundry.operations.applications.ApplicationHealthCheck; import org.cloudfoundry.operations.applications.ApplicationManifest; @@ -54,12 +59,17 @@ import org.springframework.cloud.appbroker.deployer.UpdateServiceInstanceRequest; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.ResourceLoader; +import org.springframework.util.CollectionUtils; import static java.util.Collections.emptyMap; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; @SuppressWarnings("UnassignedFluxMonoInstance") @@ -85,12 +95,18 @@ class CloudFoundryAppDeployerTest { @Mock private ServiceInstances clientServiceInstances; + @Mock + private org.cloudfoundry.client.v2.spaces.Spaces clientSpaces; + @Mock private CloudFoundryOperations cloudFoundryOperations; @Mock private CloudFoundryClient cloudFoundryClient; + @Mock + private CloudFoundryOperationsUtils operationsUtils; + @Mock private ResourceLoader resourceLoader; @@ -108,9 +124,12 @@ void setUp() { when(cloudFoundryOperations.applications()).thenReturn(operationsApplications); when(cloudFoundryOperations.services()).thenReturn(operationsServices); when(cloudFoundryClient.serviceInstances()).thenReturn(clientServiceInstances); + when(cloudFoundryClient.spaces()).thenReturn(clientSpaces); + when(operationsUtils.getOperations(anyMap())).thenReturn(Mono.just(cloudFoundryOperations)); + when(operationsUtils.getOperationsForSpace(anyString())).thenReturn(Mono.just(cloudFoundryOperations)); - appDeployer = new CloudFoundryAppDeployer(deploymentProperties, - cloudFoundryOperations, cloudFoundryClient, targetProperties, resourceLoader); + appDeployer = new CloudFoundryAppDeployer(deploymentProperties, cloudFoundryOperations, cloudFoundryClient, + operationsUtils, targetProperties, resourceLoader); } @Test @@ -474,6 +493,124 @@ void updateServiceInstanceDoesNothingWithoutParameters() { .verifyComplete(); } + @Test + void getServiceInstanceById() { + when(operationsServices.getInstance(any(GetServiceInstanceRequest.class))) + .thenReturn(Mono.just(ServiceInstance.builder() + .id("foo-service-instance-id") + .name("my-foo-service") + .service("foo-service") + .plan("foo-plan") + .type(ServiceInstanceType.MANAGED) + .build())); + + when(clientServiceInstances + .get(any(org.cloudfoundry.client.v2.serviceinstances.GetServiceInstanceRequest.class))) + .thenReturn(Mono.just(GetServiceInstanceResponse.builder() + .entity(ServiceInstanceEntity.builder() + .name("my-foo-service") + .spaceId("foo-space-id") + .build()) + .build())); + + when(clientSpaces.get(GetSpaceRequest.builder() + .spaceId("foo-space-id") + .build())) + .thenReturn(Mono.just(GetSpaceResponse.builder() + .entity(SpaceEntity.builder() + .name("foo-space") + .build()) + .build())); + + org.springframework.cloud.appbroker.deployer.GetServiceInstanceRequest request = + org.springframework.cloud.appbroker.deployer.GetServiceInstanceRequest + .builder() + .serviceInstanceId("foo-service-instance-id") + .properties(emptyMap()) + .build(); + + StepVerifier.create(appDeployer.getServiceInstance(request)) + .assertNext(response -> { + assertThat(response.getName()).isEqualTo("my-foo-service"); + assertThat(response.getService()).isEqualTo("foo-service"); + assertThat(response.getPlan()).isEqualTo("foo-plan"); + }) + .verifyComplete(); + + verify(operationsUtils).getOperationsForSpace(argThat("foo-space"::equals)); + verify(cloudFoundryClient).serviceInstances(); + verify(clientServiceInstances).get(argThat(req -> "foo-service-instance-id".equals(req.getServiceInstanceId()))); + verify(cloudFoundryClient).spaces(); + verify(clientSpaces).get(argThat(req -> "foo-space-id".equals(req.getSpaceId()))); + verify(cloudFoundryOperations).services(); + verify(operationsServices).getInstance(argThat(req -> "my-foo-service".equals(req.getName()))); + verifyNoMoreInteractions(cloudFoundryClient, cloudFoundryOperations, operationsUtils); + } + + @Test + void getServiceInstanceByName() { + when(operationsServices.getInstance(any(GetServiceInstanceRequest.class))) + .thenReturn(Mono.just(ServiceInstance.builder() + .id("foo-service-instance-id") + .name("my-foo-service") + .service("foo-service") + .plan("foo-plan") + .type(ServiceInstanceType.MANAGED) + .build())); + + org.springframework.cloud.appbroker.deployer.GetServiceInstanceRequest request = org.springframework.cloud.appbroker.deployer.GetServiceInstanceRequest + .builder() + .name("my-foo-service") + .build(); + + StepVerifier.create(appDeployer.getServiceInstance(request)) + .assertNext(response -> { + assertThat(response.getName()).isEqualTo("my-foo-service"); + assertThat(response.getService()).isEqualTo("foo-service"); + assertThat(response.getPlan()).isEqualTo("foo-plan"); + }) + .verifyComplete(); + + verify(operationsUtils).getOperations(argThat(CollectionUtils::isEmpty)); + verify(cloudFoundryOperations).services(); + verify(operationsServices).getInstance(argThat(req -> "my-foo-service".equals(req.getName()))); + verifyZeroInteractions(cloudFoundryClient); + verifyNoMoreInteractions(cloudFoundryOperations, operationsUtils); + } + + @Test + void getServiceInstanceByNameAndSpace() { + when(operationsServices.getInstance(any(GetServiceInstanceRequest.class))) + .thenReturn(Mono.just(ServiceInstance.builder() + .id("foo-service-instance-id") + .name("my-foo-service") + .service("foo-service") + .plan("foo-plan") + .type(ServiceInstanceType.MANAGED) + .build())); + + 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")) + .build(); + + StepVerifier.create(appDeployer.getServiceInstance(request)) + .assertNext(response -> { + assertThat(response.getName()).isEqualTo("my-foo-service"); + assertThat(response.getService()).isEqualTo("foo-service"); + assertThat(response.getPlan()).isEqualTo("foo-plan"); + }) + .verifyComplete(); + + verify(operationsUtils).getOperations( + argThat(argument -> "foo-space".equals(argument.get(DeploymentProperties.TARGET_PROPERTY_KEY)))); + verify(cloudFoundryOperations).services(); + verify(operationsServices).getInstance(argThat(req -> "my-foo-service".equals(req.getName()))); + verifyZeroInteractions(cloudFoundryClient); + verifyNoMoreInteractions(cloudFoundryOperations, operationsUtils); + } + private ApplicationManifest.Builder baseManifest() { return ApplicationManifest.builder() .services(new ArrayList<>()); diff --git a/spring-cloud-app-broker-deployer-cloudfoundry/src/test/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployerUpdateApplicationTest.java b/spring-cloud-app-broker-deployer-cloudfoundry/src/test/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployerUpdateApplicationTest.java index 6a74668c..2ac5d65a 100644 --- a/spring-cloud-app-broker-deployer-cloudfoundry/src/test/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployerUpdateApplicationTest.java +++ b/spring-cloud-app-broker-deployer-cloudfoundry/src/test/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployerUpdateApplicationTest.java @@ -60,6 +60,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -92,6 +94,9 @@ class CloudFoundryAppDeployerUpdateApplicationTest { @Mock private CloudFoundryClient cloudFoundryClient; + @Mock + private CloudFoundryOperationsUtils operationsUtils; + @Mock private ResourceLoader resourceLoader; @@ -108,9 +113,11 @@ void setUp() { when(cloudFoundryClient.packages()).thenReturn(packages); when(cloudFoundryClient.builds()).thenReturn(builds); when(cloudFoundryClient.deploymentsV3()).thenReturn(deploymentsV3); + when(operationsUtils.getOperations(anyMap())).thenReturn(Mono.just(cloudFoundryOperations)); + when(operationsUtils.getOperationsForSpace(anyString())).thenReturn(Mono.just(cloudFoundryOperations)); appDeployer = new CloudFoundryAppDeployer(deploymentProperties, - cloudFoundryOperations, cloudFoundryClient, targetProperties, resourceLoader); + cloudFoundryOperations, cloudFoundryClient, operationsUtils, targetProperties, resourceLoader); } @Test diff --git a/spring-cloud-app-broker-deployer-cloudfoundry/src/test/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppManagerTest.java b/spring-cloud-app-broker-deployer-cloudfoundry/src/test/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppManagerTest.java new file mode 100644 index 00000000..06b124fa --- /dev/null +++ b/spring-cloud-app-broker-deployer-cloudfoundry/src/test/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppManagerTest.java @@ -0,0 +1,229 @@ +/* + * Copyright 2016-2019 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.springframework.cloud.appbroker.deployer.cloudfoundry; + +import org.cloudfoundry.operations.CloudFoundryOperations; +import org.cloudfoundry.operations.applications.Applications; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.cloud.appbroker.manager.RestageApplicationRequest; +import org.springframework.cloud.appbroker.manager.RestartApplicationRequest; +import org.springframework.cloud.appbroker.manager.StartApplicationRequest; +import org.springframework.cloud.appbroker.manager.StopApplicationRequest; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CloudFoundryAppManagerTest { + + private CloudFoundryAppManager appManager; + + @Mock + private Applications operationsApplications; + + @Mock + private CloudFoundryOperations operations; + + @Mock + private CloudFoundryOperationsUtils operationsUtils; + + @BeforeEach + void setUp() { + this.appManager = new CloudFoundryAppManager(operationsUtils); + } + + @Test + void startNullApplication() { + StepVerifier.create(appManager.start(null)) + .verifyComplete(); + + verifyZeroInteractions(operationsApplications); + verifyNoMoreInteractions(operations); + } + + @Test + void startApplication() { + setupStubs(); + + when(operationsApplications.start(any(org.cloudfoundry.operations.applications.StartApplicationRequest.class))) + .thenReturn(Mono.empty()); + + StartApplicationRequest request = StartApplicationRequest.builder() + .name("my-foo-app") + .build(); + + StepVerifier.create(appManager.start(request)) + .verifyComplete(); + + verify(operationsApplications).start(argThat(req -> "my-foo-app".equals(req.getName()))); + verifyNoMoreInteractions(operations); + } + + @Test + void startApplicationWithEmptyName() { + when(operationsUtils.getOperations(anyMap())).thenReturn(Mono.just(operations)); + + StartApplicationRequest request = StartApplicationRequest.builder() + .build(); + + StepVerifier.create(appManager.start(request)) + .verifyComplete(); + + verifyZeroInteractions(operationsApplications); + verifyNoMoreInteractions(operations); + } + + @Test + void stopNullApplication() { + StepVerifier.create(appManager.stop(null)) + .verifyComplete(); + + verifyZeroInteractions(operationsApplications); + verifyNoMoreInteractions(operations); + } + + @Test + void stopApplication() { + setupStubs(); + + when(operationsApplications.stop(any(org.cloudfoundry.operations.applications.StopApplicationRequest.class))) + .thenReturn(Mono.empty()); + + StopApplicationRequest request = StopApplicationRequest.builder() + .name("my-foo-app") + .build(); + + StepVerifier.create(appManager.stop(request)) + .verifyComplete(); + + verify(operationsApplications).stop(argThat(req -> "my-foo-app".equals(req.getName()))); + verifyNoMoreInteractions(operations); + } + + @Test + void stopApplicationWithEmptyName() { + when(operationsUtils.getOperations(anyMap())).thenReturn(Mono.just(operations)); + + StopApplicationRequest request = StopApplicationRequest.builder() + .build(); + + StepVerifier.create(appManager.stop(request)) + .verifyComplete(); + + verifyZeroInteractions(operationsApplications); + verifyNoMoreInteractions(operations); + } + + @Test + void restartNullApplication() { + StepVerifier.create(appManager.restart(null)) + .verifyComplete(); + + verifyZeroInteractions(operationsApplications); + verifyNoMoreInteractions(operations); + } + + @Test + void restartApplication() { + setupStubs(); + + when(operationsApplications.restart(any(org.cloudfoundry.operations.applications.RestartApplicationRequest.class))) + .thenReturn(Mono.empty()); + + RestartApplicationRequest request = RestartApplicationRequest.builder() + .name("my-foo-app") + .build(); + + StepVerifier.create(appManager.restart(request)) + .verifyComplete(); + + verify(operationsApplications).restart(argThat(req -> "my-foo-app".equals(req.getName()))); + verifyNoMoreInteractions(operations); + } + + @Test + void restartApplicationWithEmptyName() { + when(operationsUtils.getOperations(anyMap())).thenReturn(Mono.just(operations)); + + RestartApplicationRequest request = RestartApplicationRequest.builder() + .build(); + + StepVerifier.create(appManager.restart(request)) + .verifyComplete(); + + verifyZeroInteractions(operationsApplications); + verifyNoMoreInteractions(operations); + } + + @Test + void restageNullApplication() { + StepVerifier.create(appManager.restage(null)) + .verifyComplete(); + + verifyZeroInteractions(operationsApplications); + verifyNoMoreInteractions(operations); + } + + @Test + void restageApplication() { + setupStubs(); + + when(operationsApplications.restage(any(org.cloudfoundry.operations.applications.RestageApplicationRequest.class))) + .thenReturn(Mono.empty()); + + RestageApplicationRequest request = RestageApplicationRequest.builder() + .name("my-foo-app") + .build(); + + StepVerifier.create(appManager.restage(request)) + .verifyComplete(); + + verify(operationsApplications).restage(argThat(req -> "my-foo-app".equals(req.getName()))); + verifyNoMoreInteractions(operations); + } + + @Test + void restageApplicationWithEmptyName() { + when(operationsUtils.getOperations(anyMap())).thenReturn(Mono.just(operations)); + + RestageApplicationRequest request = RestageApplicationRequest.builder() + .build(); + + StepVerifier.create(appManager.restage(request)) + .verifyComplete(); + + verifyZeroInteractions(operationsApplications); + verifyNoMoreInteractions(operations); + } + + private void setupStubs() { + when(operations.applications()).thenReturn(operationsApplications); + when(operationsUtils.getOperations(anyMap())).thenReturn(Mono.just(operations)); + } +} \ No newline at end of file diff --git a/spring-cloud-app-broker-deployer-cloudfoundry/src/test/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryOperationsUtilsTest.java b/spring-cloud-app-broker-deployer-cloudfoundry/src/test/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryOperationsUtilsTest.java new file mode 100644 index 00000000..4ccca89a --- /dev/null +++ b/spring-cloud-app-broker-deployer-cloudfoundry/src/test/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryOperationsUtilsTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2016-2019 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.springframework.cloud.appbroker.deployer.cloudfoundry; + +import java.util.Collections; + +import org.cloudfoundry.operations.CloudFoundryOperations; +import org.cloudfoundry.operations.DefaultCloudFoundryOperations; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +import org.springframework.cloud.appbroker.deployer.DeploymentProperties; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +class CloudFoundryOperationsUtilsTest { + + private CloudFoundryOperationsUtils operationsUtils; + + private CloudFoundryOperations operations; + + @BeforeEach + void setUp() { + this.operations = DefaultCloudFoundryOperations.builder().build(); + this.operationsUtils = new CloudFoundryOperationsUtils(operations); + } + + @Test + void getOperationsWithEmptyProperties() { + StepVerifier.create(operationsUtils.getOperations(Collections.emptyMap())) + .expectNext(operations) + .verifyComplete(); + } + + @Test + void getOperationsWithProperties() { + StepVerifier.create(operationsUtils.getOperations(Collections.singletonMap(DeploymentProperties.TARGET_PROPERTY_KEY, "foo-space1"))) + .assertNext(ops -> { + String space = (String) ReflectionTestUtils.getField(ops, "space"); + assertThat(space).isEqualTo("foo-space1"); + }) + .verifyComplete(); + } + + @Test + void getOperationsForSpace() { + StepVerifier.create(operationsUtils.getOperationsForSpace("foo-space2")) + .assertNext(ops -> { + String space = (String) ReflectionTestUtils.getField(ops, "space"); + assertThat(space).isEqualTo("foo-space2"); + }) + .verifyComplete(); + } +} \ No newline at end of file diff --git a/spring-cloud-app-broker-deployer/build.gradle b/spring-cloud-app-broker-deployer/build.gradle index c27deb5b..1d079f45 100644 --- a/spring-cloud-app-broker-deployer/build.gradle +++ b/spring-cloud-app-broker-deployer/build.gradle @@ -25,4 +25,7 @@ dependencyManagement { dependencies { compile("org.springframework:spring-core") compile("io.projectreactor:reactor-core") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + testCompile("org.junit.jupiter:junit-jupiter-api") + testCompile("org.assertj:assertj-core") } diff --git a/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/AppDeployer.java b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/AppDeployer.java index c2586b43..3bf67b01 100644 --- a/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/AppDeployer.java +++ b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/AppDeployer.java @@ -32,6 +32,10 @@ default Mono undeploy(UndeployApplicationRequest re return Mono.empty(); } + default Mono getServiceInstance(GetServiceInstanceRequest request) { + return Mono.empty(); + } + default Mono createServiceInstance(CreateServiceInstanceRequest request) { return Mono.empty(); } diff --git a/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/GetServiceInstanceRequest.java b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/GetServiceInstanceRequest.java new file mode 100644 index 00000000..61dad63c --- /dev/null +++ b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/GetServiceInstanceRequest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2016-2019 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.springframework.cloud.appbroker.deployer; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.util.CollectionUtils; + +public class GetServiceInstanceRequest { + + private final String name; + + private final String serviceInstanceId; + + private final Map properties; + + GetServiceInstanceRequest(String name, String serviceInstanceId, Map properties) { + this.name = name; + this.serviceInstanceId = serviceInstanceId; + this.properties = properties; + } + + public static GetServiceInstanceRequestBuilder builder() { + return new GetServiceInstanceRequestBuilder(); + } + + public String getName() { + return name; + } + + public String getServiceInstanceId() { + return serviceInstanceId; + } + + public Map getProperties() { + return properties; + } + + public static class GetServiceInstanceRequestBuilder { + + private String name; + + private String serviceInstanceId; + + private final Map properties = new HashMap<>(); + + GetServiceInstanceRequestBuilder() { + } + + public GetServiceInstanceRequestBuilder name(String name) { + this.name = name; + return this; + } + + public GetServiceInstanceRequestBuilder serviceInstanceId(String serviceInstanceId) { + this.serviceInstanceId = serviceInstanceId; + return this; + } + + public GetServiceInstanceRequestBuilder properties(Map properties) { + if (!CollectionUtils.isEmpty(properties)) { + this.properties.putAll(properties); + } + return this; + } + + public GetServiceInstanceRequest build() { + return new GetServiceInstanceRequest(name, serviceInstanceId, properties); + } + + } +} diff --git a/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/GetServiceInstanceResponse.java b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/GetServiceInstanceResponse.java new file mode 100644 index 00000000..5aa031b7 --- /dev/null +++ b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/GetServiceInstanceResponse.java @@ -0,0 +1,80 @@ +/* + * Copyright 2016-2019 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.springframework.cloud.appbroker.deployer; + +public class GetServiceInstanceResponse { + + private final String name; + + private final String service; + + private final String plan; + + GetServiceInstanceResponse(String name, String service, String plan) { + this.name = name; + this.service = service; + this.plan = plan; + } + + public static CreateServiceInstanceRequestBuilder builder() { + return new CreateServiceInstanceRequestBuilder(); + } + + public String getName() { + return name; + } + + public String getService() { + return service; + } + + public String getPlan() { + return plan; + } + + public static class CreateServiceInstanceRequestBuilder { + + private String name; + + private String service; + + private String plan; + + CreateServiceInstanceRequestBuilder() { + } + + public CreateServiceInstanceRequestBuilder name(String name) { + this.name = name; + return this; + } + + public CreateServiceInstanceRequestBuilder service(String service) { + this.service = service; + return this; + } + + public CreateServiceInstanceRequestBuilder plan(String plan) { + this.plan = plan; + return this; + } + + public GetServiceInstanceResponse build() { + return new GetServiceInstanceResponse(name, service, plan); + } + } + +} diff --git a/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/manager/AppManager.java b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/manager/AppManager.java new file mode 100644 index 00000000..5b053a68 --- /dev/null +++ b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/manager/AppManager.java @@ -0,0 +1,38 @@ +/* + * Copyright 2016-2019 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.springframework.cloud.appbroker.manager; + +import reactor.core.publisher.Mono; + +public interface AppManager { + + default Mono start(StartApplicationRequest request) { + return Mono.empty(); + } + + default Mono stop(StopApplicationRequest request) { + return Mono.empty(); + } + + default Mono restart(RestartApplicationRequest request) { + return Mono.empty(); + } + + default Mono restage(RestageApplicationRequest request) { + return Mono.empty(); + } +} diff --git a/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/manager/RestageApplicationRequest.java b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/manager/RestageApplicationRequest.java new file mode 100644 index 00000000..92ef9c88 --- /dev/null +++ b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/manager/RestageApplicationRequest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2016-2019 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.springframework.cloud.appbroker.manager; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.util.CollectionUtils; + +public class RestageApplicationRequest { + + private final String name; + + private final Map properties; + + RestageApplicationRequest(String name, Map properties) { + this.name = name; + this.properties = properties; + } + + public String getName() { + return name; + } + + public Map getProperties() { + return properties; + } + + public static RestageApplicationRequestBuilder builder() { + return new RestageApplicationRequestBuilder(); + } + + public static class RestageApplicationRequestBuilder { + + private String name; + + private final Map properties = new HashMap<>(); + + RestageApplicationRequestBuilder() { + } + + public RestageApplicationRequestBuilder name(String name) { + this.name = name; + return this; + } + + public RestageApplicationRequestBuilder properties(Map properties) { + if (!CollectionUtils.isEmpty(properties)) { + this.properties.putAll(properties); + } + return this; + } + + public RestageApplicationRequest build() { + return new RestageApplicationRequest(name, properties); + } + + } +} diff --git a/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/manager/RestartApplicationRequest.java b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/manager/RestartApplicationRequest.java new file mode 100644 index 00000000..3da05747 --- /dev/null +++ b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/manager/RestartApplicationRequest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2016-2019 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.springframework.cloud.appbroker.manager; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.util.CollectionUtils; + +public class RestartApplicationRequest { + + private final String name; + + private final Map properties; + + RestartApplicationRequest(String name, Map properties) { + this.name = name; + this.properties = properties; + } + + public String getName() { + return name; + } + + public Map getProperties() { + return properties; + } + + public static RestartApplicationRequestBuilder builder() { + return new RestartApplicationRequestBuilder(); + } + + public static class RestartApplicationRequestBuilder { + + private String name; + + private final Map properties = new HashMap<>(); + + RestartApplicationRequestBuilder() { + } + + public RestartApplicationRequestBuilder name(String name) { + this.name = name; + return this; + } + + public RestartApplicationRequestBuilder properties(Map properties) { + if (!CollectionUtils.isEmpty(properties)) { + this.properties.putAll(properties); + } + return this; + } + + public RestartApplicationRequest build() { + return new RestartApplicationRequest(name, properties); + } + + } +} diff --git a/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/manager/StartApplicationRequest.java b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/manager/StartApplicationRequest.java new file mode 100644 index 00000000..fbf2c9e2 --- /dev/null +++ b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/manager/StartApplicationRequest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2016-2019 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.springframework.cloud.appbroker.manager; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.util.CollectionUtils; + +public class StartApplicationRequest { + + private final String name; + + private final Map properties; + + StartApplicationRequest(String name, Map properties) { + this.name = name; + this.properties = properties; + } + + public String getName() { + return name; + } + + public Map getProperties() { + return properties; + } + + public static StartApplicationRequestBuilder builder() { + return new StartApplicationRequestBuilder(); + } + + public static class StartApplicationRequestBuilder { + + private String name; + + private final Map properties = new HashMap<>(); + + StartApplicationRequestBuilder() { + } + + public StartApplicationRequestBuilder name(String name) { + this.name = name; + return this; + } + + public StartApplicationRequestBuilder properties(Map properties) { + if (!CollectionUtils.isEmpty(properties)) { + this.properties.putAll(properties); + } + return this; + } + + public StartApplicationRequest build() { + return new StartApplicationRequest(name, properties); + } + + } +} diff --git a/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/manager/StopApplicationRequest.java b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/manager/StopApplicationRequest.java new file mode 100644 index 00000000..c382ea44 --- /dev/null +++ b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/manager/StopApplicationRequest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2016-2019 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.springframework.cloud.appbroker.manager; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.util.CollectionUtils; + +public class StopApplicationRequest { + + private final String name; + + private final Map properties; + + StopApplicationRequest(String name, Map properties) { + this.name = name; + this.properties = properties; + } + + public String getName() { + return name; + } + + public Map getProperties() { + return properties; + } + + public static StopApplicationRequestBuilder builder() { + return new StopApplicationRequestBuilder(); + } + + public static class StopApplicationRequestBuilder { + + private String name; + + private final Map properties = new HashMap<>(); + + StopApplicationRequestBuilder() { + } + + public StopApplicationRequestBuilder name(String name) { + this.name = name; + return this; + } + + public StopApplicationRequestBuilder properties(Map properties) { + if (!CollectionUtils.isEmpty(properties)) { + this.properties.putAll(properties); + } + return this; + } + + public StopApplicationRequest build() { + return new StopApplicationRequest(name, properties); + } + + } +} diff --git a/spring-cloud-app-broker-deployer/src/test/java/org/springframework/cloud/appbroker/deployer/GetServiceInstanceRequestTest.java b/spring-cloud-app-broker-deployer/src/test/java/org/springframework/cloud/appbroker/deployer/GetServiceInstanceRequestTest.java new file mode 100644 index 00000000..cdbb3fe4 --- /dev/null +++ b/spring-cloud-app-broker-deployer/src/test/java/org/springframework/cloud/appbroker/deployer/GetServiceInstanceRequestTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2016-2019 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.springframework.cloud.appbroker.deployer; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.MapEntry.entry; + +class GetServiceInstanceRequestTest { + + @Test + void builderWithNoValues() { + GetServiceInstanceRequest request = GetServiceInstanceRequest.builder() + .build(); + assertThat(request).isNotNull(); + assertThat(request.getName()).isNull(); + assertThat(request.getServiceInstanceId()).isNull(); + assertThat(request.getProperties()).isEmpty(); + } + + @Test + void builderWithValues() { + GetServiceInstanceRequest request = GetServiceInstanceRequest.builder() + .name("foo-name") + .serviceInstanceId("foo-id") + .properties(Collections.singletonMap("foo", "bar")) + .build(); + assertThat(request).isNotNull(); + assertThat(request.getName()).isEqualTo("foo-name"); + assertThat(request.getServiceInstanceId()).isEqualTo("foo-id"); + assertThat(request.getProperties()).containsOnly(entry("foo", "bar")); + } + + @Test + void builderAcceptsNullProperties() { + GetServiceInstanceRequest request = GetServiceInstanceRequest.builder() + .properties(null) + .build(); + assertThat(request).isNotNull(); + assertThat(request.getProperties()).isEmpty(); + } + + @Test + void builderAcceptsEmptyProperties() { + GetServiceInstanceRequest request = GetServiceInstanceRequest.builder() + .properties(Collections.emptyMap()) + .build(); + assertThat(request).isNotNull(); + assertThat(request.getProperties()).isEmpty(); + } + +} \ No newline at end of file diff --git a/spring-cloud-app-broker-deployer/src/test/java/org/springframework/cloud/appbroker/manager/RestageApplicationRequestTest.java b/spring-cloud-app-broker-deployer/src/test/java/org/springframework/cloud/appbroker/manager/RestageApplicationRequestTest.java new file mode 100644 index 00000000..7738bdd1 --- /dev/null +++ b/spring-cloud-app-broker-deployer/src/test/java/org/springframework/cloud/appbroker/manager/RestageApplicationRequestTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2016-2019 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.springframework.cloud.appbroker.manager; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.MapEntry.entry; + +class RestageApplicationRequestTest { + + @Test + void builderWithNoValues() { + RestageApplicationRequest request = RestageApplicationRequest.builder() + .build(); + assertThat(request).isNotNull(); + assertThat(request.getName()).isNull(); + assertThat(request.getProperties()).isEmpty(); + } + + @Test + void builderWithValues() { + RestageApplicationRequest request = RestageApplicationRequest.builder() + .name("foo") + .properties(Collections.singletonMap("foo", "bar")) + .build(); + assertThat(request).isNotNull(); + assertThat(request.getName()).isEqualTo("foo"); + assertThat(request.getProperties()).containsOnly(entry("foo", "bar")); + } + + @Test + void builderAcceptsNullProperties() { + RestageApplicationRequest request = RestageApplicationRequest.builder() + .name("foo") + .properties(null) + .build(); + assertThat(request).isNotNull(); + assertThat(request.getName()).isEqualTo("foo"); + assertThat(request.getProperties()).isEmpty(); + } + + @Test + void builderAcceptsEmptyProperties() { + RestageApplicationRequest request = RestageApplicationRequest.builder() + .name("foo") + .properties(Collections.emptyMap()) + .build(); + assertThat(request).isNotNull(); + assertThat(request.getName()).isEqualTo("foo"); + assertThat(request.getProperties()).isEmpty(); + } +} \ No newline at end of file diff --git a/spring-cloud-app-broker-deployer/src/test/java/org/springframework/cloud/appbroker/manager/RestartApplicationRequestTest.java b/spring-cloud-app-broker-deployer/src/test/java/org/springframework/cloud/appbroker/manager/RestartApplicationRequestTest.java new file mode 100644 index 00000000..851c12e1 --- /dev/null +++ b/spring-cloud-app-broker-deployer/src/test/java/org/springframework/cloud/appbroker/manager/RestartApplicationRequestTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2016-2019 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.springframework.cloud.appbroker.manager; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.MapEntry.entry; + +class RestartApplicationRequestTest { + + @Test + void builderWithNoValues() { + RestartApplicationRequest request = RestartApplicationRequest.builder() + .build(); + assertThat(request).isNotNull(); + assertThat(request.getName()).isNull(); + assertThat(request.getProperties()).isEmpty(); + } + + @Test + void builderWithValues() { + RestartApplicationRequest request = RestartApplicationRequest.builder() + .name("foo") + .properties(Collections.singletonMap("foo", "bar")) + .build(); + assertThat(request).isNotNull(); + assertThat(request.getName()).isEqualTo("foo"); + assertThat(request.getProperties()).containsOnly(entry("foo", "bar")); + } + + @Test + void builderAcceptsNullProperties() { + RestartApplicationRequest request = RestartApplicationRequest.builder() + .name("foo") + .properties(null) + .build(); + assertThat(request).isNotNull(); + assertThat(request.getName()).isEqualTo("foo"); + assertThat(request.getProperties()).isEmpty(); + } + + @Test + void builderAcceptsEmptyProperties() { + RestartApplicationRequest request = RestartApplicationRequest.builder() + .name("foo") + .properties(Collections.emptyMap()) + .build(); + assertThat(request).isNotNull(); + assertThat(request.getName()).isEqualTo("foo"); + assertThat(request.getProperties()).isEmpty(); + } +} \ No newline at end of file diff --git a/spring-cloud-app-broker-deployer/src/test/java/org/springframework/cloud/appbroker/manager/StartApplicationRequestTest.java b/spring-cloud-app-broker-deployer/src/test/java/org/springframework/cloud/appbroker/manager/StartApplicationRequestTest.java new file mode 100644 index 00000000..f9f395d4 --- /dev/null +++ b/spring-cloud-app-broker-deployer/src/test/java/org/springframework/cloud/appbroker/manager/StartApplicationRequestTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2016-2019 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.springframework.cloud.appbroker.manager; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.MapEntry.entry; + +class StartApplicationRequestTest { + + @Test + void builderWithNoValues() { + StartApplicationRequest request = StartApplicationRequest.builder() + .build(); + assertThat(request).isNotNull(); + assertThat(request.getName()).isNull(); + assertThat(request.getProperties()).isEmpty(); + } + + @Test + void builderWithValues() { + StartApplicationRequest request = StartApplicationRequest.builder() + .name("foo") + .properties(Collections.singletonMap("foo", "bar")) + .build(); + assertThat(request).isNotNull(); + assertThat(request.getName()).isEqualTo("foo"); + assertThat(request.getProperties()).containsOnly(entry("foo", "bar")); + } + + @Test + void builderAcceptsNullProperties() { + StartApplicationRequest request = StartApplicationRequest.builder() + .name("foo") + .properties(null) + .build(); + assertThat(request).isNotNull(); + assertThat(request.getName()).isEqualTo("foo"); + assertThat(request.getProperties()).isEmpty(); + } + + @Test + void builderAcceptsEmptyProperties() { + StartApplicationRequest request = StartApplicationRequest.builder() + .name("foo") + .properties(Collections.emptyMap()) + .build(); + assertThat(request).isNotNull(); + assertThat(request.getName()).isEqualTo("foo"); + assertThat(request.getProperties()).isEmpty(); + } +} \ No newline at end of file diff --git a/spring-cloud-app-broker-deployer/src/test/java/org/springframework/cloud/appbroker/manager/StopApplicationRequestTest.java b/spring-cloud-app-broker-deployer/src/test/java/org/springframework/cloud/appbroker/manager/StopApplicationRequestTest.java new file mode 100644 index 00000000..5cb73372 --- /dev/null +++ b/spring-cloud-app-broker-deployer/src/test/java/org/springframework/cloud/appbroker/manager/StopApplicationRequestTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2016-2019 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.springframework.cloud.appbroker.manager; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.MapEntry.entry; + +class StopApplicationRequestTest { + + @Test + void builderWithNoValues() { + StopApplicationRequest request = StopApplicationRequest.builder() + .build(); + assertThat(request).isNotNull(); + assertThat(request.getName()).isNull(); + assertThat(request.getProperties()).isEmpty(); + } + + @Test + void builderWithValues() { + StopApplicationRequest request = StopApplicationRequest.builder() + .name("foo") + .properties(Collections.singletonMap("foo", "bar")) + .build(); + assertThat(request).isNotNull(); + assertThat(request.getName()).isEqualTo("foo"); + assertThat(request.getProperties()).containsOnly(entry("foo", "bar")); + } + + @Test + void builderAcceptsNullProperties() { + StopApplicationRequest request = StopApplicationRequest.builder() + .name("foo") + .properties(null) + .build(); + assertThat(request).isNotNull(); + assertThat(request.getName()).isEqualTo("foo"); + assertThat(request.getProperties()).isEmpty(); + } + + @Test + void builderAcceptsEmptyProperties() { + StopApplicationRequest request = StopApplicationRequest.builder() + .name("foo") + .properties(Collections.emptyMap()) + .build(); + assertThat(request).isNotNull(); + assertThat(request.getName()).isEqualTo("foo"); + assertThat(request.getProperties()).isEmpty(); + } +} \ No newline at end of file