From f6f9ae69f3f683c1c5a7e172c003d02723ff3fec Mon Sep 17 00:00:00 2001 From: LeMedi Date: Sat, 7 Feb 2026 22:52:23 +0100 Subject: [PATCH] Add endpoint to list all namespaces --- libsql-server/src/http/admin/mod.rs | 49 +++++++++ libsql-server/src/namespace/meta_store.rs | 6 ++ libsql-server/src/namespace/store.rs | 2 +- libsql-server/tests/standalone/admin.rs | 116 +++++++++++++++++++++- 4 files changed, 170 insertions(+), 3 deletions(-) diff --git a/libsql-server/src/http/admin/mod.rs b/libsql-server/src/http/admin/mod.rs index 6953696312..ecd5de5795 100644 --- a/libsql-server/src/http/admin/mod.rs +++ b/libsql-server/src/http/admin/mod.rs @@ -142,6 +142,7 @@ where }; let router = axum::Router::new() .route("/", get(handle_get_index)) + .route("/v1/namespaces", get(handle_list_namespaces)) .route( "/v1/namespaces/:namespace/config", get(handle_get_config).post(handle_post_config), @@ -262,10 +263,56 @@ async fn handle_get_config( allow_attach: config.allow_attach, txn_timeout_s: config.txn_timeout.map(|d| d.as_secs() as u64), durability_mode: Some(config.durability_mode), + shared_schema_name: config.shared_schema_name.clone(), }; Ok(Json(resp)) } +#[derive(Debug, Serialize)] +struct NamespaceListItem { + name: NamespaceName, + #[serde(flatten)] + config: HttpDatabaseConfig, +} + +#[derive(Debug, Serialize)] +struct ListNamespacesResponse { + namespaces: Vec, +} + +async fn handle_list_namespaces( + State(app_state): State>>, +) -> crate::Result> { + let namespace_names = app_state.namespaces.meta_store().list_namespaces().await?; + + let mut namespaces = Vec::with_capacity(namespace_names.len()); + + for name in namespace_names { + let store = app_state.namespaces.config_store(name.clone()).await?; + let config = store.get(); + let max_db_size = bytesize::ByteSize::b(config.max_db_pages * LIBSQL_PAGE_SIZE); + + let item = NamespaceListItem { + name: name.clone(), + config: HttpDatabaseConfig { + block_reads: config.block_reads, + block_writes: config.block_writes, + block_reason: config.block_reason.clone(), + max_db_size: Some(max_db_size), + heartbeat_url: config.heartbeat_url.clone().map(|u| u.into()), + jwt_key: config.jwt_key.clone(), + allow_attach: config.allow_attach, + txn_timeout_s: config.txn_timeout.map(|d| d.as_secs() as u64), + durability_mode: Some(config.durability_mode), + shared_schema_name: config.shared_schema_name.clone(), + }, + }; + namespaces.push(item); + } + + Ok(Json(ListNamespacesResponse { namespaces })) +} + async fn handle_diagnostics( State(app_state): State>>, ) -> crate::Result>> { @@ -311,6 +358,8 @@ struct HttpDatabaseConfig { txn_timeout_s: Option, #[serde(default)] durability_mode: Option, + #[serde(default)] + shared_schema_name: Option, } async fn handle_post_config( diff --git a/libsql-server/src/namespace/meta_store.rs b/libsql-server/src/namespace/meta_store.rs index 70b419ebe9..67e30708b9 100644 --- a/libsql-server/src/namespace/meta_store.rs +++ b/libsql-server/src/namespace/meta_store.rs @@ -616,6 +616,12 @@ impl MetaStore { } None } + + pub async fn list_namespaces(&self) -> crate::Result> { + let configs = self.inner.configs.lock().await; + let namespaces = configs.keys().cloned().collect(); + Ok(namespaces) + } } impl MetaStoreHandle { diff --git a/libsql-server/src/namespace/store.rs b/libsql-server/src/namespace/store.rs index 86e9438ccd..af5196df2b 100644 --- a/libsql-server/src/namespace/store.rs +++ b/libsql-server/src/namespace/store.rs @@ -502,7 +502,7 @@ impl NamespaceStore { self.with(namespace, |ns| ns.db_config_store.clone()).await } - pub(crate) fn meta_store(&self) -> &MetaStore { + pub fn meta_store(&self) -> &MetaStore { &self.inner.metadata } diff --git a/libsql-server/tests/standalone/admin.rs b/libsql-server/tests/standalone/admin.rs index ed70315c6b..c7d00916b3 100644 --- a/libsql-server/tests/standalone/admin.rs +++ b/libsql-server/tests/standalone/admin.rs @@ -1,16 +1,50 @@ +use std::path::PathBuf; use std::time::Duration; use hyper::StatusCode; -use libsql_server::config::{AdminApiConfig, UserApiConfig}; +use libsql_server::config::{AdminApiConfig, RpcServerConfig, UserApiConfig}; use s3s::header::AUTHORIZATION; use serde_json::json; use tempfile::tempdir; +use turmoil::Sim; use crate::common::{ http::Client, - net::{SimServer as _, TestServer, TurmoilAcceptor, TurmoilConnector}, + net::{init_tracing, SimServer as _, TestServer, TurmoilAcceptor, TurmoilConnector}, }; +fn make_primary(sim: &mut Sim, path: PathBuf) { + init_tracing(); + sim.host("primary", move || { + let path = path.clone(); + async move { + let server = TestServer { + path: path.into(), + user_api_config: UserApiConfig { + ..Default::default() + }, + admin_api_config: Some(AdminApiConfig { + acceptor: TurmoilAcceptor::bind(([0, 0, 0, 0], 9090)).await?, + connector: TurmoilConnector, + disable_metrics: true, + auth_key: None, + }), + rpc_server_config: Some(RpcServerConfig { + acceptor: TurmoilAcceptor::bind(([0, 0, 0, 0], 4567)).await?, + tls_config: None, + }), + disable_namespaces: false, + disable_default_namespace: false, + ..Default::default() + }; + + server.start_sim(8080).await?; + + Ok(()) + } + }); +} + #[test] fn admin_auth() { let mut sim = turmoil::Builder::new() @@ -65,3 +99,81 @@ fn admin_auth() { sim.run().unwrap(); } + +#[test] +fn list_namespaces_basic() { + let mut sim = turmoil::Builder::new() + .simulation_duration(Duration::from_secs(1000)) + .build(); + let tmp = tempdir().unwrap(); + make_primary(&mut sim, tmp.path().to_path_buf()); + + sim.client("client", async { + let client = Client::new(); + + // Step 1: List initially - should have default namespace + let resp = client + .get("http://primary:9090/v1/namespaces") + .await?; + assert!(resp.status().is_success()); + + let body: serde_json::Value = resp.json().await?; + let namespaces = body["namespaces"].as_array().unwrap(); + assert_eq!(namespaces.len(), 1); + assert_eq!(namespaces[0]["name"], "default"); + + // Step 2: Create foo namespace + client + .post("http://primary:9090/v1/namespaces/foo/create", json!({})) + .await?; + + // Step 3: Create schema namespace and bar with shared_schema_name + client + .post( + "http://primary:9090/v1/namespaces/schema/create", + json!({ "shared_schema": true }), + ) + .await?; + client + .post( + "http://primary:9090/v1/namespaces/bar/create", + json!({ "shared_schema_name": "schema" }), + ) + .await?; + + // Step 4: List again - should have 3 namespaces + let resp = client + .get("http://primary:9090/v1/namespaces") + .await?; + let body: serde_json::Value = resp.json().await?; + let namespaces = body["namespaces"].as_array().unwrap(); + assert_eq!(namespaces.len(), 3); + + // Verify all namespace names are present + let names: Vec<_> = namespaces + .iter() + .map(|n| n["name"].as_str().unwrap()) + .collect(); + assert!(names.contains(&"default")); + assert!(names.contains(&"foo")); + assert!(names.contains(&"bar")); + + // Verify shared_schema_name for bar + let bar = namespaces + .iter() + .find(|n| n["name"] == "bar") + .unwrap(); + assert_eq!(bar["shared_schema_name"], "schema"); + + // Verify foo doesn't have shared_schema_name + let foo = namespaces + .iter() + .find(|n| n["name"] == "foo") + .unwrap(); + assert!(foo["shared_schema_name"].is_null()); + + Ok(()) + }); + + sim.run().unwrap(); +}