All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hubspot.singularity.resources.S3LogResource Maven / Gradle / Ivy

package com.hubspot.singularity.resources;

import static com.hubspot.singularity.WebExceptions.checkBadRequest;
import static com.hubspot.singularity.WebExceptions.checkNotFound;
import static com.hubspot.singularity.WebExceptions.timeout;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;

import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
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.core.MediaType;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.amazonaws.HttpMethod;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
import com.amazonaws.services.s3.model.GetObjectMetadataRequest;
import com.amazonaws.services.s3.model.ListObjectsV2Request;
import com.amazonaws.services.s3.model.ListObjectsV2Result;
import com.amazonaws.services.s3.model.ResponseHeaderOverrides;
import com.amazonaws.services.s3.model.S3ObjectSummary;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.primitives.Longs;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.inject.Inject;
import com.hubspot.mesos.JavaUtils;
import com.hubspot.singularity.SingularityAuthorizationScope;
import com.hubspot.singularity.SingularityDeployHistory;
import com.hubspot.singularity.SingularityRequest;
import com.hubspot.singularity.SingularityRequestHistory;
import com.hubspot.singularity.SingularityRequestHistory.RequestHistoryType;
import com.hubspot.singularity.SingularityRequestWithState;
import com.hubspot.singularity.SingularityS3FormatHelper;
import com.hubspot.singularity.SingularityS3Log;
import com.hubspot.singularity.SingularityS3LogMetadata;
import com.hubspot.singularity.SingularityS3UploaderFile;
import com.hubspot.singularity.SingularityTaskHistory;
import com.hubspot.singularity.SingularityTaskHistoryUpdate;
import com.hubspot.singularity.SingularityTaskHistoryUpdate.SimplifiedTaskState;
import com.hubspot.singularity.SingularityTaskId;
import com.hubspot.singularity.SingularityUser;
import com.hubspot.singularity.api.ContinuationToken;
import com.hubspot.singularity.api.SingularityS3SearchRequest;
import com.hubspot.singularity.api.SingularityS3SearchResult;
import com.hubspot.singularity.auth.SingularityAuthorizationHelper;
import com.hubspot.singularity.config.ApiPaths;
import com.hubspot.singularity.config.S3Configuration;
import com.hubspot.singularity.data.DeployManager;
import com.hubspot.singularity.data.RequestManager;
import com.hubspot.singularity.data.TaskManager;
import com.hubspot.singularity.data.history.HistoryManager;
import com.hubspot.singularity.data.history.RequestHistoryHelper;
import com.hubspot.singularity.helpers.S3ObjectSummaryHolder;
import com.hubspot.singularity.helpers.SingularityS3Service;
import com.hubspot.singularity.helpers.SingularityS3Services;

import io.dropwizard.auth.Auth;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.tags.Tags;

@Path(ApiPaths.S3_LOG_RESOURCE_PATH)
@Produces({ MediaType.APPLICATION_JSON })
@Schema(title = "Manage Singularity task logs stored in S3")
@Tags({@Tag(name = "S3 Logs")})
public class S3LogResource extends AbstractHistoryResource {
  private static final Logger LOG = LoggerFactory.getLogger(S3LogResource.class);
  private static final String CONTENT_DISPOSITION_DOWNLOAD_HEADER = "attachment";
  private static final String CONTENT_ENCODING_DOWNLOAD_HEADER = "identity";
  private static final String CONTINUATION_TOKEN_KEY_FORMAT = "%s-%s-%s";
  private static final int DEFAULT_TARGET_MAX_RESULTS = 10;

  private final SingularityS3Services s3Services;
  private final Optional configuration;
  private final RequestHistoryHelper requestHistoryHelper;
  private final RequestManager requestManager;

  private static final Comparator LOG_COMPARATOR = new Comparator() {

    @Override
    public int compare(SingularityS3LogMetadata o1, SingularityS3LogMetadata o2) {
      return Longs.compare(o2.getLastModified(), o1.getLastModified());
    }

  };

  @Inject
  public S3LogResource(RequestManager requestManager, HistoryManager historyManager, RequestHistoryHelper requestHistoryHelper, TaskManager taskManager, DeployManager deployManager,
      Optional configuration, SingularityAuthorizationHelper authorizationHelper, SingularityS3Services s3Services) {
    super(historyManager, taskManager, deployManager, authorizationHelper);
    this.requestManager = requestManager;
    this.configuration = configuration;
    this.requestHistoryHelper = requestHistoryHelper;
    this.s3Services = s3Services;
  }

  // Generation of prefixes
  private Collection getS3PrefixesForTask(S3Configuration s3Configuration, SingularityTaskId taskId, Optional startArg, Optional endArg, String group, SingularityUser user) {
    Optional history = getTaskHistory(taskId, user);

    long start = taskId.getStartedAt();
    if (startArg.isPresent()) {
      start = Math.max(startArg.get(), start);
    }

    long end = start + s3Configuration.getMissingTaskDefaultS3SearchPeriodMillis();

    if (history.isPresent()) {
      SimplifiedTaskState taskState = SingularityTaskHistoryUpdate.getCurrentState(history.get().getTaskUpdates());

      if (taskState == SimplifiedTaskState.DONE) {
        end = Iterables.getLast(history.get().getTaskUpdates()).getTimestamp();
      } else {
        end = System.currentTimeMillis();
      }
    }

    if (endArg.isPresent()) {
      end = Math.min(endArg.get(), end);
    }

    Optional tag = Optional.absent();
    if (history.isPresent() && history.get().getTask().getTaskRequest().getDeploy().getExecutorData().isPresent()) {
      tag = history.get().getTask().getTaskRequest().getDeploy().getExecutorData().get().getLoggingTag();
    }

    Collection prefixes = SingularityS3FormatHelper.getS3KeyPrefixes(s3Configuration.getS3KeyFormat(), taskId, tag, start, end, group);
    for (SingularityS3UploaderFile additionalFile : s3Configuration.getS3UploaderAdditionalFiles()) {
      if (additionalFile.getS3UploaderKeyPattern().isPresent() && !additionalFile.getS3UploaderKeyPattern().get().equals(s3Configuration.getS3KeyFormat())) {
        prefixes.addAll(SingularityS3FormatHelper.getS3KeyPrefixes(additionalFile.getS3UploaderKeyPattern().get(), taskId, tag, start, end, group));
      }
    }

    LOG.trace("Task {} got S3 prefixes {} for start {}, end {}, tag {}", taskId, prefixes, start, end, tag);

    return prefixes;
  }

  private boolean isCurrentDeploy(String requestId, String deployId) {
    return deployId.equals(deployManager.getInUseDeployId(requestId).orNull());
  }

  private Collection getS3PrefixesForRequest(S3Configuration s3Configuration, String requestId, Optional startArg, Optional endArg, String group) {
    Optional firstHistory = requestHistoryHelper.getFirstHistory(requestId);

    checkNotFound(firstHistory.isPresent(), "No request history found for %s", requestId);

    long start = firstHistory.get().getCreatedAt();
    if (startArg.isPresent()) {
      start = Math.max(startArg.get(), start);
    }

    Optional lastHistory = requestHistoryHelper.getLastHistory(requestId);

    long end = System.currentTimeMillis();

    if (lastHistory.isPresent() && (lastHistory.get().getEventType() == RequestHistoryType.DELETED || lastHistory.get().getEventType() == RequestHistoryType.PAUSED)) {
      end = lastHistory.get().getCreatedAt() + TimeUnit.DAYS.toMillis(1);
    }

    if (endArg.isPresent()) {
      end = Math.min(endArg.get(), end);
    }

    Collection prefixes = SingularityS3FormatHelper.getS3KeyPrefixes(s3Configuration.getS3KeyFormat(), requestId, start, end, group);
    for (SingularityS3UploaderFile additionalFile : s3Configuration.getS3UploaderAdditionalFiles()) {
      if (additionalFile.getS3UploaderKeyPattern().isPresent() && !additionalFile.getS3UploaderKeyPattern().get().equals(s3Configuration.getS3KeyFormat())) {
        prefixes.addAll(SingularityS3FormatHelper.getS3KeyPrefixes(additionalFile.getS3UploaderKeyPattern().get(), requestId, start, end, group));
      }
    }

    LOG.trace("Request {} got S3 prefixes {} for start {}, end {}", requestId, prefixes, start, end);

    return prefixes;
  }

  private Collection getS3PrefixesForDeploy(S3Configuration s3Configuration, String requestId, String deployId, Optional startArg, Optional endArg, String group, SingularityUser user) {
    SingularityDeployHistory deployHistory = getDeployHistory(requestId, deployId, user);

    long start = deployHistory.getDeployMarker().getTimestamp();
    if (startArg.isPresent()) {
      start = Math.max(startArg.get(), start);
    }

    long end = System.currentTimeMillis();

    if (!isCurrentDeploy(requestId, deployId) && deployHistory.getDeployStatistics().isPresent() && deployHistory.getDeployStatistics().get().getLastFinishAt().isPresent()) {
      end = deployHistory.getDeployStatistics().get().getLastFinishAt().get() + TimeUnit.DAYS.toMillis(1);
    }

    if (endArg.isPresent()) {
      end = Math.min(endArg.get(), end);
    }

    Optional tag = Optional.absent();

    if (deployHistory.getDeploy().isPresent() && deployHistory.getDeploy().get().getExecutorData().isPresent()) {
      tag = deployHistory.getDeploy().get().getExecutorData().get().getLoggingTag();
    }

    Collection prefixes = SingularityS3FormatHelper.getS3KeyPrefixes(s3Configuration.getS3KeyFormat(), requestId, deployId, tag, start, end, group);
    for (SingularityS3UploaderFile additionalFile : s3Configuration.getS3UploaderAdditionalFiles()) {
      if (additionalFile.getS3UploaderKeyPattern().isPresent() && !additionalFile.getS3UploaderKeyPattern().get().equals(s3Configuration.getS3KeyFormat())) {
        prefixes.addAll(SingularityS3FormatHelper.getS3KeyPrefixes(additionalFile.getS3UploaderKeyPattern().get(), requestId, deployId, tag, start, end, group));
      }
    }

    LOG.trace("Request {}, deploy {} got S3 prefixes {} for start {}, end {}, tag {}", requestId, deployId, prefixes, start, end, tag);

    return prefixes;
  }

  private Map> getServiceToPrefixes(SingularityS3SearchRequest search, SingularityUser user) {
    Map> servicesToPrefixes = new HashMap<>();

    if (!search.getTaskIds().isEmpty()) {
      for (String taskId : search.getTaskIds()) {
        SingularityTaskId taskIdObject = getTaskIdObject(taskId);
        String group = getRequestGroupForTask(taskIdObject, user).or(SingularityS3FormatHelper.DEFAULT_GROUP_NAME);
        Set s3Buckets = getBuckets(group);
        Collection prefixes = getS3PrefixesForTask(configuration.get(), taskIdObject, search.getStart(), search.getEnd(), group, user);
        for (String s3Bucket : s3Buckets) {
          SingularityS3Service s3Service = s3Services.getServiceByGroupAndBucketOrDefault(group, s3Bucket);
          if (!servicesToPrefixes.containsKey(s3Service)) {
            servicesToPrefixes.put(s3Service, new HashSet());
          }
          servicesToPrefixes.get(s3Service).addAll(prefixes);
        }
      }
    }
    if (!search.getRequestsAndDeploys().isEmpty()) {
      for (Map.Entry> entry : search.getRequestsAndDeploys().entrySet()) {
        String group = getRequestGroup(entry.getKey(), user).or(SingularityS3FormatHelper.DEFAULT_GROUP_NAME);
        Set s3Buckets = getBuckets(group);
        List prefixes = new ArrayList<>();
        if (!entry.getValue().isEmpty()) {
          for (String deployId : entry.getValue()) {
            prefixes.addAll(getS3PrefixesForDeploy(configuration.get(), entry.getKey(), deployId, search.getStart(), search.getEnd(), group, user));
          }
        } else {
          prefixes.addAll(getS3PrefixesForRequest(configuration.get(), entry.getKey(), search.getStart(), search.getEnd(), group));
        }
        for (String s3Bucket : s3Buckets) {
          SingularityS3Service s3Service = s3Services.getServiceByGroupAndBucketOrDefault(group, s3Bucket);
          if (!servicesToPrefixes.containsKey(s3Service)) {
            servicesToPrefixes.put(s3Service, new HashSet());
          }
          servicesToPrefixes.get(s3Service).addAll(prefixes);
        }
      }
    }

    // Trim prefixes to search. Less specific prefixes will contain all results of matching + more specific ones
    for (Map.Entry> entry : servicesToPrefixes.entrySet()) {
      Set results = new HashSet<>();
      boolean contains = false;
      for (String prefix : entry.getValue()) {
        for (String unique : results) {
          if (prefix.startsWith(unique) && prefix.length() > unique.length()) {
            contains = true;
            break;
          } else if (unique.startsWith(prefix) && unique.length() > prefix.length()) {
            results.remove(unique);
            results.add(prefix);
            contains = true;
            break;
          }
        }
        if (!contains) {
          results.add(prefix);
        }
      }
      entry.getValue().retainAll(results);
    }

    return servicesToPrefixes;
  }

  private Set getBuckets(String group) {
    Set s3Buckets = new HashSet<>();
    s3Buckets.add(configuration.get().getGroupOverrides().containsKey(group) ? configuration.get().getGroupOverrides().get(group).getS3Bucket() : configuration.get().getS3Bucket());
    s3Buckets.add(configuration.get().getGroupS3SearchConfigs().containsKey(group) ? configuration.get().getGroupS3SearchConfigs().get(group).getS3Bucket() : configuration.get().getS3Bucket());
    for (SingularityS3UploaderFile uploaderFile : configuration.get().getS3UploaderAdditionalFiles()) {
      if (uploaderFile.getS3UploaderBucket().isPresent() && !s3Buckets.contains(uploaderFile.getS3UploaderBucket().get())) {
        s3Buckets.add(uploaderFile.getS3UploaderBucket().get());
      }
    }
    return s3Buckets;
  }

  // Fetching logs
  private List getS3LogsWithExecutorService(S3Configuration s3Configuration, ListeningExecutorService executorService, Map> servicesToPrefixes, int totalPrefixCount, final SingularityS3SearchRequest search, final ConcurrentHashMap continuationTokens, final boolean paginated) throws InterruptedException, ExecutionException, TimeoutException {
    List>> futures = Lists.newArrayListWithCapacity(totalPrefixCount);

    final AtomicInteger resultCount = new AtomicInteger();

    for (final Map.Entry> entry : servicesToPrefixes.entrySet()) {
      final String s3Bucket = entry.getKey().getBucket();
      final String group = entry.getKey().getGroup();
      final AmazonS3 s3Client = entry.getKey().getS3Client();

      for (final String s3Prefix : entry.getValue()) {
        final String key = String.format(CONTINUATION_TOKEN_KEY_FORMAT, group, s3Bucket, s3Prefix);
        if (search.getContinuationTokens().containsKey(key) && search.getContinuationTokens().get(key).isLastPage()) {
          LOG.trace("No further content for prefix {} in bucket {}, skipping", s3Prefix, s3Bucket);
          continuationTokens.putIfAbsent(key, search.getContinuationTokens().get(key));
          continue;
        }
        futures.add(executorService.submit(new Callable>() {

          @Override
          public List call() throws Exception {
            ListObjectsV2Request request = new ListObjectsV2Request().withBucketName(s3Bucket).withPrefix(s3Prefix);
            if (paginated) {
              Optional token = Optional.absent();
              if (search.getContinuationTokens().containsKey(key) && !Strings.isNullOrEmpty(search.getContinuationTokens().get(key).getValue())) {
                request.setContinuationToken(search.getContinuationTokens().get(key).getValue());
                token = Optional.of(search.getContinuationTokens().get(key));
              }
              int targetResultCount = search.getMaxPerPage().or(DEFAULT_TARGET_MAX_RESULTS);
              request.setMaxKeys(targetResultCount);
              if (resultCount.get() < targetResultCount) {
                ListObjectsV2Result result = s3Client.listObjectsV2(request);
                if (result.getObjectSummaries().isEmpty()) {
                  continuationTokens.putIfAbsent(key, new ContinuationToken(result.getNextContinuationToken(), true));
                  return Collections.emptyList();
                } else {
                  boolean addToList = incrementIfLessThan(resultCount, result.getObjectSummaries().size(), targetResultCount);
                  if (addToList) {
                    continuationTokens.putIfAbsent(key, new ContinuationToken(result.getNextContinuationToken(), !result.isTruncated()));
                    List objectSummaryHolders = new ArrayList<>();
                    for (S3ObjectSummary objectSummary : result.getObjectSummaries()) {
                      objectSummaryHolders.add(new S3ObjectSummaryHolder(group, objectSummary));
                    }
                    return objectSummaryHolders;
                  } else {
                    continuationTokens.putIfAbsent(key, token.or(new ContinuationToken(null, false)));
                    return Collections.emptyList();
                  }
                }
              } else {
                continuationTokens.putIfAbsent(key, token.or(new ContinuationToken(null, false)));
                return Collections.emptyList();
              }
            } else {
              ListObjectsV2Result result = s3Client.listObjectsV2(request);
              List objectSummaryHolders = new ArrayList<>();
              for (S3ObjectSummary objectSummary : result.getObjectSummaries()) {
                objectSummaryHolders.add(new S3ObjectSummaryHolder(group, objectSummary));
              }
              return objectSummaryHolders;
            }
          }
        }));
      }
    }

    final long start = System.currentTimeMillis();
    List> results = Futures.allAsList(futures).get(s3Configuration.getWaitForS3ListSeconds(), TimeUnit.SECONDS);

    List objects = Lists.newArrayListWithExpectedSize(results.size() * 2);

    for (List s3ObjectSummaryHolders : results) {
      for (final S3ObjectSummaryHolder s3ObjectHolder : s3ObjectSummaryHolders) {
        objects.add(s3ObjectHolder);
      }
    }

    LOG.trace("Got {} objects from S3 after {}", objects.size(), JavaUtils.duration(start));

    List> logFutures = Lists.newArrayListWithCapacity(objects.size());
    final Date expireAt = new Date(System.currentTimeMillis() + s3Configuration.getExpireS3LinksAfterMillis());

    for (final S3ObjectSummaryHolder s3ObjectHolder : objects) {
      final S3ObjectSummary s3Object = s3ObjectHolder.getObjectSummary();
      final AmazonS3 s3Client = s3Services.getServiceByGroupAndBucketOrDefault(s3ObjectHolder.getGroup(), s3Object.getBucketName()).getS3Client();

      logFutures.add(executorService.submit(new Callable() {
        @Override
        public SingularityS3LogMetadata call() throws Exception {
          Optional maybeStartTime = Optional.absent();
          Optional maybeEndTime = Optional.absent();
          if (!search.isExcludeMetadata()) {
            GetObjectMetadataRequest metadataRequest = new GetObjectMetadataRequest(s3Object.getBucketName(), s3Object.getKey());
            Map objectMetadata = s3Client.getObjectMetadata(metadataRequest).getUserMetadata();
            maybeStartTime = getMetadataAsLong(objectMetadata, SingularityS3Log.LOG_START_S3_ATTR);
            maybeEndTime = getMetadataAsLong(objectMetadata, SingularityS3Log.LOG_END_S3_ATTR);
          }

          if (search.isListOnly()) {
            return new SingularityS3LogMetadata(s3Object.getKey(), s3Object.getLastModified().getTime(), s3Object.getSize(), maybeStartTime, maybeEndTime);
          } else {
            GeneratePresignedUrlRequest getUrlRequest = new GeneratePresignedUrlRequest(s3Object.getBucketName(), s3Object.getKey())
                .withMethod(HttpMethod.GET)
                .withExpiration(expireAt);
            String getUrl = s3Client.generatePresignedUrl(getUrlRequest).toString();

            ResponseHeaderOverrides downloadHeaders = new ResponseHeaderOverrides();
            downloadHeaders.setContentDisposition(CONTENT_DISPOSITION_DOWNLOAD_HEADER);
            downloadHeaders.setContentEncoding(CONTENT_ENCODING_DOWNLOAD_HEADER);
            GeneratePresignedUrlRequest downloadUrlRequest = new GeneratePresignedUrlRequest(s3Object.getBucketName(), s3Object.getKey())
                .withMethod(HttpMethod.GET)
                .withExpiration(expireAt)
                .withResponseHeaders(downloadHeaders);
            String downloadUrl = s3Client.generatePresignedUrl(downloadUrlRequest).toString();

            return new SingularityS3Log(getUrl, s3Object.getKey(), s3Object.getLastModified().getTime(), s3Object.getSize(), downloadUrl, maybeStartTime, maybeEndTime);
          }
        }

      }));
    }

    return Futures.allAsList(logFutures).get(s3Configuration.getWaitForS3LinksSeconds(), TimeUnit.SECONDS);
  }

  private boolean incrementIfLessThan(AtomicInteger count, int add, int threshold) {
    while (true) {
      int current = count.get();
      if (current >= threshold) {
        return false;
      }
      if (count.compareAndSet(current, current + add)) {
        return true;
      }
    }
  }

  private Optional getMetadataAsLong(Map objectMetadata, String keyName) {
    try {
      if (objectMetadata.containsKey(keyName)) {
        Object maybeLong = objectMetadata.get(keyName);
        return Optional.of(Long.parseLong((String) maybeLong));
      } else {
        return Optional.absent();
      }
    } catch (Exception e) {
      return Optional.absent();
    }
  }

  private SingularityS3SearchResult getS3Logs(S3Configuration s3Configuration, Map> servicesToPrefixes, final SingularityS3SearchRequest search, final boolean paginated) throws InterruptedException, ExecutionException, TimeoutException {
    int totalPrefixCount = 0;
    for (Map.Entry> entry : servicesToPrefixes.entrySet()) {
      totalPrefixCount += entry.getValue().size();
    }

    if (totalPrefixCount == 0) {
      return SingularityS3SearchResult.empty();
    }

    ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(Math.min(totalPrefixCount, s3Configuration.getMaxS3Threads()),
        new ThreadFactoryBuilder().setNameFormat("S3LogFetcher-%d").build()));

    try {
      final ConcurrentHashMap continuationTokens = new ConcurrentHashMap<>();
      List logs = Lists.newArrayList(getS3LogsWithExecutorService(s3Configuration, executorService, servicesToPrefixes, totalPrefixCount, search, continuationTokens, paginated));
      Collections.sort(logs, LOG_COMPARATOR);
      return new SingularityS3SearchResult(continuationTokens, isFinalPageForAllPrefixes(continuationTokens.values()), logs);
    } finally {
      executorService.shutdownNow();
    }
  }

  private boolean isFinalPageForAllPrefixes(Collection continuationTokens) {
    for (ContinuationToken token : continuationTokens) {
      if (!token.isLastPage()) {
        return false;
      }
    }
    return true;
  }

  // Finding request group
  private Optional getRequestGroupForTask(final SingularityTaskId taskId, SingularityUser user) {
    Optional maybeTaskHistory = getTaskHistory(taskId, user);
    if (maybeTaskHistory.isPresent()) {
      SingularityRequest request = maybeTaskHistory.get().getTask().getTaskRequest().getRequest();
      authorizationHelper.checkForAuthorization(request, user, SingularityAuthorizationScope.READ);
      return request.getGroup();
    } else {
      return getRequestGroup(taskId.getRequestId(), user);
    }
  }

  private Optional getRequestGroup(final String requestId, SingularityUser user) {
    final Optional maybeRequest = requestManager.getRequest(requestId);
    if (maybeRequest.isPresent()) {
      authorizationHelper.checkForAuthorization(maybeRequest.get().getRequest(), user, SingularityAuthorizationScope.READ);
      return maybeRequest.get().getRequest().getGroup();
    } else {
      Optional maybeRequestHistory = requestHistoryHelper.getLastHistory(requestId);
      if (maybeRequestHistory.isPresent()) {
        authorizationHelper.checkForAuthorization(maybeRequestHistory.get().getRequest(), user, SingularityAuthorizationScope.READ);
        return maybeRequestHistory.get().getRequest().getGroup();
      } else {
        // Deleted requests with no history data are searchable, but only by admins since we have no auth information about them
        authorizationHelper.checkAdminAuthorization(user);
        return Optional.absent();
      }
    }
  }

  private void checkS3() {
    checkNotFound(s3Services.isS3ConfigPresent(), "S3 configuration was absent");
    checkNotFound(configuration.isPresent(), "S3 configuration was absent");
  }

  @GET
  @Path("/task/{taskId}")
  @Operation(
      summary = "Retrieve the list of logs stored in S3 for a specific task",
      responses = {
          @ApiResponse(
              responseCode = "200",
              description = "Returns a list of metadata about log files for the specified task",
              content = {
                  @Content(array = @ArraySchema(schema = @Schema(implementation = SingularityS3LogMetadata.class))),
                  @Content(array = @ArraySchema(schema = @Schema(implementation = SingularityS3Log.class)))
              }
          ),
          @ApiResponse(responseCode = "404", description = "S3 configuration is not present")
      }
  )
  public List getS3LogsForTask(
      @Parameter(hidden = true) @Auth SingularityUser user,
      @Parameter(required = true, description = "The task ID to search for") @PathParam("taskId") String taskId,
      @Parameter(description = "Start timestamp (millis, 13 digit)") @QueryParam("start") Optional start,
      @Parameter(description = "End timestamp (mills, 13 digit)") @QueryParam("end") Optional end,
      @Parameter(description = "Exclude custom object metadata") @QueryParam("excludeMetadata") @DefaultValue ("false") boolean excludeMetadata,
      @Parameter(description = "Do not generate download/get urls, only list the files and metadata") @QueryParam("list") @DefaultValue ("false") boolean listOnly) throws Exception {
    checkS3();

    final SingularityS3SearchRequest search = new SingularityS3SearchRequest(
        Collections.emptyMap(),
        Collections.singletonList(taskId),
        start,
        end,
        excludeMetadata,
        listOnly,
        Optional.absent(),
        Collections.emptyMap());

    try {
      return getS3Logs(configuration.get(), getServiceToPrefixes(search, user), search, false).getResults();
    } catch (TimeoutException te) {
      throw timeout("Timed out waiting for response from S3 for %s", taskId);
    } catch (Throwable t) {
      throw Throwables.propagate(t);
    }
  }

  @GET
  @Path("/request/{requestId}")
  @Operation(
      summary = "Retrieve the list of logs stored in S3 for a specific request",
      responses = {
          @ApiResponse(
              responseCode = "200",
              description = "Returns a list of metadata about log files for the specified task",
              content = {
                  @Content(array = @ArraySchema(schema = @Schema(implementation = SingularityS3LogMetadata.class))),
                  @Content(array = @ArraySchema(schema = @Schema(implementation = SingularityS3Log.class)))
              }
          ),
          @ApiResponse(responseCode = "404", description = "S3 configuration is not present")
      }
  )
  public List getS3LogsForRequest(
      @Parameter(hidden = true) @Auth SingularityUser user,
      @Parameter(required = true, description = "The request ID to search for") @PathParam("requestId") String requestId,
      @Parameter(description = "Start timestamp (millis, 13 digit)") @QueryParam("start") Optional start,
      @Parameter(description = "End timestamp (mills, 13 digit)") @QueryParam("end") Optional end,
      @Parameter(description = "Exclude custom object metadata") @QueryParam("excludeMetadata") @DefaultValue ("false") boolean excludeMetadata,
      @Parameter(description = "Do not generate download/get urls, only list the files and metadata") @QueryParam("list") @DefaultValue ("false") boolean listOnly,
      @Parameter(description = "Max number of results to return per bucket searched") @QueryParam("maxPerPage") Optional maxPerPage) throws Exception {
    checkS3();

    try {
      final SingularityS3SearchRequest search = new SingularityS3SearchRequest(
          ImmutableMap.of(requestId, Collections.emptyList()),
          Collections.emptyList(),
          start,
          end,
          excludeMetadata,
          listOnly,
          Optional.absent(),
          Collections.emptyMap());

      return getS3Logs(configuration.get(), getServiceToPrefixes(search, user), search, false).getResults();
    } catch (TimeoutException te) {
      throw timeout("Timed out waiting for response from S3 for %s", requestId);
    } catch (Throwable t) {
      throw Throwables.propagate(t);
    }
  }

  @GET
  @Path("/request/{requestId}/deploy/{deployId}")
  @Operation(
      summary = "Retrieve the list of logs stored in S3 for a specific deploy",
      responses = {
          @ApiResponse(
              responseCode = "200",
              description = "Returns a list of metadata about log files for the specified task",
              content = {
                  @Content(array = @ArraySchema(schema = @Schema(implementation = SingularityS3LogMetadata.class))),
                  @Content(array = @ArraySchema(schema = @Schema(implementation = SingularityS3Log.class)))
              }
          ),
          @ApiResponse(responseCode = "404", description = "S3 configuration is not present")
      }
  )
  public List getS3LogsForDeploy(
      @Parameter(hidden = true) @Auth SingularityUser user,
      @Parameter(required = true, description = "The request ID to search for") @PathParam("requestId") String requestId,
      @Parameter(required = true, description = "The deploy ID to search for") @PathParam("deployId") String deployId,
      @Parameter(description = "Start timestamp (millis, 13 digit)") @QueryParam("start") Optional start,
      @Parameter(description = "End timestamp (mills, 13 digit)") @QueryParam("end") Optional end,
      @Parameter(description = "Exclude custom object metadata") @QueryParam("excludeMetadata") @DefaultValue ("false") boolean excludeMetadata,
      @Parameter(description = "Do not generate download/get urls, only list the files and metadata") @QueryParam("list") @DefaultValue ("false") boolean listOnly,
      @Parameter(description = "Max number of results to return per bucket searched") @QueryParam("maxPerPage") Optional maxPerPage) throws Exception {
    checkS3();

    try {
      final SingularityS3SearchRequest search = new SingularityS3SearchRequest(
          ImmutableMap.of(requestId, Collections.singletonList(deployId)),
          Collections.emptyList(),
          start,
          end,
          excludeMetadata,
          listOnly,
          Optional.absent(),
          Collections.emptyMap());

      return getS3Logs(configuration.get(), getServiceToPrefixes(search, user), search, false).getResults();
    } catch (TimeoutException te) {
      throw timeout("Timed out waiting for response from S3 for %s-%s", requestId, deployId);
    } catch (Throwable t) {
      throw Throwables.propagate(t);
    }
  }

  @POST
  @Path("/search")
  @Consumes(MediaType.APPLICATION_JSON)
  @Operation(
      summary = "Retrieve a paginated list of logs stored in S3",
      responses = {
          @ApiResponse(
              responseCode = "200",
              description = "Returns a list of metadata about log files for the specified task",
              content = {
                  @Content(array = @ArraySchema(schema = @Schema(implementation = SingularityS3LogMetadata.class))),
                  @Content(array = @ArraySchema(schema = @Schema(implementation = SingularityS3Log.class)))
              }
          ),
          @ApiResponse(responseCode = "404", description = "S3 configuration is not present"),
          @ApiResponse(responseCode = "400", description = "Missing required data for search")
      }
  )
  public SingularityS3SearchResult getPaginatedS3Logs(
      @Parameter(hidden = true) @Auth SingularityUser user,
      @RequestBody(required = true) SingularityS3SearchRequest search) throws Exception {
    checkS3();

    checkBadRequest(!search.getRequestsAndDeploys().isEmpty() || !search.getTaskIds().isEmpty(), "Must specify at least one request or task to search");

    try {
      return getS3Logs(configuration.get(), getServiceToPrefixes(search, user), search, true);
    } catch (TimeoutException te) {
      throw timeout("Timed out waiting for response from S3 for %s", search);
    } catch (Throwable t) {
      throw Throwables.propagate(t);
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy