package com.hubspot.singularity.resources; import static com.hubspot.singularity.WebExceptions.badRequest; import static com.hubspot.singularity.WebExceptions.checkNotFound; import static com.hubspot.singularity.WebExceptions.notFound; import java.util.Collections; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import org.apache.curator.framework.recipes.leader.LeaderLatch; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.inject.Inject; import com.hubspot.jackson.jaxrs.PropertyFiltering; import com.hubspot.mesos.JavaUtils; import com.hubspot.mesos.client.MesosClient; import com.hubspot.mesos.json.MesosTaskMonitorObject; import com.hubspot.mesos.json.MesosTaskStatisticsObject; import com.hubspot.singularity.InvalidSingularityTaskIdException; import com.hubspot.singularity.SingularityAction; import com.hubspot.singularity.SingularityAuthorizationScope; import com.hubspot.singularity.SingularityCreateResult; import com.hubspot.singularity.SingularityKilledTaskIdRecord; import com.hubspot.singularity.SingularityPendingRequest; import com.hubspot.singularity.SingularityPendingRequest.PendingType; import com.hubspot.singularity.SingularityPendingTask; import com.hubspot.singularity.SingularityPendingTaskId; import com.hubspot.singularity.SingularityService; import com.hubspot.singularity.SingularityShellCommand; import com.hubspot.singularity.SingularitySlave; import com.hubspot.singularity.SingularityTask; import com.hubspot.singularity.SingularityTaskCleanup; import com.hubspot.singularity.SingularityTaskId; import com.hubspot.singularity.SingularityTaskMetadata; import com.hubspot.singularity.SingularityTaskRequest; import com.hubspot.singularity.SingularityTaskShellCommandRequest; import com.hubspot.singularity.SingularityTaskShellCommandRequestId; import com.hubspot.singularity.SingularityTransformHelpers; import com.hubspot.singularity.SingularityUser; import com.hubspot.singularity.TaskCleanupType; import com.hubspot.singularity.WebExceptions; import com.hubspot.singularity.api.SingularityKillTaskRequest; import com.hubspot.singularity.api.SingularityTaskMetadataRequest; import com.hubspot.singularity.auth.SingularityAuthorizationHelper; import com.hubspot.singularity.config.SingularityTaskMetadataConfiguration; import com.hubspot.singularity.data.DisasterManager; import com.hubspot.singularity.data.RequestManager; import com.hubspot.singularity.data.SingularityValidator; import com.hubspot.singularity.data.SlaveManager; import com.hubspot.singularity.data.TaskManager; import com.hubspot.singularity.data.TaskRequestManager; import com.ning.http.client.AsyncHttpClient; import com.wordnik.swagger.annotations.Api; import com.wordnik.swagger.annotations.ApiOperation; import com.wordnik.swagger.annotations.ApiResponse; import com.wordnik.swagger.annotations.ApiResponses; @Path(TaskResource.PATH) @Produces({ MediaType.APPLICATION_JSON }) @Api(description="Manages Singularity tasks.", value=TaskResource.PATH) public class TaskResource extends AbstractLeaderAwareResource { public static final String PATH = SingularityService.API_BASE_PATH + "/tasks"; private static final Logger LOG = LoggerFactory.getLogger(TaskResource.class); private final TaskManager taskManager; private final RequestManager requestManager; private final SlaveManager slaveManager; private final TaskRequestManager taskRequestManager; private final MesosClient mesosClient; private final SingularityAuthorizationHelper authorizationHelper; private final Optional<SingularityUser> user; private final SingularityTaskMetadataConfiguration taskMetadataConfiguration; private final SingularityValidator validator; private final DisasterManager disasterManager; @Inject public TaskResource(TaskRequestManager taskRequestManager, TaskManager taskManager, SlaveManager slaveManager, MesosClient mesosClient, SingularityTaskMetadataConfiguration taskMetadataConfiguration, SingularityAuthorizationHelper authorizationHelper, Optional<SingularityUser> user, RequestManager requestManager, SingularityValidator validator, DisasterManager disasterManager, AsyncHttpClient httpClient, LeaderLatch leaderLatch, ObjectMapper objectMapper) { super(httpClient, leaderLatch, objectMapper); this.taskManager = taskManager; this.taskRequestManager = taskRequestManager; this.taskMetadataConfiguration = taskMetadataConfiguration; this.slaveManager = slaveManager; this.mesosClient = mesosClient; this.requestManager = requestManager; this.authorizationHelper = authorizationHelper; this.user = user; this.validator = validator; this.disasterManager = disasterManager; } @GET @PropertyFiltering @Path("/scheduled") @ApiOperation("Retrieve list of scheduled tasks.") public List<SingularityTaskRequest> getScheduledTasks(@QueryParam("useWebCache") Boolean useWebCache) { if (!authorizationHelper.hasAdminAuthorization(user) && disasterManager.isDisabled(SingularityAction.EXPENSIVE_API_CALLS)) { LOG.trace("Short circuting getScheduledTasks() to [] due to EXPENSIVE_API_CALLS disabled"); return Collections.emptyList(); } return taskRequestManager.getTaskRequests(ImmutableList.copyOf(authorizationHelper.filterByAuthorizedRequests(user, taskManager.getPendingTasks(useWebCache(useWebCache)), SingularityTransformHelpers.PENDING_TASK_TO_REQUEST_ID, SingularityAuthorizationScope.READ))); } @GET @PropertyFiltering @Path("/scheduled/ids") @ApiOperation("Retrieve list of scheduled task IDs.") public Iterable<SingularityPendingTaskId> getScheduledTaskIds(@QueryParam("useWebCache") Boolean useWebCache) { return authorizationHelper.filterByAuthorizedRequests(user, taskManager.getPendingTaskIds(useWebCache(useWebCache)), SingularityTransformHelpers.PENDING_TASK_ID_TO_REQUEST_ID, SingularityAuthorizationScope.READ); } private SingularityPendingTaskId getPendingTaskIdFromStr(String pendingTaskIdStr) { try { return SingularityPendingTaskId.valueOf(pendingTaskIdStr); } catch (InvalidSingularityTaskIdException e) { throw badRequest("%s is not a valid pendingTaskId: %s", pendingTaskIdStr, e.getMessage()); } } private SingularityTaskId getTaskIdFromStr(String activeTaskIdStr) { try { return SingularityTaskId.valueOf(activeTaskIdStr); } catch (InvalidSingularityTaskIdException e) { throw badRequest("%s is not a valid taskId: %s", activeTaskIdStr, e.getMessage()); } } @GET @PropertyFiltering @Path("/scheduled/task/{pendingTaskId}") @ApiOperation("Retrieve information about a pending task.") public SingularityTaskRequest getPendingTask(@PathParam("pendingTaskId") String pendingTaskIdStr) { Optional<SingularityPendingTask> pendingTask = taskManager.getPendingTask(getPendingTaskIdFromStr(pendingTaskIdStr)); checkNotFound(pendingTask.isPresent(), "Couldn't find %s", pendingTaskIdStr); List<SingularityTaskRequest> taskRequestList = taskRequestManager.getTaskRequests(Collections.singletonList(pendingTask.get())); checkNotFound(!taskRequestList.isEmpty(), "Couldn't find: " + pendingTaskIdStr); authorizationHelper.checkForAuthorization(taskRequestList.get(0).getRequest(), user, SingularityAuthorizationScope.READ); return taskRequestList.get(0); } @GET @PropertyFiltering @Path("/scheduled/request/{requestId}") @ApiOperation("Retrieve list of scheduled tasks for a specific request.") public List<SingularityTaskRequest> getScheduledTasksForRequest(@PathParam("requestId") String requestId, @QueryParam("useWebCache") Boolean useWebCache) { authorizationHelper.checkForAuthorizationByRequestId(requestId, user, SingularityAuthorizationScope.READ); final List<SingularityPendingTask> tasks = Lists.newArrayList(Iterables.filter(taskManager.getPendingTasks(useWebCache(useWebCache)), SingularityPendingTask.matchingRequest(requestId))); return taskRequestManager.getTaskRequests(tasks); } @GET @Path("/active/slave/{slaveId}") @ApiOperation("Retrieve list of active tasks on a specific slave.") public Iterable<SingularityTask> getTasksForSlave(@PathParam("slaveId") String slaveId, @QueryParam("useWebCache") Boolean useWebCache) { Optional<SingularitySlave> maybeSlave = slaveManager.getObject(slaveId); checkNotFound(maybeSlave.isPresent(), "Couldn't find a slave in any state with id %s", slaveId); return authorizationHelper.filterByAuthorizedRequests(user, taskManager.getTasksOnSlave(taskManager.getActiveTaskIds(useWebCache(useWebCache)), maybeSlave.get()), SingularityTransformHelpers.TASK_TO_REQUEST_ID, SingularityAuthorizationScope.READ); } @GET @PropertyFiltering @Path("/active") @ApiOperation("Retrieve the list of active tasks.") public Iterable<SingularityTask> getActiveTasks(@QueryParam("useWebCache") Boolean useWebCache) { return authorizationHelper.filterByAuthorizedRequests(user, taskManager.getActiveTasks(useWebCache(useWebCache)), SingularityTransformHelpers.TASK_TO_REQUEST_ID, SingularityAuthorizationScope.READ); } @GET @PropertyFiltering @Path("/cleaning") @ApiOperation("Retrieve the list of cleaning tasks.") public Iterable<SingularityTaskCleanup> getCleaningTasks(@QueryParam("useWebCache") Boolean useWebCache) { if (!authorizationHelper.hasAdminAuthorization(user) && disasterManager.isDisabled(SingularityAction.EXPENSIVE_API_CALLS)) { LOG.trace("Short circuting getCleaningTasks() to [] due to EXPENSIVE_API_CALLS disabled"); return Collections.emptyList(); } return authorizationHelper.filterByAuthorizedRequests(user, taskManager.getCleanupTasks(useWebCache(useWebCache)), SingularityTransformHelpers.TASK_CLEANUP_TO_REQUEST_ID, SingularityAuthorizationScope.READ); } @GET @Path("/killed") @ApiOperation("Retrieve the list of killed tasks.") public Iterable<SingularityKilledTaskIdRecord> getKilledTasks() { return authorizationHelper.filterByAuthorizedRequests(user, taskManager.getKilledTaskIdRecords(), SingularityTransformHelpers.KILLED_TASK_ID_RECORD_TO_REQUEST_ID, SingularityAuthorizationScope.READ); } @GET @PropertyFiltering @Path("/lbcleanup") @ApiOperation("Retrieve the list of tasks being cleaned from load balancers.") public Iterable<SingularityTaskId> getLbCleanupTasks() { return authorizationHelper.filterByAuthorizedRequests(user, taskManager.getLBCleanupTasks(), SingularityTransformHelpers.TASK_ID_TO_REQUEST_ID, SingularityAuthorizationScope.READ); } private SingularityTask checkActiveTask(String taskId, SingularityAuthorizationScope scope) { SingularityTaskId taskIdObj = getTaskIdFromStr(taskId); Optional<SingularityTask> task = taskManager.getTask(taskIdObj); checkNotFound(task.isPresent() && taskManager.isActiveTask(taskId), "No active task with id %s", taskId); if (task.isPresent()) { authorizationHelper.checkForAuthorizationByRequestId(task.get().getTaskId().getRequestId(), user, scope); } return task.get(); } @GET @Path("/task/{taskId}") @ApiOperation("Retrieve information about a specific active task.") public SingularityTask getActiveTask(@PathParam("taskId") String taskId) { return checkActiveTask(taskId, SingularityAuthorizationScope.READ); } @GET @Path("/task/{taskId}/statistics") @ApiOperation("Retrieve statistics about a specific active task.") public MesosTaskStatisticsObject getTaskStatistics(@PathParam("taskId") String taskId) { SingularityTask task = checkActiveTask(taskId, SingularityAuthorizationScope.READ); String executorIdToMatch = null; if (task.getMesosTask().getExecutor().hasExecutorId()) { executorIdToMatch = task.getMesosTask().getExecutor().getExecutorId().getValue(); } else { executorIdToMatch = taskId; } for (MesosTaskMonitorObject taskMonitor : mesosClient.getSlaveResourceUsage(task.getOffer().getHostname())) { if (taskMonitor.getExecutorId().equals(executorIdToMatch)) { return taskMonitor.getStatistics(); } } throw notFound("Couldn't find executor %s for %s on slave %s", executorIdToMatch, taskId, task.getOffer().getHostname()); } @GET @Path("/task/{taskId}/cleanup") @ApiOperation("Get the cleanup object for the task, if it exists") public Optional<SingularityTaskCleanup> getTaskCleanup(@PathParam("taskId") String taskId) { authorizationHelper.checkForAuthorizationByTaskId(taskId, user, SingularityAuthorizationScope.READ); return taskManager.getTaskCleanup(taskId); } @DELETE @Path("/task/{taskId}") public SingularityTaskCleanup killTask(@PathParam("taskId") String taskId, @Context HttpServletRequest requestContext) { return killTask(taskId, requestContext, null); } @DELETE @Path("/task/{taskId}") @Consumes({ MediaType.APPLICATION_JSON }) @ApiOperation(value="Attempt to kill task, optionally overriding an existing cleanup request (that may be waiting for replacement tasks to become healthy)", response=SingularityTaskCleanup.class) @ApiResponses({ @ApiResponse(code=409, message="Task already has a cleanup request (can be overridden with override=true)") }) public SingularityTaskCleanup killTask(@PathParam("taskId") String taskId, @Context HttpServletRequest requestContext, SingularityKillTaskRequest killTaskRequest) { final Optional<SingularityKillTaskRequest> maybeKillTaskRequest = Optional.fromNullable(killTaskRequest); return maybeProxyToLeader(requestContext, SingularityTaskCleanup.class, maybeKillTaskRequest.orNull(), () -> killTask(taskId, maybeKillTaskRequest)); } public SingularityTaskCleanup killTask(String taskId, Optional<SingularityKillTaskRequest> killTaskRequest) { final SingularityTask task = checkActiveTask(taskId, SingularityAuthorizationScope.WRITE); Optional<String> message = Optional.absent(); Optional<Boolean> override = Optional.absent(); Optional<String> actionId = Optional.absent(); Optional<Boolean> waitForReplacementTask = Optional.absent(); Optional<SingularityTaskShellCommandRequestId> runBeforeKillId = Optional.absent(); if (killTaskRequest.isPresent()) { actionId = killTaskRequest.get().getActionId(); message = killTaskRequest.get().getMessage(); override = killTaskRequest.get().getOverride(); waitForReplacementTask = killTaskRequest.get().getWaitForReplacementTask(); if (killTaskRequest.get().getRunShellCommandBeforeKill().isPresent()) { SingularityTaskShellCommandRequest shellCommandRequest = startShellCommand(task.getTaskId(), killTaskRequest.get().getRunShellCommandBeforeKill().get()); runBeforeKillId = Optional.of(shellCommandRequest.getId()); } } TaskCleanupType cleanupType = TaskCleanupType.USER_REQUESTED; if (waitForReplacementTask.or(Boolean.FALSE)) { cleanupType = TaskCleanupType.USER_REQUESTED_TASK_BOUNCE; validator.checkActionEnabled(SingularityAction.BOUNCE_TASK); } else { validator.checkActionEnabled(SingularityAction.KILL_TASK); } final long now = System.currentTimeMillis(); final SingularityTaskCleanup taskCleanup; if (override.isPresent() && override.get()) { LOG.debug("Requested destroy of {}", taskId); cleanupType = TaskCleanupType.USER_REQUESTED_DESTROY; taskCleanup = new SingularityTaskCleanup(JavaUtils.getUserEmail(user), cleanupType, now, task.getTaskId(), message, actionId, runBeforeKillId); taskManager.saveTaskCleanup(taskCleanup); } else { taskCleanup = new SingularityTaskCleanup(JavaUtils.getUserEmail(user), cleanupType, now, task.getTaskId(), message, actionId, runBeforeKillId); SingularityCreateResult result = taskManager.createTaskCleanup(taskCleanup); if (result == SingularityCreateResult.EXISTED && userRequestedKillTakesPriority(taskId)) { taskManager.saveTaskCleanup(taskCleanup); } else { while (result == SingularityCreateResult.EXISTED) { Optional<SingularityTaskCleanup> cleanup = taskManager.getTaskCleanup(taskId); if (cleanup.isPresent()) { throw new WebApplicationException(Response.status(Status.CONFLICT).entity(cleanup.get()).type(MediaType.APPLICATION_JSON).build()); } result = taskManager.createTaskCleanup(taskCleanup); } } } if (cleanupType == TaskCleanupType.USER_REQUESTED_TASK_BOUNCE) { requestManager.addToPendingQueue(new SingularityPendingRequest(task.getTaskId().getRequestId(), task.getTaskId().getDeployId(), now, JavaUtils.getUserEmail(user), PendingType.TASK_BOUNCE, Optional.<List<String>> absent(), Optional.<String> absent(), Optional.<Boolean> absent(), message, actionId)); } return taskCleanup; } boolean userRequestedKillTakesPriority(String taskId) { Optional<SingularityTaskCleanup> existingCleanup = taskManager.getTaskCleanup(taskId); if (!existingCleanup.isPresent()) { return true; } return existingCleanup.get().getCleanupType() != TaskCleanupType.USER_REQUESTED_DESTROY; } @Path("/commands/queued") @ApiOperation(value="Retrieve a list of all the shell commands queued for execution") public List<SingularityTaskShellCommandRequest> getQueuedShellCommands() { authorizationHelper.checkAdminAuthorization(user); return taskManager.getAllQueuedTaskShellCommandRequests(); } @POST @Path("/task/{taskId}/metadata") @ApiOperation(value="Post metadata about a task that will be persisted along with it and displayed in the UI") @ApiResponses({ @ApiResponse(code=400, message="Invalid metadata object or doesn't match allowed types"), @ApiResponse(code=404, message="Task doesn't exist"), @ApiResponse(code=409, message="Metadata with this type/timestamp already existed") }) @Consumes({ MediaType.APPLICATION_JSON }) public void postTaskMetadata(@PathParam("taskId") String taskId, final SingularityTaskMetadataRequest taskMetadataRequest) { SingularityTaskId taskIdObj = getTaskIdFromStr(taskId); authorizationHelper.checkForAuthorizationByTaskId(taskId, user, SingularityAuthorizationScope.WRITE); validator.checkActionEnabled(SingularityAction.ADD_METADATA); WebExceptions.checkBadRequest(taskMetadataRequest.getTitle().length() < taskMetadataConfiguration.getMaxMetadataTitleLength(), "Task metadata title too long, must be less than %s bytes", taskMetadataConfiguration.getMaxMetadataTitleLength()); int messageLength = taskMetadataRequest.getMessage().isPresent() ? taskMetadataRequest.getMessage().get().length() : 0; WebExceptions.checkBadRequest(!taskMetadataRequest.getMessage().isPresent() || messageLength < taskMetadataConfiguration.getMaxMetadataMessageLength(), "Task metadata message too long, must be less than %s bytes", taskMetadataConfiguration.getMaxMetadataMessageLength()); if (taskMetadataConfiguration.getAllowedMetadataTypes().isPresent()) { WebExceptions.checkBadRequest(taskMetadataConfiguration.getAllowedMetadataTypes().get().contains(taskMetadataRequest.getType()), "%s is not one of the allowed metadata types %s", taskMetadataRequest.getType(), taskMetadataConfiguration.getAllowedMetadataTypes().get()); } WebExceptions.checkNotFound(taskManager.taskExistsInZk(taskIdObj), "Task %s not found in ZooKeeper (can not save metadata to tasks which have been persisted", taskIdObj); final SingularityTaskMetadata taskMetadata = new SingularityTaskMetadata(taskIdObj, System.currentTimeMillis(), taskMetadataRequest.getType(), taskMetadataRequest.getTitle(), taskMetadataRequest.getMessage(), JavaUtils.getUserEmail(user), taskMetadataRequest.getLevel()); SingularityCreateResult result = taskManager.saveTaskMetadata(taskMetadata); WebExceptions.checkConflict(result == SingularityCreateResult.CREATED, "Task metadata conficted with existing metadata for %s at %s", taskMetadata.getType(), taskMetadata.getTimestamp()); } @POST @Path("/task/{taskId}/command") @ApiOperation(value="Run a configured shell command against the given task") @ApiResponses({ @ApiResponse(code=400, message="Given shell command option doesn't exist"), @ApiResponse(code=403, message="Given shell command doesn't exist") }) @Consumes({ MediaType.APPLICATION_JSON }) public SingularityTaskShellCommandRequest runShellCommand(@PathParam("taskId") String taskId, final SingularityShellCommand shellCommand) { SingularityTaskId taskIdObj = getTaskIdFromStr(taskId); authorizationHelper.checkForAuthorizationByTaskId(taskId, user, SingularityAuthorizationScope.WRITE); validator.checkActionEnabled(SingularityAction.RUN_SHELL_COMMAND); if (!taskManager.isActiveTask(taskId)) { throw WebExceptions.badRequest("%s is not an active task, can't run %s on it", taskId, shellCommand.getName()); } return startShellCommand(taskIdObj, shellCommand); } private SingularityTaskShellCommandRequest startShellCommand(SingularityTaskId taskId, final SingularityShellCommand shellCommand) { validator.checkValidShellCommand(shellCommand); SingularityTaskShellCommandRequest shellRequest = new SingularityTaskShellCommandRequest(taskId, JavaUtils.getUserEmail(user), System.currentTimeMillis(), shellCommand); taskManager.saveTaskShellCommandRequestToQueue(shellRequest); return shellRequest; } }