blob: 1aa32ec3ccddf549322e15e55c354d0591b822dd [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 <string>
#include <gmock/gmock.h>
#include <mesos/slave/isolator.hpp>
#include <process/gtest.hpp>
#include <stout/gtest.hpp>
#include <stout/json.hpp>
#include <stout/os.hpp>
#include <stout/path.hpp>
#include <stout/stringify.hpp>
#include <stout/uuid.hpp>
#include <stout/os/realpath.hpp>
#include <stout/tests/utils.hpp>
#include <mesos/appc/spec.hpp>
#include "common/command_utils.hpp"
#include "slave/paths.hpp"
#include "slave/containerizer/mesos/provisioner/constants.hpp"
#include "slave/containerizer/mesos/provisioner/paths.hpp"
#include "slave/containerizer/mesos/provisioner/provisioner.hpp"
#include "slave/containerizer/mesos/provisioner/appc/fetcher.hpp"
#include "slave/containerizer/mesos/provisioner/appc/paths.hpp"
#include "slave/containerizer/mesos/provisioner/appc/store.hpp"
#include "tests/mesos.hpp"
#ifdef __linux__
#include "tests/containerizer/rootfs.hpp"
#endif
namespace http = process::http;
namespace paths = mesos::internal::slave::appc::paths;
using std::list;
using std::string;
using std::vector;
using process::Future;
using process::Owned;
using process::Process;
using testing::_;
using testing::Return;
using mesos::internal::slave::BIND_BACKEND;
using mesos::internal::slave::COPY_BACKEND;
using mesos::internal::slave::Fetcher;
using mesos::internal::slave::Provisioner;
using mesos::internal::slave::appc::Store;
using mesos::master::detector::MasterDetector;
namespace mesos {
namespace internal {
namespace tests {
/**
* Helper function that returns a Appc protobuf object for given
* parameters.
*
* @param name Appc image name.
* @param arch Machine architecture(e.g, x86, amd64).
* @param os Operating system(e.g, linux).
* @return Appc protobuf message object.
*/
static Image::Appc getAppcImage(
const string& name,
const string& arch = "amd64",
const string& os = "linux")
{
Image::Appc appc;
appc.set_name(name);
Label archLabel;
archLabel.set_key("arch");
archLabel.set_value(arch);
Label osLabel;
osLabel.set_key("os");
osLabel.set_value(os);
Labels labels;
labels.add_labels()->CopyFrom(archLabel);
labels.add_labels()->CopyFrom(osLabel);
appc.mutable_labels()->CopyFrom(labels);
return appc;
}
class AppcStoreTest : public TemporaryDirectoryTest
{
protected:
// Reusable method to create a simple test image directory inside the store
// directory. The directory structure reflects the Appc image spec.
//
// @param storeDir Path to the store directory where all images are stored.
// @param manifest Manifest JSON to be used for the test image.
// @return Path to the test image directory.
Try<string> createTestImage(
const string& storeDir,
const JSON::Value& manifest)
{
Try<Nothing> mkdir = os::mkdir(storeDir, true);
if (mkdir.isError()) {
return mkdir.error();
}
// Create a simple image in the store:
// <store>
// |--images
// |--<id>
// |--manifest
// |--rootfs/tmp/test
// The 'imageId' below has the correct format but it's not computed
// by hashing the tarball of the image. It's OK here as we assume
// the images under 'images' have passed such check when they are
// downloaded and validated.
const string imageId =
"sha512-e77d96aa0240eedf134b8c90baeaf76dca8e78691836301d7498c84020446042e"
"797b296d6ab296e0954c2626bfb264322ebeb8f447dac4fac6511ea06bc61f0";
// Create rootfs inside the store directory.
const string rootfsPath = paths::getImageRootfsPath(storeDir, imageId);
Try<Nothing> mkRootfsDir = os::mkdir(rootfsPath);
if (mkRootfsDir.isError()) {
return mkRootfsDir.error();
}
// Create tmp directory inside rootfs.
const string tmpDirPath = path::join(rootfsPath, "tmp");
Try<Nothing> mkTmpDir = os::mkdir(tmpDirPath);
if (mkTmpDir.isError()) {
return mkTmpDir.error();
}
// Create test file in the tmp directory.
Try<Nothing> testFileWrite =
os::write(path::join(tmpDirPath, "test"), "test");
if (testFileWrite.isError()) {
return testFileWrite.error();
}
// Create manifest in the image directory.
Try<Nothing> manifestWrite = os::write(
paths::getImageManifestPath(storeDir, imageId),
stringify(manifest));
if (manifestWrite.isError()) {
return manifestWrite.error();
}
return paths::getImagePath(storeDir, imageId);
}
Image::Appc getTestImage() const
{
Image::Appc appc;
appc.set_name("foo.com/bar");
Label version;
version.set_key("version");
version.set_value("1.0.0");
Label arch;
arch.set_key("arch");
arch.set_value("amd64");
Label os;
os.set_key("os");
os.set_value("linux");
Labels labels;
labels.add_labels()->CopyFrom(version);
labels.add_labels()->CopyFrom(arch);
labels.add_labels()->CopyFrom(os);
appc.mutable_labels()->CopyFrom(labels);
return appc;
}
// Abstracts the manifest accessor for the test fixture. This provides the
// ability for customizing manifests for fixtures.
virtual JSON::Value getManifest() const
{
return JSON::parse(
R"~(
{
"acKind": "ImageManifest",
"acVersion": "0.6.1",
"name": "foo.com/bar",
"labels": [
{
"name": "version",
"value": "1.0.0"
},
{
"name": "arch",
"value": "amd64"
},
{
"name": "os",
"value": "linux"
}
],
"annotations": [
{
"name": "created",
"value": "1438983392"
}
]
})~").get();
}
};
TEST_F(AppcStoreTest, Recover)
{
// Create store.
slave::Flags flags;
flags.appc_store_dir = path::join(os::getcwd(), "store");
Try<Owned<slave::Store>> store = Store::create(flags);
ASSERT_SOME(store);
Try<string> createImage = createTestImage(
flags.appc_store_dir,
getManifest());
ASSERT_SOME(createImage);
const string imagePath = createImage.get();
// Recover the image from disk.
AWAIT_READY(store.get()->recover());
Image image;
image.mutable_appc()->CopyFrom(getTestImage());
Future<slave::ImageInfo> ImageInfo = store.get()->get(image, COPY_BACKEND);
AWAIT_READY(ImageInfo);
EXPECT_EQ(1u, ImageInfo->layers.size());
ASSERT_SOME(os::realpath(imagePath));
EXPECT_EQ(
os::realpath(path::join(imagePath, "rootfs")).get(),
ImageInfo->layers.front());
}
class ProvisionerAppcTest : public AppcStoreTest {};
#ifdef __linux__
// This test verifies that the provisioner can provision an rootfs
// from an image that is already put into the store directory.
TEST_F(ProvisionerAppcTest, ROOT_Provision)
{
// Create provisioner.
slave::Flags flags;
flags.image_providers = "APPC";
flags.appc_store_dir = path::join(os::getcwd(), "store");
flags.image_provisioner_backend = BIND_BACKEND;
flags.work_dir = path::join(sandbox.get(), "work_dir");
Try<Owned<Provisioner>> provisioner = Provisioner::create(flags);
ASSERT_SOME(provisioner);
Try<string> createImage = createTestImage(
flags.appc_store_dir,
getManifest());
ASSERT_SOME(createImage);
// Recover. This is when the image in the store is loaded.
AWAIT_READY(provisioner.get()->recover({}));
// Simulate a task that requires an image.
Image image;
image.mutable_appc()->CopyFrom(getTestImage());
ContainerID containerId;
containerId.set_value("12345");
Future<slave::ProvisionInfo> provisionInfo =
provisioner.get()->provision(containerId, image);
AWAIT_READY(provisionInfo);
string provisionerDir = slave::paths::getProvisionerDir(flags.work_dir);
string containerDir =
slave::provisioner::paths::getContainerDir(
provisionerDir,
containerId);
Try<hashmap<string, hashset<string>>> rootfses =
slave::provisioner::paths::listContainerRootfses(
provisionerDir,
containerId);
ASSERT_SOME(rootfses);
// Verify that the rootfs is successfully provisioned.
ASSERT_TRUE(rootfses->contains(flags.image_provisioner_backend.get()));
ASSERT_EQ(1u, rootfses->get(flags.image_provisioner_backend.get())->size());
EXPECT_EQ(*rootfses->get(flags.image_provisioner_backend.get())->begin(),
Path(provisionInfo->rootfs).basename());
Future<bool> destroy = provisioner.get()->destroy(containerId);
AWAIT_READY(destroy);
// One rootfs is destroyed.
EXPECT_TRUE(destroy.get());
// The container directory is successfully cleaned up.
EXPECT_FALSE(os::exists(containerDir));
}
// This test verifies that the provisioner can provision an rootfs
// from an image for a child container.
TEST_F(ProvisionerAppcTest, ROOT_ProvisionNestedContainer)
{
slave::Flags flags;
flags.image_providers = "APPC";
flags.appc_store_dir = path::join(os::getcwd(), "store");
flags.image_provisioner_backend = BIND_BACKEND;
flags.work_dir = path::join(sandbox.get(), "work_dir");
Try<Owned<Provisioner>> provisioner = Provisioner::create(flags);
ASSERT_SOME(provisioner);
Try<string> createImage = createTestImage(
flags.appc_store_dir,
getManifest());
ASSERT_SOME(createImage);
// Recover. This is when the image in the store is loaded.
AWAIT_READY(provisioner.get()->recover({}));
Image image;
image.mutable_appc()->CopyFrom(getTestImage());
ContainerID parent;
ContainerID child;
parent.set_value(id::UUID::random().toString());
child.set_value(id::UUID::random().toString());
child.mutable_parent()->CopyFrom(parent);
Future<slave::ProvisionInfo> provisionInfo =
provisioner.get()->provision(child, image);
AWAIT_READY(provisionInfo);
const string provisionerDir = slave::paths::getProvisionerDir(flags.work_dir);
const string containerDir =
slave::provisioner::paths::getContainerDir(
provisionerDir,
child);
Try<hashmap<string, hashset<string>>> rootfses =
slave::provisioner::paths::listContainerRootfses(
provisionerDir,
child);
ASSERT_SOME(rootfses);
// Verify that the rootfs is successfully provisioned.
ASSERT_TRUE(rootfses->contains(flags.image_provisioner_backend.get()));
ASSERT_EQ(1u, rootfses->get(flags.image_provisioner_backend.get())->size());
EXPECT_EQ(*rootfses->get(flags.image_provisioner_backend.get())->begin(),
Path(provisionInfo->rootfs).basename());
// TODO(jieyu): Verify that 'containerDir' is nested under its
// parent container's 'containerDir'.
Future<bool> destroy = provisioner.get()->destroy(child);
AWAIT_READY(destroy);
EXPECT_TRUE(destroy.get());
EXPECT_FALSE(os::exists(containerDir));
}
#endif // __linux__
// This test verifies that a provisioner can recover the rootfs
// provisioned by a previous provisioner and then destroy it. Note
// that we use the copy backend in this test so Linux is not required.
TEST_F(ProvisionerAppcTest, Recover)
{
// Create provisioner.
slave::Flags flags;
flags.image_providers = "APPC";
flags.appc_store_dir = path::join(os::getcwd(), "store");
flags.image_provisioner_backend = COPY_BACKEND;
flags.work_dir = path::join(sandbox.get(), "work_dir");
Try<Owned<Provisioner>> provisioner = Provisioner::create(flags);
ASSERT_SOME(provisioner);
Try<string> createImage = createTestImage(
flags.appc_store_dir,
getManifest());
ASSERT_SOME(createImage);
// Recover. This is when the image in the store is loaded.
AWAIT_READY(provisioner.get()->recover({}));
Image image;
image.mutable_appc()->CopyFrom(getTestImage());
ContainerID containerId;
containerId.set_value(id::UUID::random().toString());
Future<slave::ProvisionInfo> provisionInfo =
provisioner.get()->provision(containerId, image);
AWAIT_READY(provisionInfo);
provisioner->reset();
// Create a new provisioner to recover the state from the container.
provisioner = Provisioner::create(flags);
ASSERT_SOME(provisioner);
AWAIT_READY(provisioner.get()->recover({containerId}));
// It's possible for the user to provision two different rootfses
// from the same image.
AWAIT_READY(provisioner.get()->provision(containerId, image));
string provisionerDir = slave::paths::getProvisionerDir(flags.work_dir);
string containerDir =
slave::provisioner::paths::getContainerDir(
provisionerDir,
containerId);
Try<hashmap<string, hashset<string>>> rootfses =
slave::provisioner::paths::listContainerRootfses(
provisionerDir,
containerId);
ASSERT_SOME(rootfses);
// Verify that the rootfs is successfully provisioned.
ASSERT_TRUE(rootfses->contains(flags.image_provisioner_backend.get()));
EXPECT_EQ(2u, rootfses->get(flags.image_provisioner_backend.get())->size());
Future<bool> destroy = provisioner.get()->destroy(containerId);
AWAIT_READY(destroy);
EXPECT_TRUE(destroy.get());
// The container directory is successfully cleaned up.
EXPECT_FALSE(os::exists(containerDir));
}
// This test verifies that the provisioner can recover the rootfses
// for both parent and child containers.
TEST_F(ProvisionerAppcTest, RecoverNestedContainer)
{
slave::Flags flags;
flags.image_providers = "APPC";
flags.appc_store_dir = path::join(os::getcwd(), "store");
flags.image_provisioner_backend = COPY_BACKEND;
flags.work_dir = path::join(sandbox.get(), "work_dir");
Try<Owned<Provisioner>> provisioner = Provisioner::create(flags);
ASSERT_SOME(provisioner);
Try<string> createImage = createTestImage(
flags.appc_store_dir,
getManifest());
ASSERT_SOME(createImage);
// Recover. This is when the image in the store is loaded.
AWAIT_READY(provisioner.get()->recover({}));
Image image;
image.mutable_appc()->CopyFrom(getTestImage());
ContainerID parent;
ContainerID child;
parent.set_value(id::UUID::random().toString());
child.set_value(id::UUID::random().toString());
child.mutable_parent()->CopyFrom(parent);
AWAIT_READY(provisioner.get()->provision(parent, image));
AWAIT_READY(provisioner.get()->provision(child, image));
provisioner->reset();
// Create a new provisioner to recover the state from the container.
provisioner = Provisioner::create(flags);
ASSERT_SOME(provisioner);
AWAIT_READY(provisioner.get()->recover({parent, child}));
AWAIT_READY(provisioner.get()->provision(child, image));
const string provisionerDir = slave::paths::getProvisionerDir(flags.work_dir);
string containerDir =
slave::provisioner::paths::getContainerDir(
provisionerDir,
child);
Future<bool> destroy = provisioner.get()->destroy(child);
AWAIT_READY(destroy);
EXPECT_TRUE(destroy.get());
EXPECT_FALSE(os::exists(containerDir));
containerDir =
slave::provisioner::paths::getContainerDir(
provisionerDir,
parent);
destroy = provisioner.get()->destroy(parent);
AWAIT_READY(destroy);
EXPECT_TRUE(destroy.get());
EXPECT_FALSE(os::exists(containerDir));
}
// This test verifies that the provisioner can recover the rootfses
// for the child containers if there is no image specified for its
// parent container.
TEST_F(ProvisionerAppcTest, RecoverNestedContainerNoParentImage)
{
slave::Flags flags;
flags.image_providers = "APPC";
flags.appc_store_dir = path::join(os::getcwd(), "store");
flags.image_provisioner_backend = COPY_BACKEND;
flags.work_dir = path::join(sandbox.get(), "work_dir");
Try<Owned<Provisioner>> provisioner = Provisioner::create(flags);
ASSERT_SOME(provisioner);
Try<string> createImage = createTestImage(
flags.appc_store_dir,
getManifest());
ASSERT_SOME(createImage);
// Recover. This is when the image in the store is loaded.
AWAIT_READY(provisioner.get()->recover({}));
Image image;
image.mutable_appc()->CopyFrom(getTestImage());
ContainerID parent;
ContainerID child;
parent.set_value(id::UUID::random().toString());
child.set_value(id::UUID::random().toString());
child.mutable_parent()->CopyFrom(parent);
AWAIT_READY(provisioner.get()->provision(child, image));
provisioner->reset();
// Create a new provisioner to recover the state from the container.
provisioner = Provisioner::create(flags);
ASSERT_SOME(provisioner);
AWAIT_READY(provisioner.get()->recover({parent, child}));
AWAIT_READY(provisioner.get()->provision(child, image));
const string provisionerDir = slave::paths::getProvisionerDir(flags.work_dir);
string containerDir =
slave::provisioner::paths::getContainerDir(
provisionerDir,
child);
Future<bool> destroy = provisioner.get()->destroy(child);
AWAIT_READY(destroy);
EXPECT_TRUE(destroy.get());
EXPECT_FALSE(os::exists(containerDir));
containerDir =
slave::provisioner::paths::getContainerDir(
provisionerDir,
parent);
destroy = provisioner.get()->destroy(parent);
AWAIT_READY(destroy);
EXPECT_TRUE(destroy.get());
EXPECT_FALSE(os::exists(containerDir));
}
// Mock HTTP image server.
class TestAppcImageServer : public Process<TestAppcImageServer>
{
public:
TestAppcImageServer()
: ProcessBase("TestAppcImageServer"),
imagesDirName("server") {}
void load()
{
const string imagesDir = path::join(os::getcwd(), imagesDirName);
Try<list<string>> imageBundles = os::ls(imagesDir);
ASSERT_SOME(imageBundles);
foreach (const string& imageName, imageBundles.get()) {
route("/" + imageName, None(), &TestAppcImageServer::serveRequest);
}
}
Future<http::Response> serveRequest(const http::Request& request)
{
const string imageBundleName = request.url.path;
const string imageBundlePath = path::join(
os::getcwd(),
imagesDirName,
Path(imageBundleName).basename());
http::OK response;
response.type = response.PATH;
response.path = imageBundlePath;
response.headers["Content-Type"] = "application/octet-stream";
response.headers["Content-Disposition"] = strings::format(
"attachment; filename=%s",
imageBundlePath).get();
return response;
}
private:
// TODO(jojy): Currently hard-codes the images directory name.
// Consider parameterizing the directory name. This could be done
// by removing the 'const' ness of the variable and adding mutator.
const string imagesDirName;
};
// Test fixture that uses the mock HTTP image server. This fixture provides the
// abstraction for creating and composing complex test cases for Appc image
// fetcher and store.
class AppcImageFetcherTest : public AppcStoreTest
{
protected:
// Custom implementation that overrides the host and port of the image name.
JSON::Value getManifest() const override
{
string imageName = strings::format(
"%s:%d/TestAppcImageServer/image",
stringify(server.self().address.ip),
server.self().address.port).get();
const string manifest = strings::format(
R"~(
{
"acKind": "ImageManifest",
"acVersion": "0.6.1",
"name": "%s",
"labels": [
{
"name": "version",
"value": "latest"
},
{
"name": "arch",
"value": "amd64"
},
{
"name": "os",
"value": "linux"
}
],
"annotations": [
{
"name": "created",
"value": "1438983392"
}
]
})~",
imageName).get();
return JSON::parse(manifest).get();
}
void prepareImage(
const string& directory,
const string& imageBundlePath,
const JSON::Value& manifest)
{
// Place the image in dir '@imageTopDir/images'.
Try<string> createImage = createTestImage(directory, getManifest());
ASSERT_SOME(createImage);
// Test image is created in a directory with this name.
const string imageId =
"sha512-e77d96aa0240eedf134b8c90baeaf76dca8e78691836301d7498c8402044604"
"2e797b296d6ab296e0954c2626bfb264322ebeb8f447dac4fac6511ea06bc61f0";
const Path imageDir(path::join(directory, "images", imageId));
Future<Nothing> future = command::tar(
Path("."),
Path(imageBundlePath),
imageDir,
command::Compression::GZIP);
AWAIT_READY(future);
}
void startServer()
{
spawn(server);
}
void TearDown() override
{
terminate(server);
wait(server);
TemporaryDirectoryTest::TearDown();
}
TestAppcImageServer server;
};
// Tests simple http fetch functionality of the appc::Fetcher component.
// The test fetches a test Appc image from the http server and
// verifies its content. The image is served in 'tar + gzip' format.
TEST_F(AppcImageFetcherTest, CURL_SimpleHttpFetch)
{
const string imageName = "image";
const string imageBundleName = imageName + "-latest-linux-amd64.aci";
// Use the default server directory of the image server.
const Path serverDir(path::join(os::getcwd(), "server"));
const string imageBundlePath = path::join(serverDir, imageBundleName);
// Setup the image.
prepareImage(serverDir, imageBundlePath, getManifest());
// Setup http server to serve the image prepared above.
server.load();
startServer();
// Appc Image to be fetched.
const string discoverableImageName = strings::format(
"%s/TestAppcImageServer/%s",
stringify(server.self().address),
imageName).get();
Image::Appc appc = getAppcImage(discoverableImageName);
// Create image fetcher.
Try<Owned<uri::Fetcher>> uriFetcher = uri::fetcher::create();
ASSERT_SOME(uriFetcher);
slave::Flags flags;
Try<Owned<slave::appc::Fetcher>> fetcher =
slave::appc::Fetcher::create(flags, uriFetcher->share());
ASSERT_SOME(fetcher);
// Prepare fetch directory.
const Path imageFetchDir(path::join(os::getcwd(), "fetched-images"));
Try<Nothing> mkdir = os::mkdir(imageFetchDir);
ASSERT_SOME(mkdir);
// Now fetch the image.
AWAIT_READY(fetcher.get()->fetch(appc, imageFetchDir));
// Verify that there is an image directory.
Try<list<string>> imageDirs = os::ls(imageFetchDir);
ASSERT_SOME(imageDirs);
// Verify that there is only ONE image directory.
ASSERT_EQ(1u, imageDirs->size());
// Verify that there is a roofs.
const Path imageRootfs(path::join(
imageFetchDir,
imageDirs->front(),
"rootfs"));
ASSERT_TRUE(os::exists(imageRootfs));
// Verify that the image fetched is the same as on the server.
ASSERT_SOME_EQ("test", os::read(path::join(imageRootfs, "tmp", "test")));
}
// Tests simple file fetch functionality of the appc::Fetcher component.
TEST_F(AppcImageFetcherTest, SimpleFileFetch)
{
const string imageName = "image";
const string imageBundleName = imageName + "-latest-linux-amd64.aci";
// This represents the directory where images volume could be mounted.
const string imageDirMountPath(path::join(os::getcwd(), "mnt"));
const string imageBundlePath = path::join(imageDirMountPath, imageBundleName);
// Setup the image bundle.
prepareImage(imageDirMountPath, imageBundlePath, getManifest());
Image::Appc appc = getAppcImage("image");
// Create image fetcher.
Try<Owned<uri::Fetcher>> uriFetcher = uri::fetcher::create();
ASSERT_SOME(uriFetcher);
slave::Flags flags;
// Set file path prefix for simple image discovery.
flags.appc_simple_discovery_uri_prefix = imageDirMountPath + "/";
Try<Owned<slave::appc::Fetcher>> fetcher =
slave::appc::Fetcher::create(flags, uriFetcher->share());
ASSERT_SOME(fetcher);
// Prepare fetch directory.
const Path imageFetchDir(path::join(os::getcwd(), "fetched-images"));
Try<Nothing> mkdir = os::mkdir(imageFetchDir);
ASSERT_SOME(mkdir);
// Now fetch the image.
AWAIT_READY(fetcher.get()->fetch(appc, imageFetchDir));
// Verify that there is an image directory.
Try<list<string>> imageDirs = os::ls(imageFetchDir);
ASSERT_SOME(imageDirs);
// Verify that there is only ONE image directory.
ASSERT_EQ(1u, imageDirs->size());
// Verify that there is a roofs.
const Path imageRootfs(path::join(
imageFetchDir,
imageDirs->front(),
"rootfs"));
ASSERT_TRUE(os::exists(imageRootfs));
// Verify that the image fetched is the same as on the server.
ASSERT_SOME_EQ("test", os::read(path::join(imageRootfs, "tmp", "test")));
}
#ifdef __linux__
// Test fixture for Appc image provisioner integration tests. It also provides a
// helper for creating a base linux image bundle.
class AppcProvisionerIntegrationTest : public MesosTest
{
protected:
// Prepares a base 'linux' Appc image bundle from host's rootfs.
void prepareImageBundle(const string& imageName, const string& mntDir)
{
// TODO(jojy): Consider parameterizing the labels for image name decoration.
const string imageBundleName = imageName + "-latest-linux-amd64.aci";
const string imagesPath = path::join(mntDir, "images");
const string imagePath = path::join(imagesPath, imageBundleName);
const string manifest = strings::format(
R"~(
{
"acKind": "ImageManifest",
"acVersion": "0.6.1",
"name": "%s",
"labels": [
{
"name": "version",
"value": "latest"
},
{
"name": "arch",
"value": "amd64"
},
{
"name": "os",
"value": "linux"
}
],
"annotations": [
{
"name": "created",
"value": "1438983392"
}
]
})~",
imageName).get();
const string rootfsPath = path::join(imagePath, "rootfs");
// Setup image on the server.
Try<Owned<Rootfs>> rootfs = LinuxRootfs::create(rootfsPath);
ASSERT_SOME(rootfs);
Try<Nothing> manifestWrite = os::write(
path::join(imagePath, "manifest"),
manifest);
ASSERT_SOME(manifestWrite);
const string imageBundlePath = path::join(mntDir, imageBundleName);
Future<Nothing> future = command::tar(
Path("."),
Path(imageBundlePath),
imagePath,
command::Compression::GZIP);
AWAIT_READY_FOR(future, Seconds(120));
}
};
// Tests the Appc image provisioner for a single layered Linux image. The
// image's rootfs is built from local host's file system.
TEST_F(AppcProvisionerIntegrationTest, ROOT_SimpleLinuxImageTest)
{
const string imageName = "linux";
// Represents the directory where image bundles would be mounted.
const string mntDir = path::join(os::getcwd(), "mnt");
// Prepare the image bundle at the mount point.
prepareImageBundle(imageName, mntDir);
// Start master.
master::Flags masterFlags = CreateMasterFlags();
Try<Owned<cluster::Master>> master = StartMaster(masterFlags);
ASSERT_SOME(master);
// Start agent.
slave::Flags flags = CreateSlaveFlags();
flags.isolation = "filesystem/linux";
flags.image_providers = "APPC";
flags.appc_simple_discovery_uri_prefix = path::join(mntDir, "");
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 /");
// Setup image for task.
Image::Appc appc = getAppcImage(imageName);
Image image;
image.set_type(Image::APPC);
image.mutable_appc()->CopyFrom(appc);
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(120));
EXPECT_EQ(task.task_id(), statusStarting->task_id());
EXPECT_EQ(TASK_STARTING, statusStarting->state());
AWAIT_READY_FOR(statusRunning, Seconds(120));
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();
}
#endif
// TODO(jojy): Add integration test for image with dependencies.
} // namespace tests {
} // namespace internal {
} // namespace mesos {