Adding the ability to customize the way creation parameters are mapped to SPRING_APPLICATION_JSON

Some trade-offs:
The `ParametersTransformer` interface takes all the backing apps because otherwise we wouldn't allow users to config properties for each one of them.
I went for the simple solution of adding the `Bean` instead of going all the way through strategy matched via config.

Closes #22
This commit is contained in:
Alberto Rios
2018-08-27 13:31:02 +02:00
parent a73c600181
commit f97e1e633c
7 changed files with 259 additions and 22 deletions

View File

@@ -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`

View File

@@ -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);
}
}

View File

@@ -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<String> create(Map<String, Object> parameters) {
backingApps.forEach(backingApplication -> {
final Map<String, String> environment = new HashMap<>();
final Map<String, String> 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))

View File

@@ -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<String, Object> parameters) {
backingApps.forEach(backingApplication -> {
final Map<String, String> environment = new HashMap<>();
final Map<String, String> backingAppEnvironment = backingApplication.getEnvironment();
if (backingAppEnvironment != null) {
environment.putAll(backingAppEnvironment);
}
if (parameters != null) {
parameters.forEach((key, value) -> environment.put(key, value.toString()));
}
backingApplication.setEnvironment(environment);
});
}
}

View File

@@ -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<String, Object> parameters);
}

View File

@@ -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<String, String> 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<String, Object> parameters = singletonMap("ENV_VAR_1", "value from parameters");
@@ -117,7 +131,9 @@ class CreateServiceInstanceWorkflowTest {
// given that properties contains app details and environment variables
Map<String, String> 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<String, Object> 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<String, Object> parameters = singletonMap("ENV_VAR_1", "value from parameters");

View File

@@ -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<String, Object> 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<String, Object> parameters) {
backingApps.forEach(backingApplication -> backingApplication.setEnvironment(createEnvironmentMap(parameters)));
}
private Map<String, String> createEnvironmentMap(Map<String, Object> 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;
}
}
}
}