Skip to content

Commit c71c675

Browse files
committed
fix(evidence): SKIP absent Dynamo workload instead of failing submission
The robust-operator section recorded Result: FAIL when the Dynamo operator was present but no DynamoGraphDeployment existed, collapsing a failed API query and a successful zero-row query into the same 0. With section FAILs now propagating to a nonzero exit, a stock Dynamo inference bundle + --cncf-submission failed on the default path: the service-metrics section deploys and cleans up its own DGD before the operator section runs, so the operator section saw zero DGDs. Distinguish a failed DGD query (fail closed) from a successful query with zero rows (absent prerequisite -> SKIP), matching the SKIP treatment given to absent gateway/operator/autoscaler prerequisites. Add a subprocess test for the new branch and assert each section collector emits exactly one column-zero verdict. Fixes the cross-review finding on PR #1730. Signed-off-by: Yuan Chen <yuanchen97@gmail.com>
1 parent 7a3853d commit c71c675

2 files changed

Lines changed: 110 additions & 40 deletions

File tree

pkg/evidence/cncf/result_propagation_test.go

Lines changed: 95 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,22 @@ case "${mode}" in
6464
operator-absent)
6565
SECTION="operator"
6666
;;
67+
operator-dynamo-no-dgd)
68+
# Dynamo operator is installed but no DynamoGraphDeployment exists and the
69+
# DGD query itself succeeds: an absent inference workload is an absent
70+
# prerequisite (SKIP), not a failure.
71+
kubectl() {
72+
if [ "${1:-}" = "cluster-info" ]; then
73+
return 0
74+
fi
75+
case " $* " in
76+
*"dynamo-platform-dynamo-operator-controller-manager"*) echo "dynamo-operator 1/1" ;;
77+
*" dynamographdeployments "*) return 0 ;;
78+
esac
79+
return 0
80+
}
81+
SECTION="operator"
82+
;;
6783
autoscaler-absent)
6884
SECTION="cluster-autoscaling"
6985
;;
@@ -170,7 +186,13 @@ func TestEvidenceResultPropagatesThroughCollector(t *testing.T) {
170186
staleOutput bool
171187
wantStatus string
172188
wantEvidence string
173-
wantErr bool
189+
// singleVerdictFile, when set, is an evidence artifact (relative to the
190+
// evidence dir) that must contain exactly one column-zero "**Result:"
191+
// verdict line — the invariant evidence_result relies on. Only set for
192+
// section collectors that emit their own verdict, not for injected
193+
// fixtures that deliberately carry zero or multiple verdicts.
194+
singleVerdictFile string
195+
wantErr bool
174196
}{
175197
{
176198
name: "pass",
@@ -191,22 +213,32 @@ func TestEvidenceResultPropagatesThroughCollector(t *testing.T) {
191213
wantStatus: "SKIP",
192214
},
193215
{
194-
name: "absent gateway emits explicit skip",
195-
mode: "gateway-absent",
196-
displayName: "Inference Gateway",
197-
wantStatus: "SKIP",
216+
name: "absent gateway emits explicit skip",
217+
mode: "gateway-absent",
218+
displayName: "Inference Gateway",
219+
wantStatus: "SKIP",
220+
singleVerdictFile: "inference-gateway.md",
198221
},
199222
{
200-
name: "absent operator emits explicit skip",
201-
mode: "operator-absent",
202-
displayName: "Robust AI Operator",
203-
wantStatus: "SKIP",
223+
name: "absent operator emits explicit skip",
224+
mode: "operator-absent",
225+
displayName: "Robust AI Operator",
226+
wantStatus: "SKIP",
227+
singleVerdictFile: "robust-operator.md",
204228
},
205229
{
206-
name: "absent autoscaler emits explicit skip",
207-
mode: "autoscaler-absent",
208-
displayName: "Cluster Autoscaling",
209-
wantStatus: "SKIP",
230+
name: "present operator without DynamoGraphDeployment skips",
231+
mode: "operator-dynamo-no-dgd",
232+
displayName: "Robust AI Operator",
233+
wantStatus: "SKIP",
234+
singleVerdictFile: "robust-operator.md",
235+
},
236+
{
237+
name: "absent autoscaler emits explicit skip",
238+
mode: "autoscaler-absent",
239+
displayName: "Cluster Autoscaling",
240+
wantStatus: "SKIP",
241+
singleVerdictFile: "cluster-autoscaling.md",
210242
},
211243
{
212244
name: "fail is not masked by overview prose",
@@ -273,34 +305,38 @@ func TestEvidenceResultPropagatesThroughCollector(t *testing.T) {
273305
wantErr: true,
274306
},
275307
{
276-
name: "present but unhealthy Dynamo is fail",
277-
mode: "dynamo-unhealthy",
278-
displayName: "AI Service Metrics",
279-
wantStatus: "FAIL",
280-
wantErr: true,
308+
name: "present but unhealthy Dynamo is fail",
309+
mode: "dynamo-unhealthy",
310+
displayName: "AI Service Metrics",
311+
wantStatus: "FAIL",
312+
singleVerdictFile: "ai-service-metrics.md",
313+
wantErr: true,
281314
},
282315
{
283-
name: "present but unhealthy NIM is fail",
284-
mode: "nim-unhealthy",
285-
displayName: "AI Service Metrics",
286-
wantStatus: "FAIL",
287-
wantErr: true,
316+
name: "present but unhealthy NIM is fail",
317+
mode: "nim-unhealthy",
318+
displayName: "AI Service Metrics",
319+
wantStatus: "FAIL",
320+
singleVerdictFile: "ai-service-metrics.md",
321+
wantErr: true,
288322
},
289323
{
290-
name: "Dynamo Prometheus connection failure is explicit fail",
291-
mode: "dynamo-prometheus-unavailable",
292-
displayName: "AI Service Metrics",
293-
wantStatus: "FAIL",
294-
wantEvidence: "**Result: FAIL** — Could not connect to Prometheus.",
295-
wantErr: true,
324+
name: "Dynamo Prometheus connection failure is explicit fail",
325+
mode: "dynamo-prometheus-unavailable",
326+
displayName: "AI Service Metrics",
327+
wantStatus: "FAIL",
328+
wantEvidence: "**Result: FAIL** — Could not connect to Prometheus.",
329+
singleVerdictFile: "ai-service-metrics.md",
330+
wantErr: true,
296331
},
297332
{
298-
name: "trainer Prometheus connection failure is explicit fail",
299-
mode: "trainer-prometheus-unavailable",
300-
displayName: "AI Service Metrics",
301-
wantStatus: "FAIL",
302-
wantEvidence: "**Result: FAIL** — Could not connect to Prometheus.",
303-
wantErr: true,
333+
name: "trainer Prometheus connection failure is explicit fail",
334+
mode: "trainer-prometheus-unavailable",
335+
displayName: "AI Service Metrics",
336+
wantStatus: "FAIL",
337+
wantEvidence: "**Result: FAIL** — Could not connect to Prometheus.",
338+
singleVerdictFile: "ai-service-metrics.md",
339+
wantErr: true,
304340
},
305341
}
306342

@@ -393,6 +429,30 @@ func TestEvidenceResultPropagatesThroughCollector(t *testing.T) {
393429
t.Errorf("evidence does not contain %q", tt.wantEvidence)
394430
}
395431
}
432+
if tt.singleVerdictFile != "" {
433+
evidencePath := filepath.Join(outputDir, tt.singleVerdictFile)
434+
evidence, evidenceErr := os.ReadFile(evidencePath)
435+
if evidenceErr != nil {
436+
t.Fatalf("read evidence artifact %s: %v", tt.singleVerdictFile, evidenceErr)
437+
}
438+
if got := countColumnZeroVerdicts(string(evidence)); got != 1 {
439+
t.Errorf("evidence %s has %d column-zero **Result: verdicts, want exactly 1", tt.singleVerdictFile, got)
440+
}
441+
}
396442
})
397443
}
398444
}
445+
446+
// countColumnZeroVerdicts counts lines that begin at column zero with the
447+
// "**Result:" verdict marker — the same anchoring evidence_result() uses to
448+
// ignore numbered overview prose. Section collectors must emit exactly one so
449+
// the parser never fails closed on a healthy check.
450+
func countColumnZeroVerdicts(evidence string) int {
451+
count := 0
452+
for _, line := range strings.Split(evidence, "\n") {
453+
if strings.HasPrefix(line, "**Result:") {
454+
count++
455+
}
456+
}
457+
return count
458+
}

pkg/evidence/cncf/scripts/collect-evidence.sh

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2470,23 +2470,33 @@ INVALID_CR
24702470
echo "WARNING: Webhook did not reject the invalid resource." >> "${EVIDENCE_FILE}"
24712471
fi
24722472

2473-
# Verdict — require DGD + healthy workload pods; webhook rejection strengthens but is optional
2473+
# Verdict — require DGD + healthy workload pods; webhook rejection strengthens but is optional.
2474+
# Distinguish a failed DGD query (fail closed) from a successful query returning
2475+
# zero rows: an absent inference workload is an absent prerequisite (SKIP), not a
2476+
# failure, because a stock inference recipe deploys the operator but no persistent
2477+
# DynamoGraphDeployment (the service-metrics section deploys and cleans up its own).
24742478
echo "" >> "${EVIDENCE_FILE}"
2475-
local dgd_count
2476-
dgd_count=$(kubectl get dynamographdeployments -A --no-headers 2>/dev/null | wc -l | tr -d ' ')
2479+
local dgd_query_ok=true dgd_count=0 dgd_out
2480+
if dgd_out=$(kubectl get dynamographdeployments -A --no-headers 2>/dev/null); then
2481+
dgd_count=$(printf '%s' "${dgd_out}" | grep -c . || true)
2482+
else
2483+
dgd_query_ok=false
2484+
fi
24772485
local running_pods
24782486
running_pods=$(kubectl get pods -n dynamo-workload -l nvidia.com/dynamo-graph-deployment-name --no-headers 2>/dev/null | grep -c "Running" || true)
24792487
local webhook_ok
24802488
webhook_ok=$(echo "${webhook_result}" | grep -ci "denied\|forbidden\|invalid\|error" || true)
24812489

2482-
if [ "${dgd_count}" -gt 0 ] && [ "${running_pods}" -gt 0 ] && [ "${webhook_ok}" -gt 0 ]; then
2490+
if [ "${dgd_query_ok}" != "true" ]; then
2491+
echo "**Result: FAIL** — DynamoGraphDeployment query failed (fail closed); rerun against a reachable API server." >> "${EVIDENCE_FILE}"
2492+
elif [ "${dgd_count}" -gt 0 ] && [ "${running_pods}" -gt 0 ] && [ "${webhook_ok}" -gt 0 ]; then
24832493
echo "**Result: PASS** — Dynamo operator running, webhooks operational (rejection verified), CRDs registered, DynamoGraphDeployment reconciled with ${running_pods} healthy workload pod(s)." >> "${EVIDENCE_FILE}"
24842494
elif [ "${dgd_count}" -gt 0 ] && [ "${running_pods}" -gt 0 ]; then
24852495
echo "**Result: PASS** — Dynamo operator running, CRDs registered, DynamoGraphDeployment reconciled with ${running_pods} healthy workload pod(s)." >> "${EVIDENCE_FILE}"
24862496
elif [ "${dgd_count}" -gt 0 ]; then
24872497
echo "**Result: FAIL** — DynamoGraphDeployment found but no healthy workload pods." >> "${EVIDENCE_FILE}"
24882498
else
2489-
echo "**Result: FAIL** — No DynamoGraphDeployment found." >> "${EVIDENCE_FILE}"
2499+
echo "**Result: SKIP (prerequisite absent)** — Dynamo operator present but no DynamoGraphDeployment exists; deploy an inference workload (e.g. demos/workloads/inference/vllm-agg.yaml) to exercise operator reconciliation." >> "${EVIDENCE_FILE}"
24902500
fi
24912501

24922502
log_info "Robust operator evidence collection complete."

0 commit comments

Comments
 (0)