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