Skip to main content

canic_host/deployment_truth/
plan.rs

1use super::*;
2use crate::{
3    install_root::read_named_fleet_install_state_from_root,
4    release_set::{configured_controllers, configured_fleet_name, configured_fleet_roles},
5};
6use std::path::PathBuf;
7
8///
9/// LocalDeploymentPlanRequest
10///
11#[derive(Clone, Debug, Eq, PartialEq)]
12pub struct LocalDeploymentPlanRequest {
13    pub deployment_name: String,
14    pub network: String,
15    pub workspace_root: PathBuf,
16    pub icp_root: PathBuf,
17    pub config_path: Option<PathBuf>,
18    pub runtime_variant: String,
19    pub build_profile: String,
20}
21
22/// Build a local deployment plan from resolved host config and local artifact
23/// observations without querying or mutating IC state.
24#[must_use]
25pub fn build_local_deployment_plan(request: &LocalDeploymentPlanRequest) -> DeploymentPlanV1 {
26    let config = deployment_config_path(&request.workspace_root, request.config_path.as_deref());
27    let mut unresolved_assumptions = Vec::new();
28    let fleet_template = configured_fleet_name(&config).unwrap_or_else(|err| {
29        unresolved_assumptions.push(assumption(
30            "local_config.fleet_name",
31            format!(
32                "could not resolve fleet template name from {}: {err}",
33                config.display()
34            ),
35        ));
36        request.deployment_name.clone()
37    });
38    let roles = configured_fleet_roles(&config).unwrap_or_else(|err| {
39        unresolved_assumptions.push(assumption(
40            "local_config.roles",
41            format!(
42                "could not resolve configured roles from {}: {err}",
43                config.display()
44            ),
45        ));
46        Vec::new()
47    });
48    let expected_controllers = configured_controllers(&config).unwrap_or_else(|err| {
49        unresolved_assumptions.push(assumption(
50            "local_config.controllers",
51            format!(
52                "could not resolve configured controllers from {}: {err}",
53                config.display()
54            ),
55        ));
56        Vec::new()
57    });
58    let root_canister_id =
59        local_root_canister_id(request, &fleet_template, &mut unresolved_assumptions);
60    let raw_config_sha256 = config_sha256_assumption(&config, &mut unresolved_assumptions);
61    let artifact_manifest = collect_local_role_artifact_manifest(&LocalArtifactManifestRequest {
62        network: request.network.clone(),
63        workspace_root: request.workspace_root.clone(),
64        icp_root: request.icp_root.clone(),
65        config_path: Some(config),
66    });
67    unresolved_assumptions.extend(
68        artifact_manifest
69            .unresolved_artifacts
70            .into_iter()
71            .map(|gap| assumption(gap.key, gap.description)),
72    );
73
74    DeploymentPlanV1 {
75        schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
76        plan_id: format!("local:{}:{}:plan", request.network, request.deployment_name),
77        deployment_identity: local_deployment_identity(request, root_canister_id.clone()),
78        trust_domain: TrustDomainV1 {
79            root_trust_anchor: root_canister_id.clone(),
80            migration_from: None,
81        },
82        fleet_template,
83        runtime_variant: request.runtime_variant.clone(),
84        authority_profile: local_authority_profile(request, expected_controllers),
85        role_artifacts: artifact_manifest
86            .role_artifacts
87            .into_iter()
88            .map(|mut artifact| {
89                artifact.build_profile.clone_from(&request.build_profile);
90                artifact.raw_config_sha256.clone_from(&raw_config_sha256);
91                artifact
92            })
93            .collect(),
94        expected_canisters: local_expected_canisters(roles, root_canister_id.as_deref()),
95        expected_pool: Vec::new(),
96        expected_verifier_readiness: VerifierReadinessExpectationV1 {
97            required: false,
98            expected_role_epochs: Vec::new(),
99        },
100        unresolved_assumptions,
101    }
102}
103
104fn local_root_canister_id(
105    request: &LocalDeploymentPlanRequest,
106    fleet_template: &str,
107    assumptions: &mut Vec<DeploymentAssumptionV1>,
108) -> Option<String> {
109    match read_named_fleet_install_state_from_root(
110        &request.icp_root,
111        &request.network,
112        fleet_template,
113    ) {
114        Ok(Some(state)) if state.network == request.network => Some(state.root_canister_id),
115        Ok(Some(state)) => {
116            assumptions.push(assumption(
117                "local_state.root_canister_id",
118                format!(
119                    "install state for fleet {fleet_template} has network {}, expected {}",
120                    state.network, request.network
121                ),
122            ));
123            None
124        }
125        Ok(None) => {
126            assumptions.push(assumption(
127                "local_state.root_canister_id",
128                format!(
129                    "no local install state exists for fleet {fleet_template}; root identity is unknown until install"
130                ),
131            ));
132            None
133        }
134        Err(err) => {
135            assumptions.push(assumption(
136                "local_state.root_canister_id",
137                format!("could not read install state for fleet {fleet_template}: {err}"),
138            ));
139            None
140        }
141    }
142}
143
144fn local_deployment_identity(
145    request: &LocalDeploymentPlanRequest,
146    root_canister_id: Option<String>,
147) -> DeploymentIdentityV1 {
148    DeploymentIdentityV1 {
149        deployment_name: request.deployment_name.clone(),
150        network: request.network.clone(),
151        root_principal: root_canister_id,
152        authority_profile_hash: None,
153        role_topology_hash: None,
154        deployment_manifest_digest: None,
155        canonical_runtime_config_digest: None,
156        role_embedded_config_set_digest: None,
157        artifact_set_digest: None,
158        pool_identity_set_digest: None,
159        canic_version: Some(env!("CARGO_PKG_VERSION").to_string()),
160        ic_memory_version: None,
161    }
162}
163
164fn local_authority_profile(
165    request: &LocalDeploymentPlanRequest,
166    expected_controllers: Vec<String>,
167) -> AuthorityProfileV1 {
168    AuthorityProfileV1 {
169        profile_id: format!(
170            "local:{}:{}:authority",
171            request.network, request.deployment_name
172        ),
173        expected_controllers,
174        staging_controllers: Vec::new(),
175        emergency_controllers: Vec::new(),
176    }
177}
178
179fn local_expected_canisters(
180    roles: Vec<String>,
181    root_canister_id: Option<&str>,
182) -> Vec<ExpectedCanisterV1> {
183    roles
184        .into_iter()
185        .map(|role| ExpectedCanisterV1 {
186            canister_id: if role == "root" {
187                root_canister_id.map(str::to_string)
188            } else {
189                None
190            },
191            role,
192            control_class: CanisterControlClassV1::DeploymentControlled,
193        })
194        .collect()
195}
196
197fn assumption(key: impl Into<String>, description: impl Into<String>) -> DeploymentAssumptionV1 {
198    DeploymentAssumptionV1 {
199        key: key.into(),
200        description: description.into(),
201    }
202}
203
204fn config_sha256_assumption(
205    path: &std::path::Path,
206    assumptions: &mut Vec<DeploymentAssumptionV1>,
207) -> Option<String> {
208    match file_sha256_hex(path) {
209        Ok(hash) => Some(hash),
210        Err(err) => {
211            assumptions.push(assumption(
212                "local_config.raw_sha256",
213                format!("could not hash config {}: {err}", path.display()),
214            ));
215            None
216        }
217    }
218}