1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
use super::errors::OrchestratorError;
use std::path::Path;
use clap::ValueEnum;
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub enum OrchestratorRole {
SingleTenant,
MultiTenant,
}
impl OrchestratorRole {
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::SingleTenant => "single-tenant",
Self::MultiTenant => "multi-tenant",
}
}
pub(crate) fn registry_mode(self) -> &'static str {
match self {
Self::SingleTenant => "one shared trigger/connector registry",
Self::MultiTenant => "per-tenant registries",
}
}
/// Build the orchestrator VM with its persistent state rooted at
/// `state_dir`.
///
/// `state_dir` is passed to each registrar directly rather than exported as
/// `HARN_STATE_DIR`. It used to be exported, which made an orchestrator's
/// configured state dir the answer every *other* component in the process
/// got from `runtime_paths::state_root()` — including ones handed an
/// explicit base dir, since an absolute env value replaces that argument
/// outright. That is wrong on three counts: this function is a reusable
/// builder (a config reload calls it again, and a multi-tenant host may
/// have several VMs), `set_var` races concurrent readers in any threaded
/// process, and the export outlived the VM it was meant to configure.
pub(crate) fn build_vm(
self,
workspace_root: &Path,
source_dir: &Path,
state_dir: &Path,
) -> Result<harn_vm::Vm, OrchestratorError> {
match self {
Self::SingleTenant | Self::MultiTenant => {
// `register_store_builtins` would install this lazily off the
// ambient state root; the orchestrator knows its own, so it
// installs the log itself before the registrars run.
harn_vm::event_log::install_default_at_state_root(state_dir)
.map_err(|error| OrchestratorError::from(error.to_string()))?;
let mut vm = harn_vm::Vm::new();
harn_vm::register_vm_stdlib(&mut vm);
crate::install_default_hostlib(&mut vm);
harn_vm::register_persistent_state_builtins_at_root(
&mut vm,
workspace_root,
harn_vm::PersistentStateRoot::new(state_dir),
"orchestrator",
);
vm.set_project_root(workspace_root);
vm.set_source_dir(source_dir);
Ok(vm)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use harn_vm::event_log::EventLog as _;
/// `build_vm` must route its state dir through arguments only.
///
/// Exporting it instead pointed every `state_root()` caller in the process
/// at this orchestrator's directory — including callers that passed their
/// own base dir, which an absolute env value overrides. The visible symptom
/// was unrelated in-process tests reading each other's event logs; the
/// same aliasing applies to any host that builds a VM beside other work.
#[tokio::test]
async fn build_vm_roots_state_without_exporting_the_state_dir() {
let _guard = crate::tests::common::harn_state_lock::lock_harn_state_async().await;
harn_vm::event_log::reset_active_event_log();
let workspace = tempfile::TempDir::new().unwrap();
let state_dir = workspace.path().join("orchestrator-state");
std::fs::create_dir_all(&state_dir).unwrap();
let _vm = OrchestratorRole::SingleTenant
.build_vm(workspace.path(), workspace.path(), &state_dir)
.expect("build orchestrator vm");
assert_eq!(
std::env::var(harn_vm::runtime_paths::HARN_STATE_DIR_ENV).ok(),
None,
"build_vm must not publish its state dir to the process environment",
);
// The state dir still wins for the orchestrator's own log, which is the
// contract `--state-dir` documents.
let location = harn_vm::event_log::active_event_log()
.expect("build_vm installs the active event log")
.describe()
.location;
assert_eq!(
location.as_deref(),
Some(state_dir.join("events.sqlite").as_path()),
"event log must be rooted at the configured state dir",
);
harn_vm::event_log::reset_active_event_log();
}
}