canic_host/install_root/
state.rs1use crate::{durable_io::write_bytes, release_set::icp_root};
2use std::{fs, path::Path, path::PathBuf};
3
4use serde::{Deserialize, Serialize};
5
6pub(super) const INSTALL_STATE_SCHEMA_VERSION: u32 = 2;
7
8#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
13#[serde(rename_all = "snake_case")]
14pub enum RootVerificationStatus {
15 Verified,
16 NotVerified,
17}
18
19#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
24#[serde(deny_unknown_fields)]
25pub struct InstallState {
26 pub schema_version: u32,
27 pub deployment_name: String,
28 pub fleet_template: String,
29 pub created_at_unix_secs: u64,
30 pub updated_at_unix_secs: u64,
31 pub network: String,
32 pub root_target: String,
33 pub root_canister_id: String,
34 pub root_verification: RootVerificationStatus,
35 pub root_build_target: String,
36 pub workspace_root: String,
37 pub icp_root: String,
38 pub config_path: String,
39 pub release_set_manifest_path: String,
40}
41
42pub(super) fn read_deployment_install_state(
44 icp_root: &Path,
45 network: &str,
46 deployment: &str,
47) -> Result<Option<InstallState>, Box<dyn std::error::Error>> {
48 validate_network_name(network)?;
49 validate_state_name(deployment)?;
50 let path = deployment_install_state_path(icp_root, network, deployment);
51 if !path.is_file() {
52 return Ok(None);
53 }
54
55 let bytes = fs::read(&path)?;
56 let state: InstallState = serde_json::from_slice(&bytes)?;
57 Ok(Some(state))
58}
59
60pub fn read_named_deployment_install_state(
62 network: &str,
63 deployment: &str,
64) -> Result<Option<InstallState>, Box<dyn std::error::Error>> {
65 let icp_root = icp_root()?;
66 read_deployment_install_state(&icp_root, network, deployment)
67}
68
69pub fn read_named_deployment_install_state_from_root(
71 icp_root: &Path,
72 network: &str,
73 deployment: &str,
74) -> Result<Option<InstallState>, Box<dyn std::error::Error>> {
75 read_deployment_install_state(icp_root, network, deployment)
76}
77
78#[must_use]
80pub(super) fn deployment_install_state_path(
81 icp_root: &Path,
82 network: &str,
83 deployment: &str,
84) -> PathBuf {
85 deployments_dir(icp_root, network).join(format!("{deployment}.json"))
86}
87
88fn deployments_dir(icp_root: &Path, network: &str) -> PathBuf {
90 icp_root.join(".canic").join(network).join("deployments")
91}
92
93pub(super) fn write_install_state(
95 icp_root: &Path,
96 network: &str,
97 state: &InstallState,
98) -> Result<PathBuf, Box<dyn std::error::Error>> {
99 validate_network_name(network)?;
100 validate_state_name(&state.deployment_name)?;
101 if state.network != network {
102 return Err(format!(
103 "deployment state network mismatch: state is for {}, requested {network}",
104 state.network
105 )
106 .into());
107 }
108 let path = deployment_install_state_path(icp_root, network, &state.deployment_name);
109 write_bytes(&path, &serde_json::to_vec_pretty(state)?)?;
110 Ok(path)
111}
112
113pub(super) fn validate_state_name(name: &str) -> Result<(), Box<dyn std::error::Error>> {
115 let valid = !name.is_empty()
116 && name
117 .bytes()
118 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'));
119 if valid {
120 Ok(())
121 } else {
122 Err(
123 format!("invalid deployment/template name {name:?}; use letters, numbers, '-' or '_'")
124 .into(),
125 )
126 }
127}
128
129pub(super) fn validate_network_name(name: &str) -> Result<(), Box<dyn std::error::Error>> {
131 let valid = !name.is_empty()
132 && name
133 .bytes()
134 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'));
135 if valid {
136 Ok(())
137 } else {
138 Err(format!("invalid network name {name:?}; use letters, numbers, '-' or '_'").into())
139 }
140}