diff --git a/.gitignore b/.gitignore
index b0e4ad47..98c533fe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,5 @@ docker-compose.yml
# Host config files
host_config.json
+# No dumps in git
+*.dump
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index 134aa0fc..58260ca6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4267,6 +4267,7 @@ dependencies = [
"tokio",
"tokio-stream",
"tokio-tar",
+ "tokio-util 0.7.8",
"toml 0.7.4",
"wadm",
"walkdir",
diff --git a/Cargo.toml b/Cargo.toml
index d8011122..270ef80d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -27,6 +27,7 @@ ctrlc = { workspace = true }
dirs = { workspace = true }
env_logger = { workspace = true }
envmnt = { workspace = true }
+futures = { workspace = true }
indicatif = { workspace = true }
log = { workspace = true }
nkeys = { workspace = true }
@@ -134,6 +135,7 @@ thiserror = "1.0"
tokio = { version = "1.28.2", default-features = false, features = ["fs"] }
tokio-stream = "0.1"
tokio-tar = "0.3"
+tokio-util = "0.7.8"
toml = "0.7.4"
wadm = "0.4.0"
walkdir = "2.3"
diff --git a/Makefile b/Makefile
index f924ff3f..3dfc3eb3 100644
--- a/Makefile
+++ b/Makefile
@@ -30,7 +30,7 @@ build-watch: ## Continuously build the project
test: ## Run unit test suite
@$(CARGO) nextest run $(CARGO_TEST_TARGET) --no-fail-fast --bin wash
- @$(CARGO) nextest run $(CARGO_TEST_TARGET) --no-fail-fast -p wash-lib
+ @$(CARGO) nextest run $(CARGO_TEST_TARGET) --no-fail-fast -p wash-lib --features=cli
test-wash-ci:
@$(CARGO) nextest run --profile ci --workspace --bin wash
diff --git a/crates/wash-lib/Cargo.toml b/crates/wash-lib/Cargo.toml
index 6cbf1625..807a3b36 100644
--- a/crates/wash-lib/Cargo.toml
+++ b/crates/wash-lib/Cargo.toml
@@ -60,6 +60,7 @@ time = "0.3"
tokio = { workspace = true, features = ["process"] }
tokio-stream = { workspace = true }
tokio-tar = { workspace = true }
+tokio-util = { workspace = true }
toml = { workspace = true }
wadm = { workspace = true, optional = true}
walkdir = { workspace = true }
diff --git a/crates/wash-lib/src/registry.rs b/crates/wash-lib/src/registry.rs
index b9babac0..e450cd8f 100644
--- a/crates/wash-lib/src/registry.rs
+++ b/crates/wash-lib/src/registry.rs
@@ -6,6 +6,7 @@ use std::{
};
use anyhow::{anyhow, bail, Result};
+#[cfg(feature = "cli")]
use clap::{Parser, Subcommand};
use oci_distribution::manifest::OciImageManifest;
use oci_distribution::{
diff --git a/crates/wash-lib/src/start/nats.rs b/crates/wash-lib/src/start/nats.rs
index fe9db5d4..e8974b3d 100644
--- a/crates/wash-lib/src/start/nats.rs
+++ b/crates/wash-lib/src/start/nats.rs
@@ -268,6 +268,14 @@ where
.map(|_| child)
}
+/// Helper function to get the path to the NATS server pid file
+pub fn nats_pid_path
(install_dir: P) -> PathBuf
+where
+ P: AsRef,
+{
+ install_dir.as_ref().join(NATS_SERVER_PID)
+}
+
/// Helper function to determine the NATS server release path given an os/arch and version
fn nats_url(os: &str, arch: &str, version: &str) -> String {
// Replace "macos" with "darwin" to match NATS release scheme
diff --git a/crates/wash-lib/src/start/wasmcloud.rs b/crates/wash-lib/src/start/wasmcloud.rs
index e4f2f642..d09d06d5 100644
--- a/crates/wash-lib/src/start/wasmcloud.rs
+++ b/crates/wash-lib/src/start/wasmcloud.rs
@@ -1,37 +1,37 @@
use std::collections::HashMap;
-use std::io::Cursor;
#[cfg(target_family = "unix")]
use std::os::unix::prelude::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::Stdio;
use anyhow::{anyhow, Result};
-use async_compression::tokio::bufread::GzipDecoder;
#[cfg(target_family = "unix")]
use command_group::AsyncCommandGroup;
-use futures::future::join_all;
use log::warn;
+use reqwest::StatusCode;
use tokio::fs::{create_dir_all, metadata, File};
use tokio::process::{Child, Command};
use tokio_stream::StreamExt;
-use tokio_tar::Archive;
+use tokio_util::io::StreamReader;
const WASMCLOUD_GITHUB_RELEASE_URL: &str =
"https://github.com/wasmCloud/wasmcloud-otp/releases/download";
#[cfg(target_family = "unix")]
-pub const WASMCLOUD_HOST_BIN: &str = "bin/wasmcloud_host";
+pub const WASMCLOUD_HOST_BIN: &str = "wasmcloud_host";
#[cfg(target_family = "windows")]
-pub const WASMCLOUD_HOST_BIN: &str = "bin\\wasmcloud_host.bat";
+pub const WASMCLOUD_HOST_BIN: &str = "wasmcloud_host.exe";
-// Any version of wasmCloud under 0.57.0 uses distillery releases and is incompatible
-const MINIMUM_WASMCLOUD_VERSION: &str = "0.57.0";
+// Any version of wasmCloud under 0.63.0 uses Elixir releases and is incompatible
+// See https://github.com/wasmCloud/wasmcloud-otp/pull/616 for the move to burrito releases
+const MINIMUM_WASMCLOUD_VERSION: &str = "0.63.0";
+const DEFAULT_DASHBOARD_PORT: u16 = 4000;
/// A wrapper around the [ensure_wasmcloud_for_os_arch_pair] function that uses the
/// architecture and operating system of the current host machine.
///
/// # Arguments
///
-/// * `version` - Specifies the version of the binary to download in the form of `vX.Y.Z`. Must be at least v0.57.0.
+/// * `version` - Specifies the version of the binary to download in the form of `vX.Y.Z`. Must be at least v0.63.0.
/// * `dir` - Where to unpack the wasmCloud host contents into
/// # Examples
///
@@ -39,9 +39,9 @@ const MINIMUM_WASMCLOUD_VERSION: &str = "0.57.0";
/// # #[tokio::main]
/// # async fn main() {
/// use wash_lib::start::ensure_wasmcloud;
-/// let res = ensure_wasmcloud("v0.57.1", "/tmp/wasmcloud/").await;
+/// let res = ensure_wasmcloud("v0.63.0", "/tmp/wasmcloud/").await;
/// assert!(res.is_ok());
-/// assert!(res.unwrap().to_string_lossy() == "/tmp/wasmcloud/bin/wasmcloud_host".to_string());
+/// assert!(res.unwrap().to_string_lossy() == "/tmp/wasmcloud/v0.63.0/wasmcloud_host".to_string());
/// # }
/// ```
pub async fn ensure_wasmcloud(version: &str, dir: P) -> Result
@@ -55,17 +55,17 @@ where
/// Ensures the `wasmcloud_host` application is installed, returning the path to the executable
/// early if it exists or downloading the specified GitHub release version of the wasmCloud host
/// from and unpacking the contents for a
-/// specified OS/ARCH pair to a directory. Returns the path to the Elixir executable.
+/// specified OS/ARCH pair to a directory. Returns the path to the executable.
///
/// # Arguments
///
/// * `os` - Specifies the operating system of the binary to download, e.g. `linux`
/// * `arch` - Specifies the architecture of the binary to download, e.g. `amd64`
/// * `version` - Specifies the version of the binary to download in the form of `vX.Y.Z`. Must be
-/// at least v0.57.0.
+/// at least v0.63.0.
/// * `dir` - Where to unpack the wasmCloud host contents into. This should be the root level
/// directory where to store hosts. Each host will be stored in a directory maching its version
-/// (e.g. "/tmp/wasmcloud/v0.59.0")
+/// (e.g. "/tmp/wasmcloud/v0.63.0")
/// # Examples
///
/// ```no_run
@@ -74,9 +74,9 @@ where
/// use wash_lib::start::ensure_wasmcloud_for_os_arch_pair;
/// let os = std::env::consts::OS;
/// let arch = std::env::consts::ARCH;
-/// let res = ensure_wasmcloud_for_os_arch_pair(os, arch, "v0.57.1", "/tmp/wasmcloud/").await;
+/// let res = ensure_wasmcloud_for_os_arch_pair(os, arch, "v0.63.0", "/tmp/wasmcloud/").await;
/// assert!(res.is_ok());
-/// assert!(res.unwrap().to_string_lossy() == "/tmp/wasmcloud/bin/wasmcloud_host".to_string());
+/// assert!(res.unwrap().to_string_lossy() == "/tmp/wasmcloud/v0.63.0/wasmcloud_host".to_string());
/// # }
/// ```
pub async fn ensure_wasmcloud_for_os_arch_pair(
@@ -113,7 +113,7 @@ where
/// use wash_lib::start::download_wasmcloud;
/// let res = download_wasmcloud("v0.57.1", "/tmp/wasmcloud/").await;
/// assert!(res.is_ok());
-/// assert!(res.unwrap().to_string_lossy() == "/tmp/wasmcloud/bin/wasmcloud_host".to_string());
+/// assert!(res.unwrap().to_string_lossy() == "/tmp/wasmcloud/v0.63.0/wasmcloud_host".to_string());
/// # }
/// ```
pub async fn download_wasmcloud
(version: &str, dir: P) -> Result
@@ -143,9 +143,9 @@ where
/// use wash_lib::start::download_wasmcloud_for_os_arch_pair;
/// let os = std::env::consts::OS;
/// let arch = std::env::consts::ARCH;
-/// let res = download_wasmcloud_for_os_arch_pair(os, arch, "v0.57.1", "/tmp/wasmcloud/").await;
+/// let res = download_wasmcloud_for_os_arch_pair(os, arch, "v0.63.0", "/tmp/wasmcloud/").await;
/// assert!(res.is_ok());
-/// assert!(res.unwrap().to_string_lossy() == "/tmp/wasmcloud/bin/wasmcloud_host".to_string());
+/// assert!(res.unwrap().to_string_lossy() == "/tmp/wasmcloud/v0.63.0/wasmcloud_host".to_string());
/// # }
/// ```
pub async fn download_wasmcloud_for_os_arch_pair(
@@ -158,50 +158,40 @@ where
P: AsRef,
{
let url = wasmcloud_url(os, arch, version);
- let body = reqwest::get(url).await?.bytes().await?;
- let cursor = Cursor::new(body);
- let mut wasmcloud_host = Archive::new(Box::new(GzipDecoder::new(cursor)));
- let mut entries = wasmcloud_host.entries()?;
+ // NOTE(brooksmtownsend): This seems like a lot of work when I really just want to use AsyncRead
+ // to pipe the response body into a file. I'm not sure if there's a better way to do this.
+ let download_response = reqwest::get(url.clone()).await?;
+ if download_response.status() != StatusCode::OK {
+ return Err(anyhow!(
+ "Failed to download wasmCloud host from {}. Status code: {}",
+ url,
+ download_response.status()
+ ));
+ }
+
+ let burrito_bites_stream = download_response
+ .bytes_stream()
+ .map(|result| result.map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err)));
+ let mut wasmcloud_host_burrito = StreamReader::new(burrito_bites_stream);
let version_dir = dir.as_ref().join(version);
- // Copy all of the files out of the tarball into the bin directory
- while let Some(res) = entries.next().await {
- let mut entry = res.map_err(|_e| {
- anyhow!(
- "Failed to retrieve file from archive, ensure wasmcloud_host version '{}' exists",
- version
- )
- })?;
- if let Ok(path) = entry.path() {
- let file_path = version_dir.join(path);
- if let Some(parent_folder) = file_path.parent() {
- // If the user doesn't have permission to create files in the provided directory,
- // this will bubble the error up noting permission denied
- create_dir_all(parent_folder).await?;
- }
- if let Ok(mut wasmcloud_file) = File::create(&file_path).await {
- // This isn't an `if let` to avoid a Windows lint warning
- if file_path.file_name().is_some() {
- // Set permissions of executable files and binaries to allow executing
- #[cfg(target_family = "unix")]
- {
- let file_name = file_path.file_name().unwrap().to_string_lossy();
- if file_path.to_string_lossy().contains("bin")
- || file_name.contains(".sh")
- || file_name.contains(".bat")
- || file_name.eq("iex")
- || file_name.eq("elixir")
- || file_name.eq("wasmcloud_host")
- || file_name.eq("mac_listener")
- {
- let mut perms = wasmcloud_file.metadata().await?.permissions();
- perms.set_mode(0o755);
- wasmcloud_file.set_permissions(perms).await?;
- }
- }
- }
- tokio::io::copy(&mut entry, &mut wasmcloud_file).await?;
+ let file_path = version_dir.join(WASMCLOUD_HOST_BIN);
+ if let Some(parent_folder) = file_path.parent() {
+ // If the user doesn't have permission to create files in the provided directory,
+ // this will bubble the error up noting permission denied
+ create_dir_all(parent_folder).await?;
+ }
+ if let Ok(mut wasmcloud_file) = File::create(&file_path).await {
+ // This isn't an `if let` to avoid a Windows lint warning
+ if file_path.file_name().is_some() {
+ // Set permissions of executable files and binaries to allow executing
+ #[cfg(target_family = "unix")]
+ {
+ let mut perms = wasmcloud_file.metadata().await?.permissions();
+ perms.set_mode(0o755);
+ wasmcloud_file.set_permissions(perms).await?;
}
}
+ tokio::io::copy(&mut wasmcloud_host_burrito, &mut wasmcloud_file).await?;
}
// Return success if wasmCloud components exist, error otherwise
@@ -212,10 +202,11 @@ where
)),
}
}
-/// Helper function to start a wasmCloud host given the path to the elixir release script
+
+/// Helper function to start a wasmCloud host given the path to the burrito release application
/// /// # Arguments
///
-/// * `bin_path` - Path to the wasmcloud_host script to execute
+/// * `bin_path` - Path to the wasmcloud_host burrito application
/// * `stdout` - Specify where wasmCloud stdout logs should be written to. Logs can be written to stdout by the erlang process
/// * `stderr` - Specify where wasmCloud stderr logs should be written to. Logs are written to stderr that are generated by wasmCloud
/// * `env_vars` - Environment variables to pass to the host, see for details
@@ -234,7 +225,7 @@ where
let port = env_vars
.get("WASMCLOUD_DASHBOARD_PORT")
.cloned()
- .unwrap_or_else(|| "4000".to_string());
+ .unwrap_or_else(|| DEFAULT_DASHBOARD_PORT.to_string());
if tokio::net::TcpStream::connect(format!("localhost:{port}"))
.await
.is_ok()
@@ -245,23 +236,6 @@ where
));
}
- #[cfg(target_family = "unix")]
- if let Ok(output) = Command::new(bin_path.as_ref())
- .envs(&env_vars)
- .arg("pid")
- .output()
- .await
- {
- // Stderr will include :nodedown if no other host is running, otherwise
- // stdout will contain the PID
- if !String::from_utf8_lossy(&output.stderr).contains(":nodedown") {
- return Err(anyhow!(
- "Another wasmCloud host is already running on this machine with PID {}",
- String::from_utf8_lossy(&output.stdout)
- ));
- }
- }
-
// Constructing this object in one step results in a temporary value that's dropped
let mut cmd = Command::new(bin_path.as_ref());
let cmd = cmd
@@ -269,8 +243,7 @@ where
.stderr(stderr)
.stdout(stdout)
.stdin(Stdio::null())
- .envs(&env_vars)
- .arg("start");
+ .envs(&env_vars);
#[cfg(target_family = "unix")]
{
@@ -289,31 +262,25 @@ where
P: AsRef,
{
let versioned_dir = dir.as_ref().join(version);
- let bin_dir = versioned_dir.join("bin");
let bin_file = versioned_dir.join(WASMCLOUD_HOST_BIN);
- let lib_dir = versioned_dir.join("lib");
- let releases_dir = versioned_dir.join("releases");
- let file_checks = vec![
- metadata(versioned_dir),
- metadata(bin_dir),
- metadata(bin_file.clone()),
- metadata(lib_dir),
- metadata(releases_dir),
- ];
- join_all(file_checks)
- .await
- .iter()
- .all(|i| i.is_ok())
- .then_some(bin_file)
+
+ metadata(&bin_file).await.is_ok().then_some(bin_file)
}
/// Helper function to determine the wasmCloud host release path given an os/arch and version
fn wasmcloud_url(os: &str, arch: &str, version: &str) -> String {
- format!("{WASMCLOUD_GITHUB_RELEASE_URL}/{version}/{arch}-{os}.tar.gz")
+ // NOTE(brooksmtownsend): I'm hardcoding `gnu` here because I'm not sure how to determine
+ // that programmatically. This essentially is what we had before (gnu only) but we do have a musl
+ // release that we should consider.
+ let os = os
+ .replace("macos", "darwin")
+ .replace("linux", "linux_gnu")
+ .replace("windows", "windows.exe");
+ format!("{WASMCLOUD_GITHUB_RELEASE_URL}/{version}/wasmcloud_host_{arch}_{os}")
}
/// Helper function to ensure the version of wasmCloud is above the minimum
-/// supported version (v0.57.0) that runs mix releases
+/// supported version (v0.63.0) that runs burrito releases
fn check_version(version: &str) -> Result<()> {
let version_req = semver::VersionReq::parse(&format!(">={MINIMUM_WASMCLOUD_VERSION}"))?;
match semver::Version::parse(version.trim_start_matches('v')) {
@@ -345,7 +312,7 @@ mod test {
use reqwest::StatusCode;
use std::{collections::HashMap, env::temp_dir};
use tokio::fs::{create_dir_all, remove_dir_all};
- const WASMCLOUD_VERSION: &str = "v0.60.0";
+ const WASMCLOUD_VERSION: &str = "v0.63.0";
#[tokio::test]
async fn can_request_supported_wasmcloud_urls() {
@@ -364,12 +331,9 @@ mod test {
}
}
- #[cfg(target_family = "unix")]
- use std::os::unix::prelude::PermissionsExt;
-
#[tokio::test]
- async fn can_download_wasmcloud_tarball() {
- let download_dir = temp_dir().join("can_download_wasmcloud_tarball");
+ async fn can_download_wasmcloud_burrito() {
+ let download_dir = temp_dir().join("can_download_wasmcloud_burrito");
let res =
ensure_wasmcloud_for_os_arch_pair("macos", "aarch64", WASMCLOUD_VERSION, &download_dir)
.await
@@ -383,39 +347,13 @@ mod test {
res
);
- // Permit execution of file-watching on macos.
- #[cfg(target_family = "unix")]
- {
- let mut fs_dir: Option = None;
- let mut entries = tokio::fs::read_dir(download_dir.join(WASMCLOUD_VERSION).join("lib"))
- .await
- .unwrap();
-
- while let Some(entry) = entries.next_entry().await.unwrap() {
- let dir = entry.path();
- let file_name = dir.file_name().unwrap().to_string_lossy();
-
- if file_name.contains("file_system") {
- fs_dir = Some(dir);
- break;
- }
- }
-
- let path = fs_dir.unwrap().join("priv/mac_listener");
- let file = tokio::fs::File::open(path).await.unwrap();
- let metadata = file.metadata().await.unwrap();
- let perms = metadata.permissions();
-
- assert_eq!(perms.mode(), 0o100755);
- }
-
let _ = remove_dir_all(download_dir).await;
}
#[tokio::test]
async fn can_handle_missing_wasmcloud_version() {
let download_dir = temp_dir().join("can_handle_missing_wasmcloud_version");
- let res = ensure_wasmcloud("v010233.123.3.4", &download_dir).await;
+ let res = ensure_wasmcloud("v10233.123.3.4", &download_dir).await;
assert!(res.is_err());
let _ = remove_dir_all(download_dir).await;
@@ -435,12 +373,12 @@ mod test {
"wasmCloud should be installed"
);
- ensure_wasmcloud_for_os_arch_pair("macos", "aarch64", "v0.59.0", &download_dir)
+ ensure_wasmcloud_for_os_arch_pair("macos", "aarch64", "v0.63.1", &download_dir)
.await
.expect("Should be able to download host");
assert!(
- find_wasmcloud_binary(&download_dir, "v0.59.0")
+ find_wasmcloud_binary(&download_dir, "v0.63.1")
.await
.is_some(),
"wasmCloud should be installed"
@@ -452,7 +390,7 @@ mod test {
"Directory should exist"
);
assert!(
- download_dir.join("v0.59.0").exists(),
+ download_dir.join("v0.63.1").exists(),
"Directory should exist"
);
@@ -460,7 +398,7 @@ mod test {
}
const NATS_SERVER_VERSION: &str = "v2.8.4";
- const WASMCLOUD_HOST_VERSION: &str = "v0.61.0";
+ const WASMCLOUD_HOST_VERSION: &str = "v0.63.1";
#[tokio::test]
async fn can_download_and_start_wasmcloud() -> anyhow::Result<()> {
@@ -519,6 +457,7 @@ mod test {
.await;
let mut host_env = HashMap::new();
+ host_env.insert("WASMCLOUD_DASHBOARD_PORT".to_string(), "5003".to_string());
host_env.insert("WASMCLOUD_RPC_PORT".to_string(), nats_port.to_string());
host_env.insert("WASMCLOUD_CTL_PORT".to_string(), nats_port.to_string());
host_env.insert("WASMCLOUD_PROV_RPC_PORT".to_string(), nats_port.to_string());
@@ -551,6 +490,7 @@ mod test {
// Should fail because the port is already in use by another host
let mut host_env = HashMap::new();
+ host_env.insert("WASMCLOUD_DASHBOARD_PORT".to_string(), "5003".to_string());
host_env.insert("WASMCLOUD_RPC_PORT".to_string(), nats_port.to_string());
host_env.insert("WASMCLOUD_CTL_PORT".to_string(), nats_port.to_string());
host_env.insert("WASMCLOUD_PROV_RPC_PORT".to_string(), nats_port.to_string());
@@ -563,24 +503,21 @@ mod test {
.await
.expect_err("Starting a second process should error");
- // Should fail because another erlang wasmcloud_host node is running
- #[cfg(target_family = "unix")]
- // Windows is unable to properly check running erlang nodes with `pid`
- {
- let mut host_env = HashMap::new();
- host_env.insert("WASMCLOUD_DASHBOARD_PORT".to_string(), "4002".to_string());
- host_env.insert("WASMCLOUD_RPC_PORT".to_string(), nats_port.to_string());
- host_env.insert("WASMCLOUD_CTL_PORT".to_string(), nats_port.to_string());
- host_env.insert("WASMCLOUD_PROV_RPC_PORT".to_string(), nats_port.to_string());
- let child_res = start_wasmcloud_host(
- &wasmcloud_binary,
- std::process::Stdio::null(),
- std::process::Stdio::null(),
- host_env,
- )
- .await;
- assert!(child_res.is_err());
- }
+ // Burrito releases (0.63.0+) do support multiple hosts, so this should work fine
+ let mut host_env = HashMap::new();
+ host_env.insert("WASMCLOUD_DASHBOARD_PORT".to_string(), "4002".to_string());
+ host_env.insert("WASMCLOUD_RPC_PORT".to_string(), nats_port.to_string());
+ host_env.insert("WASMCLOUD_CTL_PORT".to_string(), nats_port.to_string());
+ host_env.insert("WASMCLOUD_PROV_RPC_PORT".to_string(), nats_port.to_string());
+ let child_res = start_wasmcloud_host(
+ &wasmcloud_binary,
+ std::process::Stdio::null(),
+ std::process::Stdio::null(),
+ host_env,
+ )
+ .await;
+ assert!(child_res.is_ok());
+ child_res.unwrap().kill().await?;
host_child.kill().await?;
nats_child.kill().await?;
@@ -589,28 +526,30 @@ mod test {
}
#[tokio::test]
- async fn can_properly_deny_distillery_release_hosts() -> anyhow::Result<()> {
- // Ensure we allow versions >= 0.57.0
+ async fn can_properly_deny_elixir_release_hosts() -> anyhow::Result<()> {
+ // Ensure we allow versions >= 0.63.0
assert!(check_version("v1.56.0").is_ok());
- assert!(check_version("v0.57.0").is_ok());
- assert!(check_version("v0.57.1").is_ok());
- assert!(check_version("v0.57.2").is_ok());
- assert!(check_version("v0.58.0").is_ok());
+ assert!(check_version("v0.63.0").is_ok());
+ assert!(check_version("v0.63.1").is_ok());
+ assert!(check_version("v0.63.2").is_ok());
+ assert!(check_version("v0.64.0").is_ok());
assert!(check_version("v0.100.0").is_ok());
assert!(check_version("v0.203.0").is_ok());
// Ensure we allow prerelease tags for testing
- assert!(check_version("v0.60.0-rc.1").is_ok());
- assert!(check_version("v0.60.0-alpha.23").is_ok());
- assert!(check_version("v0.60.0-beta.0").is_ok());
+ assert!(check_version("v0.64.0-rc.1").is_ok());
+ assert!(check_version("v0.64.0-alpha.23").is_ok());
+ assert!(check_version("v0.64.0-beta.0").is_ok());
- // Ensure we deny versions < 0.57.0
+ // Ensure we deny versions < 0.63.0
assert!(check_version("v0.48.0").is_err());
assert!(check_version("v0.56.0").is_err());
+ assert!(check_version("v0.58.0").is_err());
+ assert!(check_version("v0.62.3").is_err());
assert!(check_version("v0.12.0").is_err());
assert!(check_version("v0.56.999").is_err());
if let Err(e) = check_version("v0.56.0") {
- assert_eq!(e.to_string(), "wasmCloud version v0.56.0 is earlier than the minimum supported version of v0.57.0");
+ assert_eq!(e.to_string(), "wasmCloud version v0.56.0 is earlier than the minimum supported version of v0.63.0");
} else {
panic!("v0.56.0 should be before the minimum version")
}
diff --git a/crates/wash-lib/tests/parser/files/withcargotoml/Cargo.toml b/crates/wash-lib/tests/parser/files/withcargotoml/Cargo.toml
index 05e1356f..6125f865 100644
--- a/crates/wash-lib/tests/parser/files/withcargotoml/Cargo.toml
+++ b/crates/wash-lib/tests/parser/files/withcargotoml/Cargo.toml
@@ -5,4 +5,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+[lib]
+crate-type = ["cdylib", "rlib"]
+
[dependencies]
diff --git a/src/down/mod.rs b/src/down/mod.rs
index a804aa01..416d91f1 100644
--- a/src/down/mod.rs
+++ b/src/down/mod.rs
@@ -1,21 +1,63 @@
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::{Output, Stdio};
-use std::time::Duration;
-use anyhow::Result;
+use anyhow::{anyhow, Result};
+use async_nats::Client;
use clap::Parser;
+use log::{error, warn};
use serde_json::json;
use tokio::process::Command;
use wash_lib::cli::{CommandOutput, OutputKind};
-use wash_lib::start::{find_wasmcloud_binary, NATS_SERVER_BINARY, NATS_SERVER_PID, WADM_PID};
+use wash_lib::config::{DEFAULT_NATS_HOST, DEFAULT_NATS_PORT};
+use wash_lib::id::ServerId;
+use wash_lib::start::{nats_pid_path, NATS_SERVER_BINARY, WADM_PID};
use crate::appearance::spinner::Spinner;
use crate::cfg::cfg_dir;
-use crate::up::{DOWNLOADS_DIR, WASMCLOUD_PID_FILE};
+use crate::up::{
+ DEFAULT_LATTICE_PREFIX, DOWNLOADS_DIR, WASMCLOUD_CTL_CREDSFILE, WASMCLOUD_CTL_HOST,
+ WASMCLOUD_CTL_JWT, WASMCLOUD_CTL_PORT, WASMCLOUD_CTL_SEED, WASMCLOUD_LATTICE_PREFIX,
+};
+use crate::util::nats_client_from_opts;
#[derive(Parser, Debug, Clone)]
-pub(crate) struct DownCommand {}
+pub(crate) struct DownCommand {
+ /// A lattice prefix is a unique identifier for a lattice, and is frequently used within NATS topics to isolate messages from different lattices
+ #[clap(
+ short = 'x',
+ long = "lattice-prefix",
+ default_value = DEFAULT_LATTICE_PREFIX,
+ env = WASMCLOUD_LATTICE_PREFIX,
+ )]
+ pub(crate) lattice_prefix: String,
+
+ /// An IP address or DNS name to use to connect to NATS for Control Interface (CTL) messages, defaults to the value supplied to --nats-host if not supplied
+ #[clap(long = "ctl-host", env = WASMCLOUD_CTL_HOST)]
+ pub(crate) ctl_host: Option,
+
+ /// A port to use to connect to NATS for CTL messages, defaults to the value supplied to --nats-port if not supplied
+ #[clap(long = "ctl-port", env = WASMCLOUD_CTL_PORT)]
+ pub(crate) ctl_port: Option,
+
+ /// Convenience flag for CTL authentication, internally this parses the JWT and seed from the credsfile
+ #[clap(long = "ctl-credsfile", env = WASMCLOUD_CTL_CREDSFILE)]
+ pub(crate) ctl_credsfile: Option,
+
+ /// A seed nkey to use to authenticate to NATS for CTL messages
+ #[clap(long = "ctl-seed", env = WASMCLOUD_CTL_SEED, requires = "ctl_jwt")]
+ pub(crate) ctl_seed: Option,
+
+ /// A user JWT to use to authenticate to NATS for CTL messages
+ #[clap(long = "ctl-jwt", env = WASMCLOUD_CTL_JWT, requires = "ctl_seed")]
+ pub(crate) ctl_jwt: Option,
+
+ #[clap(long = "host-id")]
+ pub host_id: Option,
+
+ #[clap(long = "all")]
+ pub all: bool,
+}
pub(crate) async fn handle_command(
command: DownCommand,
@@ -25,37 +67,53 @@ pub(crate) async fn handle_command(
}
pub(crate) async fn handle_down(
- _cmd: DownCommand,
+ cmd: DownCommand,
output_kind: OutputKind,
) -> Result {
let install_dir = cfg_dir()?.join(DOWNLOADS_DIR);
let sp = Spinner::new(&output_kind)?;
+ sp.update_spinner_message(" Stopping wasmCloud ...".to_string());
let mut out_json = HashMap::new();
let mut out_text = String::from("");
- let version = tokio::fs::read_to_string(install_dir.join(WASMCLOUD_PID_FILE))
- .await
- .map_err(|e| {
- anyhow::anyhow!("Unable to find wasmcloud pid file for stopping process: {e}")
- })?;
- let host_bin = find_wasmcloud_binary(&install_dir, &version)
- .await
- .ok_or_else(|| anyhow::anyhow!("Couldn't find path to wasmCloud binary. Is it running?"))?;
- if host_bin.is_file() {
- sp.update_spinner_message(" Stopping host ...".to_string());
- if let Ok(output) = stop_wasmcloud(host_bin).await {
- if output.stderr.is_empty() && output.stdout.is_empty() {
- // if there was a host running, 'stop' has no output.
- // Give it time to stop before stopping nats
- tokio::time::sleep(Duration::from_secs(6)).await;
- out_json.insert("host_stopped".to_string(), json!(true));
- out_text.push_str("✅ wasmCloud host stopped successfully\n");
- } else {
- out_json.insert("host_stopped".to_string(), json!(true));
- out_text.push_str(
- "🤔 Host did not appear to be running, assuming it's already stopped\n",
- );
- }
+
+ if let Ok(client) = nats_client_from_opts(
+ &cmd.ctl_host
+ .unwrap_or_else(|| DEFAULT_NATS_HOST.to_string()),
+ &cmd.ctl_port
+ .map(|port| port.to_string())
+ .unwrap_or_else(|| DEFAULT_NATS_PORT.to_string()),
+ cmd.ctl_jwt,
+ cmd.ctl_seed,
+ cmd.ctl_credsfile,
+ )
+ .await
+ {
+ let (hosts, hosts_remain) =
+ stop_hosts(client, &cmd.lattice_prefix, &cmd.host_id, cmd.all).await?;
+ out_json.insert("hosts_stopped".to_string(), json!(hosts));
+ out_text.push_str("✅ wasmCloud hosts stopped successfully\n");
+ if hosts_remain {
+ out_json.insert("nats_stopped".to_string(), json!(false));
+ out_json.insert("wadm_stopped".to_string(), json!(false));
+ out_text.push_str(
+ "🛁 Exiting without stopping NATS or wadm, there are still hosts running",
+ );
+ return Ok(CommandOutput::new(out_text, out_json));
+ }
+ } else {
+ warn!("Couldn't connect to NATS, unable to stop running hosts")
+ }
+
+ match stop_wadm(&install_dir).await {
+ Ok(_) => {
+ tokio::fs::remove_file(&install_dir.join(WADM_PID)).await?;
+ out_json.insert("wadm_stopped".to_string(), json!(true));
+ out_text.push_str("✅ wadm stopped successfully\n");
+ }
+ Err(e) => {
+ out_json.insert("wadm_stopped".to_string(), json!(false));
+ out_text.push_str(&format!("❌ Could not stop wadm: {e:?}\n"));
}
}
@@ -73,18 +131,6 @@ pub(crate) async fn handle_down(
}
}
- match stop_wadm(&install_dir).await {
- Ok(_) => {
- tokio::fs::remove_file(&install_dir.join(WADM_PID)).await?;
- out_json.insert("wadm_stopped".to_string(), json!(true));
- out_text.push_str("✅ wadm stopped successfully\n");
- }
- Err(e) => {
- out_json.insert("wadm_stopped".to_string(), json!(false));
- out_text.push_str(&format!("❌ wadm did not stop successfully: {e:?}\n"));
- }
- }
-
out_json.insert("success".to_string(), json!(true));
out_text.push_str("🛁 wash down completed successfully");
@@ -92,17 +138,71 @@ pub(crate) async fn handle_down(
Ok(CommandOutput::new(out_text, out_json))
}
-/// Helper function to send wasmCloud the `stop` command and wait for it to clean up
-pub(crate) async fn stop_wasmcloud(bin_path: P) -> Result