Skip to content

Commit 67e07d5

Browse files
committed
web: render Resolution metadata for Resolved build steps
Build step pages branch on status=13 to show the original drv, resolved basename, and the terminal step link instead of the Build/Substitution info table; the log endpoint 302s to the terminal's log or back to the step page when no terminal log exists yet.
1 parent 4030c68 commit 67e07d5

8 files changed

Lines changed: 719 additions & 45 deletions

File tree

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
use strict;
2+
use warnings;
3+
use Setup;
4+
use JSON::MaybeXS qw(decode_json);
5+
use Nix::Config;
6+
use Test2::V0;
7+
use HTTP::Request::Common;
8+
9+
my %ctx = test_init();
10+
setup_catalyst_test($ctx{context});
11+
12+
my $db = $ctx{context}->db();
13+
14+
my $project = $db->resultset('Projects')->create({
15+
name => "tests", displayname => "", owner => "root",
16+
});
17+
my $jobset = createBaseJobset($db, "basic", "basic.nix", $ctx{jobsdir});
18+
ok(evalSucceeds($ctx{context}, $jobset), "Evaluating basic.nix succeeds");
19+
my @builds = queuedBuildsForJobset($jobset);
20+
my ($build) = grep { $_->nixname eq "empty-dir" } @builds;
21+
ok(defined $build, "got a build out of the jobset");
22+
23+
my $resolvedBasename = "00000000000000000000000000000001-resolved.drv";
24+
my $storeDir = $Nix::Config::storeDir;
25+
my $unresolvedDrv = "$storeDir/00000000000000000000000000000000-unresolved.drv";
26+
27+
# Step 101: Resolved with terminal at step 102 (Succeeded).
28+
$db->resultset('BuildSteps')->create({
29+
build => $build->id,
30+
stepnr => 101,
31+
type => 0,
32+
drvpath => $unresolvedDrv,
33+
busy => 0,
34+
status => 13,
35+
machine => "",
36+
resolveddrvpath => $resolvedBasename,
37+
});
38+
39+
$db->resultset('BuildSteps')->create({
40+
build => $build->id,
41+
stepnr => 102,
42+
type => 0,
43+
drvpath => "$storeDir/$resolvedBasename",
44+
busy => 0,
45+
status => 0,
46+
stoptime => 1000,
47+
machine => "",
48+
});
49+
50+
# Step 103: Resolved but terminal drv has no buildstep row yet.
51+
$db->resultset('BuildSteps')->create({
52+
build => $build->id,
53+
stepnr => 103,
54+
type => 0,
55+
drvpath => "$storeDir/00000000000000000000000000000002-unresolved2.drv",
56+
busy => 0,
57+
status => 13,
58+
machine => "",
59+
resolveddrvpath => "00000000000000000000000000000099-missing.drv",
60+
});
61+
62+
my $buildUrl = "/build/" . $build->id;
63+
64+
subtest "log of a Resolved step 302s to the terminal step's log" => sub {
65+
my $r = request(GET "$buildUrl/step/101/log");
66+
is($r->code, 302, "GET log of Resolved step redirects");
67+
my $loc = $r->header('location') // "";
68+
like($loc, qr{/build/\Q@{[$build->id]}\E/step/102/log},
69+
"Location points at terminal step's log");
70+
};
71+
72+
subtest "page of a Resolved step renders Resolution layout with terminal link" => sub {
73+
my $r = request(GET "$buildUrl/step/101");
74+
is($r->code, 200, "step page renders");
75+
like($r->content, qr/Resolution/,
76+
"Type cell shows Resolution");
77+
like($r->content, qr/unresolved\.drv/,
78+
"page shows the original (pre-resolution) derivation");
79+
like($r->content, qr/\Q$resolvedBasename\E/,
80+
"page shows the resolved drv basename");
81+
like($r->content, qr/step #102/,
82+
"page links to the terminal step");
83+
};
84+
85+
subtest "page of a Resolved step whose terminal is missing renders pending state" => sub {
86+
my $r = request(GET "$buildUrl/step/103");
87+
is($r->code, 200, "missing-terminal page renders");
88+
like($r->content, qr/missing\.drv/,
89+
"page names the missing resolved drv");
90+
like($r->content, qr/not yet scheduled/i,
91+
"page describes the unscheduled state");
92+
};
93+
94+
subtest "log of a Resolved step with no terminal redirects to its page" => sub {
95+
my $r = request(GET "$buildUrl/step/103/log");
96+
is($r->code, 302, "log endpoint still redirects");
97+
my $loc = $r->header('location') // "";
98+
like($loc, qr{/build/\Q@{[$build->id]}\E/step/103$},
99+
"redirects to step page, not a log URL");
100+
};
101+
102+
subtest "get-info JSON exposes resolvedSteps" => sub {
103+
my $r = request(GET "$buildUrl/api/get-info", Accept => 'application/json');
104+
is($r->code, 200, "api/get-info responds");
105+
my $data = decode_json($r->content);
106+
ok($data->{resolvedSteps}, "resolvedSteps key present");
107+
is(scalar @{$data->{resolvedSteps}}, 2, "two resolved steps reported");
108+
my ($terminalEntry) = grep { $_->{stepnr} == 101 } @{$data->{resolvedSteps}};
109+
is($terminalEntry->{resolvedDrvPath}, $resolvedBasename,
110+
"step 101 reports resolved basename");
111+
is($terminalEntry->{terminal}{stepnr}, 102,
112+
"step 101 terminal is step 102");
113+
is($terminalEntry->{terminal}{status}, 0,
114+
"terminal status is Success");
115+
};
116+
117+
subtest "self-referential resolved step renders cycle copy on its page" => sub {
118+
my $cycleBasename = "00000000000000000000000000000003-cycle.drv";
119+
$db->resultset('BuildSteps')->create({
120+
build => $build->id,
121+
stepnr => 104,
122+
type => 0,
123+
drvpath => "$storeDir/$cycleBasename",
124+
busy => 0,
125+
status => 13,
126+
machine => "",
127+
resolveddrvpath => $cycleBasename,
128+
});
129+
my $r = request(GET "$buildUrl/step/104");
130+
is($r->code, 200, "self-cycle page renders");
131+
like($r->content, qr/cycles back/i,
132+
"page describes the cycle, not 'not yet scheduled'");
133+
134+
my $logR = request(GET "$buildUrl/step/104/log");
135+
is($logR->code, 302, "log endpoint redirects");
136+
like($logR->header('location') // "",
137+
qr{/build/\Q@{[$build->id]}\E/step/104$},
138+
"self-cycle log redirects to the step page");
139+
};
140+
141+
subtest "terminal log carries a 'resolution target' banner" => sub {
142+
my $r = request(GET "$buildUrl/step/102/log");
143+
is($r->code, 200, "terminal log renders");
144+
like($r->content, qr/resolution target/i,
145+
"banner labels the relation, not a redirect");
146+
like($r->content, qr/step #101/,
147+
"banner names the resolved origin");
148+
like($r->content, qr/Original derivation.*unresolved\.drv/s,
149+
"banner shows the original (pre-resolution) drvpath");
150+
};
151+
152+
subtest "page with running terminal shows Running status" => sub {
153+
$db->resultset('BuildSteps')->create({
154+
build => $build->id,
155+
stepnr => 200,
156+
type => 0,
157+
drvpath => "$storeDir/00000000000000000000000000000020-pending-orig.drv",
158+
busy => 0,
159+
status => 13,
160+
machine => "",
161+
resolveddrvpath => "00000000000000000000000000000021-busy-terminal.drv",
162+
});
163+
$db->resultset('BuildSteps')->create({
164+
build => $build->id,
165+
stepnr => 201,
166+
type => 0,
167+
drvpath => "$storeDir/00000000000000000000000000000021-busy-terminal.drv",
168+
busy => 1,
169+
status => undef,
170+
machine => "builder-a",
171+
starttime => 500,
172+
});
173+
my $r = request(GET "$buildUrl/step/200");
174+
is($r->code, 200, "page renders for busy-terminal case");
175+
like($r->content, qr/Running/,
176+
"status shows Running for the busy terminal");
177+
like($r->content, qr/step #201/,
178+
"names the running step");
179+
180+
my $logR = request(GET "$buildUrl/step/200/log");
181+
is($logR->code, 302, "log endpoint redirects");
182+
like($logR->header('location') // "",
183+
qr{/build/\Q@{[$build->id]}\E/step/200$},
184+
"running terminal has no log yet, redirects back to step page");
185+
};
186+
187+
subtest "cross-build scoping: chain does not reach into other builds" => sub {
188+
my $buildB = $db->resultset('Builds')->create({
189+
finished => 1,
190+
timestamp => 0,
191+
jobset_id => $build->jobset_id,
192+
job => "empty-dir",
193+
nixname => "empty-dir",
194+
drvpath => "$storeDir/00000000000000000000000000000040-other-unresolved.drv",
195+
system => "x86_64-linux",
196+
starttime => 1, stoptime => 1,
197+
buildstatus => 0, iscurrent => 0,
198+
});
199+
200+
$db->resultset('BuildSteps')->create({
201+
build => $build->id,
202+
stepnr => 301,
203+
type => 0,
204+
drvpath => "$storeDir/00000000000000000000000000000040-origA.drv",
205+
busy => 0,
206+
status => 13,
207+
machine => "",
208+
resolveddrvpath => "00000000000000000000000000000041-collide.drv",
209+
});
210+
211+
$db->resultset('BuildSteps')->create({
212+
build => $buildB->id,
213+
stepnr => 500,
214+
type => 0,
215+
drvpath => "$storeDir/00000000000000000000000000000041-collide.drv",
216+
busy => 0,
217+
status => 0,
218+
stoptime => 1000,
219+
machine => "",
220+
});
221+
222+
my $r = request(GET "$buildUrl/step/301");
223+
is($r->code, 200, "page renders without chasing cross-build terminal");
224+
like($r->content, qr/not yet scheduled/i,
225+
"chain stops at build boundary");
226+
};
227+
228+
subtest "sibling steps with shared terminal redirect to the same target" => sub {
229+
$db->resultset('BuildSteps')->create({
230+
build => $build->id,
231+
stepnr => 400,
232+
type => 0,
233+
drvpath => "$storeDir/00000000000000000000000000000050-first.drv",
234+
busy => 0,
235+
status => 13,
236+
machine => "",
237+
resolveddrvpath => "00000000000000000000000000000051-shared.drv",
238+
});
239+
$db->resultset('BuildSteps')->create({
240+
build => $build->id,
241+
stepnr => 401,
242+
type => 0,
243+
drvpath => "$storeDir/00000000000000000000000000000060-second.drv",
244+
busy => 0,
245+
status => 13,
246+
machine => "",
247+
resolveddrvpath => "00000000000000000000000000000051-shared.drv",
248+
});
249+
$db->resultset('BuildSteps')->create({
250+
build => $build->id,
251+
stepnr => 402,
252+
type => 0,
253+
drvpath => "$storeDir/00000000000000000000000000000051-shared.drv",
254+
busy => 0,
255+
status => 0,
256+
stoptime => 1000,
257+
machine => "",
258+
});
259+
260+
for my $origin (400, 401) {
261+
my $r = request(GET "$buildUrl/step/$origin/log");
262+
is($r->code, 302, "step $origin log redirects (no false cycle)");
263+
like($r->header('location') // "", qr{/step/402/log},
264+
"step $origin lands on the shared terminal's log");
265+
}
266+
267+
my $term = request(GET "$buildUrl/step/402/log");
268+
is($term->code, 200, "shared terminal log renders");
269+
like($term->content, qr/resolution target of:/i,
270+
"banner enters the multi-origin form when more than one step resolved here");
271+
like($term->content, qr/step #400/, "banner lists step 400");
272+
like($term->content, qr/step #401/, "banner lists step 401");
273+
};
274+
275+
subtest "corrupt resolveddrvpath does not crash or chase" => sub {
276+
$db->resultset('BuildSteps')->create({
277+
build => $build->id,
278+
stepnr => 500,
279+
type => 0,
280+
drvpath => "$storeDir/00000000000000000000000000000070-corrupt.drv",
281+
busy => 0,
282+
status => 13,
283+
machine => "",
284+
resolveddrvpath => "../../etc/passwd",
285+
});
286+
my $r = request(GET "$buildUrl/step/500");
287+
is($r->code, 200, "corrupt basename does not crash");
288+
like($r->content, qr/not yet scheduled/i,
289+
"falls through to pending state rather than following the hop");
290+
};
291+
292+
subtest "multi-step resolution cycle is classified as cycle" => sub {
293+
$db->resultset('BuildSteps')->create({
294+
build => $build->id,
295+
stepnr => 600,
296+
type => 0,
297+
drvpath => "$storeDir/00000000000000000000000000000080-cycle-a.drv",
298+
busy => 0,
299+
status => 13,
300+
machine => "",
301+
resolveddrvpath => "00000000000000000000000000000081-cycle-b.drv",
302+
});
303+
$db->resultset('BuildSteps')->create({
304+
build => $build->id,
305+
stepnr => 601,
306+
type => 0,
307+
drvpath => "$storeDir/00000000000000000000000000000081-cycle-b.drv",
308+
busy => 0,
309+
status => 13,
310+
machine => "",
311+
resolveddrvpath => "00000000000000000000000000000080-cycle-a.drv",
312+
});
313+
my $r = request(GET "$buildUrl/step/600");
314+
is($r->code, 200, "page renders for multi-step cycle");
315+
like($r->content, qr/cycles back/i,
316+
"multi-step cycle uses the cycle copy");
317+
};
318+
319+
subtest "raw log mode propagates through the resolved redirect" => sub {
320+
my $r = request(GET "$buildUrl/step/101/log/raw");
321+
is($r->code, 302, "raw on a resolved step still redirects");
322+
like($r->header('location') // "", qr{/step/102/log/raw},
323+
"raw mode is preserved in the redirect target");
324+
};
325+
326+
subtest "old /nixlog/:nr URL still 301-redirects to /step/:nr/log" => sub {
327+
my $r = request(GET "$buildUrl/nixlog/101");
328+
is($r->code, 301, "old nixlog URL 301s to the step log endpoint");
329+
like($r->header('location') // "", qr{/step/101/log},
330+
"preserves stepnr in the redirect target");
331+
};
332+
333+
done_testing;

subprojects/hydra/lib/Hydra/Controller/Build.pm

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,20 @@ sub build_GET {
117117

118118
$c->stash->{steps} = [$build->buildsteps->search({}, {order_by => "stepnr desc"})];
119119

120+
$c->stash->{resolvedTerminals} = {};
121+
my %chainCache;
122+
for my $step (@{$c->stash->{steps}}) {
123+
next unless defined $step->status && $step->status == 13 && $step->resolveddrvpath;
124+
my ($terminal, $chain) = followResolvedChain($c, $step, \%chainCache);
125+
next unless $terminal;
126+
next if $terminal->get_column('build') == $step->get_column('build')
127+
&& $terminal->stepnr == $step->stepnr;
128+
$c->stash->{resolvedTerminals}->{$step->stepnr} = {
129+
terminal => $terminal,
130+
chain => $chain,
131+
};
132+
}
133+
120134
$c->stash->{binaryCachePublicUri} = $c->config->{binary_cache_public_uri};
121135
}
122136

@@ -563,6 +577,32 @@ sub get_info : Chained('buildChain') PathPart('api/get-info') Args(0) {
563577
$c->stash->{json}->{drvPath} = $build->drvpath;
564578
my $out = getMainOutput($build);
565579
$c->stash->{json}->{outPath} = $out->path if defined $out;
580+
581+
my @resolved;
582+
my %chainCache;
583+
for my $step ($build->buildsteps->search({ status => 13 })) {
584+
next unless $step->resolveddrvpath;
585+
my ($terminal, $chain) = followResolvedChain($c, $step, \%chainCache);
586+
my $entry = {
587+
stepnr => $step->stepnr,
588+
resolvedDrvPath => $step->resolveddrvpath,
589+
chain => $chain,
590+
};
591+
if ($terminal
592+
&& !($terminal->get_column('build') == $build->id
593+
&& $terminal->stepnr == $step->stepnr))
594+
{
595+
$entry->{terminal} = {
596+
buildId => $terminal->get_column('build'),
597+
stepnr => $terminal->stepnr,
598+
status => $terminal->status,
599+
busy => $terminal->busy,
600+
};
601+
}
602+
push @resolved, $entry;
603+
}
604+
$c->stash->{json}->{resolvedSteps} = \@resolved if @resolved;
605+
566606
$c->forward('View::JSON');
567607
}
568608

0 commit comments

Comments
 (0)