uhhh
This commit is contained in:
@@ -85,6 +85,12 @@ You can scaffold this file with:
|
|||||||
wakey-control-plane init-config
|
wakey-control-plane init-config
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Or bootstrap once during serve (writes only if config is missing):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
wakey-control-plane serve --bootstrap-config
|
||||||
|
```
|
||||||
|
|
||||||
Inspect current persisted state:
|
Inspect current persisted state:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -37,19 +37,28 @@ pub struct ListEnrollTokenQuery {
|
|||||||
pub include_expired: Option<bool>,
|
pub include_expired: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct EnrollTokenStatus {
|
pub struct EnrollTokenStatus {
|
||||||
pub enroll_token: String,
|
pub enroll_token: String,
|
||||||
pub expires_at_unix: u64,
|
pub expires_at_unix: u64,
|
||||||
pub expired: bool,
|
pub expired: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct RevokeEnrollTokenResponse {
|
pub struct RevokeEnrollTokenResponse {
|
||||||
pub token: String,
|
pub token: String,
|
||||||
pub revoked: bool,
|
pub revoked: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct StateStatsResponse {
|
||||||
|
pub db_path: String,
|
||||||
|
pub schema_version: u32,
|
||||||
|
pub agent_count: usize,
|
||||||
|
pub enroll_token_count: usize,
|
||||||
|
pub expired_enroll_token_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct AgentStatus {
|
pub struct AgentStatus {
|
||||||
pub agent_id: String,
|
pub agent_id: String,
|
||||||
@@ -201,6 +210,31 @@ pub async fn revoke_enroll_token(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn state_stats(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||||
|
match state.store.stats().await {
|
||||||
|
Ok(stats) => Ok((
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(StateStatsResponse {
|
||||||
|
db_path: stats.db_path.display().to_string(),
|
||||||
|
schema_version: stats.schema_version,
|
||||||
|
agent_count: stats.agent_count,
|
||||||
|
enroll_token_count: stats.enroll_token_count,
|
||||||
|
expired_enroll_token_count: stats.expired_enroll_token_count,
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = %err, "failed to read state stats");
|
||||||
|
Err(json_error(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"state_stats_failed",
|
||||||
|
&err.to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn run_command(
|
pub async fn run_command(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
AxumPath(agent_id): AxumPath<String>,
|
AxumPath(agent_id): AxumPath<String>,
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ pub struct ServeArgs {
|
|||||||
|
|
||||||
#[arg(long, default_value = DEFAULT_CONFIG_FILE)]
|
#[arg(long, default_value = DEFAULT_CONFIG_FILE)]
|
||||||
pub config_file: PathBuf,
|
pub config_file: PathBuf,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub bootstrap_config: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args, Clone)]
|
#[derive(Args, Clone)]
|
||||||
@@ -140,6 +143,9 @@ pub struct ListEnrollTokensArgs {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub data_dir: Option<PathBuf>,
|
pub data_dir: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub public_url: Option<String>,
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub include_expired: bool,
|
pub include_expired: bool,
|
||||||
|
|
||||||
@@ -158,6 +164,9 @@ pub struct RevokeEnrollTokenArgs {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub data_dir: Option<PathBuf>,
|
pub data_dir: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub public_url: Option<String>,
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub token: String,
|
pub token: String,
|
||||||
}
|
}
|
||||||
@@ -173,6 +182,9 @@ pub struct StateStatsArgs {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub data_dir: Option<PathBuf>,
|
pub data_dir: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub public_url: Option<String>,
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub json: bool,
|
pub json: bool,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,6 +268,31 @@ pub fn write_init_config(args: &InitConfigArgs) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn bootstrap_config_if_missing(args: &ServeArgs) -> Result<bool> {
|
||||||
|
if args.config_file.exists() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let init = InitConfigArgs {
|
||||||
|
config_file: args.config_file.clone(),
|
||||||
|
data_dir: args.data_dir.clone(),
|
||||||
|
bind: args.bind,
|
||||||
|
public_url: args.public_url.clone(),
|
||||||
|
state_file: args.state_file.clone(),
|
||||||
|
pid_file: args.pid_file.clone(),
|
||||||
|
command_timeout_ms: args.command_timeout_ms,
|
||||||
|
enroll_token_ttl_seconds: args.enroll_token_ttl_seconds,
|
||||||
|
enroll_tokens: args.enroll_tokens.clone(),
|
||||||
|
telemetry_otlp_endpoint: None,
|
||||||
|
telemetry_service_name: None,
|
||||||
|
telemetry_json_logs: false,
|
||||||
|
force: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
write_init_config(&init)?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn resolve_path(data_dir: &Path, candidate: PathBuf) -> PathBuf {
|
pub fn resolve_path(data_dir: &Path, candidate: PathBuf) -> PathBuf {
|
||||||
if candidate.is_absolute() {
|
if candidate.is_absolute() {
|
||||||
candidate
|
candidate
|
||||||
@@ -285,11 +310,17 @@ pub struct IssueTokenSettings {
|
|||||||
pub struct StateAccessSettings {
|
pub struct StateAccessSettings {
|
||||||
pub data_dir: PathBuf,
|
pub data_dir: PathBuf,
|
||||||
pub state_file: PathBuf,
|
pub state_file: PathBuf,
|
||||||
|
pub public_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_issue_token_settings(args: &IssueEnrollTokenArgs) -> Result<IssueTokenSettings> {
|
pub fn resolve_issue_token_settings(args: &IssueEnrollTokenArgs) -> Result<IssueTokenSettings> {
|
||||||
let file = load_file_config(&args.config_file)?;
|
let file = load_file_config(&args.config_file)?;
|
||||||
let state = resolve_state_access(&args.config_file, args.data_dir.clone(), args.state_file.clone())?;
|
let state = resolve_state_access(
|
||||||
|
&args.config_file,
|
||||||
|
args.data_dir.clone(),
|
||||||
|
args.state_file.clone(),
|
||||||
|
args.public_url.clone(),
|
||||||
|
)?;
|
||||||
|
|
||||||
let ttl = Duration::from_secs(
|
let ttl = Duration::from_secs(
|
||||||
args.ttl_seconds
|
args.ttl_seconds
|
||||||
@@ -306,21 +337,37 @@ pub fn resolve_issue_token_settings(args: &IssueEnrollTokenArgs) -> Result<Issue
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_list_enroll_token_settings(args: &ListEnrollTokensArgs) -> Result<StateAccessSettings> {
|
pub fn resolve_list_enroll_token_settings(args: &ListEnrollTokensArgs) -> Result<StateAccessSettings> {
|
||||||
resolve_state_access(&args.config_file, args.data_dir.clone(), args.state_file.clone())
|
resolve_state_access(
|
||||||
|
&args.config_file,
|
||||||
|
args.data_dir.clone(),
|
||||||
|
args.state_file.clone(),
|
||||||
|
args.public_url.clone(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_revoke_enroll_token_settings(args: &RevokeEnrollTokenArgs) -> Result<StateAccessSettings> {
|
pub fn resolve_revoke_enroll_token_settings(args: &RevokeEnrollTokenArgs) -> Result<StateAccessSettings> {
|
||||||
resolve_state_access(&args.config_file, args.data_dir.clone(), args.state_file.clone())
|
resolve_state_access(
|
||||||
|
&args.config_file,
|
||||||
|
args.data_dir.clone(),
|
||||||
|
args.state_file.clone(),
|
||||||
|
args.public_url.clone(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_state_stats_settings(args: &StateStatsArgs) -> Result<StateAccessSettings> {
|
pub fn resolve_state_stats_settings(args: &StateStatsArgs) -> Result<StateAccessSettings> {
|
||||||
resolve_state_access(&args.config_file, args.data_dir.clone(), args.state_file.clone())
|
resolve_state_access(
|
||||||
|
&args.config_file,
|
||||||
|
args.data_dir.clone(),
|
||||||
|
args.state_file.clone(),
|
||||||
|
args.public_url.clone(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_state_access(
|
fn resolve_state_access(
|
||||||
config_file: &Path,
|
config_file: &Path,
|
||||||
cli_data_dir: Option<PathBuf>,
|
cli_data_dir: Option<PathBuf>,
|
||||||
cli_state_file: Option<PathBuf>,
|
cli_state_file: Option<PathBuf>,
|
||||||
|
cli_public_url: Option<String>,
|
||||||
) -> Result<StateAccessSettings> {
|
) -> Result<StateAccessSettings> {
|
||||||
let file = load_file_config(config_file)?;
|
let file = load_file_config(config_file)?;
|
||||||
|
|
||||||
@@ -335,8 +382,13 @@ fn resolve_state_access(
|
|||||||
.unwrap_or_else(|| PathBuf::from("state.db")),
|
.unwrap_or_else(|| PathBuf::from("state.db")),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let public_url = cli_public_url
|
||||||
|
.or(file.public_url)
|
||||||
|
.map(|url| normalize_public_url(&url));
|
||||||
|
|
||||||
Ok(StateAccessSettings {
|
Ok(StateAccessSettings {
|
||||||
data_dir,
|
data_dir,
|
||||||
state_file,
|
state_file,
|
||||||
|
public_url,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,15 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Command::Serve(args) => {
|
Command::Serve(args) => {
|
||||||
|
if args.bootstrap_config {
|
||||||
|
let created = config::bootstrap_config_if_missing(&args)?;
|
||||||
|
if created {
|
||||||
|
eprintln!(
|
||||||
|
"bootstrapped missing config at {}",
|
||||||
|
args.config_file.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
let daemon = config::DaemonConfig::from_serve_args(&args)?;
|
let daemon = config::DaemonConfig::from_serve_args(&args)?;
|
||||||
tracing::init(cli.verbose, &daemon.telemetry)?;
|
tracing::init(cli.verbose, &daemon.telemetry)?;
|
||||||
runtime::serve(daemon).await
|
runtime::serve(daemon).await
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ pub async fn serve(daemon: config::DaemonConfig) -> Result<()> {
|
|||||||
"/api/v1/control/enroll-tokens/{token}",
|
"/api/v1/control/enroll-tokens/{token}",
|
||||||
axum::routing::delete(api::revoke_enroll_token),
|
axum::routing::delete(api::revoke_enroll_token),
|
||||||
)
|
)
|
||||||
|
.route("/api/v1/control/state-stats", get(api::state_stats))
|
||||||
.route("/api/v1/agent/ws", get(ws::agent_ws))
|
.route("/api/v1/agent/ws", get(ws::agent_ws))
|
||||||
.route("/api/v1/control/agents", get(api::list_agents))
|
.route("/api/v1/control/agents", get(api::list_agents))
|
||||||
.route(
|
.route(
|
||||||
@@ -186,6 +187,43 @@ pub async fn issue_enroll_token(args: IssueEnrollTokenArgs) -> Result<()> {
|
|||||||
|
|
||||||
pub async fn list_enroll_tokens(args: ListEnrollTokensArgs) -> Result<()> {
|
pub async fn list_enroll_tokens(args: ListEnrollTokensArgs) -> Result<()> {
|
||||||
let settings = config::resolve_list_enroll_token_settings(&args)?;
|
let settings = config::resolve_list_enroll_token_settings(&args)?;
|
||||||
|
if let Some(base) = settings.public_url.as_deref() {
|
||||||
|
let url = format!(
|
||||||
|
"{}/api/v1/control/enroll-tokens?include_expired={}",
|
||||||
|
base,
|
||||||
|
args.include_expired
|
||||||
|
);
|
||||||
|
let response = reqwest::get(&url)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("failed to call {url}"))?;
|
||||||
|
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 list-enroll-tokens failed with {status}: {body}");
|
||||||
|
}
|
||||||
|
let body: Vec<api::EnrollTokenStatus> = response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.context("failed to decode list-enroll-tokens response")?;
|
||||||
|
if args.json {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
serde_json::to_string_pretty(&body).context("failed to render json")?
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
for token in body {
|
||||||
|
println!(
|
||||||
|
"token={} expires_at_unix={} expired={}",
|
||||||
|
token.enroll_token, token.expires_at_unix, token.expired
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let store = state::Store::load_or_init(&settings.state_file, Vec::new(), Duration::from_secs(1))
|
let store = state::Store::load_or_init(&settings.state_file, Vec::new(), Duration::from_secs(1))
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("failed to initialize store {}", settings.state_file.display()))?;
|
.with_context(|| format!("failed to initialize store {}", settings.state_file.display()))?;
|
||||||
@@ -208,6 +246,30 @@ pub async fn list_enroll_tokens(args: ListEnrollTokensArgs) -> Result<()> {
|
|||||||
|
|
||||||
pub async fn revoke_enroll_token(args: RevokeEnrollTokenArgs) -> Result<()> {
|
pub async fn revoke_enroll_token(args: RevokeEnrollTokenArgs) -> Result<()> {
|
||||||
let settings = config::resolve_revoke_enroll_token_settings(&args)?;
|
let settings = config::resolve_revoke_enroll_token_settings(&args)?;
|
||||||
|
if let Some(base) = settings.public_url.as_deref() {
|
||||||
|
let url = format!("{}/api/v1/control/enroll-tokens/{}", base, args.token);
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let response = client
|
||||||
|
.delete(&url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("failed to call {url}"))?;
|
||||||
|
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 revoke-enroll-token failed with {status}: {body}");
|
||||||
|
}
|
||||||
|
let body: api::RevokeEnrollTokenResponse = response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.context("failed to decode revoke-enroll-token response")?;
|
||||||
|
println!("token={} revoked={}", body.token, body.revoked);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let store = state::Store::load_or_init(&settings.state_file, Vec::new(), Duration::from_secs(1))
|
let store = state::Store::load_or_init(&settings.state_file, Vec::new(), Duration::from_secs(1))
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("failed to initialize store {}", settings.state_file.display()))?;
|
.with_context(|| format!("failed to initialize store {}", settings.state_file.display()))?;
|
||||||
@@ -218,6 +280,38 @@ pub async fn revoke_enroll_token(args: RevokeEnrollTokenArgs) -> Result<()> {
|
|||||||
|
|
||||||
pub async fn state_stats(args: StateStatsArgs) -> Result<()> {
|
pub async fn state_stats(args: StateStatsArgs) -> Result<()> {
|
||||||
let settings = config::resolve_state_stats_settings(&args)?;
|
let settings = config::resolve_state_stats_settings(&args)?;
|
||||||
|
if let Some(base) = settings.public_url.as_deref() {
|
||||||
|
let url = format!("{}/api/v1/control/state-stats", base);
|
||||||
|
let response = reqwest::get(&url)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("failed to call {url}"))?;
|
||||||
|
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 state-stats failed with {status}: {body}");
|
||||||
|
}
|
||||||
|
let body: api::StateStatsResponse = response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.context("failed to decode state-stats response")?;
|
||||||
|
if args.json {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
serde_json::to_string_pretty(&body).context("failed to render json")?
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
println!("db_path={}", body.db_path);
|
||||||
|
println!("schema_version={}", body.schema_version);
|
||||||
|
println!("agent_count={}", body.agent_count);
|
||||||
|
println!("enroll_token_count={}", body.enroll_token_count);
|
||||||
|
println!("expired_enroll_token_count={}", body.expired_enroll_token_count);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let store = state::Store::load_or_init(&settings.state_file, Vec::new(), Duration::from_secs(1))
|
let store = state::Store::load_or_init(&settings.state_file, Vec::new(), Duration::from_secs(1))
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("failed to initialize store {}", settings.state_file.display()))?;
|
.with_context(|| format!("failed to initialize store {}", settings.state_file.display()))?;
|
||||||
|
|||||||
Reference in New Issue
Block a user