From 02696c45aa02cbdecc7d76fb4c216618a5ab7b7a Mon Sep 17 00:00:00 2001 From: Yuxin Bai Date: Tue, 9 Mar 2021 13:26:15 -0500 Subject: [PATCH] Set target on bound services so deleteServiceInstance can find the right service Also temporarily disable delete space on deleteServiceInstance Connects to #413 --- ...WithNewServiceAndTargetAcceptanceTest.java | 177 +++++++++++++++++ .../AppBrokerAutoConfiguration.java | 18 +- .../BackingSpaceManagementService.java | 27 +++ .../DefaultBackingSpaceManagementService.java | 56 ++++++ .../appbroker/deployer/DeployerClient.java | 25 ++- .../manager/BackingAppManagementService.java | 3 +- ...ploymentDeleteServiceInstanceWorkflow.java | 109 ++++++----- ...ploymentUpdateServiceInstanceWorkflow.java | 24 ++- ...aultBackingSpaceManagementServiceTest.java | 68 +++++++ .../deployer/DeployerClientTest.java | 30 ++- .../BackingAppManagementServiceTest.java | 6 +- ...mentDeleteServiceInstanceWorkflowTest.java | 83 +++++++- ...mentUpdateServiceInstanceWorkflowTest.java | 8 +- .../cloudfoundry/CloudFoundryAppDeployer.java | 21 +- ...loudFoundryAppDeployerDeleteSpaceTest.java | 180 ++++++++++++++++++ .../CloudFoundryAppDeployerTest.java | 59 +----- .../cloud/appbroker/deployer/AppDeployer.java | 6 +- .../deployer/DeleteBackingSpaceRequest.java | 53 ++++++ .../deployer/DeleteBackingSpaceResponse.java | 53 ++++++ ...InstanceWithCustomTargetComponentTest.java | 18 +- ...PerServiceInstanceTargetComponentTest.java | 11 +- ...eWithNewServiceAndTargetComponentTest.java | 121 ++++++++++++ .../fixtures/CloudControllerStubFixture.java | 125 ++++++++---- .../fixtures/WiremockStubFixture.java | 7 +- .../cloudcontroller/list-shared-domains.json | 2 +- .../cloudcontroller/list-spaces.json | 2 +- 26 files changed, 1105 insertions(+), 187 deletions(-) create mode 100644 spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/UpdateInstanceWithNewServiceAndTargetAcceptanceTest.java create mode 100644 spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/BackingSpaceManagementService.java create mode 100644 spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/DefaultBackingSpaceManagementService.java create mode 100644 spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/deployer/DefaultBackingSpaceManagementServiceTest.java create mode 100644 spring-cloud-app-broker-deployer-cloudfoundry/src/test/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployerDeleteSpaceTest.java create mode 100644 spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/DeleteBackingSpaceRequest.java create mode 100644 spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/DeleteBackingSpaceResponse.java create mode 100644 spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/UpdateInstanceWithNewServiceAndTargetComponentTest.java diff --git a/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/UpdateInstanceWithNewServiceAndTargetAcceptanceTest.java b/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/UpdateInstanceWithNewServiceAndTargetAcceptanceTest.java new file mode 100644 index 0000000..ff6398e --- /dev/null +++ b/spring-cloud-app-broker-acceptance-tests/src/test/java/org/springframework/cloud/appbroker/acceptance/UpdateInstanceWithNewServiceAndTargetAcceptanceTest.java @@ -0,0 +1,177 @@ +/* + * Copyright 2002-2021 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 UpdateInstanceWithNewServiceAndTargetAcceptanceTest extends CloudFoundryAcceptanceTest { + + private static final String APP_NAME = "app-new-service"; + + private static final int FIRST_TEST = 1; + + private static final int SECOND_TEST = 2; + + private static final String SI_NAME = "si-new-service"; + + 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-and-target"; + + 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, + + "spring.cloud.appbroker.services[0].target.name=SpacePerServiceInstance" + }) + void weCreateAService() { + // when a service instance is created + createServiceInstance(SI_NAME); + + // then a backing application is deployed + String spaceName = getServiceInstanceGuid(SI_NAME); + + Optional backingApplication = getApplicationSummary(APP_NAME, spaceName); + assertThat(backingApplication).hasValueSatisfying(app -> assertThat(app.getRunningInstances()).isEqualTo(1)); + + // and the services are bound to it + ServiceInstance backingServiceInstance = getBackingServiceInstance(OLD_BACKING_SI_NAME, spaceName); + 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, + + "spring.cloud.appbroker.services[0].target.name=SpacePerServiceInstance" + }) + void weUpdateTheServiceInstanceWithANewBackingService() { + // when the service instance is updated with a new service + updateServiceInstance(SI_NAME, Collections.emptyMap()); + String spaceName = getServiceInstanceGuid(SI_NAME); + + // 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, spaceName); + assertThat(updatedBackingApplication).hasValueSatisfying(app -> + assertThat(app.getRunningInstances()).isEqualTo(1)); + + // and the new backing service is bound to it + ServiceInstance newBackingServiceInstance = getBackingServiceInstance(NEW_BACKING_SI_NAME, spaceName); + 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-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 3a20a1e..abd52e4 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -31,9 +31,11 @@ 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.BackingSpaceManagementService; import org.springframework.cloud.appbroker.deployer.BrokeredServices; import org.springframework.cloud.appbroker.deployer.DefaultBackingAppDeploymentService; import org.springframework.cloud.appbroker.deployer.DefaultBackingServicesProvisionService; +import org.springframework.cloud.appbroker.deployer.DefaultBackingSpaceManagementService; import org.springframework.cloud.appbroker.deployer.DeployerClient; import org.springframework.cloud.appbroker.extensions.credentials.CredentialGenerator; import org.springframework.cloud.appbroker.extensions.credentials.CredentialProviderFactory; @@ -324,6 +326,18 @@ public class AppBrokerAutoConfiguration { return new DefaultBackingServicesProvisionService(deployerClient); } + /** + * Provide a {@link BackingSpaceManagementService} bean + * + * @param deployerClient the DeployerClient bean + * @return the bean + */ + @Bean + @ConditionalOnMissingBean + public BackingSpaceManagementService backingSpaceProvisionService(DeployerClient deployerClient) { + return new DefaultBackingSpaceManagementService(deployerClient); + } + /** * Provide a {@link CreateServiceInstanceWorkflow} bean * @@ -401,6 +415,7 @@ public class AppBrokerAutoConfiguration { BrokeredServices brokeredServices, BackingAppDeploymentService backingAppDeploymentService, BackingAppManagementService backingAppManagementService, BackingServicesProvisionService backingServicesProvisionService, + BackingSpaceManagementService backingSpaceManagementService, CredentialProviderService credentialProviderService, TargetService targetService) { return new AppDeploymentDeleteServiceInstanceWorkflow( @@ -408,6 +423,7 @@ public class AppBrokerAutoConfiguration { backingAppDeploymentService, backingAppManagementService, backingServicesProvisionService, + backingSpaceManagementService, credentialProviderService, targetService ); diff --git a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/BackingSpaceManagementService.java b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/BackingSpaceManagementService.java new file mode 100644 index 0000000..41ad96e --- /dev/null +++ b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/BackingSpaceManagementService.java @@ -0,0 +1,27 @@ +/* + * Copyright 2002-2021 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.List; + +import reactor.core.publisher.Flux; + +public interface BackingSpaceManagementService { + + Flux deleteTargetSpaces(List targetSpaces); + +} diff --git a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/DefaultBackingSpaceManagementService.java b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/DefaultBackingSpaceManagementService.java new file mode 100644 index 0000000..31219a2 --- /dev/null +++ b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/DefaultBackingSpaceManagementService.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2021 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.List; + +import reactor.core.publisher.Flux; +import reactor.core.scheduler.Schedulers; +import reactor.util.Logger; +import reactor.util.Loggers; + +public class DefaultBackingSpaceManagementService implements BackingSpaceManagementService { + + private static final Logger LOG = Loggers.getLogger(DefaultBackingSpaceManagementService.class); + + private static final String BACKINGSPACES_LOG_TEMPLATE = "backingSpaces={}"; + + private final DeployerClient deployerClient; + + public DefaultBackingSpaceManagementService(DeployerClient deployerClient) { + this.deployerClient = deployerClient; + } + + @Override + public Flux deleteTargetSpaces(List targetSpaces) { + return Flux.fromIterable(targetSpaces) + .parallel() + .runOn(Schedulers.parallel()) + .flatMap(deployerClient::deleteSpace) + .sequential() + .doOnRequest(l -> { + LOG.info("Deleting backing spaces"); + LOG.debug(BACKINGSPACES_LOG_TEMPLATE, targetSpaces); + }) + .doOnComplete(() -> { + LOG.info("Finish deleting backing spaces"); + LOG.debug(BACKINGSPACES_LOG_TEMPLATE, targetSpaces); + }) + .doOnError(e -> LOG.error(String.format("Error deleting backing spaces. error=%s", e.getMessage()), e)); + } + +} diff --git a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/DeployerClient.java b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/DeployerClient.java index 27add78..cfaacaa 100644 --- a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/DeployerClient.java +++ b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/DeployerClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -204,6 +204,29 @@ public class DeployerClient { .map(DeleteServiceInstanceResponse::getName); } + public Mono deleteSpace(String spaceName) { + return appDeployer + .deleteBackingSpace( + DeleteBackingSpaceRequest + .builder() + .name(spaceName) + .build()) + .doOnRequest(l -> { + LOG.info("Deleting backing space {}", spaceName); + }) + .doOnSuccess(response -> { + LOG.info("Success deleting backing space {}", spaceName); + }) + .doOnError(e -> { + LOG.error(String.format("Error deleting backing space. backingSpaceName=%s, error=%s", + spaceName, e.getMessage()), e); + }) + .onErrorReturn(DeleteBackingSpaceResponse.builder() + .name(spaceName) + .build()) + .map(DeleteBackingSpaceResponse::getName); + } + private static UpdateApplicationRequest getUpdateApplicationRequest(BackingApplication backingApplication, String serviceInstanceId) { return UpdateApplicationRequest 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 85aede7..acdfcdc 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors + * Copyright 2002-2021 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. @@ -275,6 +275,7 @@ public class BackingAppManagementService { .builder() .name(response.getName()) .services(services) + .properties(app.getProperties()) .environment(response.getEnvironment()) .build())) .doOnRequest(l -> { 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 37da8e3..6905141 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -17,6 +17,7 @@ package org.springframework.cloud.appbroker.workflow.instance; import java.util.Collections; +import java.util.List; import java.util.Map; import reactor.core.publisher.Flux; @@ -25,8 +26,10 @@ 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.BackingSpaceManagementService; import org.springframework.cloud.appbroker.deployer.BrokeredServices; import org.springframework.cloud.appbroker.deployer.DeploymentProperties; import org.springframework.cloud.appbroker.extensions.credentials.CredentialProviderService; @@ -52,21 +55,25 @@ public class AppDeploymentDeleteServiceInstanceWorkflow private final BackingAppManagementService backingAppManagementService; + private final BackingServicesProvisionService backingServicesProvisionService; + + private final BackingSpaceManagementService backingSpaceManagementService; + private final CredentialProviderService credentialProviderService; private final TargetService targetService; - private final BackingServicesProvisionService backingServicesProvisionService; - public AppDeploymentDeleteServiceInstanceWorkflow(BrokeredServices brokeredServices, BackingAppDeploymentService deploymentService, BackingAppManagementService backingAppManagementService, BackingServicesProvisionService backingServicesProvisionService, + BackingSpaceManagementService backingSpaceManagementService, CredentialProviderService credentialProviderService, TargetService targetService) { super(brokeredServices); this.deploymentService = deploymentService; this.backingAppManagementService = backingAppManagementService; + this.backingSpaceManagementService = backingSpaceManagementService; this.credentialProviderService = credentialProviderService; this.targetService = targetService; this.backingServicesProvisionService = backingServicesProvisionService; @@ -75,36 +82,47 @@ public class AppDeploymentDeleteServiceInstanceWorkflow @Override public Mono delete(DeleteServiceInstanceRequest request, DeleteServiceInstanceResponse response) { return deleteBackingServices(request) - .thenMany(undeployBackingApplications(request)) + .flatMapMany(Flux::fromIterable) + .map(BackingService::getProperties) + .concatWith(undeployBackingApplications(request) + .flatMapMany(Flux::fromIterable) + .map(BackingApplication::getProperties)) + .filter(properties -> + properties != null && properties.containsKey(DeploymentProperties.TARGET_PROPERTY_KEY)) + .map(properties -> properties.get(DeploymentProperties.TARGET_PROPERTY_KEY)) + .distinct() + .collectList() + .flatMapMany(backingSpaceManagementService::deleteTargetSpaces) .then(); } - private Flux deleteBackingServices(DeleteServiceInstanceRequest request) { + private Mono> deleteBackingServices(DeleteServiceInstanceRequest request) { return collectBackingServices(request) .collectList() - .flatMapMany(backingServices -> { + .delayUntil(backingServices -> { if (!CollectionUtils.isEmpty(backingServices)) { - return backingServicesProvisionService.deleteServiceInstance(backingServices); + return backingServicesProvisionService.deleteServiceInstance(backingServices) + .doOnRequest(l -> { + LOG.info("Deleting backing services. serviceDefinitionName={}, planName={}", + request.getServiceDefinition().getName(), request.getPlan().getName()); + LOG.debug(REQUEST_LOG_TEMPLATE, request); + }) + .doOnComplete(() -> { + LOG.info("Finish deleting backing services. serviceDefinitionName={}, planName={}", + request.getServiceDefinition().getName(), request.getPlan().getName()); + LOG.debug(REQUEST_LOG_TEMPLATE, request); + }) + .doOnError(e -> { + if (LOG.isErrorEnabled()) { + LOG.error(String.format("Error deleting backing services. " + + "serviceDefinitionName=%s, planName=%s, error=%s", + request.getServiceDefinition().getName(), + request.getPlan().getName(), e.getMessage()), e); + } + LOG.debug(REQUEST_LOG_TEMPLATE, request); + }); } return Flux.empty(); - }) - .doOnRequest(l -> { - LOG.info("Deleting backing services. serviceDefinitionName={}, planName={}", - request.getServiceDefinition().getName(), request.getPlan().getName()); - LOG.debug(REQUEST_LOG_TEMPLATE, request); - }) - .doOnComplete(() -> { - LOG.info("Finish deleting backing services. serviceDefinitionName={}, planName={}", - request.getServiceDefinition().getName(), request.getPlan().getName()); - LOG.debug(REQUEST_LOG_TEMPLATE, request); - }) - .doOnError(e -> { - if (LOG.isErrorEnabled()) { - LOG.error(String.format("Error deleting backing services. " + - "serviceDefinitionName=%s, planName=%s, error=%s", request.getServiceDefinition().getName(), - request.getPlan().getName(), e.getMessage()), e); - } - LOG.debug(REQUEST_LOG_TEMPLATE, request); }); } @@ -144,7 +162,7 @@ public class AppDeploymentDeleteServiceInstanceWorkflow })); } - private Flux undeployBackingApplications(DeleteServiceInstanceRequest request) { + private Mono> undeployBackingApplications(DeleteServiceInstanceRequest request) { return getBackingApplicationsForService(request.getServiceDefinition(), request.getPlan()) .flatMap(backingApps -> credentialProviderService.deleteCredentials(backingApps, @@ -153,25 +171,26 @@ public class AppDeploymentDeleteServiceInstanceWorkflow .flatMap(targetSpec -> targetService.addToBackingApplications(backingApps, targetSpec, request.getServiceInstanceId())) .defaultIfEmpty(backingApps)) - .flatMapMany(deploymentService::undeploy) - .doOnRequest(l -> { - LOG.info("Undeploying backing applications. serviceDefinitionName={}, planName={}", - request.getServiceDefinition().getName(), request.getPlan().getName()); - LOG.debug(REQUEST_LOG_TEMPLATE, request); - }) - .doOnComplete(() -> { - LOG.info("Finish undeploying backing applications. serviceDefinitionName={}, planName={}", - request.getServiceDefinition().getName(), request.getPlan().getName()); - LOG.debug(REQUEST_LOG_TEMPLATE, request); - }) - .doOnError(e -> { - if (LOG.isErrorEnabled()) { - LOG.error(String.format("Error undeploying backing applications. serviceDefinitionName=%s, " + - "planName=%s, error=%s", request.getServiceDefinition().getName(), request.getPlan().getName(), - e.getMessage()), e); - } - LOG.debug(REQUEST_LOG_TEMPLATE, request); - }); + .delayUntil(backingApps -> deploymentService.undeploy(backingApps) + .doOnRequest(l -> { + LOG.info("Undeploying backing applications. serviceDefinitionName={}, planName={}", + request.getServiceDefinition().getName(), request.getPlan().getName()); + LOG.debug(REQUEST_LOG_TEMPLATE, request); + }) + .doOnComplete(() -> { + LOG.info("Finish undeploying backing applications. serviceDefinitionName={}, planName={}", + request.getServiceDefinition().getName(), request.getPlan().getName()); + LOG.debug(REQUEST_LOG_TEMPLATE, request); + }) + .doOnError(e -> { + if (LOG.isErrorEnabled()) { + LOG.error(String.format("Error undeploying backing applications. serviceDefinitionName=%s, " + + "planName=%s, error=%s", request.getServiceDefinition().getName(), + request.getPlan().getName(), + e.getMessage()), e); + } + LOG.debug(REQUEST_LOG_TEMPLATE, request); + })); } @Override 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 6e67709..fe6e6f9 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -17,6 +17,7 @@ package org.springframework.cloud.appbroker.workflow.instance; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -34,6 +35,7 @@ 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.deployer.DeploymentProperties; import org.springframework.cloud.appbroker.extensions.parameters.BackingApplicationsParametersTransformationService; import org.springframework.cloud.appbroker.extensions.parameters.BackingServicesParametersTransformationService; import org.springframework.cloud.appbroker.extensions.targets.TargetService; @@ -178,12 +180,22 @@ public class AppDeploymentUpdateServiceInstanceWorkflow extends AppDeploymentIns return backingAppManagementService.getDeployedBackingApplications(request.getServiceInstanceId(), request.getServiceDefinition().getName(), request.getPlan().getName()) .flatMapMany(Flux::fromIterable) - .map(BackingApplication::getServices) - .flatMap(Flux::fromIterable) + .flatMap(backingApplication -> Flux + .fromIterable(backingApplication.getServices()) + .map(servicesSpec -> { + Map properties = null; + if (backingApplication.getProperties() != null) { + String target = backingApplication.getProperties().get(DeploymentProperties.TARGET_PROPERTY_KEY); + if (target != null) { + properties = Collections.singletonMap(DeploymentProperties.TARGET_PROPERTY_KEY, target); + } + } + return BackingService.builder() + .serviceInstanceName(servicesSpec.getServiceInstanceName()) + .properties(properties) + .build(); + })) .distinct() - .map(servicesSpec -> BackingService.builder() - .serviceInstanceName(servicesSpec.getServiceInstanceName()) - .build()) .collectMap(BackingService::getServiceInstanceName, Function.identity()); } diff --git a/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/deployer/DefaultBackingSpaceManagementServiceTest.java b/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/deployer/DefaultBackingSpaceManagementServiceTest.java new file mode 100644 index 0000000..24721dd --- /dev/null +++ b/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/deployer/DefaultBackingSpaceManagementServiceTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2021 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.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import static java.util.Arrays.asList; +import static org.mockito.Mockito.doReturn; + +@ExtendWith(MockitoExtension.class) +class DefaultBackingSpaceManagementServiceTest { + + @Mock + private DeployerClient deployerClient; + + private BackingSpaceManagementService backingSpaceManagementService; + + @BeforeEach + void setUp() { + backingSpaceManagementService = new DefaultBackingSpaceManagementService(deployerClient); + } + + @Test + @SuppressWarnings("UnassignedFluxMonoInstance") + void deleteSpace() { + final String spaceName1 = "space1"; + final String spaceName2 = "space2"; + doReturn(Mono.just("returned-space-1")) + .when(deployerClient).deleteSpace(spaceName1); + doReturn(Mono.just("returned-space-2")) + .when(deployerClient).deleteSpace(spaceName2); + + List expectedValues = new ArrayList<>(); + expectedValues.add("returned-space-1"); + expectedValues.add("returned-space-2"); + + StepVerifier.create(backingSpaceManagementService.deleteTargetSpaces(asList(spaceName1, spaceName2))) + // deployments are run in parallel, so the order of completion is not predictable + // ensure that both expected signals are sent in any order + .expectNextMatches(expectedValues::remove) + .expectNextMatches(expectedValues::remove) + .verifyComplete(); + } + +} diff --git a/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/deployer/DeployerClientTest.java b/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/deployer/DeployerClientTest.java index ae4ac65..206bbaf 100644 --- a/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/deployer/DeployerClientTest.java +++ b/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/deployer/DeployerClientTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -420,4 +420,32 @@ class DeployerClientTest { } + @Nested + class DeleteSpace { + + private static final String RETURNED_SPACE_NAME = "space"; + + @Test + void shouldDeleteSpace() { + // given + setupAppDeployer(); + + // when + StepVerifier.create(deployerClient.deleteSpace("test-space")) + // then + .expectNext(RETURNED_SPACE_NAME) + .verifyComplete(); + + then(appDeployer).should().deleteBackingSpace(argThat(space -> "test-space".equals(space.getName()))); + } + + + private void setupAppDeployer() { + given(appDeployer.deleteBackingSpace(any())) + .willReturn(Mono.just(DeleteBackingSpaceResponse.builder() + .name(RETURNED_SPACE_NAME) + .build())); + } + } + } 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 8f54f8b..0137d8c 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors + * Copyright 2002-2021 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. @@ -16,6 +16,8 @@ package org.springframework.cloud.appbroker.manager; +import java.util.Collections; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -68,6 +70,7 @@ class BackingAppManagementServiceTest { this.backingApps = BackingApplications.builder() .backingApplication(BackingApplication.builder() .name("testApp1") + .properties(Collections.singletonMap("target", "customTarget")) .path("https://myfiles/app1.jar") .build()) .backingApplication(BackingApplication.builder() @@ -344,6 +347,7 @@ class BackingAppManagementServiceTest { .expectNext(BackingApplications.builder() .backingApplication(BackingApplication.builder() .name("testApp1") + .properties(Collections.singletonMap("target", "customTarget")) .services( ServicesSpec.builder().serviceInstanceName("service1").build(), ServicesSpec.builder().serviceInstanceName("service2").build()) 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 467fe9b..9fff1c2 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -16,7 +16,8 @@ package org.springframework.cloud.appbroker.workflow.instance; -import java.util.Collections; +import java.util.Arrays; +import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -34,6 +35,7 @@ import org.springframework.cloud.appbroker.deployer.BackingApplications; import org.springframework.cloud.appbroker.deployer.BackingService; import org.springframework.cloud.appbroker.deployer.BackingServices; import org.springframework.cloud.appbroker.deployer.BackingServicesProvisionService; +import org.springframework.cloud.appbroker.deployer.BackingSpaceManagementService; import org.springframework.cloud.appbroker.deployer.BrokeredService; import org.springframework.cloud.appbroker.deployer.BrokeredServices; import org.springframework.cloud.appbroker.deployer.DeploymentProperties; @@ -48,6 +50,9 @@ 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 java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; @@ -64,15 +69,18 @@ class AppDeploymentDeleteServiceInstanceWorkflowTest { @Mock private BackingAppManagementService backingAppManagementService; + @Mock + private BackingServicesProvisionService backingServicesProvisionService; + + @Mock + private BackingSpaceManagementService backingSpaceManagementService; + @Mock private TargetService targetService; @Mock private CredentialProviderService credentialProviderService; - @Mock - private BackingServicesProvisionService backingServicesProvisionService; - private BackingApplications backingApps; private BackingServices backingServices; @@ -116,7 +124,7 @@ class AppDeploymentDeleteServiceInstanceWorkflowTest { .name("my-service2") .plan("a-plan2") .serviceInstanceName("my-service-instance2") - .properties(Collections.singletonMap(DeploymentProperties.TARGET_PROPERTY_KEY, "my-space2")) + .properties(getPropertiesWithSpace("my-space2")) .build()) .build(); @@ -153,7 +161,9 @@ class AppDeploymentDeleteServiceInstanceWorkflowTest { new AppDeploymentDeleteServiceInstanceWorkflow( brokeredServices, backingAppDeploymentService, - backingAppManagementService, backingServicesProvisionService, + backingAppManagementService, + backingServicesProvisionService, + backingSpaceManagementService, credentialProviderService, targetService ); @@ -186,13 +196,56 @@ class AppDeploymentDeleteServiceInstanceWorkflowTest { return sizeMatch && nameMatch; }))).willReturn(Flux.just("my-service-instance")); + given(this.backingSpaceManagementService.deleteTargetSpaces(any())).willReturn(Flux.empty()); + StepVerifier .create(deleteServiceInstanceWorkflow.delete(request, response)) - .expectNext() - .expectNext() .verifyComplete(); verify(this.backingServicesProvisionService, Mockito.times(1)).deleteServiceInstance(any()); + verify(this.backingSpaceManagementService).deleteTargetSpaces(eq(emptyList())); + verifyNoMoreInteractionsWithServices(); + } + + @Test + void deleteBackingSpacesSpecifiedInTargetProperties() { + DeleteServiceInstanceRequest request = buildRequest("service1", "plan1"); + DeleteServiceInstanceResponse response = DeleteServiceInstanceResponse.builder().build(); + String space1 = "CustomSpace1"; + String space2 = "CustomSpace2"; + + // configured backing services + BackingServices servicesWithTarget = BackingServices.builder().backingServices(backingServices).build(); + servicesWithTarget.get(0).setProperties(getPropertiesWithSpace(space1)); + given(this.targetService.addToBackingServices(eq(backingServices), eq(targetSpec), eq("service-instance-id"))) + .willReturn(Mono.just(servicesWithTarget)); + + // configured backing apps + BackingApplications appsWithTarget = BackingApplications.builder().backingApplications(backingApps).build(); + appsWithTarget.get(0).setProperties(getPropertiesWithSpace(space2)); + given(this.targetService.addToBackingApplications(eq(backingApps), eq(targetSpec), eq("service-instance-id"))) + .willReturn(Mono.just(appsWithTarget)); + + // services bound to deployed apps + given(this.backingAppManagementService.getDeployedBackingApplications(request.getServiceInstanceId(), + request.getServiceDefinition().getName(), request.getPlan().getName())) + .willReturn(Mono.just(getExistingBackingAppsWithService("my-service-instance"))); + given(this.credentialProviderService.deleteCredentials(eq(backingApps), eq(request.getServiceInstanceId()))) + .willReturn(Mono.just(backingApps)); + + // delete in action + given(this.backingAppDeploymentService.undeploy(eq(appsWithTarget))) + .willReturn(Flux.just("undeployed1", "undeployed2")); + given(this.backingServicesProvisionService.deleteServiceInstance(any())) + .willReturn(Flux.just("my-service-instance")); + given(this.backingSpaceManagementService.deleteTargetSpaces(any())).willReturn(Flux.just("space-name")); + + StepVerifier + .create(deleteServiceInstanceWorkflow.delete(request, response)) + .verifyComplete(); + + verify(this.backingServicesProvisionService, Mockito.times(1)).deleteServiceInstance(any()); + verify(this.backingSpaceManagementService).deleteTargetSpaces(Arrays.asList(space1, space2)); verifyNoMoreInteractionsWithServices(); } @@ -228,6 +281,9 @@ class AppDeploymentDeleteServiceInstanceWorkflowTest { return sizeMatch && (nameMatch0 && spaceMatch0 || nameMatch1 && spaceMatch1); }))).willReturn(Flux.just("different-service-instance")); + given(this.backingSpaceManagementService.deleteTargetSpaces(eq(singletonList("my-space2")))) + .willReturn(Flux.just("space-name")); + StepVerifier.create(deleteServiceInstanceWorkflow.delete(request, response)) .expectNext() .expectNext() @@ -246,6 +302,8 @@ class AppDeploymentDeleteServiceInstanceWorkflowTest { request.getServiceDefinition().getName(), request.getPlan().getName())) .willReturn(Mono.empty()); + given(this.backingSpaceManagementService.deleteTargetSpaces(any())).willReturn(Flux.empty()); + StepVerifier .create(deleteServiceInstanceWorkflow.delete(request, response)) .verifyComplete(); @@ -273,6 +331,8 @@ class AppDeploymentDeleteServiceInstanceWorkflowTest { return sizeMatch && nameMatch; }))).willReturn(Flux.just("my-service-instance")); + given(this.backingSpaceManagementService.deleteTargetSpaces(any())).willReturn(Flux.empty()); + StepVerifier.create(deleteServiceInstanceWorkflow.delete(request, response)) .expectNext() .expectNext() @@ -285,10 +345,15 @@ class AppDeploymentDeleteServiceInstanceWorkflowTest { private void verifyNoMoreInteractionsWithServices() { verifyNoMoreInteractions(this.backingServicesProvisionService); verifyNoMoreInteractions(this.backingAppDeploymentService); + verifyNoMoreInteractions(this.backingSpaceManagementService); verifyNoMoreInteractions(this.credentialProviderService); verifyNoMoreInteractions(this.targetService); } + private Map getPropertiesWithSpace(String customSpace) { + return singletonMap(DeploymentProperties.TARGET_PROPERTY_KEY, customSpace); + } + private DeleteServiceInstanceRequest buildRequest(String serviceName, String planName) { return DeleteServiceInstanceRequest .builder() 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 1eb34a8..f0abb6b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -207,6 +207,7 @@ class AppDeploymentUpdateServiceInstanceWorkflowTest { .builder() .backingService(BackingService .builder() + .properties(singletonMap("target", "customTarget")) .serviceInstanceName("existing-service-instance") .build()) .build()); @@ -295,11 +296,15 @@ class AppDeploymentUpdateServiceInstanceWorkflowTest { } private BackingApplications getExistingBackingAppsWithService(String serviceInstanceName) { + Map properties = new HashMap<>(); + properties.put("target", "customTarget"); + properties.put("not-important-property", "not-important-value"); return BackingApplications .builder() .backingApplication(BackingApplication .builder() .name("app1") + .properties(properties) .services(ServicesSpec .builder() .serviceInstanceName(serviceInstanceName) @@ -308,6 +313,7 @@ class AppDeploymentUpdateServiceInstanceWorkflowTest { .backingApplication(BackingApplication .builder() .name("app2") + .properties(properties) .services(ServicesSpec .builder() .serviceInstanceName(serviceInstanceName) 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 ad0f7ba..c4f8d81 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -108,6 +108,8 @@ import reactor.core.publisher.Mono; import org.springframework.cloud.appbroker.deployer.AppDeployer; import org.springframework.cloud.appbroker.deployer.CreateServiceInstanceRequest; import org.springframework.cloud.appbroker.deployer.CreateServiceInstanceResponse; +import org.springframework.cloud.appbroker.deployer.DeleteBackingSpaceRequest; +import org.springframework.cloud.appbroker.deployer.DeleteBackingSpaceResponse; import org.springframework.cloud.appbroker.deployer.DeleteServiceInstanceRequest; import org.springframework.cloud.appbroker.deployer.DeleteServiceInstanceResponse; import org.springframework.cloud.appbroker.deployer.DeployApplicationRequest; @@ -802,8 +804,7 @@ public class CloudFoundryAppDeployer implements AppDeployer, ResourceLoaderAware Mono requestDeleteApplication; if (deploymentProperties.containsKey(DeploymentProperties.TARGET_PROPERTY_KEY)) { String space = deploymentProperties.get(DeploymentProperties.TARGET_PROPERTY_KEY); - requestDeleteApplication = deleteApplicationInSpace(appName, space) - .then(deleteSpace(space)); + requestDeleteApplication = deleteApplicationInSpace(appName, space); } else { requestDeleteApplication = deleteApplication(appName); @@ -840,19 +841,21 @@ public class CloudFoundryAppDeployer implements AppDeployer, ResourceLoaderAware .onErrorResume(e -> Mono.empty()); } - private Mono deleteSpace(String spaceName) { + @Override + public Mono deleteBackingSpace(DeleteBackingSpaceRequest request) { + String spaceName = request.getName(); return getSpaceId(spaceName) .doOnError(e -> LOG.error(String.format("Unable to get space name. spaceName=%s, " + ERROR_LOG_TEMPLATE, spaceName, e.getMessage()), e)) + .onErrorResume(e -> Mono.empty()) .flatMap(spaceId -> this.client.spaces() .delete(DeleteSpaceRequest.builder() .spaceId(spaceId) .recursive(true) - .build()) - .then()) + .build())) .doOnError(e -> LOG.error(String.format("Error deleting space. spaceName=%s, " + ERROR_LOG_TEMPLATE, spaceName, e.getMessage()), e)) - .onErrorResume(e -> Mono.empty()); + .thenReturn(DeleteBackingSpaceResponse.builder().name(spaceName).build()); } private Mono getSpaceId(String spaceName) { @@ -1185,11 +1188,9 @@ public class CloudFoundryAppDeployer implements AppDeployer, ResourceLoaderAware Mono requestDeleteServiceInstance; if (deploymentProperties.containsKey(DeploymentProperties.TARGET_PROPERTY_KEY)) { - String space = deploymentProperties.get(DeploymentProperties.TARGET_PROPERTY_KEY); requestDeleteServiceInstance = operationsUtils.getOperations(deploymentProperties) .flatMap(cfOperations -> unbindServiceInstance(serviceInstanceName, cfOperations) - .then(deleteServiceInstance(serviceInstanceName, cfOperations, deploymentProperties))) - .then(deleteSpace(space)); + .then(deleteServiceInstance(serviceInstanceName, cfOperations, deploymentProperties))); } else { requestDeleteServiceInstance = unbindServiceInstance(serviceInstanceName, operations) diff --git a/spring-cloud-app-broker-deployer-cloudfoundry/src/test/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployerDeleteSpaceTest.java b/spring-cloud-app-broker-deployer-cloudfoundry/src/test/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployerDeleteSpaceTest.java new file mode 100644 index 0000000..c6a9b96 --- /dev/null +++ b/spring-cloud-app-broker-deployer-cloudfoundry/src/test/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployerDeleteSpaceTest.java @@ -0,0 +1,180 @@ +/* + * Copyright 2002-2021 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.cloudfoundry; + +import org.cloudfoundry.client.CloudFoundryClient; +import org.cloudfoundry.client.v2.Metadata; +import org.cloudfoundry.client.v2.organizations.ListOrganizationSpacesRequest; +import org.cloudfoundry.client.v2.organizations.ListOrganizationSpacesResponse; +import org.cloudfoundry.client.v2.spaces.DeleteSpaceRequest; +import org.cloudfoundry.client.v2.spaces.SpaceEntity; +import org.cloudfoundry.client.v2.spaces.SpaceResource; +import org.cloudfoundry.operations.CloudFoundryOperations; +import org.cloudfoundry.operations.organizations.OrganizationDetail; +import org.cloudfoundry.operations.organizations.OrganizationInfoRequest; +import org.cloudfoundry.operations.organizations.OrganizationQuota; +import org.cloudfoundry.operations.organizations.Organizations; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.cloud.appbroker.deployer.AppDeployer; +import org.springframework.cloud.appbroker.deployer.DeleteBackingSpaceRequest; +import org.springframework.core.io.ResourceLoader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +@ExtendWith(MockitoExtension.class) +class CloudFoundryAppDeployerDeleteSpaceTest { + + private AppDeployer appDeployer; + + @Mock + private Organizations operationsOrganizations; + + @Mock + private org.cloudfoundry.client.v2.spaces.Spaces clientSpaces; + + @Mock + private org.cloudfoundry.client.v2.organizations.Organizations clientOrganizations; + + @Mock + private CloudFoundryOperations cloudFoundryOperations; + + @Mock + private CloudFoundryClient cloudFoundryClient; + + @Mock + private CloudFoundryOperationsUtils operationsUtils; + + @Mock + private ResourceLoader resourceLoader; + + @BeforeEach + void setUp() { + CloudFoundryDeploymentProperties deploymentProperties = new CloudFoundryDeploymentProperties(); + + CloudFoundryTargetProperties targetProperties = new CloudFoundryTargetProperties(); + targetProperties.setDefaultOrg("default-org"); + targetProperties.setDefaultSpace("default-space"); + + given(cloudFoundryOperations.organizations()).willReturn(operationsOrganizations); + given(cloudFoundryClient.organizations()).willReturn(clientOrganizations); + + given(operationsOrganizations + .get( + OrganizationInfoRequest + .builder() + .name("default-org") + .build())) + .willReturn(Mono.just( + OrganizationDetail + .builder() + .id("default-org-id") + .name("default-org") + .quota(OrganizationQuota + .builder() + .id("quota-id") + .instanceMemoryLimit(0) + .organizationId("default-org-id") + .name("quota") + .paidServicePlans(false) + .totalMemoryLimit(0) + .totalRoutes(0) + .totalServiceInstances(0) + .build()) + .build())); + + appDeployer = new CloudFoundryAppDeployer(deploymentProperties, cloudFoundryOperations, cloudFoundryClient, + operationsUtils, targetProperties, resourceLoader); + } + + @Test + void deleteSpaceIfExists() { + given(clientOrganizations + .listSpaces(ListOrganizationSpacesRequest + .builder() + .name("test-space-name") + .organizationId("default-org-id") + .page(1) + .build())) + .willReturn(Mono.just(ListOrganizationSpacesResponse + .builder() + .resource(SpaceResource + .builder() + .entity(SpaceEntity + .builder() + .name("test-space-name") + .build()) + .metadata(Metadata + .builder() + .id("test-space-id") + .build()) + .build()) + .build())); + + given(cloudFoundryClient.spaces()).willReturn(clientSpaces); + given(clientSpaces + .delete(DeleteSpaceRequest + .builder() + .spaceId("test-space-id") + .recursive(true) + .build())) + .willReturn(Mono.empty()); + + DeleteBackingSpaceRequest request = + DeleteBackingSpaceRequest.builder() + .name("test-space-name") + .build(); + + StepVerifier.create( + appDeployer.deleteBackingSpace(request)) + .assertNext(response -> assertThat(response.getName()).isEqualTo("test-space-name")) + .verifyComplete(); + } + + @Test + void doNothingIfSpaceDoesNotExist() { + given(clientOrganizations + .listSpaces(ListOrganizationSpacesRequest + .builder() + .name("test-space-name") + .organizationId("default-org-id") + .page(1) + .build())) + .willReturn(Mono.just(ListOrganizationSpacesResponse + .builder() + .resources() + .build())); + + DeleteBackingSpaceRequest request = + DeleteBackingSpaceRequest.builder() + .name("test-space-name") + .build(); + + StepVerifier.create( + appDeployer.deleteBackingSpace(request)) + .assertNext(response -> assertThat(response.getName()).isEqualTo("test-space-name")) + .verifyComplete(); + } + +} 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 ebc021c..3baf2c9 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -23,24 +23,20 @@ import java.util.Collections; 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.applications.UpdateApplicationResponse; import org.cloudfoundry.client.v2.organizations.GetOrganizationRequest; import org.cloudfoundry.client.v2.organizations.GetOrganizationResponse; import org.cloudfoundry.client.v2.organizations.ListOrganizationSpacesRequest; -import org.cloudfoundry.client.v2.organizations.ListOrganizationSpacesResponse; import org.cloudfoundry.client.v2.organizations.OrganizationEntity; import org.cloudfoundry.client.v2.serviceinstances.GetServiceInstanceResponse; import org.cloudfoundry.client.v2.serviceinstances.ServiceInstanceEntity; import org.cloudfoundry.client.v2.serviceinstances.ServiceInstances; import org.cloudfoundry.client.v2.spaces.CreateSpaceRequest; -import org.cloudfoundry.client.v2.spaces.DeleteSpaceRequest; import org.cloudfoundry.client.v2.spaces.GetSpaceRequest; 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; @@ -618,59 +614,6 @@ class CloudFoundryAppDeployerTest { .build())) .willReturn(Mono.empty()); - given(operationsOrganizations - .get( - OrganizationInfoRequest - .builder() - .name("default-org") - .build())) - .willReturn(Mono.just( - OrganizationDetail - .builder() - .id("default-org-id") - .name("default-org") - .quota(OrganizationQuota - .builder() - .id("quota-id") - .instanceMemoryLimit(0) - .organizationId("default-org-id") - .name("quota") - .paidServicePlans(false) - .totalMemoryLimit(0) - .totalRoutes(0) - .totalServiceInstances(0) - .build()) - .build())); - - given(clientOrganizations - .listSpaces(ListOrganizationSpacesRequest - .builder() - .name("service-instance-id") - .organizationId("default-org-id") - .page(1) - .build())) - .willReturn(Mono.just(ListOrganizationSpacesResponse - .builder() - .resource(SpaceResource - .builder() - .entity(SpaceEntity - .builder() - .name("service-instance-id") - .build()) - .metadata(Metadata - .builder() - .id("service-instance-space-id") - .build()) - .build()) - .build())); - - given(clientSpaces - .delete(DeleteSpaceRequest - .builder() - .spaceId("service-instance-space-id") - .build())) - .willReturn(Mono.empty()); - DeleteServiceInstanceRequest request = DeleteServiceInstanceRequest.builder() .serviceInstanceName("service-instance-name") 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 907384d..753ceb6 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -56,4 +56,8 @@ public interface AppDeployer { return Mono.empty(); } + default Mono deleteBackingSpace(DeleteBackingSpaceRequest request) { + return Mono.empty(); + } + } diff --git a/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/DeleteBackingSpaceRequest.java b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/DeleteBackingSpaceRequest.java new file mode 100644 index 0000000..bc12354 --- /dev/null +++ b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/DeleteBackingSpaceRequest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2021 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; + +public class DeleteBackingSpaceRequest { + + private final String name; + + protected DeleteBackingSpaceRequest(String name) { + this.name = name; + } + + public static DeleteBackingSpaceRequestBuilder builder() { + return new DeleteBackingSpaceRequestBuilder(); + } + + public String getName() { + return name; + } + + public static final class DeleteBackingSpaceRequestBuilder { + + private String name; + + private DeleteBackingSpaceRequestBuilder() { + } + + public DeleteBackingSpaceRequestBuilder name(String name) { + this.name = name; + return this; + } + + public DeleteBackingSpaceRequest build() { + return new DeleteBackingSpaceRequest(name); + } + + } + +} diff --git a/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/DeleteBackingSpaceResponse.java b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/DeleteBackingSpaceResponse.java new file mode 100644 index 0000000..5bedf35 --- /dev/null +++ b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/DeleteBackingSpaceResponse.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2021 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; + +public class DeleteBackingSpaceResponse { + + private final String name; + + protected DeleteBackingSpaceResponse(String name) { + this.name = name; + } + + public static DeleteBackingSpaceResponseBuilder builder() { + return new DeleteBackingSpaceResponseBuilder(); + } + + public String getName() { + return name; + } + + public static final class DeleteBackingSpaceResponseBuilder { + + private String name; + + private DeleteBackingSpaceResponseBuilder() { + } + + public DeleteBackingSpaceResponseBuilder name(String name) { + this.name = name; + return this; + } + + public DeleteBackingSpaceResponse build() { + return new DeleteBackingSpaceResponse(name); + } + + } + +} diff --git a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithCustomTargetComponentTest.java b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithCustomTargetComponentTest.java index 2ed87bc..96e9ffe 100644 --- a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithCustomTargetComponentTest.java +++ b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/CreateInstanceWithCustomTargetComponentTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -38,9 +38,9 @@ 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.CreateInstanceWithSpacePerServiceInstanceTargetComponentTest.APP_NAME; -import static org.springframework.cloud.appbroker.integration.CreateInstanceWithSpacePerServiceInstanceTargetComponentTest.BACKING_SERVICE_NAME; -import static org.springframework.cloud.appbroker.integration.CreateInstanceWithSpacePerServiceInstanceTargetComponentTest.BACKING_SI_NAME; +import static org.springframework.cloud.appbroker.integration.CreateInstanceWithCustomTargetComponentTest.APP_NAME; +import static org.springframework.cloud.appbroker.integration.CreateInstanceWithCustomTargetComponentTest.BACKING_SERVICE_NAME; +import static org.springframework.cloud.appbroker.integration.CreateInstanceWithCustomTargetComponentTest.BACKING_SI_NAME; @TestPropertySource(properties = { "spring.cloud.appbroker.services[0].service-name=example", @@ -76,18 +76,18 @@ class CreateInstanceWithCustomTargetComponentTest extends WiremockComponentTest void pushAppWithServicesInSpace() { String serviceInstanceId = "instance-id"; String customSpace = "my-space"; - - cloudControllerFixture.stubCreateSpace(customSpace); - cloudControllerFixture.stubAssociatePermissions(customSpace); + String customSpaceGuid = "my-space-guid"; + cloudControllerFixture.stubCreateSpace(customSpace, customSpaceGuid); + cloudControllerFixture.stubAssociatePermissions(customSpace, customSpaceGuid); cloudControllerFixture.stubPushApp(APP_NAME); // given services are available in the marketplace - cloudControllerFixture.stubServiceExists(BACKING_SERVICE_NAME, BACKING_PLAN_NAME); + cloudControllerFixture.stubServiceExistsInSpace(BACKING_SERVICE_NAME, BACKING_PLAN_NAME, customSpaceGuid); // will create and bind the service instance cloudControllerFixture.stubCreateServiceInstance(BACKING_SI_NAME); cloudControllerFixture.stubCreateServiceBinding(APP_NAME, BACKING_SI_NAME); - cloudControllerFixture.stubServiceInstanceExists(BACKING_SI_NAME); + cloudControllerFixture.stubServiceInstanceExistsInSpace(BACKING_SI_NAME, customSpaceGuid); // when a service instance is created given(brokerFixture.serviceInstanceRequest()) 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 9060b2e..7dddeeb 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -65,18 +65,19 @@ class CreateInstanceWithSpacePerServiceInstanceTargetComponentTest extends Wirem @Test void pushAppWithServicesInSpace() { String serviceInstanceId = "instance-id"; + String backingSpaceGuid = "my-space-guid"; - cloudControllerFixture.stubCreateSpace(serviceInstanceId); - cloudControllerFixture.stubAssociatePermissions(serviceInstanceId); + cloudControllerFixture.stubCreateSpace(serviceInstanceId, backingSpaceGuid); + cloudControllerFixture.stubAssociatePermissions(serviceInstanceId, backingSpaceGuid); cloudControllerFixture.stubPushAppWithHost(APP_NAME, APP_NAME + "-" + serviceInstanceId); // given services are available in the marketplace - cloudControllerFixture.stubServiceExists(BACKING_SERVICE_NAME, BACKING_PLAN_NAME); + cloudControllerFixture.stubServiceExistsInSpace(BACKING_SERVICE_NAME, BACKING_PLAN_NAME, backingSpaceGuid); // will create and bind the service instance cloudControllerFixture.stubCreateServiceInstance(BACKING_SI_NAME); cloudControllerFixture.stubCreateServiceBinding(APP_NAME, BACKING_SI_NAME); - cloudControllerFixture.stubServiceInstanceExists(BACKING_SI_NAME); + cloudControllerFixture.stubServiceInstanceExistsInSpace(BACKING_SI_NAME, backingSpaceGuid); // when a service instance is created given(brokerFixture.serviceInstanceRequest()) diff --git a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/UpdateInstanceWithNewServiceAndTargetComponentTest.java b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/UpdateInstanceWithNewServiceAndTargetComponentTest.java new file mode 100644 index 0000000..7ee8c6d --- /dev/null +++ b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/UpdateInstanceWithNewServiceAndTargetComponentTest.java @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2021 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 com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; +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.UpdateInstanceWithNewServiceAndTargetComponentTest.APP_NAME; +import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithNewServiceAndTargetComponentTest.NEW_BACKING_PLAN_NAME; +import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithNewServiceAndTargetComponentTest.NEW_BACKING_SERVICE_NAME; +import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithNewServiceAndTargetComponentTest.NEW_BACKING_SI_NAME; +import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithNewServiceAndTargetComponentTest.PLAN_NAME; +import static org.springframework.cloud.appbroker.integration.UpdateInstanceWithNewServiceAndTargetComponentTest.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, + + "spring.cloud.appbroker.services[0].target.name=SpacePerServiceInstance" +}) +class UpdateInstanceWithNewServiceAndTargetComponentTest extends WiremockComponentTest { + + protected static final String APP_NAME = "app-update-with-new-service-and-target"; + + 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 updateAppWithNewServiceAndTarget() { + final String backingSpaceGuid = "instance-id-space-guid"; + final String serviceInstanceId = "instance-id"; + + cloudControllerFixture.stubFindSpace(serviceInstanceId, backingSpaceGuid, STARTED); + cloudControllerFixture.stubSpaceExists(serviceInstanceId, backingSpaceGuid, STARTED); + cloudControllerFixture.stubAppExistsWithBackingService(APP_NAME, BACKING_SI_NAME, + BACKING_SERVICE_NAME, BACKING_PLAN_NAME, backingSpaceGuid); + cloudControllerFixture.stubUpdateAppWithTarget(APP_NAME, backingSpaceGuid); + + // will unbind and delete the existing service instance + cloudControllerFixture.stubGetBackingServiceInstanceFromSpace(BACKING_SI_NAME, BACKING_SERVICE_NAME, + BACKING_PLAN_NAME, backingSpaceGuid); + 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.stubServiceExistsInSpace(NEW_BACKING_SERVICE_NAME, NEW_BACKING_PLAN_NAME, backingSpaceGuid); + cloudControllerFixture.stubCreateServiceInstance(NEW_BACKING_SI_NAME); + cloudControllerFixture.stubCreateServiceBinding(APP_NAME, NEW_BACKING_SI_NAME); + cloudControllerFixture.stubServiceInstanceExistsInSpace(NEW_BACKING_SI_NAME, backingSpaceGuid); + + // when a service instance is updated + given(brokerFixture.serviceInstanceRequest()) + .when() + .patch(brokerFixture.createServiceInstanceUrl(), serviceInstanceId) + .then() + .statusCode(HttpStatus.ACCEPTED.value()); + + // when the "last_operation" API is polled + given(brokerFixture.serviceInstanceRequest()) + .when() + .get(brokerFixture.getLastInstanceOperationUrl(), serviceInstanceId) + .then() + .statusCode(HttpStatus.OK.value()) + .body("state", is(equalTo(OperationState.IN_PROGRESS.toString()))); + + String state = brokerFixture.waitForAsyncOperationComplete(serviceInstanceId); + assertThat(state).isEqualTo(OperationState.SUCCEEDED.toString()); + } + +} 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 37d2a69..3733157 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -35,10 +35,15 @@ import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.put; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; @TestComponent public class CloudControllerStubFixture extends WiremockStubFixture { + private static final String SCENARIO_NAME = "CreateSpace"; + + private static final String SPACE_CREATED_STATE = "SpaceCreated"; + private static final String TEST_SPACE_GUID = "TEST-SPACE-GUID"; private static final String TEST_ORG_GUID = "TEST-ORG-GUID"; @@ -52,7 +57,7 @@ public class CloudControllerStubFixture extends WiremockStubFixture { public void stubCommonCloudControllerRequests() { stubGetPlatformInfo(); stubFindTestOrg(); - stubFindTestSpace(); + stubFindSpace("development", TEST_SPACE_GUID, STARTED); stubFindDomains(); } @@ -82,29 +87,39 @@ public class CloudControllerStubFixture extends WiremockStubFixture { replace("@org-guid", TEST_ORG_GUID))))); } - private void stubFindTestSpace() { + public void stubFindSpace(String spaceName, String spaceGuid, String scenario) { stubFor(get(urlPathEqualTo("/v2/spaces")) + .inScenario(SCENARIO_NAME) + .whenScenarioStateIs(scenario) + .withQueryParam("q", equalTo("name:" + spaceName)) .withMetadata(optionalStubMapping()) .willReturn(ok() .withBody(cc("list-spaces", replace("@org-guid", TEST_ORG_GUID), - replace("@space-guid", TEST_SPACE_GUID))))); + replace("@name", spaceName), + replace("@space-guid", spaceGuid))))); - stubFor(get(urlPathEqualTo("/v2/spaces/" + TEST_SPACE_GUID)) + stubFor(get(urlPathEqualTo("/v2/spaces/" + spaceGuid)) + .inScenario(SCENARIO_NAME) + .whenScenarioStateIs(scenario) .withMetadata(optionalStubMapping()) .willReturn(ok() .withBody(cc("get-space", replace("@name", "test"), replace("@org-guid", TEST_ORG_GUID), - replace("@space-guid", TEST_SPACE_GUID))))); + replace("@space-guid", spaceGuid))))); - stubFor(get(urlPathEqualTo("/v2/spaces/" + TEST_SPACE_GUID + "/apps")) + stubFor(get(urlPathEqualTo("/v2/spaces/" + spaceGuid + "/apps")) + .inScenario(SCENARIO_NAME) + .whenScenarioStateIs(scenario) .withQueryParam("page", equalTo("1")) .withMetadata(optionalStubMapping()) .willReturn(ok() .withBody(cc("empty-query-results")))); - stubFor(get(urlPathEqualTo("/v2/spaces/" + TEST_SPACE_GUID + "/security_groups")) + stubFor(get(urlPathEqualTo("/v2/spaces/" + spaceGuid + "/security_groups")) + .inScenario(SCENARIO_NAME) + .whenScenarioStateIs(scenario) .withMetadata(optionalStubMapping()) .willReturn(ok() .withBody(cc("get-space-security_groups")))); @@ -144,15 +159,20 @@ public class CloudControllerStubFixture extends WiremockStubFixture { replace("@org-guid", TEST_ORG_GUID))))); } - public void stubCreateSpace(final String spaceName) { + public void stubCreateSpace(final String spaceName, final String spaceGuid) { stubFor(post(urlPathEqualTo("/v2/spaces")) + .inScenario(SCENARIO_NAME) + .whenScenarioStateIs(STARTED) + .willSetStateTo(SPACE_CREATED_STATE) .withRequestBody(matchingJsonPath("$.[?(@.name == '" + spaceName + "')]")) .withRequestBody(matchingJsonPath("$.[?(@.organization_guid == '" + TEST_ORG_GUID + "')]")) .willReturn(ok() .withBody(cc("get-space", replace("@name", spaceName), - replace("@space-guid", "CREATED-SPACE-GUID"), + replace("@space-guid", spaceGuid), replace("@org-guid", TEST_ORG_GUID))))); + + stubFindSpace(spaceName, spaceGuid, SPACE_CREATED_STATE); } public void stubAppDoesNotExist(final String appName) { @@ -163,26 +183,24 @@ public class CloudControllerStubFixture extends WiremockStubFixture { .withBody(cc("empty-query-results")))); } - public void stubAppExists(final String appName) { + public void stubAppExistsInSpace(final String appName, final String spaceGuid) { stubFor(get(urlPathEqualTo("/v2/apps/" + appGuid(appName))) .withMetadata(optionalStubMapping()) .willReturn(ok() .withBody(cc("get-app-STAGED", replace("@name", appName))))); - stubFor(get(urlPathEqualTo("/v2/spaces/" + TEST_SPACE_GUID + "/apps")) + stubFor(get(urlPathEqualTo("/v2/spaces/" + spaceGuid + "/apps")) .withQueryParam("q", equalTo("name:" + appName)) .withQueryParam("page", equalTo("1")) - .withMetadata(optionalStubMapping()) .willReturn(ok() .withBody(cc("list-space-apps", replace("@name", appName), replace("@guid", appGuid(appName)), - replace("@space-guid", TEST_SPACE_GUID), + replace("@space-guid", spaceGuid), replace("@stack-guid", stackGuid(appName)))))); stubFor(get(urlPathEqualTo("/v2/apps/" + appGuid(appName) + "/instances")) - .withMetadata(optionalStubMapping()) .willReturn(ok() .withBody(cc("get-app-instances")))); @@ -196,24 +214,30 @@ public class CloudControllerStubFixture extends WiremockStubFixture { replace("@route-guid", routeGuid(appName)))))); stubFor(get(urlPathEqualTo("/v2/apps/" + appGuid(appName) + "/stats")) - .withMetadata(optionalStubMapping()) .willReturn(ok() .withBody(cc("get-app-stats", replace("@name", appName))))); stubFor(get(urlPathEqualTo("/v2/stacks/" + stackGuid(appName))) - .withMetadata(optionalStubMapping()) .willReturn(ok() .withBody(cc("get-stack", replace("@guid", stackGuid(appName)))))); } + public void stubAppExists(final String appName) { + stubAppExistsInSpace(appName, TEST_SPACE_GUID); + } + public void stubAppExistsWithBackingService(final String appName, final String serviceInstanceName, final String serviceName, final String planName) { - stubAppExists(appName); + stubAppExistsWithBackingService(appName, serviceInstanceName, serviceName, planName, TEST_SPACE_GUID); + } + + public void stubAppExistsWithBackingService(final String appName, final String serviceInstanceName, + final String serviceName, final String planName, final String spaceGuid) { + stubAppExistsInSpace(appName, spaceGuid); stubFor(get(urlPathEqualTo("/v2/apps/" + appGuid(appName) + "/summary")) - .withMetadata(optionalStubMapping()) .willReturn(ok() .withBody(cc("get-app-summary-with-backing-service", replace("@name", appName), @@ -248,6 +272,11 @@ public class CloudControllerStubFixture extends WiremockStubFixture { stubCreateRoute(appName); } + public void stubUpdateAppWithTarget(final String appName, final String spaceGuid) { + stubUpdateApp(appName); + stubCreateRouteInSpace(appName, spaceGuid); + } + public void stubUpdateApp(final String appName) { stubGetV3App(appName); stubUpdateEnvironment(appName); @@ -425,14 +454,14 @@ public class CloudControllerStubFixture extends WiremockStubFixture { .withBody(cc("get-app-instances")))); } - private void stubCreateRoute(final String appName) { + private void stubCreateRouteInSpace(final String appName, final String spaceGuid) { stubFor(get(urlPathEqualTo("/v2/private_domains")) .withQueryParam("page", equalTo("1")) .withMetadata(optionalStubMapping()) .willReturn(ok() .withBody(cc("empty-query-results")))); - stubFor(get(urlPathEqualTo("/v2/spaces/" + TEST_SPACE_GUID + "/services")) + stubFor(get(urlPathEqualTo("/v2/spaces/" + spaceGuid + "/services")) .withQueryParam("page", equalTo("1")) .willReturn(ok() .withBody(cc("list-space-services") @@ -449,6 +478,10 @@ public class CloudControllerStubFixture extends WiremockStubFixture { .withBody(cc("get-app-summary")))); } + private void stubCreateRoute(final String appName) { + stubCreateRouteInSpace(appName, TEST_SPACE_GUID); + } + public void stubDeleteApp(String appName) { stubFor(delete(urlPathEqualTo("/v2/routes/" + appName + "-ROUTE-GUID")) .willReturn(ok() @@ -463,19 +496,30 @@ public class CloudControllerStubFixture extends WiremockStubFixture { stubServiceInstanceExists(serviceInstanceName, "doNotCare", "doNotCare"); } - public void stubServiceInstanceExists(String serviceInstanceName, String serviceName, String planName) { - stubServiceInstanceExists(serviceInstanceGuid(serviceInstanceName), serviceInstanceName, serviceName, planName); + public void stubServiceInstanceExistsInSpace(String serviceInstanceName, String spaceGuid) { + stubServiceInstanceExistsInSpace(serviceInstanceName, "doNotCare", "doNotCare", spaceGuid); } - private void stubServiceInstanceExists(String serviceInstanceId, String serviceInstanceName, String serviceName, - String planName) { - stubFor(get(urlPathEqualTo("/v2/spaces/" + TEST_SPACE_GUID + "/service_instances")) + public void stubServiceInstanceExists(String serviceInstanceName, String serviceName, String planName) { + stubServiceInstanceExists(serviceInstanceGuid(serviceInstanceName), serviceInstanceName, serviceName, + planName, TEST_SPACE_GUID); + } + + public void stubServiceInstanceExistsInSpace(String serviceInstanceName, String serviceName, String planName, + String spaceGuid) { + stubServiceInstanceExists(serviceInstanceGuid(serviceInstanceName), serviceInstanceName, serviceName, + planName, spaceGuid); + } + + private void stubServiceInstanceExists(String serviceInstanceId, String serviceInstanceName, + String serviceName, String planName, String spaceGuid) { + stubFor(get(urlPathEqualTo("/v2/spaces/" + spaceGuid + "/service_instances")) .withQueryParam("q", equalTo("name:" + serviceInstanceName)) .withQueryParam("page", equalTo("1")) .withQueryParam("return_user_provided_service_instances", equalTo("true")) .willReturn(ok() .withBody(cc("list-space-service_instances", - replace("@space-guid", TEST_SPACE_GUID), + replace("@space-guid", spaceGuid), replace("@service-guid", serviceGuid(serviceName)), replace("@plan-guid", planGuid(planName)), replace("@name", serviceInstanceName), @@ -491,13 +535,18 @@ public class CloudControllerStubFixture extends WiremockStubFixture { } public void stubGetBackingServiceInstance(String serviceInstanceName, String serviceName, String planName) { + stubGetBackingServiceInstanceFromSpace(serviceInstanceName, serviceName, planName, TEST_SPACE_GUID); + } + + public void stubGetBackingServiceInstanceFromSpace(String serviceInstanceName, String serviceName, + String planName, String spaceGuid) { String serviceInstanceId = serviceInstanceGuid(serviceInstanceName); - stubServiceInstanceExists(serviceInstanceId, serviceInstanceName, serviceName, planName); + stubServiceInstanceExists(serviceInstanceId, serviceInstanceName, serviceName, planName, spaceGuid); stubGetServiceAndGetPlan(serviceName, planName); } - public void stubServiceExists(String serviceName, String planName) { - stubFor(get(urlPathEqualTo("/v2/spaces/" + TEST_SPACE_GUID + "/services")) + public void stubServiceExistsInSpace(String serviceName, String planName, String spaceGuid) { + stubFor(get(urlPathEqualTo("/v2/spaces/" + spaceGuid + "/services")) .withQueryParam("q", equalTo("label:" + serviceName)) .withQueryParam("page", equalTo("1")) .willReturn(ok() @@ -516,6 +565,10 @@ public class CloudControllerStubFixture extends WiremockStubFixture { .replace("@plan-guid", planGuid(planName))))); } + public void stubServiceExists(String serviceName, String planName) { + stubServiceExistsInSpace(serviceName, planName, TEST_SPACE_GUID); + } + public void stubCreateServiceInstance(String serviceInstanceName) { stubFor(post(urlPathEqualTo("/v2/service_instances")) .withQueryParam("accepts_incomplete", equalTo("true")) @@ -596,11 +649,11 @@ public class CloudControllerStubFixture extends WiremockStubFixture { .withBody(cc("empty-query-results")))); } - public void stubAssociatePermissions(final String spaceName) { + public void stubAssociatePermissions(final String spaceName, final String spaceGuid) { stubFor(get(urlPathEqualTo("/v2/config/feature_flags/set_roles_by_username")) .willReturn(ok() .withBody(cc("get-feature-flag-roles")))); - stubSpaceExists(spaceName); + stubSpaceExists(spaceName, spaceGuid, SPACE_CREATED_STATE); stubFor(put(urlPathEqualTo("/v2/organizations/" + TEST_ORG_GUID + "/users")) .willReturn(ok())); @@ -621,13 +674,15 @@ public class CloudControllerStubFixture extends WiremockStubFixture { .replace("@service-name", serviceName)))); } - private void stubSpaceExists(final String spaceName) { + public void stubSpaceExists(final String spaceName, final String spaceGuid, final String scenario) { stubFor(get(urlPathEqualTo("/v2/organizations/" + TEST_ORG_GUID + "/spaces")) + .inScenario(SCENARIO_NAME) + .whenScenarioStateIs(scenario) .withQueryParam("q", equalTo("name:" + spaceName)) .willReturn(ok() .withBody(cc("list-spaces", replace("@org-guid", TEST_ORG_GUID), - replace("@space-guid", TEST_SPACE_GUID), + replace("@space-guid", spaceGuid), replace("@name", spaceName))))); } @@ -659,7 +714,7 @@ public class CloudControllerStubFixture extends WiremockStubFixture { return planName + "-PLAN-GUID"; } - private static String serviceInstanceGuid(String serviceInstanceName) { + public static String serviceInstanceGuid(String serviceInstanceName) { return serviceInstanceName + "-INSTANCE-GUID"; } diff --git a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/fixtures/WiremockStubFixture.java b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/fixtures/WiremockStubFixture.java index e922d62..ab9e4f1 100644 --- a/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/fixtures/WiremockStubFixture.java +++ b/spring-cloud-app-broker-integration-tests/src/test/java/org/springframework/cloud/appbroker/integration/fixtures/WiremockStubFixture.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -21,6 +21,7 @@ import java.io.IOException; import java.io.InputStreamReader; import java.util.stream.Collectors; +import com.github.tomakehurst.wiremock.admin.model.ListStubMappingsResult; import com.github.tomakehurst.wiremock.client.MappingBuilder; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.common.Metadata; @@ -45,6 +46,10 @@ public class WiremockStubFixture { .build(); } + public ListStubMappingsResult getAllStubs() { + return wireMock.allStubMappings(); + } + protected StubMapping stubFor(MappingBuilder mappingBuilder) { return givenThat(mappingBuilder); } diff --git a/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/list-shared-domains.json b/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/list-shared-domains.json index f27f2d0..890e337 100644 --- a/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/list-shared-domains.json +++ b/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/list-shared-domains.json @@ -1,5 +1,5 @@ { - "total_results": 2, + "total_results": 3, "total_pages": 1, "prev_url": null, "next_url": null, diff --git a/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/list-spaces.json b/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/list-spaces.json index e190957..b94f099 100644 --- a/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/list-spaces.json +++ b/spring-cloud-app-broker-integration-tests/src/test/resources/responses/cloudcontroller/list-spaces.json @@ -12,7 +12,7 @@ "updated_at": "2018-07-19T20:34:16Z" }, "entity": { - "name": "development", + "name": "@name", "organization_guid": "@org-guid", "space_quota_definition_guid": null, "isolation_segment_guid": null,