Skip to main content

canic_backup/restore/apply/journal/commands/
mod.rs

1use super::{RestoreApplyJournal, RestoreApplyJournalOperation, RestoreApplyOperationKind};
2use crate::persistence::resolve_backup_artifact_path;
3use serde::{Deserialize, Serialize};
4use std::path::Path;
5
6///
7/// RestoreApplyCommandPreview
8///
9
10#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
11#[expect(
12    clippy::struct_excessive_bools,
13    reason = "runner preview exposes machine-readable availability and safety flags"
14)]
15pub struct RestoreApplyCommandPreview {
16    pub response_version: u16,
17    pub backup_id: String,
18    pub ready: bool,
19    pub complete: bool,
20    pub operation_available: bool,
21    pub command_available: bool,
22    pub blocked_reasons: Vec<String>,
23    pub operation: Option<RestoreApplyJournalOperation>,
24    pub command: Option<RestoreApplyRunnerCommand>,
25}
26
27impl RestoreApplyCommandPreview {
28    /// Build a no-execute runner command preview from a restore apply journal.
29    #[must_use]
30    pub fn from_journal(journal: &RestoreApplyJournal) -> Self {
31        Self::from_journal_with_config(journal, &RestoreApplyCommandConfig::default())
32    }
33
34    /// Build a configured no-execute runner command preview from a journal.
35    #[must_use]
36    pub fn from_journal_with_config(
37        journal: &RestoreApplyJournal,
38        config: &RestoreApplyCommandConfig,
39    ) -> Self {
40        let operation = journal.next_transition_operation().cloned();
41        let command = operation.as_ref().and_then(|operation| {
42            RestoreApplyRunnerCommand::from_operation(operation, journal, config)
43        });
44
45        Self {
46            response_version: 1,
47            backup_id: journal.backup_id.clone(),
48            ready: journal.ready,
49            complete: journal.is_complete(),
50            operation_available: operation.is_some(),
51            command_available: command.is_some(),
52            blocked_reasons: journal.blocked_reasons.clone(),
53            operation,
54            command,
55        }
56    }
57}
58
59///
60/// RestoreApplyCommandConfig
61///
62
63#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
64pub struct RestoreApplyCommandConfig {
65    pub program: String,
66    pub network: Option<String>,
67}
68
69impl Default for RestoreApplyCommandConfig {
70    /// Build the default restore apply command preview configuration.
71    fn default() -> Self {
72        Self {
73            program: "icp".to_string(),
74            network: None,
75        }
76    }
77}
78
79///
80/// RestoreApplyRunnerCommand
81///
82
83#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
84pub struct RestoreApplyRunnerCommand {
85    pub program: String,
86    pub args: Vec<String>,
87    pub mutates: bool,
88    pub requires_stopped_canister: bool,
89    pub note: String,
90}
91
92impl RestoreApplyRunnerCommand {
93    // Build a no-execute ICP CLI command preview for one ready operation.
94    fn from_operation(
95        operation: &RestoreApplyJournalOperation,
96        journal: &RestoreApplyJournal,
97        config: &RestoreApplyCommandConfig,
98    ) -> Option<Self> {
99        match operation.operation {
100            RestoreApplyOperationKind::StopCanister => Some(Self {
101                program: config.program.clone(),
102                args: icp_canister_args(
103                    config,
104                    vec!["stop".to_string(), operation.target_canister.clone()],
105                ),
106                mutates: true,
107                requires_stopped_canister: false,
108                note: "stops the target canister before snapshot restore".to_string(),
109            }),
110            RestoreApplyOperationKind::StartCanister => Some(Self {
111                program: config.program.clone(),
112                args: icp_canister_args(
113                    config,
114                    vec!["start".to_string(), operation.target_canister.clone()],
115                ),
116                mutates: true,
117                requires_stopped_canister: false,
118                note: "starts the target canister after snapshot restore".to_string(),
119            }),
120            RestoreApplyOperationKind::UploadSnapshot => {
121                let artifact_path = upload_artifact_command_path(operation, journal)?;
122                Some(Self {
123                    program: config.program.clone(),
124                    args: icp_canister_args(
125                        config,
126                        vec![
127                            "snapshot".to_string(),
128                            "upload".to_string(),
129                            operation.target_canister.clone(),
130                            "--input".to_string(),
131                            artifact_path,
132                            "--resume".to_string(),
133                        ],
134                    ),
135                    mutates: true,
136                    requires_stopped_canister: false,
137                    note: "uploads the downloaded snapshot artifact to the target canister"
138                        .to_string(),
139                })
140            }
141            RestoreApplyOperationKind::LoadSnapshot => {
142                let snapshot_id = journal.uploaded_snapshot_id_for_load(operation)?;
143                Some(Self {
144                    program: config.program.clone(),
145                    args: icp_canister_args(
146                        config,
147                        vec![
148                            "snapshot".to_string(),
149                            "restore".to_string(),
150                            operation.target_canister.clone(),
151                            snapshot_id.to_string(),
152                        ],
153                    ),
154                    mutates: true,
155                    requires_stopped_canister: true,
156                    note: "loads the uploaded snapshot into the target canister".to_string(),
157                })
158            }
159            RestoreApplyOperationKind::VerifyMember | RestoreApplyOperationKind::VerifyFleet => {
160                match operation.verification_kind.as_deref() {
161                    Some("status") => Some(Self {
162                        program: config.program.clone(),
163                        args: icp_canister_args(
164                            config,
165                            vec!["status".to_string(), operation.target_canister.clone()],
166                        ),
167                        mutates: false,
168                        requires_stopped_canister: false,
169                        note: verification_command_note(
170                            &operation.operation,
171                            "checks target canister status",
172                            "checks target fleet root canister status",
173                        )
174                        .to_string(),
175                    }),
176                    Some(_) | None => None,
177                }
178            }
179        }
180    }
181}
182
183// Return an operator note for member-level or fleet-level verification commands.
184const fn verification_command_note(
185    operation: &RestoreApplyOperationKind,
186    member_note: &'static str,
187    fleet_note: &'static str,
188) -> &'static str {
189    match operation {
190        RestoreApplyOperationKind::VerifyFleet => fleet_note,
191        RestoreApplyOperationKind::StopCanister
192        | RestoreApplyOperationKind::StartCanister
193        | RestoreApplyOperationKind::UploadSnapshot
194        | RestoreApplyOperationKind::LoadSnapshot
195        | RestoreApplyOperationKind::VerifyMember => member_note,
196    }
197}
198
199// Build `icp canister` arguments with the optional network selector.
200fn icp_canister_args(config: &RestoreApplyCommandConfig, mut tail: Vec<String>) -> Vec<String> {
201    let mut args = vec!["canister".to_string()];
202    if let Some(network) = &config.network {
203        args.push("-n".to_string());
204        args.push(network.clone());
205    }
206    args.append(&mut tail);
207    args
208}
209
210// Resolve upload artifact paths the same way validation resolved them.
211fn upload_artifact_command_path(
212    operation: &RestoreApplyJournalOperation,
213    journal: &RestoreApplyJournal,
214) -> Option<String> {
215    let artifact_path = operation.artifact_path.as_ref()?;
216    let backup_root = journal.backup_root.as_ref()?;
217    resolve_backup_artifact_path(Path::new(backup_root), artifact_path)
218        .map(|path| path.to_string_lossy().to_string())
219}