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

com.spotify.styx.api.StatusResource Maven / Gradle / Ivy

/*-
 * -\-\-
 * Spotify Styx API Service
 * --
 * Copyright (C) 2017 Spotify AB
 * --
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * -/-/-
 */

package com.spotify.styx.api;

import static com.spotify.styx.api.Api.Version.V3;
import static com.spotify.styx.util.CloserUtil.register;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;

import com.google.api.client.util.Lists;
import com.google.common.io.Closer;
import com.spotify.apollo.RequestContext;
import com.spotify.apollo.Response;
import com.spotify.apollo.Status;
import com.spotify.apollo.entity.EntityMiddleware;
import com.spotify.apollo.entity.JacksonEntityCodec;
import com.spotify.apollo.route.AsyncHandler;
import com.spotify.apollo.route.Middleware;
import com.spotify.apollo.route.Route;
import com.spotify.styx.api.RunStateDataPayload.RunStateData;
import com.spotify.styx.api.ServiceAccountUsageAuthorizer.ServiceAccountUsageAuthorizationResult;
import com.spotify.styx.api.util.InvalidParametersException;
import com.spotify.styx.model.SequenceEvent;
import com.spotify.styx.model.WorkflowId;
import com.spotify.styx.model.WorkflowInstance;
import com.spotify.styx.serialization.Json;
import com.spotify.styx.state.RunState;
import com.spotify.styx.storage.Storage;
import java.io.Closeable;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ForkJoinPool;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javaslang.control.Try;
import okio.ByteString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * API endpoints for the retrieving events and active states
 */
public class StatusResource implements Closeable {

  private static final Logger log = LoggerFactory.getLogger(StatusResource.class);
  private static final int CONCURRENCY = 32;

  static final String BASE = "/status";

  private final Storage storage;
  private final ServiceAccountUsageAuthorizer accountUsageAuthorizer;
  private final ForkJoinPool forkJoinPool;
  private final Closer closer = Closer.create();

  public StatusResource(Storage storage, ServiceAccountUsageAuthorizer accountUsageAuthorizer) {
    this.storage = requireNonNull(storage);
    this.accountUsageAuthorizer = requireNonNull(accountUsageAuthorizer);
    this.forkJoinPool = register(closer, new ForkJoinPool(CONCURRENCY), "status-resource");
  }

  public Stream>>> routes() {
    final EntityMiddleware em =
        EntityMiddleware.forCodec(JacksonEntityCodec.forMapper(Json.OBJECT_MAPPER));

    final List>>> routes = Stream.of(
        Route.with(
            em.serializerResponse(RunStateDataPayload.class),
            "GET", BASE + "/activeStates",
            this::activeStates),
        Route.with(
            em.serializerDirect(EventsPayload.class),
            "GET", BASE + "/events///",
            rc -> eventsForWorkflowInstance(arg("cid", rc), arg("wfid", rc), arg("iid", rc))),
        Route.with(
            em.response(TestServiceAccountUsageAuthorizationRequest.class,
                TestServiceAccountUsageAuthorizationResponse.class),
            "POST", BASE + "/testServiceAccountUsageAuthorization",
            rc -> this::testServiceAccountUsageAuthorization)
    )

        .map(r -> r.withMiddleware(Middleware::syncToAsync))
        .collect(toList());

    return Api.prefixRoutes(routes, V3);
  }

  @Override
  public void close() throws IOException {
    closer.close();
  }

  private Response testServiceAccountUsageAuthorization(
      TestServiceAccountUsageAuthorizationRequest request) {
    final ServiceAccountUsageAuthorizationResult result =
        accountUsageAuthorizer.checkServiceAccountUsageAuthorization(request.serviceAccount(), request.principal());

    result.errorResponse().ifPresent(e -> { throw new ResponseException(e); });

    final TestServiceAccountUsageAuthorizationResponse response =
        new TestServiceAccountUsageAuthorizationResponseBuilder()
            .authorized(result.authorized())
            .blacklisted(result.blacklisted())
            .serviceAccount(request.serviceAccount())
            .principal(request.principal())
            .message(result.message())
            .build();

    return Response.forPayload(response);
  }

  private static String arg(String name, RequestContext rc) {
    return rc.pathArgs().get(name);
  }

  private Response activeStates(RequestContext requestContext) {
    final Optional componentOpt = requestContext.request().parameter("component");
    final Optional workflowOpt = requestContext.request().parameter("workflow");
    final Optional componentsOpt = requestContext.request().parameter("components");
    final Map activeStates;

    final List runStates = Lists.newArrayList();
    try {
      activeStates = componentsOpt.isPresent()
                     ? getActiveStates(componentsOpt.get())
                     : getActiveStates(componentOpt, workflowOpt);

    } catch (InvalidParametersException e) {
      return Response.forStatus(Status.BAD_REQUEST.withReasonPhrase(e.getMessage()));
    } catch (IOException e) {
      var errorMsg = "Could not read Active states: " + e;
      log.error(errorMsg);
      return Response.forStatus(Status.INTERNAL_SERVER_ERROR.withReasonPhrase(errorMsg));
    }
    runStates.addAll(
        activeStates.values().stream().map(this::runStateToRunStateData).collect(toList()));

    return Response.forPayload(RunStateDataPayload.create(runStates));
  }

  private Map getActiveStates(String componentsStr) throws IOException {
    var components = new HashSet<>(Arrays.asList(componentsStr.split(",")));
    var activeStatesOrExceptions = forkJoinPool.submit(() ->
        components.parallelStream()
            .map(componentId -> Try.of(() -> storage.readActiveStates(componentId)))
            .collect(toList()))
        .join();

    return Try.sequence(activeStatesOrExceptions)
        .getOrElseThrow((Function) IOException::new)
        .toJavaStream()
        .flatMap(map -> map.entrySet().stream())
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
  }

  private Map getActiveStates(Optional componentOpt, Optional workflowOpt)
      throws IOException {
    if (workflowOpt.isPresent()) {
      if (componentOpt.isPresent()){
        return storage.readActiveStates(componentOpt.get(), workflowOpt.get());
      } else {
        throw new InvalidParametersException("No component id specified!");
      }
    } else if(componentOpt.isPresent()) {
      return storage.readActiveStates(componentOpt.get());
    } else {
      return storage.readActiveStates();
    }
  }

  private RunStateData runStateToRunStateData(RunState state) {
    return RunStateData.newBuilder()
        .workflowInstance(state.workflowInstance())
        .state(state.state().name())
        .stateData(state.data())
        .latestTimestamp(state.timestamp())
        .build();
  }

  private EventsPayload eventsForWorkflowInstance(String cid, String eid, String iid) {
    final WorkflowId workflowId = WorkflowId.create(cid, eid);
    final WorkflowInstance workflowInstance = WorkflowInstance.create(workflowId, iid);

    try {
      final Set sequenceEvents = storage.readEvents(workflowInstance);
      final List timestampedEvents = sequenceEvents.stream()
          .map(sequenceEvent -> EventsPayload.TimestampedEvent.create(
              sequenceEvent.event(),
              sequenceEvent.timestamp()))
          .collect(toList());

      return EventsPayload.create(timestampedEvents);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy