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}