feat: add option to activate continuous monitoring on config file

This commit is contained in:
Jun Kurihara 2023-07-24 21:53:01 +09:00
commit 411fbaf296
No known key found for this signature in database
GPG key ID: D992B3E3DE1DED23
8 changed files with 216 additions and 30 deletions

View file

@ -1,4 +1,4 @@
name: Build and Publish Docker
name: Build and Publish Docker x86_64
on:
push:
@ -39,7 +39,7 @@ jobs:
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:latest
file: ./docker/Dockerfile
platforms: linux/amd64,linux/arm64
platforms: linux/amd64
- name: Release build and push x86_64-slim
if: ${{ env.BRANCH == 'main' }}
@ -61,4 +61,4 @@ jobs:
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:nightly
file: ./docker/Dockerfile
platforms: linux/amd64,linux/arm64
platforms: linux/amd64

View file

@ -0,0 +1,64 @@
name: Build and Publish Docker Aarch64
on:
push:
branches:
- main
- develop
jobs:
build_and_push:
runs-on: ubuntu-latest
env:
IMAGE_NAME: rpxy
steps:
- name: Checkout
uses: actions/checkout@v3
with:
submodules: recursive
- name: GitHub Environment
run: echo "BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Release build and push
if: ${{ env.BRANCH == 'main' }}
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:latest
file: ./docker/Dockerfile
platforms: linux/arm64
- name: Release build and push x86_64-slim
if: ${{ env.BRANCH == 'main' }}
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:slim, ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:latest-slim
file: ./docker/Dockerfile.arm64-slim
platforms: linux/arm64
- name: Nightly build and push
if: ${{ env.BRANCH == 'develop' }}
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:nightly
file: ./docker/Dockerfile
platforms: linux/arm64

View file

@ -6,6 +6,8 @@
- Feat: Continuous watching on a specified config file and hot-reloading the file when updated
- Feat: Enabled to specify TCP listen backlog in the config file
- Feat: Add a GitHub action to build `arm64` docker image.
- Bench: Add benchmark result on `amd64` architecture.
- Refactor: Split `rpxy` into `rpxy-lib` and `rpxy-bin`
- Refactor: lots of minor improvements

View file

@ -48,6 +48,20 @@ You can run `rpxy` with a configuration file like
% ./target/release/rpxy --config config.toml
```
If you specify `-w` option along with the config file path, `rpxy` tracks the change of `config.toml` in the real-time manner and apply the change immediately without restarting the process.
The full help messages are given follows.
```bash:
usage: rpxy [OPTIONS] --config <FILE>
Options:
-c, --config <FILE> Configuration file path like ./config.toml
-w, --watch Activate dynamic reloading of the config file via continuous monitoring
-h, --help Print help
-V, --version Print version
```
That's all!
## Basic Configuration
@ -217,7 +231,7 @@ Since it is currently a work-in-progress project, we are frequently adding new o
## Using Docker Image
You can also use [docker image](https://hub.docker.com/r/jqtype/rpxy) instead of directly executing the binary. There are only two docker-specific environment variables.
You can also use [docker image](https://hub.docker.com/r/jqtype/rpxy) instead of directly executing the binary. There are only several docker-specific environment variables.
- `HOST_USER` (default: `user`): User name executing `rpxy` inside the container.
- `HOST_UID` (default: `900`): `UID` of `HOST_USER`.

View file

@ -0,0 +1,45 @@
########################################
FROM messense/rust-musl-cross:aarch64-musl as builder
ENV TARGET_DIR=aarch64-unknown-linux-musl
ENV CFLAGS=-Ofast
WORKDIR /tmp
COPY . /tmp/
ENV RUSTFLAGS "-C link-arg=-s"
RUN echo "Building rpxy from source" && \
cargo build --release && \
musl-strip --strip-all /tmp/target/${TARGET_DIR}/release/rpxy
########################################
FROM alpine:latest as runner
LABEL maintainer="Jun Kurihara"
ENV TARGET_DIR=aarch64-unknown-linux-musl
ENV RUNTIME_DEPS logrotate ca-certificates su-exec
RUN apk add --no-cache ${RUNTIME_DEPS} && \
update-ca-certificates && \
find / -type d -path /proc -prune -o -type f -perm /u+s -exec chmod u-s {} \; && \
find / -type d -path /proc -prune -o -type f -perm /g+s -exec chmod g-s {} \; && \
mkdir -p /rpxy/bin &&\
mkdir -p /rpxy/log
COPY --from=builder /tmp/target/${TARGET_DIR}/release/rpxy /rpxy/bin/rpxy
COPY ./docker/run.sh /rpxy
COPY ./docker/entrypoint.sh /rpxy
RUN chmod +x /rpxy/run.sh && \
chmod +x /rpxy/entrypoint.sh
ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
ENV SSL_CERT_DIR=/etc/ssl/certs
EXPOSE 80 443
CMD ["/rpxy/entrypoint.sh"]
ENTRYPOINT ["/rpxy/entrypoint.sh"]

View file

@ -3,25 +3,44 @@ use crate::{
cert_file_reader::CryptoFileSource,
error::{anyhow, ensure},
};
use clap::Arg;
use clap::{Arg, ArgAction};
use rpxy_lib::{AppConfig, AppConfigList, ProxyConfig};
pub fn parse_opts() -> Result<String, anyhow::Error> {
/// Parsed options
pub struct Opts {
pub config_file_path: String,
pub watch: bool,
}
/// Parse arg values passed from cli
pub fn parse_opts() -> Result<Opts, anyhow::Error> {
let _ = include_str!("../../Cargo.toml");
let options = clap::command!().arg(
Arg::new("config_file")
.long("config")
.short('c')
.value_name("FILE")
.required(true)
.help("Configuration file path like ./config.toml"),
);
let options = clap::command!()
.arg(
Arg::new("config_file")
.long("config")
.short('c')
.value_name("FILE")
.required(true)
.help("Configuration file path like ./config.toml"),
)
.arg(
Arg::new("watch")
.long("watch")
.short('w')
.action(ArgAction::SetTrue)
.help("Activate dynamic reloading of the config file via continuous monitoring"),
);
let matches = options.get_matches();
///////////////////////////////////
let config_file_path = matches.get_one::<String>("config_file").unwrap();
let config_file_path = matches.get_one::<String>("config_file").unwrap().to_owned();
let watch = matches.get_one::<bool>("watch").unwrap().to_owned();
Ok(config_file_path.to_string())
Ok(Opts {
config_file_path,
watch,
})
}
pub fn build_settings(

View file

@ -28,32 +28,69 @@ fn main() {
let runtime = runtime_builder.build().unwrap();
runtime.block_on(async {
// Initially load config
let Ok(config_path) = parse_opts() else {
// Initially load options
let Ok(parsed_opts) = parse_opts() else {
error!("Invalid toml file");
std::process::exit(1);
};
let (config_service, config_rx) =
ReloaderService::<ConfigTomlReloader, ConfigToml>::new(&config_path, CONFIG_WATCH_DELAY_SECS, false)
.await
.unwrap();
tokio::select! {
_ = config_service.start() => {
error!("config reloader service exited");
if !parsed_opts.watch {
if let Err(e) = rpxy_service_without_watcher(&parsed_opts.config_file_path, runtime.handle().clone()).await {
error!("rpxy service existed: {e}");
std::process::exit(1);
}
_ = rpxy_service(config_rx, runtime.handle().clone()) => {
error!("rpxy service existed");
} else {
let (config_service, config_rx) = ReloaderService::<ConfigTomlReloader, ConfigToml>::new(
&parsed_opts.config_file_path,
CONFIG_WATCH_DELAY_SECS,
false,
)
.await
.unwrap();
tokio::select! {
Err(e) = config_service.start() => {
error!("config reloader service exited: {e}");
std::process::exit(1);
}
Err(e) = rpxy_service_with_watcher(config_rx, runtime.handle().clone()) => {
error!("rpxy service existed: {e}");
std::process::exit(1);
}
}
}
});
}
async fn rpxy_service(
mut config_rx: ReloaderReceiver<ConfigToml>,
async fn rpxy_service_without_watcher(
config_file_path: &str,
runtime_handle: tokio::runtime::Handle,
) -> Result<(), anyhow::Error> {
info!("Start rpxy service");
let config_toml = match ConfigToml::new(config_file_path) {
Ok(v) => v,
Err(e) => {
error!("Invalid toml file: {e}");
std::process::exit(1);
}
};
let (proxy_conf, app_conf) = match build_settings(&config_toml) {
Ok(v) => v,
Err(e) => {
error!("Invalid configuration: {e}");
return Err(anyhow::anyhow!(e));
}
};
entrypoint(&proxy_conf, &app_conf, &runtime_handle)
.await
.map_err(|e| anyhow::anyhow!(e))
}
async fn rpxy_service_with_watcher(
mut config_rx: ReloaderReceiver<ConfigToml>,
runtime_handle: tokio::runtime::Handle,
) -> Result<(), anyhow::Error> {
info!("Start rpxy service with dynamic config reloader");
// Initial loading
config_rx.changed().await?;
let config_toml = config_rx.borrow().clone().unwrap();
@ -92,5 +129,6 @@ async fn rpxy_service(
else => break
}
}
Ok(())
Err(anyhow::anyhow!("rpxy or continuous monitoring service exited"))
}

View file

@ -6,6 +6,8 @@ use std::net::SocketAddr;
use std::net::UdpSocket;
use tokio::net::TcpSocket;
/// Bind TCP socket to the given `SocketAddr`, and returns the TCP socket with `SO_REUSEADDR` and `SO_REUSEPORT` options.
/// This option is required to re-bind the socket address when the proxy instance is reconstructed.
pub(super) fn bind_tcp_socket(listening_on: &SocketAddr) -> Result<TcpSocket> {
let tcp_socket = if listening_on.is_ipv6() {
TcpSocket::new_v6()
@ -22,6 +24,8 @@ pub(super) fn bind_tcp_socket(listening_on: &SocketAddr) -> Result<TcpSocket> {
}
#[cfg(feature = "http3")]
/// Bind UDP socket to the given `SocketAddr`, and returns the UDP socket with `SO_REUSEADDR` and `SO_REUSEPORT` options.
/// This option is required to re-bind the socket address when the proxy instance is reconstructed.
pub(super) fn bind_udp_socket(listening_on: &SocketAddr) -> Result<UdpSocket> {
let socket = if listening_on.is_ipv6() {
Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::UDP))