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::UploadSnapshot => {
101 let artifact_path = upload_artifact_command_path(operation, journal)?;
102 Some(Self {
103 program: config.program.clone(),
104 args: icp_canister_args(
105 config,
106 vec![
107 "snapshot".to_string(),
108 "upload".to_string(),
109 operation.target_canister.clone(),
110 "--input".to_string(),
111 artifact_path,
112 "--resume".to_string(),
113 ],
114 ),
115 mutates: true,
116 requires_stopped_canister: false,
117 note: "uploads the downloaded snapshot artifact to the target canister"
118 .to_string(),
119 })
120 }
121 RestoreApplyOperationKind::LoadSnapshot => {
122 let snapshot_id = journal.uploaded_snapshot_id_for_load(operation)?;
123 Some(Self {
124 program: config.program.clone(),
125 args: icp_canister_args(
126 config,
127 vec![
128 "snapshot".to_string(),
129 "restore".to_string(),
130 operation.target_canister.clone(),
131 snapshot_id.to_string(),
132 ],
133 ),
134 mutates: true,
135 requires_stopped_canister: true,
136 note: "loads the uploaded snapshot into the target canister".to_string(),
137 })
138 }
139 RestoreApplyOperationKind::VerifyMember | RestoreApplyOperationKind::VerifyFleet => {
140 match operation.verification_kind.as_deref() {
141 Some("status") => Some(Self {
142 program: config.program.clone(),
143 args: icp_canister_args(
144 config,
145 vec!["status".to_string(), operation.target_canister.clone()],
146 ),
147 mutates: false,
148 requires_stopped_canister: false,
149 note: verification_command_note(
150 &operation.operation,
151 "checks target canister status",
152 "checks target fleet root canister status",
153 )
154 .to_string(),
155 }),
156 Some(_) | None => None,
157 }
158 }
159 }
160 }
161}
162
163const fn verification_command_note(
165 operation: &RestoreApplyOperationKind,
166 member_note: &'static str,
167 fleet_note: &'static str,
168) -> &'static str {
169 match operation {
170 RestoreApplyOperationKind::VerifyFleet => fleet_note,
171 RestoreApplyOperationKind::UploadSnapshot
172 | RestoreApplyOperationKind::LoadSnapshot
173 | RestoreApplyOperationKind::VerifyMember => member_note,
174 }
175}
176
177fn icp_canister_args(config: &RestoreApplyCommandConfig, mut tail: Vec<String>) -> Vec<String> {
179 let mut args = vec!["canister".to_string()];
180 if let Some(network) = &config.network {
181 args.push("-n".to_string());
182 args.push(network.clone());
183 }
184 args.append(&mut tail);
185 args
186}
187
188fn upload_artifact_command_path(
190 operation: &RestoreApplyJournalOperation,
191 journal: &RestoreApplyJournal,
192) -> Option<String> {
193 let artifact_path = operation.artifact_path.as_ref()?;
194 let backup_root = journal.backup_root.as_ref()?;
195 resolve_backup_artifact_path(Path::new(backup_root), artifact_path)
196 .map(|path| path.to_string_lossy().to_string())
197}