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;
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 setup_answers: serde_json::Map<String, serde_json::Value>,
57}
58
59#[derive(Clone, Debug, Serialize)]
61pub struct SetupStep {
62 pub kind: SetupStepKind,
63 pub description: String,
64 pub details: BTreeMap<String, String>,
65}
66
67#[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#[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#[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#[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#[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#[derive(Clone, Debug, Serialize, Deserialize)]
167pub struct PackRemoveSelection {
168 pub pack_identifier: String,
169 #[serde(default)]
170 pub scope: Option<PackScope>,
171}
172
173#[derive(Clone, Debug, Serialize, Deserialize)]
175pub struct PackDefaultSelection {
176 pub pack_identifier: String,
177 pub scope: PackScope,
178}
179
180#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
182#[serde(rename_all = "snake_case")]
183pub enum AccessOperation {
184 AllowAdd,
185 AllowRemove,
186}
187
188#[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#[derive(Clone, Debug, Serialize, Deserialize)]
200pub struct PackListing {
201 pub id: String,
202 pub label: String,
203 pub reference: String,
204}
205
206#[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#[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
228pub 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
245pub 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#[derive(Clone, Debug, Serialize)]
296pub struct QaSpec {
297 pub mode: String,
298 pub questions: Vec<QaQuestion>,
299}
300
301#[derive(Clone, Debug, Serialize)]
303pub struct QaQuestion {
304 pub id: String,
305 pub title: String,
306 pub required: bool,
307}
308
309pub 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}