deploy flow dx

This commit is contained in:
Edoardo Marangoni
2024-04-19 19:04:34 +02:00
parent 6eacaaa9d2
commit 79e961532d
27 changed files with 992 additions and 1330 deletions

14
Cargo.lock generated
View File

@@ -1413,10 +1413,11 @@ dependencies = [
[[package]] [[package]]
name = "edge-schema" name = "edge-schema"
version = "0.0.3" version = "0.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "183ddfb52c2441be9d8c3c870632135980ba98e0c4f688da11bcbebb4e26f128"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytesize", "bytesize",
"hex",
"once_cell", "once_cell",
"parking_lot 0.12.1", "parking_lot 0.12.1",
"rand_chacha", "rand_chacha",
@@ -1436,11 +1437,10 @@ dependencies = [
[[package]] [[package]]
name = "edge-schema" name = "edge-schema"
version = "0.1.0" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0966f1fd49610cc67a835124e6fb4d00a36104e1aa34383c5ef5a265ca00ea2a"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytesize", "bytesize",
"hex",
"once_cell", "once_cell",
"parking_lot 0.12.1", "parking_lot 0.12.1",
"rand_chacha", "rand_chacha",
@@ -2228,7 +2228,7 @@ dependencies = [
"httpdate", "httpdate",
"itoa", "itoa",
"pin-project-lite", "pin-project-lite",
"socket2 0.5.6", "socket2 0.4.10",
"tokio 1.37.0", "tokio 1.37.0",
"tower-service", "tower-service",
"tracing", "tracing",
@@ -5587,7 +5587,7 @@ version = "1.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 0.1.10",
"static_assertions", "static_assertions",
] ]
@@ -6253,6 +6253,7 @@ dependencies = [
"tracing", "tracing",
"url", "url",
"uuid", "uuid",
"wasmer-config 0.1.0",
"webc", "webc",
] ]
@@ -6412,7 +6413,7 @@ dependencies = [
"semver 1.0.22", "semver 1.0.22",
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml 0.8.26", "serde_yaml 0.9.34+deprecated",
"sha2", "sha2",
"shared-buffer", "shared-buffer",
"tar", "tar",
@@ -6783,6 +6784,7 @@ dependencies = [
"wasmer-config 0.1.0", "wasmer-config 0.1.0",
"wasmer-wasm-interface", "wasmer-wasm-interface",
"wasmparser 0.121.2", "wasmparser 0.121.2",
"webc",
"whoami", "whoami",
] ]

View File

@@ -98,6 +98,7 @@ rkyv = { version = "0.7.40", features = ["indexmap", "validation", "strict"] }
memmap2 = { version = "0.6.2" } memmap2 = { version = "0.6.2" }
edge-schema = { version = "=0.1.0" } edge-schema = { version = "=0.1.0" }
indexmap = "1" indexmap = "1"
serde_yaml = "0.9.0"
[build-dependencies] [build-dependencies]
test-generator = { path = "tests/lib/test-generator" } test-generator = { path = "tests/lib/test-generator" }

View File

@@ -17,6 +17,7 @@ rust-version.workspace = true
[dependencies] [dependencies]
# Wasmer dependencies. # Wasmer dependencies.
edge-schema.workspace = true edge-schema.workspace = true
wasmer-config.workspace = true
webc = "5" webc = "5"
# crates.io dependencies. # crates.io dependencies.

View File

@@ -2,11 +2,12 @@ use std::{collections::HashSet, pin::Pin, time::Duration};
use anyhow::{bail, Context}; use anyhow::{bail, Context};
use cynic::{MutationBuilder, QueryBuilder}; use cynic::{MutationBuilder, QueryBuilder};
use edge_schema::schema::{NetworkTokenV1, PackageIdentifier}; use edge_schema::schema::NetworkTokenV1;
use futures::{Stream, StreamExt}; use futures::{Stream, StreamExt};
use time::OffsetDateTime; use time::OffsetDateTime;
use tracing::Instrument; use tracing::Instrument;
use url::Url; use url::Url;
use wasmer_config::package::PackageIdent;
use crate::{ use crate::{
types::{ types::{
@@ -24,10 +25,21 @@ use crate::{
/// the API, and should not be used where possible. /// the API, and should not be used where possible.
pub async fn fetch_webc_package( pub async fn fetch_webc_package(
client: &WasmerClient, client: &WasmerClient,
ident: &PackageIdentifier, ident: &PackageIdent,
default_registry: &Url, default_registry: &Url,
) -> Result<webc::compat::Container, anyhow::Error> { ) -> Result<webc::compat::Container, anyhow::Error> {
let url = ident.build_download_url_with_default_registry(default_registry); let url = match ident {
PackageIdent::Named(n) => Url::parse(&format!(
"{default_registry}/{}:{}",
n.full_name(),
n.version_or_default()
))?,
PackageIdent::Hash(h) => match get_package_release(client, &h.to_string()).await? {
Some(webc) => Url::parse(&webc.webc_url)?,
None => anyhow::bail!("Could not find package with hash '{}'", h),
},
};
let data = client let data = client
.client .client
.get(url) .get(url)

View File

@@ -166,7 +166,7 @@ interfaces = { version = "0.0.9", optional = true }
uuid = { version = "1.3.0", features = ["v4"] } uuid = { version = "1.3.0", features = ["v4"] }
time = { version = "0.3.17", features = ["macros"] } time = { version = "0.3.17", features = ["macros"] }
serde_yaml = "0.8.26" serde_yaml = {workspace = true}
comfy-table = "7.0.1" comfy-table = "7.0.1"

View File

@@ -1,34 +1,46 @@
//! Create a new Edge app. //! Create a new Edge app.
use std::{path::PathBuf, str::FromStr};
use anyhow::{bail, Context}; use anyhow::{bail, Context};
use clap::Parser;
use colored::Colorize; use colored::Colorize;
use dialoguer::Confirm; use dialoguer::Confirm;
use edge_schema::schema::PackageIdentifier; use indicatif::ProgressBar;
use is_terminal::IsTerminal; use is_terminal::IsTerminal;
use std::{path::PathBuf, str::FromStr, time::Duration};
use wasmer_api::{ use wasmer_api::{
query::current_user_with_namespaces,
types::{DeployAppVersion, Package, UserWithNamespaces}, types::{DeployAppVersion, Package, UserWithNamespaces},
WasmerClient, WasmerClient,
}; };
use wasmer_config::{
app::AppConfigV1,
package::{Manifest, NamedPackageIdent, PackageIdent, PackageSource, MANIFEST_FILE_NAME},
};
use wasmer_registry::wasmer_env::WasmerEnv;
const TICK: Duration = Duration::from_millis(250);
use crate::{ use crate::{
commands::{ commands::{
app::{deploy_app_verbose, AppConfigV1, DeployAppOpts, WaitMode}, app::deploy::{deploy_app_verbose, DeployAppOpts, WaitMode},
AsyncCliCommand, AsyncCliCommand, Login,
}, },
opts::{ApiOpts, ItemFormatOpts}, opts::{ApiOpts, ItemFormatOpts},
utils::package_wizard::{CreateMode, PackageType, PackageWizard}, utils::{
package_wizard::{CreateMode, PackageType, PackageWizard},
prompts::prompt_for_namespace,
},
}; };
/// Create a new Edge app. /// Create a new Edge app.
#[derive(clap::Parser, Debug)] #[derive(clap::Parser, Debug)]
pub struct CmdAppCreate { pub struct CmdAppCreate {
#[clap(name = "type", short = 't', long)] #[clap(name = "type", short = 't', long)]
template: Option<AppType>, pub template: Option<AppType>,
#[clap(long)] #[clap(long)]
publish_package: bool, pub publish_package: bool,
/// Skip local schema validation. /// Skip local schema validation.
#[clap(long)] #[clap(long)]
@@ -46,10 +58,6 @@ pub struct CmdAppCreate {
#[clap(long)] #[clap(long)]
pub owner: Option<String>, pub owner: Option<String>,
/// Name to use when creating a new package.
#[clap(long)]
pub new_package_name: Option<String>,
/// The name of the app (can be changed later) /// The name of the app (can be changed later)
#[clap(long)] #[clap(long)]
pub name: Option<String>, pub name: Option<String>,
@@ -117,252 +125,256 @@ struct AppCreatorOutput {
impl AppCreator { impl AppCreator {
async fn build_browser_shell_app(self) -> Result<AppCreatorOutput, anyhow::Error> { async fn build_browser_shell_app(self) -> Result<AppCreatorOutput, anyhow::Error> {
const WASM_BROWSER_CONTAINER_PACKAGE: &str = "wasmer/wasmer-sh"; todo!()
const WASM_BROWSER_CONTAINER_VERSION: &str = "0.2"; // const WASM_BROWSER_CONTAINER_PACKAGE: &str = "wasmer/wasmer-sh";
// const WASM_BROWSER_CONTAINER_VERSION: &str = "0.2";
eprintln!("A browser web shell wraps another package and runs it in the browser"); // eprintln!("A browser web shell wraps another package and runs it in the browser");
eprintln!("Select the package to wrap."); // eprintln!("Select the package to wrap.");
let (inner_pkg, _inner_pkg_api) = crate::utils::prompt_for_package( // let (inner_pkg, _inner_pkg_api) = crate::utils::prompt_for_package(
"Package", // "Package",
None, // None,
Some(crate::utils::PackageCheckMode::MustExist), // Some(crate::utils::PackageCheckMode::MustExist),
self.api.as_ref(), // self.api.as_ref(),
) // )
.await?; // .await?;
eprintln!("What should be the name of the wrapper package?"); // eprintln!("What should be the name of the wrapper package?");
let default_name = format!("{}-webshell", inner_pkg.name); // let default_name = format!("{}-webshell", inner_pkg.name);
let outer_pkg_name = // let outer_pkg_name =
crate::utils::prompts::prompt_for_ident("Package name", Some(&default_name))?; // crate::utils::prompts::prompt_for_ident("Package name", Some(&default_name))?;
let outer_pkg_full_name = format!("{}/{}", self.owner, outer_pkg_name); // let outer_pkg_full_name = format!("{}/{}", self.owner, outer_pkg_name);
eprintln!("What should be the name of the app?"); // eprintln!("What should be the name of the app?");
let default_name = if outer_pkg_name.ends_with("webshell") { // let default_name = if outer_pkg_name.ends_with("webshell") {
format!("{}-{}", self.owner, outer_pkg_name) // format!("{}-{}", self.owner, outer_pkg_name)
} else { // } else {
format!("{}-{}-webshell", self.owner, outer_pkg_name) // format!("{}-{}-webshell", self.owner, outer_pkg_name)
}; // };
let app_name = crate::utils::prompts::prompt_for_ident("App name", Some(&default_name))?; // let app_name = crate::utils::prompts::prompt_for_ident("App name", Some(&default_name))?;
// Build the package. // // Build the package.
let public_dir = self.dir.join("public"); // let public_dir = self.dir.join("public");
if !public_dir.exists() { // if !public_dir.exists() {
std::fs::create_dir_all(&public_dir)?; // std::fs::create_dir_all(&public_dir)?;
} // }
let init = serde_json::json!({ // let init = serde_json::json!({
"init": format!("{}/{}", inner_pkg.namespace, inner_pkg.name), // "init": format!("{}/{}", inner_pkg.namespace.unwrap(), inner_pkg.name),
"prompt": inner_pkg.name, // "prompt": inner_pkg.name,
"no_welcome": true, // "no_welcome": true,
"connect": format!("wss://{app_name}.wasmer.app/.well-known/edge-vpn"), // "connect": format!("wss://{app_name}.wasmer.app/.well-known/edge-vpn"),
}); // });
let init_path = public_dir.join("init.json"); // let init_path = public_dir.join("init.json");
std::fs::write(&init_path, init.to_string()) // std::fs::write(&init_path, init.to_string())
.with_context(|| format!("Failed to write to '{}'", init_path.display()))?; // .with_context(|| format!("Failed to write to '{}'", init_path.display()))?;
let package = wasmer_config::package::PackageBuilder::new( // let package = wasmer_config::package::PackageBuilder::new(
outer_pkg_full_name, // outer_pkg_full_name,
"0.1.0".parse().unwrap(), // "0.1.0".parse().unwrap(),
format!("{} web shell", inner_pkg.name), // format!("{} web shell", inner_pkg.name),
) // )
.rename_commands_to_raw_command_name(false) // .rename_commands_to_raw_command_name(false)
.build()?; // .build()?;
let manifest = wasmer_config::package::ManifestBuilder::new(package) // let manifest = wasmer_config::package::ManifestBuilder::new(package)
.with_dependency( // .with_dependency(
WASM_BROWSER_CONTAINER_PACKAGE, // WASM_BROWSER_CONTAINER_PACKAGE,
WASM_BROWSER_CONTAINER_VERSION.to_string().parse().unwrap(), // WASM_BROWSER_CONTAINER_VERSION.to_string().parse().unwrap(),
) // )
.map_fs("public", PathBuf::from("public")) // .map_fs("public", PathBuf::from("public"))
.build()?; // .build()?;
let manifest_path = self.dir.join("wasmer.toml"); // let manifest_path = self.dir.join("wasmer.toml");
let raw = manifest.to_string()?; // let raw = manifest.to_string()?;
eprintln!( // eprintln!(
"Writing wasmer.toml package to '{}'", // "Writing wasmer.toml package to '{}'",
manifest_path.display() // manifest_path.display()
); // );
std::fs::write(&manifest_path, raw)?; // std::fs::write(&manifest_path, raw)?;
let app_cfg = AppConfigV1 { // let app_cfg = AppConfigV1 {
app_id: None, // app_id: None,
name: app_name, // name: app_name,
owner: Some(self.owner.clone()), // owner: Some(self.owner.clone()),
cli_args: None, // cli_args: None,
env: Default::default(), // env: Default::default(),
volumes: None, // volumes: None,
domains: None, // domains: None,
scaling: None, // scaling: None,
package: edge_schema::schema::PackageIdentifier { // package: NamedPackageIdent {
repository: None, // registry: None,
namespace: self.owner, // namespace: Some(self.owner),
name: outer_pkg_name, // name: outer_pkg_name,
tag: None, // tag: None,
} // }
.into(), // .into(),
capabilities: None, // capabilities: None,
scheduled_tasks: None, // scheduled_tasks: None,
debug: Some(false), // debug: Some(false),
extra: Default::default(), // extra: Default::default(),
health_checks: None, // health_checks: None,
}; // };
Ok(AppCreatorOutput { // Ok(AppCreatorOutput {
app: app_cfg, // app: app_cfg,
api_pkg: None, // api_pkg: None,
local_package: Some((self.dir, manifest)), // local_package: Some((self.dir, manifest)),
}) // })
} }
async fn build_app(self) -> Result<AppCreatorOutput, anyhow::Error> { async fn build_app(self) -> Result<AppCreatorOutput, anyhow::Error> {
let package_opt: Option<PackageIdentifier> = if let Some(package) = self.package { todo!()
Some(package.parse()?) // let package_opt: Option<PackageIdent> = if let Some(package) = self.package {
} else if let Some((_, local)) = self.local_package.as_ref() { // Some(package.parse()?)
let full = format!( // } else if let Some((_, local)) = self.local_package.as_ref() {
"{}@{}", // let full = format!(
local.package.clone().unwrap().name, // "{}@{}",
local.package.clone().unwrap().version // local.package.clone().unwrap().name,
); // local.package.clone().unwrap().version
let mut pkg_ident = PackageIdentifier::from_str(&local.package.clone().unwrap().name) // );
.with_context(|| { // let mut pkg_ident = NamedPackageIdent::from_str(&local.package.clone().unwrap().name)
format!("local package manifest has invalid name: '{full}'") // .with_context(|| {
})?; // format!("local package manifest has invalid name: '{full}'")
// })?;
// // pkg
// // Pin the version.
// pkg_ident.tag = Some(wasmer_config::package::Tag::VersionReq(
// local.package.clone().unwrap().version.,
// ));
// Pin the version. // if self.interactive {
pkg_ident.tag = Some(local.package.clone().unwrap().version.to_string()); // eprintln!("Found local package: '{}'", full.green());
if self.interactive { // let msg = format!("Use package '{pkg_ident}'");
eprintln!("Found local package: '{}'", full.green());
let msg = format!("Use package '{pkg_ident}'"); // let should_use = Confirm::new()
// .with_prompt(&msg)
// .interact_opt()?
// .unwrap_or_default();
let should_use = Confirm::new() // if should_use {
.with_prompt(&msg) // Some(pkg_ident)
.interact_opt()? // } else {
.unwrap_or_default(); // None
// }
// } else {
// Some(pkg_ident)
// }
// } else {
// None
// };
if should_use { // let (pkg, api_pkg, local_package) = if let Some(pkg) = package_opt {
Some(pkg_ident) // if let Some(api) = &self.api {
} else { // let p2 =
None // wasmer_api::query::get_package(api, format!("{}/{}", pkg.namespace, pkg.name))
} // .await?;
} else {
Some(pkg_ident)
}
} else {
None
};
let (pkg, api_pkg, local_package) = if let Some(pkg) = package_opt { // (pkg.into(), p2, self.local_package)
if let Some(api) = &self.api { // } else {
let p2 = // (pkg.into(), None, self.local_package)
wasmer_api::query::get_package(api, format!("{}/{}", pkg.namespace, pkg.name)) // }
.await?; // } else {
// eprintln!("No package found or specified.");
(pkg.into(), p2, self.local_package) // let ty = match self.type_ {
} else { // AppType::HttpServer => None,
(pkg.into(), None, self.local_package) // AppType::StaticWebsite => Some(PackageType::StaticWebsite),
} // AppType::BrowserShell => None,
} else { // AppType::JsWorker => Some(PackageType::JsWorker),
eprintln!("No package found or specified."); // AppType::PyApplication => Some(PackageType::PyApplication),
// };
let ty = match self.type_ { // let create_mode = match ty {
AppType::HttpServer => None, // Some(PackageType::StaticWebsite)
AppType::StaticWebsite => Some(PackageType::StaticWebsite), // | Some(PackageType::JsWorker)
AppType::BrowserShell => None, // | Some(PackageType::PyApplication) => CreateMode::Create,
AppType::JsWorker => Some(PackageType::JsWorker), // // Only static website creation is currently supported.
AppType::PyApplication => Some(PackageType::PyApplication), // _ => CreateMode::SelectExisting,
}; // };
let create_mode = match ty { // let w = PackageWizard {
Some(PackageType::StaticWebsite) // path: self.dir.clone(),
| Some(PackageType::JsWorker) // name: self.new_package_name.clone(),
| Some(PackageType::PyApplication) => CreateMode::Create, // type_: ty,
// Only static website creation is currently supported. // create_mode,
_ => CreateMode::SelectExisting, // namespace: Some(self.owner.clone()),
}; // namespace_default: self.user.as_ref().map(|u| u.username.clone()),
// user: self.user.clone(),
// };
let w = PackageWizard { // let output = w.run(self.api.as_ref()).await?;
path: self.dir.clone(), // (
name: self.new_package_name.clone(), // output.ident,
type_: ty, // output.api,
create_mode, // output
namespace: Some(self.owner.clone()), // .local_path
namespace_default: self.user.as_ref().map(|u| u.username.clone()), // .and_then(move |x| Some((x, output.local_manifest?))),
user: self.user.clone(), // )
}; // };
let output = w.run(self.api.as_ref()).await?; // let ident = pkg.as_ident().context("unnamed packages not supported")?;
(
output.ident,
output.api,
output
.local_path
.and_then(move |x| Some((x, output.local_manifest?))),
)
};
let ident = pkg.as_ident().context("unnamed packages not supported")?; // let name = if let Some(name) = self.app_name {
// name
// } else {
// let default = match self.type_ {
// AppType::HttpServer | AppType::StaticWebsite => {
// format!("{}-{}", ident.namespace, ident.name)
// }
// AppType::JsWorker | AppType::PyApplication => {
// format!("{}-{}-worker", ident.namespace, ident.name)
// }
// AppType::BrowserShell => {
// format!("{}-{}-webshell", ident.namespace, ident.name)
// }
// };
let name = if let Some(name) = self.app_name { // dialoguer::Input::new()
name // .with_prompt("What should be the name of the app? <NAME>.wasmer.app")
} else { // .with_initial_text(default)
let default = match self.type_ { // .interact_text()
AppType::HttpServer | AppType::StaticWebsite => { // .unwrap()
format!("{}-{}", ident.namespace, ident.name) // };
}
AppType::JsWorker | AppType::PyApplication => {
format!("{}-{}-worker", ident.namespace, ident.name)
}
AppType::BrowserShell => {
format!("{}-{}-webshell", ident.namespace, ident.name)
}
};
dialoguer::Input::new() // let cli_args = match self.type_ {
.with_prompt("What should be the name of the app? <NAME>.wasmer.app") // AppType::PyApplication => Some(vec!["/src/main.py".to_string()]),
.with_initial_text(default) // AppType::JsWorker => Some(vec!["/src/index.js".to_string()]),
.interact_text() // _ => None,
.unwrap() // };
};
let cli_args = match self.type_ { // // TODO: check if name already exists.
AppType::PyApplication => Some(vec!["/src/main.py".to_string()]), // let cfg = AppConfigV1 {
AppType::JsWorker => Some(vec!["/src/index.js".to_string()]), // app_id: None,
_ => None, // owner: Some(self.owner.clone()),
}; // volumes: None,
// name,
// env: Default::default(),
// scaling: None,
// // CLI args are only set for JS and Py workers for now.
// cli_args,
// // TODO: allow setting the description.
// // description: Some("".to_string()),
// package: pkg.clone(),
// capabilities: None,
// scheduled_tasks: None,
// debug: Some(false),
// domains: None,
// extra: Default::default(),
// health_checks: None,
// };
// TODO: check if name already exists. // Ok(AppCreatorOutput {
let cfg = AppConfigV1 { // app: cfg,
app_id: None, // api_pkg,
owner: Some(self.owner.clone()), // local_package,
volumes: None, // })
name,
env: Default::default(),
scaling: None,
// CLI args are only set for JS and Py workers for now.
cli_args,
// TODO: allow setting the description.
// description: Some("".to_string()),
package: pkg.clone(),
capabilities: None,
scheduled_tasks: None,
debug: Some(false),
domains: None,
extra: Default::default(),
health_checks: None,
};
Ok(AppCreatorOutput {
app: cfg,
api_pkg,
local_package,
})
} }
} }
@@ -371,212 +383,7 @@ impl AsyncCliCommand for CmdAppCreate {
type Output = (AppConfigV1, Option<DeployAppVersion>); type Output = (AppConfigV1, Option<DeployAppVersion>);
async fn run_async(self) -> Result<(AppConfigV1, Option<DeployAppVersion>), anyhow::Error> { async fn run_async(self) -> Result<(AppConfigV1, Option<DeployAppVersion>), anyhow::Error> {
let interactive = self.non_interactive == false && std::io::stdin().is_terminal(); todo!()
let base_path = if let Some(p) = self.path {
p
} else {
std::env::current_dir()?
};
let (base_dir, appcfg_path) = if base_path.is_file() {
let dir = base_path
.canonicalize()?
.parent()
.context("could not determine parent directory")?
.to_owned();
(dir, base_path)
} else if base_path.is_dir() {
let full = base_path.join(AppConfigV1::CANONICAL_FILE_NAME);
(base_path, full)
} else {
bail!("No such file or directory: '{}'", base_path.display());
};
if appcfg_path.is_file() {
bail!(
"App configuration file already exists at '{}'",
appcfg_path.display()
);
}
let api = if self.offline {
None
} else {
Some(self.api.client()?)
};
let user = if let Some(api) = &api {
let u = wasmer_api::query::current_user_with_namespaces(
api,
Some(wasmer_api::types::GrapheneRole::Admin),
)
.await?;
Some(u)
} else {
None
};
let type_ = match self.template {
Some(t) => t,
None => {
if interactive {
let index = dialoguer::Select::new()
.with_prompt("App type")
.default(0)
.items(&[
"Static website",
"HTTP server",
"Browser shell",
"JS Worker (WinterJS)",
"Python Application",
])
.interact()?;
match index {
0 => AppType::StaticWebsite,
1 => AppType::HttpServer,
2 => AppType::BrowserShell,
3 => AppType::JsWorker,
4 => AppType::PyApplication,
x => panic!("unhandled app type index '{x}'"),
}
} else {
bail!("No app type specified: use --type XXX");
}
}
};
let owner = if let Some(owner) = self.owner {
owner
} else if interactive {
crate::utils::prompts::prompt_for_namespace(
"Who should own this package?",
None,
user.as_ref(),
)?
} else {
bail!("No owner specified: use --owner XXX");
};
let allow_local_package = match type_ {
AppType::HttpServer => true,
AppType::StaticWebsite => true,
AppType::BrowserShell => false,
AppType::JsWorker => true,
AppType::PyApplication => true,
};
let local_package = if allow_local_package {
match crate::utils::load_package_manifest(&base_dir) {
Ok(Some(p)) => Some(p),
Ok(None) => None,
Err(err) => {
eprintln!(
"{warning}: could not load package manifest: {err}",
warning = "Warning".yellow(),
);
None
}
}
} else {
None
};
let creator = AppCreator {
app_name: self.name,
new_package_name: self.new_package_name,
package: self.package,
type_,
interactive,
dir: base_dir,
owner: owner.clone(),
api,
user,
local_package,
};
let output = match type_ {
AppType::HttpServer
| AppType::StaticWebsite
| AppType::JsWorker
| AppType::PyApplication => creator.build_app().await?,
AppType::BrowserShell => creator.build_browser_shell_app().await?,
};
let AppCreatorOutput {
app: cfg,
api_pkg,
local_package,
..
} = output;
let deploy_now = if self.offline {
false
} else if self.non_interactive {
true
} else {
Confirm::new()
.with_prompt("Would you like to publish the app now?".to_string())
.interact()?
};
// Make sure to write out the app.yaml to avoid not creating it when the
// publish or deploy step fails.
// (the later flow only writes a new app.yaml after a success)
let raw_app_config = cfg.clone().to_yaml()?;
std::fs::write(&appcfg_path, raw_app_config).with_context(|| {
format!("could not write app config to '{}'", appcfg_path.display())
})?;
let (final_config, app_version) = if deploy_now {
eprintln!("Creating the app...");
let api = self.api.client()?;
if api_pkg.is_none() {
if let Some((path, manifest)) = &local_package {
eprintln!("Publishing package...");
let manifest = manifest.clone();
crate::utils::republish_package(&api, path, manifest, None).await?;
}
}
let raw_config = cfg.clone().to_yaml()?;
std::fs::write(&appcfg_path, raw_config).with_context(|| {
format!("could not write config to '{}'", appcfg_path.display())
})?;
let wait_mode = if self.no_wait {
WaitMode::Deployed
} else {
WaitMode::Reachable
};
let opts = DeployAppOpts {
app: &cfg,
original_config: None,
allow_create: true,
make_default: true,
owner: Some(owner.clone()),
wait: wait_mode,
};
let (_app, app_version) = deploy_app_verbose(&api, opts).await?;
let new_cfg = super::app_config_from_api(&app_version)?;
(new_cfg, Some(app_version))
} else {
(cfg, None)
};
eprintln!("Writing app config to '{}'", appcfg_path.display());
let raw_final_config = final_config.clone().to_yaml()?;
std::fs::write(&appcfg_path, raw_final_config)
.with_context(|| format!("could not write config to '{}'", appcfg_path.display()))?;
eprintln!("To (re)deploy your app, run 'wasmer deploy'");
Ok((final_config, app_version))
} }
} }
@@ -595,13 +402,12 @@ mod tests {
non_interactive: true, non_interactive: true,
offline: true, offline: true,
owner: Some("testuser".to_string()), owner: Some("testuser".to_string()),
new_package_name: Some("static-site-1".to_string()),
name: Some("static-site-1".to_string()), name: Some("static-site-1".to_string()),
path: Some(dir.path().to_owned()), path: Some(dir.path().to_owned()),
no_wait: true, no_wait: true,
api: ApiOpts::default(), api: ApiOpts::default(),
fmt: ItemFormatOpts::default(), fmt: ItemFormatOpts::default(),
package: None, package: Some("testuser/static-site1@0.1.0".to_string()),
}; };
cmd.run_async().await.unwrap(); cmd.run_async().await.unwrap();
@@ -629,7 +435,6 @@ debug: false
non_interactive: true, non_interactive: true,
offline: true, offline: true,
owner: Some("wasmer".to_string()), owner: Some("wasmer".to_string()),
new_package_name: None,
name: Some("testapp".to_string()), name: Some("testapp".to_string()),
path: Some(dir.path().to_owned()), path: Some(dir.path().to_owned()),
no_wait: true, no_wait: true,
@@ -662,7 +467,6 @@ debug: false
non_interactive: true, non_interactive: true,
offline: true, offline: true,
owner: Some("wasmer".to_string()), owner: Some("wasmer".to_string()),
new_package_name: None,
name: Some("test-js-worker".to_string()), name: Some("test-js-worker".to_string()),
path: Some(dir.path().to_owned()), path: Some(dir.path().to_owned()),
no_wait: true, no_wait: true,
@@ -698,7 +502,6 @@ debug: false
non_interactive: true, non_interactive: true,
offline: true, offline: true,
owner: Some("wasmer".to_string()), owner: Some("wasmer".to_string()),
new_package_name: None,
name: Some("test-py-worker".to_string()), name: Some("test-py-worker".to_string()),
path: Some(dir.path().to_owned()), path: Some(dir.path().to_owned()),
no_wait: true, no_wait: true,

View File

@@ -0,0 +1,448 @@
use super::AsyncCliCommand;
use crate::{
commands::{app::create::CmdAppCreate, package, Publish},
opts::{ApiOpts, ItemFormatOpts},
utils::load_package_manifest,
};
use anyhow::Context;
use is_terminal::IsTerminal;
use std::io::Write;
use std::{path::PathBuf, str::FromStr, time::Duration};
use wasmer_api::{
types::{DeployApp, DeployAppVersion},
WasmerClient,
};
use wasmer_config::{
app::AppConfigV1,
package::{PackageIdent, PackageSource},
};
use wasmer_registry::wasmer_env::WasmerEnv;
/// Deploy an app to Wasmer Edge.
#[derive(clap::Parser, Debug)]
pub struct CmdAppDeploy {
#[clap(flatten)]
pub api: ApiOpts,
#[clap(flatten)]
pub fmt: ItemFormatOpts,
/// Skip local schema validation.
#[clap(long)]
pub no_validate: bool,
/// Do not prompt for user input.
#[clap(long)]
pub non_interactive: bool,
/// Automatically publish the package referenced by this app.
///
/// Only works if the corresponding wasmer.toml is in the same directory.
#[clap(long)]
pub publish_package: bool,
/// The path to the app.yaml file.
#[clap(long)]
pub path: Option<PathBuf>,
/// Do not wait for the app to become reachable.
#[clap(long)]
pub no_wait: bool,
/// Do not make the new app version the default (active) version.
/// This is useful for testing a deployment first, before moving it to "production".
#[clap(long)]
pub no_default: bool,
/// Do not persist the app version ID in the app.yaml.
#[clap(long)]
pub no_persist_id: bool,
/// Specify the owner (user or namespace) of the app.
/// Will default to the currently logged in user, or the existing one
/// if the app can be found.
#[clap(long)]
pub owner: Option<String>,
}
impl CmdAppDeploy {
async fn publish(
&self,
owner: String,
manifest_dir_path: PathBuf,
) -> anyhow::Result<PackageIdent> {
let (_, manifest) = match load_package_manifest(&manifest_dir_path)? {
Some(r) => r,
None => anyhow::bail!(
"Could not read or find manifest in path '{}'!",
manifest_dir_path.display()
),
};
let publish_cmd = Publish {
env: WasmerEnv::default(),
dry_run: false,
quiet: false,
package_name: None,
version: None,
no_validate: false,
package_path: Some(manifest_dir_path.to_str().unwrap().to_string()),
wait: !self.no_wait,
wait_all: false,
timeout: humantime::Duration::from_str("2m").unwrap(),
package_namespace: match manifest.package {
Some(_) => None,
None => Some(owner),
},
non_interactive: self.non_interactive,
};
match publish_cmd.run_async().await? {
Some(id) => Ok(id),
None => anyhow::bail!("Error while publishing package. Stopping."),
}
}
async fn get_owner(&self, app: &AppConfigV1) -> anyhow::Result<String> {
if let Some(owner) = &app.owner {
return Ok(owner.clone());
}
if !(std::io::stdin().is_terminal() && !self.non_interactive) {
// if not interactive we can't prompt the user to choose the owner of the app.
anyhow::bail!("No owner specified: use --owner XXX");
}
match self.api.client() {
Ok(client) => {
let user = wasmer_api::query::current_user_with_namespaces(&client, None).await?;
crate::utils::prompts::prompt_for_namespace(
"Who should own this package?",
None,
Some(&user),
)
}
Err(e) => anyhow::bail!(
"Can't determine user info: {e}. Please, user `wasmer login` before deploying an app or use the --owner <owner> flag to signal the owner of the app to deploy."
),
}
}
}
#[async_trait::async_trait]
impl AsyncCliCommand for CmdAppDeploy {
type Output = ();
async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
let interactive = std::io::stdin().is_terminal() && !self.non_interactive;
let client = self
.api
.client()
.with_context(|| "Can't begin deploy flow")?;
let app_config_path = {
let base_path = self.path.clone().unwrap_or(std::env::current_dir()?);
if base_path.is_file() {
base_path
} else if base_path.is_dir() {
let f = base_path.join(AppConfigV1::CANONICAL_FILE_NAME);
if !f.is_file() {
anyhow::bail!("Could not find app.yaml at path '{}'", f.display());
}
f
} else {
anyhow::bail!("No such file or directory '{}'", base_path.display());
}
};
if !app_config_path.is_file() {
if interactive {
eprintln!("It seems you are trying to create a new app!");
let create_cmd = CmdAppCreate {
template: None,
publish_package: false,
no_validate: false,
non_interactive: false,
offline: false,
owner: None,
name: None,
path: None,
no_wait: false,
api: self.api.clone(),
fmt: ItemFormatOpts {
format: self.fmt.format.clone(),
},
package: None,
};
create_cmd.run_async().await?;
} else {
anyhow::bail!(
"Cannot deploy app as no app.yaml was found in path '{}'",
app_config_path.display()
)
}
}
assert!(app_config_path.is_file());
let config_str = std::fs::read_to_string(&app_config_path)
.with_context(|| format!("Could not read file '{}'", app_config_path.display()))?;
let mut app_config: AppConfigV1 = AppConfigV1::parse_yaml(&config_str)?;
eprintln!("Loaded app from: '{}'", app_config_path.display());
let owner = self.get_owner(&app_config).await?;
let wait = if self.no_wait {
WaitMode::Deployed
} else {
WaitMode::Reachable
};
let opts = match app_config.package {
PackageSource::Path(ref path) => {
eprintln!("Checking local package at path '{}'...", path);
let package =
PackageSource::from(self.publish(owner.clone(), PathBuf::from(path)).await?);
// We should now assume that the package pointed to by the path is now published,
// and `package_spec` is either a hash or an identifier.
app_config.package = package;
DeployAppOpts {
app: &app_config,
original_config: Some(app_config.clone().to_yaml_value().unwrap()),
allow_create: true,
make_default: !self.no_default,
owner: Some(owner),
wait,
}
}
_ => {
eprintln!("Using package {}", app_config.package.to_string());
DeployAppOpts {
app: &app_config,
original_config: Some(app_config.clone().to_yaml_value().unwrap()),
allow_create: true,
make_default: !self.no_default,
owner: Some(owner),
wait,
}
}
};
let (_app, app_version) = deploy_app_verbose(&client, opts).await?;
let mut new_app_config = app_config_from_api(&app_version)?;
if self.no_persist_id {
new_app_config.app_id = None;
}
// If the config changed, write it back.
if new_app_config != app_config {
// We want to preserve unknown fields to allow for newer app.yaml
// settings without requring new CLI versions, so instead of just
// serializing the new config, we merge it with the old one.
let new_merged = crate::utils::merge_yaml_values(
&app_config.to_yaml_value()?,
&new_app_config.to_yaml_value()?,
);
let new_config_raw = serde_yaml::to_string(&new_merged)?;
std::fs::write(&app_config_path, new_config_raw).with_context(|| {
format!("Could not write file: '{}'", app_config_path.display())
})?;
}
if self.fmt.format == crate::utils::render::ItemFormat::Json {
println!("{}", serde_json::to_string_pretty(&app_version)?);
}
Ok(())
}
}
#[derive(Debug)]
pub struct DeployAppOpts<'a> {
pub app: &'a AppConfigV1,
// Original raw yaml config.
// Present here to enable forwarding unknown fields to the backend, which
// preserves forwards-compatibility for schema changes.
pub original_config: Option<serde_yaml::value::Value>,
pub allow_create: bool,
pub make_default: bool,
pub owner: Option<String>,
pub wait: WaitMode,
}
pub async fn deploy_app(
client: &WasmerClient,
opts: DeployAppOpts<'_>,
) -> Result<DeployAppVersion, anyhow::Error> {
let app = opts.app;
let config_value = app.clone().to_yaml_value()?;
let final_config = if let Some(old) = &opts.original_config {
crate::utils::merge_yaml_values(old, &config_value)
} else {
config_value
};
let mut raw_config = serde_yaml::to_string(&final_config)?.trim().to_string();
raw_config.push('\n');
// TODO: respect allow_create flag
let version = wasmer_api::query::publish_deploy_app(
client,
dbg!(wasmer_api::types::PublishDeployAppVars {
config: raw_config,
name: app.name.clone().into(),
owner: opts.owner.map(|o| o.into()),
make_default: Some(opts.make_default),
}),
)
.await
.context("could not create app in the backend")?;
Ok(version)
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum WaitMode {
/// Wait for the app to be deployed.
Deployed,
/// Wait for the app to be deployed and ready.
Reachable,
}
/// Same as [Self::deploy], but also prints verbose information.
pub async fn deploy_app_verbose(
client: &WasmerClient,
opts: DeployAppOpts<'_>,
) -> Result<(DeployApp, DeployAppVersion), anyhow::Error> {
let owner = &opts.owner.clone().or_else(|| opts.app.owner.clone());
let app = &opts.app;
let pretty_name = if let Some(owner) = &owner {
format!("{}/{}", owner, app.name)
} else {
app.name.clone()
};
let make_default = opts.make_default;
eprintln!("Deploying app {pretty_name}...\n");
let wait = opts.wait;
let version = deploy_app(client, opts).await?;
let app_id = version
.app
.as_ref()
.context("app field on app version is empty")?
.id
.inner()
.to_string();
let app = wasmer_api::query::get_app_by_id(client, app_id.clone())
.await
.context("could not fetch app from backend")?;
let full_name = format!("{}/{}", app.owner.global_name, app.name);
eprintln!(" ✅ App {full_name} was successfully deployed!");
eprintln!();
eprintln!("> App URL: {}", app.url);
eprintln!("> Versioned URL: {}", version.url);
eprintln!("> Admin dashboard: {}", app.admin_url);
match wait {
WaitMode::Deployed => {}
WaitMode::Reachable => {
eprintln!();
eprintln!("Waiting for new deployment to become available...");
eprintln!("(You can safely stop waiting now with CTRL-C)");
let stderr = std::io::stderr();
tokio::time::sleep(Duration::from_secs(2)).await;
let start = tokio::time::Instant::now();
let client = reqwest::Client::new();
let check_url = if make_default { &app.url } else { &version.url };
let mut sleep_millis: u64 = 1_000;
loop {
let total_elapsed = start.elapsed();
if total_elapsed > Duration::from_secs(60 * 5) {
eprintln!();
anyhow::bail!("\nApp still not reachable after 5 minutes...");
}
{
let mut lock = stderr.lock();
write!(&mut lock, ".").unwrap();
lock.flush().unwrap();
}
let request_start = tokio::time::Instant::now();
match client.get(check_url).send().await {
Ok(res) => {
let header = res
.headers()
.get(edge_util::headers::HEADER_APP_VERSION_ID)
.and_then(|x| x.to_str().ok())
.unwrap_or_default();
if header == version.id.inner() {
eprintln!("\nNew version is now reachable at {check_url}");
eprintln!("Deployment complete");
break;
}
tracing::debug!(
current=%header,
expected=%app.active_version.id.inner(),
"app is not at the right version yet",
);
}
Err(err) => {
tracing::debug!(?err, "health check request failed");
}
};
let elapsed: u64 = request_start
.elapsed()
.as_millis()
.try_into()
.unwrap_or_default();
let to_sleep = Duration::from_millis(sleep_millis.saturating_sub(elapsed));
tokio::time::sleep(to_sleep).await;
sleep_millis = (sleep_millis * 2).max(10_000);
}
}
}
Ok((app, version))
}
pub fn app_config_from_api(version: &DeployAppVersion) -> Result<AppConfigV1, anyhow::Error> {
let app_id = version
.app
.as_ref()
.context("app field on app version is empty")?
.id
.inner()
.to_string();
let cfg = &version.user_yaml_config;
let mut cfg = AppConfigV1::parse_yaml(cfg)
.context("could not parse app config from backend app version")?;
cfg.app_id = Some(app_id);
Ok(cfg)
}

View File

@@ -1,7 +1,9 @@
//! Edge app commands. //! Edge app commands.
#![allow(unused, dead_code)]
pub mod create; pub mod create;
pub mod delete; pub mod delete;
pub mod deploy;
pub mod get; pub mod get;
pub mod info; pub mod info;
pub mod list; pub mod list;
@@ -10,16 +12,8 @@ pub mod version;
mod util; mod util;
use std::{io::Write, time::Duration};
use anyhow::{bail, Context};
use edge_schema::schema::AppConfigV1;
use wasmer_api::{
types::{DeployApp, DeployAppVersion},
WasmerClient,
};
use crate::commands::AsyncCliCommand; use crate::commands::AsyncCliCommand;
use edge_schema::schema::AppConfigV1;
/// Manage Wasmer Deploy apps. /// Manage Wasmer Deploy apps.
#[derive(clap::Subcommand, Debug)] #[derive(clap::Subcommand, Debug)]
@@ -32,13 +26,14 @@ pub enum CmdApp {
Delete(delete::CmdAppDelete), Delete(delete::CmdAppDelete),
#[clap(subcommand)] #[clap(subcommand)]
Version(version::CmdAppVersion), Version(version::CmdAppVersion),
Deploy(deploy::CmdAppDeploy),
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl AsyncCliCommand for CmdApp { impl AsyncCliCommand for CmdApp {
type Output = (); type Output = ();
async fn run_async(self) -> Result<(), anyhow::Error> { async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
match self { match self {
Self::Get(cmd) => { Self::Get(cmd) => {
cmd.run_async().await?; cmd.run_async().await?;
@@ -56,188 +51,7 @@ impl AsyncCliCommand for CmdApp {
Self::Logs(cmd) => cmd.run_async().await, Self::Logs(cmd) => cmd.run_async().await,
Self::Delete(cmd) => cmd.run_async().await, Self::Delete(cmd) => cmd.run_async().await,
Self::Version(cmd) => cmd.run_async().await, Self::Version(cmd) => cmd.run_async().await,
Self::Deploy(cmd) => cmd.run_async().await,
} }
} }
} }
pub struct DeployAppOpts<'a> {
pub app: &'a AppConfigV1,
// Original raw yaml config.
// Present here to enable forwarding unknown fields to the backend, which
// preserves forwards-compatibility for schema changes.
pub original_config: Option<serde_yaml::Value>,
pub allow_create: bool,
pub make_default: bool,
pub owner: Option<String>,
pub wait: WaitMode,
}
pub async fn deploy_app(
client: &WasmerClient,
opts: DeployAppOpts<'_>,
) -> Result<DeployAppVersion, anyhow::Error> {
let app = opts.app;
let config_value = app.clone().to_yaml_value()?;
let final_config = if let Some(old) = &opts.original_config {
crate::utils::merge_yaml_values(old, &config_value)
} else {
config_value
};
let mut raw_config = serde_yaml::to_string(&final_config)?.trim().to_string();
raw_config.push('\n');
// TODO: respect allow_create flag
let version = wasmer_api::query::publish_deploy_app(
client,
wasmer_api::types::PublishDeployAppVars {
config: raw_config,
name: app.name.clone().into(),
owner: opts.owner.map(|o| o.into()),
make_default: Some(opts.make_default),
},
)
.await
.context("could not create app in the backend")?;
Ok(version)
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum WaitMode {
/// Wait for the app to be deployed.
Deployed,
/// Wait for the app to be deployed and ready.
Reachable,
}
/// Same as [Self::deploy], but also prints verbose information.
pub async fn deploy_app_verbose(
client: &WasmerClient,
opts: DeployAppOpts<'_>,
) -> Result<(DeployApp, DeployAppVersion), anyhow::Error> {
let owner = &opts.owner.clone().or_else(|| opts.app.owner.clone());
let app = &opts.app;
let pretty_name = if let Some(owner) = &owner {
format!("{}/{}", owner, app.name)
} else {
app.name.clone()
};
let make_default = opts.make_default;
eprintln!("Deploying app {pretty_name}...\n");
let wait = opts.wait;
let version = deploy_app(client, opts).await?;
let app_id = version
.app
.as_ref()
.context("app field on app version is empty")?
.id
.inner()
.to_string();
let app = wasmer_api::query::get_app_by_id(client, app_id.clone())
.await
.context("could not fetch app from backend")?;
let full_name = format!("{}/{}", app.owner.global_name, app.name);
eprintln!(" ✅ App {full_name} was successfully deployed!");
eprintln!();
eprintln!("> App URL: {}", app.url);
eprintln!("> Versioned URL: {}", version.url);
eprintln!("> Admin dashboard: {}", app.admin_url);
match wait {
WaitMode::Deployed => {}
WaitMode::Reachable => {
eprintln!();
eprintln!("Waiting for new deployment to become available...");
eprintln!("(You can safely stop waiting now with CTRL-C)");
let stderr = std::io::stderr();
tokio::time::sleep(Duration::from_secs(2)).await;
let start = tokio::time::Instant::now();
let client = reqwest::Client::new();
let check_url = if make_default { &app.url } else { &version.url };
let mut sleep_millis: u64 = 1_000;
loop {
let total_elapsed = start.elapsed();
if total_elapsed > Duration::from_secs(60 * 5) {
eprintln!();
bail!("\nApp still not reachable after 5 minutes...");
}
{
let mut lock = stderr.lock();
write!(&mut lock, ".").unwrap();
lock.flush().unwrap();
}
let request_start = tokio::time::Instant::now();
match client.get(check_url).send().await {
Ok(res) => {
let header = res
.headers()
.get(edge_util::headers::HEADER_APP_VERSION_ID)
.and_then(|x| x.to_str().ok())
.unwrap_or_default();
if header == version.id.inner() {
eprintln!("\nNew version is now reachable at {check_url}");
eprintln!("Deployment complete");
break;
}
tracing::debug!(
current=%header,
expected=%app.active_version.id.inner(),
"app is not at the right version yet",
);
}
Err(err) => {
tracing::debug!(?err, "health check request failed");
}
};
let elapsed: u64 = request_start
.elapsed()
.as_millis()
.try_into()
.unwrap_or_default();
let to_sleep = Duration::from_millis(sleep_millis.saturating_sub(elapsed));
tokio::time::sleep(to_sleep).await;
sleep_millis = (sleep_millis * 2).max(10_000);
}
}
}
Ok((app, version))
}
pub fn app_config_from_api(version: &DeployAppVersion) -> Result<AppConfigV1, anyhow::Error> {
let app_id = version
.app
.as_ref()
.context("app field on app version is empty")?
.id
.inner()
.to_string();
let cfg = &version.user_yaml_config;
let mut cfg = AppConfigV1::parse_yaml(cfg)
.context("could not parse app config from backend app version")?;
cfg.app_id = Some(app_id);
Ok(cfg)
}

View File

@@ -1,87 +0,0 @@
use crate::commands::{
app::{DeployAppOpts, WaitMode},
deploy::{CmdDeploy, DeployAppVersion},
};
use edge_schema::schema::{AppConfigV1, PackageSpecifier};
use std::{path::PathBuf, str::FromStr};
#[derive(Debug)]
/// Deploy an unnamed package from its manifest's path.
pub struct DeployFromPackageManifestPath {
pub pkg_manifest_path: PathBuf,
pub config: AppConfigV1,
}
impl DeployFromPackageManifestPath {
pub async fn deploy(&self, cmd: &CmdDeploy) -> Result<DeployAppVersion, anyhow::Error> {
let client = cmd.api.client()?;
let owner = match &self.config.owner {
Some(owner) => Some(owner.clone()),
None => cmd.owner.clone(),
};
let manifest =
match crate::utils::load_package_manifest(&self.pkg_manifest_path)?.map(|x| x.1) {
Some(manifest) => manifest,
None => anyhow::bail!(
"The path '{}' doesn't point to a (valid) manifest",
self.pkg_manifest_path.display()
),
};
if manifest.package.is_some() {
anyhow::bail!("Cannot publish package as unnamed, as the manifest pointed to by '{}' contains a package field", self.pkg_manifest_path.display());
}
eprintln!("Publishing package...");
let (_, maybe_hash) = crate::utils::republish_package(
&client,
&self.pkg_manifest_path,
manifest.clone(),
owner.clone(),
)
.await?;
eprintln!(
"Unnamed package from manifest '{}' published successfully!",
self.pkg_manifest_path.display()
);
eprintln!();
let wait_mode = if cmd.no_wait {
WaitMode::Deployed
} else {
WaitMode::Reachable
};
match maybe_hash {
Some(hash) => {
let package_spec = PackageSpecifier::from_str(&format!("sha256:{}", hash))?;
let new_config = AppConfigV1 {
package: package_spec,
..self.config.clone()
};
let opts = DeployAppOpts {
app: &new_config,
original_config: Some(self.config.clone().to_yaml_value().unwrap()),
allow_create: true,
make_default: !cmd.no_default,
owner,
wait: wait_mode,
};
let (_app, app_version) =
crate::commands::app::deploy_app_verbose(&client, opts).await?;
if cmd.fmt.format == crate::utils::render::ItemFormat::Json {
println!("{}", serde_json::to_string_pretty(&app_version)?);
}
Ok(app_version)
}
None => {
anyhow::bail!("Backend did not return a hash for the published unnamed package")
}
}
}
}

View File

@@ -1,56 +0,0 @@
use self::{
manifest_path::DeployFromPackageManifestPath, sha256::DeployFromSha256Hash,
webc::DeployFromWebc,
};
use super::CmdDeploy;
use edge_schema::schema::{AppConfigV1, PackageHash, PackageSpecifier};
use std::path::PathBuf;
use wasmer_api::types::DeployAppVersion;
pub(super) mod manifest_path;
pub(super) mod sha256;
pub(super) mod webc;
#[derive(Debug)]
pub enum DeployApp {
Path(DeployFromPackageManifestPath),
Ident(DeployFromWebc),
Sha256Hash(DeployFromSha256Hash),
}
impl From<AppConfigV1> for DeployApp {
fn from(config: AppConfigV1) -> Self {
match &config.package {
PackageSpecifier::Ident(webc_id) => DeployApp::Ident(DeployFromWebc {
webc_id: webc_id.clone(),
config,
}),
PackageSpecifier::Path(pkg_manifest_path) => {
DeployApp::Path(DeployFromPackageManifestPath {
pkg_manifest_path: PathBuf::from(pkg_manifest_path),
config,
})
}
PackageSpecifier::Hash(PackageHash(hash)) => {
DeployApp::Sha256Hash(DeployFromSha256Hash {
hash: hash.clone(),
config,
})
}
}
}
}
impl DeployApp {
pub(super) async fn deploy(
self,
app_config_path: PathBuf,
cmd: &CmdDeploy,
) -> Result<DeployAppVersion, anyhow::Error> {
match self {
DeployApp::Path(p) => p.deploy(cmd).await,
DeployApp::Ident(i) => i.deploy(app_config_path, cmd).await,
DeployApp::Sha256Hash(s) => s.deploy(app_config_path, cmd).await,
}
}
}

View File

@@ -1,20 +0,0 @@
use crate::commands::deploy::CmdDeploy;
use edge_schema::schema::{AppConfigV1, Sha256Hash};
use std::path::PathBuf;
use wasmer_api::types::DeployAppVersion;
#[derive(Debug)]
pub struct DeployFromSha256Hash {
pub hash: Sha256Hash,
pub config: AppConfigV1,
}
impl DeployFromSha256Hash {
pub async fn deploy(
&self,
_app_config_path: PathBuf,
_cmd: &CmdDeploy,
) -> Result<DeployAppVersion, anyhow::Error> {
todo!()
}
}

View File

@@ -1,178 +0,0 @@
use crate::commands::{
app::{DeployAppOpts, WaitMode},
deploy::CmdDeploy,
};
use anyhow::Context;
use edge_schema::schema::{AppConfigV1, PackageIdentifier, PackageSpecifier};
use is_terminal::IsTerminal;
use std::{io::Write, path::PathBuf};
use url::Url;
use wasmer_api::types::DeployAppVersion;
/// Deploy a named package from its Webc identifier.
#[derive(Debug)]
pub struct DeployFromWebc {
pub webc_id: PackageIdentifier,
pub config: AppConfigV1,
}
impl DeployFromWebc {
pub async fn deploy(
&self,
app_config_path: PathBuf,
cmd: &CmdDeploy,
) -> Result<DeployAppVersion, anyhow::Error> {
let webc_id = &self.webc_id;
let client = cmd.api.client()?;
let pkg_name = webc_id.to_string();
let interactive = std::io::stdin().is_terminal() && !cmd.non_interactive;
let dir_path = app_config_path.canonicalize()?.parent().unwrap().to_owned();
// Find and load the mandatory `wasmer.toml` file.
let local_manifest_path = dir_path.join(crate::utils::DEFAULT_PACKAGE_MANIFEST_FILE);
let local_manifest = crate::utils::load_package_manifest(&local_manifest_path)?
.map(|x| x.1)
// Ignore local package if it is not referenced by the app.
.filter(|m| {
if let Some(pkg) = &m.package {
pkg.name == format!("{}/{}", webc_id.namespace, webc_id.name)
} else {
false
}
});
let new_package_manifest = if let Some(manifest) = local_manifest {
let should_publish = if cmd.publish_package {
true
} else if interactive {
eprintln!();
dialoguer::Confirm::new()
.with_prompt(format!("Publish new version of package '{}'?", pkg_name))
.interact_opt()?
.unwrap_or_default()
} else {
false
};
if should_publish {
eprintln!("Publishing package...");
let (new_manifest, _maybe_hash) =
crate::utils::republish_package(&client, &local_manifest_path, manifest, None)
.await?;
eprint!("Waiting for package to become available...");
std::io::stderr().flush().unwrap();
let start_wait = std::time::Instant::now();
loop {
if start_wait.elapsed().as_secs() > 300 {
anyhow::bail!("Timed out waiting for package to become available");
}
eprint!(".");
std::io::stderr().flush().unwrap();
let new_version_opt = wasmer_api::query::get_package_version(
&client,
new_manifest.package.as_ref().unwrap().name.clone(),
new_manifest.package.as_ref().unwrap().version.to_string(),
)
.await;
match new_version_opt {
Ok(Some(new_version)) => {
if new_version.distribution.pirita_sha256_hash.is_some() {
eprintln!();
break;
}
}
Ok(None) => {
anyhow::bail!(
"Error - could not query package info: package not found"
);
}
Err(e) => {
anyhow::bail!("Error - could not query package info: {e}");
}
}
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
}
eprintln!("Package '{pkg_name}' published successfully!",);
eprintln!();
Some(new_manifest)
} else {
if interactive {
eprintln!();
}
None
}
} else {
None
};
let config = if let Some(manifest) = new_package_manifest {
let package = manifest.package.unwrap();
let mut package_splits = package.name.split("/");
let package_namespace = package_splits.next().unwrap();
let package_name = package_splits.next().unwrap();
let package_spec = PackageSpecifier::Ident(PackageIdentifier {
repository: package.repository.map(|s| Url::parse(&s).unwrap()),
namespace: package_namespace.to_string(),
name: package_name.to_string(),
tag: Some(package.version.to_string()),
});
AppConfigV1 {
package: package_spec,
..self.config.clone()
}
} else {
self.config.clone()
};
let wait_mode = if cmd.no_wait {
WaitMode::Deployed
} else {
WaitMode::Reachable
};
let opts = DeployAppOpts {
app: &config,
original_config: Some(config.clone().to_yaml_value().unwrap()),
allow_create: true,
make_default: !cmd.no_default,
owner: match &config.owner {
Some(owner) => Some(owner.clone()),
None => cmd.owner.clone(),
},
wait: wait_mode,
};
let (_app, app_version) = crate::commands::app::deploy_app_verbose(&client, opts).await?;
let mut new_config = crate::commands::app::app_config_from_api(&app_version)?;
if cmd.no_persist_id {
new_config.app_id = None;
}
// If the config changed, write it back.
if new_config != config {
// We want to preserve unknown fields to allow for newer app.yaml
// settings without requring new CLI versions, so instead of just
// serializing the new config, we merge it with the old one.
let new_merged = crate::utils::merge_yaml_values(
&config.to_yaml_value()?,
&new_config.to_yaml_value()?,
);
let new_config_raw = serde_yaml::to_string(&new_merged)?;
std::fs::write(&app_config_path, new_config_raw).with_context(|| {
format!("Could not write file: '{}'", app_config_path.display())
})?;
}
if cmd.fmt.format == crate::utils::render::ItemFormat::Json {
println!("{}", serde_json::to_string_pretty(&app_version)?);
}
Ok(app_version)
}
}

View File

@@ -1,92 +0,0 @@
use super::AsyncCliCommand;
use crate::{
commands::deploy::deploy::DeployApp,
opts::{ApiOpts, ItemFormatOpts},
};
use anyhow::Context;
use edge_schema::schema::AppConfigV1;
use std::path::PathBuf;
use wasmer_api::types::DeployAppVersion;
// [todo]: deploy inside deploy? Let's think of a better name.
mod deploy;
/// Deploy an app to Wasmer Edge.
#[derive(clap::Parser, Debug)]
pub struct CmdDeploy {
#[clap(flatten)]
pub api: ApiOpts,
#[clap(flatten)]
pub fmt: ItemFormatOpts,
/// Skip local schema validation.
#[clap(long)]
pub no_validate: bool,
/// Do not prompt for user input.
#[clap(long)]
pub non_interactive: bool,
/// Automatically publish the package referenced by this app.
///
/// Only works if the corresponding wasmer.toml is in the same directory.
#[clap(long)]
pub publish_package: bool,
/// The path to the app.yaml file.
#[clap(long)]
pub path: Option<PathBuf>,
/// Do not wait for the app to become reachable.
#[clap(long)]
pub no_wait: bool,
/// Do not make the new app version the default (active) version.
/// This is useful for testing a deployment first, before moving it to "production".
#[clap(long)]
pub no_default: bool,
/// Do not persist the app version ID in the app.yaml.
#[clap(long)]
pub no_persist_id: bool,
/// Specify the owner (user or namespace) of the app.
/// Will default to the currently logged in user, or the existing one
/// if the app can be found.
#[clap(long)]
pub owner: Option<String>,
}
#[async_trait::async_trait]
impl AsyncCliCommand for CmdDeploy {
type Output = DeployAppVersion;
async fn run_async(self) -> Result<DeployAppVersion, anyhow::Error> {
let app_path = {
let base_path = self.path.clone().unwrap_or(std::env::current_dir()?);
if base_path.is_file() {
base_path
} else if base_path.is_dir() {
let f = base_path.join(AppConfigV1::CANONICAL_FILE_NAME);
if !f.is_file() {
anyhow::bail!("Could not find app.yaml at path '{}'", f.display());
}
f
} else {
anyhow::bail!("No such file or directory '{}'", base_path.display());
}
};
let config_str = std::fs::read_to_string(&app_path)
.with_context(|| format!("Could not read file '{}'", app_path.display()))?;
let config: AppConfigV1 = AppConfigV1::parse_yaml(&config_str)?;
eprintln!("Loaded app from: '{}'", app_path.display());
Into::<DeployApp>::into(config)
.deploy(app_path, &self)
.await
}
}

View File

@@ -13,7 +13,6 @@ mod container;
mod create_exe; mod create_exe;
#[cfg(feature = "static-artifact-create")] #[cfg(feature = "static-artifact-create")]
mod create_obj; mod create_obj;
pub(crate) mod deploy;
pub(crate) mod domain; pub(crate) mod domain;
#[cfg(feature = "static-artifact-create")] #[cfg(feature = "static-artifact-create")]
mod gen_c_header; mod gen_c_header;
@@ -131,10 +130,10 @@ impl WasmerCmd {
Some(Cmd::Inspect(inspect)) => inspect.execute(), Some(Cmd::Inspect(inspect)) => inspect.execute(),
Some(Cmd::Init(init)) => init.execute(), Some(Cmd::Init(init)) => init.execute(),
Some(Cmd::Login(login)) => login.execute(), Some(Cmd::Login(login)) => login.execute(),
Some(Cmd::Publish(publish)) => publish.execute(), Some(Cmd::Publish(publish)) => publish.run().map(|_| ()),
Some(Cmd::Package(cmd)) => match cmd { Some(Cmd::Package(cmd)) => match cmd {
Package::Download(cmd) => cmd.execute(), Package::Download(cmd) => cmd.execute(),
Package::Build(cmd) => cmd.execute(), Package::Build(cmd) => cmd.execute().map(|_| ()),
}, },
Some(Cmd::Container(cmd)) => match cmd { Some(Cmd::Container(cmd)) => match cmd {
crate::commands::Container::Unpack(cmd) => cmd.execute(), crate::commands::Container::Unpack(cmd) => cmd.execute(),
@@ -345,8 +344,8 @@ enum Cmd {
Container(crate::commands::Container), Container(crate::commands::Container),
// Edge commands // Edge commands
/// Deploy apps to Wasmer Edge. /// Deploy apps to Wasmer Edge. [alias: app deploy]
Deploy(crate::commands::deploy::CmdDeploy), Deploy(crate::commands::app::deploy::CmdAppDeploy),
/// Manage deployed Edge apps. /// Manage deployed Edge apps.
#[clap(subcommand, alias = "apps")] #[clap(subcommand, alias = "apps")]

View File

@@ -3,6 +3,9 @@ use std::path::PathBuf;
use anyhow::Context; use anyhow::Context;
use dialoguer::console::{style, Emoji}; use dialoguer::console::{style, Emoji};
use indicatif::ProgressBar; use indicatif::ProgressBar;
use wasmer_config::package::PackageHash;
use crate::utils::load_package_manifest;
/// Build a container from a package manifest. /// Build a container from a package manifest.
#[derive(clap::Parser, Debug)] #[derive(clap::Parser, Debug)]
@@ -41,9 +44,21 @@ impl PackageBuild {
} }
} }
pub(crate) fn execute(&self) -> Result<(), anyhow::Error> { pub(crate) fn execute(&self) -> Result<PackageHash, anyhow::Error> {
let manifest_path = self.manifest_path()?; let manifest_path = self.manifest_path()?;
let Some((_, manifest)) = load_package_manifest(&manifest_path)? else {
anyhow::bail!(
"Could not locate manifest in path '{}'",
manifest_path.display()
)
};
let pkg = webc::wasmer_package::Package::from_manifest(manifest_path)?; let pkg = webc::wasmer_package::Package::from_manifest(manifest_path)?;
let pkg_hash = PackageHash::from_sha256_bytes(pkg.webc_hash());
let name = if let Some(manifest_pkg) = manifest.package {
format!("{}-{}.webc", manifest_pkg.name, manifest_pkg.version)
} else {
format!("{}.webc", pkg_hash)
};
// Setup the progress bar // Setup the progress bar
let pb = if self.quiet { let pb = if self.quiet {
@@ -58,20 +73,13 @@ impl PackageBuild {
READING_MANIFEST_EMOJI READING_MANIFEST_EMOJI
)); ));
let manifest = pkg
.manifest()
.wapm()
.context("could not load package manifest")?
.context("package does not contain a Wasmer manifest")?;
// rest of the code writes the package to disk and is irrelevant // rest of the code writes the package to disk and is irrelevant
// to checking. // to checking.
if self.check { if self.check {
return Ok(()); return Ok(pkg_hash);
} }
let pkgname = manifest.name.unwrap().replace('/', "-"); let manifest = pkg.manifest();
let name = format!("{}-{}.webc", pkgname, manifest.version.unwrap(),);
pb.println(format!( pb.println(format!(
"{} {}Creating output directory...", "{} {}Creating output directory...",
@@ -119,7 +127,7 @@ impl PackageBuild {
out_path.display() out_path.display()
)); ));
Ok(()) Ok(pkg_hash)
} }
fn manifest_path(&self) -> Result<PathBuf, anyhow::Error> { fn manifest_path(&self) -> Result<PathBuf, anyhow::Error> {

View File

@@ -4,8 +4,8 @@ use anyhow::{bail, Context};
use dialoguer::console::{style, Emoji}; use dialoguer::console::{style, Emoji};
use indicatif::{ProgressBar, ProgressStyle}; use indicatif::{ProgressBar, ProgressStyle};
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use wasmer_config::package::{PackageIdent, PackageSource};
use wasmer_registry::wasmer_env::WasmerEnv; use wasmer_registry::wasmer_env::WasmerEnv;
use wasmer_wasix::runtime::resolver::PackageSpecifier;
/// Download a package from the registry. /// Download a package from the registry.
#[derive(clap::Parser, Debug)] #[derive(clap::Parser, Debug)]
@@ -27,10 +27,7 @@ pub struct PackageDownload {
pub quiet: bool, pub quiet: bool,
/// The package to download. /// The package to download.
/// Can be: package: PackageSource,
/// * a pakage specifier: `namespace/package[@vesion]`
/// * a URL
package: PackageSpecifier,
} }
static CREATING_OUTPUT_DIRECTORY_EMOJI: Emoji<'_, '_> = Emoji("📁 ", ""); static CREATING_OUTPUT_DIRECTORY_EMOJI: Emoji<'_, '_> = Emoji("📁 ", "");
@@ -94,10 +91,11 @@ impl PackageDownload {
step_num += 1; step_num += 1;
let (download_url, token) = match &self.package { let (download_url, token) = match &self.package {
PackageSpecifier::Registry { full_name, version } => { PackageSource::Ident(PackageIdent::Named(id)) => {
let endpoint = self.env.registry_endpoint()?; let endpoint = self.env.registry_endpoint()?;
let version = version.to_string(); let version = id.version_or_default().to_string();
let version = if version == "*" { None } else { Some(version) }; let version = if version == "*" { None } else { Some(version) };
let full_name = id.full_name();
let token = self.env.get_token_opt().map(|x| x.to_string()); let token = self.env.get_token_opt().map(|x| x.to_string());
let package = wasmer_registry::query_package_from_registry( let package = wasmer_registry::query_package_from_registry(
@@ -119,7 +117,7 @@ impl PackageDownload {
(download_url, token) (download_url, token)
} }
PackageSpecifier::HashSha256(hash) => { PackageSource::Ident(PackageIdent::Hash(hash)) => {
let endpoint = self.env.registry_endpoint()?; let endpoint = self.env.registry_endpoint()?;
let token = self.env.get_token_opt().map(|x| x.to_string()); let token = self.env.get_token_opt().map(|x| x.to_string());
@@ -131,17 +129,13 @@ impl PackageDownload {
}; };
let rt = tokio::runtime::Runtime::new()?; let rt = tokio::runtime::Runtime::new()?;
let pkg = rt.block_on(wasmer_api::query::get_package_release(&client, &hash))? let pkg = rt.block_on(wasmer_api::query::get_package_release(&client, &hash.to_string()))?
.with_context(|| format!("Package with sha256:{hash} does not exist in the registry, or is not accessible"))?; .with_context(|| format!("Package with {hash} does not exist in the registry, or is not accessible"))?;
(pkg.webc_url, token) (pkg.webc_url, token)
} }
PackageSpecifier::Url(url) => { PackageSource::Path(p) => bail!("cannot download a package from a local path: '{p}'"),
bail!("cannot download a package from a URL: '{}'", url); PackageSource::Url(url) => bail!("cannot download a package from a URL: '{}'", url),
}
PackageSpecifier::Path(_) => {
bail!("cannot download a package from a local path");
}
}; };
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();

View File

@@ -1,21 +1,29 @@
use anyhow::Context as _; use anyhow::Context as _;
use clap::Parser; use clap::Parser;
use dialoguer::Confirm;
use is_terminal::IsTerminal;
use wasmer_config::package::PackageIdent;
use wasmer_registry::{publish::PublishWait, wasmer_env::WasmerEnv}; use wasmer_registry::{publish::PublishWait, wasmer_env::WasmerEnv};
use super::PackageBuild; use crate::{opts::ApiOpts, utils::load_package_manifest};
use super::{AsyncCliCommand, PackageBuild};
/// Publish a package to the package registry. /// Publish a package to the package registry.
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
pub struct Publish { pub struct Publish {
#[clap(flatten)] #[clap(flatten)]
env: WasmerEnv, pub env: WasmerEnv,
/// Run the publish logic without sending anything to the registry server /// Run the publish logic without sending anything to the registry server
#[clap(long, name = "dry-run")] #[clap(long, name = "dry-run")]
pub dry_run: bool, pub dry_run: bool,
/// Run the publish command without any output /// Run the publish command without any output
#[clap(long)] #[clap(long)]
pub quiet: bool, pub quiet: bool,
/// Override the package of the uploaded package in the wasmer.toml /// Override the namespace of the package to upload
#[clap(long)]
pub package_namespace: Option<String>,
/// Override the name of the package to upload
#[clap(long)] #[clap(long)]
pub package_name: Option<String>, pub package_name: Option<String>,
/// Override the package version of the uploaded package in the wasmer.toml /// Override the package version of the uploaded package in the wasmer.toml
@@ -44,17 +52,67 @@ pub struct Publish {
/// for each individual query to the registry during the publish flow. /// for each individual query to the registry during the publish flow.
#[clap(long, default_value = "2m")] #[clap(long, default_value = "2m")]
pub timeout: humantime::Duration, pub timeout: humantime::Duration,
/// Do not prompt for user input.
#[clap(long)]
pub non_interactive: bool,
} }
impl Publish { #[async_trait::async_trait]
/// Executes `wasmer publish` impl AsyncCliCommand for Publish {
pub fn execute(&self) -> Result<(), anyhow::Error> { type Output = Option<PackageIdent>;
// first check if the package could be built successfuly
let package_path = match self.package_path.as_ref() { async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
let manifest_dir_path = match self.package_path.as_ref() {
Some(s) => std::env::current_dir()?.join(s), Some(s) => std::env::current_dir()?.join(s),
None => std::env::current_dir()?, None => std::env::current_dir()?,
}; };
PackageBuild::check(package_path).execute()?;
let (_, manifest) = match load_package_manifest(&manifest_dir_path)? {
Some(r) => r,
None => anyhow::bail!(
"Path '{}' does not contain a valid `wasmer.toml` manifest.",
manifest_dir_path.display()
),
};
let hash = PackageBuild::check(manifest_dir_path).execute()?;
let api = ApiOpts {
token: self.env.token().clone(),
registry: Some(self.env.registry_endpoint()?),
};
let client = api.client()?;
let maybe_already_published =
wasmer_api::query::get_package_release(&client, &hash.to_string())
.await
.is_ok();
if maybe_already_published.is_some() {
eprintln!("Package with hash {hash} already present on registry");
return Ok(Some(PackageIdent::Hash(hash)));
}
let mut version = self.version.clone();
if let Some(pkg) = manifest.package {
if std::io::stdin().is_terminal() && !self.non_interactive {
eprintln!("Current package version is {}.", pkg.version);
let mut next_version = pkg.version.clone();
next_version.patch += 1;
if Confirm::new()
.with_prompt(format!(
"Do you want to bump it to a new version ({} -> {})?",
pkg.version, next_version
))
.interact()
.unwrap_or_default()
{
version = Some(next_version);
}
}
}
let token = self let token = self
.env .env
@@ -80,9 +138,9 @@ impl Publish {
package_path: self.package_path.clone(), package_path: self.package_path.clone(),
wait, wait,
timeout: self.timeout.into(), timeout: self.timeout.into(),
package_namespace: None, package_namespace: self.package_namespace,
}; };
publish.execute().map_err(on_error)?; let res = publish.execute().map_err(on_error)?;
if let Err(e) = invalidate_graphql_query_cache(&self.env) { if let Err(e) = invalidate_graphql_query_cache(&self.env) {
tracing::warn!( tracing::warn!(
@@ -91,7 +149,7 @@ impl Publish {
); );
} }
Ok(()) Ok(res)
} }
} }

View File

@@ -27,6 +27,7 @@ use wasmer::{
}; };
#[cfg(feature = "compiler")] #[cfg(feature = "compiler")]
use wasmer_compiler::ArtifactBuild; use wasmer_compiler::ArtifactBuild;
use wasmer_config::package::PackageSource as PackageSpecifier;
use wasmer_registry::{wasmer_env::WasmerEnv, Package}; use wasmer_registry::{wasmer_env::WasmerEnv, Package};
#[cfg(feature = "journal")] #[cfg(feature = "journal")]
use wasmer_wasix::journal::{LogFileJournal, SnapshotTrigger}; use wasmer_wasix::journal::{LogFileJournal, SnapshotTrigger};
@@ -44,7 +45,7 @@ use wasmer_wasix::{
runtime::{ runtime::{
module_cache::{CacheError, ModuleHash}, module_cache::{CacheError, ModuleHash},
package_loader::PackageLoader, package_loader::PackageLoader,
resolver::{PackageSpecifier, QueryError}, resolver::QueryError,
task_manager::VirtualTaskManagerExt, task_manager::VirtualTaskManagerExt,
}, },
Runtime, WasiError, Runtime, WasiError,
@@ -210,7 +211,7 @@ impl Run {
let mut dependencies = Vec::new(); let mut dependencies = Vec::new();
for name in &self.wasi.uses { for name in &self.wasi.uses {
let specifier = PackageSpecifier::parse(name) let specifier = PackageSpecifier::from_str(name)
.with_context(|| format!("Unable to parse \"{name}\" as a package specifier"))?; .with_context(|| format!("Unable to parse \"{name}\" as a package specifier"))?;
let pkg = { let pkg = {
let specifier = specifier.clone(); let specifier = specifier.clone();
@@ -560,7 +561,7 @@ impl PackageSource {
return Ok(PackageSource::Dir(path.to_path_buf())); return Ok(PackageSource::Dir(path.to_path_buf()));
} }
if let Ok(pkg) = PackageSpecifier::parse(s) { if let Ok(pkg) = PackageSpecifier::from_str(s) {
return Ok(PackageSource::Package(pkg)); return Ok(PackageSource::Package(pkg));
} }

View File

@@ -13,6 +13,7 @@ use tokio::runtime::Handle;
use url::Url; use url::Url;
use virtual_fs::{DeviceFile, FileSystem, PassthruFileSystem, RootFileSystemBuilder}; use virtual_fs::{DeviceFile, FileSystem, PassthruFileSystem, RootFileSystemBuilder};
use wasmer::{Engine, Function, Instance, Memory32, Memory64, Module, RuntimeError, Store, Value}; use wasmer::{Engine, Function, Instance, Memory32, Memory64, Module, RuntimeError, Store, Value};
use wasmer_config::package::PackageSource as PackageSpecifier;
use wasmer_registry::wasmer_env::WasmerEnv; use wasmer_registry::wasmer_env::WasmerEnv;
#[cfg(feature = "journal")] #[cfg(feature = "journal")]
use wasmer_wasix::journal::{LogFileJournal, SnapshotTrigger}; use wasmer_wasix::journal::{LogFileJournal, SnapshotTrigger};
@@ -28,10 +29,7 @@ use wasmer_wasix::{
runtime::{ runtime::{
module_cache::{FileSystemCache, ModuleCache, ModuleHash}, module_cache::{FileSystemCache, ModuleCache, ModuleHash},
package_loader::{BuiltinPackageLoader, PackageLoader}, package_loader::{BuiltinPackageLoader, PackageLoader},
resolver::{ resolver::{FileSystemSource, InMemorySource, MultiSource, Source, WapmSource, WebSource},
FileSystemSource, InMemorySource, MultiSource, PackageSpecifier, Source, WapmSource,
WebSource,
},
task_manager::{ task_manager::{
tokio::{RuntimeOrHandle, TokioTaskManager}, tokio::{RuntimeOrHandle, TokioTaskManager},
VirtualTaskManagerExt, VirtualTaskManagerExt,
@@ -228,7 +226,7 @@ impl Wasi {
let mut uses = Vec::new(); let mut uses = Vec::new();
for name in &self.uses { for name in &self.uses {
let specifier = PackageSpecifier::parse(name) let specifier = PackageSpecifier::from_str(name)
.with_context(|| format!("Unable to parse \"{name}\" as a package specifier"))?; .with_context(|| format!("Unable to parse \"{name}\" as a package specifier"))?;
let pkg = { let pkg = {
let inner_rt = rt.clone(); let inner_rt = rt.clone();

View File

@@ -4,10 +4,10 @@
#![doc(html_logo_url = "https://github.com/wasmerio.png?size=200")] #![doc(html_logo_url = "https://github.com/wasmerio.png?size=200")]
#![deny( #![deny(
missing_docs, missing_docs,
dead_code, // dead_code,
nonstandard_style, nonstandard_style,
unused_mut, unused_mut,
unused_variables, // unused_variables,
unused_unsafe, unused_unsafe,
unreachable_patterns unreachable_patterns
)] )]

View File

@@ -10,11 +10,10 @@ use std::{
}; };
use anyhow::{bail, Context as _, Result}; use anyhow::{bail, Context as _, Result};
use edge_schema::schema::PackageIdentifier;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use wasmer_api::WasmerClient; use wasmer_api::WasmerClient;
use wasmer_config::package::Manifest; use wasmer_config::package::NamedPackageIdent;
use wasmer_wasix::runners::MappedDirectory; use wasmer_wasix::runners::MappedDirectory;
fn retrieve_alias_pathbuf(alias: &str, real_dir: &str) -> Result<MappedDirectory> { fn retrieve_alias_pathbuf(alias: &str, real_dir: &str) -> Result<MappedDirectory> {
@@ -114,7 +113,7 @@ pub fn load_package_manifest(
pub fn prompt_for_package_name( pub fn prompt_for_package_name(
message: &str, message: &str,
default: Option<&str>, default: Option<&str>,
) -> Result<PackageIdentifier, anyhow::Error> { ) -> Result<NamedPackageIdent, anyhow::Error> {
loop { loop {
let raw: String = dialoguer::Input::new() let raw: String = dialoguer::Input::new()
.with_prompt(message) .with_prompt(message)
@@ -122,7 +121,7 @@ pub fn prompt_for_package_name(
.interact_text() .interact_text()
.context("could not read user input")?; .context("could not read user input")?;
match raw.parse::<PackageIdentifier>() { match raw.parse::<NamedPackageIdent>() {
Ok(p) => break Ok(p), Ok(p) => break Ok(p),
Err(err) => { Err(err) => {
eprintln!("invalid package name: {err}"); eprintln!("invalid package name: {err}");
@@ -150,7 +149,7 @@ pub async fn prompt_for_package(
default: Option<&str>, default: Option<&str>,
check: Option<PackageCheckMode>, check: Option<PackageCheckMode>,
client: Option<&WasmerClient>, client: Option<&WasmerClient>,
) -> Result<(PackageIdentifier, Option<wasmer_api::types::Package>), anyhow::Error> { ) -> Result<(NamedPackageIdent, Option<wasmer_api::types::Package>), anyhow::Error> {
loop { loop {
let name = prompt_for_package_name(message, default)?; let name = prompt_for_package_name(message, default)?;
@@ -181,122 +180,122 @@ pub async fn prompt_for_package(
} }
} }
/// Republish the package described by the [`wasmer_config::package::Manifest`] given as argument and return a // /// Republish the package described by the [`wasmer_config::package::Manifest`] given as argument and return a
/// [`Result<wasmer_config::package::Manifest>`]. // /// [`Result<wasmer_config::package::Manifest>`].
/// // ///
/// If the package described is named (i.e. has name, namespace and version), the returned manifest // /// If the package described is named (i.e. has name, namespace and version), the returned manifest
/// will have its minor version bumped. If the package is unnamed, the returned manifest will be // /// will have its minor version bumped. If the package is unnamed, the returned manifest will be
/// equal to the one given as input. // /// equal to the one given as input.
pub async fn republish_package( // pub async fn republish_package(
client: &WasmerClient, // client: &WasmerClient,
manifest_path: &Path, // manifest_path: &Path,
manifest: wasmer_config::package::Manifest, // manifest: wasmer_config::package::Manifest,
patch_owner: Option<String>, // patch_owner: Option<String>,
) -> Result<(wasmer_config::package::Manifest, Option<String>), anyhow::Error> { // ) -> Result<(wasmer_config::package::Manifest, Option<PackageIdent>), anyhow::Error> {
let manifest_path = if manifest_path.is_file() { // let manifest_path = if manifest_path.is_file() {
manifest_path.to_owned() // manifest_path.to_owned()
} else { // } else {
manifest_path.join(DEFAULT_PACKAGE_MANIFEST_FILE) // manifest_path.join(DEFAULT_PACKAGE_MANIFEST_FILE)
}; // };
//
let dir = manifest_path // let dir = manifest_path
.parent() // .parent()
.context("could not determine wasmer.toml parent directory")? // .context("could not determine wasmer.toml parent directory")?
.to_owned(); // .to_owned();
//
let new_manifest = match &manifest.package { // let new_manifest = match &manifest.package {
None => manifest.clone(), // None => manifest.clone(),
Some(pkg) => { // Some(pkg) => {
let mut pkg = pkg.clone(); // let mut pkg = pkg.clone();
let name = pkg.name.clone(); // let name = pkg.name.clone();
//
let current_opt = wasmer_api::query::get_package(client, pkg.name.clone()) // let current_opt = wasmer_api::query::get_package(client, pkg.name.clone())
.await // .await
.context("could not load package info from backend")? // .context("could not load package info from backend")?
.and_then(|x| x.last_version); // .and_then(|x| x.last_version);
//
let new_version = if let Some(current) = &current_opt { // let new_version = if let Some(current) = &current_opt {
let mut v = semver::Version::parse(&current.version).with_context(|| { // let mut v = semver::Version::parse(&current.version).with_context(|| {
format!("Could not parse package version: '{}'", current.version) // format!("Could not parse package version: '{}'", current.version)
})?; // })?;
//
v.patch += 1; // v.patch += 1;
//
// The backend does not have a reliable way to return the latest version, // // The backend does not have a reliable way to return the latest version,
// so we have to check each version in a loop. // // so we have to check each version in a loop.
loop { // loop {
let version = format!("={}", v); // let version = format!("={}", v);
let version = wasmer_api::query::get_package_version( // let version = wasmer_api::query::get_package_version(
client, // client,
name.clone(), // name.clone(),
version.clone(), // version.clone(),
) // )
.await // .await
.context("could not load package info from backend")?; // .context("could not load package info from backend")?;
//
if version.is_some() { // if version.is_some() {
v.patch += 1; // v.patch += 1;
} else { // } else {
break; // break;
} // }
} // }
//
v // v
} else { // } else {
pkg.version // pkg.version
}; // };
//
pkg.version = new_version; // pkg.version = new_version;
//
let mut manifest = manifest.clone(); // let mut manifest = manifest.clone();
manifest.package = Some(pkg); // manifest.package = Some(pkg);
//
let contents = toml::to_string(&manifest).with_context(|| { // let contents = toml::to_string(&manifest).with_context(|| {
format!( // format!(
"could not persist manifest to '{}'", // "could not persist manifest to '{}'",
manifest_path.display() // manifest_path.display()
) // )
})?; // })?;
//
std::fs::write(manifest_path.clone(), contents).with_context(|| { // std::fs::write(manifest_path.clone(), contents).with_context(|| {
format!("could not write manifest to '{}'", manifest_path.display()) // format!("could not write manifest to '{}'", manifest_path.display())
})?; // })?;
//
manifest // manifest
} // }
}; // };
//
let registry = client.graphql_endpoint().to_string(); // let registry = client.graphql_endpoint().to_string();
let token = client // let token = client
.auth_token() // .auth_token()
.context("no auth token configured - run 'wasmer login'")? // .context("no auth token configured - run 'wasmer login'")?
.to_string(); // .to_string();
//
let publish = wasmer_registry::package::builder::Publish { // let publish = wasmer_registry::package::builder::Publish {
registry: Some(registry), // registry: Some(registry),
dry_run: false, // dry_run: false,
quiet: false, // quiet: false,
package_name: None, // package_name: None,
version: None, // version: None,
wait: wasmer_registry::publish::PublishWait::new_none(), // wait: wasmer_registry::publish::PublishWait::new_none(),
token, // token,
no_validate: true, // no_validate: true,
package_path: Some(dir.to_str().unwrap().to_string()), // package_path: Some(dir.to_str().unwrap().to_string()),
// Use a high timeout to prevent interrupting uploads of // // Use a high timeout to prevent interrupting uploads of
// large packages. // // large packages.
timeout: std::time::Duration::from_secs(60 * 60 * 12), // timeout: std::time::Duration::from_secs(60 * 60 * 12),
package_namespace: patch_owner, // package_namespace: patch_owner,
}; // };
//
// Publish uses a blocking http client internally, which leads to a // // Publish uses a blocking http client internally, which leads to a
// "can't drop a runtime within an async context" error, so this has // // "can't drop a runtime within an async context" error, so this has
// to be run in a separate thread. // // to be run in a separate thread.
let maybe_hash = std::thread::spawn(move || publish.execute()) // let maybe_hash = std::thread::spawn(move || publish.execute())
.join() // .join()
.map_err(|e| anyhow::format_err!("failed to publish package: {:?}", e))??; // .map_err(|e| anyhow::format_err!("failed to publish package: {:?}", e))??;
//
Ok((new_manifest.clone(), maybe_hash)) // Ok((new_manifest.clone(), maybe_hash))
} // }
///// Re-publish a package with an increased minor version. ///// Re-publish a package with an increased minor version.
//pub async fn republish_package_with_bumped_version( //pub async fn republish_package_with_bumped_version(

View File

@@ -19,7 +19,7 @@ toml = "0.8"
thiserror = "1" thiserror = "1"
semver = { version = "1", features = ["serde"] } semver = { version = "1", features = ["serde"] }
serde_json = "1" serde_json = "1"
serde_yaml = "0.9.0" serde_yaml.workspace = true
serde_cbor = "0.11.2" serde_cbor = "0.11.2"
indexmap = { workspace = true, features = ["serde"] } indexmap = { workspace = true, features = ["serde"] }
derive_builder = "0.12.0" derive_builder = "0.12.0"

View File

@@ -53,6 +53,7 @@ wasmer-config = { workspace = true }
wasmer-wasm-interface = { version = "4.2.8", path = "../wasm-interface", optional = true } wasmer-wasm-interface = { version = "4.2.8", path = "../wasm-interface", optional = true }
wasmparser = { workspace = true, optional = true } wasmparser = { workspace = true, optional = true }
whoami = "1.2.3" whoami = "1.2.3"
webc.workspace = true
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1.3.0" pretty_assertions = "1.3.0"

View File

@@ -1,44 +0,0 @@
mutation PublishPackageMutationChunked(
$name: String
$namespace: String
$version: String
$description: String
$manifest: String!
$license: String
$licenseFile: String
$readme: String
$fileName: String
$repository: String
$homepage: String
$signature: InputSignature
$signedUrl: String
$private: Boolean
$wait: Boolean
) {
publishPackage(
input: {
name: $name
namespace: $namespace
version: $version
description: $description
manifest: $manifest
license: $license
licenseFile: $licenseFile
readme: $readme
file: $fileName
signedUrl: $signedUrl
repository: $repository
homepage: $homepage
signature: $signature
clientMutationId: ""
private: $private
wait: $wait
}
) {
success
packageVersion {
id
version
}
}
}

View File

@@ -9,6 +9,7 @@ use rusqlite::{params, Connection, OpenFlags, TransactionBehavior};
use tar::Builder; use tar::Builder;
use thiserror::Error; use thiserror::Error;
use time::{self, OffsetDateTime}; use time::{self, OffsetDateTime};
use wasmer_config::package::PackageIdent;
use crate::publish::PublishWait; use crate::publish::PublishWait;
use crate::{package::builder::validate::ValidationPolicy, publish::SignArchiveResult}; use crate::{package::builder::validate::ValidationPolicy, publish::SignArchiveResult};
@@ -22,7 +23,7 @@ const MIGRATIONS: &[(i32, &str)] = &[
const CURRENT_DATA_VERSION: usize = MIGRATIONS.len(); const CURRENT_DATA_VERSION: usize = MIGRATIONS.len();
/// CLI options for the `wasmer publish` command /// An abstraction for the action of publishing a named or unnamed package.
pub struct Publish { pub struct Publish {
/// Registry to publish to /// Registry to publish to
pub registry: Option<String>, pub registry: Option<String>,
@@ -30,7 +31,9 @@ pub struct Publish {
pub dry_run: bool, pub dry_run: bool,
/// Run the publish command without any output /// Run the publish command without any output
pub quiet: bool, pub quiet: bool,
/// Override the package of the uploaded package in the wasmer.toml /// Override the namespace of the package to upload
pub package_namespace: Option<String>,
/// Override the name of the package to upload
pub package_name: Option<String>, pub package_name: Option<String>,
/// Override the package version of the uploaded package in the wasmer.toml /// Override the package version of the uploaded package in the wasmer.toml
pub version: Option<semver::Version>, pub version: Option<semver::Version>,
@@ -44,8 +47,6 @@ pub struct Publish {
pub wait: PublishWait, pub wait: PublishWait,
/// Timeout (in seconds) for the publish query to the registry /// Timeout (in seconds) for the publish query to the registry
pub timeout: Duration, pub timeout: Duration,
pub package_namespace: Option<String>,
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]
@@ -65,8 +66,8 @@ enum PackageBuildError {
} }
impl Publish { impl Publish {
/// Executes `wasmer publish` /// Publish the package to the selected (or default) registry.
pub fn execute(&self) -> Result<Option<String>, anyhow::Error> { pub fn execute(&self) -> Result<Option<PackageIdent>, anyhow::Error> {
let input_path = match self.package_path.as_ref() { let input_path = match self.package_path.as_ref() {
Some(s) => std::env::current_dir()?.join(s), Some(s) => std::env::current_dir()?.join(s),
None => std::env::current_dir()?, None => std::env::current_dir()?,
@@ -164,19 +165,7 @@ impl Publish {
if self.dry_run { if self.dry_run {
// dry run: publish is done here // dry run: publish is done here
println!("🚀 Package published successfully!");
match manifest.package {
Some(pkg) => {
println!(
"🚀 Successfully published package `{}@{}`",
pkg.name, pkg.version
);
}
None => println!(
"🚀 Successfully published unnamed package from `{}`",
manifest_path.display()
),
}
let path = archive_dir.into_path(); let path = archive_dir.into_path();
eprintln!("Archive persisted at: {}", path.display()); eprintln!("Archive persisted at: {}", path.display());

View File

@@ -16,9 +16,11 @@ use std::collections::BTreeMap;
use std::fmt::Write; use std::fmt::Write;
use std::io::BufRead; use std::io::BufRead;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use wasmer_config::package::{NamedPackageIdent, PackageHash, PackageIdent};
static UPLOAD: Emoji<'_, '_> = Emoji("⬆️ ", ""); static UPLOAD: Emoji<'_, '_> = Emoji("⬆️ ", "");
static PACKAGE: Emoji<'_, '_> = Emoji("📦", ""); static PACKAGE: Emoji<'_, '_> = Emoji("📦", "");
@@ -91,7 +93,7 @@ pub fn try_chunked_uploading(
wait: PublishWait, wait: PublishWait,
timeout: Duration, timeout: Duration,
patch_namespace: Option<String>, patch_namespace: Option<String>,
) -> Result<Option<String>, anyhow::Error> { ) -> Result<Option<PackageIdent>, anyhow::Error> {
let (registry, token) = initialize_registry_and_token(registry, token)?; let (registry, token) = initialize_registry_and_token(registry, token)?;
let maybe_signature_data = sign_package(maybe_signature_data); let maybe_signature_data = sign_package(maybe_signature_data);
@@ -117,11 +119,11 @@ pub fn try_chunked_uploading(
version: package.as_ref().map(|p| p.version.to_string()), version: package.as_ref().map(|p| p.version.to_string()),
description: package.as_ref().map(|p| p.description.clone()), description: package.as_ref().map(|p| p.description.clone()),
manifest: manifest_string.to_string(), manifest: manifest_string.to_string(),
license: package.as_ref().map(|p| p.license.clone()).flatten(), license: package.as_ref().and_then(|p| p.license.clone()),
license_file: license_file.to_owned(), license_file: license_file.to_owned(),
readme: readme.to_owned(), readme: readme.to_owned(),
repository: package.as_ref().map(|p| p.repository.clone()).flatten(), repository: package.as_ref().and_then(|p| p.repository.clone()),
homepage: package.as_ref().map(|p| p.homepage.clone()).flatten(), homepage: package.as_ref().and_then(|p| p.homepage.clone()),
file_name: Some(archive_name.to_string()), file_name: Some(archive_name.to_string()),
signature: maybe_signature_data, signature: maybe_signature_data,
signed_url: Some(signed_url.url), signed_url: Some(signed_url.url),
@@ -135,12 +137,15 @@ pub fn try_chunked_uploading(
let response: publish_package_mutation_chunked::ResponseData = let response: publish_package_mutation_chunked::ResponseData =
crate::graphql::execute_query_with_timeout(&registry, &token, timeout, &q)?; crate::graphql::execute_query_with_timeout(&registry, &token, timeout, &q)?;
let mut package_hash = None; if let Some(payload) = response.publish_package {
if let Some(pkg) = response.publish_package { if !payload.success {
if !pkg.success {
return Err(anyhow::anyhow!("Could not publish package")); return Err(anyhow::anyhow!("Could not publish package"));
} }
if let Some(pkg_version) = pkg.package_version {
if let Some(pkg_version) = payload.package_version {
// Here we can assume that the package is *Some*.
let package = package.clone().unwrap();
if wait.is_any() { if wait.is_any() {
let f = wait_for_package_version_to_become_ready( let f = wait_for_package_version_to_become_ready(
&registry, &registry,
@@ -156,22 +161,28 @@ pub fn try_chunked_uploading(
tokio::runtime::Runtime::new().unwrap().block_on(f)?; tokio::runtime::Runtime::new().unwrap().block_on(f)?;
} }
} }
}
if let Some(pkg_hash) = pkg.package_webc {
package_hash = Some(pkg_hash.webc.unwrap().webc_sha256);
}
}
if let Some(pkg) = package { let package_ident = PackageIdent::Named(NamedPackageIdent::from_str(&format!(
println!( "{}@{}",
"🚀 Successfully published package `{}@{}`", package.name, package.version
pkg.name, pkg.version, ))?);
);
println!("🚀 Successfully published package `{}`", package_ident);
return Ok(Some(package_ident));
}
if let Some(pkg_hash) = payload.package_webc {
let package_ident = PackageIdent::Hash(
PackageHash::from_str(&pkg_hash.webc.unwrap().webc_sha256).unwrap(),
);
println!("🚀 Successfully published package `{}`", package_ident);
return Ok(Some(package_ident));
}
unreachable!();
} else { } else {
println!("🚀 Successfully published unnamed package",); unreachable!();
} }
Ok(package_hash)
} }
fn initialize_registry_and_token( fn initialize_registry_and_token(

View File

@@ -56,7 +56,7 @@ async-trait = { version = "^0.1" }
urlencoding = { version = "^2" } urlencoding = { version = "^2" }
serde_derive = { version = "^1" } serde_derive = { version = "^1" }
serde_json = { version = "^1" } serde_json = { version = "^1" }
serde_yaml = { version = "^0.9" } serde_yaml.workspace = true
weezl = { version = "^0.1" } weezl = { version = "^0.1" }
hex = { version = "^0.4" } hex = { version = "^0.4" }
linked_hash_set = { version = "0.1" } linked_hash_set = { version = "0.1" }