Skip to main content

canic_cli/restore/
mod.rs

1mod enforce;
2mod error;
3mod io;
4mod options;
5
6use crate::version_text;
7use canic_backup::restore::{
8    RestoreApplyCommandConfig, RestoreApplyDryRun, RestorePlan, RestorePlanner, RestoreRunResponse,
9    RestoreRunnerConfig,
10};
11use std::ffi::OsString;
12
13use enforce::{enforce_restore_plan_requirements, enforce_restore_run_requirements};
14use io::{
15    read_manifest_source, read_mapping, read_plan, verify_backup_layout_if_required,
16    write_apply_dry_run, write_apply_journal_if_requested, write_plan, write_restore_run,
17};
18
19pub use canic_backup::restore::parse_uploaded_snapshot_id;
20pub use error::RestoreCommandError;
21pub use options::{RestoreApplyOptions, RestorePlanOptions, RestoreRunOptions};
22
23/// Run a restore subcommand.
24pub fn run<I>(args: I) -> Result<(), RestoreCommandError>
25where
26    I: IntoIterator<Item = OsString>,
27{
28    let mut args = args.into_iter();
29    let Some(command) = args.next().and_then(|arg| arg.into_string().ok()) else {
30        return Err(RestoreCommandError::Usage(usage()));
31    };
32
33    match command.as_str() {
34        "plan" => {
35            let options = RestorePlanOptions::parse(args)?;
36            let plan = plan_restore(&options)?;
37            write_plan(&options, &plan)?;
38            enforce_restore_plan_requirements(&options, &plan)?;
39            Ok(())
40        }
41        "apply" => {
42            let options = RestoreApplyOptions::parse(args)?;
43            let dry_run = restore_apply_dry_run(&options)?;
44            write_apply_dry_run(&options, &dry_run)?;
45            write_apply_journal_if_requested(&options, &dry_run)?;
46            Ok(())
47        }
48        "run" => {
49            let options = RestoreRunOptions::parse(args)?;
50            let run = if options.execute {
51                restore_run_execute_result(&options)?
52            } else if options.unclaim_pending {
53                canic_backup::restore::RestoreRunnerOutcome {
54                    response: restore_run_unclaim_pending(&options)?,
55                    error: None,
56                }
57            } else {
58                canic_backup::restore::RestoreRunnerOutcome {
59                    response: restore_run_dry_run(&options)?,
60                    error: None,
61                }
62            };
63            write_restore_run(&options, &run.response)?;
64            if let Some(error) = run.error {
65                return Err(error.into());
66            }
67            enforce_restore_run_requirements(&options, &run.response)?;
68            Ok(())
69        }
70        "help" | "--help" | "-h" => {
71            println!("{}", usage());
72            Ok(())
73        }
74        "version" | "--version" | "-V" => {
75            println!("{}", version_text());
76            Ok(())
77        }
78        _ => Err(RestoreCommandError::UnknownOption(command)),
79    }
80}
81
82/// Build a no-mutation restore plan from a manifest and optional mapping.
83pub fn plan_restore(options: &RestorePlanOptions) -> Result<RestorePlan, RestoreCommandError> {
84    verify_backup_layout_if_required(options)?;
85
86    let manifest = read_manifest_source(options)?;
87    let mapping = options.mapping.as_ref().map(read_mapping).transpose()?;
88
89    RestorePlanner::plan(&manifest, mapping.as_ref()).map_err(RestoreCommandError::from)
90}
91
92/// Build a no-mutation restore apply dry-run from a restore plan.
93pub fn restore_apply_dry_run(
94    options: &RestoreApplyOptions,
95) -> Result<RestoreApplyDryRun, RestoreCommandError> {
96    let plan = read_plan(&options.plan)?;
97    if let Some(backup_dir) = &options.backup_dir {
98        return RestoreApplyDryRun::try_from_plan_with_artifacts(&plan, backup_dir)
99            .map_err(RestoreCommandError::from);
100    }
101
102    Ok(RestoreApplyDryRun::from_plan(&plan))
103}
104
105/// Build a no-mutation native restore runner preview from a journal file.
106pub fn restore_run_dry_run(
107    options: &RestoreRunOptions,
108) -> Result<RestoreRunResponse, RestoreCommandError> {
109    canic_backup::restore::restore_run_dry_run(&restore_runner_config(options))
110        .map_err(RestoreCommandError::from)
111}
112
113/// Recover an interrupted restore runner by unclaiming the pending operation.
114pub fn restore_run_unclaim_pending(
115    options: &RestoreRunOptions,
116) -> Result<RestoreRunResponse, RestoreCommandError> {
117    canic_backup::restore::restore_run_unclaim_pending(&restore_runner_config(options))
118        .map_err(RestoreCommandError::from)
119}
120
121// Execute ready restore apply operations and retain any deferred runner error.
122fn restore_run_execute_result(
123    options: &RestoreRunOptions,
124) -> Result<canic_backup::restore::RestoreRunnerOutcome, RestoreCommandError> {
125    canic_backup::restore::restore_run_execute_result(&restore_runner_config(options))
126        .map_err(RestoreCommandError::from)
127}
128
129// Build command-preview configuration from common dfx/network inputs.
130fn restore_command_config(program: &str, network: Option<&str>) -> RestoreApplyCommandConfig {
131    RestoreApplyCommandConfig {
132        program: program.to_string(),
133        network: network.map(str::to_string),
134    }
135}
136
137// Build the lower-level restore runner configuration from CLI flags.
138fn restore_runner_config(options: &RestoreRunOptions) -> RestoreRunnerConfig {
139    RestoreRunnerConfig {
140        journal: options.journal.clone(),
141        command: restore_command_config(&options.dfx, options.network.as_deref()),
142        max_steps: options.max_steps,
143        updated_at: None,
144    }
145}
146
147// Return restore command usage text.
148const fn usage() -> &'static str {
149    "usage: canic restore <command> [<args>]\n\ncommands:\n  plan   Build a no-mutation restore plan.\n  apply  Render restore operations and optionally write an apply journal.\n  run    Preview, execute, or recover the native restore runner."
150}
151
152#[cfg(test)]
153mod tests;