lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 688543521e41a742df9d7152cdeda107f08d2903
parent 886ff6b38763019f12cb7a81e3cbb654e0d2710a
Author: triesap <tyson@radroots.org>
Date:   Wed,  8 Apr 2026 23:54:44 +0000

manager: add shared runtime control boundary

- add the runtime-manager crate with typed management contract parsing
- resolve shared and per-instance runtime control paths from runtime-paths
- persist managed runtime instance registry records and bootstrap lookups
- wire the crate into workspace coverage and release metadata

Diffstat:
MCargo.lock | 11+++++++++++
MCargo.toml | 2++
Mcontract/coverage/policy.toml | 1+
Mcontract/release/publish-set.toml | 2++
Acrates/runtime-manager/Cargo.toml | 23+++++++++++++++++++++++
Acrates/runtime-manager/README.md | 14++++++++++++++
Acrates/runtime-manager/src/error.rs | 46++++++++++++++++++++++++++++++++++++++++++++++
Acrates/runtime-manager/src/lib.rs | 305++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/runtime-manager/src/model.rs | 162+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/runtime-manager/src/paths.rs | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/runtime-manager/src/registry.rs | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
11 files changed, 804 insertions(+), 0 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -2628,6 +2628,17 @@ dependencies = [ ] [[package]] +name = "radroots-runtime-manager" +version = "0.1.0-alpha.1" +dependencies = [ + "radroots-runtime-paths", + "serde", + "tempfile", + "thiserror 1.0.69", + "toml", +] + +[[package]] name = "radroots-runtime-paths" version = "0.1.0-alpha.1" dependencies = [ diff --git a/Cargo.toml b/Cargo.toml @@ -37,6 +37,7 @@ members = [ "crates/replica-db-wasm", "crates/runtime-paths", "crates/runtime-distribution", + "crates/runtime-manager", "crates/trade", "crates/types", "crates/protected-store", @@ -68,6 +69,7 @@ radroots-nostr-ndb = { path = "crates/nostr-ndb", version = "0.1.0-alpha.1", def radroots-runtime = { path = "crates/runtime", version = "0.1.0-alpha.1", default-features = false } radroots-runtime-paths = { path = "crates/runtime-paths", version = "0.1.0-alpha.1", default-features = false } radroots-runtime-distribution = { path = "crates/runtime-distribution", version = "0.1.0-alpha.1", default-features = false } +radroots-runtime-manager = { path = "crates/runtime-manager", version = "0.1.0-alpha.1", default-features = false } radroots-log = { path = "crates/log", version = "0.1.0-alpha.1", default-features = false } radroots-net = { path = "crates/net", version = "0.1.0-alpha.1", default-features = false } radroots-net-core = { path = "crates/net-core", version = "0.1.0-alpha.1", default-features = false } diff --git a/contract/coverage/policy.toml b/contract/coverage/policy.toml @@ -31,6 +31,7 @@ crates = [ "radroots-runtime", "radroots-runtime-paths", "radroots-runtime-distribution", + "radroots-runtime-manager", "radroots-secret-vault", "radroots-simplex-chat-proto", "radroots-simplex-smp-proto", diff --git a/contract/release/publish-set.toml b/contract/release/publish-set.toml @@ -11,6 +11,7 @@ crates = [ "radroots-runtime", "radroots-runtime-paths", "radroots-runtime-distribution", + "radroots-runtime-manager", "radroots-secret-vault", "radroots-simplex-chat-proto", "radroots-simplex-smp-proto", @@ -51,6 +52,7 @@ crates = [ "radroots-runtime", "radroots-runtime-paths", "radroots-runtime-distribution", + "radroots-runtime-manager", "radroots-secret-vault", "radroots-simplex-chat-proto", "radroots-simplex-smp-proto", diff --git a/crates/runtime-manager/Cargo.toml b/crates/runtime-manager/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "radroots-runtime-manager" +version = "0.1.0-alpha.1" +edition.workspace = true +authors = [ + "Radroots Authors", +] +rust-version.workspace = true +license.workspace = true +description = "shared local managed-runtime paths, registry, and lifecycle boundary for radroots" +repository.workspace = true +homepage.workspace = true +documentation = "https://docs.rs/radroots-runtime-manager" +readme = "README.md" + +[dependencies] +radroots-runtime-paths = { workspace = true } +serde = { workspace = true, features = ["derive"] } +thiserror = { workspace = true } +toml = { workspace = true } + +[dev-dependencies] +tempfile = { workspace = true } diff --git a/crates/runtime-manager/README.md b/crates/runtime-manager/README.md @@ -0,0 +1,14 @@ +# radroots-runtime-manager + +Shared local managed-runtime paths, registry, and lifecycle boundary for Rad Roots runtimes. + +## Goals + +- parse the machine-readable runtime management contract +- resolve shared manager roots and per-instance runtime-manager paths +- persist one shared managed-runtime instance registry +- define typed install and health state surfaces for CLI and app adoption later + +## License + +Licensed under AGPL-3.0. See LICENSE. diff --git a/crates/runtime-manager/src/error.rs b/crates/runtime-manager/src/error.rs @@ -0,0 +1,46 @@ +use std::path::PathBuf; + +use radroots_runtime_paths::RadrootsRuntimePathsError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum RadrootsRuntimeManagerError { + #[error("parse runtime management contract: {0}")] + Parse(String), + #[error("runtime management schema `{found}` does not match `{expected}`")] + UnexpectedSchema { + expected: &'static str, + found: String, + }, + #[error("management mode `{0}` not found in runtime management contract")] + UnknownManagementMode(String), + #[error("management mode `{mode_id}` does not support profile `{profile}`")] + UnsupportedProfile { mode_id: String, profile: String }, + #[error("management mode `{0}` has no shared path specification")] + MissingPathSpec(String), + #[error("unknown root class `{0}` in runtime management contract")] + UnknownRootClass(String), + #[error("runtime `{0}` has no bootstrap entry in runtime management contract")] + UnknownBootstrapRuntime(String), + #[error("read runtime instance registry {path}: {source}")] + ReadRegistry { + path: PathBuf, + source: std::io::Error, + }, + #[error("parse runtime instance registry {path}: {details}")] + ParseRegistry { path: PathBuf, details: String }, + #[error("serialize runtime instance registry: {0}")] + SerializeRegistry(String), + #[error("create runtime instance registry parent {path}: {source}")] + CreateRegistryParent { + path: PathBuf, + source: std::io::Error, + }, + #[error("write runtime instance registry {path}: {source}")] + WriteRegistry { + path: PathBuf, + source: std::io::Error, + }, + #[error(transparent)] + RuntimePaths(#[from] RadrootsRuntimePathsError), +} diff --git a/crates/runtime-manager/src/lib.rs b/crates/runtime-manager/src/lib.rs @@ -0,0 +1,305 @@ +#![forbid(unsafe_code)] + +pub mod error; +pub mod model; +pub mod paths; +pub mod registry; + +pub use error::RadrootsRuntimeManagerError; +pub use model::{ + BootstrapRuntimeContract, LifecycleContract, ManagedRuntimeHealthState, + ManagedRuntimeInstallState, ManagedRuntimeInstanceRecord, ManagedRuntimeInstanceRegistry, + ManagementDefaults, ManagementModeContract, ManagementPathContract, + RadrootsRuntimeManagementContract, RuntimeGroups, +}; +pub use paths::{ + ManagedRuntimeInstancePaths, ManagedRuntimeSharedPaths, bootstrap_runtime, + resolve_instance_paths, resolve_shared_paths, +}; +pub use registry::{instance, load_registry, save_registry, upsert_instance}; + +pub const RUNTIME_MANAGEMENT_SCHEMA: &str = "radroots-runtime-management"; + +pub fn parse_contract_str( + raw: &str, +) -> Result<RadrootsRuntimeManagementContract, RadrootsRuntimeManagerError> { + let contract = toml::from_str::<RadrootsRuntimeManagementContract>(raw) + .map_err(|err| RadrootsRuntimeManagerError::Parse(err.to_string()))?; + if contract.schema != RUNTIME_MANAGEMENT_SCHEMA { + return Err(RadrootsRuntimeManagerError::UnexpectedSchema { + expected: RUNTIME_MANAGEMENT_SCHEMA, + found: contract.schema.clone(), + }); + } + Ok(contract) +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use radroots_runtime_paths::{ + RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, + RadrootsPlatform, + }; + use tempfile::tempdir; + + use crate::{ + ManagedRuntimeHealthState, ManagedRuntimeInstallState, ManagedRuntimeInstanceRecord, + bootstrap_runtime, instance, load_registry, parse_contract_str, resolve_instance_paths, + resolve_shared_paths, save_registry, upsert_instance, + }; + + const CONTRACT: &str = r#" +schema = "radroots-runtime-management" +schema_version = 1 +owner_doc = "docs/migration/radroots-modular-runtime-management-bootstrap-rcl.md" +runtime_registry = "registry.toml" +distribution_contract = "distribution.toml" +capabilities_contract = "capabilities.toml" + +[defaults] +instance_cardinality = "single_default_instance" +managed_runtime_lookup = "shared_instance_registry" +explicit_runtime_endpoint_overrides_precede_managed_instance_binding = true +global_path_mutation_forbidden = true + +[management_clients] +active = ["cli"] +defined = ["community-app-desktop"] + +[managed_runtime_targets] +active = ["radrootsd"] +defined = ["myc", "rhi"] +bootstrap_only = ["hyf"] + +[lifecycle] +actions = ["install", "uninstall", "start", "stop", "restart", "status", "logs", "config_show", "config_set"] +destructive_actions = ["uninstall"] +health_states = ["not_installed", "stopped", "starting", "running", "degraded", "failed"] + +[mode.interactive_user_managed] +contract_state = "active" +platforms = ["linux", "macos", "windows"] +supported_profiles = ["interactive_user", "repo_local"] +service_manager_integration = false +uses_absolute_binary_paths = true +requires_explicit_pid_tracking = true +requires_explicit_log_tracking = true +default_instance_cardinality = "single_default_instance" + +[mode.service_host_managed] +contract_state = "defined" +platforms = ["linux", "macos", "windows"] +supported_profiles = ["service_host"] +service_manager_integration = true +uses_absolute_binary_paths = true +default_instance_cardinality = "single_default_instance" + +[paths.interactive_user_managed] +shared_namespace = "shared/runtime-manager" +instance_registry_root_class = "config" +instance_registry_rel = "shared/runtime-manager/instances.toml" +artifact_cache_root_class = "cache" +artifact_cache_rel = "shared/runtime-manager/artifacts" +install_root_class = "data" +install_root_rel = "shared/runtime-manager/installs" +state_root_class = "data" +state_root_rel = "shared/runtime-manager/state" +logs_root_class = "logs" +logs_root_rel = "shared/runtime-manager" +run_root_class = "run" +run_root_rel = "shared/runtime-manager" +secrets_root_class = "secrets" +secrets_namespace_rel = "shared/runtime-manager" + +[instance_metadata] +required_fields = [ + "runtime_id", + "instance_id", + "management_mode", + "install_state", + "binary_path", + "config_path", + "logs_path", + "run_path", + "installed_version", +] +optional_fields = [ + "health_endpoint", + "secret_material_ref", + "last_started_at", + "last_stopped_at", + "notes", +] + +[bootstrap.radrootsd] +runtime_id = "radrootsd" +management_mode = "interactive_user_managed" +default_instance_id = "local" +install_strategy = "archive_unpack" +config_format = "toml" +requires_bootstrap_secret = true +requires_config_bootstrap = true +requires_signer_provider = false +health_surface = "jsonrpc_status" +preferred_cli_binding = true +"#; + + #[test] + fn parse_contract_accepts_expected_schema() { + let contract = parse_contract_str(CONTRACT).expect("parse contract"); + assert_eq!(contract.schema, crate::RUNTIME_MANAGEMENT_SCHEMA); + assert!(contract.mode.contains_key("interactive_user_managed")); + } + + #[test] + fn resolve_shared_paths_uses_interactive_user_roots() { + let contract = parse_contract_str(CONTRACT).expect("parse contract"); + let resolver = RadrootsPathResolver::new( + RadrootsPlatform::Linux, + RadrootsHostEnvironment { + home_dir: Some(PathBuf::from("/home/treesap")), + ..RadrootsHostEnvironment::default() + }, + ); + + let paths = resolve_shared_paths( + &contract, + &resolver, + RadrootsPathProfile::InteractiveUser, + &RadrootsPathOverrides::default(), + "interactive_user_managed", + ) + .expect("resolve shared manager paths"); + + assert_eq!( + paths.instance_registry_path, + PathBuf::from("/home/treesap/.radroots/config/shared/runtime-manager/instances.toml") + ); + assert_eq!( + paths.install_root, + PathBuf::from("/home/treesap/.radroots/data/shared/runtime-manager/installs") + ); + assert_eq!( + paths.logs_root, + PathBuf::from("/home/treesap/.radroots/logs/shared/runtime-manager") + ); + } + + #[test] + fn resolve_repo_local_paths_uses_explicit_base_root() { + let contract = parse_contract_str(CONTRACT).expect("parse contract"); + let resolver = + RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()); + + let paths = resolve_shared_paths( + &contract, + &resolver, + RadrootsPathProfile::RepoLocal, + &RadrootsPathOverrides::repo_local("/repo/.local/radroots"), + "interactive_user_managed", + ) + .expect("resolve repo local manager paths"); + + assert_eq!( + paths.state_root, + PathBuf::from("/repo/.local/radroots/data/shared/runtime-manager/state") + ); + } + + #[test] + fn resolve_instance_paths_builds_per_runtime_layout() { + let contract = parse_contract_str(CONTRACT).expect("parse contract"); + let resolver = RadrootsPathResolver::new( + RadrootsPlatform::Macos, + RadrootsHostEnvironment { + home_dir: Some(PathBuf::from("/Users/treesap")), + ..RadrootsHostEnvironment::default() + }, + ); + let shared = resolve_shared_paths( + &contract, + &resolver, + RadrootsPathProfile::InteractiveUser, + &RadrootsPathOverrides::default(), + "interactive_user_managed", + ) + .expect("resolve shared manager paths"); + + let instance_paths = resolve_instance_paths(&shared, "radrootsd", "local"); + assert_eq!( + instance_paths.install_dir, + PathBuf::from( + "/Users/treesap/.radroots/data/shared/runtime-manager/installs/radrootsd/local" + ) + ); + assert_eq!( + instance_paths.pid_file_path, + PathBuf::from( + "/Users/treesap/.radroots/run/shared/runtime-manager/radrootsd/local/runtime.pid" + ) + ); + assert_eq!( + instance_paths.metadata_path, + PathBuf::from( + "/Users/treesap/.radroots/data/shared/runtime-manager/state/radrootsd/local/instance.toml" + ) + ); + } + + #[test] + fn registry_round_trip_persists_and_reloads_instances() { + let dir = tempdir().expect("tempdir"); + let registry_path = dir.path().join("instances.toml"); + let mut registry = crate::ManagedRuntimeInstanceRegistry::default(); + upsert_instance( + &mut registry, + ManagedRuntimeInstanceRecord { + runtime_id: "radrootsd".to_string(), + instance_id: "local".to_string(), + management_mode: "interactive_user_managed".to_string(), + install_state: ManagedRuntimeInstallState::Configured, + binary_path: PathBuf::from("/tmp/radrootsd"), + config_path: PathBuf::from("/tmp/config.toml"), + logs_path: PathBuf::from("/tmp/logs"), + run_path: PathBuf::from("/tmp/run"), + installed_version: "0.1.0-alpha.1".to_string(), + health_endpoint: Some("jsonrpc_status".to_string()), + secret_material_ref: Some( + "shared/runtime-manager/radrootsd/local/token".to_string(), + ), + last_started_at: Some("2026-04-08T00:00:00Z".to_string()), + last_stopped_at: None, + notes: Some("test".to_string()), + }, + ); + + save_registry(&registry_path, &registry).expect("save registry"); + let reloaded = load_registry(&registry_path).expect("load registry"); + let record = instance(&reloaded, "radrootsd", "local").expect("instance record"); + assert_eq!(record.install_state, ManagedRuntimeInstallState::Configured); + assert_eq!(record.health_endpoint.as_deref(), Some("jsonrpc_status")); + } + + #[test] + fn bootstrap_lookup_returns_radrootsd_contract() { + let contract = parse_contract_str(CONTRACT).expect("parse contract"); + let bootstrap = bootstrap_runtime(&contract, "radrootsd").expect("bootstrap contract"); + assert_eq!(bootstrap.default_instance_id, "local"); + assert_eq!(bootstrap.health_surface, "jsonrpc_status"); + assert!(bootstrap.preferred_cli_binding); + } + + #[test] + fn install_and_health_state_surface_is_typed() { + assert_eq!( + ManagedRuntimeInstallState::Installed, + ManagedRuntimeInstallState::Installed + ); + assert_eq!( + ManagedRuntimeHealthState::Degraded, + ManagedRuntimeHealthState::Degraded + ); + } +} diff --git a/crates/runtime-manager/src/model.rs b/crates/runtime-manager/src/model.rs @@ -0,0 +1,162 @@ +use std::collections::BTreeMap; +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +pub struct RadrootsRuntimeManagementContract { + pub schema: String, + pub schema_version: u32, + pub owner_doc: String, + pub runtime_registry: String, + pub distribution_contract: String, + pub capabilities_contract: String, + pub defaults: ManagementDefaults, + pub management_clients: RuntimeGroups, + pub managed_runtime_targets: RuntimeGroups, + pub lifecycle: LifecycleContract, + pub mode: BTreeMap<String, ManagementModeContract>, + pub paths: BTreeMap<String, ManagementPathContract>, + pub instance_metadata: InstanceMetadataContract, + pub bootstrap: BTreeMap<String, BootstrapRuntimeContract>, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +pub struct ManagementDefaults { + pub instance_cardinality: String, + pub managed_runtime_lookup: String, + pub explicit_runtime_endpoint_overrides_precede_managed_instance_binding: bool, + pub global_path_mutation_forbidden: bool, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Default)] +pub struct RuntimeGroups { + #[serde(default)] + pub active: Vec<String>, + #[serde(default)] + pub defined: Vec<String>, + #[serde(default)] + pub bootstrap_only: Vec<String>, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +pub struct LifecycleContract { + #[serde(default)] + pub actions: Vec<String>, + #[serde(default)] + pub destructive_actions: Vec<String>, + #[serde(default)] + pub health_states: Vec<String>, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +pub struct ManagementModeContract { + pub contract_state: String, + #[serde(default)] + pub platforms: Vec<String>, + #[serde(default)] + pub supported_profiles: Vec<String>, + pub service_manager_integration: bool, + pub uses_absolute_binary_paths: bool, + pub default_instance_cardinality: String, + pub requires_explicit_pid_tracking: Option<bool>, + pub requires_explicit_log_tracking: Option<bool>, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +pub struct ManagementPathContract { + pub shared_namespace: String, + pub instance_registry_root_class: String, + pub instance_registry_rel: String, + pub artifact_cache_root_class: String, + pub artifact_cache_rel: String, + pub install_root_class: String, + pub install_root_rel: String, + pub state_root_class: String, + pub state_root_rel: String, + pub logs_root_class: String, + pub logs_root_rel: String, + pub run_root_class: String, + pub run_root_rel: String, + pub secrets_root_class: String, + pub secrets_namespace_rel: String, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +pub struct InstanceMetadataContract { + #[serde(default)] + pub required_fields: Vec<String>, + #[serde(default)] + pub optional_fields: Vec<String>, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +pub struct BootstrapRuntimeContract { + pub runtime_id: String, + pub management_mode: String, + pub default_instance_id: String, + pub install_strategy: String, + pub config_format: String, + pub requires_bootstrap_secret: bool, + pub requires_config_bootstrap: bool, + pub requires_signer_provider: bool, + pub health_surface: String, + pub preferred_cli_binding: bool, + pub notes: Option<String>, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ManagedRuntimeInstallState { + NotInstalled, + Installed, + Configured, + Failed, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ManagedRuntimeHealthState { + NotInstalled, + Stopped, + Starting, + Running, + Degraded, + Failed, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ManagedRuntimeInstanceRecord { + pub runtime_id: String, + pub instance_id: String, + pub management_mode: String, + pub install_state: ManagedRuntimeInstallState, + pub binary_path: PathBuf, + pub config_path: PathBuf, + pub logs_path: PathBuf, + pub run_path: PathBuf, + pub installed_version: String, + pub health_endpoint: Option<String>, + pub secret_material_ref: Option<String>, + pub last_started_at: Option<String>, + pub last_stopped_at: Option<String>, + pub notes: Option<String>, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ManagedRuntimeInstanceRegistry { + pub schema: String, + pub schema_version: u32, + #[serde(default)] + pub instances: Vec<ManagedRuntimeInstanceRecord>, +} + +impl Default for ManagedRuntimeInstanceRegistry { + fn default() -> Self { + Self { + schema: "radroots-runtime-instance-registry".to_string(), + schema_version: 1, + instances: Vec::new(), + } + } +} diff --git a/crates/runtime-manager/src/paths.rs b/crates/runtime-manager/src/paths.rs @@ -0,0 +1,157 @@ +use std::path::PathBuf; + +use radroots_runtime_paths::{ + RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, RadrootsPaths, +}; + +use crate::error::RadrootsRuntimeManagerError; +use crate::model::RadrootsRuntimeManagementContract; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ManagedRuntimeSharedPaths { + pub instance_registry_path: PathBuf, + pub artifact_cache_dir: PathBuf, + pub install_root: PathBuf, + pub state_root: PathBuf, + pub logs_root: PathBuf, + pub run_root: PathBuf, + pub secrets_root: PathBuf, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ManagedRuntimeInstancePaths { + pub install_dir: PathBuf, + pub state_dir: PathBuf, + pub logs_dir: PathBuf, + pub run_dir: PathBuf, + pub secrets_dir: PathBuf, + pub pid_file_path: PathBuf, + pub stdout_log_path: PathBuf, + pub stderr_log_path: PathBuf, + pub metadata_path: PathBuf, +} + +pub fn resolve_shared_paths( + contract: &RadrootsRuntimeManagementContract, + resolver: &RadrootsPathResolver, + profile: RadrootsPathProfile, + overrides: &RadrootsPathOverrides, + mode_id: &str, +) -> Result<ManagedRuntimeSharedPaths, RadrootsRuntimeManagerError> { + ensure_profile_supported(contract, mode_id, profile)?; + let roots = resolver.resolve(profile, overrides)?; + let path_spec = contract + .paths + .get(mode_id) + .ok_or_else(|| RadrootsRuntimeManagerError::MissingPathSpec(mode_id.to_string()))?; + + Ok(ManagedRuntimeSharedPaths { + instance_registry_path: root_class_path( + &roots, + &path_spec.instance_registry_root_class, + &path_spec.instance_registry_rel, + )?, + artifact_cache_dir: root_class_path( + &roots, + &path_spec.artifact_cache_root_class, + &path_spec.artifact_cache_rel, + )?, + install_root: root_class_path( + &roots, + &path_spec.install_root_class, + &path_spec.install_root_rel, + )?, + state_root: root_class_path( + &roots, + &path_spec.state_root_class, + &path_spec.state_root_rel, + )?, + logs_root: root_class_path(&roots, &path_spec.logs_root_class, &path_spec.logs_root_rel)?, + run_root: root_class_path(&roots, &path_spec.run_root_class, &path_spec.run_root_rel)?, + secrets_root: root_class_path( + &roots, + &path_spec.secrets_root_class, + &path_spec.secrets_namespace_rel, + )?, + }) +} + +pub fn resolve_instance_paths( + shared: &ManagedRuntimeSharedPaths, + runtime_id: &str, + instance_id: &str, +) -> ManagedRuntimeInstancePaths { + let suffix = PathBuf::from(runtime_id).join(instance_id); + let install_dir = shared.install_root.join(&suffix); + let state_dir = shared.state_root.join(&suffix); + let logs_dir = shared.logs_root.join(&suffix); + let run_dir = shared.run_root.join(&suffix); + let secrets_dir = shared.secrets_root.join(&suffix); + + ManagedRuntimeInstancePaths { + install_dir, + state_dir: state_dir.clone(), + logs_dir: logs_dir.clone(), + run_dir: run_dir.clone(), + secrets_dir, + pid_file_path: run_dir.join("runtime.pid"), + stdout_log_path: logs_dir.join("stdout.log"), + stderr_log_path: logs_dir.join("stderr.log"), + metadata_path: state_dir.join("instance.toml"), + } +} + +pub fn bootstrap_runtime<'a>( + contract: &'a RadrootsRuntimeManagementContract, + runtime_id: &str, +) -> Result<&'a crate::model::BootstrapRuntimeContract, RadrootsRuntimeManagerError> { + contract + .bootstrap + .get(runtime_id) + .ok_or_else(|| RadrootsRuntimeManagerError::UnknownBootstrapRuntime(runtime_id.to_string())) +} + +fn ensure_profile_supported( + contract: &RadrootsRuntimeManagementContract, + mode_id: &str, + profile: RadrootsPathProfile, +) -> Result<(), RadrootsRuntimeManagerError> { + let mode = contract + .mode + .get(mode_id) + .ok_or_else(|| RadrootsRuntimeManagerError::UnknownManagementMode(mode_id.to_string()))?; + let profile_id = profile.to_string(); + if mode + .supported_profiles + .iter() + .any(|entry| entry == &profile_id) + { + Ok(()) + } else { + Err(RadrootsRuntimeManagerError::UnsupportedProfile { + mode_id: mode_id.to_string(), + profile: profile_id, + }) + } +} + +fn root_class_path( + roots: &RadrootsPaths, + root_class: &str, + rel: &str, +) -> Result<PathBuf, RadrootsRuntimeManagerError> { + let base = match root_class { + "config" => &roots.config, + "data" => &roots.data, + "cache" => &roots.cache, + "logs" => &roots.logs, + "run" => &roots.run, + "secrets" => &roots.secrets, + other => { + return Err(RadrootsRuntimeManagerError::UnknownRootClass( + other.to_string(), + )); + } + }; + Ok(base.join(rel)) +} diff --git a/crates/runtime-manager/src/registry.rs b/crates/runtime-manager/src/registry.rs @@ -0,0 +1,81 @@ +use std::fs; +use std::path::Path; + +use crate::error::RadrootsRuntimeManagerError; +use crate::model::{ManagedRuntimeInstanceRecord, ManagedRuntimeInstanceRegistry}; + +pub fn load_registry( + path: impl AsRef<Path>, +) -> Result<ManagedRuntimeInstanceRegistry, RadrootsRuntimeManagerError> { + let path = path.as_ref(); + let raw = match fs::read_to_string(path) { + Ok(raw) => raw, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + return Ok(ManagedRuntimeInstanceRegistry::default()); + } + Err(source) => { + return Err(RadrootsRuntimeManagerError::ReadRegistry { + path: path.to_path_buf(), + source, + }); + } + }; + + toml::from_str::<ManagedRuntimeInstanceRegistry>(&raw).map_err(|source| { + RadrootsRuntimeManagerError::ParseRegistry { + path: path.to_path_buf(), + details: source.to_string(), + } + }) +} + +pub fn save_registry( + path: impl AsRef<Path>, + registry: &ManagedRuntimeInstanceRegistry, +) -> Result<(), RadrootsRuntimeManagerError> { + let path = path.as_ref(); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).map_err(|source| { + RadrootsRuntimeManagerError::CreateRegistryParent { + path: parent.to_path_buf(), + source, + } + })?; + } + + let raw = toml::to_string_pretty(registry) + .map_err(|err| RadrootsRuntimeManagerError::SerializeRegistry(err.to_string()))?; + fs::write(path, raw).map_err(|source| RadrootsRuntimeManagerError::WriteRegistry { + path: path.to_path_buf(), + source, + }) +} + +pub fn upsert_instance( + registry: &mut ManagedRuntimeInstanceRegistry, + record: ManagedRuntimeInstanceRecord, +) { + if let Some(existing) = registry.instances.iter_mut().find(|existing| { + existing.runtime_id == record.runtime_id && existing.instance_id == record.instance_id + }) { + *existing = record; + } else { + registry.instances.push(record); + registry.instances.sort_by(|left, right| { + left.runtime_id + .cmp(&right.runtime_id) + .then_with(|| left.instance_id.cmp(&right.instance_id)) + }); + } +} + +pub fn instance<'a>( + registry: &'a ManagedRuntimeInstanceRegistry, + runtime_id: &str, + instance_id: &str, +) -> Option<&'a ManagedRuntimeInstanceRecord> { + registry + .instances + .iter() + .find(|record| record.runtime_id == runtime_id && record.instance_id == instance_id) +}