Add local KV store commands
This commit is contained in:
parent
4ec50d7473
commit
1a0f1e7671
14 changed files with 395 additions and 13 deletions
|
|
@ -120,6 +120,8 @@ Roadmap items should be actionable and checkable:
|
||||||
`cas cleanup` evicts unpinned blobs while retaining pinned blobs.
|
`cas cleanup` evicts unpinned blobs while retaining pinned blobs.
|
||||||
- DB resources can be registered locally and report local-only status. cr-sqlite
|
- DB resources can be registered locally and report local-only status. cr-sqlite
|
||||||
loading, change extraction, and sync are still roadmap work.
|
loading, change extraction, and sync are still roadmap work.
|
||||||
|
- KV stores support local SQLite-backed create/set/get. Iroh Documents
|
||||||
|
replication and prefix-capability enforcement are still roadmap work.
|
||||||
- Signed peer-card LAN discovery payloads, peer auth over Iroh, cr-sqlite,
|
- Signed peer-card LAN discovery payloads, peer auth over Iroh, cr-sqlite,
|
||||||
iroh-docs, iroh-gossip, iroh-blobs, Automerge sync, real auth enforcement,
|
iroh-docs, iroh-gossip, iroh-blobs, Automerge sync, real auth enforcement,
|
||||||
OpenSSH KRL generation, and Keyhive/BeeKEM-style authorization are future
|
OpenSSH KRL generation, and Keyhive/BeeKEM-style authorization are future
|
||||||
|
|
|
||||||
3
Cargo.lock
generated
3
Cargo.lock
generated
|
|
@ -1117,6 +1117,7 @@ dependencies = [
|
||||||
"geth-auth",
|
"geth-auth",
|
||||||
"geth-db",
|
"geth-db",
|
||||||
"geth-keychain",
|
"geth-keychain",
|
||||||
|
"geth-kv",
|
||||||
"geth-resource",
|
"geth-resource",
|
||||||
"geth-ssh-identity",
|
"geth-ssh-identity",
|
||||||
"geth-types",
|
"geth-types",
|
||||||
|
|
@ -1196,6 +1197,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"geth-types",
|
"geth-types",
|
||||||
"serde",
|
"serde",
|
||||||
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1210,6 +1212,7 @@ dependencies = [
|
||||||
"geth-db",
|
"geth-db",
|
||||||
"geth-iroh",
|
"geth-iroh",
|
||||||
"geth-keychain",
|
"geth-keychain",
|
||||||
|
"geth-kv",
|
||||||
"geth-resource",
|
"geth-resource",
|
||||||
"geth-ssh-identity",
|
"geth-ssh-identity",
|
||||||
"geth-store",
|
"geth-store",
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ The bootstrap implementation provides:
|
||||||
`cleanup`, `list`
|
`cleanup`, `list`
|
||||||
- local DB resource registration: `geth db add <name> <path>` and
|
- local DB resource registration: `geth db add <name> <path>` and
|
||||||
`geth db status <name>`
|
`geth db status <name>`
|
||||||
|
- local SQLite-backed KV commands: `geth kv create/set/get`
|
||||||
- SSH certificate flow metadata:
|
- SSH certificate flow metadata:
|
||||||
- `geth ssh cert request --public-key <path> --principal <name>`
|
- `geth ssh cert request --public-key <path> --principal <name>`
|
||||||
- `geth ssh cert requests`
|
- `geth ssh cert requests`
|
||||||
|
|
@ -93,8 +94,8 @@ The bootstrap implementation provides:
|
||||||
- `geth ssh revocation list`
|
- `geth ssh revocation list`
|
||||||
- `geth ssh revocation export --out <path>`
|
- `geth ssh revocation export --out <path>`
|
||||||
|
|
||||||
Other command groups exist as explicit stubs: `kv`, `pipe`, `document`,
|
Other command groups exist as explicit stubs: `pipe`, `document`, `pubsub`,
|
||||||
`pubsub`, `secret`, and `ssh`.
|
`secret`, and `ssh`.
|
||||||
|
|
||||||
## Resource Modules
|
## Resource Modules
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -407,9 +407,10 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
|
||||||
CasCommand::Cleanup { dry_run } => ControlRequest::CasCleanup { dry_run },
|
CasCommand::Cleanup { dry_run } => ControlRequest::CasCleanup { dry_run },
|
||||||
CasCommand::List => ControlRequest::CasList,
|
CasCommand::List => ControlRequest::CasList,
|
||||||
},
|
},
|
||||||
Command::Kv { command } => ControlRequest::ModuleStub {
|
Command::Kv { command } => match command {
|
||||||
module: "kv".to_owned(),
|
KvCommand::Create { name } => ControlRequest::KvCreate { name },
|
||||||
command: format!("{command:?}"),
|
KvCommand::Set { name, key, value } => ControlRequest::KvSet { name, key, value },
|
||||||
|
KvCommand::Get { name, key } => ControlRequest::KvGet { name, key },
|
||||||
},
|
},
|
||||||
Command::Pubsub { command } => ControlRequest::ModuleStub {
|
Command::Pubsub { command } => ControlRequest::ModuleStub {
|
||||||
module: "pubsub".to_owned(),
|
module: "pubsub".to_owned(),
|
||||||
|
|
@ -766,6 +767,22 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
|
||||||
println!("schema_metadata: {}", db.schema_metadata);
|
println!("schema_metadata: {}", db.schema_metadata);
|
||||||
println!("sync_status: {}", db.sync_status);
|
println!("sync_status: {}", db.sync_status);
|
||||||
}
|
}
|
||||||
|
ControlResponse::KvCreated { kv } => {
|
||||||
|
println!("created kv: {}", kv.name);
|
||||||
|
println!("id: {}", kv.id);
|
||||||
|
println!("resource: {}", kv.resource);
|
||||||
|
println!("sync_status: {}", kv.sync_status);
|
||||||
|
}
|
||||||
|
ControlResponse::KvSet { entry } => {
|
||||||
|
println!("set {} {}", entry.store, entry.key);
|
||||||
|
}
|
||||||
|
ControlResponse::KvGet { entry } => {
|
||||||
|
if let Some(entry) = entry {
|
||||||
|
println!("{}", entry.value);
|
||||||
|
} else {
|
||||||
|
println!("not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
ControlResponse::NotImplemented { module, command } => {
|
ControlResponse::NotImplemented { module, command } => {
|
||||||
println!("{module} {command}: not implemented yet");
|
println!("{module} {command}: not implemented yet");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ thiserror.workspace = true
|
||||||
geth-auth = { path = "../geth-auth" }
|
geth-auth = { path = "../geth-auth" }
|
||||||
geth-db = { path = "../geth-db" }
|
geth-db = { path = "../geth-db" }
|
||||||
geth-keychain = { path = "../geth-keychain" }
|
geth-keychain = { path = "../geth-keychain" }
|
||||||
|
geth-kv = { path = "../geth-kv" }
|
||||||
geth-resource = { path = "../geth-resource" }
|
geth-resource = { path = "../geth-resource" }
|
||||||
geth-ssh-identity = { path = "../geth-ssh-identity" }
|
geth-ssh-identity = { path = "../geth-ssh-identity" }
|
||||||
geth-types = { path = "../geth-types" }
|
geth-types = { path = "../geth-types" }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use geth_auth::{AuthExplanation, AuthOp};
|
use geth_auth::{AuthExplanation, AuthOp};
|
||||||
use geth_db::DbResource;
|
use geth_db::DbResource;
|
||||||
use geth_keychain::KeychainOp;
|
use geth_keychain::KeychainOp;
|
||||||
|
use geth_kv::{KvEntry, KvResource};
|
||||||
use geth_resource::ResourceDescriptor;
|
use geth_resource::ResourceDescriptor;
|
||||||
use geth_ssh_identity::{
|
use geth_ssh_identity::{
|
||||||
SshCertApproval, SshCertRequest, SshCertificateRecord, SshRevocationEntry,
|
SshCertApproval, SshCertRequest, SshCertificateRecord, SshRevocationEntry,
|
||||||
|
|
@ -98,6 +99,18 @@ pub enum ControlRequest {
|
||||||
DbStatus {
|
DbStatus {
|
||||||
name: String,
|
name: String,
|
||||||
},
|
},
|
||||||
|
KvCreate {
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
KvSet {
|
||||||
|
name: String,
|
||||||
|
key: String,
|
||||||
|
value: String,
|
||||||
|
},
|
||||||
|
KvGet {
|
||||||
|
name: String,
|
||||||
|
key: String,
|
||||||
|
},
|
||||||
ModuleStub {
|
ModuleStub {
|
||||||
module: String,
|
module: String,
|
||||||
command: String,
|
command: String,
|
||||||
|
|
@ -183,6 +196,15 @@ pub enum ControlResponse {
|
||||||
DbStatus {
|
DbStatus {
|
||||||
db: DbResource,
|
db: DbResource,
|
||||||
},
|
},
|
||||||
|
KvCreated {
|
||||||
|
kv: KvResource,
|
||||||
|
},
|
||||||
|
KvSet {
|
||||||
|
entry: KvEntry,
|
||||||
|
},
|
||||||
|
KvGet {
|
||||||
|
entry: Option<KvEntry>,
|
||||||
|
},
|
||||||
NotImplemented {
|
NotImplemented {
|
||||||
module: String,
|
module: String,
|
||||||
command: String,
|
command: String,
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,5 @@ license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
geth-types = { path = "../geth-types" }
|
geth-types = { path = "../geth-types" }
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,66 @@ pub struct KvResource {
|
||||||
pub id: KvId,
|
pub id: KvId,
|
||||||
pub resource: ResourceId,
|
pub resource: ResourceId,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub sync_status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct KvEntry {
|
||||||
|
pub store: KvId,
|
||||||
|
pub key: String,
|
||||||
|
pub value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum KvError {
|
||||||
|
#[error("invalid kv store name: {0}")]
|
||||||
|
InvalidName(String),
|
||||||
|
#[error("invalid kv key: {0}")]
|
||||||
|
InvalidKey(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_kv_name(name: &str) -> Result<(), KvError> {
|
||||||
|
if name.is_empty()
|
||||||
|
|| !name
|
||||||
|
.bytes()
|
||||||
|
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
|
||||||
|
{
|
||||||
|
return Err(KvError::InvalidName(name.to_owned()));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_kv_key(key: &str) -> Result<(), KvError> {
|
||||||
|
if key.is_empty() || key.bytes().any(|byte| byte == 0 || byte == b'\n') {
|
||||||
|
return Err(KvError::InvalidKey(key.to_owned()));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn iroh_docs_roadmap() -> &'static str {
|
pub fn iroh_docs_roadmap() -> &'static str {
|
||||||
"future kv storage uses Iroh Documents namespaces with prefix-scoped authorization"
|
"future kv storage uses Iroh Documents namespaces with prefix-scoped authorization"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn kv_name_validation_rejects_paths_and_empty_names() {
|
||||||
|
assert!(validate_kv_name("prefs").is_ok());
|
||||||
|
assert!(validate_kv_name("apps.foo").is_ok());
|
||||||
|
assert!(validate_kv_name("").is_err());
|
||||||
|
assert!(validate_kv_name("../prefs").is_err());
|
||||||
|
assert!(validate_kv_name("prefs/main").is_err());
|
||||||
|
assert!(validate_kv_name("prefs main").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn kv_key_validation_rejects_empty_newline_and_nul() {
|
||||||
|
assert!(validate_kv_key("apps/foo/theme").is_ok());
|
||||||
|
assert!(validate_kv_key("").is_err());
|
||||||
|
assert!(validate_kv_key("apps/foo\nbar").is_err());
|
||||||
|
assert!(validate_kv_key("apps/foo\0bar").is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ geth-crypto = { path = "../geth-crypto" }
|
||||||
geth-db = { path = "../geth-db" }
|
geth-db = { path = "../geth-db" }
|
||||||
geth-iroh = { path = "../geth-iroh" }
|
geth-iroh = { path = "../geth-iroh" }
|
||||||
geth-keychain = { path = "../geth-keychain" }
|
geth-keychain = { path = "../geth-keychain" }
|
||||||
|
geth-kv = { path = "../geth-kv" }
|
||||||
geth-resource = { path = "../geth-resource" }
|
geth-resource = { path = "../geth-resource" }
|
||||||
geth-ssh-identity = { path = "../geth-ssh-identity" }
|
geth-ssh-identity = { path = "../geth-ssh-identity" }
|
||||||
geth-store = { path = "../geth-store" }
|
geth-store = { path = "../geth-store" }
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ use geth_crypto::AgentKey;
|
||||||
use geth_db::DbResource;
|
use geth_db::DbResource;
|
||||||
use geth_iroh::{EndpointStatus, GethIrohConfig, GethIrohEndpoint, GethRelayMode};
|
use geth_iroh::{EndpointStatus, GethIrohConfig, GethIrohEndpoint, GethRelayMode};
|
||||||
use geth_keychain::{KeychainOp, KeychainOpKind};
|
use geth_keychain::{KeychainOp, KeychainOpKind};
|
||||||
|
use geth_kv::{KvEntry, KvResource};
|
||||||
use geth_resource::ResourceDescriptor;
|
use geth_resource::ResourceDescriptor;
|
||||||
use geth_ssh_identity::{
|
use geth_ssh_identity::{
|
||||||
SshCertApproval, SshCertKind, SshCertRequest, SshCertRequestStatus, SshCertificateRecord,
|
SshCertApproval, SshCertKind, SshCertRequest, SshCertRequestStatus, SshCertificateRecord,
|
||||||
|
|
@ -18,8 +19,8 @@ use geth_ssh_identity::{
|
||||||
certificate_id, revocation_id, ssh_public_key_fingerprint,
|
certificate_id, revocation_id, ssh_public_key_fingerprint,
|
||||||
};
|
};
|
||||||
use geth_store::{
|
use geth_store::{
|
||||||
Store, StoredAuthOp, StoredDbResource, StoredKeychainOp, StoredResource, StoredSshCertRequest,
|
Store, StoredAuthOp, StoredDbResource, StoredKeychainOp, StoredKvEntry, StoredKvStore,
|
||||||
StoredSshCertificate, StoredSshRevocation,
|
StoredResource, StoredSshCertRequest, StoredSshCertificate, StoredSshRevocation,
|
||||||
};
|
};
|
||||||
use geth_types::{
|
use geth_types::{
|
||||||
AuthOpId, Capability, KeyId, NodeId, PrincipalId, ResourceId, ResourceKind, ResourceName,
|
AuthOpId, Capability, KeyId, NodeId, PrincipalId, ResourceId, ResourceKind, ResourceName,
|
||||||
|
|
@ -53,6 +54,12 @@ pub enum NodeError {
|
||||||
InvalidDbPath(String),
|
InvalidDbPath(String),
|
||||||
#[error("db resource not found: {0}")]
|
#[error("db resource not found: {0}")]
|
||||||
DbNotFound(String),
|
DbNotFound(String),
|
||||||
|
#[error("invalid kv store name: {0}")]
|
||||||
|
InvalidKvName(String),
|
||||||
|
#[error("invalid kv key: {0}")]
|
||||||
|
InvalidKvKey(String),
|
||||||
|
#[error("kv store not found: {0}")]
|
||||||
|
KvNotFound(String),
|
||||||
#[error("invalid ssh certificate kind: {0}")]
|
#[error("invalid ssh certificate kind: {0}")]
|
||||||
InvalidSshCertKind(String),
|
InvalidSshCertKind(String),
|
||||||
#[error("invalid ssh certificate request status: {0}")]
|
#[error("invalid ssh certificate request status: {0}")]
|
||||||
|
|
@ -592,6 +599,55 @@ pub fn handle_request(
|
||||||
db: db_resource_from_stored(&stored)?,
|
db: db_resource_from_stored(&stored)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
ControlRequest::KvCreate { name } => {
|
||||||
|
geth_kv::validate_kv_name(&name).map_err(|_| NodeError::InvalidKvName(name.clone()))?;
|
||||||
|
let resource_id = format!("resource:kv:{name}");
|
||||||
|
let kv_id = format!("kv:{name}");
|
||||||
|
let resource = StoredResource {
|
||||||
|
resource_id: resource_id.clone(),
|
||||||
|
kind: ResourceKind::Kv.to_string(),
|
||||||
|
name: name.clone(),
|
||||||
|
status: "active".to_owned(),
|
||||||
|
};
|
||||||
|
store.insert_resource(&resource)?;
|
||||||
|
let stored = StoredKvStore {
|
||||||
|
kv_id,
|
||||||
|
resource_id,
|
||||||
|
name,
|
||||||
|
};
|
||||||
|
store.insert_kv_store(&stored)?;
|
||||||
|
Ok(ControlResponse::KvCreated {
|
||||||
|
kv: kv_resource_from_stored(&stored),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ControlRequest::KvSet { name, key, value } => {
|
||||||
|
geth_kv::validate_kv_name(&name).map_err(|_| NodeError::InvalidKvName(name.clone()))?;
|
||||||
|
geth_kv::validate_kv_key(&key).map_err(|_| NodeError::InvalidKvKey(key.clone()))?;
|
||||||
|
let kv = store
|
||||||
|
.get_kv_store_by_name(&name)?
|
||||||
|
.ok_or_else(|| NodeError::KvNotFound(name.clone()))?;
|
||||||
|
let stored = StoredKvEntry {
|
||||||
|
kv_id: kv.kv_id,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
updated_at_ms: geth_store::now_ms(),
|
||||||
|
};
|
||||||
|
store.set_kv_entry(&stored)?;
|
||||||
|
Ok(ControlResponse::KvSet {
|
||||||
|
entry: kv_entry_from_stored(stored),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ControlRequest::KvGet { name, key } => {
|
||||||
|
geth_kv::validate_kv_name(&name).map_err(|_| NodeError::InvalidKvName(name.clone()))?;
|
||||||
|
geth_kv::validate_kv_key(&key).map_err(|_| NodeError::InvalidKvKey(key.clone()))?;
|
||||||
|
let kv = store
|
||||||
|
.get_kv_store_by_name(&name)?
|
||||||
|
.ok_or_else(|| NodeError::KvNotFound(name.clone()))?;
|
||||||
|
let entry = store
|
||||||
|
.get_kv_entry(&kv.kv_id, &key)?
|
||||||
|
.map(kv_entry_from_stored);
|
||||||
|
Ok(ControlResponse::KvGet { entry })
|
||||||
|
}
|
||||||
ControlRequest::ModuleStub { module, command } => {
|
ControlRequest::ModuleStub { module, command } => {
|
||||||
Ok(ControlResponse::NotImplemented { module, command })
|
Ok(ControlResponse::NotImplemented { module, command })
|
||||||
}
|
}
|
||||||
|
|
@ -625,6 +681,23 @@ fn db_resource_from_stored(stored: &StoredDbResource) -> Result<DbResource, Node
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn kv_resource_from_stored(stored: &StoredKvStore) -> KvResource {
|
||||||
|
KvResource {
|
||||||
|
id: stored.kv_id.clone().into(),
|
||||||
|
resource: stored.resource_id.clone().into(),
|
||||||
|
name: stored.name.clone(),
|
||||||
|
sync_status: "local-only".to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kv_entry_from_stored(stored: StoredKvEntry) -> KvEntry {
|
||||||
|
KvEntry {
|
||||||
|
store: stored.kv_id.into(),
|
||||||
|
key: stored.key,
|
||||||
|
value: stored.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn store_auth_op(store: &Store, op: &AuthOp) -> Result<(), NodeError> {
|
fn store_auth_op(store: &Store, op: &AuthOp) -> Result<(), NodeError> {
|
||||||
store.insert_auth_op(&StoredAuthOp {
|
store.insert_auth_op(&StoredAuthOp {
|
||||||
op_id: op.id.to_string(),
|
op_id: op.id.to_string(),
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,13 @@ impl Store {
|
||||||
resource_id TEXT NOT NULL,
|
resource_id TEXT NOT NULL,
|
||||||
name TEXT NOT NULL
|
name TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS kv_entries (
|
||||||
|
kv_id TEXT NOT NULL,
|
||||||
|
key TEXT NOT NULL,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
updated_at_ms INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY(kv_id, key)
|
||||||
|
);
|
||||||
CREATE TABLE IF NOT EXISTS db_resources (
|
CREATE TABLE IF NOT EXISTS db_resources (
|
||||||
db_id TEXT PRIMARY KEY,
|
db_id TEXT PRIMARY KEY,
|
||||||
resource_id TEXT NOT NULL,
|
resource_id TEXT NOT NULL,
|
||||||
|
|
@ -275,6 +282,63 @@ impl Store {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn insert_kv_store(&self, kv: &StoredKvStore) -> Result<(), StoreError> {
|
||||||
|
self.conn.execute(
|
||||||
|
r#"INSERT OR REPLACE INTO kv_stores(kv_id, resource_id, name)
|
||||||
|
VALUES (?1, ?2, ?3)"#,
|
||||||
|
params![kv.kv_id, kv.resource_id, kv.name],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_kv_store_by_name(&self, name: &str) -> Result<Option<StoredKvStore>, StoreError> {
|
||||||
|
let mut stmt = self.conn.prepare(
|
||||||
|
r#"SELECT kv_id, resource_id, name
|
||||||
|
FROM kv_stores WHERE name = ?1"#,
|
||||||
|
)?;
|
||||||
|
let mut rows = stmt.query(params![name])?;
|
||||||
|
if let Some(row) = rows.next()? {
|
||||||
|
Ok(Some(StoredKvStore {
|
||||||
|
kv_id: row.get(0)?,
|
||||||
|
resource_id: row.get(1)?,
|
||||||
|
name: row.get(2)?,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_kv_entry(&self, entry: &StoredKvEntry) -> Result<(), StoreError> {
|
||||||
|
self.conn.execute(
|
||||||
|
r#"INSERT OR REPLACE INTO kv_entries(kv_id, key, value, updated_at_ms)
|
||||||
|
VALUES (?1, ?2, ?3, ?4)"#,
|
||||||
|
params![entry.kv_id, entry.key, entry.value, entry.updated_at_ms],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_kv_entry(
|
||||||
|
&self,
|
||||||
|
kv_id: &str,
|
||||||
|
key: &str,
|
||||||
|
) -> Result<Option<StoredKvEntry>, StoreError> {
|
||||||
|
let mut stmt = self.conn.prepare(
|
||||||
|
r#"SELECT kv_id, key, value, updated_at_ms
|
||||||
|
FROM kv_entries WHERE kv_id = ?1 AND key = ?2"#,
|
||||||
|
)?;
|
||||||
|
let mut rows = stmt.query(params![kv_id, key])?;
|
||||||
|
if let Some(row) = rows.next()? {
|
||||||
|
Ok(Some(StoredKvEntry {
|
||||||
|
kv_id: row.get(0)?,
|
||||||
|
key: row.get(1)?,
|
||||||
|
value: row.get(2)?,
|
||||||
|
updated_at_ms: row.get(3)?,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn record_cas_object(
|
pub fn record_cas_object(
|
||||||
&self,
|
&self,
|
||||||
hash: &str,
|
hash: &str,
|
||||||
|
|
@ -622,6 +686,21 @@ pub struct StoredDbResource {
|
||||||
pub path: String,
|
pub path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct StoredKvStore {
|
||||||
|
pub kv_id: String,
|
||||||
|
pub resource_id: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct StoredKvEntry {
|
||||||
|
pub kv_id: String,
|
||||||
|
pub key: String,
|
||||||
|
pub value: String,
|
||||||
|
pub updated_at_ms: i64,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct CasObject {
|
pub struct CasObject {
|
||||||
pub hash: String,
|
pub hash: String,
|
||||||
|
|
@ -883,4 +962,33 @@ mod tests {
|
||||||
Some("/tmp/notes.sqlite".to_owned())
|
Some("/tmp/notes.sqlite".to_owned())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn kv_store_and_entries_roundtrip() {
|
||||||
|
let store = Store::open_memory().expect("open");
|
||||||
|
let kv = StoredKvStore {
|
||||||
|
kv_id: "kv:prefs".to_owned(),
|
||||||
|
resource_id: "resource:kv:prefs".to_owned(),
|
||||||
|
name: "prefs".to_owned(),
|
||||||
|
};
|
||||||
|
store.insert_kv_store(&kv).expect("insert kv");
|
||||||
|
assert_eq!(
|
||||||
|
store.get_kv_store_by_name("prefs").expect("get kv"),
|
||||||
|
Some(kv.clone())
|
||||||
|
);
|
||||||
|
|
||||||
|
let entry = StoredKvEntry {
|
||||||
|
kv_id: kv.kv_id.clone(),
|
||||||
|
key: "apps/foo/theme".to_owned(),
|
||||||
|
value: "dark".to_owned(),
|
||||||
|
updated_at_ms: 1,
|
||||||
|
};
|
||||||
|
store.set_kv_entry(&entry).expect("set entry");
|
||||||
|
assert_eq!(
|
||||||
|
store
|
||||||
|
.get_kv_entry("kv:prefs", "apps/foo/theme")
|
||||||
|
.expect("get entry"),
|
||||||
|
Some(entry)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -454,6 +454,97 @@ fn db_add_and_status_register_local_db_metadata() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn kv_create_set_get_use_local_store() {
|
||||||
|
let home = tempfile::tempdir().expect("tempdir");
|
||||||
|
let paths = geth_config::GethPaths::from_home(home.path());
|
||||||
|
let node = geth_node::init_node(&paths).expect("init node");
|
||||||
|
|
||||||
|
let response = geth_node::handle_request(
|
||||||
|
&node,
|
||||||
|
geth_control::ControlRequest::KvCreate {
|
||||||
|
name: "prefs".to_owned(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("create kv");
|
||||||
|
match response {
|
||||||
|
geth_control::ControlResponse::KvCreated { kv } => {
|
||||||
|
assert_eq!(kv.name, "prefs");
|
||||||
|
assert_eq!(kv.sync_status, "local-only");
|
||||||
|
}
|
||||||
|
other => panic!("unexpected response: {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = geth_node::handle_request(
|
||||||
|
&node,
|
||||||
|
geth_control::ControlRequest::KvSet {
|
||||||
|
name: "prefs".to_owned(),
|
||||||
|
key: "apps/foo/theme".to_owned(),
|
||||||
|
value: "dark".to_owned(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("set kv");
|
||||||
|
match response {
|
||||||
|
geth_control::ControlResponse::KvSet { entry } => {
|
||||||
|
assert_eq!(entry.store.to_string(), "kv:prefs");
|
||||||
|
assert_eq!(entry.key, "apps/foo/theme");
|
||||||
|
assert_eq!(entry.value, "dark");
|
||||||
|
}
|
||||||
|
other => panic!("unexpected response: {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = geth_node::handle_request(
|
||||||
|
&node,
|
||||||
|
geth_control::ControlRequest::KvGet {
|
||||||
|
name: "prefs".to_owned(),
|
||||||
|
key: "apps/foo/theme".to_owned(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("get kv");
|
||||||
|
match response {
|
||||||
|
geth_control::ControlResponse::KvGet { entry } => {
|
||||||
|
assert_eq!(entry.expect("entry").value, "dark");
|
||||||
|
}
|
||||||
|
other => panic!("unexpected response: {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = geth_node::handle_request(
|
||||||
|
&node,
|
||||||
|
geth_control::ControlRequest::KvGet {
|
||||||
|
name: "prefs".to_owned(),
|
||||||
|
key: "apps/foo/missing".to_owned(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("get missing kv");
|
||||||
|
match response {
|
||||||
|
geth_control::ControlResponse::KvGet { entry } => {
|
||||||
|
assert!(entry.is_none());
|
||||||
|
}
|
||||||
|
other => panic!("unexpected response: {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
geth_node::handle_request(
|
||||||
|
&node,
|
||||||
|
geth_control::ControlRequest::KvCreate {
|
||||||
|
name: "../bad".to_owned(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
geth_node::handle_request(
|
||||||
|
&node,
|
||||||
|
geth_control::ControlRequest::KvSet {
|
||||||
|
name: "missing".to_owned(),
|
||||||
|
key: "apps/foo/theme".to_owned(),
|
||||||
|
value: "dark".to_owned(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ssh_cert_request_approval_and_revocation_export_use_local_state() {
|
fn ssh_cert_request_approval_and_revocation_export_use_local_state() {
|
||||||
let home = tempfile::tempdir().expect("tempdir");
|
let home = tempfile::tempdir().expect("tempdir");
|
||||||
|
|
|
||||||
|
|
@ -97,8 +97,12 @@ and file sync trees are future work.
|
||||||
local-only sync status plus placeholder schema metadata. cr-sqlite loading,
|
local-only sync status plus placeholder schema metadata. cr-sqlite loading,
|
||||||
change extraction, and DB sync are future work.
|
change extraction, and DB sync are future work.
|
||||||
|
|
||||||
`geth-kv`, `geth-document`, `geth-pubsub`, `geth-pipe`, and `geth-ssh-proxy`
|
`geth-kv` currently provides a SQLite-backed local fallback for named KV stores
|
||||||
currently define types, command shape, and roadmap stubs.
|
through `kv create/set/get`. Iroh Documents namespaces, prefix authorization
|
||||||
|
enforcement, and replication are future work.
|
||||||
|
|
||||||
|
`geth-document`, `geth-pubsub`, `geth-pipe`, and `geth-ssh-proxy` currently
|
||||||
|
define types, command shape, and roadmap stubs.
|
||||||
|
|
||||||
`geth-ssh-identity` defines SSH trust namespaces plus certificate request,
|
`geth-ssh-identity` defines SSH trust namespaces plus certificate request,
|
||||||
approval, certificate import, and revocation-list data models. The bootstrap
|
approval, certificate import, and revocation-list data models. The bootstrap
|
||||||
|
|
|
||||||
|
|
@ -215,11 +215,12 @@ authorization and durable-state boundaries clear.
|
||||||
- Access is gated by resource secret epoch material.
|
- Access is gated by resource secret epoch material.
|
||||||
- Docs explicitly avoid claiming forward secrecy or PCS.
|
- Docs explicitly avoid claiming forward secrecy or PCS.
|
||||||
|
|
||||||
- `[ ]` Iroh-docs KV integration.
|
- `[~]` Iroh-docs KV integration.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
- `geth kv create/set/get` works against a named KV resource.
|
- `[x]` `geth kv create/set/get` works against a named local KV resource.
|
||||||
- Prefix-scoped capabilities can allow or deny writes.
|
- `[x]` KV metadata and entries are durable in the local SQLite store.
|
||||||
- KV metadata is durable and replicated through Iroh Documents.
|
- `[ ]` Prefix-scoped capabilities can allow or deny writes.
|
||||||
|
- `[ ]` KV metadata is replicated through Iroh Documents.
|
||||||
|
|
||||||
- `[ ]` Iroh-gossip pubsub integration.
|
- `[ ]` Iroh-gossip pubsub integration.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue