use std::collections::BTreeSet;
use isoloom_manifest::{
Delivery, EnvName, Identifier, Machine, Name, NonEmpty, Param, PublishMode, Role, SecretSlot, SecretSource, SetBy, SlotKind, StepId, StepSelection, Store,
};
use isoloom_plan::{Environment, MachineShape, ValueSource};
use isoloom_spec::model::{SecretSource as SpecSource, secret_variable};
use isoloom_render::container::project_variables;
use super::lifecycle::{job_step, owner};
use super::machines::Member;
use crate::error::Result;
use crate::start::StartPlan;
pub(super) const PUBLISH_ADDRESS: &str = "ISOLOOM_PUBLISH_ADDRESS";
const PUBLISH_FIXED: &str = "ISOLOOM_PUBLISH_FIXED";
const DERIVED: &str = "ISOLOOM_DERIVED";
fn env_name(name: &str) -> Result<EnvName> {
Ok(EnvName::parse(name)?)
}
fn env(var: &str, steps: StepSelection) -> Result<NonEmpty<Delivery>> {
Ok(NonEmpty::one(Delivery::Env { var: env_name(var)?, steps }))
}
fn param(name: &str, description: &str, default: Option<&str>, values: &[&str], steps: StepSelection) -> Result<Param> {
Ok(Param {
name: env_name(name)?,
description: description.into(),
default: default.map(String::from),
values: values.iter().map(|v| (*v).to_owned()).collect(),
required: false,
hosts: BTreeSet::new(),
providers: BTreeSet::new(),
set_by: SetBy::Embedder,
deliver: env(name, steps)?,
})
}
pub(super) fn params(machines: &[Machine], derived: &[StepId]) -> Result<Vec<Param>> {
let published: Vec<PublishMode> = machines
.iter()
.flat_map(|m| &m.ports)
.filter_map(|p| p.publish.as_ref().map(|p| p.mode))
.collect();
let mut out = Vec::new();
if !published.is_empty() {
out.push(param(
PUBLISH_ADDRESS,
"The host address published ports listen on.",
Some("127.0.0.1"),
&[],
StepSelection::Every,
)?);
}
if published.contains(&PublishMode::Ephemeral) {
let what = "Set: each published port takes its `publish:` number instead of an ephemeral one.";
out.push(param(PUBLISH_FIXED, what, None, &["1"], StepSelection::Every)?);
}
if let Ok(steps) = NonEmpty::new(derived.to_vec(), "the check steps") {
let what = "0: the checks derived from the spec (reachability, no internet) are skipped.";
out.push(param(DERIVED, what, Some("1"), &["0", "1"], StepSelection::Only(steps))?);
}
Ok(out)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Starts {
Host,
Guest,
}
impl Starts {
fn steps(self) -> &'static [&'static str] {
match self {
Starts::Host => &["up", "start"],
Starts::Guest => &["start"],
}
}
}
pub(super) fn slots(env: &Environment, members: &[Member], plan: &StartPlan, starts: Starts) -> Result<Vec<SecretSlot>> {
let containers: Vec<(&String, &isoloom_plan::MachinePlan)> = env.machines.iter().filter(|(_, m)| m.shape == MachineShape::Container).collect();
let mut out = Vec::new();
for input in &env.spec().inputs {
let file = |m: &isoloom_plan::MachinePlan| m.secrets.iter().any(|s| s.source == ValueSource::Input(input.clone()));
let readers: Vec<&str> = containers
.iter()
.filter(|(_, m)| m.inputs.contains(input) || file(m))
.map(|(n, _)| n.as_str())
.collect();
if !readers.is_empty() {
let mut s = slot(input, SlotKind::Input, None, input, reach(&readers, members, plan, starts)?)?;
s.sensitive = !env.spec().plain_inputs.contains(input);
s.default = Some(String::new());
out.push(s);
}
}
for (name, source) in &env.spec().secrets {
let Some(source) = spec_source(source) else { continue };
let holders: Vec<&str> = containers
.iter()
.filter(|(_, m)| m.secrets.iter().any(|s| &s.name == name))
.map(|(n, _)| n.as_str())
.collect();
if !holders.is_empty() {
let var = secret_variable(name);
out.push(slot(name, SlotKind::Secret, Some(source), &var, reach(&holders, members, plan, starts)?)?);
}
}
if starts == Starts::Guest {
return Ok(out);
}
for (var, path) in project_variables(env) {
let source = SecretSource {
store: Store::File,
reference: path,
};
out.push(slot(&var, SlotKind::Secret, Some(source), &var, reach(&[], members, plan, starts)?)?);
}
Ok(out)
}
fn spec_source(s: &SpecSource) -> Option<SecretSource> {
let store = match s {
SpecSource::Env { .. } => Store::Env,
SpecSource::File { .. } => Store::File,
SpecSource::Vault { .. } => Store::Vault,
SpecSource::Sops { .. } => Store::Sops,
SpecSource::Input { .. } => return None,
};
Some(SecretSource {
store,
reference: s.reference().to_owned(),
})
}
struct Reach {
machines: Vec<Name>,
steps: StepSelection,
}
fn reach(machines: &[&str], members: &[Member], plan: &StartPlan, starts: Starts) -> Result<Reach> {
let jobs = members
.iter()
.filter(|m| m.role == Role::Job && m.of.as_deref().is_some_and(|o| machines.contains(&o)));
let names = machines.iter().copied().chain(jobs.map(|j| j.name.as_str()));
let leaf_jobs = plan.jobs.iter().filter(|j| machines.contains(&owner(members, j))).map(|j| job_step(j));
let steps = starts.steps().iter().map(|s| (*s).to_owned()).chain(leaf_jobs);
Ok(Reach {
machines: names.map(Name::parse).collect::<Result<_, _>>()?,
steps: StepSelection::Only(NonEmpty::new(steps.map(|s| StepId::parse(&s)).collect::<Result<_, _>>()?, "the steps")?),
})
}
fn slot(name: &str, kind: SlotKind, source: Option<SecretSource>, var: &str, reach: Reach) -> Result<SecretSlot> {
Ok(SecretSlot {
name: Identifier::parse(name)?,
kind,
required: kind != SlotKind::Input,
sensitive: true,
machines: reach.machines,
providers: BTreeSet::new(),
source,
default: None,
deliver: env(var, reach.steps)?,
})
}