blob: 0b4d96c137ee3d6b2e0e5c342f2d31c30026e4bc [file] [log] [blame]
// 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 <process/future.hpp>
#include <process/gtest.hpp>
#include <process/http.hpp>
#include <process/process.hpp>
#include <stout/check.hpp>
#include <stout/duration.hpp>
#include <stout/gtest.hpp>
#include <stout/os.hpp>
#include <stout/path.hpp>
#include <stout/os/exists.hpp>
#include <stout/os/getcwd.hpp>
#include <stout/os/ls.hpp>
#include <stout/os/write.hpp>
#include <stout/uri.hpp>
#include <stout/tests/utils.hpp>
#ifndef __WINDOWS__
#include <mesos/docker/spec.hpp>
#endif // __WINDOWS__
#include "uri/fetcher.hpp"
#include "uri/schemes/docker.hpp"
#include "uri/schemes/file.hpp"
#include "uri/schemes/hdfs.hpp"
#include "uri/schemes/http.hpp"
namespace http = process::http;
using std::list;
using std::string;
#ifndef __WINDOWS__
using mesos::uri::DockerFetcherPlugin;
#endif // __WINDOWS__
using process::Future;
using process::Owned;
using process::Process;
using testing::_;
using testing::Return;
namespace mesos {
namespace internal {
namespace tests {
class TestHttpServer : public Process<TestHttpServer>
{
public:
TestHttpServer() : ProcessBase("TestHttpServer")
{
route("/test", None(), &TestHttpServer::test);
}
MOCK_METHOD1(test, Future<http::Response>(const http::Request&));
};
class CurlFetcherPluginTest : public TemporaryDirectoryTest
{
protected:
void SetUp() override
{
TemporaryDirectoryTest::SetUp();
spawn(server);
}
void TearDown() override
{
terminate(server);
wait(server);
TemporaryDirectoryTest::TearDown();
}
TestHttpServer server;
};
TEST_F(CurlFetcherPluginTest, CURL_ValidUri)
{
URI uri = uri::http(
stringify(server.self().address.ip),
"/TestHttpServer/test",
server.self().address.port);
EXPECT_CALL(server, test(_))
.WillOnce(Return(http::OK("test")));
Try<Owned<uri::Fetcher>> fetcher = uri::fetcher::create();
ASSERT_SOME(fetcher);
AWAIT_READY(fetcher.get()->fetch(uri, os::getcwd()));
EXPECT_TRUE(os::exists(path::join(os::getcwd(), "test")));
}
TEST_F(CurlFetcherPluginTest, CURL_ValidUriWithOutputFileName)
{
URI uri = uri::http(
stringify(server.self().address.ip),
"/TestHttpServer/test",
server.self().address.port);
EXPECT_CALL(server, test(_))
.WillOnce(Return(http::OK("test")));
Try<Owned<uri::Fetcher>> fetcher = uri::fetcher::create();
ASSERT_SOME(fetcher);
AWAIT_READY(fetcher.get()->fetch(uri, os::getcwd(), None(), "file"));
EXPECT_TRUE(os::exists(path::join(os::getcwd(), "file")));
}
TEST_F(CurlFetcherPluginTest, CURL_InvalidUri)
{
URI uri = uri::http(
stringify(server.self().address.ip),
"/TestHttpServer/test",
server.self().address.port);
EXPECT_CALL(server, test(_))
.WillOnce(Return(http::NotFound()));
Try<Owned<uri::Fetcher>> fetcher = uri::fetcher::create();
ASSERT_SOME(fetcher);
AWAIT_FAILED(fetcher.get()->fetch(uri, os::getcwd()));
}
// This test verifies invoking 'fetch' by plugin name.
TEST_F(CurlFetcherPluginTest, CURL_InvokeFetchByName)
{
URI uri = uri::http(
stringify(server.self().address.ip),
"/TestHttpServer/test",
server.self().address.port);
EXPECT_CALL(server, test(_))
.WillOnce(Return(http::OK("test")));
Try<Owned<uri::Fetcher>> fetcher = uri::fetcher::create();
ASSERT_SOME(fetcher);
AWAIT_READY(fetcher.get()->fetch(uri, os::getcwd(), "curl", None(), None()));
EXPECT_TRUE(os::exists(path::join(os::getcwd(), "test")));
}
class HadoopFetcherPluginTest : public TemporaryDirectoryTest
{
public:
void SetUp() override
{
TemporaryDirectoryTest::SetUp();
// Create a fake hadoop command line tool. It emulates the hadoop
// client's logic while operating on the local filesystem.
#ifndef __WINDOWS__
hadoop = path::join(os::getcwd(), "hadoop");
#else
hadoop = path::join(os::getcwd(), "hadoop.bat");
#endif // __WINDOWS__
// The script emulating 'hadoop fs -copyToLocal <from> <to>'.
// NOTE: We emulate a version call here which is exercised when
// creating the HDFS client. But, we don't expect any other
// command to be called.
#ifndef __WINDOWS__
ASSERT_SOME(os::write(
hadoop,
"#!/bin/sh\n"
"if [ \"$1\" = \"version\" ]; then\n"
" exit 0\n"
"fi\n"
"if [ \"$1\" != \"fs\" ]; then\n"
" exit 1\n"
"fi\n"
"if [ \"$2\" != \"-copyToLocal\" ]; then\n"
" exit 1\n"
"fi\n"
"cp $3 $4\n"));
// Make sure the script has execution permission.
ASSERT_SOME(os::chmod(
hadoop,
S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH));
#else
// NOTE: This is a `.bat` file instead of PowerShell so that it can be
// directly executed. Windows "knows" how to launch files ending in `.bat`,
// similar to the shebang logic for POSIX. However, this does not extend to
// PowerShell's `.ps1` scripts.
ASSERT_SOME(os::write(
hadoop,
"if \"%1\" == \"version\" (exit 0)\n"
"if NOT \"%1\" == \"fs\" (exit 1)\n"
"if NOT \"%2\" == \"-copyToLocal\" (exit 1)\n"
"copy %3 %4\n"));
// Windows has no notion of "execution permission".
#endif // __WINDOWS__
}
protected:
string hadoop;
};
TEST_F(HadoopFetcherPluginTest, FetchExistingFile)
{
string file = path::join(os::getcwd(), "file");
ASSERT_SOME(os::write(file, "abc"));
URI uri = uri::hdfs(file);
uri::fetcher::Flags flags;
flags.hadoop_client = hadoop;
Try<Owned<uri::Fetcher>> fetcher = uri::fetcher::create(flags);
ASSERT_SOME(fetcher);
string dir = path::join(os::getcwd(), "dir");
AWAIT_READY(fetcher.get()->fetch(uri, dir));
EXPECT_SOME_EQ("abc", os::read(path::join(dir, "file")));
}
TEST_F(HadoopFetcherPluginTest, FetchNonExistingFile)
{
URI uri = uri::hdfs(path::join(os::getcwd(), "non-exist"));
uri::fetcher::Flags flags;
flags.hadoop_client = hadoop;
Try<Owned<uri::Fetcher>> fetcher = uri::fetcher::create(flags);
ASSERT_SOME(fetcher);
string dir = path::join(os::getcwd(), "dir");
AWAIT_FAILED(fetcher.get()->fetch(uri, dir));
}
// This test verifies invoking 'fetch' by plugin name.
TEST_F(HadoopFetcherPluginTest, InvokeFetchByName)
{
string file = path::join(os::getcwd(), "file");
ASSERT_SOME(os::write(file, "abc"));
URI uri = uri::hdfs(file);
uri::fetcher::Flags flags;
flags.hadoop_client = hadoop;
Try<Owned<uri::Fetcher>> fetcher = uri::fetcher::create(flags);
ASSERT_SOME(fetcher);
string dir = path::join(os::getcwd(), "dir");
AWAIT_READY(fetcher.get()->fetch(uri, dir, "hadoop", None(), None()));
EXPECT_SOME_EQ("abc", os::read(path::join(dir, "file")));
}
// TODO(jieyu): Expose this constant so that other docker related
// tests can use this as well.
static constexpr char DOCKER_REGISTRY_HOST[] = "registry-1.docker.io";
#ifdef __WINDOWS__
static constexpr char TEST_REPOSITORY[] = "microsoft/nanoserver";
static constexpr char TEST_DIGEST[] = "sha256:54389c2d19b423943102864aaf3fc"
"1296e5dd140a074b5bd6700de858a8e5479";
#else
static constexpr char TEST_REPOSITORY[] = "library/busybox";
static constexpr char TEST_DIGEST[] = "sha256:a3ed95caeb02ffe68cdd9fd844066"
"80ae93d633cb16422d00e8a7c22955b46d4";
#endif // __WINDOWS__
// NOTE: Windows images exist and can be fetched, but there is no Windows
// provisioner in the Mesos containerizer that can use these images.
#ifndef __WINDOWS__
class DockerFetcherPluginTest : public TemporaryDirectoryTest {};
TEST_F(DockerFetcherPluginTest, DISABLED_INTERNET_CURL_FetchManifest)
{
URI uri = uri::docker::manifest(
TEST_REPOSITORY, "latest", DOCKER_REGISTRY_HOST);
Try<Owned<uri::Fetcher>> fetcher = uri::fetcher::create();
ASSERT_SOME(fetcher);
string dir = path::join(os::getcwd(), "dir");
AWAIT_READY_FOR(fetcher.get()->fetch(uri, dir), Seconds(60));
// Version 2 schema 2 image manifest test
Try<string> _manifest = os::read(path::join(dir, "manifest"));
ASSERT_SOME(_manifest);
Try<docker::spec::v2_2::ImageManifest> manifest =
docker::spec::v2_2::parse(_manifest.get());
ASSERT_SOME(manifest);
EXPECT_EQ(2u, manifest->schemaversion());
EXPECT_EQ("application/vnd.docker.distribution.manifest.v2+json",
manifest->mediatype());
}
TEST_F(DockerFetcherPluginTest, INTERNET_CURL_FetchBlob)
{
URI uri = uri::docker::blob(
TEST_REPOSITORY, TEST_DIGEST, DOCKER_REGISTRY_HOST);
Try<Owned<uri::Fetcher>> fetcher = uri::fetcher::create();
ASSERT_SOME(fetcher);
string dir = path::join(os::getcwd(), "dir");
AWAIT_READY_FOR(fetcher.get()->fetch(uri, dir), Seconds(60));
EXPECT_TRUE(os::exists(DockerFetcherPlugin::getBlobPath(dir, TEST_DIGEST)));
}
// Fetches the image manifest and all blobs in that image.
TEST_F(DockerFetcherPluginTest, DISABLED_INTERNET_CURL_FetchImage)
{
URI uri = uri::docker::image(
TEST_REPOSITORY, "latest", DOCKER_REGISTRY_HOST);
Try<Owned<uri::Fetcher>> fetcher = uri::fetcher::create();
ASSERT_SOME(fetcher);
string dir = path::join(os::getcwd(), "dir");
AWAIT_READY_FOR(fetcher.get()->fetch(uri, dir), Seconds(60));
Try<string> _manifest = os::read(path::join(dir, "manifest"));
ASSERT_SOME(_manifest);
Try<docker::spec::v2_2::ImageManifest> manifest =
docker::spec::v2_2::parse(_manifest.get());
ASSERT_SOME(manifest);
EXPECT_EQ(2u, manifest->schemaversion());
EXPECT_EQ("application/vnd.docker.distribution.manifest.v2+json",
manifest->mediatype());
EXPECT_TRUE(os::exists(DockerFetcherPlugin::getBlobPath(
dir, manifest->config().digest())));
for (int i = 0; i < manifest->layers_size(); i++) {
EXPECT_TRUE(os::exists(
DockerFetcherPlugin::getBlobPath(
dir,
manifest->layers(i).digest())));
}
}
// Fetches an image from a registry for which fetcher falls back to generating
// repository scope (see MESOS-10092). This test uses 'nvcr.io' as a registry
// that does not provide scope on its own.
//
// TODO(asekretenko): Set up a mock registry and use it in this test.
TEST_F(DockerFetcherPluginTest, INTERNET_CURL_GeneratedRepositoryScope)
{
// Using `nvidia/k8s/cuda-sample:nbody` as it seems to be the smallest one
// on nvcr.io
const URI uri =
uri::docker::image("nvidia/k8s/cuda-sample", "nbody", "nvcr.io");
const string dir = path::join(os::getcwd(), "dir");
// First, we create a docker fetcher plugin with fallback disabled
// to make sure that nvcr.io is still suitable for the purpose of this test.
Try<process::Owned<uri::Fetcher::Plugin>> fetcherWithDisabledFallback =
DockerFetcherPlugin::create(DockerFetcherPlugin::Flags(), false);
ASSERT_SOME(fetcherWithDisabledFallback);
Future<Nothing> NoFallbackFetch =
fetcherWithDisabledFallback.get()->fetch(uri, dir);
AWAIT_FAILED_FOR(NoFallbackFetch, Seconds(120));
ASSERT_TRUE(strings::contains(
NoFallbackFetch.failure(),
"Missing 'realm', 'service' or 'scope' in header WWW-Authenticate"));
// Now, we can proceed with testing the fallback.
Try<Owned<uri::Fetcher>> fetcher = uri::fetcher::create();
ASSERT_SOME(fetcher);
AWAIT_READY_FOR(fetcher.get()->fetch(uri, dir), Seconds(120));
Try<string> _manifest = os::read(path::join(dir, "manifest"));
ASSERT_SOME(_manifest);
Try<docker::spec::v2_2::ImageManifest> manifest =
docker::spec::v2_2::parse(_manifest.get());
ASSERT_SOME(manifest);
EXPECT_EQ(2u, manifest->schemaversion());
EXPECT_EQ("application/vnd.docker.distribution.manifest.v2+json",
manifest->mediatype());
EXPECT_TRUE(os::exists(path::join(dir, manifest->config().digest())));
for (int i = 0; i < manifest->layers_size(); i++) {
EXPECT_TRUE(os::exists(
DockerFetcherPlugin::getBlobPath(dir, manifest->layers(i).digest())));
}
}
// This test verifies invoking 'fetch' by plugin name.
TEST_F(DockerFetcherPluginTest, DISABLED_INTERNET_CURL_InvokeFetchByName)
{
URI uri = uri::docker::image(
TEST_REPOSITORY, "latest", DOCKER_REGISTRY_HOST);
Try<Owned<uri::Fetcher>> fetcher = uri::fetcher::create();
ASSERT_SOME(fetcher);
string dir = path::join(os::getcwd(), "dir");
AWAIT_READY_FOR(
fetcher.get()->fetch(uri, dir, "docker", None(), None()),
Seconds(60));
Try<string> _manifest = os::read(path::join(dir, "manifest"));
ASSERT_SOME(_manifest);
Try<docker::spec::v2_2::ImageManifest> manifest =
docker::spec::v2_2::parse(_manifest.get());
ASSERT_SOME(manifest);
EXPECT_EQ(2u, manifest->schemaversion());
EXPECT_EQ("application/vnd.docker.distribution.manifest.v2+json",
manifest->mediatype());
EXPECT_TRUE(os::exists(DockerFetcherPlugin::getBlobPath(
dir, manifest->config().digest())));
for (int i = 0; i < manifest->layers_size(); i++) {
EXPECT_TRUE(os::exists(
DockerFetcherPlugin::getBlobPath(
dir,
manifest->layers(i).digest())));
}
}
#endif // __WINDOWS__
class CopyFetcherPluginTest : public TemporaryDirectoryTest {};
// Tests CopyFetcher plugin for fetching a valid file.
TEST_F(CopyFetcherPluginTest, FetchExistingFile)
{
const string file = path::join(os::getcwd(), "file");
ASSERT_SOME(os::write(file, "abc"));
// Create a URI for the test file.
const URI uri = uri::file(file);
// Use the file fetcher to fetch the URI.
Try<Owned<uri::Fetcher>> fetcher = uri::fetcher::create();
ASSERT_SOME(fetcher);
const string dir = path::join(os::getcwd(), "dir");
AWAIT_READY(fetcher.get()->fetch(uri, dir));
// Validate the fetched file's content.
EXPECT_SOME_EQ("abc", os::read(path::join(dir, "file")));
}
// Negative test case that tests CopyFetcher plugin for a non-exiting file.
TEST_F(CopyFetcherPluginTest, FetchNonExistingFile)
{
const URI uri = uri::file(path::join(os::getcwd(), "non-exist"));
// Use the file fetcher to fetch the URI.
Try<Owned<uri::Fetcher>> fetcher = uri::fetcher::create();
ASSERT_SOME(fetcher);
const string dir = path::join(os::getcwd(), "dir");
// Validate that the fetch failed.
AWAIT_FAILED(fetcher.get()->fetch(uri, dir));
}
// This test verifies invoking 'fetch' by plugin name.
TEST_F_TEMP_DISABLED_ON_WINDOWS(CopyFetcherPluginTest, InvokeFetchByName)
{
const string file = path::join(os::getcwd(), "file");
ASSERT_SOME(os::write(file, "abc"));
// Create a URI for the test file.
const URI uri = uri::file(file);
// Use the file fetcher to fetch the URI.
Try<Owned<uri::Fetcher>> fetcher = uri::fetcher::create();
ASSERT_SOME(fetcher);
const string dir = path::join(os::getcwd(), "dir");
AWAIT_READY(fetcher.get()->fetch(uri, dir, "copy", None(), None()));
// Validate the fetched file's content.
EXPECT_SOME_EQ("abc", os::read(path::join(dir, "file")));
}
// TODO(jieyu): Add Docker fetcher plugin tests to test with a local
// registry server (w/ or w/o authentication).
} // namespace tests {
} // namespace internal {
} // namespace mesos {