Skip to content
Merged
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ and listed with their assets on the
shows its start and its worktrees.
- **`ServiceView.singleton`** (ADR-0080, ADR-0081): whether a service runs
once per machine, in the main working tree only.
- **The ports of a machine** (ADR-0082): `GET /v1/ports` lists every port
a stack holds or uses on this machine, per lane: port, protocol, lane,
what it is (a service's fixed port, an instance's, a container's
published or held port, a lane on a node's forward), what holds it, how
(proxy, forward, the process itself, Docker), where it leads and whether
it listens. The main tree's daemon includes its lanes'. The hub's `GET
/hub/ports` joins every registered stack's (a stopped stack's from what
its stack file declares) and marks the ports two stacks share;
`devstack doctor` lists them.

## [0.32.0] - 2026-10-30

Expand Down
3 changes: 3 additions & 0 deletions crates/devstack-compose/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,9 @@ pub struct Publisher {
pub target_port: u16,
#[serde(default, deserialize_with = "nullable")]
pub published_port: u16,
/// `tcp` or `udp`; empty when Compose does not say.
#[serde(default, deserialize_with = "nullable")]
pub protocol: String,
}

// What a published port does when this machine connects to it.
Expand Down
16 changes: 16 additions & 0 deletions crates/devstack-daemon/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ pub fn router(state: AppState) -> axum::Router {
fn routes(state: AppState) -> axum::Router {
axum::Router::new()
.route("/v1/stack", get(stack))
.route("/v1/ports", get(ports))
.route("/v1/journal", get(journal))
.route("/v1/telemetry/take", post(telemetry_take))
.route("/v1/warm", get(warm_view))
Expand Down Expand Up @@ -297,6 +298,21 @@ async fn stack(State(s): State<AppState>) -> Response {
Json(s.sup.view()).into_response()
}

/// `GET /v1/ports`: every port this tree holds or uses on this machine
/// (ADR-0082), in the main tree its lanes' too: those here from their
/// supervisors, those on a node by their forwards here.
async fn ports(State(s): State<AppState>) -> Response {
let entries = tokio::task::spawn_blocking(move || match &s.lanes {
Some(l) => l.port_entries(),
None => crate::ports::tree_entries(&s.sup.port_facts()),
})
.await;
match entries {
Ok(ports) => Json(serde_json::json!({ "ports": ports })).into_response(),
Err(e) => err(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
}

/// `GET /v1/warm`: in a warm lease's container on a node, whether its
/// `prepare` runs are done and what each did (ADR-0065 point 5).
async fn warm_view() -> Response {
Expand Down
41 changes: 41 additions & 0 deletions crates/devstack-daemon/src/lanes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1488,6 +1488,47 @@ impl Lanes {
})
}

/// Every port the stack holds on this machine (ADR-0082): the main
/// tree's, each started lane's here from its supervisor, and each lane
/// on a node's forwards here.
pub fn port_entries(&self) -> Vec<crate::ports::PortEntry> {
let mut out = crate::ports::tree_entries(&self.main.port_facts());
let local: Vec<Arc<Lane>> = self.map.lock().unwrap().values().cloned().collect();
for l in local {
out.extend(crate::ports::tree_entries(&l.sup.port_facts()));
}
let remote: Vec<Arc<RemoteLane>> = self.remote.lock().unwrap().values().cloned().collect();
if remote.is_empty() {
return out;
}
let step = self.main.stack.lanes.port_step;
let numbers: BTreeMap<String, u16> = self
.known
.lock()
.unwrap()
.iter()
.map(|(n, k)| (n.clone(), k.entry.number))
.collect();
let published = self.main.published_fixed();
for r in remote {
let (tcp, udp, listening) = r.forwarded();
let holders = numbers
.get(&r.name)
.and_then(|n| lane_offset(*n, step))
.map(|off| crate::ports::lane_holders(&self.main.stack, &published, off))
.unwrap_or_default();
out.extend(crate::ports::node_entries(&crate::ports::NodeFacts {
lane: r.name.clone(),
node: r.node.name.clone(),
tcp,
udp,
listening,
holders,
}));
}
out
}

/// Stop a lane, if it runs, and drop it with its state. Not while it is
/// being started (its supervisor is being set up from that state): the
/// next sync forgets it, once the start is over.
Expand Down
1 change: 1 addition & 0 deletions crates/devstack-daemon/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub mod health;
pub mod journal;
pub mod lanes;
pub mod logfiles;
pub mod ports;
pub mod proxy;
pub mod rebuild;
pub mod registry;
Expand Down
Loading
Loading