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    pub fn register_embedded_module_source(role: CanisterRole, source: ApprovedModuleSource) {
161        let sources = EMBEDDED_MODULE_SOURCES.get_or_init(|| Mutex::new(BTreeMap::new()));
162        let mut sources = sources
163            .lock()
164            .unwrap_or_else(std::sync::PoisonError::into_inner);
165
166        match sources.get(&role) {
167            Some(existing) if existing == &source => {}
168            Some(existing) => {
169                panic!(
170                    "embedded module source for role '{role}' was already registered with a different payload: existing='{}' new='{}'",
171                    existing.source_label(),
172                    source.source_label()
173                );
174            }
175            None => {
176                sources.insert(role, source);
177            }
178        }
179    }
180
181    /// Register one embedded wasm payload as the built-in install source for one role.
182    pub fn register_embedded_module_wasm(
183        role: CanisterRole,
184        source_label: impl Into<String>,
185        wasm_module: &'static [u8],
186    ) {
187        Self::register_embedded_module_source(
188            role,
189            ApprovedModuleSource::embedded(source_label.into(), wasm_module),
190        );
191    }
192
193    /// Register the control-plane resolver used by root-owned installation flows.
194    pub fn register_module_source_resolver(resolver: &'static dyn ModuleSourceResolver) {
195        let _ = MODULE_SOURCE_RESOLVER.set(resolver);
196    }
197
198    /// Return whether one embedded module source override has been registered.
199    #[must_use]
200    pub fn has_embedded_module_source(role: &CanisterRole) -> bool {
201        EMBEDDED_MODULE_SOURCES.get().is_some_and(|sources| {
202            let sources = sources
203                .lock()
204                .unwrap_or_else(std::sync::PoisonError::into_inner);
205            sources.contains_key(role)
206        })
207    }
208
209    /// Resolve the approved install source for one canister role through the registered driver.
210    pub(crate) async fn approved_module_source(
211        role: &CanisterRole,
212    ) -> Result<ApprovedModuleSource, InternalError> {
213        if let Some(source) = EMBEDDED_MODULE_SOURCES.get().and_then(|sources| {
214            let sources = sources
215                .lock()
216                .unwrap_or_else(std::sync::PoisonError::into_inner);
217            sources.get(role).cloned()
218        }) {
219            WasmStoreMetrics::record(
220                WasmStoreMetricOperation::SourceResolve,
221                WasmStoreMetricSource::Embedded,
222                WasmStoreMetricOutcome::Completed,
223                WasmStoreMetricReason::Ok,
224            );
225            return Ok(source);
226        }
227
228        let resolver = MODULE_SOURCE_RESOLVER.get().ok_or_else(|| {
229            WasmStoreMetrics::record(
230                WasmStoreMetricOperation::SourceResolve,
231                WasmStoreMetricSource::Resolver,
232                WasmStoreMetricOutcome::Failed,
233                WasmStoreMetricReason::InvalidState,
234            );
235            InternalError::workflow(
236                InternalErrorOrigin::Workflow,
237                "module source resolver is not registered; root/control-plane install flows are unavailable".to_string(),
238            )
239        })?;
240
241        match resolver.approved_module_source(role).await {
242            Ok(source) => {
243                WasmStoreMetrics::record(
244                    WasmStoreMetricOperation::SourceResolve,
245                    WasmStoreMetricSource::Resolver,
246                    WasmStoreMetricOutcome::Completed,
247                    WasmStoreMetricReason::Ok,
248                );
249                Ok(source)
250            }
251            Err(err) => {
252                WasmStoreMetrics::record(
253                    WasmStoreMetricOperation::SourceResolve,
254                    WasmStoreMetricSource::Resolver,
255                    WasmStoreMetricOutcome::Failed,
256                    WasmStoreMetricReason::StoreCall,
257                );
258                Err(err)
259            }
260        }
261    }
262}