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 {

Reply via email to