From e0c15ae4575335fb079e2d33fc853a547b2380c9 Mon Sep 17 00:00:00 2001 From: Ava Hahn Date: Fri, 14 Jun 2024 21:04:15 -0700 Subject: [PATCH] tools/unitctl: implement application subcommand * application subcommand UI schema * application subcommand handler * additions to unit-client-rs to expose application API * elaborate on OpenAPI error handling * adds wasm and wasi app schemas to OpenAPI Schema * updates tools/unitctl OpenAPI library * many linter fixes * README.md updates Signed-off-by: Ava Hahn --- docs/unit-openapi.yaml | 85 +++++++++++++- tools/unitctl/GNUmakefile | 4 +- tools/unitctl/README.md | 26 +++- .../src/control_socket_address.rs | 2 - .../unitctl/unit-client-rs/src/unit_client.rs | 48 +++++++- .../unit-client-rs/src/unitd_docker.rs | 111 ++++++++---------- .../unit-openapi/.openapi-generator/FILES | 18 +-- .../unit-openapi/.openapi-generator/VERSION | 2 +- tools/unitctl/unit-openapi/README.md | 10 +- tools/unitctl/unit-openapi/src/lib.rs | 4 +- tools/unitctl/unitctl/src/cmd/applications.rs | 36 ++++++ tools/unitctl/unitctl/src/cmd/instances.rs | 77 ++++++------ tools/unitctl/unitctl/src/cmd/mod.rs | 1 + tools/unitctl/unitctl/src/main.rs | 7 +- tools/unitctl/unitctl/src/unitctl.rs | 41 ++++++- 15 files changed, 335 insertions(+), 137 deletions(-) create mode 100644 tools/unitctl/unitctl/src/cmd/applications.rs diff --git a/docs/unit-openapi.yaml b/docs/unit-openapi.yaml index b2e02e89..f69e615c 100644 --- a/docs/unit-openapi.yaml +++ b/docs/unit-openapi.yaml @@ -4730,6 +4730,9 @@ components: isolation: rootfs: "/www/" + wasiapp: + type: "wasm-wasi-component" + # /config/listeners configListeners: summary: "Multiple listeners" @@ -5304,6 +5307,8 @@ components: - $ref: "#/components/schemas/configApplicationPHP" - $ref: "#/components/schemas/configApplicationPython" - $ref: "#/components/schemas/configApplicationRuby" + - $ref: "#/components/schemas/configApplicationWasm" + - $ref: "#/components/schemas/configApplicationWasi" discriminator: propertyName: type @@ -5314,6 +5319,8 @@ components: php: "#/components/schemas/configApplicationPHP" python: "#/components/schemas/configApplicationPython" ruby: "#/components/schemas/configApplicationRuby" + wasm: "#/components/schemas/configApplicationWasm" + wasm-wasi-component: "#/components/schemas/configApplicationWasi" # ABSTRACT BASE SCHEMA, NOT PRESENT IN THE CONFIGURATION; STORES COMMON OPTIONS configApplicationCommon: @@ -5326,7 +5333,7 @@ components: type: type: string description: "Application type and language version." - enum: [external, java, perl, php, python, ruby] + enum: [external, java, perl, php, python, ruby, wasm, wasm-wasi-component] environment: type: object @@ -5592,6 +5599,82 @@ components: description: "Number of worker threads per app process." default: 1 + configApplicationWasm: + description: "WASM application on Unit." + allOf: + - $ref: "#/components/schemas/configApplicationCommon" + - type: object + required: + - module + - request_handler + - malloc_handler + - free_handler + + properties: + module: + type: string + description: "Path to WebAssembly module." + + request_handler: + type: string + description: "Name of request handling function." + + malloc_handler: + type: string + description: "Name of memory allocator function." + + free_handler: + type: string + description: "Name of memory free function." + + access: + type: object + properties: + filesystem: + $ref: "#/components/schemas/stringArray" + description: "Host directories this application may have access to." + + module_init_handler: + type: string + description: "Name of function called to initialize module." + + module_end_handler: + type: string + description: "Name of function called to teardown module." + + request_init_handler: + type: string + description: "Name of function called to initialize request." + + request_end_handler: + type: string + description: "Name of function called to teardown request." + + response_end_handler: + type: string + description: "Name of function called to teardown response." + + + configApplicationWasi: + description: "WASI application on Unit." + allOf: + - $ref: "#/components/schemas/configApplicationCommon" + - type: object + required: + - component + + properties: + component: + type: string + description: "Path to wasm wasi component application." + + access: + type: object + properties: + filesystem: + $ref: "#/components/schemas/stringArray" + description: "Host directories this application may have access to." + configApplicationPHP: description: "PHP application on Unit." allOf: diff --git a/tools/unitctl/GNUmakefile b/tools/unitctl/GNUmakefile index e7cb379a..9992a322 100644 --- a/tools/unitctl/GNUmakefile +++ b/tools/unitctl/GNUmakefile @@ -23,7 +23,7 @@ CARGO ?= cargo DOCKER ?= docker DOCKER_BUILD_FLAGS ?= --load CHECKSUM ?= sha256sum -OPENAPI_GENERATOR_VERSION ?= 6.6.0 +OPENAPI_GENERATOR_VERSION ?= 7.6.0 # Define platform targets based off of the current host OS # If running MacOS, then build for MacOS platform targets installed in rustup @@ -137,7 +137,7 @@ openapi-clean: ## Clean up generated OpenAPI files $Q find "$(CURDIR)/unit-openapi/src/apis" \ ! -name 'error.rs' -type f -exec rm -f {} + $Q $(info $(M) cleaning up generated OpenAPI models code) - $Q rm -rf "$(CURDIR)/unit-openapi/src/models/*" + $Q rm -rf "$(CURDIR)/unit-openapi/src/models" include $(CURDIR)/build/package.mk include $(CURDIR)/build/container.mk diff --git a/tools/unitctl/README.md b/tools/unitctl/README.md index 2e5a2da1..dca16e63 100644 --- a/tools/unitctl/README.md +++ b/tools/unitctl/README.md @@ -31,13 +31,13 @@ their own makefile targets. Alternatively, all available binary targets can be built with `make all`. See the below example for illustration: ``` -[ava@calliope cli]$ make list-targets +$ make list-targets x86_64-unknown-linux-gnu -[ava@calliope cli]$ make x86_64-unknown-linux-gnu +$ make x86_64-unknown-linux-gnu ▶ building unitctl with flags [--quiet --release --bin unitctl --target x86_64-unknown-linux-gnu] -[ava@calliope cli]$ file ./target/x86_64-unknown-linux-gnu/release/unitctl +$ file ./target/x86_64-unknown-linux-gnu/release/unitctl ./target/x86_64-unknown-linux-gnu/release/unitctl: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=ef4b094ffd549b39a8cb27a7ba2cc0dbad87a3bc, for GNU/Linux 4.4.0, @@ -91,6 +91,26 @@ To the subcommand `unitctl instances new` the user must provide three things: After deployment the user will have one Unit container running on the host network. +### Lists active applications and provides means to restart them +Listing applications: +``` +$ unitctl app list +{ + "wasm": { + "type": "wasm-wasi-component", + "component": "/www/wasmapp-proxy-component.wasm" + } +} +``` + +Restarting an application: +``` +$ unitctl app reload wasm +{ + "success": "Ok" +} +``` + ### Lists active listeners from running Unit processes ``` unitctl listeners diff --git a/tools/unitctl/unit-client-rs/src/control_socket_address.rs b/tools/unitctl/unit-client-rs/src/control_socket_address.rs index 402d2293..438ab0ad 100644 --- a/tools/unitctl/unit-client-rs/src/control_socket_address.rs +++ b/tools/unitctl/unit-client-rs/src/control_socket_address.rs @@ -34,7 +34,6 @@ impl ControlSocketScheme { } } - impl Display for ControlSocket { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { @@ -312,7 +311,6 @@ impl ControlSocket { } } - #[cfg(test)] mod tests { use rand::distributions::{Alphanumeric, DistString}; diff --git a/tools/unitctl/unit-client-rs/src/unit_client.rs b/tools/unitctl/unit-client-rs/src/unit_client.rs index b8c73ec0..b3f07308 100644 --- a/tools/unitctl/unit-client-rs/src/unit_client.rs +++ b/tools/unitctl/unit-client-rs/src/unit_client.rs @@ -15,9 +15,11 @@ use serde::{Deserialize, Serialize}; use crate::control_socket_address::ControlSocket; use unit_openapi::apis::configuration::Configuration; -use unit_openapi::apis::{Error as OpenAPIError, StatusApi}; -use unit_openapi::apis::{ListenersApi, ListenersApiClient, StatusApiClient}; -use unit_openapi::models::{ConfigListener, Status}; +use unit_openapi::apis::{ + ApplicationsApi, ApplicationsApiClient, AppsApi, AppsApiClient, Error as OpenAPIError, ListenersApi, + ListenersApiClient, StatusApi, StatusApiClient, +}; +use unit_openapi::models::{ConfigApplication, ConfigListener, Status}; const USER_AGENT: &str = concat!("UNIT CLI/", env!("CARGO_PKG_VERSION"), "/rust"); @@ -276,6 +278,46 @@ impl UnitClient { }) } + pub fn applications_api(&self) -> Box { + new_openapi_client!(self, ApplicationsApiClient, ApplicationsApi) + } + + pub async fn applications(&self) -> Result, Box> { + self.applications_api().get_applications().await.or_else(|err| { + if let OpenAPIError::Hyper(hyper_error) = err { + Err(Box::new(UnitClientError::new( + hyper_error, + self.control_socket.to_string(), + "/applications".to_string(), + ))) + } else { + Err(Box::new(UnitClientError::OpenAPIError { source: err })) + } + }) + } + + pub async fn per_application_api(&self) -> Box { + new_openapi_client!(self, AppsApiClient, AppsApi) + } + + pub async fn restart_application(&self, name: &String) -> Result, Box> { + self.per_application_api() + .await + .get_app_restart(name.as_str()) + .await + .or_else(|err| { + if let OpenAPIError::Hyper(hyper_error) = err { + Err(Box::new(UnitClientError::new( + hyper_error, + self.control_socket.to_string(), + format!("/control/applications/{}/restart", name), + ))) + } else { + Err(Box::new(UnitClientError::OpenAPIError { source: err })) + } + }) + } + pub async fn is_running(&self) -> bool { self.status().await.is_ok() } diff --git a/tools/unitctl/unit-client-rs/src/unitd_docker.rs b/tools/unitctl/unit-client-rs/src/unitd_docker.rs index 6881893d..0d318096 100644 --- a/tools/unitctl/unit-client-rs/src/unitd_docker.rs +++ b/tools/unitctl/unit-client-rs/src/unitd_docker.rs @@ -1,19 +1,16 @@ use std::collections::HashMap; use std::fs::read_to_string; -use std::path::{PathBuf, MAIN_SEPARATOR}; use std::io::stderr; +use std::path::{PathBuf, MAIN_SEPARATOR}; +use crate::control_socket_address::ControlSocket; use crate::futures::StreamExt; use crate::unit_client::UnitClientError; use crate::unitd_process::UnitdProcess; -use crate::control_socket_address::ControlSocket; use bollard::container::{Config, ListContainersOptions, StartContainerOptions}; use bollard::image::CreateImageOptions; -use bollard::models::{ - ContainerCreateResponse, HostConfig, Mount, - MountTypeEnum, ContainerSummary, -}; +use bollard::models::{ContainerCreateResponse, ContainerSummary, HostConfig, Mount, MountTypeEnum}; use bollard::secret::ContainerInspectResponse; use bollard::Docker; @@ -156,22 +153,18 @@ impl UnitdContainer { // cant do this functionally because of the async call let mut mapped = vec![]; for ctr in summary { - if unitd_command_re.is_match(&ctr.clone().command - .or(Some(String::new())) - .unwrap()) { - let mut c = UnitdContainer::from(&ctr); - if let Some(names) = ctr.names { - if names.len() > 0 { - let name = names[0].strip_prefix("/") - .or(Some(names[0].as_str())).unwrap(); - if let Ok(cir) = docker - .inspect_container(name, None).await { - c.details = Some(cir); - } + if unitd_command_re.is_match(&ctr.clone().command.or(Some(String::new())).unwrap()) { + let mut c = UnitdContainer::from(&ctr); + if let Some(names) = ctr.names { + if names.len() > 0 { + let name = names[0].strip_prefix("/").or(Some(names[0].as_str())).unwrap(); + if let Ok(cir) = docker.inspect_container(name, None).await { + c.details = Some(cir); } } - mapped.push(c); } + mapped.push(c); + } } mapped } @@ -196,11 +189,11 @@ impl UnitdContainer { // either return translated path or original prefixed with "container" if keys.len() > 0 { - let mut matches = self.mounts[&keys[0]] - .clone() - .join(cp.as_path() - .strip_prefix(keys[0].clone()) - .expect("error checking path prefix")); + let mut matches = self.mounts[&keys[0]].clone().join( + cp.as_path() + .strip_prefix(keys[0].clone()) + .expect("error checking path prefix"), + ); /* Observed on M1 Mac that Docker on OSX * adds a bunch of garbage to the mount path * converting it into a useless directory @@ -208,15 +201,14 @@ impl UnitdContainer { */ if cfg!(target_os = "macos") { let mut abs = PathBuf::from(String::from(MAIN_SEPARATOR)); - let m = matches.strip_prefix("/host_mnt/private") - .unwrap_or(matches.strip_prefix("/host_mnt") - .unwrap_or(matches.as_path())); + let m = matches + .strip_prefix("/host_mnt/private") + .unwrap_or(matches.strip_prefix("/host_mnt").unwrap_or(matches.as_path())); // make it absolute again abs.push(m); matches = abs; } - matches.to_string_lossy() - .to_string() + matches.to_string_lossy().to_string() } else { format!(":{}", cp.display()) } @@ -264,10 +256,7 @@ pub async fn deploy_new_container( let mut mounts = vec![]; // if a unix socket is specified, mounts its directory if socket.is_local_socket() { - let mount_path = PathBuf::from(socket.clone()) - .as_path() - .to_string_lossy() - .to_string(); + let mount_path = PathBuf::from(socket.clone()).as_path().to_string_lossy().to_string(); mounts.push(Mount { typ: Some(MountTypeEnum::BIND), source: Some(mount_path), @@ -290,22 +279,22 @@ pub async fn deploy_new_container( Some(CreateImageOptions { from_image: image.as_str(), ..Default::default() - }), None, None + }), + None, + None, ); while let Some(res) = stream.next().await { if let Ok(info) = res { if let Some(id) = info.id { if let Some(_) = totals.get_mut(&id) { - if let Some(delta) = info.progress_detail - .and_then(|detail| detail.current) { - pb.add(delta as u64); - } + if let Some(delta) = info.progress_detail.and_then(|detail| detail.current) { + pb.add(delta as u64); + } } else { - if let Some(total) = info.progress_detail - .and_then(|detail| detail.total) { - totals.insert(id, total); - pb.total += total as u64; - } + if let Some(total) = info.progress_detail.and_then(|detail| detail.total) { + totals.insert(id, total); + pb.total += total as u64; + } } } } @@ -357,27 +346,27 @@ pub async fn deploy_new_container( .await { // somehow our container doesnt exist - Err(e) => Err(UnitClientError::UnitdDockerError{ - message: e.to_string() - }), + Err(e) => Err(UnitClientError::UnitdDockerError { message: e.to_string() }), // here it is! Ok(info) => { if info.len() < 1 { return Err(UnitClientError::UnitdDockerError { message: "couldnt find new container".to_string(), }); - } else if info[0].names.is_none() || - info[0].names.clone().unwrap().len() < 1 { - return Err(UnitClientError::UnitdDockerError { - message: "new container has no name".to_string(), - }); - } + } else if info[0].names.is_none() || info[0].names.clone().unwrap().len() < 1 { + return Err(UnitClientError::UnitdDockerError { + message: "new container has no name".to_string(), + }); + } // start our container - match docker.start_container( - info[0].names.clone().unwrap()[0].strip_prefix(MAIN_SEPARATOR).unwrap(), - None::>, - ).await { + match docker + .start_container( + info[0].names.clone().unwrap()[0].strip_prefix(MAIN_SEPARATOR).unwrap(), + None::>, + ) + .await + { Err(err) => Err(UnitClientError::UnitdDockerError { message: err.to_string(), }), @@ -439,14 +428,8 @@ mod tests { ctr.host_path("/path/to/conf".to_string()) ); if cfg!(target_os = "macos") { - assert_eq!( - "/6/test".to_string(), - ctr.host_path("/var/test".to_string()) - ); - assert_eq!( - "/7/test".to_string(), - ctr.host_path("/var/var/test".to_string()) - ); + assert_eq!("/6/test".to_string(), ctr.host_path("/var/test".to_string())); + assert_eq!("/7/test".to_string(), ctr.host_path("/var/var/test".to_string())); } } diff --git a/tools/unitctl/unit-openapi/.openapi-generator/FILES b/tools/unitctl/unit-openapi/.openapi-generator/FILES index 4f177f5f..f487c081 100644 --- a/tools/unitctl/unit-openapi/.openapi-generator/FILES +++ b/tools/unitctl/unit-openapi/.openapi-generator/FILES @@ -26,21 +26,18 @@ docs/ConfigApplicationCommonLimits.md docs/ConfigApplicationCommonProcesses.md docs/ConfigApplicationCommonProcessesAnyOf.md docs/ConfigApplicationExternal.md -docs/ConfigApplicationExternalAllOf.md docs/ConfigApplicationJava.md -docs/ConfigApplicationJavaAllOf.md docs/ConfigApplicationPerl.md -docs/ConfigApplicationPerlAllOf.md docs/ConfigApplicationPhp.md -docs/ConfigApplicationPhpAllOf.md docs/ConfigApplicationPhpAllOfOptions.md docs/ConfigApplicationPhpAllOfTargets.md docs/ConfigApplicationPython.md -docs/ConfigApplicationPythonAllOf.md docs/ConfigApplicationPythonAllOfPath.md docs/ConfigApplicationPythonAllOfTargets.md docs/ConfigApplicationRuby.md -docs/ConfigApplicationRubyAllOf.md +docs/ConfigApplicationWasi.md +docs/ConfigApplicationWasm.md +docs/ConfigApplicationWasmAllOfAccess.md docs/ConfigListener.md docs/ConfigListenerForwarded.md docs/ConfigListenerForwardedSource.md @@ -114,21 +111,18 @@ src/models/config_application_common_limits.rs src/models/config_application_common_processes.rs src/models/config_application_common_processes_any_of.rs src/models/config_application_external.rs -src/models/config_application_external_all_of.rs src/models/config_application_java.rs -src/models/config_application_java_all_of.rs src/models/config_application_perl.rs -src/models/config_application_perl_all_of.rs src/models/config_application_php.rs -src/models/config_application_php_all_of.rs src/models/config_application_php_all_of_options.rs src/models/config_application_php_all_of_targets.rs src/models/config_application_python.rs -src/models/config_application_python_all_of.rs src/models/config_application_python_all_of_path.rs src/models/config_application_python_all_of_targets.rs src/models/config_application_ruby.rs -src/models/config_application_ruby_all_of.rs +src/models/config_application_wasi.rs +src/models/config_application_wasm.rs +src/models/config_application_wasm_all_of_access.rs src/models/config_listener.rs src/models/config_listener_forwarded.rs src/models/config_listener_forwarded_source.rs diff --git a/tools/unitctl/unit-openapi/.openapi-generator/VERSION b/tools/unitctl/unit-openapi/.openapi-generator/VERSION index cd802a1e..93c8ddab 100644 --- a/tools/unitctl/unit-openapi/.openapi-generator/VERSION +++ b/tools/unitctl/unit-openapi/.openapi-generator/VERSION @@ -1 +1 @@ -6.6.0 \ No newline at end of file +7.6.0 diff --git a/tools/unitctl/unit-openapi/README.md b/tools/unitctl/unit-openapi/README.md index 05aba6d9..5bad3fa4 100644 --- a/tools/unitctl/unit-openapi/README.md +++ b/tools/unitctl/unit-openapi/README.md @@ -22,6 +22,7 @@ This API client was generated by the [OpenAPI Generator](https://openapi-generat - API version: 0.2.0 - Package version: 0.4.0-beta +- Generator version: 7.6.0 - Build package: `org.openapitools.codegen.languages.RustClientCodegen` ## Installation @@ -354,21 +355,18 @@ Class | Method | HTTP request | Description - [ConfigApplicationCommonProcesses](docs/ConfigApplicationCommonProcesses.md) - [ConfigApplicationCommonProcessesAnyOf](docs/ConfigApplicationCommonProcessesAnyOf.md) - [ConfigApplicationExternal](docs/ConfigApplicationExternal.md) - - [ConfigApplicationExternalAllOf](docs/ConfigApplicationExternalAllOf.md) - [ConfigApplicationJava](docs/ConfigApplicationJava.md) - - [ConfigApplicationJavaAllOf](docs/ConfigApplicationJavaAllOf.md) - [ConfigApplicationPerl](docs/ConfigApplicationPerl.md) - - [ConfigApplicationPerlAllOf](docs/ConfigApplicationPerlAllOf.md) - [ConfigApplicationPhp](docs/ConfigApplicationPhp.md) - - [ConfigApplicationPhpAllOf](docs/ConfigApplicationPhpAllOf.md) - [ConfigApplicationPhpAllOfOptions](docs/ConfigApplicationPhpAllOfOptions.md) - [ConfigApplicationPhpAllOfTargets](docs/ConfigApplicationPhpAllOfTargets.md) - [ConfigApplicationPython](docs/ConfigApplicationPython.md) - - [ConfigApplicationPythonAllOf](docs/ConfigApplicationPythonAllOf.md) - [ConfigApplicationPythonAllOfPath](docs/ConfigApplicationPythonAllOfPath.md) - [ConfigApplicationPythonAllOfTargets](docs/ConfigApplicationPythonAllOfTargets.md) - [ConfigApplicationRuby](docs/ConfigApplicationRuby.md) - - [ConfigApplicationRubyAllOf](docs/ConfigApplicationRubyAllOf.md) + - [ConfigApplicationWasi](docs/ConfigApplicationWasi.md) + - [ConfigApplicationWasm](docs/ConfigApplicationWasm.md) + - [ConfigApplicationWasmAllOfAccess](docs/ConfigApplicationWasmAllOfAccess.md) - [ConfigListener](docs/ConfigListener.md) - [ConfigListenerForwarded](docs/ConfigListenerForwarded.md) - [ConfigListenerForwardedSource](docs/ConfigListenerForwardedSource.md) diff --git a/tools/unitctl/unit-openapi/src/lib.rs b/tools/unitctl/unit-openapi/src/lib.rs index a71f18d6..5435cfdb 100644 --- a/tools/unitctl/unit-openapi/src/lib.rs +++ b/tools/unitctl/unit-openapi/src/lib.rs @@ -1,6 +1,6 @@ #![allow(clippy::all)] -#[macro_use] -extern crate serde_derive; +#![allow(unused_imports)] +#![allow(clippy::too_many_arguments)] extern crate futures; extern crate hyper; diff --git a/tools/unitctl/unitctl/src/cmd/applications.rs b/tools/unitctl/unitctl/src/cmd/applications.rs new file mode 100644 index 00000000..f4c44105 --- /dev/null +++ b/tools/unitctl/unitctl/src/cmd/applications.rs @@ -0,0 +1,36 @@ +use crate::unitctl::{ApplicationArgs, ApplicationCommands, UnitCtl}; +use crate::{wait, UnitctlError}; +use crate::requests::send_empty_body_deserialize_response; +use unit_client_rs::unit_client::UnitClient; + +pub(crate) async fn cmd(cli: &UnitCtl, args: &ApplicationArgs) -> Result<(), UnitctlError> { + let control_socket = wait::wait_for_socket(cli).await?; + let client = UnitClient::new(control_socket); + + match &args.command { + ApplicationCommands::Reload { ref name } => client + .restart_application(name) + .await + .map_err(|e| UnitctlError::UnitClientError { source: *e }) + .and_then(|r| args.output_format.write_to_stdout(&r)), + + /* we should be able to use this but the openapi generator library + * is fundamentally incorrect and provides a broken API for the + * applications endpoint. + ApplicationCommands::List {} => client + .applications() + .await + .map_err(|e| UnitctlError::UnitClientError { source: *e }) + .and_then(|response| args.output_format.write_to_stdout(&response)),*/ + + ApplicationCommands::List {} => { + args.output_format.write_to_stdout( + &send_empty_body_deserialize_response( + &client, + "GET", + "/config/applications", + ).await? + ) + }, + } +} diff --git a/tools/unitctl/unitctl/src/cmd/instances.rs b/tools/unitctl/unitctl/src/cmd/instances.rs index ee58f697..e532a151 100644 --- a/tools/unitctl/unitctl/src/cmd/instances.rs +++ b/tools/unitctl/unitctl/src/cmd/instances.rs @@ -1,11 +1,11 @@ use crate::unitctl::{InstanceArgs, InstanceCommands}; -use crate::{OutputFormat, UnitctlError}; use crate::unitctl_error::ControlSocketErrorKind; +use crate::{OutputFormat, UnitctlError}; use std::path::PathBuf; +use unit_client_rs::control_socket_address::ControlSocket; use unit_client_rs::unitd_docker::deploy_new_container; use unit_client_rs::unitd_instance::UnitdInstance; -use unit_client_rs::control_socket_address::ControlSocket; pub(crate) async fn cmd(args: InstanceArgs) -> Result<(), UnitctlError> { if let Some(cmd) = args.command { @@ -22,37 +22,38 @@ pub(crate) async fn cmd(args: InstanceArgs) -> Result<(), UnitctlError> { } else if !PathBuf::from(application).as_path().exists() { eprintln!("application path must exist"); Err(UnitctlError::NoFilesImported) - } else { let addr = ControlSocket::parse_address(socket); if let Err(e) = addr { - return Err(UnitctlError::UnitClientError{source: e}); + return Err(UnitctlError::UnitClientError { source: e }); } // validate we arent processing an abstract socket if let ControlSocket::UnixLocalAbstractSocket(_) = addr.as_ref().unwrap() { - return Err(UnitctlError::ControlSocketError{ + return Err(UnitctlError::ControlSocketError { kind: ControlSocketErrorKind::General, message: "cannot pass abstract socket to docker container".to_string(), - }) + }); } // warn user of OSX docker limitations if let ControlSocket::UnixLocalSocket(ref sock_path) = addr.as_ref().unwrap() { if cfg!(target_os = "macos") { - return Err(UnitctlError::ControlSocketError{ + return Err(UnitctlError::ControlSocketError { kind: ControlSocketErrorKind::General, - message: format!("Docker on OSX will break unix sockets mounted {} {}", - "in containers, see the following link for more information", - "https://github.com/docker/for-mac/issues/483"), - }) + message: format!( + "Docker on macOS will break unix domain sockets mounted {} {}", + "in containers, see the following link for more information", + "https://github.com/docker/for-mac/issues/483" + ), + }); } if !sock_path.is_dir() { - return Err(UnitctlError::ControlSocketError{ + return Err(UnitctlError::ControlSocketError { kind: ControlSocketErrorKind::General, message: "user must specify a directory of UNIX socket directory".to_string(), - }) + }); } } @@ -60,28 +61,30 @@ pub(crate) async fn cmd(args: InstanceArgs) -> Result<(), UnitctlError> { if let ControlSocket::TcpSocket(uri) = addr.as_ref().unwrap() { if let Some(host) = uri.host() { if host != "127.0.0.1" { - return Err(UnitctlError::ControlSocketError{ + return Err(UnitctlError::ControlSocketError { kind: ControlSocketErrorKind::General, message: "TCP URI must point to 127.0.0.1".to_string(), - }) + }); } } else { - return Err(UnitctlError::ControlSocketError{ + return Err(UnitctlError::ControlSocketError { kind: ControlSocketErrorKind::General, message: "TCP URI must point to a host".to_string(), - }) + }); } if let Some(port) = uri.port_u16() { if port < 1025 { - eprintln!("warning! you are asking docker to forward a privileged port. {}", - "please make sure docker has access to it"); + eprintln!( + "warning! you are asking docker to forward a privileged port. {}", + "please make sure docker has access to it" + ); } } else { - return Err(UnitctlError::ControlSocketError{ + return Err(UnitctlError::ControlSocketError { kind: ControlSocketErrorKind::General, message: "TCP URI must specify a port".to_string(), - }) + }); } if uri.path() != "/" { @@ -95,11 +98,13 @@ pub(crate) async fn cmd(args: InstanceArgs) -> Result<(), UnitctlError> { eprintln!("> Will READ ONLY mount {} to /www for application access", application); eprintln!("> Container will be on host network"); match addr.as_ref().unwrap() { - ControlSocket::UnixLocalSocket(path) => - eprintln!("> Will mount directory containing {} to /var/www for control API", - path.as_path().to_string_lossy()), - ControlSocket::TcpSocket(uri) => - eprintln!("> Will forward port {} for control API", uri.port_u16().unwrap()), + ControlSocket::UnixLocalSocket(path) => eprintln!( + "> Will mount directory containing {} to /var/www for control API", + path.as_path().to_string_lossy() + ), + ControlSocket::TcpSocket(uri) => { + eprintln!("> Will forward port {} for control API", uri.port_u16().unwrap()) + } _ => unimplemented!(), // abstract socket case ruled out previously } @@ -108,15 +113,17 @@ pub(crate) async fn cmd(args: InstanceArgs) -> Result<(), UnitctlError> { } // do the actual deployment - deploy_new_container(addr.unwrap(), application, image).await.map_or_else( - |e| Err(UnitctlError::UnitClientError { source: e }), - |warn| { - for i in warn { - eprintln!("warning! from docker: {}", i); - } - Ok(()) - }, - ) + deploy_new_container(addr.unwrap(), application, image) + .await + .map_or_else( + |e| Err(UnitctlError::UnitClientError { source: e }), + |warn| { + for i in warn { + eprintln!("warning! from docker: {}", i); + } + Ok(()) + }, + ) } } } diff --git a/tools/unitctl/unitctl/src/cmd/mod.rs b/tools/unitctl/unitctl/src/cmd/mod.rs index 989a0109..07c50912 100644 --- a/tools/unitctl/unitctl/src/cmd/mod.rs +++ b/tools/unitctl/unitctl/src/cmd/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod applications; pub(crate) mod edit; pub(crate) mod execute; pub(crate) mod import; diff --git a/tools/unitctl/unitctl/src/main.rs b/tools/unitctl/unitctl/src/main.rs index 12322873..6c9faaf7 100644 --- a/tools/unitctl/unitctl/src/main.rs +++ b/tools/unitctl/unitctl/src/main.rs @@ -8,7 +8,7 @@ extern crate unit_client_rs; use clap::Parser; -use crate::cmd::{edit, execute as execute_cmd, import, instances, listeners, status}; +use crate::cmd::{applications, edit, execute as execute_cmd, import, instances, listeners, status}; use crate::output_format::OutputFormat; use crate::unitctl::{Commands, UnitCtl}; use crate::unitctl_error::UnitctlError; @@ -30,6 +30,8 @@ async fn main() -> Result<(), UnitctlError> { match cli.command { Commands::Instances(args) => instances::cmd(args).await, + Commands::App(ref args) => applications::cmd(&cli, args).await, + Commands::Edit { output_format } => edit::cmd(&cli, output_format).await, Commands::Import { ref directory } => import::cmd(&cli, directory).await, @@ -67,6 +69,9 @@ fn eprint_error(error: &UnitctlError) { eprintln!("{}", source); eprintln!("Try running again with the same permissions as the unit control socket"); } + UnitClientError::OpenAPIError { source } => { + eprintln!("OpenAPI Error: {}", source); + } _ => { eprintln!("Unit client error: {}", source); } diff --git a/tools/unitctl/unitctl/src/unitctl.rs b/tools/unitctl/unitctl/src/unitctl.rs index b1bdbbd1..47f33820 100644 --- a/tools/unitctl/unitctl/src/unitctl.rs +++ b/tools/unitctl/unitctl/src/unitctl.rs @@ -41,9 +41,9 @@ pub(crate) struct UnitCtl { #[derive(Debug, Subcommand)] pub(crate) enum Commands { - #[command(about = "List all running UNIT processes")] + #[command(about = "List all running Unit processes")] Instances(InstanceArgs), - #[command(about = "Open current UNIT configuration in editor")] + #[command(about = "Open current Unit configuration in editor")] Edit { #[arg( required = false, @@ -60,7 +60,7 @@ pub(crate) enum Commands { #[arg(required = true, help = "Directory to import from")] directory: PathBuf, }, - #[command(about = "Sends raw JSON payload to UNIT")] + #[command(about = "Sends raw JSON payload to Unit")] Execute { #[arg( required = false, @@ -90,7 +90,7 @@ pub(crate) enum Commands { #[arg(required = true, short = 'p', long = "path")] path: String, }, - #[command(about = "Get the current status of UNIT")] + #[command(about = "Get the current status of Unit")] Status { #[arg( required = false, @@ -114,6 +114,8 @@ pub(crate) enum Commands { )] output_format: OutputFormat, }, + #[command(about = "List all configured Unit applications")] + App(ApplicationArgs), } #[derive(Debug, Args)] @@ -135,7 +137,7 @@ pub struct InstanceArgs { #[derive(Debug, Subcommand)] #[command(args_conflicts_with_subcommands = true)] pub enum InstanceCommands { - #[command(about = "deploy a new docker instance of unitd")] + #[command(about = "deploy a new docker instance of Unit")] New { #[arg(required = true, help = "Path to mount control socket to host")] socket: String, @@ -151,6 +153,35 @@ pub enum InstanceCommands { }, } +#[derive(Debug, Args)] +pub struct ApplicationArgs { + #[arg( + required = false, + global = true, + short = 't', + long = "output-format", + default_value = "text", + help = "Output format: text, yaml, json, json-pretty (default)" + )] + pub output_format: OutputFormat, + + #[command(subcommand)] + pub command: ApplicationCommands, +} + +#[derive(Debug, Subcommand)] +#[command(args_conflicts_with_subcommands = true)] +pub enum ApplicationCommands { + #[command(about = "reload a running application")] + Reload { + #[arg(required = true, help = "name of application")] + name: String, + }, + + #[command(about = "list running applications")] + List {}, +} + fn parse_control_socket_address(s: &str) -> Result { ControlSocket::try_from(s).map_err(|e| ClapError::raw(ValueValidation, e.to_string())) }