diff --git a/storage_controller/src/http.rs b/storage_controller/src/http.rs
index 2e83bbc5ed..09a25a5be0 100644
--- a/storage_controller/src/http.rs
+++ b/storage_controller/src/http.rs
@@ -522,6 +522,18 @@ async fn handle_tenant_drop(req: Request
) -> Result, ApiErr
json_response(StatusCode::OK, state.service.tenant_drop(tenant_id).await?)
}
+async fn handle_tenant_import(req: Request) -> Result, ApiError> {
+ let tenant_id: TenantId = parse_request_param(&req, "tenant_id")?;
+ check_permissions(&req, Scope::PageServerApi)?;
+
+ let state = get_state(&req);
+
+ json_response(
+ StatusCode::OK,
+ state.service.tenant_import(tenant_id).await?,
+ )
+}
+
async fn handle_tenants_dump(req: Request) -> Result, ApiError> {
check_permissions(&req, Scope::Admin)?;
@@ -759,6 +771,13 @@ pub fn make_router(
.post("/debug/v1/node/:node_id/drop", |r| {
named_request_span(r, handle_node_drop, RequestName("debug_v1_node_drop"))
})
+ .post("/debug/v1/tenant/:tenant_id/import", |r| {
+ named_request_span(
+ r,
+ handle_tenant_import,
+ RequestName("debug_v1_tenant_import"),
+ )
+ })
.get("/debug/v1/tenant", |r| {
named_request_span(r, handle_tenants_dump, RequestName("debug_v1_tenant"))
})
diff --git a/storage_controller/src/service.rs b/storage_controller/src/service.rs
index 0565f8e7b4..fb67bee22f 100644
--- a/storage_controller/src/service.rs
+++ b/storage_controller/src/service.rs
@@ -3595,6 +3595,39 @@ impl Service {
Ok(())
}
+ /// This is for debug/support only: assuming tenant data is already present in S3, we "create" a
+ /// tenant with a very high generation number so that it will see the existing data.
+ pub(crate) async fn tenant_import(
+ &self,
+ tenant_id: TenantId,
+ ) -> Result {
+ let (response, waiters) = self
+ .do_tenant_create(TenantCreateRequest {
+ new_tenant_id: TenantShardId::unsharded(tenant_id),
+ // A sufficiently high generation is de-facto guaranteed to be high enough to see any
+ // indices in S3 (unless this tenant was at some point in the past recovered via this path).
+ // TODO: we should really probe remote storage to learn the generation, so that we don't
+ // eat a large swath of the generation number space in an irreversible way.
+ generation: Some(0x3fffffff),
+
+ shard_parameters: ShardParameters::default(),
+ placement_policy: Some(PlacementPolicy::Attached(0)), // No secondaries, for convenient debug/hacking
+
+ // There is no way to know what the tenant's config was: revert to defaults
+ config: TenantConfig::default(),
+ })
+ .await?;
+
+ if let Err(e) = self.await_waiters(waiters, SHORT_RECONCILE_TIMEOUT).await {
+ // Since this is a debug/support operation, all kinds of weird issues are possible (e.g. this
+ // tenant doesn't exist in the control plane), so don't fail the request if it can't fully
+ // reconcile, as reconciliation includes notifying compute.
+ tracing::warn!(%tenant_id, "Reconcile not done yet while importing tenant ({e})");
+ }
+
+ Ok(response)
+ }
+
/// For debug/support: a full JSON dump of TenantShards. Returns a response so that
/// we don't have to make TenantShard clonable in the return path.
pub(crate) fn tenants_dump(&self) -> Result, ApiError> {