blob: b1f38aefab3d9b60d8f184dce82870b91a427929 [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 <gmock/gmock.h>
#include <stout/duration.hpp>
#include <stout/gtest.hpp>
#include <stout/json.hpp>
#include <stout/os.hpp>
#include <stout/path.hpp>
#include <stout/stringify.hpp>
#include <process/clock.hpp>
#include <process/future.hpp>
#include <process/gmock.hpp>
#include <process/owned.hpp>
#include <mesos/docker/spec.hpp>
#ifdef __linux__
#include "linux/fs.hpp"
#endif // __linux__
#include "slave/containerizer/mesos/provisioner/constants.hpp"
#include "slave/containerizer/mesos/provisioner/paths.hpp"
#include "slave/containerizer/mesos/provisioner/docker/message.hpp"
#include "slave/containerizer/mesos/provisioner/docker/metadata_manager.hpp"
#include "slave/containerizer/mesos/provisioner/docker/paths.hpp"
#include "slave/containerizer/mesos/provisioner/docker/puller.hpp"
#include "slave/containerizer/mesos/provisioner/docker/registry_puller.hpp"
#include "slave/containerizer/mesos/provisioner/docker/store.hpp"
#include "tests/environment.hpp"
#include "tests/mesos.hpp"
#include "tests/utils.hpp"
#ifdef __linux__
#include "tests/containerizer/docker_archive.hpp"
#endif // __linux__
namespace master = mesos::internal::master;
namespace paths = mesos::internal::slave::docker::paths;
namespace slave = mesos::internal::slave;
namespace spec = ::docker::spec;
using std::string;
using std::vector;
using process::Clock;
using process::Future;
using process::Owned;
using process::PID;
using process::Promise;
using master::Master;
using mesos::internal::slave::AUFS_BACKEND;
using mesos::internal::slave::Containerizer;
using mesos::internal::slave::COPY_BACKEND;
using mesos::internal::slave::Fetcher;
using mesos::internal::slave::MesosContainerizer;
using mesos::internal::slave::OVERLAY_BACKEND;
using mesos::internal::slave::Provisioner;
using mesos::master::detector::MasterDetector;
using mesos::slave::ContainerTermination;
using slave::ImageInfo;
using slave::Slave;
using slave::docker::Puller;
using slave::docker::RegistryPuller;
using slave::docker::Store;
using testing::WithParamInterface;
namespace mesos {
namespace internal {
namespace tests {
class ProvisionerDockerLocalStoreTest : public TemporaryDirectoryTest
{
public:
void verifyLocalDockerImage(
const slave::Flags& flags,
const vector<string>& layers)
{
// Verify contents of the image in store directory.
const string layerPath1 = paths::getImageLayerRootfsPath(
flags.docker_store_dir,
"123",
COPY_BACKEND);
const string layerPath2 = paths::getImageLayerRootfsPath(
flags.docker_store_dir,
"456",
COPY_BACKEND);
EXPECT_TRUE(os::exists(layerPath1));
EXPECT_TRUE(os::exists(layerPath2));
EXPECT_SOME_EQ(
"foo 123",
os::read(path::join(layerPath1, "temp")));
EXPECT_SOME_EQ(
"bar 456",
os::read(path::join(layerPath2, "temp")));
// Verify the Docker Image provided.
vector<string> expectedLayers;
expectedLayers.push_back(layerPath1);
expectedLayers.push_back(layerPath2);
EXPECT_EQ(expectedLayers, layers);
}
protected:
void SetUp() override
{
TemporaryDirectoryTest::SetUp();
const string archivesDir = path::join(os::getcwd(), "images");
const string image = path::join(archivesDir, "abc");
ASSERT_SOME(os::mkdir(archivesDir));
ASSERT_SOME(os::mkdir(image));
JSON::Value repositories = JSON::parse(
"{"
" \"abc\": {"
" \"latest\": \"456\""
" }"
"}").get();
ASSERT_SOME(
os::write(path::join(image, "repositories"), stringify(repositories)));
ASSERT_SOME(os::mkdir(path::join(image, "123")));
JSON::Value manifest123 = JSON::parse(
"{"
" \"parent\": \"\""
"}").get();
ASSERT_SOME(os::write(
path::join(image, "123", "json"), stringify(manifest123)));
ASSERT_SOME(os::mkdir(path::join(image, "123", "layer")));
ASSERT_SOME(
os::write(path::join(image, "123", "layer", "temp"), "foo 123"));
// Must change directory to avoid carrying over /path/to/archive during tar.
const string cwd = os::getcwd();
ASSERT_SOME(os::chdir(path::join(image, "123", "layer")));
ASSERT_SOME(os::tar(".", "../layer.tar"));
ASSERT_SOME(os::chdir(cwd));
ASSERT_SOME(os::rmdir(path::join(image, "123", "layer")));
ASSERT_SOME(os::mkdir(path::join(image, "456")));
JSON::Value manifest456 = JSON::parse(
"{"
" \"parent\": \"123\""
"}").get();
ASSERT_SOME(
os::write(path::join(image, "456", "json"), stringify(manifest456)));
ASSERT_SOME(os::mkdir(path::join(image, "456", "layer")));
ASSERT_SOME(
os::write(path::join(image, "456", "layer", "temp"), "bar 456"));
ASSERT_SOME(os::chdir(path::join(image, "456", "layer")));
ASSERT_SOME(os::tar(".", "../layer.tar"));
ASSERT_SOME(os::chdir(cwd));
ASSERT_SOME(os::rmdir(path::join(image, "456", "layer")));
ASSERT_SOME(os::chdir(image));
ASSERT_SOME(os::tar(".", "../abc.tar"));
ASSERT_SOME(os::chdir(cwd));
ASSERT_SOME(os::rmdir(image));
}
};
// This test verifies that a locally stored Docker image in the form of a
// tar archive created from a 'docker save' command can be unpacked and
// stored in the proper locations accessible to the Docker provisioner.
TEST_F(ProvisionerDockerLocalStoreTest, LocalStoreTestWithTar)
{
slave::Flags flags;
flags.docker_registry = path::join(os::getcwd(), "images");
flags.docker_store_dir = path::join(os::getcwd(), "store");
flags.image_provisioner_backend = COPY_BACKEND;
Try<Owned<slave::Store>> store = slave::docker::Store::create(flags);
ASSERT_SOME(store);
Image mesosImage;
mesosImage.set_type(Image::DOCKER);
mesosImage.mutable_docker()->set_name("abc");
Future<slave::ImageInfo> imageInfo =
store.get()->get(mesosImage, COPY_BACKEND);
AWAIT_READY(imageInfo);
verifyLocalDockerImage(flags, imageInfo->layers);
}
// This tests the ability of the metadata manger to recover the images it has
// already stored on disk when it is initialized.
TEST_F(ProvisionerDockerLocalStoreTest, MetadataManagerInitialization)
{
slave::Flags flags;
flags.docker_registry = path::join(os::getcwd(), "images");
flags.docker_store_dir = path::join(os::getcwd(), "store");
flags.image_provisioner_backend = COPY_BACKEND;
Try<Owned<slave::Store>> store = slave::docker::Store::create(flags);
ASSERT_SOME(store);
Image image;
image.set_type(Image::DOCKER);
image.mutable_docker()->set_name("abc");
Future<slave::ImageInfo> imageInfo = store.get()->get(image, COPY_BACKEND);
AWAIT_READY(imageInfo);
// Store is deleted and recreated. Metadata Manager is initialized upon
// creation of the store.
store->reset();
store = slave::docker::Store::create(flags);
ASSERT_SOME(store);
Future<Nothing> recover = store.get()->recover();
AWAIT_READY(recover);
imageInfo = store.get()->get(image, COPY_BACKEND);
AWAIT_READY(imageInfo);
verifyLocalDockerImage(flags, imageInfo->layers);
}
// This is a regression test for MESOS-8871.
// This test the ability of the metadata manger to ignore the empty images
// file when it recover images.
TEST_F(ProvisionerDockerLocalStoreTest,
MetadataManagerRecoveryWithEmptyImagesFile)
{
slave::Flags flags;
flags.docker_registry = path::join(os::getcwd(), "images");
flags.docker_store_dir = path::join(os::getcwd(), "store");
flags.image_provisioner_backend = COPY_BACKEND;
const string emptyImages = paths::getStoredImagesPath(flags.docker_store_dir);
ASSERT_SOME(os::mkdir(flags.docker_store_dir));
ASSERT_SOME(os::touch(emptyImages));
Try<Owned<slave::Store>> store = slave::docker::Store::create(flags);
ASSERT_SOME(store);
Future<Nothing> recover = store.get()->recover();
AWAIT_READY(recover);
}
// This test verifies that the layer that is missing from the store
// will be pulled.
TEST_F(ProvisionerDockerLocalStoreTest, MissingLayer)
{
slave::Flags flags;
flags.docker_registry = path::join(os::getcwd(), "images");
flags.docker_store_dir = path::join(os::getcwd(), "store");
flags.image_provisioner_backend = COPY_BACKEND;
Try<Owned<slave::Store>> store = Store::create(flags);
ASSERT_SOME(store);
Image image;
image.set_type(Image::DOCKER);
image.mutable_docker()->set_name("abc");
Future<slave::ImageInfo> imageInfo = store.get()->get(
image, flags.image_provisioner_backend.get());
AWAIT_READY(imageInfo);
verifyLocalDockerImage(flags, imageInfo->layers);
// Remove one of the layers from the store.
ASSERT_SOME(os::rmdir(paths::getImageLayerRootfsPath(
flags.docker_store_dir, "456", flags.image_provisioner_backend.get())));
// Pull the image again to get the missing layer.
imageInfo = store.get()->get(image, flags.image_provisioner_backend.get());
AWAIT_READY(imageInfo);
verifyLocalDockerImage(flags, imageInfo->layers);
}
class MockPuller : public Puller
{
public:
MockPuller()
{
EXPECT_CALL(*this, pull(_, _, _, _))
.WillRepeatedly(Invoke(this, &MockPuller::unmocked_pull));
}
~MockPuller() override {}
MOCK_METHOD4(
pull,
Future<slave::docker::Image>(
const spec::ImageReference&,
const string&,
const string&,
const Option<Secret>&));
Future<slave::docker::Image> unmocked_pull(
const spec::ImageReference& reference,
const string& directory,
const string& backend,
const Option<Secret>& config)
{
// TODO(gilbert): Allow return Image to be overridden.
return slave::docker::Image();
}
};
// This tests the store to pull the same image simultaneously.
// This test verifies that the store only calls the puller once
// when multiple requests for the same image is in flight.
// In addition, this test verifies that if some of the futures
// returned by `Store::get()` that are pending image pull are discarded,
// the pull still completes.
TEST_F(ProvisionerDockerLocalStoreTest, PullingSameImageSimultaneously)
{
slave::Flags flags;
flags.docker_registry = path::join(os::getcwd(), "images");
flags.docker_store_dir = path::join(os::getcwd(), "store");
MockPuller* puller = new MockPuller();
Future<Nothing> pull;
Future<string> directory;
Promise<slave::docker::Image> promise;
EXPECT_CALL(*puller, pull(_, _, _, _))
.WillOnce(testing::DoAll(FutureSatisfy(&pull),
FutureArg<1>(&directory),
Return(promise.future())));
Try<Owned<slave::Store>> store =
slave::docker::Store::create(flags, Owned<Puller>(puller));
ASSERT_SOME(store);
Image mesosImage;
mesosImage.set_type(Image::DOCKER);
mesosImage.mutable_docker()->set_name("abc");
Future<slave::ImageInfo> imageInfo1 =
store.get()->get(mesosImage, COPY_BACKEND);
AWAIT_READY(pull);
AWAIT_READY(directory);
// TODO(gilbert): Need a helper method to create test layers
// which will allow us to set manifest so that we can add
// checks here.
const string layerPath = path::join(directory.get(), "456");
Try<Nothing> mkdir = os::mkdir(layerPath);
ASSERT_SOME(mkdir);
JSON::Value manifest = JSON::parse(
"{"
" \"parent\": \"\""
"}").get();
ASSERT_SOME(
os::write(path::join(layerPath, "json"), stringify(manifest)));
ASSERT_TRUE(imageInfo1.isPending());
Future<slave::ImageInfo> imageInfo2 =
store.get()->get(mesosImage, COPY_BACKEND);
Future<slave::ImageInfo> imageInfo3 =
store.get()->get(mesosImage, COPY_BACKEND);
Try<spec::ImageReference> reference =
spec::parseImageReference(mesosImage.docker().name());
ASSERT_SOME(reference);
slave::docker::Image result;
result.mutable_reference()->CopyFrom(reference.get());
result.add_layer_ids("456");
ASSERT_TRUE(imageInfo2.isPending());
ASSERT_TRUE(imageInfo3.isPending());
// Pull should still complete, despite of the discard of a single future.
imageInfo3.discard();
promise.set(result);
AWAIT_READY(imageInfo1);
AWAIT_READY(imageInfo2);
EXPECT_EQ(imageInfo1->layers, imageInfo2->layers);
}
// This tests that pulling the image will be cancelled if all the pending
// futures returned by `Store::get()` for this pull are discarded.
TEST_F(ProvisionerDockerLocalStoreTest, PullDiscarded)
{
slave::Flags flags;
flags.docker_registry = path::join(os::getcwd(), "images");
flags.docker_store_dir = path::join(os::getcwd(), "store");
MockPuller* puller = new MockPuller();
Future<Nothing> pullCalled;
Promise<slave::docker::Image> promise;
EXPECT_CALL(*puller, pull(_, _, _, _))
.WillOnce(testing::DoAll(FutureSatisfy(&pullCalled),
Return(promise.future())));
Try<Owned<slave::Store>> store =
slave::docker::Store::create(flags, Owned<Puller>(puller));
ASSERT_SOME(store);
Image mesosImage;
mesosImage.set_type(Image::DOCKER);
mesosImage.mutable_docker()->set_name("abc");
Future<slave::ImageInfo> imageInfo1 =
store.get()->get(mesosImage, COPY_BACKEND);
Future<slave::ImageInfo> imageInfo2 =
store.get()->get(mesosImage, COPY_BACKEND);
AWAIT_READY(pullCalled);
imageInfo1.discard();
imageInfo2.discard();
Clock::pause();
Clock::settle();
ASSERT_TRUE(promise.future().hasDiscard());
}
#ifdef __linux__
class ProvisionerDockerTest
: public MesosTest,
public WithParamInterface<string> {};
// This test verifies that local docker image can be pulled and
// provisioned correctly, and shell command should be executed.
TEST_F(ProvisionerDockerTest, ROOT_ImageTarPullerSimpleCommand)
{
Try<Owned<cluster::Master>> master = StartMaster();
ASSERT_SOME(master);
const string directory = path::join(os::getcwd(), "archives");
Future<Nothing> testImage = DockerArchive::create(directory, "alpine");
AWAIT_READY(testImage);
ASSERT_TRUE(os::exists(path::join(directory, "alpine.tar")));
slave::Flags flags = CreateSlaveFlags();
flags.isolation = "docker/runtime,filesystem/linux";
flags.image_providers = "docker";
flags.docker_registry = directory;
flags.docker_store_dir = path::join(os::getcwd(), "store");
Owned<MasterDetector> detector = master.get()->createDetector();
Try<Owned<cluster::Slave>> slave = StartSlave(detector.get(), flags);
ASSERT_SOME(slave);
MockScheduler sched;
MesosSchedulerDriver driver(
&sched, DEFAULT_FRAMEWORK_INFO, master.get()->pid, DEFAULT_CREDENTIAL);
EXPECT_CALL(sched, registered(&driver, _, _));
Future<vector<Offer>> offers;
EXPECT_CALL(sched, resourceOffers(&driver, _))
.WillOnce(FutureArg<1>(&offers))
.WillRepeatedly(Return()); // Ignore subsequent offers.
driver.start();
AWAIT_READY(offers);
ASSERT_EQ(1u, offers->size());
const Offer& offer = offers.get()[0];
TaskInfo task = createTask(
offer.slave_id(),
Resources::parse("cpus:1;mem:128").get(),
"ls -al /");
Image image;
image.set_type(Image::DOCKER);
image.mutable_docker()->set_name("alpine");
ContainerInfo* container = task.mutable_container();
container->set_type(ContainerInfo::MESOS);
container->mutable_mesos()->mutable_image()->CopyFrom(image);
Future<TaskStatus> statusStarting;
Future<TaskStatus> statusRunning;
Future<TaskStatus> statusFinished;
EXPECT_CALL(sched, statusUpdate(&driver, _))
.WillOnce(FutureArg<1>(&statusStarting))
.WillOnce(FutureArg<1>(&statusRunning))
.WillOnce(FutureArg<1>(&statusFinished));
driver.launchTasks(offer.id(), {task});
AWAIT_READY_FOR(statusStarting, Seconds(60));
EXPECT_EQ(task.task_id(), statusStarting->task_id());
EXPECT_EQ(TASK_STARTING, statusStarting->state());
AWAIT_READY_FOR(statusRunning, Seconds(60));
EXPECT_EQ(task.task_id(), statusRunning->task_id());
EXPECT_EQ(TASK_RUNNING, statusRunning->state());
AWAIT_READY(statusFinished);
EXPECT_EQ(task.task_id(), statusFinished->task_id());
EXPECT_EQ(TASK_FINISHED, statusFinished->state());
driver.stop();
driver.join();
}
class ProvisionerDockerHdfsTest
: public MesosTest,
public WithParamInterface<string> {};
// The host of HDFS can be a remote host or local directory.
INSTANTIATE_TEST_CASE_P(
HdfsHost,
ProvisionerDockerHdfsTest,
::testing::ValuesIn(vector<string>({
"hdfs://localhost:8020",
"hdfs://"})));
// This test verifies that the image tar puller could pull image
// with the hdfs uri fetcher plugin.
TEST_P(ProvisionerDockerHdfsTest, ROOT_ImageTarPullerHdfsFetcherSimpleCommand)
{
string hadoopPath = os::getcwd();
ASSERT_TRUE(os::exists(hadoopPath));
string hadoopBinPath = path::join(hadoopPath, "bin");
ASSERT_SOME(os::mkdir(hadoopBinPath));
ASSERT_SOME(os::chmod(hadoopBinPath, S_IRWXU | S_IRWXG | S_IRWXO));
const string& proof = path::join(hadoopPath, "proof");
// This acts exactly as "hadoop" for testing purposes. On some platforms, the
// "hadoop" wrapper command will emit a warning that Hadoop installation has
// no native code support. We always emit that here to make sure it is parsed
// correctly.
string mockHadoopScript =
"#!/usr/bin/env bash\n"
"\n"
"touch " + proof + "\n"
"\n"
"now=$(date '+%y/%m/%d %I:%M:%S')\n"
"echo \"$now WARN util.NativeCodeLoader: "
"Unable to load native-hadoop library for your platform...\" 1>&2\n"
"\n"
"if [[ 'version' == $1 ]]; then\n"
" echo $0 'for Mesos testing'\n"
"fi\n"
"\n"
"# hadoop fs -copyToLocal $3 $4\n"
"if [[ 'fs' == $1 && '-copyToLocal' == $2 ]]; then\n"
" if [[ $3 == 'hdfs://'* ]]; then\n"
" # Remove 'hdfs://<host>/' and use just the (absolute) path.\n"
" withoutProtocol=${3/'hdfs:'\\/\\//}\n"
" withoutHost=${withoutProtocol#*\\/}\n"
" absolutePath='/'$withoutHost\n"
" cp $absolutePath $4\n"
" else\n"
" cp $3 $4\n"
" fi\n"
"fi\n";
string hadoopCommand = path::join(hadoopBinPath, "hadoop");
ASSERT_SOME(os::write(hadoopCommand, mockHadoopScript));
ASSERT_SOME(os::chmod(hadoopCommand,
S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH));
Try<Owned<cluster::Master>> master = StartMaster();
ASSERT_SOME(master);
const string directory = path::join(os::getcwd(), "archives");
Future<Nothing> testImage = DockerArchive::create(directory, "alpine");
AWAIT_READY(testImage);
ASSERT_TRUE(os::exists(path::join(directory, "alpine.tar")));
slave::Flags flags = CreateSlaveFlags();
flags.isolation = "docker/runtime,filesystem/linux";
flags.image_providers = "docker";
flags.docker_registry = GetParam() + directory;
flags.docker_store_dir = path::join(os::getcwd(), "store");
flags.hadoop_home = hadoopPath;
Owned<MasterDetector> detector = master.get()->createDetector();
Try<Owned<cluster::Slave>> slave = StartSlave(detector.get(), flags);
ASSERT_SOME(slave);
MockScheduler sched;
MesosSchedulerDriver driver(
&sched, DEFAULT_FRAMEWORK_INFO, master.get()->pid, DEFAULT_CREDENTIAL);
EXPECT_CALL(sched, registered(&driver, _, _));
Future<vector<Offer>> offers;
EXPECT_CALL(sched, resourceOffers(&driver, _))
.WillOnce(FutureArg<1>(&offers))
.WillRepeatedly(Return()); // Ignore subsequent offers.
driver.start();
AWAIT_READY(offers);
ASSERT_EQ(1u, offers->size());
const Offer& offer = offers.get()[0];
TaskInfo task = createTask(
offer.slave_id(),
Resources::parse("cpus:1;mem:128").get(),
"ls -al /");
Image image;
image.set_type(Image::DOCKER);
image.mutable_docker()->set_name("alpine");
ContainerInfo* container = task.mutable_container();
container->set_type(ContainerInfo::MESOS);
container->mutable_mesos()->mutable_image()->CopyFrom(image);
Future<TaskStatus> statusStarting;
Future<TaskStatus> statusRunning;
Future<TaskStatus> statusFinished;
EXPECT_CALL(sched, statusUpdate(&driver, _))
.WillOnce(FutureArg<1>(&statusStarting))
.WillOnce(FutureArg<1>(&statusRunning))
.WillOnce(FutureArg<1>(&statusFinished));
driver.launchTasks(offer.id(), {task});
AWAIT_READY_FOR(statusStarting, Seconds(60));
EXPECT_EQ(task.task_id(), statusStarting->task_id());
EXPECT_EQ(TASK_STARTING, statusStarting->state());
AWAIT_READY_FOR(statusRunning, Seconds(60));
EXPECT_EQ(task.task_id(), statusRunning->task_id());
EXPECT_EQ(TASK_RUNNING, statusRunning->state());
AWAIT_READY(statusFinished);
EXPECT_EQ(task.task_id(), statusFinished->task_id());
EXPECT_EQ(TASK_FINISHED, statusFinished->state());
driver.stop();
driver.join();
}
// For official Docker images, users can omit the 'library/' prefix
// when specifying the repository name (e.g., 'busybox'). The registry
// puller normalize docker official images if necessary.
INSTANTIATE_TEST_CASE_P(
ContainerImage,
ProvisionerDockerTest,
::testing::ValuesIn(vector<string>({
"alpine", // Verifies the normalization of the Docker repository name.
"library/alpine",
"gcr.io/google-containers/busybox:1.24", // manifest.v1+prettyjws
"gcr.io/google-containers/busybox:1.27", // manifest.v2+json
// TODO(alexr): The registry below is unreliable and hence disabled.
// Consider re-enabling shall it become more stable.
// "registry.cn-hangzhou.aliyuncs.com/acs-sample/alpine",
"quay.io/coreos/alpine-sh" // manifest.v1+prettyjws
})));
// TODO(jieyu): This is a ROOT test because of MESOS-4757. Remove the
// ROOT restriction after MESOS-4757 is resolved.
TEST_P(ProvisionerDockerTest, ROOT_INTERNET_CURL_SimpleCommand)
{
Try<Owned<cluster::Master>> master = StartMaster();
ASSERT_SOME(master);
slave::Flags flags = CreateSlaveFlags();
flags.isolation = "docker/runtime,filesystem/linux";
flags.image_providers = "docker";
// Image pulling time may be long, depending on the location of
// the registry server.
flags.executor_registration_timeout = Minutes(10);
Owned<MasterDetector> detector = master.get()->createDetector();
Try<Owned<cluster::Slave>> slave = StartSlave(detector.get(), flags);
ASSERT_SOME(slave);
MockScheduler sched;
MesosSchedulerDriver driver(
&sched, DEFAULT_FRAMEWORK_INFO, master.get()->pid, DEFAULT_CREDENTIAL);
EXPECT_CALL(sched, registered(&driver, _, _));
Future<vector<Offer>> offers;
EXPECT_CALL(sched, resourceOffers(&driver, _))
.WillOnce(FutureArg<1>(&offers))
.WillRepeatedly(Return()); // Ignore subsequent offers.
driver.start();
AWAIT_READY(offers);
ASSERT_EQ(1u, offers->size());
const Offer& offer = offers.get()[0];
// NOTE: We use a non-shell command here because 'sh' might not be
// in the PATH. 'alpine' does not specify env PATH in the image. On
// some linux distribution, '/bin' is not in the PATH by default.
CommandInfo command;
command.set_shell(false);
command.set_value("/bin/ls");
command.add_arguments("ls");
command.add_arguments("-al");
command.add_arguments("/");
TaskInfo task = createTask(
offer.slave_id(),
Resources::parse("cpus:1;mem:128").get(),
command);
Image image;
image.set_type(Image::DOCKER);
image.mutable_docker()->set_name(GetParam());
ContainerInfo* container = task.mutable_container();
container->set_type(ContainerInfo::MESOS);
container->mutable_mesos()->mutable_image()->CopyFrom(image);
Future<TaskStatus> statusStarting;
Future<TaskStatus> statusRunning;
Future<TaskStatus> statusFinished;
EXPECT_CALL(sched, statusUpdate(&driver, _))
.WillOnce(FutureArg<1>(&statusStarting))
.WillOnce(FutureArg<1>(&statusRunning))
.WillOnce(FutureArg<1>(&statusFinished));
driver.launchTasks(offer.id(), {task});
AWAIT_READY_FOR(statusStarting, Minutes(10));
EXPECT_EQ(task.task_id(), statusStarting->task_id());
EXPECT_EQ(TASK_STARTING, statusStarting->state());
AWAIT_READY(statusRunning);
EXPECT_EQ(task.task_id(), statusRunning->task_id());
EXPECT_EQ(TASK_RUNNING, statusRunning->state());
AWAIT_READY(statusFinished);
EXPECT_EQ(task.task_id(), statusFinished->task_id());
EXPECT_EQ(TASK_FINISHED, statusFinished->state());
driver.stop();
driver.join();
}
// This test verifies the functionality of image `cached` option
// for image force pulling.
TEST_F(ProvisionerDockerTest, ROOT_INTERNET_CURL_ImageForcePulling)
{
Try<Owned<cluster::Master>> master = StartMaster();
ASSERT_SOME(master);
slave::Flags flags = CreateSlaveFlags();
flags.isolation = "docker/runtime,filesystem/linux";
flags.image_providers = "docker";
// Image pulling time may be long, depending on the location of
// the registry server.
flags.executor_registration_timeout = Minutes(10);
Owned<MasterDetector> detector = master.get()->createDetector();
Try<Owned<cluster::Slave>> slave = StartSlave(detector.get(), flags);
ASSERT_SOME(slave);
MockScheduler sched;
MesosSchedulerDriver driver(
&sched, DEFAULT_FRAMEWORK_INFO, master.get()->pid, DEFAULT_CREDENTIAL);
EXPECT_CALL(sched, registered(&driver, _, _));
Future<vector<Offer>> offers1, offers2;
EXPECT_CALL(sched, resourceOffers(&driver, _))
.WillOnce(FutureArg<1>(&offers1))
.WillOnce(FutureArg<1>(&offers2))
.WillRepeatedly(Return()); // Ignore subsequent offers.
driver.start();
AWAIT_READY(offers1);
ASSERT_EQ(1u, offers1->size());
const Offer& offer1 = offers1.get()[0];
// NOTE: We use a non-shell command here because 'sh' might not be
// in the PATH. 'alpine' does not specify env PATH in the image. On
// some linux distribution, '/bin' is not in the PATH by default.
CommandInfo command;
command.set_shell(false);
command.set_value("/bin/ls");
command.add_arguments("ls");
command.add_arguments("-al");
command.add_arguments("/");
TaskInfo task = createTask(
offer1.slave_id(),
Resources::parse("cpus:1;mem:128").get(),
command);
Image image;
image.set_type(Image::DOCKER);
image.mutable_docker()->set_name("alpine");
ContainerInfo* container = task.mutable_container();
container->set_type(ContainerInfo::MESOS);
container->mutable_mesos()->mutable_image()->CopyFrom(image);
Future<TaskStatus> statusStarting1;
Future<TaskStatus> statusRunning1;
Future<TaskStatus> statusFinished1;
EXPECT_CALL(sched, statusUpdate(&driver, _))
.WillOnce(FutureArg<1>(&statusStarting1))
.WillOnce(FutureArg<1>(&statusRunning1))
.WillOnce(FutureArg<1>(&statusFinished1));
driver.launchTasks(offer1.id(), {task});
AWAIT_READY_FOR(statusStarting1, Minutes(10));
EXPECT_EQ(task.task_id(), statusStarting1->task_id());
EXPECT_EQ(TASK_STARTING, statusStarting1->state());
AWAIT_READY(statusRunning1);
EXPECT_EQ(task.task_id(), statusRunning1->task_id());
EXPECT_EQ(TASK_RUNNING, statusRunning1->state());
AWAIT_READY(statusFinished1);
EXPECT_EQ(task.task_id(), statusFinished1->task_id());
EXPECT_EQ(TASK_FINISHED, statusFinished1->state());
AWAIT_READY(offers2);
ASSERT_EQ(1u, offers2->size());
const Offer& offer2 = offers2.get()[0];
task = createTask(
offer2.slave_id(),
Resources::parse("cpus:1;mem:128").get(),
command);
// Image force pulling.
image.set_cached(false);
container = task.mutable_container();
container->set_type(ContainerInfo::MESOS);
container->mutable_mesos()->mutable_image()->CopyFrom(image);
Future<TaskStatus> statusStarting2;
Future<TaskStatus> statusRunning2;
Future<TaskStatus> statusFinished2;
EXPECT_CALL(sched, statusUpdate(&driver, _))
.WillOnce(FutureArg<1>(&statusStarting2))
.WillOnce(FutureArg<1>(&statusRunning2))
.WillOnce(FutureArg<1>(&statusFinished2));
driver.launchTasks(offer2.id(), {task});
AWAIT_READY_FOR(statusStarting2, Minutes(10));
EXPECT_EQ(task.task_id(), statusStarting2->task_id());
EXPECT_EQ(TASK_STARTING, statusStarting2->state());
AWAIT_READY(statusRunning2);
EXPECT_EQ(task.task_id(), statusRunning2->task_id());
EXPECT_EQ(TASK_RUNNING, statusRunning2->state());
AWAIT_READY(statusFinished2);
EXPECT_EQ(task.task_id(), statusFinished2->task_id());
EXPECT_EQ(TASK_FINISHED, statusFinished2->state());
driver.stop();
driver.join();
}
// This test verifies that the scratch based docker image (that
// only contain a single binary and its dependencies) can be
// launched correctly.
TEST_F(ProvisionerDockerTest, ROOT_INTERNET_CURL_ScratchImage)
{
Try<Owned<cluster::Master>> master = StartMaster();
ASSERT_SOME(master);
slave::Flags flags = CreateSlaveFlags();
flags.isolation = "docker/runtime,filesystem/linux";
flags.image_providers = "docker";
Owned<MasterDetector> detector = master.get()->createDetector();
Try<Owned<cluster::Slave>> slave = StartSlave(detector.get(), flags);
ASSERT_SOME(slave);
MockScheduler sched;
MesosSchedulerDriver driver(
&sched, DEFAULT_FRAMEWORK_INFO, master.get()->pid, DEFAULT_CREDENTIAL);
EXPECT_CALL(sched, registered(&driver, _, _));
Future<vector<Offer>> offers;
EXPECT_CALL(sched, resourceOffers(&driver, _))
.WillOnce(FutureArg<1>(&offers))
.WillRepeatedly(Return()); // Ignore subsequent offers.
driver.start();
AWAIT_READY(offers);
ASSERT_EQ(1u, offers->size());
const Offer& offer = offers.get()[0];
CommandInfo command;
command.set_shell(false);
TaskInfo task = createTask(
offer.slave_id(),
Resources::parse("cpus:1;mem:128").get(),
command);
Image image;
image.set_type(Image::DOCKER);
// 'hello-world' is a scratch image. It contains only one
// binary 'hello' in its rootfs.
image.mutable_docker()->set_name("hello-world");
ContainerInfo* container = task.mutable_container();
container->set_type(ContainerInfo::MESOS);
container->mutable_mesos()->mutable_image()->CopyFrom(image);
Future<TaskStatus> statusStarting;
Future<TaskStatus> statusRunning;
Future<TaskStatus> statusFinished;
EXPECT_CALL(sched, statusUpdate(&driver, _))
.WillOnce(FutureArg<1>(&statusStarting))
.WillOnce(FutureArg<1>(&statusRunning))
.WillOnce(FutureArg<1>(&statusFinished));
driver.launchTasks(offer.id(), {task});
AWAIT_READY_FOR(statusStarting, Seconds(60));
EXPECT_EQ(task.task_id(), statusStarting->task_id());
EXPECT_EQ(TASK_STARTING, statusStarting->state());
AWAIT_READY_FOR(statusRunning, Seconds(60));
EXPECT_EQ(task.task_id(), statusRunning->task_id());
EXPECT_EQ(TASK_RUNNING, statusRunning->state());
AWAIT_READY(statusFinished);
EXPECT_EQ(task.task_id(), statusFinished->task_id());
EXPECT_EQ(TASK_FINISHED, statusFinished->state());
driver.stop();
driver.join();
}
class ProvisionerDockerBackendTest
: public MesosTest,
public WithParamInterface<string>
{
public:
// Returns the supported backends.
static vector<string> parameters()
{
vector<string> backends = {COPY_BACKEND};
Try<bool> aufsSupported = fs::supported("aufs");
if (aufsSupported.isSome() && aufsSupported.get()) {
backends.push_back(AUFS_BACKEND);
}
Try<bool> overlayfsSupported = fs::supported("overlayfs");
if (overlayfsSupported.isSome() && overlayfsSupported.get()) {
backends.push_back(OVERLAY_BACKEND);
}
return backends;
}
};
INSTANTIATE_TEST_CASE_P(
BackendFlag,
ProvisionerDockerBackendTest,
::testing::ValuesIn(ProvisionerDockerBackendTest::parameters()));
// This test verifies that a docker image containing whiteout files
// will be processed correctly by copy, aufs and overlay backends.
TEST_P(ProvisionerDockerBackendTest, ROOT_INTERNET_CURL_DTYPE_Whiteout)
{
Try<Owned<cluster::Master>> master = StartMaster();
ASSERT_SOME(master);
slave::Flags flags = CreateSlaveFlags();
flags.isolation = "docker/runtime,filesystem/linux";
flags.image_providers = "docker";
flags.image_provisioner_backend = GetParam();
Owned<MasterDetector> detector = master.get()->createDetector();
Try<Owned<cluster::Slave>> slave = StartSlave(detector.get(), flags);
ASSERT_SOME(slave);
MockScheduler sched;
MesosSchedulerDriver driver(
&sched, DEFAULT_FRAMEWORK_INFO, master.get()->pid, DEFAULT_CREDENTIAL);
EXPECT_CALL(sched, registered(&driver, _, _));
Future<vector<Offer>> offers;
EXPECT_CALL(sched, resourceOffers(&driver, _))
.WillOnce(FutureArg<1>(&offers))
.WillRepeatedly(Return()); // Ignore subsequent offers.
driver.start();
AWAIT_READY(offers);
ASSERT_EQ(1u, offers->size());
const Offer& offer = offers.get()[0];
// We are using the docker image 'zhq527725/whiteout' to verify that the
// files '/dir1/file1' and '/dir1/dir2/file2' do not exist in container's
// rootfs because of the following two whiteout files in the image:
// '/dir1/.wh.file1'
// '/dir1/dir2/.wh..wh..opq'
// And we also verify that the file '/dir1/dir2/file3' exists in container's
// rootfs which will NOT be applied by '/dir1/dir2/.wh..wh..opq' since they
// are in the same layer.
// See more details about this docker image in the link below:
// https://hub.docker.com/r/zhq527725/whiteout/
CommandInfo command = createCommandInfo(
"test ! -f /dir1/file1 && "
"test ! -f /dir1/dir2/file2 && "
"test -f /dir1/dir2/file3");
TaskInfo task = createTask(
offer.slave_id(),
Resources::parse("cpus:1;mem:128").get(),
command);
Image image = createDockerImage("zhq527725/whiteout");
ContainerInfo* container = task.mutable_container();
container->set_type(ContainerInfo::MESOS);
container->mutable_mesos()->mutable_image()->CopyFrom(image);
Future<TaskStatus> statusStarting;
Future<TaskStatus> statusRunning;
Future<TaskStatus> statusFinished;
EXPECT_CALL(sched, statusUpdate(&driver, _))
.WillOnce(FutureArg<1>(&statusStarting))
.WillOnce(FutureArg<1>(&statusRunning))
.WillOnce(FutureArg<1>(&statusFinished));
driver.launchTasks(offer.id(), {task});
AWAIT_READY_FOR(statusStarting, Seconds(60));
EXPECT_EQ(task.task_id(), statusStarting->task_id());
EXPECT_EQ(TASK_STARTING, statusStarting->state());
AWAIT_READY(statusRunning);
EXPECT_EQ(task.task_id(), statusRunning->task_id());
EXPECT_EQ(TASK_RUNNING, statusRunning->state());
AWAIT_READY(statusFinished);
EXPECT_EQ(task.task_id(), statusFinished->task_id());
EXPECT_EQ(TASK_FINISHED, statusFinished->state());
driver.stop();
driver.join();
}
// This test verifies that the provisioner correctly overwrites a
// directory in underlying layers with a with a regular file or symbolic
// link of the same name in an upper layer, and vice versa.
TEST_P(ProvisionerDockerBackendTest, ROOT_INTERNET_CURL_DTYPE_Overwrite)
{
Try<Owned<cluster::Master>> master = StartMaster();
ASSERT_SOME(master);
slave::Flags flags = CreateSlaveFlags();
flags.isolation = "docker/runtime,filesystem/linux";
flags.image_providers = "docker";
flags.image_provisioner_backend = GetParam();
Owned<MasterDetector> detector = master.get()->createDetector();
Try<Owned<cluster::Slave>> slave = StartSlave(detector.get(), flags);
ASSERT_SOME(slave);
MockScheduler sched;
MesosSchedulerDriver driver(
&sched, DEFAULT_FRAMEWORK_INFO, master.get()->pid, DEFAULT_CREDENTIAL);
EXPECT_CALL(sched, registered(&driver, _, _));
Future<vector<Offer>> offers;
EXPECT_CALL(sched, resourceOffers(&driver, _))
.WillOnce(FutureArg<1>(&offers))
.WillRepeatedly(Return()); // Ignore subsequent offers.
driver.start();
AWAIT_READY(offers);
ASSERT_EQ(1u, offers->size());
const Offer& offer = offers.get()[0];
// We are using the docker image 'chhsiao/overwrite' to verify that:
// 1. The '/merged' directory is merged.
// 2. All '/replaced*' files/directories are correctly overwritten.
// 3. The '/foo', '/bar' and '/baz' files are correctly overwritten.
// See more details in the following link:
// https://hub.docker.com/r/chhsiao/overwrite/
CommandInfo command = createCommandInfo(
"test -f /replaced1 &&"
"test -L /replaced2 &&"
"test -f /replaced2/m1 &&"
"test -f /replaced2/m2 &&"
"! test -e /replaced2/r2 &&"
"test -d /replaced3 &&"
"test -d /replaced4 &&"
"! test -e /replaced4/m1 &&"
"test -f /foo &&"
"! test -L /bar &&"
"test -L /baz");
TaskInfo task = createTask(
offer.slave_id(),
Resources::parse("cpus:1;mem:128").get(),
command);
Image image = createDockerImage("chhsiao/overwrite");
ContainerInfo* container = task.mutable_container();
container->set_type(ContainerInfo::MESOS);
container->mutable_mesos()->mutable_image()->CopyFrom(image);
Future<TaskStatus> statusStarting;
Future<TaskStatus> statusRunning;
Future<TaskStatus> statusFinished;
EXPECT_CALL(sched, statusUpdate(&driver, _))
.WillOnce(FutureArg<1>(&statusStarting))
.WillOnce(FutureArg<1>(&statusRunning))
.WillOnce(FutureArg<1>(&statusFinished));
// Create a non-empty file 'abc' in the agent's work directory on the
// host filesystem. This file is symbolically linked by
// '/xyz -> ../../../../../../../abc'
// in the 2nd layer of the testing image during provisioning.
// For more details about the provisioner directory please see:
// https://github.com/apache/mesos/blob/master/src/slave/containerizer/mesos/provisioner/paths.hpp#L34-L48 // NOLINT
const string hostFile = path::join(flags.work_dir, "abc");
ASSERT_SOME(os::write(hostFile, "abc"));
ASSERT_SOME(os::shell("test -s " + hostFile));
driver.launchTasks(offer.id(), {task});
AWAIT_READY_FOR(statusStarting, Seconds(60));
EXPECT_EQ(task.task_id(), statusStarting->task_id());
EXPECT_EQ(TASK_STARTING, statusStarting->state());
AWAIT_READY_FOR(statusRunning, Seconds(60));
EXPECT_EQ(task.task_id(), statusRunning->task_id());
EXPECT_EQ(TASK_RUNNING, statusRunning->state());
AWAIT_READY(statusFinished);
EXPECT_EQ(task.task_id(), statusFinished->task_id());
EXPECT_EQ(TASK_FINISHED, statusFinished->state());
// The non-empty file should not be overwritten by the empty file
// '/xyz' in the 3rd layer of the testing image during provisioning.
EXPECT_SOME(os::shell("test -s " + hostFile));
driver.stop();
driver.join();
}
// This test verifies that the container rootfs can be unmounted correctly
// during cleanup. This is a regression test for container cleanpu EBUSY
// issue. Please see MESOS-9196 for details.
TEST_P(ProvisionerDockerBackendTest, ROOT_INTERNET_CURL_DTYPE_RootfsCleanup)
{
Try<Owned<cluster::Master>> master = StartMaster();
ASSERT_SOME(master);
slave::Flags flags = CreateSlaveFlags();
flags.isolation = "docker/runtime,filesystem/linux";
flags.image_providers = "docker";
flags.image_provisioner_backend = GetParam();
Fetcher fetcher(flags);
Try<MesosContainerizer*> create =
MesosContainerizer::create(flags, true, &fetcher);
ASSERT_SOME(create);
Owned<Containerizer> containerizer(create.get());
Owned<MasterDetector> detector = master.get()->createDetector();
Try<Owned<cluster::Slave>> slave = StartSlave(
detector.get(),
containerizer.get(),
flags);
ASSERT_SOME(slave);
MockScheduler sched;
MesosSchedulerDriver driver(
&sched, DEFAULT_FRAMEWORK_INFO, master.get()->pid, DEFAULT_CREDENTIAL);
EXPECT_CALL(sched, registered(&driver, _, _));
Future<vector<Offer>> offers;
EXPECT_CALL(sched, resourceOffers(&driver, _))
.WillOnce(FutureArg<1>(&offers))
.WillRepeatedly(Return()); // Ignore subsequent offers.
driver.start();
AWAIT_READY(offers);
ASSERT_EQ(1u, offers->size());
const Offer& offer = offers.get()[0];
CommandInfo command = createCommandInfo("sleep 1000");
TaskInfo task = createTask(
offer.slave_id(),
Resources::parse("cpus:1;mem:128").get(),
command);
Image image = createDockerImage("alpine");
ContainerInfo* container = task.mutable_container();
container->set_type(ContainerInfo::MESOS);
container->mutable_mesos()->mutable_image()->CopyFrom(image);
Future<TaskStatus> statusStarting;
Future<TaskStatus> statusRunning;
EXPECT_CALL(sched, statusUpdate(&driver, _))
.WillOnce(FutureArg<1>(&statusStarting))
.WillOnce(FutureArg<1>(&statusRunning))
.WillRepeatedly(Return());
driver.launchTasks(offer.id(), {task});
AWAIT_READY_FOR(statusStarting, Seconds(60));
EXPECT_EQ(task.task_id(), statusStarting->task_id());
EXPECT_EQ(TASK_STARTING, statusStarting->state());
AWAIT_READY(statusRunning);
EXPECT_EQ(task.task_id(), statusRunning->task_id());
EXPECT_EQ(TASK_RUNNING, statusRunning->state());
Future<hashset<ContainerID>> containerIds = containerizer->containers();
AWAIT_READY(containerIds);
ASSERT_EQ(1u, containerIds->size());
const ContainerID& containerId = *containerIds->begin();
Try<hashmap<string, hashset<string>>> rootfses =
slave::provisioner::paths::listContainerRootfses(
slave::paths::getProvisionerDir(flags.work_dir),
containerId);
ASSERT_SOME(rootfses);
ASSERT_EQ(1u, rootfses->size());
ASSERT_EQ(1u, rootfses->values().begin()->size());
const string rootfsDir = slave::provisioner::paths::getContainerRootfsDir(
slave::paths::getProvisionerDir(flags.work_dir),
containerId,
*rootfses->keys().begin(),
*rootfses->values().begin()->begin());
// This keeps a reference to the persistent volume mount.
Try<int_fd> fd = os::open(
path::join(rootfsDir, "etc/hostname"),
O_WRONLY | O_TRUNC | O_CLOEXEC,
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
ASSERT_SOME(fd);
Future<Option<ContainerTermination>> wait = containerizer->wait(containerId);
containerizer->destroy(containerId);
AWAIT_READY(wait);
ASSERT_SOME(wait.get());
ASSERT_TRUE(wait->get().has_status());
EXPECT_WTERMSIG_EQ(SIGKILL, wait.get()->status());
// Verifies that mount point has been removed.
EXPECT_FALSE(os::exists(path::join(rootfsDir, "etc/hostname")));
os::close(fd.get());
driver.stop();
driver.join();
}
// This test verifies that Docker image can be pulled from the
// repository by digest.
TEST_F(ProvisionerDockerTest, ROOT_INTERNET_CURL_ImageDigest)
{
Try<Owned<cluster::Master>> master = StartMaster();
ASSERT_SOME(master);
slave::Flags flags = CreateSlaveFlags();
flags.isolation = "docker/runtime,filesystem/linux";
flags.image_providers = "docker";
Owned<MasterDetector> detector = master.get()->createDetector();
Try<Owned<cluster::Slave>> slave = StartSlave(detector.get(), flags);
ASSERT_SOME(slave);
MockScheduler sched;
MesosSchedulerDriver driver(
&sched, DEFAULT_FRAMEWORK_INFO, master.get()->pid, DEFAULT_CREDENTIAL);
EXPECT_CALL(sched, registered(&driver, _, _));
Future<vector<Offer>> offers;
EXPECT_CALL(sched, resourceOffers(&driver, _))
.WillOnce(FutureArg<1>(&offers))
.WillRepeatedly(Return()); // Ignore subsequent offers.
driver.start();
AWAIT_READY(offers);
ASSERT_EQ(1u, offers->size());
const Offer& offer = offers.get()[0];
// NOTE: We use a non-shell command here because 'sh' might not be
// in the PATH. 'alpine' does not specify env PATH in the image. On
// some linux distribution, '/bin' is not in the PATH by default.
CommandInfo command;
command.set_shell(false);
command.set_value("/bin/ls");
command.add_arguments("ls");
command.add_arguments("-al");
command.add_arguments("/");
TaskInfo task = createTask(
offer.slave_id(),
Resources::parse("cpus:1;mem:128").get(),
command);
// NOTE: We use the digest of the 'alpine:2.7' image because it has a
// Schema 1 manifest (the only manifest schema that we currently support).
const string digest =
"sha256:9f08005dff552038f0ad2f46b8e65ff3d25641747d3912e3ea8da6785046561a";
Image image;
image.set_type(Image::DOCKER);
image.mutable_docker()->set_name("library/alpine@" + digest);
ContainerInfo* container = task.mutable_container();
container->set_type(ContainerInfo::MESOS);
container->mutable_mesos()->mutable_image()->CopyFrom(image);
Future<TaskStatus> statusStarting;
Future<TaskStatus> statusRunning;
Future<TaskStatus> statusFinished;
EXPECT_CALL(sched, statusUpdate(&driver, _))
.WillOnce(FutureArg<1>(&statusStarting))
.WillOnce(FutureArg<1>(&statusRunning))
.WillOnce(FutureArg<1>(&statusFinished));
driver.launchTasks(offer.id(), {task});
AWAIT_READY_FOR(statusStarting, Seconds(60));
EXPECT_EQ(task.task_id(), statusStarting->task_id());
EXPECT_EQ(TASK_STARTING, statusStarting->state());
AWAIT_READY_FOR(statusRunning, Seconds(60));
EXPECT_EQ(task.task_id(), statusRunning->task_id());
EXPECT_EQ(TASK_RUNNING, statusRunning->state());
AWAIT_READY(statusFinished);
EXPECT_EQ(task.task_id(), statusFinished->task_id());
EXPECT_EQ(TASK_FINISHED, statusFinished->state());
driver.stop();
driver.join();
}
// This test verifies that Docker manifest configuration is not carried over
// into the container if the `--docker_ignore_runtime` flag is set. Due to the
// complexity of the `CommandInfo` mapping, simply check to see if the
// environment was merged by the isolator into the task container.
TEST_F(ProvisionerDockerTest, ROOT_DockerIgnoreRuntime)
{
Try<Owned<cluster::Master>> master = StartMaster();
ASSERT_SOME(master);
const string directory = path::join(os::getcwd(), "registry");
const std::vector<std::string>& environment = {
{"DOCKER_RUNTIME_ENV=true"},
};
// Setting the entrypoint and command that will be reflected in the
// manifest.
Future<Nothing> testImage = DockerArchive::create(
directory, "alpine", "null", "null", environment);
AWAIT_READY(testImage);
ASSERT_TRUE(os::exists(path::join(directory, "alpine.tar")));
slave::Flags flags = CreateSlaveFlags();
flags.isolation = "docker/runtime,filesystem/linux";
flags.image_providers = "docker";
flags.docker_registry = directory;
flags.docker_store_dir = path::join(os::getcwd(), "store");
flags.docker_ignore_runtime = true;
Owned<MasterDetector> detector = master.get()->createDetector();
Try<Owned<cluster::Slave>> slave = StartSlave(detector.get(), flags);
ASSERT_SOME(slave);
MockScheduler sched;
MesosSchedulerDriver driver(
&sched, DEFAULT_FRAMEWORK_INFO, master.get()->pid, DEFAULT_CREDENTIAL);
EXPECT_CALL(sched, registered(&driver, _, _));
Future<vector<Offer>> offers;
EXPECT_CALL(sched, resourceOffers(&driver, _))
.WillOnce(FutureArg<1>(&offers))
.WillRepeatedly(Return()); // Ignore subsequent offers.
driver.start();
AWAIT_READY(offers);
ASSERT_EQ(1u, offers->size());
const Offer& offer = offers.get()[0];
// This task will fail if Docker manifest metadata is propagated
// to the container i.e. the `DOCKER_RUNTIME_ENV` variable should
// not be present in the container since we are setting the
// `--docker_ignore_runtime` flag.
TaskInfo task = createTask(
offer.slave_id(),
offer.resources(),
"test -z $DOCKER_RUNTIME_ENV");
Image image;
image.set_type(Image::DOCKER);
image.mutable_docker()->set_name("alpine");
ContainerInfo* container = task.mutable_container();
container->set_type(ContainerInfo::MESOS);
container->mutable_mesos()->mutable_image()->CopyFrom(image);
Future<TaskStatus> statusStarting;
Future<TaskStatus> statusRunning;
Future<TaskStatus> statusFinished;
EXPECT_CALL(sched, statusUpdate(&driver, _))
.WillOnce(FutureArg<1>(&statusStarting))
.WillOnce(FutureArg<1>(&statusRunning))
.WillOnce(FutureArg<1>(&statusFinished));
driver.launchTasks(offer.id(), {task});
AWAIT_READY(statusStarting);
EXPECT_EQ(task.task_id(), statusStarting->task_id());
EXPECT_EQ(TASK_STARTING, statusStarting->state());
AWAIT_READY(statusRunning);
EXPECT_EQ(task.task_id(), statusRunning->task_id());
EXPECT_EQ(TASK_RUNNING, statusRunning->state());
AWAIT_READY(statusFinished);
EXPECT_EQ(task.task_id(), statusFinished->task_id());
EXPECT_EQ(TASK_FINISHED, statusFinished->state());
driver.stop();
driver.join();
}
// This test verifies that if a container image is specified, the
// command runs as the specified user "$SUDO_USER" and the sandbox of
// the command task is writeable by the specified user. It also
// verifies that stdout/stderr are owned by the specified user.
TEST_F(ProvisionerDockerTest,
ROOT_INTERNET_CURL_UNPRIVILEGED_USER_CommandTaskUser)
{
Try<Owned<cluster::Master>> master = StartMaster();
ASSERT_SOME(master);
slave::Flags flags = CreateSlaveFlags();
flags.isolation = "docker/runtime,filesystem/linux";
flags.image_providers = "docker";
Owned<MasterDetector> detector = master.get()->createDetector();
Try<Owned<cluster::Slave>> slave = StartSlave(detector.get(), flags);
ASSERT_SOME(slave);
MockScheduler sched;
MesosSchedulerDriver driver(
&sched, DEFAULT_FRAMEWORK_INFO, master.get()->pid, DEFAULT_CREDENTIAL);
EXPECT_CALL(sched, registered(&driver, _, _));
Future<vector<Offer>> offers;
EXPECT_CALL(sched, resourceOffers(&driver, _))
.WillOnce(FutureArg<1>(&offers))
.WillRepeatedly(Return()); // Ignore subsequent offers.
driver.start();
AWAIT_READY(offers);
ASSERT_EQ(1u, offers->size());
const Offer& offer = offers.get()[0];
Option<string> user = os::getenv("SUDO_USER");
ASSERT_SOME(user);
Result<uid_t> uid = os::getuid(user.get());
ASSERT_SOME(uid);
CommandInfo command;
command.set_user(user.get());
command.set_value(strings::format(
"#!/bin/sh\n"
"touch $MESOS_SANDBOX/file\n"
"FILE_UID=`stat -c %%u $MESOS_SANDBOX/file`\n"
"test $FILE_UID = %d\n"
"STDOUT_UID=`stat -c %%u $MESOS_SANDBOX/stdout`\n"
"test $STDOUT_UID = %d\n"
"STDERR_UID=`stat -c %%u $MESOS_SANDBOX/stderr`\n"
"test $STDERR_UID = %d\n",
uid.get(), uid.get(), uid.get()).get());
TaskInfo task = createTask(
offer.slave_id(),
Resources::parse("cpus:1;mem:128").get(),
command);
Image image;
image.set_type(Image::DOCKER);
image.mutable_docker()->set_name("alpine");
ContainerInfo* container = task.mutable_container();
container->set_type(ContainerInfo::MESOS);
container->mutable_mesos()->mutable_image()->CopyFrom(image);
Future<TaskStatus> statusStarting;
Future<TaskStatus> statusRunning;
Future<TaskStatus> statusFinished;
EXPECT_CALL(sched, statusUpdate(&driver, _))
.WillOnce(FutureArg<1>(&statusStarting))
.WillOnce(FutureArg<1>(&statusRunning))
.WillOnce(FutureArg<1>(&statusFinished));
driver.launchTasks(offer.id(), {task});
AWAIT_READY_FOR(statusStarting, Seconds(60));
EXPECT_EQ(task.task_id(), statusStarting->task_id());
EXPECT_EQ(TASK_STARTING, statusStarting->state());
AWAIT_READY_FOR(statusRunning, Seconds(60));
EXPECT_EQ(task.task_id(), statusRunning->task_id());
EXPECT_EQ(TASK_RUNNING, statusRunning->state());
AWAIT_READY(statusFinished);
EXPECT_EQ(task.task_id(), statusFinished->task_id());
EXPECT_EQ(TASK_FINISHED, statusFinished->state());
driver.stop();
driver.join();
}
// This test simulate the case that after the agent reboots the
// container runtime directory is gone while the provisioner
// directory still survives. The recursive `provisioner::destroy()`
// can make sure that a child container is always cleaned up
// before its parent container.
TEST_F(ProvisionerDockerTest, ROOT_RecoverNestedOnReboot)
{
const string directory = path::join(os::getcwd(), "archives");
Future<Nothing> testImage = DockerArchive::create(directory, "alpine");
AWAIT_READY(testImage);
slave::Flags flags = CreateSlaveFlags();
flags.isolation = "docker/runtime,filesystem/linux";
flags.image_providers = "docker";
flags.docker_registry = directory;
flags.docker_store_dir = path::join(os::getcwd(), "store");
Try<Owned<Provisioner>> provisioner = Provisioner::create(flags);
ASSERT_SOME(provisioner);
ContainerID containerId;
containerId.set_value(id::UUID::random().toString());
ContainerID nestedContainerId;
nestedContainerId.mutable_parent()->CopyFrom(containerId);
nestedContainerId.set_value(id::UUID::random().toString());
Image image;
image.set_type(Image::DOCKER);
image.mutable_docker()->set_name("alpine");
AWAIT_READY(provisioner.get()->provision(nestedContainerId, image));
// Passing an empty hashset to `provisioner::recover()` to
// simulate the agent reboot scenario.
AWAIT_READY(provisioner.get()->recover({}));
const string containerDir = slave::provisioner::paths::getContainerDir(
slave::paths::getProvisionerDir(flags.work_dir),
containerId);
EXPECT_FALSE(os::exists(containerDir));
}
#endif // __linux__
} // namespace tests {
} // namespace internal {
} // namespace mesos {