Skip to main content

canic_host/install_root/
state.rs

1use crate::{durable_io::write_bytes, release_set::icp_root};
2use std::{fs, path::Path, path::PathBuf};
3
4use serde::{Deserialize, Serialize};
5
6pub(super) const INSTALL_STATE_SCHEMA_VERSION: u32 = 2;
7
8///
9/// RootVerificationStatus
10///
11
12#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
13#[serde(rename_all = "snake_case")]
14pub enum RootVerificationStatus {
15    Verified,
16    NotVerified,
17}
18
19///
20/// InstallState
21///
22
23#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
24#[serde(deny_unknown_fields)]
25pub struct InstallState {
26    pub schema_version: u32,
27    pub deployment_name: String,
28    pub fleet_template: String,
29    pub created_at_unix_secs: u64,
30    pub updated_at_unix_secs: u64,
31    pub network: String,
32    pub root_target: String,
33    pub root_canister_id: String,
34    pub root_verification: RootVerificationStatus,
35    pub root_build_target: String,
36    pub workspace_root: String,
37    pub icp_root: String,
38    pub config_path: String,
39    pub release_set_manifest_path: String,
40}
41
42/// Read deployment-target install state for one project/network when present.
43pub(super) fn read_deployment_install_state(
44    icp_root: &Path,
45    network: &str,
46    deployment: &str,
47) -> Result<Option<InstallState>, Box<dyn std::error::Error>> {
48    validate_network_name(network)?;
49    validate_state_name(deployment)?;
50    let path = deployment_install_state_path(icp_root, network, deployment);
51    if !path.is_file() {
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
60/// Read deployment-target install state for the discovered current project.
61pub 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
69/// Read deployment-target install state for an explicit ICP project root.
70pub 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/// Return the project-local state path for one deployment target.
79#[must_use]
80pub(super) fn deployment_install_state_path(
81    icp_root: &Path,
82    network: &str,
83    deployment: &str,
84) -> PathBuf {
85    deployments_dir(icp_root, network).join(format!("{deployment}.json"))
86}
87
88// Return the directory that owns deployment-target state files.
89fn deployments_dir(icp_root: &Path, network: &str) -> PathBuf {
90    icp_root.join(".canic").join(network).join("deployments")
91}
92
93// Persist the completed install state under the project-local `.canic` directory.
94pub(super) fn write_install_state(
95    icp_root: &Path,
96    network: &str,
97    state: &InstallState,
98) -> Result<PathBuf, Box<dyn std::error::Error>> {
99    validate_network_name(network)?;
100    validate_state_name(&state.deployment_name)?;
101    if state.network != network {
102        return Err(format!(
103            "deployment state network mismatch: state is for {}, requested {network}",
104            state.network
105        )
106        .into());
107    }
108    let path = deployment_install_state_path(icp_root, network, &state.deployment_name);
109    write_bytes(&path, &serde_json::to_vec_pretty(state)?)?;
110    Ok(path)
111}
112
113// Keep deployment and template names filesystem-safe and easy to type.
114pub(super) fn validate_state_name(name: &str) -> Result<(), Box<dyn std::error::Error>> {
115    let valid = !name.is_empty()
116        && name
117            .bytes()
118            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'));
119    if valid {
120        Ok(())
121    } else {
122        Err(
123            format!("invalid deployment/template name {name:?}; use letters, numbers, '-' or '_'")
124                .into(),
125        )
126    }
127}
128
129// Keep network names safe for `.canic/<network>` state paths.
130pub(super) fn validate_network_name(name: &str) -> Result<(), Box<dyn std::error::Error>> {
131    let valid = !name.is_empty()
132        && name
133            .bytes()
134            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'));
135    if valid {
136        Ok(())
137    } else {
138        Err(format!("invalid network name {name:?}; use letters, numbers, '-' or '_'").into())
139    }
140}