greentic_dev/
pack_cli.rs

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