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:
Ava Hahn 2024-04-22 13:26:34 +01:00 committed by avahahn
parent b26c119f4e
commit db3cf3e42d
54 changed files with 7212 additions and 0 deletions

View file

@ -0,0 +1,2 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"

16
tools/unitctl/.gitignore vendored Normal file
View 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

File diff suppressed because it is too large Load diff

8
tools/unitctl/Cargo.toml Normal file
View file

@ -0,0 +1,8 @@
[workspace]
resolver = "2"
members = [
"unit-openapi",
"unit-client-rs",
"unitctl"
]

37
tools/unitctl/Dockerfile Normal file
View 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
View 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

View file

@ -0,0 +1 @@
pkg/brew

134
tools/unitctl/README.md Normal file
View 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
```

View 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

View 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

View 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} "$@"

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

View 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

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

View file

@ -0,0 +1,6 @@
{
"packageName": "unit-openapi",
"packageVersion": "0.4.0-beta",
"library": "hyper",
"preferUnsignedInt": true
}

View 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

View 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

View file

@ -0,0 +1 @@
max_width = 120

View 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"

View 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,
})
}
}

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

View 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)
}
}

View 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");
}
}
}
}

View 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)
}
}

View 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),
};
}
}

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

View 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");
}
}

View 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
View file

@ -0,0 +1,3 @@
/target/
**/*.rs.bk
Cargo.lock

View 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

View 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

View file

@ -0,0 +1 @@
6.6.0

View 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"

View 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

View 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}}

View 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(&param).unwrap());
self
}
pub fn with_header_param(mut self, basename: String, param: String) -> Self {
self.header_params.insert(basename, param);
self
}
#[allow(unused)]
pub fn with_query_param(mut self, basename: String, param: String) -> Self {
self.query_params.insert(basename, param);
self
}
#[allow(unused)]
pub fn with_path_param(mut self, basename: String, param: String) -> Self {
self.path_params.insert(basename, param);
self
}
#[allow(unused)]
pub fn with_form_param(mut self, basename: String, param: String) -> Self {
self.form_params.insert(basename, param);
self
}
pub fn returns_nothing(mut self) -> Self {
self.no_return_type = true;
self
}
pub fn with_auth(mut self, auth: Auth) -> Self {
self.auth = Some(auth);
self
}
pub fn execute<'a, C, U>(
self,
conf: &configuration::Configuration<C>,
) -> Pin<Box<dyn Future<Output=Result<U, Error>> + 'a>>
where
C: hyper::client::connect::Connect + Clone + std::marker::Send + Sync,
U: Sized + std::marker::Send + 'a,
for<'de> U: serde::Deserialize<'de>,
{
let mut query_string = ::url::form_urlencoded::Serializer::new("".to_owned());
let mut path = self.path;
for (k, v) in self.path_params {
// replace {id} with the value of the id path param
path = path.replace(&format!("{{{}}}", k), &v);
}
for (key, val) in self.query_params {
query_string.append_pair(&key, &val);
}
let mut uri_str = format!("{}{}", conf.base_path, path);
let query_string_str = query_string.finish();
if query_string_str != "" {
uri_str += "?";
uri_str += &query_string_str;
}
let uri: hyper::Uri = match uri_str.parse() {
Err(e) => return Box::pin(futures::future::err(Error::UriError(e))),
Ok(u) => u,
};
let mut req_builder = hyper::Request::builder()
.uri(uri)
.method(self.method);
// Detect the authorization type if it hasn't been set.
let auth = self.auth.unwrap_or_else(||
if conf.api_key.is_some() {
panic!("Cannot automatically set the API key from the configuration, it must be specified in the OpenAPI definition")
} else if conf.oauth_access_token.is_some() {
Auth::Oauth
} else if conf.basic_auth.is_some() {
Auth::Basic
} else {
Auth::None
}
);
match auth {
Auth::ApiKey(apikey) => {
if let Some(ref key) = conf.api_key {
let val = apikey.key(&key.prefix, &key.key);
if apikey.in_query {
query_string.append_pair(&apikey.param_name, &val);
}
if apikey.in_header {
req_builder = req_builder.header(&apikey.param_name, val);
}
}
}
Auth::Basic => {
if let Some(ref auth_conf) = conf.basic_auth {
let mut text = auth_conf.0.clone();
text.push(':');
if let Some(ref pass) = auth_conf.1 {
text.push_str(&pass[..]);
}
let encoded = MIME_ENCODER.encode(&text);
req_builder = req_builder.header(AUTHORIZATION, encoded);
}
}
Auth::Oauth => {
if let Some(ref token) = conf.oauth_access_token {
let text = "Bearer ".to_owned() + token;
req_builder = req_builder.header(AUTHORIZATION, text);
}
}
Auth::None => {}
}
if let Some(ref user_agent) = conf.user_agent {
req_builder = req_builder.header(USER_AGENT, match HeaderValue::from_str(user_agent) {
Ok(header_value) => header_value,
Err(e) => return Box::pin(futures::future::err(super::Error::Header(e)))
});
}
for (k, v) in self.header_params {
req_builder = req_builder.header(&k, v);
}
let req_headers = req_builder.headers_mut().unwrap();
let request_result = if self.form_params.len() > 0 {
req_headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/x-www-form-urlencoded"));
let mut enc = ::url::form_urlencoded::Serializer::new("".to_owned());
for (k, v) in self.form_params {
enc.append_pair(&k, &v);
}
req_builder.body(hyper::Body::from(enc.finish()))
} else if let Some(body) = self.serialized_body {
req_headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
req_headers.insert(CONTENT_LENGTH, body.len().into());
req_builder.body(hyper::Body::from(body))
} else {
req_builder.body(hyper::Body::default())
};
let request = match request_result {
Ok(request) => request,
Err(e) => return Box::pin(futures::future::err(Error::from(e)))
};
let no_return_type = self.no_return_type;
Box::pin(conf.client
.request(request)
.map_err(|e| Error::from(e))
.and_then(move |response| {
let status = response.status();
if !status.is_success() {
futures::future::err::<U, Error>(Error::from((status, response.into_body()))).boxed()
} else if no_return_type {
// This is a hack; if there's no_ret_type, U is (), but serde_json gives an
// error when deserializing "" into (), so deserialize 'null' into it
// instead.
// An alternate option would be to require U: Default, and then return
// U::default() here instead since () implements that, but then we'd
// need to impl default for all models.
futures::future::ok::<U, Error>(serde_json::from_str("null").expect("serde null value")).boxed()
} else {
hyper::body::to_bytes(response.into_body())
.map(|bytes| serde_json::from_slice(&bytes.unwrap()))
.map_err(|e| Error::from(e)).boxed()
}
}))
}
}

View 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 {}

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

View 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" },
]

View 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(), &current_config)
.map_err(|e| UnitctlError::SerializationError { message: e.to_string() })?;
// Load edited file
let temp_file_path = temp_file.path();
let before_edit_mod_time = temp_file_path.metadata().ok().map(|m| m.modified().ok());
let inputfile = InputFile::FileWithFormat(temp_file_path.into(), InputFormat::Json5);
open_editor(temp_file_path)?;
let after_edit_mod_time = temp_file_path.metadata().ok().map(|m| m.modified().ok());
// Check if file was modified before sending to UNIT
if let (Some(before), Some(after)) = (before_edit_mod_time, after_edit_mod_time) {
if before == after {
eprintln!("File was not modified - no changes will be sent to UNIT");
return Ok(());
}
};
// Send edited file to UNIT to overwrite current configuration
send_and_validate_config_deserialize_response(&client, "PUT", "/config", Some(&inputfile))
.and_then(|status| output_format.write_to_stdout(&status))
}
/// Look for an editor in the environment variables
fn find_editor_from_env() -> Option<PathBuf> {
EDITOR_ENV_VARS
.iter()
.filter_map(std::env::var_os)
.filter(|s| !s.is_empty())
.filter_map(|s| which(s).ok())
.filter_map(|path| path.canonicalize().ok())
.find(|path| path.exists())
}
/// Look for editor in path by matching against a list of known editors or aliases
fn find_editor_from_known_list() -> Option<PathBuf> {
EDITOR_KNOWN_LIST
.iter()
.filter_map(|editor| which(editor).ok())
.filter_map(|path| path.canonicalize().ok())
.find(|editor| editor.exists())
}
/// Find the path to an editor
pub fn find_editor_path() -> Result<PathBuf, UnitctlError> {
find_editor_from_env()
.or_else(find_editor_from_known_list)
.ok_or_else(|| UnitctlError::EditorError {
message: "Could not find an editor".to_string(),
})
}
/// Start an editor with a given path
pub fn open_editor(path: &Path) -> Result<(), UnitctlError> {
let editor_path = find_editor_path()?;
let status = std::process::Command::new(editor_path)
.arg(path)
.status()
.map_err(|e| UnitctlError::EditorError {
message: format!("Could not open editor: {}", e),
})?;
if status.success() {
Ok(())
} else {
Err(UnitctlError::EditorError {
message: format!("Editor exited with non-zero status: {}", status),
})
}
}

View 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))
}

View 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)
}
}
}

View 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)
}
}

View 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))
}

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

View 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))
}

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

View 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)
}
}

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

View 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 }),
}
}
}

View 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)
}

View 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)))
}

View 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)
}
}

View 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"),
}
}