Implemented a 'read-only' Appc image store. Review: https://reviews.apache.org/r/37311
Project: http://git-wip-us.apache.org/repos/asf/mesos/repo Commit: http://git-wip-us.apache.org/repos/asf/mesos/commit/dedd1ed6 Tree: http://git-wip-us.apache.org/repos/asf/mesos/tree/dedd1ed6 Diff: http://git-wip-us.apache.org/repos/asf/mesos/diff/dedd1ed6 Branch: refs/heads/master Commit: dedd1ed69611e143fefd6a246cc85943457efc39 Parents: ab75d1d Author: Jiang Yan Xu <[email protected]> Authored: Thu Aug 13 13:57:15 2015 -0700 Committer: Jiang Yan Xu <[email protected]> Committed: Thu Aug 13 15:16:23 2015 -0700 ---------------------------------------------------------------------- src/Makefile.am | 2 + .../containerizer/provisioners/appc/store.cpp | 198 +++++++++++++++++++ .../containerizer/provisioners/appc/store.hpp | 94 +++++++++ src/slave/flags.cpp | 5 + src/slave/flags.hpp | 3 + .../containerizer/appc_provisioner_tests.cpp | 72 +++++++ 6 files changed, 374 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/mesos/blob/dedd1ed6/src/Makefile.am ---------------------------------------------------------------------- diff --git a/src/Makefile.am b/src/Makefile.am index d664df6..b5dbdc3 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -459,6 +459,7 @@ libmesos_no_3rdparty_la_SOURCES = \ slave/containerizer/provisioner.cpp \ slave/containerizer/provisioners/appc/paths.cpp \ slave/containerizer/provisioners/appc/spec.cpp \ + slave/containerizer/provisioners/appc/store.cpp \ slave/resource_estimators/noop.cpp \ usage/usage.cpp \ v1/attributes.cpp \ @@ -715,6 +716,7 @@ libmesos_no_3rdparty_la_SOURCES += \ slave/containerizer/provisioner.hpp \ slave/containerizer/provisioners/appc/paths.hpp \ slave/containerizer/provisioners/appc/spec.hpp \ + slave/containerizer/provisioners/appc/store.hpp \ slave/containerizer/isolators/posix.hpp \ slave/containerizer/isolators/posix/disk.hpp \ slave/containerizer/isolators/cgroups/constants.hpp \ http://git-wip-us.apache.org/repos/asf/mesos/blob/dedd1ed6/src/slave/containerizer/provisioners/appc/store.cpp ---------------------------------------------------------------------- diff --git a/src/slave/containerizer/provisioners/appc/store.cpp b/src/slave/containerizer/provisioners/appc/store.cpp new file mode 100644 index 0000000..fbd1c53 --- /dev/null +++ b/src/slave/containerizer/provisioners/appc/store.cpp @@ -0,0 +1,198 @@ +/** + * 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 <glog/logging.h> + +#include <process/defer.hpp> +#include <process/dispatch.hpp> + +#include <stout/check.hpp> +#include <stout/hashmap.hpp> +#include <stout/os.hpp> +#include <stout/path.hpp> + +#include "slave/containerizer/provisioners/appc/paths.hpp" +#include "slave/containerizer/provisioners/appc/spec.hpp" +#include "slave/containerizer/provisioners/appc/store.hpp" + +using namespace process; + +using std::list; +using std::string; +using std::vector; + +namespace mesos { +namespace internal { +namespace slave { +namespace appc { + +class StoreProcess : public Process<StoreProcess> +{ +public: + StoreProcess(const string& root); + + ~StoreProcess() {} + + Future<Nothing> recover(); + + Future<std::vector<Store::Image>> get(const string& name); + +private: + // Absolute path to the root directory of the store as defined by + // --appc_store_dir. + const string root; + + // Mappings: name -> id -> image. + hashmap<string, hashmap<string, Store::Image>> images; +}; + + +Try<Owned<Store>> Store::create(const Flags& flags) +{ + Try<Nothing> mkdir = os::mkdir(paths::getImagesDir(flags.appc_store_dir)); + if (mkdir.isError()) { + return Error("Failed to create the images directory: " + mkdir.error()); + } + + // Make sure the root path is canonical so all image paths derived + // from it are canonical too. + Result<string> root = os::realpath(flags.appc_store_dir); + if (!root.isSome()) { + // The above mkdir call recursively creates the store directory + // if necessary so it cannot be None here. + CHECK_ERROR(root); + return Error( + "Failed to get the realpath of the store directory: " + root.error()); + } + + return Owned<Store>(new Store( + Owned<StoreProcess>(new StoreProcess(root.get())))); +} + + +Store::Store(Owned<StoreProcess> _process) + : process(_process) +{ + spawn(CHECK_NOTNULL(process.get())); +} + + +Store::~Store() +{ + terminate(process.get()); + wait(process.get()); +} + + +Future<Nothing> Store::recover() +{ + return dispatch(process.get(), &StoreProcess::recover); +} + + +Future<vector<Store::Image>> Store::get(const string& name) +{ + return dispatch(process.get(), &StoreProcess::get, name); +} + + +StoreProcess::StoreProcess(const string& _root) : root(_root) {} + + +// Implemented as a helper function because it's going to be used by 'put()'. +static Try<Store::Image> createImage(const string& imagePath) +{ + Option<Error> error = spec::validateLayout(imagePath); + if (error.isSome()) { + return Error("Invalid image layout: " + error.get().message); + } + + string imageId = Path(imagePath).basename(); + + error = spec::validateImageID(imageId); + if (error.isSome()) { + return Error("Invalid image ID: " + error.get().message); + } + + Try<string> read = os::read(paths::getImageManifestPath(imagePath)); + if (read.isError()) { + return Error("Failed to read manifest: " + read.error()); + } + + Try<AppcImageManifest> manifest = spec::parse(read.get()); + if (manifest.isError()) { + return Error("Failed to parse manifest: " + manifest.error()); + } + + return Store::Image(manifest.get(), imageId, imagePath); +} + + +Future<vector<Store::Image>> StoreProcess::get(const string& name) +{ + if (!images.contains(name)) { + return vector<Store::Image>(); + } + + vector<Store::Image> result; + foreach (const Store::Image& image, images[name].values()) { + result.push_back(image); + } + + return result; +} + + +Future<Nothing> StoreProcess::recover() +{ + // Recover everything in the store. + Try<list<string>> imageIds = os::ls(paths::getImagesDir(root)); + if (imageIds.isError()) { + return Failure( + "Failed to list images under '" + + paths::getImagesDir(root) + "': " + + imageIds.error()); + } + + foreach (const string& imageId, imageIds.get()) { + string path = paths::getImagePath(root, imageId); + if (!os::stat::isdir(path)) { + LOG(WARNING) << "Unexpected entry in storage: " << imageId; + continue; + } + + Try<Store::Image> image = createImage(path); + if (image.isError()) { + LOG(WARNING) << "Unexpected entry in storage: " << image.error(); + continue; + } + + LOG(INFO) << "Restored image '" << image.get().manifest.name() << "'"; + + images[image.get().manifest.name()].put(image.get().id, image.get()); + } + + return Nothing(); +} + +} // namespace appc { +} // namespace slave { +} // namespace internal { +} // namespace mesos { http://git-wip-us.apache.org/repos/asf/mesos/blob/dedd1ed6/src/slave/containerizer/provisioners/appc/store.hpp ---------------------------------------------------------------------- diff --git a/src/slave/containerizer/provisioners/appc/store.hpp b/src/slave/containerizer/provisioners/appc/store.hpp new file mode 100644 index 0000000..e48d91b --- /dev/null +++ b/src/slave/containerizer/provisioners/appc/store.hpp @@ -0,0 +1,94 @@ +/** + * 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. + */ + +#ifndef __MESOS_APPC_STORE__ +#define __MESOS_APPC_STORE__ + +#include <string> +#include <vector> + +#include <mesos/mesos.hpp> + +#include <process/future.hpp> +#include <process/owned.hpp> + +#include <stout/try.hpp> + +#include "slave/flags.hpp" + +namespace mesos { +namespace internal { +namespace slave { +namespace appc { + +// Forward declaration. +class StoreProcess; + + +// Provides the provisioner with access to locally stored / cached Appc images. +class Store +{ +public: + // Defines an image in the store (which has passed validation). + struct Image + { + Image( + const AppcImageManifest& _manifest, + const std::string& _id, + const std::string& _path) + : manifest(_manifest), id(_id), path(_path) {} + + const AppcImageManifest manifest; + + // Image ID of the format "sha512-value" where "value" is the hex + // encoded string of the sha512 digest of the uncompressed tar file + // of the image. + const std::string id; + + // Absolute path of the extracted image. + const std::string path; + }; + + static Try<process::Owned<Store>> create(const Flags& flags); + + ~Store(); + + process::Future<Nothing> recover(); + + // Get all images matched by name. + process::Future<std::vector<Image>> get(const std::string& name); + + // TODO(xujyan): Implement a put() method that fetches an image into + // the store. i.e., + // process::Future<StoredImage> put(const std::string& uri); + +private: + Store(process::Owned<StoreProcess> process); + + Store(const Store&); // Not copyable. + Store& operator=(const Store&); // Not assignable. + + process::Owned<StoreProcess> process; +}; + +} // namespace appc { +} // namespace slave { +} // namespace internal { +} // namespace mesos { + +#endif // __MESOS_APPC_STORE__ http://git-wip-us.apache.org/repos/asf/mesos/blob/dedd1ed6/src/slave/flags.cpp ---------------------------------------------------------------------- diff --git a/src/slave/flags.cpp b/src/slave/flags.cpp index 7aece2d..e43dd1c 100644 --- a/src/slave/flags.cpp +++ b/src/slave/flags.cpp @@ -64,6 +64,11 @@ mesos::internal::slave::Flags::Flags() "Comma separated list of image rootfs provisioners,\n" "e.g., appc,docker"); + add(&Flags::appc_store_dir, + "appc_store_dir", + "Directory the appc provisioner will store images in", + "/tmp/mesos/store/appc"); + add(&Flags::default_role, "default_role", "Any resources in the --resources flag that\n" http://git-wip-us.apache.org/repos/asf/mesos/blob/dedd1ed6/src/slave/flags.hpp ---------------------------------------------------------------------- diff --git a/src/slave/flags.hpp b/src/slave/flags.hpp index 881d494..e56738e 100644 --- a/src/slave/flags.hpp +++ b/src/slave/flags.hpp @@ -46,7 +46,10 @@ public: Option<std::string> hostname; Option<std::string> resources; std::string isolation; + Option<std::string> provisioners; + std::string appc_store_dir; + std::string default_role; Option<std::string> attributes; Bytes fetcher_cache_size; http://git-wip-us.apache.org/repos/asf/mesos/blob/dedd1ed6/src/tests/containerizer/appc_provisioner_tests.cpp ---------------------------------------------------------------------- diff --git a/src/tests/containerizer/appc_provisioner_tests.cpp b/src/tests/containerizer/appc_provisioner_tests.cpp index 1335cfd..47b66b9 100644 --- a/src/tests/containerizer/appc_provisioner_tests.cpp +++ b/src/tests/containerizer/appc_provisioner_tests.cpp @@ -27,6 +27,7 @@ #include <stout/stringify.hpp> #include "slave/containerizer/provisioners/appc/spec.hpp" +#include "slave/containerizer/provisioners/appc/store.hpp" #include "tests/utils.hpp" @@ -109,6 +110,77 @@ TEST_F(AppcProvisionerTest, ValidateLayout) EXPECT_NONE(spec::validateLayout(image)); } + +TEST_F(AppcProvisionerTest, StoreRecover) +{ + // Create store. + slave::Flags flags; + flags.appc_store_dir = path::join(os::getcwd(), "store"); + Try<Owned<Store>> store = Store::create(flags); + ASSERT_SOME(store); + + // Create a simple image in the store: + // <store> + // |--images + // |--<id> + // |--manifest + // |--rootfs/tmp/test + JSON::Value manifest = JSON::parse( + "{" + " \"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(); + + // 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. + string imageId = + "sha512-e77d96aa0240eedf134b8c90baeaf76dca8e78691836301d7498c84020446042e" + "797b296d6ab296e0954c2626bfb264322ebeb8f447dac4fac6511ea06bc61f0"; + + string imagePath = path::join(flags.appc_store_dir, "images", imageId); + + ASSERT_SOME(os::mkdir(path::join(imagePath, "rootfs", "tmp"))); + ASSERT_SOME( + os::write(path::join(imagePath, "rootfs", "tmp", "test"), "test")); + ASSERT_SOME( + os::write(path::join(imagePath, "manifest"), stringify(manifest))); + + // Recover the image from disk. + AWAIT_READY(store.get()->recover()); + + Future<vector<Store::Image>> _images = store.get()->get("foo.com/bar"); + AWAIT_READY(_images); + + vector<Store::Image> images = _images.get(); + + EXPECT_EQ(1u, images.size()); + ASSERT_SOME(os::realpath(imagePath)); + EXPECT_EQ(os::realpath(imagePath).get(), images.front().path); +} + } // namespace tests { } // namespace internal { } // namespace mesos {
