Skip to main content

greentic_operator/demo/
build.rs

1use 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}