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 #[arg(value_name = "PATH")]
19 pub input: std::path::PathBuf,
20
21 #[arg(long, default_value = "tenant-local")]
23 pub tenant: String,
24
25 #[arg(long, default_value = "local")]
27 pub environment: String,
28
29 #[arg(long)]
31 pub json: bool,
32
33 #[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}