feat: implement Dockerfile and service scripts for yanpm-agent
This commit is contained in:
56
apps/agent/Dockerfile
Normal file
56
apps/agent/Dockerfile
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
FROM rust:1.92-alpine3.23 AS builder
|
||||||
|
|
||||||
|
# Install build deps and binutils (for strip)
|
||||||
|
RUN apk add --no-cache build-base musl-dev openssl-dev pkgconfig ca-certificates curl binutils
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy manifest first to leverage Docker layer caching for dependencies
|
||||||
|
COPY ./Cargo.toml ./
|
||||||
|
RUN cargo fetch --locked || true
|
||||||
|
|
||||||
|
COPY ./src ./src
|
||||||
|
|
||||||
|
# Build the release binary and strip it to reduce size
|
||||||
|
RUN cargo build --release --bin yanpm-agent && \
|
||||||
|
strip target/release/yanpm-agent || true
|
||||||
|
|
||||||
|
FROM nginx:mainline-alpine3.23 AS base
|
||||||
|
|
||||||
|
# Expose typical HTTP ports used by nginx
|
||||||
|
EXPOSE 80 443
|
||||||
|
|
||||||
|
ENV YANPM_AGENT_SOCK=/var/run/yanpm/yanpm-agent.sock
|
||||||
|
ENV YANPM_NGINX_CONFIG_DIR=/etc/nginx/conf.d
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install ca-certificates for TLS and minimal tools
|
||||||
|
RUN apk add --no-cache ca-certificates curl
|
||||||
|
|
||||||
|
# Install s6-overlay
|
||||||
|
ENV S6_OVERLAY_VERSION=v3.2.1.0
|
||||||
|
ADD https://github.com/just-containers/s6-overlay/releases/download/${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz /tmp
|
||||||
|
RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz && rm /tmp/s6-overlay-noarch.tar.xz
|
||||||
|
ADD https://github.com/just-containers/s6-overlay/releases/download/${S6_OVERLAY_VERSION}/s6-overlay-x86_64.tar.xz /tmp/s6-overlay.tar.xz
|
||||||
|
RUN tar -C / -Jxpf /tmp/s6-overlay.tar.xz && rm /tmp/s6-overlay.tar.xz
|
||||||
|
|
||||||
|
# Create non-root user for agent and set permissions
|
||||||
|
RUN addgroup -S app && adduser -S -G app app
|
||||||
|
|
||||||
|
# add user to nginx group to allow reading of nginx configs
|
||||||
|
RUN adduser app nginx
|
||||||
|
# create directory for yanpm agent socket
|
||||||
|
RUN mkdir -p /var/run/yanpm && chown -R app:app /var/run/yanpm
|
||||||
|
|
||||||
|
# Copy s6 service definitions (created in repo under s6/) into image
|
||||||
|
COPY ./docker/s6/services.d /etc/services.d
|
||||||
|
RUN chmod +x /etc/services.d/*/run
|
||||||
|
|
||||||
|
COPY --from=builder /app/target/release/yanpm-agent ./yanpm-agent
|
||||||
|
|
||||||
|
RUN chown -R app:app /app/yanpm-agent \
|
||||||
|
&& chmod +x /app/yanpm-agent \
|
||||||
|
&& chown app:app /app
|
||||||
|
|
||||||
|
# s6-overlay provides /init as the init process
|
||||||
|
ENTRYPOINT ["/init"]
|
||||||
4
apps/agent/docker/s6/services.d/agent/run
Normal file
4
apps/agent/docker/s6/services.d/agent/run
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Run the agent as the unprivileged 'app' user
|
||||||
|
cd /app
|
||||||
|
exec s6-setuidgid app ./yanpm-agent
|
||||||
3
apps/agent/docker/s6/services.d/nginx/run
Normal file
3
apps/agent/docker/s6/services.d/nginx/run
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Run nginx in foreground (s6 will supervise it)
|
||||||
|
exec nginx -g 'daemon off;'
|
||||||
@@ -5,7 +5,7 @@ mod routes;
|
|||||||
|
|
||||||
use axum::routing::get;
|
use axum::routing::get;
|
||||||
use axum::{Router, routing::post};
|
use axum::{Router, routing::post};
|
||||||
use clap::Parser;
|
use clap::{Arg, Command};
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -15,33 +15,89 @@ use tracing::{error, info};
|
|||||||
use crate::commands::NginxService;
|
use crate::commands::NginxService;
|
||||||
use crate::routes::{status, validate, validate_and_reload, write_config};
|
use crate::routes::{status, validate, validate_and_reload, write_config};
|
||||||
|
|
||||||
#[derive(Parser)]
|
const SOCK_ARG: &str = "sock";
|
||||||
struct Args {
|
const NGINX_CONFIG_DIR_ARG: &str = "nginx_config_dir";
|
||||||
/// Unix socket path to bind the daemon to
|
const SOCK_ENV: &str = "YANPM_AGENT_SOCK";
|
||||||
sock: String,
|
const NGINX_CONFIG_DIR_ENV: &str = "YANPM_NGINX_CONFIG_DIR";
|
||||||
|
const SOCK_DEFAULT: &str = "./yanpm-agent.sock";
|
||||||
/// Directory where generated nginx config files will be written
|
const NGINX_CONFIG_DIR_DEFAULT: &str = "/etc/nginx/conf.d";
|
||||||
#[arg(long, default_value = "/etc/nginx/conf.d")]
|
|
||||||
nginx_config_dir: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let args = Args::parse();
|
let args = Command::new("yanpm-agent")
|
||||||
let sock = args.sock;
|
.arg(
|
||||||
|
Arg::new("sock")
|
||||||
|
.short('s')
|
||||||
|
.long("sock")
|
||||||
|
.value_name("SOCK_PATH")
|
||||||
|
.help("Unix socket path to bind the agent daemon to")
|
||||||
|
.required(false),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("nginx_config_dir")
|
||||||
|
.short('d')
|
||||||
|
.long("nginx-config-dir")
|
||||||
|
.value_name("NGINX_CONFIG_DIR")
|
||||||
|
.help("Directory where generated nginx config files will be written")
|
||||||
|
.required(false),
|
||||||
|
)
|
||||||
|
.about("YANPM Agent Daemon")
|
||||||
|
.get_matches();
|
||||||
|
|
||||||
|
let subscriber = tracing_subscriber::fmt()
|
||||||
|
.with_max_level(tracing::Level::INFO)
|
||||||
|
.with_target(false)
|
||||||
|
.with_level(true)
|
||||||
|
.with_timer(tracing_subscriber::fmt::time::SystemTime)
|
||||||
|
.finish();
|
||||||
|
|
||||||
|
tracing::subscriber::set_global_default(subscriber)
|
||||||
|
.expect("Failed to set global default subscriber");
|
||||||
|
|
||||||
|
let sock = args
|
||||||
|
.get_one::<String>(SOCK_ARG)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| std::env::var(SOCK_ENV).unwrap_or_else(|_| SOCK_DEFAULT.to_string()));
|
||||||
|
let nginx_config_dir = args
|
||||||
|
.get_one::<String>(NGINX_CONFIG_DIR_ARG)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
std::env::var(NGINX_CONFIG_DIR_ENV)
|
||||||
|
.unwrap_or_else(|_| NGINX_CONFIG_DIR_DEFAULT.to_string())
|
||||||
|
});
|
||||||
|
|
||||||
let path = PathBuf::from(&sock);
|
let path = PathBuf::from(&sock);
|
||||||
if let Some(dir) = path.parent() {
|
if let Some(dir) = path.parent() {
|
||||||
tokio::fs::create_dir_all(dir).await?;
|
tokio::fs::create_dir_all(dir).await.unwrap_or_else(|err| {
|
||||||
|
error!(
|
||||||
|
"Warning: failed to create socket directory {}: {}",
|
||||||
|
dir.display(),
|
||||||
|
err
|
||||||
|
)
|
||||||
|
});
|
||||||
// permissive; set tighter perms in production via image/build steps
|
// permissive; set tighter perms in production via image/build steps
|
||||||
tokio::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o770)).await?;
|
tokio::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o770))
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|err| {
|
||||||
|
error!(
|
||||||
|
"Warning: failed to set permissions on socket directory {}: {}",
|
||||||
|
dir.display(),
|
||||||
|
err
|
||||||
|
)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// If an existing path exists at the socket location, ensure it's a socket
|
// If an existing path exists at the socket location, ensure it's a socket
|
||||||
match tokio::fs::metadata(&path).await {
|
match tokio::fs::metadata(&path).await {
|
||||||
Ok(md) => {
|
Ok(md) => {
|
||||||
use std::os::unix::fs::FileTypeExt;
|
use std::os::unix::fs::FileTypeExt;
|
||||||
if md.file_type().is_socket() {
|
if md.file_type().is_socket() {
|
||||||
tokio::fs::remove_file(&path).await?;
|
tokio::fs::remove_file(&path).await.unwrap_or_else(|err| {
|
||||||
|
error!(
|
||||||
|
"Warning: failed to remove existing socket file {}: {}",
|
||||||
|
path.display(),
|
||||||
|
err
|
||||||
|
)
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
return Err(
|
return Err(
|
||||||
format!("Socket path {} exists and is not a socket", path.display()).into(),
|
format!("Socket path {} exists and is not a socket", path.display()).into(),
|
||||||
@@ -49,11 +105,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||||
Err(e) => return Err(e.into()),
|
Err(e) => {
|
||||||
|
return Err(format!("Failed to stat socket path {}: {}", path.display(), e).into());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// bind using tokio's UnixListener (avoids converting a blocking std listener)
|
// bind using tokio's UnixListener (avoids converting a blocking std listener)
|
||||||
let listener = UnixListener::bind(&path)?;
|
let listener = UnixListener::bind(&path).expect("Failed to bind to unix socket");
|
||||||
// set socket perms to 0660 (best-effort)
|
// set socket perms to 0660 (best-effort)
|
||||||
if let Err(err) =
|
if let Err(err) =
|
||||||
tokio::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o660)).await
|
tokio::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o660)).await
|
||||||
@@ -72,12 +130,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||||||
.route("/validate_and_reload", post(validate_and_reload))
|
.route("/validate_and_reload", post(validate_and_reload))
|
||||||
.route("/validate", post(validate))
|
.route("/validate", post(validate))
|
||||||
.route("/write_config", post(write_config))
|
.route("/write_config", post(write_config))
|
||||||
.with_state(NginxService::new(scheduler.clone(), args.nginx_config_dir).await?);
|
.with_state(NginxService::new(scheduler.clone(), PathBuf::from(nginx_config_dir)).await?);
|
||||||
|
|
||||||
scheduler.start().await?;
|
scheduler.start().await?;
|
||||||
|
|
||||||
info!("Starting yanpm-daemon on unix socket: {}", sock);
|
info!("Starting yanpm-daemon on unix socket: {}", sock);
|
||||||
axum::serve::serve(listener, app).await?;
|
axum::serve::serve(listener, app)
|
||||||
|
.await
|
||||||
|
.expect("Failed to start axum server");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user