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/09/09 20:11:50 UTC
mesos git commit: Added TokenManager for Docker provisioner remote
store.
Repository: mesos
Updated Branches:
refs/heads/master 5c8065e9a -> b72fb052b
Added TokenManager for Docker provisioner remote store.
Review: https://reviews.apache.org/r/37427
Project: http://git-wip-us.apache.org/repos/asf/mesos/repo
Commit: http://git-wip-us.apache.org/repos/asf/mesos/commit/b72fb052
Tree: http://git-wip-us.apache.org/repos/asf/mesos/tree/b72fb052
Diff: http://git-wip-us.apache.org/repos/asf/mesos/diff/b72fb052
Branch: refs/heads/master
Commit: b72fb052bb2ca9f754b9d2c40e7f76df97dc55ae
Parents: 5c8065e
Author: Jojy Varghese <jo...@mesosphere.io>
Authored: Wed Sep 9 10:29:28 2015 -0700
Committer: Timothy Chen <tn...@apache.org>
Committed: Wed Sep 9 11:10:04 2015 -0700
----------------------------------------------------------------------
src/Makefile.am | 9 +-
.../provisioners/docker/token_manager.cpp | 361 +++++++++++++++++++
.../provisioners/docker/token_manager.hpp | 179 +++++++++
.../provisioners/docker_provisioner_tests.cpp | 354 ++++++++++++++++++
4 files changed, 900 insertions(+), 3 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/mesos/blob/b72fb052/src/Makefile.am
----------------------------------------------------------------------
diff --git a/src/Makefile.am b/src/Makefile.am
index 0a8ef6d..4ef58cd 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -483,12 +483,13 @@ libmesos_no_3rdparty_la_SOURCES = \
slave/containerizer/mesos/launch.cpp \
slave/containerizer/provisioner.cpp \
slave/containerizer/provisioners/appc.cpp \
- slave/containerizer/provisioners/paths.cpp \
+ slave/containerizer/provisioners/paths.cpp \
slave/containerizer/provisioners/appc/paths.cpp \
slave/containerizer/provisioners/appc/spec.cpp \
slave/containerizer/provisioners/appc/store.cpp \
slave/containerizer/provisioners/backend.cpp \
- slave/containerizer/provisioners/backends/copy.cpp \
+ slave/containerizer/provisioners/backends/copy.cpp \
+ slave/containerizer/provisioners/docker/token_manager.cpp \
slave/resource_estimators/noop.cpp \
usage/usage.cpp \
v1/attributes.cpp \
@@ -763,13 +764,14 @@ libmesos_no_3rdparty_la_SOURCES += \
slave/containerizer/linux_launcher.hpp \
slave/containerizer/provisioner.hpp \
slave/containerizer/provisioners/appc.hpp \
- slave/containerizer/provisioners/paths.hpp \
+ slave/containerizer/provisioners/paths.hpp \
slave/containerizer/provisioners/appc/paths.hpp \
slave/containerizer/provisioners/appc/spec.hpp \
slave/containerizer/provisioners/appc/store.hpp \
slave/containerizer/provisioners/backend.hpp \
slave/containerizer/provisioners/backends/bind.hpp \
slave/containerizer/provisioners/backends/copy.hpp \
+ slave/containerizer/provisioners/docker/token_manager.hpp \
slave/containerizer/isolators/posix.hpp \
slave/containerizer/isolators/posix/disk.hpp \
slave/containerizer/isolators/cgroups/constants.hpp \
@@ -1659,6 +1661,7 @@ mesos_tests_SOURCES = \
tests/paths_tests.cpp \
tests/persistent_volume_tests.cpp \
tests/protobuf_io_tests.cpp \
+ tests/provisioners/docker_provisioner_tests.cpp \
tests/rate_limiting_tests.cpp \
tests/reconciliation_tests.cpp \
tests/registrar_tests.cpp \
http://git-wip-us.apache.org/repos/asf/mesos/blob/b72fb052/src/slave/containerizer/provisioners/docker/token_manager.cpp
----------------------------------------------------------------------
diff --git a/src/slave/containerizer/provisioners/docker/token_manager.cpp b/src/slave/containerizer/provisioners/docker/token_manager.cpp
new file mode 100644
index 0000000..aec915f
--- /dev/null
+++ b/src/slave/containerizer/provisioners/docker/token_manager.cpp
@@ -0,0 +1,361 @@
+/**
+ * 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 <process/defer.hpp>
+#include <process/dispatch.hpp>
+
+#include "slave/containerizer/provisioners/docker/token_manager.hpp"
+
+using std::hash;
+using std::string;
+using std::vector;
+
+using process::Clock;
+using process::Failure;
+using process::Future;
+using process::Owned;
+using process::Process;
+using process::Time;
+
+using process::http::Request;
+using process::http::Response;
+using process::http::URL;
+
+namespace mesos {
+namespace internal {
+namespace slave {
+namespace docker {
+namespace registry {
+
+class TokenManagerProcess : public Process<TokenManagerProcess>
+{
+public:
+ static Try<Owned<TokenManagerProcess>> create(const URL& realm);
+
+ Future<Token> getToken(
+ const string& service,
+ const string& scope,
+ const Option<string>& account);
+
+private:
+ static const string TOKEN_PATH_PREFIX;
+ static const Duration RESPONSE_TIMEOUT;
+
+ TokenManagerProcess(const URL& realm)
+ : realm_(realm) {}
+
+ Try<Token> getTokenFromResponse(const Response& response) const;
+
+ /**
+ * Key for the token cache.
+ */
+ struct TokenCacheKey
+ {
+ string service;
+ string scope;
+ };
+
+ struct TokenCacheKeyHash
+ {
+ size_t operator()(const TokenCacheKey& key) const
+ {
+ hash<string> hashFn;
+
+ return (hashFn(key.service) ^
+ (hashFn(key.scope) << 1));
+ }
+ };
+
+ struct TokenCacheKeyEqual
+ {
+ bool operator()(
+ const TokenCacheKey& left,
+ const TokenCacheKey& right) const
+ {
+ return ((left.service == right.service) &&
+ (left.scope == right.scope));
+ }
+ };
+
+ typedef hashmap<
+ const TokenCacheKey,
+ Token,
+ TokenCacheKeyHash,
+ TokenCacheKeyEqual> TokenCacheType;
+
+ const URL realm_;
+ TokenCacheType tokenCache_;
+
+ TokenManagerProcess(const TokenManagerProcess&) = delete;
+ TokenManagerProcess& operator=(const TokenManagerProcess&) = delete;
+};
+
+const Duration TokenManagerProcess::RESPONSE_TIMEOUT = Seconds(10);
+const string TokenManagerProcess::TOKEN_PATH_PREFIX = "/v2/token/";
+
+
+Token::Token(
+ const string& _raw,
+ const JSON::Object& _header,
+ const JSON::Object& _claims,
+ const Option<Time>& _expiration,
+ const Option<Time>& _notBefore)
+ : raw(_raw),
+ header(_header),
+ claims(_claims),
+ expiration(_expiration),
+ notBefore(_notBefore) {}
+
+
+Try<Token> Token::create(const string& raw)
+{
+ auto decode = [](
+ const string& segment) -> Try<JSON::Object> {
+ const auto padding = segment.length() % 4;
+ string paddedSegment(segment);
+
+ if (padding) {
+ paddedSegment.append(padding, '=');
+ }
+
+ Try<string> decoded = base64::decode(paddedSegment);
+ if (decoded.isError()) {
+ return Error(decoded.error());
+ }
+
+ return JSON::parse<JSON::Object>(decoded.get());
+ };
+
+ const vector<string> tokens = strings::tokenize(raw, ".");
+
+ if (tokens.size() != 3) {
+ return Error("Invalid raw token string");
+ }
+
+ Try<JSON::Object> header = decode(tokens[0]);
+ if (header.isError()) {
+ return Error("Failed to decode 'header' segment: " + header.error());
+ }
+
+ Try<JSON::Object> claims = decode(tokens[1]);
+ if (claims.isError()) {
+ return Error("Failed to decode 'claims' segment: " + claims.error());
+ }
+
+ Result<Time> expirationTime = getTimeValue(claims.get(), "exp");
+ if (expirationTime.isError()) {
+ return Error("Failed to decode expiration time: " + expirationTime.error());
+ }
+
+ Option<Time> expiration;
+ if (expirationTime.isSome()) {
+ expiration = expirationTime.get();
+ }
+
+ Result<Time> notBeforeTime = getTimeValue(claims.get(), "nbf");
+ if (notBeforeTime.isError()) {
+ return Error("Failed to decode not-before time: " + notBeforeTime.error());
+ }
+
+ Option<Time> notBefore;
+ if (notBeforeTime.isSome()) {
+ notBefore = notBeforeTime.get();
+ }
+
+ Token token(raw, header.get(), claims.get(), expiration, notBefore);
+
+ if (token.isExpired()) {
+ return Error("Token has expired");
+ }
+
+ // TODO(jojy): Add signature validation.
+ return token;
+}
+
+
+Result<Time> Token::getTimeValue(const JSON::Object& object, const string& key)
+{
+ Result<JSON::Number> jsonValue = object.find<JSON::Number>(key);
+
+ Option<Time> timeValue;
+
+ // If expiration is provided, we will process it for future validations.
+ if (jsonValue.isSome()) {
+ Try<Time> time = Time::create(jsonValue.get().value);
+ if (time.isError()) {
+ return Error("Failed to decode time: " + time.error());
+ }
+
+ timeValue = time.get();
+ }
+
+ return timeValue;
+}
+
+
+bool Token::isExpired() const
+{
+ if (expiration.isSome()) {
+ return (Clock::now() >= expiration.get());
+ }
+
+ return false;
+}
+
+
+bool Token::isValid() const
+{
+ if (!isExpired()) {
+ if (notBefore.isSome()) {
+ return (Clock::now() >= notBefore.get());
+ }
+
+ return true;
+ }
+
+ // TODO(jojy): Add signature validation.
+ return false;
+}
+
+
+Try<Owned<TokenManager>> TokenManager::create(
+ const URL& realm)
+{
+ Try<Owned<TokenManagerProcess>> process = TokenManagerProcess::create(realm);
+ if (process.isError()) {
+ return Error(process.error());
+ }
+
+ return Owned<TokenManager>(new TokenManager(process.get()));
+}
+
+
+TokenManager::TokenManager(Owned<TokenManagerProcess>& process)
+ : process_(process)
+{
+ spawn(CHECK_NOTNULL(process_.get()));
+}
+
+
+TokenManager::~TokenManager()
+{
+ terminate(process_.get());
+ process::wait(process_.get());
+}
+
+
+Future<Token> TokenManager::getToken(
+ const string& service,
+ const string& scope,
+ const Option<string>& account)
+{
+ return dispatch(
+ process_.get(),
+ &TokenManagerProcess::getToken,
+ service,
+ scope,
+ account);
+}
+
+
+Try<Owned<TokenManagerProcess>> TokenManagerProcess::create(const URL& realm)
+{
+ return Owned<TokenManagerProcess>(new TokenManagerProcess(realm));
+}
+
+
+Try<Token> TokenManagerProcess::getTokenFromResponse(
+ const Response& response) const
+{
+ Try<JSON::Object> tokenJSON = JSON::parse<JSON::Object>(response.body);
+ if (tokenJSON.isError()) {
+ return Error(tokenJSON.error());
+ }
+
+ Result<JSON::String> tokenString =
+ tokenJSON.get().find<JSON::String>("token");
+
+ if (tokenString.isError()) {
+ return Error(tokenString.error());
+ }
+
+ Try<Token> result = Token::create(tokenString.get().value);
+ if (result.isError()) {
+ return Error(result.error());
+ }
+
+ return result.get();;
+}
+
+
+Future<Token> TokenManagerProcess::getToken(
+ const string& service,
+ const string& scope,
+ const Option<string>& account)
+{
+ const TokenCacheKey tokenKey = {service, scope};
+
+ if (tokenCache_.contains(tokenKey)) {
+ Token token = tokenCache_.at(tokenKey);
+
+ if (token.isValid()) {
+ return token;
+ } else {
+ LOG(WARNING) << "Cached token was invalid. Will fetch once again";
+ }
+ }
+
+ URL tokenUrl = realm_;
+ tokenUrl.path = TOKEN_PATH_PREFIX;
+
+ tokenUrl.query = {
+ {"service", service},
+ {"scope", scope},
+ };
+
+ if (account.isSome()) {
+ tokenUrl.query.insert({"account", account.get()});
+ }
+
+ return process::http::get(tokenUrl, None())
+ .after(RESPONSE_TIMEOUT, [] (Future<Response> resp) -> Future<Response> {
+ resp.discard();
+ return Failure("Timeout waiting for response to token request");
+ })
+ .then(defer(self(), [this, tokenKey](
+ const Future<Response>& response) -> Future<Token> {
+ Try<Token> token = getTokenFromResponse(response.get());
+ if (token.isError()) {
+ return Failure(
+ "Failed to parse JSON Web Token object from response: " +
+ token.error());
+ }
+
+ tokenCache_.insert({tokenKey, token.get()});
+
+ return token.get();
+ }));
+}
+
+// TODO(jojy): Add implementation for basic authentication based getToken API.
+
+} // namespace registry {
+} // namespace docker {
+} // namespace slave {
+} // namespace internal {
+} // namespace mesos {
http://git-wip-us.apache.org/repos/asf/mesos/blob/b72fb052/src/slave/containerizer/provisioners/docker/token_manager.hpp
----------------------------------------------------------------------
diff --git a/src/slave/containerizer/provisioners/docker/token_manager.hpp b/src/slave/containerizer/provisioners/docker/token_manager.hpp
new file mode 100644
index 0000000..879269d
--- /dev/null
+++ b/src/slave/containerizer/provisioners/docker/token_manager.hpp
@@ -0,0 +1,179 @@
+/**
+ * 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 __PROVISIONERS_DOCKER_TOKEN_MANAGER_HPP__
+#define __PROVISIONERS_DOCKER_TOKEN_MANAGER_HPP__
+
+#include <functional>
+#include <string>
+
+#include <stout/base64.hpp>
+#include <stout/duration.hpp>
+#include <stout/hashmap.hpp>
+#include <stout/strings.hpp>
+
+#include <process/future.hpp>
+#include <process/http.hpp>
+#include <process/process.hpp>
+#include <process/time.hpp>
+
+namespace mesos {
+namespace internal {
+namespace slave {
+namespace docker {
+namespace registry {
+
+
+/**
+ * Encapsulates JSON Web Token.
+ *
+ * Reference: https://tools.ietf.org/html/rfc7519.
+ */
+struct Token
+{
+ /**
+ * Factory method for Token object.
+ *
+ * Parses the raw token string and validates for token's expiration.
+ *
+ * @returns Token if parsing and validation succeeds.
+ * Error if parsing or validation fails.
+ */
+ static Try<Token> create(const std::string& rawString);
+
+ /**
+ * Compares token's expiration time(expressed in seconds) with current time.
+ *
+ * @returns True if token's expiration time is greater than current time.
+ * False if token's expiration time is less than or equal to current
+ * time.
+ */
+ bool isExpired() const;
+
+ /**
+ * Validates the token if its "exp" "nbf" values are in range.
+ *
+ * @returns True if current time is within token's "exp" and "nbf" values.
+ * False if current time is not within token's "exp" and "nbf"
+ * values.
+ */
+ bool isValid() const;
+
+ const std::string raw;
+ const JSON::Object header;
+ const JSON::Object claims;
+ // TODO(jojy): Add signature information.
+
+private:
+ Token(
+ const std::string& raw,
+ const JSON::Object& headerJson,
+ const JSON::Object& claimsJson,
+ const Option<process::Time>& expireTime,
+ const Option<process::Time>& notBeforeTime);
+
+ static Result<process::Time> getTimeValue(
+ const JSON::Object& object,
+ const std::string& key);
+
+ const Option<process::Time> expiration;
+ const Option<process::Time> notBefore;
+};
+
+
+// Forward declaration.
+class TokenManagerProcess;
+
+
+/**
+ * Acquires and manages docker registry tokens. It keeps the tokens in its
+ * cache to server any future request for the same token.
+ * The cache grows unbounded.
+ * TODO(jojy): The cache can be optimized to prune based on the expiry time of
+ * the token and server's issue time.
+ */
+class TokenManager
+{
+public:
+ /**
+ * Factory method for creating TokenManager object.
+ *
+ * TokenManager and registry authorization realm has a 1:1 relationship.
+ *
+ * @param realm URL of the authorization server from where token will be
+ * requested by this TokenManager.
+ * @returns Owned<TokenManager> if success.
+ * Error on failure.
+ */
+ static Try<process::Owned<TokenManager>> create(
+ const process::http::URL& realm);
+
+ /**
+ * Returns JSON Web Token from cache or from remote server using "Basic
+ * authorization".
+ *
+ * @param service Name of the service that hosts the resource for which
+ * token is being requested.
+ * @param scope unique scope returned by the 401 Unauthorized response
+ * from the registry.
+ * @param account Name of the account which the client is acting as.
+ * @param user base64 encoded userid for basic authorization.
+ * @param password base64 encoded password for basic authorization.
+ * @returns Token struct that encapsulates JSON Web Token.
+ */
+ process::Future<Token> getToken(
+ const std::string& service,
+ const std::string& scope,
+ const Option<std::string>& account,
+ const std::string& user,
+ const Option<std::string>& password);
+
+ /**
+ * Returns JSON Web Token from cache or from remote server using "TLS/Cert"
+ * based authorization.
+ *
+ * @param service Name of the service that hosts the resource for which
+ * token is being requested.
+ * @param scope unique scope returned by the 401 Unauthorized response
+ * from the registry.
+ * @param account Name of the account which the client is acting as.
+ * @returns Token struct that encapsulates JSON Web Token.
+ */
+ process::Future<Token> getToken(
+ const std::string& service,
+ const std::string& scope,
+ const Option<std::string>& account);
+
+ ~TokenManager();
+
+private:
+ TokenManager(process::Owned<TokenManagerProcess>& process);
+
+ TokenManager(const TokenManager&) = delete;
+ TokenManager& operator=(const TokenManager&) = delete;
+
+ process::Owned<TokenManagerProcess> process_;
+};
+
+} // namespace registry {
+} // namespace docker {
+} // namespace slave {
+} // namespace internal {
+} // namespace mesos {
+
+#endif // __PROVISIONERS_DOCKER_TOKEN_MANAGER_HPP__
http://git-wip-us.apache.org/repos/asf/mesos/blob/b72fb052/src/tests/provisioners/docker_provisioner_tests.cpp
----------------------------------------------------------------------
diff --git a/src/tests/provisioners/docker_provisioner_tests.cpp b/src/tests/provisioners/docker_provisioner_tests.cpp
new file mode 100644
index 0000000..ff29d56
--- /dev/null
+++ b/src/tests/provisioners/docker_provisioner_tests.cpp
@@ -0,0 +1,354 @@
+/**
+ * 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 <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include <stout/duration.hpp>
+
+#include <process/address.hpp>
+#include <process/clock.hpp>
+#include <process/future.hpp>
+#include <process/gmock.hpp>
+#include <process/owned.hpp>
+#include <process/socket.hpp>
+#include <process/subprocess.hpp>
+
+#include <process/ssl/gtest.hpp>
+
+#include "slave/containerizer/provisioners/docker/token_manager.hpp"
+
+#include "tests/mesos.hpp"
+
+using std::map;
+using std::string;
+using std::vector;
+
+using namespace mesos::internal::slave::docker::registry;
+using namespace process;
+
+namespace mesos {
+namespace internal {
+namespace tests {
+
+
+/**
+ * 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;
+ }
+
+ const string signBase64 = base64::encode("{\"\"}");
+ string claimsJsonString;
+};
+
+
+/**
+ * Fixture for testing TokenManager component.
+ */
+class DockerRegistryTokenTest : public TokenHelper, public ::testing::Test
+{};
+
+
+// Tests JSON Web Token parsing for a valid token string.
+TEST_F(DockerRegistryTokenTest, 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(DockerRegistryTokenTest, ExpiredToken)
+{
+ 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\":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(DockerRegistryTokenTest, 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(DockerRegistryTokenTest, 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 DockerRegistryClientTest : public virtual SSLTest, public TokenHelper
+{
+protected:
+ DockerRegistryClientTest() {}
+
+ static void SetUpTestCase()
+ {
+ SSLTest::SetUpTestCase();
+ // TODO(jojy): Add registry specific directory setup. Will be added in the
+ // next patch when docker registry client tests are added.
+ }
+
+ static void TearDownTestCase()
+ {
+ SSLTest::TearDownTestCase();
+ // TODO(jojy): Add registry specific directory cleanup. Will be added in the
+ // next patch when docker registry client tests are added.
+ }
+};
+
+
+// Tests TokenManager for a simple token request.
+TEST_F(DockerRegistryClientTest, SimpleGetToken)
+{
+ Try<Socket> server = setup_server({
+ {"SSL_ENABLED", "true"},
+ {"SSL_KEY_FILE", key_path().value},
+ {"SSL_CERT_FILE", certificate_path().value}});
+
+ 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 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(DockerRegistryClientTest, BadTokenResponse)
+{
+ Try<Socket> server = setup_server({
+ {"SSL_ENABLED", "true"},
+ {"SSL_KEY_FILE", key_path().value},
+ {"SSL_CERT_FILE", certificate_path().value}});
+
+ 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 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(DockerRegistryClientTest, BadTokenServerAddress)
+{
+ // Create an invalid URL with current time.
+ const 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);
+}
+
+#endif // USE_SSL_SOCKET
+
+
+} // namespace tests {
+} // namespace internal {
+} // namespace mesos {