From db3cf3e42d93112278f5e86cca2c886627ef48b2 Mon Sep 17 00:00:00 2001 From: Ava Hahn Date: Mon, 22 Apr 2024 13:26:34 +0100 Subject: 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 Signed-off-by: Elijah Zupancic Signed-off-by: Ava Hahn Reviewed-by: Andrew Clayton # non rust stuff [ tools/cli => tools/unitctl and subject tweak - Andrew ] Signed-off-by: Andrew Clayton --- tools/unitctl/.cargo/config.toml | 2 + tools/unitctl/.gitignore | 16 + tools/unitctl/Cargo.lock | 1998 ++++++++++++++++++++ tools/unitctl/Cargo.toml | 8 + tools/unitctl/Dockerfile | 37 + tools/unitctl/GNUmakefile | 145 ++ tools/unitctl/HomebrewFormula | 1 + tools/unitctl/README.md | 134 ++ tools/unitctl/build/container.mk | 67 + tools/unitctl/build/github.mk | 22 + tools/unitctl/build/openapi-generator-cli.sh | 77 + tools/unitctl/build/package.mk | 139 ++ tools/unitctl/build/release.mk | 57 + tools/unitctl/man/unitctl.1 | 27 + tools/unitctl/openapi-config.json | 6 + tools/unitctl/pkg/brew/unitctl.rb | 29 + tools/unitctl/pkg/brew/unitctl.rb.template | 29 + tools/unitctl/rustfmt.toml | 1 + tools/unitctl/unit-client-rs/Cargo.toml | 32 + .../unit-client-rs/src/control_socket_address.rs | 571 ++++++ tools/unitctl/unit-client-rs/src/lib.rs | 15 + tools/unitctl/unit-client-rs/src/runtime_flags.rs | 90 + tools/unitctl/unit-client-rs/src/unit_client.rs | 393 ++++ tools/unitctl/unit-client-rs/src/unitd_cmd.rs | 85 + .../unit-client-rs/src/unitd_configure_options.rs | 235 +++ tools/unitctl/unit-client-rs/src/unitd_instance.rs | 360 ++++ tools/unitctl/unit-client-rs/src/unitd_process.rs | 170 ++ .../unit-client-rs/src/unitd_process_user.rs | 36 + tools/unitctl/unit-openapi/.gitignore | 3 + .../unitctl/unit-openapi/.openapi-generator-ignore | 27 + .../unitctl/unit-openapi/.openapi-generator/FILES | 161 ++ .../unit-openapi/.openapi-generator/VERSION | 1 + tools/unitctl/unit-openapi/Cargo.toml | 17 + tools/unitctl/unit-openapi/README.md | 411 ++++ .../unit-openapi/openapi-templates/Cargo.mustache | 65 + .../unit-openapi/openapi-templates/request.rs | 248 +++ tools/unitctl/unit-openapi/src/apis/error.rs | 18 + tools/unitctl/unit-openapi/src/lib.rs | 12 + tools/unitctl/unitctl/Cargo.toml | 56 + tools/unitctl/unitctl/src/cmd/edit.rs | 105 + tools/unitctl/unitctl/src/cmd/execute.rs | 68 + tools/unitctl/unitctl/src/cmd/import.rs | 124 ++ tools/unitctl/unitctl/src/cmd/instances.rs | 16 + tools/unitctl/unitctl/src/cmd/listeners.rs | 13 + tools/unitctl/unitctl/src/cmd/mod.rs | 6 + tools/unitctl/unitctl/src/cmd/status.rs | 13 + tools/unitctl/unitctl/src/inputfile.rs | 289 +++ tools/unitctl/unitctl/src/known_size.rs | 77 + tools/unitctl/unitctl/src/main.rs | 101 + tools/unitctl/unitctl/src/output_format.rs | 43 + tools/unitctl/unitctl/src/requests.rs | 175 ++ tools/unitctl/unitctl/src/unitctl.rs | 144 ++ tools/unitctl/unitctl/src/unitctl_error.rs | 72 + tools/unitctl/unitctl/src/wait.rs | 165 ++ 54 files changed, 7212 insertions(+) create mode 100644 tools/unitctl/.cargo/config.toml create mode 100644 tools/unitctl/.gitignore create mode 100644 tools/unitctl/Cargo.lock create mode 100644 tools/unitctl/Cargo.toml create mode 100644 tools/unitctl/Dockerfile create mode 100644 tools/unitctl/GNUmakefile create mode 120000 tools/unitctl/HomebrewFormula create mode 100644 tools/unitctl/README.md create mode 100644 tools/unitctl/build/container.mk create mode 100644 tools/unitctl/build/github.mk create mode 100755 tools/unitctl/build/openapi-generator-cli.sh create mode 100644 tools/unitctl/build/package.mk create mode 100644 tools/unitctl/build/release.mk create mode 100644 tools/unitctl/man/unitctl.1 create mode 100644 tools/unitctl/openapi-config.json create mode 100644 tools/unitctl/pkg/brew/unitctl.rb create mode 100644 tools/unitctl/pkg/brew/unitctl.rb.template create mode 100644 tools/unitctl/rustfmt.toml create mode 100644 tools/unitctl/unit-client-rs/Cargo.toml create mode 100644 tools/unitctl/unit-client-rs/src/control_socket_address.rs create mode 100644 tools/unitctl/unit-client-rs/src/lib.rs create mode 100644 tools/unitctl/unit-client-rs/src/runtime_flags.rs create mode 100644 tools/unitctl/unit-client-rs/src/unit_client.rs create mode 100644 tools/unitctl/unit-client-rs/src/unitd_cmd.rs create mode 100644 tools/unitctl/unit-client-rs/src/unitd_configure_options.rs create mode 100644 tools/unitctl/unit-client-rs/src/unitd_instance.rs create mode 100644 tools/unitctl/unit-client-rs/src/unitd_process.rs create mode 100644 tools/unitctl/unit-client-rs/src/unitd_process_user.rs create mode 100644 tools/unitctl/unit-openapi/.gitignore create mode 100644 tools/unitctl/unit-openapi/.openapi-generator-ignore create mode 100644 tools/unitctl/unit-openapi/.openapi-generator/FILES create mode 100644 tools/unitctl/unit-openapi/.openapi-generator/VERSION create mode 100644 tools/unitctl/unit-openapi/Cargo.toml create mode 100644 tools/unitctl/unit-openapi/README.md create mode 100644 tools/unitctl/unit-openapi/openapi-templates/Cargo.mustache create mode 100644 tools/unitctl/unit-openapi/openapi-templates/request.rs create mode 100644 tools/unitctl/unit-openapi/src/apis/error.rs create mode 100644 tools/unitctl/unit-openapi/src/lib.rs create mode 100644 tools/unitctl/unitctl/Cargo.toml create mode 100644 tools/unitctl/unitctl/src/cmd/edit.rs create mode 100644 tools/unitctl/unitctl/src/cmd/execute.rs create mode 100644 tools/unitctl/unitctl/src/cmd/import.rs create mode 100644 tools/unitctl/unitctl/src/cmd/instances.rs create mode 100644 tools/unitctl/unitctl/src/cmd/listeners.rs create mode 100644 tools/unitctl/unitctl/src/cmd/mod.rs create mode 100644 tools/unitctl/unitctl/src/cmd/status.rs create mode 100644 tools/unitctl/unitctl/src/inputfile.rs create mode 100644 tools/unitctl/unitctl/src/known_size.rs create mode 100644 tools/unitctl/unitctl/src/main.rs create mode 100644 tools/unitctl/unitctl/src/output_format.rs create mode 100644 tools/unitctl/unitctl/src/requests.rs create mode 100644 tools/unitctl/unitctl/src/unitctl.rs create mode 100644 tools/unitctl/unitctl/src/unitctl_error.rs create mode 100644 tools/unitctl/unitctl/src/wait.rs (limited to 'tools/unitctl') 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\fR +. +.SH ACKNOWLEDGEMENTS +WRITE ME +. +.SH AUTHOR +Elijah Zupancic \fB\fR +. +.SH COPYRIGHT +Copyright \(co 2022 F5. All Rights Reserved. +.br +License: Apache License 2.0 (Apache-2.0) +.br +Full License Text: 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 for String { + fn from(val: ControlSocket) -> Self { + val.to_string() + } +} + +impl From 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 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 { + 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 { + // 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>(socket_address: S) -> Result { + 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 for ControlSocket { + type Error = UnitClientError; + + fn try_from(socket_address: String) -> Result { + ControlSocket::parse_address(socket_address.as_str()) + } +} + +impl TryFrom<&str> for ControlSocket { + type Error = UnitClientError; + + fn try_from(socket_address: &str) -> Result { + ControlSocket::parse_address(socket_address) + } +} + +impl TryFrom for ControlSocket { + type Error = UnitClientError; + + fn try_from(socket_uri: Uri) -> Result { + 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 { + 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 { + 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(flags: S) -> RuntimeFlags + where + S: Into, + { + 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 { + let flag_parts = self.flags.split_ascii_whitespace().collect::>(); + 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 { + self.get_flag_value("control") + } + + pub fn pid_path(&self) -> Option> { + self.get_flag_value("pid") + .map(PathBuf::from) + .map(PathBuf::into_boxed_path) + } + + pub fn log_path(&self) -> Option> { + self.get_flag_value("log") + .map(PathBuf::from) + .map(PathBuf::into_boxed_path) + } + + pub fn modules_directory(&self) -> Option> { + self.get_flag_value("modules") + .map(PathBuf::from) + .map(PathBuf::into_boxed_path) + } + + pub fn state_directory(&self) -> Option> { + self.get_flag_value("state") + .map(PathBuf::from) + .map(PathBuf::into_boxed_path) + } + + pub fn tmp_directory(&self) -> Option> { + self.get_flag_value("tmp") + .map(PathBuf::from) + .map(PathBuf::into_boxed_path) + } + + pub fn user(&self) -> Option { + self.get_flag_value("user").map(String::from) + } + + pub fn group(&self) -> Option { + 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, + 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::() { + 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 + }}; +} + +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 +where + B: HttpBody + Send + 'static, + B::Data: Send, + B::Error: Into>, +{ + Unix { + client: Client, + }, + Tcp { + client: Client, B>, + }, +} + +impl RemoteClient +where + B: HttpBody + Send + 'static, + B::Data: Send, + B::Error: Into>, +{ + fn client_name(&self) -> &str { + match self { + RemoteClient::Unix { .. } => "Client", + RemoteClient::Tcp { .. } => "Client, Body>", + } + } + + pub fn request(&self, req: Request) -> ResponseFuture { + match self { + RemoteClient::Unix { client } => client.request(req), + RemoteClient::Tcp { client } => client.request(req), + } + } +} + +impl Debug for RemoteClient +where + B: HttpBody + Send + 'static, + B::Data: Send, + B::Error: Into>, +{ + 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>, +} + +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 serde::Deserialize<'de>>( + &self, + mut request: Request, + ) -> Result { + 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 = + 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 { + new_openapi_client!(self, ListenersApiClient, ListenersApi) + } + + pub fn listeners(&self) -> Result, Box> { + let list_listeners = self.listeners_api().get_listeners(); + self.execute_openapi_future(list_listeners) + } + + pub fn execute_openapi_future>, R: for<'de> serde::Deserialize<'de>>( + &self, + future: F, + ) -> Result> { + 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 { + new_openapi_client!(self, StatusApiClient, StatusApi) + } + + pub fn status(&self) -> Result> { + 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; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct UnitStatus { + pub connections: UnitStatusConnections, + pub requests: UnitStatusRequests, + pub applications: HashMap, +} + +#[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, + #[serde(default)] + pub requests: HashMap, +} + +#[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>, + pub version: Option, + pub flags: Option, +} + +impl UnitdCmd { + pub(crate) fn new(full_cmd: S, binary_name: &str) -> Result> + where + S: Into, + { + 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::>(); + 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 = 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(full_cmd: S, binary_name: &str) -> Option> + where + S: Into, + { + let cmd = full_cmd.into(); + if cmd.is_empty() { + return None; + } + + let split = cmd.splitn(2, binary_name).collect::>(); + 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(full_cmd: S) -> Option + where + S: Into, + { + 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::>(); + 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> { + fn parse_configure_settings_from_unitd_stderr_output( + lines: &mut Lines, + ) -> Result> { + 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, Option), + line: String, + ) -> (Option, Option) { + 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); + } else if options_lines.1.is_none() { + return Err(Box::new(UnitdStderrParseError::BuildSettingsNotFound) as Box); + } + + 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 { + 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> { + self.get_flag_value("prefix") + .map(PathBuf::from) + .map(PathBuf::into_boxed_path) + } + + fn join_to_prefix_path(&self, sub_path: S) -> Option> + where + S: Into, + { + self.prefix_path() + .map(|path| path.join(sub_path.into()).into_boxed_path()) + } + + pub fn default_control_api_socket_address(&self) -> Option { + // 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> { + 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> { + 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> { + 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> { + 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> { + 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 { + self.get_flag_value("user").map(String::from) + } + pub fn default_group(&self) -> Option { + 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::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::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); + 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, + pub errors: Vec, +} + +impl Serialize for UnitdInstance { + fn serialize(&self, serializer: S) -> Result + 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::>(); + state.serialize_entry("errors", string_errors)?; + + state.end() + } +} + +impl UnitdInstance { + pub fn running_unitd_instances() -> Vec { + 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) -> Vec { + Self::map_processes_to_instances(processes).into_iter().collect() + } + + fn map_processes_to_instances(processes: Vec) -> Vec { + fn unitd_path_from_process(process: &UnitdProcess) -> Result, 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 { + 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( + &self, + read_flag: fn(RuntimeFlags) -> Option, + read_opts: fn(UnitdConfigureOptions) -> Option, + ) -> Option { + 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 { + self.flag_or_default_option( + |flags| flags.control_api_socket_address(), + |opts| opts.default_control_api_socket_address(), + ) + } + + pub fn pid_path(&self) -> Option> { + self.flag_or_default_option(|flags| flags.pid_path(), |opts| opts.default_pid_path()) + } + + pub fn log_path(&self) -> Option> { + self.flag_or_default_option(|flags| flags.log_path(), |opts| opts.default_log_path()) + } + + pub fn modules_directory(&self) -> Option> { + self.flag_or_default_option( + |flags| flags.modules_directory(), + |opts| opts.default_modules_directory(), + ) + } + + pub fn state_directory(&self) -> Option> { + self.flag_or_default_option(|flags| flags.state_directory(), |opts| opts.default_state_directory()) + } + + pub fn tmp_directory(&self) -> Option> { + 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::>() + .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>) -> Result> { + fn find_unitd_in_system_path() -> Vec { + UNITD_BINARY_NAMES + .iter() + .map(which) + .filter_map(Result::ok) + .collect::>() + } + + 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>( + rng: &mut StdRng, + binary_name: S, + executable_path: Option, + ) -> 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); + 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>, + pub environ: Vec, + pub all_cmds: Vec, + pub working_dir: Option>, + pub child_pids: Vec, + pub user: Option, + pub effective_user: Option, +} + +impl UnitdProcess { + pub fn find_unitd_processes() -> Vec { + 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::>(); + 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> = process.exe().map(|p| p.to_path_buf().into_boxed_path()); + let environ: Vec = process.environ().into(); + let cmd: Vec = process.cmd().into(); + let working_dir: Option> = 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::>(); + + 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::>() + } + + pub fn cmd(&self) -> Option { + 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> { + 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, +} + +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, 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, + method: hyper::Method, + path: String, + query_params: HashMap, + no_return_type: bool, + path_params: HashMap, + form_params: HashMap, + header_params: HashMap, + // TODO: multiple body params are possible technically, but not supported here. + serialized_body: Option, +} + +#[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(mut self, param: T) -> Self { + self.serialized_body = Some(serde_json::to_string(¶m).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, + ) -> Pin> + '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::(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::(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(), ¤t_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 { + 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 { + 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 { + 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, + 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, + 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::(&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> = 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::( + 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(file_extension: S) -> Self + where + S: Into, + { + 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(remote_path: S) -> Self + where + S: Into, + { + 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), + // Data that is on the file system where the format is explicitly specified + FileWithFormat(Box, InputFormat), +} + +impl InputFile { + /// Creates a new instance of `InputFile` from a string + pub fn new(file_path_or_dash: S, remote_path: S) -> Self + where + S: Into, + { + 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 { + let reader: Box = 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 = 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> for &InputFile { + type Error = UnitctlError; + + fn try_into(self) -> Result, Self::Error> { + let reader: Box = 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> for &InputFile { + type Error = UnitctlError; + + fn try_into(self) -> Result, Self::Error> { + let mut buf: Vec = vec![]; + let mut reader: Box = self.try_into()?; + reader + .read_to_end(&mut buf) + .map_err(|e| UnitctlError::IoError { source: e })?; + Ok(buf) + } +} + +impl TryInto for &InputFile { + type Error = UnitctlError; + + fn try_into(self) -> Result { + let known_size: KnownSize = match self { + InputFile::Stdin(_) => { + let mut buf: Vec = 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), + Read(Box, 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>; + + fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + 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) { + (0, Some(self.len() as usize)) + } +} + +impl From for Box { + 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 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(&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 { + 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 { + 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 { + let bytes: Vec = 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<(), 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 = 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 serde::Deserialize<'de>>( + client: &UnitClient, + method: &str, + path: &str, + input_file: Option<&InputFile>, +) -> Result { + 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 serde::Deserialize<'de>>( + client: &UnitClient, + method: &str, + path: &str, + mime_type: Option, + read: KnownSize, +) -> Result { + 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, + #[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, + #[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, + #[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, + #[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::try_from(s).map_err(|e| ClapError::raw(ValueValidation, e.to_string())) +} + +fn parse_http_method(s: &str) -> Result { + 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 { + s.parse::() + .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 { + // 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; +} + +impl OptionControlSocket for Option { + fn instance_value_if_none(&self) -> Result { + if let Some(control_socket) = self { + Ok(control_socket.to_owned()) + } else { + find_socket_address_from_instance() + } + } +} + +trait ResultControlSocket { + fn and_validate(self) -> Result; +} + +impl ResultControlSocket for Result { + fn and_validate(self) -> Result { + 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 { + 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"), + } +} -- cgit From 6e8f7bbb91e7069d82abd22fbe8d0fcaa1bb2f8c Mon Sep 17 00:00:00 2001 From: Ava Hahn Date: Thu, 25 Apr 2024 20:02:26 -0700 Subject: tools/unitctl: Initial Docker Procedures * move UnitdProcess serialization logic into UnitdProcess * filter out docker processes from process output on Linux * initial implementation of a UnitdContainer type * initial implementation of a docker container search for unitd * pull out custom openapi future executor and use same tokio runtime as docker client * refactor openapi client to not manage its own tokio runtime * process mount points per docker container * correctly output docker container info in relevant unitd instances * create UnitdProcess from UnitdContainer * UnitdProcess now owns UnitdContainer * get and parse container details from docker API * introduce procedure to rewrite file paths based on docker container mounts * test path rewrite facilities * apply path rewrite to unix socket Signed-off-by: Ava Hahn --- tools/unitctl/Cargo.lock | 474 ++++++++++++++++++++- tools/unitctl/unit-client-rs/Cargo.toml | 2 + tools/unitctl/unit-client-rs/src/lib.rs | 1 + tools/unitctl/unit-client-rs/src/unit_client.rs | 146 +++---- tools/unitctl/unit-client-rs/src/unitd_cmd.rs | 5 +- tools/unitctl/unit-client-rs/src/unitd_docker.rs | 282 ++++++++++++ tools/unitctl/unit-client-rs/src/unitd_instance.rs | 77 +++- tools/unitctl/unit-client-rs/src/unitd_process.rs | 34 +- tools/unitctl/unitctl/src/cmd/edit.rs | 7 +- tools/unitctl/unitctl/src/cmd/execute.rs | 17 +- tools/unitctl/unitctl/src/cmd/import.rs | 50 ++- tools/unitctl/unitctl/src/cmd/instances.rs | 4 +- tools/unitctl/unitctl/src/cmd/listeners.rs | 5 +- tools/unitctl/unitctl/src/cmd/status.rs | 5 +- tools/unitctl/unitctl/src/main.rs | 15 +- tools/unitctl/unitctl/src/requests.rs | 17 +- tools/unitctl/unitctl/src/wait.rs | 34 +- 17 files changed, 990 insertions(+), 185 deletions(-) create mode 100644 tools/unitctl/unit-client-rs/src/unitd_docker.rs (limited to 'tools/unitctl') diff --git a/tools/unitctl/Cargo.lock b/tools/unitctl/Cargo.lock index 2940b3ae..2acbfb9a 100644 --- a/tools/unitctl/Cargo.lock +++ b/tools/unitctl/Cargo.lock @@ -26,6 +26,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.5" @@ -128,6 +143,12 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +[[package]] +name = "base64" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" + [[package]] name = "bindgen" version = "0.69.4" @@ -172,6 +193,56 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bollard" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aed08d3adb6ebe0eff737115056652670ae290f177759aac19c30456135f94c" +dependencies = [ + "base64 0.22.0", + "bollard-stubs", + "bytes", + "futures-core", + "futures-util", + "hex", + "http 1.1.0", + "http-body-util", + "hyper 1.3.1", + "hyper-named-pipe", + "hyper-util", + "hyperlocal-next", + "log", + "pin-project-lite", + "serde", + "serde_derive", + "serde_json", + "serde_repr", + "serde_urlencoded", + "thiserror", + "tokio", + "tokio-util", + "tower-service", + "url", + "winapi", +] + +[[package]] +name = "bollard-stubs" +version = "1.44.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709d9aa1c37abb89d40f19f5d0ad6f0d88cb1581264e571c9350fc5bb89cf1c5" +dependencies = [ + "serde", + "serde_repr", + "serde_with", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "bytes" version = "1.2.1" @@ -204,6 +275,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.52.0", +] + [[package]] name = "clang-sys" version = "1.7.0" @@ -355,6 +439,16 @@ version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f8a51dd197fa6ba5b4dc98a990a43cc13693c23eb0089ebb0fcc1f04152bca6" +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "digest" version = "0.10.6" @@ -377,6 +471,12 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.1" @@ -568,6 +668,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + [[package]] name = "heck" version = "0.4.0" @@ -609,6 +715,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.5" @@ -616,7 +733,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", - "http", + "http 0.2.8", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +dependencies = [ + "bytes", + "futures-core", + "http 1.1.0", + "http-body 1.0.0", "pin-project-lite", ] @@ -642,8 +782,8 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http", - "http-body", + "http 0.2.8", + "http-body 0.4.5", "httparse", "httpdate", "itoa", @@ -655,6 +795,40 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-named-pipe" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" +dependencies = [ + "hex", + "hyper 1.3.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", + "winapi", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -662,12 +836,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper", + "hyper 0.14.27", "native-tls", "tokio", "tokio-native-tls", ] +[[package]] +name = "hyper-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "hyper 1.3.1", + "pin-project-lite", + "socket2 0.5.5", + "tokio", + "tower", + "tower-service", + "tracing", +] + [[package]] name = "hyperlocal" version = "0.8.0" @@ -676,11 +870,49 @@ checksum = "0fafdf7b2b2de7c9784f76e02c0935e65a8117ec3b768644379983ab333ac98c" dependencies = [ "futures-util", "hex", - "hyper", + "hyper 0.14.27", "pin-project", "tokio", ] +[[package]] +name = "hyperlocal-next" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acf569d43fa9848e510358c07b80f4adf34084ddc28c6a4a651ee8474c070dcc" +dependencies = [ + "hex", + "http-body-util", + "hyper 1.3.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.3.0" @@ -698,7 +930,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", + "serde", ] [[package]] @@ -725,6 +969,15 @@ dependencies = [ "libc", ] +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "json5" version = "0.4.1" @@ -883,6 +1136,12 @@ dependencies = [ "serde", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" version = "0.2.15" @@ -1055,6 +1314,12 @@ version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1238,7 +1503,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35e4980fa29e4c4b212ffb3db068a564cbf560e51d3944b7c88bd8bf5bec64f4" dependencies = [ - "base64", + "base64 0.21.5", "rustls-pki-types", ] @@ -1316,22 +1581,22 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.147" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" +checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.147" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" +checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.60", ] [[package]] @@ -1340,19 +1605,59 @@ version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" dependencies = [ - "indexmap", + "indexmap 1.9.1", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", "itoa", "ryu", "serde", ] +[[package]] +name = "serde_with" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c85f8e96d1d6857f13768fcbd895fcb06225510022a2774ed8b5150581847b0" +dependencies = [ + "base64 0.22.0", + "chrono", + "hex", + "indexmap 1.9.1", + "indexmap 2.2.6", + "serde", + "serde_derive", + "serde_json", + "time", +] + [[package]] name = "serde_yaml" version = "0.9.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d232d893b10de3eb7258ff01974d6ee20663d8e833263c99409d4b13a0209da" dependencies = [ - "indexmap", + "indexmap 1.9.1", "itoa", "ryu", "serde", @@ -1385,6 +1690,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + [[package]] name = "socket2" version = "0.4.7" @@ -1493,6 +1804,37 @@ dependencies = [ "syn 1.0.103", ] +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1515,6 +1857,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" dependencies = [ "backtrace", + "bytes", "libc", "mio", "num_cpus", @@ -1545,6 +1888,42 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + [[package]] name = "tower-service" version = "0.3.2" @@ -1558,6 +1937,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", + "log", "pin-project-lite", "tracing-core", ] @@ -1614,13 +1994,15 @@ dependencies = [ name = "unit-client-rs" version = "0.4.0-beta" dependencies = [ + "bollard", "custom_error", "futures", "hex", - "hyper", + "hyper 0.14.27", "hyper-tls", "hyperlocal", "rand", + "regex", "rustls", "serde", "serde_json", @@ -1634,10 +2016,10 @@ dependencies = [ name = "unit-openapi" version = "0.4.0-beta" dependencies = [ - "base64", + "base64 0.21.5", "futures", - "http", - "hyper", + "http 0.2.8", + "hyper 0.14.27", "serde", "serde_derive", "serde_json", @@ -1652,7 +2034,7 @@ dependencies = [ "colored_json", "custom_error", "futures", - "hyper", + "hyper 0.14.27", "hyper-tls", "hyperlocal", "json5", @@ -1735,6 +2117,60 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.60", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + [[package]] name = "which" version = "4.4.2" diff --git a/tools/unitctl/unit-client-rs/Cargo.toml b/tools/unitctl/unit-client-rs/Cargo.toml index d3b2f9cf..3e48ee23 100644 --- a/tools/unitctl/unit-client-rs/Cargo.toml +++ b/tools/unitctl/unit-client-rs/Cargo.toml @@ -27,6 +27,8 @@ which = "5.0" unit-openapi = { path = "../unit-openapi" } rustls = "0.23.5" +bollard = "0.16.1" +regex = "1.10.4" [dev-dependencies] rand = "0.8.5" diff --git a/tools/unitctl/unit-client-rs/src/lib.rs b/tools/unitctl/unit-client-rs/src/lib.rs index dca8a86f..a0933f42 100644 --- a/tools/unitctl/unit-client-rs/src/lib.rs +++ b/tools/unitctl/unit-client-rs/src/lib.rs @@ -10,6 +10,7 @@ mod runtime_flags; pub mod unit_client; mod unitd_cmd; pub mod unitd_configure_options; +pub mod unitd_docker; pub mod unitd_instance; pub mod unitd_process; mod unitd_process_user; diff --git a/tools/unitctl/unit-client-rs/src/unit_client.rs b/tools/unitctl/unit-client-rs/src/unit_client.rs index b856fd20..7456b106 100644 --- a/tools/unitctl/unit-client-rs/src/unit_client.rs +++ b/tools/unitctl/unit-client-rs/src/unit_client.rs @@ -1,7 +1,6 @@ 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}; @@ -13,7 +12,6 @@ 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; @@ -168,51 +166,38 @@ where #[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>, } impl UnitClient { - pub fn new_with_runtime(control_socket: ControlSocket, runtime: Runtime) -> Self { + pub fn new(control_socket: ControlSocket) -> Self { if control_socket.is_local_socket() { - Self::new_unix(control_socket, runtime) + Self::new_unix(control_socket) } else { - Self::new_http(control_socket, runtime) + Self::new_http(control_socket) } } - 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 { + pub fn new_http(control_socket: ControlSocket) -> 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 { + pub fn new_unix(control_socket: ControlSocket) -> 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 serde::Deserialize<'de>>( + pub async fn send_request_and_deserialize_response serde::Deserialize<'de>>( &self, mut request: Request, ) -> Result { @@ -223,34 +208,32 @@ impl UnitClient { 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 = - serde_json::from_reader(reader).map_err(|error| UnitClientError::JsonError { - source: error, - path: path.to_string(), - })?; - - return Err(UnitClientError::HttpResponseJsonBodyError { - status, + 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 = + serde_json::from_reader(reader).map_err(|error| UnitClientError::JsonError { + source: error, 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, + })?; + + 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(), }) } @@ -258,23 +241,17 @@ impl UnitClient { new_openapi_client!(self, ListenersApiClient, ListenersApi) } - pub fn listeners(&self) -> Result, Box> { - let list_listeners = self.listeners_api().get_listeners(); - self.execute_openapi_future(list_listeners) - } - - pub fn execute_openapi_future>, R: for<'de> serde::Deserialize<'de>>( - &self, - future: F, - ) -> Result> { - 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()) + pub async fn listeners(&self) -> Result, Box> { + self.listeners_api().get_listeners().await.or_else(|err| { + if let OpenAPIError::Hyper(hyper_error) = err { + Err(Box::new(UnitClientError::new( + hyper_error, + self.control_socket.to_string(), + "".to_string(), + ))) } else { - UnitClientError::OpenAPIError { source: error } - }; - - Box::new(remapped_error) + Err(Box::new(UnitClientError::OpenAPIError { source: err })) + } }) } @@ -282,13 +259,22 @@ impl UnitClient { new_openapi_client!(self, StatusApiClient, StatusApi) } - pub fn status(&self) -> Result> { - let status = self.status_api().get_status(); - self.execute_openapi_future(status) + pub async fn status(&self) -> Result> { + self.status_api().get_status().await.or_else(|err| { + if let OpenAPIError::Hyper(hyper_error) = err { + Err(Box::new(UnitClientError::new( + hyper_error, + self.control_socket.to_string(), + "".to_string(), + ))) + } else { + Err(Box::new(UnitClientError::OpenAPIError { source: err })) + } + }) } - pub fn is_running(&self) -> bool { - self.status().is_ok() + pub async fn is_running(&self) -> bool { + self.status().await.is_ok() } } @@ -336,9 +322,9 @@ mod tests { use super::*; // Integration tests - #[test] - fn can_connect_to_unit_api() { - match UnitdInstance::running_unitd_instances().first() { + #[tokio::test] + async fn can_connect_to_unit_api() { + match UnitdInstance::running_unitd_instances().await.first() { Some(unit_instance) => { let control_api_socket_address = unit_instance .control_api_socket_address() @@ -346,7 +332,7 @@ mod tests { 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()); + assert!(unit_client.is_running().await); } None => { eprintln!("No running unitd instances found - skipping test"); @@ -354,9 +340,9 @@ mod tests { } } - #[test] - fn can_get_unit_status() { - match UnitdInstance::running_unitd_instances().first() { + #[tokio::test] + async fn can_get_unit_status() { + match UnitdInstance::running_unitd_instances().await.first() { Some(unit_instance) => { let control_api_socket_address = unit_instance .control_api_socket_address() @@ -364,7 +350,7 @@ mod tests { 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"); + let status = unit_client.status().await.expect("Unable to get unit status"); println!("Unit status: {:?}", status); } None => { @@ -373,9 +359,9 @@ mod tests { } } - #[test] - fn can_get_unit_listeners() { - match UnitdInstance::running_unitd_instances().first() { + #[tokio::test] + async fn can_get_unit_listeners() { + match UnitdInstance::running_unitd_instances().await.first() { Some(unit_instance) => { let control_api_socket_address = unit_instance .control_api_socket_address() @@ -383,7 +369,7 @@ mod tests { 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"); + unit_client.listeners().await.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 index c4883ed5..17563cb0 100644 --- a/tools/unitctl/unit-client-rs/src/unitd_cmd.rs +++ b/tools/unitctl/unit-client-rs/src/unitd_cmd.rs @@ -28,11 +28,13 @@ impl UnitdCmd { .expect("Unable to parse cmd") .splitn(2, " [") .collect::>(); + 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 = Some(parts[0].to_string()); + + let version = 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]); @@ -69,6 +71,7 @@ impl UnitdCmd { if cmd.is_empty() { return None; } + // Split out everything in between the brackets [ and ] let split = cmd.trim_end_matches(']').splitn(2, '[').collect::>(); if split.is_empty() { diff --git a/tools/unitctl/unit-client-rs/src/unitd_docker.rs b/tools/unitctl/unit-client-rs/src/unitd_docker.rs new file mode 100644 index 00000000..d5028afc --- /dev/null +++ b/tools/unitctl/unit-client-rs/src/unitd_docker.rs @@ -0,0 +1,282 @@ +use std::collections::HashMap; +use std::fs::read_to_string; +use std::path::PathBuf; + +use crate::unitd_process::UnitdProcess; + +use bollard::secret::ContainerInspectResponse; +use regex::Regex; +use serde::ser::SerializeMap; +use serde::{Serialize, Serializer}; + +use bollard::{models::ContainerSummary, Docker}; + +#[derive(Clone, Debug)] +pub struct UnitdContainer { + pub container_id: Option, + pub container_image: String, + pub command: Option, + pub mounts: HashMap, + pub platform: String, + details: Option, +} + +impl From<&ContainerSummary> for UnitdContainer { + fn from(ctr: &ContainerSummary) -> Self { + // we assume paths from the docker api are absolute + // they certainly have to be later... + let mut mounts = HashMap::new(); + if let Some(mts) = &ctr.mounts { + for i in mts { + if let Some(ref src) = i.source { + if let Some(ref dest) = i.destination { + mounts.insert(PathBuf::from(dest.clone()), PathBuf::from(src.clone())); + } + } + } + } + + UnitdContainer { + container_id: ctr.id.clone(), + container_image: format!( + "{} (docker)", + ctr.image.clone().unwrap_or(String::from("unknown container")), + ), + command: ctr.command.clone(), + mounts: mounts, + platform: String::from("Docker"), + details: None, + } + } +} + +impl From<&UnitdContainer> for UnitdProcess { + fn from(ctr: &UnitdContainer) -> Self { + let version = ctr.details.as_ref().and_then(|details| { + details.config.as_ref().and_then(|conf| { + conf.labels.as_ref().and_then(|labels| { + labels + .get("org.opencontainers.image.version") + .and_then(|version| Some(version.clone())) + }) + }) + }); + let command = ctr.command.clone().and_then(|cmd| { + Some(format!( + "{}{} [{}{}]", + "unit: main v", + version.or(Some(String::from(""))).unwrap(), + ctr.container_image, + ctr.rewrite_socket( + cmd.strip_prefix("/usr/local/bin/docker-entrypoint.sh") + .or_else(|| Some("")) + .unwrap() + .to_string()) + )) + }); + let mut cmds = vec![]; + let _ = command.map_or((), |cmd| cmds.push(cmd)); + UnitdProcess { + all_cmds: cmds, + binary_name: ctr.container_image.clone(), + process_id: ctr + .details + .as_ref() + .and_then(|details| { + details + .state + .as_ref() + .and_then(|state| state.pid.and_then(|pid| Some(pid.clone() as u64))) + }) + .or(Some(0 as u64)) + .unwrap(), + executable_path: None, + environ: vec![], + working_dir: ctr.details.as_ref().and_then(|details| { + details.config.as_ref().and_then(|conf| { + Some( + PathBuf::from( + conf.working_dir + .as_ref() + .map_or(String::new(), |dir| ctr.host_path(dir.clone())), + ) + .into_boxed_path(), + ) + }) + }), + child_pids: vec![], + user: None, + effective_user: None, + container: Some(ctr.clone()), + } + } +} + +impl Serialize for UnitdContainer { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_map(Some(5))?; + state.serialize_entry("container_id", &self.container_id)?; + state.serialize_entry("container_image", &self.container_image)?; + state.serialize_entry("command", &self.command)?; + state.serialize_entry("mounts", &self.mounts)?; + state.serialize_entry("platform", &self.platform)?; + state.end() + } +} + +impl UnitdContainer { + pub async fn find_unitd_containers() -> Vec { + if let Ok(docker) = Docker::connect_with_local_defaults() { + match docker.list_containers::(None).await { + Err(e) => { + eprintln!("{}", e); + vec![] + } + Ok(summary) => { + // cant do this functionally because of the async call + let mut mapped = vec![]; + for ctr in summary { + if ctr.clone().image.or(Some(String::new())).unwrap().contains("unit") { + let mut c = UnitdContainer::from(&ctr); + if let Some(names) = ctr.names { + if names.len() > 0 { + let name = names[0].strip_prefix("/").or(Some(names[0].as_str())).unwrap(); + if let Ok(cir) = docker.inspect_container(name, None).await { + c.details = Some(cir); + } + } + } + mapped.push(c); + } + } + mapped + } + } + } else { + vec![] + } + } + + pub fn host_path(&self, container_path: String) -> String { + let cp = PathBuf::from(container_path); + + // get only possible mount points + // sort to deepest mountpoint first + // assumed deepest possible mount point takes precedence + let mut keys = self + .mounts + .clone() + .into_keys() + .filter(|mp| cp.as_path().starts_with(mp)) + .collect::>(); + keys.sort_by_key(|a| 0 as isize - a.ancestors().count() as isize); + + // either return translated path or original prefixed with "container" + if keys.len() > 0 { + self.mounts[&keys[0]] + .clone() + .join( + cp.as_path() + .strip_prefix(keys[0].clone()) + .expect("error checking path prefix"), + ) + .to_string_lossy() + .to_string() + } else { + format!(":{}", cp.display()) + } + } + + pub fn rewrite_socket(&self, command: String) -> String { + command + .split(" ") + .map(|tok| if tok.starts_with("unix:") { + format!("unix:{}", self.host_path( + tok.strip_prefix("unix:") + .unwrap() + .to_string())) + } else { + tok.to_string() + }) + .collect::>() + .join(" ") + } + + pub fn container_is_running(&self) -> Option { + self.details + .as_ref() + .and_then(|details| details.state.as_ref().and_then(|state| state.running)) + } +} + +/* Returns either 64 char docker container ID or None */ +pub fn pid_is_dockerized(pid: u64) -> bool { + let cg_filepath = format!("/proc/{}/cgroup", pid); + match read_to_string(cg_filepath) { + Err(e) => { + eprintln!("{}", e); + false + } + Ok(contents) => { + let docker_re = Regex::new(r"docker-([a-zA-Z0-9]{64})").unwrap(); + docker_re.is_match(contents.as_str()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_path_translation() { + let mut mounts = HashMap::new(); + mounts.insert("/1/2/3/4/5/6/7".into(), "/0".into()); + mounts.insert("/root".into(), "/1".into()); + mounts.insert("/root/mid".into(), "/2".into()); + mounts.insert("/root/mid/child".into(), "/3".into()); + mounts.insert("/mid/child".into(), "/4".into()); + mounts.insert("/child".into(), "/5".into()); + + let ctr = UnitdContainer { + container_id: None, + container_image: String::from(""), + command: None, + platform: "test".to_string(), + details: None, + mounts: mounts, + }; + + assert_eq!( + "/3/c2/test".to_string(), + ctr.host_path("/root/mid/child/c2/test".to_string()) + ); + assert_eq!( + ":/path/to/conf".to_string(), + ctr.host_path("/path/to/conf".to_string()) + ); + } + + #[test] + fn test_unix_sock_path_translate() { + let mut mounts = HashMap::new(); + mounts.insert("/var/run".into(), "/tmp".into()); + + let ctr = UnitdContainer { + container_id: None, + container_image: String::from(""), + command: None, + platform: "test".to_string(), + details: None, + mounts: mounts, + }; + + assert_eq!( + ctr.rewrite_socket("unitd --no-daemon --control unix:/var/run/control.unit.sock".to_string()), + "unitd --no-daemon --control unix:/tmp/control.unit.sock".to_string()); + + } +} diff --git a/tools/unitctl/unit-client-rs/src/unitd_instance.rs b/tools/unitctl/unit-client-rs/src/unitd_instance.rs index 9467fcb7..86f8e73d 100644 --- a/tools/unitctl/unit-client-rs/src/unitd_instance.rs +++ b/tools/unitctl/unit-client-rs/src/unitd_instance.rs @@ -1,4 +1,5 @@ use crate::unit_client::UnitClientError; +use crate::unitd_docker::UnitdContainer; use serde::ser::SerializeMap; use serde::{Serialize, Serializer}; use std::error::Error as StdError; @@ -25,7 +26,7 @@ impl Serialize for UnitdInstance { where S: Serializer, { - let mut state = serializer.serialize_map(Some(15))?; + let mut state = serializer.serialize_map(Some(11))?; let runtime_flags = self .process .cmd() @@ -34,13 +35,9 @@ impl Serialize for UnitdInstance { 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("process", &self.process)?; 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())?; @@ -56,8 +53,19 @@ impl Serialize for UnitdInstance { } impl UnitdInstance { - pub fn running_unitd_instances() -> Vec { - Self::collect_unitd_processes(UnitdProcess::find_unitd_processes()) + pub async fn running_unitd_instances() -> Vec { + Self::collect_unitd_processes( + UnitdProcess::find_unitd_processes() + .into_iter() + .chain( + UnitdContainer::find_unitd_containers() + .await + .into_iter() + .map(|x| UnitdProcess::from(&x)) + .collect::>(), + ) + .collect(), + ) } /// Find all running unitd processes and convert them into UnitdInstances and filter @@ -91,11 +99,14 @@ impl UnitdInstance { pid: process.process_id, })?; Ok(new_path) - } else { + } else if process.container.is_none() { Err(UnitClientError::UnitdProcessParseError { message: "Unable to get absolute unitd executable path from process".to_string(), pid: process.process_id, }) + } else { + // container case + Ok(PathBuf::from("/").into_boxed_path()) } } None => Err(UnitClientError::UnitdProcessParseError { @@ -107,7 +118,30 @@ impl UnitdInstance { 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(_) if process.container.is_some() => { + let mut err = vec![]; + // double check that it is running + let running = process.container + .as_ref() + .unwrap() + .container_is_running(); + + if running.is_none() || !running.unwrap() { + err.push(UnitClientError::UnitdProcessParseError{ + message: "process container is not running".to_string(), + pid: process.process_id, + }); + } + + UnitdInstance { + process: process.to_owned(), + configure_options: None, + errors: err, + } + }, + 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), @@ -250,10 +284,22 @@ impl fmt::Display for UnitdInstance { 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)?; + writeln!(f, " Configure options: {}", configure_flags)?; + + if let Some(ctr) = &self.process.container { + writeln!(f, " Container:")?; + writeln!(f, " Platform: {}", ctr.platform)?; + if let Some(id) = ctr.container_id.clone() { + writeln!(f, " Container ID: {}", id)?; + } + writeln!(f, " Mounts:")?; + for (k, v) in &ctr.mounts { + writeln!(f, " {} => {}", k.to_string_lossy(), v.to_string_lossy())?; + } + } if !self.errors.is_empty() { - write!(f, "\n Errors:")?; + write!(f, " Errors:")?; for error in &self.errors { write!(f, "\n {}", error)?; } @@ -302,9 +348,9 @@ mod tests { 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| { + #[tokio::test] + async fn can_find_unitd_instances() { + UnitdInstance::running_unitd_instances().await.iter().for_each(|p| { println!("{:?}", p); println!("Runtime Flags: {:?}", p.process.cmd().map(|c| c.flags)); println!("Temp directory: {:?}", p.tmp_directory()); @@ -326,6 +372,7 @@ mod tests { child_pids: vec![], user: None, effective_user: None, + container: None, } } diff --git a/tools/unitctl/unit-client-rs/src/unitd_process.rs b/tools/unitctl/unit-client-rs/src/unitd_process.rs index b8604e89..2a78bfc6 100644 --- a/tools/unitctl/unit-client-rs/src/unitd_process.rs +++ b/tools/unitctl/unit-client-rs/src/unitd_process.rs @@ -1,6 +1,9 @@ use crate::unitd_cmd::UnitdCmd; +use crate::unitd_docker::{pid_is_dockerized, UnitdContainer}; use crate::unitd_instance::UNITD_BINARY_NAMES; use crate::unitd_process_user::UnitdProcessUser; +use serde::ser::SerializeMap; +use serde::{Serialize, Serializer}; use std::collections::HashMap; use std::path::Path; use sysinfo::{Pid, Process, ProcessRefreshKind, System, UpdateKind, Users}; @@ -16,6 +19,23 @@ pub struct UnitdProcess { pub child_pids: Vec, pub user: Option, pub effective_user: Option, + pub container: Option, +} + +impl Serialize for UnitdProcess { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_map(Some(6))?; + state.serialize_entry("pid", &self.process_id)?; + state.serialize_entry("user", &self.user)?; + state.serialize_entry("effective_user", &self.effective_user)?; + state.serialize_entry("executable", &self.executable_path())?; + state.serialize_entry("child_pids", &self.child_pids)?; + state.serialize_entry("container", &self.container)?; + state.end() + } } impl UnitdProcess { @@ -41,10 +61,15 @@ impl UnitdProcess { .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, + #[cfg(target_os = "linux")] + if pid_is_dockerized(p.0.as_u32().into()) { + false + } else { + let parent_pid = p.1.parent(); + match parent_pid { + Some(pid) => !unitd_processes.contains_key(&pid), + None => false, + } } }) .map(|p| { @@ -85,6 +110,7 @@ impl UnitdProcess { child_pids, user, effective_user, + container: None, } }) .collect::>() diff --git a/tools/unitctl/unitctl/src/cmd/edit.rs b/tools/unitctl/unitctl/src/cmd/edit.rs index cbe01289..21bba519 100644 --- a/tools/unitctl/unitctl/src/cmd/edit.rs +++ b/tools/unitctl/unitctl/src/cmd/edit.rs @@ -18,11 +18,11 @@ const EDITOR_KNOWN_LIST: [&str; 8] = [ "emacs", ]; -pub(crate) fn cmd(cli: &UnitCtl, output_format: OutputFormat) -> Result<(), UnitctlError> { - let control_socket = wait::wait_for_socket(cli)?; +pub(crate) async fn cmd(cli: &UnitCtl, output_format: OutputFormat) -> Result<(), UnitctlError> { + let control_socket = wait::wait_for_socket(cli).await?; let client = UnitClient::new(control_socket); // Get latest configuration - let current_config = send_empty_body_deserialize_response(&client, "GET", "/config")?; + let current_config = send_empty_body_deserialize_response(&client, "GET", "/config").await?; // Write JSON to temporary file - this file will automatically be deleted by the OS when // the last file handle to it is removed. @@ -54,6 +54,7 @@ pub(crate) fn cmd(cli: &UnitCtl, output_format: OutputFormat) -> Result<(), Unit // Send edited file to UNIT to overwrite current configuration send_and_validate_config_deserialize_response(&client, "PUT", "/config", Some(&inputfile)) + .await .and_then(|status| output_format.write_to_stdout(&status)) } diff --git a/tools/unitctl/unitctl/src/cmd/execute.rs b/tools/unitctl/unitctl/src/cmd/execute.rs index 60957a83..1bde437d 100644 --- a/tools/unitctl/unitctl/src/cmd/execute.rs +++ b/tools/unitctl/unitctl/src/cmd/execute.rs @@ -8,14 +8,14 @@ use crate::wait; use crate::{OutputFormat, UnitctlError}; use unit_client_rs::unit_client::UnitClient; -pub(crate) fn cmd( +pub(crate) async fn cmd( cli: &UnitCtl, output_format: &OutputFormat, input_file: &Option, method: &str, path: &str, ) -> Result<(), UnitctlError> { - let control_socket = wait::wait_for_socket(cli)?; + let control_socket = wait::wait_for_socket(cli).await?; let client = UnitClient::new(control_socket); let path_trimmed = path.trim(); @@ -28,10 +28,10 @@ pub(crate) fn cmd( eprintln!("Cannot use GET method with input file - ignoring input file"); } - send_and_deserialize(client, method_upper, input_file_arg, path_trimmed, output_format) + send_and_deserialize(client, method_upper, input_file_arg, path_trimmed, output_format).await } -fn send_and_deserialize( +async fn send_and_deserialize( client: UnitClient, method: String, input_file: Option, @@ -43,7 +43,8 @@ fn send_and_deserialize( // 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::(&client, method.as_str(), path, input_file.as_ref())?; + let script = + send_body_deserialize_response::(&client, method.as_str(), path, input_file.as_ref()).await?; println!("{}", script); return Ok(()); } @@ -52,17 +53,17 @@ fn send_and_deserialize( 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)) + send_and_validate_config_deserialize_response(&client, method.as_str(), path, Some(&input_file)).await // TLS certificate data } else if input_file.is_pem_bundle() { - send_and_validate_pem_data_deserialize_response(&client, method.as_str(), path, &input_file) + send_and_validate_pem_data_deserialize_response(&client, method.as_str(), path, &input_file).await // 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), + None => send_empty_body_deserialize_response(&client, method.as_str(), path).await, } .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 index e5e57456..81f925bc 100644 --- a/tools/unitctl/unitctl/src/cmd/import.rs +++ b/tools/unitctl/unitctl/src/cmd/import.rs @@ -43,24 +43,25 @@ impl UploadFormat { } } -pub fn cmd(cli: &UnitCtl, directory: &PathBuf) -> Result<(), UnitctlError> { +pub async 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 control_socket = wait::wait_for_socket(cli).await?; let client = UnitClient::new(control_socket); - - let results: Vec> = WalkDir::new(directory) + let mut results = vec![]; + for i in 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(); + { + results.push(process_entry(i, &client).await); + } if results.iter().filter(|r| r.is_err()).count() == results.len() { Err(UnitctlError::NoFilesImported) @@ -70,7 +71,7 @@ pub fn cmd(cli: &UnitCtl, directory: &PathBuf) -> Result<(), UnitctlError> { } } -fn process_entry(entry: DirEntry, client: &UnitClient) -> Result<(), UnitctlError> { +async fn process_entry(entry: DirEntry, client: &UnitClient) -> Result<(), UnitctlError> { let input_file = InputFile::from(entry.path()); if input_file.format() == InputFormat::Unknown { println!( @@ -86,25 +87,34 @@ fn process_entry(entry: DirEntry, client: &UnitClient) -> Result<(), UnitctlErro // 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 _ = requests::send_empty_body_deserialize_response(client, "DELETE", upload_path.as_str()) + .await + .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::Config => { + requests::send_and_validate_config_deserialize_response( + client, + "PUT", + upload_path.as_str(), + Some(&input_file), + ) + .await + } UploadFormat::PemBundle => { requests::send_and_validate_pem_data_deserialize_response(client, "PUT", upload_path.as_str(), &input_file) + .await + } + UploadFormat::Javascript => { + requests::send_body_deserialize_response::( + client, + "PUT", + upload_path.as_str(), + Some(&input_file), + ) + .await } - UploadFormat::Javascript => requests::send_body_deserialize_response::( - client, - "PUT", - upload_path.as_str(), - Some(&input_file), - ), }; match result { diff --git a/tools/unitctl/unitctl/src/cmd/instances.rs b/tools/unitctl/unitctl/src/cmd/instances.rs index 26e15027..09e3eb0f 100644 --- a/tools/unitctl/unitctl/src/cmd/instances.rs +++ b/tools/unitctl/unitctl/src/cmd/instances.rs @@ -1,8 +1,8 @@ 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(); +pub(crate) async fn cmd(output_format: OutputFormat) -> Result<(), UnitctlError> { + let instances = UnitdInstance::running_unitd_instances().await; if instances.is_empty() { Err(UnitctlError::NoUnitInstancesError) } else if output_format.eq(&OutputFormat::Text) { diff --git a/tools/unitctl/unitctl/src/cmd/listeners.rs b/tools/unitctl/unitctl/src/cmd/listeners.rs index 081a6cd9..4eb48355 100644 --- a/tools/unitctl/unitctl/src/cmd/listeners.rs +++ b/tools/unitctl/unitctl/src/cmd/listeners.rs @@ -3,11 +3,12 @@ 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)?; +pub async fn cmd(cli: &UnitCtl, output_format: OutputFormat) -> Result<(), UnitctlError> { + let control_socket = wait::wait_for_socket(cli).await?; let client = UnitClient::new(control_socket); client .listeners() + .await .map_err(|e| UnitctlError::UnitClientError { source: *e }) .and_then(|response| output_format.write_to_stdout(&response)) } diff --git a/tools/unitctl/unitctl/src/cmd/status.rs b/tools/unitctl/unitctl/src/cmd/status.rs index 1f40735f..2cac5714 100644 --- a/tools/unitctl/unitctl/src/cmd/status.rs +++ b/tools/unitctl/unitctl/src/cmd/status.rs @@ -3,11 +3,12 @@ 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)?; +pub async fn cmd(cli: &UnitCtl, output_format: OutputFormat) -> Result<(), UnitctlError> { + let control_socket = wait::wait_for_socket(cli).await?; let client = UnitClient::new(control_socket); client .status() + .await .map_err(|e| UnitctlError::UnitClientError { source: *e }) .and_then(|response| output_format.write_to_stdout(&response)) } diff --git a/tools/unitctl/unitctl/src/main.rs b/tools/unitctl/unitctl/src/main.rs index 2e8cedf1..a52c4ed3 100644 --- a/tools/unitctl/unitctl/src/main.rs +++ b/tools/unitctl/unitctl/src/main.rs @@ -23,26 +23,27 @@ mod unitctl; mod unitctl_error; mod wait; -fn main() -> Result<(), UnitctlError> { +#[tokio::main] +async fn main() -> Result<(), UnitctlError> { let cli = UnitCtl::parse(); match cli.command { - Commands::Instances { output_format } => instances::cmd(output_format), + Commands::Instances { output_format } => instances::cmd(output_format).await, - Commands::Edit { output_format } => edit::cmd(&cli, output_format), + Commands::Edit { output_format } => edit::cmd(&cli, output_format).await, - Commands::Import { ref directory } => import::cmd(&cli, directory), + Commands::Import { ref directory } => import::cmd(&cli, directory).await, Commands::Execute { ref output_format, ref input_file, ref method, ref path, - } => execute_cmd::cmd(&cli, output_format, input_file, method, path), + } => execute_cmd::cmd(&cli, output_format, input_file, method, path).await, - Commands::Status { output_format } => status::cmd(&cli, output_format), + Commands::Status { output_format } => status::cmd(&cli, output_format).await, - Commands::Listeners { output_format } => listeners::cmd(&cli, output_format), + Commands::Listeners { output_format } => listeners::cmd(&cli, output_format).await, } .map_err(|error| { eprint_error(&error); diff --git a/tools/unitctl/unitctl/src/requests.rs b/tools/unitctl/unitctl/src/requests.rs index bd47c645..2743c984 100644 --- a/tools/unitctl/unitctl/src/requests.rs +++ b/tools/unitctl/unitctl/src/requests.rs @@ -12,7 +12,7 @@ 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( +pub async fn send_and_validate_config_deserialize_response( client: &UnitClient, method: &str, path: &str, @@ -35,20 +35,21 @@ pub fn send_and_validate_config_deserialize_response( let reader = KnownSize::String(json.to_string()); streaming_upload_deserialize_response(client, method, path, mime_type, reader) + .await .map_err(|e| UnitctlError::UnitClientError { source: e }) } /// Send an empty body to the unit server -pub fn send_empty_body_deserialize_response( +pub async fn send_empty_body_deserialize_response( client: &UnitClient, method: &str, path: &str, ) -> Result { - send_body_deserialize_response(client, method, path, None) + send_body_deserialize_response(client, method, path, None).await } /// Send the contents of a PEM file to the unit server -pub fn send_and_validate_pem_data_deserialize_response( +pub async fn send_and_validate_pem_data_deserialize_response( client: &UnitClient, method: &str, path: &str, @@ -65,6 +66,7 @@ pub fn send_and_validate_pem_data_deserialize_response( let known_size = KnownSize::Vec((*bytes).to_owned()); streaming_upload_deserialize_response(client, method, path, Some(input_file.mime_type()), known_size) + .await .map_err(|e| UnitctlError::UnitClientError { source: e }) } @@ -131,7 +133,7 @@ fn validate_pem_items(pem_items: Vec>) -> Result<(), Ok(()) } -pub fn send_body_deserialize_response serde::Deserialize<'de>>( +pub async fn send_body_deserialize_response serde::Deserialize<'de>>( client: &UnitClient, method: &str, path: &str, @@ -143,10 +145,11 @@ pub fn send_body_deserialize_response serde::Deserialize<'de> } None => streaming_upload_deserialize_response(client, method, path, None, KnownSize::Empty), } + .await .map_err(|e| UnitctlError::UnitClientError { source: e }) } -fn streaming_upload_deserialize_response serde::Deserialize<'de>>( +async fn streaming_upload_deserialize_response serde::Deserialize<'de>>( client: &UnitClient, method: &str, path: &str, @@ -171,5 +174,5 @@ fn streaming_upload_deserialize_response serde::Deserialize<' .insert("Content-Type", content_type.parse().unwrap()); } - client.send_request_and_deserialize_response(request) + client.send_request_and_deserialize_response(request).await } diff --git a/tools/unitctl/unitctl/src/wait.rs b/tools/unitctl/unitctl/src/wait.rs index 998dc59c..313403a8 100644 --- a/tools/unitctl/unitctl/src/wait.rs +++ b/tools/unitctl/unitctl/src/wait.rs @@ -8,10 +8,10 @@ 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 { +pub async fn wait_for_socket(cli: &UnitCtl) -> Result { // 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(); + return cli.control_socket_address.instance_value_if_none().await.and_validate(); } let wait_time = @@ -33,7 +33,7 @@ pub fn wait_for_socket(cli: &UnitCtl) -> Result { attempt += 1; - let result = cli.control_socket_address.instance_value_if_none().and_validate(); + let result = cli.control_socket_address.instance_value_if_none().await.and_validate(); if let Err(error) = result { if error.retryable() { @@ -46,7 +46,7 @@ pub fn wait_for_socket(cli: &UnitCtl) -> Result { control_socket = result.unwrap(); let client = UnitClient::new(control_socket.clone()); - match client.status() { + match client.status().await { Ok(_) => { return Ok(control_socket.to_owned()); } @@ -65,15 +65,15 @@ pub fn wait_for_socket(cli: &UnitCtl) -> Result { } trait OptionControlSocket { - fn instance_value_if_none(&self) -> Result; + async fn instance_value_if_none(&self) -> Result; } impl OptionControlSocket for Option { - fn instance_value_if_none(&self) -> Result { + async fn instance_value_if_none(&self) -> Result { if let Some(control_socket) = self { Ok(control_socket.to_owned()) } else { - find_socket_address_from_instance() + find_socket_address_from_instance().await } } } @@ -109,8 +109,8 @@ impl ResultControlSocket for Result Result { - let instances = UnitdInstance::running_unitd_instances(); +async fn find_socket_address_from_instance() -> Result { + let instances = UnitdInstance::running_unitd_instances().await; if instances.is_empty() { return Err(UnitctlError::NoUnitInstancesError); } else if instances.len() > 1 { @@ -127,8 +127,8 @@ fn find_socket_address_from_instance() -> Result { } } -#[test] -fn wait_for_unavailable_unix_socket() { +#[tokio::test] +async 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()), @@ -138,15 +138,17 @@ fn wait_for_unavailable_unix_socket() { output_format: crate::output_format::OutputFormat::JsonPretty, }, }; - let error = wait_for_socket(&cli).expect_err("Expected error, but no error received"); + let error = wait_for_socket(&cli) + .await + .expect_err("Expected error, but no error received"); match error { UnitctlError::WaitTimeoutError => {} _ => panic!("Expected WaitTimeoutError: {}", error), } } -#[test] -fn wait_for_unavailable_tcp_socket() { +#[tokio::test] +async 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()), @@ -157,7 +159,9 @@ fn wait_for_unavailable_tcp_socket() { }, }; - let error = wait_for_socket(&cli).expect_err("Expected error, but no error received"); + let error = wait_for_socket(&cli) + .await + .expect_err("Expected error, but no error received"); match error { UnitctlError::WaitTimeoutError => {} _ => panic!("Expected WaitTimeoutError"), -- cgit From 818d4ad76592c87a5c0c7bbd728636023c07daa0 Mon Sep 17 00:00:00 2001 From: Ava Hahn Date: Wed, 1 May 2024 13:59:33 -0700 Subject: tools/unitctl: API Plumbing for docker deployments * refactored "instance" command out of enum * plumbed through function stub from client library * error handling Signed-off-by: Ava Hahn --- tools/unitctl/unit-client-rs/src/unitd_docker.rs | 12 ++++++ tools/unitctl/unitctl/src/cmd/instances.rs | 41 +++++++++++++----- tools/unitctl/unitctl/src/main.rs | 10 ++--- tools/unitctl/unitctl/src/unitctl.rs | 55 ++++++++++++++++++------ 4 files changed, 91 insertions(+), 27 deletions(-) (limited to 'tools/unitctl') diff --git a/tools/unitctl/unit-client-rs/src/unitd_docker.rs b/tools/unitctl/unit-client-rs/src/unitd_docker.rs index d5028afc..0f30ae8a 100644 --- a/tools/unitctl/unit-client-rs/src/unitd_docker.rs +++ b/tools/unitctl/unit-client-rs/src/unitd_docker.rs @@ -3,6 +3,7 @@ use std::fs::read_to_string; use std::path::PathBuf; use crate::unitd_process::UnitdProcess; +use crate::unit_client::UnitClientError; use bollard::secret::ContainerInspectResponse; use regex::Regex; @@ -212,6 +213,17 @@ impl UnitdContainer { } } +/* deploys a new docker image of tag $image_tag. + * mounts $socket to /var/run in the new container. + * mounts $application to /www in the new container. */ +pub fn deploy_new_container( + _socket: &String, + _application: &String, + _image: &String +) -> Result<(), UnitClientError> { + todo!() +} + /* Returns either 64 char docker container ID or None */ pub fn pid_is_dockerized(pid: u64) -> bool { let cg_filepath = format!("/proc/{}/cgroup", pid); diff --git a/tools/unitctl/unitctl/src/cmd/instances.rs b/tools/unitctl/unitctl/src/cmd/instances.rs index 09e3eb0f..84725957 100644 --- a/tools/unitctl/unitctl/src/cmd/instances.rs +++ b/tools/unitctl/unitctl/src/cmd/instances.rs @@ -1,16 +1,37 @@ use crate::{OutputFormat, UnitctlError}; +use crate::unitctl::{InstanceArgs, InstanceCommands}; use unit_client_rs::unitd_instance::UnitdInstance; +use unit_client_rs::unitd_docker::deploy_new_container; +use std::path::PathBuf; -pub(crate) async fn cmd(output_format: OutputFormat) -> Result<(), UnitctlError> { - let instances = UnitdInstance::running_unitd_instances().await; - if instances.is_empty() { - Err(UnitctlError::NoUnitInstancesError) - } else if output_format.eq(&OutputFormat::Text) { - instances.iter().for_each(|instance| { - println!("{}", instance); - }); - Ok(()) +pub(crate) async fn cmd(args: InstanceArgs) -> Result<(), UnitctlError> { + if let Some(cmd) = args.command { + match cmd { + InstanceCommands::New{ + ref socket, + ref application, + ref image + } => { + if !PathBuf::from(socket).is_dir() || !PathBuf::from(application).is_dir() { + eprintln!("application and socket paths must be directories"); + Err(UnitctlError::NoFilesImported) + } else { + deploy_new_container(socket, application, image) + .or_else(|e| Err(UnitctlError::UnitClientError{source: e})) + } + } + } } else { - output_format.write_to_stdout(&instances) + let instances = UnitdInstance::running_unitd_instances().await; + if instances.is_empty() { + Err(UnitctlError::NoUnitInstancesError) + } else if args.output_format.eq(&OutputFormat::Text) { + instances.iter().for_each(|instance| { + println!("{}", instance); + }); + Ok(()) + } else { + args.output_format.write_to_stdout(&instances) + } } } diff --git a/tools/unitctl/unitctl/src/main.rs b/tools/unitctl/unitctl/src/main.rs index a52c4ed3..9c42bdf0 100644 --- a/tools/unitctl/unitctl/src/main.rs +++ b/tools/unitctl/unitctl/src/main.rs @@ -28,11 +28,11 @@ async fn main() -> Result<(), UnitctlError> { let cli = UnitCtl::parse(); match cli.command { - Commands::Instances { output_format } => instances::cmd(output_format).await, + Commands::Instances(args) => instances::cmd(args).await, - Commands::Edit { output_format } => edit::cmd(&cli, output_format).await, + Commands::Edit{output_format} => edit::cmd(&cli, output_format).await, - Commands::Import { ref directory } => import::cmd(&cli, directory).await, + Commands::Import{ref directory} => import::cmd(&cli, directory).await, Commands::Execute { ref output_format, @@ -41,9 +41,9 @@ async fn main() -> Result<(), UnitctlError> { ref path, } => execute_cmd::cmd(&cli, output_format, input_file, method, path).await, - Commands::Status { output_format } => status::cmd(&cli, output_format).await, + Commands::Status{output_format} => status::cmd(&cli, output_format).await, - Commands::Listeners { output_format } => listeners::cmd(&cli, output_format).await, + Commands::Listeners{output_format}=> listeners::cmd(&cli, output_format).await, } .map_err(|error| { eprint_error(&error); diff --git a/tools/unitctl/unitctl/src/unitctl.rs b/tools/unitctl/unitctl/src/unitctl.rs index 49e87e8e..eb7da1b0 100644 --- a/tools/unitctl/unitctl/src/unitctl.rs +++ b/tools/unitctl/unitctl/src/unitctl.rs @@ -2,7 +2,7 @@ extern crate clap; use crate::output_format::OutputFormat; use clap::error::ErrorKind::ValueValidation; -use clap::{Error as ClapError, Parser, Subcommand}; +use clap::{Error as ClapError, Parser, Subcommand, Args}; use std::path::PathBuf; use unit_client_rs::control_socket_address::ControlSocket; @@ -42,17 +42,7 @@ pub(crate) struct UnitCtl { #[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, - }, + Instances(InstanceArgs), #[command(about = "Open current UNIT configuration in editor")] Edit { #[arg( @@ -126,6 +116,47 @@ pub(crate) enum Commands { }, } +#[derive(Debug, Args)] +pub struct InstanceArgs { + #[arg( + required = false, + global = true, + short = 't', + long = "output-format", + default_value = "text", + help = "Output format: text, yaml, json, json-pretty (default)" + )] + pub output_format: OutputFormat, + + #[command(subcommand)] + pub command: Option, +} + +#[derive(Debug, Subcommand)] +#[command(args_conflicts_with_subcommands = true)] +pub enum InstanceCommands { + #[command(about = "deploy a new docker instance of unitd")] + New { + #[arg( + required = true, + help = "Path to mount control socket to host", + )] + socket: String, + + #[arg( + required = true, + help = "Path to mount application into container", + )] + application: String, + + #[arg( + help = "Unitd Image to deploy", + default_value = env!("CARGO_PKG_VERSION"), + )] + image: String, + } +} + fn parse_control_socket_address(s: &str) -> Result { ControlSocket::try_from(s).map_err(|e| ClapError::raw(ValueValidation, e.to_string())) } -- cgit From f6989dd67965c7489f8c68ecd0e25f0358b5993f Mon Sep 17 00:00:00 2001 From: Ava Hahn Date: Wed, 1 May 2024 17:08:56 -0700 Subject: tools/unitctl: Add Docker deployment functionality * add UnitdDockerError type * write complete procedure to deploy unit via docker * additional tweaks verifying it fails peacefully * print important information in client Signed-off-by: Ava Hahn --- tools/unitctl/unit-client-rs/src/unit_client.rs | 3 + tools/unitctl/unit-client-rs/src/unitd_docker.rs | 102 +++++++++++++++++++++-- tools/unitctl/unitctl/src/cmd/instances.rs | 13 ++- 3 files changed, 108 insertions(+), 10 deletions(-) (limited to 'tools/unitctl') diff --git a/tools/unitctl/unit-client-rs/src/unit_client.rs b/tools/unitctl/unit-client-rs/src/unit_client.rs index 7456b106..f76004cd 100644 --- a/tools/unitctl/unit-client-rs/src/unit_client.rs +++ b/tools/unitctl/unit-client-rs/src/unit_client.rs @@ -66,6 +66,9 @@ custom_error! {pub UnitClientError executable_path: String, pid: u64 } = "{message} for [pid={pid}, executable_path={executable_path}]: {source}", + UnitdDockerError { + message: String + } = "Failed to communicate with docker daemon: {message}", } impl UnitClientError { diff --git a/tools/unitctl/unit-client-rs/src/unitd_docker.rs b/tools/unitctl/unit-client-rs/src/unitd_docker.rs index 0f30ae8a..47f017a9 100644 --- a/tools/unitctl/unit-client-rs/src/unitd_docker.rs +++ b/tools/unitctl/unit-client-rs/src/unitd_docker.rs @@ -4,13 +4,16 @@ use std::path::PathBuf; use crate::unitd_process::UnitdProcess; use crate::unit_client::UnitClientError; - +use bollard::{models::ContainerSummary, Docker}; +use bollard::container::{Config, StartContainerOptions, ListContainersOptions}; +use bollard::image::CreateImageOptions; use bollard::secret::ContainerInspectResponse; +use bollard::models::{HostConfig, MountTypeEnum, Mount, ContainerCreateResponse}; use regex::Regex; use serde::ser::SerializeMap; use serde::{Serialize, Serializer}; +use crate::futures::StreamExt; -use bollard::{models::ContainerSummary, Docker}; #[derive(Clone, Debug)] pub struct UnitdContainer { @@ -215,13 +218,94 @@ impl UnitdContainer { /* deploys a new docker image of tag $image_tag. * mounts $socket to /var/run in the new container. - * mounts $application to /www in the new container. */ -pub fn deploy_new_container( - _socket: &String, - _application: &String, - _image: &String -) -> Result<(), UnitClientError> { - todo!() + * mounts $application read only to /www. + * new container is on host network. + * + * ON SUCCESS returns vector of warnings from Docker API + * ON FAILURE returns wrapped error from Docker API + */ +pub async fn deploy_new_container( + socket: &String, + application: &String, + image: &String +) -> Result, UnitClientError> { + match Docker::connect_with_local_defaults() { + Ok(docker) => { + let mut mounts = vec![]; + mounts.push(Mount{ + typ: Some(MountTypeEnum::BIND), + source: Some(socket.clone()), + target: Some("/var/run".to_string()), + ..Default::default() + }); + mounts.push(Mount{ + typ: Some(MountTypeEnum::BIND), + source: Some(application.clone()), + target: Some("/www".to_string()), + read_only: Some(true), + ..Default::default() + }); + + let _ = docker.create_image( + Some(CreateImageOptions { + from_image: image.as_str(), + ..Default::default() + }), None, None) + .next() + .await + .unwrap() + .or_else(|err| Err(UnitClientError::UnitdDockerError{message: err.to_string()})); + + let resp: ContainerCreateResponse; + match docker.create_container::( + None, Config { + image: Some(image.clone()), + host_config: Some(HostConfig { + network_mode: Some("host".to_string()), + mounts: Some(mounts), + ..Default::default() + }), ..Default::default()}) + .await { + Err(err) => return Err(UnitClientError::UnitdDockerError{message: err.to_string()}), + Ok(response) => resp = response, + } + + let mut list_container_filters = HashMap::new(); + list_container_filters.insert("id".to_string(), vec![resp.id]); + match docker.list_containers::( + Some(ListContainersOptions{ + all: true, + limit: None, + size: false, + filters: list_container_filters, + })) + .await { + Err(e) => Err(UnitClientError::UnitdDockerError{message: e.to_string()}), + Ok(info) => { + if info.len() < 1 { + return Err(UnitClientError::UnitdDockerError{message: "couldnt find new container".to_string()}); + } + if info[0].names.is_none() || info[0].names.clone().unwrap().len() < 1 { + return Err(UnitClientError::UnitdDockerError{message: "new container has no name".to_string()}); + } + + match docker.start_container( + info[0] + .names + .clone() + .unwrap()[0] + .strip_prefix("/") + .unwrap(), + None::> + ).await { + Err(err) => Err(UnitClientError::UnitdDockerError{message: err.to_string()}), + Ok(_) => Ok(resp.warnings) + } + } + } + }, + Err(e) => Err(UnitClientError::UnitdDockerError{message: e.to_string()}) + } } /* Returns either 64 char docker container ID or None */ diff --git a/tools/unitctl/unitctl/src/cmd/instances.rs b/tools/unitctl/unitctl/src/cmd/instances.rs index 84725957..cd59b436 100644 --- a/tools/unitctl/unitctl/src/cmd/instances.rs +++ b/tools/unitctl/unitctl/src/cmd/instances.rs @@ -12,12 +12,23 @@ pub(crate) async fn cmd(args: InstanceArgs) -> Result<(), UnitctlError> { ref application, ref image } => { + println!("Pulling and starting a container from {}", image); + println!("Will mount {} to /var/run for socket access", socket); + println!("Will READ ONLY mount {} to /www for application access", application); + println!("Note: Container will be on host network"); if !PathBuf::from(socket).is_dir() || !PathBuf::from(application).is_dir() { eprintln!("application and socket paths must be directories"); Err(UnitctlError::NoFilesImported) } else { deploy_new_container(socket, application, image) - .or_else(|e| Err(UnitctlError::UnitClientError{source: e})) + .await + .map_or_else(|e| Err(UnitctlError::UnitClientError{source: e}), + |warn| { + for i in warn { + println!("warning from docker: {}", i); + } + Ok(()) + }) } } } -- cgit From 1d237990c589a90d7ec85f211c01b709b1e5dc65 Mon Sep 17 00:00:00 2001 From: Ava Hahn Date: Wed, 1 May 2024 18:21:55 -0700 Subject: tools/unitctl: Add new functionality to README.md and fmt code Signed-off-by: Ava Hahn --- tools/unitctl/README.md | 40 ++++-- tools/unitctl/unit-client-rs/src/unitd_docker.rs | 145 ++++++++++++--------- tools/unitctl/unit-client-rs/src/unitd_instance.rs | 13 +- tools/unitctl/unitctl/src/cmd/instances.rs | 28 ++-- tools/unitctl/unitctl/src/main.rs | 8 +- tools/unitctl/unitctl/src/unitctl.rs | 14 +- 6 files changed, 140 insertions(+), 108 deletions(-) (limited to 'tools/unitctl') diff --git a/tools/unitctl/README.md b/tools/unitctl/README.md index 7292cd86..e514cd56 100644 --- a/tools/unitctl/README.md +++ b/tools/unitctl/README.md @@ -36,9 +36,9 @@ 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 +- 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. ``` @@ -52,6 +52,30 @@ unitd instance [pid: 79489, version: 1.32.0]: Configure options: --prefix=/opt/unit --user=elijah --group=elijah --openssl ``` +### Start a new UNIT process via docker +``` +$ unitctl instances new /tmp/2 $(pwd) 'unit:wasm' +Pulling and starting a container from unit:wasm +Will mount /tmp/2 to /var/run for socket access +Will READ ONLY mount /home/ava/repositories/nginx/unit/tools/unitctl to /www for application access +Note: Container will be on host network + +``` + +To the subcommand `unitctl instances new` the user must provide three things: +1. **A directory such as `/tmp/2`.** + The UNIT container will mount this to `/var/run` internally. + Thus, the control socket and pid file will be accessible from the host. +2. **A path to an application.** + In the example, `$(pwd)` is provided. The UNIT container will mount + this READ ONLY to `/www/`. This will allow the user to configure + their UNIT container to expose an application stored on the host. +3. **An image tag.** + In the example, `unit:wasm` is used. This will be the image that unitctl + will deploy. Custom repos and images can be deployed in this manner. + +After deployment the user will have one UNIT container running on the host network. + ### Lists active listeners from running UNIT processes ``` unitctl listeners @@ -109,13 +133,6 @@ $ unitctl edit } ``` -### 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 @@ -124,6 +141,7 @@ 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` @@ -131,4 +149,4 @@ 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/unit-client-rs/src/unitd_docker.rs b/tools/unitctl/unit-client-rs/src/unitd_docker.rs index 47f017a9..2b51ddd3 100644 --- a/tools/unitctl/unit-client-rs/src/unitd_docker.rs +++ b/tools/unitctl/unit-client-rs/src/unitd_docker.rs @@ -2,18 +2,17 @@ use std::collections::HashMap; use std::fs::read_to_string; use std::path::PathBuf; -use crate::unitd_process::UnitdProcess; +use crate::futures::StreamExt; use crate::unit_client::UnitClientError; -use bollard::{models::ContainerSummary, Docker}; -use bollard::container::{Config, StartContainerOptions, ListContainersOptions}; +use crate::unitd_process::UnitdProcess; +use bollard::container::{Config, ListContainersOptions, StartContainerOptions}; use bollard::image::CreateImageOptions; +use bollard::models::{ContainerCreateResponse, HostConfig, Mount, MountTypeEnum}; use bollard::secret::ContainerInspectResponse; -use bollard::models::{HostConfig, MountTypeEnum, Mount, ContainerCreateResponse}; +use bollard::{models::ContainerSummary, Docker}; use regex::Regex; use serde::ser::SerializeMap; use serde::{Serialize, Serializer}; -use crate::futures::StreamExt; - #[derive(Clone, Debug)] pub struct UnitdContainer { @@ -75,7 +74,8 @@ impl From<&UnitdContainer> for UnitdProcess { cmd.strip_prefix("/usr/local/bin/docker-entrypoint.sh") .or_else(|| Some("")) .unwrap() - .to_string()) + .to_string() + ) )) }); let mut cmds = vec![]; @@ -197,13 +197,15 @@ impl UnitdContainer { pub fn rewrite_socket(&self, command: String) -> String { command .split(" ") - .map(|tok| if tok.starts_with("unix:") { - format!("unix:{}", self.host_path( - tok.strip_prefix("unix:") - .unwrap() - .to_string())) - } else { - tok.to_string() + .map(|tok| { + if tok.starts_with("unix:") { + format!( + "unix:{}", + self.host_path(tok.strip_prefix("unix:").unwrap().to_string()) + ) + } else { + tok.to_string() + } }) .collect::>() .join(" ") @@ -227,18 +229,18 @@ impl UnitdContainer { pub async fn deploy_new_container( socket: &String, application: &String, - image: &String + image: &String, ) -> Result, UnitClientError> { match Docker::connect_with_local_defaults() { Ok(docker) => { let mut mounts = vec![]; - mounts.push(Mount{ + mounts.push(Mount { typ: Some(MountTypeEnum::BIND), source: Some(socket.clone()), target: Some("/var/run".to_string()), ..Default::default() }); - mounts.push(Mount{ + mounts.push(Mount { typ: Some(MountTypeEnum::BIND), source: Some(application.clone()), target: Some("/www".to_string()), @@ -246,65 +248,88 @@ pub async fn deploy_new_container( ..Default::default() }); - let _ = docker.create_image( - Some(CreateImageOptions { - from_image: image.as_str(), - ..Default::default() - }), None, None) + let _ = docker + .create_image( + Some(CreateImageOptions { + from_image: image.as_str(), + ..Default::default() + }), + None, + None, + ) .next() .await .unwrap() - .or_else(|err| Err(UnitClientError::UnitdDockerError{message: err.to_string()})); + .or_else(|err| { + Err(UnitClientError::UnitdDockerError { + message: err.to_string(), + }) + }); let resp: ContainerCreateResponse; - match docker.create_container::( - None, Config { - image: Some(image.clone()), - host_config: Some(HostConfig { - network_mode: Some("host".to_string()), - mounts: Some(mounts), + match docker + .create_container::( + None, + Config { + image: Some(image.clone()), + host_config: Some(HostConfig { + network_mode: Some("host".to_string()), + mounts: Some(mounts), + ..Default::default() + }), ..Default::default() - }), ..Default::default()}) - .await { - Err(err) => return Err(UnitClientError::UnitdDockerError{message: err.to_string()}), - Ok(response) => resp = response, + }, + ) + .await + { + Err(err) => { + return Err(UnitClientError::UnitdDockerError { + message: err.to_string(), + }) } + Ok(response) => resp = response, + } let mut list_container_filters = HashMap::new(); list_container_filters.insert("id".to_string(), vec![resp.id]); - match docker.list_containers::( - Some(ListContainersOptions{ + match docker + .list_containers::(Some(ListContainersOptions { all: true, limit: None, size: false, filters: list_container_filters, })) - .await { - Err(e) => Err(UnitClientError::UnitdDockerError{message: e.to_string()}), - Ok(info) => { - if info.len() < 1 { - return Err(UnitClientError::UnitdDockerError{message: "couldnt find new container".to_string()}); - } - if info[0].names.is_none() || info[0].names.clone().unwrap().len() < 1 { - return Err(UnitClientError::UnitdDockerError{message: "new container has no name".to_string()}); - } + .await + { + Err(e) => Err(UnitClientError::UnitdDockerError { message: e.to_string() }), + Ok(info) => { + if info.len() < 1 { + return Err(UnitClientError::UnitdDockerError { + message: "couldnt find new container".to_string(), + }); + } + if info[0].names.is_none() || info[0].names.clone().unwrap().len() < 1 { + return Err(UnitClientError::UnitdDockerError { + message: "new container has no name".to_string(), + }); + } - match docker.start_container( - info[0] - .names - .clone() - .unwrap()[0] - .strip_prefix("/") - .unwrap(), - None::> - ).await { - Err(err) => Err(UnitClientError::UnitdDockerError{message: err.to_string()}), - Ok(_) => Ok(resp.warnings) - } + match docker + .start_container( + info[0].names.clone().unwrap()[0].strip_prefix("/").unwrap(), + None::>, + ) + .await + { + Err(err) => Err(UnitClientError::UnitdDockerError { + message: err.to_string(), + }), + Ok(_) => Ok(resp.warnings), } } - }, - Err(e) => Err(UnitClientError::UnitdDockerError{message: e.to_string()}) + } + } + Err(e) => Err(UnitClientError::UnitdDockerError { message: e.to_string() }), } } @@ -372,7 +397,7 @@ mod tests { assert_eq!( ctr.rewrite_socket("unitd --no-daemon --control unix:/var/run/control.unit.sock".to_string()), - "unitd --no-daemon --control unix:/tmp/control.unit.sock".to_string()); - + "unitd --no-daemon --control unix:/tmp/control.unit.sock".to_string() + ); } } diff --git a/tools/unitctl/unit-client-rs/src/unitd_instance.rs b/tools/unitctl/unit-client-rs/src/unitd_instance.rs index 86f8e73d..a7fb1bdc 100644 --- a/tools/unitctl/unit-client-rs/src/unitd_instance.rs +++ b/tools/unitctl/unit-client-rs/src/unitd_instance.rs @@ -121,13 +121,10 @@ impl UnitdInstance { Ok(_) if process.container.is_some() => { let mut err = vec![]; // double check that it is running - let running = process.container - .as_ref() - .unwrap() - .container_is_running(); + let running = process.container.as_ref().unwrap().container_is_running(); if running.is_none() || !running.unwrap() { - err.push(UnitClientError::UnitdProcessParseError{ + err.push(UnitClientError::UnitdProcessParseError { message: "process container is not running".to_string(), pid: process.process_id, }); @@ -138,10 +135,8 @@ impl UnitdInstance { configure_options: None, errors: err, } - }, - Ok(unitd_path) => match UnitdConfigureOptions::new( - &unitd_path.clone() - .into_path_buf()) { + } + 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), diff --git a/tools/unitctl/unitctl/src/cmd/instances.rs b/tools/unitctl/unitctl/src/cmd/instances.rs index cd59b436..b9af75f6 100644 --- a/tools/unitctl/unitctl/src/cmd/instances.rs +++ b/tools/unitctl/unitctl/src/cmd/instances.rs @@ -1,16 +1,16 @@ -use crate::{OutputFormat, UnitctlError}; use crate::unitctl::{InstanceArgs, InstanceCommands}; -use unit_client_rs::unitd_instance::UnitdInstance; -use unit_client_rs::unitd_docker::deploy_new_container; +use crate::{OutputFormat, UnitctlError}; use std::path::PathBuf; +use unit_client_rs::unitd_docker::deploy_new_container; +use unit_client_rs::unitd_instance::UnitdInstance; pub(crate) async fn cmd(args: InstanceArgs) -> Result<(), UnitctlError> { if let Some(cmd) = args.command { match cmd { - InstanceCommands::New{ + InstanceCommands::New { ref socket, ref application, - ref image + ref image, } => { println!("Pulling and starting a container from {}", image); println!("Will mount {} to /var/run for socket access", socket); @@ -20,15 +20,15 @@ pub(crate) async fn cmd(args: InstanceArgs) -> Result<(), UnitctlError> { eprintln!("application and socket paths must be directories"); Err(UnitctlError::NoFilesImported) } else { - deploy_new_container(socket, application, image) - .await - .map_or_else(|e| Err(UnitctlError::UnitClientError{source: e}), - |warn| { - for i in warn { - println!("warning from docker: {}", i); - } - Ok(()) - }) + deploy_new_container(socket, application, image).await.map_or_else( + |e| Err(UnitctlError::UnitClientError { source: e }), + |warn| { + for i in warn { + println!("warning from docker: {}", i); + } + Ok(()) + }, + ) } } } diff --git a/tools/unitctl/unitctl/src/main.rs b/tools/unitctl/unitctl/src/main.rs index 9c42bdf0..12322873 100644 --- a/tools/unitctl/unitctl/src/main.rs +++ b/tools/unitctl/unitctl/src/main.rs @@ -30,9 +30,9 @@ async fn main() -> Result<(), UnitctlError> { match cli.command { Commands::Instances(args) => instances::cmd(args).await, - Commands::Edit{output_format} => edit::cmd(&cli, output_format).await, + Commands::Edit { output_format } => edit::cmd(&cli, output_format).await, - Commands::Import{ref directory} => import::cmd(&cli, directory).await, + Commands::Import { ref directory } => import::cmd(&cli, directory).await, Commands::Execute { ref output_format, @@ -41,9 +41,9 @@ async fn main() -> Result<(), UnitctlError> { ref path, } => execute_cmd::cmd(&cli, output_format, input_file, method, path).await, - Commands::Status{output_format} => status::cmd(&cli, output_format).await, + Commands::Status { output_format } => status::cmd(&cli, output_format).await, - Commands::Listeners{output_format}=> listeners::cmd(&cli, output_format).await, + Commands::Listeners { output_format } => listeners::cmd(&cli, output_format).await, } .map_err(|error| { eprint_error(&error); diff --git a/tools/unitctl/unitctl/src/unitctl.rs b/tools/unitctl/unitctl/src/unitctl.rs index eb7da1b0..b1bdbbd1 100644 --- a/tools/unitctl/unitctl/src/unitctl.rs +++ b/tools/unitctl/unitctl/src/unitctl.rs @@ -2,7 +2,7 @@ extern crate clap; use crate::output_format::OutputFormat; use clap::error::ErrorKind::ValueValidation; -use clap::{Error as ClapError, Parser, Subcommand, Args}; +use clap::{Args, Error as ClapError, Parser, Subcommand}; use std::path::PathBuf; use unit_client_rs::control_socket_address::ControlSocket; @@ -137,16 +137,10 @@ pub struct InstanceArgs { pub enum InstanceCommands { #[command(about = "deploy a new docker instance of unitd")] New { - #[arg( - required = true, - help = "Path to mount control socket to host", - )] + #[arg(required = true, help = "Path to mount control socket to host")] socket: String, - #[arg( - required = true, - help = "Path to mount application into container", - )] + #[arg(required = true, help = "Path to mount application into container")] application: String, #[arg( @@ -154,7 +148,7 @@ pub enum InstanceCommands { default_value = env!("CARGO_PKG_VERSION"), )] image: String, - } + }, } fn parse_control_socket_address(s: &str) -> Result { -- cgit From 4e4d1dd20508a4dd4bdf7b3caaec92ed09ed59bf Mon Sep 17 00:00:00 2001 From: Ava Hahn Date: Thu, 2 May 2024 17:59:08 -0700 Subject: tools/unitctl: temporarily ignore issues with autogenerated readme Suggested-by: Andrew Clayton Signed-off-by: Ava Hahn --- tools/unitctl/unit-openapi/.gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 tools/unitctl/unit-openapi/.gitattributes (limited to 'tools/unitctl') diff --git a/tools/unitctl/unit-openapi/.gitattributes b/tools/unitctl/unit-openapi/.gitattributes new file mode 100644 index 00000000..b4361577 --- /dev/null +++ b/tools/unitctl/unit-openapi/.gitattributes @@ -0,0 +1 @@ +README.md whitespace=-blank-at-eof -- cgit From e61d9e7a1f80d62fc17f2dabb6e3318eb70bfdbc Mon Sep 17 00:00:00 2001 From: Ava Hahn Date: Fri, 3 May 2024 08:45:53 -0700 Subject: tools/unitctl: Readme fixes * fix Unit spelling in Readme * remove trailiing whitespace Signed-off-by: Ava Hahn --- tools/unitctl/README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) (limited to 'tools/unitctl') diff --git a/tools/unitctl/README.md b/tools/unitctl/README.md index e514cd56..8a0d92a7 100644 --- a/tools/unitctl/README.md +++ b/tools/unitctl/README.md @@ -1,7 +1,7 @@ -# NGINX UNIT Rust SDK and CLI +# NGINX Unit Rust SDK and CLI This project provides a Rust SDK interface to the -[NGINX UNIT](https://unit.nginx.org/) +[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. @@ -38,9 +38,9 @@ desired. - Consumes alternative configuration formats Like YAML and converts them - Syntactic highlighting of JSON output -- Interpretation of UNIT errors with (arguably more) useful error messages +- Interpretation of Unit errors with (arguably more) useful error messages -### Lists all running UNIT processes and provides details about each process. +### Lists all running Unit processes and provides details about each process. ``` $ unitctl instances No socket path provided - attempting to detect from running instance @@ -52,7 +52,7 @@ unitd instance [pid: 79489, version: 1.32.0]: Configure options: --prefix=/opt/unit --user=elijah --group=elijah --openssl ``` -### Start a new UNIT process via docker +### Start a new Unit process via docker ``` $ unitctl instances new /tmp/2 $(pwd) 'unit:wasm' Pulling and starting a container from unit:wasm @@ -64,19 +64,19 @@ Note: Container will be on host network To the subcommand `unitctl instances new` the user must provide three things: 1. **A directory such as `/tmp/2`.** - The UNIT container will mount this to `/var/run` internally. + The Unit container will mount this to `/var/run` internally. Thus, the control socket and pid file will be accessible from the host. 2. **A path to an application.** - In the example, `$(pwd)` is provided. The UNIT container will mount + In the example, `$(pwd)` is provided. The Unit container will mount this READ ONLY to `/www/`. This will allow the user to configure - their UNIT container to expose an application stored on the host. + their Unit container to expose an application stored on the host. 3. **An image tag.** In the example, `unit:wasm` is used. This will be the image that unitctl will deploy. Custom repos and images can be deployed in this manner. -After deployment the user will have one UNIT container running on the host network. +After deployment the user will have one Unit container running on the host network. -### Lists active listeners from running UNIT processes +### Lists active listeners from running Unit processes ``` unitctl listeners No socket path provided - attempting to detect from running instance @@ -87,7 +87,7 @@ No socket path provided - attempting to detect from running instance } ``` -### Get the current status of NGINX UNIT processes +### Get the current status of NGINX Unit processes ``` $ unitctl status -t yaml No socket path provided - attempting to detect from running instance @@ -101,7 +101,7 @@ requests: applications: {} ``` -### Send arbitrary configuration payloads to UNIT +### Send arbitrary configuration payloads to Unit ``` $ echo '{ "listeners": { -- cgit From 787980db2e4cdc39c6195584e54661841e542a47 Mon Sep 17 00:00:00 2001 From: Ava Hahn Date: Fri, 3 May 2024 16:14:36 -0700 Subject: tools/unitctl: Improve quality of life on osx * unit-client-rs Mac build fix * elaborate in Readme on build requirements with examples for Mac users. Signed-off-by: Ava Hahn --- tools/unitctl/README.md | 21 ++++++++++++++++++--- tools/unitctl/unit-client-rs/src/unitd_process.rs | 11 +++++------ 2 files changed, 23 insertions(+), 9 deletions(-) (limited to 'tools/unitctl') diff --git a/tools/unitctl/README.md b/tools/unitctl/README.md index 8a0d92a7..2e5a2da1 100644 --- a/tools/unitctl/README.md +++ b/tools/unitctl/README.md @@ -6,10 +6,23 @@ This project provides a Rust SDK interface to the 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 +In order to build and use `unitctl` one needs a working installation of +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/). +[Official Site](https://rustup.rs/). Additionally, Macintosh users will +need to install GNU core utilities using brew (see the following command) + +``` +$ brew install make gnu-sed grep gawk maven +``` + +After installing a modern distribution of Make, Macintosh users can invoke +the makefile commands using `gmake`. For example: `gmake clean` or `gmake all`. + +Finally, in order to run the OpenAPI code generation tooling, Users will +need a working +[Java runtime](https://www.java.com/en/) +as well as Maven. Macintosh users can install Maven from Brew. 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 @@ -20,8 +33,10 @@ 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, diff --git a/tools/unitctl/unit-client-rs/src/unitd_process.rs b/tools/unitctl/unit-client-rs/src/unitd_process.rs index 2a78bfc6..848f31bc 100644 --- a/tools/unitctl/unit-client-rs/src/unitd_process.rs +++ b/tools/unitctl/unit-client-rs/src/unitd_process.rs @@ -64,12 +64,11 @@ impl UnitdProcess { #[cfg(target_os = "linux")] if pid_is_dockerized(p.0.as_u32().into()) { false - } else { - let parent_pid = p.1.parent(); - match parent_pid { - Some(pid) => !unitd_processes.contains_key(&pid), - None => false, - } + } + let parent_pid = p.1.parent(); + match parent_pid { + Some(pid) => !unitd_processes.contains_key(&pid), + None => false, } }) .map(|p| { -- cgit From cb03d31e02741b87fc3a3aa379bf8f1849442ae2 Mon Sep 17 00:00:00 2001 From: Ava Hahn Date: Fri, 3 May 2024 17:52:47 -0700 Subject: tools/unitctl: Update host_path() to account for OSX special behaviour Signed-off-by: Ava Hahn --- tools/unitctl/unit-client-rs/src/unitd_docker.rs | 39 ++++++++++++++++++----- tools/unitctl/unit-client-rs/src/unitd_process.rs | 2 +- 2 files changed, 32 insertions(+), 9 deletions(-) (limited to 'tools/unitctl') diff --git a/tools/unitctl/unit-client-rs/src/unitd_docker.rs b/tools/unitctl/unit-client-rs/src/unitd_docker.rs index 2b51ddd3..4c86c870 100644 --- a/tools/unitctl/unit-client-rs/src/unitd_docker.rs +++ b/tools/unitctl/unit-client-rs/src/unitd_docker.rs @@ -166,7 +166,6 @@ impl UnitdContainer { pub fn host_path(&self, container_path: String) -> String { let cp = PathBuf::from(container_path); - // get only possible mount points // sort to deepest mountpoint first // assumed deepest possible mount point takes precedence @@ -180,14 +179,26 @@ impl UnitdContainer { // either return translated path or original prefixed with "container" if keys.len() > 0 { - self.mounts[&keys[0]] + let mut matches = self.mounts[&keys[0]] .clone() - .join( - cp.as_path() - .strip_prefix(keys[0].clone()) - .expect("error checking path prefix"), - ) - .to_string_lossy() + .join(cp.as_path() + .strip_prefix(keys[0].clone()) + .expect("error checking path prefix")); + /* Observed on M1 Mac that Docker on OSX + * adds a bunch of garbage to the mount path + * converting it into a useless directory + * that doesnt actually exist + */ + if cfg!(target_os = "macos") { + let mut abs = PathBuf::from("/"); + let m = matches.strip_prefix("/host_mnt/private") + .unwrap_or(matches.strip_prefix("/host_mnt") + .unwrap_or(matches.as_path())); + // make it absolute again + abs.push(m); + matches = abs; + } + matches.to_string_lossy() .to_string() } else { format!(":{}", cp.display()) @@ -361,6 +372,8 @@ mod tests { mounts.insert("/root/mid/child".into(), "/3".into()); mounts.insert("/mid/child".into(), "/4".into()); mounts.insert("/child".into(), "/5".into()); + mounts.insert("/var".into(), "/host_mnt/private/6".into()); + mounts.insert("/var/var".into(), "/host_mnt/7".into()); let ctr = UnitdContainer { container_id: None, @@ -379,6 +392,16 @@ mod tests { ":/path/to/conf".to_string(), ctr.host_path("/path/to/conf".to_string()) ); + if cfg!(target_os = "macos") { + assert_eq!( + "/6/test".to_string(), + ctr.host_path("/var/test".to_string()) + ); + assert_eq!( + "/7/test".to_string(), + ctr.host_path("/var/var/test".to_string()) + ); + } } #[test] diff --git a/tools/unitctl/unit-client-rs/src/unitd_process.rs b/tools/unitctl/unit-client-rs/src/unitd_process.rs index 848f31bc..47ffcb5d 100644 --- a/tools/unitctl/unit-client-rs/src/unitd_process.rs +++ b/tools/unitctl/unit-client-rs/src/unitd_process.rs @@ -63,7 +63,7 @@ impl UnitdProcess { .filter(|p| { #[cfg(target_os = "linux")] if pid_is_dockerized(p.0.as_u32().into()) { - false + return false; } let parent_pid = p.1.parent(); match parent_pid { -- cgit From 6ad1fa342813f2c9f00813108c3f2eec8824fd6f Mon Sep 17 00:00:00 2001 From: Ava Hahn Date: Mon, 6 May 2024 12:13:52 -0700 Subject: tools/unitctl: clean up control socket impls Signed-off-by: Ava Hahn --- .../unit-client-rs/src/control_socket_address.rs | 174 ++++++++++----------- 1 file changed, 87 insertions(+), 87 deletions(-) (limited to 'tools/unitctl') diff --git a/tools/unitctl/unit-client-rs/src/control_socket_address.rs b/tools/unitctl/unit-client-rs/src/control_socket_address.rs index b9ae5afc..402d2293 100644 --- a/tools/unitctl/unit-client-rs/src/control_socket_address.rs +++ b/tools/unitctl/unit-client-rs/src/control_socket_address.rs @@ -34,6 +34,92 @@ impl ControlSocketScheme { } } + +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 for String { + fn from(val: ControlSocket) -> Self { + val.to_string() + } +} + +impl From 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 for Uri { + fn from(val: ControlSocket) -> Self { + val.create_uri_with_path("") + } +} + +impl TryFrom for ControlSocket { + type Error = UnitClientError; + + fn try_from(socket_address: String) -> Result { + ControlSocket::parse_address(socket_address.as_str()) + } +} + +impl TryFrom<&str> for ControlSocket { + type Error = UnitClientError; + + fn try_from(socket_address: &str) -> Result { + ControlSocket::parse_address(socket_address) + } +} + +impl TryFrom for ControlSocket { + type Error = UnitClientError; + + fn try_from(socket_uri: Uri) -> Result { + 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(), + }), + } + } +} + impl ControlSocket { pub fn socket_scheme(&self) -> ControlSocketScheme { match self { @@ -69,41 +155,7 @@ impl ControlSocket { } } } -} - -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 for String { - fn from(val: ControlSocket) -> Self { - val.to_string() - } -} - -impl From 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 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() { @@ -202,7 +254,7 @@ impl ControlSocket { } /// Flexibly parse a textual representation of a socket address - fn parse_address>(socket_address: S) -> Result { + pub fn parse_address>(socket_address: S) -> Result { let full_socket_address: String = socket_address.into(); let socket_prefix = "unix:"; let socket_uri_prefix = "unix://"; @@ -260,58 +312,6 @@ impl ControlSocket { } } -impl TryFrom for ControlSocket { - type Error = UnitClientError; - - fn try_from(socket_address: String) -> Result { - ControlSocket::parse_address(socket_address.as_str()) - } -} - -impl TryFrom<&str> for ControlSocket { - type Error = UnitClientError; - - fn try_from(socket_address: &str) -> Result { - ControlSocket::parse_address(socket_address) - } -} - -impl TryFrom for ControlSocket { - type Error = UnitClientError; - - fn try_from(socket_uri: Uri) -> Result { - 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 { -- cgit From cc9eb8e756e84cd3fd59baf5b80efab0ffd5757d Mon Sep 17 00:00:00 2001 From: Ava Hahn Date: Mon, 6 May 2024 12:28:40 -0700 Subject: tools/unitctl: enable passing IP addresses to the 'instances new' command * use path seperator constant from rust std package * pass a ControlSocket into deploy_new_container instead of a string * parse and validate a ControlSocket from argument to instances new * conditionally mount control socket only if its a unix socket * use create_image in a way that actually pulls nonpresent images * possibly override container command if TCP socket passed in * handle more weird error cases * add a ton of validation cases in the CLI command handler * add a nice little progress bar :) Signed-off-by: Ava Hahn --- tools/unitctl/Cargo.lock | 28 +++- tools/unitctl/unit-client-rs/Cargo.toml | 1 + tools/unitctl/unit-client-rs/src/unit_client.rs | 4 +- tools/unitctl/unit-client-rs/src/unitd_docker.rs | 177 +++++++++++++-------- tools/unitctl/unit-client-rs/src/unitd_instance.rs | 1 + tools/unitctl/unit-client-rs/src/unitd_process.rs | 1 + tools/unitctl/unitctl/src/cmd/instances.rs | 104 +++++++++++- 7 files changed, 235 insertions(+), 81 deletions(-) (limited to 'tools/unitctl') diff --git a/tools/unitctl/Cargo.lock b/tools/unitctl/Cargo.lock index 2acbfb9a..16241296 100644 --- a/tools/unitctl/Cargo.lock +++ b/tools/unitctl/Cargo.lock @@ -390,6 +390,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.2" @@ -416,12 +425,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.12" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac" -dependencies = [ - "cfg-if", -] +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crypto-common" @@ -1226,6 +1232,17 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "pbr" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5827dfa0d69b6c92493d6c38e633bbaa5937c153d0d7c28bf12313f8c6d514" +dependencies = [ + "crossbeam-channel", + "libc", + "winapi", +] + [[package]] name = "percent-encoding" version = "2.2.0" @@ -2001,6 +2018,7 @@ dependencies = [ "hyper 0.14.27", "hyper-tls", "hyperlocal", + "pbr", "rand", "regex", "rustls", diff --git a/tools/unitctl/unit-client-rs/Cargo.toml b/tools/unitctl/unit-client-rs/Cargo.toml index 3e48ee23..b7b8b496 100644 --- a/tools/unitctl/unit-client-rs/Cargo.toml +++ b/tools/unitctl/unit-client-rs/Cargo.toml @@ -29,6 +29,7 @@ unit-openapi = { path = "../unit-openapi" } rustls = "0.23.5" bollard = "0.16.1" regex = "1.10.4" +pbr = "1.1.1" [dev-dependencies] rand = "0.8.5" diff --git a/tools/unitctl/unit-client-rs/src/unit_client.rs b/tools/unitctl/unit-client-rs/src/unit_client.rs index f76004cd..b8c73ec0 100644 --- a/tools/unitctl/unit-client-rs/src/unit_client.rs +++ b/tools/unitctl/unit-client-rs/src/unit_client.rs @@ -250,7 +250,7 @@ impl UnitClient { Err(Box::new(UnitClientError::new( hyper_error, self.control_socket.to_string(), - "".to_string(), + "/listeners".to_string(), ))) } else { Err(Box::new(UnitClientError::OpenAPIError { source: err })) @@ -268,7 +268,7 @@ impl UnitClient { Err(Box::new(UnitClientError::new( hyper_error, self.control_socket.to_string(), - "".to_string(), + "/status".to_string(), ))) } else { Err(Box::new(UnitClientError::OpenAPIError { source: err })) diff --git a/tools/unitctl/unit-client-rs/src/unitd_docker.rs b/tools/unitctl/unit-client-rs/src/unitd_docker.rs index 4c86c870..b9199e40 100644 --- a/tools/unitctl/unit-client-rs/src/unitd_docker.rs +++ b/tools/unitctl/unit-client-rs/src/unitd_docker.rs @@ -1,19 +1,29 @@ use std::collections::HashMap; use std::fs::read_to_string; -use std::path::PathBuf; +use std::path::{PathBuf, MAIN_SEPARATOR}; +use std::io::stderr; use crate::futures::StreamExt; use crate::unit_client::UnitClientError; use crate::unitd_process::UnitdProcess; +use crate::control_socket_address::ControlSocket; + use bollard::container::{Config, ListContainersOptions, StartContainerOptions}; use bollard::image::CreateImageOptions; -use bollard::models::{ContainerCreateResponse, HostConfig, Mount, MountTypeEnum}; +use bollard::models::{ + ContainerCreateResponse, HostConfig, Mount, + MountTypeEnum, ContainerSummary, +}; use bollard::secret::ContainerInspectResponse; -use bollard::{models::ContainerSummary, Docker}; +use bollard::Docker; + use regex::Regex; + use serde::ser::SerializeMap; use serde::{Serialize, Serializer}; +use pbr::ProgressBar; + #[derive(Clone, Debug)] pub struct UnitdContainer { pub container_id: Option, @@ -121,6 +131,7 @@ impl Serialize for UnitdContainer { where S: Serializer, { + // 5 = fields to serialize let mut state = serializer.serialize_map(Some(5))?; state.serialize_entry("container_id", &self.container_id)?; state.serialize_entry("container_image", &self.container_image)?; @@ -143,18 +154,23 @@ impl UnitdContainer { // cant do this functionally because of the async call let mut mapped = vec![]; for ctr in summary { - if ctr.clone().image.or(Some(String::new())).unwrap().contains("unit") { - let mut c = UnitdContainer::from(&ctr); - if let Some(names) = ctr.names { - if names.len() > 0 { - let name = names[0].strip_prefix("/").or(Some(names[0].as_str())).unwrap(); - if let Ok(cir) = docker.inspect_container(name, None).await { - c.details = Some(cir); + if ctr.clone().image + .or(Some(String::new())) + .unwrap() + .contains("unit") { + let mut c = UnitdContainer::from(&ctr); + if let Some(names) = ctr.names { + if names.len() > 0 { + let name = names[0].strip_prefix("/") + .or(Some(names[0].as_str())).unwrap(); + if let Ok(cir) = docker + .inspect_container(name, None).await { + c.details = Some(cir); + } } } + mapped.push(c); } - mapped.push(c); - } } mapped } @@ -190,7 +206,7 @@ impl UnitdContainer { * that doesnt actually exist */ if cfg!(target_os = "macos") { - let mut abs = PathBuf::from("/"); + let mut abs = PathBuf::from(String::from(MAIN_SEPARATOR)); let m = matches.strip_prefix("/host_mnt/private") .unwrap_or(matches.strip_prefix("/host_mnt") .unwrap_or(matches.as_path())); @@ -238,19 +254,27 @@ impl UnitdContainer { * ON FAILURE returns wrapped error from Docker API */ pub async fn deploy_new_container( - socket: &String, + socket: ControlSocket, application: &String, image: &String, ) -> Result, UnitClientError> { match Docker::connect_with_local_defaults() { Ok(docker) => { let mut mounts = vec![]; - mounts.push(Mount { - typ: Some(MountTypeEnum::BIND), - source: Some(socket.clone()), - target: Some("/var/run".to_string()), - ..Default::default() - }); + // if a unix socket is specified, mounts its directory + if socket.is_local_socket() { + let mount_path = PathBuf::from(socket.clone()) + .as_path() + .to_string_lossy() + .to_string(); + mounts.push(Mount { + typ: Some(MountTypeEnum::BIND), + source: Some(mount_path), + target: Some("/var/run".to_string()), + ..Default::default() + }); + } + // mount application dir mounts.push(Mount { typ: Some(MountTypeEnum::BIND), source: Some(application.clone()), @@ -259,40 +283,57 @@ pub async fn deploy_new_container( ..Default::default() }); - let _ = docker - .create_image( - Some(CreateImageOptions { - from_image: image.as_str(), - ..Default::default() - }), - None, - None, - ) - .next() - .await - .unwrap() - .or_else(|err| { - Err(UnitClientError::UnitdDockerError { - message: err.to_string(), - }) - }); + let mut pb = ProgressBar::on(stderr(), 10); + let mut totals = HashMap::new(); + let mut stream = docker.create_image( + Some(CreateImageOptions { + from_image: image.as_str(), + ..Default::default() + }), None, None + ); + while let Some(res) = stream.next().await { + if let Ok(info) = res { + if let Some(id) = info.id { + if let Some(_) = totals.get_mut(&id) { + if let Some(delta) = info.progress_detail + .and_then(|detail| detail.current) { + pb.add(delta as u64); + } + } else { + if let Some(total) = info.progress_detail + .and_then(|detail| detail.total) { + totals.insert(id, total); + pb.total += total as u64; + } + } + } + } + } + pb.finish(); + // create the new unit container let resp: ContainerCreateResponse; - match docker - .create_container::( - None, - Config { - image: Some(image.clone()), - host_config: Some(HostConfig { - network_mode: Some("host".to_string()), - mounts: Some(mounts), - ..Default::default() - }), - ..Default::default() - }, - ) - .await - { + let host_conf = HostConfig { + mounts: Some(mounts), + network_mode: Some("host".to_string()), + ..Default::default() + }; + let mut container_conf = Config { + image: Some(image.clone()), + ..Default::default() + }; + if let ControlSocket::TcpSocket(ref uri) = socket { + let port = uri.port_u16().or(Some(80)).unwrap(); + // override port + container_conf.cmd = Some(vec![ + "unitd".to_string(), + "--no-daemon".to_string(), + "--control".to_string(), + format!("{}:{}", uri.host().unwrap(), port), + ]); + } + container_conf.host_config = Some(host_conf); + match docker.create_container::(None, container_conf).await { Err(err) => { return Err(UnitClientError::UnitdDockerError { message: err.to_string(), @@ -301,6 +342,8 @@ pub async fn deploy_new_container( Ok(response) => resp = response, } + // create container gives us an ID + // but start container requires a name let mut list_container_filters = HashMap::new(); list_container_filters.insert("id".to_string(), vec![resp.id]); match docker @@ -312,26 +355,28 @@ pub async fn deploy_new_container( })) .await { - Err(e) => Err(UnitClientError::UnitdDockerError { message: e.to_string() }), + // somehow our container doesnt exist + Err(e) => Err(UnitClientError::UnitdDockerError{ + message: e.to_string() + }), + // here it is! Ok(info) => { if info.len() < 1 { return Err(UnitClientError::UnitdDockerError { message: "couldnt find new container".to_string(), }); - } - if info[0].names.is_none() || info[0].names.clone().unwrap().len() < 1 { - return Err(UnitClientError::UnitdDockerError { - message: "new container has no name".to_string(), - }); - } + } else if info[0].names.is_none() || + info[0].names.clone().unwrap().len() < 1 { + return Err(UnitClientError::UnitdDockerError { + message: "new container has no name".to_string(), + }); + } - match docker - .start_container( - info[0].names.clone().unwrap()[0].strip_prefix("/").unwrap(), - None::>, - ) - .await - { + // start our container + match docker.start_container( + info[0].names.clone().unwrap()[0].strip_prefix(MAIN_SEPARATOR).unwrap(), + None::>, + ).await { Err(err) => Err(UnitClientError::UnitdDockerError { message: err.to_string(), }), diff --git a/tools/unitctl/unit-client-rs/src/unitd_instance.rs b/tools/unitctl/unit-client-rs/src/unitd_instance.rs index a7fb1bdc..ace8e858 100644 --- a/tools/unitctl/unit-client-rs/src/unitd_instance.rs +++ b/tools/unitctl/unit-client-rs/src/unitd_instance.rs @@ -26,6 +26,7 @@ impl Serialize for UnitdInstance { where S: Serializer, { + // 11 = fields to serialize let mut state = serializer.serialize_map(Some(11))?; let runtime_flags = self .process diff --git a/tools/unitctl/unit-client-rs/src/unitd_process.rs b/tools/unitctl/unit-client-rs/src/unitd_process.rs index 47ffcb5d..3dc0c3af 100644 --- a/tools/unitctl/unit-client-rs/src/unitd_process.rs +++ b/tools/unitctl/unit-client-rs/src/unitd_process.rs @@ -27,6 +27,7 @@ impl Serialize for UnitdProcess { where S: Serializer, { + // 6 = fields to serialize let mut state = serializer.serialize_map(Some(6))?; state.serialize_entry("pid", &self.process_id)?; state.serialize_entry("user", &self.user)?; diff --git a/tools/unitctl/unitctl/src/cmd/instances.rs b/tools/unitctl/unitctl/src/cmd/instances.rs index b9af75f6..a030f7d3 100644 --- a/tools/unitctl/unitctl/src/cmd/instances.rs +++ b/tools/unitctl/unitctl/src/cmd/instances.rs @@ -1,8 +1,11 @@ use crate::unitctl::{InstanceArgs, InstanceCommands}; use crate::{OutputFormat, UnitctlError}; +use crate::unitctl_error::ControlSocketErrorKind; + use std::path::PathBuf; use unit_client_rs::unitd_docker::deploy_new_container; use unit_client_rs::unitd_instance::UnitdInstance; +use unit_client_rs::control_socket_address::ControlSocket; pub(crate) async fn cmd(args: InstanceArgs) -> Result<(), UnitctlError> { if let Some(cmd) = args.command { @@ -12,19 +15,104 @@ pub(crate) async fn cmd(args: InstanceArgs) -> Result<(), UnitctlError> { ref application, ref image, } => { - println!("Pulling and starting a container from {}", image); - println!("Will mount {} to /var/run for socket access", socket); - println!("Will READ ONLY mount {} to /www for application access", application); - println!("Note: Container will be on host network"); - if !PathBuf::from(socket).is_dir() || !PathBuf::from(application).is_dir() { - eprintln!("application and socket paths must be directories"); + // validation for application dir + if !PathBuf::from(application).is_dir() { + eprintln!("application path must be a directory"); + Err(UnitctlError::NoFilesImported) + } else if !PathBuf::from(application).as_path().exists() { + eprintln!("application path must exist"); Err(UnitctlError::NoFilesImported) + } else { - deploy_new_container(socket, application, image).await.map_or_else( + let addr = ControlSocket::parse_address(socket); + if let Err(e) = addr { + return Err(UnitctlError::UnitClientError{source: e}); + } + + // validate we arent processing an abstract socket + if let ControlSocket::UnixLocalAbstractSocket(_) = addr.as_ref().unwrap() { + return Err(UnitctlError::ControlSocketError{ + kind: ControlSocketErrorKind::General, + message: "cannot pass abstract socket to docker container".to_string(), + }) + } + + // warn user of OSX docker limitations + if let ControlSocket::UnixLocalSocket(ref sock_path) = addr.as_ref().unwrap() { + if cfg!(target_os = "macos") { + return Err(UnitctlError::ControlSocketError{ + kind: ControlSocketErrorKind::General, + message: format!("Docker on OSX will break unix sockets mounted {} {}", + "in containers, see the following link for more information", + "https://github.com/docker/for-mac/issues/483"), + }) + } + + if !sock_path.is_dir() { + return Err(UnitctlError::ControlSocketError{ + kind: ControlSocketErrorKind::General, + message: format!("user must specify a directory of UNIX socket directory"), + }) + } + } + + // validate a TCP URI + if let ControlSocket::TcpSocket(uri) = addr.as_ref().unwrap() { + if let Some(host) = uri.host() { + if host != "127.0.0.1" { + return Err(UnitctlError::ControlSocketError{ + kind: ControlSocketErrorKind::General, + message: "TCP URI must point to 127.0.0.1".to_string(), + }) + } + } else { + return Err(UnitctlError::ControlSocketError{ + kind: ControlSocketErrorKind::General, + message: "TCP URI must point to a host".to_string(), + }) + } + + if let Some(port) = uri.port_u16() { + if port < 1025 { + eprintln!("warning! you are asking docker to forward a privileged port. {}", + "please make sure docker has access to it"); + } + } else { + return Err(UnitctlError::ControlSocketError{ + kind: ControlSocketErrorKind::General, + message: "TCP URI must specify a port".to_string(), + }) + } + + if uri.path() != "/" { + eprintln!("warning! path {} will be ignored", uri.path()) + } + } + + // reflect changes to user + // print this to STDERR to avoid polluting deserialized data output + eprintln!("> Pulling and starting a container from {}", image); + eprintln!("> Will READ ONLY mount {} to /www for application access", application); + eprintln!("> Container will be on host network"); + match addr.as_ref().unwrap() { + ControlSocket::UnixLocalSocket(path) => + eprintln!("> Will mount directory containing {} to /var/www for control API", + path.as_path().to_string_lossy()), + ControlSocket::TcpSocket(uri) => + eprintln!("> Will forward port {} for control API", uri.port_u16().unwrap()), + _ => unimplemented!(), // abstract socket case ruled out previously + } + + if cfg!(target_os = "macos") { + eprintln!("> mac users: enable host networking in docker desktop"); + } + + // do the actual deployment + deploy_new_container(addr.unwrap(), application, image).await.map_or_else( |e| Err(UnitctlError::UnitClientError { source: e }), |warn| { for i in warn { - println!("warning from docker: {}", i); + eprintln!("warning! from docker: {}", i); } Ok(()) }, -- cgit From 6d0880c9956243ba476ce25ca7c1060692d172a2 Mon Sep 17 00:00:00 2001 From: Dylan Arbour Date: Tue, 7 May 2024 14:38:15 -0400 Subject: Add unitctl build and release CI Adds a GitHub Actions workflow that builds and releases unitctl binaries when a tag prefixed with `unitctl/` is pushed. Binaries are built on pull-requests that change any files within `tools/unitctl`, on `master` branch pushes and when `unitctl/` prefixed tags are pushed. --- tools/unitctl/unit-openapi/README.md | 1 + 1 file changed, 1 insertion(+) (limited to 'tools/unitctl') diff --git a/tools/unitctl/unit-openapi/README.md b/tools/unitctl/unit-openapi/README.md index b8506bda..05aba6d9 100644 --- a/tools/unitctl/unit-openapi/README.md +++ b/tools/unitctl/unit-openapi/README.md @@ -409,3 +409,4 @@ cargo doc --open ## Author unit-owner@nginx.org + -- cgit From a98acdedd737c48329aef7d414b4391adcc578c8 Mon Sep 17 00:00:00 2001 From: Ava Hahn Date: Thu, 9 May 2024 17:25:00 -0700 Subject: ci: Add unit testing to unitctl CI workflow * fix a few misspellings in unitctl CI workflow * add unit testing job * exclude unitd integration test from unit tests * add workflow dispatch trigger * add calls to get workflow dispatch version Signed-off-by: Ava Hahn --- tools/unitctl/unit-client-rs/src/unitd_configure_options.rs | 1 + 1 file changed, 1 insertion(+) (limited to 'tools/unitctl') diff --git a/tools/unitctl/unit-client-rs/src/unitd_configure_options.rs b/tools/unitctl/unit-client-rs/src/unitd_configure_options.rs index 88ab1101..00ee22a3 100644 --- a/tools/unitctl/unit-client-rs/src/unitd_configure_options.rs +++ b/tools/unitctl/unit-client-rs/src/unitd_configure_options.rs @@ -221,6 +221,7 @@ mod tests { } #[test] + #[ignore] // run this one manually - not in CI fn can_run_unitd() { let specific_path = std::env::var(UNITD_PATH_ENV_KEY).map_err(|error| Box::new(error) as Box); let unitd_path = unitd_instance::find_executable_path(specific_path); -- cgit From 4e884d9ecce90e262ea2a8dd6fa53f6d5dce011e Mon Sep 17 00:00:00 2001 From: Gabor Javorszky Date: Fri, 17 May 2024 11:23:47 +0100 Subject: tools/unitctl: Replace matching image name to matching command Closes #1254 Matching to the `unitd` command is a far more reliable way to filtering docker instances that are running Unit. --- tools/unitctl/unit-client-rs/src/unitd_docker.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'tools/unitctl') diff --git a/tools/unitctl/unit-client-rs/src/unitd_docker.rs b/tools/unitctl/unit-client-rs/src/unitd_docker.rs index b9199e40..6881893d 100644 --- a/tools/unitctl/unit-client-rs/src/unitd_docker.rs +++ b/tools/unitctl/unit-client-rs/src/unitd_docker.rs @@ -151,13 +151,14 @@ impl UnitdContainer { vec![] } Ok(summary) => { + let unitd_command_re = Regex::new(r"^(.* )?unitd( .*)?$").unwrap(); + // cant do this functionally because of the async call let mut mapped = vec![]; for ctr in summary { - if ctr.clone().image + if unitd_command_re.is_match(&ctr.clone().command .or(Some(String::new())) - .unwrap() - .contains("unit") { + .unwrap()) { let mut c = UnitdContainer::from(&ctr); if let Some(names) = ctr.names { if names.len() > 0 { -- cgit From b91073e5b958d227c283654f26189f1457d43809 Mon Sep 17 00:00:00 2001 From: Gabor Javorszky Date: Fri, 17 May 2024 11:25:03 +0100 Subject: tools/unitctl: Replace format! with .to_string() --- tools/unitctl/unitctl/src/cmd/instances.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tools/unitctl') diff --git a/tools/unitctl/unitctl/src/cmd/instances.rs b/tools/unitctl/unitctl/src/cmd/instances.rs index a030f7d3..ee58f697 100644 --- a/tools/unitctl/unitctl/src/cmd/instances.rs +++ b/tools/unitctl/unitctl/src/cmd/instances.rs @@ -51,7 +51,7 @@ pub(crate) async fn cmd(args: InstanceArgs) -> Result<(), UnitctlError> { if !sock_path.is_dir() { return Err(UnitctlError::ControlSocketError{ kind: ControlSocketErrorKind::General, - message: format!("user must specify a directory of UNIX socket directory"), + message: "user must specify a directory of UNIX socket directory".to_string(), }) } } -- cgit From e0c15ae4575335fb079e2d33fc853a547b2380c9 Mon Sep 17 00:00:00 2001 From: Ava Hahn Date: Fri, 14 Jun 2024 21:04:15 -0700 Subject: tools/unitctl: implement application subcommand * application subcommand UI schema * application subcommand handler * additions to unit-client-rs to expose application API * elaborate on OpenAPI error handling * adds wasm and wasi app schemas to OpenAPI Schema * updates tools/unitctl OpenAPI library * many linter fixes * README.md updates Signed-off-by: Ava Hahn --- tools/unitctl/GNUmakefile | 4 +- tools/unitctl/README.md | 26 ++++- .../unit-client-rs/src/control_socket_address.rs | 2 - tools/unitctl/unit-client-rs/src/unit_client.rs | 48 ++++++++- tools/unitctl/unit-client-rs/src/unitd_docker.rs | 111 +++++++++------------ .../unitctl/unit-openapi/.openapi-generator/FILES | 18 ++-- .../unit-openapi/.openapi-generator/VERSION | 2 +- tools/unitctl/unit-openapi/README.md | 10 +- tools/unitctl/unit-openapi/src/lib.rs | 4 +- tools/unitctl/unitctl/src/cmd/applications.rs | 36 +++++++ tools/unitctl/unitctl/src/cmd/instances.rs | 77 +++++++------- tools/unitctl/unitctl/src/cmd/mod.rs | 1 + tools/unitctl/unitctl/src/main.rs | 7 +- tools/unitctl/unitctl/src/unitctl.rs | 41 +++++++- 14 files changed, 251 insertions(+), 136 deletions(-) create mode 100644 tools/unitctl/unitctl/src/cmd/applications.rs (limited to 'tools/unitctl') diff --git a/tools/unitctl/GNUmakefile b/tools/unitctl/GNUmakefile index e7cb379a..9992a322 100644 --- a/tools/unitctl/GNUmakefile +++ b/tools/unitctl/GNUmakefile @@ -23,7 +23,7 @@ CARGO ?= cargo DOCKER ?= docker DOCKER_BUILD_FLAGS ?= --load CHECKSUM ?= sha256sum -OPENAPI_GENERATOR_VERSION ?= 6.6.0 +OPENAPI_GENERATOR_VERSION ?= 7.6.0 # Define platform targets based off of the current host OS # If running MacOS, then build for MacOS platform targets installed in rustup @@ -137,7 +137,7 @@ openapi-clean: ## Clean up generated OpenAPI files $Q find "$(CURDIR)/unit-openapi/src/apis" \ ! -name 'error.rs' -type f -exec rm -f {} + $Q $(info $(M) cleaning up generated OpenAPI models code) - $Q rm -rf "$(CURDIR)/unit-openapi/src/models/*" + $Q rm -rf "$(CURDIR)/unit-openapi/src/models" include $(CURDIR)/build/package.mk include $(CURDIR)/build/container.mk diff --git a/tools/unitctl/README.md b/tools/unitctl/README.md index 2e5a2da1..dca16e63 100644 --- a/tools/unitctl/README.md +++ b/tools/unitctl/README.md @@ -31,13 +31,13 @@ their own makefile targets. Alternatively, all available binary targets can be built with `make all`. See the below example for illustration: ``` -[ava@calliope cli]$ make list-targets +$ make list-targets x86_64-unknown-linux-gnu -[ava@calliope cli]$ make x86_64-unknown-linux-gnu +$ make x86_64-unknown-linux-gnu â–¶ building unitctl with flags [--quiet --release --bin unitctl --target x86_64-unknown-linux-gnu] -[ava@calliope cli]$ file ./target/x86_64-unknown-linux-gnu/release/unitctl +$ file ./target/x86_64-unknown-linux-gnu/release/unitctl ./target/x86_64-unknown-linux-gnu/release/unitctl: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=ef4b094ffd549b39a8cb27a7ba2cc0dbad87a3bc, for GNU/Linux 4.4.0, @@ -91,6 +91,26 @@ To the subcommand `unitctl instances new` the user must provide three things: After deployment the user will have one Unit container running on the host network. +### Lists active applications and provides means to restart them +Listing applications: +``` +$ unitctl app list +{ + "wasm": { + "type": "wasm-wasi-component", + "component": "/www/wasmapp-proxy-component.wasm" + } +} +``` + +Restarting an application: +``` +$ unitctl app reload wasm +{ + "success": "Ok" +} +``` + ### Lists active listeners from running Unit processes ``` unitctl listeners diff --git a/tools/unitctl/unit-client-rs/src/control_socket_address.rs b/tools/unitctl/unit-client-rs/src/control_socket_address.rs index 402d2293..438ab0ad 100644 --- a/tools/unitctl/unit-client-rs/src/control_socket_address.rs +++ b/tools/unitctl/unit-client-rs/src/control_socket_address.rs @@ -34,7 +34,6 @@ impl ControlSocketScheme { } } - impl Display for ControlSocket { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { @@ -312,7 +311,6 @@ impl ControlSocket { } } - #[cfg(test)] mod tests { use rand::distributions::{Alphanumeric, DistString}; diff --git a/tools/unitctl/unit-client-rs/src/unit_client.rs b/tools/unitctl/unit-client-rs/src/unit_client.rs index b8c73ec0..b3f07308 100644 --- a/tools/unitctl/unit-client-rs/src/unit_client.rs +++ b/tools/unitctl/unit-client-rs/src/unit_client.rs @@ -15,9 +15,11 @@ use serde::{Deserialize, Serialize}; use crate::control_socket_address::ControlSocket; use unit_openapi::apis::configuration::Configuration; -use unit_openapi::apis::{Error as OpenAPIError, StatusApi}; -use unit_openapi::apis::{ListenersApi, ListenersApiClient, StatusApiClient}; -use unit_openapi::models::{ConfigListener, Status}; +use unit_openapi::apis::{ + ApplicationsApi, ApplicationsApiClient, AppsApi, AppsApiClient, Error as OpenAPIError, ListenersApi, + ListenersApiClient, StatusApi, StatusApiClient, +}; +use unit_openapi::models::{ConfigApplication, ConfigListener, Status}; const USER_AGENT: &str = concat!("UNIT CLI/", env!("CARGO_PKG_VERSION"), "/rust"); @@ -276,6 +278,46 @@ impl UnitClient { }) } + pub fn applications_api(&self) -> Box { + new_openapi_client!(self, ApplicationsApiClient, ApplicationsApi) + } + + pub async fn applications(&self) -> Result, Box> { + self.applications_api().get_applications().await.or_else(|err| { + if let OpenAPIError::Hyper(hyper_error) = err { + Err(Box::new(UnitClientError::new( + hyper_error, + self.control_socket.to_string(), + "/applications".to_string(), + ))) + } else { + Err(Box::new(UnitClientError::OpenAPIError { source: err })) + } + }) + } + + pub async fn per_application_api(&self) -> Box { + new_openapi_client!(self, AppsApiClient, AppsApi) + } + + pub async fn restart_application(&self, name: &String) -> Result, Box> { + self.per_application_api() + .await + .get_app_restart(name.as_str()) + .await + .or_else(|err| { + if let OpenAPIError::Hyper(hyper_error) = err { + Err(Box::new(UnitClientError::new( + hyper_error, + self.control_socket.to_string(), + format!("/control/applications/{}/restart", name), + ))) + } else { + Err(Box::new(UnitClientError::OpenAPIError { source: err })) + } + }) + } + pub async fn is_running(&self) -> bool { self.status().await.is_ok() } diff --git a/tools/unitctl/unit-client-rs/src/unitd_docker.rs b/tools/unitctl/unit-client-rs/src/unitd_docker.rs index 6881893d..0d318096 100644 --- a/tools/unitctl/unit-client-rs/src/unitd_docker.rs +++ b/tools/unitctl/unit-client-rs/src/unitd_docker.rs @@ -1,19 +1,16 @@ use std::collections::HashMap; use std::fs::read_to_string; -use std::path::{PathBuf, MAIN_SEPARATOR}; use std::io::stderr; +use std::path::{PathBuf, MAIN_SEPARATOR}; +use crate::control_socket_address::ControlSocket; use crate::futures::StreamExt; use crate::unit_client::UnitClientError; use crate::unitd_process::UnitdProcess; -use crate::control_socket_address::ControlSocket; use bollard::container::{Config, ListContainersOptions, StartContainerOptions}; use bollard::image::CreateImageOptions; -use bollard::models::{ - ContainerCreateResponse, HostConfig, Mount, - MountTypeEnum, ContainerSummary, -}; +use bollard::models::{ContainerCreateResponse, ContainerSummary, HostConfig, Mount, MountTypeEnum}; use bollard::secret::ContainerInspectResponse; use bollard::Docker; @@ -156,22 +153,18 @@ impl UnitdContainer { // cant do this functionally because of the async call let mut mapped = vec![]; for ctr in summary { - if unitd_command_re.is_match(&ctr.clone().command - .or(Some(String::new())) - .unwrap()) { - let mut c = UnitdContainer::from(&ctr); - if let Some(names) = ctr.names { - if names.len() > 0 { - let name = names[0].strip_prefix("/") - .or(Some(names[0].as_str())).unwrap(); - if let Ok(cir) = docker - .inspect_container(name, None).await { - c.details = Some(cir); - } + if unitd_command_re.is_match(&ctr.clone().command.or(Some(String::new())).unwrap()) { + let mut c = UnitdContainer::from(&ctr); + if let Some(names) = ctr.names { + if names.len() > 0 { + let name = names[0].strip_prefix("/").or(Some(names[0].as_str())).unwrap(); + if let Ok(cir) = docker.inspect_container(name, None).await { + c.details = Some(cir); } } - mapped.push(c); } + mapped.push(c); + } } mapped } @@ -196,11 +189,11 @@ impl UnitdContainer { // either return translated path or original prefixed with "container" if keys.len() > 0 { - let mut matches = self.mounts[&keys[0]] - .clone() - .join(cp.as_path() - .strip_prefix(keys[0].clone()) - .expect("error checking path prefix")); + let mut matches = self.mounts[&keys[0]].clone().join( + cp.as_path() + .strip_prefix(keys[0].clone()) + .expect("error checking path prefix"), + ); /* Observed on M1 Mac that Docker on OSX * adds a bunch of garbage to the mount path * converting it into a useless directory @@ -208,15 +201,14 @@ impl UnitdContainer { */ if cfg!(target_os = "macos") { let mut abs = PathBuf::from(String::from(MAIN_SEPARATOR)); - let m = matches.strip_prefix("/host_mnt/private") - .unwrap_or(matches.strip_prefix("/host_mnt") - .unwrap_or(matches.as_path())); + let m = matches + .strip_prefix("/host_mnt/private") + .unwrap_or(matches.strip_prefix("/host_mnt").unwrap_or(matches.as_path())); // make it absolute again abs.push(m); matches = abs; } - matches.to_string_lossy() - .to_string() + matches.to_string_lossy().to_string() } else { format!(":{}", cp.display()) } @@ -264,10 +256,7 @@ pub async fn deploy_new_container( let mut mounts = vec![]; // if a unix socket is specified, mounts its directory if socket.is_local_socket() { - let mount_path = PathBuf::from(socket.clone()) - .as_path() - .to_string_lossy() - .to_string(); + let mount_path = PathBuf::from(socket.clone()).as_path().to_string_lossy().to_string(); mounts.push(Mount { typ: Some(MountTypeEnum::BIND), source: Some(mount_path), @@ -290,22 +279,22 @@ pub async fn deploy_new_container( Some(CreateImageOptions { from_image: image.as_str(), ..Default::default() - }), None, None + }), + None, + None, ); while let Some(res) = stream.next().await { if let Ok(info) = res { if let Some(id) = info.id { if let Some(_) = totals.get_mut(&id) { - if let Some(delta) = info.progress_detail - .and_then(|detail| detail.current) { - pb.add(delta as u64); - } + if let Some(delta) = info.progress_detail.and_then(|detail| detail.current) { + pb.add(delta as u64); + } } else { - if let Some(total) = info.progress_detail - .and_then(|detail| detail.total) { - totals.insert(id, total); - pb.total += total as u64; - } + if let Some(total) = info.progress_detail.and_then(|detail| detail.total) { + totals.insert(id, total); + pb.total += total as u64; + } } } } @@ -357,27 +346,27 @@ pub async fn deploy_new_container( .await { // somehow our container doesnt exist - Err(e) => Err(UnitClientError::UnitdDockerError{ - message: e.to_string() - }), + Err(e) => Err(UnitClientError::UnitdDockerError { message: e.to_string() }), // here it is! Ok(info) => { if info.len() < 1 { return Err(UnitClientError::UnitdDockerError { message: "couldnt find new container".to_string(), }); - } else if info[0].names.is_none() || - info[0].names.clone().unwrap().len() < 1 { - return Err(UnitClientError::UnitdDockerError { - message: "new container has no name".to_string(), - }); - } + } else if info[0].names.is_none() || info[0].names.clone().unwrap().len() < 1 { + return Err(UnitClientError::UnitdDockerError { + message: "new container has no name".to_string(), + }); + } // start our container - match docker.start_container( - info[0].names.clone().unwrap()[0].strip_prefix(MAIN_SEPARATOR).unwrap(), - None::>, - ).await { + match docker + .start_container( + info[0].names.clone().unwrap()[0].strip_prefix(MAIN_SEPARATOR).unwrap(), + None::>, + ) + .await + { Err(err) => Err(UnitClientError::UnitdDockerError { message: err.to_string(), }), @@ -439,14 +428,8 @@ mod tests { ctr.host_path("/path/to/conf".to_string()) ); if cfg!(target_os = "macos") { - assert_eq!( - "/6/test".to_string(), - ctr.host_path("/var/test".to_string()) - ); - assert_eq!( - "/7/test".to_string(), - ctr.host_path("/var/var/test".to_string()) - ); + assert_eq!("/6/test".to_string(), ctr.host_path("/var/test".to_string())); + assert_eq!("/7/test".to_string(), ctr.host_path("/var/var/test".to_string())); } } diff --git a/tools/unitctl/unit-openapi/.openapi-generator/FILES b/tools/unitctl/unit-openapi/.openapi-generator/FILES index 4f177f5f..f487c081 100644 --- a/tools/unitctl/unit-openapi/.openapi-generator/FILES +++ b/tools/unitctl/unit-openapi/.openapi-generator/FILES @@ -26,21 +26,18 @@ docs/ConfigApplicationCommonLimits.md docs/ConfigApplicationCommonProcesses.md docs/ConfigApplicationCommonProcessesAnyOf.md docs/ConfigApplicationExternal.md -docs/ConfigApplicationExternalAllOf.md docs/ConfigApplicationJava.md -docs/ConfigApplicationJavaAllOf.md docs/ConfigApplicationPerl.md -docs/ConfigApplicationPerlAllOf.md docs/ConfigApplicationPhp.md -docs/ConfigApplicationPhpAllOf.md docs/ConfigApplicationPhpAllOfOptions.md docs/ConfigApplicationPhpAllOfTargets.md docs/ConfigApplicationPython.md -docs/ConfigApplicationPythonAllOf.md docs/ConfigApplicationPythonAllOfPath.md docs/ConfigApplicationPythonAllOfTargets.md docs/ConfigApplicationRuby.md -docs/ConfigApplicationRubyAllOf.md +docs/ConfigApplicationWasi.md +docs/ConfigApplicationWasm.md +docs/ConfigApplicationWasmAllOfAccess.md docs/ConfigListener.md docs/ConfigListenerForwarded.md docs/ConfigListenerForwardedSource.md @@ -114,21 +111,18 @@ src/models/config_application_common_limits.rs src/models/config_application_common_processes.rs src/models/config_application_common_processes_any_of.rs src/models/config_application_external.rs -src/models/config_application_external_all_of.rs src/models/config_application_java.rs -src/models/config_application_java_all_of.rs src/models/config_application_perl.rs -src/models/config_application_perl_all_of.rs src/models/config_application_php.rs -src/models/config_application_php_all_of.rs src/models/config_application_php_all_of_options.rs src/models/config_application_php_all_of_targets.rs src/models/config_application_python.rs -src/models/config_application_python_all_of.rs src/models/config_application_python_all_of_path.rs src/models/config_application_python_all_of_targets.rs src/models/config_application_ruby.rs -src/models/config_application_ruby_all_of.rs +src/models/config_application_wasi.rs +src/models/config_application_wasm.rs +src/models/config_application_wasm_all_of_access.rs src/models/config_listener.rs src/models/config_listener_forwarded.rs src/models/config_listener_forwarded_source.rs diff --git a/tools/unitctl/unit-openapi/.openapi-generator/VERSION b/tools/unitctl/unit-openapi/.openapi-generator/VERSION index cd802a1e..93c8ddab 100644 --- a/tools/unitctl/unit-openapi/.openapi-generator/VERSION +++ b/tools/unitctl/unit-openapi/.openapi-generator/VERSION @@ -1 +1 @@ -6.6.0 \ No newline at end of file +7.6.0 diff --git a/tools/unitctl/unit-openapi/README.md b/tools/unitctl/unit-openapi/README.md index 05aba6d9..5bad3fa4 100644 --- a/tools/unitctl/unit-openapi/README.md +++ b/tools/unitctl/unit-openapi/README.md @@ -22,6 +22,7 @@ This API client was generated by the [OpenAPI Generator](https://openapi-generat - API version: 0.2.0 - Package version: 0.4.0-beta +- Generator version: 7.6.0 - Build package: `org.openapitools.codegen.languages.RustClientCodegen` ## Installation @@ -354,21 +355,18 @@ Class | Method | HTTP request | Description - [ConfigApplicationCommonProcesses](docs/ConfigApplicationCommonProcesses.md) - [ConfigApplicationCommonProcessesAnyOf](docs/ConfigApplicationCommonProcessesAnyOf.md) - [ConfigApplicationExternal](docs/ConfigApplicationExternal.md) - - [ConfigApplicationExternalAllOf](docs/ConfigApplicationExternalAllOf.md) - [ConfigApplicationJava](docs/ConfigApplicationJava.md) - - [ConfigApplicationJavaAllOf](docs/ConfigApplicationJavaAllOf.md) - [ConfigApplicationPerl](docs/ConfigApplicationPerl.md) - - [ConfigApplicationPerlAllOf](docs/ConfigApplicationPerlAllOf.md) - [ConfigApplicationPhp](docs/ConfigApplicationPhp.md) - - [ConfigApplicationPhpAllOf](docs/ConfigApplicationPhpAllOf.md) - [ConfigApplicationPhpAllOfOptions](docs/ConfigApplicationPhpAllOfOptions.md) - [ConfigApplicationPhpAllOfTargets](docs/ConfigApplicationPhpAllOfTargets.md) - [ConfigApplicationPython](docs/ConfigApplicationPython.md) - - [ConfigApplicationPythonAllOf](docs/ConfigApplicationPythonAllOf.md) - [ConfigApplicationPythonAllOfPath](docs/ConfigApplicationPythonAllOfPath.md) - [ConfigApplicationPythonAllOfTargets](docs/ConfigApplicationPythonAllOfTargets.md) - [ConfigApplicationRuby](docs/ConfigApplicationRuby.md) - - [ConfigApplicationRubyAllOf](docs/ConfigApplicationRubyAllOf.md) + - [ConfigApplicationWasi](docs/ConfigApplicationWasi.md) + - [ConfigApplicationWasm](docs/ConfigApplicationWasm.md) + - [ConfigApplicationWasmAllOfAccess](docs/ConfigApplicationWasmAllOfAccess.md) - [ConfigListener](docs/ConfigListener.md) - [ConfigListenerForwarded](docs/ConfigListenerForwarded.md) - [ConfigListenerForwardedSource](docs/ConfigListenerForwardedSource.md) diff --git a/tools/unitctl/unit-openapi/src/lib.rs b/tools/unitctl/unit-openapi/src/lib.rs index a71f18d6..5435cfdb 100644 --- a/tools/unitctl/unit-openapi/src/lib.rs +++ b/tools/unitctl/unit-openapi/src/lib.rs @@ -1,6 +1,6 @@ #![allow(clippy::all)] -#[macro_use] -extern crate serde_derive; +#![allow(unused_imports)] +#![allow(clippy::too_many_arguments)] extern crate futures; extern crate hyper; diff --git a/tools/unitctl/unitctl/src/cmd/applications.rs b/tools/unitctl/unitctl/src/cmd/applications.rs new file mode 100644 index 00000000..f4c44105 --- /dev/null +++ b/tools/unitctl/unitctl/src/cmd/applications.rs @@ -0,0 +1,36 @@ +use crate::unitctl::{ApplicationArgs, ApplicationCommands, UnitCtl}; +use crate::{wait, UnitctlError}; +use crate::requests::send_empty_body_deserialize_response; +use unit_client_rs::unit_client::UnitClient; + +pub(crate) async fn cmd(cli: &UnitCtl, args: &ApplicationArgs) -> Result<(), UnitctlError> { + let control_socket = wait::wait_for_socket(cli).await?; + let client = UnitClient::new(control_socket); + + match &args.command { + ApplicationCommands::Reload { ref name } => client + .restart_application(name) + .await + .map_err(|e| UnitctlError::UnitClientError { source: *e }) + .and_then(|r| args.output_format.write_to_stdout(&r)), + + /* we should be able to use this but the openapi generator library + * is fundamentally incorrect and provides a broken API for the + * applications endpoint. + ApplicationCommands::List {} => client + .applications() + .await + .map_err(|e| UnitctlError::UnitClientError { source: *e }) + .and_then(|response| args.output_format.write_to_stdout(&response)),*/ + + ApplicationCommands::List {} => { + args.output_format.write_to_stdout( + &send_empty_body_deserialize_response( + &client, + "GET", + "/config/applications", + ).await? + ) + }, + } +} diff --git a/tools/unitctl/unitctl/src/cmd/instances.rs b/tools/unitctl/unitctl/src/cmd/instances.rs index ee58f697..e532a151 100644 --- a/tools/unitctl/unitctl/src/cmd/instances.rs +++ b/tools/unitctl/unitctl/src/cmd/instances.rs @@ -1,11 +1,11 @@ use crate::unitctl::{InstanceArgs, InstanceCommands}; -use crate::{OutputFormat, UnitctlError}; use crate::unitctl_error::ControlSocketErrorKind; +use crate::{OutputFormat, UnitctlError}; use std::path::PathBuf; +use unit_client_rs::control_socket_address::ControlSocket; use unit_client_rs::unitd_docker::deploy_new_container; use unit_client_rs::unitd_instance::UnitdInstance; -use unit_client_rs::control_socket_address::ControlSocket; pub(crate) async fn cmd(args: InstanceArgs) -> Result<(), UnitctlError> { if let Some(cmd) = args.command { @@ -22,37 +22,38 @@ pub(crate) async fn cmd(args: InstanceArgs) -> Result<(), UnitctlError> { } else if !PathBuf::from(application).as_path().exists() { eprintln!("application path must exist"); Err(UnitctlError::NoFilesImported) - } else { let addr = ControlSocket::parse_address(socket); if let Err(e) = addr { - return Err(UnitctlError::UnitClientError{source: e}); + return Err(UnitctlError::UnitClientError { source: e }); } // validate we arent processing an abstract socket if let ControlSocket::UnixLocalAbstractSocket(_) = addr.as_ref().unwrap() { - return Err(UnitctlError::ControlSocketError{ + return Err(UnitctlError::ControlSocketError { kind: ControlSocketErrorKind::General, message: "cannot pass abstract socket to docker container".to_string(), - }) + }); } // warn user of OSX docker limitations if let ControlSocket::UnixLocalSocket(ref sock_path) = addr.as_ref().unwrap() { if cfg!(target_os = "macos") { - return Err(UnitctlError::ControlSocketError{ + return Err(UnitctlError::ControlSocketError { kind: ControlSocketErrorKind::General, - message: format!("Docker on OSX will break unix sockets mounted {} {}", - "in containers, see the following link for more information", - "https://github.com/docker/for-mac/issues/483"), - }) + message: format!( + "Docker on macOS will break unix domain sockets mounted {} {}", + "in containers, see the following link for more information", + "https://github.com/docker/for-mac/issues/483" + ), + }); } if !sock_path.is_dir() { - return Err(UnitctlError::ControlSocketError{ + return Err(UnitctlError::ControlSocketError { kind: ControlSocketErrorKind::General, message: "user must specify a directory of UNIX socket directory".to_string(), - }) + }); } } @@ -60,28 +61,30 @@ pub(crate) async fn cmd(args: InstanceArgs) -> Result<(), UnitctlError> { if let ControlSocket::TcpSocket(uri) = addr.as_ref().unwrap() { if let Some(host) = uri.host() { if host != "127.0.0.1" { - return Err(UnitctlError::ControlSocketError{ + return Err(UnitctlError::ControlSocketError { kind: ControlSocketErrorKind::General, message: "TCP URI must point to 127.0.0.1".to_string(), - }) + }); } } else { - return Err(UnitctlError::ControlSocketError{ + return Err(UnitctlError::ControlSocketError { kind: ControlSocketErrorKind::General, message: "TCP URI must point to a host".to_string(), - }) + }); } if let Some(port) = uri.port_u16() { if port < 1025 { - eprintln!("warning! you are asking docker to forward a privileged port. {}", - "please make sure docker has access to it"); + eprintln!( + "warning! you are asking docker to forward a privileged port. {}", + "please make sure docker has access to it" + ); } } else { - return Err(UnitctlError::ControlSocketError{ + return Err(UnitctlError::ControlSocketError { kind: ControlSocketErrorKind::General, message: "TCP URI must specify a port".to_string(), - }) + }); } if uri.path() != "/" { @@ -95,11 +98,13 @@ pub(crate) async fn cmd(args: InstanceArgs) -> Result<(), UnitctlError> { eprintln!("> Will READ ONLY mount {} to /www for application access", application); eprintln!("> Container will be on host network"); match addr.as_ref().unwrap() { - ControlSocket::UnixLocalSocket(path) => - eprintln!("> Will mount directory containing {} to /var/www for control API", - path.as_path().to_string_lossy()), - ControlSocket::TcpSocket(uri) => - eprintln!("> Will forward port {} for control API", uri.port_u16().unwrap()), + ControlSocket::UnixLocalSocket(path) => eprintln!( + "> Will mount directory containing {} to /var/www for control API", + path.as_path().to_string_lossy() + ), + ControlSocket::TcpSocket(uri) => { + eprintln!("> Will forward port {} for control API", uri.port_u16().unwrap()) + } _ => unimplemented!(), // abstract socket case ruled out previously } @@ -108,15 +113,17 @@ pub(crate) async fn cmd(args: InstanceArgs) -> Result<(), UnitctlError> { } // do the actual deployment - deploy_new_container(addr.unwrap(), application, image).await.map_or_else( - |e| Err(UnitctlError::UnitClientError { source: e }), - |warn| { - for i in warn { - eprintln!("warning! from docker: {}", i); - } - Ok(()) - }, - ) + deploy_new_container(addr.unwrap(), application, image) + .await + .map_or_else( + |e| Err(UnitctlError::UnitClientError { source: e }), + |warn| { + for i in warn { + eprintln!("warning! from docker: {}", i); + } + Ok(()) + }, + ) } } } diff --git a/tools/unitctl/unitctl/src/cmd/mod.rs b/tools/unitctl/unitctl/src/cmd/mod.rs index 989a0109..07c50912 100644 --- a/tools/unitctl/unitctl/src/cmd/mod.rs +++ b/tools/unitctl/unitctl/src/cmd/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod applications; pub(crate) mod edit; pub(crate) mod execute; pub(crate) mod import; diff --git a/tools/unitctl/unitctl/src/main.rs b/tools/unitctl/unitctl/src/main.rs index 12322873..6c9faaf7 100644 --- a/tools/unitctl/unitctl/src/main.rs +++ b/tools/unitctl/unitctl/src/main.rs @@ -8,7 +8,7 @@ extern crate unit_client_rs; use clap::Parser; -use crate::cmd::{edit, execute as execute_cmd, import, instances, listeners, status}; +use crate::cmd::{applications, edit, execute as execute_cmd, import, instances, listeners, status}; use crate::output_format::OutputFormat; use crate::unitctl::{Commands, UnitCtl}; use crate::unitctl_error::UnitctlError; @@ -30,6 +30,8 @@ async fn main() -> Result<(), UnitctlError> { match cli.command { Commands::Instances(args) => instances::cmd(args).await, + Commands::App(ref args) => applications::cmd(&cli, args).await, + Commands::Edit { output_format } => edit::cmd(&cli, output_format).await, Commands::Import { ref directory } => import::cmd(&cli, directory).await, @@ -67,6 +69,9 @@ fn eprint_error(error: &UnitctlError) { eprintln!("{}", source); eprintln!("Try running again with the same permissions as the unit control socket"); } + UnitClientError::OpenAPIError { source } => { + eprintln!("OpenAPI Error: {}", source); + } _ => { eprintln!("Unit client error: {}", source); } diff --git a/tools/unitctl/unitctl/src/unitctl.rs b/tools/unitctl/unitctl/src/unitctl.rs index b1bdbbd1..47f33820 100644 --- a/tools/unitctl/unitctl/src/unitctl.rs +++ b/tools/unitctl/unitctl/src/unitctl.rs @@ -41,9 +41,9 @@ pub(crate) struct UnitCtl { #[derive(Debug, Subcommand)] pub(crate) enum Commands { - #[command(about = "List all running UNIT processes")] + #[command(about = "List all running Unit processes")] Instances(InstanceArgs), - #[command(about = "Open current UNIT configuration in editor")] + #[command(about = "Open current Unit configuration in editor")] Edit { #[arg( required = false, @@ -60,7 +60,7 @@ pub(crate) enum Commands { #[arg(required = true, help = "Directory to import from")] directory: PathBuf, }, - #[command(about = "Sends raw JSON payload to UNIT")] + #[command(about = "Sends raw JSON payload to Unit")] Execute { #[arg( required = false, @@ -90,7 +90,7 @@ pub(crate) enum Commands { #[arg(required = true, short = 'p', long = "path")] path: String, }, - #[command(about = "Get the current status of UNIT")] + #[command(about = "Get the current status of Unit")] Status { #[arg( required = false, @@ -114,6 +114,8 @@ pub(crate) enum Commands { )] output_format: OutputFormat, }, + #[command(about = "List all configured Unit applications")] + App(ApplicationArgs), } #[derive(Debug, Args)] @@ -135,7 +137,7 @@ pub struct InstanceArgs { #[derive(Debug, Subcommand)] #[command(args_conflicts_with_subcommands = true)] pub enum InstanceCommands { - #[command(about = "deploy a new docker instance of unitd")] + #[command(about = "deploy a new docker instance of Unit")] New { #[arg(required = true, help = "Path to mount control socket to host")] socket: String, @@ -151,6 +153,35 @@ pub enum InstanceCommands { }, } +#[derive(Debug, Args)] +pub struct ApplicationArgs { + #[arg( + required = false, + global = true, + short = 't', + long = "output-format", + default_value = "text", + help = "Output format: text, yaml, json, json-pretty (default)" + )] + pub output_format: OutputFormat, + + #[command(subcommand)] + pub command: ApplicationCommands, +} + +#[derive(Debug, Subcommand)] +#[command(args_conflicts_with_subcommands = true)] +pub enum ApplicationCommands { + #[command(about = "reload a running application")] + Reload { + #[arg(required = true, help = "name of application")] + name: String, + }, + + #[command(about = "list running applications")] + List {}, +} + fn parse_control_socket_address(s: &str) -> Result { ControlSocket::try_from(s).map_err(|e| ClapError::raw(ValueValidation, e.to_string())) } -- cgit From 57a0f94efb2550ea1c6cb593b2b968e022c89346 Mon Sep 17 00:00:00 2001 From: Ava Hahn Date: Tue, 18 Jun 2024 23:06:05 -0700 Subject: tools/unitctl: unitctl export * new subcommand for "export" in CLI * new cmd submodule for exporting config tarballs * logic to also output to stdout * README additions * limitations documented Signed-off-by: Ava Hahn --- tools/unitctl/Cargo.lock | 66 ++++++++++++++++++++++++----------- tools/unitctl/README.md | 14 ++++++++ tools/unitctl/unitctl/Cargo.toml | 1 + tools/unitctl/unitctl/src/cmd/mod.rs | 1 + tools/unitctl/unitctl/src/cmd/save.rs | 52 +++++++++++++++++++++++++++ tools/unitctl/unitctl/src/main.rs | 8 ++++- tools/unitctl/unitctl/src/unitctl.rs | 10 ++++++ 7 files changed, 130 insertions(+), 22 deletions(-) create mode 100644 tools/unitctl/unitctl/src/cmd/save.rs (limited to 'tools/unitctl') diff --git a/tools/unitctl/Cargo.lock b/tools/unitctl/Cargo.lock index 16241296..20279963 100644 --- a/tools/unitctl/Cargo.lock +++ b/tools/unitctl/Cargo.lock @@ -485,23 +485,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.1" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 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", + "windows-sys 0.52.0", ] [[package]] @@ -510,6 +499,18 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys 0.52.0", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1009,9 +1010,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.150" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" @@ -1034,9 +1035,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.11" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "log" @@ -1488,15 +1489,15 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.25" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1788,6 +1789,17 @@ dependencies = [ "windows", ] +[[package]] +name = "tar" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.8.1" @@ -2061,6 +2073,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "tar", "tempfile", "tokio", "unit-client-rs", @@ -2439,6 +2452,17 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] + [[package]] name = "yansi" version = "0.5.1" diff --git a/tools/unitctl/README.md b/tools/unitctl/README.md index dca16e63..74366007 100644 --- a/tools/unitctl/README.md +++ b/tools/unitctl/README.md @@ -177,6 +177,20 @@ Imported /opt/unit/config/put.json -> /config Imported 3 files ``` +### Export configuration from a running Unit instance +``` +$ unitctl export -f config.tar +``` + +Addtionally, standard out can be used: +``` +$ unitctl export -f - +$ unitctl export -f - | tar xf - config.json +$ unitctl export -f - > config.tar +``` + +*Note:* The exported configuration omits certificates. + ### Wait for socket to become available ``` $ unitctl --wait-timeout-seconds=3 --wait-max-tries=4 import /opt/unit/config` diff --git a/tools/unitctl/unitctl/Cargo.toml b/tools/unitctl/unitctl/Cargo.toml index 98930fb3..8d83b424 100644 --- a/tools/unitctl/unitctl/Cargo.toml +++ b/tools/unitctl/unitctl/Cargo.toml @@ -32,6 +32,7 @@ hyperlocal = "0.8" hyper-tls = "0.5" tokio = { version = "1.35", features = ["macros"] } futures = "0.3" +tar = "0.4.41" [package.metadata.deb] copyright = "2022, F5" diff --git a/tools/unitctl/unitctl/src/cmd/mod.rs b/tools/unitctl/unitctl/src/cmd/mod.rs index 07c50912..f2a2c120 100644 --- a/tools/unitctl/unitctl/src/cmd/mod.rs +++ b/tools/unitctl/unitctl/src/cmd/mod.rs @@ -5,3 +5,4 @@ pub(crate) mod import; pub(crate) mod instances; pub(crate) mod listeners; pub(crate) mod status; +pub(crate) mod save; diff --git a/tools/unitctl/unitctl/src/cmd/save.rs b/tools/unitctl/unitctl/src/cmd/save.rs new file mode 100644 index 00000000..bce8fdb9 --- /dev/null +++ b/tools/unitctl/unitctl/src/cmd/save.rs @@ -0,0 +1,52 @@ +use crate::unitctl::UnitCtl; +use crate::wait; +use crate::UnitctlError; +use crate::requests::send_empty_body_deserialize_response; +use unit_client_rs::unit_client::UnitClient; +use tar::{Builder, Header}; +use std::fs::File; +use std::io::stdout; + + +pub async fn cmd( + cli: &UnitCtl, + filename: &String +) -> Result<(), UnitctlError> { + if !filename.ends_with(".tar") { + eprintln!("Warning: writing uncompressed tarball to {}", filename); + } + + let control_socket = wait::wait_for_socket(cli).await?; + let client = UnitClient::new(control_socket); + + let config_res = serde_json::to_string_pretty( + &send_empty_body_deserialize_response(&client, "GET", "/config").await? + ); + if let Err(e) = config_res { + return Err(UnitctlError::DeserializationError{message: e.to_string()}) + } + + let current_config = config_res + .unwrap() + .into_bytes(); + + //let current_js_modules = send_empty_body_deserialize_response(&client, "GET", "/js_modules") + // .await?; + + let mut conf_header = Header::new_gnu(); + conf_header.set_size(current_config.len() as u64); + conf_header.set_mode(0o644); + conf_header.set_cksum(); + + // builder has a different type depending on output + if filename == "-" { + let mut ar = Builder::new(stdout()); + ar.append_data(&mut conf_header, "config.json", current_config.as_slice()).unwrap(); + } else { + let file = File::create(filename).unwrap(); + let mut ar = Builder::new(file); + ar.append_data(&mut conf_header, "config.json", current_config.as_slice()).unwrap(); + } + + Ok(()) +} diff --git a/tools/unitctl/unitctl/src/main.rs b/tools/unitctl/unitctl/src/main.rs index 6c9faaf7..8f33fc16 100644 --- a/tools/unitctl/unitctl/src/main.rs +++ b/tools/unitctl/unitctl/src/main.rs @@ -8,7 +8,11 @@ extern crate unit_client_rs; use clap::Parser; -use crate::cmd::{applications, edit, execute as execute_cmd, import, instances, listeners, status}; +use crate::cmd::{ + applications, edit, execute as execute_cmd, + import, instances, listeners, status, + save +}; use crate::output_format::OutputFormat; use crate::unitctl::{Commands, UnitCtl}; use crate::unitctl_error::UnitctlError; @@ -46,6 +50,8 @@ async fn main() -> Result<(), UnitctlError> { Commands::Status { output_format } => status::cmd(&cli, output_format).await, Commands::Listeners { output_format } => listeners::cmd(&cli, output_format).await, + + Commands::Export { ref filename } => save::cmd(&cli, filename).await, } .map_err(|error| { eprint_error(&error); diff --git a/tools/unitctl/unitctl/src/unitctl.rs b/tools/unitctl/unitctl/src/unitctl.rs index 47f33820..e567116b 100644 --- a/tools/unitctl/unitctl/src/unitctl.rs +++ b/tools/unitctl/unitctl/src/unitctl.rs @@ -116,6 +116,16 @@ pub(crate) enum Commands { }, #[command(about = "List all configured Unit applications")] App(ApplicationArgs), + + #[command(about = "Export the current configuration of UNIT")] + Export { + #[arg( + required = true, + short = 'f', + help = "tarball filename to save configuration to" + )] + filename: String + }, } #[derive(Debug, Args)] -- cgit From 706ea1a6890ac243d0aded8cfbcbd152affc8701 Mon Sep 17 00:00:00 2001 From: Ava Hahn Date: Wed, 3 Jul 2024 12:44:56 -0700 Subject: tools/unitctl: Enable Multi Socket Support This commit refactors the CLI code to accept multiple instances of the control socket flag. All subcommands except for edit and save now support being run against multiple specified instances of unitd. * control_socket_addresses CLI field is now a vector * centralize error related logic into the error module * wait_for_socket now returns a vector of sockets. all sockets in vector are waited upon and validated * extraneous code is removed * applications, execute, import, listeners, and status commands all run against N control sockets now * edit and save commands return error when run against a single control socket Signed-off-by: Ava Hahn --- tools/unitctl/README.md | 33 ++++++ tools/unitctl/unitctl/src/cmd/applications.rs | 62 +++++----- tools/unitctl/unitctl/src/cmd/edit.rs | 13 ++- tools/unitctl/unitctl/src/cmd/execute.rs | 25 +++- tools/unitctl/unitctl/src/cmd/import.rs | 12 +- tools/unitctl/unitctl/src/cmd/listeners.rs | 25 ++-- tools/unitctl/unitctl/src/cmd/save.rs | 15 ++- tools/unitctl/unitctl/src/cmd/status.rs | 25 ++-- tools/unitctl/unitctl/src/main.rs | 57 +-------- tools/unitctl/unitctl/src/unitctl.rs | 2 +- tools/unitctl/unitctl/src/unitctl_error.rs | 53 +++++++++ tools/unitctl/unitctl/src/wait.rs | 162 +++++++++++--------------- 12 files changed, 282 insertions(+), 202 deletions(-) (limited to 'tools/unitctl') diff --git a/tools/unitctl/README.md b/tools/unitctl/README.md index 74366007..4aa6068c 100644 --- a/tools/unitctl/README.md +++ b/tools/unitctl/README.md @@ -111,6 +111,14 @@ $ unitctl app reload wasm } ``` +*Note:* Both of the above commands support operating on multiple instances +of Unit at once. To do this, pass multiple values for the `-s` flag as +shown below: + +``` +$ unitctl -s '127.0.0.1:8001' -s /run/nginx-unit.control.sock app list +``` + ### Lists active listeners from running Unit processes ``` unitctl listeners @@ -122,6 +130,13 @@ No socket path provided - attempting to detect from running instance } ``` +*Note:* This command supports operating on multiple instances of Unit at once. +To do this, pass multiple values for the `-s` flag as shown below: + +``` +$ unitctl -s '127.0.0.1:8001' -s /run/nginx-unit.control.sock listeners +``` + ### Get the current status of NGINX Unit processes ``` $ unitctl status -t yaml @@ -136,6 +151,13 @@ requests: applications: {} ``` +*Note:* This command supports operating on multiple instances of Unit at once. +To do this, pass multiple values for the `-s` flag as shown below: + +``` +$ unitctl -s '127.0.0.1:8001' -s /run/nginx-unit.control.sock status +``` + ### Send arbitrary configuration payloads to Unit ``` $ echo '{ @@ -158,6 +180,13 @@ $ echo '{ } ``` +*Note:* This command supports operating on multiple instances of Unit at once. +To do this, pass multiple values for the `-s` flag as shown below: + +``` +$ unitctl -s '127.0.0.1:8001' -s /run/nginx-unit.control.sock execute ... +``` + ### Edit current configuration in your favorite editor ``` $ unitctl edit @@ -168,6 +197,8 @@ $ unitctl edit } ``` +*Note:* This command does not support operating on multiple instances of Unit at once. + ### Import configuration, certificates, and NJS modules from directory ``` $ unitctl import /opt/unit/config @@ -191,6 +222,8 @@ $ unitctl export -f - > config.tar *Note:* The exported configuration omits certificates. +*Note:* This command does not support operating on multiple instances of Unit at once. + ### Wait for socket to become available ``` $ unitctl --wait-timeout-seconds=3 --wait-max-tries=4 import /opt/unit/config` diff --git a/tools/unitctl/unitctl/src/cmd/applications.rs b/tools/unitctl/unitctl/src/cmd/applications.rs index f4c44105..41af679e 100644 --- a/tools/unitctl/unitctl/src/cmd/applications.rs +++ b/tools/unitctl/unitctl/src/cmd/applications.rs @@ -1,36 +1,46 @@ use crate::unitctl::{ApplicationArgs, ApplicationCommands, UnitCtl}; -use crate::{wait, UnitctlError}; +use crate::{wait, UnitctlError, eprint_error}; use crate::requests::send_empty_body_deserialize_response; use unit_client_rs::unit_client::UnitClient; pub(crate) async fn cmd(cli: &UnitCtl, args: &ApplicationArgs) -> Result<(), UnitctlError> { - let control_socket = wait::wait_for_socket(cli).await?; - let client = UnitClient::new(control_socket); + let clients: Vec = wait::wait_for_sockets(cli) + .await? + .into_iter() + .map(|sock| UnitClient::new(sock)) + .collect(); - match &args.command { - ApplicationCommands::Reload { ref name } => client - .restart_application(name) - .await - .map_err(|e| UnitctlError::UnitClientError { source: *e }) - .and_then(|r| args.output_format.write_to_stdout(&r)), + for client in clients { + let _ = match &args.command { + ApplicationCommands::Reload { ref name } => client + .restart_application(name) + .await + .map_err(|e| UnitctlError::UnitClientError { source: *e }) + .and_then(|r| args.output_format.write_to_stdout(&r)), - /* we should be able to use this but the openapi generator library - * is fundamentally incorrect and provides a broken API for the - * applications endpoint. - ApplicationCommands::List {} => client - .applications() - .await - .map_err(|e| UnitctlError::UnitClientError { source: *e }) - .and_then(|response| args.output_format.write_to_stdout(&response)),*/ + /* we should be able to use this but the openapi generator library + * is fundamentally incorrect and provides a broken API for the + * applications endpoint. + ApplicationCommands::List {} => client + .applications() + .await + .map_err(|e| UnitctlError::UnitClientError { source: *e }) + .and_then(|response| args.output_format.write_to_stdout(&response)),*/ - ApplicationCommands::List {} => { - args.output_format.write_to_stdout( - &send_empty_body_deserialize_response( - &client, - "GET", - "/config/applications", - ).await? - ) - }, + ApplicationCommands::List {} => { + args.output_format.write_to_stdout( + &send_empty_body_deserialize_response( + &client, + "GET", + "/config/applications", + ).await? + ) + }, + }.map_err(|error| { + eprint_error(&error); + std::process::exit(error.exit_code()); + }); } + + Ok(()) } diff --git a/tools/unitctl/unitctl/src/cmd/edit.rs b/tools/unitctl/unitctl/src/cmd/edit.rs index 21bba519..34c1e7a3 100644 --- a/tools/unitctl/unitctl/src/cmd/edit.rs +++ b/tools/unitctl/unitctl/src/cmd/edit.rs @@ -1,6 +1,7 @@ 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::unitctl_error::ControlSocketErrorKind; use crate::{wait, OutputFormat, UnitctlError}; use std::path::{Path, PathBuf}; use unit_client_rs::unit_client::UnitClient; @@ -19,8 +20,16 @@ const EDITOR_KNOWN_LIST: [&str; 8] = [ ]; pub(crate) async fn cmd(cli: &UnitCtl, output_format: OutputFormat) -> Result<(), UnitctlError> { - let control_socket = wait::wait_for_socket(cli).await?; - let client = UnitClient::new(control_socket); + if cli.control_socket_addresses.is_some() && + cli.control_socket_addresses.clone().unwrap().len() > 1 { + return Err(UnitctlError::ControlSocketError{ + kind: ControlSocketErrorKind::General, + message: "too many control sockets. specify at most one.".to_string(), + }); + } + + let mut control_sockets = wait::wait_for_sockets(cli).await?; + let client = UnitClient::new(control_sockets.pop().unwrap()); // Get latest configuration let current_config = send_empty_body_deserialize_response(&client, "GET", "/config").await?; diff --git a/tools/unitctl/unitctl/src/cmd/execute.rs b/tools/unitctl/unitctl/src/cmd/execute.rs index 1bde437d..85aea404 100644 --- a/tools/unitctl/unitctl/src/cmd/execute.rs +++ b/tools/unitctl/unitctl/src/cmd/execute.rs @@ -5,7 +5,7 @@ use crate::requests::{ }; use crate::unitctl::UnitCtl; use crate::wait; -use crate::{OutputFormat, UnitctlError}; +use crate::{OutputFormat, UnitctlError, eprint_error}; use unit_client_rs::unit_client::UnitClient; pub(crate) async fn cmd( @@ -15,8 +15,11 @@ pub(crate) async fn cmd( method: &str, path: &str, ) -> Result<(), UnitctlError> { - let control_socket = wait::wait_for_socket(cli).await?; - let client = UnitClient::new(control_socket); + let clients: Vec<_> = wait::wait_for_sockets(cli) + .await? + .into_iter() + .map(|sock| UnitClient::new(sock)) + .collect(); let path_trimmed = path.trim(); let method_upper = method.to_uppercase(); @@ -28,7 +31,21 @@ pub(crate) async fn cmd( eprintln!("Cannot use GET method with input file - ignoring input file"); } - send_and_deserialize(client, method_upper, input_file_arg, path_trimmed, output_format).await + for client in clients { + let _ = send_and_deserialize( + client, + method_upper.clone(), + input_file_arg.clone(), + path_trimmed, + output_format + ).await + .map_err(|e| { + eprint_error(&e); + std::process::exit(e.exit_code()); + }); + } + + Ok(()) } async fn send_and_deserialize( diff --git a/tools/unitctl/unitctl/src/cmd/import.rs b/tools/unitctl/unitctl/src/cmd/import.rs index 81f925bc..956832f3 100644 --- a/tools/unitctl/unitctl/src/cmd/import.rs +++ b/tools/unitctl/unitctl/src/cmd/import.rs @@ -50,8 +50,12 @@ pub async fn cmd(cli: &UnitCtl, directory: &PathBuf) -> Result<(), UnitctlError> }); } - let control_socket = wait::wait_for_socket(cli).await?; - let client = UnitClient::new(control_socket); + let clients: Vec<_> = wait::wait_for_sockets(cli) + .await? + .into_iter() + .map(|sock| UnitClient::new(sock)) + .collect(); + let mut results = vec![]; for i in WalkDir::new(directory) .follow_links(true) @@ -60,7 +64,9 @@ pub async fn cmd(cli: &UnitCtl, directory: &PathBuf) -> Result<(), UnitctlError> .filter_map(Result::ok) .filter(|e| !e.path().is_dir()) { - results.push(process_entry(i, &client).await); + for client in &clients { + results.push(process_entry(i.clone(), client).await); + } } if results.iter().filter(|r| r.is_err()).count() == results.len() { diff --git a/tools/unitctl/unitctl/src/cmd/listeners.rs b/tools/unitctl/unitctl/src/cmd/listeners.rs index 4eb48355..05fbec07 100644 --- a/tools/unitctl/unitctl/src/cmd/listeners.rs +++ b/tools/unitctl/unitctl/src/cmd/listeners.rs @@ -1,14 +1,23 @@ use crate::unitctl::UnitCtl; use crate::wait; -use crate::{OutputFormat, UnitctlError}; +use crate::{OutputFormat, UnitctlError, eprint_error}; use unit_client_rs::unit_client::UnitClient; pub async fn cmd(cli: &UnitCtl, output_format: OutputFormat) -> Result<(), UnitctlError> { - let control_socket = wait::wait_for_socket(cli).await?; - let client = UnitClient::new(control_socket); - client - .listeners() - .await - .map_err(|e| UnitctlError::UnitClientError { source: *e }) - .and_then(|response| output_format.write_to_stdout(&response)) + let socks = wait::wait_for_sockets(cli) + .await?; + let clients = socks.iter() + .map(|sock| UnitClient::new(sock.clone())); + + for client in clients { + let _ = client.listeners() + .await + .map_err(|e| { + let err = UnitctlError::UnitClientError { source: *e }; + eprint_error(&err); + std::process::exit(err.exit_code()); + }) + .and_then(|response| output_format.write_to_stdout(&response)); + } + Ok(()) } diff --git a/tools/unitctl/unitctl/src/cmd/save.rs b/tools/unitctl/unitctl/src/cmd/save.rs index bce8fdb9..d93ce221 100644 --- a/tools/unitctl/unitctl/src/cmd/save.rs +++ b/tools/unitctl/unitctl/src/cmd/save.rs @@ -2,6 +2,7 @@ use crate::unitctl::UnitCtl; use crate::wait; use crate::UnitctlError; use crate::requests::send_empty_body_deserialize_response; +use crate::unitctl_error::ControlSocketErrorKind; use unit_client_rs::unit_client::UnitClient; use tar::{Builder, Header}; use std::fs::File; @@ -12,13 +13,21 @@ pub async fn cmd( cli: &UnitCtl, filename: &String ) -> Result<(), UnitctlError> { + if cli.control_socket_addresses.is_some() && + cli.control_socket_addresses.clone().unwrap().len() > 1 { + return Err(UnitctlError::ControlSocketError{ + kind: ControlSocketErrorKind::General, + message: "too many control sockets. specify at most one.".to_string(), + }); + } + + let mut control_sockets = wait::wait_for_sockets(cli).await?; + let client = UnitClient::new(control_sockets.pop().unwrap()); + if !filename.ends_with(".tar") { eprintln!("Warning: writing uncompressed tarball to {}", filename); } - let control_socket = wait::wait_for_socket(cli).await?; - let client = UnitClient::new(control_socket); - let config_res = serde_json::to_string_pretty( &send_empty_body_deserialize_response(&client, "GET", "/config").await? ); diff --git a/tools/unitctl/unitctl/src/cmd/status.rs b/tools/unitctl/unitctl/src/cmd/status.rs index 2cac5714..6d5eb00a 100644 --- a/tools/unitctl/unitctl/src/cmd/status.rs +++ b/tools/unitctl/unitctl/src/cmd/status.rs @@ -1,14 +1,23 @@ use crate::unitctl::UnitCtl; use crate::wait; -use crate::{OutputFormat, UnitctlError}; +use crate::{OutputFormat, UnitctlError, eprint_error}; use unit_client_rs::unit_client::UnitClient; pub async fn cmd(cli: &UnitCtl, output_format: OutputFormat) -> Result<(), UnitctlError> { - let control_socket = wait::wait_for_socket(cli).await?; - let client = UnitClient::new(control_socket); - client - .status() - .await - .map_err(|e| UnitctlError::UnitClientError { source: *e }) - .and_then(|response| output_format.write_to_stdout(&response)) + let socks = wait::wait_for_sockets(cli) + .await?; + let clients = socks.iter() + .map(|sock| UnitClient::new(sock.clone())); + + for client in clients { + let _ = client.status() + .await + .map_err(|e| { + let err = UnitctlError::UnitClientError { source: *e }; + eprint_error(&err); + std::process::exit(err.exit_code()); + }) + .and_then(|response| output_format.write_to_stdout(&response)); + } + Ok(()) } diff --git a/tools/unitctl/unitctl/src/main.rs b/tools/unitctl/unitctl/src/main.rs index 8f33fc16..822b2ae7 100644 --- a/tools/unitctl/unitctl/src/main.rs +++ b/tools/unitctl/unitctl/src/main.rs @@ -15,8 +15,8 @@ use crate::cmd::{ }; use crate::output_format::OutputFormat; use crate::unitctl::{Commands, UnitCtl}; -use crate::unitctl_error::UnitctlError; -use unit_client_rs::unit_client::{UnitClient, UnitClientError, UnitSerializableMap}; +use crate::unitctl_error::{UnitctlError, eprint_error}; +use unit_client_rs::unit_client::{UnitClient, UnitSerializableMap}; mod cmd; mod inputfile; @@ -58,56 +58,3 @@ async fn main() -> Result<(), UnitctlError> { 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"); - } - UnitClientError::OpenAPIError { source } => { - eprintln!("OpenAPI Error: {}", source); - } - _ => { - 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/unitctl.rs b/tools/unitctl/unitctl/src/unitctl.rs index e567116b..a36f006c 100644 --- a/tools/unitctl/unitctl/src/unitctl.rs +++ b/tools/unitctl/unitctl/src/unitctl.rs @@ -16,7 +16,7 @@ pub(crate) struct UnitCtl { 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, + pub(crate) control_socket_addresses: Option>, #[arg( required = false, default_missing_value = "1", diff --git a/tools/unitctl/unitctl/src/unitctl_error.rs b/tools/unitctl/unitctl/src/unitctl_error.rs index 1cf4fe48..83b2da46 100644 --- a/tools/unitctl/unitctl/src/unitctl_error.rs +++ b/tools/unitctl/unitctl/src/unitctl_error.rs @@ -70,3 +70,56 @@ impl Termination for UnitctlError { ExitCode::from(self.exit_code() as u8) } } + +pub 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"); + } + UnitClientError::OpenAPIError { source } => { + eprintln!("OpenAPI Error: {}", source); + } + _ => { + 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/wait.rs b/tools/unitctl/unitctl/src/wait.rs index 313403a8..860fb0b5 100644 --- a/tools/unitctl/unitctl/src/wait.rs +++ b/tools/unitctl/unitctl/src/wait.rs @@ -8,105 +8,83 @@ 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 async fn wait_for_socket(cli: &UnitCtl) -> Result { - // 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().await.and_validate(); +pub async fn wait_for_sockets(cli: &UnitCtl) -> Result, UnitctlError> { + let socks: Vec; + match &cli.control_socket_addresses { + None => { + socks = vec![find_socket_address_from_instance().await?]; + }, + Some(s) => socks = s.clone(), } - 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); + let mut mapped = vec![]; + for addr in socks { + if cli.wait_time_seconds.is_none() { + mapped.push(addr.to_owned().validate()?); + continue; } - attempt += 1; - - let result = cli.control_socket_address.instance_value_if_none().await.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 = 0; + 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); + } - if let Err(error) = result { - if error.retryable() { - continue; + attempt += 1; + + let res = addr.to_owned().validate(); + if res.is_err() { + let err = res.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), + }, + }); + if err.as_ref().is_err_and(|e| e.retryable()) { + continue; + } else { + return Err(err.expect_err("impossible error condition")); + } } else { - return Err(error); + let sock = res.unwrap(); + if let Err(e) = UnitClient::new(sock.clone()).status().await { + eprintln!("Unable to access status endpoint: {}", *e); + continue; + } + mapped.push(sock); } } - control_socket = result.unwrap(); - let client = UnitClient::new(control_socket.clone()); - - match client.status().await { - Ok(_) => { - return Ok(control_socket.to_owned()); - } - Err(error) => { - eprintln!("Unable to access status endpoint: {}", *error); - continue; - } + if attempt >= max_tries { + return Err(UnitctlError::WaitTimeoutError); } } - if attempt >= max_tries { - Err(UnitctlError::WaitTimeoutError) - } else { - panic!("Unexpected state - this should never happen"); - } -} - -trait OptionControlSocket { - async fn instance_value_if_none(&self) -> Result; -} - -impl OptionControlSocket for Option { - async fn instance_value_if_none(&self) -> Result { - if let Some(control_socket) = self { - Ok(control_socket.to_owned()) - } else { - find_socket_address_from_instance().await - } - } -} - -trait ResultControlSocket { - fn and_validate(self) -> Result; -} - -impl ResultControlSocket for Result { - fn and_validate(self) -> Result { - 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), - }, - }) - }) - } + return Ok(mapped); } async fn find_socket_address_from_instance() -> Result { @@ -114,7 +92,7 @@ async fn find_socket_address_from_instance() -> Result 1 { - let suggestion: String = "Multiple unit instances found. Specify the socket address to the instance you wish \ + let suggestion: String = "Multiple unit instances found. Specify the socket address(es) to the instance you wish \ to control using the `--control-socket-address` flag" .to_string(); return Err(UnitctlError::MultipleUnitInstancesError { suggestion }); @@ -131,14 +109,14 @@ async fn find_socket_address_from_instance() -> Result Date: Mon, 22 Jul 2024 18:14:05 +0000 Subject: build(deps): bump openssl from 0.10.64 to 0.10.66 in /tools/unitctl Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.64 to 0.10.66. - [Release notes](https://github.com/sfackler/rust-openssl/releases) - [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.64...openssl-v0.10.66) --- updated-dependencies: - dependency-name: openssl dependency-type: indirect ... Signed-off-by: dependabot[bot] --- tools/unitctl/Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'tools/unitctl') diff --git a/tools/unitctl/Cargo.lock b/tools/unitctl/Cargo.lock index 20279963..bcbe53b7 100644 --- a/tools/unitctl/Cargo.lock +++ b/tools/unitctl/Cargo.lock @@ -1185,9 +1185,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl" -version = "0.10.64" +version = "0.10.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" dependencies = [ "bitflags 2.4.1", "cfg-if", @@ -1217,9 +1217,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.102" +version = "0.9.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" dependencies = [ "cc", "libc", -- cgit From b892e99458ec964c3af7cf8b5f973f98ba06cd4b Mon Sep 17 00:00:00 2001 From: Ava Hahn Date: Mon, 15 Jul 2024 13:59:48 -0700 Subject: tools/unitctl: update readme Signed-off-by: Ava Hahn --- tools/unitctl/README.md | 94 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 84 insertions(+), 10 deletions(-) (limited to 'tools/unitctl') diff --git a/tools/unitctl/README.md b/tools/unitctl/README.md index 4aa6068c..977ee1a8 100644 --- a/tools/unitctl/README.md +++ b/tools/unitctl/README.md @@ -51,11 +51,44 @@ desired. ## Features (Current) +``` +CLI interface to the NGINX UNIT Control API + +Usage: unitctl [OPTIONS] + +Commands: + instances List all running UNIT processes + edit Open current UNIT configuration in editor + import Import configuration from a directory + execute Sends raw JSON payload to UNIT + status Get the current status of UNIT + listeners List active listeners + help Print this message or the help of the given subcommand(s) + +Options: + -s, --control-socket-address + Path (unix:/var/run/unit/control.sock), tcp address with port (127.0.0.1:80), or URL + -w, --wait-timeout-seconds + Number of seconds to wait for control socket to become available + -t, --wait-max-tries + Number of times to try to access control socket when waiting [default: 3] + -h, --help + Print help + -V, --version + Print version +``` + - Consumes alternative configuration formats Like YAML and converts them +- Can convert output to multiple different formats. - 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 will detect and connect to running process of Unit on the host. +It will pull information about the running Unit configuration +(including how to access its control API) from the process information of +each detected Unit process. + ``` $ unitctl instances No socket path provided - attempting to detect from running instance @@ -68,6 +101,11 @@ unitd instance [pid: 79489, version: 1.32.0]: ``` ### Start a new Unit process via docker +Unitctl can launch new containers of Unit. +These can be official Unit images or custom Unit images. +The new containers will then be shown in a call to +`unitctl instances` + ``` $ unitctl instances new /tmp/2 $(pwd) 'unit:wasm' Pulling and starting a container from unit:wasm @@ -77,21 +115,32 @@ Note: Container will be on host network ``` -To the subcommand `unitctl instances new` the user must provide three things: -1. **A directory such as `/tmp/2`.** - The Unit container will mount this to `/var/run` internally. - Thus, the control socket and pid file will be accessible from the host. -2. **A path to an application.** +To the subcommand `unitctl instances new` the user must provide three arguments: +1. **A means of showing the control API:** + There are two possibilities for this argument. + A filepath on which to open a unix socket, + or a TCP address. + - If a directory is specified the Unit container + will mount this to `/var/run` internally. + Thus, the control socket and pid file will be + accessible from the host. For example: `/tmp/2`. + - If a TCP endpoint is specified Unit will be configured + to offer its control API on the given port and address. + For example: `127.0.0.1:7171`. +2. **A path to an application:** In the example, `$(pwd)` is provided. The Unit container will mount this READ ONLY to `/www/`. This will allow the user to configure their Unit container to expose an application stored on the host. -3. **An image tag.** +3. **An image tag:** In the example, `unit:wasm` is used. This will be the image that unitctl will deploy. Custom repos and images can be deployed in this manner. After deployment the user will have one Unit container running on the host network. ### Lists active applications and provides means to restart them +Unitctl can list running applications by accessing the specified control API. +Unitctl can also request from the API that an application be restarted. + Listing applications: ``` $ unitctl app list @@ -120,6 +169,9 @@ $ unitctl -s '127.0.0.1:8001' -s /run/nginx-unit.control.sock app list ``` ### Lists active listeners from running Unit processes +Unitctl can query a given control API to fetch all configured +listeners. + ``` unitctl listeners No socket path provided - attempting to detect from running instance @@ -138,6 +190,9 @@ $ unitctl -s '127.0.0.1:8001' -s /run/nginx-unit.control.sock listeners ``` ### Get the current status of NGINX Unit processes +Unitctl can query the control API to provide the status of the running +Unit daemon. + ``` $ unitctl status -t yaml No socket path provided - attempting to detect from running instance @@ -159,6 +214,10 @@ $ unitctl -s '127.0.0.1:8001' -s /run/nginx-unit.control.sock status ``` ### Send arbitrary configuration payloads to Unit +Unitctl can accept custom request payloads and query given API endpoints with them. +The request payload must be passed in using the `-f` flag either as a filename or +using the `-` filename to denote the use of stdin as shown in the example below. + ``` $ echo '{ "listeners": { @@ -188,6 +247,12 @@ $ unitctl -s '127.0.0.1:8001' -s /run/nginx-unit.control.sock execute ... ``` ### Edit current configuration in your favorite editor +Unitctl can fetch the configuration from a running instance of Unit and +load it in any number of preconfigured editors on your command line. + +Unitctl will try to use whatever editor is configured with the `EDITOR` +environment variable, but will default to vim, emacs, nano, vi, or pico. + ``` $ unitctl edit [[EDITOR LOADS SHOWING CURRENT CONFIGURATION - USER EDITS AND SAVES]] @@ -200,6 +265,10 @@ $ unitctl edit *Note:* This command does not support operating on multiple instances of Unit at once. ### Import configuration, certificates, and NJS modules from directory +Unitctl will parse existing configuration, certificates, and NJS modules +stored in a directory and convert them into a payload to reconfigure a +given Unit daemon. + ``` $ unitctl import /opt/unit/config Imported /opt/unit/config/certificates/snake.pem -> /certificates/snake.pem @@ -209,12 +278,15 @@ Imported 3 files ``` ### Export configuration from a running Unit instance -``` -$ unitctl export -f config.tar -``` +Unitctl will query a control API to fetch running configuration +and NJS modules from a Unit process. Due to a technical limitation +this output will not contain currently stored certificate bundles. +The output is saved as a tarball at the filename given with the `-f` +argument. Standard out may be used with `-f -` as shown in the +following examples. -Addtionally, standard out can be used: ``` +$ unitctl export -f config.tar $ unitctl export -f - $ unitctl export -f - | tar xf - config.json $ unitctl export -f - > config.tar @@ -225,6 +297,8 @@ $ unitctl export -f - > config.tar *Note:* This command does not support operating on multiple instances of Unit at once. ### Wait for socket to become available +All commands support waiting on unix sockets for availability. + ``` $ unitctl --wait-timeout-seconds=3 --wait-max-tries=4 import /opt/unit/config` Waiting for 3s control socket to be available try 2/4... -- cgit From 1b4843036d6aa10fea6c23ed455c30f4cc9d873d Mon Sep 17 00:00:00 2001 From: Ava Hahn <110854134+avahahn@users.noreply.github.com> Date: Tue, 16 Jul 2024 14:07:57 -0700 Subject: tools/unitctl: update readme Signed-off-by: Ava Hahn --- tools/unitctl/README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) (limited to 'tools/unitctl') diff --git a/tools/unitctl/README.md b/tools/unitctl/README.md index 977ee1a8..e6fca477 100644 --- a/tools/unitctl/README.md +++ b/tools/unitctl/README.md @@ -52,16 +52,16 @@ desired. ## Features (Current) ``` -CLI interface to the NGINX UNIT Control API +CLI interface to the NGINX Unit Control API Usage: unitctl [OPTIONS] Commands: - instances List all running UNIT processes - edit Open current UNIT configuration in editor + instances List all running Unit processes + edit Open current Unit configuration in editor import Import configuration from a directory - execute Sends raw JSON payload to UNIT - status Get the current status of UNIT + execute Sends raw JSON payload to Unit + status Get the current status of Unit listeners List active listeners help Print this message or the help of the given subcommand(s) @@ -79,7 +79,7 @@ Options: ``` - Consumes alternative configuration formats Like YAML and converts them -- Can convert output to multiple different formats. +- Can convert output to multiple different formats (YAML, plain JSON, highlighted JSON) - Syntactic highlighting of JSON output - Interpretation of Unit errors with (arguably more) useful error messages @@ -103,6 +103,8 @@ unitd instance [pid: 79489, version: 1.32.0]: ### Start a new Unit process via docker Unitctl can launch new containers of Unit. These can be official Unit images or custom Unit images. +Any container that calls `unitd` in a CMD declaration will suffice. + The new containers will then be shown in a call to `unitctl instances` -- cgit From e743b6cef5d86e340a5538286baf733a21204e63 Mon Sep 17 00:00:00 2001 From: Gabor Javorszky Date: Thu, 1 Aug 2024 10:15:57 +0100 Subject: tools/unitctl: remove (default) from option text --- tools/unitctl/unitctl/src/unitctl.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'tools/unitctl') diff --git a/tools/unitctl/unitctl/src/unitctl.rs b/tools/unitctl/unitctl/src/unitctl.rs index a36f006c..50bd1eb6 100644 --- a/tools/unitctl/unitctl/src/unitctl.rs +++ b/tools/unitctl/unitctl/src/unitctl.rs @@ -51,7 +51,7 @@ pub(crate) enum Commands { short = 't', long = "output-format", default_value = "json-pretty", - help = "Output format: yaml, json, json-pretty (default)" + help = "Output format: yaml, json, json-pretty" )] output_format: OutputFormat, }, @@ -68,7 +68,7 @@ pub(crate) enum Commands { short = 't', long = "output-format", default_value = "json-pretty", - help = "Output format: yaml, json, json-pretty (default)" + help = "Output format: yaml, json, json-pretty" )] output_format: OutputFormat, #[arg( @@ -98,7 +98,7 @@ pub(crate) enum Commands { short = 't', long = "output-format", default_value = "json-pretty", - help = "Output format: yaml, json, json-pretty (default)" + help = "Output format: yaml, json, json-pretty" )] output_format: OutputFormat, }, @@ -110,7 +110,7 @@ pub(crate) enum Commands { short = 't', long = "output-format", default_value = "json-pretty", - help = "Output format: yaml, json, json-pretty (default)" + help = "Output format: yaml, json, json-pretty" )] output_format: OutputFormat, }, @@ -136,7 +136,7 @@ pub struct InstanceArgs { short = 't', long = "output-format", default_value = "text", - help = "Output format: text, yaml, json, json-pretty (default)" + help = "Output format: text, yaml, json, json-pretty" )] pub output_format: OutputFormat, @@ -171,7 +171,7 @@ pub struct ApplicationArgs { short = 't', long = "output-format", default_value = "text", - help = "Output format: text, yaml, json, json-pretty (default)" + help = "Output format: text, yaml, json, json-pretty" )] pub output_format: OutputFormat, -- cgit From 2a2437409a5649d2d005d1b8821591ad94a725fd Mon Sep 17 00:00:00 2001 From: Gabor Javorszky Date: Thu, 1 Aug 2024 10:17:54 +0100 Subject: tools/unitctl: make json-pretty default output fmt --- tools/unitctl/unitctl/src/unitctl.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'tools/unitctl') diff --git a/tools/unitctl/unitctl/src/unitctl.rs b/tools/unitctl/unitctl/src/unitctl.rs index 50bd1eb6..0d815fc1 100644 --- a/tools/unitctl/unitctl/src/unitctl.rs +++ b/tools/unitctl/unitctl/src/unitctl.rs @@ -135,7 +135,7 @@ pub struct InstanceArgs { global = true, short = 't', long = "output-format", - default_value = "text", + default_value = "json-pretty", help = "Output format: text, yaml, json, json-pretty" )] pub output_format: OutputFormat, @@ -170,7 +170,7 @@ pub struct ApplicationArgs { global = true, short = 't', long = "output-format", - default_value = "text", + default_value = "json-pretty", help = "Output format: text, yaml, json, json-pretty" )] pub output_format: OutputFormat, -- cgit From 43faf99d0347c13ddffecb4aaaf76d5771116d53 Mon Sep 17 00:00:00 2001 From: Gabor Javorszky Date: Thu, 1 Aug 2024 10:31:38 +0100 Subject: tools/unitctl: reword freeform message for output --- tools/unitctl/unitctl/src/unitctl.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'tools/unitctl') diff --git a/tools/unitctl/unitctl/src/unitctl.rs b/tools/unitctl/unitctl/src/unitctl.rs index 0d815fc1..1421669f 100644 --- a/tools/unitctl/unitctl/src/unitctl.rs +++ b/tools/unitctl/unitctl/src/unitctl.rs @@ -51,7 +51,7 @@ pub(crate) enum Commands { short = 't', long = "output-format", default_value = "json-pretty", - help = "Output format: yaml, json, json-pretty" + help = "Output format of the result" )] output_format: OutputFormat, }, @@ -68,7 +68,7 @@ pub(crate) enum Commands { short = 't', long = "output-format", default_value = "json-pretty", - help = "Output format: yaml, json, json-pretty" + help = "Output format of the result" )] output_format: OutputFormat, #[arg( @@ -98,7 +98,7 @@ pub(crate) enum Commands { short = 't', long = "output-format", default_value = "json-pretty", - help = "Output format: yaml, json, json-pretty" + help = "Output format of the result" )] output_format: OutputFormat, }, @@ -110,7 +110,7 @@ pub(crate) enum Commands { short = 't', long = "output-format", default_value = "json-pretty", - help = "Output format: yaml, json, json-pretty" + help = "Output format of the result" )] output_format: OutputFormat, }, @@ -136,7 +136,7 @@ pub struct InstanceArgs { short = 't', long = "output-format", default_value = "json-pretty", - help = "Output format: text, yaml, json, json-pretty" + help = "Output format of the result" )] pub output_format: OutputFormat, @@ -171,7 +171,7 @@ pub struct ApplicationArgs { short = 't', long = "output-format", default_value = "json-pretty", - help = "Output format: text, yaml, json, json-pretty" + help = "Output format of the result" )] pub output_format: OutputFormat, -- cgit From a91b961d620dc17c98b998a9142050defe46b56e Mon Sep 17 00:00:00 2001 From: Ava Hahn Date: Wed, 31 Jul 2024 13:39:37 -0700 Subject: tools/unitctl: make application directory configurable * default behavior is now a read write application mount * use can specify a flag (-r) to mount app dir as read only Signed-off-by: Ava Hahn --- tools/unitctl/README.md | 11 ++++++++--- tools/unitctl/unit-client-rs/src/unitd_docker.rs | 3 ++- tools/unitctl/unitctl/src/cmd/instances.rs | 10 ++++++++-- tools/unitctl/unitctl/src/unitctl.rs | 11 +++++------ 4 files changed, 23 insertions(+), 12 deletions(-) (limited to 'tools/unitctl') diff --git a/tools/unitctl/README.md b/tools/unitctl/README.md index e6fca477..9f7e010b 100644 --- a/tools/unitctl/README.md +++ b/tools/unitctl/README.md @@ -112,7 +112,7 @@ The new containers will then be shown in a call to $ unitctl instances new /tmp/2 $(pwd) 'unit:wasm' Pulling and starting a container from unit:wasm Will mount /tmp/2 to /var/run for socket access -Will READ ONLY mount /home/ava/repositories/nginx/unit/tools/unitctl to /www for application access +Will mount /home/user/repositories/nginx/unit/tools/unitctl to /www for application access Note: Container will be on host network ``` @@ -131,12 +131,17 @@ To the subcommand `unitctl instances new` the user must provide three arguments: For example: `127.0.0.1:7171`. 2. **A path to an application:** In the example, `$(pwd)` is provided. The Unit container will mount - this READ ONLY to `/www/`. This will allow the user to configure - their Unit container to expose an application stored on the host. + this to `/www/`. This will allow the user to configure their + Unit container to expose an application stored on the host. 3. **An image tag:** In the example, `unit:wasm` is used. This will be the image that unitctl will deploy. Custom repos and images can be deployed in this manner. +In addition to the above arguments, the user may add the `-r` flag. This flag will +set the Docker volume mount for the application directory to be read only. Do note +that this flag will break compatibility with WordPress, and other applications +which store state on the file system. + After deployment the user will have one Unit container running on the host network. ### Lists active applications and provides means to restart them diff --git a/tools/unitctl/unit-client-rs/src/unitd_docker.rs b/tools/unitctl/unit-client-rs/src/unitd_docker.rs index 0d318096..2b9e0c7d 100644 --- a/tools/unitctl/unit-client-rs/src/unitd_docker.rs +++ b/tools/unitctl/unit-client-rs/src/unitd_docker.rs @@ -249,6 +249,7 @@ impl UnitdContainer { pub async fn deploy_new_container( socket: ControlSocket, application: &String, + application_read_only: bool, image: &String, ) -> Result, UnitClientError> { match Docker::connect_with_local_defaults() { @@ -269,7 +270,7 @@ pub async fn deploy_new_container( typ: Some(MountTypeEnum::BIND), source: Some(application.clone()), target: Some("/www".to_string()), - read_only: Some(true), + read_only: Some(application_read_only), ..Default::default() }); diff --git a/tools/unitctl/unitctl/src/cmd/instances.rs b/tools/unitctl/unitctl/src/cmd/instances.rs index e532a151..92e09201 100644 --- a/tools/unitctl/unitctl/src/cmd/instances.rs +++ b/tools/unitctl/unitctl/src/cmd/instances.rs @@ -13,6 +13,7 @@ pub(crate) async fn cmd(args: InstanceArgs) -> Result<(), UnitctlError> { InstanceCommands::New { ref socket, ref application, + ref application_read_only, ref image, } => { // validation for application dir @@ -95,7 +96,12 @@ pub(crate) async fn cmd(args: InstanceArgs) -> Result<(), UnitctlError> { // reflect changes to user // print this to STDERR to avoid polluting deserialized data output eprintln!("> Pulling and starting a container from {}", image); - eprintln!("> Will READ ONLY mount {} to /www for application access", application); + eprintln!("> Will mount {} to /www for application access", application); + + if *application_read_only { + eprintln!("> Application mount will be read only"); + } + eprintln!("> Container will be on host network"); match addr.as_ref().unwrap() { ControlSocket::UnixLocalSocket(path) => eprintln!( @@ -113,7 +119,7 @@ pub(crate) async fn cmd(args: InstanceArgs) -> Result<(), UnitctlError> { } // do the actual deployment - deploy_new_container(addr.unwrap(), application, image) + deploy_new_container(addr.unwrap(), application, *application_read_only, image) .await .map_or_else( |e| Err(UnitctlError::UnitClientError { source: e }), diff --git a/tools/unitctl/unitctl/src/unitctl.rs b/tools/unitctl/unitctl/src/unitctl.rs index 1421669f..8db71b8f 100644 --- a/tools/unitctl/unitctl/src/unitctl.rs +++ b/tools/unitctl/unitctl/src/unitctl.rs @@ -119,12 +119,8 @@ pub(crate) enum Commands { #[command(about = "Export the current configuration of UNIT")] Export { - #[arg( - required = true, - short = 'f', - help = "tarball filename to save configuration to" - )] - filename: String + #[arg(required = true, short = 'f', help = "tarball filename to save configuration to")] + filename: String, }, } @@ -155,6 +151,9 @@ pub enum InstanceCommands { #[arg(required = true, help = "Path to mount application into container")] application: String, + #[arg(help = "Mount application directory as read only", short = 'r', long = "read-only")] + application_read_only: bool, + #[arg( help = "Unitd Image to deploy", default_value = env!("CARGO_PKG_VERSION"), -- cgit From 3c563849f13b7948d160a71ce8d480c4bb91b8f8 Mon Sep 17 00:00:00 2001 From: Andrew Clayton Date: Mon, 16 Sep 2024 14:54:38 +0100 Subject: unitctl: Don't track unit-openapi/.openapi-generator/ The two files under unit-openapi/.openapi-generator/, FILES and VERSIONS are auto-generated. Signed-off-by: Andrew Clayton --- tools/unitctl/unit-openapi/.gitignore | 1 + .../unitctl/unit-openapi/.openapi-generator/FILES | 155 --------------------- .../unit-openapi/.openapi-generator/VERSION | 1 - 3 files changed, 1 insertion(+), 156 deletions(-) delete mode 100644 tools/unitctl/unit-openapi/.openapi-generator/FILES delete mode 100644 tools/unitctl/unit-openapi/.openapi-generator/VERSION (limited to 'tools/unitctl') diff --git a/tools/unitctl/unit-openapi/.gitignore b/tools/unitctl/unit-openapi/.gitignore index 6aa10640..830fc6b7 100644 --- a/tools/unitctl/unit-openapi/.gitignore +++ b/tools/unitctl/unit-openapi/.gitignore @@ -1,3 +1,4 @@ +.openapi-generator/ /target/ **/*.rs.bk Cargo.lock diff --git a/tools/unitctl/unit-openapi/.openapi-generator/FILES b/tools/unitctl/unit-openapi/.openapi-generator/FILES deleted file mode 100644 index f487c081..00000000 --- a/tools/unitctl/unit-openapi/.openapi-generator/FILES +++ /dev/null @@ -1,155 +0,0 @@ -.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/ConfigApplicationJava.md -docs/ConfigApplicationPerl.md -docs/ConfigApplicationPhp.md -docs/ConfigApplicationPhpAllOfOptions.md -docs/ConfigApplicationPhpAllOfTargets.md -docs/ConfigApplicationPython.md -docs/ConfigApplicationPythonAllOfPath.md -docs/ConfigApplicationPythonAllOfTargets.md -docs/ConfigApplicationRuby.md -docs/ConfigApplicationWasi.md -docs/ConfigApplicationWasm.md -docs/ConfigApplicationWasmAllOfAccess.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_java.rs -src/models/config_application_perl.rs -src/models/config_application_php.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_path.rs -src/models/config_application_python_all_of_targets.rs -src/models/config_application_ruby.rs -src/models/config_application_wasi.rs -src/models/config_application_wasm.rs -src/models/config_application_wasm_all_of_access.rs -src/models/config_listener.rs -src/models/config_listener_forwarded.rs -src/models/config_listener_forwarded_source.rs -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 deleted file mode 100644 index 93c8ddab..00000000 --- a/tools/unitctl/unit-openapi/.openapi-generator/VERSION +++ /dev/null @@ -1 +0,0 @@ -7.6.0 -- cgit From 63148a31d52e325c912fcef7d53a39a325718c99 Mon Sep 17 00:00:00 2001 From: Gabor Javorszky Date: Mon, 16 Sep 2024 10:20:51 +0100 Subject: tools/unitctl: whitespace fixes Signed-off-by: Gabor Javorszky --- tools/unitctl/unitctl/src/unitctl.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) (limited to 'tools/unitctl') diff --git a/tools/unitctl/unitctl/src/unitctl.rs b/tools/unitctl/unitctl/src/unitctl.rs index 8db71b8f..322031cf 100644 --- a/tools/unitctl/unitctl/src/unitctl.rs +++ b/tools/unitctl/unitctl/src/unitctl.rs @@ -17,6 +17,7 @@ pub(crate) struct UnitCtl { help = "Path (unix:/var/run/unit/control.sock), tcp address with port (127.0.0.1:80), or URL" )] pub(crate) control_socket_addresses: Option>, + #[arg( required = false, default_missing_value = "1", @@ -26,6 +27,7 @@ pub(crate) struct UnitCtl { help = "Number of seconds to wait for control socket to become available" )] pub(crate) wait_time_seconds: Option, + #[arg( required = false, default_value = "3", @@ -35,6 +37,7 @@ pub(crate) struct UnitCtl { help = "Number of times to try to access control socket when waiting" )] pub(crate) wait_max_tries: Option, + #[command(subcommand)] pub(crate) command: Commands, } @@ -43,6 +46,7 @@ pub(crate) struct UnitCtl { pub(crate) enum Commands { #[command(about = "List all running Unit processes")] Instances(InstanceArgs), + #[command(about = "Open current Unit configuration in editor")] Edit { #[arg( @@ -55,11 +59,13 @@ pub(crate) enum Commands { )] 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( @@ -71,6 +77,7 @@ pub(crate) enum Commands { help = "Output format of the result" )] output_format: OutputFormat, + #[arg( required = false, global = true, @@ -79,17 +86,20 @@ pub(crate) enum Commands { help = "Input file (json, json5, cjson, hjson yaml, pem) to send to unit when applicable use - for stdin" )] input_file: Option, + #[arg( - help = "HTTP method to use (GET, POST, PUT, DELETE)", - required = true, - short = 'm', - long = "http-method", - value_parser = parse_http_method, + required = true, + short = 'm', + long = "http-method", + value_parser = parse_http_method, + help = "HTTP method to use (GET, POST, PUT, DELETE)", )] method: String, + #[arg(required = true, short = 'p', long = "path")] path: String, }, + #[command(about = "Get the current status of Unit")] Status { #[arg( @@ -102,6 +112,7 @@ pub(crate) enum Commands { )] output_format: OutputFormat, }, + #[command(about = "List active listeners")] Listeners { #[arg( @@ -114,6 +125,7 @@ pub(crate) enum Commands { )] output_format: OutputFormat, }, + #[command(about = "List all configured Unit applications")] App(ApplicationArgs), -- cgit From 5e8a68935f614100bbab7865e693b33856022a98 Mon Sep 17 00:00:00 2001 From: Gabor Javorszky Date: Mon, 16 Sep 2024 10:38:38 +0100 Subject: tools/unitctl: rename app -> apps, fix readme Signed-off-by: Gabor Javorszky --- tools/unitctl/README.md | 5 +++-- tools/unitctl/unitctl/src/main.rs | 2 +- tools/unitctl/unitctl/src/unitctl.rs | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) (limited to 'tools/unitctl') diff --git a/tools/unitctl/README.md b/tools/unitctl/README.md index 9f7e010b..66d8f50d 100644 --- a/tools/unitctl/README.md +++ b/tools/unitctl/README.md @@ -63,6 +63,7 @@ Commands: execute Sends raw JSON payload to Unit status Get the current status of Unit listeners List active listeners + apps List all configured Unit applications help Print this message or the help of the given subcommand(s) Options: @@ -150,7 +151,7 @@ Unitctl can also request from the API that an application be restarted. Listing applications: ``` -$ unitctl app list +$ unitctl apps list { "wasm": { "type": "wasm-wasi-component", @@ -161,7 +162,7 @@ $ unitctl app list Restarting an application: ``` -$ unitctl app reload wasm +$ unitctl apps reload wasm { "success": "Ok" } diff --git a/tools/unitctl/unitctl/src/main.rs b/tools/unitctl/unitctl/src/main.rs index 822b2ae7..dc3c09d1 100644 --- a/tools/unitctl/unitctl/src/main.rs +++ b/tools/unitctl/unitctl/src/main.rs @@ -34,7 +34,7 @@ async fn main() -> Result<(), UnitctlError> { match cli.command { Commands::Instances(args) => instances::cmd(args).await, - Commands::App(ref args) => applications::cmd(&cli, args).await, + Commands::Apps(ref args) => applications::cmd(&cli, args).await, Commands::Edit { output_format } => edit::cmd(&cli, output_format).await, diff --git a/tools/unitctl/unitctl/src/unitctl.rs b/tools/unitctl/unitctl/src/unitctl.rs index 322031cf..d01d0356 100644 --- a/tools/unitctl/unitctl/src/unitctl.rs +++ b/tools/unitctl/unitctl/src/unitctl.rs @@ -127,7 +127,7 @@ pub(crate) enum Commands { }, #[command(about = "List all configured Unit applications")] - App(ApplicationArgs), + Apps(ApplicationArgs), #[command(about = "Export the current configuration of UNIT")] Export { -- cgit From 0dcd3a91995d5ad0a9d27347bba22d1e8eae08b1 Mon Sep 17 00:00:00 2001 From: Gabor Javorszky Date: Mon, 16 Sep 2024 11:28:18 +0100 Subject: tools/unitctl: rename UNIT -> Unit The correct capitalisation of the name of the software is Unit, not all caps. Signed-off-by: Gabor Javorszky [ A bunch more s/UNIT/Unit/ - Andrew ] Signed-off-by: Andrew Clayton --- tools/unitctl/GNUmakefile | 4 ++-- tools/unitctl/man/unitctl.1 | 2 +- tools/unitctl/pkg/brew/unitctl.rb | 2 +- tools/unitctl/pkg/brew/unitctl.rb.template | 2 +- tools/unitctl/unit-client-rs/src/unit_client.rs | 4 ++-- tools/unitctl/unitctl/Cargo.toml | 6 +++--- tools/unitctl/unitctl/src/cmd/edit.rs | 8 ++++---- tools/unitctl/unitctl/src/unitctl.rs | 2 +- 8 files changed, 15 insertions(+), 15 deletions(-) (limited to 'tools/unitctl') diff --git a/tools/unitctl/GNUmakefile b/tools/unitctl/GNUmakefile index 9992a322..3ae8e34c 100644 --- a/tools/unitctl/GNUmakefile +++ b/tools/unitctl/GNUmakefile @@ -110,11 +110,11 @@ 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 +## 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" + 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 \ diff --git a/tools/unitctl/man/unitctl.1 b/tools/unitctl/man/unitctl.1 index 1bd725c6..0d775b6f 100644 --- a/tools/unitctl/man/unitctl.1 +++ b/tools/unitctl/man/unitctl.1 @@ -2,7 +2,7 @@ .\" .TH UNITCTL "1" "2022-12-29" "%%VERSION%%" "unitctl" .SH NAME -unitctl \- NGINX UNIT Control Utility +unitctl \- NGINX Unit Control Utility .SH SYNOPSIS unitctl [\fI\,FLAGS\/\fR] [\fI\,OPTIONS\/\fR] [\fI\,FILE\/\fR]... .SH DESCRIPTION diff --git a/tools/unitctl/pkg/brew/unitctl.rb b/tools/unitctl/pkg/brew/unitctl.rb index 771f2806..05d17d3f 100644 --- a/tools/unitctl/pkg/brew/unitctl.rb +++ b/tools/unitctl/pkg/brew/unitctl.rb @@ -1,5 +1,5 @@ class Unitctl < Formula - desc "CLI interface to the NGINX UNIT Control API" + desc "CLI interface to the NGINX Unit Control API" homepage "https://github.com/nginxinc/unit-rust-sdk" version "0.3.0" package_name = "unitctl" diff --git a/tools/unitctl/pkg/brew/unitctl.rb.template b/tools/unitctl/pkg/brew/unitctl.rb.template index db6991f6..f690abe2 100644 --- a/tools/unitctl/pkg/brew/unitctl.rb.template +++ b/tools/unitctl/pkg/brew/unitctl.rb.template @@ -1,5 +1,5 @@ class Unitctl < Formula - desc "CLI interface to the NGINX UNIT Control API" + desc "CLI interface to the NGINX Unit Control API" homepage "https://github.com/nginxinc/unit-rust-sdk" version "$VERSION" package_name = "$PACKAGE_NAME" diff --git a/tools/unitctl/unit-client-rs/src/unit_client.rs b/tools/unitctl/unit-client-rs/src/unit_client.rs index b3f07308..3d09e67a 100644 --- a/tools/unitctl/unit-client-rs/src/unit_client.rs +++ b/tools/unitctl/unit-client-rs/src/unit_client.rs @@ -21,7 +21,7 @@ use unit_openapi::apis::{ }; use unit_openapi::models::{ConfigApplication, ConfigListener, Status}; -const USER_AGENT: &str = concat!("UNIT CLI/", env!("CARGO_PKG_VERSION"), "/rust"); +const USER_AGENT: &str = concat!("Unit CLI/", env!("CARGO_PKG_VERSION"), "/rust"); custom_error! {pub UnitClientError OpenAPIError { source: OpenAPIError } = "OpenAPI error", @@ -201,7 +201,7 @@ impl UnitClient { } } - /// Sends a request to UNIT and deserializes the JSON response body into the value of type `RESPONSE`. + /// Sends a request to Unit and deserializes the JSON response body into the value of type `RESPONSE`. pub async fn send_request_and_deserialize_response serde::Deserialize<'de>>( &self, mut request: Request, diff --git a/tools/unitctl/unitctl/Cargo.toml b/tools/unitctl/unitctl/Cargo.toml index 8d83b424..80110d51 100644 --- a/tools/unitctl/unitctl/Cargo.toml +++ b/tools/unitctl/unitctl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "unitctl" -description = "CLI interface to the NGINX UNIT Control API" +description = "CLI interface to the NGINX Unit Control API" version = "0.4.0-beta" authors = ["Elijah Zupancic"] edition = "2021" @@ -38,7 +38,7 @@ tar = "0.4.41" copyright = "2022, F5" license-file = ["../LICENSE.txt", "0"] extended-description = """\ -A utility for controlling NGINX UNIT.""" +A utility for controlling NGINX Unit.""" section = "utility" priority = "optional" assets = [ @@ -48,7 +48,7 @@ assets = [ [package.metadata.generate-rpm] summary = """\ -A utility for controlling NGINX UNIT.""" +A utility for controlling NGINX Unit.""" section = "utility" priority = "optional" assets = [ diff --git a/tools/unitctl/unitctl/src/cmd/edit.rs b/tools/unitctl/unitctl/src/cmd/edit.rs index 34c1e7a3..6679d4a9 100644 --- a/tools/unitctl/unitctl/src/cmd/edit.rs +++ b/tools/unitctl/unitctl/src/cmd/edit.rs @@ -41,7 +41,7 @@ pub(crate) async fn cmd(cli: &UnitCtl, output_format: OutputFormat) -> Result<() .tempfile() .map_err(|e| UnitctlError::IoError { source: e })?; - // Pretty format JSON received from UNIT and write to the temporary file + // Pretty format JSON received from Unit and write to the temporary file serde_json::to_writer_pretty(temp_file.as_file_mut(), ¤t_config) .map_err(|e| UnitctlError::SerializationError { message: e.to_string() })?; @@ -53,15 +53,15 @@ pub(crate) async fn cmd(cli: &UnitCtl, output_format: OutputFormat) -> Result<() 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 + // 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"); + eprintln!("File was not modified - no changes will be sent to Unit"); return Ok(()); } }; - // Send edited file to UNIT to overwrite current configuration + // Send edited file to Unit to overwrite current configuration send_and_validate_config_deserialize_response(&client, "PUT", "/config", Some(&inputfile)) .await .and_then(|status| output_format.write_to_stdout(&status)) diff --git a/tools/unitctl/unitctl/src/unitctl.rs b/tools/unitctl/unitctl/src/unitctl.rs index d01d0356..a4c13648 100644 --- a/tools/unitctl/unitctl/src/unitctl.rs +++ b/tools/unitctl/unitctl/src/unitctl.rs @@ -129,7 +129,7 @@ pub(crate) enum Commands { #[command(about = "List all configured Unit applications")] Apps(ApplicationArgs), - #[command(about = "Export the current configuration of UNIT")] + #[command(about = "Export the current configuration of Unit")] Export { #[arg(required = true, short = 'f', help = "tarball filename to save configuration to")] filename: String, -- cgit From 9e5f961bbaf07b6af426a9dc3c3a4a965d67c152 Mon Sep 17 00:00:00 2001 From: Gabor Javorszky Date: Mon, 16 Sep 2024 11:29:29 +0100 Subject: tools/unitctl: add export subcommand to readme Signed-off-by: Gabor Javorszky Signed-off-by: Andrew Clayton --- tools/unitctl/README.md | 1 + 1 file changed, 1 insertion(+) (limited to 'tools/unitctl') diff --git a/tools/unitctl/README.md b/tools/unitctl/README.md index 66d8f50d..953956a3 100644 --- a/tools/unitctl/README.md +++ b/tools/unitctl/README.md @@ -64,6 +64,7 @@ Commands: status Get the current status of Unit listeners List active listeners apps List all configured Unit applications + export Export the current configuration of Unit help Print this message or the help of the given subcommand(s) Options: -- cgit From 7c48546ad38be961940a13a6021d8c61a47c594f Mon Sep 17 00:00:00 2001 From: Gabor Javorszky Date: Mon, 16 Sep 2024 11:30:08 +0100 Subject: tools/unitctl: adjust readme for socket addresses CONTROL_SOCKET_ADDRESS is singular, adds note that the flag can be specified multiple times, and adjusts code to print CONTROL_SOCKET_ADDRESS as singular. Signed-off-by: Gabor Javorszky Signed-off-by: Andrew Clayton --- tools/unitctl/README.md | 2 +- tools/unitctl/unitctl/src/unitctl.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'tools/unitctl') diff --git a/tools/unitctl/README.md b/tools/unitctl/README.md index 953956a3..1a5fcb9d 100644 --- a/tools/unitctl/README.md +++ b/tools/unitctl/README.md @@ -69,7 +69,7 @@ Commands: Options: -s, --control-socket-address - Path (unix:/var/run/unit/control.sock), tcp address with port (127.0.0.1:80), or URL + Path (unix:/var/run/unit/control.sock), tcp address with port (127.0.0.1:80), or URL. This flag can be specified multiple times. -w, --wait-timeout-seconds Number of seconds to wait for control socket to become available -t, --wait-max-tries diff --git a/tools/unitctl/unitctl/src/unitctl.rs b/tools/unitctl/unitctl/src/unitctl.rs index a4c13648..460c7e7b 100644 --- a/tools/unitctl/unitctl/src/unitctl.rs +++ b/tools/unitctl/unitctl/src/unitctl.rs @@ -14,7 +14,8 @@ pub(crate) struct UnitCtl { 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" + value_name = "CONTROL_SOCKET_ADDRESS", + help = "Path (unix:/var/run/unit/control.sock), tcp address with port (127.0.0.1:80), or URL. This flag can be specified multiple times." )] pub(crate) control_socket_addresses: Option>, -- cgit From 15f7650686a1c2688bb4d1219504e1fada3314c0 Mon Sep 17 00:00:00 2001 From: Ava Hahn Date: Mon, 16 Sep 2024 15:00:14 -0700 Subject: tools/unitctl: change reload to restart Signed-off-by: Ava Hahn Signed-off-by: Andrew Clayton --- tools/unitctl/README.md | 2 +- tools/unitctl/unitctl/src/cmd/applications.rs | 2 +- tools/unitctl/unitctl/src/unitctl.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'tools/unitctl') diff --git a/tools/unitctl/README.md b/tools/unitctl/README.md index 1a5fcb9d..bcd31006 100644 --- a/tools/unitctl/README.md +++ b/tools/unitctl/README.md @@ -163,7 +163,7 @@ $ unitctl apps list Restarting an application: ``` -$ unitctl apps reload wasm +$ unitctl apps restart wasm { "success": "Ok" } diff --git a/tools/unitctl/unitctl/src/cmd/applications.rs b/tools/unitctl/unitctl/src/cmd/applications.rs index 41af679e..b0145724 100644 --- a/tools/unitctl/unitctl/src/cmd/applications.rs +++ b/tools/unitctl/unitctl/src/cmd/applications.rs @@ -12,7 +12,7 @@ pub(crate) async fn cmd(cli: &UnitCtl, args: &ApplicationArgs) -> Result<(), Uni for client in clients { let _ = match &args.command { - ApplicationCommands::Reload { ref name } => client + ApplicationCommands::Restart { ref name } => client .restart_application(name) .await .map_err(|e| UnitctlError::UnitClientError { source: *e }) diff --git a/tools/unitctl/unitctl/src/unitctl.rs b/tools/unitctl/unitctl/src/unitctl.rs index 460c7e7b..43f2b777 100644 --- a/tools/unitctl/unitctl/src/unitctl.rs +++ b/tools/unitctl/unitctl/src/unitctl.rs @@ -194,8 +194,8 @@ pub struct ApplicationArgs { #[derive(Debug, Subcommand)] #[command(args_conflicts_with_subcommands = true)] pub enum ApplicationCommands { - #[command(about = "reload a running application")] - Reload { + #[command(about = "restart a running application")] + Restart { #[arg(required = true, help = "name of application")] name: String, }, -- cgit From 3144710fe3cb06112d0c0a79c0435310cafd29da Mon Sep 17 00:00:00 2001 From: Andrew Clayton Date: Mon, 16 Sep 2024 21:41:41 +0100 Subject: tools/unitctl: Update for version 1.33.0 Signed-off-by: Andrew Clayton --- tools/unitctl/Cargo.lock | 6 +++--- tools/unitctl/openapi-config.json | 4 ++-- tools/unitctl/unit-client-rs/Cargo.toml | 2 +- tools/unitctl/unit-openapi/Cargo.toml | 2 +- tools/unitctl/unit-openapi/README.md | 2 +- tools/unitctl/unitctl/Cargo.toml | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) (limited to 'tools/unitctl') diff --git a/tools/unitctl/Cargo.lock b/tools/unitctl/Cargo.lock index bcbe53b7..58f07b8b 100644 --- a/tools/unitctl/Cargo.lock +++ b/tools/unitctl/Cargo.lock @@ -2021,7 +2021,7 @@ dependencies = [ [[package]] name = "unit-client-rs" -version = "0.4.0-beta" +version = "1.33.0" dependencies = [ "bollard", "custom_error", @@ -2044,7 +2044,7 @@ dependencies = [ [[package]] name = "unit-openapi" -version = "0.4.0-beta" +version = "1.33.0" dependencies = [ "base64 0.21.5", "futures", @@ -2058,7 +2058,7 @@ dependencies = [ [[package]] name = "unitctl" -version = "0.4.0-beta" +version = "1.33.0" dependencies = [ "clap", "colored_json", diff --git a/tools/unitctl/openapi-config.json b/tools/unitctl/openapi-config.json index 783c8740..c47caadb 100644 --- a/tools/unitctl/openapi-config.json +++ b/tools/unitctl/openapi-config.json @@ -1,6 +1,6 @@ { "packageName": "unit-openapi", - "packageVersion": "0.4.0-beta", + "packageVersion": "1.33.0", "library": "hyper", "preferUnsignedInt": true -} \ No newline at end of file +} diff --git a/tools/unitctl/unit-client-rs/Cargo.toml b/tools/unitctl/unit-client-rs/Cargo.toml index b7b8b496..6d873417 100644 --- a/tools/unitctl/unit-client-rs/Cargo.toml +++ b/tools/unitctl/unit-client-rs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "unit-client-rs" -version = "0.4.0-beta" +version = "1.33.0" authors = ["Elijah Zupancic"] edition = "2021" license = "Apache-2.0" diff --git a/tools/unitctl/unit-openapi/Cargo.toml b/tools/unitctl/unit-openapi/Cargo.toml index 12435985..c7a177f9 100644 --- a/tools/unitctl/unit-openapi/Cargo.toml +++ b/tools/unitctl/unit-openapi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "unit-openapi" -version = "0.4.0-beta" +version = "1.33.0" 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" diff --git a/tools/unitctl/unit-openapi/README.md b/tools/unitctl/unit-openapi/README.md index 5bad3fa4..3a792b6e 100644 --- a/tools/unitctl/unit-openapi/README.md +++ b/tools/unitctl/unit-openapi/README.md @@ -21,7 +21,7 @@ For more information, please visit [https://unit.nginx.org/](https://unit.nginx. 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 +- Package version: 1.33.0 - Generator version: 7.6.0 - Build package: `org.openapitools.codegen.languages.RustClientCodegen` diff --git a/tools/unitctl/unitctl/Cargo.toml b/tools/unitctl/unitctl/Cargo.toml index 80110d51..ec89c975 100644 --- a/tools/unitctl/unitctl/Cargo.toml +++ b/tools/unitctl/unitctl/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "unitctl" description = "CLI interface to the NGINX Unit Control API" -version = "0.4.0-beta" +version = "1.33.0" authors = ["Elijah Zupancic"] edition = "2021" license = "Apache-2.0" -- cgit