package com.hubspot.singularity.mesos;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map.Entry;
import javax.inject.Singleton;
import org.apache.mesos.Protos;
import org.apache.mesos.Protos.CommandInfo;
import org.apache.mesos.Protos.CommandInfo.URI;
import org.apache.mesos.Protos.ContainerInfo;
import org.apache.mesos.Protos.ContainerInfo.DockerInfo;
import org.apache.mesos.Protos.Environment;
import org.apache.mesos.Protos.Environment.Variable;
import org.apache.mesos.Protos.ExecutorID;
import org.apache.mesos.Protos.ExecutorInfo;
import org.apache.mesos.Protos.Label;
import org.apache.mesos.Protos.Labels;
import org.apache.mesos.Protos.Labels.Builder;
import org.apache.mesos.Protos.Offer;
import org.apache.mesos.Protos.Parameter;
import org.apache.mesos.Protos.Resource;
import org.apache.mesos.Protos.TaskID;
import org.apache.mesos.Protos.TaskInfo;
import org.apache.mesos.Protos.Volume;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Ints;
import com.google.inject.Inject;
import com.google.protobuf.ByteString;
import com.hubspot.deploy.ExecutorDataBuilder;
import com.hubspot.mesos.JavaUtils;
import com.hubspot.mesos.MesosUtils;
import com.hubspot.mesos.Resources;
import com.hubspot.mesos.SingularityContainerInfo;
import com.hubspot.mesos.SingularityDockerInfo;
import com.hubspot.mesos.SingularityDockerNetworkType;
import com.hubspot.mesos.SingularityDockerParameter;
import com.hubspot.mesos.SingularityDockerPortMapping;
import com.hubspot.mesos.SingularityMesosArtifact;
import com.hubspot.mesos.SingularityMesosTaskLabel;
import com.hubspot.mesos.SingularityVolume;
import com.hubspot.singularity.SingularityS3UploaderFile;
import com.hubspot.singularity.SingularityTask;
import com.hubspot.singularity.SingularityTaskExecutorData;
import com.hubspot.singularity.SingularityTaskId;
import com.hubspot.singularity.SingularityTaskRequest;
import com.hubspot.singularity.config.SingularityConfiguration;
import com.hubspot.singularity.data.ExecutorIdGenerator;
@Singleton
class SingularityMesosTaskBuilder {
private static final Logger LOG = LoggerFactory.getLogger(SingularityMesosTaskBuilder.class);
private final ObjectMapper objectMapper;
private final SingularitySlaveAndRackHelper slaveAndRackHelper;
private final ExecutorIdGenerator idGenerator;
private final SingularityConfiguration configuration;
@Inject
SingularityMesosTaskBuilder(ObjectMapper objectMapper, SingularitySlaveAndRackHelper slaveAndRackHelper, ExecutorIdGenerator idGenerator, SingularityConfiguration configuration) {
this.objectMapper = objectMapper;
this.slaveAndRackHelper = slaveAndRackHelper;
this.idGenerator = idGenerator;
this.configuration = configuration;
}
public SingularityTask buildTask(Protos.Offer offer, List<Resource> availableResources, SingularityTaskRequest taskRequest, Resources desiredTaskResources, Resources desiredExecutorResources) {
final String sanitizedRackId = JavaUtils.getReplaceHyphensWithUnderscores(slaveAndRackHelper.getRackIdOrDefault(offer));
final String sanitizedHost = JavaUtils.getReplaceHyphensWithUnderscores(slaveAndRackHelper.getMaybeTruncatedHost(offer));
final SingularityTaskId taskId = new SingularityTaskId(taskRequest.getPendingTask().getPendingTaskId().getRequestId(), taskRequest.getDeploy().getId(), System.currentTimeMillis(),
taskRequest.getPendingTask().getPendingTaskId().getInstanceNo(), sanitizedHost, sanitizedRackId);
final TaskInfo.Builder bldr = TaskInfo.newBuilder()
.setTaskId(TaskID.newBuilder().setValue(taskId.toString()));
Optional<long[]> ports = Optional.absent();
Optional<Resource> portsResource = Optional.absent();
final Optional<SingularityContainerInfo> containerInfo = taskRequest.getDeploy().getContainerInfo();
if (desiredTaskResources.getNumPorts() > 0 || hasLiteralPortMapping(containerInfo)) {
List<Long> requestedPorts = new ArrayList<>();
if (hasLiteralPortMapping(containerInfo)) {
requestedPorts.addAll(containerInfo.get().getDocker().get().getLiteralHostPorts());
}
portsResource = Optional.of(MesosUtils.getPortsResource(desiredTaskResources.getNumPorts(), availableResources, requestedPorts));
ports = Optional.of(MesosUtils.getPorts(portsResource.get(), desiredTaskResources.getNumPorts()));
}
if (containerInfo.isPresent()) {
prepareContainerInfo(offer, taskId, bldr, containerInfo.get(), ports);
}
if (taskRequest.getDeploy().getCustomExecutorCmd().isPresent()) {
prepareCustomExecutor(bldr, taskId, taskRequest, offer, ports, desiredExecutorResources);
} else {
prepareCommand(bldr, taskId, taskRequest, offer, ports);
}
if (portsResource.isPresent()) {
bldr.addResources(portsResource.get());
}
Optional<String> requiredRole = taskRequest.getRequest().getRequiredRole();
bldr.addResources(MesosUtils.getCpuResource(desiredTaskResources.getCpus(), requiredRole));
bldr.addResources(MesosUtils.getMemoryResource(desiredTaskResources.getMemoryMb(), requiredRole));
if (desiredTaskResources.getDiskMb() > 0) {
bldr.addResources(MesosUtils.getDiskResource(desiredTaskResources.getDiskMb(), requiredRole));
}
bldr.setSlaveId(offer.getSlaveId());
bldr.setName(taskRequest.getRequest().getId());
final Builder labelsBuilder = Labels.newBuilder();
// apply request-specific labels, if any
if (taskRequest.getDeploy().getMesosLabels().isPresent() && !taskRequest.getDeploy().getMesosLabels().get().isEmpty()) {
for (SingularityMesosTaskLabel label : taskRequest.getDeploy().getMesosLabels().get()) {
org.apache.mesos.Protos.Label.Builder labelBuilder = Label.newBuilder();
labelBuilder.setKey(label.getKey());
if ((label.getValue().isPresent())) {
labelBuilder.setValue(label.getValue().get());
}
labelsBuilder.addLabels(labelBuilder.build());
}
}
// apply task-specific labels, if any
final int taskInstanceNo = taskRequest.getPendingTask().getPendingTaskId().getInstanceNo();
if (taskRequest.getDeploy().getMesosTaskLabels().isPresent() && taskRequest.getDeploy().getMesosTaskLabels().get().containsKey(taskInstanceNo) && !taskRequest.getDeploy().getMesosTaskLabels().get().get(taskInstanceNo).isEmpty()) {
for (SingularityMesosTaskLabel label : taskRequest.getDeploy().getMesosTaskLabels().get().get(taskInstanceNo)) {
org.apache.mesos.Protos.Label.Builder labelBuilder = Label.newBuilder();
labelBuilder.setKey(label.getKey());
if ((label.getValue().isPresent())) {
labelBuilder.setValue(label.getValue().get());
}
labelsBuilder.addLabels(labelBuilder.build());
}
}
bldr.setLabels(labelsBuilder);
TaskInfo task = bldr.build();
return new SingularityTask(taskRequest, taskId, offer, task, slaveAndRackHelper.getRackId(offer));
}
private boolean hasLiteralPortMapping(Optional<SingularityContainerInfo> maybeContainerInfo) {
return maybeContainerInfo.isPresent() && maybeContainerInfo.get().getDocker().isPresent() && !maybeContainerInfo.get().getDocker().get().getLiteralHostPorts().isEmpty();
}
private void setEnv(Environment.Builder envBldr, String key, Object value) {
if (value == null) {
return;
}
envBldr.addVariables(Variable.newBuilder().setName(key).setValue(value.toString()));
}
private void prepareEnvironment(final SingularityTaskRequest task, SingularityTaskId taskId, CommandInfo.Builder commandBuilder, final Protos.Offer offer, final Optional<long[]> ports) {
Environment.Builder envBldr = Environment.newBuilder();
setEnv(envBldr, "INSTANCE_NO", task.getPendingTask().getPendingTaskId().getInstanceNo());
setEnv(envBldr, "TASK_HOST", offer.getHostname());
Optional<String> rack = slaveAndRackHelper.getRackId(offer);
if (rack.isPresent()) {
setEnv(envBldr, "TASK_RACK_ID", rack.get());
}
setEnv(envBldr, "TASK_REQUEST_ID", task.getPendingTask().getPendingTaskId().getRequestId());
setEnv(envBldr, "TASK_DEPLOY_ID", taskId.getDeployId());
setEnv(envBldr, "TASK_ID", taskId.getId());
setEnv(envBldr, "ESTIMATED_INSTANCE_COUNT", task.getRequest().getInstancesSafe());
if (task.getPendingTask().getUser().isPresent()) {
setEnv(envBldr, "STARTED_BY_USER", task.getPendingTask().getUser().get());
}
for (Entry<String, String> envEntry : task.getDeploy().getEnv().or(Collections.<String, String>emptyMap()).entrySet()) {
setEnv(envBldr, envEntry.getKey(), fillInTaskIdValues(envEntry.getValue(), offer, taskId));
}
if (task.getDeploy().getTaskEnv().isPresent() && task.getDeploy().getTaskEnv().get().containsKey(taskId.getInstanceNo()) && !task.getDeploy().getTaskEnv().get().get(taskId.getInstanceNo()).isEmpty()) {
for (Entry<String, String> envEntry : task.getDeploy().getTaskEnv().get().get(taskId.getInstanceNo()).entrySet()) {
setEnv(envBldr, envEntry.getKey(), fillInTaskIdValues(envEntry.getValue(), offer, taskId));
}
}
if (ports.isPresent()) {
for (int portNum = 0; portNum < ports.get().length; portNum++) {
if (portNum == 0) {
setEnv(envBldr, "PORT", ports.get()[portNum]);
}
setEnv(envBldr, String.format("PORT%s", portNum), ports.get()[portNum]);
}
}
commandBuilder.setEnvironment(envBldr.build());
}
private Optional<DockerInfo.PortMapping> buildPortMapping(final SingularityDockerPortMapping singularityDockerPortMapping, final Optional<long[]> ports) {
final int containerPort;
switch (singularityDockerPortMapping.getContainerPortType()) {
case LITERAL:
containerPort = singularityDockerPortMapping.getContainerPort();
break;
case FROM_OFFER:
containerPort = Ints.checkedCast(ports.get()[singularityDockerPortMapping.getContainerPort()]);
break;
default:
return Optional.absent();
}
final int hostPort;
switch (singularityDockerPortMapping.getHostPortType()) {
case LITERAL:
hostPort = singularityDockerPortMapping.getHostPort();
break;
case FROM_OFFER:
hostPort = Ints.checkedCast(ports.get()[singularityDockerPortMapping.getHostPort()]);
break;
default:
return Optional.absent();
}
return Optional.of(DockerInfo.PortMapping.newBuilder()
.setContainerPort(containerPort)
.setHostPort(hostPort)
.setProtocol(singularityDockerPortMapping.getProtocol())
.build());
}
private String fillInTaskIdValues(String string, Offer offer, SingularityTaskId taskId) {
if (!Strings.isNullOrEmpty(string)) {
string = string.replace("${TASK_REQUEST_ID}", taskId.getRequestId())
.replace("${TASK_DEPLOY_ID}", taskId.getDeployId())
.replace("${TASK_STARTED_AT}", Long.toString(taskId.getStartedAt()))
.replace("${TASK_INSTANCE_NO}", Integer.toString(taskId.getInstanceNo()))
.replace("${TASK_HOST}", offer.getHostname())
.replace("${TASK_RACK_ID}", slaveAndRackHelper.getRackIdOrDefault(offer))
.replace("${TASK_ID}", taskId.getId());
}
return string;
}
private void prepareContainerInfo(final Offer offer, final SingularityTaskId taskId, final TaskInfo.Builder bldr, final SingularityContainerInfo containerInfo, final Optional<long[]> ports) {
ContainerInfo.Builder containerBuilder = ContainerInfo.newBuilder();
containerBuilder.setType(ContainerInfo.Type.valueOf(containerInfo.getType().toString()));
final Optional<SingularityDockerInfo> dockerInfo = containerInfo.getDocker();
if (dockerInfo.isPresent()) {
final DockerInfo.Builder dockerInfoBuilder = DockerInfo.newBuilder();
dockerInfoBuilder.setImage(dockerInfo.get().getImage());
if (dockerInfo.get().getNetwork().isPresent()) {
dockerInfoBuilder.setNetwork(DockerInfo.Network.valueOf(dockerInfo.get().getNetwork().get().toString()));
}
final List<SingularityDockerPortMapping> portMappings = dockerInfo.get().getPortMappings();
final boolean isBridged = SingularityDockerNetworkType.BRIDGE.equals(dockerInfo.get().getNetwork().orNull());
if ((dockerInfo.get().hasAllLiteralHostPortMappings() || ports.isPresent()) && !portMappings.isEmpty()) {
for (SingularityDockerPortMapping singularityDockerPortMapping : portMappings) {
final Optional<DockerInfo.PortMapping> maybePortMapping = buildPortMapping(singularityDockerPortMapping, ports);
if (maybePortMapping.isPresent()) {
dockerInfoBuilder.addPortMappings(maybePortMapping.get());
}
}
} else if (configuration.getNetworkConfiguration().isDefaultPortMapping() && isBridged && portMappings.isEmpty() && ports.isPresent()) {
for (long longPort : ports.get()) {
int port = Ints.checkedCast(longPort);
dockerInfoBuilder.addPortMappings(DockerInfo.PortMapping.newBuilder()
.setHostPort(port)
.setContainerPort(port)
.build());
}
}
if (!dockerInfo.get().getDockerParameters().isEmpty()) {
List<Parameter> parameters = new ArrayList<>();
for (SingularityDockerParameter parameter : dockerInfo.get().getDockerParameters()) {
parameters.add(Parameter.newBuilder().setKey(parameter.getKey()).setValue(parameter.getValue()).build());
}
dockerInfoBuilder.addAllParameters(parameters);
}
dockerInfoBuilder.setPrivileged(dockerInfo.get().isPrivileged());
dockerInfoBuilder.setForcePullImage(dockerInfo.get().isForcePullImage());
containerBuilder.setDocker(dockerInfoBuilder);
}
for (SingularityVolume volumeInfo : containerInfo.getVolumes().or(Collections.<SingularityVolume>emptyList())) {
final Volume.Builder volumeBuilder = Volume.newBuilder();
volumeBuilder.setContainerPath(fillInTaskIdValues(volumeInfo.getContainerPath(), offer, taskId));
if (volumeInfo.getHostPath().isPresent()) {
volumeBuilder.setHostPath(fillInTaskIdValues(volumeInfo.getHostPath().get(), offer, taskId));
}
if (volumeInfo.getMode().isPresent()) {
volumeBuilder.setMode(Volume.Mode.valueOf(volumeInfo.getMode().get().toString()));
} else {
volumeBuilder.setMode(Volume.Mode.RO);
}
containerBuilder.addVolumes(volumeBuilder);
}
bldr.setContainer(containerBuilder);
}
private List<Resource> buildMesosResources(final Resources resources, Optional<String> role) {
ImmutableList.Builder<Resource> builder = ImmutableList.builder();
if (resources.getCpus() > 0) {
builder.add(MesosUtils.getCpuResource(resources.getCpus(), role));
}
if (resources.getMemoryMb() > 0) {
builder.add(MesosUtils.getMemoryResource(resources.getMemoryMb(), role));
}
if (resources.getDiskMb() > 0) {
builder.add(MesosUtils.getDiskResource(resources.getDiskMb(), role));
}
return builder.build();
}
private void prepareCustomExecutor(final TaskInfo.Builder bldr, final SingularityTaskId taskId, final SingularityTaskRequest task, final Protos.Offer offer,
final Optional<long[]> ports, final Resources desiredExecutorResources) {
CommandInfo.Builder commandBuilder = CommandInfo.newBuilder().setValue(task.getDeploy().getCustomExecutorCmd().get());
prepareEnvironment(task, taskId, commandBuilder, offer, ports);
if (task.getDeploy().getUser().isPresent()) {
commandBuilder.setUser(task.getDeploy().getUser().get());
}
bldr.setExecutor(ExecutorInfo.newBuilder()
.setCommand(commandBuilder.build())
.setExecutorId(ExecutorID.newBuilder().setValue(task.getDeploy().getCustomExecutorId().or(idGenerator.getNextExecutorId())))
.setSource(task.getDeploy().getCustomExecutorSource().or(taskId.getId())) // set source to taskId for use in statistics endpoint
.addAllResources(buildMesosResources(desiredExecutorResources, task.getRequest().getRequiredRole()))
.build()
);
if (task.getDeploy().getExecutorData().isPresent()) {
final ExecutorDataBuilder executorDataBldr = task.getDeploy().getExecutorData().get().toBuilder();
String defaultS3Bucket = "";
String s3UploaderKeyPattern = "";
if (configuration.getS3ConfigurationOptional().isPresent()) {
if (task.getRequest().getGroup().isPresent() && configuration.getS3ConfigurationOptional().get().getGroupOverrides().containsKey(task.getRequest().getGroup().get())) {
defaultS3Bucket = configuration.getS3ConfigurationOptional().get().getGroupOverrides().get(task.getRequest().getGroup().get()).getS3Bucket();
LOG.trace("Setting defaultS3Bucket to {} for task {} executorData", defaultS3Bucket, taskId.getId());
} else {
defaultS3Bucket = configuration.getS3ConfigurationOptional().get().getS3Bucket();
}
s3UploaderKeyPattern = configuration.getS3ConfigurationOptional().get().getS3KeyFormat();
}
if (task.getPendingTask().getCmdLineArgsList().isPresent() && !task.getPendingTask().getCmdLineArgsList().get().isEmpty()) {
LOG.trace("Adding cmd line args {} to task {} executorData", task.getPendingTask().getCmdLineArgsList(), taskId.getId());
final ImmutableList.Builder<String> extraCmdLineArgsBuilder = ImmutableList.builder();
if (executorDataBldr.getExtraCmdLineArgs() != null && !executorDataBldr.getExtraCmdLineArgs().isEmpty()) {
extraCmdLineArgsBuilder.addAll(executorDataBldr.getExtraCmdLineArgs());
}
extraCmdLineArgsBuilder.addAll(task.getPendingTask().getCmdLineArgsList().get());
executorDataBldr.setExtraCmdLineArgs(extraCmdLineArgsBuilder.build());
}
List<SingularityS3UploaderFile> uploaderAdditionalFiles = configuration.getS3ConfigurationOptional().isPresent() ? configuration.getS3ConfigurationOptional().get().getS3UploaderAdditionalFiles() : Collections.<SingularityS3UploaderFile>emptyList();
Optional<String> maybeS3StorageClass = configuration.getS3ConfigurationOptional().isPresent() ? configuration.getS3ConfigurationOptional().get().getS3StorageClass() : Optional.<String>absent();
Optional<Long> maybeApplyAfterBytes = configuration.getS3ConfigurationOptional().isPresent() ? configuration.getS3ConfigurationOptional().get().getApplyS3StorageClassAfterBytes() : Optional.<Long>absent();
final SingularityTaskExecutorData executorData = new SingularityTaskExecutorData(executorDataBldr.build(), uploaderAdditionalFiles, defaultS3Bucket, s3UploaderKeyPattern,
configuration.getCustomExecutorConfiguration().getServiceLog(), configuration.getCustomExecutorConfiguration().getServiceFinishedTailLog(), task.getRequest().getGroup(),
maybeS3StorageClass, maybeApplyAfterBytes);
try {
bldr.setData(ByteString.copyFromUtf8(objectMapper.writeValueAsString(executorData)));
} catch (JsonProcessingException e) {
LOG.warn("Unable to process executor data {} for task {} as json (trying as string)", executorData, taskId.getId(), e);
bldr.setData(ByteString.copyFromUtf8(executorData.toString()));
}
} else if (task.getDeploy().getCommand().isPresent()) {
bldr.setData(ByteString.copyFromUtf8(task.getDeploy().getCommand().get()));
}
}
private void prepareCommand(final TaskInfo.Builder bldr, final SingularityTaskId taskId, final SingularityTaskRequest task, final Protos.Offer offer, final Optional<long[]> ports) {
CommandInfo.Builder commandBldr = CommandInfo.newBuilder();
if (task.getDeploy().getUser().isPresent()) {
commandBldr.setUser(task.getDeploy().getUser().get());
}
if (task.getDeploy().getCommand().isPresent()) {
commandBldr.setValue(task.getDeploy().getCommand().get());
}
if (task.getDeploy().getArguments().isPresent()) {
commandBldr.addAllArguments(task.getDeploy().getArguments().get());
}
if (task.getPendingTask().getCmdLineArgsList().isPresent()) {
commandBldr.addAllArguments(task.getPendingTask().getCmdLineArgsList().get());
}
if (task.getDeploy().getShell().isPresent()){
commandBldr.setShell(task.getDeploy().getShell().get());
} else if ((task.getDeploy().getArguments().isPresent() && !task.getDeploy().getArguments().get().isEmpty()) ||
// Hopefully temporary workaround for
// http://www.mail-archive.com/user@mesos.apache.org/msg01449.html
task.getDeploy().getContainerInfo().isPresent() ||
(task.getPendingTask().getCmdLineArgsList().isPresent() && !task.getPendingTask().getCmdLineArgsList().get().isEmpty())) {
commandBldr.setShell(false);
}
for (SingularityMesosArtifact artifact : task.getDeploy().getUris().or(Collections.<SingularityMesosArtifact> emptyList())) {
commandBldr.addUris(URI.newBuilder()
.setValue(artifact.getUri())
.setCache(artifact.isCache())
.setExecutable(artifact.isExecutable())
.setExtract(artifact.isExtract())
.build());
}
prepareEnvironment(task, taskId, commandBldr, offer, ports);
bldr.setCommand(commandBldr);
}
}