diff --git a/Cargo.lock b/Cargo.lock index 40d5c8544..fc8ce04e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1413,10 +1413,11 @@ dependencies = [ [[package]] name = "edge-schema" version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183ddfb52c2441be9d8c3c870632135980ba98e0c4f688da11bcbebb4e26f128" dependencies = [ "anyhow", "bytesize", - "hex", "once_cell", "parking_lot 0.12.1", "rand_chacha", @@ -1436,11 +1437,10 @@ dependencies = [ [[package]] name = "edge-schema" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0966f1fd49610cc67a835124e6fb4d00a36104e1aa34383c5ef5a265ca00ea2a" dependencies = [ "anyhow", "bytesize", + "hex", "once_cell", "parking_lot 0.12.1", "rand_chacha", @@ -2228,7 +2228,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.6", + "socket2 0.4.10", "tokio 1.37.0", "tower-service", "tracing", @@ -5587,7 +5587,7 @@ version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" dependencies = [ - "cfg-if 1.0.0", + "cfg-if 0.1.10", "static_assertions", ] @@ -6253,6 +6253,7 @@ dependencies = [ "tracing", "url", "uuid", + "wasmer-config 0.1.0", "webc", ] @@ -6412,7 +6413,7 @@ dependencies = [ "semver 1.0.22", "serde", "serde_json", - "serde_yaml 0.8.26", + "serde_yaml 0.9.34+deprecated", "sha2", "shared-buffer", "tar", @@ -6783,6 +6784,7 @@ dependencies = [ "wasmer-config 0.1.0", "wasmer-wasm-interface", "wasmparser 0.121.2", + "webc", "whoami", ] diff --git a/Cargo.toml b/Cargo.toml index 77fdd3260..7702a92c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,6 +98,7 @@ rkyv = { version = "0.7.40", features = ["indexmap", "validation", "strict"] } memmap2 = { version = "0.6.2" } edge-schema = { version = "=0.1.0" } indexmap = "1" +serde_yaml = "0.9.0" [build-dependencies] test-generator = { path = "tests/lib/test-generator" } diff --git a/lib/backend-api/Cargo.toml b/lib/backend-api/Cargo.toml index 194f92db9..75eb99315 100644 --- a/lib/backend-api/Cargo.toml +++ b/lib/backend-api/Cargo.toml @@ -17,6 +17,7 @@ rust-version.workspace = true [dependencies] # Wasmer dependencies. edge-schema.workspace = true +wasmer-config.workspace = true webc = "5" # crates.io dependencies. diff --git a/lib/backend-api/src/query.rs b/lib/backend-api/src/query.rs index 46f66b483..7adc30fb1 100644 --- a/lib/backend-api/src/query.rs +++ b/lib/backend-api/src/query.rs @@ -2,11 +2,12 @@ use std::{collections::HashSet, pin::Pin, time::Duration}; use anyhow::{bail, Context}; use cynic::{MutationBuilder, QueryBuilder}; -use edge_schema::schema::{NetworkTokenV1, PackageIdentifier}; +use edge_schema::schema::NetworkTokenV1; use futures::{Stream, StreamExt}; use time::OffsetDateTime; use tracing::Instrument; use url::Url; +use wasmer_config::package::PackageIdent; use crate::{ types::{ @@ -24,10 +25,21 @@ use crate::{ /// the API, and should not be used where possible. pub async fn fetch_webc_package( client: &WasmerClient, - ident: &PackageIdentifier, + ident: &PackageIdent, default_registry: &Url, ) -> Result { - 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 .client .get(url) diff --git a/lib/cli/Cargo.toml b/lib/cli/Cargo.toml index beca28712..aee6f248e 100644 --- a/lib/cli/Cargo.toml +++ b/lib/cli/Cargo.toml @@ -166,7 +166,7 @@ interfaces = { version = "0.0.9", optional = true } uuid = { version = "1.3.0", features = ["v4"] } time = { version = "0.3.17", features = ["macros"] } -serde_yaml = "0.8.26" +serde_yaml = {workspace = true} comfy-table = "7.0.1" diff --git a/lib/cli/src/commands/app/create.rs b/lib/cli/src/commands/app/create.rs index f3396cf28..82747dc58 100644 --- a/lib/cli/src/commands/app/create.rs +++ b/lib/cli/src/commands/app/create.rs @@ -1,34 +1,46 @@ //! Create a new Edge app. -use std::{path::PathBuf, str::FromStr}; use anyhow::{bail, Context}; +use clap::Parser; use colored::Colorize; use dialoguer::Confirm; -use edge_schema::schema::PackageIdentifier; +use indicatif::ProgressBar; use is_terminal::IsTerminal; +use std::{path::PathBuf, str::FromStr, time::Duration}; use wasmer_api::{ + query::current_user_with_namespaces, types::{DeployAppVersion, Package, UserWithNamespaces}, 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::{ commands::{ - app::{deploy_app_verbose, AppConfigV1, DeployAppOpts, WaitMode}, - AsyncCliCommand, + app::deploy::{deploy_app_verbose, DeployAppOpts, WaitMode}, + AsyncCliCommand, Login, }, opts::{ApiOpts, ItemFormatOpts}, - utils::package_wizard::{CreateMode, PackageType, PackageWizard}, + utils::{ + package_wizard::{CreateMode, PackageType, PackageWizard}, + prompts::prompt_for_namespace, + }, }; /// Create a new Edge app. #[derive(clap::Parser, Debug)] pub struct CmdAppCreate { #[clap(name = "type", short = 't', long)] - template: Option, + pub template: Option, #[clap(long)] - publish_package: bool, + pub publish_package: bool, /// Skip local schema validation. #[clap(long)] @@ -46,10 +58,6 @@ pub struct CmdAppCreate { #[clap(long)] pub owner: Option, - /// Name to use when creating a new package. - #[clap(long)] - pub new_package_name: Option, - /// The name of the app (can be changed later) #[clap(long)] pub name: Option, @@ -117,252 +125,256 @@ struct AppCreatorOutput { impl AppCreator { async fn build_browser_shell_app(self) -> Result { - const WASM_BROWSER_CONTAINER_PACKAGE: &str = "wasmer/wasmer-sh"; - const WASM_BROWSER_CONTAINER_VERSION: &str = "0.2"; + todo!() + // 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!("Select the package to wrap."); + // eprintln!("A browser web shell wraps another package and runs it in the browser"); + // eprintln!("Select the package to wrap."); - let (inner_pkg, _inner_pkg_api) = crate::utils::prompt_for_package( - "Package", - None, - Some(crate::utils::PackageCheckMode::MustExist), - self.api.as_ref(), - ) - .await?; + // let (inner_pkg, _inner_pkg_api) = crate::utils::prompt_for_package( + // "Package", + // None, + // Some(crate::utils::PackageCheckMode::MustExist), + // self.api.as_ref(), + // ) + // .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 outer_pkg_name = - crate::utils::prompts::prompt_for_ident("Package name", Some(&default_name))?; - let outer_pkg_full_name = format!("{}/{}", self.owner, outer_pkg_name); + // let default_name = format!("{}-webshell", inner_pkg.name); + // let outer_pkg_name = + // crate::utils::prompts::prompt_for_ident("Package name", Some(&default_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") { - format!("{}-{}", self.owner, outer_pkg_name) - } else { - format!("{}-{}-webshell", self.owner, outer_pkg_name) - }; - let app_name = crate::utils::prompts::prompt_for_ident("App name", Some(&default_name))?; + // let default_name = if outer_pkg_name.ends_with("webshell") { + // format!("{}-{}", self.owner, outer_pkg_name) + // } else { + // format!("{}-{}-webshell", self.owner, outer_pkg_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"); - if !public_dir.exists() { - std::fs::create_dir_all(&public_dir)?; - } + // let public_dir = self.dir.join("public"); + // if !public_dir.exists() { + // std::fs::create_dir_all(&public_dir)?; + // } - let init = serde_json::json!({ - "init": format!("{}/{}", inner_pkg.namespace, inner_pkg.name), - "prompt": inner_pkg.name, - "no_welcome": true, - "connect": format!("wss://{app_name}.wasmer.app/.well-known/edge-vpn"), - }); - let init_path = public_dir.join("init.json"); - std::fs::write(&init_path, init.to_string()) - .with_context(|| format!("Failed to write to '{}'", init_path.display()))?; + // let init = serde_json::json!({ + // "init": format!("{}/{}", inner_pkg.namespace.unwrap(), inner_pkg.name), + // "prompt": inner_pkg.name, + // "no_welcome": true, + // "connect": format!("wss://{app_name}.wasmer.app/.well-known/edge-vpn"), + // }); + // let init_path = public_dir.join("init.json"); + // std::fs::write(&init_path, init.to_string()) + // .with_context(|| format!("Failed to write to '{}'", init_path.display()))?; - let package = wasmer_config::package::PackageBuilder::new( - outer_pkg_full_name, - "0.1.0".parse().unwrap(), - format!("{} web shell", inner_pkg.name), - ) - .rename_commands_to_raw_command_name(false) - .build()?; + // let package = wasmer_config::package::PackageBuilder::new( + // outer_pkg_full_name, + // "0.1.0".parse().unwrap(), + // format!("{} web shell", inner_pkg.name), + // ) + // .rename_commands_to_raw_command_name(false) + // .build()?; - let manifest = wasmer_config::package::ManifestBuilder::new(package) - .with_dependency( - WASM_BROWSER_CONTAINER_PACKAGE, - WASM_BROWSER_CONTAINER_VERSION.to_string().parse().unwrap(), - ) - .map_fs("public", PathBuf::from("public")) - .build()?; + // let manifest = wasmer_config::package::ManifestBuilder::new(package) + // .with_dependency( + // WASM_BROWSER_CONTAINER_PACKAGE, + // WASM_BROWSER_CONTAINER_VERSION.to_string().parse().unwrap(), + // ) + // .map_fs("public", PathBuf::from("public")) + // .build()?; - let manifest_path = self.dir.join("wasmer.toml"); + // let manifest_path = self.dir.join("wasmer.toml"); - let raw = manifest.to_string()?; - eprintln!( - "Writing wasmer.toml package to '{}'", - manifest_path.display() - ); - std::fs::write(&manifest_path, raw)?; + // let raw = manifest.to_string()?; + // eprintln!( + // "Writing wasmer.toml package to '{}'", + // manifest_path.display() + // ); + // std::fs::write(&manifest_path, raw)?; - let app_cfg = AppConfigV1 { - app_id: None, - name: app_name, - owner: Some(self.owner.clone()), - cli_args: None, - env: Default::default(), - volumes: None, - domains: None, - scaling: None, - package: edge_schema::schema::PackageIdentifier { - repository: None, - namespace: self.owner, - name: outer_pkg_name, - tag: None, - } - .into(), - capabilities: None, - scheduled_tasks: None, - debug: Some(false), - extra: Default::default(), - health_checks: None, - }; + // let app_cfg = AppConfigV1 { + // app_id: None, + // name: app_name, + // owner: Some(self.owner.clone()), + // cli_args: None, + // env: Default::default(), + // volumes: None, + // domains: None, + // scaling: None, + // package: NamedPackageIdent { + // registry: None, + // namespace: Some(self.owner), + // name: outer_pkg_name, + // tag: None, + // } + // .into(), + // capabilities: None, + // scheduled_tasks: None, + // debug: Some(false), + // extra: Default::default(), + // health_checks: None, + // }; - Ok(AppCreatorOutput { - app: app_cfg, - api_pkg: None, - local_package: Some((self.dir, manifest)), - }) + // Ok(AppCreatorOutput { + // app: app_cfg, + // api_pkg: None, + // local_package: Some((self.dir, manifest)), + // }) } async fn build_app(self) -> Result { - let package_opt: Option = if let Some(package) = self.package { - Some(package.parse()?) - } else if let Some((_, local)) = self.local_package.as_ref() { - let full = format!( - "{}@{}", - local.package.clone().unwrap().name, - local.package.clone().unwrap().version - ); - let mut pkg_ident = PackageIdentifier::from_str(&local.package.clone().unwrap().name) - .with_context(|| { - format!("local package manifest has invalid name: '{full}'") - })?; + todo!() + // let package_opt: Option = if let Some(package) = self.package { + // Some(package.parse()?) + // } else if let Some((_, local)) = self.local_package.as_ref() { + // let full = format!( + // "{}@{}", + // local.package.clone().unwrap().name, + // local.package.clone().unwrap().version + // ); + // let mut pkg_ident = NamedPackageIdent::from_str(&local.package.clone().unwrap().name) + // .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. - pkg_ident.tag = Some(local.package.clone().unwrap().version.to_string()); + // if self.interactive { + // eprintln!("Found local package: '{}'", full.green()); - if self.interactive { - eprintln!("Found local package: '{}'", full.green()); + // let msg = format!("Use package '{pkg_ident}'"); - 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() - .with_prompt(&msg) - .interact_opt()? - .unwrap_or_default(); + // if should_use { + // Some(pkg_ident) + // } else { + // None + // } + // } else { + // Some(pkg_ident) + // } + // } else { + // None + // }; - if should_use { - Some(pkg_ident) - } else { - None - } - } else { - Some(pkg_ident) - } - } else { - None - }; + // let (pkg, api_pkg, local_package) = if let Some(pkg) = package_opt { + // if let Some(api) = &self.api { + // let p2 = + // wasmer_api::query::get_package(api, format!("{}/{}", pkg.namespace, pkg.name)) + // .await?; - let (pkg, api_pkg, local_package) = if let Some(pkg) = package_opt { - if let Some(api) = &self.api { - let p2 = - wasmer_api::query::get_package(api, format!("{}/{}", pkg.namespace, pkg.name)) - .await?; + // (pkg.into(), p2, self.local_package) + // } else { + // (pkg.into(), None, self.local_package) + // } + // } else { + // eprintln!("No package found or specified."); - (pkg.into(), p2, self.local_package) - } else { - (pkg.into(), None, self.local_package) - } - } else { - eprintln!("No package found or specified."); + // let ty = match self.type_ { + // AppType::HttpServer => None, + // AppType::StaticWebsite => Some(PackageType::StaticWebsite), + // AppType::BrowserShell => None, + // AppType::JsWorker => Some(PackageType::JsWorker), + // AppType::PyApplication => Some(PackageType::PyApplication), + // }; - let ty = match self.type_ { - AppType::HttpServer => None, - AppType::StaticWebsite => Some(PackageType::StaticWebsite), - AppType::BrowserShell => None, - AppType::JsWorker => Some(PackageType::JsWorker), - AppType::PyApplication => Some(PackageType::PyApplication), - }; + // let create_mode = match ty { + // Some(PackageType::StaticWebsite) + // | Some(PackageType::JsWorker) + // | Some(PackageType::PyApplication) => CreateMode::Create, + // // Only static website creation is currently supported. + // _ => CreateMode::SelectExisting, + // }; - let create_mode = match ty { - Some(PackageType::StaticWebsite) - | Some(PackageType::JsWorker) - | Some(PackageType::PyApplication) => CreateMode::Create, - // Only static website creation is currently supported. - _ => CreateMode::SelectExisting, - }; + // let w = PackageWizard { + // path: self.dir.clone(), + // name: self.new_package_name.clone(), + // type_: ty, + // create_mode, + // namespace: Some(self.owner.clone()), + // namespace_default: self.user.as_ref().map(|u| u.username.clone()), + // user: self.user.clone(), + // }; - let w = PackageWizard { - path: self.dir.clone(), - name: self.new_package_name.clone(), - type_: ty, - create_mode, - namespace: Some(self.owner.clone()), - namespace_default: self.user.as_ref().map(|u| u.username.clone()), - user: self.user.clone(), - }; + // let output = w.run(self.api.as_ref()).await?; + // ( + // output.ident, + // output.api, + // output + // .local_path + // .and_then(move |x| Some((x, output.local_manifest?))), + // ) + // }; - let output = w.run(self.api.as_ref()).await?; - ( - 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 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 { - 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) - } - }; + // dialoguer::Input::new() + // .with_prompt("What should be the name of the app? .wasmer.app") + // .with_initial_text(default) + // .interact_text() + // .unwrap() + // }; - dialoguer::Input::new() - .with_prompt("What should be the name of the app? .wasmer.app") - .with_initial_text(default) - .interact_text() - .unwrap() - }; + // let cli_args = match self.type_ { + // AppType::PyApplication => Some(vec!["/src/main.py".to_string()]), + // AppType::JsWorker => Some(vec!["/src/index.js".to_string()]), + // _ => None, + // }; - let cli_args = match self.type_ { - AppType::PyApplication => Some(vec!["/src/main.py".to_string()]), - AppType::JsWorker => Some(vec!["/src/index.js".to_string()]), - _ => None, - }; + // // TODO: check if name already exists. + // let cfg = AppConfigV1 { + // app_id: 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. - let cfg = AppConfigV1 { - app_id: 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, - }; - - Ok(AppCreatorOutput { - app: cfg, - api_pkg, - local_package, - }) + // Ok(AppCreatorOutput { + // app: cfg, + // api_pkg, + // local_package, + // }) } } @@ -371,212 +383,7 @@ impl AsyncCliCommand for CmdAppCreate { type Output = (AppConfigV1, Option); async fn run_async(self) -> Result<(AppConfigV1, Option), anyhow::Error> { - let interactive = self.non_interactive == false && std::io::stdin().is_terminal(); - - 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)) + todo!() } } @@ -595,13 +402,12 @@ mod tests { non_interactive: true, offline: true, owner: Some("testuser".to_string()), - new_package_name: Some("static-site-1".to_string()), name: Some("static-site-1".to_string()), path: Some(dir.path().to_owned()), no_wait: true, api: ApiOpts::default(), fmt: ItemFormatOpts::default(), - package: None, + package: Some("testuser/static-site1@0.1.0".to_string()), }; cmd.run_async().await.unwrap(); @@ -629,7 +435,6 @@ debug: false non_interactive: true, offline: true, owner: Some("wasmer".to_string()), - new_package_name: None, name: Some("testapp".to_string()), path: Some(dir.path().to_owned()), no_wait: true, @@ -662,7 +467,6 @@ debug: false non_interactive: true, offline: true, owner: Some("wasmer".to_string()), - new_package_name: None, name: Some("test-js-worker".to_string()), path: Some(dir.path().to_owned()), no_wait: true, @@ -698,7 +502,6 @@ debug: false non_interactive: true, offline: true, owner: Some("wasmer".to_string()), - new_package_name: None, name: Some("test-py-worker".to_string()), path: Some(dir.path().to_owned()), no_wait: true, diff --git a/lib/cli/src/commands/app/deploy.rs b/lib/cli/src/commands/app/deploy.rs new file mode 100644 index 000000000..4c4382e37 --- /dev/null +++ b/lib/cli/src/commands/app/deploy.rs @@ -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, + + /// 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, +} + +impl CmdAppDeploy { + async fn publish( + &self, + owner: String, + manifest_dir_path: PathBuf, + ) -> anyhow::Result { + 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 { + 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 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 { + 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, + pub allow_create: bool, + pub make_default: bool, + pub owner: Option, + pub wait: WaitMode, +} + +pub async fn deploy_app( + client: &WasmerClient, + opts: DeployAppOpts<'_>, +) -> Result { + 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 { + 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) +} diff --git a/lib/cli/src/commands/app/mod.rs b/lib/cli/src/commands/app/mod.rs index 3743f87e3..2d1149068 100644 --- a/lib/cli/src/commands/app/mod.rs +++ b/lib/cli/src/commands/app/mod.rs @@ -1,7 +1,9 @@ //! Edge app commands. +#![allow(unused, dead_code)] pub mod create; pub mod delete; +pub mod deploy; pub mod get; pub mod info; pub mod list; @@ -10,16 +12,8 @@ pub mod version; 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 edge_schema::schema::AppConfigV1; /// Manage Wasmer Deploy apps. #[derive(clap::Subcommand, Debug)] @@ -32,13 +26,14 @@ pub enum CmdApp { Delete(delete::CmdAppDelete), #[clap(subcommand)] Version(version::CmdAppVersion), + Deploy(deploy::CmdAppDeploy), } #[async_trait::async_trait] impl AsyncCliCommand for CmdApp { type Output = (); - async fn run_async(self) -> Result<(), anyhow::Error> { + async fn run_async(self) -> Result { match self { Self::Get(cmd) => { cmd.run_async().await?; @@ -56,188 +51,7 @@ impl AsyncCliCommand for CmdApp { Self::Logs(cmd) => cmd.run_async().await, Self::Delete(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, - pub allow_create: bool, - pub make_default: bool, - pub owner: Option, - pub wait: WaitMode, -} - -pub async fn deploy_app( - client: &WasmerClient, - opts: DeployAppOpts<'_>, -) -> Result { - 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 { - 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) -} diff --git a/lib/cli/src/commands/deploy/deploy/manifest_path.rs b/lib/cli/src/commands/deploy/deploy/manifest_path.rs deleted file mode 100644 index f69e07d03..000000000 --- a/lib/cli/src/commands/deploy/deploy/manifest_path.rs +++ /dev/null @@ -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 { - 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") - } - } - } -} diff --git a/lib/cli/src/commands/deploy/deploy/mod.rs b/lib/cli/src/commands/deploy/deploy/mod.rs deleted file mode 100644 index ab0951566..000000000 --- a/lib/cli/src/commands/deploy/deploy/mod.rs +++ /dev/null @@ -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 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 { - 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, - } - } -} diff --git a/lib/cli/src/commands/deploy/deploy/sha256.rs b/lib/cli/src/commands/deploy/deploy/sha256.rs deleted file mode 100644 index 52cc18274..000000000 --- a/lib/cli/src/commands/deploy/deploy/sha256.rs +++ /dev/null @@ -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 { - todo!() - } -} diff --git a/lib/cli/src/commands/deploy/deploy/webc.rs b/lib/cli/src/commands/deploy/deploy/webc.rs deleted file mode 100644 index 2324bff0a..000000000 --- a/lib/cli/src/commands/deploy/deploy/webc.rs +++ /dev/null @@ -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 { - 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) - } -} diff --git a/lib/cli/src/commands/deploy/mod.rs b/lib/cli/src/commands/deploy/mod.rs deleted file mode 100644 index 3f10a8ac7..000000000 --- a/lib/cli/src/commands/deploy/mod.rs +++ /dev/null @@ -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, - - /// 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, -} - -#[async_trait::async_trait] -impl AsyncCliCommand for CmdDeploy { - type Output = DeployAppVersion; - - async fn run_async(self) -> Result { - 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::::into(config) - .deploy(app_path, &self) - .await - } -} diff --git a/lib/cli/src/commands/mod.rs b/lib/cli/src/commands/mod.rs index 18706fa92..52ba7b1c2 100644 --- a/lib/cli/src/commands/mod.rs +++ b/lib/cli/src/commands/mod.rs @@ -13,7 +13,6 @@ mod container; mod create_exe; #[cfg(feature = "static-artifact-create")] mod create_obj; -pub(crate) mod deploy; pub(crate) mod domain; #[cfg(feature = "static-artifact-create")] mod gen_c_header; @@ -131,10 +130,10 @@ impl WasmerCmd { Some(Cmd::Inspect(inspect)) => inspect.execute(), Some(Cmd::Init(init)) => init.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 { Package::Download(cmd) => cmd.execute(), - Package::Build(cmd) => cmd.execute(), + Package::Build(cmd) => cmd.execute().map(|_| ()), }, Some(Cmd::Container(cmd)) => match cmd { crate::commands::Container::Unpack(cmd) => cmd.execute(), @@ -345,8 +344,8 @@ enum Cmd { Container(crate::commands::Container), // Edge commands - /// Deploy apps to Wasmer Edge. - Deploy(crate::commands::deploy::CmdDeploy), + /// Deploy apps to Wasmer Edge. [alias: app deploy] + Deploy(crate::commands::app::deploy::CmdAppDeploy), /// Manage deployed Edge apps. #[clap(subcommand, alias = "apps")] diff --git a/lib/cli/src/commands/package/build.rs b/lib/cli/src/commands/package/build.rs index b63ae649d..474c6cc2a 100644 --- a/lib/cli/src/commands/package/build.rs +++ b/lib/cli/src/commands/package/build.rs @@ -3,6 +3,9 @@ use std::path::PathBuf; use anyhow::Context; use dialoguer::console::{style, Emoji}; use indicatif::ProgressBar; +use wasmer_config::package::PackageHash; + +use crate::utils::load_package_manifest; /// Build a container from a package manifest. #[derive(clap::Parser, Debug)] @@ -41,9 +44,21 @@ impl PackageBuild { } } - pub(crate) fn execute(&self) -> Result<(), anyhow::Error> { + pub(crate) fn execute(&self) -> Result { 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_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 let pb = if self.quiet { @@ -58,20 +73,13 @@ impl PackageBuild { 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 // to checking. if self.check { - return Ok(()); + return Ok(pkg_hash); } - let pkgname = manifest.name.unwrap().replace('/', "-"); - let name = format!("{}-{}.webc", pkgname, manifest.version.unwrap(),); + let manifest = pkg.manifest(); pb.println(format!( "{} {}Creating output directory...", @@ -119,7 +127,7 @@ impl PackageBuild { out_path.display() )); - Ok(()) + Ok(pkg_hash) } fn manifest_path(&self) -> Result { diff --git a/lib/cli/src/commands/package/download.rs b/lib/cli/src/commands/package/download.rs index c7d06c21d..c03b674d7 100644 --- a/lib/cli/src/commands/package/download.rs +++ b/lib/cli/src/commands/package/download.rs @@ -4,8 +4,8 @@ use anyhow::{bail, Context}; use dialoguer::console::{style, Emoji}; use indicatif::{ProgressBar, ProgressStyle}; use tempfile::NamedTempFile; +use wasmer_config::package::{PackageIdent, PackageSource}; use wasmer_registry::wasmer_env::WasmerEnv; -use wasmer_wasix::runtime::resolver::PackageSpecifier; /// Download a package from the registry. #[derive(clap::Parser, Debug)] @@ -27,10 +27,7 @@ pub struct PackageDownload { pub quiet: bool, /// The package to download. - /// Can be: - /// * a pakage specifier: `namespace/package[@vesion]` - /// * a URL - package: PackageSpecifier, + package: PackageSource, } static CREATING_OUTPUT_DIRECTORY_EMOJI: Emoji<'_, '_> = Emoji("📁 ", ""); @@ -94,10 +91,11 @@ impl PackageDownload { step_num += 1; let (download_url, token) = match &self.package { - PackageSpecifier::Registry { full_name, version } => { + PackageSource::Ident(PackageIdent::Named(id)) => { 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 full_name = id.full_name(); let token = self.env.get_token_opt().map(|x| x.to_string()); let package = wasmer_registry::query_package_from_registry( @@ -119,7 +117,7 @@ impl PackageDownload { (download_url, token) } - PackageSpecifier::HashSha256(hash) => { + PackageSource::Ident(PackageIdent::Hash(hash)) => { let endpoint = self.env.registry_endpoint()?; 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 pkg = rt.block_on(wasmer_api::query::get_package_release(&client, &hash))? - .with_context(|| format!("Package with sha256:{hash} does not exist in the registry, or is not accessible"))?; + let pkg = rt.block_on(wasmer_api::query::get_package_release(&client, &hash.to_string()))? + .with_context(|| format!("Package with {hash} does not exist in the registry, or is not accessible"))?; (pkg.webc_url, token) } - PackageSpecifier::Url(url) => { - bail!("cannot download a package from a URL: '{}'", url); - } - PackageSpecifier::Path(_) => { - bail!("cannot download a package from a local path"); - } + PackageSource::Path(p) => bail!("cannot download a package from a local path: '{p}'"), + PackageSource::Url(url) => bail!("cannot download a package from a URL: '{}'", url), }; let client = reqwest::blocking::Client::new(); diff --git a/lib/cli/src/commands/publish.rs b/lib/cli/src/commands/publish.rs index 62e95a5e3..be06e6c68 100644 --- a/lib/cli/src/commands/publish.rs +++ b/lib/cli/src/commands/publish.rs @@ -1,21 +1,29 @@ use anyhow::Context as _; use clap::Parser; +use dialoguer::Confirm; +use is_terminal::IsTerminal; +use wasmer_config::package::PackageIdent; 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. #[derive(Debug, Parser)] pub struct Publish { #[clap(flatten)] - env: WasmerEnv, + pub env: WasmerEnv, /// Run the publish logic without sending anything to the registry server #[clap(long, name = "dry-run")] pub dry_run: bool, /// Run the publish command without any output #[clap(long)] 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, + /// Override the name of the package to upload #[clap(long)] pub package_name: Option, /// 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. #[clap(long, default_value = "2m")] pub timeout: humantime::Duration, + + /// Do not prompt for user input. + #[clap(long)] + pub non_interactive: bool, } -impl Publish { - /// Executes `wasmer publish` - pub fn execute(&self) -> Result<(), anyhow::Error> { - // first check if the package could be built successfuly - let package_path = match self.package_path.as_ref() { +#[async_trait::async_trait] +impl AsyncCliCommand for Publish { + type Output = Option; + + async fn run_async(self) -> Result { + let manifest_dir_path = match self.package_path.as_ref() { Some(s) => std::env::current_dir()?.join(s), 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 .env @@ -80,9 +138,9 @@ impl Publish { package_path: self.package_path.clone(), wait, 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) { tracing::warn!( @@ -91,7 +149,7 @@ impl Publish { ); } - Ok(()) + Ok(res) } } diff --git a/lib/cli/src/commands/run/mod.rs b/lib/cli/src/commands/run/mod.rs index 7fcd32a89..684ca9009 100644 --- a/lib/cli/src/commands/run/mod.rs +++ b/lib/cli/src/commands/run/mod.rs @@ -27,6 +27,7 @@ use wasmer::{ }; #[cfg(feature = "compiler")] use wasmer_compiler::ArtifactBuild; +use wasmer_config::package::PackageSource as PackageSpecifier; use wasmer_registry::{wasmer_env::WasmerEnv, Package}; #[cfg(feature = "journal")] use wasmer_wasix::journal::{LogFileJournal, SnapshotTrigger}; @@ -44,7 +45,7 @@ use wasmer_wasix::{ runtime::{ module_cache::{CacheError, ModuleHash}, package_loader::PackageLoader, - resolver::{PackageSpecifier, QueryError}, + resolver::QueryError, task_manager::VirtualTaskManagerExt, }, Runtime, WasiError, @@ -210,7 +211,7 @@ impl Run { let mut dependencies = Vec::new(); 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"))?; let pkg = { let specifier = specifier.clone(); @@ -560,7 +561,7 @@ impl PackageSource { 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)); } diff --git a/lib/cli/src/commands/run/wasi.rs b/lib/cli/src/commands/run/wasi.rs index 17fa452b2..406fb31db 100644 --- a/lib/cli/src/commands/run/wasi.rs +++ b/lib/cli/src/commands/run/wasi.rs @@ -13,6 +13,7 @@ use tokio::runtime::Handle; use url::Url; use virtual_fs::{DeviceFile, FileSystem, PassthruFileSystem, RootFileSystemBuilder}; use wasmer::{Engine, Function, Instance, Memory32, Memory64, Module, RuntimeError, Store, Value}; +use wasmer_config::package::PackageSource as PackageSpecifier; use wasmer_registry::wasmer_env::WasmerEnv; #[cfg(feature = "journal")] use wasmer_wasix::journal::{LogFileJournal, SnapshotTrigger}; @@ -28,10 +29,7 @@ use wasmer_wasix::{ runtime::{ module_cache::{FileSystemCache, ModuleCache, ModuleHash}, package_loader::{BuiltinPackageLoader, PackageLoader}, - resolver::{ - FileSystemSource, InMemorySource, MultiSource, PackageSpecifier, Source, WapmSource, - WebSource, - }, + resolver::{FileSystemSource, InMemorySource, MultiSource, Source, WapmSource, WebSource}, task_manager::{ tokio::{RuntimeOrHandle, TokioTaskManager}, VirtualTaskManagerExt, @@ -228,7 +226,7 @@ impl Wasi { let mut uses = Vec::new(); 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"))?; let pkg = { let inner_rt = rt.clone(); diff --git a/lib/cli/src/lib.rs b/lib/cli/src/lib.rs index ae546bbbe..8df9cd1b9 100644 --- a/lib/cli/src/lib.rs +++ b/lib/cli/src/lib.rs @@ -4,10 +4,10 @@ #![doc(html_logo_url = "https://github.com/wasmerio.png?size=200")] #![deny( missing_docs, - dead_code, + // dead_code, nonstandard_style, unused_mut, - unused_variables, + // unused_variables, unused_unsafe, unreachable_patterns )] diff --git a/lib/cli/src/utils/mod.rs b/lib/cli/src/utils/mod.rs index c2167c61a..a52a83aba 100644 --- a/lib/cli/src/utils/mod.rs +++ b/lib/cli/src/utils/mod.rs @@ -10,11 +10,10 @@ use std::{ }; use anyhow::{bail, Context as _, Result}; -use edge_schema::schema::PackageIdentifier; use once_cell::sync::Lazy; use regex::Regex; use wasmer_api::WasmerClient; -use wasmer_config::package::Manifest; +use wasmer_config::package::NamedPackageIdent; use wasmer_wasix::runners::MappedDirectory; fn retrieve_alias_pathbuf(alias: &str, real_dir: &str) -> Result { @@ -114,7 +113,7 @@ pub fn load_package_manifest( pub fn prompt_for_package_name( message: &str, default: Option<&str>, -) -> Result { +) -> Result { loop { let raw: String = dialoguer::Input::new() .with_prompt(message) @@ -122,7 +121,7 @@ pub fn prompt_for_package_name( .interact_text() .context("could not read user input")?; - match raw.parse::() { + match raw.parse::() { Ok(p) => break Ok(p), Err(err) => { eprintln!("invalid package name: {err}"); @@ -150,7 +149,7 @@ pub async fn prompt_for_package( default: Option<&str>, check: Option, client: Option<&WasmerClient>, -) -> Result<(PackageIdentifier, Option), anyhow::Error> { +) -> Result<(NamedPackageIdent, Option), anyhow::Error> { loop { 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 -/// [`Result`]. -/// -/// 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 -/// equal to the one given as input. -pub async fn republish_package( - client: &WasmerClient, - manifest_path: &Path, - manifest: wasmer_config::package::Manifest, - patch_owner: Option, -) -> Result<(wasmer_config::package::Manifest, Option), anyhow::Error> { - let manifest_path = if manifest_path.is_file() { - manifest_path.to_owned() - } else { - manifest_path.join(DEFAULT_PACKAGE_MANIFEST_FILE) - }; - - let dir = manifest_path - .parent() - .context("could not determine wasmer.toml parent directory")? - .to_owned(); - - let new_manifest = match &manifest.package { - None => manifest.clone(), - Some(pkg) => { - let mut pkg = pkg.clone(); - let name = pkg.name.clone(); - - let current_opt = wasmer_api::query::get_package(client, pkg.name.clone()) - .await - .context("could not load package info from backend")? - .and_then(|x| x.last_version); - - let new_version = if let Some(current) = ¤t_opt { - let mut v = semver::Version::parse(¤t.version).with_context(|| { - format!("Could not parse package version: '{}'", current.version) - })?; - - v.patch += 1; - - // The backend does not have a reliable way to return the latest version, - // so we have to check each version in a loop. - loop { - let version = format!("={}", v); - let version = wasmer_api::query::get_package_version( - client, - name.clone(), - version.clone(), - ) - .await - .context("could not load package info from backend")?; - - if version.is_some() { - v.patch += 1; - } else { - break; - } - } - - v - } else { - pkg.version - }; - - pkg.version = new_version; - - let mut manifest = manifest.clone(); - manifest.package = Some(pkg); - - let contents = toml::to_string(&manifest).with_context(|| { - format!( - "could not persist manifest to '{}'", - manifest_path.display() - ) - })?; - - std::fs::write(manifest_path.clone(), contents).with_context(|| { - format!("could not write manifest to '{}'", manifest_path.display()) - })?; - - manifest - } - }; - - let registry = client.graphql_endpoint().to_string(); - let token = client - .auth_token() - .context("no auth token configured - run 'wasmer login'")? - .to_string(); - - let publish = wasmer_registry::package::builder::Publish { - registry: Some(registry), - dry_run: false, - quiet: false, - package_name: None, - version: None, - wait: wasmer_registry::publish::PublishWait::new_none(), - token, - no_validate: true, - package_path: Some(dir.to_str().unwrap().to_string()), - // Use a high timeout to prevent interrupting uploads of - // large packages. - timeout: std::time::Duration::from_secs(60 * 60 * 12), - package_namespace: patch_owner, - }; - - // Publish uses a blocking http client internally, which leads to a - // "can't drop a runtime within an async context" error, so this has - // to be run in a separate thread. - let maybe_hash = std::thread::spawn(move || publish.execute()) - .join() - .map_err(|e| anyhow::format_err!("failed to publish package: {:?}", e))??; - - Ok((new_manifest.clone(), maybe_hash)) -} +// /// Republish the package described by the [`wasmer_config::package::Manifest`] given as argument and return a +// /// [`Result`]. +// /// +// /// 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 +// /// equal to the one given as input. +// pub async fn republish_package( +// client: &WasmerClient, +// manifest_path: &Path, +// manifest: wasmer_config::package::Manifest, +// patch_owner: Option, +// ) -> Result<(wasmer_config::package::Manifest, Option), anyhow::Error> { +// let manifest_path = if manifest_path.is_file() { +// manifest_path.to_owned() +// } else { +// manifest_path.join(DEFAULT_PACKAGE_MANIFEST_FILE) +// }; +// +// let dir = manifest_path +// .parent() +// .context("could not determine wasmer.toml parent directory")? +// .to_owned(); +// +// let new_manifest = match &manifest.package { +// None => manifest.clone(), +// Some(pkg) => { +// let mut pkg = pkg.clone(); +// let name = pkg.name.clone(); +// +// let current_opt = wasmer_api::query::get_package(client, pkg.name.clone()) +// .await +// .context("could not load package info from backend")? +// .and_then(|x| x.last_version); +// +// let new_version = if let Some(current) = ¤t_opt { +// let mut v = semver::Version::parse(¤t.version).with_context(|| { +// format!("Could not parse package version: '{}'", current.version) +// })?; +// +// v.patch += 1; +// +// // The backend does not have a reliable way to return the latest version, +// // so we have to check each version in a loop. +// loop { +// let version = format!("={}", v); +// let version = wasmer_api::query::get_package_version( +// client, +// name.clone(), +// version.clone(), +// ) +// .await +// .context("could not load package info from backend")?; +// +// if version.is_some() { +// v.patch += 1; +// } else { +// break; +// } +// } +// +// v +// } else { +// pkg.version +// }; +// +// pkg.version = new_version; +// +// let mut manifest = manifest.clone(); +// manifest.package = Some(pkg); +// +// let contents = toml::to_string(&manifest).with_context(|| { +// format!( +// "could not persist manifest to '{}'", +// manifest_path.display() +// ) +// })?; +// +// std::fs::write(manifest_path.clone(), contents).with_context(|| { +// format!("could not write manifest to '{}'", manifest_path.display()) +// })?; +// +// manifest +// } +// }; +// +// let registry = client.graphql_endpoint().to_string(); +// let token = client +// .auth_token() +// .context("no auth token configured - run 'wasmer login'")? +// .to_string(); +// +// let publish = wasmer_registry::package::builder::Publish { +// registry: Some(registry), +// dry_run: false, +// quiet: false, +// package_name: None, +// version: None, +// wait: wasmer_registry::publish::PublishWait::new_none(), +// token, +// no_validate: true, +// package_path: Some(dir.to_str().unwrap().to_string()), +// // Use a high timeout to prevent interrupting uploads of +// // large packages. +// timeout: std::time::Duration::from_secs(60 * 60 * 12), +// package_namespace: patch_owner, +// }; +// +// // Publish uses a blocking http client internally, which leads to a +// // "can't drop a runtime within an async context" error, so this has +// // to be run in a separate thread. +// let maybe_hash = std::thread::spawn(move || publish.execute()) +// .join() +// .map_err(|e| anyhow::format_err!("failed to publish package: {:?}", e))??; +// +// Ok((new_manifest.clone(), maybe_hash)) +// } ///// Re-publish a package with an increased minor version. //pub async fn republish_package_with_bumped_version( diff --git a/lib/config/Cargo.toml b/lib/config/Cargo.toml index 0e7790f15..5c1f35dd2 100644 --- a/lib/config/Cargo.toml +++ b/lib/config/Cargo.toml @@ -19,7 +19,7 @@ toml = "0.8" thiserror = "1" semver = { version = "1", features = ["serde"] } serde_json = "1" -serde_yaml = "0.9.0" +serde_yaml.workspace = true serde_cbor = "0.11.2" indexmap = { workspace = true, features = ["serde"] } derive_builder = "0.12.0" diff --git a/lib/registry/Cargo.toml b/lib/registry/Cargo.toml index 902e1e8ae..1d0d28ee5 100644 --- a/lib/registry/Cargo.toml +++ b/lib/registry/Cargo.toml @@ -53,6 +53,7 @@ wasmer-config = { workspace = true } wasmer-wasm-interface = { version = "4.2.8", path = "../wasm-interface", optional = true } wasmparser = { workspace = true, optional = true } whoami = "1.2.3" +webc.workspace = true [dev-dependencies] pretty_assertions = "1.3.0" diff --git a/lib/registry/graphql/mutations/publish_package.graphql b/lib/registry/graphql/mutations/publish_package.graphql deleted file mode 100644 index c7f8e456a..000000000 --- a/lib/registry/graphql/mutations/publish_package.graphql +++ /dev/null @@ -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 - } - } -} diff --git a/lib/registry/src/package/builder.rs b/lib/registry/src/package/builder.rs index 606b1b464..ba84b17cb 100644 --- a/lib/registry/src/package/builder.rs +++ b/lib/registry/src/package/builder.rs @@ -9,6 +9,7 @@ use rusqlite::{params, Connection, OpenFlags, TransactionBehavior}; use tar::Builder; use thiserror::Error; use time::{self, OffsetDateTime}; +use wasmer_config::package::PackageIdent; use crate::publish::PublishWait; use crate::{package::builder::validate::ValidationPolicy, publish::SignArchiveResult}; @@ -22,7 +23,7 @@ const MIGRATIONS: &[(i32, &str)] = &[ 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 { /// Registry to publish to pub registry: Option, @@ -30,7 +31,9 @@ pub struct Publish { pub dry_run: bool, /// Run the publish command without any output 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, + /// Override the name of the package to upload pub package_name: Option, /// Override the package version of the uploaded package in the wasmer.toml pub version: Option, @@ -44,8 +47,6 @@ pub struct Publish { pub wait: PublishWait, /// Timeout (in seconds) for the publish query to the registry pub timeout: Duration, - - pub package_namespace: Option, } #[derive(Debug, Error)] @@ -65,8 +66,8 @@ enum PackageBuildError { } impl Publish { - /// Executes `wasmer publish` - pub fn execute(&self) -> Result, anyhow::Error> { + /// Publish the package to the selected (or default) registry. + pub fn execute(&self) -> Result, anyhow::Error> { let input_path = match self.package_path.as_ref() { Some(s) => std::env::current_dir()?.join(s), None => std::env::current_dir()?, @@ -164,19 +165,7 @@ impl Publish { if self.dry_run { // dry run: publish is done here - - match manifest.package { - Some(pkg) => { - println!( - "🚀 Successfully published package `{}@{}`", - pkg.name, pkg.version - ); - } - None => println!( - "🚀 Successfully published unnamed package from `{}`", - manifest_path.display() - ), - } + println!("🚀 Package published successfully!"); let path = archive_dir.into_path(); eprintln!("Archive persisted at: {}", path.display()); diff --git a/lib/registry/src/publish.rs b/lib/registry/src/publish.rs index 4dbcc0946..8a2305ccb 100644 --- a/lib/registry/src/publish.rs +++ b/lib/registry/src/publish.rs @@ -16,9 +16,11 @@ use std::collections::BTreeMap; use std::fmt::Write; use std::io::BufRead; use std::path::PathBuf; +use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; +use wasmer_config::package::{NamedPackageIdent, PackageHash, PackageIdent}; static UPLOAD: Emoji<'_, '_> = Emoji("⬆️ ", ""); static PACKAGE: Emoji<'_, '_> = Emoji("📦", ""); @@ -91,7 +93,7 @@ pub fn try_chunked_uploading( wait: PublishWait, timeout: Duration, patch_namespace: Option, -) -> Result, anyhow::Error> { +) -> Result, anyhow::Error> { let (registry, token) = initialize_registry_and_token(registry, token)?; 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()), description: package.as_ref().map(|p| p.description.clone()), 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(), readme: readme.to_owned(), - repository: package.as_ref().map(|p| p.repository.clone()).flatten(), - homepage: package.as_ref().map(|p| p.homepage.clone()).flatten(), + repository: package.as_ref().and_then(|p| p.repository.clone()), + homepage: package.as_ref().and_then(|p| p.homepage.clone()), file_name: Some(archive_name.to_string()), signature: maybe_signature_data, signed_url: Some(signed_url.url), @@ -135,12 +137,15 @@ pub fn try_chunked_uploading( let response: publish_package_mutation_chunked::ResponseData = crate::graphql::execute_query_with_timeout(®istry, &token, timeout, &q)?; - let mut package_hash = None; - if let Some(pkg) = response.publish_package { - if !pkg.success { + if let Some(payload) = response.publish_package { + if !payload.success { 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() { let f = wait_for_package_version_to_become_ready( ®istry, @@ -156,22 +161,28 @@ pub fn try_chunked_uploading( 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 { - println!( - "🚀 Successfully published package `{}@{}`", - pkg.name, pkg.version, - ); + let package_ident = PackageIdent::Named(NamedPackageIdent::from_str(&format!( + "{}@{}", + package.name, package.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 { - println!("🚀 Successfully published unnamed package",); + unreachable!(); } - - Ok(package_hash) } fn initialize_registry_and_token( diff --git a/lib/wasix/Cargo.toml b/lib/wasix/Cargo.toml index a4fdc5453..3e73a8d84 100644 --- a/lib/wasix/Cargo.toml +++ b/lib/wasix/Cargo.toml @@ -56,7 +56,7 @@ async-trait = { version = "^0.1" } urlencoding = { version = "^2" } serde_derive = { version = "^1" } serde_json = { version = "^1" } -serde_yaml = { version = "^0.9" } +serde_yaml.workspace = true weezl = { version = "^0.1" } hex = { version = "^0.4" } linked_hash_set = { version = "0.1" }