canic_backup/restore/apply/journal/commands/
mod.rs1use crate::{
8 manifest::VERIFICATION_KIND_STATUS,
9 persistence::resolve_backup_artifact_path,
10 restore::{RestoreApplyJournal, RestoreApplyJournalOperation, RestoreApplyOperationKind},
11};
12
13use std::path::Path;
14
15use serde::{Deserialize, Serialize};
16
17#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
25#[serde(deny_unknown_fields)]
26#[expect(
27 clippy::struct_excessive_bools,
28 reason = "runner preview exposes machine-readable availability and safety flags"
29)]
30pub struct RestoreApplyCommandPreview {
31 pub response_version: u16,
32 pub backup_id: String,
33 pub ready: bool,
34 pub complete: bool,
35 pub operation_available: bool,
36 pub command_available: bool,
37 pub blocked_reasons: Vec<String>,
38 pub operation: Option<RestoreApplyJournalOperation>,
39 pub command: Option<RestoreApplyRunnerCommand>,
40}
41
42impl RestoreApplyCommandPreview {
43 #[must_use]
45 pub fn from_journal_with_config(
46 journal: &RestoreApplyJournal,
47 config: &RestoreApplyCommandConfig,
48 ) -> Self {
49 let operation = journal.next_transition_operation().cloned();
50 let command = operation.as_ref().and_then(|operation| {
51 RestoreApplyRunnerCommand::from_operation(operation, journal, config)
52 });
53
54 Self {
55 response_version: 1,
56 backup_id: journal.backup_id.clone(),
57 ready: journal.ready,
58 complete: journal.is_complete(),
59 operation_available: operation.is_some(),
60 command_available: command.is_some(),
61 blocked_reasons: journal.blocked_reasons.clone(),
62 operation,
63 command,
64 }
65 }
66}
67
68#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
76#[serde(deny_unknown_fields)]
77pub struct RestoreApplyCommandConfig {
78 pub program: String,
79 pub environment: Option<String>,
80}
81
82impl Default for RestoreApplyCommandConfig {
83 fn default() -> Self {
85 Self {
86 program: "icp".to_string(),
87 environment: None,
88 }
89 }
90}
91
92#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
100#[serde(deny_unknown_fields)]
101pub struct RestoreApplyRunnerCommand {
102 pub program: String,
103 pub args: Vec<String>,
104 pub mutates: bool,
105 pub requires_stopped_canister: bool,
106 pub note: String,
107}
108
109impl RestoreApplyRunnerCommand {
110 fn from_operation(
111 operation: &RestoreApplyJournalOperation,
112 journal: &RestoreApplyJournal,
113 config: &RestoreApplyCommandConfig,
114 ) -> Option<Self> {
115 Self::from_operation_with_artifact_path(operation, journal, config, None)
116 }
117
118 pub(in crate::restore) fn from_operation_with_artifact_path(
119 operation: &RestoreApplyJournalOperation,
120 journal: &RestoreApplyJournal,
121 config: &RestoreApplyCommandConfig,
122 staged_artifact_path: Option<&Path>,
123 ) -> Option<Self> {
124 match operation.operation {
125 RestoreApplyOperationKind::StopCanister => Some(Self {
126 program: config.program.clone(),
127 args: icp_canister_args(
128 config,
129 vec!["stop".to_string(), operation.target_canister.clone()],
130 ),
131 mutates: true,
132 requires_stopped_canister: false,
133 note: "stops the target canister before snapshot restore".to_string(),
134 }),
135 RestoreApplyOperationKind::StartCanister => Some(Self {
136 program: config.program.clone(),
137 args: icp_canister_args(
138 config,
139 vec!["start".to_string(), operation.target_canister.clone()],
140 ),
141 mutates: true,
142 requires_stopped_canister: false,
143 note: "starts the target canister after snapshot restore".to_string(),
144 }),
145 RestoreApplyOperationKind::UploadSnapshot => {
146 let artifact_path = staged_artifact_path.map_or_else(
147 || upload_artifact_command_path(operation, journal),
148 |path| Some(path.to_string_lossy().to_string()),
149 )?;
150 Some(Self {
151 program: config.program.clone(),
152 args: icp_canister_args(
153 config,
154 vec![
155 "snapshot".to_string(),
156 "upload".to_string(),
157 operation.target_canister.clone(),
158 "--input".to_string(),
159 artifact_path,
160 "--json".to_string(),
161 ],
162 ),
163 mutates: true,
164 requires_stopped_canister: false,
165 note: "uploads a private checksum-verified snapshot artifact copy to the target canister"
166 .to_string(),
167 })
168 }
169 RestoreApplyOperationKind::LoadSnapshot => {
170 let snapshot_id = journal.uploaded_snapshot_id_for_load(operation)?;
171 Some(Self {
172 program: config.program.clone(),
173 args: icp_canister_args(
174 config,
175 vec![
176 "snapshot".to_string(),
177 "restore".to_string(),
178 operation.target_canister.clone(),
179 snapshot_id.to_string(),
180 ],
181 ),
182 mutates: true,
183 requires_stopped_canister: true,
184 note: "loads the uploaded snapshot into the target canister".to_string(),
185 })
186 }
187 RestoreApplyOperationKind::VerifyMember
188 | RestoreApplyOperationKind::VerifyDeployment => {
189 match operation.verification_kind.as_deref() {
190 Some(VERIFICATION_KIND_STATUS) => Some(Self {
191 program: config.program.clone(),
192 args: icp_canister_args(
193 config,
194 vec![
195 VERIFICATION_KIND_STATUS.to_string(),
196 operation.target_canister.clone(),
197 "--json".to_string(),
198 ],
199 ),
200 mutates: false,
201 requires_stopped_canister: false,
202 note: verification_command_note(
203 &operation.operation,
204 "checks target canister status",
205 "checks target deployment root canister status",
206 )
207 .to_string(),
208 }),
209 Some(_) | None => None,
210 }
211 }
212 }
213 }
214}
215
216const fn verification_command_note(
217 operation: &RestoreApplyOperationKind,
218 member_note: &'static str,
219 deployment_note: &'static str,
220) -> &'static str {
221 match operation {
222 RestoreApplyOperationKind::VerifyDeployment => deployment_note,
223 RestoreApplyOperationKind::StopCanister
224 | RestoreApplyOperationKind::StartCanister
225 | RestoreApplyOperationKind::UploadSnapshot
226 | RestoreApplyOperationKind::LoadSnapshot
227 | RestoreApplyOperationKind::VerifyMember => member_note,
228 }
229}
230
231fn icp_canister_args(config: &RestoreApplyCommandConfig, mut tail: Vec<String>) -> Vec<String> {
232 let mut args = vec!["canister".to_string()];
233 args.append(&mut tail);
234 if let Some(environment) = &config.environment {
235 args.push("-e".to_string());
236 args.push(environment.clone());
237 }
238 args
239}
240
241fn upload_artifact_command_path(
242 operation: &RestoreApplyJournalOperation,
243 journal: &RestoreApplyJournal,
244) -> Option<String> {
245 let artifact_path = operation.artifact_path.as_ref()?;
246 let backup_root = journal.backup_root.as_ref()?;
247 resolve_backup_artifact_path(Path::new(backup_root), artifact_path)
248 .map(|path| path.to_string_lossy().to_string())
249}