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)]
23pub struct InstallState {
24 pub schema_version: u32,
25 pub deployment_name: String,
26 pub fleet_template: String,
27 pub fleet: String,
28 pub installed_at_unix_secs: u64,
29 pub network: String,
30 pub root_target: String,
31 pub root_canister_id: String,
32 pub root_verification: RootVerificationStatus,
33 pub root_build_target: String,
34 pub workspace_root: String,
35 pub icp_root: String,
36 pub config_path: String,
37 pub release_set_manifest_path: String,
38}
39
40pub(super) fn read_fleet_install_state(
42 icp_root: &Path,
43 network: &str,
44 deployment: &str,
45) -> Result<Option<InstallState>, Box<dyn std::error::Error>> {
46 validate_network_name(network)?;
47 validate_fleet_name(deployment)?;
48 let path = deployment_install_state_path(icp_root, network, deployment);
49 if !path.is_file() {
50 reject_legacy_fleet_state(icp_root, network, deployment)?;
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_fleet_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_fleet_install_state(&icp_root, network, deployment)
66}
67
68pub fn read_named_fleet_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_fleet_install_state(icp_root, network, deployment)
75}
76
77#[must_use]
79pub(super) fn fleet_install_state_path(icp_root: &Path, network: &str, fleet: &str) -> PathBuf {
80 fleets_dir(icp_root, network).join(format!("{fleet}.json"))
81}
82
83#[must_use]
85pub(super) fn deployment_install_state_path(
86 icp_root: &Path,
87 network: &str,
88 deployment: &str,
89) -> PathBuf {
90 deployments_dir(icp_root, network).join(format!("{deployment}.json"))
91}
92
93fn fleets_dir(icp_root: &Path, network: &str) -> PathBuf {
95 icp_root.join(".canic").join(network).join("fleets")
96}
97
98fn deployments_dir(icp_root: &Path, network: &str) -> PathBuf {
100 icp_root.join(".canic").join(network).join("deployments")
101}
102
103pub(super) fn write_install_state(
105 icp_root: &Path,
106 network: &str,
107 state: &InstallState,
108) -> Result<PathBuf, Box<dyn std::error::Error>> {
109 validate_network_name(network)?;
110 validate_fleet_name(&state.deployment_name)?;
111 if state.network != network {
112 return Err(format!(
113 "deployment state network mismatch: state is for {}, requested {network}",
114 state.network
115 )
116 .into());
117 }
118 let path = deployment_install_state_path(icp_root, network, &state.deployment_name);
119 if let Some(parent) = path.parent() {
120 fs::create_dir_all(parent)?;
121 }
122 fs::write(&path, serde_json::to_vec_pretty(state)?)?;
123 Ok(path)
124}
125
126fn reject_legacy_fleet_state(
127 icp_root: &Path,
128 network: &str,
129 deployment: &str,
130) -> Result<(), Box<dyn std::error::Error>> {
131 let path = fleet_install_state_path(icp_root, network, deployment);
132 if path.exists() {
133 return Err(format!(
134 "legacy fleet install state found: {}\n\nCanic 0.46 stores live deployment state by deployment target, not fleet template.\nCreate explicit deployment state with:\n canic deploy register {deployment} --fleet-template {deployment} --root <principal>\n\nOr reinstall the deployment with a 0.46 install path that writes deployment-target state:\n canic install {deployment}\n\nIf the old state is obsolete, remove:\n {}",
135 path.display(),
136 path.display()
137 )
138 .into());
139 }
140 Ok(())
141}
142
143pub(super) fn validate_fleet_name(name: &str) -> Result<(), Box<dyn std::error::Error>> {
145 let valid = !name.is_empty()
146 && name
147 .bytes()
148 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'));
149 if valid {
150 Ok(())
151 } else {
152 Err(format!("invalid fleet name {name:?}; use letters, numbers, '-' or '_'").into())
153 }
154}
155
156pub(super) fn validate_network_name(name: &str) -> Result<(), Box<dyn std::error::Error>> {
158 let valid = !name.is_empty()
159 && name
160 .bytes()
161 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'));
162 if valid {
163 Ok(())
164 } else {
165 Err(format!("invalid network name {name:?}; use letters, numbers, '-' or '_'").into())
166 }
167}