Skip to main content

packc/cli/
plan.rs

1#![forbid(unsafe_code)]
2
3use std::collections::HashMap;
4use std::fs::File;
5use std::io::Read;
6use std::path::Path;
7use std::str::FromStr;
8
9use anyhow::{Context, Result, anyhow};
10use greentic_pack::plan::infer_base_deployment_plan;
11use greentic_pack::reader::{PackLoad, SigningPolicy, open_pack};
12use greentic_types::component::ComponentManifest;
13use greentic_types::{EnvId, SecretRequirement, TenantCtx, TenantId};
14use zip::ZipArchive;
15
16use crate::cli::input::materialize_pack_path;
17
18#[derive(Debug, clap::Args)]
19pub struct PlanArgs {
20    /// Path to a .gtpack archive or pack source directory.
21    #[arg(value_name = "PATH")]
22    pub input: std::path::PathBuf,
23
24    /// Tenant identifier to embed in the plan.
25    #[arg(long, default_value = "tenant-local")]
26    pub tenant: String,
27
28    /// Environment identifier to embed in the plan.
29    #[arg(long, default_value = "local")]
30    pub environment: String,
31
32    /// Emit compact JSON output instead of pretty-printing.
33    #[arg(long)]
34    pub json: bool,
35
36    /// When set, print additional diagnostics (for directory builds).
37    #[arg(long)]
38    pub verbose: bool,
39}
40
41pub fn handle(args: &PlanArgs) -> Result<()> {
42    let (temp, pack_path) = materialize_pack_path(&args.input, args.verbose)?;
43    let tenant_ctx = build_tenant_ctx(&args.environment, &args.tenant)?;
44    let plan = plan_for_pack(&pack_path, &tenant_ctx, &args.environment)?;
45
46    if args.json {
47        println!("{}", serde_json::to_string(&plan)?);
48    } else {
49        println!("{}", serde_json::to_string_pretty(&plan)?);
50    }
51
52    drop(temp);
53    Ok(())
54}
55
56fn plan_for_pack(
57    path: &Path,
58    tenant: &TenantCtx,
59    environment: &str,
60) -> Result<greentic_types::deployment::DeploymentPlan> {
61    let load = open_pack(path, SigningPolicy::DevOk).map_err(|err| anyhow!(err.message))?;
62    let connectors = load.manifest.meta.annotations.get("connectors");
63    let components = load_component_manifests(&load)?;
64    let secret_requirements = load_secret_requirements(path).unwrap_or(None);
65
66    Ok(infer_base_deployment_plan(
67        &load.manifest.meta,
68        &load.manifest.flows,
69        connectors,
70        &components,
71        secret_requirements,
72        tenant,
73        environment,
74    ))
75}
76
77fn build_tenant_ctx(environment: &str, tenant: &str) -> Result<TenantCtx> {
78    let env_id = EnvId::from_str(environment)
79        .with_context(|| format!("invalid environment id `{}`", environment))?;
80    let tenant_id =
81        TenantId::from_str(tenant).with_context(|| format!("invalid tenant id `{}`", tenant))?;
82    Ok(TenantCtx::new(env_id, tenant_id))
83}
84
85fn load_component_manifests(load: &PackLoad) -> Result<HashMap<String, ComponentManifest>> {
86    let mut manifests = HashMap::new();
87    for component in &load.manifest.components {
88        let id = &component.name;
89        if let Some(manifest) = load
90            .get_component_manifest_prefer_file(id)
91            .with_context(|| format!("failed to load manifest for component `{id}`"))?
92        {
93            manifests.insert(component.name.clone(), manifest);
94        }
95    }
96    Ok(manifests)
97}
98
99fn load_secret_requirements(path: &Path) -> Result<Option<Vec<SecretRequirement>>> {
100    let file = File::open(path).with_context(|| format!("failed to open {}", path.display()))?;
101    let mut archive = ZipArchive::new(file)
102        .with_context(|| format!("{} is not a valid gtpack archive", path.display()))?;
103
104    for name in [
105        "assets/secret-requirements.json",
106        "secret-requirements.json",
107    ] {
108        if let Ok(mut entry) = archive.by_name(name) {
109            let mut buf = String::new();
110            entry
111                .read_to_string(&mut buf)
112                .context("failed to read secret requirements file")?;
113            let reqs: Vec<SecretRequirement> =
114                serde_json::from_str(&buf).context("secret requirements file is invalid JSON")?;
115            return Ok(Some(reqs));
116        }
117    }
118
119    Ok(None)
120}