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("invalid deployment/template name {name:?}; use letters, numbers, '-' or '_'")]
13 InvalidStateName { name: String },
14
15 #[error("invalid network name {name:?}; use letters, numbers, '-' or '_'")]
16 InvalidNetworkName { name: String },
17
18 #[error(
19 "deployment state network mismatch: state is for {state_network}, requested {requested_network}"
20 )]
21 NetworkMismatch {
22 state_network: String,
23 requested_network: String,
24 },
25
26 #[error("failed to resolve ICP root from {}: {source}", path.display())]
27 ResolveIcpRoot {
28 path: PathBuf,
29 #[source]
30 source: io::Error,
31 },
32
33 #[error("failed to read deployment state {}: {source}", path.display())]
34 Read {
35 path: PathBuf,
36 #[source]
37 source: io::Error,
38 },
39
40 #[error("failed to decode deployment state {}: {source}", path.display())]
41 Decode {
42 path: PathBuf,
43 #[source]
44 source: serde_json::Error,
45 },
46
47 #[error("failed to encode deployment state {}: {source}", path.display())]
48 Encode {
49 path: PathBuf,
50 #[source]
51 source: serde_json::Error,
52 },
53
54 #[error("failed to write deployment state {}: {source}", path.display())]
55 Write {
56 path: PathBuf,
57 #[source]
58 source: io::Error,
59 },
60}
61
62#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
67#[serde(rename_all = "snake_case")]
68pub enum RootVerificationStatus {
69 Verified,
70 NotVerified,
71}
72
73#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
78#[serde(deny_unknown_fields)]
79pub struct InstallState {
80 pub schema_version: u32,
81 pub deployment_name: String,
82 pub fleet_template: String,
83 pub created_at_unix_secs: u64,
84 pub updated_at_unix_secs: u64,
85 pub network: String,
86 pub root_target: String,
87 pub root_canister_id: String,
88 pub root_verification: RootVerificationStatus,
89 pub root_build_target: String,
90 pub workspace_root: String,
91 pub icp_root: String,
92 pub config_path: String,
93 pub release_set_manifest_path: String,
94}
95
96pub(super) fn read_deployment_install_state(
98 icp_root: &Path,
99 network: &str,
100 deployment: &str,
101) -> Result<Option<InstallState>, InstallStateError> {
102 validate_network_name(network)?;
103 validate_state_name(deployment)?;
104 let path = deployment_install_state_path(icp_root, network, deployment);
105 let bytes = match fs::read(&path) {
106 Ok(bytes) => bytes,
107 Err(source) if source.kind() == io::ErrorKind::NotFound => return Ok(None),
108 Err(source) => {
109 return Err(InstallStateError::Read { path, source });
110 }
111 };
112 let state = serde_json::from_slice(&bytes)
113 .map_err(|source| InstallStateError::Decode { path, source })?;
114 Ok(Some(state))
115}
116
117pub fn read_named_deployment_install_state(
119 network: &str,
120 deployment: &str,
121) -> Result<Option<InstallState>, InstallStateError> {
122 let start = PathBuf::from(".");
123 let icp_root = icp_root().map_err(|source| InstallStateError::ResolveIcpRoot {
124 path: start,
125 source,
126 })?;
127 read_deployment_install_state(&icp_root, network, deployment)
128}
129
130pub fn read_named_deployment_install_state_from_root(
132 icp_root: &Path,
133 network: &str,
134 deployment: &str,
135) -> Result<Option<InstallState>, InstallStateError> {
136 read_deployment_install_state(icp_root, network, deployment)
137}
138
139#[must_use]
141pub(super) fn deployment_install_state_path(
142 icp_root: &Path,
143 network: &str,
144 deployment: &str,
145) -> PathBuf {
146 deployments_dir(icp_root, network).join(format!("{deployment}.json"))
147}
148
149fn deployments_dir(icp_root: &Path, network: &str) -> PathBuf {
151 icp_root.join(".canic").join(network).join("deployments")
152}
153
154pub(super) fn write_install_state(
156 icp_root: &Path,
157 network: &str,
158 state: &InstallState,
159) -> Result<PathBuf, InstallStateError> {
160 validate_network_name(network)?;
161 validate_state_name(&state.deployment_name)?;
162 if state.network != network {
163 return Err(InstallStateError::NetworkMismatch {
164 state_network: state.network.clone(),
165 requested_network: network.to_string(),
166 });
167 }
168 let path = deployment_install_state_path(icp_root, network, &state.deployment_name);
169 let bytes = serde_json::to_vec_pretty(state).map_err(|source| InstallStateError::Encode {
170 path: path.clone(),
171 source,
172 })?;
173 write_bytes(&path, &bytes).map_err(|source| InstallStateError::Write {
174 path: path.clone(),
175 source,
176 })?;
177 Ok(path)
178}
179
180pub(super) fn validate_state_name(name: &str) -> Result<(), InstallStateError> {
182 let valid = !name.is_empty()
183 && name
184 .bytes()
185 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'));
186 if valid {
187 Ok(())
188 } else {
189 Err(InstallStateError::InvalidStateName {
190 name: name.to_string(),
191 })
192 }
193}
194
195pub(super) fn validate_network_name(name: &str) -> Result<(), InstallStateError> {
197 let valid = !name.is_empty()
198 && name
199 .bytes()
200 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'));
201 if valid {
202 Ok(())
203 } else {
204 Err(InstallStateError::InvalidNetworkName {
205 name: name.to_string(),
206 })
207 }
208}