1use crate::{durable_io::write_bytes, release_set::icp_root};
2use std::{fs, io, path::Path, path::PathBuf};
3
4use serde::{Deserialize, Serialize};
5use thiserror::Error as ThisError;
6
7pub(super) const INSTALL_STATE_SCHEMA_VERSION: u32 = 2;
8
9#[derive(Debug, ThisError)]
11pub enum InstallStateError {
12 #[error(
13 "deployment state identity mismatch: state is for {state_deployment}, requested {requested_deployment}"
14 )]
15 DeploymentMismatch {
16 state_deployment: String,
17 requested_deployment: String,
18 },
19
20 #[error("invalid deployment/template name {name:?}; use letters, numbers, '-' or '_'")]
21 InvalidStateName { name: String },
22
23 #[error("invalid network name {name:?}; use letters, numbers, '-' or '_'")]
24 InvalidNetworkName { name: String },
25
26 #[error(
27 "deployment state network mismatch: state is for {state_network}, requested {requested_network}"
28 )]
29 NetworkMismatch {
30 state_network: String,
31 requested_network: String,
32 },
33
34 #[error(
35 "unsupported deployment state schema version {state_version}; supported version is {supported_version}"
36 )]
37 SchemaVersionMismatch {
38 state_version: u32,
39 supported_version: u32,
40 },
41
42 #[error("failed to resolve ICP root from {}: {source}", path.display())]
43 ResolveIcpRoot {
44 path: PathBuf,
45 #[source]
46 source: io::Error,
47 },
48
49 #[error("failed to read deployment state {}: {source}", path.display())]
50 Read {
51 path: PathBuf,
52 #[source]
53 source: io::Error,
54 },
55
56 #[error("failed to decode deployment state {}: {source}", path.display())]
57 Decode {
58 path: PathBuf,
59 #[source]
60 source: serde_json::Error,
61 },
62
63 #[error("failed to encode deployment state {}: {source}", path.display())]
64 Encode {
65 path: PathBuf,
66 #[source]
67 source: serde_json::Error,
68 },
69
70 #[error("failed to write deployment state {}: {source}", path.display())]
71 Write {
72 path: PathBuf,
73 #[source]
74 source: io::Error,
75 },
76}
77
78#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
83#[serde(rename_all = "snake_case")]
84pub enum RootVerificationStatus {
85 Verified,
86 NotVerified,
87}
88
89#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
94#[serde(deny_unknown_fields)]
95pub struct InstallState {
96 pub schema_version: u32,
97 pub deployment_name: String,
98 pub fleet_template: String,
99 pub created_at_unix_secs: u64,
100 pub updated_at_unix_secs: u64,
101 pub network: String,
102 pub root_target: String,
103 pub root_canister_id: String,
104 pub root_verification: RootVerificationStatus,
105 pub root_build_target: String,
106 pub workspace_root: String,
107 pub icp_root: String,
108 pub config_path: String,
109 pub release_set_manifest_path: String,
110}
111
112pub(super) fn read_deployment_install_state(
114 icp_root: &Path,
115 network: &str,
116 deployment: &str,
117) -> Result<Option<InstallState>, InstallStateError> {
118 validate_network_name(network)?;
119 validate_state_name(deployment)?;
120 let path = deployment_install_state_path(icp_root, network, deployment);
121 let bytes = match fs::read(&path) {
122 Ok(bytes) => bytes,
123 Err(source) if source.kind() == io::ErrorKind::NotFound => return Ok(None),
124 Err(source) => {
125 return Err(InstallStateError::Read { path, source });
126 }
127 };
128 decode_install_state(&bytes, &path, network, deployment).map(Some)
129}
130
131pub fn decode_install_state(
133 bytes: &[u8],
134 path: &Path,
135 network: &str,
136 deployment: &str,
137) -> Result<InstallState, InstallStateError> {
138 validate_network_name(network)?;
139 validate_state_name(deployment)?;
140 let state = serde_json::from_slice(bytes).map_err(|source| InstallStateError::Decode {
141 path: path.to_path_buf(),
142 source,
143 })?;
144 validate_loaded_install_state(&state, network, deployment)?;
145 Ok(state)
146}
147
148pub fn read_named_deployment_install_state(
150 network: &str,
151 deployment: &str,
152) -> Result<Option<InstallState>, InstallStateError> {
153 let start = PathBuf::from(".");
154 let icp_root = icp_root().map_err(|source| InstallStateError::ResolveIcpRoot {
155 path: start,
156 source,
157 })?;
158 read_deployment_install_state(&icp_root, network, deployment)
159}
160
161pub fn read_named_deployment_install_state_from_root(
163 icp_root: &Path,
164 network: &str,
165 deployment: &str,
166) -> Result<Option<InstallState>, InstallStateError> {
167 read_deployment_install_state(icp_root, network, deployment)
168}
169
170#[must_use]
172pub(super) fn deployment_install_state_path(
173 icp_root: &Path,
174 network: &str,
175 deployment: &str,
176) -> PathBuf {
177 deployments_dir(icp_root, network).join(format!("{deployment}.json"))
178}
179
180fn deployments_dir(icp_root: &Path, network: &str) -> PathBuf {
182 icp_root.join(".canic").join(network).join("deployments")
183}
184
185pub(super) fn write_install_state(
187 icp_root: &Path,
188 network: &str,
189 state: &InstallState,
190) -> Result<PathBuf, InstallStateError> {
191 validate_network_name(network)?;
192 validate_state_name(&state.deployment_name)?;
193 validate_schema_version(state.schema_version)?;
194 if state.network != network {
195 return Err(InstallStateError::NetworkMismatch {
196 state_network: state.network.clone(),
197 requested_network: network.to_string(),
198 });
199 }
200 let path = deployment_install_state_path(icp_root, network, &state.deployment_name);
201 let bytes = serde_json::to_vec_pretty(state).map_err(|source| InstallStateError::Encode {
202 path: path.clone(),
203 source,
204 })?;
205 write_bytes(&path, &bytes).map_err(|source| InstallStateError::Write {
206 path: path.clone(),
207 source,
208 })?;
209 Ok(path)
210}
211
212fn validate_loaded_install_state(
214 state: &InstallState,
215 requested_network: &str,
216 requested_deployment: &str,
217) -> Result<(), InstallStateError> {
218 validate_schema_version(state.schema_version)?;
219 if state.deployment_name != requested_deployment {
220 return Err(InstallStateError::DeploymentMismatch {
221 state_deployment: state.deployment_name.clone(),
222 requested_deployment: requested_deployment.to_string(),
223 });
224 }
225 if state.network != requested_network {
226 return Err(InstallStateError::NetworkMismatch {
227 state_network: state.network.clone(),
228 requested_network: requested_network.to_string(),
229 });
230 }
231 Ok(())
232}
233
234const fn validate_schema_version(schema_version: u32) -> Result<(), InstallStateError> {
236 if schema_version == INSTALL_STATE_SCHEMA_VERSION {
237 Ok(())
238 } else {
239 Err(InstallStateError::SchemaVersionMismatch {
240 state_version: schema_version,
241 supported_version: INSTALL_STATE_SCHEMA_VERSION,
242 })
243 }
244}
245
246pub(super) fn validate_state_name(name: &str) -> Result<(), InstallStateError> {
248 let valid = !name.is_empty()
249 && name
250 .bytes()
251 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'));
252 if valid {
253 Ok(())
254 } else {
255 Err(InstallStateError::InvalidStateName {
256 name: name.to_string(),
257 })
258 }
259}
260
261pub fn validate_network_name(name: &str) -> Result<(), InstallStateError> {
263 let valid = !name.is_empty()
264 && name
265 .bytes()
266 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'));
267 if valid {
268 Ok(())
269 } else {
270 Err(InstallStateError::InvalidNetworkName {
271 name: name.to_string(),
272 })
273 }
274}