diff --git a/spring-cloud-app-broker-acceptance-tests/src/main/resources/application.yml b/spring-cloud-app-broker-acceptance-tests/src/main/resources/application.yml index 51f15b3..43ce590 100644 --- a/spring-cloud-app-broker-acceptance-tests/src/main/resources/application.yml +++ b/spring-cloud-app-broker-acceptance-tests/src/main/resources/application.yml @@ -10,4 +10,5 @@ logging: level: - cloudfoundry-client: DEBUG \ No newline at end of file + cloudfoundry-client: DEBUG + org.springframework.cloud.appbroker: DEBUG diff --git a/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/CloudFoundryAcceptanceTest.java b/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/CloudFoundryAcceptanceTest.java index 01a1e97..c8afdba 100644 --- a/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/CloudFoundryAcceptanceTest.java +++ b/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/CloudFoundryAcceptanceTest.java @@ -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 appBrokerProperties = getAppBrokerProperties(brokerProperties); + blockingSubscribe(initializeBroker(appBrokerProperties)); + } + + void setUpForBrokerUpdate(BrokerProperties brokerProperties) { + List appBrokerProperties = getAppBrokerProperties(brokerProperties); + blockingSubscribe(updateBroker(appBrokerProperties)); + } + + private List 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 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 updateBroker(List appBrokerProperties) { + return cloudFoundryService + .pushBrokerApp(testBrokerAppName(), getTestBrokerAppPath(), brokerClientId(), appBrokerProperties) + .then(cloudFoundryService.updateServiceBroker(serviceBrokerName(), testBrokerAppName())); + } + private Mono 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 listServiceInstances() { + return cloudFoundryService.listServiceInstances() + .map(ServiceInstanceSummary::getName) + .collectList() + .block(); + } + protected ServiceInstance getServiceInstance(String serviceInstanceName) { return getServiceInstanceMono(serviceInstanceName).block(); } diff --git a/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/CreateInstanceAcceptanceTest.java b/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/CreateInstanceAcceptanceTest.java index f853ffb..48e168b 100644 --- a/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/CreateInstanceAcceptanceTest.java +++ b/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/CreateInstanceAcceptanceTest.java @@ -84,7 +84,7 @@ class CreateInstanceAcceptanceTest extends CloudFoundryAcceptanceTest { Optional 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); }); diff --git a/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/CreateInstanceWithServiceInstanceGuidSuffixTargetAcceptanceTest.java b/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/CreateInstanceWithServiceInstanceGuidSuffixTargetAcceptanceTest.java index 3841b33..56be63c 100644 --- a/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/CreateInstanceWithServiceInstanceGuidSuffixTargetAcceptanceTest.java +++ b/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/CreateInstanceWithServiceInstanceGuidSuffixTargetAcceptanceTest.java @@ -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); } } diff --git a/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/CreateInstanceWithServicesAcceptanceTest.java b/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/CreateInstanceWithServicesAcceptanceTest.java index 502c5e9..bde39ac 100644 --- a/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/CreateInstanceWithServicesAcceptanceTest.java +++ b/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/CreateInstanceWithServicesAcceptanceTest.java @@ -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); } } diff --git a/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/UpdateInstanceWithNewServiceAcceptanceTest.java b/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/UpdateInstanceWithNewServiceAcceptanceTest.java new file mode 100644 index 0000000..883857d --- /dev/null +++ b/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/UpdateInstanceWithNewServiceAcceptanceTest.java @@ -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 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 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); + } + } + +} diff --git a/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/UpdateInstanceWithServicesAcceptanceTest.java b/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/UpdateInstanceWithServicesAcceptanceTest.java index c133ea1..c03c710 100644 --- a/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/UpdateInstanceWithServicesAcceptanceTest.java +++ b/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/UpdateInstanceWithServicesAcceptanceTest.java @@ -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); } } diff --git a/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/fixtures/cf/CloudFoundryService.java b/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/fixtures/cf/CloudFoundryService.java index 1334e8b..fd364e3 100644 --- a/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/fixtures/cf/CloudFoundryService.java +++ b/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/fixtures/cf/CloudFoundryService.java @@ -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 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 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 listServiceInstances() { + return cloudFoundryOperations.services().listInstances(); + } + public Mono getServiceInstance(String serviceInstanceName) { return getServiceInstance(cloudFoundryOperations, serviceInstanceName); } diff --git a/spring-cloud-app-broker-autoconfigure/src/main/java/org/springframework/cloud/appbroker/autoconfigure/AppBrokerAutoConfiguration.java b/spring-cloud-app-broker-autoconfigure/src/main/java/org/springframework/cloud/appbroker/autoconfigure/AppBrokerAutoConfiguration.java index fdf4785..62415bb 100644 --- a/spring-cloud-app-broker-autoconfigure/src/main/java/org/springframework/cloud/appbroker/autoconfigure/AppBrokerAutoConfiguration.java +++ b/spring-cloud-app-broker-autoconfigure/src/main/java/org/springframework/cloud/appbroker/autoconfigure/AppBrokerAutoConfiguration.java @@ -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 ); } diff --git a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/ServicesSpec.java b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/ServicesSpec.java index fc641f4..6363af3 100644 --- a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/ServicesSpec.java +++ b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/ServicesSpec.java @@ -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; diff --git a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/manager/BackingAppManagementService.java b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/manager/BackingAppManagementService.java index a6cf62a..c39bf8f 100644 --- a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/manager/BackingAppManagementService.java +++ b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/manager/BackingAppManagementService.java @@ -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> getBackingApplicationsForService(String serviceInstanceId) { + public Mono 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 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 findBrokeredService(String serviceName, String planName) { diff --git a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentDeleteServiceInstanceWorkflow.java b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentDeleteServiceInstanceWorkflow.java index aa15ae8..f124f45 100644 --- a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentDeleteServiceInstanceWorkflow.java +++ b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentDeleteServiceInstanceWorkflow.java @@ -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 delete(DeleteServiceInstanceRequest request, DeleteServiceInstanceResponse response) { - return undeployBackingApplications(request) - .thenMany(deleteBackingServices(request)) + return deleteBackingServices(request) + .thenMany(undeployBackingApplications(request)) .then(); } private Flux 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'", diff --git a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentUpdateServiceInstanceWorkflow.java b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentUpdateServiceInstanceWorkflow.java index 024324d..177a11f 100644 --- a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentUpdateServiceInstanceWorkflow.java +++ b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentUpdateServiceInstanceWorkflow.java @@ -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 newServices = newAndExisting.getT1(); + Map existingServices = newAndExisting.getT2(); + + Set serviceNamesToUpdate = intersection(newServices.keySet(), existingServices.keySet()); + Set serviceNamesToCreate = subtract(newServices.keySet(), existingServices.keySet()); + Set serviceNamesToDelete = subtract(existingServices.keySet(), newServices.keySet()); + + List servicesToUpdate = servicesInNameList(newServices, serviceNamesToUpdate); + List servicesToCreate = servicesInNameList(newServices, serviceNamesToCreate); + List 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> 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 servicesInNameList(Map services, Set nameList) { + List servicesToKeep = new ArrayList<>(services.values()); + servicesToKeep.removeIf(service -> !nameList.contains(service.getServiceInstanceName())); + return servicesToKeep; + } + + private Set subtract(Set set1, Set set2) { + Set set = new HashSet<>(set1); + set.removeAll(set2); + return set; + } + + private Set intersection(Set set1, Set set2) { + Set set = new HashSet<>(set1); + set.retainAll(set2); + return set; } private Flux updateBackingApplications(UpdateServiceInstanceRequest request) { diff --git a/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/manager/BackingAppManagementServiceTest.java b/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/manager/BackingAppManagementServiceTest.java index 1ec6d43..8f54f8b 100644 --- a/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/manager/BackingAppManagementServiceTest.java +++ b/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/manager/BackingAppManagementServiceTest.java @@ -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))) diff --git a/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentDeleteServiceInstanceWorkflowTest.java b/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentDeleteServiceInstanceWorkflowTest.java index 269c9a2..fc2335d 100644 --- a/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentDeleteServiceInstanceWorkflowTest.java +++ b/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentDeleteServiceInstanceWorkflowTest.java @@ -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(); + } + } diff --git a/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentUpdateServiceInstanceWorkflowTest.java b/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentUpdateServiceInstanceWorkflowTest.java index 138bee7..b35c11d 100644 --- a/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentUpdateServiceInstanceWorkflowTest.java +++ b/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentUpdateServiceInstanceWorkflowTest.java @@ -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(); + } + } diff --git a/spring-cloud-app-broker-deployer-cloudfoundry/src/main/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployer.java b/spring-cloud-app-broker-deployer-cloudfoundry/src/main/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployer.java index 05be80a..07253f5 100644 --- a/spring-cloud-app-broker-deployer-cloudfoundry/src/main/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployer.java +++ b/spring-cloud-app-broker-deployer-cloudfoundry/src/main/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployer.java @@ -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 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 deploy(DeployApplicationRequest request) { String appName = request.getName(); @@ -200,37 +231,58 @@ public class CloudFoundryAppDeployer implements AppDeployer, ResourceLoaderAware public Mono 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 bindNewServices(GetApplicationResponse deployedApp, List services, + Map properties) { + String id = deployedApp.getId(); + List boundServices = deployedApp.getServices(); + List 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 associateHostName(String applicationId, Map properties) { @@ -799,8 +851,8 @@ public class CloudFoundryAppDeployer implements AppDeployer, ResourceLoaderAware private Duration apiPollingTimeout(Map 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 properties) { diff --git a/spring-cloud-app-broker-deployer-cloudfoundry/src/test/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployerTest.java b/spring-cloud-app-broker-deployer-cloudfoundry/src/test/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployerTest.java index 89efb85..b648eec 100644 --- a/spring-cloud-app-broker-deployer-cloudfoundry/src/test/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployerTest.java +++ b/spring-cloud-app-broker-deployer-cloudfoundry/src/test/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployerTest.java @@ -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<>()); diff --git a/spring-cloud-app-broker-deployer-cloudfoundry/src/test/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployerUpdateApplicationTest.java b/spring-cloud-app-broker-deployer-cloudfoundry/src/test/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployerUpdateApplicationTest.java index 7d06aa5..7c000ea 100644 --- a/spring-cloud-app-broker-deployer-cloudfoundry/src/test/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployerUpdateApplicationTest.java +++ b/spring-cloud-app-broker-deployer-cloudfoundry/src/test/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployerUpdateApplicationTest.java @@ -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 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 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(); diff --git a/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/AppDeployer.java b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/AppDeployer.java index fcbae96..fa23a91 100644 --- a/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/AppDeployer.java +++ b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/AppDeployer.java @@ -32,6 +32,10 @@ public interface AppDeployer { return Mono.empty(); } + default Mono get(GetApplicationRequest request) { + return Mono.empty(); + } + default Mono getServiceInstance(GetServiceInstanceRequest request) { return Mono.empty(); } diff --git a/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/GetApplicationRequest.java b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/GetApplicationRequest.java new file mode 100644 index 0000000..18f6807 --- /dev/null +++ b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/GetApplicationRequest.java @@ -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 properties; + + protected GetApplicationRequest(String name, Map properties) { + this.name = name; + this.properties = properties; + } + + public static GetDeployedAppRequestBuilder builder() { + return new GetDeployedAppRequestBuilder(); + } + + public String getName() { + return name; + } + + public Map getProperties() { + return properties; + } + + public static final class GetDeployedAppRequestBuilder { + + private String name; + + private final Map properties = new HashMap<>(); + + private GetDeployedAppRequestBuilder() { + } + + public GetDeployedAppRequestBuilder name(String name) { + this.name = name; + return this; + } + + public GetDeployedAppRequestBuilder properties(Map properties) { + if (!CollectionUtils.isEmpty(properties)) { + this.properties.putAll(properties); + } + return this; + } + + public GetApplicationRequest build() { + return new GetApplicationRequest(name, properties); + } + + } + +} diff --git a/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/GetApplicationResponse.java b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/GetApplicationResponse.java new file mode 100644 index 0000000..055dd0f --- /dev/null +++ b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/GetApplicationResponse.java @@ -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 environment; + + private final List services; + + protected GetApplicationResponse(String id, String name, Map environment, List 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 getEnvironment() { + return environment; + } + + public List getServices() { + return services; + } + + public static GetApplicationResponseBuilder builder() { + return new GetApplicationResponseBuilder(); + } + + public static final class GetApplicationResponseBuilder { + + private String id; + + private String name; + + private final Map environment = new HashMap<>(); + + private final List 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 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 services) { + if (!CollectionUtils.isEmpty(services)) { + this.services.addAll(services); + } + return this; + } + + public GetApplicationResponse build() { + return new GetApplicationResponse(id, name, environment, services); + } + + } + +} diff --git a/spring-cloud-app-broker-docs/src/docs/asciidoc/service-instances.adoc b/spring-cloud-app-broker-docs/src/docs/asciidoc/service-instances.adoc index dd40577..7f22111 100644 --- a/spring-cloud-app-broker-docs/src/docs/asciidoc/service-instances.adoc +++ b/spring-cloud-app-broker-docs/src/docs/asciidoc/service-instances.adoc @@ -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. diff --git a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CatalogComponentTest.java b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CatalogComponentTest.java index 7fac030..06802ff 100644 --- a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CatalogComponentTest.java +++ b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CatalogComponentTest.java @@ -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)) diff --git a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithOAuth2CredentialsComponentTest.java b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithOAuth2CredentialsComponentTest.java index 6c3e3d9..b2b84cc 100644 --- a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithOAuth2CredentialsComponentTest.java +++ b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithOAuth2CredentialsComponentTest.java @@ -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); diff --git a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithOnlyABackingServiceComponentTest.java b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithOnlyABackingServiceComponentTest.java index 925fdcb..c5670fe 100644 --- a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithOnlyABackingServiceComponentTest.java +++ b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithOnlyABackingServiceComponentTest.java @@ -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); diff --git a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithServiceInstanceGuidSuffixTargetComponentTest.java b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithServiceInstanceGuidSuffixTargetComponentTest.java index 28b4ce5..9958c92 100644 --- a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithServiceInstanceGuidSuffixTargetComponentTest.java +++ b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithServiceInstanceGuidSuffixTargetComponentTest.java @@ -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); diff --git a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithServicesComponentTest.java b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithServicesComponentTest.java index 5a92478..c4f8eac 100644 --- a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithServicesComponentTest.java +++ b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithServicesComponentTest.java @@ -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); diff --git a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithServicesParametersComponentTest.java b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithServicesParametersComponentTest.java index f946880..220950f 100644 --- a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithServicesParametersComponentTest.java +++ b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithServicesParametersComponentTest.java @@ -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 expectedCreationParameters = new HashMap<>(); diff --git a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithSpacePerServiceInstanceTargetComponentTest.java b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithSpacePerServiceInstanceTargetComponentTest.java index 26c7e7a..9060b2e 100644 --- a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithSpacePerServiceInstanceTargetComponentTest.java +++ b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithSpacePerServiceInstanceTargetComponentTest.java @@ -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); diff --git a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/DeleteInstanceComponentTest.java b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/DeleteInstanceComponentTest.java index 7fd6de9..2878cd7 100644 --- a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/DeleteInstanceComponentTest.java +++ b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/DeleteInstanceComponentTest.java @@ -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); diff --git a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/DeleteInstanceWithServicesComponentTest.java b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/DeleteInstanceWithServicesComponentTest.java index 578eeb9..07aa8b8 100644 --- a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/DeleteInstanceWithServicesComponentTest.java +++ b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/DeleteInstanceWithServicesComponentTest.java @@ -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() diff --git a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/UpdateInstanceWithNewServiceComponentTest.java b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/UpdateInstanceWithNewServiceComponentTest.java new file mode 100644 index 0000000..f3758bf --- /dev/null +++ b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/UpdateInstanceWithNewServiceComponentTest.java @@ -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()); + } + +} diff --git a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/UpdateInstanceWithServicesComponentTest.java b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/UpdateInstanceWithServicesComponentTest.java index 06394c4..469c9ac 100644 --- a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/UpdateInstanceWithServicesComponentTest.java +++ b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/UpdateInstanceWithServicesComponentTest.java @@ -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 diff --git a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/UpdateInstanceWithServicesParametersComponentTest.java b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/UpdateInstanceWithServicesParametersComponentTest.java index 1993f5b..ba9b30a 100644 --- a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/UpdateInstanceWithServicesParametersComponentTest.java +++ b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/UpdateInstanceWithServicesParametersComponentTest.java @@ -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 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 diff --git a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/UpdateInstanceWithServicesRebindComponentTest.java b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/UpdateInstanceWithServicesRebindComponentTest.java index 3347331..8c6ff41 100644 --- a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/UpdateInstanceWithServicesRebindComponentTest.java +++ b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/UpdateInstanceWithServicesRebindComponentTest.java @@ -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); diff --git a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/fixtures/CloudControllerStubFixture.java b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/fixtures/CloudControllerStubFixture.java index 055f433..7c8c8b3 100644 --- a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/fixtures/CloudControllerStubFixture.java +++ b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/fixtures/CloudControllerStubFixture.java @@ -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"; } diff --git a/spring-cloud-app-broker-integration-tests/src/test/resources/application-openservicebroker-catalog.yml b/spring-cloud-app-broker-integration-tests/src/test/resources/application-openservicebroker-catalog.yml index f90670a..c56f3b4 100644 --- a/spring-cloud-app-broker-integration-tests/src/test/resources/application-openservicebroker-catalog.yml +++ b/spring-cloud-app-broker-integration-tests/src/test/resources/application-openservicebroker-catalog.yml @@ -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 diff --git a/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/get-app-summary-with-backing-service.json b/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/get-app-summary-with-backing-service.json new file mode 100644 index 0000000..7b2cb42 --- /dev/null +++ b/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/get-app-summary-with-backing-service.json @@ -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 +} diff --git a/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/get-service-plan.json b/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/get-service-plan.json new file mode 100644 index 0000000..bc5ff49 --- /dev/null +++ b/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/get-service-plan.json @@ -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": {} + } + } + } + } +} diff --git a/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/get-service.json b/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/get-service.json new file mode 100644 index 0000000..11e253c --- /dev/null +++ b/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/get-service.json @@ -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" + } +} diff --git a/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/get-service_instances.json b/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/get-service_instance.json similarity index 78% rename from spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/get-service_instances.json rename to spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/get-service_instance.json index 9517979..b3671a0 100644 --- a/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/get-service_instances.json +++ b/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/get-service_instance.json @@ -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" } -} \ No newline at end of file +} diff --git a/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/list-service-plans.json b/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/list-service-plans.json index 36ed36c..27f26d7 100644 --- a/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/list-service-plans.json +++ b/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/list-service-plans.json @@ -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 @@ } } ] -} \ No newline at end of file +} diff --git a/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/list-space-apps.json b/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/list-space-apps.json index 643c944..a95c046 100644 --- a/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/list-space-apps.json +++ b/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/list-space-apps.json @@ -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" } } ] -} \ No newline at end of file +} diff --git a/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/list-space-service_instances.json b/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/list-space-service_instances.json index 3563aff..44c3777 100644 --- a/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/list-space-service_instances.json +++ b/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/list-space-service_instances.json @@ -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" } } ] -} \ No newline at end of file +} diff --git a/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/list-space-services.json b/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/list-space-services.json index a412d13..55b0234 100644 --- a/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/list-space-services.json +++ b/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/list-space-services.json @@ -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" } } ] -} \ No newline at end of file +} diff --git a/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/no-service_bindings.json b/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/no-service_bindings.json new file mode 100644 index 0000000..a5fd509 --- /dev/null +++ b/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/no-service_bindings.json @@ -0,0 +1,7 @@ +{ + "total_results": 0, + "total_pages": 1, + "prev_url": null, + "next_url": null, + "resources": [] +}