diff --git a/README.adoc b/README.adoc index 09bd8fa..75d6107 100644 --- a/README.adoc +++ b/README.adoc @@ -7,3 +7,11 @@ image:https://codecov.io/gh/spring-cloud-incubator/spring-cloud-app-broker/branc Spring Cloud App Broker is a project for implementing service broker functionality within a Spring Boot application. +Spring Cloud App Broker allows you to implement different strategies for different steps. + +== Implementing a custom ParametersTransformer + +By default, the parameters created via `-c '{"key", "value"}'` will be added to `SPRING_APPLICATION_JSON`. +In the case, the same properties were provided via `spring.cloud.appbroker.apps[0].environment.key=value`. The one provided via `-c` will take precedence. + +The default implementation can be overwritten by just creating a `Bean` that implements the `ParametersTransformer` interface as in `CreateInstanceWithCustomCreationParametersComponentTest` 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 4dcf178..ea6523c 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 @@ -18,16 +18,19 @@ package org.springframework.cloud.appbroker.autoconfigure; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.appbroker.deployer.BackingAppDeploymentService; import org.springframework.cloud.appbroker.deployer.BackingApplications; import org.springframework.cloud.appbroker.deployer.DeployerClient; import org.springframework.cloud.appbroker.deployer.ReactiveAppDeployer; +import org.springframework.cloud.appbroker.service.WorkflowServiceInstanceService; import org.springframework.cloud.appbroker.state.InMemoryServiceInstanceStateRepository; import org.springframework.cloud.appbroker.state.ServiceInstanceStateRepository; -import org.springframework.cloud.appbroker.service.WorkflowServiceInstanceService; import org.springframework.cloud.appbroker.workflow.instance.CreateServiceInstanceWorkflow; +import org.springframework.cloud.appbroker.workflow.instance.DefaultParametersTransformer; import org.springframework.cloud.appbroker.workflow.instance.DeleteServiceInstanceWorkflow; +import org.springframework.cloud.appbroker.workflow.instance.ParametersTransformer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -59,10 +62,17 @@ public class AppBrokerAutoConfiguration { return new InMemoryServiceInstanceStateRepository(); } + @Bean + @ConditionalOnMissingBean(ParametersTransformer.class) + public ParametersTransformer parametersTransformer() { + return new DefaultParametersTransformer(); + } + @Bean public CreateServiceInstanceWorkflow createServiceInstanceWorkflow(BackingApplications backingApplications, - BackingAppDeploymentService backingAppDeploymentService) { - return new CreateServiceInstanceWorkflow(backingApplications, backingAppDeploymentService); + BackingAppDeploymentService backingAppDeploymentService, + ParametersTransformer parametersTransformer) { + return new CreateServiceInstanceWorkflow(backingApplications, backingAppDeploymentService, parametersTransformer); } @Bean @@ -75,6 +85,6 @@ public class AppBrokerAutoConfiguration { public WorkflowServiceInstanceService serviceInstanceService(ServiceInstanceStateRepository stateRepository, CreateServiceInstanceWorkflow createWorkflow, DeleteServiceInstanceWorkflow deleteWorkflow) { - return new WorkflowServiceInstanceService(stateRepository,createWorkflow, deleteWorkflow); + return new WorkflowServiceInstanceService(stateRepository, createWorkflow, deleteWorkflow); } } diff --git a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/CreateServiceInstanceWorkflow.java b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/CreateServiceInstanceWorkflow.java index 91da7eb..cfd51d3 100644 --- a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/CreateServiceInstanceWorkflow.java +++ b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/CreateServiceInstanceWorkflow.java @@ -16,7 +16,6 @@ package org.springframework.cloud.appbroker.workflow.instance; -import java.util.HashMap; import java.util.Map; import org.springframework.cloud.appbroker.deployer.BackingAppDeploymentService; import org.springframework.cloud.appbroker.deployer.BackingApplications; @@ -29,25 +28,18 @@ public class CreateServiceInstanceWorkflow { private BackingApplications backingApps; private BackingAppDeploymentService deploymentService; + private ParametersTransformer parametersTransformer; public CreateServiceInstanceWorkflow(BackingApplications backingApps, - BackingAppDeploymentService deploymentService) { + BackingAppDeploymentService deploymentService, + ParametersTransformer parametersTransformer) { this.backingApps = backingApps; this.deploymentService = deploymentService; + this.parametersTransformer = parametersTransformer; } public Mono create(Map parameters) { - backingApps.forEach(backingApplication -> { - final Map environment = new HashMap<>(); - final Map backingAppEnvironment = backingApplication.getEnvironment(); - if (backingAppEnvironment != null) { - environment.putAll(backingAppEnvironment); - } - if (parameters != null) { - parameters.forEach((key, value) -> environment.put(key, value.toString())); - } - backingApplication.setEnvironment(environment); - }); + parametersTransformer.transform(backingApps, parameters); return deploymentService.deploy(backingApps) .doOnRequest(l -> log.info("Deploying applications {}", backingApps)) diff --git a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/DefaultParametersTransformer.java b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/DefaultParametersTransformer.java new file mode 100644 index 0000000..ade3356 --- /dev/null +++ b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/DefaultParametersTransformer.java @@ -0,0 +1,39 @@ +/* + * 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.workflow.instance; + +import java.util.HashMap; +import java.util.Map; +import org.springframework.cloud.appbroker.deployer.BackingApplications; + +public class DefaultParametersTransformer implements ParametersTransformer { + + public void transform(BackingApplications backingApps, Map parameters) { + backingApps.forEach(backingApplication -> { + final Map environment = new HashMap<>(); + final Map backingAppEnvironment = backingApplication.getEnvironment(); + if (backingAppEnvironment != null) { + environment.putAll(backingAppEnvironment); + } + if (parameters != null) { + parameters.forEach((key, value) -> environment.put(key, value.toString())); + } + backingApplication.setEnvironment(environment); + }); + } + +} diff --git a/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/ParametersTransformer.java b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/ParametersTransformer.java new file mode 100644 index 0000000..d1cae2a --- /dev/null +++ b/spring-cloud-app-broker-core/src/main/java/org/springframework/cloud/appbroker/workflow/instance/ParametersTransformer.java @@ -0,0 +1,26 @@ +/* + * 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.workflow.instance; + +import java.util.Map; +import org.springframework.cloud.appbroker.deployer.BackingApplications; + +public interface ParametersTransformer { + + void transform(BackingApplications backingApps, Map parameters); + +} diff --git a/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/workflow/instance/CreateServiceInstanceWorkflowTest.java b/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/workflow/instance/CreateServiceInstanceWorkflowTest.java index 5af6d5d..081b99c 100644 --- a/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/workflow/instance/CreateServiceInstanceWorkflowTest.java +++ b/spring-cloud-app-broker-core/src/test/java/org/springframework/cloud/appbroker/workflow/instance/CreateServiceInstanceWorkflowTest.java @@ -18,6 +18,7 @@ package org.springframework.cloud.appbroker.workflow.instance; import java.util.Collections; import java.util.Map; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -40,6 +41,12 @@ class CreateServiceInstanceWorkflowTest { @Mock private BackingAppDeploymentService backingAppDeploymentService; + private ParametersTransformer parametersTransformer; + + @BeforeEach + void setUp() { + parametersTransformer = new DefaultParametersTransformer(); + } @Test void shouldCreateServiceInstance() { @@ -47,8 +54,11 @@ class CreateServiceInstanceWorkflowTest { .willReturn(Mono.just("deployment-id-app1")); // given that properties contains app details + parametersTransformer = new DefaultParametersTransformer(); CreateServiceInstanceWorkflow createServiceInstanceWorkflow = - new CreateServiceInstanceWorkflow(createBackingApplications(), backingAppDeploymentService); + new CreateServiceInstanceWorkflow(createBackingApplications(), + backingAppDeploymentService, + parametersTransformer); // when createServiceInstanceWorkflow.create(Collections.emptyMap()); @@ -71,7 +81,9 @@ class CreateServiceInstanceWorkflowTest { // given that properties contains app details and environment variables Map environment = singletonMap("ENV_VAR_1", "value from environment"); CreateServiceInstanceWorkflow createServiceInstanceWorkflow = - new CreateServiceInstanceWorkflow(createBackingApplicationWithEnvironment(environment), backingAppDeploymentService); + new CreateServiceInstanceWorkflow(createBackingApplicationWithEnvironment(environment), + backingAppDeploymentService, + parametersTransformer); // when createServiceInstanceWorkflow.create(Collections.emptyMap()); @@ -93,7 +105,9 @@ class CreateServiceInstanceWorkflowTest { // given that properties contains app details CreateServiceInstanceWorkflow createServiceInstanceWorkflow = - new CreateServiceInstanceWorkflow(createBackingApplications(), backingAppDeploymentService); + new CreateServiceInstanceWorkflow(createBackingApplications(), + backingAppDeploymentService, + parametersTransformer); // when create is called with parameters Map parameters = singletonMap("ENV_VAR_1", "value from parameters"); @@ -117,7 +131,9 @@ class CreateServiceInstanceWorkflowTest { // given that properties contains app details and environment variables Map environment = singletonMap("ENV_VAR_1", "value from environment"); CreateServiceInstanceWorkflow createServiceInstanceWorkflow = - new CreateServiceInstanceWorkflow(createBackingApplicationWithEnvironment(environment), backingAppDeploymentService); + new CreateServiceInstanceWorkflow(createBackingApplicationWithEnvironment(environment), + backingAppDeploymentService, + parametersTransformer); // when create is called with parameters Map parameters = singletonMap("ENV_VAR_1", "value from parameters"); @@ -151,7 +167,9 @@ class CreateServiceInstanceWorkflowTest { .build(); CreateServiceInstanceWorkflow createServiceInstanceWorkflow = - new CreateServiceInstanceWorkflow(backingApplications, backingAppDeploymentService); + new CreateServiceInstanceWorkflow(backingApplications, + backingAppDeploymentService, + parametersTransformer); // when create is called with parameters Map parameters = singletonMap("ENV_VAR_1", "value from parameters"); diff --git a/spring-cloud-app-broker-sample/src/test/java/org.springframework.cloud.appbroker/sample/CreateInstanceWithCustomCreationParametersComponentTest.java b/spring-cloud-app-broker-sample/src/test/java/org.springframework.cloud.appbroker/sample/CreateInstanceWithCustomCreationParametersComponentTest.java new file mode 100644 index 0000000..564bc82 --- /dev/null +++ b/spring-cloud-app-broker-sample/src/test/java/org.springframework.cloud.appbroker/sample/CreateInstanceWithCustomCreationParametersComponentTest.java @@ -0,0 +1,144 @@ +/* + * 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 com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.appbroker.deployer.BackingApplications; +import org.springframework.cloud.appbroker.sample.fixtures.CloudControllerStubFixture; +import org.springframework.cloud.appbroker.sample.fixtures.OpenServiceBrokerApiFixture; +import org.springframework.cloud.appbroker.workflow.instance.ParametersTransformer; +import org.springframework.cloud.servicebroker.model.instance.OperationState; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; + + +import static com.github.tomakehurst.wiremock.client.WireMock.matchingJsonPath; +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.CreateInstanceWithCustomCreationParametersComponentTest.APP_NAME; + +@TestPropertySource(properties = { + "spring.cloud.appbroker.apps[0].path=classpath:demo.jar", + "spring.cloud.appbroker.apps[0].name=" + APP_NAME +}) +@ContextConfiguration(classes = CreateInstanceWithCustomCreationParametersComponentTest.CustomConfig.class) +class CreateInstanceWithCustomCreationParametersComponentTest extends WiremockComponentTest { + static final String APP_NAME = "app-with-request-create-params"; + + @Autowired + private OpenServiceBrokerApiFixture brokerFixture; + + @Autowired + private CloudControllerStubFixture cloudControllerFixture; + + @Test + void shouldPushAppWithEnvironmentWhenCreateServiceEndpointCalledWithCreationParameters() { + cloudControllerFixture.stubAppDoesNotExist(APP_NAME); + cloudControllerFixture.stubPushApp(APP_NAME, + matchingJsonPath("$.environment_json[?(@.SPRING_APPLICATION_JSON =~ /.*otherNestedKey.*:.*otherKey.*:.*keyValue.*/)]"), + matchingJsonPath("$.environment_json[?(@.SPRING_APPLICATION_JSON =~ /.*otherNestedKey.*:.*otherLabel.*:.*labelValue.*/)]")); + + // given a set of parameters + Map params = new HashMap<>(); + params.put("firstKey", "{\"label\":\"labelValue\",\"secondKey\":\"keyValue\"}"); + + // when a service instance is created + given(brokerFixture.serviceInstanceRequest(params)) + .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()); + } + + @Configuration + static class CustomConfig { + @Bean + public ParametersTransformer parametersTransformer() { + return new CustomParametersTransformer(); + } + + public class CustomParametersTransformer implements ParametersTransformer { + @Override + public void transform(BackingApplications backingApps, Map parameters) { + backingApps.forEach(backingApplication -> backingApplication.setEnvironment(createEnvironmentMap(parameters))); + } + + private Map createEnvironmentMap(Map parameters) { + ObjectMapper objectMapper = new ObjectMapper(); + ObjectNode customOutputEnvironmentParameters = objectMapper.createObjectNode(); + try { + CustomInputParameters customInputParameters = objectMapper.readValue(parameters.get("firstKey").toString(), CustomInputParameters.class); + customOutputEnvironmentParameters.put("otherKey", customInputParameters.getSecondKey()); + customOutputEnvironmentParameters.put("otherLabel", customInputParameters.getLabel()); + } catch (Exception e) { + e.printStackTrace(); + } + return Collections.singletonMap("otherNestedKey", customOutputEnvironmentParameters.toString()); + } + + } + + static class CustomInputParameters { + + private CustomInputParameters() { + } + + private String secondKey; + private String label; + + String getSecondKey() { + return secondKey; + } + + String getLabel() { + return label; + } + + public void setSecondKey(String secondKey) { + this.secondKey = secondKey; + } + + public void setLabel(String label) { + this.label = label; + } + } + } + +} +