/* * Copyright 2016-2019 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 * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.cloud.deployer.spi.cloudfoundry; import java.time.Duration; import io.jsonwebtoken.lang.Assert; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.organizations.ListOrganizationsRequest; import org.cloudfoundry.client.v2.spaces.ListSpacesRequest; import org.cloudfoundry.client.v3.tasks.CancelTaskRequest; import org.cloudfoundry.client.v3.tasks.CancelTaskResponse; import org.cloudfoundry.client.v3.tasks.GetTaskRequest; import org.cloudfoundry.client.v3.tasks.GetTaskResponse; import org.cloudfoundry.client.v3.tasks.ListTasksRequest; import org.cloudfoundry.client.v3.tasks.TaskState; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import org.springframework.cloud.deployer.spi.task.LaunchState; import org.springframework.cloud.deployer.spi.task.TaskLauncher; import org.springframework.cloud.deployer.spi.task.TaskStatus; /** * Abstract class to provide base functionality for launching Tasks on Cloud Foundry. This * class provides the base SPI for the {@link CloudFoundryTaskLauncher}. * * Does not override the default no-op implementation for * {@link TaskLauncher#cleanup(String)} and {@link TaskLauncher#destroy(String)}. */ abstract class AbstractCloudFoundryTaskLauncher extends AbstractCloudFoundryDeployer implements TaskLauncher { private static final Logger logger = LoggerFactory.getLogger(AbstractCloudFoundryTaskLauncher.class); private final CloudFoundryClient client; private final Mono<String> organizationId; private final Mono<String> spaceId; AbstractCloudFoundryTaskLauncher(CloudFoundryClient client, CloudFoundryDeploymentProperties deploymentProperties, RuntimeEnvironmentInfo runtimeEnvironmentInfo) { super(deploymentProperties, runtimeEnvironmentInfo); this.client = client; organizationId = organizationId(); spaceId = spaceId(); } /** * Setup a reactor flow to cancel a running task. This implementation opts to be * asynchronous. * * @param id the task's id to be canceled as returned from the * {@link TaskLauncher#launch(AppDeploymentRequest)} */ @Override public void cancel(String id) { requestCancelTask(id) .timeout(Duration.ofSeconds(this.deploymentProperties.getApiTimeout())) .doOnSuccess(r -> logger.info("Task {} cancellation successful", id)) .doOnError(logError(String.format("Task %s cancellation failed", id))) .subscribe(); } /** * Lookup the current status based on task id. * * @param id taskId as returned from the {@link TaskLauncher#launch(AppDeploymentRequest)} * @return the current task status */ @Override public TaskStatus status(String id) { try { return getStatus(id) .doOnSuccess(v -> logger.info("Successfully computed status [{}] for id={}", v, id)) .doOnError(logError(String.format("Failed to compute status for %s", id))) .block(Duration.ofMillis(this.deploymentProperties.getStatusTimeout())); } catch (Exception timeoutDueToBlock) { logger.error("Caught exception while querying for status of id={}", id, timeoutDueToBlock); return createErrorTaskStatus(id); } } @Override public int getRunningTaskExecutionCount() { Mono<Tuple2<String,String>> orgAndSpace = Mono.zip(organizationId, spaceId); Mono<ListTasksRequest> listTasksRequest = orgAndSpace.map(tuple-> ListTasksRequest.builder() .state(TaskState.RUNNING) .organizationId(tuple.getT1()) .spaceId(tuple.getT2()) .build()); return listTasksRequest.flatMap(request-> this.client.tasks().list(request)) .map(listTasksResponse -> listTasksResponse.getPagination().getTotalResults()) .doOnError(logError("Failed to list running tasks")) .doOnSuccess(count -> logger.info(String.format("There are %d running tasks", count))) .block(Duration.ofMillis(this.deploymentProperties.getStatusTimeout())); } @Override public int getMaximumConcurrentTasks() { return this.deploymentProperties.getMaximumConcurrentTasks(); } protected boolean maxConcurrentExecutionsReached() { return this.getRunningTaskExecutionCount() >= this.getMaximumConcurrentTasks(); } private Mono<TaskStatus> getStatus(String id) { return requestGetTask(id) .map(this::toTaskStatus) .onErrorResume(isNotFoundError(), t -> { logger.debug("Task for id={} does not exist", id); return Mono.just(new TaskStatus(id, LaunchState.unknown, null)); }) .transform(statusRetry(id)) .onErrorReturn(createErrorTaskStatus(id)); } private TaskStatus createErrorTaskStatus(String id) { return new TaskStatus(id, LaunchState.error, null); } protected TaskStatus toTaskStatus(GetTaskResponse response) { switch (response.getState()) { case SUCCEEDED: return new TaskStatus(response.getId(), LaunchState.complete, null); case RUNNING: return new TaskStatus(response.getId(), LaunchState.running, null); case PENDING: return new TaskStatus(response.getId(), LaunchState.launching, null); case CANCELING: return new TaskStatus(response.getId(), LaunchState.cancelled, null); case FAILED: return new TaskStatus(response.getId(), LaunchState.failed, null); default: throw new IllegalStateException(String.format("Unsupported CF task state %s", response.getState())); } } private Mono<CancelTaskResponse> requestCancelTask(String taskId) { return this.client.tasks() .cancel(CancelTaskRequest.builder() .taskId(taskId) .build()); } private Mono<GetTaskResponse> requestGetTask(String taskId) { return this.client.tasks() .get(GetTaskRequest.builder() .taskId(taskId) .build()); } private Mono<String> organizationId() { String org = this.runtimeEnvironmentInfo.getPlatformSpecificInfo().get(CloudFoundryPlatformSpecificInfo.ORG); Assert.hasText(org,"Missing runtimeEnvironmentInfo : 'org' required."); ListOrganizationsRequest listOrganizationsRequest = ListOrganizationsRequest.builder() .name(org).build(); return this.client.organizations().list(listOrganizationsRequest) .doOnError(logError("Failed to list organizations")) .map(listOrganizationsResponse -> listOrganizationsResponse.getResources().get(0).getMetadata().getId()) .cache(aValue -> Duration.ofMillis(Long.MAX_VALUE), aValue -> Duration.ZERO, () -> Duration.ZERO); } private Mono<String> spaceId() { String space = this.runtimeEnvironmentInfo.getPlatformSpecificInfo().get(CloudFoundryPlatformSpecificInfo.SPACE); Assert.hasText(space,"Missing runtimeEnvironmentInfo : 'space' required."); ListSpacesRequest listSpacesRequest = ListSpacesRequest.builder() .name(space).build(); return this.client.spaces().list(listSpacesRequest) .doOnError(logError("Failed to list spaces")) .map(listSpacesResponse -> listSpacesResponse.getResources().get(0).getMetadata().getId()) .cache(aValue -> Duration.ofMillis(Long.MAX_VALUE), aValue -> Duration.ZERO, () -> Duration.ZERO); } @Override public void cleanup(String id) { } @Override public void destroy(String appName) { } }