blob: 33df60065903228749833bbad20449ba8784594a [file]
// 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 <utility>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <stout/duration.hpp>
#include <stout/gtest.hpp>
#include <stout/json.hpp>
#include <stout/os.hpp>
#include <stout/path.hpp>
#include <stout/stringify.hpp>
#include <process/address.hpp>
#include <process/clock.hpp>
#include <process/future.hpp>
#include <process/gmock.hpp>
#include <process/io.hpp>
#include <process/owned.hpp>
#include <process/socket.hpp>
#include <process/subprocess.hpp>
#include <process/ssl/gtest.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_client.hpp"
#include "slave/containerizer/mesos/provisioner/docker/registry_puller.hpp"
#include "slave/containerizer/mesos/provisioner/docker/store.hpp"
#include "slave/containerizer/mesos/provisioner/docker/token_manager.hpp"
#include "tests/mesos.hpp"
#include "tests/utils.hpp"
namespace io = process::io;
namespace slave = mesos::internal::slave;
namespace spec = ::docker::spec;
using std::list;
using std::map;
using std::pair;
using std::string;
using std::vector;
using process::Clock;
using process::Future;
using process::Owned;
using process::Promise;
using process::Subprocess;
using process::network::Socket;
using slave::docker::parseImageName;
using slave::docker::Puller;
using slave::docker::RegistryPuller;
using slave::docker::paths::getImageLayerRootfsPath;
using slave::docker::registry::RegistryClient;
using slave::docker::registry::Token;
using slave::docker::registry::TokenManager;
namespace mesos {
namespace internal {
namespace tests {
// TODO(jieyu): Remove this test in favor of using
// DockerSpecTest.ParseImageReference.
TEST(DockerUtilsTest, ParseImageName)
{
slave::docker::Image::Name name;
name = parseImageName("library/busybox");
EXPECT_FALSE(name.has_registry());
EXPECT_EQ("library/busybox", name.repository());
EXPECT_EQ("latest", name.tag());
name = parseImageName("busybox");
EXPECT_FALSE(name.has_registry());
EXPECT_EQ("busybox", name.repository());
EXPECT_EQ("latest", name.tag());
name = parseImageName("library/busybox:tag");
EXPECT_FALSE(name.has_registry());
EXPECT_EQ("library/busybox", name.repository());
EXPECT_EQ("tag", name.tag());
// Note that the digest is stored as a tag.
name = parseImageName(
"library/busybox"
"@sha256:bc8813ea7b3603864987522f02a7"
"6101c17ad122e1c46d790efc0fca78ca7bfb");
EXPECT_FALSE(name.has_registry());
EXPECT_EQ("library/busybox", name.repository());
EXPECT_EQ("sha256:bc8813ea7b3603864987522f02a7"
"6101c17ad122e1c46d790efc0fca78ca7bfb",
name.tag());
name = parseImageName("registry.io/library/busybox");
EXPECT_EQ("registry.io", name.registry());
EXPECT_EQ("library/busybox", name.repository());
EXPECT_EQ("latest", name.tag());
name = parseImageName("registry.io/library/busybox:tag");
EXPECT_EQ("registry.io", name.registry());
EXPECT_EQ("library/busybox", name.repository());
EXPECT_EQ("tag", name.tag());
name = parseImageName("registry.io:80/library/busybox:tag");
EXPECT_EQ("registry.io:80", name.registry());
EXPECT_EQ("library/busybox", name.repository());
EXPECT_EQ("tag", name.tag());
// Note that the digest is stored as a tag.
name = parseImageName(
"registry.io:80/library/busybox"
"@sha256:bc8813ea7b3603864987522f02a7"
"6101c17ad122e1c46d790efc0fca78ca7bfb");
EXPECT_EQ("registry.io:80", name.registry());
EXPECT_EQ("library/busybox", name.repository());
EXPECT_EQ("sha256:bc8813ea7b3603864987522f02a7"
"6101c17ad122e1c46d790efc0fca78ca7bfb",
name.tag());
}
/**
* Provides token operations and defaults.
*/
class TokenHelper {
protected:
const string hdrBase64 = base64::encode(
"{ \
\"alg\":\"ES256\", \
\"typ\":\"JWT\", \
\"x5c\":[\"test\"] \
}");
string getClaimsBase64() const
{
return base64::encode(claimsJsonString);
}
string getTokenString() const
{
return hdrBase64 + "." + getClaimsBase64() + "." + signBase64;
}
string getDefaultTokenString()
{
// Construct response and send(server side).
const double expirySecs = Clock::now().secs() + Days(365).secs();
claimsJsonString =
"{\"access\" \
:[ \
{ \
\"type\":\"repository\", \
\"name\":\"library/busybox\", \
\"actions\":[\"pull\"]}], \
\"aud\":\"registry.docker.io\", \
\"exp\":" + stringify(expirySecs) + ", \
\"iat\":1438887168, \
\"iss\":\"auth.docker.io\", \
\"jti\":\"l2PJDFkzwvoL7-TajJF7\", \
\"nbf\":1438887166, \
\"sub\":\"\" \
}";
return getTokenString();
}
const string signBase64 = base64::encode("{\"\"}");
string claimsJsonString;
};
/**
* Fixture for testing TokenManager component.
*/
class RegistryTokenTest : public TokenHelper, public ::testing::Test {};
// Tests JSON Web Token parsing for a valid token string.
TEST_F(RegistryTokenTest, ValidToken)
{
const double expirySecs = Clock::now().secs() + Days(365).secs();
claimsJsonString =
"{\"access\" \
:[ \
{ \
\"type\":\"repository\", \
\"name\":\"library/busybox\", \
\"actions\":[\"pull\"]}], \
\"aud\":\"registry.docker.io\", \
\"exp\":" + stringify(expirySecs) + ", \
\"iat\":1438887168, \
\"iss\":\"auth.docker.io\", \
\"jti\":\"l2PJDFkzwvoL7-TajJF7\", \
\"nbf\":1438887166, \
\"sub\":\"\" \
}";
Try<Token> token = Token::create(getTokenString());
ASSERT_SOME(token);
}
// Tests JSON Web Token parsing for a token string with expiration
// date in the past.
TEST_F(RegistryTokenTest, ExpiredToken)
{
// Use an arbitrary fixed date that is far in the past (12 weeks
// after the Unix epoch).
const double expirySecs = Weeks(12).secs();
claimsJsonString =
"{\"access\" \
:[ \
{ \
\"type\":\"repository\", \
\"name\":\"library/busybox\", \
\"actions\":[\"pull\"]}], \
\"aud\":\"registry.docker.io\", \
\"exp\":" + stringify(expirySecs) + ", \
\"iat\":1438887166, \
\"iss\":\"auth.docker.io\", \
\"jti\":\"l2PJDFkzwvoL7-TajJF7\", \
\"nbf\":1438887166, \
\"sub\":\"\" \
}";
Try<Token> token = Token::create(getTokenString());
EXPECT_ERROR(token);
}
// Tests JSON Web Token parsing for a token string with no expiration date.
TEST_F(RegistryTokenTest, NoExpiration)
{
claimsJsonString =
"{\"access\" \
:[ \
{ \
\"type\":\"repository\", \
\"name\":\"library/busybox\", \
\"actions\":[\"pull\"]}], \
\"aud\":\"registry.docker.io\", \
\"iat\":1438887166, \
\"iss\":\"auth.docker.io\", \
\"jti\":\"l2PJDFkzwvoL7-TajJF7\", \
\"nbf\":1438887166, \
\"sub\":\"\" \
}";
const Try<Token> token = Token::create(getTokenString());
ASSERT_SOME(token);
}
// Tests JSON Web Token parsing for a token string with not-before date in the
// future.
TEST_F(RegistryTokenTest, NotBeforeInFuture)
{
const double expirySecs = Clock::now().secs() + Days(365).secs();
const double nbfSecs = Clock::now().secs() + Days(7).secs();
claimsJsonString =
"{\"access\" \
:[ \
{ \
\"type\":\"repository\", \
\"name\":\"library/busybox\", \
\"actions\":[\"pull\"]}], \
\"aud\":\"registry.docker.io\", \
\"exp\":" + stringify(expirySecs) + ", \
\"iat\":1438887166, \
\"iss\":\"auth.docker.io\", \
\"jti\":\"l2PJDFkzwvoL7-TajJF7\", \
\"nbf\":" + stringify(nbfSecs) + ", \
\"sub\":\"\" \
}";
const Try<Token> token = Token::create(getTokenString());
ASSERT_SOME(token);
ASSERT_EQ(token.get().isValid(), false);
}
#ifdef USE_SSL_SOCKET
// Test suite for docker registry tests.
class RegistryClientTest : public virtual SSLTest, public TokenHelper
{
protected:
RegistryClientTest() {}
Try<Socket> getServer() {
return setup_server({
{"SSL_ENABLED", "true"},
{"SSL_KEY_FILE", key_path().value},
{"SSL_CERT_FILE", certificate_path().value}});
}
};
// Tests TokenManager for a simple token request.
TEST_F(RegistryClientTest, SimpleGetToken)
{
Try<Socket> server = getServer();
ASSERT_SOME(server);
ASSERT_SOME(server.get().address());
ASSERT_SOME(server.get().address().get().hostname());
Future<Socket> socket = server.get().accept();
// Create URL from server hostname and port.
const process::http::URL url(
"https",
server.get().address().get().hostname().get(),
server.get().address().get().port);
Try<Owned<TokenManager>> tokenMgr = TokenManager::create(url);
ASSERT_SOME(tokenMgr);
Future<Token> token =
tokenMgr.get()->getToken(
"registry.docker.io",
"repository:library/busybox:pull",
None());
AWAIT_ASSERT_READY(socket);
// Construct response and send(server side).
const double expirySecs = Clock::now().secs() + Days(365).secs();
claimsJsonString =
"{\"access\" \
:[ \
{ \
\"type\":\"repository\", \
\"name\":\"library/busybox\", \
\"actions\":[\"pull\"]}], \
\"aud\":\"registry.docker.io\", \
\"exp\":" + stringify(expirySecs) + ", \
\"iat\":1438887168, \
\"iss\":\"auth.docker.io\", \
\"jti\":\"l2PJDFkzwvoL7-TajJF7\", \
\"nbf\":1438887166, \
\"sub\":\"\" \
}";
const string tokenString(getTokenString());
const string tokenResponse = "{\"token\":\"" + tokenString + "\"}";
const string buffer =
string("HTTP/1.1 200 OK\r\n") +
"Content-Length : " +
stringify(tokenResponse.length()) + "\r\n" +
"\r\n" +
tokenResponse;
AWAIT_ASSERT_READY(Socket(socket.get()).send(buffer));
AWAIT_ASSERT_READY(token);
ASSERT_EQ(token.get().raw, tokenString);
}
// Tests TokenManager for bad token response from server.
TEST_F(RegistryClientTest, BadTokenResponse)
{
Try<Socket> server = getServer();
ASSERT_SOME(server);
ASSERT_SOME(server.get().address());
ASSERT_SOME(server.get().address().get().hostname());
Future<Socket> socket = server.get().accept();
// Create URL from server hostname and port.
const process::http::URL url(
"https",
server.get().address().get().hostname().get(),
server.get().address().get().port);
Try<Owned<TokenManager>> tokenMgr = TokenManager::create(url);
ASSERT_SOME(tokenMgr);
Future<Token> token =
tokenMgr.get()->getToken(
"registry.docker.io",
"repository:library/busybox:pull",
None());
AWAIT_ASSERT_READY(socket);
const string tokenString("bad token");
const string tokenResponse = "{\"token\":\"" + tokenString + "\"}";
const string buffer =
string("HTTP/1.1 200 OK\r\n") +
"Content-Length : " +
stringify(tokenResponse.length()) + "\r\n" +
"\r\n" +
tokenResponse;
AWAIT_ASSERT_READY(Socket(socket.get()).send(buffer));
AWAIT_FAILED(token);
}
// Tests TokenManager for request to invalid server.
TEST_F(RegistryClientTest, BadTokenServerAddress)
{
// Create an invalid URL with current time.
const process::http::URL url("https", stringify(Clock::now().secs()), 0);
Try<Owned<TokenManager>> tokenMgr = TokenManager::create(url);
ASSERT_SOME(tokenMgr);
Future<Token> token =
tokenMgr.get()->getToken(
"registry.docker.io",
"repository:library/busybox:pull",
None());
AWAIT_FAILED(token);
}
// Tests docker registry's getManifest API.
TEST_F(RegistryClientTest, SimpleGetManifest)
{
Try<Socket> server = getServer();
ASSERT_SOME(server);
ASSERT_SOME(server.get().address());
ASSERT_SOME(server.get().address().get().hostname());
Future<Socket> socket = server.get().accept();
const process::http::URL url(
"https",
server.get().address().get().hostname().get(),
server.get().address().get().port);
Try<Owned<RegistryClient>> registryClient =
RegistryClient::create(url, url, None());
ASSERT_SOME(registryClient);
Future<spec::v2::ImageManifest> manifestResponse =
registryClient.get()->getManifest(parseImageName("library/busybox"));
const string unauthResponseHeaders = "Www-Authenticate: Bearer"
" realm=\"https://auth.docker.io/token\","
"service=" + stringify(server.get().address().get()) + ","
"scope=\"repository:library/busybox:pull\"";
const string unauthHttpResponse =
string("HTTP/1.1 401 Unauthorized\r\n") +
unauthResponseHeaders + "\r\n" +
"\r\n";
AWAIT_ASSERT_READY(socket);
// Send 401 Unauthorized response for a manifest request.
Future<string> manifestHttpRequest = Socket(socket.get()).recv();
AWAIT_ASSERT_READY(manifestHttpRequest);
AWAIT_ASSERT_READY(Socket(socket.get()).send(unauthHttpResponse));
// Token response.
socket = server.get().accept();
AWAIT_ASSERT_READY(socket);
Future<string> tokenRequest = Socket(socket.get()).recv();
AWAIT_ASSERT_READY(tokenRequest);
const string tokenResponse =
"{\"token\":\"" + getDefaultTokenString() + "\"}";
const string tokenHttpResponse =
string("HTTP/1.1 200 OK\r\n") +
"Content-Length : " +
stringify(tokenResponse.length()) + "\r\n" +
"\r\n" +
tokenResponse;
AWAIT_ASSERT_READY(Socket(socket.get()).send(tokenHttpResponse));
// Manifest response.
socket = server.get().accept();
AWAIT_ASSERT_READY(socket);
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_yperuhfefF4uvTbKO2L45gYGDs5bIEgO"
"EarAs7_4dbEV5u-W7uR8gF6EDKfowUCmTq3a5vEOJ3w\","
" \"protected\": \"eyJmb3JtYXRMZW5ndGgiOjYwNjMsImZvcm1hdFRhaWwiOiJ"
"DbjAiLCJ0aW1lIjoiMjAxNC0wOS0xMVQxNzoxNDozMFoifQ\""
" }"
" ]"
"}";
const string manifestHttpResponse =
string("HTTP/1.1 200 OK\r\n") +
"Content-Length : " +
stringify(manifestJSON.length()) + "\r\n" +
"Docker-Content-Digest: "
"sha256:df9e13f36d2d5b30c16bfbf2a6110c45ebed0bfa1ea42d357651bc6c736d5322"
+ "\r\n" +
"\r\n" +
manifestJSON;
AWAIT_ASSERT_READY(Socket(socket.get()).send(manifestHttpResponse));
AWAIT_ASSERT_READY(manifestResponse);
EXPECT_EQ(
manifestResponse.get().history(2).v1().id(),
"1ce2e90b0bc7224de3db1f0d646fe8e2c4dd37f1793928287f6074bc451a57ea");
EXPECT_EQ(
manifestResponse.get().history(1).v1().id(),
"2ce2e90b0bc7224de3db1f0d646fe8e2c4dd37f1793928287f6074bc451a57ea");
EXPECT_EQ(
manifestResponse.get().history(0).v1().id(),
"3ce2e90b0bc7224de3db1f0d646fe8e2c4dd37f1793928287f6074bc451a57ea");
}
// Tests docker registry's getBlob API.
TEST_F(RegistryClientTest, SimpleGetBlob)
{
Try<Socket> server = getServer();
ASSERT_SOME(server);
ASSERT_SOME(server.get().address());
ASSERT_SOME(server.get().address().get().hostname());
Future<Socket> socket = server.get().accept();
Try<Socket> blobServer = getServer();
ASSERT_SOME(blobServer);
ASSERT_SOME(blobServer.get().address());
ASSERT_SOME(blobServer.get().address().get().hostname());
Future<Socket> blobServerAcceptSocket = blobServer.get().accept();
const process::http::URL url(
"https",
server.get().address().get().hostname().get(),
server.get().address().get().port);
Try<Owned<RegistryClient>> registryClient =
RegistryClient::create(url, url, None());
ASSERT_SOME(registryClient);
const Path blobPath(path::join(os::getcwd(), "blob"));
Future<size_t> result =
registryClient.get()->getBlob(
parseImageName("blob"),
"digest",
blobPath);
const string unauthResponseHeaders = "WWW-Authenticate: Bearer"
" realm=\"https://auth.docker.io/token\","
"service=" + stringify(server.get().address().get()) + ","
"scope=\"repository:library/busybox:pull\"";
const string unauthHttpResponse =
string("HTTP/1.1 401 Unauthorized\r\n") +
unauthResponseHeaders + "\r\n" +
"\r\n";
AWAIT_ASSERT_READY(socket);
// Send 401 Unauthorized response.
Future<string> blobHttpRequest = Socket(socket.get()).recv();
AWAIT_ASSERT_READY(blobHttpRequest);
AWAIT_ASSERT_READY(Socket(socket.get()).send(unauthHttpResponse));
// Send token response.
socket = server.get().accept();
AWAIT_ASSERT_READY(socket);
Future<string> tokenRequest = Socket(socket.get()).recv();
AWAIT_ASSERT_READY(tokenRequest);
const string tokenResponse =
"{\"token\":\"" + getDefaultTokenString() + "\"}";
const string tokenHttpResponse =
string("HTTP/1.1 200 OK\r\n") +
"Content-Length : " +
stringify(tokenResponse.length()) + "\r\n" +
"\r\n" +
tokenResponse;
AWAIT_ASSERT_READY(Socket(socket.get()).send(tokenHttpResponse));
// Send redirect.
socket = server.get().accept();
AWAIT_ASSERT_READY(socket);
blobHttpRequest = Socket(socket.get()).recv();
AWAIT_ASSERT_READY(blobHttpRequest);
const string redirectHttpResponse =
string("HTTP/1.1 307 Temporary Redirect\r\n") +
"Location: https://" +
blobServer.get().address().get().hostname().get() + ":" +
stringify(blobServer.get().address().get().port) + "/blob \r\n" +
"\r\n";
AWAIT_ASSERT_READY(Socket(socket.get()).send(redirectHttpResponse));
// Finally send blob response.
AWAIT_ASSERT_READY(blobServerAcceptSocket);
blobHttpRequest = Socket(blobServerAcceptSocket.get()).recv();
AWAIT_ASSERT_READY(blobHttpRequest);
const string blobResponse = stringify(Clock::now());
const string blobHttpResponse =
string("HTTP/1.1 200 OK\r\n") +
"Content-Length : " +
stringify(blobResponse.length()) + "\r\n" +
"\r\n" +
blobResponse;
AWAIT_ASSERT_READY(Socket(blobServerAcceptSocket.get()).send(
blobHttpResponse));
AWAIT_ASSERT_READY(result);
Try<string> blob = os::read(blobPath);
ASSERT_SOME(blob);
ASSERT_EQ(blob.get(), blobResponse);
}
TEST_F(RegistryClientTest, BadRequest)
{
Try<Socket> server = getServer();
ASSERT_SOME(server);
ASSERT_SOME(server.get().address());
ASSERT_SOME(server.get().address().get().hostname());
Future<Socket> socket = server.get().accept();
const process::http::URL url(
"https",
server.get().address().get().hostname().get(),
server.get().address().get().port);
Try<Owned<RegistryClient>> registryClient =
RegistryClient::create(url, url, None());
ASSERT_SOME(registryClient);
const Path blobPath(path::join(os::getcwd(), "blob"));
Future<size_t> result =
registryClient.get()->getBlob(
parseImageName("blob"),
"digest",
blobPath);
const string badRequestResponse =
"{\"errors\": [{\"message\": \"Error1\" }, {\"message\": \"Error2\"}]}";
const string badRequestHttpResponse =
string("HTTP/1.1 400 Bad Request\r\n") +
"Content-Length : " + stringify(badRequestResponse.length()) + "\r\n" +
"\r\n" +
badRequestResponse;
AWAIT_ASSERT_READY(socket);
// Send 400 Bad Request.
Future<string> blobHttpRequest = Socket(socket.get()).recv();
AWAIT_ASSERT_READY(blobHttpRequest);
AWAIT_ASSERT_READY(Socket(socket.get()).send(badRequestHttpResponse));
AWAIT_FAILED(result);
ASSERT_TRUE(strings::contains(result.failure(), "Error1"));
ASSERT_TRUE(strings::contains(result.failure(), "Error2"));
}
// Tests docker RegistryPuller component. It simulates pulling an image layer
// from remote registry and then verifies the content saved on disk.
TEST_F(RegistryClientTest, SimpleRegistryPuller)
{
Try<Socket> server = getServer();
ASSERT_SOME(server);
ASSERT_SOME(server.get().address());
ASSERT_SOME(server.get().address().get().hostname());
Future<Socket> socket = server.get().accept();
Try<Socket> blobServer = getServer();
ASSERT_SOME(blobServer);
ASSERT_SOME(blobServer.get().address());
ASSERT_SOME(blobServer.get().address().get().hostname());
Future<Socket> blobServerAcceptSock = blobServer.get().accept();
slave::Flags flags;
process::network::Address address = server.get().address().get();
const string url = "https://" + address.hostname().get() + ":" +
stringify(address.port);
flags.docker_registry = url;
flags.docker_auth_server = url;
Try<Owned<Puller>> registryPuller = RegistryPuller::create(flags);
ASSERT_SOME(registryPuller);
const Path registryPullerPath(os::getcwd());
Try<slave::docker::Image::Name> imageName = parseImageName("busybox");
ASSERT_SOME(imageName);
Future<list<pair<string, string>>> registryPullerFuture =
registryPuller.get()->pull(imageName.get(), registryPullerPath);
const string unauthResponseHeaders = "WWW-Authenticate: Bearer"
" realm=\"https://auth.docker.io/token\","
"service=" + stringify(server.get().address().get()) + ","
"scope=\"repository:library/busybox:pull\"";
const string unauthHttpResponse =
string("HTTP/1.1 401 Unauthorized\r\n") +
unauthResponseHeaders + "\r\n" +
"\r\n";
AWAIT_ASSERT_READY(socket);
// Send 401 Unauthorized response for a manifest request.
Future<string> registryPullerHttpRequestFuture = Socket(socket.get()).recv();
AWAIT_ASSERT_READY(registryPullerHttpRequestFuture);
AWAIT_ASSERT_READY(Socket(socket.get()).send(unauthHttpResponse));
// Token response.
socket = server.get().accept();
AWAIT_ASSERT_READY(socket);
Future<string> tokenRequestFuture = Socket(socket.get()).recv();
AWAIT_ASSERT_READY(tokenRequestFuture);
const string tokenResponse =
"{\"token\":\"" + getDefaultTokenString() + "\"}";
const string tokenHttpResponse =
string("HTTP/1.1 200 OK\r\n") +
"Content-Length : " +
stringify(tokenResponse.length()) + "\r\n" +
"\r\n" +
tokenResponse;
AWAIT_ASSERT_READY(Socket(socket.get()).send(tokenHttpResponse));
// Manifest response.
socket = server.get().accept();
AWAIT_ASSERT_READY(socket);
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_yperuhfefF4uvTbKO2L45gYGDs5bIEgO"
"EarAs7_4dbEV5u-W7uR8gF6EDKfowUCmTq3a5vEOJ3w\","
" \"protected\": \"eyJmb3JtYXRMZW5ndGgiOjYwNjMsImZvcm1hdFRhaWwiOiJ"
"DbjAiLCJ0aW1lIjoiMjAxNC0wOS0xMVQxNzoxNDozMFoifQ\""
" }"
" ]"
"}";
const string manifestHttpResponse =
string("HTTP/1.1 200 OK\r\n") +
"Content-Length : " +
stringify(manifestResponse.length()) + "\r\n" +
"Docker-Content-Digest: "
"sha256:df9e13f36d2d5b30c16bfbf2a6110c45ebed0bfa1ea42d357651bc6c736d5322"
+ "\r\n" +
"\r\n" +
manifestResponse;
AWAIT_ASSERT_READY(Socket(socket.get()).send(manifestHttpResponse));
// Redirect response.
socket = server.get().accept();
AWAIT_ASSERT_READY(socket);
registryPullerHttpRequestFuture = Socket(socket.get()).recv();
AWAIT_ASSERT_READY(registryPullerHttpRequestFuture);
const string redirectHttpResponse =
string("HTTP/1.1 307 Temporary Redirect\r\n") +
"Content-Length : 0\r\n" +
"Location: https://" +
blobServer.get().address().get().hostname().get() + ":" +
stringify(blobServer.get().address().get().port) + "/blob \r\n" +
"\r\n";
AWAIT_ASSERT_READY(Socket(socket.get()).send(redirectHttpResponse));
AWAIT_ASSERT_READY(blobServerAcceptSock);
registryPullerHttpRequestFuture = Socket(blobServerAcceptSock.get()).recv();
AWAIT_ASSERT_READY(registryPullerHttpRequestFuture);
// Prepare the blob response from the server. The blob response buffer is a
// tarball. So we create a tarball of our test response and send that.
const string blobFile = "blob";
const string blobResponse = "hello docker";
Path blobPath(path::join(registryPullerPath, blobFile));
ASSERT_SOME(os::write(blobPath, blobResponse));
Path blobTarPath(path::join(registryPullerPath, blobFile + ".tar"));
vector<string> argv = {
"tar",
"-C",
registryPullerPath,
"-c",
"-f",
blobTarPath,
blobFile
};
Try<Subprocess> s = subprocess(
"tar",
argv,
Subprocess::PATH("/dev/null"),
Subprocess::PATH("/dev/null"),
Subprocess::PATH("/dev/null"));
ASSERT_SOME(s);
AWAIT_ASSERT_READY(s.get().status());
Try<Bytes> tarSize = os::stat::size(blobTarPath);
ASSERT_SOME(tarSize);
ASSERT_SOME(os::rm(blobPath));
std::unique_ptr<char[]> tarBuffer(new char[tarSize.get().bytes()]);
ASSERT_NE(tarBuffer.get(), nullptr);
Try<int> fd = os::open(
blobTarPath,
O_RDONLY,
S_IRUSR | S_IRGRP | S_IROTH);
ASSERT_SOME(fd);
ASSERT_SOME(os::nonblock(fd.get()));
AWAIT_ASSERT_READY(
io::read(fd.get(), tarBuffer.get(), tarSize.get().bytes()));
ASSERT_SOME(os::close(fd.get()));
const string blobHttpResponse =
string("HTTP/1.1 200 OK\r\n") +
"Content-type : application/octet-stream\r\n" +
"Content-Length : " +
stringify(tarSize.get().bytes()) + "\r\n" +
"\r\n";
const size_t blobResponseSize =
blobHttpResponse.length() + tarSize.get().bytes();
std::unique_ptr<char[]> responseBuffer(new char[blobResponseSize]);
ASSERT_NE(responseBuffer.get(), nullptr);
memcpy(
responseBuffer.get(),
blobHttpResponse.c_str(),
blobHttpResponse.length());
memcpy(
responseBuffer.get() + blobHttpResponse.length(),
tarBuffer.get(),
tarSize.get().bytes());
AWAIT_ASSERT_READY(Socket(blobServerAcceptSock.get()).send(
responseBuffer.get(),
blobResponseSize));
AWAIT_ASSERT_READY(registryPullerFuture);
list<pair<string, string>> layers = registryPullerFuture.get();
ASSERT_EQ(1u, layers.size());
ASSERT_EQ(layers.front().first,
"1ce2e90b0bc7224de3db1f0d646fe8e2c4dd37f1793928287f6074bc451a57ea");
Try<string> blob = os::read(
path::join(layers.front().second, "rootfs", blobFile));
ASSERT_SOME(blob);
ASSERT_EQ(blob.get(), blobResponse);
}
#endif // USE_SSL_SOCKET
class ProvisionerDockerLocalStoreTest : public TemporaryDirectoryTest
{
public:
void verifyLocalDockerImage(
const slave::Flags& flags,
const vector<string>& layers)
{
const string layersPath = path::join(flags.docker_store_dir, "layers");
// Verify contents of the image in store directory.
const string layerPath1 =
getImageLayerRootfsPath(flags.docker_store_dir, "123");
const string layerPath2 =
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:latest");
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:latest.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: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");
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 = "file://" + 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<list<pair<string, string>>>(
const slave::docker::Image::Name&,
const Path&));
Future<list<pair<string, string>>> unmocked_pull(
const slave::docker::Image::Name& name,
const Path& directory)
{
// TODO(gilbert): Allow return list to be overridden.
return list<pair<string, 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;
Promise<list<pair<string, string>>> promise;
EXPECT_CALL(*puller, pull(_, _))
.WillOnce(testing::DoAll(FutureSatisfy(&pull),
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);
const string rootfsPath1 = path::join(os::getcwd(), "rootfs1");
const string rootfsPath2 = path::join(os::getcwd(), "rootfs2");
Try<Nothing> mkdir1 = os::mkdir(rootfsPath1);
ASSERT_SOME(mkdir1);
Try<Nothing> mkdir2 = os::mkdir(rootfsPath2);
ASSERT_SOME(mkdir2);
ASSERT_TRUE(imageInfo1.isPending());
Future<slave::ImageInfo> imageInfo2 = store.get()->get(mesosImage);
const std::list<std::pair<std::string, std::string>> result =
{{"123", rootfsPath1},
{"456", rootfsPath2}};
ASSERT_TRUE(imageInfo2.isPending());
promise.set(result);
AWAIT_READY(imageInfo1);
AWAIT_READY(imageInfo2);
EXPECT_EQ(imageInfo1.get().layers, imageInfo2.get().layers);
}
} // namespace tests {
} // namespace internal {
} // namespace mesos {