tools: Add unitctl CLI
* Pull in entire unit-rust-sdk project * not included: CLA, COC, License * not included: duplicate openapi spec * not included: CI workflows * not included: changelog tooling * not included: commitsar tooling * not included: OpenAPI Web UI feature * update links in unitctl manpage * remove IDE configuration from .gitignore * rename Containerfile.debian to Dockerfile * simplify call to uname * keep Readmes and Makefiles to 80 character lines * outline specifically how to build unitctl for any desired target, and where to then find the binary for use * remove a section on the vision of the CLI which was superfluous given the state of completeness of the code and its use in unit * remove out of date feature proposals from readme * makefile: do not run when Rustup is not present * bump mio version to latest * generate openapi client library on demand * generate-openapi only runs when not present * generate-openapi now a dependency of binary build targets * deleted autogenerated code * reverted readme and Cargo document to autogenerated state * add additional build requirement to Readme Co-developed-by: Elijah Zupancic <e.zupancic@f5.com> Signed-off-by: Elijah Zupancic <e.zupancic@f5.com> Signed-off-by: Ava Hahn <a.hahn@f5.com> Reviewed-by: Andrew Clayton <a.clayton@nginx.com> # non rust stuff [ tools/cli => tools/unitctl and subject tweak - Andrew ] Signed-off-by: Andrew Clayton <a.clayton@nginx.com>
This commit is contained in:
parent
b26c119f4e
commit
db3cf3e42d
54 changed files with 7212 additions and 0 deletions
2
tools/unitctl/.cargo/config.toml
Normal file
2
tools/unitctl/.cargo/config.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
16
tools/unitctl/.gitignore
vendored
Normal file
16
tools/unitctl/.gitignore
vendored
Normal file
|
@ -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
|
1998
tools/unitctl/Cargo.lock
generated
Normal file
1998
tools/unitctl/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
8
tools/unitctl/Cargo.toml
Normal file
8
tools/unitctl/Cargo.toml
Normal file
|
@ -0,0 +1,8 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
|
||||
members = [
|
||||
"unit-openapi",
|
||||
"unit-client-rs",
|
||||
"unitctl"
|
||||
]
|
37
tools/unitctl/Dockerfile
Normal file
37
tools/unitctl/Dockerfile
Normal file
|
@ -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
|
145
tools/unitctl/GNUmakefile
Normal file
145
tools/unitctl/GNUmakefile
Normal file
|
@ -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
|
1
tools/unitctl/HomebrewFormula
Symbolic link
1
tools/unitctl/HomebrewFormula
Symbolic link
|
@ -0,0 +1 @@
|
|||
pkg/brew
|
134
tools/unitctl/README.md
Normal file
134
tools/unitctl/README.md
Normal file
|
@ -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
|
||||
```
|
67
tools/unitctl/build/container.mk
Normal file
67
tools/unitctl/build/container.mk
Normal file
|
@ -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
|
22
tools/unitctl/build/github.mk
Normal file
22
tools/unitctl/build/github.mk
Normal file
|
@ -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
|
77
tools/unitctl/build/openapi-generator-cli.sh
Executable file
77
tools/unitctl/build/openapi-generator-cli.sh
Executable file
|
@ -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} "$@"
|
139
tools/unitctl/build/package.mk
Normal file
139
tools/unitctl/build/package.mk
Normal file
|
@ -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)
|
57
tools/unitctl/build/release.mk
Normal file
57
tools/unitctl/build/release.mk
Normal file
|
@ -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
|
27
tools/unitctl/man/unitctl.1
Normal file
27
tools/unitctl/man/unitctl.1
Normal file
|
@ -0,0 +1,27 @@
|
|||
.\" Manpage for unitctl
|
||||
.\"
|
||||
.TH UNITCTL "1" "2022-12-29" "%%VERSION%%" "unitctl"
|
||||
.SH NAME
|
||||
unitctl \- NGINX UNIT Control Utility
|
||||
.SH SYNOPSIS
|
||||
unitctl [\fI\,FLAGS\/\fR] [\fI\,OPTIONS\/\fR] [\fI\,FILE\/\fR]...
|
||||
.SH DESCRIPTION
|
||||
WRITE ME
|
||||
.
|
||||
.SH "REPORTING BUGS"
|
||||
Report any issues on the project issue tracker at:
|
||||
.br
|
||||
\fB<https://github.com/nginx/unit>\fR
|
||||
.
|
||||
.SH ACKNOWLEDGEMENTS
|
||||
WRITE ME
|
||||
.
|
||||
.SH AUTHOR
|
||||
Elijah Zupancic \fB<e.zupancic@f5.com>\fR
|
||||
.
|
||||
.SH COPYRIGHT
|
||||
Copyright \(co 2022 F5. All Rights Reserved.
|
||||
.br
|
||||
License: Apache License 2.0 (Apache-2.0)
|
||||
.br
|
||||
Full License Text: <https://www.apache.org/licenses/LICENSE-2.0>
|
6
tools/unitctl/openapi-config.json
Normal file
6
tools/unitctl/openapi-config.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"packageName": "unit-openapi",
|
||||
"packageVersion": "0.4.0-beta",
|
||||
"library": "hyper",
|
||||
"preferUnsignedInt": true
|
||||
}
|
29
tools/unitctl/pkg/brew/unitctl.rb
Normal file
29
tools/unitctl/pkg/brew/unitctl.rb
Normal file
|
@ -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
|
29
tools/unitctl/pkg/brew/unitctl.rb.template
Normal file
29
tools/unitctl/pkg/brew/unitctl.rb.template
Normal file
|
@ -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
|
1
tools/unitctl/rustfmt.toml
Normal file
1
tools/unitctl/rustfmt.toml
Normal file
|
@ -0,0 +1 @@
|
|||
max_width = 120
|
32
tools/unitctl/unit-client-rs/Cargo.toml
Normal file
32
tools/unitctl/unit-client-rs/Cargo.toml
Normal file
|
@ -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"
|
571
tools/unitctl/unit-client-rs/src/control_socket_address.rs
Normal file
571
tools/unitctl/unit-client-rs/src/control_socket_address.rs
Normal file
|
@ -0,0 +1,571 @@
|
|||
use crate::control_socket_address::ControlSocket::{TcpSocket, UnixLocalAbstractSocket, UnixLocalSocket};
|
||||
use crate::control_socket_address::ControlSocketScheme::{HTTP, HTTPS};
|
||||
use crate::unit_client::UnitClientError;
|
||||
use hyper::http::uri::{Authority, PathAndQuery};
|
||||
use hyper::Uri;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::fs;
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
use std::path::{PathBuf, MAIN_SEPARATOR};
|
||||
|
||||
type AbstractSocketName = String;
|
||||
type UnixSocketPath = PathBuf;
|
||||
type Port = u16;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ControlSocket {
|
||||
UnixLocalAbstractSocket(AbstractSocketName),
|
||||
UnixLocalSocket(UnixSocketPath),
|
||||
TcpSocket(Uri),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ControlSocketScheme {
|
||||
HTTP,
|
||||
HTTPS,
|
||||
}
|
||||
|
||||
impl ControlSocketScheme {
|
||||
fn port(&self) -> Port {
|
||||
match self {
|
||||
HTTP => 80,
|
||||
HTTPS => 443,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ControlSocket {
|
||||
pub fn socket_scheme(&self) -> ControlSocketScheme {
|
||||
match self {
|
||||
UnixLocalAbstractSocket(_) => ControlSocketScheme::HTTP,
|
||||
UnixLocalSocket(_) => ControlSocketScheme::HTTP,
|
||||
TcpSocket(uri) => match uri.scheme_str().expect("Scheme should not be None") {
|
||||
"http" => ControlSocketScheme::HTTP,
|
||||
"https" => ControlSocketScheme::HTTPS,
|
||||
_ => unreachable!("Scheme should be http or https"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_uri_with_path(&self, str_path: &str) -> Uri {
|
||||
match self {
|
||||
UnixLocalAbstractSocket(name) => {
|
||||
let socket_path = PathBuf::from(format!("@{}", name));
|
||||
hyperlocal::Uri::new(socket_path, str_path).into()
|
||||
}
|
||||
UnixLocalSocket(socket_path) => hyperlocal::Uri::new(socket_path, str_path).into(),
|
||||
TcpSocket(uri) => {
|
||||
if str_path.is_empty() {
|
||||
uri.clone()
|
||||
} else {
|
||||
let authority = uri.authority().expect("Authority should not be None");
|
||||
Uri::builder()
|
||||
.scheme(uri.scheme_str().expect("Scheme should not be None"))
|
||||
.authority(authority.clone())
|
||||
.path_and_query(str_path)
|
||||
.build()
|
||||
.expect("URI should be valid")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ControlSocket {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
UnixLocalAbstractSocket(name) => f.write_fmt(format_args!("unix:@{}", name)),
|
||||
UnixLocalSocket(path) => f.write_fmt(format_args!("unix:{}", path.to_string_lossy())),
|
||||
TcpSocket(uri) => uri.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ControlSocket> for String {
|
||||
fn from(val: ControlSocket) -> Self {
|
||||
val.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ControlSocket> for PathBuf {
|
||||
fn from(val: ControlSocket) -> Self {
|
||||
match val {
|
||||
UnixLocalAbstractSocket(socket_name) => PathBuf::from(format!("@{}", socket_name)),
|
||||
UnixLocalSocket(socket_path) => socket_path,
|
||||
TcpSocket(_) => PathBuf::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ControlSocket> for Uri {
|
||||
fn from(val: ControlSocket) -> Self {
|
||||
val.create_uri_with_path("")
|
||||
}
|
||||
}
|
||||
|
||||
impl ControlSocket {
|
||||
pub fn validate_http_address(uri: Uri) -> Result<(), UnitClientError> {
|
||||
let http_address = uri.to_string();
|
||||
if uri.authority().is_none() {
|
||||
return Err(UnitClientError::TcpSocketAddressParseError {
|
||||
message: "No authority found in socket address".to_string(),
|
||||
control_socket_address: http_address,
|
||||
});
|
||||
}
|
||||
if uri.port_u16().is_none() {
|
||||
return Err(UnitClientError::TcpSocketAddressNoPortError {
|
||||
control_socket_address: http_address,
|
||||
});
|
||||
}
|
||||
if !(uri.path().is_empty() || uri.path().eq("/")) {
|
||||
return Err(UnitClientError::TcpSocketAddressParseError {
|
||||
message: format!("Path is not empty or is not / [path={}]", uri.path()),
|
||||
control_socket_address: http_address,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_unix_address(socket: PathBuf) -> Result<(), UnitClientError> {
|
||||
if !socket.exists() {
|
||||
return Err(UnitClientError::UnixSocketNotFound {
|
||||
control_socket_address: socket.to_string_lossy().to_string(),
|
||||
});
|
||||
}
|
||||
let metadata = fs::metadata(&socket).map_err(|error| UnitClientError::UnixSocketAddressError {
|
||||
source: error,
|
||||
control_socket_address: socket.to_string_lossy().to_string(),
|
||||
})?;
|
||||
let file_type = metadata.file_type();
|
||||
if !file_type.is_socket() {
|
||||
return Err(UnitClientError::UnixSocketAddressError {
|
||||
source: std::io::Error::new(std::io::ErrorKind::Other, "Control socket path is not a socket"),
|
||||
control_socket_address: socket.to_string_lossy().to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<Self, UnitClientError> {
|
||||
match self {
|
||||
UnixLocalAbstractSocket(socket_name) => {
|
||||
let socket_path = PathBuf::from(format!("@{}", socket_name));
|
||||
Self::validate_unix_address(socket_path.clone())
|
||||
}
|
||||
UnixLocalSocket(socket_path) => Self::validate_unix_address(socket_path.clone()),
|
||||
TcpSocket(socket_uri) => Self::validate_http_address(socket_uri.clone()),
|
||||
}
|
||||
.map(|_| self.to_owned())
|
||||
}
|
||||
|
||||
fn normalize_and_parse_http_address(http_address: String) -> Result<Uri, UnitClientError> {
|
||||
// Convert *:1 style network addresses to URI format
|
||||
let address = if http_address.starts_with("*:") {
|
||||
http_address.replacen("*:", "http://127.0.0.1:", 1)
|
||||
// Add scheme if not present
|
||||
} else if !(http_address.starts_with("http://") || http_address.starts_with("https://")) {
|
||||
format!("http://{}", http_address)
|
||||
} else {
|
||||
http_address.to_owned()
|
||||
};
|
||||
|
||||
let is_https = address.starts_with("https://");
|
||||
|
||||
let parsed_uri =
|
||||
Uri::try_from(address.as_str()).map_err(|error| UnitClientError::TcpSocketAddressUriError {
|
||||
source: error,
|
||||
control_socket_address: address,
|
||||
})?;
|
||||
let authority = parsed_uri.authority().expect("Authority should not be None");
|
||||
let expected_port = if is_https { HTTPS.port() } else { HTTP.port() };
|
||||
let normalized_authority = match authority.port_u16() {
|
||||
Some(_) => authority.to_owned(),
|
||||
None => {
|
||||
let host = format!("{}:{}", authority.host(), expected_port);
|
||||
Authority::try_from(host.as_str()).expect("Authority should be valid")
|
||||
}
|
||||
};
|
||||
|
||||
let normalized_uri = Uri::builder()
|
||||
.scheme(parsed_uri.scheme_str().expect("Scheme should not be None"))
|
||||
.authority(normalized_authority)
|
||||
.path_and_query(PathAndQuery::from_static(""))
|
||||
.build()
|
||||
.map_err(|error| UnitClientError::TcpSocketAddressParseError {
|
||||
message: error.to_string(),
|
||||
control_socket_address: http_address.clone(),
|
||||
})?;
|
||||
|
||||
Ok(normalized_uri)
|
||||
}
|
||||
|
||||
/// Flexibly parse a textual representation of a socket address
|
||||
fn parse_address<S: Into<String>>(socket_address: S) -> Result<Self, UnitClientError> {
|
||||
let full_socket_address: String = socket_address.into();
|
||||
let socket_prefix = "unix:";
|
||||
let socket_uri_prefix = "unix://";
|
||||
let mut buf = String::with_capacity(socket_prefix.len());
|
||||
for (i, c) in full_socket_address.char_indices() {
|
||||
// Abstract unix socket with no prefix
|
||||
if i == 0 && c == '@' {
|
||||
return Ok(UnixLocalAbstractSocket(full_socket_address[1..].to_string()));
|
||||
}
|
||||
buf.push(c);
|
||||
// Unix socket with prefix
|
||||
if i == socket_prefix.len() - 1 && buf.eq(socket_prefix) {
|
||||
let path_text = full_socket_address[socket_prefix.len()..].to_string();
|
||||
// Return here if this URI does not have a scheme followed by double slashes
|
||||
if !path_text.starts_with("//") {
|
||||
return match path_text.strip_prefix('@') {
|
||||
Some(name) => Ok(UnixLocalAbstractSocket(name.to_string())),
|
||||
None => {
|
||||
let path = PathBuf::from(path_text);
|
||||
Ok(UnixLocalSocket(path))
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Unix socket with URI prefix
|
||||
if i == socket_uri_prefix.len() - 1 && buf.eq(socket_uri_prefix) {
|
||||
let uri = Uri::try_from(full_socket_address.as_str()).map_err(|error| {
|
||||
UnitClientError::TcpSocketAddressParseError {
|
||||
message: error.to_string(),
|
||||
control_socket_address: full_socket_address.clone(),
|
||||
}
|
||||
})?;
|
||||
return ControlSocket::try_from(uri);
|
||||
}
|
||||
}
|
||||
|
||||
/* Sockets on Windows are not supported, so there is no need to check
|
||||
* if the socket address is a valid path, so we can do this shortcut
|
||||
* here to see if a path was specified without a unix: prefix. */
|
||||
if buf.starts_with(MAIN_SEPARATOR) {
|
||||
let path = PathBuf::from(buf);
|
||||
return Ok(UnixLocalSocket(path));
|
||||
}
|
||||
|
||||
let uri = Self::normalize_and_parse_http_address(buf)?;
|
||||
Ok(TcpSocket(uri))
|
||||
}
|
||||
|
||||
pub fn is_local_socket(&self) -> bool {
|
||||
match self {
|
||||
UnixLocalAbstractSocket(_) | UnixLocalSocket(_) => true,
|
||||
TcpSocket(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for ControlSocket {
|
||||
type Error = UnitClientError;
|
||||
|
||||
fn try_from(socket_address: String) -> Result<Self, Self::Error> {
|
||||
ControlSocket::parse_address(socket_address.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ControlSocket {
|
||||
type Error = UnitClientError;
|
||||
|
||||
fn try_from(socket_address: &str) -> Result<Self, Self::Error> {
|
||||
ControlSocket::parse_address(socket_address)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Uri> for ControlSocket {
|
||||
type Error = UnitClientError;
|
||||
|
||||
fn try_from(socket_uri: Uri) -> Result<Self, Self::Error> {
|
||||
match socket_uri.scheme_str() {
|
||||
// URIs with the unix scheme will have a hostname that is a hex encoded string
|
||||
// representing the path to the socket
|
||||
Some("unix") => {
|
||||
let host = match socket_uri.host() {
|
||||
Some(host) => host,
|
||||
None => {
|
||||
return Err(UnitClientError::TcpSocketAddressParseError {
|
||||
message: "No host found in socket address".to_string(),
|
||||
control_socket_address: socket_uri.to_string(),
|
||||
})
|
||||
}
|
||||
};
|
||||
let bytes = hex::decode(host).map_err(|error| UnitClientError::TcpSocketAddressParseError {
|
||||
message: error.to_string(),
|
||||
control_socket_address: socket_uri.to_string(),
|
||||
})?;
|
||||
let path = String::from_utf8_lossy(&bytes);
|
||||
ControlSocket::parse_address(path)
|
||||
}
|
||||
Some("http") | Some("https") => Ok(TcpSocket(socket_uri)),
|
||||
Some(unknown) => Err(UnitClientError::TcpSocketAddressParseError {
|
||||
message: format!("Unsupported scheme found in socket address: {}", unknown).to_string(),
|
||||
control_socket_address: socket_uri.to_string(),
|
||||
}),
|
||||
None => Err(UnitClientError::TcpSocketAddressParseError {
|
||||
message: "No scheme found in socket address".to_string(),
|
||||
control_socket_address: socket_uri.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rand::distributions::{Alphanumeric, DistString};
|
||||
use std::env::temp_dir;
|
||||
use std::fmt::Display;
|
||||
use std::io;
|
||||
use std::os::unix::net::UnixListener;
|
||||
|
||||
use super::*;
|
||||
|
||||
struct TempSocket {
|
||||
socket_path: PathBuf,
|
||||
_listener: UnixListener,
|
||||
}
|
||||
|
||||
impl TempSocket {
|
||||
fn shutdown(&mut self) -> io::Result<()> {
|
||||
fs::remove_file(&self.socket_path)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for TempSocket {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "unix:{}", self.socket_path.to_string_lossy().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempSocket {
|
||||
fn drop(&mut self) {
|
||||
self.shutdown()
|
||||
.expect(format!("Unable to shutdown socket {}", self.socket_path.to_string_lossy()).as_str());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn will_error_with_nonexistent_unix_socket() {
|
||||
let socket_address = "unix:/tmp/some_random_filename_that_doesnt_exist.sock";
|
||||
let control_socket =
|
||||
ControlSocket::try_from(socket_address).expect("No error should be returned until validate() is called");
|
||||
assert!(control_socket.is_local_socket(), "Not parsed as a local socket");
|
||||
assert!(control_socket.validate().is_err(), "Socket should not be valid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_parse_socket_with_prefix() {
|
||||
let temp_socket = create_file_socket().expect("Unable to create socket");
|
||||
let control_socket = ControlSocket::try_from(temp_socket.to_string()).expect("Error parsing good socket path");
|
||||
assert!(control_socket.is_local_socket(), "Not parsed as a local socket");
|
||||
if let Err(e) = control_socket.validate() {
|
||||
panic!("Socket should be valid: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_parse_socket_from_uri() {
|
||||
let temp_socket = create_file_socket().expect("Unable to create socket");
|
||||
let uri: Uri = hyperlocal::Uri::new(temp_socket.socket_path.clone(), "").into();
|
||||
let control_socket = ControlSocket::try_from(uri).expect("Error parsing good socket path");
|
||||
assert!(control_socket.is_local_socket(), "Not parsed as a local socket");
|
||||
if let Err(e) = control_socket.validate() {
|
||||
panic!("Socket should be valid: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_parse_socket_from_uri_text() {
|
||||
let temp_socket = create_file_socket().expect("Unable to create socket");
|
||||
let uri: Uri = hyperlocal::Uri::new(temp_socket.socket_path.clone(), "").into();
|
||||
let control_socket = ControlSocket::parse_address(uri.to_string()).expect("Error parsing good socket path");
|
||||
assert!(control_socket.is_local_socket(), "Not parsed as a local socket");
|
||||
if let Err(e) = control_socket.validate() {
|
||||
panic!("Socket for input text should be valid: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "linux")]
|
||||
fn can_parse_abstract_socket_from_uri() {
|
||||
let temp_socket = create_abstract_socket().expect("Unable to create socket");
|
||||
let uri: Uri = hyperlocal::Uri::new(temp_socket.socket_path.clone(), "").into();
|
||||
let control_socket = ControlSocket::try_from(uri).expect("Error parsing good socket path");
|
||||
assert!(control_socket.is_local_socket(), "Not parsed as a local socket");
|
||||
if let Err(e) = control_socket.validate() {
|
||||
panic!("Socket should be valid: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "linux")]
|
||||
fn can_parse_abstract_socket_from_uri_text() {
|
||||
let temp_socket = create_abstract_socket().expect("Unable to create socket");
|
||||
let uri: Uri = hyperlocal::Uri::new(temp_socket.socket_path.clone(), "").into();
|
||||
let control_socket = ControlSocket::parse_address(uri.to_string()).expect("Error parsing good socket path");
|
||||
assert!(control_socket.is_local_socket(), "Not parsed as a local socket");
|
||||
if let Err(e) = control_socket.validate() {
|
||||
panic!("Socket should be valid: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_parse_socket_without_prefix() {
|
||||
let temp_socket = create_file_socket().expect("Unable to create socket");
|
||||
let control_socket = ControlSocket::try_from(temp_socket.socket_path.to_string_lossy().to_string())
|
||||
.expect("Error parsing good socket path");
|
||||
assert!(control_socket.is_local_socket(), "Not parsed as a local socket");
|
||||
if let Err(e) = control_socket.validate() {
|
||||
panic!("Socket should be valid: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn can_parse_abstract_socket() {
|
||||
let temp_socket = create_abstract_socket().expect("Unable to create socket");
|
||||
let control_socket = ControlSocket::try_from(temp_socket.to_string()).expect("Error parsing good socket path");
|
||||
assert!(control_socket.is_local_socket(), "Not parsed as a local socket");
|
||||
if let Err(e) = control_socket.validate() {
|
||||
panic!("Socket should be valid: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_normalize_good_http_socket_addresses() {
|
||||
let valid_socket_addresses = vec![
|
||||
"http://127.0.0.1:8080",
|
||||
"https://127.0.0.1:8080",
|
||||
"http://127.0.0.1:8080/",
|
||||
"127.0.0.1:8080",
|
||||
"http://0.0.0.0:8080",
|
||||
"https://0.0.0.0:8080",
|
||||
"http://0.0.0.0:8080/",
|
||||
"0.0.0.0:8080",
|
||||
"http://localhost:8080",
|
||||
"https://localhost:8080",
|
||||
"http://localhost:8080/",
|
||||
"localhost:8080",
|
||||
"http://[::1]:8080",
|
||||
"https://[::1]:8080",
|
||||
"http://[::1]:8080/",
|
||||
"[::1]:8080",
|
||||
"http://[0000:0000:0000:0000:0000:0000:0000:0000]:8080",
|
||||
"https://[0000:0000:0000:0000:0000:0000:0000:0000]:8080",
|
||||
"http://[0000:0000:0000:0000:0000:0000:0000:0000]:8080/",
|
||||
"[0000:0000:0000:0000:0000:0000:0000:0000]:8080",
|
||||
];
|
||||
for socket_address in valid_socket_addresses {
|
||||
let mut expected = if socket_address.starts_with("http") {
|
||||
socket_address.to_string().trim_end_matches('/').to_string()
|
||||
} else {
|
||||
format!("http://{}", socket_address).trim_end_matches('/').to_string()
|
||||
};
|
||||
expected.push('/');
|
||||
|
||||
let control_socket = ControlSocket::try_from(socket_address).expect("Error parsing good socket path");
|
||||
assert!(!control_socket.is_local_socket(), "Not parsed as a local socket");
|
||||
if let Err(e) = control_socket.validate() {
|
||||
panic!("Socket should be valid: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_normalize_wildcard_http_socket_address() {
|
||||
let socket_address = "*:8080";
|
||||
let expected = "http://127.0.0.1:8080/";
|
||||
let normalized_result = ControlSocket::normalize_and_parse_http_address(socket_address.to_string());
|
||||
let normalized = normalized_result
|
||||
.expect("Unable to normalize socket address")
|
||||
.to_string();
|
||||
assert_eq!(normalized, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_normalize_http_socket_address_with_no_port() {
|
||||
let socket_address = "http://localhost";
|
||||
let expected = "http://localhost:80/";
|
||||
let normalized_result = ControlSocket::normalize_and_parse_http_address(socket_address.to_string());
|
||||
let normalized = normalized_result
|
||||
.expect("Unable to normalize socket address")
|
||||
.to_string();
|
||||
assert_eq!(normalized, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_normalize_https_socket_address_with_no_port() {
|
||||
let socket_address = "https://localhost";
|
||||
let expected = "https://localhost:443/";
|
||||
let normalized_result = ControlSocket::normalize_and_parse_http_address(socket_address.to_string());
|
||||
let normalized = normalized_result
|
||||
.expect("Unable to normalize socket address")
|
||||
.to_string();
|
||||
assert_eq!(normalized, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_parse_http_addresses() {
|
||||
let valid_socket_addresses = vec![
|
||||
"http://127.0.0.1:8080",
|
||||
"https://127.0.0.1:8080",
|
||||
"http://127.0.0.1:8080/",
|
||||
"127.0.0.1:8080",
|
||||
"http://0.0.0.0:8080",
|
||||
"https://0.0.0.0:8080",
|
||||
"http://0.0.0.0:8080/",
|
||||
"0.0.0.0:8080",
|
||||
"http://localhost:8080",
|
||||
"https://localhost:8080",
|
||||
"http://localhost:8080/",
|
||||
"localhost:8080",
|
||||
"http://[::1]:8080",
|
||||
"https://[::1]:8080",
|
||||
"http://[::1]:8080/",
|
||||
"[::1]:8080",
|
||||
"http://[0000:0000:0000:0000:0000:0000:0000:0000]:8080",
|
||||
"https://[0000:0000:0000:0000:0000:0000:0000:0000]:8080",
|
||||
"http://[0000:0000:0000:0000:0000:0000:0000:0000]:8080/",
|
||||
"[0000:0000:0000:0000:0000:0000:0000:0000]:8080",
|
||||
];
|
||||
for socket_address in valid_socket_addresses {
|
||||
let mut expected = if socket_address.starts_with("http") {
|
||||
socket_address.to_string().trim_end_matches('/').to_string()
|
||||
} else {
|
||||
format!("http://{}", socket_address).trim_end_matches('/').to_string()
|
||||
};
|
||||
expected.push('/');
|
||||
|
||||
let normalized = ControlSocket::normalize_and_parse_http_address(socket_address.to_string())
|
||||
.expect("Unable to normalize socket address")
|
||||
.to_string();
|
||||
assert_eq!(normalized, expected);
|
||||
}
|
||||
}
|
||||
|
||||
fn create_file_socket() -> Result<TempSocket, io::Error> {
|
||||
let random = Alphanumeric.sample_string(&mut rand::thread_rng(), 10);
|
||||
let socket_name = format!("unit-client-socket-test-{}.sock", random);
|
||||
let socket_path = temp_dir().join(socket_name);
|
||||
let listener = UnixListener::bind(&socket_path)?;
|
||||
Ok(TempSocket {
|
||||
socket_path,
|
||||
_listener: listener,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn create_abstract_socket() -> Result<TempSocket, io::Error> {
|
||||
let random = Alphanumeric.sample_string(&mut rand::thread_rng(), 10);
|
||||
let socket_name = format!("@unit-client-socket-test-{}.sock", random);
|
||||
let socket_path = PathBuf::from(socket_name);
|
||||
let listener = UnixListener::bind(&socket_path)?;
|
||||
Ok(TempSocket {
|
||||
socket_path,
|
||||
_listener: listener,
|
||||
})
|
||||
}
|
||||
}
|
15
tools/unitctl/unit-client-rs/src/lib.rs
Normal file
15
tools/unitctl/unit-client-rs/src/lib.rs
Normal file
|
@ -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;
|
90
tools/unitctl/unit-client-rs/src/runtime_flags.rs
Normal file
90
tools/unitctl/unit-client-rs/src/runtime_flags.rs
Normal file
|
@ -0,0 +1,90 @@
|
|||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
use std::fmt::Display;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RuntimeFlags {
|
||||
pub flags: Cow<'static, str>,
|
||||
}
|
||||
|
||||
impl Display for RuntimeFlags {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.flags)
|
||||
}
|
||||
}
|
||||
|
||||
impl RuntimeFlags {
|
||||
pub fn new<S>(flags: S) -> RuntimeFlags
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
RuntimeFlags {
|
||||
flags: Cow::from(flags.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_flag(&self, flag_name: &str) -> bool {
|
||||
self.flags.contains(format!("--{}", flag_name).as_str())
|
||||
}
|
||||
|
||||
pub fn get_flag_value(&self, flag_name: &str) -> Option<String> {
|
||||
let flag_parts = self.flags.split_ascii_whitespace().collect::<Vec<&str>>();
|
||||
for (i, flag) in flag_parts.iter().enumerate() {
|
||||
if let Some(name) = flag.strip_prefix("--") {
|
||||
/* If there is no flag value after the current one, there is by definition no
|
||||
* flag value for the current flag. */
|
||||
let index_lt_len = flag_parts.len() > i + 1;
|
||||
if index_lt_len {
|
||||
let next_value_isnt_flag = !flag_parts[i + 1].starts_with("--");
|
||||
if name.eq(flag_name) && next_value_isnt_flag {
|
||||
return Some(flag_parts[i + 1].to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn control_api_socket_address(&self) -> Option<String> {
|
||||
self.get_flag_value("control")
|
||||
}
|
||||
|
||||
pub fn pid_path(&self) -> Option<Box<Path>> {
|
||||
self.get_flag_value("pid")
|
||||
.map(PathBuf::from)
|
||||
.map(PathBuf::into_boxed_path)
|
||||
}
|
||||
|
||||
pub fn log_path(&self) -> Option<Box<Path>> {
|
||||
self.get_flag_value("log")
|
||||
.map(PathBuf::from)
|
||||
.map(PathBuf::into_boxed_path)
|
||||
}
|
||||
|
||||
pub fn modules_directory(&self) -> Option<Box<Path>> {
|
||||
self.get_flag_value("modules")
|
||||
.map(PathBuf::from)
|
||||
.map(PathBuf::into_boxed_path)
|
||||
}
|
||||
|
||||
pub fn state_directory(&self) -> Option<Box<Path>> {
|
||||
self.get_flag_value("state")
|
||||
.map(PathBuf::from)
|
||||
.map(PathBuf::into_boxed_path)
|
||||
}
|
||||
|
||||
pub fn tmp_directory(&self) -> Option<Box<Path>> {
|
||||
self.get_flag_value("tmp")
|
||||
.map(PathBuf::from)
|
||||
.map(PathBuf::into_boxed_path)
|
||||
}
|
||||
|
||||
pub fn user(&self) -> Option<String> {
|
||||
self.get_flag_value("user").map(String::from)
|
||||
}
|
||||
|
||||
pub fn group(&self) -> Option<String> {
|
||||
self.get_flag_value("group").map(String::from)
|
||||
}
|
||||
}
|
393
tools/unitctl/unit-client-rs/src/unit_client.rs
Normal file
393
tools/unitctl/unit-client-rs/src/unit_client.rs
Normal file
|
@ -0,0 +1,393 @@
|
|||
use std::collections::HashMap;
|
||||
use std::error::Error as StdError;
|
||||
use std::fmt::Debug;
|
||||
use std::future::Future;
|
||||
use std::rc::Rc;
|
||||
use std::{fmt, io};
|
||||
|
||||
use custom_error::custom_error;
|
||||
use hyper::body::{Buf, HttpBody};
|
||||
use hyper::client::{HttpConnector, ResponseFuture};
|
||||
use hyper::Error as HyperError;
|
||||
use hyper::{http, Body, Client, Request};
|
||||
use hyper_tls::HttpsConnector;
|
||||
use hyperlocal::{UnixClientExt, UnixConnector};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use crate::control_socket_address::ControlSocket;
|
||||
use unit_openapi::apis::configuration::Configuration;
|
||||
use unit_openapi::apis::{Error as OpenAPIError, StatusApi};
|
||||
use unit_openapi::apis::{ListenersApi, ListenersApiClient, StatusApiClient};
|
||||
use unit_openapi::models::{ConfigListener, Status};
|
||||
|
||||
const USER_AGENT: &str = concat!("UNIT CLI/", env!("CARGO_PKG_VERSION"), "/rust");
|
||||
|
||||
custom_error! {pub UnitClientError
|
||||
OpenAPIError { source: OpenAPIError } = "OpenAPI error",
|
||||
JsonError { source: serde_json::Error,
|
||||
path: String} = "JSON error [path={path}]",
|
||||
HyperError { source: hyper::Error,
|
||||
control_socket_address: String,
|
||||
path: String} = "Communications error [control_socket_address={control_socket_address}, path={path}]: {source}",
|
||||
HttpRequestError { source: http::Error,
|
||||
path: String} = "HTTP error [path={path}]",
|
||||
HttpResponseError { status: http::StatusCode,
|
||||
path: String,
|
||||
body: String} = "HTTP response error [path={path}, status={status}]:\n{body}",
|
||||
HttpResponseJsonBodyError { status: http::StatusCode,
|
||||
path: String,
|
||||
error: String,
|
||||
detail: String} = "HTTP response error [path={path}, status={status}]:\n Error: {error}\n Detail: {detail}",
|
||||
IoError { source: io::Error, socket: String } = "IO error [socket={socket}]",
|
||||
UnixSocketAddressError {
|
||||
source: io::Error,
|
||||
control_socket_address: String
|
||||
} = "Invalid unix domain socket address [control_socket_address={control_socket_address}]",
|
||||
SocketPermissionsError { control_socket_address: String } =
|
||||
"Insufficient permissions to connect to control socket [control_socket_address={control_socket_address}]",
|
||||
UnixSocketNotFound { control_socket_address: String } = "Unix socket not found [control_socket_address={control_socket_address}]",
|
||||
TcpSocketAddressUriError {
|
||||
source: http::uri::InvalidUri,
|
||||
control_socket_address: String
|
||||
} = "Invalid TCP socket address [control_socket_address={control_socket_address}]",
|
||||
TcpSocketAddressParseError {
|
||||
message: String,
|
||||
control_socket_address: String
|
||||
} = "Invalid TCP socket address [control_socket_address={control_socket_address}]: {message}",
|
||||
TcpSocketAddressNoPortError {
|
||||
control_socket_address: String
|
||||
} = "TCP socket address does not have a port specified [control_socket_address={control_socket_address}]",
|
||||
UnitdProcessParseError {
|
||||
message: String,
|
||||
pid: u64
|
||||
} = "{message} for [pid={pid}]",
|
||||
UnitdProcessExecError {
|
||||
source: Box<dyn StdError>,
|
||||
message: String,
|
||||
executable_path: String,
|
||||
pid: u64
|
||||
} = "{message} for [pid={pid}, executable_path={executable_path}]: {source}",
|
||||
}
|
||||
|
||||
impl UnitClientError {
|
||||
fn new(error: HyperError, control_socket_address: String, path: String) -> Self {
|
||||
if error.is_connect() {
|
||||
if let Some(source) = error.source() {
|
||||
if let Some(io_error) = source.downcast_ref::<io::Error>() {
|
||||
if io_error.kind().eq(&io::ErrorKind::PermissionDenied) {
|
||||
return UnitClientError::SocketPermissionsError { control_socket_address };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UnitClientError::HyperError {
|
||||
source: error,
|
||||
control_socket_address,
|
||||
path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! new_openapi_client_from_hyper_client {
|
||||
($unit_client:expr, $hyper_client: ident, $api_client:ident, $api_trait:ident) => {{
|
||||
let config = Configuration {
|
||||
base_path: $unit_client.control_socket.create_uri_with_path("/").to_string(),
|
||||
user_agent: Some(format!("{}/OpenAPI-Generator", USER_AGENT).to_owned()),
|
||||
client: $hyper_client.clone(),
|
||||
basic_auth: None,
|
||||
oauth_access_token: None,
|
||||
api_key: None,
|
||||
};
|
||||
let rc_config = Rc::new(config);
|
||||
Box::new($api_client::new(rc_config)) as Box<dyn $api_trait>
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! new_openapi_client {
|
||||
($unit_client:expr, $api_client:ident, $api_trait:ident) => {
|
||||
match &*$unit_client.client {
|
||||
RemoteClient::Tcp { client } => {
|
||||
new_openapi_client_from_hyper_client!($unit_client, client, $api_client, $api_trait)
|
||||
}
|
||||
RemoteClient::Unix { client } => {
|
||||
new_openapi_client_from_hyper_client!($unit_client, client, $api_client, $api_trait)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum RemoteClient<B>
|
||||
where
|
||||
B: HttpBody + Send + 'static,
|
||||
B::Data: Send,
|
||||
B::Error: Into<Box<dyn StdError + Send + Sync>>,
|
||||
{
|
||||
Unix {
|
||||
client: Client<UnixConnector, B>,
|
||||
},
|
||||
Tcp {
|
||||
client: Client<HttpsConnector<HttpConnector>, B>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<B> RemoteClient<B>
|
||||
where
|
||||
B: HttpBody + Send + 'static,
|
||||
B::Data: Send,
|
||||
B::Error: Into<Box<dyn StdError + Send + Sync>>,
|
||||
{
|
||||
fn client_name(&self) -> &str {
|
||||
match self {
|
||||
RemoteClient::Unix { .. } => "Client<UnixConnector, Body>",
|
||||
RemoteClient::Tcp { .. } => "Client<HttpsConnector<HttpConnector>, Body>",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request(&self, req: Request<B>) -> ResponseFuture {
|
||||
match self {
|
||||
RemoteClient::Unix { client } => client.request(req),
|
||||
RemoteClient::Tcp { client } => client.request(req),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> Debug for RemoteClient<B>
|
||||
where
|
||||
B: HttpBody + Send + 'static,
|
||||
B::Data: Send,
|
||||
B::Error: Into<Box<dyn StdError + Send + Sync>>,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.client_name())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UnitClient {
|
||||
pub control_socket: ControlSocket,
|
||||
/// A `current_thread` runtime for executing operations on the
|
||||
/// asynchronous client in a blocking manner.
|
||||
rt: Runtime,
|
||||
/// Client for communicating with the control API over the UNIX domain socket
|
||||
client: Box<RemoteClient<Body>>,
|
||||
}
|
||||
|
||||
impl UnitClient {
|
||||
pub fn new_with_runtime(control_socket: ControlSocket, runtime: Runtime) -> Self {
|
||||
if control_socket.is_local_socket() {
|
||||
Self::new_unix(control_socket, runtime)
|
||||
} else {
|
||||
Self::new_http(control_socket, runtime)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(control_socket: ControlSocket) -> Self {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("Unable to create a current_thread runtime");
|
||||
Self::new_with_runtime(control_socket, runtime)
|
||||
}
|
||||
|
||||
pub fn new_http(control_socket: ControlSocket, runtime: Runtime) -> Self {
|
||||
let remote_client = Client::builder().build(HttpsConnector::new());
|
||||
Self {
|
||||
control_socket,
|
||||
rt: runtime,
|
||||
client: Box::from(RemoteClient::Tcp { client: remote_client }),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_unix(control_socket: ControlSocket, runtime: Runtime) -> UnitClient {
|
||||
let remote_client = Client::unix();
|
||||
|
||||
Self {
|
||||
control_socket,
|
||||
rt: runtime,
|
||||
client: Box::from(RemoteClient::Unix { client: remote_client }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a request to UNIT and deserializes the JSON response body into the value of type `RESPONSE`.
|
||||
pub fn send_request_and_deserialize_response<RESPONSE: for<'de> serde::Deserialize<'de>>(
|
||||
&self,
|
||||
mut request: Request<Body>,
|
||||
) -> Result<RESPONSE, UnitClientError> {
|
||||
let uri = request.uri().clone();
|
||||
let path: &str = uri.path();
|
||||
|
||||
request.headers_mut().insert("User-Agent", USER_AGENT.parse().unwrap());
|
||||
|
||||
let response_future = self.client.request(request);
|
||||
|
||||
self.rt.block_on(async {
|
||||
let response = response_future
|
||||
.await
|
||||
.map_err(|error| UnitClientError::new(error, self.control_socket.to_string(), path.to_string()))?;
|
||||
|
||||
let status = response.status();
|
||||
let body = hyper::body::aggregate(response)
|
||||
.await
|
||||
.map_err(|error| UnitClientError::new(error, self.control_socket.to_string(), path.to_string()))?;
|
||||
let reader = &mut body.reader();
|
||||
if !status.is_success() {
|
||||
let error: HashMap<String, String> =
|
||||
serde_json::from_reader(reader).map_err(|error| UnitClientError::JsonError {
|
||||
source: error,
|
||||
path: path.to_string(),
|
||||
})?;
|
||||
|
||||
return Err(UnitClientError::HttpResponseJsonBodyError {
|
||||
status,
|
||||
path: path.to_string(),
|
||||
error: error.get("error").unwrap_or(&"Unknown error".into()).to_string(),
|
||||
detail: error.get("detail").unwrap_or(&"".into()).to_string(),
|
||||
});
|
||||
}
|
||||
serde_json::from_reader(reader).map_err(|error| UnitClientError::JsonError {
|
||||
source: error,
|
||||
path: path.to_string(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn listeners_api(&self) -> Box<dyn ListenersApi + 'static> {
|
||||
new_openapi_client!(self, ListenersApiClient, ListenersApi)
|
||||
}
|
||||
|
||||
pub fn listeners(&self) -> Result<HashMap<String, ConfigListener>, Box<UnitClientError>> {
|
||||
let list_listeners = self.listeners_api().get_listeners();
|
||||
self.execute_openapi_future(list_listeners)
|
||||
}
|
||||
|
||||
pub fn execute_openapi_future<F: Future<Output = Result<R, OpenAPIError>>, R: for<'de> serde::Deserialize<'de>>(
|
||||
&self,
|
||||
future: F,
|
||||
) -> Result<R, Box<UnitClientError>> {
|
||||
self.rt.block_on(future).map_err(|error| {
|
||||
let remapped_error = if let OpenAPIError::Hyper(hyper_error) = error {
|
||||
UnitClientError::new(hyper_error, self.control_socket.to_string(), "".to_string())
|
||||
} else {
|
||||
UnitClientError::OpenAPIError { source: error }
|
||||
};
|
||||
|
||||
Box::new(remapped_error)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn status_api(&self) -> Box<dyn StatusApi + 'static> {
|
||||
new_openapi_client!(self, StatusApiClient, StatusApi)
|
||||
}
|
||||
|
||||
pub fn status(&self) -> Result<Status, Box<UnitClientError>> {
|
||||
let status = self.status_api().get_status();
|
||||
self.execute_openapi_future(status)
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.status().is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
pub type UnitSerializableMap = HashMap<String, serde_json::Value>;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct UnitStatus {
|
||||
pub connections: UnitStatusConnections,
|
||||
pub requests: UnitStatusRequests,
|
||||
pub applications: HashMap<String, UnitStatusApplication>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct UnitStatusConnections {
|
||||
#[serde(default)]
|
||||
pub closed: usize,
|
||||
#[serde(default)]
|
||||
pub idle: usize,
|
||||
#[serde(default)]
|
||||
pub active: usize,
|
||||
#[serde(default)]
|
||||
pub accepted: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct UnitStatusRequests {
|
||||
#[serde(default)]
|
||||
pub active: usize,
|
||||
#[serde(default)]
|
||||
pub total: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct UnitStatusApplication {
|
||||
#[serde(default)]
|
||||
pub processes: HashMap<String, usize>,
|
||||
#[serde(default)]
|
||||
pub requests: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::unitd_instance::UnitdInstance;
|
||||
|
||||
use super::*;
|
||||
// Integration tests
|
||||
|
||||
#[test]
|
||||
fn can_connect_to_unit_api() {
|
||||
match UnitdInstance::running_unitd_instances().first() {
|
||||
Some(unit_instance) => {
|
||||
let control_api_socket_address = unit_instance
|
||||
.control_api_socket_address()
|
||||
.expect("No control API socket path found");
|
||||
let control_socket = ControlSocket::try_from(control_api_socket_address)
|
||||
.expect("Unable to parse control socket address");
|
||||
let unit_client = UnitClient::new(control_socket);
|
||||
assert!(unit_client.is_running());
|
||||
}
|
||||
None => {
|
||||
eprintln!("No running unitd instances found - skipping test");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_get_unit_status() {
|
||||
match UnitdInstance::running_unitd_instances().first() {
|
||||
Some(unit_instance) => {
|
||||
let control_api_socket_address = unit_instance
|
||||
.control_api_socket_address()
|
||||
.expect("No control API socket path found");
|
||||
let control_socket = ControlSocket::try_from(control_api_socket_address)
|
||||
.expect("Unable to parse control socket address");
|
||||
let unit_client = UnitClient::new(control_socket);
|
||||
let status = unit_client.status().expect("Unable to get unit status");
|
||||
println!("Unit status: {:?}", status);
|
||||
}
|
||||
None => {
|
||||
eprintln!("No running unitd instances found - skipping test");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_get_unit_listeners() {
|
||||
match UnitdInstance::running_unitd_instances().first() {
|
||||
Some(unit_instance) => {
|
||||
let control_api_socket_address = unit_instance
|
||||
.control_api_socket_address()
|
||||
.expect("No control API socket path found");
|
||||
let control_socket = ControlSocket::try_from(control_api_socket_address)
|
||||
.expect("Unable to parse control socket address");
|
||||
let unit_client = UnitClient::new(control_socket);
|
||||
unit_client.listeners().expect("Unable to get Unit listeners");
|
||||
}
|
||||
None => {
|
||||
eprintln!("No running unitd instances found - skipping test");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
85
tools/unitctl/unit-client-rs/src/unitd_cmd.rs
Normal file
85
tools/unitctl/unit-client-rs/src/unitd_cmd.rs
Normal file
|
@ -0,0 +1,85 @@
|
|||
use std::error::Error as StdError;
|
||||
use std::io::{Error as IoError, ErrorKind};
|
||||
|
||||
use crate::runtime_flags::RuntimeFlags;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UnitdCmd {
|
||||
pub(crate) process_executable_path: Option<Box<Path>>,
|
||||
pub version: Option<String>,
|
||||
pub flags: Option<RuntimeFlags>,
|
||||
}
|
||||
|
||||
impl UnitdCmd {
|
||||
pub(crate) fn new<S>(full_cmd: S, binary_name: &str) -> Result<UnitdCmd, Box<dyn StdError>>
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
let process_cmd: String = full_cmd.into();
|
||||
let parsable = process_cmd
|
||||
.strip_prefix("unit: main v")
|
||||
.and_then(|s| s.strip_suffix(']'));
|
||||
if parsable.is_none() {
|
||||
let msg = format!("cmd does not have the expected format: {}", process_cmd);
|
||||
return Err(IoError::new(ErrorKind::InvalidInput, msg).into());
|
||||
}
|
||||
let parts = parsable
|
||||
.expect("Unable to parse cmd")
|
||||
.splitn(2, " [")
|
||||
.collect::<Vec<&str>>();
|
||||
if parts.len() != 2 {
|
||||
let msg = format!("cmd does not have the expected format: {}", process_cmd);
|
||||
return Err(IoError::new(ErrorKind::InvalidInput, msg).into());
|
||||
}
|
||||
let version: Option<String> = Some(parts[0].to_string());
|
||||
let executable_path = UnitdCmd::parse_executable_path_from_cmd(parts[1], binary_name);
|
||||
let flags = UnitdCmd::parse_runtime_flags_from_cmd(parts[1]);
|
||||
|
||||
Ok(UnitdCmd {
|
||||
process_executable_path: executable_path,
|
||||
version,
|
||||
flags,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_executable_path_from_cmd<S>(full_cmd: S, binary_name: &str) -> Option<Box<Path>>
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
let cmd = full_cmd.into();
|
||||
if cmd.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let split = cmd.splitn(2, binary_name).collect::<Vec<&str>>();
|
||||
if split.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let path = format!("{}{}", split[0], binary_name);
|
||||
Some(PathBuf::from(path).into_boxed_path())
|
||||
}
|
||||
|
||||
fn parse_runtime_flags_from_cmd<S>(full_cmd: S) -> Option<RuntimeFlags>
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
let cmd = full_cmd.into();
|
||||
if cmd.is_empty() {
|
||||
return None;
|
||||
}
|
||||
// Split out everything in between the brackets [ and ]
|
||||
let split = cmd.trim_end_matches(']').splitn(2, '[').collect::<Vec<&str>>();
|
||||
if split.is_empty() {
|
||||
return None;
|
||||
}
|
||||
/* Now we need to parse a string like this:
|
||||
* ./sbin/unitd --no-daemon --tmp /tmp
|
||||
* and only return what is after the invoking command */
|
||||
split[0]
|
||||
.find("--")
|
||||
.map(|index| cmd[index..].to_string())
|
||||
.map(RuntimeFlags::new)
|
||||
}
|
||||
}
|
235
tools/unitctl/unit-client-rs/src/unitd_configure_options.rs
Normal file
235
tools/unitctl/unit-client-rs/src/unitd_configure_options.rs
Normal file
|
@ -0,0 +1,235 @@
|
|||
use custom_error::custom_error;
|
||||
use std::borrow::Cow;
|
||||
use std::error::Error as stdError;
|
||||
use std::io::{BufRead, BufReader, Lines};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
custom_error! {UnitdStderrParseError
|
||||
VersionNotFound = "Version string output not found",
|
||||
BuildSettingsNotFound = "Build settings not found"
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UnitdConfigureOptions {
|
||||
pub version: Cow<'static, str>,
|
||||
pub all_flags: Cow<'static, str>,
|
||||
}
|
||||
|
||||
impl UnitdConfigureOptions {
|
||||
pub fn new(unitd_path: &Path) -> Result<UnitdConfigureOptions, Box<dyn stdError>> {
|
||||
fn parse_configure_settings_from_unitd_stderr_output<B: BufRead>(
|
||||
lines: &mut Lines<B>,
|
||||
) -> Result<UnitdConfigureOptions, Box<dyn stdError>> {
|
||||
const VERSION_PREFIX: &str = "unit version: ";
|
||||
const CONFIGURED_AS_PREFIX: &str = "configured as ";
|
||||
const CONFIGURE_PREFIX: &str = "configured as ./configure ";
|
||||
|
||||
fn aggregate_parsable_lines(
|
||||
mut accum: (Option<String>, Option<String>),
|
||||
line: String,
|
||||
) -> (Option<String>, Option<String>) {
|
||||
if line.starts_with(VERSION_PREFIX) {
|
||||
accum.0 = line.strip_prefix(VERSION_PREFIX).map(|l| l.to_string());
|
||||
} else if line.starts_with(CONFIGURED_AS_PREFIX) {
|
||||
accum.1 = line.strip_prefix(CONFIGURE_PREFIX).map(|l| l.to_string());
|
||||
}
|
||||
|
||||
accum
|
||||
}
|
||||
|
||||
let options_lines = lines
|
||||
.filter_map(|line| line.ok())
|
||||
.fold((None, None), aggregate_parsable_lines);
|
||||
|
||||
if options_lines.0.is_none() {
|
||||
return Err(Box::new(UnitdStderrParseError::VersionNotFound) as Box<dyn stdError>);
|
||||
} else if options_lines.1.is_none() {
|
||||
return Err(Box::new(UnitdStderrParseError::BuildSettingsNotFound) as Box<dyn stdError>);
|
||||
}
|
||||
|
||||
Ok(UnitdConfigureOptions {
|
||||
version: options_lines.0.unwrap().into(),
|
||||
all_flags: options_lines.1.unwrap().into(),
|
||||
})
|
||||
}
|
||||
|
||||
let program = unitd_path.as_os_str();
|
||||
let child = Command::new(program)
|
||||
.arg("--version")
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
let output = child.wait_with_output()?;
|
||||
let err = BufReader::new(&*output.stderr);
|
||||
parse_configure_settings_from_unitd_stderr_output(&mut err.lines())
|
||||
}
|
||||
|
||||
pub fn has_flag(&self, flag_name: &str) -> bool {
|
||||
self.all_flags
|
||||
.split_ascii_whitespace()
|
||||
.any(|flag| flag.starts_with(format!("--{}", flag_name).as_str()))
|
||||
}
|
||||
|
||||
pub fn get_flag_value(&self, flag_name: &str) -> Option<String> {
|
||||
self.all_flags
|
||||
.split_ascii_whitespace()
|
||||
.find(|flag| flag.starts_with(format!("--{}", flag_name).as_str()))
|
||||
.and_then(|flag| {
|
||||
let parts: Vec<&str> = flag.split('=').collect();
|
||||
if parts.len() >= 2 {
|
||||
Some(parts[1].to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn debug_enabled(&self) -> bool {
|
||||
self.has_flag("debug")
|
||||
}
|
||||
|
||||
pub fn openssl_enabled(&self) -> bool {
|
||||
self.has_flag("openssl")
|
||||
}
|
||||
|
||||
pub fn prefix_path(&self) -> Option<Box<Path>> {
|
||||
self.get_flag_value("prefix")
|
||||
.map(PathBuf::from)
|
||||
.map(PathBuf::into_boxed_path)
|
||||
}
|
||||
|
||||
fn join_to_prefix_path<S>(&self, sub_path: S) -> Option<Box<Path>>
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
self.prefix_path()
|
||||
.map(|path| path.join(sub_path.into()).into_boxed_path())
|
||||
}
|
||||
|
||||
pub fn default_control_api_socket_address(&self) -> Option<String> {
|
||||
// If the socket address is specific configured in the configure options, we use
|
||||
// that. Otherwise, we use the default path as assumed to be unix:$prefix/control.unit.sock.
|
||||
match self.get_flag_value("control") {
|
||||
Some(socket_address) => Some(socket_address),
|
||||
None => {
|
||||
// Give up if the unitd is compiled with unix sockets disabled
|
||||
if self.has_flag("no-unix-sockets") {
|
||||
return None;
|
||||
}
|
||||
let socket_path = self.join_to_prefix_path("control.unit.sock");
|
||||
socket_path.map(|path| format!("unix:{}", path.to_string_lossy()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_pid_path(&self) -> Option<Box<Path>> {
|
||||
match self.get_flag_value("pid") {
|
||||
Some(pid_path) => self.join_to_prefix_path(pid_path),
|
||||
None => self.join_to_prefix_path("unit.pid"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_log_path(&self) -> Option<Box<Path>> {
|
||||
match self.get_flag_value("log") {
|
||||
Some(pid_path) => self.join_to_prefix_path(pid_path),
|
||||
None => self.join_to_prefix_path("unit.log"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_modules_directory(&self) -> Option<Box<Path>> {
|
||||
match self.get_flag_value("modules") {
|
||||
Some(modules_dir_name) => self.join_to_prefix_path(modules_dir_name),
|
||||
None => self.join_to_prefix_path("modules"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_state_directory(&self) -> Option<Box<Path>> {
|
||||
match self.get_flag_value("state") {
|
||||
Some(state_dir_name) => self.join_to_prefix_path(state_dir_name),
|
||||
None => self.join_to_prefix_path("state"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_tmp_directory(&self) -> Option<Box<Path>> {
|
||||
match self.get_flag_value("tmp") {
|
||||
Some(tmp_dir_name) => self.join_to_prefix_path(tmp_dir_name),
|
||||
None => self.join_to_prefix_path("tmp"),
|
||||
}
|
||||
}
|
||||
pub fn default_user(&self) -> Option<String> {
|
||||
self.get_flag_value("user").map(String::from)
|
||||
}
|
||||
pub fn default_group(&self) -> Option<String> {
|
||||
self.get_flag_value("group").map(String::from)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::unitd_instance;
|
||||
use crate::unitd_instance::UNITD_PATH_ENV_KEY;
|
||||
|
||||
#[test]
|
||||
fn can_detect_key() {
|
||||
let options = UnitdConfigureOptions {
|
||||
version: Default::default(),
|
||||
all_flags: Cow::from("--debug --openssl --prefix=/opt/unit"),
|
||||
};
|
||||
assert!(options.has_flag("debug"));
|
||||
assert!(options.has_flag("openssl"));
|
||||
assert!(options.has_flag("prefix"));
|
||||
assert!(!options.has_flag("fobar"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_get_flag_value_by_key() {
|
||||
let expected = "/opt/unit";
|
||||
let options = UnitdConfigureOptions {
|
||||
version: Default::default(),
|
||||
all_flags: Cow::from("--debug --openssl --prefix=/opt/unit"),
|
||||
};
|
||||
|
||||
let actual = options.get_flag_value("prefix");
|
||||
assert_eq!(expected, actual.unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_get_prefix_path() {
|
||||
let expected: Box<Path> = Path::new("/opt/unit").into();
|
||||
let options = UnitdConfigureOptions {
|
||||
version: Default::default(),
|
||||
all_flags: Cow::from("--debug --openssl --prefix=/opt/unit"),
|
||||
};
|
||||
|
||||
let actual = options.prefix_path();
|
||||
assert_eq!(expected, actual.unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_parse_complicated_configure_options() {
|
||||
let expected: Box<Path> = Path::new("/usr").into();
|
||||
let options = UnitdConfigureOptions {
|
||||
version: Default::default(),
|
||||
all_flags: Cow::from("--prefix=/usr --state=/var/lib/unit --control=unix:/var/run/control.unit.sock --pid=/var/run/unit.pid --log=/var/log/unit.log --tmp=/var/tmp --user=unit --group=unit --tests --openssl --modules=/usr/lib/unit/modules --libdir=/usr/lib/x86_64-linux-gnu --cc-opt='-g -O2 -fdebug-prefix-map=/data/builder/debuild/unit-1.28.0/pkg/deb/debuild/unit-1.28.0=. -specs=/usr/share/dpkg/no-pie-compile.specs -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC' --ld-opt='-Wl,-Bsymbolic-functions -specs=/usr/share/dpkg/no-pie-link.specs -Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie'
|
||||
"),
|
||||
};
|
||||
|
||||
let actual = options.prefix_path();
|
||||
assert_eq!(expected, actual.unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_run_unitd() {
|
||||
let specific_path = std::env::var(UNITD_PATH_ENV_KEY).map_err(|error| Box::new(error) as Box<dyn stdError>);
|
||||
let unitd_path = unitd_instance::find_executable_path(specific_path);
|
||||
let config_options = UnitdConfigureOptions::new(&unitd_path.unwrap());
|
||||
match config_options {
|
||||
Ok(options) => {
|
||||
println!("{:?}", options)
|
||||
}
|
||||
Err(error) => panic!("{}", error),
|
||||
};
|
||||
}
|
||||
}
|
360
tools/unitctl/unit-client-rs/src/unitd_instance.rs
Normal file
360
tools/unitctl/unit-client-rs/src/unitd_instance.rs
Normal file
|
@ -0,0 +1,360 @@
|
|||
use crate::unit_client::UnitClientError;
|
||||
use serde::ser::SerializeMap;
|
||||
use serde::{Serialize, Serializer};
|
||||
use std::error::Error as StdError;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{fmt, io};
|
||||
use which::which;
|
||||
|
||||
use crate::runtime_flags::RuntimeFlags;
|
||||
use crate::unitd_configure_options::UnitdConfigureOptions;
|
||||
use crate::unitd_process::UnitdProcess;
|
||||
|
||||
pub const UNITD_PATH_ENV_KEY: &str = "UNITD_PATH";
|
||||
pub const UNITD_BINARY_NAMES: [&str; 2] = ["unitd", "unitd-debug"];
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UnitdInstance {
|
||||
pub process: UnitdProcess,
|
||||
pub configure_options: Option<UnitdConfigureOptions>,
|
||||
pub errors: Vec<UnitClientError>,
|
||||
}
|
||||
|
||||
impl Serialize for UnitdInstance {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut state = serializer.serialize_map(Some(15))?;
|
||||
let runtime_flags = self
|
||||
.process
|
||||
.cmd()
|
||||
.and_then(|cmd| cmd.flags)
|
||||
.map(|flags| flags.to_string());
|
||||
|
||||
let configure_flags = self.configure_options.as_ref().map(|opts| opts.all_flags.clone());
|
||||
|
||||
state.serialize_entry("pid", &self.process.process_id)?;
|
||||
state.serialize_entry("version", &self.version())?;
|
||||
state.serialize_entry("user", &self.process.user)?;
|
||||
state.serialize_entry("effective_user", &self.process.effective_user)?;
|
||||
state.serialize_entry("executable", &self.process.executable_path())?;
|
||||
state.serialize_entry("control_socket", &self.control_api_socket_address())?;
|
||||
state.serialize_entry("child_pids", &self.process.child_pids)?;
|
||||
state.serialize_entry("log_path", &self.log_path())?;
|
||||
state.serialize_entry("pid_path", &self.pid_path())?;
|
||||
state.serialize_entry("modules_directory", &self.modules_directory())?;
|
||||
state.serialize_entry("state_directory", &self.state_directory())?;
|
||||
state.serialize_entry("tmp_directory", &self.tmp_directory())?;
|
||||
state.serialize_entry("runtime_flags", &runtime_flags)?;
|
||||
state.serialize_entry("configure_flags", &configure_flags)?;
|
||||
let string_errors = &self.errors.iter().map(|e| e.to_string()).collect::<Vec<String>>();
|
||||
state.serialize_entry("errors", string_errors)?;
|
||||
|
||||
state.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl UnitdInstance {
|
||||
pub fn running_unitd_instances() -> Vec<UnitdInstance> {
|
||||
Self::collect_unitd_processes(UnitdProcess::find_unitd_processes())
|
||||
}
|
||||
|
||||
/// Find all running unitd processes and convert them into UnitdInstances and filter
|
||||
/// out all errors by printing them to stderr and leaving errored instances out of
|
||||
/// the returned vector.
|
||||
fn collect_unitd_processes(processes: Vec<UnitdProcess>) -> Vec<UnitdInstance> {
|
||||
Self::map_processes_to_instances(processes).into_iter().collect()
|
||||
}
|
||||
|
||||
fn map_processes_to_instances(processes: Vec<UnitdProcess>) -> Vec<UnitdInstance> {
|
||||
fn unitd_path_from_process(process: &UnitdProcess) -> Result<Box<Path>, UnitClientError> {
|
||||
match process.executable_path() {
|
||||
Some(executable_path) => {
|
||||
let is_absolute_working_dir = process
|
||||
.working_dir
|
||||
.as_ref()
|
||||
.map(|p| p.is_absolute())
|
||||
.unwrap_or_default();
|
||||
if executable_path.is_absolute() {
|
||||
Ok(executable_path.to_owned())
|
||||
} else if executable_path.is_relative() && is_absolute_working_dir {
|
||||
let new_path = process
|
||||
.working_dir
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.join(executable_path)
|
||||
.canonicalize()
|
||||
.map(|path| path.into_boxed_path())
|
||||
.map_err(|error| UnitClientError::UnitdProcessParseError {
|
||||
message: format!("Error canonicalizing unitd executable path: {}", error),
|
||||
pid: process.process_id,
|
||||
})?;
|
||||
Ok(new_path)
|
||||
} else {
|
||||
Err(UnitClientError::UnitdProcessParseError {
|
||||
message: "Unable to get absolute unitd executable path from process".to_string(),
|
||||
pid: process.process_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
None => Err(UnitClientError::UnitdProcessParseError {
|
||||
message: "Unable to get unitd executable path from process".to_string(),
|
||||
pid: process.process_id,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_process_to_unitd_instance(process: &UnitdProcess) -> UnitdInstance {
|
||||
match unitd_path_from_process(process) {
|
||||
Ok(unitd_path) => match UnitdConfigureOptions::new(&unitd_path.clone().into_path_buf()) {
|
||||
Ok(configure_options) => UnitdInstance {
|
||||
process: process.to_owned(),
|
||||
configure_options: Some(configure_options),
|
||||
errors: vec![],
|
||||
},
|
||||
Err(error) => {
|
||||
let error = UnitClientError::UnitdProcessExecError {
|
||||
source: error,
|
||||
executable_path: unitd_path.to_string_lossy().parse().unwrap_or_default(),
|
||||
message: "Error running unitd binary to get configure options".to_string(),
|
||||
pid: process.process_id,
|
||||
};
|
||||
UnitdInstance {
|
||||
process: process.to_owned(),
|
||||
configure_options: None,
|
||||
errors: vec![error],
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(err) => UnitdInstance {
|
||||
process: process.to_owned(),
|
||||
configure_options: None,
|
||||
errors: vec![err],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
processes
|
||||
.iter()
|
||||
// This converts processes into a UnitdInstance
|
||||
.map(map_process_to_unitd_instance)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn version(&self) -> Option<String> {
|
||||
match self.process.cmd()?.version {
|
||||
Some(version) => Some(version),
|
||||
None => self.configure_options.as_ref().map(|opts| opts.version.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn flag_or_default_option<R>(
|
||||
&self,
|
||||
read_flag: fn(RuntimeFlags) -> Option<R>,
|
||||
read_opts: fn(UnitdConfigureOptions) -> Option<R>,
|
||||
) -> Option<R> {
|
||||
self.process
|
||||
.cmd()?
|
||||
.flags
|
||||
.and_then(read_flag)
|
||||
.or_else(|| self.configure_options.to_owned().and_then(read_opts))
|
||||
}
|
||||
|
||||
pub fn control_api_socket_address(&self) -> Option<String> {
|
||||
self.flag_or_default_option(
|
||||
|flags| flags.control_api_socket_address(),
|
||||
|opts| opts.default_control_api_socket_address(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn pid_path(&self) -> Option<Box<Path>> {
|
||||
self.flag_or_default_option(|flags| flags.pid_path(), |opts| opts.default_pid_path())
|
||||
}
|
||||
|
||||
pub fn log_path(&self) -> Option<Box<Path>> {
|
||||
self.flag_or_default_option(|flags| flags.log_path(), |opts| opts.default_log_path())
|
||||
}
|
||||
|
||||
pub fn modules_directory(&self) -> Option<Box<Path>> {
|
||||
self.flag_or_default_option(
|
||||
|flags| flags.modules_directory(),
|
||||
|opts| opts.default_modules_directory(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn state_directory(&self) -> Option<Box<Path>> {
|
||||
self.flag_or_default_option(|flags| flags.state_directory(), |opts| opts.default_state_directory())
|
||||
}
|
||||
|
||||
pub fn tmp_directory(&self) -> Option<Box<Path>> {
|
||||
self.flag_or_default_option(|flags| flags.tmp_directory(), |opts| opts.default_tmp_directory())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for UnitdInstance {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
const UNKNOWN: &str = "[unknown]";
|
||||
let version = self.version().unwrap_or_else(|| String::from("[unknown]"));
|
||||
let runtime_flags = self
|
||||
.process
|
||||
.cmd()
|
||||
.and_then(|cmd| cmd.flags)
|
||||
.map(|flags| flags.to_string())
|
||||
.unwrap_or_else(|| UNKNOWN.into());
|
||||
let configure_flags = self
|
||||
.configure_options
|
||||
.as_ref()
|
||||
.map(|opts| opts.all_flags.clone())
|
||||
.unwrap_or_else(|| UNKNOWN.into());
|
||||
let unitd_path: String = self
|
||||
.process
|
||||
.executable_path()
|
||||
.map(|p| p.to_string_lossy().into())
|
||||
.unwrap_or_else(|| UNKNOWN.into());
|
||||
let working_dir: String = self
|
||||
.process
|
||||
.working_dir
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().into())
|
||||
.unwrap_or_else(|| UNKNOWN.into());
|
||||
let socket_address = self.control_api_socket_address().unwrap_or_else(|| UNKNOWN.to_string());
|
||||
let child_pids = self
|
||||
.process
|
||||
.child_pids
|
||||
.iter()
|
||||
.map(u64::to_string)
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
|
||||
writeln!(
|
||||
f,
|
||||
"{} instance [pid: {}, version: {}]:",
|
||||
self.process.binary_name, self.process.process_id, version
|
||||
)?;
|
||||
writeln!(f, " Executable: {}", unitd_path)?;
|
||||
writeln!(f, " Process working directory: {}", working_dir)?;
|
||||
write!(f, " Process ownership: ")?;
|
||||
if let Some(user) = &self.process.user {
|
||||
writeln!(f, "name: {}, uid: {}, gid: {}", user.name, user.uid, user.gid)?;
|
||||
} else {
|
||||
writeln!(f, "{}", UNKNOWN)?;
|
||||
}
|
||||
write!(f, " Process effective ownership: ")?;
|
||||
if let Some(user) = &self.process.effective_user {
|
||||
writeln!(f, "name: {}, uid: {}, gid: {}", user.name, user.uid, user.gid)?;
|
||||
} else {
|
||||
writeln!(f, "{}", UNKNOWN)?;
|
||||
}
|
||||
|
||||
writeln!(f, " API control unix socket: {}", socket_address)?;
|
||||
writeln!(f, " Child processes ids: {}", child_pids)?;
|
||||
writeln!(f, " Runtime flags: {}", runtime_flags)?;
|
||||
write!(f, " Configure options: {}", configure_flags)?;
|
||||
|
||||
if !self.errors.is_empty() {
|
||||
write!(f, "\n Errors:")?;
|
||||
for error in &self.errors {
|
||||
write!(f, "\n {}", error)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_executable_path(specific_path: Result<String, Box<dyn StdError>>) -> Result<PathBuf, Box<dyn StdError>> {
|
||||
fn find_unitd_in_system_path() -> Vec<PathBuf> {
|
||||
UNITD_BINARY_NAMES
|
||||
.iter()
|
||||
.map(which)
|
||||
.filter_map(Result::ok)
|
||||
.collect::<Vec<PathBuf>>()
|
||||
}
|
||||
|
||||
match specific_path {
|
||||
Ok(path) => Ok(PathBuf::from(path)),
|
||||
Err(_) => {
|
||||
let unitd_paths = find_unitd_in_system_path();
|
||||
if unitd_paths.is_empty() {
|
||||
let err_msg = format!(
|
||||
"Could not find unitd in system path or in UNITD_PATH environment variable. Searched for: {:?}",
|
||||
UNITD_BINARY_NAMES
|
||||
);
|
||||
let err = io::Error::new(io::ErrorKind::NotFound, err_msg);
|
||||
Err(Box::from(err))
|
||||
} else {
|
||||
Ok(unitd_paths[0].clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand::rngs::StdRng;
|
||||
use rand::{RngCore, SeedableRng};
|
||||
|
||||
// We don't need a secure seed for testing, in fact it is better that we have a
|
||||
// predictable value
|
||||
const SEED: [u8; 32] = [
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
|
||||
30, 31,
|
||||
];
|
||||
#[test]
|
||||
fn can_find_unitd_instances() {
|
||||
UnitdInstance::running_unitd_instances().iter().for_each(|p| {
|
||||
println!("{:?}", p);
|
||||
println!("Runtime Flags: {:?}", p.process.cmd().map(|c| c.flags));
|
||||
println!("Temp directory: {:?}", p.tmp_directory());
|
||||
})
|
||||
}
|
||||
|
||||
fn mock_process<S: Into<String>>(
|
||||
rng: &mut StdRng,
|
||||
binary_name: S,
|
||||
executable_path: Option<String>,
|
||||
) -> UnitdProcess {
|
||||
UnitdProcess {
|
||||
process_id: rng.next_u32() as u64,
|
||||
binary_name: binary_name.into(),
|
||||
executable_path: executable_path.map(|p| Box::from(Path::new(&p))),
|
||||
environ: vec![],
|
||||
all_cmds: vec![],
|
||||
working_dir: Some(Box::from(Path::new("/opt/unit"))),
|
||||
child_pids: vec![],
|
||||
user: None,
|
||||
effective_user: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn will_list_without_errors_valid_processes() {
|
||||
let specific_path = std::env::var(UNITD_PATH_ENV_KEY).map_err(|error| Box::new(error) as Box<dyn StdError>);
|
||||
let binding = match find_executable_path(specific_path) {
|
||||
Ok(path) => path,
|
||||
Err(error) => {
|
||||
eprintln!("Could not find unitd executable path: {} - skipping test", error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let binary_name = binding
|
||||
.file_name()
|
||||
.expect("Could not get binary name")
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let unitd_path = binding.to_string_lossy();
|
||||
let mut rng: StdRng = SeedableRng::from_seed(SEED);
|
||||
|
||||
let processes = vec![
|
||||
mock_process(&mut rng, &binary_name, Some(unitd_path.to_string())),
|
||||
mock_process(&mut rng, &binary_name, Some(unitd_path.to_string())),
|
||||
];
|
||||
let instances = UnitdInstance::collect_unitd_processes(processes);
|
||||
// assert_eq!(instances.len(), 3);
|
||||
instances.iter().for_each(|p| {
|
||||
assert_eq!(p.errors.len(), 0, "Expected no errors, got: {:?}", p.errors);
|
||||
})
|
||||
}
|
||||
}
|
170
tools/unitctl/unit-client-rs/src/unitd_process.rs
Normal file
170
tools/unitctl/unit-client-rs/src/unitd_process.rs
Normal file
|
@ -0,0 +1,170 @@
|
|||
use crate::unitd_cmd::UnitdCmd;
|
||||
use crate::unitd_instance::UNITD_BINARY_NAMES;
|
||||
use crate::unitd_process_user::UnitdProcessUser;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use sysinfo::{Pid, Process, ProcessRefreshKind, System, UpdateKind, Users};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UnitdProcess {
|
||||
pub binary_name: String,
|
||||
pub process_id: u64,
|
||||
pub executable_path: Option<Box<Path>>,
|
||||
pub environ: Vec<String>,
|
||||
pub all_cmds: Vec<String>,
|
||||
pub working_dir: Option<Box<Path>>,
|
||||
pub child_pids: Vec<u64>,
|
||||
pub user: Option<UnitdProcessUser>,
|
||||
pub effective_user: Option<UnitdProcessUser>,
|
||||
}
|
||||
|
||||
impl UnitdProcess {
|
||||
pub fn find_unitd_processes() -> Vec<UnitdProcess> {
|
||||
let process_refresh_kind = ProcessRefreshKind::new()
|
||||
.with_cmd(UpdateKind::Always)
|
||||
.with_cwd(UpdateKind::Always)
|
||||
.with_exe(UpdateKind::Always)
|
||||
.with_user(UpdateKind::Always);
|
||||
let refresh_kind = sysinfo::RefreshKind::new().with_processes(process_refresh_kind);
|
||||
let sys = System::new_with_specifics(refresh_kind);
|
||||
let unitd_processes: HashMap<&Pid, &Process> = sys
|
||||
.processes()
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
let process_name = p.1.name();
|
||||
UNITD_BINARY_NAMES.contains(&process_name)
|
||||
})
|
||||
.collect::<HashMap<&Pid, &Process>>();
|
||||
let users = Users::new_with_refreshed_list();
|
||||
|
||||
unitd_processes
|
||||
.iter()
|
||||
// Filter out child processes
|
||||
.filter(|p| {
|
||||
let parent_pid = p.1.parent();
|
||||
match parent_pid {
|
||||
Some(pid) => !unitd_processes.contains_key(&pid),
|
||||
None => false,
|
||||
}
|
||||
})
|
||||
.map(|p| {
|
||||
let tuple = p.to_owned();
|
||||
/* The sysinfo library only supports 32-bit pids, yet larger values are possible
|
||||
* if the OS is configured to support it, thus we use 64-bit integers internally
|
||||
* because it is just a matter of time until the library changes to larger values. */
|
||||
let pid = *tuple.0;
|
||||
let process = *tuple.1;
|
||||
let process_id: u64 = pid.as_u32().into();
|
||||
let executable_path: Option<Box<Path>> = process.exe().map(|p| p.to_path_buf().into_boxed_path());
|
||||
let environ: Vec<String> = process.environ().into();
|
||||
let cmd: Vec<String> = process.cmd().into();
|
||||
let working_dir: Option<Box<Path>> = process.cwd().map(|p| p.to_path_buf().into_boxed_path());
|
||||
let child_pids = unitd_processes
|
||||
.iter()
|
||||
.filter_map(|p| p.to_owned().1.parent())
|
||||
.filter(|parent_pid| parent_pid == pid)
|
||||
.map(|p| p.as_u32() as u64)
|
||||
.collect::<Vec<u64>>();
|
||||
|
||||
let user = process
|
||||
.user_id()
|
||||
.and_then(|uid| users.get_user_by_id(uid))
|
||||
.map(UnitdProcessUser::from);
|
||||
let effective_user = process
|
||||
.effective_user_id()
|
||||
.and_then(|uid| users.get_user_by_id(uid))
|
||||
.map(UnitdProcessUser::from);
|
||||
|
||||
UnitdProcess {
|
||||
binary_name: process.name().to_string(),
|
||||
process_id,
|
||||
executable_path,
|
||||
environ,
|
||||
all_cmds: cmd,
|
||||
working_dir,
|
||||
child_pids,
|
||||
user,
|
||||
effective_user,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<UnitdProcess>>()
|
||||
}
|
||||
|
||||
pub fn cmd(&self) -> Option<UnitdCmd> {
|
||||
if self.all_cmds.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
match UnitdCmd::new(self.all_cmds[0].clone(), self.binary_name.as_ref()) {
|
||||
Ok(cmd) => Some(cmd),
|
||||
Err(error) => {
|
||||
eprintln!("Failed to parse process cmd: {}", error);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn executable_path(&self) -> Option<Box<Path>> {
|
||||
if self.executable_path.is_some() {
|
||||
return self.executable_path.clone();
|
||||
}
|
||||
self.cmd().and_then(|cmd| cmd.process_executable_path)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn can_parse_runtime_cmd_absolute_path(binary_name: &str) {
|
||||
let cmd = format!(
|
||||
"unit: main v1.28.0 [/usr/sbin/{} --log /var/log/unit.log --pid /var/run/unit.pid]",
|
||||
binary_name
|
||||
);
|
||||
let unitd_cmd = UnitdCmd::new(cmd, binary_name).expect("Failed to parse unitd cmd");
|
||||
assert_eq!(unitd_cmd.version.unwrap(), "1.28.0");
|
||||
assert_eq!(
|
||||
unitd_cmd.process_executable_path.unwrap().to_string_lossy(),
|
||||
format!("/usr/sbin/{}", binary_name)
|
||||
);
|
||||
let flags = unitd_cmd.flags.unwrap();
|
||||
assert_eq!(flags.get_flag_value("log").unwrap(), "/var/log/unit.log");
|
||||
assert_eq!(flags.get_flag_value("pid").unwrap(), "/var/run/unit.pid");
|
||||
}
|
||||
|
||||
fn can_parse_runtime_cmd_relative_path(binary_name: &str) {
|
||||
let cmd = format!(
|
||||
"unit: main v1.29.0 [./sbin/{} --no-daemon --tmp /tmp --something]",
|
||||
binary_name
|
||||
);
|
||||
let unitd_cmd = UnitdCmd::new(cmd, binary_name).expect("Failed to parse unitd cmd");
|
||||
assert_eq!(unitd_cmd.version.unwrap(), "1.29.0");
|
||||
assert_eq!(
|
||||
unitd_cmd.process_executable_path.unwrap().to_string_lossy(),
|
||||
format!("./sbin/{}", binary_name)
|
||||
);
|
||||
let flags = unitd_cmd.flags.unwrap();
|
||||
assert_eq!(flags.get_flag_value("tmp").unwrap(), "/tmp");
|
||||
assert!(flags.has_flag("something"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_parse_runtime_cmd_unitd_absolute_path() {
|
||||
can_parse_runtime_cmd_absolute_path("unitd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_parse_runtime_cmd_unitd_debug_absolute_path() {
|
||||
can_parse_runtime_cmd_absolute_path("unitd-debug");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_parse_runtime_cmd_unitd_relative_path() {
|
||||
can_parse_runtime_cmd_relative_path("unitd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_parse_runtime_cmd_unitd_debug_relative_path() {
|
||||
can_parse_runtime_cmd_relative_path("unitd-debug");
|
||||
}
|
||||
}
|
36
tools/unitctl/unit-client-rs/src/unitd_process_user.rs
Normal file
36
tools/unitctl/unit-client-rs/src/unitd_process_user.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
use serde::Serialize;
|
||||
use std::fmt;
|
||||
use std::fmt::Display;
|
||||
use sysinfo::User;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct UnitdProcessUser {
|
||||
pub name: String,
|
||||
pub uid: u32,
|
||||
pub gid: u32,
|
||||
pub groups: Vec<String>,
|
||||
}
|
||||
|
||||
impl Display for UnitdProcessUser {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"name: {}, uid: {}, gid: {}, groups: {}",
|
||||
self.name,
|
||||
self.uid,
|
||||
self.gid,
|
||||
self.groups.join(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&User> for UnitdProcessUser {
|
||||
fn from(user: &User) -> Self {
|
||||
UnitdProcessUser {
|
||||
name: user.name().into(),
|
||||
uid: *user.id().clone(),
|
||||
gid: *user.group_id(),
|
||||
groups: user.groups().iter().map(|g| g.name().into()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
3
tools/unitctl/unit-openapi/.gitignore
vendored
Normal file
3
tools/unitctl/unit-openapi/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/target/
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
27
tools/unitctl/unit-openapi/.openapi-generator-ignore
Normal file
27
tools/unitctl/unit-openapi/.openapi-generator-ignore
Normal file
|
@ -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
|
161
tools/unitctl/unit-openapi/.openapi-generator/FILES
Normal file
161
tools/unitctl/unit-openapi/.openapi-generator/FILES
Normal file
|
@ -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
|
1
tools/unitctl/unit-openapi/.openapi-generator/VERSION
Normal file
1
tools/unitctl/unit-openapi/.openapi-generator/VERSION
Normal file
|
@ -0,0 +1 @@
|
|||
6.6.0
|
17
tools/unitctl/unit-openapi/Cargo.toml
Normal file
17
tools/unitctl/unit-openapi/Cargo.toml
Normal file
|
@ -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"
|
411
tools/unitctl/unit-openapi/README.md
Normal file
411
tools/unitctl/unit-openapi/README.md
Normal file
|
@ -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
|
65
tools/unitctl/unit-openapi/openapi-templates/Cargo.mustache
Normal file
65
tools/unitctl/unit-openapi/openapi-templates/Cargo.mustache
Normal file
|
@ -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}}
|
248
tools/unitctl/unit-openapi/openapi-templates/request.rs
Normal file
248
tools/unitctl/unit-openapi/openapi-templates/request.rs
Normal file
|
@ -0,0 +1,248 @@
|
|||
use std::collections::HashMap;
|
||||
use std::pin::Pin;
|
||||
|
||||
use base64::{alphabet, Engine};
|
||||
use base64::engine::general_purpose::NO_PAD;
|
||||
use base64::engine::GeneralPurpose;
|
||||
|
||||
use futures;
|
||||
use futures::Future;
|
||||
use futures::future::*;
|
||||
use hyper;
|
||||
use hyper::header::{AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE, HeaderValue, USER_AGENT};
|
||||
use serde;
|
||||
use serde_json;
|
||||
|
||||
use super::{configuration, Error};
|
||||
|
||||
const MIME_ENCODER: GeneralPurpose = GeneralPurpose::new(&alphabet::STANDARD, NO_PAD);
|
||||
|
||||
pub(crate) struct ApiKey {
|
||||
pub in_header: bool,
|
||||
pub in_query: bool,
|
||||
pub param_name: String,
|
||||
}
|
||||
|
||||
impl ApiKey {
|
||||
fn key(&self, prefix: &Option<String>, key: &str) -> String {
|
||||
match prefix {
|
||||
None => key.to_owned(),
|
||||
Some(ref prefix) => format!("{} {}", prefix, key),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) enum Auth {
|
||||
None,
|
||||
ApiKey(ApiKey),
|
||||
Basic,
|
||||
Oauth,
|
||||
}
|
||||
|
||||
/// If the authorization type is unspecified then it will be automatically detected based
|
||||
/// on the configuration. This functionality is useful when the OpenAPI definition does not
|
||||
/// include an authorization scheme.
|
||||
pub(crate) struct Request {
|
||||
auth: Option<Auth>,
|
||||
method: hyper::Method,
|
||||
path: String,
|
||||
query_params: HashMap<String, String>,
|
||||
no_return_type: bool,
|
||||
path_params: HashMap<String, String>,
|
||||
form_params: HashMap<String, String>,
|
||||
header_params: HashMap<String, String>,
|
||||
// TODO: multiple body params are possible technically, but not supported here.
|
||||
serialized_body: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Request {
|
||||
pub fn new(method: hyper::Method, path: String) -> Self {
|
||||
Request {
|
||||
auth: None,
|
||||
method,
|
||||
path,
|
||||
query_params: HashMap::new(),
|
||||
path_params: HashMap::new(),
|
||||
form_params: HashMap::new(),
|
||||
header_params: HashMap::new(),
|
||||
serialized_body: None,
|
||||
no_return_type: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_body_param<T: serde::Serialize>(mut self, param: T) -> Self {
|
||||
self.serialized_body = Some(serde_json::to_string(¶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<C>,
|
||||
) -> Pin<Box<dyn Future<Output=Result<U, Error>> + 'a>>
|
||||
where
|
||||
C: hyper::client::connect::Connect + Clone + std::marker::Send + Sync,
|
||||
U: Sized + std::marker::Send + 'a,
|
||||
for<'de> U: serde::Deserialize<'de>,
|
||||
{
|
||||
let mut query_string = ::url::form_urlencoded::Serializer::new("".to_owned());
|
||||
|
||||
let mut path = self.path;
|
||||
for (k, v) in self.path_params {
|
||||
// replace {id} with the value of the id path param
|
||||
path = path.replace(&format!("{{{}}}", k), &v);
|
||||
}
|
||||
|
||||
for (key, val) in self.query_params {
|
||||
query_string.append_pair(&key, &val);
|
||||
}
|
||||
|
||||
let mut uri_str = format!("{}{}", conf.base_path, path);
|
||||
|
||||
let query_string_str = query_string.finish();
|
||||
if query_string_str != "" {
|
||||
uri_str += "?";
|
||||
uri_str += &query_string_str;
|
||||
}
|
||||
let uri: hyper::Uri = match uri_str.parse() {
|
||||
Err(e) => return Box::pin(futures::future::err(Error::UriError(e))),
|
||||
Ok(u) => u,
|
||||
};
|
||||
|
||||
let mut req_builder = hyper::Request::builder()
|
||||
.uri(uri)
|
||||
.method(self.method);
|
||||
|
||||
// Detect the authorization type if it hasn't been set.
|
||||
let auth = self.auth.unwrap_or_else(||
|
||||
if conf.api_key.is_some() {
|
||||
panic!("Cannot automatically set the API key from the configuration, it must be specified in the OpenAPI definition")
|
||||
} else if conf.oauth_access_token.is_some() {
|
||||
Auth::Oauth
|
||||
} else if conf.basic_auth.is_some() {
|
||||
Auth::Basic
|
||||
} else {
|
||||
Auth::None
|
||||
}
|
||||
);
|
||||
match auth {
|
||||
Auth::ApiKey(apikey) => {
|
||||
if let Some(ref key) = conf.api_key {
|
||||
let val = apikey.key(&key.prefix, &key.key);
|
||||
if apikey.in_query {
|
||||
query_string.append_pair(&apikey.param_name, &val);
|
||||
}
|
||||
if apikey.in_header {
|
||||
req_builder = req_builder.header(&apikey.param_name, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
Auth::Basic => {
|
||||
if let Some(ref auth_conf) = conf.basic_auth {
|
||||
let mut text = auth_conf.0.clone();
|
||||
text.push(':');
|
||||
if let Some(ref pass) = auth_conf.1 {
|
||||
text.push_str(&pass[..]);
|
||||
}
|
||||
let encoded = MIME_ENCODER.encode(&text);
|
||||
req_builder = req_builder.header(AUTHORIZATION, encoded);
|
||||
}
|
||||
}
|
||||
Auth::Oauth => {
|
||||
if let Some(ref token) = conf.oauth_access_token {
|
||||
let text = "Bearer ".to_owned() + token;
|
||||
req_builder = req_builder.header(AUTHORIZATION, text);
|
||||
}
|
||||
}
|
||||
Auth::None => {}
|
||||
}
|
||||
|
||||
if let Some(ref user_agent) = conf.user_agent {
|
||||
req_builder = req_builder.header(USER_AGENT, match HeaderValue::from_str(user_agent) {
|
||||
Ok(header_value) => header_value,
|
||||
Err(e) => return Box::pin(futures::future::err(super::Error::Header(e)))
|
||||
});
|
||||
}
|
||||
|
||||
for (k, v) in self.header_params {
|
||||
req_builder = req_builder.header(&k, v);
|
||||
}
|
||||
|
||||
let req_headers = req_builder.headers_mut().unwrap();
|
||||
let request_result = if self.form_params.len() > 0 {
|
||||
req_headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/x-www-form-urlencoded"));
|
||||
let mut enc = ::url::form_urlencoded::Serializer::new("".to_owned());
|
||||
for (k, v) in self.form_params {
|
||||
enc.append_pair(&k, &v);
|
||||
}
|
||||
req_builder.body(hyper::Body::from(enc.finish()))
|
||||
} else if let Some(body) = self.serialized_body {
|
||||
req_headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
|
||||
req_headers.insert(CONTENT_LENGTH, body.len().into());
|
||||
req_builder.body(hyper::Body::from(body))
|
||||
} else {
|
||||
req_builder.body(hyper::Body::default())
|
||||
};
|
||||
let request = match request_result {
|
||||
Ok(request) => request,
|
||||
Err(e) => return Box::pin(futures::future::err(Error::from(e)))
|
||||
};
|
||||
|
||||
let no_return_type = self.no_return_type;
|
||||
Box::pin(conf.client
|
||||
.request(request)
|
||||
.map_err(|e| Error::from(e))
|
||||
.and_then(move |response| {
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
futures::future::err::<U, Error>(Error::from((status, response.into_body()))).boxed()
|
||||
} else if no_return_type {
|
||||
// This is a hack; if there's no_ret_type, U is (), but serde_json gives an
|
||||
// error when deserializing "" into (), so deserialize 'null' into it
|
||||
// instead.
|
||||
// An alternate option would be to require U: Default, and then return
|
||||
// U::default() here instead since () implements that, but then we'd
|
||||
// need to impl default for all models.
|
||||
futures::future::ok::<U, Error>(serde_json::from_str("null").expect("serde null value")).boxed()
|
||||
} else {
|
||||
hyper::body::to_bytes(response.into_body())
|
||||
.map(|bytes| serde_json::from_slice(&bytes.unwrap()))
|
||||
.map_err(|e| Error::from(e)).boxed()
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
18
tools/unitctl/unit-openapi/src/apis/error.rs
Normal file
18
tools/unitctl/unit-openapi/src/apis/error.rs
Normal file
|
@ -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 {}
|
12
tools/unitctl/unit-openapi/src/lib.rs
Normal file
12
tools/unitctl/unit-openapi/src/lib.rs
Normal file
|
@ -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;
|
56
tools/unitctl/unitctl/Cargo.toml
Normal file
56
tools/unitctl/unitctl/Cargo.toml
Normal file
|
@ -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" },
|
||||
]
|
105
tools/unitctl/unitctl/src/cmd/edit.rs
Normal file
105
tools/unitctl/unitctl/src/cmd/edit.rs
Normal file
|
@ -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<PathBuf> {
|
||||
EDITOR_ENV_VARS
|
||||
.iter()
|
||||
.filter_map(std::env::var_os)
|
||||
.filter(|s| !s.is_empty())
|
||||
.filter_map(|s| which(s).ok())
|
||||
.filter_map(|path| path.canonicalize().ok())
|
||||
.find(|path| path.exists())
|
||||
}
|
||||
|
||||
/// Look for editor in path by matching against a list of known editors or aliases
|
||||
fn find_editor_from_known_list() -> Option<PathBuf> {
|
||||
EDITOR_KNOWN_LIST
|
||||
.iter()
|
||||
.filter_map(|editor| which(editor).ok())
|
||||
.filter_map(|path| path.canonicalize().ok())
|
||||
.find(|editor| editor.exists())
|
||||
}
|
||||
|
||||
/// Find the path to an editor
|
||||
pub fn find_editor_path() -> Result<PathBuf, UnitctlError> {
|
||||
find_editor_from_env()
|
||||
.or_else(find_editor_from_known_list)
|
||||
.ok_or_else(|| UnitctlError::EditorError {
|
||||
message: "Could not find an editor".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Start an editor with a given path
|
||||
pub fn open_editor(path: &Path) -> Result<(), UnitctlError> {
|
||||
let editor_path = find_editor_path()?;
|
||||
let status = std::process::Command::new(editor_path)
|
||||
.arg(path)
|
||||
.status()
|
||||
.map_err(|e| UnitctlError::EditorError {
|
||||
message: format!("Could not open editor: {}", e),
|
||||
})?;
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(UnitctlError::EditorError {
|
||||
message: format!("Editor exited with non-zero status: {}", status),
|
||||
})
|
||||
}
|
||||
}
|
68
tools/unitctl/unitctl/src/cmd/execute.rs
Normal file
68
tools/unitctl/unitctl/src/cmd/execute.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
use crate::inputfile::InputFile;
|
||||
use crate::requests::{
|
||||
send_and_validate_config_deserialize_response, send_and_validate_pem_data_deserialize_response,
|
||||
send_body_deserialize_response, send_empty_body_deserialize_response,
|
||||
};
|
||||
use crate::unitctl::UnitCtl;
|
||||
use crate::wait;
|
||||
use crate::{OutputFormat, UnitctlError};
|
||||
use unit_client_rs::unit_client::UnitClient;
|
||||
|
||||
pub(crate) fn cmd(
|
||||
cli: &UnitCtl,
|
||||
output_format: &OutputFormat,
|
||||
input_file: &Option<String>,
|
||||
method: &str,
|
||||
path: &str,
|
||||
) -> Result<(), UnitctlError> {
|
||||
let control_socket = wait::wait_for_socket(cli)?;
|
||||
let client = UnitClient::new(control_socket);
|
||||
|
||||
let path_trimmed = path.trim();
|
||||
let method_upper = method.to_uppercase();
|
||||
let input_file_arg = input_file
|
||||
.as_ref()
|
||||
.map(|file| InputFile::new(file, &path_trimmed.to_string()));
|
||||
|
||||
if method_upper.eq("GET") && input_file.is_some() {
|
||||
eprintln!("Cannot use GET method with input file - ignoring input file");
|
||||
}
|
||||
|
||||
send_and_deserialize(client, method_upper, input_file_arg, path_trimmed, output_format)
|
||||
}
|
||||
|
||||
fn send_and_deserialize(
|
||||
client: UnitClient,
|
||||
method: String,
|
||||
input_file: Option<InputFile>,
|
||||
path: &str,
|
||||
output_format: &OutputFormat,
|
||||
) -> Result<(), UnitctlError> {
|
||||
let is_js_modules_dir = path.starts_with("/js_modules/") || path.starts_with("js_modules/");
|
||||
|
||||
// If we are sending a GET request to a JS modules directory, we want to print the contents of the JS file
|
||||
// instead of the JSON response
|
||||
if method.eq("GET") && is_js_modules_dir && path.ends_with(".js") {
|
||||
let script = send_body_deserialize_response::<String>(&client, method.as_str(), path, input_file.as_ref())?;
|
||||
println!("{}", script);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Otherwise, we want to print the JSON response (a map) as represented by the output format
|
||||
match input_file {
|
||||
Some(input_file) => {
|
||||
if input_file.is_config() {
|
||||
send_and_validate_config_deserialize_response(&client, method.as_str(), path, Some(&input_file))
|
||||
// TLS certificate data
|
||||
} else if input_file.is_pem_bundle() {
|
||||
send_and_validate_pem_data_deserialize_response(&client, method.as_str(), path, &input_file)
|
||||
// This is unknown data
|
||||
} else {
|
||||
panic!("Unknown input file type")
|
||||
}
|
||||
}
|
||||
// A none value for an input file can be considered a request to send an empty body
|
||||
None => send_empty_body_deserialize_response(&client, method.as_str(), path),
|
||||
}
|
||||
.and_then(|status| output_format.write_to_stdout(&status))
|
||||
}
|
124
tools/unitctl/unitctl/src/cmd/import.rs
Normal file
124
tools/unitctl/unitctl/src/cmd/import.rs
Normal file
|
@ -0,0 +1,124 @@
|
|||
use crate::inputfile::{InputFile, InputFormat};
|
||||
use crate::unitctl::UnitCtl;
|
||||
use crate::unitctl_error::UnitctlError;
|
||||
use crate::{requests, wait};
|
||||
use std::path::{Path, PathBuf};
|
||||
use unit_client_rs::unit_client::{UnitClient, UnitSerializableMap};
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
|
||||
enum UploadFormat {
|
||||
Config,
|
||||
PemBundle,
|
||||
Javascript,
|
||||
}
|
||||
|
||||
impl From<&InputFile> for UploadFormat {
|
||||
fn from(input_file: &InputFile) -> Self {
|
||||
if input_file.is_config() {
|
||||
UploadFormat::Config
|
||||
} else if input_file.is_pem_bundle() {
|
||||
UploadFormat::PemBundle
|
||||
} else if input_file.is_javascript() {
|
||||
UploadFormat::Javascript
|
||||
} else {
|
||||
panic!("Unknown input file type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UploadFormat {
|
||||
fn can_be_overwritten(&self) -> bool {
|
||||
matches!(self, UploadFormat::Config)
|
||||
}
|
||||
fn upload_path(&self, path: &Path) -> String {
|
||||
match self {
|
||||
UploadFormat::Config => "/config".to_string(),
|
||||
UploadFormat::PemBundle => format!("/certificates/{}.pem", Self::file_stem(path)),
|
||||
UploadFormat::Javascript => format!("/js_modules/{}.js", Self::file_stem(path)),
|
||||
}
|
||||
}
|
||||
|
||||
fn file_stem(path: &Path) -> String {
|
||||
path.file_stem().unwrap_or_default().to_string_lossy().into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cmd(cli: &UnitCtl, directory: &PathBuf) -> Result<(), UnitctlError> {
|
||||
if !directory.exists() {
|
||||
return Err(UnitctlError::PathNotFound {
|
||||
path: directory.to_string_lossy().into(),
|
||||
});
|
||||
}
|
||||
|
||||
let control_socket = wait::wait_for_socket(cli)?;
|
||||
let client = UnitClient::new(control_socket);
|
||||
|
||||
let results: Vec<Result<(), UnitctlError>> = WalkDir::new(directory)
|
||||
.follow_links(true)
|
||||
.sort_by_file_name()
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|e| !e.path().is_dir())
|
||||
.map(|pe| process_entry(pe, &client))
|
||||
.collect();
|
||||
|
||||
if results.iter().filter(|r| r.is_err()).count() == results.len() {
|
||||
Err(UnitctlError::NoFilesImported)
|
||||
} else {
|
||||
println!("Imported {} files", results.len());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn process_entry(entry: DirEntry, client: &UnitClient) -> Result<(), UnitctlError> {
|
||||
let input_file = InputFile::from(entry.path());
|
||||
if input_file.format() == InputFormat::Unknown {
|
||||
println!(
|
||||
"Skipping unknown file type: {}",
|
||||
input_file.to_path()?.to_string_lossy()
|
||||
);
|
||||
return Err(UnitctlError::UnknownInputFileType {
|
||||
path: input_file.to_path()?.to_string_lossy().into(),
|
||||
});
|
||||
}
|
||||
let upload_format = UploadFormat::from(&input_file);
|
||||
let upload_path = upload_format.upload_path(entry.path());
|
||||
|
||||
// We can't overwrite JS or PEM files, so we delete them first
|
||||
if !upload_format.can_be_overwritten() {
|
||||
let _ = requests::send_empty_body_deserialize_response(client, "DELETE", upload_path.as_str()).ok();
|
||||
}
|
||||
|
||||
let result = match upload_format {
|
||||
UploadFormat::Config => requests::send_and_validate_config_deserialize_response(
|
||||
client,
|
||||
"PUT",
|
||||
upload_path.as_str(),
|
||||
Some(&input_file),
|
||||
),
|
||||
UploadFormat::PemBundle => {
|
||||
requests::send_and_validate_pem_data_deserialize_response(client, "PUT", upload_path.as_str(), &input_file)
|
||||
}
|
||||
UploadFormat::Javascript => requests::send_body_deserialize_response::<UnitSerializableMap>(
|
||||
client,
|
||||
"PUT",
|
||||
upload_path.as_str(),
|
||||
Some(&input_file),
|
||||
),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
eprintln!(
|
||||
"Imported {} -> {}",
|
||||
input_file.to_path()?.to_string_lossy(),
|
||||
upload_path
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("Error {} -> {}", input_file.to_path()?.to_string_lossy(), error);
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
16
tools/unitctl/unitctl/src/cmd/instances.rs
Normal file
16
tools/unitctl/unitctl/src/cmd/instances.rs
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
13
tools/unitctl/unitctl/src/cmd/listeners.rs
Normal file
13
tools/unitctl/unitctl/src/cmd/listeners.rs
Normal file
|
@ -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))
|
||||
}
|
6
tools/unitctl/unitctl/src/cmd/mod.rs
Normal file
6
tools/unitctl/unitctl/src/cmd/mod.rs
Normal file
|
@ -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;
|
13
tools/unitctl/unitctl/src/cmd/status.rs
Normal file
13
tools/unitctl/unitctl/src/cmd/status.rs
Normal file
|
@ -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))
|
||||
}
|
289
tools/unitctl/unitctl/src/inputfile.rs
Normal file
289
tools/unitctl/unitctl/src/inputfile.rs
Normal file
|
@ -0,0 +1,289 @@
|
|||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
use std::io::{BufRead, BufReader, Error as IoError, Read};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::known_size::KnownSize;
|
||||
use clap::ValueEnum;
|
||||
|
||||
use super::UnitSerializableMap;
|
||||
use super::UnitctlError;
|
||||
|
||||
/// Input file data format
|
||||
#[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum InputFormat {
|
||||
Yaml,
|
||||
Json,
|
||||
Json5,
|
||||
Hjson,
|
||||
Pem,
|
||||
JavaScript,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl InputFormat {
|
||||
pub fn from_file_extension<S>(file_extension: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
match file_extension.into().to_lowercase().as_str() {
|
||||
"yaml" => InputFormat::Yaml,
|
||||
"yml" => InputFormat::Yaml,
|
||||
"json" => InputFormat::Json,
|
||||
"json5" => InputFormat::Json5,
|
||||
"hjson" => InputFormat::Hjson,
|
||||
"cjson" => InputFormat::Hjson,
|
||||
"pem" => InputFormat::Pem,
|
||||
"js" => InputFormat::JavaScript,
|
||||
"njs" => InputFormat::JavaScript,
|
||||
_ => InputFormat::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
/// This function allows us to infer the input format based on the remote path which is
|
||||
/// useful when processing input from STDIN.
|
||||
pub fn from_remote_path<S>(remote_path: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
let remote_upload_path = remote_path.into();
|
||||
let lead_slash_removed = remote_upload_path.trim_start_matches('/');
|
||||
let first_path = lead_slash_removed
|
||||
.split_once('/')
|
||||
.map_or(lead_slash_removed, |(first, _)| first);
|
||||
match first_path {
|
||||
"config" => InputFormat::Hjson,
|
||||
"certificates" => InputFormat::Pem,
|
||||
"js_modules" => InputFormat::JavaScript,
|
||||
_ => InputFormat::Json,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A "file" that can be used as input to a command
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum InputFile {
|
||||
// Data received via STDIN
|
||||
Stdin(InputFormat),
|
||||
// Data that is on the file system where the format is inferred from the extension
|
||||
File(Box<Path>),
|
||||
// Data that is on the file system where the format is explicitly specified
|
||||
FileWithFormat(Box<Path>, InputFormat),
|
||||
}
|
||||
|
||||
impl InputFile {
|
||||
/// Creates a new instance of `InputFile` from a string
|
||||
pub fn new<S>(file_path_or_dash: S, remote_path: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
let file_path: String = file_path_or_dash.into();
|
||||
|
||||
match file_path.as_str() {
|
||||
"-" => InputFile::Stdin(InputFormat::from_remote_path(remote_path)),
|
||||
_ => InputFile::File(PathBuf::from(&file_path).into_boxed_path()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the format of the input file
|
||||
pub fn format(&self) -> InputFormat {
|
||||
match self {
|
||||
InputFile::Stdin(format) => *format,
|
||||
InputFile::File(path) => {
|
||||
// Figure out the file format based on the file extension
|
||||
match path.extension().and_then(|s| s.to_str()) {
|
||||
Some(ext) => InputFormat::from_file_extension(ext),
|
||||
None => InputFormat::Unknown,
|
||||
}
|
||||
}
|
||||
InputFile::FileWithFormat(_file, format) => *format,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mime_type(&self) -> String {
|
||||
match self.format() {
|
||||
InputFormat::Yaml => "application/x-yaml".to_string(),
|
||||
InputFormat::Json => "application/json".to_string(),
|
||||
InputFormat::Json5 => "application/json5".to_string(),
|
||||
InputFormat::Hjson => "application/hjson".to_string(),
|
||||
InputFormat::Pem => "application/x-pem-file".to_string(),
|
||||
InputFormat::JavaScript => "application/javascript".to_string(),
|
||||
InputFormat::Unknown => "application/octet-stream".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the input file is in the format of a configuration file
|
||||
pub fn is_config(&self) -> bool {
|
||||
matches!(
|
||||
self.format(),
|
||||
InputFormat::Yaml | InputFormat::Json | InputFormat::Json5 | InputFormat::Hjson
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_javascript(&self) -> bool {
|
||||
matches!(self.format(), InputFormat::JavaScript)
|
||||
}
|
||||
|
||||
pub fn is_pem_bundle(&self) -> bool {
|
||||
matches!(self.format(), InputFormat::Pem)
|
||||
}
|
||||
|
||||
/// Returns the path to the input file if it is a file and not a stream
|
||||
pub fn to_path(&self) -> Result<&Path, UnitctlError> {
|
||||
match self {
|
||||
InputFile::Stdin(_) => {
|
||||
let io_error = IoError::new(std::io::ErrorKind::InvalidInput, "Input file is stdin");
|
||||
Err(UnitctlError::IoError { source: io_error })
|
||||
}
|
||||
InputFile::File(path) | InputFile::FileWithFormat(path, _) => Ok(path),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a HJSON Value type to a JSON Value type
|
||||
fn hjson_value_to_json_value(value: nu_json::Value) -> serde_json::Value {
|
||||
serde_json::to_value(value).expect("Failed to convert HJSON value to JSON value")
|
||||
}
|
||||
|
||||
pub fn to_unit_serializable_map(&self) -> Result<UnitSerializableMap, UnitctlError> {
|
||||
let reader: Box<dyn BufRead + Send> = self.try_into()?;
|
||||
let body_data: UnitSerializableMap = match self.format() {
|
||||
InputFormat::Yaml => serde_yaml::from_reader(reader)
|
||||
.map_err(|e| UnitctlError::DeserializationError { message: e.to_string() })?,
|
||||
InputFormat::Json => serde_json::from_reader(reader)
|
||||
.map_err(|e| UnitctlError::DeserializationError { message: e.to_string() })?,
|
||||
InputFormat::Json5 => {
|
||||
let mut reader = BufReader::new(reader);
|
||||
let mut json5_string: String = String::new();
|
||||
reader
|
||||
.read_to_string(&mut json5_string)
|
||||
.map_err(|e| UnitctlError::DeserializationError { message: e.to_string() })?;
|
||||
json5::from_str(&json5_string)
|
||||
.map_err(|e| UnitctlError::DeserializationError { message: e.to_string() })?
|
||||
}
|
||||
InputFormat::Hjson => {
|
||||
let hjson_value: HashMap<String, nu_json::Value> = nu_json::from_reader(reader)
|
||||
.map_err(|e| UnitctlError::DeserializationError { message: e.to_string() })?;
|
||||
|
||||
hjson_value
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
let json_value = Self::hjson_value_to_json_value(v.clone());
|
||||
(k.clone(), json_value)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
_ => Err(UnitctlError::DeserializationError {
|
||||
message: format!("Unsupported input format for serialization: {:?}", self),
|
||||
})?,
|
||||
};
|
||||
Ok(body_data)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Path> for InputFile {
|
||||
fn from(path: &Path) -> Self {
|
||||
InputFile::File(path.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<Box<dyn BufRead + Send>> for &InputFile {
|
||||
type Error = UnitctlError;
|
||||
|
||||
fn try_into(self) -> Result<Box<dyn BufRead + Send>, Self::Error> {
|
||||
let reader: Box<dyn BufRead + Send> = match self {
|
||||
InputFile::Stdin(_) => Box::new(BufReader::new(io::stdin())),
|
||||
InputFile::File(_) | InputFile::FileWithFormat(_, _) => {
|
||||
let path = self.to_path()?;
|
||||
let file = std::fs::File::open(path).map_err(|e| UnitctlError::IoError { source: e })?;
|
||||
let reader = Box::new(BufReader::new(file));
|
||||
Box::new(reader)
|
||||
}
|
||||
};
|
||||
Ok(reader)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<Vec<u8>> for &InputFile {
|
||||
type Error = UnitctlError;
|
||||
|
||||
fn try_into(self) -> Result<Vec<u8>, Self::Error> {
|
||||
let mut buf: Vec<u8> = vec![];
|
||||
let mut reader: Box<dyn BufRead + Send> = self.try_into()?;
|
||||
reader
|
||||
.read_to_end(&mut buf)
|
||||
.map_err(|e| UnitctlError::IoError { source: e })?;
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<KnownSize> for &InputFile {
|
||||
type Error = UnitctlError;
|
||||
|
||||
fn try_into(self) -> Result<KnownSize, Self::Error> {
|
||||
let known_size: KnownSize = match self {
|
||||
InputFile::Stdin(_) => {
|
||||
let mut buf: Vec<u8> = vec![];
|
||||
let _ = io::stdin()
|
||||
.read_to_end(&mut buf)
|
||||
.map_err(|e| UnitctlError::IoError { source: e })?;
|
||||
KnownSize::Vec(buf)
|
||||
}
|
||||
InputFile::File(_) | InputFile::FileWithFormat(_, _) => {
|
||||
let path = self.to_path()?;
|
||||
let file = std::fs::File::open(path).map_err(|e| UnitctlError::IoError { source: e })?;
|
||||
let len = file.metadata().map_err(|e| UnitctlError::IoError { source: e })?.len();
|
||||
let reader = Box::new(file);
|
||||
KnownSize::Read(reader, len)
|
||||
}
|
||||
};
|
||||
Ok(known_size)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn can_parse_file_extensions() {
|
||||
assert_eq!(InputFormat::from_file_extension("yaml"), InputFormat::Yaml);
|
||||
assert_eq!(InputFormat::from_file_extension("yml"), InputFormat::Yaml);
|
||||
assert_eq!(InputFormat::from_file_extension("json"), InputFormat::Json);
|
||||
assert_eq!(InputFormat::from_file_extension("json5"), InputFormat::Json5);
|
||||
assert_eq!(InputFormat::from_file_extension("pem"), InputFormat::Pem);
|
||||
assert_eq!(InputFormat::from_file_extension("js"), InputFormat::JavaScript);
|
||||
assert_eq!(InputFormat::from_file_extension("njs"), InputFormat::JavaScript);
|
||||
assert_eq!(InputFormat::from_file_extension("txt"), InputFormat::Unknown);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_parse_remote_paths() {
|
||||
assert_eq!(InputFormat::from_remote_path("//config"), InputFormat::Hjson);
|
||||
assert_eq!(InputFormat::from_remote_path("/config"), InputFormat::Hjson);
|
||||
assert_eq!(InputFormat::from_remote_path("/config/"), InputFormat::Hjson);
|
||||
assert_eq!(InputFormat::from_remote_path("config/"), InputFormat::Hjson);
|
||||
assert_eq!(InputFormat::from_remote_path("config"), InputFormat::Hjson);
|
||||
assert_eq!(InputFormat::from_remote_path("/config/something/"), InputFormat::Hjson);
|
||||
assert_eq!(InputFormat::from_remote_path("config/something/"), InputFormat::Hjson);
|
||||
assert_eq!(InputFormat::from_remote_path("config/something"), InputFormat::Hjson);
|
||||
assert_eq!(InputFormat::from_remote_path("/certificates"), InputFormat::Pem);
|
||||
assert_eq!(InputFormat::from_remote_path("/certificates/"), InputFormat::Pem);
|
||||
assert_eq!(InputFormat::from_remote_path("certificates/"), InputFormat::Pem);
|
||||
assert_eq!(InputFormat::from_remote_path("certificates"), InputFormat::Pem);
|
||||
assert_eq!(InputFormat::from_remote_path("js_modules"), InputFormat::JavaScript);
|
||||
assert_eq!(InputFormat::from_remote_path("js_modules/"), InputFormat::JavaScript);
|
||||
|
||||
assert_eq!(
|
||||
InputFormat::from_remote_path("/certificates/something/"),
|
||||
InputFormat::Pem
|
||||
);
|
||||
assert_eq!(
|
||||
InputFormat::from_remote_path("certificates/something/"),
|
||||
InputFormat::Pem
|
||||
);
|
||||
assert_eq!(
|
||||
InputFormat::from_remote_path("certificates/something"),
|
||||
InputFormat::Pem
|
||||
);
|
||||
}
|
||||
}
|
77
tools/unitctl/unitctl/src/known_size.rs
Normal file
77
tools/unitctl/unitctl/src/known_size.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
use futures::Stream;
|
||||
use hyper::Body;
|
||||
use std::io;
|
||||
use std::io::{Cursor, Read};
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
pub enum KnownSize {
|
||||
Vec(Vec<u8>),
|
||||
Read(Box<dyn Read + Send>, u64),
|
||||
String(String),
|
||||
Empty,
|
||||
}
|
||||
|
||||
impl KnownSize {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
pub fn len(&self) -> u64 {
|
||||
match self {
|
||||
KnownSize::Vec(v) => v.len() as u64,
|
||||
KnownSize::Read(_, size) => *size,
|
||||
KnownSize::String(s) => s.len() as u64,
|
||||
KnownSize::Empty => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for KnownSize {
|
||||
type Item = io::Result<Vec<u8>>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let buf = &mut [0u8; 1024];
|
||||
|
||||
if let KnownSize::Read(r, _) = self.get_mut() {
|
||||
return match r.read(buf) {
|
||||
Ok(0) => Poll::Ready(None),
|
||||
Ok(n) => Poll::Ready(Some(Ok(buf[..n].to_vec()))),
|
||||
Err(e) => Poll::Ready(Some(Err(e))),
|
||||
};
|
||||
}
|
||||
|
||||
panic!("not implemented")
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
(0, Some(self.len() as usize))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KnownSize> for Box<dyn Read + Send> {
|
||||
fn from(value: KnownSize) -> Self {
|
||||
match value {
|
||||
KnownSize::Vec(v) => Box::new(Cursor::new(v)),
|
||||
KnownSize::Read(r, _) => r,
|
||||
KnownSize::String(s) => Box::new(Cursor::new(s)),
|
||||
KnownSize::Empty => Box::new(Cursor::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KnownSize> for Body {
|
||||
fn from(value: KnownSize) -> Self {
|
||||
if value.is_empty() {
|
||||
return Body::empty();
|
||||
}
|
||||
if let KnownSize::Vec(v) = value {
|
||||
return Body::from(v);
|
||||
}
|
||||
if let KnownSize::String(s) = value {
|
||||
return Body::from(s);
|
||||
}
|
||||
|
||||
Body::wrap_stream(value)
|
||||
}
|
||||
}
|
101
tools/unitctl/unitctl/src/main.rs
Normal file
101
tools/unitctl/unitctl/src/main.rs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
43
tools/unitctl/unitctl/src/output_format.rs
Normal file
43
tools/unitctl/unitctl/src/output_format.rs
Normal file
|
@ -0,0 +1,43 @@
|
|||
use crate::UnitctlError;
|
||||
use clap::ValueEnum;
|
||||
use colored_json::ColorMode;
|
||||
use serde::Serialize;
|
||||
use std::io::{stdout, BufWriter, Write};
|
||||
|
||||
#[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum OutputFormat {
|
||||
Yaml,
|
||||
Json,
|
||||
#[value(id = "json-pretty")]
|
||||
JsonPretty,
|
||||
Text,
|
||||
}
|
||||
|
||||
impl OutputFormat {
|
||||
pub fn write_to_stdout<T>(&self, object: &T) -> Result<(), UnitctlError>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
let no_color = std::env::var("NO_COLOR").map_or(false, |_| true);
|
||||
let mut out = stdout();
|
||||
let value =
|
||||
serde_json::to_value(object).map_err(|e| UnitctlError::SerializationError { message: e.to_string() })?;
|
||||
|
||||
match (self, no_color) {
|
||||
(OutputFormat::Yaml, _) => serde_yaml::to_writer(BufWriter::new(out), &value)
|
||||
.map_err(|e| UnitctlError::SerializationError { message: e.to_string() }),
|
||||
(OutputFormat::Json, _) => serde_json::to_writer(BufWriter::new(out), &value)
|
||||
.map_err(|e| UnitctlError::SerializationError { message: e.to_string() }),
|
||||
(OutputFormat::JsonPretty, true) => serde_json::to_writer_pretty(BufWriter::new(out), &value)
|
||||
.map_err(|e| UnitctlError::SerializationError { message: e.to_string() }),
|
||||
(OutputFormat::JsonPretty, false) => {
|
||||
let mode = ColorMode::Auto(colored_json::Output::StdOut);
|
||||
colored_json::write_colored_json_with_mode(&value, &mut out, mode)
|
||||
.map_err(|e| UnitctlError::SerializationError { message: e.to_string() })
|
||||
}
|
||||
(OutputFormat::Text, _) => stdout()
|
||||
.write_fmt(format_args!("{:?}", &value))
|
||||
.map_err(|e| UnitctlError::IoError { source: e }),
|
||||
}
|
||||
}
|
||||
}
|
175
tools/unitctl/unitctl/src/requests.rs
Normal file
175
tools/unitctl/unitctl/src/requests.rs
Normal file
|
@ -0,0 +1,175 @@
|
|||
use super::inputfile::InputFile;
|
||||
use super::UnitClient;
|
||||
use super::UnitSerializableMap;
|
||||
use super::UnitctlError;
|
||||
use crate::known_size::KnownSize;
|
||||
use hyper::{Body, Request};
|
||||
use rustls_pemfile::Item;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use unit_client_rs::unit_client::UnitClientError;
|
||||
|
||||
/// Send the contents of a file to the unit server
|
||||
/// We assume that the file is valid and can be sent to the server
|
||||
pub fn send_and_validate_config_deserialize_response(
|
||||
client: &UnitClient,
|
||||
method: &str,
|
||||
path: &str,
|
||||
input_file: Option<&InputFile>,
|
||||
) -> Result<UnitSerializableMap, UnitctlError> {
|
||||
let body_data = match input_file {
|
||||
Some(input) => Some(input.to_unit_serializable_map()?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
/* Unfortunately, we have load the json text into memory before sending it to the server.
|
||||
* This allows for validation of the json content before sending to the server. There may be
|
||||
* a better way of doing this and it is worth investigating. */
|
||||
let json = serde_json::to_value(&body_data).map_err(|error| UnitClientError::JsonError {
|
||||
source: error,
|
||||
path: path.into(),
|
||||
})?;
|
||||
|
||||
let mime_type = input_file.map(|f| f.mime_type());
|
||||
let reader = KnownSize::String(json.to_string());
|
||||
|
||||
streaming_upload_deserialize_response(client, method, path, mime_type, reader)
|
||||
.map_err(|e| UnitctlError::UnitClientError { source: e })
|
||||
}
|
||||
|
||||
/// Send an empty body to the unit server
|
||||
pub fn send_empty_body_deserialize_response(
|
||||
client: &UnitClient,
|
||||
method: &str,
|
||||
path: &str,
|
||||
) -> Result<UnitSerializableMap, UnitctlError> {
|
||||
send_body_deserialize_response(client, method, path, None)
|
||||
}
|
||||
|
||||
/// Send the contents of a PEM file to the unit server
|
||||
pub fn send_and_validate_pem_data_deserialize_response(
|
||||
client: &UnitClient,
|
||||
method: &str,
|
||||
path: &str,
|
||||
input_file: &InputFile,
|
||||
) -> Result<UnitSerializableMap, UnitctlError> {
|
||||
let bytes: Vec<u8> = input_file.try_into()?;
|
||||
{
|
||||
let mut cursor = Cursor::new(&bytes);
|
||||
let items = rustls_pemfile::read_all(&mut cursor)
|
||||
.map(|item| item.map_err(|e| UnitctlError::IoError { source: e }))
|
||||
.collect();
|
||||
validate_pem_items(items)?;
|
||||
}
|
||||
let known_size = KnownSize::Vec((*bytes).to_owned());
|
||||
|
||||
streaming_upload_deserialize_response(client, method, path, Some(input_file.mime_type()), known_size)
|
||||
.map_err(|e| UnitctlError::UnitClientError { source: e })
|
||||
}
|
||||
|
||||
/// Validate the contents of a PEM file
|
||||
fn validate_pem_items(pem_items: Vec<Result<Item, UnitctlError>>) -> Result<(), UnitctlError> {
|
||||
fn item_name(item: Item) -> String {
|
||||
match item {
|
||||
Item::X509Certificate(_) => "X509Certificate",
|
||||
Item::Sec1Key(_) => "Sec1Key",
|
||||
Item::Crl(_) => "Crl",
|
||||
Item::Pkcs1Key(_) => "Pkcs1Key",
|
||||
Item::Pkcs8Key(_) => "Pkcs8Key",
|
||||
// Note: this is not a valid PEM item, but rustls_pemfile library defines the enum as non-exhaustive
|
||||
_ => "Unknown",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
if pem_items.is_empty() {
|
||||
let error = UnitctlError::CertificateError {
|
||||
message: "No certificates found in file".to_string(),
|
||||
};
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
let mut items_tally: HashMap<String, AtomicUsize> = HashMap::new();
|
||||
|
||||
for pem_item_result in pem_items {
|
||||
let pem_item = pem_item_result?;
|
||||
let key = item_name(pem_item);
|
||||
if let Some(count) = items_tally.get_mut(key.clone().as_str()) {
|
||||
count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
} else {
|
||||
items_tally.insert(key, AtomicUsize::new(1));
|
||||
}
|
||||
}
|
||||
|
||||
let key_count = items_tally
|
||||
.iter()
|
||||
.filter(|(key, _)| key.ends_with("Key"))
|
||||
.fold(0, |acc, (_, count)| {
|
||||
acc + count.load(std::sync::atomic::Ordering::Relaxed)
|
||||
});
|
||||
let cert_count = items_tally
|
||||
.iter()
|
||||
.filter(|(key, _)| key.ends_with("Certificate"))
|
||||
.fold(0, |acc, (_, count)| {
|
||||
acc + count.load(std::sync::atomic::Ordering::Relaxed)
|
||||
});
|
||||
|
||||
if key_count == 0 {
|
||||
let error = UnitctlError::CertificateError {
|
||||
message: "No private keys found in file".to_string(),
|
||||
};
|
||||
return Err(error);
|
||||
}
|
||||
if cert_count == 0 {
|
||||
let error = UnitctlError::CertificateError {
|
||||
message: "No certificates found in file".to_string(),
|
||||
};
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_body_deserialize_response<RESPONSE: for<'de> serde::Deserialize<'de>>(
|
||||
client: &UnitClient,
|
||||
method: &str,
|
||||
path: &str,
|
||||
input_file: Option<&InputFile>,
|
||||
) -> Result<RESPONSE, UnitctlError> {
|
||||
match input_file {
|
||||
Some(input) => {
|
||||
streaming_upload_deserialize_response(client, method, path, Some(input.mime_type()), input.try_into()?)
|
||||
}
|
||||
None => streaming_upload_deserialize_response(client, method, path, None, KnownSize::Empty),
|
||||
}
|
||||
.map_err(|e| UnitctlError::UnitClientError { source: e })
|
||||
}
|
||||
|
||||
fn streaming_upload_deserialize_response<RESPONSE: for<'de> serde::Deserialize<'de>>(
|
||||
client: &UnitClient,
|
||||
method: &str,
|
||||
path: &str,
|
||||
mime_type: Option<String>,
|
||||
read: KnownSize,
|
||||
) -> Result<RESPONSE, UnitClientError> {
|
||||
let uri = client.control_socket.create_uri_with_path(path);
|
||||
|
||||
let content_length = read.len();
|
||||
let body = Body::from(read);
|
||||
|
||||
let mut request = Request::builder()
|
||||
.method(method)
|
||||
.header("Content-Length", content_length)
|
||||
.uri(uri)
|
||||
.body(body)
|
||||
.expect("Unable to build request");
|
||||
|
||||
if let Some(content_type) = mime_type {
|
||||
request
|
||||
.headers_mut()
|
||||
.insert("Content-Type", content_type.parse().unwrap());
|
||||
}
|
||||
|
||||
client.send_request_and_deserialize_response(request)
|
||||
}
|
144
tools/unitctl/unitctl/src/unitctl.rs
Normal file
144
tools/unitctl/unitctl/src/unitctl.rs
Normal file
|
@ -0,0 +1,144 @@
|
|||
extern crate clap;
|
||||
|
||||
use crate::output_format::OutputFormat;
|
||||
use clap::error::ErrorKind::ValueValidation;
|
||||
use clap::{Error as ClapError, Parser, Subcommand};
|
||||
use std::path::PathBuf;
|
||||
use unit_client_rs::control_socket_address::ControlSocket;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about)]
|
||||
pub(crate) struct UnitCtl {
|
||||
#[arg(
|
||||
required = false,
|
||||
short = 's',
|
||||
long = "control-socket-address",
|
||||
value_parser = parse_control_socket_address,
|
||||
help = "Path (unix:/var/run/unit/control.sock), tcp address with port (127.0.0.1:80), or URL"
|
||||
)]
|
||||
pub(crate) control_socket_address: Option<ControlSocket>,
|
||||
#[arg(
|
||||
required = false,
|
||||
default_missing_value = "1",
|
||||
value_parser = parse_u8,
|
||||
short = 'w',
|
||||
long = "wait-timeout-seconds",
|
||||
help = "Number of seconds to wait for control socket to become available"
|
||||
)]
|
||||
pub(crate) wait_time_seconds: Option<u8>,
|
||||
#[arg(
|
||||
required = false,
|
||||
default_value = "3",
|
||||
value_parser = parse_u8,
|
||||
short = 't',
|
||||
long = "wait-max-tries",
|
||||
help = "Number of times to try to access control socket when waiting"
|
||||
)]
|
||||
pub(crate) wait_max_tries: Option<u8>,
|
||||
#[command(subcommand)]
|
||||
pub(crate) command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub(crate) enum Commands {
|
||||
#[command(about = "List all running UNIT processes")]
|
||||
Instances {
|
||||
#[arg(
|
||||
required = false,
|
||||
global = true,
|
||||
short = 't',
|
||||
long = "output-format",
|
||||
default_value = "text",
|
||||
help = "Output format: text, yaml, json, json-pretty (default)"
|
||||
)]
|
||||
output_format: OutputFormat,
|
||||
},
|
||||
#[command(about = "Open current UNIT configuration in editor")]
|
||||
Edit {
|
||||
#[arg(
|
||||
required = false,
|
||||
global = true,
|
||||
short = 't',
|
||||
long = "output-format",
|
||||
default_value = "json-pretty",
|
||||
help = "Output format: yaml, json, json-pretty (default)"
|
||||
)]
|
||||
output_format: OutputFormat,
|
||||
},
|
||||
#[command(about = "Import configuration from a directory")]
|
||||
Import {
|
||||
#[arg(required = true, help = "Directory to import from")]
|
||||
directory: PathBuf,
|
||||
},
|
||||
#[command(about = "Sends raw JSON payload to UNIT")]
|
||||
Execute {
|
||||
#[arg(
|
||||
required = false,
|
||||
global = true,
|
||||
short = 't',
|
||||
long = "output-format",
|
||||
default_value = "json-pretty",
|
||||
help = "Output format: yaml, json, json-pretty (default)"
|
||||
)]
|
||||
output_format: OutputFormat,
|
||||
#[arg(
|
||||
required = false,
|
||||
global = true,
|
||||
short = 'f',
|
||||
long = "file",
|
||||
help = "Input file (json, json5, cjson, hjson yaml, pem) to send to unit when applicable use - for stdin"
|
||||
)]
|
||||
input_file: Option<String>,
|
||||
#[arg(
|
||||
help = "HTTP method to use (GET, POST, PUT, DELETE)",
|
||||
required = true,
|
||||
short = 'm',
|
||||
long = "http-method",
|
||||
value_parser = parse_http_method,
|
||||
)]
|
||||
method: String,
|
||||
#[arg(required = true, short = 'p', long = "path")]
|
||||
path: String,
|
||||
},
|
||||
#[command(about = "Get the current status of UNIT")]
|
||||
Status {
|
||||
#[arg(
|
||||
required = false,
|
||||
global = true,
|
||||
short = 't',
|
||||
long = "output-format",
|
||||
default_value = "json-pretty",
|
||||
help = "Output format: yaml, json, json-pretty (default)"
|
||||
)]
|
||||
output_format: OutputFormat,
|
||||
},
|
||||
#[command(about = "List active listeners")]
|
||||
Listeners {
|
||||
#[arg(
|
||||
required = false,
|
||||
global = true,
|
||||
short = 't',
|
||||
long = "output-format",
|
||||
default_value = "json-pretty",
|
||||
help = "Output format: yaml, json, json-pretty (default)"
|
||||
)]
|
||||
output_format: OutputFormat,
|
||||
},
|
||||
}
|
||||
|
||||
fn parse_control_socket_address(s: &str) -> Result<ControlSocket, ClapError> {
|
||||
ControlSocket::try_from(s).map_err(|e| ClapError::raw(ValueValidation, e.to_string()))
|
||||
}
|
||||
|
||||
fn parse_http_method(s: &str) -> Result<String, ClapError> {
|
||||
let method = s.to_uppercase();
|
||||
match method.as_str() {
|
||||
"GET" | "POST" | "PUT" | "DELETE" => Ok(method),
|
||||
_ => Err(ClapError::raw(ValueValidation, format!("Invalid HTTP method: {}", s))),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_u8(s: &str) -> Result<u8, ClapError> {
|
||||
s.parse::<u8>()
|
||||
.map_err(|e| ClapError::raw(ValueValidation, format!("Invalid number: {}", e)))
|
||||
}
|
72
tools/unitctl/unitctl/src/unitctl_error.rs
Normal file
72
tools/unitctl/unitctl/src/unitctl_error.rs
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
165
tools/unitctl/unitctl/src/wait.rs
Normal file
165
tools/unitctl/unitctl/src/wait.rs
Normal file
|
@ -0,0 +1,165 @@
|
|||
use crate::unitctl::UnitCtl;
|
||||
use crate::unitctl_error::{ControlSocketErrorKind, UnitctlError};
|
||||
use std::time::Duration;
|
||||
use unit_client_rs::control_socket_address::ControlSocket;
|
||||
use unit_client_rs::unit_client::{UnitClient, UnitClientError};
|
||||
use unit_client_rs::unitd_instance::UnitdInstance;
|
||||
|
||||
/// Waits for a socket to become available. Availability is tested by attempting to access the
|
||||
/// status endpoint via the control socket. When socket is available, ControlSocket instance
|
||||
/// is returned.
|
||||
pub fn wait_for_socket(cli: &UnitCtl) -> Result<ControlSocket, UnitctlError> {
|
||||
// Don't wait, if wait_time is not specified
|
||||
if cli.wait_time_seconds.is_none() {
|
||||
return cli.control_socket_address.instance_value_if_none().and_validate();
|
||||
}
|
||||
|
||||
let wait_time =
|
||||
Duration::from_secs(cli.wait_time_seconds.expect("wait_time_option default was not applied") as u64);
|
||||
let max_tries = cli.wait_max_tries.expect("max_tries_option default was not applied");
|
||||
|
||||
let mut attempt: u8 = 0;
|
||||
let mut control_socket: ControlSocket;
|
||||
while attempt < max_tries {
|
||||
if attempt > 0 {
|
||||
eprintln!(
|
||||
"Waiting for {}s control socket to be available try {}/{}...",
|
||||
wait_time.as_secs(),
|
||||
attempt + 1,
|
||||
max_tries
|
||||
);
|
||||
std::thread::sleep(wait_time);
|
||||
}
|
||||
|
||||
attempt += 1;
|
||||
|
||||
let result = cli.control_socket_address.instance_value_if_none().and_validate();
|
||||
|
||||
if let Err(error) = result {
|
||||
if error.retryable() {
|
||||
continue;
|
||||
} else {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
|
||||
control_socket = result.unwrap();
|
||||
let client = UnitClient::new(control_socket.clone());
|
||||
|
||||
match client.status() {
|
||||
Ok(_) => {
|
||||
return Ok(control_socket.to_owned());
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("Unable to access status endpoint: {}", *error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if attempt >= max_tries {
|
||||
Err(UnitctlError::WaitTimeoutError)
|
||||
} else {
|
||||
panic!("Unexpected state - this should never happen");
|
||||
}
|
||||
}
|
||||
|
||||
trait OptionControlSocket {
|
||||
fn instance_value_if_none(&self) -> Result<ControlSocket, UnitctlError>;
|
||||
}
|
||||
|
||||
impl OptionControlSocket for Option<ControlSocket> {
|
||||
fn instance_value_if_none(&self) -> Result<ControlSocket, UnitctlError> {
|
||||
if let Some(control_socket) = self {
|
||||
Ok(control_socket.to_owned())
|
||||
} else {
|
||||
find_socket_address_from_instance()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait ResultControlSocket<T, E> {
|
||||
fn and_validate(self) -> Result<ControlSocket, UnitctlError>;
|
||||
}
|
||||
|
||||
impl ResultControlSocket<ControlSocket, UnitctlError> for Result<ControlSocket, UnitctlError> {
|
||||
fn and_validate(self) -> Result<ControlSocket, UnitctlError> {
|
||||
self.and_then(|control_socket| {
|
||||
control_socket.validate().map_err(|error| match error {
|
||||
UnitClientError::UnixSocketNotFound { .. } => UnitctlError::ControlSocketError {
|
||||
kind: ControlSocketErrorKind::NotFound,
|
||||
message: format!("{}", error),
|
||||
},
|
||||
UnitClientError::SocketPermissionsError { .. } => UnitctlError::ControlSocketError {
|
||||
kind: ControlSocketErrorKind::Permissions,
|
||||
message: format!("{}", error),
|
||||
},
|
||||
UnitClientError::TcpSocketAddressUriError { .. }
|
||||
| UnitClientError::TcpSocketAddressNoPortError { .. }
|
||||
| UnitClientError::TcpSocketAddressParseError { .. } => UnitctlError::ControlSocketError {
|
||||
kind: ControlSocketErrorKind::Parse,
|
||||
message: format!("{}", error),
|
||||
},
|
||||
_ => UnitctlError::ControlSocketError {
|
||||
kind: ControlSocketErrorKind::General,
|
||||
message: format!("{}", error),
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn find_socket_address_from_instance() -> Result<ControlSocket, UnitctlError> {
|
||||
let instances = UnitdInstance::running_unitd_instances();
|
||||
if instances.is_empty() {
|
||||
return Err(UnitctlError::NoUnitInstancesError);
|
||||
} else if instances.len() > 1 {
|
||||
let suggestion: String = "Multiple unit instances found. Specify the socket address to the instance you wish \
|
||||
to control using the `--control-socket-address` flag"
|
||||
.to_string();
|
||||
return Err(UnitctlError::MultipleUnitInstancesError { suggestion });
|
||||
}
|
||||
|
||||
let instance = instances.first().unwrap();
|
||||
match instance.control_api_socket_address() {
|
||||
Some(path) => Ok(ControlSocket::try_from(path).unwrap()),
|
||||
None => Err(UnitctlError::NoSocketPathError),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wait_for_unavailable_unix_socket() {
|
||||
let control_socket = ControlSocket::try_from("unix:/tmp/this_socket_does_not_exist.sock");
|
||||
let cli = UnitCtl {
|
||||
control_socket_address: Some(control_socket.unwrap()),
|
||||
wait_time_seconds: Some(1u8),
|
||||
wait_max_tries: Some(3u8),
|
||||
command: crate::unitctl::Commands::Status {
|
||||
output_format: crate::output_format::OutputFormat::JsonPretty,
|
||||
},
|
||||
};
|
||||
let error = wait_for_socket(&cli).expect_err("Expected error, but no error received");
|
||||
match error {
|
||||
UnitctlError::WaitTimeoutError => {}
|
||||
_ => panic!("Expected WaitTimeoutError: {}", error),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wait_for_unavailable_tcp_socket() {
|
||||
let control_socket = ControlSocket::try_from("http://127.0.0.1:9783456");
|
||||
let cli = UnitCtl {
|
||||
control_socket_address: Some(control_socket.unwrap()),
|
||||
wait_time_seconds: Some(1u8),
|
||||
wait_max_tries: Some(3u8),
|
||||
command: crate::unitctl::Commands::Status {
|
||||
output_format: crate::output_format::OutputFormat::JsonPretty,
|
||||
},
|
||||
};
|
||||
|
||||
let error = wait_for_socket(&cli).expect_err("Expected error, but no error received");
|
||||
match error {
|
||||
UnitctlError::WaitTimeoutError => {}
|
||||
_ => panic!("Expected WaitTimeoutError"),
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue