greentic_setup/cli_commands/
setup.rs1use anyhow::{Context, Result, bail};
4
5use crate::cli_args::*;
6use crate::cli_helpers::{
7 complete_loaded_answers_with_prompts, ensure_deployment_targets_present, resolve_bundle_dir,
8 resolve_setup_scope, run_interactive_wizard,
9};
10use crate::cli_i18n::CliI18n;
11use crate::engine::{LoadedAnswers, SetupConfig, SetupRequest};
12use crate::plan::TenantSelection;
13use crate::platform_setup::StaticRoutesPolicy;
14use crate::{SetupEngine, SetupMode, bundle};
15
16pub fn setup(args: BundleSetupArgs, i18n: &CliI18n) -> Result<()> {
18 setup_or_update(args, SetupMode::Create, i18n)
19}
20
21pub fn update(args: BundleSetupArgs, i18n: &CliI18n) -> Result<()> {
23 setup_or_update(args, SetupMode::Update, i18n)
24}
25
26fn setup_or_update(args: BundleSetupArgs, mode: SetupMode, i18n: &CliI18n) -> Result<()> {
28 let bundle_dir = resolve_bundle_dir(args.bundle)?;
29 let BundleSetupArgs {
30 provider_id,
31 bundle: _,
32 tenant: cli_tenant,
33 team: cli_team,
34 env: cli_env,
35 domain,
36 dry_run,
37 emit_answers,
38 answers,
39 key,
40 non_interactive,
41 advanced,
42 parallel,
43 backup,
44 skip_secrets_init,
45 best_effort,
46 } = args;
47
48 bundle::validate_bundle_exists(&bundle_dir).context(i18n.t("cli.error.invalid_bundle"))?;
49
50 let provider_display = provider_id.clone().unwrap_or_else(|| "all".to_string());
51
52 let header_key = match mode {
53 SetupMode::Update => "cli.bundle.update.updating",
54 _ => "cli.bundle.setup.setting_up",
55 };
56 println!("{}", i18n.t(header_key));
57 println!(
58 "{}",
59 i18n.tf("cli.bundle.setup.provider", &[&provider_display])
60 );
61 println!(
62 "{}",
63 i18n.tf(
64 "cli.bundle.add.bundle",
65 &[&bundle_dir.display().to_string()]
66 )
67 );
68 let loader_engine = SetupEngine::new(SetupConfig {
69 tenant: cli_tenant.clone(),
70 team: cli_team.clone(),
71 env: cli_env.clone(),
72 offline: false,
73 verbose: true,
74 });
75
76 let loaded_answers = if let Some(answers_path) = &answers {
77 loader_engine
78 .load_answers(answers_path, key.as_deref(), !non_interactive)
79 .context(i18n.t("cli.error.failed_read_answers"))?
80 } else if emit_answers.is_some() {
81 LoadedAnswers::default()
82 } else if non_interactive {
83 bail!("{}", i18n.t("cli.error.answers_required"));
84 } else {
85 println!("\n{}", i18n.t("cli.simple.interactive_mode"));
86 println!();
87 run_interactive_wizard(
88 &bundle_dir,
89 &cli_tenant,
90 cli_team.as_deref(),
91 &cli_env,
92 advanced,
93 )?
94 };
95 let (tenant, team, env) = if answers.is_some() {
96 resolve_setup_scope(cli_tenant, cli_team, cli_env, &loaded_answers)
97 } else {
98 (cli_tenant, cli_team, cli_env)
99 };
100
101 println!("{}", i18n.tf("cli.bundle.add.tenant", &[&tenant]));
102 println!(
103 "{}",
104 i18n.tf(
105 "cli.bundle.add.team",
106 &[team.as_deref().unwrap_or("default")]
107 )
108 );
109 println!("{}", i18n.tf("cli.bundle.add.env", &[&env]));
110 println!("{}", i18n.tf("cli.bundle.setup.domain", &[&domain]));
111
112 let loaded_answers = if answers.is_some() && !non_interactive {
113 complete_loaded_answers_with_prompts(
114 &bundle_dir,
115 &tenant,
116 team.as_deref(),
117 &env,
118 advanced,
119 loaded_answers,
120 )?
121 } else {
122 loaded_answers
123 };
124 if non_interactive {
125 ensure_deployment_targets_present(&bundle_dir, &loaded_answers)?;
126 }
127
128 let providers = provider_id.clone().map_or_else(Vec::new, |id| vec![id]);
129
130 let request = SetupRequest {
131 bundle: bundle_dir.clone(),
132 providers,
133 tenants: vec![TenantSelection {
134 tenant: tenant.clone(),
135 team: team.clone(),
136 allow_paths: Vec::new(),
137 }],
138 static_routes: StaticRoutesPolicy::normalize(
139 loaded_answers.platform_setup.static_routes.as_ref(),
140 &env,
141 )
142 .context(i18n.t("cli.error.failed_read_answers"))?,
143 deployment_targets: loaded_answers.platform_setup.deployment_targets,
144 setup_answers: loaded_answers.setup_answers,
145 domain_filter: if domain == "all" {
146 None
147 } else {
148 Some(domain.clone())
149 },
150 parallel,
151 backup,
152 skip_secrets_init,
153 best_effort,
154 ..Default::default()
155 };
156
157 let engine = SetupEngine::new(SetupConfig {
158 tenant,
159 team,
160 env,
161 offline: false,
162 verbose: true,
163 });
164
165 let plan = engine
166 .plan(mode, &request, dry_run || emit_answers.is_some())
167 .context(i18n.t("cli.error.failed_build_plan"))?;
168
169 engine.print_plan(&plan);
170
171 if let Some(emit_path) = &emit_answers {
172 let emit_path_str = emit_path.display().to_string();
173 engine
174 .emit_answers(&plan, emit_path, key.as_deref(), !non_interactive)
175 .context(i18n.t("cli.error.failed_emit_answers"))?;
176 println!(
177 "\n{}",
178 i18n.tf("cli.bundle.setup.emit_written", &[&emit_path_str])
179 );
180 let usage_key = match mode {
181 SetupMode::Update => "cli.bundle.update.emit_usage",
182 _ => "cli.bundle.setup.emit_usage",
183 };
184 println!("{}", i18n.tf(usage_key, &[&emit_path_str]));
185 return Ok(());
186 }
187
188 if dry_run {
189 let dry_key = match mode {
190 SetupMode::Update => "cli.bundle.update.dry_run",
191 _ => "cli.bundle.setup.dry_run",
192 };
193 println!("\n{}", i18n.tf(dry_key, &[&provider_display]));
194 return Ok(());
195 }
196
197 engine
198 .execute(&plan)
199 .context(i18n.t("cli.error.failed_execute_plan"))?;
200
201 let done_key = match mode {
202 SetupMode::Update => "cli.bundle.update.complete",
203 _ => "cli.bundle.setup.complete",
204 };
205 println!("\n{}", i18n.tf(done_key, &[&provider_display]));
206
207 Ok(())
208}