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