diff --git a/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/CreateInstanceWithServicesAcceptanceTest.java b/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/CreateInstanceWithServicesAcceptanceTest.java new file mode 100644 index 0000000..cb37781 --- /dev/null +++ b/spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/CreateInstanceWithServicesAcceptanceTest.java @@ -0,0 +1,60 @@ +package org.springframework.cloud.appbroker.acceptance; + +import java.util.Optional; + +import org.cloudfoundry.operations.applications.ApplicationSummary; +import org.cloudfoundry.operations.services.ServiceInstanceSummary; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class CreateInstanceWithServicesAcceptanceTest extends CloudFoundryAcceptanceTest { + + private static final String BROKER_APP_SERVICES = "broker-app-new-services"; + private static final String SI_1_NAME = "service-instance-1"; + private static final String SERVICE_1_NAME = "db-service"; + + @Test + @AppBrokerTestProperties({ + "spring.cloud.appbroker.services[0].service-name=example", + "spring.cloud.appbroker.services[0].plan-name=standard", + "spring.cloud.appbroker.services[0].apps[0].name=" + BROKER_APP_SERVICES, + "spring.cloud.appbroker.services[0].apps[0].path=classpath:demo.jar", + "spring.cloud.appbroker.services[0].apps[0].services[0].service-instance-name=" + SI_1_NAME, + "spring.cloud.appbroker.services[0].services[0].service-instance-name=" + SI_1_NAME, + "spring.cloud.appbroker.services[0].services[0].name=" + SERVICE_1_NAME, + "spring.cloud.appbroker.services[0].services[0].plan=standard" + }) + void shouldPushAppWithServicesBind() { + // given that a service is available in the marketplace + setupServiceBrokerForService("db-service"); + + // when a service instance is created + createServiceInstance(); + + // then a backing application is deployed + Optional backingApplication = getApplicationSummaryByName(BROKER_APP_SERVICES); + assertThat(backingApplication).isNotEmpty(); + + // and the service bind to it + Optional serviceInstance = getServiceInstance(SI_1_NAME); + assertThat(serviceInstance).isNotEmpty(); + assertThat(serviceInstance.get().getApplications()).contains(BROKER_APP_SERVICES); + + // when the service instance is deleted + deleteServiceInstance(); + + // service has no applications bound to it + Optional serviceInstanceAfterDeletion = getServiceInstance(SI_1_NAME); + assertThat(serviceInstanceAfterDeletion).isEmpty(); + } + + @Override + @AfterEach + void tearDown() { + super.tearDown(); + + deleteServiceBrokerForService("db-service"); + } +} 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 78836c1..76e1319 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 @@ -25,6 +25,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.appbroker.deployer.AppDeployer; import org.springframework.cloud.appbroker.deployer.BackingAppDeploymentService; +import org.springframework.cloud.appbroker.deployer.BackingServicesProvisionService; import org.springframework.cloud.appbroker.deployer.BrokeredServices; import org.springframework.cloud.appbroker.deployer.DeployerClient; import org.springframework.cloud.appbroker.extensions.credentials.CredentialGenerator; @@ -131,21 +132,39 @@ public class AppBrokerAutoConfiguration { return new TargetService(targets); } + @Bean + public BackingServicesProvisionService backingServicesProvisionService(DeployerClient deployerClient) { + return new BackingServicesProvisionService(deployerClient); + } + @Bean public CreateServiceInstanceWorkflow appDeploymentCreateServiceInstanceWorkflow(BrokeredServices brokeredServices, BackingAppDeploymentService backingAppDeploymentService, ParametersTransformationService parametersTransformationService, CredentialProviderService credentialProviderService, - TargetService targetService) { - return new AppDeploymentCreateServiceInstanceWorkflow(brokeredServices, backingAppDeploymentService, parametersTransformationService, credentialProviderService, targetService); + TargetService targetService, + BackingServicesProvisionService backingServicesProvisionService) { + return new AppDeploymentCreateServiceInstanceWorkflow( + brokeredServices, + backingAppDeploymentService, + parametersTransformationService, + credentialProviderService, + targetService, + backingServicesProvisionService); } @Bean public DeleteServiceInstanceWorkflow appDeploymentDeleteServiceInstanceWorkflow(BrokeredServices brokeredServices, BackingAppDeploymentService backingAppDeploymentService, CredentialProviderService credentialProviderService, - TargetService targetService) { - return new AppDeploymentDeleteServiceInstanceWorkflow(brokeredServices, backingAppDeploymentService, credentialProviderService, targetService); + TargetService targetService, + BackingServicesProvisionService backingServicesProvisionService) { + return new AppDeploymentDeleteServiceInstanceWorkflow( + brokeredServices, + backingAppDeploymentService, + credentialProviderService, + targetService, + backingServicesProvisionService); } @Bean diff --git a/spring-cloud-app-broker-autoconfigure/src/test/java/org/springframework/cloud/appbroker/autoconfigure/AppBrokerAutoConfigurationTest.java b/spring-cloud-app-broker-autoconfigure/src/test/java/org/springframework/cloud/appbroker/autoconfigure/AppBrokerAutoConfigurationTest.java index 5ecdce1..27f4174 100644 --- a/spring-cloud-app-broker-autoconfigure/src/test/java/org/springframework/cloud/appbroker/autoconfigure/AppBrokerAutoConfigurationTest.java +++ b/spring-cloud-app-broker-autoconfigure/src/test/java/org/springframework/cloud/appbroker/autoconfigure/AppBrokerAutoConfigurationTest.java @@ -22,6 +22,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.appbroker.deployer.BackingAppDeploymentService; import org.springframework.cloud.appbroker.deployer.BackingApplications; +import org.springframework.cloud.appbroker.deployer.BackingServicesProvisionService; import org.springframework.cloud.appbroker.deployer.BrokeredServices; import org.springframework.cloud.appbroker.deployer.DeployerClient; import org.springframework.cloud.appbroker.extensions.credentials.CredentialProviderService; @@ -101,6 +102,7 @@ class AppBrokerAutoConfigurationTest { assertThat(context).hasSingleBean(ParametersTransformationService.class); assertThat(context).hasSingleBean(CredentialProviderService.class); assertThat(context).hasSingleBean(TargetService.class); + assertThat(context).hasSingleBean(BackingServicesProvisionService.class); assertThat(context).hasSingleBean(WorkflowServiceInstanceService.class); assertThat(context).hasSingleBean(WorkflowServiceInstanceBindingService.class); assertThat(context).hasSingleBean(AppDeploymentCreateServiceInstanceWorkflow.class); diff --git a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/BackingService.java b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/BackingService.java new file mode 100644 index 0000000..d21de14 --- /dev/null +++ b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/BackingService.java @@ -0,0 +1,150 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.appbroker.deployer; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class BackingService { + + private String serviceInstanceName; + private String name; + private String plan; + private Map parameters; + + private BackingService() { + } + + BackingService(String serviceInstanceName, String name, String plan, Map parameters) { + this.serviceInstanceName = serviceInstanceName; + this.name = name; + this.plan = plan; + this.parameters = parameters; + } + + BackingService(BackingService backingServiceToCopy) { + this.serviceInstanceName = backingServiceToCopy.serviceInstanceName; + this.name = backingServiceToCopy.name; + this.plan = backingServiceToCopy.plan; + this.parameters = backingServiceToCopy.parameters == null + ? new HashMap<>() + : new HashMap<>(backingServiceToCopy.parameters); + } + + public String getServiceInstanceName() { + return serviceInstanceName; + } + + public void setServiceInstanceName(String serviceInstanceName) { + this.serviceInstanceName = serviceInstanceName; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPlan() { + return plan; + } + + public void setPlan(String plan) { + this.plan = plan; + } + + public Map getParameters() { + return parameters; + } + + public void setParameters(Map parameters) { + this.parameters = parameters; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BackingService that = (BackingService) o; + return Objects.equals(serviceInstanceName, that.serviceInstanceName) && + Objects.equals(name, that.name) && + Objects.equals(plan, that.plan) && + Objects.equals(parameters, that.parameters); + } + + @Override + public int hashCode() { + return Objects.hash(serviceInstanceName, name, plan, parameters); + } + + @Override + public String toString() { + return "BackingService{" + + "serviceInstanceName='" + serviceInstanceName + '\'' + + ", name='" + name + '\'' + + ", plan='" + plan + '\'' + + ", parameters=" + parameters + + '}'; + } + + public static BackingServiceBuilder builder() { + return new BackingServiceBuilder(); + } + + public static final class BackingServiceBuilder { + + private String serviceInstanceName; + private String name; + private String plan; + private Map parameters = new HashMap<>(); + + BackingServiceBuilder() { + } + + public BackingService build() { + return new BackingService(serviceInstanceName, name, plan, parameters); + } + + public BackingServiceBuilder serviceInstanceName(String serviceInstanceName) { + this.serviceInstanceName = serviceInstanceName; + return this; + } + + public BackingServiceBuilder name(String name) { + this.name = name; + return this; + } + + public BackingServiceBuilder plan(String plan) { + this.plan = plan; + return this; + } + + public BackingServiceBuilder parameters(Map parameters) { + this.parameters = parameters; + return this; + } + } + +} diff --git a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/BackingServices.java b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/BackingServices.java new file mode 100644 index 0000000..fb6b0f4 --- /dev/null +++ b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/BackingServices.java @@ -0,0 +1,55 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.appbroker.deployer; + +import java.util.ArrayList; +import java.util.List; + +public class BackingServices extends ArrayList { + + private static final long serialVersionUID = 1L; + + private BackingServices() { + } + + BackingServices(List backingServices) { + super.addAll(backingServices); + } + + public BackingServices(BackingServices backingServicesToCopy) { + backingServicesToCopy.forEach(backingServiceToCopy -> + this.add(new BackingService(backingServiceToCopy))); + } + + public static BackingServicesBuilder builder() { + return new BackingServicesBuilder(); + } + + public static class BackingServicesBuilder { + + private final List backingServices = new ArrayList<>(); + + public BackingServicesBuilder backingService(BackingService backingService) { + this.backingServices.add(backingService); + return this; + } + + public BackingServices build() { + return new BackingServices(backingServices); + } + } +} diff --git a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/BackingServicesProvisionService.java b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/BackingServicesProvisionService.java new file mode 100644 index 0000000..50f483b --- /dev/null +++ b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/BackingServicesProvisionService.java @@ -0,0 +1,60 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.appbroker.deployer; + +import java.util.List; + +import reactor.core.publisher.Flux; +import reactor.core.scheduler.Schedulers; +import reactor.util.Logger; +import reactor.util.Loggers; + +public class BackingServicesProvisionService { + + private final Logger log = Loggers.getLogger(BackingServicesProvisionService.class); + + private final DeployerClient deployerClient; + + public BackingServicesProvisionService(DeployerClient deployerClient) { + this.deployerClient = deployerClient; + } + + public Flux createServiceInstance(List backingServices) { + return Flux.fromIterable(backingServices) + .parallel() + .runOn(Schedulers.parallel()) + .flatMap(deployerClient::createServiceInstance) + .doOnRequest(l -> log.info("Creating backing services {}", backingServices)) + .doOnEach(d -> log.info("Finished creating backing service {}", d)) + .doOnComplete(() -> log.info("Finished creating backing service {}", backingServices)) + .doOnError(e -> log.info("Error creating backing services {} with error {}", backingServices, e)) + .sequential(); + } + + public Flux deleteServiceInstance(List backingServices) { + return Flux.fromIterable(backingServices) + .parallel() + .runOn(Schedulers.parallel()) + .flatMap(deployerClient::deleteServiceInstance) + .doOnRequest(l -> log.info("Deleting backing services {}", backingServices)) + .doOnEach(d -> log.info("Finished deleting backing service {}", d)) + .doOnComplete(() -> log.info("Finished deleting backing service {}", backingServices)) + .doOnError(e -> log.info("Error deleting backing services {} with error {}", backingServices, e)) + .sequential(); + } + +} diff --git a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/BrokeredService.java b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/BrokeredService.java index 0d1d671..9dbf37f 100644 --- a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/BrokeredService.java +++ b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/deployer/BrokeredService.java @@ -19,17 +19,20 @@ package org.springframework.cloud.appbroker.deployer; import java.util.Objects; public class BrokeredService { + private String serviceName; private String planName; private BackingApplications apps; + private BackingServices services; private BrokeredService() { } - BrokeredService(String serviceName, String planName, BackingApplications apps) { + BrokeredService(String serviceName, String planName, BackingApplications apps, BackingServices services) { this.serviceName = serviceName; this.planName = planName; this.apps = apps; + this.services = services; } public String getServiceName() { @@ -56,6 +59,14 @@ public class BrokeredService { this.apps = apps; } + public BackingServices getServices() { + return services; + } + + public void setServices(BackingServices services) { + this.services = services; + } + public static BrokeredServiceBuilder builder() { return new BrokeredServiceBuilder(); } @@ -71,12 +82,13 @@ public class BrokeredService { BrokeredService that = (BrokeredService) o; return Objects.equals(serviceName, that.serviceName) && Objects.equals(planName, that.planName) && - Objects.equals(apps, that.apps); + Objects.equals(apps, that.apps) && + Objects.equals(services, that.services); } @Override public final int hashCode() { - return Objects.hash(serviceName, planName, apps); + return Objects.hash(serviceName, planName, apps, services); } @Override @@ -85,13 +97,16 @@ public class BrokeredService { "serviceName='" + serviceName + '\'' + ", planName='" + planName + '\'' + ", apps=" + apps + + ", services=" + services + '}'; } public static class BrokeredServiceBuilder { + private String id; private String planId; private BackingApplications backingApplications; + private BackingServices backingServices; public BrokeredServiceBuilder serviceName(String id) { this.id = id; @@ -108,8 +123,13 @@ public class BrokeredService { return this; } + public BrokeredServiceBuilder services(BackingServices backingServices) { + this.backingServices = backingServices; + return this; + } + public BrokeredService build() { - return new BrokeredService(id, planId, backingApplications); + return new BrokeredService(id, planId, backingApplications, backingServices); } } } 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 559c2af..2a83d4e 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 @@ -54,4 +54,23 @@ public class DeployerClient { .map(UndeployApplicationResponse::getName); } + Mono createServiceInstance(BackingService backingService) { + return appDeployer.createServiceInstance(CreateServiceInstanceRequest.builder() + .serviceInstanceName(backingService.getServiceInstanceName()) + .name(backingService.getName()) + .plan(backingService.getPlan()) + .parameters(backingService.getParameters()) + .build()) + .doOnRequest(l -> log.info("Creating backing service {}", backingService.getName())) + .doOnSuccess(d -> log.info("Finished creating backing service {}", backingService.getName())) + .doOnError(e -> log.info("Error creating backing service {} with error {}", backingService.getName(), e)) + .map(CreateServiceInstanceResponse::getName); + } + + Mono deleteServiceInstance(BackingService backingService) { + return appDeployer.deleteServiceInstance(DeleteServiceInstanceRequest.builder() + .name(backingService.getServiceInstanceName()) + .build()) + .map(DeleteServiceInstanceResponse::getName); + } } diff --git a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentCreateServiceInstanceWorkflow.java b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentCreateServiceInstanceWorkflow.java index 840bba3..3aa4086 100644 --- a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentCreateServiceInstanceWorkflow.java +++ b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentCreateServiceInstanceWorkflow.java @@ -22,6 +22,7 @@ import reactor.util.Logger; import reactor.util.Loggers; import org.springframework.cloud.appbroker.deployer.BackingAppDeploymentService; +import org.springframework.cloud.appbroker.deployer.BackingServicesProvisionService; import org.springframework.cloud.appbroker.deployer.BrokeredServices; import org.springframework.cloud.appbroker.extensions.credentials.CredentialProviderService; import org.springframework.cloud.appbroker.extensions.parameters.ParametersTransformationService; @@ -42,33 +43,38 @@ public class AppDeploymentCreateServiceInstanceWorkflow private final ParametersTransformationService parametersTransformationService; private final CredentialProviderService credentialProviderService; private final TargetService targetService; + private final BackingServicesProvisionService backingServicesProvisionService; public AppDeploymentCreateServiceInstanceWorkflow(BrokeredServices brokeredServices, BackingAppDeploymentService deploymentService, ParametersTransformationService parametersTransformationService, CredentialProviderService credentialProviderService, - TargetService targetService) { + TargetService targetService, + BackingServicesProvisionService backingServicesProvisionService) { super(brokeredServices); this.deploymentService = deploymentService; this.parametersTransformationService = parametersTransformationService; this.credentialProviderService = credentialProviderService; this.targetService = targetService; + this.backingServicesProvisionService = backingServicesProvisionService; } @Override public Flux create(CreateServiceInstanceRequest request) { - return getBackingApplicationsForService(request.getServiceDefinition(), request.getPlanId()) - .flatMap(backingApps -> targetService.add(backingApps, request.getServiceInstanceId())) - .flatMap(backingApps -> - parametersTransformationService.transformParameters(backingApps, request.getParameters())) - .flatMap(backingApplications -> - credentialProviderService.addCredentials(backingApplications, request.getServiceInstanceId())) - .flatMapMany(deploymentService::deploy) - .doOnRequest(l -> log.info("Deploying applications {}", brokeredServices)) - .doOnEach(s -> log.info("Finished deploying {}", s)) - .doOnComplete(() -> log.info("Finished deploying applications {}", brokeredServices)) - .doOnError(e -> log.info("Error deploying applications {} with error {}", brokeredServices, e)) - .flatMap(apps -> Flux.empty()); + return + getBackingServicesForService(request.getServiceDefinition(), request.getPlanId()) + .flatMapMany(backingServicesProvisionService::createServiceInstance) + .thenMany( + getBackingApplicationsForService(request.getServiceDefinition(), request.getPlanId()) + .flatMap(backingApps -> targetService.add(backingApps, request.getServiceInstanceId())) + .flatMap(backingApps -> parametersTransformationService.transformParameters(backingApps, request.getParameters())) + .flatMap(backingApplications -> credentialProviderService.addCredentials(backingApplications, request.getServiceInstanceId())) + .flatMapMany(deploymentService::deploy) + .doOnRequest(l -> log.info("Deploying applications {}", brokeredServices)) + .doOnEach(s -> log.info("Finished deploying {}", s)) + .doOnComplete(() -> log.info("Finished deploying applications {}", brokeredServices)) + .doOnError(e -> log.info("Error deploying applications {} with error {}", brokeredServices, e)) + .flatMap(apps -> Flux.empty())); } @Override 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 b477146..ecf285c 100644 --- a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentDeleteServiceInstanceWorkflow.java +++ b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentDeleteServiceInstanceWorkflow.java @@ -22,6 +22,7 @@ import reactor.util.Logger; import reactor.util.Loggers; import org.springframework.cloud.appbroker.deployer.BackingAppDeploymentService; +import org.springframework.cloud.appbroker.deployer.BackingServicesProvisionService; import org.springframework.cloud.appbroker.deployer.BrokeredServices; import org.springframework.cloud.appbroker.extensions.credentials.CredentialProviderService; import org.springframework.cloud.appbroker.extensions.targets.TargetService; @@ -40,29 +41,35 @@ public class AppDeploymentDeleteServiceInstanceWorkflow private final BackingAppDeploymentService deploymentService; private final CredentialProviderService credentialProviderService; private final TargetService targetService; + private final BackingServicesProvisionService backingServicesProvisionService; public AppDeploymentDeleteServiceInstanceWorkflow(BrokeredServices brokeredServices, BackingAppDeploymentService deploymentService, CredentialProviderService credentialProviderService, - TargetService targetService) { + TargetService targetService, + BackingServicesProvisionService backingServicesProvisionService) { super(brokeredServices); this.deploymentService = deploymentService; this.credentialProviderService = credentialProviderService; this.targetService = targetService; + this.backingServicesProvisionService = backingServicesProvisionService; } @Override public Flux delete(DeleteServiceInstanceRequest request) { - return getBackingApplicationsForService(request.getServiceDefinition(), request.getPlanId()) - .flatMap(backingApplications -> - credentialProviderService.deleteCredentials(backingApplications, request.getServiceInstanceId())) - .flatMap(backingApps -> targetService.add(backingApps, request.getServiceInstanceId())) - .flatMapMany(deploymentService::undeploy) - .doOnRequest(l -> log.info("Undeploying applications {}", brokeredServices)) - .doOnEach(s -> log.info("Finished undeploying {}", s)) - .doOnComplete(() -> log.info("Finished undeploying applications {}", brokeredServices)) - .doOnError(e -> log.info("Error undeploying applications {} with error {}", brokeredServices, e)) - .flatMap(apps -> Flux.empty()); + return + getBackingServicesForService(request.getServiceDefinition(), request.getPlanId()) + .flatMapMany(backingServicesProvisionService::deleteServiceInstance) + .thenMany( + getBackingApplicationsForService(request.getServiceDefinition(), request.getPlanId()) + .flatMap(backingApplications -> credentialProviderService.deleteCredentials(backingApplications, request.getServiceInstanceId())) + .flatMap(backingApps -> targetService.add(backingApps, request.getServiceInstanceId())) + .flatMapMany(deploymentService::undeploy) + .doOnRequest(l -> log.info("Undeploying applications {}", brokeredServices)) + .doOnEach(s -> log.info("Finished undeploying {}", s)) + .doOnComplete(() -> log.info("Finished undeploying applications {}", brokeredServices)) + .doOnError(e -> log.info("Error undeploying applications {} with error {}", brokeredServices, e)) + .flatMap(apps -> Flux.empty())); } @Override diff --git a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentInstanceWorkflow.java b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentInstanceWorkflow.java index 337ce0a..cc54b0a 100644 --- a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentInstanceWorkflow.java +++ b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentInstanceWorkflow.java @@ -16,14 +16,17 @@ package org.springframework.cloud.appbroker.workflow.instance; +import java.util.List; + +import reactor.core.publisher.Mono; + import org.springframework.cloud.appbroker.deployer.BackingApplication; 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.BrokeredService; import org.springframework.cloud.appbroker.deployer.BrokeredServices; import org.springframework.cloud.servicebroker.model.catalog.ServiceDefinition; -import reactor.core.publisher.Mono; - -import java.util.List; class AppDeploymentInstanceWorkflow { @@ -44,25 +47,38 @@ class AppDeploymentInstanceWorkflow { Mono.justOrEmpty(findBackingApplications(serviceDefinition, planId))); } + Mono> getBackingServicesForService(ServiceDefinition serviceDefinition, String planId) { + return Mono.defer(() -> + Mono.justOrEmpty(findBackingServices(serviceDefinition, planId))); + } + private BackingApplications findBackingApplications(ServiceDefinition serviceDefinition, String planId) { BrokeredService brokeredService = findBrokeredService(serviceDefinition, planId); return brokeredService == null ? null : new BackingApplications(brokeredService.getApps()); } + private BackingServices findBackingServices(ServiceDefinition serviceDefinition, + String planId) { + BrokeredService brokeredService = findBrokeredService(serviceDefinition, planId); + return brokeredService == null || brokeredService.getServices() == null + ? null + : new BackingServices(brokeredService.getServices()); + } + private BrokeredService findBrokeredService(ServiceDefinition serviceDefinition, String planId) { String serviceName = serviceDefinition.getName(); String planName = serviceDefinition.getPlans().stream() - .filter(plan -> plan.getId().equals(planId)) - .findFirst().get().getName(); + .filter(plan -> plan.getId().equals(planId)) + .findFirst().get().getName(); return brokeredServices.stream() - .filter(brokeredService -> - brokeredService.getServiceName().equals(serviceName) - && brokeredService.getPlanName().equals(planName)) - .findFirst() - .orElse(null); + .filter(brokeredService -> + brokeredService.getServiceName().equals(serviceName) + && brokeredService.getPlanName().equals(planName)) + .findFirst() + .orElse(null); } } diff --git a/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/deployer/BackingServicesProvisionServiceTest.java b/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/deployer/BackingServicesProvisionServiceTest.java new file mode 100644 index 0000000..2b1c97a --- /dev/null +++ b/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/deployer/BackingServicesProvisionServiceTest.java @@ -0,0 +1,85 @@ +package org.springframework.cloud.appbroker.deployer; + +import java.util.ArrayList; +import java.util.Collections; +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 org.mockito.Mockito.doReturn; + +@ExtendWith(MockitoExtension.class) +class BackingServicesProvisionServiceTest { + + @Mock + private DeployerClient deployerClient; + + private BackingServicesProvisionService backingServicesProvisionService; + private BackingServices backingServices; + + @BeforeEach + void setUp() { + backingServicesProvisionService = new BackingServicesProvisionService(deployerClient); + backingServices = BackingServices.builder() + .backingService(BackingService.builder() + .serviceInstanceName("si1") + .name("service1") + .plan("standard") + .parameters(Collections.singletonMap("key1", "value1")) + .build()) + .backingService(BackingService.builder() + .serviceInstanceName("si2") + .name("service2") + .plan("free") + .parameters(Collections.singletonMap("key2", "value2")) + .build()) + .build(); + } + + @Test + @SuppressWarnings("UnassignedFluxMonoInstance") + void shouldCreateServiceInstance() { + doReturn(Mono.just("si1")) + .when(deployerClient).createServiceInstance(backingServices.get(0)); + doReturn(Mono.just("si2")) + .when(deployerClient).createServiceInstance(backingServices.get(1)); + + List expectedValues = new ArrayList<>(); + expectedValues.add("si1"); + expectedValues.add("si2"); + + StepVerifier.create(backingServicesProvisionService.createServiceInstance(backingServices)) + // 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(); + } + + @Test + @SuppressWarnings("UnassignedFluxMonoInstance") + void shouldDeleteServiceInstance() { + doReturn(Mono.just("deleted1")) + .when(deployerClient).deleteServiceInstance(backingServices.get(0)); + doReturn(Mono.just("deleted2")) + .when(deployerClient).deleteServiceInstance(backingServices.get(1)); + + List expectedValues = new ArrayList<>(); + expectedValues.add("deleted1"); + expectedValues.add("deleted2"); + + StepVerifier.create(backingServicesProvisionService.deleteServiceInstance(backingServices)) + // 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(); + } + +} \ No newline at end of file diff --git a/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentCreateServiceInstanceWorkflowTest.java b/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentCreateServiceInstanceWorkflowTest.java index 0a87f94..7272d3d 100644 --- a/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentCreateServiceInstanceWorkflowTest.java +++ b/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/workflow/instance/AppDeploymentCreateServiceInstanceWorkflowTest.java @@ -31,6 +31,9 @@ import reactor.test.StepVerifier; import org.springframework.cloud.appbroker.deployer.BackingAppDeploymentService; import org.springframework.cloud.appbroker.deployer.BackingApplication; 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.BrokeredService; import org.springframework.cloud.appbroker.deployer.BrokeredServices; import org.springframework.cloud.appbroker.extensions.credentials.CredentialProviderService; @@ -62,7 +65,11 @@ class AppDeploymentCreateServiceInstanceWorkflowTest { @Mock private TargetService targetService; + @Mock + private BackingServicesProvisionService backingServicesProvisionService; + private BackingApplications backingApps; + private BackingServices backingServices; private CreateServiceInstanceWorkflow createServiceInstanceWorkflow; @@ -82,6 +89,16 @@ class AppDeploymentCreateServiceInstanceWorkflowTest { .build()) .build(); + backingServices = BackingServices + .builder() + .backingService(BackingService + .builder() + .name("my-service") + .plan("a-plan") + .serviceInstanceName("my-service-instance") + .build()) + .build(); + BrokeredServices brokeredServices = BrokeredServices .builder() .service(BrokeredService @@ -89,6 +106,7 @@ class AppDeploymentCreateServiceInstanceWorkflowTest { .serviceName("service1") .planName("plan1") .apps(backingApps) + .services(backingServices) .build()) .build(); @@ -97,7 +115,8 @@ class AppDeploymentCreateServiceInstanceWorkflowTest { backingAppDeploymentService, parametersTransformationService, credentialProviderService, - targetService); + targetService, + backingServicesProvisionService); } @Test @@ -105,14 +124,7 @@ class AppDeploymentCreateServiceInstanceWorkflowTest { void createServiceInstanceSucceeds() { CreateServiceInstanceRequest request = buildRequest("service1", "plan1"); - given(this.backingAppDeploymentService.deploy(eq(backingApps))) - .willReturn(Flux.just("app1", "app2")); - given(this.parametersTransformationService.transformParameters(eq(backingApps), eq(request.getParameters()))) - .willReturn(Mono.just(backingApps)); - given(this.credentialProviderService.addCredentials(eq(backingApps), eq(request.getServiceInstanceId()))) - .willReturn(Mono.just(backingApps)); - given(this.targetService.add(eq(backingApps), eq(request.getServiceInstanceId()))) - .willReturn(Mono.just(backingApps)); + setupMocks(request); StepVerifier .create(createServiceInstanceWorkflow.create(request)) @@ -121,6 +133,7 @@ class AppDeploymentCreateServiceInstanceWorkflowTest { .verifyComplete(); verify(backingAppDeploymentService).deploy(backingApps); + verify(backingServicesProvisionService).createServiceInstance(backingServices); verifyNoMoreInteractionsWithServices(); } @@ -130,14 +143,7 @@ class AppDeploymentCreateServiceInstanceWorkflowTest { CreateServiceInstanceRequest request = buildRequest("service1", "plan1", singletonMap("ENV_VAR_1", "value from parameters")); - given(this.backingAppDeploymentService.deploy(eq(backingApps))) - .willReturn(Flux.just("app1", "app2")); - given(this.parametersTransformationService.transformParameters(eq(backingApps), eq(request.getParameters()))) - .willReturn(Mono.just(backingApps)); - given(this.credentialProviderService.addCredentials(eq(backingApps), eq(request.getServiceInstanceId()))) - .willReturn(Mono.just(backingApps)); - given(this.targetService.add(eq(backingApps), eq(request.getServiceInstanceId()))) - .willReturn(Mono.just(backingApps)); + setupMocks(request); StepVerifier .create(createServiceInstanceWorkflow.create(request)) @@ -155,14 +161,7 @@ class AppDeploymentCreateServiceInstanceWorkflowTest { CreateServiceInstanceRequest request = buildRequest("service1", "plan1", singletonMap("ENV_VAR_1", "value from parameters")); - given(this.backingAppDeploymentService.deploy(eq(backingApps))) - .willReturn(Flux.just("app1", "app2")); - given(this.parametersTransformationService.transformParameters(eq(backingApps), eq(request.getParameters()))) - .willReturn(Mono.just(backingApps)); - given(this.credentialProviderService.addCredentials(eq(backingApps), eq(request.getServiceInstanceId()))) - .willReturn(Mono.just(backingApps)); - given(this.targetService.add(eq(backingApps), eq(request.getServiceInstanceId()))) - .willReturn(Mono.just(backingApps)); + setupMocks(request); StepVerifier .create(createServiceInstanceWorkflow.create(request)) @@ -187,11 +186,25 @@ class AppDeploymentCreateServiceInstanceWorkflowTest { verifyNoMoreInteractionsWithServices(); } + private void setupMocks(CreateServiceInstanceRequest request) { + given(this.backingAppDeploymentService.deploy(eq(backingApps))) + .willReturn(Flux.just("app1", "app2")); + given(this.parametersTransformationService.transformParameters(eq(backingApps), eq(request.getParameters()))) + .willReturn(Mono.just(backingApps)); + given(this.credentialProviderService.addCredentials(eq(backingApps), eq(request.getServiceInstanceId()))) + .willReturn(Mono.just(backingApps)); + given(this.targetService.add(eq(backingApps), eq(request.getServiceInstanceId()))) + .willReturn(Mono.just(backingApps)); + given(this.backingServicesProvisionService.createServiceInstance(eq(backingServices))) + .willReturn(Flux.just("my-service-instance")); + } + private void verifyNoMoreInteractionsWithServices() { verifyNoMoreInteractions(this.backingAppDeploymentService); verifyNoMoreInteractions(this.parametersTransformationService); verifyNoMoreInteractions(this.credentialProviderService); verifyNoMoreInteractions(this.targetService); + verifyNoMoreInteractions(this.backingServicesProvisionService); } private CreateServiceInstanceRequest buildRequest(String serviceName, String planName) { 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 9641a14..712ca29 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 @@ -28,6 +28,9 @@ import reactor.test.StepVerifier; import org.springframework.cloud.appbroker.deployer.BackingAppDeploymentService; import org.springframework.cloud.appbroker.deployer.BackingApplication; 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.BrokeredService; import org.springframework.cloud.appbroker.deployer.BrokeredServices; import org.springframework.cloud.appbroker.extensions.credentials.CredentialProviderService; @@ -53,30 +56,49 @@ class AppDeploymentDeleteServiceInstanceWorkflowTest { @Mock private CredentialProviderService credentialProviderService; - private DeleteServiceInstanceWorkflow deleteServiceInstanceWorkflow; + @Mock + private BackingServicesProvisionService backingServicesProvisionService; + private BackingApplications backingApps; + private BackingServices backingServices; + + private DeleteServiceInstanceWorkflow deleteServiceInstanceWorkflow; @BeforeEach void setUp() { backingApps = BackingApplications .builder() - .backingApplication(BackingApplication.builder() - .name("app1") - .path("http://myfiles/app1.jar") - .build()) - .backingApplication(BackingApplication.builder() - .name("app2") - .path("http://myfiles/app2.jar") - .build()) + .backingApplication(BackingApplication + .builder() + .name("app1") + .path("http://myfiles/app1.jar") + .build()) + .backingApplication(BackingApplication + .builder() + .name("app2") + .path("http://myfiles/app2.jar") + .build()) + .build(); + + backingServices = BackingServices + .builder() + .backingService(BackingService + .builder() + .name("my-service") + .plan("a-plan") + .serviceInstanceName("my-service-instance") + .build()) .build(); BrokeredServices brokeredServices = BrokeredServices .builder() - .service(BrokeredService.builder() - .serviceName("service1") - .planName("plan1") - .apps(backingApps) - .build()) + .service(BrokeredService + .builder() + .serviceName("service1") + .planName("plan1") + .apps(backingApps) + .services(backingServices) + .build()) .build(); deleteServiceInstanceWorkflow = @@ -84,7 +106,8 @@ class AppDeploymentDeleteServiceInstanceWorkflowTest { brokeredServices, backingAppDeploymentService, credentialProviderService, - targetService); + targetService, + backingServicesProvisionService); } @Test @@ -97,6 +120,8 @@ class AppDeploymentDeleteServiceInstanceWorkflowTest { .willReturn(Mono.just(backingApps)); given(this.targetService.add(eq(backingApps), eq("service-instance-id"))) .willReturn(Mono.just(backingApps)); + given(this.backingServicesProvisionService.deleteServiceInstance(eq(backingServices))) + .willReturn(Flux.just("my-service-instance")); StepVerifier .create(deleteServiceInstanceWorkflow.delete(request)) @@ -104,9 +129,7 @@ class AppDeploymentDeleteServiceInstanceWorkflowTest { .expectNext() .verifyComplete(); - verifyNoMoreInteractions(this.backingAppDeploymentService); - verifyNoMoreInteractions(this.credentialProviderService); - verifyNoMoreInteractions(this.targetService); + verifyNoMoreInteractionsWithServices(); } @Test @@ -115,6 +138,11 @@ class AppDeploymentDeleteServiceInstanceWorkflowTest { .create(deleteServiceInstanceWorkflow.delete(buildRequest("unsupported-service", "plan1"))) .verifyComplete(); + verifyNoMoreInteractionsWithServices(); + } + + private void verifyNoMoreInteractionsWithServices() { + verifyNoMoreInteractions(this.backingServicesProvisionService); verifyNoMoreInteractions(this.backingAppDeploymentService); verifyNoMoreInteractions(this.credentialProviderService); verifyNoMoreInteractions(this.targetService); 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 ffd0b93..6997c6c 100644 --- a/spring-cloud-app-broker-deployer-cloudfoundry/src/main/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployer.java +++ b/spring-cloud-app-broker-deployer-cloudfoundry/src/main/java/org/springframework/cloud/appbroker/deployer/cloudfoundry/CloudFoundryAppDeployer.java @@ -21,10 +21,12 @@ import java.nio.file.Path; import java.time.Duration; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -45,14 +47,22 @@ import org.cloudfoundry.operations.applications.PushApplicationManifestRequest; import org.cloudfoundry.operations.applications.Route; import org.cloudfoundry.operations.organizations.OrganizationDetail; import org.cloudfoundry.operations.organizations.OrganizationInfoRequest; +import org.cloudfoundry.operations.services.GetServiceInstanceRequest; +import org.cloudfoundry.operations.services.ServiceInstance; +import org.cloudfoundry.operations.services.UnbindServiceInstanceRequest; import org.cloudfoundry.operations.spaces.GetSpaceRequest; import org.cloudfoundry.operations.spaces.SpaceDetail; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.Exceptions; +import reactor.core.publisher.Flux; 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.DeleteServiceInstanceRequest; +import org.springframework.cloud.appbroker.deployer.DeleteServiceInstanceResponse; import org.springframework.cloud.appbroker.deployer.DeployApplicationRequest; import org.springframework.cloud.appbroker.deployer.DeployApplicationResponse; import org.springframework.cloud.appbroker.deployer.DeploymentProperties; @@ -494,4 +504,54 @@ public class CloudFoundryAppDeployer implements AppDeployer, ResourceLoaderAware } }; } + + @Override + public Mono createServiceInstance(CreateServiceInstanceRequest request) { + return this.operations + .services() + .createInstance( + org.cloudfoundry.operations.services.CreateServiceInstanceRequest + .builder() + .serviceInstanceName(request.getServiceInstanceName()) + .serviceName(request.getName()) + .planName(request.getPlan()) + .parameters(request.getParameters()) + .build()) + .then(Mono.just(CreateServiceInstanceResponse.builder().name(request.getServiceInstanceName()).build())); + } + + @Override + public Mono deleteServiceInstance(DeleteServiceInstanceRequest request) { + final String serviceInstanceName = request.getName(); + return this.operations + .services() + .getInstance(GetServiceInstanceRequest.builder().name(serviceInstanceName).build()) + .map(ServiceInstance::getApplications) + .flatMap(unbindApplications(serviceInstanceName)) + .then( + this.operations + .services() + .deleteInstance( + org.cloudfoundry.operations.services.DeleteServiceInstanceRequest + .builder() + .name(serviceInstanceName) + .build()) + .then(Mono.just(DeleteServiceInstanceResponse.builder().name(serviceInstanceName).build()))); + } + + private Function, Mono> unbindApplications(String serviceInstanceName) { + return applications -> + Flux.fromIterable(applications) + .flatMap( + application -> + this.operations + .services() + .unbind( + UnbindServiceInstanceRequest + .builder() + .applicationName(application) + .serviceInstanceName(serviceInstanceName) + .build()) + ).collectList(); + } } \ No newline at end of file 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 51f4d61..107c401 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 @@ -25,18 +25,26 @@ import org.cloudfoundry.operations.applications.ApplicationHealthCheck; import org.cloudfoundry.operations.applications.ApplicationManifest; import org.cloudfoundry.operations.applications.Applications; import org.cloudfoundry.operations.applications.PushApplicationManifestRequest; +import org.cloudfoundry.operations.services.GetServiceInstanceRequest; +import org.cloudfoundry.operations.services.ServiceInstance; +import org.cloudfoundry.operations.services.ServiceInstanceType; +import org.cloudfoundry.operations.services.Services; +import org.cloudfoundry.operations.services.UnbindServiceInstanceRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.cloud.appbroker.deployer.DeploymentProperties; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import org.springframework.cloud.appbroker.deployer.AppDeployer; +import org.springframework.cloud.appbroker.deployer.DeleteServiceInstanceRequest; import org.springframework.cloud.appbroker.deployer.DeployApplicationRequest; +import org.springframework.cloud.appbroker.deployer.DeploymentProperties; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.ResourceLoader; @@ -48,6 +56,7 @@ import static org.mockito.Mockito.when; @SuppressWarnings("UnassignedFluxMonoInstance") @ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) class CloudFoundryAppDeployerTest { private static final String APP_NAME = "test-app"; @@ -58,6 +67,9 @@ class CloudFoundryAppDeployerTest { @Mock private Applications applications; + @Mock + private Services services; + @Mock private CloudFoundryOperations cloudFoundryOperations; @@ -81,6 +93,9 @@ class CloudFoundryAppDeployerTest { when(resourceLoader.getResource(APP_PATH)) .thenReturn(new FileSystemResource(APP_PATH)); + when(cloudFoundryOperations.services()) + .thenReturn(services); + appDeployer = new CloudFoundryAppDeployer(deploymentProperties, cloudFoundryOperations, cloudFoundryClient, targetProperties, resourceLoader); } @@ -276,6 +291,46 @@ class CloudFoundryAppDeployerTest { verify(applications).pushManifest(argThat(matchesManifest(expectedManifest))); } + @Test + void deleteServiceInstanceShouldUnbindServices() { + when(services.deleteInstance( + org.cloudfoundry.operations.services.DeleteServiceInstanceRequest.builder() + .name("service-instance-name") + .build())) + .thenReturn(Mono.empty()); + + when(services.getInstance(GetServiceInstanceRequest.builder().name("service-instance-name").build())) + .thenReturn(Mono.just(ServiceInstance.builder() + .id("siid") + .type(ServiceInstanceType.MANAGED) + .name("service-instance-name") + .applications("app1", "app2") + .build())); + + when(services.unbind(UnbindServiceInstanceRequest.builder() + .serviceInstanceName("service-instance-name") + .applicationName("app1") + .build())) + .thenReturn(Mono.empty()); + + when(services.unbind(UnbindServiceInstanceRequest.builder() + .serviceInstanceName("service-instance-name") + .applicationName("app2") + .build())) + .thenReturn(Mono.empty()); + + DeleteServiceInstanceRequest request = + DeleteServiceInstanceRequest.builder() + .name("service-instance-name") + .build(); + + StepVerifier.create( + appDeployer.deleteServiceInstance(request)) + .assertNext(response -> assertThat(response.getName()).isEqualTo("service-instance-name")) + .verifyComplete(); + + } + private ApplicationManifest.Builder baseManifest() { return ApplicationManifest.builder() .environmentVariable("SPRING_APPLICATION_INDEX", "${vcap.application.instance_index}") 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 b6c6c80..5c7d9da 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 @@ -28,4 +28,12 @@ public interface AppDeployer { return Mono.empty(); } + default Mono createServiceInstance(CreateServiceInstanceRequest request) { + return Mono.empty(); + } + + default Mono deleteServiceInstance(DeleteServiceInstanceRequest request) { + return Mono.empty(); + } + } \ No newline at end of file diff --git a/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/CreateServiceInstanceRequest.java b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/CreateServiceInstanceRequest.java new file mode 100644 index 0000000..93f33e5 --- /dev/null +++ b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/CreateServiceInstanceRequest.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.appbroker.deployer; + +import java.util.HashMap; +import java.util.Map; + +public class CreateServiceInstanceRequest { + + private final String serviceInstanceName; + private final String name; + private final String plan; + private final Map parameters; + private final String target; + + CreateServiceInstanceRequest(String serviceInstanceName, + String name, + String plan, + Map parameters, + String target) { + this.serviceInstanceName = serviceInstanceName; + this.name = name; + this.plan = plan; + this.parameters = parameters; + this.target = target; + } + + + + public static CreateServiceInstanceRequestBuilder builder() { + return new CreateServiceInstanceRequestBuilder(); + } + + public String getServiceInstanceName() { + return serviceInstanceName; + } + + public String getName() { + return name; + } + + public String getPlan() { + return plan; + } + + public Map getParameters() { + return parameters; + } + + public String getTarget() { + return target; + } + + public static class CreateServiceInstanceRequestBuilder { + + private String serviceInstanceName; + private String name; + private String plan; + private final Map parameters = new HashMap<>(); + private String target; + + CreateServiceInstanceRequestBuilder() { + } + + public CreateServiceInstanceRequestBuilder serviceInstanceName(String serviceInstanceName) { + this.serviceInstanceName = serviceInstanceName; + return this; + } + + public CreateServiceInstanceRequestBuilder name(String name) { + this.name = name; + return this; + } + + public CreateServiceInstanceRequestBuilder plan(String plan) { + this.plan = plan; + return this; + } + + public CreateServiceInstanceRequestBuilder parameters(String key, String value) { + this.parameters.put(key, value); + return this; + } + + public CreateServiceInstanceRequestBuilder parameters(Map parameters) { + if (parameters == null) { + return this; + } + this.parameters.putAll(parameters); + return this; + } + + public CreateServiceInstanceRequestBuilder target(String target) { + this.target = target; + return this; + } + + public CreateServiceInstanceRequest build() { + return new CreateServiceInstanceRequest(serviceInstanceName, name, plan, parameters, target); + } + } + +} diff --git a/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/CreateServiceInstanceResponse.java b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/CreateServiceInstanceResponse.java new file mode 100644 index 0000000..1adebbb --- /dev/null +++ b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/CreateServiceInstanceResponse.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.appbroker.deployer; + +public class CreateServiceInstanceResponse { + + private final String name; + + CreateServiceInstanceResponse(String name) { + this.name = name; + } + + public static CreateServiceInstanceResponseBuilder builder() { + return new CreateServiceInstanceResponseBuilder(); + } + + public String getName() { + return name; + } + + public static class CreateServiceInstanceResponseBuilder { + + private String name; + + CreateServiceInstanceResponseBuilder() { + } + + public CreateServiceInstanceResponseBuilder name(String name) { + this.name = name; + return this; + } + + public CreateServiceInstanceResponse build() { + return new CreateServiceInstanceResponse(name); + } + + } +} diff --git a/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/DeleteServiceInstanceRequest.java b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/DeleteServiceInstanceRequest.java new file mode 100644 index 0000000..e60410f --- /dev/null +++ b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/DeleteServiceInstanceRequest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.appbroker.deployer; + +public class DeleteServiceInstanceRequest { + + private final String name; + + DeleteServiceInstanceRequest(String name) { + this.name = name; + } + + public static DeleteServiceInstanceRequestBuilder builder() { + return new DeleteServiceInstanceRequestBuilder(); + } + + public String getName() { + return name; + } + + public static class DeleteServiceInstanceRequestBuilder { + + private String name; + + DeleteServiceInstanceRequestBuilder() { + } + + public DeleteServiceInstanceRequestBuilder name(String name) { + this.name = name; + return this; + } + + public DeleteServiceInstanceRequest build() { + return new DeleteServiceInstanceRequest(name); + } + + } +} diff --git a/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/DeleteServiceInstanceResponse.java b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/DeleteServiceInstanceResponse.java new file mode 100644 index 0000000..070a92b --- /dev/null +++ b/spring-cloud-app-broker-deployer/src/main/java/org/springframework/cloud/appbroker/deployer/DeleteServiceInstanceResponse.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.appbroker.deployer; + +public class DeleteServiceInstanceResponse { + + private final String name; + + DeleteServiceInstanceResponse(String name) { + this.name = name; + } + + public static DeleteServiceInstanceResponseBuilder builder() { + return new DeleteServiceInstanceResponseBuilder(); + } + + public String getName() { + return name; + } + + public static class DeleteServiceInstanceResponseBuilder { + + private String name; + + DeleteServiceInstanceResponseBuilder() { + } + + public DeleteServiceInstanceResponseBuilder name(String name) { + this.name = name; + return this; + } + + public DeleteServiceInstanceResponse build() { + return new DeleteServiceInstanceResponse(name); + } + + } +} diff --git a/spring-cloud-app-broker-sample/src/test/java/org.springframework.cloud.appbroker/sample/CreateInstanceWithServicesComponentTest.java b/spring-cloud-app-broker-sample/src/test/java/org.springframework.cloud.appbroker/sample/CreateInstanceWithServicesComponentTest.java new file mode 100644 index 0000000..4c5c412 --- /dev/null +++ b/spring-cloud-app-broker-sample/src/test/java/org.springframework.cloud.appbroker/sample/CreateInstanceWithServicesComponentTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2016-2018. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.appbroker.sample; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.appbroker.sample.fixtures.CloudControllerStubFixture; +import org.springframework.cloud.appbroker.sample.fixtures.OpenServiceBrokerApiFixture; +import org.springframework.cloud.servicebroker.model.instance.OperationState; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.TestPropertySource; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.springframework.cloud.appbroker.sample.CreateInstanceWithServicesComponentTest.APP_NAME; +import static org.springframework.cloud.appbroker.sample.CreateInstanceWithServicesComponentTest.SERVICE_1_NAME; +import static org.springframework.cloud.appbroker.sample.CreateInstanceWithServicesComponentTest.SERVICE_INSTANCE_1_NAME; + +@TestPropertySource(properties = { + "spring.cloud.appbroker.services[0].service-name=example", + "spring.cloud.appbroker.services[0].plan-name=standard", + "spring.cloud.appbroker.services[0].apps[0].path=classpath:demo.jar", + "spring.cloud.appbroker.services[0].apps[0].name=" + APP_NAME, + "spring.cloud.appbroker.services[0].apps[0].services[0].service-instance-name=" + SERVICE_INSTANCE_1_NAME, + "spring.cloud.appbroker.services[0].services[0].service-instance-name=" + SERVICE_INSTANCE_1_NAME, + "spring.cloud.appbroker.services[0].services[0].name=" + SERVICE_1_NAME, + "spring.cloud.appbroker.services[0].services[0].plan=standard" +}) +class CreateInstanceWithServicesComponentTest extends WiremockComponentTest { + + static final String APP_NAME = "app-with-new-services"; + + static final String SERVICE_INSTANCE_1_NAME = "my-db-service"; + static final String SERVICE_1_NAME = "db-service"; + + @Autowired + private OpenServiceBrokerApiFixture brokerFixture; + + @Autowired + private CloudControllerStubFixture cloudControllerFixture; + + @Test + void pushAppWithServicesWhenServicesExist() { + cloudControllerFixture.stubAppDoesNotExist(APP_NAME); + cloudControllerFixture.stubPushApp(APP_NAME); + + // given that service instances does not exist + cloudControllerFixture.stubServiceInstanceDoesNotExists(SERVICE_INSTANCE_1_NAME); + + // and the services are available in the marketplace + cloudControllerFixture.stubServiceExists(SERVICE_1_NAME); + + // will create and bind the service instance + cloudControllerFixture.stubCreateServiceInstance(SERVICE_INSTANCE_1_NAME); + cloudControllerFixture.stubCreateServiceBinding(APP_NAME, SERVICE_INSTANCE_1_NAME); + cloudControllerFixture.stubServiceInstanceExists(SERVICE_INSTANCE_1_NAME); + + // when a service instance is created + given(brokerFixture.serviceInstanceRequest()) + .when() + .put(brokerFixture.createServiceInstanceUrl(), "instance-id") + .then() + .statusCode(HttpStatus.ACCEPTED.value()); + + // when the "last_operation" API is polled + given(brokerFixture.serviceInstanceRequest()) + .when() + .get(brokerFixture.getLastInstanceOperationUrl(), "instance-id") + .then() + .statusCode(HttpStatus.OK.value()) + .body("state", is(equalTo(OperationState.IN_PROGRESS.toString()))); + + String state = brokerFixture.waitForAsyncOperationComplete("instance-id"); + assertThat(state).isEqualTo(OperationState.SUCCEEDED.toString()); + } +} \ No newline at end of file diff --git a/spring-cloud-app-broker-sample/src/test/java/org.springframework.cloud.appbroker/sample/fixtures/CloudControllerStubFixture.java b/spring-cloud-app-broker-sample/src/test/java/org.springframework.cloud.appbroker/sample/fixtures/CloudControllerStubFixture.java index 0cd315f..9cb3886 100644 --- a/spring-cloud-app-broker-sample/src/test/java/org.springframework.cloud.appbroker/sample/fixtures/CloudControllerStubFixture.java +++ b/spring-cloud-app-broker-sample/src/test/java/org.springframework.cloud.appbroker/sample/fixtures/CloudControllerStubFixture.java @@ -291,6 +291,32 @@ public class CloudControllerStubFixture extends WiremockStubFixture { replace("@guid", serviceInstanceName + "-GUID"))))); } + public void stubServiceInstanceDoesNotExists(String serviceInstanceName) { + stubFor(get(urlEqualTo("/v2/spaces/" + TEST_SPACE_GUID + "/service_instances" + + "?q=name:" + serviceInstanceName + + "&page=1" + + "&return_user_provided_service_instances=true")) + .willReturn(ok() + .withBody(cc("list-space-service_instances-empty")))); + } + + public void stubServiceExists(String serviceName) { + stubFor(get(urlEqualTo("/v2/spaces/" + TEST_SPACE_GUID + "/services" + + "?q=label:" + serviceName + + "&page=1")) + .willReturn(ok() + .withBody(cc("list-space-services")))); + + stubFor(get(urlEqualTo("/v2/service_plans?q=service_guid:SERVICE-ID&page=1")) + .willReturn(ok() + .withBody(cc("list-service-plans")))); + } + public void stubCreateServiceInstance(String serviceInstanceName) { + stubFor(post(urlEqualTo("/v2/service_instances?accepts_incomplete=true")) + .withRequestBody(matchingJsonPath("$.[?(@.name == '" + serviceInstanceName + "')]")) + .willReturn(ok())); + } + public void stubCreateServiceBinding(String appName, String serviceInstanceName) { String serviceInstanceGuid = serviceInstanceName + "-GUID"; String serviceBindingGuid = appGuid(appName) + "-" + serviceInstanceGuid; diff --git a/spring-cloud-app-broker-sample/src/test/resources/responses/cloudcontroller/list-service-plans.json b/spring-cloud-app-broker-sample/src/test/resources/responses/cloudcontroller/list-service-plans.json new file mode 100644 index 0000000..36ed36c --- /dev/null +++ b/spring-cloud-app-broker-sample/src/test/resources/responses/cloudcontroller/list-service-plans.json @@ -0,0 +1,43 @@ +{ + "total_results": 1, + "total_pages": 1, + "prev_url": null, + "next_url": null, + "resources": [ + { + "metadata": { + "guid": "SERVICE-PLAN-ID", + "url": "/v2/service_plans/SERVICE-PLAN-ID", + "created_at": "2018-01-26T20:48:20Z", + "updated_at": "2018-06-11T18:08:05Z" + }, + "entity": { + "name": "standard", + "free": true, + "description": "basic", + "service_guid": "SERVICE-ID", + "extra": "{}", + "public": true, + "bindable": true, + "active": true, + "service_url": "/v2/services/SERVICE-ID", + "service_instances_url": "/v2/service_plans/SERVICE-PLAN-ID/service_instances", + "schemas": { + "service_instance": { + "create": { + "parameters": {} + }, + "update": { + "parameters": {} + } + }, + "service_binding": { + "create": { + "parameters": {} + } + } + } + } + } + ] +} \ No newline at end of file diff --git a/spring-cloud-app-broker-sample/src/test/resources/responses/cloudcontroller/list-space-service_instances-empty.json b/spring-cloud-app-broker-sample/src/test/resources/responses/cloudcontroller/list-space-service_instances-empty.json new file mode 100644 index 0000000..6001309 --- /dev/null +++ b/spring-cloud-app-broker-sample/src/test/resources/responses/cloudcontroller/list-space-service_instances-empty.json @@ -0,0 +1,8 @@ +{ + "total_results": 0, + "total_pages": 1, + "prev_url": null, + "next_url": null, + "resources": [ + ] +} \ No newline at end of file diff --git a/spring-cloud-app-broker-sample/src/test/resources/responses/cloudcontroller/list-space-services.json b/spring-cloud-app-broker-sample/src/test/resources/responses/cloudcontroller/list-space-services.json new file mode 100644 index 0000000..a412d13 --- /dev/null +++ b/spring-cloud-app-broker-sample/src/test/resources/responses/cloudcontroller/list-space-services.json @@ -0,0 +1,34 @@ +{ + "total_results": 1, + "total_pages": 1, + "prev_url": null, + "next_url": null, + "resources": [ + { + "metadata": { + "guid": "SERVICE-ID", + "url": "/v2/services/SERVICE-ID", + "created_at": "2018-01-26T20:48:20Z", + "updated_at": "2018-06-11T18:08:05Z" + }, + "entity": { + "label": "db-service", + "provider": null, + "url": null, + "description": "My DB Service", + "long_description": null, + "version": null, + "info_url": null, + "active": 1, + "bindable": 1, + "extra": "{}", + "tags": [ + ], + "requires": [], + "documentation_url": null, + "plan_updateable": 0, + "service_plans_url": "/v2/services/SERVICE-ID/service_plans" + } + } + ] +} \ No newline at end of file