Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,18 @@ table is a summary and may not be exhaustive.
| GET | `/miner` | Full state snapshot |
| PATCH | `/miner` | Update miner config (e.g. pause) |

### Config

| Method | Path | Description |
|--------|-----------|---------------------------------------|
| GET | `/config` | Current configuration tree, read-only |

Read-only for now: this reflects the `MUJINA_POOL_*` environment
variables (see the [top-level README](../README.md) for those).
File-based config layers, writes, and persistence land in later
increments. Pool passwords are never included in the response;
`pool.password_set` only reports whether one is configured.

### Boards

| Method | Path | Description |
Expand Down
65 changes: 62 additions & 3 deletions mujina-miner/src/api/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use super::{
v0,
};
use crate::api_client::types::MinerTelemetry;
use crate::config::PoolConfig;

/// API server configuration.
#[derive(Debug, Clone)]
Expand All @@ -34,6 +35,7 @@ pub(crate) struct SharedState {
pub miner_telemetry_rx: watch::Receiver<MinerTelemetry>,
pub board_registry: Arc<Mutex<BoardRegistry>>,
pub scheduler_cmd_tx: mpsc::Sender<SchedulerCommand>,
pub pool_config: Arc<Option<PoolConfig>>,
}

impl SharedState {
Expand Down Expand Up @@ -65,6 +67,7 @@ pub async fn serve(
miner_telemetry_rx: watch::Receiver<MinerTelemetry>,
mut board_reg_rx: mpsc::Receiver<BoardRegistration>,
scheduler_cmd_tx: mpsc::Sender<SchedulerCommand>,
pool_config: Arc<Option<PoolConfig>>,
) -> Result<()> {
let board_registry = Arc::new(Mutex::new(BoardRegistry::new()));

Expand All @@ -79,7 +82,12 @@ pub async fn serve(
}
});

let app = build_router(miner_telemetry_rx, board_registry, scheduler_cmd_tx);
let app = build_router(
miner_telemetry_rx,
board_registry,
scheduler_cmd_tx,
pool_config,
);

let listener = TcpListener::bind(&config.bind_addr).await?;
let actual_addr = listener.local_addr()?;
Expand Down Expand Up @@ -110,11 +118,13 @@ pub(crate) fn build_router(
miner_telemetry_rx: watch::Receiver<MinerTelemetry>,
board_registry: Arc<Mutex<BoardRegistry>>,
scheduler_cmd_tx: mpsc::Sender<SchedulerCommand>,
pool_config: Arc<Option<PoolConfig>>,
) -> Router {
let state = SharedState {
miner_telemetry_rx,
board_registry,
scheduler_cmd_tx,
pool_config,
};

let (router, api) = OpenApiRouter::new()
Expand Down Expand Up @@ -142,7 +152,7 @@ mod tests {
use super::*;
use crate::api::commands::SchedulerCommand;
use crate::api::registry::BoardRegistration;
use crate::api_client::types::{BoardTelemetry, SourceTelemetry};
use crate::api_client::types::{BoardTelemetry, MinerConfig, SourceTelemetry};

/// Test fixtures returned by the router builder.
struct TestFixtures {
Expand All @@ -158,6 +168,14 @@ mod tests {
fn build_test_router(
miner_state: MinerTelemetry,
board_states: Vec<BoardTelemetry>,
) -> TestFixtures {
build_test_router_with_pool_config(miner_state, board_states, None)
}

fn build_test_router_with_pool_config(
miner_state: MinerTelemetry,
board_states: Vec<BoardTelemetry>,
pool_config: Option<PoolConfig>,
) -> TestFixtures {
let (miner_tx, miner_rx) = watch::channel(miner_state);
let (cmd_tx, cmd_rx) = mpsc::channel::<SchedulerCommand>(16);
Expand All @@ -171,7 +189,12 @@ mod tests {
}

TestFixtures {
router: build_router(miner_rx, Arc::new(Mutex::new(registry)), cmd_tx),
router: build_router(
miner_rx,
Arc::new(Mutex::new(registry)),
cmd_tx,
Arc::new(pool_config),
),
_board_senders: board_senders,
_miner_tx: miner_tx,
_cmd_rx: cmd_rx,
Expand Down Expand Up @@ -362,4 +385,40 @@ mod tests {
let (status, _body) = get(fixtures.router.clone(), "/api/v0/nope").await;
assert_eq!(status, 404);
}

#[tokio::test]
async fn config_returns_null_pool_when_none_configured() {
let fixtures = build_test_router_with_pool_config(MinerTelemetry::default(), vec![], None);
let (status, body) = get(fixtures.router.clone(), "/api/v0/config").await;
assert_eq!(status, 200);

let config: MinerConfig = serde_json::from_str(&body).unwrap();
assert!(config.pool.is_none());
}

#[tokio::test]
async fn config_exposes_pool_without_password() {
let fixtures = build_test_router_with_pool_config(
MinerTelemetry::default(),
vec![],
Some(PoolConfig {
url: "stratum+tcp://pool.example:3333".into(),
username: "alice.worker1".into(),
password: "hunter2".into(),
password_set: true,
}),
);
let (status, body) = get(fixtures.router.clone(), "/api/v0/config").await;
assert_eq!(status, 200);
assert!(
!body.contains("hunter2"),
"response must never echo the pool password: {body}"
);

let config: MinerConfig = serde_json::from_str(&body).unwrap();
let pool = config.pool.expect("pool should be present");
assert_eq!(pool.url, "stratum+tcp://pool.example:3333");
assert_eq!(pool.username, "alice.worker1");
assert!(pool.password_set);
}
}
31 changes: 30 additions & 1 deletion mujina-miner/src/api/v0.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ use utoipa_axum::{router::OpenApiRouter, routes};
use super::commands::SchedulerCommand;
use super::server::SharedState;
use crate::api_client::types::{
BoardTelemetry, MinerPatchRequest, MinerTelemetry, SourceTelemetry,
BoardTelemetry, MinerConfig, MinerPatchRequest, MinerTelemetry, PoolConfig, SourceTelemetry,
};

/// Build the v0 API routes with OpenAPI metadata.
pub fn routes() -> OpenApiRouter<SharedState> {
OpenApiRouter::new()
.routes(routes!(health))
.routes(routes!(get_miner, patch_miner))
.routes(routes!(get_config))
.routes(routes!(get_boards))
.routes(routes!(get_board))
.routes(routes!(get_sources))
Expand Down Expand Up @@ -92,6 +93,34 @@ async fn patch_miner(
Ok(Json(state.miner_telemetry()))
}

/// Return the current configuration tree.
///
/// Read-only for now; writes and file-based config layers land in
/// later increments.
#[utoipa::path(
get,
path = "/config",
tag = "config",
responses(
(status = OK, description = "Current configuration tree", body = MinerConfig),
),
)]
async fn get_config(State(state): State<SharedState>) -> Json<MinerConfig> {
Json(MinerConfig {
pool: (*state.pool_config).as_ref().map(PoolConfig::from),
})
}

impl From<&crate::config::PoolConfig> for PoolConfig {
fn from(pool: &crate::config::PoolConfig) -> Self {
Self {
url: pool.url.clone(),
username: pool.username.clone(),
password_set: pool.password_set,
}
}
}

/// Return all connected boards.
#[utoipa::path(
get,
Expand Down
17 changes: 17 additions & 0 deletions mujina-miner/src/api_client/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,23 @@ pub struct SetFanTargetRequest {
pub target_percent: Option<u8>,
}

/// Configuration tree snapshot, read-only for now.
#[derive(Clone, Debug, Default, Deserialize, Serialize, ToSchema)]
pub struct MinerConfig {
pub pool: Option<PoolConfig>,
}

/// Pool configuration as exposed over the API.
///
/// The password is never echoed back; `password_set` only reports
/// whether one is configured.
#[derive(Clone, Debug, Deserialize, Serialize, ToSchema)]
pub struct PoolConfig {
pub url: String,
pub username: String,
pub password_set: bool,
}

/// Job source telemetry.
#[derive(Clone, Debug, Default, Deserialize, Serialize, ToSchema)]
pub struct SourceTelemetry {
Expand Down
Loading
Loading