Skip to main content

canic_core/api/template/
mod.rs

1use crate::{
2    cdk::types::Principal,
3    dto::{
4        error::Error,
5        template::{
6            TemplateChunkInput, TemplateChunkResponse, TemplateChunkSetInfoResponse,
7            TemplateChunkSetPrepareInput, TemplateManifestInput, WasmStoreAdminCommand,
8            WasmStoreAdminResponse, WasmStoreBootstrapDebugResponse, WasmStoreCatalogEntryResponse,
9            WasmStoreOverviewResponse, WasmStorePublicationSlotResponse, WasmStoreStatusResponse,
10        },
11    },
12    ids::{CanisterRole, TemplateId, TemplateVersion, WasmStoreBinding, WasmStoreGcStatus},
13    ops::{
14        config::ConfigOps,
15        ic::IcOps,
16        storage::{
17            state::subnet::SubnetStateOps,
18            template::{TemplateManifestOps, WasmStoreGcExecutionStats, WasmStoreLimits},
19        },
20    },
21    workflow::runtime::template::WasmStorePublicationWorkflow,
22};
23
24const ROOT_WASM_STORE_BOOTSTRAP_TEMPLATE_ID: TemplateId = TemplateId::new("embedded:wasm_store");
25const ROOT_WASM_STORE_BOOTSTRAP_BINDING: WasmStoreBinding = WasmStoreBinding::new("bootstrap");
26
27///
28/// EmbeddedTemplateApi
29///
30
31pub struct EmbeddedTemplateApi;
32
33impl EmbeddedTemplateApi {
34    // Seed approved manifests and template-keyed embedded payloads for the current release set.
35    pub fn import_embedded_release_set(wasms: &'static [(CanisterRole, &[u8])]) {
36        WasmStorePublicationWorkflow::import_embedded_release_set(wasms);
37    }
38}
39
40///
41/// WasmStoreBootstrapApi
42///
43
44pub struct WasmStoreBootstrapApi;
45
46impl WasmStoreBootstrapApi {
47    // Validate that one staged template request targets the root-local WasmStore bootstrap source.
48    fn ensure_root_wasm_store_bootstrap_template(template_id: &TemplateId) -> Result<(), Error> {
49        if template_id == &ROOT_WASM_STORE_BOOTSTRAP_TEMPLATE_ID {
50            Ok(())
51        } else {
52            Err(Error::invalid(format!(
53                "bootstrap only accepts template '{ROOT_WASM_STORE_BOOTSTRAP_TEMPLATE_ID}'"
54            )))
55        }
56    }
57
58    // Normalize one staged manifest onto the root-local WasmStore bootstrap source of truth.
59    fn normalize_root_wasm_store_bootstrap_manifest(
60        request: TemplateManifestInput,
61    ) -> Result<TemplateManifestInput, Error> {
62        if request.role != CanisterRole::WASM_STORE {
63            return Err(Error::invalid(format!(
64                "bootstrap only accepts role '{}'",
65                CanisterRole::WASM_STORE
66            )));
67        }
68
69        Self::ensure_root_wasm_store_bootstrap_template(&request.template_id)?;
70
71        let now_secs = IcOps::now_secs();
72
73        Ok(TemplateManifestInput {
74            template_id: ROOT_WASM_STORE_BOOTSTRAP_TEMPLATE_ID,
75            role: CanisterRole::WASM_STORE,
76            version: request.version,
77            payload_hash: request.payload_hash,
78            payload_size_bytes: request.payload_size_bytes,
79            store_binding: ROOT_WASM_STORE_BOOTSTRAP_BINDING,
80            chunking_mode: crate::ids::TemplateChunkingMode::Chunked,
81            manifest_state: crate::ids::TemplateManifestState::Approved,
82            approved_at: Some(now_secs),
83            created_at: now_secs,
84        })
85    }
86
87    // Stage the normalized root-local bootstrap manifest for `embedded:wasm_store`.
88    pub fn stage_root_wasm_store_manifest(request: TemplateManifestInput) -> Result<(), Error> {
89        Self::stage_manifest(Self::normalize_root_wasm_store_bootstrap_manifest(request)?);
90        Ok(())
91    }
92
93    // Prepare root-local chunk metadata for the staged `embedded:wasm_store` bootstrap source.
94    pub fn prepare_root_wasm_store_chunk_set(
95        request: TemplateChunkSetPrepareInput,
96    ) -> Result<TemplateChunkSetInfoResponse, Error> {
97        Self::ensure_root_wasm_store_bootstrap_template(&request.template_id)?;
98        Self::prepare_chunk_set(request)
99    }
100
101    // Publish one root-local chunk into the staged `embedded:wasm_store` bootstrap source.
102    pub fn publish_root_wasm_store_chunk(request: TemplateChunkInput) -> Result<(), Error> {
103        Self::ensure_root_wasm_store_bootstrap_template(&request.template_id)?;
104        Self::publish_chunk(request)
105    }
106
107    // Stage one approved manifest in the current canister's local bootstrap source.
108    pub fn stage_manifest(input: TemplateManifestInput) {
109        TemplateManifestOps::replace_approved_from_input(input);
110    }
111
112    // Prepare one local chunk set for chunk-by-chunk staging in the current canister.
113    pub fn prepare_chunk_set(
114        request: TemplateChunkSetPrepareInput,
115    ) -> Result<TemplateChunkSetInfoResponse, Error> {
116        let now_secs = IcOps::now_secs();
117        TemplateManifestOps::prepare_chunk_set_from_input(request, now_secs).map_err(Error::from)
118    }
119
120    // Stage one chunk into the current canister's local bootstrap source.
121    pub fn publish_chunk(request: TemplateChunkInput) -> Result<(), Error> {
122        TemplateManifestOps::publish_chunk_from_input(request).map_err(Error::from)
123    }
124
125    // Publish all root-local staged releases into the current subnet's selected wasm store.
126    pub async fn publish_staged_release_set_to_current_store() -> Result<(), Error> {
127        WasmStorePublicationWorkflow::publish_staged_release_set_to_current_store()
128            .await
129            .map_err(Error::from)
130    }
131
132    // Return root-owned staged bootstrap visibility for the bootstrap role and current release buffer.
133    pub fn debug_bootstrap() -> Result<WasmStoreBootstrapDebugResponse, Error> {
134        TemplateManifestOps::bootstrap_debug_response(&CanisterRole::WASM_STORE)
135            .map_err(Error::from)
136    }
137}
138
139///
140/// WasmStorePublicationApi
141///
142
143pub struct WasmStorePublicationApi;
144
145impl WasmStorePublicationApi {
146    fn current_overview_store_limits() -> WasmStoreLimits {
147        let store = ConfigOps::current_subnet_default_wasm_store();
148
149        WasmStoreLimits {
150            max_store_bytes: store.max_store_bytes(),
151            max_templates: store.max_templates(),
152            max_template_versions_per_template: store.max_template_versions_per_template(),
153        }
154    }
155
156    fn current_overview_store_headroom_bytes() -> Option<u64> {
157        ConfigOps::current_subnet_default_wasm_store().headroom_bytes()
158    }
159
160    // Execute one typed root-owned WasmStore publication or lifecycle admin command.
161    pub async fn admin(cmd: WasmStoreAdminCommand) -> Result<WasmStoreAdminResponse, Error> {
162        WasmStorePublicationWorkflow::handle_admin(cmd)
163            .await
164            .map_err(Error::from)
165    }
166
167    // Publish the current release set into one subnet-local wasm store.
168    pub async fn publish_current_release_set_to_store(store_pid: Principal) -> Result<(), Error> {
169        WasmStorePublicationWorkflow::publish_current_release_set_to_store(store_pid)
170            .await
171            .map_err(Error::from)
172    }
173
174    // Publish the current release set into the current subnet's selected publication wasm store.
175    pub async fn publish_current_release_set_to_current_store() -> Result<(), Error> {
176        WasmStorePublicationWorkflow::publish_current_release_set_to_current_store()
177            .await
178            .map_err(Error::from)
179    }
180
181    // Persist one explicit publication binding for the current subnet.
182    pub fn set_current_publication_store_binding(binding: WasmStoreBinding) -> Result<(), Error> {
183        WasmStorePublicationWorkflow::set_current_publication_store_binding(binding)
184            .map_err(Error::from)
185    }
186
187    // Clear the explicit publication binding for the current subnet.
188    pub fn clear_current_publication_store_binding() {
189        WasmStorePublicationWorkflow::clear_current_publication_store_binding();
190    }
191
192    // Retire the current detached publication binding for the current subnet.
193    #[must_use]
194    pub fn retire_detached_publication_store_binding() -> Option<WasmStoreBinding> {
195        WasmStorePublicationWorkflow::retire_detached_publication_store_binding()
196    }
197
198    // Return one root-owned overview for every tracked runtime-managed wasm store.
199    pub fn overview() -> Result<WasmStoreOverviewResponse, Error> {
200        let publication = SubnetStateOps::publication_store_state_response();
201        let limits = Self::current_overview_store_limits();
202        let headroom_bytes = Self::current_overview_store_headroom_bytes();
203        let stores = SubnetStateOps::wasm_stores()
204            .into_iter()
205            .map(|store| {
206                let publication_slot =
207                    if publication.active_binding.as_ref() == Some(&store.binding) {
208                        Some(WasmStorePublicationSlotResponse::Active)
209                    } else if publication.detached_binding.as_ref() == Some(&store.binding) {
210                        Some(WasmStorePublicationSlotResponse::Detached)
211                    } else if publication.retired_binding.as_ref() == Some(&store.binding) {
212                        Some(WasmStorePublicationSlotResponse::Retired)
213                    } else {
214                        None
215                    };
216
217                TemplateManifestOps::root_store_overview_response(
218                    &store.binding,
219                    store.pid,
220                    store.created_at,
221                    limits,
222                    headroom_bytes,
223                    crate::ids::WasmStoreGcStatus {
224                        mode: store.gc.mode,
225                        changed_at: store.gc.changed_at,
226                        prepared_at: store.gc.prepared_at,
227                        started_at: store.gc.started_at,
228                        completed_at: store.gc.completed_at,
229                        runs_completed: store.gc.runs_completed,
230                    },
231                    publication_slot,
232                )
233            })
234            .collect();
235
236        Ok(WasmStoreOverviewResponse {
237            publication,
238            stores,
239        })
240    }
241
242    // Mark the current retired publication store as prepared for store-local GC execution.
243    pub async fn prepare_retired_publication_store_for_gc()
244    -> Result<Option<WasmStoreBinding>, Error> {
245        WasmStorePublicationWorkflow::prepare_retired_publication_store_for_gc()
246            .await
247            .map_err(Error::from)
248    }
249
250    // Mark the current retired publication store as actively executing store-local GC.
251    pub async fn begin_retired_publication_store_gc() -> Result<Option<WasmStoreBinding>, Error> {
252        WasmStorePublicationWorkflow::begin_retired_publication_store_gc()
253            .await
254            .map_err(Error::from)
255    }
256
257    // Mark the current retired publication store as having completed its local GC pass.
258    pub async fn complete_retired_publication_store_gc() -> Result<Option<WasmStoreBinding>, Error>
259    {
260        WasmStorePublicationWorkflow::complete_retired_publication_store_gc()
261            .await
262            .map_err(Error::from)
263    }
264
265    // Clear the current retired publication binding after the local store GC run has completed.
266    pub async fn finalize_retired_publication_store_binding()
267    -> Result<Option<(WasmStoreBinding, Principal)>, Error> {
268        WasmStorePublicationWorkflow::finalize_retired_publication_store_binding()
269            .await
270            .map_err(Error::from)
271    }
272
273    // Delete one finalized retired publication store after root publication state no longer references it.
274    pub async fn delete_finalized_publication_store(
275        binding: WasmStoreBinding,
276        store_pid: Principal,
277    ) -> Result<(), Error> {
278        WasmStorePublicationWorkflow::delete_finalized_publication_store(binding, store_pid)
279            .await
280            .map_err(Error::from)
281    }
282}
283
284///
285/// WasmStoreApi
286///
287
288pub struct WasmStoreApi;
289
290impl WasmStoreApi {
291    fn current_store_limits() -> Result<WasmStoreLimits, Error> {
292        let store = ConfigOps::current_wasm_store()?;
293
294        Ok(WasmStoreLimits {
295            max_store_bytes: store.max_store_bytes(),
296            max_templates: store.max_templates(),
297            max_template_versions_per_template: store.max_template_versions_per_template(),
298        })
299    }
300
301    fn current_store_headroom_bytes() -> Result<Option<u64>, Error> {
302        Ok(ConfigOps::current_wasm_store()?.headroom_bytes())
303    }
304
305    // Return the approved template release catalog for this local store.
306    pub fn template_catalog() -> Result<Vec<WasmStoreCatalogEntryResponse>, Error> {
307        Ok(TemplateManifestOps::approved_catalog_response())
308    }
309
310    // Return current occupied-byte, retention, and store-local GC state for this local wasm store.
311    pub fn template_status(gc: WasmStoreGcStatus) -> Result<WasmStoreStatusResponse, Error> {
312        Ok(TemplateManifestOps::store_status_response(
313            Self::current_store_limits()?,
314            Self::current_store_headroom_bytes()?,
315            gc,
316        ))
317    }
318
319    // Prepare deterministic chunk-set metadata before chunk-by-chunk publication begins.
320    pub fn prepare_chunk_set(
321        request: TemplateChunkSetPrepareInput,
322    ) -> Result<TemplateChunkSetInfoResponse, Error> {
323        let now_secs = IcOps::now_secs();
324        TemplateManifestOps::prepare_chunk_set_in_store_from_input(
325            request,
326            now_secs,
327            Self::current_store_limits()?,
328        )
329        .map_err(Error::from)
330    }
331
332    // Publish one deterministic chunk into an already prepared local template release.
333    pub fn publish_chunk(request: TemplateChunkInput) -> Result<(), Error> {
334        TemplateManifestOps::publish_chunk_in_store_from_input(
335            request,
336            Self::current_store_limits()?,
337        )
338        .map_err(Error::from)
339    }
340
341    // Clear all local template metadata and chunk bytes for store-local GC execution.
342    pub async fn execute_local_store_gc() -> Result<WasmStoreGcExecutionStats, Error> {
343        TemplateManifestOps::execute_local_store_gc()
344            .await
345            .map_err(Error::from)
346    }
347
348    // Return deterministic chunk-set metadata for one local template release.
349    pub fn template_info(
350        template_id: TemplateId,
351        version: TemplateVersion,
352    ) -> Result<TemplateChunkSetInfoResponse, Error> {
353        TemplateManifestOps::chunk_set_info_response(&template_id, &version).map_err(Error::from)
354    }
355
356    // Return one deterministic chunk for one local template release.
357    pub fn template_chunk(
358        template_id: TemplateId,
359        version: TemplateVersion,
360        chunk_index: u32,
361    ) -> Result<TemplateChunkResponse, Error> {
362        TemplateManifestOps::chunk_response(&template_id, &version, chunk_index)
363            .map_err(Error::from)
364    }
365}