Skip to main content

greentic_setup/cli_commands/
setup.rs

1//! Setup and update commands for bundle configuration.
2
3use 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
16/// Run the setup command.
17pub fn setup(args: BundleSetupArgs, i18n: &CliI18n) -> Result<()> {
18    setup_or_update(args, SetupMode::Create, i18n)
19}
20
21/// Run the update command.
22pub fn update(args: BundleSetupArgs, i18n: &CliI18n) -> Result<()> {
23    setup_or_update(args, SetupMode::Update, i18n)
24}
25
26/// Shared implementation for setup and update commands.
27fn 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}