config + stop outsourcing other commands

This commit is contained in:
lda
2026-04-11 17:36:53 +07:00 Unverified
parent fec10a61d4
commit ce1c0c0558
11 changed files with 160 additions and 30 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
We work towards clean, modular, maintainable design, clear documentation... im not the best at this thing, so you help me.
We work towards clean, modular, maintainable design, clear documentation... im not the best at this thing, my choices can be bad, so you help me.
DRY: spawn subagents.
Generated
+3
View File
@@ -2411,6 +2411,7 @@ dependencies = [
"futures-util",
"http",
"macaddr",
"nix",
"reqwest",
"serde",
"serde_json",
@@ -2432,6 +2433,8 @@ dependencies = [
"axum",
"clap",
"futures-util",
"nix",
"reqwest",
"serde",
"serde_json",
"tokio",
+1
View File
@@ -9,6 +9,7 @@ clap = { version = "4", features = ["derive"] }
futures-util = "0.3"
http = "1"
macaddr = { version = "1", features = ["serde", "serde_std"] }
nix = { version = "0.30", default-features = false, features = ["signal", "process"] }
reqwest = { version = "0.12", default-features = false, features = [
"json",
"rustls-tls",
+8
View File
@@ -52,6 +52,14 @@ pub struct EnrollArgs {
/// Path to the agent config file to write.
#[arg(long, default_value = config::DEFAULT_CONFIG_PATH)]
pub config: PathBuf,
/// Reload a running daemon after writing config.
#[arg(long)]
pub reload_running: bool,
/// Path to pid file used when `--reload-running` is enabled.
#[arg(long, default_value = DEFAULT_PID_FILE)]
pub pid_file: PathBuf,
}
#[derive(Args)]
+6
View File
@@ -22,6 +22,12 @@ async fn main() -> Result<()> {
let config = enroll::enroll(&args.server_url, &args.enroll_token, &args.config).await?;
println!("agent_id={}", config.agent_id);
println!("config={}", args.config.display());
if args.reload_running {
match serve::reload_daemon(&args.pid_file) {
Ok(()) => println!("reload=signaled pid_file={}", args.pid_file.display()),
Err(err) => ::tracing::warn!(error = %err, pid_file = %args.pid_file.display(), "enroll completed but daemon reload failed"),
}
}
}
Command::InitConfig(args) => init_config(args)?,
Command::Reload(args) => serve::reload_daemon(&args.pid_file)?,
+14 -8
View File
@@ -105,13 +105,19 @@ fn read_pid(path: &Path) -> Result<i32> {
}
fn send_hup(pid: i32) -> Result<()> {
let status = std::process::Command::new("kill")
.arg("-HUP")
.arg(pid.to_string())
.status()
.context("failed to invoke kill -HUP")?;
if !status.success() {
anyhow::bail!("kill -HUP failed for pid {pid}");
#[cfg(unix)]
{
use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;
kill(Pid::from_raw(pid), Signal::SIGHUP)
.with_context(|| format!("failed to send SIGHUP to pid {pid}"))?;
Ok(())
}
#[cfg(not(unix))]
{
let _ = pid;
anyhow::bail!("reload is only supported on Unix (SIGHUP unavailable on this platform)")
}
Ok(())
}
+2
View File
@@ -8,6 +8,8 @@ anyhow = "1"
axum = { version = "0.8", features = ["ws", "json"] }
clap = { version = "4", features = ["derive"] }
futures-util = "0.3"
nix = { version = "0.30", default-features = false, features = ["signal", "process"] }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = [
+23
View File
@@ -20,6 +20,11 @@ pub struct EnrollResponse {
pub server_url: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct IssueEnrollTokenResponse {
pub enroll_token: String,
}
#[derive(Debug, Serialize)]
pub struct AgentStatus {
pub agent_id: String,
@@ -67,6 +72,24 @@ pub async fn enroll(
}
}
pub async fn issue_enroll_token(
State(state): State<AppState>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
match state.store.issue_enroll_token().await {
Ok(token) => Ok((
StatusCode::OK,
Json(IssueEnrollTokenResponse {
enroll_token: token,
}),
)),
Err(err) => Err(json_error(
StatusCode::INTERNAL_SERVER_ERROR,
"issue_enroll_token_failed",
&err.to_string(),
)),
}
}
pub async fn list_agents(
State(state): State<AppState>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
+37
View File
@@ -0,0 +1,37 @@
use std::net::SocketAddr;
use std::path::PathBuf;
use std::time::Duration;
use crate::cli::ServeArgs;
#[derive(Debug, Clone)]
pub struct DaemonConfig {
pub bind: SocketAddr,
pub public_url: String,
pub state_file: PathBuf,
pub command_timeout: Duration,
pub pid_file: PathBuf,
}
impl DaemonConfig {
pub fn from_serve_args(args: &ServeArgs) -> Self {
Self {
bind: args.bind,
public_url: normalize_public_url(&args.public_url),
state_file: args.state_file.clone(),
command_timeout: Duration::from_millis(args.command_timeout_ms.max(1)),
pid_file: args.pid_file.clone(),
}
}
}
pub fn normalize_public_url(url: &str) -> String {
url.trim_end_matches('/').to_string()
}
pub fn issue_token_endpoint(base_url: &str) -> String {
format!(
"{}/api/v1/control/enroll-token",
normalize_public_url(base_url)
)
}
+1
View File
@@ -1,5 +1,6 @@
mod api;
mod cli;
mod config;
mod runtime;
mod state;
mod tracing;
+64 -21
View File
@@ -13,6 +13,7 @@ use wakey_agent::protocol::{ErrorPayload, ServerMessage};
use crate::api;
use crate::cli::{IssueEnrollTokenArgs, ServeArgs};
use crate::config;
use crate::state;
use crate::ws;
@@ -31,23 +32,25 @@ pub enum AgentReply {
}
pub async fn serve(args: ServeArgs) -> Result<()> {
write_pid_file(&args.pid_file)?;
let daemon = config::DaemonConfig::from_serve_args(&args);
write_pid_file(&daemon.pid_file)?;
let store = state::Store::load_or_init(&args.state_file, args.enroll_tokens)
let store = state::Store::load_or_init(&daemon.state_file, args.enroll_tokens)
.await
.with_context(|| format!("failed to initialize store {}", args.state_file.display()))?;
.with_context(|| format!("failed to initialize store {}", daemon.state_file.display()))?;
let app_state = AppState {
store: Arc::new(store),
sessions: Arc::new(RwLock::new(HashMap::new())),
pending: Arc::new(Mutex::new(HashMap::new())),
public_url: args.public_url.trim_end_matches('/').to_string(),
command_timeout: Duration::from_millis(args.command_timeout_ms.max(1)),
public_url: daemon.public_url.clone(),
command_timeout: daemon.command_timeout,
};
let app = Router::new()
.route("/healthz", get(api::healthz))
.route("/api/v1/agents/enroll", post(api::enroll))
.route("/api/v1/control/enroll-token", post(api::issue_enroll_token))
.route("/api/v1/agent/ws", get(ws::agent_ws))
.route("/api/v1/control/agents", get(api::list_agents))
.route(
@@ -56,8 +59,8 @@ pub async fn serve(args: ServeArgs) -> Result<()> {
)
.with_state(app_state.clone());
info!(bind = %args.bind, pid_file = %args.pid_file.display(), "starting control-plane server");
let listener = TcpListener::bind(args.bind).await?;
info!(bind = %daemon.bind, pid_file = %daemon.pid_file.display(), "starting control-plane server");
let listener = TcpListener::bind(daemon.bind).await?;
let mut server = tokio::spawn(async move {
axum::serve(listener, app)
.await
@@ -83,7 +86,7 @@ pub async fn serve(args: ServeArgs) -> Result<()> {
}
}
join = &mut server => {
let _ = remove_pid_file(&args.pid_file);
let _ = remove_pid_file(&daemon.pid_file);
return join.context("control-plane join failed")?;
}
}
@@ -98,20 +101,54 @@ pub async fn serve(args: ServeArgs) -> Result<()> {
server.abort();
}
let _ = remove_pid_file(&args.pid_file);
let _ = remove_pid_file(&daemon.pid_file);
Ok(())
}
pub async fn issue_enroll_token(args: IssueEnrollTokenArgs) -> Result<()> {
if let Some(url) = args.public_url {
let base = config::normalize_public_url(&url);
let endpoint = config::issue_token_endpoint(&base);
let client = reqwest::Client::new();
let response = client
.post(&endpoint)
.send()
.await
.with_context(|| format!("failed to call live issuance endpoint {endpoint}"))?;
if !response.status().is_success() {
let status = response.status();
let body = response
.text()
.await
.unwrap_or_else(|_| "<unreadable error body>".to_string());
anyhow::bail!("live issuance failed with {status}: {body}");
}
let payload: api::IssueEnrollTokenResponse = response
.json()
.await
.context("failed to decode live issuance response")?;
println!("enroll_token={}", payload.enroll_token);
println!(
"agent_command=wakey-agent enroll --server-url {base} --enroll-token {}",
payload.enroll_token
);
return Ok(());
}
// Fallback for offline tooling: writes to state file, requires daemon reload to pick up.
let store = state::Store::load_or_init(&args.state_file, args.enroll_tokens)
.await
.with_context(|| format!("failed to initialize store {}", args.state_file.display()))?;
let token = store.issue_enroll_token().await?;
println!("enroll_token={token}");
if let Some(url) = args.public_url {
let base = url.trim_end_matches('/');
println!("agent_command=wakey-agent enroll --server-url {base} --enroll-token {token}");
}
eprintln!(
"note: token was written to {}. running daemon must reload state to see it",
args.state_file.display()
);
Ok(())
}
@@ -153,13 +190,19 @@ fn read_pid(path: &Path) -> Result<i32> {
}
fn send_hup(pid: i32) -> Result<()> {
let status = std::process::Command::new("kill")
.arg("-HUP")
.arg(pid.to_string())
.status()
.context("failed to invoke kill -HUP")?;
if !status.success() {
anyhow::bail!("kill -HUP failed for pid {pid}");
#[cfg(unix)]
{
use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;
kill(Pid::from_raw(pid), Signal::SIGHUP)
.with_context(|| format!("failed to send SIGHUP to pid {pid}"))?;
Ok(())
}
#[cfg(not(unix))]
{
let _ = pid;
anyhow::bail!("reload is only supported on Unix (SIGHUP unavailable on this platform)")
}
Ok(())
}