use std::path::{Path, PathBuf};
use std::sync::Arc;
use ikigai_core::{EndpointSpace, Iri, Space, SystemClock};
pub const DIR: &str = "layers";
pub const STATE_KEY: &str = "layers.state";
pub const STATE_FLAG: &str = "--layers-state";
pub const KEY_KEY: &str = "layers.key";
pub const KEY_FLAG: &str = "--layers-key";
pub const RESEAL_FLAG: &str = "--layers-reseal";
pub const SPACE: &str = "urn:ikigai:space:served:layers";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StateSource {
Flag,
Config,
Default,
}
impl StateSource {
pub fn as_str(self) -> &'static str {
match self {
StateSource::Flag => "flag",
StateSource::Config => "config",
StateSource::Default => "default",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum KeySource {
Flag,
Config,
}
impl KeySource {
pub fn as_str(self) -> &'static str {
match self {
KeySource::Flag => "flag",
KeySource::Config => "config",
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Flags<'a> {
pub state: Option<&'a Path>,
pub key: Option<&'a Path>,
pub reseal: bool,
}
impl Flags<'_> {
fn given(&self) -> Vec<&'static str> {
let mut given = Vec::new();
if self.state.is_some() {
given.push(STATE_FLAG);
}
if self.key.is_some() {
given.push(KEY_FLAG);
}
if self.reseal {
given.push(RESEAL_FLAG);
}
given
}
}
pub struct Hosted {
pub host: Arc<ikigai_layers::Host>,
pub config: PathBuf,
pub state: PathBuf,
pub source: StateSource,
pub key: PathBuf,
pub key_source: KeySource,
pub recipient: String,
pub resealed: bool,
}
impl Hosted {
pub fn layer_lines(&self) -> Vec<String> {
self.host
.layers()
.iter()
.map(|(name, state)| format!("{name} ({})", state.def.model.as_str()))
.collect()
}
}
pub fn configured() -> Option<PathBuf> {
configured_in(&crate::config::config_home()?)
}
pub fn configured_in(home: &Path) -> Option<PathBuf> {
let dir = home.join(DIR);
dir.is_dir().then_some(dir)
}
pub fn state_dir(flag: Option<&Path>) -> Result<(PathBuf, StateSource), String> {
let instance = crate::instance_name();
let configured = crate::config::get(&format!("{instance}.{STATE_KEY}"))
.or_else(|| crate::config::get(STATE_KEY));
state_dir_from(
flag,
configured.as_deref(),
crate::config::config_home().as_deref(),
std::env::var_os("HOME").as_deref().map(Path::new),
instance,
)
}
pub fn state_dir_from(
flag: Option<&Path>,
configured: Option<&str>,
config_home: Option<&Path>,
home: Option<&Path>,
instance: &str,
) -> Result<(PathBuf, StateSource), String> {
if let Some(dir) = flag {
return Ok((dir.to_path_buf(), StateSource::Flag));
}
if let Some(value) = configured.map(str::trim).filter(|v| !v.is_empty()) {
let path = ikigai_resolve::expand_home(value, home)
.map_err(|e| format!("`{STATE_KEY}` in the config home: {e}"))?;
let path = match config_home {
Some(base) if path.is_relative() => base.join(path),
_ => path,
};
return Ok((path, StateSource::Config));
}
let home = home.filter(|h| !h.as_os_str().is_empty()).ok_or_else(|| {
format!(
"layers are configured but no state directory is: pass {STATE_FLAG} <dir>, set \
`{STATE_KEY}` in the config home, or set HOME so the default \
(~/.ikigai/layers/{instance}) has somewhere to be"
)
})?;
Ok((
ikigai_core::config::data_home_in(home)
.join("layers")
.join(instance),
StateSource::Default,
))
}
pub fn key_path(flag: Option<&Path>) -> Result<(PathBuf, KeySource), String> {
let instance = crate::instance_name();
let configured = crate::config::get(&format!("{instance}.{KEY_KEY}"))
.or_else(|| crate::config::get(KEY_KEY));
key_path_from(
flag,
configured.as_deref(),
crate::config::config_home().as_deref(),
std::env::var_os("HOME").as_deref().map(Path::new),
)
}
pub fn key_path_from(
flag: Option<&Path>,
configured: Option<&str>,
config_home: Option<&Path>,
home: Option<&Path>,
) -> Result<(PathBuf, KeySource), String> {
if let Some(path) = flag {
return Ok((path.to_path_buf(), KeySource::Flag));
}
if let Some(value) = configured.map(str::trim).filter(|v| !v.is_empty()) {
let path = ikigai_resolve::expand_home(value, home)
.map_err(|e| format!("`{KEY_KEY}` in the config home: {e}"))?;
let path = match config_home {
Some(base) if path.is_relative() => base.join(path),
_ => path,
};
return Ok((path, KeySource::Config));
}
Err(format!(
"layers are configured, and their state is sealed at rest with the host's key, but no \
key is named: pass {KEY_FLAG} <file> or set `{KEY_KEY}` in the config home. The file \
is an age identity, as `age-keygen -o <file>` writes one; no environment variable \
names it, and the host does not serve its state unsealed"
))
}
pub fn open(flags: Flags<'_>) -> Result<Option<Hosted>, String> {
let given = flags.given();
let Some(home) = crate::config::config_home() else {
return match given.is_empty() {
false => Err(format!(
"{} need{} a config home holding `{DIR}/`",
given.join(" and "),
if given.len() == 1 { "s" } else { "" }
)),
true => Ok(None),
};
};
if configured_in(&home).is_none() {
return match given.is_empty() {
false => Err(format!(
"{} appl{} to a door that hosts layers, but {} does not exist, so no layer is \
configured",
given.join(" and "),
if given.len() == 1 { "ies" } else { "y" },
home.join(DIR).display()
)),
true => Ok(None),
};
}
let (state, source) = state_dir(flags.state)?;
let (key, key_source) = key_path(flags.key)?;
let opened = ikigai_layers::StateKey::load(&key).map_err(|e| e.to_string())?;
let migration = if flags.reseal {
ikigai_layers::Migration::Reseal
} else {
ikigai_layers::Migration::None
};
let host = ikigai_layers::Host::open_with(&home, &state, &opened, migration).map_err(|e| {
let e = e.to_string();
let hint = if !flags.reseal && e.contains("Migration::Reseal") {
format!(
". For this door: start it ONCE with {RESEAL_FLAG} beside the same key, which \
seals them in place and then serves"
)
} else {
String::new()
};
format!(
"layers are configured in {} but the host cannot open: {e}{hint}",
home.join(DIR).display()
)
})?;
Ok(Some(Hosted {
host,
config: home,
state,
source,
recipient: opened.recipient(),
key,
key_source,
resealed: flags.reseal,
}))
}
pub fn spaces(host: &Arc<ikigai_layers::Host>) -> Vec<Arc<dyn Space>> {
let layers: EndpointSpace = ikigai_layers::space(host)
.named(Iri::parse(SPACE).expect("the layer space's name is an IRI"));
vec![Arc::new(layers) as Arc<dyn Space>]
}
pub fn clock() -> Arc<dyn ikigai_core::Clock> {
Arc::new(SystemClock)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_flag_wins_then_the_config_home_then_the_data_home() {
let flag = Path::new("/srv/flagged");
let home = Path::new("/home/b");
let config = Path::new("/home/b/.config/ikigai");
assert_eq!(
state_dir_from(
Some(flag),
Some("ignored"),
Some(config),
Some(home),
"layers"
),
Ok((flag.to_path_buf(), StateSource::Flag))
);
assert_eq!(
state_dir_from(None, Some("state"), Some(config), Some(home), "layers"),
Ok((config.join("state"), StateSource::Config))
);
assert_eq!(
state_dir_from(
None,
Some("~/layer-state"),
Some(config),
Some(home),
"layers"
),
Ok((home.join("layer-state"), StateSource::Config))
);
assert_eq!(
state_dir_from(
None,
Some("/var/layers"),
Some(config),
Some(home),
"layers"
),
Ok((PathBuf::from("/var/layers"), StateSource::Config))
);
assert_eq!(
state_dir_from(None, None, Some(config), Some(home), "layers"),
Ok((home.join(".ikigai/layers/layers"), StateSource::Default))
);
assert_eq!(
state_dir_from(None, Some(" "), Some(config), Some(home), "edge"),
Ok((home.join(".ikigai/layers/edge"), StateSource::Default)),
"a blank setting is no setting"
);
let err = state_dir_from(None, None, Some(config), None, "layers").unwrap_err();
assert!(err.contains(STATE_FLAG) && err.contains(STATE_KEY), "{err}");
}
#[test]
fn the_state_key_takes_the_shared_tilde_rule() {
let home = Path::new("/home/b");
let config = Path::new("/home/b/.config/ikigai");
assert_eq!(
state_dir_from(None, Some("~"), Some(config), Some(home), "layers"),
Ok((home.to_path_buf(), StateSource::Config))
);
let err =
state_dir_from(None, Some("~bob/s"), Some(config), Some(home), "layers").unwrap_err();
assert!(
err.contains(STATE_KEY) && err.contains("another user's home"),
"{err}"
);
let err = state_dir_from(None, Some("~/s"), Some(config), None, "layers").unwrap_err();
assert!(
err.contains(STATE_KEY) && err.contains("HOME is not set"),
"{err}"
);
}
#[test]
fn the_key_is_the_flag_then_the_config_home_and_never_a_default() {
let flag = Path::new("/run/credentials/edge/layers.key");
let home = Path::new("/home/b");
let config = Path::new("/home/b/.config/ikigai");
assert_eq!(
key_path_from(Some(flag), Some("ignored"), Some(config), Some(home)),
Ok((flag.to_path_buf(), KeySource::Flag))
);
assert_eq!(
key_path_from(None, Some("layers.key"), Some(config), Some(home)),
Ok((config.join("layers.key"), KeySource::Config))
);
assert_eq!(
key_path_from(None, Some("~/keys/l.key"), Some(config), Some(home)),
Ok((home.join("keys/l.key"), KeySource::Config))
);
assert_eq!(
key_path_from(None, Some("/etc/l.key"), Some(config), Some(home)),
Ok((PathBuf::from("/etc/l.key"), KeySource::Config))
);
for configured in [None, Some(""), Some(" ")] {
let err = key_path_from(None, configured, Some(config), Some(home)).unwrap_err();
assert!(
err.contains(KEY_FLAG) && err.contains(KEY_KEY) && err.contains("age-keygen"),
"{configured:?}: {err}"
);
}
let err = key_path_from(None, Some("~bob/k"), Some(config), Some(home)).unwrap_err();
assert!(err.contains(KEY_KEY), "{err}");
}
#[test]
fn only_a_directory_switches_the_host_on() {
let dir = std::env::temp_dir().join(format!("iki-layers-switch-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
assert_eq!(configured_in(&dir), None);
std::fs::write(dir.join(DIR), "not a directory").unwrap();
assert_eq!(
configured_in(&dir),
None,
"a FILE named layers is not a configuration"
);
std::fs::remove_file(dir.join(DIR)).unwrap();
std::fs::create_dir(dir.join(DIR)).unwrap();
assert_eq!(configured_in(&dir), Some(dir.join(DIR)));
let _ = std::fs::remove_dir_all(&dir);
}
}