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