wash_cli/cmd/
up.rs

1use anyhow::{anyhow, bail, Context, Result};
2use async_nats::Client;
3use clap::Parser;
4use serde_json::{json, Value};
5use std::collections::HashMap;
6use std::fmt::Write;
7use std::io::ErrorKind;
8use std::path::{Path, PathBuf};
9use std::process::Stdio;
10use std::sync::{
11    atomic::{AtomicBool, Ordering},
12    Arc,
13};
14use sysinfo::{System, SystemExt};
15use tokio::fs::create_dir_all;
16use tokio::{
17    io::{AsyncBufReadExt, BufReader},
18    process::Child,
19};
20use tracing::{debug, warn};
21use wash_lib::app::{load_app_manifest, AppManifest, AppManifestSource};
22use wash_lib::cli::{CommandOutput, OutputKind};
23use wash_lib::common::CommandGroupUsage;
24use wash_lib::config::{
25    create_nats_client_from_opts, downloads_dir, host_pid_file, DEFAULT_NATS_TIMEOUT_MS,
26};
27use wash_lib::context::fs::ContextDir;
28use wash_lib::context::ContextManager;
29use wash_lib::generate::emoji;
30use wash_lib::start::{
31    ensure_nats_server, ensure_wadm, ensure_wasmcloud, find_wasmcloud_binary, nats_pid_path,
32    new_patch_version_of_after_string, start_nats_server, start_wadm, start_wasmcloud_host,
33    NatsConfig, WadmConfig, GITHUB_WASMCLOUD_ORG, GITHUB_WASMCLOUD_WADM_REPO,
34    GITHUB_WASMCLOUD_WASMCLOUD_REPO, NATS_SERVER_BINARY, NATS_SERVER_CONF, WADM_PID,
35};
36use wasmcloud_control_interface::{Client as CtlClient, ClientBuilder as CtlClientBuilder};
37
38use crate::app::deploy_model_from_manifest;
39use crate::appearance::spinner::Spinner;
40use crate::config::{
41    configure_host_env, DEFAULT_ALLOW_FILE_LOAD, DEFAULT_LATTICE, DEFAULT_MAX_EXECUTION_TIME_MS,
42    DEFAULT_NATS_HOST, DEFAULT_NATS_PORT, DEFAULT_NATS_WEBSOCKET_PORT,
43    DEFAULT_PROV_SHUTDOWN_DELAY_MS, DEFAULT_RPC_TIMEOUT_MS, DEFAULT_STRUCTURED_LOG_LEVEL,
44    NATS_SERVER_VERSION, WADM_VERSION, WASMCLOUD_ALLOW_FILE_LOAD, WASMCLOUD_CLUSTER_ISSUERS,
45    WASMCLOUD_CLUSTER_SEED, WASMCLOUD_CONFIG_SERVICE, WASMCLOUD_CTL_CREDSFILE, WASMCLOUD_CTL_HOST,
46    WASMCLOUD_CTL_JWT, WASMCLOUD_CTL_PORT, WASMCLOUD_CTL_SEED, WASMCLOUD_CTL_TLS,
47    WASMCLOUD_CTL_TLS_CA_FILE, WASMCLOUD_CTL_TLS_FIRST, WASMCLOUD_ENABLE_IPV6,
48    WASMCLOUD_HOST_LOG_PATH, WASMCLOUD_HOST_PATH, WASMCLOUD_HOST_SEED, WASMCLOUD_HOST_VERSION,
49    WASMCLOUD_JS_DOMAIN, WASMCLOUD_LATTICE, WASMCLOUD_LOG_LEVEL, WASMCLOUD_MAX_EXECUTION_TIME_MS,
50    WASMCLOUD_OCI_ALLOWED_INSECURE, WASMCLOUD_OCI_ALLOW_LATEST, WASMCLOUD_POLICY_TOPIC,
51    WASMCLOUD_PROV_SHUTDOWN_DELAY_MS, WASMCLOUD_RPC_CREDSFILE, WASMCLOUD_RPC_HOST,
52    WASMCLOUD_RPC_JWT, WASMCLOUD_RPC_PORT, WASMCLOUD_RPC_SEED, WASMCLOUD_RPC_TIMEOUT_MS,
53    WASMCLOUD_RPC_TLS, WASMCLOUD_RPC_TLS_CA_FILE, WASMCLOUD_RPC_TLS_FIRST, WASMCLOUD_SECRETS_TOPIC,
54    WASMCLOUD_STRUCTURED_LOGGING_ENABLED,
55};
56
57use crate::down::stop_nats;
58
59#[derive(Parser, Debug, Clone)]
60pub struct UpCommand {
61    /// Launch NATS and wasmCloud detached from the current terminal as background processes
62    #[clap(short = 'd', long = "detached", alias = "detach")]
63    pub detached: bool,
64
65    #[clap(flatten)]
66    pub nats_opts: NatsOpts,
67
68    #[clap(flatten)]
69    pub wasmcloud_opts: WasmcloudOpts,
70
71    #[clap(flatten)]
72    pub wadm_opts: WadmOpts,
73}
74
75#[derive(Parser, Debug, Clone)]
76pub struct NatsOpts {
77    /// Optional path to a NATS credentials file to authenticate and extend existing NATS infrastructure.
78    #[clap(
79        long = "nats-credsfile",
80        env = "NATS_CREDSFILE",
81        requires = "nats_remote_url"
82    )]
83    pub nats_credsfile: Option<PathBuf>,
84    /// Optional path to a NATS config file
85    /// NOTE: If your configuration changes the address or port to listen on from 0.0.0.0:4222, ensure you set --nats-host and --nats-port
86    #[clap(
87        long = "nats-config-file",
88        env = "NATS_CONFIG",
89        requires = "nats_host",
90        requires = "nats_port"
91    )]
92    pub nats_configfile: Option<PathBuf>,
93
94    /// Optional remote URL of existing NATS infrastructure to extend.
95    #[clap(long = "nats-remote-url", env = "NATS_REMOTE_URL")]
96    pub nats_remote_url: Option<String>,
97
98    /// If a connection can't be established, exit and don't start a NATS server. Will be ignored if a remote_url and credsfile are specified
99    #[clap(
100        long = "nats-connect-only",
101        env = "NATS_CONNECT_ONLY",
102        conflicts_with = "nats_remote_url"
103    )]
104    pub connect_only: bool,
105
106    /// NATS server version to download, e.g. `v2.10.7`. See https://github.com/nats-io/nats-server/releases/ for releases
107    #[clap(long = "nats-version", default_value = NATS_SERVER_VERSION, env = "NATS_VERSION")]
108    pub nats_version: String,
109
110    /// NATS server host to connect to
111    #[clap(long = "nats-host", env = "WASMCLOUD_NATS_HOST")]
112    pub nats_host: Option<String>,
113
114    /// NATS server port to connect to. This will be used as the NATS listen port if `--nats-connect-only` isn't set
115    #[clap(long = "nats-port", env = "WASMCLOUD_NATS_PORT")]
116    pub nats_port: Option<u16>,
117
118    /// NATS websocket port to use. TLS is not supported. This is required for the wash ui to connect from localhost
119    #[clap(
120        long = "nats-websocket-port",
121        env = "NATS_WEBSOCKET_PORT",
122        default_value = DEFAULT_NATS_WEBSOCKET_PORT
123    )]
124    pub nats_websocket_port: u16,
125
126    /// NATS Server Jetstream domain for extending superclusters
127    #[clap(long = "nats-js-domain", env = "NATS_JS_DOMAIN")]
128    pub nats_js_domain: Option<String>,
129}
130
131impl From<NatsOpts> for NatsConfig {
132    fn from(other: NatsOpts) -> NatsConfig {
133        let host = other
134            .nats_host
135            .unwrap_or_else(|| DEFAULT_NATS_HOST.to_string());
136        let port = other.nats_port.unwrap_or_else(|| {
137            DEFAULT_NATS_PORT
138                .parse()
139                .expect("failed to parse default NATS port")
140        });
141        NatsConfig {
142            host,
143            port,
144            store_dir: std::env::temp_dir().join(format!("wash-jetstream-{port}")),
145            js_domain: other.nats_js_domain,
146            remote_url: other.nats_remote_url,
147            credentials: other.nats_credsfile,
148            websocket_port: other.nats_websocket_port,
149            config_path: other.nats_configfile,
150        }
151    }
152}
153
154#[derive(Parser, Debug, Clone)]
155pub struct WasmcloudOpts {
156    /// wasmcloud host version to download, e.g. `v1.4.2`.
157    ///
158    /// defaults to the [`WASMCLOUD_HOST_VERSION`] if not provided
159    /// or the latest patch version after that when `wash up` issued,
160    /// see <https://github.com/wasmCloud/wasmCloud/releases> for releases
161    #[clap(long = "wasmcloud-version", env = "WASMCLOUD_VERSION")]
162    pub wasmcloud_version: Option<String>,
163
164    /// A unique identifier for a lattice, frequently used within NATS topics to isolate messages among different lattices
165    #[clap(
166        short = 'x',
167        long = "lattice",
168        env = WASMCLOUD_LATTICE
169    )]
170    pub lattice: Option<String>,
171
172    /// The seed key (a printable 256-bit Ed25519 private key) used by this host to generate it's public key
173    #[clap(long = "host-seed", env = WASMCLOUD_HOST_SEED)]
174    pub host_seed: Option<String>,
175
176    /// An IP address or DNS name to use to connect to NATS for RPC messages, defaults to the value supplied to --nats-host if not supplied
177    #[clap(long = "rpc-host", env = WASMCLOUD_RPC_HOST)]
178    pub rpc_host: Option<String>,
179
180    /// A port to use to connect to NATS for RPC messages, defaults to the value supplied to --nats-port if not supplied
181    #[clap(long = "rpc-port", env = WASMCLOUD_RPC_PORT)]
182    pub rpc_port: Option<u16>,
183
184    /// A seed nkey to use to authenticate to NATS for RPC messages
185    #[clap(long = "rpc-seed", env = WASMCLOUD_RPC_SEED, requires = "rpc_jwt")]
186    pub rpc_seed: Option<String>,
187
188    /// Timeout in milliseconds for all RPC calls
189    #[clap(long = "rpc-timeout-ms", default_value = DEFAULT_RPC_TIMEOUT_MS, env = WASMCLOUD_RPC_TIMEOUT_MS)]
190    pub rpc_timeout_ms: Option<u64>,
191
192    /// A user JWT to use to authenticate to NATS for RPC messages
193    #[clap(long = "rpc-jwt", env = WASMCLOUD_RPC_JWT, requires = "rpc_seed")]
194    pub rpc_jwt: Option<String>,
195
196    /// Optional flag to enable host communication with a NATS server over TLS for RPC messages
197    #[clap(long = "rpc-tls", env = WASMCLOUD_RPC_TLS)]
198    pub rpc_tls: bool,
199
200    /// Optional flag to enable performing TLS handshake before expecting the server greeting for RPC messages
201    #[clap(long = "rpc-tls-first", env = WASMCLOUD_RPC_TLS_FIRST, requires = "rpc_tls")]
202    pub rpc_tls_first: bool,
203
204    /// A TLS CA file to use to authenticate to NATS for RPC messages
205    #[clap(long = "rpc-tls-ca-file", env = WASMCLOUD_RPC_TLS_CA_FILE)]
206    pub rpc_tls_ca_file: Option<PathBuf>,
207
208    /// Convenience flag for RPC authentication, internally this parses the JWT and seed from the credsfile
209    #[clap(long = "rpc-credsfile", env = WASMCLOUD_RPC_CREDSFILE)]
210    pub rpc_credsfile: Option<PathBuf>,
211
212    /// 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
213    #[clap(long = "ctl-host", env = WASMCLOUD_CTL_HOST)]
214    pub ctl_host: Option<String>,
215
216    /// A port to use to connect to NATS for CTL messages, defaults to the value supplied to --nats-port if not supplied
217    #[clap(long = "ctl-port", env = WASMCLOUD_CTL_PORT)]
218    pub ctl_port: Option<u16>,
219
220    /// A seed nkey to use to authenticate to NATS for CTL messages
221    #[clap(long = "ctl-seed", env = WASMCLOUD_CTL_SEED, requires = "ctl_jwt")]
222    pub ctl_seed: Option<String>,
223
224    /// A user JWT to use to authenticate to NATS for CTL messages
225    #[clap(long = "ctl-jwt", env = WASMCLOUD_CTL_JWT, requires = "ctl_seed")]
226    pub ctl_jwt: Option<String>,
227
228    /// Convenience flag for CTL authentication, internally this parses the JWT and seed from the credsfile
229    #[clap(long = "ctl-credsfile", env = WASMCLOUD_CTL_CREDSFILE)]
230    pub ctl_credsfile: Option<PathBuf>,
231
232    /// Optional flag to enable host communication with a NATS server over TLS for CTL messages
233    #[clap(long = "ctl-tls", env = WASMCLOUD_CTL_TLS)]
234    pub ctl_tls: bool,
235
236    /// Optional flag to enable performing TLS handshake before expecting the server greeting for CTL messages
237    #[clap(long = "ctl-tls-first", env = WASMCLOUD_CTL_TLS_FIRST, requires = "ctl_tls")]
238    pub ctl_tls_first: bool,
239
240    /// A TLS CA file to use to authenticate to NATS for CTL messages
241    #[clap(long = "ctl-tls-ca-file", env = WASMCLOUD_CTL_TLS_CA_FILE)]
242    pub ctl_tls_ca_file: Option<PathBuf>,
243
244    /// The seed key (a printable 256-bit Ed25519 private key) used by this host to sign all invocations
245    #[clap(long = "cluster-seed", env = WASMCLOUD_CLUSTER_SEED)]
246    pub cluster_seed: Option<String>,
247
248    /// A comma-delimited list of public keys that can be used as issuers on signed invocations
249    #[clap(long = "cluster-issuers", env = WASMCLOUD_CLUSTER_ISSUERS)]
250    pub cluster_issuers: Option<Vec<String>>,
251
252    /// Delay, in milliseconds, between requesting a provider shut down and forcibly terminating its process
253    #[clap(long = "provider-delay", default_value = DEFAULT_PROV_SHUTDOWN_DELAY_MS, env = WASMCLOUD_PROV_SHUTDOWN_DELAY_MS)]
254    pub provider_delay: u32,
255
256    /// Determines whether OCI images tagged latest are allowed to be pulled from OCI registries and started
257    #[clap(long = "allow-latest", env = WASMCLOUD_OCI_ALLOW_LATEST)]
258    pub allow_latest: bool,
259
260    /// A comma-separated list of OCI hosts to which insecure (non-TLS) connections are allowed
261    #[clap(long = "allowed-insecure", env = WASMCLOUD_OCI_ALLOWED_INSECURE)]
262    pub allowed_insecure: Option<Vec<String>>,
263
264    /// Jetstream domain name, configures a host to properly connect to a NATS supercluster
265    #[clap(long = "wasmcloud-js-domain", env = WASMCLOUD_JS_DOMAIN)]
266    pub wasmcloud_js_domain: Option<String>,
267
268    /// Denotes if a wasmCloud host should issue requests to a config service on startup
269    #[clap(long = "config-service-enabled", env = WASMCLOUD_CONFIG_SERVICE)]
270    pub config_service_enabled: bool,
271
272    /// Denotes if a wasmCloud host should allow starting components from the file system
273    #[clap(long = "allow-file-load", default_value = DEFAULT_ALLOW_FILE_LOAD, env = WASMCLOUD_ALLOW_FILE_LOAD)]
274    pub allow_file_load: Option<bool>,
275
276    /// Enable JSON structured logging from the wasmCloud host
277    #[clap(
278        long = "enable-structured-logging",
279        env = WASMCLOUD_STRUCTURED_LOGGING_ENABLED
280    )]
281    pub enable_structured_logging: bool,
282
283    /// A label to apply to the host, in the form of `key=value`. This flag can be repeated to supply multiple labels
284    #[clap(short = 'l', long = "label", alias = "labels")]
285    pub label: Option<Vec<String>>,
286
287    /// Controls the verbosity of JSON structured logs from the wasmCloud host
288    #[clap(long = "log-level", alias = "structured-log-level", default_value = DEFAULT_STRUCTURED_LOG_LEVEL, env = WASMCLOUD_LOG_LEVEL)]
289    pub structured_log_level: String,
290
291    /// Enables IPV6 addressing for wasmCloud hosts
292    #[clap(long = "enable-ipv6", env = WASMCLOUD_ENABLE_IPV6)]
293    pub enable_ipv6: bool,
294
295    /// If enabled, wasmCloud will not be downloaded if it's not installed
296    #[clap(long = "wasmcloud-start-only")]
297    pub start_only: bool,
298
299    /// If enabled, allows starting additional wasmCloud hosts on this machine
300    #[clap(long = "multi-local")]
301    pub multi_local: bool,
302
303    /// Defines the Max Execution time (in ms) that the host runtime will execute for
304    #[clap(long = "max-execution-time-ms", alias = "max-time-ms", env = WASMCLOUD_MAX_EXECUTION_TIME_MS, default_value = DEFAULT_MAX_EXECUTION_TIME_MS)]
305    pub max_execution_time: u64,
306
307    /// If provided, enables interfacing with a secrets backend for secret retrieval over the given topic prefix.
308    #[clap(long = "secrets-topic", env = WASMCLOUD_SECRETS_TOPIC)]
309    pub secrets_topic: Option<String>,
310
311    /// If provided, enables policy checks on start actions and component invocations
312    #[clap(long = "policy-topic", env = WASMCLOUD_POLICY_TOPIC)]
313    pub policy_topic: Option<String>,
314
315    /// Path to which to log information from the wasmCloud host
316    #[clap(long = "host-log-path", env = WASMCLOUD_HOST_LOG_PATH)]
317    pub host_log_path: Option<PathBuf>,
318
319    /// Path to a binary that should be used to start the wasmCloud host
320    #[clap(long = "host-path", env = WASMCLOUD_HOST_PATH)]
321    pub host_path: Option<PathBuf>,
322}
323
324impl WasmcloudOpts {
325    pub async fn into_ctl_client(self, auction_timeout_ms: Option<u64>) -> Result<CtlClient> {
326        let lattice = self.lattice.unwrap_or_else(|| DEFAULT_LATTICE.to_string());
327        let ctl_host = self
328            .ctl_host
329            .unwrap_or_else(|| DEFAULT_NATS_HOST.to_string());
330        let ctl_port = self
331            .ctl_port
332            .map(|p| p.to_string())
333            .unwrap_or_else(|| DEFAULT_NATS_PORT.to_string())
334            .to_string();
335        let auction_timeout_ms = auction_timeout_ms.unwrap_or(DEFAULT_NATS_TIMEOUT_MS);
336
337        let nc = create_nats_client_from_opts(
338            &ctl_host,
339            &ctl_port,
340            self.ctl_jwt,
341            self.ctl_seed,
342            self.ctl_credsfile,
343            self.ctl_tls_ca_file,
344            self.ctl_tls_first,
345        )
346        .await
347        .context("Failed to create NATS client")?;
348
349        let mut builder = CtlClientBuilder::new(nc)
350            .lattice(lattice)
351            .auction_timeout(tokio::time::Duration::from_millis(auction_timeout_ms));
352
353        if let Some(rpc_timeout_ms) = self.rpc_timeout_ms {
354            builder = builder.timeout(tokio::time::Duration::from_millis(rpc_timeout_ms))
355        }
356
357        if let Ok(topic_prefix) = std::env::var("WASMCLOUD_CTL_TOPIC_PREFIX") {
358            builder = builder.topic_prefix(topic_prefix);
359        }
360
361        let ctl_client = builder.build();
362
363        Ok(ctl_client)
364    }
365}
366
367#[derive(Parser, Debug, Clone)]
368pub struct WadmOpts {
369    /// wadm version to download, e.g. `v0.18.0`.
370    ///
371    /// defaults to the [`WADM_VERSION`] if not provided
372    /// or the latest patch version after that when `wash up` issued,
373    /// see <https://github.com/wasmCloud/wadm/releases> for releases
374    #[clap(long = "wadm-version", env = "WADM_VERSION")]
375    pub wadm_version: Option<String>,
376
377    /// If enabled, wadm will not be downloaded or run as a part of the up command
378    #[clap(long = "disable-wadm")]
379    pub disable_wadm: bool,
380
381    /// The JetStream domain to use for wadm
382    #[clap(long = "wadm-js-domain", env = "WADM_JS_DOMAIN")]
383    pub wadm_js_domain: Option<String>,
384
385    /// The path to a wadm application manifest to run while the host is up
386    #[clap(long = "wadm-manifest", env = "WADM_MANIFEST")]
387    pub wadm_manifest: Option<PathBuf>,
388}
389
390#[derive(Debug, PartialEq, PartialOrd)]
391enum WasmCloudHostState {
392    NotRunning,
393    Starting,
394    Running,
395    MultipleRunning,
396}
397
398pub async fn handle_command(command: UpCommand, output_kind: OutputKind) -> Result<CommandOutput> {
399    handle_up(command, output_kind).await
400}
401
402pub async fn handle_up(cmd: UpCommand, output_kind: OutputKind) -> Result<CommandOutput> {
403    let install_dir = downloads_dir()?;
404    create_dir_all(&install_dir).await?;
405    let spinner = Spinner::new(&output_kind)?;
406
407    let ctx = ContextDir::new()?
408        .load_default_context()
409        .context("failed to load context")?;
410    // falling back to the context's ctl_ connection won't always be right, but we have to pick one, since the context values are not optional
411    let nats_host = cmd.nats_opts.nats_host.clone().unwrap_or(ctx.ctl_host);
412    let nats_port = cmd.nats_opts.nats_port.unwrap_or(ctx.ctl_port);
413    let wasmcloud_opts = WasmcloudOpts {
414        lattice: Some(cmd.wasmcloud_opts.lattice.unwrap_or(ctx.lattice)),
415        ctl_host: Some(cmd.wasmcloud_opts.ctl_host.unwrap_or(nats_host.clone())),
416        ctl_port: Some(cmd.wasmcloud_opts.ctl_port.unwrap_or(nats_port)),
417        ctl_jwt: cmd.wasmcloud_opts.ctl_jwt.or(ctx.ctl_jwt),
418        ctl_seed: cmd.wasmcloud_opts.ctl_seed.or(ctx.ctl_seed),
419        ctl_credsfile: cmd.wasmcloud_opts.ctl_credsfile.or(ctx.ctl_credsfile),
420        rpc_host: Some(cmd.wasmcloud_opts.rpc_host.unwrap_or(nats_host.clone())),
421        rpc_port: Some(cmd.wasmcloud_opts.rpc_port.unwrap_or(nats_port)),
422        rpc_timeout_ms: Some(cmd.wasmcloud_opts.rpc_timeout_ms.unwrap_or(ctx.rpc_timeout)),
423        rpc_jwt: cmd.wasmcloud_opts.rpc_jwt.or(ctx.rpc_jwt),
424        rpc_seed: cmd.wasmcloud_opts.rpc_seed.or(ctx.rpc_seed),
425        rpc_credsfile: cmd.wasmcloud_opts.rpc_credsfile.or(ctx.rpc_credsfile),
426        max_execution_time: cmd.wasmcloud_opts.max_execution_time,
427        secrets_topic: cmd.wasmcloud_opts.secrets_topic,
428        policy_topic: cmd.wasmcloud_opts.policy_topic,
429        cluster_seed: cmd
430            .wasmcloud_opts
431            .cluster_seed
432            .or_else(|| ctx.cluster_seed.map(|seed| seed.to_string())),
433        wasmcloud_js_domain: cmd.wasmcloud_opts.wasmcloud_js_domain.or(ctx.js_domain),
434        wasmcloud_version: cmd.wasmcloud_opts.wasmcloud_version.clone(),
435        ..cmd.wasmcloud_opts
436    };
437    let host_env = configure_host_env(wasmcloud_opts.clone()).await?;
438    let nats_listen_address = format!("{nats_host}:{nats_port}");
439
440    let nats_client = nats_client_from_wasmcloud_opts(&wasmcloud_opts).await;
441
442    // Avoid downloading + starting NATS if the user already runs their own server and we can connect.
443    let should_run_nats = !cmd.nats_opts.connect_only && nats_client.is_err();
444    // Ignore connect_only if this server has a remote as we have to start a leafnode in that scenario
445    let supplied_remote_credentials = cmd.nats_opts.nats_remote_url.is_some();
446
447    let nats_bin = if should_run_nats || supplied_remote_credentials {
448        // Download NATS if not already installed
449        spinner.update_spinner_message(" Downloading NATS ...".to_string());
450        let nats_binary = ensure_nats_server(&cmd.nats_opts.nats_version, &install_dir).await?;
451
452        spinner.update_spinner_message(" Starting NATS ...".to_string());
453
454        let nats_config = NatsConfig {
455            host: nats_host.clone(),
456            port: nats_port,
457            store_dir: std::env::temp_dir().join(format!("wash-jetstream-{nats_port}")),
458            js_domain: cmd.nats_opts.nats_js_domain,
459            remote_url: cmd.nats_opts.nats_remote_url,
460            credentials: cmd.nats_opts.nats_credsfile.clone(),
461            websocket_port: cmd.nats_opts.nats_websocket_port,
462            config_path: cmd.nats_opts.nats_configfile,
463        };
464        let nats_log_path = install_dir.join("nats.log");
465        start_nats(
466            &install_dir,
467            &nats_binary,
468            nats_config,
469            &nats_log_path,
470            CommandGroupUsage::UseParent,
471        )
472        .await?;
473        Some(nats_binary)
474    } else {
475        // The user is running their own NATS server, so we don't need to download or start one
476        None
477    };
478
479    // Based on the options provided for wasmCloud, form a client connection to NATS.
480    // If this fails, we should return early since wasmCloud wouldn't be able to connect either
481    let client = nats_client_from_wasmcloud_opts(&wasmcloud_opts).await?;
482
483    // Start building the CommandOutput providing some useful information like pids, ports, and logfiles
484    let mut out_json = HashMap::new();
485    let mut out_text = String::from("");
486    let lattice = wasmcloud_opts
487        .clone()
488        .lattice
489        .context("missing lattice prefix")?;
490    let host_started = Arc::new(AtomicBool::new(false));
491    let host_log_path = wasmcloud_opts
492        .host_log_path
493        .clone()
494        .unwrap_or_else(|| install_dir.join("wasmcloud.log"));
495    let ctl_client = wasmcloud_opts.clone().into_ctl_client(None).await?;
496
497    if !cmd.wasmcloud_opts.multi_local
498        && tokio::fs::try_exists(host_pid_file()?)
499            .await
500            .is_ok_and(|exists| exists)
501    {
502        // Check if host is running.
503        let host_state = running_host_count(&ctl_client).await?;
504        if host_state == WasmCloudHostState::NotRunning {
505            eprintln!("🟨 Pid file {:?} exists but no hosts are running. Removing Pid file and proceeding with \"wash up\"",
506            host_pid_file()?);
507            tokio::fs::remove_file(host_pid_file()?).await?;
508        } else if host_state == WasmCloudHostState::MultipleRunning {
509            bail!("🟨 Multiple hosts are running. Please use --multi-local to start another");
510        } else {
511            // Host is already running or starting, so interactive mode cannot continue. Close out as if
512            // detached.
513            spinner.finish_and_clear();
514            if let Some(ref manifest_path) = cmd.wadm_opts.wadm_manifest {
515                eprintln!("🟨 Wasmcloud host is already running. Deploying wadm manifest in detached mode.");
516                out_json.insert("deployed_wadm_manifest_path".into(), json!(manifest_path));
517                // Host has already started, no need to wait.
518                match process_wadm_manifest(
519                    client.clone(),
520                    lattice.clone(),
521                    host_started.clone(),
522                    host_state,
523                    ctl_client,
524                    manifest_path.clone(),
525                    true,
526                )
527                .await
528                {
529                    Ok(_) => out_text.push_str("Deployed wadm manifest"),
530                    Err(e) => {
531                        let _ = write!(out_text, "Deployment failed {}", e);
532                    }
533                };
534            }
535            out_text.push_str("šŸ› wash up completed successfully, already running");
536            out_json.insert("success".to_string(), json!(true));
537            let _ = write!(
538                out_text,
539                "\nšŸ•ø  NATS is running in the background at {nats_listen_address}"
540            );
541
542            let _ = write!(
543                out_text,
544                "\nšŸ“œ Logs for the host are being written to {}",
545                host_log_path.to_string_lossy()
546            );
547            let _ = write!(out_text, "\n\nā¬‡ļø  To stop wasmCloud, run \"wash down\"");
548            return Ok(CommandOutput::new(out_text, out_json));
549        }
550    }
551
552    let wadm_process = if !cmd.wadm_opts.disable_wadm
553        && !is_wadm_running(
554            &nats_host,
555            nats_port,
556            cmd.nats_opts.nats_credsfile.clone(),
557            &lattice,
558        )
559        .await
560        .unwrap_or(false)
561    {
562        spinner.update_spinner_message(" Starting wadm ...".to_string());
563        let config = WadmConfig {
564            structured_logging: wasmcloud_opts.enable_structured_logging,
565            js_domain: cmd.wadm_opts.wadm_js_domain.clone(),
566            nats_server_url: nats_listen_address.clone(),
567            nats_credsfile: cmd.nats_opts.nats_credsfile,
568        };
569        // Start wadm, redirecting output to a log file
570        let wadm_log_path = install_dir.join("wadm.log");
571        let wadm_log_file = tokio::fs::File::create(&wadm_log_path)
572            .await?
573            .into_std()
574            .await;
575
576        let wadm_path =
577            install_patch_or_default_wadm_version(&cmd.wadm_opts.wadm_version, &install_dir).await;
578        match wadm_path {
579            Ok(wadm_bin_path) => {
580                let wadm_child = start_wadm(
581                    &install_dir,
582                    &wadm_bin_path,
583                    wadm_log_file,
584                    Some(config),
585                    CommandGroupUsage::UseParent,
586                )
587                .await;
588                if let Err(e) = &wadm_child {
589                    eprintln!("🟨 Couldn't start wadm: {e}");
590                    None
591                } else {
592                    Some(wadm_child.unwrap())
593                }
594            }
595            Err(e) => {
596                let wadm_version: String = cmd
597                    .wadm_opts
598                    .wadm_version
599                    .unwrap_or(WADM_VERSION.to_string());
600                eprintln!("🟨 Couldn't download wadm {wadm_version}: {e}");
601                if e.to_string().contains("Text file busy") {
602                    eprintln!("šŸ›Ÿ Please ensure there aren't any leftover wadm processes");
603                }
604                None
605            }
606        }
607    } else {
608        None
609    };
610    let wasmcloud_version = if let Some(version) = wasmcloud_opts.wasmcloud_version {
611        version
612    } else if let Some(new_version) = (new_patch_version_of_after_string(
613        GITHUB_WASMCLOUD_ORG,
614        GITHUB_WASMCLOUD_WASMCLOUD_REPO,
615        WASMCLOUD_HOST_VERSION,
616    )
617    .await)
618        .unwrap_or_default()
619    {
620        new_version.to_string()
621    } else {
622        WASMCLOUD_HOST_VERSION.to_string()
623    };
624    let wasmcloud_version = match wasmcloud_version {
625        version if version.starts_with('v') => version,
626        version => format!("v{}", version),
627    };
628    // Download wasmCloud if not already installed
629    let wasmcloud_bin_path = match wasmcloud_opts.host_path {
630        // If an override was provided we can use it
631        Some(path) => {
632            debug!(
633                wasmcloud_bin_path = %path.display(),
634                "using custom wasmcloud binary path"
635            );
636            path
637        }
638        // If start only was not specified, we can download the binary
639        None if !wasmcloud_opts.start_only => {
640            spinner.update_spinner_message(" Downloading wasmCloud ...".to_string());
641
642            ensure_wasmcloud(&wasmcloud_version, &install_dir).await?
643        }
644        // If no override was provided, we must attempt to find the binary
645        None => {
646            if let Some(path) = find_wasmcloud_binary(&install_dir, &wasmcloud_version).await {
647                path
648            } else {
649                // Ensure we clean up the NATS server and wadm if we can't start wasmCloud
650                if let Some(child) = wadm_process {
651                    stop_wadm(child, &install_dir).await?;
652                }
653                if nats_bin.is_some() {
654                    let nats_bin = install_dir.join(NATS_SERVER_BINARY);
655                    stop_nats(install_dir, nats_bin).await?;
656                }
657                bail!("wasmCloud was not installed, exiting without downloading as --wasmcloud-start-only was set");
658            }
659        }
660    };
661
662    // Redirect output (which is on stderr) to a log file in detached mode, or use the terminal
663    spinner.update_spinner_message(" Starting wasmCloud ...".to_string());
664    let stderr: Stdio = if cmd.detached {
665        tokio::fs::File::create(&host_log_path)
666            .await?
667            .into_std()
668            .await
669            .into()
670    } else {
671        Stdio::piped()
672    };
673
674    let mut wasmcloud_child = match start_wasmcloud_host(
675        &wasmcloud_bin_path,
676        std::process::Stdio::null(),
677        stderr,
678        host_env,
679    )
680    .await
681    {
682        Ok(child) => child,
683        Err(e) => {
684            // Ensure we clean up the NATS server and wadm if we can't start wasmCloud
685            if let Some(child) = wadm_process {
686                stop_wadm(child, &install_dir).await?;
687            }
688            if nats_bin.is_some() {
689                let nats_bin = install_dir.join(NATS_SERVER_BINARY);
690                stop_nats(install_dir, nats_bin).await?;
691            }
692            return Err(e);
693        }
694    };
695
696    spinner.finish_and_clear();
697
698    out_json.insert("success".to_string(), json!(true));
699    out_text.push_str("šŸ› wash up completed successfully");
700
701    if let Some(ref manifest_path) = cmd.wadm_opts.wadm_manifest {
702        out_json.insert("deployed_wadm_manifest_path".into(), json!(manifest_path));
703        match process_wadm_manifest(
704            client.clone(),
705            lattice.clone(),
706            host_started.clone(),
707            WasmCloudHostState::NotRunning,
708            ctl_client,
709            manifest_path.clone(),
710            cmd.detached,
711        )
712        .await
713        {
714            Ok(_) => out_text.push_str("Deployed wadm manifest"),
715            Err(e) => {
716                let _ = write!(out_text, "Deployment failed {}", e);
717            }
718        };
719    }
720
721    // Write the pid file with the selected version and process ID.
722    let pid_file_contents = json!({
723        "version": wasmcloud_version,
724        "pid": wasmcloud_child.id().unwrap()
725    });
726
727    tokio::fs::write(host_pid_file()?, pid_file_contents.to_string()).await?;
728
729    // If we're running in detached mode, then we can print out some logs, build output and return early.
730    if cmd.detached {
731        out_json.insert("wasmcloud_log".to_string(), json!(host_log_path));
732        out_json.insert("kill_cmd".to_string(), json!("wash down"));
733        out_json.insert("nats_url".to_string(), json!(nats_listen_address));
734
735        let _ = write!(
736            out_text,
737            "\nšŸ•ø  NATS is running in the background at {nats_listen_address}"
738        );
739
740        let _ = write!(
741            out_text,
742            "\nšŸ“œ Logs for the host are being written to {}",
743            host_log_path.to_string_lossy()
744        );
745        let _ = write!(out_text, "\n\nā¬‡ļø  To stop wasmCloud, run \"wash down\"");
746        return Ok(CommandOutput::new(out_text, out_json));
747    }
748
749    // If we're running in interactive mode, let's start the host
750    run_wasmcloud_interactive(
751        &mut wasmcloud_child,
752        cmd.wadm_opts.wadm_manifest,
753        host_started.clone(),
754        output_kind,
755    )
756    .await?;
757
758    let spinner = Spinner::new(&output_kind)?;
759    spinner.update_spinner_message(
760        // wadm and NATS both exit immediately when sent SIGINT
761        "CTRL+c received, stopping wasmCloud, wadm, and NATS...".to_string(),
762    );
763    stop_wasmcloud(wasmcloud_child).await?;
764    if let Err(e) = tokio::fs::remove_file(host_pid_file()?).await {
765        warn!("failed to remove host pid file: {e}");
766    };
767
768    if let Some(mut wadm_process) = wadm_process {
769        if let Err(e) = wadm_process.kill().await {
770            warn!("failed to kill wadm: {e}");
771        };
772        // remove wadm pidfile, the process is stopped automatically by CTRL+c
773        remove_wadm_pidfile(&install_dir).await?;
774    }
775
776    spinner.finish_and_clear();
777    Ok(CommandOutput::new(out_text, out_json))
778}
779
780/// Check if a wasmcloud host is running
781async fn running_host_count(ctl_client: &CtlClient) -> Result<WasmCloudHostState> {
782    match ctl_client
783        .get_hosts()
784        .await
785        .map_err(|e| anyhow!(e))?
786        .into_iter()
787        .filter_map(|r| r.into_data())
788        .count()
789    {
790        1 => return Ok(WasmCloudHostState::Running),
791        2.. => return Ok(WasmCloudHostState::MultipleRunning),
792        _ => (),
793    }
794
795    // Wasmcloud host might be starting but not up yet. Check if the process in the pid file is running.
796    let pid_file_string = tokio::fs::read_to_string(host_pid_file()?).await?;
797    let pid_file_value: Value = serde_json::from_str(&pid_file_string)?;
798    if let Some(pid) = pid_file_value.get("pid") {
799        if is_process_running(&pid.to_string()) {
800            return Ok(WasmCloudHostState::Starting);
801        }
802    }
803    Ok(WasmCloudHostState::NotRunning)
804}
805
806/// Check is process is running
807fn is_process_running(pid: &str) -> bool {
808    match pid.parse() {
809        Ok(pid) => {
810            let mut sys = System::new_all();
811            sys.refresh_processes();
812            sys.processes().get(&pid).is_some()
813        }
814        Err(_) => false,
815    }
816}
817
818/// Spawn off a task that waits until the host has started,
819/// then loads and deploys the WADM manifest.
820#[allow(clippy::too_many_arguments)]
821fn process_wadm_manifest(
822    client: Client,
823    lattice: String,
824    host_started: Arc<AtomicBool>,
825    host_state: WasmCloudHostState,
826    ctl_client: CtlClient,
827    manifest_path: PathBuf,
828    detached: bool,
829) -> tokio::task::JoinHandle<std::result::Result<(), anyhow::Error>> {
830    // Spawn a task that waits for the host to start
831    tokio::spawn(async move {
832        if detached && host_state < WasmCloudHostState::Running {
833            tokio::time::timeout(tokio::time::Duration::from_secs(3), async {
834                loop {
835                    if let Ok(WasmCloudHostState::Running) = running_host_count(&ctl_client).await {
836                        break;
837                    }
838                }
839            })
840            .await
841            .context("failed to wait for host start while deploying WADM application")?;
842        } else if !detached {
843            // If the host was *not* detached, wait until host_started is updated from run_wasmcloud_interactive()
844            while !host_started.load(Ordering::SeqCst) {
845                tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
846            }
847        };
848
849        // Load the manifest, now that we're done waiting
850        let manifest = load_app_manifest(AppManifestSource::File(manifest_path.to_path_buf()))
851            .await
852            .with_context(|| {
853                format!(
854                    "failed to load manifest from path [{}]",
855                    manifest_path.display()
856                )
857            })?;
858
859        // Deploy the WADM application
860        deploy_wadm_application(&client, manifest, lattice.as_ref())
861            .await
862            .with_context(|| {
863                format!(
864                    "failed to deploy wadm application [{}]",
865                    manifest_path.display()
866                )
867            })?;
868
869        Ok(()) as Result<()>
870    })
871}
872
873/// Helper function to deploy a WADM application (including removing a previous version)
874/// for use when calling `wash up --manifest`
875async fn deploy_wadm_application(
876    client: &Client,
877    manifest: AppManifest,
878    lattice: &str,
879) -> Result<()> {
880    let model_name = manifest.name().context("failed to find model name")?;
881    let _ = wash_lib::app::undeploy_model(client, Some(lattice.into()), model_name).await;
882    match deploy_model_from_manifest(client, Some(lattice.into()), manifest, None).await {
883        // Ignore if the model is already deployed
884        Err(e) if e.to_string().contains("already exists") => {}
885        // All other failures are unexpected
886        Err(e) => bail!(e),
887        _ => {}
888    }
889    Ok(())
890}
891
892/// Helper function to start the NATS binary, redirecting output to nats.log
893pub(crate) async fn start_nats(
894    install_dir: &Path,
895    nats_binary: &Path,
896    nats_config: NatsConfig,
897    nats_log_path: &Path,
898    command_group: CommandGroupUsage,
899) -> Result<Child> {
900    // Ensure that leaf node remote connection can be established before launching NATS
901    if let (Some(url), Some(creds)) = (
902        nats_config.remote_url.as_ref(),
903        nats_config.credentials.as_ref(),
904    ) {
905        if let Err(e) = create_nats_client_from_opts(
906            url,
907            &nats_config.port.to_string(),
908            None,
909            None,
910            Some(creds.to_owned()),
911            None,
912            false,
913        )
914        .await
915        {
916            bail!("Could not connect to leafnode remote: {}", e);
917        }
918    }
919
920    // Start NATS server, redirecting output to a log file
921    let nats_log_file = tokio::fs::File::create(&nats_log_path)
922        .await?
923        .into_std()
924        .await;
925    let nats_process =
926        start_nats_server(nats_binary, nats_log_file, nats_config, command_group).await?;
927    eprintln!(
928        "{} NATS server successfully started, using config @ [{}]",
929        emoji::INFO_SQUARE,
930        nats_binary
931            .parent()
932            .context("unexpectedly missing parent dir")?
933            .join(NATS_SERVER_CONF)
934            .display()
935    );
936    eprintln!(
937        "{} NATS server logs written to [{}]",
938        emoji::INFO_SQUARE,
939        nats_log_path.display()
940    );
941
942    // save the PID so we can kill it later
943    if let Some(pid) = nats_process.id() {
944        let pid_file = nats_pid_path(install_dir);
945        tokio::fs::write(&pid_file, pid.to_string()).await?;
946    }
947
948    Ok(nats_process)
949}
950
951/// Helper function to run an optimistic patch update, but fall back to the previous version if the new version fails
952/// to download.
953async fn install_patch_or_default_wadm_version(
954    version: &Option<String>,
955    install_dir: &Path,
956) -> Result<PathBuf> {
957    if let Some(version) = version {
958        return ensure_wadm(version, install_dir).await;
959    }
960
961    let version = version.clone().unwrap_or_else(|| WADM_VERSION.to_owned());
962    let new_patch_version = (new_patch_version_of_after_string(
963        GITHUB_WASMCLOUD_ORG,
964        GITHUB_WASMCLOUD_WADM_REPO,
965        &version,
966    )
967    .await)
968        .unwrap_or_default();
969    match new_patch_version {
970        Some(new_patch) => {
971            eprintln!(
972                "{} Found a new patch version of wadm: {}",
973                emoji::INFO_SQUARE,
974                new_patch
975            );
976            // Re-add stripped 'v' prefix due to semver parsing
977            let new_version = format!("v{}", new_patch);
978            match ensure_wadm(&new_version, install_dir).await {
979                Ok(path) => Ok(path),
980                Err(e) => {
981                    debug!(
982                        "🟨 Couldn't download the patched wadm {new_version}, falling back to {version}: {e}"
983                    );
984                    ensure_wadm(&version, install_dir).await
985                }
986            }
987        }
988        None => {
989            debug!("No new version found, using the provided: {}", version);
990            ensure_wadm(&version, install_dir).await
991        }
992    }
993}
994
995/// Helper function to run wasmCloud in interactive mode
996async fn run_wasmcloud_interactive(
997    wasmcloud_child: &mut Child,
998    wadm_manifest: Option<PathBuf>,
999    host_started: Arc<AtomicBool>,
1000    output_kind: OutputKind,
1001) -> Result<()> {
1002    use std::sync::mpsc::channel;
1003    let (running_sender, running_receiver) = channel();
1004    let running = Arc::new(AtomicBool::new(true));
1005
1006    // Handle Ctrl + c with Tokio
1007    tokio::spawn(async move {
1008        tokio::signal::ctrl_c()
1009            .await
1010            .context("failed to wait for ctrl_c signal")?;
1011        // Set the host as not running
1012        if running.load(Ordering::SeqCst) {
1013            running.store(false, Ordering::SeqCst);
1014            let _ = running_sender.send(true);
1015        } else {
1016            warn!("\nRepeated CTRL+C received, killing wasmCloud and NATS. This may result in zombie processes")
1017        }
1018        Result::<_, anyhow::Error>::Ok(())
1019    });
1020
1021    if output_kind != OutputKind::Json {
1022        println!("šŸƒ Running in interactive mode.");
1023        if let Some(ref manifest_path) = wadm_manifest {
1024            println!(
1025                "šŸš€ Deploying WADM manifest at [{}]",
1026                manifest_path.display()
1027            );
1028        }
1029        println!("šŸŽ›ļø To start the dashboard, run `wash ui`");
1030        println!("🚪 Press `CTRL+c` at any time to exit");
1031    }
1032
1033    // Create a separate thread to log host output
1034    let handle = wasmcloud_child.stderr.take().map(|stderr| {
1035        tokio::spawn(async {
1036            let mut lines = BufReader::new(stderr).lines();
1037            loop {
1038                if let Ok(Some(line)) = lines.next_line().await {
1039                    // TODO(brooksmtownsend): in the future, would be great to print these in a prettier format
1040                    println!("{line}");
1041                }
1042            }
1043        })
1044    });
1045
1046    // Mark the host as started
1047    host_started.store(true, Ordering::SeqCst);
1048
1049    // Wait for the user to send Ctrl+C in a thread where blocking is acceptable
1050    let _ = running_receiver.recv();
1051
1052    // Prevent extraneous messages from the host getting printed as the host shuts down
1053    if let Some(handle) = handle {
1054        handle.abort()
1055    };
1056    Ok(())
1057}
1058
1059#[cfg(unix)]
1060async fn stop_wasmcloud(mut wasmcloud_child: Child) -> Result<()> {
1061    use nix::sys::signal::{kill, Signal};
1062    use nix::unistd::Pid;
1063
1064    if let Some(pid) = wasmcloud_child.id() {
1065        // Send the SIGTERM signal to ensure that wasmcloud is graceful shutdown.
1066        kill(Pid::from_raw(pid as i32), Signal::SIGTERM)?;
1067
1068        // TODO(iceber): the timeout for the SIGTERM could be added in the future,
1069        // but it doesn't look like it's needed yet.
1070        wasmcloud_child.wait().await?;
1071    }
1072    Ok(())
1073}
1074
1075#[cfg(target_family = "windows")]
1076async fn stop_wasmcloud(mut wasmcloud_child: Child) -> Result<()> {
1077    wasmcloud_child.kill().await?;
1078    Ok(())
1079}
1080
1081async fn is_wadm_running(
1082    nats_host: &str,
1083    nats_port: u16,
1084    credsfile: Option<PathBuf>,
1085    lattice: &str,
1086) -> Result<bool> {
1087    let client = create_nats_client_from_opts(
1088        nats_host,
1089        &nats_port.to_string(),
1090        None,
1091        None,
1092        credsfile,
1093        None,
1094        false,
1095    )
1096    .await?;
1097
1098    Ok(
1099        wash_lib::app::get_models(&client, Some(lattice.to_string()))
1100            .await
1101            .is_ok(),
1102    )
1103}
1104
1105async fn stop_wadm<P>(mut wadm: Child, install_dir: P) -> Result<()>
1106where
1107    P: AsRef<Path>,
1108{
1109    wadm.kill().await?;
1110    remove_wadm_pidfile(install_dir).await
1111}
1112
1113pub(crate) async fn remove_wadm_pidfile<P>(install_dir: P) -> Result<()>
1114where
1115    P: AsRef<Path>,
1116{
1117    if let Err(err) = tokio::fs::remove_file(install_dir.as_ref().join(WADM_PID)).await {
1118        if err.kind() != ErrorKind::NotFound {
1119            bail!(err);
1120        }
1121    }
1122    Ok(())
1123}
1124
1125/// Helper function to create a NATS client from the same arguments wasmCloud will use
1126pub(crate) async fn nats_client_from_wasmcloud_opts(
1127    wasmcloud_opts: &WasmcloudOpts,
1128) -> Result<Client> {
1129    create_nats_client_from_opts(
1130        &wasmcloud_opts
1131            .ctl_host
1132            .clone()
1133            .unwrap_or_else(|| DEFAULT_NATS_HOST.to_string()),
1134        &wasmcloud_opts
1135            .ctl_port
1136            .map(|port| port.to_string())
1137            .unwrap_or_else(|| DEFAULT_NATS_PORT.to_string()),
1138        wasmcloud_opts.ctl_jwt.clone(),
1139        wasmcloud_opts.ctl_seed.clone(),
1140        wasmcloud_opts.ctl_credsfile.clone(),
1141        wasmcloud_opts.ctl_tls_ca_file.clone(),
1142        wasmcloud_opts.ctl_tls_first,
1143    )
1144    .await
1145}
1146
1147#[cfg(test)]
1148mod tests {
1149    use super::UpCommand;
1150    use anyhow::Result;
1151    use clap::Parser;
1152
1153    const LOCAL_REGISTRY: &str = "localhost:5001";
1154
1155    // Assert that our API doesn't unknowingly drift
1156    #[test]
1157    fn test_up_comprehensive() -> Result<()> {
1158        // Not explicitly used, just a placeholder for a directory
1159        const TESTDIR: &str = "./tests/fixtures";
1160
1161        let up_all_flags: UpCommand = Parser::try_parse_from([
1162            "up",
1163            "--allow-latest",
1164            "--allowed-insecure",
1165            LOCAL_REGISTRY,
1166            "--cluster-issuers",
1167            "CBZZ6BLE7PIJNCEJMXOHAJ65KIXRVXDA74W6LUKXC4EPFHTJREXQCOYI",
1168            "--cluster-seed",
1169            "SCAKLQ2FFT4LZUUVQMH6N37US3IZUEVJBUR3V532VV3DAAHSZXPQY6DYIM",
1170            "--config-service-enabled",
1171            "--ctl-credsfile",
1172            TESTDIR,
1173            "--ctl-host",
1174            "127.0.0.2",
1175            "--ctl-jwt",
1176            "eyyjWT",
1177            "--ctl-port",
1178            "4232",
1179            "--ctl-seed",
1180            "SUALIKDKMIUAKRT5536EXKC3CX73TJD3CFXZMJSHIKSP3LTYIIUQGCUVGA",
1181            "--ctl-tls",
1182            "--enable-ipv6",
1183            "--enable-structured-logging",
1184            "--host-seed",
1185            "SNAP4UVNHVWSBJ5MHAQ6M3RB23S3ALA3O3A4RF25G2FQB5CCZJBBBWCKBY",
1186            "--detached",
1187            "--nats-credsfile",
1188            TESTDIR,
1189            "--nats-host",
1190            "127.0.0.2",
1191            "--nats-js-domain",
1192            "domain",
1193            "--nats-port",
1194            "4232",
1195            "--nats-remote-url",
1196            "tls://remote.global",
1197            "--nats-version",
1198            "v2.10.7",
1199            "--provider-delay",
1200            "500",
1201            "--rpc-credsfile",
1202            TESTDIR,
1203            "--rpc-host",
1204            "127.0.0.2",
1205            "--rpc-jwt",
1206            "eyyjWT",
1207            "--rpc-port",
1208            "4232",
1209            "--rpc-seed",
1210            "SUALIKDKMIUAKRT5536EXKC3CX73TJD3CFXZMJSHIKSP3LTYIIUQGCUVGA",
1211            "--rpc-timeout-ms",
1212            "500",
1213            "--rpc-tls",
1214            "--structured-log-level",
1215            "warn",
1216            "--wasmcloud-js-domain",
1217            "domain",
1218            "--wasmcloud-version",
1219            "v0.57.1",
1220            "--lattice",
1221            "anotherprefix",
1222        ])?;
1223        assert!(up_all_flags.wasmcloud_opts.allow_latest);
1224        assert_eq!(
1225            up_all_flags.wasmcloud_opts.allowed_insecure,
1226            Some(vec![LOCAL_REGISTRY.to_string()])
1227        );
1228        assert_eq!(
1229            up_all_flags.wasmcloud_opts.cluster_issuers,
1230            Some(vec![
1231                "CBZZ6BLE7PIJNCEJMXOHAJ65KIXRVXDA74W6LUKXC4EPFHTJREXQCOYI".to_string()
1232            ])
1233        );
1234        assert_eq!(
1235            up_all_flags.wasmcloud_opts.cluster_seed,
1236            Some("SCAKLQ2FFT4LZUUVQMH6N37US3IZUEVJBUR3V532VV3DAAHSZXPQY6DYIM".to_string())
1237        );
1238        assert!(up_all_flags.wasmcloud_opts.config_service_enabled);
1239        assert!(!up_all_flags.nats_opts.connect_only);
1240        assert!(up_all_flags.wasmcloud_opts.ctl_credsfile.is_some());
1241        assert_eq!(
1242            up_all_flags.wasmcloud_opts.ctl_host,
1243            Some("127.0.0.2".to_string())
1244        );
1245        assert_eq!(
1246            up_all_flags.wasmcloud_opts.ctl_jwt,
1247            Some("eyyjWT".to_string())
1248        );
1249        assert_eq!(up_all_flags.wasmcloud_opts.ctl_port, Some(4232));
1250        assert_eq!(
1251            up_all_flags.wasmcloud_opts.ctl_seed,
1252            Some("SUALIKDKMIUAKRT5536EXKC3CX73TJD3CFXZMJSHIKSP3LTYIIUQGCUVGA".to_string())
1253        );
1254        assert!(up_all_flags.wasmcloud_opts.ctl_tls);
1255        assert!(up_all_flags.wasmcloud_opts.rpc_credsfile.is_some());
1256        assert_eq!(
1257            up_all_flags.wasmcloud_opts.rpc_host,
1258            Some("127.0.0.2".to_string())
1259        );
1260        assert_eq!(
1261            up_all_flags.wasmcloud_opts.rpc_jwt,
1262            Some("eyyjWT".to_string())
1263        );
1264        assert_eq!(up_all_flags.wasmcloud_opts.rpc_port, Some(4232));
1265        assert_eq!(
1266            up_all_flags.wasmcloud_opts.rpc_seed,
1267            Some("SUALIKDKMIUAKRT5536EXKC3CX73TJD3CFXZMJSHIKSP3LTYIIUQGCUVGA".to_string())
1268        );
1269        assert!(up_all_flags.wasmcloud_opts.rpc_tls);
1270        assert!(up_all_flags.wasmcloud_opts.enable_ipv6);
1271        assert!(up_all_flags.wasmcloud_opts.enable_structured_logging);
1272        assert_eq!(
1273            up_all_flags.wasmcloud_opts.host_seed,
1274            Some("SNAP4UVNHVWSBJ5MHAQ6M3RB23S3ALA3O3A4RF25G2FQB5CCZJBBBWCKBY".to_string())
1275        );
1276        assert_eq!(
1277            up_all_flags.wasmcloud_opts.structured_log_level,
1278            "warn".to_string()
1279        );
1280        assert_eq!(
1281            up_all_flags.wasmcloud_opts.wasmcloud_version,
1282            Some("v0.57.1".to_string())
1283        );
1284        assert_eq!(
1285            up_all_flags.wasmcloud_opts.lattice.unwrap(),
1286            "anotherprefix".to_string()
1287        );
1288        assert_eq!(
1289            up_all_flags.wasmcloud_opts.wasmcloud_js_domain,
1290            Some("domain".to_string())
1291        );
1292        assert_eq!(up_all_flags.nats_opts.nats_version, "v2.10.7".to_string());
1293        assert_eq!(
1294            up_all_flags.nats_opts.nats_remote_url,
1295            Some("tls://remote.global".to_string())
1296        );
1297        assert_eq!(up_all_flags.wasmcloud_opts.provider_delay, 500);
1298        assert!(up_all_flags.detached);
1299
1300        Ok(())
1301    }
1302
1303    #[test]
1304    fn test_is_process_running() {
1305        let current_pid = std::process::id().to_string();
1306        assert!(
1307            super::is_process_running(&current_pid),
1308            "Current process should be running"
1309        );
1310
1311        let non_existent_pid = "-1";
1312        assert!(
1313            !super::is_process_running(non_existent_pid),
1314            "Non-existent process should not be running"
1315        );
1316
1317        let invalid_pid = "wasmcloud";
1318        assert!(
1319            !super::is_process_running(invalid_pid),
1320            "Invalid pid should not be running"
1321        );
1322    }
1323}