feat: implement Dockerfile and service scripts for yanpm-agent

This commit is contained in:
GW_MC
2025-12-21 17:51:43 +08:00
parent 4ca59d2bb6
commit 7781878c2d
4 changed files with 142 additions and 19 deletions

56
apps/agent/Dockerfile Normal file
View 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"]

View File

@@ -0,0 +1,4 @@
#!/bin/sh
# Run the agent as the unprivileged 'app' user
cd /app
exec s6-setuidgid app ./yanpm-agent

View File

@@ -0,0 +1,3 @@
#!/bin/sh
# Run nginx in foreground (s6 will supervise it)
exec nginx -g 'daemon off;'

View File

@@ -5,7 +5,7 @@ mod routes;
use axum::routing::get;
use axum::{Router, routing::post};
use clap::Parser;
use clap::{Arg, Command};
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use std::sync::Arc;
@@ -15,33 +15,89 @@ use tracing::{error, info};
use crate::commands::NginxService;
use crate::routes::{status, validate, validate_and_reload, write_config};
#[derive(Parser)]
struct Args {
/// Unix socket path to bind the daemon to
sock: String,
/// Directory where generated nginx config files will be written
#[arg(long, default_value = "/etc/nginx/conf.d")]
nginx_config_dir: PathBuf,
}
const SOCK_ARG: &str = "sock";
const NGINX_CONFIG_DIR_ARG: &str = "nginx_config_dir";
const SOCK_ENV: &str = "YANPM_AGENT_SOCK";
const NGINX_CONFIG_DIR_ENV: &str = "YANPM_NGINX_CONFIG_DIR";
const SOCK_DEFAULT: &str = "./yanpm-agent.sock";
const NGINX_CONFIG_DIR_DEFAULT: &str = "/etc/nginx/conf.d";
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let args = Args::parse();
let sock = args.sock;
let args = Command::new("yanpm-agent")
.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);
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
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
match tokio::fs::metadata(&path).await {
Ok(md) => {
use std::os::unix::fs::FileTypeExt;
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 {
return Err(
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) => 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)
let listener = UnixListener::bind(&path)?;
let listener = UnixListener::bind(&path).expect("Failed to bind to unix socket");
// set socket perms to 0660 (best-effort)
if let Err(err) =
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", post(validate))
.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?;
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(())
}