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::UploadSnapshot => {
101                let artifact_path = upload_artifact_command_path(operation, journal)?;
102                Some(Self {
103                    program: config.program.clone(),
104                    args: icp_canister_args(
105                        config,
106                        vec![
107                            "snapshot".to_string(),
108                            "upload".to_string(),
109                            operation.target_canister.clone(),
110                            "--input".to_string(),
111                            artifact_path,
112                            "--resume".to_string(),
113                        ],
114                    ),
115                    mutates: true,
116                    requires_stopped_canister: false,
117                    note: "uploads the downloaded snapshot artifact to the target canister"
118                        .to_string(),
119                })
120            }
121            RestoreApplyOperationKind::LoadSnapshot => {
122                let snapshot_id = journal.uploaded_snapshot_id_for_load(operation)?;
123                Some(Self {
124                    program: config.program.clone(),
125                    args: icp_canister_args(
126                        config,
127                        vec![
128                            "snapshot".to_string(),
129                            "restore".to_string(),
130                            operation.target_canister.clone(),
131                            snapshot_id.to_string(),
132                        ],
133                    ),
134                    mutates: true,
135                    requires_stopped_canister: true,
136                    note: "loads the uploaded snapshot into the target canister".to_string(),
137                })
138            }
139            RestoreApplyOperationKind::VerifyMember | RestoreApplyOperationKind::VerifyFleet => {
140                match operation.verification_kind.as_deref() {
141                    Some("status") => Some(Self {
142                        program: config.program.clone(),
143                        args: icp_canister_args(
144                            config,
145                            vec!["status".to_string(), operation.target_canister.clone()],
146                        ),
147                        mutates: false,
148                        requires_stopped_canister: false,
149                        note: verification_command_note(
150                            &operation.operation,
151                            "checks target canister status",
152                            "checks target fleet root canister status",
153                        )
154                        .to_string(),
155                    }),
156                    Some(_) | None => None,
157                }
158            }
159        }
160    }
161}
162
163// Return an operator note for member-level or fleet-level verification commands.
164const fn verification_command_note(
165    operation: &RestoreApplyOperationKind,
166    member_note: &'static str,
167    fleet_note: &'static str,
168) -> &'static str {
169    match operation {
170        RestoreApplyOperationKind::VerifyFleet => fleet_note,
171        RestoreApplyOperationKind::UploadSnapshot
172        | RestoreApplyOperationKind::LoadSnapshot
173        | RestoreApplyOperationKind::VerifyMember => member_note,
174    }
175}
176
177// Build `icp canister` arguments with the optional network selector.
178fn icp_canister_args(config: &RestoreApplyCommandConfig, mut tail: Vec<String>) -> Vec<String> {
179    let mut args = vec!["canister".to_string()];
180    if let Some(network) = &config.network {
181        args.push("-n".to_string());
182        args.push(network.clone());
183    }
184    args.append(&mut tail);
185    args
186}
187
188// Resolve upload artifact paths the same way validation resolved them.
189fn upload_artifact_command_path(
190    operation: &RestoreApplyJournalOperation,
191    journal: &RestoreApplyJournal,
192) -> Option<String> {
193    let artifact_path = operation.artifact_path.as_ref()?;
194    let backup_root = journal.backup_root.as_ref()?;
195    resolve_backup_artifact_path(Path::new(backup_root), artifact_path)
196        .map(|path| path.to_string_lossy().to_string())
197}