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