/* * Copyright 2013-2017 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.cloudfoundry.operations.applications; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.OrderDirection; import org.cloudfoundry.client.v2.applications.AbstractApplicationResource; import org.cloudfoundry.client.v2.applications.ApplicationEnvironmentRequest; import org.cloudfoundry.client.v2.applications.ApplicationEnvironmentResponse; import org.cloudfoundry.client.v2.applications.ApplicationInstanceInfo; import org.cloudfoundry.client.v2.applications.ApplicationInstancesRequest; import org.cloudfoundry.client.v2.applications.ApplicationInstancesResponse; import org.cloudfoundry.client.v2.applications.ApplicationStatisticsRequest; import org.cloudfoundry.client.v2.applications.ApplicationStatisticsResponse; import org.cloudfoundry.client.v2.applications.AssociateApplicationRouteRequest; import org.cloudfoundry.client.v2.applications.AssociateApplicationRouteResponse; import org.cloudfoundry.client.v2.applications.CopyApplicationRequest; import org.cloudfoundry.client.v2.applications.CopyApplicationResponse; import org.cloudfoundry.client.v2.applications.CreateApplicationRequest; import org.cloudfoundry.client.v2.applications.CreateApplicationResponse; import org.cloudfoundry.client.v2.applications.InstanceStatistics; import org.cloudfoundry.client.v2.applications.ListApplicationRoutesRequest; import org.cloudfoundry.client.v2.applications.ListApplicationServiceBindingsRequest; import org.cloudfoundry.client.v2.applications.RemoveApplicationRouteRequest; import org.cloudfoundry.client.v2.applications.RemoveApplicationServiceBindingRequest; import org.cloudfoundry.client.v2.applications.RestageApplicationResponse; import org.cloudfoundry.client.v2.applications.Statistics; import org.cloudfoundry.client.v2.applications.SummaryApplicationRequest; import org.cloudfoundry.client.v2.applications.SummaryApplicationResponse; import org.cloudfoundry.client.v2.applications.TerminateApplicationInstanceRequest; import org.cloudfoundry.client.v2.applications.UpdateApplicationRequest; import org.cloudfoundry.client.v2.applications.UploadApplicationRequest; import org.cloudfoundry.client.v2.applications.UploadApplicationResponse; import org.cloudfoundry.client.v2.applications.Usage; import org.cloudfoundry.client.v2.events.EventEntity; import org.cloudfoundry.client.v2.events.EventResource; import org.cloudfoundry.client.v2.events.ListEventsRequest; import org.cloudfoundry.client.v2.organizations.ListOrganizationPrivateDomainsRequest; import org.cloudfoundry.client.v2.organizations.ListOrganizationSpacesRequest; import org.cloudfoundry.client.v2.organizations.ListOrganizationsRequest; import org.cloudfoundry.client.v2.organizations.OrganizationResource; import org.cloudfoundry.client.v2.privatedomains.PrivateDomainEntity; import org.cloudfoundry.client.v2.privatedomains.PrivateDomainResource; import org.cloudfoundry.client.v2.routes.CreateRouteResponse; import org.cloudfoundry.client.v2.routes.DeleteRouteRequest; import org.cloudfoundry.client.v2.routes.DeleteRouteResponse; import org.cloudfoundry.client.v2.routes.ListRoutesRequest; import org.cloudfoundry.client.v2.routes.RouteResource; import org.cloudfoundry.client.v2.servicebindings.CreateServiceBindingRequest; import org.cloudfoundry.client.v2.servicebindings.CreateServiceBindingResponse; import org.cloudfoundry.client.v2.servicebindings.ServiceBindingResource; import org.cloudfoundry.client.v2.serviceinstances.ServiceInstance; import org.cloudfoundry.client.v2.serviceinstances.UnionServiceInstanceResource; import org.cloudfoundry.client.v2.shareddomains.ListSharedDomainsRequest; import org.cloudfoundry.client.v2.shareddomains.SharedDomainEntity; import org.cloudfoundry.client.v2.shareddomains.SharedDomainResource; import org.cloudfoundry.client.v2.spaces.GetSpaceRequest; import org.cloudfoundry.client.v2.spaces.GetSpaceResponse; import org.cloudfoundry.client.v2.spaces.GetSpaceSummaryRequest; import org.cloudfoundry.client.v2.spaces.GetSpaceSummaryResponse; import org.cloudfoundry.client.v2.spaces.ListSpaceApplicationsRequest; import org.cloudfoundry.client.v2.spaces.ListSpaceServiceInstancesRequest; import org.cloudfoundry.client.v2.spaces.SpaceApplicationSummary; import org.cloudfoundry.client.v2.spaces.SpaceResource; import org.cloudfoundry.client.v2.stacks.GetStackRequest; import org.cloudfoundry.client.v2.stacks.GetStackResponse; import org.cloudfoundry.client.v2.stacks.ListStacksRequest; import org.cloudfoundry.client.v2.stacks.StackResource; import org.cloudfoundry.doppler.DopplerClient; import org.cloudfoundry.doppler.Envelope; import org.cloudfoundry.doppler.EventType; import org.cloudfoundry.doppler.LogMessage; import org.cloudfoundry.doppler.RecentLogsRequest; import org.cloudfoundry.doppler.StreamRequest; import org.cloudfoundry.operations.util.OperationsLogging; import org.cloudfoundry.util.DateUtils; import org.cloudfoundry.util.DelayTimeoutException; import org.cloudfoundry.util.ExceptionUtils; import org.cloudfoundry.util.FileUtils; import org.cloudfoundry.util.FluentMap; import org.cloudfoundry.util.JobUtils; import org.cloudfoundry.util.PaginationUtils; import org.cloudfoundry.util.ResourceMatchingUtils; import org.cloudfoundry.util.ResourceUtils; import org.cloudfoundry.util.SortingUtils; import reactor.core.Exceptions; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; import reactor.util.function.Tuple4; import reactor.util.function.Tuples; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; import java.util.function.BiFunction; import java.util.function.Predicate; import java.util.function.UnaryOperator; import java.util.stream.Collectors; import static org.cloudfoundry.util.DelayUtils.exponentialBackOff; import static org.cloudfoundry.util.tuple.TupleUtils.function; import static org.cloudfoundry.util.tuple.TupleUtils.predicate; public final class DefaultApplications implements Applications { private static final int CF_APP_STOPPED_STATS_ERROR = 200003; private static final int CF_BUILDPACK_COMPILED_FAILED = 170004; private static final int CF_INSTANCES_ERROR = 220001; private static final int CF_SERVICE_ALREADY_BOUND = 90003; private static final int CF_STAGING_NOT_FINISHED = 170002; private static final int CF_STAGING_TIME_EXPIRED = 170007; private static final Comparator<LogMessage> LOG_MESSAGE_COMPARATOR = Comparator.comparing(LogMessage::getTimestamp); private static final Duration LOG_MESSAGE_TIMESPAN = Duration.ofMillis(500); private static final int MAX_NUMBER_OF_RECENT_EVENTS = 50; private static final String STARTED_STATE = "STARTED"; private static final String STOPPED_STATE = "STOPPED"; private final Mono<CloudFoundryClient> cloudFoundryClient; private final Mono<DopplerClient> dopplerClient; private final RandomWords randomWords; private final Mono<String> spaceId; public DefaultApplications(Mono<CloudFoundryClient> cloudFoundryClient, Mono<DopplerClient> dopplerClient, Mono<String> spaceId) { this(cloudFoundryClient, dopplerClient, spaceId, new WordListRandomWords()); } DefaultApplications(Mono<CloudFoundryClient> cloudFoundryClient, Mono<DopplerClient> dopplerClient, Mono<String> spaceId, RandomWords randomWords) { this.cloudFoundryClient = cloudFoundryClient; this.dopplerClient = dopplerClient; this.spaceId = spaceId; this.randomWords = randomWords; } @Override public Mono<Void> copySource(CopySourceApplicationRequest request) { return Mono .when(this.cloudFoundryClient, this.spaceId) .then(function((cloudFoundryClient, spaceId) -> Mono.when( Mono.just(cloudFoundryClient), getApplicationId(cloudFoundryClient, request.getName(), spaceId), getApplicationIdFromOrgSpace(cloudFoundryClient, request.getTargetName(), spaceId, request.getTargetOrganization(), request.getTargetSpace()) ))) .then(function((cloudFoundryClient, sourceApplicationId, targetApplicationId) -> copyBits(cloudFoundryClient, request.getStagingTimeout(), sourceApplicationId, targetApplicationId) .then(Mono.just(Tuples.of(cloudFoundryClient, targetApplicationId))))) .filter(predicate((cloudFoundryClient, targetApplicationId) -> Optional.ofNullable(request.getRestart()).orElse(false))) .then(function((cloudFoundryClient, targetApplicationId) -> restartApplication(cloudFoundryClient, request.getTargetName(), targetApplicationId, request.getStagingTimeout(), request.getStartupTimeout()))) .transform(OperationsLogging.log("Copy Application Source")) .checkpoint(); } @Override public Mono<Void> delete(DeleteApplicationRequest request) { return Mono .when(this.cloudFoundryClient, this.spaceId) .then(function((cloudFoundryClient, spaceId) -> getRoutesAndApplicationId(cloudFoundryClient, request, spaceId, Optional.ofNullable(request.getDeleteRoutes()).orElse(false)) .map(function((routes, applicationId) -> Tuples.of(cloudFoundryClient, routes, applicationId))))) .then(function((cloudFoundryClient, routes, applicationId) -> deleteRoutes(cloudFoundryClient, request.getCompletionTimeout(), routes) .then(Mono.just(Tuples.of(cloudFoundryClient, applicationId))))) .then(function((cloudFoundryClient, applicationId) -> removeServiceBindings(cloudFoundryClient, applicationId) .then(Mono.just(Tuples.of(cloudFoundryClient, applicationId))))) .then(function(DefaultApplications::requestDeleteApplication)) .transform(OperationsLogging.log("Delete Application")) .checkpoint(); } @Override public Mono<Void> disableSsh(DisableApplicationSshRequest request) { return Mono .when(this.cloudFoundryClient, this.spaceId) .then(function((cloudFoundryClient, spaceId) -> Mono.when( Mono.just(cloudFoundryClient), getApplicationIdWhere(cloudFoundryClient, request.getName(), spaceId, sshEnabled(true)) ))) .then(function((cloudFoundryClient, applicationId) -> requestUpdateApplicationSsh(cloudFoundryClient, applicationId, false))) .then() .transform(OperationsLogging.log("Disable Application SSH")) .checkpoint(); } @Override public Mono<Void> enableSsh(EnableApplicationSshRequest request) { return Mono .when(this.cloudFoundryClient, this.spaceId) .then(function((cloudFoundryClient, spaceId) -> Mono.when( Mono.just(cloudFoundryClient), getApplicationIdWhere(cloudFoundryClient, request.getName(), spaceId, sshEnabled(false)) ))) .then(function((cloudFoundryClient, applicationId) -> requestUpdateApplicationSsh(cloudFoundryClient, applicationId, true))) .then() .transform(OperationsLogging.log("Enable Application SSH")) .checkpoint(); } @Override public Mono<ApplicationDetail> get(GetApplicationRequest request) { return Mono .when(this.cloudFoundryClient, this.spaceId) .then(function((cloudFoundryClient, spaceId) -> Mono.when( Mono.just(cloudFoundryClient), getApplication(cloudFoundryClient, request.getName(), spaceId) ))) .then(function(DefaultApplications::getAuxiliaryContent)) .map(function(DefaultApplications::toApplicationDetail)) .transform(OperationsLogging.log("Get Application")) .checkpoint(); } @Override public Mono<ApplicationManifest> getApplicationManifest(GetApplicationManifestRequest request) { return Mono .when(this.cloudFoundryClient, this.spaceId) .then(function((cloudFoundryClient, spaceId) -> Mono.when( Mono.just(cloudFoundryClient), getApplicationId(cloudFoundryClient, request.getName(), spaceId) ))) .then(function((cloudFoundryClient, applicationId) -> Mono.when( Mono.just(cloudFoundryClient), requestApplicationSummary(cloudFoundryClient, applicationId) ))) .then(function((cloudFoundryClient, response) -> Mono.when( Mono.just(response), getStackName(cloudFoundryClient, response.getStackId()) ))) .then(function(DefaultApplications::toApplicationManifest)) .transform(OperationsLogging.log("Get Application Manifest")) .checkpoint(); } @Override public Mono<ApplicationEnvironments> getEnvironments(GetApplicationEnvironmentsRequest request) { return Mono .when(this.cloudFoundryClient, this.spaceId) .then(function((cloudFoundryClient, spaceId) -> Mono.when( Mono.just(cloudFoundryClient), getApplicationId(cloudFoundryClient, request.getName(), spaceId) ))) .then(function(DefaultApplications::requestApplicationEnvironment)) .map(DefaultApplications::toApplicationEnvironments) .transform(OperationsLogging.log("Get Application Environments")) .checkpoint(); } @Override public Flux<ApplicationEvent> getEvents(GetApplicationEventsRequest request) { return Mono .when(this.cloudFoundryClient, this.spaceId) .then(function((cloudFoundryClient, spaceId) -> Mono.when( Mono.just(cloudFoundryClient), getApplicationId(cloudFoundryClient, request.getName(), spaceId) ))) .flatMapMany(function((cloudFoundryClient, applicationId) -> requestEvents(applicationId, cloudFoundryClient) .take(Optional.ofNullable(request.getMaxNumberOfEvents()).orElse(MAX_NUMBER_OF_RECENT_EVENTS)))) .map(DefaultApplications::convertToApplicationEvent) .transform(OperationsLogging.log("Get Application Events")) .checkpoint(); } @Override public Mono<ApplicationHealthCheck> getHealthCheck(GetApplicationHealthCheckRequest request) { return Mono .when(this.cloudFoundryClient, this.spaceId) .then(function((cloudFoundryClient, spaceId) -> getApplication(cloudFoundryClient, request.getName(), spaceId))) .map(DefaultApplications::toHealthCheck) .transform(OperationsLogging.log("Get Application Health Check")) .checkpoint(); } @Override public Flux<ApplicationSummary> list() { return Mono .when(this.cloudFoundryClient, this.spaceId) .then(function(DefaultApplications::requestSpaceSummary)) .flatMapMany(DefaultApplications::extractApplications) .map(DefaultApplications::toApplicationSummary) .transform(OperationsLogging.log("List Applications")) .checkpoint(); } @Override public Flux<LogMessage> logs(LogsRequest request) { return Mono .when(this.cloudFoundryClient, this.spaceId) .then(function((cloudFoundryClient, spaceId) -> getApplicationId(cloudFoundryClient, request.getName(), spaceId))) .flatMapMany(applicationId -> getLogs(this.dopplerClient, applicationId, request.getRecent())) .transform(OperationsLogging.log("Get Application Logs")) .checkpoint(); } @Override @SuppressWarnings("deprecation") public Mono<Void> push(PushApplicationRequest request) { ApplicationManifest.Builder builder = ApplicationManifest.builder() .buildpack(request.getBuildpack()) .command(request.getCommand()) .disk(request.getDiskQuota()) .dockerImage(request.getDockerImage()) .healthCheckType(request.getHealthCheckType()) .instances(request.getInstances()) .memory(request.getMemory()) .name(request.getName()) .noRoute(request.getNoRoute()) .path(Optional.ofNullable(request.getPath()).orElse(request.getApplication())) .randomRoute(request.getRandomRoute()) .stack(request.getStack()) .timeout(request.getTimeout()); if (request.getDomain() == null) { if (request.getHost() != null) { builder.host(request.getHost()); } } else { StringBuilder sb = new StringBuilder(); Optional.ofNullable(request.getHost()) .ifPresent(host -> sb.append(host).append(".")); Optional.ofNullable(request.getDomain()) .ifPresent(sb::append); Optional.ofNullable(request.getRoutePath()) .ifPresent(sb::append); builder.route(Route.builder() .route(sb.toString()) .build()); } return pushManifest(PushApplicationManifestRequest.builder() .manifest(builder.build()) .noStart(request.getNoStart()) .stagingTimeout(request.getStagingTimeout()) .startupTimeout(request.getStartupTimeout()) .build()) .transform(OperationsLogging.log("Push")) .checkpoint(); } @Override public Mono<Void> pushManifest(PushApplicationManifestRequest request) { return Mono .when(this.cloudFoundryClient, this.spaceId) .then(function((cloudFoundryClient, spaceId) -> Mono.when( Mono.just(cloudFoundryClient), getSpaceOrganizationId(cloudFoundryClient, spaceId), Mono.just(spaceId) ))) .then(function((cloudFoundryClient, organizationId, spaceId) -> Mono.when( Mono.just(cloudFoundryClient), listAvailableDomains(cloudFoundryClient, organizationId), Mono.just(spaceId)))) .flatMapMany(function((cloudFoundryClient, availableDomains, spaceId) -> Flux.fromIterable(request.getManifests()) .flatMap(manifest -> { if (manifest.getPath() != null) { return pushApplication(cloudFoundryClient, availableDomains, manifest, this.randomWords, request, spaceId); } else if (!manifest.getDockerImage().isEmpty()) { return pushDocker(cloudFoundryClient, availableDomains, manifest, this.randomWords, request, spaceId); } else { throw new IllegalStateException("One of application or dockerImage must be supplied"); } }))) .then() .transform(OperationsLogging.log("Push Manifest")) .checkpoint(); } @Override public Mono<Void> rename(RenameApplicationRequest request) { return Mono .when(this.cloudFoundryClient, this.spaceId) .then(function((cloudFoundryClient, spaceId) -> Mono.when( Mono.just(cloudFoundryClient), getApplicationId(cloudFoundryClient, request.getName(), spaceId) ))) .then(function((cloudFoundryClient, applicationId) -> requestUpdateApplicationName(cloudFoundryClient, applicationId, request.getNewName()))) .then() .transform(OperationsLogging.log("Rename Application")) .checkpoint(); } @Override public Mono<Void> restage(RestageApplicationRequest request) { return Mono .when(this.cloudFoundryClient, this.spaceId) .then(function((cloudFoundryClient, spaceId) -> Mono.when( Mono.just(cloudFoundryClient), getApplicationId(cloudFoundryClient, request.getName(), spaceId) ))) .then(function((cloudFoundryClient, applicationId) -> restageApplication(cloudFoundryClient, request.getName(), applicationId, request.getStagingTimeout(), request.getStartupTimeout()))) .transform(OperationsLogging.log("Restage Application")) .checkpoint(); } @Override public Mono<Void> restart(RestartApplicationRequest request) { return Mono .when(this.cloudFoundryClient, this.spaceId) .then(function((cloudFoundryClient, spaceId) -> Mono.when( Mono.just(cloudFoundryClient), getApplication(cloudFoundryClient, request.getName(), spaceId) ))) .then(function((cloudFoundryClient, resource) -> Mono.when( Mono.just(cloudFoundryClient), stopApplicationIfNotStopped(cloudFoundryClient, resource) ))) .then(function((cloudFoundryClient, stoppedApplication) -> startApplicationAndWait(cloudFoundryClient, request.getName(), ResourceUtils.getId(stoppedApplication), request.getStagingTimeout(), request.getStartupTimeout()))) .transform(OperationsLogging.log("Restart Application")) .checkpoint(); } @Override public Mono<Void> restartInstance(RestartApplicationInstanceRequest request) { return Mono .when(this.cloudFoundryClient, this.spaceId) .then(function((cloudFoundryClient, spaceId) -> Mono.when( Mono.just(cloudFoundryClient), getApplicationId(cloudFoundryClient, request.getName(), spaceId) ))) .then(function((cloudFoundryClient, applicationId) -> requestTerminateApplicationInstance(cloudFoundryClient, applicationId, String.valueOf(request.getInstanceIndex())))) .transform(OperationsLogging.log("Restart Application Instance")) .checkpoint(); } @Override public Mono<Void> scale(ScaleApplicationRequest request) { return Mono .when(this.cloudFoundryClient, this.spaceId) .filter(predicate((cloudFoundryClient, spaceId) -> areModifiersPresent(request))) .then(function((cloudFoundryClient, spaceId) -> Mono.when( Mono.just(cloudFoundryClient), getApplicationId(cloudFoundryClient, request.getName(), spaceId) ))) .then(function((cloudFoundryClient, applicationId) -> Mono.when( Mono.just(cloudFoundryClient), requestUpdateApplicationScale(cloudFoundryClient, applicationId, request.getDiskLimit(), request.getInstances(), request.getMemoryLimit()) ))) .filter(predicate((cloudFoundryClient, resource) -> isRestartRequired(request, resource))) .then(function((cloudFoundryClient, resource) -> restartApplication(cloudFoundryClient, request.getName(), ResourceUtils.getId(resource), request.getStagingTimeout(), request.getStartupTimeout()))) .transform(OperationsLogging.log("Scale Application")) .checkpoint(); } @Override public Mono<Void> setEnvironmentVariable(SetEnvironmentVariableApplicationRequest request) { return Mono .when(this.cloudFoundryClient, this.spaceId) .then(function((cloudFoundryClient, spaceId) -> Mono.when( Mono.just(cloudFoundryClient), getApplication(cloudFoundryClient, request.getName(), spaceId) ))) .then(function((cloudFoundryClient, resource) -> requestUpdateApplicationEnvironment(cloudFoundryClient, ResourceUtils.getId(resource), addToEnvironment(getEnvironment(resource), request.getVariableName(), request.getVariableValue())))) .then() .transform(OperationsLogging.log("Set Application Environment Variable")) .checkpoint(); } @Override public Mono<Void> setHealthCheck(SetApplicationHealthCheckRequest request) { return Mono .when(this.cloudFoundryClient, this.spaceId) .then(function((cloudFoundryClient, spaceId) -> Mono.when( Mono.just(cloudFoundryClient), getApplicationId(cloudFoundryClient, request.getName(), spaceId) ))) .then(function((cloudFoundryClient, applicationId) -> requestUpdateApplicationHealthCheckType(cloudFoundryClient, applicationId, request.getType()))) .then() .transform(OperationsLogging.log("Set Application Health Check")) .checkpoint(); } @Override public Mono<Boolean> sshEnabled(ApplicationSshEnabledRequest request) { return Mono .when(this.cloudFoundryClient, this.spaceId) .then(function((cloudFoundryClient, spaceId) -> getApplication(cloudFoundryClient, request.getName(), spaceId))) .map(applicationResource -> ResourceUtils.getEntity(applicationResource).getEnableSsh()) .transform(OperationsLogging.log("Is Application SSH Enabled")) .checkpoint(); } @Override public Mono<Void> start(StartApplicationRequest request) { return Mono .when(this.cloudFoundryClient, this.spaceId) .then(function((cloudFoundryClient, spaceId) -> Mono.when( Mono.just(cloudFoundryClient), getApplicationIdWhere(cloudFoundryClient, request.getName(), spaceId, isNotIn(STARTED_STATE)) ))) .then(function((cloudFoundryClient, applicationId) -> startApplicationAndWait(cloudFoundryClient, request.getName(), applicationId, request.getStagingTimeout(), request.getStartupTimeout()))) .transform(OperationsLogging.log("Start Application")) .checkpoint(); } @Override public Mono<Void> stop(StopApplicationRequest request) { return Mono .when(this.cloudFoundryClient, this.spaceId) .then(function((cloudFoundryClient, spaceId) -> Mono.when( Mono.just(cloudFoundryClient), getApplicationIdWhere(cloudFoundryClient, request.getName(), spaceId, isNotIn(STOPPED_STATE)) ))) .then(function(DefaultApplications::stopApplication)) .then() .transform(OperationsLogging.log("Stop Application")) .checkpoint(); } @Override public Mono<Void> unsetEnvironmentVariable(UnsetEnvironmentVariableApplicationRequest request) { return Mono .when(this.cloudFoundryClient, this.spaceId) .then(function((cloudFoundryClient, spaceId) -> Mono.when( Mono.just(cloudFoundryClient), getApplication(cloudFoundryClient, request.getName(), spaceId) ))) .then(function((cloudFoundryClient, resource) -> requestUpdateApplicationEnvironment(cloudFoundryClient, ResourceUtils.getId(resource), removeFromEnvironment(getEnvironment(resource), request.getVariableName())))) .then() .transform(OperationsLogging.log("Unset Application Environment Variable")) .checkpoint(); } private static Map<String, Object> addToEnvironment(Map<String, Object> environment, String variableName, Object variableValue) { return FluentMap.<String, Object>builder() .entries(environment) .entry(variableName, variableValue) .build(); } private static boolean areModifiersPresent(ScaleApplicationRequest request) { return request.getMemoryLimit() != null || request.getDiskLimit() != null || request.getInstances() != null; } private static Flux<String> associateDefaultDomain(CloudFoundryClient cloudFoundryClient, String applicationId, List<DomainSummary> availableDomains, ApplicationManifest manifest, String spaceId) { return getDefaultDomainId(cloudFoundryClient) .flatMapMany(domainId -> getPushRouteIdFromDomain(cloudFoundryClient, availableDomains, domainId, manifest, spaceId)) .flatMap(routeId -> requestAssociateRoute(cloudFoundryClient, applicationId, routeId)) .map(ResourceUtils::getId); } private static Mono<Void> bindServices(CloudFoundryClient cloudFoundryClient, String applicationId, ApplicationManifest manifest, String spaceId) { if (manifest.getServices() == null || manifest.getServices().size() == 0) { return Mono.empty(); } return Flux.fromIterable(manifest.getServices()) .flatMap(serviceInstanceName -> getServiceId(cloudFoundryClient, serviceInstanceName, spaceId)) .flatMap(serviceInstanceId -> requestCreateServiceBinding(cloudFoundryClient, applicationId, serviceInstanceId) .onErrorResume(ExceptionUtils.statusCode(CF_SERVICE_ALREADY_BOUND), t -> Mono.empty())) .then(); } private static BiFunction<String, String, String> collectStates() { return (totalState, instanceState) -> { if ("RUNNING".equals(instanceState) || "RUNNING".equals(totalState)) { return "RUNNING"; } if ("FLAPPING".equals(instanceState) || "CRASHED".equals(instanceState)) { return "FAILED"; } return totalState; }; } private static ApplicationEvent convertToApplicationEvent(EventResource resource) { EventEntity entity = resource.getEntity(); Date timestamp = null; try { timestamp = DateUtils.parseFromIso8601(entity.getTimestamp()); } catch (IllegalArgumentException iae) { // do not set time } return ApplicationEvent.builder() .actor(entity.getActorName()) .description(eventDescription(getMetadataRequest(entity), "instances", "memory", "state", "environment_json")) .id(ResourceUtils.getId(resource)) .event(entity.getType()) .time(timestamp) .build(); } private static Mono<Void> copyBits(CloudFoundryClient cloudFoundryClient, Duration completionTimeout, String sourceApplicationId, String targetApplicationId) { return requestCopyBits(cloudFoundryClient, sourceApplicationId, targetApplicationId) .then(job -> JobUtils.waitForCompletion(cloudFoundryClient, completionTimeout, job)); } private static Mono<Void> deleteRoute(CloudFoundryClient cloudFoundryClient, String routeId, Duration completionTimeout) { return requestDeleteRoute(cloudFoundryClient, routeId) .then(job -> JobUtils.waitForCompletion(cloudFoundryClient, completionTimeout, job)); } private static Mono<Void> deleteRoutes(CloudFoundryClient cloudFoundryClient, Duration completionTimeout, Optional<List<org.cloudfoundry.client.v2.routes.Route>> routes) { return routes .map(Flux::fromIterable) .orElse(Flux.empty()) .map(org.cloudfoundry.client.v2.routes.Route::getId) .flatMap(routeId -> deleteRoute(cloudFoundryClient, routeId, completionTimeout)) .then(); } private static String deriveHostname(String host, ApplicationManifest manifest, RandomWords randomWords) { if (Optional.ofNullable(manifest.getNoHostname()).orElse(false)) { return ""; } else if (host != null) { return host; } else if (Optional.ofNullable(manifest.getRandomRoute()).orElse(false)) { return String.join("-", manifest.getName(), randomWords.getAdjective(), randomWords.getNoun()); } else { return manifest.getName(); } } private static Statistics emptyApplicationStatistics() { return Statistics.builder() .usage(emptyApplicationUsage()) .build(); } private static Usage emptyApplicationUsage() { return Usage.builder() .build(); } private static InstanceStatistics emptyInstanceStats() { return InstanceStatistics.builder() .statistics(emptyApplicationStatistics()) .build(); } private static String eventDescription(Map<String, Object> request, String... entryNames) { if (request == null) { return ""; } boolean first = true; StringBuilder sb = new StringBuilder(); for (String entryName : entryNames) { Object value = request.get(entryName); if (value == null) { continue; } if (!first) { sb.append(", "); } first = false; sb.append(entryName).append(": ").append(String.valueOf(value)); } return sb.toString(); } private static Flux<SpaceApplicationSummary> extractApplications(GetSpaceSummaryResponse getSpaceSummaryResponse) { return Flux.fromIterable(getSpaceSummaryResponse.getApplications()); } private static Mono<AbstractApplicationResource> getApplication(CloudFoundryClient cloudFoundryClient, String application, String spaceId) { return requestApplications(cloudFoundryClient, application, spaceId) .single() .onErrorResume(NoSuchElementException.class, t -> ExceptionUtils.illegalArgument("Application %s does not exist", application)); } private static Mono<String> getApplicationId(CloudFoundryClient cloudFoundryClient, String application, String spaceId) { return getApplication(cloudFoundryClient, application, spaceId) .map(ResourceUtils::getId); } private static Mono<String> getApplicationId(CloudFoundryClient cloudFoundryClient, ApplicationManifest manifest, String spaceId, String stackId) { return requestApplications(cloudFoundryClient, manifest.getName(), spaceId) .singleOrEmpty() .then(application -> { Map<String, Object> environmentJsons = new HashMap<>(ResourceUtils.getEntity(application).getEnvironmentJsons()); Optional.ofNullable(manifest.getEnvironmentVariables()).ifPresent(environmentJsons::putAll); return requestUpdateApplication(cloudFoundryClient, ResourceUtils.getId(application), environmentJsons, manifest, stackId) .map(ResourceUtils::getId); }) .switchIfEmpty(requestCreateApplication(cloudFoundryClient, manifest, spaceId, stackId) .map(ResourceUtils::getId)); } private static Mono<String> getApplicationIdFromOrgSpace(CloudFoundryClient cloudFoundryClient, String application, String spaceId, String organization, String space) { return getSpaceOrganizationId(cloudFoundryClient, spaceId) .then(organizationId -> organization != null ? getOrganizationId(cloudFoundryClient, organization) : Mono.just(organizationId)) .then(organizationId -> space != null ? getSpaceId(cloudFoundryClient, organizationId, space) : Mono.just(spaceId)) .then(spaceId1 -> getApplicationId(cloudFoundryClient, application, spaceId1)); } private static Mono<String> getApplicationIdWhere(CloudFoundryClient cloudFoundryClient, String application, String spaceId, Predicate<AbstractApplicationResource> predicate) { return getApplication(cloudFoundryClient, application, spaceId) .filter(predicate) .map(ResourceUtils::getId); } private static Mono<ApplicationInstancesResponse> getApplicationInstances(CloudFoundryClient cloudFoundryClient, String applicationId) { return requestApplicationInstances(cloudFoundryClient, applicationId) .onErrorResume(ExceptionUtils.statusCode(CF_BUILDPACK_COMPILED_FAILED, CF_INSTANCES_ERROR, CF_STAGING_NOT_FINISHED, CF_STAGING_TIME_EXPIRED), t -> Mono.just(ApplicationInstancesResponse.builder().build())); } private static Mono<ApplicationStatisticsResponse> getApplicationStatistics(CloudFoundryClient cloudFoundryClient, String applicationId) { return requestApplicationStatistics(cloudFoundryClient, applicationId) .onErrorResume(ExceptionUtils.statusCode(CF_APP_STOPPED_STATS_ERROR), t -> Mono.just(ApplicationStatisticsResponse.builder().build())); } private static Mono<Tuple4<SummaryApplicationResponse, GetStackResponse, List<InstanceDetail>, List<String>>> getAuxiliaryContent(CloudFoundryClient cloudFoundryClient, AbstractApplicationResource applicationResource) { String applicationId = ResourceUtils.getId(applicationResource); String stackId = ResourceUtils.getEntity(applicationResource).getStackId(); return Mono .when( getApplicationStatistics(cloudFoundryClient, applicationId), requestApplicationSummary(cloudFoundryClient, applicationId), getApplicationInstances(cloudFoundryClient, applicationId) ) .then(function((applicationStatisticsResponse, summaryApplicationResponse, applicationInstancesResponse) -> Mono.when( Mono.just(summaryApplicationResponse), requestStack(cloudFoundryClient, stackId), toInstanceDetailList(applicationInstancesResponse, applicationStatisticsResponse), toUrls(summaryApplicationResponse.getRoutes()) ))); } private static String getBuildpack(SummaryApplicationResponse response) { return Optional .ofNullable(response.getBuildpack()) .orElse(response.getDetectedBuildpack()); } private static Mono<String> getDefaultDomainId(CloudFoundryClient cloudFoundryClient) { return requestSharedDomains(cloudFoundryClient) .map(ResourceUtils::getId) .next() .switchIfEmpty(ExceptionUtils.illegalArgument("No default domain found")); } private static String getDomainId(List<DomainSummary> availableDomains, String domainName) { return availableDomains.stream() .filter(domain -> domainName.equals(domain.getName())) .map(DomainSummary::getId) .findFirst() .orElseThrow(() -> new IllegalArgumentException(String.format("Domain %s not found", domainName))); } private static Map<String, Object> getEnvironment(AbstractApplicationResource resource) { return ResourceUtils.getEntity(resource).getEnvironmentJsons(); } private static Flux<LogMessage> getLogs(Mono<DopplerClient> dopplerClient, String applicationId, Boolean recent) { if (Optional.ofNullable(recent).orElse(false)) { return requestLogsRecent(dopplerClient, applicationId) .filter(e -> EventType.LOG_MESSAGE == e.getEventType()) .map(Envelope::getLogMessage) .collectSortedList(LOG_MESSAGE_COMPARATOR) .flatMapIterable(d -> d); } else { return requestLogsStream(dopplerClient, applicationId) .filter(e -> EventType.LOG_MESSAGE == e.getEventType()) .map(Envelope::getLogMessage) .compose(SortingUtils.timespan(LOG_MESSAGE_COMPARATOR, LOG_MESSAGE_TIMESPAN)); } } @SuppressWarnings("unchecked") private static Map<String, Object> getMetadataRequest(EventEntity entity) { Map<String, Optional<Object>> metadata = Optional .ofNullable(entity.getMetadatas()) .orElse(Collections.emptyMap()); if (metadata.get("request") != null) { return metadata.get("request") .map(m -> (Map<String, Object>) m) .orElse(Collections.emptyMap()); } else { return Collections.emptyMap(); } } private static Mono<Optional<List<org.cloudfoundry.client.v2.routes.Route>>> getOptionalRoutes(CloudFoundryClient cloudFoundryClient, boolean deleteRoutes, String applicationId) { if (deleteRoutes) { return getRoutes(cloudFoundryClient, applicationId) .map(Optional::of); } else { return Mono.just(Optional.empty()); } } private static Mono<Optional<String>> getOptionalStackId(CloudFoundryClient cloudFoundryClient, String stack) { return Optional.ofNullable(stack) .map(stack1 -> getStackId(cloudFoundryClient, stack1) .map(Optional::of)) .orElse(Mono.just(Optional.empty())); } private static Mono<OrganizationResource> getOrganization(CloudFoundryClient cloudFoundryClient, String organization) { return requestOrganizations(cloudFoundryClient, organization) .single() .onErrorResume(NoSuchElementException.class, t -> ExceptionUtils.illegalArgument("Organization %s not found", organization)); } private static Mono<String> getOrganizationId(CloudFoundryClient cloudFoundryClient, String organization) { return getOrganization(cloudFoundryClient, organization) .map(ResourceUtils::getId); } private static Mono<SpaceResource> getOrganizationSpaceByName(CloudFoundryClient cloudFoundryClient, String organizationId, String space) { return requestOrganizationSpacesByName(cloudFoundryClient, organizationId, space) .single() .onErrorResume(NoSuchElementException.class, t -> ExceptionUtils.illegalArgument("Space %s not found", space)); } private static Flux<String> getPushRouteIdFromDomain(CloudFoundryClient cloudFoundryClient, List<DomainSummary> availableDomains, String domainId, ApplicationManifest manifest, String spaceId) { if (isTcpDomain(availableDomains, domainId)) { return requestCreateTcpRoute(cloudFoundryClient, domainId, spaceId) .map(ResourceUtils::getId) .flux(); } if (manifest.getNoHostname() != null && manifest.getNoHostname()) { return getRouteId(cloudFoundryClient, domainId, null, null) .switchIfEmpty(requestCreateRoute(cloudFoundryClient, domainId, null, null, spaceId) .map(ResourceUtils::getId)) .flux(); } List<String> hosts = manifest.getHosts() == null ? Collections.singletonList(manifest.getName()) : manifest.getHosts(); return Flux.fromIterable(hosts) .flatMap(host -> getRouteId(cloudFoundryClient, domainId, host, null) .switchIfEmpty(requestCreateRoute(cloudFoundryClient, domainId, host, null, spaceId) .map(ResourceUtils::getId))); } private static Flux<String> getPushRouteIdFromRoute(CloudFoundryClient cloudFoundryClient, List<DomainSummary> availableDomains, ApplicationManifest manifest, String spaceId, RandomWords randomWords) { return Flux.fromIterable(manifest.getRoutes()) .flatMap(route -> RouteUtils.decomposeRoute(availableDomains, route.getRoute())) .flatMap(decomposedRoute -> { String domainId = getDomainId(availableDomains, decomposedRoute.getDomain()); if (isTcpDomain(availableDomains, domainId)) { return getRouteIdForTcpRoute(cloudFoundryClient, decomposedRoute, domainId, spaceId); } else { return getRouteIdForHttpRoute(cloudFoundryClient, decomposedRoute, domainId, manifest, spaceId, randomWords); } }); } private static Mono<String> getRouteId(CloudFoundryClient cloudFoundryClient, String domainId, String host, String routePath) { return requestRoutes(cloudFoundryClient, domainId, host, null, routePath) .filter(resource -> isIdentical(host, ResourceUtils.getEntity(resource).getHost())) .filter(resource -> isIdentical(Optional.ofNullable(routePath).orElse(""), ResourceUtils.getEntity(resource).getPath())) .singleOrEmpty() .map(ResourceUtils::getId); } private static Mono<String> getRouteIdForHttpRoute(CloudFoundryClient cloudFoundryClient, DecomposedRoute decomposedRoute, String domainId, ApplicationManifest manifest, String spaceId, RandomWords randomWords) { String derivedHost = deriveHostname(decomposedRoute.getHost(), manifest, randomWords); return getRouteId(cloudFoundryClient, domainId, derivedHost, decomposedRoute.getPath()) .switchIfEmpty(requestCreateRoute(cloudFoundryClient, domainId, derivedHost, decomposedRoute.getPath(), spaceId) .map(ResourceUtils::getId)); } private static Mono<String> getRouteIdForTcpRoute(CloudFoundryClient cloudFoundryClient, DecomposedRoute decomposedRoute, String domainId, String spaceId) { return getTcpRouteId(cloudFoundryClient, domainId, decomposedRoute.getPort()) .switchIfEmpty(requestCreateTcpRoute(cloudFoundryClient, domainId, decomposedRoute.getPort(), spaceId) .map(ResourceUtils::getId)); } private static Mono<List<org.cloudfoundry.client.v2.routes.Route>> getRoutes(CloudFoundryClient cloudFoundryClient, String applicationId) { return requestApplicationSummary(cloudFoundryClient, applicationId) .map(SummaryApplicationResponse::getRoutes); } private static Mono<Tuple2<Optional<List<org.cloudfoundry.client.v2.routes.Route>>, String>> getRoutesAndApplicationId(CloudFoundryClient cloudFoundryClient, DeleteApplicationRequest request, String spaceId, boolean deleteRoutes) { return getApplicationId(cloudFoundryClient, request.getName(), spaceId) .then(applicationId -> getOptionalRoutes(cloudFoundryClient, deleteRoutes, applicationId) .and(Mono.just(applicationId))); } private static Mono<String> getServiceId(CloudFoundryClient cloudFoundryClient, String serviceInstanceName, String spaceId) { return requestListServiceInstances(cloudFoundryClient, serviceInstanceName, spaceId) .map(ResourceUtils::getId) .single() .onErrorResume(NoSuchElementException.class, t -> ExceptionUtils.illegalArgument("Service instance %s could not be found", serviceInstanceName)); } private static Mono<String> getSpaceId(CloudFoundryClient cloudFoundryClient, String organizationId, String space) { return getOrganizationSpaceByName(cloudFoundryClient, organizationId, space) .map(ResourceUtils::getId); } private static Mono<String> getSpaceOrganizationId(CloudFoundryClient cloudFoundryClient, String spaceId) { return requestSpace(cloudFoundryClient, spaceId) .map(response -> ResourceUtils.getEntity(response).getOrganizationId()); } private static Mono<String> getStackId(CloudFoundryClient cloudFoundryClient, String stack) { return requestStacks(cloudFoundryClient, stack) .map(ResourceUtils::getId) .single() .onErrorResume(NoSuchElementException.class, t -> ExceptionUtils.illegalArgument("Stack %s does not exist", stack)); } private static Mono<String> getStackName(CloudFoundryClient cloudFoundryClient, String stackId) { return requestStack(cloudFoundryClient, stackId) .map(getStackResponse -> getStackResponse.getEntity().getName()); } private static Mono<String> getTcpRouteId(CloudFoundryClient cloudFoundryClient, String domainId, Integer port) { return requestRoutes(cloudFoundryClient, domainId, null, port, null) .singleOrEmpty() .map(ResourceUtils::getId); } private static boolean isIdentical(String s, String t) { return s == null ? t == null : s.equals(t); } private static Predicate<String> isInstanceComplete() { return state -> "RUNNING".equals(state) || "FAILED".equals(state); } private static Predicate<AbstractApplicationResource> isNotIn(String expectedState) { return resource -> isNotIn(resource, expectedState); } private static boolean isNotIn(AbstractApplicationResource resource, String expectedState) { return !expectedState.equals(ResourceUtils.getEntity(resource).getState()); } private static boolean isRestartRequired(ScaleApplicationRequest request, AbstractApplicationResource applicationResource) { return (request.getDiskLimit() != null || request.getMemoryLimit() != null) && STARTED_STATE.equals(ResourceUtils.getEntity(applicationResource).getState()); } private static Predicate<String> isRunning() { return "RUNNING"::equals; } private static Predicate<String> isStaged() { return "STAGED"::equals; } private static Predicate<String> isStagingComplete() { return state -> "STAGED".equals(state) || "FAILED".equals(state); } private static boolean isTcpDomain(List<DomainSummary> availableDomains, String domainId) { List<String> tcpDomainIds = availableDomains.stream() .filter(domain -> "tcp".equals(domain.getType())) .map(DomainSummary::getId) .collect(Collectors.toList()); return tcpDomainIds.contains(domainId); } private static Mono<List<DomainSummary>> listAvailableDomains(CloudFoundryClient cloudFoundryClient, String organizationId) { return requestListPrivateDomains(cloudFoundryClient, organizationId) .map(DefaultApplications::toDomain) .mergeWith(requestListSharedDomains(cloudFoundryClient) .map(DefaultApplications::toDomain)) .collectList(); } private static Mono<Void> prepareDomainsAndRoutes(CloudFoundryClient cloudFoundryClient, String applicationId, List<DomainSummary> availableDomains, ApplicationManifest manifest, String spaceId, RandomWords randomWords) { if (Optional.ofNullable(manifest.getNoRoute()).orElse(false)) { return requestApplicationRoutes(cloudFoundryClient, applicationId) .map(ResourceUtils::getId) .flatMap(routeId -> requestRemoveRouteFromApplication(cloudFoundryClient, applicationId, routeId)) .then(); } if (manifest.getRoutes() == null) { if (manifest.getDomains() == null) { return requestApplicationRoutes(cloudFoundryClient, applicationId) .map(ResourceUtils::getId) //A route already exists for the application, do nothing .switchIfEmpty(associateDefaultDomain(cloudFoundryClient, applicationId, availableDomains, manifest, spaceId)) .then(); } else { return Flux.fromIterable(manifest.getDomains()) .flatMap(domain -> getPushRouteIdFromDomain(cloudFoundryClient, availableDomains, getDomainId(availableDomains, domain), manifest, spaceId) .flatMap(routeId -> requestAssociateRoute(cloudFoundryClient, applicationId, routeId))) .then(); } } else { return getPushRouteIdFromRoute(cloudFoundryClient, availableDomains, manifest, spaceId, randomWords) .flatMapSequential(routeId -> requestAssociateRoute(cloudFoundryClient, applicationId, routeId), 1) .then(); } } private static Flux<Void> pushApplication(CloudFoundryClient cloudFoundryClient, List<DomainSummary> availableDomains, ApplicationManifest manifest, RandomWords randomWords, PushApplicationManifestRequest request, String spaceId) { return getOptionalStackId(cloudFoundryClient, manifest.getStack()) .flatMapMany(stackId -> Mono.when( getApplicationId(cloudFoundryClient, manifest, spaceId, stackId.orElse(null)), ResourceMatchingUtils.getMatchedResources(cloudFoundryClient, manifest.getPath()) )) .flatMap(function((applicationId, matchedResources) -> prepareDomainsAndRoutes(cloudFoundryClient, applicationId, availableDomains, manifest, spaceId, randomWords) .then(Mono.just(Tuples.of(applicationId, matchedResources))))) .flatMap(function((applicationId, matchedResources) -> Mono .when( uploadApplicationAndWait(cloudFoundryClient, applicationId, manifest.getPath(), matchedResources, request.getStagingTimeout()), bindServices(cloudFoundryClient, applicationId, manifest, spaceId) ) .then(Mono.just(applicationId)))) .flatMap(applicationId -> stopAndStartApplication(cloudFoundryClient, applicationId, manifest.getName(), request)); } private static Flux<Void> pushDocker(CloudFoundryClient cloudFoundryClient, List<DomainSummary> availableDomains, ApplicationManifest manifest, RandomWords randomWords, PushApplicationManifestRequest request, String spaceId) { return getOptionalStackId(cloudFoundryClient, manifest.getStack()) .flatMapMany(stackId -> getApplicationId(cloudFoundryClient, manifest, spaceId, stackId.orElse(null))) .flatMap(applicationId -> prepareDomainsAndRoutes(cloudFoundryClient, applicationId, availableDomains, manifest, spaceId, randomWords) .then(Mono.just(applicationId))) .flatMap(applicationId -> bindServices(cloudFoundryClient, applicationId, manifest, spaceId) .then(Mono.just(applicationId))) .flatMap(applicationId -> stopAndStartApplication(cloudFoundryClient, applicationId, manifest.getName(), request)); } private static Map<String, Object> removeFromEnvironment(Map<String, Object> environment, String variableName) { Map<String, Object> modified = new HashMap<>(environment); modified.remove(variableName); return modified; } private static Mono<Void> removeServiceBindings(CloudFoundryClient cloudFoundryClient, String applicationId) { return requestListServiceBindings(cloudFoundryClient, applicationId) .map(ResourceUtils::getId) .flatMap(serviceBindingId -> requestRemoveServiceBinding(cloudFoundryClient, applicationId, serviceBindingId)) .then(); } private static Mono<ApplicationEnvironmentResponse> requestApplicationEnvironment(CloudFoundryClient cloudFoundryClient, String applicationId) { return cloudFoundryClient.applicationsV2() .environment(ApplicationEnvironmentRequest.builder() .applicationId(applicationId) .build()); } private static Mono<ApplicationInstancesResponse> requestApplicationInstances(CloudFoundryClient cloudFoundryClient, String applicationId) { return cloudFoundryClient.applicationsV2() .instances(ApplicationInstancesRequest.builder() .applicationId(applicationId) .build()); } private static Flux<RouteResource> requestApplicationRoutes(CloudFoundryClient cloudFoundryClient, String applicationId) { return PaginationUtils .requestClientV2Resources(page -> cloudFoundryClient.applicationsV2() .listRoutes(ListApplicationRoutesRequest.builder() .applicationId(applicationId) .page(page) .build())); } private static Mono<ApplicationStatisticsResponse> requestApplicationStatistics(CloudFoundryClient cloudFoundryClient, String applicationId) { return cloudFoundryClient.applicationsV2() .statistics(ApplicationStatisticsRequest.builder() .applicationId(applicationId) .build()); } private static Mono<SummaryApplicationResponse> requestApplicationSummary(CloudFoundryClient cloudFoundryClient, String applicationId) { return cloudFoundryClient.applicationsV2() .summary(SummaryApplicationRequest.builder() .applicationId(applicationId) .build()); } private static Flux<AbstractApplicationResource> requestApplications(CloudFoundryClient cloudFoundryClient, String application, String spaceId) { return PaginationUtils .requestClientV2Resources(page -> cloudFoundryClient.spaces() .listApplications(ListSpaceApplicationsRequest.builder() .name(application) .spaceId(spaceId) .page(page) .build())) .cast(AbstractApplicationResource.class); } private static Mono<AssociateApplicationRouteResponse> requestAssociateRoute(CloudFoundryClient cloudFoundryClient, String applicationId, String routeId) { return cloudFoundryClient.applicationsV2() .associateRoute(AssociateApplicationRouteRequest.builder() .applicationId(applicationId) .routeId(routeId) .build()); } private static Mono<CopyApplicationResponse> requestCopyBits(CloudFoundryClient cloudFoundryClient, String sourceApplicationId, String targetApplicationId) { return cloudFoundryClient.applicationsV2() .copy(CopyApplicationRequest.builder() .applicationId(targetApplicationId) .sourceApplicationId(sourceApplicationId) .build()); } private static Mono<CreateApplicationResponse> requestCreateApplication(CloudFoundryClient cloudFoundryClient, ApplicationManifest manifest, String spaceId, String stackId) { CreateApplicationRequest.Builder builder = CreateApplicationRequest.builder() .buildpack(manifest.getBuildpack()) .command(manifest.getCommand()) .diskQuota(manifest.getDisk()) .environmentJsons(manifest.getEnvironmentVariables()) .healthCheckTimeout(manifest.getTimeout()) .healthCheckType(Optional.ofNullable(manifest.getHealthCheckType()).map(ApplicationHealthCheck::getValue).orElse(null)) .instances(manifest.getInstances()) .memory(manifest.getMemory()) .name(manifest.getName()) .spaceId(spaceId) .stackId(stackId); Optional.ofNullable(manifest.getDockerImage()) .ifPresent(dockerImage -> builder .diego(true) .dockerImage(dockerImage)); return cloudFoundryClient.applicationsV2() .create(builder.build()); } private static Mono<CreateRouteResponse> requestCreateRoute(CloudFoundryClient cloudFoundryClient, String domainId, String host, String routePath, String spaceId) { return cloudFoundryClient.routes() .create(org.cloudfoundry.client.v2.routes.CreateRouteRequest.builder() .domainId(domainId) .host(host) .path(routePath) .spaceId(spaceId) .build()); } private static Mono<CreateServiceBindingResponse> requestCreateServiceBinding(CloudFoundryClient cloudFoundryClient, String applicationId, String serviceInstanceId) { return cloudFoundryClient.serviceBindingsV2() .create(CreateServiceBindingRequest.builder() .applicationId(applicationId) .serviceInstanceId(serviceInstanceId) .build()); } private static Mono<CreateRouteResponse> requestCreateTcpRoute(CloudFoundryClient cloudFoundryClient, String domainId, String spaceId) { return cloudFoundryClient.routes() .create(org.cloudfoundry.client.v2.routes.CreateRouteRequest.builder() .domainId(domainId) .generatePort(true) .spaceId(spaceId) .build()); } private static Mono<CreateRouteResponse> requestCreateTcpRoute(CloudFoundryClient cloudFoundryClient, String domainId, Integer port, String spaceId) { return cloudFoundryClient.routes() .create(org.cloudfoundry.client.v2.routes.CreateRouteRequest.builder() .domainId(domainId) .port(port) .spaceId(spaceId) .build()); } private static Mono<Void> requestDeleteApplication(CloudFoundryClient cloudFoundryClient, String applicationId) { return cloudFoundryClient.applicationsV2() .delete(org.cloudfoundry.client.v2.applications.DeleteApplicationRequest.builder() .applicationId(applicationId) .build()); } private static Mono<DeleteRouteResponse> requestDeleteRoute(CloudFoundryClient cloudFoundryClient, String routeId) { return cloudFoundryClient.routes() .delete(DeleteRouteRequest.builder() .async(true) .routeId(routeId) .build()); } private static Flux<EventResource> requestEvents(String applicationId, CloudFoundryClient cloudFoundryClient) { return PaginationUtils .requestClientV2Resources(page -> cloudFoundryClient.events() .list(ListEventsRequest.builder() .actee(applicationId) .orderDirection(OrderDirection.DESCENDING) .resultsPerPage(50) .page(page) .build())); } private static Mono<AbstractApplicationResource> requestGetApplication(CloudFoundryClient cloudFoundryClient, String applicationId) { return cloudFoundryClient.applicationsV2() .get(org.cloudfoundry.client.v2.applications.GetApplicationRequest.builder() .applicationId(applicationId) .build()) .cast(AbstractApplicationResource.class); } private static Flux<PrivateDomainResource> requestListPrivateDomains(CloudFoundryClient cloudFoundryClient, String organizationId) { return PaginationUtils .requestClientV2Resources(page -> cloudFoundryClient.organizations() .listPrivateDomains(ListOrganizationPrivateDomainsRequest.builder() .organizationId(organizationId) .page(page) .build())); } private static Flux<ServiceBindingResource> requestListServiceBindings(CloudFoundryClient cloudFoundryClient, String applicationId) { return PaginationUtils .requestClientV2Resources(page -> cloudFoundryClient.applicationsV2() .listServiceBindings(ListApplicationServiceBindingsRequest.builder() .applicationId(applicationId) .page(page) .build())); } private static Flux<UnionServiceInstanceResource> requestListServiceInstances(CloudFoundryClient cloudFoundryClient, String serviceInstanceName, String spaceId) { return PaginationUtils .requestClientV2Resources(page -> cloudFoundryClient.spaces() .listServiceInstances(ListSpaceServiceInstancesRequest.builder() .page(page) .returnUserProvidedServiceInstances(true) .name(serviceInstanceName) .spaceId(spaceId) .build())); } private static Flux<SharedDomainResource> requestListSharedDomains(CloudFoundryClient cloudFoundryClient) { return PaginationUtils .requestClientV2Resources(page -> cloudFoundryClient.sharedDomains() .list(ListSharedDomainsRequest.builder() .page(page) .build())); } private static Flux<Envelope> requestLogsRecent(Mono<DopplerClient> dopplerClient, String applicationId) { return dopplerClient .flatMapMany(client -> client .recentLogs(RecentLogsRequest.builder() .applicationId(applicationId) .build())); } private static Flux<Envelope> requestLogsStream(Mono<DopplerClient> dopplerClient, String applicationId) { return dopplerClient .flatMapMany(client -> client .stream(StreamRequest.builder() .applicationId(applicationId) .build())); } private static Flux<SpaceResource> requestOrganizationSpacesByName(CloudFoundryClient cloudFoundryClient, String organizationId, String space) { return PaginationUtils .requestClientV2Resources(page -> cloudFoundryClient.organizations() .listSpaces(ListOrganizationSpacesRequest.builder() .name(space) .organizationId(organizationId) .page(page) .build())); } private static Flux<OrganizationResource> requestOrganizations(CloudFoundryClient cloudFoundryClient, String organization) { return PaginationUtils .requestClientV2Resources(page -> cloudFoundryClient.organizations() .list(ListOrganizationsRequest.builder() .name(organization) .page(page) .build())); } private static Mono<Void> requestRemoveRouteFromApplication(CloudFoundryClient cloudFoundryClient, String applicationId, String routeId) { return cloudFoundryClient.applicationsV2() .removeRoute(RemoveApplicationRouteRequest.builder() .applicationId(applicationId) .routeId(routeId) .build()); } private static Mono<Void> requestRemoveServiceBinding(CloudFoundryClient cloudFoundryClient, String applicationId, String serviceBindingId) { return cloudFoundryClient.applicationsV2() .removeServiceBinding(RemoveApplicationServiceBindingRequest.builder() .applicationId(applicationId) .serviceBindingId(serviceBindingId) .build()); } private static Mono<RestageApplicationResponse> requestRestageApplication(CloudFoundryClient cloudFoundryClient, String applicationId) { return cloudFoundryClient.applicationsV2() .restage(org.cloudfoundry.client.v2.applications.RestageApplicationRequest.builder() .applicationId(applicationId) .build()); } private static Flux<RouteResource> requestRoutes(CloudFoundryClient cloudFoundryClient, String domainId, String host, Integer port, String routePath) { ListRoutesRequest.Builder requestBuilder = ListRoutesRequest.builder() .domainId(domainId); Optional.ofNullable(host).ifPresent(requestBuilder::host); Optional.ofNullable(routePath).ifPresent(requestBuilder::path); Optional.ofNullable(port).ifPresent(requestBuilder::port); return PaginationUtils .requestClientV2Resources(page -> cloudFoundryClient.routes() .list(requestBuilder .page(page) .build())); } private static Flux<SharedDomainResource> requestSharedDomains(CloudFoundryClient cloudFoundryClient) { return PaginationUtils .requestClientV2Resources(page -> cloudFoundryClient.sharedDomains() .list(ListSharedDomainsRequest.builder() .page(page) .build())); } private static Mono<GetSpaceResponse> requestSpace(CloudFoundryClient cloudFoundryClient, String spaceId) { return cloudFoundryClient.spaces() .get(GetSpaceRequest.builder() .spaceId(spaceId) .build()); } private static Mono<GetSpaceSummaryResponse> requestSpaceSummary(CloudFoundryClient cloudFoundryClient, String spaceId) { return cloudFoundryClient.spaces() .getSummary(GetSpaceSummaryRequest.builder() .spaceId(spaceId) .build()); } private static Mono<GetStackResponse> requestStack(CloudFoundryClient cloudFoundryClient, String stackId) { return cloudFoundryClient.stacks() .get(GetStackRequest.builder() .stackId(stackId) .build()); } private static Flux<StackResource> requestStacks(CloudFoundryClient cloudFoundryClient, String stack) { return PaginationUtils .requestClientV2Resources(page -> cloudFoundryClient.stacks() .list(ListStacksRequest.builder() .page(page) .name(stack) .build())); } private static Mono<Void> requestTerminateApplicationInstance(CloudFoundryClient cloudFoundryClient, String applicationId, String instanceIndex) { return cloudFoundryClient.applicationsV2() .terminateInstance(TerminateApplicationInstanceRequest.builder() .applicationId(applicationId) .index(instanceIndex) .build()); } private static Mono<AbstractApplicationResource> requestUpdateApplication(CloudFoundryClient cloudFoundryClient, String applicationId, Map<String, Object> environmentJsons, ApplicationManifest manifest, String stackId) { return requestUpdateApplication(cloudFoundryClient, applicationId, builder -> { builder .buildpack(manifest.getBuildpack()) .command(manifest.getCommand()) .diskQuota(manifest.getDisk()) .environmentJsons(environmentJsons) .healthCheckTimeout(manifest.getTimeout()) .healthCheckType(Optional.ofNullable(manifest.getHealthCheckType()).map(ApplicationHealthCheck::getValue).orElse(null)) .instances(manifest.getInstances()) .memory(manifest.getMemory()) .name(manifest.getName()) .stackId(stackId); Optional.ofNullable(manifest.getDockerImage()) .ifPresent(dockerImage -> builder .diego(true) .dockerImage(dockerImage)); return builder; }); } private static Mono<AbstractApplicationResource> requestUpdateApplication(CloudFoundryClient cloudFoundryClient, String applicationId, UnaryOperator<UpdateApplicationRequest.Builder> modifier) { return cloudFoundryClient.applicationsV2() .update(modifier.apply(UpdateApplicationRequest.builder() .applicationId(applicationId)) .build()) .cast(AbstractApplicationResource.class); } private static Mono<AbstractApplicationResource> requestUpdateApplicationEnvironment(CloudFoundryClient cloudFoundryClient, String applicationId, Map<String, Object> environment) { return requestUpdateApplication(cloudFoundryClient, applicationId, builder -> builder.environmentJsons(environment)); } private static Mono<AbstractApplicationResource> requestUpdateApplicationHealthCheckType(CloudFoundryClient cloudFoundryClient, String applicationId, ApplicationHealthCheck type) { return requestUpdateApplication(cloudFoundryClient, applicationId, builder -> builder.healthCheckType(type.getValue())); } private static Mono<AbstractApplicationResource> requestUpdateApplicationName(CloudFoundryClient cloudFoundryClient, String applicationId, String name) { return requestUpdateApplication(cloudFoundryClient, applicationId, builder -> builder.name(name)); } private static Mono<AbstractApplicationResource> requestUpdateApplicationScale(CloudFoundryClient cloudFoundryClient, String applicationId, Integer disk, Integer instances, Integer memory) { return requestUpdateApplication(cloudFoundryClient, applicationId, builder -> builder.diskQuota(disk).instances(instances).memory(memory)); } private static Mono<AbstractApplicationResource> requestUpdateApplicationSsh(CloudFoundryClient cloudFoundryClient, String applicationId, Boolean enabled) { return requestUpdateApplication(cloudFoundryClient, applicationId, builder -> builder.enableSsh(enabled)); } private static Mono<AbstractApplicationResource> requestUpdateApplicationState(CloudFoundryClient cloudFoundryClient, String applicationId, String state) { return requestUpdateApplication(cloudFoundryClient, applicationId, builder -> builder.state(state)); } private static Mono<UploadApplicationResponse> requestUploadApplication(CloudFoundryClient cloudFoundryClient, String applicationId, Path application, List<ResourceMatchingUtils .ArtifactMetadata> matchedResources) { UploadApplicationRequest request = matchedResources.stream() .reduce(UploadApplicationRequest.builder() .application(application) .applicationId(applicationId) .async(true), (builder, artifactMetadata) -> builder.resource(org.cloudfoundry.client.v2.applications.Resource.builder() .hash(artifactMetadata.getHash()) .mode(artifactMetadata.getPermissions()) .path(artifactMetadata.getPath()) .size(artifactMetadata.getSize()) .build()), (a, b) -> a) .build(); return cloudFoundryClient.applicationsV2() .upload(request); } private static Mono<Void> restageApplication(CloudFoundryClient cloudFoundryClient, String application, String applicationId, Duration stagingTimeout, Duration startupTimeout) { return requestRestageApplication(cloudFoundryClient, applicationId) .then(response -> waitForStaging(cloudFoundryClient, application, applicationId, stagingTimeout)) .then(waitForRunning(cloudFoundryClient, application, applicationId, startupTimeout)); } private static Mono<Void> restartApplication(CloudFoundryClient cloudFoundryClient, String application, String applicationId, Duration stagingTimeout, Duration startupTimeout) { return stopApplication(cloudFoundryClient, applicationId) .then(startApplicationAndWait(cloudFoundryClient, application, applicationId, stagingTimeout, startupTimeout)); } private static Predicate<AbstractApplicationResource> sshEnabled(Boolean enabled) { return resource -> enabled.equals(ResourceUtils.getEntity(resource).getEnableSsh()); } private static Mono<Void> startApplicationAndWait(CloudFoundryClient cloudFoundryClient, String application, String applicationId, Duration stagingTimeout, Duration startupTimeout) { return requestUpdateApplicationState(cloudFoundryClient, applicationId, STARTED_STATE) .then(response -> waitForStaging(cloudFoundryClient, application, applicationId, stagingTimeout)) .then(waitForRunning(cloudFoundryClient, application, applicationId, startupTimeout)); } private static Mono<Void> stopAndStartApplication(CloudFoundryClient cloudFoundryClient, String applicationId, String name, PushApplicationManifestRequest request) { return stopApplication(cloudFoundryClient, applicationId) .filter(resource -> !Optional.ofNullable(request.getNoStart()).orElse(false)) .then(resource -> startApplicationAndWait(cloudFoundryClient, name, applicationId, request.getStagingTimeout(), request.getStartupTimeout())); } private static Mono<AbstractApplicationResource> stopApplication(CloudFoundryClient cloudFoundryClient, String applicationId) { return requestUpdateApplicationState(cloudFoundryClient, applicationId, STOPPED_STATE); } private static Mono<AbstractApplicationResource> stopApplicationIfNotStopped(CloudFoundryClient cloudFoundryClient, AbstractApplicationResource resource) { return isNotIn(resource, STOPPED_STATE) ? stopApplication(cloudFoundryClient, ResourceUtils.getId(resource)) : Mono.just(resource); } private static ApplicationDetail toApplicationDetail(SummaryApplicationResponse summaryApplicationResponse, GetStackResponse getStackResponse, List<InstanceDetail> instanceDetails, List<String> urls) { return ApplicationDetail.builder() .buildpack(getBuildpack(summaryApplicationResponse)) .diskQuota(summaryApplicationResponse.getDiskQuota()) .id(summaryApplicationResponse.getId()) .instanceDetails(instanceDetails) .instances(summaryApplicationResponse.getInstances()) .lastUploaded(toDate(summaryApplicationResponse.getPackageUpdatedAt())) .memoryLimit(summaryApplicationResponse.getMemory()) .name(summaryApplicationResponse.getName()) .requestedState(summaryApplicationResponse.getState()) .runningInstances(summaryApplicationResponse.getRunningInstances()) .stack(getStackResponse.getEntity().getName()) .urls(urls) .build(); } private static ApplicationEnvironments toApplicationEnvironments(ApplicationEnvironmentResponse response) { return ApplicationEnvironments.builder() .running(response.getRunningEnvironmentJsons()) .staging(response.getStagingEnvironmentJsons()) .systemProvided(response.getSystemEnvironmentJsons()) .userProvided(response.getEnvironmentJsons()) .build(); } private static Mono<ApplicationManifest> toApplicationManifest(SummaryApplicationResponse response, String stackName) { ApplicationManifest.Builder manifestBuilder = ApplicationManifest.builder() .buildpack(response.getBuildpack()) .command(response.getCommand()) .disk(response.getDiskQuota()) .environmentVariables(response.getEnvironmentJsons()) .healthCheckHttpEndpoint(response.getHealthCheckHttpEndpoint()) .healthCheckType(ApplicationHealthCheck.from(response.getHealthCheckType())) .instances(response.getInstances()) .memory(response.getMemory()) .name(response.getName()) .stack(stackName) .timeout(response.getHealthCheckTimeout()); for (org.cloudfoundry.client.v2.routes.Route route : Optional.ofNullable(response.getRoutes()).orElse(Collections.emptyList())) { StringBuilder sb = new StringBuilder(); Optional.ofNullable(route.getHost()) .ifPresent(host -> sb.append(host).append(".")); Optional.ofNullable(route.getDomain().getName()) .ifPresent(sb::append); if (route.getPort() == null) { Optional.ofNullable(route.getPath()) .ifPresent(sb::append); } else { sb.append(":").append(route.getPort()); } manifestBuilder.route(Route.builder() .route(sb.toString()) .build()); } if (Optional.ofNullable(response.getRoutes()).orElse(Collections.emptyList()).isEmpty()) { manifestBuilder.noRoute(true); } for (ServiceInstance service : Optional.ofNullable(response.getServices()).orElse(Collections.emptyList())) { Optional.ofNullable(service.getName()).ifPresent(manifestBuilder::service); } return Mono .just(manifestBuilder .build()); } private static ApplicationSummary toApplicationSummary(SpaceApplicationSummary spaceApplicationSummary) { return ApplicationSummary.builder() .diskQuota(spaceApplicationSummary.getDiskQuota()) .id(spaceApplicationSummary.getId()) .instances(spaceApplicationSummary.getInstances()) .memoryLimit(spaceApplicationSummary.getMemory()) .name(spaceApplicationSummary.getName()) .requestedState(spaceApplicationSummary.getState()) .runningInstances(spaceApplicationSummary.getRunningInstances()) .urls(spaceApplicationSummary.getUrls()) .build(); } private static Date toDate(String date) { return date == null ? null : DateUtils.parseFromIso8601(date); } private static Date toDate(Double date) { return date == null ? null : DateUtils.parseSecondsFromEpoch(date); } private static DomainSummary toDomain(SharedDomainResource resource) { SharedDomainEntity entity = ResourceUtils.getEntity(resource); return DomainSummary.builder() .id(ResourceUtils.getId(resource)) .name(entity.getName()) .type(entity.getRouterGroupType()) .build(); } private static DomainSummary toDomain(PrivateDomainResource resource) { PrivateDomainEntity entity = ResourceUtils.getEntity(resource); return DomainSummary.builder() .id(ResourceUtils.getId(resource)) .name(entity.getName()) .build(); } private static ApplicationHealthCheck toHealthCheck(AbstractApplicationResource resource) { String type = resource.getEntity().getHealthCheckType(); if (ApplicationHealthCheck.NONE.getValue().equals(type)) { return ApplicationHealthCheck.NONE; } else if (ApplicationHealthCheck.PORT.getValue().equals(type)) { return ApplicationHealthCheck.PORT; } else { return null; } } private static InstanceDetail toInstanceDetail(Map.Entry<String, ApplicationInstanceInfo> entry, ApplicationStatisticsResponse statisticsResponse) { InstanceStatistics instanceStatistics = Optional.ofNullable(statisticsResponse.getInstances().get(entry.getKey())).orElse(emptyInstanceStats()); Statistics stats = Optional.ofNullable(instanceStatistics.getStatistics()).orElse(emptyApplicationStatistics()); Usage usage = Optional.ofNullable(stats.getUsage()).orElse(emptyApplicationUsage()); return InstanceDetail.builder() .index(entry.getKey()) .state(entry.getValue().getState()) .since(toDate(entry.getValue().getSince())) .cpu(usage.getCpu()) .memoryUsage(usage.getMemory()) .diskUsage(usage.getDisk()) .diskQuota(stats.getDiskQuota()) .memoryQuota(stats.getMemoryQuota()) .build(); } private static Mono<List<InstanceDetail>> toInstanceDetailList(ApplicationInstancesResponse instancesResponse, ApplicationStatisticsResponse statisticsResponse) { return Flux .fromIterable(instancesResponse.getInstances().entrySet()) .map(entry -> toInstanceDetail(entry, statisticsResponse)) .collectList(); } private static String toUrl(org.cloudfoundry.client.v2.routes.Route route) { String hostName = route.getHost(); String domainName = route.getDomain().getName(); return hostName.isEmpty() ? domainName : String.format("%s.%s", hostName, domainName); } private static Mono<List<String>> toUrls(List<org.cloudfoundry.client.v2.routes.Route> routes) { return Flux .fromIterable(routes) .map(DefaultApplications::toUrl) .collectList(); } private static Mono<Void> uploadApplicationAndWait(CloudFoundryClient cloudFoundryClient, String applicationId, Path application, List<ResourceMatchingUtils.ArtifactMetadata> matchedResources, Duration stagingTimeout) { return Mono .defer(() -> { if (matchedResources.isEmpty()) { return requestUploadApplication(cloudFoundryClient, applicationId, application, matchedResources); } else { List<String> paths = matchedResources.stream() .map(ResourceMatchingUtils.ArtifactMetadata::getPath) .collect(Collectors.toList()); return FileUtils.compress(application, p -> !paths.contains(p)) .then(filteredApplication -> requestUploadApplication(cloudFoundryClient, applicationId, filteredApplication, matchedResources) .doOnTerminate((v, t) -> { try { Files.delete(filteredApplication); } catch (IOException e) { throw Exceptions.propagate(e); } })); } }) .then(job -> JobUtils.waitForCompletion(cloudFoundryClient, stagingTimeout, job)); } private static Mono<Void> waitForRunning(CloudFoundryClient cloudFoundryClient, String application, String applicationId, Duration startupTimeout) { Duration timeout = Optional.ofNullable(startupTimeout).orElse(Duration.ofMinutes(5)); return requestApplicationInstances(cloudFoundryClient, applicationId) .flatMapMany(response -> Flux.fromIterable(response.getInstances().values())) .map(ApplicationInstanceInfo::getState) .reduce("UNKNOWN", collectStates()) .filter(isInstanceComplete()) .repeatWhenEmpty(exponentialBackOff(Duration.ofSeconds(1), Duration.ofSeconds(15), timeout)) .filter(isRunning()) .switchIfEmpty(ExceptionUtils.illegalState("Application %s failed during start", application)) .onErrorResume(DelayTimeoutException.class, t -> ExceptionUtils.illegalState("Application %s timed out during start", application)) .then(); } private static Mono<Void> waitForStaging(CloudFoundryClient cloudFoundryClient, String application, String applicationId, Duration stagingTimeout) { Duration timeout = Optional.ofNullable(stagingTimeout).orElse(Duration.ofMinutes(15)); return requestGetApplication(cloudFoundryClient, applicationId) .map(response -> ResourceUtils.getEntity(response).getPackageState()) .filter(isStagingComplete()) .repeatWhenEmpty(exponentialBackOff(Duration.ofSeconds(1), Duration.ofSeconds(15), timeout)) .filter(isStaged()) .switchIfEmpty(ExceptionUtils.illegalState("Application %s failed during staging", application)) .onErrorResume(DelayTimeoutException.class, t -> ExceptionUtils.illegalState("Application %s timed out during staging", application)) .then(); } }