blob: 990c138d3e18f6f2bffb98915da8e69a16f2e30e [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.
#include <list>
#include <string>
#include <glog/logging.h>
#include <process/id.hpp>
#include <stout/error.hpp>
#include <stout/foreach.hpp>
#include <stout/stringify.hpp>
#include <stout/strings.hpp>
#include <mesos/docker/v1.hpp>
#include "slave/flags.hpp"
#include "slave/containerizer/mesos/isolators/docker/runtime.hpp"
using std::list;
using std::string;
using google::protobuf::RepeatedPtrField;
using process::Failure;
using process::Future;
using process::Owned;
using process::PID;
using mesos::slave::ContainerConfig;
using mesos::slave::ContainerLaunchInfo;
using mesos::slave::Isolator;
namespace mesos {
namespace internal {
namespace slave {
DockerRuntimeIsolatorProcess::DockerRuntimeIsolatorProcess(
const Flags& _flags)
: ProcessBase(process::ID::generate("docker-runtime-isolator")),
flags(_flags) {}
DockerRuntimeIsolatorProcess::~DockerRuntimeIsolatorProcess() {}
bool DockerRuntimeIsolatorProcess::supportsNesting()
{
return true;
}
bool DockerRuntimeIsolatorProcess::supportsStandalone()
{
return true;
}
Try<Isolator*> DockerRuntimeIsolatorProcess::create(const Flags& flags)
{
process::Owned<MesosIsolatorProcess> process(
new DockerRuntimeIsolatorProcess(flags));
return new MesosIsolator(process);
}
Future<Option<ContainerLaunchInfo>> DockerRuntimeIsolatorProcess::prepare(
const ContainerID& containerId,
const ContainerConfig& containerConfig)
{
if (flags.docker_ignore_runtime) {
return None();
}
if (!containerConfig.has_container_info()) {
return None();
}
if (containerConfig.container_info().type() != ContainerInfo::MESOS) {
return Failure("Can only prepare docker runtime for a MESOS container");
}
if (!containerConfig.has_docker()) {
// No docker image default config available.
return None();
}
Option<Environment> environment =
getLaunchEnvironment(containerId, containerConfig);
Option<string> workingDirectory =
getWorkingDirectory(containerConfig);
Option<string> user = getContainerUser(containerConfig);
if (user.isSome()) {
// TODO(gilbert): Parse the container user from 'user|uid[:group|gid]'
// to corresponding user and group. UID and GID should be numerical,
// while username and groupname should be non-numerical.
// Please see:
// https://github.com/docker/docker/blob/master/image/spec/v1.md#container-runconfig-field-descriptions // NOLINT
// TODO(gilbert): Support container user once container capabilities
// land. Currently, we just log a warning instead of a failure, so
// images with user defined can still be executable by ROOT.
LOG(WARNING) << "Container user '" << user.get() << "' is not "
<< "supported yet for container " << containerId;
}
Result<CommandInfo> command =
getLaunchCommand(containerId, containerConfig);
if (command.isError()) {
return Failure("Failed to determine the launch command: " +
command.error());
}
// Set 'launchInfo'.
ContainerLaunchInfo launchInfo;
// If working directory or command exists, operation has to be
// handled specially for the command task. For the command task,
// the working directory and task command will be passed to
// command executor as flags. For custom executor, default
// executor and nested container cases, these information will
// be included in 'ContainerLaunchInfo', and will be passed
// back to containerizer.
if (containerConfig.has_task_info()) {
// Command task case.
if (environment.isSome()) {
launchInfo.mutable_task_environment()->CopyFrom(environment.get());
}
// Pass working directory to command executor as a flag.
if (workingDirectory.isSome()) {
launchInfo.mutable_command()->add_arguments(
"--working_directory=" + workingDirectory.get());
}
// Pass task command as a flag, which will be loaded by
// command executor.
if (command.isSome()) {
launchInfo.mutable_command()->add_arguments(
"--task_command=" +
stringify(JSON::protobuf(command.get())));
}
} else {
// The custom executor, default executor and nested container cases.
if (environment.isSome()) {
launchInfo.mutable_environment()->CopyFrom(environment.get());
}
if (workingDirectory.isSome()) {
launchInfo.set_working_directory(workingDirectory.get());
}
if (command.isSome()) {
launchInfo.mutable_command()->CopyFrom(command.get());
}
}
return launchInfo;
}
Option<Environment> DockerRuntimeIsolatorProcess::getLaunchEnvironment(
const ContainerID& containerId,
const ContainerConfig& containerConfig)
{
CHECK(containerConfig.docker().manifest().has_config());
if (containerConfig.docker().manifest().config().env_size() == 0) {
return None();
}
Environment environment;
foreach (const string& env,
containerConfig.docker().manifest().config().env()) {
// Use `find_first_of` to prevent to case that there are
// multiple '=' existing.
size_t position = env.find_first_of('=');
if (position == string::npos) {
VLOG(1) << "Skipping invalid environment variable: '"
<< env << "' in docker manifest for container "
<< containerId;
continue;
}
const string name = env.substr(0, position);
const string value = env.substr(position + 1);
// Keep all environment from runtime isolator. If there exists
// environment variable duplicate cases, it will be overwritten
// in mesos containerizer.
Environment::Variable* variable = environment.add_variables();
variable->set_name(name);
variable->set_value(value);
}
return environment;
}
// This method reads the CommandInfo from ContainerConfig and optional
// TaskInfo, and merge them with docker image default Entrypoint and
// Cmd. It returns a merged CommandInfo which will be used to launch
// the docker container. If no need to modify the command, this method
// will return none.
Result<CommandInfo> DockerRuntimeIsolatorProcess::getLaunchCommand(
const ContainerID& containerId,
const ContainerConfig& containerConfig)
{
CHECK(containerConfig.docker().manifest().has_config());
// We may or may not mutate the CommandInfo for executor depending
// on the logic table below. For custom executor, default executor
// and nested container cases, we make changes to the command
// directly. For command task case, if no need to change the launch
// command for the user task, we do not do anything and return the
// CommandInfo from ContainerConfig. We only add a flag
// `--task_command` to carry command as a JSON object if it is
// necessary to mutate.
CommandInfo command;
if (containerConfig.has_task_info()) {
// Command task case.
CHECK(containerConfig.task_info().has_command());
command = containerConfig.task_info().command();
} else {
// Custom executor, default executor or nested container case.
command = containerConfig.command_info();
}
// We merge the CommandInfo following the logic:
// 1. If 'shell' is true, we will ignore Entrypoint and Cmd from
// the docker image (row 5-8).
// 2. If 'shell' is false and 'value' is set, we will ignore the
// Entrypoint and Cmd from the docker image as well (row 3-4).
// 3. If 'shell' is false and 'value' is not set, use the docker
// image specified runtime configuration.
// i. If 'Entrypoint' is set, it is treated as executable,
// then 'Cmd' get appended as arguments.
// ii.If 'Entrypoint' is not set, use the first 'Cmd' as
// executable and the others as arguments.
// +---------+--------------+--------------+--------------+--------------+
// | | Entrypoint=0 | Entrypoint=0 | Entrypoint=1 | Entrypoint=1 |
// | | Cmd=0 | Cmd=1 | Cmd=0 | Cmd=1 |
// +---------+--------------+--------------+--------------+--------------+
// | sh=0 | Error | ./Cmd[0] | ./Entrypt[0] | ./Entrypt[0] |
// | value=0 | | Cmd[1].. | Entrypt[1].. | Entrypt[1].. |
// | argv=0 | | | | Cmd.. |
// +---------+--------------+--------------+--------------+--------------+
// | sh=0 | Error | ./Cmd[0] | ./Entrypt[0] | ./Entrypt[0] |
// | value=0 | | argv | Entrypt[1].. | Entrypt[1].. |
// | argv=1 | | | argv | argv |
// +---------+--------------+--------------+--------------+--------------+
// | sh=0 | ./value | ./value | ./value | ./value |
// | value=1 | | | | |
// | argv=0 | | | | |
// +---------+--------------+--------------+--------------+--------------+
// | sh=0 | ./value | ./value | ./value | ./value |
// | value=1 | argv | argv | argv | argv |
// | argv=1 | | | | |
// +---------+--------------+--------------+--------------+--------------+
// | sh=1 | Error | Error | Error | Error |
// | value=0 | | | | |
// | argv=0 | | | | |
// +---------+--------------+--------------+--------------+--------------+
// | sh=1 | Error | Error | Error | Error |
// | value=0 | | | | |
// | argv=1 | | | | |
// +---------+--------------+--------------+--------------+--------------+
// | sh=1 | /bin/sh -c | /bin/sh -c | /bin/sh -c | /bin/sh -c |
// | value=1 | value | value | value | value |
// | argv=0 | | | | |
// +---------+--------------+--------------+--------------+--------------+
// | sh=1 | /bin/sh -c | /bin/sh -c | /bin/sh -c | /bin/sh -c |
// | value=1 | value | value | value | value |
// | argv=1 | | | | |
// +---------+--------------+--------------+--------------+--------------+
if (command.shell()) {
if (!command.has_value()) {
return Error("Shell specified but no command value provided");
}
// No need to mutate command (row 7-8).
return None();
}
if (command.has_value()) {
// No need to mutate command (row 3-4).
return None();
}
// TODO(gilbert): Deprecate 'override' flag option in command task.
// We do not exclude override case here.
// We keep the arguments of commandInfo while it does not have a
// value, so that arguments can be specified by user, which is
// running with default image Entrypoint.
const docker::spec::v1::ImageManifest::Config& config =
containerConfig.docker().manifest().config();
// Filter out executable for commandInfo value.
if (config.entrypoint_size() > 0) {
command.set_value(config.entrypoint(0));
// Put user defined argv after default entrypoint argv
// in sequence.
const RepeatedPtrField<string> arguments = command.arguments();
command.clear_arguments();
command.add_arguments(config.entrypoint(0));
for (int i = 1; i < config.entrypoint_size(); i++) {
command.add_arguments(config.entrypoint(i));
}
// Append all possible user argv after entrypoint arguments.
command.mutable_arguments()->MergeFrom(arguments);
// Overwrite default cmd arguments if CommandInfo arguments are
// set by user. The logic below is the case that no argument is
// set by user.
if (command.arguments_size() == config.entrypoint_size()) {
foreach (const string& cmd, config.cmd()) {
command.add_arguments(cmd);
}
}
} else if (config.cmd_size() > 0) {
command.set_value(config.cmd(0));
// Put user defined argv after default cmd[0].
const RepeatedPtrField<string> arguments = command.arguments();
command.clear_arguments();
command.add_arguments(config.cmd(0));
// Append all possible user argv after cmd[0].
command.mutable_arguments()->MergeFrom(arguments);
// Overwrite default cmd arguments if CommandInfo arguments
// are set by user.
if (command.arguments_size() == 1) {
for (int i = 1; i < config.cmd_size(); i++) {
command.add_arguments(config.cmd(i));
}
}
} else {
return Error("No executable is found");
}
return command;
}
Option<string> DockerRuntimeIsolatorProcess::getWorkingDirectory(
const ContainerConfig& containerConfig)
{
CHECK(containerConfig.docker().manifest().has_config());
// NOTE: In docker manifest, if an image working directory is none,
// it will be set as `"WorkingDir": ""`.
if (!containerConfig.docker().manifest().config().has_workingdir() ||
containerConfig.docker().manifest().config().workingdir() == "") {
return None();
}
return containerConfig.docker().manifest().config().workingdir();
}
Option<string> DockerRuntimeIsolatorProcess::getContainerUser(
const ContainerConfig& containerConfig)
{
// NOTE: In docker manifest, if its container user is none, it may be
// set as `"User": ""`.
if (!containerConfig.docker().manifest().config().has_user() ||
containerConfig.docker().manifest().config().user() == "") {
return None();
}
return containerConfig.docker().manifest().config().user();
}
} // namespace slave {
} // namespace internal {
} // namespace mesos {