Skip to main content

canic_host/install_root/
state.rs

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///
8/// RootVerificationStatus
9///
10
11#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
12#[serde(rename_all = "snake_case")]
13pub enum RootVerificationStatus {
14    Verified,
15    NotVerified,
16}
17
18///
19/// InstallState
20///
21
22#[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
40/// Read deployment-target install state for one project/network when present.
41pub(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
59/// Read deployment-target install state for the discovered current project.
60pub 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
68/// Read deployment-target install state for an explicit ICP project root.
69pub 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/// Return the legacy project-local fleet state path.
78#[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/// Return the project-local state path for one deployment target.
84#[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
93// Return the directory that owns named fleet state files.
94fn fleets_dir(icp_root: &Path, network: &str) -> PathBuf {
95    icp_root.join(".canic").join(network).join("fleets")
96}
97
98// Return the directory that owns deployment-target state files.
99fn deployments_dir(icp_root: &Path, network: &str) -> PathBuf {
100    icp_root.join(".canic").join(network).join("deployments")
101}
102
103// Persist the completed install state under the project-local `.canic` directory.
104pub(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
143// Keep fleet names filesystem-safe and easy to type in commands.
144pub(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
156// Keep network names safe for `.canic/<network>` state paths.
157pub(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}