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;
6pub(super) const CURRENT_DEPLOYMENT_STATE_BOUNDARY_MESSAGE: &str =
7 "Current Canic reads live deployment state by deployment target, not fleet template.";
8
9#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
14#[serde(rename_all = "snake_case")]
15pub enum RootVerificationStatus {
16 Verified,
17 NotVerified,
18}
19
20#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
25#[serde(deny_unknown_fields)]
26pub struct InstallState {
27 pub schema_version: u32,
28 pub deployment_name: String,
29 pub fleet_template: String,
30 pub created_at_unix_secs: u64,
31 pub updated_at_unix_secs: u64,
32 pub network: String,
33 pub root_target: String,
34 pub root_canister_id: String,
35 pub root_verification: RootVerificationStatus,
36 pub root_build_target: String,
37 pub workspace_root: String,
38 pub icp_root: String,
39 pub config_path: String,
40 pub release_set_manifest_path: String,
41}
42
43pub(super) fn read_deployment_install_state(
45 icp_root: &Path,
46 network: &str,
47 deployment: &str,
48) -> Result<Option<InstallState>, Box<dyn std::error::Error>> {
49 validate_network_name(network)?;
50 validate_state_name(deployment)?;
51 let path = deployment_install_state_path(icp_root, network, deployment);
52 if !path.is_file() {
53 reject_legacy_fleet_state(icp_root, network, deployment)?;
54 return Ok(None);
55 }
56
57 let bytes = fs::read(&path)?;
58 let state: InstallState = serde_json::from_slice(&bytes)?;
59 Ok(Some(state))
60}
61
62pub fn read_named_deployment_install_state(
64 network: &str,
65 deployment: &str,
66) -> Result<Option<InstallState>, Box<dyn std::error::Error>> {
67 let icp_root = icp_root()?;
68 read_deployment_install_state(&icp_root, network, deployment)
69}
70
71pub fn read_named_deployment_install_state_from_root(
73 icp_root: &Path,
74 network: &str,
75 deployment: &str,
76) -> Result<Option<InstallState>, Box<dyn std::error::Error>> {
77 read_deployment_install_state(icp_root, network, deployment)
78}
79
80#[must_use]
82pub(super) fn legacy_fleet_install_state_path(
83 icp_root: &Path,
84 network: &str,
85 fleet: &str,
86) -> PathBuf {
87 fleets_dir(icp_root, network).join(format!("{fleet}.json"))
88}
89
90#[must_use]
92pub(super) fn deployment_install_state_path(
93 icp_root: &Path,
94 network: &str,
95 deployment: &str,
96) -> PathBuf {
97 deployments_dir(icp_root, network).join(format!("{deployment}.json"))
98}
99
100fn fleets_dir(icp_root: &Path, network: &str) -> PathBuf {
102 icp_root.join(".canic").join(network).join("fleets")
103}
104
105fn deployments_dir(icp_root: &Path, network: &str) -> PathBuf {
107 icp_root.join(".canic").join(network).join("deployments")
108}
109
110pub(super) fn write_install_state(
112 icp_root: &Path,
113 network: &str,
114 state: &InstallState,
115) -> Result<PathBuf, Box<dyn std::error::Error>> {
116 validate_network_name(network)?;
117 validate_state_name(&state.deployment_name)?;
118 if state.network != network {
119 return Err(format!(
120 "deployment state network mismatch: state is for {}, requested {network}",
121 state.network
122 )
123 .into());
124 }
125 let path = deployment_install_state_path(icp_root, network, &state.deployment_name);
126 if let Some(parent) = path.parent() {
127 fs::create_dir_all(parent)?;
128 }
129 fs::write(&path, serde_json::to_vec_pretty(state)?)?;
130 Ok(path)
131}
132
133fn reject_legacy_fleet_state(
134 icp_root: &Path,
135 network: &str,
136 deployment: &str,
137) -> Result<(), Box<dyn std::error::Error>> {
138 let path = legacy_fleet_install_state_path(icp_root, network, deployment);
139 if path.exists() {
140 return Err(format!(
141 "legacy fleet install state found: {}\n\n{CURRENT_DEPLOYMENT_STATE_BOUNDARY_MESSAGE}\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 {}",
142 path.display(),
143 path.display()
144 )
145 .into());
146 }
147 Ok(())
148}
149
150pub(super) fn validate_state_name(name: &str) -> Result<(), Box<dyn std::error::Error>> {
152 let valid = !name.is_empty()
153 && name
154 .bytes()
155 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'));
156 if valid {
157 Ok(())
158 } else {
159 Err(
160 format!("invalid deployment/template name {name:?}; use letters, numbers, '-' or '_'")
161 .into(),
162 )
163 }
164}
165
166pub(super) fn validate_network_name(name: &str) -> Result<(), Box<dyn std::error::Error>> {
168 let valid = !name.is_empty()
169 && name
170 .bytes()
171 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'));
172 if valid {
173 Ok(())
174 } else {
175 Err(format!("invalid network name {name:?}; use letters, numbers, '-' or '_'").into())
176 }
177}