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