org.apache.aurora.scheduler.configuration.ConfigurationManager.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.aurora.scheduler.configuration.ConfigurationManager.java

Source

/**
 * 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);
        }
    }
}