/** * 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 org.apache.aurora.scheduler.mesos; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import javax.inject.Inject; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.protobuf.ByteString; import org.apache.aurora.Protobufs; import org.apache.aurora.codec.ThriftBinaryCodec; import org.apache.aurora.scheduler.TierManager; import org.apache.aurora.scheduler.base.JobKeys; import org.apache.aurora.scheduler.base.SchedulerException; import org.apache.aurora.scheduler.base.Tasks; import org.apache.aurora.scheduler.configuration.executor.ExecutorSettings; import org.apache.aurora.scheduler.resources.AcceptedOffer; import org.apache.aurora.scheduler.resources.ResourceBag; import org.apache.aurora.scheduler.resources.ResourceManager; import org.apache.aurora.scheduler.storage.entities.IAppcImage; import org.apache.aurora.scheduler.storage.entities.IAssignedTask; import org.apache.aurora.scheduler.storage.entities.IDockerContainer; import org.apache.aurora.scheduler.storage.entities.IDockerImage; import org.apache.aurora.scheduler.storage.entities.IImage; import org.apache.aurora.scheduler.storage.entities.IJobKey; import org.apache.aurora.scheduler.storage.entities.IMesosContainer; import org.apache.aurora.scheduler.storage.entities.IServerInfo; import org.apache.aurora.scheduler.storage.entities.ITaskConfig; import org.apache.mesos.v1.Protos; import org.apache.mesos.v1.Protos.CommandInfo; import org.apache.mesos.v1.Protos.ContainerInfo; import org.apache.mesos.v1.Protos.DiscoveryInfo; import org.apache.mesos.v1.Protos.ExecutorID; import org.apache.mesos.v1.Protos.ExecutorInfo; import org.apache.mesos.v1.Protos.Label; import org.apache.mesos.v1.Protos.Labels; import org.apache.mesos.v1.Protos.Offer; import org.apache.mesos.v1.Protos.Port; import org.apache.mesos.v1.Protos.Resource; import org.apache.mesos.v1.Protos.TaskID; import org.apache.mesos.v1.Protos.TaskInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static java.util.Objects.requireNonNull; import static org.apache.aurora.gen.apiConstants.TASK_FILESYSTEM_MOUNT_POINT; /** * A factory to create mesos task objects. */ public interface MesosTaskFactory { /** * Creates a mesos task object. * * @param task Assigned task to translate into a task object. * @param offer Resource offer the task is being assigned to. * @return A new task. * @throws SchedulerException If the task could not be encoded. */ TaskInfo createFrom(IAssignedTask task, Offer offer) throws SchedulerException; // TODO(wfarner): Move this class to its own file to reduce visibility to package private. class MesosTaskFactoryImpl implements MesosTaskFactory { private static final Logger LOG = LoggerFactory.getLogger(MesosTaskFactoryImpl.class); private static final String AURORA_LABEL_PREFIX = "org.apache.aurora"; @VisibleForTesting static final String METADATA_LABEL_PREFIX = AURORA_LABEL_PREFIX + ".metadata."; @VisibleForTesting static final String DEFAULT_PORT_PROTOCOL = "TCP"; // N.B. We intentionally do not prefix this label. It was added when the explicit source field // was removed from the ExecutorInfo proto and named "source" per guidance from Mesos devs. @VisibleForTesting static final String SOURCE_LABEL = "source"; @VisibleForTesting static final String TIER_LABEL = AURORA_LABEL_PREFIX + ".tier"; private final ExecutorSettings executorSettings; private final TierManager tierManager; private final IServerInfo serverInfo; @Inject MesosTaskFactoryImpl( ExecutorSettings executorSettings, TierManager tierManager, IServerInfo serverInfo) { this.executorSettings = requireNonNull(executorSettings); this.tierManager = requireNonNull(tierManager); this.serverInfo = requireNonNull(serverInfo); } @VisibleForTesting static ExecutorID getExecutorId(String taskId, String taskPrefix) { return ExecutorID.newBuilder().setValue(taskPrefix + taskId).build(); } private static String getJobSourceName(IJobKey jobkey) { return String.join(".", jobkey.getRole(), jobkey.getEnvironment(), jobkey.getName()); } private static String getJobSourceName(ITaskConfig task) { return getJobSourceName(task.getJob()); } private static String getExecutorName(IAssignedTask task) { return task.getTask().getExecutorConfig().getName(); } @VisibleForTesting static String getInstanceSourceName(ITaskConfig task, int instanceId) { return String.join(".", getJobSourceName(task), Integer.toString(instanceId)); } @VisibleForTesting static String getInverseJobSourceName(IJobKey job) { return String.join(".", job.getName(), job.getEnvironment(), job.getRole()); } private static byte[] serializeTask(IAssignedTask task) throws SchedulerException { try { return ThriftBinaryCodec.encode(task.newBuilder()); } catch (ThriftBinaryCodec.CodingException e) { LOG.error("Unable to serialize task.", e); throw new SchedulerException("Internal error.", e); } } @Override public TaskInfo createFrom(IAssignedTask task, Offer offer) throws SchedulerException { requireNonNull(task); requireNonNull(offer); ITaskConfig config = task.getTask(); // Docker-based tasks don't need executors ResourceBag executorOverhead = ResourceBag.EMPTY; if (config.isSetExecutorConfig()) { executorOverhead = executorSettings.getExecutorOverhead(getExecutorName(task)).orElse(ResourceBag.EMPTY); } AcceptedOffer acceptedOffer; // TODO(wfarner): Re-evaluate if/why we need to continue handling unset assignedPorts field. try { acceptedOffer = AcceptedOffer.create( offer, task, executorOverhead, tierManager.getTier(task.getTask())); } catch (ResourceManager.InsufficientResourcesException e) { throw new SchedulerException(e); } Iterable<Resource> resources = acceptedOffer.getTaskResources(); if (LOG.isDebugEnabled()) { LOG.debug( "Setting task resources to {}", Iterables.transform(resources, Protobufs::toString)); } TaskInfo.Builder taskBuilder = TaskInfo.newBuilder() .setName(JobKeys.canonicalString(Tasks.getJob(task))) .setTaskId(TaskID.newBuilder().setValue(task.getTaskId())) .setAgentId(offer.getAgentId()) .addAllResources(resources); configureTaskLabels(config, taskBuilder); if (executorSettings.shouldPopulateDiscoverInfo()) { configureDiscoveryInfos(task, taskBuilder); } if (config.getContainer().isSetMesos()) { ExecutorInfo.Builder executorInfoBuilder = configureTaskForExecutor(task, acceptedOffer); Optional<ContainerInfo.Builder> containerInfoBuilder = configureTaskForImage( task.getTask().getContainer().getMesos(), getExecutorName(task)); if (containerInfoBuilder.isPresent()) { executorInfoBuilder.setContainer(containerInfoBuilder.get()); } taskBuilder.setExecutor(executorInfoBuilder.build()); } else if (config.getContainer().isSetDocker()) { IDockerContainer dockerContainer = config.getContainer().getDocker(); if (config.isSetExecutorConfig()) { ExecutorInfo.Builder execBuilder = configureTaskForExecutor(task, acceptedOffer) .setContainer(getDockerContainerInfo( dockerContainer, Optional.of(getExecutorName(task)))); taskBuilder.setExecutor(execBuilder.build()); } else { LOG.warn("Running Docker-based task without an executor."); taskBuilder.setContainer(getDockerContainerInfo(dockerContainer, Optional.absent())) .setCommand(CommandInfo.newBuilder().setShell(false)); } } else { throw new SchedulerException("Task had no supported container set."); } if (taskBuilder.hasExecutor()) { taskBuilder.setData(ByteString.copyFrom(serializeTask(task))); } return taskBuilder.build(); } private Optional<ContainerInfo.Builder> configureTaskForImage( IMesosContainer mesosContainer, String executorName) { requireNonNull(mesosContainer); if (mesosContainer.isSetImage()) { IImage image = mesosContainer.getImage(); Protos.Image.Builder imageBuilder = Protos.Image.newBuilder(); if (image.isSetAppc()) { IAppcImage appcImage = image.getAppc(); imageBuilder.setType(Protos.Image.Type.APPC); imageBuilder.setAppc(Protos.Image.Appc.newBuilder() .setName(appcImage.getName()) .setId(appcImage.getImageId())); } else if (image.isSetDocker()) { IDockerImage dockerImage = image.getDocker(); imageBuilder.setType(Protos.Image.Type.DOCKER); imageBuilder.setDocker(Protos.Image.Docker.newBuilder() .setName(dockerImage.getName() + ":" + dockerImage.getTag())); } else { throw new SchedulerException("Task had no supported image set."); } ContainerInfo.MesosInfo.Builder mesosContainerBuilder = ContainerInfo.MesosInfo.newBuilder(); Iterable<Protos.Volume> containerVolumes = Iterables.transform(mesosContainer.getVolumes(), input -> Protos.Volume.newBuilder() .setMode(Protos.Volume.Mode.valueOf(input.getMode().name())) .setHostPath(input.getHostPath()) .setContainerPath(input.getContainerPath()) .build()); Protos.Volume volume = Protos.Volume.newBuilder() .setImage(imageBuilder) .setContainerPath(TASK_FILESYSTEM_MOUNT_POINT) .setMode(Protos.Volume.Mode.RO) .build(); return Optional.of(ContainerInfo.newBuilder() .setType(ContainerInfo.Type.MESOS) .setMesos(mesosContainerBuilder) .addAllVolumes(executorSettings.getExecutorConfig(executorName).get().getVolumeMounts()) .addAllVolumes(containerVolumes) .addVolumes(volume)); } return Optional.absent(); } private ContainerInfo getDockerContainerInfo( IDockerContainer config, Optional<String> executorName) { Iterable<Protos.Parameter> parameters = Iterables.transform(config.getParameters(), item -> Protos.Parameter.newBuilder().setKey(item.getName()) .setValue(item.getValue()).build()); ContainerInfo.DockerInfo.Builder dockerBuilder = ContainerInfo.DockerInfo.newBuilder() .setImage(config.getImage()).addAllParameters(parameters); return ContainerInfo.newBuilder() .setType(ContainerInfo.Type.DOCKER) .setDocker(dockerBuilder.build()) .addAllVolumes( executorName.isPresent() ? executorSettings.getExecutorConfig(executorName.get()).get().getVolumeMounts() : ImmutableList.of()) .build(); } @SuppressWarnings("deprecation") // we set the source field for backwards compat. private ExecutorInfo.Builder configureTaskForExecutor( IAssignedTask task, AcceptedOffer acceptedOffer) { String sourceName = getInstanceSourceName(task.getTask(), task.getInstanceId()); ExecutorInfo.Builder builder = executorSettings.getExecutorConfig(getExecutorName(task)).get() .getExecutor() .toBuilder() .setExecutorId(getExecutorId( task.getTaskId(), executorSettings.getExecutorConfig(getExecutorName(task)).get().getTaskPrefix())) .setSource(sourceName) .setLabels( Labels.newBuilder().addLabels( Label.newBuilder() .setKey(SOURCE_LABEL) .setValue(sourceName))); //TODO: (rdelvalle) add output_file when Aurora's Mesos dep is updated (MESOS-4735) List<CommandInfo.URI> mesosFetcherUris = task.getTask().getMesosFetcherUris().stream() .map(u -> Protos.CommandInfo.URI.newBuilder().setValue(u.getValue()) .setExecutable(false) .setExtract(u.isExtract()) .setCache(u.isCache()).build()) .collect(Collectors.toList()); builder.setCommand(builder.getCommand().toBuilder().addAllUris(mesosFetcherUris)); Iterable<Resource> executorResources = acceptedOffer.getExecutorResources(); if (LOG.isDebugEnabled()) { LOG.debug( "Setting executor resources to {}", Iterables.transform(executorResources, Protobufs::toString)); } builder.clearResources().addAllResources(executorResources); return builder; } private void configureTaskLabels(ITaskConfig config, TaskInfo.Builder taskBuilder) { Labels.Builder labelsBuilder = Labels.newBuilder(); labelsBuilder.addLabels(Label.newBuilder().setKey(TIER_LABEL).setValue(config.getTier())); config.getMetadata().stream().forEach(m -> labelsBuilder.addLabels(Label.newBuilder() .setKey(METADATA_LABEL_PREFIX + m.getKey()) .setValue(m.getValue()) .build())); taskBuilder.setLabels(labelsBuilder); } private void configureDiscoveryInfos(IAssignedTask task, TaskInfo.Builder taskBuilder) { DiscoveryInfo.Builder builder = taskBuilder.getDiscoveryBuilder(); builder.setVisibility(DiscoveryInfo.Visibility.CLUSTER); builder.setName(getInverseJobSourceName(task.getTask().getJob())); builder.setEnvironment(task.getTask().getJob().getEnvironment()); // A good sane choice for default location is current Aurora cluster name. builder.setLocation(serverInfo.getClusterName()); for (Map.Entry<String, Integer> entry : task.getAssignedPorts().entrySet()) { builder.getPortsBuilder().addPorts( Port.newBuilder() .setName(entry.getKey()) .setNumber(entry.getValue()) .setProtocol(DEFAULT_PORT_PROTOCOL) ); } } } }