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