You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mesos.apache.org by tn...@apache.org on 2015/11/24 23:47:45 UTC

[3/3] mesos git commit: Refactored Docker registry client to use protobuf messages.

Refactored Docker registry client to use protobuf messages.

Review: https://reviews.apache.org/r/39712


Project: http://git-wip-us.apache.org/repos/asf/mesos/repo
Commit: http://git-wip-us.apache.org/repos/asf/mesos/commit/48bc61a2
Tree: http://git-wip-us.apache.org/repos/asf/mesos/tree/48bc61a2
Diff: http://git-wip-us.apache.org/repos/asf/mesos/diff/48bc61a2

Branch: refs/heads/master
Commit: 48bc61a2e37d8845d3d6a3beaa9cd89ffe9ee09e
Parents: 95c5691
Author: Gilbert Song <so...@gmail.com>
Authored: Tue Nov 24 14:23:42 2015 -0800
Committer: Timothy Chen <tn...@gmail.com>
Committed: Tue Nov 24 22:47:30 2015 +0000

----------------------------------------------------------------------
 .../provisioner/docker/registry_client.cpp      | 128 ++--------
 .../provisioner/docker/registry_client.hpp      |  26 +-
 .../provisioner/docker/registry_puller.cpp      |  45 ++--
 .../containerizer/provisioner_docker_tests.cpp  | 238 +++++++++----------
 4 files changed, 162 insertions(+), 275 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mesos/blob/48bc61a2/src/slave/containerizer/mesos/provisioner/docker/registry_client.cpp
----------------------------------------------------------------------
diff --git a/src/slave/containerizer/mesos/provisioner/docker/registry_client.cpp b/src/slave/containerizer/mesos/provisioner/docker/registry_client.cpp
index 99a421b..e69bab4 100644
--- a/src/slave/containerizer/mesos/provisioner/docker/registry_client.cpp
+++ b/src/slave/containerizer/mesos/provisioner/docker/registry_client.cpp
@@ -37,6 +37,7 @@
 #include <stout/try.hpp>
 
 #include "slave/containerizer/mesos/provisioner/docker/registry_client.hpp"
+#include "slave/containerizer/mesos/provisioner/docker/spec.hpp"
 #include "slave/containerizer/mesos/provisioner/docker/token_manager.hpp"
 
 using std::string;
@@ -69,7 +70,7 @@ public:
       const http::URL& authenticationServer,
       const Option<Credentials>& credentials);
 
-  Future<Manifest> getManifest(
+  Future<DockerImageManifest> getManifest(
       const Image::Name& imageName);
 
   Future<size_t> getBlob(
@@ -169,7 +170,7 @@ RegistryClient::~RegistryClient()
 }
 
 
-Future<Manifest> RegistryClient::getManifest(
+Future<DockerImageManifest> RegistryClient::getManifest(
     const Image::Name& imageName)
 {
   return dispatch(
@@ -551,111 +552,6 @@ Future<http::Response> RegistryClientProcess::doHttpGet(
 }
 
 
-Try<Manifest> Manifest::create(const string& jsonString)
-{
-    Try<JSON::Object> manifestJSON = JSON::parse<JSON::Object>(jsonString);
-
-    if (manifestJSON.isError()) {
-      return Error(manifestJSON.error());
-    }
-
-    Result<JSON::String> name = manifestJSON.get().find<JSON::String>("name");
-    if (name.isNone()) {
-      return Error("Failed to find \"name\" in manifest response");
-    }
-
-    Result<JSON::Array> fsLayersJSON =
-      manifestJSON.get().find<JSON::Array>("fsLayers");
-
-    if (fsLayersJSON.isNone()) {
-      return Error("Failed to find \"fsLayers\" in manifest response");
-    }
-
-    Result<JSON::Array> historyArray =
-      manifestJSON.get().find<JSON::Array>("history");
-
-    if (historyArray.isNone()) {
-      return Error("Failed to find \"history\" in manifest response");
-    }
-
-    if (historyArray.get().values.size() != fsLayersJSON.get().values.size()) {
-      return Error(
-          "\"history\" and \"fsLayers\" array count mismatch"
-          "in manifest response");
-    }
-
-    vector<FileSystemLayerInfo> fsLayers;
-
-    // We add layers in reverse order because 'fsLayers' in the manifest
-    // response is ordered with the latest layer on the top. When we apply the
-    // layer changes, we want the filesystem modification order to be the same
-    // as its history(oldest layer applied first).
-    for (size_t index = fsLayersJSON.get().values.size(); index-- > 0; ) {
-      const JSON::Value& layer = fsLayersJSON.get().values[index];
-
-      if (!layer.is<JSON::Object>()) {
-        return Error(
-            "Failed to parse layer as a JSON object for index: " +
-            stringify(index));
-      }
-
-      const JSON::Object& layerInfoJSON = layer.as<JSON::Object>();
-
-      // Get blobsum for layer.
-      const Result<JSON::String> blobSumInfo =
-        layerInfoJSON.find<JSON::String>("blobSum");
-
-      if (blobSumInfo.isNone()) {
-        return Error("Failed to find \"blobSum\" in manifest response");
-      }
-
-      // Get history for layer.
-      if (!historyArray.get().values[index].is<JSON::Object>()) {
-        return Error(
-            "Failed to parse history as a JSON object for index: " +
-            stringify(index));
-      }
-      const JSON::Object& historyObj =
-        historyArray.get().values[index].as<JSON::Object>();
-
-      // Get layer id.
-      const Result<JSON::String> v1CompatibilityJSON =
-        historyObj.find<JSON::String>("v1Compatibility");
-
-      if (!v1CompatibilityJSON.isSome()) {
-        return Error(
-            "Failed to obtain layer v1 compability json in manifest for layer: "
-            + stringify(index));
-      }
-
-      Try<JSON::Object> v1CompatibilityObj =
-        JSON::parse<JSON::Object>(v1CompatibilityJSON.get().value);
-
-      if (!v1CompatibilityObj.isSome()) {
-        return Error(
-            "Failed to parse v1 compability json in manifest for layer: "
-            + stringify(index));
-      }
-
-      const Result<JSON::String> id =
-        v1CompatibilityObj.get().find<JSON::String>("id");
-
-      if (!id.isSome()) {
-        return Error(
-            "Failed to find \"id\" in manifest for layer: " + stringify(index));
-      }
-
-      fsLayers.emplace_back(
-          FileSystemLayerInfo{
-            blobSumInfo.get().value,
-            id.get().value,
-          });
-    }
-
-    return Manifest{name.get().value, fsLayers};
-}
-
-
 // TODO(tnachen): Support other Docker registry API versions.
 string RegistryClientProcess::getAPIVersion() const
 {
@@ -676,7 +572,7 @@ string RegistryClientProcess::getRepositoryPath(
 }
 
 
-Future<Manifest> RegistryClientProcess::getManifest(
+Future<DockerImageManifest> RegistryClientProcess::getManifest(
     const Image::Name& imageName)
 {
   http::URL manifestURL(registryServer_);
@@ -685,16 +581,28 @@ Future<Manifest> RegistryClientProcess::getManifest(
 
   return doHttpGet(manifestURL, None(), false, true, None())
     .then(defer(self(), [this] (
-        const http::Response& response) -> Future<Manifest> {
+        const http::Response& response) -> Future<DockerImageManifest> {
       // TODO(jojy): We dont use the digest that is returned in header.
       // This is a good place to validate the manifest.
 
-      Try<Manifest> manifest = Manifest::create(response.body);
+      Try<DockerImageManifest> manifest =
+        spec::parse(JSON::parse<JSON::Object>(response.body).get());
       if (manifest.isError()) {
         return Failure(
             "Failed to parse manifest response: " + manifest.error());
       }
 
+      // We reverse the order of fsLayers and history because in
+      // manifest response the latest layer is on the top.
+      // We assume fslayers and history has the same length as
+      // it's already checked in spec::parse.
+      for (int i = 0; i < manifest.get().fslayers_size() / 2; i++) {
+        manifest.get().mutable_fslayers()->SwapElements(i,
+          manifest.get().fslayers_size() - 1 - i);
+        manifest.get().mutable_history()->SwapElements(i,
+          manifest.get().fslayers_size() - 1 - i);
+      }
+
       return manifest.get();
     }));
 }

http://git-wip-us.apache.org/repos/asf/mesos/blob/48bc61a2/src/slave/containerizer/mesos/provisioner/docker/registry_client.hpp
----------------------------------------------------------------------
diff --git a/src/slave/containerizer/mesos/provisioner/docker/registry_client.hpp b/src/slave/containerizer/mesos/provisioner/docker/registry_client.hpp
index 2287327..9b02b6f 100644
--- a/src/slave/containerizer/mesos/provisioner/docker/registry_client.hpp
+++ b/src/slave/containerizer/mesos/provisioner/docker/registry_client.hpp
@@ -44,30 +44,6 @@ class RegistryClientProcess;
 // TODO(bmahler): Replace these with the existing protobuf counterparts.
 
 
-struct FileSystemLayerInfo
-{
-  // TODO(jojy): This includes both the checksum and
-  // the type of the checksum; separate these.
-  const std::string checksumInfo;
-  const std::string layerId;
-};
-
-
-/**
- * Response for a "GET Manifest" request.
- *
- * Reference:
- * https://github.com/docker/distribution/blob/master/docs/spec/manifest-v2-1.md
- */
-struct Manifest
-{
-  static Try<Manifest> create(const std::string& jsonString);
-
-  const std::string name;
-  const std::vector<FileSystemLayerInfo> fsLayerInfos;
-};
-
-
 /**
  * Authentication credentials for the client sessions.
  */
@@ -103,7 +79,7 @@ public:
    * @return Manifest on success.
    *         Failure on process failure.
    */
-  process::Future<Manifest> getManifest(
+  process::Future<DockerImageManifest> getManifest(
       const Image::Name& imageName);
 
 

http://git-wip-us.apache.org/repos/asf/mesos/blob/48bc61a2/src/slave/containerizer/mesos/provisioner/docker/registry_puller.cpp
----------------------------------------------------------------------
diff --git a/src/slave/containerizer/mesos/provisioner/docker/registry_puller.cpp b/src/slave/containerizer/mesos/provisioner/docker/registry_puller.cpp
index 94b7539..89f61c2 100644
--- a/src/slave/containerizer/mesos/provisioner/docker/registry_puller.cpp
+++ b/src/slave/containerizer/mesos/provisioner/docker/registry_puller.cpp
@@ -47,8 +47,6 @@ namespace internal {
 namespace slave {
 namespace docker {
 
-using FileSystemLayerInfo = registry::FileSystemLayerInfo;
-using Manifest = registry::Manifest;
 using RegistryClient = registry::RegistryClient;
 
 
@@ -69,10 +67,11 @@ private:
   Future<pair<string, string>> downloadLayer(
       const Image::Name& imageName,
       const Path& directory,
-      const FileSystemLayerInfo& layer);
+      const string& blobSum,
+      const string& id);
 
   Future<list<pair<string, string>>> downloadLayers(
-      const Manifest& manifest,
+      const DockerImageManifest& manifest,
       const Image::Name& imageName,
       const Path& downloadDir);
 
@@ -190,46 +189,47 @@ RegistryPullerProcess::RegistryPullerProcess(
 Future<pair<string, string>> RegistryPullerProcess::downloadLayer(
     const Image::Name& imageName,
     const Path& directory,
-    const FileSystemLayerInfo& layer)
+    const string& blobSum,
+    const string& layerId)
 {
-  VLOG(1) << "Downloading layer '"  << layer.layerId
+  VLOG(1) << "Downloading layer '"  << layerId
           << "' for image '" << stringify(imageName) << "'";
 
-  if (downloadTracker_.contains(layer.layerId)) {
+  if (downloadTracker_.contains(layerId)) {
     VLOG(1) << "Download already in progress for image '"
-            << stringify(imageName) << "', layer '" << layer.layerId << "'";
+            << stringify(imageName) << "', layer '" << layerId << "'";
 
-    return downloadTracker_.at(layer.layerId)->future();
+    return downloadTracker_.at(layerId)->future();
   }
 
   Owned<Promise<pair<string, string>>> downloadPromise(
       new Promise<pair<string, string>>());
 
-  downloadTracker_.insert({layer.layerId, downloadPromise});
+  downloadTracker_.insert({layerId, downloadPromise});
 
-  const Path downloadFile(path::join(directory, layer.layerId + ".tar"));
+  const Path downloadFile(path::join(directory, layerId + ".tar"));
 
   registryClient_->getBlob(
       imageName,
-      layer.checksumInfo,
+      blobSum,
       downloadFile)
     .onAny(process::defer(
         self(),
-        [this, layer, downloadPromise, downloadFile](
+        [this, layerId, downloadPromise, downloadFile](
             const Future<size_t>& future) {
-          downloadTracker_.erase(layer.layerId);
+          downloadTracker_.erase(layerId);
 
           if (!future.isReady()) {
               downloadPromise->fail(
-                  "Failed to download layer '" + layer.layerId + "': " +
+                  "Failed to download layer '" + layerId + "': " +
                   (future.isFailed() ? future.failure() : "future discarded"));
           } else if (future.get() == 0) {
             // We don't expect Docker registry to return empty response
             // even with empty layers.
             downloadPromise->fail(
-                "Failed to download layer '" + layer.layerId + "': no content");
+                "Failed to download layer '" + layerId + "': no content");
           } else {
-            downloadPromise->set({layer.layerId, downloadFile});
+            downloadPromise->set({layerId, downloadFile});
           }
         }));
 
@@ -244,7 +244,7 @@ Future<list<pair<string, string>>> RegistryPullerProcess::pull(
   // TODO(jojy): Have one outgoing manifest request per image.
   return registryClient_->getManifest(imageName)
     .then(process::defer(self(), [this, directory, imageName](
-        const Manifest& manifest) {
+        const DockerImageManifest& manifest) {
       return downloadLayers(manifest, imageName, directory);
     }))
     .then(process::defer(self(), [this, directory](
@@ -262,15 +262,18 @@ Future<list<pair<string, string>>> RegistryPullerProcess::pull(
 
 
 Future<list<pair<string, string>>> RegistryPullerProcess::downloadLayers(
-    const Manifest& manifest,
+    const DockerImageManifest& manifest,
     const Image::Name& imageName,
     const Path& directory)
 {
   list<Future<pair<string, string>>> downloadFutures;
 
-  foreach (const FileSystemLayerInfo& layer, manifest.fsLayerInfos) {
+  for (int i = 0; i < manifest.fslayers_size(); i++) {
     downloadFutures.push_back(
-        downloadLayer(imageName, directory, layer));
+        downloadLayer(imageName,
+                      directory,
+                      manifest.fslayers(i).blobsum(),
+                      manifest.history(i).v1compatibility().id()));
   }
 
   // TODO(jojy): Delete downloaded files in the directory on discard and

http://git-wip-us.apache.org/repos/asf/mesos/blob/48bc61a2/src/tests/containerizer/provisioner_docker_tests.cpp
----------------------------------------------------------------------
diff --git a/src/tests/containerizer/provisioner_docker_tests.cpp b/src/tests/containerizer/provisioner_docker_tests.cpp
index 88b914d..34d9762 100644
--- a/src/tests/containerizer/provisioner_docker_tests.cpp
+++ b/src/tests/containerizer/provisioner_docker_tests.cpp
@@ -688,7 +688,7 @@ TEST_F(RegistryClientTest, SimpleGetManifest)
 
   ASSERT_SOME(registryClient);
 
-  Future<Manifest> manifestResponse =
+  Future<DockerImageManifest> manifestResponse =
     registryClient.get()->getManifest(parseImageName("library/busybox"));
 
   const string unauthResponseHeaders = "Www-Authenticate: Bearer"
@@ -734,74 +734,75 @@ TEST_F(RegistryClientTest, SimpleGetManifest)
   manifestHttpRequest = Socket(socket.get()).recv();
   AWAIT_ASSERT_READY(manifestHttpRequest);
 
-  const string manifestJSON = " \
-    { \
-      \"schemaVersion\": 1, \
-      \"name\": \"library/busybox\", \
-      \"tag\": \"latest\",  \
-      \"architecture\": \"amd64\",  \
-      \"fsLayers\": [ \
-        { \
-          \"blobSum\": \
-  \"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4\"  \
-        },  \
-        { \
-          \"blobSum\": \
-  \"sha256:1db09adb5ddd7f1a07b6d585a7db747a51c7bd17418d47e91f901bdf420abd66\"  \
-        },  \
-        { \
-          \"blobSum\": \
-  \"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4\"  \
-        } \
-      ],  \
-    \"history\": [  \
-      { \
-        \"v1Compatibility\": \
-          \"{\\\"id\\\": \
-    \\\"1ce2e90b0bc7224de3db1f0d646fe8e2c4dd37f1793928287f6074bc451a57ea\\\", \
-            \\\"parent\\\": \
-    \\\"cf2616975b4a3cba083ca99bc3f0bf25f5f528c3c52be1596b30f60b0b1c37ff\\\" \
-            }\" \
-      }, \
-      { \
-        \"v1Compatibility\": \
-          \"{\\\"id\\\": \
-    \\\"2ce2e90b0bc7224de3db1f0d646fe8e2c4dd37f1793928287f6074bc451a57ea\\\", \
-            \\\"parent\\\": \
-    \\\"cf2616975b4a3cba083ca99bc3f0bf25f5f528c3c52be1596b30f60b0b1c37ff\\\" \
-            }\" \
-      }, \
-      { \
-        \"v1Compatibility\": \
-          \"{\\\"id\\\": \
-    \\\"3ce2e90b0bc7224de3db1f0d646fe8e2c4dd37f1793928287f6074bc451a57ea\\\", \
-            \\\"parent\\\": \
-    \\\"cf2616975b4a3cba083ca99bc3f0bf25f5f528c3c52be1596b30f60b0b1c37ff\\\" \
-            }\" \
-      } \
-    ], \
-       \"signatures\": [  \
-          { \
-             \"header\": {  \
-                \"jwk\": {  \
-                   \"crv\": \"P-256\",  \
-                   \"kid\": \
-           \"OOI5:SI3T:LC7D:O7DX:FY6S:IAYW:WDRN:VQEM:BCFL:OIST:Q3LO:GTQQ\",  \
-                   \"kty\": \"EC\", \
-                   \"x\": \"J2N5ePGhlblMI2cdsR6NrAG_xbNC_X7s1HRtk5GXvzM\", \
-                   \"y\": \"Idr-tEBjnNnfq6_71aeXBi3Z9ah_rrE209l4wiaohk0\" \
-                },  \
-                \"alg\": \"ES256\"  \
-             }, \
-             \"signature\": \
-\"65vq57TakC_yperuhfefF4uvTbKO2L45gYGDs5bIEgOEarAs7_"
-"4dbEV5u-W7uR8gF6EDKfowUCmTq3a5vEOJ3w\", \
-       \"protected\": \
-       \"eyJmb3JtYXRMZW5ndGgiOjUwNTgsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNS"
-       "0wOC0xMVQwMzo0Mjo1OVoifQ\"  \
-          } \
-       ]  \
-    }";
+  const string manifestJSON =
+    "{"
+    "   \"schemaVersion\": 1,"
+    "   \"name\": \"library/busybox\","
+    "   \"tag\": \"latest\","
+    "   \"architecture\": \"amd64\","
+    "   \"fsLayers\": ["
+    "      {"
+    "         \"blobSum\": "
+  "\"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4\""
+    "      },"
+    "      {"
+    "         \"blobSum\": "
+  "\"sha256:1db09adb5ddd7f1a07b6d585a7db747a51c7bd17418d47e91f901bdf420abd66\""
+    "      },"
+    "      {"
+    "         \"blobSum\": "
+  "\"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4\""
+    "      }"
+    "   ],"
+    "   \"history\": ["
+    "      {"
+    "         \"v1Compatibility\": "
+    "           {"
+    "             \"id\": "
+    "\"1ce2e90b0bc7224de3db1f0d646fe8e2c4dd37f1793928287f6074bc451a57ea\","
+    "             \"parent\": "
+    "\"cf2616975b4a3cba083ca99bc3f0bf25f5f528c3c52be1596b30f60b0b1c37ff\""
+    "           }"
+    "      },"
+    "      {"
+    "         \"v1Compatibility\": "
+    "           {"
+    "             \"id\": "
+    "\"2ce2e90b0bc7224de3db1f0d646fe8e2c4dd37f1793928287f6074bc451a57ea\","
+    "             \"parent\": "
+    "\"cf2616975b4a3cba083ca99bc3f0bf25f5f528c3c52be1596b30f60b0b1c37ff\""
+    "           }"
+    "      },"
+    "      {"
+    "         \"v1Compatibility\": "
+    "           {"
+    "             \"id\": "
+    "\"3ce2e90b0bc7224de3db1f0d646fe8e2c4dd37f1793928287f6074bc451a57ea\","
+    "             \"parent\": "
+    "\"cf2616975b4a3cba083ca99bc3f0bf25f5f528c3c52be1596b30f60b0b1c37ff\""
+    "           }"
+    "      }"
+    "   ],"
+    "   \"signatures\": ["
+    "      {"
+    "         \"header\": {"
+    "            \"jwk\": {"
+    "               \"crv\": \"P-256\","
+    "               \"kid\": "
+    "\"OOI5:SI3T:LC7D:O7DX:FY6S:IAYW:WDRN:VQEM:BCFL:OIST:Q3LO:GTQQ\","
+    "               \"kty\": \"EC\","
+    "               \"x\": \"J2N5ePGhlblMI2cdsR6NrAG_xbNC_X7s1HRtk5GXvzM\","
+    "               \"y\": \"Idr-tEBjnNnfq6_71aeXBi3Z9ah_rrE209l4wiaohk0\""
+    "            },"
+    "            \"alg\": \"ES256\""
+    "         },"
+    "         \"signature\": \"65vq57TakC_yperuhfefF4uvTbKO2L45gYGDs5bIEgO"
+    "EarAs7_4dbEV5u-W7uR8gF6EDKfowUCmTq3a5vEOJ3w\","
+    "         \"protected\": \"eyJmb3JtYXRMZW5ndGgiOjYwNjMsImZvcm1hdFRhaWwiOiJ"
+    "DbjAiLCJ0aW1lIjoiMjAxNC0wOS0xMVQxNzoxNDozMFoifQ\""
+    "      }"
+    "   ]"
+    "}";
 
   const string manifestHttpResponse =
     string("HTTP/1.1 200 OK\r\n") +
@@ -817,16 +818,16 @@ TEST_F(RegistryClientTest, SimpleGetManifest)
 
   AWAIT_ASSERT_READY(manifestResponse);
 
-  ASSERT_EQ(
-      manifestResponse.get().fsLayerInfos[2].layerId,
+  EXPECT_EQ(
+      manifestResponse.get().history(2).v1compatibility().id(),
       "1ce2e90b0bc7224de3db1f0d646fe8e2c4dd37f1793928287f6074bc451a57ea");
 
-  ASSERT_EQ(
-      manifestResponse.get().fsLayerInfos[1].layerId,
+  EXPECT_EQ(
+      manifestResponse.get().history(1).v1compatibility().id(),
       "2ce2e90b0bc7224de3db1f0d646fe8e2c4dd37f1793928287f6074bc451a57ea");
 
-  ASSERT_EQ(
-      manifestResponse.get().fsLayerInfos[0].layerId,
+  EXPECT_EQ(
+      manifestResponse.get().history(0).v1compatibility().id(),
       "3ce2e90b0bc7224de3db1f0d646fe8e2c4dd37f1793928287f6074bc451a57ea");
 }
 
@@ -1063,50 +1064,49 @@ TEST_F(RegistryClientTest, SimpleRegistryPuller)
   registryPullerHttpRequestFuture = Socket(socket.get()).recv();
   AWAIT_ASSERT_READY(registryPullerHttpRequestFuture);
 
-  const string manifestResponse = " \
-    { \
-      \"schemaVersion\": 1, \
-      \"name\": \"library/busybox\", \
-      \"tag\": \"latest\",  \
-      \"architecture\": \"amd64\",  \
-      \"fsLayers\": [ \
-        { \
-          \"blobSum\": \
-  \"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4\"  \
-        } \
-      ],  \
-    \"history\": [  \
-      { \
-        \"v1Compatibility\": \
-          \"{\\\"id\\\": \
-    \\\"1ce2e90b0bc7224de3db1f0d646fe8e2c4dd37f1793928287f6074bc451a57ea\\\", \
-            \\\"parent\\\": \
-    \\\"cf2616975b4a3cba083ca99bc3f0bf25f5f528c3c52be1596b30f60b0b1c37ff\\\" \
-            }\" \
-      } \
-    ], \
-       \"signatures\": [  \
-          { \
-             \"header\": {  \
-                \"jwk\": {  \
-                   \"crv\": \"P-256\",  \
-                   \"kid\": \
-           \"OOI5:SI3T:LC7D:O7DX:FY6S:IAYW:WDRN:VQEM:BCFL:OIST:Q3LO:GTQQ\",  \
-                   \"kty\": \"EC\", \
-                   \"x\": \"J2N5ePGhlblMI2cdsR6NrAG_xbNC_X7s1HRtk5GXvzM\", \
-                   \"y\": \"Idr-tEBjnNnfq6_71aeXBi3Z9ah_rrE209l4wiaohk0\" \
-                },  \
-                \"alg\": \"ES256\"  \
-             }, \
-             \"signature\": \
-\"65vq57TakC_yperuhfefF4uvTbKO2L45gYGDs5bIEgOEarAs7_"
-"4dbEV5u-W7uR8gF6EDKfowUCmTq3a5vEOJ3w\", \
-       \"protected\": \
-       \"eyJmb3JtYXRMZW5ndGgiOjUwNTgsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNS"
-       "0wOC0xMVQwMzo0Mjo1OVoifQ\"  \
-          } \
-       ]  \
-    }";
+  const string manifestResponse =
+    "{"
+    "   \"schemaVersion\": 1,"
+    "   \"name\": \"library/busybox\","
+    "   \"tag\": \"latest\","
+    "   \"architecture\": \"amd64\","
+    "   \"fsLayers\": ["
+    "      {"
+    "         \"blobSum\": "
+  "\"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4\""
+    "      }"
+    "   ],"
+    "   \"history\": ["
+    "      {"
+    "         \"v1Compatibility\": "
+    "           {"
+    "             \"id\": "
+    "\"1ce2e90b0bc7224de3db1f0d646fe8e2c4dd37f1793928287f6074bc451a57ea\","
+    "             \"parent\": "
+    "\"cf2616975b4a3cba083ca99bc3f0bf25f5f528c3c52be1596b30f60b0b1c37ff\""
+    "           }"
+    "      }"
+    "   ],"
+    "   \"signatures\": ["
+    "      {"
+    "         \"header\": {"
+    "            \"jwk\": {"
+    "               \"crv\": \"P-256\","
+    "               \"kid\": "
+    "\"OOI5:SI3T:LC7D:O7DX:FY6S:IAYW:WDRN:VQEM:BCFL:OIST:Q3LO:GTQQ\","
+    "               \"kty\": \"EC\","
+    "               \"x\": \"J2N5ePGhlblMI2cdsR6NrAG_xbNC_X7s1HRtk5GXvzM\","
+    "               \"y\": \"Idr-tEBjnNnfq6_71aeXBi3Z9ah_rrE209l4wiaohk0\""
+    "            },"
+    "            \"alg\": \"ES256\""
+    "         },"
+    "         \"signature\": \"65vq57TakC_yperuhfefF4uvTbKO2L45gYGDs5bIEgO"
+    "EarAs7_4dbEV5u-W7uR8gF6EDKfowUCmTq3a5vEOJ3w\","
+    "         \"protected\": \"eyJmb3JtYXRMZW5ndGgiOjYwNjMsImZvcm1hdFRhaWwiOiJ"
+    "DbjAiLCJ0aW1lIjoiMjAxNC0wOS0xMVQxNzoxNDozMFoifQ\""
+    "      }"
+    "   ]"
+    "}";
 
   const string manifestHttpResponse =
     string("HTTP/1.1 200 OK\r\n") +