Skip to content

Commit

Permalink
feat: create methods to manage runtimes (#74)
Browse files Browse the repository at this point in the history
* feat: create methods to manage runtimes. Refactor common structs and modules

* feat: upgrade TOML to 0.6.0 and allow serializing metadata

* fix: store temporary files (.wws) in the project folder

* fix: bubble config loading error and remove the Display impl
  • Loading branch information
Angelmmiguel authored Jan 26, 2023
1 parent d59d0f9 commit c1b1408
Show file tree
Hide file tree
Showing 19 changed files with 848 additions and 245 deletions.
529 changes: 518 additions & 11 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,13 @@ env_logger = "0.9.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.85"
wax = "0.5.0"
toml = "0.5.9"
toml = "0.6.0"
base64 = "0.13.1"
clap = { version = "4.0.10", features = ["derive"] }
regex = "1"
url = "2.3.1"
reqwest = { version = "0.11" }
sha256 = "1.1.1"

[workspace]
members = [
Expand Down
11 changes: 6 additions & 5 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
// SPDX-License-Identifier: Apache-2.0

use crate::data::kv::KVConfigData;
use anyhow::{anyhow, Result};
use serde::{Deserialize, Deserializer};
use std::collections::HashMap;
use std::path::PathBuf;
use std::{env, fs};
use toml::from_slice;
use toml::from_str;

/// Workers configuration. These files are optional when no configuration change is required.
#[derive(Deserialize, Clone)]
Expand Down Expand Up @@ -45,14 +46,14 @@ impl Config {
/// [data.kv]
/// namespace = "todos"
/// ```
pub fn try_from_file(path: PathBuf) -> Result<Self, String> {
let contents = fs::read(&path).expect("The configuration file was not properly loaded");
pub fn try_from_file(path: PathBuf) -> Result<Self> {
let contents = fs::read_to_string(&path)?;

let try_config: Result<Config, toml::de::Error> = from_slice(&contents);
let try_config: Result<Config, toml::de::Error> = from_str(&contents);

match try_config {
Ok(c) => Ok(c),
Err(err) => Err(format!(
Err(err) => Err(anyhow!(
"Error reading the configuration file at {}: {}",
&path.to_str().unwrap_or("?"),
err
Expand Down
26 changes: 26 additions & 0 deletions src/fetch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright 2022 VMware, Inc.
// SPDX-License-Identifier: Apache-2.0

use crate::runtimes::metadata::Checksum;
use anyhow::Result;

// TODO: Remove it when implementing the manager
#[allow(dead_code)]
/// Fetch the contents of a given file and validates it
/// using the Sha256.
pub async fn fetch<T: AsRef<str>>(file: T) -> Result<Vec<u8>> {
let body: Vec<u8> = reqwest::get(file.as_ref()).await?.bytes().await?.into();

Ok(body)
}

// TODO: Remove it when implementing the manager
#[allow(dead_code)]
/// Fetch the contents of a given file and validates it
/// using the Sha256.
pub async fn fetch_and_validate<T: AsRef<str>>(file: T, checksum: &Checksum) -> Result<Vec<u8>> {
let body: Vec<u8> = fetch(file).await?;
checksum.validate(&body)?;

Ok(body)
}
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ extern crate lazy_static;

mod config;
mod data;
mod fetch;
mod router;
mod runtimes;
mod store;
mod workers;

use actix_files::{Files, NamedFile};
Expand Down
2 changes: 1 addition & 1 deletion src/router/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ impl Route {
///
/// This method also initializes the Runner and loads the Config if available.
pub fn new(base_path: &Path, filepath: PathBuf, prefix: &str) -> Self {
let worker = Worker::new(&filepath).unwrap();
let worker = Worker::new(base_path, &filepath).unwrap();

// Load configuration
let mut config_path = filepath.clone();
Expand Down
69 changes: 69 additions & 0 deletions src/runtimes/manager.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//// Copyright 2022 VMware, Inc.
// SPDX-License-Identifier: Apache-2.0

use super::{
metadata::{RemoteFile, Runtime as RuntimeMetadata},
modules::{javascript::JavaScriptRuntime, native::NativeRuntime},
runtime::Runtime,
};
use crate::{fetch::fetch_and_validate, store::Store};
use anyhow::{anyhow, Result};
use std::path::Path;

// A collection of methods to manage runtimes

/// Initializes a runtime based on the file extension. In the future,
/// This will contain a more complete struct that will identify local
/// runtimes.
pub fn init_runtime(project_root: &Path, path: &Path) -> Result<Box<dyn Runtime + Sync + Send>> {
if let Some(ext) = path.extension() {
let ext_as_str = ext.to_str().unwrap();

match ext_as_str {
"js" => Ok(Box::new(JavaScriptRuntime::new(
project_root,
path.to_path_buf(),
)?)),
"wasm" => Ok(Box::new(NativeRuntime::new(path.to_path_buf()))),
_ => Err(anyhow!(format!(
"The '{}' extension does not have an associated runtime",
ext_as_str
))),
}
} else {
Err(anyhow!("The given file does not have a valid extension"))
}
}

// TODO: Remove it when implementing the full logic
#[allow(dead_code)]
// Install a given runtime based on its metadata
pub async fn install_runtime(
project_root: &Path,
repository: &str,
metadata: &RuntimeMetadata,
) -> Result<()> {
let store = Store::new(
project_root,
&["runtimes", repository, &metadata.name, &metadata.version],
)?;

// Install the different files
download_file(&metadata.binary, &store).await?;

if let Some(polyfill) = &metadata.polyfill {
download_file(polyfill, &store).await?;
}

if let Some(template) = &metadata.template {
download_file(template, &store).await?;
}

Ok(())
}

// TODO: Remove it when implementing the full logic
async fn download_file(file: &RemoteFile, store: &Store) -> Result<()> {
let contents = fetch_and_validate(&file.url, &file.checksum).await?;
store.write(&[&file.filename], &contents)
}
141 changes: 115 additions & 26 deletions src/runtimes/metadata.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,50 @@
// Copyright 2022 VMware, Inc.
// SPDX-License-Identifier: Apache-2.0

use super::{remote_file::RemoteFile, runtime::RuntimeStatus};
use crate::fetch::fetch;
use anyhow::{anyhow, Result};
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use sha256::digest as sha256_digest;
use std::collections::HashMap;
use url::Url;

/// A Repository contains the list of runtimes available on it.
/// This file is used by wws to properly show the list of available
/// repos and install them.
///
/// By default, this repository class rely on the
/// [WebAssembly Language Runtimes](https://github.com/vmware-labs/webassembly-language-runtimes)
/// repository. It looks for a repository.toml file in the Git repo.
#[derive(Deserialize)]
pub struct Repository {
/// Version of the repository file
pub version: u32,
/// The list of runtimes available in the repository
pub runtimes: Vec<Runtime>,
}

// TODO: Remove it when implementing the manager
#[allow(dead_code)]
impl Repository {
/// Reads and parses the metadata from a slice of bytes. It will return
/// a result as the deserialization may fail.
pub fn from_str(data: &str) -> Result<Self> {
toml::from_str::<Repository>(data).map_err(|err| {
println!("Err: {:?}", err);
anyhow!("wws could not deserialize the repository metadata")
})
}

/// Retrieve a repository from a remote file. It will download the content
/// using reqwest and initializing the repository with it.
pub async fn from_remote_file(repository_url: &str) -> Result<Self> {
let url = Url::parse(repository_url)?;
let data = fetch(&url).await?;
let str_data = String::from_utf8(data)?;

Repository::from_str(&str_data)
}
}

// TODO: Remove it when implementing the manager
#[allow(dead_code)]
Expand All @@ -16,53 +56,102 @@ use std::collections::HashMap;
/// a source code as a worker. The configuration includes
/// different pieces like polyfills files, templates,
/// arguments, etc.
#[derive(Deserialize)]
pub struct RuntimeMetadata<'a> {
#[derive(Deserialize, Serialize, Clone)]
pub struct Runtime {
/// Name of the runtime (like ruby, python, etc)
name: &'a str,
pub name: String,
/// Specific version of the runtime
version: &'a str,
pub version: String,
/// Current status in the repository
status: RuntimeStatus,
pub status: RuntimeStatus,
/// Associated extensions
extensions: Vec<&'a str>,
pub extensions: Vec<String>,
/// Arguments to pass to the Wasm module via WASI
args: Vec<&'a str>,
pub args: Vec<String>,
/// A list of environment variables that must be configured
/// for the runtime to work.
envs: Option<HashMap<&'a str, &'a str>>,
pub envs: Option<HashMap<String, String>>,
/// The reference to a remote binary (url + checksum)
binary: RemoteFile<'a>,
pub binary: RemoteFile,
/// The reference to a remote polyfill file (url + checksum)
polyfill: Option<RemoteFile<'a>>,
/// The refernmece to a template file for the worker. It will wrap the
pub polyfill: Option<RemoteFile>,
/// The reference to a template file for the worker. It will wrap the
/// source code into a template that can include imports,
/// function calls, etc.
template: Option<RemoteFile<'a>>,
pub template: Option<RemoteFile>,
}

// TODO: Remove it when implementing the manager
#[allow(dead_code)]
impl<'a> RuntimeMetadata<'a> {
/// Reads and parses the metadata from a slice of bytes. It will return
/// a result as the deserialization may fail.
pub fn from_slice(data: &'a [u8]) -> Result<Self> {
toml::from_slice::<RuntimeMetadata>(data)
.map_err(|_| anyhow!("wws could not deserialize the runtime metadata"))
/// Define the status of a runtime in a target repository
#[derive(Deserialize, Serialize, Clone)]
#[serde(rename_all = "lowercase")]
pub enum RuntimeStatus {
Active,
Yanked,
Deprecated,
Unknown,
}

impl From<&str> for RuntimeStatus {
/// Create a RuntimeStatus variant from a &str. It uses predefined
/// values
fn from(value: &str) -> Self {
match value {
"active" => RuntimeStatus::Active,
"yanked" => RuntimeStatus::Yanked,
"deprecated" => RuntimeStatus::Deprecated,
_ => RuntimeStatus::Unknown,
}
}
}

/// A file represents a combination of both a remote URL, filename
/// and checksum.
#[derive(Deserialize, Serialize, Clone)]
pub struct RemoteFile {
/// URL pointing to the file
pub url: String,
/// Checksum to validate the given file
pub checksum: Checksum,
/// Provide a filename
pub filename: String,
}

/// A list of available checksums. For now, we will support only sha256
#[derive(Deserialize, Serialize, Clone)]
#[serde(rename_all = "lowercase", tag = "type")]
pub enum Checksum {
Sha256 { value: String },
}

impl Checksum {
/// Validate the provided slice of bytes with the given checksum.
/// Depending on the type, it will calculate a different digest.
pub fn validate(&self, bytes: &[u8]) -> Result<()> {
match self {
Checksum::Sha256 { value } if value == &sha256_digest(bytes) => Ok(()),
_ => Err(anyhow!("The checksums dont not match")),
}
}
}

#[cfg(test)]
mod tests {
use crate::runtimes::remote_file::Checksum;

use super::*;
use std::{any::Any, fs};

#[test]
fn parse_index_toml() {
let contents = fs::read_to_string("tests/data/metadata/repository.toml").unwrap();
let repo = Repository::from_str(&contents).unwrap();

assert_eq!(repo.version, 1);
assert_eq!(repo.runtimes.len(), 1);
}

#[test]
fn parse_runtime_toml() {
let contents = fs::read("tests/data/metadata/runtime.toml").unwrap();
let metadata = RuntimeMetadata::from_slice(&contents).unwrap();
let contents = fs::read_to_string("tests/data/metadata/runtime.toml").unwrap();
let metadata = toml::from_str::<Runtime>(&contents).unwrap();

assert_eq!(metadata.name, "ruby");
assert_eq!(metadata.version, "3.2.0+20230118-8aec06d");
Expand Down
5 changes: 2 additions & 3 deletions src/runtimes/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
// Copyright 2022 VMware, Inc.
// SPDX-License-Identifier: Apache-2.0

mod metadata;
pub mod manager;
pub mod metadata;
mod modules;
mod remote_file;
mod repository;
pub mod runtime;
Loading

0 comments on commit c1b1408

Please sign in to comment.