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

io.cdap.cdap.internal.app.services.ProgramLifecycleService Maven / Gradle / Ivy

There is a newer version: 6.10.1
Show newest version
/*
 * Copyright © 2015-2020 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 io.cdap.cdap.internal.app.services;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.inject.Inject;
import io.cdap.cdap.api.ProgramSpecification;
import io.cdap.cdap.api.annotation.Name;
import io.cdap.cdap.api.app.ApplicationSpecification;
import io.cdap.cdap.api.artifact.ApplicationClass;
import io.cdap.cdap.api.plugin.Plugin;
import io.cdap.cdap.app.guice.ClusterMode;
import io.cdap.cdap.app.program.ProgramDescriptor;
import io.cdap.cdap.app.runtime.LogLevelUpdater;
import io.cdap.cdap.app.runtime.ProgramController;
import io.cdap.cdap.app.runtime.ProgramOptions;
import io.cdap.cdap.app.runtime.ProgramRuntimeService;
import io.cdap.cdap.app.runtime.ProgramRuntimeService.RuntimeInfo;
import io.cdap.cdap.app.runtime.ProgramStateWriter;
import io.cdap.cdap.app.store.ScanApplicationsRequest;
import io.cdap.cdap.app.store.Store;
import io.cdap.cdap.common.ApplicationNotFoundException;
import io.cdap.cdap.common.BadRequestException;
import io.cdap.cdap.common.ConflictException;
import io.cdap.cdap.common.NotFoundException;
import io.cdap.cdap.common.ProfileConflictException;
import io.cdap.cdap.common.ProgramNotFoundException;
import io.cdap.cdap.common.ProgramRunForbiddenException;
import io.cdap.cdap.common.TooManyRequestsException;
import io.cdap.cdap.common.app.RunIds;
import io.cdap.cdap.common.conf.CConfiguration;
import io.cdap.cdap.common.conf.Constants;
import io.cdap.cdap.common.id.Id;
import io.cdap.cdap.common.io.CaseInsensitiveEnumTypeAdapterFactory;
import io.cdap.cdap.config.PreferencesService;
import io.cdap.cdap.internal.app.ApplicationSpecificationAdapter;
import io.cdap.cdap.internal.app.runtime.BasicArguments;
import io.cdap.cdap.internal.app.runtime.ProgramOptionConstants;
import io.cdap.cdap.internal.app.runtime.SimpleProgramOptions;
import io.cdap.cdap.internal.app.runtime.SystemArguments;
import io.cdap.cdap.internal.app.runtime.artifact.ArtifactRepository;
import io.cdap.cdap.internal.app.store.AppMetadataStore;
import io.cdap.cdap.internal.app.store.ApplicationMeta;
import io.cdap.cdap.internal.app.store.RunRecordDetail;
import io.cdap.cdap.internal.capability.CapabilityReader;
import io.cdap.cdap.internal.pipeline.PluginRequirement;
import io.cdap.cdap.internal.profile.ProfileService;
import io.cdap.cdap.internal.provision.ProvisionerNotifier;
import io.cdap.cdap.internal.provision.ProvisioningService;
import io.cdap.cdap.proto.ProgramHistory;
import io.cdap.cdap.proto.ProgramRecord;
import io.cdap.cdap.proto.ProgramRunClusterStatus;
import io.cdap.cdap.proto.ProgramRunStatus;
import io.cdap.cdap.proto.ProgramStatus;
import io.cdap.cdap.proto.ProgramType;
import io.cdap.cdap.proto.RunCountResult;
import io.cdap.cdap.proto.RunRecord;
import io.cdap.cdap.proto.id.ApplicationId;
import io.cdap.cdap.proto.id.ApplicationReference;
import io.cdap.cdap.proto.id.EntityId;
import io.cdap.cdap.proto.id.KerberosPrincipalId;
import io.cdap.cdap.proto.id.NamespaceId;
import io.cdap.cdap.proto.id.ProfileId;
import io.cdap.cdap.proto.id.ProgramId;
import io.cdap.cdap.proto.id.ProgramReference;
import io.cdap.cdap.proto.id.ProgramRunId;
import io.cdap.cdap.proto.profile.Profile;
import io.cdap.cdap.proto.provisioner.ProvisionerDetail;
import io.cdap.cdap.proto.security.AccessPermission;
import io.cdap.cdap.proto.security.ApplicationPermission;
import io.cdap.cdap.proto.security.Principal;
import io.cdap.cdap.proto.security.StandardPermission;
import io.cdap.cdap.runtime.spi.profile.ProfileStatus;
import io.cdap.cdap.security.spi.authentication.AuthenticationContext;
import io.cdap.cdap.security.spi.authentication.SecurityRequestContext;
import io.cdap.cdap.security.spi.authorization.AccessEnforcer;
import io.cdap.cdap.security.spi.authorization.UnauthorizedException;
import org.apache.twill.api.RunId;
import org.apache.twill.api.logging.LogEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javax.annotation.Nullable;

/**
 * Service that manages lifecycle of Programs.
 */
public class ProgramLifecycleService {
  private static final Logger LOG = LoggerFactory.getLogger(ProgramLifecycleService.class);

  private static final Gson GSON = ApplicationSpecificationAdapter
    .addTypeAdapters(new GsonBuilder())
    .registerTypeAdapterFactory(new CaseInsensitiveEnumTypeAdapterFactory())
    .create();
  private static final EnumSet ACTIVE_STATES = EnumSet.of(ProgramRunStatus.PENDING,
                                                                            ProgramRunStatus.STARTING,
                                                                            ProgramRunStatus.RUNNING,
                                                                            ProgramRunStatus.SUSPENDED);

  private final Store store;
  private final ProfileService profileService;
  private final ProgramRuntimeService runtimeService;
  private final PropertiesResolver propertiesResolver;
  private final PreferencesService preferencesService;
  private final AccessEnforcer accessEnforcer;
  private final AuthenticationContext authenticationContext;
  private final ProvisionerNotifier provisionerNotifier;
  private final ProvisioningService provisioningService;
  private final ProgramStateWriter programStateWriter;
  private final CapabilityReader capabilityReader;
  private final int maxConcurrentRuns;
  private final int maxConcurrentLaunching;
  private final int defaultStopTimeoutSecs;
  private final int batchSize;
  private final ArtifactRepository artifactRepository;
  private final RunRecordMonitorService runRecordMonitorService;
  private final boolean userProgramLaunchDisabled;

  @Inject
  ProgramLifecycleService(CConfiguration cConf,
                          Store store, ProfileService profileService, ProgramRuntimeService runtimeService,
                          PropertiesResolver propertiesResolver,
                          PreferencesService preferencesService, AccessEnforcer accessEnforcer,
                          AuthenticationContext authenticationContext,
                          ProvisionerNotifier provisionerNotifier, ProvisioningService provisioningService,
                          ProgramStateWriter programStateWriter, CapabilityReader capabilityReader,
                          ArtifactRepository artifactRepository,
                          RunRecordMonitorService runRecordMonitorService) {
    this.maxConcurrentRuns = cConf.getInt(Constants.AppFabric.MAX_CONCURRENT_RUNS);
    this.maxConcurrentLaunching = cConf.getInt(Constants.AppFabric.MAX_CONCURRENT_LAUNCHING);
    this.defaultStopTimeoutSecs = cConf.getInt(Constants.AppFabric.PROGRAM_MAX_STOP_SECONDS);
    this.userProgramLaunchDisabled = cConf.getBoolean(Constants.AppFabric.USER_PROGRAM_LAUNCH_DISABLED, false);
    this.batchSize = cConf.getInt(Constants.AppFabric.STREAMING_BATCH_SIZE);
    this.store = store;
    this.profileService = profileService;
    this.runtimeService = runtimeService;
    this.propertiesResolver = propertiesResolver;
    this.preferencesService = preferencesService;
    this.accessEnforcer = accessEnforcer;
    this.authenticationContext = authenticationContext;
    this.provisionerNotifier = provisionerNotifier;
    this.provisioningService = provisioningService;
    this.programStateWriter = programStateWriter;
    this.capabilityReader = capabilityReader;
    this.artifactRepository = artifactRepository;
    this.runRecordMonitorService = runRecordMonitorService;
  }

  /**
   * Returns the program status.
   * @param programId the id of the program for which the status call is made
   * @return the status of the program
   * @throws NotFoundException if the application to which this program belongs was not found
   */
  public ProgramStatus getProgramStatus(ProgramId programId) throws Exception {
    // check that app exists
    ApplicationId appId = programId.getParent();
    ApplicationSpecification appSpec = store.getApplication(appId);
    if (appSpec == null) {
      throw new NotFoundException(appId);
    }

    return getExistingAppProgramStatus(appSpec, programId);
  }

  /**
   * Returns the status of the latest version program.
   * @param programReference the program to get status
   * @return the status of the program
   * @throws NotFoundException if the application to which this program belongs was not found
   */
  public ProgramStatus getProgramStatus(ProgramReference programReference) throws Exception {
    ProgramId programId = getLatestProgramId(programReference);
    return getProgramStatus(programId);
  }

  /**
   * Gets the {@link ProgramStatus} for the given set of programs.
   *
   * @param programRefs collection of versionless program ids for retrieving status
   * @return a {@link Map} from the {@link ProgramId} to the corresponding status; there will be no entry for programs
   * that do not exist.
   */
  public Map getProgramStatuses(Collection programRefs) throws Exception {
    // filter the result
    Set visibleEntities = accessEnforcer.isVisible(new LinkedHashSet<>(programRefs),
                                                                       authenticationContext.getPrincipal());
    List filteredRefs = programRefs.stream()
      .filter(visibleEntities::contains).collect(Collectors.toList());

    Map result = new HashMap<>();
    for (Map.Entry> entry : store.getActiveRuns(filteredRefs).entrySet()) {
      result.put(entry.getKey(), getProgramStatus(entry.getValue()));
    }
    return result;
  }

  /**
   * Returns the program run count of the given program.
   *
   * @param programId the id of the program for which the count call is made
   * @return the run count of the program
   * @throws NotFoundException if the application to which this program belongs was not found or the program is not
   *                           found in the app
   */
  public long getProgramRunCount(ProgramId programId) throws Exception {
    accessEnforcer.enforce(programId, authenticationContext.getPrincipal(), StandardPermission.GET);
    return store.getProgramRunCount(programId);
  }

  /**
   * Returns the program run count of the latest version program.
   *
   * @param programReference the program reference of the program to get run count
   * @return the run count of the latest version program
   * @throws NotFoundException if the application to which this program belongs was not found or the program is not
   *                           found in the app
   */
  public long getProgramRunCount(ProgramReference programReference)
    throws Exception {
    ProgramId programId = getLatestProgramId(programReference);
    return store.getProgramRunCount(programId);
  }

  /**
   * Returns the program run count of the program across all versions.
   *
   * @param programReference the reference of the program for which the count call is made
   * @return the run count of the program across all versions
   * @throws NotFoundException if the application to which this program belongs was not found or the program is not
   *                           found in the app
   */
  public long getProgramTotalRunCount(ProgramReference programReference) throws Exception {
    accessEnforcer.enforce(programReference, authenticationContext.getPrincipal(), StandardPermission.GET);
    return store.getProgramTotalRunCount(programReference);
  }

  /**
   * Returns the program run count of the given program id list.
   *
   * @param programRefs the list of program ids to get the count
   * @return the counts of given program ids
   */
  public List getProgramTotalRunCounts(List programRefs) throws Exception {
    // filter the result
    Principal principal = authenticationContext.getPrincipal();
    Set visibleEntities = accessEnforcer.isVisible(new HashSet<>(programRefs), principal);
    Set filteredRefs = programRefs.stream()
      .filter(visibleEntities::contains).collect(Collectors.toSet());

    Map programCounts = store.getProgramTotalRunCounts(filteredRefs).stream()
      .collect(Collectors.toMap(RunCountResult::getProgramReference, c -> c));

    List result = new ArrayList<>();
    for (ProgramReference programReference : programRefs) {
      if (!visibleEntities.contains(programReference)) {
        result.add(new RunCountResult(programReference, null, new UnauthorizedException(principal, programReference)));
      } else {
        RunCountResult count = programCounts.get(programReference);
        if (count != null) {
          result.add(count);
        } else {
          result.add(new RunCountResult(programReference, 0L, null));
        }
      }
    }
    return result;
  }

  /**
   * Returns the {@link RunRecordDetail} for the given program run reference.
   *
   * @param programRef the program reference of the run record
   * @param runId the run id of the run record
   * @return the {@link RunRecordDetail} for the given run
   * @throws NotFoundException if the given program or program run doesn't exist
   * @throws Exception if authorization failed
   */
  public RunRecordDetail getRunRecordMeta(ProgramReference programRef, String runId) throws Exception {
    accessEnforcer.enforce(programRef, authenticationContext.getPrincipal(), StandardPermission.GET);
    RunRecordDetail runRecord = store.getRun(programRef, runId);
    if (runRecord == null) {
      throw new NotFoundException(String.format("No run record found for program %s and runID: %s", programRef, runId));
    }
    ApplicationId appId = runRecord.getProgramRunId().getParent().getParent();
    ApplicationSpecification appSpec = store.getApplication(appId);
    if (appSpec == null) {
      throw new ApplicationNotFoundException(appId);
    }
    return runRecord;
  }

  /**
   * Get the latest runs within the specified start and end times for the specified program.
   *
   * @param programId the program to get runs for
   * @param programRunStatus status of runs to return
   * @param start earliest start time of runs to return
   * @param end latest start time of runs to return
   * @param limit the maximum number of runs to return
   * @return the latest runs for the program sorted by start time, with the newest run as the first run
   * @throws NotFoundException if the application to which this program belongs was not found or the program is not
   *                           found in the app
   * @throws UnauthorizedException if the principal does not have access to the program
   * @throws Exception if there was some other exception performing authorization checks
   */
  public List getRunRecordMetas(ProgramId programId, ProgramRunStatus programRunStatus,
                                                 long start, long end, int limit) throws Exception {
    accessEnforcer.enforce(programId, authenticationContext.getPrincipal(), StandardPermission.GET);
    ProgramSpecification programSpec = getProgramSpecificationWithoutAuthz(programId);
    if (programSpec == null) {
      throw new NotFoundException(programId);
    }
    return new ArrayList<>(store.getRuns(programId, programRunStatus, start, end, limit).values());
  }

  public List getRunRecords(ProgramId programId, ProgramRunStatus programRunStatus,
                                       long start, long end, int limit) throws Exception {
    return getRunRecordMetas(programId, programRunStatus, start, end, limit).stream()
      .map(record -> RunRecord.builder(record).build()).collect(Collectors.toList());
  }

  /**
   * Get the all runs within the specified start and end times for the specified program.
   *
   * @param programReference the program to get runs for
   * @param programRunStatus status of runs to return
   * @param start earliest start time of runs to return
   * @param end latest start time of runs to return
   * @param limit the maximum number of runs to return
   * @return the latest runs for the program sorted by start time, with the newest run as the first run
   * @throws NotFoundException if the application to which this program belongs was not found or the program is not
   *                           found in the app
   * @throws UnauthorizedException if the principal does not have access to the program
   * @throws Exception if there was some other exception performing authorization checks
   */
  public List getAllRunRecordMetas(ProgramReference programReference,
                                                    ProgramRunStatus programRunStatus, long start, long end,
                                                    int limit, Predicate filter) throws Exception {
    accessEnforcer.enforce(programReference, authenticationContext.getPrincipal(), StandardPermission.GET);
    ProgramSpecification programSpec = getLatestProgramSpecificationWithoutAuthz(programReference);
    if (programSpec == null) {
      throw new NotFoundException(programReference);
    }
    return new ArrayList<>(store.getAllRuns(programReference, programRunStatus, start, end, limit, filter).values());
  }

  public List getAllRunRecords(ProgramReference programReference, ProgramRunStatus programRunStatus,
                                          long start, long end, int limit, Predicate filter)
    throws Exception {
    return getAllRunRecordMetas(programReference, programRunStatus, start, end, limit, filter).stream()
      .map(record -> RunRecord.builder(record).build()).collect(Collectors.toList());
  }

  public List getRunRecords(ProgramReference programReference, ProgramRunStatus programRunStatus,
                                       long start, long end, int limit) throws Exception {
    ProgramId programId = getLatestProgramId(programReference);
    return getRunRecords(programId, programRunStatus, start, end, limit);
  }

  /**
   * Get the latest runs within the specified start and end times for the specified programs.
   *
   * @param programs the programs to get runs for
   * @param programRunStatus status of runs to return
   * @param start earliest start time of runs to return
   * @param end latest start time of runs to return
   * @param limit the maximum number of runs per program to return
   * @return the latest runs for the program sorted by start time, with the newest run as the first run
   * @throws NotFoundException if the application to which this program belongs was not found or the program is not
   *                           found in the app
   * @throws UnauthorizedException if the principal does not have access to the program
   * @throws Exception if there was some other exception performing authorization checks
   */
  public List getRunRecords(Collection programs, ProgramRunStatus programRunStatus,
                                            long start, long end, int limit) throws Exception {
    List result = new ArrayList<>();

    // do this in batches to avoid transaction timeouts.
    List batch = new ArrayList<>(20);

    for (ProgramReference program : programs) {
      batch.add(program);

      if (batch.size() >= 20) {
        addProgramHistory(result, batch, programRunStatus, start, end, limit);
        batch.clear();
      }
    }
    if (!batch.isEmpty()) {
      addProgramHistory(result, batch, programRunStatus, start, end, limit);
    }
    return result;
  }

  private void addProgramHistory(List histories, List programs,
                                 ProgramRunStatus programRunStatus, long start, long end, int limit) throws Exception {
    Set visibleEntities = accessEnforcer.isVisible(new HashSet<>(programs),
                                                                       authenticationContext.getPrincipal());

    for (ProgramHistory programHistory : store.getRuns(programs, programRunStatus, start, end, limit)) {
      ProgramReference programId = programHistory.getProgramId().getProgramReference();
      if (visibleEntities.contains(programId)) {
        histories.add(programHistory);
      } else {
        histories.add(new ProgramHistory(programHistory.getProgramId(), Collections.emptyList(),
                                         new UnauthorizedException(authenticationContext.getPrincipal(),
                                                                   programHistory.getProgramId())));
      }
    }
  }

  /**
   * Returns the program status with no need of application existence check.
   * @param appSpec the ApplicationSpecification of the existing application
   * @param programId the id of the program for which the status call is made
   * @return the status of the program
   * @throws NotFoundException if the application to which this program belongs was not found
   */
  private ProgramStatus getExistingAppProgramStatus(ApplicationSpecification appSpec,
                                                    ProgramId programId) throws Exception {
    accessEnforcer.enforce(programId, authenticationContext.getPrincipal(), StandardPermission.GET);
    ProgramSpecification spec = getExistingAppProgramSpecification(appSpec, programId.getProgramReference());
    if (spec == null) {
      // program doesn't exist
      throw new NotFoundException(programId);
    }

    return getProgramStatus(store.getActiveRuns(programId).values());
  }

  /**
   * Returns the program status based on the active run records of a program.
   * A program is RUNNING if there are any RUNNING, STOPPING, or SUSPENDED run records.
   * A program is starting if there are any PENDING or STARTING run records and no RUNNING run records.
   * Otherwise, it is STOPPED.
   *
   * @param runRecords run records for the program
   * @return the program status
   */
  @VisibleForTesting
  static ProgramStatus getProgramStatus(Collection runRecords) {
    boolean hasStarting = false;
    for (RunRecordDetail runRecord : runRecords) {
      ProgramRunStatus runStatus = runRecord.getStatus();
      if (runStatus == ProgramRunStatus.RUNNING || runStatus == ProgramRunStatus.SUSPENDED
        || runStatus == ProgramRunStatus.STOPPING) {
        return ProgramStatus.RUNNING;
      }
      hasStarting = hasStarting || runStatus == ProgramRunStatus.STARTING || runStatus == ProgramRunStatus.PENDING;
    }
    return hasStarting ? ProgramStatus.STARTING : ProgramStatus.STOPPED;
  }

  /**
   * Returns the {@link ProgramSpecification} for the specified {@link ProgramId program}.
   *
   * @param programId the {@link ProgramId program} for which the {@link ProgramSpecification} is requested
   * @return the {@link ProgramSpecification} for the specified {@link ProgramId program}, or {@code null} if it does
   *         not exist
   */
  @Nullable
  public ProgramSpecification getProgramSpecification(ProgramId programId) throws Exception {
    accessEnforcer.enforce(programId, authenticationContext.getPrincipal(), StandardPermission.GET);
    return getProgramSpecificationWithoutAuthz(programId);
  }

  /**
   * Returns the {@link ProgramSpecification} for the latest version program.
   *
   * @param programReference the program to get specification
   * @return the {@link ProgramSpecification} for the specified {@link ProgramId program}, or {@code null} if it does
   *         not exist
   */
  @Nullable
  public ProgramSpecification getProgramSpecification(ProgramReference programReference) throws Exception {
    ProgramId programId = getLatestProgramId(programReference);
    return getProgramSpecification(programId);
  }

  /**
   * Returns the {@link ProgramSpecification} for the specified {@link ProgramId program}.
   * @param appSpec the {@link ApplicationSpecification} of the existing application
   * @param programReference the {@link ProgramReference program} for which the {@link ProgramSpecification}
   * is requested
   * @return the {@link ProgramSpecification} for the specified {@link ProgramId program}, or {@code null} if it does
   *         not exist
   */
  @Nullable
  private ProgramSpecification getExistingAppProgramSpecification(ApplicationSpecification appSpec,
                                                                  ProgramReference programReference) {
    String programName = programReference.getProgram();
    ProgramType type = programReference.getType();
    ProgramSpecification programSpec;
    if (type == ProgramType.MAPREDUCE && appSpec.getMapReduce().containsKey(programName)) {
      programSpec = appSpec.getMapReduce().get(programName);
    } else if (type == ProgramType.SPARK && appSpec.getSpark().containsKey(programName)) {
      programSpec = appSpec.getSpark().get(programName);
    } else if (type == ProgramType.WORKFLOW && appSpec.getWorkflows().containsKey(programName)) {
      programSpec = appSpec.getWorkflows().get(programName);
    } else if (type == ProgramType.SERVICE && appSpec.getServices().containsKey(programName)) {
      programSpec = appSpec.getServices().get(programName);
    } else if (type == ProgramType.WORKER && appSpec.getWorkers().containsKey(programName)) {
      programSpec = appSpec.getWorkers().get(programName);
    } else {
      programSpec = null;
    }
    return programSpec;
  }

  /**
   * Starts a Program with the specified argument overrides.
   *
   * @param programId the {@link ProgramId} to start/stop
   * @param overrides the arguments to override in the program's configured user arguments before starting
   * @param debug {@code true} if the program is to be started in debug mode, {@code false} otherwise
   * @return {@link RunId}
   * @throws ConflictException if the specified program is already running, and if concurrent runs are not allowed
   * @throws NotFoundException if the specified program or the app it belongs to is not found in the specified namespace
   * @throws IOException if there is an error starting the program
   * @throws UnauthorizedException if the logged in user is not authorized to start the program. To start a program,
   *                               a user requires {@link ApplicationPermission#EXECUTE} on the program
   * @throws Exception if there were other exceptions checking if the current user is authorized to start the program
   */
  @Deprecated
  public RunId run(ProgramId programId, Map overrides, boolean debug) throws Exception {
    accessEnforcer.enforce(programId, authenticationContext.getPrincipal(), ApplicationPermission.EXECUTE);
    checkLatestVersionExeceution(programId);
    checkConcurrentExecution(programId);

    Map sysArgs = propertiesResolver.getSystemProperties(programId);
    Map userArgs = propertiesResolver.getUserProperties(programId);
    if (overrides != null) {
      userArgs.putAll(overrides);
    }

    authorizePipelineRuntimeImpersonation(userArgs);

    return runInternal(programId, userArgs, sysArgs, debug);
  }

  /**
   * Starts the latest version of a Program with the specified argument overrides.
   *
   * @param programReference the program to run
   * @param overrides the arguments to override in the program's configured user arguments before starting
   * @param debug {@code true} if the program is to be started in debug mode, {@code false} otherwise
   * @return {@link RunId}
   * @throws ConflictException if the specified program is already running, and if concurrent runs are not allowed
   * @throws NotFoundException if the specified program or the app it belongs to is not found in the specified namespace
   * @throws IOException if there is an error starting the program
   * @throws UnauthorizedException if the logged in user is not authorized to start the program. To start a program,
   *                               a user requires {@link ApplicationPermission#EXECUTE} on the program
   * @throws Exception if there were other exceptions checking if the current user is authorized to start the program
   */
  public RunId run(ProgramReference programReference, Map overrides, boolean debug) throws Exception {
    ProgramId programId = getLatestProgramId(programReference);
    return run(programId, overrides, debug);
  }

  /**
   * Starts programs which were suspended between startTime and endTime.
   *
   * @param applicationId the application to restart programs in
   * @param startTimeSeconds earliest stop time in seconds of programs to restart
   * @param endTimeSeconds latest stop time in seconds of programs to restart
   * @return {@link Set} of runs restarted
   * @throws ConflictException if the specified program is already running, and if concurrent runs are not allowed
   * @throws NotFoundException if the specified program or the app it belongs to is not found in the specified namespace
   * @throws IOException if there is an error starting the program
   * @throws UnauthorizedException if the logged in user is not authorized to start the program. To start a program,
   *                               a user requires {@link ApplicationPermission#EXECUTE} on the program
   * @throws Exception if there were other exceptions checking if the current user is authorized to start the program
   */
  public Set restart(ApplicationId applicationId, long startTimeSeconds, long endTimeSeconds) throws Exception {
    Set runs = new HashSet<>();
    Map runMap =
      store.getRuns(applicationId, ProgramRunStatus.KILLED, Integer.MAX_VALUE, meta -> {
        Long stopTime = meta.getStopTs();
        return stopTime != null && stopTime >= startTimeSeconds && stopTime < endTimeSeconds;
      });

    for (ProgramRunId programRunId : runMap.keySet()) {
      ProgramId programId = programRunId.getParent();
      runs.add(run(programId, getRuntimeArgs(programId), false));
    }

    return runs;
  }

  public Set restart(ApplicationReference appReference, long startTimeSeconds, long endTimeSeconds)
    throws Exception {
    ApplicationId applicationId = getLatestApplicationId(appReference);
    return restart(applicationId, startTimeSeconds, endTimeSeconds);
  }

  /**
   * Stop all active programs for the given application across all versions
   * @param applicationReference application reference of the app to stop all active programs
   * @throws Exception
   */
  public void stopAll(ApplicationReference applicationReference) throws Exception {
    Map runMap = store.getAllActiveRuns(applicationReference);
    for (ProgramRunId programRunId : runMap.keySet()) {
      stop(programRunId.getParent(), programRunId.getRun(), 0);
    }
  }

  /**
   * Runs a Program without authorization.
   *
   * Note that this method should only be called through internal service, it does not have auth check for starting the
   * program.
   *
   * @param programId the {@link ProgramId program} to run
   * @param userArgs user arguments
   * @param sysArgs system arguments
   * @param debug whether to start as a debug run
   * @return {@link RunId}
   * @throws IOException if there is an error starting the program
   * @throws NotFoundException if the namespace, application, or program is not found
   * @throws ProfileConflictException if the profile is disabled
   * @throws Exception if there were other exceptions
   */
  public RunId runInternal(ProgramId programId, Map userArgs, Map sysArgs,
                           boolean debug) throws Exception {
    RunId runId = RunIds.generate();
    ProgramOptions programOptions = createProgramOptions(programId, userArgs, sysArgs, debug);
    ProgramDescriptor programDescriptor = store.loadProgram(programId);
    String userId = SecurityRequestContext.getUserId();
    userId = userId == null ? "" : userId;

    checkCapability(programDescriptor);

    ProgramRunId programRunId = programId.run(runId);
    RunRecordMonitorService.Counter counter = runRecordMonitorService.addRequestAndGetCount(programRunId);

    boolean done = false;
    try {
      if (maxConcurrentRuns >= 0 && maxConcurrentRuns < counter.getLaunchingCount() + counter.getRunningCount()) {
        String msg =
          String.format("Program %s cannot start because the maximum of %d outstanding runs is allowed",
                        programId, maxConcurrentRuns);
        LOG.info(msg);

        TooManyRequestsException e = new TooManyRequestsException(msg);
        programStateWriter.reject(programRunId, programOptions, programDescriptor, userId, e);
        throw e;
      }

      if (maxConcurrentLaunching >= 0 && maxConcurrentLaunching < counter.getLaunchingCount()) {
        String msg = String.format("Program %s cannot start because the maximum of %d concurrent " +
                                     "provisioning/starting runs is allowed", programId, maxConcurrentLaunching);
        LOG.info(msg);

        TooManyRequestsException e = new TooManyRequestsException(msg);
        programStateWriter.reject(programRunId, programOptions, programDescriptor, userId, e);
        throw e;
      }

      LOG.info("Attempt to run {} program {} as user {} with arguments {}", programId.getType(),
               programId.getProgram(), decodeUserId(userId), userArgs);

      provisionerNotifier.provisioning(programRunId, programOptions, programDescriptor, userId);
      done = true;
    } finally {
      if (!done) {
        runRecordMonitorService.removeRequest(programRunId, false);
      }
    }

    return runId;
  }

  @VisibleForTesting
  ProgramOptions createProgramOptions(ProgramId programId, Map userArgs, Map sysArgs,
                                      boolean debug)
    throws NotFoundException, ProfileConflictException, ProgramRunForbiddenException {
    ProfileId profileId = SystemArguments.getProfileIdForProgram(programId, userArgs);
    Map profileProperties = SystemArguments.getProfileProperties(userArgs);
    Profile profile = profileService.getProfile(profileId, profileProperties);
    if (profile.getStatus() == ProfileStatus.DISABLED) {
      throw new ProfileConflictException(String.format("Profile %s in namespace %s is disabled. It cannot be " +
                                                         "used to start the program %s",
                                                       profileId.getProfile(), profileId.getNamespace(),
                                                       programId.toString()), profileId);
    }
    if (userProgramLaunchDisabled && profileId == ProfileId.NATIVE
      && programId.getNamespaceId() != NamespaceId.SYSTEM) {
      throw new ProgramRunForbiddenException(programId);
    }
    ProvisionerDetail spec = provisioningService.getProvisionerDetail(profile.getProvisioner().getName());
    if (spec == null) {
      throw new NotFoundException(String.format("Provisioner '%s' not found.", profile.getProvisioner().getName()));
    }
    // get and add any user overrides for profile properties
    Map systemArgs = new HashMap<>(sysArgs);
    // add profile properties to the system arguments
    SystemArguments.addProfileArgs(systemArgs, profile);

    // Set the ClusterMode. If it is NATIVE profile, then it is ON_PREMISE, otherwise is ISOLATED
    // This should probably move into the provisioner later once we have a better contract for the
    // provisioner to actually pick what launching mechanism it wants to use.
    systemArgs.put(ProgramOptionConstants.CLUSTER_MODE,
                   (ProfileId.NATIVE.equals(profileId) ? ClusterMode.ON_PREMISE : ClusterMode.ISOLATED).name());
    ProgramSpecification programSpecification = getProgramSpecificationWithoutAuthz(programId);
    if (programSpecification == null) {
      throw new NotFoundException(programId);
    }
    addAppCDAPVersion(programId, systemArgs);
    // put all the plugin requirements if any involved in the run
    systemArgs.put(ProgramOptionConstants.PLUGIN_REQUIREMENTS,
                   GSON.toJson(getPluginRequirements(programSpecification)));
    return new SimpleProgramOptions(programId, new BasicArguments(systemArgs), new BasicArguments(userArgs), debug);
  }

  /**
   * Starts a Program with the specified argument overrides, skipping cluster lifecycle steps in the run.
   * NOTE: This method should only be called from preview runner.
   *
   * @param programId the {@link ProgramId} to start/stop
   * @param overrides the arguments to override in the program's configured user arguments before starting
   * @param debug {@code true} if the program is to be started in debug mode, {@code false} otherwise
   * @param isPreview true if the program is for preview run, for preview run, the app is already deployed with resolved
   *                  properties, so no need to regenerate app spec again
   * @return {@link ProgramController}
   * @throws ConflictException if the specified program is already running, and if concurrent runs are not allowed
   * @throws NotFoundException if the specified program or the app it belongs to is not found in the specified namespace
   * @throws IOException if there is an error starting the program
   * @throws UnauthorizedException if the logged in user is not authorized to start the program. To start a program,
   *                               a user requires {@link ApplicationPermission#EXECUTE} on the program
   * @throws Exception if there were other exceptions checking if the current user is authorized to start the program
   */
  public ProgramController start(ProgramId programId, Map overrides, boolean debug,
                                 boolean isPreview) throws Exception {
    accessEnforcer.enforce(programId, authenticationContext.getPrincipal(), ApplicationPermission.EXECUTE);
    checkConcurrentExecution(programId);

    Map sysArgs = propertiesResolver.getSystemProperties(programId);
    addAppCDAPVersion(programId, sysArgs);
    sysArgs.put(ProgramOptionConstants.SKIP_PROVISIONING, "true");
    sysArgs.put(SystemArguments.PROFILE_NAME, ProfileId.NATIVE.getScopedName());
    sysArgs.put(ProgramOptionConstants.IS_PREVIEW, Boolean.toString(isPreview));
    Map userArgs = propertiesResolver.getUserProperties(programId);
    if (overrides != null) {
      userArgs.putAll(overrides);
    }

    authorizePipelineRuntimeImpersonation(userArgs);

    BasicArguments systemArguments = new BasicArguments(sysArgs);
    BasicArguments userArguments = new BasicArguments(userArgs);
    ProgramOptions options = new SimpleProgramOptions(programId, systemArguments, userArguments, debug);
    ProgramDescriptor programDescriptor = store.loadProgram(programId);
    ProgramRunId programRunId = programId.run(RunIds.generate());

    checkCapability(programDescriptor);

    programStateWriter.start(programRunId, options, null, programDescriptor);
    return startInternal(programDescriptor, options, programRunId);
  }

  private void checkCapability(ProgramDescriptor programDescriptor) throws Exception {
    //check for capability at application class level
    Set applicationClasses = artifactRepository
      .getArtifact(Id.Artifact.fromEntityId(programDescriptor.getArtifactId())).getMeta().getClasses()
      .getApps();
    for (ApplicationClass applicationClass : applicationClasses) {
      Set capabilities = applicationClass.getRequirements().getCapabilities();
      capabilityReader.checkAllEnabled(capabilities);
    }
    for (Map.Entry pluginEntry : programDescriptor.getApplicationSpecification().getPlugins()
      .entrySet()) {
      Set capabilities = pluginEntry.getValue().getPluginClass().getRequirements().getCapabilities();
      capabilityReader.checkAllEnabled(capabilities);
    }
  }

  /**
   * Starts a Program run with the given arguments. This method skips cluster lifecycle steps and
   * does not perform authorization checks. If the program is already started, returns the controller for the program.
   * NOTE: This method should only be used from this service and the {@link ProgramNotificationSubscriberService}
   * upon receiving a {@link ProgramRunClusterStatus#PROVISIONED} state.
   *
   * @param programDescriptor descriptor of the program to run
   * @param programOptions options for the program run
   * @param programRunId program run id
   * @return controller for the program
   */
  ProgramController startInternal(ProgramDescriptor programDescriptor,
                                  ProgramOptions programOptions, ProgramRunId programRunId) {
    RunId runId = RunIds.fromString(programRunId.getRun());

    synchronized (this) {
      RuntimeInfo runtimeInfo = runtimeService.lookup(programRunId.getParent(), runId);
      if (runtimeInfo != null) {
        return runtimeInfo.getController();
      }
      return runtimeService.run(programDescriptor, programOptions, runId).getController();
    }
  }

  /**
   * Stops the specified program. The first run of the program as found by {@link ProgramRuntimeService} is stopped.
   *
   * @param programId the {@link ProgramId program} to stop
   * @throws NotFoundException if the app, program or run was not found
   * @throws BadRequestException if an attempt is made to stop a program that is either not running or
   *                             was started by a workflow
   * @throws InterruptedException if there was a problem while waiting for the stop call to complete
   * @throws ExecutionException if there was a problem while waiting for the stop call to complete
   */
  public void stop(ProgramId programId) throws Exception {
    stop(programId, null, 0);
  }

  /**
   * Stops the latest version of a program. The first run of the latest version program as found by
   * {@link ProgramRuntimeService} is stopped.
   *
   * @param programReference the programReference of the program to stop
   * @throws NotFoundException if the app, program or run was not found
   * @throws BadRequestException if an attempt is made to stop a program that is either not running or
   *                             was started by a workflow
   * @throws InterruptedException if there was a problem while waiting for the stop call to complete
   * @throws ExecutionException if there was a problem while waiting for the stop call to complete
   */
  public void stop(ProgramReference programReference) throws Exception {
    ProgramId programId = getLatestProgramId(programReference);
    stop(programId, null, 0);
  }

  /**
   * Stops the specified run of the specified program.
   *
   * @param programId the {@link ProgramId program} to stop
   * @param runId the runId of the program run to stop. If null, all runs of the program as returned by
   *              {@link ProgramRuntimeService} are stopped.
   * @param gracefulShutdownSecs amount of seconds to wait for graceful shutdown before killing the run
   * @throws NotFoundException if the app, program or run was not found
   * @throws BadRequestException if an attempt is made to stop a program that is either not running or
   *                             was started by a workflow
   * @throws InterruptedException if there was a problem while waiting for the stop call to complete
   * @throws ExecutionException if there was a problem while waiting for the stop call to complete
   */
  public void stop(ProgramId programId, @Nullable String runId, @Nullable Integer gracefulShutdownSecs)
    throws Exception {
    issueStop(programId, runId, gracefulShutdownSecs);
  }

  /**
   * Stops the specified run of the specified program.
   *
   * @param programRef the {@link ProgramReference program} to stop
   * @param runId the run id, cannot be null
   * @param gracefulShutdownSecs amount of seconds to wait for graceful shutdown before killing the run
   * @throws NotFoundException if the app, program or run was not found
   * @throws BadRequestException if an attempt is made to stop a program that is either not running or
   *                             was started by a workflow
   * @throws InterruptedException if there was a problem while waiting for the stop call to complete
   * @throws ExecutionException if there was a problem while waiting for the stop call to complete
   */
  public void stop(ProgramReference programRef, String runId, @Nullable Integer gracefulShutdownSecs)
    throws Exception {
    issueStop(programRef, runId, gracefulShutdownSecs);
  }

  /**
   * Issues a command to stop the specified {@link ProgramReference} and run id returns a
   * {@link ListenableFuture} with the {@link ProgramRunId} for the runs that were stopped.
   * Clients can wait for completion of the {@link ListenableFuture}.
   *
   * @param programRef the {@link ProgramReference program} to issue a stop for
   * @param runId run id to stop
   * @param gracefulShutdownSecs amount of seconds to wait for graceful shutdown before killing the run
   * @return a list of {@link ListenableFuture} with the {@link ProgramRunId} that clients can wait on for stop
   *         to complete.
   * @throws NotFoundException if the app, program or run was not found
   * @throws BadRequestException if an attempt is made to stop a program that is either not running or
   *                             was started by a workflow
   * @throws UnauthorizedException if the user issuing the command is not authorized to stop the program. To stop a
   *                               program, a user requires {@link ApplicationPermission#EXECUTE} permission on
   *                               the program.
   */
  public Collection issueStop(ProgramReference programRef, String runId,
                                            @Nullable Integer gracefulShutdownSecs) throws Exception {
    ProgramId defaultVersionedProgram = programRef.id(ApplicationId.DEFAULT_VERSION);
    accessEnforcer.enforce(defaultVersionedProgram, authenticationContext.getPrincipal(),
                           ApplicationPermission.EXECUTE);

    if (gracefulShutdownSecs == null) {
      gracefulShutdownSecs = this.defaultStopTimeoutSecs;
    }

    RunRecordDetail runRecord = store.getRun(programRef, runId);

    if (runRecord == null || !ACTIVE_STATES.contains(runRecord.getStatus())) {
      // Error out if no run information from run record
      ensureLatestProgramExists(programRef);
      throw new BadRequestException(String.format("Program '%s' is not running.", programRef));
    }

    return issueStopInternal(Collections.singletonMap(runRecord.getProgramRunId(), runRecord),
                             gracefulShutdownSecs);
  }

  /**
   * Issues a command to stop the specified {@link RunId} of the specified {@link ProgramId} and returns a
   * {@link ListenableFuture} with the {@link ProgramRunId} for the runs that were stopped.
   * Clients can wait for completion of the {@link ListenableFuture}.
   *
   * @param programId the {@link ProgramId program} to issue a stop for
   * @param runId the runId of the program run to stop. If null, all runs of the program are stopped.
   * @param gracefulShutdownSecs amount of seconds to wait for graceful shutdown before killing the run
   * @return a list of {@link ListenableFuture} with the {@link ProgramRunId} that clients can wait on for stop
   *         to complete.
   * @throws NotFoundException if the app, program or run was not found
   * @throws BadRequestException if an attempt is made to stop a program that is either not running or
   *                             was started by a workflow
   * @throws UnauthorizedException if the user issuing the command is not authorized to stop the program. To stop a
   *                               program, a user requires {@link ApplicationPermission#EXECUTE} permission on
   *                               the program.
   */
  public Collection issueStop(ProgramId programId, @Nullable String runId,
                                            @Nullable Integer gracefulShutdownSecs) throws Exception {
    accessEnforcer.enforce(programId, authenticationContext.getPrincipal(), ApplicationPermission.EXECUTE);

    if (gracefulShutdownSecs == null) {
      gracefulShutdownSecs = this.defaultStopTimeoutSecs;
    }
    Map activeRunRecords = getActiveRuns(programId, runId);

    if (activeRunRecords.isEmpty()) {
      // Error out if no run information from run record
      ensureProgramExists(programId);
      throw new BadRequestException(String.format("Program '%s' is not running.", programId));
    }

    return issueStopInternal(activeRunRecords, gracefulShutdownSecs);
  }

  private Collection issueStopInternal(Map activeRunRecords,
                                                     Integer gracefulShutdownSecs) throws BadRequestException {
    for (Map.Entry activeRunRecord : activeRunRecords.entrySet()) {
      ProgramRunId activeRunId = activeRunRecord.getKey();
      RunRecordDetail runRecord = activeRunRecord.getValue();

      // Check if the program is actually started from workflow and the workflow is running
      if (runRecord != null && runRecord.getProperties().containsKey(AppMetadataStore.WORKFLOW_RUNID)
        && runRecord.getStatus().equals(ProgramRunStatus.RUNNING)) {
        String workflowRunId = runRecord.getProperties().get(AppMetadataStore.WORKFLOW_RUNID);
        throw new BadRequestException(String.format("Cannot stop the program run '%s' started by the Workflow " +
                                                      "run '%s'. Please stop the Workflow.", activeRunId,
                                                    workflowRunId));
      }
      // send a message to stop the program run
      LOG.info("Issuing a program stop request with a timeout value of {} secs by user {}", gracefulShutdownSecs,
               decodeUserId(SecurityRequestContext.getUserId()));
      programStateWriter.stop(activeRunId, gracefulShutdownSecs);
    }

    return activeRunRecords.keySet();
  }

  /**
   * Save runtime arguments for all future runs of this program. The runtime arguments are saved in the
   * {@link PreferencesService}.
   *
   * @param programId the {@link ProgramId program} for which runtime arguments are to be saved
   * @param runtimeArgs the runtime arguments to save
   * @throws NotFoundException if the specified program was not found
   * @throws UnauthorizedException if the current user does not have sufficient privileges to save runtime arguments for
   *                               the specified program. To save runtime arguments for a program, a user requires
   *                               {@link StandardPermission#UPDATE} privileges on the program.
   */
  public void saveRuntimeArgs(ProgramId programId, Map runtimeArgs) throws Exception {
    accessEnforcer.enforce(programId, authenticationContext.getPrincipal(), StandardPermission.UPDATE);
    ensureProgramExists(programId);
    preferencesService.setProperties(programId, runtimeArgs);
  }

  /**
   * Gets runtime arguments for the program from the {@link PreferencesService}
   *
   * @param programId the {@link ProgramId program} for which runtime arguments needs to be retrieved
   * @return {@link Map} containing runtime arguments of the program
   * @throws NotFoundException if the specified program was not found
   * @throws UnauthorizedException if the current user does not have sufficient privileges to get runtime arguments for
   * the specified program. To get runtime arguments for a program, a user requires
   * {@link StandardPermission#GET} privileges on the program.
   */
  public Map getRuntimeArgs(@Name("programId") ProgramId programId) throws Exception {
    accessEnforcer.enforce(programId, authenticationContext.getPrincipal(), StandardPermission.GET);
    ensureProgramExists(programId);
    return preferencesService.getProperties(programId);
  }

  /**
   * Update log levels for the given program. Only supported program types for this action are
   * {@link ProgramType#SERVICE} and {@link ProgramType#WORKER}.
   *
   * @param programId the {@link ProgramId} of the program for which log levels are to be updated
   * @param logLevels the {@link Map} of the log levels to be updated.
   * @param runId the run id of the program.
   * @throws InterruptedException if there is an error while asynchronously updating log levels.
   * @throws ExecutionException if there is an error while asynchronously updating log levels.
   * @throws BadRequestException if the log level is not valid or the program type is not supported.
   * @throws UnauthorizedException if the user does not have privileges to update log levels for the specified program.
   *                               To update log levels for a program, a user needs {@link StandardPermission#UPDATE}
   *                               on the program.
   */
  public void updateProgramLogLevels(ProgramId programId, Map logLevels,
                                     @Nullable String runId) throws Exception {
    accessEnforcer.enforce(programId, authenticationContext.getPrincipal(), StandardPermission.UPDATE);
    if (!EnumSet.of(ProgramType.SERVICE, ProgramType.WORKER).contains(programId.getType())) {
      throw new BadRequestException(String.format("Updating log levels for program type %s is not supported",
                                                  programId.getType().getPrettyName()));
    }
    updateLogLevels(programId, logLevels, runId);
  }

  /**
   * Reset log levels for the given program. Only supported program types for this action are
   * {@link ProgramType#SERVICE} and {@link ProgramType#WORKER}.
   *
   * @param programId the {@link ProgramId} of the program for which log levels are to be reset.
   * @param loggerNames the {@link String} set of the logger names to be updated, empty means reset for all
   *                    loggers.
   * @param runId the run id of the program.
   * @throws InterruptedException if there is an error while asynchronously resetting log levels.
   * @throws ExecutionException if there is an error while asynchronously resetting log levels.
   * @throws UnauthorizedException if the user does not have privileges to reset log levels for the specified program.
   *                               To reset log levels for a program, a user needs {@link StandardPermission#UPDATE}
   *                               on the program.
   */
  public void resetProgramLogLevels(ProgramId programId, Set loggerNames,
                                    @Nullable String runId) throws Exception {
    accessEnforcer.enforce(programId, authenticationContext.getPrincipal(), StandardPermission.UPDATE);
    if (!EnumSet.of(ProgramType.SERVICE, ProgramType.WORKER).contains(programId.getType())) {
      throw new BadRequestException(String.format("Resetting log levels for program type %s is not supported",
                                                  programId.getType().getPrettyName()));
    }
    resetLogLevels(programId, loggerNames, runId);
  }

  /**
   * Ensures the caller is authorized to check if the given program exists.
   */
  public void ensureProgramExists(ProgramId programId) throws Exception {
    accessEnforcer.enforce(programId, authenticationContext.getPrincipal(), StandardPermission.GET);

    ApplicationSpecification appSpec = store.getApplication(programId.getParent());
    if (appSpec == null) {
      throw new ProgramNotFoundException(programId);
    }

    Store.ensureProgramExists(programId, appSpec);
  }

  /**
   * Ensures the caller is authorized to check if the latest version of given program exists.
   */
  public void ensureLatestProgramExists(ProgramReference programRef) throws Exception {
    accessEnforcer.enforce(programRef.id(ApplicationId.DEFAULT_VERSION),
                           authenticationContext.getPrincipal(), StandardPermission.GET);

    ApplicationSpecification appSpec = Optional.ofNullable(store.getLatest(programRef.getParent()))
      .map(ApplicationMeta::getSpec)
      .orElse(null);
    if (appSpec == null) {
      throw new ProgramNotFoundException(programRef);
    }

    Store.ensureProgramExists(programRef.id(appSpec.getAppVersion()), appSpec);
  }

  private boolean isStopped(ProgramId programId) throws Exception {
    return ProgramStatus.STOPPED == getProgramStatus(programId);
  }

  /**
   * Checks if the given program is running and is allowed for concurrent execution.
   *
   * @param programId the program Id to check
   * @throws ConflictException if concurrent execution is not allowed and has an existing run
   * @throws NotFoundException if the program is not found
   * @throws Exception if failed to determine the state
   */
  private synchronized void checkConcurrentExecution(ProgramId programId) throws Exception {
    if (isConcurrentRunsInSameAppForbidden(programId.getType())) {
      Map runs = runtimeService.list(programId);
      if (!runs.isEmpty() || !isStoppedInSameProgram(programId)) {
        throw new ConflictException(
          String.format("Program %s is already running in an version of the same application with run ids %s",
                        programId, runs.keySet()));
      }
    }
    if (!isConcurrentRunsAllowed(programId.getType())) {
      List runIds = new ArrayList<>();
      for (Map.Entry entry : runtimeService.list(programId.getType()).entrySet()) {
        if (programId.equals(entry.getValue().getProgramId())) {
          runIds.add(entry.getKey());
        }
      }
      if (!runIds.isEmpty() || !isStopped(programId)) {
        throw new ConflictException(String.format("Program %s is already running with run ids %s", programId, runIds));
      }
    }
  }

  /**
   * Returns whether the given program is stopped in all versions of the app.
   * @param programId the id of the program for which the stopped status in all versions of the app is found
   * @return whether the given program is stopped in all versions of the app
   * @throws NotFoundException if the application to which this program belongs was not found
   */
  private boolean isStoppedInSameProgram(ProgramId programId) throws Exception {
    // check that app exists
    Collection appIds = store.getAllAppVersionsAppIds(programId.getParent().getAppReference());
    if (appIds == null || appIds.isEmpty()) {
      throw new NotFoundException(Id.Application.from(programId.getNamespace(), programId.getApplication()));
    }
    ApplicationSpecification appSpec = store.getApplication(programId.getParent());
    for (ApplicationId appId : appIds) {
      ProgramId pId = appId.program(programId.getType(), programId.getProgram());
      if (!getExistingAppProgramStatus(appSpec, pId).equals(ProgramStatus.STOPPED)) {
        return false;
      }
    }
    return true;
  }

  private boolean isConcurrentRunsInSameAppForbidden(ProgramType type) {
    // Concurrent runs in different (or same) versions of an application are forbidden for worker
    return EnumSet.of(ProgramType.WORKER).contains(type);
  }

  private boolean isConcurrentRunsAllowed(ProgramType type) {
    // Concurrent runs are only allowed for the Workflow, MapReduce and Spark
    return EnumSet.of(ProgramType.WORKFLOW, ProgramType.MAPREDUCE, ProgramType.SPARK).contains(type);
  }

  private Map findRuntimeInfo(
    ProgramId programId, @Nullable String runId) throws BadRequestException {

    if (runId != null) {
      RunId run;
      try {
        run = RunIds.fromString(runId);
      } catch (IllegalArgumentException e) {
        throw new BadRequestException("Error parsing run-id.", e);
      }
      ProgramRuntimeService.RuntimeInfo runtimeInfo = runtimeService.lookup(programId, run);
      return runtimeInfo == null ? Collections.emptyMap() : Collections.singletonMap(run, runtimeInfo);
    }
    return new HashMap<>(runtimeService.list(programId));
  }

  @Nullable
  private ProgramRuntimeService.RuntimeInfo findRuntimeInfo(ProgramId programId) throws BadRequestException {
    return findRuntimeInfo(programId, null).values().stream().findFirst().orElse(null);
  }

  /**
   * Set instances for the given program. Only supported program types for this action are
   * {@link ProgramType#SERVICE} and {@link ProgramType#WORKER}.
   *
   * @param programId the {@link ProgramId} of the program for which instances are to be updated
   * @param instances the number of instances to be updated.
   * @throws InterruptedException if there is an error while asynchronously updating instances
   * @throws ExecutionException if there is an error while asynchronously updating instances
   * @throws BadRequestException if the number of instances specified is less than 0
   * @throws UnauthorizedException if the user does not have privileges to set instances for the specified program.
   *                               To set instances for a program, a user needs {@link StandardPermission#UPDATE}
   *                               on the program.
   */
  public void setInstances(ProgramId programId, int instances) throws Exception {
    accessEnforcer.enforce(programId, authenticationContext.getPrincipal(), StandardPermission.UPDATE);
    if (instances < 1) {
      throw new BadRequestException(String.format("Instance count should be greater than 0. Got %s.", instances));
    }
    switch (programId.getType()) {
      case SERVICE:
        setServiceInstances(programId, instances);
        break;
      case WORKER:
        setWorkerInstances(programId, instances);
        break;
      default:
        throw new BadRequestException(String.format("Setting instances for program type %s is not supported",
                                                    programId.getType().getPrettyName()));
    }
  }

  /**
   * Lists all programs with the specified program type in a namespace. If perimeter security and authorization are
   * enabled, only returns the programs that the current user has access to.
   *
   * @param namespaceId the namespace to list datasets for
   * @return the programs in the provided namespace
   */
  public List list(NamespaceId namespaceId, ProgramType type) throws Exception {
    Function> func;
    switch (type) {
      case MAPREDUCE:
        func = ApplicationSpecification::getMapReduce;
        break;
      case SPARK:
        func = ApplicationSpecification::getSpark;
        break;
      case SERVICE:
        func = ApplicationSpecification::getServices;
        break;
      case WORKER:
        func = ApplicationSpecification::getWorkers;
        break;
      case WORKFLOW:
        func = ApplicationSpecification::getWorkflows;
        break;
      default:
        throw new RuntimeException("Unknown program type: " + type.name());
    }

    List programRecords = new ArrayList<>();
    store.scanApplications(
      ScanApplicationsRequest.builder().setNamespaceId(namespaceId).build(),
      batchSize,
      (appId, appMeta) -> {
        ApplicationSpecification appSpec = appMeta.getSpec();
        createProgramRecords(namespaceId, appSpec.getName(), type, func.apply(appSpec).values(), programRecords);
      });

    return programRecords;
  }

  private void createProgramRecords(NamespaceId namespaceId, String appId, ProgramType type,
                                    Iterable programSpecs,
                                    List programRecords) {
    for (ProgramSpecification programSpec : programSpecs) {
      if (hasAccess(namespaceId.app(appId).program(type, programSpec.getName()))) {
        programRecords.add(new ProgramRecord(type, appId, programSpec.getName(), programSpec.getDescription()));
      }
    }
  }

  private boolean hasAccess(ProgramId programId) {
    Principal principal = authenticationContext.getPrincipal();
    return !accessEnforcer.isVisible(Collections.singleton(programId), principal).isEmpty();
  }

  private void setWorkerInstances(ProgramId programId, int instances)
    throws ExecutionException, InterruptedException, BadRequestException {
    int oldInstances = store.getWorkerInstances(programId);
    if (oldInstances != instances) {
      store.setWorkerInstances(programId, instances);
      ProgramRuntimeService.RuntimeInfo runtimeInfo = findRuntimeInfo(programId);
      if (runtimeInfo != null) {
        runtimeInfo.getController().command(ProgramOptionConstants.INSTANCES,
                                            ImmutableMap.of("runnable", programId.getProgram(),
                                                            "newInstances", String.valueOf(instances),
                                                            "oldInstances", String.valueOf(oldInstances))).get();
      }
    }
  }

  private void setServiceInstances(ProgramId programId, int instances)
    throws ExecutionException, InterruptedException, BadRequestException {
    int oldInstances = store.getServiceInstances(programId);
    if (oldInstances != instances) {
      store.setServiceInstances(programId, instances);
      ProgramRuntimeService.RuntimeInfo runtimeInfo = findRuntimeInfo(programId);
      if (runtimeInfo != null) {
        runtimeInfo.getController().command(ProgramOptionConstants.INSTANCES,
                                            ImmutableMap.of("runnable", programId.getProgram(),
                                                            "newInstances", String.valueOf(instances),
                                                            "oldInstances", String.valueOf(oldInstances))).get();
      }
    }
  }

  /**
   * Helper method to update log levels for Worker or Service.
   */
  private void updateLogLevels(ProgramId programId, Map logLevels,
                               @Nullable String runId) throws Exception {
    ProgramRuntimeService.RuntimeInfo runtimeInfo = findRuntimeInfo(programId, runId).values().stream()
                                                                                     .findFirst().orElse(null);
    if (runtimeInfo != null) {
      LogLevelUpdater logLevelUpdater = getLogLevelUpdater(runtimeInfo);
      logLevelUpdater.updateLogLevels(logLevels, null);
    }
  }

  /**
   * Helper method to reset log levels for Worker or Service.
   */
  private void resetLogLevels(ProgramId programId, Set loggerNames, @Nullable String runId) throws Exception {
    ProgramRuntimeService.RuntimeInfo runtimeInfo = findRuntimeInfo(programId, runId).values().stream()
                                                                                     .findFirst().orElse(null);
    if (runtimeInfo != null) {
      LogLevelUpdater logLevelUpdater = getLogLevelUpdater(runtimeInfo);
      logLevelUpdater.resetLogLevels(loggerNames, null);
    }
  }

  /**
   * Helper method to get the {@link LogLevelUpdater} for the program.
   */
  private LogLevelUpdater getLogLevelUpdater(RuntimeInfo runtimeInfo) throws Exception {
    ProgramController programController = runtimeInfo.getController();
    if (!(programController instanceof LogLevelUpdater)) {
      throw new BadRequestException("Update log levels at runtime is only supported in distributed mode");
    }
    return ((LogLevelUpdater) programController);
  }

  /**
   * Returns the active run records (PENDING / STARTING / RUNNING / SUSPENDED) based on the given program id and an
   * optional run id.
   */
  private Map getActiveRuns(ProgramId programId, @Nullable String runId) {
    if (runId == null) {
      return store.getActiveRuns(programId);
    }
    RunRecordDetail runRecord = store.getRun(programId.run(runId));
    return runRecord == null || !ACTIVE_STATES.contains(runRecord.getStatus())
      ? Collections.emptyMap()
      : Collections.singletonMap(programId.run(runId), runRecord);
  }

  /**
   * Returns the {@link ProgramSpecification} for the specified {@link ProgramId program} without performing
   * authorization enforcement.
   *
   * @param programId the {@link ProgramId program} for which the {@link ProgramSpecification} is requested
   * @return the {@link ProgramSpecification} for the specified {@link ProgramId program}
   */
  @Nullable
  private ProgramSpecification getProgramSpecificationWithoutAuthz(ProgramId programId) {
    ApplicationSpecification appSpec = store.getApplication(programId.getParent());
    if (appSpec == null) {
      return null;
    }
    return getExistingAppProgramSpecification(appSpec, programId.getProgramReference());
  }

  /**
   * Returns the {@link ProgramSpecification} for the specified {@link ProgramReference programReference}
   * without performing authorization enforcement.
   *
   * @param programReference the {@link ProgramReference program} for which the {@link ProgramSpecification}
   * is requested
   * @return the {@link ProgramSpecification} for the specified {@link ProgramId program}
   */
  @Nullable
  private ProgramSpecification getLatestProgramSpecificationWithoutAuthz(ProgramReference programReference) {
    ApplicationMeta appMeta = store.getLatest(programReference.getParent());
    if (appMeta == null) {
      return null;
    }
    return getExistingAppProgramSpecification(appMeta.getSpec(), programReference);
  }

  /**
   * Adds {@link Constants#APP_CDAP_VERSION} system argument to the argument map if known.
   * @param programId program that corresponds to application with version information
   * @param systemArgs map to add version information to
   */
  public void addAppCDAPVersion(ProgramId programId, Map systemArgs) {
    ApplicationSpecification appSpec = store.getApplication(programId.getParent());
    if (appSpec != null) {
      String appCDAPVersion = appSpec.getAppCDAPVersion();
      if (appCDAPVersion != null) {
        systemArgs.put(Constants.APP_CDAP_VERSION, appCDAPVersion);
      }
    }
  }

  private Set getPluginRequirements(ProgramSpecification programSpecification) {
    return programSpecification.getPlugins().values()
      .stream().map(plugin -> new PluginRequirement(plugin.getPluginClass().getName(),
                                                    plugin.getPluginClass().getType(),
                                                    plugin.getPluginClass().getRequirements()))
      .collect(Collectors.toSet());
  }

  private void authorizePipelineRuntimeImpersonation(Map userArgs) throws Exception {
    if ((userArgs.containsKey(SystemArguments.RUNTIME_PRINCIPAL_NAME)) &&
            (userArgs.containsKey(SystemArguments.RUNTIME_KEYTAB_PATH))) {
      String principal = userArgs.get(SystemArguments.RUNTIME_PRINCIPAL_NAME);
      LOG.debug("Checking authorisation for user: {}, using runtime config principal: {}",
                authenticationContext.getPrincipal(), principal);
      KerberosPrincipalId kid = new KerberosPrincipalId(principal);
      accessEnforcer.enforce(kid, authenticationContext.getPrincipal(), AccessPermission.IMPERSONATE);
    }
  }

  private String decodeUserId(@Nullable String encodedUserId) {
    if (encodedUserId == null) {
      return "";
    }
    String decodedUserId = "emptyUserId";
    try {
      byte[] decodedBytes = Base64.getDecoder().decode(encodedUserId);
      decodedUserId = new String(decodedBytes);
    } catch (Exception e) {
      LOG.debug("Failed to decode userId {} with exception {}", encodedUserId, e);
    }
    return decodedUserId;
  }

  private void checkLatestVersionExeceution(ProgramId programId)
    throws BadRequestException, ApplicationNotFoundException {
    String latestVersion = getLatestApplicationId(programId.getAppReference()).getVersion();
    if (!latestVersion.equals(programId.getVersion())) {
      throw new BadRequestException(String.format("Start action is only allowed on the latest version %s. " +
                                                    "Please use the versionless start API instead.", latestVersion));
    }
  }

  private ApplicationId getLatestApplicationId(ApplicationReference appReference)
    throws ApplicationNotFoundException {
    ApplicationMeta applicationMeta = store.getLatest(appReference);
    if (applicationMeta == null) {
      throw new ApplicationNotFoundException(appReference);
    }
    String latestVersion = applicationMeta.getSpec().getAppVersion();
    return appReference.app(latestVersion);
  }

  private ProgramId getLatestProgramId(ProgramReference programReference)
    throws ApplicationNotFoundException {
    ApplicationId applicationId = getLatestApplicationId(programReference.getParent());
    return applicationId.program(programReference.getType(), programReference.getProgram());
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy