Remove dependency on Spring Cloud Deployer. Remove unused code from the deployer abstraction.

This commit is contained in:
Scott Frederick
2018-10-01 16:38:26 -05:00
parent e93a0d0eb9
commit 21a1053cca
23 changed files with 756 additions and 694 deletions

View File

@@ -46,6 +46,7 @@ configure(allprojects) {
ext {
openServiceBrokerVersion = "3.0.0.BUILD-SNAPSHOT"
springVersion = "5.0.7.RELEASE"
reactorVersion = "3.1.8.RELEASE"
junitJupiterVersion = "5.2.0"
assertjVersion = "3.10.0"

View File

@@ -25,6 +25,7 @@ dependencyManagement {
dependencies {
api project(":spring-cloud-app-broker-core")
api project(":spring-cloud-app-broker-deployer-cloudfoundry")
api("org.springframework.boot:spring-boot-autoconfigure")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
annotationProcessor("org.springframework.boot:spring-boot-autoconfigure-processor")

View File

@@ -22,9 +22,7 @@ import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.cloud.appbroker.deployer.AppDeployer;
import org.springframework.cloud.appbroker.deployer.cloudfoundry.CloudFoundryAppDeployer;
import org.springframework.cloud.appbroker.deployer.cloudfoundry.NoOpAppNameGenerator;
import org.springframework.cloud.deployer.spi.cloudfoundry.AppNameGenerator;
import org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryDeploymentProperties;
import org.springframework.cloud.appbroker.deployer.cloudfoundry.CloudFoundryDeploymentProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ResourceLoader;
@@ -35,16 +33,10 @@ public class AppDeployerAutoConfiguration {
@Bean
@ConditionalOnBean(CloudFoundryOperations.class)
public AppDeployer cloudFoundryReactiveAppDeployer(AppNameGenerator noOpApplicationNameGenerator,
CloudFoundryOperations cloudFoundryOperations,
ResourceLoader resourceLoader) {
public AppDeployer cloudFoundryAppDeployer(CloudFoundryOperations cloudFoundryOperations,
ResourceLoader resourceLoader) {
CloudFoundryDeploymentProperties cloudFoundryDeploymentProperties = new CloudFoundryDeploymentProperties();
return new CloudFoundryAppDeployer(noOpApplicationNameGenerator, cloudFoundryDeploymentProperties,
cloudFoundryOperations, null, resourceLoader);
}
@Bean
public AppNameGenerator noOpApplicationNameGenerator() {
return new NoOpAppNameGenerator();
return new CloudFoundryAppDeployer(cloudFoundryDeploymentProperties,
cloudFoundryOperations, resourceLoader);
}
}

View File

@@ -48,7 +48,7 @@ public class DeployerClient {
return appDeployer.undeploy(UndeployApplicationRequest.builder()
.name(backingApplication.getName())
.build())
.map(UndeployApplicationResponse::getStatus);
.map(UndeployApplicationResponse::getName);
}
}

View File

@@ -152,7 +152,7 @@ class DeployerClientTest {
// given
when(appDeployer.undeploy(any()))
.thenReturn(Mono.just(UndeployApplicationResponse.builder()
.status("undeployed")
.name(APP_NAME)
.build()));
BackingApplication application = BackingApplication.builder()
@@ -163,7 +163,7 @@ class DeployerClientTest {
// when
StepVerifier.create(deployerClient.undeploy(application))
// then
.expectNext("undeployed")
.expectNext(APP_NAME)
.verifyComplete();
verify(appDeployer).undeploy(argThat(request -> APP_NAME.equals(request.getName())));

View File

@@ -18,17 +18,15 @@ description = "Spring Cloud App Broker Deployer Cloud Foundry"
ext {
reactorNettyVersion = '0.7.5.RELEASE'
// webflux defaults; must be overridden for org.cloudfoundry:cloudfoundry-client-reactor:3.9.0.RELEASE
// reactorVersion = '3.1.7.RELEASE'
// reactorNettyVersion = '0.7.7.RELEASE'
deployerCFVersion = '1.4.0.RELEASE'
cfJavaClientVersion = "3.9.0.RELEASE"
}
dependencies {
api project(":spring-cloud-app-broker-deployer")
api("org.springframework.cloud:spring-cloud-deployer-cloudfoundry:${deployerCFVersion}")
api("org.cloudfoundry:cloudfoundry-operations:${cfJavaClientVersion}")
api("org.cloudfoundry:cloudfoundry-client-reactor:${cfJavaClientVersion}")
api("org.cloudfoundry:cloudfoundry-util:${cfJavaClientVersion}")
// fix the Boot version to prevent 1.5.x and 2.0.x on the classpath
implementation("org.springframework.boot:spring-boot-starter-validation:${springBootVersion}")

View File

@@ -1,177 +0,0 @@
/*
* 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.cloudfoundry;
import java.io.IOException;
import java.nio.file.Path;
import java.time.Duration;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import org.cloudfoundry.AbstractCloudFoundryException;
import org.cloudfoundry.UnknownCloudFoundryException;
import org.cloudfoundry.util.DelayUtils;
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.deployer.spi.app.AppDeployer;
import org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryDeploymentProperties;
import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest;
import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo;
import org.springframework.cloud.deployer.spi.util.ByteSizeUtils;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import static org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryDeploymentProperties.BUILDPACK_PROPERTY_KEY;
import static org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryDeploymentProperties.JAVA_OPTS_PROPERTY_KEY;
import static org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryDeploymentProperties.SERVICES_PROPERTY_KEY;
class AbstractCloudFoundryAppDeployer {
private final RuntimeEnvironmentInfo runtimeEnvironmentInfo;
final CloudFoundryDeploymentProperties deploymentProperties;
private final Logger logger = LoggerFactory.getLogger(AbstractCloudFoundryAppDeployer.class);
AbstractCloudFoundryAppDeployer(CloudFoundryDeploymentProperties deploymentProperties, RuntimeEnvironmentInfo runtimeEnvironmentInfo) {
this.deploymentProperties = deploymentProperties;
this.runtimeEnvironmentInfo = runtimeEnvironmentInfo;
}
int memory(AppDeploymentRequest request) {
String withUnit = request.getDeploymentProperties()
.getOrDefault(org.springframework.cloud.deployer.spi.app.AppDeployer.MEMORY_PROPERTY_KEY, this.deploymentProperties.getMemory());
return (int) ByteSizeUtils.parseToMebibytes(withUnit);
}
Set<String> servicesToBind(AppDeploymentRequest request) {
Set<String> services = new HashSet<>();
services.addAll(this.deploymentProperties.getServices());
services.addAll(StringUtils.commaDelimitedListToSet(request.getDeploymentProperties().get(SERVICES_PROPERTY_KEY)));
return services;
}
int diskQuota(AppDeploymentRequest request) {
String withUnit = request.getDeploymentProperties()
.getOrDefault(AppDeployer.DISK_PROPERTY_KEY, this.deploymentProperties.getDisk());
return (int) ByteSizeUtils.parseToMebibytes(withUnit);
}
String buildpack(AppDeploymentRequest request) {
return Optional.ofNullable(request.getDeploymentProperties().get(BUILDPACK_PROPERTY_KEY))
.orElse(this.deploymentProperties.getBuildpack());
}
String javaOpts(AppDeploymentRequest request) {
return Optional.ofNullable(request.getDeploymentProperties().get(JAVA_OPTS_PROPERTY_KEY))
.orElse(this.deploymentProperties.getJavaOpts());
}
Predicate<Throwable> isNotFoundError() {
return t -> t instanceof AbstractCloudFoundryException && ((AbstractCloudFoundryException) t).getStatusCode() == HttpStatus.NOT_FOUND.value();
}
/**
* Return a Docker image identifier if the application Resource is for a Docker image, or {@literal null} otherwise.
*
* @see #getApplication(AppDeploymentRequest)
*/
String getDockerImage(AppDeploymentRequest request) {
try {
String uri = request.getResource().getURI().toString();
if (uri.startsWith("docker:")) {
return uri.substring("docker:".length());
} else {
return null;
}
} catch (IOException e) {
throw Exceptions.propagate(e);
}
}
/**
* Return a Path to the application Resource or {@literal null} if the request is for a Docker image.
*
* @see #getDockerImage(AppDeploymentRequest)
*/
Path getApplication(AppDeploymentRequest request) {
try {
if (!request.getResource().getURI().toString().startsWith("docker:")) {
return request.getResource().getFile().toPath();
} else {
return null;
}
} catch (IOException e) {
throw Exceptions.propagate(e);
}
}
/**
* Return a function usable in {@literal doOnError} constructs that will unwrap unrecognized Cloud Foundry Exceptions
* and log the text payload.
*/
protected Consumer<Throwable> logError(String msg) {
return e -> {
if (e instanceof UnknownCloudFoundryException) {
logger.error(msg + "\nUnknownCloudFoundryException encountered, whose payload follows:\n" + ((UnknownCloudFoundryException)e).getPayload(), e);
} else {
logger.error(msg, e);
}
};
}
/**
* To be used in order to retry the status operation for an application or task.
* @param id The application id or the task id
* @param <T> The type of status object being queried for, usually AppStatus or TaskStatus
* @return The function that executes the retry logic around for determining App or Task Status
*/
<T> Function<Mono<T>, Mono<T>> statusRetry(String id) {
long statusTimeout = this.deploymentProperties.getStatusTimeout();
long requestTimeout = Math.round(statusTimeout * 0.40); // wait 500ms with default status timeout of 2000ms
long initialRetryDelay = Math.round(statusTimeout * 0.10); // wait 200ms with status timeout of 2000ms
if (requestTimeout < 500L) {
logger.info("Computed statusRetry Request timeout = {} ms is below 500ms minimum value. Setting to 500ms", requestTimeout);
requestTimeout = 500L;
}
final long requestTimeoutToUse = requestTimeout;
return m -> m.timeout(Duration.ofMillis(requestTimeoutToUse))
.doOnError(e -> logger.warn(String.format("Error getting status for %s within %sms, Retrying operation.", id, requestTimeoutToUse)))
.retryWhen(DelayUtils.exponentialBackOffError(
Duration.ofMillis(initialRetryDelay), //initial retry delay
Duration.ofMillis(statusTimeout / 2), // max retry delay
Duration.ofMillis(statusTimeout)) // max total retry time
.andThen(retries -> Flux.from(retries).doOnComplete(() ->
logger.info("Successfully retried getStatus operation status [{}] for {}", id))))
.doOnError(e -> logger.error(String.format("Retry operation on getStatus failed for %s. Max retry time %sms", id, statusTimeout)));
}
public RuntimeEnvironmentInfo environmentInfo() {
return runtimeEnvironmentInfo;
}
}

View File

@@ -16,87 +16,84 @@
package org.springframework.cloud.appbroker.deployer.cloudfoundry;
import java.io.IOException;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Collections;
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.Predicate;
import java.util.stream.Collectors;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.cloudfoundry.AbstractCloudFoundryException;
import org.cloudfoundry.UnknownCloudFoundryException;
import org.cloudfoundry.operations.CloudFoundryOperations;
import org.cloudfoundry.operations.applications.ApplicationDetail;
import org.cloudfoundry.operations.applications.ApplicationHealthCheck;
import org.cloudfoundry.operations.applications.ApplicationManifest;
import org.cloudfoundry.operations.applications.ApplicationSummary;
import org.cloudfoundry.operations.applications.DeleteApplicationRequest;
import org.cloudfoundry.operations.applications.Docker;
import org.cloudfoundry.operations.applications.GetApplicationRequest;
import org.cloudfoundry.operations.applications.InstanceDetail;
import org.cloudfoundry.operations.applications.PushApplicationManifestRequest;
import org.cloudfoundry.operations.applications.Route;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.Yaml;
import reactor.core.publisher.Flux;
import org.springframework.cloud.appbroker.deployer.util.ByteSizeUtils;
import org.springframework.http.HttpStatus;
import reactor.core.Exceptions;
import reactor.core.publisher.Mono;
import org.springframework.cloud.appbroker.deployer.AppDeployer;
import org.springframework.cloud.appbroker.deployer.DeployApplicationRequest;
import org.springframework.cloud.appbroker.deployer.DeployApplicationResponse;
import org.springframework.cloud.appbroker.deployer.GetApplicationStatusRequest;
import org.springframework.cloud.appbroker.deployer.GetApplicationStatusResponse;
import org.springframework.cloud.appbroker.deployer.UndeployApplicationRequest;
import org.springframework.cloud.appbroker.deployer.UndeployApplicationResponse;
import org.springframework.cloud.deployer.spi.app.AppStatus;
import org.springframework.cloud.deployer.spi.app.DeploymentState;
import org.springframework.cloud.deployer.spi.cloudfoundry.AppNameGenerator;
import org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryAppInstanceStatus;
import org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryDeploymentProperties;
import org.springframework.cloud.deployer.spi.core.AppDefinition;
import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest;
import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.StringUtils;
import static org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryDeploymentProperties.DOMAIN_PROPERTY;
import static org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryDeploymentProperties.HEALTHCHECK_HTTP_ENDPOINT_PROPERTY_KEY;
import static org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryDeploymentProperties.HEALTHCHECK_PROPERTY_KEY;
import static org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryDeploymentProperties.HEALTHCHECK_TIMEOUT_PROPERTY_KEY;
import static org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryDeploymentProperties.HOST_PROPERTY;
import static org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryDeploymentProperties.NO_ROUTE_PROPERTY;
import static org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryDeploymentProperties.ROUTES_PROPERTY;
import static org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryDeploymentProperties.ROUTE_PATH_PROPERTY;
import static org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryDeploymentProperties.ROUTE_PROPERTY;
import static org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryDeploymentProperties.USE_SPRING_APPLICATION_JSON_KEY;
import static org.springframework.cloud.appbroker.deployer.DeploymentProperties.COUNT_PROPERTY_KEY;
import static org.springframework.cloud.appbroker.deployer.DeploymentProperties.DISK_PROPERTY_KEY;
import static org.springframework.cloud.appbroker.deployer.DeploymentProperties.GROUP_PROPERTY_KEY;
import static org.springframework.cloud.appbroker.deployer.DeploymentProperties.MEMORY_PROPERTY_KEY;
import static org.springframework.cloud.appbroker.deployer.cloudfoundry.CloudFoundryDeploymentProperties.BUILDPACK_PROPERTY_KEY;
import static org.springframework.cloud.appbroker.deployer.cloudfoundry.CloudFoundryDeploymentProperties.DOMAIN_PROPERTY;
import static org.springframework.cloud.appbroker.deployer.cloudfoundry.CloudFoundryDeploymentProperties.HEALTHCHECK_HTTP_ENDPOINT_PROPERTY_KEY;
import static org.springframework.cloud.appbroker.deployer.cloudfoundry.CloudFoundryDeploymentProperties.HEALTHCHECK_PROPERTY_KEY;
import static org.springframework.cloud.appbroker.deployer.cloudfoundry.CloudFoundryDeploymentProperties.HEALTHCHECK_TIMEOUT_PROPERTY_KEY;
import static org.springframework.cloud.appbroker.deployer.cloudfoundry.CloudFoundryDeploymentProperties.HOST_PROPERTY;
import static org.springframework.cloud.appbroker.deployer.cloudfoundry.CloudFoundryDeploymentProperties.JAVA_OPTS_PROPERTY_KEY;
import static org.springframework.cloud.appbroker.deployer.cloudfoundry.CloudFoundryDeploymentProperties.NO_ROUTE_PROPERTY;
import static org.springframework.cloud.appbroker.deployer.cloudfoundry.CloudFoundryDeploymentProperties.ROUTES_PROPERTY;
import static org.springframework.cloud.appbroker.deployer.cloudfoundry.CloudFoundryDeploymentProperties.ROUTE_PATH_PROPERTY;
import static org.springframework.cloud.appbroker.deployer.cloudfoundry.CloudFoundryDeploymentProperties.ROUTE_PROPERTY;
import static org.springframework.cloud.appbroker.deployer.cloudfoundry.CloudFoundryDeploymentProperties.USE_SPRING_APPLICATION_JSON_KEY;
public class CloudFoundryAppDeployer extends AbstractCloudFoundryAppDeployer implements AppDeployer, ResourceLoaderAware {
public class CloudFoundryAppDeployer implements AppDeployer, ResourceLoaderAware {
private static final Logger logger = LoggerFactory.getLogger(CloudFoundryAppDeployer.class);
private static final String SPRING_CLOUD_DEPLOYER_CLOUDFOUNDRY_SERVICES_KEY = "spring.cloud.deployer.cloudfoundry.services";
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final AppNameGenerator applicationNameGenerator;
private final CloudFoundryDeploymentProperties defaultDeploymentProperties;
private final CloudFoundryOperations operations;
private ResourceLoader resourceLoader;
public CloudFoundryAppDeployer(AppNameGenerator applicationNameGenerator,
CloudFoundryDeploymentProperties deploymentProperties,
public CloudFoundryAppDeployer(CloudFoundryDeploymentProperties deploymentProperties,
CloudFoundryOperations operations,
RuntimeEnvironmentInfo runtimeEnvironmentInfo,
ResourceLoader resourceLoader) {
super(deploymentProperties, runtimeEnvironmentInfo);
this.defaultDeploymentProperties = deploymentProperties;
this.operations = operations;
this.applicationNameGenerator = applicationNameGenerator;
this.resourceLoader = resourceLoader;
}
@@ -106,180 +103,147 @@ public class CloudFoundryAppDeployer extends AbstractCloudFoundryAppDeployer imp
}
@Override
public Mono<DeployApplicationResponse> deploy(DeployApplicationRequest deployApplicationRequest) {
AppDeploymentRequest request = createAppDeploymentRequest(deployApplicationRequest);
public Mono<DeployApplicationResponse> deploy(DeployApplicationRequest request) {
String appName = request.getName();
Resource appResource = getAppResource(request);
Map<String, String> deploymentProperties = request.getProperties();
logger.trace("Entered deploy: Deploying AppDeploymentRequest: AppDefinition = {}, Resource = {}, Deployment Properties = {}",
request.getDefinition(), request.getResource(), request.getDeploymentProperties());
String deploymentId = deploymentId(request);
logger.trace("Deploying application: request={}, resource={}",
appName, appResource);
logger.trace("deploy: Pushing application");
return pushApplication(deploymentId, request)
.timeout(Duration.ofSeconds(this.deploymentProperties.getApiTimeout()))
.doOnSuccess(item -> logger.info("Successfully deployed {}", deploymentId))
return pushApplication(request, deploymentProperties, appResource)
.timeout(Duration.ofSeconds(this.defaultDeploymentProperties.getApiTimeout()))
.doOnSuccess(item -> logger.info("Successfully deployed {}", appName))
.doOnError(error -> {
if (isNotFoundError().test(error)) {
logger.warn("Unable to deploy application. It may have been destroyed before start completed: " + error.getMessage());
}
else {
logError(String.format("Failed to deploy %s", deploymentId)).accept(error);
logError(String.format("Failed to deploy %s", appName)).accept(error);
}
})
.thenReturn(DeployApplicationResponse.builder()
.name(deploymentId)
.name(appName)
.build());
}
private Mono<Void> pushApplication(String deploymentId, AppDeploymentRequest request) {
ApplicationManifest manifest = buildAppManifest(deploymentId, request);
private Mono<Void> pushApplication(DeployApplicationRequest request,
Map<String, String> deploymentProperties,
Resource appResource) {
ApplicationManifest manifest = buildAppManifest(request, deploymentProperties, appResource);
logger.debug("Pushing manifest" + manifest.toString());
return requestPushApplication(
PushApplicationManifestRequest.builder()
.manifest(manifest)
.stagingTimeout(this.deploymentProperties.getStagingTimeout())
.startupTimeout(this.deploymentProperties.getStartupTimeout())
.stagingTimeout(this.defaultDeploymentProperties.getStagingTimeout())
.startupTimeout(this.defaultDeploymentProperties.getStartupTimeout())
.build())
.doOnSuccess(v -> logger.info("Done uploading bits for {}", deploymentId))
.doOnError(e -> logger.error(String.format("Error creating app %s. Exception Message %s", deploymentId, e.getMessage())));
.doOnSuccess(v -> logger.info("Done uploading bits for {}", request.getName()))
.doOnError(e -> logger.error(String.format("Error creating app %s. Exception Message %s",
request.getName(), e.getMessage())));
}
private ApplicationManifest buildAppManifest(String deploymentId, AppDeploymentRequest request) {
private ApplicationManifest buildAppManifest(DeployApplicationRequest request,
Map<String, String> deploymentProperties,
Resource appResource) {
ApplicationManifest.Builder manifest = ApplicationManifest.builder()
.path(getApplication(request)) // Only one of the two is non-null
.disk(diskQuota(request))
.environmentVariables(getEnvironmentVariables(deploymentId, request))
.healthCheckType(healthCheck(request))
.healthCheckHttpEndpoint(healthCheckEndpoint(request))
.timeout(healthCheckTimeout(request))
.instances(instances(request))
.memory(memory(request))
.name(deploymentId)
.noRoute(toggleNoRoute(request))
.services(servicesToBind(request));
.name(request.getName())
.path(getApplication(appResource))
.environmentVariables(getEnvironmentVariables(request.getEnvironment()))
.services(servicesToBind(request.getServices()))
.disk(diskQuota(deploymentProperties))
.healthCheckType(healthCheck(deploymentProperties))
.healthCheckHttpEndpoint(healthCheckEndpoint(deploymentProperties))
.timeout(healthCheckTimeout(deploymentProperties))
.instances(instances(deploymentProperties))
.memory(memory(deploymentProperties))
.noRoute(toggleNoRoute(deploymentProperties));
Optional.ofNullable(host(request)).ifPresent(manifest::host);
Optional.ofNullable(domain(request)).ifPresent(manifest::domain);
Optional.ofNullable(routePath(request)).ifPresent(manifest::routePath);
Optional.ofNullable(host(deploymentProperties)).ifPresent(manifest::host);
Optional.ofNullable(domain(deploymentProperties)).ifPresent(manifest::domain);
Optional.ofNullable(routePath(deploymentProperties)).ifPresent(manifest::routePath);
if (route(request) != null) {
manifest.route(Route.builder().route(route(request)).build());
if (route(deploymentProperties) != null) {
manifest.route(Route.builder().route(route(deploymentProperties)).build());
}
if (!routes(request).isEmpty()) {
Set<Route> routes = routes(request).stream()
if (!routes(deploymentProperties).isEmpty()) {
Set<Route> routes = routes(deploymentProperties).stream()
.map(r -> Route.builder().route(r).build())
.collect(Collectors.toSet());
manifest.routes(routes);
}
if (getDockerImage(request) != null) {
manifest.docker(Docker.builder().image(getDockerImage(request)).build());
if (getDockerImage(appResource) != null) {
manifest.docker(Docker.builder().image(getDockerImage(appResource)).build());
} else {
manifest.buildpack(buildpack(request));
manifest.buildpack(buildpack(deploymentProperties));
}
return manifest.build();
}
private DeploymentState mapShallowAppState(ApplicationSummary applicationSummary) {
if (applicationSummary.getRunningInstances().equals(applicationSummary.getInstances())) {
return DeploymentState.deployed;
}
else if (applicationSummary.getInstances() > 0) {
return DeploymentState.partial;
} else {
return DeploymentState.undeployed;
}
}
@Override
public Mono<GetApplicationStatusResponse> status(GetApplicationStatusRequest request) {
return getStatus(request.getName())
.doOnSuccess(v -> logger.info("Successfully computed status [{}] for {}", v, request.getName()))
.doOnError(logError(String.format("Failed to compute status for %s", request.getName())))
.timeout(Duration.ofMillis(this.deploymentProperties.getStatusTimeout()))
.onErrorResume(t -> {
logger.error("Caught exception while querying for status of {}", request.getName(), t);
return Mono.just(createErrorAppStatus(request.getName()));
})
.flatMap(status -> Mono.just(GetApplicationStatusResponse.builder()
.deploymentId(status.getDeploymentId())
.status(status.toString())
.build()));
private Mono<Void> requestPushApplication(PushApplicationManifestRequest request) {
return this.operations.applications()
.pushManifest(request);
}
@Override
public Mono<UndeployApplicationResponse> undeploy(UndeployApplicationRequest request) {
return getStatus(request.getName())
.doOnNext(status -> assertApplicationExists(request.getName(), status))
.timeout(Duration.ofSeconds(this.deploymentProperties.getApiTimeout()))
.then(requestDeleteApplication(request.getName())
.timeout(Duration.ofSeconds(this.deploymentProperties.getApiTimeout()))
.doOnSuccess(v -> logger.info("Successfully undeployed app {}", request.getName()))
.doOnError(logError(String.format("Failed to undeploy app %s", request.getName()))))
.then(getStatus(request.getName()))
//TODO: do we wait for deletion here? maybe we don't need status.
.flatMap(status -> Mono.just(UndeployApplicationResponse.builder()
.status(status.getState().toString())
.build()));
String appName = request.getName();
logger.trace("Undeploying application: request={}", request);
return requestGetApplication(appName)
.timeout(Duration.ofSeconds(this.defaultDeploymentProperties.getApiTimeout()))
.then(requestDeleteApplication(appName)
.timeout(Duration.ofSeconds(this.defaultDeploymentProperties.getApiTimeout()))
.doOnSuccess(v -> logger.info("Successfully undeployed app {}", appName))
.doOnError(logError(String.format("Failed to undeploy app %s", appName)))
.then(Mono.just(UndeployApplicationResponse.builder()
.name(appName)
.build())));
}
private void assertApplicationExists(String deploymentId, AppStatus status) {
DeploymentState state = status.getState();
if (state == DeploymentState.unknown) {
throw new IllegalStateException(String.format("App %s is not in a deployed state", deploymentId));
}
private Mono<ApplicationDetail> requestGetApplication(String name) {
return this.operations.applications()
.get(GetApplicationRequest.builder()
.name(name)
.build());
}
private AppStatus createAppStatus(ApplicationDetail applicationDetail, String deploymentId) {
logger.trace("Gathering instances for " + applicationDetail);
logger.trace("InstanceDetails: " + applicationDetail.getInstanceDetails());
private Mono<Void> requestDeleteApplication(String name) {
return this.operations.applications()
.delete(DeleteApplicationRequest.builder()
.deleteRoutes(defaultDeploymentProperties.isDeleteRoutes())
.name(name)
.build());
}
AppStatus.Builder builder = AppStatus.of(deploymentId);
private Map<String, String> getEnvironmentVariables(Map<String, String> environment) {
Map<String, String> envVariables = new HashMap<>(getApplicationEnvironment(environment));
int i = 0;
for (InstanceDetail instanceDetail : applicationDetail.getInstanceDetails()) {
builder.with(new CloudFoundryAppInstanceStatus(applicationDetail, instanceDetail, i++));
}
for (; i < applicationDetail.getInstances(); i++) {
builder.with(new CloudFoundryAppInstanceStatus(applicationDetail, null, i));
String javaOpts = javaOpts(environment);
if (StringUtils.hasText(javaOpts)) {
envVariables.put("JAVA_OPTS", javaOpts(environment));
}
return builder.build();
String group = environment.get(GROUP_PROPERTY_KEY);
if (StringUtils.hasText(group)) {
envVariables.put("SPRING_CLOUD_APPLICATION_GROUP", group);
}
envVariables.put("SPRING_CLOUD_APPLICATION_GUID", "${vcap.application.name}:${vcap.application.instance_index}");
envVariables.put("SPRING_APPLICATION_INDEX", "${vcap.application.instance_index}");
return envVariables;
}
private AppStatus createEmptyAppStatus(String deploymentId) {
return AppStatus.of(deploymentId)
.build();
}
private Map<String, String> getApplicationEnvironment(Map<String, String> environment) {
Map<String, String> applicationProperties = getSanitizedApplicationEnvironment(environment);
private AppStatus createErrorAppStatus(String deploymentId) {
return AppStatus.of(deploymentId)
.generalState(DeploymentState.error)
.build();
}
private String deploymentId(AppDeploymentRequest request) {
String prefix = Optional.ofNullable(request.getDeploymentProperties().get(org.springframework.cloud.deployer.spi.app.AppDeployer.GROUP_PROPERTY_KEY))
.map(group -> String.format("%s-", group))
.orElse("");
String appName = String.format("%s%s", prefix, request.getDefinition().getName());
return this.applicationNameGenerator.generateAppName(appName);
}
private String domain(AppDeploymentRequest request) {
return Optional.ofNullable(request.getDeploymentProperties().get(DOMAIN_PROPERTY))
.orElse(this.deploymentProperties.getDomain());
}
private Map<String, String> getApplicationProperties(String deploymentId, AppDeploymentRequest request) {
Map<String, String> applicationProperties = getSanitizedApplicationProperties(deploymentId, request);
if (!useSpringApplicationJson(request)) {
if (!useSpringApplicationJson(environment)) {
return applicationProperties;
}
@@ -290,127 +254,40 @@ public class CloudFoundryAppDeployer extends AbstractCloudFoundryAppDeployer imp
}
}
private Map<String, String> getCommandLineArguments(AppDeploymentRequest request) {
if (request.getCommandlineArguments().isEmpty()) {
return Collections.emptyMap();
}
String argumentsAsString = request.getCommandlineArguments().stream()
.collect(Collectors.joining(" "));
String yaml = new Yaml().dump(Collections.singletonMap("arguments", argumentsAsString));
return Collections.singletonMap("JBP_CONFIG_JAVA_MAIN", yaml);
}
private Map<String, String> getEnvironmentVariables(String deploymentId, AppDeploymentRequest request) {
Map<String, String> envVariables = new HashMap<>();
envVariables.putAll(getApplicationProperties(deploymentId, request));
envVariables.putAll(getCommandLineArguments(request));
String javaOpts = javaOpts(request);
if (StringUtils.hasText(javaOpts)) {
envVariables.put("JAVA_OPTS", javaOpts(request));
}
String group = request.getDeploymentProperties().get(org.springframework.cloud.deployer.spi.app.AppDeployer.GROUP_PROPERTY_KEY);
if (StringUtils.hasText(group)) {
envVariables.put("SPRING_CLOUD_APPLICATION_GROUP", group);
}
envVariables.put("SPRING_CLOUD_APPLICATION_GUID", "${vcap.application.name}:${vcap.application.instance_index}");
envVariables.put("SPRING_APPLICATION_INDEX", "${vcap.application.instance_index}");
return envVariables;
}
private Map<String, String> getSanitizedApplicationProperties(String deploymentId, AppDeploymentRequest request) {
Map<String, String> applicationProperties = new HashMap<>(request.getDefinition().getProperties());
private Map<String, String> getSanitizedApplicationEnvironment(Map<String, String> environment) {
Map<String, String> applicationProperties = new HashMap<>(environment);
// Remove server.port as CF assigns a port for us, and we don't want to override that
Optional.ofNullable(applicationProperties.remove("server.port"))
.ifPresent(port -> logger.warn("Ignoring 'server.port={}' for app {}, as Cloud Foundry will assign a local dynamic port. Route to the app will use port 80.", port, deploymentId));
.ifPresent(port -> logger.warn("Ignoring 'server.port={}', " +
"as Cloud Foundry will assign a local dynamic port. " +
"Route to the app will use port 80.", port));
return applicationProperties;
}
private Mono<AppStatus> getStatus(String deploymentId) {
return requestGetApplication(deploymentId)
.map(applicationDetail -> createAppStatus(applicationDetail, deploymentId))
.onErrorResume(IllegalArgumentException.class, t -> {
logger.debug("Application for {} does not exist.", deploymentId);
return Mono.just(createEmptyAppStatus(deploymentId));
})
.transform(statusRetry(deploymentId))
.onErrorReturn(createErrorAppStatus(deploymentId));
private boolean useSpringApplicationJson(Map<String, String> environment) {
return Optional.ofNullable(environment.get(USE_SPRING_APPLICATION_JSON_KEY))
.map(Boolean::valueOf)
.orElse(this.defaultDeploymentProperties.isUseSpringApplicationJson());
}
private ApplicationHealthCheck healthCheck(AppDeploymentRequest request) {
return Optional.ofNullable(request.getDeploymentProperties().get(HEALTHCHECK_PROPERTY_KEY))
private Set<String> servicesToBind(List<String> services) {
Set<String> allServices = new HashSet<>();
allServices.addAll(this.defaultDeploymentProperties.getServices());
allServices.addAll(services);
return allServices;
}
private String domain(Map<String, String> properties) {
return Optional.ofNullable(properties.get(DOMAIN_PROPERTY))
.orElse(this.defaultDeploymentProperties.getDomain());
}
private ApplicationHealthCheck healthCheck(Map<String, String> properties) {
return Optional.ofNullable(properties.get(HEALTHCHECK_PROPERTY_KEY))
.map(this::toApplicationHealthCheck)
.orElse(this.deploymentProperties.getHealthCheck());
}
private String healthCheckEndpoint(AppDeploymentRequest request) {
return Optional.ofNullable(request.getDeploymentProperties().get(HEALTHCHECK_HTTP_ENDPOINT_PROPERTY_KEY))
.orElse(this.deploymentProperties.getHealthCheckHttpEndpoint());
}
private Integer healthCheckTimeout(AppDeploymentRequest request) {
String timeoutString = request.getDeploymentProperties()
.getOrDefault(HEALTHCHECK_TIMEOUT_PROPERTY_KEY, this.deploymentProperties.getHealthCheckTimeout());
return Integer.parseInt(timeoutString);
}
private String host(AppDeploymentRequest request) {
return Optional.ofNullable(request.getDeploymentProperties().get(HOST_PROPERTY))
.orElse(this.deploymentProperties.getHost());
}
private int instances(AppDeploymentRequest request) {
return Optional.ofNullable(request.getDeploymentProperties().get(org.springframework.cloud.deployer.spi.app.AppDeployer.COUNT_PROPERTY_KEY))
.map(Integer::parseInt)
.orElse(this.deploymentProperties.getInstances());
}
private Mono<Void> requestDeleteApplication(String id) {
return this.operations.applications()
.delete(DeleteApplicationRequest.builder()
.deleteRoutes(deploymentProperties.isDeleteRoutes())
.name(id)
.build());
}
private Mono<ApplicationDetail> requestGetApplication(String id) {
return this.operations.applications()
.get(GetApplicationRequest.builder()
.name(id)
.build());
}
private Mono<Void> requestPushApplication(PushApplicationManifestRequest request) {
return this.operations.applications()
.pushManifest(request);
}
private Flux<ApplicationSummary> requestSummary() {
return this.operations.applications().list();
}
private String routePath(AppDeploymentRequest request) {
String routePath = request.getDeploymentProperties().get(ROUTE_PATH_PROPERTY);
if (StringUtils.hasText(routePath) && !routePath.startsWith("/")) {
throw new IllegalArgumentException(
"Cloud Foundry routes must start with \"/\". Route passed = [" + routePath + "].");
}
return routePath;
}
private String route(AppDeploymentRequest request) {
return request.getDeploymentProperties().get(ROUTE_PROPERTY);
}
private Set<String> routes(AppDeploymentRequest request) {
Set<String> routes = new HashSet<>();
routes.addAll(this.deploymentProperties.getRoutes());
routes.addAll(StringUtils.commaDelimitedListToSet(request.getDeploymentProperties().get(ROUTES_PROPERTY)));
return routes;
.orElse(this.defaultDeploymentProperties.getHealthCheck());
}
private ApplicationHealthCheck toApplicationHealthCheck(String raw) {
@@ -418,41 +295,136 @@ public class CloudFoundryAppDeployer extends AbstractCloudFoundryAppDeployer imp
return ApplicationHealthCheck.from(raw);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(String.format("Unsupported health-check value '%s'. Available values are %s", raw,
StringUtils.arrayToCommaDelimitedString(ApplicationHealthCheck.values())), e);
StringUtils.arrayToCommaDelimitedString(ApplicationHealthCheck.values())), e);
}
}
private Boolean toggleNoRoute(AppDeploymentRequest request) {
return Optional.ofNullable(request.getDeploymentProperties().get(NO_ROUTE_PROPERTY))
private String healthCheckEndpoint(Map<String, String> properties) {
return Optional.ofNullable(properties.get(HEALTHCHECK_HTTP_ENDPOINT_PROPERTY_KEY))
.orElse(this.defaultDeploymentProperties.getHealthCheckHttpEndpoint());
}
private Integer healthCheckTimeout(Map<String, String> properties) {
String timeoutString = properties
.getOrDefault(HEALTHCHECK_TIMEOUT_PROPERTY_KEY, this.defaultDeploymentProperties.getHealthCheckTimeout());
return Integer.parseInt(timeoutString);
}
private int instances(Map<String, String> properties) {
return Optional.ofNullable(properties.get(COUNT_PROPERTY_KEY))
.map(Integer::parseInt)
.orElse(this.defaultDeploymentProperties.getInstances());
}
private String host(Map<String, String> properties) {
return Optional.ofNullable(properties.get(HOST_PROPERTY))
.orElse(this.defaultDeploymentProperties.getHost());
}
private String routePath(Map<String, String> properties) {
String routePath = properties.get(ROUTE_PATH_PROPERTY);
if (StringUtils.hasText(routePath) && !routePath.startsWith("/")) {
throw new IllegalArgumentException(
"Cloud Foundry routes must start with \"/\". Route passed = [" + routePath + "].");
}
return routePath;
}
private String route(Map<String, String> properties) {
return properties.get(ROUTE_PROPERTY);
}
private Set<String> routes(Map<String, String> properties) {
Set<String> routes = new HashSet<>();
routes.addAll(this.defaultDeploymentProperties.getRoutes());
routes.addAll(StringUtils.commaDelimitedListToSet(properties.get(ROUTES_PROPERTY)));
return routes;
}
private Boolean toggleNoRoute(Map<String, String> properties) {
return Optional.ofNullable(properties.get(NO_ROUTE_PROPERTY))
.map(Boolean::valueOf)
.orElse(null);
}
private boolean useSpringApplicationJson(AppDeploymentRequest request) {
return Optional.ofNullable(request.getDeploymentProperties().get(USE_SPRING_APPLICATION_JSON_KEY))
.map(Boolean::valueOf)
.orElse(this.deploymentProperties.isUseSpringApplicationJson());
private int memory(Map<String, String> properties) {
String withUnit = properties
.getOrDefault(MEMORY_PROPERTY_KEY, this.defaultDeploymentProperties.getMemory());
return (int) ByteSizeUtils.parseToMebibytes(withUnit);
}
private AppDeploymentRequest createAppDeploymentRequest(DeployApplicationRequest request) {
AppDefinition appDefinition = new AppDefinition(request.getName(), request.getEnvironment());
Resource resource = resourceLoader.getResource(request.getPath());
Map<String, String> deploymentProperties = createDeploymentProperties(request);
return new AppDeploymentRequest(appDefinition, resource, deploymentProperties);
private int diskQuota(Map<String, String> properties) {
String withUnit = properties
.getOrDefault(DISK_PROPERTY_KEY, this.defaultDeploymentProperties.getDisk());
return (int) ByteSizeUtils.parseToMebibytes(withUnit);
}
private Map<String, String> createDeploymentProperties(DeployApplicationRequest request) {
Map<String, String> deploymentProperties = new HashMap<>();
if (request.getProperties() != null) {
deploymentProperties.putAll(request.getProperties());
private String buildpack(Map<String, String> properties) {
return Optional.ofNullable(properties.get(BUILDPACK_PROPERTY_KEY))
.orElse(this.defaultDeploymentProperties.getBuildpack());
}
private String javaOpts(Map<String, String> properties) {
return Optional.ofNullable(properties.get(JAVA_OPTS_PROPERTY_KEY))
.orElse(this.defaultDeploymentProperties.getJavaOpts());
}
private Predicate<Throwable> isNotFoundError() {
return t -> t instanceof AbstractCloudFoundryException && ((AbstractCloudFoundryException) t).getStatusCode() == HttpStatus.NOT_FOUND.value();
}
/**
* Return a Docker image identifier if the application Resource is for a Docker image, or {@literal null} otherwise.
*
* @see #getApplication(Resource)
*/
private String getDockerImage(Resource resource) {
try {
String uri = resource.getURI().toString();
if (uri.startsWith("docker:")) {
return uri.substring("docker:".length());
} else {
return null;
}
} catch (IOException e) {
throw Exceptions.propagate(e);
}
if (request.getServices() != null) {
Map<String, String> services = Collections.singletonMap(SPRING_CLOUD_DEPLOYER_CLOUDFOUNDRY_SERVICES_KEY,
StringUtils.collectionToCommaDelimitedString(request.getServices()));
deploymentProperties.putAll(services);
}
return deploymentProperties;
}
private Resource getAppResource(DeployApplicationRequest request) {
return resourceLoader.getResource(request.getPath());
}
/**
* Return a Path to the application Resource or {@literal null} if the request is for a Docker image.
*
* @see #getDockerImage(Resource)
* @param resource the resource representing the app bits
*/
private Path getApplication(Resource resource) {
try {
if (!resource.getURI().toString().startsWith("docker:")) {
return resource.getFile().toPath();
} else {
return null;
}
} catch (IOException e) {
throw Exceptions.propagate(e);
}
}
/**
* Return a function usable in {@literal doOnError} constructs that will unwrap unrecognized Cloud Foundry Exceptions
* and log the text payload.
*/
private Consumer<Throwable> logError(String msg) {
return e -> {
if (e instanceof UnknownCloudFoundryException) {
logger.error(msg + "\nUnknownCloudFoundryException encountered, whose payload follows:\n"
+ ((UnknownCloudFoundryException)e).getPayload(), e);
} else {
logger.error(msg, e);
}
};
}
}

View File

@@ -0,0 +1,320 @@
/*
* 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.cloudfoundry;
import org.cloudfoundry.operations.applications.ApplicationHealthCheck;
import org.springframework.beans.factory.annotation.Value;
import java.time.Duration;
import java.util.HashSet;
import java.util.Set;
/**
* Holds configuration properties for specifying what resources and services an app deployed to a Cloud Foundry runtime
* will get.
*
* @author Eric Bottard
* @author Greg Turnquist
* @author Ilayaperumal Gopinathan
*/
public class CloudFoundryDeploymentProperties {
public static final String CLOUDFOUNDRY_PROPERTIES = "spring.cloud.deployer.cloudfoundry";
public static final String HEALTHCHECK_PROPERTY_KEY = CLOUDFOUNDRY_PROPERTIES + ".health-check";
public static final String HEALTHCHECK_HTTP_ENDPOINT_PROPERTY_KEY = CLOUDFOUNDRY_PROPERTIES + ".health-check-http-endpoint";
public static final String HEALTHCHECK_TIMEOUT_PROPERTY_KEY = CLOUDFOUNDRY_PROPERTIES + ".health-check-timeout";
public static final String ROUTE_PATH_PROPERTY = CLOUDFOUNDRY_PROPERTIES + ".route-path";
public static final String ROUTE_PROPERTY = CLOUDFOUNDRY_PROPERTIES + ".route";
public static final String ROUTES_PROPERTY = CLOUDFOUNDRY_PROPERTIES + ".routes";
public static final String NO_ROUTE_PROPERTY = CLOUDFOUNDRY_PROPERTIES + ".no-route";
public static final String HOST_PROPERTY = CLOUDFOUNDRY_PROPERTIES + ".host";
public static final String DOMAIN_PROPERTY = CLOUDFOUNDRY_PROPERTIES + ".domain";
public static final String BUILDPACK_PROPERTY_KEY = CLOUDFOUNDRY_PROPERTIES + ".buildpack";
public static final String JAVA_OPTS_PROPERTY_KEY = CLOUDFOUNDRY_PROPERTIES + ".javaOpts";
public static final String USE_SPRING_APPLICATION_JSON_KEY = CLOUDFOUNDRY_PROPERTIES + ".use-spring-application-json";
/**
* The names of services to bind to all applications deployed as a module.
* This should typically contain a service capable of playing the role of a binding transport.
*/
private Set<String> services = new HashSet<>();
/**
* The host name to use as part of the route. Defaults to hostname derived by Cloud Foundry.
*/
private String host = null;
/**
* The domain to use when mapping routes for applications.
*/
private String domain;
/**
* The routes that the application should be bound to.
* Mutually exclusive with host and domain.
*/
private Set<String> routes = new HashSet<>();
/**
* The buildpack to use for deploying the application.
*/
private String buildpack = "https://github.com/cloudfoundry/java-buildpack.git#v4.7.1";
/**
* The amount of memory to allocate, if not overridden per-app. Default unit is mebibytes, 'M' and 'G" suffixes supported.
*/
private String memory = "1024m";
/**
* The amount of disk space to allocate, if not overridden per-app. Default unit is mebibytes, 'M' and 'G" suffixes supported.
*/
private String disk = "1024m";
/**
* The type of health check to perform on deployed application, if not overridden per-app. Defaults to PORT
*/
private ApplicationHealthCheck healthCheck = ApplicationHealthCheck.PORT;
/**
* The path that the http health check will use, defaults to @{code /health}
*/
private String healthCheckHttpEndpoint = "/health";
/**
* The timeout value for health checks in seconds. Defaults to 120 seconds.
*/
private String healthCheckTimeout = "120";
/**
* The number of instances to run.
*/
private int instances = 1;
/**
* Flag to enable prefixing the app name with a random prefix.
*/
private boolean enableRandomAppNamePrefix = true;
/**
* Timeout for blocking API calls, in seconds.
*/
private long apiTimeout = 360L;
/**
* Timeout for name API operations in milliseconds
*/
private long statusTimeout = 5_000L;
/**
* Flag to indicate whether application properties are fed into SPRING_APPLICATION_JSON or ENVIRONMENT VARIABLES.
*/
private boolean useSpringApplicationJson = true;
/**
* If set, override the timeout allocated for staging the app by the client.
*/
private Duration stagingTimeout = Duration.ofMinutes(15L);
/**
* If set, override the timeout allocated for starting the app by the client.
*/
private Duration startupTimeout = Duration.ofMinutes(5L);
/**
* String to use as prefix for name of deployed app. Defaults to spring.application.name.
*/
@Value("${spring.application.name:}")
private String appNamePrefix;
/**
* Whether to also delete routes when un-deploying an application.
*/
private boolean deleteRoutes = true;
private String javaOpts;
public Set<String> getServices() {
return services;
}
public void setServices(Set<String> services) {
this.services = services;
}
public String getBuildpack() {
return buildpack;
}
public void setBuildpack(String buildpack) {
this.buildpack = buildpack;
}
public String getMemory() {
return memory;
}
public void setMemory(String memory) {
this.memory = memory;
}
public String getDisk() {
return disk;
}
public void setDisk(String disk) {
this.disk = disk;
}
public int getInstances() {
return instances;
}
public void setInstances(int instances) {
this.instances = instances;
}
public boolean isEnableRandomAppNamePrefix() {
return enableRandomAppNamePrefix;
}
public void setEnableRandomAppNamePrefix(boolean enableRandomAppNamePrefix) {
this.enableRandomAppNamePrefix = enableRandomAppNamePrefix;
}
public String getAppNamePrefix() {
return appNamePrefix;
}
public void setAppNamePrefix(String appNamePrefix) {
this.appNamePrefix = appNamePrefix;
}
public long getApiTimeout() {
return apiTimeout;
}
public void setApiTimeout(long apiTimeout) {
this.apiTimeout = apiTimeout;
}
public boolean isUseSpringApplicationJson() {
return useSpringApplicationJson;
}
public void setUseSpringApplicationJson(boolean useSpringApplicationJson) {
this.useSpringApplicationJson = useSpringApplicationJson;
}
public ApplicationHealthCheck getHealthCheck() {
return healthCheck;
}
public void setHealthCheck(ApplicationHealthCheck healthCheck) {
this.healthCheck = healthCheck;
}
public String getHealthCheckHttpEndpoint() {
return healthCheckHttpEndpoint;
}
public void setHealthCheckHttpEndpoint(String healthCheckHttpEndpoint) {
this.healthCheckHttpEndpoint = healthCheckHttpEndpoint;
}
public String getHealthCheckTimeout() {
return healthCheckTimeout;
}
public void setHealthCheckTimeout(String healthCheckTimeout) {
this.healthCheckTimeout = healthCheckTimeout;
}
public String getDomain() {
return domain;
}
public void setDomain(String domain) {
this.domain = domain;
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public Set<String> getRoutes() {
return routes;
}
public void setRoutes(Set<String> routes) {
this.routes = routes;
}
public Duration getStagingTimeout() {
return stagingTimeout;
}
public void setStagingTimeout(Duration stagingTimeout) {
this.stagingTimeout = stagingTimeout;
}
public Duration getStartupTimeout() {
return startupTimeout;
}
public void setStartupTimeout(Duration startupTimeout) {
this.startupTimeout = startupTimeout;
}
public long getStatusTimeout() {
return statusTimeout;
}
public void setStatusTimeout(long statusTimeout) {
this.statusTimeout = statusTimeout;
}
public boolean isDeleteRoutes() {
return deleteRoutes;
}
public void setDeleteRoutes(boolean deleteRoutes) {
this.deleteRoutes = deleteRoutes;
}
public String getJavaOpts() {
return javaOpts;
}
public void setJavaOpts(String javaOpts) {
this.javaOpts = javaOpts;
}
}

View File

@@ -1,28 +0,0 @@
/*
* 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.cloudfoundry;
import org.springframework.cloud.deployer.spi.cloudfoundry.AppNameGenerator;
public class NoOpAppNameGenerator implements AppNameGenerator {
@Override
public String generateAppName(String appName) {
return appName;
}
}

View File

@@ -33,7 +33,6 @@ import reactor.test.StepVerifier;
import org.springframework.cloud.appbroker.deployer.AppDeployer;
import org.springframework.cloud.appbroker.deployer.DeployApplicationRequest;
import org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryDeploymentProperties;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.ResourceLoader;
@@ -64,7 +63,6 @@ class CloudFoundryAppDeployerTest {
@BeforeEach
void setUp() {
NoOpAppNameGenerator applicationNameGenerator = new NoOpAppNameGenerator();
CloudFoundryDeploymentProperties deploymentProperties = new CloudFoundryDeploymentProperties();
deploymentProperties.setDisk(APP_MEMORY_STRING);
@@ -72,8 +70,8 @@ class CloudFoundryAppDeployerTest {
when(cloudFoundryOperations.applications()).thenReturn(applications);
when(resourceLoader.getResource(APP_PATH)).thenReturn(new FileSystemResource(APP_PATH));
appDeployer = new CloudFoundryAppDeployer(applicationNameGenerator,
deploymentProperties, cloudFoundryOperations, null, resourceLoader);
appDeployer = new CloudFoundryAppDeployer(
deploymentProperties, cloudFoundryOperations, resourceLoader);
}
@Test

View File

@@ -1,33 +0,0 @@
/*
* 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.cloudfoundry;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.deployer.spi.cloudfoundry.AppNameGenerator;
import static org.assertj.core.api.Assertions.assertThat;
class NoOpAppNameGeneratorTest {
private AppNameGenerator appNameGenerator = new NoOpAppNameGenerator();
@Test
void shouldNoOp() {
assertThat(appNameGenerator.generateAppName("app-name")).isEqualTo("app-name");
}
}

View File

@@ -17,5 +17,6 @@
description = "Spring Cloud App Broker Deployer"
dependencies {
api("org.springframework:spring-core:${springVersion}")
api("io.projectreactor:reactor-core:${reactorVersion}")
}

View File

@@ -28,7 +28,4 @@ public interface AppDeployer {
return Mono.empty();
}
default Mono<GetApplicationStatusResponse> status(GetApplicationStatusRequest request) {
return Mono.empty();
}
}

View File

@@ -33,7 +33,8 @@ public class DeployApplicationRequest {
private List<String> services;
DeployApplicationRequest(String name, String path, Map<String, String> properties, Map<String, String> environment, List<String> services) {
private DeployApplicationRequest(String name, String path, Map<String, String> properties,
Map<String, String> environment, List<String> services) {
this.name = name;
this.path = path;
this.properties = properties;

View File

@@ -20,7 +20,7 @@ public class DeployApplicationResponse {
private String name;
DeployApplicationResponse(String name) {
private DeployApplicationResponse(String name) {
this.name = name;
}
@@ -37,7 +37,6 @@ public class DeployApplicationResponse {
private String name;
DeployApplicationResponseBuilder() {
}
public DeployApplicationResponseBuilder name(String name) {

View File

@@ -0,0 +1,92 @@
/*
* 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 org.springframework.cloud.appbroker.deployer.util.ByteSizeUtils;
public interface DeploymentProperties {
/**
* Common prefix used for deployment properties.
*/
String PREFIX = "spring.cloud.deployer.";
/**
* The deployment property for the count (number of app instances).
* If not provided, a deployer should assume 1 instance.
*/
String COUNT_PROPERTY_KEY = PREFIX + "count";
/**
* The deployment property for the group to which an app belongs.
* If not provided, a deployer should assume no group.
*/
String GROUP_PROPERTY_KEY = PREFIX + "group";
/**
* The deployment property that indicates if each app instance should have an index value
* within a sequence from 0 to N-1, where N is the value of the {@value #COUNT_PROPERTY_KEY}
* property. If not provided, a deployer should assume app instance indexing is not necessary.
*/
String INDEXED_PROPERTY_KEY = PREFIX + "indexed";
/**
* The property to be set at each instance level to specify the sequence number
* amongst 0 to N-1, where N is the value of the {@value #COUNT_PROPERTY_KEY} property.
* Specified as CAPITAL_WITH_UNDERSCORES as this is typically passed as an environment
* variable, but when targeting a Spring app, other variations may apply.
*
* @see #INDEXED_PROPERTY_KEY
*/
String INSTANCE_INDEX_PROPERTY_KEY = "INSTANCE_INDEX";
/**
* The deployment property for the memory setting for the container that will run the app.
* The memory is specified in <a href="https://en.wikipedia.org/wiki/Mebibyte">Mebibytes</a>,
* by default, with optional case-insensitive trailing unit 'm' and 'g' being supported,
* for mebi- and giga- respectively.
* <p>
* 1 MiB = 2^20 bytes = 1024*1024 bytes vs. the decimal based 1MB = 10^6 bytes = 1000*1000 bytes,
* <p>
* Implementations are expected to translate this value to the target platform as faithfully as possible.
*
* @see ByteSizeUtils
*/
String MEMORY_PROPERTY_KEY = PREFIX + "memory";
/**
* The deployment property for the disk setting for the container that will run the app.
* The memory is specified in <a href="https://en.wikipedia.org/wiki/Mebibyte">Mebibytes</a>,
* by default, with optional case-insensitive trailing unit 'm' and 'g' being supported,
* for mebi- and giga- respectively.
* <p>
* 1 MiB = 2^20 bytes = 1024*1024 bytes vs. the decimal based 1MB = 10^6 bytes = 1000*1000 bytes,
* <p>
* Implementations are expected to translate this value to the target platform as faithfully as possible.
*
* @see ByteSizeUtils
*/
String DISK_PROPERTY_KEY = PREFIX + "disk";
/**
* The deployment property for the cpu setting for the container that will run the app.
* The cpu is specified as whole multiples or decimal fractions of virtual cores. Some platforms will not
* support setting cpu and will ignore this setting. Other platforms may require whole numbers and might
* round up. Exactly how this property affects the deployments will vary between implementations.
*/
String CPU_PROPERTY_KEY = PREFIX + "cpu";
}

View File

@@ -1,53 +0,0 @@
/*
* 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 GetApplicationStatusRequest {
private String name;
GetApplicationStatusRequest(String name) {
this.name = name;
}
public static GetApplicationStatusRequestBuilder builder() {
return new GetApplicationStatusRequestBuilder();
}
public String getName() {
return name;
}
public static class GetApplicationStatusRequestBuilder {
private String name;
GetApplicationStatusRequestBuilder() {
}
public GetApplicationStatusRequestBuilder name(String name) {
this.name = name;
return this;
}
public GetApplicationStatusRequest build() {
return new GetApplicationStatusRequest(name);
}
}
}

View File

@@ -1,68 +0,0 @@
/*
* 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 GetApplicationStatusResponse {
private String deploymentId;
private String status;
GetApplicationStatusResponse(String deploymentId, String status) {
this.deploymentId = deploymentId;
this.status = status;
}
public static GetApplicationStatusResponseBuilder builder() {
return new GetApplicationStatusResponseBuilder();
}
public String getDeploymentId() {
return deploymentId;
}
public String getStatus() {
return status;
}
public static class GetApplicationStatusResponseBuilder {
private String deploymentId;
private String status;
GetApplicationStatusResponseBuilder() {
}
public GetApplicationStatusResponseBuilder deploymentId(String deploymentId) {
this.deploymentId = deploymentId;
return this;
}
public GetApplicationStatusResponseBuilder status(String status) {
this.status = status;
return this;
}
public GetApplicationStatusResponse build() {
return new GetApplicationStatusResponse(deploymentId, status);
}
}
}

View File

@@ -20,7 +20,7 @@ public class UndeployApplicationRequest {
private String name;
UndeployApplicationRequest(String name) {
private UndeployApplicationRequest(String name) {
this.name = name;
}

View File

@@ -18,35 +18,34 @@ package org.springframework.cloud.appbroker.deployer;
public class UndeployApplicationResponse {
private String status;
private String name;
UndeployApplicationResponse(String status) {
this.status = status;
private UndeployApplicationResponse(String name) {
this.name = name;
}
public static UndeployApplicationResponseBuilder builder() {
return new UndeployApplicationResponseBuilder();
}
public String getStatus() {
return this.status;
public String getName() {
return this.name;
}
public static class UndeployApplicationResponseBuilder {
private String status;
private String name;
UndeployApplicationResponseBuilder() {
}
public UndeployApplicationResponseBuilder status(String status) {
this.status = status;
public UndeployApplicationResponseBuilder name(String name) {
this.name = name;
return this;
}
public UndeployApplicationResponse build() {
return new UndeployApplicationResponse(status);
return new UndeployApplicationResponse(name);
}
}
}

View File

@@ -0,0 +1,51 @@
/*
* 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.util;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Utility class for dealing with parseable byte sizes, such as memory and disk limits.
*
* @author Eric Bottard
*/
public class ByteSizeUtils {
private ByteSizeUtils() {
}
private static final Pattern SIZE_PATTERN = Pattern.compile("(?<amount>\\d+)(?<unit>(m|g)?)", Pattern.CASE_INSENSITIVE);
/**
* Return the number of mebibytes (1024*1024) denoted by the given text, where an optional case-insensitive unit of
* 'm' or 'g' can be used to mean mebi- or gebi- bytes, respectively. Lack of unit assumes mebibytes.
*/
public static long parseToMebibytes(String text) {
Matcher matcher = SIZE_PATTERN.matcher(text);
if (!matcher.matches()) {
throw new IllegalArgumentException(String.format("Could not parse '%s' as a byte size." +
" Expected a number with optional 'm' or 'g' suffix", text));
}
long size = Long.parseLong(matcher.group("amount"));
if (matcher.group("unit").equalsIgnoreCase("g")) {
size *= 1024L;
}
return size;
}
}

View File

@@ -18,9 +18,8 @@ package org.springframework.cloud.appbroker.sample;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryDeployerAutoConfiguration;
@SpringBootApplication(exclude = CloudFoundryDeployerAutoConfiguration.class)
@SpringBootApplication
public class AppBrokerSampleApplication {
public static void main(String[] args) {