summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAva Hahn <a.hahn@f5.com>2024-04-22 13:26:34 +0100
committeravahahn <110854134+avahahn@users.noreply.github.com>2024-04-30 09:14:55 -0700
commitdb3cf3e42d93112278f5e86cca2c886627ef48b2 (patch)
tree3c546922404965b3fc7773d319388b9241f9896b
parentb26c119f4e535f617c9ffb10076f15c4ee54414d (diff)
downloadunit-db3cf3e42d93112278f5e86cca2c886627ef48b2.tar.gz
unit-db3cf3e42d93112278f5e86cca2c886627ef48b2.tar.bz2
tools: Add unitctl CLI
* Pull in entire unit-rust-sdk project * not included: CLA, COC, License * not included: duplicate openapi spec * not included: CI workflows * not included: changelog tooling * not included: commitsar tooling * not included: OpenAPI Web UI feature * update links in unitctl manpage * remove IDE configuration from .gitignore * rename Containerfile.debian to Dockerfile * simplify call to uname * keep Readmes and Makefiles to 80 character lines * outline specifically how to build unitctl for any desired target, and where to then find the binary for use * remove a section on the vision of the CLI which was superfluous given the state of completeness of the code and its use in unit * remove out of date feature proposals from readme * makefile: do not run when Rustup is not present * bump mio version to latest * generate openapi client library on demand * generate-openapi only runs when not present * generate-openapi now a dependency of binary build targets * deleted autogenerated code * reverted readme and Cargo document to autogenerated state * add additional build requirement to Readme Co-developed-by: Elijah Zupancic <e.zupancic@f5.com> Signed-off-by: Elijah Zupancic <e.zupancic@f5.com> Signed-off-by: Ava Hahn <a.hahn@f5.com> Reviewed-by: Andrew Clayton <a.clayton@nginx.com> # non rust stuff [ tools/cli => tools/unitctl and subject tweak - Andrew ] Signed-off-by: Andrew Clayton <a.clayton@nginx.com>
-rw-r--r--tools/unitctl/.cargo/config.toml2
-rw-r--r--tools/unitctl/.gitignore16
-rw-r--r--tools/unitctl/Cargo.lock1998
-rw-r--r--tools/unitctl/Cargo.toml8
-rw-r--r--tools/unitctl/Dockerfile37
-rw-r--r--tools/unitctl/GNUmakefile145
l---------tools/unitctl/HomebrewFormula1
-rw-r--r--tools/unitctl/README.md134
-rw-r--r--tools/unitctl/build/container.mk67
-rw-r--r--tools/unitctl/build/github.mk22
-rwxr-xr-xtools/unitctl/build/openapi-generator-cli.sh77
-rw-r--r--tools/unitctl/build/package.mk139
-rw-r--r--tools/unitctl/build/release.mk57
-rw-r--r--tools/unitctl/man/unitctl.127
-rw-r--r--tools/unitctl/openapi-config.json6
-rw-r--r--tools/unitctl/pkg/brew/unitctl.rb29
-rw-r--r--tools/unitctl/pkg/brew/unitctl.rb.template29
-rw-r--r--tools/unitctl/rustfmt.toml1
-rw-r--r--tools/unitctl/unit-client-rs/Cargo.toml32
-rw-r--r--tools/unitctl/unit-client-rs/src/control_socket_address.rs571
-rw-r--r--tools/unitctl/unit-client-rs/src/lib.rs15
-rw-r--r--tools/unitctl/unit-client-rs/src/runtime_flags.rs90
-rw-r--r--tools/unitctl/unit-client-rs/src/unit_client.rs393
-rw-r--r--tools/unitctl/unit-client-rs/src/unitd_cmd.rs85
-rw-r--r--tools/unitctl/unit-client-rs/src/unitd_configure_options.rs235
-rw-r--r--tools/unitctl/unit-client-rs/src/unitd_instance.rs360
-rw-r--r--tools/unitctl/unit-client-rs/src/unitd_process.rs170
-rw-r--r--tools/unitctl/unit-client-rs/src/unitd_process_user.rs36
-rw-r--r--tools/unitctl/unit-openapi/.gitignore3
-rw-r--r--tools/unitctl/unit-openapi/.openapi-generator-ignore27
-rw-r--r--tools/unitctl/unit-openapi/.openapi-generator/FILES161
-rw-r--r--tools/unitctl/unit-openapi/.openapi-generator/VERSION1
-rw-r--r--tools/unitctl/unit-openapi/Cargo.toml17
-rw-r--r--tools/unitctl/unit-openapi/README.md411
-rw-r--r--tools/unitctl/unit-openapi/openapi-templates/Cargo.mustache65
-rw-r--r--tools/unitctl/unit-openapi/openapi-templates/request.rs248
-rw-r--r--tools/unitctl/unit-openapi/src/apis/error.rs18
-rw-r--r--tools/unitctl/unit-openapi/src/lib.rs12
-rw-r--r--tools/unitctl/unitctl/Cargo.toml56
-rw-r--r--tools/unitctl/unitctl/src/cmd/edit.rs105
-rw-r--r--tools/unitctl/unitctl/src/cmd/execute.rs68
-rw-r--r--tools/unitctl/unitctl/src/cmd/import.rs124
-rw-r--r--tools/unitctl/unitctl/src/cmd/instances.rs16
-rw-r--r--tools/unitctl/unitctl/src/cmd/listeners.rs13
-rw-r--r--tools/unitctl/unitctl/src/cmd/mod.rs6
-rw-r--r--tools/unitctl/unitctl/src/cmd/status.rs13
-rw-r--r--tools/unitctl/unitctl/src/inputfile.rs289
-rw-r--r--tools/unitctl/unitctl/src/known_size.rs77
-rw-r--r--tools/unitctl/unitctl/src/main.rs101
-rw-r--r--tools/unitctl/unitctl/src/output_format.rs43
-rw-r--r--tools/unitctl/unitctl/src/requests.rs175
-rw-r--r--tools/unitctl/unitctl/src/unitctl.rs144
-rw-r--r--tools/unitctl/unitctl/src/unitctl_error.rs72
-rw-r--r--tools/unitctl/unitctl/src/wait.rs165
54 files changed, 7212 insertions, 0 deletions
diff --git a/tools/unitctl/.cargo/config.toml b/tools/unitctl/.cargo/config.toml
new file mode 100644
index 00000000..ff7f7580
--- /dev/null
+++ b/tools/unitctl/.cargo/config.toml
@@ -0,0 +1,2 @@
+[target.aarch64-unknown-linux-gnu]
+linker = "aarch64-linux-gnu-gcc" \ No newline at end of file
diff --git a/tools/unitctl/.gitignore b/tools/unitctl/.gitignore
new file mode 100644
index 00000000..2319f815
--- /dev/null
+++ b/tools/unitctl/.gitignore
@@ -0,0 +1,16 @@
+# Generated by Cargo
+# will have compiled files and executables
+debug/
+target/
+
+# These are backup files generated by rustfmt
+**/*.rs.bk
+
+# Ignore OpenAPI cache files
+.openapi_cache
+# Ignore generated OpenAPI documentation
+unit-openapi/docs
+# Ignore autogenerated OpenAPI code
+unit-openapi/src
+
+config \ No newline at end of file
diff --git a/tools/unitctl/Cargo.lock b/tools/unitctl/Cargo.lock
new file mode 100644
index 00000000..2940b3ae
--- /dev/null
+++ b/tools/unitctl/Cargo.lock
@@ -0,0 +1,1998 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "addr2line"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
+dependencies = [
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "aws-lc-rs"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5509d663b2c00ee421bda8d6a24d6c42e15970957de1701b8df9f6fbe5707df1"
+dependencies = [
+ "aws-lc-sys",
+ "mirai-annotations",
+ "paste",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-lc-sys"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d5d317212c2a78d86ba6622e969413c38847b62f48111f8b763af3dac2f9840"
+dependencies = [
+ "bindgen",
+ "cc",
+ "cmake",
+ "dunce",
+ "fs_extra",
+ "libc",
+ "paste",
+]
+
+[[package]]
+name = "backtrace"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "base64"
+version = "0.21.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
+
+[[package]]
+name = "bindgen"
+version = "0.69.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0"
+dependencies = [
+ "bitflags 2.4.1",
+ "cexpr",
+ "clang-sys",
+ "itertools",
+ "lazy_static",
+ "lazycell",
+ "log",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn 2.0.60",
+ "which 4.4.2",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bytes"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
+
+[[package]]
+name = "cc"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b"
+dependencies = [
+ "jobserver",
+ "libc",
+ "once_cell",
+]
+
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clang-sys"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "clap"
+version = "4.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52bdc885e4cacc7f7c9eedc1ef6da641603180c783c41a15c264944deeaab642"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.60",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
+
+[[package]]
+name = "cmake"
+version = "0.1.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "colorchoice"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+
+[[package]]
+name = "colored_json"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79cff32df5cfea75e6484eeff0b4e48ad3977fb6582676a7862b3590dddc7a87"
+dependencies = [
+ "serde",
+ "serde_json",
+ "yansi",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc"
+dependencies = [
+ "cfg-if",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f916dfc5d356b0ed9dae65f1db9fc9770aa2851d2662b988ccf4fe3516e86348"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "crossbeam-utils",
+ "memoffset",
+ "scopeguard",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "custom_error"
+version = "1.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f8a51dd197fa6ba5b4dc98a990a43cc13693c23eb0089ebb0fcc1f04152bca6"
+
+[[package]]
+name = "digest"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "dunce"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b"
+
+[[package]]
+name = "either"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
+
+[[package]]
+name = "errno"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "fs_extra"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+
+[[package]]
+name = "futures"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.60",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
+
+[[package]]
+name = "futures-task"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
+
+[[package]]
+name = "futures-util"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gimli"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
+
+[[package]]
+name = "glob"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "heck"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "home"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
+dependencies = [
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "http"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
+dependencies = [
+ "bytes",
+ "http",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
+
+[[package]]
+name = "httpdate"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
+
+[[package]]
+name = "hyper"
+version = "0.14.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2 0.4.7",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes",
+ "hyper",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
+
+[[package]]
+name = "hyperlocal"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fafdf7b2b2de7c9784f76e02c0935e65a8117ec3b768644379983ab333ac98c"
+dependencies = [
+ "futures-util",
+ "hex",
+ "hyper",
+ "pin-project",
+ "tokio",
+]
+
+[[package]]
+name = "idna"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "itertools"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
+
+[[package]]
+name = "jobserver"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "json5"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
+dependencies = [
+ "pest",
+ "pest_derive",
+ "serde",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "lazycell"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+
+[[package]]
+name = "libc"
+version = "0.2.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
+
+[[package]]
+name = "libloading"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
+dependencies = [
+ "cfg-if",
+ "windows-targets 0.52.0",
+]
+
+[[package]]
+name = "linked-hash-map"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829"
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "memchr"
+version = "2.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
+
+[[package]]
+name = "memoffset"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "mirai-annotations"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1"
+
+[[package]]
+name = "native-tls"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "ntapi"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "nu-json"
+version = "0.89.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "563eff2fa513ceee37a147701a75e259b4514b31b0bac3496f16297851946caf"
+dependencies = [
+ "linked-hash-map",
+ "num-traits",
+ "serde",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "object"
+version = "0.32.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+
+[[package]]
+name = "openssl"
+version = "0.10.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
+dependencies = [
+ "bitflags 2.4.1",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.60",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
+
+[[package]]
+name = "percent-encoding"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
+
+[[package]]
+name = "pest"
+version = "2.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f6e86fb9e7026527a0d46bc308b841d73170ef8f443e1807f6ef88526a816d4"
+dependencies = [
+ "thiserror",
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96504449aa860c8dcde14f9fba5c58dc6658688ca1fe363589d6327b8662c603"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "798e0220d1111ae63d66cb66a5dcb3fc2d986d520b98e49e1852bfdb11d7c5e7"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.103",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "984298b75898e30a843e278a9f2452c31e349a073a0ce6fd950a12a74464e065"
+dependencies = [
+ "once_cell",
+ "pest",
+ "sha1",
+]
+
+[[package]]
+name = "pin-project"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.103",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "prettyplease"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ac2cf0f2e4f42b49f5ffd07dae8d746508ef7526c13940e5f524012ae6c6550"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.60",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rayon"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "regex"
+version = "1.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
+
+[[package]]
+name = "ring"
+version = "0.17.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom",
+ "libc",
+ "spin",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "rustix"
+version = "0.38.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e"
+dependencies = [
+ "bitflags 2.4.1",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afabcee0551bd1aa3e18e5adbf2c0544722014b899adb31bd186ec638d3da97e"
+dependencies = [
+ "aws-lc-rs",
+ "log",
+ "once_cell",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35e4980fa29e4c4b212ffb3db068a564cbf560e51d3944b7c88bd8bf5bec64f4"
+dependencies = [
+ "base64",
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54"
+
+[[package]]
+name = "rustls-webpki"
+version = "0.102.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf"
+dependencies = [
+ "aws-lc-rs",
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
+dependencies = [
+ "lazy_static",
+ "windows-sys 0.36.1",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "security-framework"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.103",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.87"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45"
+dependencies = [
+ "indexmap",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_yaml"
+version = "0.9.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d232d893b10de3eb7258ff01974d6ee20663d8e833263c99409d4b13a0209da"
+dependencies = [
+ "indexmap",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "slab"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "socket2"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "socket2"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
+dependencies = [
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "subtle"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
+
+[[package]]
+name = "syn"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sysinfo"
+version = "0.30.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fb4f3438c8f6389c864e61221cbc97e9bca98b4daf39a5beb7bea660f528bb2"
+dependencies = [
+ "cfg-if",
+ "core-foundation-sys",
+ "libc",
+ "ntapi",
+ "once_cell",
+ "rayon",
+ "windows",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "redox_syscall",
+ "rustix",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.103",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
+[[package]]
+name = "tokio"
+version = "1.35.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104"
+dependencies = [
+ "backtrace",
+ "libc",
+ "mio",
+ "num_cpus",
+ "pin-project-lite",
+ "socket2 0.5.5",
+ "tokio-macros",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.60",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
+[[package]]
+name = "tracing"
+version = "0.1.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
+dependencies = [
+ "cfg-if",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
+
+[[package]]
+name = "typenum"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unit-client-rs"
+version = "0.4.0-beta"
+dependencies = [
+ "custom_error",
+ "futures",
+ "hex",
+ "hyper",
+ "hyper-tls",
+ "hyperlocal",
+ "rand",
+ "rustls",
+ "serde",
+ "serde_json",
+ "sysinfo",
+ "tokio",
+ "unit-openapi",
+ "which 5.0.0",
+]
+
+[[package]]
+name = "unit-openapi"
+version = "0.4.0-beta"
+dependencies = [
+ "base64",
+ "futures",
+ "http",
+ "hyper",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "url",
+]
+
+[[package]]
+name = "unitctl"
+version = "0.4.0-beta"
+dependencies = [
+ "clap",
+ "colored_json",
+ "custom_error",
+ "futures",
+ "hyper",
+ "hyper-tls",
+ "hyperlocal",
+ "json5",
+ "nu-json",
+ "rustls-pemfile",
+ "serde",
+ "serde_json",
+ "serde_yaml",
+ "tempfile",
+ "tokio",
+ "unit-client-rs",
+ "walkdir",
+ "which 5.0.0",
+]
+
+[[package]]
+name = "unsafe-libyaml"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "walkdir"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
+dependencies = [
+ "log",
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "which"
+version = "4.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
+dependencies = [
+ "either",
+ "home",
+ "once_cell",
+ "rustix",
+]
+
+[[package]]
+name = "which"
+version = "5.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bf3ea8596f3a0dd5980b46430f2058dfe2c36a27ccfbb1845d6fbfcd9ba6e14"
+dependencies = [
+ "either",
+ "home",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
+dependencies = [
+ "windows-core",
+ "windows-targets 0.52.0",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+dependencies = [
+ "windows-targets 0.52.0",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
+dependencies = [
+ "windows_aarch64_msvc 0.36.1",
+ "windows_i686_gnu 0.36.1",
+ "windows_i686_msvc 0.36.1",
+ "windows_x86_64_gnu 0.36.1",
+ "windows_x86_64_msvc 0.36.1",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.0",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.0",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.0",
+ "windows_aarch64_msvc 0.48.0",
+ "windows_i686_gnu 0.48.0",
+ "windows_i686_msvc 0.48.0",
+ "windows_x86_64_gnu 0.48.0",
+ "windows_x86_64_gnullvm 0.48.0",
+ "windows_x86_64_msvc 0.48.0",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.0",
+ "windows_aarch64_msvc 0.52.0",
+ "windows_i686_gnu 0.52.0",
+ "windows_i686_msvc 0.52.0",
+ "windows_x86_64_gnu 0.52.0",
+ "windows_x86_64_gnullvm 0.52.0",
+ "windows_x86_64_msvc 0.52.0",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
+
+[[package]]
+name = "yansi"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
+
+[[package]]
+name = "zeroize"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
diff --git a/tools/unitctl/Cargo.toml b/tools/unitctl/Cargo.toml
new file mode 100644
index 00000000..c9c6a272
--- /dev/null
+++ b/tools/unitctl/Cargo.toml
@@ -0,0 +1,8 @@
+[workspace]
+resolver = "2"
+
+members = [
+ "unit-openapi",
+ "unit-client-rs",
+ "unitctl"
+] \ No newline at end of file
diff --git a/tools/unitctl/Dockerfile b/tools/unitctl/Dockerfile
new file mode 100644
index 00000000..812ce28c
--- /dev/null
+++ b/tools/unitctl/Dockerfile
@@ -0,0 +1,37 @@
+FROM rust:slim-bullseye
+
+ADD https://unit.nginx.org/keys/nginx-keyring.gpg \
+ /usr/share/keyrings/nginx-keyring.gpg
+
+RUN set -eux \
+ export DEBIAN_FRONTEND=noninteractive; \
+ echo 'fc27fd284cceb4bf6c8ac2118dbb5e834590836f8d6ba3944da0e0451cbadeca /usr/share/keyrings/nginx-keyring.gpg' |\
+ sha256sum --check -; \
+ chmod 0644 /usr/share/keyrings/nginx-keyring.gpg; \
+ echo "deb [signed-by=/usr/share/keyrings/nginx-keyring.gpg] https://packages.nginx.org/unit/debian/ bullseye unit" > /etc/apt/sources.list.d/unit.list; \
+ apt-get -qq update; \
+ apt-get -qq upgrade --yes; \
+ apt-get -qq install --yes --no-install-recommends --no-install-suggests \
+ bsdmainutils \
+ ca-certificates \
+ git \
+ gzip \
+ grep \
+ gawk \
+ sed \
+ make \
+ rpm \
+ pkg-config \
+ libssl-dev \
+ dpkg-dev \
+ musl-dev \
+ musl-tools \
+ unit \
+ gcc-aarch64-linux-gnu \
+ libc6-dev-arm64-cross \
+ gcc-x86-64-linux-gnu \
+ libc6-dev-amd64-cross; \
+ rustup target install x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu x86_64-unknown-linux-musl; \
+ cargo install --quiet cargo-deb cargo-generate-rpm; \
+ rm -rf /var/lib/apt/lists/* /var/tmp/* /tmp/*; \
+ git config --global --add safe.directory /project
diff --git a/tools/unitctl/GNUmakefile b/tools/unitctl/GNUmakefile
new file mode 100644
index 00000000..e7cb379a
--- /dev/null
+++ b/tools/unitctl/GNUmakefile
@@ -0,0 +1,145 @@
+MAKE_MAJOR_VER := $(shell echo $(MAKE_VERSION) | cut -d'.' -f1)
+
+ifneq ($(shell test $(MAKE_MAJOR_VER) -gt 3; echo $$?),0)
+$(error Make version $(MAKE_VERSION) not supported, please install GNU Make 4.x)
+endif
+
+GREP ?= $(shell command -v ggrep 2> /dev/null || command -v grep 2> /dev/null)
+SED ?= $(shell command -v gsed 2> /dev/null || command -v sed 2> /dev/null)
+AWK ?= $(shell command -v gawk 2> /dev/null || command -v awk 2> /dev/null)
+RUSTUP ?= $(shell command -v rustup 2> /dev/null)
+ifeq ($(RUSTUP),)
+$(error Please install Rustup)
+endif
+
+RPM_ARCH := $(shell uname -m)
+VERSION ?= $(shell $(GREP) -Po '^version\s+=\s+"\K.*?(?=")' $(CURDIR)/unitctl/Cargo.toml)
+SRC_REPO := https://github.com/nginxinc/unit-rust-sdk
+DEFAULT_TARGET ?= $(shell $(RUSTUP) toolchain list | $(GREP) '(default)' | cut -d' ' -f1 | cut -d- -f2-)
+SHELL := /bin/bash
+OUTPUT_BINARY ?= unitctl
+PACKAGE_NAME ?= unitctl
+CARGO ?= cargo
+DOCKER ?= docker
+DOCKER_BUILD_FLAGS ?= --load
+CHECKSUM ?= sha256sum
+OPENAPI_GENERATOR_VERSION ?= 6.6.0
+
+# Define platform targets based off of the current host OS
+# If running MacOS, then build for MacOS platform targets installed in rustup
+# If running Linux, then build for Linux platform targets installed in rustup
+ifeq ($(shell uname -s),Darwin)
+ TARGETS := $(sort $(shell $(RUSTUP) target list | \
+ $(GREP) '(installed)' | \
+ $(GREP) 'apple' | \
+ cut -d' ' -f1))
+else ifeq ($(shell uname -s),Linux)
+ TARGETS := $(sort $(shell $(RUSTUP) target list | \
+ $(GREP) '(installed)' | \
+ $(GREP) 'linux' | \
+ cut -d' ' -f1))
+else
+ TARGETS := $(DEFAULT_TARGET)
+endif
+
+RELEASE_BUILD_FLAGS ?= --quiet --release --bin $(OUTPUT_BINARY)
+
+Q = $(if $(filter 1,$V),,@)
+M = $(shell printf "\033[34;1mâ–¶\033[0m")
+
+.PHONY: help
+help:
+ @$(GREP) --no-filename -E '^[ a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
+ $(AWK) 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-28s\033[0m %s\n", $$1, $$2}' | \
+ sort
+
+.PHONY: clean
+clean: ; $(info $(M) cleaning...)@ ## Cleanup everything
+ $Q rm -rf $(CURDIR)/target
+
+.PHONY: list-targets
+list-targets: ## List all available platform targets
+ $Q echo $(TARGETS) | $(SED) -e 's/ /\n/g'
+
+.PHONY: all
+all: $(TARGETS) ## Build all available platform targets [see: list-targets]
+
+.PHONY: $(TARGETS)
+.ONESHELL: $(TARGETS)
+$(TARGETS): openapi-generate
+ $Q if [ ! -f "$(CURDIR)/target/$(@)/release/$(OUTPUT_BINARY)" ]; then
+ echo "$(M) building $(OUTPUT_BINARY) with flags [$(RELEASE_BUILD_FLAGS) --target $(@)]"
+ $(CARGO) build $(RELEASE_BUILD_FLAGS) --target $@
+ fi
+
+target target/debug:
+ $Q mkdir -p $@
+
+.PHONY: debug
+debug: target/debug/$(OUTPUT_BINARY)
+
+target/debug/$(OUTPUT_BINARY): openapi-generate
+ $Q echo "$(M) building $(OUTPUT_BINARY) in debug mode for the current platform"
+ $Q $(CARGO) build --bin $(OUTPUT_BINARY)
+
+.PHONY: release
+release: target/release/$(OUTPUT_BINARY)
+
+target/release/$(OUTPUT_BINARY): openapi-generate
+ $Q echo "$(M) building $(OUTPUT_BINARY) in release mode for the current platform"
+ $Q $(CARGO) build $(RELEASE_BUILD_FLAGS)
+
+.PHONY: test
+test: ## Run tests
+ $Q $(CARGO) test
+
+.ONESHELL: target/man/$(OUTPUT_BINARY).1.gz
+target/man/$(OUTPUT_BINARY).1.gz:
+ $Q $(info $(M) building distributable manpage)
+ mkdir -p target/man
+ $(SED) 's/%%VERSION%%/$(VERSION)/' \
+ man/$(OUTPUT_BINARY).1 > $(CURDIR)/target/man/$(OUTPUT_BINARY).1
+ gzip $(CURDIR)/target/man/$(OUTPUT_BINARY).1
+
+target/gz:
+ $Q mkdir -p target/gz
+
+.PHONY: manpage
+manpage: target/man/$(OUTPUT_BINARY).1.gz ## Builds man page
+
+.openapi_cache:
+ $Q mkdir -p $@
+
+## Generate (or regenerate) UNIT API access code via a OpenAPI spec
+.PHONY: openapi-generate
+openapi-generate: .openapi_cache
+ $Q if [ ! -f "$(CURDIR)/unit-openapi/src/models/mod.rs" ]; then
+ echo "$(M) generating UNIT API access code via a OpenAPI spec"
+ OPENAPI_GENERATOR_VERSION="$(OPENAPI_GENERATOR_VERSION)" \
+ OPENAPI_GENERATOR_DOWNLOAD_CACHE_DIR="$(CURDIR)/.openapi_cache" \
+ $(CURDIR)/build/openapi-generator-cli.sh \
+ generate \
+ --input-spec "$(CURDIR)/../../docs/unit-openapi.yaml" \
+ --config "$(CURDIR)/openapi-config.json" \
+ --template-dir "$(CURDIR)/unit-openapi/openapi-templates" \
+ --output "$(CURDIR)/unit-openapi" \
+ --generator-name rust
+ echo "mod error;" >> "$(CURDIR)/unit-openapi/src/apis/mod.rs"
+ $(SED) -i '1i #![allow(clippy::all)]' "$(CURDIR)/unit-openapi/src/lib.rs"
+ $(CARGO) fmt
+ fi
+
+.PHONY: openapi-clean
+openapi-clean: ## Clean up generated OpenAPI files
+ $Q $(info $(M) cleaning up generated OpenAPI documentation)
+ $Q rm -rf "$(CURDIR)/unit-openapi/docs/*"
+ $Q $(info $(M) cleaning up generated OpenAPI api code)
+ $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/*"
+
+include $(CURDIR)/build/package.mk
+include $(CURDIR)/build/container.mk
+include $(CURDIR)/build/release.mk
+include $(CURDIR)/build/github.mk
diff --git a/tools/unitctl/HomebrewFormula b/tools/unitctl/HomebrewFormula
new file mode 120000
index 00000000..1ffaf042
--- /dev/null
+++ b/tools/unitctl/HomebrewFormula
@@ -0,0 +1 @@
+pkg/brew \ No newline at end of file
diff --git a/tools/unitctl/README.md b/tools/unitctl/README.md
new file mode 100644
index 00000000..7292cd86
--- /dev/null
+++ b/tools/unitctl/README.md
@@ -0,0 +1,134 @@
+# NGINX UNIT Rust SDK and CLI
+
+This project provides a Rust SDK interface to the
+[NGINX UNIT](https://unit.nginx.org/)
+[control API](https://unit.nginx.org/howto/source/#source-startup)
+and a CLI (`unitctl`) that exposes the functionality provided by the SDK.
+
+## Installation and Use
+In order to build and use `unitctl` one needs a working installation of Maven
+and Cargo. It is recommended to procure Cargo with Rustup. Rustup is packaged
+for use in many systems, but you can also find it at its
+[Official Site](https://rustup.rs/).
+
+With a working installation of Cargo it is advised to build unitctl with the
+provided makefile. The `list-targets` target will inform the user of what
+platforms are available to be built. One or more of these can then be run as
+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
+x86_64-unknown-linux-gnu
+[ava@calliope cli]$ 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
+./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,
+with debug_info, not stripped
+```
+
+As demonstrated in the example above, compiled binaries may be found in the
+targets folder, under the subdirectory corresponding to the build target
+desired.
+
+
+## Features (Current)
+
+### Consumes alternative configuration formats Like YAML and converts them
+### Syntactic highlighting of JSON output
+### Interpretation of UNIT errors with (arguably more) useful error messages
+
+### Lists all running UNIT processes and provides details about each process.
+```
+$ unitctl instances
+No socket path provided - attempting to detect from running instance
+unitd instance [pid: 79489, version: 1.32.0]:
+ Executable: /opt/unit/sbin/unitd
+ API control unix socket: unix:/opt/unit/control.unit.sock
+ Child processes ids: 79489, 79489
+ Runtime flags: --no-daemon
+ Configure options: --prefix=/opt/unit --user=elijah --group=elijah --openssl
+```
+
+### Lists active listeners from running UNIT processes
+```
+unitctl listeners
+No socket path provided - attempting to detect from running instance
+{
+ "127.0.0.1:8080": {
+ "pass": "routes"
+ }
+}
+```
+
+### Get the current status of NGINX UNIT processes
+```
+$ unitctl status -t yaml
+No socket path provided - attempting to detect from running instance
+connections:
+ accepted: 0
+ active: 0
+ idle: 0
+ closed: 0
+requests:
+ total: 0
+applications: {}
+```
+
+### Send arbitrary configuration payloads to UNIT
+```
+$ echo '{
+ "listeners": {
+ "127.0.0.1:8080": {
+ "pass": "routes"
+ }
+ },
+
+ "routes": [
+ {
+ "action": {
+ "share": "/www/data$uri"
+ }
+ }
+ ]
+}' | unitctl execute --http-method PUT --path /config -f -
+{
+ "success": "Reconfiguration done."
+}
+```
+
+### Edit current configuration in your favorite editor
+```
+$ unitctl edit
+[[EDITOR LOADS SHOWING CURRENT CONFIGURATION - USER EDITS AND SAVES]]
+
+{
+ "success": "Reconfiguration done."
+}
+```
+
+### Display interactive OpenAPI control panel
+```
+$ unitctl ui
+Starting UI server on http://127.0.0.1:3000/control-ui/
+Press Ctrl-C to stop the server
+```
+
+### Import configuration, certificates, and NJS modules from directory
+```
+$ unitctl import /opt/unit/config
+Imported /opt/unit/config/certificates/snake.pem -> /certificates/snake.pem
+Imported /opt/unit/config/hello.js -> /js_modules/hello.js
+Imported /opt/unit/config/put.json -> /config
+Imported 3 files
+```
+### Wait for socket to become available
+```
+$ unitctl --wait-timeout-seconds=3 --wait-max-tries=4 import /opt/unit/config`
+Waiting for 3s control socket to be available try 2/4...
+Waiting for 3s control socket to be available try 3/4...
+Waiting for 3s control socket to be available try 4/4...
+Timeout waiting for unit to start has been exceeded
+``` \ No newline at end of file
diff --git a/tools/unitctl/build/container.mk b/tools/unitctl/build/container.mk
new file mode 100644
index 00000000..c892db2e
--- /dev/null
+++ b/tools/unitctl/build/container.mk
@@ -0,0 +1,67 @@
+ ## Builds a container image for building on Debian Linux
+.PHONY: container-debian-build-image
+.ONESHELL: container-debian-build-image
+container-debian-build-image:
+container-debian-build-image:
+ $Q echo "$(M) building debian linux docker build image: $(@)"
+ $(DOCKER) buildx build $(DOCKER_BUILD_FLAGS)\
+ -t debian_builder -f Dockerfile $(CURDIR);
+
+ ## Builds deb packages using a container image
+.PHONY: container-deb-packages
+container-deb-packages: container-debian-build-image
+ $Q $(DOCKER) run --rm --volume "$(CURDIR):/project" \
+ --workdir /project debian_builder make deb-packages
+ # Reset permissions on the target directory to the current user
+ if command -v id > /dev/null; then \
+ $(DOCKER) run --rm --volume "$(CURDIR):/project" \
+ --workdir /project debian_builder \
+ chown --recursive "$(shell id -u):$(shell id -g)" /project/target
+ fi
+
+ ## Builds a rpm packages using a container image
+.PHONY: container-rpm-packages
+container-rpm-packages: container-debian-build-image
+ $Q $(DOCKER) run --rm --volume "$(CURDIR):/project" \
+ --workdir /project debian_builder make rpm-packages
+ # Reset permissions on the target directory to the current user
+ if command -v id > /dev/null; then \
+ $(DOCKER) run --rm --volume "$(CURDIR):/project" \
+ --workdir /project debian_builder chown --recursive \
+ "$(shell id -u):$(shell id -g)" /project/target
+ fi
+
+## Builds all packages using a container image
+.PHONY: container-all-packages
+container-all-packages: container-debian-build-image
+ $Q $(DOCKER) run --rm --volume "$(CURDIR):/project" \
+ --workdir /project debian_builder make all-packages
+ # Reset permissions on the target directory to the current user
+ if command -v id > /dev/null; then \
+ $(DOCKER) run --rm --volume "$(CURDIR):/project" \
+ --workdir /project debian_builder \
+ chown --recursive "$(shell id -u):$(shell id -g)" /project/target
+ fi
+
+## Run tests inside container
+.PHONY: container-test
+container-test: container-debian-build-image
+ $Q $(DOCKER) run --rm --volume "$(CURDIR):/project" \
+ --workdir /project debian_builder make test
+ # Reset permissions on the target directory to the current user
+ if command -v id > /dev/null; then \
+ $(DOCKER) run --rm --volume "$(CURDIR):/project" \
+ --workdir /project debian_builder \
+ chown --recursive "$(shell id -u):$(shell id -g)" /project/target
+ fi
+
+.PHONY: container-shell
+container-shell: container-debian-build-image ## Run tests inside container
+ $Q $(DOCKER) run -it --rm --volume "$(CURDIR):/project" \
+ --workdir /project debian_builder bash
+ # Reset permissions on the target directory to the current user
+ if command -v id > /dev/null; then \
+ $(DOCKER) run --rm --volume "$(CURDIR):/project" \
+ --workdir /project debian_builder \
+ chown --recursive "$(shell id -u):$(shell id -g)" /project/target
+ fi
diff --git a/tools/unitctl/build/github.mk b/tools/unitctl/build/github.mk
new file mode 100644
index 00000000..4d31546f
--- /dev/null
+++ b/tools/unitctl/build/github.mk
@@ -0,0 +1,22 @@
+.PHONY: gh-make-release
+.ONESHELL: gh-make-release
+gh-make-release:
+ifndef CI
+ $(error must be running in CI)
+endif
+ifneq ($(shell git rev-parse --abbrev-ref HEAD),release-v$(VERSION))
+ $(error must be running on release-v$(VERSION) branch)
+endif
+ $(info $(M) updating files with release version [$(GIT_BRANCH)]) @
+ git commit -m "ci: update files to version $(VERSION)" \
+ Cargo.toml pkg/brew/$(PACKAGE_NAME).rb
+ git push origin "release-v$(VERSION)"
+ git tag -a "v$(VERSION)" -m "ci: tagging v$(VERSION)"
+ git push origin --tags
+ gh release create "v$(VERSION)" \
+ --title "v$(VERSION)" \
+ --notes-file $(CURDIR)/target/dist/release_notes.md \
+ $(CURDIR)/target/dist/*.gz \
+ $(CURDIR)/target/dist/*.deb \
+ $(CURDIR)/target/dist/*.rpm \
+ $(CURDIR)/target/dist/SHA256SUMS
diff --git a/tools/unitctl/build/openapi-generator-cli.sh b/tools/unitctl/build/openapi-generator-cli.sh
new file mode 100755
index 00000000..3a65e5ce
--- /dev/null
+++ b/tools/unitctl/build/openapi-generator-cli.sh
@@ -0,0 +1,77 @@
+#!/usr/bin/env bash
+
+# Source: https://github.com/OpenAPITools/openapi-generator/blob/master/bin/utils/openapi-generator-cli.sh
+# License: Apache 2.0
+
+####
+# Save as openapi-generator-cli on your PATH. chmod u+x. Enjoy.
+#
+# This script will query github on every invocation to pull the latest released
+# version of openapi-generator.
+#
+# If you want repeatable executions, you can explicitly set a version via
+# OPENAPI_GENERATOR_VERSION
+# e.g. (in Bash)
+# export OPENAPI_GENERATOR_VERSION=3.1.0
+# openapi-generator-cli.sh
+# or
+# OPENAPI_GENERATOR_VERSION=3.1.0 openapi-generator-cli.sh
+#
+# This is also helpful, for example, if you want to evaluate a SNAPSHOT version.
+#
+# NOTE: Jars are downloaded on demand from maven into the same directory as this
+# script for every 'latest' version pulled from github. Consider putting this
+# under its own directory.
+####
+set -o pipefail
+
+for cmd in {mvn,jq,curl}; do
+ if ! command -v ${cmd} > /dev/null; then
+ >&2 echo "This script requires '${cmd}' to be installed."
+ exit 1
+ fi
+done
+
+function latest.tag {
+ local uri="https://api.github.com/repos/${1}/releases"
+ local ver=$(curl -s ${uri} | jq -r 'first(.[]|select(.prerelease==false)).tag_name')
+ if [[ $ver == v* ]]; then
+ ver=${ver:1}
+ fi
+ echo $ver
+}
+
+ghrepo=openapitools/openapi-generator
+groupid=org.openapitools
+artifactid=openapi-generator-cli
+ver=${OPENAPI_GENERATOR_VERSION:-$(latest.tag $ghrepo)}
+
+echo "Using OpenAPI Generator version: ${ver}"
+
+jar=${artifactid}-${ver}.jar
+cachedir=${OPENAPI_GENERATOR_DOWNLOAD_CACHE_DIR}
+
+DIR=${cachedir:-"$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"}
+
+if [ ! -d "${DIR}" ]; then
+ mkdir -p "${DIR}"
+fi
+
+if [ ! -f ${DIR}/${jar} ]; then
+ repo="central::default::https://repo1.maven.org/maven2/"
+ if [[ ${ver} =~ ^.*-SNAPSHOT$ ]]; then
+ repo="central::default::https://oss.sonatype.org/content/repositories/snapshots"
+ fi
+ mvn org.apache.maven.plugins:maven-dependency-plugin:2.9:get \
+ -DremoteRepositories=${repo} \
+ -Dartifact=${groupid}:${artifactid}:${ver} \
+ -Dtransitive=false \
+ -Ddest=${DIR}/${jar}
+fi
+
+java -ea \
+ ${JAVA_OPTS} \
+ -Xms512M \
+ -Xmx1024M \
+ -server \
+ -jar ${DIR}/${jar} "$@" \ No newline at end of file
diff --git a/tools/unitctl/build/package.mk b/tools/unitctl/build/package.mk
new file mode 100644
index 00000000..7009e2b1
--- /dev/null
+++ b/tools/unitctl/build/package.mk
@@ -0,0 +1,139 @@
+.PHONY: install-packaging-deb
+install-packaging-deb:
+ $Q if ! command -v cargo-deb > /dev/null; then \
+ $(CARGO) install --quiet cargo-deb; \
+ fi
+
+.PHONY: install-packaging-rpm
+install-packaging-rpm:
+ $Q if ! command -v cargo-generate-rpm > /dev/null; then \
+ $(CARGO) install --quiet cargo-generate-rpm; \
+ fi
+
+## Installs tools needed for building distributable packages
+.PHONY: install-packaging-tools
+install-packaging-tools:
+ $Q $(CARGO) install --quiet cargo-deb cargo-generate-rpm
+
+target/dist:
+ $Q mkdir -p $@
+
+## Builds all packages for all targets
+.PHONY: all-packages
+all-packages: deb-packages rpm-packages gz-packages
+
+target/dist/SHA256SUMS: target/dist
+ $Q cd target/dist && $(CHECKSUM) * > SHA256SUMS
+
+.PHONY: checksums
+checksums: target/dist/SHA256SUMS ## Generates checksums for all packages
+
+################################################################################
+### Debian Packages
+################################################################################
+
+to_debian_arch = $(shell echo $(1) | \
+ $(SED) -e 's/x86_64/amd64/' -e 's/aarch64/arm64/' -e 's/armv7/armhf/')
+DEBIAN_PACKAGE_TARGETS := \
+ $(foreach t, $(TARGETS), target/$(t)/debian/$(PACKAGE_NAME)_$(VERSION)_$(call to_debian_arch, $(firstword $(subst -, , $(t)))).deb)
+
+.ONESHELL: $(DEBIAN_PACKAGE_TARGETS)
+.NOTPARALLEL: $(DEBIAN_PACKAGE_TARGETS)
+$(DEBIAN_PACKAGE_TARGETS): $(TARGETS) target/man/$(OUTPUT_BINARY).1.gz target/dist
+ $Q TARGET="$(word 2, $(subst /, , $(dir $@)))"
+ # Skip building debs for musl targets
+ if echo "$(@)" | $(GREP) -q 'musl\|apple'; then \
+ exit 0
+ fi
+ if [ ! -f "$(CURDIR)/$(@)" ]; then
+ if [ -d "$(CURDIR)/target/release" ]; then \
+ echo "$(M) removing existing release directory: $(CURDIR)/target/release"
+ rm -rf "$(CURDIR)/target/release"
+ fi
+ echo "$(M) copying target architecture [$${TARGET}] build to target/release directory"
+ cp -r "$(CURDIR)/target/$${TARGET}/release" "$(CURDIR)/target/release"
+ echo "$(M) building debian package for target [$${TARGET}]: $(@)"
+ $(CARGO) deb --package unitctl --no-build --target "$${TARGET}" --output "$(CURDIR)/$(@)"
+ ln -f "$(CURDIR)/$(@)" "$(CURDIR)/target/dist/"
+ fi
+
+## Creates a debian package for the current platform
+.PHONY: deb-packages
+deb-packages: install-packaging-deb $(TARGETS) manpage $(DEBIAN_PACKAGE_TARGETS)
+
+################################################################################
+### RPM Packages
+################################################################################
+
+RPM_PACKAGE_TARGETS := $(foreach t, $(TARGETS), target/$(t)/generate-rpm/$(PACKAGE_NAME)_$(VERSION)_$(firstword $(subst -, , $(t))).rpm)
+
+.ONESHELL: $(RPM_PACKAGE_TARGETS)
+.NOTPARALLEL: $(RPM_PACKAGE_TARGETS)
+$(RPM_PACKAGE_TARGETS): $(TARGETS) target/man/$(OUTPUT_BINARY).1.gz target/dist
+ $Q TARGET="$(word 2, $(subst /, , $(dir $@)))"
+ ARCH="$(firstword $(subst -, , $(word 2, $(subst /, , $(dir $@)))))"
+ # Skip building rpms for musl targets
+ if echo "$(@)" | $(GREP) -q 'musl\|apple'; then \
+ exit 0
+ fi
+ if [ ! -f "$(CURDIR)/$(@)" ]; then
+ if [ -d "$(CURDIR)/target/release" ]; then \
+ echo "$(M) removing existing release directory: $(CURDIR)/target/release"
+ rm -rf "$(CURDIR)/target/release"
+ fi
+ echo "$(M) copying target architecture [$${ARCH}] build to target/release directory"
+ cp -r "$(CURDIR)/target/$${TARGET}/release" "$(CURDIR)/target/release"
+ echo "$(M) building rpm package: $(@)"
+ $(CARGO) generate-rpm --package unitctl --arch "$${ARCH}" --target "$${TARGET}" --output "$(CURDIR)/$(@)"
+ rm -rf "$(CURDIR)/target/release"
+ ln -f "$(CURDIR)/$(@)" "$(CURDIR)/target/dist/"
+ fi
+
+## Creates a rpm package for the current platform
+.PHONY: rpm-packages
+rpm-packages: install-packaging-rpm $(TARGETS) manpage $(RPM_PACKAGE_TARGETS)
+
+################################################################################
+### Homebrew Packages
+################################################################################
+
+## Modifies the homebrew formula to point to the latest release
+.PHONY: homebrew-packages
+.ONESHELL: homebrew-packages
+homebrew-packages: target/dist/SHA256SUMS
+ifdef NEW_VERSION
+ VERSION=$(NEW_VERSION)
+endif
+ $Q \
+ VERSION="$(VERSION)" \
+ PACKAGE_NAME="$(PACKAGE_NAME)" \
+ SRC_REPO="$(SRC_REPO)" \
+ AARCH64_UNKNOWN_LINUX_GNU_SHA256="$$($(GREP) $(PACKAGE_NAME)_v$(VERSION)_aarch64-unknown-linux-gnu.tar.gz $(CURDIR)/target/dist/SHA256SUMS | cut -d ' ' -f 1)" \
+ X86_64_UNKNOWN_LINUX_GNU_SHA256="$$($(GREP) $(PACKAGE_NAME)_v$(VERSION)_x86_64-unknown-linux-gnu.tar.gz $(CURDIR)/target/dist/SHA256SUMS | cut -d ' ' -f 1)" \
+ X86_64_APPLE_DARWIN_SHA256="$$($(GREP) $(PACKAGE_NAME)_v$(VERSION)_x86_64-apple-darwin.tar.gz $(CURDIR)/target/dist/SHA256SUMS | cut -d ' ' -f 1)" \
+ AARCH64_APPLE_DARWIN_SHA256="$$($(GREP) $(PACKAGE_NAME)_v$(VERSION)_aarch64-apple-darwin.tar.gz $(CURDIR)/target/dist/SHA256SUMS | cut -d ' ' -f 1)" \
+ envsubst < $(CURDIR)/pkg/brew/$(PACKAGE_NAME).rb.template > $(CURDIR)/pkg/brew/$(PACKAGE_NAME).rb
+
+
+################################################################################
+### Tarball Packages
+################################################################################
+
+GZ_PACKAGE_TARGETS = $(foreach t, $(TARGETS), target/gz/$(t)/$(PACKAGE_NAME)_$(VERSION)_$(firstword $(subst -, , $(t))).tar.gz)
+
+.ONESHELL: $(GZ_PACKAGE_TARGETS)
+$(GZ_PACKAGE_TARGETS): $(TARGETS) target/man/$(PACKAGE_NAME).1.gz target/dist
+ $Q mkdir -p "$(CURDIR)/target/gz"
+ TARGET="$(word 3, $(subst /, , $(dir $@)))"
+ PACKAGE="$(CURDIR)/target/gz/$(PACKAGE_NAME)_v$(VERSION)_$${TARGET}.tar.gz"
+ if [ ! -f "$${PACKAGE}}" ]; then
+ tar -cz -f $${PACKAGE} \
+ -C $(CURDIR)/target/man $(PACKAGE_NAME).1.gz \
+ -C $(CURDIR)/target/$${TARGET}/release $(PACKAGE_NAME) \
+ -C $(CURDIR) LICENSE.txt
+ ln -f "$${PACKAGE}" "$(CURDIR)/target/dist/"
+ fi
+
+## Creates a gzipped tarball all target platforms
+.PHONE: gz-packages
+gz-packages: $(GZ_PACKAGE_TARGETS)
diff --git a/tools/unitctl/build/release.mk b/tools/unitctl/build/release.mk
new file mode 100644
index 00000000..949e9301
--- /dev/null
+++ b/tools/unitctl/build/release.mk
@@ -0,0 +1,57 @@
+.ONESHELL: target/dist/release_notes.md
+target/dist/release_notes.md: target/dist target/dist/SHA256SUMS
+ $(info $(M) building release notes) @
+ $Q echo "# Release Notes" > target/dist/release_notes.md
+ echo '## SHA256 Checksums' >> target/dist/release_notes.md
+ echo '```' >> target/dist/release_notes.md
+ cat target/dist/SHA256SUMS >> target/dist/release_notes.md
+ echo '```' >> target/dist/release_notes.md
+
+.PHONY: release-notes
+release-notes: target/dist/release_notes.md ## Build release notes
+
+.PHONY: version
+version: ## Outputs the current version
+ $Q echo "Version: $(VERSION)"
+
+.PHONY: version-update
+.ONESHELL: version-update
+version-update: ## Prompts for a new version
+ $(info $(M) updating repository to new version) @
+ $Q echo " last committed version: $(LAST_VERSION)"
+ $Q echo " Cargo.toml file version : $(VERSION)"
+ read -p " Enter new version in the format (MAJOR.MINOR.PATCH): " version
+ $Q echo "$$version" | $(GREP) -qE '^[0-9]+\.[0-9]+\.[0-9]+-?.*$$' || \
+ (echo "invalid version identifier: $$version" && exit 1) && \
+ $(SED) -i "s/^version\s*=.*$$/version = \"$$version\"/" \
+ $(CURDIR)/unit-client-rs/Cargo.toml
+ $(SED) -i "s/^version\s*=.*$$/version = \"$$version\"/" \
+ $(CURDIR)/unitctl/Cargo.toml
+ $(SED) -i "s/^version\s*=.*$$/version = \"$$version\"/" \
+ $(CURDIR)/unit-openapi/Cargo.toml
+ $(SED) -i "s/^\s*\"packageVersion\":\s*.*$$/ \"packageVersion\": \"$$version\",/" \
+ $(CURDIR)/openapi-config.json
+ @ VERSION=$(shell $(GREP) -Po '^version\s+=\s+"\K.*?(?=")' \
+ $(CURDIR)/unitctl/Cargo.toml)
+
+.PHONY: version-release
+.ONESHELL: version-release
+version-release: ## Change from a pre-release to full release version
+ $Q echo "$(VERSION)" | $(GREP) -qE '^[0-9]+\.[0-9]+\.[0-9]+-beta$$' || \
+ (echo "invalid version identifier - must contain suffix -beta: $(VERSION)" && exit 1)
+ export NEW_VERSION="$(shell echo $(VERSION) | $(SED) -e 's/-beta$$//')"
+ $(SED) -i "s/^version\s*=.*$$/version = \"$$NEW_VERSION\"/" \
+ $(CURDIR)/unit-client-rs/Cargo.toml
+ $(SED) -i "s/^version\s*=.*$$/version = \"$$NEW_VERSION\"/" \
+ $(CURDIR)/unitctl/Cargo.toml
+ $(SED) -i "s/^version\s*=.*$$/version = \"$$NEW_VERSION\"/" \
+ $(CURDIR)/unit-openapi/Cargo.toml
+ $(SED) -i "s/^\s*\"packageVersion\":\s*.*$$/ \"packageVersion\": \"$$NEW_VERSION\",/" \
+ $(CURDIR)/openapi-config.json
+ @ VERSION=$(shell $(GREP) -Po '^version\s+=\s+"\K.*?(?=")' \
+ $(CURDIR)/unitctl/Cargo.toml)
+
+.PHONY: cargo-release
+cargo-release: ## Releases a new version to crates.io
+ $(info $(M) releasing version $(VERSION) to crates.io) @
+ $Q $(CARGO) publish
diff --git a/tools/unitctl/man/unitctl.1 b/tools/unitctl/man/unitctl.1
new file mode 100644
index 00000000..1bd725c6
--- /dev/null
+++ b/tools/unitctl/man/unitctl.1
@@ -0,0 +1,27 @@
+.\" Manpage for unitctl
+.\"
+.TH UNITCTL "1" "2022-12-29" "%%VERSION%%" "unitctl"
+.SH NAME
+unitctl \- NGINX UNIT Control Utility
+.SH SYNOPSIS
+unitctl [\fI\,FLAGS\/\fR] [\fI\,OPTIONS\/\fR] [\fI\,FILE\/\fR]...
+.SH DESCRIPTION
+WRITE ME
+.
+.SH "REPORTING BUGS"
+Report any issues on the project issue tracker at:
+.br
+\fB<https://github.com/nginx/unit>\fR
+.
+.SH ACKNOWLEDGEMENTS
+WRITE ME
+.
+.SH AUTHOR
+Elijah Zupancic \fB<e.zupancic@f5.com>\fR
+.
+.SH COPYRIGHT
+Copyright \(co 2022 F5. All Rights Reserved.
+.br
+License: Apache License 2.0 (Apache-2.0)
+.br
+Full License Text: <https://www.apache.org/licenses/LICENSE-2.0>
diff --git a/tools/unitctl/openapi-config.json b/tools/unitctl/openapi-config.json
new file mode 100644
index 00000000..783c8740
--- /dev/null
+++ b/tools/unitctl/openapi-config.json
@@ -0,0 +1,6 @@
+{
+ "packageName": "unit-openapi",
+ "packageVersion": "0.4.0-beta",
+ "library": "hyper",
+ "preferUnsignedInt": true
+} \ No newline at end of file
diff --git a/tools/unitctl/pkg/brew/unitctl.rb b/tools/unitctl/pkg/brew/unitctl.rb
new file mode 100644
index 00000000..771f2806
--- /dev/null
+++ b/tools/unitctl/pkg/brew/unitctl.rb
@@ -0,0 +1,29 @@
+class Unitctl < Formula
+ desc "CLI interface to the NGINX UNIT Control API"
+ homepage "https://github.com/nginxinc/unit-rust-sdk"
+ version "0.3.0"
+ package_name = "unitctl"
+ src_repo = "https://github.com/nginxinc/unit-rust-sdk"
+
+ if OS.mac? and Hardware::CPU.intel?
+ url "#{src_repo}/releases/download/v#{version}/#{package_name}_v#{version}_x86_64-apple-darwin.tar.gz"
+ sha256 "3e476850d1fc08aabc3cb25d19d42d171f52d55cea887aec754d47d1142c3638"
+ elsif OS.mac? and Hardware::CPU.arm?
+ url "#{src_repo}/releases/download/v#{version}/#{package_name}_#{version}_aarch64-apple-darwin.tar.gz"
+ sha256 "c1ec83ae67c08640f1712fba1c8aa305c063570fb7f96203228bf75413468bab"
+ elsif OS.linux? and Hardware::CPU.intel?
+ url "#{src_repo}/releases/download/v#{version}/#{package_name}_#{version}_x86_64-unknown-linux-gnu.tar.gz"
+ sha256 "9616687a7e4319c8399c0071059e6c1bb80b7e5b616714edc81a92717264a70f"
+ elsif OS.linux? and Hardware::CPU.arm? and Hardware::CPU.is_64_bit?
+ url "#{src_repo}/releases/download/v#{version}/#{package_name}_#{version}_aarch64-unknown-linux-gnu.tar.gz"
+ sha256 "88c2c7a8bc3d1930080c2b9a397a33e156ae4f876903b6565775270584055534"
+ else
+ odie "Unsupported architecture"
+ end
+
+
+ def install
+ bin.install "unitctl"
+ man1.install "unitctl.1.gz"
+ end
+end
diff --git a/tools/unitctl/pkg/brew/unitctl.rb.template b/tools/unitctl/pkg/brew/unitctl.rb.template
new file mode 100644
index 00000000..db6991f6
--- /dev/null
+++ b/tools/unitctl/pkg/brew/unitctl.rb.template
@@ -0,0 +1,29 @@
+class Unitctl < Formula
+ desc "CLI interface to the NGINX UNIT Control API"
+ homepage "https://github.com/nginxinc/unit-rust-sdk"
+ version "$VERSION"
+ package_name = "$PACKAGE_NAME"
+ src_repo = "$SRC_REPO"
+
+ if OS.mac? and Hardware::CPU.intel?
+ url "#{src_repo}/releases/download/v#{version}/#{package_name}_v#{version}_x86_64-apple-darwin.tar.gz"
+ sha256 "$X86_64_APPLE_DARWIN_SHA256"
+ elsif OS.mac? and Hardware::CPU.arm?
+ url "#{src_repo}/releases/download/v#{version}/#{package_name}_#{version}_aarch64-apple-darwin.tar.gz"
+ sha256 "$AARCH64_APPLE_DARWIN_SHA256"
+ elsif OS.linux? and Hardware::CPU.intel?
+ url "#{src_repo}/releases/download/v#{version}/#{package_name}_#{version}_x86_64-unknown-linux-gnu.tar.gz"
+ sha256 "$X86_64_UNKNOWN_LINUX_GNU_SHA256"
+ elsif OS.linux? and Hardware::CPU.arm? and Hardware::CPU.is_64_bit?
+ url "#{src_repo}/releases/download/v#{version}/#{package_name}_#{version}_aarch64-unknown-linux-gnu.tar.gz"
+ sha256 "$AARCH64_UNKNOWN_LINUX_GNU_SHA256"
+ else
+ odie "Unsupported architecture"
+ end
+
+
+ def install
+ bin.install "unitctl"
+ man1.install "unitctl.1.gz"
+ end
+end
diff --git a/tools/unitctl/rustfmt.toml b/tools/unitctl/rustfmt.toml
new file mode 100644
index 00000000..866c7561
--- /dev/null
+++ b/tools/unitctl/rustfmt.toml
@@ -0,0 +1 @@
+max_width = 120 \ No newline at end of file
diff --git a/tools/unitctl/unit-client-rs/Cargo.toml b/tools/unitctl/unit-client-rs/Cargo.toml
new file mode 100644
index 00000000..d3b2f9cf
--- /dev/null
+++ b/tools/unitctl/unit-client-rs/Cargo.toml
@@ -0,0 +1,32 @@
+[package]
+name = "unit-client-rs"
+version = "0.4.0-beta"
+authors = ["Elijah Zupancic"]
+edition = "2021"
+license = "Apache-2.0"
+
+[lib]
+name = "unit_client_rs"
+
+[features]
+# this preserves the ordering of json
+default = ["serde_json/preserve_order"]
+
+[dependencies]
+custom_error = "1.9"
+hyper = { version = "0.14", features = ["stream"] }
+hyper-tls = "0.5"
+hyperlocal = "0.8"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+sysinfo = "0.30.5"
+tokio = { version = "1.34", features = ["macros"] }
+futures = "0.3"
+hex = "0.4"
+which = "5.0"
+
+unit-openapi = { path = "../unit-openapi" }
+rustls = "0.23.5"
+
+[dev-dependencies]
+rand = "0.8.5"
diff --git a/tools/unitctl/unit-client-rs/src/control_socket_address.rs b/tools/unitctl/unit-client-rs/src/control_socket_address.rs
new file mode 100644
index 00000000..b9ae5afc
--- /dev/null
+++ b/tools/unitctl/unit-client-rs/src/control_socket_address.rs
@@ -0,0 +1,571 @@
+use crate::control_socket_address::ControlSocket::{TcpSocket, UnixLocalAbstractSocket, UnixLocalSocket};
+use crate::control_socket_address::ControlSocketScheme::{HTTP, HTTPS};
+use crate::unit_client::UnitClientError;
+use hyper::http::uri::{Authority, PathAndQuery};
+use hyper::Uri;
+use std::fmt::{Display, Formatter};
+use std::fs;
+use std::os::unix::fs::FileTypeExt;
+use std::path::{PathBuf, MAIN_SEPARATOR};
+
+type AbstractSocketName = String;
+type UnixSocketPath = PathBuf;
+type Port = u16;
+
+#[derive(Debug, Clone)]
+pub enum ControlSocket {
+ UnixLocalAbstractSocket(AbstractSocketName),
+ UnixLocalSocket(UnixSocketPath),
+ TcpSocket(Uri),
+}
+
+#[derive(Debug)]
+pub enum ControlSocketScheme {
+ HTTP,
+ HTTPS,
+}
+
+impl ControlSocketScheme {
+ fn port(&self) -> Port {
+ match self {
+ HTTP => 80,
+ HTTPS => 443,
+ }
+ }
+}
+
+impl ControlSocket {
+ pub fn socket_scheme(&self) -> ControlSocketScheme {
+ match self {
+ UnixLocalAbstractSocket(_) => ControlSocketScheme::HTTP,
+ UnixLocalSocket(_) => ControlSocketScheme::HTTP,
+ TcpSocket(uri) => match uri.scheme_str().expect("Scheme should not be None") {
+ "http" => ControlSocketScheme::HTTP,
+ "https" => ControlSocketScheme::HTTPS,
+ _ => unreachable!("Scheme should be http or https"),
+ },
+ }
+ }
+
+ pub fn create_uri_with_path(&self, str_path: &str) -> Uri {
+ match self {
+ UnixLocalAbstractSocket(name) => {
+ let socket_path = PathBuf::from(format!("@{}", name));
+ hyperlocal::Uri::new(socket_path, str_path).into()
+ }
+ UnixLocalSocket(socket_path) => hyperlocal::Uri::new(socket_path, str_path).into(),
+ TcpSocket(uri) => {
+ if str_path.is_empty() {
+ uri.clone()
+ } else {
+ let authority = uri.authority().expect("Authority should not be None");
+ Uri::builder()
+ .scheme(uri.scheme_str().expect("Scheme should not be None"))
+ .authority(authority.clone())
+ .path_and_query(str_path)
+ .build()
+ .expect("URI should be valid")
+ }
+ }
+ }
+ }
+}
+
+impl Display for ControlSocket {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match self {
+ UnixLocalAbstractSocket(name) => f.write_fmt(format_args!("unix:@{}", name)),
+ UnixLocalSocket(path) => f.write_fmt(format_args!("unix:{}", path.to_string_lossy())),
+ TcpSocket(uri) => uri.fmt(f),
+ }
+ }
+}
+
+impl From<ControlSocket> for String {
+ fn from(val: ControlSocket) -> Self {
+ val.to_string()
+ }
+}
+
+impl From<ControlSocket> for PathBuf {
+ fn from(val: ControlSocket) -> Self {
+ match val {
+ UnixLocalAbstractSocket(socket_name) => PathBuf::from(format!("@{}", socket_name)),
+ UnixLocalSocket(socket_path) => socket_path,
+ TcpSocket(_) => PathBuf::default(),
+ }
+ }
+}
+
+impl From<ControlSocket> for Uri {
+ fn from(val: ControlSocket) -> Self {
+ val.create_uri_with_path("")
+ }
+}
+
+impl ControlSocket {
+ pub fn validate_http_address(uri: Uri) -> Result<(), UnitClientError> {
+ let http_address = uri.to_string();
+ if uri.authority().is_none() {
+ return Err(UnitClientError::TcpSocketAddressParseError {
+ message: "No authority found in socket address".to_string(),
+ control_socket_address: http_address,
+ });
+ }
+ if uri.port_u16().is_none() {
+ return Err(UnitClientError::TcpSocketAddressNoPortError {
+ control_socket_address: http_address,
+ });
+ }
+ if !(uri.path().is_empty() || uri.path().eq("/")) {
+ return Err(UnitClientError::TcpSocketAddressParseError {
+ message: format!("Path is not empty or is not / [path={}]", uri.path()),
+ control_socket_address: http_address,
+ });
+ }
+
+ Ok(())
+ }
+
+ pub fn validate_unix_address(socket: PathBuf) -> Result<(), UnitClientError> {
+ if !socket.exists() {
+ return Err(UnitClientError::UnixSocketNotFound {
+ control_socket_address: socket.to_string_lossy().to_string(),
+ });
+ }
+ let metadata = fs::metadata(&socket).map_err(|error| UnitClientError::UnixSocketAddressError {
+ source: error,
+ control_socket_address: socket.to_string_lossy().to_string(),
+ })?;
+ let file_type = metadata.file_type();
+ if !file_type.is_socket() {
+ return Err(UnitClientError::UnixSocketAddressError {
+ source: std::io::Error::new(std::io::ErrorKind::Other, "Control socket path is not a socket"),
+ control_socket_address: socket.to_string_lossy().to_string(),
+ });
+ }
+
+ Ok(())
+ }
+
+ pub fn validate(&self) -> Result<Self, UnitClientError> {
+ match self {
+ UnixLocalAbstractSocket(socket_name) => {
+ let socket_path = PathBuf::from(format!("@{}", socket_name));
+ Self::validate_unix_address(socket_path.clone())
+ }
+ UnixLocalSocket(socket_path) => Self::validate_unix_address(socket_path.clone()),
+ TcpSocket(socket_uri) => Self::validate_http_address(socket_uri.clone()),
+ }
+ .map(|_| self.to_owned())
+ }
+
+ fn normalize_and_parse_http_address(http_address: String) -> Result<Uri, UnitClientError> {
+ // Convert *:1 style network addresses to URI format
+ let address = if http_address.starts_with("*:") {
+ http_address.replacen("*:", "http://127.0.0.1:", 1)
+ // Add scheme if not present
+ } else if !(http_address.starts_with("http://") || http_address.starts_with("https://")) {
+ format!("http://{}", http_address)
+ } else {
+ http_address.to_owned()
+ };
+
+ let is_https = address.starts_with("https://");
+
+ let parsed_uri =
+ Uri::try_from(address.as_str()).map_err(|error| UnitClientError::TcpSocketAddressUriError {
+ source: error,
+ control_socket_address: address,
+ })?;
+ let authority = parsed_uri.authority().expect("Authority should not be None");
+ let expected_port = if is_https { HTTPS.port() } else { HTTP.port() };
+ let normalized_authority = match authority.port_u16() {
+ Some(_) => authority.to_owned(),
+ None => {
+ let host = format!("{}:{}", authority.host(), expected_port);
+ Authority::try_from(host.as_str()).expect("Authority should be valid")
+ }
+ };
+
+ let normalized_uri = Uri::builder()
+ .scheme(parsed_uri.scheme_str().expect("Scheme should not be None"))
+ .authority(normalized_authority)
+ .path_and_query(PathAndQuery::from_static(""))
+ .build()
+ .map_err(|error| UnitClientError::TcpSocketAddressParseError {
+ message: error.to_string(),
+ control_socket_address: http_address.clone(),
+ })?;
+
+ Ok(normalized_uri)
+ }
+
+ /// Flexibly parse a textual representation of a socket address
+ fn parse_address<S: Into<String>>(socket_address: S) -> Result<Self, UnitClientError> {
+ let full_socket_address: String = socket_address.into();
+ let socket_prefix = "unix:";
+ let socket_uri_prefix = "unix://";
+ let mut buf = String::with_capacity(socket_prefix.len());
+ for (i, c) in full_socket_address.char_indices() {
+ // Abstract unix socket with no prefix
+ if i == 0 && c == '@' {
+ return Ok(UnixLocalAbstractSocket(full_socket_address[1..].to_string()));
+ }
+ buf.push(c);
+ // Unix socket with prefix
+ if i == socket_prefix.len() - 1 && buf.eq(socket_prefix) {
+ let path_text = full_socket_address[socket_prefix.len()..].to_string();
+ // Return here if this URI does not have a scheme followed by double slashes
+ if !path_text.starts_with("//") {
+ return match path_text.strip_prefix('@') {
+ Some(name) => Ok(UnixLocalAbstractSocket(name.to_string())),
+ None => {
+ let path = PathBuf::from(path_text);
+ Ok(UnixLocalSocket(path))
+ }
+ };
+ }
+ }
+
+ // Unix socket with URI prefix
+ if i == socket_uri_prefix.len() - 1 && buf.eq(socket_uri_prefix) {
+ let uri = Uri::try_from(full_socket_address.as_str()).map_err(|error| {
+ UnitClientError::TcpSocketAddressParseError {
+ message: error.to_string(),
+ control_socket_address: full_socket_address.clone(),
+ }
+ })?;
+ return ControlSocket::try_from(uri);
+ }
+ }
+
+ /* Sockets on Windows are not supported, so there is no need to check
+ * if the socket address is a valid path, so we can do this shortcut
+ * here to see if a path was specified without a unix: prefix. */
+ if buf.starts_with(MAIN_SEPARATOR) {
+ let path = PathBuf::from(buf);
+ return Ok(UnixLocalSocket(path));
+ }
+
+ let uri = Self::normalize_and_parse_http_address(buf)?;
+ Ok(TcpSocket(uri))
+ }
+
+ pub fn is_local_socket(&self) -> bool {
+ match self {
+ UnixLocalAbstractSocket(_) | UnixLocalSocket(_) => true,
+ TcpSocket(_) => false,
+ }
+ }
+}
+
+impl TryFrom<String> for ControlSocket {
+ type Error = UnitClientError;
+
+ fn try_from(socket_address: String) -> Result<Self, Self::Error> {
+ ControlSocket::parse_address(socket_address.as_str())
+ }
+}
+
+impl TryFrom<&str> for ControlSocket {
+ type Error = UnitClientError;
+
+ fn try_from(socket_address: &str) -> Result<Self, Self::Error> {
+ ControlSocket::parse_address(socket_address)
+ }
+}
+
+impl TryFrom<Uri> for ControlSocket {
+ type Error = UnitClientError;
+
+ fn try_from(socket_uri: Uri) -> Result<Self, Self::Error> {
+ match socket_uri.scheme_str() {
+ // URIs with the unix scheme will have a hostname that is a hex encoded string
+ // representing the path to the socket
+ Some("unix") => {
+ let host = match socket_uri.host() {
+ Some(host) => host,
+ None => {
+ return Err(UnitClientError::TcpSocketAddressParseError {
+ message: "No host found in socket address".to_string(),
+ control_socket_address: socket_uri.to_string(),
+ })
+ }
+ };
+ let bytes = hex::decode(host).map_err(|error| UnitClientError::TcpSocketAddressParseError {
+ message: error.to_string(),
+ control_socket_address: socket_uri.to_string(),
+ })?;
+ let path = String::from_utf8_lossy(&bytes);
+ ControlSocket::parse_address(path)
+ }
+ Some("http") | Some("https") => Ok(TcpSocket(socket_uri)),
+ Some(unknown) => Err(UnitClientError::TcpSocketAddressParseError {
+ message: format!("Unsupported scheme found in socket address: {}", unknown).to_string(),
+ control_socket_address: socket_uri.to_string(),
+ }),
+ None => Err(UnitClientError::TcpSocketAddressParseError {
+ message: "No scheme found in socket address".to_string(),
+ control_socket_address: socket_uri.to_string(),
+ }),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use rand::distributions::{Alphanumeric, DistString};
+ use std::env::temp_dir;
+ use std::fmt::Display;
+ use std::io;
+ use std::os::unix::net::UnixListener;
+
+ use super::*;
+
+ struct TempSocket {
+ socket_path: PathBuf,
+ _listener: UnixListener,
+ }
+
+ impl TempSocket {
+ fn shutdown(&mut self) -> io::Result<()> {
+ fs::remove_file(&self.socket_path)
+ }
+ }
+
+ impl Display for TempSocket {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ write!(f, "unix:{}", self.socket_path.to_string_lossy().to_string())
+ }
+ }
+
+ impl Drop for TempSocket {
+ fn drop(&mut self) {
+ self.shutdown()
+ .expect(format!("Unable to shutdown socket {}", self.socket_path.to_string_lossy()).as_str());
+ }
+ }
+
+ #[test]
+ fn will_error_with_nonexistent_unix_socket() {
+ let socket_address = "unix:/tmp/some_random_filename_that_doesnt_exist.sock";
+ let control_socket =
+ ControlSocket::try_from(socket_address).expect("No error should be returned until validate() is called");
+ assert!(control_socket.is_local_socket(), "Not parsed as a local socket");
+ assert!(control_socket.validate().is_err(), "Socket should not be valid");
+ }
+
+ #[test]
+ fn can_parse_socket_with_prefix() {
+ let temp_socket = create_file_socket().expect("Unable to create socket");
+ let control_socket = ControlSocket::try_from(temp_socket.to_string()).expect("Error parsing good socket path");
+ assert!(control_socket.is_local_socket(), "Not parsed as a local socket");
+ if let Err(e) = control_socket.validate() {
+ panic!("Socket should be valid: {}", e);
+ }
+ }
+
+ #[test]
+ fn can_parse_socket_from_uri() {
+ let temp_socket = create_file_socket().expect("Unable to create socket");
+ let uri: Uri = hyperlocal::Uri::new(temp_socket.socket_path.clone(), "").into();
+ let control_socket = ControlSocket::try_from(uri).expect("Error parsing good socket path");
+ assert!(control_socket.is_local_socket(), "Not parsed as a local socket");
+ if let Err(e) = control_socket.validate() {
+ panic!("Socket should be valid: {}", e);
+ }
+ }
+
+ #[test]
+ fn can_parse_socket_from_uri_text() {
+ let temp_socket = create_file_socket().expect("Unable to create socket");
+ let uri: Uri = hyperlocal::Uri::new(temp_socket.socket_path.clone(), "").into();
+ let control_socket = ControlSocket::parse_address(uri.to_string()).expect("Error parsing good socket path");
+ assert!(control_socket.is_local_socket(), "Not parsed as a local socket");
+ if let Err(e) = control_socket.validate() {
+ panic!("Socket for input text should be valid: {}", e);
+ }
+ }
+
+ #[test]
+ #[cfg(target_os = "linux")]
+ fn can_parse_abstract_socket_from_uri() {
+ let temp_socket = create_abstract_socket().expect("Unable to create socket");
+ let uri: Uri = hyperlocal::Uri::new(temp_socket.socket_path.clone(), "").into();
+ let control_socket = ControlSocket::try_from(uri).expect("Error parsing good socket path");
+ assert!(control_socket.is_local_socket(), "Not parsed as a local socket");
+ if let Err(e) = control_socket.validate() {
+ panic!("Socket should be valid: {}", e);
+ }
+ }
+
+ #[test]
+ #[cfg(target_os = "linux")]
+ fn can_parse_abstract_socket_from_uri_text() {
+ let temp_socket = create_abstract_socket().expect("Unable to create socket");
+ let uri: Uri = hyperlocal::Uri::new(temp_socket.socket_path.clone(), "").into();
+ let control_socket = ControlSocket::parse_address(uri.to_string()).expect("Error parsing good socket path");
+ assert!(control_socket.is_local_socket(), "Not parsed as a local socket");
+ if let Err(e) = control_socket.validate() {
+ panic!("Socket should be valid: {}", e);
+ }
+ }
+
+ #[test]
+ fn can_parse_socket_without_prefix() {
+ let temp_socket = create_file_socket().expect("Unable to create socket");
+ let control_socket = ControlSocket::try_from(temp_socket.socket_path.to_string_lossy().to_string())
+ .expect("Error parsing good socket path");
+ assert!(control_socket.is_local_socket(), "Not parsed as a local socket");
+ if let Err(e) = control_socket.validate() {
+ panic!("Socket should be valid: {}", e);
+ }
+ }
+
+ #[cfg(target_os = "linux")]
+ #[test]
+ fn can_parse_abstract_socket() {
+ let temp_socket = create_abstract_socket().expect("Unable to create socket");
+ let control_socket = ControlSocket::try_from(temp_socket.to_string()).expect("Error parsing good socket path");
+ assert!(control_socket.is_local_socket(), "Not parsed as a local socket");
+ if let Err(e) = control_socket.validate() {
+ panic!("Socket should be valid: {}", e);
+ }
+ }
+
+ #[test]
+ fn can_normalize_good_http_socket_addresses() {
+ let valid_socket_addresses = vec![
+ "http://127.0.0.1:8080",
+ "https://127.0.0.1:8080",
+ "http://127.0.0.1:8080/",
+ "127.0.0.1:8080",
+ "http://0.0.0.0:8080",
+ "https://0.0.0.0:8080",
+ "http://0.0.0.0:8080/",
+ "0.0.0.0:8080",
+ "http://localhost:8080",
+ "https://localhost:8080",
+ "http://localhost:8080/",
+ "localhost:8080",
+ "http://[::1]:8080",
+ "https://[::1]:8080",
+ "http://[::1]:8080/",
+ "[::1]:8080",
+ "http://[0000:0000:0000:0000:0000:0000:0000:0000]:8080",
+ "https://[0000:0000:0000:0000:0000:0000:0000:0000]:8080",
+ "http://[0000:0000:0000:0000:0000:0000:0000:0000]:8080/",
+ "[0000:0000:0000:0000:0000:0000:0000:0000]:8080",
+ ];
+ for socket_address in valid_socket_addresses {
+ let mut expected = if socket_address.starts_with("http") {
+ socket_address.to_string().trim_end_matches('/').to_string()
+ } else {
+ format!("http://{}", socket_address).trim_end_matches('/').to_string()
+ };
+ expected.push('/');
+
+ let control_socket = ControlSocket::try_from(socket_address).expect("Error parsing good socket path");
+ assert!(!control_socket.is_local_socket(), "Not parsed as a local socket");
+ if let Err(e) = control_socket.validate() {
+ panic!("Socket should be valid: {}", e);
+ }
+ }
+ }
+
+ #[test]
+ fn can_normalize_wildcard_http_socket_address() {
+ let socket_address = "*:8080";
+ let expected = "http://127.0.0.1:8080/";
+ let normalized_result = ControlSocket::normalize_and_parse_http_address(socket_address.to_string());
+ let normalized = normalized_result
+ .expect("Unable to normalize socket address")
+ .to_string();
+ assert_eq!(normalized, expected);
+ }
+
+ #[test]
+ fn can_normalize_http_socket_address_with_no_port() {
+ let socket_address = "http://localhost";
+ let expected = "http://localhost:80/";
+ let normalized_result = ControlSocket::normalize_and_parse_http_address(socket_address.to_string());
+ let normalized = normalized_result
+ .expect("Unable to normalize socket address")
+ .to_string();
+ assert_eq!(normalized, expected);
+ }
+
+ #[test]
+ fn can_normalize_https_socket_address_with_no_port() {
+ let socket_address = "https://localhost";
+ let expected = "https://localhost:443/";
+ let normalized_result = ControlSocket::normalize_and_parse_http_address(socket_address.to_string());
+ let normalized = normalized_result
+ .expect("Unable to normalize socket address")
+ .to_string();
+ assert_eq!(normalized, expected);
+ }
+
+ #[test]
+ fn can_parse_http_addresses() {
+ let valid_socket_addresses = vec![
+ "http://127.0.0.1:8080",
+ "https://127.0.0.1:8080",
+ "http://127.0.0.1:8080/",
+ "127.0.0.1:8080",
+ "http://0.0.0.0:8080",
+ "https://0.0.0.0:8080",
+ "http://0.0.0.0:8080/",
+ "0.0.0.0:8080",
+ "http://localhost:8080",
+ "https://localhost:8080",
+ "http://localhost:8080/",
+ "localhost:8080",
+ "http://[::1]:8080",
+ "https://[::1]:8080",
+ "http://[::1]:8080/",
+ "[::1]:8080",
+ "http://[0000:0000:0000:0000:0000:0000:0000:0000]:8080",
+ "https://[0000:0000:0000:0000:0000:0000:0000:0000]:8080",
+ "http://[0000:0000:0000:0000:0000:0000:0000:0000]:8080/",
+ "[0000:0000:0000:0000:0000:0000:0000:0000]:8080",
+ ];
+ for socket_address in valid_socket_addresses {
+ let mut expected = if socket_address.starts_with("http") {
+ socket_address.to_string().trim_end_matches('/').to_string()
+ } else {
+ format!("http://{}", socket_address).trim_end_matches('/').to_string()
+ };
+ expected.push('/');
+
+ let normalized = ControlSocket::normalize_and_parse_http_address(socket_address.to_string())
+ .expect("Unable to normalize socket address")
+ .to_string();
+ assert_eq!(normalized, expected);
+ }
+ }
+
+ fn create_file_socket() -> Result<TempSocket, io::Error> {
+ let random = Alphanumeric.sample_string(&mut rand::thread_rng(), 10);
+ let socket_name = format!("unit-client-socket-test-{}.sock", random);
+ let socket_path = temp_dir().join(socket_name);
+ let listener = UnixListener::bind(&socket_path)?;
+ Ok(TempSocket {
+ socket_path,
+ _listener: listener,
+ })
+ }
+
+ #[cfg(target_os = "linux")]
+ fn create_abstract_socket() -> Result<TempSocket, io::Error> {
+ let random = Alphanumeric.sample_string(&mut rand::thread_rng(), 10);
+ let socket_name = format!("@unit-client-socket-test-{}.sock", random);
+ let socket_path = PathBuf::from(socket_name);
+ let listener = UnixListener::bind(&socket_path)?;
+ Ok(TempSocket {
+ socket_path,
+ _listener: listener,
+ })
+ }
+}
diff --git a/tools/unitctl/unit-client-rs/src/lib.rs b/tools/unitctl/unit-client-rs/src/lib.rs
new file mode 100644
index 00000000..dca8a86f
--- /dev/null
+++ b/tools/unitctl/unit-client-rs/src/lib.rs
@@ -0,0 +1,15 @@
+extern crate custom_error;
+extern crate futures;
+extern crate hyper;
+extern crate hyper_tls;
+extern crate hyperlocal;
+extern crate serde;
+extern crate serde_json;
+pub mod control_socket_address;
+mod runtime_flags;
+pub mod unit_client;
+mod unitd_cmd;
+pub mod unitd_configure_options;
+pub mod unitd_instance;
+pub mod unitd_process;
+mod unitd_process_user;
diff --git a/tools/unitctl/unit-client-rs/src/runtime_flags.rs b/tools/unitctl/unit-client-rs/src/runtime_flags.rs
new file mode 100644
index 00000000..7b31274d
--- /dev/null
+++ b/tools/unitctl/unit-client-rs/src/runtime_flags.rs
@@ -0,0 +1,90 @@
+use std::borrow::Cow;
+use std::fmt;
+use std::fmt::Display;
+use std::path::{Path, PathBuf};
+
+#[derive(Debug, Clone)]
+pub struct RuntimeFlags {
+ pub flags: Cow<'static, str>,
+}
+
+impl Display for RuntimeFlags {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{}", self.flags)
+ }
+}
+
+impl RuntimeFlags {
+ pub fn new<S>(flags: S) -> RuntimeFlags
+ where
+ S: Into<String>,
+ {
+ RuntimeFlags {
+ flags: Cow::from(flags.into()),
+ }
+ }
+
+ pub fn has_flag(&self, flag_name: &str) -> bool {
+ self.flags.contains(format!("--{}", flag_name).as_str())
+ }
+
+ pub fn get_flag_value(&self, flag_name: &str) -> Option<String> {
+ let flag_parts = self.flags.split_ascii_whitespace().collect::<Vec<&str>>();
+ for (i, flag) in flag_parts.iter().enumerate() {
+ if let Some(name) = flag.strip_prefix("--") {
+ /* If there is no flag value after the current one, there is by definition no
+ * flag value for the current flag. */
+ let index_lt_len = flag_parts.len() > i + 1;
+ if index_lt_len {
+ let next_value_isnt_flag = !flag_parts[i + 1].starts_with("--");
+ if name.eq(flag_name) && next_value_isnt_flag {
+ return Some(flag_parts[i + 1].to_string());
+ }
+ }
+ }
+ }
+ None
+ }
+
+ pub fn control_api_socket_address(&self) -> Option<String> {
+ self.get_flag_value("control")
+ }
+
+ pub fn pid_path(&self) -> Option<Box<Path>> {
+ self.get_flag_value("pid")
+ .map(PathBuf::from)
+ .map(PathBuf::into_boxed_path)
+ }
+
+ pub fn log_path(&self) -> Option<Box<Path>> {
+ self.get_flag_value("log")
+ .map(PathBuf::from)
+ .map(PathBuf::into_boxed_path)
+ }
+
+ pub fn modules_directory(&self) -> Option<Box<Path>> {
+ self.get_flag_value("modules")
+ .map(PathBuf::from)
+ .map(PathBuf::into_boxed_path)
+ }
+
+ pub fn state_directory(&self) -> Option<Box<Path>> {
+ self.get_flag_value("state")
+ .map(PathBuf::from)
+ .map(PathBuf::into_boxed_path)
+ }
+
+ pub fn tmp_directory(&self) -> Option<Box<Path>> {
+ self.get_flag_value("tmp")
+ .map(PathBuf::from)
+ .map(PathBuf::into_boxed_path)
+ }
+
+ pub fn user(&self) -> Option<String> {
+ self.get_flag_value("user").map(String::from)
+ }
+
+ pub fn group(&self) -> Option<String> {
+ self.get_flag_value("group").map(String::from)
+ }
+}
diff --git a/tools/unitctl/unit-client-rs/src/unit_client.rs b/tools/unitctl/unit-client-rs/src/unit_client.rs
new file mode 100644
index 00000000..b856fd20
--- /dev/null
+++ b/tools/unitctl/unit-client-rs/src/unit_client.rs
@@ -0,0 +1,393 @@
+use std::collections::HashMap;
+use std::error::Error as StdError;
+use std::fmt::Debug;
+use std::future::Future;
+use std::rc::Rc;
+use std::{fmt, io};
+
+use custom_error::custom_error;
+use hyper::body::{Buf, HttpBody};
+use hyper::client::{HttpConnector, ResponseFuture};
+use hyper::Error as HyperError;
+use hyper::{http, Body, Client, Request};
+use hyper_tls::HttpsConnector;
+use hyperlocal::{UnixClientExt, UnixConnector};
+use serde::{Deserialize, Serialize};
+use tokio::runtime::Runtime;
+
+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};
+
+const USER_AGENT: &str = concat!("UNIT CLI/", env!("CARGO_PKG_VERSION"), "/rust");
+
+custom_error! {pub UnitClientError
+ OpenAPIError { source: OpenAPIError } = "OpenAPI error",
+ JsonError { source: serde_json::Error,
+ path: String} = "JSON error [path={path}]",
+ HyperError { source: hyper::Error,
+ control_socket_address: String,
+ path: String} = "Communications error [control_socket_address={control_socket_address}, path={path}]: {source}",
+ HttpRequestError { source: http::Error,
+ path: String} = "HTTP error [path={path}]",
+ HttpResponseError { status: http::StatusCode,
+ path: String,
+ body: String} = "HTTP response error [path={path}, status={status}]:\n{body}",
+ HttpResponseJsonBodyError { status: http::StatusCode,
+ path: String,
+ error: String,
+ detail: String} = "HTTP response error [path={path}, status={status}]:\n Error: {error}\n Detail: {detail}",
+ IoError { source: io::Error, socket: String } = "IO error [socket={socket}]",
+ UnixSocketAddressError {
+ source: io::Error,
+ control_socket_address: String
+ } = "Invalid unix domain socket address [control_socket_address={control_socket_address}]",
+ SocketPermissionsError { control_socket_address: String } =
+ "Insufficient permissions to connect to control socket [control_socket_address={control_socket_address}]",
+ UnixSocketNotFound { control_socket_address: String } = "Unix socket not found [control_socket_address={control_socket_address}]",
+ TcpSocketAddressUriError {
+ source: http::uri::InvalidUri,
+ control_socket_address: String
+ } = "Invalid TCP socket address [control_socket_address={control_socket_address}]",
+ TcpSocketAddressParseError {
+ message: String,
+ control_socket_address: String
+ } = "Invalid TCP socket address [control_socket_address={control_socket_address}]: {message}",
+ TcpSocketAddressNoPortError {
+ control_socket_address: String
+ } = "TCP socket address does not have a port specified [control_socket_address={control_socket_address}]",
+ UnitdProcessParseError {
+ message: String,
+ pid: u64
+ } = "{message} for [pid={pid}]",
+ UnitdProcessExecError {
+ source: Box<dyn StdError>,
+ message: String,
+ executable_path: String,
+ pid: u64
+ } = "{message} for [pid={pid}, executable_path={executable_path}]: {source}",
+}
+
+impl UnitClientError {
+ fn new(error: HyperError, control_socket_address: String, path: String) -> Self {
+ if error.is_connect() {
+ if let Some(source) = error.source() {
+ if let Some(io_error) = source.downcast_ref::<io::Error>() {
+ if io_error.kind().eq(&io::ErrorKind::PermissionDenied) {
+ return UnitClientError::SocketPermissionsError { control_socket_address };
+ }
+ }
+ }
+ }
+
+ UnitClientError::HyperError {
+ source: error,
+ control_socket_address,
+ path,
+ }
+ }
+}
+
+macro_rules! new_openapi_client_from_hyper_client {
+ ($unit_client:expr, $hyper_client: ident, $api_client:ident, $api_trait:ident) => {{
+ let config = Configuration {
+ base_path: $unit_client.control_socket.create_uri_with_path("/").to_string(),
+ user_agent: Some(format!("{}/OpenAPI-Generator", USER_AGENT).to_owned()),
+ client: $hyper_client.clone(),
+ basic_auth: None,
+ oauth_access_token: None,
+ api_key: None,
+ };
+ let rc_config = Rc::new(config);
+ Box::new($api_client::new(rc_config)) as Box<dyn $api_trait>
+ }};
+}
+
+macro_rules! new_openapi_client {
+ ($unit_client:expr, $api_client:ident, $api_trait:ident) => {
+ match &*$unit_client.client {
+ RemoteClient::Tcp { client } => {
+ new_openapi_client_from_hyper_client!($unit_client, client, $api_client, $api_trait)
+ }
+ RemoteClient::Unix { client } => {
+ new_openapi_client_from_hyper_client!($unit_client, client, $api_client, $api_trait)
+ }
+ }
+ };
+}
+
+#[derive(Clone)]
+pub enum RemoteClient<B>
+where
+ B: HttpBody + Send + 'static,
+ B::Data: Send,
+ B::Error: Into<Box<dyn StdError + Send + Sync>>,
+{
+ Unix {
+ client: Client<UnixConnector, B>,
+ },
+ Tcp {
+ client: Client<HttpsConnector<HttpConnector>, B>,
+ },
+}
+
+impl<B> RemoteClient<B>
+where
+ B: HttpBody + Send + 'static,
+ B::Data: Send,
+ B::Error: Into<Box<dyn StdError + Send + Sync>>,
+{
+ fn client_name(&self) -> &str {
+ match self {
+ RemoteClient::Unix { .. } => "Client<UnixConnector, Body>",
+ RemoteClient::Tcp { .. } => "Client<HttpsConnector<HttpConnector>, Body>",
+ }
+ }
+
+ pub fn request(&self, req: Request<B>) -> ResponseFuture {
+ match self {
+ RemoteClient::Unix { client } => client.request(req),
+ RemoteClient::Tcp { client } => client.request(req),
+ }
+ }
+}
+
+impl<B> Debug for RemoteClient<B>
+where
+ B: HttpBody + Send + 'static,
+ B::Data: Send,
+ B::Error: Into<Box<dyn StdError + Send + Sync>>,
+{
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", self.client_name())
+ }
+}
+
+#[derive(Debug)]
+pub struct UnitClient {
+ pub control_socket: ControlSocket,
+ /// A `current_thread` runtime for executing operations on the
+ /// asynchronous client in a blocking manner.
+ rt: Runtime,
+ /// Client for communicating with the control API over the UNIX domain socket
+ client: Box<RemoteClient<Body>>,
+}
+
+impl UnitClient {
+ pub fn new_with_runtime(control_socket: ControlSocket, runtime: Runtime) -> Self {
+ if control_socket.is_local_socket() {
+ Self::new_unix(control_socket, runtime)
+ } else {
+ Self::new_http(control_socket, runtime)
+ }
+ }
+
+ pub fn new(control_socket: ControlSocket) -> Self {
+ let runtime = tokio::runtime::Builder::new_current_thread()
+ .enable_all()
+ .build()
+ .expect("Unable to create a current_thread runtime");
+ Self::new_with_runtime(control_socket, runtime)
+ }
+
+ pub fn new_http(control_socket: ControlSocket, runtime: Runtime) -> Self {
+ let remote_client = Client::builder().build(HttpsConnector::new());
+ Self {
+ control_socket,
+ rt: runtime,
+ client: Box::from(RemoteClient::Tcp { client: remote_client }),
+ }
+ }
+
+ pub fn new_unix(control_socket: ControlSocket, runtime: Runtime) -> UnitClient {
+ let remote_client = Client::unix();
+
+ Self {
+ control_socket,
+ rt: runtime,
+ client: Box::from(RemoteClient::Unix { client: remote_client }),
+ }
+ }
+
+ /// Sends a request to UNIT and deserializes the JSON response body into the value of type `RESPONSE`.
+ pub fn send_request_and_deserialize_response<RESPONSE: for<'de> serde::Deserialize<'de>>(
+ &self,
+ mut request: Request<Body>,
+ ) -> Result<RESPONSE, UnitClientError> {
+ let uri = request.uri().clone();
+ let path: &str = uri.path();
+
+ request.headers_mut().insert("User-Agent", USER_AGENT.parse().unwrap());
+
+ let response_future = self.client.request(request);
+
+ self.rt.block_on(async {
+ let response = response_future
+ .await
+ .map_err(|error| UnitClientError::new(error, self.control_socket.to_string(), path.to_string()))?;
+
+ let status = response.status();
+ let body = hyper::body::aggregate(response)
+ .await
+ .map_err(|error| UnitClientError::new(error, self.control_socket.to_string(), path.to_string()))?;
+ let reader = &mut body.reader();
+ if !status.is_success() {
+ let error: HashMap<String, String> =
+ serde_json::from_reader(reader).map_err(|error| UnitClientError::JsonError {
+ source: error,
+ path: path.to_string(),
+ })?;
+
+ return Err(UnitClientError::HttpResponseJsonBodyError {
+ status,
+ path: path.to_string(),
+ error: error.get("error").unwrap_or(&"Unknown error".into()).to_string(),
+ detail: error.get("detail").unwrap_or(&"".into()).to_string(),
+ });
+ }
+ serde_json::from_reader(reader).map_err(|error| UnitClientError::JsonError {
+ source: error,
+ path: path.to_string(),
+ })
+ })
+ }
+
+ pub fn listeners_api(&self) -> Box<dyn ListenersApi + 'static> {
+ new_openapi_client!(self, ListenersApiClient, ListenersApi)
+ }
+
+ pub fn listeners(&self) -> Result<HashMap<String, ConfigListener>, Box<UnitClientError>> {
+ let list_listeners = self.listeners_api().get_listeners();
+ self.execute_openapi_future(list_listeners)
+ }
+
+ pub fn execute_openapi_future<F: Future<Output = Result<R, OpenAPIError>>, R: for<'de> serde::Deserialize<'de>>(
+ &self,
+ future: F,
+ ) -> Result<R, Box<UnitClientError>> {
+ self.rt.block_on(future).map_err(|error| {
+ let remapped_error = if let OpenAPIError::Hyper(hyper_error) = error {
+ UnitClientError::new(hyper_error, self.control_socket.to_string(), "".to_string())
+ } else {
+ UnitClientError::OpenAPIError { source: error }
+ };
+
+ Box::new(remapped_error)
+ })
+ }
+
+ pub fn status_api(&self) -> Box<dyn StatusApi + 'static> {
+ new_openapi_client!(self, StatusApiClient, StatusApi)
+ }
+
+ pub fn status(&self) -> Result<Status, Box<UnitClientError>> {
+ let status = self.status_api().get_status();
+ self.execute_openapi_future(status)
+ }
+
+ pub fn is_running(&self) -> bool {
+ self.status().is_ok()
+ }
+}
+
+pub type UnitSerializableMap = HashMap<String, serde_json::Value>;
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct UnitStatus {
+ pub connections: UnitStatusConnections,
+ pub requests: UnitStatusRequests,
+ pub applications: HashMap<String, UnitStatusApplication>,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct UnitStatusConnections {
+ #[serde(default)]
+ pub closed: usize,
+ #[serde(default)]
+ pub idle: usize,
+ #[serde(default)]
+ pub active: usize,
+ #[serde(default)]
+ pub accepted: usize,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct UnitStatusRequests {
+ #[serde(default)]
+ pub active: usize,
+ #[serde(default)]
+ pub total: usize,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct UnitStatusApplication {
+ #[serde(default)]
+ pub processes: HashMap<String, usize>,
+ #[serde(default)]
+ pub requests: HashMap<String, usize>,
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::unitd_instance::UnitdInstance;
+
+ use super::*;
+ // Integration tests
+
+ #[test]
+ fn can_connect_to_unit_api() {
+ match UnitdInstance::running_unitd_instances().first() {
+ Some(unit_instance) => {
+ let control_api_socket_address = unit_instance
+ .control_api_socket_address()
+ .expect("No control API socket path found");
+ let control_socket = ControlSocket::try_from(control_api_socket_address)
+ .expect("Unable to parse control socket address");
+ let unit_client = UnitClient::new(control_socket);
+ assert!(unit_client.is_running());
+ }
+ None => {
+ eprintln!("No running unitd instances found - skipping test");
+ }
+ }
+ }
+
+ #[test]
+ fn can_get_unit_status() {
+ match UnitdInstance::running_unitd_instances().first() {
+ Some(unit_instance) => {
+ let control_api_socket_address = unit_instance
+ .control_api_socket_address()
+ .expect("No control API socket path found");
+ let control_socket = ControlSocket::try_from(control_api_socket_address)
+ .expect("Unable to parse control socket address");
+ let unit_client = UnitClient::new(control_socket);
+ let status = unit_client.status().expect("Unable to get unit status");
+ println!("Unit status: {:?}", status);
+ }
+ None => {
+ eprintln!("No running unitd instances found - skipping test");
+ }
+ }
+ }
+
+ #[test]
+ fn can_get_unit_listeners() {
+ match UnitdInstance::running_unitd_instances().first() {
+ Some(unit_instance) => {
+ let control_api_socket_address = unit_instance
+ .control_api_socket_address()
+ .expect("No control API socket path found");
+ let control_socket = ControlSocket::try_from(control_api_socket_address)
+ .expect("Unable to parse control socket address");
+ let unit_client = UnitClient::new(control_socket);
+ unit_client.listeners().expect("Unable to get Unit listeners");
+ }
+ None => {
+ eprintln!("No running unitd instances found - skipping test");
+ }
+ }
+ }
+}
diff --git a/tools/unitctl/unit-client-rs/src/unitd_cmd.rs b/tools/unitctl/unit-client-rs/src/unitd_cmd.rs
new file mode 100644
index 00000000..c4883ed5
--- /dev/null
+++ b/tools/unitctl/unit-client-rs/src/unitd_cmd.rs
@@ -0,0 +1,85 @@
+use std::error::Error as StdError;
+use std::io::{Error as IoError, ErrorKind};
+
+use crate::runtime_flags::RuntimeFlags;
+use std::path::{Path, PathBuf};
+
+#[derive(Debug, Clone)]
+pub struct UnitdCmd {
+ pub(crate) process_executable_path: Option<Box<Path>>,
+ pub version: Option<String>,
+ pub flags: Option<RuntimeFlags>,
+}
+
+impl UnitdCmd {
+ pub(crate) fn new<S>(full_cmd: S, binary_name: &str) -> Result<UnitdCmd, Box<dyn StdError>>
+ where
+ S: Into<String>,
+ {
+ let process_cmd: String = full_cmd.into();
+ let parsable = process_cmd
+ .strip_prefix("unit: main v")
+ .and_then(|s| s.strip_suffix(']'));
+ if parsable.is_none() {
+ let msg = format!("cmd does not have the expected format: {}", process_cmd);
+ return Err(IoError::new(ErrorKind::InvalidInput, msg).into());
+ }
+ let parts = parsable
+ .expect("Unable to parse cmd")
+ .splitn(2, " [")
+ .collect::<Vec<&str>>();
+ if parts.len() != 2 {
+ let msg = format!("cmd does not have the expected format: {}", process_cmd);
+ return Err(IoError::new(ErrorKind::InvalidInput, msg).into());
+ }
+ let version: Option<String> = Some(parts[0].to_string());
+ let executable_path = UnitdCmd::parse_executable_path_from_cmd(parts[1], binary_name);
+ let flags = UnitdCmd::parse_runtime_flags_from_cmd(parts[1]);
+
+ Ok(UnitdCmd {
+ process_executable_path: executable_path,
+ version,
+ flags,
+ })
+ }
+
+ fn parse_executable_path_from_cmd<S>(full_cmd: S, binary_name: &str) -> Option<Box<Path>>
+ where
+ S: Into<String>,
+ {
+ let cmd = full_cmd.into();
+ if cmd.is_empty() {
+ return None;
+ }
+
+ let split = cmd.splitn(2, binary_name).collect::<Vec<&str>>();
+ if split.is_empty() {
+ return None;
+ }
+
+ let path = format!("{}{}", split[0], binary_name);
+ Some(PathBuf::from(path).into_boxed_path())
+ }
+
+ fn parse_runtime_flags_from_cmd<S>(full_cmd: S) -> Option<RuntimeFlags>
+ where
+ S: Into<String>,
+ {
+ let cmd = full_cmd.into();
+ if cmd.is_empty() {
+ return None;
+ }
+ // Split out everything in between the brackets [ and ]
+ let split = cmd.trim_end_matches(']').splitn(2, '[').collect::<Vec<&str>>();
+ if split.is_empty() {
+ return None;
+ }
+ /* Now we need to parse a string like this:
+ * ./sbin/unitd --no-daemon --tmp /tmp
+ * and only return what is after the invoking command */
+ split[0]
+ .find("--")
+ .map(|index| cmd[index..].to_string())
+ .map(RuntimeFlags::new)
+ }
+}
diff --git a/tools/unitctl/unit-client-rs/src/unitd_configure_options.rs b/tools/unitctl/unit-client-rs/src/unitd_configure_options.rs
new file mode 100644
index 00000000..88ab1101
--- /dev/null
+++ b/tools/unitctl/unit-client-rs/src/unitd_configure_options.rs
@@ -0,0 +1,235 @@
+use custom_error::custom_error;
+use std::borrow::Cow;
+use std::error::Error as stdError;
+use std::io::{BufRead, BufReader, Lines};
+use std::path::{Path, PathBuf};
+use std::process::{Command, Stdio};
+
+custom_error! {UnitdStderrParseError
+ VersionNotFound = "Version string output not found",
+ BuildSettingsNotFound = "Build settings not found"
+}
+
+#[derive(Debug, Clone)]
+pub struct UnitdConfigureOptions {
+ pub version: Cow<'static, str>,
+ pub all_flags: Cow<'static, str>,
+}
+
+impl UnitdConfigureOptions {
+ pub fn new(unitd_path: &Path) -> Result<UnitdConfigureOptions, Box<dyn stdError>> {
+ fn parse_configure_settings_from_unitd_stderr_output<B: BufRead>(
+ lines: &mut Lines<B>,
+ ) -> Result<UnitdConfigureOptions, Box<dyn stdError>> {
+ const VERSION_PREFIX: &str = "unit version: ";
+ const CONFIGURED_AS_PREFIX: &str = "configured as ";
+ const CONFIGURE_PREFIX: &str = "configured as ./configure ";
+
+ fn aggregate_parsable_lines(
+ mut accum: (Option<String>, Option<String>),
+ line: String,
+ ) -> (Option<String>, Option<String>) {
+ if line.starts_with(VERSION_PREFIX) {
+ accum.0 = line.strip_prefix(VERSION_PREFIX).map(|l| l.to_string());
+ } else if line.starts_with(CONFIGURED_AS_PREFIX) {
+ accum.1 = line.strip_prefix(CONFIGURE_PREFIX).map(|l| l.to_string());
+ }
+
+ accum
+ }
+
+ let options_lines = lines
+ .filter_map(|line| line.ok())
+ .fold((None, None), aggregate_parsable_lines);
+
+ if options_lines.0.is_none() {
+ return Err(Box::new(UnitdStderrParseError::VersionNotFound) as Box<dyn stdError>);
+ } else if options_lines.1.is_none() {
+ return Err(Box::new(UnitdStderrParseError::BuildSettingsNotFound) as Box<dyn stdError>);
+ }
+
+ Ok(UnitdConfigureOptions {
+ version: options_lines.0.unwrap().into(),
+ all_flags: options_lines.1.unwrap().into(),
+ })
+ }
+
+ let program = unitd_path.as_os_str();
+ let child = Command::new(program)
+ .arg("--version")
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .spawn()?;
+ let output = child.wait_with_output()?;
+ let err = BufReader::new(&*output.stderr);
+ parse_configure_settings_from_unitd_stderr_output(&mut err.lines())
+ }
+
+ pub fn has_flag(&self, flag_name: &str) -> bool {
+ self.all_flags
+ .split_ascii_whitespace()
+ .any(|flag| flag.starts_with(format!("--{}", flag_name).as_str()))
+ }
+
+ pub fn get_flag_value(&self, flag_name: &str) -> Option<String> {
+ self.all_flags
+ .split_ascii_whitespace()
+ .find(|flag| flag.starts_with(format!("--{}", flag_name).as_str()))
+ .and_then(|flag| {
+ let parts: Vec<&str> = flag.split('=').collect();
+ if parts.len() >= 2 {
+ Some(parts[1].to_owned())
+ } else {
+ None
+ }
+ })
+ }
+
+ pub fn debug_enabled(&self) -> bool {
+ self.has_flag("debug")
+ }
+
+ pub fn openssl_enabled(&self) -> bool {
+ self.has_flag("openssl")
+ }
+
+ pub fn prefix_path(&self) -> Option<Box<Path>> {
+ self.get_flag_value("prefix")
+ .map(PathBuf::from)
+ .map(PathBuf::into_boxed_path)
+ }
+
+ fn join_to_prefix_path<S>(&self, sub_path: S) -> Option<Box<Path>>
+ where
+ S: Into<String>,
+ {
+ self.prefix_path()
+ .map(|path| path.join(sub_path.into()).into_boxed_path())
+ }
+
+ pub fn default_control_api_socket_address(&self) -> Option<String> {
+ // If the socket address is specific configured in the configure options, we use
+ // that. Otherwise, we use the default path as assumed to be unix:$prefix/control.unit.sock.
+ match self.get_flag_value("control") {
+ Some(socket_address) => Some(socket_address),
+ None => {
+ // Give up if the unitd is compiled with unix sockets disabled
+ if self.has_flag("no-unix-sockets") {
+ return None;
+ }
+ let socket_path = self.join_to_prefix_path("control.unit.sock");
+ socket_path.map(|path| format!("unix:{}", path.to_string_lossy()))
+ }
+ }
+ }
+
+ pub fn default_pid_path(&self) -> Option<Box<Path>> {
+ match self.get_flag_value("pid") {
+ Some(pid_path) => self.join_to_prefix_path(pid_path),
+ None => self.join_to_prefix_path("unit.pid"),
+ }
+ }
+
+ pub fn default_log_path(&self) -> Option<Box<Path>> {
+ match self.get_flag_value("log") {
+ Some(pid_path) => self.join_to_prefix_path(pid_path),
+ None => self.join_to_prefix_path("unit.log"),
+ }
+ }
+
+ pub fn default_modules_directory(&self) -> Option<Box<Path>> {
+ match self.get_flag_value("modules") {
+ Some(modules_dir_name) => self.join_to_prefix_path(modules_dir_name),
+ None => self.join_to_prefix_path("modules"),
+ }
+ }
+
+ pub fn default_state_directory(&self) -> Option<Box<Path>> {
+ match self.get_flag_value("state") {
+ Some(state_dir_name) => self.join_to_prefix_path(state_dir_name),
+ None => self.join_to_prefix_path("state"),
+ }
+ }
+
+ pub fn default_tmp_directory(&self) -> Option<Box<Path>> {
+ match self.get_flag_value("tmp") {
+ Some(tmp_dir_name) => self.join_to_prefix_path(tmp_dir_name),
+ None => self.join_to_prefix_path("tmp"),
+ }
+ }
+ pub fn default_user(&self) -> Option<String> {
+ self.get_flag_value("user").map(String::from)
+ }
+ pub fn default_group(&self) -> Option<String> {
+ self.get_flag_value("group").map(String::from)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::unitd_instance;
+ use crate::unitd_instance::UNITD_PATH_ENV_KEY;
+
+ #[test]
+ fn can_detect_key() {
+ let options = UnitdConfigureOptions {
+ version: Default::default(),
+ all_flags: Cow::from("--debug --openssl --prefix=/opt/unit"),
+ };
+ assert!(options.has_flag("debug"));
+ assert!(options.has_flag("openssl"));
+ assert!(options.has_flag("prefix"));
+ assert!(!options.has_flag("fobar"));
+ }
+
+ #[test]
+ fn can_get_flag_value_by_key() {
+ let expected = "/opt/unit";
+ let options = UnitdConfigureOptions {
+ version: Default::default(),
+ all_flags: Cow::from("--debug --openssl --prefix=/opt/unit"),
+ };
+
+ let actual = options.get_flag_value("prefix");
+ assert_eq!(expected, actual.unwrap())
+ }
+
+ #[test]
+ fn can_get_prefix_path() {
+ let expected: Box<Path> = Path::new("/opt/unit").into();
+ let options = UnitdConfigureOptions {
+ version: Default::default(),
+ all_flags: Cow::from("--debug --openssl --prefix=/opt/unit"),
+ };
+
+ let actual = options.prefix_path();
+ assert_eq!(expected, actual.unwrap())
+ }
+
+ #[test]
+ fn can_parse_complicated_configure_options() {
+ let expected: Box<Path> = Path::new("/usr").into();
+ let options = UnitdConfigureOptions {
+ version: Default::default(),
+ all_flags: Cow::from("--prefix=/usr --state=/var/lib/unit --control=unix:/var/run/control.unit.sock --pid=/var/run/unit.pid --log=/var/log/unit.log --tmp=/var/tmp --user=unit --group=unit --tests --openssl --modules=/usr/lib/unit/modules --libdir=/usr/lib/x86_64-linux-gnu --cc-opt='-g -O2 -fdebug-prefix-map=/data/builder/debuild/unit-1.28.0/pkg/deb/debuild/unit-1.28.0=. -specs=/usr/share/dpkg/no-pie-compile.specs -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC' --ld-opt='-Wl,-Bsymbolic-functions -specs=/usr/share/dpkg/no-pie-link.specs -Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie'
+"),
+ };
+
+ let actual = options.prefix_path();
+ assert_eq!(expected, actual.unwrap())
+ }
+
+ #[test]
+ fn can_run_unitd() {
+ let specific_path = std::env::var(UNITD_PATH_ENV_KEY).map_err(|error| Box::new(error) as Box<dyn stdError>);
+ let unitd_path = unitd_instance::find_executable_path(specific_path);
+ let config_options = UnitdConfigureOptions::new(&unitd_path.unwrap());
+ match config_options {
+ Ok(options) => {
+ println!("{:?}", options)
+ }
+ Err(error) => panic!("{}", error),
+ };
+ }
+}
diff --git a/tools/unitctl/unit-client-rs/src/unitd_instance.rs b/tools/unitctl/unit-client-rs/src/unitd_instance.rs
new file mode 100644
index 00000000..9467fcb7
--- /dev/null
+++ b/tools/unitctl/unit-client-rs/src/unitd_instance.rs
@@ -0,0 +1,360 @@
+use crate::unit_client::UnitClientError;
+use serde::ser::SerializeMap;
+use serde::{Serialize, Serializer};
+use std::error::Error as StdError;
+use std::path::{Path, PathBuf};
+use std::{fmt, io};
+use which::which;
+
+use crate::runtime_flags::RuntimeFlags;
+use crate::unitd_configure_options::UnitdConfigureOptions;
+use crate::unitd_process::UnitdProcess;
+
+pub const UNITD_PATH_ENV_KEY: &str = "UNITD_PATH";
+pub const UNITD_BINARY_NAMES: [&str; 2] = ["unitd", "unitd-debug"];
+
+#[derive(Debug)]
+pub struct UnitdInstance {
+ pub process: UnitdProcess,
+ pub configure_options: Option<UnitdConfigureOptions>,
+ pub errors: Vec<UnitClientError>,
+}
+
+impl Serialize for UnitdInstance {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let mut state = serializer.serialize_map(Some(15))?;
+ let runtime_flags = self
+ .process
+ .cmd()
+ .and_then(|cmd| cmd.flags)
+ .map(|flags| flags.to_string());
+
+ let configure_flags = self.configure_options.as_ref().map(|opts| opts.all_flags.clone());
+
+ state.serialize_entry("pid", &self.process.process_id)?;
+ state.serialize_entry("version", &self.version())?;
+ state.serialize_entry("user", &self.process.user)?;
+ state.serialize_entry("effective_user", &self.process.effective_user)?;
+ state.serialize_entry("executable", &self.process.executable_path())?;
+ state.serialize_entry("control_socket", &self.control_api_socket_address())?;
+ state.serialize_entry("child_pids", &self.process.child_pids)?;
+ state.serialize_entry("log_path", &self.log_path())?;
+ state.serialize_entry("pid_path", &self.pid_path())?;
+ state.serialize_entry("modules_directory", &self.modules_directory())?;
+ state.serialize_entry("state_directory", &self.state_directory())?;
+ state.serialize_entry("tmp_directory", &self.tmp_directory())?;
+ state.serialize_entry("runtime_flags", &runtime_flags)?;
+ state.serialize_entry("configure_flags", &configure_flags)?;
+ let string_errors = &self.errors.iter().map(|e| e.to_string()).collect::<Vec<String>>();
+ state.serialize_entry("errors", string_errors)?;
+
+ state.end()
+ }
+}
+
+impl UnitdInstance {
+ pub fn running_unitd_instances() -> Vec<UnitdInstance> {
+ Self::collect_unitd_processes(UnitdProcess::find_unitd_processes())
+ }
+
+ /// Find all running unitd processes and convert them into UnitdInstances and filter
+ /// out all errors by printing them to stderr and leaving errored instances out of
+ /// the returned vector.
+ fn collect_unitd_processes(processes: Vec<UnitdProcess>) -> Vec<UnitdInstance> {
+ Self::map_processes_to_instances(processes).into_iter().collect()
+ }
+
+ fn map_processes_to_instances(processes: Vec<UnitdProcess>) -> Vec<UnitdInstance> {
+ fn unitd_path_from_process(process: &UnitdProcess) -> Result<Box<Path>, UnitClientError> {
+ match process.executable_path() {
+ Some(executable_path) => {
+ let is_absolute_working_dir = process
+ .working_dir
+ .as_ref()
+ .map(|p| p.is_absolute())
+ .unwrap_or_default();
+ if executable_path.is_absolute() {
+ Ok(executable_path.to_owned())
+ } else if executable_path.is_relative() && is_absolute_working_dir {
+ let new_path = process
+ .working_dir
+ .as_ref()
+ .unwrap()
+ .join(executable_path)
+ .canonicalize()
+ .map(|path| path.into_boxed_path())
+ .map_err(|error| UnitClientError::UnitdProcessParseError {
+ message: format!("Error canonicalizing unitd executable path: {}", error),
+ pid: process.process_id,
+ })?;
+ Ok(new_path)
+ } else {
+ Err(UnitClientError::UnitdProcessParseError {
+ message: "Unable to get absolute unitd executable path from process".to_string(),
+ pid: process.process_id,
+ })
+ }
+ }
+ None => Err(UnitClientError::UnitdProcessParseError {
+ message: "Unable to get unitd executable path from process".to_string(),
+ pid: process.process_id,
+ }),
+ }
+ }
+
+ fn map_process_to_unitd_instance(process: &UnitdProcess) -> UnitdInstance {
+ match unitd_path_from_process(process) {
+ Ok(unitd_path) => match UnitdConfigureOptions::new(&unitd_path.clone().into_path_buf()) {
+ Ok(configure_options) => UnitdInstance {
+ process: process.to_owned(),
+ configure_options: Some(configure_options),
+ errors: vec![],
+ },
+ Err(error) => {
+ let error = UnitClientError::UnitdProcessExecError {
+ source: error,
+ executable_path: unitd_path.to_string_lossy().parse().unwrap_or_default(),
+ message: "Error running unitd binary to get configure options".to_string(),
+ pid: process.process_id,
+ };
+ UnitdInstance {
+ process: process.to_owned(),
+ configure_options: None,
+ errors: vec![error],
+ }
+ }
+ },
+ Err(err) => UnitdInstance {
+ process: process.to_owned(),
+ configure_options: None,
+ errors: vec![err],
+ },
+ }
+ }
+
+ processes
+ .iter()
+ // This converts processes into a UnitdInstance
+ .map(map_process_to_unitd_instance)
+ .collect()
+ }
+
+ fn version(&self) -> Option<String> {
+ match self.process.cmd()?.version {
+ Some(version) => Some(version),
+ None => self.configure_options.as_ref().map(|opts| opts.version.to_string()),
+ }
+ }
+
+ fn flag_or_default_option<R>(
+ &self,
+ read_flag: fn(RuntimeFlags) -> Option<R>,
+ read_opts: fn(UnitdConfigureOptions) -> Option<R>,
+ ) -> Option<R> {
+ self.process
+ .cmd()?
+ .flags
+ .and_then(read_flag)
+ .or_else(|| self.configure_options.to_owned().and_then(read_opts))
+ }
+
+ pub fn control_api_socket_address(&self) -> Option<String> {
+ self.flag_or_default_option(
+ |flags| flags.control_api_socket_address(),
+ |opts| opts.default_control_api_socket_address(),
+ )
+ }
+
+ pub fn pid_path(&self) -> Option<Box<Path>> {
+ self.flag_or_default_option(|flags| flags.pid_path(), |opts| opts.default_pid_path())
+ }
+
+ pub fn log_path(&self) -> Option<Box<Path>> {
+ self.flag_or_default_option(|flags| flags.log_path(), |opts| opts.default_log_path())
+ }
+
+ pub fn modules_directory(&self) -> Option<Box<Path>> {
+ self.flag_or_default_option(
+ |flags| flags.modules_directory(),
+ |opts| opts.default_modules_directory(),
+ )
+ }
+
+ pub fn state_directory(&self) -> Option<Box<Path>> {
+ self.flag_or_default_option(|flags| flags.state_directory(), |opts| opts.default_state_directory())
+ }
+
+ pub fn tmp_directory(&self) -> Option<Box<Path>> {
+ self.flag_or_default_option(|flags| flags.tmp_directory(), |opts| opts.default_tmp_directory())
+ }
+}
+
+impl fmt::Display for UnitdInstance {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ const UNKNOWN: &str = "[unknown]";
+ let version = self.version().unwrap_or_else(|| String::from("[unknown]"));
+ let runtime_flags = self
+ .process
+ .cmd()
+ .and_then(|cmd| cmd.flags)
+ .map(|flags| flags.to_string())
+ .unwrap_or_else(|| UNKNOWN.into());
+ let configure_flags = self
+ .configure_options
+ .as_ref()
+ .map(|opts| opts.all_flags.clone())
+ .unwrap_or_else(|| UNKNOWN.into());
+ let unitd_path: String = self
+ .process
+ .executable_path()
+ .map(|p| p.to_string_lossy().into())
+ .unwrap_or_else(|| UNKNOWN.into());
+ let working_dir: String = self
+ .process
+ .working_dir
+ .as_ref()
+ .map(|p| p.to_string_lossy().into())
+ .unwrap_or_else(|| UNKNOWN.into());
+ let socket_address = self.control_api_socket_address().unwrap_or_else(|| UNKNOWN.to_string());
+ let child_pids = self
+ .process
+ .child_pids
+ .iter()
+ .map(u64::to_string)
+ .collect::<Vec<String>>()
+ .join(", ");
+
+ writeln!(
+ f,
+ "{} instance [pid: {}, version: {}]:",
+ self.process.binary_name, self.process.process_id, version
+ )?;
+ writeln!(f, " Executable: {}", unitd_path)?;
+ writeln!(f, " Process working directory: {}", working_dir)?;
+ write!(f, " Process ownership: ")?;
+ if let Some(user) = &self.process.user {
+ writeln!(f, "name: {}, uid: {}, gid: {}", user.name, user.uid, user.gid)?;
+ } else {
+ writeln!(f, "{}", UNKNOWN)?;
+ }
+ write!(f, " Process effective ownership: ")?;
+ if let Some(user) = &self.process.effective_user {
+ writeln!(f, "name: {}, uid: {}, gid: {}", user.name, user.uid, user.gid)?;
+ } else {
+ writeln!(f, "{}", UNKNOWN)?;
+ }
+
+ writeln!(f, " API control unix socket: {}", socket_address)?;
+ writeln!(f, " Child processes ids: {}", child_pids)?;
+ writeln!(f, " Runtime flags: {}", runtime_flags)?;
+ write!(f, " Configure options: {}", configure_flags)?;
+
+ if !self.errors.is_empty() {
+ write!(f, "\n Errors:")?;
+ for error in &self.errors {
+ write!(f, "\n {}", error)?;
+ }
+ }
+
+ Ok(())
+ }
+}
+
+pub fn find_executable_path(specific_path: Result<String, Box<dyn StdError>>) -> Result<PathBuf, Box<dyn StdError>> {
+ fn find_unitd_in_system_path() -> Vec<PathBuf> {
+ UNITD_BINARY_NAMES
+ .iter()
+ .map(which)
+ .filter_map(Result::ok)
+ .collect::<Vec<PathBuf>>()
+ }
+
+ match specific_path {
+ Ok(path) => Ok(PathBuf::from(path)),
+ Err(_) => {
+ let unitd_paths = find_unitd_in_system_path();
+ if unitd_paths.is_empty() {
+ let err_msg = format!(
+ "Could not find unitd in system path or in UNITD_PATH environment variable. Searched for: {:?}",
+ UNITD_BINARY_NAMES
+ );
+ let err = io::Error::new(io::ErrorKind::NotFound, err_msg);
+ Err(Box::from(err))
+ } else {
+ Ok(unitd_paths[0].clone())
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use rand::rngs::StdRng;
+ use rand::{RngCore, SeedableRng};
+
+ // We don't need a secure seed for testing, in fact it is better that we have a
+ // predictable value
+ const SEED: [u8; 32] = [
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
+ 30, 31,
+ ];
+ #[test]
+ fn can_find_unitd_instances() {
+ UnitdInstance::running_unitd_instances().iter().for_each(|p| {
+ println!("{:?}", p);
+ println!("Runtime Flags: {:?}", p.process.cmd().map(|c| c.flags));
+ println!("Temp directory: {:?}", p.tmp_directory());
+ })
+ }
+
+ fn mock_process<S: Into<String>>(
+ rng: &mut StdRng,
+ binary_name: S,
+ executable_path: Option<String>,
+ ) -> UnitdProcess {
+ UnitdProcess {
+ process_id: rng.next_u32() as u64,
+ binary_name: binary_name.into(),
+ executable_path: executable_path.map(|p| Box::from(Path::new(&p))),
+ environ: vec![],
+ all_cmds: vec![],
+ working_dir: Some(Box::from(Path::new("/opt/unit"))),
+ child_pids: vec![],
+ user: None,
+ effective_user: None,
+ }
+ }
+
+ #[test]
+ fn will_list_without_errors_valid_processes() {
+ let specific_path = std::env::var(UNITD_PATH_ENV_KEY).map_err(|error| Box::new(error) as Box<dyn StdError>);
+ let binding = match find_executable_path(specific_path) {
+ Ok(path) => path,
+ Err(error) => {
+ eprintln!("Could not find unitd executable path: {} - skipping test", error);
+ return;
+ }
+ };
+ let binary_name = binding
+ .file_name()
+ .expect("Could not get binary name")
+ .to_string_lossy()
+ .to_string();
+ let unitd_path = binding.to_string_lossy();
+ let mut rng: StdRng = SeedableRng::from_seed(SEED);
+
+ let processes = vec![
+ mock_process(&mut rng, &binary_name, Some(unitd_path.to_string())),
+ mock_process(&mut rng, &binary_name, Some(unitd_path.to_string())),
+ ];
+ let instances = UnitdInstance::collect_unitd_processes(processes);
+ // assert_eq!(instances.len(), 3);
+ instances.iter().for_each(|p| {
+ assert_eq!(p.errors.len(), 0, "Expected no errors, got: {:?}", p.errors);
+ })
+ }
+}
diff --git a/tools/unitctl/unit-client-rs/src/unitd_process.rs b/tools/unitctl/unit-client-rs/src/unitd_process.rs
new file mode 100644
index 00000000..b8604e89
--- /dev/null
+++ b/tools/unitctl/unit-client-rs/src/unitd_process.rs
@@ -0,0 +1,170 @@
+use crate::unitd_cmd::UnitdCmd;
+use crate::unitd_instance::UNITD_BINARY_NAMES;
+use crate::unitd_process_user::UnitdProcessUser;
+use std::collections::HashMap;
+use std::path::Path;
+use sysinfo::{Pid, Process, ProcessRefreshKind, System, UpdateKind, Users};
+
+#[derive(Debug, Clone)]
+pub struct UnitdProcess {
+ pub binary_name: String,
+ pub process_id: u64,
+ pub executable_path: Option<Box<Path>>,
+ pub environ: Vec<String>,
+ pub all_cmds: Vec<String>,
+ pub working_dir: Option<Box<Path>>,
+ pub child_pids: Vec<u64>,
+ pub user: Option<UnitdProcessUser>,
+ pub effective_user: Option<UnitdProcessUser>,
+}
+
+impl UnitdProcess {
+ pub fn find_unitd_processes() -> Vec<UnitdProcess> {
+ let process_refresh_kind = ProcessRefreshKind::new()
+ .with_cmd(UpdateKind::Always)
+ .with_cwd(UpdateKind::Always)
+ .with_exe(UpdateKind::Always)
+ .with_user(UpdateKind::Always);
+ let refresh_kind = sysinfo::RefreshKind::new().with_processes(process_refresh_kind);
+ let sys = System::new_with_specifics(refresh_kind);
+ let unitd_processes: HashMap<&Pid, &Process> = sys
+ .processes()
+ .iter()
+ .filter(|p| {
+ let process_name = p.1.name();
+ UNITD_BINARY_NAMES.contains(&process_name)
+ })
+ .collect::<HashMap<&Pid, &Process>>();
+ let users = Users::new_with_refreshed_list();
+
+ unitd_processes
+ .iter()
+ // Filter out child processes
+ .filter(|p| {
+ let parent_pid = p.1.parent();
+ match parent_pid {
+ Some(pid) => !unitd_processes.contains_key(&pid),
+ None => false,
+ }
+ })
+ .map(|p| {
+ let tuple = p.to_owned();
+ /* The sysinfo library only supports 32-bit pids, yet larger values are possible
+ * if the OS is configured to support it, thus we use 64-bit integers internally
+ * because it is just a matter of time until the library changes to larger values. */
+ let pid = *tuple.0;
+ let process = *tuple.1;
+ let process_id: u64 = pid.as_u32().into();
+ let executable_path: Option<Box<Path>> = process.exe().map(|p| p.to_path_buf().into_boxed_path());
+ let environ: Vec<String> = process.environ().into();
+ let cmd: Vec<String> = process.cmd().into();
+ let working_dir: Option<Box<Path>> = process.cwd().map(|p| p.to_path_buf().into_boxed_path());
+ let child_pids = unitd_processes
+ .iter()
+ .filter_map(|p| p.to_owned().1.parent())
+ .filter(|parent_pid| parent_pid == pid)
+ .map(|p| p.as_u32() as u64)
+ .collect::<Vec<u64>>();
+
+ let user = process
+ .user_id()
+ .and_then(|uid| users.get_user_by_id(uid))
+ .map(UnitdProcessUser::from);
+ let effective_user = process
+ .effective_user_id()
+ .and_then(|uid| users.get_user_by_id(uid))
+ .map(UnitdProcessUser::from);
+
+ UnitdProcess {
+ binary_name: process.name().to_string(),
+ process_id,
+ executable_path,
+ environ,
+ all_cmds: cmd,
+ working_dir,
+ child_pids,
+ user,
+ effective_user,
+ }
+ })
+ .collect::<Vec<UnitdProcess>>()
+ }
+
+ pub fn cmd(&self) -> Option<UnitdCmd> {
+ if self.all_cmds.is_empty() {
+ return None;
+ }
+
+ match UnitdCmd::new(self.all_cmds[0].clone(), self.binary_name.as_ref()) {
+ Ok(cmd) => Some(cmd),
+ Err(error) => {
+ eprintln!("Failed to parse process cmd: {}", error);
+ None
+ }
+ }
+ }
+
+ pub fn executable_path(&self) -> Option<Box<Path>> {
+ if self.executable_path.is_some() {
+ return self.executable_path.clone();
+ }
+ self.cmd().and_then(|cmd| cmd.process_executable_path)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn can_parse_runtime_cmd_absolute_path(binary_name: &str) {
+ let cmd = format!(
+ "unit: main v1.28.0 [/usr/sbin/{} --log /var/log/unit.log --pid /var/run/unit.pid]",
+ binary_name
+ );
+ let unitd_cmd = UnitdCmd::new(cmd, binary_name).expect("Failed to parse unitd cmd");
+ assert_eq!(unitd_cmd.version.unwrap(), "1.28.0");
+ assert_eq!(
+ unitd_cmd.process_executable_path.unwrap().to_string_lossy(),
+ format!("/usr/sbin/{}", binary_name)
+ );
+ let flags = unitd_cmd.flags.unwrap();
+ assert_eq!(flags.get_flag_value("log").unwrap(), "/var/log/unit.log");
+ assert_eq!(flags.get_flag_value("pid").unwrap(), "/var/run/unit.pid");
+ }
+
+ fn can_parse_runtime_cmd_relative_path(binary_name: &str) {
+ let cmd = format!(
+ "unit: main v1.29.0 [./sbin/{} --no-daemon --tmp /tmp --something]",
+ binary_name
+ );
+ let unitd_cmd = UnitdCmd::new(cmd, binary_name).expect("Failed to parse unitd cmd");
+ assert_eq!(unitd_cmd.version.unwrap(), "1.29.0");
+ assert_eq!(
+ unitd_cmd.process_executable_path.unwrap().to_string_lossy(),
+ format!("./sbin/{}", binary_name)
+ );
+ let flags = unitd_cmd.flags.unwrap();
+ assert_eq!(flags.get_flag_value("tmp").unwrap(), "/tmp");
+ assert!(flags.has_flag("something"));
+ }
+
+ #[test]
+ fn can_parse_runtime_cmd_unitd_absolute_path() {
+ can_parse_runtime_cmd_absolute_path("unitd");
+ }
+
+ #[test]
+ fn can_parse_runtime_cmd_unitd_debug_absolute_path() {
+ can_parse_runtime_cmd_absolute_path("unitd-debug");
+ }
+
+ #[test]
+ fn can_parse_runtime_cmd_unitd_relative_path() {
+ can_parse_runtime_cmd_relative_path("unitd");
+ }
+
+ #[test]
+ fn can_parse_runtime_cmd_unitd_debug_relative_path() {
+ can_parse_runtime_cmd_relative_path("unitd-debug");
+ }
+}
diff --git a/tools/unitctl/unit-client-rs/src/unitd_process_user.rs b/tools/unitctl/unit-client-rs/src/unitd_process_user.rs
new file mode 100644
index 00000000..c4f9be22
--- /dev/null
+++ b/tools/unitctl/unit-client-rs/src/unitd_process_user.rs
@@ -0,0 +1,36 @@
+use serde::Serialize;
+use std::fmt;
+use std::fmt::Display;
+use sysinfo::User;
+
+#[derive(Debug, Clone, Serialize)]
+pub struct UnitdProcessUser {
+ pub name: String,
+ pub uid: u32,
+ pub gid: u32,
+ pub groups: Vec<String>,
+}
+
+impl Display for UnitdProcessUser {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "name: {}, uid: {}, gid: {}, groups: {}",
+ self.name,
+ self.uid,
+ self.gid,
+ self.groups.join(", ")
+ )
+ }
+}
+
+impl From<&User> for UnitdProcessUser {
+ fn from(user: &User) -> Self {
+ UnitdProcessUser {
+ name: user.name().into(),
+ uid: *user.id().clone(),
+ gid: *user.group_id(),
+ groups: user.groups().iter().map(|g| g.name().into()).collect(),
+ }
+ }
+}
diff --git a/tools/unitctl/unit-openapi/.gitignore b/tools/unitctl/unit-openapi/.gitignore
new file mode 100644
index 00000000..6aa10640
--- /dev/null
+++ b/tools/unitctl/unit-openapi/.gitignore
@@ -0,0 +1,3 @@
+/target/
+**/*.rs.bk
+Cargo.lock
diff --git a/tools/unitctl/unit-openapi/.openapi-generator-ignore b/tools/unitctl/unit-openapi/.openapi-generator-ignore
new file mode 100644
index 00000000..aa9e0e40
--- /dev/null
+++ b/tools/unitctl/unit-openapi/.openapi-generator-ignore
@@ -0,0 +1,27 @@
+# OpenAPI Generator Ignore
+# Generated by openapi-generator https://github.com/openapitools/openapi-generator
+
+# Use this file to prevent files from being overwritten by the generator.
+# The patterns follow closely to .gitignore or .dockerignore.
+
+# As an example, the C# client generator defines ApiClient.cs.
+# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
+#ApiClient.cs
+
+# You can match any string of characters against a directory, file or extension with a single asterisk (*):
+#foo/*/qux
+# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
+
+# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
+#foo/**/qux
+# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
+
+# You can also negate patterns with an exclamation (!).
+# For example, you can ignore all files in a docs folder with the file extension .md:
+#docs/*.md
+# Then explicitly reverse the ignore rule for a single file:
+#!docs/README.md
+
+src/apis/error.rs
+.travis.yml
+git_push.sh \ No newline at end of file
diff --git a/tools/unitctl/unit-openapi/.openapi-generator/FILES b/tools/unitctl/unit-openapi/.openapi-generator/FILES
new file mode 100644
index 00000000..4f177f5f
--- /dev/null
+++ b/tools/unitctl/unit-openapi/.openapi-generator/FILES
@@ -0,0 +1,161 @@
+.gitignore
+Cargo.toml
+README.md
+docs/AccessLogApi.md
+docs/ApplicationsApi.md
+docs/AppsApi.md
+docs/CertBundle.md
+docs/CertBundleChainCert.md
+docs/CertBundleChainCertIssuer.md
+docs/CertBundleChainCertSubj.md
+docs/CertBundleChainCertValidity.md
+docs/CertificatesApi.md
+docs/Config.md
+docs/ConfigAccessLog.md
+docs/ConfigAccessLogObject.md
+docs/ConfigApi.md
+docs/ConfigApplication.md
+docs/ConfigApplicationCommon.md
+docs/ConfigApplicationCommonIsolation.md
+docs/ConfigApplicationCommonIsolationAutomount.md
+docs/ConfigApplicationCommonIsolationCgroup.md
+docs/ConfigApplicationCommonIsolationGidmapInner.md
+docs/ConfigApplicationCommonIsolationNamespaces.md
+docs/ConfigApplicationCommonIsolationUidmapInner.md
+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/ConfigListener.md
+docs/ConfigListenerForwarded.md
+docs/ConfigListenerForwardedSource.md
+docs/ConfigListenerTls.md
+docs/ConfigListenerTlsCertificate.md
+docs/ConfigListenerTlsSession.md
+docs/ConfigListenerTlsSessionTickets.md
+docs/ConfigRouteStep.md
+docs/ConfigRouteStepAction.md
+docs/ConfigRouteStepActionPass.md
+docs/ConfigRouteStepActionProxy.md
+docs/ConfigRouteStepActionReturn.md
+docs/ConfigRouteStepActionShare.md
+docs/ConfigRouteStepMatch.md
+docs/ConfigRouteStepMatchArguments.md
+docs/ConfigRouteStepMatchCookies.md
+docs/ConfigRouteStepMatchHeaders.md
+docs/ConfigRoutes.md
+docs/ConfigSettings.md
+docs/ConfigSettingsHttp.md
+docs/ConfigSettingsHttpStatic.md
+docs/ConfigSettingsHttpStaticMimeType.md
+docs/ControlApi.md
+docs/ListenersApi.md
+docs/RoutesApi.md
+docs/SettingsApi.md
+docs/Status.md
+docs/StatusApi.md
+docs/StatusApplicationsApp.md
+docs/StatusApplicationsAppProcesses.md
+docs/StatusApplicationsAppRequests.md
+docs/StatusConnections.md
+docs/StatusRequests.md
+docs/StringOrStringArray.md
+docs/TlsApi.md
+docs/XffApi.md
+src/apis/access_log_api.rs
+src/apis/applications_api.rs
+src/apis/apps_api.rs
+src/apis/certificates_api.rs
+src/apis/client.rs
+src/apis/config_api.rs
+src/apis/configuration.rs
+src/apis/control_api.rs
+src/apis/listeners_api.rs
+src/apis/mod.rs
+src/apis/request.rs
+src/apis/routes_api.rs
+src/apis/settings_api.rs
+src/apis/status_api.rs
+src/apis/tls_api.rs
+src/apis/xff_api.rs
+src/lib.rs
+src/models/cert_bundle.rs
+src/models/cert_bundle_chain_cert.rs
+src/models/cert_bundle_chain_cert_issuer.rs
+src/models/cert_bundle_chain_cert_subj.rs
+src/models/cert_bundle_chain_cert_validity.rs
+src/models/config.rs
+src/models/config_access_log.rs
+src/models/config_access_log_object.rs
+src/models/config_application.rs
+src/models/config_application_common.rs
+src/models/config_application_common_isolation.rs
+src/models/config_application_common_isolation_automount.rs
+src/models/config_application_common_isolation_cgroup.rs
+src/models/config_application_common_isolation_gidmap_inner.rs
+src/models/config_application_common_isolation_namespaces.rs
+src/models/config_application_common_isolation_uidmap_inner.rs
+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_listener.rs
+src/models/config_listener_forwarded.rs
+src/models/config_listener_forwarded_source.rs
+src/models/config_listener_tls.rs
+src/models/config_listener_tls_certificate.rs
+src/models/config_listener_tls_session.rs
+src/models/config_listener_tls_session_tickets.rs
+src/models/config_route_step.rs
+src/models/config_route_step_action.rs
+src/models/config_route_step_action_pass.rs
+src/models/config_route_step_action_proxy.rs
+src/models/config_route_step_action_return.rs
+src/models/config_route_step_action_share.rs
+src/models/config_route_step_match.rs
+src/models/config_route_step_match_arguments.rs
+src/models/config_route_step_match_cookies.rs
+src/models/config_route_step_match_headers.rs
+src/models/config_routes.rs
+src/models/config_settings.rs
+src/models/config_settings_http.rs
+src/models/config_settings_http_static.rs
+src/models/config_settings_http_static_mime_type.rs
+src/models/mod.rs
+src/models/status.rs
+src/models/status_applications_app.rs
+src/models/status_applications_app_processes.rs
+src/models/status_applications_app_requests.rs
+src/models/status_connections.rs
+src/models/status_requests.rs
+src/models/string_or_string_array.rs
diff --git a/tools/unitctl/unit-openapi/.openapi-generator/VERSION b/tools/unitctl/unit-openapi/.openapi-generator/VERSION
new file mode 100644
index 00000000..cd802a1e
--- /dev/null
+++ b/tools/unitctl/unit-openapi/.openapi-generator/VERSION
@@ -0,0 +1 @@
+6.6.0 \ No newline at end of file
diff --git a/tools/unitctl/unit-openapi/Cargo.toml b/tools/unitctl/unit-openapi/Cargo.toml
new file mode 100644
index 00000000..12435985
--- /dev/null
+++ b/tools/unitctl/unit-openapi/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "unit-openapi"
+version = "0.4.0-beta"
+authors = ["unit-owner@nginx.org"]
+description = "NGINX Unit is a lightweight and versatile application runtime that provides the essential components for your web application as a single open-source server: running application code, serving static assets, handling TLS and request routing. **Important**: Unit's API is designed to expose any part of its configuration as an addressable endpoint. Suppose a JSON object is stored at `/config/listeners/`: ```json { \"*:8080\": { \"pass\": \"applications/wp_emea_dev\" } } ``` Here, `/config/listeners/_*:8080` and `/config/listeners/_*:8080/pass` are also endpoints. Generally, object options are addressable by their names, array items—by their indexes (`/array/0/`). **Note**: By default, Unit is configured through a UNIX domain socket. To use this specification with OpenAPI tools interactively, [start](https://unit.nginx.org/howto/source/#source-startup) Unit with a TCP port as the control socket."
+license = "Apache 2.0"
+edition = "2018"
+
+[dependencies]
+serde = "1.0"
+serde_derive = "1.0"
+serde_json = "1.0"
+url = "2.2"
+hyper = { version = "0.14" }
+http = "0.2"
+base64 = "0.21"
+futures = "0.3"
diff --git a/tools/unitctl/unit-openapi/README.md b/tools/unitctl/unit-openapi/README.md
new file mode 100644
index 00000000..b8506bda
--- /dev/null
+++ b/tools/unitctl/unit-openapi/README.md
@@ -0,0 +1,411 @@
+# Rust API client for unit-openapi
+
+NGINX Unit is a lightweight and versatile application runtime that provides the essential components for your web application as a single open-source server: running application code, serving static assets, handling TLS and request routing.
+
+
+**Important**: Unit's API is designed to expose any part of its configuration as an addressable endpoint. Suppose a JSON object is stored at `/config/listeners/`:
+
+
+```json { \"*:8080\": { \"pass\": \"applications/wp_emea_dev\" } } ```
+
+Here, `/config/listeners/_*:8080` and `/config/listeners/_*:8080/pass` are also endpoints. Generally, object options are addressable by their names, array items—by their indexes (`/array/0/`).
+
+
+
+**Note**: By default, Unit is configured through a UNIX domain socket. To use this specification with OpenAPI tools interactively, [start](https://unit.nginx.org/howto/source/#source-startup) Unit with a TCP port as the control socket.
+
+For more information, please visit [https://unit.nginx.org/](https://unit.nginx.org/)
+
+## Overview
+
+This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API client.
+
+- API version: 0.2.0
+- Package version: 0.4.0-beta
+- Build package: `org.openapitools.codegen.languages.RustClientCodegen`
+
+## Installation
+
+Put the package under your project folder in a directory named `unit-openapi` and add the following to `Cargo.toml` under `[dependencies]`:
+
+```
+unit-openapi = { path = "./unit-openapi" }
+```
+
+## Documentation for API Endpoints
+
+All URIs are relative to *http://localhost:8080*
+
+Class | Method | HTTP request | Description
+------------ | ------------- | ------------- | -------------
+*AccessLogApi* | [**delete_access_log**](docs/AccessLogApi.md#delete_access_log) | **Delete** /config/access_log | Delete the access log
+*AccessLogApi* | [**delete_access_log_format**](docs/AccessLogApi.md#delete_access_log_format) | **Delete** /config/access_log/format | Delete the access log format
+*AccessLogApi* | [**delete_access_log_path**](docs/AccessLogApi.md#delete_access_log_path) | **Delete** /config/access_log/path | Delete the access log path
+*AccessLogApi* | [**get_access_log**](docs/AccessLogApi.md#get_access_log) | **Get** /config/access_log | Retrieve the access log
+*AccessLogApi* | [**get_access_log_format**](docs/AccessLogApi.md#get_access_log_format) | **Get** /config/access_log/format | Retrieve the access log format option
+*AccessLogApi* | [**get_access_log_path**](docs/AccessLogApi.md#get_access_log_path) | **Get** /config/access_log/path | Retrieve the access log path option
+*AccessLogApi* | [**update_access_log**](docs/AccessLogApi.md#update_access_log) | **Put** /config/access_log | Create or overwrite the access log
+*AccessLogApi* | [**update_access_log_format**](docs/AccessLogApi.md#update_access_log_format) | **Put** /config/access_log/format | Create or overwrite the access log format
+*AccessLogApi* | [**update_access_log_path**](docs/AccessLogApi.md#update_access_log_path) | **Put** /config/access_log/path | Create or overwrite the access log path
+*ApplicationsApi* | [**delete_application**](docs/ApplicationsApi.md#delete_application) | **Delete** /config/applications/{appName} | Delete the application object
+*ApplicationsApi* | [**delete_applications**](docs/ApplicationsApi.md#delete_applications) | **Delete** /config/applications | Delete the applications object
+*ApplicationsApi* | [**get_application**](docs/ApplicationsApi.md#get_application) | **Get** /config/applications/{appName} | Retrieve an application object
+*ApplicationsApi* | [**get_applications**](docs/ApplicationsApi.md#get_applications) | **Get** /config/applications | Retrieve the applications object
+*ApplicationsApi* | [**update_application**](docs/ApplicationsApi.md#update_application) | **Put** /config/applications/{appName} | Create or overwrite the application object
+*ApplicationsApi* | [**update_applications**](docs/ApplicationsApi.md#update_applications) | **Put** /config/applications | Overwrite the applications object
+*AppsApi* | [**get_app_restart**](docs/AppsApi.md#get_app_restart) | **Get** /control/applications/{appName}/restart | Restart the {appName} application
+*CertificatesApi* | [**get_cert_bundle**](docs/CertificatesApi.md#get_cert_bundle) | **Get** /certificates/{bundleName} | Retrieve the certificate bundle object
+*CertificatesApi* | [**get_cert_bundle_chain**](docs/CertificatesApi.md#get_cert_bundle_chain) | **Get** /certificates/{bundleName}/chain | Retrieve the certificate bundle chain
+*CertificatesApi* | [**get_cert_bundle_chain_cert**](docs/CertificatesApi.md#get_cert_bundle_chain_cert) | **Get** /certificates/{bundleName}/chain/{arrayIndex} | Retrieve certificate object from the chain array
+*CertificatesApi* | [**get_cert_bundle_chain_cert_issuer**](docs/CertificatesApi.md#get_cert_bundle_chain_cert_issuer) | **Get** /certificates/{bundleName}/chain/{arrayIndex}/issuer | Retrieve the issuer object from the certificate object
+*CertificatesApi* | [**get_cert_bundle_chain_cert_issuer_cn**](docs/CertificatesApi.md#get_cert_bundle_chain_cert_issuer_cn) | **Get** /certificates/{bundleName}/chain/{arrayIndex}/issuer/common_name | Retrieve the common name from the certificate issuer
+*CertificatesApi* | [**get_cert_bundle_chain_cert_issuer_org**](docs/CertificatesApi.md#get_cert_bundle_chain_cert_issuer_org) | **Get** /certificates/{bundleName}/chain/{arrayIndex}/issuer/organization | Retrieve the organization name from the certificate issuer
+*CertificatesApi* | [**get_cert_bundle_chain_cert_issuer_state**](docs/CertificatesApi.md#get_cert_bundle_chain_cert_issuer_state) | **Get** /certificates/{bundleName}/chain/{arrayIndex}/issuer/state_or_province | Retrieve the state or province code from the certificate issuer
+*CertificatesApi* | [**get_cert_bundle_chain_cert_subj**](docs/CertificatesApi.md#get_cert_bundle_chain_cert_subj) | **Get** /certificates/{bundleName}/chain/{arrayIndex}/subject | Retrieve the subject from the certificate object
+*CertificatesApi* | [**get_cert_bundle_chain_cert_subj_alt**](docs/CertificatesApi.md#get_cert_bundle_chain_cert_subj_alt) | **Get** /certificates/{bundleName}/chain/{arrayIndex}/subject/alt_names/{arrayIndex2} | Retrieve an alternative name from the certificate subject
+*CertificatesApi* | [**get_cert_bundle_chain_cert_subj_alt_array**](docs/CertificatesApi.md#get_cert_bundle_chain_cert_subj_alt_array) | **Get** /certificates/{bundleName}/chain/{arrayIndex}/subject/alt_names | Retrieve the alternative names array from the certificate subject
+*CertificatesApi* | [**get_cert_bundle_chain_cert_subj_cn**](docs/CertificatesApi.md#get_cert_bundle_chain_cert_subj_cn) | **Get** /certificates/{bundleName}/chain/{arrayIndex}/subject/common_name | Retrieve the common name from the certificate subject
+*CertificatesApi* | [**get_cert_bundle_chain_cert_subj_country**](docs/CertificatesApi.md#get_cert_bundle_chain_cert_subj_country) | **Get** /certificates/{bundleName}/chain/{arrayIndex}/subject/country | Retrieve the country code from the certificate subject
+*CertificatesApi* | [**get_cert_bundle_chain_cert_subj_org**](docs/CertificatesApi.md#get_cert_bundle_chain_cert_subj_org) | **Get** /certificates/{bundleName}/chain/{arrayIndex}/subject/organization | Retrieve the organization name from the certificate subject
+*CertificatesApi* | [**get_cert_bundle_chain_cert_subj_state**](docs/CertificatesApi.md#get_cert_bundle_chain_cert_subj_state) | **Get** /certificates/{bundleName}/chain/{arrayIndex}/subject/state_or_province | Retrieve the state or province code from the certificate subject
+*CertificatesApi* | [**get_cert_bundle_chain_cert_valid**](docs/CertificatesApi.md#get_cert_bundle_chain_cert_valid) | **Get** /certificates/{bundleName}/chain/{arrayIndex}/validity | Retrieve the validity object from the certificate object
+*CertificatesApi* | [**get_cert_bundle_chain_cert_valid_since**](docs/CertificatesApi.md#get_cert_bundle_chain_cert_valid_since) | **Get** /certificates/{bundleName}/chain/{arrayIndex}/validity/since | Retrieve the starting time of certificate validity
+*CertificatesApi* | [**get_cert_bundle_chain_cert_valid_until**](docs/CertificatesApi.md#get_cert_bundle_chain_cert_valid_until) | **Get** /certificates/{bundleName}/chain/{arrayIndex}/validity/until | Retrieve the ending time of certificate validity
+*CertificatesApi* | [**get_cert_bundle_chain_certissuer_country**](docs/CertificatesApi.md#get_cert_bundle_chain_certissuer_country) | **Get** /certificates/{bundleName}/chain/{arrayIndex}/issuer/country | Retrieve the country code from the certificate issuer
+*CertificatesApi* | [**get_cert_bundle_key**](docs/CertificatesApi.md#get_cert_bundle_key) | **Get** /certificates/{bundleName}/key | Retrieve the certificate bundle key type
+*CertificatesApi* | [**get_certs**](docs/CertificatesApi.md#get_certs) | **Get** /certificates | Retrieve the certificates object
+*CertificatesApi* | [**put_cert_bundle**](docs/CertificatesApi.md#put_cert_bundle) | **Put** /certificates/{bundleName} | Create or overwrite the actual certificate bundle
+*ConfigApi* | [**delete_access_log**](docs/ConfigApi.md#delete_access_log) | **Delete** /config/access_log | Delete the access log
+*ConfigApi* | [**delete_access_log_format**](docs/ConfigApi.md#delete_access_log_format) | **Delete** /config/access_log/format | Delete the access log format
+*ConfigApi* | [**delete_access_log_path**](docs/ConfigApi.md#delete_access_log_path) | **Delete** /config/access_log/path | Delete the access log path
+*ConfigApi* | [**delete_application**](docs/ConfigApi.md#delete_application) | **Delete** /config/applications/{appName} | Delete the application object
+*ConfigApi* | [**delete_applications**](docs/ConfigApi.md#delete_applications) | **Delete** /config/applications | Delete the applications object
+*ConfigApi* | [**delete_config**](docs/ConfigApi.md#delete_config) | **Delete** /config | Delete the config object
+*ConfigApi* | [**delete_listener**](docs/ConfigApi.md#delete_listener) | **Delete** /config/listeners/{listenerName} | Delete a listener object
+*ConfigApi* | [**delete_listener_forwarded_recursive**](docs/ConfigApi.md#delete_listener_forwarded_recursive) | **Delete** /config/listeners/{listenerName}/forwarded/recursive | Delete the recursive object in a listener
+*ConfigApi* | [**delete_listener_forwarded_source**](docs/ConfigApi.md#delete_listener_forwarded_source) | **Delete** /config/listeners/{listenerName}/forwarded/source/{arrayIndex} | Delete a source array item in a listener
+*ConfigApi* | [**delete_listener_forwarded_sources**](docs/ConfigApi.md#delete_listener_forwarded_sources) | **Delete** /config/listeners/{listenerName}/forwarded/source | Delete the source option in a listener
+*ConfigApi* | [**delete_listener_forwared**](docs/ConfigApi.md#delete_listener_forwared) | **Delete** /config/listeners/{listenerName}/forwarded | Delete the forwarded object in a listener
+*ConfigApi* | [**delete_listener_tls**](docs/ConfigApi.md#delete_listener_tls) | **Delete** /config/listeners/{listenerName}/tls | Delete the tls object in a listener
+*ConfigApi* | [**delete_listener_tls_certificate**](docs/ConfigApi.md#delete_listener_tls_certificate) | **Delete** /config/listeners/{listenerName}/tls/certificate/{arrayIndex} | Delete a certificate array item in a listener
+*ConfigApi* | [**delete_listener_tls_certificates**](docs/ConfigApi.md#delete_listener_tls_certificates) | **Delete** /config/listeners/{listenerName}/tls/certificate | Delete the certificate option in a listener
+*ConfigApi* | [**delete_listener_tls_conf_commands**](docs/ConfigApi.md#delete_listener_tls_conf_commands) | **Delete** /config/listeners/{listenerName}/tls/conf_commands | Delete the conf_commands object in a listener
+*ConfigApi* | [**delete_listener_tls_session**](docs/ConfigApi.md#delete_listener_tls_session) | **Delete** /config/listeners/{listenerName}/tls/session | Delete the session object in a listener
+*ConfigApi* | [**delete_listener_tls_session_ticket**](docs/ConfigApi.md#delete_listener_tls_session_ticket) | **Delete** /config/listeners/{listenerName}/tls/session/tickets/{arrayIndex} | Delete a ticket array item in a listener
+*ConfigApi* | [**delete_listener_tls_session_tickets**](docs/ConfigApi.md#delete_listener_tls_session_tickets) | **Delete** /config/listeners/{listenerName}/tls/session/tickets | Delete the tickets option in a listener
+*ConfigApi* | [**delete_listeners**](docs/ConfigApi.md#delete_listeners) | **Delete** /config/listeners | Delete all the listeners
+*ConfigApi* | [**delete_routes**](docs/ConfigApi.md#delete_routes) | **Delete** /config/routes | Delete the routes entity
+*ConfigApi* | [**delete_settings**](docs/ConfigApi.md#delete_settings) | **Delete** /config/settings | Delete the settings object
+*ConfigApi* | [**delete_settings_discard_unsafe_fields**](docs/ConfigApi.md#delete_settings_discard_unsafe_fields) | **Delete** /config/settings/http/discard_unsafe_fields | Delete the discard_unsafe_fields option
+*ConfigApi* | [**delete_settings_http**](docs/ConfigApi.md#delete_settings_http) | **Delete** /config/settings/http | Delete the http object
+*ConfigApi* | [**delete_settings_http_body_read_timeout**](docs/ConfigApi.md#delete_settings_http_body_read_timeout) | **Delete** /config/settings/http/body_read_timeout | Delete the body_read_timeout option
+*ConfigApi* | [**delete_settings_http_header_read_timeout**](docs/ConfigApi.md#delete_settings_http_header_read_timeout) | **Delete** /config/settings/http/header_read_timeout | Delete the header_read_timeout option
+*ConfigApi* | [**delete_settings_http_idle_timeout**](docs/ConfigApi.md#delete_settings_http_idle_timeout) | **Delete** /config/settings/http/idle_timeout | Delete the idle_timeout option
+*ConfigApi* | [**delete_settings_http_max_body_size**](docs/ConfigApi.md#delete_settings_http_max_body_size) | **Delete** /config/settings/http/max_body_size | Delete the max_body_size option
+*ConfigApi* | [**delete_settings_http_send_timeout**](docs/ConfigApi.md#delete_settings_http_send_timeout) | **Delete** /config/settings/http/send_timeout | Delete the send_timeout option
+*ConfigApi* | [**delete_settings_http_static**](docs/ConfigApi.md#delete_settings_http_static) | **Delete** /config/settings/http/static | Delete the static object
+*ConfigApi* | [**delete_settings_http_static_mime_type**](docs/ConfigApi.md#delete_settings_http_static_mime_type) | **Delete** /config/settings/http/static/mime_types/{mimeType} | Delete the MIME type option
+*ConfigApi* | [**delete_settings_http_static_mime_types**](docs/ConfigApi.md#delete_settings_http_static_mime_types) | **Delete** /config/settings/http/static/mime_types | Delete the mime_types object
+*ConfigApi* | [**delete_settings_log_route**](docs/ConfigApi.md#delete_settings_log_route) | **Delete** /config/settings/http/log_route | Delete the log_route option
+*ConfigApi* | [**delete_settings_server_version**](docs/ConfigApi.md#delete_settings_server_version) | **Delete** /config/settings/http/server_version | Delete the server_version option
+*ConfigApi* | [**get_access_log**](docs/ConfigApi.md#get_access_log) | **Get** /config/access_log | Retrieve the access log
+*ConfigApi* | [**get_access_log_format**](docs/ConfigApi.md#get_access_log_format) | **Get** /config/access_log/format | Retrieve the access log format option
+*ConfigApi* | [**get_access_log_path**](docs/ConfigApi.md#get_access_log_path) | **Get** /config/access_log/path | Retrieve the access log path option
+*ConfigApi* | [**get_application**](docs/ConfigApi.md#get_application) | **Get** /config/applications/{appName} | Retrieve an application object
+*ConfigApi* | [**get_applications**](docs/ConfigApi.md#get_applications) | **Get** /config/applications | Retrieve the applications object
+*ConfigApi* | [**get_config**](docs/ConfigApi.md#get_config) | **Get** /config | Retrieve the config
+*ConfigApi* | [**get_listener**](docs/ConfigApi.md#get_listener) | **Get** /config/listeners/{listenerName} | Retrieve a listener object
+*ConfigApi* | [**get_listener_forwarded**](docs/ConfigApi.md#get_listener_forwarded) | **Get** /config/listeners/{listenerName}/forwarded | Retrieve the forwarded object in a listener
+*ConfigApi* | [**get_listener_forwarded_client_ip**](docs/ConfigApi.md#get_listener_forwarded_client_ip) | **Get** /config/listeners/{listenerName}/forwarded/client_ip | Retrieve the client_ip option in a listener
+*ConfigApi* | [**get_listener_forwarded_protocol**](docs/ConfigApi.md#get_listener_forwarded_protocol) | **Get** /config/listeners/{listenerName}/forwarded/protocol | Retrieve the protocol option in a listener
+*ConfigApi* | [**get_listener_forwarded_recursive**](docs/ConfigApi.md#get_listener_forwarded_recursive) | **Get** /config/listeners/{listenerName}/forwarded/recursive | Retrieve the recursive option in a listener
+*ConfigApi* | [**get_listener_forwarded_source**](docs/ConfigApi.md#get_listener_forwarded_source) | **Get** /config/listeners/{listenerName}/forwarded/source/{arrayIndex} | Retrieve a source array item in a listener
+*ConfigApi* | [**get_listener_pass**](docs/ConfigApi.md#get_listener_pass) | **Get** /config/listeners/{listenerName}/pass | Retrieve the pass option in a listener
+*ConfigApi* | [**get_listener_tls**](docs/ConfigApi.md#get_listener_tls) | **Get** /config/listeners/{listenerName}/tls | Retrieve the tls object in a listener
+*ConfigApi* | [**get_listener_tls_certificate**](docs/ConfigApi.md#get_listener_tls_certificate) | **Get** /config/listeners/{listenerName}/tls/certificate/{arrayIndex} | Retrieve a certificate array item in a listener
+*ConfigApi* | [**get_listener_tls_session**](docs/ConfigApi.md#get_listener_tls_session) | **Get** /config/listeners/{listenerName}/tls/session | Retrieve the session object in a listener
+*ConfigApi* | [**get_listener_tls_session_ticket**](docs/ConfigApi.md#get_listener_tls_session_ticket) | **Get** /config/listeners/{listenerName}/tls/session/tickets/{arrayIndex} | Retrieve a ticket array item in a listener
+*ConfigApi* | [**get_listeners**](docs/ConfigApi.md#get_listeners) | **Get** /config/listeners | Retrieve all the listeners
+*ConfigApi* | [**get_routes**](docs/ConfigApi.md#get_routes) | **Get** /config/routes | Retrieve the routes entity
+*ConfigApi* | [**get_settings**](docs/ConfigApi.md#get_settings) | **Get** /config/settings | Retrieve the settings object
+*ConfigApi* | [**get_settings_discard_unsafe_fields**](docs/ConfigApi.md#get_settings_discard_unsafe_fields) | **Get** /config/settings/http/discard_unsafe_fields | Retrieve the discard_unsafe_fields option from http settings
+*ConfigApi* | [**get_settings_http**](docs/ConfigApi.md#get_settings_http) | **Get** /config/settings/http | Retrieve the http object from settings
+*ConfigApi* | [**get_settings_http_body_read_timeout**](docs/ConfigApi.md#get_settings_http_body_read_timeout) | **Get** /config/settings/http/body_read_timeout | Retrieve the body_read_timeout option from http settings
+*ConfigApi* | [**get_settings_http_header_read_timeout**](docs/ConfigApi.md#get_settings_http_header_read_timeout) | **Get** /config/settings/http/header_read_timeout | Retrieve the header_read_timeout option from http settings
+*ConfigApi* | [**get_settings_http_idle_timeout**](docs/ConfigApi.md#get_settings_http_idle_timeout) | **Get** /config/settings/http/idle_timeout | Retrieve the idle_timeout option from http settings
+*ConfigApi* | [**get_settings_http_max_body_size**](docs/ConfigApi.md#get_settings_http_max_body_size) | **Get** /config/settings/http/max_body_size | Retrieve the max_body_size option from http settings
+*ConfigApi* | [**get_settings_http_send_timeout**](docs/ConfigApi.md#get_settings_http_send_timeout) | **Get** /config/settings/http/send_timeout | Retrieve the send_timeout option from http settings
+*ConfigApi* | [**get_settings_http_static**](docs/ConfigApi.md#get_settings_http_static) | **Get** /config/settings/http/static | Retrieve the static object from http settings
+*ConfigApi* | [**get_settings_http_static_mime_type**](docs/ConfigApi.md#get_settings_http_static_mime_type) | **Get** /config/settings/http/static/mime_types/{mimeType} | Retrieve the MIME type option from MIME type settings
+*ConfigApi* | [**get_settings_http_static_mime_types**](docs/ConfigApi.md#get_settings_http_static_mime_types) | **Get** /config/settings/http/static/mime_types | Retrieve the mime_types object from static settings
+*ConfigApi* | [**get_settings_log_route**](docs/ConfigApi.md#get_settings_log_route) | **Get** /config/settings/http/log_route | Retrieve the log_route option from http settings
+*ConfigApi* | [**get_settings_server_version**](docs/ConfigApi.md#get_settings_server_version) | **Get** /config/settings/http/server_version | Retrieve the server_version option from http settings
+*ConfigApi* | [**insert_listener_forwarded_source**](docs/ConfigApi.md#insert_listener_forwarded_source) | **Post** /config/listeners/{listenerName}/forwarded/source | Add a new source array item in a listener
+*ConfigApi* | [**insert_listener_tls_certificate**](docs/ConfigApi.md#insert_listener_tls_certificate) | **Post** /config/listeners/{listenerName}/tls/certificate | Add a new certificate array item in a listener
+*ConfigApi* | [**insert_listener_tls_session_ticket**](docs/ConfigApi.md#insert_listener_tls_session_ticket) | **Post** /config/listeners/{listenerName}/tls/session/tickets | Add a new tickets array item in a listener
+*ConfigApi* | [**list_listener_forwarded_sources**](docs/ConfigApi.md#list_listener_forwarded_sources) | **Get** /config/listeners/{listenerName}/forwarded/source | Retrieve the source option in a listener
+*ConfigApi* | [**list_listener_tls_certificates**](docs/ConfigApi.md#list_listener_tls_certificates) | **Get** /config/listeners/{listenerName}/tls/certificate | Retrieve the certificate option in a listener
+*ConfigApi* | [**list_listener_tls_conf_commands**](docs/ConfigApi.md#list_listener_tls_conf_commands) | **Get** /config/listeners/{listenerName}/tls/conf_commands | Retrieve the conf_commands object in a listener
+*ConfigApi* | [**list_listener_tls_session_tickets**](docs/ConfigApi.md#list_listener_tls_session_tickets) | **Get** /config/listeners/{listenerName}/tls/session/tickets | Retrieve the tickets option in a listener
+*ConfigApi* | [**update_access_log**](docs/ConfigApi.md#update_access_log) | **Put** /config/access_log | Create or overwrite the access log
+*ConfigApi* | [**update_access_log_format**](docs/ConfigApi.md#update_access_log_format) | **Put** /config/access_log/format | Create or overwrite the access log format
+*ConfigApi* | [**update_access_log_path**](docs/ConfigApi.md#update_access_log_path) | **Put** /config/access_log/path | Create or overwrite the access log path
+*ConfigApi* | [**update_application**](docs/ConfigApi.md#update_application) | **Put** /config/applications/{appName} | Create or overwrite the application object
+*ConfigApi* | [**update_applications**](docs/ConfigApi.md#update_applications) | **Put** /config/applications | Overwrite the applications object
+*ConfigApi* | [**update_config**](docs/ConfigApi.md#update_config) | **Put** /config | Create or overwrite the config
+*ConfigApi* | [**update_listener**](docs/ConfigApi.md#update_listener) | **Put** /config/listeners/{listenerName} | Create or overwrite a listener object
+*ConfigApi* | [**update_listener_forwarded**](docs/ConfigApi.md#update_listener_forwarded) | **Put** /config/listeners/{listenerName}/forwarded | Create or overwrite the forwarded object in a listener
+*ConfigApi* | [**update_listener_forwarded_client_ip**](docs/ConfigApi.md#update_listener_forwarded_client_ip) | **Put** /config/listeners/{listenerName}/forwarded/client_ip | Create or overwrite the client_ip option in a listener
+*ConfigApi* | [**update_listener_forwarded_protocol**](docs/ConfigApi.md#update_listener_forwarded_protocol) | **Put** /config/listeners/{listenerName}/forwarded/protocol | Create or overwrite the protocol option in a listener
+*ConfigApi* | [**update_listener_forwarded_recursive**](docs/ConfigApi.md#update_listener_forwarded_recursive) | **Put** /config/listeners/{listenerName}/forwarded/recursive | Create or overwrite the recursive option in a listener
+*ConfigApi* | [**update_listener_forwarded_source**](docs/ConfigApi.md#update_listener_forwarded_source) | **Put** /config/listeners/{listenerName}/forwarded/source/{arrayIndex} | Update a source array item in a listener
+*ConfigApi* | [**update_listener_forwarded_sources**](docs/ConfigApi.md#update_listener_forwarded_sources) | **Put** /config/listeners/{listenerName}/forwarded/source | Create or overwrite the source option in a listener
+*ConfigApi* | [**update_listener_pass**](docs/ConfigApi.md#update_listener_pass) | **Put** /config/listeners/{listenerName}/pass | Update the pass option in a listener
+*ConfigApi* | [**update_listener_tls**](docs/ConfigApi.md#update_listener_tls) | **Put** /config/listeners/{listenerName}/tls | Create or overwrite the tls object in a listener
+*ConfigApi* | [**update_listener_tls_certificate**](docs/ConfigApi.md#update_listener_tls_certificate) | **Put** /config/listeners/{listenerName}/tls/certificate/{arrayIndex} | Update a certificate array item in a listener
+*ConfigApi* | [**update_listener_tls_certificates**](docs/ConfigApi.md#update_listener_tls_certificates) | **Put** /config/listeners/{listenerName}/tls/certificate | Create or overwrite the certificate option in a listener
+*ConfigApi* | [**update_listener_tls_conf_commands**](docs/ConfigApi.md#update_listener_tls_conf_commands) | **Put** /config/listeners/{listenerName}/tls/conf_commands | Create or overwrite the conf_commands object in a listener
+*ConfigApi* | [**update_listener_tls_session**](docs/ConfigApi.md#update_listener_tls_session) | **Put** /config/listeners/{listenerName}/tls/session | Create or overwrite the session object in a listener
+*ConfigApi* | [**update_listener_tls_session_ticket**](docs/ConfigApi.md#update_listener_tls_session_ticket) | **Put** /config/listeners/{listenerName}/tls/session/tickets/{arrayIndex} | Create or overwrite a ticket array item in a listener
+*ConfigApi* | [**update_listener_tls_session_tickets**](docs/ConfigApi.md#update_listener_tls_session_tickets) | **Put** /config/listeners/{listenerName}/tls/session/tickets | Create or overwrite the tickets option in a listener
+*ConfigApi* | [**update_listeners**](docs/ConfigApi.md#update_listeners) | **Put** /config/listeners | Create or overwrite all the listeners
+*ConfigApi* | [**update_routes**](docs/ConfigApi.md#update_routes) | **Put** /config/routes | Overwrite the routes entity
+*ConfigApi* | [**update_settings**](docs/ConfigApi.md#update_settings) | **Put** /config/settings | Create or overwrite the settings object
+*ConfigApi* | [**update_settings_discard_unsafe_fields**](docs/ConfigApi.md#update_settings_discard_unsafe_fields) | **Put** /config/settings/http/discard_unsafe_fields | Create or overwrite the discard_unsafe_fields option
+*ConfigApi* | [**update_settings_http**](docs/ConfigApi.md#update_settings_http) | **Put** /config/settings/http | Create or overwrite the http object
+*ConfigApi* | [**update_settings_http_body_read_timeout**](docs/ConfigApi.md#update_settings_http_body_read_timeout) | **Put** /config/settings/http/body_read_timeout | Create or overwrite the body_read_timeout option
+*ConfigApi* | [**update_settings_http_header_read_timeout**](docs/ConfigApi.md#update_settings_http_header_read_timeout) | **Put** /config/settings/http/header_read_timeout | Create or overwrite the header_read_timeout option
+*ConfigApi* | [**update_settings_http_idle_timeout**](docs/ConfigApi.md#update_settings_http_idle_timeout) | **Put** /config/settings/http/idle_timeout | Create or overwrite the idle_timeout option
+*ConfigApi* | [**update_settings_http_max_body_size**](docs/ConfigApi.md#update_settings_http_max_body_size) | **Put** /config/settings/http/max_body_size | Create or overwrite the max_body_size option
+*ConfigApi* | [**update_settings_http_send_timeout**](docs/ConfigApi.md#update_settings_http_send_timeout) | **Put** /config/settings/http/send_timeout | Create or overwrite the send_timeout option
+*ConfigApi* | [**update_settings_http_static**](docs/ConfigApi.md#update_settings_http_static) | **Put** /config/settings/http/static | Create or overwrite the static object
+*ConfigApi* | [**update_settings_http_static_mime_type**](docs/ConfigApi.md#update_settings_http_static_mime_type) | **Put** /config/settings/http/static/mime_types/{mimeType} | Create or overwrite the MIME type option
+*ConfigApi* | [**update_settings_http_static_mime_types**](docs/ConfigApi.md#update_settings_http_static_mime_types) | **Put** /config/settings/http/static/mime_types | Create or overwrite the mime_types object
+*ConfigApi* | [**update_settings_log_route**](docs/ConfigApi.md#update_settings_log_route) | **Put** /config/settings/http/log_route | Create or overwrite the log_route option
+*ConfigApi* | [**update_settings_server_version**](docs/ConfigApi.md#update_settings_server_version) | **Put** /config/settings/http/server_version | Create or overwrite the server_version option
+*ControlApi* | [**get_app_restart**](docs/ControlApi.md#get_app_restart) | **Get** /control/applications/{appName}/restart | Restart the {appName} application
+*ListenersApi* | [**delete_listener**](docs/ListenersApi.md#delete_listener) | **Delete** /config/listeners/{listenerName} | Delete a listener object
+*ListenersApi* | [**delete_listener_forwarded_recursive**](docs/ListenersApi.md#delete_listener_forwarded_recursive) | **Delete** /config/listeners/{listenerName}/forwarded/recursive | Delete the recursive object in a listener
+*ListenersApi* | [**delete_listener_forwarded_source**](docs/ListenersApi.md#delete_listener_forwarded_source) | **Delete** /config/listeners/{listenerName}/forwarded/source/{arrayIndex} | Delete a source array item in a listener
+*ListenersApi* | [**delete_listener_forwarded_sources**](docs/ListenersApi.md#delete_listener_forwarded_sources) | **Delete** /config/listeners/{listenerName}/forwarded/source | Delete the source option in a listener
+*ListenersApi* | [**delete_listener_forwared**](docs/ListenersApi.md#delete_listener_forwared) | **Delete** /config/listeners/{listenerName}/forwarded | Delete the forwarded object in a listener
+*ListenersApi* | [**delete_listener_tls**](docs/ListenersApi.md#delete_listener_tls) | **Delete** /config/listeners/{listenerName}/tls | Delete the tls object in a listener
+*ListenersApi* | [**delete_listener_tls_certificate**](docs/ListenersApi.md#delete_listener_tls_certificate) | **Delete** /config/listeners/{listenerName}/tls/certificate/{arrayIndex} | Delete a certificate array item in a listener
+*ListenersApi* | [**delete_listener_tls_certificates**](docs/ListenersApi.md#delete_listener_tls_certificates) | **Delete** /config/listeners/{listenerName}/tls/certificate | Delete the certificate option in a listener
+*ListenersApi* | [**delete_listener_tls_conf_commands**](docs/ListenersApi.md#delete_listener_tls_conf_commands) | **Delete** /config/listeners/{listenerName}/tls/conf_commands | Delete the conf_commands object in a listener
+*ListenersApi* | [**delete_listener_tls_session**](docs/ListenersApi.md#delete_listener_tls_session) | **Delete** /config/listeners/{listenerName}/tls/session | Delete the session object in a listener
+*ListenersApi* | [**delete_listener_tls_session_ticket**](docs/ListenersApi.md#delete_listener_tls_session_ticket) | **Delete** /config/listeners/{listenerName}/tls/session/tickets/{arrayIndex} | Delete a ticket array item in a listener
+*ListenersApi* | [**delete_listener_tls_session_tickets**](docs/ListenersApi.md#delete_listener_tls_session_tickets) | **Delete** /config/listeners/{listenerName}/tls/session/tickets | Delete the tickets option in a listener
+*ListenersApi* | [**delete_listeners**](docs/ListenersApi.md#delete_listeners) | **Delete** /config/listeners | Delete all the listeners
+*ListenersApi* | [**get_listener**](docs/ListenersApi.md#get_listener) | **Get** /config/listeners/{listenerName} | Retrieve a listener object
+*ListenersApi* | [**get_listener_forwarded**](docs/ListenersApi.md#get_listener_forwarded) | **Get** /config/listeners/{listenerName}/forwarded | Retrieve the forwarded object in a listener
+*ListenersApi* | [**get_listener_forwarded_client_ip**](docs/ListenersApi.md#get_listener_forwarded_client_ip) | **Get** /config/listeners/{listenerName}/forwarded/client_ip | Retrieve the client_ip option in a listener
+*ListenersApi* | [**get_listener_forwarded_protocol**](docs/ListenersApi.md#get_listener_forwarded_protocol) | **Get** /config/listeners/{listenerName}/forwarded/protocol | Retrieve the protocol option in a listener
+*ListenersApi* | [**get_listener_forwarded_recursive**](docs/ListenersApi.md#get_listener_forwarded_recursive) | **Get** /config/listeners/{listenerName}/forwarded/recursive | Retrieve the recursive option in a listener
+*ListenersApi* | [**get_listener_forwarded_source**](docs/ListenersApi.md#get_listener_forwarded_source) | **Get** /config/listeners/{listenerName}/forwarded/source/{arrayIndex} | Retrieve a source array item in a listener
+*ListenersApi* | [**get_listener_pass**](docs/ListenersApi.md#get_listener_pass) | **Get** /config/listeners/{listenerName}/pass | Retrieve the pass option in a listener
+*ListenersApi* | [**get_listener_tls**](docs/ListenersApi.md#get_listener_tls) | **Get** /config/listeners/{listenerName}/tls | Retrieve the tls object in a listener
+*ListenersApi* | [**get_listener_tls_certificate**](docs/ListenersApi.md#get_listener_tls_certificate) | **Get** /config/listeners/{listenerName}/tls/certificate/{arrayIndex} | Retrieve a certificate array item in a listener
+*ListenersApi* | [**get_listener_tls_session**](docs/ListenersApi.md#get_listener_tls_session) | **Get** /config/listeners/{listenerName}/tls/session | Retrieve the session object in a listener
+*ListenersApi* | [**get_listener_tls_session_ticket**](docs/ListenersApi.md#get_listener_tls_session_ticket) | **Get** /config/listeners/{listenerName}/tls/session/tickets/{arrayIndex} | Retrieve a ticket array item in a listener
+*ListenersApi* | [**get_listeners**](docs/ListenersApi.md#get_listeners) | **Get** /config/listeners | Retrieve all the listeners
+*ListenersApi* | [**insert_listener_forwarded_source**](docs/ListenersApi.md#insert_listener_forwarded_source) | **Post** /config/listeners/{listenerName}/forwarded/source | Add a new source array item in a listener
+*ListenersApi* | [**insert_listener_tls_certificate**](docs/ListenersApi.md#insert_listener_tls_certificate) | **Post** /config/listeners/{listenerName}/tls/certificate | Add a new certificate array item in a listener
+*ListenersApi* | [**insert_listener_tls_session_ticket**](docs/ListenersApi.md#insert_listener_tls_session_ticket) | **Post** /config/listeners/{listenerName}/tls/session/tickets | Add a new tickets array item in a listener
+*ListenersApi* | [**list_listener_forwarded_sources**](docs/ListenersApi.md#list_listener_forwarded_sources) | **Get** /config/listeners/{listenerName}/forwarded/source | Retrieve the source option in a listener
+*ListenersApi* | [**list_listener_tls_certificates**](docs/ListenersApi.md#list_listener_tls_certificates) | **Get** /config/listeners/{listenerName}/tls/certificate | Retrieve the certificate option in a listener
+*ListenersApi* | [**list_listener_tls_conf_commands**](docs/ListenersApi.md#list_listener_tls_conf_commands) | **Get** /config/listeners/{listenerName}/tls/conf_commands | Retrieve the conf_commands object in a listener
+*ListenersApi* | [**list_listener_tls_session_tickets**](docs/ListenersApi.md#list_listener_tls_session_tickets) | **Get** /config/listeners/{listenerName}/tls/session/tickets | Retrieve the tickets option in a listener
+*ListenersApi* | [**update_listener**](docs/ListenersApi.md#update_listener) | **Put** /config/listeners/{listenerName} | Create or overwrite a listener object
+*ListenersApi* | [**update_listener_forwarded**](docs/ListenersApi.md#update_listener_forwarded) | **Put** /config/listeners/{listenerName}/forwarded | Create or overwrite the forwarded object in a listener
+*ListenersApi* | [**update_listener_forwarded_client_ip**](docs/ListenersApi.md#update_listener_forwarded_client_ip) | **Put** /config/listeners/{listenerName}/forwarded/client_ip | Create or overwrite the client_ip option in a listener
+*ListenersApi* | [**update_listener_forwarded_protocol**](docs/ListenersApi.md#update_listener_forwarded_protocol) | **Put** /config/listeners/{listenerName}/forwarded/protocol | Create or overwrite the protocol option in a listener
+*ListenersApi* | [**update_listener_forwarded_recursive**](docs/ListenersApi.md#update_listener_forwarded_recursive) | **Put** /config/listeners/{listenerName}/forwarded/recursive | Create or overwrite the recursive option in a listener
+*ListenersApi* | [**update_listener_forwarded_source**](docs/ListenersApi.md#update_listener_forwarded_source) | **Put** /config/listeners/{listenerName}/forwarded/source/{arrayIndex} | Update a source array item in a listener
+*ListenersApi* | [**update_listener_forwarded_sources**](docs/ListenersApi.md#update_listener_forwarded_sources) | **Put** /config/listeners/{listenerName}/forwarded/source | Create or overwrite the source option in a listener
+*ListenersApi* | [**update_listener_pass**](docs/ListenersApi.md#update_listener_pass) | **Put** /config/listeners/{listenerName}/pass | Update the pass option in a listener
+*ListenersApi* | [**update_listener_tls**](docs/ListenersApi.md#update_listener_tls) | **Put** /config/listeners/{listenerName}/tls | Create or overwrite the tls object in a listener
+*ListenersApi* | [**update_listener_tls_certificate**](docs/ListenersApi.md#update_listener_tls_certificate) | **Put** /config/listeners/{listenerName}/tls/certificate/{arrayIndex} | Update a certificate array item in a listener
+*ListenersApi* | [**update_listener_tls_certificates**](docs/ListenersApi.md#update_listener_tls_certificates) | **Put** /config/listeners/{listenerName}/tls/certificate | Create or overwrite the certificate option in a listener
+*ListenersApi* | [**update_listener_tls_conf_commands**](docs/ListenersApi.md#update_listener_tls_conf_commands) | **Put** /config/listeners/{listenerName}/tls/conf_commands | Create or overwrite the conf_commands object in a listener
+*ListenersApi* | [**update_listener_tls_session**](docs/ListenersApi.md#update_listener_tls_session) | **Put** /config/listeners/{listenerName}/tls/session | Create or overwrite the session object in a listener
+*ListenersApi* | [**update_listener_tls_session_ticket**](docs/ListenersApi.md#update_listener_tls_session_ticket) | **Put** /config/listeners/{listenerName}/tls/session/tickets/{arrayIndex} | Create or overwrite a ticket array item in a listener
+*ListenersApi* | [**update_listener_tls_session_tickets**](docs/ListenersApi.md#update_listener_tls_session_tickets) | **Put** /config/listeners/{listenerName}/tls/session/tickets | Create or overwrite the tickets option in a listener
+*ListenersApi* | [**update_listeners**](docs/ListenersApi.md#update_listeners) | **Put** /config/listeners | Create or overwrite all the listeners
+*RoutesApi* | [**delete_routes**](docs/RoutesApi.md#delete_routes) | **Delete** /config/routes | Delete the routes entity
+*RoutesApi* | [**get_routes**](docs/RoutesApi.md#get_routes) | **Get** /config/routes | Retrieve the routes entity
+*RoutesApi* | [**update_routes**](docs/RoutesApi.md#update_routes) | **Put** /config/routes | Overwrite the routes entity
+*SettingsApi* | [**delete_settings**](docs/SettingsApi.md#delete_settings) | **Delete** /config/settings | Delete the settings object
+*SettingsApi* | [**delete_settings_discard_unsafe_fields**](docs/SettingsApi.md#delete_settings_discard_unsafe_fields) | **Delete** /config/settings/http/discard_unsafe_fields | Delete the discard_unsafe_fields option
+*SettingsApi* | [**delete_settings_http**](docs/SettingsApi.md#delete_settings_http) | **Delete** /config/settings/http | Delete the http object
+*SettingsApi* | [**delete_settings_http_body_read_timeout**](docs/SettingsApi.md#delete_settings_http_body_read_timeout) | **Delete** /config/settings/http/body_read_timeout | Delete the body_read_timeout option
+*SettingsApi* | [**delete_settings_http_header_read_timeout**](docs/SettingsApi.md#delete_settings_http_header_read_timeout) | **Delete** /config/settings/http/header_read_timeout | Delete the header_read_timeout option
+*SettingsApi* | [**delete_settings_http_idle_timeout**](docs/SettingsApi.md#delete_settings_http_idle_timeout) | **Delete** /config/settings/http/idle_timeout | Delete the idle_timeout option
+*SettingsApi* | [**delete_settings_http_max_body_size**](docs/SettingsApi.md#delete_settings_http_max_body_size) | **Delete** /config/settings/http/max_body_size | Delete the max_body_size option
+*SettingsApi* | [**delete_settings_http_send_timeout**](docs/SettingsApi.md#delete_settings_http_send_timeout) | **Delete** /config/settings/http/send_timeout | Delete the send_timeout option
+*SettingsApi* | [**delete_settings_http_static**](docs/SettingsApi.md#delete_settings_http_static) | **Delete** /config/settings/http/static | Delete the static object
+*SettingsApi* | [**delete_settings_http_static_mime_type**](docs/SettingsApi.md#delete_settings_http_static_mime_type) | **Delete** /config/settings/http/static/mime_types/{mimeType} | Delete the MIME type option
+*SettingsApi* | [**delete_settings_http_static_mime_types**](docs/SettingsApi.md#delete_settings_http_static_mime_types) | **Delete** /config/settings/http/static/mime_types | Delete the mime_types object
+*SettingsApi* | [**delete_settings_log_route**](docs/SettingsApi.md#delete_settings_log_route) | **Delete** /config/settings/http/log_route | Delete the log_route option
+*SettingsApi* | [**delete_settings_server_version**](docs/SettingsApi.md#delete_settings_server_version) | **Delete** /config/settings/http/server_version | Delete the server_version option
+*SettingsApi* | [**get_settings**](docs/SettingsApi.md#get_settings) | **Get** /config/settings | Retrieve the settings object
+*SettingsApi* | [**get_settings_discard_unsafe_fields**](docs/SettingsApi.md#get_settings_discard_unsafe_fields) | **Get** /config/settings/http/discard_unsafe_fields | Retrieve the discard_unsafe_fields option from http settings
+*SettingsApi* | [**get_settings_http**](docs/SettingsApi.md#get_settings_http) | **Get** /config/settings/http | Retrieve the http object from settings
+*SettingsApi* | [**get_settings_http_body_read_timeout**](docs/SettingsApi.md#get_settings_http_body_read_timeout) | **Get** /config/settings/http/body_read_timeout | Retrieve the body_read_timeout option from http settings
+*SettingsApi* | [**get_settings_http_header_read_timeout**](docs/SettingsApi.md#get_settings_http_header_read_timeout) | **Get** /config/settings/http/header_read_timeout | Retrieve the header_read_timeout option from http settings
+*SettingsApi* | [**get_settings_http_idle_timeout**](docs/SettingsApi.md#get_settings_http_idle_timeout) | **Get** /config/settings/http/idle_timeout | Retrieve the idle_timeout option from http settings
+*SettingsApi* | [**get_settings_http_max_body_size**](docs/SettingsApi.md#get_settings_http_max_body_size) | **Get** /config/settings/http/max_body_size | Retrieve the max_body_size option from http settings
+*SettingsApi* | [**get_settings_http_send_timeout**](docs/SettingsApi.md#get_settings_http_send_timeout) | **Get** /config/settings/http/send_timeout | Retrieve the send_timeout option from http settings
+*SettingsApi* | [**get_settings_http_static**](docs/SettingsApi.md#get_settings_http_static) | **Get** /config/settings/http/static | Retrieve the static object from http settings
+*SettingsApi* | [**get_settings_http_static_mime_type**](docs/SettingsApi.md#get_settings_http_static_mime_type) | **Get** /config/settings/http/static/mime_types/{mimeType} | Retrieve the MIME type option from MIME type settings
+*SettingsApi* | [**get_settings_http_static_mime_types**](docs/SettingsApi.md#get_settings_http_static_mime_types) | **Get** /config/settings/http/static/mime_types | Retrieve the mime_types object from static settings
+*SettingsApi* | [**get_settings_log_route**](docs/SettingsApi.md#get_settings_log_route) | **Get** /config/settings/http/log_route | Retrieve the log_route option from http settings
+*SettingsApi* | [**get_settings_server_version**](docs/SettingsApi.md#get_settings_server_version) | **Get** /config/settings/http/server_version | Retrieve the server_version option from http settings
+*SettingsApi* | [**update_settings**](docs/SettingsApi.md#update_settings) | **Put** /config/settings | Create or overwrite the settings object
+*SettingsApi* | [**update_settings_discard_unsafe_fields**](docs/SettingsApi.md#update_settings_discard_unsafe_fields) | **Put** /config/settings/http/discard_unsafe_fields | Create or overwrite the discard_unsafe_fields option
+*SettingsApi* | [**update_settings_http**](docs/SettingsApi.md#update_settings_http) | **Put** /config/settings/http | Create or overwrite the http object
+*SettingsApi* | [**update_settings_http_body_read_timeout**](docs/SettingsApi.md#update_settings_http_body_read_timeout) | **Put** /config/settings/http/body_read_timeout | Create or overwrite the body_read_timeout option
+*SettingsApi* | [**update_settings_http_header_read_timeout**](docs/SettingsApi.md#update_settings_http_header_read_timeout) | **Put** /config/settings/http/header_read_timeout | Create or overwrite the header_read_timeout option
+*SettingsApi* | [**update_settings_http_idle_timeout**](docs/SettingsApi.md#update_settings_http_idle_timeout) | **Put** /config/settings/http/idle_timeout | Create or overwrite the idle_timeout option
+*SettingsApi* | [**update_settings_http_max_body_size**](docs/SettingsApi.md#update_settings_http_max_body_size) | **Put** /config/settings/http/max_body_size | Create or overwrite the max_body_size option
+*SettingsApi* | [**update_settings_http_send_timeout**](docs/SettingsApi.md#update_settings_http_send_timeout) | **Put** /config/settings/http/send_timeout | Create or overwrite the send_timeout option
+*SettingsApi* | [**update_settings_http_static**](docs/SettingsApi.md#update_settings_http_static) | **Put** /config/settings/http/static | Create or overwrite the static object
+*SettingsApi* | [**update_settings_http_static_mime_type**](docs/SettingsApi.md#update_settings_http_static_mime_type) | **Put** /config/settings/http/static/mime_types/{mimeType} | Create or overwrite the MIME type option
+*SettingsApi* | [**update_settings_http_static_mime_types**](docs/SettingsApi.md#update_settings_http_static_mime_types) | **Put** /config/settings/http/static/mime_types | Create or overwrite the mime_types object
+*SettingsApi* | [**update_settings_log_route**](docs/SettingsApi.md#update_settings_log_route) | **Put** /config/settings/http/log_route | Create or overwrite the log_route option
+*SettingsApi* | [**update_settings_server_version**](docs/SettingsApi.md#update_settings_server_version) | **Put** /config/settings/http/server_version | Create or overwrite the server_version option
+*StatusApi* | [**get_status**](docs/StatusApi.md#get_status) | **Get** /status | Retrieve the status object
+*StatusApi* | [**get_status_applications**](docs/StatusApi.md#get_status_applications) | **Get** /status/applications | Retrieve the applications status object
+*StatusApi* | [**get_status_applications_app**](docs/StatusApi.md#get_status_applications_app) | **Get** /status/applications/{appName} | Retrieve the app status object
+*StatusApi* | [**get_status_applications_app_processes**](docs/StatusApi.md#get_status_applications_app_processes) | **Get** /status/applications/{appName}/processes | Retrieve the processes app status object
+*StatusApi* | [**get_status_applications_app_processes_idle**](docs/StatusApi.md#get_status_applications_app_processes_idle) | **Get** /status/applications/{appName}/processes/idle | Retrieve the idle processes app status number
+*StatusApi* | [**get_status_applications_app_processes_running**](docs/StatusApi.md#get_status_applications_app_processes_running) | **Get** /status/applications/{appName}/processes/running | Retrieve the running processes app status number
+*StatusApi* | [**get_status_applications_app_processes_starting**](docs/StatusApi.md#get_status_applications_app_processes_starting) | **Get** /status/applications/{appName}/processes/starting | Retrieve the starting processes app status number
+*StatusApi* | [**get_status_applications_app_requests**](docs/StatusApi.md#get_status_applications_app_requests) | **Get** /status/applications/{appName}/requests | Retrieve the requests app status object
+*StatusApi* | [**get_status_applications_app_requests_active**](docs/StatusApi.md#get_status_applications_app_requests_active) | **Get** /status/applications/{appName}/requests/active | Retrieve the active requests app status number
+*StatusApi* | [**get_status_connections**](docs/StatusApi.md#get_status_connections) | **Get** /status/connections | Retrieve the connections status object
+*StatusApi* | [**get_status_connections_accepted**](docs/StatusApi.md#get_status_connections_accepted) | **Get** /status/connections/accepted | Retrieve the accepted connections number
+*StatusApi* | [**get_status_connections_active**](docs/StatusApi.md#get_status_connections_active) | **Get** /status/connections/active | Retrieve the active connections number
+*StatusApi* | [**get_status_connections_closed**](docs/StatusApi.md#get_status_connections_closed) | **Get** /status/connections/closed | Retrieve the closed connections number
+*StatusApi* | [**get_status_connections_idle**](docs/StatusApi.md#get_status_connections_idle) | **Get** /status/connections/idle | Retrieve the idle connections number
+*StatusApi* | [**get_status_requests**](docs/StatusApi.md#get_status_requests) | **Get** /status/requests | Retrieve the requests status object
+*StatusApi* | [**get_status_requests_total**](docs/StatusApi.md#get_status_requests_total) | **Get** /status/requests/total | Retrieve the total requests number
+*TlsApi* | [**delete_listener_tls**](docs/TlsApi.md#delete_listener_tls) | **Delete** /config/listeners/{listenerName}/tls | Delete the tls object in a listener
+*TlsApi* | [**delete_listener_tls_certificate**](docs/TlsApi.md#delete_listener_tls_certificate) | **Delete** /config/listeners/{listenerName}/tls/certificate/{arrayIndex} | Delete a certificate array item in a listener
+*TlsApi* | [**delete_listener_tls_certificates**](docs/TlsApi.md#delete_listener_tls_certificates) | **Delete** /config/listeners/{listenerName}/tls/certificate | Delete the certificate option in a listener
+*TlsApi* | [**delete_listener_tls_conf_commands**](docs/TlsApi.md#delete_listener_tls_conf_commands) | **Delete** /config/listeners/{listenerName}/tls/conf_commands | Delete the conf_commands object in a listener
+*TlsApi* | [**delete_listener_tls_session**](docs/TlsApi.md#delete_listener_tls_session) | **Delete** /config/listeners/{listenerName}/tls/session | Delete the session object in a listener
+*TlsApi* | [**delete_listener_tls_session_ticket**](docs/TlsApi.md#delete_listener_tls_session_ticket) | **Delete** /config/listeners/{listenerName}/tls/session/tickets/{arrayIndex} | Delete a ticket array item in a listener
+*TlsApi* | [**delete_listener_tls_session_tickets**](docs/TlsApi.md#delete_listener_tls_session_tickets) | **Delete** /config/listeners/{listenerName}/tls/session/tickets | Delete the tickets option in a listener
+*TlsApi* | [**get_listener_tls**](docs/TlsApi.md#get_listener_tls) | **Get** /config/listeners/{listenerName}/tls | Retrieve the tls object in a listener
+*TlsApi* | [**get_listener_tls_certificate**](docs/TlsApi.md#get_listener_tls_certificate) | **Get** /config/listeners/{listenerName}/tls/certificate/{arrayIndex} | Retrieve a certificate array item in a listener
+*TlsApi* | [**get_listener_tls_session**](docs/TlsApi.md#get_listener_tls_session) | **Get** /config/listeners/{listenerName}/tls/session | Retrieve the session object in a listener
+*TlsApi* | [**get_listener_tls_session_ticket**](docs/TlsApi.md#get_listener_tls_session_ticket) | **Get** /config/listeners/{listenerName}/tls/session/tickets/{arrayIndex} | Retrieve a ticket array item in a listener
+*TlsApi* | [**insert_listener_tls_certificate**](docs/TlsApi.md#insert_listener_tls_certificate) | **Post** /config/listeners/{listenerName}/tls/certificate | Add a new certificate array item in a listener
+*TlsApi* | [**insert_listener_tls_session_ticket**](docs/TlsApi.md#insert_listener_tls_session_ticket) | **Post** /config/listeners/{listenerName}/tls/session/tickets | Add a new tickets array item in a listener
+*TlsApi* | [**list_listener_tls_certificates**](docs/TlsApi.md#list_listener_tls_certificates) | **Get** /config/listeners/{listenerName}/tls/certificate | Retrieve the certificate option in a listener
+*TlsApi* | [**list_listener_tls_conf_commands**](docs/TlsApi.md#list_listener_tls_conf_commands) | **Get** /config/listeners/{listenerName}/tls/conf_commands | Retrieve the conf_commands object in a listener
+*TlsApi* | [**list_listener_tls_session_tickets**](docs/TlsApi.md#list_listener_tls_session_tickets) | **Get** /config/listeners/{listenerName}/tls/session/tickets | Retrieve the tickets option in a listener
+*TlsApi* | [**update_listener_tls**](docs/TlsApi.md#update_listener_tls) | **Put** /config/listeners/{listenerName}/tls | Create or overwrite the tls object in a listener
+*TlsApi* | [**update_listener_tls_certificate**](docs/TlsApi.md#update_listener_tls_certificate) | **Put** /config/listeners/{listenerName}/tls/certificate/{arrayIndex} | Update a certificate array item in a listener
+*TlsApi* | [**update_listener_tls_certificates**](docs/TlsApi.md#update_listener_tls_certificates) | **Put** /config/listeners/{listenerName}/tls/certificate | Create or overwrite the certificate option in a listener
+*TlsApi* | [**update_listener_tls_conf_commands**](docs/TlsApi.md#update_listener_tls_conf_commands) | **Put** /config/listeners/{listenerName}/tls/conf_commands | Create or overwrite the conf_commands object in a listener
+*TlsApi* | [**update_listener_tls_session**](docs/TlsApi.md#update_listener_tls_session) | **Put** /config/listeners/{listenerName}/tls/session | Create or overwrite the session object in a listener
+*TlsApi* | [**update_listener_tls_session_ticket**](docs/TlsApi.md#update_listener_tls_session_ticket) | **Put** /config/listeners/{listenerName}/tls/session/tickets/{arrayIndex} | Create or overwrite a ticket array item in a listener
+*TlsApi* | [**update_listener_tls_session_tickets**](docs/TlsApi.md#update_listener_tls_session_tickets) | **Put** /config/listeners/{listenerName}/tls/session/tickets | Create or overwrite the tickets option in a listener
+*XffApi* | [**delete_listener_forwarded_recursive**](docs/XffApi.md#delete_listener_forwarded_recursive) | **Delete** /config/listeners/{listenerName}/forwarded/recursive | Delete the recursive object in a listener
+*XffApi* | [**delete_listener_forwarded_source**](docs/XffApi.md#delete_listener_forwarded_source) | **Delete** /config/listeners/{listenerName}/forwarded/source/{arrayIndex} | Delete a source array item in a listener
+*XffApi* | [**delete_listener_forwarded_sources**](docs/XffApi.md#delete_listener_forwarded_sources) | **Delete** /config/listeners/{listenerName}/forwarded/source | Delete the source option in a listener
+*XffApi* | [**delete_listener_forwared**](docs/XffApi.md#delete_listener_forwared) | **Delete** /config/listeners/{listenerName}/forwarded | Delete the forwarded object in a listener
+*XffApi* | [**get_listener_forwarded**](docs/XffApi.md#get_listener_forwarded) | **Get** /config/listeners/{listenerName}/forwarded | Retrieve the forwarded object in a listener
+*XffApi* | [**get_listener_forwarded_client_ip**](docs/XffApi.md#get_listener_forwarded_client_ip) | **Get** /config/listeners/{listenerName}/forwarded/client_ip | Retrieve the client_ip option in a listener
+*XffApi* | [**get_listener_forwarded_protocol**](docs/XffApi.md#get_listener_forwarded_protocol) | **Get** /config/listeners/{listenerName}/forwarded/protocol | Retrieve the protocol option in a listener
+*XffApi* | [**get_listener_forwarded_recursive**](docs/XffApi.md#get_listener_forwarded_recursive) | **Get** /config/listeners/{listenerName}/forwarded/recursive | Retrieve the recursive option in a listener
+*XffApi* | [**get_listener_forwarded_source**](docs/XffApi.md#get_listener_forwarded_source) | **Get** /config/listeners/{listenerName}/forwarded/source/{arrayIndex} | Retrieve a source array item in a listener
+*XffApi* | [**insert_listener_forwarded_source**](docs/XffApi.md#insert_listener_forwarded_source) | **Post** /config/listeners/{listenerName}/forwarded/source | Add a new source array item in a listener
+*XffApi* | [**list_listener_forwarded_sources**](docs/XffApi.md#list_listener_forwarded_sources) | **Get** /config/listeners/{listenerName}/forwarded/source | Retrieve the source option in a listener
+*XffApi* | [**update_listener_forwarded**](docs/XffApi.md#update_listener_forwarded) | **Put** /config/listeners/{listenerName}/forwarded | Create or overwrite the forwarded object in a listener
+*XffApi* | [**update_listener_forwarded_client_ip**](docs/XffApi.md#update_listener_forwarded_client_ip) | **Put** /config/listeners/{listenerName}/forwarded/client_ip | Create or overwrite the client_ip option in a listener
+*XffApi* | [**update_listener_forwarded_protocol**](docs/XffApi.md#update_listener_forwarded_protocol) | **Put** /config/listeners/{listenerName}/forwarded/protocol | Create or overwrite the protocol option in a listener
+*XffApi* | [**update_listener_forwarded_recursive**](docs/XffApi.md#update_listener_forwarded_recursive) | **Put** /config/listeners/{listenerName}/forwarded/recursive | Create or overwrite the recursive option in a listener
+*XffApi* | [**update_listener_forwarded_source**](docs/XffApi.md#update_listener_forwarded_source) | **Put** /config/listeners/{listenerName}/forwarded/source/{arrayIndex} | Update a source array item in a listener
+*XffApi* | [**update_listener_forwarded_sources**](docs/XffApi.md#update_listener_forwarded_sources) | **Put** /config/listeners/{listenerName}/forwarded/source | Create or overwrite the source option in a listener
+
+
+## Documentation For Models
+
+ - [CertBundle](docs/CertBundle.md)
+ - [CertBundleChainCert](docs/CertBundleChainCert.md)
+ - [CertBundleChainCertIssuer](docs/CertBundleChainCertIssuer.md)
+ - [CertBundleChainCertSubj](docs/CertBundleChainCertSubj.md)
+ - [CertBundleChainCertValidity](docs/CertBundleChainCertValidity.md)
+ - [Config](docs/Config.md)
+ - [ConfigAccessLog](docs/ConfigAccessLog.md)
+ - [ConfigAccessLogObject](docs/ConfigAccessLogObject.md)
+ - [ConfigApplication](docs/ConfigApplication.md)
+ - [ConfigApplicationCommon](docs/ConfigApplicationCommon.md)
+ - [ConfigApplicationCommonIsolation](docs/ConfigApplicationCommonIsolation.md)
+ - [ConfigApplicationCommonIsolationAutomount](docs/ConfigApplicationCommonIsolationAutomount.md)
+ - [ConfigApplicationCommonIsolationCgroup](docs/ConfigApplicationCommonIsolationCgroup.md)
+ - [ConfigApplicationCommonIsolationGidmapInner](docs/ConfigApplicationCommonIsolationGidmapInner.md)
+ - [ConfigApplicationCommonIsolationNamespaces](docs/ConfigApplicationCommonIsolationNamespaces.md)
+ - [ConfigApplicationCommonIsolationUidmapInner](docs/ConfigApplicationCommonIsolationUidmapInner.md)
+ - [ConfigApplicationCommonLimits](docs/ConfigApplicationCommonLimits.md)
+ - [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)
+ - [ConfigListener](docs/ConfigListener.md)
+ - [ConfigListenerForwarded](docs/ConfigListenerForwarded.md)
+ - [ConfigListenerForwardedSource](docs/ConfigListenerForwardedSource.md)
+ - [ConfigListenerTls](docs/ConfigListenerTls.md)
+ - [ConfigListenerTlsCertificate](docs/ConfigListenerTlsCertificate.md)
+ - [ConfigListenerTlsSession](docs/ConfigListenerTlsSession.md)
+ - [ConfigListenerTlsSessionTickets](docs/ConfigListenerTlsSessionTickets.md)
+ - [ConfigRouteStep](docs/ConfigRouteStep.md)
+ - [ConfigRouteStepAction](docs/ConfigRouteStepAction.md)
+ - [ConfigRouteStepActionPass](docs/ConfigRouteStepActionPass.md)
+ - [ConfigRouteStepActionProxy](docs/ConfigRouteStepActionProxy.md)
+ - [ConfigRouteStepActionReturn](docs/ConfigRouteStepActionReturn.md)
+ - [ConfigRouteStepActionShare](docs/ConfigRouteStepActionShare.md)
+ - [ConfigRouteStepMatch](docs/ConfigRouteStepMatch.md)
+ - [ConfigRouteStepMatchArguments](docs/ConfigRouteStepMatchArguments.md)
+ - [ConfigRouteStepMatchCookies](docs/ConfigRouteStepMatchCookies.md)
+ - [ConfigRouteStepMatchHeaders](docs/ConfigRouteStepMatchHeaders.md)
+ - [ConfigRoutes](docs/ConfigRoutes.md)
+ - [ConfigSettings](docs/ConfigSettings.md)
+ - [ConfigSettingsHttp](docs/ConfigSettingsHttp.md)
+ - [ConfigSettingsHttpStatic](docs/ConfigSettingsHttpStatic.md)
+ - [ConfigSettingsHttpStaticMimeType](docs/ConfigSettingsHttpStaticMimeType.md)
+ - [Status](docs/Status.md)
+ - [StatusApplicationsApp](docs/StatusApplicationsApp.md)
+ - [StatusApplicationsAppProcesses](docs/StatusApplicationsAppProcesses.md)
+ - [StatusApplicationsAppRequests](docs/StatusApplicationsAppRequests.md)
+ - [StatusConnections](docs/StatusConnections.md)
+ - [StatusRequests](docs/StatusRequests.md)
+ - [StringOrStringArray](docs/StringOrStringArray.md)
+
+
+To get access to the crate's generated documentation, use:
+
+```
+cargo doc --open
+```
+
+## Author
+
+unit-owner@nginx.org
diff --git a/tools/unitctl/unit-openapi/openapi-templates/Cargo.mustache b/tools/unitctl/unit-openapi/openapi-templates/Cargo.mustache
new file mode 100644
index 00000000..feca05ee
--- /dev/null
+++ b/tools/unitctl/unit-openapi/openapi-templates/Cargo.mustache
@@ -0,0 +1,65 @@
+[package]
+name = "{{{packageName}}}"
+version = "{{#lambdaVersion}}{{{packageVersion}}}{{/lambdaVersion}}"
+{{#infoEmail}}
+authors = ["{{{.}}}"]
+{{/infoEmail}}
+{{^infoEmail}}
+authors = ["OpenAPI Generator team and contributors"]
+{{/infoEmail}}
+{{#appDescription}}
+description = "{{{.}}}"
+{{/appDescription}}
+{{#licenseInfo}}
+license = "{{.}}"
+{{/licenseInfo}}
+{{^licenseInfo}}
+# Override this license by providing a License Object in the OpenAPI.
+license = "Unlicense"
+{{/licenseInfo}}
+edition = "2018"
+{{#publishRustRegistry}}
+publish = ["{{.}}"]
+{{/publishRustRegistry}}
+{{#repositoryUrl}}
+repository = "{{.}}"
+{{/repositoryUrl}}
+{{#documentationUrl}}
+documentation = "{{.}}"
+{{/documentationUrl}}
+{{#homePageUrl}}
+homepage = "{{.}}
+{{/homePageUrl}}
+
+[dependencies]
+serde = "1.0"
+serde_derive = "1.0"
+{{#serdeWith}}
+serde_with = "^2.0"
+{{/serdeWith}}
+serde_json = "1.0"
+url = "2.2"
+{{#hyper}}
+hyper = { version = "0.14" }
+http = "0.2"
+base64 = "0.21"
+futures = "0.3"
+{{/hyper}}
+{{#withAWSV4Signature}}
+aws-sigv4 = "0.3.0"
+http = "0.2.5"
+secrecy = "0.8.0"
+{{/withAWSV4Signature}}
+{{#reqwest}}
+{{^supportAsync}}
+reqwest = "~0.9"
+{{/supportAsync}}
+{{#supportAsync}}
+{{#supportMiddleware}}
+reqwest-middleware = "0.2.0"
+{{/supportMiddleware}}
+[dependencies.reqwest]
+version = "^0.11"
+features = ["json", "multipart"]
+{{/supportAsync}}
+{{/reqwest}}
diff --git a/tools/unitctl/unit-openapi/openapi-templates/request.rs b/tools/unitctl/unit-openapi/openapi-templates/request.rs
new file mode 100644
index 00000000..9cf480cc
--- /dev/null
+++ b/tools/unitctl/unit-openapi/openapi-templates/request.rs
@@ -0,0 +1,248 @@
+use std::collections::HashMap;
+use std::pin::Pin;
+
+use base64::{alphabet, Engine};
+use base64::engine::general_purpose::NO_PAD;
+use base64::engine::GeneralPurpose;
+
+use futures;
+use futures::Future;
+use futures::future::*;
+use hyper;
+use hyper::header::{AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE, HeaderValue, USER_AGENT};
+use serde;
+use serde_json;
+
+use super::{configuration, Error};
+
+const MIME_ENCODER: GeneralPurpose = GeneralPurpose::new(&alphabet::STANDARD, NO_PAD);
+
+pub(crate) struct ApiKey {
+ pub in_header: bool,
+ pub in_query: bool,
+ pub param_name: String,
+}
+
+impl ApiKey {
+ fn key(&self, prefix: &Option<String>, key: &str) -> String {
+ match prefix {
+ None => key.to_owned(),
+ Some(ref prefix) => format!("{} {}", prefix, key),
+ }
+ }
+}
+
+#[allow(dead_code)]
+pub(crate) enum Auth {
+ None,
+ ApiKey(ApiKey),
+ Basic,
+ Oauth,
+}
+
+/// If the authorization type is unspecified then it will be automatically detected based
+/// on the configuration. This functionality is useful when the OpenAPI definition does not
+/// include an authorization scheme.
+pub(crate) struct Request {
+ auth: Option<Auth>,
+ method: hyper::Method,
+ path: String,
+ query_params: HashMap<String, String>,
+ no_return_type: bool,
+ path_params: HashMap<String, String>,
+ form_params: HashMap<String, String>,
+ header_params: HashMap<String, String>,
+ // TODO: multiple body params are possible technically, but not supported here.
+ serialized_body: Option<String>,
+}
+
+#[allow(dead_code)]
+impl Request {
+ pub fn new(method: hyper::Method, path: String) -> Self {
+ Request {
+ auth: None,
+ method,
+ path,
+ query_params: HashMap::new(),
+ path_params: HashMap::new(),
+ form_params: HashMap::new(),
+ header_params: HashMap::new(),
+ serialized_body: None,
+ no_return_type: false,
+ }
+ }
+
+ pub fn with_body_param<T: serde::Serialize>(mut self, param: T) -> Self {
+ self.serialized_body = Some(serde_json::to_string(&param).unwrap());
+ self
+ }
+
+ pub fn with_header_param(mut self, basename: String, param: String) -> Self {
+ self.header_params.insert(basename, param);
+ self
+ }
+
+ #[allow(unused)]
+ pub fn with_query_param(mut self, basename: String, param: String) -> Self {
+ self.query_params.insert(basename, param);
+ self
+ }
+
+ #[allow(unused)]
+ pub fn with_path_param(mut self, basename: String, param: String) -> Self {
+ self.path_params.insert(basename, param);
+ self
+ }
+
+ #[allow(unused)]
+ pub fn with_form_param(mut self, basename: String, param: String) -> Self {
+ self.form_params.insert(basename, param);
+ self
+ }
+
+ pub fn returns_nothing(mut self) -> Self {
+ self.no_return_type = true;
+ self
+ }
+
+ pub fn with_auth(mut self, auth: Auth) -> Self {
+ self.auth = Some(auth);
+ self
+ }
+
+ pub fn execute<'a, C, U>(
+ self,
+ conf: &configuration::Configuration<C>,
+ ) -> Pin<Box<dyn Future<Output=Result<U, Error>> + 'a>>
+ where
+ C: hyper::client::connect::Connect + Clone + std::marker::Send + Sync,
+ U: Sized + std::marker::Send + 'a,
+ for<'de> U: serde::Deserialize<'de>,
+ {
+ let mut query_string = ::url::form_urlencoded::Serializer::new("".to_owned());
+
+ let mut path = self.path;
+ for (k, v) in self.path_params {
+ // replace {id} with the value of the id path param
+ path = path.replace(&format!("{{{}}}", k), &v);
+ }
+
+ for (key, val) in self.query_params {
+ query_string.append_pair(&key, &val);
+ }
+
+ let mut uri_str = format!("{}{}", conf.base_path, path);
+
+ let query_string_str = query_string.finish();
+ if query_string_str != "" {
+ uri_str += "?";
+ uri_str += &query_string_str;
+ }
+ let uri: hyper::Uri = match uri_str.parse() {
+ Err(e) => return Box::pin(futures::future::err(Error::UriError(e))),
+ Ok(u) => u,
+ };
+
+ let mut req_builder = hyper::Request::builder()
+ .uri(uri)
+ .method(self.method);
+
+ // Detect the authorization type if it hasn't been set.
+ let auth = self.auth.unwrap_or_else(||
+ if conf.api_key.is_some() {
+ panic!("Cannot automatically set the API key from the configuration, it must be specified in the OpenAPI definition")
+ } else if conf.oauth_access_token.is_some() {
+ Auth::Oauth
+ } else if conf.basic_auth.is_some() {
+ Auth::Basic
+ } else {
+ Auth::None
+ }
+ );
+ match auth {
+ Auth::ApiKey(apikey) => {
+ if let Some(ref key) = conf.api_key {
+ let val = apikey.key(&key.prefix, &key.key);
+ if apikey.in_query {
+ query_string.append_pair(&apikey.param_name, &val);
+ }
+ if apikey.in_header {
+ req_builder = req_builder.header(&apikey.param_name, val);
+ }
+ }
+ }
+ Auth::Basic => {
+ if let Some(ref auth_conf) = conf.basic_auth {
+ let mut text = auth_conf.0.clone();
+ text.push(':');
+ if let Some(ref pass) = auth_conf.1 {
+ text.push_str(&pass[..]);
+ }
+ let encoded = MIME_ENCODER.encode(&text);
+ req_builder = req_builder.header(AUTHORIZATION, encoded);
+ }
+ }
+ Auth::Oauth => {
+ if let Some(ref token) = conf.oauth_access_token {
+ let text = "Bearer ".to_owned() + token;
+ req_builder = req_builder.header(AUTHORIZATION, text);
+ }
+ }
+ Auth::None => {}
+ }
+
+ if let Some(ref user_agent) = conf.user_agent {
+ req_builder = req_builder.header(USER_AGENT, match HeaderValue::from_str(user_agent) {
+ Ok(header_value) => header_value,
+ Err(e) => return Box::pin(futures::future::err(super::Error::Header(e)))
+ });
+ }
+
+ for (k, v) in self.header_params {
+ req_builder = req_builder.header(&k, v);
+ }
+
+ let req_headers = req_builder.headers_mut().unwrap();
+ let request_result = if self.form_params.len() > 0 {
+ req_headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/x-www-form-urlencoded"));
+ let mut enc = ::url::form_urlencoded::Serializer::new("".to_owned());
+ for (k, v) in self.form_params {
+ enc.append_pair(&k, &v);
+ }
+ req_builder.body(hyper::Body::from(enc.finish()))
+ } else if let Some(body) = self.serialized_body {
+ req_headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
+ req_headers.insert(CONTENT_LENGTH, body.len().into());
+ req_builder.body(hyper::Body::from(body))
+ } else {
+ req_builder.body(hyper::Body::default())
+ };
+ let request = match request_result {
+ Ok(request) => request,
+ Err(e) => return Box::pin(futures::future::err(Error::from(e)))
+ };
+
+ let no_return_type = self.no_return_type;
+ Box::pin(conf.client
+ .request(request)
+ .map_err(|e| Error::from(e))
+ .and_then(move |response| {
+ let status = response.status();
+ if !status.is_success() {
+ futures::future::err::<U, Error>(Error::from((status, response.into_body()))).boxed()
+ } else if no_return_type {
+ // This is a hack; if there's no_ret_type, U is (), but serde_json gives an
+ // error when deserializing "" into (), so deserialize 'null' into it
+ // instead.
+ // An alternate option would be to require U: Default, and then return
+ // U::default() here instead since () implements that, but then we'd
+ // need to impl default for all models.
+ futures::future::ok::<U, Error>(serde_json::from_str("null").expect("serde null value")).boxed()
+ } else {
+ hyper::body::to_bytes(response.into_body())
+ .map(|bytes| serde_json::from_slice(&bytes.unwrap()))
+ .map_err(|e| Error::from(e)).boxed()
+ }
+ }))
+ }
+}
diff --git a/tools/unitctl/unit-openapi/src/apis/error.rs b/tools/unitctl/unit-openapi/src/apis/error.rs
new file mode 100644
index 00000000..a4a1e354
--- /dev/null
+++ b/tools/unitctl/unit-openapi/src/apis/error.rs
@@ -0,0 +1,18 @@
+use crate::apis::Error;
+use std::error::Error as StdError;
+use std::fmt::{Display, Formatter};
+
+impl Display for Error {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Error::Api(e) => write!(f, "ApiError: {:#?}", e),
+ Error::Header(e) => write!(f, "HeaderError: {}", e),
+ Error::Http(e) => write!(f, "HttpError: {:#?}", e),
+ Error::Hyper(e) => write!(f, "HyperError: {:#?}", e),
+ Error::Serde(e) => write!(f, "SerdeError: {:#?}", e),
+ Error::UriError(e) => write!(f, "UriError: {:#?}", e),
+ }
+ }
+}
+
+impl StdError for Error {}
diff --git a/tools/unitctl/unit-openapi/src/lib.rs b/tools/unitctl/unit-openapi/src/lib.rs
new file mode 100644
index 00000000..a71f18d6
--- /dev/null
+++ b/tools/unitctl/unit-openapi/src/lib.rs
@@ -0,0 +1,12 @@
+#![allow(clippy::all)]
+#[macro_use]
+extern crate serde_derive;
+
+extern crate futures;
+extern crate hyper;
+extern crate serde;
+extern crate serde_json;
+extern crate url;
+
+pub mod apis;
+pub mod models;
diff --git a/tools/unitctl/unitctl/Cargo.toml b/tools/unitctl/unitctl/Cargo.toml
new file mode 100644
index 00000000..98930fb3
--- /dev/null
+++ b/tools/unitctl/unitctl/Cargo.toml
@@ -0,0 +1,56 @@
+[package]
+name = "unitctl"
+description = "CLI interface to the NGINX UNIT Control API"
+version = "0.4.0-beta"
+authors = ["Elijah Zupancic"]
+edition = "2021"
+license = "Apache-2.0"
+
+[[bin]]
+name = "unitctl"
+path = "src/main.rs"
+
+[features]
+
+[dependencies]
+clap = { version = "4.4", features = ["default", "derive", "cargo"] }
+custom_error = "1.9"
+serde = "1.0"
+json5 = "0.4"
+nu-json = "0.89"
+serde_json = { version = "1.0", optional = false }
+serde_yaml = "0.9"
+rustls-pemfile = "2.0.0"
+unit-client-rs = { path = "../unit-client-rs" }
+colored_json = "4.1"
+tempfile = "3.8"
+which = "5.0"
+walkdir = "2.4"
+
+hyper = { version = "0.14", features = ["http1", "server", "client"] }
+hyperlocal = "0.8"
+hyper-tls = "0.5"
+tokio = { version = "1.35", features = ["macros"] }
+futures = "0.3"
+
+[package.metadata.deb]
+copyright = "2022, F5"
+license-file = ["../LICENSE.txt", "0"]
+extended-description = """\
+A utility for controlling NGINX UNIT."""
+section = "utility"
+priority = "optional"
+assets = [
+ ["../target/release/unitctl", "usr/bin/", "755"],
+ ["../target/man/unitctl.1.gz", "usr/share/man/man1/", "644"]
+]
+
+[package.metadata.generate-rpm]
+summary = """\
+A utility for controlling NGINX UNIT."""
+section = "utility"
+priority = "optional"
+assets = [
+ { source = "../target/release/unitctl", dest = "/usr/bin/unitctl", mode = "755" },
+ { source = "../target/man/unitctl.1.gz", dest = "/usr/share/man/man1/unitctl.1.gz", mode = "644" },
+]
diff --git a/tools/unitctl/unitctl/src/cmd/edit.rs b/tools/unitctl/unitctl/src/cmd/edit.rs
new file mode 100644
index 00000000..cbe01289
--- /dev/null
+++ b/tools/unitctl/unitctl/src/cmd/edit.rs
@@ -0,0 +1,105 @@
+use crate::inputfile::{InputFile, InputFormat};
+use crate::requests::{send_and_validate_config_deserialize_response, send_empty_body_deserialize_response};
+use crate::unitctl::UnitCtl;
+use crate::{wait, OutputFormat, UnitctlError};
+use std::path::{Path, PathBuf};
+use unit_client_rs::unit_client::UnitClient;
+use which::which;
+
+const EDITOR_ENV_VARS: [&str; 2] = ["EDITOR", "VISUAL"];
+const EDITOR_KNOWN_LIST: [&str; 8] = [
+ "sensible-editor",
+ "editor",
+ "vim",
+ "nano",
+ "nvim",
+ "vi",
+ "pico",
+ "emacs",
+];
+
+pub(crate) fn cmd(cli: &UnitCtl, output_format: OutputFormat) -> Result<(), UnitctlError> {
+ let control_socket = wait::wait_for_socket(cli)?;
+ let client = UnitClient::new(control_socket);
+ // Get latest configuration
+ let current_config = send_empty_body_deserialize_response(&client, "GET", "/config")?;
+
+ // Write JSON to temporary file - this file will automatically be deleted by the OS when
+ // the last file handle to it is removed.
+ let mut temp_file = tempfile::Builder::new()
+ .prefix("unitctl-")
+ .suffix(".json")
+ .tempfile()
+ .map_err(|e| UnitctlError::IoError { source: e })?;
+
+ // Pretty format JSON received from UNIT and write to the temporary file
+ serde_json::to_writer_pretty(temp_file.as_file_mut(), &current_config)
+ .map_err(|e| UnitctlError::SerializationError { message: e.to_string() })?;
+
+ // Load edited file
+ let temp_file_path = temp_file.path();
+ let before_edit_mod_time = temp_file_path.metadata().ok().map(|m| m.modified().ok());
+
+ let inputfile = InputFile::FileWithFormat(temp_file_path.into(), InputFormat::Json5);
+ open_editor(temp_file_path)?;
+ let after_edit_mod_time = temp_file_path.metadata().ok().map(|m| m.modified().ok());
+
+ // Check if file was modified before sending to UNIT
+ if let (Some(before), Some(after)) = (before_edit_mod_time, after_edit_mod_time) {
+ if before == after {
+ eprintln!("File was not modified - no changes will be sent to UNIT");
+ return Ok(());
+ }
+ };
+
+ // Send edited file to UNIT to overwrite current configuration
+ send_and_validate_config_deserialize_response(&client, "PUT", "/config", Some(&inputfile))
+ .and_then(|status| output_format.write_to_stdout(&status))
+}
+
+/// Look for an editor in the environment variables
+fn find_editor_from_env() -> Option<PathBuf> {
+ EDITOR_ENV_VARS
+ .iter()
+ .filter_map(std::env::var_os)
+ .filter(|s| !s.is_empty())
+ .filter_map(|s| which(s).ok())
+ .filter_map(|path| path.canonicalize().ok())
+ .find(|path| path.exists())
+}
+
+/// Look for editor in path by matching against a list of known editors or aliases
+fn find_editor_from_known_list() -> Option<PathBuf> {
+ EDITOR_KNOWN_LIST
+ .iter()
+ .filter_map(|editor| which(editor).ok())
+ .filter_map(|path| path.canonicalize().ok())
+ .find(|editor| editor.exists())
+}
+
+/// Find the path to an editor
+pub fn find_editor_path() -> Result<PathBuf, UnitctlError> {
+ find_editor_from_env()
+ .or_else(find_editor_from_known_list)
+ .ok_or_else(|| UnitctlError::EditorError {
+ message: "Could not find an editor".to_string(),
+ })
+}
+
+/// Start an editor with a given path
+pub fn open_editor(path: &Path) -> Result<(), UnitctlError> {
+ let editor_path = find_editor_path()?;
+ let status = std::process::Command::new(editor_path)
+ .arg(path)
+ .status()
+ .map_err(|e| UnitctlError::EditorError {
+ message: format!("Could not open editor: {}", e),
+ })?;
+ if status.success() {
+ Ok(())
+ } else {
+ Err(UnitctlError::EditorError {
+ message: format!("Editor exited with non-zero status: {}", status),
+ })
+ }
+}
diff --git a/tools/unitctl/unitctl/src/cmd/execute.rs b/tools/unitctl/unitctl/src/cmd/execute.rs
new file mode 100644
index 00000000..60957a83
--- /dev/null
+++ b/tools/unitctl/unitctl/src/cmd/execute.rs
@@ -0,0 +1,68 @@
+use crate::inputfile::InputFile;
+use crate::requests::{
+ send_and_validate_config_deserialize_response, send_and_validate_pem_data_deserialize_response,
+ send_body_deserialize_response, send_empty_body_deserialize_response,
+};
+use crate::unitctl::UnitCtl;
+use crate::wait;
+use crate::{OutputFormat, UnitctlError};
+use unit_client_rs::unit_client::UnitClient;
+
+pub(crate) fn cmd(
+ cli: &UnitCtl,
+ output_format: &OutputFormat,
+ input_file: &Option<String>,
+ method: &str,
+ path: &str,
+) -> Result<(), UnitctlError> {
+ let control_socket = wait::wait_for_socket(cli)?;
+ let client = UnitClient::new(control_socket);
+
+ let path_trimmed = path.trim();
+ let method_upper = method.to_uppercase();
+ let input_file_arg = input_file
+ .as_ref()
+ .map(|file| InputFile::new(file, &path_trimmed.to_string()));
+
+ if method_upper.eq("GET") && input_file.is_some() {
+ eprintln!("Cannot use GET method with input file - ignoring input file");
+ }
+
+ send_and_deserialize(client, method_upper, input_file_arg, path_trimmed, output_format)
+}
+
+fn send_and_deserialize(
+ client: UnitClient,
+ method: String,
+ input_file: Option<InputFile>,
+ path: &str,
+ output_format: &OutputFormat,
+) -> Result<(), UnitctlError> {
+ let is_js_modules_dir = path.starts_with("/js_modules/") || path.starts_with("js_modules/");
+
+ // If we are sending a GET request to a JS modules directory, we want to print the contents of the JS file
+ // instead of the JSON response
+ if method.eq("GET") && is_js_modules_dir && path.ends_with(".js") {
+ let script = send_body_deserialize_response::<String>(&client, method.as_str(), path, input_file.as_ref())?;
+ println!("{}", script);
+ return Ok(());
+ }
+
+ // Otherwise, we want to print the JSON response (a map) as represented by the output format
+ match input_file {
+ Some(input_file) => {
+ if input_file.is_config() {
+ send_and_validate_config_deserialize_response(&client, method.as_str(), path, Some(&input_file))
+ // TLS certificate data
+ } else if input_file.is_pem_bundle() {
+ send_and_validate_pem_data_deserialize_response(&client, method.as_str(), path, &input_file)
+ // This is unknown data
+ } else {
+ panic!("Unknown input file type")
+ }
+ }
+ // A none value for an input file can be considered a request to send an empty body
+ None => send_empty_body_deserialize_response(&client, method.as_str(), path),
+ }
+ .and_then(|status| output_format.write_to_stdout(&status))
+}
diff --git a/tools/unitctl/unitctl/src/cmd/import.rs b/tools/unitctl/unitctl/src/cmd/import.rs
new file mode 100644
index 00000000..e5e57456
--- /dev/null
+++ b/tools/unitctl/unitctl/src/cmd/import.rs
@@ -0,0 +1,124 @@
+use crate::inputfile::{InputFile, InputFormat};
+use crate::unitctl::UnitCtl;
+use crate::unitctl_error::UnitctlError;
+use crate::{requests, wait};
+use std::path::{Path, PathBuf};
+use unit_client_rs::unit_client::{UnitClient, UnitSerializableMap};
+use walkdir::{DirEntry, WalkDir};
+
+enum UploadFormat {
+ Config,
+ PemBundle,
+ Javascript,
+}
+
+impl From<&InputFile> for UploadFormat {
+ fn from(input_file: &InputFile) -> Self {
+ if input_file.is_config() {
+ UploadFormat::Config
+ } else if input_file.is_pem_bundle() {
+ UploadFormat::PemBundle
+ } else if input_file.is_javascript() {
+ UploadFormat::Javascript
+ } else {
+ panic!("Unknown input file type");
+ }
+ }
+}
+
+impl UploadFormat {
+ fn can_be_overwritten(&self) -> bool {
+ matches!(self, UploadFormat::Config)
+ }
+ fn upload_path(&self, path: &Path) -> String {
+ match self {
+ UploadFormat::Config => "/config".to_string(),
+ UploadFormat::PemBundle => format!("/certificates/{}.pem", Self::file_stem(path)),
+ UploadFormat::Javascript => format!("/js_modules/{}.js", Self::file_stem(path)),
+ }
+ }
+
+ fn file_stem(path: &Path) -> String {
+ path.file_stem().unwrap_or_default().to_string_lossy().into()
+ }
+}
+
+pub fn cmd(cli: &UnitCtl, directory: &PathBuf) -> Result<(), UnitctlError> {
+ if !directory.exists() {
+ return Err(UnitctlError::PathNotFound {
+ path: directory.to_string_lossy().into(),
+ });
+ }
+
+ let control_socket = wait::wait_for_socket(cli)?;
+ let client = UnitClient::new(control_socket);
+
+ let results: Vec<Result<(), UnitctlError>> = WalkDir::new(directory)
+ .follow_links(true)
+ .sort_by_file_name()
+ .into_iter()
+ .filter_map(Result::ok)
+ .filter(|e| !e.path().is_dir())
+ .map(|pe| process_entry(pe, &client))
+ .collect();
+
+ if results.iter().filter(|r| r.is_err()).count() == results.len() {
+ Err(UnitctlError::NoFilesImported)
+ } else {
+ println!("Imported {} files", results.len());
+ Ok(())
+ }
+}
+
+fn process_entry(entry: DirEntry, client: &UnitClient) -> Result<(), UnitctlError> {
+ let input_file = InputFile::from(entry.path());
+ if input_file.format() == InputFormat::Unknown {
+ println!(
+ "Skipping unknown file type: {}",
+ input_file.to_path()?.to_string_lossy()
+ );
+ return Err(UnitctlError::UnknownInputFileType {
+ path: input_file.to_path()?.to_string_lossy().into(),
+ });
+ }
+ let upload_format = UploadFormat::from(&input_file);
+ let upload_path = upload_format.upload_path(entry.path());
+
+ // We can't overwrite JS or PEM files, so we delete them first
+ if !upload_format.can_be_overwritten() {
+ let _ = requests::send_empty_body_deserialize_response(client, "DELETE", upload_path.as_str()).ok();
+ }
+
+ let result = match upload_format {
+ UploadFormat::Config => requests::send_and_validate_config_deserialize_response(
+ client,
+ "PUT",
+ upload_path.as_str(),
+ Some(&input_file),
+ ),
+ UploadFormat::PemBundle => {
+ requests::send_and_validate_pem_data_deserialize_response(client, "PUT", upload_path.as_str(), &input_file)
+ }
+ UploadFormat::Javascript => requests::send_body_deserialize_response::<UnitSerializableMap>(
+ client,
+ "PUT",
+ upload_path.as_str(),
+ Some(&input_file),
+ ),
+ };
+
+ match result {
+ Ok(_) => {
+ eprintln!(
+ "Imported {} -> {}",
+ input_file.to_path()?.to_string_lossy(),
+ upload_path
+ );
+ Ok(())
+ }
+ Err(error) => {
+ eprintln!("Error {} -> {}", input_file.to_path()?.to_string_lossy(), error);
+ Err(error)
+ }
+ }
+}
diff --git a/tools/unitctl/unitctl/src/cmd/instances.rs b/tools/unitctl/unitctl/src/cmd/instances.rs
new file mode 100644
index 00000000..26e15027
--- /dev/null
+++ b/tools/unitctl/unitctl/src/cmd/instances.rs
@@ -0,0 +1,16 @@
+use crate::{OutputFormat, UnitctlError};
+use unit_client_rs::unitd_instance::UnitdInstance;
+
+pub(crate) fn cmd(output_format: OutputFormat) -> Result<(), UnitctlError> {
+ let instances = UnitdInstance::running_unitd_instances();
+ if instances.is_empty() {
+ Err(UnitctlError::NoUnitInstancesError)
+ } else if output_format.eq(&OutputFormat::Text) {
+ instances.iter().for_each(|instance| {
+ println!("{}", instance);
+ });
+ Ok(())
+ } else {
+ output_format.write_to_stdout(&instances)
+ }
+}
diff --git a/tools/unitctl/unitctl/src/cmd/listeners.rs b/tools/unitctl/unitctl/src/cmd/listeners.rs
new file mode 100644
index 00000000..081a6cd9
--- /dev/null
+++ b/tools/unitctl/unitctl/src/cmd/listeners.rs
@@ -0,0 +1,13 @@
+use crate::unitctl::UnitCtl;
+use crate::wait;
+use crate::{OutputFormat, UnitctlError};
+use unit_client_rs::unit_client::UnitClient;
+
+pub fn cmd(cli: &UnitCtl, output_format: OutputFormat) -> Result<(), UnitctlError> {
+ let control_socket = wait::wait_for_socket(cli)?;
+ let client = UnitClient::new(control_socket);
+ client
+ .listeners()
+ .map_err(|e| UnitctlError::UnitClientError { source: *e })
+ .and_then(|response| output_format.write_to_stdout(&response))
+}
diff --git a/tools/unitctl/unitctl/src/cmd/mod.rs b/tools/unitctl/unitctl/src/cmd/mod.rs
new file mode 100644
index 00000000..989a0109
--- /dev/null
+++ b/tools/unitctl/unitctl/src/cmd/mod.rs
@@ -0,0 +1,6 @@
+pub(crate) mod edit;
+pub(crate) mod execute;
+pub(crate) mod import;
+pub(crate) mod instances;
+pub(crate) mod listeners;
+pub(crate) mod status;
diff --git a/tools/unitctl/unitctl/src/cmd/status.rs b/tools/unitctl/unitctl/src/cmd/status.rs
new file mode 100644
index 00000000..1f40735f
--- /dev/null
+++ b/tools/unitctl/unitctl/src/cmd/status.rs
@@ -0,0 +1,13 @@
+use crate::unitctl::UnitCtl;
+use crate::wait;
+use crate::{OutputFormat, UnitctlError};
+use unit_client_rs::unit_client::UnitClient;
+
+pub fn cmd(cli: &UnitCtl, output_format: OutputFormat) -> Result<(), UnitctlError> {
+ let control_socket = wait::wait_for_socket(cli)?;
+ let client = UnitClient::new(control_socket);
+ client
+ .status()
+ .map_err(|e| UnitctlError::UnitClientError { source: *e })
+ .and_then(|response| output_format.write_to_stdout(&response))
+}
diff --git a/tools/unitctl/unitctl/src/inputfile.rs b/tools/unitctl/unitctl/src/inputfile.rs
new file mode 100644
index 00000000..b2479d50
--- /dev/null
+++ b/tools/unitctl/unitctl/src/inputfile.rs
@@ -0,0 +1,289 @@
+use std::collections::HashMap;
+use std::io;
+use std::io::{BufRead, BufReader, Error as IoError, Read};
+use std::path::{Path, PathBuf};
+
+use crate::known_size::KnownSize;
+use clap::ValueEnum;
+
+use super::UnitSerializableMap;
+use super::UnitctlError;
+
+/// Input file data format
+#[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)]
+pub enum InputFormat {
+ Yaml,
+ Json,
+ Json5,
+ Hjson,
+ Pem,
+ JavaScript,
+ Unknown,
+}
+
+impl InputFormat {
+ pub fn from_file_extension<S>(file_extension: S) -> Self
+ where
+ S: Into<String>,
+ {
+ match file_extension.into().to_lowercase().as_str() {
+ "yaml" => InputFormat::Yaml,
+ "yml" => InputFormat::Yaml,
+ "json" => InputFormat::Json,
+ "json5" => InputFormat::Json5,
+ "hjson" => InputFormat::Hjson,
+ "cjson" => InputFormat::Hjson,
+ "pem" => InputFormat::Pem,
+ "js" => InputFormat::JavaScript,
+ "njs" => InputFormat::JavaScript,
+ _ => InputFormat::Unknown,
+ }
+ }
+
+ /// This function allows us to infer the input format based on the remote path which is
+ /// useful when processing input from STDIN.
+ pub fn from_remote_path<S>(remote_path: S) -> Self
+ where
+ S: Into<String>,
+ {
+ let remote_upload_path = remote_path.into();
+ let lead_slash_removed = remote_upload_path.trim_start_matches('/');
+ let first_path = lead_slash_removed
+ .split_once('/')
+ .map_or(lead_slash_removed, |(first, _)| first);
+ match first_path {
+ "config" => InputFormat::Hjson,
+ "certificates" => InputFormat::Pem,
+ "js_modules" => InputFormat::JavaScript,
+ _ => InputFormat::Json,
+ }
+ }
+}
+
+/// A "file" that can be used as input to a command
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum InputFile {
+ // Data received via STDIN
+ Stdin(InputFormat),
+ // Data that is on the file system where the format is inferred from the extension
+ File(Box<Path>),
+ // Data that is on the file system where the format is explicitly specified
+ FileWithFormat(Box<Path>, InputFormat),
+}
+
+impl InputFile {
+ /// Creates a new instance of `InputFile` from a string
+ pub fn new<S>(file_path_or_dash: S, remote_path: S) -> Self
+ where
+ S: Into<String>,
+ {
+ let file_path: String = file_path_or_dash.into();
+
+ match file_path.as_str() {
+ "-" => InputFile::Stdin(InputFormat::from_remote_path(remote_path)),
+ _ => InputFile::File(PathBuf::from(&file_path).into_boxed_path()),
+ }
+ }
+
+ /// Returns the format of the input file
+ pub fn format(&self) -> InputFormat {
+ match self {
+ InputFile::Stdin(format) => *format,
+ InputFile::File(path) => {
+ // Figure out the file format based on the file extension
+ match path.extension().and_then(|s| s.to_str()) {
+ Some(ext) => InputFormat::from_file_extension(ext),
+ None => InputFormat::Unknown,
+ }
+ }
+ InputFile::FileWithFormat(_file, format) => *format,
+ }
+ }
+
+ pub fn mime_type(&self) -> String {
+ match self.format() {
+ InputFormat::Yaml => "application/x-yaml".to_string(),
+ InputFormat::Json => "application/json".to_string(),
+ InputFormat::Json5 => "application/json5".to_string(),
+ InputFormat::Hjson => "application/hjson".to_string(),
+ InputFormat::Pem => "application/x-pem-file".to_string(),
+ InputFormat::JavaScript => "application/javascript".to_string(),
+ InputFormat::Unknown => "application/octet-stream".to_string(),
+ }
+ }
+
+ /// Returns true if the input file is in the format of a configuration file
+ pub fn is_config(&self) -> bool {
+ matches!(
+ self.format(),
+ InputFormat::Yaml | InputFormat::Json | InputFormat::Json5 | InputFormat::Hjson
+ )
+ }
+
+ pub fn is_javascript(&self) -> bool {
+ matches!(self.format(), InputFormat::JavaScript)
+ }
+
+ pub fn is_pem_bundle(&self) -> bool {
+ matches!(self.format(), InputFormat::Pem)
+ }
+
+ /// Returns the path to the input file if it is a file and not a stream
+ pub fn to_path(&self) -> Result<&Path, UnitctlError> {
+ match self {
+ InputFile::Stdin(_) => {
+ let io_error = IoError::new(std::io::ErrorKind::InvalidInput, "Input file is stdin");
+ Err(UnitctlError::IoError { source: io_error })
+ }
+ InputFile::File(path) | InputFile::FileWithFormat(path, _) => Ok(path),
+ }
+ }
+
+ /// Converts a HJSON Value type to a JSON Value type
+ fn hjson_value_to_json_value(value: nu_json::Value) -> serde_json::Value {
+ serde_json::to_value(value).expect("Failed to convert HJSON value to JSON value")
+ }
+
+ pub fn to_unit_serializable_map(&self) -> Result<UnitSerializableMap, UnitctlError> {
+ let reader: Box<dyn BufRead + Send> = self.try_into()?;
+ let body_data: UnitSerializableMap = match self.format() {
+ InputFormat::Yaml => serde_yaml::from_reader(reader)
+ .map_err(|e| UnitctlError::DeserializationError { message: e.to_string() })?,
+ InputFormat::Json => serde_json::from_reader(reader)
+ .map_err(|e| UnitctlError::DeserializationError { message: e.to_string() })?,
+ InputFormat::Json5 => {
+ let mut reader = BufReader::new(reader);
+ let mut json5_string: String = String::new();
+ reader
+ .read_to_string(&mut json5_string)
+ .map_err(|e| UnitctlError::DeserializationError { message: e.to_string() })?;
+ json5::from_str(&json5_string)
+ .map_err(|e| UnitctlError::DeserializationError { message: e.to_string() })?
+ }
+ InputFormat::Hjson => {
+ let hjson_value: HashMap<String, nu_json::Value> = nu_json::from_reader(reader)
+ .map_err(|e| UnitctlError::DeserializationError { message: e.to_string() })?;
+
+ hjson_value
+ .iter()
+ .map(|(k, v)| {
+ let json_value = Self::hjson_value_to_json_value(v.clone());
+ (k.clone(), json_value)
+ })
+ .collect()
+ }
+ _ => Err(UnitctlError::DeserializationError {
+ message: format!("Unsupported input format for serialization: {:?}", self),
+ })?,
+ };
+ Ok(body_data)
+ }
+}
+
+impl From<&Path> for InputFile {
+ fn from(path: &Path) -> Self {
+ InputFile::File(path.into())
+ }
+}
+
+impl TryInto<Box<dyn BufRead + Send>> for &InputFile {
+ type Error = UnitctlError;
+
+ fn try_into(self) -> Result<Box<dyn BufRead + Send>, Self::Error> {
+ let reader: Box<dyn BufRead + Send> = match self {
+ InputFile::Stdin(_) => Box::new(BufReader::new(io::stdin())),
+ InputFile::File(_) | InputFile::FileWithFormat(_, _) => {
+ let path = self.to_path()?;
+ let file = std::fs::File::open(path).map_err(|e| UnitctlError::IoError { source: e })?;
+ let reader = Box::new(BufReader::new(file));
+ Box::new(reader)
+ }
+ };
+ Ok(reader)
+ }
+}
+
+impl TryInto<Vec<u8>> for &InputFile {
+ type Error = UnitctlError;
+
+ fn try_into(self) -> Result<Vec<u8>, Self::Error> {
+ let mut buf: Vec<u8> = vec![];
+ let mut reader: Box<dyn BufRead + Send> = self.try_into()?;
+ reader
+ .read_to_end(&mut buf)
+ .map_err(|e| UnitctlError::IoError { source: e })?;
+ Ok(buf)
+ }
+}
+
+impl TryInto<KnownSize> for &InputFile {
+ type Error = UnitctlError;
+
+ fn try_into(self) -> Result<KnownSize, Self::Error> {
+ let known_size: KnownSize = match self {
+ InputFile::Stdin(_) => {
+ let mut buf: Vec<u8> = vec![];
+ let _ = io::stdin()
+ .read_to_end(&mut buf)
+ .map_err(|e| UnitctlError::IoError { source: e })?;
+ KnownSize::Vec(buf)
+ }
+ InputFile::File(_) | InputFile::FileWithFormat(_, _) => {
+ let path = self.to_path()?;
+ let file = std::fs::File::open(path).map_err(|e| UnitctlError::IoError { source: e })?;
+ let len = file.metadata().map_err(|e| UnitctlError::IoError { source: e })?.len();
+ let reader = Box::new(file);
+ KnownSize::Read(reader, len)
+ }
+ };
+ Ok(known_size)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn can_parse_file_extensions() {
+ assert_eq!(InputFormat::from_file_extension("yaml"), InputFormat::Yaml);
+ assert_eq!(InputFormat::from_file_extension("yml"), InputFormat::Yaml);
+ assert_eq!(InputFormat::from_file_extension("json"), InputFormat::Json);
+ assert_eq!(InputFormat::from_file_extension("json5"), InputFormat::Json5);
+ assert_eq!(InputFormat::from_file_extension("pem"), InputFormat::Pem);
+ assert_eq!(InputFormat::from_file_extension("js"), InputFormat::JavaScript);
+ assert_eq!(InputFormat::from_file_extension("njs"), InputFormat::JavaScript);
+ assert_eq!(InputFormat::from_file_extension("txt"), InputFormat::Unknown);
+ }
+
+ #[test]
+ fn can_parse_remote_paths() {
+ assert_eq!(InputFormat::from_remote_path("//config"), InputFormat::Hjson);
+ assert_eq!(InputFormat::from_remote_path("/config"), InputFormat::Hjson);
+ assert_eq!(InputFormat::from_remote_path("/config/"), InputFormat::Hjson);
+ assert_eq!(InputFormat::from_remote_path("config/"), InputFormat::Hjson);
+ assert_eq!(InputFormat::from_remote_path("config"), InputFormat::Hjson);
+ assert_eq!(InputFormat::from_remote_path("/config/something/"), InputFormat::Hjson);
+ assert_eq!(InputFormat::from_remote_path("config/something/"), InputFormat::Hjson);
+ assert_eq!(InputFormat::from_remote_path("config/something"), InputFormat::Hjson);
+ assert_eq!(InputFormat::from_remote_path("/certificates"), InputFormat::Pem);
+ assert_eq!(InputFormat::from_remote_path("/certificates/"), InputFormat::Pem);
+ assert_eq!(InputFormat::from_remote_path("certificates/"), InputFormat::Pem);
+ assert_eq!(InputFormat::from_remote_path("certificates"), InputFormat::Pem);
+ assert_eq!(InputFormat::from_remote_path("js_modules"), InputFormat::JavaScript);
+ assert_eq!(InputFormat::from_remote_path("js_modules/"), InputFormat::JavaScript);
+
+ assert_eq!(
+ InputFormat::from_remote_path("/certificates/something/"),
+ InputFormat::Pem
+ );
+ assert_eq!(
+ InputFormat::from_remote_path("certificates/something/"),
+ InputFormat::Pem
+ );
+ assert_eq!(
+ InputFormat::from_remote_path("certificates/something"),
+ InputFormat::Pem
+ );
+ }
+}
diff --git a/tools/unitctl/unitctl/src/known_size.rs b/tools/unitctl/unitctl/src/known_size.rs
new file mode 100644
index 00000000..d73aff91
--- /dev/null
+++ b/tools/unitctl/unitctl/src/known_size.rs
@@ -0,0 +1,77 @@
+use futures::Stream;
+use hyper::Body;
+use std::io;
+use std::io::{Cursor, Read};
+use std::pin::Pin;
+use std::task::{Context, Poll};
+
+pub enum KnownSize {
+ Vec(Vec<u8>),
+ Read(Box<dyn Read + Send>, u64),
+ String(String),
+ Empty,
+}
+
+impl KnownSize {
+ pub fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+
+ pub fn len(&self) -> u64 {
+ match self {
+ KnownSize::Vec(v) => v.len() as u64,
+ KnownSize::Read(_, size) => *size,
+ KnownSize::String(s) => s.len() as u64,
+ KnownSize::Empty => 0,
+ }
+ }
+}
+
+impl Stream for KnownSize {
+ type Item = io::Result<Vec<u8>>;
+
+ fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
+ let buf = &mut [0u8; 1024];
+
+ if let KnownSize::Read(r, _) = self.get_mut() {
+ return match r.read(buf) {
+ Ok(0) => Poll::Ready(None),
+ Ok(n) => Poll::Ready(Some(Ok(buf[..n].to_vec()))),
+ Err(e) => Poll::Ready(Some(Err(e))),
+ };
+ }
+
+ panic!("not implemented")
+ }
+
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ (0, Some(self.len() as usize))
+ }
+}
+
+impl From<KnownSize> for Box<dyn Read + Send> {
+ fn from(value: KnownSize) -> Self {
+ match value {
+ KnownSize::Vec(v) => Box::new(Cursor::new(v)),
+ KnownSize::Read(r, _) => r,
+ KnownSize::String(s) => Box::new(Cursor::new(s)),
+ KnownSize::Empty => Box::new(Cursor::new(Vec::new())),
+ }
+ }
+}
+
+impl From<KnownSize> for Body {
+ fn from(value: KnownSize) -> Self {
+ if value.is_empty() {
+ return Body::empty();
+ }
+ if let KnownSize::Vec(v) = value {
+ return Body::from(v);
+ }
+ if let KnownSize::String(s) = value {
+ return Body::from(s);
+ }
+
+ Body::wrap_stream(value)
+ }
+}
diff --git a/tools/unitctl/unitctl/src/main.rs b/tools/unitctl/unitctl/src/main.rs
new file mode 100644
index 00000000..2e8cedf1
--- /dev/null
+++ b/tools/unitctl/unitctl/src/main.rs
@@ -0,0 +1,101 @@
+extern crate clap;
+extern crate colored_json;
+extern crate custom_error;
+extern crate nu_json;
+extern crate rustls_pemfile;
+extern crate serde;
+extern crate unit_client_rs;
+
+use clap::Parser;
+
+use crate::cmd::{edit, execute as execute_cmd, import, instances, listeners, status};
+use crate::output_format::OutputFormat;
+use crate::unitctl::{Commands, UnitCtl};
+use crate::unitctl_error::UnitctlError;
+use unit_client_rs::unit_client::{UnitClient, UnitClientError, UnitSerializableMap};
+
+mod cmd;
+mod inputfile;
+pub mod known_size;
+mod output_format;
+mod requests;
+mod unitctl;
+mod unitctl_error;
+mod wait;
+
+fn main() -> Result<(), UnitctlError> {
+ let cli = UnitCtl::parse();
+
+ match cli.command {
+ Commands::Instances { output_format } => instances::cmd(output_format),
+
+ Commands::Edit { output_format } => edit::cmd(&cli, output_format),
+
+ Commands::Import { ref directory } => import::cmd(&cli, directory),
+
+ Commands::Execute {
+ ref output_format,
+ ref input_file,
+ ref method,
+ ref path,
+ } => execute_cmd::cmd(&cli, output_format, input_file, method, path),
+
+ Commands::Status { output_format } => status::cmd(&cli, output_format),
+
+ Commands::Listeners { output_format } => listeners::cmd(&cli, output_format),
+ }
+ .map_err(|error| {
+ eprint_error(&error);
+ std::process::exit(error.exit_code());
+ })
+}
+
+fn eprint_error(error: &UnitctlError) {
+ match error {
+ UnitctlError::NoUnitInstancesError => {
+ eprintln!("No running unit instances found");
+ }
+ UnitctlError::MultipleUnitInstancesError { ref suggestion } => {
+ eprintln!("{}", suggestion);
+ }
+ UnitctlError::NoSocketPathError => {
+ eprintln!("Unable to detect socket path from running instance");
+ }
+ UnitctlError::UnitClientError { source } => match source {
+ UnitClientError::SocketPermissionsError { .. } => {
+ eprintln!("{}", source);
+ eprintln!("Try running again with the same permissions as the unit control socket");
+ }
+ _ => {
+ eprintln!("Unit client error: {}", source);
+ }
+ },
+ UnitctlError::SerializationError { message } => {
+ eprintln!("Serialization error: {}", message);
+ }
+ UnitctlError::DeserializationError { message } => {
+ eprintln!("Deserialization error: {}", message);
+ }
+ UnitctlError::IoError { ref source } => {
+ eprintln!("IO error: {}", source);
+ }
+ UnitctlError::PathNotFound { path } => {
+ eprintln!("Path not found: {}", path);
+ }
+ UnitctlError::EditorError { message } => {
+ eprintln!("Error opening editor: {}", message);
+ }
+ UnitctlError::CertificateError { message } => {
+ eprintln!("Certificate error: {}", message);
+ }
+ UnitctlError::NoInputFileError => {
+ eprintln!("No input file specified when required");
+ }
+ UnitctlError::UiServerError { ref message } => {
+ eprintln!("UI server error: {}", message);
+ }
+ _ => {
+ eprintln!("{}", error);
+ }
+ }
+}
diff --git a/tools/unitctl/unitctl/src/output_format.rs b/tools/unitctl/unitctl/src/output_format.rs
new file mode 100644
index 00000000..eb7f954e
--- /dev/null
+++ b/tools/unitctl/unitctl/src/output_format.rs
@@ -0,0 +1,43 @@
+use crate::UnitctlError;
+use clap::ValueEnum;
+use colored_json::ColorMode;
+use serde::Serialize;
+use std::io::{stdout, BufWriter, Write};
+
+#[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)]
+pub(crate) enum OutputFormat {
+ Yaml,
+ Json,
+ #[value(id = "json-pretty")]
+ JsonPretty,
+ Text,
+}
+
+impl OutputFormat {
+ pub fn write_to_stdout<T>(&self, object: &T) -> Result<(), UnitctlError>
+ where
+ T: ?Sized + Serialize,
+ {
+ let no_color = std::env::var("NO_COLOR").map_or(false, |_| true);
+ let mut out = stdout();
+ let value =
+ serde_json::to_value(object).map_err(|e| UnitctlError::SerializationError { message: e.to_string() })?;
+
+ match (self, no_color) {
+ (OutputFormat::Yaml, _) => serde_yaml::to_writer(BufWriter::new(out), &value)
+ .map_err(|e| UnitctlError::SerializationError { message: e.to_string() }),
+ (OutputFormat::Json, _) => serde_json::to_writer(BufWriter::new(out), &value)
+ .map_err(|e| UnitctlError::SerializationError { message: e.to_string() }),
+ (OutputFormat::JsonPretty, true) => serde_json::to_writer_pretty(BufWriter::new(out), &value)
+ .map_err(|e| UnitctlError::SerializationError { message: e.to_string() }),
+ (OutputFormat::JsonPretty, false) => {
+ let mode = ColorMode::Auto(colored_json::Output::StdOut);
+ colored_json::write_colored_json_with_mode(&value, &mut out, mode)
+ .map_err(|e| UnitctlError::SerializationError { message: e.to_string() })
+ }
+ (OutputFormat::Text, _) => stdout()
+ .write_fmt(format_args!("{:?}", &value))
+ .map_err(|e| UnitctlError::IoError { source: e }),
+ }
+ }
+}
diff --git a/tools/unitctl/unitctl/src/requests.rs b/tools/unitctl/unitctl/src/requests.rs
new file mode 100644
index 00000000..bd47c645
--- /dev/null
+++ b/tools/unitctl/unitctl/src/requests.rs
@@ -0,0 +1,175 @@
+use super::inputfile::InputFile;
+use super::UnitClient;
+use super::UnitSerializableMap;
+use super::UnitctlError;
+use crate::known_size::KnownSize;
+use hyper::{Body, Request};
+use rustls_pemfile::Item;
+use std::collections::HashMap;
+use std::io::Cursor;
+use std::sync::atomic::AtomicUsize;
+use unit_client_rs::unit_client::UnitClientError;
+
+/// Send the contents of a file to the unit server
+/// We assume that the file is valid and can be sent to the server
+pub fn send_and_validate_config_deserialize_response(
+ client: &UnitClient,
+ method: &str,
+ path: &str,
+ input_file: Option<&InputFile>,
+) -> Result<UnitSerializableMap, UnitctlError> {
+ let body_data = match input_file {
+ Some(input) => Some(input.to_unit_serializable_map()?),
+ None => None,
+ };
+
+ /* Unfortunately, we have load the json text into memory before sending it to the server.
+ * This allows for validation of the json content before sending to the server. There may be
+ * a better way of doing this and it is worth investigating. */
+ let json = serde_json::to_value(&body_data).map_err(|error| UnitClientError::JsonError {
+ source: error,
+ path: path.into(),
+ })?;
+
+ let mime_type = input_file.map(|f| f.mime_type());
+ let reader = KnownSize::String(json.to_string());
+
+ streaming_upload_deserialize_response(client, method, path, mime_type, reader)
+ .map_err(|e| UnitctlError::UnitClientError { source: e })
+}
+
+/// Send an empty body to the unit server
+pub fn send_empty_body_deserialize_response(
+ client: &UnitClient,
+ method: &str,
+ path: &str,
+) -> Result<UnitSerializableMap, UnitctlError> {
+ send_body_deserialize_response(client, method, path, None)
+}
+
+/// Send the contents of a PEM file to the unit server
+pub fn send_and_validate_pem_data_deserialize_response(
+ client: &UnitClient,
+ method: &str,
+ path: &str,
+ input_file: &InputFile,
+) -> Result<UnitSerializableMap, UnitctlError> {
+ let bytes: Vec<u8> = input_file.try_into()?;
+ {
+ let mut cursor = Cursor::new(&bytes);
+ let items = rustls_pemfile::read_all(&mut cursor)
+ .map(|item| item.map_err(|e| UnitctlError::IoError { source: e }))
+ .collect();
+ validate_pem_items(items)?;
+ }
+ let known_size = KnownSize::Vec((*bytes).to_owned());
+
+ streaming_upload_deserialize_response(client, method, path, Some(input_file.mime_type()), known_size)
+ .map_err(|e| UnitctlError::UnitClientError { source: e })
+}
+
+/// Validate the contents of a PEM file
+fn validate_pem_items(pem_items: Vec<Result<Item, UnitctlError>>) -> Result<(), UnitctlError> {
+ fn item_name(item: Item) -> String {
+ match item {
+ Item::X509Certificate(_) => "X509Certificate",
+ Item::Sec1Key(_) => "Sec1Key",
+ Item::Crl(_) => "Crl",
+ Item::Pkcs1Key(_) => "Pkcs1Key",
+ Item::Pkcs8Key(_) => "Pkcs8Key",
+ // Note: this is not a valid PEM item, but rustls_pemfile library defines the enum as non-exhaustive
+ _ => "Unknown",
+ }
+ .to_string()
+ }
+
+ if pem_items.is_empty() {
+ let error = UnitctlError::CertificateError {
+ message: "No certificates found in file".to_string(),
+ };
+ return Err(error);
+ }
+
+ let mut items_tally: HashMap<String, AtomicUsize> = HashMap::new();
+
+ for pem_item_result in pem_items {
+ let pem_item = pem_item_result?;
+ let key = item_name(pem_item);
+ if let Some(count) = items_tally.get_mut(key.clone().as_str()) {
+ count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
+ } else {
+ items_tally.insert(key, AtomicUsize::new(1));
+ }
+ }
+
+ let key_count = items_tally
+ .iter()
+ .filter(|(key, _)| key.ends_with("Key"))
+ .fold(0, |acc, (_, count)| {
+ acc + count.load(std::sync::atomic::Ordering::Relaxed)
+ });
+ let cert_count = items_tally
+ .iter()
+ .filter(|(key, _)| key.ends_with("Certificate"))
+ .fold(0, |acc, (_, count)| {
+ acc + count.load(std::sync::atomic::Ordering::Relaxed)
+ });
+
+ if key_count == 0 {
+ let error = UnitctlError::CertificateError {
+ message: "No private keys found in file".to_string(),
+ };
+ return Err(error);
+ }
+ if cert_count == 0 {
+ let error = UnitctlError::CertificateError {
+ message: "No certificates found in file".to_string(),
+ };
+ return Err(error);
+ }
+
+ Ok(())
+}
+
+pub fn send_body_deserialize_response<RESPONSE: for<'de> serde::Deserialize<'de>>(
+ client: &UnitClient,
+ method: &str,
+ path: &str,
+ input_file: Option<&InputFile>,
+) -> Result<RESPONSE, UnitctlError> {
+ match input_file {
+ Some(input) => {
+ streaming_upload_deserialize_response(client, method, path, Some(input.mime_type()), input.try_into()?)
+ }
+ None => streaming_upload_deserialize_response(client, method, path, None, KnownSize::Empty),
+ }
+ .map_err(|e| UnitctlError::UnitClientError { source: e })
+}
+
+fn streaming_upload_deserialize_response<RESPONSE: for<'de> serde::Deserialize<'de>>(
+ client: &UnitClient,
+ method: &str,
+ path: &str,
+ mime_type: Option<String>,
+ read: KnownSize,
+) -> Result<RESPONSE, UnitClientError> {
+ let uri = client.control_socket.create_uri_with_path(path);
+
+ let content_length = read.len();
+ let body = Body::from(read);
+
+ let mut request = Request::builder()
+ .method(method)
+ .header("Content-Length", content_length)
+ .uri(uri)
+ .body(body)
+ .expect("Unable to build request");
+
+ if let Some(content_type) = mime_type {
+ request
+ .headers_mut()
+ .insert("Content-Type", content_type.parse().unwrap());
+ }
+
+ client.send_request_and_deserialize_response(request)
+}
diff --git a/tools/unitctl/unitctl/src/unitctl.rs b/tools/unitctl/unitctl/src/unitctl.rs
new file mode 100644
index 00000000..49e87e8e
--- /dev/null
+++ b/tools/unitctl/unitctl/src/unitctl.rs
@@ -0,0 +1,144 @@
+extern crate clap;
+
+use crate::output_format::OutputFormat;
+use clap::error::ErrorKind::ValueValidation;
+use clap::{Error as ClapError, Parser, Subcommand};
+use std::path::PathBuf;
+use unit_client_rs::control_socket_address::ControlSocket;
+
+#[derive(Parser, Debug)]
+#[command(author, version, about, long_about)]
+pub(crate) struct UnitCtl {
+ #[arg(
+ required = false,
+ short = 's',
+ long = "control-socket-address",
+ value_parser = parse_control_socket_address,
+ help = "Path (unix:/var/run/unit/control.sock), tcp address with port (127.0.0.1:80), or URL"
+ )]
+ pub(crate) control_socket_address: Option<ControlSocket>,
+ #[arg(
+ required = false,
+ default_missing_value = "1",
+ value_parser = parse_u8,
+ short = 'w',
+ long = "wait-timeout-seconds",
+ help = "Number of seconds to wait for control socket to become available"
+ )]
+ pub(crate) wait_time_seconds: Option<u8>,
+ #[arg(
+ required = false,
+ default_value = "3",
+ value_parser = parse_u8,
+ short = 't',
+ long = "wait-max-tries",
+ help = "Number of times to try to access control socket when waiting"
+ )]
+ pub(crate) wait_max_tries: Option<u8>,
+ #[command(subcommand)]
+ pub(crate) command: Commands,
+}
+
+#[derive(Debug, Subcommand)]
+pub(crate) enum Commands {
+ #[command(about = "List all running UNIT processes")]
+ Instances {
+ #[arg(
+ required = false,
+ global = true,
+ short = 't',
+ long = "output-format",
+ default_value = "text",
+ help = "Output format: text, yaml, json, json-pretty (default)"
+ )]
+ output_format: OutputFormat,
+ },
+ #[command(about = "Open current UNIT configuration in editor")]
+ Edit {
+ #[arg(
+ required = false,
+ global = true,
+ short = 't',
+ long = "output-format",
+ default_value = "json-pretty",
+ help = "Output format: yaml, json, json-pretty (default)"
+ )]
+ output_format: OutputFormat,
+ },
+ #[command(about = "Import configuration from a directory")]
+ Import {
+ #[arg(required = true, help = "Directory to import from")]
+ directory: PathBuf,
+ },
+ #[command(about = "Sends raw JSON payload to UNIT")]
+ Execute {
+ #[arg(
+ required = false,
+ global = true,
+ short = 't',
+ long = "output-format",
+ default_value = "json-pretty",
+ help = "Output format: yaml, json, json-pretty (default)"
+ )]
+ output_format: OutputFormat,
+ #[arg(
+ required = false,
+ global = true,
+ short = 'f',
+ long = "file",
+ help = "Input file (json, json5, cjson, hjson yaml, pem) to send to unit when applicable use - for stdin"
+ )]
+ input_file: Option<String>,
+ #[arg(
+ help = "HTTP method to use (GET, POST, PUT, DELETE)",
+ required = true,
+ short = 'm',
+ long = "http-method",
+ value_parser = parse_http_method,
+ )]
+ method: String,
+ #[arg(required = true, short = 'p', long = "path")]
+ path: String,
+ },
+ #[command(about = "Get the current status of UNIT")]
+ Status {
+ #[arg(
+ required = false,
+ global = true,
+ short = 't',
+ long = "output-format",
+ default_value = "json-pretty",
+ help = "Output format: yaml, json, json-pretty (default)"
+ )]
+ output_format: OutputFormat,
+ },
+ #[command(about = "List active listeners")]
+ Listeners {
+ #[arg(
+ required = false,
+ global = true,
+ short = 't',
+ long = "output-format",
+ default_value = "json-pretty",
+ help = "Output format: yaml, json, json-pretty (default)"
+ )]
+ output_format: OutputFormat,
+ },
+}
+
+fn parse_control_socket_address(s: &str) -> Result<ControlSocket, ClapError> {
+ ControlSocket::try_from(s).map_err(|e| ClapError::raw(ValueValidation, e.to_string()))
+}
+
+fn parse_http_method(s: &str) -> Result<String, ClapError> {
+ let method = s.to_uppercase();
+ match method.as_str() {
+ "GET" | "POST" | "PUT" | "DELETE" => Ok(method),
+ _ => Err(ClapError::raw(ValueValidation, format!("Invalid HTTP method: {}", s))),
+ }
+}
+
+fn parse_u8(s: &str) -> Result<u8, ClapError> {
+ s.parse::<u8>()
+ .map_err(|e| ClapError::raw(ValueValidation, format!("Invalid number: {}", e)))
+}
diff --git a/tools/unitctl/unitctl/src/unitctl_error.rs b/tools/unitctl/unitctl/src/unitctl_error.rs
new file mode 100644
index 00000000..1cf4fe48
--- /dev/null
+++ b/tools/unitctl/unitctl/src/unitctl_error.rs
@@ -0,0 +1,72 @@
+use std::fmt::{Display, Formatter};
+use std::io::Error as IoError;
+use std::process::{ExitCode, Termination};
+use unit_client_rs::unit_client::UnitClientError;
+
+use custom_error::custom_error;
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub(crate) enum ControlSocketErrorKind {
+ NotFound,
+ Permissions,
+ Parse,
+ General,
+}
+
+impl Display for ControlSocketErrorKind {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ writeln!(f, "{:?}", self)
+ }
+}
+
+custom_error! {pub UnitctlError
+ ControlSocketError { kind: ControlSocketErrorKind, message: String } = "{message}",
+ CertificateError { message: String } = "Certificate error: {message}",
+ EditorError { message: String } = "Error opening editor: {message}",
+ NoUnitInstancesError = "No running unit instances found",
+ MultipleUnitInstancesError {
+ suggestion: String} = "Multiple unit instances found: {suggestion}",
+ NoSocketPathError = "Unable to detect socket path from running instance",
+ NoInputFileError = "No input file specified when required",
+ UiServerError { message: String } = "UI server error: {message}",
+ UnitClientError { source: UnitClientError } = "Unit client error: {source}",
+ SerializationError { message: String } = "Serialization error: {message}",
+ DeserializationError { message: String } = "Deserialization error: {message}",
+ IoError { source: IoError } = "IO error: {source}",
+ PathNotFound { path: String } = "Path not found: {path}",
+ UnknownInputFileType { path: String } = "Unknown input type for file: {path}",
+ NoFilesImported = "All imports failed",
+ WaitTimeoutError = "Timeout waiting for unit to start has been exceeded",
+}
+
+impl UnitctlError {
+ pub fn exit_code(&self) -> i32 {
+ match self {
+ UnitctlError::NoUnitInstancesError => 10,
+ UnitctlError::MultipleUnitInstancesError { .. } => 11,
+ UnitctlError::NoSocketPathError => 12,
+ UnitctlError::UnitClientError { .. } => 13,
+ UnitctlError::WaitTimeoutError => 14,
+ _ => 99,
+ }
+ }
+
+ pub fn retryable(&self) -> bool {
+ match self {
+ UnitctlError::ControlSocketError { kind, .. } => {
+ // try again because there is no socket created yet
+ ControlSocketErrorKind::NotFound == *kind
+ }
+ // try again because unit isn't running
+ UnitctlError::NoUnitInstancesError => true,
+ // do not retry because this is an unrecoverable error
+ _ => false,
+ }
+ }
+}
+
+impl Termination for UnitctlError {
+ fn report(self) -> ExitCode {
+ ExitCode::from(self.exit_code() as u8)
+ }
+}
diff --git a/tools/unitctl/unitctl/src/wait.rs b/tools/unitctl/unitctl/src/wait.rs
new file mode 100644
index 00000000..998dc59c
--- /dev/null
+++ b/tools/unitctl/unitctl/src/wait.rs
@@ -0,0 +1,165 @@
+use crate::unitctl::UnitCtl;
+use crate::unitctl_error::{ControlSocketErrorKind, UnitctlError};
+use std::time::Duration;
+use unit_client_rs::control_socket_address::ControlSocket;
+use unit_client_rs::unit_client::{UnitClient, UnitClientError};
+use unit_client_rs::unitd_instance::UnitdInstance;
+
+/// Waits for a socket to become available. Availability is tested by attempting to access the
+/// status endpoint via the control socket. When socket is available, ControlSocket instance
+/// is returned.
+pub fn wait_for_socket(cli: &UnitCtl) -> Result<ControlSocket, UnitctlError> {
+ // Don't wait, if wait_time is not specified
+ if cli.wait_time_seconds.is_none() {
+ return cli.control_socket_address.instance_value_if_none().and_validate();
+ }
+
+ let wait_time =
+ Duration::from_secs(cli.wait_time_seconds.expect("wait_time_option default was not applied") as u64);
+ let max_tries = cli.wait_max_tries.expect("max_tries_option default was not applied");
+
+ let mut attempt: u8 = 0;
+ let mut control_socket: ControlSocket;
+ while attempt < max_tries {
+ if attempt > 0 {
+ eprintln!(
+ "Waiting for {}s control socket to be available try {}/{}...",
+ wait_time.as_secs(),
+ attempt + 1,
+ max_tries
+ );
+ std::thread::sleep(wait_time);
+ }
+
+ attempt += 1;
+
+ let result = cli.control_socket_address.instance_value_if_none().and_validate();
+
+ if let Err(error) = result {
+ if error.retryable() {
+ continue;
+ } else {
+ return Err(error);
+ }
+ }
+
+ control_socket = result.unwrap();
+ let client = UnitClient::new(control_socket.clone());
+
+ match client.status() {
+ Ok(_) => {
+ return Ok(control_socket.to_owned());
+ }
+ Err(error) => {
+ eprintln!("Unable to access status endpoint: {}", *error);
+ continue;
+ }
+ }
+ }
+
+ if attempt >= max_tries {
+ Err(UnitctlError::WaitTimeoutError)
+ } else {
+ panic!("Unexpected state - this should never happen");
+ }
+}
+
+trait OptionControlSocket {
+ fn instance_value_if_none(&self) -> Result<ControlSocket, UnitctlError>;
+}
+
+impl OptionControlSocket for Option<ControlSocket> {
+ fn instance_value_if_none(&self) -> Result<ControlSocket, UnitctlError> {
+ if let Some(control_socket) = self {
+ Ok(control_socket.to_owned())
+ } else {
+ find_socket_address_from_instance()
+ }
+ }
+}
+
+trait ResultControlSocket<T, E> {
+ fn and_validate(self) -> Result<ControlSocket, UnitctlError>;
+}
+
+impl ResultControlSocket<ControlSocket, UnitctlError> for Result<ControlSocket, UnitctlError> {
+ fn and_validate(self) -> Result<ControlSocket, UnitctlError> {
+ self.and_then(|control_socket| {
+ control_socket.validate().map_err(|error| match error {
+ UnitClientError::UnixSocketNotFound { .. } => UnitctlError::ControlSocketError {
+ kind: ControlSocketErrorKind::NotFound,
+ message: format!("{}", error),
+ },
+ UnitClientError::SocketPermissionsError { .. } => UnitctlError::ControlSocketError {
+ kind: ControlSocketErrorKind::Permissions,
+ message: format!("{}", error),
+ },
+ UnitClientError::TcpSocketAddressUriError { .. }
+ | UnitClientError::TcpSocketAddressNoPortError { .. }
+ | UnitClientError::TcpSocketAddressParseError { .. } => UnitctlError::ControlSocketError {
+ kind: ControlSocketErrorKind::Parse,
+ message: format!("{}", error),
+ },
+ _ => UnitctlError::ControlSocketError {
+ kind: ControlSocketErrorKind::General,
+ message: format!("{}", error),
+ },
+ })
+ })
+ }
+}
+
+fn find_socket_address_from_instance() -> Result<ControlSocket, UnitctlError> {
+ let instances = UnitdInstance::running_unitd_instances();
+ if instances.is_empty() {
+ return Err(UnitctlError::NoUnitInstancesError);
+ } else if instances.len() > 1 {
+ let suggestion: String = "Multiple unit instances found. Specify the socket address to the instance you wish \
+ to control using the `--control-socket-address` flag"
+ .to_string();
+ return Err(UnitctlError::MultipleUnitInstancesError { suggestion });
+ }
+
+ let instance = instances.first().unwrap();
+ match instance.control_api_socket_address() {
+ Some(path) => Ok(ControlSocket::try_from(path).unwrap()),
+ None => Err(UnitctlError::NoSocketPathError),
+ }
+}
+
+#[test]
+fn wait_for_unavailable_unix_socket() {
+ let control_socket = ControlSocket::try_from("unix:/tmp/this_socket_does_not_exist.sock");
+ let cli = UnitCtl {
+ control_socket_address: Some(control_socket.unwrap()),
+ wait_time_seconds: Some(1u8),
+ wait_max_tries: Some(3u8),
+ command: crate::unitctl::Commands::Status {
+ output_format: crate::output_format::OutputFormat::JsonPretty,
+ },
+ };
+ let error = wait_for_socket(&cli).expect_err("Expected error, but no error received");
+ match error {
+ UnitctlError::WaitTimeoutError => {}
+ _ => panic!("Expected WaitTimeoutError: {}", error),
+ }
+}
+
+#[test]
+fn wait_for_unavailable_tcp_socket() {
+ let control_socket = ControlSocket::try_from("http://127.0.0.1:9783456");
+ let cli = UnitCtl {
+ control_socket_address: Some(control_socket.unwrap()),
+ wait_time_seconds: Some(1u8),
+ wait_max_tries: Some(3u8),
+ command: crate::unitctl::Commands::Status {
+ output_format: crate::output_format::OutputFormat::JsonPretty,
+ },
+ };
+
+ let error = wait_for_socket(&cli).expect_err("Expected error, but no error received");
+ match error {
+ UnitctlError::WaitTimeoutError => {}
+ _ => panic!("Expected WaitTimeoutError"),
+ }
+}