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