blob: 5b685bfd842d0d98e8ea5ec5ddea8d2cd893dd81 [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/gtest.hpp>
#include <stout/json.hpp>
#include <stout/os.hpp>
#include <stout/path.hpp>
#include <stout/stringify.hpp>
#include <process/future.hpp>
#include <process/gmock.hpp>
#include <process/owned.hpp>
#include <mesos/docker/spec.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/mesos.hpp"
#include "tests/utils.hpp"
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::Future;
using process::Owned;
using process::PID;
using process::Promise;
using master::Master;
using slave::ImageInfo;
using slave::Slave;
using slave::docker::Puller;
using slave::docker::RegistryPuller;
using slave::docker::Store;
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");
const string layerPath2 = paths::getImageLayerRootfsPath(
flags.docker_store_dir,
"456");
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:
virtual void SetUp()
{
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 achive created from a 'docker save' command can be unpacked and
// stored in the proper locations accessible to the Docker provisioner.
TEST_F(ProvisionerDockerLocalStoreTest, LocalStoreTestWithTar)
{
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));
slave::Flags flags;
flags.docker_registry = archivesDir;
flags.docker_store_dir = path::join(os::getcwd(), "store");
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);
AWAIT_READY(imageInfo);
verifyLocalDockerImage(flags, imageInfo.get().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");
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);
AWAIT_READY(imageInfo);
// Store is deleted and recreated. Metadata Manager is initialized upon
// creation of the store.
store.get().reset();
store = slave::docker::Store::create(flags);
ASSERT_SOME(store);
Future<Nothing> recover = store.get()->recover();
AWAIT_READY(recover);
imageInfo = store.get()->get(image);
AWAIT_READY(imageInfo);
verifyLocalDockerImage(flags, imageInfo.get().layers);
}
class MockPuller : public Puller
{
public:
MockPuller()
{
EXPECT_CALL(*this, pull(_, _))
.WillRepeatedly(Invoke(this, &MockPuller::unmocked_pull));
}
virtual ~MockPuller() {}
MOCK_METHOD2(
pull,
Future<vector<string>>(
const spec::ImageReference&,
const string&));
Future<vector<string>> unmocked_pull(
const spec::ImageReference& reference,
const string& directory)
{
// TODO(gilbert): Allow return list to be overridden.
return vector<string>();
}
};
// This tests the store to pull the same image simutanuously.
// This test verifies that the store only calls the puller once
// when multiple requests for the same image is in flight.
TEST_F(ProvisionerDockerLocalStoreTest, PullingSameImageSimutanuously)
{
const string archivesDir = path::join(os::getcwd(), "images");
const string image = path::join(archivesDir, "abc:latest");
ASSERT_SOME(os::mkdir(archivesDir));
ASSERT_SOME(os::mkdir(image));
slave::Flags flags;
flags.docker_registry = "file://" + archivesDir;
flags.docker_store_dir = path::join(os::getcwd(), "store");
MockPuller* puller = new MockPuller();
Future<Nothing> pull;
Future<string> directory;
Promise<vector<string>> 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);
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);
const vector<string> result = {"456"};
ASSERT_TRUE(imageInfo2.isPending());
promise.set(result);
AWAIT_READY(imageInfo1);
AWAIT_READY(imageInfo2);
EXPECT_EQ(imageInfo1.get().layers, imageInfo2.get().layers);
}
#ifdef __linux__
class ProvisionerDockerRegistryPullerTest : public MesosTest {};
// TODO(jieyu): This is a ROOT test because of MESOS-4757. Remove the
// ROOT restriction after MESOS-4757 is resolved.
TEST_F(ProvisionerDockerRegistryPullerTest, ROOT_INTERNET_CURL_ShellCommand)
{
Try<PID<Master>> master = StartMaster();
ASSERT_SOME(master);
slave::Flags flags = CreateSlaveFlags();
flags.isolation = "docker/runtime,filesystem/linux";
flags.image_providers = "docker";
flags.docker_registry = "https://registry-1.docker.io";
Try<PID<Slave>> slave = StartSlave(flags);
ASSERT_SOME(slave);
MockScheduler sched;
MesosSchedulerDriver driver(
&sched, DEFAULT_FRAMEWORK_INFO, master.get(), 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("library/alpine");
ContainerInfo* container = task.mutable_container();
container->set_type(ContainerInfo::MESOS);
container->mutable_mesos()->mutable_image()->CopyFrom(image);
Future<TaskStatus> statusRunning;
Future<TaskStatus> statusFinished;
EXPECT_CALL(sched, statusUpdate(&driver, _))
.WillOnce(FutureArg<1>(&statusRunning))
.WillOnce(FutureArg<1>(&statusFinished));
driver.launchTasks(offer.id(), {task});
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();
Shutdown();
}
#endif
} // namespace tests {
} // namespace internal {
} // namespace mesos {