Skip to main content

packc/cli/
plan.rs

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