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::{
5        ConfiguredPoolExpectation, configured_controllers, configured_fleet_name,
6        configured_fleet_roles, configured_pool_expectations,
7    },
8};
9use std::path::PathBuf;
10
11///
12/// LocalDeploymentPlanRequest
13///
14#[derive(Clone, Debug, Eq, PartialEq)]
15pub struct LocalDeploymentPlanRequest {
16    pub deployment_name: String,
17    pub network: String,
18    pub workspace_root: PathBuf,
19    pub icp_root: PathBuf,
20    pub config_path: Option<PathBuf>,
21    pub runtime_variant: String,
22    pub build_profile: String,
23}
24
25/// Build a local deployment plan from resolved host config and local artifact
26/// observations without querying or mutating IC state.
27#[must_use]
28pub fn build_local_deployment_plan(request: &LocalDeploymentPlanRequest) -> DeploymentPlanV1 {
29    let config = deployment_config_path(&request.workspace_root, request.config_path.as_deref());
30    let mut unresolved_assumptions = Vec::new();
31    let fleet_template = configured_fleet_name(&config).unwrap_or_else(|err| {
32        unresolved_assumptions.push(assumption(
33            "local_config.fleet_name",
34            format!(
35                "could not resolve fleet template name from {}: {err}",
36                config.display()
37            ),
38        ));
39        request.deployment_name.clone()
40    });
41    let roles = configured_fleet_roles(&config).map_or_else(
42        |err| {
43            unresolved_assumptions.push(assumption(
44                "local_config.roles",
45                format!(
46                    "could not resolve configured roles from {}: {err}",
47                    config.display()
48                ),
49            ));
50            Vec::new()
51        },
52        deployment_truth_roles_with_implicit_wasm_store,
53    );
54    let expected_controllers = configured_controllers(&config).unwrap_or_else(|err| {
55        unresolved_assumptions.push(assumption(
56            "local_config.controllers",
57            format!(
58                "could not resolve configured controllers from {}: {err}",
59                config.display()
60            ),
61        ));
62        Vec::new()
63    });
64    let expected_pool = configured_pool_expectations(&config).map_or_else(
65        |err| {
66            unresolved_assumptions.push(assumption(
67                "local_config.pools",
68                format!(
69                    "could not resolve configured pool expectations from {}: {err}",
70                    config.display()
71                ),
72            ));
73            Vec::new()
74        },
75        local_expected_pool,
76    );
77    let root_canister_id =
78        local_root_canister_id(request, &fleet_template, &mut unresolved_assumptions);
79    let raw_config_sha256 = config_sha256_assumption(&config, &mut unresolved_assumptions);
80    let canonical_runtime_config_digest =
81        canonical_runtime_config_assumption(&config, &mut unresolved_assumptions);
82    let deployment_manifest_digest =
83        deployment_manifest_digest_assumption(request, &mut unresolved_assumptions);
84    let artifact_manifest = local_artifact_manifest(request, config);
85    extend_artifact_assumptions(
86        &mut unresolved_assumptions,
87        artifact_manifest.unresolved_artifacts,
88    );
89    let authority_profile = local_authority_profile(request, expected_controllers);
90    let role_artifacts = local_plan_role_artifacts(
91        artifact_manifest.role_artifacts,
92        &request.build_profile,
93        raw_config_sha256.as_ref(),
94    );
95    let expected_canisters = local_expected_canisters(roles, root_canister_id.as_deref());
96    let identity = local_plan_identity(
97        request,
98        PlanIdentityFacts {
99            root_canister_id: root_canister_id.clone(),
100            deployment_manifest_digest,
101            canonical_runtime_config_digest,
102            authority_profile: &authority_profile,
103            expected_canisters: &expected_canisters,
104            role_artifacts: &role_artifacts,
105            expected_pool: &expected_pool,
106        },
107    );
108
109    DeploymentPlanV1 {
110        schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
111        plan_id: format!("local:{}:{}:plan", request.network, request.deployment_name),
112        deployment_identity: identity,
113        trust_domain: TrustDomainV1 {
114            root_trust_anchor: root_canister_id,
115            migration_from: None,
116        },
117        fleet_template,
118        runtime_variant: request.runtime_variant.clone(),
119        authority_profile,
120        role_artifacts,
121        expected_canisters,
122        expected_pool,
123        expected_verifier_readiness: VerifierReadinessExpectationV1 {
124            required: false,
125            expected_role_epochs: Vec::new(),
126        },
127        unresolved_assumptions,
128    }
129}
130
131struct PlanIdentityFacts<'a> {
132    root_canister_id: Option<String>,
133    deployment_manifest_digest: Option<String>,
134    canonical_runtime_config_digest: Option<String>,
135    authority_profile: &'a AuthorityProfileV1,
136    expected_canisters: &'a [ExpectedCanisterV1],
137    role_artifacts: &'a [RoleArtifactV1],
138    expected_pool: &'a [ExpectedPoolCanisterV1],
139}
140
141fn local_artifact_manifest(
142    request: &LocalDeploymentPlanRequest,
143    config: PathBuf,
144) -> RoleArtifactManifestV1 {
145    collect_local_role_artifact_manifest(&LocalArtifactManifestRequest {
146        network: request.network.clone(),
147        workspace_root: request.workspace_root.clone(),
148        icp_root: request.icp_root.clone(),
149        config_path: Some(config),
150    })
151}
152
153fn local_plan_identity(
154    request: &LocalDeploymentPlanRequest,
155    facts: PlanIdentityFacts<'_>,
156) -> DeploymentIdentityV1 {
157    local_deployment_identity(
158        request,
159        PlanIdentityInput {
160            root_canister_id: facts.root_canister_id,
161            deployment_manifest_digest: facts.deployment_manifest_digest,
162            canonical_runtime_config_digest: facts.canonical_runtime_config_digest,
163            authority_profile_hash: Some(stable_json_sha256_hex(facts.authority_profile)),
164            role_topology_hash: Some(stable_json_sha256_hex(&facts.expected_canisters)),
165            artifact_set_digest: Some(stable_json_sha256_hex(&facts.role_artifacts)),
166            pool_identity_set_digest: Some(stable_json_sha256_hex(&facts.expected_pool)),
167        },
168    )
169}
170
171fn local_root_canister_id(
172    request: &LocalDeploymentPlanRequest,
173    fleet_template: &str,
174    assumptions: &mut Vec<DeploymentAssumptionV1>,
175) -> Option<String> {
176    match read_named_fleet_install_state_from_root(
177        &request.icp_root,
178        &request.network,
179        fleet_template,
180    ) {
181        Ok(Some(state)) if state.network == request.network => Some(state.root_canister_id),
182        Ok(Some(state)) => {
183            assumptions.push(assumption(
184                "local_state.root_canister_id",
185                format!(
186                    "install state for fleet {fleet_template} has network {}, expected {}",
187                    state.network, request.network
188                ),
189            ));
190            None
191        }
192        Ok(None) => {
193            assumptions.push(assumption(
194                "local_state.root_canister_id",
195                format!(
196                    "no local install state exists for fleet {fleet_template}; root identity is unknown until install"
197                ),
198            ));
199            None
200        }
201        Err(err) => {
202            assumptions.push(assumption(
203                "local_state.root_canister_id",
204                format!("could not read install state for fleet {fleet_template}: {err}"),
205            ));
206            None
207        }
208    }
209}
210
211struct PlanIdentityInput {
212    root_canister_id: Option<String>,
213    deployment_manifest_digest: Option<String>,
214    canonical_runtime_config_digest: Option<String>,
215    authority_profile_hash: Option<String>,
216    role_topology_hash: Option<String>,
217    artifact_set_digest: Option<String>,
218    pool_identity_set_digest: Option<String>,
219}
220
221fn local_deployment_identity(
222    request: &LocalDeploymentPlanRequest,
223    input: PlanIdentityInput,
224) -> DeploymentIdentityV1 {
225    DeploymentIdentityV1 {
226        deployment_name: request.deployment_name.clone(),
227        network: request.network.clone(),
228        root_principal: input.root_canister_id,
229        authority_profile_hash: input.authority_profile_hash,
230        role_topology_hash: input.role_topology_hash,
231        deployment_manifest_digest: input.deployment_manifest_digest,
232        canonical_runtime_config_digest: input.canonical_runtime_config_digest,
233        role_embedded_config_set_digest: None,
234        artifact_set_digest: input.artifact_set_digest,
235        pool_identity_set_digest: input.pool_identity_set_digest,
236        canic_version: Some(env!("CARGO_PKG_VERSION").to_string()),
237        ic_memory_version: None,
238    }
239}
240
241fn local_authority_profile(
242    request: &LocalDeploymentPlanRequest,
243    expected_controllers: Vec<String>,
244) -> AuthorityProfileV1 {
245    AuthorityProfileV1 {
246        profile_id: format!(
247            "local:{}:{}:authority",
248            request.network, request.deployment_name
249        ),
250        expected_controllers,
251        staging_controllers: Vec::new(),
252        emergency_controllers: Vec::new(),
253    }
254}
255
256fn local_expected_canisters(
257    roles: Vec<String>,
258    root_canister_id: Option<&str>,
259) -> Vec<ExpectedCanisterV1> {
260    roles
261        .into_iter()
262        .map(|role| ExpectedCanisterV1 {
263            canister_id: if role == "root" {
264                root_canister_id.map(str::to_string)
265            } else {
266                None
267            },
268            role,
269            control_class: CanisterControlClassV1::DeploymentControlled,
270        })
271        .collect()
272}
273
274fn local_expected_pool(pools: Vec<ConfiguredPoolExpectation>) -> Vec<ExpectedPoolCanisterV1> {
275    pools
276        .into_iter()
277        .map(|pool| ExpectedPoolCanisterV1 {
278            pool: pool.pool,
279            canister_id: None,
280            role: Some(pool.canister_role),
281        })
282        .collect()
283}
284
285fn local_plan_role_artifacts(
286    artifacts: Vec<RoleArtifactV1>,
287    build_profile: &str,
288    raw_config_sha256: Option<&String>,
289) -> Vec<RoleArtifactV1> {
290    artifacts
291        .into_iter()
292        .map(|mut artifact| {
293            artifact.build_profile = build_profile.to_string();
294            artifact.raw_config_sha256 = raw_config_sha256.cloned();
295            artifact
296        })
297        .collect()
298}
299
300fn extend_artifact_assumptions(
301    assumptions: &mut Vec<DeploymentAssumptionV1>,
302    gaps: Vec<DeploymentObservationGapV1>,
303) {
304    assumptions.extend(
305        gaps.into_iter()
306            .map(|gap| assumption(gap.key, gap.description)),
307    );
308}
309
310fn assumption(key: impl Into<String>, description: impl Into<String>) -> DeploymentAssumptionV1 {
311    DeploymentAssumptionV1 {
312        key: key.into(),
313        description: description.into(),
314    }
315}
316
317fn config_sha256_assumption(
318    path: &std::path::Path,
319    assumptions: &mut Vec<DeploymentAssumptionV1>,
320) -> Option<String> {
321    match file_sha256_hex(path) {
322        Ok(hash) => Some(hash),
323        Err(err) => {
324            assumptions.push(assumption(
325                "local_config.raw_sha256",
326                format!("could not hash config {}: {err}", path.display()),
327            ));
328            None
329        }
330    }
331}
332
333fn canonical_runtime_config_assumption(
334    path: &std::path::Path,
335    assumptions: &mut Vec<DeploymentAssumptionV1>,
336) -> Option<String> {
337    match canonical_runtime_config_sha256_hex(path) {
338        Ok(hash) => Some(hash),
339        Err(err) => {
340            assumptions.push(assumption(
341                "local_config.canonical_runtime_config_sha256",
342                format!(
343                    "could not hash canonical runtime config {}: {err}",
344                    path.display()
345                ),
346            ));
347            None
348        }
349    }
350}
351
352fn deployment_manifest_digest_assumption(
353    request: &LocalDeploymentPlanRequest,
354    assumptions: &mut Vec<DeploymentAssumptionV1>,
355) -> Option<String> {
356    let mut gaps = Vec::new();
357    let digest =
358        super::observe::release_set_manifest_digest(&request.icp_root, &request.network, &mut gaps);
359    assumptions.extend(
360        gaps.into_iter()
361            .map(|gap| assumption(gap.key, gap.description)),
362    );
363    digest
364}