greentic_deployer/environment/bootstrap.rs
1//! Bootstrap types for [`super::mutations_local::LocalFsStore::ensure_local_environment`].
2//!
3//! These types are split out from `cli/bootstrap.rs` so the typed verb on
4//! `LocalFsStore` can return them without pulling in CLI concerns.
5
6use greentic_deploy_spec::{CapabilitySlot, Environment};
7
8use crate::defaults::local_pack_bindings;
9
10use super::store::StoreError;
11
12/// Whether the bootstrap verb created the env, found it intact, or
13/// repaired missing default bindings on an existing env.
14///
15/// `Healed` carries the slots that were missing and got the default binding
16/// inserted. Slots already bound to a non-default descriptor are NOT
17/// overwritten — bootstrap only fills *missing* slots, never replaces user
18/// intent.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum LocalEnvOutcome {
21 Created,
22 AlreadyExists,
23 Healed { added_slots: Vec<CapabilitySlot> },
24}
25
26/// Payload for [`super::mutations_local::LocalFsStore::ensure_local_environment`].
27///
28/// `public_base_url`: when `Some`, persisted on the env's `host_config` ONLY
29/// during creation. For `AlreadyExists` and `Healed` outcomes the existing URL
30/// is preserved — passing `Some` when the env already exists is rejected with
31/// [`StoreError::Conflict`].
32#[derive(Debug, Clone, Default)]
33pub struct EnsureLocalEnvironmentPayload {
34 pub public_base_url: Option<String>,
35}
36
37/// Walks the five default capability slots and appends a default
38/// [`greentic_deploy_spec::EnvPackBinding`] for any slot not already bound on
39/// `env`. Slots already bound — regardless of descriptor — are left untouched
40/// so user-customized bindings (e.g. an externally-provisioned secrets backend)
41/// survive.
42///
43/// Returns the slots that were appended, in the canonical default order.
44/// Empty return means the env already satisfied the A4 invariant.
45pub(crate) fn fill_missing_default_bindings(
46 env: &mut Environment,
47) -> Result<Vec<CapabilitySlot>, StoreError> {
48 let defaults = local_pack_bindings()
49 .map_err(|e| StoreError::InvalidArgument(format!("default pack binding parse: {e}")))?;
50 let mut added = Vec::new();
51 for binding in defaults {
52 if env.packs.iter().any(|b| b.slot == binding.slot) {
53 continue;
54 }
55 added.push(binding.slot);
56 env.packs.push(binding);
57 }
58 Ok(added)
59}