commit 1db1a0d7047463c39ceb7b6cc4a551d3ed1abd7b
parent 301daed1f3e3327727e475723cfe58dda347a7ac
Author: triesap <tyson@radroots.org>
Date: Tue, 11 Aug 2026 08:34:52 +0000
runtime-manager: adopt service instance paths
- bind manager targets and lifecycle paths to sealed runtime contexts
- seal typed registries and reject cross-profile or cross-root reuse
- remove credential authority and contain config, install, and cleanup operations
- verify focused, workspace, partitioned Studio, Clippy, and Rustdoc gates
Diffstat:
9 files changed, 1626 insertions(+), 2075 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
@@ -157,6 +157,16 @@ Before editing code:
the owning store, keep live mutable state daemon-owned, and route live-state
mutations from local tools through the typed, permissioned Unix-socket
local-admin boundary.
+- Runtime-management flows consume a sealed `RuntimeContext` for every service
+ instance. They must not reconstruct service paths from raw identifiers,
+ ambient selectors, or manager-owned roots, and registries must not persist
+ duplicate config, state, logs, run, secrets, or binary paths. Manager-owned
+ install and process-tracking artifacts remain separate; uninstall and
+ cleanup must never recursively delete canonical service state or secrets.
+ The manager has no credential read/write authority, executable artifact
+ names are validated single path components, and ordinary manager errors and
+ `Debug` output must not expose filesystem paths, file contents, or raw
+ dependency-owned causes.
- Library code must not initialize a tracing subscriber, parse a process CLI,
read service configuration from environment variables, install signal
handlers, create a Tokio runtime, call `process::exit`, or spawn arbitrary
diff --git a/crates/runtime_manager/README b/crates/runtime_manager/README
@@ -6,13 +6,18 @@ runtime lifecycle and inspection helpers for the `radroots` core libraries.
## Overview
* schema-checked management contract parsing and shared management constants;
- * shared and per-instance path resolution layered on `radroots_runtime_paths`;
- * registry load, save, lookup, and upsert helpers for managed runtime
- instances;
- * lifecycle helpers for archive install, process start and stop, managed-file
- writes, and secret-file handling;
- * managed runtime inspection and target-resolution helpers for status, logs,
- config, and action availability.
+ * manager-owned shared and per-instance tracking paths kept separate from
+ canonical service-instance paths supplied by a sealed `RuntimeContext`;
+ * typed, deterministic registry load, save, lookup, and upsert helpers that
+ persist service/instance identities without duplicating service paths or
+ secrets;
+ * lifecycle helpers for contained archive install, process start and stop,
+ and context-bound non-secret configuration writes; the manager has no
+ credential read/write authority;
+ * context-bound runtime inspection and target-resolution helpers for status,
+ logs, config, and action availability; and
+ * cleanup behavior limited to manager-owned install and tracking artifacts,
+ preserving canonical service state and secrets.
## Copyright
diff --git a/crates/runtime_manager/src/error.rs b/crates/runtime_manager/src/error.rs
@@ -1,126 +1,68 @@
-use std::path::PathBuf;
-
-use radroots_runtime_paths::{RadrootsRuntimePathSelectionError, RadrootsRuntimePathsError};
use thiserror::Error;
-#[derive(Debug, Error)]
+/// Stable, path-free runtime-management failures.
+///
+/// Filesystem paths, file contents, and dependency-owned causes are retained
+/// only inside the operation that handles them. Ordinary `Display`, `Debug`,
+/// and error-chain traversal therefore cannot disclose them.
+#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)]
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("create directory {path}: {source}")]
- CreateDirectory {
- path: PathBuf,
- source: std::io::Error,
- },
- #[error("copy runtime binary from {from} to {to}: {source}")]
- CopyBinary {
- from: PathBuf,
- to: PathBuf,
- source: std::io::Error,
- },
- #[error("serialize runtime instance metadata: {0}")]
- SerializeInstanceMetadata(String),
- #[error("write runtime instance metadata {path}: {source}")]
- WriteInstanceMetadata {
- path: PathBuf,
- source: std::io::Error,
- },
- #[error("write managed file {path}: {source}")]
- WriteManagedFile {
- path: PathBuf,
- source: std::io::Error,
- },
- #[error("read managed file {path}: {source}")]
- ReadManagedFile {
- path: PathBuf,
- source: std::io::Error,
- },
- #[error("open runtime log file {path}: {source}")]
- OpenLogFile {
- path: PathBuf,
- source: std::io::Error,
- },
- #[error("spawn managed runtime process {binary_path}: {source}")]
- SpawnProcess {
- binary_path: PathBuf,
- source: std::io::Error,
- },
- #[error("write pid file {path}: {source}")]
- WritePidFile {
- path: PathBuf,
- source: std::io::Error,
- },
- #[error("read pid file {path}: {source}")]
- ReadPidFile {
- path: PathBuf,
- source: std::io::Error,
- },
- #[error("parse pid file {path}: invalid contents `{contents}`")]
- ParsePidFile { path: PathBuf, contents: String },
- #[error("remove managed path {path}: {source}")]
- RemovePath {
- path: PathBuf,
- source: std::io::Error,
- },
- #[error("set file permissions for {path}: {source}")]
- SetPermissions {
- path: PathBuf,
- source: std::io::Error,
- },
- #[error("signal pid {pid} with {signal}: {source}")]
- ExecuteProcessSignal {
- pid: u32,
- signal: String,
- source: std::io::Error,
- },
- #[error("stop pid {pid}: {details}")]
- StopProcess { pid: u32, details: String },
- #[error("unsupported archive format `{archive_format}` for {archive_path}")]
- UnsupportedArchiveFormat {
- archive_path: PathBuf,
- archive_format: String,
- },
- #[error("unpack archive {archive_path}: {source}")]
- UnpackArchive {
- archive_path: PathBuf,
- source: std::io::Error,
- },
- #[error(transparent)]
- RuntimePaths(#[from] RadrootsRuntimePathsError),
- #[error(transparent)]
- RuntimePathSelection(#[from] RadrootsRuntimePathSelectionError),
+ #[error("parse runtime management contract failed")]
+ Parse,
+ #[error("runtime management schema is unsupported")]
+ UnexpectedSchema,
+ #[error("management mode does not support the selected profile")]
+ UnsupportedProfile,
+ #[error("runtime has no bootstrap entry in runtime management contract")]
+ UnknownBootstrapRuntime,
+ #[error("runtime context does not share the manager path scope")]
+ RuntimeContextMismatch,
+ #[error("read runtime instance registry failed: {kind}")]
+ ReadRegistry { kind: std::io::ErrorKind },
+ #[error("parse runtime instance registry failed")]
+ ParseRegistry,
+ #[error("runtime instance registry schema is unsupported")]
+ UnexpectedRegistrySchema,
+ #[error("runtime instance registry version is unsupported")]
+ UnexpectedRegistryVersion,
+ #[error("runtime instance registry contains a duplicate service instance")]
+ DuplicateRegistryInstance,
+ #[error("serialize runtime instance registry failed")]
+ SerializeRegistry,
+ #[error("create runtime instance registry parent failed: {kind}")]
+ CreateRegistryParent { kind: std::io::ErrorKind },
+ #[error("write runtime instance registry failed: {kind}")]
+ WriteRegistry { kind: std::io::ErrorKind },
+ #[error("create managed runtime directory failed: {kind}")]
+ CreateDirectory { kind: std::io::ErrorKind },
+ #[error("copy managed runtime binary failed: {kind}")]
+ CopyBinary { kind: std::io::ErrorKind },
+ #[error("write managed runtime config failed: {kind}")]
+ WriteManagedConfig { kind: std::io::ErrorKind },
+ #[error("read managed runtime file failed: {kind}")]
+ ReadManagedFile { kind: std::io::ErrorKind },
+ #[error("open managed runtime log failed: {kind}")]
+ OpenLogFile { kind: std::io::ErrorKind },
+ #[error("spawn managed runtime process failed: {kind}")]
+ SpawnProcess { kind: std::io::ErrorKind },
+ #[error("write managed runtime pid failed: {kind}")]
+ WritePidFile { kind: std::io::ErrorKind },
+ #[error("read managed runtime pid failed: {kind}")]
+ ReadPidFile { kind: std::io::ErrorKind },
+ #[error("managed runtime pid is malformed")]
+ ParsePidFile,
+ #[error("remove manager-owned runtime path failed: {kind}")]
+ RemovePath { kind: std::io::ErrorKind },
+ #[error("set managed runtime file permissions failed: {kind}")]
+ SetPermissions { kind: std::io::ErrorKind },
+ #[error("signal managed runtime process failed: {kind}")]
+ ExecuteProcessSignal { kind: std::io::ErrorKind },
+ #[error("managed runtime process did not stop")]
+ StopProcess,
+ #[error("managed runtime archive format is unsupported")]
+ UnsupportedArchiveFormat,
+ #[error("unpack managed runtime archive failed: {kind}")]
+ UnpackArchive { kind: std::io::ErrorKind },
+ #[error("managed runtime artifact name is invalid")]
+ InvalidArtifactName,
}
diff --git a/crates/runtime_manager/src/lib.rs b/crates/runtime_manager/src/lib.rs
@@ -9,9 +9,8 @@ pub mod registry;
pub use error::RadrootsRuntimeManagerError;
pub use lifecycle::{
- ensure_instance_layout, extract_binary_archive, install_binary, process_running,
- read_secret_file, remove_instance_artifacts, start_process, stop_process,
- write_instance_metadata, write_managed_file, write_secret_file,
+ ManagedRuntimeArtifactName, ensure_instance_layout, extract_binary_archive, install_binary,
+ process_running, remove_instance_artifacts, start_process, stop_process, write_instance_config,
};
pub use managed::{
ManagedRuntimeActionInspection, ManagedRuntimeConfigInspection, ManagedRuntimeContext,
@@ -19,19 +18,17 @@ pub use managed::{
ManagedRuntimeLifecycleAction, ManagedRuntimeLogsInspection, ManagedRuntimeStatusInspection,
ManagedRuntimeTarget, active_management_mode_for_profile, inspect_runtime_action,
inspect_runtime_config, inspect_runtime_logs, inspect_runtime_status, load_management_context,
- load_management_context_with_selection, resolve_runtime_target, runtime_group,
+ resolve_runtime_target, runtime_group,
};
pub use model::{
BootstrapRuntimeContract, LifecycleContract, ManagedRuntimeHealthState,
ManagedRuntimeInstallState, ManagedRuntimeInstanceRecord, ManagedRuntimeInstanceRegistry,
ManagementDefaults, ManagementModeContract, ManagementPathContract,
+ RUNTIME_INSTANCE_REGISTRY_SCHEMA, RUNTIME_INSTANCE_REGISTRY_VERSION,
RadrootsRuntimeManagementContract, RuntimeGroups,
};
-pub use paths::{
- ManagedRuntimeInstancePaths, ManagedRuntimeSharedPaths, bootstrap_runtime,
- resolve_instance_paths, resolve_shared_paths,
-};
-pub use registry::{instance, load_registry, remove_instance, save_registry, upsert_instance};
+pub use paths::{ManagedRuntimeInstancePaths, ManagedRuntimeSharedPaths, bootstrap_runtime};
+pub use registry::{instance, load_registry, save_registry};
pub const RUNTIME_MANAGEMENT_SCHEMA: &str = "radroots-runtime-management";
@@ -39,325 +36,139 @@ 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()))?;
+ .map_err(|_| RadrootsRuntimeManagerError::Parse)?;
if contract.schema != RUNTIME_MANAGEMENT_SCHEMA {
- return Err(RadrootsRuntimeManagerError::UnexpectedSchema {
- expected: RUNTIME_MANAGEMENT_SCHEMA,
- found: contract.schema.clone(),
- });
+ return Err(RadrootsRuntimeManagerError::UnexpectedSchema);
}
Ok(contract)
}
#[cfg(test)]
mod tests {
- use std::path::PathBuf;
-
- use radroots_runtime_paths::{
- RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver,
- RadrootsPlatform,
- };
- use tempfile::tempdir;
+ use std::error::Error as _;
- use crate::{
- ManagedRuntimeHealthState, ManagedRuntimeInstallState, ManagedRuntimeInstanceRecord,
- bootstrap_runtime, instance, load_registry, parse_contract_str, resolve_instance_paths,
- resolve_shared_paths, save_registry, upsert_instance,
- };
-
- fn assert_error_contains(err: &crate::RadrootsRuntimeManagerError, parts: &[&str]) {
- let rendered = err.to_string();
- for part in parts {
- assert!(
- rendered.contains(part),
- "expected `{rendered}` to contain `{part}`"
- );
- }
- }
+ use super::{RUNTIME_MANAGEMENT_SCHEMA, parse_contract_str};
const CONTRACT: &str = r#"
schema = "radroots-runtime-management"
schema_version = 1
-owner_doc = "docs/execution/rcl/radroots-modular-runtime-management-bootstrap-rcl.md"
-runtime_registry = "registry.toml"
-distribution_contract = "distribution.toml"
-capabilities_contract = "capabilities.toml"
+owner_doc = "owner"
+runtime_registry = "registry"
+distribution_contract = "distribution"
+capabilities_contract = "capabilities"
[defaults]
-instance_cardinality = "single_default_instance"
-managed_runtime_lookup = "shared_instance_registry"
+instance_cardinality = "multiple"
+managed_runtime_lookup = "typed_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"]
+actions = ["status"]
+health_states = ["running"]
-[mode.interactive_user_managed]
+[mode.interactive]
contract_state = "active"
-platforms = ["linux", "macos", "windows"]
-supported_profiles = ["interactive_user", "repo_local"]
+platforms = ["linux"]
+supported_profiles = ["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"
+default_instance_cardinality = "multiple"
-[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"
+[paths.interactive]
+shared_namespace = "obsolete"
instance_registry_root_class = "config"
-instance_registry_rel = "shared/runtime-manager/instances.toml"
+instance_registry_rel = "obsolete"
artifact_cache_root_class = "cache"
-artifact_cache_rel = "shared/runtime-manager/artifacts"
+artifact_cache_rel = "obsolete"
install_root_class = "data"
-install_root_rel = "shared/runtime-manager/installs"
+install_root_rel = "obsolete"
state_root_class = "data"
-state_root_rel = "shared/runtime-manager/state"
+state_root_rel = "obsolete"
logs_root_class = "logs"
-logs_root_rel = "shared/runtime-manager"
+logs_root_rel = "obsolete"
run_root_class = "run"
-run_root_rel = "shared/runtime-manager"
+run_root_rel = "obsolete"
secrets_root_class = "secrets"
-secrets_namespace_rel = "shared/runtime-manager"
+secrets_namespace_rel = "obsolete"
[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",
-]
+required_fields = ["service_id", "instance_id"]
-[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
+[bootstrap]
"#;
#[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 parse_contract_reports_invalid_toml() {
- let err = parse_contract_str("schema = [").expect_err("invalid toml should fail");
- assert_error_contains(&err, &["parse runtime management contract"]);
- }
+ fn contract_parser_accepts_only_the_expected_schema() {
+ let contract = parse_contract_str(CONTRACT).expect("contract");
+ assert_eq!(contract.schema, RUNTIME_MANAGEMENT_SCHEMA);
- #[test]
- fn parse_contract_rejects_unexpected_schema() {
- let err = parse_contract_str(&CONTRACT.replace(
+ let wrong = CONTRACT.replace(
"schema = \"radroots-runtime-management\"",
- "schema = \"wrong-schema\"",
- ))
- .expect_err("unexpected schema should fail");
- assert_error_contains(&err, &["wrong-schema", crate::RUNTIME_MANAGEMENT_SCHEMA]);
- }
-
- #[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")),
- xdg_runtime_dir: Some(PathBuf::from("/run/user/1000")),
- ..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/.config/radroots/shared/runtime-manager/instances.toml")
- );
- assert_eq!(
- paths.install_root,
- PathBuf::from("/home/treesap/.local/share/radroots/shared/runtime-manager/installs")
- );
- assert_eq!(
- paths.artifact_cache_dir,
- PathBuf::from("/home/treesap/.cache/radroots/shared/runtime-manager/artifacts")
- );
- assert_eq!(
- paths.state_root,
- PathBuf::from("/home/treesap/.local/share/radroots/shared/runtime-manager/state")
- );
- assert_eq!(
- paths.logs_root,
- PathBuf::from("/home/treesap/.local/state/radroots/logs/shared/runtime-manager")
- );
- assert_eq!(
- paths.run_root,
- PathBuf::from("/run/user/1000/radroots/shared/runtime-manager")
- );
- assert_eq!(
- paths.secrets_root,
- PathBuf::from("/home/treesap/.config/radroots/secrets/shared/runtime-manager")
+ "schema = \"wrong\"",
);
+ assert!(parse_contract_str(&wrong).is_err());
+ assert!(parse_contract_str("schema = [").is_err());
}
#[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/Library/Application Support/Radroots/data/shared/runtime-manager/installs/radrootsd/local"
- )
- );
- assert_eq!(
- instance_paths.pid_file_path,
- PathBuf::from(
- "/Users/treesap/Library/Application Support/Radroots/run/shared/runtime-manager/radrootsd/local/runtime.pid"
- )
- );
- assert_eq!(
- instance_paths.metadata_path,
- PathBuf::from(
- "/Users/treesap/Library/Application Support/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: "1.0.0-alpha.1".to_string(),
- health_endpoint: Some("jsonrpc_status".to_string()),
- secret_material_ref: Some(
- "shared/runtime-manager/radrootsd/local/token".to_string(),
+ fn contract_errors_redact_raw_schema_values_and_parser_causes() {
+ for (raw, secret) in [
+ (
+ CONTRACT.replace(
+ "schema = \"radroots-runtime-management\"",
+ "schema = \"/sensitive/root/secret-schema\"",
),
- last_started_at: Some("2026-04-08T00:00:00Z".to_string()),
- last_stopped_at: None,
- notes: Some("test".to_string()),
- },
- );
-
- save_registry(®istry_path, ®istry).expect("save registry");
- let reloaded = load_registry(®istry_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);
+ "/sensitive/root/secret-schema",
+ ),
+ (
+ "credential = 'secret-value'\ninvalid = [".to_owned(),
+ "secret-value",
+ ),
+ ] {
+ let err = parse_contract_str(&raw).expect_err("invalid contract");
+ let rendered = format!("{err} {err:?}");
+ assert!(!rendered.contains(secret));
+ assert!(err.source().is_none());
+ }
}
#[test]
- fn install_and_health_state_surface_is_typed() {
- assert_eq!(
- ManagedRuntimeInstallState::Installed,
- ManagedRuntimeInstallState::Installed
- );
- assert_eq!(
- ManagedRuntimeHealthState::Degraded,
- ManagedRuntimeHealthState::Degraded
- );
+ fn manager_source_no_longer_consumes_legacy_path_selection_or_raw_identity_joins() {
+ let sources = [
+ include_str!("error.rs"),
+ include_str!("lifecycle.rs"),
+ include_str!("managed.rs"),
+ include_str!("model.rs"),
+ include_str!("paths.rs"),
+ include_str!("registry.rs"),
+ ];
+ for forbidden in [
+ "RadrootsRuntimePathSelection",
+ "load_management_context_with_selection",
+ "PathBuf::from(runtime_id).join(instance_id)",
+ "record.config_path",
+ "record.logs_path",
+ "record.run_path",
+ "workers/rhi",
+ "pub fn read_secret_file",
+ "pub fn write_secret_file",
+ "pub fn write_managed_file",
+ "pub fn registry_mut",
+ "pub fn upsert_instance",
+ "pub fn resolve_shared_paths",
+ "pub fn resolve_instance_paths",
+ ] {
+ assert!(
+ sources.iter().all(|source| !source.contains(forbidden)),
+ "runtime manager retained forbidden source `{forbidden}`"
+ );
+ }
}
}
diff --git a/crates/runtime_manager/src/lifecycle.rs b/crates/runtime_manager/src/lifecycle.rs
@@ -7,23 +7,46 @@ use std::time::Duration;
use flate2::read::GzDecoder;
use crate::error::RadrootsRuntimeManagerError;
-use crate::model::ManagedRuntimeInstanceRecord;
use crate::paths::ManagedRuntimeInstancePaths;
+/// A validated single-component manager-owned executable artifact name.
+#[derive(Clone, PartialEq, Eq)]
+pub struct ManagedRuntimeArtifactName(String);
+
+impl ManagedRuntimeArtifactName {
+ pub fn new(value: &str) -> Result<Self, RadrootsRuntimeManagerError> {
+ if value.is_empty()
+ || value.len() > 128
+ || !value.as_bytes()[0].is_ascii_alphanumeric()
+ || !value
+ .bytes()
+ .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
+ || Path::new(value).components().count() != 1
+ {
+ return Err(RadrootsRuntimeManagerError::InvalidArtifactName);
+ }
+ Ok(Self(value.to_owned()))
+ }
+
+ #[must_use]
+ pub fn as_str(&self) -> &str {
+ &self.0
+ }
+}
+
+impl core::fmt::Debug for ManagedRuntimeArtifactName {
+ fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ formatter.write_str("ManagedRuntimeArtifactName([redacted])")
+ }
+}
+
pub fn ensure_instance_layout(
paths: &ManagedRuntimeInstancePaths,
) -> Result<(), RadrootsRuntimeManagerError> {
- for path in [
- &paths.install_dir,
- &paths.state_dir,
- &paths.logs_dir,
- &paths.run_dir,
- &paths.secrets_dir,
- ] {
+ for path in [paths.install_dir(), paths.logs_dir(), paths.run_dir()] {
fs::create_dir_all(path).map_err(|source| {
RadrootsRuntimeManagerError::CreateDirectory {
- path: path.clone(),
- source,
+ kind: source.kind(),
}
})?;
}
@@ -33,7 +56,7 @@ pub fn ensure_instance_layout(
pub fn install_binary(
source_binary_path: impl AsRef<Path>,
paths: &ManagedRuntimeInstancePaths,
- binary_name: &str,
+ binary_name: &ManagedRuntimeArtifactName,
) -> Result<PathBuf, RadrootsRuntimeManagerError> {
install_binary_path(source_binary_path.as_ref(), paths, binary_name)
}
@@ -41,15 +64,13 @@ pub fn install_binary(
fn install_binary_path(
source_binary_path: &Path,
paths: &ManagedRuntimeInstancePaths,
- binary_name: &str,
+ binary_name: &ManagedRuntimeArtifactName,
) -> Result<PathBuf, RadrootsRuntimeManagerError> {
ensure_instance_layout(paths)?;
- let installed_binary_path = paths.install_dir.join(binary_name);
+ let installed_binary_path = paths.install_dir().join(binary_name.as_str());
fs::copy(source_binary_path, &installed_binary_path).map_err(|source| {
RadrootsRuntimeManagerError::CopyBinary {
- from: source_binary_path.to_path_buf(),
- to: installed_binary_path.clone(),
- source,
+ kind: source.kind(),
}
})?;
set_executable_mode(&installed_binary_path)?;
@@ -60,7 +81,7 @@ pub fn extract_binary_archive(
archive_path: impl AsRef<Path>,
archive_format: &str,
paths: &ManagedRuntimeInstancePaths,
- binary_name: &str,
+ binary_name: &ManagedRuntimeArtifactName,
) -> Result<PathBuf, RadrootsRuntimeManagerError> {
extract_binary_archive_path(archive_path.as_ref(), archive_format, paths, binary_name)
}
@@ -69,107 +90,53 @@ fn extract_binary_archive_path(
archive_path: &Path,
archive_format: &str,
paths: &ManagedRuntimeInstancePaths,
- binary_name: &str,
+ binary_name: &ManagedRuntimeArtifactName,
) -> Result<PathBuf, RadrootsRuntimeManagerError> {
- remove_path_if_exists(&paths.install_dir)?;
+ remove_path_if_exists(paths.install_dir())?;
ensure_instance_layout(paths)?;
match archive_format {
- "tar.gz" => unpack_tar_gz_archive(archive_path, &paths.install_dir)?,
- other => {
- return Err(RadrootsRuntimeManagerError::UnsupportedArchiveFormat {
- archive_path: archive_path.to_path_buf(),
- archive_format: other.to_owned(),
- });
- }
+ "tar.gz" => unpack_tar_gz_archive(archive_path, paths.install_dir())?,
+ _ => return Err(RadrootsRuntimeManagerError::UnsupportedArchiveFormat),
}
- let installed_binary_path = paths.install_dir.join(binary_name);
+ let installed_binary_path = paths.install_dir().join(binary_name.as_str());
let resolved_binary_path = if installed_binary_path.is_file() {
installed_binary_path
} else {
- find_binary_with_name(&paths.install_dir, binary_name).ok_or_else(|| {
+ find_binary_with_name(paths.install_dir(), binary_name.as_str()).ok_or(
RadrootsRuntimeManagerError::ReadManagedFile {
- path: paths.install_dir.join(binary_name),
- source: std::io::Error::new(
- std::io::ErrorKind::NotFound,
- format!(
- "archive {} did not produce a `{binary_name}` binary under {}",
- archive_path.display(),
- paths.install_dir.display()
- ),
- ),
- }
- })?
+ kind: std::io::ErrorKind::NotFound,
+ },
+ )?
};
set_executable_mode(&resolved_binary_path)?;
Ok(resolved_binary_path)
}
-pub fn write_instance_metadata(
+pub fn write_instance_config(
paths: &ManagedRuntimeInstancePaths,
- record: &ManagedRuntimeInstanceRecord,
-) -> Result<(), RadrootsRuntimeManagerError> {
- ensure_instance_layout(paths)?;
- let raw = serialize_instance_metadata(record)?;
- fs::write(&paths.metadata_path, raw).map_err(|source| {
- RadrootsRuntimeManagerError::WriteInstanceMetadata {
- path: paths.metadata_path.clone(),
- source,
- }
- })
-}
-
-pub fn write_managed_file(
- path: impl AsRef<Path>,
- contents: &str,
-) -> Result<(), RadrootsRuntimeManagerError> {
- write_managed_file_path(path.as_ref(), contents)
-}
-
-fn write_managed_file_path(path: &Path, contents: &str) -> Result<(), RadrootsRuntimeManagerError> {
- ensure_parent_dir(path)?;
- fs::write(path, contents).map_err(|source| RadrootsRuntimeManagerError::WriteManagedFile {
- path: path.to_path_buf(),
- source,
- })
-}
-
-pub fn write_secret_file(
- path: impl AsRef<Path>,
contents: &str,
) -> Result<(), RadrootsRuntimeManagerError> {
- write_secret_file_path(path.as_ref(), contents)
-}
-
-fn write_secret_file_path(path: &Path, contents: &str) -> Result<(), RadrootsRuntimeManagerError> {
- ensure_parent_dir(path)?;
- fs::write(path, contents).map_err(|source| RadrootsRuntimeManagerError::WriteManagedFile {
- path: path.to_path_buf(),
- source,
- })?;
- set_secret_mode(path)?;
- Ok(())
-}
-
-pub fn read_secret_file(path: impl AsRef<Path>) -> Result<String, RadrootsRuntimeManagerError> {
- read_secret_file_path(path.as_ref())
-}
-
-fn read_secret_file_path(path: &Path) -> Result<String, RadrootsRuntimeManagerError> {
- fs::read_to_string(path).map_err(|source| RadrootsRuntimeManagerError::ReadManagedFile {
- path: path.to_path_buf(),
- source,
+ let path = paths.config_path();
+ ensure_parent_dir(&path)?;
+ fs::write(path, contents).map_err(|source| RadrootsRuntimeManagerError::WriteManagedConfig {
+ kind: source.kind(),
})
}
pub fn start_process(
- binary_path: impl AsRef<Path>,
+ paths: &ManagedRuntimeInstancePaths,
+ binary_name: &ManagedRuntimeArtifactName,
args: &[String],
envs: &[(String, String)],
- paths: &ManagedRuntimeInstancePaths,
) -> Result<u32, RadrootsRuntimeManagerError> {
- start_process_path(binary_path.as_ref(), args, envs, paths)
+ start_process_path(
+ &paths.install_dir().join(binary_name.as_str()),
+ args,
+ envs,
+ paths,
+ )
}
fn start_process_path(
@@ -179,8 +146,8 @@ fn start_process_path(
paths: &ManagedRuntimeInstancePaths,
) -> Result<u32, RadrootsRuntimeManagerError> {
ensure_instance_layout(paths)?;
- let stdout = open_log_file(&paths.stdout_log_path)?;
- let stderr = open_log_file(&paths.stderr_log_path)?;
+ let stdout = open_log_file(paths.stdout_log_path())?;
+ let stderr = open_log_file(paths.stderr_log_path())?;
let child = Command::new(binary_path)
.args(args)
.envs(envs.iter().map(|(key, value)| (key, value)))
@@ -189,14 +156,12 @@ fn start_process_path(
.stderr(Stdio::from(stderr))
.spawn()
.map_err(|source| RadrootsRuntimeManagerError::SpawnProcess {
- binary_path: binary_path.to_path_buf(),
- source,
+ kind: source.kind(),
})?;
let pid = child.id();
- fs::write(&paths.pid_file_path, pid.to_string()).map_err(|source| {
+ fs::write(paths.pid_file_path(), pid.to_string()).map_err(|source| {
RadrootsRuntimeManagerError::WritePidFile {
- path: paths.pid_file_path.clone(),
- source,
+ kind: source.kind(),
}
})?;
Ok(pid)
@@ -239,33 +204,12 @@ pub fn stop_process(
pub fn remove_instance_artifacts(
paths: &ManagedRuntimeInstancePaths,
) -> Result<(), RadrootsRuntimeManagerError> {
- for path in [
- &paths.install_dir,
- &paths.state_dir,
- &paths.logs_dir,
- &paths.run_dir,
- &paths.secrets_dir,
- ] {
+ for path in [paths.install_dir(), paths.logs_dir(), paths.run_dir()] {
remove_path_if_exists(path)?;
}
Ok(())
}
-fn serialize_instance_metadata(
- record: &ManagedRuntimeInstanceRecord,
-) -> Result<String, RadrootsRuntimeManagerError> {
- serialize_instance_metadata_with(record, toml::to_string_pretty)
-}
-
-fn serialize_instance_metadata_with(
- record: &ManagedRuntimeInstanceRecord,
- serializer: fn(&ManagedRuntimeInstanceRecord) -> Result<String, toml::ser::Error>,
-) -> Result<String, RadrootsRuntimeManagerError> {
- serializer(record).map_err(|details| {
- RadrootsRuntimeManagerError::SerializeInstanceMetadata(details.to_string())
- })
-}
-
fn stop_process_for_pid(
paths: &ManagedRuntimeInstancePaths,
pid: u32,
@@ -292,10 +236,7 @@ fn stop_process_for_pid(
sleep(Duration::from_millis(100));
}
- Err(RadrootsRuntimeManagerError::StopProcess {
- pid,
- details: "process did not exit after terminate and force-kill attempts".to_owned(),
- })
+ Err(RadrootsRuntimeManagerError::StopProcess)
}
fn unpack_tar_gz_archive(
@@ -304,8 +245,7 @@ fn unpack_tar_gz_archive(
) -> Result<(), RadrootsRuntimeManagerError> {
let archive_file = File::open(archive_path).map_err(|source| {
RadrootsRuntimeManagerError::ReadManagedFile {
- path: archive_path.to_path_buf(),
- source,
+ kind: source.kind(),
}
})?;
let decoder = GzDecoder::new(archive_file);
@@ -313,8 +253,7 @@ fn unpack_tar_gz_archive(
archive
.unpack(destination_dir)
.map_err(|source| RadrootsRuntimeManagerError::UnpackArchive {
- archive_path: archive_path.to_path_buf(),
- source,
+ kind: source.kind(),
})
}
@@ -342,8 +281,7 @@ fn open_log_file(path: &Path) -> Result<File, RadrootsRuntimeManagerError> {
.append(true)
.open(path)
.map_err(|source| RadrootsRuntimeManagerError::OpenLogFile {
- path: path.to_path_buf(),
- source,
+ kind: source.kind(),
})
}
@@ -352,21 +290,19 @@ fn ensure_parent_dir(path: &Path) -> Result<(), RadrootsRuntimeManagerError> {
return Ok(());
};
fs::create_dir_all(parent).map_err(|source| RadrootsRuntimeManagerError::CreateDirectory {
- path: parent.to_path_buf(),
- source,
+ kind: source.kind(),
})
}
fn read_pid(
paths: &ManagedRuntimeInstancePaths,
) -> Result<Option<u32>, RadrootsRuntimeManagerError> {
- let raw = match fs::read_to_string(&paths.pid_file_path) {
+ let raw = match fs::read_to_string(paths.pid_file_path()) {
Ok(raw) => raw,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(source) => {
return Err(RadrootsRuntimeManagerError::ReadPidFile {
- path: paths.pid_file_path.clone(),
- source,
+ kind: source.kind(),
});
}
};
@@ -377,19 +313,15 @@ fn read_pid(
trimmed
.parse::<u32>()
.map(Some)
- .map_err(|_| RadrootsRuntimeManagerError::ParsePidFile {
- path: paths.pid_file_path.clone(),
- contents: trimmed.to_owned(),
- })
+ .map_err(|_| RadrootsRuntimeManagerError::ParsePidFile)
}
fn remove_pid_file(paths: &ManagedRuntimeInstancePaths) -> Result<(), RadrootsRuntimeManagerError> {
- match fs::remove_file(&paths.pid_file_path) {
+ match fs::remove_file(paths.pid_file_path()) {
Ok(()) => Ok(()),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(source) => Err(RadrootsRuntimeManagerError::RemovePath {
- path: paths.pid_file_path.clone(),
- source,
+ kind: source.kind(),
}),
}
}
@@ -419,20 +351,17 @@ fn remove_path_from_state(
match state {
Ok(Some(ExistingPathKind::Directory)) => {
remove_dir_all(path).map_err(|source| RadrootsRuntimeManagerError::RemovePath {
- path: path.to_path_buf(),
- source,
+ kind: source.kind(),
})
}
Ok(Some(ExistingPathKind::File)) => {
remove_file(path).map_err(|source| RadrootsRuntimeManagerError::RemovePath {
- path: path.to_path_buf(),
- source,
+ kind: source.kind(),
})
}
Ok(None) => Ok(()),
Err(source) => Err(RadrootsRuntimeManagerError::ReadManagedFile {
- path: path.to_path_buf(),
- source,
+ kind: source.kind(),
}),
}
}
@@ -456,11 +385,6 @@ fn set_executable_mode(_path: &Path) -> Result<(), RadrootsRuntimeManagerError>
}
#[cfg(unix)]
-fn set_secret_mode(path: &Path) -> Result<(), RadrootsRuntimeManagerError> {
- apply_mode(path, 0o600, set_permissions_path)
-}
-
-#[cfg(unix)]
fn apply_mode(
path: &Path,
mode: u32,
@@ -470,15 +394,13 @@ fn apply_mode(
let metadata =
fs::metadata(path).map_err(|source| RadrootsRuntimeManagerError::ReadManagedFile {
- path: path.to_path_buf(),
- source,
+ kind: source.kind(),
})?;
let mut permissions = metadata.permissions();
permissions.set_mode(mode);
set_permissions(path, permissions).map_err(|source| {
RadrootsRuntimeManagerError::SetPermissions {
- path: path.to_path_buf(),
- source,
+ kind: source.kind(),
}
})
}
@@ -488,11 +410,6 @@ fn set_permissions_path(path: &Path, permissions: fs::Permissions) -> std::io::R
fs::set_permissions(path, permissions)
}
-#[cfg(not(unix))]
-fn set_secret_mode(_path: &Path) -> Result<(), RadrootsRuntimeManagerError> {
- Ok(())
-}
-
#[cfg(unix)]
fn process_running_for_pid(pid: u32) -> bool {
let pid_arg = pid.to_string();
@@ -582,18 +499,13 @@ fn signal_process_with(
) -> Result<(), RadrootsRuntimeManagerError> {
let status = runner(pid, signal).map_err(|source| {
RadrootsRuntimeManagerError::ExecuteProcessSignal {
- pid,
- signal: signal.to_owned(),
- source,
+ kind: source.kind(),
}
})?;
if status.success() {
Ok(())
} else {
- Err(RadrootsRuntimeManagerError::StopProcess {
- pid,
- details: format!("`kill {signal}` returned {status}"),
- })
+ Err(RadrootsRuntimeManagerError::StopProcess)
}
}
@@ -610,34 +522,23 @@ fn force_kill_process(pid: u32) -> Result<(), RadrootsRuntimeManagerError> {
.stderr(Stdio::null())
.status()
.map_err(|source| RadrootsRuntimeManagerError::ExecuteProcessSignal {
- pid,
- signal: "taskkill".to_owned(),
- source,
+ kind: source.kind(),
})?;
if status.success() {
Ok(())
} else {
- Err(RadrootsRuntimeManagerError::StopProcess {
- pid,
- details: format!("`taskkill` returned {status}"),
- })
+ Err(RadrootsRuntimeManagerError::StopProcess)
}
}
#[cfg(not(any(unix, windows)))]
-fn terminate_process(pid: u32) -> Result<(), RadrootsRuntimeManagerError> {
- Err(RadrootsRuntimeManagerError::StopProcess {
- pid,
- details: "process signaling is unsupported on this platform".to_owned(),
- })
+fn terminate_process(_pid: u32) -> Result<(), RadrootsRuntimeManagerError> {
+ Err(RadrootsRuntimeManagerError::StopProcess)
}
#[cfg(not(any(unix, windows)))]
-fn force_kill_process(pid: u32) -> Result<(), RadrootsRuntimeManagerError> {
- Err(RadrootsRuntimeManagerError::StopProcess {
- pid,
- details: "process signaling is unsupported on this platform".to_owned(),
- })
+fn force_kill_process(_pid: u32) -> Result<(), RadrootsRuntimeManagerError> {
+ Err(RadrootsRuntimeManagerError::StopProcess)
}
#[cfg(test)]
@@ -653,65 +554,67 @@ mod tests {
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
- #[cfg(unix)]
- use serde::ser::Error as _;
+ use radroots_runtime_paths::{
+ InstanceId, RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver,
+ RadrootsPlatform, RuntimeContext, RuntimeContextBootstrap, RuntimeContextSource, ServiceId,
+ };
use tempfile::tempdir;
use super::{
- ExistingPathKind, apply_mode, ensure_instance_layout, ensure_parent_dir,
- extract_binary_archive, find_binary_with_name, force_kill_process, install_binary,
- open_log_file, process_running, process_running_for_pid,
- process_running_state_from_ps_output, read_pid, read_secret_file,
- remove_instance_artifacts, remove_path_from_state, remove_path_if_exists,
- serialize_instance_metadata_with, set_executable_mode, set_secret_mode, signal_process,
+ ExistingPathKind, ManagedRuntimeArtifactName, apply_mode, ensure_instance_layout,
+ ensure_parent_dir, extract_binary_archive, find_binary_with_name, force_kill_process,
+ install_binary, open_log_file, process_running, process_running_for_pid,
+ process_running_state_from_ps_output, read_pid, remove_instance_artifacts,
+ remove_path_from_state, remove_path_if_exists, set_executable_mode, signal_process,
signal_process_with, start_process, stop_process, stop_process_for_pid, terminate_process,
- write_instance_metadata, write_managed_file, write_secret_file,
+ write_instance_config,
};
use crate::error::RadrootsRuntimeManagerError;
- use crate::model::{ManagedRuntimeInstallState, ManagedRuntimeInstanceRecord};
- use crate::paths::ManagedRuntimeInstancePaths;
+ use crate::paths::{ManagedRuntimeInstancePaths, resolve_instance_paths, resolve_shared_paths};
fn sample_paths(root: &Path) -> ManagedRuntimeInstancePaths {
- ManagedRuntimeInstancePaths {
- install_dir: root.join("install"),
- state_dir: root.join("state"),
- logs_dir: root.join("logs"),
- run_dir: root.join("run"),
- secrets_dir: root.join("secrets"),
- pid_file_path: root.join("run/runtime.pid"),
- stdout_log_path: root.join("logs/stdout.log"),
- stderr_log_path: root.join("logs/stderr.log"),
- metadata_path: root.join("state/instance.toml"),
+ fn context(root: &Path, service: &str) -> RuntimeContext {
+ RuntimeContext::resolve(
+ &RadrootsPathResolver::new(
+ RadrootsPlatform::Linux,
+ RadrootsHostEnvironment::default(),
+ ),
+ RuntimeContextBootstrap::new(
+ RadrootsPathProfile::RepoLocal,
+ Some(root.to_path_buf()),
+ RuntimeContextSource::BootstrapCli,
+ RuntimeContextSource::BootstrapCli,
+ )
+ .expect("bootstrap"),
+ ServiceId::new(service).expect("service"),
+ InstanceId::new("local").expect("instance"),
+ )
+ .expect("context")
}
+
+ let shared = resolve_shared_paths(&context(root, "runtime-manager"));
+ resolve_instance_paths(&shared, &context(root, "radrootsd"))
}
- fn assert_error_contains(err: &RadrootsRuntimeManagerError, parts: &[&str]) {
- let rendered = err.to_string();
- for part in parts {
- assert!(
- rendered.contains(part),
- "expected `{rendered}` to contain `{part}`"
- );
- }
+ fn artifact(value: &str) -> ManagedRuntimeArtifactName {
+ ManagedRuntimeArtifactName::new(value).expect("artifact name")
}
- fn sample_record(paths: &ManagedRuntimeInstancePaths) -> ManagedRuntimeInstanceRecord {
- ManagedRuntimeInstanceRecord {
- runtime_id: "radrootsd".to_owned(),
- instance_id: "local".to_owned(),
- management_mode: "interactive_user_managed".to_owned(),
- install_state: ManagedRuntimeInstallState::Configured,
- binary_path: paths.install_dir.join("radrootsd"),
- config_path: paths.state_dir.join("config.toml"),
- logs_path: paths.logs_dir.clone(),
- run_path: paths.run_dir.clone(),
- installed_version: "0.1.0".to_owned(),
- health_endpoint: Some("http://127.0.0.1:7070".to_owned()),
- secret_material_ref: Some(paths.secrets_dir.join("token.txt").display().to_string()),
- last_started_at: None,
- last_stopped_at: None,
- notes: Some("test".to_owned()),
+ fn assert_safe_error(err: &RadrootsRuntimeManagerError, expected: &str, forbidden: &[&str]) {
+ use std::error::Error as _;
+
+ let rendered = format!("{err} {err:?}");
+ assert!(
+ rendered.contains(expected),
+ "expected `{rendered}` to contain `{expected}`"
+ );
+ for part in forbidden {
+ assert!(
+ !rendered.contains(part),
+ "expected `{rendered}` not to contain `{part}`"
+ );
}
+ assert!(err.source().is_none());
}
#[cfg(unix)]
@@ -757,19 +660,16 @@ mod tests {
}
#[test]
- fn layout_and_metadata_helpers_write_expected_files() {
+ fn layout_creates_only_manager_owned_install_and_tracking_roots() {
let dir = tempdir().expect("tempdir");
let paths = sample_paths(dir.path());
ensure_instance_layout(&paths).expect("layout");
- write_managed_file(paths.state_dir.join("config.toml"), "value = true").expect("config");
- write_secret_file(paths.secrets_dir.join("token.txt"), "secret").expect("secret");
- write_instance_metadata(&paths, &sample_record(&paths)).expect("metadata");
- assert_eq!(
- read_secret_file(paths.secrets_dir.join("token.txt")).expect("read secret"),
- "secret"
- );
- assert!(paths.metadata_path.is_file());
- assert!(paths.state_dir.join("config.toml").is_file());
+ assert!(paths.install_dir().is_dir());
+ assert!(paths.logs_dir().is_dir());
+ assert!(paths.run_dir().is_dir());
+ assert!(!paths.config_dir().exists());
+ assert!(!paths.state_dir().exists());
+ assert!(!paths.secrets_dir().exists());
}
#[test]
@@ -778,8 +678,34 @@ mod tests {
let source = dir.path().join("radrootsd");
fs::write(&source, "#!/bin/sh\nexit 0\n").expect("source");
let paths = sample_paths(dir.path());
- let installed = install_binary(&source, &paths, "radrootsd").expect("install");
+ let installed = install_binary(&source, &paths, &artifact("radrootsd")).expect("install");
assert!(installed.is_file());
+ assert!(installed.starts_with(paths.install_dir()));
+ }
+
+ #[test]
+ fn artifact_names_reject_absolute_parent_and_multicomponent_escapes() {
+ for invalid in [
+ "",
+ "/tmp/escape",
+ "../escape",
+ "nested/escape",
+ r"nested\escape",
+ ".",
+ "..",
+ " secret",
+ ] {
+ let err = ManagedRuntimeArtifactName::new(invalid).expect_err("reject artifact name");
+ if invalid.is_empty() {
+ assert_safe_error(&err, "artifact name is invalid", &[]);
+ } else {
+ assert_safe_error(&err, "artifact name is invalid", &[invalid]);
+ }
+ }
+
+ let maximum = format!("a{}", "b".repeat(127));
+ assert!(ManagedRuntimeArtifactName::new(&maximum).is_ok());
+ assert!(ManagedRuntimeArtifactName::new(&format!("{maximum}c")).is_err());
}
#[cfg(unix)]
@@ -805,7 +731,8 @@ mod tests {
let paths = sample_paths(dir.path());
let installed =
- extract_binary_archive(&archive_path, "tar.gz", &paths, "radrootsd").expect("extract");
+ extract_binary_archive(&archive_path, "tar.gz", &paths, &artifact("radrootsd"))
+ .expect("extract");
assert!(installed.is_file());
}
@@ -829,8 +756,9 @@ mod tests {
let paths = sample_paths(dir.path());
let installed =
- extract_binary_archive(&archive_path, "tar.gz", &paths, "radrootsd").expect("extract");
- assert_eq!(installed, paths.install_dir.join("radrootsd"));
+ extract_binary_archive(&archive_path, "tar.gz", &paths, &artifact("radrootsd"))
+ .expect("extract");
+ assert_eq!(installed, paths.install_dir().join("radrootsd"));
}
#[cfg(unix)]
@@ -840,15 +768,15 @@ mod tests {
let binary = dir.path().join("sleepy.sh");
fs::write(&binary, "#!/bin/sh\nexec sleep 30\n").expect("script");
let paths = sample_paths(dir.path());
- let installed = install_binary(&binary, &paths, "sleepy.sh").expect("install");
+ install_binary(&binary, &paths, &artifact("sleepy.sh")).expect("install");
let envs = vec![("RADROOTS_RUNTIME_MANAGER_TEST".to_owned(), "1".to_owned())];
- let pid = start_process(&installed, &Vec::new(), &envs, &paths).expect("start");
+ let pid = start_process(&paths, &artifact("sleepy.sh"), &Vec::new(), &envs).expect("start");
assert!(pid > 0);
thread::sleep(Duration::from_millis(100));
- assert!(paths.pid_file_path.is_file());
+ assert!(paths.pid_file_path().is_file());
assert!(process_running(&paths).expect("running"));
assert!(stop_process(&paths).expect("stop"));
- assert!(!paths.pid_file_path.exists());
+ assert!(!paths.pid_file_path().exists());
}
#[test]
@@ -856,27 +784,30 @@ mod tests {
let dir = tempdir().expect("tempdir");
let paths = sample_paths(dir.path());
ensure_instance_layout(&paths).expect("layout");
+ fs::create_dir_all(paths.state_dir()).expect("canonical state sentinel");
+ fs::create_dir_all(paths.secrets_dir()).expect("canonical secrets sentinel");
+ fs::write(paths.state_dir().join("state.sqlite"), "state").expect("state sentinel");
+ fs::write(paths.secrets_dir().join("identity.secret"), "secret").expect("secret sentinel");
remove_instance_artifacts(&paths).expect("remove");
- assert!(!paths.install_dir.exists());
- assert!(!paths.state_dir.exists());
- assert!(!paths.logs_dir.exists());
- assert!(!paths.run_dir.exists());
- assert!(!paths.secrets_dir.exists());
+ assert!(!paths.install_dir().exists());
+ assert!(!paths.logs_dir().exists());
+ assert!(!paths.run_dir().exists());
+ assert!(paths.state_dir().join("state.sqlite").is_file());
+ assert!(paths.secrets_dir().join("identity.secret").is_file());
}
#[test]
fn ensure_instance_layout_reports_directory_errors() {
let dir = tempdir().expect("tempdir");
let paths = sample_paths(dir.path());
- fs::write(&paths.install_dir, "occupied").expect("file");
+ fs::create_dir_all(paths.install_dir().parent().expect("install parent")).expect("parent");
+ fs::write(paths.install_dir(), "occupied").expect("file");
let err = ensure_instance_layout(&paths).expect_err("file path should fail");
- assert_error_contains(
+ assert_safe_error(
&err,
- &[
- paths.install_dir.to_string_lossy().as_ref(),
- "create directory",
- ],
+ "create managed runtime directory",
+ &[paths.install_dir().to_string_lossy().as_ref()],
);
}
@@ -884,18 +815,18 @@ mod tests {
fn install_binary_reports_copy_errors() {
let dir = tempdir().expect("tempdir");
let paths = sample_paths(dir.path());
- let err = install_binary(dir.path().join("missing"), &paths, "radrootsd")
+ let err = install_binary(dir.path().join("missing"), &paths, &artifact("radrootsd"))
.expect_err("missing source should fail");
- assert_error_contains(
+ assert_safe_error(
&err,
+ "copy managed runtime binary",
&[
dir.path().join("missing").to_string_lossy().as_ref(),
paths
- .install_dir
+ .install_dir()
.join("radrootsd")
.to_string_lossy()
.as_ref(),
- "copy runtime binary",
],
);
}
@@ -906,9 +837,13 @@ mod tests {
let paths = sample_paths(dir.path());
let archive_path = dir.path().join("radrootsd.zip");
- let err = extract_binary_archive(&archive_path, "zip", &paths, "radrootsd")
+ let err = extract_binary_archive(&archive_path, "zip", &paths, &artifact("radrootsd"))
.expect_err("unsupported archive format should fail");
- assert_error_contains(&err, &[archive_path.to_string_lossy().as_ref(), "zip"]);
+ assert_safe_error(
+ &err,
+ "archive format is unsupported",
+ &[archive_path.to_string_lossy().as_ref(), "zip"],
+ );
}
#[test]
@@ -917,11 +852,12 @@ mod tests {
let paths = sample_paths(dir.path());
let archive_path = dir.path().join("missing.tar.gz");
- let err = extract_binary_archive(&archive_path, "tar.gz", &paths, "radrootsd")
+ let err = extract_binary_archive(&archive_path, "tar.gz", &paths, &artifact("radrootsd"))
.expect_err("missing archive should fail");
- assert_error_contains(
+ assert_safe_error(
&err,
- &[archive_path.to_string_lossy().as_ref(), "read managed file"],
+ "read managed runtime file",
+ &[archive_path.to_string_lossy().as_ref()],
);
}
@@ -944,17 +880,18 @@ mod tests {
encoder.finish().expect("finish gzip");
let paths = sample_paths(dir.path());
- let err = extract_binary_archive(&archive_path, "tar.gz", &paths, "radrootsd")
+ let err = extract_binary_archive(&archive_path, "tar.gz", &paths, &artifact("radrootsd"))
.expect_err("archive should not resolve missing binary");
- assert_error_contains(
+ assert_safe_error(
&err,
+ "read managed runtime file",
&[
paths
- .install_dir
+ .install_dir()
.join("radrootsd")
.to_string_lossy()
.as_ref(),
- "did not produce",
+ archive_path.to_string_lossy().as_ref(),
],
);
}
@@ -967,92 +904,38 @@ mod tests {
fs::write(&archive_path, "not a gzip archive").expect("write archive");
let paths = sample_paths(dir.path());
- let err = extract_binary_archive(&archive_path, "tar.gz", &paths, "radrootsd")
+ let err = extract_binary_archive(&archive_path, "tar.gz", &paths, &artifact("radrootsd"))
.expect_err("invalid archive should fail");
- assert_error_contains(
- &err,
- &[archive_path.to_string_lossy().as_ref(), "unpack archive"],
- );
- }
-
- #[test]
- fn write_managed_file_reports_write_errors() {
- let dir = tempdir().expect("tempdir");
- let path = dir.path().join("as-dir");
- fs::create_dir(&path).expect("create directory target");
-
- let err = write_managed_file(&path, "value").expect_err("directory write should fail");
- assert_error_contains(
- &err,
- &[path.to_string_lossy().as_ref(), "write managed file"],
- );
- }
-
- #[test]
- fn write_secret_file_reports_write_errors() {
- let dir = tempdir().expect("tempdir");
- let path = dir.path().join("as-dir");
- fs::create_dir(&path).expect("create directory target");
-
- let err = write_secret_file(&path, "secret").expect_err("directory write should fail");
- assert_error_contains(
- &err,
- &[path.to_string_lossy().as_ref(), "write managed file"],
- );
- }
-
- #[test]
- fn read_secret_file_reports_missing_path() {
- let dir = tempdir().expect("tempdir");
- let path = dir.path().join("missing.secret");
- let err = read_secret_file(&path).expect_err("missing secret should fail");
- assert_error_contains(
- &err,
- &[path.to_string_lossy().as_ref(), "read managed file"],
- );
- }
-
- #[test]
- fn write_instance_metadata_reports_write_errors() {
- let dir = tempdir().expect("tempdir");
- let mut paths = sample_paths(dir.path());
- ensure_instance_layout(&paths).expect("layout");
- fs::create_dir_all(&paths.metadata_path).expect("create metadata dir");
- paths.metadata_path = paths.metadata_path.clone();
-
- let err = write_instance_metadata(
- &paths,
- &ManagedRuntimeInstanceRecord {
- health_endpoint: None,
- secret_material_ref: None,
- notes: None,
- ..sample_record(&paths)
- },
- )
- .expect_err("metadata dir target should fail");
- assert_error_contains(
+ assert_safe_error(
&err,
+ "unpack managed runtime archive",
&[
- paths.metadata_path.to_string_lossy().as_ref(),
- "write runtime instance metadata",
+ archive_path.to_string_lossy().as_ref(),
+ "not a gzip archive",
],
);
}
#[test]
- fn serialize_instance_metadata_reports_serializer_errors() {
+ fn write_instance_config_is_context_bound_and_reports_redacted_errors() {
let dir = tempdir().expect("tempdir");
let paths = sample_paths(dir.path());
- let err = serialize_instance_metadata_with(&sample_record(&paths), |_| {
- Err(toml::ser::Error::custom("forced serializer failure"))
- })
- .expect_err("serializer should fail");
- assert_error_contains(
+ let config = paths.config_path();
+ write_instance_config(&paths, "enabled = true").expect("write config");
+ assert_eq!(
+ fs::read_to_string(&config).expect("read config"),
+ "enabled = true"
+ );
+ assert!(!paths.secrets_dir().exists());
+
+ fs::remove_file(&config).expect("remove config");
+ fs::create_dir(&config).expect("occupy config path");
+ let err = write_instance_config(&paths, "credential = 'secret-value'")
+ .expect_err("directory write should fail");
+ assert_safe_error(
&err,
- &[
- "serialize runtime instance metadata",
- "forced serializer failure",
- ],
+ "write managed runtime config",
+ &[config.to_string_lossy().as_ref(), "secret-value"],
);
}
@@ -1060,14 +943,16 @@ mod tests {
fn start_process_reports_spawn_errors() {
let dir = tempdir().expect("tempdir");
let paths = sample_paths(dir.path());
- let err = start_process(dir.path().join("missing"), &[], &[], &paths)
+ let err = start_process(&paths, &artifact("missing"), &[], &[])
.expect_err("missing binary should fail");
- assert_error_contains(
+ assert_safe_error(
&err,
- &[
- dir.path().join("missing").to_string_lossy().as_ref(),
- "spawn managed runtime process",
- ],
+ "spawn managed runtime process",
+ &[paths
+ .install_dir()
+ .join("missing")
+ .to_string_lossy()
+ .as_ref()],
);
}
@@ -1077,16 +962,17 @@ mod tests {
let dir = tempdir().expect("tempdir");
let binary = dir.path().join("sleepy.sh");
fs::write(&binary, "#!/bin/sh\nexec sleep 1\n").expect("script");
- let mut paths = sample_paths(dir.path());
- paths.pid_file_path = paths.run_dir.clone();
- let installed =
- install_binary(&binary, &sample_paths(dir.path()), "sleepy.sh").expect("install");
+ let paths = sample_paths(dir.path());
+ fs::create_dir_all(paths.pid_file_path()).expect("occupy pid path");
+ install_binary(&binary, &sample_paths(dir.path()), &artifact("sleepy.sh"))
+ .expect("install");
- let err =
- start_process(&installed, &[], &[], &paths).expect_err("pid file write should fail");
- assert_error_contains(
+ let err = start_process(&paths, &artifact("sleepy.sh"), &[], &[])
+ .expect_err("pid file write should fail");
+ assert_safe_error(
&err,
- &[paths.run_dir.to_string_lossy().as_ref(), "write pid file"],
+ "write managed runtime pid",
+ &[paths.pid_file_path().to_string_lossy().as_ref()],
);
}
@@ -1104,12 +990,16 @@ mod tests {
let dir = tempdir().expect("tempdir");
let paths = sample_paths(dir.path());
ensure_instance_layout(&paths).expect("layout");
- fs::write(&paths.pid_file_path, "not-a-pid").expect("write pid");
+ fs::write(paths.pid_file_path(), "not-a-pid").expect("write pid");
let err = process_running(&paths).expect_err("invalid pid should fail");
- assert_error_contains(
+ assert_safe_error(
&err,
- &[paths.pid_file_path.to_string_lossy().as_ref(), "not-a-pid"],
+ "managed runtime pid is malformed",
+ &[
+ paths.pid_file_path().to_string_lossy().as_ref(),
+ "not-a-pid",
+ ],
);
}
@@ -1118,10 +1008,10 @@ mod tests {
let dir = tempdir().expect("tempdir");
let paths = sample_paths(dir.path());
ensure_instance_layout(&paths).expect("layout");
- fs::write(&paths.pid_file_path, "999999").expect("write pid");
+ fs::write(paths.pid_file_path(), "999999").expect("write pid");
assert!(!stop_process(&paths).expect("stale pid should return false"));
- assert!(!paths.pid_file_path.exists());
+ assert!(!paths.pid_file_path().exists());
}
#[test]
@@ -1129,7 +1019,7 @@ mod tests {
let dir = tempdir().expect("tempdir");
let paths = sample_paths(dir.path());
ensure_instance_layout(&paths).expect("layout");
- fs::write(&paths.pid_file_path, "42").expect("write pid");
+ fs::write(paths.pid_file_path(), "42").expect("write pid");
let mut polls = 0_u32;
let mut is_running = |_pid| {
@@ -1150,7 +1040,7 @@ mod tests {
.expect("force-kill path should stop");
assert!(stopped);
- assert!(!paths.pid_file_path.exists());
+ assert!(!paths.pid_file_path().exists());
assert_eq!(polls, 21);
}
@@ -1159,7 +1049,7 @@ mod tests {
let dir = tempdir().expect("tempdir");
let paths = sample_paths(dir.path());
ensure_instance_layout(&paths).expect("layout");
- fs::write(&paths.pid_file_path, "42").expect("write pid");
+ fs::write(paths.pid_file_path(), "42").expect("write pid");
let mut is_running = runtime_is_stopped;
let mut terminate = ok_runtime_signal;
@@ -1176,7 +1066,7 @@ mod tests {
.expect("terminate poll should stop");
assert!(stopped);
- assert!(!paths.pid_file_path.exists());
+ assert!(!paths.pid_file_path().exists());
}
#[test]
@@ -1184,7 +1074,7 @@ mod tests {
let dir = tempdir().expect("tempdir");
let paths = sample_paths(dir.path());
ensure_instance_layout(&paths).expect("layout");
- fs::write(&paths.pid_file_path, "42").expect("write pid");
+ fs::write(paths.pid_file_path(), "42").expect("write pid");
let mut sleeps = 0_u32;
let mut is_running = runtime_is_running;
@@ -1203,9 +1093,9 @@ mod tests {
)
.expect_err("force-kill exhaustion should fail");
- assert_error_contains(&err, &["42", "did not exit after terminate and force-kill"]);
+ assert_safe_error(&err, "managed runtime process did not stop", &["42"]);
assert_eq!(sleeps, 40);
- assert!(paths.pid_file_path.exists());
+ assert!(paths.pid_file_path().exists());
}
#[test]
@@ -1221,9 +1111,10 @@ mod tests {
let err =
ensure_parent_dir(&file_parent.join("child")).expect_err("file parent should fail");
- assert_error_contains(
+ assert_safe_error(
&err,
- &[file_parent.to_string_lossy().as_ref(), "create directory"],
+ "create managed runtime directory",
+ &[file_parent.to_string_lossy().as_ref()],
);
}
@@ -1251,28 +1142,31 @@ mod tests {
let bad_path = dir.path().join("bad");
fs::create_dir(&bad_path).expect("create dir");
let err = open_log_file(&bad_path).expect_err("directory open should fail");
- assert_error_contains(
+ assert_safe_error(
&err,
- &[bad_path.to_string_lossy().as_ref(), "open runtime log file"],
+ "open managed runtime log",
+ &[bad_path.to_string_lossy().as_ref()],
);
}
#[test]
fn read_pid_handles_empty_missing_and_read_error_cases() {
let dir = tempdir().expect("tempdir");
- let mut paths = sample_paths(dir.path());
+ let paths = sample_paths(dir.path());
assert_eq!(read_pid(&paths).expect("missing pid"), None);
ensure_instance_layout(&paths).expect("layout");
- fs::write(&paths.pid_file_path, " ").expect("write pid");
+ fs::write(paths.pid_file_path(), " ").expect("write pid");
assert_eq!(read_pid(&paths).expect("empty pid"), None);
- paths.pid_file_path = paths.run_dir.clone();
+ fs::remove_file(paths.pid_file_path()).expect("remove pid file");
+ fs::create_dir(paths.pid_file_path()).expect("occupy pid path");
let err = read_pid(&paths).expect_err("directory pid file should fail");
- assert_error_contains(
+ assert_safe_error(
&err,
- &[paths.run_dir.to_string_lossy().as_ref(), "read pid file"],
+ "read managed runtime pid",
+ &[paths.pid_file_path().to_string_lossy().as_ref()],
);
}
@@ -1307,9 +1201,10 @@ mod tests {
ok_remove_path,
)
.expect_err("directory removal should fail");
- assert_error_contains(
+ assert_safe_error(
&dir_err,
- &[dir_path.to_string_lossy().as_ref(), "remove managed path"],
+ "remove manager-owned runtime path",
+ &[dir_path.to_string_lossy().as_ref(), "remove path denied"],
);
let file_err = remove_path_from_state(
@@ -1319,9 +1214,10 @@ mod tests {
deny_remove_path,
)
.expect_err("file removal should fail");
- assert_error_contains(
+ assert_safe_error(
&file_err,
- &[file_path.to_string_lossy().as_ref(), "remove managed path"],
+ "remove manager-owned runtime path",
+ &[file_path.to_string_lossy().as_ref(), "remove path denied"],
);
let metadata_err = remove_path_from_state(
@@ -1334,11 +1230,12 @@ mod tests {
ok_remove_path,
)
.expect_err("metadata lookup should fail");
- assert_error_contains(
+ assert_safe_error(
&metadata_err,
+ "read managed runtime file",
&[
metadata_path.to_string_lossy().as_ref(),
- "read managed file",
+ "metadata lookup failed",
],
);
}
@@ -1346,17 +1243,15 @@ mod tests {
#[test]
fn remove_pid_file_reports_directory_errors() {
let dir = tempdir().expect("tempdir");
- let mut paths = sample_paths(dir.path());
+ let paths = sample_paths(dir.path());
ensure_instance_layout(&paths).expect("layout");
- paths.pid_file_path = paths.run_dir.clone();
+ fs::create_dir(paths.pid_file_path()).expect("occupy pid path");
let err = super::remove_pid_file(&paths).expect_err("directory pid path should fail");
- assert_error_contains(
+ assert_safe_error(
&err,
- &[
- paths.run_dir.to_string_lossy().as_ref(),
- "remove managed path",
- ],
+ "remove manager-owned runtime path",
+ &[paths.pid_file_path().to_string_lossy().as_ref()],
);
}
@@ -1369,20 +1264,15 @@ mod tests {
#[cfg(unix)]
#[test]
- fn set_mode_helpers_report_missing_path_errors() {
+ fn executable_mode_reports_missing_path_errors() {
let dir = tempdir().expect("tempdir");
let missing = dir.path().join("missing");
let err = set_executable_mode(&missing).expect_err("missing executable should fail");
- assert_error_contains(
- &err,
- &[missing.to_string_lossy().as_ref(), "read managed file"],
- );
-
- let err = set_secret_mode(&missing).expect_err("missing secret should fail");
- assert_error_contains(
+ assert_safe_error(
&err,
- &[missing.to_string_lossy().as_ref(), "read managed file"],
+ "read managed runtime file",
+ &[missing.to_string_lossy().as_ref()],
);
}
@@ -1400,7 +1290,11 @@ mod tests {
))
})
.expect_err("set permissions should fail");
- assert_error_contains(&err, &[path.to_string_lossy().as_ref(), "set permissions"]);
+ assert_safe_error(
+ &err,
+ "set managed runtime file permissions",
+ &[path.to_string_lossy().as_ref(), "set permissions failed"],
+ );
}
#[cfg(unix)]
@@ -1423,9 +1317,10 @@ mod tests {
restore.set_mode(0o755);
fs::set_permissions(&restricted, restore).expect("restore permissions");
- assert_error_contains(
+ assert_safe_error(
&err,
- &[blocked_path.to_string_lossy().as_ref(), "read managed file"],
+ "read managed runtime file",
+ &[blocked_path.to_string_lossy().as_ref()],
);
}
@@ -1436,13 +1331,13 @@ mod tests {
assert!(!process_running_for_pid(missing_pid));
let err = terminate_process(missing_pid).expect_err("terminate should fail");
- assert_error_contains(&err, &[&missing_pid.to_string(), "stop pid"]);
+ assert_safe_error(&err, "managed runtime process did not stop", &["999999"]);
let err = force_kill_process(missing_pid).expect_err("force kill should fail");
- assert_error_contains(&err, &[&missing_pid.to_string(), "stop pid"]);
+ assert_safe_error(&err, "managed runtime process did not stop", &["999999"]);
let err = signal_process(missing_pid, "-BOGUS").expect_err("invalid signal should fail");
- assert_error_contains(&err, &[&missing_pid.to_string(), "stop pid"]);
+ assert_safe_error(&err, "managed runtime process did not stop", &["999999"]);
}
#[cfg(unix)]
@@ -1455,7 +1350,11 @@ mod tests {
))
})
.expect_err("signal execution should fail");
- assert_error_contains(&err, &["42", "-TERM", "kill executable missing"]);
+ assert_safe_error(
+ &err,
+ "signal managed runtime process",
+ &["42", "-TERM", "kill executable missing"],
+ );
}
#[cfg(unix)]
diff --git a/crates/runtime_manager/src/managed.rs b/crates/runtime_manager/src/managed.rs
@@ -1,21 +1,81 @@
-use std::path::PathBuf;
+use core::fmt;
+use std::path::Path;
-use radroots_runtime_paths::{
- RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, RadrootsRuntimePathSelection,
-};
+use radroots_runtime_paths::{RadrootsPathProfile, RuntimeContext, RuntimeContextSource};
+use crate::paths::{resolve_instance_paths, resolve_shared_paths};
+use crate::registry::{remove_instance, upsert_instance};
use crate::{
BootstrapRuntimeContract, ManagedRuntimeHealthState, ManagedRuntimeInstallState,
ManagedRuntimeInstancePaths, ManagedRuntimeInstanceRecord, ManagedRuntimeInstanceRegistry,
ManagementModeContract, RadrootsRuntimeManagementContract, RadrootsRuntimeManagerError,
- load_registry, resolve_instance_paths, resolve_shared_paths,
+ load_registry,
};
-#[derive(Debug, Clone)]
+#[derive(Clone)]
pub struct ManagedRuntimeContext {
- pub contract: RadrootsRuntimeManagementContract,
- pub shared_paths: crate::ManagedRuntimeSharedPaths,
- pub registry: ManagedRuntimeInstanceRegistry,
+ contract: RadrootsRuntimeManagementContract,
+ manager_context: RuntimeContext,
+ shared_paths: crate::ManagedRuntimeSharedPaths,
+ registry: ManagedRuntimeInstanceRegistry,
+}
+
+impl ManagedRuntimeContext {
+ #[must_use]
+ pub fn contract(&self) -> &RadrootsRuntimeManagementContract {
+ &self.contract
+ }
+
+ #[must_use]
+ pub fn manager_context(&self) -> &RuntimeContext {
+ &self.manager_context
+ }
+
+ #[must_use]
+ pub fn shared_paths(&self) -> &crate::ManagedRuntimeSharedPaths {
+ &self.shared_paths
+ }
+
+ #[must_use]
+ pub fn registry(&self) -> &ManagedRuntimeInstanceRegistry {
+ &self.registry
+ }
+
+ pub fn register_instance(
+ &mut self,
+ runtime_context: &RuntimeContext,
+ install_state: ManagedRuntimeInstallState,
+ ) -> Result<(), RadrootsRuntimeManagerError> {
+ ensure_context_scope(&self.manager_context, runtime_context)?;
+ upsert_instance(
+ &mut self.registry,
+ ManagedRuntimeInstanceRecord::new(runtime_context, install_state),
+ );
+ Ok(())
+ }
+
+ pub fn remove_instance(
+ &mut self,
+ runtime_context: &RuntimeContext,
+ ) -> Result<Option<ManagedRuntimeInstanceRecord>, RadrootsRuntimeManagerError> {
+ ensure_context_scope(&self.manager_context, runtime_context)?;
+ Ok(remove_instance(
+ &mut self.registry,
+ runtime_context.service(),
+ runtime_context.instance(),
+ ))
+ }
+}
+
+impl fmt::Debug for ManagedRuntimeContext {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("ManagedRuntimeContext")
+ .field("manager_context", &self.manager_context)
+ .field("shared_paths", &self.shared_paths)
+ .field("registry", &"[redacted]")
+ .finish_non_exhaustive()
+ }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -27,6 +87,7 @@ pub enum ManagedRuntimeGroup {
}
impl ManagedRuntimeGroup {
+ #[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::ActiveManagedTarget => "active_managed_target",
@@ -36,6 +97,7 @@ impl ManagedRuntimeGroup {
}
}
+ #[must_use]
pub fn posture(self) -> &'static str {
match self {
Self::ActiveManagedTarget => "active_managed_target",
@@ -46,18 +108,85 @@ impl ManagedRuntimeGroup {
}
}
-#[derive(Debug, Clone)]
+/// A sealed, internally cross-bound management target.
+///
+/// ```compile_fail
+/// use radroots_runtime_manager::ManagedRuntimeTarget;
+///
+/// let _ = ManagedRuntimeTarget {
+/// context: todo!(),
+/// instance_source: todo!(),
+/// runtime_group: todo!(),
+/// management_mode: None,
+/// mode_contract: None,
+/// bootstrap: None,
+/// instance_record: None,
+/// predicted_paths: None,
+/// };
+/// ```
+#[derive(Clone)]
pub struct ManagedRuntimeTarget {
- pub runtime_id: String,
- pub instance_id: String,
- pub instance_source: String,
- pub runtime_group: ManagedRuntimeGroup,
- pub management_mode: Option<String>,
- pub mode_contract: Option<ManagementModeContract>,
- pub bootstrap: Option<BootstrapRuntimeContract>,
- pub instance_record: Option<ManagedRuntimeInstanceRecord>,
- pub predicted_paths: Option<ManagedRuntimeInstancePaths>,
- pub registry_path: PathBuf,
+ context: RuntimeContext,
+ instance_source: RuntimeContextSource,
+ runtime_group: ManagedRuntimeGroup,
+ management_mode: Option<String>,
+ mode_contract: Option<ManagementModeContract>,
+ bootstrap: Option<BootstrapRuntimeContract>,
+ instance_record: Option<ManagedRuntimeInstanceRecord>,
+ predicted_paths: Option<ManagedRuntimeInstancePaths>,
+}
+
+impl ManagedRuntimeTarget {
+ #[must_use]
+ pub fn context(&self) -> &RuntimeContext {
+ &self.context
+ }
+
+ #[must_use]
+ pub fn instance_source(&self) -> RuntimeContextSource {
+ self.instance_source
+ }
+
+ #[must_use]
+ pub fn runtime_group(&self) -> ManagedRuntimeGroup {
+ self.runtime_group
+ }
+
+ #[must_use]
+ pub fn management_mode(&self) -> Option<&str> {
+ self.management_mode.as_deref()
+ }
+
+ #[must_use]
+ pub fn mode_contract(&self) -> Option<&ManagementModeContract> {
+ self.mode_contract.as_ref()
+ }
+
+ #[must_use]
+ pub fn bootstrap(&self) -> Option<&BootstrapRuntimeContract> {
+ self.bootstrap.as_ref()
+ }
+
+ #[must_use]
+ pub fn instance_record(&self) -> Option<&ManagedRuntimeInstanceRecord> {
+ self.instance_record.as_ref()
+ }
+
+ #[must_use]
+ pub fn predicted_paths(&self) -> Option<&ManagedRuntimeInstancePaths> {
+ self.predicted_paths.as_ref()
+ }
+}
+
+impl fmt::Debug for ManagedRuntimeTarget {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("ManagedRuntimeTarget")
+ .field("context", &self.context)
+ .field("runtime_group", &self.runtime_group)
+ .field("predicted_paths", &self.predicted_paths)
+ .finish_non_exhaustive()
+ }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -84,6 +213,7 @@ pub enum ManagedRuntimeLifecycleAction {
}
impl ManagedRuntimeLifecycleAction {
+ #[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Install => "install",
@@ -100,7 +230,7 @@ impl ManagedRuntimeLifecycleAction {
pub struct ManagedRuntimeStatusInspection {
pub runtime_id: String,
pub instance_id: String,
- pub instance_source: String,
+ pub instance_source: RuntimeContextSource,
pub runtime_group: String,
pub management_posture: String,
pub state: String,
@@ -113,23 +243,18 @@ pub struct ManagedRuntimeStatusInspection {
pub install_state: String,
pub health_state: String,
pub health_source: String,
- pub registry_path: PathBuf,
pub lifecycle_actions: Vec<String>,
- pub instance_paths: Option<ManagedRuntimeInstancePaths>,
- pub instance_record: Option<ManagedRuntimeInstanceRecord>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ManagedRuntimeLogsInspection {
pub runtime_id: String,
pub instance_id: String,
- pub instance_source: String,
+ pub instance_source: RuntimeContextSource,
pub runtime_group: String,
pub state: String,
pub source: String,
pub detail: String,
- pub stdout_log_path: Option<PathBuf>,
- pub stderr_log_path: Option<PathBuf>,
pub stdout_log_present: bool,
pub stderr_log_present: bool,
}
@@ -138,13 +263,12 @@ pub struct ManagedRuntimeLogsInspection {
pub struct ManagedRuntimeConfigInspection {
pub runtime_id: String,
pub instance_id: String,
- pub instance_source: String,
+ pub instance_source: RuntimeContextSource,
pub runtime_group: String,
pub state: String,
pub source: String,
pub detail: String,
pub config_format: Option<String>,
- pub config_path: Option<PathBuf>,
pub config_present: bool,
pub requires_bootstrap_secret: Option<bool>,
pub requires_config_bootstrap: Option<bool>,
@@ -156,7 +280,7 @@ pub struct ManagedRuntimeActionInspection {
pub action: String,
pub runtime_id: String,
pub instance_id: String,
- pub instance_source: String,
+ pub instance_source: RuntimeContextSource,
pub runtime_group: String,
pub state: String,
pub source: String,
@@ -167,29 +291,19 @@ pub struct ManagedRuntimeActionInspection {
pub fn load_management_context(
contract: RadrootsRuntimeManagementContract,
- resolver: &RadrootsPathResolver,
- profile: RadrootsPathProfile,
- overrides: &RadrootsPathOverrides,
+ manager_context: RuntimeContext,
) -> Result<ManagedRuntimeContext, RadrootsRuntimeManagerError> {
- let mode_id = active_management_mode_for_profile(&contract, profile)?;
- let shared_paths = resolve_shared_paths(&contract, resolver, profile, overrides, mode_id)?;
- let registry = load_registry(&shared_paths.instance_registry_path)?;
+ active_management_mode_for_profile(&contract, manager_context.profile())?;
+ let shared_paths = resolve_shared_paths(&manager_context);
+ let registry = load_registry(shared_paths.instance_registry_path())?;
Ok(ManagedRuntimeContext {
contract,
+ manager_context,
shared_paths,
registry,
})
}
-pub fn load_management_context_with_selection(
- contract: RadrootsRuntimeManagementContract,
- resolver: &RadrootsPathResolver,
- selection: &RadrootsRuntimePathSelection,
-) -> Result<ManagedRuntimeContext, RadrootsRuntimeManagerError> {
- let overrides = selection.caller_overrides()?;
- load_management_context(contract, resolver, selection.profile, &overrides)
-}
-
pub fn active_management_mode_for_profile(
contract: &RadrootsRuntimeManagementContract,
profile: RadrootsPathProfile,
@@ -206,34 +320,17 @@ pub fn active_management_mode_for_profile(
.any(|entry| entry == &profile_id)
})
.map(|(mode_id, _)| mode_id.as_str())
- .ok_or_else(|| RadrootsRuntimeManagerError::UnsupportedProfile {
- mode_id: "active".to_owned(),
- profile: profile_id,
- })
+ .ok_or(RadrootsRuntimeManagerError::UnsupportedProfile)
}
pub fn resolve_runtime_target(
context: &ManagedRuntimeContext,
- runtime_id: &str,
- requested_instance_id: Option<&str>,
-) -> ManagedRuntimeTarget {
+ runtime_context: RuntimeContext,
+) -> Result<ManagedRuntimeTarget, RadrootsRuntimeManagerError> {
+ ensure_context_scope(&context.manager_context, &runtime_context)?;
+ let runtime_id = runtime_context.service().as_str();
let runtime_group = runtime_group(&context.contract, runtime_id);
let bootstrap = context.contract.bootstrap.get(runtime_id).cloned();
- let instance_id = requested_instance_id
- .map(ToOwned::to_owned)
- .or_else(|| {
- bootstrap
- .as_ref()
- .map(|entry| entry.default_instance_id.clone())
- })
- .unwrap_or_else(|| "default".to_owned());
- let instance_source = if requested_instance_id.is_some() {
- "command_arg".to_owned()
- } else if bootstrap.is_some() {
- "bootstrap_default".to_owned()
- } else {
- "implicit_default".to_owned()
- };
let management_mode = bootstrap
.as_ref()
.map(|entry| entry.management_mode.clone());
@@ -244,32 +341,61 @@ pub fn resolve_runtime_target(
.registry
.instances
.iter()
- .find(|record| record.runtime_id == runtime_id && record.instance_id == instance_id)
+ .find(|record| record.matches_context(&runtime_context))
.cloned();
- let predicted_paths = if runtime_group == ManagedRuntimeGroup::ActiveManagedTarget {
- Some(resolve_instance_paths(
- &context.shared_paths,
- runtime_id,
- instance_id.as_str(),
- ))
- } else {
- None
- };
+ let predicted_paths = matches!(
+ runtime_group,
+ ManagedRuntimeGroup::ActiveManagedTarget | ManagedRuntimeGroup::DefinedManagedTarget
+ )
+ .then(|| resolve_instance_paths(&context.shared_paths, &runtime_context));
- ManagedRuntimeTarget {
- runtime_id: runtime_id.to_owned(),
- instance_id,
- instance_source,
+ Ok(ManagedRuntimeTarget {
+ instance_source: runtime_context.sources().instance(),
+ context: runtime_context,
runtime_group,
management_mode,
mode_contract,
bootstrap,
instance_record,
predicted_paths,
- registry_path: context.shared_paths.instance_registry_path.clone(),
+ })
+}
+
+fn ensure_context_scope(
+ manager: &RuntimeContext,
+ target: &RuntimeContext,
+) -> Result<(), RadrootsRuntimeManagerError> {
+ if manager.profile() != target.profile()
+ || context_roots(manager)
+ .iter()
+ .zip(context_roots(target))
+ .any(|(left, right)| left != &right)
+ {
+ return Err(RadrootsRuntimeManagerError::RuntimeContextMismatch);
}
+ Ok(())
}
+fn context_roots(context: &RuntimeContext) -> [&Path; 6] {
+ let paths = context.paths();
+ [
+ instance_root(paths.config()),
+ instance_root(paths.state()),
+ instance_root(paths.cache()),
+ instance_root(paths.logs()),
+ instance_root(paths.run()),
+ instance_root(paths.secrets()),
+ ]
+}
+
+fn instance_root(path: &Path) -> &Path {
+ path.parent()
+ .and_then(Path::parent)
+ .and_then(Path::parent)
+ .expect("RuntimeContext service paths contain the sealed services/service/instance suffix")
+}
+
+#[must_use]
pub fn inspect_runtime_status(
target: &ManagedRuntimeTarget,
lifecycle_actions: &[String],
@@ -279,17 +405,18 @@ pub fn inspect_runtime_status(
} else {
ManagedRuntimeInspectionAvailability::Success
};
+ let (health_state, health_source) = infer_health_state(target);
ManagedRuntimeInspection {
availability,
view: ManagedRuntimeStatusInspection {
- runtime_id: target.runtime_id.clone(),
- instance_id: target.instance_id.clone(),
- instance_source: target.instance_source.clone(),
+ runtime_id: target.context.service().to_string(),
+ instance_id: target.context.instance().to_string(),
+ instance_source: target.instance_source,
runtime_group: target.runtime_group.as_str().to_owned(),
management_posture: target.runtime_group.posture().to_owned(),
state: status_state(target).to_owned(),
- source: "runtime management contract + shared instance registry".to_owned(),
+ source: "runtime management contract + typed instance registry".to_owned(),
detail: status_detail(target),
management_mode: target.management_mode.clone(),
service_manager_integration: target
@@ -307,160 +434,84 @@ pub fn inspect_runtime_status(
install_state: target
.instance_record
.as_ref()
- .map(|record| install_state_label(record.install_state))
+ .map(|record| install_state_label(record.install_state()))
.unwrap_or_else(|| install_state_label(ManagedRuntimeInstallState::NotInstalled))
.to_owned(),
- health_state: infer_health_state(target).0.to_owned(),
- health_source: infer_health_state(target).1.to_owned(),
- registry_path: target.registry_path.clone(),
+ health_state: health_state.to_owned(),
+ health_source: health_source.to_owned(),
lifecycle_actions: if target.runtime_group == ManagedRuntimeGroup::ActiveManagedTarget {
lifecycle_actions.to_vec()
} else {
Vec::new()
},
- instance_paths: target.predicted_paths.clone(),
- instance_record: target.instance_record.clone(),
},
}
}
+#[must_use]
pub fn inspect_runtime_logs(
target: &ManagedRuntimeTarget,
) -> ManagedRuntimeInspection<ManagedRuntimeLogsInspection> {
- let stdout_log_path = target
+ let availability = managed_inspection_availability(target);
+ let stdout_log_present = target
.predicted_paths
.as_ref()
- .map(|paths| paths.stdout_log_path.clone());
- let stderr_log_path = target
+ .is_some_and(|paths| paths.stdout_log_path().exists());
+ let stderr_log_present = target
.predicted_paths
.as_ref()
- .map(|paths| paths.stderr_log_path.clone());
- let availability = match target.runtime_group {
- ManagedRuntimeGroup::Unknown => ManagedRuntimeInspectionAvailability::Unconfigured,
- ManagedRuntimeGroup::ActiveManagedTarget => ManagedRuntimeInspectionAvailability::Success,
- ManagedRuntimeGroup::DefinedManagedTarget | ManagedRuntimeGroup::BootstrapOnly => {
- if target.instance_record.is_some() {
- ManagedRuntimeInspectionAvailability::Success
- } else {
- ManagedRuntimeInspectionAvailability::Unsupported
- }
- }
- };
- let detail = match target.runtime_group {
- ManagedRuntimeGroup::ActiveManagedTarget => {
- "runtime logs report the managed stdout/stderr locations for the active managed instance"
- .to_owned()
- }
- ManagedRuntimeGroup::DefinedManagedTarget => format!(
- "runtime `{}` is only a defined future managed target; no active generic logs surface exists without a registered instance",
- target.runtime_id
- ),
- ManagedRuntimeGroup::BootstrapOnly => format!(
- "runtime `{}` remains bootstrap_only and direct-bindable in this wave; generic managed logs are not admitted",
- target.runtime_id
- ),
- ManagedRuntimeGroup::Unknown => unknown_runtime_detail(target),
- };
+ .is_some_and(|paths| paths.stderr_log_path().exists());
ManagedRuntimeInspection {
availability,
view: ManagedRuntimeLogsInspection {
- runtime_id: target.runtime_id.clone(),
- instance_id: target.instance_id.clone(),
- instance_source: target.instance_source.clone(),
+ runtime_id: target.context.service().to_string(),
+ instance_id: target.context.instance().to_string(),
+ instance_source: target.instance_source,
runtime_group: target.runtime_group.as_str().to_owned(),
- state: match availability {
- ManagedRuntimeInspectionAvailability::Success => "ready".to_owned(),
- ManagedRuntimeInspectionAvailability::Unconfigured => "unknown_runtime".to_owned(),
- ManagedRuntimeInspectionAvailability::Unsupported => "unsupported".to_owned(),
- },
- source: "runtime management contract + shared instance registry".to_owned(),
- detail,
- stdout_log_path: stdout_log_path.clone(),
- stderr_log_path: stderr_log_path.clone(),
- stdout_log_present: path_present(stdout_log_path.as_ref()).unwrap_or_else(|| {
- target
- .instance_record
- .as_ref()
- .is_some_and(|record| record.logs_path.join("stdout.log").exists())
- }),
- stderr_log_present: path_present(stderr_log_path.as_ref()).unwrap_or_else(|| {
- target
- .instance_record
- .as_ref()
- .is_some_and(|record| record.logs_path.join("stderr.log").exists())
- }),
+ state: availability_state(availability),
+ source: "runtime management contract + manager-owned tracking".to_owned(),
+ detail: logs_detail(target),
+ stdout_log_present,
+ stderr_log_present,
},
}
}
+#[must_use]
pub fn inspect_runtime_config(
target: &ManagedRuntimeTarget,
) -> ManagedRuntimeInspection<ManagedRuntimeConfigInspection> {
- let availability = match target.runtime_group {
- ManagedRuntimeGroup::Unknown => ManagedRuntimeInspectionAvailability::Unconfigured,
- ManagedRuntimeGroup::ActiveManagedTarget => ManagedRuntimeInspectionAvailability::Success,
- ManagedRuntimeGroup::DefinedManagedTarget | ManagedRuntimeGroup::BootstrapOnly => {
- if target.instance_record.is_some() {
- ManagedRuntimeInspectionAvailability::Success
- } else {
- ManagedRuntimeInspectionAvailability::Unsupported
- }
- }
- };
- let config_path = target
- .instance_record
- .as_ref()
- .map(|record| record.config_path.clone());
- let detail = match target.runtime_group {
- ManagedRuntimeGroup::ActiveManagedTarget => {
- if config_path.is_some() {
- "runtime config show reports the managed config location without mutating bindings"
- .to_owned()
- } else {
- format!(
- "managed runtime `{}` has no registered instance config yet",
- target.runtime_id
- )
- }
- }
- ManagedRuntimeGroup::DefinedManagedTarget => format!(
- "runtime `{}` is only a defined future managed target; generic config surfaces are not admitted without a registered instance",
- target.runtime_id
- ),
- ManagedRuntimeGroup::BootstrapOnly => format!(
- "runtime `{}` remains bootstrap_only and direct-bindable in this wave; generic managed config is not admitted",
- target.runtime_id
- ),
- ManagedRuntimeGroup::Unknown => unknown_runtime_detail(target),
- };
+ let availability = managed_inspection_availability(target);
+ let config_path = target.instance_record.as_ref().and_then(|_| {
+ target
+ .predicted_paths
+ .as_ref()
+ .map(ManagedRuntimeInstancePaths::config_path)
+ });
+ let config_present = config_path.as_deref().is_some_and(Path::exists);
ManagedRuntimeInspection {
availability,
view: ManagedRuntimeConfigInspection {
- runtime_id: target.runtime_id.clone(),
- instance_id: target.instance_id.clone(),
- instance_source: target.instance_source.clone(),
+ runtime_id: target.context.service().to_string(),
+ instance_id: target.context.instance().to_string(),
+ instance_source: target.instance_source,
runtime_group: target.runtime_group.as_str().to_owned(),
state: match availability {
- ManagedRuntimeInspectionAvailability::Success => {
- if config_path.is_some() {
- "ready".to_owned()
- } else {
- "not_installed".to_owned()
- }
+ ManagedRuntimeInspectionAvailability::Success if config_path.is_some() => {
+ "ready".to_owned()
}
- ManagedRuntimeInspectionAvailability::Unconfigured => "unknown_runtime".to_owned(),
- ManagedRuntimeInspectionAvailability::Unsupported => "unsupported".to_owned(),
+ ManagedRuntimeInspectionAvailability::Success => "not_installed".to_owned(),
+ other => availability_state(other),
},
- source: "runtime management contract + shared instance registry".to_owned(),
- detail,
+ source: "runtime context + typed instance registry".to_owned(),
+ detail: config_detail(target, config_path.is_some()),
config_format: target
.bootstrap
.as_ref()
.map(|entry| entry.config_format.clone()),
- config_path: config_path.clone(),
- config_present: config_path.as_ref().is_some_and(|path| path.exists()),
+ config_present,
requires_bootstrap_secret: target
.bootstrap
.as_ref()
@@ -477,53 +528,43 @@ pub fn inspect_runtime_config(
}
}
+#[must_use]
pub fn inspect_runtime_action(
target: &ManagedRuntimeTarget,
action: ManagedRuntimeLifecycleAction,
- detail_override: Option<String>,
) -> ManagedRuntimeInspection<ManagedRuntimeActionInspection> {
- let (availability, state, detail, next_step) = match target.runtime_group {
+ let (availability, state, detail) = match target.runtime_group {
ManagedRuntimeGroup::ActiveManagedTarget => (
ManagedRuntimeInspectionAvailability::Unsupported,
"deferred",
- detail_override.unwrap_or_else(|| {
- format!(
- "runtime {} `{}` is not supported for this managed target",
- action.as_str().replace('_', " "),
- target.runtime_id
- )
- }),
- None,
+ format!(
+ "runtime {} `{}` is not supported for this managed target",
+ action.as_str().replace('_', " "),
+ target.context.service()
+ ),
),
ManagedRuntimeGroup::DefinedManagedTarget => (
ManagedRuntimeInspectionAvailability::Unsupported,
"unsupported",
- detail_override.unwrap_or_else(|| {
- format!(
- "runtime `{}` is only a defined future managed target; `{}` is not admitted in the current wave",
- target.runtime_id,
- action.as_str().replace('_', " ")
- )
- }),
- None,
+ format!(
+ "runtime `{}` is only a defined future managed target; `{}` is not admitted in the current wave",
+ target.context.service(),
+ action.as_str().replace('_', " ")
+ ),
),
ManagedRuntimeGroup::BootstrapOnly => (
ManagedRuntimeInspectionAvailability::Unsupported,
"unsupported",
- detail_override.unwrap_or_else(|| {
- format!(
- "runtime `{}` remains bootstrap_only and direct-bindable in this wave; generic managed `{}` is not admitted",
- target.runtime_id,
- action.as_str().replace('_', " ")
- )
- }),
- None,
+ format!(
+ "runtime `{}` remains bootstrap_only; generic managed `{}` is not admitted",
+ target.context.service(),
+ action.as_str().replace('_', " ")
+ ),
),
ManagedRuntimeGroup::Unknown => (
ManagedRuntimeInspectionAvailability::Unconfigured,
"unknown_runtime",
- detail_override.unwrap_or_else(|| unknown_runtime_detail(target)),
- None,
+ unknown_runtime_detail(target),
),
};
@@ -531,25 +572,51 @@ pub fn inspect_runtime_action(
availability,
view: ManagedRuntimeActionInspection {
action: action.as_str().to_owned(),
- runtime_id: target.runtime_id.clone(),
- instance_id: target.instance_id.clone(),
- instance_source: target.instance_source.clone(),
+ runtime_id: target.context.service().to_string(),
+ instance_id: target.context.instance().to_string(),
+ instance_source: target.instance_source,
runtime_group: target.runtime_group.as_str().to_owned(),
state: state.to_owned(),
source: "generic runtime-management command family".to_owned(),
detail,
mutates_bindings: false,
- next_step,
+ next_step: None,
},
}
}
+fn managed_inspection_availability(
+ target: &ManagedRuntimeTarget,
+) -> ManagedRuntimeInspectionAvailability {
+ match target.runtime_group {
+ ManagedRuntimeGroup::Unknown => ManagedRuntimeInspectionAvailability::Unconfigured,
+ ManagedRuntimeGroup::ActiveManagedTarget => ManagedRuntimeInspectionAvailability::Success,
+ ManagedRuntimeGroup::DefinedManagedTarget | ManagedRuntimeGroup::BootstrapOnly => {
+ if target.instance_record.is_some() {
+ ManagedRuntimeInspectionAvailability::Success
+ } else {
+ ManagedRuntimeInspectionAvailability::Unsupported
+ }
+ }
+ }
+}
+
+fn availability_state(availability: ManagedRuntimeInspectionAvailability) -> String {
+ match availability {
+ ManagedRuntimeInspectionAvailability::Success => "ready",
+ ManagedRuntimeInspectionAvailability::Unconfigured => "unknown_runtime",
+ ManagedRuntimeInspectionAvailability::Unsupported => "unsupported",
+ }
+ .to_owned()
+}
+
fn status_state(target: &ManagedRuntimeTarget) -> &'static str {
match target.runtime_group {
- ManagedRuntimeGroup::ActiveManagedTarget => match target.instance_record.as_ref() {
- Some(record) => install_state_label(record.install_state),
- None => "not_installed",
- },
+ ManagedRuntimeGroup::ActiveManagedTarget => target
+ .instance_record
+ .as_ref()
+ .map(|record| install_state_label(record.install_state()))
+ .unwrap_or("not_installed"),
ManagedRuntimeGroup::DefinedManagedTarget => "defined_not_active",
ManagedRuntimeGroup::BootstrapOnly => "bootstrap_only",
ManagedRuntimeGroup::Unknown => "unknown_runtime",
@@ -558,27 +625,61 @@ fn status_state(target: &ManagedRuntimeTarget) -> &'static str {
fn status_detail(target: &ManagedRuntimeTarget) -> String {
match target.runtime_group {
- ManagedRuntimeGroup::ActiveManagedTarget => match &target.instance_record {
- Some(record) => format!(
- "managed runtime `{}` instance `{}` is registered with config at {}",
- target.runtime_id,
- target.instance_id,
- record.config_path.display()
- ),
- None => format!(
- "managed runtime `{}` has no registered instance `{}` in {}",
- target.runtime_id,
- target.instance_id,
- target.registry_path.display()
- ),
- },
+ ManagedRuntimeGroup::ActiveManagedTarget if target.instance_record.is_some() => format!(
+ "managed runtime `{}` instance `{}` is registered",
+ target.context.service(),
+ target.context.instance()
+ ),
+ ManagedRuntimeGroup::ActiveManagedTarget => format!(
+ "managed runtime `{}` has no registered instance `{}`",
+ target.context.service(),
+ target.context.instance()
+ ),
ManagedRuntimeGroup::DefinedManagedTarget => format!(
- "runtime `{}` is defined in the management contract but not yet admitted as an active managed target",
- target.runtime_id
+ "runtime `{}` is defined but not yet an active managed target",
+ target.context.service()
),
ManagedRuntimeGroup::BootstrapOnly => format!(
- "runtime `{}` is bootstrap_only in the management contract and remains direct-bindable outside managed lifecycle in this wave",
- target.runtime_id
+ "runtime `{}` is bootstrap_only in the management contract",
+ target.context.service()
+ ),
+ ManagedRuntimeGroup::Unknown => unknown_runtime_detail(target),
+ }
+}
+
+fn logs_detail(target: &ManagedRuntimeTarget) -> String {
+ match target.runtime_group {
+ ManagedRuntimeGroup::ActiveManagedTarget => {
+ "runtime logs use manager-owned stdout/stderr tracking".to_owned()
+ }
+ ManagedRuntimeGroup::DefinedManagedTarget => format!(
+ "runtime `{}` is a defined future managed target",
+ target.context.service()
+ ),
+ ManagedRuntimeGroup::BootstrapOnly => format!(
+ "runtime `{}` is bootstrap_only; generic managed logs are not admitted",
+ target.context.service()
+ ),
+ ManagedRuntimeGroup::Unknown => unknown_runtime_detail(target),
+ }
+}
+
+fn config_detail(target: &ManagedRuntimeTarget, registered: bool) -> String {
+ match target.runtime_group {
+ ManagedRuntimeGroup::ActiveManagedTarget if registered => {
+ "runtime config is derived from the validated service context".to_owned()
+ }
+ ManagedRuntimeGroup::ActiveManagedTarget => format!(
+ "managed runtime `{}` has no registered instance config",
+ target.context.service()
+ ),
+ ManagedRuntimeGroup::DefinedManagedTarget => format!(
+ "runtime `{}` is a defined future managed target",
+ target.context.service()
+ ),
+ ManagedRuntimeGroup::BootstrapOnly => format!(
+ "runtime `{}` is bootstrap_only; generic managed config is not admitted",
+ target.context.service()
),
ManagedRuntimeGroup::Unknown => unknown_runtime_detail(target),
}
@@ -587,7 +688,7 @@ fn status_detail(target: &ManagedRuntimeTarget) -> String {
fn unknown_runtime_detail(target: &ManagedRuntimeTarget) -> String {
format!(
"runtime `{}` is not present in the current runtime-management contract",
- target.runtime_id
+ target.context.service()
)
}
@@ -598,28 +699,23 @@ fn infer_health_state(target: &ManagedRuntimeTarget) -> (&'static str, &'static
"registry_absent",
);
};
- if record.install_state == ManagedRuntimeInstallState::Failed {
+ if record.install_state() == ManagedRuntimeInstallState::Failed {
return (
health_state_label(ManagedRuntimeHealthState::Failed),
"registry_install_state",
);
}
-
- if let Some(paths) = target.predicted_paths.as_ref() {
- if crate::process_running(paths).unwrap_or(false) {
- return (
- health_state_label(ManagedRuntimeHealthState::Running),
- "process_probe",
- );
- }
- } else if record.run_path.join("runtime.pid").exists() {
+ if target
+ .predicted_paths
+ .as_ref()
+ .is_some_and(|paths| crate::process_running(paths).unwrap_or(false))
+ {
return (
health_state_label(ManagedRuntimeHealthState::Running),
- "pid_file_presence",
+ "process_probe",
);
}
-
- if record.install_state == ManagedRuntimeInstallState::NotInstalled {
+ if record.install_state() == ManagedRuntimeInstallState::NotInstalled {
(
health_state_label(ManagedRuntimeHealthState::NotInstalled),
"registry_install_state",
@@ -652,10 +748,7 @@ fn health_state_label(state: ManagedRuntimeHealthState) -> &'static str {
}
}
-fn path_present(path: Option<&PathBuf>) -> Option<bool> {
- path.map(|value| value.exists())
-}
-
+#[must_use]
pub fn runtime_group(
contract: &RadrootsRuntimeManagementContract,
runtime_id: &str,
@@ -688,652 +781,300 @@ pub fn runtime_group(
#[cfg(test)]
mod tests {
- use std::{
- fs,
- path::{Path, PathBuf},
- };
+ use std::fs;
use radroots_runtime_paths::{
- RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver,
- RadrootsPlatform, RadrootsRuntimePathSelection,
+ InstanceId, RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver,
+ RadrootsPlatform, RuntimeContext, RuntimeContextBootstrap, RuntimeContextSource, ServiceId,
};
use tempfile::tempdir;
use super::{
- ManagedRuntimeContext, ManagedRuntimeGroup, ManagedRuntimeInspectionAvailability,
- ManagedRuntimeLifecycleAction, active_management_mode_for_profile, health_state_label,
- inspect_runtime_action, inspect_runtime_config, inspect_runtime_logs,
- inspect_runtime_status, load_management_context, load_management_context_with_selection,
+ ManagedRuntimeGroup, ManagedRuntimeInspectionAvailability, ManagedRuntimeLifecycleAction,
+ active_management_mode_for_profile, inspect_runtime_action, inspect_runtime_config,
+ inspect_runtime_logs, inspect_runtime_status, load_management_context,
resolve_runtime_target, runtime_group,
};
- use crate::{
- ManagedRuntimeHealthState, ManagedRuntimeInstallState, ManagedRuntimeInstanceRecord,
- parse_contract_str,
- };
+ use crate::{ManagedRuntimeInstallState, RadrootsRuntimeManagerError, parse_contract_str};
const CONTRACT: &str = r#"
schema = "radroots-runtime-management"
schema_version = 1
-owner_doc = "docs/execution/rcl/radroots-modular-runtime-management-bootstrap-rcl.md"
+owner_doc = "owner"
runtime_registry = "registry.toml"
distribution_contract = "distribution.toml"
capabilities_contract = "capabilities.toml"
[defaults]
-instance_cardinality = "single_default_instance"
-managed_runtime_lookup = "shared_instance_registry"
+instance_cardinality = "multiple"
+managed_runtime_lookup = "typed_instance_registry"
explicit_runtime_endpoint_overrides_precede_managed_instance_binding = true
global_path_mutation_forbidden = true
[management_clients]
active = ["cli"]
-defined = []
[managed_runtime_targets]
active = ["radrootsd"]
-defined = ["myc"]
+defined = ["myc", "rhi"]
bootstrap_only = ["hyf"]
[lifecycle]
actions = ["install", "start"]
-destructive_actions = []
health_states = ["not_installed", "running"]
[mode.interactive_user_managed]
contract_state = "active"
platforms = ["linux"]
-supported_profiles = ["interactive_user", "repo_local"]
+supported_profiles = ["repo_local"]
service_manager_integration = false
- uses_absolute_binary_paths = true
- default_instance_cardinality = "single_default_instance"
-
- [mode.service_host_managed]
- contract_state = "defined"
- platforms = ["linux"]
- 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"
+uses_absolute_binary_paths = true
+default_instance_cardinality = "multiple"
+
+[mode.service_host_managed]
+contract_state = "defined"
+platforms = ["linux"]
+supported_profiles = ["service_host"]
+service_manager_integration = true
+uses_absolute_binary_paths = true
+default_instance_cardinality = "multiple"
+
+[paths.interactive_user_managed]
+shared_namespace = "obsolete"
+instance_registry_root_class = "config"
+instance_registry_rel = "obsolete"
+artifact_cache_root_class = "cache"
+artifact_cache_rel = "obsolete"
install_root_class = "data"
-install_root_rel = "shared/runtime-manager/installs"
+install_root_rel = "obsolete"
state_root_class = "data"
-state_root_rel = "shared/runtime-manager/state"
+state_root_rel = "obsolete"
logs_root_class = "logs"
-logs_root_rel = "shared/runtime-manager"
+logs_root_rel = "obsolete"
run_root_class = "run"
-run_root_rel = "shared/runtime-manager"
+run_root_rel = "obsolete"
secrets_root_class = "secrets"
-secrets_namespace_rel = "shared/runtime-manager"
+secrets_namespace_rel = "obsolete"
[instance_metadata]
-required_fields = ["runtime_id"]
-optional_fields = ["notes"]
+required_fields = ["service_id", "instance_id"]
- [bootstrap.radrootsd]
- runtime_id = "radrootsd"
- management_mode = "interactive_user_managed"
- default_instance_id = "local"
+[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
- "#;
-
- fn resolver_for_home(home_dir: PathBuf) -> RadrootsPathResolver {
- RadrootsPathResolver::new(
- RadrootsPlatform::Linux,
- RadrootsHostEnvironment {
- home_dir: Some(home_dir),
- xdg_runtime_dir: Some(PathBuf::from("/run/user/1000")),
- ..RadrootsHostEnvironment::default()
- },
+preferred_cli_binding = true
+"#;
+
+ fn context(service: &str, instance: &str, root: &std::path::Path) -> RuntimeContext {
+ RuntimeContext::resolve(
+ &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()),
+ RuntimeContextBootstrap::new(
+ RadrootsPathProfile::RepoLocal,
+ Some(root.to_path_buf()),
+ RuntimeContextSource::BootstrapCli,
+ RuntimeContextSource::BootstrapCli,
+ )
+ .expect("bootstrap"),
+ ServiceId::new(service).expect("service"),
+ InstanceId::new(instance).expect("instance"),
)
+ .expect("context")
}
- fn repo_local_context(root: &Path) -> ManagedRuntimeContext {
- let contract = parse_contract_str(CONTRACT).expect("contract");
- let resolver =
- RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default());
+ fn manager(root: &std::path::Path) -> super::ManagedRuntimeContext {
load_management_context(
- contract,
- &resolver,
- RadrootsPathProfile::RepoLocal,
- &RadrootsPathOverrides::repo_local(root),
+ parse_contract_str(CONTRACT).expect("contract"),
+ context("runtime-manager", "default", root),
)
- .expect("context")
- }
-
- fn sample_record(
- runtime_id: &str,
- instance_id: &str,
- install_state: ManagedRuntimeInstallState,
- root: &Path,
- ) -> ManagedRuntimeInstanceRecord {
- let instance_root = root.join(runtime_id).join(instance_id);
- ManagedRuntimeInstanceRecord {
- runtime_id: runtime_id.to_owned(),
- instance_id: instance_id.to_owned(),
- management_mode: "interactive_user_managed".to_owned(),
- install_state,
- binary_path: instance_root.join("bin/runtime"),
- config_path: instance_root.join("config/runtime.toml"),
- logs_path: instance_root.join("logs"),
- run_path: instance_root.join("run"),
- installed_version: "1.0.0-alpha.1".to_owned(),
- health_endpoint: Some("jsonrpc_status".to_owned()),
- secret_material_ref: None,
- last_started_at: None,
- last_stopped_at: None,
- notes: Some("managed test record".to_owned()),
- }
+ .expect("manager")
}
#[test]
- fn active_management_mode_matches_supported_profile() {
- let contract = parse_contract_str(CONTRACT).expect("contract");
- let mode_id =
- active_management_mode_for_profile(&contract, RadrootsPathProfile::InteractiveUser)
- .expect("mode");
- assert_eq!(mode_id, "interactive_user_managed");
- }
-
- #[test]
- fn active_management_mode_rejects_profiles_without_active_mode() {
- let contract = parse_contract_str(CONTRACT).expect("contract");
- let err = active_management_mode_for_profile(&contract, RadrootsPathProfile::ServiceHost)
- .expect_err("service host mode is defined but inactive");
-
- assert!(err.to_string().contains("service_host"));
- }
-
- #[test]
- fn management_context_reports_selection_and_context_errors() {
+ fn manager_loads_from_its_context_and_rejects_inactive_profiles() {
let dir = tempdir().expect("tempdir");
- let contract = parse_contract_str(CONTRACT).expect("contract");
- let resolver =
- RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default());
- let selection = RadrootsRuntimePathSelection::caller(RadrootsPathProfile::RepoLocal, None);
- let err = load_management_context_with_selection(contract.clone(), &resolver, &selection)
- .expect_err("repo local selection without root should fail");
- assert!(err.to_string().contains("repo_local"));
-
- let err = load_management_context(
- contract.clone(),
- &resolver,
- RadrootsPathProfile::ServiceHost,
- &RadrootsPathOverrides::default(),
- )
- .expect_err("service host mode is inactive");
- assert!(err.to_string().contains("service_host"));
-
- let root = dir.path().join("runtime-root");
- let overrides = RadrootsPathOverrides::repo_local(&root);
- fs::create_dir_all(root.join("config/shared/runtime-manager")).expect("registry parent");
- fs::create_dir(root.join("config/shared/runtime-manager/instances.toml"))
- .expect("registry directory");
- let err = load_management_context(
- contract,
- &resolver,
- RadrootsPathProfile::RepoLocal,
- &overrides,
- )
- .expect_err("directory registry path should fail");
- assert!(err.to_string().contains("read runtime instance registry"));
- }
-
- #[test]
- fn resolve_runtime_target_uses_bootstrap_default_instance_id() {
- let contract = parse_contract_str(CONTRACT).expect("contract");
- let resolver = resolver_for_home(PathBuf::from("/home/treesap"));
- let mut context = load_management_context(
- contract,
- &resolver,
- RadrootsPathProfile::InteractiveUser,
- &RadrootsPathOverrides::default(),
- )
- .expect("context");
- context
- .registry
- .instances
- .push(ManagedRuntimeInstanceRecord {
- runtime_id: "radrootsd".to_owned(),
- instance_id: "local".to_owned(),
- management_mode: "interactive_user_managed".to_owned(),
- install_state: ManagedRuntimeInstallState::Configured,
- binary_path: PathBuf::from("/tmp/bin/radrootsd"),
- config_path: PathBuf::from("/tmp/config.toml"),
- logs_path: PathBuf::from("/tmp/logs"),
- run_path: PathBuf::from("/tmp/run"),
- installed_version: "1.0.0-alpha.1".to_owned(),
- health_endpoint: None,
- secret_material_ref: None,
- last_started_at: None,
- last_stopped_at: None,
- notes: None,
- });
-
- let target = resolve_runtime_target(&context, "radrootsd", None);
- assert_eq!(target.instance_id, "local");
- assert_eq!(target.instance_source, "bootstrap_default");
+ let manager = manager(dir.path());
assert_eq!(
- target.runtime_group,
- ManagedRuntimeGroup::ActiveManagedTarget
+ manager.manager_context().service().as_str(),
+ "runtime-manager"
);
- assert!(target.predicted_paths.is_some());
- }
-
- #[test]
- fn load_context_with_selection_uses_caller_path_selection() {
- let dir = tempdir().expect("tempdir");
- let root = dir.path().join("runtime-root");
- let contract = parse_contract_str(CONTRACT).expect("contract");
- let resolver =
- RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default());
- let selection = RadrootsRuntimePathSelection::caller(
- RadrootsPathProfile::RepoLocal,
- Some(root.clone()),
- );
-
- let context = load_management_context_with_selection(contract, &resolver, &selection)
- .expect("selection context");
-
- assert_eq!(
- context.shared_paths.instance_registry_path,
- root.join("config/shared/runtime-manager/instances.toml")
+ assert!(manager.registry().instances.is_empty());
+ assert!(
+ manager
+ .shared_paths()
+ .instance_registry_path()
+ .ends_with("services/runtime-manager/default/instances.toml")
);
- assert!(context.registry.instances.is_empty());
- }
- #[test]
- fn runtime_groups_and_action_labels_cover_declared_surfaces() {
let contract = parse_contract_str(CONTRACT).expect("contract");
assert_eq!(
- runtime_group(&contract, "radrootsd"),
- ManagedRuntimeGroup::ActiveManagedTarget
- );
- assert_eq!(
- runtime_group(&contract, "myc"),
- ManagedRuntimeGroup::DefinedManagedTarget
- );
- assert_eq!(
- runtime_group(&contract, "hyf"),
- ManagedRuntimeGroup::BootstrapOnly
- );
- assert_eq!(
- runtime_group(&contract, "unknown"),
- ManagedRuntimeGroup::Unknown
- );
-
- assert_eq!(
- ManagedRuntimeGroup::ActiveManagedTarget.as_str(),
- "active_managed_target"
+ active_management_mode_for_profile(&contract, RadrootsPathProfile::RepoLocal)
+ .expect("active mode"),
+ "interactive_user_managed"
);
- assert_eq!(
- ManagedRuntimeGroup::DefinedManagedTarget.posture(),
- "defined_future_target"
- );
- assert_eq!(
- ManagedRuntimeGroup::BootstrapOnly.posture(),
- "bootstrap_only_direct_binding"
+ assert!(
+ active_management_mode_for_profile(&contract, RadrootsPathProfile::ServiceHost)
+ .is_err()
);
- assert_eq!(ManagedRuntimeGroup::Unknown.as_str(), "unknown");
-
- let actions = [
- (ManagedRuntimeLifecycleAction::Install, "install"),
- (ManagedRuntimeLifecycleAction::Uninstall, "uninstall"),
- (ManagedRuntimeLifecycleAction::Start, "start"),
- (ManagedRuntimeLifecycleAction::Stop, "stop"),
- (ManagedRuntimeLifecycleAction::Restart, "restart"),
- (ManagedRuntimeLifecycleAction::ConfigSet, "config_set"),
- ];
- for (action, expected) in actions {
- assert_eq!(action.as_str(), expected);
- }
}
#[test]
- fn resolve_runtime_target_covers_requested_and_non_active_sources() {
+ fn targets_bind_exact_typed_contexts_and_multi_instance_records() {
let dir = tempdir().expect("tempdir");
- let mut context = repo_local_context(dir.path());
- context.registry.instances.push(sample_record(
- "myc",
- "default",
- ManagedRuntimeInstallState::Configured,
- dir.path(),
- ));
-
- let requested = resolve_runtime_target(&context, "radrootsd", Some("manual"));
- assert_eq!(requested.instance_id, "manual");
- assert_eq!(requested.instance_source, "command_arg");
- assert_eq!(
- requested.runtime_group,
- ManagedRuntimeGroup::ActiveManagedTarget
+ let mut manager = manager(dir.path());
+ let primary = context("myc", "primary", dir.path());
+ let secondary = context("myc", "secondary", dir.path());
+ manager
+ .register_instance(&primary, ManagedRuntimeInstallState::Configured)
+ .expect("register primary");
+
+ let primary_target = resolve_runtime_target(&manager, primary.clone()).expect("primary");
+ let secondary_target =
+ resolve_runtime_target(&manager, secondary.clone()).expect("secondary");
+ assert!(primary_target.instance_record.is_some());
+ assert!(secondary_target.instance_record.is_none());
+ assert_eq!(primary_target.context, primary);
+ assert_eq!(secondary_target.context, secondary);
+ assert_ne!(
+ primary_target.predicted_paths,
+ secondary_target.predicted_paths
);
- assert!(requested.predicted_paths.is_some());
-
- let defined = resolve_runtime_target(&context, "myc", None);
- assert_eq!(defined.instance_id, "default");
- assert_eq!(defined.instance_source, "implicit_default");
assert_eq!(
- defined.runtime_group,
+ primary_target.runtime_group,
ManagedRuntimeGroup::DefinedManagedTarget
);
- assert!(defined.predicted_paths.is_none());
- assert!(defined.instance_record.is_some());
-
- let bootstrap = resolve_runtime_target(&context, "hyf", None);
- assert_eq!(bootstrap.instance_source, "implicit_default");
- assert_eq!(bootstrap.runtime_group, ManagedRuntimeGroup::BootstrapOnly);
- assert!(bootstrap.management_mode.is_none());
-
- let unknown = resolve_runtime_target(&context, "unknown", Some("manual"));
- assert_eq!(unknown.instance_id, "manual");
- assert_eq!(unknown.instance_source, "command_arg");
- assert_eq!(unknown.runtime_group, ManagedRuntimeGroup::Unknown);
- assert!(unknown.predicted_paths.is_none());
+ assert!(primary_target.predicted_paths.is_some());
}
#[test]
- fn status_inspection_covers_install_and_health_states() {
- let dir = tempdir().expect("tempdir");
- let context = repo_local_context(dir.path());
- let active_missing = resolve_runtime_target(&context, "radrootsd", None);
- let status =
- inspect_runtime_status(&active_missing, &["install".to_owned(), "start".to_owned()]);
- assert_eq!(
- status.availability,
- ManagedRuntimeInspectionAvailability::Success
- );
- assert_eq!(status.view.state, "not_installed");
- assert_eq!(status.view.health_state, "not_installed");
- assert_eq!(status.view.health_source, "registry_absent");
- assert_eq!(status.view.lifecycle_actions, ["install", "start"]);
-
- let mut context = repo_local_context(dir.path());
- context.registry.instances.push(sample_record(
- "radrootsd",
- "local",
- ManagedRuntimeInstallState::Configured,
- dir.path(),
+ fn manager_rejects_same_identity_from_a_different_root_scope() {
+ let first = tempdir().expect("first");
+ let second = tempdir().expect("second");
+ let mut manager = manager(first.path());
+ let mismatched = context("myc", "primary", second.path());
+
+ assert!(matches!(
+ manager.register_instance(&mismatched, ManagedRuntimeInstallState::Configured),
+ Err(RadrootsRuntimeManagerError::RuntimeContextMismatch)
));
- let active_configured = resolve_runtime_target(&context, "radrootsd", None);
- let configured_status = inspect_runtime_status(&active_configured, &[]);
- assert_eq!(configured_status.view.state, "configured");
- assert_eq!(configured_status.view.health_state, "stopped");
- assert_eq!(configured_status.view.health_source, "pid_file_absent");
- assert_eq!(configured_status.view.install_state, "configured");
-
- let predicted = active_configured
- .predicted_paths
- .as_ref()
- .expect("predicted active paths");
- fs::create_dir_all(&predicted.run_dir).expect("run dir");
- fs::write(&predicted.pid_file_path, std::process::id().to_string()).expect("pid");
- let running_status = inspect_runtime_status(&active_configured, &[]);
- assert_eq!(running_status.view.health_state, "running");
- assert_eq!(running_status.view.health_source, "process_probe");
- fs::remove_file(&predicted.pid_file_path).expect("remove pid");
-
- let mut context = repo_local_context(dir.path());
- context.registry.instances.push(sample_record(
- "radrootsd",
- "local",
- ManagedRuntimeInstallState::Failed,
- dir.path(),
- ));
- let active_failed = resolve_runtime_target(&context, "radrootsd", None);
- let failed_status = inspect_runtime_status(&active_failed, &[]);
- assert_eq!(failed_status.view.state, "failed");
- assert_eq!(failed_status.view.health_state, "failed");
- assert_eq!(failed_status.view.health_source, "registry_install_state");
-
- let mut context = repo_local_context(dir.path());
- context.registry.instances.push(sample_record(
- "radrootsd",
- "local",
- ManagedRuntimeInstallState::NotInstalled,
- dir.path(),
- ));
- let active_not_installed = resolve_runtime_target(&context, "radrootsd", None);
- let not_installed_status = inspect_runtime_status(&active_not_installed, &[]);
- assert_eq!(not_installed_status.view.health_state, "not_installed");
- assert_eq!(
- not_installed_status.view.health_source,
- "registry_install_state"
- );
-
- let mut context = repo_local_context(dir.path());
- let defined_record = sample_record(
- "myc",
- "default",
- ManagedRuntimeInstallState::Installed,
- dir.path(),
- );
- fs::create_dir_all(&defined_record.run_path).expect("run dir");
- fs::write(defined_record.run_path.join("runtime.pid"), "42").expect("pid");
- context.registry.instances.push(defined_record);
- let defined = resolve_runtime_target(&context, "myc", None);
- let defined_status = inspect_runtime_status(&defined, &["install".to_owned()]);
- assert_eq!(defined_status.view.state, "defined_not_active");
- assert_eq!(defined_status.view.health_state, "running");
- assert_eq!(defined_status.view.health_source, "pid_file_presence");
- assert!(defined_status.view.lifecycle_actions.is_empty());
-
- let no_pid_dir = tempdir().expect("no-pid tempdir");
- let mut context = repo_local_context(no_pid_dir.path());
- context.registry.instances.push(sample_record(
- "myc",
- "default",
- ManagedRuntimeInstallState::Installed,
- no_pid_dir.path(),
+ assert!(matches!(
+ resolve_runtime_target(&manager, mismatched),
+ Err(RadrootsRuntimeManagerError::RuntimeContextMismatch)
));
- let defined_without_pid = resolve_runtime_target(&context, "myc", None);
- let defined_without_pid_status = inspect_runtime_status(&defined_without_pid, &[]);
- assert_eq!(defined_without_pid_status.view.health_state, "stopped");
- assert_eq!(
- defined_without_pid_status.view.health_source,
- "pid_file_absent"
- );
-
- let bootstrap = resolve_runtime_target(&context, "hyf", None);
- let bootstrap_status = inspect_runtime_status(&bootstrap, &[]);
- assert_eq!(bootstrap_status.view.state, "bootstrap_only");
- assert_eq!(
- bootstrap_status.view.management_posture,
- "bootstrap_only_direct_binding"
- );
- assert_eq!(
- health_state_label(ManagedRuntimeHealthState::Starting),
- "starting"
- );
- assert_eq!(
- health_state_label(ManagedRuntimeHealthState::Degraded),
- "degraded"
- );
-
- let unknown = resolve_runtime_target(&context, "unknown", None);
- let unknown_status = inspect_runtime_status(&unknown, &[]);
- assert_eq!(
- unknown_status.availability,
- ManagedRuntimeInspectionAvailability::Unconfigured
- );
- assert_eq!(unknown_status.view.state, "unknown_runtime");
+ assert!(manager.registry().instances().is_empty());
}
#[test]
- fn logs_and_config_inspections_cover_availability_paths() {
+ fn groups_and_unknown_targets_remain_contract_controlled() {
let dir = tempdir().expect("tempdir");
- let mut context = repo_local_context(dir.path());
- context.registry.instances.push(sample_record(
- "radrootsd",
- "local",
- ManagedRuntimeInstallState::Configured,
- dir.path(),
- ));
- let active = resolve_runtime_target(&context, "radrootsd", None);
- let predicted = active.predicted_paths.as_ref().expect("predicted paths");
- fs::create_dir_all(&predicted.logs_dir).expect("predicted logs dir");
- fs::write(&predicted.stdout_log_path, "stdout").expect("stdout");
- fs::write(&predicted.stderr_log_path, "stderr").expect("stderr");
- let config_path = active
- .instance_record
- .as_ref()
- .expect("record")
- .config_path
- .clone();
- fs::create_dir_all(config_path.parent().expect("config parent")).expect("config parent");
- fs::write(&config_path, "listen = true").expect("config");
-
- let active_logs = inspect_runtime_logs(&active);
- assert_eq!(
- active_logs.availability,
- ManagedRuntimeInspectionAvailability::Success
- );
- assert_eq!(active_logs.view.state, "ready");
- assert!(active_logs.view.stdout_log_present);
- assert!(active_logs.view.stderr_log_present);
- assert!(active_logs.view.stdout_log_path.is_some());
-
- let active_config = inspect_runtime_config(&active);
- assert_eq!(active_config.view.state, "ready");
- assert!(active_config.view.config_present);
- assert_eq!(active_config.view.config_format.as_deref(), Some("toml"));
- assert_eq!(active_config.view.requires_bootstrap_secret, Some(true));
- assert_eq!(active_config.view.requires_config_bootstrap, Some(true));
- assert_eq!(active_config.view.requires_signer_provider, Some(false));
-
- let empty_dir = tempdir().expect("empty tempdir");
- let empty_context = repo_local_context(empty_dir.path());
- let active_missing = resolve_runtime_target(&empty_context, "radrootsd", None);
- let missing_logs = inspect_runtime_logs(&active_missing);
- assert_eq!(missing_logs.view.state, "ready");
- assert!(!missing_logs.view.stdout_log_present);
- assert!(!missing_logs.view.stderr_log_present);
- let missing_config = inspect_runtime_config(&active_missing);
- assert_eq!(missing_config.view.state, "not_installed");
- assert!(!missing_config.view.config_present);
-
- let mut context = repo_local_context(dir.path());
- let defined_record = sample_record(
- "myc",
- "default",
- ManagedRuntimeInstallState::Configured,
- dir.path(),
- );
- fs::create_dir_all(&defined_record.logs_path).expect("defined logs dir");
- fs::write(defined_record.logs_path.join("stdout.log"), "stdout").expect("defined stdout");
- fs::create_dir_all(defined_record.config_path.parent().expect("config parent"))
- .expect("defined config parent");
- fs::write(&defined_record.config_path, "enabled = true").expect("defined config");
- context.registry.instances.push(defined_record);
- let defined = resolve_runtime_target(&context, "myc", None);
- let defined_logs = inspect_runtime_logs(&defined);
- assert_eq!(
- defined_logs.availability,
- ManagedRuntimeInspectionAvailability::Success
- );
- assert_eq!(defined_logs.view.state, "ready");
- assert!(defined_logs.view.stdout_log_present);
- assert!(!defined_logs.view.stderr_log_present);
- assert!(defined_logs.view.stdout_log_path.is_none());
- let defined_config = inspect_runtime_config(&defined);
- assert_eq!(
- defined_config.availability,
- ManagedRuntimeInspectionAvailability::Success
- );
- assert_eq!(defined_config.view.state, "ready");
- assert!(defined_config.view.config_present);
-
- let defined_without_record = resolve_runtime_target(&empty_context, "myc", None);
+ let manager = manager(dir.path());
+ let contract = manager.contract();
assert_eq!(
- inspect_runtime_logs(&defined_without_record).availability,
- ManagedRuntimeInspectionAvailability::Unsupported
+ runtime_group(contract, "radrootsd"),
+ ManagedRuntimeGroup::ActiveManagedTarget
);
assert_eq!(
- inspect_runtime_config(&defined_without_record).availability,
- ManagedRuntimeInspectionAvailability::Unsupported
+ runtime_group(contract, "myc"),
+ ManagedRuntimeGroup::DefinedManagedTarget
);
-
- let bootstrap = resolve_runtime_target(&empty_context, "hyf", None);
assert_eq!(
- inspect_runtime_logs(&bootstrap).availability,
- ManagedRuntimeInspectionAvailability::Unsupported
+ runtime_group(contract, "hyf"),
+ ManagedRuntimeGroup::BootstrapOnly
);
assert_eq!(
- inspect_runtime_config(&bootstrap).availability,
- ManagedRuntimeInspectionAvailability::Unsupported
+ runtime_group(contract, "unknown"),
+ ManagedRuntimeGroup::Unknown
);
- let unknown = resolve_runtime_target(&empty_context, "unknown", None);
- assert_eq!(
- inspect_runtime_logs(&unknown).availability,
- ManagedRuntimeInspectionAvailability::Unconfigured
- );
+ let unknown = resolve_runtime_target(&manager, context("unknown", "default", dir.path()))
+ .expect("unknown target");
+ assert!(unknown.predicted_paths.is_none());
assert_eq!(
- inspect_runtime_config(&unknown).availability,
+ inspect_runtime_status(&unknown, &[]).availability,
ManagedRuntimeInspectionAvailability::Unconfigured
);
}
#[test]
- fn action_inspection_covers_all_group_postures() {
+ fn status_uses_manager_tracking_without_disclosing_paths() {
let dir = tempdir().expect("tempdir");
- let context = repo_local_context(dir.path());
- let active = resolve_runtime_target(&context, "radrootsd", None);
- let defined = resolve_runtime_target(&context, "myc", None);
- let bootstrap = resolve_runtime_target(&context, "hyf", None);
- let unknown = resolve_runtime_target(&context, "unknown", None);
-
- let active_install =
- inspect_runtime_action(&active, ManagedRuntimeLifecycleAction::Install, None);
+ let mut manager = manager(dir.path());
+ let service = context("radrootsd", "local", dir.path());
+ manager
+ .register_instance(&service, ManagedRuntimeInstallState::Configured)
+ .expect("register service");
+ let target = resolve_runtime_target(&manager, service).expect("target");
+ let paths = target.predicted_paths.as_ref().expect("paths");
+ fs::create_dir_all(paths.run_dir()).expect("run dir");
+ fs::write(paths.pid_file_path(), std::process::id().to_string()).expect("pid");
+
+ let status = inspect_runtime_status(&target, &["start".to_owned()]);
+ assert_eq!(status.view.health_state, "running");
+ assert_eq!(status.view.health_source, "process_probe");
assert_eq!(
- active_install.availability,
- ManagedRuntimeInspectionAvailability::Unsupported
- );
- assert_eq!(active_install.view.state, "deferred");
- assert!(active_install.view.detail.contains("runtime install"));
- assert!(!active_install.view.mutates_bindings);
- assert!(active_install.view.next_step.is_none());
-
- let overridden = inspect_runtime_action(
- &active,
- ManagedRuntimeLifecycleAction::ConfigSet,
- Some("custom detail".to_owned()),
- );
- assert_eq!(overridden.view.action, "config_set");
- assert_eq!(overridden.view.detail, "custom detail");
-
- let defined_start =
- inspect_runtime_action(&defined, ManagedRuntimeLifecycleAction::Start, None);
- assert_eq!(defined_start.view.state, "unsupported");
- assert!(
- defined_start
- .view
- .detail
- .contains("defined future managed target")
+ status.view.instance_source,
+ RuntimeContextSource::BootstrapCli
);
+ let rendered = format!("{status:?}");
+ assert!(!rendered.contains(dir.path().to_string_lossy().as_ref()));
+ assert!(!status.view.detail.contains('/'));
+ }
- let bootstrap_stop =
- inspect_runtime_action(&bootstrap, ManagedRuntimeLifecycleAction::Stop, None);
- assert_eq!(bootstrap_stop.view.state, "unsupported");
- assert!(bootstrap_stop.view.detail.contains("bootstrap_only"));
+ #[test]
+ fn log_and_config_inspections_use_manager_and_service_context_paths() {
+ let dir = tempdir().expect("tempdir");
+ let mut manager = manager(dir.path());
+ let service = context("radrootsd", "local", dir.path());
+ manager
+ .register_instance(&service, ManagedRuntimeInstallState::Configured)
+ .expect("register service");
+ let target = resolve_runtime_target(&manager, service).expect("target");
+ let paths = target.predicted_paths.as_ref().expect("paths");
+ fs::create_dir_all(paths.logs_dir()).expect("logs");
+ fs::write(paths.stdout_log_path(), "stdout").expect("stdout");
+ let config_path = paths.config_path();
+ fs::create_dir_all(config_path.parent().expect("config parent")).expect("config parent");
+ fs::write(&config_path, "enabled = true").expect("config");
+
+ let logs = inspect_runtime_logs(&target);
+ assert!(logs.view.stdout_log_present);
+ assert!(!logs.view.stderr_log_present);
+ let config = inspect_runtime_config(&target);
+ assert!(config.view.config_present);
+ assert_eq!(config.view.config_format.as_deref(), Some("toml"));
+ for rendered in [format!("{logs:?}"), format!("{config:?}")] {
+ assert!(!rendered.contains(dir.path().to_string_lossy().as_ref()));
+ }
+ }
- let unknown_restart =
- inspect_runtime_action(&unknown, ManagedRuntimeLifecycleAction::Restart, None);
- assert_eq!(
- unknown_restart.availability,
- ManagedRuntimeInspectionAvailability::Unconfigured
- );
- assert_eq!(unknown_restart.view.state, "unknown_runtime");
+ #[test]
+ fn actions_do_not_mutate_bindings_for_any_group() {
+ let dir = tempdir().expect("tempdir");
+ let manager = manager(dir.path());
+ for (service, expected) in [
+ (
+ "radrootsd",
+ ManagedRuntimeInspectionAvailability::Unsupported,
+ ),
+ ("myc", ManagedRuntimeInspectionAvailability::Unsupported),
+ ("hyf", ManagedRuntimeInspectionAvailability::Unsupported),
+ (
+ "unknown",
+ ManagedRuntimeInspectionAvailability::Unconfigured,
+ ),
+ ] {
+ let target = resolve_runtime_target(&manager, context(service, "default", dir.path()))
+ .expect("target");
+ let action = inspect_runtime_action(&target, ManagedRuntimeLifecycleAction::ConfigSet);
+ assert_eq!(action.availability, expected);
+ assert!(!action.view.mutates_bindings);
+ assert!(action.view.next_step.is_none());
+ }
}
}
diff --git a/crates/runtime_manager/src/model.rs b/crates/runtime_manager/src/model.rs
@@ -1,7 +1,6 @@
-use std::collections::BTreeMap;
-use std::path::PathBuf;
-
+use radroots_runtime_paths::{InstanceId, RuntimeContext, ServiceId};
use serde::{Deserialize, Serialize};
+use std::collections::BTreeMap;
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct RadrootsRuntimeManagementContract {
@@ -125,37 +124,112 @@ pub enum ManagedRuntimeHealthState {
Failed,
}
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+/// Sealed registry state for one typed service instance.
+///
+/// ```compile_fail
+/// use radroots_runtime_manager::ManagedRuntimeInstanceRecord;
+///
+/// let _ = ManagedRuntimeInstanceRecord {
+/// service_id: todo!(),
+/// instance_id: todo!(),
+/// install_state: todo!(),
+/// };
+/// ```
+#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(deny_unknown_fields)]
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>,
+ service_id: ServiceId,
+ instance_id: InstanceId,
+ install_state: ManagedRuntimeInstallState,
}
+impl ManagedRuntimeInstanceRecord {
+ #[must_use]
+ pub(crate) fn new(context: &RuntimeContext, install_state: ManagedRuntimeInstallState) -> Self {
+ Self {
+ service_id: context.service().clone(),
+ instance_id: context.instance().clone(),
+ install_state,
+ }
+ }
+
+ #[must_use]
+ pub fn service_id(&self) -> &ServiceId {
+ &self.service_id
+ }
+
+ #[must_use]
+ pub fn instance_id(&self) -> &InstanceId {
+ &self.instance_id
+ }
+
+ #[must_use]
+ pub fn install_state(&self) -> ManagedRuntimeInstallState {
+ self.install_state
+ }
+
+ #[must_use]
+ pub fn matches_context(&self, context: &RuntimeContext) -> bool {
+ self.service_id == *context.service() && self.instance_id == *context.instance()
+ }
+}
+
+impl core::fmt::Debug for ManagedRuntimeInstanceRecord {
+ fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ formatter
+ .debug_struct("ManagedRuntimeInstanceRecord")
+ .field("service_id", &self.service_id)
+ .field("instance_id", &self.instance_id)
+ .field("install_state", &self.install_state)
+ .finish()
+ }
+}
+
+pub const RUNTIME_INSTANCE_REGISTRY_SCHEMA: &str = "radroots.service-instance-registry";
+pub const RUNTIME_INSTANCE_REGISTRY_VERSION: u32 = 1;
+
+/// A sealed, schema-fixed, normalized instance registry.
+///
+/// ```compile_fail
+/// use radroots_runtime_manager::ManagedRuntimeInstanceRegistry;
+///
+/// let _ = ManagedRuntimeInstanceRegistry {
+/// schema: "wrong".to_owned(),
+/// schema_version: 99,
+/// instances: Vec::new(),
+/// };
+/// ```
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(deny_unknown_fields)]
pub struct ManagedRuntimeInstanceRegistry {
- pub schema: String,
- pub schema_version: u32,
+ pub(crate) schema: String,
+ pub(crate) schema_version: u32,
#[serde(default)]
- pub instances: Vec<ManagedRuntimeInstanceRecord>,
+ pub(crate) instances: Vec<ManagedRuntimeInstanceRecord>,
+}
+
+impl ManagedRuntimeInstanceRegistry {
+ #[must_use]
+ pub fn schema(&self) -> &str {
+ &self.schema
+ }
+
+ #[must_use]
+ pub fn schema_version(&self) -> u32 {
+ self.schema_version
+ }
+
+ #[must_use]
+ pub fn instances(&self) -> &[ManagedRuntimeInstanceRecord] {
+ &self.instances
+ }
}
impl Default for ManagedRuntimeInstanceRegistry {
fn default() -> Self {
Self {
- schema: "radroots_runtime-instance-registry".to_string(),
- schema_version: 1,
+ schema: RUNTIME_INSTANCE_REGISTRY_SCHEMA.to_string(),
+ schema_version: RUNTIME_INSTANCE_REGISTRY_VERSION,
instances: Vec::new(),
}
}
diff --git a/crates/runtime_manager/src/paths.rs b/crates/runtime_manager/src/paths.rs
@@ -1,101 +1,208 @@
-use std::path::PathBuf;
+use core::fmt;
+use std::path::{Path, PathBuf};
-use radroots_runtime_paths::{
- RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, RadrootsPaths,
-};
+use radroots_runtime_paths::{RuntimeContext, default_service_instance_artifacts};
use crate::error::RadrootsRuntimeManagerError;
use crate::model::RadrootsRuntimeManagementContract;
-#[derive(Debug, Clone, PartialEq, Eq)]
+/// Manager-owned paths derived from the manager's validated service context.
+///
+/// External callers cannot forge another root set:
+///
+/// ```compile_fail
+/// use std::path::PathBuf;
+/// use radroots_runtime_manager::ManagedRuntimeSharedPaths;
+///
+/// let _ = ManagedRuntimeSharedPaths {
+/// instance_registry_path: PathBuf::from("/tmp/escape"),
+/// artifact_cache_dir: PathBuf::from("/tmp/escape"),
+/// install_root: PathBuf::from("/tmp/escape"),
+/// };
+/// ```
+#[derive(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,
+ instance_registry_path: PathBuf,
+ artifact_cache_dir: PathBuf,
+ install_root: PathBuf,
+ logs_root: PathBuf,
+ run_root: PathBuf,
}
-#[derive(Debug, Clone, PartialEq, Eq)]
+impl ManagedRuntimeSharedPaths {
+ #[must_use]
+ pub fn instance_registry_path(&self) -> &Path {
+ &self.instance_registry_path
+ }
+
+ #[must_use]
+ pub fn artifact_cache_dir(&self) -> &Path {
+ &self.artifact_cache_dir
+ }
+
+ #[must_use]
+ pub fn install_root(&self) -> &Path {
+ &self.install_root
+ }
+
+ #[must_use]
+ pub fn logs_root(&self) -> &Path {
+ &self.logs_root
+ }
+
+ #[must_use]
+ pub fn run_root(&self) -> &Path {
+ &self.run_root
+ }
+}
+
+impl fmt::Debug for ManagedRuntimeSharedPaths {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("ManagedRuntimeSharedPaths([redacted])")
+ }
+}
+
+/// Operational paths for one validated service instance.
+///
+/// Service-owned directories and fixed artifacts come only from the supplied
+/// [`RuntimeContext`]. The manager-owned install directory comes only from its
+/// own validated context. Process tracking and captured stdout/stderr remain
+/// under manager-owned roots, so lifecycle removal cannot delete canonical
+/// service state or secrets.
+///
+/// ```compile_fail
+/// use std::path::PathBuf;
+/// use radroots_runtime_manager::ManagedRuntimeInstancePaths;
+///
+/// let _ = ManagedRuntimeInstancePaths {
+/// install_dir: PathBuf::from("/tmp/escape"),
+/// };
+/// ```
+#[derive(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,
+ context: RuntimeContext,
+ install_dir: PathBuf,
+ logs_dir: PathBuf,
+ run_dir: PathBuf,
+ pid_file_path: PathBuf,
+ stdout_log_path: PathBuf,
+ stderr_log_path: PathBuf,
+}
+
+impl ManagedRuntimeInstancePaths {
+ #[must_use]
+ pub fn context(&self) -> &RuntimeContext {
+ &self.context
+ }
+
+ #[must_use]
+ pub fn install_dir(&self) -> &Path {
+ &self.install_dir
+ }
+
+ #[must_use]
+ pub fn config_dir(&self) -> &Path {
+ self.context.paths().config()
+ }
+
+ #[must_use]
+ pub fn state_dir(&self) -> &Path {
+ self.context.paths().state()
+ }
+
+ #[must_use]
+ pub fn logs_dir(&self) -> &Path {
+ &self.logs_dir
+ }
+
+ #[must_use]
+ pub fn run_dir(&self) -> &Path {
+ &self.run_dir
+ }
+
+ #[must_use]
+ pub fn secrets_dir(&self) -> &Path {
+ self.context.paths().secrets()
+ }
+
+ #[must_use]
+ pub fn config_path(&self) -> PathBuf {
+ default_service_instance_artifacts(self.context.paths())
+ .config()
+ .to_path_buf()
+ }
+
+ #[must_use]
+ pub fn state_database_path(&self) -> PathBuf {
+ default_service_instance_artifacts(self.context.paths())
+ .state_database()
+ .to_path_buf()
+ }
+
+ #[must_use]
+ pub fn state_lock_path(&self) -> PathBuf {
+ default_service_instance_artifacts(self.context.paths())
+ .state_lock()
+ .to_path_buf()
+ }
+
+ #[must_use]
+ pub fn admin_socket_path(&self) -> PathBuf {
+ default_service_instance_artifacts(self.context.paths())
+ .admin_socket()
+ .to_path_buf()
+ }
+
+ #[must_use]
+ pub fn pid_file_path(&self) -> &Path {
+ &self.pid_file_path
+ }
+
+ #[must_use]
+ pub fn stdout_log_path(&self) -> &Path {
+ &self.stdout_log_path
+ }
+
+ #[must_use]
+ pub fn stderr_log_path(&self) -> &Path {
+ &self.stderr_log_path
+ }
+}
+
+impl fmt::Debug for ManagedRuntimeInstancePaths {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("ManagedRuntimeInstancePaths([redacted])")
+ }
}
-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()))?;
- let root = |root_class: &str, rel: &str| root_class_path(&roots, root_class, rel);
-
- let instance_registry_path = root(
- &path_spec.instance_registry_root_class,
- &path_spec.instance_registry_rel,
- )?;
- let artifact_cache_dir = root(
- &path_spec.artifact_cache_root_class,
- &path_spec.artifact_cache_rel,
- )?;
- let install_root = root(&path_spec.install_root_class, &path_spec.install_root_rel)?;
- let state_root = root(&path_spec.state_root_class, &path_spec.state_root_rel)?;
- let logs_root = root(&path_spec.logs_root_class, &path_spec.logs_root_rel)?;
- let run_root = root(&path_spec.run_root_class, &path_spec.run_root_rel)?;
- let secrets_root = root(
- &path_spec.secrets_root_class,
- &path_spec.secrets_namespace_rel,
- )?;
-
- Ok(ManagedRuntimeSharedPaths {
- instance_registry_path,
- artifact_cache_dir,
- install_root,
- state_root,
- logs_root,
- run_root,
- secrets_root,
- })
+#[must_use]
+pub(crate) fn resolve_shared_paths(context: &RuntimeContext) -> ManagedRuntimeSharedPaths {
+ ManagedRuntimeSharedPaths {
+ instance_registry_path: context.paths().config().join("instances.toml"),
+ artifact_cache_dir: context.paths().cache().join("artifacts"),
+ install_root: context.paths().state().join("installs"),
+ logs_root: context.paths().logs().join("instances"),
+ run_root: context.paths().run().join("instances"),
+ }
}
-pub fn resolve_instance_paths(
+#[must_use]
+pub(crate) fn resolve_instance_paths(
shared: &ManagedRuntimeSharedPaths,
- runtime_id: &str,
- instance_id: &str,
+ context: &RuntimeContext,
) -> 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 suffix = PathBuf::from(context.service().as_str()).join(context.instance().as_str());
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(),
+ context: context.clone(),
+ install_dir: shared.install_root.join(suffix),
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"),
}
}
@@ -106,52 +213,7 @@ pub fn bootstrap_runtime<'a>(
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))
+ .ok_or(RadrootsRuntimeManagerError::UnknownBootstrapRuntime)
}
#[cfg(test)]
@@ -159,239 +221,152 @@ mod tests {
use std::path::PathBuf;
use radroots_runtime_paths::{
- RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver,
- RadrootsPaths, RadrootsPlatform,
+ InstanceId, RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver,
+ RadrootsPlatform, RuntimeContext, RuntimeContextBootstrap, RuntimeContextSource, ServiceId,
};
- use super::{bootstrap_runtime, resolve_shared_paths, root_class_path};
- use crate::{
- ManagementPathContract, RadrootsRuntimeManagerError,
- model::RadrootsRuntimeManagementContract, parse_contract_str,
- };
-
- const CONTRACT: &str = r#"
-schema = "radroots-runtime-management"
-schema_version = 1
-owner_doc = "docs/execution/rcl/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"]
-destructive_actions = ["uninstall"]
-health_states = ["not_installed", "running"]
-
-[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"]
-optional_fields = ["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
-"#;
-
- fn contract() -> RadrootsRuntimeManagementContract {
- parse_contract_str(CONTRACT).expect("parse contract")
- }
+ use super::{resolve_instance_paths, resolve_shared_paths};
- fn assert_error_contains(err: &RadrootsRuntimeManagerError, parts: &[&str]) {
- let rendered = err.to_string();
- for part in parts {
- assert!(
- rendered.contains(part),
- "expected `{rendered}` to contain `{part}`"
- );
- }
+ fn repo_context(service: &str, instance: &str, root: &str) -> RuntimeContext {
+ RuntimeContext::resolve(
+ &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()),
+ RuntimeContextBootstrap::new(
+ RadrootsPathProfile::RepoLocal,
+ Some(PathBuf::from(root)),
+ RuntimeContextSource::BootstrapCli,
+ RuntimeContextSource::BootstrapCli,
+ )
+ .expect("bootstrap"),
+ ServiceId::new(service).expect("service"),
+ InstanceId::new(instance).expect("instance"),
+ )
+ .expect("context")
}
- fn linux_resolver() -> RadrootsPathResolver {
- RadrootsPathResolver::new(
- RadrootsPlatform::Linux,
- RadrootsHostEnvironment {
- home_dir: Some(PathBuf::from("/home/treesap")),
- xdg_runtime_dir: Some(PathBuf::from("/run/user/1000")),
- ..RadrootsHostEnvironment::default()
- },
+ fn service_host_context(service: &str, instance: &str) -> RuntimeContext {
+ RuntimeContext::resolve(
+ &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()),
+ RuntimeContextBootstrap::new(
+ RadrootsPathProfile::ServiceHost,
+ None,
+ RuntimeContextSource::SafeDefault,
+ RuntimeContextSource::BootstrapCli,
+ )
+ .expect("bootstrap"),
+ ServiceId::new(service).expect("service"),
+ InstanceId::new(instance).expect("instance"),
)
+ .expect("context")
}
#[test]
- fn bootstrap_lookup_reports_unknown_runtime() {
- let err = bootstrap_runtime(&contract(), "missing-runtime").expect_err("missing runtime");
- assert_error_contains(&err, &["missing-runtime", "no bootstrap entry"]);
- }
+ fn shared_paths_derive_only_from_the_manager_context() {
+ let manager = repo_context("runtime-manager", "default", "/repo/.radroots");
+ let paths = resolve_shared_paths(&manager);
- #[test]
- fn resolve_shared_paths_reports_unknown_management_mode() {
- let err = resolve_shared_paths(
- &contract(),
- &linux_resolver(),
- RadrootsPathProfile::InteractiveUser,
- &RadrootsPathOverrides::default(),
- "missing-mode",
- )
- .expect_err("missing mode should fail");
- assert_error_contains(&err, &["management mode `missing-mode`"]);
+ assert_eq!(
+ paths.instance_registry_path(),
+ PathBuf::from("/repo/.radroots/config/services/runtime-manager/default/instances.toml")
+ );
+ assert_eq!(
+ paths.artifact_cache_dir(),
+ PathBuf::from("/repo/.radroots/cache/services/runtime-manager/default/artifacts")
+ );
+ assert_eq!(
+ paths.install_root(),
+ PathBuf::from("/repo/.radroots/data/services/runtime-manager/default/installs")
+ );
+ assert_eq!(
+ format!("{paths:?}"),
+ "ManagedRuntimeSharedPaths([redacted])"
+ );
}
#[test]
- fn resolve_shared_paths_reports_unsupported_profile() {
- let err = resolve_shared_paths(
- &contract(),
- &linux_resolver(),
- RadrootsPathProfile::ServiceHost,
- &RadrootsPathOverrides::default(),
- "interactive_user_managed",
- )
- .expect_err("service_host should be unsupported for interactive mode");
- assert_error_contains(&err, &["interactive_user_managed", "service_host"]);
- }
+ fn instance_paths_use_the_exact_service_context_and_fixed_artifacts() {
+ let shared = resolve_shared_paths(&repo_context(
+ "runtime-manager",
+ "default",
+ "/repo/.radroots",
+ ));
+ let service = repo_context("myc", "north", "/repo/.radroots");
+ let paths = resolve_instance_paths(&shared, &service);
- #[test]
- fn resolve_shared_paths_reports_missing_path_spec() {
- let mut contract = contract();
- contract.paths.remove("interactive_user_managed");
-
- let err = resolve_shared_paths(
- &contract,
- &linux_resolver(),
- RadrootsPathProfile::InteractiveUser,
- &RadrootsPathOverrides::default(),
- "interactive_user_managed",
- )
- .expect_err("missing path spec should fail");
- assert_error_contains(
- &err,
- &["interactive_user_managed", "no shared path specification"],
+ assert_eq!(
+ paths.install_dir(),
+ PathBuf::from(
+ "/repo/.radroots/data/services/runtime-manager/default/installs/myc/north"
+ )
+ );
+ assert_eq!(
+ paths.config_path(),
+ PathBuf::from("/repo/.radroots/config/services/myc/north/config.toml")
+ );
+ assert_eq!(
+ paths.state_database_path(),
+ PathBuf::from("/repo/.radroots/data/services/myc/north/state.sqlite")
+ );
+ assert_eq!(
+ paths.state_lock_path(),
+ PathBuf::from("/repo/.radroots/data/services/myc/north/state.lock")
+ );
+ assert_eq!(
+ paths.admin_socket_path(),
+ PathBuf::from("/repo/.radroots/run/services/myc/north/admin.sock")
+ );
+ assert_eq!(
+ paths.stdout_log_path(),
+ PathBuf::from(
+ "/repo/.radroots/logs/services/runtime-manager/default/instances/myc/north/stdout.log"
+ )
+ );
+ assert_eq!(
+ format!("{paths:?}"),
+ "ManagedRuntimeInstancePaths([redacted])"
);
}
#[test]
- fn resolve_shared_paths_reports_unknown_root_class() {
- let mutators: &[fn(&mut ManagementPathContract)] = &[
- |paths| paths.instance_registry_root_class = "bogus".to_string(),
- |paths| paths.artifact_cache_root_class = "bogus".to_string(),
- |paths| paths.install_root_class = "bogus".to_string(),
- |paths| paths.state_root_class = "bogus".to_string(),
- |paths| paths.logs_root_class = "bogus".to_string(),
- |paths| paths.run_root_class = "bogus".to_string(),
- |paths| paths.secrets_root_class = "bogus".to_string(),
- ];
-
- for mutate in mutators {
- let mut contract = contract();
- mutate(
- contract
- .paths
- .get_mut("interactive_user_managed")
- .expect("path spec"),
- );
-
- let err = resolve_shared_paths(
- &contract,
- &linux_resolver(),
- RadrootsPathProfile::InteractiveUser,
- &RadrootsPathOverrides::default(),
- "interactive_user_managed",
- )
- .expect_err("unknown root class should fail");
- assert_error_contains(&err, &["unknown root class `bogus`"]);
- }
+ fn multi_instance_paths_cannot_cross_service_contexts() {
+ let shared = resolve_shared_paths(&repo_context(
+ "runtime-manager",
+ "default",
+ "/repo/.radroots",
+ ));
+ let north =
+ resolve_instance_paths(&shared, &repo_context("rhi", "north", "/repo/.radroots"));
+ let south =
+ resolve_instance_paths(&shared, &repo_context("rhi", "south", "/repo/.radroots"));
+
+ assert_ne!(north, south);
+ assert!(north.state_dir().ends_with("services/rhi/north"));
+ assert!(south.state_dir().ends_with("services/rhi/south"));
+ assert!(!north.state_dir().starts_with(south.state_dir()));
+ assert!(!south.state_dir().starts_with(north.state_dir()));
}
#[test]
- fn root_class_path_maps_all_known_classes() {
- let roots = RadrootsPaths {
- config: PathBuf::from("/roots/config"),
- data: PathBuf::from("/roots/data"),
- cache: PathBuf::from("/roots/cache"),
- logs: PathBuf::from("/roots/logs"),
- run: PathBuf::from("/roots/run"),
- secrets: PathBuf::from("/roots/secrets"),
- };
+ fn linux_service_host_paths_preserve_the_canonical_service_layout() {
+ let manager = service_host_context("runtime-manager", "default");
+ let shared = resolve_shared_paths(&manager);
+ let service = service_host_context("myc", "primary");
+ let paths = resolve_instance_paths(&shared, &service);
assert_eq!(
- root_class_path(&roots, "config", "a/b").expect("config root"),
- PathBuf::from("/roots/config/a/b")
- );
- assert_eq!(
- root_class_path(&roots, "data", "a/b").expect("data root"),
- PathBuf::from("/roots/data/a/b")
- );
- assert_eq!(
- root_class_path(&roots, "cache", "a/b").expect("cache root"),
- PathBuf::from("/roots/cache/a/b")
+ shared.instance_registry_path(),
+ PathBuf::from("/etc/radroots/services/runtime-manager/default/instances.toml")
);
assert_eq!(
- root_class_path(&roots, "logs", "a/b").expect("logs root"),
- PathBuf::from("/roots/logs/a/b")
+ paths.config_path(),
+ PathBuf::from("/etc/radroots/services/myc/primary/config.toml")
);
assert_eq!(
- root_class_path(&roots, "run", "a/b").expect("run root"),
- PathBuf::from("/roots/run/a/b")
+ paths.state_database_path(),
+ PathBuf::from("/var/lib/radroots/services/myc/primary/state.sqlite")
);
assert_eq!(
- root_class_path(&roots, "secrets", "a/b").expect("secrets root"),
- PathBuf::from("/roots/secrets/a/b")
+ paths.admin_socket_path(),
+ PathBuf::from("/run/radroots/services/myc/primary/admin.sock")
);
}
}
diff --git a/crates/runtime_manager/src/registry.rs b/crates/runtime_manager/src/registry.rs
@@ -1,8 +1,13 @@
use std::fs;
use std::path::Path;
+use radroots_runtime_paths::{InstanceId, ServiceId};
+
use crate::error::RadrootsRuntimeManagerError;
-use crate::model::{ManagedRuntimeInstanceRecord, ManagedRuntimeInstanceRegistry};
+use crate::model::{
+ ManagedRuntimeInstanceRecord, ManagedRuntimeInstanceRegistry, RUNTIME_INSTANCE_REGISTRY_SCHEMA,
+ RUNTIME_INSTANCE_REGISTRY_VERSION,
+};
pub fn load_registry(
path: impl AsRef<Path>,
@@ -20,18 +25,14 @@ fn load_registry_path(
}
Err(source) => {
return Err(RadrootsRuntimeManagerError::ReadRegistry {
- path: path.to_path_buf(),
- source,
+ kind: source.kind(),
});
}
};
- toml::from_str::<ManagedRuntimeInstanceRegistry>(&raw).map_err(|source| {
- RadrootsRuntimeManagerError::ParseRegistry {
- path: path.to_path_buf(),
- details: source.to_string(),
- }
- })
+ let registry = toml::from_str::<ManagedRuntimeInstanceRegistry>(&raw)
+ .map_err(|_| RadrootsRuntimeManagerError::ParseRegistry)?;
+ normalize_registry(registry)
}
pub fn save_registry(
@@ -55,52 +56,75 @@ fn save_registry_path_with(
) -> Result<(), RadrootsRuntimeManagerError> {
ensure_registry_parent(path)?;
- let raw = serializer(registry)
- .map_err(|err| RadrootsRuntimeManagerError::SerializeRegistry(err.to_string()))?;
+ let normalized = normalize_registry(registry.clone())?;
+ let raw =
+ serializer(&normalized).map_err(|_| RadrootsRuntimeManagerError::SerializeRegistry)?;
fs::write(path, raw).map_err(|source| RadrootsRuntimeManagerError::WriteRegistry {
- path: path.to_path_buf(),
- source,
+ kind: source.kind(),
})
}
-pub fn upsert_instance(
+fn normalize_registry(
+ mut registry: ManagedRuntimeInstanceRegistry,
+) -> Result<ManagedRuntimeInstanceRegistry, RadrootsRuntimeManagerError> {
+ if registry.schema != RUNTIME_INSTANCE_REGISTRY_SCHEMA {
+ return Err(RadrootsRuntimeManagerError::UnexpectedRegistrySchema);
+ }
+ if registry.schema_version != RUNTIME_INSTANCE_REGISTRY_VERSION {
+ return Err(RadrootsRuntimeManagerError::UnexpectedRegistryVersion);
+ }
+ registry.instances.sort_by(|left, right| {
+ left.service_id()
+ .cmp(right.service_id())
+ .then_with(|| left.instance_id().cmp(right.instance_id()))
+ });
+ if registry.instances.windows(2).any(|pair| {
+ pair[0].service_id() == pair[1].service_id()
+ && pair[0].instance_id() == pair[1].instance_id()
+ }) {
+ return Err(RadrootsRuntimeManagerError::DuplicateRegistryInstance);
+ }
+ Ok(registry)
+}
+
+pub(crate) 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.service_id() == record.service_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))
+ left.service_id()
+ .cmp(right.service_id())
+ .then_with(|| left.instance_id().cmp(right.instance_id()))
});
}
}
pub fn instance<'a>(
registry: &'a ManagedRuntimeInstanceRegistry,
- runtime_id: &str,
- instance_id: &str,
+ service_id: &ServiceId,
+ instance_id: &InstanceId,
) -> Option<&'a ManagedRuntimeInstanceRecord> {
registry
.instances
.iter()
- .find(|record| record.runtime_id == runtime_id && record.instance_id == instance_id)
+ .find(|record| record.service_id() == service_id && record.instance_id() == instance_id)
}
-pub fn remove_instance(
+pub(crate) fn remove_instance(
registry: &mut ManagedRuntimeInstanceRegistry,
- runtime_id: &str,
- instance_id: &str,
+ service_id: &ServiceId,
+ instance_id: &InstanceId,
) -> Option<ManagedRuntimeInstanceRecord> {
- let index = registry
- .instances
- .iter()
- .position(|record| record.runtime_id == runtime_id && record.instance_id == instance_id)?;
+ let index = registry.instances.iter().position(|record| {
+ record.service_id() == service_id && record.instance_id() == instance_id
+ })?;
Some(registry.instances.remove(index))
}
@@ -112,8 +136,7 @@ fn ensure_registry_parent(path: &Path) -> Result<(), RadrootsRuntimeManagerError
return Ok(());
}
fs::create_dir_all(parent).map_err(|source| RadrootsRuntimeManagerError::CreateRegistryParent {
- path: parent.to_path_buf(),
- source,
+ kind: source.kind(),
})
}
@@ -122,6 +145,10 @@ mod tests {
use std::fs;
use std::path::{Path, PathBuf};
+ use radroots_runtime_paths::{
+ InstanceId, RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver,
+ RadrootsPlatform, RuntimeContext, RuntimeContextBootstrap, RuntimeContextSource, ServiceId,
+ };
use serde::ser::Error as _;
use tempfile::tempdir;
@@ -134,26 +161,30 @@ mod tests {
RadrootsRuntimeManagerError,
};
- fn sample_record(runtime_id: &str, instance_id: &str) -> ManagedRuntimeInstanceRecord {
- ManagedRuntimeInstanceRecord {
- runtime_id: runtime_id.to_string(),
- instance_id: instance_id.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: "1.0.0-alpha.1".to_string(),
- health_endpoint: Some("jsonrpc_status".to_string()),
- secret_material_ref: None,
- last_started_at: None,
- last_stopped_at: None,
- notes: Some("test".to_string()),
- }
+ fn runtime_context(service_id: &str, instance_id: &str) -> RuntimeContext {
+ RuntimeContext::resolve(
+ &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()),
+ RuntimeContextBootstrap::new(
+ RadrootsPathProfile::RepoLocal,
+ Some(PathBuf::from("/repo/.radroots")),
+ RuntimeContextSource::BootstrapCli,
+ RuntimeContextSource::BootstrapCli,
+ )
+ .expect("bootstrap"),
+ ServiceId::new(service_id).expect("service"),
+ InstanceId::new(instance_id).expect("instance"),
+ )
+ .expect("context")
+ }
+
+ fn sample_record(service_id: &str, instance_id: &str) -> ManagedRuntimeInstanceRecord {
+ let context = runtime_context(service_id, instance_id);
+ ManagedRuntimeInstanceRecord::new(&context, ManagedRuntimeInstallState::Configured)
}
fn assert_error_contains(err: &RadrootsRuntimeManagerError, parts: &[&str]) {
+ use std::error::Error as _;
+
let rendered = err.to_string();
for part in parts {
assert!(
@@ -161,6 +192,7 @@ mod tests {
"expected `{rendered}` to contain `{part}`"
);
}
+ assert!(err.source().is_none());
}
#[test]
@@ -174,29 +206,21 @@ mod tests {
fn load_registry_reports_read_errors() {
let dir = tempdir().expect("tempdir");
let err = load_registry(dir.path()).expect_err("directory should fail");
- assert_error_contains(
- &err,
- &[
- dir.path().to_string_lossy().as_ref(),
- "read runtime instance registry",
- ],
- );
+ assert_error_contains(&err, &["read runtime instance registry", "is a directory"]);
}
#[test]
fn load_registry_reports_parse_errors() {
let dir = tempdir().expect("tempdir");
let path = dir.path().join("instances.toml");
- fs::write(&path, "not = [valid").expect("write invalid registry");
+ fs::write(&path, "credential = 'secret-value'\nnot = [valid")
+ .expect("write invalid registry");
let err = load_registry(&path).expect_err("invalid registry should fail");
- assert_error_contains(
- &err,
- &[
- path.to_string_lossy().as_ref(),
- "parse runtime instance registry",
- ],
- );
+ assert_error_contains(&err, &["parse runtime instance registry"]);
+ let rendered = format!("{err} {err:?}");
+ assert!(!rendered.contains("secret-value"));
+ assert!(!rendered.contains(path.to_string_lossy().as_ref()));
}
#[test]
@@ -207,13 +231,7 @@ mod tests {
let err = save_registry(&path, &ManagedRuntimeInstanceRegistry::default())
.expect_err("directory path should fail");
- assert_error_contains(
- &err,
- &[
- path.to_string_lossy().as_ref(),
- "write runtime instance registry",
- ],
- );
+ assert_error_contains(&err, &["write runtime instance registry", "is a directory"]);
}
#[test]
@@ -225,13 +243,7 @@ mod tests {
let err = save_registry(&path, &ManagedRuntimeInstanceRegistry::default())
.expect_err("file parent should fail");
- assert_error_contains(
- &err,
- &[
- file_parent.to_string_lossy().as_ref(),
- "create runtime instance registry parent",
- ],
- );
+ assert_error_contains(&err, &["create runtime instance registry parent"]);
}
#[test]
@@ -247,13 +259,7 @@ mod tests {
})
.expect_err("serializer should fail");
- assert_error_contains(
- &err,
- &[
- "serialize runtime instance registry",
- "forced registry serializer failure",
- ],
- );
+ assert_error_contains(&err, &["serialize runtime instance registry"]);
}
#[test]
@@ -269,16 +275,21 @@ mod tests {
upsert_instance(&mut registry, sample_record("radrootsd", "a"));
upsert_instance(&mut registry, sample_record("myc", "a"));
- let mut replacement = sample_record("radrootsd", "b");
- replacement.installed_version = "0.2.0".to_string();
+ let replacement = ManagedRuntimeInstanceRecord::new(
+ &runtime_context("radrootsd", "b"),
+ ManagedRuntimeInstallState::Failed,
+ );
upsert_instance(&mut registry, replacement);
assert_eq!(registry.instances.len(), 3);
- assert_eq!(registry.instances[0].runtime_id, "myc");
- assert_eq!(registry.instances[1].instance_id, "a");
- assert_eq!(registry.instances[2].runtime_id, "radrootsd");
- assert_eq!(registry.instances[2].instance_id, "b");
- assert_eq!(registry.instances[2].installed_version, "0.2.0");
+ assert_eq!(registry.instances[0].service_id().as_str(), "myc");
+ assert_eq!(registry.instances[1].instance_id().as_str(), "a");
+ assert_eq!(registry.instances[2].service_id().as_str(), "radrootsd");
+ assert_eq!(registry.instances[2].instance_id().as_str(), "b");
+ assert_eq!(
+ registry.instances[2].install_state(),
+ ManagedRuntimeInstallState::Failed
+ );
}
#[test]
@@ -286,11 +297,94 @@ mod tests {
let mut registry = ManagedRuntimeInstanceRegistry::default();
upsert_instance(&mut registry, sample_record("radrootsd", "local"));
- assert!(instance(®istry, "myc", "local").is_none());
- assert!(remove_instance(&mut registry, "myc", "local").is_none());
+ let myc = ServiceId::new("myc").expect("service");
+ let radrootsd = ServiceId::new("radrootsd").expect("service");
+ let local = InstanceId::new("local").expect("instance");
+
+ assert!(instance(®istry, &myc, &local).is_none());
+ assert!(remove_instance(&mut registry, &myc, &local).is_none());
- let removed = remove_instance(&mut registry, "radrootsd", "local").expect("remove");
- assert_eq!(removed.runtime_id, "radrootsd");
+ let removed = remove_instance(&mut registry, &radrootsd, &local).expect("remove");
+ assert_eq!(removed.service_id().as_str(), "radrootsd");
assert!(registry.instances.is_empty());
}
+
+ #[test]
+ fn registry_round_trip_is_typed_sorted_and_contains_no_service_paths_or_secrets() {
+ let dir = tempdir().expect("tempdir");
+ let path = dir.path().join("instances.toml");
+ let mut registry = ManagedRuntimeInstanceRegistry::default();
+ upsert_instance(&mut registry, sample_record("rhi", "secondary"));
+ upsert_instance(&mut registry, sample_record("myc", "primary"));
+
+ save_registry(&path, ®istry).expect("save registry");
+ let raw = fs::read_to_string(&path).expect("read registry");
+ for forbidden in [
+ "binary_path",
+ "config_path",
+ "logs_path",
+ "run_path",
+ "secrets_path",
+ "secret_material_ref",
+ "/repo/",
+ "/etc/",
+ ] {
+ assert!(!raw.contains(forbidden), "registry leaked `{forbidden}`");
+ }
+
+ let loaded = load_registry(&path).expect("load registry");
+ assert_eq!(loaded, registry);
+ assert_eq!(loaded.instances[0].service_id().as_str(), "myc");
+ assert_eq!(loaded.instances[1].service_id().as_str(), "rhi");
+ }
+
+ #[test]
+ fn registry_rejects_schema_version_unknown_fields_and_duplicate_keys() {
+ let dir = tempdir().expect("tempdir");
+ let path = dir.path().join("instances.toml");
+ let registry = ManagedRuntimeInstanceRegistry {
+ instances: vec![sample_record("myc", "primary")],
+ ..ManagedRuntimeInstanceRegistry::default()
+ };
+ save_registry(&path, ®istry).expect("save registry");
+ let raw = fs::read_to_string(&path).expect("read registry");
+
+ fs::write(
+ &path,
+ raw.replace("radroots.service-instance-registry", "wrong"),
+ )
+ .expect("write wrong schema");
+ assert!(matches!(
+ load_registry(&path),
+ Err(RadrootsRuntimeManagerError::UnexpectedRegistrySchema)
+ ));
+
+ fs::write(
+ &path,
+ raw.replace("schema_version = 1", "schema_version = 2"),
+ )
+ .expect("write wrong version");
+ assert!(matches!(
+ load_registry(&path),
+ Err(RadrootsRuntimeManagerError::UnexpectedRegistryVersion)
+ ));
+
+ fs::write(&path, format!("{raw}\nunknown = true\n")).expect("write unknown field");
+ assert!(matches!(
+ load_registry(&path),
+ Err(RadrootsRuntimeManagerError::ParseRegistry)
+ ));
+
+ let duplicate = ManagedRuntimeInstanceRegistry {
+ instances: vec![
+ sample_record("myc", "primary"),
+ sample_record("myc", "primary"),
+ ],
+ ..ManagedRuntimeInstanceRegistry::default()
+ };
+ assert!(matches!(
+ save_registry(&path, &duplicate),
+ Err(RadrootsRuntimeManagerError::DuplicateRegistryInstance)
+ ));
+ }
}