/* * Copyright © 2015-2016 Cask Data, Inc. * * 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 co.cask.cdap.gateway.handlers; import co.cask.cdap.api.app.ApplicationSpecification; import co.cask.cdap.api.common.Bytes; import co.cask.cdap.api.dataset.DatasetManagementException; import co.cask.cdap.api.dataset.InstanceNotFoundException; import co.cask.cdap.api.metrics.MetricStore; import co.cask.cdap.api.schedule.SchedulableProgramType; import co.cask.cdap.api.schedule.ScheduleSpecification; import co.cask.cdap.api.workflow.NodeValue; import co.cask.cdap.api.workflow.Value; import co.cask.cdap.api.workflow.WorkflowSpecification; import co.cask.cdap.api.workflow.WorkflowToken; import co.cask.cdap.app.mapreduce.MRJobInfoFetcher; import co.cask.cdap.app.runtime.ProgramController; import co.cask.cdap.app.runtime.ProgramRuntimeService; import co.cask.cdap.app.store.Store; import co.cask.cdap.common.ApplicationNotFoundException; import co.cask.cdap.common.ConflictException; import co.cask.cdap.common.NotFoundException; import co.cask.cdap.common.ProgramNotFoundException; import co.cask.cdap.common.app.RunIds; import co.cask.cdap.common.conf.Constants; import co.cask.cdap.config.PreferencesStore; import co.cask.cdap.data2.dataset2.DatasetFramework; import co.cask.cdap.data2.transaction.queue.QueueAdmin; import co.cask.cdap.internal.app.runtime.schedule.Scheduler; import co.cask.cdap.internal.app.runtime.schedule.SchedulerException; import co.cask.cdap.internal.app.services.ProgramLifecycleService; import co.cask.cdap.internal.dataset.DatasetCreationSpec; import co.cask.cdap.proto.DatasetSpecificationSummary; import co.cask.cdap.proto.Id; import co.cask.cdap.proto.ProgramType; import co.cask.cdap.proto.ScheduledRuntime; import co.cask.cdap.proto.WorkflowNodeStateDetail; import co.cask.cdap.proto.WorkflowTokenDetail; import co.cask.cdap.proto.WorkflowTokenNodeDetail; import co.cask.cdap.proto.codec.ScheduleSpecificationCodec; import co.cask.cdap.proto.codec.WorkflowTokenDetailCodec; import co.cask.cdap.proto.codec.WorkflowTokenNodeDetailCodec; import co.cask.cdap.proto.id.ApplicationId; import co.cask.cdap.proto.id.Ids; import co.cask.cdap.proto.id.ProgramId; import co.cask.cdap.proto.id.ProgramRunId; import co.cask.http.HttpResponder; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.Lists; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import com.google.inject.Inject; import com.google.inject.Singleton; import org.jboss.netty.handler.codec.http.HttpHeaders; import org.jboss.netty.handler.codec.http.HttpRequest; import org.jboss.netty.handler.codec.http.HttpResponseStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.lang.reflect.Type; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.ws.rs.DELETE; 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.QueryParam; /** * Workflow HTTP Handler. */ @Singleton @Path(Constants.Gateway.API_VERSION_3 + "/namespaces/{namespace-id}") public class WorkflowHttpHandler extends ProgramLifecycleHttpHandler { private static final Logger LOG = LoggerFactory.getLogger(WorkflowHttpHandler.class); private static final Type STRING_TO_NODESTATEDETAIL_MAP_TYPE = new TypeToken<Map<String, WorkflowNodeStateDetail>>() { }.getType(); private static final Gson GSON = new GsonBuilder() .registerTypeAdapter(ScheduleSpecification.class, new ScheduleSpecificationCodec()) .registerTypeAdapter(WorkflowTokenDetail.class, new WorkflowTokenDetailCodec()) .registerTypeAdapter(WorkflowTokenNodeDetail.class, new WorkflowTokenNodeDetailCodec()) .create(); private final WorkflowClient workflowClient; private final DatasetFramework datasetFramework; @Inject WorkflowHttpHandler(Store store, WorkflowClient workflowClient, ProgramRuntimeService runtimeService, QueueAdmin queueAdmin, Scheduler scheduler, PreferencesStore preferencesStore, MRJobInfoFetcher mrJobInfoFetcher, ProgramLifecycleService lifecycleService, MetricStore metricStore, DatasetFramework datasetFramework) { super(store, runtimeService, lifecycleService, queueAdmin, scheduler, preferencesStore, mrJobInfoFetcher, metricStore); this.workflowClient = workflowClient; this.datasetFramework = datasetFramework; } @POST @Path("/apps/{app-id}/workflows/{workflow-name}/runs/{run-id}/suspend") public void suspendWorkflowRun(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId, @PathParam("workflow-name") String workflowName, @PathParam("run-id") String runId) throws Exception { Id.Program id = Id.Program.from(namespaceId, appId, ProgramType.WORKFLOW, workflowName); ProgramRuntimeService.RuntimeInfo runtimeInfo = runtimeService.list(id).get(RunIds.fromString(runId)); if (runtimeInfo == null) { throw new NotFoundException(new Id.Run(id, runId)); } ProgramController controller = runtimeInfo.getController(); if (controller.getState() == ProgramController.State.SUSPENDED) { throw new ConflictException("Program run already suspended"); } controller.suspend().get(); responder.sendString(HttpResponseStatus.OK, "Program run suspended."); } @POST @Path("/apps/{app-id}/workflows/{workflow-name}/runs/{run-id}/resume") public void resumeWorkflowRun(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId, @PathParam("workflow-name") String workflowName, @PathParam("run-id") String runId) throws Exception { Id.Program id = Id.Program.from(namespaceId, appId, ProgramType.WORKFLOW, workflowName); ProgramRuntimeService.RuntimeInfo runtimeInfo = runtimeService.list(id).get(RunIds.fromString(runId)); if (runtimeInfo == null) { throw new NotFoundException(new Id.Run(id, runId)); } ProgramController controller = runtimeInfo.getController(); if (controller.getState() == ProgramController.State.ALIVE) { throw new ConflictException("Program is already running"); } controller.resume().get(); responder.sendString(HttpResponseStatus.OK, "Program run resumed."); } @GET @Path("/apps/{app-id}/workflows/{workflow-name}/runs/{run-id}/current") public void getWorkflowStatus(HttpRequest request, final HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId, @PathParam("workflow-name") String workflowName, @PathParam("run-id") String runId) throws IOException { try { workflowClient.getWorkflowStatus(namespaceId, appId, workflowName, runId, new WorkflowClient.Callback() { @Override public void handle(WorkflowClient.Status status) { if (status.getCode() == WorkflowClient.Status.Code.NOT_FOUND) { responder.sendStatus(HttpResponseStatus.NOT_FOUND); } else if (status.getCode() == WorkflowClient.Status.Code.OK) { // This uses responder.sendByteArray because status.getResult returns a // json string, and responder.sendJson would need deserialization and // serialization. responder.sendByteArray(HttpResponseStatus.OK, Bytes.toBytes(status.getResult()), ImmutableMultimap.of( HttpHeaders.Names.CONTENT_TYPE, "application/json; charset=utf-8")); } else { responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, status.getResult()); } } }); } catch (SecurityException e) { responder.sendStatus(HttpResponseStatus.UNAUTHORIZED); } } /** * Returns the previous runtime when the scheduled program ran. */ @GET @Path("/apps/{app-id}/workflows/{workflow-id}/previousruntime") public void getPreviousScheduledRunTime(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId, @PathParam("workflow-id") String workflowId) throws SchedulerException, NotFoundException { getScheduledRuntime(responder, namespaceId, appId, workflowId, true); } /** * Returns next scheduled runtime of a workflow. */ @GET @Path("/apps/{app-id}/workflows/{workflow-id}/nextruntime") public void getNextScheduledRunTime(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId, @PathParam("workflow-id") String workflowId) throws SchedulerException, NotFoundException { getScheduledRuntime(responder, namespaceId, appId, workflowId, false); } private void getScheduledRuntime(HttpResponder responder, String namespaceId, String appName, String workflowName, boolean previousRuntimeRequested) throws SchedulerException, NotFoundException { try { Id.Application appId = Id.Application.from(namespaceId, appName); Id.Program workflowId = Id.Program.from(appId, ProgramType.WORKFLOW, workflowName); ApplicationSpecification appSpec = store.getApplication(appId); if (appSpec == null) { throw new ApplicationNotFoundException(appId); } if (appSpec.getWorkflows().get(workflowName) == null) { throw new ProgramNotFoundException(workflowId); } List<ScheduledRuntime> runtimes; if (previousRuntimeRequested) { runtimes = scheduler.previousScheduledRuntime(workflowId, SchedulableProgramType.WORKFLOW); } else { runtimes = scheduler.nextScheduledRuntime(workflowId, SchedulableProgramType.WORKFLOW); } responder.sendJson(HttpResponseStatus.OK, runtimes); } catch (SecurityException e) { responder.sendStatus(HttpResponseStatus.UNAUTHORIZED); } } /** * Get Workflow schedules */ @GET @Path("/apps/{app-id}/workflows/{workflow-id}/schedules") public void getWorkflowSchedules(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId, @PathParam("workflow-id") String workflowId) { ApplicationSpecification appSpec = store.getApplication(Id.Application.from(namespaceId, appId)); if (appSpec == null) { responder.sendString(HttpResponseStatus.NOT_FOUND, "App:" + appId + " not found"); return; } List<ScheduleSpecification> specList = Lists.newArrayList(); for (Map.Entry<String, ScheduleSpecification> entry : appSpec.getSchedules().entrySet()) { ScheduleSpecification spec = entry.getValue(); if (spec.getProgram().getProgramName().equals(workflowId) && spec.getProgram().getProgramType() == SchedulableProgramType.WORKFLOW) { specList.add(entry.getValue()); } } responder.sendJson(HttpResponseStatus.OK, specList, new TypeToken<List<ScheduleSpecification>>() { }.getType(), GSON); } @GET @Path("/apps/{app-id}/workflows/{workflow-id}/runs/{run-id}/token") public void getWorkflowToken(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId, @PathParam("workflow-id") String workflowId, @PathParam("run-id") String runId, @QueryParam("scope") @DefaultValue("user") String scope, @QueryParam("key") @DefaultValue("") String key) throws NotFoundException { WorkflowToken workflowToken = getWorkflowToken(namespaceId, appId, workflowId, runId); WorkflowToken.Scope tokenScope = WorkflowToken.Scope.valueOf(scope.toUpperCase()); WorkflowTokenDetail workflowTokenDetail = WorkflowTokenDetail.of(workflowToken.getAll(tokenScope)); Type workflowTokenDetailType = new TypeToken<WorkflowTokenDetail>() { }.getType(); if (key.isEmpty()) { responder.sendJson(HttpResponseStatus.OK, workflowTokenDetail, workflowTokenDetailType, GSON); return; } List<NodeValue> nodeValueEntries = workflowToken.getAll(key, tokenScope); if (nodeValueEntries.isEmpty()) { throw new NotFoundException(key); } responder.sendJson(HttpResponseStatus.OK, WorkflowTokenDetail.of(ImmutableMap.of(key, nodeValueEntries)), workflowTokenDetailType, GSON); } @GET @Path("/apps/{app-id}/workflows/{workflow-id}/runs/{run-id}/nodes/{node-id}/token") public void getWorkflowToken(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId, @PathParam("workflow-id") String workflowId, @PathParam("run-id") String runId, @PathParam("node-id") String nodeId, @QueryParam("scope") @DefaultValue("user") String scope, @QueryParam("key") @DefaultValue("") String key) throws NotFoundException { WorkflowToken workflowToken = getWorkflowToken(namespaceId, appId, workflowId, runId); WorkflowToken.Scope tokenScope = WorkflowToken.Scope.valueOf(scope.toUpperCase()); Map<String, Value> workflowTokenFromNode = workflowToken.getAllFromNode(nodeId, tokenScope); WorkflowTokenNodeDetail tokenAtNode = WorkflowTokenNodeDetail.of(workflowTokenFromNode); Type workflowTokenNodeDetailType = new TypeToken<WorkflowTokenNodeDetail>() { }.getType(); if (key.isEmpty()) { responder.sendJson(HttpResponseStatus.OK, tokenAtNode, workflowTokenNodeDetailType, GSON); return; } if (!workflowTokenFromNode.containsKey(key)) { throw new NotFoundException(key); } responder.sendJson(HttpResponseStatus.OK, WorkflowTokenNodeDetail.of(ImmutableMap.of(key, workflowTokenFromNode.get(key))), workflowTokenNodeDetailType, GSON); } private WorkflowToken getWorkflowToken(String namespaceId, String appName, String workflow, String runId) throws NotFoundException { Id.Application appId = Id.Application.from(namespaceId, appName); ApplicationSpecification appSpec = store.getApplication(appId); if (appSpec == null) { throw new NotFoundException(appId); } Id.Workflow workflowId = Id.Workflow.from(appId, workflow); if (!appSpec.getWorkflows().containsKey(workflow)) { throw new NotFoundException(workflowId); } if (store.getRun(workflowId, runId) == null) { throw new NotFoundException(new Id.Run(workflowId, runId)); } return store.getWorkflowToken(workflowId, runId); } @GET @Path("/apps/{app-id}/workflows/{workflow-id}/runs/{run-id}/nodes/state") public void getWorkflowNodeStates(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String applicationId, @PathParam("workflow-id") String workflowId, @PathParam("run-id") String runId) throws NotFoundException { ApplicationId appId = Ids.namespace(namespaceId).app(applicationId); ApplicationSpecification appSpec = store.getApplication(appId.toId()); if (appSpec == null) { throw new ApplicationNotFoundException(appId.toId()); } ProgramId workflowProgramId = appId.workflow(workflowId); WorkflowSpecification workflowSpec = appSpec.getWorkflows().get(workflowProgramId.getProgram()); if (workflowSpec == null) { throw new ProgramNotFoundException(workflowProgramId.toId()); } ProgramRunId workflowRunId = workflowProgramId.run(runId); if (store.getRun(workflowProgramId.toId(), runId) == null) { throw new NotFoundException(workflowRunId); } List<WorkflowNodeStateDetail> nodeStateDetails = store.getWorkflowNodeStates(workflowRunId); Map<String, WorkflowNodeStateDetail> nodeStates = new HashMap<>(); for (WorkflowNodeStateDetail nodeStateDetail : nodeStateDetails) { nodeStates.put(nodeStateDetail.getNodeId(), nodeStateDetail); } responder.sendJson(HttpResponseStatus.OK, nodeStates, STRING_TO_NODESTATEDETAIL_MAP_TYPE); } @GET @Path("/apps/{app-id}/workflows/{workflow-id}/runs/{run-id}/localdatasets") public void getWorkflowLocalDatasets(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String applicationId, @PathParam("workflow-id") String workflowId, @PathParam("run-id") String runId) throws NotFoundException, DatasetManagementException { WorkflowSpecification workflowSpec = getWorkflowSpecForValidRun(namespaceId, applicationId, workflowId, runId); Map<String, DatasetSpecificationSummary> localDatasetSummaries = new HashMap<>(); for (Map.Entry<String, DatasetCreationSpec> localDatasetEntry : workflowSpec.getLocalDatasetSpecs().entrySet()) { String mappedDatasetName = localDatasetEntry.getKey() + "." + runId; String datasetType = localDatasetEntry.getValue().getTypeName(); Map<String, String> datasetProperties = localDatasetEntry.getValue().getProperties().getProperties(); if (datasetFramework.hasInstance(Id.DatasetInstance.from(namespaceId, mappedDatasetName))) { localDatasetSummaries.put(localDatasetEntry.getKey(), new DatasetSpecificationSummary(mappedDatasetName, datasetType, datasetProperties)); } } responder.sendJson(HttpResponseStatus.OK, localDatasetSummaries); } @DELETE @Path("/apps/{app-id}/workflows/{workflow-id}/runs/{run-id}/localdatasets") public void deleteWorkflowLocalDatasets(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String applicationId, @PathParam("workflow-id") String workflowId, @PathParam("run-id") String runId) throws NotFoundException { WorkflowSpecification workflowSpec = getWorkflowSpecForValidRun(namespaceId, applicationId, workflowId, runId); Set<String> errorOnDelete = new HashSet<>(); for (Map.Entry<String, DatasetCreationSpec> localDatasetEntry : workflowSpec.getLocalDatasetSpecs().entrySet()) { String mappedDatasetName = localDatasetEntry.getKey() + "." + runId; // try best to delete the local datasets. try { datasetFramework.deleteInstance(Id.DatasetInstance.from(namespaceId, mappedDatasetName)); } catch (InstanceNotFoundException e) { // Dataset instance is already deleted. so its no-op. } catch (Throwable t) { errorOnDelete.add(mappedDatasetName); LOG.error("Failed to delete the Workflow local dataset {}. Reason - {}", mappedDatasetName, t.getMessage()); } } if (errorOnDelete.isEmpty()) { responder.sendStatus(HttpResponseStatus.OK); return; } String errorMessage = "Failed to delete Workflow local datasets - " + Joiner.on(",").join(errorOnDelete); throw new RuntimeException(errorMessage); } /** * Get the {@link WorkflowSpecification} if valid application id, workflow id, and runid are provided. * @param namespaceId the namespace id * @param applicationId the application id * @param workflowId the workflow id * @param runId the runid of the workflow * @return the specifications for the Workflow * @throws NotFoundException is thrown when the application, workflow, or runid is not found */ private WorkflowSpecification getWorkflowSpecForValidRun(String namespaceId, String applicationId, String workflowId, String runId) throws NotFoundException { ApplicationId appId = new ApplicationId(namespaceId, applicationId); ApplicationSpecification appSpec = store.getApplication(appId.toId()); if (appSpec == null) { throw new ApplicationNotFoundException(appId.toId()); } WorkflowSpecification workflowSpec = appSpec.getWorkflows().get(workflowId); ProgramId programId = new ProgramId(namespaceId, applicationId, ProgramType.WORKFLOW, workflowId); if (workflowSpec == null) { throw new ProgramNotFoundException(programId.toId()); } if (store.getRun(programId.toId(), runId) == null) { throw new NotFoundException(new ProgramRunId(programId.getNamespace(), programId.getApplication(), programId.getType(), programId.getProgram(), runId)); } return workflowSpec; } }