UpdateServiceInstance creates/binds new backing services and unbinds/deletes old backing services.

To keep the behavior consisten, also updated delete flow to delete backing
services that the backing app are actually bound to.

As part of making the new AT pass, moved delete backing services step to
before deleting backing app.

Connects to #293
This commit is contained in:
Yuxin Bai
2020-01-27 13:33:47 -05:00
committed by Roy Clarkson
parent 52d9051a08
commit b0f2cbc5f4
47 changed files with 1618 additions and 184 deletions

View File

@@ -10,4 +10,5 @@
logging:
level:
cloudfoundry-client: DEBUG
cloudfoundry-client: DEBUG
org.springframework.cloud.appbroker: DEBUG

View File

@@ -47,10 +47,12 @@ import org.cloudfoundry.operations.applications.ApplicationEnvironments;
import org.cloudfoundry.operations.applications.ApplicationSummary;
import org.cloudfoundry.operations.organizations.OrganizationSummary;
import org.cloudfoundry.operations.services.ServiceInstance;
import org.cloudfoundry.operations.services.ServiceInstanceSummary;
import org.cloudfoundry.operations.spaces.SpaceSummary;
import org.cloudfoundry.uaa.clients.GetClientResponse;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -89,6 +91,14 @@ abstract class CloudFoundryAcceptanceTest {
private static final Logger LOG = LoggerFactory.getLogger(CloudFoundryAcceptanceTest.class);
private static final String BACKING_SERVICE_PLAN_ID = UUID.randomUUID().toString();
private static final String SERVICE_ID = UUID.randomUUID().toString();
private static final String PLAN_ID = UUID.randomUUID().toString();
private static final String BACKING_SERVICE_ID = UUID.randomUUID().toString();
protected static final String PLAN_NAME = "standard";
protected static final String BACKING_APP_PATH = "classpath:backing-app.jar";
@@ -123,22 +133,32 @@ abstract class CloudFoundryAcceptanceTest {
}
@BeforeEach
void setUp(BrokerProperties brokerProperties) {
void setUp(TestInfo testInfo, BrokerProperties brokerProperties) {
List<String> appBrokerProperties = getAppBrokerProperties(brokerProperties);
blockingSubscribe(initializeBroker(appBrokerProperties));
}
void setUpForBrokerUpdate(BrokerProperties brokerProperties) {
List<String> appBrokerProperties = getAppBrokerProperties(brokerProperties);
blockingSubscribe(updateBroker(appBrokerProperties));
}
private List<String> getAppBrokerProperties(BrokerProperties brokerProperties) {
String[] openServiceBrokerProperties = {
"spring.cloud.openservicebroker.catalog.services[0].id=" + UUID.randomUUID().toString(),
"spring.cloud.openservicebroker.catalog.services[0].id=" + SERVICE_ID,
"spring.cloud.openservicebroker.catalog.services[0].name=" + appServiceName(),
"spring.cloud.openservicebroker.catalog.services[0].description=A service that deploys a backing app",
"spring.cloud.openservicebroker.catalog.services[0].bindable=true",
"spring.cloud.openservicebroker.catalog.services[0].plans[0].id=" + UUID.randomUUID().toString(),
"spring.cloud.openservicebroker.catalog.services[0].plans[0].id=" + PLAN_ID,
"spring.cloud.openservicebroker.catalog.services[0].plans[0].name=standard",
"spring.cloud.openservicebroker.catalog.services[0].plans[0].bindable=true",
"spring.cloud.openservicebroker.catalog.services[0].plans[0].description=A simple plan",
"spring.cloud.openservicebroker.catalog.services[0].plans[0].free=true",
"spring.cloud.openservicebroker.catalog.services[1].id=" + UUID.randomUUID().toString(),
"spring.cloud.openservicebroker.catalog.services[1].id=" + BACKING_SERVICE_ID,
"spring.cloud.openservicebroker.catalog.services[1].name=" + backingServiceName(),
"spring.cloud.openservicebroker.catalog.services[1].description=A backing service that can be bound to backing apps",
"spring.cloud.openservicebroker.catalog.services[1].bindable=true",
"spring.cloud.openservicebroker.catalog.services[1].plans[0].id=" + UUID.randomUUID().toString(),
"spring.cloud.openservicebroker.catalog.services[1].plans[0].id=" + BACKING_SERVICE_PLAN_ID,
"spring.cloud.openservicebroker.catalog.services[1].plans[0].name=standard",
"spring.cloud.openservicebroker.catalog.services[1].plans[0].bindable=true",
"spring.cloud.openservicebroker.catalog.services[1].plans[0].description=A simple plan",
@@ -148,8 +168,7 @@ abstract class CloudFoundryAcceptanceTest {
List<String> appBrokerProperties = new ArrayList<>();
appBrokerProperties.addAll(Arrays.asList(openServiceBrokerProperties));
appBrokerProperties.addAll(brokerProperties.getProperties());
blockingSubscribe(initializeBroker(appBrokerProperties));
return appBrokerProperties;
}
@BeforeEach
@@ -177,7 +196,7 @@ abstract class CloudFoundryAcceptanceTest {
}
@AfterEach
public void tearDown() {
public void tearDown(TestInfo testInfo) {
blockingSubscribe(cloudFoundryService.getOrCreateDefaultOrganization()
.map(OrganizationSummary::getId)
.flatMap(orgId -> cloudFoundryService.getOrCreateDefaultSpace()
@@ -206,6 +225,12 @@ abstract class CloudFoundryAcceptanceTest {
.then(cloudFoundryService.enableServiceBrokerAccess(backingServiceName()))));
}
private Mono<Void> updateBroker(List<String> appBrokerProperties) {
return cloudFoundryService
.pushBrokerApp(testBrokerAppName(), getTestBrokerAppPath(), brokerClientId(), appBrokerProperties)
.then(cloudFoundryService.updateServiceBroker(serviceBrokerName(), testBrokerAppName()));
}
private Mono<Void> cleanup(String orgId, String spaceId) {
return cloudFoundryService.deleteServiceBroker(serviceBrokerName())
.then(cloudFoundryService.deleteApp(testBrokerAppName()))
@@ -252,6 +277,13 @@ abstract class CloudFoundryAcceptanceTest {
blockingSubscribe(cloudFoundryService.deleteServiceInstance(serviceInstanceName));
}
protected List<String> listServiceInstances() {
return cloudFoundryService.listServiceInstances()
.map(ServiceInstanceSummary::getName)
.collectList()
.block();
}
protected ServiceInstance getServiceInstance(String serviceInstanceName) {
return getServiceInstanceMono(serviceInstanceName).block();
}

View File

@@ -84,7 +84,7 @@ class CreateInstanceAcceptanceTest extends CloudFoundryAcceptanceTest {
Optional<ApplicationSummary> backingApplication1 = getApplicationSummary(APP_CREATE_1);
assertThat(backingApplication1).hasValueSatisfying(app -> {
assertThat(app.getInstances()).isEqualTo(2);
assertThat(app.getRunningInstances()).isEqualTo(2);
assertThat(app.getRunningInstances()).isGreaterThanOrEqualTo(1);
assertThat(app.getMemoryLimit()).isEqualTo(2048);
});

View File

@@ -87,6 +87,9 @@ class CreateInstanceWithServiceInstanceGuidSuffixTargetAcceptanceTest extends Cl
// when the service instance is deleted
deleteServiceInstance(SI_NAME);
// and the backing service is deleted
assertThat(listServiceInstances()).doesNotContain(expectedServiceInstanceName);
}
}

View File

@@ -90,11 +90,16 @@ class CreateInstanceWithServicesAcceptanceTest extends CloudFoundryAcceptanceTes
// when the service instance is deleted
deleteServiceInstance(SI_NAME);
// service has no applications bound to it
ServiceInstance serviceInstance2AfterDeletion = getServiceInstance(BACKING_SI_2_NAME);
assertThat(serviceInstance2AfterDeletion.getApplications()).isEmpty();
// and the backing services are deleted
assertThat(listServiceInstances()).doesNotContain(BACKING_SI_1_NAME);
assertThat(listServiceInstances()).doesNotContain(BACKING_SI_2_NAME);
deleteServiceInstance(BACKING_SI_2_NAME);
// TODO: another story to only remove the instances with service definition specified (https://github.com/spring-cloud/spring-cloud-app-broker/issues/316)
// // service without specification has no applications bound to it
// ServiceInstance serviceInstance2AfterDeletion = getServiceInstance(BACKING_SI_2_NAME);
// assertThat(serviceInstance2AfterDeletion.getApplications()).isEmpty();
//
// deleteServiceInstance(BACKING_SI_2_NAME);
}
}

View File

@@ -0,0 +1,171 @@
/*
* Copyright 2002-2020 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
*
* https://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 java.util.Collections;
import java.util.Optional;
import org.cloudfoundry.operations.applications.ApplicationSummary;
import org.cloudfoundry.operations.services.ServiceInstance;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.annotation.Autowired;
import static org.assertj.core.api.Assertions.assertThat;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class UpdateInstanceWithNewServiceAcceptanceTest extends CloudFoundryAcceptanceTest {
private static final String APP_NAME = "app-update-with-new-services";
private static final int FIRST_TEST = 1;
private static final int SECOND_TEST = 2;
private static final String SI_NAME = "si-update-with-new-services";
private static final String OLD_BACKING_SI_NAME = "backing-service-instance-old";
private static final String NEW_BACKING_SI_NAME = "backing-service-instance-new";
private static final String SUFFIX = "update-with-new-services";
private static final String APP_SERVICE_NAME = "app-service-" + SUFFIX;
private static final String BACKING_SERVICE_NAME = "backing-service-" + SUFFIX;
@Autowired
private HealthListener healthListener;
@Override
protected String testSuffix() {
return SUFFIX;
}
@Override
protected String appServiceName() {
return APP_SERVICE_NAME;
}
@Override
protected String backingServiceName() {
return BACKING_SERVICE_NAME;
}
@Test
@Tag("first")
@Order(FIRST_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_NAME,
"spring.cloud.appbroker.services[0].apps[0].path=" + BACKING_APP_PATH,
"spring.cloud.appbroker.services[0].apps[0].services[0].service-instance-name=" + OLD_BACKING_SI_NAME,
"spring.cloud.appbroker.services[0].services[0].name=" + BACKING_SERVICE_NAME,
"spring.cloud.appbroker.services[0].services[0].plan=" + PLAN_NAME,
"spring.cloud.appbroker.services[0].services[0].service-instance-name=" + OLD_BACKING_SI_NAME
})
void weCreateAService() {
// when a service instance is created
createServiceInstance(SI_NAME);
// then a backing application is deployed
Optional<ApplicationSummary> backingApplication = getApplicationSummary(APP_NAME);
assertThat(backingApplication).hasValueSatisfying(app ->
assertThat(app.getRunningInstances()).isEqualTo(1));
// and the services are bound to it
ServiceInstance backingServiceInstance = getServiceInstance(OLD_BACKING_SI_NAME);
assertThat(backingServiceInstance.getApplications()).contains(APP_NAME);
String path = backingApplication.get().getUrls().get(0);
healthListener.start(path);
}
@Test
@Order(SECOND_TEST)
@Tag("last")
@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_NAME,
"spring.cloud.appbroker.services[0].apps[0].path=" + BACKING_APP_PATH,
"spring.cloud.appbroker.services[0].apps[0].services[0].service-instance-name=" + NEW_BACKING_SI_NAME,
"spring.cloud.appbroker.services[0].services[0].name=" + BACKING_SERVICE_NAME,
"spring.cloud.appbroker.services[0].services[0].plan=" + PLAN_NAME,
"spring.cloud.appbroker.services[0].services[0].service-instance-name=" + NEW_BACKING_SI_NAME
})
void weUpdateTheServiceInstanceWithANewBackingService() {
// when the service instance is updated with a new service
updateServiceInstance(SI_NAME, Collections.emptyMap());
// then the backing application was updated with zero downtime
healthListener.stop();
assertThat(healthListener.getFailures()).isEqualTo(0);
assertThat(healthListener.getSuccesses()).isGreaterThan(0);
// then a backing application is re-deployed
Optional<ApplicationSummary> updatedBackingApplication = getApplicationSummary(APP_NAME);
assertThat(updatedBackingApplication).hasValueSatisfying(app ->
assertThat(app.getRunningInstances()).isEqualTo(1));
// and the new backing service is bound to it
ServiceInstance newBackingServiceInstance = getServiceInstance(NEW_BACKING_SI_NAME);
assertThat(newBackingServiceInstance.getApplications()).contains(APP_NAME);
// and the old backing service is deleted
assertThat(listServiceInstances()).doesNotContain(OLD_BACKING_SI_NAME);
// then the service instance is deleted
deleteServiceInstance(SI_NAME);
// and the backing service is deleted
assertThat(listServiceInstances()).doesNotContain(NEW_BACKING_SI_NAME);
}
@Override
@BeforeEach
void setUp(TestInfo testInfo, BrokerProperties brokerProperties) {
if (testInfo.getTags().contains("first")) {
super.setUp(testInfo, brokerProperties);
}
else {
setUpForBrokerUpdate(brokerProperties);
}
}
@Override
@AfterEach
public void tearDown(TestInfo testInfo) {
if (testInfo.getTags().contains("last")) {
super.tearDown(testInfo);
}
}
}

View File

@@ -113,6 +113,9 @@ class UpdateInstanceWithServicesAcceptanceTest extends CloudFoundryAcceptanceTes
// then the service instance is deleted
deleteServiceInstance(SI_NAME);
// and the backing service is deleted
assertThat(listServiceInstances()).doesNotContain(BACKING_SI_NAME);
}
}

View File

@@ -52,10 +52,12 @@ import org.cloudfoundry.operations.organizations.Organizations;
import org.cloudfoundry.operations.serviceadmin.CreateServiceBrokerRequest;
import org.cloudfoundry.operations.serviceadmin.DeleteServiceBrokerRequest;
import org.cloudfoundry.operations.serviceadmin.EnableServiceAccessRequest;
import org.cloudfoundry.operations.serviceadmin.UpdateServiceBrokerRequest;
import org.cloudfoundry.operations.services.CreateServiceInstanceRequest;
import org.cloudfoundry.operations.services.DeleteServiceInstanceRequest;
import org.cloudfoundry.operations.services.GetServiceInstanceRequest;
import org.cloudfoundry.operations.services.ServiceInstance;
import org.cloudfoundry.operations.services.ServiceInstanceSummary;
import org.cloudfoundry.operations.services.UpdateServiceInstanceRequest;
import org.cloudfoundry.operations.spaces.CreateSpaceRequest;
import org.cloudfoundry.operations.spaces.SpaceSummary;
@@ -114,6 +116,19 @@ public class CloudFoundryService {
.doOnError(error -> LOGGER.error("Error creating service broker " + brokerName + ": " + error)));
}
public Mono<Void> updateServiceBroker(String brokerName, String testBrokerAppName) {
return getApplicationRoute(testBrokerAppName)
.flatMap(url -> cloudFoundryOperations.serviceAdmin()
.update(UpdateServiceBrokerRequest.builder()
.name(brokerName)
.username("user")
.password("password")
.url(url)
.build())
.doOnSuccess(item -> LOGGER.info("Updating service broker " + brokerName))
.doOnError(error -> LOGGER.error("Error updating service broker " + brokerName + ": " + error)));
}
public Mono<String> getApplicationRoute(String appName) {
return cloudFoundryOperations.applications()
.get(GetApplicationRequest.builder()
@@ -203,6 +218,10 @@ public class CloudFoundryService {
.doOnError(error -> LOGGER.error("Error updating service instance " + serviceInstanceName + ": " + error));
}
public Flux<ServiceInstanceSummary> listServiceInstances() {
return cloudFoundryOperations.services().listInstances();
}
public Mono<ServiceInstance> getServiceInstance(String serviceInstanceName) {
return getServiceInstance(cloudFoundryOperations, serviceInstanceName);
}

View File

@@ -345,6 +345,7 @@ public class AppBrokerAutoConfiguration {
*
* @param brokeredServices the BrokeredServices bean
* @param backingAppDeploymentService the BackingAppDeploymentService bean
* @param backingAppManagementService the BackingAppManagementService bean
* @param backingServicesProvisionService the BackingServicesProvisionService bean
* @param appsParametersTransformationService the BackingApplicationsParametersTransformationService bean
* @param servicesParametersTransformationService the BackingServicesParametersTransformationService bean
@@ -353,7 +354,9 @@ public class AppBrokerAutoConfiguration {
*/
@Bean
public UpdateServiceInstanceWorkflow appDeploymentUpdateServiceInstanceWorkflow(
BrokeredServices brokeredServices, BackingAppDeploymentService backingAppDeploymentService,
BrokeredServices brokeredServices,
BackingAppDeploymentService backingAppDeploymentService,
BackingAppManagementService backingAppManagementService,
BackingServicesProvisionService backingServicesProvisionService,
BackingApplicationsParametersTransformationService appsParametersTransformationService,
BackingServicesParametersTransformationService servicesParametersTransformationService,
@@ -362,6 +365,7 @@ public class AppBrokerAutoConfiguration {
return new AppDeploymentUpdateServiceInstanceWorkflow(
brokeredServices,
backingAppDeploymentService,
backingAppManagementService,
backingServicesProvisionService,
appsParametersTransformationService,
servicesParametersTransformationService,
@@ -373,6 +377,7 @@ public class AppBrokerAutoConfiguration {
*
* @param brokeredServices the BrokeredServices bean
* @param backingAppDeploymentService the BackingAppDeploymentService bean
* @param backingAppManagementService the BackingAppManagementService bean
* @param backingServicesProvisionService the BackingServicesProvisionService bean
* @param credentialProviderService the CredentialProviderService bean
* @param targetService the TargetService bean
@@ -381,13 +386,16 @@ public class AppBrokerAutoConfiguration {
@Bean
public DeleteServiceInstanceWorkflow appDeploymentDeleteServiceInstanceWorkflow(
BrokeredServices brokeredServices, BackingAppDeploymentService backingAppDeploymentService,
BackingAppManagementService backingAppManagementService,
BackingServicesProvisionService backingServicesProvisionService,
CredentialProviderService credentialProviderService, TargetService targetService) {
return new AppDeploymentDeleteServiceInstanceWorkflow(
brokeredServices,
backingAppDeploymentService,
backingServicesProvisionService, credentialProviderService,
backingAppManagementService,
backingServicesProvisionService,
credentialProviderService,
targetService
);
}

View File

@@ -16,6 +16,8 @@
package org.springframework.cloud.appbroker.deployer;
import java.util.Objects;
public class ServicesSpec {
private String serviceInstanceName;
@@ -39,6 +41,30 @@ public class ServicesSpec {
return new ServicesSpecBuilder();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ServicesSpec that = (ServicesSpec) o;
return Objects.equals(serviceInstanceName, that.serviceInstanceName);
}
@Override
public int hashCode() {
return Objects.hash(serviceInstanceName);
}
@Override
public String toString() {
return "ServicesSpec{" +
"serviceInstanceName='" + serviceInstanceName + '\'' +
'}';
}
public static final class ServicesSpecBuilder {
private String serviceInstanceName;

View File

@@ -29,7 +29,9 @@ 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.GetApplicationRequest;
import org.springframework.cloud.appbroker.deployer.GetServiceInstanceRequest;
import org.springframework.cloud.appbroker.deployer.ServicesSpec;
import org.springframework.cloud.appbroker.extensions.targets.TargetService;
public class BackingAppManagementService {
@@ -108,12 +110,42 @@ public class BackingAppManagementService {
.then();
}
private Mono<List<BackingApplication>> getBackingApplicationsForService(String serviceInstanceId) {
public Mono<BackingApplications> getDeployedBackingApplications(String serviceInstanceId) {
return getBackingApplicationsForService(serviceInstanceId)
.flatMapMany(Flux::fromIterable)
.flatMap(app ->
appDeployer
.get(GetApplicationRequest.builder()
.name(app.getName())
.properties(app.getProperties())
.build())
.flatMap(response -> Flux.fromIterable(response.getServices())
.map(serviceName ->
ServicesSpec.builder()
.serviceInstanceName(serviceName)
.build())
.collectList()
.map(services -> BackingApplication
.builder()
.name(response.getName())
.services(services)
.environment(response.getEnvironment())
.build()))
.doOnRequest(l -> log.debug("Getting deployed backing applications {}", app))
.doOnError(exception -> log.error(String.format("Error getting deployed backing application %s " +
"with error '%s'", app.getName(), exception.getMessage()), exception))
.onErrorResume(exception -> Mono.empty()))
.collectList()
.map(BackingApplications::new);
}
private Mono<BackingApplications> getBackingApplicationsForService(String serviceInstanceId) {
return appDeployer.getServiceInstance(GetServiceInstanceRequest.builder()
.serviceInstanceId(serviceInstanceId)
.build())
.flatMap(response -> findBrokeredService(response.getService(), response.getPlan()))
.flatMap(brokeredService -> updateBackingApps(brokeredService, serviceInstanceId));
.flatMap(brokeredService -> updateBackingApps(brokeredService, serviceInstanceId))
.map(backingApplications -> BackingApplications.builder().backingApplications(backingApplications).build());
}
private Mono<BrokeredService> findBrokeredService(String serviceName, String planName) {

View File

@@ -22,10 +22,12 @@ import reactor.util.Logger;
import reactor.util.Loggers;
import org.springframework.cloud.appbroker.deployer.BackingAppDeploymentService;
import org.springframework.cloud.appbroker.deployer.BackingService;
import org.springframework.cloud.appbroker.deployer.BackingServicesProvisionService;
import org.springframework.cloud.appbroker.deployer.BrokeredServices;
import org.springframework.cloud.appbroker.extensions.credentials.CredentialProviderService;
import org.springframework.cloud.appbroker.extensions.targets.TargetService;
import org.springframework.cloud.appbroker.manager.BackingAppManagementService;
import org.springframework.cloud.appbroker.service.DeleteServiceInstanceWorkflow;
import org.springframework.cloud.servicebroker.model.instance.DeleteServiceInstanceRequest;
import org.springframework.cloud.servicebroker.model.instance.DeleteServiceInstanceResponse;
@@ -41,6 +43,8 @@ public class AppDeploymentDeleteServiceInstanceWorkflow
private final BackingAppDeploymentService deploymentService;
private final BackingAppManagementService backingAppManagementService;
private final CredentialProviderService credentialProviderService;
private final TargetService targetService;
@@ -49,11 +53,13 @@ public class AppDeploymentDeleteServiceInstanceWorkflow
public AppDeploymentDeleteServiceInstanceWorkflow(BrokeredServices brokeredServices,
BackingAppDeploymentService deploymentService,
BackingAppManagementService backingAppManagementService,
BackingServicesProvisionService backingServicesProvisionService,
CredentialProviderService credentialProviderService,
TargetService targetService) {
super(brokeredServices);
this.deploymentService = deploymentService;
this.backingAppManagementService = backingAppManagementService;
this.credentialProviderService = credentialProviderService;
this.targetService = targetService;
this.backingServicesProvisionService = backingServicesProvisionService;
@@ -61,20 +67,23 @@ public class AppDeploymentDeleteServiceInstanceWorkflow
@Override
public Mono<Void> delete(DeleteServiceInstanceRequest request, DeleteServiceInstanceResponse response) {
return undeployBackingApplications(request)
.thenMany(deleteBackingServices(request))
return deleteBackingServices(request)
.thenMany(undeployBackingApplications(request))
.then();
}
private Flux<String> deleteBackingServices(DeleteServiceInstanceRequest request) {
return getBackingServicesForService(request.getServiceDefinition(), request.getPlan())
.flatMapMany(backingServices ->
targetService.addToBackingServices(backingServices,
getTargetForService(request.getServiceDefinition(), request.getPlan()),
request.getServiceInstanceId()))
return backingAppManagementService.getDeployedBackingApplications(request.getServiceInstanceId())
.flatMapMany(Flux::fromIterable)
.flatMap(backingApplication ->
Flux.fromIterable(backingApplication.getServices())
.map(servicesSpec -> BackingService.builder()
.serviceInstanceName(servicesSpec.getServiceInstanceName())
.build())
.collectList())
.doOnEach(backingServices -> log.debug("Deleting backing services {} for {}/{}",
backingServices, request.getServiceDefinition().getName(), request.getPlan().getName()))
.flatMap(backingServicesProvisionService::deleteServiceInstance)
.doOnRequest(l -> log.debug("Deleting backing services for{}/{}",
request.getServiceDefinition().getName(), request.getPlan().getName()))
.doOnComplete(() -> log.debug("Finished deleting backing services for {}/{}",
request.getServiceDefinition().getName(), request.getPlan().getName()))
.doOnError(exception -> log.error(String.format("Error deleting backing services for %s/%s with error '%s'",

View File

@@ -16,17 +16,28 @@
package org.springframework.cloud.appbroker.workflow.instance;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
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.BackingAppDeploymentService;
import org.springframework.cloud.appbroker.deployer.BackingApplication;
import org.springframework.cloud.appbroker.deployer.BackingService;
import org.springframework.cloud.appbroker.deployer.BackingServicesProvisionService;
import org.springframework.cloud.appbroker.deployer.BrokeredServices;
import org.springframework.cloud.appbroker.extensions.parameters.BackingApplicationsParametersTransformationService;
import org.springframework.cloud.appbroker.extensions.parameters.BackingServicesParametersTransformationService;
import org.springframework.cloud.appbroker.extensions.targets.TargetService;
import org.springframework.cloud.appbroker.manager.BackingAppManagementService;
import org.springframework.cloud.appbroker.service.UpdateServiceInstanceWorkflow;
import org.springframework.cloud.servicebroker.model.instance.UpdateServiceInstanceRequest;
import org.springframework.cloud.servicebroker.model.instance.UpdateServiceInstanceResponse;
@@ -41,6 +52,8 @@ public class AppDeploymentUpdateServiceInstanceWorkflow extends AppDeploymentIns
private final BackingAppDeploymentService deploymentService;
private final BackingAppManagementService backingAppManagementService;
private final BackingServicesProvisionService backingServicesProvisionService;
private final BackingApplicationsParametersTransformationService appsParametersTransformationService;
@@ -51,12 +64,14 @@ public class AppDeploymentUpdateServiceInstanceWorkflow extends AppDeploymentIns
public AppDeploymentUpdateServiceInstanceWorkflow(BrokeredServices brokeredServices,
BackingAppDeploymentService deploymentService,
BackingAppManagementService backingAppManagementService,
BackingServicesProvisionService backingServicesProvisionService,
BackingApplicationsParametersTransformationService appsParametersTransformationService,
BackingServicesParametersTransformationService servicesParametersTransformationService,
TargetService targetService) {
super(brokeredServices);
this.deploymentService = deploymentService;
this.backingAppManagementService = backingAppManagementService;
this.backingServicesProvisionService = backingServicesProvisionService;
this.appsParametersTransformationService = appsParametersTransformationService;
this.servicesParametersTransformationService = servicesParametersTransformationService;
@@ -79,14 +94,71 @@ public class AppDeploymentUpdateServiceInstanceWorkflow extends AppDeploymentIns
.flatMap(backingServices ->
servicesParametersTransformationService.transformParameters(backingServices,
request.getParameters()))
.flatMapMany(backingServicesProvisionService::updateServiceInstance)
.flatMap(backingServices -> Flux.fromIterable(backingServices)
.collectMap(BackingService::getServiceInstanceName, Function.identity()))
.zipWith(getExistingBackingServiceNameMap(request))
.flatMapMany(newAndExisting -> {
Map<String, BackingService> newServices = newAndExisting.getT1();
Map<String, BackingService> existingServices = newAndExisting.getT2();
Set<String> serviceNamesToUpdate = intersection(newServices.keySet(), existingServices.keySet());
Set<String> serviceNamesToCreate = subtract(newServices.keySet(), existingServices.keySet());
Set<String> serviceNamesToDelete = subtract(existingServices.keySet(), newServices.keySet());
List<BackingService> servicesToUpdate = servicesInNameList(newServices, serviceNamesToUpdate);
List<BackingService> servicesToCreate = servicesInNameList(newServices, serviceNamesToCreate);
List<BackingService> servicesToDelete = servicesInNameList(existingServices,
serviceNamesToDelete);
log.debug("Backing services to update: {}", serviceNamesToUpdate);
log.debug("Backing services to create: {}", serviceNamesToCreate);
log.debug("Backing services to delete: {}", serviceNamesToDelete);
return Flux.concat(
backingServicesProvisionService.updateServiceInstance(servicesToUpdate),
backingServicesProvisionService.createServiceInstance(servicesToCreate),
backingServicesProvisionService.deleteServiceInstance(servicesToDelete))
.parallel()
.runOn(Schedulers.parallel());
})
.doOnRequest(l -> log.debug("Updating backing services for {}/{}",
request.getServiceDefinition().getName(), request.getPlan().getName()))
.doOnComplete(() -> log.debug("Finished updating backing services for {}/{}",
request.getServiceDefinition().getName(), request.getPlan().getName()))
.doOnError(exception -> log.error(String.format("Error updating backing services for %s/%s with error '%s'",
request.getServiceDefinition().getName(), request.getPlan().getName(), exception.getMessage()),
exception));
.doOnError(
exception -> log.error(String.format("Error updating backing services for %s/%s with error '%s'",
request.getServiceDefinition().getName(), request.getPlan().getName(), exception.getMessage()),
exception));
}
private Mono<Map<String, BackingService>> getExistingBackingServiceNameMap(UpdateServiceInstanceRequest request) {
return backingAppManagementService.getDeployedBackingApplications(request.getServiceInstanceId())
.flatMapMany(Flux::fromIterable)
.map(BackingApplication::getServices)
.flatMap(Flux::fromIterable)
.distinct()
.map(servicesSpec -> BackingService.builder()
.serviceInstanceName(servicesSpec.getServiceInstanceName())
.build())
.collectMap(BackingService::getServiceInstanceName, Function.identity());
}
private List<BackingService> servicesInNameList(Map<String, BackingService> services, Set<String> nameList) {
List<BackingService> servicesToKeep = new ArrayList<>(services.values());
servicesToKeep.removeIf(service -> !nameList.contains(service.getServiceInstanceName()));
return servicesToKeep;
}
private Set<String> subtract(Set<String> set1, Set<String> set2) {
Set<String> set = new HashSet<>(set1);
set.removeAll(set2);
return set;
}
private Set<String> intersection(Set<String> set1, Set<String> set2) {
Set<String> set = new HashSet<>(set1);
set.retainAll(set2);
return set;
}
private Flux<String> updateBackingApplications(UpdateServiceInstanceRequest request) {

View File

@@ -29,13 +29,18 @@ 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.GetApplicationRequest;
import org.springframework.cloud.appbroker.deployer.GetApplicationResponse;
import org.springframework.cloud.appbroker.deployer.GetServiceInstanceRequest;
import org.springframework.cloud.appbroker.deployer.GetServiceInstanceResponse;
import org.springframework.cloud.appbroker.deployer.ServicesSpec;
import org.springframework.cloud.appbroker.extensions.targets.TargetService;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -311,6 +316,54 @@ class BackingAppManagementServiceTest {
verifyNoMoreInteractions(appDeployer, targetService, managementClient);
}
@Test
@SuppressWarnings("unchecked")
void getDeployedBackingApplications() {
given(appDeployer.getServiceInstance(any(GetServiceInstanceRequest.class)))
.willReturn(Mono.just(GetServiceInstanceResponse.builder()
.name("foo-service")
.plan("plan1")
.service("service1")
.build()));
given(appDeployer.get(any(GetApplicationRequest.class)))
.willReturn(Mono.just(GetApplicationResponse.builder()
.name("testApp1")
.service("service1")
.service("service2")
.build()),
Mono.just(GetApplicationResponse.builder()
.name("testApp2")
.service("service3")
.build()));
given(targetService.addToBackingApplications(eq(backingApps), any(), eq("foo-service-id")))
.willReturn(Mono.just(backingApps));
StepVerifier.create(backingAppManagementService.getDeployedBackingApplications("foo-service-id"))
.expectNext(BackingApplications.builder()
.backingApplication(BackingApplication.builder()
.name("testApp1")
.services(
ServicesSpec.builder().serviceInstanceName("service1").build(),
ServicesSpec.builder().serviceInstanceName("service2").build())
.build())
.backingApplication(BackingApplication.builder()
.name("testApp2")
.services(ServicesSpec.builder().serviceInstanceName("service3").build())
.build())
.build())
.verifyComplete();
then(targetService).should().addToBackingApplications(eq(backingApps), any(), eq("foo-service-id"));
then(appDeployer).should().getServiceInstance(argThat(req -> "foo-service-id".equals(req.getServiceInstanceId())));
then(appDeployer).should().get(argThat(req -> "testApp1".equals(req.getName())));
then(appDeployer).should().get(argThat(req -> "testApp2".equals(req.getName())));
verifyNoInteractions(managementClient);
verifyNoMoreInteractions(appDeployer, targetService, managementClient);
}
@Test
void restageApplications() {
given(appDeployer.getServiceInstance(any(GetServiceInstanceRequest.class)))

View File

@@ -33,15 +33,18 @@ import org.springframework.cloud.appbroker.deployer.BackingServices;
import org.springframework.cloud.appbroker.deployer.BackingServicesProvisionService;
import org.springframework.cloud.appbroker.deployer.BrokeredService;
import org.springframework.cloud.appbroker.deployer.BrokeredServices;
import org.springframework.cloud.appbroker.deployer.ServicesSpec;
import org.springframework.cloud.appbroker.deployer.TargetSpec;
import org.springframework.cloud.appbroker.extensions.credentials.CredentialProviderService;
import org.springframework.cloud.appbroker.extensions.targets.TargetService;
import org.springframework.cloud.appbroker.manager.BackingAppManagementService;
import org.springframework.cloud.appbroker.service.DeleteServiceInstanceWorkflow;
import org.springframework.cloud.servicebroker.model.catalog.Plan;
import org.springframework.cloud.servicebroker.model.catalog.ServiceDefinition;
import org.springframework.cloud.servicebroker.model.instance.DeleteServiceInstanceRequest;
import org.springframework.cloud.servicebroker.model.instance.DeleteServiceInstanceResponse;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -52,6 +55,9 @@ class AppDeploymentDeleteServiceInstanceWorkflowTest {
@Mock
private BackingAppDeploymentService backingAppDeploymentService;
@Mock
private BackingAppManagementService backingAppManagementService;
@Mock
private TargetService targetService;
@@ -63,8 +69,6 @@ class AppDeploymentDeleteServiceInstanceWorkflowTest {
private BackingApplications backingApps;
private BackingServices backingServices;
private TargetSpec targetSpec;
private DeleteServiceInstanceWorkflow deleteServiceInstanceWorkflow;
@@ -85,7 +89,7 @@ class AppDeploymentDeleteServiceInstanceWorkflowTest {
.build())
.build();
backingServices = BackingServices
BackingServices backingServices = BackingServices
.builder()
.backingService(BackingService
.builder()
@@ -112,7 +116,7 @@ class AppDeploymentDeleteServiceInstanceWorkflowTest {
new AppDeploymentDeleteServiceInstanceWorkflow(
brokeredServices,
backingAppDeploymentService,
backingServicesProvisionService,
backingAppManagementService, backingServicesProvisionService,
credentialProviderService,
targetService
);
@@ -125,14 +129,45 @@ class AppDeploymentDeleteServiceInstanceWorkflowTest {
given(this.backingAppDeploymentService.undeploy(eq(backingApps)))
.willReturn(Flux.just("undeployed1", "undeployed2"));
given(this.backingAppManagementService.getDeployedBackingApplications(eq(request.getServiceInstanceId())))
.willReturn(Mono.just(getExistingBackingAppsWithService("my-service-instance")));
given(this.credentialProviderService.deleteCredentials(eq(backingApps), eq(request.getServiceInstanceId())))
.willReturn(Mono.just(backingApps));
given(this.targetService.addToBackingApplications(eq(backingApps), eq(targetSpec), eq("service-instance-id")))
.willReturn(Mono.just(backingApps));
given(this.targetService.addToBackingServices(eq(backingServices), eq(targetSpec), eq("service-instance-id")))
.willReturn(Mono.just(backingServices));
given(this.backingServicesProvisionService.deleteServiceInstance(eq(backingServices)))
.willReturn(Flux.just("my-service-instance"));
given(this.backingServicesProvisionService.deleteServiceInstance(argThat(backingServices -> {
boolean nameMatch = "my-service-instance".equals(backingServices.get(0).getServiceInstanceName());
boolean sizeMatch = backingServices.size() == 1;
return sizeMatch && nameMatch;
}))).willReturn(Flux.just("my-service-instance"));
StepVerifier
.create(deleteServiceInstanceWorkflow.delete(request, response))
.expectNext()
.expectNext()
.verifyComplete();
verifyNoMoreInteractionsWithServices();
}
@Test
void deleteServiceInstanceSucceedsWhenBackingServicesDifferFromConfiguration() {
DeleteServiceInstanceRequest request = buildRequest("service1", "plan1");
DeleteServiceInstanceResponse response = DeleteServiceInstanceResponse.builder().build();
given(this.backingAppDeploymentService.undeploy(eq(backingApps)))
.willReturn(Flux.just("undeployed1", "undeployed2"));
given(this.backingAppManagementService.getDeployedBackingApplications(eq(request.getServiceInstanceId())))
.willReturn(Mono.just(getExistingBackingAppsWithService("different-service-instance")));
given(this.credentialProviderService.deleteCredentials(eq(backingApps), eq(request.getServiceInstanceId())))
.willReturn(Mono.just(backingApps));
given(this.targetService.addToBackingApplications(eq(backingApps), eq(targetSpec), eq("service-instance-id")))
.willReturn(Mono.just(backingApps));
given(this.backingServicesProvisionService.deleteServiceInstance(argThat(backingServices -> {
boolean nameMatch = "different-service-instance".equals(backingServices.get(0).getServiceInstanceName());
boolean sizeMatch = backingServices.size() == 1;
return sizeMatch && nameMatch;
}))).willReturn(Flux.just("different-service-instance"));
StepVerifier
.create(deleteServiceInstanceWorkflow.delete(request, response))
@@ -148,6 +183,9 @@ class AppDeploymentDeleteServiceInstanceWorkflowTest {
DeleteServiceInstanceRequest request = buildRequest("unsupported-service", "plan1");
DeleteServiceInstanceResponse response = DeleteServiceInstanceResponse.builder().build();
given(this.backingAppManagementService.getDeployedBackingApplications(eq(request.getServiceInstanceId())))
.willReturn(Mono.just(BackingApplications.builder().build()));
StepVerifier
.create(deleteServiceInstanceWorkflow.delete(request, response))
.verifyComplete();
@@ -183,4 +221,26 @@ class AppDeploymentDeleteServiceInstanceWorkflowTest {
.build();
}
private BackingApplications getExistingBackingAppsWithService(String serviceInstanceName) {
return BackingApplications
.builder()
.backingApplication(BackingApplication
.builder()
.name("app1")
.services(ServicesSpec
.builder()
.serviceInstanceName(serviceInstanceName)
.build())
.build())
.backingApplication(BackingApplication
.builder()
.name("app2")
.services(ServicesSpec
.builder()
.serviceInstanceName(serviceInstanceName)
.build())
.build())
.build();
}
}

View File

@@ -16,6 +16,7 @@
package org.springframework.cloud.appbroker.workflow.instance;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@@ -36,16 +37,19 @@ import org.springframework.cloud.appbroker.deployer.BackingServices;
import org.springframework.cloud.appbroker.deployer.BackingServicesProvisionService;
import org.springframework.cloud.appbroker.deployer.BrokeredService;
import org.springframework.cloud.appbroker.deployer.BrokeredServices;
import org.springframework.cloud.appbroker.deployer.ServicesSpec;
import org.springframework.cloud.appbroker.deployer.TargetSpec;
import org.springframework.cloud.appbroker.extensions.parameters.BackingApplicationsParametersTransformationService;
import org.springframework.cloud.appbroker.extensions.parameters.BackingServicesParametersTransformationService;
import org.springframework.cloud.appbroker.extensions.targets.TargetService;
import org.springframework.cloud.appbroker.manager.BackingAppManagementService;
import org.springframework.cloud.servicebroker.model.catalog.Plan;
import org.springframework.cloud.servicebroker.model.catalog.ServiceDefinition;
import org.springframework.cloud.servicebroker.model.instance.UpdateServiceInstanceRequest;
import org.springframework.cloud.servicebroker.model.instance.UpdateServiceInstanceResponse;
import static java.util.Collections.singletonMap;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
@@ -57,6 +61,9 @@ class AppDeploymentUpdateServiceInstanceWorkflowTest {
@Mock
private BackingAppDeploymentService appDeploymentService;
@Mock
private BackingAppManagementService backingAppManagementService;
@Mock
private BackingServicesProvisionService servicesProvisionService;
@@ -118,6 +125,7 @@ class AppDeploymentUpdateServiceInstanceWorkflowTest {
updateServiceInstanceWorkflow = new AppDeploymentUpdateServiceInstanceWorkflow(
brokeredServices,
appDeploymentService,
backingAppManagementService,
servicesProvisionService,
appsParametersTransformationService,
servicesParametersTransformationService,
@@ -125,12 +133,13 @@ class AppDeploymentUpdateServiceInstanceWorkflowTest {
}
@Test
@SuppressWarnings({"unchecked", "UnassignedFluxMonoInstance"})
@SuppressWarnings({"UnassignedFluxMonoInstance"})
void updateServiceInstanceSucceeds() {
UpdateServiceInstanceRequest request = buildRequest("service1", "plan1");
UpdateServiceInstanceResponse response = UpdateServiceInstanceResponse.builder().build();
setupMocks(request);
mockNoChangeInBackingServices(request);
StepVerifier
.create(updateServiceInstanceWorkflow.update(request, response))
@@ -155,6 +164,7 @@ class AppDeploymentUpdateServiceInstanceWorkflowTest {
UpdateServiceInstanceResponse response = UpdateServiceInstanceResponse.builder().build();
setupMocks(request);
mockNoChangeInBackingServices(request);
StepVerifier
.create(updateServiceInstanceWorkflow.update(request, response))
@@ -165,11 +175,48 @@ class AppDeploymentUpdateServiceInstanceWorkflowTest {
verifyNoMoreInteractionsWithServices();
}
@Test
@SuppressWarnings({"UnassignedFluxMonoInstance"})
void updateServiceInstanceWithNewBackingServiceSucceeds() {
UpdateServiceInstanceRequest request = buildRequest("service1", "plan1");
UpdateServiceInstanceResponse response = UpdateServiceInstanceResponse.builder().build();
setupMocks(request);
given(this.backingAppManagementService.getDeployedBackingApplications(eq(request.getServiceInstanceId())))
.willReturn(Mono.just(getExistingBackingAppsWithService("existing-service-instance")));
given(this.servicesProvisionService.createServiceInstance(any()))
.willReturn(Flux.just("my-service-instance"));
given(this.servicesProvisionService.deleteServiceInstance(any()))
.willReturn(Flux.just("existing-service-instance"));
given(this.servicesProvisionService.updateServiceInstance(eq(Collections.emptyList())))
.willReturn(Flux.empty());
StepVerifier
.create(updateServiceInstanceWorkflow.update(request, response))
.expectNext()
.expectNext()
.verifyComplete();
verify(servicesProvisionService).createServiceInstance(backingServices);
verify(servicesProvisionService).deleteServiceInstance(BackingServices
.builder()
.backingService(BackingService
.builder()
.serviceInstanceName("existing-service-instance")
.build())
.build());
verifyNoMoreInteractionsWithServices();
}
@Test
void updateServiceInstanceWithNoAppsDoesNothing() {
UpdateServiceInstanceRequest request = buildRequest("unsupported-service", "plan1");
UpdateServiceInstanceResponse response = UpdateServiceInstanceResponse.builder().build();
given(this.backingAppManagementService.getDeployedBackingApplications(eq(request.getServiceInstanceId())))
.willReturn(Mono.empty());
StepVerifier
.create(updateServiceInstanceWorkflow.update(request, response))
.verifyComplete();
@@ -180,8 +227,6 @@ class AppDeploymentUpdateServiceInstanceWorkflowTest {
private void setupMocks(UpdateServiceInstanceRequest request) {
given(this.appDeploymentService.update(eq(backingApps), eq(request.getServiceInstanceId())))
.willReturn(Flux.just("app1", "app2"));
given(this.servicesProvisionService.updateServiceInstance(eq(backingServices)))
.willReturn(Flux.just("my-service-instance"));
given(
this.appsParametersTransformationService.transformParameters(eq(backingApps), eq(request.getParameters())))
@@ -197,6 +242,18 @@ class AppDeploymentUpdateServiceInstanceWorkflowTest {
.willReturn(Mono.just(backingServices));
}
private void mockNoChangeInBackingServices(UpdateServiceInstanceRequest request) {
given(this.servicesProvisionService.updateServiceInstance(eq(backingServices)))
.willReturn(Flux.just("my-service-instance"));
given(this.servicesProvisionService.createServiceInstance(any()))
.willReturn(Flux.empty());
given(this.servicesProvisionService.deleteServiceInstance(any()))
.willReturn(Flux.empty());
given(this.backingAppManagementService.getDeployedBackingApplications(eq(request.getServiceInstanceId())))
.willReturn(Mono.just(getExistingBackingAppsWithService("my-service-instance")));
}
private void verifyNoMoreInteractionsWithServices() {
verifyNoMoreInteractions(this.appDeploymentService);
verifyNoMoreInteractions(this.servicesProvisionService);
@@ -232,4 +289,26 @@ class AppDeploymentUpdateServiceInstanceWorkflowTest {
.build();
}
private BackingApplications getExistingBackingAppsWithService(String serviceInstanceName) {
return BackingApplications
.builder()
.backingApplication(BackingApplication
.builder()
.name("app1")
.services(ServicesSpec
.builder()
.serviceInstanceName(serviceInstanceName)
.build())
.build())
.backingApplication(BackingApplication
.builder()
.name("app2")
.services(ServicesSpec
.builder()
.serviceInstanceName(serviceInstanceName)
.build())
.build())
.build();
}
}

View File

@@ -21,6 +21,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -39,6 +40,7 @@ import org.cloudfoundry.AbstractCloudFoundryException;
import org.cloudfoundry.UnknownCloudFoundryException;
import org.cloudfoundry.client.CloudFoundryClient;
import org.cloudfoundry.client.v2.applications.AssociateApplicationRouteRequest;
import org.cloudfoundry.client.v2.applications.SummaryApplicationRequest;
import org.cloudfoundry.client.v2.organizations.GetOrganizationRequest;
import org.cloudfoundry.client.v2.organizations.GetOrganizationResponse;
import org.cloudfoundry.client.v2.organizations.ListOrganizationSpacesRequest;
@@ -80,7 +82,6 @@ import org.cloudfoundry.operations.applications.ApplicationHealthCheck;
import org.cloudfoundry.operations.applications.ApplicationManifest;
import org.cloudfoundry.operations.applications.DeleteApplicationRequest;
import org.cloudfoundry.operations.applications.Docker;
import org.cloudfoundry.operations.applications.GetApplicationRequest;
import org.cloudfoundry.operations.applications.PushApplicationManifestRequest;
import org.cloudfoundry.operations.applications.Route;
import org.cloudfoundry.operations.domains.Domain;
@@ -111,6 +112,8 @@ import org.springframework.cloud.appbroker.deployer.DeleteServiceInstanceRespons
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.GetApplicationRequest;
import org.springframework.cloud.appbroker.deployer.GetApplicationResponse;
import org.springframework.cloud.appbroker.deployer.GetServiceInstanceRequest;
import org.springframework.cloud.appbroker.deployer.GetServiceInstanceResponse;
import org.springframework.cloud.appbroker.deployer.UndeployApplicationRequest;
@@ -165,6 +168,34 @@ public class CloudFoundryAppDeployer implements AppDeployer, ResourceLoaderAware
this.resourceLoader = resourceLoader;
}
@Override
public Mono<GetApplicationResponse> get(GetApplicationRequest request) {
final String name = request.getName();
return operationsUtils.getOperations(request.getProperties())
.flatMap(cfOperations -> cfOperations.applications()
.get(org.cloudfoundry.operations.applications.GetApplicationRequest.builder().name(name).build())
.doOnRequest(l -> LOG.debug("Getting application {}", name))
.doOnSuccess(response -> LOG.info("Success getting application {} id: {}", name, response.getId()))
.doOnError(e -> LOG.warn(String.format("Error getting application %s: %s", name, e.getMessage())))
.map(ApplicationDetail::getId)
.flatMap(id ->
client.applicationsV2()
.summary(SummaryApplicationRequest.builder().applicationId(id).build())))
.flatMap(summary -> Flux.fromIterable(summary.getServices())
.map(org.cloudfoundry.client.v2.serviceinstances.ServiceInstance::getName)
.collectList()
.map(services -> GetApplicationResponse.builder()
.id(summary.getId())
.name(summary.getName())
.services(services)
.environment(summary.getEnvironmentJsons())
.build()))
.doOnRequest(l -> LOG.debug("Getting application summary for {}", name))
.doOnSuccess(item -> LOG.info("Success getting application summary for {}", name))
.doOnError(error -> LOG.error("Failed to get application summary for {}", name));
}
@Override
public Mono<DeployApplicationResponse> deploy(DeployApplicationRequest request) {
String appName = request.getName();
@@ -200,37 +231,58 @@ public class CloudFoundryAppDeployer implements AppDeployer, ResourceLoaderAware
public Mono<UpdateApplicationResponse> update(UpdateApplicationRequest request) {
final String name = request.getName();
return operationsUtils.getOperations(request.getProperties())
.flatMap(cfOperations -> cfOperations.applications()
.get(GetApplicationRequest.builder().name(name).build())
.doOnRequest(l -> LOG.debug("Getting application {}", name))
.doOnSuccess(response -> LOG.info("Success getting application {}", name))
.doOnError(e -> LOG.warn(String.format("Error getting application %s: %s", name, e.getMessage())))
.map(ApplicationDetail::getId)
.flatMap(applicationId -> associateHostName(applicationId, request.getProperties()))
.flatMap(applicationId -> Mono.zip(Mono.just(applicationId),
upgradeApplication(request, applicationId)))
.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 -> LOG.debug("Updating application {}", name))
.doOnSuccess(item -> LOG.info("Successfully updated application {}", name))
.doOnError(error -> LOG.error("Failed to update application {}", name))
.thenReturn(UpdateApplicationResponse.builder().name(name).build()));
return get(GetApplicationRequest.builder().name(name).properties(request.getProperties()).build())
.flatMap(response -> bindNewServices(response, request.getServices(), request.getProperties()))
.flatMap(applicationId -> associateHostName(applicationId, request.getProperties()))
.flatMap(applicationId -> Mono.zip(Mono.just(applicationId),
upgradeApplication(request, applicationId)))
.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 -> LOG.debug("Updating application {}", name))
.doOnSuccess(item -> LOG.info("Successfully updated application {}", name))
.doOnError(error -> LOG.error("Failed to update application {}", name))
.thenReturn(UpdateApplicationResponse.builder().name(name).build());
}
private Mono<String> bindNewServices(GetApplicationResponse deployedApp, List<String> services,
Map<String, String> properties) {
String id = deployedApp.getId();
List<String> boundServices = deployedApp.getServices();
List<String> servicesToBind = new ArrayList<>(services);
servicesToBind.removeAll(boundServices);
if (servicesToBind.isEmpty()) {
return Mono.just(id);
}
String name = deployedApp.getName();
return operationsUtils.getOperations(properties)
.flatMapMany(cfOperations -> Flux.fromIterable(servicesToBind)
.flatMap(service -> cfOperations.services()
.bind(BindServiceInstanceRequest.builder()
.applicationName(name)
.serviceInstanceName(service)
.build())
.doOnRequest(l -> LOG.debug("Binding application {} to service {}", name, service))
.doOnNext(item -> LOG.info("Successfully bind application {} to service {}", name, service))
.doOnError(error -> LOG.error("Failed to bind application {} to service {}", name, service))))
.then()
.thenReturn(id);
}
private Mono<String> associateHostName(String applicationId, Map<String, String> properties) {
@@ -799,8 +851,8 @@ public class CloudFoundryAppDeployer implements AppDeployer, ResourceLoaderAware
private Duration apiPollingTimeout(Map<String, String> properties) {
return Duration.ofSeconds(
Optional.ofNullable(properties.get(CloudFoundryDeploymentProperties.API_POLLING_TIMEOUT_PROPERTY_KEY))
.map(Long::parseLong)
.orElse(this.defaultDeploymentProperties.getApiPollingTimeout()));
.map(Long::parseLong)
.orElse(this.defaultDeploymentProperties.getApiPollingTimeout()));
}
private Integer instances(Map<String, String> properties) {

View File

@@ -23,6 +23,8 @@ import java.util.Map;
import org.cloudfoundry.client.CloudFoundryClient;
import org.cloudfoundry.client.v2.Metadata;
import org.cloudfoundry.client.v2.applications.SummaryApplicationRequest;
import org.cloudfoundry.client.v2.applications.SummaryApplicationResponse;
import org.cloudfoundry.client.v2.organizations.GetOrganizationRequest;
import org.cloudfoundry.client.v2.organizations.GetOrganizationResponse;
import org.cloudfoundry.client.v2.organizations.ListOrganizationSpacesRequest;
@@ -38,6 +40,7 @@ import org.cloudfoundry.client.v2.spaces.GetSpaceResponse;
import org.cloudfoundry.client.v2.spaces.SpaceEntity;
import org.cloudfoundry.client.v2.spaces.SpaceResource;
import org.cloudfoundry.operations.CloudFoundryOperations;
import org.cloudfoundry.operations.applications.ApplicationDetail;
import org.cloudfoundry.operations.applications.ApplicationHealthCheck;
import org.cloudfoundry.operations.applications.ApplicationManifest;
import org.cloudfoundry.operations.applications.Applications;
@@ -69,6 +72,7 @@ import org.springframework.cloud.appbroker.deployer.CreateServiceInstanceRequest
import org.springframework.cloud.appbroker.deployer.DeleteServiceInstanceRequest;
import org.springframework.cloud.appbroker.deployer.DeployApplicationRequest;
import org.springframework.cloud.appbroker.deployer.DeploymentProperties;
import org.springframework.cloud.appbroker.deployer.GetApplicationRequest;
import org.springframework.cloud.appbroker.deployer.UpdateServiceInstanceRequest;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.ResourceLoader;
@@ -97,7 +101,9 @@ class CloudFoundryAppDeployerTest {
private static final String APP_PATH = "test.jar";
private static final String SERVICE_INSTANCE_ID = "service-instance-id";
private static final long DEFAULT_COMPLETION_DURATION = Duration.ofSeconds(DEFAULT_API_POLLING_TIMEOUT_SECONDS).getSeconds();
private static final long DEFAULT_COMPLETION_DURATION = Duration.ofSeconds(DEFAULT_API_POLLING_TIMEOUT_SECONDS)
.getSeconds();
private static final int EXPECTED_MANIFESTS = 1;
@@ -121,6 +127,9 @@ class CloudFoundryAppDeployerTest {
@Mock
private org.cloudfoundry.client.v2.spaces.Spaces clientSpaces;
@Mock
private org.cloudfoundry.client.v2.applications.ApplicationsV2 clientApplications;
@Mock
private org.cloudfoundry.client.v2.organizations.Organizations clientOrganizations;
@@ -155,6 +164,7 @@ class CloudFoundryAppDeployerTest {
given(cloudFoundryClient.serviceInstances()).willReturn(clientServiceInstances);
given(cloudFoundryClient.spaces()).willReturn(clientSpaces);
given(cloudFoundryClient.organizations()).willReturn(clientOrganizations);
given(cloudFoundryClient.applicationsV2()).willReturn(clientApplications);
given(operationsUtils.getOperations(anyMap())).willReturn(Mono.just(cloudFoundryOperations));
given(operationsUtils.getOperationsForSpace(anyString())).willReturn(Mono.just(cloudFoundryOperations));
given(operationsUtils.getOperationsForOrgAndSpace(anyString(), anyString()))
@@ -916,6 +926,95 @@ class CloudFoundryAppDeployerTest {
then(operationsUtils).shouldHaveNoMoreInteractions();
}
@Test
void getDeployedAppByName() {
given(operationsApplications.get(any(org.cloudfoundry.operations.applications.GetApplicationRequest.class)))
.willReturn(Mono.just(ApplicationDetail.builder()
.id("foo-id")
.name("foo-name")
// fields below are required by builder but we don't care
.stack("").diskQuota(1).instances(1).memoryLimit(1).requestedState("").runningInstances(1) //
.build()));
given(clientApplications.summary(any(SummaryApplicationRequest.class)))
.willReturn(Mono.just(SummaryApplicationResponse.builder()
.id("foo-id")
.name("foo-name")
.service(org.cloudfoundry.client.v2.serviceinstances.ServiceInstance.builder()
.name("foo-service-1")
.build())
.service(org.cloudfoundry.client.v2.serviceinstances.ServiceInstance.builder()
.name("foo-service-2")
.build())
.environmentJson("foo-key", "foo-value")
.build()));
StepVerifier.create(appDeployer.get(GetApplicationRequest.builder().name("foo-name").build()))
.assertNext(response -> {
assertThat(response.getName()).isEqualTo("foo-name");
assertThat(response.getServices()).hasSize(2);
assertThat(response.getServices().get(0)).isEqualTo("foo-service-1");
assertThat(response.getServices().get(1)).isEqualTo("foo-service-2");
assertThat(response.getEnvironment()).isEqualTo(singletonMap("foo-key", "foo-value"));
})
.verifyComplete();
then(operationsUtils).should().getOperations(argThat(CollectionUtils::isEmpty));
then(cloudFoundryOperations).should().applications();
then(operationsApplications).should().get(argThat(request -> "foo-name".equals(request.getName())));
then(clientApplications).should().summary(argThat(request -> "foo-id".equals(request.getApplicationId())));
then(operationsApplications).shouldHaveNoMoreInteractions();
then(clientApplications).shouldHaveNoMoreInteractions();
then(cloudFoundryOperations).shouldHaveNoMoreInteractions();
then(operationsUtils).shouldHaveNoMoreInteractions();
}
@Test
void getDeployedAppByNameAndSpace() {
given(operationsApplications.get(any(org.cloudfoundry.operations.applications.GetApplicationRequest.class)))
.willReturn(Mono.just(ApplicationDetail.builder()
.id("foo-id")
.name("foo-name")
// fields below are required by builder but we don't care
.stack("").diskQuota(1).instances(1).memoryLimit(1).requestedState("").runningInstances(1) //
.build()));
given(clientApplications.summary(any(SummaryApplicationRequest.class)))
.willReturn(Mono.just(SummaryApplicationResponse.builder()
.id("foo-id")
.name("foo-name")
.service(org.cloudfoundry.client.v2.serviceinstances.ServiceInstance.builder()
.name("foo-service-1")
.build())
.service(org.cloudfoundry.client.v2.serviceinstances.ServiceInstance.builder()
.name("foo-service-2")
.build())
.environmentJson("foo-key", "foo-value")
.build()));
GetApplicationRequest request = GetApplicationRequest.builder()
.name("foo-name")
.properties(singletonMap(TARGET_PROPERTY_KEY, "foo-space"))
.build();
StepVerifier.create(appDeployer.get(request))
.assertNext(response -> {
assertThat(response.getName()).isEqualTo("foo-name");
assertThat(response.getServices()).hasSize(2);
assertThat(response.getServices().get(0)).isEqualTo("foo-service-1");
assertThat(response.getServices().get(1)).isEqualTo("foo-service-2");
assertThat(response.getEnvironment()).isEqualTo(singletonMap("foo-key", "foo-value"));
})
.verifyComplete();
then(operationsUtils).should().getOperations(
argThat(argument -> "foo-space".equals(argument.get(TARGET_PROPERTY_KEY))));
then(cloudFoundryOperations).should().applications();
then(operationsApplications).should().get(argThat(req -> "foo-name".equals(req.getName())));
then(clientApplications).should().summary(argThat(req -> "foo-id".equals(req.getApplicationId())));
then(operationsApplications).shouldHaveNoMoreInteractions();
then(clientApplications).shouldHaveNoMoreInteractions();
then(cloudFoundryOperations).shouldHaveNoMoreInteractions();
then(operationsUtils).shouldHaveNoMoreInteractions();
}
private ApplicationManifest.Builder baseManifest() {
return ApplicationManifest.builder()
.services(new ArrayList<>());

View File

@@ -16,6 +16,7 @@
package org.springframework.cloud.appbroker.deployer.cloudfoundry;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@@ -23,10 +24,13 @@ import org.cloudfoundry.client.CloudFoundryClient;
import org.cloudfoundry.client.v2.Metadata;
import org.cloudfoundry.client.v2.applications.ApplicationsV2;
import org.cloudfoundry.client.v2.applications.AssociateApplicationRouteRequest;
import org.cloudfoundry.client.v2.applications.SummaryApplicationRequest;
import org.cloudfoundry.client.v2.applications.SummaryApplicationResponse;
import org.cloudfoundry.client.v2.applications.UpdateApplicationResponse;
import org.cloudfoundry.client.v2.routes.CreateRouteRequest;
import org.cloudfoundry.client.v2.routes.CreateRouteResponse;
import org.cloudfoundry.client.v2.routes.Routes;
import org.cloudfoundry.client.v2.serviceinstances.ServiceInstance;
import org.cloudfoundry.client.v3.BuildpackData;
import org.cloudfoundry.client.v3.Lifecycle;
import org.cloudfoundry.client.v3.LifecycleType;
@@ -57,6 +61,8 @@ import org.cloudfoundry.operations.applications.Applications;
import org.cloudfoundry.operations.domains.Domain;
import org.cloudfoundry.operations.domains.Domains;
import org.cloudfoundry.operations.domains.Status;
import org.cloudfoundry.operations.services.BindServiceInstanceRequest;
import org.cloudfoundry.operations.services.Services;
import org.cloudfoundry.operations.spaces.SpaceDetail;
import org.cloudfoundry.operations.spaces.Spaces;
import org.junit.jupiter.api.BeforeEach;
@@ -76,17 +82,22 @@ import org.springframework.cloud.appbroker.deployer.UpdateApplicationRequest;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.ResourceLoader;
import static java.util.Collections.singletonMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.springframework.cloud.appbroker.deployer.DeploymentProperties.TARGET_PROPERTY_KEY;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class CloudFoundryAppDeployerUpdateApplicationTest {
private static final String APP_ID = "app-id";
private static final String APP_NAME = "test-app";
private static final String APP_PATH = "test.jar";
@@ -96,6 +107,9 @@ class CloudFoundryAppDeployerUpdateApplicationTest {
@Mock
private Applications operationsApplications;
@Mock
private Services operationsServices;
@Mock
private ApplicationsV2 applicationsV2;
@@ -142,6 +156,7 @@ class CloudFoundryAppDeployerUpdateApplicationTest {
given(operationsApplications.pushManifest(any())).willReturn(Mono.empty());
given(resourceLoader.getResource(APP_PATH)).willReturn(new FileSystemResource(APP_PATH));
given(cloudFoundryOperations.services()).willReturn(operationsServices);
given(cloudFoundryOperations.applications()).willReturn(operationsApplications);
given(cloudFoundryOperations.domains()).willReturn(domains);
given(cloudFoundryOperations.spaces()).willReturn(spaces);
@@ -160,6 +175,13 @@ class CloudFoundryAppDeployerUpdateApplicationTest {
given(operationsApplications.get(any()))
.willReturn(Mono.just(createApplicationDetail()));
given(applicationsV2.summary(any(SummaryApplicationRequest.class)))
.willReturn(Mono.just(SummaryApplicationResponse.builder()
.id(APP_ID)
.name(APP_NAME)
.services(Collections.emptyList())
.build()));
given(packages.create(any()))
.willReturn(Mono.just(CreatePackageResponse.builder()
.data(BitsData.builder().build())
@@ -247,6 +269,27 @@ class CloudFoundryAppDeployerUpdateApplicationTest {
.verifyComplete();
}
@Test
void updateAppWithTarget() {
given(applicationsV2.update(any()))
.willReturn(Mono.just(UpdateApplicationResponse.builder()
.build()));
Map<String, String> properties = singletonMap(TARGET_PROPERTY_KEY, "service-instance-id");
UpdateApplicationRequest request = UpdateApplicationRequest.builder()
.name(APP_NAME)
.path(APP_PATH)
.properties(properties)
.build();
StepVerifier.create(appDeployer.update(request))
.assertNext(response -> assertThat(response.getName()).isEqualTo(APP_NAME))
.verifyComplete();
then(operationsUtils).should().getOperations(properties);
then(operationsUtils).shouldHaveNoMoreInteractions();
}
@Test
void updateAppProperties() {
ArgumentCaptor<org.cloudfoundry.client.v2.applications.UpdateApplicationRequest> updateApplicationRequestCaptor =
@@ -295,7 +338,74 @@ class CloudFoundryAppDeployerUpdateApplicationTest {
.assertNext(response -> assertThat(response.getName()).isEqualTo(APP_NAME))
.verifyComplete();
then(applicationsV2).shouldHaveNoInteractions();
then(applicationsV2).should().summary(any(SummaryApplicationRequest.class));
then(applicationsV2).shouldHaveNoMoreInteractions();
}
@Test
void updateAppWithNoNewServices() {
given(applicationsV2.summary(any(SummaryApplicationRequest.class)))
.willReturn(Mono.just(SummaryApplicationResponse.builder()
.id(APP_ID)
.name(APP_NAME)
.service(ServiceInstance.builder()
.name("service-1")
.build())
.service(ServiceInstance.builder()
.name("service-2")
.build())
.build()));
given(applicationsV2.update(any()))
.willReturn(Mono.just(UpdateApplicationResponse.builder()
.build()));
UpdateApplicationRequest request = UpdateApplicationRequest.builder()
.name(APP_NAME)
.path(APP_PATH)
.service("service-1")
.service("service-2")
.build();
StepVerifier.create(appDeployer.update(request))
.assertNext(response -> assertThat(response.getName()).isEqualTo(APP_NAME))
.verifyComplete();
then(applicationsV2).should().summary(SummaryApplicationRequest.builder().applicationId(APP_ID).build());
then(applicationsV2).should().update(argThat(arg -> arg.getApplicationId().equals(APP_ID)));
then(operationsServices).shouldHaveNoMoreInteractions();
then(applicationsV2).shouldHaveNoMoreInteractions();
}
@Test
void updateAppWithNewServices() {
given(operationsServices.bind(any(BindServiceInstanceRequest.class)))
.willReturn(Mono.empty());
given(applicationsV2.update(any()))
.willReturn(Mono.just(UpdateApplicationResponse.builder()
.build()));
UpdateApplicationRequest request = UpdateApplicationRequest.builder()
.name(APP_NAME)
.path(APP_PATH)
.service("service-1")
.service("service-2")
.build();
StepVerifier.create(appDeployer.update(request))
.assertNext(response -> assertThat(response.getName()).isEqualTo(APP_NAME))
.verifyComplete();
then(applicationsV2).should().summary(SummaryApplicationRequest.builder().applicationId(APP_ID).build());
then(operationsServices).should()
.bind(BindServiceInstanceRequest.builder().serviceInstanceName("service-1").applicationName(APP_NAME)
.build());
then(operationsServices).should()
.bind(BindServiceInstanceRequest.builder().serviceInstanceName("service-2").applicationName(APP_NAME)
.build());
then(applicationsV2).should().update(argThat(arg -> arg.getApplicationId().equals(APP_ID)));
then(operationsServices).shouldHaveNoMoreInteractions();
then(applicationsV2).shouldHaveNoMoreInteractions();
}
@Test
@@ -342,7 +452,7 @@ class CloudFoundryAppDeployerUpdateApplicationTest {
.host("my.host")
.build());
then(applicationsV2).should().associateRoute(AssociateApplicationRouteRequest.builder()
.applicationId("app-id")
.applicationId(APP_ID)
.routeId("route-id")
.build());
}
@@ -376,9 +486,9 @@ class CloudFoundryAppDeployerUpdateApplicationTest {
.assertNext(response -> assertThat(response.getName()).isEqualTo(APP_NAME))
.verifyComplete();
verifyRouteCreatedAndMapped("myDomainComId", "my.host", "space-id", "app-id");
verifyRouteCreatedAndMapped("myDomainInternalId", "my.host", "space-id", "app-id");
verifyRouteCreatedAndMapped("myDomainDefaultId", "my.host", "space-id", "app-id");
verifyRouteCreatedAndMapped("myDomainComId", "my.host", "space-id", APP_ID);
verifyRouteCreatedAndMapped("myDomainInternalId", "my.host", "space-id", APP_ID);
verifyRouteCreatedAndMapped("myDomainDefaultId", "my.host", "space-id", APP_ID);
}
@Test
@@ -409,8 +519,8 @@ class CloudFoundryAppDeployerUpdateApplicationTest {
.assertNext(response -> assertThat(response.getName()).isEqualTo(APP_NAME))
.verifyComplete();
verifyRouteCreatedAndMapped("myDomainInternalId", "my.host", "space-id", "app-id");
verifyRouteCreatedAndMapped("myDomainDefaultId", "my.host", "space-id", "app-id");
verifyRouteCreatedAndMapped("myDomainInternalId", "my.host", "space-id", APP_ID);
verifyRouteCreatedAndMapped("myDomainDefaultId", "my.host", "space-id", APP_ID);
}
@Test
@@ -457,7 +567,7 @@ class CloudFoundryAppDeployerUpdateApplicationTest {
.host("my.host")
.build());
then(applicationsV2).should().associateRoute(AssociateApplicationRouteRequest.builder()
.applicationId("app-id")
.applicationId(APP_ID)
.routeId("route-id")
.build());
}
@@ -508,7 +618,7 @@ class CloudFoundryAppDeployerUpdateApplicationTest {
.build());
then(applicationsV2).should().associateRoute(AssociateApplicationRouteRequest
.builder()
.applicationId("app-id")
.applicationId(APP_ID)
.routeId("route-id")
.build());
}
@@ -572,12 +682,12 @@ class CloudFoundryAppDeployerUpdateApplicationTest {
private ApplicationDetail createApplicationDetail() {
return ApplicationDetail
.builder()
.id("app-id")
.id(APP_ID)
.stack("")
.diskQuota(512)
.instances(1)
.memoryLimit(512)
.name("app")
.name(APP_NAME)
.requestedState("STARTED")
.runningInstances(1)
.build();

View File

@@ -32,6 +32,10 @@ public interface AppDeployer {
return Mono.empty();
}
default Mono<GetApplicationResponse> get(GetApplicationRequest request) {
return Mono.empty();
}
default Mono<GetServiceInstanceResponse> getServiceInstance(GetServiceInstanceRequest request) {
return Mono.empty();
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2002-2020 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
*
* https://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 GetApplicationRequest {
private final String name;
private final Map<String, String> properties;
protected GetApplicationRequest(String name, Map<String, String> properties) {
this.name = name;
this.properties = properties;
}
public static GetDeployedAppRequestBuilder builder() {
return new GetDeployedAppRequestBuilder();
}
public String getName() {
return name;
}
public Map<String, String> getProperties() {
return properties;
}
public static final class GetDeployedAppRequestBuilder {
private String name;
private final Map<String, String> properties = new HashMap<>();
private GetDeployedAppRequestBuilder() {
}
public GetDeployedAppRequestBuilder name(String name) {
this.name = name;
return this;
}
public GetDeployedAppRequestBuilder properties(Map<String, String> properties) {
if (!CollectionUtils.isEmpty(properties)) {
this.properties.putAll(properties);
}
return this;
}
public GetApplicationRequest build() {
return new GetApplicationRequest(name, properties);
}
}
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2002-2020 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
*
* https://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.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.util.CollectionUtils;
public class GetApplicationResponse {
private final String id;
private final String name;
private final Map<String, Object> environment;
private final List<String> services;
protected GetApplicationResponse(String id, String name, Map<String, Object> environment, List<String> services) {
this.id = id;
this.name = name;
this.environment = environment;
this.services = services;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public Map<String, Object> getEnvironment() {
return environment;
}
public List<String> getServices() {
return services;
}
public static GetApplicationResponseBuilder builder() {
return new GetApplicationResponseBuilder();
}
public static final class GetApplicationResponseBuilder {
private String id;
private String name;
private final Map<String, Object> environment = new HashMap<>();
private final List<String> services = new ArrayList<>();
private GetApplicationResponseBuilder() {
}
public GetApplicationResponseBuilder id(String id) {
this.id = id;
return this;
}
public GetApplicationResponseBuilder name(String name) {
this.name = name;
return this;
}
public GetApplicationResponseBuilder environment(String key, String value) {
if (key != null && value != null) {
this.environment.put(key, value);
}
return this;
}
public GetApplicationResponseBuilder environment(Map<String, Object> environment) {
if (!CollectionUtils.isEmpty(environment)) {
this.environment.putAll(environment);
}
return this;
}
public GetApplicationResponseBuilder service(String service) {
if (service != null) {
this.services.add(service);
}
return this;
}
public GetApplicationResponseBuilder services(List<String> services) {
if (!CollectionUtils.isEmpty(services)) {
this.services.addAll(services);
}
return this;
}
public GetApplicationResponse build() {
return new GetApplicationResponse(id, name, environment, services);
}
}
}

View File

@@ -331,7 +331,9 @@ Spring Cloud App Broker provides the https://docs.spring.io/spring-cloud-app-bro
=== Updating a Service Instance
Spring Cloud App Broker provides the https://docs.spring.io/spring-cloud-app-broker/docs/1.0.0.BUILD-SNAPSHOT/api/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentUpdateServiceInstanceWorkflow.html[`AppDeploymentUpdateServiceInstanceWorkflow`] workflow, which handles updating the configured backing applications and services as illustrated in the previous sections. The service broker application can implement the https://docs.spring.io/spring-cloud-app-broker/docs/1.0.0.BUILD-SNAPSHOT/api/org/springframework/cloud/appbroker/service/UpdateServiceInstanceWorkflow.html[`UpdateServiceInstanceWorkflow`] interface to further modify the deployment. Multiple workflows may be annotated with `@Order` so as to process the workflows in a specific order. Alternatively, the service broker application can implement the `ServiceInstanceService` interface provided by Spring Cloud Open Service Broker. See https://docs.spring.io/spring-cloud-open-service-broker/docs/current/reference/html5/#service-instances[Service Instances] in the https://docs.spring.io/spring-cloud-open-service-broker/docs/current/reference/html5/[Spring Cloud Open Service Broker documentation].
Spring Cloud App Broker provides the https://docs.spring.io/spring-cloud-app-broker/docs/1.0.0.BUILD-SNAPSHOT/api/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentUpdateServiceInstanceWorkflow.html[`AppDeploymentUpdateServiceInstanceWorkflow`] workflow, which handles updating the configured backing applications and services as illustrated in the previous sections. If the list of backing services is updated, the default behavior is to create and bind the new backing service instances, and to unbind and delete the existing backing service instances that are no longer listed in the configuration.
The service broker application can implement the https://docs.spring.io/spring-cloud-app-broker/docs/1.0.0.BUILD-SNAPSHOT/api/org/springframework/cloud/appbroker/service/UpdateServiceInstanceWorkflow.html[`UpdateServiceInstanceWorkflow`] interface to further modify the deployment. Multiple workflows may be annotated with `@Order` so as to process the workflows in a specific order. Alternatively, the service broker application can implement the `ServiceInstanceService` interface provided by Spring Cloud Open Service Broker. See https://docs.spring.io/spring-cloud-open-service-broker/docs/current/reference/html5/#service-instances[Service Instances] in the https://docs.spring.io/spring-cloud-open-service-broker/docs/current/reference/html5/[Spring Cloud Open Service Broker documentation].
CAUTION: Modifying certain properties, such as disk and memory, when updating an application, may result in downtime.

View File

@@ -29,6 +29,8 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.springframework.cloud.appbroker.integration.fixtures.CloudControllerStubFixture.planGuid;
import static org.springframework.cloud.appbroker.integration.fixtures.CloudControllerStubFixture.serviceGuid;
@ExtendWith(SpringExtension.class)
@SpringBootTest(
@@ -54,12 +56,13 @@ class CatalogComponentTest {
.then()
.statusCode(HttpStatus.OK.value())
.body("services[0].name", equalTo("example"))
.body("services[0].id", equalTo(serviceGuid("example")))
.body("services[0].description", equalTo("A simple example"))
.body("services[0].bindable", equalTo(true))
.body("services[0].metadata.size()", is(0))
.body("services[0].plan_updateable", equalTo(null))
.body("services[0].instances_retrievable", equalTo(null))
.body("services[0].plans[0].id", equalTo("standard-plan-id"))
.body("services[0].plans[0].id", equalTo(planGuid("standard")))
.body("services[0].plans[0].name", equalTo("standard"))
.body("services[0].plans[0].metadata", equalTo(null))
.body("services[0].plans[0].bindable", equalTo(true))

View File

@@ -33,10 +33,12 @@ import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.springframework.cloud.appbroker.integration.CreateInstanceWithOAuth2CredentialsComponentTest.APP_NAME_1;
import static org.springframework.cloud.appbroker.integration.CreateInstanceWithOAuth2CredentialsComponentTest.APP_NAME_2;
import static org.springframework.cloud.appbroker.integration.CreateInstanceWithOAuth2CredentialsComponentTest.PLAN_NAME;
import static org.springframework.cloud.appbroker.integration.CreateInstanceWithOAuth2CredentialsComponentTest.SERVICE_NAME;
@TestPropertySource(properties = {
"spring.cloud.appbroker.services[0].service-name=example",
"spring.cloud.appbroker.services[0].plan-name=standard",
"spring.cloud.appbroker.services[0].service-name=" + SERVICE_NAME,
"spring.cloud.appbroker.services[0].plan-name=" + PLAN_NAME,
"spring.cloud.appbroker.services[0].apps[0].path=classpath:demo.jar",
"spring.cloud.appbroker.services[0].apps[0].name=" + APP_NAME_1,
@@ -69,6 +71,10 @@ class CreateInstanceWithOAuth2CredentialsComponentTest extends WiremockComponent
protected static final String APP_NAME_2 = "app-with-outh2-credentials2";
protected static final String SERVICE_NAME = "example";
protected static final String PLAN_NAME = "standard";
private static final String SERVICE_INSTANCE_ID = "instance-id";
@Autowired
@@ -122,6 +128,8 @@ class CreateInstanceWithOAuth2CredentialsComponentTest extends WiremockComponent
@Test
void deleteAppWithOAuth2Credentials() {
cloudControllerFixture.stubGetServiceInstanceWithNoBinding("instance-id", "instance-name",
SERVICE_NAME, PLAN_NAME);
cloudControllerFixture.stubAppExists(APP_NAME_1);
cloudControllerFixture.stubServiceBindingDoesNotExist(APP_NAME_1);
cloudControllerFixture.stubDeleteApp(APP_NAME_1);

View File

@@ -47,6 +47,8 @@ class CreateInstanceWithOnlyABackingServiceComponentTest extends WiremockCompone
protected static final String BACKING_SERVICE_NAME = "db-service";
protected static final String BACKING_PLAN_NAME = "standard";
@Autowired
private OpenServiceBrokerApiFixture brokerFixture;
@@ -57,7 +59,7 @@ class CreateInstanceWithOnlyABackingServiceComponentTest extends WiremockCompone
void createsServicesWhenOnlyBackingServiceIsRequested() {
// given services are available in the marketplace
cloudControllerFixture.stubServiceExists(BACKING_SERVICE_NAME);
cloudControllerFixture.stubServiceExists(BACKING_SERVICE_NAME, BACKING_PLAN_NAME);
// will create the service instance
cloudControllerFixture.stubCreateServiceInstance(BACKING_SI_NAME);

View File

@@ -54,6 +54,8 @@ class CreateInstanceWithServiceInstanceGuidSuffixTargetComponentTest extends Wir
protected static final String BACKING_SERVICE_NAME = "db-service";
protected static final String BACKING_PLAN_NAME = "standard";
@Autowired
private OpenServiceBrokerApiFixture brokerFixture;
@@ -70,7 +72,7 @@ class CreateInstanceWithServiceInstanceGuidSuffixTargetComponentTest extends Wir
cloudControllerFixture.stubPushApp(applicationName);
// given services are available in the marketplace
cloudControllerFixture.stubServiceExists(BACKING_SERVICE_NAME);
cloudControllerFixture.stubServiceExists(BACKING_SERVICE_NAME, BACKING_PLAN_NAME);
// will create and bind the service instance
cloudControllerFixture.stubCreateServiceInstance(backingServiceInstanceName);

View File

@@ -51,6 +51,8 @@ class CreateInstanceWithServicesComponentTest extends WiremockComponentTest {
protected static final String BACKING_SERVICE_NAME = "db-service";
protected static final String BACKING_PLAN_NAME = "standard";
@Autowired
private OpenServiceBrokerApiFixture brokerFixture;
@@ -63,7 +65,7 @@ class CreateInstanceWithServicesComponentTest extends WiremockComponentTest {
cloudControllerFixture.stubPushApp(APP_NAME);
// given services are available in the marketplace
cloudControllerFixture.stubServiceExists(BACKING_SERVICE_NAME);
cloudControllerFixture.stubServiceExists(BACKING_SERVICE_NAME, BACKING_PLAN_NAME);
// will create and bind the service instance
cloudControllerFixture.stubCreateServiceInstance(BACKING_SI_NAME);

View File

@@ -56,6 +56,8 @@ class CreateInstanceWithServicesParametersComponentTest extends WiremockComponen
protected static final String BACKING_SERVICE_NAME = "db-service";
protected static final String BACKING_PLAN_NAME = "standard";
@Autowired
private OpenServiceBrokerApiFixture brokerFixture;
@@ -68,7 +70,7 @@ class CreateInstanceWithServicesParametersComponentTest extends WiremockComponen
cloudControllerFixture.stubPushApp(APP_NAME);
// given services are available in the marketplace
cloudControllerFixture.stubServiceExists(BACKING_SERVICE_NAME);
cloudControllerFixture.stubServiceExists(BACKING_SERVICE_NAME, BACKING_PLAN_NAME);
// will create with filtered parameters and bind the service instance
HashMap<String, Object> expectedCreationParameters = new HashMap<>();

View File

@@ -54,6 +54,8 @@ class CreateInstanceWithSpacePerServiceInstanceTargetComponentTest extends Wirem
protected static final String BACKING_SERVICE_NAME = "db-service";
protected static final String BACKING_PLAN_NAME = "standard";
@Autowired
private OpenServiceBrokerApiFixture brokerFixture;
@@ -69,7 +71,7 @@ class CreateInstanceWithSpacePerServiceInstanceTargetComponentTest extends Wirem
cloudControllerFixture.stubPushAppWithHost(APP_NAME, APP_NAME + "-" + serviceInstanceId);
// given services are available in the marketplace
cloudControllerFixture.stubServiceExists(BACKING_SERVICE_NAME);
cloudControllerFixture.stubServiceExists(BACKING_SERVICE_NAME, BACKING_PLAN_NAME);
// will create and bind the service instance
cloudControllerFixture.stubCreateServiceInstance(BACKING_SI_NAME);

View File

@@ -31,10 +31,12 @@ import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.springframework.cloud.appbroker.integration.DeleteInstanceComponentTest.APP_NAME_1;
import static org.springframework.cloud.appbroker.integration.DeleteInstanceComponentTest.APP_NAME_2;
import static org.springframework.cloud.appbroker.integration.DeleteInstanceComponentTest.PLAN_NAME;
import static org.springframework.cloud.appbroker.integration.DeleteInstanceComponentTest.SERVICE_NAME;
@TestPropertySource(properties = {
"spring.cloud.appbroker.services[0].service-name=example",
"spring.cloud.appbroker.services[0].plan-name=standard",
"spring.cloud.appbroker.services[0].service-name=" + SERVICE_NAME,
"spring.cloud.appbroker.services[0].plan-name=" + PLAN_NAME,
"spring.cloud.appbroker.services[0].apps[0].path=classpath:demo.jar",
"spring.cloud.appbroker.services[0].apps[0].name=" + APP_NAME_1,
"spring.cloud.appbroker.services[0].apps[1].path=classpath:demo.jar",
@@ -46,6 +48,10 @@ class DeleteInstanceComponentTest extends WiremockComponentTest {
protected static final String APP_NAME_2 = "second-app";
protected static final String SERVICE_NAME = "example";
protected static final String PLAN_NAME = "standard";
@Autowired
private OpenServiceBrokerApiFixture brokerFixture;
@@ -54,6 +60,9 @@ class DeleteInstanceComponentTest extends WiremockComponentTest {
@Test
void deleteAppsWhenTheyExist() {
cloudControllerFixture.stubGetServiceInstanceWithNoBinding("instance-id", "instance-name",
SERVICE_NAME, PLAN_NAME);
cloudControllerFixture.stubAppExists(APP_NAME_1);
cloudControllerFixture.stubAppExists(APP_NAME_2);
@@ -84,6 +93,9 @@ class DeleteInstanceComponentTest extends WiremockComponentTest {
@Test
void deleteAppsWhenTheyDoNotExist() {
cloudControllerFixture.stubGetServiceInstanceWithNoBinding("instance-id", "instance-name",
SERVICE_NAME, PLAN_NAME);
cloudControllerFixture.stubAppDoesNotExist(APP_NAME_1);
cloudControllerFixture.stubAppDoesNotExist(APP_NAME_2);

View File

@@ -31,27 +31,36 @@ import static org.hamcrest.Matchers.either;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.springframework.cloud.appbroker.integration.DeleteInstanceWithServicesComponentTest.APP_NAME;
import static org.springframework.cloud.appbroker.integration.DeleteInstanceWithServicesComponentTest.BACKING_PLAN_NAME;
import static org.springframework.cloud.appbroker.integration.DeleteInstanceWithServicesComponentTest.BACKING_SERVICE_NAME;
import static org.springframework.cloud.appbroker.integration.DeleteInstanceWithServicesComponentTest.BACKING_SI_NAME;
import static org.springframework.cloud.appbroker.integration.DeleteInstanceWithServicesComponentTest.PLAN_NAME;
import static org.springframework.cloud.appbroker.integration.DeleteInstanceWithServicesComponentTest.SERVICE_NAME;
@TestPropertySource(properties = {
"spring.cloud.appbroker.services[0].service-name=example",
"spring.cloud.appbroker.services[0].plan-name=standard",
"spring.cloud.appbroker.services[0].service-name=" + SERVICE_NAME,
"spring.cloud.appbroker.services[0].plan-name=" + PLAN_NAME,
"spring.cloud.appbroker.services[0].apps[0].path=classpath:demo.jar",
"spring.cloud.appbroker.services[0].apps[0].name=" + APP_NAME,
"spring.cloud.appbroker.services[0].apps[0].services[0].service-instance-name=" + BACKING_SI_NAME,
"spring.cloud.appbroker.services[0].services[0].service-instance-name=" + BACKING_SI_NAME,
"spring.cloud.appbroker.services[0].services[0].name=" + BACKING_SERVICE_NAME,
"spring.cloud.appbroker.services[0].services[0].plan=standard"
"spring.cloud.appbroker.services[0].services[0].plan=" + BACKING_PLAN_NAME
})
class DeleteInstanceWithServicesComponentTest extends WiremockComponentTest {
protected static final String APP_NAME = "app-delete-with-services";
protected static final String SERVICE_NAME = "example";
protected static final String PLAN_NAME = "standard";
protected static final String BACKING_SI_NAME = "my-db-service";
protected static final String BACKING_SERVICE_NAME = "db-service";
protected static final String BACKING_PLAN_NAME = "backing-standard";
@Autowired
private OpenServiceBrokerApiFixture brokerFixture;
@@ -60,13 +69,15 @@ class DeleteInstanceWithServicesComponentTest extends WiremockComponentTest {
@Test
void deleteAppsAndServicesWhenTheyExist() {
cloudControllerFixture.stubAppExists(APP_NAME);
cloudControllerFixture.stubGetServiceInstanceWithNoBinding("instance-id", "instance-name",
SERVICE_NAME, PLAN_NAME);
cloudControllerFixture.stubAppExistsWithBackingService(APP_NAME, BACKING_SI_NAME,
BACKING_SERVICE_NAME, BACKING_PLAN_NAME);
cloudControllerFixture.stubServiceBindingDoesNotExist(APP_NAME);
cloudControllerFixture.stubDeleteApp(APP_NAME);
cloudControllerFixture.stubServiceInstanceExists(BACKING_SI_NAME);
cloudControllerFixture.stubGetBackingServiceInstance(BACKING_SI_NAME, BACKING_SERVICE_NAME, BACKING_PLAN_NAME);
cloudControllerFixture.stubListServiceBindings(APP_NAME, BACKING_SI_NAME);
cloudControllerFixture.stubServiceBindingExists(APP_NAME, BACKING_SI_NAME);
cloudControllerFixture.stubDeleteServiceBinding(APP_NAME, BACKING_SI_NAME);
cloudControllerFixture.stubDeleteServiceInstance(BACKING_SI_NAME);
@@ -92,12 +103,12 @@ class DeleteInstanceWithServicesComponentTest extends WiremockComponentTest {
@Test
void deleteAppsWhenTheyExistAndServicesWhenTheyDoNotExist() {
cloudControllerFixture.stubGetServiceInstanceWithNoBinding("instance-id", "instance-name",
SERVICE_NAME, PLAN_NAME);
cloudControllerFixture.stubAppExists(APP_NAME);
cloudControllerFixture.stubServiceBindingDoesNotExist(APP_NAME);
cloudControllerFixture.stubDeleteApp(APP_NAME);
cloudControllerFixture.stubServiceInstanceDoesNotExist(BACKING_SI_NAME);
// when the service instance is deleted
given(brokerFixture.serviceInstanceRequest())
.when()
@@ -119,10 +130,10 @@ class DeleteInstanceWithServicesComponentTest extends WiremockComponentTest {
@Test
void deleteAppsAndServicesWhenTheyDoNotExist() {
cloudControllerFixture.stubGetServiceInstanceWithNoBinding("instance-id", "instance-name",
SERVICE_NAME, PLAN_NAME);
cloudControllerFixture.stubAppDoesNotExist(APP_NAME);
cloudControllerFixture.stubServiceInstanceDoesNotExist(BACKING_SI_NAME);
// when the service instance is deleted
given(brokerFixture.serviceInstanceRequest())
.when()

View File

@@ -0,0 +1,114 @@
/*
* Copyright 2002-2020 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
*
* https://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.integration;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.appbroker.integration.fixtures.CloudControllerStubFixture;
import org.springframework.cloud.appbroker.integration.fixtures.OpenServiceBrokerApiFixture;
import org.springframework.cloud.servicebroker.model.instance.OperationState;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.TestPropertySource;
import static io.restassured.RestAssured.given;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithNewServiceComponentTest.APP_NAME;
import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithNewServiceComponentTest.NEW_BACKING_PLAN_NAME;
import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithNewServiceComponentTest.NEW_BACKING_SERVICE_NAME;
import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithNewServiceComponentTest.NEW_BACKING_SI_NAME;
import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithNewServiceComponentTest.PLAN_NAME;
import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithNewServiceComponentTest.SERVICE_NAME;
@TestPropertySource(properties = {
"spring.cloud.appbroker.services[0].service-name=" + SERVICE_NAME,
"spring.cloud.appbroker.services[0].plan-name=" + PLAN_NAME,
"spring.cloud.appbroker.services[0].apps[0].path=classpath:demo.jar",
"spring.cloud.appbroker.services[0].apps[0].name=" + APP_NAME,
"spring.cloud.appbroker.services[0].apps[0].services[0].service-instance-name=" + NEW_BACKING_SI_NAME,
"spring.cloud.appbroker.services[0].services[0].service-instance-name=" + NEW_BACKING_SI_NAME,
"spring.cloud.appbroker.services[0].services[0].name=" + NEW_BACKING_SERVICE_NAME,
"spring.cloud.appbroker.services[0].services[0].plan=" + NEW_BACKING_PLAN_NAME
})
class UpdateInstanceWithNewServiceComponentTest extends WiremockComponentTest {
protected static final String APP_NAME = "app-update-with-new-service";
protected static final String BACKING_SI_NAME = "my-db-service";
protected static final String BACKING_SERVICE_NAME = "db-service";
protected static final String BACKING_PLAN_NAME = "backing-standard";
protected static final String NEW_BACKING_SERVICE_NAME = "new-service";
protected static final String NEW_BACKING_SI_NAME = "my-new-service";
protected static final String NEW_BACKING_PLAN_NAME = "new-backing-standard";
protected static final String SERVICE_NAME = "example";
protected static final String PLAN_NAME = "standard";
@Autowired
private OpenServiceBrokerApiFixture brokerFixture;
@Autowired
private CloudControllerStubFixture cloudControllerFixture;
@Test
void updateAppWithNewService() {
cloudControllerFixture.stubAppExistsWithBackingService(APP_NAME, BACKING_SI_NAME,
BACKING_SERVICE_NAME, BACKING_PLAN_NAME);
cloudControllerFixture.stubGetServiceInstanceWithNoBinding("instance-id", "instance-name",
SERVICE_NAME, PLAN_NAME);
cloudControllerFixture.stubUpdateApp(APP_NAME);
// will unbind and delete the existing service instance
cloudControllerFixture.stubGetBackingServiceInstance(BACKING_SI_NAME, BACKING_SERVICE_NAME, BACKING_PLAN_NAME);
cloudControllerFixture.stubServiceBindingExists(APP_NAME, BACKING_SI_NAME);
cloudControllerFixture.stubDeleteServiceBinding(APP_NAME, BACKING_SI_NAME);
cloudControllerFixture.stubDeleteServiceInstance(BACKING_SI_NAME);
// will create and bind the service instance
cloudControllerFixture.stubServiceExists(NEW_BACKING_SERVICE_NAME, NEW_BACKING_PLAN_NAME);
cloudControllerFixture.stubCreateServiceInstance(NEW_BACKING_SI_NAME);
cloudControllerFixture.stubCreateServiceBinding(APP_NAME, NEW_BACKING_SI_NAME);
cloudControllerFixture.stubServiceInstanceExists(NEW_BACKING_SI_NAME);
// when a service instance is updated
given(brokerFixture.serviceInstanceRequest())
.when()
.patch(brokerFixture.createServiceInstanceUrl(), "instance-id")
.then()
.statusCode(HttpStatus.ACCEPTED.value());
// when the "last_operation" API is polled
given(brokerFixture.serviceInstanceRequest())
.when()
.get(brokerFixture.getLastInstanceOperationUrl(), "instance-id")
.then()
.statusCode(HttpStatus.OK.value())
.body("state", is(equalTo(OperationState.IN_PROGRESS.toString())));
String state = brokerFixture.waitForAsyncOperationComplete("instance-id");
assertThat(state).isEqualTo(OperationState.SUCCEEDED.toString());
}
}

View File

@@ -30,26 +30,35 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithServicesComponentTest.APP_NAME;
import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithServicesComponentTest.SERVICE_INSTANCE_NAME;
import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithServicesComponentTest.BACKING_PLAN_NAME;
import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithServicesComponentTest.BACKING_SERVICE_INSTANCE_NAME;
import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithServicesComponentTest.BACKING_SERVICE_NAME;
import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithServicesComponentTest.PLAN_NAME;
import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithServicesComponentTest.SERVICE_NAME;
@TestPropertySource(properties = {
"spring.cloud.appbroker.services[0].service-name=example",
"spring.cloud.appbroker.services[0].plan-name=standard",
"spring.cloud.appbroker.services[0].service-name=" + SERVICE_NAME,
"spring.cloud.appbroker.services[0].plan-name=" + PLAN_NAME,
"spring.cloud.appbroker.services[0].apps[0].path=classpath:demo.jar",
"spring.cloud.appbroker.services[0].apps[0].name=" + APP_NAME,
"spring.cloud.appbroker.services[0].apps[0].services[0].service-instance-name=" + SERVICE_INSTANCE_NAME,
"spring.cloud.appbroker.services[0].services[0].service-instance-name=" + SERVICE_INSTANCE_NAME,
"spring.cloud.appbroker.services[0].services[0].name=" + SERVICE_NAME,
"spring.cloud.appbroker.services[0].services[0].plan=standard"
"spring.cloud.appbroker.services[0].apps[0].services[0].service-instance-name=" + BACKING_SERVICE_INSTANCE_NAME,
"spring.cloud.appbroker.services[0].services[0].service-instance-name=" + BACKING_SERVICE_INSTANCE_NAME,
"spring.cloud.appbroker.services[0].services[0].name=" + BACKING_SERVICE_NAME,
"spring.cloud.appbroker.services[0].services[0].plan=" + BACKING_PLAN_NAME
})
class UpdateInstanceWithServicesComponentTest extends WiremockComponentTest {
protected static final String APP_NAME = "app-update-with-services";
protected static final String SERVICE_INSTANCE_NAME = "my-db-service";
protected static final String SERVICE_NAME = "example";
protected static final String SERVICE_NAME = "db-service";
protected static final String PLAN_NAME = "standard";
protected static final String BACKING_SERVICE_INSTANCE_NAME = "my-db-service";
protected static final String BACKING_SERVICE_NAME = "db-service";
protected static final String BACKING_PLAN_NAME = "backing-standard";
@Autowired
private OpenServiceBrokerApiFixture brokerFixture;
@@ -59,7 +68,10 @@ class UpdateInstanceWithServicesComponentTest extends WiremockComponentTest {
@Test
void updateAppWithServices() {
cloudControllerFixture.stubAppExists(APP_NAME);
cloudControllerFixture.stubAppExistsWithBackingService(APP_NAME, BACKING_SERVICE_INSTANCE_NAME,
BACKING_SERVICE_NAME, BACKING_PLAN_NAME);
cloudControllerFixture.stubGetServiceInstanceWithNoBinding("instance-id", "instance-name",
SERVICE_NAME, PLAN_NAME);
cloudControllerFixture.stubUpdateApp(APP_NAME);
// when a service instance is updated

View File

@@ -33,18 +33,21 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithServicesParametersComponentTest.APP_NAME;
import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithServicesParametersComponentTest.BACKING_PLAN_NAME;
import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithServicesParametersComponentTest.BACKING_SERVICE_NAME;
import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithServicesParametersComponentTest.BACKING_SI_NAME;
import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithServicesParametersComponentTest.PLAN_NAME;
import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithServicesParametersComponentTest.SERVICE_NAME;
@TestPropertySource(properties = {
"spring.cloud.appbroker.services[0].service-name=example",
"spring.cloud.appbroker.services[0].plan-name=standard",
"spring.cloud.appbroker.services[0].service-name=" + SERVICE_NAME,
"spring.cloud.appbroker.services[0].plan-name=" + PLAN_NAME,
"spring.cloud.appbroker.services[0].apps[0].path=classpath:demo.jar",
"spring.cloud.appbroker.services[0].apps[0].name=" + APP_NAME,
"spring.cloud.appbroker.services[0].apps[0].services[0].service-instance-name=" + BACKING_SI_NAME,
"spring.cloud.appbroker.services[0].services[0].service-instance-name=" + BACKING_SI_NAME,
"spring.cloud.appbroker.services[0].services[0].name=" + BACKING_SERVICE_NAME,
"spring.cloud.appbroker.services[0].services[0].plan=standard",
"spring.cloud.appbroker.services[0].services[0].plan=" + BACKING_PLAN_NAME,
"spring.cloud.appbroker.services[0].services[0].parameters-transformers[0].name=ParameterMapping",
"spring.cloud.appbroker.services[0].services[0].parameters-transformers[0].args.include=paramA,paramC"
})
@@ -52,10 +55,16 @@ class UpdateInstanceWithServicesParametersComponentTest extends WiremockComponen
protected static final String APP_NAME = "app-update-services-param";
protected static final String SERVICE_NAME = "example";
protected static final String PLAN_NAME = "standard";
protected static final String BACKING_SI_NAME = "my-db-service";
protected static final String BACKING_SERVICE_NAME = "db-service";
protected static final String BACKING_PLAN_NAME = "backing-standard";
@Autowired
private OpenServiceBrokerApiFixture brokerFixture;
@@ -64,16 +73,18 @@ class UpdateInstanceWithServicesParametersComponentTest extends WiremockComponen
@Test
void updateAppWithBackingServicesParameters() {
cloudControllerFixture.stubAppExists(APP_NAME);
cloudControllerFixture.stubAppExistsWithBackingService(APP_NAME, BACKING_SI_NAME, BACKING_SERVICE_NAME,
BACKING_PLAN_NAME);
cloudControllerFixture.stubGetServiceInstanceWithNoBinding("instance-id", "instance-name",
SERVICE_NAME, PLAN_NAME);
cloudControllerFixture.stubUpdateApp(APP_NAME);
cloudControllerFixture.stubServiceInstanceExists(BACKING_SI_NAME);
// will update with filtered parameters and bind the service instance
HashMap<String, Object> expectedCreationParameters = new HashMap<>();
expectedCreationParameters.put("paramA", "valueA");
expectedCreationParameters.put("paramC", Collections.singletonMap("paramC1", "valueC1"));
cloudControllerFixture.stubServiceInstanceExists(BACKING_SI_NAME, BACKING_SERVICE_NAME, BACKING_PLAN_NAME);
cloudControllerFixture.stubUpdateServiceInstanceWithParameters(BACKING_SI_NAME, expectedCreationParameters);
// when a service instance is created with parameters

View File

@@ -30,28 +30,37 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithServicesRebindComponentTest.APP_NAME;
import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithServicesRebindComponentTest.BACKING_PLAN_NAME;
import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithServicesRebindComponentTest.BACKING_SERVICE_NAME;
import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithServicesRebindComponentTest.BACKING_SI_NAME;
import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithServicesRebindComponentTest.PLAN_NAME;
import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithServicesRebindComponentTest.SERVICE_NAME;
@TestPropertySource(properties = {
"spring.cloud.appbroker.services[0].service-name=example",
"spring.cloud.appbroker.services[0].plan-name=standard",
"spring.cloud.appbroker.services[0].service-name=" + SERVICE_NAME,
"spring.cloud.appbroker.services[0].plan-name=" + PLAN_NAME,
"spring.cloud.appbroker.services[0].apps[0].path=classpath:demo.jar",
"spring.cloud.appbroker.services[0].apps[0].name=" + APP_NAME,
"spring.cloud.appbroker.services[0].apps[0].services[0].service-instance-name=" + BACKING_SI_NAME,
"spring.cloud.appbroker.services[0].services[0].service-instance-name=" + BACKING_SI_NAME,
"spring.cloud.appbroker.services[0].services[0].name=" + BACKING_SERVICE_NAME,
"spring.cloud.appbroker.services[0].services[0].plan=standard",
"spring.cloud.appbroker.services[0].services[0].plan=" + BACKING_PLAN_NAME,
"spring.cloud.appbroker.services[0].services[0].rebind-on-update=true"
})
class UpdateInstanceWithServicesRebindComponentTest extends WiremockComponentTest {
protected static final String APP_NAME = "app-update-rebind-with-services";
protected static final String SERVICE_NAME = "example";
protected static final String PLAN_NAME = "standard";
protected static final String BACKING_SI_NAME = "my-db-service";
protected static final String BACKING_SERVICE_NAME = "db-service";
protected static final String BACKING_PLAN_NAME = "backing-standard";
@Autowired
private OpenServiceBrokerApiFixture brokerFixture;
@@ -60,12 +69,13 @@ class UpdateInstanceWithServicesRebindComponentTest extends WiremockComponentTes
@Test
void updateAppWithServices() {
cloudControllerFixture.stubAppExists(APP_NAME);
cloudControllerFixture.stubAppExistsWithBackingService(APP_NAME, BACKING_SI_NAME, BACKING_SERVICE_NAME,
BACKING_PLAN_NAME);
cloudControllerFixture.stubGetServiceInstanceWithNoBinding("instance-id", "instance-name",
SERVICE_NAME, PLAN_NAME);
cloudControllerFixture.stubUpdateApp(APP_NAME);
cloudControllerFixture.stubServiceInstanceExists(BACKING_SI_NAME);
cloudControllerFixture.stubListServiceBindings(APP_NAME, BACKING_SI_NAME);
cloudControllerFixture.stubGetBackingServiceInstance(BACKING_SI_NAME, BACKING_SERVICE_NAME, BACKING_PLAN_NAME);
cloudControllerFixture.stubServiceBindingExists(APP_NAME, BACKING_SI_NAME);
cloudControllerFixture.stubDeleteServiceBinding(APP_NAME, BACKING_SI_NAME);
cloudControllerFixture.stubCreateServiceBinding(APP_NAME, BACKING_SI_NAME);

View File

@@ -208,6 +208,23 @@ public class CloudControllerStubFixture extends WiremockStubFixture {
replace("@guid", stackGuid(appName))))));
}
public void stubAppExistsWithBackingService(final String appName, final String serviceInstanceName,
final String serviceName, final String planName) {
stubAppExists(appName);
stubFor(get(urlPathEqualTo("/v2/apps/" + appGuid(appName) + "/summary"))
.withMetadata(optionalStubMapping())
.willReturn(ok()
.withBody(cc("get-app-summary-with-backing-service",
replace("@name", appName),
replace("@service-instance-name", serviceInstanceName),
replace("@service-guid", serviceGuid(serviceName)),
replace("@plan-guid", planGuid(planName)),
replace("@guid", appGuid(appName)),
replace("@stack-guid", stackGuid(appName)),
replace("@route-guid", routeGuid(appName))))));
}
public void stubPushApp(final String appName, ContentPattern<?>... appMetadataPatterns) {
stubCreateAppMetadata(appName, appMetadataPatterns);
stubAppAfterCreation(appName, appName);
@@ -406,7 +423,9 @@ public class CloudControllerStubFixture extends WiremockStubFixture {
stubFor(get(urlPathEqualTo("/v2/spaces/" + TEST_SPACE_GUID + "/services"))
.withQueryParam("page", equalTo("1"))
.willReturn(ok()
.withBody(cc("list-space-services"))));
.withBody(cc("list-space-services")
.replace("@service-guid", "SERVICE-ID")
.replace("@service-name", "db-service"))));
stubFor(post(urlPathEqualTo("/v2/routes"))
.willReturn(ok()
@@ -429,6 +448,15 @@ public class CloudControllerStubFixture extends WiremockStubFixture {
}
public void stubServiceInstanceExists(String serviceInstanceName) {
stubServiceInstanceExists(serviceInstanceName, "doNotCare", "doNotCare");
}
public void stubServiceInstanceExists(String serviceInstanceName, String serviceName, String planName) {
stubServiceInstanceExists(serviceInstanceGuid(serviceInstanceName), serviceInstanceName, serviceName, planName);
}
private void stubServiceInstanceExists(String serviceInstanceId, String serviceInstanceName, String serviceName,
String planName) {
stubFor(get(urlPathEqualTo("/v2/spaces/" + TEST_SPACE_GUID + "/service_instances"))
.withQueryParam("q", equalTo("name:" + serviceInstanceName))
.withQueryParam("page", equalTo("1"))
@@ -436,31 +464,57 @@ public class CloudControllerStubFixture extends WiremockStubFixture {
.willReturn(ok()
.withBody(cc("list-space-service_instances",
replace("@space-guid", TEST_SPACE_GUID),
replace("@service-guid", serviceGuid(serviceName)),
replace("@plan-guid", planGuid(planName)),
replace("@name", serviceInstanceName),
replace("@guid", serviceInstanceName + "-GUID")))));
replace("@guid", serviceInstanceId)))));
}
public void stubServiceInstanceDoesNotExist(String serviceInstanceName) {
stubFor(get(urlPathEqualTo("/v2/spaces/" + TEST_SPACE_GUID + "/service_instances"))
.withQueryParam("q", equalTo("name:" + serviceInstanceName))
.withQueryParam("page", equalTo("1"))
.withQueryParam("return_user_provided_service_instances", equalTo("true"))
public void stubGetServiceInstanceWithNoBinding(String serviceInstanceId, String serviceInstanceName, String serviceName,
String planName) {
stubFor(get(urlPathEqualTo("/v2/service_instances/" + serviceInstanceId))
.willReturn(ok()
.withBody(cc("list-space-service_instances-empty"))));
.withBody(cc("get-service_instance",
replace("@space-guid", TEST_SPACE_GUID),
replace("@service-guid", serviceGuid(serviceName)),
replace("@plan-guid", planGuid(planName)),
replace("@name", serviceInstanceName),
replace("@guid", serviceInstanceId)))));
stubServiceInstanceExists(serviceInstanceId, serviceInstanceName, serviceName, planName);
stubFor(get(urlPathEqualTo("/v2/service_bindings"))
.withQueryParam("q", equalTo("service_instance_guid:" + serviceInstanceId))
.withQueryParam("page", equalTo("1"))
.willReturn(ok()
.withBody(cc("empty-query-results"))));
stubGetServiceAndGetPlan(serviceName, planName);
}
public void stubServiceExists(String serviceName) {
public void stubGetBackingServiceInstance(String serviceInstanceName, String serviceName, String planName) {
String serviceInstanceId = serviceInstanceGuid(serviceInstanceName);
stubServiceInstanceExists(serviceInstanceId, serviceInstanceName, serviceName, planName);
stubGetServiceAndGetPlan(serviceName, planName);
}
public void stubServiceExists(String serviceName, String planName) {
stubFor(get(urlPathEqualTo("/v2/spaces/" + TEST_SPACE_GUID + "/services"))
.withQueryParam("q", equalTo("label:" + serviceName))
.withQueryParam("page", equalTo("1"))
.willReturn(ok()
.withBody(cc("list-space-services"))));
.withBody(cc("list-space-services")
.replace("@service-guid", serviceGuid(serviceName))
.replace("@plan-guid", planGuid(planName))
.replace("@service-name", serviceName))));
stubFor(get(urlPathEqualTo("/v2/service_plans"))
.withQueryParam("q", equalTo("service_guid:SERVICE-ID"))
.withQueryParam("q", equalTo("service_guid:" + serviceGuid(serviceName)))
.withQueryParam("page", equalTo("1"))
.willReturn(ok()
.withBody(cc("list-service-plans"))));
.withBody(cc("list-service-plans")
.replace("@service-guid", serviceGuid(serviceName))
.replace("@plan-name", planName)
.replace("@plan-guid", planGuid(planName)))));
}
public void stubCreateServiceInstance(String serviceInstanceName) {
@@ -514,7 +568,7 @@ public class CloudControllerStubFixture extends WiremockStubFixture {
.willReturn(noContent()));
}
public void stubListServiceBindings(String appName, String serviceInstanceName) {
public void stubServiceBindingExists(String appName, String serviceInstanceName) {
String serviceInstanceGuid = serviceInstanceGuid(serviceInstanceName);
String serviceBindingGuid = serviceBindingGuid(appName, serviceInstanceName);
@@ -526,11 +580,6 @@ public class CloudControllerStubFixture extends WiremockStubFixture {
replace("@guid", serviceBindingGuid),
replace("@app-guid", appGuid(appName)),
replace("@service-instance-guid", serviceInstanceGuid)))));
}
public void stubServiceBindingExists(String appName, String serviceInstanceName) {
String serviceInstanceGuid = serviceInstanceGuid(serviceInstanceName);
String serviceBindingGuid = serviceBindingGuid(appName, serviceInstanceName);
stubFor(get(urlPathEqualTo("/v2/apps/" + appGuid(appName) + "/service_bindings"))
.withQueryParam("q", equalTo("service_instance_guid:" + serviceInstanceGuid))
@@ -558,6 +607,21 @@ public class CloudControllerStubFixture extends WiremockStubFixture {
.willReturn(ok()));
}
private void stubGetServiceAndGetPlan(String serviceName, String planName) {
stubFor(get(urlPathEqualTo("/v2/service_plans/" + planGuid(planName)))
.willReturn(ok()
.withBody(cc("get-service-plan")
.replace("@service-guid", serviceGuid(serviceName))
.replace("@plan-name", planName)
.replace("@plan-guid", planGuid(planName)))));
stubFor(get(urlPathEqualTo("/v2/services/" + serviceGuid(serviceName)))
.willReturn(ok()
.withBody(cc("get-service")
.replace("@service-guid", serviceGuid(serviceName))
.replace("@service-name", serviceName))));
}
private void stubSpaceExists(final String spaceName) {
stubFor(get(urlPathEqualTo("/v2/organizations/" + TEST_ORG_GUID + "/spaces"))
.withQueryParam("q", equalTo("name:" + spaceName))
@@ -576,35 +640,43 @@ public class CloudControllerStubFixture extends WiremockStubFixture {
return response;
}
private String appGuid(String appName) {
private static String appGuid(String appName) {
return appName + "-GUID";
}
private String routeGuid(String appName) {
private static String routeGuid(String appName) {
return appName + "-ROUTE-GUID";
}
private String stackGuid(String appName) {
private static String stackGuid(String appName) {
return appName + "-STACK-GUID";
}
private String serviceInstanceGuid(String serviceInstanceName) {
return serviceInstanceName + "-GUID";
public static String serviceGuid(String serviceName) {
return serviceName + "-SERVICE-GUID";
}
private String serviceBindingGuid(String appName, String serviceInstanceName) {
public static String planGuid(String planName) {
return planName + "-PLAN-GUID";
}
private static String serviceInstanceGuid(String serviceInstanceName) {
return serviceInstanceName + "-INSTANCE-GUID";
}
private static String serviceBindingGuid(String appName, String serviceInstanceName) {
return appGuid(appName) + "-" + serviceInstanceGuid(serviceInstanceName);
}
private String packageGuid(String appName) {
private static String packageGuid(String appName) {
return appName + "-PACKAGE-GUID";
}
private String buildGuid(String appName) {
private static String buildGuid(String appName) {
return appName + "-BUILD-GUID";
}
private String deploymentGuid(String appname) {
private static String deploymentGuid(String appname) {
return appname + "-DEPLOYMENT-GUID";
}

View File

@@ -3,7 +3,7 @@ spring:
openservicebroker:
catalog:
services:
- id: example-service-id
- id: example-SERVICE-GUID
name: example
description: A simple example
bindable: true
@@ -11,7 +11,7 @@ spring:
- example
tags
plans:
- id: standard-plan-id
- id: standard-PLAN-GUID
bindable: true
name: standard
description: A simple plan

View File

@@ -0,0 +1,65 @@
{
"guid": "@guid",
"name": "@name",
"routes": [
{
"guid": "@route-guid",
"host": "@name",
"port": null,
"path": "",
"domain": {
"guid": "28e83b6f-3cb4-4df4-a056-4e4ae5d0efad",
"name": "apps.example.com"
}
}
],
"running_instances": 1,
"services": [
{
"guid": "@service-guid",
"name": "@service-instance-name",
"bound_app_count": 1,
"type": "managed_service_instance"
}
],
"available_domains": [
{
"guid": "28e83b6f-3cb4-4df4-a056-4e4ae5d0efad",
"name": "apps.example.com",
"router_group_guid": null,
"router_group_type": null
}
],
"production": false,
"space_guid": "c32f8285-5127-42dc-a9b9-6d22bb99fa9d",
"stack_guid": "@stack-guid",
"buildpack": "https://github.com/cloudfoundry/java-buildpack.git#v4.7.1",
"detected_buildpack": "",
"detected_buildpack_guid": null,
"environment_json": {
"SPRING_APPLICATION_INDEX": "${vcap.application.instance_index}",
"SPRING_CLOUD_APPLICATION_GUID": "${vcap.application.name}:${vcap.application.instance_index}",
"SPRING_APPLICATION_JSON": "{}"
},
"memory": 1024,
"instances": 1,
"disk_quota": 1024,
"state": "STARTED",
"version": "54b1eed3-e30c-40ad-8cc6-a7b87fbfb699",
"command": null,
"console": false,
"debug": null,
"staging_task_id": "66a74858-c432-446d-bec5-13026d4cbb28",
"package_state": "STAGED",
"health_check_type": "port",
"health_check_timeout": 120,
"health_check_http_endpoint": "/health",
"staging_failed_reason": null,
"staging_failed_description": null,
"diego": true,
"docker_image": null,
"package_updated_at": "2018-08-15T19:33:41Z",
"detected_start_command": "JAVA_OPTS=\"-agentpath:$PWD/.java-buildpack/open_jdk_jre/bin/jvmkill-1.12.0_RELEASE=printHeapHistogram=1 -Djava.io.tmpdir=$TMPDIR -Djava.ext.dirs=$PWD/.java-buildpack/container_security_provider:$PWD/.java-buildpack/open_jdk_jre/lib/ext -Djava.security.properties=$PWD/.java-buildpack/java_security/java.security $JAVA_OPTS\" && CALCULATED_MEMORY=$($PWD/.java-buildpack/open_jdk_jre/bin/java-buildpack-memory-calculator-3.10.0_RELEASE -totMemory=$MEMORY_LIMIT -stackThreads=300 -loadedClasses=14289 -poolType=metaspace -vmOptions=\"$JAVA_OPTS\") && echo JVM Memory Configuration: $CALCULATED_MEMORY && JAVA_OPTS=\"$JAVA_OPTS $CALCULATED_MEMORY\" && SERVER_PORT=$PORT eval exec $PWD/.java-buildpack/open_jdk_jre/bin/java $JAVA_OPTS -cp $PWD/. org.springframework.boot.loader.JarLauncher",
"enable_ssh": true,
"ports": null
}

View File

@@ -0,0 +1,35 @@
{
"metadata": {
"guid": "@plan-guid",
"url": "/v2/service_plans/@plan-guid",
"created_at": "2018-01-26T20:48:20Z",
"updated_at": "2018-06-11T18:08:05Z"
},
"entity": {
"name": "@plan-name",
"free": true,
"description": "basic",
"service_guid": "@service-guid",
"extra": "{}",
"public": true,
"bindable": true,
"active": true,
"service_url": "/v2/services/@service-guid",
"service_instances_url": "/v2/service_plans/@plan-guid/service_instances",
"schemas": {
"service_instance": {
"create": {
"parameters": {}
},
"update": {
"parameters": {}
}
},
"service_binding": {
"create": {
"parameters": {}
}
}
}
}
}

View File

@@ -0,0 +1,26 @@
{
"metadata": {
"guid": "@service-guid",
"url": "/v2/services/@service-guid",
"created_at": "2018-01-26T20:48:20Z",
"updated_at": "2018-06-11T18:08:05Z"
},
"entity": {
"label": "@service-name",
"provider": null,
"url": null,
"description": "My DB Service",
"long_description": null,
"version": null,
"info_url": null,
"active": 1,
"bindable": 1,
"extra": "{}",
"tags": [
],
"requires": [],
"documentation_url": null,
"plan_updateable": 0,
"service_plans_url": "/v2/services/@service-guid/service_plans"
}
}

View File

@@ -8,8 +8,8 @@
"entity": {
"name": "@name",
"credentials": {},
"service_guid": "a14baddf-1ccc-5299-0152-ab9s49de4422",
"service_plan_guid": "779d2df0-9cdd-48e8-9781-ea05301cedb1",
"service_guid": "@service-guid",
"service_plan_guid": "@plan-guid",
"space_guid": "@space-guid",
"gateway_data": null,
"dashboard_url": null,
@@ -23,8 +23,8 @@
},
"tags": [],
"space_url": "/v2/spaces/@space-guid",
"service_url": "/v2/services/a14baddf-1ccc-5299-0152-ab9s49de4422",
"service_plan_url": "/v2/service_plans/779d2df0-9cdd-48e8-9781-ea05301cedb1",
"service_url": "/v2/services/@service-guid",
"service_plan_url": "/v2/service_plans/@plan-guid",
"service_bindings_url": "/v2/service_instances/@guid/service_bindings",
"service_keys_url": "/v2/service_instances/@guid/service_keys",
"routes_url": "/v2/service_instances/@guid/routes",
@@ -32,4 +32,4 @@
"shared_to_url": "/v2/service_instances/@guid/shared_to",
"service_instance_parameters_url": "/v2/service_instances/@guid/parameters"
}
}
}

View File

@@ -6,22 +6,22 @@
"resources": [
{
"metadata": {
"guid": "SERVICE-PLAN-ID",
"url": "/v2/service_plans/SERVICE-PLAN-ID",
"guid": "@plan-guid",
"url": "/v2/service_plans/@plan-guid",
"created_at": "2018-01-26T20:48:20Z",
"updated_at": "2018-06-11T18:08:05Z"
},
"entity": {
"name": "standard",
"name": "@plan-name",
"free": true,
"description": "basic",
"service_guid": "SERVICE-ID",
"service_guid": "@service-guid",
"extra": "{}",
"public": true,
"bindable": true,
"active": true,
"service_url": "/v2/services/SERVICE-ID",
"service_instances_url": "/v2/service_plans/SERVICE-PLAN-ID/service_instances",
"service_url": "/v2/services/@service-guid",
"service_instances_url": "/v2/service_plans/@plan-guid/service_instances",
"schemas": {
"service_instance": {
"create": {
@@ -40,4 +40,4 @@
}
}
]
}
}

View File

@@ -53,11 +53,11 @@
],
"space_url": "/v2/spaces/@space-guid",
"stack_url": "/v2/stacks/@stack-guid",
"routes_url": "/v2/apps/cae5e4ca-d856-4473-80fd-6f58f9099401/routes",
"events_url": "/v2/apps/cae5e4ca-d856-4473-80fd-6f58f9099401/events",
"service_bindings_url": "/v2/apps/cae5e4ca-d856-4473-80fd-6f58f9099401/service_bindings",
"route_mappings_url": "/v2/apps/cae5e4ca-d856-4473-80fd-6f58f9099401/route_mappings"
"routes_url": "/v2/apps/@guid/routes",
"events_url": "/v2/apps/@guid/events",
"service_bindings_url": "/v2/apps/@guid/service_bindings",
"route_mappings_url": "/v2/apps/@guid/route_mappings"
}
}
]
}
}

View File

@@ -15,14 +15,18 @@
"name": "@name",
"credentials": {},
"space_guid": "@space-guid",
"service_guid": "@service-guid",
"service_plan_guid": "@plan-guid",
"type": "managed_service_instance",
"syslog_drain_url": "",
"route_service_url": "",
"space_url": "/v2/spaces/@space-guid",
"service_bindings_url": "/v2/service_instances/@guid/service_bindings",
"service_keys_url": "/v2/service_instances/@guid/service_keys",
"service_url": "/v2/services/@service-guid",
"service_plan_url": "/v2/service_plans/@plan-guid",
"routes_url": "/v2/service_instances/@guid/routes"
}
}
]
}
}

View File

@@ -6,13 +6,13 @@
"resources": [
{
"metadata": {
"guid": "SERVICE-ID",
"url": "/v2/services/SERVICE-ID",
"guid": "@service-guid",
"url": "/v2/services/@service-guid",
"created_at": "2018-01-26T20:48:20Z",
"updated_at": "2018-06-11T18:08:05Z"
},
"entity": {
"label": "db-service",
"label": "@service-name",
"provider": null,
"url": null,
"description": "My DB Service",
@@ -27,8 +27,8 @@
"requires": [],
"documentation_url": null,
"plan_updateable": 0,
"service_plans_url": "/v2/services/SERVICE-ID/service_plans"
"service_plans_url": "/v2/services/@service-guid/service_plans"
}
}
]
}
}

View File

@@ -0,0 +1,7 @@
{
"total_results": 0,
"total_pages": 1,
"prev_url": null,
"next_url": null,
"resources": []
}