greentic_dev/
pack_cli.rs

1use std::collections::HashMap;
2use std::fs::File;
3use std::io::Read;
4use std::path::Path;
5use std::str::FromStr;
6
7use anyhow::{Context, Result, anyhow};
8use greentic_pack::builder::PackManifest;
9use greentic_pack::events::EventProviderSpec;
10use greentic_pack::plan::infer_base_deployment_plan;
11use greentic_pack::reader::{SigningPolicy, open_pack};
12use greentic_types::SecretRequirement;
13use greentic_types::component::ComponentManifest;
14use greentic_types::{EnvId, TenantCtx, TenantId};
15use serde_json::json;
16use zip::ZipArchive;
17
18use crate::cli::{PackEventsFormatArg, PackEventsListArgs, PackPlanArgs, PackPolicyArg};
19use crate::pack_temp::materialize_pack_path;
20
21#[derive(Copy, Clone, Debug)]
22pub enum PackEventsFormat {
23    Table,
24    Json,
25    Yaml,
26}
27
28impl From<PackEventsFormatArg> for PackEventsFormat {
29    fn from(value: PackEventsFormatArg) -> Self {
30        match value {
31            PackEventsFormatArg::Table => PackEventsFormat::Table,
32            PackEventsFormatArg::Json => PackEventsFormat::Json,
33            PackEventsFormatArg::Yaml => PackEventsFormat::Yaml,
34        }
35    }
36}
37
38impl From<PackPolicyArg> for SigningPolicy {
39    fn from(value: PackPolicyArg) -> Self {
40        match value {
41            PackPolicyArg::Devok => SigningPolicy::DevOk,
42            PackPolicyArg::Strict => SigningPolicy::Strict,
43        }
44    }
45}
46
47pub fn pack_inspect(path: &Path, policy: PackPolicyArg, json: bool) -> Result<()> {
48    let (temp, pack_path) = materialize_pack_path(path, false)?;
49    let load = open_pack(&pack_path, policy.into()).map_err(|err| anyhow!(err.message))?;
50    if json {
51        print_inspect_json(&load.manifest, &load.report, &load.sbom)?;
52    } else {
53        print_inspect_human(&load.manifest, &load.report, &load.sbom);
54    }
55    drop(temp);
56    Ok(())
57}
58
59pub fn pack_plan(args: &PackPlanArgs) -> Result<()> {
60    let (temp, pack_path) = materialize_pack_path(&args.input, args.verbose)?;
61    let tenant_ctx = build_tenant_ctx(&args.environment, &args.tenant)?;
62    let plan = plan_for_pack(&pack_path, &tenant_ctx, &args.environment)?;
63
64    if args.json {
65        println!("{}", serde_json::to_string(&plan)?);
66    } else {
67        println!("{}", serde_json::to_string_pretty(&plan)?);
68    }
69
70    drop(temp);
71    Ok(())
72}
73
74pub fn pack_events_list(args: &PackEventsListArgs) -> Result<()> {
75    let (temp, pack_path) = materialize_pack_path(&args.path, args.verbose)?;
76    let load = open_pack(&pack_path, SigningPolicy::DevOk).map_err(|err| anyhow!(err.message))?;
77    let providers: Vec<EventProviderSpec> = load
78        .manifest
79        .meta
80        .events
81        .as_ref()
82        .map(|events| events.providers.clone())
83        .unwrap_or_default();
84
85    match PackEventsFormat::from(args.format) {
86        PackEventsFormat::Table => print_table(&providers),
87        PackEventsFormat::Json => print_json(&providers)?,
88        PackEventsFormat::Yaml => print_yaml(&providers)?,
89    }
90
91    drop(temp);
92    Ok(())
93}
94
95fn plan_for_pack(
96    path: &Path,
97    tenant: &TenantCtx,
98    environment: &str,
99) -> Result<greentic_types::deployment::DeploymentPlan> {
100    let load = open_pack(path, SigningPolicy::DevOk).map_err(|err| anyhow!(err.message))?;
101    let connectors = load.manifest.meta.annotations.get("connectors");
102    let components = load_component_manifests(path, &load.manifest)?;
103    let secret_requirements = load_secret_requirements(path)?;
104
105    Ok(infer_base_deployment_plan(
106        &load.manifest.meta,
107        &load.manifest.flows,
108        connectors,
109        &components,
110        secret_requirements,
111        tenant,
112        environment,
113    ))
114}
115
116fn build_tenant_ctx(environment: &str, tenant: &str) -> Result<TenantCtx> {
117    let env_id = EnvId::from_str(environment)
118        .with_context(|| format!("invalid environment id `{}`", environment))?;
119    let tenant_id =
120        TenantId::from_str(tenant).with_context(|| format!("invalid tenant id `{}`", tenant))?;
121    Ok(TenantCtx::new(env_id, tenant_id))
122}
123
124fn load_component_manifests(
125    pack_path: &Path,
126    pack_manifest: &PackManifest,
127) -> Result<HashMap<String, ComponentManifest>> {
128    let file =
129        File::open(pack_path).with_context(|| format!("failed to open {}", pack_path.display()))?;
130    let mut archive = ZipArchive::new(file)
131        .with_context(|| format!("{} is not a valid gtpack archive", pack_path.display()))?;
132
133    let mut manifests = HashMap::new();
134    for component in &pack_manifest.components {
135        if let Some(manifest_path) = component.manifest_file.as_deref() {
136            let mut entry = archive
137                .by_name(manifest_path)
138                .with_context(|| format!("component manifest `{}` missing", manifest_path))?;
139            let manifest: ComponentManifest =
140                serde_json::from_reader(&mut entry).with_context(|| {
141                    format!("failed to parse component manifest `{}`", manifest_path)
142                })?;
143            manifests.insert(component.name.clone(), manifest);
144        }
145    }
146
147    Ok(manifests)
148}
149
150fn load_secret_requirements(path: &Path) -> Result<Option<Vec<SecretRequirement>>> {
151    let file = File::open(path).with_context(|| format!("failed to open {}", path.display()))?;
152    let mut archive = ZipArchive::new(file)
153        .with_context(|| format!("{} is not a valid gtpack archive", path.display()))?;
154
155    for name in [
156        "assets/secret-requirements.json",
157        "secret-requirements.json",
158    ] {
159        if let Ok(mut entry) = archive.by_name(name) {
160            let mut buf = String::new();
161            entry
162                .read_to_string(&mut buf)
163                .context("failed to read secret requirements file")?;
164            let reqs: Vec<SecretRequirement> =
165                serde_json::from_str(&buf).context("secret requirements file is invalid JSON")?;
166            return Ok(Some(reqs));
167        }
168    }
169
170    Ok(None)
171}
172
173fn print_inspect_human(
174    manifest: &PackManifest,
175    report: &greentic_pack::reader::VerifyReport,
176    sbom: &[greentic_pack::builder::SbomEntry],
177) {
178    println!(
179        "Pack: {} ({})",
180        manifest.meta.pack_id, manifest.meta.version
181    );
182    println!("Flows: {}", manifest.flows.len());
183    println!("Components: {}", manifest.components.len());
184    println!("SBOM entries: {}", sbom.len());
185    println!("Signature OK: {}", report.signature_ok);
186    println!("SBOM OK: {}", report.sbom_ok);
187    if report.warnings.is_empty() {
188        println!("Warnings: none");
189    } else {
190        println!("Warnings:");
191        for warning in &report.warnings {
192            println!("  - {}", warning);
193        }
194    }
195}
196
197fn print_inspect_json(
198    manifest: &PackManifest,
199    report: &greentic_pack::reader::VerifyReport,
200    sbom: &[greentic_pack::builder::SbomEntry],
201) -> Result<()> {
202    let payload = json!({
203        "manifest": {
204            "pack_id": manifest.meta.pack_id,
205            "version": manifest.meta.version,
206            "flows": manifest.flows.len(),
207            "components": manifest.components.len(),
208        },
209        "report": {
210            "signature_ok": report.signature_ok,
211            "sbom_ok": report.sbom_ok,
212            "warnings": report.warnings,
213        },
214        "sbom": sbom,
215    });
216    println!("{}", serde_json::to_string_pretty(&payload)?);
217    Ok(())
218}
219
220fn print_table(providers: &[EventProviderSpec]) {
221    if providers.is_empty() {
222        println!("No events providers declared.");
223        return;
224    }
225
226    println!(
227        "{:<20} {:<8} {:<28} {:<12} TOPICS",
228        "NAME", "KIND", "COMPONENT", "TRANSPORT"
229    );
230    for provider in providers {
231        let transport = provider
232            .capabilities
233            .transport
234            .as_ref()
235            .map(|t| t.to_string())
236            .unwrap_or_else(|| "-".to_string());
237        let topics = summarize_topics(&provider.capabilities.topics);
238        println!(
239            "{:<20} {:<8} {:<28} {:<12} {}",
240            provider.name, provider.kind, provider.component, transport, topics
241        );
242    }
243}
244
245fn print_json(providers: &[EventProviderSpec]) -> Result<()> {
246    let payload = json!(providers);
247    println!("{}", serde_json::to_string_pretty(&payload)?);
248    Ok(())
249}
250
251fn print_yaml(providers: &[EventProviderSpec]) -> Result<()> {
252    let doc = serde_yaml_bw::to_string(providers)?;
253    println!("{doc}");
254    Ok(())
255}
256
257fn summarize_topics(topics: &[String]) -> String {
258    if topics.is_empty() {
259        return "-".to_string();
260    }
261    let combined = topics.join(", ");
262    if combined.len() > 60 {
263        format!("{}...", &combined[..57])
264    } else {
265        combined
266    }
267}