greentic_operator/demo/
build.rs1use std::collections::{BTreeMap, BTreeSet};
2use std::path::{Path, PathBuf};
3
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone)]
7pub struct BuildOptions {
8 pub out_dir: PathBuf,
9 pub tenant: Option<String>,
10 pub team: Option<String>,
11 pub allow_pack_dirs: bool,
12 pub only_used_providers: bool,
13 pub run_doctor: bool,
14}
15
16#[derive(Debug, Deserialize, Serialize)]
17struct ResolvedManifest {
18 version: String,
19 tenant: String,
20 team: Option<String>,
21 project_root: String,
22 providers: BTreeMap<String, Vec<String>>,
23 packs: Vec<String>,
24 env_passthrough: Vec<String>,
25 policy: serde_yaml_bw::Value,
26}
27
28pub fn build_bundle(
29 project_root: &Path,
30 options: BuildOptions,
31 pack_command: Option<&Path>,
32) -> anyhow::Result<()> {
33 if options.run_doctor && std::env::var("GREENTIC_OPERATOR_SKIP_DOCTOR").is_err() {
34 let pack_command = pack_command
35 .ok_or_else(|| anyhow::anyhow!("greentic-pack command is required for demo doctor"))?;
36 crate::doctor::run_doctor(
37 project_root,
38 crate::doctor::DoctorScope::All,
39 crate::doctor::DoctorOptions {
40 tenant: options.tenant.clone(),
41 team: options.team.clone(),
42 strict: false,
43 validator_packs: Vec::new(),
44 },
45 pack_command,
46 )?;
47 }
48
49 let resolved_dir = project_root.join("state").join("resolved");
50 if !resolved_dir.exists() {
51 return Err(anyhow::anyhow!(
52 "Resolved manifests not found. Run `greentic-operator dev sync` first."
53 ));
54 }
55
56 let manifests = select_manifests(
57 &resolved_dir,
58 options.tenant.as_deref(),
59 options.team.as_deref(),
60 )?;
61 if manifests.is_empty() {
62 return Err(anyhow::anyhow!(
63 "No resolved manifests found for selection."
64 ));
65 }
66
67 let bundle_root = options.out_dir;
68 std::fs::create_dir_all(&bundle_root)?;
69 std::fs::create_dir_all(bundle_root.join("providers"))?;
70 std::fs::create_dir_all(bundle_root.join("packs"))?;
71 std::fs::create_dir_all(bundle_root.join("tenants"))?;
72 std::fs::create_dir_all(bundle_root.join("resolved"))?;
73 std::fs::create_dir_all(bundle_root.join("state"))?;
74
75 let mut used_provider_paths = BTreeSet::new();
76 let mut loaded_manifests = Vec::new();
77 for manifest_path in &manifests {
78 let manifest = load_manifest(manifest_path)?;
79 for packs in manifest.providers.values() {
80 for pack in packs {
81 used_provider_paths.insert(pack.clone());
82 }
83 }
84 loaded_manifests.push((manifest_path.clone(), manifest));
85 }
86
87 if options.only_used_providers {
88 for provider_path in &used_provider_paths {
89 let from = project_root.join(provider_path);
90 let to = bundle_root.join(provider_path);
91 copy_file(&from, &to)?;
92 }
93 } else {
94 copy_dir(
95 project_root.join("providers"),
96 bundle_root.join("providers"),
97 )?;
98 }
99
100 let mut tenants_to_copy = BTreeSet::new();
101 for (manifest_path, mut manifest) in loaded_manifests {
102 tenants_to_copy.insert(manifest.tenant.clone());
103
104 let pack_paths = manifest.packs.clone();
105 for pack in pack_paths {
106 let pack_path = project_root.join(&pack);
107 if pack.ends_with(".gtpack") {
108 copy_file(&pack_path, &bundle_root.join(&pack))?;
109 } else {
110 if !options.allow_pack_dirs {
111 return Err(anyhow::anyhow!(
112 "Pack directory not allowed in demo bundle: {} (use --allow-pack-dirs)",
113 pack
114 ));
115 }
116 eprintln!(
117 "{}",
118 crate::operator_i18n::trf(
119 "demo.build.warn_copying_pack_directory",
120 "Warning: copying pack directory into demo bundle (not portable): {}",
121 &[&pack]
122 )
123 );
124 copy_dir(pack_path, bundle_root.join(&pack))?;
125 }
126 }
127
128 manifest.project_root = "./".to_string();
129 let filename = manifest_path
130 .file_name()
131 .ok_or_else(|| anyhow::anyhow!("Invalid manifest filename"))?;
132 let out_path = bundle_root.join("resolved").join(filename);
133 write_manifest(&out_path, &manifest)?;
134 }
135
136 for tenant in tenants_to_copy {
137 let tenant_path = project_root.join("tenants").join(&tenant);
138 if tenant_path.exists() {
139 copy_dir(tenant_path, bundle_root.join("tenants").join(&tenant))?;
140 }
141 }
142
143 let demo_meta = bundle_root.join("greentic.demo.yaml");
144 write_demo_metadata(&demo_meta)?;
145
146 Ok(())
147}
148
149fn select_manifests(
150 resolved_dir: &Path,
151 tenant: Option<&str>,
152 team: Option<&str>,
153) -> anyhow::Result<Vec<PathBuf>> {
154 let mut manifests = Vec::new();
155 if let Some(tenant) = tenant {
156 let filename = match team {
157 Some(team) => format!("{tenant}.{team}.yaml"),
158 None => format!("{tenant}.yaml"),
159 };
160 let path = resolved_dir.join(filename);
161 if path.exists() {
162 manifests.push(path);
163 }
164 return Ok(manifests);
165 }
166
167 for entry in std::fs::read_dir(resolved_dir)? {
168 let entry = entry?;
169 if entry.file_type()?.is_file() {
170 let path = entry.path();
171 if path.extension().and_then(|ext| ext.to_str()) == Some("yaml") {
172 manifests.push(path);
173 }
174 }
175 }
176 manifests.sort();
177 Ok(manifests)
178}
179
180fn load_manifest(path: &Path) -> anyhow::Result<ResolvedManifest> {
181 let contents = std::fs::read_to_string(path)?;
182 let manifest: ResolvedManifest = serde_yaml_bw::from_str(&contents)?;
183 Ok(manifest)
184}
185
186fn write_manifest(path: &Path, manifest: &ResolvedManifest) -> anyhow::Result<()> {
187 if let Some(parent) = path.parent() {
188 std::fs::create_dir_all(parent)?;
189 }
190 let yaml = serde_yaml_bw::to_string(manifest)?;
191 std::fs::write(path, yaml)?;
192 Ok(())
193}
194
195fn write_demo_metadata(path: &Path) -> anyhow::Result<()> {
196 let contents = "version: \"1\"\nproject_root: \"./\"\n";
197 std::fs::write(path, contents)?;
198 Ok(())
199}
200
201fn copy_file(from: &Path, to: &Path) -> anyhow::Result<()> {
202 if let Some(parent) = to.parent() {
203 std::fs::create_dir_all(parent)?;
204 }
205 std::fs::copy(from, to)?;
206 Ok(())
207}
208
209fn copy_dir(from: PathBuf, to: PathBuf) -> anyhow::Result<()> {
210 if !from.exists() {
211 return Ok(());
212 }
213 std::fs::create_dir_all(&to)?;
214 for entry in std::fs::read_dir(&from)? {
215 let entry = entry?;
216 let path = entry.path();
217 let target = to.join(entry.file_name());
218 if entry.file_type()?.is_dir() {
219 copy_dir(path, target)?;
220 } else {
221 copy_file(&path, &target)?;
222 }
223 }
224 Ok(())
225}