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