Skip to main content

greentic_setup/
plan.rs

1//! Setup plan types — mode, steps, and metadata for bundle lifecycle operations.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::path::PathBuf;
5use std::str::FromStr;
6
7use serde::{Deserialize, Serialize};
8
9use crate::deployment_targets::DeploymentTargetRecord;
10use crate::platform_setup::StaticRoutesPolicy;
11
12/// The operation mode for a setup plan.
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum SetupMode {
15    Create,
16    Update,
17    Remove,
18}
19
20impl SetupMode {
21    pub fn as_str(self) -> &'static str {
22        match self {
23            Self::Create => "create",
24            Self::Update => "update",
25            Self::Remove => "remove",
26        }
27    }
28}
29
30/// A complete setup plan with ordered steps and metadata.
31#[derive(Clone, Debug, Serialize)]
32pub struct SetupPlan {
33    pub mode: String,
34    pub dry_run: bool,
35    pub bundle: PathBuf,
36    pub steps: Vec<SetupStep>,
37    pub metadata: SetupPlanMetadata,
38}
39
40/// Metadata carried alongside the plan for execution.
41#[derive(Clone, Debug, Serialize)]
42pub struct SetupPlanMetadata {
43    pub bundle_name: Option<String>,
44    pub pack_refs: Vec<String>,
45    pub tenants: Vec<TenantSelection>,
46    pub default_assignments: Vec<PackDefaultSelection>,
47    pub providers: Vec<String>,
48    pub update_ops: BTreeSet<UpdateOp>,
49    pub remove_targets: BTreeSet<RemoveTarget>,
50    pub packs_remove: Vec<PackRemoveSelection>,
51    pub providers_remove: Vec<String>,
52    pub tenants_remove: Vec<TenantSelection>,
53    pub access_changes: Vec<AccessChangeSelection>,
54    pub static_routes: StaticRoutesPolicy,
55    pub deployment_targets: Vec<DeploymentTargetRecord>,
56    pub setup_answers: serde_json::Map<String, serde_json::Value>,
57}
58
59/// A single step in the setup plan.
60#[derive(Clone, Debug, Serialize)]
61pub struct SetupStep {
62    pub kind: SetupStepKind,
63    pub description: String,
64    pub details: BTreeMap<String, String>,
65}
66
67/// The kind of operation a setup step performs.
68#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
69#[serde(rename_all = "snake_case")]
70pub enum SetupStepKind {
71    NoOp,
72    ResolvePacks,
73    CreateBundle,
74    AddPacksToBundle,
75    ValidateCapabilities,
76    ApplyPackSetup,
77    WriteGmapRules,
78    RunResolver,
79    CopyResolvedManifest,
80    ValidateBundle,
81    BuildFlowIndex,
82}
83
84/// Tenant + optional team + allow-list paths.
85#[derive(Clone, Debug, Serialize, Deserialize)]
86pub struct TenantSelection {
87    pub tenant: String,
88    pub team: Option<String>,
89    pub allow_paths: Vec<String>,
90}
91
92/// Update operations that can be combined in an update plan.
93#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
94#[serde(rename_all = "snake_case")]
95pub enum UpdateOp {
96    PacksAdd,
97    PacksRemove,
98    ProvidersAdd,
99    ProvidersRemove,
100    TenantsAdd,
101    TenantsRemove,
102    AccessChange,
103}
104
105impl UpdateOp {
106    pub fn parse(value: &str) -> Option<Self> {
107        match value {
108            "packs_add" => Some(Self::PacksAdd),
109            "packs_remove" => Some(Self::PacksRemove),
110            "providers_add" => Some(Self::ProvidersAdd),
111            "providers_remove" => Some(Self::ProvidersRemove),
112            "tenants_add" => Some(Self::TenantsAdd),
113            "tenants_remove" => Some(Self::TenantsRemove),
114            "access_change" => Some(Self::AccessChange),
115            _ => None,
116        }
117    }
118}
119
120impl FromStr for UpdateOp {
121    type Err = ();
122
123    fn from_str(value: &str) -> Result<Self, Self::Err> {
124        Self::parse(value).ok_or(())
125    }
126}
127
128/// Remove targets for a remove plan.
129#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
130#[serde(rename_all = "snake_case")]
131pub enum RemoveTarget {
132    Packs,
133    Providers,
134    TenantsTeams,
135}
136
137impl RemoveTarget {
138    pub fn parse(value: &str) -> Option<Self> {
139        match value {
140            "packs" => Some(Self::Packs),
141            "providers" => Some(Self::Providers),
142            "tenants_teams" => Some(Self::TenantsTeams),
143            _ => None,
144        }
145    }
146}
147
148impl FromStr for RemoveTarget {
149    type Err = ();
150
151    fn from_str(value: &str) -> Result<Self, Self::Err> {
152        Self::parse(value).ok_or(())
153    }
154}
155
156/// Pack scope for default assignments and removal.
157#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
158#[serde(rename_all = "snake_case")]
159pub enum PackScope {
160    Bundle,
161    Global,
162    Tenant { tenant_id: String },
163    Team { tenant_id: String, team_id: String },
164}
165
166/// Selection for removing a pack.
167#[derive(Clone, Debug, Serialize, Deserialize)]
168pub struct PackRemoveSelection {
169    pub pack_identifier: String,
170    #[serde(default)]
171    pub scope: Option<PackScope>,
172}
173
174/// Selection for setting a pack as default.
175#[derive(Clone, Debug, Serialize, Deserialize)]
176pub struct PackDefaultSelection {
177    pub pack_identifier: String,
178    pub scope: PackScope,
179}
180
181/// Access rule change operation.
182#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
183#[serde(rename_all = "snake_case")]
184pub enum AccessOperation {
185    AllowAdd,
186    AllowRemove,
187}
188
189/// Access rule change selection.
190#[derive(Clone, Debug, Serialize, Deserialize)]
191pub struct AccessChangeSelection {
192    pub pack_id: String,
193    pub operation: AccessOperation,
194    pub tenant_id: String,
195    #[serde(default)]
196    pub team_id: Option<String>,
197}
198
199/// Pack catalog listing entry.
200#[derive(Clone, Debug, Serialize, Deserialize)]
201pub struct PackListing {
202    pub id: String,
203    pub label: String,
204    pub reference: String,
205}
206
207/// Resolved pack information after fetching from a registry.
208#[derive(Clone, Debug, Serialize)]
209pub struct ResolvedPackInfo {
210    pub source_ref: String,
211    pub mapped_ref: String,
212    pub resolved_digest: String,
213    pub pack_id: String,
214    pub entry_flows: Vec<String>,
215    pub cached_path: PathBuf,
216    pub output_path: PathBuf,
217}
218
219/// Report from executing a setup plan.
220#[derive(Clone, Debug, Serialize)]
221pub struct SetupExecutionReport {
222    pub bundle: PathBuf,
223    pub resolved_packs: Vec<ResolvedPackInfo>,
224    pub resolved_manifests: Vec<PathBuf>,
225    pub provider_updates: usize,
226    pub warnings: Vec<String>,
227}
228
229/// Build a step with a kind, description, and key-value details.
230pub fn step<const N: usize>(
231    kind: SetupStepKind,
232    description: &str,
233    details: [(&str, String); N],
234) -> SetupStep {
235    let mut map = BTreeMap::new();
236    for (key, value) in details {
237        map.insert(key.to_string(), value);
238    }
239    SetupStep {
240        kind,
241        description: description.to_string(),
242        details: map,
243    }
244}
245
246/// Load a pack catalog from a JSON/YAML file.
247pub fn load_catalog_from_file(path: &std::path::Path) -> anyhow::Result<Vec<PackListing>> {
248    use anyhow::Context;
249    let raw = std::fs::read_to_string(path)
250        .with_context(|| format!("read catalog file {}", path.display()))?;
251
252    if let Ok(parsed) = serde_json::from_str::<Vec<PackListing>>(&raw)
253        .or_else(|_| serde_yaml_bw::from_str::<Vec<PackListing>>(&raw))
254    {
255        return Ok(parsed);
256    }
257
258    let registry: ProviderRegistryFile = serde_json::from_str(&raw)
259        .or_else(|_| serde_yaml_bw::from_str(&raw))
260        .with_context(|| format!("parse catalog/provider registry file {}", path.display()))?;
261    Ok(registry
262        .items
263        .into_iter()
264        .map(|item| PackListing {
265            id: item.id,
266            label: item.label.fallback,
267            reference: item.reference,
268        })
269        .collect())
270}
271
272#[derive(Clone, Debug, Serialize, Deserialize)]
273struct ProviderRegistryFile {
274    #[serde(default)]
275    registry_version: Option<String>,
276    #[serde(default)]
277    items: Vec<ProviderRegistryItem>,
278}
279
280#[derive(Clone, Debug, Serialize, Deserialize)]
281struct ProviderRegistryItem {
282    id: String,
283    label: ProviderRegistryLabel,
284    #[serde(alias = "ref")]
285    reference: String,
286}
287
288#[derive(Clone, Debug, Serialize, Deserialize)]
289struct ProviderRegistryLabel {
290    #[serde(default)]
291    i18n_key: Option<String>,
292    fallback: String,
293}
294
295/// QA spec returned by the wizard mode query.
296#[derive(Clone, Debug, Serialize)]
297pub struct QaSpec {
298    pub mode: String,
299    pub questions: Vec<QaQuestion>,
300}
301
302/// A question in the wizard QA spec.
303#[derive(Clone, Debug, Serialize)]
304pub struct QaQuestion {
305    pub id: String,
306    pub title: String,
307    pub required: bool,
308}
309
310/// Return the QA spec (questions) for a given setup mode.
311pub fn spec(mode: SetupMode) -> QaSpec {
312    QaSpec {
313        mode: mode.as_str().to_string(),
314        questions: vec![
315            QaQuestion {
316                id: "operator.bundle.path".to_string(),
317                title: "Bundle output path".to_string(),
318                required: true,
319            },
320            QaQuestion {
321                id: "operator.packs.refs".to_string(),
322                title: "Pack refs (catalog + custom)".to_string(),
323                required: false,
324            },
325            QaQuestion {
326                id: "operator.tenants".to_string(),
327                title: "Tenants and optional teams".to_string(),
328                required: true,
329            },
330            QaQuestion {
331                id: "operator.allow.paths".to_string(),
332                title: "Allow rules as PACK[/FLOW[/NODE]]".to_string(),
333                required: false,
334            },
335        ],
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342
343    #[test]
344    fn setup_mode_roundtrip() {
345        assert_eq!(SetupMode::Create.as_str(), "create");
346        assert_eq!(SetupMode::Update.as_str(), "update");
347        assert_eq!(SetupMode::Remove.as_str(), "remove");
348    }
349
350    #[test]
351    fn update_op_parse() {
352        assert_eq!(UpdateOp::parse("packs_add"), Some(UpdateOp::PacksAdd));
353        assert_eq!(UpdateOp::parse("unknown"), None);
354    }
355
356    #[test]
357    fn remove_target_parse() {
358        assert_eq!(RemoveTarget::parse("packs"), Some(RemoveTarget::Packs));
359        assert_eq!(RemoveTarget::parse("xyz"), None);
360    }
361
362    #[test]
363    fn qa_spec_has_required_questions() {
364        let s = spec(SetupMode::Create);
365        assert!(s.questions.iter().any(|q| q.required));
366    }
367}