Coverage Report

Created: 2026-07-19 16:36

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