config + stop outsourcing other commands
This commit is contained in:
@@ -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
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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)?,
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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>)> {
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
mod api;
|
||||
mod cli;
|
||||
mod config;
|
||||
mod runtime;
|
||||
mod state;
|
||||
mod tracing;
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user