canic_backup/restore/apply/journal/commands/
mod.rs1use super::{RestoreApplyJournal, RestoreApplyJournalOperation, RestoreApplyOperationKind};
2use crate::persistence::resolve_backup_artifact_path;
3use serde::{Deserialize, Serialize};
4use std::path::Path;
5
6#[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 #[must_use]
30 pub fn from_journal(journal: &RestoreApplyJournal) -> Self {
31 Self::from_journal_with_config(journal, &RestoreApplyCommandConfig::default())
32 }
33
34 #[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#[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 fn default() -> Self {
72 Self {
73 program: "icp".to_string(),
74 network: None,
75 }
76 }
77}
78
79#[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 fn from_operation(
95 operation: &RestoreApplyJournalOperation,
96 journal: &RestoreApplyJournal,
97 config: &RestoreApplyCommandConfig,
98 ) -> Option<Self> {
99 match operation.operation {
100 RestoreApplyOperationKind::StopCanister => Some(Self {
101 program: config.program.clone(),
102 args: icp_canister_args(
103 config,
104 vec!["stop".to_string(), operation.target_canister.clone()],
105 ),
106 mutates: true,
107 requires_stopped_canister: false,
108 note: "stops the target canister before snapshot restore".to_string(),
109 }),
110 RestoreApplyOperationKind::StartCanister => Some(Self {
111 program: config.program.clone(),
112 args: icp_canister_args(
113 config,
114 vec!["start".to_string(), operation.target_canister.clone()],
115 ),
116 mutates: true,
117 requires_stopped_canister: false,
118 note: "starts the target canister after snapshot restore".to_string(),
119 }),
120 RestoreApplyOperationKind::UploadSnapshot => {
121 let artifact_path = upload_artifact_command_path(operation, journal)?;
122 Some(Self {
123 program: config.program.clone(),
124 args: icp_canister_args(
125 config,
126 vec![
127 "snapshot".to_string(),
128 "upload".to_string(),
129 operation.target_canister.clone(),
130 "--input".to_string(),
131 artifact_path,
132 "--json".to_string(),
133 ],
134 ),
135 mutates: true,
136 requires_stopped_canister: false,
137 note: "uploads the downloaded snapshot artifact to the target canister"
138 .to_string(),
139 })
140 }
141 RestoreApplyOperationKind::LoadSnapshot => {
142 let snapshot_id = journal.uploaded_snapshot_id_for_load(operation)?;
143 Some(Self {
144 program: config.program.clone(),
145 args: icp_canister_args(
146 config,
147 vec![
148 "snapshot".to_string(),
149 "restore".to_string(),
150 operation.target_canister.clone(),
151 snapshot_id.to_string(),
152 ],
153 ),
154 mutates: true,
155 requires_stopped_canister: true,
156 note: "loads the uploaded snapshot into the target canister".to_string(),
157 })
158 }
159 RestoreApplyOperationKind::VerifyMember | RestoreApplyOperationKind::VerifyFleet => {
160 match operation.verification_kind.as_deref() {
161 Some("status") => Some(Self {
162 program: config.program.clone(),
163 args: icp_canister_args(
164 config,
165 vec![
166 "status".to_string(),
167 operation.target_canister.clone(),
168 "--json".to_string(),
169 ],
170 ),
171 mutates: false,
172 requires_stopped_canister: false,
173 note: verification_command_note(
174 &operation.operation,
175 "checks target canister status",
176 "checks target fleet root canister status",
177 )
178 .to_string(),
179 }),
180 Some(_) | None => None,
181 }
182 }
183 }
184 }
185}
186
187const fn verification_command_note(
189 operation: &RestoreApplyOperationKind,
190 member_note: &'static str,
191 fleet_note: &'static str,
192) -> &'static str {
193 match operation {
194 RestoreApplyOperationKind::VerifyFleet => fleet_note,
195 RestoreApplyOperationKind::StopCanister
196 | RestoreApplyOperationKind::StartCanister
197 | RestoreApplyOperationKind::UploadSnapshot
198 | RestoreApplyOperationKind::LoadSnapshot
199 | RestoreApplyOperationKind::VerifyMember => member_note,
200 }
201}
202
203fn icp_canister_args(config: &RestoreApplyCommandConfig, mut tail: Vec<String>) -> Vec<String> {
205 let mut args = vec!["canister".to_string()];
206 args.append(&mut tail);
207 if let Some(network) = &config.network {
208 args.push("-n".to_string());
209 args.push(network.clone());
210 }
211 args
212}
213
214fn upload_artifact_command_path(
216 operation: &RestoreApplyJournalOperation,
217 journal: &RestoreApplyJournal,
218) -> Option<String> {
219 let artifact_path = operation.artifact_path.as_ref()?;
220 let backup_root = journal.backup_root.as_ref()?;
221 resolve_backup_artifact_path(Path::new(backup_root), artifact_path)
222 .map(|path| path.to_string_lossy().to_string())
223}