holochain_types/app/
app_bundle.rs

1//! An App Bundle is an AppManifest bundled together with DNA bundles.
2
3use std::{collections::HashMap, path::PathBuf, sync::Arc};
4
5use super::{AppManifest, AppManifestValidated};
6use crate::prelude::*;
7
8#[allow(missing_docs)]
9mod error;
10pub use error::*;
11use futures::future::join_all;
12
13#[cfg(test)]
14mod tests;
15
16/// A bundle of an AppManifest and collection of DNAs
17#[derive(Debug, Serialize, Deserialize, Clone, derive_more::From, shrinkwraprs::Shrinkwrap)]
18pub struct AppBundle(mr_bundle::Bundle<AppManifest>);
19
20impl AppBundle {
21    /// Create an AppBundle from a manifest and DNA files
22    pub async fn new<R: IntoIterator<Item = (PathBuf, DnaBundle)>>(
23        manifest: AppManifest,
24        resources: R,
25        root_dir: PathBuf,
26    ) -> AppBundleResult<Self> {
27        let resources = join_all(resources.into_iter().map(|(path, dna_bundle)| async move {
28            dna_bundle.encode().map(|bytes| (path, bytes.into()))
29        }))
30        .await
31        .into_iter()
32        .collect::<Result<Vec<_>, _>>()?;
33        Ok(mr_bundle::Bundle::new(manifest, resources, root_dir)?.into())
34    }
35
36    /// Construct from raw bytes
37    pub fn decode(bytes: &[u8]) -> AppBundleResult<Self> {
38        mr_bundle::Bundle::decode(bytes)
39            .map(Into::into)
40            .map_err(Into::into)
41    }
42
43    /// Convert to the inner Bundle
44    pub fn into_inner(self) -> mr_bundle::Bundle<AppManifest> {
45        self.0
46    }
47
48    /// Look up every installed_hash of every role, getting the DnaFiles from the DnaStore
49    pub fn get_all_dnas_from_store(&self, dna_store: &impl DnaStore) -> HashMap<DnaHash, DnaFile> {
50        self.manifest()
51            .app_roles()
52            .iter()
53            .flat_map(|role| role.dna.installed_hash.to_owned())
54            .map(Into::into)
55            .flat_map(|hash| dna_store.get_dna(&hash).map(|dna| (hash, dna)))
56            .collect()
57    }
58
59    /// Given a partial list of already available DnaFiles, fetch the missing others via
60    /// mr_bundle::Location resolution
61    pub async fn resolve_cells(
62        self,
63        dna_store: &impl DnaStore,
64        membrane_proofs: MemproofMap,
65        existing_cells: ExistingCellsMap,
66    ) -> AppBundleResult<AppRoleResolution> {
67        let AppManifestValidated { name: _, roles } = self.manifest().clone().validate()?;
68        let bundle = Arc::new(self);
69        let tasks = roles.into_iter().map(|(role_name, role)| async {
70            let bundle = bundle.clone();
71            Ok((
72                role_name.clone(),
73                bundle
74                    .resolve_cell(dna_store, role_name, role, &existing_cells)
75                    .await?,
76            ))
77        });
78
79        futures::future::join_all(tasks)
80            .await
81            .into_iter()
82            .collect::<AppBundleResult<Vec<_>>>()?
83            .into_iter()
84            .try_fold(
85                AppRoleResolution::default(),
86                |mut resolution: AppRoleResolution, (role_name, op)| {
87                    match op {
88                        CellProvisioningOp::CreateFromDnaFile(dna, clone_limit) => {
89                            let dna_hash = dna.dna_hash().clone();
90                            let role = AppRolePrimary::new(dna_hash, true, clone_limit).into();
91                            // TODO: could sequentialize this to remove the clone
92                            let proof = membrane_proofs.get(&role_name).cloned();
93                            resolution.dnas_to_register.push((dna, proof));
94                            resolution.role_assignments.push((role_name, role));
95                        }
96
97                        CellProvisioningOp::Existing(cell_id, protected) => {
98                            let role = AppRoleDependency { cell_id, protected }.into();
99                            resolution.role_assignments.push((role_name, role));
100                        }
101
102                        CellProvisioningOp::ProvisionOnly(dna, clone_limit) => {
103                            let dna_hash = dna.dna_hash().clone();
104
105                            // TODO: could sequentialize this to remove the clone
106                            let proof = membrane_proofs.get(&role_name).cloned();
107                            resolution.dnas_to_register.push((dna, proof));
108                            resolution.role_assignments.push((
109                                role_name,
110                                AppRolePrimary::new(dna_hash, false, clone_limit).into(),
111                            ));
112                        }
113                    }
114
115                    Ok(resolution)
116                },
117            )
118    }
119
120    async fn resolve_cell(
121        &self,
122        dna_store: &impl DnaStore,
123        role_name: RoleName,
124        role: AppRoleManifestValidated,
125        existing_cells: &ExistingCellsMap,
126    ) -> AppBundleResult<CellProvisioningOp> {
127        match role {
128            AppRoleManifestValidated::Create {
129                location,
130                installed_hash,
131                clone_limit,
132                modifiers,
133                deferred: _,
134            } => {
135                let dna = self
136                    .resolve_dna(
137                        role_name,
138                        dna_store,
139                        &location,
140                        installed_hash.as_ref(),
141                        modifiers,
142                    )
143                    .await?;
144                Ok(CellProvisioningOp::CreateFromDnaFile(dna, clone_limit))
145            }
146
147            AppRoleManifestValidated::UseExisting {
148                compatible_hash,
149                protected,
150            } => {
151                if let Some(cell_id) = existing_cells.get(&role_name) {
152                    Ok(CellProvisioningOp::Existing(cell_id.clone(), protected))
153                } else {
154                    Err(AppBundleError::CellResolutionFailure(
155                        role_name,
156                        format!("No existing cell was specified for the role with DNA {compatible_hash}"),
157                    ))
158                }
159            }
160
161            AppRoleManifestValidated::CloneOnly {
162                clone_limit,
163                location,
164                modifiers,
165                installed_hash,
166            } => {
167                let dna = self
168                    .resolve_dna(
169                        role_name,
170                        dna_store,
171                        &location,
172                        installed_hash.as_ref(),
173                        modifiers,
174                    )
175                    .await?;
176                Ok(CellProvisioningOp::ProvisionOnly(dna, clone_limit))
177            }
178        }
179    }
180
181    async fn resolve_dna(
182        &self,
183        role_name: RoleName,
184        dna_store: &impl DnaStore,
185        location: &mr_bundle::Location,
186        expected_hash: Option<&DnaHashB64>,
187        modifiers: DnaModifiersOpt,
188    ) -> AppBundleResult<DnaFile> {
189        let dna_file = if let Some(expected_hash) = expected_hash {
190            let expected_hash = expected_hash.clone().into();
191            let (dna_file, original_hash) =
192                if let Some(mut dna_file) = dna_store.get_dna(&expected_hash) {
193                    let original_hash = dna_file.dna_hash().clone();
194                    dna_file = dna_file.update_modifiers(modifiers);
195                    (dna_file, original_hash)
196                } else {
197                    self.resolve_location(location, modifiers).await?
198                };
199            if expected_hash != original_hash {
200                return Err(AppBundleError::CellResolutionFailure(
201                    role_name,
202                    format!("Hash mismatch: {} != {}", expected_hash, original_hash),
203                ));
204            }
205            dna_file
206        } else {
207            self.resolve_location(location, modifiers).await?.0
208        };
209        Ok(dna_file)
210    }
211
212    async fn resolve_location(
213        &self,
214        location: &mr_bundle::Location,
215        modifiers: DnaModifiersOpt,
216    ) -> AppBundleResult<(DnaFile, DnaHash)> {
217        let bytes = self.resolve(location).await?;
218        let dna_bundle: DnaBundle = mr_bundle::Bundle::decode(&bytes)?.into();
219        let (dna_file, original_hash) = dna_bundle.into_dna_file(modifiers).await?;
220        Ok((dna_file, original_hash))
221    }
222}
223
224/// The answer to the question:
225/// "how do we concretely assign DNAs to the open roles of this App?"
226/// Includes the DNAs selected to fill the roles and the details of the role assignments.
227// TODO: rework, make fields private
228#[allow(missing_docs)]
229#[derive(PartialEq, Eq, Debug, Default)]
230pub struct AppRoleResolution {
231    pub dnas_to_register: Vec<(DnaFile, Option<MembraneProof>)>,
232    pub role_assignments: Vec<(RoleName, AppRoleAssignment)>,
233}
234
235#[allow(missing_docs)]
236impl AppRoleResolution {
237    /// Return the IDs of new cells to be created as part of the resolution.
238    /// Does not return existing cells to be reused.
239    pub fn cells_to_create(&self, agent_key: AgentPubKey) -> Vec<(CellId, Option<MembraneProof>)> {
240        let provisioned = self
241            .role_assignments
242            .iter()
243            .filter_map(|(_name, role)| {
244                let role = role.as_primary()?;
245                if role.is_provisioned {
246                    Some(CellId::new(role.dna_hash().clone(), agent_key.clone()))
247                } else {
248                    None
249                }
250            })
251            .collect::<std::collections::HashSet<_>>();
252
253        self.dnas_to_register
254            .iter()
255            .filter_map(|(dna, proof)| {
256                let cell_id = CellId::new(dna.dna_hash().clone(), agent_key.clone());
257                if provisioned.contains(&cell_id) {
258                    Some((cell_id, proof.clone()))
259                } else {
260                    None
261                }
262            })
263            .collect()
264    }
265}
266
267/// Specifies what step should be taken to provision a cell while installing an App
268#[warn(missing_docs)]
269#[derive(Debug)]
270pub enum CellProvisioningOp {
271    /// Create a new Cell from the given DNA file
272    CreateFromDnaFile(DnaFile, u32),
273    /// Use an existing Cell
274    Existing(CellId, bool),
275    /// No creation needed, but there might be a clone_limit, and so we need
276    /// to know which DNA to use for making clones
277    ProvisionOnly(DnaFile, u32),
278}