1use 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 reject_legacy_fleet_state(icp_root, network, deployment)?;
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 legacy_fleet_install_state_path(
81 icp_root: &Path,
82 network: &str,
83 fleet: &str,
84) -> PathBuf {
85 fleets_dir(icp_root, network).join(format!("{fleet}.json"))
86}
87
88#[must_use]
90pub(super) fn deployment_install_state_path(
91 icp_root: &Path,
92 network: &str,
93 deployment: &str,
94) -> PathBuf {
95 deployments_dir(icp_root, network).join(format!("{deployment}.json"))
96}
97
98fn fleets_dir(icp_root: &Path, network: &str) -> PathBuf {
100 icp_root.join(".canic").join(network).join("fleets")
101}
102
103fn deployments_dir(icp_root: &Path, network: &str) -> PathBuf {
105 icp_root.join(".canic").join(network).join("deployments")
106}
107
108pub(super) fn write_install_state(
110 icp_root: &Path,
111 network: &str,
112 state: &InstallState,
113) -> Result<PathBuf, Box<dyn std::error::Error>> {
114 validate_network_name(network)?;
115 validate_state_name(&state.deployment_name)?;
116 if state.network != network {
117 return Err(format!(
118 "deployment state network mismatch: state is for {}, requested {network}",
119 state.network
120 )
121 .into());
122 }
123 let path = deployment_install_state_path(icp_root, network, &state.deployment_name);
124 if let Some(parent) = path.parent() {
125 fs::create_dir_all(parent)?;
126 }
127 fs::write(&path, serde_json::to_vec_pretty(state)?)?;
128 Ok(path)
129}
130
131fn reject_legacy_fleet_state(
132 icp_root: &Path,
133 network: &str,
134 deployment: &str,
135) -> Result<(), Box<dyn std::error::Error>> {
136 let path = legacy_fleet_install_state_path(icp_root, network, deployment);
137 if path.exists() {
138 return Err(format!(
139 "legacy fleet install state found: {}\n\nCanic 0.46 stores live deployment state by deployment target, not fleet template.\nCreate explicit deployment state with the fleet template that owns this target:\n canic deploy register {deployment} --fleet-template <fleet-template> --root <principal> --allow-unverified\n\nOr reinstall using the fleet template/config that should produce this deployment:\n canic install <fleet-template>\n\nIf the old state is obsolete, remove:\n {}",
140 path.display(),
141 path.display()
142 )
143 .into());
144 }
145 Ok(())
146}
147
148pub(super) fn validate_state_name(name: &str) -> Result<(), Box<dyn std::error::Error>> {
150 let valid = !name.is_empty()
151 && name
152 .bytes()
153 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'));
154 if valid {
155 Ok(())
156 } else {
157 Err(
158 format!("invalid deployment/template name {name:?}; use letters, numbers, '-' or '_'")
159 .into(),
160 )
161 }
162}
163
164pub(super) fn validate_network_name(name: &str) -> Result<(), Box<dyn std::error::Error>> {
166 let valid = !name.is_empty()
167 && name
168 .bytes()
169 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'));
170 if valid {
171 Ok(())
172 } else {
173 Err(format!("invalid network name {name:?}; use letters, numbers, '-' or '_'").into())
174 }
175}