1use 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#[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#[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#[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#[derive(Clone, Debug, Serialize)]
64pub struct SetupStep {
65 pub kind: SetupStepKind,
66 pub description: String,
67 pub details: BTreeMap<String, String>,
68}
69
70#[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#[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#[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#[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#[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#[derive(Clone, Debug, Serialize, Deserialize)]
171pub struct PackRemoveSelection {
172 pub pack_identifier: String,
173 #[serde(default)]
174 pub scope: Option<PackScope>,
175}
176
177#[derive(Clone, Debug, Serialize, Deserialize)]
179pub struct PackDefaultSelection {
180 pub pack_identifier: String,
181 pub scope: PackScope,
182}
183
184#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
186#[serde(rename_all = "snake_case")]
187pub enum AccessOperation {
188 AllowAdd,
189 AllowRemove,
190}
191
192#[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#[derive(Clone, Debug, Serialize, Deserialize)]
204pub struct PackListing {
205 pub id: String,
206 pub label: String,
207 pub reference: String,
208}
209
210#[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#[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
234pub 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
251pub 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#[derive(Clone, Debug, Serialize)]
302pub struct QaSpec {
303 pub mode: String,
304 pub questions: Vec<QaQuestion>,
305}
306
307#[derive(Clone, Debug, Serialize)]
309pub struct QaQuestion {
310 pub id: String,
311 pub title: String,
312 pub required: bool,
313}
314
315pub 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}