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(journal: &RestoreApplyJournal) -> Self {
46 Self::from_journal_with_config(journal, &RestoreApplyCommandConfig::default())
47 }
48
49 #[must_use]
51 pub fn from_journal_with_config(
52 journal: &RestoreApplyJournal,
53 config: &RestoreApplyCommandConfig,
54 ) -> Self {
55 let operation = journal.next_transition_operation().cloned();
56 let command = operation.as_ref().and_then(|operation| {
57 RestoreApplyRunnerCommand::from_operation(operation, journal, config)
58 });
59
60 Self {
61 response_version: 1,
62 backup_id: journal.backup_id.clone(),
63 ready: journal.ready,
64 complete: journal.is_complete(),
65 operation_available: operation.is_some(),
66 command_available: command.is_some(),
67 blocked_reasons: journal.blocked_reasons.clone(),
68 operation,
69 command,
70 }
71 }
72}
73
74#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
82#[serde(deny_unknown_fields)]
83pub struct RestoreApplyCommandConfig {
84 pub program: String,
85 pub network: Option<String>,
86}
87
88impl Default for RestoreApplyCommandConfig {
89 fn default() -> Self {
91 Self {
92 program: "icp".to_string(),
93 network: None,
94 }
95 }
96}
97
98#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
106#[serde(deny_unknown_fields)]
107pub struct RestoreApplyRunnerCommand {
108 pub program: String,
109 pub args: Vec<String>,
110 pub mutates: bool,
111 pub requires_stopped_canister: bool,
112 pub note: String,
113}
114
115impl RestoreApplyRunnerCommand {
116 fn from_operation(
117 operation: &RestoreApplyJournalOperation,
118 journal: &RestoreApplyJournal,
119 config: &RestoreApplyCommandConfig,
120 ) -> Option<Self> {
121 match operation.operation {
122 RestoreApplyOperationKind::StopCanister => Some(Self {
123 program: config.program.clone(),
124 args: icp_canister_args(
125 config,
126 vec!["stop".to_string(), operation.target_canister.clone()],
127 ),
128 mutates: true,
129 requires_stopped_canister: false,
130 note: "stops the target canister before snapshot restore".to_string(),
131 }),
132 RestoreApplyOperationKind::StartCanister => Some(Self {
133 program: config.program.clone(),
134 args: icp_canister_args(
135 config,
136 vec!["start".to_string(), operation.target_canister.clone()],
137 ),
138 mutates: true,
139 requires_stopped_canister: false,
140 note: "starts the target canister after snapshot restore".to_string(),
141 }),
142 RestoreApplyOperationKind::UploadSnapshot => {
143 let artifact_path = upload_artifact_command_path(operation, journal)?;
144 Some(Self {
145 program: config.program.clone(),
146 args: icp_canister_args(
147 config,
148 vec![
149 "snapshot".to_string(),
150 "upload".to_string(),
151 operation.target_canister.clone(),
152 "--input".to_string(),
153 artifact_path,
154 "--json".to_string(),
155 ],
156 ),
157 mutates: true,
158 requires_stopped_canister: false,
159 note: "uploads the downloaded snapshot artifact to the target canister"
160 .to_string(),
161 })
162 }
163 RestoreApplyOperationKind::LoadSnapshot => {
164 let snapshot_id = journal.uploaded_snapshot_id_for_load(operation)?;
165 Some(Self {
166 program: config.program.clone(),
167 args: icp_canister_args(
168 config,
169 vec![
170 "snapshot".to_string(),
171 "restore".to_string(),
172 operation.target_canister.clone(),
173 snapshot_id.to_string(),
174 ],
175 ),
176 mutates: true,
177 requires_stopped_canister: true,
178 note: "loads the uploaded snapshot into the target canister".to_string(),
179 })
180 }
181 RestoreApplyOperationKind::VerifyMember
182 | RestoreApplyOperationKind::VerifyDeployment => {
183 match operation.verification_kind.as_deref() {
184 Some(VERIFICATION_KIND_STATUS) => Some(Self {
185 program: config.program.clone(),
186 args: icp_canister_args(
187 config,
188 vec![
189 VERIFICATION_KIND_STATUS.to_string(),
190 operation.target_canister.clone(),
191 "--json".to_string(),
192 ],
193 ),
194 mutates: false,
195 requires_stopped_canister: false,
196 note: verification_command_note(
197 &operation.operation,
198 "checks target canister status",
199 "checks target deployment root canister status",
200 )
201 .to_string(),
202 }),
203 Some(_) | None => None,
204 }
205 }
206 }
207 }
208}
209
210const fn verification_command_note(
211 operation: &RestoreApplyOperationKind,
212 member_note: &'static str,
213 deployment_note: &'static str,
214) -> &'static str {
215 match operation {
216 RestoreApplyOperationKind::VerifyDeployment => deployment_note,
217 RestoreApplyOperationKind::StopCanister
218 | RestoreApplyOperationKind::StartCanister
219 | RestoreApplyOperationKind::UploadSnapshot
220 | RestoreApplyOperationKind::LoadSnapshot
221 | RestoreApplyOperationKind::VerifyMember => member_note,
222 }
223}
224
225fn icp_canister_args(config: &RestoreApplyCommandConfig, mut tail: Vec<String>) -> Vec<String> {
226 let mut args = vec!["canister".to_string()];
227 args.append(&mut tail);
228 if let Some(network) = &config.network {
229 args.push("-n".to_string());
230 args.push(network.clone());
231 }
232 args
233}
234
235fn upload_artifact_command_path(
236 operation: &RestoreApplyJournalOperation,
237 journal: &RestoreApplyJournal,
238) -> Option<String> {
239 let artifact_path = operation.artifact_path.as_ref()?;
240 let backup_root = journal.backup_root.as_ref()?;
241 resolve_backup_artifact_path(Path::new(backup_root), artifact_path)
242 .map(|path| path.to_string_lossy().to_string())
243}