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