mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 08:18:18 +00:00
feat(table-catalog): add credential endpoint boundary (#3407)
* feat(table-catalog): add credential scope boundary * feat(table-catalog): add credential endpoint boundary * fix(table-catalog): remove redundant scope clone --------- Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
This commit is contained in:
@@ -534,6 +534,8 @@ pub enum AdminAction {
|
||||
GetTableLifecycleAction,
|
||||
#[strum(serialize = "admin:SetTableLifecycle")]
|
||||
SetTableLifecycleAction,
|
||||
#[strum(serialize = "admin:GetTableCredentials")]
|
||||
GetTableCredentialsAction,
|
||||
#[strum(serialize = "admin:RunTableMaintenance")]
|
||||
RunTableMaintenanceAction,
|
||||
#[strum(serialize = "admin:GetTableMetadataLocation")]
|
||||
@@ -582,6 +584,7 @@ impl AdminAction {
|
||||
| AdminAction::DeleteTableAction
|
||||
| AdminAction::GetTableLifecycleAction
|
||||
| AdminAction::SetTableLifecycleAction
|
||||
| AdminAction::GetTableCredentialsAction
|
||||
| AdminAction::RunTableMaintenanceAction
|
||||
| AdminAction::GetTableMetadataLocationAction
|
||||
| AdminAction::SetTableMetadataLocationAction
|
||||
@@ -668,6 +671,7 @@ impl AdminAction {
|
||||
| AdminAction::DeleteTableAction
|
||||
| AdminAction::GetTableLifecycleAction
|
||||
| AdminAction::SetTableLifecycleAction
|
||||
| AdminAction::GetTableCredentialsAction
|
||||
| AdminAction::RunTableMaintenanceAction
|
||||
| AdminAction::GetTableMetadataLocationAction
|
||||
| AdminAction::SetTableMetadataLocationAction
|
||||
@@ -869,6 +873,8 @@ mod tests {
|
||||
AdminAction::try_from("admin:SetTableLifecycle").expect("Should parse SetTableLifecycle action");
|
||||
let run_maintenance_action =
|
||||
AdminAction::try_from("admin:RunTableMaintenance").expect("Should parse RunTableMaintenance action");
|
||||
let get_credentials_action =
|
||||
AdminAction::try_from("admin:GetTableCredentials").expect("Should parse GetTableCredentials action");
|
||||
|
||||
assert_eq!(get_action, AdminAction::GetTableAction);
|
||||
assert_eq!(set_action, AdminAction::SetTableAction);
|
||||
@@ -879,6 +885,7 @@ mod tests {
|
||||
assert_eq!(get_lifecycle_action, AdminAction::GetTableLifecycleAction);
|
||||
assert_eq!(set_lifecycle_action, AdminAction::SetTableLifecycleAction);
|
||||
assert_eq!(run_maintenance_action, AdminAction::RunTableMaintenanceAction);
|
||||
assert_eq!(get_credentials_action, AdminAction::GetTableCredentialsAction);
|
||||
assert!(get_action.is_valid());
|
||||
assert!(set_action.is_valid());
|
||||
assert!(create_action.is_valid());
|
||||
@@ -888,6 +895,7 @@ mod tests {
|
||||
assert!(get_lifecycle_action.is_valid());
|
||||
assert!(set_lifecycle_action.is_valid());
|
||||
assert!(run_maintenance_action.is_valid());
|
||||
assert!(get_credentials_action.is_valid());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -36,7 +36,15 @@ const WAREHOUSE_PROPERTY: &str = "warehouse";
|
||||
const CATALOG_ENDPOINT_PREFIX_CONFIG_KEY: &str = "rustfs.catalog-endpoint-prefix";
|
||||
const CATALOG_COMPAT_ENDPOINT_PREFIX_CONFIG_KEY: &str = "rustfs.catalog-compat-endpoint-prefix";
|
||||
const CREDENTIAL_VENDING_CONFIG_KEY: &str = "rustfs.credential-vending";
|
||||
const CREDENTIAL_VENDING_REASON_CONFIG_KEY: &str = "rustfs.credential-vending-reason";
|
||||
const CREDENTIAL_SCOPE_CONFIG_KEY: &str = "rustfs.credential-scope";
|
||||
const CREDENTIAL_SCOPE_PREFIX_CONFIG_KEY: &str = "rustfs.credential-scope-prefix";
|
||||
const CREDENTIAL_MODE_CONFIG_KEY: &str = "rustfs.credential-mode";
|
||||
const CREDENTIAL_VENDING_UNSUPPORTED: &str = "unsupported";
|
||||
const CREDENTIAL_VENDING_UNSUPPORTED_REASON: &str = "temporary-credentials-not-implemented";
|
||||
const CREDENTIAL_SCOPE_WAREHOUSE_PREFIX: &str = "warehouse-prefix";
|
||||
const CREDENTIAL_SCOPE_TABLE_PREFIX: &str = "table-prefix";
|
||||
const CREDENTIAL_MODE_CLIENT_PROVIDED: &str = "client-provided-s3-credentials-required";
|
||||
const TABLE_CATALOG_NAMESPACE_RESOURCE_ROOT: &str = "namespaces";
|
||||
const TABLE_CATALOG_TABLE_RESOURCE_ROOT: &str = "tables";
|
||||
const TABLE_CATALOG_ENDPOINTS: &[&str] = &[
|
||||
@@ -50,6 +58,7 @@ const TABLE_CATALOG_ENDPOINTS: &[&str] = &[
|
||||
"POST /v1/{prefix}/namespaces/{namespace}/register",
|
||||
"GET /v1/{prefix}/namespaces/{namespace}/tables/{table}",
|
||||
"HEAD /v1/{prefix}/namespaces/{namespace}/tables/{table}",
|
||||
"GET /v1/{prefix}/namespaces/{namespace}/tables/{table}/credentials",
|
||||
"POST /v1/{prefix}/namespaces/{namespace}/tables/{table}",
|
||||
"DELETE /v1/{prefix}/namespaces/{namespace}/tables/{table}",
|
||||
"PUT /buckets/{warehouse}",
|
||||
@@ -64,6 +73,7 @@ const TABLE_CATALOG_ENDPOINTS: &[&str] = &[
|
||||
"POST /{warehouse}/namespaces/{namespace}/register",
|
||||
"GET /{warehouse}/namespaces/{namespace}/tables/{table}",
|
||||
"HEAD /{warehouse}/namespaces/{namespace}/tables/{table}",
|
||||
"GET /{warehouse}/namespaces/{namespace}/tables/{table}/credentials",
|
||||
"POST /{warehouse}/namespaces/{namespace}/tables/{table}",
|
||||
"DELETE /{warehouse}/namespaces/{namespace}/tables/{table}",
|
||||
"POST /{warehouse}/namespaces/{namespace}/tables/{table}/maintenance/metadata",
|
||||
@@ -91,6 +101,7 @@ static CREATE_TABLE_HANDLER: RestCreateTableHandler = RestCreateTableHandler {};
|
||||
static REGISTER_TABLE_HANDLER: RestRegisterTableHandler = RestRegisterTableHandler {};
|
||||
static LOAD_TABLE_HANDLER: RestLoadTableHandler = RestLoadTableHandler {};
|
||||
static TABLE_EXISTS_HANDLER: RestTableExistsHandler = RestTableExistsHandler {};
|
||||
static LOAD_CREDENTIALS_HANDLER: RestLoadCredentialsHandler = RestLoadCredentialsHandler {};
|
||||
static COMMIT_TABLE_HANDLER: RestCommitTableHandler = RestCommitTableHandler {};
|
||||
static DROP_TABLE_HANDLER: RestDropTableHandler = RestDropTableHandler {};
|
||||
static GET_TABLE_METADATA_LOCATION_HANDLER: GetTableMetadataLocationHandler = GetTableMetadataLocationHandler {};
|
||||
@@ -231,6 +242,10 @@ struct TableBucketResponse {
|
||||
compat_catalog_uri: String,
|
||||
#[serde(rename = "credential-vending")]
|
||||
credential_vending: &'static str,
|
||||
#[serde(rename = "credential-scope")]
|
||||
credential_scope: &'static str,
|
||||
#[serde(rename = "credential-scope-prefix")]
|
||||
credential_scope_prefix: String,
|
||||
#[serde(rename = "catalog-entry-present")]
|
||||
catalog_entry_present: bool,
|
||||
properties: BTreeMap<String, String>,
|
||||
@@ -274,6 +289,12 @@ struct RestLoadTableResponse {
|
||||
storage_credentials: Vec<RestStorageCredential>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct RestLoadCredentialsResponse {
|
||||
#[serde(rename = "storage-credentials")]
|
||||
storage_credentials: Vec<RestStorageCredential>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct RestCommitTableResponse {
|
||||
#[serde(rename = "metadata-location")]
|
||||
@@ -367,6 +388,11 @@ fn register_table_catalog_prefix_routes(r: &mut S3Router<AdminOperation>, prefix
|
||||
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}").as_str(),
|
||||
AdminOperation(&TABLE_EXISTS_HANDLER),
|
||||
)?;
|
||||
r.insert(
|
||||
Method::GET,
|
||||
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}/credentials").as_str(),
|
||||
AdminOperation(&LOAD_CREDENTIALS_HANDLER),
|
||||
)?;
|
||||
r.insert(
|
||||
Method::POST,
|
||||
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}").as_str(),
|
||||
@@ -663,10 +689,12 @@ where
|
||||
enabled,
|
||||
catalog_type,
|
||||
warehouse: bucket.to_string(),
|
||||
warehouse_location,
|
||||
warehouse_location: warehouse_location.clone(),
|
||||
catalog_uri: format!("{TABLE_CATALOG_PREFIX}/{bucket}"),
|
||||
compat_catalog_uri: format!("{TABLE_CATALOG_COMPAT_PREFIX}/{bucket}"),
|
||||
credential_vending: CREDENTIAL_VENDING_UNSUPPORTED,
|
||||
credential_scope: CREDENTIAL_SCOPE_WAREHOUSE_PREFIX,
|
||||
credential_scope_prefix: warehouse_location,
|
||||
catalog_entry_present,
|
||||
properties,
|
||||
})
|
||||
@@ -738,8 +766,16 @@ fn list_tables_response_from_entries(entries: Vec<crate::table_catalog::TableEnt
|
||||
|
||||
fn load_table_response_from_entry(entry: crate::table_catalog::TableEntry, metadata: serde_json::Value) -> RestLoadTableResponse {
|
||||
let mut config = BTreeMap::new();
|
||||
config.insert("warehouse-location".to_string(), entry.warehouse_location);
|
||||
let warehouse_location = entry.warehouse_location.clone();
|
||||
config.insert("warehouse-location".to_string(), warehouse_location.clone());
|
||||
config.insert(CREDENTIAL_VENDING_CONFIG_KEY.to_string(), CREDENTIAL_VENDING_UNSUPPORTED.to_string());
|
||||
config.insert(
|
||||
CREDENTIAL_VENDING_REASON_CONFIG_KEY.to_string(),
|
||||
CREDENTIAL_VENDING_UNSUPPORTED_REASON.to_string(),
|
||||
);
|
||||
config.insert(CREDENTIAL_SCOPE_CONFIG_KEY.to_string(), CREDENTIAL_SCOPE_TABLE_PREFIX.to_string());
|
||||
config.insert(CREDENTIAL_SCOPE_PREFIX_CONFIG_KEY.to_string(), warehouse_location);
|
||||
config.insert(CREDENTIAL_MODE_CONFIG_KEY.to_string(), CREDENTIAL_MODE_CLIENT_PROVIDED.to_string());
|
||||
|
||||
RestLoadTableResponse {
|
||||
metadata_location: entry.metadata_location,
|
||||
@@ -749,6 +785,12 @@ fn load_table_response_from_entry(entry: crate::table_catalog::TableEntry, metad
|
||||
}
|
||||
}
|
||||
|
||||
fn load_credentials_response_from_entry(_entry: crate::table_catalog::TableEntry) -> RestLoadCredentialsResponse {
|
||||
RestLoadCredentialsResponse {
|
||||
storage_credentials: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn commit_table_response_from_result(
|
||||
result: crate::table_catalog::TableCommitResult,
|
||||
metadata: serde_json::Value,
|
||||
@@ -1819,6 +1861,25 @@ where
|
||||
Ok(exists_status(exists))
|
||||
}
|
||||
|
||||
async fn load_credentials_response<S>(
|
||||
store: &S,
|
||||
bucket: &str,
|
||||
namespace: &crate::table_catalog::Namespace,
|
||||
table: &str,
|
||||
) -> S3Result<RestLoadCredentialsResponse>
|
||||
where
|
||||
S: crate::table_catalog::TableCatalogStore + ?Sized,
|
||||
{
|
||||
let Some(entry) = store
|
||||
.load_table(bucket, &namespace.public_name(), table)
|
||||
.await
|
||||
.map_err(catalog_store_error)?
|
||||
else {
|
||||
return Err(s3_error!(InvalidRequest, "table not found"));
|
||||
};
|
||||
Ok(load_credentials_response_from_entry(entry))
|
||||
}
|
||||
|
||||
async fn get_table_metadata_location_response<S>(
|
||||
store: &S,
|
||||
bucket: &str,
|
||||
@@ -2285,6 +2346,22 @@ impl Operation for RestTableExistsHandler {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RestLoadCredentialsHandler {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for RestLoadCredentialsHandler {
|
||||
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let warehouse = warehouse_from_params(¶ms)?;
|
||||
let namespace = namespace_from_params(¶ms)?;
|
||||
let table = table_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::GetTableCredentialsAction).await?;
|
||||
let store = table_catalog_store()?;
|
||||
let response = load_credentials_response(&store, &warehouse, &namespace, &table).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RestCommitTableHandler {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@@ -2544,6 +2621,11 @@ mod tests {
|
||||
.endpoints
|
||||
.contains(&"HEAD /v1/{prefix}/namespaces/{namespace}/tables/{table}")
|
||||
);
|
||||
assert!(
|
||||
response
|
||||
.endpoints
|
||||
.contains(&"GET /v1/{prefix}/namespaces/{namespace}/tables/{table}/credentials")
|
||||
);
|
||||
assert!(response.endpoints.contains(&"GET /{warehouse}/namespaces"));
|
||||
assert!(response.endpoints.contains(&"POST /{warehouse}/namespaces"));
|
||||
assert!(response.endpoints.contains(&"HEAD /{warehouse}/namespaces/{namespace}"));
|
||||
@@ -2572,6 +2654,11 @@ mod tests {
|
||||
.endpoints
|
||||
.contains(&"POST /{warehouse}/namespaces/{namespace}/tables/{table}")
|
||||
);
|
||||
assert!(
|
||||
response
|
||||
.endpoints
|
||||
.contains(&"GET /{warehouse}/namespaces/{namespace}/tables/{table}/credentials")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2600,6 +2687,7 @@ mod tests {
|
||||
("RestRegisterTableHandler", "AdminAction::RegisterTableAction"),
|
||||
("RestLoadTableHandler", "AdminAction::GetTableMetadataAction"),
|
||||
("RestTableExistsHandler", "AdminAction::GetTableAction"),
|
||||
("RestLoadCredentialsHandler", "AdminAction::GetTableCredentialsAction"),
|
||||
("RestCommitTableHandler", "AdminAction::CommitTableAction"),
|
||||
("RestDropTableHandler", "AdminAction::DeleteTableAction"),
|
||||
("GetTableMetadataLocationHandler", "AdminAction::GetTableMetadataLocationAction"),
|
||||
@@ -2631,6 +2719,7 @@ mod tests {
|
||||
for (handler, action) in [
|
||||
("RestLoadTableHandler", "AdminAction::GetTableMetadataAction"),
|
||||
("RestTableExistsHandler", "AdminAction::GetTableAction"),
|
||||
("RestLoadCredentialsHandler", "AdminAction::GetTableCredentialsAction"),
|
||||
("RestCommitTableHandler", "AdminAction::CommitTableAction"),
|
||||
("RestDropTableHandler", "AdminAction::DeleteTableAction"),
|
||||
("GetTableMetadataLocationHandler", "AdminAction::GetTableMetadataLocationAction"),
|
||||
@@ -2702,6 +2791,7 @@ mod tests {
|
||||
let _: &RestRegisterTableHandler = ®ISTER_TABLE_HANDLER;
|
||||
let _: &RestLoadTableHandler = &LOAD_TABLE_HANDLER;
|
||||
let _: &RestTableExistsHandler = &TABLE_EXISTS_HANDLER;
|
||||
let _: &RestLoadCredentialsHandler = &LOAD_CREDENTIALS_HANDLER;
|
||||
let _: &RestCommitTableHandler = &COMMIT_TABLE_HANDLER;
|
||||
let _: &RestDropTableHandler = &DROP_TABLE_HANDLER;
|
||||
let _: &GetTableMetadataLocationHandler = &GET_TABLE_METADATA_LOCATION_HANDLER;
|
||||
@@ -2727,6 +2817,7 @@ mod tests {
|
||||
assert_operation::<RestRegisterTableHandler>();
|
||||
assert_operation::<RestLoadTableHandler>();
|
||||
assert_operation::<RestTableExistsHandler>();
|
||||
assert_operation::<RestLoadCredentialsHandler>();
|
||||
assert_operation::<RestCommitTableHandler>();
|
||||
assert_operation::<RestDropTableHandler>();
|
||||
assert_operation::<GetTableMetadataLocationHandler>();
|
||||
@@ -2780,6 +2871,8 @@ mod tests {
|
||||
assert_eq!(response.catalog_uri, "/iceberg/v1/warehouse");
|
||||
assert_eq!(response.compat_catalog_uri, "/_iceberg/v1/warehouse");
|
||||
assert_eq!(response.credential_vending, CREDENTIAL_VENDING_UNSUPPORTED);
|
||||
assert_eq!(response.credential_scope, "warehouse-prefix");
|
||||
assert_eq!(response.credential_scope_prefix, "s3://warehouse/");
|
||||
assert!(response.catalog_entry_present);
|
||||
}
|
||||
|
||||
@@ -3612,11 +3705,49 @@ mod tests {
|
||||
assert_eq!(response.metadata, metadata);
|
||||
assert!(response.storage_credentials.is_empty());
|
||||
assert_eq!(response.config.get("rustfs.credential-vending"), Some(&"unsupported".to_string()));
|
||||
assert_eq!(
|
||||
response.config.get("rustfs.credential-vending-reason"),
|
||||
Some(&"temporary-credentials-not-implemented".to_string())
|
||||
);
|
||||
assert_eq!(response.config.get("rustfs.credential-scope"), Some(&"table-prefix".to_string()));
|
||||
assert_eq!(
|
||||
response.config.get("rustfs.credential-scope-prefix"),
|
||||
Some(&"s3://warehouse/tables/table-id".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
response.config.get("rustfs.credential-mode"),
|
||||
Some(&"client-provided-s3-credentials-required".to_string())
|
||||
);
|
||||
assert!(!response.config.contains_key("s3.access-key-id"));
|
||||
assert!(!response.config.contains_key("s3.secret-access-key"));
|
||||
assert!(!response.config.contains_key("s3.session-token"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_credentials_response_never_returns_static_s3_credentials() {
|
||||
let response = load_credentials_response_from_entry(crate::table_catalog::TableEntry {
|
||||
version: crate::table_catalog::TABLE_CATALOG_ENTRY_VERSION,
|
||||
table_bucket: "warehouse".to_string(),
|
||||
namespace: "analytics".to_string(),
|
||||
table: "events".to_string(),
|
||||
table_id: "table-id".to_string(),
|
||||
table_uuid: "table-uuid".to_string(),
|
||||
format: "ICEBERG".to_string(),
|
||||
format_version: 2,
|
||||
warehouse_location: "s3://warehouse/tables/table-id".to_string(),
|
||||
metadata_location: ".rustfs-table/warehouses/default/namespaces/analytics/tables/events/metadata/00001.metadata.json"
|
||||
.to_string(),
|
||||
version_token: "token-v1".to_string(),
|
||||
generation: 1,
|
||||
state: crate::table_catalog::TableCatalogEntryState::Active,
|
||||
properties: BTreeMap::new(),
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
});
|
||||
|
||||
assert!(response.storage_credentials.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commit_table_request_uses_rest_commit_fields() {
|
||||
let request: RestCommitTableRequest = serde_json::from_value(serde_json::json!({
|
||||
|
||||
@@ -41,6 +41,7 @@ const GET_REPLICATION_METRICS: AdminActionRef = AdminActionRef::new("GetReplicat
|
||||
const GET_TABLE: AdminActionRef = AdminActionRef::new("GetTableAction");
|
||||
const GET_TABLE_BUCKET: AdminActionRef = AdminActionRef::new("GetTableBucketAction");
|
||||
const GET_TABLE_CATALOG: AdminActionRef = AdminActionRef::new("GetTableCatalogAction");
|
||||
const GET_TABLE_CREDENTIALS: AdminActionRef = AdminActionRef::new("GetTableCredentialsAction");
|
||||
const GET_TABLE_LIFECYCLE: AdminActionRef = AdminActionRef::new("GetTableLifecycleAction");
|
||||
const GET_TABLE_METADATA: AdminActionRef = AdminActionRef::new("GetTableMetadataAction");
|
||||
const GET_TABLE_METADATA_LOCATION: AdminActionRef = AdminActionRef::new("GetTableMetadataLocationAction");
|
||||
@@ -675,6 +676,12 @@ pub const ADMIN_ROUTE_POLICY_SPECS: &[AdminRouteSpec] = &[
|
||||
GET_TABLE,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}/credentials",
|
||||
GET_TABLE_CREDENTIALS,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Post,
|
||||
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}",
|
||||
@@ -820,6 +827,12 @@ pub const ADMIN_ROUTE_POLICY_SPECS: &[AdminRouteSpec] = &[
|
||||
GET_TABLE,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/_iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}/credentials",
|
||||
GET_TABLE_CREDENTIALS,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Post,
|
||||
"/_iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}",
|
||||
@@ -1065,7 +1078,7 @@ mod tests {
|
||||
let table_specs = ADMIN_ROUTE_POLICY_SPECS
|
||||
.iter()
|
||||
.filter(|spec| spec.path().starts_with("/iceberg/v1") || spec.path().starts_with("/_iceberg/v1"));
|
||||
assert_eq!(table_specs.count(), 50);
|
||||
assert_eq!(table_specs.count(), 52);
|
||||
assert_action(HttpMethod::Put, "/iceberg/v1/buckets/{warehouse}", SET_TABLE_BUCKET);
|
||||
assert_action(HttpMethod::Get, "/_iceberg/v1/buckets/{warehouse}", GET_TABLE_BUCKET);
|
||||
assert_action(HttpMethod::Get, "/iceberg/v1/{warehouse}/namespaces", GET_TABLE_NAMESPACE);
|
||||
@@ -1079,6 +1092,11 @@ mod tests {
|
||||
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}",
|
||||
GET_TABLE_METADATA,
|
||||
);
|
||||
assert_action(
|
||||
HttpMethod::Get,
|
||||
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}/credentials",
|
||||
GET_TABLE_CREDENTIALS,
|
||||
);
|
||||
assert_action(
|
||||
HttpMethod::Get,
|
||||
"/_iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}",
|
||||
@@ -1094,6 +1112,16 @@ mod tests {
|
||||
"/_iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}",
|
||||
GET_TABLE,
|
||||
);
|
||||
assert_action(
|
||||
HttpMethod::Get,
|
||||
"/_iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}/credentials",
|
||||
GET_TABLE_CREDENTIALS,
|
||||
);
|
||||
assert_action(
|
||||
HttpMethod::Get,
|
||||
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}/credentials",
|
||||
GET_TABLE_CREDENTIALS,
|
||||
);
|
||||
assert_action(
|
||||
HttpMethod::Post,
|
||||
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}",
|
||||
|
||||
@@ -315,6 +315,11 @@ fn expected_admin_route_matrix() -> Vec<RouteMatrixEntry> {
|
||||
"/{warehouse}/namespaces/{namespace}/tables/{table}",
|
||||
"/analytics/namespaces/sales/tables/orders",
|
||||
),
|
||||
table_route_sample(
|
||||
Method::GET,
|
||||
"/{warehouse}/namespaces/{namespace}/tables/{table}/credentials",
|
||||
"/analytics/namespaces/sales/tables/orders/credentials",
|
||||
),
|
||||
table_route_sample(
|
||||
Method::POST,
|
||||
"/{warehouse}/namespaces/{namespace}/tables/{table}",
|
||||
@@ -408,6 +413,11 @@ fn expected_admin_route_matrix() -> Vec<RouteMatrixEntry> {
|
||||
"/{warehouse}/namespaces/{namespace}/tables/{table}",
|
||||
"/analytics/namespaces/sales/tables/orders",
|
||||
),
|
||||
compat_table_route_sample(
|
||||
Method::GET,
|
||||
"/{warehouse}/namespaces/{namespace}/tables/{table}/credentials",
|
||||
"/analytics/namespaces/sales/tables/orders/credentials",
|
||||
),
|
||||
compat_table_route_sample(
|
||||
Method::POST,
|
||||
"/{warehouse}/namespaces/{namespace}/tables/{table}",
|
||||
@@ -587,6 +597,11 @@ fn test_register_routes_cover_representative_admin_paths() {
|
||||
assert_route(&router, Method::POST, &table_catalog_path("/analytics/namespaces/sales/tables"));
|
||||
assert_route(&router, Method::POST, &table_catalog_path("/analytics/namespaces/sales/register"));
|
||||
assert_route(&router, Method::GET, &table_catalog_path("/analytics/namespaces/sales/tables/orders"));
|
||||
assert_route(
|
||||
&router,
|
||||
Method::GET,
|
||||
&table_catalog_path("/analytics/namespaces/sales/tables/orders/credentials"),
|
||||
);
|
||||
assert_route(&router, Method::POST, &table_catalog_path("/analytics/namespaces/sales/tables/orders"));
|
||||
assert_route(&router, Method::DELETE, &table_catalog_path("/analytics/namespaces/sales/tables/orders"));
|
||||
assert_route(
|
||||
@@ -654,6 +669,11 @@ fn test_register_routes_cover_representative_admin_paths() {
|
||||
Method::GET,
|
||||
&compat_table_catalog_path("/analytics/namespaces/sales/tables/orders"),
|
||||
);
|
||||
assert_route(
|
||||
&router,
|
||||
Method::GET,
|
||||
&compat_table_catalog_path("/analytics/namespaces/sales/tables/orders/credentials"),
|
||||
);
|
||||
assert_route(
|
||||
&router,
|
||||
Method::POST,
|
||||
|
||||
@@ -112,13 +112,27 @@ added.
|
||||
Unsupported behavior is documented instead of hidden behind internal errors. The
|
||||
current unsupported inventory is:
|
||||
|
||||
- credential vending: unsupported response boundary exists; real temporary credentials are not issued
|
||||
- credential vending: non-secret table scope preview and credentials endpoint exist; real temporary credentials are not issued
|
||||
- background maintenance worker: unsupported
|
||||
- manifest/data reachability cleanup: unsupported
|
||||
- snapshot expiration and compaction: unsupported
|
||||
- Iceberg views: unsupported
|
||||
- multi-table transactions: not a short-term production claim
|
||||
|
||||
## Credential Boundary
|
||||
|
||||
RustFS advertises table credential scope metadata without returning reusable
|
||||
storage secrets. `loadTable` includes the table warehouse prefix in the response
|
||||
config, and the standard credentials endpoint is registered:
|
||||
|
||||
```text
|
||||
GET /v1/{prefix}/namespaces/{namespace}/tables/{table}/credentials
|
||||
```
|
||||
|
||||
The endpoint returns an empty `storage-credentials` list until temporary,
|
||||
table-scoped credential issuance is implemented. Clients should continue using
|
||||
their configured S3 credentials for object data access.
|
||||
|
||||
## Spark Manual Baseline
|
||||
|
||||
Spark validation should use the same RustFS endpoint and warehouse bucket as the
|
||||
|
||||
@@ -111,9 +111,10 @@ CLIENT_MATRIX: list[dict[str, str]] = [
|
||||
UNSUPPORTED_INVENTORY: list[dict[str, str]] = [
|
||||
{
|
||||
"capability": "credential-vending",
|
||||
"status": "unsupported-response-boundary",
|
||||
"status": "scope-preview-no-temporary-credentials",
|
||||
"roadmap_area": "credential-boundary",
|
||||
"expected_behavior": "load table advertises rustfs.credential-vending=unsupported and returns no long-lived credentials",
|
||||
"catalog_endpoint": "GET /v1/{prefix}/namespaces/{namespace}/tables/{table}/credentials",
|
||||
"expected_behavior": "load table advertises the table credential scope; the credentials endpoint returns an empty storage-credentials list and no long-lived credentials",
|
||||
},
|
||||
{
|
||||
"capability": "background-maintenance-worker",
|
||||
|
||||
@@ -4,13 +4,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import pyiceberg_smoke
|
||||
|
||||
|
||||
INTERNAL_ROADMAP_LABEL_RE = re.compile(r"\b(?:PR|PG|P)[0-9]+\b")
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
class PyIcebergSmokeConfigTest(unittest.TestCase):
|
||||
def parse_with_args(self, argv: list[str]) -> object:
|
||||
env_keys = [
|
||||
@@ -70,6 +76,12 @@ class PyIcebergSmokeConfigTest(unittest.TestCase):
|
||||
self.assertIn("status", entry)
|
||||
self.assertIn("roadmap_area", entry)
|
||||
|
||||
def test_published_table_catalog_docs_do_not_use_internal_roadmap_labels(self) -> None:
|
||||
readme = (SCRIPT_DIR / "README.md").read_text(encoding="utf-8")
|
||||
|
||||
self.assertIsNone(INTERNAL_ROADMAP_LABEL_RE.search(readme))
|
||||
self.assertIsNone(INTERNAL_ROADMAP_LABEL_RE.search(str(pyiceberg_smoke.unsupported_inventory())))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user