mirror of
https://github.com/mii443/wasmer.git
synced 2025-12-08 21:58:20 +00:00
deploy flow dx
This commit is contained in:
14
Cargo.lock
generated
14
Cargo.lock
generated
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -17,6 +17,7 @@ rust-version.workspace = true
|
||||
[dependencies]
|
||||
# Wasmer dependencies.
|
||||
edge-schema.workspace = true
|
||||
wasmer-config.workspace = true
|
||||
webc = "5"
|
||||
|
||||
# crates.io dependencies.
|
||||
|
||||
@@ -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<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
|
||||
.client
|
||||
.get(url)
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -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<AppType>,
|
||||
pub template: Option<AppType>,
|
||||
|
||||
#[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<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)
|
||||
#[clap(long)]
|
||||
pub name: Option<String>,
|
||||
@@ -117,252 +125,256 @@ struct AppCreatorOutput {
|
||||
|
||||
impl AppCreator {
|
||||
async fn build_browser_shell_app(self) -> Result<AppCreatorOutput, anyhow::Error> {
|
||||
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<AppCreatorOutput, anyhow::Error> {
|
||||
let package_opt: Option<PackageIdentifier> = 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<PackageIdent> = 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? <NAME>.wasmer.app")
|
||||
// .with_initial_text(default)
|
||||
// .interact_text()
|
||||
// .unwrap()
|
||||
// };
|
||||
|
||||
dialoguer::Input::new()
|
||||
.with_prompt("What should be the name of the app? <NAME>.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<DeployAppVersion>);
|
||||
|
||||
async fn run_async(self) -> Result<(AppConfigV1, Option<DeployAppVersion>), 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,
|
||||
|
||||
448
lib/cli/src/commands/app/deploy.rs
Normal file
448
lib/cli/src/commands/app/deploy.rs
Normal 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)
|
||||
}
|
||||
@@ -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<Self::Output, anyhow::Error> {
|
||||
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<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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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")]
|
||||
|
||||
@@ -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<PackageHash, anyhow::Error> {
|
||||
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<PathBuf, anyhow::Error> {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<String>,
|
||||
/// Override the name of the package to upload
|
||||
#[clap(long)]
|
||||
pub package_name: Option<String>,
|
||||
/// 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<PackageIdent>;
|
||||
|
||||
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),
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
)]
|
||||
|
||||
@@ -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<MappedDirectory> {
|
||||
@@ -114,7 +113,7 @@ pub fn load_package_manifest(
|
||||
pub fn prompt_for_package_name(
|
||||
message: &str,
|
||||
default: Option<&str>,
|
||||
) -> Result<PackageIdentifier, anyhow::Error> {
|
||||
) -> Result<NamedPackageIdent, anyhow::Error> {
|
||||
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::<PackageIdentifier>() {
|
||||
match raw.parse::<NamedPackageIdent>() {
|
||||
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<PackageCheckMode>,
|
||||
client: Option<&WasmerClient>,
|
||||
) -> Result<(PackageIdentifier, Option<wasmer_api::types::Package>), anyhow::Error> {
|
||||
) -> Result<(NamedPackageIdent, Option<wasmer_api::types::Package>), 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<wasmer_config::package::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
|
||||
/// 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<String>,
|
||||
) -> Result<(wasmer_config::package::Manifest, Option<String>), 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<wasmer_config::package::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
|
||||
// /// 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<String>,
|
||||
// ) -> Result<(wasmer_config::package::Manifest, Option<PackageIdent>), 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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
@@ -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<String>,
|
||||
/// Override the name of the package to upload
|
||||
pub package_name: Option<String>,
|
||||
/// Override the package version of the uploaded package in the wasmer.toml
|
||||
pub version: Option<semver::Version>,
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -65,8 +66,8 @@ enum PackageBuildError {
|
||||
}
|
||||
|
||||
impl Publish {
|
||||
/// Executes `wasmer publish`
|
||||
pub fn execute(&self) -> Result<Option<String>, anyhow::Error> {
|
||||
/// Publish the package to the selected (or default) registry.
|
||||
pub fn execute(&self) -> Result<Option<PackageIdent>, 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());
|
||||
|
||||
@@ -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<String>,
|
||||
) -> Result<Option<String>, anyhow::Error> {
|
||||
) -> Result<Option<PackageIdent>, 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(
|
||||
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user