Skip to content

Commit f6f9ae6

Browse files
committed
Add endpoint to list all namespaces
1 parent b5dab26 commit f6f9ae6

File tree

4 files changed

+170
-3
lines changed

4 files changed

+170
-3
lines changed

libsql-server/src/http/admin/mod.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ where
142142
};
143143
let router = axum::Router::new()
144144
.route("/", get(handle_get_index))
145+
.route("/v1/namespaces", get(handle_list_namespaces))
145146
.route(
146147
"/v1/namespaces/:namespace/config",
147148
get(handle_get_config).post(handle_post_config),
@@ -262,10 +263,56 @@ async fn handle_get_config<C: Connector>(
262263
allow_attach: config.allow_attach,
263264
txn_timeout_s: config.txn_timeout.map(|d| d.as_secs() as u64),
264265
durability_mode: Some(config.durability_mode),
266+
shared_schema_name: config.shared_schema_name.clone(),
265267
};
266268
Ok(Json(resp))
267269
}
268270

271+
#[derive(Debug, Serialize)]
272+
struct NamespaceListItem {
273+
name: NamespaceName,
274+
#[serde(flatten)]
275+
config: HttpDatabaseConfig,
276+
}
277+
278+
#[derive(Debug, Serialize)]
279+
struct ListNamespacesResponse {
280+
namespaces: Vec<NamespaceListItem>,
281+
}
282+
283+
async fn handle_list_namespaces<C>(
284+
State(app_state): State<Arc<AppState<C>>>,
285+
) -> crate::Result<Json<ListNamespacesResponse>> {
286+
let namespace_names = app_state.namespaces.meta_store().list_namespaces().await?;
287+
288+
let mut namespaces = Vec::with_capacity(namespace_names.len());
289+
290+
for name in namespace_names {
291+
let store = app_state.namespaces.config_store(name.clone()).await?;
292+
let config = store.get();
293+
let max_db_size = bytesize::ByteSize::b(config.max_db_pages * LIBSQL_PAGE_SIZE);
294+
295+
let item = NamespaceListItem {
296+
name: name.clone(),
297+
config: HttpDatabaseConfig {
298+
block_reads: config.block_reads,
299+
block_writes: config.block_writes,
300+
block_reason: config.block_reason.clone(),
301+
max_db_size: Some(max_db_size),
302+
heartbeat_url: config.heartbeat_url.clone().map(|u| u.into()),
303+
jwt_key: config.jwt_key.clone(),
304+
allow_attach: config.allow_attach,
305+
txn_timeout_s: config.txn_timeout.map(|d| d.as_secs() as u64),
306+
durability_mode: Some(config.durability_mode),
307+
shared_schema_name: config.shared_schema_name.clone(),
308+
},
309+
};
310+
namespaces.push(item);
311+
}
312+
313+
Ok(Json(ListNamespacesResponse { namespaces }))
314+
}
315+
269316
async fn handle_diagnostics<C>(
270317
State(app_state): State<Arc<AppState<C>>>,
271318
) -> crate::Result<Json<Vec<String>>> {
@@ -311,6 +358,8 @@ struct HttpDatabaseConfig {
311358
txn_timeout_s: Option<u64>,
312359
#[serde(default)]
313360
durability_mode: Option<DurabilityMode>,
361+
#[serde(default)]
362+
shared_schema_name: Option<NamespaceName>,
314363
}
315364

316365
async fn handle_post_config<C>(

libsql-server/src/namespace/meta_store.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,12 @@ impl MetaStore {
616616
}
617617
None
618618
}
619+
620+
pub async fn list_namespaces(&self) -> crate::Result<Vec<NamespaceName>> {
621+
let configs = self.inner.configs.lock().await;
622+
let namespaces = configs.keys().cloned().collect();
623+
Ok(namespaces)
624+
}
619625
}
620626

621627
impl MetaStoreHandle {

libsql-server/src/namespace/store.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ impl NamespaceStore {
502502
self.with(namespace, |ns| ns.db_config_store.clone()).await
503503
}
504504

505-
pub(crate) fn meta_store(&self) -> &MetaStore {
505+
pub fn meta_store(&self) -> &MetaStore {
506506
&self.inner.metadata
507507
}
508508

libsql-server/tests/standalone/admin.rs

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,50 @@
1+
use std::path::PathBuf;
12
use std::time::Duration;
23

34
use hyper::StatusCode;
4-
use libsql_server::config::{AdminApiConfig, UserApiConfig};
5+
use libsql_server::config::{AdminApiConfig, RpcServerConfig, UserApiConfig};
56
use s3s::header::AUTHORIZATION;
67
use serde_json::json;
78
use tempfile::tempdir;
9+
use turmoil::Sim;
810

911
use crate::common::{
1012
http::Client,
11-
net::{SimServer as _, TestServer, TurmoilAcceptor, TurmoilConnector},
13+
net::{init_tracing, SimServer as _, TestServer, TurmoilAcceptor, TurmoilConnector},
1214
};
1315

16+
fn make_primary(sim: &mut Sim, path: PathBuf) {
17+
init_tracing();
18+
sim.host("primary", move || {
19+
let path = path.clone();
20+
async move {
21+
let server = TestServer {
22+
path: path.into(),
23+
user_api_config: UserApiConfig {
24+
..Default::default()
25+
},
26+
admin_api_config: Some(AdminApiConfig {
27+
acceptor: TurmoilAcceptor::bind(([0, 0, 0, 0], 9090)).await?,
28+
connector: TurmoilConnector,
29+
disable_metrics: true,
30+
auth_key: None,
31+
}),
32+
rpc_server_config: Some(RpcServerConfig {
33+
acceptor: TurmoilAcceptor::bind(([0, 0, 0, 0], 4567)).await?,
34+
tls_config: None,
35+
}),
36+
disable_namespaces: false,
37+
disable_default_namespace: false,
38+
..Default::default()
39+
};
40+
41+
server.start_sim(8080).await?;
42+
43+
Ok(())
44+
}
45+
});
46+
}
47+
1448
#[test]
1549
fn admin_auth() {
1650
let mut sim = turmoil::Builder::new()
@@ -65,3 +99,81 @@ fn admin_auth() {
6599

66100
sim.run().unwrap();
67101
}
102+
103+
#[test]
104+
fn list_namespaces_basic() {
105+
let mut sim = turmoil::Builder::new()
106+
.simulation_duration(Duration::from_secs(1000))
107+
.build();
108+
let tmp = tempdir().unwrap();
109+
make_primary(&mut sim, tmp.path().to_path_buf());
110+
111+
sim.client("client", async {
112+
let client = Client::new();
113+
114+
// Step 1: List initially - should have default namespace
115+
let resp = client
116+
.get("http://primary:9090/v1/namespaces")
117+
.await?;
118+
assert!(resp.status().is_success());
119+
120+
let body: serde_json::Value = resp.json().await?;
121+
let namespaces = body["namespaces"].as_array().unwrap();
122+
assert_eq!(namespaces.len(), 1);
123+
assert_eq!(namespaces[0]["name"], "default");
124+
125+
// Step 2: Create foo namespace
126+
client
127+
.post("http://primary:9090/v1/namespaces/foo/create", json!({}))
128+
.await?;
129+
130+
// Step 3: Create schema namespace and bar with shared_schema_name
131+
client
132+
.post(
133+
"http://primary:9090/v1/namespaces/schema/create",
134+
json!({ "shared_schema": true }),
135+
)
136+
.await?;
137+
client
138+
.post(
139+
"http://primary:9090/v1/namespaces/bar/create",
140+
json!({ "shared_schema_name": "schema" }),
141+
)
142+
.await?;
143+
144+
// Step 4: List again - should have 3 namespaces
145+
let resp = client
146+
.get("http://primary:9090/v1/namespaces")
147+
.await?;
148+
let body: serde_json::Value = resp.json().await?;
149+
let namespaces = body["namespaces"].as_array().unwrap();
150+
assert_eq!(namespaces.len(), 3);
151+
152+
// Verify all namespace names are present
153+
let names: Vec<_> = namespaces
154+
.iter()
155+
.map(|n| n["name"].as_str().unwrap())
156+
.collect();
157+
assert!(names.contains(&"default"));
158+
assert!(names.contains(&"foo"));
159+
assert!(names.contains(&"bar"));
160+
161+
// Verify shared_schema_name for bar
162+
let bar = namespaces
163+
.iter()
164+
.find(|n| n["name"] == "bar")
165+
.unwrap();
166+
assert_eq!(bar["shared_schema_name"], "schema");
167+
168+
// Verify foo doesn't have shared_schema_name
169+
let foo = namespaces
170+
.iter()
171+
.find(|n| n["name"] == "foo")
172+
.unwrap();
173+
assert!(foo["shared_schema_name"].is_null());
174+
175+
Ok(())
176+
});
177+
178+
sim.run().unwrap();
179+
}

0 commit comments

Comments
 (0)