/******************************************************************************* * Copyright (c) 2016, 2017 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ package org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.v2; import static org.cloudfoundry.util.tuple.TupleUtils.function; import java.io.IOException; import java.time.Duration; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.function.Function; import java.util.function.Predicate; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.applications.ApplicationEntity; import org.cloudfoundry.client.v2.applications.CreateApplicationRequest; import org.cloudfoundry.client.v2.applications.GetApplicationResponse; import org.cloudfoundry.client.v2.applications.UpdateApplicationRequest; import org.cloudfoundry.client.v2.applications.UpdateApplicationResponse; import org.cloudfoundry.client.v2.buildpacks.ListBuildpacksRequest; import org.cloudfoundry.client.v2.buildpacks.ListBuildpacksResponse; import org.cloudfoundry.client.v2.domains.DomainResource; import org.cloudfoundry.client.v2.domains.ListDomainsRequest; import org.cloudfoundry.client.v2.domains.ListDomainsResponse; import org.cloudfoundry.client.v2.info.GetInfoRequest; import org.cloudfoundry.client.v2.info.GetInfoResponse; import org.cloudfoundry.client.v2.serviceinstances.DeleteServiceInstanceRequest; import org.cloudfoundry.client.v2.stacks.GetStackRequest; import org.cloudfoundry.client.v2.stacks.GetStackResponse; import org.cloudfoundry.client.v2.userprovidedserviceinstances.DeleteUserProvidedServiceInstanceRequest; import org.cloudfoundry.doppler.LogMessage; import org.cloudfoundry.operations.CloudFoundryOperations; import org.cloudfoundry.operations.DefaultCloudFoundryOperations; import org.cloudfoundry.operations.applications.ApplicationDetail; import org.cloudfoundry.operations.applications.DeleteApplicationRequest; import org.cloudfoundry.operations.applications.GetApplicationEnvironmentsRequest; import org.cloudfoundry.operations.applications.GetApplicationRequest; import org.cloudfoundry.operations.applications.LogsRequest; import org.cloudfoundry.operations.applications.RestartApplicationRequest; import org.cloudfoundry.operations.applications.StartApplicationRequest; import org.cloudfoundry.operations.applications.StopApplicationRequest; import org.cloudfoundry.operations.organizations.OrganizationDetail; import org.cloudfoundry.operations.organizations.OrganizationInfoRequest; import org.cloudfoundry.operations.organizations.OrganizationSummary; import org.cloudfoundry.operations.routes.Level; import org.cloudfoundry.operations.routes.ListRoutesRequest; import org.cloudfoundry.operations.routes.MapRouteRequest; import org.cloudfoundry.operations.routes.UnmapRouteRequest; import org.cloudfoundry.operations.services.BindServiceInstanceRequest; import org.cloudfoundry.operations.services.CreateServiceInstanceRequest; import org.cloudfoundry.operations.services.CreateUserProvidedServiceInstanceRequest; import org.cloudfoundry.operations.services.GetServiceInstanceRequest; import org.cloudfoundry.operations.services.ServiceInstance; import org.cloudfoundry.operations.services.ServiceInstanceSummary; import org.cloudfoundry.operations.services.UnbindServiceInstanceRequest; import org.cloudfoundry.operations.spaces.GetSpaceRequest; import org.cloudfoundry.operations.spaces.SpaceDetail; import org.cloudfoundry.reactor.ConnectionContext; import org.cloudfoundry.reactor.tokenprovider.AbstractUaaTokenProvider; import org.cloudfoundry.uaa.UaaClient; import org.cloudfoundry.util.PaginationUtils; import org.eclipse.core.runtime.Platform; import org.osgi.framework.Version; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.ApplicationRunningStateTracker; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFApplication; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFApplicationDetail; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFBuildpack; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFClientParams; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFCloudDomain; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFCredentials; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFServiceInstance; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFSpace; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFStack; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.ClientRequests; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.SshClientSupport; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.SshHost; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.v1.DefaultClientRequestsV1; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.v2.CloudFoundryClientCache.CFClientProvider; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.console.IApplicationLogConsole; import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens; import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens.CancelationToken; import org.springframework.ide.eclipse.boot.util.Log; import org.springframework.ide.eclipse.editor.support.util.StringUtil; import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap.Builder; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import reactor.core.Cancellation; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; /** * @author Kris De Volder * @author Nieraj Singh */ public class DefaultClientRequestsV2 implements ClientRequests { private static final Duration APP_START_TIMEOUT = Duration.ofMillis(ApplicationRunningStateTracker.APP_START_TIMEOUT); private static final Duration GET_SERVICES_TIMEOUT = Duration.ofSeconds(60); private static final Duration GET_SPACES_TIMEOUT = Duration.ofSeconds(20); private static final Duration GET_USERNAME_TIMEOUT = Duration.ofSeconds(5); private static final boolean DEBUG = (""+Platform.getLocation()).contains("kdvolder") || (""+Platform.getLocation()).contains("bamboo"); // private static final boolean DEBUG_REACTOR = false;//(""+Platform.getLocation()).contains("kdvolder") //|| (""+Platform.getLocation()).contains("bamboo"); private static void debug(String string) { if (DEBUG) { System.out.println(string); } } // static { // if (DEBUG_REACTOR) { // Loggers.enableExtension(new Extension() { // @Override // public void log(String category, java.util.logging.Level level, String msg, Object... arguments) { // debug(category +"["+level + "] : "+MessageFormatter.format(msg, arguments).getMessage()); // } // }); // } // } // TODO: it would be good not to create another 'threadpool' and use something like the below code // instead so that eclipse job scheduler is used for reactor 'tasks'. However... the code below // may not be 100% correct. // private static final Callable<? extends Consumer<Runnable>> SCHEDULER_GROUP = () -> { // return (Runnable task) -> { // Job job = new Job("CF Client background task") { // @Override // protected IStatus run(IProgressMonitor monitor) { // if (task!=null) { // task.run(); // } // return Status.OK_STATUS; // } // }; // job.setRule(JobUtil.lightRule("reactor-job-rule")); // job.setSystem(true); // job.schedule(); // }; // }; private CFClientParams params; private CloudFoundryClient _client ; private UaaClient _uaa; private CloudFoundryOperations _operations; @Deprecated private DefaultClientRequestsV1 _v1; private Mono<String> orgId; private Mono<GetInfoResponse> info; private Mono<String> spaceId; private AbstractUaaTokenProvider _tokenProvider; private ConnectionContext _connection; private String refreshToken = null; public DefaultClientRequestsV2(CloudFoundryClientCache clients, CFClientParams params) { this.params = params; CFClientProvider provider = clients.getOrCreate(params.getUsername(), params.getCredentials(), params.getHost(), params.skipSslValidation()); this._client = provider.client; this._uaa = provider.uaaClient; this._tokenProvider = (AbstractUaaTokenProvider) provider.tokenProvider; this._connection = provider.connection; _tokenProvider.getRefreshTokens(_connection).doOnNext((t) -> this.refreshToken = t).subscribe(); debug(">>> creating cf operations"); this._operations = DefaultCloudFoundryOperations.builder() .cloudFoundryClient(_client) .dopplerClient(provider.doppler) .uaaClient(provider.uaaClient) .organization(params.getOrgName()) .space(params.getSpaceName()) .build(); debug("<<< creating cf operations"); this.orgId = getOrgId(); this.spaceId = getSpaceId(); this.info = client_getInfo().cache(); } private Mono<CloudFoundryOperations> client_createOperations(OrganizationSummary org) { return log("client.createOperations(org="+org.getName()+")", Mono.fromCallable(() -> DefaultCloudFoundryOperations.builder() .cloudFoundryClient(_client) .organization(org.getName()) .build() ) ); } private Mono<String> getOrgId() { String orgName = params.getOrgName(); if (orgName==null) { return Mono.error(new IOException("No organization targetted")); } else { return operations_getOrgId().cache(); } } private Mono<String> getSpaceId() { String spaceName = params.getSpaceName(); if (spaceName==null) { return Mono.error(new IOException("No space targetted")); } else { return _operations.spaces().get(GetSpaceRequest.builder() .name(params.getSpaceName()) .build() ) .map(SpaceDetail::getId) .cache(); } } @Override public List<CFApplication> getApplicationsWithBasicInfo() throws Exception { return ReactorUtils.get(operations_listApps()); } private ApplicationExtras getApplicationExtras(String appName) { //Stuff used in computing the 'extras'... Mono<UUID> appIdMono = getApplicationId(appName); Mono<ApplicationEntity> entity = appIdMono .then((appId) -> client_getApplication(appId) ) .map((appResource) -> appResource.getEntity()) .cache(); //The stuff returned from the getters of 'extras'... Mono<List<String>> services = prefetch("services", getBoundServicesList(appName)); Mono<Map<String, String>> env = prefetch("env", DefaultClientRequestsV2.this.getEnv(appName) ); Mono<String> buildpack = prefetch("buildpack", entity.then((e) -> Mono.justOrEmpty(e.getBuildpack())) ); Mono<String> stack = prefetch("stack", entity.then((e) -> Mono.justOrEmpty(e.getStackId())) .then((stackId) -> { return client_getStack(stackId); }).map((response) -> { return response.getEntity().getName(); }) ); Mono<Integer> timeout = prefetch("timeout", entity .then((v) -> Mono.justOrEmpty(v.getHealthCheckTimeout())) ); Mono<String> command = prefetch("command", entity.then((e) -> Mono.justOrEmpty(e.getCommand())) ); Mono<String> healthCheckType = prefetch("healthCheckType", entity.then((e) -> Mono.justOrEmpty(e.getHealthCheckType())) ); return new ApplicationExtras() { @Override public Mono<List<String>> getServices() { return services; } @Override public Mono<Map<String, String>> getEnv() { return env; } @Override public Mono<String> getBuildpack() { return buildpack; } @Override public Mono<String> getStack() { return stack; } public Mono<Integer> getTimeout() { return timeout; } @Override public Mono<String> getCommand() { return command; } @Override public Mono<String> getHealthCheckType() { return healthCheckType; } }; } private <T> Mono<T> prefetch(String id, Mono<T> toFetch) { return toFetch // .log(id + " before error handler") .otherwise((error) -> { Log.log(new IOException("Failed prefetch '"+id+"'", error)); return Mono.empty(); }) // .log(id + " after error handler") .cache() // .log(id + "after cache") ; } // private <T> Mono<T> prefetch(Mono<T> toFetch) { // Mono<T> result = toFetch // .cache(); // It should only be fetched once. // // //We must ensure the 'result' is being consumed by something to force its execution: // result // .publishOn(SCHEDULER_GROUP) //Ensure the consume is truly async or it may block here. // .consume((dont_care) -> {}); // // return result; // } @Override public List<CFServiceInstance> getServices() throws Exception { return ReactorUtils.get(GET_SERVICES_TIMEOUT, CancelationTokens.NULL, log("operations.services.listInstances()", _operations .services() .listInstances() .flatMap(this::getServiceDetails) .map(CFWrappingV2::wrap) .collectList() .map(ImmutableList::copyOf) ) ); } private Mono<ServiceInstance> getServiceDetails(ServiceInstanceSummary summary) { return log("operations.service.getServiceInstance", _operations.services().getInstance(GetServiceInstanceRequest.builder() .name(summary.getName()) .build() ) ); } /** * Get details for a given list of applications. This does a 'best' effort getting the details for * as many apps as possible but it does not guarantee that it will return details for each app in the * list. This is to avoid one 'bad apple' from spoiling the whole batch. (I.e if failing to fetch details for * some apps we can still return details for the others rather than throw an exception). */ @Override public Flux<CFApplicationDetail> getApplicationDetails(List<CFApplication> appsToLookUp) throws Exception { return Flux.fromIterable(appsToLookUp) .flatMap((CFApplication appSummary) -> { return getApplicationDetail(appSummary.getName()) .otherwise((error) -> { Log.log(ExceptionUtil.coreException("getting application details for '"+appSummary.getName()+"' failed", error)); return Mono.empty(); }) .map((ApplicationDetail appDetails) -> CFWrappingV2.wrap((CFApplicationSummaryData)appSummary, appDetails)); }); } @Override public Cancellation streamLogs(String appName, IApplicationLogConsole logConsole) throws Exception { Flux<LogMessage> stream = log("operations.applications.logs()", _operations.applications() .logs(LogsRequest.builder() .name(appName) // BUG: show recent appears to throw exception with PWS. May be fixed in the future, but now only "pure" streaming is supported .recent(false) .build() ) ) .retryWhen(retryInterval(Duration.ofMillis(500), Duration.ofMinutes(1))) ; Cancellation cancellation = ReactorUtils.sort( stream, (m1, m2) -> Long.compare(m1.getTimestamp(), m2.getTimestamp()), Duration.ofSeconds(1) ) .subscribe(logConsole::onMessage, logConsole::onError); return cancellation; // ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); // try { // // // TODO: Retain this from old code. Not sure what bug it addresses // Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader()); // // } catch (Exception e) { // BootDashActivator.log(e); // } finally { // Thread.currentThread().setContextClassLoader(contextClassLoader); // } } /** * Creates a retry 'signal factory' to be used with Flux.retryWhen. * <p> * @param timeBetween How much time to wait before retrying after a failure * @param duration If this much time has elapsed when the error happens there will be no further retries. * @return Functon that can be passed to retryWhen. */ private Function<Flux<Throwable>, Flux<Long>> retryInterval(Duration timeBetween, Duration duration) { Predicate<Throwable> falseAfterDuration = falseAfter(duration); return (errors) -> { return errors.flatMap((error) -> { if (falseAfterDuration.test(error)) { return Mono.delay(timeBetween); } else { return Mono.error(error); } }); }; } private Predicate<Throwable> falseAfter(Duration timeToWait) { return new Predicate<Throwable>() { private Long firstCalledAt; @Override public boolean test(Throwable t) { if (firstCalledAt==null) { firstCalledAt = System.currentTimeMillis(); } long waitedTime = System.currentTimeMillis() - firstCalledAt; //debug("falseAfter: remaining = "+(timeToWait.toMillis() - waitedTime)); return waitedTime < timeToWait.toMillis(); } }; } @Override public void stopApplication(String appName) throws Exception { ReactorUtils.get( stopApp(appName) ); } private Mono<Void> stopApp(String appName) { return log("operations.applications.stop(name="+appName+")", _operations.applications().stop(StopApplicationRequest.builder() .name(appName) .build() ) ); } @Override public void restartApplication(String appName, CancelationToken cancelationToken) throws Exception { ReactorUtils.get(APP_START_TIMEOUT, cancelationToken, restartApp(appName) ); } private Mono<Void> restartApp(String appName) { return log("operations.applications().restart(name="+appName+")", _operations.applications().restart(RestartApplicationRequest.builder() .name(appName) .build()) ); } @Override public void logout() { _operations = null; _client = null; if (_v1!=null) { _v1.logout(); _v1 = null; } } public boolean isLoggedOut() { return _client==null; } @Override public List<CFStack> getStacks() throws Exception { return ReactorUtils.get( log("operations.stacks().list()", _operations.stacks() .list() .map(CFWrappingV2::wrap) .collectList() ) ); } @Override public SshClientSupport getSshClientSupport() throws Exception { return new SshClientSupport() { @Override public String getSshUser(UUID appGuid, int instance) { return "cf:"+appGuid+"/" + instance; } @Override public String getSshUser(String appName, int instance) throws Exception { return ReactorUtils.get( getApplicationId(appName) .map((guid) -> getSshUser(guid, instance)) ); } @Override public SshHost getSshHost() throws Exception { return ReactorUtils.get( info.then((i) -> { String fingerPrint = i.getApplicationSshHostKeyFingerprint(); String host = i.getApplicationSshEndpoint(); int port = 22; //Default ssh port if (host!=null) { if (host.contains(":")) { String[] pieces = host.split(":"); host = pieces[0]; port = Integer.parseInt(pieces[1]); } } if (host!=null) { return Mono.just(new SshHost(host, port, fingerPrint)); } // Workaround for bug in Eclipse Neon.1 JDT cannot properly infer type SshHost. // Works in Mars. Returning Mono.empty() results in compilation error in Neon.1 return Mono.<SshHost>empty(); }) ); } @Override public String getSshCode() throws Exception { return ReactorUtils.get( log("operations.advanced.sshCode()", _operations.advanced().sshCode() ) ); } }; } private Mono<CloudFoundryOperations> operationsFor(OrganizationSummary org) { return client_createOperations(org); } @Override public List<CFSpace> getSpaces() throws Exception { Object it = ReactorUtils.get(GET_SPACES_TIMEOUT, log("operations.organizations().list()", _operations.organizations() .list() ) .flatMap((OrganizationSummary org) -> { return operationsFor(org).flatMap((operations) -> log("operations.spaces.list(org="+org.getId()+")", operations .spaces() .list() .map((space) -> CFWrappingV2.wrap(org, space)) ) ); }) .collectList() ); //workaround eclipse jdt bug: https://bugs.eclipse.org/bugs/show_bug.cgi?id=501949 return (List<CFSpace>) it; } @Override public String getHealthCheck(UUID appGuid) throws Exception { //XXX CF V2: getHealthcheck (via operations API) // See: https://www.pivotaltracker.com/story/show/116462215 return ReactorUtils.get( client_getApplication(appGuid) .map((response) -> response.getEntity().getHealthCheckType()) ); } @Override public void setHealthCheck(UUID guid, String hcType) throws Exception { //XXX CF V2: setHealthCheck (via operations API) // See: https://www.pivotaltracker.com/story/show/116462369 ReactorUtils.get( client_setHealthCheck(guid, hcType) ); } @Override public List<CFCloudDomain> getDomains() throws Exception { //XXX CF V2: list domains using 'operations' api. return ReactorUtils.get(Duration.ofMinutes(2), orgId.flatMap(this::requestDomains) .map(CFWrappingV2::wrap) .collectList() ); } private Flux<DomainResource> requestDomains(String orgId) { return PaginationUtils.requestClientV2Resources((page) -> client_listDomains(page) ); } @Override public List<CFBuildpack> getBuildpacks() throws Exception { //XXX CF V2: getBuilpacks using 'operations' API. return ReactorUtils.get( PaginationUtils.requestClientV2Resources((page) -> { return client_listBuildpacks(page); }) .map(CFWrappingV2::wrap) .collectList() ); } @Override public CFApplicationDetail getApplication(String appName) throws Exception { return ReactorUtils.get( getApplicationMono(appName) // .log("getApplication("+appName+")") ); } private Mono<CFApplicationDetail> getApplicationMono(String appName) { return getApplicationDetail(appName) .map((appDetail) -> { //TODO: we have 'real' appdetails now so we could get most of the 'application extras' info from that. return CFWrappingV2.wrap(appDetail, getApplicationExtras(appName)); }) .otherwise(ReactorUtils.suppressException(IllegalArgumentException.class)); } @Override public Version getApiVersion() throws Exception { return ReactorUtils.get(info .map((i) -> new Version(i.getApiVersion())) ); } @Override public Version getSupportedApiVersion() { return new Version(CloudFoundryClient.SUPPORTED_API_VERSION); } @Override public void deleteApplication(String appName) throws Exception { ReactorUtils.get( log("operations.applications().delete(name="+appName+")", _operations.applications().delete(DeleteApplicationRequest .builder() .name(appName) .build() ) ) ); } @Override public boolean applicationExists(String appName) throws Exception { return ReactorUtils.get( getApplicationMono(appName) .map((app) -> true) .otherwiseIfEmpty(Mono.just(false)) ); } public Mono<Void> ifApplicationExists(String appName, Function<ApplicationDetail, Mono<Void>> then, Mono<Void> els) throws Exception { return getApplicationDetail(appName) .map((app) -> Optional.of(app)) .otherwiseIfEmpty(Mono.just(Optional.<ApplicationDetail>empty())) .otherwise((error) -> Mono.just(Optional.<ApplicationDetail>empty())) .then((Optional<ApplicationDetail> app) -> { if (app.isPresent()) { return then.apply(app.get()); } else { return els; } }); } @Override public void push(CFPushArguments params, CancelationToken cancelationToken) throws Exception { String appName = params.getAppName(); ReactorUtils.get(APP_START_TIMEOUT, cancelationToken, ifApplicationExists(appName, ((app) -> pushExisting(app, params)), firstPush(params) ) ); } private Mono<Void> pushExisting(ApplicationDetail app, CFPushArguments params) { String appName = params.getAppName(); UUID appId = UUID.fromString(app.getId()); return updateApp(appId, params) .then(getApplicationDetail(appName)) .then((appDetail) -> { return Flux.merge( setRoutes(appDetail, params.getRoutes(), params.getRandomRoute()), bindAndUnbindServices(appName, params.getServices()) ).then(); }) .then(mono_debug("Uploading[1]...")) .then(Mono.fromCallable(() -> { debug("Uploading[2]..."); v1().uploadApplication(appName, params.getApplicationData()); debug("Uploading[2] DONE"); return "who cares"; })) .then(mono_debug("Uploading[1] DONE")) .then(params.isNoStart() ? stopApp(appName) : restartApp(appName) ); } private DefaultClientRequestsV1 v1() throws Exception { if (_v1==null) { CFClientParams v1params = new CFClientParams( params.getApiUrl(), params.getUsername(), CFCredentials.fromRefreshToken(getRefreshToken()), params.isSelfsigned(), params.getOrgName(), params.getSpaceName(), params.skipSslValidation() ); _v1 = new DefaultClientRequestsV1(v1params); } return _v1; } private Mono<Void> mono_debug(String string) { return Mono.fromRunnable(() -> debug(string)); } private Mono<Void> firstPush(CFPushArguments params) { String appName = params.getAppName(); return createApp(params) .then(getApplicationDetail(appName)) .then((appDetail) -> Flux.merge( setRoutes(appDetail, params.getRoutes(), params.getRandomRoute()), bindAndUnbindServices(appName, params.getServices()) ).then() ) .then(Mono.fromCallable(() -> { v1().uploadApplication(appName, params.getApplicationData()); return "who cares"; })) .then(params.isNoStart() ? Mono.empty() : startApp(appName) ) .then(); } private Mono<UUID> createApp(CFPushArguments params) { return spaceId.and(getStackId(params)) .then(function((spaceId, stackId) -> { CreateApplicationRequest req = CreateApplicationRequest.builder() .spaceId(spaceId) .stackId(stackId.orElse(null)) .name(params.getAppName()) .memory(params.getMemory()) .diskQuota(params.getDiskQuota()) .healthCheckType(params.getHealthCheckType()) .healthCheckTimeout(params.getTimeout()) .buildpack(params.getBuildpack()) .command(params.getCommand()) .environmentJsons(params.getEnv()) .instances(params.getInstances()) .build(); return log("client.applications.create("+req+")", _client.applicationsV2().create(req) ); })) .map(response -> UUID.fromString(response.getMetadata().getId())); } private Mono<Optional<String>> getStackId(CFPushArguments params) { String stackName = params.getStack(); if (stackName==null) { return Mono.just(Optional.empty()); } else { return log("operations.stacks.get("+stackName+")", _operations.stacks().get(org.cloudfoundry.operations.stacks.GetStackRequest.builder() .name(stackName) .build() ) .map((stack) -> Optional.of(stack.getId())) ); } } private Mono<UUID> updateApp(UUID appId, CFPushArguments params) { return getStackId(params) .then((stackId) -> { UpdateApplicationRequest req = UpdateApplicationRequest.builder() .applicationId(appId.toString()) .name(params.getAppName()) .memory(params.getMemory()) .diskQuota(params.getDiskQuota()) .healthCheckType(params.getHealthCheckType()) .healthCheckTimeout(params.getTimeout()) .buildpack(params.getBuildpack()) .command(params.getCommand()) .stackId(stackId.orElse(null)) .environmentJsons(params.getEnv()) .instances(params.getInstances()) .build(); return log("client.applications.update("+req+")", _client.applicationsV2().update(req) ); }) .then(Mono.just(appId)); } // public void pushV2(CFPushArguments params, CancelationToken cancelationToken) throws Exception { // debug("Pushing app starting: "+params.getAppName()); // //XXX CF V2: push should use 'manifest' in a future version of V2 // PushApplicationRequest pushRequest = toPushRequest(params) // .noStart(true) // .noRoute(true) // .build(); // ReactorUtils.get(APP_START_TIMEOUT, cancelationToken, // log("operations.applications().push("+pushRequest+")", // _operations.applications() // .push(pushRequest) // ) // .then(getApplicationDetail(params.getAppName())) // .then(appDetail -> { // return Flux.merge( // setRoutes(appDetail, params.getRoutes()), // setEnvVars(appDetail, params.getEnv()), // bindAndUnbindServices(params.getAppName(), params.getServices()) // ) // .then(); // }) // .then(() -> { // if (!params.isNoStart()) { // return startApp(params.getAppName()); // } else { // return Mono.empty(); // } // }) // ); // debug("Pushing app succeeded: "+params.getAppName()); // } private Mono<ApplicationDetail> getApplicationDetail(String appName) { return log("operations.applications.get(name="+appName+")", _operations.applications().get(GetApplicationRequest.builder() .name(appName) .build() ) ); } public Mono<Void> setRoutes(ApplicationDetail appDetails, Collection<String> desiredUrls, boolean randomRoute) { debug("setting routes for '"+appDetails.getName()+"': "+desiredUrls); //Carefull! It is not safe map/unnmap multiple routes in parallel. Doing so causes some of the // operations to fail, presumably because of some 'optimisitic locking' being used in the database // that keeps track of routes. //To avoid this problem we must execute all that map / unmap calls in sequence! return ReactorUtils.sequence( unmapUndesiredRoutes(appDetails.getName(), desiredUrls), mapDesiredRoutes(appDetails, desiredUrls, randomRoute) ); } public Mono<Void> setRoutes(String appName, Collection<String> desiredUrls, boolean randomRoute) { return getApplicationDetail(appName) .then(appDetails -> setRoutes(appDetails, desiredUrls, randomRoute)); } private Mono<Void> mapDesiredRoutes(ApplicationDetail appDetail, Collection<String> desiredUrls, boolean randomRoute) { Set<String> currentUrls = ImmutableSet.copyOf(appDetail.getUrls()); Mono<Set<String>> domains = getDomainNames().cache(); String appName = appDetail.getName(); debug("currentUrls = "+currentUrls); return Flux.fromIterable(desiredUrls) .flatMap((url) -> { if (currentUrls.contains(url)) { debug("skipping: "+url); return Mono.empty(); } else { debug("mapping: "+url); return mapRoute(domains, appName, url, randomRoute); } }, 1) //!!!IN SEQUENCE!!! .then(); } private Mono<Void> mapRoute(Mono<Set<String>> domains, String appName, String desiredUrl, boolean randomRoute) { debug("mapRoute: "+appName+" -> "+desiredUrl); return toRoute(domains, desiredUrl) .then((CFRoute route) -> mapRoute(appName, route, randomRoute)) .doOnError((e) -> { Log.info("mapRoute FAILED!"); Log.log(e); }); } private Mono<Void> mapRoute(String appName, CFRoute route, boolean randomRoute) { org.cloudfoundry.operations.routes.MapRouteRequest.Builder builder = MapRouteRequest.builder() .applicationName(appName); // Let the client validate if any of these combinations are correct. // However, only set these values only if they are present as not doing so causes NPE if (StringUtil.hasText(route.getDomain())) { if (isTcp(route.getDomain())) { // Can only set random port if route is TCP route builder.randomPort(randomRoute); } builder.domain(route.getDomain()); } if (StringUtil.hasText(route.getHost())) { builder.host(route.getHost()); } if (StringUtil.hasText(route.getPath())) { builder.path(route.getPath()); } if (route.getPort() != CFRoute.NO_PORT) { builder.port(route.getPort()); } MapRouteRequest mapRouteReq = builder.build(); return log("operations.routes.map("+mapRouteReq+")", _operations.routes().map(mapRouteReq) ) .then(); } private boolean isTcp(String domain) { //TODO: need to fill this in when the client can provider "RouterGroup" type of a domain return false; } private Mono<CFRoute> toRoute(Mono<Set<String>> domains, String desiredUrl) { return domains.then((ds) -> { try { CFRoute route = CFRoute.builder().from(desiredUrl, ds).build(); route.validate(); return Mono.just(route); } catch (Exception e) { return Mono.error(e); } }); } private Mono<Set<String>> getDomainNames() { return orgId.flatMap(this::requestDomains) .map((r) -> r.getEntity().getName()) .collectList() .map(ImmutableSet::copyOf); } // private Set<String> getUrls(ApplicationDetail app) { // return operations.applications().get(GetApplicationRequest.builder() // .name(appName) // .build() // ) // .map((app) -> ImmutableSet.copyOf(app.getUrls())); // } private Mono<Void> unmapUndesiredRoutes(String appName, Collection<String> desiredUrls) { return getExistingRoutes(appName) .flatMap((route) -> { debug("unmap? "+route); if (desiredUrls.contains(getUrl(route))) { debug("unmap? "+route+" SKIP"); return Mono.empty(); } else { debug("unmap? "+route+" UNMAP"); return unmapRoute(appName, route); } }, 1) //!!!IN SEQUENCE!!! .then(); } private String getUrl(CFRoute route) { // String url = route.getDomain(); // if (route.getHost()!=null) { // url = route.getHost() + "." + url; // } // String path = route.getPath(); // if (path!=null) { // while (path.startsWith("/")) { // path = path.substring(1); // } // if (StringUtils.hasText(path)) { // url = url +"/" +path; // } // } return route.getRoute(); } private Mono<Void> unmapRoute(String appName, CFRoute route) { // if (!StringUtil.hasText(path)) { // //client doesn't like to get 'empty string' it will complain that route doesn't exist. // path = null; // } org.cloudfoundry.operations.routes.UnmapRouteRequest.Builder unmapBuilder = UnmapRouteRequest.builder() .applicationName(appName) .domain(route.getDomain()) .host(route.getHost()); // Have to check if port and path are set. Cannot just set them without checking // otherwise exception throw, even if these values are "empty/null" in the route if (route.getPort() != CFRoute.NO_PORT) { unmapBuilder.port(route.getPort()); } if (StringUtil.hasText(route.getPath())) { unmapBuilder.path(route.getPath()); } UnmapRouteRequest req = unmapBuilder .build(); return log("operations.routes.unmap("+req+")", _operations.routes().unmap(req) ); } public Flux<CFRoute> getExistingRoutes(String appName) { return log("operations.routes.list(level=SPACE)", _operations.routes().list(ListRoutesRequest.builder() .level(Level.SPACE) .build() ) ) .flatMap((route) -> { for (String app : route.getApplications()) { if (app.equals(appName)) { return Mono.just(CFRoute.builder().from(route).build()); } }; return Mono.empty(); }); } // private static PushApplicationRequest.Builder toPushRequest(CFPushArguments params) { // return PushApplicationRequest.builder() // .name(params.getAppName()) // .memory(params.getMemory()) // .diskQuota(params.getDiskQuota()) // .timeout(params.getTimeout()) // .buildpack(params.getBuildpack()) // .command(params.getCommand()) // .stack(params.getStack()) // .instances(params.getInstances()) // .application(params.getApplicationData()); // } public Mono<Void> bindAndUnbindServices(String appName, List<String> _services) { debug("bindAndUnbindServices "+_services); Set<String> services = ImmutableSet.copyOf(_services); return getBoundServicesSet(appName) .flatMap((boundServices) -> { debug("boundServices = "+boundServices); Set<String> toUnbind = Sets.difference(boundServices, services); Set<String> toBind = Sets.difference(services, boundServices); debug("toBind = "+toBind); debug("toUnbind = "+toUnbind); return Flux.merge( bindServices(appName, toBind), unbindServices(appName, toUnbind) ); }) .then(); } public Flux<String> getBoundServices(String appName) { return log("operations.services.listInstances()", _operations.services().listInstances() ) .filter((service) -> isBoundTo(service, appName)) .map(ServiceInstanceSummary::getName); } public Mono<Set<String>> getBoundServicesSet(String appName) { return getBoundServices(appName) .collectList() .map(ImmutableSet::copyOf); } public Mono<List<String>> getBoundServicesList(String appName) { return getBoundServices(appName) .collectList() .map(ImmutableList::copyOf); } private boolean isBoundTo(ServiceInstanceSummary service, String appName) { return service.getApplications().stream() .anyMatch((boundAppName) -> boundAppName.equals(appName)); } private Flux<Void> bindServices(String appName, Set<String> services) { return Flux.fromIterable(services) .flatMap((service) -> { return log("operations.services().bind(appName="+appName+", services="+services+")", _operations.services().bind(BindServiceInstanceRequest.builder() .applicationName(appName) .serviceInstanceName(service) .build() ) ); }); } private Flux<Void> unbindServices(String appName, Set<String> toUnbind) { return Flux.fromIterable(toUnbind) .flatMap((service) -> { return log("operations.services.unbind(appName="+appName+", serviceInstanceName="+service+")", _operations.services().unbind(UnbindServiceInstanceRequest.builder() .applicationName(appName) .serviceInstanceName(service) .build() ) ); }); } protected Mono<Void> startApp(String appName) { return log("operations.applications.start(name="+appName+")", _operations.applications() .start(StartApplicationRequest.builder() .name(appName) .build() ) ); } private Mono<UUID> getApplicationId(String appName) { return log("operations.applications.get(name="+appName+")", _operations.applications().get(GetApplicationRequest.builder() .name(appName) .build() ) ).map((app) -> UUID.fromString(app.getId())); } public Mono<Void> setEnvVars(ApplicationDetail appDetail, Map<String, String> environment) { return setEnvVars(UUID.fromString(appDetail.getId()), environment); } public Mono<Void> setEnvVars(UUID appId, Map<String, String> environment) { return client_setEnv(appId, environment); } public Mono<Void> setEnvVars(String appName, Map<String, String> environment) { return getApplicationId(appName) .then((applicationId) -> setEnvVars(applicationId, environment)); } // protected Publisher<? extends Object> setEnvVar(String appName, String var, String value) { // System.out.println("Set var starting: "+var +" = "+value); // return operations.applications() // .setEnvironmentVariable(SetEnvironmentVariableApplicationRequest.builder() // .name(appName) // .variableName(var) // .variableValue(value) // .build() // ) // .after(() -> { // System.out.println("Set var complete: "+var +" = "+value); // return Mono.empty(); // }); // } public Mono<Void> createService(String name, String service, String plan) { return log("operations.services.createInstance(instanceName="+name+",serviceName="+service+",planName="+plan+")", _operations.services().createInstance(CreateServiceInstanceRequest.builder() .serviceInstanceName(name) .serviceName(service) .planName(plan) .build() ) ); } public Mono<Void> createUserProvidedService(String name, Map<String, Object> credentials) { return log("operations.services.createUserProvidedInstance(name="+name+")", _operations.services().createUserProvidedInstance(CreateUserProvidedServiceInstanceRequest.builder() .name(name) .credentials(credentials) .build() ) ); } // @Override // public void deleteService(String serviceName) { // deleteServiceMono(serviceName).get(); // } @Override public Mono<Void> deleteServiceAsync(String serviceName) { return getService(serviceName) .then(this::deleteServiceInstance); } protected Mono<Void> deleteServiceInstance(ServiceInstance s) { switch (s.getType()) { case MANAGED: return client_deleteServiceInstance(s); case USER_PROVIDED: return client_deleteUserProvidedService(s); default: return Mono.error(new IllegalStateException("Unknown service type: "+s.getType())); } } protected Mono<ServiceInstance> getService(String serviceName) { return log("operations.services.getInstance(name="+serviceName+")", _operations.services().getInstance(GetServiceInstanceRequest.builder() .name(serviceName) .build() ) ); } public Mono<Map<String,String>> getEnv(String appName) { return log("operations.applications.getEnvironments(appName="+appName+")", _operations.applications().getEnvironments(GetApplicationEnvironmentsRequest.builder() .name(appName) .build() ) ) .map((envs) -> envs.getUserProvided()) .map(this::dropObjectsFromMap); } @Override public Map<String, String> getApplicationEnvironment(String appName) throws Exception { return ReactorUtils.get(getEnv(appName)); } private Map<String, String> dropObjectsFromMap(Map<String, Object> map) { Builder<String, String> builder = ImmutableMap.builder(); for (Entry<String, Object> entry : map.entrySet()) { try { builder.put(entry.getKey(), (String) entry.getValue()); } catch (ClassCastException e) { Log.log(e); } } return builder.build(); } ////////////////////////////////////////////////////////////////////////////////////////////////////// //// calls to client and operations with 'logging'. private <T> Flux<T> log(String msg, Flux<T> flux) { if (DEBUG) { return flux .doOnSubscribe((sub) -> debug(">>> "+msg)) .doOnComplete(() -> { debug("<<< "+msg+" OK"); }) .doOnCancel(() -> { debug("<<< "+msg+" CANCEL"); }) .doOnError((error) -> { debug("<<< "+msg+" ERROR: "+ExceptionUtil.getMessage(error)); }); } else { return flux; } } private <T> Mono<T> log(String msg, Mono<T> mono) { if (DEBUG) { return mono .doOnSubscribe((sub) -> debug(">>> "+msg)) .doOnCancel(() -> debug("<<< "+msg+" CANCEL")) .doOnSuccess((data) -> { debug("<<< "+msg+" OK"); }) .doOnError((error) -> { debug("<<< "+msg+" ERROR: "+ExceptionUtil.getMessage(error)); }); } else { return mono; } } private Mono<GetInfoResponse> client_getInfo() { return log("client.info().get()", _client.info().get(GetInfoRequest.builder().build()) ); } private Mono<String> operations_getOrgId() { return log("operations.organizations.get(name="+params.getOrgName()+")", _operations.organizations().get(OrganizationInfoRequest.builder() .name(params.getOrgName()) .build() ) .map(OrganizationDetail::getId) ); } private Mono<ImmutableList<CFApplication>> operations_listApps() { return log("operations.applications.list()", _operations.applications() .list() .map((appSummary) -> CFWrappingV2.wrap(appSummary, getApplicationExtras(appSummary.getName())) ) .collectList() .map(ImmutableList::copyOf) ); } private Mono<GetApplicationResponse> client_getApplication(UUID appId) { return log("client.applicationsV2.get(id="+appId+")", _client.applicationsV2() .get(org.cloudfoundry.client.v2.applications.GetApplicationRequest.builder() .applicationId(appId.toString()) .build() ) ); } private Mono<GetStackResponse> client_getStack(String stackId) { return log("client.stacks.get(id="+stackId+")", _client.stacks().get(GetStackRequest.builder() .stackId(stackId) .build() ) ); } private Mono<UpdateApplicationResponse> client_setHealthCheck(UUID guid, String hcType) { return log("client.applicationsV2.update(id="+guid+", hcType="+hcType+")", _client.applicationsV2() .update(UpdateApplicationRequest.builder() .applicationId(guid.toString()) .healthCheckType(hcType) .build() ) ); } private Mono<ListDomainsResponse> client_listDomains(Integer page) { return log("client.domains.list(page="+page+")", _client.domains().list(ListDomainsRequest.builder() .page(page) // .owningOrganizationId(orgId) .build() ) ); } private Mono<ListBuildpacksResponse> client_listBuildpacks(Integer page) { return log("client.buildpacks.list(page="+page+")", _client.buildpacks() .list(ListBuildpacksRequest.builder() .page(page) .build() ) ); } private Mono<Void> client_setEnv(UUID appId, Map<String, String> environment) { return log("client.applicationsV2.update(id="+appId+", env=...)", _client.applicationsV2() .update(UpdateApplicationRequest.builder() .applicationId(appId.toString()) .environmentJsons(environment) .build()) .then() ); } private Mono<Void> client_deleteServiceInstance(ServiceInstance s) { return log("client.serviceInstances.delete(id="+s.getId()+")", _client.serviceInstances().delete(DeleteServiceInstanceRequest.builder() .serviceInstanceId(s.getId()) .build() ) .then() ); } private Mono<Void> client_deleteUserProvidedService(ServiceInstance s) { return log("client.userProvidedServiceInstances.delete(id="+s.getId()+")", _client.userProvidedServiceInstances().delete(DeleteUserProvidedServiceInstanceRequest.builder() .userProvidedServiceInstanceId(s.getId()) .build() ) ); } @Override public Mono<String> getUserName() { return log("uaa.getUsername", _uaa.getUsername() ).timeout(GET_USERNAME_TIMEOUT); } @Override public String getRefreshToken() { return refreshToken; } }