Skip to content

Commit c39d9ee

Browse files
committed
feat: annotate each progress line with the Kubernetes resource it tracks
A 0-second "Tunnel ready" with a 503-serving data plane (the observedGeneration bug we just fixed) made it clear that users need a fast pivot from a stuck progress line to 'datumctl describe ...' on the exact resource. Without that, the operator's reason string is useful text but its provenance is buried. Add a 'resource: Option<String>' field on ProgressStep, pre-formatted as "HTTPProxy/<tunnel-id>" or "Connector/<connector-name>", populated from the live resource metadata in from_resources. Mapping per step: tunnel accepted → HTTPProxy/<tunnel-id> TLS certificate issued → HTTPProxy/<tunnel-id> connector ready → Connector/<connector-name> iroh DNS published → Connector/<connector-name> route programmed → HTTPProxy/<tunnel-id> envoy metadata propagated → HTTPProxy/<tunnel-id> CLI renders the label inline: ✓ tunnel accepted (0.1s) [HTTPProxy/tunnel-gchhg] … route programmed still pending after 30s [HTTPProxy/tunnel-gchhg]: … ProgressStepKind::resource_kind() is the source of truth for which kind backs each step, used by the test that asserts the wiring is correct across all six steps. No extra API call needed — the connector name was already in scope inside TunnelService::get_active_progress.
1 parent d65ec4d commit c39d9ee

2 files changed

Lines changed: 83 additions & 5 deletions

File tree

cli/src/main.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,14 +1116,19 @@ async fn await_tunnel_progress(
11161116
};
11171117

11181118
for step in &progress.steps {
1119+
let resource = step
1120+
.resource
1121+
.as_deref()
1122+
.map(|r| format!(" [{r}]"))
1123+
.unwrap_or_default();
11191124
let prev = last_status.get(&step.kind).copied();
11201125
if prev != Some(step.status) {
11211126
match step.status {
11221127
StepStatus::Ready => {
11231128
println!(
1124-
" ✓ {} ({:.1}s)",
1129+
" ✓ {} ({:.1}s){resource}",
11251130
step.kind.label(),
1126-
start.elapsed().as_secs_f32()
1131+
start.elapsed().as_secs_f32(),
11271132
);
11281133
pending_since.remove(&step.kind);
11291134
}
@@ -1150,10 +1155,9 @@ async fn await_tunnel_progress(
11501155
.or(step.reason.as_deref())
11511156
.unwrap_or("no detail from controller");
11521157
eprintln!(
1153-
" … {} still pending after {}s: {}",
1158+
" … {} still pending after {}s{resource}: {detail}",
11541159
step.kind.label(),
11551160
since.elapsed().as_secs(),
1156-
detail,
11571161
);
11581162
}
11591163
}

lib/src/tunnels.rs

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,26 @@ pub struct ProgressStep {
187187
pub status: StepStatus,
188188
pub reason: Option<String>,
189189
pub message: Option<String>,
190+
/// Pre-formatted "Kind/name" of the underlying Kubernetes resource
191+
/// (`HTTPProxy/<tunnel_id>` or `Connector/<connector_name>`). The CLI
192+
/// renders this alongside each step so the user can pivot to
193+
/// `datumctl describe ...` on the exact resource that's stuck or
194+
/// reporting a stale Ready. `None` only when the resource doesn't
195+
/// exist server-side (e.g. probing for a tunnel id that's not there).
196+
pub resource: Option<String>,
197+
}
198+
199+
impl ProgressStepKind {
200+
/// The Kubernetes resource kind whose conditions back this step.
201+
pub fn resource_kind(&self) -> &'static str {
202+
match self {
203+
Self::ConnectorReady | Self::IrohDnsPublished => "Connector",
204+
Self::ProxyAccepted
205+
| Self::CertificatesReady
206+
| Self::ProxyProgrammed
207+
| Self::ConnectorMetadataProgrammed => "HTTPProxy",
208+
}
209+
}
190210
}
191211

192212
impl ProgressStep {
@@ -223,10 +243,18 @@ impl TunnelProgress {
223243
fn from_resources(proxy: &HTTPProxy, connector: Option<&Connector>) -> Self {
224244
let proxy_conds = proxy.status.as_ref().and_then(|s| s.conditions.as_deref());
225245
let proxy_gen = proxy.metadata.generation.unwrap_or(0);
246+
let proxy_resource = proxy
247+
.metadata
248+
.name
249+
.as_deref()
250+
.map(|n| format!("HTTPProxy/{n}"));
226251
let conn_conds = connector
227252
.and_then(|c| c.status.as_ref())
228253
.and_then(|s| s.conditions.as_deref());
229254
let conn_gen = connector.and_then(|c| c.metadata.generation).unwrap_or(0);
255+
let connector_resource = connector
256+
.and_then(|c| c.metadata.name.as_deref())
257+
.map(|n| format!("Connector/{n}"));
230258

231259
// A condition is Ready only if its observedGeneration has caught up
232260
// with the resource's current generation. After we PATCH the spec
@@ -238,7 +266,8 @@ impl TunnelProgress {
238266
let make_step = |kind: ProgressStepKind,
239267
conds: Option<&[k8s_openapi::apimachinery::pkg::apis::meta::v1::Condition]>,
240268
type_: &str,
241-
current_gen: i64|
269+
current_gen: i64,
270+
resource: Option<String>|
242271
-> ProgressStep {
243272
let cond = find_condition(conds, type_);
244273
let observed = cond.and_then(|c| c.observed_generation).unwrap_or(0);
@@ -253,6 +282,7 @@ impl TunnelProgress {
253282
status,
254283
reason: cond.map(|c| c.reason.clone()),
255284
message: cond.map(|c| c.message.clone()),
285+
resource,
256286
}
257287
};
258288

@@ -262,36 +292,42 @@ impl TunnelProgress {
262292
proxy_conds,
263293
HTTP_PROXY_CONDITION_ACCEPTED,
264294
proxy_gen,
295+
proxy_resource.clone(),
265296
),
266297
make_step(
267298
ProgressStepKind::CertificatesReady,
268299
proxy_conds,
269300
HTTP_PROXY_CONDITION_CERTIFICATES_READY,
270301
proxy_gen,
302+
proxy_resource.clone(),
271303
),
272304
make_step(
273305
ProgressStepKind::ConnectorReady,
274306
conn_conds,
275307
CONNECTOR_CONDITION_READY,
276308
conn_gen,
309+
connector_resource.clone(),
277310
),
278311
make_step(
279312
ProgressStepKind::IrohDnsPublished,
280313
conn_conds,
281314
CONNECTOR_CONDITION_IROH_DNS_PUBLISHED,
282315
conn_gen,
316+
connector_resource.clone(),
283317
),
284318
make_step(
285319
ProgressStepKind::ProxyProgrammed,
286320
proxy_conds,
287321
HTTP_PROXY_CONDITION_PROGRAMMED,
288322
proxy_gen,
323+
proxy_resource.clone(),
289324
),
290325
make_step(
291326
ProgressStepKind::ConnectorMetadataProgrammed,
292327
proxy_conds,
293328
HTTP_PROXY_CONDITION_CONNECTOR_METADATA_PROGRAMMED,
294329
proxy_gen,
330+
proxy_resource,
295331
),
296332
];
297333

@@ -1604,6 +1640,44 @@ mod tests {
16041640
assert!(progress.terminal_failure().is_none());
16051641
}
16061642

1643+
#[test]
1644+
fn progress_step_carries_resource_label() {
1645+
// Every step should know which Kubernetes resource backs it so the
1646+
// CLI can render "[HTTPProxy/tunnel-test]" or
1647+
// "[Connector/datum-connect-test]" alongside the line — that's
1648+
// what the user copy-pastes into `datumctl describe`.
1649+
let p = proxy(vec![]);
1650+
let c = connector(vec![]);
1651+
let progress = TunnelProgress::from_resources(&p, Some(&c));
1652+
1653+
for step in &progress.steps {
1654+
let resource = step.resource.as_deref().expect("resource label set");
1655+
let expected_kind = step.kind.resource_kind();
1656+
assert!(
1657+
resource.starts_with(&format!("{expected_kind}/")),
1658+
"step {:?} should be backed by {expected_kind}, got {resource}",
1659+
step.kind,
1660+
);
1661+
}
1662+
1663+
// Connector-backed steps fall back to None when no connector exists.
1664+
let progress_no_conn = TunnelProgress::from_resources(&p, None);
1665+
let iroh = progress_no_conn
1666+
.step(ProgressStepKind::IrohDnsPublished)
1667+
.unwrap();
1668+
assert!(
1669+
iroh.resource.is_none(),
1670+
"connector-backed step has no resource when connector is missing"
1671+
);
1672+
let proxy_step = progress_no_conn
1673+
.step(ProgressStepKind::ProxyAccepted)
1674+
.unwrap();
1675+
assert_eq!(
1676+
proxy_step.resource.as_deref(),
1677+
Some("HTTPProxy/tunnel-test")
1678+
);
1679+
}
1680+
16071681
#[test]
16081682
fn progress_pending_when_status_is_stale_for_current_generation() {
16091683
// `tunnel listen --id` PATCHes the HTTPProxy spec to re-point the

0 commit comments

Comments
 (0)