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 #[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 #[clap(
79 long = "nats-credsfile",
80 env = "NATS_CREDSFILE",
81 requires = "nats_remote_url"
82 )]
83 pub nats_credsfile: Option<PathBuf>,
84 #[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 #[clap(long = "nats-remote-url", env = "NATS_REMOTE_URL")]
96 pub nats_remote_url: Option<String>,
97
98 #[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 #[clap(long = "nats-version", default_value = NATS_SERVER_VERSION, env = "NATS_VERSION")]
108 pub nats_version: String,
109
110 #[clap(long = "nats-host", env = "WASMCLOUD_NATS_HOST")]
112 pub nats_host: Option<String>,
113
114 #[clap(long = "nats-port", env = "WASMCLOUD_NATS_PORT")]
116 pub nats_port: Option<u16>,
117
118 #[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 #[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 #[clap(long = "wasmcloud-version", env = "WASMCLOUD_VERSION")]
162 pub wasmcloud_version: Option<String>,
163
164 #[clap(
166 short = 'x',
167 long = "lattice",
168 env = WASMCLOUD_LATTICE
169 )]
170 pub lattice: Option<String>,
171
172 #[clap(long = "host-seed", env = WASMCLOUD_HOST_SEED)]
174 pub host_seed: Option<String>,
175
176 #[clap(long = "rpc-host", env = WASMCLOUD_RPC_HOST)]
178 pub rpc_host: Option<String>,
179
180 #[clap(long = "rpc-port", env = WASMCLOUD_RPC_PORT)]
182 pub rpc_port: Option<u16>,
183
184 #[clap(long = "rpc-seed", env = WASMCLOUD_RPC_SEED, requires = "rpc_jwt")]
186 pub rpc_seed: Option<String>,
187
188 #[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 #[clap(long = "rpc-jwt", env = WASMCLOUD_RPC_JWT, requires = "rpc_seed")]
194 pub rpc_jwt: Option<String>,
195
196 #[clap(long = "rpc-tls", env = WASMCLOUD_RPC_TLS)]
198 pub rpc_tls: bool,
199
200 #[clap(long = "rpc-tls-first", env = WASMCLOUD_RPC_TLS_FIRST, requires = "rpc_tls")]
202 pub rpc_tls_first: bool,
203
204 #[clap(long = "rpc-tls-ca-file", env = WASMCLOUD_RPC_TLS_CA_FILE)]
206 pub rpc_tls_ca_file: Option<PathBuf>,
207
208 #[clap(long = "rpc-credsfile", env = WASMCLOUD_RPC_CREDSFILE)]
210 pub rpc_credsfile: Option<PathBuf>,
211
212 #[clap(long = "ctl-host", env = WASMCLOUD_CTL_HOST)]
214 pub ctl_host: Option<String>,
215
216 #[clap(long = "ctl-port", env = WASMCLOUD_CTL_PORT)]
218 pub ctl_port: Option<u16>,
219
220 #[clap(long = "ctl-seed", env = WASMCLOUD_CTL_SEED, requires = "ctl_jwt")]
222 pub ctl_seed: Option<String>,
223
224 #[clap(long = "ctl-jwt", env = WASMCLOUD_CTL_JWT, requires = "ctl_seed")]
226 pub ctl_jwt: Option<String>,
227
228 #[clap(long = "ctl-credsfile", env = WASMCLOUD_CTL_CREDSFILE)]
230 pub ctl_credsfile: Option<PathBuf>,
231
232 #[clap(long = "ctl-tls", env = WASMCLOUD_CTL_TLS)]
234 pub ctl_tls: bool,
235
236 #[clap(long = "ctl-tls-first", env = WASMCLOUD_CTL_TLS_FIRST, requires = "ctl_tls")]
238 pub ctl_tls_first: bool,
239
240 #[clap(long = "ctl-tls-ca-file", env = WASMCLOUD_CTL_TLS_CA_FILE)]
242 pub ctl_tls_ca_file: Option<PathBuf>,
243
244 #[clap(long = "cluster-seed", env = WASMCLOUD_CLUSTER_SEED)]
246 pub cluster_seed: Option<String>,
247
248 #[clap(long = "cluster-issuers", env = WASMCLOUD_CLUSTER_ISSUERS)]
250 pub cluster_issuers: Option<Vec<String>>,
251
252 #[clap(long = "provider-delay", default_value = DEFAULT_PROV_SHUTDOWN_DELAY_MS, env = WASMCLOUD_PROV_SHUTDOWN_DELAY_MS)]
254 pub provider_delay: u32,
255
256 #[clap(long = "allow-latest", env = WASMCLOUD_OCI_ALLOW_LATEST)]
258 pub allow_latest: bool,
259
260 #[clap(long = "allowed-insecure", env = WASMCLOUD_OCI_ALLOWED_INSECURE)]
262 pub allowed_insecure: Option<Vec<String>>,
263
264 #[clap(long = "wasmcloud-js-domain", env = WASMCLOUD_JS_DOMAIN)]
266 pub wasmcloud_js_domain: Option<String>,
267
268 #[clap(long = "config-service-enabled", env = WASMCLOUD_CONFIG_SERVICE)]
270 pub config_service_enabled: bool,
271
272 #[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 #[clap(
278 long = "enable-structured-logging",
279 env = WASMCLOUD_STRUCTURED_LOGGING_ENABLED
280 )]
281 pub enable_structured_logging: bool,
282
283 #[clap(short = 'l', long = "label", alias = "labels")]
285 pub label: Option<Vec<String>>,
286
287 #[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 #[clap(long = "enable-ipv6", env = WASMCLOUD_ENABLE_IPV6)]
293 pub enable_ipv6: bool,
294
295 #[clap(long = "wasmcloud-start-only")]
297 pub start_only: bool,
298
299 #[clap(long = "multi-local")]
301 pub multi_local: bool,
302
303 #[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 #[clap(long = "secrets-topic", env = WASMCLOUD_SECRETS_TOPIC)]
309 pub secrets_topic: Option<String>,
310
311 #[clap(long = "policy-topic", env = WASMCLOUD_POLICY_TOPIC)]
313 pub policy_topic: Option<String>,
314
315 #[clap(long = "host-log-path", env = WASMCLOUD_HOST_LOG_PATH)]
317 pub host_log_path: Option<PathBuf>,
318
319 #[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 #[clap(long = "wadm-version", env = "WADM_VERSION")]
375 pub wadm_version: Option<String>,
376
377 #[clap(long = "disable-wadm")]
379 pub disable_wadm: bool,
380
381 #[clap(long = "wadm-js-domain", env = "WADM_JS_DOMAIN")]
383 pub wadm_js_domain: Option<String>,
384
385 #[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 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 let should_run_nats = !cmd.nats_opts.connect_only && nats_client.is_err();
444 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 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 None
477 };
478
479 let client = nats_client_from_wasmcloud_opts(&wasmcloud_opts).await?;
482
483 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 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 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 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 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 let wasmcloud_bin_path = match wasmcloud_opts.host_path {
630 Some(path) => {
632 debug!(
633 wasmcloud_bin_path = %path.display(),
634 "using custom wasmcloud binary path"
635 );
636 path
637 }
638 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 None => {
646 if let Some(path) = find_wasmcloud_binary(&install_dir, &wasmcloud_version).await {
647 path
648 } else {
649 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 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 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 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 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 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 "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(&install_dir).await?;
774 }
775
776 spinner.finish_and_clear();
777 Ok(CommandOutput::new(out_text, out_json))
778}
779
780async 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 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
806fn 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#[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 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 while !host_started.load(Ordering::SeqCst) {
845 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
846 }
847 };
848
849 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_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
873async 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 Err(e) if e.to_string().contains("already exists") => {}
885 Err(e) => bail!(e),
887 _ => {}
888 }
889 Ok(())
890}
891
892pub(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 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 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 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
951async 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 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
995async 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 tokio::spawn(async move {
1008 tokio::signal::ctrl_c()
1009 .await
1010 .context("failed to wait for ctrl_c signal")?;
1011 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 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 println!("{line}");
1041 }
1042 }
1043 })
1044 });
1045
1046 host_started.store(true, Ordering::SeqCst);
1048
1049 let _ = running_receiver.recv();
1051
1052 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 kill(Pid::from_raw(pid as i32), Signal::SIGTERM)?;
1067
1068 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
1125pub(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 #[test]
1157 fn test_up_comprehensive() -> Result<()> {
1158 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(¤t_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}