Skip to main content

canic_host/deployment_truth/
plan.rs

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