blob: a45ed1b2175b7dc16d621a44fbccfb8f957ae2b5 [file]
// 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 <map>
#include <string>
#include <vector>
#include <gmock/gmock.h>
#include <mesos/mesos.hpp>
#include <mesos/slave/container_logger.hpp>
#include <mesos/slave/isolator.hpp>
#include <process/future.hpp>
#include <process/owned.hpp>
#include <stout/net.hpp>
#include <stout/strings.hpp>
#include "slave/flags.hpp"
#include "slave/containerizer/fetcher.hpp"
#include "slave/containerizer/mesos/containerizer.hpp"
#include "slave/containerizer/mesos/launcher.hpp"
#include "slave/containerizer/mesos/provisioner/provisioner.hpp"
#include "tests/flags.hpp"
#include "tests/mesos.hpp"
#include "tests/utils.hpp"
#include "tests/containerizer/isolator.hpp"
#include "tests/containerizer/launcher.hpp"
using namespace process;
using mesos::internal::master::Master;
using mesos::internal::slave::Fetcher;
using mesos::internal::slave::Launcher;
using mesos::internal::slave::MesosContainerizer;
using mesos::internal::slave::MesosContainerizerProcess;
using mesos::internal::slave::PosixLauncher;
using mesos::internal::slave::Provisioner;
using mesos::internal::slave::Slave;
using mesos::internal::slave::state::ExecutorState;
using mesos::internal::slave::state::FrameworkState;
using mesos::internal::slave::state::RunState;
using mesos::internal::slave::state::SlaveState;
using mesos::slave::ContainerConfig;
using mesos::slave::ContainerLaunchInfo;
using mesos::slave::ContainerLimitation;
using mesos::slave::ContainerLogger;
using mesos::slave::ContainerState;
using mesos::slave::Isolator;
using std::list;
using std::map;
using std::string;
using std::vector;
using testing::_;
using testing::DoAll;
using testing::Invoke;
using testing::Return;
namespace mesos {
namespace internal {
namespace tests {
class MesosContainerizerIsolatorPreparationTest :
public TemporaryDirectoryTest
{
public:
// Construct a MesosContainerizer with TestIsolator(s) which use the provided
// 'prepare' command(s).
Try<MesosContainerizer*> CreateContainerizer(
Fetcher* fetcher,
const vector<Option<ContainerLaunchInfo>>& launchInfos)
{
vector<Owned<Isolator>> isolators;
foreach (const Option<ContainerLaunchInfo>& launchInfo, launchInfos) {
Try<Isolator*> isolator = TestIsolatorProcess::create(launchInfo);
if (isolator.isError()) {
return Error(isolator.error());
}
isolators.push_back(Owned<Isolator>(isolator.get()));
}
slave::Flags flags;
flags.launcher_dir = path::join(tests::flags.build_dir, "src");
Try<Launcher*> launcher = PosixLauncher::create(flags);
if (launcher.isError()) {
return Error(launcher.error());
}
// Create and initialize a new container logger.
Try<ContainerLogger*> logger =
ContainerLogger::create(flags.container_logger);
if (logger.isError()) {
return Error("Failed to create container logger: " + logger.error());
}
Try<Owned<Provisioner>> provisioner = Provisioner::create(flags);
if (provisioner.isError()) {
return Error("Failed to create provisioner: " + provisioner.error());
}
return new MesosContainerizer(
flags,
false,
fetcher,
Owned<ContainerLogger>(logger.get()),
Owned<Launcher>(launcher.get()),
provisioner.get(),
isolators);
}
Try<MesosContainerizer*> CreateContainerizer(
Fetcher* fetcher,
const Option<ContainerLaunchInfo>& launchInfo)
{
vector<Option<ContainerLaunchInfo>> launchInfos;
launchInfos.push_back(launchInfo);
return CreateContainerizer(fetcher, launchInfos);
}
};
// The isolator has a prepare command that succeeds.
TEST_F(MesosContainerizerIsolatorPreparationTest, ScriptSucceeds)
{
string directory = os::getcwd(); // We're inside a temporary sandbox.
string file = path::join(directory, "child.script.executed");
Fetcher fetcher;
ContainerLaunchInfo launchInfo;
launchInfo.add_commands()->set_value("touch " + file);
Try<MesosContainerizer*> containerizer = CreateContainerizer(
&fetcher,
launchInfo);
CHECK_SOME(containerizer);
ContainerID containerId;
containerId.set_value("test_container");
Future<bool> launch = containerizer.get()->launch(
containerId,
CREATE_EXECUTOR_INFO("executor", "exit 0"),
directory,
None(),
SlaveID(),
PID<Slave>(),
false);
// Wait until the launch completes.
AWAIT_READY(launch);
// Wait for the child (preparation script + executor) to complete.
Future<containerizer::Termination> wait =
containerizer.get()->wait(containerId);
AWAIT_READY(wait);
// Check the child exited correctly.
EXPECT_TRUE(wait.get().has_status());
EXPECT_EQ(0, wait.get().status());
// Check the preparation script actually ran.
EXPECT_TRUE(os::exists(file));
// Destroy the container.
containerizer.get()->destroy(containerId);
delete containerizer.get();
}
// The isolator has a prepare command that fails.
TEST_F(MesosContainerizerIsolatorPreparationTest, ScriptFails)
{
string directory = os::getcwd(); // We're inside a temporary sandbox.
string file = path::join(directory, "child.script.executed");
Fetcher fetcher;
ContainerLaunchInfo launchInfo;
launchInfo.add_commands()->set_value("touch " + file + " && exit 1");
Try<MesosContainerizer*> containerizer = CreateContainerizer(
&fetcher,
launchInfo);
CHECK_SOME(containerizer);
ContainerID containerId;
containerId.set_value("test_container");
Future<bool> launch = containerizer.get()->launch(
containerId,
CREATE_EXECUTOR_INFO("executor", "exit 0"),
directory,
None(),
SlaveID(),
PID<Slave>(),
false);
// Wait until the launch completes.
AWAIT_READY(launch);
// Wait for the child (preparation script + executor) to complete.
Future<containerizer::Termination> wait =
containerizer.get()->wait(containerId);
AWAIT_READY(wait);
// Check the child failed to exit correctly.
EXPECT_TRUE(wait.get().has_status());
EXPECT_NE(0, wait.get().status());
// Check the preparation script actually ran.
EXPECT_TRUE(os::exists(file));
// Destroy the container.
containerizer.get()->destroy(containerId);
delete containerizer.get();
}
// There are two isolators, one with a prepare command that succeeds
// and another that fails. The execution order is not defined but the
// launch should fail from the failing prepare command.
TEST_F(MesosContainerizerIsolatorPreparationTest, MultipleScripts)
{
string directory = os::getcwd(); // We're inside a temporary sandbox.
string file1 = path::join(directory, "child.script.executed.1");
string file2 = path::join(directory, "child.script.executed.2");
vector<Option<ContainerLaunchInfo>> launchInfos;
// This isolator prepare command one will succeed if called first, otherwise
// it won't get run.
ContainerLaunchInfo launch1;
launch1.add_commands()->set_value("touch " + file1 + " && exit 0");
launchInfos.push_back(launch1);
// This will fail, either first or after the successful command.
ContainerLaunchInfo launch2;
launch2.add_commands()->set_value("touch " + file2 + " && exit 1");
launchInfos.push_back(launch2);
Fetcher fetcher;
Try<MesosContainerizer*> containerizer =
CreateContainerizer(&fetcher, launchInfos);
CHECK_SOME(containerizer);
ContainerID containerId;
containerId.set_value("test_container");
Future<bool> launch = containerizer.get()->launch(
containerId,
CREATE_EXECUTOR_INFO("executor", "exit 0"),
directory,
None(),
SlaveID(),
PID<Slave>(),
false);
// Wait until the launch completes.
AWAIT_READY(launch);
// Wait for the child (preparation script(s) + executor) to complete.
Future<containerizer::Termination> wait =
containerizer.get()->wait(containerId);
AWAIT_READY(wait);
// Check the child failed to exit correctly.
EXPECT_TRUE(wait.get().has_status());
EXPECT_NE(0, wait.get().status());
// Check the failing preparation script has actually ran.
EXPECT_TRUE(os::exists(file2));
// Destroy the container.
containerizer.get()->destroy(containerId);
delete containerizer.get();
}
// The isolator sets an environment variable for the Executor. The
// Executor then creates a file as pointed to by environment
// variable. Finally, after the executor has terminated, we check for
// the existence of the file.
TEST_F(MesosContainerizerIsolatorPreparationTest, ExecutorEnvironmentVariable)
{
// Set LIBPROCESS_IP so that we can test if it gets passed to the executor.
Option<string> libprocessIP = os::getenv("LIBPROCESS_IP");
net::IP ip = net::IP(INADDR_LOOPBACK);
os::setenv("LIBPROCESS_IP", stringify(ip));
string directory = os::getcwd(); // We're inside a temporary sandbox.
string file = path::join(directory, "child.script.executed");
Fetcher fetcher;
ContainerLaunchInfo launchInfo;
Environment::Variable* variable =
launchInfo.mutable_environment()->add_variables();
variable->set_name("TEST_ENVIRONMENT");
variable->set_value(file);
Try<MesosContainerizer*> containerizer = CreateContainerizer(
&fetcher,
launchInfo);
CHECK_SOME(containerizer);
ContainerID containerId;
containerId.set_value("test_container");
// Ensure that LIBPROCESS_IP has been passed despite the explicit
// specification of the environment. If so, then touch the test file.
string executorCmd =
"if [ -n \"$LIBPROCESS_IP\" ]; "
"then touch $TEST_ENVIRONMENT; fi";
Future<bool> launch = containerizer.get()->launch(
containerId,
CREATE_EXECUTOR_INFO("executor", executorCmd),
directory,
None(),
SlaveID(),
PID<Slave>(),
false);
// Wait until the launch completes.
AWAIT_READY(launch);
// Wait for the child (preparation script + executor) to complete.
Future<containerizer::Termination> wait =
containerizer.get()->wait(containerId);
AWAIT_READY(wait);
// Check the child exited correctly.
EXPECT_TRUE(wait.get().has_status());
EXPECT_EQ(0, wait.get().status());
// Check the preparation script actually ran.
EXPECT_TRUE(os::exists(file));
// Destroy the container.
containerizer.get()->destroy(containerId);
// Reset LIBPROCESS_IP if necessary.
if (libprocessIP.isSome()) {
os::setenv("LIBPROCESS_IP", libprocessIP.get());
} else {
os::unsetenv("LIBPROCESS_IP");
}
delete containerizer.get();
}
class MesosContainerizerExecuteTest : public TemporaryDirectoryTest {};
TEST_F(MesosContainerizerExecuteTest, IoRedirection)
{
string directory = os::getcwd(); // We're inside a temporary sandbox.
slave::Flags flags;
flags.launcher_dir = path::join(tests::flags.build_dir, "src");
Fetcher fetcher;
// Use local=false so std{err,out} are redirected to files.
Try<MesosContainerizer*> containerizer =
MesosContainerizer::create(flags, false, &fetcher);
ASSERT_SOME(containerizer);
ContainerID containerId;
containerId.set_value("test_container");
string errMsg = "this is stderr";
string outMsg = "this is stdout";
string command =
"(echo '" + errMsg + "' 1>&2) && echo '" + outMsg + "'";
Future<bool> launch = containerizer.get()->launch(
containerId,
CREATE_EXECUTOR_INFO("executor", command),
directory,
None(),
SlaveID(),
PID<Slave>(),
false);
// Wait for the launch to complete.
AWAIT_READY(launch);
// Wait on the container.
Future<containerizer::Termination> wait =
containerizer.get()->wait(containerId);
AWAIT_READY(wait);
// Check the executor exited correctly.
EXPECT_TRUE(wait.get().has_status());
EXPECT_EQ(0, wait.get().status());
// Check that std{err, out} was redirected.
// NOTE: Fetcher uses GLOG, which outputs extra information to
// stderr.
Try<string> stderr = os::read(path::join(directory, "stderr"));
ASSERT_SOME(stderr);
EXPECT_TRUE(strings::contains(stderr.get(), errMsg));
EXPECT_SOME_EQ(outMsg + "\n", os::read(path::join(directory, "stdout")));
delete containerizer.get();
}
class MesosContainerizerDestroyTest : public MesosTest {};
class MockMesosContainerizerProcess : public MesosContainerizerProcess
{
public:
MockMesosContainerizerProcess(
const slave::Flags& flags,
bool local,
Fetcher* fetcher,
const Owned<ContainerLogger>& logger,
const Owned<Launcher>& launcher,
const Owned<Provisioner>& provisioner,
const vector<Owned<Isolator>>& isolators)
: MesosContainerizerProcess(
flags,
local,
fetcher,
logger,
launcher,
provisioner,
isolators)
{
// NOTE: See TestContainerizer::setup for why we use
// 'EXPECT_CALL' and 'WillRepeatedly' here instead of
// 'ON_CALL' and 'WillByDefault'.
EXPECT_CALL(*this, exec(_, _))
.WillRepeatedly(Invoke(this, &MockMesosContainerizerProcess::_exec));
}
MOCK_METHOD2(
exec,
Future<bool>(
const ContainerID& containerId,
int pipeWrite));
Future<bool> _exec(
const ContainerID& containerId,
int pipeWrite)
{
return MesosContainerizerProcess::exec(
containerId,
pipeWrite);
}
};
class MockIsolator : public mesos::slave::Isolator
{
public:
MockIsolator()
{
EXPECT_CALL(*this, watch(_))
.WillRepeatedly(Return(watchPromise.future()));
EXPECT_CALL(*this, isolate(_, _))
.WillRepeatedly(Return(Nothing()));
EXPECT_CALL(*this, cleanup(_))
.WillRepeatedly(Return(Nothing()));
EXPECT_CALL(*this, prepare(_, _))
.WillRepeatedly(Invoke(this, &MockIsolator::_prepare));
}
MOCK_METHOD2(
recover,
Future<Nothing>(
const list<ContainerState>&,
const hashset<ContainerID>&));
MOCK_METHOD2(
prepare,
Future<Option<ContainerLaunchInfo>>(
const ContainerID&,
const ContainerConfig&));
virtual Future<Option<ContainerLaunchInfo>> _prepare(
const ContainerID& containerId,
const ContainerConfig& containerConfig)
{
return None();
}
MOCK_METHOD2(
isolate,
Future<Nothing>(const ContainerID&, pid_t));
MOCK_METHOD1(
watch,
Future<mesos::slave::ContainerLimitation>(const ContainerID&));
MOCK_METHOD2(
update,
Future<Nothing>(const ContainerID&, const Resources&));
MOCK_METHOD1(
usage,
Future<ResourceStatistics>(const ContainerID&));
MOCK_METHOD1(
cleanup,
Future<Nothing>(const ContainerID&));
Promise<mesos::slave::ContainerLimitation> watchPromise;
};
// Destroying a mesos containerizer while it is fetching should
// complete without waiting for the fetching to finish.
TEST_F(MesosContainerizerDestroyTest, DestroyWhileFetching)
{
slave::Flags flags = CreateSlaveFlags();
Try<Launcher*> launcher = PosixLauncher::create(flags);
ASSERT_SOME(launcher);
Fetcher fetcher;
Try<ContainerLogger*> logger =
ContainerLogger::create(flags.container_logger);
ASSERT_SOME(logger);
Try<Owned<Provisioner>> provisioner = Provisioner::create(flags);
ASSERT_SOME(provisioner);
MockMesosContainerizerProcess* process = new MockMesosContainerizerProcess(
flags,
true,
&fetcher,
Owned<ContainerLogger>(logger.get()),
Owned<Launcher>(launcher.get()),
provisioner.get(),
vector<Owned<Isolator>>());
Future<Nothing> exec;
Promise<bool> promise;
// Letting exec hang to simulate a long fetch.
EXPECT_CALL(*process, exec(_, _))
.WillOnce(DoAll(FutureSatisfy(&exec),
Return(promise.future())));
MesosContainerizer containerizer((Owned<MesosContainerizerProcess>(process)));
ContainerID containerId;
containerId.set_value("test_container");
TaskInfo taskInfo;
CommandInfo commandInfo;
taskInfo.mutable_command()->MergeFrom(commandInfo);
containerizer.launch(
containerId,
taskInfo,
CREATE_EXECUTOR_INFO("executor", "exit 0"),
os::getcwd(),
None(),
SlaveID(),
PID<Slave>(),
false);
Future<containerizer::Termination> wait = containerizer.wait(containerId);
AWAIT_READY(exec);
containerizer.destroy(containerId);
// The container should still exit even if fetch didn't complete.
AWAIT_READY(wait);
}
// Destroying a mesos containerizer while it is preparing should wait
// until isolators are finished preparing before destroying.
TEST_F(MesosContainerizerDestroyTest, DestroyWhilePreparing)
{
slave::Flags flags = CreateSlaveFlags();
Try<Launcher*> launcher = PosixLauncher::create(flags);
ASSERT_SOME(launcher);
MockIsolator* isolator = new MockIsolator();
Future<Nothing> prepare;
Promise<Option<ContainerLaunchInfo>> promise;
// Simulate a long prepare from the isolator.
EXPECT_CALL(*isolator, prepare(_, _))
.WillOnce(DoAll(FutureSatisfy(&prepare),
Return(promise.future())));
Fetcher fetcher;
Try<ContainerLogger*> logger =
ContainerLogger::create(flags.container_logger);
ASSERT_SOME(logger);
Try<Owned<Provisioner>> provisioner = Provisioner::create(flags);
ASSERT_SOME(provisioner);
MockMesosContainerizerProcess* process = new MockMesosContainerizerProcess(
flags,
true,
&fetcher,
Owned<ContainerLogger>(logger.get()),
Owned<Launcher>(launcher.get()),
provisioner.get(),
{Owned<Isolator>(isolator)});
MesosContainerizer containerizer((Owned<MesosContainerizerProcess>(process)));
ContainerID containerId;
containerId.set_value("test_container");
TaskInfo taskInfo;
CommandInfo commandInfo;
taskInfo.mutable_command()->MergeFrom(commandInfo);
containerizer.launch(
containerId,
taskInfo,
CREATE_EXECUTOR_INFO("executor", "exit 0"),
os::getcwd(),
None(),
SlaveID(),
PID<Slave>(),
false);
Future<containerizer::Termination> wait = containerizer.wait(containerId);
AWAIT_READY(prepare);
containerizer.destroy(containerId);
// The container should not exit until prepare is complete.
ASSERT_TRUE(wait.isPending());
// Need to help the compiler to disambiguate between overloads.
ContainerLaunchInfo launchInfo;
launchInfo.add_commands()->CopyFrom(commandInfo);
Option<ContainerLaunchInfo> option = launchInfo;
promise.set(option);
AWAIT_READY(wait);
containerizer::Termination termination = wait.get();
EXPECT_EQ(
"Container destroyed while preparing isolators",
termination.message());
EXPECT_FALSE(termination.has_status());
}
// This action destroys the container using the real launcher and
// waits until the destroy is complete.
ACTION_P(InvokeDestroyAndWait, launcher)
{
Future<Nothing> destroy = launcher->real->destroy(arg0);
AWAIT_READY(destroy);
}
// This test verifies that when a container destruction fails the
// 'container_destroy_errors' metric is updated.
TEST_F(MesosContainerizerDestroyTest, LauncherDestroyFailure)
{
// Create a TestLauncher backed by PosixLauncher.
slave::Flags flags = CreateSlaveFlags();
Try<Launcher*> launcher_ = PosixLauncher::create(flags);
ASSERT_SOME(launcher_);
TestLauncher* launcher = new TestLauncher(Owned<Launcher>(launcher_.get()));
Fetcher fetcher;
Try<ContainerLogger*> logger =
ContainerLogger::create(flags.container_logger);
ASSERT_SOME(logger);
Try<Owned<Provisioner>> provisioner = Provisioner::create(flags);
ASSERT_SOME(provisioner);
MesosContainerizerProcess* process = new MesosContainerizerProcess(
flags,
true,
&fetcher,
Owned<ContainerLogger>(logger.get()),
Owned<Launcher>(launcher),
provisioner.get(),
vector<Owned<Isolator>>());
MesosContainerizer containerizer((Owned<MesosContainerizerProcess>(process)));
ContainerID containerId;
containerId.set_value("test_container");
TaskInfo taskInfo;
CommandInfo commandInfo;
taskInfo.mutable_command()->MergeFrom(commandInfo);
// Destroy the container using the PosixLauncher but return a failed
// future to the containerizer.
EXPECT_CALL(*launcher, destroy(_))
.WillOnce(DoAll(InvokeDestroyAndWait(launcher),
Return(Failure("Destroy failure"))));
Future<bool> launch = containerizer.launch(
containerId,
taskInfo,
CREATE_EXECUTOR_INFO("executor", "sleep 1000"),
os::getcwd(),
None(),
SlaveID(),
PID<Slave>(),
false);
AWAIT_READY(launch);
Future<containerizer::Termination> wait = containerizer.wait(containerId);
containerizer.destroy(containerId);
// The container destroy should fail.
AWAIT_FAILED(wait);
// We settle the clock here to ensure that the processing of
// 'MesosContainerizerProcess::__destroy()' is complete and the
// metric is updated.
Clock::pause();
Clock::settle();
Clock::resume();
// Ensure that the metric is updated.
JSON::Object metrics = Metrics();
ASSERT_EQ(
1u,
metrics.values.count("containerizer/mesos/container_destroy_errors"));
ASSERT_EQ(
1u,
metrics.values["containerizer/mesos/container_destroy_errors"]);
}
class MesosContainerizerRecoverTest : public MesosTest {};
// This test checks that MesosContainerizer doesn't recover executors
// that were started by another containerizer (e.g: Docker).
TEST_F(MesosContainerizerRecoverTest, SkipRecoverNonMesosContainers)
{
slave::Flags flags = CreateSlaveFlags();
Fetcher fetcher;
Try<MesosContainerizer*> containerizer =
MesosContainerizer::create(flags, true, &fetcher);
ASSERT_SOME(containerizer);
ExecutorID executorId;
executorId.set_value(UUID::random().toString());
ContainerID containerId;
containerId.set_value(UUID::random().toString());
ExecutorInfo executorInfo;
executorInfo.mutable_container()->set_type(ContainerInfo::DOCKER);
ExecutorState executorState;
executorState.info = executorInfo;
executorState.latest = containerId;
RunState runState;
runState.id = containerId;
executorState.runs.put(containerId, runState);
FrameworkState frameworkState;
frameworkState.executors.put(executorId, executorState);
SlaveState slaveState;
FrameworkID frameworkId;
frameworkId.set_value(UUID::random().toString());
slaveState.frameworks.put(frameworkId, frameworkState);
Future<Nothing> recover = containerizer.get()->recover(slaveState);
AWAIT_READY(recover);
Future<hashset<ContainerID>> containers = containerizer.get()->containers();
AWAIT_READY(containers);
EXPECT_EQ(0u, containers.get().size());
delete containerizer.get();
}
} // namespace tests {
} // namespace internal {
} // namespace mesos {