feat(wfe): integrate workflow engine for up, seed, verify, bootstrap

Dispatch `sunbeam up`, `sunbeam seed`, `sunbeam verify`, and
`sunbeam bootstrap` through WFE workflows instead of monolithic
functions. Steps communicate via JSON workflow data and each
workflow is persisted in a per-context SQLite database.
This commit is contained in:
2026-04-05 18:21:59 +01:00
parent dce085cd0c
commit 9cd3c641da
38 changed files with 5355 additions and 181 deletions

View File

@@ -39,6 +39,8 @@ const PG_USERS: &[&str] = &[
"find",
"calendars",
"projects",
"penpot",
"stalwart",
];
const SMTP_URI: &str = "smtp://postfix.lasuite.svc.cluster.local:25/?skip_ssl_verify=true";
@@ -1044,9 +1046,9 @@ mod tests {
fn test_constants() {
assert_eq!(ADMIN_USERNAME, "estudio-admin");
assert_eq!(GITEA_ADMIN_USER, "gitea_admin");
assert_eq!(PG_USERS.len(), 13);
assert_eq!(PG_USERS.len(), 15);
assert!(PG_USERS.contains(&"kratos"));
assert!(PG_USERS.contains(&"projects"));
assert!(PG_USERS.contains(&"stalwart"));
}
#[test]
@@ -1109,6 +1111,8 @@ mod tests {
"find",
"calendars",
"projects",
"penpot",
"stalwart",
];
assert_eq!(PG_USERS, &expected[..]);
}

View File

@@ -454,6 +454,17 @@ pub async fn seed_openbao() -> Result<Option<SeedResult>> {
)
.await?;
let stalwart = get_or_create(
&bao,
"stalwart",
&[
("admin-password", &rand_token as &dyn Fn() -> String),
("dkim-private-key", &empty_fn),
],
&mut dirty_paths,
)
.await?;
let admin_fn = || "admin".to_string();
let collabora = get_or_create(
&bao,
@@ -531,6 +542,7 @@ pub async fn seed_openbao() -> Result<Option<SeedResult>> {
("projects", &projects),
("calendars", &calendars),
("messages", &messages),
("stalwart", &stalwart),
("collabora", &collabora),
("tuwunel", &tuwunel),
("grafana", &grafana),
@@ -606,7 +618,7 @@ pub async fn seed_openbao() -> Result<Option<SeedResult>> {
"auth/kubernetes/role/vso",
&serde_json::json!({
"bound_service_account_names": "default",
"bound_service_account_namespaces": "ory,devtools,storage,lasuite,matrix,media,data,monitoring",
"bound_service_account_namespaces": "ory,devtools,storage,lasuite,stalwart,matrix,media,data,monitoring",
"policies": "vso-reader",
"ttl": "1h"
}),

View File

@@ -51,11 +51,8 @@ pub enum SyncStatus {
// Path helpers
// ---------------------------------------------------------------------------
/// Base directory for vault keystore files.
fn base_dir(override_dir: Option<&Path>) -> PathBuf {
if let Some(d) = override_dir {
return d.to_path_buf();
}
/// Legacy vault dir — used only for migration.
fn legacy_vault_dir() -> PathBuf {
dirs::data_dir()
.unwrap_or_else(|| {
dirs::home_dir()
@@ -66,6 +63,41 @@ fn base_dir(override_dir: Option<&Path>) -> PathBuf {
.join("vault")
}
/// Base directory for vault keystore files: ~/.sunbeam/vault/
fn base_dir(override_dir: Option<&Path>) -> PathBuf {
if let Some(d) = override_dir {
return d.to_path_buf();
}
let new_dir = dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".sunbeam")
.join("vault");
// Migration: copy files from legacy location if new dir doesn't exist yet
if !new_dir.exists() {
let legacy = legacy_vault_dir();
if legacy.is_dir() {
let _ = std::fs::create_dir_all(&new_dir);
if let Ok(entries) = std::fs::read_dir(&legacy) {
for entry in entries.flatten() {
let dest = new_dir.join(entry.file_name());
let _ = std::fs::copy(entry.path(), &dest);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(
&dest,
std::fs::Permissions::from_mode(0o600),
);
}
}
}
}
}
new_dir
}
/// Path to the encrypted keystore file for a domain.
pub fn keystore_path(domain: &str) -> PathBuf {
keystore_path_in(domain, None)
@@ -83,6 +115,11 @@ pub fn keystore_exists(domain: &str) -> bool {
keystore_path(domain).exists()
}
/// Whether a keystore exists in a specific directory (context-aware).
pub fn keystore_exists_at(domain: &str, dir: &Path) -> bool {
keystore_path_in(domain, Some(dir)).exists()
}
fn keystore_exists_in(domain: &str, dir: Option<&Path>) -> bool {
keystore_path_in(domain, dir).exists()
}
@@ -92,7 +129,13 @@ fn keystore_exists_in(domain: &str, dir: Option<&Path>) -> bool {
// ---------------------------------------------------------------------------
fn machine_salt_path(override_dir: Option<&Path>) -> PathBuf {
base_dir(override_dir).join(".machine-salt")
if let Some(d) = override_dir {
return d.join(".machine-salt");
}
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".sunbeam")
.join(".machine-salt")
}
fn load_or_create_machine_salt(override_dir: Option<&Path>) -> Result<Vec<u8>> {
@@ -203,11 +246,16 @@ fn decrypt(data: &[u8], domain: &str, override_dir: Option<&Path>) -> Result<Vec
// Public API
// ---------------------------------------------------------------------------
/// Save a keystore, encrypted, to the local filesystem.
/// Save a keystore, encrypted, to the local filesystem (default dir).
pub fn save_keystore(ks: &VaultKeystore) -> Result<()> {
save_keystore_in(ks, None)
}
/// Save a keystore to a specific directory (context-aware).
pub fn save_keystore_to(ks: &VaultKeystore, dir: &Path) -> Result<()> {
save_keystore_in(ks, Some(dir))
}
fn save_keystore_in(ks: &VaultKeystore, override_dir: Option<&Path>) -> Result<()> {
let path = keystore_path_in(&ks.domain, override_dir);
@@ -235,11 +283,16 @@ fn save_keystore_in(ks: &VaultKeystore, override_dir: Option<&Path>) -> Result<(
Ok(())
}
/// Load and decrypt a keystore from the local filesystem.
/// Load and decrypt a keystore from the local filesystem (default dir).
pub fn load_keystore(domain: &str) -> Result<VaultKeystore> {
load_keystore_in(domain, None)
}
/// Load a keystore from a specific directory (context-aware).
pub fn load_keystore_from(domain: &str, dir: &Path) -> Result<VaultKeystore> {
load_keystore_in(domain, Some(dir))
}
fn load_keystore_in(domain: &str, override_dir: Option<&Path>) -> Result<VaultKeystore> {
let path = keystore_path_in(domain, override_dir);
if !path.exists() {
@@ -275,6 +328,11 @@ pub fn verify_vault_keys(domain: &str) -> Result<VaultKeystore> {
verify_vault_keys_in(domain, None)
}
/// Verify vault keys from a specific directory (context-aware).
pub fn verify_vault_keys_from(domain: &str, dir: &Path) -> Result<VaultKeystore> {
verify_vault_keys_in(domain, Some(dir))
}
fn verify_vault_keys_in(domain: &str, override_dir: Option<&Path>) -> Result<VaultKeystore> {
let ks = load_keystore_in(domain, override_dir)?;