Skip to main content

greentic_bundle/setup/
mod.rs

1pub mod backend;
2pub mod legacy_formspec;
3pub mod persist;
4pub mod qa_bridge;
5
6use std::collections::BTreeMap;
7
8use anyhow::Result;
9use greentic_deploy_spec::SecretRef;
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13pub const SETUP_STATE_DIR: &str = "state/setup";
14/// Bumped to `3` for C7: the schema adds a required `env_id` field so each
15/// persisted state is bound to the environment that produced it. Older
16/// (`schema_version <= 2`) state files have no `env_id` and the wizard now
17/// rejects them at re-mint time — they must be re-emitted under the active
18/// `--env`. See [`PersistedSetupState::env_id`].
19///
20/// `2` was the B12 shape (plaintext `secret_values` → reference-only
21/// `secret_refs` + `secret_refs` drop from `normalized_answers`). `1` was the
22/// pre-B12 plaintext shape. Neither is deserialization-compatible with `3`
23/// (the `env_id` field is required, not `#[serde(default)]`).
24pub const SETUP_STATE_SCHEMA_VERSION: u32 = 3;
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct FormSpec {
28    pub id: String,
29    pub title: String,
30    pub version: String,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub description: Option<String>,
33    pub questions: Vec<QuestionSpec>,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37pub struct QuestionSpec {
38    pub id: String,
39    pub kind: QuestionKind,
40    pub title: String,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub description: Option<String>,
43    pub required: bool,
44    #[serde(default, skip_serializing_if = "Vec::is_empty")]
45    pub choices: Vec<String>,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub default_value: Option<Value>,
48    pub secret: bool,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "snake_case")]
53pub enum QuestionKind {
54    String,
55    Number,
56    Boolean,
57    Enum,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(tag = "type", rename_all = "snake_case")]
62pub enum SetupSpecInput {
63    Legacy {
64        spec: Value,
65    },
66    ProviderQa {
67        qa_output: Value,
68        #[serde(default)]
69        i18n: BTreeMap<String, String>,
70    },
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74pub struct PersistedSetupState {
75    pub schema_version: u32,
76    /// Environment id that minted this state (C7). The same scope that builds
77    /// the `secret://<env>/<bundle>/<provider>/<question>` URIs in
78    /// [`secret_refs`](Self::secret_refs). Re-running the wizard against a
79    /// different `--env` for the same provider in the same bundle is rejected
80    /// at persist time: an existing state's `env_id` is compared against the
81    /// active wizard scope's, and a mismatch fails closed rather than aliasing
82    /// two envs' secret refs onto the same provider path.
83    pub env_id: String,
84    pub provider_id: String,
85    pub source_kind: String,
86    pub form: FormSpec,
87    pub normalized_answers: BTreeMap<String, Value>,
88    pub non_secret_config: BTreeMap<String, Value>,
89    /// `secret://<env>/<bundle>/<provider_id>/<question_id>` references for
90    /// secret-marked answers. Plaintext never persists in this state; only the
91    /// reference is recorded (B12). The actual secret bytes are routed to the
92    /// env's secrets backend by the *consuming* wizard pipeline (greentic-setup
93    /// / greentic-operator `qa_persist` → DevStore), which writes under the
94    /// distinct `secrets://<env>/<tenant>/<team>/<provider>/<key>` scheme — a
95    /// separate address space. Phase D wires the resolver that bridges these
96    /// two URI families (A10's deferred handler-backed `SecretsSink`).
97    pub secret_refs: BTreeMap<String, SecretRef>,
98}
99
100pub fn form_spec_from_input(
101    input: &SetupSpecInput,
102    provider_id: &str,
103) -> Result<(String, FormSpec)> {
104    match input {
105        SetupSpecInput::Legacy { spec } => {
106            let parsed = match spec {
107                Value::String(raw) => legacy_formspec::parse_setup_spec_str(raw)?,
108                value => legacy_formspec::parse_setup_spec_value(value.clone())?,
109            };
110            Ok((
111                "legacy".to_string(),
112                legacy_formspec::setup_spec_to_form_spec(&parsed, provider_id),
113            ))
114        }
115        SetupSpecInput::ProviderQa { qa_output, i18n } => Ok((
116            "provider_qa".to_string(),
117            qa_bridge::provider_qa_to_form_spec(qa_output, i18n, provider_id),
118        )),
119    }
120}