1use std::collections::{HashMap, HashSet};
2use std::fs::{self, File};
3use std::io::{Read, Write};
4use std::path::{Path, PathBuf};
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::ExtensionRef;
13use greentic_types::SecretRequirement;
14use greentic_types::component::ComponentManifest;
15use greentic_types::provider::{
16 ProviderDecl, ProviderExtensionInline, ProviderManifest, ProviderRuntimeRef,
17};
18use greentic_types::{EnvId, TenantCtx, TenantId};
19use serde_json::json;
20use zip::ZipArchive;
21
22use crate::cli::{
23 PackEventsFormatArg, PackEventsListArgs, PackNewProviderArgs, PackPlanArgs, PackPolicyArg,
24};
25use crate::pack_init::slugify;
26use crate::pack_temp::materialize_pack_path;
27
28const PROVIDER_EXTENSION_ID: &str = "greentic.provider-extension.v1";
29
30#[derive(Copy, Clone, Debug)]
31pub enum PackEventsFormat {
32 Table,
33 Json,
34 Yaml,
35}
36
37impl From<PackEventsFormatArg> for PackEventsFormat {
38 fn from(value: PackEventsFormatArg) -> Self {
39 match value {
40 PackEventsFormatArg::Table => PackEventsFormat::Table,
41 PackEventsFormatArg::Json => PackEventsFormat::Json,
42 PackEventsFormatArg::Yaml => PackEventsFormat::Yaml,
43 }
44 }
45}
46
47impl From<PackPolicyArg> for SigningPolicy {
48 fn from(value: PackPolicyArg) -> Self {
49 match value {
50 PackPolicyArg::Devok => SigningPolicy::DevOk,
51 PackPolicyArg::Strict => SigningPolicy::Strict,
52 }
53 }
54}
55
56pub fn pack_inspect(path: &Path, policy: PackPolicyArg, json: bool) -> Result<()> {
57 let (temp, pack_path) = materialize_pack_path(path, false)?;
58 let load = open_pack(&pack_path, policy.into()).map_err(|err| anyhow!(err.message))?;
59 if json {
60 print_inspect_json(&load.manifest, &load.report, &load.sbom)?;
61 } else {
62 print_inspect_human(&load.manifest, &load.report, &load.sbom);
63 }
64 drop(temp);
65 Ok(())
66}
67
68pub fn pack_plan(args: &PackPlanArgs) -> Result<()> {
69 let (temp, pack_path) = materialize_pack_path(&args.input, args.verbose)?;
70 let tenant_ctx = build_tenant_ctx(&args.environment, &args.tenant)?;
71 let plan = plan_for_pack(&pack_path, &tenant_ctx, &args.environment)?;
72
73 if args.json {
74 println!("{}", serde_json::to_string(&plan)?);
75 } else {
76 println!("{}", serde_json::to_string_pretty(&plan)?);
77 }
78
79 drop(temp);
80 Ok(())
81}
82
83pub fn pack_new_provider(args: &PackNewProviderArgs) -> Result<()> {
84 let (mut manifest, location, pack_root) = load_manifest(&args.pack)?;
85
86 let runtime = parse_runtime_ref(&args.runtime)?;
87 let config_ref = args
88 .manifest
89 .as_ref()
90 .map(|p| p.display().to_string())
91 .unwrap_or_else(|| format!("providers/{}/provider.yaml", slugify(&args.id)));
92
93 let mut decl = ProviderDecl {
94 provider_type: args.id.clone(),
95 capabilities: Vec::new(),
96 ops: Vec::new(),
97 config_schema_ref: config_ref.clone(),
98 state_schema_ref: None,
99 runtime,
100 docs_ref: None,
101 };
102 if let Some(kind) = &args.kind {
103 decl.capabilities.push(kind.clone());
104 }
105
106 let mut inline = load_provider_extension(&manifest)?;
107 if let Some(existing) = inline
108 .providers
109 .iter()
110 .position(|p| p.provider_type == args.id)
111 {
112 if !args.force {
113 anyhow::bail!(
114 "provider `{}` already exists; pass --force to update",
115 args.id
116 );
117 }
118 inline.providers.remove(existing);
119 }
120 inline.providers.push(decl.clone());
121 inline
122 .providers
123 .sort_by(|a, b| a.provider_type.cmp(&b.provider_type));
124 validate_provider_extension(&inline)?;
125
126 if args.json {
127 println!("{}", serde_json::to_string_pretty(&decl)?);
128 }
129
130 if !args.dry_run {
131 set_provider_extension(&mut manifest, &inline)?;
132 write_manifest(location, &manifest)?;
133 if args.scaffold_files {
134 scaffold_provider_manifest(&pack_root, &config_ref, &decl)?;
135 }
136 }
137
138 Ok(())
139}
140
141fn scaffold_provider_manifest(
142 pack_root: &Path,
143 manifest_ref: &str,
144 decl: &ProviderDecl,
145) -> Result<()> {
146 let path = pack_root.join(manifest_ref);
147 let parent = path
148 .parent()
149 .with_context(|| format!("cannot derive parent for {}", path.display()))?;
150 fs::create_dir_all(parent).with_context(|| format!("failed to create {}", parent.display()))?;
151 let provider_manifest = ProviderManifest {
152 provider_type: decl.provider_type.clone(),
153 capabilities: decl.capabilities.clone(),
154 ops: decl.ops.clone(),
155 config_schema_ref: Some(decl.config_schema_ref.clone()),
156 state_schema_ref: decl.state_schema_ref.clone(),
157 };
158 let serialized = serde_yaml_bw::to_string(&provider_manifest)?;
159 fs::write(&path, serialized).with_context(|| format!("failed to write {}", path.display()))?;
160 Ok(())
161}
162
163fn parse_runtime_ref(input: &str) -> Result<ProviderRuntimeRef> {
164 let (left, world) = input
165 .rsplit_once('@')
166 .context("runtime must be in form component_ref::export@world")?;
167 let (component_ref, export) = left
168 .split_once("::")
169 .context("runtime must be in form component_ref::export@world")?;
170 Ok(ProviderRuntimeRef {
171 component_ref: component_ref.to_string(),
172 export: export.to_string(),
173 world: world.to_string(),
174 })
175}
176
177fn load_provider_extension(
178 manifest: &greentic_types::PackManifest,
179) -> Result<ProviderExtensionInline> {
180 let mut inline = ProviderExtensionInline::default();
181 if let Some(inline_ref) = manifest
182 .extensions
183 .as_ref()
184 .and_then(|exts| exts.get(PROVIDER_EXTENSION_ID))
185 .and_then(|ext| ext.inline.as_ref())
186 {
187 inline = match inline_ref {
188 greentic_types::pack_manifest::ExtensionInline::Provider(value) => value.clone(),
189 greentic_types::pack_manifest::ExtensionInline::Other(value) => {
190 serde_json::from_value(value.clone()).unwrap_or_default()
191 }
192 };
193 }
194 Ok(inline)
195}
196
197fn set_provider_extension(
198 manifest: &mut greentic_types::PackManifest,
199 inline: &ProviderExtensionInline,
200) -> Result<()> {
201 let extensions = manifest.extensions.get_or_insert_with(Default::default);
202 let entry = extensions
203 .entry(PROVIDER_EXTENSION_ID.to_string())
204 .or_insert_with(|| ExtensionRef {
205 kind: PROVIDER_EXTENSION_ID.to_string(),
206 version: "1.0.0".to_string(),
207 digest: None,
208 location: None,
209 inline: None,
210 });
211 entry.inline = Some(greentic_types::pack_manifest::ExtensionInline::Provider(
212 inline.clone(),
213 ));
214 Ok(())
215}
216
217fn validate_provider_extension(inline: &ProviderExtensionInline) -> Result<()> {
218 let mut seen = HashSet::new();
219 for provider in &inline.providers {
220 if provider.provider_type.trim().is_empty() {
221 anyhow::bail!("provider_type must not be empty");
222 }
223 if !seen.insert(provider.provider_type.as_str()) {
224 anyhow::bail!("duplicate provider_type `{}`", provider.provider_type);
225 }
226 if provider.runtime.component_ref.trim().is_empty()
227 || provider.runtime.export.trim().is_empty()
228 || provider.runtime.world.trim().is_empty()
229 {
230 anyhow::bail!(
231 "runtime fields must be set for provider `{}`",
232 provider.provider_type
233 );
234 }
235 }
236 Ok(())
237}
238
239enum ManifestLocation {
240 File(PathBuf),
241 Gtpack(PathBuf),
242}
243
244fn load_manifest(path: &Path) -> Result<(greentic_types::PackManifest, ManifestLocation, PathBuf)> {
245 if path.is_dir() {
246 let dist = path.join("dist/manifest.cbor");
247 let root_manifest = path.join("manifest.cbor");
248 let target = if dist.exists() {
249 dist
250 } else if root_manifest.exists() {
251 root_manifest
252 } else {
253 anyhow::bail!(
254 "pack path {} is a directory but manifest.cbor not found (looked in ./dist/ and root)",
255 path.display()
256 );
257 };
258 let bytes = fs::read(&target)
259 .with_context(|| format!("failed to read manifest {}", target.display()))?;
260 let manifest = greentic_types::decode_pack_manifest(&bytes)?;
261 return Ok((manifest, ManifestLocation::File(target), path.to_path_buf()));
262 }
263
264 if path.extension().is_some_and(|ext| ext == "gtpack") {
265 let mut archive = zip::ZipArchive::new(File::open(path).context("open gtpack")?)
266 .context("read gtpack zip")?;
267 let mut manifest_bytes = Vec::new();
268 archive
269 .by_name("manifest.cbor")
270 .context("manifest.cbor missing in gtpack")?
271 .read_to_end(&mut manifest_bytes)
272 .context("read manifest.cbor")?;
273 let manifest = greentic_types::decode_pack_manifest(&manifest_bytes)?;
274 return Ok((
275 manifest,
276 ManifestLocation::Gtpack(path.to_path_buf()),
277 path.parent()
278 .map(Path::to_path_buf)
279 .unwrap_or_else(|| PathBuf::from(".")),
280 ));
281 }
282
283 let bytes =
284 fs::read(path).with_context(|| format!("failed to read manifest {}", path.display()))?;
285 let manifest = greentic_types::decode_pack_manifest(&bytes)?;
286 let parent = path
287 .parent()
288 .map(Path::to_path_buf)
289 .unwrap_or_else(|| PathBuf::from("."));
290 Ok((manifest, ManifestLocation::File(path.to_path_buf()), parent))
291}
292
293fn write_manifest(
294 location: ManifestLocation,
295 manifest: &greentic_types::PackManifest,
296) -> Result<()> {
297 let encoded = greentic_types::encode_pack_manifest(manifest)?;
298 match location {
299 ManifestLocation::File(path) => {
300 fs::write(&path, encoded).with_context(|| format!("write {}", path.display()))?;
301 }
302 ManifestLocation::Gtpack(path) => {
303 let mut archive =
304 zip::ZipArchive::new(File::open(&path).context("open gtpack for write")?)
305 .context("read gtpack zip")?;
306 let mut entries = Vec::new();
307 for i in 0..archive.len() {
308 let mut file = archive.by_index(i).context("gtpack entry")?;
309 let mut buf = Vec::new();
310 file.read_to_end(&mut buf)
311 .with_context(|| format!("read {}", file.name()))?;
312 entries.push((file.name().to_string(), buf, file.compression()));
313 }
314 let temp_path = path.with_extension("gtpack.tmp");
315 {
316 let temp_file = File::create(&temp_path)
317 .with_context(|| format!("create {}", temp_path.display()))?;
318 let mut writer = zip::ZipWriter::new(temp_file);
319 let opts = zip::write::SimpleFileOptions::default();
320 for (name, data, method) in entries {
321 let mut entry_opts = opts;
322 entry_opts = entry_opts.compression_method(method);
323 if name == "manifest.cbor" {
324 writer
325 .start_file(name, entry_opts)
326 .context("start manifest entry")?;
327 writer.write_all(&encoded).context("write manifest.cbor")?;
328 } else {
329 writer
330 .start_file(name, entry_opts)
331 .with_context(|| "start entry")?;
332 writer.write_all(&data).with_context(|| "write entry")?;
333 }
334 }
335 writer.finish().context("finish gtpack rewrite")?;
336 }
337 fs::rename(&temp_path, &path).with_context(|| format!("replace {}", path.display()))?;
338 }
339 }
340 Ok(())
341}
342pub fn pack_events_list(args: &PackEventsListArgs) -> Result<()> {
343 let (temp, pack_path) = materialize_pack_path(&args.path, args.verbose)?;
344 let load = open_pack(&pack_path, SigningPolicy::DevOk).map_err(|err| anyhow!(err.message))?;
345 let providers: Vec<EventProviderSpec> = load
346 .manifest
347 .meta
348 .events
349 .as_ref()
350 .map(|events| events.providers.clone())
351 .unwrap_or_default();
352
353 match PackEventsFormat::from(args.format) {
354 PackEventsFormat::Table => print_table(&providers),
355 PackEventsFormat::Json => print_json(&providers)?,
356 PackEventsFormat::Yaml => print_yaml(&providers)?,
357 }
358
359 drop(temp);
360 Ok(())
361}
362
363fn plan_for_pack(
364 path: &Path,
365 tenant: &TenantCtx,
366 environment: &str,
367) -> Result<greentic_types::deployment::DeploymentPlan> {
368 let load = open_pack(path, SigningPolicy::DevOk).map_err(|err| anyhow!(err.message))?;
369 let connectors = load.manifest.meta.annotations.get("connectors");
370 let components = load_component_manifests(path, &load.manifest)?;
371 let secret_requirements = load_secret_requirements(path)?;
372
373 Ok(infer_base_deployment_plan(
374 &load.manifest.meta,
375 &load.manifest.flows,
376 connectors,
377 &components,
378 secret_requirements,
379 tenant,
380 environment,
381 ))
382}
383
384fn build_tenant_ctx(environment: &str, tenant: &str) -> Result<TenantCtx> {
385 let env_id = EnvId::from_str(environment)
386 .with_context(|| format!("invalid environment id `{}`", environment))?;
387 let tenant_id =
388 TenantId::from_str(tenant).with_context(|| format!("invalid tenant id `{}`", tenant))?;
389 Ok(TenantCtx::new(env_id, tenant_id))
390}
391
392fn load_component_manifests(
393 pack_path: &Path,
394 pack_manifest: &PackManifest,
395) -> Result<HashMap<String, ComponentManifest>> {
396 let file =
397 File::open(pack_path).with_context(|| format!("failed to open {}", pack_path.display()))?;
398 let mut archive = ZipArchive::new(file)
399 .with_context(|| format!("{} is not a valid gtpack archive", pack_path.display()))?;
400
401 let mut manifests = HashMap::new();
402 for component in &pack_manifest.components {
403 if let Some(manifest_path) = component.manifest_file.as_deref() {
404 let mut entry = archive
405 .by_name(manifest_path)
406 .with_context(|| format!("component manifest `{}` missing", manifest_path))?;
407 let manifest: ComponentManifest =
408 serde_json::from_reader(&mut entry).with_context(|| {
409 format!("failed to parse component manifest `{}`", manifest_path)
410 })?;
411 manifests.insert(component.name.clone(), manifest);
412 }
413 }
414
415 Ok(manifests)
416}
417
418fn load_secret_requirements(path: &Path) -> Result<Option<Vec<SecretRequirement>>> {
419 let file = File::open(path).with_context(|| format!("failed to open {}", path.display()))?;
420 let mut archive = ZipArchive::new(file)
421 .with_context(|| format!("{} is not a valid gtpack archive", path.display()))?;
422
423 for name in [
424 "assets/secret-requirements.json",
425 "secret-requirements.json",
426 ] {
427 if let Ok(mut entry) = archive.by_name(name) {
428 let mut buf = String::new();
429 entry
430 .read_to_string(&mut buf)
431 .context("failed to read secret requirements file")?;
432 let reqs: Vec<SecretRequirement> =
433 serde_json::from_str(&buf).context("secret requirements file is invalid JSON")?;
434 return Ok(Some(reqs));
435 }
436 }
437
438 Ok(None)
439}
440
441fn print_inspect_human(
442 manifest: &PackManifest,
443 report: &greentic_pack::reader::VerifyReport,
444 sbom: &[greentic_pack::builder::SbomEntry],
445) {
446 println!(
447 "Pack: {} ({})",
448 manifest.meta.pack_id, manifest.meta.version
449 );
450 println!("Flows: {}", manifest.flows.len());
451 println!("Components: {}", manifest.components.len());
452 println!("SBOM entries: {}", sbom.len());
453 println!("Signature OK: {}", report.signature_ok);
454 println!("SBOM OK: {}", report.sbom_ok);
455 if report.warnings.is_empty() {
456 println!("Warnings: none");
457 } else {
458 println!("Warnings:");
459 for warning in &report.warnings {
460 println!(" - {}", warning);
461 }
462 }
463}
464
465fn print_inspect_json(
466 manifest: &PackManifest,
467 report: &greentic_pack::reader::VerifyReport,
468 sbom: &[greentic_pack::builder::SbomEntry],
469) -> Result<()> {
470 let payload = json!({
471 "manifest": {
472 "pack_id": manifest.meta.pack_id,
473 "version": manifest.meta.version,
474 "flows": manifest.flows.len(),
475 "components": manifest.components.len(),
476 },
477 "report": {
478 "signature_ok": report.signature_ok,
479 "sbom_ok": report.sbom_ok,
480 "warnings": report.warnings,
481 },
482 "sbom": sbom,
483 });
484 println!("{}", serde_json::to_string_pretty(&payload)?);
485 Ok(())
486}
487
488fn print_table(providers: &[EventProviderSpec]) {
489 if providers.is_empty() {
490 println!("No events providers declared.");
491 return;
492 }
493
494 println!(
495 "{:<20} {:<8} {:<28} {:<12} TOPICS",
496 "NAME", "KIND", "COMPONENT", "TRANSPORT"
497 );
498 for provider in providers {
499 let transport = provider
500 .capabilities
501 .transport
502 .as_ref()
503 .map(|t| t.to_string())
504 .unwrap_or_else(|| "-".to_string());
505 let topics = summarize_topics(&provider.capabilities.topics);
506 println!(
507 "{:<20} {:<8} {:<28} {:<12} {}",
508 provider.name, provider.kind, provider.component, transport, topics
509 );
510 }
511}
512
513fn print_json(providers: &[EventProviderSpec]) -> Result<()> {
514 let payload = json!(providers);
515 println!("{}", serde_json::to_string_pretty(&payload)?);
516 Ok(())
517}
518
519fn print_yaml(providers: &[EventProviderSpec]) -> Result<()> {
520 let doc = serde_yaml_bw::to_string(providers)?;
521 println!("{doc}");
522 Ok(())
523}
524
525fn summarize_topics(topics: &[String]) -> String {
526 if topics.is_empty() {
527 return "-".to_string();
528 }
529 let combined = topics.join(", ");
530 if combined.len() > 60 {
531 format!("{}...", &combined[..57])
532 } else {
533 combined
534 }
535}