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, 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 pub setup_answers: serde_json::Map<String, serde_json::Value>,
58}
59
60#[derive(Clone, Debug, Serialize)]
62pub struct SetupStep {
63 pub kind: SetupStepKind,
64 pub description: String,
65 pub details: BTreeMap<String, String>,
66}
67
68#[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#[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#[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#[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#[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#[derive(Clone, Debug, Serialize, Deserialize)]
169pub struct PackRemoveSelection {
170 pub pack_identifier: String,
171 #[serde(default)]
172 pub scope: Option<PackScope>,
173}
174
175#[derive(Clone, Debug, Serialize, Deserialize)]
177pub struct PackDefaultSelection {
178 pub pack_identifier: String,
179 pub scope: PackScope,
180}
181
182#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
184#[serde(rename_all = "snake_case")]
185pub enum AccessOperation {
186 AllowAdd,
187 AllowRemove,
188}
189
190#[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#[derive(Clone, Debug, Serialize, Deserialize)]
202pub struct PackListing {
203 pub id: String,
204 pub label: String,
205 pub reference: String,
206}
207
208#[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#[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
230pub 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
247pub 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#[derive(Clone, Debug, Serialize)]
298pub struct QaSpec {
299 pub mode: String,
300 pub questions: Vec<QaQuestion>,
301}
302
303#[derive(Clone, Debug, Serialize)]
305pub struct QaQuestion {
306 pub id: String,
307 pub title: String,
308 pub required: bool,
309}
310
311pub 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}