Skip to content

Commit

Permalink
Add support to manage backing apps via a service
Browse files Browse the repository at this point in the history
This commit adds support to stop, start, restart, and restage the backing
applications associated with a deployed service instance. The management
functions are available from the new `BackingAppManagementService`.

Resolves #118
  • Loading branch information
royclarkson committed Mar 15, 2019
1 parent 0bc328e commit 0cf2e0e
Show file tree
Hide file tree
Showing 37 changed files with 2,676 additions and 129 deletions.
8 changes: 4 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions spring-cloud-app-broker-acceptance-tests/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion spring-cloud-app-broker-acceptance-tests/build.gradle
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> startApplications(@PathVariable String serviceInstanceId) {
return service.start(serviceInstanceId)
.thenReturn("starting " + serviceInstanceId);
}

@GetMapping("/stop/{serviceInstanceId}")
public Mono<String> stopApplications(@PathVariable String serviceInstanceId) {
return service.stop(serviceInstanceId)
.thenReturn("stopping " + serviceInstanceId);
}

@GetMapping("/restart/{serviceInstanceId}")
public Mono<String> restartApplications(@PathVariable String serviceInstanceId) {
return service.restart(serviceInstanceId)
.thenReturn("restarting " + serviceInstanceId);
}

@GetMapping("/restage/{serviceInstanceId}")
public Mono<String> restageApplications(@PathVariable String serviceInstanceId) {
return service.restage(serviceInstanceId)
.thenReturn("restaging " + serviceInstanceId);
}
}
Original file line number Diff line number Diff line change
@@ -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<ApplicationDetail> 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<ApplicationDetail> 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<ApplicationDetail> 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<ApplicationDetail> 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<List<ApplicationDetail>> getApplications() {
return Flux.merge(cloudFoundryService.getApplication(APP_1),
cloudFoundryService.getApplication(APP_2))
.parallel()
.runOn(Schedulers.parallel())
.sequential()
.collectList();
}

private Mono<String> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand Down Expand Up @@ -201,7 +201,7 @@ Optional<ApplicationSummary> getApplicationSummary(String appName) {
}

Optional<ApplicationSummary> getApplicationSummary(String appName, String space) {
return cloudFoundryService.getApplicationSummary(appName, space).blockOptional();
return cloudFoundryService.getApplication(appName, space).blockOptional();
}

ApplicationEnvironments getApplicationEnvironment(String appName) {
Expand All @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -93,7 +94,7 @@ public Mono<Void> createServiceBroker(String brokerName, String testBrokerAppNam
.doOnError(error -> LOGGER.error("Error creating service broker " + brokerName + ": " + error)));
}

private Mono<String> getApplicationRoute(String appName) {
public Mono<String> getApplicationRoute(String appName) {
return cloudFoundryOperations.applications()
.get(GetApplicationRequest.builder()
.name(appName)
Expand Down Expand Up @@ -203,7 +204,13 @@ public Mono<List<ApplicationSummary>> getApplications() {
.collectList();
}

public Mono<ApplicationSummary> getApplicationSummary(String appName, String space) {
public Mono<ApplicationDetail> getApplication(String appName) {
return cloudFoundryOperations.applications().get(GetApplicationRequest.builder()
.name(appName)
.build());
}

public Mono<ApplicationSummary> getApplication(String appName, String space) {
return listApplications(createOperationsForSpace(space))
.filter(applicationSummary -> applicationSummary.getName().equals(appName))
.single();
Expand Down Expand Up @@ -233,6 +240,12 @@ private Mono<ApplicationEnvironments> getApplicationEnvironment(CloudFoundryOper
.doOnError(error -> LOGGER.error("Error getting environment for application " + appName + ": " + error));
}

public Mono<Void> stopApplication(String appName) {
return cloudFoundryOperations.applications().stop(StopApplicationRequest.builder()
.name(appName)
.build());
}

public Mono<List<String>> getSpaces() {
return cloudFoundryOperations.spaces()
.list()
Expand Down
Loading

0 comments on commit 0cf2e0e

Please sign in to comment.