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: mediates embedded sources and registered resolver-backed sources.
6
7use crate::{
8    InternalError, InternalErrorOrigin,
9    cdk::{types::Principal, utils::hash::wasm_hash},
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::{
20    borrow::Cow,
21    collections::BTreeMap,
22    sync::{Mutex, OnceLock},
23};
24
25///
26/// ApprovedModulePayload
27///
28/// Runtime representation of the installable wasm payload backing one source.
29///
30
31#[derive(Clone, Debug, Eq, PartialEq)]
32pub enum ApprovedModulePayload {
33    Chunked {
34        source_canister: Principal,
35        chunk_hashes: Vec<Vec<u8>>,
36    },
37    Embedded {
38        wasm_module: Cow<'static, [u8]>,
39    },
40}
41
42///
43/// ApprovedModuleSource
44///
45/// Approved install source metadata and payload for one canister role.
46///
47
48#[derive(Clone, Debug, Eq, PartialEq)]
49pub struct ApprovedModuleSource {
50    source_label: String,
51    module_hash: Vec<u8>,
52    payload_size_bytes: u64,
53    payload: ApprovedModulePayload,
54}
55
56impl ApprovedModuleSource {
57    /// Construct one chunk-store-backed module source.
58    #[must_use]
59    pub const fn chunked(
60        source_canister: Principal,
61        source_label: String,
62        module_hash: Vec<u8>,
63        chunk_hashes: Vec<Vec<u8>>,
64        payload_size_bytes: u64,
65    ) -> Self {
66        Self {
67            source_label,
68            module_hash,
69            payload_size_bytes,
70            payload: ApprovedModulePayload::Chunked {
71                source_canister,
72                chunk_hashes,
73            },
74        }
75    }
76
77    /// Construct one embedded module source from already packaged wasm bytes.
78    #[must_use]
79    pub fn embedded(source_label: String, wasm_module: &'static [u8]) -> Self {
80        let payload_size_bytes = wasm_module.len() as u64;
81
82        Self {
83            source_label,
84            module_hash: wasm_hash(wasm_module),
85            payload_size_bytes,
86            payload: ApprovedModulePayload::Embedded {
87                wasm_module: Cow::Borrowed(wasm_module),
88            },
89        }
90    }
91
92    /// Return the logical source label used for logs and status output.
93    #[must_use]
94    pub fn source_label(&self) -> &str {
95        &self.source_label
96    }
97
98    /// Return the installable wasm module hash.
99    #[must_use]
100    pub fn module_hash(&self) -> &[u8] {
101        &self.module_hash
102    }
103
104    /// Return the formatted module payload size for logs and status output.
105    #[must_use]
106    pub fn payload_size(&self) -> String {
107        byte_size(self.payload_size_bytes)
108    }
109
110    /// Return the raw payload size in bytes.
111    #[must_use]
112    pub const fn payload_size_bytes(&self) -> u64 {
113        self.payload_size_bytes
114    }
115
116    /// Return the chunk count when the source is chunk-store-backed.
117    #[must_use]
118    pub const fn chunk_count(&self) -> usize {
119        match &self.payload {
120            ApprovedModulePayload::Chunked { chunk_hashes, .. } => chunk_hashes.len(),
121            ApprovedModulePayload::Embedded { .. } => 0,
122        }
123    }
124
125    /// Return the underlying payload representation.
126    #[must_use]
127    pub const fn payload(&self) -> &ApprovedModulePayload {
128        &self.payload
129    }
130}
131
132///
133/// ModuleSourceResolver
134///
135/// Driver interface for resolving approved install sources outside the runtime.
136///
137
138#[async_trait]
139pub trait ModuleSourceResolver: Send + Sync {
140    /// Resolve the currently approved install source for one canister role.
141    async fn approved_module_source(
142        &self,
143        role: &CanisterRole,
144    ) -> Result<ApprovedModuleSource, InternalError>;
145}
146
147static MODULE_SOURCE_RESOLVER: OnceLock<&'static dyn ModuleSourceResolver> = OnceLock::new();
148static EMBEDDED_MODULE_SOURCES: OnceLock<Mutex<BTreeMap<CanisterRole, ApprovedModuleSource>>> =
149    OnceLock::new();
150
151///
152/// ModuleSourceRuntimeApi
153///
154/// Process-local registry and resolver facade for approved module sources.
155///
156
157pub struct ModuleSourceRuntimeApi;
158
159impl ModuleSourceRuntimeApi {
160    /// Register one built-in module source override for the current process.
161    ///
162    /// # Panics
163    ///
164    /// Panics when the same role has already been registered with a different
165    /// embedded module source in this process.
166    pub fn register_embedded_module_source(role: CanisterRole, source: ApprovedModuleSource) {
167        let sources = EMBEDDED_MODULE_SOURCES.get_or_init(|| Mutex::new(BTreeMap::new()));
168        let mut sources = sources
169            .lock()
170            .unwrap_or_else(std::sync::PoisonError::into_inner);
171
172        match sources.get(&role) {
173            Some(existing) if existing == &source => {}
174            Some(existing) => {
175                panic!(
176                    "embedded module source for role '{role}' was already registered with a different payload: existing='{}' new='{}'",
177                    existing.source_label(),
178                    source.source_label()
179                );
180            }
181            None => {
182                sources.insert(role, source);
183            }
184        }
185    }
186
187    /// Register one embedded wasm payload as the built-in install source for one role.
188    pub fn register_embedded_module_wasm(
189        role: CanisterRole,
190        source_label: impl Into<String>,
191        wasm_module: &'static [u8],
192    ) {
193        Self::register_embedded_module_source(
194            role,
195            ApprovedModuleSource::embedded(source_label.into(), wasm_module),
196        );
197    }
198
199    /// Register the control-plane resolver used by root-owned installation flows.
200    pub fn register_module_source_resolver(resolver: &'static dyn ModuleSourceResolver) {
201        let _ = MODULE_SOURCE_RESOLVER.set(resolver);
202    }
203
204    /// Return whether one embedded module source override has been registered.
205    #[must_use]
206    pub fn has_embedded_module_source(role: &CanisterRole) -> bool {
207        EMBEDDED_MODULE_SOURCES.get().is_some_and(|sources| {
208            let sources = sources
209                .lock()
210                .unwrap_or_else(std::sync::PoisonError::into_inner);
211            sources.contains_key(role)
212        })
213    }
214
215    /// Resolve the approved install source for one canister role through the registered driver.
216    pub(crate) async fn approved_module_source(
217        role: &CanisterRole,
218    ) -> Result<ApprovedModuleSource, InternalError> {
219        if let Some(source) = EMBEDDED_MODULE_SOURCES.get().and_then(|sources| {
220            let sources = sources
221                .lock()
222                .unwrap_or_else(std::sync::PoisonError::into_inner);
223            sources.get(role).cloned()
224        }) {
225            WasmStoreMetrics::record(
226                WasmStoreMetricOperation::SourceResolve,
227                WasmStoreMetricSource::Embedded,
228                WasmStoreMetricOutcome::Completed,
229                WasmStoreMetricReason::Ok,
230            );
231            return Ok(source);
232        }
233
234        let resolver = MODULE_SOURCE_RESOLVER.get().ok_or_else(|| {
235            WasmStoreMetrics::record(
236                WasmStoreMetricOperation::SourceResolve,
237                WasmStoreMetricSource::Resolver,
238                WasmStoreMetricOutcome::Failed,
239                WasmStoreMetricReason::InvalidState,
240            );
241            InternalError::workflow(
242                InternalErrorOrigin::Workflow,
243                "module source resolver is not registered; root/control-plane install flows are unavailable".to_string(),
244            )
245        })?;
246
247        match resolver.approved_module_source(role).await {
248            Ok(source) => {
249                WasmStoreMetrics::record(
250                    WasmStoreMetricOperation::SourceResolve,
251                    WasmStoreMetricSource::Resolver,
252                    WasmStoreMetricOutcome::Completed,
253                    WasmStoreMetricReason::Ok,
254                );
255                Ok(source)
256            }
257            Err(err) => {
258                WasmStoreMetrics::record(
259                    WasmStoreMetricOperation::SourceResolve,
260                    WasmStoreMetricSource::Resolver,
261                    WasmStoreMetricOutcome::Failed,
262                    WasmStoreMetricReason::StoreCall,
263                );
264                Err(err)
265            }
266        }
267    }
268}