Adding zero downtime backing app updates

Finishes #113
This commit is contained in:
Alberto Rios
2018-12-21 10:24:33 +01:00
committed by Scott Frederick
parent 465cda9718
commit 7b73e06522
20 changed files with 923 additions and 25 deletions

View File

@@ -18,6 +18,7 @@ package org.springframework.cloud.appbroker.deployer.cloudfoundry;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.HashMap;
import java.util.HashSet;
@@ -25,6 +26,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@@ -35,12 +37,37 @@ import org.cloudfoundry.UnknownCloudFoundryException;
import org.cloudfoundry.client.CloudFoundryClient;
import org.cloudfoundry.client.v2.spaces.CreateSpaceRequest;
import org.cloudfoundry.client.v2.spaces.DeleteSpaceRequest;
import org.cloudfoundry.client.v3.Relationship;
import org.cloudfoundry.client.v3.ToOneRelationship;
import org.cloudfoundry.client.v3.builds.BuildState;
import org.cloudfoundry.client.v3.builds.CreateBuildRequest;
import org.cloudfoundry.client.v3.builds.CreateBuildResponse;
import org.cloudfoundry.client.v3.builds.GetBuildRequest;
import org.cloudfoundry.client.v3.builds.GetBuildResponse;
import org.cloudfoundry.client.v3.deployments.CreateDeploymentRequest;
import org.cloudfoundry.client.v3.deployments.CreateDeploymentResponse;
import org.cloudfoundry.client.v3.deployments.DeploymentRelationships;
import org.cloudfoundry.client.v3.deployments.DeploymentState;
import org.cloudfoundry.client.v3.deployments.GetDeploymentRequest;
import org.cloudfoundry.client.v3.deployments.GetDeploymentResponse;
import org.cloudfoundry.client.v3.packages.CreatePackageRequest;
import org.cloudfoundry.client.v3.packages.CreatePackageResponse;
import org.cloudfoundry.client.v3.packages.GetPackageRequest;
import org.cloudfoundry.client.v3.packages.GetPackageResponse;
import org.cloudfoundry.client.v3.packages.Package;
import org.cloudfoundry.client.v3.packages.PackageRelationships;
import org.cloudfoundry.client.v3.packages.PackageState;
import org.cloudfoundry.client.v3.packages.PackageType;
import org.cloudfoundry.client.v3.packages.UploadPackageRequest;
import org.cloudfoundry.client.v3.packages.UploadPackageResponse;
import org.cloudfoundry.operations.CloudFoundryOperations;
import org.cloudfoundry.operations.DefaultCloudFoundryOperations;
import org.cloudfoundry.operations.applications.ApplicationDetail;
import org.cloudfoundry.operations.applications.ApplicationHealthCheck;
import org.cloudfoundry.operations.applications.ApplicationManifest;
import org.cloudfoundry.operations.applications.DeleteApplicationRequest;
import org.cloudfoundry.operations.applications.Docker;
import org.cloudfoundry.operations.applications.GetApplicationRequest;
import org.cloudfoundry.operations.applications.PushApplicationManifestRequest;
import org.cloudfoundry.operations.applications.Route;
import org.cloudfoundry.operations.organizations.OrganizationDetail;
@@ -50,6 +77,8 @@ import org.cloudfoundry.operations.services.ServiceInstance;
import org.cloudfoundry.operations.services.UnbindServiceInstanceRequest;
import org.cloudfoundry.operations.spaces.GetSpaceRequest;
import org.cloudfoundry.operations.spaces.SpaceDetail;
import org.cloudfoundry.util.DelayUtils;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.Exceptions;
@@ -66,6 +95,8 @@ import org.springframework.cloud.appbroker.deployer.DeployApplicationResponse;
import org.springframework.cloud.appbroker.deployer.DeploymentProperties;
import org.springframework.cloud.appbroker.deployer.UndeployApplicationRequest;
import org.springframework.cloud.appbroker.deployer.UndeployApplicationResponse;
import org.springframework.cloud.appbroker.deployer.UpdateApplicationRequest;
import org.springframework.cloud.appbroker.deployer.UpdateApplicationResponse;
import org.springframework.cloud.appbroker.deployer.UpdateServiceInstanceRequest;
import org.springframework.cloud.appbroker.deployer.UpdateServiceInstanceResponse;
import org.springframework.cloud.appbroker.deployer.util.ByteSizeUtils;
@@ -110,7 +141,7 @@ public class CloudFoundryAppDeployer implements AppDeployer, ResourceLoaderAware
@Override
public Mono<DeployApplicationResponse> deploy(DeployApplicationRequest request) {
String appName = request.getName();
Resource appResource = getAppResource(request);
Resource appResource = getAppResource(request.getPath());
Map<String, String> deploymentProperties = request.getProperties();
logger.trace("Deploying application: request={}, resource={}",
@@ -132,6 +163,150 @@ public class CloudFoundryAppDeployer implements AppDeployer, ResourceLoaderAware
.build());
}
@Override
public Mono<UpdateApplicationResponse> update(UpdateApplicationRequest request) {
final String name = request.getName();
final Map<String, Object> environmentVariables =
getApplicationEnvironment(request.getProperties(), request.getEnvironment());
return this.operations
.applications()
.get(GetApplicationRequest.builder().name(name).build())
.map(ApplicationDetail::getId)
.flatMap(applicationId ->
updateApplicationEnvironment(environmentVariables, applicationId)
.thenReturn(applicationId)
)
.flatMap(applicationId -> Mono.zip(Mono.just(applicationId),
createPackageForApplication(applicationId)))
.map(tuple2 -> tuple2.mapT2(CreatePackageResponse::getId))
.flatMap(tuple2 -> {
String packageId = tuple2.getT2();
return Mono.zip(Mono.just(tuple2.getT1()), uploadPackage(request, packageId));
})
.map(tuple2 -> tuple2.mapT2(Package::getId))
.flatMap(tuple2 -> {
String packageId1 = tuple2.getT2();
return Mono.zip(Mono.just(tuple2.getT1()), waitForPackageReady(packageId1));
}
)
.map(tuple2 -> tuple2.mapT2(Package::getId))
.flatMap(tuple2 -> {
String packageId = tuple2.getT2();
return Mono.zip(Mono.just(tuple2.getT1()), createBuildForPackage(packageId));
})
.flatMap(tuple2 -> {
String buildId = tuple2.getT2();
return Mono.zip(Mono.just(tuple2.getT1()), waitForBuildStaged(buildId));
}
)
.map(tuple2 -> tuple2.mapT2((t2) -> t2.getDroplet().getId()))
.flatMap(tuple2 -> {
String dropletId = tuple2.getT2();
String applicationId = tuple2.getT1();
return createDeployment(dropletId, applicationId);
})
.map(CreateDeploymentResponse::getId)
.flatMap(this::waitForDeploymentDeployed)
.thenReturn(UpdateApplicationResponse.builder().name(name).build());
}
private Mono<GetDeploymentResponse> waitForDeploymentDeployed(String deploymentId) {
return this.client.deploymentsV3()
.get(GetDeploymentRequest
.builder()
.deploymentId(deploymentId)
.build())
.filter(p -> p.getState().equals(DeploymentState.DEPLOYED))
.repeatWhenEmpty(getExponentialBackOff());
}
private Function<Flux<Long>, Publisher<?>> getExponentialBackOff() {
return DelayUtils.exponentialBackOff(Duration.ofSeconds(2), Duration.ofSeconds(15), Duration.ofMinutes(10));
}
private Mono<CreateDeploymentResponse> createDeployment(String dropletId, String applicationId) {
return this.client.deploymentsV3()
.create(CreateDeploymentRequest
.builder()
.droplet(Relationship
.builder()
.id(dropletId).build()).relationships(DeploymentRelationships
.builder()
.app(ToOneRelationship
.builder()
.data(Relationship.builder().id(applicationId).build())
.build()
).build())
.build());
}
private Mono<GetBuildResponse> waitForBuildStaged(String buildId) {
return this.client.builds().get(GetBuildRequest.builder().buildId(buildId).build())
.filter(p -> p.getState().equals(BuildState.STAGED))
.repeatWhenEmpty(getExponentialBackOff());
}
private Mono<String> createBuildForPackage(String packageId) {
return this.client.builds()
.create(CreateBuildRequest
.builder()
.getPackage(Relationship.builder().id(packageId).build())
.build())
.map(CreateBuildResponse::getId);
}
private Mono<GetPackageResponse> waitForPackageReady(String packageId1) {
return this.client.packages()
.get(GetPackageRequest.builder().packageId(packageId1).build())
.filter(p -> p.getState().equals(PackageState.READY))
.repeatWhenEmpty(getExponentialBackOff());
}
private Mono<UploadPackageResponse> uploadPackage(UpdateApplicationRequest request, String packageId) {
try {
return this.client.packages()
.upload(UploadPackageRequest
.builder()
.packageId(packageId)
.bits(Paths.get(getAppResource(request.getPath()).getURI()))
.build());
}
catch (IOException e) {
throw Exceptions.propagate(e);
}
}
private Mono<CreatePackageResponse> createPackageForApplication(String applicationId) {
return this.client
.packages()
.create(CreatePackageRequest
.builder()
.relationships(PackageRelationships
.builder()
.application(ToOneRelationship
.builder()
.data(Relationship
.builder()
.id(applicationId)
.build())
.build())
.build())
.type(PackageType.BITS)
.build());
}
private Mono<org.cloudfoundry.client.v2.applications.UpdateApplicationResponse> updateApplicationEnvironment(
Map<String, Object> environmentVariables, String applicationId) {
return this.client.applicationsV2()
.update(org.cloudfoundry.client.v2.applications.UpdateApplicationRequest
.builder()
.applicationId(applicationId)
.putAllEnvironmentJsons(environmentVariables)
.build());
}
private Mono<Void> pushApplication(DeployApplicationRequest request,
Map<String, String> deploymentProperties,
Resource appResource) {
@@ -468,8 +643,8 @@ public class CloudFoundryAppDeployer implements AppDeployer, ResourceLoaderAware
}
}
private Resource getAppResource(DeployApplicationRequest request) {
return resourceLoader.getResource(request.getPath());
private Resource getAppResource(String path) {
return resourceLoader.getResource(path);
}
/**

View File

@@ -117,7 +117,7 @@ class CloudFoundryAppDeployerTest {
.name(APP_NAME)
.path(APP_PATH)
.build();
StepVerifier.create(appDeployer.deploy(request))
.assertNext(response -> assertThat(response.getName()).isEqualTo(APP_NAME))
.verifyComplete();

View File

@@ -0,0 +1,220 @@
/*
* 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.client.CloudFoundryClient;
import org.cloudfoundry.client.v2.applications.ApplicationsV2;
import org.cloudfoundry.client.v2.applications.UpdateApplicationResponse;
import org.cloudfoundry.client.v3.BuildpackData;
import org.cloudfoundry.client.v3.Lifecycle;
import org.cloudfoundry.client.v3.LifecycleType;
import org.cloudfoundry.client.v3.Relationship;
import org.cloudfoundry.client.v3.builds.BuildState;
import org.cloudfoundry.client.v3.builds.Builds;
import org.cloudfoundry.client.v3.builds.CreateBuildResponse;
import org.cloudfoundry.client.v3.builds.CreatedBy;
import org.cloudfoundry.client.v3.builds.Droplet;
import org.cloudfoundry.client.v3.builds.GetBuildResponse;
import org.cloudfoundry.client.v3.deployments.CreateDeploymentResponse;
import org.cloudfoundry.client.v3.deployments.DeploymentState;
import org.cloudfoundry.client.v3.deployments.DeploymentsV3;
import org.cloudfoundry.client.v3.deployments.GetDeploymentResponse;
import org.cloudfoundry.client.v3.packages.BitsData;
import org.cloudfoundry.client.v3.packages.CreatePackageResponse;
import org.cloudfoundry.client.v3.packages.GetPackageResponse;
import org.cloudfoundry.client.v3.packages.PackageState;
import org.cloudfoundry.client.v3.packages.PackageType;
import org.cloudfoundry.client.v3.packages.Packages;
import org.cloudfoundry.client.v3.packages.UploadPackageResponse;
import org.cloudfoundry.operations.CloudFoundryOperations;
import org.cloudfoundry.operations.applications.ApplicationDetail;
import org.cloudfoundry.operations.applications.Applications;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.cloud.appbroker.deployer.AppDeployer;
import org.springframework.cloud.appbroker.deployer.UpdateApplicationRequest;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.ResourceLoader;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class CloudFoundryAppDeployerUpdateApplicationTest {
private static final String APP_NAME = "test-app";
private static final String APP_PATH = "test.jar";
private AppDeployer appDeployer;
@Mock
private Applications operationsApplications;
@Mock
private ApplicationsV2 applicationsV2;
@Mock
private Builds builds;
@Mock
private DeploymentsV3 deploymentsV3;
@Mock
private Packages packages;
@Mock
private CloudFoundryOperations cloudFoundryOperations;
@Mock
private CloudFoundryClient cloudFoundryClient;
@Mock
private ResourceLoader resourceLoader;
@BeforeEach
void setUp() {
CloudFoundryDeploymentProperties deploymentProperties = new CloudFoundryDeploymentProperties();
CloudFoundryTargetProperties targetProperties = new CloudFoundryTargetProperties();
when(operationsApplications.pushManifest(any())).thenReturn(Mono.empty());
when(resourceLoader.getResource(APP_PATH)).thenReturn(new FileSystemResource(APP_PATH));
when(cloudFoundryOperations.applications()).thenReturn(operationsApplications);
when(cloudFoundryClient.applicationsV2()).thenReturn(applicationsV2);
when(cloudFoundryClient.packages()).thenReturn(packages);
when(cloudFoundryClient.builds()).thenReturn(builds);
when(cloudFoundryClient.deploymentsV3()).thenReturn(deploymentsV3);
appDeployer = new CloudFoundryAppDeployer(deploymentProperties,
cloudFoundryOperations, cloudFoundryClient, targetProperties, resourceLoader);
}
@Test
void updateApp() {
when(operationsApplications.get(any()))
.thenReturn(Mono.just(createApplicationDetail()));
when(applicationsV2.update(any()))
.thenReturn(Mono.just(UpdateApplicationResponse.builder().build()));
when(packages.create(any()))
.thenReturn(Mono.just(CreatePackageResponse
.builder()
.data(BitsData.builder().build())
.state(PackageState.READY)
.type(PackageType.BITS)
.createdAt("DATETIME")
.id("package-id")
.build()));
when(packages.upload(any()))
.thenReturn(Mono.just(UploadPackageResponse
.builder()
.data(BitsData.builder().build())
.state(PackageState.READY)
.type(PackageType.BITS)
.createdAt("DATETIME")
.id("package-id")
.build()));
when(packages.get(any()))
.thenReturn(Mono.just(GetPackageResponse
.builder()
.data(BitsData.builder().build())
.state(PackageState.READY)
.type(PackageType.BITS)
.createdAt("DATETIME")
.id("package-id")
.build()));
when(builds.create(any()))
.thenReturn(Mono.just(CreateBuildResponse
.builder()
.state(BuildState.STAGING)
.createdBy(CreatedBy.builder().id("create-by-id").email("an-email").name("creator").build())
.inputPackage(Relationship.builder().id("package-id").build())
.lifecycle(createLifecycle())
.createdAt("DATETIME")
.id("build-id")
.build()));
when(builds.get(any()))
.thenReturn(Mono.just(GetBuildResponse
.builder()
.state(BuildState.STAGED)
.createdBy(CreatedBy.builder().id("create-by-id").email("an-email").name("creator").build())
.inputPackage(Relationship.builder().id("package-id").build())
.lifecycle(createLifecycle())
.droplet(Droplet.builder().id("droplet-id").build())
.createdAt("DATETIME")
.id("build-id")
.build()));
when(deploymentsV3.create(any()))
.thenReturn(Mono.just(CreateDeploymentResponse
.builder()
.state(DeploymentState.DEPLOYED)
.createdAt("DATETIME")
.id("deployment-id")
.build()));
when(deploymentsV3.get(any()))
.thenReturn(Mono.just(GetDeploymentResponse
.builder()
.state(DeploymentState.DEPLOYED)
.createdAt("DATETIME")
.id("deployment-id")
.build()));
UpdateApplicationRequest request =
UpdateApplicationRequest
.builder()
.name(APP_NAME)
.path(APP_PATH)
.build();
StepVerifier.create(appDeployer.update(request))
.assertNext(response -> assertThat(response.getName()).isEqualTo(APP_NAME))
.verifyComplete();
}
private ApplicationDetail createApplicationDetail() {
return ApplicationDetail
.builder()
.id("app-id")
.stack("")
.diskQuota(512)
.instances(1)
.memoryLimit(512)
.name("app")
.requestedState("STARTED")
.runningInstances(1)
.build();
}
private static Lifecycle createLifecycle() {
return Lifecycle.builder().data(BuildpackData.builder().build()).type(LifecycleType.BUILDPACK).build();
}
}