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 <a.hahn@f5.com>
This commit is contained in:
Ava Hahn 2024-06-14 21:04:15 -07:00 committed by Ava Hahn
parent d96d583328
commit e0c15ae457
15 changed files with 335 additions and 137 deletions

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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};

View file

@ -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<dyn ApplicationsApi + 'static> {
new_openapi_client!(self, ApplicationsApiClient, ApplicationsApi)
}
pub async fn applications(&self) -> Result<HashMap<String, ConfigApplication>, Box<UnitClientError>> {
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<dyn AppsApi + 'static> {
new_openapi_client!(self, AppsApiClient, AppsApi)
}
pub async fn restart_application(&self, name: &String) -> Result<HashMap<String, String>, Box<UnitClientError>> {
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()
}

View file

@ -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!("<container>:{}", 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::<StartContainerOptions<String>>,
).await {
match docker
.start_container(
info[0].names.clone().unwrap()[0].strip_prefix(MAIN_SEPARATOR).unwrap(),
None::<StartContainerOptions<String>>,
)
.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()));
}
}

View file

@ -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

View file

@ -1 +1 @@
6.6.0
7.6.0

View file

@ -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)

View file

@ -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;

View file

@ -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?
)
},
}
}

View file

@ -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(())
},
)
}
}
}

View file

@ -1,3 +1,4 @@
pub(crate) mod applications;
pub(crate) mod edit;
pub(crate) mod execute;
pub(crate) mod import;

View file

@ -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);
}

View file

@ -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, ClapError> {
ControlSocket::try_from(s).map_err(|e| ClapError::raw(ValueValidation, e.to_string()))
}