Skip to main content

canic_core/ops/runtime/
install_source.rs

1//! Module: ops::runtime::install_source
2//!
3//! Responsibility: resolve approved wasm module sources for install workflows.
4//! Does not own: control-plane publication, wasm-store storage, or install execution.
5//! Boundary: delegates to the registered resolver and returns Store-backed chunk sources.
6
7use crate::{
8    InternalError, InternalErrorOrigin,
9    cdk::types::Principal,
10    domain::metrics::{
11        WasmStoreMetricOperation, WasmStoreMetricOutcome, WasmStoreMetricReason,
12        WasmStoreMetricSource,
13    },
14    format::byte_size,
15    ids::CanisterRole,
16    ops::runtime::metrics::wasm_store::WasmStoreMetrics,
17};
18use async_trait::async_trait;
19use std::sync::OnceLock;
20
21///
22/// ApprovedModuleSource
23///
24/// Approved install source metadata and payload for one canister role.
25///
26
27#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct ApprovedModuleSource {
29    source_canister: Principal,
30    source_label: String,
31    module_hash: Vec<u8>,
32    chunk_hashes: Vec<Vec<u8>>,
33    payload_size_bytes: u64,
34}
35
36impl ApprovedModuleSource {
37    /// Construct one chunk-store-backed module source.
38    #[must_use]
39    pub const fn chunked(
40        source_canister: Principal,
41        source_label: String,
42        module_hash: Vec<u8>,
43        chunk_hashes: Vec<Vec<u8>>,
44        payload_size_bytes: u64,
45    ) -> Self {
46        Self {
47            source_canister,
48            source_label,
49            module_hash,
50            chunk_hashes,
51            payload_size_bytes,
52        }
53    }
54
55    /// Return the Store canister that owns the approved chunk set.
56    #[must_use]
57    pub const fn source_canister(&self) -> &Principal {
58        &self.source_canister
59    }
60
61    /// Return the logical source label used for logs and status output.
62    #[must_use]
63    pub fn source_label(&self) -> &str {
64        &self.source_label
65    }
66
67    /// Return the installable wasm module hash.
68    #[must_use]
69    pub fn module_hash(&self) -> &[u8] {
70        &self.module_hash
71    }
72
73    /// Return the formatted module payload size for logs and status output.
74    #[must_use]
75    pub fn payload_size(&self) -> String {
76        byte_size(self.payload_size_bytes)
77    }
78
79    /// Return the raw payload size in bytes.
80    #[must_use]
81    pub const fn payload_size_bytes(&self) -> u64 {
82        self.payload_size_bytes
83    }
84
85    /// Return the approved chunk hashes in deterministic install order.
86    #[must_use]
87    pub fn chunk_hashes(&self) -> &[Vec<u8>] {
88        &self.chunk_hashes
89    }
90
91    /// Return the approved chunk count.
92    #[must_use]
93    pub const fn chunk_count(&self) -> usize {
94        self.chunk_hashes.len()
95    }
96}
97
98///
99/// ModuleSourceResolver
100///
101/// Driver interface for resolving approved install sources outside the runtime.
102///
103
104#[async_trait]
105pub trait ModuleSourceResolver: Send + Sync {
106    /// Resolve the currently approved install source for one canister role.
107    async fn approved_module_source(
108        &self,
109        role: &CanisterRole,
110    ) -> Result<ApprovedModuleSource, InternalError>;
111}
112
113static MODULE_SOURCE_RESOLVER: OnceLock<&'static dyn ModuleSourceResolver> = OnceLock::new();
114
115///
116/// ModuleSourceRuntimeApi
117///
118/// Process-local registry and resolver facade for approved module sources.
119///
120
121pub struct ModuleSourceRuntimeApi;
122
123impl ModuleSourceRuntimeApi {
124    /// Register the control-plane resolver used by root-owned installation flows.
125    pub fn register_module_source_resolver(resolver: &'static dyn ModuleSourceResolver) {
126        let _ = MODULE_SOURCE_RESOLVER.set(resolver);
127    }
128
129    /// Resolve the approved install source for one canister role through the registered driver.
130    pub(crate) async fn approved_module_source(
131        role: &CanisterRole,
132    ) -> Result<ApprovedModuleSource, InternalError> {
133        let resolver = MODULE_SOURCE_RESOLVER.get().ok_or_else(|| {
134            WasmStoreMetrics::record(
135                WasmStoreMetricOperation::SourceResolve,
136                WasmStoreMetricSource::Resolver,
137                WasmStoreMetricOutcome::Failed,
138                WasmStoreMetricReason::InvalidState,
139            );
140            InternalError::workflow(
141                InternalErrorOrigin::Workflow,
142                "module source resolver is not registered; root/control-plane install flows are unavailable".to_string(),
143            )
144        })?;
145
146        match resolver.approved_module_source(role).await {
147            Ok(source) => {
148                WasmStoreMetrics::record(
149                    WasmStoreMetricOperation::SourceResolve,
150                    WasmStoreMetricSource::Resolver,
151                    WasmStoreMetricOutcome::Completed,
152                    WasmStoreMetricReason::Ok,
153                );
154                Ok(source)
155            }
156            Err(err) => {
157                WasmStoreMetrics::record(
158                    WasmStoreMetricOperation::SourceResolve,
159                    WasmStoreMetricSource::Resolver,
160                    WasmStoreMetricOutcome::Failed,
161                    WasmStoreMetricReason::StoreCall,
162                );
163                Err(err)
164            }
165        }
166    }
167}