Skip to content

Commit 2bab861

Browse files
Sandeep GottimukkalaSandeep Gottimukkala
authored andcommitted
feat(rest): support storage credentials in scan planning responses
Parse storage-credentials from PlanTableScanResponse, FetchPlanningResultResponse, and FetchScanTasksResponse. When credentials are present, build a scan-scoped FileIO via MakeTableFileIO and expose it through RestTableScan::effective_io() for callers to use when reading the returned scan tasks.
1 parent d8a7d6c commit 2bab861

8 files changed

Lines changed: 205 additions & 89 deletions

File tree

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,15 @@ Result<nlohmann::json> ScanTaskFieldsToJson(
531531
json[kFileScanTasks] = std::move(tasks_json);
532532
}
533533

534+
if (!response.storage_credentials.empty()) {
535+
nlohmann::json creds_json = nlohmann::json::array();
536+
for (const auto& cred : response.storage_credentials) {
537+
ICEBERG_ASSIGN_OR_RAISE(auto entry, StorageCredentialToJson(cred));
538+
creds_json.push_back(std::move(entry));
539+
}
540+
json[kStorageCredentials] = std::move(creds_json);
541+
}
542+
534543
return json;
535544
}
536545

@@ -571,6 +580,21 @@ Status ScanTaskFieldsFromJson(
571580
FileScanTasksFromJson(file_scan_tasks_json, response.delete_files,
572581
partition_specs_by_id, schema));
573582
}
583+
584+
// 4. storage_credentials
585+
if (json.contains(kStorageCredentials)) {
586+
ICEBERG_ASSIGN_OR_RAISE(auto creds_json,
587+
GetJsonValue<nlohmann::json>(json, kStorageCredentials));
588+
if (!creds_json.is_array()) {
589+
return JsonParseError("Cannot parse storage credentials from non-array: {}",
590+
SafeDumpJson(creds_json));
591+
}
592+
for (const auto& entry : creds_json) {
593+
ICEBERG_ASSIGN_OR_RAISE(auto cred, StorageCredentialFromJson(entry));
594+
response.storage_credentials.push_back(std::move(cred));
595+
}
596+
}
597+
574598
return {};
575599
}
576600

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@
4343
#include "iceberg/catalog/rest/rest_util.h"
4444
#include "iceberg/catalog/rest/types.h"
4545
#include "iceberg/json_serde_internal.h"
46-
#include "iceberg/logging/logger.h"
4746
#include "iceberg/logging/log_level.h"
47+
#include "iceberg/logging/logger.h"
4848
#include "iceberg/metrics/metrics_reporters.h"
4949
#include "iceberg/partition_spec.h"
5050
#include "iceberg/result.h"
@@ -934,6 +934,8 @@ Result<std::shared_ptr<Table>> RestCatalog::MakeTableFromLoadResult(
934934
.session = table_session,
935935
.supported_endpoints = supported_endpoints_,
936936
.identifier = identifier,
937+
.catalog_config = config_.configs(),
938+
.table_config = table_config,
937939
};
938940
return RestTable::Make(identifier, std::move(result.metadata),
939941
std::move(result.metadata_location), std::move(table_io),

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

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include "iceberg/catalog/rest/http_client.h"
3030
#include "iceberg/catalog/rest/json_serde_internal.h"
3131
#include "iceberg/catalog/rest/resource_paths.h"
32+
#include "iceberg/catalog/rest/rest_file_io.h"
3233
#include "iceberg/catalog/rest/types.h"
3334
#include "iceberg/json_serde_internal.h"
3435
#include "iceberg/partition_spec.h"
@@ -115,8 +116,7 @@ Result<std::vector<std::shared_ptr<FileScanTask>>> RestTableScan::PlanTableScan(
115116
}
116117
}
117118

118-
ICEBERG_ASSIGN_OR_RAISE(auto path,
119-
rest_context_.paths->Plan(rest_context_.identifier));
119+
ICEBERG_ASSIGN_OR_RAISE(auto path, rest_context_.paths->Plan(rest_context_.identifier));
120120
ICEBERG_ASSIGN_OR_RAISE(auto request_json, ToJson(request));
121121
ICEBERG_ASSIGN_OR_RAISE(auto json_request, ToJsonString(request_json));
122122
ICEBERG_ASSIGN_OR_RAISE(
@@ -132,6 +132,7 @@ Result<std::vector<std::shared_ptr<FileScanTask>>> RestTableScan::PlanTableScan(
132132

133133
switch (result.plan_status) {
134134
case PlanStatus::kCompleted: {
135+
ICEBERG_RETURN_UNEXPECTED(ApplyStorageCredentials(result.storage_credentials));
135136
auto tasks = ResolveScanTasks(result.plan_tasks, result.file_scan_tasks, specs);
136137
if (!tasks.has_value()) CancelPlanning(plan_id);
137138
return tasks;
@@ -140,7 +141,7 @@ Result<std::vector<std::shared_ptr<FileScanTask>>> RestTableScan::PlanTableScan(
140141
return FetchPlanningResult(plan_id, specs);
141142
case PlanStatus::kFailed:
142143
return IOError("Scan planning failed: {}",
143-
result.error ? result.error->message : "unknown error");
144+
result.error ? result.error->message : "unknown error");
144145
case PlanStatus::kCancelled:
145146
return IOError("Scan planning was cancelled for plan_id={}", plan_id);
146147
}
@@ -171,6 +172,7 @@ Result<std::vector<std::shared_ptr<FileScanTask>>> RestTableScan::FetchPlanningR
171172

172173
switch (result.plan_status) {
173174
case PlanStatus::kCompleted: {
175+
ICEBERG_RETURN_UNEXPECTED(ApplyStorageCredentials(result.storage_credentials));
174176
auto tasks = ResolveScanTasks(result.plan_tasks, result.file_scan_tasks, specs);
175177
if (!tasks.has_value()) CancelPlanning(plan_id);
176178
return tasks;
@@ -181,9 +183,8 @@ Result<std::vector<std::shared_ptr<FileScanTask>>> RestTableScan::FetchPlanningR
181183
.count();
182184
if (elapsed_ms >= kMaxWaitTimeMs) {
183185
CancelPlanning(plan_id);
184-
return IOError(
185-
"Scan planning timed out after {}ms waiting for plan_id={}", elapsed_ms,
186-
plan_id);
186+
return IOError("Scan planning timed out after {}ms waiting for plan_id={}",
187+
elapsed_ms, plan_id);
187188
}
188189
std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));
189190
delay_ms = std::min(delay_ms * 2, kMaxSleepMs);
@@ -192,15 +193,15 @@ Result<std::vector<std::shared_ptr<FileScanTask>>> RestTableScan::FetchPlanningR
192193
case PlanStatus::kFailed:
193194
CancelPlanning(plan_id);
194195
return IOError("Scan planning failed: {}",
195-
result.error ? result.error->message : "unknown error");
196+
result.error ? result.error->message : "unknown error");
196197
case PlanStatus::kCancelled:
197198
return IOError("Scan planning was cancelled for plan_id={}", plan_id);
198199
}
199200
}
200201

201202
CancelPlanning(plan_id);
202-
return IOError("Scan planning exceeded max retries ({}) for plan_id={}",
203-
kMaxRetries, plan_id);
203+
return IOError("Scan planning exceeded max retries ({}) for plan_id={}", kMaxRetries,
204+
plan_id);
204205
}
205206

206207
Result<std::vector<std::shared_ptr<FileScanTask>>> RestTableScan::FetchScanTasks(
@@ -212,15 +213,15 @@ Result<std::vector<std::shared_ptr<FileScanTask>>> RestTableScan::FetchScanTasks
212213
rest_context_.paths->FetchScanTasks(rest_context_.identifier));
213214
FetchScanTasksRequest request{.planTask = plan_task};
214215
ICEBERG_ASSIGN_OR_RAISE(auto json_request, ToJsonString(ToJson(request)));
215-
ICEBERG_ASSIGN_OR_RAISE(
216-
const auto response,
217-
rest_context_.client->Post(path, json_request, /*headers=*/{},
218-
*PlanTaskErrorHandler::Instance(),
219-
*rest_context_.session));
216+
ICEBERG_ASSIGN_OR_RAISE(const auto response,
217+
rest_context_.client->Post(path, json_request, /*headers=*/{},
218+
*PlanTaskErrorHandler::Instance(),
219+
*rest_context_.session));
220220
ICEBERG_ASSIGN_OR_RAISE(auto json, FromJsonString(response.body()));
221221
ICEBERG_ASSIGN_OR_RAISE(auto result,
222222
FetchScanTasksResponseFromJson(json, specs, *schema_));
223223
ICEBERG_RETURN_UNEXPECTED(result.Validate());
224+
ICEBERG_RETURN_UNEXPECTED(ApplyStorageCredentials(result.storage_credentials));
224225

225226
return ResolveScanTasks(result.plan_tasks, result.file_scan_tasks, specs);
226227
}
@@ -253,18 +254,31 @@ void RestTableScan::CancelPlanning(const std::string& plan_id) const {
253254
if (!path.has_value()) return;
254255

255256
// Best-effort: ignore errors.
256-
std::ignore = rest_context_.client->Delete(*path, /*params=*/{}, /*headers=*/{},
257-
*PlanErrorHandler::Instance(),
258-
*rest_context_.session);
257+
std::ignore =
258+
rest_context_.client->Delete(*path, /*params=*/{}, /*headers=*/{},
259+
*PlanErrorHandler::Instance(), *rest_context_.session);
260+
}
261+
262+
const std::shared_ptr<FileIO>& RestTableScan::effective_io() const {
263+
return scan_io_ ? scan_io_ : io_;
264+
}
265+
266+
Status RestTableScan::ApplyStorageCredentials(
267+
const std::vector<StorageCredential>& credentials) const {
268+
if (credentials.empty()) return {};
269+
ICEBERG_ASSIGN_OR_RAISE(
270+
auto io, MakeTableFileIO(rest_context_.catalog_config, rest_context_.table_config,
271+
credentials));
272+
scan_io_ = std::move(io);
273+
return {};
259274
}
260275

261276
// RestTableScanBuilder
262277

263-
RestTableScanBuilder::RestTableScanBuilder(std::shared_ptr<TableMetadata> metadata,
264-
std::shared_ptr<FileIO> io,
265-
std::string table_name,
266-
std::shared_ptr<MetricsReporter> metrics_reporter,
267-
RestScanContext rest_context)
278+
RestTableScanBuilder::RestTableScanBuilder(
279+
std::shared_ptr<TableMetadata> metadata, std::shared_ptr<FileIO> io,
280+
std::string table_name, std::shared_ptr<MetricsReporter> metrics_reporter,
281+
RestScanContext rest_context)
268282
: DataTableScanBuilder(std::move(metadata), std::move(io), std::move(table_name),
269283
std::move(metrics_reporter)),
270284
rest_context_(std::move(rest_context)) {}

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

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include "iceberg/catalog/rest/iceberg_rest_export.h"
3030
#include "iceberg/metrics/metrics_reporter.h"
3131
#include "iceberg/result.h"
32+
#include "iceberg/storage_credential.h"
3233
#include "iceberg/table_identifier.h"
3334
#include "iceberg/table_scan.h"
3435
#include "iceberg/type_fwd.h"
@@ -52,6 +53,11 @@ struct ICEBERG_REST_EXPORT RestScanContext {
5253
std::shared_ptr<auth::AuthSession> session;
5354
std::unordered_set<Endpoint> supported_endpoints;
5455
TableIdentifier identifier;
56+
/// Catalog-level config, used with table_config to build a scan-scoped FileIO
57+
/// when the server vends storage credentials in a planning response.
58+
std::unordered_map<std::string, std::string> catalog_config;
59+
/// Table-level config merged with catalog_config for scan-scoped FileIO creation.
60+
std::unordered_map<std::string, std::string> table_config;
5561
};
5662

5763
/// \brief A DataTableScan that delegates PlanFiles() to the REST catalog server
@@ -69,6 +75,13 @@ class ICEBERG_REST_EXPORT RestTableScan : public DataTableScan {
6975
/// \brief Plans files via the REST scan planning endpoints.
7076
Result<std::vector<std::shared_ptr<FileScanTask>>> PlanFiles() const override;
7177

78+
/// \brief Returns the FileIO to use when reading scan results.
79+
///
80+
/// If the server vended storage credentials during planning, returns a FileIO
81+
/// initialised with those credentials; otherwise returns the table's FileIO.
82+
/// Must be called after PlanFiles().
83+
const std::shared_ptr<FileIO>& effective_io() const;
84+
7285
private:
7386
RestTableScan(std::shared_ptr<TableMetadata> metadata, std::shared_ptr<Schema> schema,
7487
std::shared_ptr<FileIO> io, internal::TableScanContext context,
@@ -98,14 +111,19 @@ class ICEBERG_REST_EXPORT RestTableScan : public DataTableScan {
98111
/// DELETE /plan/{plan_id}; best-effort, errors are silently ignored.
99112
void CancelPlanning(const std::string& plan_id) const;
100113

114+
/// Builds a scan-scoped FileIO from vended credentials and caches it in scan_io_.
115+
/// No-op if credentials is empty.
116+
Status ApplyStorageCredentials(const std::vector<StorageCredential>& credentials) const;
117+
101118
RestScanContext rest_context_;
119+
mutable std::shared_ptr<FileIO> scan_io_;
102120
};
103121

104122
/// \brief Builder that produces a RestTableScan with the REST HTTP context injected.
105123
class ICEBERG_REST_EXPORT RestTableScanBuilder : public DataTableScanBuilder {
106124
public:
107-
RestTableScanBuilder(std::shared_ptr<TableMetadata> metadata, std::shared_ptr<FileIO> io,
108-
std::string table_name,
125+
RestTableScanBuilder(std::shared_ptr<TableMetadata> metadata,
126+
std::shared_ptr<FileIO> io, std::string table_name,
109127
std::shared_ptr<MetricsReporter> metrics_reporter,
110128
RestScanContext rest_context);
111129

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ bool OptionalSharedPtrVectorEqual(
210210
template <typename Response>
211211
bool ScanTaskFieldsEqual(const Response& lhs, const Response& rhs) {
212212
return lhs.plan_tasks == rhs.plan_tasks &&
213+
lhs.storage_credentials == rhs.storage_credentials &&
213214
SharedPtrVectorEqual(lhs.delete_files, rhs.delete_files) &&
214215
OptionalSharedPtrVectorEqual(lhs.file_scan_tasks, rhs.file_scan_tasks,
215216
FileScanTaskEqual);

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ struct ICEBERG_REST_EXPORT PlanTableScanResponse {
337337
PlanStatus plan_status = PlanStatus::kCompleted;
338338
std::string plan_id;
339339
std::optional<ErrorResponse> error;
340-
// TODO(sandeepg): Add storage credentials and bind scan FileIO to them.
340+
std::vector<StorageCredential> storage_credentials;
341341

342342
Status Validate() const;
343343

@@ -352,7 +352,7 @@ struct ICEBERG_REST_EXPORT FetchPlanningResultResponse {
352352
std::vector<std::shared_ptr<DataFile>> delete_files;
353353
PlanStatus plan_status = PlanStatus::kCompleted;
354354
std::optional<ErrorResponse> error;
355-
// TODO(sandeepg): Add storage credentials and bind scan FileIO to them.
355+
std::vector<StorageCredential> storage_credentials;
356356

357357
Status Validate() const;
358358

@@ -373,6 +373,7 @@ struct ICEBERG_REST_EXPORT FetchScanTasksResponse {
373373
std::optional<std::vector<std::string>> plan_tasks;
374374
std::optional<std::vector<std::shared_ptr<FileScanTask>>> file_scan_tasks;
375375
std::vector<std::shared_ptr<DataFile>> delete_files;
376+
std::vector<StorageCredential> storage_credentials;
376377

377378
Status Validate() const;
378379

0 commit comments

Comments
 (0)