Skip to content

Commit e9a2878

Browse files
plusplusjiajiawgtmac
authored andcommitted
feat(rest): route each object path to its vended S3 credential (per-path FileIO)
1 parent 567603f commit e9a2878

11 files changed

Lines changed: 250 additions & 200 deletions

File tree

‎src/iceberg/arrow/arrow_io.cc‎

Lines changed: 86 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include <limits>
2323
#include <mutex>
2424
#include <optional>
25+
#include <unordered_map>
2526
#include <vector>
2627

2728
#include <arrow/buffer.h>
@@ -35,6 +36,8 @@
3536
#include "iceberg/arrow/arrow_io_internal.h"
3637
#include "iceberg/arrow/arrow_io_util.h"
3738
#include "iceberg/arrow/arrow_status_internal.h"
39+
#include "iceberg/arrow/s3/arrow_s3_internal.h"
40+
#include "iceberg/util/location_util.h"
3841
#include "iceberg/util/macros.h"
3942

4043
namespace iceberg::arrow {
@@ -483,13 +486,53 @@ class ArrowOutputFile : public OutputFile {
483486

484487
} // namespace
485488

486-
Result<std::string> ArrowFileSystemFileIO::ResolvePath(const std::string& file_location) {
489+
const std::shared_ptr<::arrow::fs::FileSystem>& ArrowFileSystemFileIO::FileSystemForPath(
490+
std::string_view location) const {
491+
if (fs_by_prefix_.empty()) {
492+
return arrow_fs_;
493+
}
494+
// Longest matching prefix wins; fall back to the default file system.
495+
const std::string canonical = LocationUtil::CanonicalizeS3Scheme(location);
496+
const std::shared_ptr<::arrow::fs::FileSystem>* best = &arrow_fs_;
497+
size_t best_len = 0;
498+
for (const auto& [prefix, fs] : fs_by_prefix_) {
499+
if (prefix.size() > best_len && LocationUtil::PathHasPrefix(canonical, prefix)) {
500+
best = &fs;
501+
best_len = prefix.size();
502+
}
503+
}
504+
return *best;
505+
}
506+
507+
Status ArrowFileSystemFileIO::SetStorageCredentials(
508+
const std::vector<
509+
std::pair<std::string, std::unordered_map<std::string, std::string>>>&
510+
properties_by_prefix) {
511+
#if ICEBERG_S3_ENABLED
512+
std::vector<std::pair<std::string, std::shared_ptr<::arrow::fs::FileSystem>>>
513+
fs_by_prefix;
514+
fs_by_prefix.reserve(properties_by_prefix.size());
515+
for (const auto& [prefix, properties] : properties_by_prefix) {
516+
ICEBERG_ASSIGN_OR_RAISE(auto fs, BuildArrowS3FileSystem(properties));
517+
fs_by_prefix.emplace_back(prefix, std::move(fs));
518+
}
519+
fs_by_prefix_ = std::move(fs_by_prefix);
520+
return {};
521+
#else
522+
(void)properties_by_prefix;
523+
return NotSupported("S3 storage credentials require Arrow S3 support");
524+
#endif
525+
}
526+
527+
Result<std::string> ArrowFileSystemFileIO::ResolvePath(
528+
const std::shared_ptr<::arrow::fs::FileSystem>& fs,
529+
const std::string& file_location) {
487530
const auto pos = file_location.find("://");
488531
if (pos == std::string::npos) {
489532
return file_location;
490533
}
491534

492-
auto path = arrow_fs_->PathFromUri(file_location);
535+
auto path = fs->PathFromUri(file_location);
493536
if (path.ok()) {
494537
return std::move(path).ValueOrDie();
495538
}
@@ -512,14 +555,14 @@ Result<std::shared_ptr<::arrow::io::RandomAccessFile>> OpenArrowInputStream(
512555
ICEBERG_PRECHECK(io != nullptr, "FileIO cannot be null");
513556

514557
if (auto arrow_io = std::dynamic_pointer_cast<ArrowFileSystemFileIO>(io)) {
515-
ICEBERG_ASSIGN_OR_RAISE(auto resolved_path, arrow_io->ResolvePath(path));
558+
const auto& fs = arrow_io->FileSystemForPath(path);
559+
ICEBERG_ASSIGN_OR_RAISE(auto resolved_path, arrow_io->ResolvePath(fs, path));
516560
::arrow::fs::FileInfo file_info(resolved_path, ::arrow::fs::FileType::File);
517561
if (length.has_value()) {
518562
ICEBERG_ASSIGN_OR_RAISE(auto size, ToInt64Length(*length));
519563
file_info.set_size(size);
520564
}
521-
ICEBERG_ARROW_ASSIGN_OR_RETURN(auto input,
522-
arrow_io->arrow_fs_->OpenInputFile(file_info));
565+
ICEBERG_ARROW_ASSIGN_OR_RETURN(auto input, fs->OpenInputFile(file_info));
523566
return input;
524567
}
525568

@@ -543,16 +586,15 @@ Result<std::shared_ptr<::arrow::io::OutputStream>> OpenArrowOutputStream(
543586
ICEBERG_PRECHECK(io != nullptr, "FileIO cannot be null");
544587

545588
if (auto arrow_io = std::dynamic_pointer_cast<ArrowFileSystemFileIO>(io)) {
546-
ICEBERG_ASSIGN_OR_RAISE(auto resolved_path, arrow_io->ResolvePath(path));
589+
const auto& fs = arrow_io->FileSystemForPath(path);
590+
ICEBERG_ASSIGN_OR_RAISE(auto resolved_path, arrow_io->ResolvePath(fs, path));
547591
if (!overwrite) {
548-
ICEBERG_ARROW_ASSIGN_OR_RETURN(auto info,
549-
arrow_io->arrow_fs_->GetFileInfo(resolved_path));
592+
ICEBERG_ARROW_ASSIGN_OR_RETURN(auto info, fs->GetFileInfo(resolved_path));
550593
if (info.type() != ::arrow::fs::FileType::NotFound) {
551594
return AlreadyExists("File already exists: {}", path);
552595
}
553596
}
554-
ICEBERG_ARROW_ASSIGN_OR_RETURN(auto output,
555-
arrow_io->arrow_fs_->OpenOutputStream(resolved_path));
597+
ICEBERG_ARROW_ASSIGN_OR_RETURN(auto output, fs->OpenOutputStream(resolved_path));
556598
return output;
557599
}
558600

@@ -568,42 +610,60 @@ Result<std::shared_ptr<::arrow::io::OutputStream>> OpenArrowOutputStream(
568610

569611
Result<std::unique_ptr<InputFile>> ArrowFileSystemFileIO::NewInputFile(
570612
std::string file_location) {
571-
ICEBERG_ASSIGN_OR_RAISE(auto path, ResolvePath(file_location));
572-
return std::make_unique<ArrowInputFile>(arrow_fs_, std::move(file_location),
573-
std::move(path), std::nullopt);
613+
const auto& fs = FileSystemForPath(file_location);
614+
ICEBERG_ASSIGN_OR_RAISE(auto path, ResolvePath(fs, file_location));
615+
return std::make_unique<ArrowInputFile>(fs, std::move(file_location), std::move(path),
616+
std::nullopt);
574617
}
575618

576619
Result<std::unique_ptr<InputFile>> ArrowFileSystemFileIO::NewInputFile(
577620
std::string file_location, size_t length) {
578621
ICEBERG_ASSIGN_OR_RAISE(auto size, ToInt64Length(length));
579-
ICEBERG_ASSIGN_OR_RAISE(auto path, ResolvePath(file_location));
580-
return std::make_unique<ArrowInputFile>(arrow_fs_, std::move(file_location),
581-
std::move(path), size);
622+
const auto& fs = FileSystemForPath(file_location);
623+
ICEBERG_ASSIGN_OR_RAISE(auto path, ResolvePath(fs, file_location));
624+
return std::make_unique<ArrowInputFile>(fs, std::move(file_location), std::move(path),
625+
size);
582626
}
583627

584628
Result<std::unique_ptr<OutputFile>> ArrowFileSystemFileIO::NewOutputFile(
585629
std::string file_location) {
586-
ICEBERG_ASSIGN_OR_RAISE(auto path, ResolvePath(file_location));
587-
return std::make_unique<ArrowOutputFile>(arrow_fs_, std::move(file_location),
588-
std::move(path));
630+
const auto& fs = FileSystemForPath(file_location);
631+
ICEBERG_ASSIGN_OR_RAISE(auto path, ResolvePath(fs, file_location));
632+
return std::make_unique<ArrowOutputFile>(fs, std::move(file_location), std::move(path));
589633
}
590634

591635
/// \brief Delete a file at the given location.
592636
Status ArrowFileSystemFileIO::DeleteFile(const std::string& file_location) {
593-
ICEBERG_ASSIGN_OR_RAISE(auto path, ResolvePath(file_location));
594-
ICEBERG_ARROW_RETURN_NOT_OK(arrow_fs_->DeleteFile(path));
637+
const auto& fs = FileSystemForPath(file_location);
638+
ICEBERG_ASSIGN_OR_RAISE(auto path, ResolvePath(fs, file_location));
639+
ICEBERG_ARROW_RETURN_NOT_OK(fs->DeleteFile(path));
595640
return {};
596641
}
597642

598643
Status ArrowFileSystemFileIO::DeleteFiles(
599644
const std::vector<std::string>& file_locations) {
600-
std::vector<std::string> paths;
601-
paths.reserve(file_locations.size());
645+
if (fs_by_prefix_.empty()) {
646+
// No per-prefix routing: one batched delete on the default file system.
647+
std::vector<std::string> paths;
648+
paths.reserve(file_locations.size());
649+
for (const auto& file_location : file_locations) {
650+
ICEBERG_ASSIGN_OR_RAISE(auto path, ResolvePath(arrow_fs_, file_location));
651+
paths.push_back(std::move(path));
652+
}
653+
ICEBERG_ARROW_RETURN_NOT_OK(arrow_fs_->DeleteFiles(paths));
654+
return {};
655+
}
656+
657+
// Paths may route to different file systems: group by fs, then batch per fs.
658+
std::unordered_map<::arrow::fs::FileSystem*, std::vector<std::string>> paths_by_fs;
602659
for (const auto& file_location : file_locations) {
603-
ICEBERG_ASSIGN_OR_RAISE(auto path, ResolvePath(file_location));
604-
paths.push_back(std::move(path));
660+
const auto& fs = FileSystemForPath(file_location);
661+
ICEBERG_ASSIGN_OR_RAISE(auto path, ResolvePath(fs, file_location));
662+
paths_by_fs[fs.get()].push_back(std::move(path));
663+
}
664+
for (auto& [fs, paths] : paths_by_fs) {
665+
ICEBERG_ARROW_RETURN_NOT_OK(fs->DeleteFiles(paths));
605666
}
606-
ICEBERG_ARROW_RETURN_NOT_OK(arrow_fs_->DeleteFiles(paths));
607667
return {};
608668
}
609669

‎src/iceberg/arrow/arrow_io_internal.h‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
#include <memory>
2424
#include <optional>
2525
#include <string>
26+
#include <string_view>
27+
#include <unordered_map>
28+
#include <utility>
2629
#include <vector>
2730

2831
#include <arrow/filesystem/type_fwd.h>
@@ -52,7 +55,8 @@ OpenArrowOutputStream(const std::shared_ptr<FileIO>& io, const std::string& path
5255
bool overwrite = true);
5356

5457
/// \brief A concrete implementation of FileIO for Arrow file system.
55-
class ICEBERG_BUNDLE_EXPORT ArrowFileSystemFileIO : public FileIO {
58+
class ICEBERG_BUNDLE_EXPORT ArrowFileSystemFileIO : public FileIO,
59+
public SupportsStorageCredentials {
5660
public:
5761
explicit ArrowFileSystemFileIO(std::shared_ptr<::arrow::fs::FileSystem> arrow_fs)
5862
: arrow_fs_(std::move(arrow_fs)) {}
@@ -81,6 +85,12 @@ class ICEBERG_BUNDLE_EXPORT ArrowFileSystemFileIO : public FileIO {
8185
/// \brief Delete files at the given locations.
8286
Status DeleteFiles(const std::vector<std::string>& file_locations) override;
8387

88+
/// \brief Build one S3 file system per credential prefix for per-path routing.
89+
Status SetStorageCredentials(
90+
const std::vector<
91+
std::pair<std::string, std::unordered_map<std::string, std::string>>>&
92+
properties_by_prefix) override;
93+
8494
/// \brief Get the Arrow file system.
8595
const std::shared_ptr<::arrow::fs::FileSystem>& fs() const { return arrow_fs_; }
8696

@@ -92,10 +102,18 @@ class ICEBERG_BUNDLE_EXPORT ArrowFileSystemFileIO : public FileIO {
92102
friend Result<std::shared_ptr<::arrow::io::OutputStream>> OpenArrowOutputStream(
93103
const std::shared_ptr<FileIO>& io, const std::string& path, bool overwrite);
94104

95-
/// \brief Resolve a file location to a filesystem path.
96-
Result<std::string> ResolvePath(const std::string& file_location);
105+
/// \brief Pick the file system for `location` (longest matching prefix, else
106+
/// the default).
107+
const std::shared_ptr<::arrow::fs::FileSystem>& FileSystemForPath(
108+
std::string_view location) const;
109+
110+
/// \brief Resolve a file location to a filesystem path using `fs`.
111+
Result<std::string> ResolvePath(const std::shared_ptr<::arrow::fs::FileSystem>& fs,
112+
const std::string& file_location);
97113

98114
std::shared_ptr<::arrow::fs::FileSystem> arrow_fs_;
115+
std::vector<std::pair<std::string, std::shared_ptr<::arrow::fs::FileSystem>>>
116+
fs_by_prefix_;
99117
};
100118

101119
} // namespace iceberg::arrow

‎src/iceberg/arrow/s3/arrow_s3_file_io.cc‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include <optional>
2222
#include <string>
2323
#include <string_view>
24+
#include <utility>
2425

2526
#include <arrow/filesystem/filesystem.h>
2627
#if ICEBERG_S3_ENABLED
@@ -172,15 +173,21 @@ Result<::arrow::fs::S3Options> ConfigureS3Options(
172173
}
173174
#endif
174175

175-
Result<std::unique_ptr<FileIO>> MakeS3FileIO(
176-
const std::unordered_map<std::string, std::string>& properties) {
177176
#if ICEBERG_S3_ENABLED
177+
Result<std::shared_ptr<::arrow::fs::FileSystem>> BuildArrowS3FileSystem(
178+
const std::unordered_map<std::string, std::string>& properties) {
178179
ICEBERG_RETURN_UNEXPECTED(EnsureS3Initialized());
179-
180-
// Configure S3 options from properties (uses default credentials if empty)
181180
ICEBERG_ASSIGN_OR_RAISE(auto options, ConfigureS3Options(properties));
182181
ICEBERG_ARROW_ASSIGN_OR_RETURN(auto fs, ::arrow::fs::S3FileSystem::Make(options));
182+
return std::shared_ptr<::arrow::fs::FileSystem>(std::move(fs));
183+
}
184+
#endif
183185

186+
Result<std::unique_ptr<FileIO>> MakeS3FileIO(
187+
const std::unordered_map<std::string, std::string>& properties) {
188+
#if ICEBERG_S3_ENABLED
189+
// Uses default credentials if properties are empty.
190+
ICEBERG_ASSIGN_OR_RAISE(auto fs, BuildArrowS3FileSystem(properties));
184191
return std::make_unique<ArrowFileSystemFileIO>(std::move(fs));
185192
#else
186193
return NotSupported("Arrow S3 support is not enabled");

‎src/iceberg/arrow/s3/arrow_s3_internal.h‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
#pragma once
2121

22+
#include <memory>
2223
#include <string>
2324
#include <unordered_map>
2425

@@ -39,6 +40,11 @@ namespace iceberg::arrow {
3940
/// addressing style) can be unit tested without a live S3 endpoint.
4041
ICEBERG_BUNDLE_EXPORT Result<::arrow::fs::S3Options> ConfigureS3Options(
4142
const std::unordered_map<std::string, std::string>& properties);
43+
44+
/// \brief Build an Arrow S3 file system from a properties map (initializes S3 if
45+
/// needed). Exposed so the credential-aware FileIO can build one fs per prefix.
46+
ICEBERG_BUNDLE_EXPORT Result<std::shared_ptr<::arrow::fs::FileSystem>>
47+
BuildArrowS3FileSystem(const std::unordered_map<std::string, std::string>& properties);
4248
#endif
4349

4450
} // namespace iceberg::arrow

‎src/iceberg/catalog/rest/rest_catalog.cc‎

Lines changed: 20 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -525,31 +525,25 @@ Result<std::shared_ptr<auth::AuthSession>> RestCatalog::TableAuthSession(
525525
Result<std::shared_ptr<FileIO>> RestCatalog::TableFileIO(
526526
const SessionContext& /*context*/,
527527
const std::unordered_map<std::string, std::string>& table_config,
528-
const std::vector<StorageCredential>& storage_credentials,
529-
std::string_view storage_location) const {
530-
// Bind one S3 FileIO from the vended credential whose prefix best matches the
531-
// table location (the common one-credential-per-table case).
532-
// TODO: support per-path credential routing, STS refresh via credentials.uri,
533-
// and non-S3 (GCS/ADLS) credentials, like Java's S3FileIO.
534-
if (const StorageCredential* s3_cred =
535-
SelectS3StorageCredential(storage_credentials, storage_location);
536-
s3_cred != nullptr) {
537-
ICEBERG_ASSIGN_OR_RAISE(
538-
auto io, MakeS3FileIOFromCredential(config_.configs(), table_config, *s3_cred));
528+
const std::vector<StorageCredential>& storage_credentials) const {
529+
if (!storage_credentials.empty()) {
530+
// Only non-S3 (GCS/ADLS) credentials vended, which we can't honor; fail fast.
531+
if (HasOnlyNonS3StorageCredentials(storage_credentials)) {
532+
auto prefixes =
533+
storage_credentials | std::views::transform(&StorageCredential::prefix);
534+
return NotSupported(
535+
"Vended storage credentials {} are unsupported (only S3-family "
536+
"credentials are supported)",
537+
FormatRange(prefixes, ", ", "[", "]"));
538+
}
539+
// Build a FileIO that routes each path to its vended credential. TODO: STS
540+
// refresh via credentials.uri is not yet supported.
541+
ICEBERG_ASSIGN_OR_RAISE(auto io,
542+
MakeS3FileIOFromCredentials(config_.configs(), table_config,
543+
storage_credentials));
539544
return std::shared_ptr<FileIO>(std::move(io));
540545
}
541546

542-
// Only non-S3 (e.g. GCS/ADLS) credentials were vended and we can't honor them;
543-
// fail fast. (S3 credentials that simply don't match fall through below.)
544-
if (HasOnlyNonS3StorageCredentials(storage_credentials)) {
545-
auto prefixes =
546-
storage_credentials | std::views::transform(&StorageCredential::prefix);
547-
return NotSupported(
548-
"Vended storage credentials {} are unsupported (only S3-family "
549-
"credentials are supported)",
550-
FormatRange(prefixes, ", ", "[", "]"));
551-
}
552-
553547
ICEBERG_RETURN_UNEXPECTED(ValidateNoFileIOConfig(table_config));
554548
return file_io_;
555549
}
@@ -789,9 +783,8 @@ Result<std::shared_ptr<Transaction>> RestCatalog::StageCreateTable(
789783
/*stage_create=*/true, *contextual_session));
790784
auto table_config = std::move(result.config);
791785
auto storage_credentials = std::move(result.storage_credentials);
792-
ICEBERG_ASSIGN_OR_RAISE(
793-
auto table_io,
794-
TableFileIO(context, table_config, storage_credentials, result.metadata->location));
786+
ICEBERG_ASSIGN_OR_RAISE(auto table_io,
787+
TableFileIO(context, table_config, storage_credentials));
795788
ICEBERG_ASSIGN_OR_RAISE(
796789
auto table_session,
797790
TableAuthSession(identifier, table_config, std::move(contextual_session)));
@@ -906,9 +899,8 @@ Result<std::shared_ptr<Table>> RestCatalog::MakeTableFromLoadResult(
906899
std::shared_ptr<auth::AuthSession> contextual_session) {
907900
auto table_config = std::move(result.config);
908901
auto storage_credentials = std::move(result.storage_credentials);
909-
ICEBERG_ASSIGN_OR_RAISE(
910-
auto table_io,
911-
TableFileIO(context, table_config, storage_credentials, result.metadata->location));
902+
ICEBERG_ASSIGN_OR_RAISE(auto table_io,
903+
TableFileIO(context, table_config, storage_credentials));
912904
ICEBERG_ASSIGN_OR_RAISE(
913905
auto table_session,
914906
TableAuthSession(identifier, table_config, std::move(contextual_session)));

‎src/iceberg/catalog/rest/rest_catalog.h‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,7 @@ class ICEBERG_REST_EXPORT RestCatalog final
8080
Result<std::shared_ptr<FileIO>> TableFileIO(
8181
const SessionContext& context,
8282
const std::unordered_map<std::string, std::string>& table_config,
83-
const std::vector<StorageCredential>& storage_credentials,
84-
std::string_view storage_location) const;
83+
const std::vector<StorageCredential>& storage_credentials) const;
8584

8685
Result<std::vector<Namespace>> ListNamespaces(const Namespace& ns,
8786
auth::AuthSession& session) const;

0 commit comments

Comments
 (0)