From be061e1b968ad8f9f784a6400030aaf58378bd4a Mon Sep 17 00:00:00 2001 From: Matheus Cardoso Date: Wed, 25 Jan 2023 12:11:17 -0300 Subject: [PATCH] chore: update clap dependency from v3 to v4 Signed-off-by: Matheus Cardoso --- .vscode/launch.json | 538 ++++++++++++++++++ Cargo.lock | 70 ++- Cargo.toml | 7 +- crates/abi-conformance/Cargo.toml | 2 +- .../src/bin/spin-abi-conformance.rs | 8 +- crates/http/Cargo.toml | 2 +- crates/http/src/lib.rs | 97 +++- crates/trigger/Cargo.toml | 2 +- crates/trigger/src/cli.rs | 69 +-- src/args.rs | 5 + src/args/app_source.rs | 125 ++++ src/args/cli.rs | 215 +++++++ src/args/component.rs | 32 ++ src/args/manifest_file.rs | 64 +++ src/args/temp.rs | 41 ++ src/bin/spin.rs | 83 +-- src/commands.rs | 3 + src/commands/bindle.rs | 92 +-- src/commands/build.rs | 59 +- src/commands/deploy.rs | 57 +- src/commands/external.rs | 25 +- src/commands/generate_completions.rs | 66 +++ src/commands/login.rs | 112 ++-- src/commands/new.rs | 44 +- src/commands/oci.rs | 49 +- src/commands/plugins.rs | 176 +++--- src/commands/templates.rs | 48 +- src/commands/up.rs | 352 +++++------- src/dispatch.rs | 33 ++ src/dispatch/macros.rs | 113 ++++ src/lib.rs | 4 +- src/opts.rs | 2 - tests/integration.rs | 40 +- 33 files changed, 1968 insertions(+), 667 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 src/args.rs create mode 100644 src/args/app_source.rs create mode 100644 src/args/cli.rs create mode 100644 src/args/component.rs create mode 100644 src/args/manifest_file.rs create mode 100644 src/args/temp.rs create mode 100644 src/commands/generate_completions.rs create mode 100644 src/dispatch.rs create mode 100644 src/dispatch/macros.rs diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..dbe3aab35d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,538 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'spin-abi-conformance'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=spin-abi-conformance" + ], + "filter": { + "name": "spin-abi-conformance", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'spin-abi-conformance'", + "cargo": { + "args": [ + "build", + "--bin=spin-abi-conformance", + "--package=spin-abi-conformance" + ], + "filter": { + "name": "spin-abi-conformance", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'spin-abi-conformance'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=spin-abi-conformance", + "--package=spin-abi-conformance" + ], + "filter": { + "name": "spin-abi-conformance", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'spin-app'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=spin-app" + ], + "filter": { + "name": "spin-app", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'spin-core'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=spin-core" + ], + "filter": { + "name": "spin-core", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug integration test 'integration_test'", + "cargo": { + "args": [ + "test", + "--no-run", + "--test=integration_test", + "--package=spin-core" + ], + "filter": { + "name": "integration_test", + "kind": "test" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'spin-build'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=spin-build" + ], + "filter": { + "name": "spin-build", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'spin-loader'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=spin-loader" + ], + "filter": { + "name": "spin-loader", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'outbound-http'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=outbound-http" + ], + "filter": { + "name": "outbound-http", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'spin-manifest'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=spin-manifest" + ], + "filter": { + "name": "spin-manifest", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'spin-config'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=spin-config" + ], + "filter": { + "name": "spin-config", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'spin-http'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=spin-http" + ], + "filter": { + "name": "spin-http", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug benchmark 'baseline'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bench=baseline", + "--package=spin-http" + ], + "filter": { + "name": "baseline", + "kind": "bench" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'spin-trigger'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=spin-trigger" + ], + "filter": { + "name": "spin-trigger", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'outbound-mysql'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=outbound-mysql" + ], + "filter": { + "name": "outbound-mysql", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'outbound-pg'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=outbound-pg" + ], + "filter": { + "name": "outbound-pg", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'outbound-redis'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=outbound-redis" + ], + "filter": { + "name": "outbound-redis", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'spin-testing'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=spin-testing" + ], + "filter": { + "name": "spin-testing", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'spin-plugins'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=spin-plugins" + ], + "filter": { + "name": "spin-plugins", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'spin-redis-engine'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=spin-redis-engine" + ], + "filter": { + "name": "spin-redis-engine", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'spin-templates'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=spin-templates" + ], + "filter": { + "name": "spin-templates", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'spin_sdk'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=spin-sdk" + ], + "filter": { + "name": "spin_sdk", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'spin-cli'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=spin-cli" + ], + "filter": { + "name": "spin-cli", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'spin'", + "cargo": { + "args": [ + "build", + "--bin=spin", + "--package=spin-cli" + ], + "filter": { + "name": "spin", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'spin'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=spin", + "--package=spin-cli" + ], + "filter": { + "name": "spin", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug integration test 'integration'", + "cargo": { + "args": [ + "test", + "--no-run", + "--test=integration", + "--package=spin-cli" + ], + "filter": { + "name": "integration", + "kind": "test" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'cloud'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=cloud" + ], + "filter": { + "name": "cloud", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'spin-publish'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=spin-publish" + ], + "filter": { + "name": "spin-publish", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index f3daf2d997..2943fe55a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -537,8 +537,8 @@ checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" dependencies = [ "atty", "bitflags", - "clap_derive", - "clap_lex", + "clap_derive 3.2.18", + "clap_lex 0.2.4", "indexmap", "once_cell", "strsim", @@ -546,6 +546,40 @@ dependencies = [ "textwrap 0.16.0", ] +[[package]] +name = "clap" +version = "4.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f13b9c79b5d1dd500d20ef541215a6423c75829ef43117e1b4d17fd8af0b5d76" +dependencies = [ + "bitflags", + "clap_derive 4.1.0", + "clap_lex 0.3.1", + "is-terminal 0.4.2", + "once_cell", + "strsim", + "termcolor", +] + +[[package]] +name = "clap_complete" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6540eedc41f8a5a76cf3d8d458057dcdf817be4158a55b5f861f7a5483de75" +dependencies = [ + "clap 4.1.4", +] + +[[package]] +name = "clap_complete_fig" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf0c76d8fcf782a1102ccfcd10ca8246e7fdd609c1cd6deddbb96cb638e9bb5c" +dependencies = [ + "clap 4.1.4", + "clap_complete", +] + [[package]] name = "clap_derive" version = "3.2.18" @@ -559,6 +593,19 @@ dependencies = [ "syn", ] +[[package]] +name = "clap_derive" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "684a277d672e91966334af371f1a7b5833f9aa00b07c84e92fbce95e00208ce8" +dependencies = [ + "heck 0.4.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "clap_lex" version = "0.2.4" @@ -568,6 +615,15 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "clap_lex" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "783fe232adfca04f90f56201b26d79682d4cd2625e0bc7290b95123afe558ade" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "cloud" version = "0.8.0" @@ -4108,7 +4164,7 @@ version = "0.8.0" dependencies = [ "anyhow", "cap-std", - "clap 3.2.23", + "clap 4.1.4", "rand 0.8.5", "rand_chacha 0.3.1", "rand_core 0.6.4", @@ -4159,7 +4215,9 @@ dependencies = [ "bytes", "cargo-target-dep", "chrono", - "clap 3.2.23", + "clap 4.1.4", + "clap_complete", + "clap_complete_fig", "cloud", "cloud-openapi", "comfy-table", @@ -4249,7 +4307,7 @@ version = "0.8.0" dependencies = [ "anyhow", "async-trait", - "clap 3.2.23", + "clap 4.1.4", "criterion", "futures", "futures-util", @@ -4470,7 +4528,7 @@ version = "0.8.0" dependencies = [ "anyhow", "async-trait", - "clap 3.2.23", + "clap 4.1.4", "ctrlc", "dirs 4.0.0", "futures", diff --git a/Cargo.toml b/Cargo.toml index f2c723bbb9..c3402017cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,9 @@ async-trait = "0.1" bindle = { workspace = true } bytes = "1.1" chrono = "0.4" -clap = { version = "3.1.15", features = ["derive", "env"] } +clap = { version = "4.1", features = ["derive", "env"] } +clap_complete = { version = "4.1", optional = true } +clap_complete_fig = { version = "4.1", optional = true } cloud = { path = "crates/cloud" } cloud-openapi = { git = "https://github.com/fermyon/cloud-openapi" } comfy-table = "5.0" @@ -80,12 +82,13 @@ vergen = { version = "7", default-features = false, features = [ ] } [features] -default = [] +default = ["generate-completions"] e2e-tests = [] outbound-redis-tests = [] config-provider-tests = [] outbound-pg-tests = [] outbound-mysql-tests = [] +generate-completions = ["dep:clap_complete", "dep:clap_complete_fig"] [workspace] members = [ diff --git a/crates/abi-conformance/Cargo.toml b/crates/abi-conformance/Cargo.toml index cb52b7d9c5..a49b307890 100644 --- a/crates/abi-conformance/Cargo.toml +++ b/crates/abi-conformance/Cargo.toml @@ -7,7 +7,7 @@ edition = { workspace = true } [dependencies] anyhow = "1.0.44" cap-std = "0.26.1" -clap = { version = "3.1.15", features = ["derive", "env"] } +clap = { version = "4.1", features = ["derive", "env"] } rand = "0.8.5" rand_chacha = "0.3.1" rand_core = "0.6.3" diff --git a/crates/abi-conformance/src/bin/spin-abi-conformance.rs b/crates/abi-conformance/src/bin/spin-abi-conformance.rs index 66cfda0ad4..430a4d6a05 100644 --- a/crates/abi-conformance/src/bin/spin-abi-conformance.rs +++ b/crates/abi-conformance/src/bin/spin-abi-conformance.rs @@ -8,18 +8,18 @@ use std::{ use wasmtime::{Config, Engine, Module}; #[derive(Parser)] -#[clap(author, version, about)] +#[command(author, version, about)] pub struct Options { /// Name of Wasm file to test (or stdin if not specified) - #[clap(short, long)] + #[arg(short, long)] pub input: Option, /// Name of JSON file to write report to (or stdout if not specified) - #[clap(short, long)] + #[arg(short, long)] pub output: Option, /// Name of TOML configuration file to use - #[clap(short, long)] + #[arg(short, long)] pub config: Option, } diff --git a/crates/http/Cargo.toml b/crates/http/Cargo.toml index e0d196b6ff..8b56bbd05f 100644 --- a/crates/http/Cargo.toml +++ b/crates/http/Cargo.toml @@ -10,7 +10,7 @@ doctest = false [dependencies] anyhow = "1.0" async-trait = "0.1" -clap = "3" +clap = "4.1" futures = "0.3" futures-util = "0.3.8" http = "0.2" diff --git a/crates/http/src/lib.rs b/crates/http/src/lib.rs index afbad7aadc..a1c8c9fb6f 100644 --- a/crates/http/src/lib.rs +++ b/crates/http/src/lib.rs @@ -8,9 +8,9 @@ mod wagi; use std::{ collections::HashMap, future::ready, - net::{Ipv4Addr, SocketAddr, ToSocketAddrs}, + net::{Ipv4Addr, SocketAddr, SocketAddrV4, ToSocketAddrs}, path::PathBuf, - sync::Arc, + sync::Arc, str::FromStr, fmt::Display, }; use anyhow::{Context, Error, Result}; @@ -56,18 +56,71 @@ pub struct HttpTrigger { component_trigger_configs: HashMap, } -#[derive(Args)] +#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +pub struct SocketAddress(SocketAddr); + +impl SocketAddress { + pub fn new(v4: Ipv4Addr, port: u16) -> Self { + Self(SocketAddr::V4(SocketAddrV4::new(v4, port))) + } +} + +impl Into for SocketAddress { + fn into(self) -> SocketAddr { + return self.0 + } +} + +impl ToSocketAddrs for SocketAddress { + type Iter = ::Iter; + + fn to_socket_addrs(&self) -> std::io::Result { + self.0.to_socket_addrs() + } +} + + + +impl Display for SocketAddress { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Default for SocketAddress { + fn default() -> Self { + Self::new(Ipv4Addr::LOCALHOST, 3000) + } +} + +impl FromStr for SocketAddress { + type Err = anyhow::Error; + fn from_str(addr: &str) -> Result { + let addrs: Vec = addr.to_socket_addrs()?.collect(); + // Prefer 127.0.0.1 over e.g. [::1] because CHANGE IS HARD + if let Some(addr) = addrs + .iter() + .find(|addr| addr.is_ipv4() && addr.ip() == Ipv4Addr::LOCALHOST) + { + return Ok(Self(*addr)); + } + // Otherwise, take the first addr (OS preference) + addrs.into_iter().next().context("couldn't resolve address").map(Self) + } +} + +#[derive(Args, Clone)] pub struct CliArgs { /// IP address and port to listen on - #[clap(long = "listen", default_value = "127.0.0.1:3000", value_parser = parse_listen_addr)] - pub address: SocketAddr, + #[arg(long = "listen", default_value_t = SocketAddress::default())] + pub address: SocketAddress, /// The path to the certificate to use for https, if this is not set, normal http will be used. The cert should be in PEM format - #[clap(long, env = "SPIN_TLS_CERT", requires = "tls-key")] + #[arg(long, env = "SPIN_TLS_CERT", requires = "tls_key")] pub tls_cert: Option, /// The path to the certificate key to use for https, if this is not set, normal http will be used. The key should be in PKCS#8 format - #[clap(long, env = "SPIN_TLS_KEY", requires = "tls-cert")] + #[arg(long, env = "SPIN_TLS_KEY", requires = "tls_cert")] pub tls_key: Option, } @@ -177,11 +230,10 @@ impl TriggerExecutor for HttpTrigger { } if let Some(tls) = tls { - self.serve_tls(listen_addr, tls).await? + self.serve_tls(listen_addr, tls).await } else { - self.serve(listen_addr).await? - }; - Ok(()) + self.serve(listen_addr).await + } } } @@ -293,7 +345,7 @@ impl HttpTrigger { .body(Body::empty())?) } - async fn serve(self, listen_addr: SocketAddr) -> Result<()> { + async fn serve(self, listen_addr: SocketAddress) -> Result<()> { let self_ = Arc::new(self); let make_service = make_service_fn(|conn: &AddrStream| { let self_ = self_.clone(); @@ -307,14 +359,14 @@ impl HttpTrigger { } }); - Server::try_bind(&listen_addr) + Server::try_bind(&listen_addr.into()) .with_context(|| format!("Unable to listen on {}", listen_addr))? .serve(make_service) .await?; Ok(()) } - async fn serve_tls(self, listen_addr: SocketAddr, tls: TlsConfig) -> Result<()> { + async fn serve_tls(self, listen_addr: SocketAddress, tls: TlsConfig) -> Result<()> { let self_ = Arc::new(self); let make_service = make_service_fn(|conn: &TlsStream| { let self_ = self_.clone(); @@ -340,7 +392,7 @@ impl HttpTrigger { } }); - let listener = TcpListener::bind(&listen_addr) + let listener = TcpListener::bind::(listen_addr.into()) .await .with_context(|| format!("Unable to listen on {}", listen_addr))?; @@ -369,19 +421,6 @@ pub struct AppInfo { pub bindle_version: Option, } -fn parse_listen_addr(addr: &str) -> anyhow::Result { - let addrs: Vec = addr.to_socket_addrs()?.collect(); - // Prefer 127.0.0.1 over e.g. [::1] because CHANGE IS HARD - if let Some(addr) = addrs - .iter() - .find(|addr| addr.is_ipv4() && addr.ip() == Ipv4Addr::LOCALHOST) - { - return Ok(*addr); - } - // Otherwise, take the first addr (OS preference) - addrs.into_iter().next().context("couldn't resolve address") -} - fn set_req_uri(req: &mut Request, scheme: Scheme) -> Result<()> { const DEFAULT_HOST: &str = "localhost"; @@ -649,7 +688,7 @@ mod tests { #[test] fn parse_listen_addr_prefers_ipv4() { - let addr = parse_listen_addr("localhost:12345").unwrap(); + let addr = SocketAddr::from_str("localhost:12345").unwrap(); assert_eq!(addr.ip(), Ipv4Addr::LOCALHOST); assert_eq!(addr.port(), 12345); } diff --git a/crates/trigger/Cargo.toml b/crates/trigger/Cargo.toml index 592f242b0c..f8685441e4 100644 --- a/crates/trigger/Cargo.toml +++ b/crates/trigger/Cargo.toml @@ -7,7 +7,7 @@ edition = { workspace = true } [dependencies] anyhow = "1.0" async-trait = "0.1" -clap = { version = "3.1.15", features = ["derive", "env"] } +clap = { version = "4.1", features = ["derive", "env"] } ctrlc = { version = "3.2", features = ["termination"] } dirs = "4" futures = "0.3" diff --git a/crates/trigger/src/cli.rs b/crates/trigger/src/cli.rs index b619c4a788..162a165687 100644 --- a/crates/trigger/src/cli.rs +++ b/crates/trigger/src/cli.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use anyhow::{Context, Result}; -use clap::{Args, IntoApp, Parser}; +use clap::{Args, CommandFactory, Parser}; use serde::de::DeserializeOwned; use tokio::{ task::JoinHandle, @@ -11,6 +11,7 @@ use tokio::{ use crate::{config::TriggerExecutorBuilderConfig, loader::TriggerLoader, stdio::FollowComponents}; use crate::{loader::OciTriggerLoader, stdio::StdioLoggingTriggerHooks}; use crate::{TriggerExecutor, TriggerExecutorBuilder}; +use async_trait::async_trait; pub const APP_LOG_DIR: &str = "APP_LOG_DIR"; pub const DISABLE_WASMTIME_CACHE: &str = "DISABLE_WASMTIME_CACHE"; @@ -24,87 +25,91 @@ pub const SPIN_WORKING_DIR: &str = "SPIN_WORKING_DIR"; /// A command that runs a TriggerExecutor. #[derive(Parser, Debug)] -#[clap(next_help_heading = "TRIGGER OPTIONS")] +#[command(next_help_heading = "TRIGGER OPTIONS")] pub struct TriggerExecutorCommand where Executor::RunConfig: Args, { /// Log directory for the stdout and stderr of components. - #[clap( - name = APP_LOG_DIR, - short = 'L', - long = "log-dir", - )] + #[arg( + short = 'L', + long = "log-dir", + id = APP_LOG_DIR, + )] pub log: Option, /// Disable Wasmtime cache. - #[clap( - name = DISABLE_WASMTIME_CACHE, + #[arg( long = "disable-cache", + id = DISABLE_WASMTIME_CACHE, env = DISABLE_WASMTIME_CACHE, - conflicts_with = WASMTIME_CACHE_FILE, - takes_value = false, + conflicts_with = WASMTIME_CACHE_FILE )] pub disable_cache: bool, /// Wasmtime cache configuration file. - #[clap( - name = WASMTIME_CACHE_FILE, + #[arg( long = "cache", + id = WASMTIME_CACHE_FILE, env = WASMTIME_CACHE_FILE, conflicts_with = DISABLE_WASMTIME_CACHE, )] pub cache: Option, /// Print output for given component(s) to stdout/stderr - #[clap( - name = FOLLOW_LOG_OPT, + #[arg( long = "follow", - multiple_occurrences = true, - )] + id = FOLLOW_LOG_OPT + )] pub follow_components: Vec, /// Print all component output to stdout/stderr - #[clap( + #[arg( long = "follow-all", - conflicts_with = FOLLOW_LOG_OPT, - )] + conflicts_with = FOLLOW_LOG_OPT + )] pub follow_all_components: bool, /// Set the static assets of the components in the temporary directory as writable. - #[clap(long = "allow-transient-write")] + #[arg(long)] pub allow_transient_write: bool, /// Configuration file for config providers and wasmtime config. - #[clap( - name = RUNTIME_CONFIG_FILE, + #[arg( long = "runtime-config-file", + id = RUNTIME_CONFIG_FILE, env = RUNTIME_CONFIG_FILE, )] pub runtime_config_file: Option, - #[clap(flatten)] + #[command(flatten)] pub run_config: Executor::RunConfig, - #[clap(long = "help-args-only", hide = true)] + #[arg(long, hide = true)] pub help_args_only: bool, - #[clap(long = "oci")] + #[arg(long = "oci")] pub oci: bool, } /// An empty implementation of clap::Args to be used as TriggerExecutor::RunConfig /// for executors that do not need additional CLI args. -#[derive(Args)] +#[derive(Args, Clone)] pub struct NoArgs; impl TriggerExecutorCommand where - Executor::RunConfig: Args, + Executor::RunConfig: Args + Clone, Executor::TriggerConfig: DeserializeOwned, { + pub async fn help(&self) -> Result<()> { + Ok(Self::command() + .disable_help_flag(true) + .help_template("{all-args}") + .print_long_help()?) + } /// Create a new TriggerExecutorBuilder from this TriggerExecutorCommand. - pub async fn run(self) -> Result<()> { + pub async fn run(&self) -> Result<()> { if self.help_args_only { Self::command() .disable_help_flag(true) @@ -133,7 +138,7 @@ where self.update_wasmtime_config(builder.wasmtime_config_mut())?; let logging_hooks = - StdioLoggingTriggerHooks::new(self.follow_components(), self.log); + StdioLoggingTriggerHooks::new(self.follow_components(), self.log.clone()); builder.hooks(logging_hooks); builder.build(locked_url, trigger_config).await? @@ -150,14 +155,14 @@ where self.update_wasmtime_config(builder.wasmtime_config_mut())?; let logging_hooks = - StdioLoggingTriggerHooks::new(self.follow_components(), self.log); + StdioLoggingTriggerHooks::new(self.follow_components(), self.log.clone()); builder.hooks(logging_hooks); builder.build(locked_url, trigger_config).await? } }; - let run_fut = executor.run(self.run_config); + let run_fut = executor.run(self.run_config.clone()); let (abortable, abort_handle) = futures::future::abortable(run_fut); ctrlc::set_handler(move || abort_handle.abort())?; diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000000..dd4e432203 --- /dev/null +++ b/src/args.rs @@ -0,0 +1,5 @@ +pub mod cli; +pub mod component; +pub mod app_source; +pub mod manifest_file; +pub mod temp; diff --git a/src/args/app_source.rs b/src/args/app_source.rs new file mode 100644 index 0000000000..9969a0faee --- /dev/null +++ b/src/args/app_source.rs @@ -0,0 +1,125 @@ +use crate::{args::manifest_file::ManifestFile, commands::up::UpCommand, opts::BINDLE_URL_ENV}; +use anyhow::Result; +use clap::{error::ErrorKind, Args, Command, CommandFactory, Error, FromArgMatches, Id}; +use spin_manifest::Application; +use std::path::PathBuf; + +#[derive(Clone, Debug)] +pub enum AppSource { + Local(LocalSource), + Bindle(BindleSource), +} + +impl Default for AppSource { + fn default() -> Self { + Self::Local(LocalSource::default()) + } +} + +impl AppSource { + pub async fn load(&self, working_dir: &PathBuf) -> Result { + match self { + AppSource::Local(app) => { + spin_loader::from_file( + &app.file, + if app.direct_mounts { + Some(&working_dir) + } else { + None + }, + &None, + ) + .await + } + AppSource::Bindle(bindle) => { + spin_loader::from_bindle( + bindle.bindle.as_str(), + bindle.bindle_server.as_str(), + &working_dir, + ) + .await + } + } + .map_err(|_| Error::new(ErrorKind::Io).with_cmd(&UpCommand::command())) + } +} + +impl FromArgMatches for AppSource { + fn from_arg_matches(matches: &clap::ArgMatches) -> Result { + let bindle = BindleSource::from_arg_matches(matches); + let local = LocalSource::from_arg_matches(matches); + + if matches.contains_id("BindleOptions") && matches.contains_id("LocalOptions") { + return Err(Error::new(ErrorKind::ArgumentConflict)); + } + + match (local, bindle) { + (Err(local), Err(_)) => return Err(local), + (Ok(local), _) => Ok(Self::Local(local)), + (_, Ok(bindle)) => Ok(Self::Bindle(bindle)), + } + } + + fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), Error> { + Ok(*self = Self::from_arg_matches(matches)?) + } +} + +impl Args for AppSource { + fn group_id() -> Option { + None + } + fn augment_args(cmd: Command) -> Command { + BindleSource::augment_args(LocalSource::augment_args(cmd)) + } + + fn augment_args_for_update(cmd: Command) -> Command { + BindleSource::augment_args_for_update(LocalSource::augment_args_for_update(cmd)) + } +} + +#[derive(Args, Clone, Debug, Default)] +#[command(next_help_heading = "Local Options")] +pub struct LocalSource { + /// Path to spin.toml + #[arg(long, short, default_value_t = ManifestFile::default(), required = false)] + file: ManifestFile, + + /// For local apps with directory mounts and no excluded files, mount them directly instead of using a temporary + /// directory. + /// + /// This allows you to update the assets on the host filesystem such that the updates are visible to the guest + /// without a restart. This cannot be used with bindle apps or apps which use file patterns and/or exclusions. + #[arg(long)] + direct_mounts: bool, +} + +impl LocalSource { + pub fn new(file: ManifestFile, direct_mounts: bool) -> Self { + Self { + file, + direct_mounts, + } + } +} + +#[derive(Args, Clone, Debug, Default)] +#[command(next_help_heading = "Bindle Options")] +pub struct BindleSource { + #[arg(long, short, required = false)] + bindle: String, + /// URL of bindle server. + #[arg(long, env = BINDLE_URL_ENV, required = false)] + bindle_server: String, + + /// Basic http auth username for the bindle server + #[arg(long, env, requires = "bindle_password")] + bindle_username: Option, + /// Basic http auth password for the bindle server + #[arg(long, env, requires = "bindle_username")] + bindle_password: Option, + + /// Ignore server certificate errors from bindle server + #[arg(short = 'k', long)] + insecure: bool, +} diff --git a/src/args/cli.rs b/src/args/cli.rs new file mode 100644 index 0000000000..26a20c8fa0 --- /dev/null +++ b/src/args/cli.rs @@ -0,0 +1,215 @@ +use std::ffi::OsString; + +use clap::{Arg, Args, CommandFactory, FromArgMatches, Parser}; + +fn format_error(err: clap::Error) -> clap::Error { + let mut cmd = I::command(); + err.format(&mut cmd) +} + +pub struct App

+where + P: Parser + CommandFactory, +{ + parser: P, +} + +impl

App

+where + P: Parser + CommandFactory, +{ + pub fn get_parser(self) -> P { + self.parser + } +} + +impl

CommandFactory for App

+where + P: Parser + CommandFactory, +{ + fn command() -> clap::Command { + P::command() + } + + fn command_for_update() -> clap::Command { + P::command_for_update() + } +} + +impl

FromArgMatches for App

+where + P: Parser + CommandFactory, +{ + fn from_arg_matches(matches: &clap::ArgMatches) -> Result { + Ok(Self { + parser: P::from_arg_matches(&matches)?, + }) + } + + fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> { + P::update_from_arg_matches(&mut self.parser, matches) + } +} + +impl

Parser for App

+where + P: Parser + CommandFactory, +{ + fn parse() -> Self { + let mut matches = ::command().get_matches(); + let res = ::from_arg_matches_mut(&mut matches) + .map_err(format_error::); + match res { + Ok(s) => s, + Err(e) => { + // Since this is more of a development-time error, we aren't doing as fancy of a quit + // as `get_matches` + e.exit() + } + } + } + + fn try_parse() -> Result { + let mut matches =

::command().try_get_matches()?; + Ok(Self { + parser:

::from_arg_matches_mut(&mut matches) + .map_err(format_error::

)?, + }) + } + + fn parse_from(itr: I) -> Self + where + I: IntoIterator, + T: Into + Clone, + { + let mut matches =

::command().get_matches_from(itr); + let res =

::from_arg_matches_mut(&mut matches) + .map_err(format_error::

); + match res { + Ok(s) => Self { parser: s }, + Err(e) => { + // Since this is more of a development-time error, we aren't doing as fancy of a quit + // as `get_matches_from` + e.exit() + } + } + } + + fn try_parse_from(itr: I) -> Result + where + I: IntoIterator, + T: Into + Clone, + { + let mut matches =

::command().try_get_matches_from(itr)?; + Ok(Self { + parser:

::from_arg_matches_mut(&mut matches) + .map_err(format_error::

)?, + }) + } + + fn update_from(&mut self, itr: I) + where + I: IntoIterator, + T: Into + Clone, + { + let mut matches =

::command_for_update().get_matches_from(itr); + let res =

::update_from_arg_matches_mut( + &mut self.parser, + &mut matches, + ) + .map_err(format_error::

); + if let Err(e) = res { + // Since this is more of a development-time error, we aren't doing as fancy of a quit + // as `get_matches_from` + e.exit() + } + } +} + +impl

App

+where + P: Parser + CommandFactory, +{ + pub fn without_errors() -> Result { + Ok(Self { + parser: P::from_arg_matches(&P::command().ignore_errors(true).try_get_matches()?)?, + }) + } + pub fn start(map_err: O) -> Result + where + O: FnOnce(Result, Self) -> Result, + { + map_err(Self::try_parse(), Self::without_errors()?) + } +} + +#[derive(Debug)] +pub struct Command

+where + P: Parser, +{ + args: P, +} + +impl

Command

+where + P: Parser, +{ + pub fn get_args(self) -> P { + self.args + } + + pub fn catch(self, error: clap::Error) -> ! { + error.exit() + } +} + +impl

FromArgMatches for Command

+where + P: Parser, +{ + fn from_arg_matches(matches: &clap::ArgMatches) -> Result { + Ok(Self { + args: P::from_arg_matches(&matches)?, + }) + } + + fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> { + P::update_from_arg_matches(&mut self.args, matches) + } +} + +impl

Args for Command

+where + P: Args + Parser, +{ + fn augment_args(cmd: clap::Command) -> clap::Command { + P::augment_args(cmd) + } + + fn augment_args_for_update(cmd: clap::Command) -> clap::Command { + P::augment_args_for_update(cmd) + } +} + +pub fn mut_subs(f: F) -> impl Fn(clap::Command) -> clap::Command +where + F: Fn(clap::Command) -> clap::Command, +{ + move |cmd| { + cmd.clone() + .get_subcommands_mut() + .fold(cmd, |cmd, sub| cmd.mut_subcommand(sub.get_name(), &f)) + } +} + +pub fn mut_args(f: F) -> impl Fn(clap::Command) -> clap::Command +where + F: Fn(Arg) -> Arg, +{ + move |cmd| { + cmd.clone() + .get_arguments() + .fold(cmd, |cmd, arg| cmd.mut_arg(arg.get_id(), &f)) + } +} diff --git a/src/args/component.rs b/src/args/component.rs new file mode 100644 index 0000000000..acb72e2ba6 --- /dev/null +++ b/src/args/component.rs @@ -0,0 +1,32 @@ +use std::path::PathBuf; + +use super::temp::TempDir; +use anyhow::{bail, Result}; +use clap::Args; + +/// Options to pass to components +#[derive(Args, Debug, Clone, Default)] +#[command(next_help_heading = "Component Options")] +pub struct ComponentOptions { + /// Temporary directory for the static assets of the components. + #[arg(long, default_value_t = TempDir::default())] + pub temp: TempDir, + /// Pass an environment variable (key=value) to all components of the application. + #[arg(short, long, value_parser = parse_env_var)] + pub env: Vec<(String, String)>, +} + +impl ComponentOptions { + pub fn working_dir(&self) -> Result { + Ok(self.temp.as_path().canonicalize()?) + } +} + +// Parse the environment variables passed in `key=value` pairs. +fn parse_env_var(env: &str) -> Result<(String, String)> { + if let Some((var, value)) = env.split_once('=') { + Ok((var.to_owned(), value.to_owned())) + } else { + bail!("Environment variable must be of the form `key=value`") + } +} diff --git a/src/args/manifest_file.rs b/src/args/manifest_file.rs new file mode 100644 index 0000000000..92a3a41999 --- /dev/null +++ b/src/args/manifest_file.rs @@ -0,0 +1,64 @@ +use std::{ + fmt, + fmt::{Debug, Display, Formatter}, + path::{Path, PathBuf}, + str::FromStr, convert::Infallible, +}; + +pub use anyhow::{Result, Error}; +pub use crate::opts::DEFAULT_MANIFEST_FILE; + +#[derive(Clone, Debug)] +pub struct ManifestFile { + relative_path: PathBuf +} + +impl ManifestFile { + pub fn new(relative_path: PathBuf) -> Self { + Self { relative_path } + } + pub fn canonicalize(&self) -> Result { + Ok(self.relative_path.canonicalize()?) + } + + pub async fn build(&self) -> Result<()> { + spin_build::build(&self.relative_path).await + } +} + +impl AsRef for ManifestFile { + fn as_ref(&self) -> &Path { + &self.relative_path + } +} + +impl From<&Path> for ManifestFile { + fn from(value: &Path) -> Self { + value.to_path_buf().into() + } +} + +impl From for ManifestFile { + fn from(value: PathBuf) -> Self { + Self::new(value) + } +} + +impl Default for ManifestFile { + fn default() -> Self { + PathBuf::from(DEFAULT_MANIFEST_FILE).into() + } +} + +impl FromStr for ManifestFile { + type Err = Infallible; + fn from_str(s: &str) -> Result { + PathBuf::from_str(s).map(Self::from) + } +} + +impl Display for ManifestFile { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { + self.relative_path.fmt(f) + } +} diff --git a/src/args/temp.rs b/src/args/temp.rs new file mode 100644 index 0000000000..e251d6195e --- /dev/null +++ b/src/args/temp.rs @@ -0,0 +1,41 @@ +use std::{ + fmt::{Debug, Display, Error, Formatter}, + path::{PathBuf, Path}, + str::FromStr, +}; + +use anyhow::Result; +use tempfile::tempdir; + +#[derive(Clone, Debug)] +pub struct TempDir { + path: PathBuf, +} + +impl TempDir { + pub fn new(path: PathBuf) -> TempDir { + Self { path } + } + pub fn as_path(&self) -> &Path { + self.path.as_path() + } +} + +impl Default for TempDir { + fn default() -> Self { + Self::new(tempdir().expect("Temp").path().to_path_buf()) + } +} + +impl Display for TempDir { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { + Ok(self.path.fmt(f)?) + } +} + +impl FromStr for TempDir { + type Err = ::Err; + fn from_str(s: &str) -> Result { + Ok(Self::new(PathBuf::from_str(s)?)) + } +} diff --git a/src/bin/spin.rs b/src/bin/spin.rs index 62664373e4..d1d7f5793a 100644 --- a/src/bin/spin.rs +++ b/src/bin/spin.rs @@ -1,32 +1,40 @@ -use anyhow::Error; -use clap::{CommandFactory, Parser, Subcommand}; +use anyhow::Result; +use async_trait::async_trait; +use clap::{Parser, Subcommand, CommandFactory}; use is_terminal::IsTerminal; use lazy_static::lazy_static; -use spin_cli::commands::{ +use spin_cli::{commands::{ bindle::BindleCommands, build::BuildCommand, deploy::DeployCommand, - external::execute_external_subcommand, login::LoginCommand, new::{AddCommand, NewCommand}, oci::OciCommands, plugins::PluginCommands, templates::TemplateCommands, - up::UpCommand, -}; + up::UpCommand, external::ExternalCommands, +}, dispatch::Action}; +use spin_cli::dispatch::{Dispatch}; +use anyhow::anyhow; +use spin_cli::*; use spin_http::HttpTrigger; use spin_redis_engine::RedisTrigger; use spin_trigger::cli::help::HelpArgsOnlyTrigger; use spin_trigger::cli::TriggerExecutorCommand; +#[cfg(feature = "generate-completions")] +use spin_cli::commands::generate_completions::GenerateCompletionsCommands; + #[tokio::main] -async fn main() -> Result<(), Error> { +async fn main() -> Result<()> { tracing_subscriber::fmt() .with_writer(std::io::stderr) .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) .with_ansi(std::io::stderr().is_terminal()) .init(); - SpinApp::parse().run().await + SpinApp::parse().dispatch(&Action::Help).await?; + + Ok(()) } lazy_static! { @@ -40,28 +48,29 @@ fn version() -> &'static str { /// The Spin CLI #[derive(Parser)] -#[clap( - name = "spin", - version = version(), -)] +#[command(id = "spin", version = version())] enum SpinApp { - #[clap(subcommand, alias = "template")] + #[command(subcommand, alias = "template")] Templates(TemplateCommands), New(NewCommand), Add(AddCommand), Up(UpCommand), - #[clap(subcommand)] + #[command(subcommand)] Bindle(BindleCommands), - #[clap(subcommand)] + #[command(subcommand)] Oci(OciCommands), Deploy(DeployCommand), Build(BuildCommand), Login(LoginCommand), - #[clap(subcommand, alias = "plugin")] + #[command(subcommand, alias = "plugin")] Plugins(PluginCommands), - #[clap(subcommand, hide = true)] + #[cfg(feature = "generate-completions")] + /// Generate shell completions + #[command(subcommand, hide = true)] + GenerateCompletions(GenerateCompletionsCommands), + #[command(subcommand, hide = true)] Trigger(TriggerCommands), - #[clap(external_subcommand)] + #[command(external_subcommand)] External(Vec), } @@ -73,24 +82,30 @@ enum TriggerCommands { HelpArgsOnly(TriggerExecutorCommand), } -impl SpinApp { +impl_dispatch!(TriggerCommands::{Http, Redis, HelpArgsOnly}); + +#[async_trait(?Send)] +impl Dispatch for SpinApp { /// The main entry point to Spin. - pub async fn run(self) -> Result<(), Error> { + async fn dispatch(&self, action: &Action) -> Result<()> { match self { - Self::Templates(cmd) => cmd.run().await, - Self::Up(cmd) => cmd.run().await, - Self::New(cmd) => cmd.run().await, - Self::Add(cmd) => cmd.run().await, - Self::Bindle(cmd) => cmd.run().await, - Self::Oci(cmd) => cmd.run().await, - Self::Deploy(cmd) => cmd.run().await, - Self::Build(cmd) => cmd.run().await, - Self::Trigger(TriggerCommands::Http(cmd)) => cmd.run().await, - Self::Trigger(TriggerCommands::Redis(cmd)) => cmd.run().await, - Self::Trigger(TriggerCommands::HelpArgsOnly(cmd)) => cmd.run().await, - Self::Login(cmd) => cmd.run().await, - Self::Plugins(cmd) => cmd.run().await, - Self::External(cmd) => execute_external_subcommand(cmd, SpinApp::command()).await, + Self::Templates(cmd) => cmd.dispatch(action).await, + Self::Up(cmd) => cmd.dispatch(action).await, + Self::New(cmd) => cmd.dispatch(action).await, + Self::Add(cmd) => cmd.dispatch(action).await, + Self::Bindle(cmd) => cmd.dispatch(action).await, + Self::Oci(cmd) => cmd.dispatch(action).await, + Self::Deploy(cmd) => cmd.dispatch(action).await, + Self::Build(cmd) => cmd.dispatch(action).await, + Self::Trigger(cmd) => match_action!(cmd[action].await), + Self::Login(cmd) => cmd.dispatch(action).await, + Self::Plugins(cmd) => cmd.dispatch(action).await, + #[cfg(feature = "generate-completions")] + Self::GenerateCompletions(cmd) => cmd.dispatch(action).await, + Self::External(cmd) => { + ExternalCommands::new(cmd.to_vec(), SpinApp::command()) + .dispatch(action).await + } // execute_external_subcommand(cmd, SpinApp::command()), } } } diff --git a/src/commands.rs b/src/commands.rs index 97a598947b..83354c4592 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -9,6 +9,9 @@ pub mod deploy; /// Commands for external subcommands (i.e. plugins) pub mod external; // Command for logging into the server +#[cfg(feature = "generate-completions")] +/// Command for generating completions. +pub mod generate_completions; pub mod login; /// Command for creating a new application. pub mod new; diff --git a/src/commands/bindle.rs b/src/commands/bindle.rs index 4b65d6dc39..2102ccfbda 100644 --- a/src/commands/bindle.rs +++ b/src/commands/bindle.rs @@ -1,11 +1,14 @@ use std::path::PathBuf; use anyhow::{Context, Result}; +use async_trait::async_trait; use clap::{Parser, Subcommand}; use semver::BuildMetadata; use spin_loader::bindle::BindleConnectionInfo; -use crate::{opts::*, parse_buildinfo, sloth::warn_if_slow_response}; +use crate::{opts::*, parse_buildinfo, sloth::warn_if_slow_response, args::manifest_file::ManifestFile, dispatch::Dispatch}; + +use crate::dispatch::Runner; /// Commands for publishing applications as bindles. #[derive(Subcommand, Debug)] @@ -17,8 +20,9 @@ pub enum BindleCommands { Push(Push), } -impl BindleCommands { - pub async fn run(self) -> Result<()> { +#[async_trait(?Send)] +impl Dispatch for BindleCommands { + async fn run(&self) -> Result<()> { match self { Self::Prepare(cmd) => cmd.run().await, Self::Push(cmd) => cmd.run().await, @@ -30,26 +34,26 @@ impl BindleCommands { #[derive(Parser, Debug)] pub struct Prepare { /// Path to spin.toml - #[clap( - name = APP_CONFIG_FILE_OPT, + #[arg( short = 'f', long = "file", + id = APP_CONFIG_FILE_OPT )] pub app: Option, /// Build metadata to append to the bindle version - #[clap( - name = BUILDINFO_OPT, + #[arg( long = "buildinfo", - parse(try_from_str = parse_buildinfo), + id = BUILDINFO_OPT, + value_parser = parse_buildinfo, )] pub buildinfo: Option, /// Path to create standalone bindle. - #[clap( - name = STAGING_DIR_OPT, - long = "staging-dir", + #[arg( short = 'd', + long = "staging-dir", + id = STAGING_DIR_OPT )] pub staging_dir: PathBuf, } @@ -58,75 +62,72 @@ pub struct Prepare { #[derive(Parser, Debug)] pub struct Push { /// Path to spin.toml - #[clap( - name = APP_CONFIG_FILE_OPT, - short = 'f', - long = "file", - )] - pub app: Option, + #[arg(long, short, default_value_t = Default::default())] + pub file: ManifestFile, /// Build metadata to append to the bindle version - #[clap( - name = BUILDINFO_OPT, + #[arg( long = "buildinfo", - parse(try_from_str = parse_buildinfo), + id = BUILDINFO_OPT, + value_parser = parse_buildinfo, )] pub buildinfo: Option, /// Path to assemble the bindle before pushing (defaults to /// temporary directory). - #[clap( - name = STAGING_DIR_OPT, - long = "staging-dir", + #[arg( short = 'd', + long = "staging-dir", + id = STAGING_DIR_OPT )] pub staging_dir: Option, /// URL of bindle server - #[clap( - name = BINDLE_SERVER_URL_OPT, + #[arg( long = "bindle-server", + id = BINDLE_SERVER_URL_OPT, env = BINDLE_URL_ENV, )] pub bindle_server_url: String, /// Basic http auth username for the bindle server - #[clap( - name = BINDLE_USERNAME, + #[arg( long = "bindle-username", + id = BINDLE_USERNAME, env = BINDLE_USERNAME, requires = BINDLE_PASSWORD )] pub bindle_username: Option, /// Basic http auth password for the bindle server - #[clap( - name = BINDLE_PASSWORD, + #[arg( long = "bindle-password", + id = BINDLE_PASSWORD, env = BINDLE_PASSWORD, requires = BINDLE_USERNAME )] pub bindle_password: Option, /// Ignore server certificate errors - #[clap( - name = INSECURE_OPT, + #[arg( short = 'k', long = "insecure", - takes_value = false, + id = INSECURE_OPT, )] pub insecure: bool, } -impl Prepare { - pub async fn run(self) -> Result<()> { +#[async_trait(?Send)] +impl Dispatch for Prepare { + async fn run(&self) -> Result<()> { let app_file = self .app .as_deref() .unwrap_or_else(|| DEFAULT_MANIFEST_FILE.as_ref()); let dest_dir = &self.staging_dir; - let bindle_id = spin_publish::bindle::prepare_bindle(app_file, self.buildinfo, dest_dir) + let buildinfo = &self.buildinfo; + let bindle_id = spin_publish::bindle::prepare_bindle(app_file, buildinfo.clone(), dest_dir) .await .map_err(crate::wrap_prepare_bindle_error)?; @@ -141,17 +142,15 @@ impl Prepare { } } -impl Push { - pub async fn run(self) -> Result<()> { - let app_file = self - .app - .as_deref() - .unwrap_or_else(|| DEFAULT_MANIFEST_FILE.as_ref()); +#[async_trait(?Send)] +impl Dispatch for Push { + async fn run(&self) -> Result<()> { + let app_file = self.file.canonicalize()?; let bindle_connection_info = BindleConnectionInfo::new( &self.bindle_server_url, self.insecure, - self.bindle_username, - self.bindle_password, + self.bindle_username.clone(), + self.bindle_password.clone(), ); // TODO: only create this if not given a staging dir @@ -162,9 +161,10 @@ impl Push { Some(path) => path.as_path(), }; - let bindle_id = spin_publish::bindle::prepare_bindle(app_file, self.buildinfo, dest_dir) - .await - .map_err(crate::wrap_prepare_bindle_error)?; + let bindle_id = + spin_publish::bindle::prepare_bindle(app_file, self.buildinfo.clone(), dest_dir) + .await + .map_err(crate::wrap_prepare_bindle_error)?; let _sloth_warning = warn_if_slow_response(format!( "Uploading application to {}", diff --git a/src/commands/build.rs b/src/commands/build.rs index 7e3683dd6a..cc65bc60aa 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -1,53 +1,26 @@ -use std::{ffi::OsString, path::PathBuf}; - +use super::up::{UpCommand, Flag}; +use crate::{args::manifest_file::ManifestFile, dispatch::Dispatch}; +use crate::dispatch::Runner; use anyhow::Result; +use async_trait::async_trait; use clap::Parser; -use crate::opts::{APP_CONFIG_FILE_OPT, BUILD_UP_OPT, DEFAULT_MANIFEST_FILE}; - -use super::up::UpCommand; - -/// Run the build command for each component. +/// Build the Spin application #[derive(Parser, Debug)] -#[clap(about = "Build the Spin application", allow_hyphen_values = true)] +#[command(subcommand_required=false)] pub struct BuildCommand { /// Path to spin.toml. - #[clap( - name = APP_CONFIG_FILE_OPT, - short = 'f', - long = "file", - )] - pub app: Option, - - /// Run the application after building. - #[clap(name = BUILD_UP_OPT, short = 'u', long = "up")] - pub up: bool, - - #[clap(requires = BUILD_UP_OPT)] - pub up_args: Vec, + #[arg(long, short, default_value_t = ManifestFile::default(), required = false)] + pub file: ManifestFile, + #[command(subcommand)] + pub up: Flag, } -impl BuildCommand { - pub async fn run(self) -> Result<()> { - let manifest_file = self - .app - .as_deref() - .unwrap_or_else(|| DEFAULT_MANIFEST_FILE.as_ref()); - - spin_build::build(manifest_file).await?; - - if self.up { - let mut cmd = UpCommand::parse_from( - std::iter::once(OsString::from(format!( - "{} up", - std::env::args().next().unwrap() - ))) - .chain(self.up_args), - ); - cmd.app = Some(manifest_file.into()); - cmd.run().await - } else { - Ok(()) - } +#[async_trait(?Send)] +impl Dispatch for BuildCommand { + async fn run(&self) -> Result<()> { + let Self { file, up } = self; + file.build().await?; + up.run().await } } diff --git a/src/commands/deploy.rs b/src/commands/deploy.rs index a885f0952d..fdec91644c 100644 --- a/src/commands/deploy.rs +++ b/src/commands/deploy.rs @@ -1,5 +1,6 @@ use anyhow::ensure; use anyhow::{anyhow, bail, Context, Result}; +use async_trait::async_trait; use bindle::Id; use chrono::{DateTime, Utc}; use clap::Parser; @@ -29,10 +30,11 @@ use std::path::PathBuf; use url::Url; use uuid::Uuid; -use crate::{opts::*, parse_buildinfo, sloth::warn_if_slow_response}; +use crate::{opts::*, parse_buildinfo, sloth::warn_if_slow_response, dispatch::Dispatch}; -use super::login::LoginCommand; use super::login::LoginConnection; +use super::login::LoginCommand; +use crate::dispatch::Runner; const SPIN_DEPLOY_CHANNEL_NAME: &str = "spin-deploy"; @@ -40,71 +42,72 @@ const BINDLE_REGISTRY_URL_PATH: &str = "api/registry"; /// Package and upload Spin artifacts, notifying Hippo #[derive(Parser, Debug)] -#[clap(about = "Deploy a Spin application")] +#[command(about = "Deploy a Spin application")] pub struct DeployCommand { /// Path to spin.toml - #[clap( - name = APP_CONFIG_FILE_OPT, + #[arg( short = 'f', long = "file", - default_value = "spin.toml" + default_value = "spin.toml", + id = APP_CONFIG_FILE_OPT )] pub app: PathBuf, /// Path to assemble the bindle before pushing (defaults to /// a temporary directory) - #[clap( - name = STAGING_DIR_OPT, - long = "staging-dir", + #[arg( short = 'd', + long, + id = STAGING_DIR_OPT, )] pub staging_dir: Option, /// Disable attaching buildinfo - #[clap( - long = "no-buildinfo", + #[arg( + long, conflicts_with = BUILDINFO_OPT, env = "SPIN_DEPLOY_NO_BUILDINFO" )] pub no_buildinfo: bool, /// Build metadata to append to the bindle version - #[clap( - name = BUILDINFO_OPT, - long = "buildinfo", - parse(try_from_str = parse_buildinfo), + #[arg( + long, + id = BUILDINFO_OPT, + value_parser = parse_buildinfo, )] pub buildinfo: Option, /// Deploy existing bindle if it already exists on bindle server - #[clap(short = 'e', long = "deploy-existing-bindle")] + #[arg(short = 'e', long = "deploy-existing-bindle")] pub redeploy: bool, /// How long in seconds to wait for a deployed HTTP application to become /// ready. The default is 60 seconds. Set it to 0 to skip waiting /// for readiness. - #[clap(long = "readiness-timeout", default_value = "60")] + #[arg(long = "readiness-timeout", default_value = "60")] pub readiness_timeout_secs: u16, /// Deploy to the Fermyon instance saved under the specified name. /// If omitted, Spin deploys to the default unnamed instance. - #[clap( - name = "environment-name", + #[arg( + id = "environment-name", long = "environment-name", env = DEPLOYMENT_ENV_NAME_ENV )] pub deployment_env_id: Option, } -impl DeployCommand { - pub async fn run(self) -> Result<()> { +#[async_trait(?Send)] +impl Dispatch for DeployCommand { + async fn run(&self) -> Result<()> { let path = self.config_file_path()?; // log in if config.json does not exist or cannot be read let data = match fs::read_to_string(path.clone()).await { Ok(d) => d, Err(e) if e.kind() == io::ErrorKind::NotFound => { - match self.deployment_env_id { + match &self.deployment_env_id { Some(name) => { // TODO: allow auto redirect to login preserving the name eprintln!("You have no instance saved as '{}'", name); @@ -130,7 +133,7 @@ impl DeployCommand { let now: DateTime = Utc::now(); if now > expiration_date { // session has expired - log back in - match self.deployment_env_id { + match &self.deployment_env_id { Some(name) => { // TODO: allow auto redirect to login preserving the name eprintln!("Your login to this environment has expired"); @@ -168,7 +171,8 @@ impl DeployCommand { .map_err(|e| anyhow!("{:?}\n\nLearn more at {}", e, DEVELOPER_CLOUD_FAQ)) } } - +} +impl DeployCommand { // TODO: unify with login fn config_file_path(&self) -> Result { let root = dirs::config_dir() @@ -186,7 +190,7 @@ impl DeployCommand { Ok(path) } - async fn deploy_hippo(self, login_connection: LoginConnection) -> Result<()> { + async fn deploy_hippo(&self, login_connection: LoginConnection) -> Result<()> { let cfg_any = spin_loader::local::raw_manifest_from_file(&self.app).await?; let RawAppManifestAnyVersion::V1(cfg) = cfg_any; @@ -305,7 +309,7 @@ impl DeployCommand { Ok(()) } - async fn deploy_cloud(self, login_connection: LoginConnection) -> Result<()> { + async fn deploy_cloud(&self, login_connection: LoginConnection) -> Result<()> { let connection_config = ConnectionConfig { url: login_connection.url.to_string(), insecure: login_connection.danger_accept_invalid_certs, @@ -632,7 +636,6 @@ impl DeployCommand { } } } - fn random_buildinfo() -> BuildMetadata { let random_bytes: [u8; 4] = rand::thread_rng().gen(); let random_hex: String = random_bytes.iter().map(|b| format!("{:x}", b)).collect(); diff --git a/src/commands/external.rs b/src/commands/external.rs index e474bdf61b..b81d3748c1 100644 --- a/src/commands/external.rs +++ b/src/commands/external.rs @@ -1,11 +1,30 @@ -use crate::opts::PLUGIN_OVERRIDE_COMPATIBILITY_CHECK_FLAG; +use crate::{opts::PLUGIN_OVERRIDE_COMPATIBILITY_CHECK_FLAG, dispatch::Dispatch}; use anyhow::{anyhow, Result}; -use clap::App; +use async_trait::async_trait; +use clap; use spin_plugins::{error::Error, manifest::check_supported_version, PluginStore}; use std::{collections::HashMap, env, process}; use tokio::process::Command; use tracing::log; +pub struct ExternalCommands { + cmd: Vec, + app: clap::Command, +} + +impl ExternalCommands { + pub fn new(cmd: Vec, app: clap::Command) -> Self { + Self { cmd, app } + } +} + +#[async_trait(?Send)] +impl Dispatch for ExternalCommands { + async fn run(&self) -> Result<()> { + execute_external_subcommand(self.cmd.to_owned(), &self.app).await + } +} + fn override_flag() -> String { format!("--{}", PLUGIN_OVERRIDE_COMPATIBILITY_CHECK_FLAG) } @@ -33,7 +52,7 @@ fn parse_subcommand(mut cmd: Vec) -> anyhow::Result<(String, Vec /// Executes a Spin plugin as a subprocess, expecting the first argument to /// indicate the plugin to execute. Passes all subsequent arguments on to the /// subprocess. -pub async fn execute_external_subcommand(cmd: Vec, app: App<'_>) -> anyhow::Result<()> { +pub async fn execute_external_subcommand(cmd: Vec, app: &clap::Command) -> Result<()> { let (plugin_name, args, override_compatibility_check) = parse_subcommand(cmd)?; let plugin_store = PluginStore::try_default()?; match plugin_store.read_plugin_manifest(&plugin_name) { diff --git a/src/commands/generate_completions.rs b/src/commands/generate_completions.rs new file mode 100644 index 0000000000..ee877689a1 --- /dev/null +++ b/src/commands/generate_completions.rs @@ -0,0 +1,66 @@ +use anyhow::Error; +use async_trait::async_trait; +use clap::{Args, CommandFactory, Parser, Subcommand}; +use clap_complete::{generate, Generator, Shell}; +use clap_complete_fig::Fig; + +use crate::dispatch::Dispatch; + +#[derive(Subcommand)] +pub enum GenerateCompletionsCommands { + Shell(GenerateCompletionsShellCommand), + Fig(GenerateCompletionsFigCommand), +} + +#[async_trait(?Send)] +impl Dispatch for GenerateCompletionsCommands { + async fn run(&self) -> Result<(), Error> { + match self { + Self::Shell(cmd) => cmd.run().await, + Self::Fig(cmd) => cmd.run().await, + } + } +} + +#[derive(Args, Clone, Debug)] +pub struct GenerateCompletionsArgs { + /// Shell to generate completions for + shell: Shell, + /// Generate completions for fig + fig: bool, +} + +/// Generate Fig completions +#[derive(Parser, Debug)] +pub struct GenerateCompletionsFigCommand; + +#[async_trait(?Send)] +impl Dispatch for GenerateCompletionsFigCommand { + async fn run(&self) -> Result<(), Error> { + Self::print_completions(Fig); + Ok(()) + } +} + +/// Generate Shell completions +#[derive(Parser, Debug)] +pub struct GenerateCompletionsShellCommand { + #[arg(value_parser = clap::value_parser!(clap_complete::Shell))] + pub shell: clap_complete::Shell, +} + +#[async_trait(?Send)] +impl Dispatch for GenerateCompletionsShellCommand { + async fn run(&self) -> Result<(), Error> { + Self::print_completions(self.shell); + Ok(()) + } +} + +trait Completions: CommandFactory { + fn print_completions(gen: G) { + generate(gen, &mut Self::command(), "spin", &mut std::io::stdout()) + } +} + +impl Completions for T {} diff --git a/src/commands/login.rs b/src/commands/login.rs index 847c3f6360..8ca2354361 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -16,12 +16,15 @@ use tokio::fs; use tracing::log; use url::Url; use uuid::Uuid; +use::async_trait::async_trait; -use crate::opts::{ +use crate::{opts::{ BINDLE_PASSWORD, BINDLE_SERVER_URL_OPT, BINDLE_URL_ENV, BINDLE_USERNAME, DEPLOYMENT_ENV_NAME_ENV, HIPPO_PASSWORD, HIPPO_SERVER_URL_OPT, HIPPO_URL_ENV, HIPPO_USERNAME, INSECURE_OPT, -}; +}, dispatch::Dispatch}; + +use crate::dispatch::Runner; // this is the client ID registered in the Cloud's backend const SPIN_CLIENT_ID: &str = "583e63e9-461f-4fbe-a246-23e0fb1cad10"; @@ -30,47 +33,46 @@ const DEFAULT_CLOUD_URL: &str = "https://cloud.fermyon.com/"; /// Log into the server #[derive(Parser, Debug)] -#[clap(about = "Log into the server")] +#[command(about = "Log into the server")] pub struct LoginCommand { /// URL of bindle server - #[clap( - name = BINDLE_SERVER_URL_OPT, + #[arg( long = "bindle-server", + id = BINDLE_SERVER_URL_OPT, env = BINDLE_URL_ENV, )] pub bindle_server_url: Option, /// Basic http auth username for the bindle server - #[clap( - name = BINDLE_USERNAME, + #[arg( long = "bindle-username", + id = BINDLE_USERNAME, env = BINDLE_USERNAME, requires = BINDLE_PASSWORD )] pub bindle_username: Option, /// Basic http auth password for the bindle server - #[clap( - name = BINDLE_PASSWORD, + #[arg( long = "bindle-password", + id = BINDLE_PASSWORD, env = BINDLE_PASSWORD, requires = BINDLE_USERNAME )] pub bindle_password: Option, /// Ignore server certificate errors from bindle and hippo - #[clap( - name = INSECURE_OPT, + #[arg( short = 'k', long = "insecure", - takes_value = false, + id = INSECURE_OPT )] pub insecure: bool, /// URL of hippo server - #[clap( - name = HIPPO_SERVER_URL_OPT, + #[arg( long = "url", + id = HIPPO_SERVER_URL_OPT, env = HIPPO_URL_ENV, default_value = DEFAULT_CLOUD_URL, value_parser = parse_url, @@ -78,82 +80,69 @@ pub struct LoginCommand { pub hippo_server_url: url::Url, /// Hippo username - #[clap( - name = HIPPO_USERNAME, + #[arg( long = "username", + id = HIPPO_USERNAME, env = HIPPO_USERNAME, requires = HIPPO_PASSWORD, )] pub hippo_username: Option, /// Hippo password - #[clap( - name = HIPPO_PASSWORD, + #[arg( long = "password", + id = HIPPO_PASSWORD, env = HIPPO_PASSWORD, requires = HIPPO_USERNAME, )] pub hippo_password: Option, /// Display login status - #[clap( - name = "status", - long = "status", - takes_value = false, + #[arg( + long, conflicts_with = "list", - conflicts_with = "get-device-code", - conflicts_with = "check-device-code" + conflicts_with = "get_device_code", + conflicts_with = "check_device_code" )] pub status: bool, // fetch a device code - #[clap( - name = "get-device-code", - long = "get-device-code", - takes_value = false, + #[arg( + long, hide = true, conflicts_with = "status", - conflicts_with = "check-device-code" + conflicts_with = "check_device_code" )] pub get_device_code: bool, // check a device code - #[clap( - name = "check-device-code", - long = "check-device-code", + #[arg( + long, hide = true, conflicts_with = "status", - conflicts_with = "get-device-code" + conflicts_with = "get_device_code" )] pub check_device_code: Option, // authentication method used for logging in (username|github) - #[clap( - name = "auth-method", - long = "auth-method", - env = "AUTH_METHOD", - arg_enum - )] + #[arg(long = "auth-method", env = "AUTH_METHOD", value_enum)] pub method: Option, /// Save the login details under the specified name instead of making them /// the default. Use named environments with `spin deploy --environment-name `. - #[clap( - name = "environment-name", + #[arg( long = "environment-name", env = DEPLOYMENT_ENV_NAME_ENV )] pub deployment_env_id: Option, /// List saved logins. - #[clap( - name = "list", - long = "list", - takes_value = false, - conflicts_with = "environment-name", + #[arg( + long, + conflicts_with = "deployment_env_id", conflicts_with = "status", - conflicts_with = "get-device-code", - conflicts_with = "check-device-code" + conflicts_with = "get_device_code", + conflicts_with = "check_device_code" )] pub list: bool, } @@ -172,24 +161,23 @@ fn parse_url(url: &str) -> Result { Ok(url) } -impl LoginCommand { - pub async fn run(&self) -> Result<()> { - match ( - self.list, - self.status, - self.get_device_code, - &self.check_device_code, - ) { +#[async_trait(?Send)] +impl Dispatch for LoginCommand { + async fn run(&self) -> Result<()> { + let Self { list, status, get_device_code, ref check_device_code, .. } = self; + + match (list, status, get_device_code, check_device_code) { (true, false, false, None) => self.run_list().await, (false, true, false, None) => self.run_status().await, (false, false, true, None) => self.run_get_device_code().await, - (false, false, false, Some(device_code)) => { - self.run_check_device_code(device_code).await - } + (false, false, false, Some(device_code)) => self.run_check_device_code(device_code).await, (false, false, false, None) => self.run_interactive_login().await, _ => Err(anyhow::anyhow!("Invalid combination of options")), // Should never happen } } +} + +impl LoginCommand { async fn run_list(&self) -> Result<()> { let root = config_root_dir()?; @@ -540,11 +528,11 @@ fn ensure(root: &PathBuf) -> Result<()> { } /// The method by which to authenticate the login. -#[derive(clap::ArgEnum, Clone, Debug, Eq, PartialEq)] +#[derive(clap::ValueEnum, Clone, Debug, Eq, PartialEq)] pub enum AuthMethod { - #[clap(name = "github")] + #[value(name = "github")] Github, - #[clap(name = "username")] + #[value(name = "username")] UsernameAndPassword, } diff --git a/src/commands/new.rs b/src/commands/new.rs index 5b581a4711..cf5bcce76a 100644 --- a/src/commands/new.rs +++ b/src/commands/new.rs @@ -12,7 +12,10 @@ use tokio; use spin_loader::local::absolutize; use spin_templates::{RunOptions, Template, TemplateManager, TemplateVariantInfo}; -use crate::opts::{APP_CONFIG_FILE_OPT, DEFAULT_MANIFEST_FILE}; +use crate::{opts::{APP_CONFIG_FILE_OPT, DEFAULT_MANIFEST_FILE}, dispatch::Dispatch}; + +use crate::dispatch::Runner; +use async_trait::async_trait; /// Scaffold a new application based on a template. #[derive(Parser, Debug)] @@ -21,62 +24,65 @@ pub struct TemplateNewCommandCore { pub template_id: Option, /// The name of the new application or component. - #[clap(value_parser = validate_name)] + #[arg(value_parser = validate_name)] pub name: Option, /// The directory in which to create the new application or component. /// The default is the name argument. - #[clap(short = 'o', long = "output")] + #[arg(short, long = "output")] pub output_path: Option, /// Parameter values to be passed to the template (in name=value format). - #[clap(short = 'v', long = "value", multiple_occurrences = true)] + #[arg(short, long = "value")] pub values: Vec, /// A TOML file which contains parameter values in name = "value" format. /// Parameters passed as CLI option overwrite parameters specified in the /// file. - #[clap(long = "values-file")] + #[arg(long)] pub values_file: Option, /// An optional argument that allows to skip prompts for the manifest file /// by accepting the defaults if available on the template - #[clap(long = "accept-defaults", takes_value = false)] + #[arg(long)] pub accept_defaults: bool, } /// Scaffold a new application based on a template. #[derive(Parser, Debug)] pub struct NewCommand { - #[clap(flatten)] + #[command(flatten)] options: TemplateNewCommandCore, } /// Scaffold a new component into an existing application. #[derive(Parser, Debug)] pub struct AddCommand { - #[clap(flatten)] + #[command(flatten)] options: TemplateNewCommandCore, /// Path to spin.toml. - #[clap( - name = APP_CONFIG_FILE_OPT, + #[arg( short = 'f', long = "file", + id = APP_CONFIG_FILE_OPT, )] pub app: Option, } -impl NewCommand { - pub async fn run(&self) -> Result<()> { +#[async_trait(?Send)] +impl Dispatch for NewCommand { + async fn run(&self) -> Result<()> { self.options.run(TemplateVariantInfo::NewApplication).await } } -impl AddCommand { - pub async fn run(&self) -> Result<()> { - let app_file = self - .app +#[async_trait(?Send)] +impl Dispatch for AddCommand { + async fn run(&self) -> Result<()> { + let Self { app, options } = self; + + let app_file = app .as_deref() .unwrap_or_else(|| DEFAULT_MANIFEST_FILE.as_ref()); let manifest_path = app_file @@ -94,9 +100,7 @@ impl AddCommand { manifest_path.display() ); } - self.options - .run(TemplateVariantInfo::AddComponent { manifest_path }) - .await + options.run(TemplateVariantInfo::AddComponent { manifest_path }).await } } @@ -157,7 +161,7 @@ impl TemplateNewCommandCore { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ParameterValue { pub name: String, pub value: String, diff --git a/src/commands/oci.rs b/src/commands/oci.rs index 91d5dad366..5794823f69 100644 --- a/src/commands/oci.rs +++ b/src/commands/oci.rs @@ -1,15 +1,17 @@ use anyhow::{bail, Context, Result}; +use async_trait::async_trait; use clap::{Parser, Subcommand}; use reqwest::Url; use spin_app::locked::LockedApp; use spin_trigger::cli::{SPIN_LOCKED_URL, SPIN_WORKING_DIR}; - use std::{ ffi::OsString, path::{Path, PathBuf}, }; -use crate::opts::*; +use crate::{opts::*, dispatch::Dispatch}; + +use crate::dispatch::Runner; /// Commands for working with OCI registries to distribute applications. /// The set of commands for OCI is EXPERIMENTAL, and may change in future versions of Spin. @@ -25,8 +27,9 @@ pub enum OciCommands { Run(Run), } -impl OciCommands { - pub async fn run(self) -> Result<()> { +#[async_trait(?Send)] +impl Dispatch for OciCommands { + async fn run(&self) -> Result<()> { match self { OciCommands::Push(cmd) => cmd.run().await, OciCommands::Pull(cmd) => cmd.run().await, @@ -49,8 +52,7 @@ pub struct Push { #[clap( name = INSECURE_OPT, short = 'k', - long = "insecure", - takes_value = false, + long = "insecure" )] pub insecure: bool, @@ -59,8 +61,9 @@ pub struct Push { pub reference: String, } -impl Push { - pub async fn run(self) -> Result<()> { +#[async_trait(?Send)] +impl Dispatch for Push { + async fn run(&self) -> Result<()> { let app_file = self .app .as_deref() @@ -78,11 +81,10 @@ impl Push { #[derive(Parser, Debug)] pub struct Pull { /// Ignore server certificate errors - #[clap( - name = INSECURE_OPT, + #[arg( + id = INSECURE_OPT, short = 'k', - long = "insecure", - takes_value = false, + long = "insecure" )] pub insecure: bool, @@ -91,9 +93,10 @@ pub struct Pull { pub reference: String, } -impl Pull { +#[async_trait(?Send)] +impl Dispatch for Pull { /// Pull a Spin application from an OCI registry - pub async fn run(self) -> Result<()> { + async fn run(&self) -> Result<()> { let mut client = spin_publish::oci::client::Client::new(self.insecure, None).await?; client.pull(&self.reference).await?; @@ -104,31 +107,31 @@ impl Pull { #[derive(Parser, Debug)] pub struct Run { /// Connect to the registry endpoint over HTTP, not HTTPS. - #[clap( + #[arg( name = INSECURE_OPT, short = 'k', - long = "insecure", - takes_value = false, + long = "insecure" )] pub insecure: bool, /// Pass an environment variable (key=value) to all components of the application. - #[clap(short = 'e', long = "env", parse(try_from_str = parse_env_var))] + #[arg(value_parser = parse_env_var)] pub env: Vec<(String, String)>, /// Reference of the Spin application - #[clap()] + #[arg()] pub reference: String, /// All other args, to be passed through to the trigger /// TODO: The arguments have to be passed like `-- --follow-all` for now. - #[clap(hide = true)] + #[arg(hide = true)] pub trigger_args: Vec, } -impl Run { +#[async_trait(?Send)] +impl Dispatch for Run { /// Run a Spin application from an OCI registry - pub async fn run(self) -> Result<()> { + async fn run(&self) -> Result<()> { let mut client = spin_publish::oci::client::Client::new(self.insecure, None).await?; client.pull(&self.reference).await?; @@ -191,7 +194,9 @@ impl Run { bail!(status); } } +} +impl Run { async fn write_locked_app(app: &LockedApp, working_dir: &Path) -> Result { let path = working_dir.join("spin.lock"); let contents = serde_json::to_vec(&app)?; diff --git a/src/commands/plugins.rs b/src/commands/plugins.rs index f75c6b6d07..fc2db62e27 100644 --- a/src/commands/plugins.rs +++ b/src/commands/plugins.rs @@ -1,5 +1,7 @@ +use crate::dispatch::Action; use anyhow::{anyhow, Context, Result}; -use clap::{Parser, Subcommand}; +use async_trait::async_trait; +use clap::{Args, Parser, Subcommand}; use semver::Version; use spin_plugins::{ error::Error, @@ -11,7 +13,9 @@ use std::path::{Path, PathBuf}; use tracing::log; use url::Url; -use crate::opts::*; +use crate::{opts::*, dispatch::Dispatch}; + +use crate::dispatch::Runner; /// Install/uninstall Spin plugins. #[derive(Subcommand, Debug)] @@ -35,82 +39,86 @@ pub enum PluginCommands { Update, } -impl PluginCommands { - pub async fn run(self) -> Result<()> { +#[async_trait(?Send)] +impl Dispatch for PluginCommands { + async fn dispatch(&self, action: &Action) -> Result<()> { match self { - PluginCommands::Install(cmd) => cmd.run().await, - PluginCommands::List(cmd) => cmd.run().await, - PluginCommands::Uninstall(cmd) => cmd.run().await, - PluginCommands::Upgrade(cmd) => cmd.run().await, + PluginCommands::Install(cmd) => cmd.dispatch(action).await, + PluginCommands::List(cmd) => cmd.dispatch(action).await, + PluginCommands::Uninstall(cmd) => cmd.dispatch(action).await, + PluginCommands::Upgrade(cmd) => cmd.dispatch(action).await, PluginCommands::Update => update().await, } } } -/// Install plugins from remote source -#[derive(Parser, Debug)] -pub struct Install { +#[derive(Args, Debug)] +pub struct ManifestSource { /// Name of Spin plugin. - #[clap( - name = PLUGIN_NAME_OPT, - conflicts_with = PLUGIN_REMOTE_PLUGIN_MANIFEST_OPT, - conflicts_with = PLUGIN_LOCAL_PLUGIN_MANIFEST_OPT, - required_unless_present_any = [PLUGIN_REMOTE_PLUGIN_MANIFEST_OPT, PLUGIN_LOCAL_PLUGIN_MANIFEST_OPT], - )] pub name: Option, + /// Specific version of a plugin to be install from the centralized plugins + /// repository. + #[arg( + short, + long, + requires = PLUGIN_NAME_OPT + )] + pub version: Option, + /// Path to local plugin manifest. - #[clap( - name = PLUGIN_LOCAL_PLUGIN_MANIFEST_OPT, - short = 'f', - long = "file", - conflicts_with = PLUGIN_REMOTE_PLUGIN_MANIFEST_OPT, - conflicts_with = PLUGIN_NAME_OPT, - )] + #[arg(short = 'f', long = "file")] pub local_manifest_src: Option, /// URL of remote plugin manifest to install. - #[clap( - name = PLUGIN_REMOTE_PLUGIN_MANIFEST_OPT, - short = 'u', - long = "url", - conflicts_with = PLUGIN_LOCAL_PLUGIN_MANIFEST_OPT, - conflicts_with = PLUGIN_NAME_OPT, - )] + #[arg(short = 'u', long = "url")] pub remote_manifest_src: Option, +} + +impl ManifestSource { + fn location(&self) -> ManifestLocation { + if let Some(path) = &self.local_manifest_src { + return ManifestLocation::Local(path.to_path_buf()); + } + + if let Some(url) = &self.remote_manifest_src { + return ManifestLocation::Remote(url.clone()); + } + + if let Some(name) = &self.name { + return ManifestLocation::PluginsRepository(PluginLookup::new( + &name, + self.version.clone(), + )); + } + + unreachable!() + } +} + +/// Install plugins from remote source +#[derive(Parser, Debug)] +pub struct Install { + /// URL of remote plugin manifest to install. + #[command(flatten)] + pub source: ManifestSource, /// Skips prompt to accept the installation of the plugin. - #[clap(short = 'y', long = "yes", takes_value = false)] + #[arg(short, long = "yes")] pub yes_to_all: bool, /// Overrides a failed compatibility check of the plugin with the current version of Spin. - #[clap(long = PLUGIN_OVERRIDE_COMPATIBILITY_CHECK_FLAG, takes_value = false)] + #[arg(long = PLUGIN_OVERRIDE_COMPATIBILITY_CHECK_FLAG)] pub override_compatibility_check: bool, - - /// Specific version of a plugin to be install from the centralized plugins - /// repository. - #[clap( - long = "version", - short = 'v', - conflicts_with = PLUGIN_REMOTE_PLUGIN_MANIFEST_OPT, - conflicts_with = PLUGIN_LOCAL_PLUGIN_MANIFEST_OPT, - requires(PLUGIN_NAME_OPT) - )] - pub version: Option, } -impl Install { - pub async fn run(self) -> Result<()> { - let manifest_location = match (self.local_manifest_src, self.remote_manifest_src, self.name) { - (Some(path), None, None) => ManifestLocation::Local(path), - (None, Some(url), None) => ManifestLocation::Remote(url), - (None, None, Some(name)) => ManifestLocation::PluginsRepository(PluginLookup::new(&name, self.version)), - _ => return Err(anyhow::anyhow!("For plugin lookup, must provide exactly one of: plugin name, url to manifest, local path to manifest")), - }; +#[async_trait(?Send)] +impl Dispatch for Install { + async fn run(&self) -> Result<()> { let manager = PluginManager::try_default()?; // Downgrades are only allowed via the `upgrade` subcommand let downgrade = false; - let manifest = manager.get_manifest(&manifest_location).await?; + let manifest = manager.get_manifest(&self.source.location()).await?; try_install( &manifest, &manager, @@ -130,8 +138,9 @@ pub struct Uninstall { pub name: String, } -impl Uninstall { - pub async fn run(self) -> Result<()> { +#[async_trait(?Send)] +impl Dispatch for Uninstall { + async fn run(&self) -> Result<()> { let manager = PluginManager::try_default()?; let uninstalled = manager.uninstall(&self.name)?; if uninstalled { @@ -149,56 +158,55 @@ impl Uninstall { #[derive(Parser, Debug)] pub struct Upgrade { /// Name of Spin plugin to upgrade. - #[clap( - name = PLUGIN_NAME_OPT, + #[arg( + id = PLUGIN_NAME_OPT, conflicts_with = PLUGIN_ALL_OPT, required_unless_present_any = [PLUGIN_ALL_OPT], )] pub name: Option, /// Upgrade all plugins. - #[clap( + #[arg( short = 'a', long = "all", - name = PLUGIN_ALL_OPT, + id = PLUGIN_ALL_OPT, conflicts_with = PLUGIN_NAME_OPT, conflicts_with = PLUGIN_REMOTE_PLUGIN_MANIFEST_OPT, - conflicts_with = PLUGIN_LOCAL_PLUGIN_MANIFEST_OPT, - takes_value = false, + conflicts_with = PLUGIN_LOCAL_PLUGIN_MANIFEST_OPT )] pub all: bool, /// Path to local plugin manifest. - #[clap( - name = PLUGIN_LOCAL_PLUGIN_MANIFEST_OPT, + #[arg( short = 'f', long = "file", + id = PLUGIN_LOCAL_PLUGIN_MANIFEST_OPT, conflicts_with = PLUGIN_REMOTE_PLUGIN_MANIFEST_OPT, )] pub local_manifest_src: Option, /// Path to remote plugin manifest. - #[clap( - name = PLUGIN_REMOTE_PLUGIN_MANIFEST_OPT, + #[arg( short = 'u', long = "url", + id = PLUGIN_REMOTE_PLUGIN_MANIFEST_OPT, conflicts_with = PLUGIN_LOCAL_PLUGIN_MANIFEST_OPT, )] pub remote_manifest_src: Option, /// Skips prompt to accept the installation of the plugin[s]. - #[clap(short = 'y', long = "yes", takes_value = false)] + #[arg(short, long = "yes")] pub yes_to_all: bool, /// Overrides a failed compatibility check of the plugin with the current version of Spin. - #[clap(long = PLUGIN_OVERRIDE_COMPATIBILITY_CHECK_FLAG, takes_value = false)] + #[arg(long = PLUGIN_OVERRIDE_COMPATIBILITY_CHECK_FLAG)] pub override_compatibility_check: bool, /// Specific version of a plugin to be install from the centralized plugins /// repository. - #[clap( - long = "version", - short = 'v', + #[arg( + short, + long, conflicts_with = PLUGIN_REMOTE_PLUGIN_MANIFEST_OPT, conflicts_with = PLUGIN_LOCAL_PLUGIN_MANIFEST_OPT, conflicts_with = PLUGIN_ALL_OPT, @@ -207,15 +215,16 @@ pub struct Upgrade { pub version: Option, /// Allow downgrading a plugin's version. - #[clap(short = 'd', long = "downgrade", takes_value = false)] + #[arg(short, long)] pub downgrade: bool, } -impl Upgrade { +#[async_trait(?Send)] +impl Dispatch for Upgrade { /// Upgrades one or all plugins by reinstalling the latest or a specified /// version of a plugin. If downgrade is specified, first uninstalls the /// plugin. - pub async fn run(self) -> Result<()> { + async fn run(&self) -> Result<()> { let manager = PluginManager::try_default()?; let manifests_dir = manager.store().installed_manifests_directory(); @@ -235,7 +244,9 @@ impl Upgrade { self.upgrade_one(&plugin_name).await } } +} +impl Upgrade { // Install the latest of all currently installed plugins async fn upgrade_all(&self, manifests_dir: impl AsRef) -> Result<()> { let manager = PluginManager::try_default()?; @@ -269,12 +280,12 @@ impl Upgrade { Ok(()) } - async fn upgrade_one(self, name: &str) -> Result<()> { + async fn upgrade_one(&self, name: &str) -> Result<()> { let manager = PluginManager::try_default()?; - let manifest_location = match (self.local_manifest_src, self.remote_manifest_src) { - (Some(path), None) => ManifestLocation::Local(path), - (None, Some(url)) => ManifestLocation::Remote(url), - _ => ManifestLocation::PluginsRepository(PluginLookup::new(name, self.version)), + let manifest_location = match (&self.local_manifest_src, &self.remote_manifest_src) { + (Some(path), None) => ManifestLocation::Local(path.to_path_buf()), + (None, Some(url)) => ManifestLocation::Remote(url.clone()), + _ => ManifestLocation::PluginsRepository(PluginLookup::new(name, self.version.clone())), }; let manifest = manager.get_manifest(&manifest_location).await?; try_install( @@ -293,12 +304,13 @@ impl Upgrade { #[derive(Parser, Debug)] pub struct List { /// List only installed plugins. - #[clap(long = "installed", takes_value = false)] + #[arg(long)] pub installed: bool, } -impl List { - pub async fn run(self) -> Result<()> { +#[async_trait(?Send)] +impl Dispatch for List { + async fn run(&self) -> Result<()> { let mut plugins = if self.installed { Self::list_installed_plugins() } else { @@ -310,7 +322,9 @@ impl List { Self::print(&plugins); Ok(()) } +} +impl List { fn list_installed_plugins() -> Result> { let manager = PluginManager::try_default()?; let store = manager.store(); diff --git a/src/commands/templates.rs b/src/commands/templates.rs index c1d012401e..7834ed5c8c 100644 --- a/src/commands/templates.rs +++ b/src/commands/templates.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use anyhow::{Context, Result}; +use async_trait::async_trait; use clap::{Parser, Subcommand, ValueEnum}; use comfy_table::Table; use path_absolutize::Absolutize; @@ -10,6 +11,7 @@ use spin_templates::{ InstallOptions, InstallationResults, InstalledTemplateWarning, ListResults, ProgressReporter, SkippedReason, Template, TemplateManager, TemplateSource, }; +use crate::dispatch::{Dispatch, Action}; const INSTALL_FROM_DIR_OPT: &str = "FROM_DIR"; const INSTALL_FROM_GIT_OPT: &str = "FROM_GIT"; @@ -34,12 +36,13 @@ pub enum TemplateCommands { List(List), } -impl TemplateCommands { - pub async fn run(self) -> Result<()> { +#[async_trait(?Send)] +impl Dispatch for TemplateCommands { + async fn dispatch(&self, action: &Action) -> Result<()> { match self { - TemplateCommands::Install(cmd) => cmd.run().await, - TemplateCommands::Uninstall(cmd) => cmd.run().await, - TemplateCommands::List(cmd) => cmd.run().await, + Self::Install(cmd) => cmd.dispatch(action).await, + Self::Uninstall(cmd) => cmd.dispatch(action).await, + Self::List(cmd) => cmd.dispatch(action).await, } } } @@ -49,27 +52,27 @@ impl TemplateCommands { pub struct Install { /// The URL of the templates git repository. /// The templates must be in a git repository in a "templates" directory. - #[clap( - name = INSTALL_FROM_GIT_OPT, + #[arg( + id = INSTALL_FROM_GIT_OPT, long = "git", conflicts_with = INSTALL_FROM_DIR_OPT, )] pub git: Option, /// The optional branch of the git repository. - #[clap(long = "branch", requires = INSTALL_FROM_GIT_OPT)] + #[arg(long, requires = INSTALL_FROM_GIT_OPT)] pub branch: Option, /// Local directory containing the template(s) to install. - #[clap( - name = INSTALL_FROM_DIR_OPT, + #[arg( + id = INSTALL_FROM_DIR_OPT, long = "dir", conflicts_with = INSTALL_FROM_GIT_OPT, )] pub dir: Option, /// If present, updates existing templates instead of skipping. - #[structopt(long = "update")] + #[arg(long)] pub update: bool, } @@ -80,8 +83,9 @@ pub struct Uninstall { pub template_id: String, } -impl Install { - pub async fn run(self) -> Result<()> { +#[async_trait(?Send)] +impl Dispatch for Install { + async fn run(&self) -> Result<()> { let template_manager = TemplateManager::try_default() .context("Failed to construct template directory path")?; let source = match (&self.git, &self.dir) { @@ -107,7 +111,9 @@ impl Install { Ok(()) } +} +impl Install { fn print_installed_templates(&self, installation_results: &InstallationResults) { let templates = &installation_results.installed; let skipped = &installation_results.skipped; @@ -147,8 +153,9 @@ impl Install { } } -impl Uninstall { - pub async fn run(self) -> Result<()> { +#[async_trait(?Send)] +impl Dispatch for Uninstall { + async fn run(&self) -> Result<()> { let template_manager = TemplateManager::try_default() .context("Failed to construct template directory path")?; @@ -165,11 +172,11 @@ impl Uninstall { #[derive(Parser, Debug)] pub struct List { /// The format in which to list the templates. - #[clap(value_enum, long = "format", default_value = "table", hide = true)] + #[arg(value_enum, long, default_value = "table", hide = true)] pub format: ListFormat, /// Whether to show additional template details in the list. - #[clap(long = "verbose", takes_value = false)] + #[arg(long)] pub verbose: bool, } @@ -179,8 +186,9 @@ pub enum ListFormat { Json, } -impl List { - pub async fn run(self) -> Result<()> { +#[async_trait(?Send)] +impl Dispatch for List { + async fn run(&self) -> Result<()> { let template_manager = TemplateManager::try_default() .context("Failed to construct template directory path")?; let list_results = template_manager @@ -198,7 +206,9 @@ impl List { Ok(()) } +} +impl List { fn print_templates_table(&self, list_results: &ListResults) { let templates = &list_results.templates; let warnings = &list_results.warnings; diff --git a/src/commands/up.rs b/src/commands/up.rs index 3098800aae..56d87b832d 100644 --- a/src/commands/up.rs +++ b/src/commands/up.rs @@ -1,175 +1,119 @@ -use std::{ - ffi::OsString, - fmt::Debug, - path::{Path, PathBuf}, -}; +use std::{fmt::Debug, path::{Path, PathBuf}}; use anyhow::{anyhow, bail, Context, Result}; -use clap::{CommandFactory, Parser}; +use clap::{Parser, Subcommand, ArgGroup, ArgAction, Arg, FromArgMatches}; use reqwest::Url; -use spin_loader::bindle::BindleConnectionInfo; -use spin_manifest::ApplicationTrigger; +use spin_manifest::{Application, ApplicationTrigger}; use spin_trigger::cli::{SPIN_LOCKED_URL, SPIN_WORKING_DIR}; -use tempfile::TempDir; -use crate::opts::*; +use crate::{args::{component::ComponentOptions, app_source::AppSource}, dispatch::{Dispatch, Action}}; +use async_trait::async_trait; +use crate::dispatch::Runner; -/// Start the Fermyon runtime. -#[derive(Parser, Debug, Default)] -#[clap( - about = "Start the Spin application", - allow_hyphen_values = true, - disable_help_flag = true -)] -pub struct UpCommand { - #[clap(short = 'h', long = "help")] - pub help: bool, - - /// Path to spin.toml. - #[clap( - name = APP_CONFIG_FILE_OPT, - short = 'f', - long = "file", - conflicts_with = BINDLE_ID_OPT, - )] - pub app: Option, - - /// ID of application bindle. - #[clap( - name = BINDLE_ID_OPT, - short = 'b', - long = "bindle", - conflicts_with = APP_CONFIG_FILE_OPT, - requires = BINDLE_SERVER_URL_OPT, - )] - pub bindle: Option, - - /// URL of bindle server. - #[clap( - name = BINDLE_SERVER_URL_OPT, - long = "bindle-server", - env = BINDLE_URL_ENV, - )] - pub server: Option, - - /// Basic http auth username for the bindle server - #[clap( - name = BINDLE_USERNAME, - long = "bindle-username", - env = BINDLE_USERNAME, - requires = BINDLE_PASSWORD - )] - pub bindle_username: Option, - - /// Basic http auth password for the bindle server - #[clap( - name = BINDLE_PASSWORD, - long = "bindle-password", - env = BINDLE_PASSWORD, - requires = BINDLE_USERNAME - )] - pub bindle_password: Option, - - /// Ignore server certificate errors from bindle server - #[clap( - name = INSECURE_OPT, - short = 'k', - long = "insecure", - takes_value = false, - )] - pub insecure: bool, - - /// Pass an environment variable (key=value) to all components of the application. - #[clap(short = 'e', long = "env", parse(try_from_str = parse_env_var))] - pub env: Vec<(String, String)>, - - /// Temporary directory for the static assets of the components. - #[clap(long = "temp")] - pub tmp: Option, - - /// For local apps with directory mounts and no excluded files, mount them directly instead of using a temporary - /// directory. - /// - /// This allows you to update the assets on the host filesystem such that the updates are visible to the guest - /// without a restart. This cannot be used with bindle apps or apps which use file patterns and/or exclusions. - #[clap(long, takes_value = false, conflicts_with = BINDLE_ID_OPT)] - pub direct_mounts: bool, - - /// All other args, to be passed through to the trigger - #[clap(hide = true)] - pub trigger_args: Vec, -} impl UpCommand { - pub async fn run(self) -> Result<()> { - // For displaying help, first print `spin up`'s own usage text, then - // attempt to load an app and print trigger-type-specific usage. - let help = self.help; - if help { - Self::command() - .name("spin-up") - .bin_name("spin up") - .print_help()?; - println!(); - } - self.run_inner().await.or_else(|err| { - if help { - tracing::warn!("Error resolving trigger-specific help: {}", err); - Ok(()) - } else { - Err(err) + pub async fn load(&self) -> Result { + let Self { source, components, trigger } = self; + let mut app = source.load(&components.working_dir()?).await?; + if !components.env.is_empty() { + for c in app.components.iter_mut() { + c.wasm.environment.extend(components.env.iter().cloned()); } - }) + } + Ok(app) } +} - async fn run_inner(self) -> Result<()> { - if self.help - && self.app.is_none() - && self.bindle.is_none() - && !PathBuf::from(DEFAULT_MANIFEST_FILE).exists() - { - return self.run_trigger( - trigger_command(HELP_ARGS_ONLY_TRIGGER_TYPE), - TriggerExecOpts::NoApp, - ); - } +/// Start the Fermyon runtime. +#[derive(Parser, Debug, Clone, Default)] +#[command(about = "Start the Spin application")] +#[command(allow_hyphen_values = true)] +pub struct UpCommand { + /// The app location to start () + #[command(flatten, next_help_heading = "App Source")] + #[group(skip)] + pub source: AppSource, + // Options to pass through to the app's components + #[command(flatten, next_help_heading = "Component Options")] + pub components: ComponentOptions, + // Options to pass through to the trigger + #[arg(hide = true, trailing_var_arg = true, help_heading = "Trigger Options")] + pub trigger: Vec, +} - let working_dir_holder = match &self.tmp { - None => WorkingDirectory::Temporary(tempfile::tempdir()?), - Some(d) => WorkingDirectory::Given(d.to_owned()), - }; - let working_dir = working_dir_holder.path().canonicalize()?; - - let mut app = match (&self.app, &self.bindle) { - (app, None) => { - let manifest_file = app - .as_deref() - .unwrap_or_else(|| DEFAULT_MANIFEST_FILE.as_ref()); - let bindle_connection = self.bindle_connection(); - let asset_dst = if self.direct_mounts { - None - } else { - Some(&working_dir) - }; - spin_loader::from_file(manifest_file, asset_dst, &bindle_connection).await? - } - (None, Some(bindle)) => match &self.server { - Some(server) => { - assert!(!self.direct_mounts); +#[derive(Debug, Clone, Default)] +pub struct Flag(Option); - spin_loader::from_bindle(bindle, server, &working_dir).await? - } - _ => bail!("Loading from a bindle requires a Bindle server URL"), - }, - (Some(_), Some(_)) => bail!("Specify only one of app file or bindle ID"), - }; +impl Flag { + fn get(&self) -> Result<&T> { + self.0.as_ref().ok_or(anyhow!("flag disabled")) + } +} + +pub trait FlagShortcut: FromArgMatches + Parser + Clone + Send + Sync + Sized + Debug { + const GROUP: &'static str; + const LONG: &'static str; + const SHORT: char; + const ACTION: ArgAction; +} - // Apply --env to component environments - if !self.env.is_empty() { - for component in app.components.iter_mut() { - component.wasm.environment.extend(self.env.iter().cloned()); +impl FromArgMatches for Flag where T: FlagShortcut { + fn from_arg_matches(matches: &clap::ArgMatches) -> std::result::Result { + match matches.try_get_one::(T::LONG) { + Ok(flag) => if let Some(flag) = flag { + match T::from_arg_matches(matches) { + Ok(inner) if *flag => Ok(Self(Some(inner))), + Ok(inner) => unreachable!("{flag} {inner:?}"), + Err(error) => Err(error), } + } else { Ok(Self(None)) }, + Err(e) => unreachable!("{e}"), } + } + + fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> std::result::Result<(), clap::Error> { + Ok(*self = Self::from_arg_matches(matches)?) + } +} + +impl Subcommand for Flag where T: FlagShortcut { + + fn augment_subcommands(cmd: clap::Command) -> clap::Command { + let upcmd = T::command_for_update(); + let upargs = upcmd.get_arguments(); + let newargs = upargs.map(|arg| arg.clone().group(T::GROUP)); + cmd.arg( + Arg::new(T::LONG).long(T::LONG).short(T::SHORT).action(T::ACTION) + ).group(ArgGroup::new(T::GROUP).requires(T::LONG)).args(newargs) + } + + fn augment_subcommands_for_update(cmd: clap::Command) -> clap::Command { + Self::augment_subcommands(cmd) + } + + fn has_subcommand(_name: &str) -> bool { + false + } +} + +impl FlagShortcut for UpCommand { + const GROUP: &'static str = "up_args"; + const LONG: &'static str = "up"; + const SHORT: char = 'u'; + const ACTION: ArgAction = ArgAction::SetTrue; +} + +#[async_trait(?Send)] +impl Dispatch for Flag where T: Dispatch { + async fn dispatch(&self, action: &Action) -> Result<()> { + self.get()?.dispatch(action).await + } +} + +#[async_trait(?Send)] +impl Dispatch for UpCommand { + async fn run(&self) -> Result<()> { + let app = self.load().await?; let trigger_type = match &app.info.trigger { ApplicationTrigger::Http(_) => trigger_command("http"), @@ -177,36 +121,32 @@ impl UpCommand { ApplicationTrigger::External(cfg) => vec![resolve_trigger_plugin(cfg.trigger_type())?], }; - let exec_opts = if self.help { - TriggerExecOpts::NoApp - } else { - TriggerExecOpts::App { app, working_dir } - }; + let working_dir = self.components.working_dir()?; - self.run_trigger(trigger_type, exec_opts) + self.run_trigger(trigger_type, app, working_dir, false) } +} + +impl UpCommand { fn run_trigger( - self, + &self, trigger_type: Vec, - exec_opts: TriggerExecOpts, + app: Application, + working_dir: PathBuf, + help: bool ) -> Result<(), anyhow::Error> { // The docs for `current_exe` warn that this may be insecure because it could be executed // via hard-link. I think it should be fine as long as we aren't `setuid`ing this binary. let mut cmd = std::process::Command::new(std::env::current_exe().unwrap()); cmd.args(&trigger_type); + if help { cmd.arg("--help-args-only"); } - match exec_opts { - TriggerExecOpts::NoApp => { - cmd.arg("--help-args-only"); - } - TriggerExecOpts::App { app, working_dir } => { - let locked_url = self.write_locked_app(app, &working_dir)?; - cmd.env(SPIN_LOCKED_URL, locked_url) - .env(SPIN_WORKING_DIR, &working_dir) - .args(&self.trigger_args); - } - } + let locked_url = self.write_locked_app(app, &working_dir)?; + cmd.env(SPIN_LOCKED_URL, locked_url) + .env(SPIN_WORKING_DIR, &working_dir) + .args(&self.trigger); + tracing::trace!("Running trigger executor: {:?}", cmd); @@ -251,40 +191,31 @@ impl UpCommand { Ok(locked_url) } - fn bindle_connection(&self) -> Option { - self.server.as_ref().map(|url| { - BindleConnectionInfo::new( - url, - self.insecure, - self.bindle_username.clone(), - self.bindle_password.clone(), - ) - }) - } + // fn bindle_connection(&self) -> Option { + // self.server.as_ref().map(|url| { + // BindleConnectionInfo::new( + // url, + // self.insecure, + // self.bindle_username.clone(), + // self.bindle_password.clone(), + // ) + // }) + // } } -enum WorkingDirectory { - Given(PathBuf), - Temporary(TempDir), -} - -impl WorkingDirectory { - fn path(&self) -> &Path { - match self { - Self::Given(p) => p, - Self::Temporary(t) => t.path(), - } - } -} +// enum WorkingDirectory { +// Given(PathBuf), +// Temporary(TempDir), +// } -// Parse the environment variables passed in `key=value` pairs. -fn parse_env_var(s: &str) -> Result<(String, String)> { - let parts: Vec<_> = s.splitn(2, '=').collect(); - if parts.len() != 2 { - bail!("Environment variable must be of the form `key=value`"); - } - Ok((parts[0].to_owned(), parts[1].to_owned())) -} +// impl WorkingDirectory { +// fn path(&self) -> &Path { +// match self { +// Self::Given(p) => p, +// Self::Temporary(t) => t.path(), +// } +// } +// } fn resolve_trigger_plugin(trigger_type: &str) -> Result { use crate::commands::plugins::PluginCompatibility; @@ -319,15 +250,6 @@ fn resolve_trigger_plugin(trigger_type: &str) -> Result { } } -#[allow(clippy::large_enum_variant)] // The large variant is the common case and really this is equivalent to an Option -enum TriggerExecOpts { - NoApp, - App { - app: spin_manifest::Application, - working_dir: PathBuf, - }, -} - fn trigger_command(trigger_type: &str) -> Vec { vec!["trigger".to_owned(), trigger_type.to_owned()] } diff --git a/src/dispatch.rs b/src/dispatch.rs new file mode 100644 index 0000000000..ae584b3eae --- /dev/null +++ b/src/dispatch.rs @@ -0,0 +1,33 @@ +pub use async_trait::async_trait; +pub use anyhow::Result; + +#[async_trait(?Send)] +pub trait Runner { + async fn run(&self) -> Result<()>; + async fn help(&self) -> Result<()>; +} + +#[async_trait(?Send)] +pub trait Dispatch { + async fn dispatch(&self, action: &Action) -> Result<()> { + match action { + Action::Run => self.run().await, + Action::Help => self.help().await + } + } + + async fn run(&self) -> Result<()> { + Ok(()) + } + + async fn help(&self) -> Result<()> { + Ok(()) + } +} + +pub enum Action { + Run, + Help +} + +pub mod macros; \ No newline at end of file diff --git a/src/dispatch/macros.rs b/src/dispatch/macros.rs new file mode 100644 index 0000000000..28416ba784 --- /dev/null +++ b/src/dispatch/macros.rs @@ -0,0 +1,113 @@ +#[macro_export] +macro_rules! __match_trait { + ($enum:ident, $type:ident, [$($variant:ident),*], $value:ident, $block:block) => { + match $enum { $($type::$variant($value) => $block,)* } + }; +} + +#[macro_export] +macro_rules! match_trait { + ( + match $enum:ident { + $type:ident::($($variant:ident)|*)($value:ident) => $block:block + } + ) => { + __match_trait!($enum, $type, [$($variant),*], $value, $block); + }; +} + +#[macro_export] +macro_rules! __impl_trait { + ([$($attrs:meta),*], $trait:ident, $type:ident, $function:ident, $value:ident, [$($variant:ident),*], $arg:ident, $argtype:ty, $ret:ty, $block:block) => { + $(#[$attrs])* + impl $trait for $type { + async fn $function(&self, $arg: $argtype) -> $ret { + __match_trait!(self, Self, [$($variant),*], $value, $block) + } + } + }; +} + +#[macro_export] +macro_rules! impl_trait { + ( + $(#[$attrs:meta])* + impl $trait:ident for $type:ident { + async fn $function:ident($value:ident: $($variant:ident)|*, $arg:ident: $argtype:ty) -> $ret:ty $block:block + } + ) => { + __impl_trait!([$($attrs),*], $trait, $type, $function, $value, [$($variant),*], $arg, $argtype, $ret, $block); + } +} + +#[macro_export] +macro_rules! __type_enum { + ($($enum_attrs:meta)*, $enum:ident, $($($var_attrs:meta)*, $variant:ident, $type:ident),*) => { + $(#[$enum_attrs])* + enum $enum { + $( + $(#[$var_attrs])* + $variant($type) + ),* + } + } +} + +#[macro_export] +macro_rules! type_enum { + ( + $(#[$enum_attrs:meta])* + enum $enum:ident { + $( + $(#[$var_attrs:meta])* + $variant:ident($type:ident) + ),* + } + ) => { + __type_enum!($($enum_attrs)*, $enum, $($($var_attrs)*, $variant, $type),*); + } +} + +#[macro_export] +macro_rules! trait_enum { + ($(#[$attrs:meta])* + enum $enum:ident: $trait:ty { + $( + $(#[$var_attrs:meta])* + $variant:ident($type:ident) + ),* + } + ) => { + __type_enum!($($attrs)*, $enum, $($($var_attrs)*, $variant, $type),*); + impl_dispatch!($enum::{$($type),*}); + } +} + + +#[macro_export] +macro_rules! match_action { + ($value:ident[$action:ident]$(.$ex:ident)*) => { + match $action { + Action::Run => $value.run()$(.$ex)*, + Action::Help => $value.help()$(.$ex)* + } + }; +} + +#[macro_export] +macro_rules! impl_dispatch { + ($type:ident::{$($variant:ident),*}) => { + __impl_trait!( + [async_trait::async_trait(?Send)], + Dispatch, + $type, + dispatch, + value, + [$($variant),*], + action, + &Action, + Result<()>, + { match_action!(value[action].await) } + ); + }; +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 00c6d91e86..7ff4c40ade 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,8 @@ +pub mod args; pub mod commands; pub(crate) mod opts; mod sloth; +pub mod dispatch; use anyhow::{anyhow, Result}; use semver::BuildMetadata; @@ -8,7 +10,7 @@ use spin_publish::bindle::PublishError; use std::path::Path; pub use crate::opts::HELP_ARGS_ONLY_TRIGGER_TYPE; - +pub use crate::dispatch::macros; pub(crate) fn push_all_failed_msg(path: &Path, server_url: &str) -> String { format!( "Failed to push bindle from '{}' to the server at '{}'", diff --git a/src/opts.rs b/src/opts.rs index b42a82e36a..b236f52365 100644 --- a/src/opts.rs +++ b/src/opts.rs @@ -1,6 +1,5 @@ pub const DEFAULT_MANIFEST_FILE: &str = "spin.toml"; pub const APP_CONFIG_FILE_OPT: &str = "APP_CONFIG_FILE"; -pub const BINDLE_ID_OPT: &str = "BINDLE_ID"; pub const BINDLE_SERVER_URL_OPT: &str = "BINDLE_SERVER_URL"; pub const BINDLE_URL_ENV: &str = "BINDLE_URL"; pub const BINDLE_USERNAME: &str = "BINDLE_USERNAME"; @@ -13,7 +12,6 @@ pub const HIPPO_URL_ENV: &str = "HIPPO_URL"; pub const HIPPO_USERNAME: &str = "HIPPO_USERNAME"; pub const HIPPO_PASSWORD: &str = "HIPPO_PASSWORD"; pub const DEPLOYMENT_ENV_NAME_ENV: &str = "FERMYON_DEPLOYMENT_ENVIRONMENT"; -pub const BUILD_UP_OPT: &str = "UP"; pub const PLUGIN_NAME_OPT: &str = "PLUGIN_NAME"; pub const PLUGIN_REMOTE_PLUGIN_MANIFEST_OPT: &str = "REMOTE_PLUGIN_MANIFEST"; pub const PLUGIN_LOCAL_PLUGIN_MANIFEST_OPT: &str = "LOCAL_PLUGIN_MANIFEST"; diff --git a/tests/integration.rs b/tests/integration.rs index 5257e9c8af..36341ba165 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -885,16 +885,24 @@ mod integration_tests { ) -> Result { // start Spin using the given application manifest and wait for the HTTP server to be available. let url = format!("127.0.0.1:{}", get_random_port()?); - let mut args = vec!["up", "--file", manifest_path, "--listen", &url]; - args.extend(spin_args); - if let Some(b) = bindle_url { - args.push("--bindle-server"); - args.push(b); - } - for v in spin_app_env { - args.push("--env"); - args.push(v); - } + let env_args = spin_app_env.iter().flat_map(|e| ["--env", e]).collect(); + + let trigger_args = vec!["--file", manifest_path, "--listen", &url]; + + let bindle_args = match bindle_url { + Some(url) => vec!["--bindle-server", url], + None => vec![], + }; + + let args = vec![ + vec!["up"], + spin_args.to_vec(), + bindle_args, + env_args, + trigger_args, + ] + .into_iter() + .flatten(); let mut spin_handle = Command::new(get_process(SPIN_BINARY)) .args(args) @@ -919,8 +927,8 @@ mod integration_tests { env: &[&str], ) -> Result { let url = format!("127.0.0.1:{}", get_random_port()?); - let mut args = vec![ - "up", + let env_args = env.iter().flat_map(|e| ["--env", e]).collect(); + let trigger_args = vec![ "--bindle", id, "--bindle-server", @@ -928,10 +936,10 @@ mod integration_tests { "--listen", &url, ]; - for v in env { - args.push("--env"); - args.push(v); - } + + let args = vec![vec!["up"], env_args, trigger_args] + .into_iter() + .flatten(); let mut spin_handle = Command::new(get_process(SPIN_BINARY)) .args(args)