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}