Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions libsql-server/src/http/admin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -262,10 +263,56 @@ async fn handle_get_config<C: Connector>(
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<NamespaceListItem>,
}

async fn handle_list_namespaces<C>(
State(app_state): State<Arc<AppState<C>>>,
) -> crate::Result<Json<ListNamespacesResponse>> {
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<C>(
State(app_state): State<Arc<AppState<C>>>,
) -> crate::Result<Json<Vec<String>>> {
Expand Down Expand Up @@ -311,6 +358,8 @@ struct HttpDatabaseConfig {
txn_timeout_s: Option<u64>,
#[serde(default)]
durability_mode: Option<DurabilityMode>,
#[serde(default)]
shared_schema_name: Option<NamespaceName>,
}

async fn handle_post_config<C>(
Expand Down
6 changes: 6 additions & 0 deletions libsql-server/src/namespace/meta_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,12 @@ impl MetaStore {
}
None
}

pub async fn list_namespaces(&self) -> crate::Result<Vec<NamespaceName>> {
let configs = self.inner.configs.lock().await;
let namespaces = configs.keys().cloned().collect();
Ok(namespaces)
}
}

impl MetaStoreHandle {
Expand Down
2 changes: 1 addition & 1 deletion libsql-server/src/namespace/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
116 changes: 114 additions & 2 deletions libsql-server/tests/standalone/admin.rs
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -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();
}
Loading