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