Skip to main content

canic_host/deployment_truth/
plan.rs

1use super::*;
2use crate::{
3    install_root::{RootVerificationStatus, 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 = local_root_canister_id(request, &mut unresolved_assumptions);
78    let raw_config_sha256 = config_sha256_assumption(&config, &mut unresolved_assumptions);
79    let canonical_runtime_config_digest =
80        canonical_runtime_config_assumption(&config, &mut unresolved_assumptions);
81    let deployment_manifest_digest =
82        deployment_manifest_digest_assumption(request, &mut unresolved_assumptions);
83    let artifact_manifest = local_artifact_manifest(request, config);
84    extend_artifact_assumptions(
85        &mut unresolved_assumptions,
86        artifact_manifest.unresolved_artifacts,
87    );
88    let authority_profile = local_authority_profile(request, expected_controllers);
89    let role_artifacts = local_plan_role_artifacts(
90        artifact_manifest.role_artifacts,
91        &request.build_profile,
92        raw_config_sha256.as_ref(),
93    );
94    let expected_canisters = local_expected_canisters(roles, root_canister_id.as_deref());
95    let identity = local_plan_identity(
96        request,
97        PlanIdentityFacts {
98            root_canister_id: root_canister_id.clone(),
99            deployment_manifest_digest,
100            canonical_runtime_config_digest,
101            authority_profile: &authority_profile,
102            expected_canisters: &expected_canisters,
103            role_artifacts: &role_artifacts,
104            expected_pool: &expected_pool,
105        },
106    );
107
108    DeploymentPlanV1 {
109        schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
110        plan_id: format!("local:{}:{}:plan", request.network, request.deployment_name),
111        deployment_identity: identity,
112        trust_domain: TrustDomainV1 {
113            root_trust_anchor: root_canister_id,
114            migration_from: None,
115        },
116        fleet_template,
117        runtime_variant: request.runtime_variant.clone(),
118        authority_profile,
119        role_artifacts,
120        expected_canisters,
121        expected_pool,
122        expected_verifier_readiness: VerifierReadinessExpectationV1 {
123            required: false,
124            expected_role_epochs: Vec::new(),
125        },
126        unresolved_assumptions,
127    }
128}
129
130struct PlanIdentityFacts<'a> {
131    root_canister_id: Option<String>,
132    deployment_manifest_digest: Option<String>,
133    canonical_runtime_config_digest: Option<String>,
134    authority_profile: &'a AuthorityProfileV1,
135    expected_canisters: &'a [ExpectedCanisterV1],
136    role_artifacts: &'a [RoleArtifactV1],
137    expected_pool: &'a [ExpectedPoolCanisterV1],
138}
139
140fn local_artifact_manifest(
141    request: &LocalDeploymentPlanRequest,
142    config: PathBuf,
143) -> RoleArtifactManifestV1 {
144    collect_local_role_artifact_manifest(&LocalArtifactManifestRequest {
145        network: request.network.clone(),
146        workspace_root: request.workspace_root.clone(),
147        icp_root: request.icp_root.clone(),
148        config_path: Some(config),
149    })
150}
151
152fn local_plan_identity(
153    request: &LocalDeploymentPlanRequest,
154    facts: PlanIdentityFacts<'_>,
155) -> DeploymentIdentityV1 {
156    local_deployment_identity(
157        request,
158        PlanIdentityInput {
159            root_canister_id: facts.root_canister_id,
160            deployment_manifest_digest: facts.deployment_manifest_digest,
161            canonical_runtime_config_digest: facts.canonical_runtime_config_digest,
162            authority_profile_hash: Some(stable_json_sha256_hex(facts.authority_profile)),
163            role_topology_hash: Some(stable_json_sha256_hex(&facts.expected_canisters)),
164            artifact_set_digest: Some(stable_json_sha256_hex(&facts.role_artifacts)),
165            pool_identity_set_digest: Some(stable_json_sha256_hex(&facts.expected_pool)),
166        },
167    )
168}
169
170fn local_root_canister_id(
171    request: &LocalDeploymentPlanRequest,
172    assumptions: &mut Vec<DeploymentAssumptionV1>,
173) -> Option<String> {
174    match read_named_fleet_install_state_from_root(
175        &request.icp_root,
176        &request.network,
177        &request.deployment_name,
178    ) {
179        Ok(Some(state))
180            if state.network == request.network
181                && state.root_verification == RootVerificationStatus::Verified =>
182        {
183            Some(state.root_canister_id)
184        }
185        Ok(Some(state)) if state.network == request.network => {
186            assumptions.push(assumption(
187                "local_state.root_canister_id",
188                format!(
189                    "deployment state for {} records root {}, but root verification is {:?}; run deploy check/verification before mutation authority is trusted",
190                    request.deployment_name, state.root_canister_id, state.root_verification
191                ),
192            ));
193            None
194        }
195        Ok(Some(state)) => {
196            assumptions.push(assumption(
197                "local_state.root_canister_id",
198                format!(
199                    "deployment state for {} has network {}, expected {}",
200                    request.deployment_name, state.network, request.network
201                ),
202            ));
203            None
204        }
205        Ok(None) => {
206            assumptions.push(assumption(
207                "local_state.root_canister_id",
208                format!(
209                    "no local deployment state exists for {}; root identity is unknown until install or deploy register",
210                    request.deployment_name
211                ),
212            ));
213            None
214        }
215        Err(err) => {
216            assumptions.push(assumption(
217                "local_state.root_canister_id",
218                format!(
219                    "could not read deployment state for {}: {err}",
220                    request.deployment_name
221                ),
222            ));
223            None
224        }
225    }
226}
227
228struct PlanIdentityInput {
229    root_canister_id: Option<String>,
230    deployment_manifest_digest: Option<String>,
231    canonical_runtime_config_digest: Option<String>,
232    authority_profile_hash: Option<String>,
233    role_topology_hash: Option<String>,
234    artifact_set_digest: Option<String>,
235    pool_identity_set_digest: Option<String>,
236}
237
238fn local_deployment_identity(
239    request: &LocalDeploymentPlanRequest,
240    input: PlanIdentityInput,
241) -> DeploymentIdentityV1 {
242    DeploymentIdentityV1 {
243        deployment_name: request.deployment_name.clone(),
244        network: request.network.clone(),
245        root_principal: input.root_canister_id,
246        authority_profile_hash: input.authority_profile_hash,
247        role_topology_hash: input.role_topology_hash,
248        deployment_manifest_digest: input.deployment_manifest_digest,
249        canonical_runtime_config_digest: input.canonical_runtime_config_digest,
250        role_embedded_config_set_digest: None,
251        artifact_set_digest: input.artifact_set_digest,
252        pool_identity_set_digest: input.pool_identity_set_digest,
253        canic_version: Some(env!("CARGO_PKG_VERSION").to_string()),
254        ic_memory_version: None,
255    }
256}
257
258fn local_authority_profile(
259    request: &LocalDeploymentPlanRequest,
260    expected_controllers: Vec<String>,
261) -> AuthorityProfileV1 {
262    AuthorityProfileV1 {
263        profile_id: format!(
264            "local:{}:{}:authority",
265            request.network, request.deployment_name
266        ),
267        expected_controllers,
268        staging_controllers: Vec::new(),
269        emergency_controllers: Vec::new(),
270    }
271}
272
273fn local_expected_canisters(
274    roles: Vec<String>,
275    root_canister_id: Option<&str>,
276) -> Vec<ExpectedCanisterV1> {
277    roles
278        .into_iter()
279        .map(|role| ExpectedCanisterV1 {
280            canister_id: if role == "root" {
281                root_canister_id.map(str::to_string)
282            } else {
283                None
284            },
285            role,
286            control_class: CanisterControlClassV1::DeploymentControlled,
287        })
288        .collect()
289}
290
291fn local_expected_pool(pools: Vec<ConfiguredPoolExpectation>) -> Vec<ExpectedPoolCanisterV1> {
292    pools
293        .into_iter()
294        .map(|pool| ExpectedPoolCanisterV1 {
295            pool: pool.pool,
296            canister_id: None,
297            role: Some(pool.canister_role),
298        })
299        .collect()
300}
301
302fn local_plan_role_artifacts(
303    artifacts: Vec<RoleArtifactV1>,
304    build_profile: &str,
305    raw_config_sha256: Option<&String>,
306) -> Vec<RoleArtifactV1> {
307    artifacts
308        .into_iter()
309        .map(|mut artifact| {
310            artifact.build_profile = build_profile.to_string();
311            artifact.raw_config_sha256 = raw_config_sha256.cloned();
312            artifact
313        })
314        .collect()
315}
316
317fn extend_artifact_assumptions(
318    assumptions: &mut Vec<DeploymentAssumptionV1>,
319    gaps: Vec<DeploymentObservationGapV1>,
320) {
321    assumptions.extend(
322        gaps.into_iter()
323            .map(|gap| assumption(gap.key, gap.description)),
324    );
325}
326
327fn assumption(key: impl Into<String>, description: impl Into<String>) -> DeploymentAssumptionV1 {
328    DeploymentAssumptionV1 {
329        key: key.into(),
330        description: description.into(),
331    }
332}
333
334fn config_sha256_assumption(
335    path: &std::path::Path,
336    assumptions: &mut Vec<DeploymentAssumptionV1>,
337) -> Option<String> {
338    match file_sha256_hex(path) {
339        Ok(hash) => Some(hash),
340        Err(err) => {
341            assumptions.push(assumption(
342                "local_config.raw_sha256",
343                format!("could not hash config {}: {err}", path.display()),
344            ));
345            None
346        }
347    }
348}
349
350fn canonical_runtime_config_assumption(
351    path: &std::path::Path,
352    assumptions: &mut Vec<DeploymentAssumptionV1>,
353) -> Option<String> {
354    match canonical_runtime_config_sha256_hex(path) {
355        Ok(hash) => Some(hash),
356        Err(err) => {
357            assumptions.push(assumption(
358                "local_config.canonical_runtime_config_sha256",
359                format!(
360                    "could not hash canonical runtime config {}: {err}",
361                    path.display()
362                ),
363            ));
364            None
365        }
366    }
367}
368
369fn deployment_manifest_digest_assumption(
370    request: &LocalDeploymentPlanRequest,
371    assumptions: &mut Vec<DeploymentAssumptionV1>,
372) -> Option<String> {
373    let mut gaps = Vec::new();
374    let digest =
375        super::observe::release_set_manifest_digest(&request.icp_root, &request.network, &mut gaps);
376    assumptions.extend(
377        gaps.into_iter()
378            .map(|gap| assumption(gap.key, gap.description)),
379    );
380    digest
381}