blob: fbd847f80a564c226175fb3c094544642a642d22 [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 "paimon/fs/oss/oss_file_system.h"
#include <chrono>
#include <cstdint>
#include <future>
#include <map>
#include <memory>
#include <sstream>
#include <string>
#include <system_error>
#include <utility>
#include <vector>
#include "alibabacloud/oss2/ClientConfiguration.h"
#include "alibabacloud/oss2/OSSClient.h"
#include "alibabacloud/oss2/credentials/CredentialsProvider.h"
#include "alibabacloud/oss2/io/ByteWriter.h"
#include "alibabacloud/oss2/transport/HttpTransport.h"
#include "gtest/gtest.h"
#include "paimon/fs/oss/oss_file_system_factory.h"
#include "paimon/testing/utils/testharness.h"
namespace paimon::oss {
namespace {
namespace oss2 = alibabacloud::oss2;
class MockHttpTransport : public oss2::HttpTransport {
public:
oss2::ResponseResult send(std::unique_ptr<oss2::RequestMessage>& request,
const oss2::RequestOptions& options) override {
requests_.emplace_back(std::make_unique<oss2::RequestMessage>(*request));
if (responses_.empty()) {
return oss2::TransportError{std::make_error_code(std::errc::no_message_available), "",
""};
}
std::unique_ptr<oss2::ResponseMessage> response = std::move(responses_.front());
responses_.erase(responses_.begin());
if (response->statusCode / 100 == 2 && options.sinkFactory.has_value() &&
response->body != nullptr) {
int64_t content_length = -1;
auto content_length_header = response->headers.find("Content-Length");
if (content_length_header != response->headers.end()) {
content_length = std::stoll(content_length_header->second);
}
std::shared_ptr<oss2::ByteWriter> sink =
options.sinkFactory.value()(content_length, response->headers);
std::ostringstream body;
body << response->body->rdbuf();
const std::string data = body.str();
sink->write(reinterpret_cast<const uint8_t*>(data.data()), data.size());
response->body.reset();
}
return response;
}
std::string getName() const override {
return "MockHttpTransport";
}
void AddResponse(int status_code, oss2::HeaderCollection headers, std::string body = "") {
std::shared_ptr<std::iostream> response_body;
if (!body.empty()) {
response_body = std::make_shared<std::stringstream>(std::move(body));
}
responses_.emplace_back(std::make_unique<oss2::ResponseMessage>(oss2::ResponseMessage{
status_code, "", std::move(headers), std::move(response_body), nullptr}));
}
std::vector<std::unique_ptr<oss2::ResponseMessage>> responses_;
std::vector<std::unique_ptr<oss2::RequestMessage>> requests_;
};
std::unique_ptr<OssFileSystem> CreateFileSystem(
const std::shared_ptr<MockHttpTransport>& transport) {
oss2::ClientConfiguration config = oss2::ClientConfiguration::loadDefault();
config.region = "cn-hangzhou";
config.credentialsProvider =
std::make_shared<oss2::StaticCredentialsProvider>("access-key", "secret-key");
config.httpTransport = transport;
return std::make_unique<OssFileSystem>("bucket", std::make_shared<oss2::OSSClient>(config),
CreateDefaultExecutor());
}
} // namespace
TEST(OssFileSystemFactoryTest, TestOptionValidation) {
OssFileSystemFactory factory;
std::map<std::string, std::string> options;
ASSERT_NOK(factory.Create("s3://bucket/key", options));
ASSERT_NOK(factory.Create("oss://bucket/key", options));
options[kOssAccessKeyIdOption] = "access-key";
options[kOssAccessKeySecretOption] = "secret-key";
options[kOssEndpointOption] = "oss-cn-hangzhou.aliyuncs.com";
options[kOssUsePathStyleOption] = "treu";
ASSERT_NOK(factory.Create("oss://bucket/key", options));
options[kOssUsePathStyleOption] = "false";
options[kOssSignatureVersionOption] = "v2";
ASSERT_NOK(factory.Create("oss://bucket/key", options));
options[kOssSignatureVersionOption] = "v4";
options[kOssExecutorThreadCountOption] = "0";
ASSERT_NOK(factory.Create("oss://bucket/key", options));
options[kOssExecutorThreadCountOption] = "4";
ASSERT_OK(factory.Create("oss://bucket/key", options));
options[kOssEndpointOption] = "";
options[kOssRegionOption] = "cn-hangzhou";
ASSERT_OK(factory.Create("oss://bucket/key", options));
}
TEST(OssFileSystemFactoryTest, TestBucketOptionsOverrideGlobalOptions) {
OssFileSystemFactory factory;
std::map<std::string, std::string> options = {
{kOssAccessKeyIdOption, ""},
{kOssAccessKeySecretOption, ""},
{kOssEndpointOption, ""},
{"fs.oss.bucket.bucket.accessKeyId", "access-key"},
{"fs.oss.bucket.bucket.accessKeySecret", "secret-key"},
{"fs.oss.bucket.bucket.endpoint", "oss-cn-hangzhou.aliyuncs.com"},
};
ASSERT_OK(factory.Create("oss://bucket/key", options));
}
TEST(OssFileSystemFactoryTest, TestBucketOptionErrorReportsBucketKey) {
OssFileSystemFactory factory;
std::map<std::string, std::string> options = {
{kOssAccessKeyIdOption, "access-key"},
{kOssAccessKeySecretOption, "secret-key"},
{kOssEndpointOption, "oss-cn-hangzhou.aliyuncs.com"},
{"fs.oss.bucket.bucket.accessKeyId", ""},
};
ASSERT_NOK_WITH_MSG(factory.Create("oss://bucket/key", options),
"fs.oss.bucket.bucket.accessKeyId");
}
TEST(OssFileSystemFactoryTest, TestEndpointRegionValidation) {
OssFileSystemFactory factory;
std::map<std::string, std::string> options = {
{kOssAccessKeyIdOption, "access-key"},
{kOssAccessKeySecretOption, "secret-key"},
};
options[kOssEndpointOption] = "oss-cn-hangzhou.aliyuncs.com";
ASSERT_OK(factory.Create("oss://bucket/key", options));
options[kOssEndpointOption] = "oss-cn-hangzhou-internal.aliyuncs.com";
ASSERT_OK(factory.Create("oss://bucket/key", options));
options[kOssEndpointOption] = "oss-ap-southeast-1.aliyuncs.com:443";
ASSERT_OK(factory.Create("oss://bucket/key", options));
options[kOssEndpointOption] = "cn-hangzhou.oss.aliyuncs.com";
ASSERT_OK(factory.Create("oss://bucket/key", options));
options[kOssEndpointOption] = "oss-accelerate.aliyuncs.com";
ASSERT_NOK_WITH_MSG(factory.Create("oss://bucket/key", options), "OSS region must be");
options[kOssEndpointOption] = "oss-accelerate-overseas.aliyuncs.com";
ASSERT_NOK_WITH_MSG(factory.Create("oss://bucket/key", options), "OSS region must be");
options[kOssEndpointOption] = "oss.example.com";
ASSERT_NOK_WITH_MSG(factory.Create("oss://bucket/key", options), "OSS region must be");
options[kOssRegionOption] = "cn-hangzhou";
ASSERT_OK(factory.Create("oss://bucket/key", options));
options.erase(kOssRegionOption);
options[kOssSignatureVersionOption] = "v1";
ASSERT_OK(factory.Create("oss://bucket/key", options));
}
TEST(OssFileSystemTest, TestHeadObjectParsesMetadata) {
std::shared_ptr<MockHttpTransport> transport = std::make_shared<MockHttpTransport>();
transport->AddResponse(200, {{"Content-Length", "3"},
{"Last-Modified", "not-a-timestamp"},
{"x-oss-request-id", "request-id"}});
std::unique_ptr<OssFileSystem> file_system = CreateFileSystem(transport);
ASSERT_OK_AND_ASSIGN(FileStatus status, file_system->GetFileStatus("oss://bucket/key"));
ASSERT_EQ(3, status.GetLen());
ASSERT_EQ(FileStatus::kUnknownModificationTime, status.GetModificationTime());
ASSERT_EQ(1U, transport->requests_.size());
ASSERT_EQ("HEAD", transport->requests_[0]->method);
}
TEST(OssFileSystemTest, TestHeadObjectNotFound) {
std::shared_ptr<MockHttpTransport> transport = std::make_shared<MockHttpTransport>();
transport->AddResponse(404, {{"x-oss-request-id", "request-id"}},
"<Error><Code>NoSuchKey</Code><Message>missing</Message></Error>");
transport->AddResponse(200, {},
"<ListBucketResult><IsTruncated>false</IsTruncated></ListBucketResult>");
std::unique_ptr<OssFileSystem> file_system = CreateFileSystem(transport);
Result<FileStatus> status = file_system->GetFileStatus("oss://bucket/missing");
ASSERT_TRUE(status.status().IsNotExist()) << status.status().ToString();
ASSERT_NOK_WITH_MSG(status, "does not exist");
}
TEST(OssFileSystemTest, TestHeadObjectErrorMapping) {
std::shared_ptr<MockHttpTransport> transport = std::make_shared<MockHttpTransport>();
transport->AddResponse(403, {{"x-oss-request-id", "request-id"}},
"<Error><Code>AccessDenied</Code><Message>denied</Message></Error>");
std::unique_ptr<OssFileSystem> file_system = CreateFileSystem(transport);
Result<FileStatus> status = file_system->GetFileStatus("oss://bucket/key");
ASSERT_TRUE(status.status().IsIOError()) << status.status().ToString();
ASSERT_NOK_WITH_MSG(status, "code=AccessDenied, status=403");
}
TEST(OssFileSystemTest, TestListObjects) {
std::shared_ptr<MockHttpTransport> transport = std::make_shared<MockHttpTransport>();
transport->AddResponse(404, {{"x-oss-request-id", "request-id"}},
"<Error><Code>NoSuchKey</Code><Message>missing</Message></Error>");
transport->AddResponse(200, {{"x-oss-request-id", "request-id"}}, R"(
<ListBucketResult>
<IsTruncated>false</IsTruncated>
<Contents>
<Key>prefix/file</Key>
<Size>3</Size>
<LastModified>2024-01-01T00:00:00.000Z</LastModified>
</Contents>
<CommonPrefixes><Prefix>prefix/sub/</Prefix></CommonPrefixes>
</ListBucketResult>)");
std::unique_ptr<OssFileSystem> file_system = CreateFileSystem(transport);
std::vector<FileStatus> statuses;
ASSERT_OK(file_system->ListFileStatus("oss://bucket/prefix", &statuses));
ASSERT_EQ(2U, statuses.size());
ASSERT_EQ(1704067200000, statuses[0].GetModificationTime());
ASSERT_EQ(2U, transport->requests_.size());
ASSERT_EQ("HEAD", transport->requests_[0]->method);
ASSERT_EQ("GET", transport->requests_[1]->method);
ASSERT_NE(std::string::npos, transport->requests_[1]->uri.find("list-type=2"));
}
TEST(OssFileSystemTest, TestListObjectsErrorMapping) {
std::shared_ptr<MockHttpTransport> transport = std::make_shared<MockHttpTransport>();
transport->AddResponse(403, {{"x-oss-request-id", "request-id"}},
"<Error><Code>AccessDenied</Code><Message>denied</Message></Error>");
std::unique_ptr<OssFileSystem> file_system = CreateFileSystem(transport);
std::vector<BasicFileStatus> statuses;
Status status = file_system->ListDir("oss://bucket/prefix/", &statuses);
ASSERT_TRUE(status.IsIOError()) << status.ToString();
ASSERT_NOK_WITH_MSG(status, "code=AccessDenied, status=403");
}
TEST(OssFileSystemTest, TestGetObjectRangeAndShortRead) {
std::shared_ptr<MockHttpTransport> transport = std::make_shared<MockHttpTransport>();
transport->AddResponse(206, {{"Content-Length", "3"}}, "abc");
std::unique_ptr<OssFileSystem> file_system = CreateFileSystem(transport);
FileStatus file_status("oss://bucket/key", 3);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<InputStream> stream, file_system->Open(file_status));
char buffer[3];
ASSERT_OK_AND_ASSIGN(int64_t bytes_read, stream->Read(buffer, 3, 0));
ASSERT_EQ(3, bytes_read);
ASSERT_EQ("abc", std::string(buffer, sizeof(buffer)));
ASSERT_EQ(1U, transport->requests_.size());
ASSERT_EQ("bytes=0-2", transport->requests_[0]->headers.at("range"));
std::shared_ptr<MockHttpTransport> short_transport = std::make_shared<MockHttpTransport>();
short_transport->AddResponse(206, {{"Content-Length", "2"}}, "ab");
std::unique_ptr<OssFileSystem> short_file_system = CreateFileSystem(short_transport);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<InputStream> short_stream,
short_file_system->Open(file_status));
ASSERT_NOK_WITH_MSG(short_stream->Read(buffer, 3, 0), "expected 3");
}
TEST(OssFileSystemTest, TestGetObjectRangeErrorMapping) {
std::shared_ptr<MockHttpTransport> transport = std::make_shared<MockHttpTransport>();
transport->AddResponse(403, {{"x-oss-request-id", "request-id"}},
"<Error><Code>AccessDenied</Code><Message>denied</Message></Error>");
std::unique_ptr<OssFileSystem> file_system = CreateFileSystem(transport);
FileStatus file_status("oss://bucket/key", 3);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<InputStream> stream, file_system->Open(file_status));
char buffer[3];
Result<int64_t> result = stream->Read(buffer, 3, 0);
ASSERT_TRUE(result.status().IsIOError()) << result.status().ToString();
ASSERT_NOK_WITH_MSG(result, "code=AccessDenied, status=403");
}
TEST(OssFileSystemTest, TestGetObjectRangeAsync) {
std::shared_ptr<MockHttpTransport> transport = std::make_shared<MockHttpTransport>();
transport->AddResponse(206, {{"Content-Length", "3"}}, "abc");
std::unique_ptr<OssFileSystem> file_system = CreateFileSystem(transport);
FileStatus file_status("oss://bucket/key", 3);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<InputStream> stream, file_system->Open(file_status));
char buffer[3];
std::promise<Status> promise;
std::future<Status> future = promise.get_future();
stream->ReadAsync(buffer, 3, 0,
[&promise](Status status) { promise.set_value(std::move(status)); });
ASSERT_EQ(std::future_status::ready, future.wait_for(std::chrono::seconds(5)));
ASSERT_OK(future.get());
ASSERT_EQ("abc", std::string(buffer, sizeof(buffer)));
ASSERT_EQ(1U, transport->requests_.size());
ASSERT_EQ("bytes=0-2", transport->requests_[0]->headers.at("range"));
}
} // namespace paimon::oss