/** * 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.configuration; import java.util.Map; import java.util.stream.Collectors; import javax.annotation.Nullable; import javax.inject.Inject; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Multimap; import org.apache.aurora.gen.Container; import org.apache.aurora.gen.DockerParameter; import org.apache.aurora.gen.JobConfiguration; import org.apache.aurora.gen.TaskConfig; import org.apache.aurora.gen.TaskConstraint; import org.apache.aurora.scheduler.TierManager; import org.apache.aurora.scheduler.base.JobKeys; import org.apache.aurora.scheduler.base.UserProvidedStrings; import org.apache.aurora.scheduler.configuration.executor.ExecutorSettings; import org.apache.aurora.scheduler.resources.ResourceManager; import org.apache.aurora.scheduler.resources.ResourceType; import org.apache.aurora.scheduler.storage.entities.IConstraint; import org.apache.aurora.scheduler.storage.entities.IContainer; import org.apache.aurora.scheduler.storage.entities.IJobConfiguration; import org.apache.aurora.scheduler.storage.entities.IMesosContainer; import org.apache.aurora.scheduler.storage.entities.IResource; import org.apache.aurora.scheduler.storage.entities.ITaskConfig; import org.apache.aurora.scheduler.storage.entities.ITaskConstraint; import org.apache.aurora.scheduler.storage.entities.IValueConstraint; import org.apache.aurora.scheduler.storage.log.ThriftBackfill; import static java.util.Objects.requireNonNull; import static org.apache.aurora.scheduler.resources.ResourceType.CPUS; import static org.apache.aurora.scheduler.resources.ResourceType.DISK_MB; import static org.apache.aurora.scheduler.resources.ResourceType.GPUS; import static org.apache.aurora.scheduler.resources.ResourceType.PORTS; import static org.apache.aurora.scheduler.resources.ResourceType.RAM_MB; /** * Manages translation from a string-mapped configuration to a concrete configuration type, and * defaults for optional values. * * TODO(William Farner): Add input validation to all fields (strings not empty, positive ints, etc). */ public class ConfigurationManager { public static final String DEDICATED_ATTRIBUTE = "dedicated"; private interface Validator<T> { void validate(T value) throws TaskDescriptionException; } private static class GreaterThan implements Validator<Number> { private final double min; private final String label; GreaterThan(double min, String label) { this.min = min; this.label = label; } @Override public void validate(Number value) throws TaskDescriptionException { if (this.min >= value.doubleValue()) { throw new TaskDescriptionException(label + " must be greater than " + this.min); } } } public static class ConfigurationManagerSettings { private final ImmutableSet<Container._Fields> allowedContainerTypes; private final boolean allowDockerParameters; private final Multimap<String, String> defaultDockerParameters; private final boolean requireDockerUseExecutor; private final boolean allowGpuResource; private final boolean enableMesosFetcher; private final boolean allowContainerVolumes; public ConfigurationManagerSettings( ImmutableSet<Container._Fields> allowedContainerTypes, boolean allowDockerParameters, Multimap<String, String> defaultDockerParameters, boolean requireDockerUseExecutor, boolean allowGpuResource, boolean enableMesosFetcher, boolean allowContainerVolumes) { this.allowedContainerTypes = requireNonNull(allowedContainerTypes); this.allowDockerParameters = allowDockerParameters; this.defaultDockerParameters = requireNonNull(defaultDockerParameters); this.requireDockerUseExecutor = requireDockerUseExecutor; this.allowGpuResource = allowGpuResource; this.enableMesosFetcher = enableMesosFetcher; this.allowContainerVolumes = allowContainerVolumes; } } private final ConfigurationManagerSettings settings; private final TierManager tierManager; private final ThriftBackfill thriftBackfill; private final ExecutorSettings executorSettings; @Inject public ConfigurationManager( ConfigurationManagerSettings settings, TierManager tierManager, ThriftBackfill thriftBackfill, ExecutorSettings executorSettings) { this.settings = requireNonNull(settings); this.tierManager = requireNonNull(tierManager); this.thriftBackfill = requireNonNull(thriftBackfill); this.executorSettings = requireNonNull(executorSettings); } private static String getRole(IValueConstraint constraint) { return Iterables.getOnlyElement(constraint.getValues()).split("/")[0]; } private static boolean isValueConstraint(ITaskConstraint taskConstraint) { return taskConstraint.getSetField() == TaskConstraint._Fields.VALUE; } public static boolean isDedicated(Iterable<IConstraint> taskConstraints) { return Iterables.any(taskConstraints, getConstraintByName(DEDICATED_ATTRIBUTE)); } @Nullable private static IConstraint getDedicatedConstraint(ITaskConfig task) { return Iterables.find(task.getConstraints(), getConstraintByName(DEDICATED_ATTRIBUTE), null); } /** * Check validity of and populates defaults in a job configuration. This will return a deep copy * of the provided job configuration with default configuration values applied, and configuration * map values sanitized and applied to their respective struct fields. * * @param job Job to validate and populate. * @return A deep copy of {@code job} that has been populated. * @throws TaskDescriptionException If the job configuration is invalid. */ public IJobConfiguration validateAndPopulate(IJobConfiguration job) throws TaskDescriptionException { requireNonNull(job); if (!job.isSetTaskConfig()) { throw new TaskDescriptionException("Job configuration must have taskConfig set."); } if (job.getInstanceCount() <= 0) { throw new TaskDescriptionException("Instance count must be positive."); } JobConfiguration builder = job.newBuilder(); if (!JobKeys.isValid(job.getKey())) { throw new TaskDescriptionException("Job key " + job.getKey() + " is invalid."); } if (job.isSetOwner() && !UserProvidedStrings.isGoodIdentifier(job.getOwner().getUser())) { throw new TaskDescriptionException( "Job user contains illegal characters: " + job.getOwner().getUser()); } builder.setTaskConfig( validateAndPopulate(ITaskConfig.build(builder.getTaskConfig())).newBuilder()); // Only one of [service=true, cron_schedule] may be set. if (!Strings.isNullOrEmpty(job.getCronSchedule()) && builder.getTaskConfig().isIsService()) { throw new TaskDescriptionException( "A service task may not be run on a cron schedule: " + builder); } return IJobConfiguration.build(builder); } @VisibleForTesting static final String NO_DOCKER_PARAMETERS = "This scheduler is configured to disallow Docker parameters."; @VisibleForTesting static final String EXECUTOR_REQUIRED_WITH_DOCKER = "This scheduler is configured to require an executor for Docker-based tasks."; @VisibleForTesting static final String MESOS_FETCHER_DISABLED = "Mesos Fetcher for individual jobs is disabled in this cluster."; @VisibleForTesting public static final String NO_EXECUTOR_OR_CONTAINER = "Configuration may not be null."; @VisibleForTesting static final String INVALID_EXECUTOR_CONFIG = "Executor name may not be left unset."; @VisibleForTesting static final String NO_CONTAINER_VOLUMES = "This scheduler is configured to disallow container volumes."; /** * Check validity of and populates defaults in a task configuration. This will return a deep copy * of the provided task configuration with default configuration values applied, and configuration * map values sanitized and applied to their respective struct fields. * * * @param config Task config to validate and populate. * @return A reference to the modified {@code config} (for chaining). * @throws TaskDescriptionException If the task is invalid. */ public ITaskConfig validateAndPopulate(ITaskConfig config) throws TaskDescriptionException { TaskConfig builder = config.newBuilder(); if (config.isSetTier() && !UserProvidedStrings.isGoodIdentifier(config.getTier())) { throw new TaskDescriptionException("Tier contains illegal characters: " + config.getTier()); } try { tierManager.getTier(config); } catch (IllegalArgumentException e) { throw new TaskDescriptionException(e.getMessage(), e); } if (!JobKeys.isValid(config.getJob())) { // Job key is set but invalid throw new TaskDescriptionException("Job key " + config.getJob() + " is invalid."); } // A task must either have an executor configuration or specify a Docker container. if (!builder.isSetExecutorConfig() && !(builder.isSetContainer() && builder.getContainer().isSetDocker())) { throw new TaskDescriptionException(NO_EXECUTOR_OR_CONTAINER); } // Docker containers don't require executors, validate the rest if (builder.isSetExecutorConfig()) { if (!builder.getExecutorConfig().isSetName()) { throw new TaskDescriptionException(INVALID_EXECUTOR_CONFIG); } executorSettings.getExecutorConfig(builder.getExecutorConfig().getName()).orElseThrow( () -> new TaskDescriptionException("Configuration for executor '" + builder.getExecutorConfig().getName() + "' doesn't exist.")); } IConstraint constraint = getDedicatedConstraint(config); if (constraint != null) { if (!isValueConstraint(constraint.getConstraint())) { throw new TaskDescriptionException("A dedicated constraint must be of value type."); } IValueConstraint valueConstraint = constraint.getConstraint().getValue(); if (valueConstraint.getValues().size() != 1) { throw new TaskDescriptionException("A dedicated constraint must have exactly one value"); } String dedicatedRole = getRole(valueConstraint); if (!("*".equals(dedicatedRole) || config.getJob().getRole().equals(dedicatedRole))) { throw new TaskDescriptionException( "Only " + dedicatedRole + " may use hosts dedicated for that role."); } } Optional<Container._Fields> containerType; if (config.isSetContainer()) { IContainer containerConfig = config.getContainer(); containerType = Optional.of(containerConfig.getSetField()); if (containerConfig.isSetDocker()) { if (!containerConfig.getDocker().isSetImage()) { throw new TaskDescriptionException("A container must specify an image."); } if (containerConfig.getDocker().getParameters().isEmpty()) { for (Map.Entry<String, String> e : settings.defaultDockerParameters.entries()) { builder.getContainer().getDocker().addToParameters( new DockerParameter(e.getKey(), e.getValue())); } } else { if (!settings.allowDockerParameters) { throw new TaskDescriptionException(NO_DOCKER_PARAMETERS); } } if (settings.requireDockerUseExecutor && !config.isSetExecutorConfig()) { throw new TaskDescriptionException(EXECUTOR_REQUIRED_WITH_DOCKER); } } } else { // Default to mesos container type if unset. containerType = Optional.of(Container._Fields.MESOS); } if (!containerType.isPresent()) { throw new TaskDescriptionException("A job must have a container type."); } if (!settings.allowedContainerTypes.contains(containerType.get())) { throw new TaskDescriptionException( "This scheduler is not configured to allow the container type " + containerType.get().toString()); } thriftBackfill.backfillTask(builder); String types = config.getResources().stream() .collect(Collectors.groupingBy(e -> ResourceType.fromResource(e))) .entrySet().stream() .filter(e -> !e.getKey().isMultipleAllowed() && e.getValue().size() > 1) .map(r -> r.getKey().getAuroraName()) .sorted() .collect(Collectors.joining(", ")); if (!Strings.isNullOrEmpty(types)) { throw new TaskDescriptionException("Multiple resource values are not supported for " + types); } Validator<Number> cpuvalidator = new GreaterThan(0.0, "num_cpus"); cpuvalidator.validate( ResourceManager.quantityOf(ResourceManager.getTaskResources(config, CPUS))); Validator<Number> ramvalidator = new GreaterThan(0.0, "ram_mb"); ramvalidator.validate( ResourceManager.quantityOf(ResourceManager.getTaskResources(config, RAM_MB))); Validator<Number> diskvalidator = new GreaterThan(0.0, "disk_mb"); diskvalidator.validate( ResourceManager.quantityOf(ResourceManager.getTaskResources(config, DISK_MB))); if (!settings.allowGpuResource && config.getResources().stream() .filter(r -> ResourceType.fromResource(r).equals(GPUS)) .findAny() .isPresent()) { throw new TaskDescriptionException("GPU resource support is disabled in this cluster."); } if (!settings.enableMesosFetcher && !config.getMesosFetcherUris().isEmpty()) { throw new TaskDescriptionException(MESOS_FETCHER_DISABLED); } if (config.getContainer().isSetMesos()) { IMesosContainer container = config.getContainer().getMesos(); if (!settings.allowContainerVolumes && !container.getVolumes().isEmpty()) { throw new TaskDescriptionException(NO_CONTAINER_VOLUMES); } } maybeFillLinks(builder); return ITaskConfig.build(builder); } /** * Provides a filter for the given constraint name. * * @param name The name of the constraint. * @return A filter that matches the constraint. */ public static Predicate<IConstraint> getConstraintByName(final String name) { return constraint -> constraint.getName().equals(name); } private static void maybeFillLinks(TaskConfig task) { if (task.getTaskLinksSize() == 0) { ImmutableMap.Builder<String, String> links = ImmutableMap.builder(); for (IResource resource : ResourceManager.getTaskResources(ITaskConfig.build(task), PORTS)) { if (resource.getNamedPort().equals("health")) { links.put("health", "http://%host%:%port:health%"); } else if (resource.getNamedPort().equals("http")) { links.put("http", "http://%host%:%port:http%"); } } task.setTaskLinks(links.build()); } } /** * Thrown when an invalid task or job configuration is encountered. */ public static class TaskDescriptionException extends Exception { public TaskDescriptionException(String msg, Exception e) { super(msg, e); } public TaskDescriptionException(String msg) { super(msg); } } }