blob: a28e19916c9fdf8d1e16bae0d406ba80a1143edd [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/rest/rest_api.h"
#include <memory>
#include <string>
#include <utility>
#include "fmt/format.h"
#include "paimon/catalog_options.h"
#include "paimon/common/utils/options_utils.h"
#include "paimon/common/utils/rapidjson_util.h"
#include "paimon/common/utils/sensitive_config_utils.h"
#include "paimon/logging.h"
#include "paimon/rest/rest_util.h"
namespace paimon {
namespace {
// The `PAIMON_ASSIGN_OR_RAISE` macro cannot take a declaration type containing a comma.
using StringMap = std::map<std::string, std::string>;
// A successful response body may carry credentials (e.g. a token response), so neither it
// nor the parse failure quoting it is echoed into the error message; only the request path
// is reported.
template <typename ResponseT>
Status ParseResponseBody(const std::string& body, const std::string& path, ResponseT* entity) {
if (!RapidJsonUtil::FromJsonString(body, entity).ok()) {
return Status::Invalid(
fmt::format("failed to deserialize the response of {} from the rest server", path));
}
return Status::OK();
}
} // namespace
RestApi::RestApi(std::unique_ptr<RestHttpClient> client,
std::unique_ptr<AuthProvider> auth_provider,
const std::map<std::string, std::string>& base_headers,
const std::map<std::string, std::string>& options, const ResourcePaths& paths)
: client_(std::move(client)),
auth_provider_(std::move(auth_provider)),
base_headers_(base_headers),
options_(options),
resource_paths_(paths) {}
Result<std::unique_ptr<RestApi>> RestApi::Create(const std::map<std::string, std::string>& options,
const std::string& warehouse, bool config_required,
const RestHttpClient::Config& http_config) {
Result<std::string> uri = OptionsUtils::GetNonEmptyValueFromMap(options, CatalogOptions::URI);
if (!uri.ok()) {
return Status::Invalid(fmt::format("option '{}' must be configured for the rest catalog",
CatalogOptions::URI));
}
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<RestHttpClient> client,
RestHttpClient::Create(uri.value(), http_config));
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<AuthProvider> auth_provider,
AuthProvider::Create(options));
std::map<std::string, std::string> merged_options = options;
std::map<std::string, std::string> base_headers =
RestUtil::ExtractPrefixMap(options, kHeaderOptionPrefix);
if (config_required) {
std::map<std::string, std::string> query_params;
if (!warehouse.empty()) {
query_params[kQueryParamWarehouse] = warehouse;
}
RestAuthParameter auth_parameter =
RestAuthParameter::Create("GET", ResourcePaths::Config(), query_params, "");
PAIMON_ASSIGN_OR_RAISE(StringMap headers,
auth_provider->MergeAuthHeader(base_headers, auth_parameter));
bool follow_redirects = auth_provider->AllowsRedirects();
PAIMON_ASSIGN_OR_RAISE(RestHttpClient::Response response,
client->Execute("GET", ResourcePaths::Config(), query_params,
headers, "", follow_redirects));
if (!response.IsSuccessful()) {
return ErrorToStatus(response, follow_redirects);
}
ConfigResponse config;
PAIMON_RETURN_NOT_OK(ParseResponseBody(response.body, ResourcePaths::Config(), &config));
merged_options = config.Merge(options);
for (const auto& [key, value] :
RestUtil::ExtractPrefixMap(merged_options, kHeaderOptionPrefix)) {
base_headers[key] = value;
}
std::shared_ptr<Logger> logger = Logger::GetLogger("RestApi");
PAIMON_LOG_DEBUG(logger,
"merged %zu client options with the /v1/config response into %zu options",
options.size(), merged_options.size());
}
std::string prefix;
auto prefix_iter = merged_options.find(kOptionUrlPrefix);
if (prefix_iter != merged_options.end()) {
prefix = prefix_iter->second;
}
return std::unique_ptr<RestApi>(new RestApi(std::move(client), std::move(auth_provider),
base_headers, merged_options,
ResourcePaths(prefix)));
}
Status RestApi::ErrorToStatus(const RestHttpClient::Response& response, bool follow_redirects) {
if (!follow_redirects && response.code >= 300 && response.code < HttpStatus::kBadRequest) {
std::string message = fmt::format(
"rest endpoint returned redirect status {}, which is not followed for "
"signed requests",
response.code);
std::string request_id = RestUtil::ExtractRequestId(response.headers);
if (request_id != RestUtil::kUnknownRequestId) {
message += fmt::format(" requestId:{}", request_id);
}
return Status::IOError(message).WithDetail(
std::make_shared<RestErrorDetail>(response.code));
}
// The code of the parsed error body takes precedence over the http status, which
// a gateway may have rewritten.
int64_t code = response.code;
std::string message;
std::string resource_info;
bool body_parsed = false;
if (!response.body.empty()) {
ErrorResponse error;
if (RapidJsonUtil::FromJsonString(response.body, &error).ok()) {
body_parsed = true;
// The message may embed secrets (e.g. "password=..."), so it is redacted.
message = SensitiveConfigUtils::RedactText(error.GetMessage());
if (!error.GetResourceType().empty()) {
resource_info = fmt::format(" (resource type: {}, resource name: {})",
error.GetResourceType(), error.GetResourceName());
}
if (error.GetCode() != 0) {
code = error.GetCode();
}
}
}
if (message.empty()) {
// The body is never echoed (it may carry credentials), so a server answering with
// something other than an error object is told apart by the message alone.
message = body_parsed ? fmt::format("empty error message (http status {})", response.code)
: fmt::format("unparsable error response body (http status {})",
response.code);
}
message += resource_info;
std::string request_id = RestUtil::ExtractRequestId(response.headers);
if (request_id != RestUtil::kUnknownRequestId) {
message += fmt::format(" requestId:{}", request_id);
}
Status status;
switch (code) {
case HttpStatus::kBadRequest:
status = Status::Invalid(message);
break;
case HttpStatus::kUnauthorized:
status = Status::IOError("not authorized: ", message);
break;
case HttpStatus::kForbidden:
status = Status::IOError("forbidden: ", message);
break;
case HttpStatus::kNotFound:
status = Status::NotExist(message);
break;
case HttpStatus::kConflict:
status = Status::Exist(message);
break;
case HttpStatus::kInternalServerError:
status = Status::IOError("server error: ", message);
break;
case HttpStatus::kNotImplemented:
status = Status::NotImplemented(message);
break;
case HttpStatus::kServiceUnavailable:
status = Status::IOError("service unavailable: ", message);
break;
default:
status =
Status::IOError(fmt::format("rest request failed with code {}: {}", code, message));
break;
}
return status.WithDetail(std::make_shared<RestErrorDetail>(code));
}
Result<RestHttpClient::Response> RestApi::Execute(
const std::string& method, const std::string& path,
const std::map<std::string, std::string>& query_params, const std::string& body) const {
RestAuthParameter auth_parameter = RestAuthParameter::Create(method, path, query_params, body);
StringMap request_headers = base_headers_;
if (!body.empty()) {
// Set before merging the auth headers so a provider that signs headers covers it.
request_headers["Content-Type"] = "application/json";
}
PAIMON_ASSIGN_OR_RAISE(StringMap headers,
auth_provider_->MergeAuthHeader(request_headers, auth_parameter));
bool follow_redirects = auth_provider_->AllowsRedirects();
PAIMON_ASSIGN_OR_RAISE(
RestHttpClient::Response response,
client_->Execute(method, path, query_params, headers, body, follow_redirects));
if (!response.IsSuccessful()) {
return ErrorToStatus(response, follow_redirects);
}
return response;
}
template <typename ResponseT>
Result<ResponseT> RestApi::GetEntity(const std::string& path,
const std::map<std::string, std::string>& query_params) const {
PAIMON_ASSIGN_OR_RAISE(RestHttpClient::Response response,
Execute("GET", path, query_params, ""));
ResponseT entity;
PAIMON_RETURN_NOT_OK(ParseResponseBody(response.body, path, &entity));
return entity;
}
template <typename ResponseT>
Result<std::vector<typename ResponseT::ItemType>> RestApi::ListAllPages(
const std::string& path) const {
std::vector<typename ResponseT::ItemType> items;
std::map<std::string, std::string> query_params;
while (true) {
PAIMON_ASSIGN_OR_RAISE(ResponseT response, GetEntity<ResponseT>(path, query_params));
const auto& data = response.Data();
items.insert(items.end(), data.begin(), data.end());
const std::optional<std::string>& next_page_token = response.NextPageToken();
if (!next_page_token || next_page_token.value().empty() || data.empty()) {
return items;
}
query_params[kQueryParamPageToken] = next_page_token.value();
}
}
Result<std::vector<std::string>> RestApi::ListDatabases() const {
return ListAllPages<ListDatabasesResponse>(resource_paths_.Databases());
}
Status RestApi::CreateDatabase(const std::string& name,
const std::map<std::string, std::string>& options) const {
CreateDatabaseRequest request(name, options);
PAIMON_ASSIGN_OR_RAISE(std::string body, request.ToJsonString());
return Execute("POST", resource_paths_.Databases(), {}, body).status();
}
Result<GetDatabaseResponse> RestApi::GetDatabase(const std::string& name) const {
return GetEntity<GetDatabaseResponse>(resource_paths_.Database(name), {});
}
Status RestApi::DropDatabase(const std::string& name) const {
return Execute("DELETE", resource_paths_.Database(name), {}, "").status();
}
Result<std::vector<std::string>> RestApi::ListTables(const std::string& database_name) const {
return ListAllPages<ListTablesResponse>(resource_paths_.Tables(database_name));
}
Result<GetTableResponse> RestApi::GetTable(const Identifier& identifier) const {
return GetEntity<GetTableResponse>(
resource_paths_.Table(identifier.GetDatabaseName(), identifier.GetTableName()), {});
}
Status RestApi::CreateTable(const Identifier& identifier, const std::string& schema_json) const {
CreateTableRequest request(identifier.GetDatabaseName(), identifier.GetTableName(),
schema_json);
PAIMON_ASSIGN_OR_RAISE(std::string body, request.ToJsonString());
return Execute("POST", resource_paths_.Tables(identifier.GetDatabaseName()), {}, body).status();
}
Status RestApi::DropTable(const Identifier& identifier) const {
return Execute("DELETE",
resource_paths_.Table(identifier.GetDatabaseName(), identifier.GetTableName()),
{}, "")
.status();
}
Status RestApi::RenameTable(const Identifier& from_table, const Identifier& to_table) const {
RenameTableRequest request(from_table.GetDatabaseName(), from_table.GetTableName(),
to_table.GetDatabaseName(), to_table.GetTableName());
PAIMON_ASSIGN_OR_RAISE(std::string body, request.ToJsonString());
return Execute("POST", resource_paths_.RenameTable(), {}, body).status();
}
Result<std::vector<Snapshot>> RestApi::ListSnapshots(const Identifier& identifier) const {
return ListAllPages<ListSnapshotsResponse>(
resource_paths_.Snapshots(identifier.GetDatabaseName(), identifier.GetTableName()));
}
} // namespace paimon