Coverage Report

Created: 2026-07-20 19:42

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/util/s3_util.cpp
Line
Count
Source
1
// Licensed to the Apache Software Foundation (ASF) under one
2
// or more contributor license agreements.  See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership.  The ASF licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License.  You may obtain a copy of the License at
8
//
9
//   http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied.  See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
#include "util/s3_util.h"
19
20
#include <aws/core/auth/AWSAuthSigner.h>
21
#include <aws/core/auth/AWSCredentials.h>
22
#include <aws/core/auth/AWSCredentialsProviderChain.h>
23
#include <aws/core/auth/STSCredentialsProvider.h>
24
#include <aws/core/client/DefaultRetryStrategy.h>
25
#include <aws/core/platform/Environment.h>
26
#include <aws/core/utils/logging/LogLevel.h>
27
#include <aws/core/utils/logging/LogSystemInterface.h>
28
#include <aws/core/utils/memory/stl/AWSStringStream.h>
29
#include <aws/identity-management/auth/STSAssumeRoleCredentialsProvider.h>
30
#include <aws/s3/S3Client.h>
31
#include <aws/s3/S3EndpointProvider.h>
32
#include <aws/sts/STSClient.h>
33
#include <bvar/reducer.h>
34
#include <cpp/token_bucket_rate_limiter.h>
35
36
#include <atomic>
37
#include <optional>
38
#include <string_view>
39
40
#include "util/string_util.h"
41
42
#ifdef USE_AZURE
43
#include <azure/core/diagnostics/logger.hpp>
44
#include <azure/core/http/curl_transport.hpp>
45
#include <azure/storage/blobs/blob_container_client.hpp>
46
#endif
47
#include <cstdlib>
48
#include <filesystem>
49
#include <fstream>
50
#include <functional>
51
#include <memory>
52
#include <ostream>
53
#include <utility>
54
55
#include "common/config.h"
56
#include "common/logging.h"
57
#include "common/status.h"
58
#include "cpp/aws_logger.h"
59
#include "cpp/custom_aws_credentials_provider_chain.h"
60
#include "cpp/obj_retry_strategy.h"
61
#include "cpp/sync_point.h"
62
#include "cpp/util.h"
63
#ifdef USE_AZURE
64
#include "io/fs/azure_obj_storage_client.h"
65
#endif
66
#include "exec/scan/scanner_scheduler.h"
67
#include "io/fs/obj_storage_client.h"
68
#include "io/fs/s3_obj_storage_client.h"
69
#include "runtime/exec_env.h"
70
#include "util/s3_uri.h"
71
72
namespace doris {
73
namespace s3_bvar {
74
bvar::LatencyRecorder s3_get_latency("s3_get");
75
bvar::LatencyRecorder s3_put_latency("s3_put");
76
bvar::LatencyRecorder s3_delete_object_latency("s3_delete_object");
77
bvar::LatencyRecorder s3_delete_objects_latency("s3_delete_objects");
78
bvar::LatencyRecorder s3_head_latency("s3_head");
79
bvar::LatencyRecorder s3_multi_part_upload_latency("s3_multi_part_upload");
80
bvar::LatencyRecorder s3_list_latency("s3_list");
81
bvar::LatencyRecorder s3_list_object_versions_latency("s3_list_object_versions");
82
bvar::LatencyRecorder s3_get_bucket_version_latency("s3_get_bucket_version");
83
bvar::LatencyRecorder s3_copy_object_latency("s3_copy_object");
84
}; // namespace s3_bvar
85
86
namespace {
87
88
39
std::optional<std::string_view> get_s3_express_zone_id(std::string_view bucket) {
89
39
    constexpr std::string_view suffix = "--x-s3";
90
39
    if (!bucket.ends_with(suffix)) {
91
32
        return std::nullopt;
92
32
    }
93
7
    bucket.remove_suffix(suffix.size());
94
7
    const auto zone_separator = bucket.rfind("--");
95
7
    if (zone_separator == std::string_view::npos || zone_separator == 0 ||
96
7
        zone_separator + 2 == bucket.size()) {
97
1
        return std::nullopt;
98
1
    }
99
6
    const auto bucket_base = bucket.substr(0, zone_separator);
100
6
    if (!((bucket_base.front() >= 'a' && bucket_base.front() <= 'z') ||
101
6
          (bucket_base.front() >= '0' && bucket_base.front() <= '9')) ||
102
6
        !((bucket_base.back() >= 'a' && bucket_base.back() <= 'z') ||
103
6
          (bucket_base.back() >= '0' && bucket_base.back() <= '9'))) {
104
0
        return std::nullopt;
105
0
    }
106
54
    for (const char c : bucket_base) {
107
54
        if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-')) {
108
0
            return std::nullopt;
109
0
        }
110
54
    }
111
6
    const auto zone_id = bucket.substr(zone_separator + 2);
112
6
    const auto az_separator = zone_id.rfind("-az");
113
6
    if (az_separator == std::string_view::npos || az_separator == 0 ||
114
6
        az_separator + 3 == zone_id.size()) {
115
0
        return std::nullopt;
116
0
    }
117
24
    for (const char c : zone_id.substr(0, az_separator)) {
118
24
        if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-')) {
119
0
            return std::nullopt;
120
0
        }
121
24
    }
122
6
    for (const char c : zone_id.substr(az_separator + 3)) {
123
6
        if (c < '0' || c > '9') {
124
1
            return std::nullopt;
125
1
        }
126
6
    }
127
5
    return zone_id;
128
6
}
129
130
40
bool use_s3_express_client(const S3ClientConf& conf) {
131
40
    return conf.provider == io::ObjStorageType::AWS &&
132
40
           get_s3_express_zone_id(conf.bucket).has_value();
133
40
}
134
135
36
doris::Status is_s3_conf_valid(const S3ClientConf& conf) {
136
36
    const bool s3_express = use_s3_express_client(conf);
137
36
    if (conf.endpoint.empty() && !s3_express) {
138
1
        return Status::InvalidArgument<false>("Invalid s3 conf, empty endpoint");
139
1
    }
140
35
    if (conf.region.empty()) {
141
1
        return Status::InvalidArgument<false>("Invalid s3 conf, empty region");
142
1
    }
143
144
34
    if (conf.role_arn.empty()) {
145
        // Allow anonymous access when both ak and sk are empty
146
28
        bool hasAk = !conf.ak.empty();
147
28
        bool hasSk = !conf.sk.empty();
148
149
        // Either both credentials are provided or both are empty (anonymous access)
150
28
        if (hasAk && conf.sk.empty()) {
151
1
            return Status::InvalidArgument<false>("Invalid s3 conf, empty sk");
152
1
        }
153
27
        if (hasSk && conf.ak.empty()) {
154
1
            return Status::InvalidArgument<false>("Invalid s3 conf, empty ak");
155
1
        }
156
27
    }
157
32
    return Status::OK();
158
34
}
159
160
// Return true is convert `str` to int successfully
161
0
bool to_int(std::string_view str, int& res) {
162
0
    auto [_, ec] = std::from_chars(str.data(), str.data() + str.size(), res);
163
0
    return ec == std::errc {};
164
0
}
165
166
#ifdef USE_AZURE
167
0
std::string env_or_empty(const char* env_name) {
168
0
    if (const char* value = std::getenv(env_name); value != nullptr) {
169
0
        return value;
170
0
    }
171
0
    return "";
172
0
}
173
174
0
std::string build_azure_tls_debug_context(const std::string& selected_ca_file) {
175
0
    bool selected_ca_exists = false;
176
0
    bool selected_ca_readable = false;
177
0
    if (!selected_ca_file.empty()) {
178
0
        std::error_code ec;
179
0
        selected_ca_exists = std::filesystem::exists(selected_ca_file, ec) && !ec;
180
0
        std::ifstream input(selected_ca_file);
181
0
        selected_ca_readable = input.good();
182
0
    }
183
184
0
    return fmt::format(
185
0
            "tls_debug(ca_cert_file_paths='{}', selected_ca_file='{}', selected_ca_exists={}, "
186
0
            "selected_ca_readable={}, SSL_CERT_FILE='{}', CURL_CA_BUNDLE='{}', SSL_CERT_DIR='{}')",
187
0
            config::ca_cert_file_paths, selected_ca_file, selected_ca_exists, selected_ca_readable,
188
0
            env_or_empty("SSL_CERT_FILE"), env_or_empty("CURL_CA_BUNDLE"),
189
0
            env_or_empty("SSL_CERT_DIR"));
190
0
}
191
#endif
192
193
constexpr char USE_PATH_STYLE[] = "use_path_style";
194
195
constexpr char AZURE_PROVIDER_STRING[] = "AZURE";
196
constexpr char S3_PROVIDER[] = "provider";
197
constexpr char S3_AK[] = "AWS_ACCESS_KEY";
198
constexpr char S3_SK[] = "AWS_SECRET_KEY";
199
constexpr char S3_ENDPOINT[] = "AWS_ENDPOINT";
200
constexpr char S3_REGION[] = "AWS_REGION";
201
constexpr char S3_TOKEN[] = "AWS_TOKEN";
202
constexpr char S3_MAX_CONN_SIZE[] = "AWS_MAX_CONNECTIONS";
203
constexpr char S3_REQUEST_TIMEOUT_MS[] = "AWS_REQUEST_TIMEOUT_MS";
204
constexpr char S3_CONN_TIMEOUT_MS[] = "AWS_CONNECTION_TIMEOUT_MS";
205
constexpr char S3_NEED_OVERRIDE_ENDPOINT[] = "AWS_NEED_OVERRIDE_ENDPOINT";
206
207
constexpr char S3_ROLE_ARN[] = "AWS_ROLE_ARN";
208
constexpr char S3_EXTERNAL_ID[] = "AWS_EXTERNAL_ID";
209
constexpr char S3_CREDENTIALS_PROVIDER_TYPE[] = "AWS_CREDENTIALS_PROVIDER_TYPE";
210
} // namespace
211
212
static std::atomic<int64_t> last_s3_get_token_bucket_tokens {0};
213
static std::atomic<int64_t> last_s3_get_token_limit {0};
214
static std::atomic<int64_t> last_s3_get_token_per_second {0};
215
static std::atomic<int64_t> last_s3_put_token_per_second {0};
216
static std::atomic<int64_t> last_s3_put_token_bucket_tokens {0};
217
static std::atomic<int64_t> last_s3_put_token_limit {0};
218
219
static std::atomic<bool> updating_get_limiter {false};
220
static std::atomic<bool> updating_put_limiter {false};
221
222
7
S3RateLimiterHolder* S3ClientFactory::rate_limiter(S3RateLimitType type) {
223
7
    CHECK(type == S3RateLimitType::GET || type == S3RateLimitType::PUT) << to_string(type);
224
7
    return _rate_limiters[static_cast<size_t>(type)].get();
225
7
}
226
227
template <S3RateLimitType LimiterType>
228
void update_rate_limiter_if_changed(int64_t current_tps, int64_t current_bucket,
229
                                    int64_t current_limit, std::atomic<int64_t>& last_tps,
230
                                    std::atomic<int64_t>& last_bucket,
231
                                    std::atomic<int64_t>& last_limit,
232
34
                                    std::atomic<bool>& updating_flag, const char* limiter_name) {
233
34
    if (last_tps.load(std::memory_order_relaxed) != current_tps ||
234
34
        last_bucket.load(std::memory_order_relaxed) != current_bucket ||
235
34
        last_limit.load(std::memory_order_relaxed) != current_limit) {
236
6
        bool expected = false;
237
6
        if (!updating_flag.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) {
238
0
            return;
239
0
        }
240
6
        if (last_tps.load(std::memory_order_acquire) != current_tps ||
241
6
            last_bucket.load(std::memory_order_acquire) != current_bucket ||
242
6
            last_limit.load(std::memory_order_acquire) != current_limit) {
243
6
            int ret =
244
6
                    reset_s3_rate_limiter(LimiterType, current_tps, current_bucket, current_limit);
245
246
6
            if (ret == 0) {
247
6
                last_tps.store(current_tps, std::memory_order_release);
248
6
                last_bucket.store(current_bucket, std::memory_order_release);
249
6
                last_limit.store(current_limit, std::memory_order_release);
250
6
            } else {
251
0
                LOG(WARNING) << "Failed to reset S3 " << limiter_name
252
0
                             << " rate limiter, error code: " << ret;
253
0
            }
254
6
        }
255
256
6
        updating_flag.store(false, std::memory_order_release);
257
6
    }
258
34
}
_ZN5doris30update_rate_limiter_if_changedILNS_15S3RateLimitTypeE0EEEvlllRSt6atomicIlES4_S4_RS2_IbEPKc
Line
Count
Source
232
17
                                    std::atomic<bool>& updating_flag, const char* limiter_name) {
233
17
    if (last_tps.load(std::memory_order_relaxed) != current_tps ||
234
17
        last_bucket.load(std::memory_order_relaxed) != current_bucket ||
235
17
        last_limit.load(std::memory_order_relaxed) != current_limit) {
236
5
        bool expected = false;
237
5
        if (!updating_flag.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) {
238
0
            return;
239
0
        }
240
5
        if (last_tps.load(std::memory_order_acquire) != current_tps ||
241
5
            last_bucket.load(std::memory_order_acquire) != current_bucket ||
242
5
            last_limit.load(std::memory_order_acquire) != current_limit) {
243
5
            int ret =
244
5
                    reset_s3_rate_limiter(LimiterType, current_tps, current_bucket, current_limit);
245
246
5
            if (ret == 0) {
247
5
                last_tps.store(current_tps, std::memory_order_release);
248
5
                last_bucket.store(current_bucket, std::memory_order_release);
249
5
                last_limit.store(current_limit, std::memory_order_release);
250
5
            } else {
251
0
                LOG(WARNING) << "Failed to reset S3 " << limiter_name
252
0
                             << " rate limiter, error code: " << ret;
253
0
            }
254
5
        }
255
256
5
        updating_flag.store(false, std::memory_order_release);
257
5
    }
258
17
}
_ZN5doris30update_rate_limiter_if_changedILNS_15S3RateLimitTypeE1EEEvlllRSt6atomicIlES4_S4_RS2_IbEPKc
Line
Count
Source
232
17
                                    std::atomic<bool>& updating_flag, const char* limiter_name) {
233
17
    if (last_tps.load(std::memory_order_relaxed) != current_tps ||
234
17
        last_bucket.load(std::memory_order_relaxed) != current_bucket ||
235
17
        last_limit.load(std::memory_order_relaxed) != current_limit) {
236
1
        bool expected = false;
237
1
        if (!updating_flag.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) {
238
0
            return;
239
0
        }
240
1
        if (last_tps.load(std::memory_order_acquire) != current_tps ||
241
1
            last_bucket.load(std::memory_order_acquire) != current_bucket ||
242
1
            last_limit.load(std::memory_order_acquire) != current_limit) {
243
1
            int ret =
244
1
                    reset_s3_rate_limiter(LimiterType, current_tps, current_bucket, current_limit);
245
246
1
            if (ret == 0) {
247
1
                last_tps.store(current_tps, std::memory_order_release);
248
1
                last_bucket.store(current_bucket, std::memory_order_release);
249
1
                last_limit.store(current_limit, std::memory_order_release);
250
1
            } else {
251
0
                LOG(WARNING) << "Failed to reset S3 " << limiter_name
252
0
                             << " rate limiter, error code: " << ret;
253
0
            }
254
1
        }
255
256
1
        updating_flag.store(false, std::memory_order_release);
257
1
    }
258
17
}
259
260
17
void check_s3_rate_limiter_config_changed() {
261
17
    update_rate_limiter_if_changed<S3RateLimitType::GET>(
262
17
            config::s3_get_token_per_second, config::s3_get_bucket_tokens,
263
17
            config::s3_get_token_limit, last_s3_get_token_per_second,
264
17
            last_s3_get_token_bucket_tokens, last_s3_get_token_limit, updating_get_limiter, "GET");
265
266
17
    update_rate_limiter_if_changed<S3RateLimitType::PUT>(
267
17
            config::s3_put_token_per_second, config::s3_put_bucket_tokens,
268
17
            config::s3_put_token_limit, last_s3_put_token_per_second,
269
17
            last_s3_put_token_bucket_tokens, last_s3_put_token_limit, updating_put_limiter, "PUT");
270
17
}
271
272
6
int reset_s3_rate_limiter(S3RateLimitType type, size_t max_speed, size_t max_burst, size_t limit) {
273
6
    if (type == S3RateLimitType::UNKNOWN) {
274
0
        return -1;
275
0
    }
276
6
    return S3ClientFactory::instance().rate_limiter(type)->reset(max_speed, max_burst, limit);
277
6
}
278
279
0
int64_t apply_s3_rate_limit(S3RateLimitType type) {
280
0
    return doris::apply_s3_rate_limit(type, S3ClientFactory::instance().rate_limiter(type),
281
0
                                      config::s3_rate_limiter_log_interval);
282
0
}
283
284
1
S3ClientFactory::S3ClientFactory() {
285
1
    _aws_options = Aws::SDKOptions {};
286
1
    auto logLevel = static_cast<Aws::Utils::Logging::LogLevel>(config::aws_log_level);
287
1
    _aws_options.loggingOptions.logLevel = logLevel;
288
1
    _aws_options.loggingOptions.logger_create_fn = [logLevel] {
289
1
        return std::make_shared<DorisAWSLogger>(logLevel);
290
1
    };
291
1
    Aws::InitAPI(_aws_options);
292
1
    _ca_cert_file_path = get_valid_ca_cert_path(doris::split(config::ca_cert_file_paths, ";"));
293
1
    _rate_limiters = {
294
1
            std::make_unique<S3RateLimiterHolder>(
295
1
                    config::s3_get_token_per_second, config::s3_get_bucket_tokens,
296
1
                    config::s3_get_token_limit, s3_rate_limiter_metric_func(S3RateLimitType::GET)),
297
1
            std::make_unique<S3RateLimiterHolder>(
298
1
                    config::s3_put_token_per_second, config::s3_put_bucket_tokens,
299
1
                    config::s3_put_token_limit, s3_rate_limiter_metric_func(S3RateLimitType::PUT))};
300
301
1
#ifdef USE_AZURE
302
1
    auto azureLogLevel =
303
1
            static_cast<Azure::Core::Diagnostics::Logger::Level>(config::azure_log_level);
304
1
    Azure::Core::Diagnostics::Logger::SetLevel(azureLogLevel);
305
1
    Azure::Core::Diagnostics::Logger::SetListener(
306
1
            [&](Azure::Core::Diagnostics::Logger::Level level, const std::string& message) {
307
0
                switch (level) {
308
0
                case Azure::Core::Diagnostics::Logger::Level::Verbose:
309
0
                    LOG(INFO) << message;
310
0
                    break;
311
0
                case Azure::Core::Diagnostics::Logger::Level::Informational:
312
0
                    LOG(INFO) << message;
313
0
                    break;
314
0
                case Azure::Core::Diagnostics::Logger::Level::Warning:
315
0
                    LOG(WARNING) << message;
316
0
                    break;
317
0
                case Azure::Core::Diagnostics::Logger::Level::Error:
318
0
                    LOG(ERROR) << message;
319
0
                    break;
320
0
                default:
321
0
                    LOG(WARNING) << "Unknown level: " << static_cast<int>(level)
322
0
                                 << ", message: " << message;
323
0
                    break;
324
0
                }
325
0
            });
326
1
#endif
327
1
}
328
329
1
S3ClientFactory::~S3ClientFactory() {
330
1
    Aws::ShutdownAPI(_aws_options);
331
1
}
332
333
31
S3ClientFactory& S3ClientFactory::instance() {
334
31
    static S3ClientFactory ret;
335
31
    return ret;
336
31
}
337
338
13
std::shared_ptr<io::ObjStorageClient> S3ClientFactory::create(const S3ClientConf& s3_conf) {
339
13
    if (!is_s3_conf_valid(s3_conf).ok()) {
340
0
        return nullptr;
341
0
    }
342
343
13
    check_s3_rate_limiter_config_changed();
344
345
13
#ifdef BE_TEST
346
13
    {
347
13
        std::lock_guard l(_lock);
348
13
        if (_test_client_creator) {
349
2
            return _test_client_creator(s3_conf);
350
2
        }
351
13
    }
352
11
#endif
353
354
11
    {
355
11
        uint64_t hash = s3_conf.get_hash();
356
11
        std::lock_guard l(_lock);
357
11
        auto it = _cache.find(hash);
358
11
        if (it != _cache.end()) {
359
7
            return it->second;
360
7
        }
361
11
    }
362
363
4
    auto obj_client = (s3_conf.provider == io::ObjStorageType::AZURE)
364
4
                              ? _create_azure_client(s3_conf)
365
4
                              : _create_s3_client(s3_conf);
366
367
4
    {
368
4
        uint64_t hash = s3_conf.get_hash();
369
4
        std::lock_guard l(_lock);
370
4
        _cache[hash] = obj_client;
371
4
    }
372
4
    return obj_client;
373
11
}
374
375
#ifdef BE_TEST
376
void S3ClientFactory::set_client_creator_for_test(
377
2
        std::function<std::shared_ptr<io::ObjStorageClient>(const S3ClientConf&)> creator) {
378
2
    std::lock_guard l(_lock);
379
2
    _test_client_creator = std::move(creator);
380
2
}
381
382
2
void S3ClientFactory::clear_client_creator_for_test() {
383
2
    std::lock_guard l(_lock);
384
2
    _test_client_creator = nullptr;
385
2
}
386
#endif
387
388
std::shared_ptr<io::ObjStorageClient> S3ClientFactory::_create_azure_client(
389
0
        const S3ClientConf& s3_conf) {
390
0
#ifdef USE_AZURE
391
0
    auto cred =
392
0
            std::make_shared<Azure::Storage::StorageSharedKeyCredential>(s3_conf.ak, s3_conf.sk);
393
394
0
    const std::string container_name = s3_conf.bucket;
395
0
    std::string uri = fmt::format("{}/{}", s3_conf.endpoint, container_name);
396
0
    if (s3_conf.endpoint.find("://") == std::string::npos) {
397
0
        uri = "https://" + uri;
398
0
    }
399
400
0
    Azure::Storage::Blobs::BlobClientOptions options;
401
0
    options.Retry.StatusCodes.insert(Azure::Core::Http::HttpStatusCode::TooManyRequests);
402
0
    options.Retry.MaxRetries = config::max_s3_client_retry;
403
0
    options.PerRetryPolicies.emplace_back(std::make_unique<AzureRetryRecordPolicy>());
404
0
    if (_ca_cert_file_path.empty()) {
405
0
        _ca_cert_file_path = get_valid_ca_cert_path(doris::split(config::ca_cert_file_paths, ";"));
406
0
    }
407
0
    if (!_ca_cert_file_path.empty()) {
408
0
        Azure::Core::Http::CurlTransportOptions curl_options;
409
0
        curl_options.CAInfo = _ca_cert_file_path;
410
0
        options.Transport.Transport =
411
0
                std::make_shared<Azure::Core::Http::CurlTransport>(std::move(curl_options));
412
0
    }
413
414
0
    std::string normalized_uri = normalize_http_uri(uri);
415
0
    VLOG_DEBUG << "uri:" << uri << ", normalized_uri:" << normalized_uri;
416
0
    std::string tls_debug_context = build_azure_tls_debug_context(_ca_cert_file_path);
417
418
0
    auto containerClient = std::make_shared<Azure::Storage::Blobs::BlobContainerClient>(
419
0
            uri, cred, std::move(options));
420
0
    LOG_INFO("create one azure client with {}", s3_conf.to_string());
421
0
    return std::make_shared<io::AzureObjStorageClient>(std::move(containerClient),
422
0
                                                       std::move(tls_debug_context));
423
#else
424
    LOG_FATAL("BE is not compiled with azure support, export BUILD_AZURE=ON before building");
425
    return nullptr;
426
#endif
427
0
}
428
429
std::shared_ptr<Aws::Auth::AWSCredentialsProvider>
430
6
S3ClientFactory::_get_aws_credentials_provider_v1(const S3ClientConf& s3_conf) {
431
6
    if (!s3_conf.ak.empty() && !s3_conf.sk.empty()) {
432
2
        Aws::Auth::AWSCredentials aws_cred(s3_conf.ak, s3_conf.sk);
433
2
        DCHECK(!aws_cred.IsExpiredOrEmpty());
434
2
        if (!s3_conf.token.empty()) {
435
0
            aws_cred.SetSessionToken(s3_conf.token);
436
0
        }
437
2
        return std::make_shared<Aws::Auth::SimpleAWSCredentialsProvider>(std::move(aws_cred));
438
2
    }
439
440
4
    if (s3_conf.cred_provider_type == CredProviderType::InstanceProfile) {
441
2
        if (s3_conf.role_arn.empty()) {
442
1
            return std::make_shared<Aws::Auth::InstanceProfileCredentialsProvider>();
443
1
        }
444
445
1
        Aws::Client::ClientConfiguration clientConfiguration =
446
1
                S3ClientFactory::getClientConfiguration();
447
448
1
        if (_ca_cert_file_path.empty()) {
449
0
            _ca_cert_file_path =
450
0
                    get_valid_ca_cert_path(doris::split(config::ca_cert_file_paths, ";"));
451
0
        }
452
1
        if (!_ca_cert_file_path.empty()) {
453
1
            clientConfiguration.caFile = _ca_cert_file_path;
454
1
        }
455
456
1
        auto stsClient = std::make_shared<Aws::STS::STSClient>(
457
1
                std::make_shared<Aws::Auth::InstanceProfileCredentialsProvider>(),
458
1
                clientConfiguration);
459
460
1
        return std::make_shared<Aws::Auth::STSAssumeRoleCredentialsProvider>(
461
1
                s3_conf.role_arn, Aws::String(), s3_conf.external_id,
462
1
                Aws::Auth::DEFAULT_CREDS_LOAD_FREQ_SECONDS, stsClient);
463
2
    }
464
465
    // Support anonymous access for public datasets when no credentials are provided
466
2
    if (s3_conf.ak.empty() && s3_conf.sk.empty()) {
467
2
        return std::make_shared<Aws::Auth::AnonymousAWSCredentialsProvider>();
468
2
    }
469
470
0
    return std::make_shared<Aws::Auth::DefaultAWSCredentialsProviderChain>();
471
2
}
472
473
std::shared_ptr<Aws::Auth::AWSCredentialsProvider> S3ClientFactory::_create_credentials_provider(
474
18
        CredProviderType type) {
475
18
    switch (type) {
476
2
    case CredProviderType::Env:
477
2
        return std::make_shared<Aws::Auth::EnvironmentAWSCredentialsProvider>();
478
2
    case CredProviderType::SystemProperties:
479
2
        return std::make_shared<Aws::Auth::ProfileConfigFileAWSCredentialsProvider>();
480
3
    case CredProviderType::WebIdentity:
481
3
        return std::make_shared<Aws::Auth::STSAssumeRoleWebIdentityCredentialsProvider>();
482
2
    case CredProviderType::Container:
483
2
        return std::make_shared<Aws::Auth::TaskRoleCredentialsProvider>(
484
2
                Aws::Environment::GetEnv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI").c_str());
485
4
    case CredProviderType::InstanceProfile:
486
4
        return std::make_shared<Aws::Auth::InstanceProfileCredentialsProvider>();
487
2
    case CredProviderType::Anonymous:
488
2
        return std::make_shared<Aws::Auth::AnonymousAWSCredentialsProvider>();
489
3
    case CredProviderType::Default:
490
3
    default:
491
3
        return std::make_shared<CustomAwsCredentialsProviderChain>();
492
18
    }
493
18
}
494
495
std::shared_ptr<Aws::Auth::AWSCredentialsProvider>
496
23
S3ClientFactory::_get_aws_credentials_provider_v2(const S3ClientConf& s3_conf) {
497
23
    if (!s3_conf.ak.empty() && !s3_conf.sk.empty()) {
498
5
        Aws::Auth::AWSCredentials aws_cred(s3_conf.ak, s3_conf.sk);
499
5
        DCHECK(!aws_cred.IsExpiredOrEmpty());
500
5
        if (!s3_conf.token.empty()) {
501
0
            aws_cred.SetSessionToken(s3_conf.token);
502
0
        }
503
5
        return std::make_shared<Aws::Auth::SimpleAWSCredentialsProvider>(std::move(aws_cred));
504
5
    }
505
506
    // Handle role_arn for assume role scenario
507
18
    if (!s3_conf.role_arn.empty()) {
508
8
        Aws::Client::ClientConfiguration clientConfiguration =
509
8
                S3ClientFactory::getClientConfiguration();
510
511
8
        if (_ca_cert_file_path.empty()) {
512
0
            _ca_cert_file_path =
513
0
                    get_valid_ca_cert_path(doris::split(config::ca_cert_file_paths, ";"));
514
0
        }
515
8
        if (!_ca_cert_file_path.empty()) {
516
8
            clientConfiguration.caFile = _ca_cert_file_path;
517
8
        }
518
519
8
        auto baseProvider = _create_credentials_provider(s3_conf.cred_provider_type);
520
8
        auto stsClient = std::make_shared<Aws::STS::STSClient>(baseProvider, clientConfiguration);
521
522
8
        return std::make_shared<Aws::Auth::STSAssumeRoleCredentialsProvider>(
523
8
                s3_conf.role_arn, Aws::String(), s3_conf.external_id,
524
8
                Aws::Auth::DEFAULT_CREDS_LOAD_FREQ_SECONDS, stsClient);
525
8
    }
526
527
    // Return provider based on cred_provider_type
528
10
    return _create_credentials_provider(s3_conf.cred_provider_type);
529
18
}
530
531
std::shared_ptr<Aws::Auth::AWSCredentialsProvider> S3ClientFactory::get_aws_credentials_provider(
532
29
        const S3ClientConf& s3_conf) {
533
29
    if (config::aws_credentials_provider_version == "v2") {
534
23
        return _get_aws_credentials_provider_v2(s3_conf);
535
23
    }
536
6
    return _get_aws_credentials_provider_v1(s3_conf);
537
29
}
538
539
std::shared_ptr<io::ObjStorageClient> S3ClientFactory::_create_s3_client(
540
4
        const S3ClientConf& s3_conf) {
541
4
    const bool s3_express = use_s3_express_client(s3_conf);
542
4
    TEST_SYNC_POINT_RETURN_WITH_VALUE(
543
3
            "s3_client_factory::create",
544
3
            std::make_shared<io::S3ObjStorageClient>(std::make_shared<Aws::S3::S3Client>()));
545
3
    Aws::Client::ClientConfiguration aws_config = S3ClientFactory::getClientConfiguration();
546
    // Let the SDK derive the zonal endpoint from the complete directory bucket name. A user
547
    // endpoint is intentionally ignored for S3 Express.
548
3
    if (!s3_express && s3_conf.need_override_endpoint) {
549
2
        aws_config.endpointOverride = s3_conf.endpoint;
550
2
    }
551
3
    aws_config.region = s3_conf.region;
552
553
3
    if (_ca_cert_file_path.empty()) {
554
0
        _ca_cert_file_path = get_valid_ca_cert_path(doris::split(config::ca_cert_file_paths, ";"));
555
0
    }
556
557
3
    if (!_ca_cert_file_path.empty()) {
558
3
        aws_config.caFile = _ca_cert_file_path;
559
3
    }
560
561
3
    if (s3_conf.max_connections > 0) {
562
0
        aws_config.maxConnections = s3_conf.max_connections;
563
3
    } else {
564
3
        aws_config.maxConnections = 102400;
565
3
    }
566
567
3
    aws_config.requestTimeoutMs = 30000;
568
3
    if (s3_conf.request_timeout_ms > 0) {
569
0
        aws_config.requestTimeoutMs = s3_conf.request_timeout_ms;
570
0
    }
571
572
3
    if (s3_conf.connect_timeout_ms > 0) {
573
0
        aws_config.connectTimeoutMs = s3_conf.connect_timeout_ms;
574
0
    }
575
576
3
    if (s3_express) {
577
1
        aws_config.scheme = Aws::Http::Scheme::HTTPS;
578
2
    } else if (config::s3_client_http_scheme == "http") {
579
2
        aws_config.scheme = Aws::Http::Scheme::HTTP;
580
2
    }
581
582
3
    aws_config.retryStrategy = std::make_shared<S3CustomRetryStrategy>(
583
3
            config::max_s3_client_retry /*scaleFactor = 25*/, /*retry_slow_down=*/true);
584
585
3
    std::shared_ptr<Aws::S3::S3Client> new_client;
586
3
    if (s3_express) {
587
        // Directory buckets use virtual-hosted endpoints. Do not propagate a user path-style
588
        // preference into the SDK client selected by the directory bucket name.
589
1
        Aws::S3::S3ClientConfiguration express_config(
590
1
                aws_config, Aws::Client::AWSAuthV4Signer::PayloadSigningPolicy::Never,
591
1
                true /* use virtual addressing */);
592
1
        express_config.disableS3ExpressAuth = false;
593
1
        new_client = std::make_shared<Aws::S3::S3Client>(
594
1
                get_aws_credentials_provider(s3_conf),
595
1
                Aws::MakeShared<Aws::S3::Endpoint::S3EndpointProvider>("S3Client"), express_config);
596
2
    } else {
597
2
        new_client = std::make_shared<Aws::S3::S3Client>(
598
2
                get_aws_credentials_provider(s3_conf), std::move(aws_config),
599
2
                Aws::Client::AWSAuthV4Signer::PayloadSigningPolicy::Never,
600
2
                s3_conf.use_virtual_addressing);
601
2
    }
602
603
3
    auto obj_client = std::make_shared<io::S3ObjStorageClient>(std::move(new_client));
604
3
    LOG_INFO("create one s3 client with {}", s3_conf.to_string());
605
3
    return obj_client;
606
4
}
607
608
Status S3ClientFactory::convert_properties_to_s3_conf(
609
23
        const std::map<std::string, std::string>& prop, const S3URI& s3_uri, S3Conf* s3_conf) {
610
23
    StringCaseMap<std::string> properties(prop.begin(), prop.end());
611
23
    if (auto it = properties.find(S3_AK); it != properties.end()) {
612
2
        s3_conf->client_conf.ak = it->second;
613
2
    }
614
23
    if (auto it = properties.find(S3_SK); it != properties.end()) {
615
2
        s3_conf->client_conf.sk = it->second;
616
2
    }
617
23
    if (auto it = properties.find(S3_TOKEN); it != properties.end()) {
618
0
        s3_conf->client_conf.token = it->second;
619
0
    }
620
23
    if (auto it = properties.find(S3_ENDPOINT); it != properties.end()) {
621
21
        s3_conf->client_conf.endpoint = it->second;
622
21
    }
623
23
    if (auto it = properties.find(S3_NEED_OVERRIDE_ENDPOINT); it != properties.end()) {
624
0
        s3_conf->client_conf.need_override_endpoint = (it->second == "true");
625
0
    }
626
23
    if (auto it = properties.find(S3_REGION); it != properties.end()) {
627
22
        s3_conf->client_conf.region = it->second;
628
22
    }
629
23
    if (auto it = properties.find(S3_MAX_CONN_SIZE); it != properties.end()) {
630
0
        if (!to_int(it->second, s3_conf->client_conf.max_connections)) {
631
0
            return Status::InvalidArgument("invalid {} value \"{}\"", S3_MAX_CONN_SIZE, it->second);
632
0
        }
633
0
    }
634
23
    if (auto it = properties.find(S3_REQUEST_TIMEOUT_MS); it != properties.end()) {
635
0
        if (!to_int(it->second, s3_conf->client_conf.request_timeout_ms)) {
636
0
            return Status::InvalidArgument("invalid {} value \"{}\"", S3_REQUEST_TIMEOUT_MS,
637
0
                                           it->second);
638
0
        }
639
0
    }
640
23
    if (auto it = properties.find(S3_CONN_TIMEOUT_MS); it != properties.end()) {
641
0
        if (!to_int(it->second, s3_conf->client_conf.connect_timeout_ms)) {
642
0
            return Status::InvalidArgument("invalid {} value \"{}\"", S3_CONN_TIMEOUT_MS,
643
0
                                           it->second);
644
0
        }
645
0
    }
646
23
    if (auto it = properties.find(S3_PROVIDER); it != properties.end()) {
647
        // S3 Provider properties should be case insensitive.
648
1
        if (0 == strcasecmp(it->second.c_str(), AZURE_PROVIDER_STRING)) {
649
0
            s3_conf->client_conf.provider = io::ObjStorageType::AZURE;
650
1
        } else if (0 == strcasecmp(it->second.c_str(), "BOS")) {
651
0
            s3_conf->client_conf.provider = io::ObjStorageType::BOS;
652
1
        } else if (0 == strcasecmp(it->second.c_str(), "COS")) {
653
0
            s3_conf->client_conf.provider = io::ObjStorageType::COS;
654
1
        } else if (0 == strcasecmp(it->second.c_str(), "OSS")) {
655
0
            s3_conf->client_conf.provider = io::ObjStorageType::OSS;
656
1
        } else if (0 == strcasecmp(it->second.c_str(), "OBS")) {
657
0
            s3_conf->client_conf.provider = io::ObjStorageType::OBS;
658
1
        } else if (0 == strcasecmp(it->second.c_str(), "GCP")) {
659
1
            s3_conf->client_conf.provider = io::ObjStorageType::GCP;
660
1
        } else if (0 == strcasecmp(it->second.c_str(), "TOS")) {
661
0
            s3_conf->client_conf.provider = io::ObjStorageType::TOS;
662
0
        }
663
1
    }
664
665
23
    if (s3_uri.get_bucket().empty()) {
666
0
        return Status::InvalidArgument("Invalid S3 URI {}, bucket is not specified",
667
0
                                       s3_uri.to_string());
668
0
    }
669
23
    s3_conf->bucket = s3_uri.get_bucket();
670
    // For azure's compatibility
671
23
    s3_conf->client_conf.bucket = s3_uri.get_bucket();
672
23
    s3_conf->prefix = "";
673
674
    // See https://sdk.amazonaws.com/cpp/api/LATEST/class_aws_1_1_s3_1_1_s3_client.html
675
23
    s3_conf->client_conf.use_virtual_addressing = true;
676
23
    if (auto it = properties.find(USE_PATH_STYLE); it != properties.end()) {
677
2
        s3_conf->client_conf.use_virtual_addressing = it->second != "true";
678
2
    }
679
680
23
    if (auto it = properties.find(S3_ROLE_ARN); it != properties.end()) {
681
        // Keep provider type as Default unless explicitly configured by
682
        // AWS_CREDENTIALS_PROVIDER_TYPE, consistent with FE behavior.
683
5
        s3_conf->client_conf.role_arn = it->second;
684
5
    }
685
686
23
    if (auto it = properties.find(S3_EXTERNAL_ID); it != properties.end()) {
687
0
        s3_conf->client_conf.external_id = it->second;
688
0
    }
689
690
23
    if (auto it = properties.find(S3_CREDENTIALS_PROVIDER_TYPE); it != properties.end()) {
691
8
        s3_conf->client_conf.cred_provider_type = cred_provider_type_from_string(it->second);
692
8
    }
693
694
23
    if (auto st = is_s3_conf_valid(s3_conf->client_conf); !st.ok()) {
695
4
        return st;
696
4
    }
697
19
    return Status::OK();
698
23
}
699
700
0
static CredProviderType cred_provider_type_from_thrift(TCredProviderType::type cred_provider_type) {
701
0
    switch (cred_provider_type) {
702
0
    case TCredProviderType::DEFAULT:
703
0
        return CredProviderType::Default;
704
0
    case TCredProviderType::SIMPLE:
705
0
        return CredProviderType::Simple;
706
0
    case TCredProviderType::INSTANCE_PROFILE:
707
0
        return CredProviderType::InstanceProfile;
708
0
    case TCredProviderType::ENV:
709
0
        return CredProviderType::Env;
710
0
    case TCredProviderType::SYSTEM_PROPERTIES:
711
0
        return CredProviderType::SystemProperties;
712
0
    case TCredProviderType::WEB_IDENTITY:
713
0
        return CredProviderType::WebIdentity;
714
0
    case TCredProviderType::CONTAINER:
715
0
        return CredProviderType::Container;
716
0
    case TCredProviderType::ANONYMOUS:
717
0
        return CredProviderType::Anonymous;
718
0
    default:
719
0
        __builtin_unreachable();
720
0
        LOG(WARNING) << "Invalid TCredProviderType value: " << cred_provider_type
721
0
                     << ", use default instead.";
722
0
        return CredProviderType::Default;
723
0
    }
724
0
}
725
726
0
S3Conf S3Conf::get_s3_conf(const cloud::ObjectStoreInfoPB& info) {
727
0
    S3Conf ret {
728
0
            .bucket = info.bucket(),
729
0
            .prefix = info.prefix(),
730
0
            .client_conf {
731
0
                    .endpoint = info.endpoint(),
732
0
                    .region = info.region(),
733
0
                    .ak = info.ak(),
734
0
                    .sk = info.sk(),
735
0
                    .token {},
736
0
                    .bucket = info.bucket(),
737
0
                    .provider = io::ObjStorageType::AWS,
738
0
                    .use_virtual_addressing =
739
0
                            info.has_use_path_style() ? !info.use_path_style() : true,
740
741
0
                    .role_arn = info.role_arn(),
742
0
                    .external_id = info.external_id(),
743
0
            },
744
0
            .sse_enabled = info.sse_enabled(),
745
0
    };
746
747
0
    if (info.has_cred_provider_type()) {
748
0
        ret.client_conf.cred_provider_type = cred_provider_type_from_pb(info.cred_provider_type());
749
0
    }
750
751
0
    io::ObjStorageType type = io::ObjStorageType::AWS;
752
0
    switch (info.provider()) {
753
0
    case cloud::ObjectStoreInfoPB_Provider_OSS:
754
0
        type = io::ObjStorageType::OSS;
755
0
        break;
756
0
    case cloud::ObjectStoreInfoPB_Provider_S3:
757
0
        type = io::ObjStorageType::AWS;
758
0
        break;
759
0
    case cloud::ObjectStoreInfoPB_Provider_COS:
760
0
        type = io::ObjStorageType::COS;
761
0
        break;
762
0
    case cloud::ObjectStoreInfoPB_Provider_OBS:
763
0
        type = io::ObjStorageType::OBS;
764
0
        break;
765
0
    case cloud::ObjectStoreInfoPB_Provider_BOS:
766
0
        type = io::ObjStorageType::BOS;
767
0
        break;
768
0
    case cloud::ObjectStoreInfoPB_Provider_GCP:
769
0
        type = io::ObjStorageType::GCP;
770
0
        break;
771
0
    case cloud::ObjectStoreInfoPB_Provider_AZURE:
772
0
        type = io::ObjStorageType::AZURE;
773
0
        break;
774
0
    case cloud::ObjectStoreInfoPB_Provider_TOS:
775
0
        type = io::ObjStorageType::TOS;
776
0
        break;
777
0
    default:
778
0
        __builtin_unreachable();
779
0
        LOG_FATAL("unknown provider type {}, info {}", info.provider(), ret.to_string());
780
0
    }
781
0
    ret.client_conf.provider = type;
782
0
    return ret;
783
0
}
784
785
0
S3Conf S3Conf::get_s3_conf(const TS3StorageParam& param) {
786
0
    S3Conf ret {
787
0
            .bucket = param.bucket,
788
0
            .prefix = param.root_path,
789
0
            .client_conf = {
790
0
                    .endpoint = param.endpoint,
791
0
                    .region = param.region,
792
0
                    .ak = param.ak,
793
0
                    .sk = param.sk,
794
0
                    .token = param.token,
795
0
                    .bucket = param.bucket,
796
0
                    .provider = io::ObjStorageType::AWS,
797
0
                    .max_connections = param.max_conn,
798
0
                    .request_timeout_ms = param.request_timeout_ms,
799
0
                    .connect_timeout_ms = param.conn_timeout_ms,
800
                    // When using cold heat separation in minio, user might use ip address directly,
801
                    // which needs enable use_virtual_addressing to true
802
0
                    .use_virtual_addressing = !param.use_path_style,
803
0
                    .role_arn = param.role_arn,
804
0
                    .external_id = param.external_id,
805
0
            }};
806
807
0
    if (param.__isset.cred_provider_type) {
808
0
        ret.client_conf.cred_provider_type =
809
0
                cred_provider_type_from_thrift(param.cred_provider_type);
810
0
    }
811
812
0
    io::ObjStorageType type = io::ObjStorageType::AWS;
813
0
    switch (param.provider) {
814
0
    case TObjStorageType::UNKNOWN:
815
0
        LOG_INFO("Receive one legal storage resource, set provider type to aws, param detail {}",
816
0
                 ret.to_string());
817
0
        type = io::ObjStorageType::AWS;
818
0
        break;
819
0
    case TObjStorageType::AWS:
820
0
        type = io::ObjStorageType::AWS;
821
0
        break;
822
0
    case TObjStorageType::AZURE:
823
0
        type = io::ObjStorageType::AZURE;
824
0
        break;
825
0
    case TObjStorageType::BOS:
826
0
        type = io::ObjStorageType::BOS;
827
0
        break;
828
0
    case TObjStorageType::COS:
829
0
        type = io::ObjStorageType::COS;
830
0
        break;
831
0
    case TObjStorageType::OBS:
832
0
        type = io::ObjStorageType::OBS;
833
0
        break;
834
0
    case TObjStorageType::OSS:
835
0
        type = io::ObjStorageType::OSS;
836
0
        break;
837
0
    case TObjStorageType::GCP:
838
0
        type = io::ObjStorageType::GCP;
839
0
        break;
840
0
    case TObjStorageType::TOS:
841
0
        type = io::ObjStorageType::TOS;
842
0
        break;
843
0
    default:
844
0
        LOG_FATAL("unknown provider type {}, info {}", param.provider, ret.to_string());
845
0
        __builtin_unreachable();
846
0
    }
847
0
    ret.client_conf.provider = type;
848
0
    return ret;
849
0
}
850
851
14
std::string hide_access_key(const std::string& ak) {
852
14
    std::string key = ak;
853
14
    size_t key_len = key.length();
854
14
    size_t reserved_count;
855
14
    if (key_len > 7) {
856
4
        reserved_count = 6;
857
10
    } else if (key_len > 2) {
858
6
        reserved_count = key_len - 2;
859
6
    } else {
860
4
        reserved_count = 0;
861
4
    }
862
863
14
    size_t x_count = key_len - reserved_count;
864
14
    size_t left_x_count = (x_count + 1) / 2;
865
866
14
    if (left_x_count > 0) {
867
13
        key.replace(0, left_x_count, left_x_count, 'x');
868
13
    }
869
870
14
    if (x_count - left_x_count > 0) {
871
12
        key.replace(key_len - (x_count - left_x_count), x_count - left_x_count,
872
12
                    x_count - left_x_count, 'x');
873
12
    }
874
14
    return key;
875
14
}
876
877
} // end namespace doris