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