wick_package/
package.rs

1use std::collections::HashSet;
2use std::future::Future;
3use std::path::{Path, PathBuf};
4
5use asset_container::{Asset, AssetFlags, AssetManager, Assets};
6use normpath::PathExt;
7use sha256::digest;
8use tokio::fs;
9use tracing::trace;
10use wick_config::config::RegistryConfig;
11use wick_config::{AssetReference, WickConfiguration};
12use wick_oci_utils::package::annotations::Annotations;
13use wick_oci_utils::package::{media_types, PackageFile};
14use wick_oci_utils::OciOptions;
15
16use crate::utils::{create_tar_gz, metadata_to_annotations};
17use crate::Error;
18
19type BoxFuture<'a, T> = std::pin::Pin<Box<dyn Future<Output = T> + Send + 'a>>;
20
21type ProcessResult = (HashSet<PathBuf>, Vec<PackageFile>);
22
23fn process_assets(
24  mut seen_assets: HashSet<PathBuf>,
25  assets: Assets<AssetReference>,
26  root_parent_dir: PathBuf,
27  parent_dir: PathBuf,
28) -> BoxFuture<Result<ProcessResult, Error>> {
29  let task = async move {
30    let mut wick_files: Vec<PackageFile> = Vec::new();
31    for asset in assets.iter() {
32      if asset.get_asset_flags() == AssetFlags::Lazy {
33        continue;
34      }
35      let pdir = parent_dir.clone();
36      asset.update_baseurl(&pdir);
37      if !asset.exists_outside_cache() {
38        continue;
39      }
40      let asset_path = asset.path()?; // the resolved, absolute path relative to the config location.
41      if seen_assets.contains(&asset_path) {
42        continue;
43      }
44      seen_assets.insert(asset_path.clone());
45
46      let relative_path = asset_path.strip_prefix(&root_parent_dir).unwrap_or(&asset_path);
47
48      let options = wick_config::FetchOptions::default();
49      let media_type: &str;
50
51      let new_parent_dir = asset_path
52        .parent()
53        .map_or_else(|| PathBuf::from("/"), |v| v.to_path_buf());
54
55      match relative_path.extension().and_then(|os_str| os_str.to_str()) {
56        Some("yaml" | "yml" | "wick") => {
57          let config = WickConfiguration::fetch((*asset).clone(), options.clone())
58            .await
59            .map(|b| b.into_inner());
60          match config {
61            Ok(WickConfiguration::App(config)) => {
62              media_type = media_types::APPLICATION;
63              let app_assets = config.assets();
64              let (newly_seen, app_files) =
65                process_assets(seen_assets, app_assets, root_parent_dir.clone(), new_parent_dir.clone()).await?;
66              seen_assets = newly_seen;
67              wick_files.extend(app_files);
68            }
69            Ok(WickConfiguration::Component(config)) => {
70              media_type = media_types::COMPONENT;
71              let component_assets = config.assets();
72              let (newly_seen, component_files) = process_assets(
73                seen_assets,
74                component_assets,
75                root_parent_dir.clone(),
76                new_parent_dir.clone(),
77              )
78              .await?;
79              seen_assets = newly_seen;
80              wick_files.extend(component_files);
81            }
82            Ok(WickConfiguration::Tests(_)) => {
83              media_type = media_types::TESTS;
84            }
85            Ok(WickConfiguration::Types(_)) => {
86              media_type = media_types::TYPES;
87            }
88            _ => {
89              media_type = media_types::OTHER;
90            }
91          }
92        }
93        Some("wasm") => {
94          media_type = media_types::WASM;
95        }
96        _ => {
97          media_type = media_types::OTHER;
98        }
99      }
100
101      if asset.exists_outside_cache() {
102        let file_bytes = asset.bytes(&options).await?;
103        let hash = format!("sha256:{}", digest(file_bytes.as_ref()));
104        let wick_file = PackageFile::new(relative_path.to_path_buf(), hash, media_type.to_owned(), file_bytes);
105        wick_files.push(wick_file);
106      }
107    }
108
109    Ok((seen_assets, wick_files))
110  };
111  Box::pin(task)
112}
113
114/// Represents a Wick package, including its files and metadata.
115#[derive(Debug, Clone)]
116pub struct WickPackage {
117  kind: wick_config::config::ConfigurationKind,
118  name: String,
119  version: String,
120  files: Vec<PackageFile>,
121  annotations: Annotations,
122  absolute_path: PathBuf,
123  registry: Option<RegistryConfig>,
124  root: String,
125  basedir: Option<PathBuf>,
126}
127
128impl WickPackage {
129  /// Creates a new WickPackage from the provided path.
130  ///
131  /// The provided path can be a file or directory. If it is a directory, the WickPackage will be created
132  /// based on the files within the directory.
133  #[allow(clippy::too_many_lines)]
134  pub async fn from_path(basedir: Option<PathBuf>, path: &Path) -> Result<Self, Error> {
135    let path = basedir
136      .as_ref()
137      .map_or_else(|| path.to_path_buf(), |basedir| basedir.join(path));
138    //add check to see if its a path or directory and call appropriate api to find files based on that.
139    if path.is_dir() {
140      return Err(Error::Directory(path));
141    }
142
143    let options = wick_config::FetchOptions::default();
144    let config = WickConfiguration::fetch(&path, options).await?.into_inner();
145    if !matches!(
146      config,
147      WickConfiguration::App(_) | WickConfiguration::Component(_) | WickConfiguration::Types(_)
148    ) {
149      return Err(Error::InvalidWickConfig(path.to_string_lossy().to_string()));
150    }
151    let full_path = path
152      .normalize()
153      .map_err(|e| Error::ReadFile(path.clone(), e))?
154      .into_path_buf();
155
156    let parent_dir = full_path
157      .parent()
158      .map_or_else(|| PathBuf::from("/"), |v| v.to_path_buf());
159
160    if config.metadata().is_none() {
161      return Err(Error::NoMetadata(path.to_string_lossy().to_string()));
162    }
163
164    let annotations = metadata_to_annotations(config.metadata().unwrap());
165    let kind = config.kind();
166    let name = config.name().ok_or(Error::NoName)?;
167    let media_type = match kind {
168      wick_config::config::ConfigurationKind::App => media_types::APPLICATION,
169      wick_config::config::ConfigurationKind::Component => media_types::COMPONENT,
170      wick_config::config::ConfigurationKind::Types => media_types::TYPES,
171      wick_config::config::ConfigurationKind::Tests => unreachable!(),
172      wick_config::config::ConfigurationKind::Lockdown => unreachable!(),
173    };
174    let registry = config.package().and_then(|package| package.registry().cloned());
175
176    let (version, extra_files) = match &config {
177      WickConfiguration::App(config) => {
178        let version = config.version();
179
180        let extra_files = config.package_files().to_owned();
181
182        (version, extra_files)
183      }
184      WickConfiguration::Component(config) => {
185        let version = config.version();
186
187        let extra_files = config.package_files().map_or_else(Vec::new, |files| files.to_owned());
188
189        (version, extra_files)
190      }
191      WickConfiguration::Types(config) => {
192        let version = config.version();
193
194        let extra_files = config.package_files().map_or_else(Vec::new, |files| files.to_owned());
195
196        (version, extra_files)
197      }
198      _ => return Err(Error::InvalidWickConfig(path.to_string_lossy().to_string())),
199    };
200
201    let assets = config.assets();
202    let mut wick_files: Vec<PackageFile> = Vec::new();
203
204    let root_bytes = fs::read(&path).await.map_err(|e| Error::ReadFile(path.clone(), e))?;
205    let root_hash = format!("sha256:{}", digest(root_bytes.as_slice()));
206
207    let root_file = PackageFile::new(
208      PathBuf::from(path.file_name().unwrap()),
209      root_hash,
210      media_type.to_owned(),
211      root_bytes.into(),
212    );
213
214    let root_file_path = path.file_name().unwrap().to_string_lossy().to_string();
215    wick_files.push(root_file);
216
217    //if length of extra_files is greater than 0, then we need create a tar of all the files
218    //and add it to the files list.
219    if !extra_files.is_empty() {
220      let gz_bytes = create_tar_gz(extra_files, &parent_dir).await?;
221
222      let tar_hash = format!("sha256:{}", digest(gz_bytes.as_slice()));
223      let tar_file = PackageFile::new(
224        PathBuf::from("extra_files.tar.gz"),
225        tar_hash,
226        media_types::TARGZ.to_owned(),
227        gz_bytes.into(),
228      );
229      wick_files.push(tar_file);
230    }
231
232    //populate wick_files
233    let (_, return_assets) = process_assets(Default::default(), assets, parent_dir.clone(), parent_dir.clone()).await?;
234    //merge return assets  vector to wick_files
235    wick_files.extend(return_assets);
236    trace!(files = ?wick_files.iter().map(|f| f.package_path()).collect::<Vec<_>>(),
237      "package files"
238    );
239
240    Ok(Self {
241      kind,
242      name: name.to_owned(),
243      version: version.map(|v| v.to_owned()).ok_or(Error::NoVersion)?,
244      files: wick_files,
245      annotations,
246      absolute_path: full_path,
247      registry,
248      root: root_file_path,
249      basedir,
250    })
251  }
252
253  #[must_use]
254  /// Returns a list of the files contained within the WickPackage.
255  pub fn list_files(&self) -> Vec<&PackageFile> {
256    self.files.iter().collect()
257  }
258
259  #[must_use]
260  /// Return the base directory of the WickPackage if it came from the filesystem.
261  pub const fn basedir(&self) -> Option<&PathBuf> {
262    self.basedir.as_ref()
263  }
264
265  #[must_use]
266  /// Returns a list of the files contained within the WickPackage.
267  pub const fn path(&self) -> &PathBuf {
268    &self.absolute_path
269  }
270
271  #[must_use]
272  /// Returns the reference.
273  pub fn registry_reference(&self) -> Option<String> {
274    self
275      .registry
276      .as_ref()
277      .map(|r| format!("{}/{}/{}:{}", r.host(), r.namespace(), self.name, self.version))
278  }
279
280  #[must_use]
281  /// Returns an OCI URL with the specified tag.
282  pub fn tagged_reference(&self, tag: &str) -> Option<String> {
283    self
284      .registry
285      .as_ref()
286      .map(|r| format!("{}/{}/{}:{}", r.host(), r.namespace(), self.name, tag))
287  }
288
289  #[must_use]
290  /// Returns the registry configuration.
291  pub const fn registry(&self) -> Option<&RegistryConfig> {
292    self.registry.as_ref()
293  }
294
295  #[must_use]
296  /// Returns a mutable reference to registry configuration.
297  pub fn registry_mut(&mut self) -> Option<&mut RegistryConfig> {
298    self.registry.as_mut()
299  }
300
301  /// Pushes the WickPackage to a specified registry using the provided reference, username, and password.
302  ///
303  /// The username and password are optional. If not provided, the function falls back to anonymous authentication.
304  pub async fn push(&mut self, reference: &str, options: &OciOptions) -> Result<String, Error> {
305    let kind = match self.kind {
306      wick_config::config::ConfigurationKind::App => wick_oci_utils::WickPackageKind::APPLICATION,
307      wick_config::config::ConfigurationKind::Component => wick_oci_utils::WickPackageKind::COMPONENT,
308      wick_config::config::ConfigurationKind::Types => wick_oci_utils::WickPackageKind::TYPES,
309      _ => {
310        return Err(Error::InvalidWickConfig(reference.to_owned()));
311      }
312    };
313    let config = wick_oci_utils::WickOciConfig::new(kind, self.root.clone());
314    let image_config_contents = serde_json::to_string(&config).unwrap();
315    let files = self.files.drain(..).collect();
316
317    let push_response = wick_oci_utils::package::push(
318      reference,
319      image_config_contents,
320      files,
321      self.annotations.clone(),
322      options,
323    )
324    .await?;
325
326    Ok(push_response.manifest_url)
327  }
328
329  /// This function pulls a WickPackage from a specified registry using the provided reference, username, and password.
330  pub async fn pull(reference: &str, options: &OciOptions) -> Result<Self, Error> {
331    let result = wick_oci_utils::package::pull(reference, options).await?;
332
333    let package = Self::from_path(Some(result.base_dir), &result.root_path).await;
334
335    match package {
336      Ok(package) => Ok(package),
337      Err(e) => Err(Error::PackageReadFailed(e.to_string())),
338    }
339  }
340}