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