1use crate::durable_io::write_bytes;
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 environment name {name:?}; use letters, numbers, '-' or '_'")]
24 InvalidEnvironmentName { name: String },
25
26 #[error(
27 "deployment state environment mismatch: state is for {state_environment}, requested {requested_environment}"
28 )]
29 EnvironmentMismatch {
30 state_environment: String,
31 requested_environment: 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 read deployment state {}: {source}", path.display())]
43 Read {
44 path: PathBuf,
45 #[source]
46 source: io::Error,
47 },
48
49 #[error("failed to decode deployment state {}: {source}", path.display())]
50 Decode {
51 path: PathBuf,
52 #[source]
53 source: serde_json::Error,
54 },
55
56 #[error("failed to encode deployment state {}: {source}", path.display())]
57 Encode {
58 path: PathBuf,
59 #[source]
60 source: serde_json::Error,
61 },
62
63 #[error("failed to write deployment state {}: {source}", path.display())]
64 Write {
65 path: PathBuf,
66 #[source]
67 source: io::Error,
68 },
69}
70
71#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
76#[serde(rename_all = "snake_case")]
77pub enum RootVerificationStatus {
78 Verified,
79 NotVerified,
80}
81
82#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
87#[serde(deny_unknown_fields)]
88pub struct InstallState {
89 pub schema_version: u32,
90 pub deployment_name: String,
91 pub fleet_template: String,
92 pub created_at_unix_secs: u64,
93 pub updated_at_unix_secs: u64,
94 pub environment: String,
95 pub root_target: String,
96 pub root_canister_id: String,
97 pub root_verification: RootVerificationStatus,
98 pub root_build_target: String,
99 pub workspace_root: String,
100 pub icp_root: String,
101 pub config_path: String,
102 pub release_set_manifest_path: String,
103}
104
105pub(super) fn read_deployment_install_state(
107 icp_root: &Path,
108 environment: &str,
109 deployment: &str,
110) -> Result<Option<InstallState>, InstallStateError> {
111 validate_environment_name(environment)?;
112 validate_state_name(deployment)?;
113 let path = deployment_install_state_path(icp_root, environment, deployment);
114 let bytes = match fs::read(&path) {
115 Ok(bytes) => bytes,
116 Err(source) if source.kind() == io::ErrorKind::NotFound => return Ok(None),
117 Err(source) => {
118 return Err(InstallStateError::Read { path, source });
119 }
120 };
121 decode_install_state(&bytes, &path, environment, deployment).map(Some)
122}
123
124pub fn decode_install_state(
126 bytes: &[u8],
127 path: &Path,
128 environment: &str,
129 deployment: &str,
130) -> Result<InstallState, InstallStateError> {
131 validate_environment_name(environment)?;
132 validate_state_name(deployment)?;
133 let state = serde_json::from_slice(bytes).map_err(|source| InstallStateError::Decode {
134 path: path.to_path_buf(),
135 source,
136 })?;
137 validate_loaded_install_state(&state, environment, deployment)?;
138 Ok(state)
139}
140
141pub fn read_named_deployment_install_state_from_root(
143 icp_root: &Path,
144 environment: &str,
145 deployment: &str,
146) -> Result<Option<InstallState>, InstallStateError> {
147 read_deployment_install_state(icp_root, environment, deployment)
148}
149
150#[must_use]
152pub(super) fn deployment_install_state_path(
153 icp_root: &Path,
154 environment: &str,
155 deployment: &str,
156) -> PathBuf {
157 deployments_dir(icp_root, environment).join(format!("{deployment}.json"))
158}
159
160fn deployments_dir(icp_root: &Path, environment: &str) -> PathBuf {
162 icp_root
163 .join(".canic")
164 .join(environment)
165 .join("deployments")
166}
167
168pub(super) fn write_install_state(
170 icp_root: &Path,
171 environment: &str,
172 state: &InstallState,
173) -> Result<PathBuf, InstallStateError> {
174 validate_environment_name(environment)?;
175 validate_state_name(&state.deployment_name)?;
176 validate_schema_version(state.schema_version)?;
177 if state.environment != environment {
178 return Err(InstallStateError::EnvironmentMismatch {
179 state_environment: state.environment.clone(),
180 requested_environment: environment.to_string(),
181 });
182 }
183 let path = deployment_install_state_path(icp_root, environment, &state.deployment_name);
184 let bytes = serde_json::to_vec_pretty(state).map_err(|source| InstallStateError::Encode {
185 path: path.clone(),
186 source,
187 })?;
188 write_bytes(&path, &bytes).map_err(|source| InstallStateError::Write {
189 path: path.clone(),
190 source,
191 })?;
192 Ok(path)
193}
194
195fn validate_loaded_install_state(
197 state: &InstallState,
198 requested_environment: &str,
199 requested_deployment: &str,
200) -> Result<(), InstallStateError> {
201 validate_schema_version(state.schema_version)?;
202 if state.deployment_name != requested_deployment {
203 return Err(InstallStateError::DeploymentMismatch {
204 state_deployment: state.deployment_name.clone(),
205 requested_deployment: requested_deployment.to_string(),
206 });
207 }
208 if state.environment != requested_environment {
209 return Err(InstallStateError::EnvironmentMismatch {
210 state_environment: state.environment.clone(),
211 requested_environment: requested_environment.to_string(),
212 });
213 }
214 Ok(())
215}
216
217const fn validate_schema_version(schema_version: u32) -> Result<(), InstallStateError> {
219 if schema_version == INSTALL_STATE_SCHEMA_VERSION {
220 Ok(())
221 } else {
222 Err(InstallStateError::SchemaVersionMismatch {
223 state_version: schema_version,
224 supported_version: INSTALL_STATE_SCHEMA_VERSION,
225 })
226 }
227}
228
229pub(super) fn validate_state_name(name: &str) -> Result<(), InstallStateError> {
231 let valid = !name.is_empty()
232 && name
233 .bytes()
234 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'));
235 if valid {
236 Ok(())
237 } else {
238 Err(InstallStateError::InvalidStateName {
239 name: name.to_string(),
240 })
241 }
242}
243
244pub fn validate_environment_name(name: &str) -> Result<(), InstallStateError> {
246 let valid = !name.is_empty()
247 && name
248 .bytes()
249 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'));
250 if valid {
251 Ok(())
252 } else {
253 Err(InstallStateError::InvalidEnvironmentName {
254 name: name.to_string(),
255 })
256 }
257}