Compare commits
518 Commits
nix-2.17
...
update-fla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83a864607c | ||
|
|
a9581bcdc4 | ||
|
|
26e4d5eb54 | ||
|
|
8bc95a96f7 | ||
|
|
82cd5e0e23 | ||
|
|
c3ed183c64 | ||
|
|
b45f0d1fa7 | ||
|
|
e4fe9d43c1 | ||
|
|
9df4b65c67 | ||
|
|
1d011baed8 | ||
|
|
52b2e4f021 | ||
|
|
f089ff87f5 | ||
|
|
43006db835 | ||
|
|
4ebfaba862 | ||
|
|
41daeb3cc9 | ||
|
|
3b1b5009f3 | ||
|
|
54699ae671 | ||
|
|
503871bac4 | ||
|
|
6dde18cb5e | ||
|
|
e0f65d4d3d | ||
|
|
5e2e9672cf | ||
|
|
650871b586 | ||
|
|
b2030cd4ef | ||
|
|
44780c786e | ||
|
|
2db62e86e7 | ||
|
|
b88b06dd3c | ||
|
|
d042e3c82c | ||
|
|
a31f5d654c | ||
|
|
6659391e26 | ||
|
|
3b901f19a4 | ||
|
|
673e18415e | ||
|
|
13ddeeb08c | ||
|
|
ed15b0a8ce | ||
|
|
f1b26134d7 | ||
|
|
425d78763d | ||
|
|
53d8e26b59 | ||
|
|
a439b7f591 | ||
|
|
7d12fa6a55 | ||
|
|
7a67ba925d | ||
|
|
662d1198d4 | ||
|
|
34ff66a460 | ||
|
|
7a42a3810c | ||
|
|
3bd87005f7 | ||
|
|
95fb69f60d | ||
|
|
241ab71800 | ||
|
|
78ed8d7aa5 | ||
|
|
4bd941daa8 | ||
|
|
0414ae64eb | ||
|
|
449791b1c7 | ||
|
|
d7b40c4233 | ||
|
|
524b232032 | ||
|
|
58846b0a1c | ||
|
|
f1463d4bce | ||
|
|
94eaad22bc | ||
|
|
a499063834 | ||
|
|
3059dc16a3 | ||
|
|
d36b943e93 | ||
|
|
4b2d60e185 | ||
|
|
528a623b32 | ||
|
|
06be60349b | ||
|
|
274027eb50 | ||
|
|
a329537e55 | ||
|
|
5f8ae153b4 | ||
|
|
990fe22f80 | ||
|
|
7fa3da755e | ||
|
|
56f07573ea | ||
|
|
8481acda2f | ||
|
|
75824e546f | ||
|
|
b0c1f689c2 | ||
|
|
a4d7e7df93 | ||
|
|
5cc6ae3ca3 | ||
|
|
19280b3466 | ||
|
|
c6139736ed | ||
|
|
29734ae51f | ||
|
|
38b4d5fa0f | ||
|
|
137761f8cc | ||
|
|
06d20bb8e0 | ||
|
|
c25a2f626d | ||
|
|
0d2a030661 | ||
|
|
fd0b8ec8e0 | ||
|
|
81fd47df42 | ||
|
|
2c4460942d | ||
|
|
2e41e7e8e2 | ||
|
|
242eb72dbb | ||
|
|
bddf15de46 | ||
|
|
5f530d7d56 | ||
|
|
e851d9f9f6 | ||
|
|
f7bda020c6 | ||
|
|
dea1e168f5 | ||
|
|
b47b187553 | ||
|
|
c6424f37a6 | ||
|
|
b94f47ed27 | ||
|
|
615798a51e | ||
|
|
99a6656b40 | ||
|
|
33b5c6fb41 | ||
|
|
5f226f3b6f | ||
|
|
7c4f0ab01a | ||
|
|
0d3842aa2f | ||
|
|
a0ba36db79 | ||
|
|
552ca356ae | ||
|
|
79ba8fdd04 | ||
|
|
b98f9f8e48 | ||
|
|
476c1a6200 | ||
|
|
c645b7ff67 | ||
|
|
c12d0a66d8 | ||
|
|
2f6ec150ec | ||
|
|
2b4f4cf6f4 | ||
|
|
e33b4f88dc | ||
|
|
a9b89ee779 | ||
|
|
84b4fe36b6 | ||
|
|
081d0c079a | ||
|
|
a75c5a405c | ||
|
|
85b330be41 | ||
|
|
1657f6fff4 | ||
|
|
957884d174 | ||
|
|
05a05667d8 | ||
|
|
0527fddd6a | ||
|
|
0017a1d0f3 | ||
|
|
e9895e81af | ||
|
|
424a767035 | ||
|
|
7096ae3a5b | ||
|
|
ec3d0c696b | ||
|
|
d2c10bf851 | ||
|
|
80b9d82ea4 | ||
|
|
85ab735653 | ||
|
|
632a59172a | ||
|
|
95f5d331ee | ||
|
|
6e9e13333f | ||
|
|
7b1968236d | ||
|
|
b812bb5017 | ||
|
|
61573c71d1 | ||
|
|
f50263976c | ||
|
|
c413b275ff | ||
|
|
f7a9113166 | ||
|
|
97ec796db5 | ||
|
|
42400ef20c | ||
|
|
2fcfa969b8 | ||
|
|
4f3b783d30 | ||
|
|
80980f8b32 | ||
|
|
d0008d4238 | ||
|
|
3b89d2d6b5 | ||
|
|
62fcacb7d2 | ||
|
|
b3b48bc237 | ||
|
|
c544042051 | ||
|
|
aa62c7f7db | ||
|
|
605a0e9ce9 | ||
|
|
6786e52eb5 | ||
|
|
9efe38c60b | ||
|
|
c621f27482 | ||
|
|
ed500ca434 | ||
|
|
635aff50dd | ||
|
|
2e3c168ec4 | ||
|
|
362524b563 | ||
|
|
de3646cb13 | ||
|
|
278a3ebfd5 | ||
|
|
dafa252d08 | ||
|
|
8a50488f6c | ||
|
|
8bb7d27588 | ||
|
|
35c9264306 | ||
|
|
da1aebe970 | ||
|
|
183bc39d1a | ||
|
|
2ae27dd20d | ||
|
|
1b5c2fb747 | ||
|
|
8d068fea3e | ||
|
|
8218a9ad1b | ||
|
|
455f1a0665 | ||
|
|
89fcb931ce | ||
|
|
b023cc8f87 | ||
|
|
23755bf001 | ||
|
|
720db63d52 | ||
|
|
bdde73acbd | ||
|
|
0ab357e435 | ||
|
|
6fcfa9e796 | ||
|
|
ffbde9c9e3 | ||
|
|
cf33a9158a | ||
|
|
5f6b075754 | ||
|
|
8d75026513 | ||
|
|
f1a976d3fd | ||
|
|
d5ad16abc2 | ||
|
|
7e0157e387 | ||
|
|
c8de5b99e3 | ||
|
|
a5b17d0686 | ||
|
|
1c52c4c0ed | ||
|
|
b4322edd05 | ||
|
|
8350f964ee | ||
|
|
143a07bff0 | ||
|
|
cc4b206d85 | ||
|
|
e77444da98 | ||
|
|
8a6482bb1c | ||
|
|
b3a433336e | ||
|
|
68b2d6da0a | ||
|
|
c94ba404fd | ||
|
|
56170dd117 | ||
|
|
d4b55f8190 | ||
|
|
78687e23cf | ||
|
|
f02fc5e2ff | ||
|
|
8e02589ac8 | ||
|
|
52a0199a9b | ||
|
|
9265fc5002 | ||
|
|
d8ffa6b56a | ||
|
|
efcf6815d9 | ||
|
|
1e2d3211d9 | ||
|
|
5a9985f96c | ||
|
|
0d0c4f278b | ||
|
|
3fdb18a4bc | ||
|
|
6133693097 | ||
|
|
abe35881e4 | ||
|
|
99359c251a | ||
|
|
9d8f30affe | ||
|
|
33b982f408 | ||
|
|
a816e8e22c | ||
|
|
0159135fc7 | ||
|
|
1d2d3ae6b7 | ||
|
|
257b211832 | ||
|
|
d6a5df25bf | ||
|
|
6534a54ee5 | ||
|
|
1595064bee | ||
|
|
1cb1e139c4 | ||
|
|
6b97e3ab7b | ||
|
|
cad08f87d2 | ||
|
|
3fef32b364 | ||
|
|
ae18a7b3ae | ||
|
|
b657bcdfb7 | ||
|
|
3b4c4972c2 | ||
|
|
b2fe3f5218 | ||
|
|
9911f0107f | ||
|
|
feebb61897 | ||
|
|
4bcbed2f1b | ||
|
|
987dad3371 | ||
|
|
d2db3c7446 | ||
|
|
97dcdae068 | ||
|
|
9a5bd39d4c | ||
|
|
f1deb22c02 | ||
|
|
d22d030503 | ||
|
|
18c0d76210 | ||
|
|
4a4a0f901c | ||
|
|
881462bb4e | ||
|
|
af72b694d8 | ||
|
|
c92342d12f | ||
|
|
df07670a21 | ||
|
|
51944a5fa5 | ||
|
|
341b2f1309 | ||
|
|
4dc0f11379 | ||
|
|
ea09952b7e | ||
|
|
81d21979ef | ||
|
|
0ed9a82912 | ||
|
|
80241fc8be | ||
|
|
4347833f45 | ||
|
|
8835cbd10f | ||
|
|
9ad8ac586c | ||
|
|
9a6928d93b | ||
|
|
810781a802 | ||
|
|
af9b0663f2 | ||
|
|
c6f98202cd | ||
|
|
1dbc7f5845 | ||
|
|
c52845f560 | ||
|
|
85383b9522 | ||
|
|
2f92846e5a | ||
|
|
d84ff32ce6 | ||
|
|
0c9726af59 | ||
|
|
5100b85537 | ||
|
|
141b5fd0b5 | ||
|
|
8d78648e65 | ||
|
|
8a8ac14877 | ||
|
|
250668a19f | ||
|
|
efadb6a26c | ||
|
|
3b16941b14 | ||
|
|
9de9cb0ad8 | ||
|
|
e75a4cbda8 | ||
|
|
6456c1d7d6 | ||
|
|
182a48c9fb | ||
|
|
f974891c76 | ||
|
|
8515cb183e | ||
|
|
60dd7ec187 | ||
|
|
53b04ddf74 | ||
|
|
4e2c06ec2c | ||
|
|
d3966d3e4c | ||
|
|
f442d74f6e | ||
|
|
a9a5b14331 | ||
|
|
e6b9f0dec7 | ||
|
|
72899596df | ||
|
|
bdeec354c3 | ||
|
|
1222ba03a6 | ||
|
|
8a54924d2a | ||
|
|
2a7b070da0 | ||
|
|
c69e30122b | ||
|
|
750275d6e8 | ||
|
|
ceb8b48cce | ||
|
|
95003f2eb5 | ||
|
|
012cbd43f5 | ||
|
|
9a75361781 | ||
|
|
029116422d | ||
|
|
108e409559 | ||
|
|
1a5bd9e103 | ||
|
|
647191cd4a | ||
|
|
73e51b94b1 | ||
|
|
1ef6b5e7b4 | ||
|
|
44248d3cf4 | ||
|
|
cc1b6d394e | ||
|
|
b472f55563 | ||
|
|
c61bdd2c28 | ||
|
|
0231453cc5 | ||
|
|
ae787e5799 | ||
|
|
2dad87ad89 | ||
|
|
b6f44b5cd0 | ||
|
|
c8b7a0fea9 | ||
|
|
2d79b0a4da | ||
|
|
f730433789 | ||
|
|
916531dc9c | ||
|
|
0ead8dc65c | ||
|
|
b1a0501520 | ||
|
|
b94a7b6d5c | ||
|
|
9ee3c6aea2 | ||
|
|
02a514234b | ||
|
|
54a9729a0f | ||
|
|
250780aaf2 | ||
|
|
4bb2f08be1 | ||
|
|
c23973785f | ||
|
|
b2b2d6e26c | ||
|
|
99ca560d58 | ||
|
|
2c886f51d3 | ||
|
|
7de7122479 | ||
|
|
54002f0fcf | ||
|
|
a6b14369ee | ||
|
|
578a3d2292 | ||
|
|
ada51d70fc | ||
|
|
bc19e7cd65 | ||
|
|
d7986226f0 | ||
|
|
2feddd8511 | ||
|
|
cd925e876f | ||
|
|
b3e0d9a8b7 | ||
|
|
5728011da1 | ||
|
|
91bb72e323 | ||
|
|
09a1e64ed2 | ||
|
|
bede2a141a | ||
|
|
b75bf5c882 | ||
|
|
d55bea2a1e | ||
|
|
346badc66f | ||
|
|
a940450875 | ||
|
|
879ceb5cdc | ||
|
|
898ca2f600 | ||
|
|
559376e907 | ||
|
|
21044bc4d9 | ||
|
|
af120e7195 | ||
|
|
71c4e2dc5b | ||
|
|
e4552ddf91 | ||
|
|
e4f2c84f8d | ||
|
|
e10fc2bd13 | ||
|
|
998df1657e | ||
|
|
f99cdaf5fe | ||
|
|
5e910fa2ce | ||
|
|
4b767aa9a2 | ||
|
|
3bf00e31c0 | ||
|
|
2926aa1d64 | ||
|
|
e149da7b9b | ||
|
|
555ea44a7a | ||
|
|
e81c36ac92 | ||
|
|
743795b2b0 | ||
|
|
50378aef22 | ||
|
|
92155f9a07 | ||
|
|
29ce5c603c | ||
|
|
410077a26e | ||
|
|
4bd687e3e6 | ||
|
|
1b8154e67f | ||
|
|
b72528be50 | ||
|
|
8b48579593 | ||
|
|
39a4e4791e | ||
|
|
ef7bf1e67b | ||
|
|
ab1f64aa4d | ||
|
|
3f913a771d | ||
|
|
71986632ce | ||
|
|
1665aed5e3 | ||
|
|
b676b08fac | ||
|
|
d614163e9c | ||
|
|
99afff03b0 | ||
|
|
8f56209bd6 | ||
|
|
806c375c33 | ||
|
|
669617ab54 | ||
|
|
c45c06509a | ||
|
|
9db5d0a88d | ||
|
|
973cb644d3 | ||
|
|
e499509595 | ||
|
|
ceff5c5cfe | ||
|
|
878c0f240e | ||
|
|
c1bd50a80d | ||
|
|
14aabc1cc9 | ||
|
|
7b826ec5ad | ||
|
|
838648c0ce | ||
|
|
6ac4292912 | ||
|
|
b503280256 | ||
|
|
b4c91b5a6a | ||
|
|
8477009310 | ||
|
|
c62eaf248f | ||
|
|
13b5f007ef | ||
|
|
7f5889559e | ||
|
|
5ee0e443e4 | ||
|
|
323b556dc8 | ||
|
|
458b9e4242 | ||
|
|
fcde5908d8 | ||
|
|
083ef46c12 | ||
|
|
7a53b866f6 | ||
|
|
8a02bb7c36 | ||
|
|
c64eed7d07 | ||
|
|
aed130cd17 | ||
|
|
7a6c401d42 | ||
|
|
b5ed0787f7 | ||
|
|
c5f37eca91 | ||
|
|
73b6c1fb11 | ||
|
|
4bbc7b8f75 | ||
|
|
d6d6d1b649 | ||
|
|
1bd195a513 | ||
|
|
1471aacadc | ||
|
|
62ddeb0ff0 | ||
|
|
a876e46894 | ||
|
|
6df06b089e | ||
|
|
cc50fdff6f | ||
|
|
b1fa6b3aac | ||
|
|
f6a2b7562a | ||
|
|
07cb5d1b7c | ||
|
|
449eb2d873 | ||
|
|
2bdbf51d7d | ||
|
|
9e7ac58042 | ||
|
|
d45e14fd43 | ||
|
|
9a86da0e7b | ||
|
|
d02e20a4c1 | ||
|
|
70e5469303 | ||
|
|
2e6ee28f9b | ||
|
|
20b0ad3ba2 | ||
|
|
7386caaecf | ||
|
|
84c46b6b68 | ||
|
|
f1d9230f25 | ||
|
|
f5c0efb11e | ||
|
|
4e8fbaa3d6 | ||
|
|
34c51fcea9 | ||
|
|
4ac31c89df | ||
|
|
db7aa01b8d | ||
|
|
89cfe26533 | ||
|
|
588a0c5269 | ||
|
|
02e453fc8c | ||
|
|
75f26f1fc4 | ||
|
|
3c89067f52 | ||
|
|
abd858d3dc | ||
|
|
163dbf7f54 | ||
|
|
642156372f | ||
|
|
7517c134c5 | ||
|
|
6e67884ff1 | ||
|
|
a6b6c5a539 | ||
|
|
ebfefb9161 | ||
|
|
8783dd53f6 | ||
|
|
f3a760ad9c | ||
|
|
8c10331ee8 | ||
|
|
20f5a2120c | ||
|
|
b56d2383c1 | ||
|
|
2bd67562b5 | ||
|
|
69a5b00e60 | ||
|
|
1d80b72ffb | ||
|
|
105fd18fee | ||
|
|
f6f817926a | ||
|
|
d0d3b0a298 | ||
|
|
3f932a6731 | ||
|
|
aaa0e128c1 | ||
|
|
4515b5aa17 | ||
|
|
411e4d0c24 | ||
|
|
831021808c | ||
|
|
2ee0068fdc | ||
|
|
31ea6458ca | ||
|
|
20c8263e3c | ||
|
|
91bbd5366f | ||
|
|
a45a27851b | ||
|
|
6a54ab24e2 | ||
|
|
58707438ba | ||
|
|
86cd5e9076 | ||
|
|
11f8030b0f | ||
|
|
3df8feb3a2 | ||
|
|
069b7775c5 | ||
|
|
e3443cd22a | ||
|
|
8046ec2668 | ||
|
|
9ba4417940 | ||
|
|
a5d44b60ea | ||
|
|
363604846a | ||
|
|
162b538912 | ||
|
|
104baef503 | ||
|
|
3c5636162a | ||
|
|
874fcae1e8 | ||
|
|
4dc8fe0b08 | ||
|
|
67eeabd518 | ||
|
|
622c25e3c4 | ||
|
|
f216bce0e6 | ||
|
|
4d1c850512 | ||
|
|
c922e73c11 | ||
|
|
e172461e55 | ||
|
|
0917145622 | ||
|
|
2bda7ca642 | ||
|
|
831a2d9bd5 | ||
|
|
5db374cb50 | ||
|
|
e9da80fff6 | ||
|
|
b7c864c515 | ||
|
|
c6263c280c | ||
|
|
3526d61ff2 | ||
|
|
143c31734f | ||
|
|
4a0c5a2570 | ||
|
|
a81c6a3a80 | ||
|
|
750978a192 | ||
|
|
cba85a6a19 | ||
|
|
6e571e26ff | ||
|
|
92b627ac1b | ||
|
|
b430d41afd | ||
|
|
fd0ae78eba | ||
|
|
a778a89f04 | ||
|
|
365776f5d7 | ||
|
|
9f1b911625 | ||
|
|
2f494b7834 | ||
|
|
5db8642224 | ||
|
|
54675a0d94 | ||
|
|
c35791fcc2 | ||
|
|
23fa93c5f8 | ||
|
|
66730993fc | ||
|
|
25d1e8900a |
23
.github/workflows/test.yml
vendored
23
.github/workflows/test.yml
vendored
@@ -1,14 +1,27 @@
|
|||||||
name: "Test"
|
name: "Test"
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
merge_group:
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
runs-on: ubuntu-latest
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- system: x86_64-linux
|
||||||
|
runner: ubuntu-latest
|
||||||
|
- system: aarch64-linux
|
||||||
|
runner: ubuntu-24.04-arm
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: cachix/install-nix-action@v17
|
- uses: cachix/install-nix-action@v31
|
||||||
#- run: nix flake check
|
with:
|
||||||
- run: nix-build -A checks.x86_64-linux.build -A checks.x86_64-linux.validate-openapi
|
extra_nix_config: |
|
||||||
|
extra-systems = ${{ matrix.system }}
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- run: nix-build -A checks.${{ matrix.system }}.build -A checks.${{ matrix.system }}.validate-openapi
|
||||||
|
|||||||
28
.github/workflows/update-flakes.yml
vendored
Normal file
28
.github/workflows/update-flakes.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: "Update Flakes"
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# Run weekly on Monday at 00:00 UTC
|
||||||
|
- cron: '0 0 * * 1'
|
||||||
|
workflow_dispatch:
|
||||||
|
jobs:
|
||||||
|
update-flakes:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: cachix/install-nix-action@v31
|
||||||
|
- name: Update flake inputs
|
||||||
|
run: nix flake update
|
||||||
|
- name: Create Pull Request
|
||||||
|
uses: peter-evans/create-pull-request@v8
|
||||||
|
with:
|
||||||
|
commit-message: "flake.lock: Update"
|
||||||
|
title: "Update flake inputs"
|
||||||
|
body: |
|
||||||
|
Automated flake input updates.
|
||||||
|
|
||||||
|
This PR was automatically created by the update-flakes workflow.
|
||||||
|
branch: update-flakes
|
||||||
|
delete-branch: true
|
||||||
48
.gitignore
vendored
48
.gitignore
vendored
@@ -1,47 +1,13 @@
|
|||||||
/.pls_cache
|
|
||||||
*.o
|
|
||||||
*~
|
*~
|
||||||
Makefile
|
/.direnv/
|
||||||
Makefile.in
|
.test_info.*
|
||||||
.deps
|
/src/root/static/bootstrap
|
||||||
.hydra-data
|
/src/root/static/fontawesome
|
||||||
/config.guess
|
/src/root/static/js/flot
|
||||||
/config.log
|
|
||||||
/config.status
|
|
||||||
/config.sub
|
|
||||||
/configure
|
|
||||||
/depcomp
|
|
||||||
/libtool
|
|
||||||
/ltmain.sh
|
|
||||||
/autom4te.cache
|
|
||||||
/aclocal.m4
|
|
||||||
/missing
|
|
||||||
/install-sh
|
|
||||||
/src/sql/hydra-postgresql.sql
|
/src/sql/hydra-postgresql.sql
|
||||||
/src/sql/hydra-sqlite.sql
|
/src/sql/hydra-sqlite.sql
|
||||||
/src/sql/tmp.sqlite
|
/src/sql/tmp.sqlite
|
||||||
/src/hydra-eval-jobs/hydra-eval-jobs
|
.hydra-data
|
||||||
/src/root/static/bootstrap
|
|
||||||
/src/root/static/js/flot
|
|
||||||
/tests
|
|
||||||
/doc/manual/images
|
|
||||||
/doc/manual/manual.html
|
|
||||||
/doc/manual/manual.pdf
|
|
||||||
/t/.bzr*
|
|
||||||
/t/.git*
|
|
||||||
/t/.hg*
|
|
||||||
/t/nix
|
|
||||||
/t/data
|
|
||||||
/t/jobs/config.nix
|
|
||||||
t/jobs/declarative/project.json
|
|
||||||
/inst
|
|
||||||
hydra-config.h
|
|
||||||
hydra-config.h.in
|
|
||||||
result
|
result
|
||||||
|
result-*
|
||||||
outputs
|
outputs
|
||||||
config
|
|
||||||
stamp-h1
|
|
||||||
src/hydra-evaluator/hydra-evaluator
|
|
||||||
src/hydra-queue-runner/hydra-queue-runner
|
|
||||||
src/root/static/fontawesome/
|
|
||||||
src/root/static/bootstrap*/
|
|
||||||
|
|||||||
@@ -2,3 +2,9 @@ theme = community
|
|||||||
|
|
||||||
# 5 is the least complainy, 1 is the most complainy
|
# 5 is the least complainy, 1 is the most complainy
|
||||||
severity = 1
|
severity = 1
|
||||||
|
|
||||||
|
# Disallow backticks - use IPC::Run3 instead for better security
|
||||||
|
include = InputOutput::ProhibitBacktickOperators
|
||||||
|
|
||||||
|
# Prohibit shell-invoking system() and exec() - use list form or IPC::Run3 instead
|
||||||
|
include = Hydra::ProhibitShellInvokingSystemCalls
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
SUBDIRS = src t doc
|
|
||||||
BOOTCLEAN_SUBDIRS = $(SUBDIRS)
|
|
||||||
DIST_SUBDIRS = $(SUBDIRS)
|
|
||||||
EXTRA_DIST = hydra-module.nix
|
|
||||||
|
|
||||||
install-data-local: hydra-module.nix
|
|
||||||
$(INSTALL) -d $(DESTDIR)$(datadir)/nix
|
|
||||||
$(INSTALL_DATA) hydra-module.nix $(DESTDIR)$(datadir)/nix/
|
|
||||||
46
README.md
46
README.md
@@ -23,7 +23,7 @@ Running Hydra is currently only supported on NixOS. The [hydra module](https://g
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
### Creating An Admin User
|
### Creating An Admin User
|
||||||
Once the Hydra service has been configured as above and activate you should already be able to access the UI interface at the specified URL. However some actions require an admin user which has to be created first:
|
Once the Hydra service has been configured as above and activated, you should already be able to access the UI interface at the specified URL. However some actions require an admin user which has to be created first:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ su - hydra
|
$ su - hydra
|
||||||
@@ -39,16 +39,16 @@ In order to evaluate and build anything you need to create _projects_ that conta
|
|||||||
#### Creating A Project
|
#### Creating A Project
|
||||||
Log in as administrator, click "_Admin_" and select "_Create project_". Fill the form as follows:
|
Log in as administrator, click "_Admin_" and select "_Create project_". Fill the form as follows:
|
||||||
|
|
||||||
- **Identifier**: `hello`
|
- **Identifier**: `hello-project`
|
||||||
- **Display name**: `hello`
|
- **Display name**: `hello`
|
||||||
- **Description**: `hello project`
|
- **Description**: `hello project`
|
||||||
|
|
||||||
Click "_Create project_".
|
Click "_Create project_".
|
||||||
|
|
||||||
#### Creating A Jobset
|
#### Creating A Jobset
|
||||||
After creating a project you are forwarded to the project page. Click "_Actions_" and choose "_Create jobset_". Fill the form with the following values:
|
After creating a project you are forwarded to the project page. Click "_Actions_" and choose "_Create jobset_". Change **Type** to Legacy for the example below. Fill the form with the following values:
|
||||||
|
|
||||||
- **Identifier**: `hello`
|
- **Identifier**: `hello-project`
|
||||||
- **Nix expression**: `examples/hello.nix` in `hydra`
|
- **Nix expression**: `examples/hello.nix` in `hydra`
|
||||||
- **Check interval**: 60
|
- **Check interval**: 60
|
||||||
- **Scheduling shares**: 1
|
- **Scheduling shares**: 1
|
||||||
@@ -57,7 +57,7 @@ We have to add two inputs for this jobset. One for _nixpkgs_ and one for _hydra_
|
|||||||
|
|
||||||
- **Input name**: `nixpkgs`
|
- **Input name**: `nixpkgs`
|
||||||
- **Type**: `Git checkout`
|
- **Type**: `Git checkout`
|
||||||
- **Value**: `https://github.com/nixos/nixpkgs-channels nixos-20.03`
|
- **Value**: `https://github.com/NixOS/nixpkgs nixos-24.05`
|
||||||
|
|
||||||
- **Input name**: `hydra`
|
- **Input name**: `hydra`
|
||||||
- **Type**: `Git checkout`
|
- **Type**: `Git checkout`
|
||||||
@@ -72,28 +72,32 @@ Make sure **State** at the top of the page is set to "_Enabled_" and click on "_
|
|||||||
You can build Hydra via `nix-build` using the provided [default.nix](./default.nix):
|
You can build Hydra via `nix-build` using the provided [default.nix](./default.nix):
|
||||||
|
|
||||||
```
|
```
|
||||||
$ nix-build
|
$ nix build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Development Environment
|
### Development Environment
|
||||||
|
|
||||||
You can use the provided shell.nix to get a working development environment:
|
You can use the provided shell.nix to get a working development environment:
|
||||||
```
|
```
|
||||||
$ nix-shell
|
$ nix develop
|
||||||
$ ./bootstrap
|
$ ln -svf ../../../build/src/bootstrap src/root/static/bootstrap
|
||||||
$ configurePhase # NOTE: not ./configure
|
$ ln -svf ../../../build/src/fontawesome src/root/static/fontawesome
|
||||||
$ make
|
$ ln -svf ../../../../build/src/flot src/root/static/js/flot
|
||||||
|
$ meson setup build
|
||||||
|
$ ninja -C build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The development environment can also automatically be established using [nix-direnv](https://github.com/nix-community/nix-direnv).
|
||||||
|
|
||||||
### Executing Hydra During Development
|
### Executing Hydra During Development
|
||||||
|
|
||||||
When working on new features or bug fixes you need to be able to run Hydra from your working copy. This
|
When working on new features or bug fixes you need to be able to run Hydra from your working copy. This
|
||||||
can be done using [foreman](https://github.com/ddollar/foreman):
|
can be done using [foreman](https://github.com/ddollar/foreman):
|
||||||
|
|
||||||
```
|
```
|
||||||
$ nix-shell
|
$ nix develop
|
||||||
$ # hack hack
|
$ # hack hack
|
||||||
$ make
|
$ ninja -C build
|
||||||
$ foreman start
|
$ foreman start
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -101,7 +105,7 @@ Have a look at the [Procfile](./Procfile) if you want to see how the processes a
|
|||||||
conflicts with services that might be running on your host, hydra and postgress are started on custom ports:
|
conflicts with services that might be running on your host, hydra and postgress are started on custom ports:
|
||||||
|
|
||||||
- hydra-server: 63333 with the username "alice" and the password "foobar"
|
- hydra-server: 63333 with the username "alice" and the password "foobar"
|
||||||
- postgresql: 64444
|
- postgresql: 64444, can be connected to using `psql -p 64444 -h localhost hydra`
|
||||||
|
|
||||||
Note that this is only ever meant as an ad-hoc way of executing Hydra during development. Please make use of the
|
Note that this is only ever meant as an ad-hoc way of executing Hydra during development. Please make use of the
|
||||||
NixOS module for actually running Hydra in production.
|
NixOS module for actually running Hydra in production.
|
||||||
@@ -115,22 +119,24 @@ Start by following the steps in [Development Environment](#development-environme
|
|||||||
Then, you can run the tests and the perlcritic linter together with:
|
Then, you can run the tests and the perlcritic linter together with:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ nix-shell
|
$ nix develop
|
||||||
$ make check
|
$ ninja -C build test
|
||||||
```
|
```
|
||||||
|
|
||||||
You can run a single test with:
|
You can run a single test with:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ nix-shell
|
$ nix develop
|
||||||
$ yath test ./t/foo/bar.t
|
$ cd build
|
||||||
|
$ meson test --test-args=../t/Hydra/Event.t testsuite
|
||||||
```
|
```
|
||||||
|
|
||||||
And you can run just perlcritic with:
|
And you can run just perlcritic with:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ nix-shell
|
$ nix develop
|
||||||
$ make perlcritic
|
$ cd build
|
||||||
|
$ meson test perlcritic
|
||||||
```
|
```
|
||||||
|
|
||||||
### JSON API
|
### JSON API
|
||||||
@@ -140,7 +146,7 @@ You can also interface with Hydra through a JSON API. The API is defined in [hyd
|
|||||||
## Additional Resources
|
## Additional Resources
|
||||||
|
|
||||||
- [Hydra User's Guide](https://nixos.org/hydra/manual/)
|
- [Hydra User's Guide](https://nixos.org/hydra/manual/)
|
||||||
- [Hydra on the NixOS Wiki](https://nixos.wiki/wiki/Hydra)
|
- [Hydra on the NixOS Wiki](https://wiki.nixos.org/wiki/Hydra)
|
||||||
- [hydra-cli](https://github.com/nlewo/hydra-cli)
|
- [hydra-cli](https://github.com/nlewo/hydra-cli)
|
||||||
- [Peter Simons - Hydra: Setting up your own build farm (NixOS)](https://www.youtube.com/watch?v=RXV0Y5Bn-QQ)
|
- [Peter Simons - Hydra: Setting up your own build farm (NixOS)](https://www.youtube.com/watch?v=RXV0Y5Bn-QQ)
|
||||||
|
|
||||||
|
|||||||
83
configure.ac
83
configure.ac
@@ -1,83 +0,0 @@
|
|||||||
AC_INIT([Hydra], [m4_esyscmd([echo -n $(cat ./version.txt)$VERSION_SUFFIX])])
|
|
||||||
AC_CONFIG_AUX_DIR(config)
|
|
||||||
AM_INIT_AUTOMAKE([foreign serial-tests])
|
|
||||||
|
|
||||||
AC_LANG([C++])
|
|
||||||
|
|
||||||
AC_PROG_CC
|
|
||||||
AC_PROG_INSTALL
|
|
||||||
AC_PROG_LN_S
|
|
||||||
AC_PROG_LIBTOOL
|
|
||||||
AC_PROG_CXX
|
|
||||||
|
|
||||||
AC_PATH_PROG([XSLTPROC], [xsltproc])
|
|
||||||
|
|
||||||
AC_ARG_WITH([docbook-xsl],
|
|
||||||
[AS_HELP_STRING([--with-docbook-xsl=PATH],
|
|
||||||
[path of the DocBook XSL stylesheets])],
|
|
||||||
[docbookxsl="$withval"],
|
|
||||||
[docbookxsl="/docbook-xsl-missing"])
|
|
||||||
AC_SUBST([docbookxsl])
|
|
||||||
|
|
||||||
|
|
||||||
AC_DEFUN([NEED_PROG],
|
|
||||||
[
|
|
||||||
AC_PATH_PROG($1, $2)
|
|
||||||
if test -z "$$1"; then
|
|
||||||
AC_MSG_ERROR([$2 is required])
|
|
||||||
fi
|
|
||||||
])
|
|
||||||
|
|
||||||
NEED_PROG(perl, perl)
|
|
||||||
|
|
||||||
NEED_PROG([NIX_STORE_PROGRAM], [nix-store])
|
|
||||||
|
|
||||||
AC_MSG_CHECKING([whether $NIX_STORE_PROGRAM is recent enough])
|
|
||||||
if test -n "$NIX_STORE" -a -n "$TMPDIR"
|
|
||||||
then
|
|
||||||
# This may be executed from within a build chroot, so pacify
|
|
||||||
# `nix-store' instead of letting it choke while trying to mkdir
|
|
||||||
# /nix/var.
|
|
||||||
NIX_STATE_DIR="$TMPDIR"
|
|
||||||
export NIX_STATE_DIR
|
|
||||||
fi
|
|
||||||
if NIX_REMOTE=daemon PAGER=cat "$NIX_STORE_PROGRAM" --timeout 123 -q; then
|
|
||||||
AC_MSG_RESULT([yes])
|
|
||||||
else
|
|
||||||
AC_MSG_RESULT([no])
|
|
||||||
AC_MSG_ERROR([`$NIX_STORE_PROGRAM' doesn't support `--timeout'; please use a newer version.])
|
|
||||||
fi
|
|
||||||
|
|
||||||
PKG_CHECK_MODULES([NIX], [nix-main nix-expr nix-store])
|
|
||||||
|
|
||||||
testPath="$(dirname $(type -p expr))"
|
|
||||||
AC_SUBST(testPath)
|
|
||||||
|
|
||||||
jobsPath="$(realpath ./t/jobs)"
|
|
||||||
AC_SUBST(jobsPath)
|
|
||||||
|
|
||||||
CXXFLAGS+=" -include nix/config.h"
|
|
||||||
|
|
||||||
AC_CONFIG_FILES([
|
|
||||||
Makefile
|
|
||||||
doc/Makefile
|
|
||||||
doc/manual/Makefile
|
|
||||||
src/Makefile
|
|
||||||
src/hydra-evaluator/Makefile
|
|
||||||
src/hydra-eval-jobs/Makefile
|
|
||||||
src/hydra-queue-runner/Makefile
|
|
||||||
src/sql/Makefile
|
|
||||||
src/ttf/Makefile
|
|
||||||
src/lib/Makefile
|
|
||||||
src/root/Makefile
|
|
||||||
src/script/Makefile
|
|
||||||
t/Makefile
|
|
||||||
t/jobs/config.nix
|
|
||||||
t/jobs/declarative/project.json
|
|
||||||
])
|
|
||||||
|
|
||||||
AC_CONFIG_COMMANDS([executable-scripts], [])
|
|
||||||
|
|
||||||
AC_CONFIG_HEADER([hydra-config.h])
|
|
||||||
|
|
||||||
AC_OUTPUT
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# The `default.nix` in flake-compat reads `flake.nix` and `flake.lock` from `src` and
|
# The `default.nix` in flake-compat reads `flake.nix` and `flake.lock` from `src` and
|
||||||
# returns an attribute set of the shape `{ defaultNix, shellNix }`
|
# returns an attribute set of the shape `{ defaultNix, shellNix }`
|
||||||
|
|
||||||
(import (fetchTarball https://github.com/edolstra/flake-compat/archive/master.tar.gz) {
|
(import (fetchTarball "https://github.com/edolstra/flake-compat/archive/master.tar.gz") {
|
||||||
src = ./.;
|
src = ./.;
|
||||||
}).defaultNix
|
}).defaultNix
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
SUBDIRS = manual
|
|
||||||
BOOTCLEAN_SUBDIRS = $(SUBDIRS)
|
|
||||||
DIST_SUBDIRS = $(SUBDIRS)
|
|
||||||
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
MD_FILES = src/*.md
|
|
||||||
|
|
||||||
EXTRA_DIST = $(MD_FILES)
|
|
||||||
|
|
||||||
install: $(MD_FILES)
|
|
||||||
mdbook build . -d $(docdir)
|
|
||||||
36
doc/manual/meson.build
Normal file
36
doc/manual/meson.build
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
srcs = files(
|
||||||
|
'src/SUMMARY.md',
|
||||||
|
'src/about.md',
|
||||||
|
'src/api.md',
|
||||||
|
'src/configuration.md',
|
||||||
|
'src/hacking.md',
|
||||||
|
'src/installation.md',
|
||||||
|
'src/introduction.md',
|
||||||
|
'src/jobs.md',
|
||||||
|
'src/monitoring/README.md',
|
||||||
|
'src/notifications.md',
|
||||||
|
'src/plugins/README.md',
|
||||||
|
'src/plugins/RunCommand.md',
|
||||||
|
'src/plugins/declarative-projects.md',
|
||||||
|
'src/projects.md',
|
||||||
|
'src/webhooks.md',
|
||||||
|
)
|
||||||
|
|
||||||
|
manual = custom_target(
|
||||||
|
'manual',
|
||||||
|
command: [
|
||||||
|
mdbook,
|
||||||
|
'build',
|
||||||
|
'@SOURCE_ROOT@/doc/manual',
|
||||||
|
'-d', meson.current_build_dir() / 'html'
|
||||||
|
],
|
||||||
|
depend_files: srcs,
|
||||||
|
output: ['html'],
|
||||||
|
build_by_default: true,
|
||||||
|
)
|
||||||
|
|
||||||
|
install_subdir(
|
||||||
|
manual.full_path(),
|
||||||
|
install_dir: get_option('datadir') / 'doc/hydra',
|
||||||
|
strip_directory: true,
|
||||||
|
)
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
- [RunCommand](./plugins/RunCommand.md)
|
- [RunCommand](./plugins/RunCommand.md)
|
||||||
- [Using the external API](api.md)
|
- [Using the external API](api.md)
|
||||||
- [Webhooks](webhooks.md)
|
- [Webhooks](webhooks.md)
|
||||||
|
- [Webhook Authentication Migration Guide](webhook-migration-guide.md)
|
||||||
- [Monitoring Hydra](./monitoring/README.md)
|
- [Monitoring Hydra](./monitoring/README.md)
|
||||||
|
|
||||||
## Developer's Guide
|
## Developer's Guide
|
||||||
|
|||||||
@@ -51,10 +51,12 @@ base_uri example.com
|
|||||||
`base_uri` should be your hydra servers proxied URL. If you are using
|
`base_uri` should be your hydra servers proxied URL. If you are using
|
||||||
Hydra nixos module then setting `hydraURL` option should be enough.
|
Hydra nixos module then setting `hydraURL` option should be enough.
|
||||||
|
|
||||||
If you want to serve Hydra with a prefix path, for example
|
You also need to configure your reverse proxy to pass `X-Request-Base`
|
||||||
[http://example.com/hydra]() then you need to configure your reverse
|
to hydra, with the same value as `base_uri`.
|
||||||
proxy to pass `X-Request-Base` to hydra, with prefix path as value. For
|
This also covers the case of serving Hydra with a prefix path,
|
||||||
example if you are using nginx, then use configuration similar to
|
as in [http://example.com/hydra]().
|
||||||
|
|
||||||
|
For example if you are using nginx, then use configuration similar to
|
||||||
following:
|
following:
|
||||||
|
|
||||||
server {
|
server {
|
||||||
@@ -63,8 +65,7 @@ following:
|
|||||||
.. other configuration ..
|
.. other configuration ..
|
||||||
location /hydra/ {
|
location /hydra/ {
|
||||||
|
|
||||||
proxy_pass http://127.0.0.1:3000;
|
proxy_pass http://127.0.0.1:3000/;
|
||||||
proxy_redirect http://127.0.0.1:3000 https://example.com/hydra;
|
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
@@ -74,6 +75,33 @@ following:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Note the trailing slash on the `proxy_pass` directive, which causes nginx to
|
||||||
|
strip off the `/hydra/` part of the URL before passing it to hydra.
|
||||||
|
|
||||||
|
Populating a Cache
|
||||||
|
------------------
|
||||||
|
|
||||||
|
A common use for Hydra is to pre-build and cache derivations which
|
||||||
|
take a long time to build. While it is possible to direcly access the
|
||||||
|
Hydra server's store over SSH, a more scalable option is to upload
|
||||||
|
built derivations to a remote store like an [S3-compatible object
|
||||||
|
store](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-help-stores.html#s3-binary-cache-store). Setting
|
||||||
|
the `store_uri` parameter will cause Hydra to sign and upload
|
||||||
|
derivations as they are built:
|
||||||
|
|
||||||
|
```
|
||||||
|
store_uri = s3://cache-bucket-name?compression=zstd¶llel-compression=true&write-nar-listing=1&ls-compression=br&log-compression=br&secret-key=/path/to/cache/private/key
|
||||||
|
```
|
||||||
|
|
||||||
|
This example uses [Zstandard](https://github.com/facebook/zstd)
|
||||||
|
compression on derivations to reduce CPU usage on the server, but
|
||||||
|
[Brotli](https://brotli.org/) compression for derivation listings and
|
||||||
|
build logs because it has better browser support.
|
||||||
|
|
||||||
|
See [`nix help
|
||||||
|
stores`](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-help-stores.html)
|
||||||
|
for a description of the store URI format.
|
||||||
|
|
||||||
Statsd Configuration
|
Statsd Configuration
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
@@ -184,7 +212,8 @@ Example configuration:
|
|||||||
<role_mapping>
|
<role_mapping>
|
||||||
# Make all users in the hydra_admin group Hydra admins
|
# Make all users in the hydra_admin group Hydra admins
|
||||||
hydra_admin = admin
|
hydra_admin = admin
|
||||||
# Allow all users in the dev group to restart jobs and cancel builds
|
# Allow all users in the dev group to eval jobsets, restart jobs and cancel builds
|
||||||
|
dev = eval-jobset
|
||||||
dev = restart-jobs
|
dev = restart-jobs
|
||||||
dev = cancel-build
|
dev = cancel-build
|
||||||
</role_mapping>
|
</role_mapping>
|
||||||
@@ -237,6 +266,40 @@ default role mapping:
|
|||||||
Note that configuring both the LDAP parameters in the hydra.conf and via
|
Note that configuring both the LDAP parameters in the hydra.conf and via
|
||||||
the environment variable is a fatal error.
|
the environment variable is a fatal error.
|
||||||
|
|
||||||
|
Webhook Authentication
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Hydra supports authenticating webhook requests from GitHub and Gitea to prevent unauthorized job evaluations.
|
||||||
|
Webhook secrets should be stored in separate files outside the Nix store for security using Config::General's include mechanism.
|
||||||
|
|
||||||
|
In your main `hydra.conf`:
|
||||||
|
```apache
|
||||||
|
<webhooks>
|
||||||
|
Include /var/lib/hydra/secrets/webhook-secrets.conf
|
||||||
|
</webhooks>
|
||||||
|
```
|
||||||
|
|
||||||
|
Then create `/var/lib/hydra/secrets/webhook-secrets.conf` with your actual secrets:
|
||||||
|
```apache
|
||||||
|
<github>
|
||||||
|
secret = your-github-webhook-secret
|
||||||
|
</github>
|
||||||
|
<gitea>
|
||||||
|
secret = your-gitea-webhook-secret
|
||||||
|
</gitea>
|
||||||
|
```
|
||||||
|
|
||||||
|
For multiple secrets (useful for rotation or multiple environments), use an array:
|
||||||
|
```apache
|
||||||
|
<github>
|
||||||
|
secret = your-github-webhook-secret-prod
|
||||||
|
secret = your-github-webhook-secret-staging
|
||||||
|
</github>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important**: The secrets file should have restricted permissions (e.g., 0600) to prevent unauthorized access.
|
||||||
|
See the [Webhooks documentation](webhooks.md) for detailed setup instructions.
|
||||||
|
|
||||||
Embedding Extra HTML
|
Embedding Extra HTML
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
|||||||
@@ -12,24 +12,26 @@ To enter a shell in which all environment variables (such as `PERL5LIB`)
|
|||||||
and dependencies can be found:
|
and dependencies can be found:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ nix-shell
|
$ nix develop
|
||||||
```
|
```
|
||||||
|
|
||||||
To build Hydra, you should then do:
|
To build Hydra, you should then do:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
[nix-shell]$ ./bootstrap
|
$ mesonConfigurePhase
|
||||||
[nix-shell]$ configurePhase
|
$ ninja
|
||||||
[nix-shell]$ make
|
|
||||||
```
|
```
|
||||||
|
|
||||||
You start a local database, the webserver, and other components with
|
You start a local database, the webserver, and other components with
|
||||||
foreman:
|
foreman:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
|
$ ninja -C build
|
||||||
$ foreman start
|
$ foreman start
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The Hydra interface will be available on port 63333, with an admin user named "alice" with password "foobar"
|
||||||
|
|
||||||
You can run just the Hydra web server in your source tree as follows:
|
You can run just the Hydra web server in your source tree as follows:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
@@ -39,17 +41,20 @@ $ ./src/script/hydra-server
|
|||||||
You can run Hydra's test suite with the following:
|
You can run Hydra's test suite with the following:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
[nix-shell]$ make check
|
$ meson test
|
||||||
[nix-shell]$ # to run as many tests as you have cores:
|
# to run as many tests as you have cores:
|
||||||
[nix-shell]$ make check YATH_JOB_COUNT=$NIX_BUILD_CORES
|
$ YATH_JOB_COUNT=$NIX_BUILD_CORES meson test
|
||||||
[nix-shell]$ # or run yath directly:
|
|
||||||
[nix-shell]$ yath test
|
|
||||||
[nix-shell]$ # to run as many tests as you have cores:
|
|
||||||
[nix-shell]$ yath test -j $NIX_BUILD_CORES
|
|
||||||
```
|
```
|
||||||
|
|
||||||
When using `yath` instead of `make check`, ensure you have run `make`
|
To run individual tests:
|
||||||
in the root of the repository at least once.
|
|
||||||
|
```console
|
||||||
|
# Run a specific test file
|
||||||
|
$ PERL5LIB=t/lib:$PERL5LIB perl t/test.pl t/Hydra/Controller/API/checks.t
|
||||||
|
|
||||||
|
# Run all tests in a directory
|
||||||
|
$ PERL5LIB=t/lib:$PERL5LIB perl t/test.pl t/Hydra/Controller/API/
|
||||||
|
```
|
||||||
|
|
||||||
**Warning**: Currently, the tests can fail
|
**Warning**: Currently, the tests can fail
|
||||||
if run with high parallelism [due to an issue in
|
if run with high parallelism [due to an issue in
|
||||||
@@ -67,7 +72,7 @@ will reload the page every time you save.
|
|||||||
To build Hydra and its dependencies:
|
To build Hydra and its dependencies:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ nix-build release.nix -A build.x86_64-linux
|
$ nix build .#packages.x86_64-linux.default
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development Tasks
|
## Development Tasks
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ Getting Nix
|
|||||||
If your server runs NixOS you are all set to continue with installation
|
If your server runs NixOS you are all set to continue with installation
|
||||||
of Hydra. Otherwise you first need to install Nix. The latest stable
|
of Hydra. Otherwise you first need to install Nix. The latest stable
|
||||||
version can be found one [the Nix web
|
version can be found one [the Nix web
|
||||||
site](http://nixos.org/nix/download.html), along with a manual, which
|
site](https://nixos.org/download/), along with a manual, which
|
||||||
includes installation instructions.
|
includes installation instructions.
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ Sets CircleCI status.
|
|||||||
|
|
||||||
## Compress build logs
|
## Compress build logs
|
||||||
|
|
||||||
Compresses build logs after a build with bzip2.
|
Compresses build logs after a build with bzip2 or zstd.
|
||||||
|
|
||||||
### Configuration options
|
### Configuration options
|
||||||
|
|
||||||
@@ -50,6 +50,14 @@ Compresses build logs after a build with bzip2.
|
|||||||
|
|
||||||
Enable log compression
|
Enable log compression
|
||||||
|
|
||||||
|
- `compress_build_logs_compression`
|
||||||
|
|
||||||
|
Which compression format to use. Valid values are bzip2 (default) and zstd.
|
||||||
|
|
||||||
|
- `compress_build_logs_silent`
|
||||||
|
|
||||||
|
Whether to compress logs silently.
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
|
|||||||
@@ -404,3 +404,10 @@ analogous:
|
|||||||
| `String value` | `gitea_status_repo` | *Name of the `Git checkout` input* |
|
| `String value` | `gitea_status_repo` | *Name of the `Git checkout` input* |
|
||||||
| `String value` | `gitea_http_url` | *Public URL of `gitea`*, optional |
|
| `String value` | `gitea_http_url` | *Public URL of `gitea`*, optional |
|
||||||
|
|
||||||
|
Content-addressed derivations
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
Hydra can to a certain extent use the [`ca-derivations` experimental Nix feature](https://github.com/NixOS/rfcs/pull/62).
|
||||||
|
To use it, make sure that the Nix version you use is at least as recent as the one used in hydra's flake.
|
||||||
|
|
||||||
|
Be warned that this support is still highly experimental, and anything beyond the basic functionality might be broken at that point.
|
||||||
|
|||||||
168
doc/manual/src/webhook-migration-guide.md
Normal file
168
doc/manual/src/webhook-migration-guide.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# Webhook Authentication Migration Guide
|
||||||
|
|
||||||
|
This guide helps Hydra administrators migrate from unauthenticated webhooks to authenticated webhooks to secure their Hydra instances against unauthorized job evaluations.
|
||||||
|
|
||||||
|
## Why Migrate?
|
||||||
|
|
||||||
|
Currently, Hydra's webhook endpoints (`/api/push-github` and `/api/push-gitea`) accept any POST request without authentication. This vulnerability allows:
|
||||||
|
- Anyone to trigger expensive job evaluations
|
||||||
|
- Potential denial of service through repeated requests
|
||||||
|
- Manipulation of build timing and scheduling
|
||||||
|
|
||||||
|
## Step-by-Step Migration for NixOS
|
||||||
|
|
||||||
|
### 1. Create Webhook Configuration
|
||||||
|
|
||||||
|
Create a webhook secrets configuration file with the generated secrets:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create the secrets configuration file with inline secret generation
|
||||||
|
cat > /var/lib/hydra/secrets/webhook-secrets.conf <<EOF
|
||||||
|
<github>
|
||||||
|
secret = $(openssl rand -hex 32)
|
||||||
|
</github>
|
||||||
|
<gitea>
|
||||||
|
secret = $(openssl rand -hex 32)
|
||||||
|
</gitea>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Set secure permissions
|
||||||
|
chmod 0440 /var/lib/hydra/secrets/webhook-secrets.conf
|
||||||
|
chown hydra:hydra /var/lib/hydra/secrets/webhook-secrets.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important**: Save the generated secrets to configure them in GitHub/Gitea later. You can view them with:
|
||||||
|
```bash
|
||||||
|
cat /var/lib/hydra/secrets/webhook-secrets.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
Then update your NixOS configuration to include the webhook configuration:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
services.hydra-dev = {
|
||||||
|
enable = true;
|
||||||
|
hydraURL = "https://hydra.example.com";
|
||||||
|
notificationSender = "hydra@example.com";
|
||||||
|
|
||||||
|
extraConfig = ''
|
||||||
|
<webhooks>
|
||||||
|
Include /var/lib/hydra/secrets/webhook-secrets.conf
|
||||||
|
</webhooks>
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For multiple secrets (useful for rotation or multiple environments), update your webhook-secrets.conf:
|
||||||
|
|
||||||
|
```apache
|
||||||
|
<github>
|
||||||
|
secret = your-github-webhook-secret-prod
|
||||||
|
secret = your-github-webhook-secret-staging
|
||||||
|
</github>
|
||||||
|
<gitea>
|
||||||
|
secret = your-gitea-webhook-secret
|
||||||
|
</gitea>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Deploy Configuration
|
||||||
|
|
||||||
|
Apply the NixOS configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nixos-rebuild switch
|
||||||
|
```
|
||||||
|
|
||||||
|
This will automatically restart Hydra services with the new configuration.
|
||||||
|
|
||||||
|
### 3. Verify Configuration
|
||||||
|
|
||||||
|
Check Hydra's logs to ensure secrets were loaded successfully:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
journalctl -u hydra-server | grep -i webhook
|
||||||
|
```
|
||||||
|
|
||||||
|
You should not see warnings about webhook authentication not being configured.
|
||||||
|
|
||||||
|
### 4. Update Your Webhooks
|
||||||
|
|
||||||
|
#### GitHub
|
||||||
|
1. Navigate to your repository settings: `https://github.com/<owner>/<repo>/settings/hooks`
|
||||||
|
2. Edit your existing Hydra webhook
|
||||||
|
3. In the "Secret" field, paste the content of `/var/lib/hydra/secrets/github-webhook-secret`
|
||||||
|
4. Click "Update webhook"
|
||||||
|
5. GitHub will send a ping event to verify the configuration
|
||||||
|
|
||||||
|
#### Gitea
|
||||||
|
1. Navigate to your repository webhook settings
|
||||||
|
2. Edit your existing Hydra webhook
|
||||||
|
3. In the "Secret" field, paste the content of `/var/lib/hydra/secrets/gitea-webhook-secret`
|
||||||
|
4. Click "Update Webhook"
|
||||||
|
5. Use the "Test Delivery" button to verify the configuration
|
||||||
|
|
||||||
|
### 5. Test the Configuration
|
||||||
|
|
||||||
|
After updating each webhook:
|
||||||
|
1. Make a test commit to trigger the webhook
|
||||||
|
2. Check Hydra's logs for successful authentication
|
||||||
|
3. Verify the evaluation was triggered in Hydra's web interface
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### 401 Unauthorized Errors
|
||||||
|
|
||||||
|
If webhooks start failing with 401 errors:
|
||||||
|
- Verify the secret in the Git forge matches the file content exactly
|
||||||
|
- Check file permissions: `ls -la /var/lib/hydra/secrets/`
|
||||||
|
- Ensure no extra whitespace in secret files
|
||||||
|
- Check Hydra logs for specific error messages
|
||||||
|
|
||||||
|
### Webhook Still Unauthenticated
|
||||||
|
|
||||||
|
If you see warnings about unauthenticated webhooks after configuration:
|
||||||
|
- Verify the configuration syntax in your NixOS module
|
||||||
|
- Ensure the NixOS configuration was successfully applied
|
||||||
|
- Check that the webhook-secrets.conf file exists and is readable by the Hydra user
|
||||||
|
- Verify the Include path is correct in your hydra.conf
|
||||||
|
- Check the syntax of your webhook-secrets.conf file
|
||||||
|
|
||||||
|
### Testing Without Git Forge
|
||||||
|
|
||||||
|
You can test webhook authentication using curl:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Read the secret
|
||||||
|
SECRET=$(cat /var/lib/hydra/secrets/github-webhook-secret)
|
||||||
|
|
||||||
|
# Create test payload
|
||||||
|
PAYLOAD='{"ref":"refs/heads/main","repository":{"clone_url":"https://github.com/test/repo.git"}}'
|
||||||
|
|
||||||
|
# Calculate signature
|
||||||
|
SIGNATURE="sha256=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)"
|
||||||
|
|
||||||
|
# Send authenticated request
|
||||||
|
curl -X POST https://your-hydra/api/push-github \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-Hub-Signature-256: $SIGNATURE" \
|
||||||
|
-d "$PAYLOAD"
|
||||||
|
```
|
||||||
|
|
||||||
|
For Gitea (no prefix in signature):
|
||||||
|
```bash
|
||||||
|
# Read the secret
|
||||||
|
SECRET=$(cat /var/lib/hydra/secrets/gitea-webhook-secret)
|
||||||
|
|
||||||
|
# Create test payload
|
||||||
|
PAYLOAD='{"ref":"refs/heads/main","repository":{"clone_url":"https://gitea.example.com/test/repo.git"}}'
|
||||||
|
|
||||||
|
# Calculate signature
|
||||||
|
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)
|
||||||
|
|
||||||
|
# Send authenticated request
|
||||||
|
curl -X POST https://your-hydra/api/push-gitea \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-Gitea-Signature: $SIGNATURE" \
|
||||||
|
-d "$PAYLOAD"
|
||||||
|
```
|
||||||
@@ -1,13 +1,101 @@
|
|||||||
# Webhooks
|
# Webhooks
|
||||||
|
|
||||||
Hydra can be notified by github's webhook to trigger a new evaluation when a
|
Hydra can be notified by github or gitea with webhooks to trigger a new evaluation when a
|
||||||
jobset has a github repo in its input.
|
jobset has a github repo in its input.
|
||||||
To set up a github webhook go to `https://github.com/<yourhandle>/<yourrepo>/settings` and in the `Webhooks` tab
|
|
||||||
click on `Add webhook`.
|
## Webhook Authentication
|
||||||
|
|
||||||
|
Hydra supports webhook signature verification for both GitHub and Gitea using HMAC-SHA256. This ensures that webhook
|
||||||
|
requests are coming from your configured Git forge and haven't been tampered with.
|
||||||
|
|
||||||
|
### Configuring Webhook Authentication
|
||||||
|
|
||||||
|
1. **Create webhook configuration**: Generate and store webhook secrets securely:
|
||||||
|
```bash
|
||||||
|
# Create directory and generate secrets in one step
|
||||||
|
mkdir -p /var/lib/hydra/secrets
|
||||||
|
cat > /var/lib/hydra/secrets/webhook-secrets.conf <<EOF
|
||||||
|
<github>
|
||||||
|
secret = $(openssl rand -hex 32)
|
||||||
|
</github>
|
||||||
|
<gitea>
|
||||||
|
secret = $(openssl rand -hex 32)
|
||||||
|
</gitea>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Set secure permissions
|
||||||
|
chmod 0600 /var/lib/hydra/secrets/webhook-secrets.conf
|
||||||
|
chown hydra:hydra /var/lib/hydra/secrets/webhook-secrets.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Configure Hydra**: Add the following to your `hydra.conf`:
|
||||||
|
```apache
|
||||||
|
<webhooks>
|
||||||
|
Include /var/lib/hydra/secrets/webhook-secrets.conf
|
||||||
|
</webhooks>
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Configure your Git forge**: View the generated secrets and configure them in GitHub/Gitea:
|
||||||
|
```bash
|
||||||
|
grep "secret =" /var/lib/hydra/secrets/webhook-secrets.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Secrets Support
|
||||||
|
|
||||||
|
Hydra supports configuring multiple secrets for each platform, which is useful for:
|
||||||
|
- Zero-downtime secret rotation
|
||||||
|
- Supporting multiple environments (production/staging)
|
||||||
|
- Gradual migration of webhooks
|
||||||
|
|
||||||
|
To configure multiple secrets, use array syntax:
|
||||||
|
```apache
|
||||||
|
<github>
|
||||||
|
secret = current-webhook-secret
|
||||||
|
secret = previous-webhook-secret
|
||||||
|
</github>
|
||||||
|
```
|
||||||
|
|
||||||
|
## GitHub
|
||||||
|
|
||||||
|
To set up a webhook for a GitHub repository go to `https://github.com/<yourhandle>/<yourrepo>/settings`
|
||||||
|
and in the `Webhooks` tab click on `Add webhook`.
|
||||||
|
|
||||||
- In `Payload URL` fill in `https://<your-hydra-domain>/api/push-github`.
|
- In `Payload URL` fill in `https://<your-hydra-domain>/api/push-github`.
|
||||||
- In `Content type` switch to `application/json`.
|
- In `Content type` switch to `application/json`.
|
||||||
- The `Secret` field can stay empty.
|
- In the `Secret` field, enter the content of your GitHub webhook secret file (if authentication is configured).
|
||||||
- For `Which events would you like to trigger this webhook?` keep the default option for events on `Just the push event.`.
|
- For `Which events would you like to trigger this webhook?` keep the default option for events on `Just the push event.`.
|
||||||
|
|
||||||
Then add the hook with `Add webhook`.
|
Then add the hook with `Add webhook`.
|
||||||
|
|
||||||
|
### Verifying GitHub Webhook Security
|
||||||
|
|
||||||
|
After configuration, GitHub will send webhook requests with an `X-Hub-Signature-256` header containing the HMAC-SHA256
|
||||||
|
signature of the request body. Hydra will verify this signature matches the configured secret.
|
||||||
|
|
||||||
|
## Gitea
|
||||||
|
|
||||||
|
To set up a webhook for a Gitea repository go to the settings of the repository in your Gitea instance
|
||||||
|
and in the `Webhooks` tab click on `Add Webhook` and choose `Gitea` in the drop down.
|
||||||
|
|
||||||
|
- In `Target URL` fill in `https://<your-hydra-domain>/api/push-gitea`.
|
||||||
|
- Keep HTTP method `POST`, POST Content Type `application/json` and Trigger On `Push Events`.
|
||||||
|
- In the `Secret` field, enter the content of your Gitea webhook secret file (if authentication is configured).
|
||||||
|
- Change the branch filter to match the git branch hydra builds.
|
||||||
|
|
||||||
|
Then add the hook with `Add webhook`.
|
||||||
|
|
||||||
|
### Verifying Gitea Webhook Security
|
||||||
|
|
||||||
|
After configuration, Gitea will send webhook requests with an `X-Gitea-Signature` header containing the HMAC-SHA256
|
||||||
|
signature of the request body. Hydra will verify this signature matches the configured secret.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
If you receive 401 Unauthorized errors:
|
||||||
|
- Verify the webhook secret in your Git forge matches the content of the secret file exactly
|
||||||
|
- Check that the secret file has proper permissions (should be 0600)
|
||||||
|
- Look at Hydra's logs for specific error messages
|
||||||
|
- Ensure the correct signature header is being sent by your Git forge
|
||||||
|
|
||||||
|
If you see warnings about webhook authentication not being configured:
|
||||||
|
- Configure webhook authentication as described above to secure your endpoints
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# jobset example file. This file canbe referenced as Nix expression
|
# jobset example file. This file can be referenced as Nix expression
|
||||||
# in a jobset configuration along with inputs for nixpkgs and the
|
# in a jobset configuration along with inputs for nixpkgs and the
|
||||||
# repository containing this file.
|
# repository containing this file.
|
||||||
#
|
#
|
||||||
|
|||||||
91
flake.lock
generated
91
flake.lock
generated
@@ -1,96 +1,59 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"flake-compat": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1673956053,
|
|
||||||
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
|
|
||||||
"owner": "edolstra",
|
|
||||||
"repo": "flake-compat",
|
|
||||||
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "edolstra",
|
|
||||||
"repo": "flake-compat",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lowdown-src": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1633514407,
|
|
||||||
"narHash": "sha256-Dw32tiMjdK9t3ETl5fzGrutQTzh2rufgZV4A/BbxuD4=",
|
|
||||||
"owner": "kristapsdz",
|
|
||||||
"repo": "lowdown",
|
|
||||||
"rev": "d2c2b44ff6c27b936ec27358a2653caaef8f73b8",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "kristapsdz",
|
|
||||||
"repo": "lowdown",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nix": {
|
"nix": {
|
||||||
"inputs": {
|
"flake": false,
|
||||||
"flake-compat": "flake-compat",
|
|
||||||
"lowdown-src": "lowdown-src",
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
],
|
|
||||||
"nixpkgs-regression": "nixpkgs-regression"
|
|
||||||
},
|
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1690219894,
|
"lastModified": 1772065213,
|
||||||
"narHash": "sha256-QMYAkdtU+g9HlZKtoJ+AI6TbWzzovKGnPZJHfZdclc8=",
|
"narHash": "sha256-DbYpmZAD6aebwxepBop5Ub4S39sLg9UIJziTbeD832k=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nix",
|
"repo": "nix",
|
||||||
"rev": "a212300a1d9f9c7b0daf19c00c87fc50480f54f4",
|
"rev": "0769726d44b0782fecbd7b9749e24320c77af317",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"ref": "2.17.0",
|
"ref": "2.33-maintenance",
|
||||||
"repo": "nix",
|
"repo": "nix",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nix-eval-jobs": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1767025318,
|
||||||
|
"narHash": "sha256-i68miKHGdueWggcDAF+Kca9g6S3ipkW629XbMpQYfn0=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nix-eval-jobs",
|
||||||
|
"rev": "79dd7adbb5f75b08fb4b9bddd712ebc52baa46bc",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"ref": "v2.33.0",
|
||||||
|
"repo": "nix-eval-jobs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1687379288,
|
"lastModified": 1772934839,
|
||||||
"narHash": "sha256-cSuwfiqYfeVyqzCRkU9AvLTysmEuSal8nh6CYr+xWog=",
|
"narHash": "sha256-6mMYkB7BTTsc4thtCFbh3Aj5yth3EPI6L9L5DR6tpWc=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "ef0bc3976340dab9a4e087a0bcff661a8b2e87f3",
|
"rev": "d351a3bce30b8f0d0a36281754b62942977fabe5",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"ref": "nixos-23.05",
|
"ref": "nixos-25.11-small",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs-regression": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1643052045,
|
|
||||||
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nix": "nix",
|
"nix": "nix",
|
||||||
|
"nix-eval-jobs": "nix-eval-jobs",
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
652
flake.nix
652
flake.nix
@@ -1,555 +1,78 @@
|
|||||||
{
|
{
|
||||||
description = "A Nix-based continuous build system";
|
description = "A Nix-based continuous build system";
|
||||||
|
|
||||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.05";
|
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11-small";
|
||||||
inputs.nix.url = "github:NixOS/nix/2.17.0";
|
|
||||||
inputs.nix.inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
|
|
||||||
outputs = { self, nixpkgs, nix }:
|
inputs.nix = {
|
||||||
|
url = "github:NixOS/nix/2.33-maintenance";
|
||||||
|
# We want to control the deps precisely
|
||||||
|
flake = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
inputs.nix-eval-jobs = {
|
||||||
|
url = "github:nix-community/nix-eval-jobs/v2.33.0";
|
||||||
|
# We want to control the deps precisely
|
||||||
|
flake = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, nix, nix-eval-jobs, ... }:
|
||||||
let
|
let
|
||||||
version = "${builtins.readFile ./version.txt}.${builtins.substring 0 8 (self.lastModifiedDate or "19700101")}.${self.shortRev or "DIRTY"}";
|
|
||||||
|
|
||||||
systems = [ "x86_64-linux" "aarch64-linux" ];
|
systems = [ "x86_64-linux" "aarch64-linux" ];
|
||||||
forEachSystem = nixpkgs.lib.genAttrs systems;
|
forEachSystem = nixpkgs.lib.genAttrs systems;
|
||||||
|
|
||||||
pkgsBySystem = forEachSystem (system: import nixpkgs {
|
|
||||||
inherit system;
|
|
||||||
overlays = [ self.overlays.default nix.overlays.default ];
|
|
||||||
});
|
|
||||||
|
|
||||||
# NixOS configuration used for VM tests.
|
|
||||||
hydraServer =
|
|
||||||
{ config, pkgs, ... }:
|
|
||||||
{
|
|
||||||
imports = [ self.nixosModules.hydraTest ];
|
|
||||||
|
|
||||||
virtualisation.memorySize = 1024;
|
|
||||||
virtualisation.writableStore = true;
|
|
||||||
|
|
||||||
environment.systemPackages = [ pkgs.perlPackages.LWP pkgs.perlPackages.JSON ];
|
|
||||||
|
|
||||||
nix = {
|
|
||||||
# Without this nix tries to fetch packages from the default
|
|
||||||
# cache.nixos.org which is not reachable from this sandboxed NixOS test.
|
|
||||||
binaryCaches = [ ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
in
|
in
|
||||||
rec {
|
rec {
|
||||||
|
|
||||||
# A Nixpkgs overlay that provides a 'hydra' package.
|
# A Nixpkgs overlay that provides a 'hydra' package.
|
||||||
overlays.default = final: prev: {
|
overlays.default = final: prev: {
|
||||||
|
nixDependenciesForHydra = final.lib.makeScope final.newScope
|
||||||
# Add LDAP dependencies that aren't currently found within nixpkgs.
|
(import (nix + "/packaging/dependencies.nix") {
|
||||||
perlPackages = prev.perlPackages // {
|
pkgs = final;
|
||||||
|
inherit (final) stdenv;
|
||||||
PrometheusTiny = final.perlPackages.buildPerlPackage {
|
inputs = {};
|
||||||
pname = "Prometheus-Tiny";
|
});
|
||||||
version = "0.007";
|
nixComponentsForHydra = final.lib.makeScope final.nixDependenciesForHydra.newScope
|
||||||
src = final.fetchurl {
|
(import (nix + "/packaging/components.nix") {
|
||||||
url = "mirror://cpan/authors/id/R/RO/ROBN/Prometheus-Tiny-0.007.tar.gz";
|
officialRelease = true;
|
||||||
sha256 = "0ef8b226a2025cdde4df80129dd319aa29e884e653c17dc96f4823d985c028ec";
|
inherit (final) lib;
|
||||||
};
|
pkgs = final;
|
||||||
buildInputs = with final.perlPackages; [ HTTPMessage Plack TestException ];
|
src = nix;
|
||||||
meta = {
|
maintainers = [ ];
|
||||||
homepage = "https://github.com/robn/Prometheus-Tiny";
|
});
|
||||||
description = "A tiny Prometheus client";
|
nix-eval-jobs = final.callPackage nix-eval-jobs {
|
||||||
license = with final.lib.licenses; [ artistic1 gpl1Plus ];
|
nixComponents = final.nixComponentsForHydra;
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
hydra = final.callPackage ./package.nix {
|
||||||
hydra = with final; let
|
inherit (final.lib) fileset;
|
||||||
perlDeps = buildEnv {
|
rawSrc = self;
|
||||||
name = "hydra-perl-deps";
|
nixComponents = final.nixComponentsForHydra;
|
||||||
paths = with perlPackages; lib.closePropagation
|
|
||||||
[
|
|
||||||
AuthenSASL
|
|
||||||
CatalystActionREST
|
|
||||||
CatalystAuthenticationStoreDBIxClass
|
|
||||||
CatalystAuthenticationStoreLDAP
|
|
||||||
CatalystDevel
|
|
||||||
CatalystPluginAccessLog
|
|
||||||
CatalystPluginAuthorizationRoles
|
|
||||||
CatalystPluginCaptcha
|
|
||||||
CatalystPluginPrometheusTiny
|
|
||||||
CatalystPluginSessionStateCookie
|
|
||||||
CatalystPluginSessionStoreFastMmap
|
|
||||||
CatalystPluginStackTrace
|
|
||||||
CatalystTraitForRequestProxyBase
|
|
||||||
CatalystViewDownload
|
|
||||||
CatalystViewJSON
|
|
||||||
CatalystViewTT
|
|
||||||
CatalystXRoleApplicator
|
|
||||||
CatalystXScriptServerStarman
|
|
||||||
CryptPassphrase
|
|
||||||
CryptPassphraseArgon2
|
|
||||||
CryptRandPasswd
|
|
||||||
DataDump
|
|
||||||
DateTime
|
|
||||||
DBDPg
|
|
||||||
DBDSQLite
|
|
||||||
DigestSHA1
|
|
||||||
EmailMIME
|
|
||||||
EmailSender
|
|
||||||
FileLibMagic
|
|
||||||
FileSlurper
|
|
||||||
FileWhich
|
|
||||||
final.nix.perl-bindings
|
|
||||||
git
|
|
||||||
IOCompress
|
|
||||||
IPCRun
|
|
||||||
IPCRun3
|
|
||||||
JSON
|
|
||||||
JSONMaybeXS
|
|
||||||
JSONXS
|
|
||||||
ListSomeUtils
|
|
||||||
LWP
|
|
||||||
LWPProtocolHttps
|
|
||||||
ModulePluggable
|
|
||||||
NetAmazonS3
|
|
||||||
NetPrometheus
|
|
||||||
NetStatsd
|
|
||||||
PadWalker
|
|
||||||
ParallelForkManager
|
|
||||||
PerlCriticCommunity
|
|
||||||
PrometheusTinyShared
|
|
||||||
ReadonlyX
|
|
||||||
SetScalar
|
|
||||||
SQLSplitStatement
|
|
||||||
Starman
|
|
||||||
StringCompareConstantTime
|
|
||||||
SysHostnameLong
|
|
||||||
TermSizeAny
|
|
||||||
TermReadKey
|
|
||||||
Test2Harness
|
|
||||||
TestPostgreSQL
|
|
||||||
TextDiff
|
|
||||||
TextTable
|
|
||||||
UUID4Tiny
|
|
||||||
YAML
|
|
||||||
XMLSimple
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
in
|
|
||||||
stdenv.mkDerivation {
|
|
||||||
|
|
||||||
name = "hydra-${version}";
|
|
||||||
|
|
||||||
src = self;
|
|
||||||
|
|
||||||
buildInputs =
|
|
||||||
[
|
|
||||||
makeWrapper
|
|
||||||
autoconf
|
|
||||||
automake
|
|
||||||
libtool
|
|
||||||
unzip
|
|
||||||
nukeReferences
|
|
||||||
pkg-config
|
|
||||||
libpqxx
|
|
||||||
top-git
|
|
||||||
mercurial
|
|
||||||
darcs
|
|
||||||
subversion
|
|
||||||
breezy
|
|
||||||
openssl
|
|
||||||
bzip2
|
|
||||||
libxslt
|
|
||||||
final.nix
|
|
||||||
perlDeps
|
|
||||||
perl
|
|
||||||
mdbook
|
|
||||||
pixz
|
|
||||||
boost
|
|
||||||
postgresql_13
|
|
||||||
(if lib.versionAtLeast lib.version "20.03pre"
|
|
||||||
then nlohmann_json
|
|
||||||
else nlohmann_json.override { multipleHeaders = true; })
|
|
||||||
prometheus-cpp
|
|
||||||
];
|
|
||||||
|
|
||||||
checkInputs = [
|
|
||||||
cacert
|
|
||||||
foreman
|
|
||||||
glibcLocales
|
|
||||||
libressl.nc
|
|
||||||
openldap
|
|
||||||
python3
|
|
||||||
];
|
|
||||||
|
|
||||||
hydraPath = lib.makeBinPath (
|
|
||||||
[
|
|
||||||
subversion
|
|
||||||
openssh
|
|
||||||
final.nix
|
|
||||||
coreutils
|
|
||||||
findutils
|
|
||||||
pixz
|
|
||||||
gzip
|
|
||||||
bzip2
|
|
||||||
xz
|
|
||||||
gnutar
|
|
||||||
unzip
|
|
||||||
git
|
|
||||||
top-git
|
|
||||||
mercurial
|
|
||||||
darcs
|
|
||||||
gnused
|
|
||||||
breezy
|
|
||||||
] ++ lib.optionals stdenv.isLinux [ rpm dpkg cdrkit ]
|
|
||||||
);
|
|
||||||
|
|
||||||
OPENLDAP_ROOT = openldap;
|
|
||||||
|
|
||||||
shellHook = ''
|
|
||||||
pushd $(git rev-parse --show-toplevel) >/dev/null
|
|
||||||
|
|
||||||
PATH=$(pwd)/src/hydra-evaluator:$(pwd)/src/script:$(pwd)/src/hydra-eval-jobs:$(pwd)/src/hydra-queue-runner:$PATH
|
|
||||||
PERL5LIB=$(pwd)/src/lib:$PERL5LIB
|
|
||||||
export HYDRA_HOME="$(pwd)/src/"
|
|
||||||
mkdir -p .hydra-data
|
|
||||||
export HYDRA_DATA="$(pwd)/.hydra-data"
|
|
||||||
export HYDRA_DBI='dbi:Pg:dbname=hydra;host=localhost;port=64444'
|
|
||||||
|
|
||||||
popd >/dev/null
|
|
||||||
'';
|
|
||||||
|
|
||||||
preConfigure = "autoreconf -vfi";
|
|
||||||
|
|
||||||
NIX_LDFLAGS = [ "-lpthread" ];
|
|
||||||
|
|
||||||
enableParallelBuilding = true;
|
|
||||||
|
|
||||||
doCheck = true;
|
|
||||||
|
|
||||||
preCheck = ''
|
|
||||||
patchShebangs .
|
|
||||||
export LOGNAME=''${LOGNAME:-foo}
|
|
||||||
# set $HOME for bzr so it can create its trace file
|
|
||||||
export HOME=$(mktemp -d)
|
|
||||||
'';
|
|
||||||
|
|
||||||
postInstall = ''
|
|
||||||
mkdir -p $out/nix-support
|
|
||||||
|
|
||||||
for i in $out/bin/*; do
|
|
||||||
read -n 4 chars < $i
|
|
||||||
if [[ $chars =~ ELF ]]; then continue; fi
|
|
||||||
wrapProgram $i \
|
|
||||||
--prefix PERL5LIB ':' $out/libexec/hydra/lib:$PERL5LIB \
|
|
||||||
--prefix PATH ':' $out/bin:$hydraPath \
|
|
||||||
--set HYDRA_RELEASE ${version} \
|
|
||||||
--set HYDRA_HOME $out/libexec/hydra \
|
|
||||||
--set NIX_RELEASE ${final.nix.name or "unknown"}
|
|
||||||
done
|
|
||||||
'';
|
|
||||||
|
|
||||||
dontStrip = true;
|
|
||||||
|
|
||||||
meta.description = "Build of Hydra on ${final.stdenv.system}";
|
|
||||||
passthru = { inherit perlDeps; inherit (final) nix; };
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
hydraJobs = {
|
hydraJobs = {
|
||||||
|
|
||||||
build = forEachSystem (system: packages.${system}.hydra);
|
build = forEachSystem (system: packages.${system}.hydra);
|
||||||
|
|
||||||
manual = forEachSystem (system:
|
buildNoTests = forEachSystem (system:
|
||||||
let pkgs = pkgsBySystem.${system}; in
|
packages.${system}.hydra.overrideAttrs (_: {
|
||||||
pkgs.runCommand "hydra-manual-${version}" { }
|
doCheck = false;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
manual = forEachSystem (system: let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
hydra = self.packages.${pkgs.stdenv.hostPlatform.system}.hydra;
|
||||||
|
in
|
||||||
|
pkgs.runCommand "hydra-manual-${hydra.version}" { }
|
||||||
''
|
''
|
||||||
mkdir -p $out/share
|
mkdir -p $out/share
|
||||||
cp -prvd ${pkgs.hydra}/share/doc $out/share/
|
cp -prvd ${hydra.doc}/share/doc $out/share/
|
||||||
|
|
||||||
mkdir $out/nix-support
|
mkdir $out/nix-support
|
||||||
echo "doc manual $out/share/doc/hydra" >> $out/nix-support/hydra-build-products
|
echo "doc manual $out/share/doc/hydra" >> $out/nix-support/hydra-build-products
|
||||||
'');
|
'');
|
||||||
|
|
||||||
tests.install = forEachSystem (system:
|
tests = import ./nixos-tests.nix {
|
||||||
with import (nixpkgs + "/nixos/lib/testing-python.nix") { inherit system; };
|
inherit forEachSystem nixpkgs nixosModules;
|
||||||
simpleTest {
|
};
|
||||||
name = "hydra-install";
|
|
||||||
nodes.machine = hydraServer;
|
|
||||||
testScript =
|
|
||||||
''
|
|
||||||
machine.wait_for_job("hydra-init")
|
|
||||||
machine.wait_for_job("hydra-server")
|
|
||||||
machine.wait_for_job("hydra-evaluator")
|
|
||||||
machine.wait_for_job("hydra-queue-runner")
|
|
||||||
machine.wait_for_open_port(3000)
|
|
||||||
machine.succeed("curl --fail http://localhost:3000/")
|
|
||||||
'';
|
|
||||||
});
|
|
||||||
|
|
||||||
tests.notifications = forEachSystem (system:
|
|
||||||
let pkgs = pkgsBySystem.${system}; in
|
|
||||||
with import (nixpkgs + "/nixos/lib/testing-python.nix") { inherit system; };
|
|
||||||
simpleTest {
|
|
||||||
name = "hydra-notifications";
|
|
||||||
nodes.machine = { pkgs, ... }: {
|
|
||||||
imports = [ hydraServer ];
|
|
||||||
services.hydra-dev.extraConfig = ''
|
|
||||||
<influxdb>
|
|
||||||
url = http://127.0.0.1:8086
|
|
||||||
db = hydra
|
|
||||||
</influxdb>
|
|
||||||
'';
|
|
||||||
services.influxdb.enable = true;
|
|
||||||
};
|
|
||||||
testScript = ''
|
|
||||||
machine.wait_for_job("hydra-init")
|
|
||||||
|
|
||||||
# Create an admin account and some other state.
|
|
||||||
machine.succeed(
|
|
||||||
"""
|
|
||||||
su - hydra -c "hydra-create-user root --email-address 'alice@example.org' --password foobar --role admin"
|
|
||||||
mkdir /run/jobset
|
|
||||||
chmod 755 /run/jobset
|
|
||||||
cp ${./t/jobs/api-test.nix} /run/jobset/default.nix
|
|
||||||
chmod 644 /run/jobset/default.nix
|
|
||||||
chown -R hydra /run/jobset
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
# Wait until InfluxDB can receive web requests
|
|
||||||
machine.wait_for_job("influxdb")
|
|
||||||
machine.wait_for_open_port(8086)
|
|
||||||
|
|
||||||
# Create an InfluxDB database where hydra will write to
|
|
||||||
machine.succeed(
|
|
||||||
"curl -XPOST 'http://127.0.0.1:8086/query' "
|
|
||||||
+ "--data-urlencode 'q=CREATE DATABASE hydra'"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Wait until hydra-server can receive HTTP requests
|
|
||||||
machine.wait_for_job("hydra-server")
|
|
||||||
machine.wait_for_open_port(3000)
|
|
||||||
|
|
||||||
# Setup the project and jobset
|
|
||||||
machine.succeed(
|
|
||||||
"su - hydra -c 'perl -I ${pkgs.hydra.perlDeps}/lib/perl5/site_perl ${./t/setup-notifications-jobset.pl}' >&2"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Wait until hydra has build the job and
|
|
||||||
# the InfluxDBNotification plugin uploaded its notification to InfluxDB
|
|
||||||
machine.wait_until_succeeds(
|
|
||||||
"curl -s -H 'Accept: application/csv' "
|
|
||||||
+ "-G 'http://127.0.0.1:8086/query?db=hydra' "
|
|
||||||
+ "--data-urlencode 'q=SELECT * FROM hydra_build_status' | grep success"
|
|
||||||
)
|
|
||||||
'';
|
|
||||||
});
|
|
||||||
|
|
||||||
tests.gitea = forEachSystem (system:
|
|
||||||
let pkgs = pkgsBySystem.${system}; in
|
|
||||||
with import (nixpkgs + "/nixos/lib/testing-python.nix") { inherit system; };
|
|
||||||
makeTest {
|
|
||||||
name = "hydra-gitea";
|
|
||||||
nodes.machine = { pkgs, ... }: {
|
|
||||||
imports = [ hydraServer ];
|
|
||||||
services.hydra-dev.extraConfig = ''
|
|
||||||
<gitea_authorization>
|
|
||||||
root=d7f16a3412e01a43a414535b16007c6931d3a9c7
|
|
||||||
</gitea_authorization>
|
|
||||||
'';
|
|
||||||
nix = {
|
|
||||||
distributedBuilds = true;
|
|
||||||
buildMachines = [{
|
|
||||||
hostName = "localhost";
|
|
||||||
systems = [ system ];
|
|
||||||
}];
|
|
||||||
binaryCaches = [ ];
|
|
||||||
};
|
|
||||||
services.gitea = {
|
|
||||||
enable = true;
|
|
||||||
database.type = "postgres";
|
|
||||||
disableRegistration = true;
|
|
||||||
httpPort = 3001;
|
|
||||||
};
|
|
||||||
services.openssh.enable = true;
|
|
||||||
environment.systemPackages = with pkgs; [ gitea git jq gawk ];
|
|
||||||
networking.firewall.allowedTCPPorts = [ 3000 ];
|
|
||||||
};
|
|
||||||
skipLint = true;
|
|
||||||
testScript =
|
|
||||||
let
|
|
||||||
scripts.mktoken = pkgs.writeText "token.sql" ''
|
|
||||||
INSERT INTO access_token (id, uid, name, created_unix, updated_unix, token_hash, token_salt, token_last_eight) VALUES (1, 1, 'hydra', 1617107360, 1617107360, 'a930f319ca362d7b49a4040ac0af74521c3a3c3303a86f327b01994430672d33b6ec53e4ea774253208686c712495e12a486', 'XRjWE9YW0g', '31d3a9c7');
|
|
||||||
'';
|
|
||||||
|
|
||||||
scripts.git-setup = pkgs.writeShellScript "setup.sh" ''
|
|
||||||
set -x
|
|
||||||
mkdir -p /tmp/repo $HOME/.ssh
|
|
||||||
cat ${snakeoilKeypair.privkey} > $HOME/.ssh/privk
|
|
||||||
chmod 0400 $HOME/.ssh/privk
|
|
||||||
git -C /tmp/repo init
|
|
||||||
cp ${smallDrv} /tmp/repo/jobset.nix
|
|
||||||
git -C /tmp/repo add .
|
|
||||||
git config --global user.email test@localhost
|
|
||||||
git config --global user.name test
|
|
||||||
git -C /tmp/repo commit -m 'Initial import'
|
|
||||||
git -C /tmp/repo remote add origin gitea@machine:root/repo
|
|
||||||
GIT_SSH_COMMAND='ssh -i $HOME/.ssh/privk -o StrictHostKeyChecking=no' \
|
|
||||||
git -C /tmp/repo push origin master
|
|
||||||
git -C /tmp/repo log >&2
|
|
||||||
'';
|
|
||||||
|
|
||||||
scripts.hydra-setup = pkgs.writeShellScript "hydra.sh" ''
|
|
||||||
set -x
|
|
||||||
su -l hydra -c "hydra-create-user root --email-address \
|
|
||||||
'alice@example.org' --password foobar --role admin"
|
|
||||||
|
|
||||||
URL=http://localhost:3000
|
|
||||||
USERNAME="root"
|
|
||||||
PASSWORD="foobar"
|
|
||||||
PROJECT_NAME="trivial"
|
|
||||||
JOBSET_NAME="trivial"
|
|
||||||
mycurl() {
|
|
||||||
curl --referer $URL -H "Accept: application/json" \
|
|
||||||
-H "Content-Type: application/json" $@
|
|
||||||
}
|
|
||||||
|
|
||||||
cat >data.json <<EOF
|
|
||||||
{ "username": "$USERNAME", "password": "$PASSWORD" }
|
|
||||||
EOF
|
|
||||||
mycurl -X POST -d '@data.json' $URL/login -c hydra-cookie.txt
|
|
||||||
|
|
||||||
cat >data.json <<EOF
|
|
||||||
{
|
|
||||||
"displayname":"Trivial",
|
|
||||||
"enabled":"1",
|
|
||||||
"visible":"1"
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
mycurl --silent -X PUT $URL/project/$PROJECT_NAME \
|
|
||||||
-d @data.json -b hydra-cookie.txt
|
|
||||||
|
|
||||||
cat >data.json <<EOF
|
|
||||||
{
|
|
||||||
"description": "Trivial",
|
|
||||||
"checkinterval": "60",
|
|
||||||
"enabled": "1",
|
|
||||||
"visible": "1",
|
|
||||||
"keepnr": "1",
|
|
||||||
"enableemail": true,
|
|
||||||
"emailoverride": "hydra@localhost",
|
|
||||||
"type": 0,
|
|
||||||
"nixexprinput": "git",
|
|
||||||
"nixexprpath": "jobset.nix",
|
|
||||||
"inputs": {
|
|
||||||
"git": {"value": "http://localhost:3001/root/repo.git", "type": "git"},
|
|
||||||
"gitea_repo_name": {"value": "repo", "type": "string"},
|
|
||||||
"gitea_repo_owner": {"value": "root", "type": "string"},
|
|
||||||
"gitea_status_repo": {"value": "git", "type": "string"},
|
|
||||||
"gitea_http_url": {"value": "http://localhost:3001", "type": "string"}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
mycurl --silent -X PUT $URL/jobset/$PROJECT_NAME/$JOBSET_NAME \
|
|
||||||
-d @data.json -b hydra-cookie.txt
|
|
||||||
'';
|
|
||||||
|
|
||||||
api_token = "d7f16a3412e01a43a414535b16007c6931d3a9c7";
|
|
||||||
|
|
||||||
snakeoilKeypair = {
|
|
||||||
privkey = pkgs.writeText "privkey.snakeoil" ''
|
|
||||||
-----BEGIN EC PRIVATE KEY-----
|
|
||||||
MHcCAQEEIHQf/khLvYrQ8IOika5yqtWvI0oquHlpRLTZiJy5dRJmoAoGCCqGSM49
|
|
||||||
AwEHoUQDQgAEKF0DYGbBwbj06tA3fd/+yP44cvmwmHBWXZCKbS+RQlAKvLXMWkpN
|
|
||||||
r1lwMyJZoSGgBHoUahoYjTh9/sJL7XLJtA==
|
|
||||||
-----END EC PRIVATE KEY-----
|
|
||||||
'';
|
|
||||||
|
|
||||||
pubkey = pkgs.lib.concatStrings [
|
|
||||||
"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHA"
|
|
||||||
"yNTYAAABBBChdA2BmwcG49OrQN33f/sj+OHL5sJhwVl2Qim0vkUJQCry1zFpKTa"
|
|
||||||
"9ZcDMiWaEhoAR6FGoaGI04ff7CS+1yybQ= sakeoil"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
smallDrv = pkgs.writeText "jobset.nix" ''
|
|
||||||
{ trivial = builtins.derivation {
|
|
||||||
name = "trivial";
|
|
||||||
system = "${system}";
|
|
||||||
builder = "/bin/sh";
|
|
||||||
allowSubstitutes = false;
|
|
||||||
preferLocalBuild = true;
|
|
||||||
args = ["-c" "echo success > $out; exit 0"];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
in
|
|
||||||
''
|
|
||||||
import json
|
|
||||||
|
|
||||||
machine.start()
|
|
||||||
machine.wait_for_unit("multi-user.target")
|
|
||||||
machine.wait_for_open_port(3000)
|
|
||||||
machine.wait_for_open_port(3001)
|
|
||||||
|
|
||||||
machine.succeed(
|
|
||||||
"su -l gitea -c 'GITEA_WORK_DIR=/var/lib/gitea gitea admin user create "
|
|
||||||
+ "--username root --password root --email test@localhost'"
|
|
||||||
)
|
|
||||||
machine.succeed("su -l postgres -c 'psql gitea < ${scripts.mktoken}'")
|
|
||||||
|
|
||||||
machine.succeed(
|
|
||||||
"curl --fail -X POST http://localhost:3001/api/v1/user/repos "
|
|
||||||
+ "-H 'Accept: application/json' -H 'Content-Type: application/json' "
|
|
||||||
+ f"-H 'Authorization: token ${api_token}'"
|
|
||||||
+ ' -d \'{"auto_init":false, "description":"string", "license":"mit", "name":"repo", "private":false}\'''
|
|
||||||
)
|
|
||||||
|
|
||||||
machine.succeed(
|
|
||||||
"curl --fail -X POST http://localhost:3001/api/v1/user/keys "
|
|
||||||
+ "-H 'Accept: application/json' -H 'Content-Type: application/json' "
|
|
||||||
+ f"-H 'Authorization: token ${api_token}'"
|
|
||||||
+ ' -d \'{"key":"${snakeoilKeypair.pubkey}","read_only":true,"title":"SSH"}\'''
|
|
||||||
)
|
|
||||||
|
|
||||||
machine.succeed(
|
|
||||||
"${scripts.git-setup}"
|
|
||||||
)
|
|
||||||
|
|
||||||
machine.succeed(
|
|
||||||
"${scripts.hydra-setup}"
|
|
||||||
)
|
|
||||||
|
|
||||||
machine.wait_until_succeeds(
|
|
||||||
'curl -Lf -s http://localhost:3000/build/1 -H "Accept: application/json" '
|
|
||||||
+ '| jq .buildstatus | xargs test 0 -eq'
|
|
||||||
)
|
|
||||||
|
|
||||||
data = machine.succeed(
|
|
||||||
'curl -Lf -s "http://localhost:3001/api/v1/repos/root/repo/statuses/$(cd /tmp/repo && git show | head -n1 | awk "{print \\$2}")" '
|
|
||||||
+ "-H 'Accept: application/json' -H 'Content-Type: application/json' "
|
|
||||||
+ f"-H 'Authorization: token ${api_token}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
response = json.loads(data)
|
|
||||||
|
|
||||||
assert len(response) == 2, "Expected exactly two status updates for latest commit!"
|
|
||||||
assert response[0]['status'] == "success", "Expected latest status to be success!"
|
|
||||||
assert response[1]['status'] == "pending", "Expected first status to be pending!"
|
|
||||||
|
|
||||||
machine.shutdown()
|
|
||||||
'';
|
|
||||||
});
|
|
||||||
|
|
||||||
tests.validate-openapi = forEachSystem (system:
|
|
||||||
let pkgs = pkgsBySystem.${system}; in
|
|
||||||
pkgs.runCommand "validate-openapi"
|
|
||||||
{ buildInputs = [ pkgs.openapi-generator-cli ]; }
|
|
||||||
''
|
|
||||||
openapi-generator-cli validate -i ${./hydra-api.yaml}
|
|
||||||
touch $out
|
|
||||||
'');
|
|
||||||
|
|
||||||
container = nixosConfigurations.container.config.system.build.toplevel;
|
container = nixosConfigurations.container.config.system.build.toplevel;
|
||||||
};
|
};
|
||||||
@@ -560,61 +83,44 @@
|
|||||||
validate-openapi = hydraJobs.tests.validate-openapi.${system};
|
validate-openapi = hydraJobs.tests.validate-openapi.${system};
|
||||||
});
|
});
|
||||||
|
|
||||||
packages = forEachSystem (system: {
|
packages = forEachSystem (system: let
|
||||||
hydra = pkgsBySystem.${system}.hydra;
|
inherit (nixpkgs) lib;
|
||||||
default = pkgsBySystem.${system}.hydra;
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
nixDependencies = lib.makeScope pkgs.newScope
|
||||||
|
(import (nix + "/packaging/dependencies.nix") {
|
||||||
|
inherit pkgs;
|
||||||
|
inherit (pkgs) stdenv;
|
||||||
|
inputs = {};
|
||||||
|
});
|
||||||
|
nixComponents = lib.makeScope nixDependencies.newScope
|
||||||
|
(import (nix + "/packaging/components.nix") {
|
||||||
|
officialRelease = true;
|
||||||
|
inherit lib pkgs;
|
||||||
|
src = nix;
|
||||||
|
maintainers = [ ];
|
||||||
|
});
|
||||||
|
in {
|
||||||
|
nix-eval-jobs = pkgs.callPackage nix-eval-jobs {
|
||||||
|
inherit nixComponents;
|
||||||
|
};
|
||||||
|
hydra = pkgs.callPackage ./package.nix {
|
||||||
|
inherit (nixpkgs.lib) fileset;
|
||||||
|
inherit nixComponents;
|
||||||
|
inherit (self.packages.${system}) nix-eval-jobs;
|
||||||
|
rawSrc = self;
|
||||||
|
};
|
||||||
|
default = self.packages.${system}.hydra;
|
||||||
});
|
});
|
||||||
|
|
||||||
nixosModules.hydra = {
|
nixosModules = import ./nixos-modules {
|
||||||
imports = [ ./hydra-module.nix ];
|
inherit self;
|
||||||
nixpkgs.overlays = [ self.overlays.default nix.overlays.default ];
|
|
||||||
};
|
|
||||||
|
|
||||||
nixosModules.hydraTest = { pkgs, ... }: {
|
|
||||||
imports = [ self.nixosModules.hydra ];
|
|
||||||
|
|
||||||
services.hydra-dev.enable = true;
|
|
||||||
services.hydra-dev.hydraURL = "http://hydra.example.org";
|
|
||||||
services.hydra-dev.notificationSender = "admin@hydra.example.org";
|
|
||||||
|
|
||||||
systemd.services.hydra-send-stats.enable = false;
|
|
||||||
|
|
||||||
services.postgresql.enable = true;
|
|
||||||
services.postgresql.package = pkgs.postgresql_11;
|
|
||||||
|
|
||||||
# The following is to work around the following error from hydra-server:
|
|
||||||
# [error] Caught exception in engine "Cannot determine local time zone"
|
|
||||||
time.timeZone = "UTC";
|
|
||||||
|
|
||||||
nix.extraOptions = ''
|
|
||||||
allowed-uris = https://github.com/
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
nixosModules.hydraProxy = {
|
|
||||||
services.httpd = {
|
|
||||||
enable = true;
|
|
||||||
adminAddr = "hydra-admin@example.org";
|
|
||||||
extraConfig = ''
|
|
||||||
<Proxy *>
|
|
||||||
Order deny,allow
|
|
||||||
Allow from all
|
|
||||||
</Proxy>
|
|
||||||
|
|
||||||
ProxyRequests Off
|
|
||||||
ProxyPreserveHost On
|
|
||||||
ProxyPass /apache-errors !
|
|
||||||
ErrorDocument 503 /apache-errors/503.html
|
|
||||||
ProxyPass / http://127.0.0.1:3000/ retry=5 disablereuse=on
|
|
||||||
ProxyPassReverse / http://127.0.0.1:3000/
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
nixosConfigurations.container = nixpkgs.lib.nixosSystem {
|
nixosConfigurations.container = nixpkgs.lib.nixosSystem {
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
modules =
|
modules =
|
||||||
[
|
[
|
||||||
|
self.nixosModules.hydra
|
||||||
self.nixosModules.hydraTest
|
self.nixosModules.hydraTest
|
||||||
self.nixosModules.hydraProxy
|
self.nixosModules.hydraProxy
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
|
export PATH=$(pwd)/src/script:$PATH
|
||||||
|
|
||||||
# wait for hydra-server to listen
|
# wait for hydra-server to listen
|
||||||
while ! nc -z localhost 63333; do sleep 1; done
|
while ! nc -z localhost 63333; do sleep 1; done
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
|
export PATH=$(pwd)/src/script:$PATH
|
||||||
|
|
||||||
# wait for postgresql to listen
|
# wait for postgresql to listen
|
||||||
while ! pg_isready -h $(pwd)/.hydra-data/postgres -p 64444; do sleep 1; done
|
while ! pg_isready -h $(pwd)/.hydra-data/postgres -p 64444; do sleep 1; done
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
|
export PATH=$(pwd)/src/script:$PATH
|
||||||
|
|
||||||
# wait for hydra-server to listen
|
# wait for hydra-server to listen
|
||||||
while ! nc -z localhost 63333; do sleep 1; done
|
while ! nc -z localhost 63333; do sleep 1; done
|
||||||
|
|
||||||
|
|||||||
132
hydra-api.yaml
132
hydra-api.yaml
@@ -70,7 +70,7 @@ paths:
|
|||||||
$ref: '#/components/examples/projects-success'
|
$ref: '#/components/examples/projects-success'
|
||||||
|
|
||||||
/api/push:
|
/api/push:
|
||||||
put:
|
post:
|
||||||
summary: trigger jobsets
|
summary: trigger jobsets
|
||||||
parameters:
|
parameters:
|
||||||
- in: query
|
- in: query
|
||||||
@@ -78,6 +78,11 @@ paths:
|
|||||||
description: project and jobset formatted as "<project>:<jobset>" to evaluate
|
description: project and jobset formatted as "<project>:<jobset>" to evaluate
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
- in: query
|
||||||
|
name: force
|
||||||
|
description: when set to true the jobset gets evaluated even when it did not change
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: jobset trigger response
|
description: jobset trigger response
|
||||||
@@ -569,6 +574,131 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/JobsetEvalBuilds'
|
$ref: '#/components/schemas/JobsetEvalBuilds'
|
||||||
|
|
||||||
|
/jobset/{project-id}/{jobset-id}/latest-eval:
|
||||||
|
get:
|
||||||
|
summary: Redirects to the latest finished evaluation for a jobset
|
||||||
|
parameters:
|
||||||
|
- name: project-id
|
||||||
|
in: path
|
||||||
|
description: project identifier
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: jobset-id
|
||||||
|
in: path
|
||||||
|
description: jobset identifier
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'302':
|
||||||
|
description: the evaluation to redirect to
|
||||||
|
headers:
|
||||||
|
Location:
|
||||||
|
example: /eval/1?name={jobset-id}
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
/job/{project-id}/{jobset-id}/{job-id}/latest:
|
||||||
|
get:
|
||||||
|
summary: Redirects to the latest succesful build for a job
|
||||||
|
parameters:
|
||||||
|
- name: project-id
|
||||||
|
in: path
|
||||||
|
description: project identifier
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: jobset-id
|
||||||
|
in: path
|
||||||
|
description: jobset identifier
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: job-id
|
||||||
|
in: path
|
||||||
|
description: job identifier
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'302':
|
||||||
|
description: the build to redirect to
|
||||||
|
headers:
|
||||||
|
Location:
|
||||||
|
example: /build/1
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
/job/{project-id}/{jobset-id}/{job-id}/latest-for/{system}:
|
||||||
|
get:
|
||||||
|
summary: Redirects to the latest succesful build for a job
|
||||||
|
parameters:
|
||||||
|
- name: project-id
|
||||||
|
in: path
|
||||||
|
description: project identifier
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: jobset-id
|
||||||
|
in: path
|
||||||
|
description: jobset identifier
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: job-id
|
||||||
|
in: path
|
||||||
|
description: job identifier
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: system
|
||||||
|
in: path
|
||||||
|
description: system
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: x86_64-linux
|
||||||
|
responses:
|
||||||
|
'302':
|
||||||
|
description: the build to redirect to
|
||||||
|
headers:
|
||||||
|
Location:
|
||||||
|
example: /build/1
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
/job/{project-id}/{jobset-id}/{job-id}/latest-finished:
|
||||||
|
get:
|
||||||
|
summary: Redirects to the latest succesful build for a job from a finished evaluation
|
||||||
|
parameters:
|
||||||
|
- name: project-id
|
||||||
|
in: path
|
||||||
|
description: project identifier
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: jobset-id
|
||||||
|
in: path
|
||||||
|
description: jobset identifier
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: job-id
|
||||||
|
in: path
|
||||||
|
description: job identifier
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'302':
|
||||||
|
description: the build to redirect to
|
||||||
|
headers:
|
||||||
|
Location:
|
||||||
|
example: /build/1
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
|
|
||||||
|
|||||||
26
meson.build
Normal file
26
meson.build
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
project('hydra', 'cpp',
|
||||||
|
version: files('version.txt'),
|
||||||
|
license: 'GPL-3.0',
|
||||||
|
default_options: [
|
||||||
|
'debug=true',
|
||||||
|
'optimization=2',
|
||||||
|
'cpp_std=c++23',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
nix_util_dep = dependency('nix-util', required: true)
|
||||||
|
nix_store_dep = dependency('nix-store', required: true)
|
||||||
|
nix_main_dep = dependency('nix-main', required: true)
|
||||||
|
|
||||||
|
pqxx_dep = dependency('libpqxx', required: true)
|
||||||
|
|
||||||
|
prom_cpp_core_dep = dependency('prometheus-cpp-core', required: true)
|
||||||
|
prom_cpp_pull_dep = dependency('prometheus-cpp-pull', required: true)
|
||||||
|
|
||||||
|
mdbook = find_program('mdbook', native: true)
|
||||||
|
perl = find_program('perl', native: true)
|
||||||
|
|
||||||
|
subdir('doc/manual')
|
||||||
|
subdir('nixos-modules')
|
||||||
|
subdir('src')
|
||||||
|
subdir('t')
|
||||||
47
nixos-modules/default.nix
Normal file
47
nixos-modules/default.nix
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{ self }:
|
||||||
|
|
||||||
|
{
|
||||||
|
hydra = { pkgs, lib,... }: {
|
||||||
|
_file = ./default.nix;
|
||||||
|
imports = [ ./hydra.nix ];
|
||||||
|
services.hydra-dev.package = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.hydra;
|
||||||
|
};
|
||||||
|
|
||||||
|
hydraTest = { pkgs, ... }: {
|
||||||
|
services.hydra-dev.enable = true;
|
||||||
|
services.hydra-dev.hydraURL = "http://hydra.example.org";
|
||||||
|
services.hydra-dev.notificationSender = "admin@hydra.example.org";
|
||||||
|
|
||||||
|
systemd.services.hydra-send-stats.enable = false;
|
||||||
|
|
||||||
|
services.postgresql.enable = true;
|
||||||
|
|
||||||
|
# The following is to work around the following error from hydra-server:
|
||||||
|
# [error] Caught exception in engine "Cannot determine local time zone"
|
||||||
|
time.timeZone = "UTC";
|
||||||
|
|
||||||
|
nix.extraOptions = ''
|
||||||
|
allowed-uris = https://github.com/
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
hydraProxy = {
|
||||||
|
services.httpd = {
|
||||||
|
enable = true;
|
||||||
|
adminAddr = "hydra-admin@example.org";
|
||||||
|
extraConfig = ''
|
||||||
|
<Proxy *>
|
||||||
|
Order deny,allow
|
||||||
|
Allow from all
|
||||||
|
</Proxy>
|
||||||
|
|
||||||
|
ProxyRequests Off
|
||||||
|
ProxyPreserveHost On
|
||||||
|
ProxyPass /apache-errors !
|
||||||
|
ErrorDocument 503 /apache-errors/503.html
|
||||||
|
ProxyPass / http://127.0.0.1:3000/ retry=5 disablereuse=on
|
||||||
|
ProxyPassReverse / http://127.0.0.1:3000/
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -68,8 +68,6 @@ in
|
|||||||
|
|
||||||
package = mkOption {
|
package = mkOption {
|
||||||
type = types.path;
|
type = types.path;
|
||||||
default = pkgs.hydra;
|
|
||||||
defaultText = literalExpression "pkgs.hydra";
|
|
||||||
description = "The Hydra package.";
|
description = "The Hydra package.";
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -230,8 +228,8 @@ in
|
|||||||
|
|
||||||
nix.settings = {
|
nix.settings = {
|
||||||
trusted-users = [ "hydra-queue-runner" ];
|
trusted-users = [ "hydra-queue-runner" ];
|
||||||
gc-keep-outputs = true;
|
keep-outputs = true;
|
||||||
gc-keep-derivations = true;
|
keep-derivations = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
services.hydra-dev.extraConfig =
|
services.hydra-dev.extraConfig =
|
||||||
@@ -340,8 +338,9 @@ in
|
|||||||
systemd.services.hydra-queue-runner =
|
systemd.services.hydra-queue-runner =
|
||||||
{ wantedBy = [ "multi-user.target" ];
|
{ wantedBy = [ "multi-user.target" ];
|
||||||
requires = [ "hydra-init.service" ];
|
requires = [ "hydra-init.service" ];
|
||||||
|
wants = [ "network-online.target" ];
|
||||||
after = [ "hydra-init.service" "network.target" "network-online.target" ];
|
after = [ "hydra-init.service" "network.target" "network-online.target" ];
|
||||||
path = [ cfg.package pkgs.nettools pkgs.openssh pkgs.bzip2 config.nix.package ];
|
path = [ cfg.package pkgs.hostname-debian pkgs.openssh pkgs.bzip2 config.nix.package ];
|
||||||
restartTriggers = [ hydraConf ];
|
restartTriggers = [ hydraConf ];
|
||||||
environment = env // {
|
environment = env // {
|
||||||
PGPASSFILE = "${baseDir}/pgpass-queue-runner"; # grrr
|
PGPASSFILE = "${baseDir}/pgpass-queue-runner"; # grrr
|
||||||
@@ -365,7 +364,7 @@ in
|
|||||||
requires = [ "hydra-init.service" ];
|
requires = [ "hydra-init.service" ];
|
||||||
restartTriggers = [ hydraConf ];
|
restartTriggers = [ hydraConf ];
|
||||||
after = [ "hydra-init.service" "network.target" ];
|
after = [ "hydra-init.service" "network.target" ];
|
||||||
path = with pkgs; [ nettools cfg.package jq ];
|
path = with pkgs; [ hostname-debian cfg.package ];
|
||||||
environment = env // {
|
environment = env // {
|
||||||
HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-evaluator";
|
HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-evaluator";
|
||||||
};
|
};
|
||||||
@@ -408,6 +407,7 @@ in
|
|||||||
requires = [ "hydra-init.service" ];
|
requires = [ "hydra-init.service" ];
|
||||||
after = [ "hydra-init.service" ];
|
after = [ "hydra-init.service" ];
|
||||||
restartTriggers = [ hydraConf ];
|
restartTriggers = [ hydraConf ];
|
||||||
|
path = [ pkgs.zstd ];
|
||||||
environment = env // {
|
environment = env // {
|
||||||
PGPASSFILE = "${baseDir}/pgpass-queue-runner"; # grrr
|
PGPASSFILE = "${baseDir}/pgpass-queue-runner"; # grrr
|
||||||
HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-notify";
|
HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-notify";
|
||||||
@@ -458,10 +458,17 @@ in
|
|||||||
# logs automatically after a step finishes, but this doesn't work
|
# logs automatically after a step finishes, but this doesn't work
|
||||||
# if the queue runner is stopped prematurely.
|
# if the queue runner is stopped prematurely.
|
||||||
systemd.services.hydra-compress-logs =
|
systemd.services.hydra-compress-logs =
|
||||||
{ path = [ pkgs.bzip2 ];
|
{ path = [ pkgs.bzip2 pkgs.zstd ];
|
||||||
script =
|
script =
|
||||||
''
|
''
|
||||||
find ${baseDir}/build-logs -type f -name "*.drv" -mtime +3 -size +0c | xargs -r bzip2 -v -f
|
set -eou pipefail
|
||||||
|
compression=$(sed -nr 's/compress_build_logs_compression = ()/\1/p' ${baseDir}/hydra.conf)
|
||||||
|
if [[ $compression == "" || $compression == bzip2 ]]; then
|
||||||
|
compressionCmd=(bzip2)
|
||||||
|
elif [[ $compression == zstd ]]; then
|
||||||
|
compressionCmd=(zstd --rm)
|
||||||
|
fi
|
||||||
|
find ${baseDir}/build-logs -ignore_readdir_race -type f -name "*.drv" -mtime +3 -size +0c -print0 | xargs -0 -r "''${compressionCmd[@]}" --force --quiet
|
||||||
'';
|
'';
|
||||||
startAt = "Sun 01:45";
|
startAt = "Sun 01:45";
|
||||||
};
|
};
|
||||||
4
nixos-modules/meson.build
Normal file
4
nixos-modules/meson.build
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
install_data('hydra.nix',
|
||||||
|
install_dir: get_option('datadir') / 'nix',
|
||||||
|
rename: ['hydra-module.nix'],
|
||||||
|
)
|
||||||
306
nixos-tests.nix
Normal file
306
nixos-tests.nix
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
{ forEachSystem, nixpkgs, nixosModules }:
|
||||||
|
|
||||||
|
let
|
||||||
|
# NixOS configuration used for VM tests.
|
||||||
|
hydraServer =
|
||||||
|
{ pkgs, ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
nixosModules.hydra
|
||||||
|
nixosModules.hydraTest
|
||||||
|
];
|
||||||
|
|
||||||
|
virtualisation.memorySize = 1024;
|
||||||
|
virtualisation.writableStore = true;
|
||||||
|
|
||||||
|
environment.systemPackages = [ pkgs.perlPackages.LWP pkgs.perlPackages.JSON ];
|
||||||
|
|
||||||
|
nix = {
|
||||||
|
# Without this nix tries to fetch packages from the default
|
||||||
|
# cache.nixos.org which is not reachable from this sandboxed NixOS test.
|
||||||
|
settings.substituters = [ ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
in
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
install = forEachSystem (system:
|
||||||
|
(import (nixpkgs + "/nixos/lib/testing-python.nix") { inherit system; }).simpleTest {
|
||||||
|
name = "hydra-install";
|
||||||
|
nodes.machine = hydraServer;
|
||||||
|
testScript =
|
||||||
|
''
|
||||||
|
machine.wait_for_job("hydra-init")
|
||||||
|
machine.wait_for_job("hydra-server")
|
||||||
|
machine.wait_for_job("hydra-evaluator")
|
||||||
|
machine.wait_for_job("hydra-queue-runner")
|
||||||
|
machine.wait_for_open_port(3000)
|
||||||
|
machine.succeed("curl --fail http://localhost:3000/")
|
||||||
|
'';
|
||||||
|
});
|
||||||
|
|
||||||
|
notifications = forEachSystem (system:
|
||||||
|
(import (nixpkgs + "/nixos/lib/testing-python.nix") { inherit system; }).simpleTest {
|
||||||
|
name = "hydra-notifications";
|
||||||
|
nodes.machine = {
|
||||||
|
imports = [ hydraServer ];
|
||||||
|
services.hydra-dev.extraConfig = ''
|
||||||
|
<influxdb>
|
||||||
|
url = http://127.0.0.1:8086
|
||||||
|
db = hydra
|
||||||
|
</influxdb>
|
||||||
|
'';
|
||||||
|
services.influxdb.enable = true;
|
||||||
|
};
|
||||||
|
testScript = { nodes, ... }: ''
|
||||||
|
machine.wait_for_job("hydra-init")
|
||||||
|
|
||||||
|
# Create an admin account and some other state.
|
||||||
|
machine.succeed(
|
||||||
|
"""
|
||||||
|
su - hydra -c "hydra-create-user root --email-address 'alice@example.org' --password foobar --role admin"
|
||||||
|
mkdir /run/jobset
|
||||||
|
chmod 755 /run/jobset
|
||||||
|
cp ${./t/jobs/api-test.nix} /run/jobset/default.nix
|
||||||
|
chmod 644 /run/jobset/default.nix
|
||||||
|
chown -R hydra /run/jobset
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wait until InfluxDB can receive web requests
|
||||||
|
machine.wait_for_job("influxdb")
|
||||||
|
machine.wait_for_open_port(8086)
|
||||||
|
|
||||||
|
# Create an InfluxDB database where hydra will write to
|
||||||
|
machine.succeed(
|
||||||
|
"curl -XPOST 'http://127.0.0.1:8086/query' "
|
||||||
|
+ "--data-urlencode 'q=CREATE DATABASE hydra'"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wait until hydra-server can receive HTTP requests
|
||||||
|
machine.wait_for_job("hydra-server")
|
||||||
|
machine.wait_for_open_port(3000)
|
||||||
|
|
||||||
|
# Setup the project and jobset
|
||||||
|
machine.succeed(
|
||||||
|
"su - hydra -c 'perl -I ${nodes.machine.services.hydra-dev.package.perlDeps}/lib/perl5/site_perl ${./t/setup-notifications-jobset.pl}' >&2"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wait until hydra has build the job and
|
||||||
|
# the InfluxDBNotification plugin uploaded its notification to InfluxDB
|
||||||
|
machine.wait_until_succeeds(
|
||||||
|
"curl -s -H 'Accept: application/csv' "
|
||||||
|
+ "-G 'http://127.0.0.1:8086/query?db=hydra' "
|
||||||
|
+ "--data-urlencode 'q=SELECT * FROM hydra_build_status' | grep success"
|
||||||
|
)
|
||||||
|
'';
|
||||||
|
});
|
||||||
|
|
||||||
|
gitea = forEachSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
in
|
||||||
|
(import (nixpkgs + "/nixos/lib/testing-python.nix") { inherit system; }).makeTest {
|
||||||
|
name = "hydra-gitea";
|
||||||
|
nodes.machine = { pkgs, ... }: {
|
||||||
|
imports = [ hydraServer ];
|
||||||
|
services.hydra-dev.extraConfig = ''
|
||||||
|
<gitea_authorization>
|
||||||
|
root=d7f16a3412e01a43a414535b16007c6931d3a9c7
|
||||||
|
</gitea_authorization>
|
||||||
|
'';
|
||||||
|
nixpkgs.config.permittedInsecurePackages = [ "gitea-1.19.4" ];
|
||||||
|
nix = {
|
||||||
|
settings.substituters = [ ];
|
||||||
|
};
|
||||||
|
services.gitea = {
|
||||||
|
enable = true;
|
||||||
|
database.type = "postgres";
|
||||||
|
settings = {
|
||||||
|
service.DISABLE_REGISTRATION = true;
|
||||||
|
server.HTTP_PORT = 3001;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
services.openssh.enable = true;
|
||||||
|
environment.systemPackages = with pkgs; [ gitea git jq gawk ];
|
||||||
|
networking.firewall.allowedTCPPorts = [ 3000 ];
|
||||||
|
};
|
||||||
|
skipLint = true;
|
||||||
|
testScript =
|
||||||
|
let
|
||||||
|
scripts.mktoken = pkgs.writeText "token.sql" ''
|
||||||
|
INSERT INTO access_token (id, uid, name, created_unix, updated_unix, token_hash, token_salt, token_last_eight, scope) VALUES (1, 1, 'hydra', 1617107360, 1617107360, 'a930f319ca362d7b49a4040ac0af74521c3a3c3303a86f327b01994430672d33b6ec53e4ea774253208686c712495e12a486', 'XRjWE9YW0g', '31d3a9c7', 'all');
|
||||||
|
'';
|
||||||
|
|
||||||
|
scripts.git-setup = pkgs.writeShellScript "setup.sh" ''
|
||||||
|
set -x
|
||||||
|
mkdir -p /tmp/repo $HOME/.ssh
|
||||||
|
cat ${snakeoilKeypair.privkey} > $HOME/.ssh/privk
|
||||||
|
chmod 0400 $HOME/.ssh/privk
|
||||||
|
git -C /tmp/repo init
|
||||||
|
cp ${smallDrv} /tmp/repo/jobset.nix
|
||||||
|
git -C /tmp/repo add .
|
||||||
|
git config --global user.email test@localhost
|
||||||
|
git config --global user.name test
|
||||||
|
git -C /tmp/repo commit -m 'Initial import'
|
||||||
|
git -C /tmp/repo remote add origin gitea@machine:root/repo
|
||||||
|
GIT_SSH_COMMAND='ssh -i $HOME/.ssh/privk -o StrictHostKeyChecking=no' \
|
||||||
|
git -C /tmp/repo push origin master
|
||||||
|
git -C /tmp/repo log >&2
|
||||||
|
'';
|
||||||
|
|
||||||
|
scripts.hydra-setup = pkgs.writeShellScript "hydra.sh" ''
|
||||||
|
set -x
|
||||||
|
su -l hydra -c "hydra-create-user root --email-address \
|
||||||
|
'alice@example.org' --password foobar --role admin"
|
||||||
|
|
||||||
|
URL=http://localhost:3000
|
||||||
|
USERNAME="root"
|
||||||
|
PASSWORD="foobar"
|
||||||
|
PROJECT_NAME="trivial"
|
||||||
|
JOBSET_NAME="trivial"
|
||||||
|
mycurl() {
|
||||||
|
curl --referer $URL -H "Accept: application/json" \
|
||||||
|
-H "Content-Type: application/json" $@
|
||||||
|
}
|
||||||
|
|
||||||
|
cat >data.json <<EOF
|
||||||
|
{ "username": "$USERNAME", "password": "$PASSWORD" }
|
||||||
|
EOF
|
||||||
|
mycurl -X POST -d '@data.json' $URL/login -c hydra-cookie.txt
|
||||||
|
|
||||||
|
cat >data.json <<EOF
|
||||||
|
{
|
||||||
|
"displayname":"Trivial",
|
||||||
|
"enabled":"1",
|
||||||
|
"visible":"1"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
mycurl --silent -X PUT $URL/project/$PROJECT_NAME \
|
||||||
|
-d @data.json -b hydra-cookie.txt
|
||||||
|
|
||||||
|
cat >data.json <<EOF
|
||||||
|
{
|
||||||
|
"description": "Trivial",
|
||||||
|
"checkinterval": "60",
|
||||||
|
"enabled": "1",
|
||||||
|
"visible": "1",
|
||||||
|
"keepnr": "1",
|
||||||
|
"enableemail": true,
|
||||||
|
"emailoverride": "hydra@localhost",
|
||||||
|
"type": 0,
|
||||||
|
"nixexprinput": "git",
|
||||||
|
"nixexprpath": "jobset.nix",
|
||||||
|
"inputs": {
|
||||||
|
"git": {"value": "http://localhost:3001/root/repo.git", "type": "git"},
|
||||||
|
"gitea_repo_name": {"value": "repo", "type": "string"},
|
||||||
|
"gitea_repo_owner": {"value": "root", "type": "string"},
|
||||||
|
"gitea_status_repo": {"value": "git", "type": "string"},
|
||||||
|
"gitea_http_url": {"value": "http://localhost:3001", "type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
mycurl --silent -X PUT $URL/jobset/$PROJECT_NAME/$JOBSET_NAME \
|
||||||
|
-d @data.json -b hydra-cookie.txt
|
||||||
|
'';
|
||||||
|
|
||||||
|
api_token = "d7f16a3412e01a43a414535b16007c6931d3a9c7";
|
||||||
|
|
||||||
|
snakeoilKeypair = {
|
||||||
|
privkey = pkgs.writeText "privkey.snakeoil" ''
|
||||||
|
-----BEGIN EC PRIVATE KEY-----
|
||||||
|
MHcCAQEEIHQf/khLvYrQ8IOika5yqtWvI0oquHlpRLTZiJy5dRJmoAoGCCqGSM49
|
||||||
|
AwEHoUQDQgAEKF0DYGbBwbj06tA3fd/+yP44cvmwmHBWXZCKbS+RQlAKvLXMWkpN
|
||||||
|
r1lwMyJZoSGgBHoUahoYjTh9/sJL7XLJtA==
|
||||||
|
-----END EC PRIVATE KEY-----
|
||||||
|
'';
|
||||||
|
|
||||||
|
pubkey = pkgs.lib.concatStrings [
|
||||||
|
"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHA"
|
||||||
|
"yNTYAAABBBChdA2BmwcG49OrQN33f/sj+OHL5sJhwVl2Qim0vkUJQCry1zFpKTa"
|
||||||
|
"9ZcDMiWaEhoAR6FGoaGI04ff7CS+1yybQ= sakeoil"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
smallDrv = pkgs.writeText "jobset.nix" ''
|
||||||
|
{ trivial = builtins.derivation {
|
||||||
|
name = "trivial";
|
||||||
|
system = "${system}";
|
||||||
|
builder = "/bin/sh";
|
||||||
|
allowSubstitutes = false;
|
||||||
|
preferLocalBuild = true;
|
||||||
|
args = ["-c" "echo success > $out; exit 0"];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
''
|
||||||
|
import json
|
||||||
|
|
||||||
|
machine.start()
|
||||||
|
machine.wait_for_unit("multi-user.target")
|
||||||
|
machine.wait_for_open_port(3000)
|
||||||
|
machine.wait_for_open_port(3001)
|
||||||
|
|
||||||
|
machine.succeed(
|
||||||
|
"su -l gitea -c 'GITEA_WORK_DIR=/var/lib/gitea gitea admin user create "
|
||||||
|
+ "--username root --password root --email test@localhost'"
|
||||||
|
)
|
||||||
|
machine.succeed("su -l postgres -c 'psql gitea < ${scripts.mktoken}'")
|
||||||
|
|
||||||
|
machine.succeed(
|
||||||
|
"curl --fail -X POST http://localhost:3001/api/v1/user/repos "
|
||||||
|
+ "-H 'Accept: application/json' -H 'Content-Type: application/json' "
|
||||||
|
+ f"-H 'Authorization: token ${api_token}'"
|
||||||
|
+ ' -d \'{"auto_init":false, "description":"string", "license":"mit", "name":"repo", "private":false}\'''
|
||||||
|
)
|
||||||
|
|
||||||
|
machine.succeed(
|
||||||
|
"curl --fail -X POST http://localhost:3001/api/v1/user/keys "
|
||||||
|
+ "-H 'Accept: application/json' -H 'Content-Type: application/json' "
|
||||||
|
+ f"-H 'Authorization: token ${api_token}'"
|
||||||
|
+ ' -d \'{"key":"${snakeoilKeypair.pubkey}","read_only":true,"title":"SSH"}\'''
|
||||||
|
)
|
||||||
|
|
||||||
|
machine.succeed(
|
||||||
|
"${scripts.git-setup}"
|
||||||
|
)
|
||||||
|
|
||||||
|
machine.succeed(
|
||||||
|
"${scripts.hydra-setup}"
|
||||||
|
)
|
||||||
|
|
||||||
|
machine.wait_until_succeeds(
|
||||||
|
'curl -Lf -s http://localhost:3000/build/1 -H "Accept: application/json" '
|
||||||
|
+ '| jq .buildstatus | xargs test 0 -eq'
|
||||||
|
)
|
||||||
|
|
||||||
|
data = machine.succeed(
|
||||||
|
'curl -Lf -s "http://localhost:3001/api/v1/repos/root/repo/statuses/$(cd /tmp/repo && git show | head -n1 | awk "{print \\$2}")" '
|
||||||
|
+ "-H 'Accept: application/json' -H 'Content-Type: application/json' "
|
||||||
|
+ f"-H 'Authorization: token ${api_token}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
response = json.loads(data)
|
||||||
|
|
||||||
|
assert len(response) == 2, "Expected exactly three status updates for latest commit (queued, finished)!"
|
||||||
|
assert response[0]['status'] == "success", "Expected finished status to be success!"
|
||||||
|
assert response[1]['status'] == "pending", "Expected queued status to be pending!"
|
||||||
|
|
||||||
|
machine.shutdown()
|
||||||
|
'';
|
||||||
|
});
|
||||||
|
|
||||||
|
validate-openapi = forEachSystem (system:
|
||||||
|
let pkgs = nixpkgs.legacyPackages.${system}; in
|
||||||
|
pkgs.runCommand "validate-openapi"
|
||||||
|
{ buildInputs = [ pkgs.openapi-generator-cli ]; }
|
||||||
|
''
|
||||||
|
openapi-generator-cli validate -i ${./hydra-api.yaml}
|
||||||
|
touch $out
|
||||||
|
'');
|
||||||
|
|
||||||
|
}
|
||||||
285
package.nix
Normal file
285
package.nix
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
{ stdenv
|
||||||
|
, lib
|
||||||
|
, fileset
|
||||||
|
|
||||||
|
, rawSrc
|
||||||
|
|
||||||
|
, buildEnv
|
||||||
|
|
||||||
|
, perlPackages
|
||||||
|
|
||||||
|
, nixComponents
|
||||||
|
, git
|
||||||
|
|
||||||
|
, makeWrapper
|
||||||
|
, meson
|
||||||
|
, ninja
|
||||||
|
, nukeReferences
|
||||||
|
, pkg-config
|
||||||
|
, mdbook
|
||||||
|
|
||||||
|
, unzip
|
||||||
|
, libpqxx
|
||||||
|
, top-git
|
||||||
|
, mercurial
|
||||||
|
, darcs
|
||||||
|
, subversion
|
||||||
|
, breezy
|
||||||
|
, openssl
|
||||||
|
, bzip2
|
||||||
|
, libxslt
|
||||||
|
, perl
|
||||||
|
, pixz
|
||||||
|
, boost
|
||||||
|
, postgresql_17
|
||||||
|
, nlohmann_json
|
||||||
|
, prometheus-cpp
|
||||||
|
|
||||||
|
, cacert
|
||||||
|
, foreman
|
||||||
|
, glibcLocales
|
||||||
|
, libressl
|
||||||
|
, openldap
|
||||||
|
, python3
|
||||||
|
|
||||||
|
, openssh
|
||||||
|
, coreutils
|
||||||
|
, findutils
|
||||||
|
, gzip
|
||||||
|
, xz
|
||||||
|
, gnutar
|
||||||
|
, gnused
|
||||||
|
, nix-eval-jobs
|
||||||
|
|
||||||
|
, rpm
|
||||||
|
, dpkg
|
||||||
|
, cdrkit
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
perlDeps = buildEnv {
|
||||||
|
name = "hydra-perl-deps";
|
||||||
|
paths = lib.closePropagation
|
||||||
|
([
|
||||||
|
nixComponents.nix-perl-bindings
|
||||||
|
git
|
||||||
|
] ++ (with perlPackages; [
|
||||||
|
AuthenSASL
|
||||||
|
CatalystActionREST
|
||||||
|
CatalystAuthenticationStoreDBIxClass
|
||||||
|
CatalystAuthenticationStoreLDAP
|
||||||
|
CatalystDevel
|
||||||
|
CatalystPluginAccessLog
|
||||||
|
CatalystPluginAuthorizationRoles
|
||||||
|
CatalystPluginCaptcha
|
||||||
|
CatalystPluginPrometheusTiny
|
||||||
|
CatalystPluginSessionStateCookie
|
||||||
|
CatalystPluginSessionStoreFastMmap
|
||||||
|
CatalystPluginStackTrace
|
||||||
|
CatalystTraitForRequestProxyBase
|
||||||
|
CatalystViewDownload
|
||||||
|
CatalystViewJSON
|
||||||
|
CatalystViewTT
|
||||||
|
CatalystXRoleApplicator
|
||||||
|
CatalystXScriptServerStarman
|
||||||
|
CryptPassphrase
|
||||||
|
CryptPassphraseArgon2
|
||||||
|
CryptRandPasswd
|
||||||
|
DataDump
|
||||||
|
DateTime
|
||||||
|
DBDPg
|
||||||
|
DBDSQLite
|
||||||
|
DBIxClassHelpers
|
||||||
|
DigestSHA1
|
||||||
|
EmailMIME
|
||||||
|
EmailSender
|
||||||
|
FileCopyRecursive
|
||||||
|
FileLibMagic
|
||||||
|
FileSlurper
|
||||||
|
FileWhich
|
||||||
|
IOCompress
|
||||||
|
IPCRun
|
||||||
|
IPCRun3
|
||||||
|
JSON
|
||||||
|
JSONMaybeXS
|
||||||
|
JSONXS
|
||||||
|
ListSomeUtils
|
||||||
|
LWP
|
||||||
|
LWPProtocolHttps
|
||||||
|
ModulePluggable
|
||||||
|
NetAmazonS3
|
||||||
|
NetPrometheus
|
||||||
|
NetStatsd
|
||||||
|
NumberBytesHuman
|
||||||
|
PadWalker
|
||||||
|
ParallelForkManager
|
||||||
|
PerlCriticCommunity
|
||||||
|
PrometheusTinyShared
|
||||||
|
ReadonlyX
|
||||||
|
SetScalar
|
||||||
|
SQLSplitStatement
|
||||||
|
Starman
|
||||||
|
StringCompareConstantTime
|
||||||
|
SysHostnameLong
|
||||||
|
TermSizeAny
|
||||||
|
TermReadKey
|
||||||
|
Test2Harness
|
||||||
|
TestPostgreSQL
|
||||||
|
TextDiff
|
||||||
|
TextTable
|
||||||
|
UUID4Tiny
|
||||||
|
YAML
|
||||||
|
XMLSimple
|
||||||
|
]));
|
||||||
|
};
|
||||||
|
|
||||||
|
version = "${builtins.readFile ./version.txt}.${builtins.substring 0 8 (rawSrc.lastModifiedDate or "19700101")}.${rawSrc.shortRev or "DIRTY"}";
|
||||||
|
in
|
||||||
|
stdenv.mkDerivation (finalAttrs: {
|
||||||
|
pname = "hydra";
|
||||||
|
inherit version;
|
||||||
|
|
||||||
|
src = fileset.toSource {
|
||||||
|
root = ./.;
|
||||||
|
fileset = fileset.unions ([
|
||||||
|
./doc
|
||||||
|
./meson.build
|
||||||
|
./nixos-modules
|
||||||
|
./src
|
||||||
|
./t
|
||||||
|
./version.txt
|
||||||
|
./.perlcriticrc
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = [ "out" "doc" ];
|
||||||
|
|
||||||
|
strictDeps = true;
|
||||||
|
|
||||||
|
nativeBuildInputs = [
|
||||||
|
makeWrapper
|
||||||
|
meson
|
||||||
|
ninja
|
||||||
|
nukeReferences
|
||||||
|
pkg-config
|
||||||
|
mdbook
|
||||||
|
nixComponents.nix-cli
|
||||||
|
perlDeps
|
||||||
|
perl
|
||||||
|
unzip
|
||||||
|
];
|
||||||
|
|
||||||
|
buildInputs = [
|
||||||
|
libpqxx
|
||||||
|
openssl
|
||||||
|
libxslt
|
||||||
|
nixComponents.nix-util
|
||||||
|
nixComponents.nix-store
|
||||||
|
nixComponents.nix-main
|
||||||
|
perlDeps
|
||||||
|
perl
|
||||||
|
boost
|
||||||
|
nlohmann_json
|
||||||
|
prometheus-cpp
|
||||||
|
];
|
||||||
|
|
||||||
|
nativeCheckInputs = [
|
||||||
|
bzip2
|
||||||
|
darcs
|
||||||
|
foreman
|
||||||
|
top-git
|
||||||
|
mercurial
|
||||||
|
subversion
|
||||||
|
breezy
|
||||||
|
openldap
|
||||||
|
postgresql_17
|
||||||
|
pixz
|
||||||
|
nix-eval-jobs
|
||||||
|
];
|
||||||
|
|
||||||
|
checkInputs = [
|
||||||
|
cacert
|
||||||
|
glibcLocales
|
||||||
|
libressl.nc
|
||||||
|
python3
|
||||||
|
nixComponents.nix-cli
|
||||||
|
];
|
||||||
|
|
||||||
|
hydraPath = lib.makeBinPath (
|
||||||
|
[
|
||||||
|
subversion
|
||||||
|
openssh
|
||||||
|
nixComponents.nix-cli
|
||||||
|
coreutils
|
||||||
|
findutils
|
||||||
|
pixz
|
||||||
|
gzip
|
||||||
|
bzip2
|
||||||
|
xz
|
||||||
|
gnutar
|
||||||
|
unzip
|
||||||
|
git
|
||||||
|
top-git
|
||||||
|
mercurial
|
||||||
|
darcs
|
||||||
|
gnused
|
||||||
|
breezy
|
||||||
|
nix-eval-jobs
|
||||||
|
] ++ lib.optionals stdenv.isLinux [ rpm dpkg cdrkit ]
|
||||||
|
);
|
||||||
|
|
||||||
|
OPENLDAP_ROOT = openldap;
|
||||||
|
|
||||||
|
mesonBuildType = "release";
|
||||||
|
|
||||||
|
postPatch = ''
|
||||||
|
patchShebangs .
|
||||||
|
'';
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
pushd $(git rev-parse --show-toplevel) >/dev/null
|
||||||
|
|
||||||
|
PATH=$(pwd)/build/src/hydra-evaluator:$(pwd)/src/script:$(pwd)/build/src/hydra-queue-runner:$PATH
|
||||||
|
PERL5LIB=$(pwd)/src/lib:$PERL5LIB
|
||||||
|
export HYDRA_HOME="$(pwd)/src/"
|
||||||
|
mkdir -p .hydra-data
|
||||||
|
export HYDRA_DATA="$(pwd)/.hydra-data"
|
||||||
|
export HYDRA_DBI='dbi:Pg:dbname=hydra;host=localhost;port=64444'
|
||||||
|
|
||||||
|
popd >/dev/null
|
||||||
|
'';
|
||||||
|
|
||||||
|
doCheck = true;
|
||||||
|
|
||||||
|
mesonCheckFlags = [ "--verbose" ];
|
||||||
|
|
||||||
|
preCheck = ''
|
||||||
|
export LOGNAME=''${LOGNAME:-foo}
|
||||||
|
# set $HOME for bzr so it can create its trace file
|
||||||
|
export HOME=$(mktemp -d)
|
||||||
|
'';
|
||||||
|
|
||||||
|
postInstall = ''
|
||||||
|
mkdir -p $out/nix-support
|
||||||
|
|
||||||
|
for i in $out/bin/*; do
|
||||||
|
read -n 4 chars < $i
|
||||||
|
if [[ $chars =~ ELF ]]; then continue; fi
|
||||||
|
wrapProgram $i \
|
||||||
|
--prefix PERL5LIB ':' $out/libexec/hydra/lib:$PERL5LIB \
|
||||||
|
--prefix PATH ':' $out/bin:$hydraPath \
|
||||||
|
--set HYDRA_RELEASE ${version} \
|
||||||
|
--set HYDRA_HOME $out/libexec/hydra \
|
||||||
|
--set NIX_RELEASE ${nixComponents.nix-cli.name or "unknown"} \
|
||||||
|
--set NIX_EVAL_JOBS_RELEASE ${nix-eval-jobs.name or "unknown"}
|
||||||
|
done
|
||||||
|
'';
|
||||||
|
|
||||||
|
dontStrip = true;
|
||||||
|
|
||||||
|
meta.description = "Build of Hydra on ${stdenv.system}";
|
||||||
|
passthru = {
|
||||||
|
inherit perlDeps;
|
||||||
|
nix = nixComponents.nix-cli;
|
||||||
|
};
|
||||||
|
})
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
SUBDIRS = hydra-evaluator hydra-eval-jobs hydra-queue-runner sql script lib root ttf
|
|
||||||
BOOTCLEAN_SUBDIRS = $(SUBDIRS)
|
|
||||||
DIST_SUBDIRS = $(SUBDIRS)
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
bin_PROGRAMS = hydra-eval-jobs
|
|
||||||
|
|
||||||
hydra_eval_jobs_SOURCES = hydra-eval-jobs.cc
|
|
||||||
hydra_eval_jobs_LDADD = $(NIX_LIBS) -lnixcmd
|
|
||||||
hydra_eval_jobs_CXXFLAGS = $(NIX_CFLAGS) -I ../libhydra
|
|
||||||
@@ -1,567 +0,0 @@
|
|||||||
#include <iostream>
|
|
||||||
#include <thread>
|
|
||||||
#include <optional>
|
|
||||||
#include <unordered_map>
|
|
||||||
|
|
||||||
#include "shared.hh"
|
|
||||||
#include "store-api.hh"
|
|
||||||
#include "eval.hh"
|
|
||||||
#include "eval-inline.hh"
|
|
||||||
#include "util.hh"
|
|
||||||
#include "get-drvs.hh"
|
|
||||||
#include "globals.hh"
|
|
||||||
#include "common-eval-args.hh"
|
|
||||||
#include "flake/flakeref.hh"
|
|
||||||
#include "flake/flake.hh"
|
|
||||||
#include "attr-path.hh"
|
|
||||||
#include "derivations.hh"
|
|
||||||
#include "local-fs-store.hh"
|
|
||||||
|
|
||||||
#include "hydra-config.hh"
|
|
||||||
|
|
||||||
#include <sys/types.h>
|
|
||||||
#include <sys/wait.h>
|
|
||||||
#include <sys/resource.h>
|
|
||||||
|
|
||||||
#include <nlohmann/json.hpp>
|
|
||||||
|
|
||||||
void check_pid_status_nonblocking(pid_t check_pid)
|
|
||||||
{
|
|
||||||
// Only check 'initialized' and known PID's
|
|
||||||
if (check_pid <= 0) { return; }
|
|
||||||
|
|
||||||
int wstatus = 0;
|
|
||||||
pid_t pid = waitpid(check_pid, &wstatus, WNOHANG);
|
|
||||||
// -1 = failure, WNOHANG: 0 = no change
|
|
||||||
if (pid <= 0) { return; }
|
|
||||||
|
|
||||||
std::cerr << "child process (" << pid << ") ";
|
|
||||||
|
|
||||||
if (WIFEXITED(wstatus)) {
|
|
||||||
std::cerr << "exited with status=" << WEXITSTATUS(wstatus) << std::endl;
|
|
||||||
} else if (WIFSIGNALED(wstatus)) {
|
|
||||||
std::cerr << "killed by signal=" << WTERMSIG(wstatus) << std::endl;
|
|
||||||
} else if (WIFSTOPPED(wstatus)) {
|
|
||||||
std::cerr << "stopped by signal=" << WSTOPSIG(wstatus) << std::endl;
|
|
||||||
} else if (WIFCONTINUED(wstatus)) {
|
|
||||||
std::cerr << "continued" << std::endl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
using namespace nix;
|
|
||||||
|
|
||||||
static Path gcRootsDir;
|
|
||||||
static size_t maxMemorySize;
|
|
||||||
|
|
||||||
struct MyArgs : MixEvalArgs, MixCommonArgs
|
|
||||||
{
|
|
||||||
Path releaseExpr;
|
|
||||||
bool flake = false;
|
|
||||||
bool dryRun = false;
|
|
||||||
|
|
||||||
MyArgs() : MixCommonArgs("hydra-eval-jobs")
|
|
||||||
{
|
|
||||||
addFlag({
|
|
||||||
.longName = "gc-roots-dir",
|
|
||||||
.description = "garbage collector roots directory",
|
|
||||||
.labels = {"path"},
|
|
||||||
.handler = {&gcRootsDir}
|
|
||||||
});
|
|
||||||
|
|
||||||
addFlag({
|
|
||||||
.longName = "dry-run",
|
|
||||||
.description = "don't create store derivations",
|
|
||||||
.handler = {&dryRun, true}
|
|
||||||
});
|
|
||||||
|
|
||||||
addFlag({
|
|
||||||
.longName = "flake",
|
|
||||||
.description = "build a flake",
|
|
||||||
.handler = {&flake, true}
|
|
||||||
});
|
|
||||||
|
|
||||||
expectArg("expr", &releaseExpr);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
static MyArgs myArgs;
|
|
||||||
|
|
||||||
static std::string queryMetaStrings(EvalState & state, DrvInfo & drv, const std::string & name, const std::string & subAttribute)
|
|
||||||
{
|
|
||||||
Strings res;
|
|
||||||
std::function<void(Value & v)> rec;
|
|
||||||
|
|
||||||
rec = [&](Value & v) {
|
|
||||||
state.forceValue(v, noPos);
|
|
||||||
if (v.type() == nString)
|
|
||||||
res.push_back(v.string.s);
|
|
||||||
else if (v.isList())
|
|
||||||
for (unsigned int n = 0; n < v.listSize(); ++n)
|
|
||||||
rec(*v.listElems()[n]);
|
|
||||||
else if (v.type() == nAttrs) {
|
|
||||||
auto a = v.attrs->find(state.symbols.create(subAttribute));
|
|
||||||
if (a != v.attrs->end())
|
|
||||||
res.push_back(std::string(state.forceString(*a->value, a->pos, "while evaluating meta attributes")));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Value * v = drv.queryMeta(name);
|
|
||||||
if (v) rec(*v);
|
|
||||||
|
|
||||||
return concatStringsSep(", ", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void worker(
|
|
||||||
EvalState & state,
|
|
||||||
Bindings & autoArgs,
|
|
||||||
AutoCloseFD & to,
|
|
||||||
AutoCloseFD & from)
|
|
||||||
{
|
|
||||||
Value vTop;
|
|
||||||
|
|
||||||
if (myArgs.flake) {
|
|
||||||
using namespace flake;
|
|
||||||
|
|
||||||
auto flakeRef = parseFlakeRef(myArgs.releaseExpr);
|
|
||||||
|
|
||||||
auto vFlake = state.allocValue();
|
|
||||||
|
|
||||||
auto lockedFlake = lockFlake(state, flakeRef,
|
|
||||||
LockFlags {
|
|
||||||
.updateLockFile = false,
|
|
||||||
.useRegistries = false,
|
|
||||||
.allowUnlocked = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
callFlake(state, lockedFlake, *vFlake);
|
|
||||||
|
|
||||||
auto vOutputs = vFlake->attrs->get(state.symbols.create("outputs"))->value;
|
|
||||||
state.forceValue(*vOutputs, noPos);
|
|
||||||
|
|
||||||
auto aHydraJobs = vOutputs->attrs->get(state.symbols.create("hydraJobs"));
|
|
||||||
if (!aHydraJobs)
|
|
||||||
aHydraJobs = vOutputs->attrs->get(state.symbols.create("checks"));
|
|
||||||
if (!aHydraJobs)
|
|
||||||
throw Error("flake '%s' does not provide any Hydra jobs or checks", flakeRef);
|
|
||||||
|
|
||||||
vTop = *aHydraJobs->value;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
state.evalFile(lookupFileArg(state, myArgs.releaseExpr), vTop);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto vRoot = state.allocValue();
|
|
||||||
state.autoCallFunction(autoArgs, vTop, *vRoot);
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
/* Wait for the master to send us a job name. */
|
|
||||||
writeLine(to.get(), "next");
|
|
||||||
|
|
||||||
auto s = readLine(from.get());
|
|
||||||
if (s == "exit") break;
|
|
||||||
if (!hasPrefix(s, "do ")) abort();
|
|
||||||
std::string attrPath(s, 3);
|
|
||||||
|
|
||||||
debug("worker process %d at '%s'", getpid(), attrPath);
|
|
||||||
|
|
||||||
/* Evaluate it and send info back to the master. */
|
|
||||||
nlohmann::json reply;
|
|
||||||
|
|
||||||
try {
|
|
||||||
auto vTmp = findAlongAttrPath(state, attrPath, autoArgs, *vRoot).first;
|
|
||||||
|
|
||||||
auto v = state.allocValue();
|
|
||||||
state.autoCallFunction(autoArgs, *vTmp, *v);
|
|
||||||
|
|
||||||
if (auto drv = getDerivation(state, *v, false)) {
|
|
||||||
|
|
||||||
DrvInfo::Outputs outputs = drv->queryOutputs();
|
|
||||||
|
|
||||||
if (drv->querySystem() == "unknown")
|
|
||||||
throw EvalError("derivation must have a 'system' attribute");
|
|
||||||
|
|
||||||
auto drvPath = state.store->printStorePath(drv->requireDrvPath());
|
|
||||||
|
|
||||||
nlohmann::json job;
|
|
||||||
|
|
||||||
job["nixName"] = drv->queryName();
|
|
||||||
job["system"] =drv->querySystem();
|
|
||||||
job["drvPath"] = drvPath;
|
|
||||||
job["description"] = drv->queryMetaString("description");
|
|
||||||
job["license"] = queryMetaStrings(state, *drv, "license", "shortName");
|
|
||||||
job["homepage"] = drv->queryMetaString("homepage");
|
|
||||||
job["maintainers"] = queryMetaStrings(state, *drv, "maintainers", "email");
|
|
||||||
job["schedulingPriority"] = drv->queryMetaInt("schedulingPriority", 100);
|
|
||||||
job["timeout"] = drv->queryMetaInt("timeout", 36000);
|
|
||||||
job["maxSilent"] = drv->queryMetaInt("maxSilent", 7200);
|
|
||||||
job["isChannel"] = drv->queryMetaBool("isHydraChannel", false);
|
|
||||||
|
|
||||||
/* If this is an aggregate, then get its constituents. */
|
|
||||||
auto a = v->attrs->get(state.symbols.create("_hydraAggregate"));
|
|
||||||
if (a && state.forceBool(*a->value, a->pos, "while evaluating the `_hydraAggregate` attribute")) {
|
|
||||||
auto a = v->attrs->get(state.symbols.create("constituents"));
|
|
||||||
if (!a)
|
|
||||||
throw EvalError("derivation must have a ‘constituents’ attribute");
|
|
||||||
|
|
||||||
NixStringContext context;
|
|
||||||
state.coerceToString(a->pos, *a->value, context, "while evaluating the `constituents` attribute", true, false);
|
|
||||||
for (auto & c : context)
|
|
||||||
std::visit(overloaded {
|
|
||||||
[&](const NixStringContextElem::Built & b) {
|
|
||||||
job["constituents"].push_back(state.store->printStorePath(b.drvPath));
|
|
||||||
},
|
|
||||||
[&](const NixStringContextElem::Opaque & o) {
|
|
||||||
},
|
|
||||||
[&](const NixStringContextElem::DrvDeep & d) {
|
|
||||||
},
|
|
||||||
}, c.raw());
|
|
||||||
|
|
||||||
state.forceList(*a->value, a->pos, "while evaluating the `constituents` attribute");
|
|
||||||
for (unsigned int n = 0; n < a->value->listSize(); ++n) {
|
|
||||||
auto v = a->value->listElems()[n];
|
|
||||||
state.forceValue(*v, noPos);
|
|
||||||
if (v->type() == nString)
|
|
||||||
job["namedConstituents"].push_back(v->str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Register the derivation as a GC root. !!! This
|
|
||||||
registers roots for jobs that we may have already
|
|
||||||
done. */
|
|
||||||
auto localStore = state.store.dynamic_pointer_cast<LocalFSStore>();
|
|
||||||
if (gcRootsDir != "" && localStore) {
|
|
||||||
Path root = gcRootsDir + "/" + std::string(baseNameOf(drvPath));
|
|
||||||
if (!pathExists(root))
|
|
||||||
localStore->addPermRoot(localStore->parseStorePath(drvPath), root);
|
|
||||||
}
|
|
||||||
|
|
||||||
nlohmann::json out;
|
|
||||||
for (auto & j : outputs)
|
|
||||||
// FIXME: handle CA/impure builds.
|
|
||||||
if (j.second)
|
|
||||||
out[j.first] = state.store->printStorePath(*j.second);
|
|
||||||
job["outputs"] = std::move(out);
|
|
||||||
|
|
||||||
reply["job"] = std::move(job);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (v->type() == nAttrs) {
|
|
||||||
auto attrs = nlohmann::json::array();
|
|
||||||
StringSet ss;
|
|
||||||
for (auto & i : v->attrs->lexicographicOrder(state.symbols)) {
|
|
||||||
std::string name(state.symbols[i->name]);
|
|
||||||
if (name.find(' ') != std::string::npos) {
|
|
||||||
printError("skipping job with illegal name '%s'", name);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
attrs.push_back(name);
|
|
||||||
}
|
|
||||||
reply["attrs"] = std::move(attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (v->type() == nNull)
|
|
||||||
;
|
|
||||||
|
|
||||||
else throw TypeError("attribute '%s' is %s, which is not supported", attrPath, showType(*v));
|
|
||||||
|
|
||||||
} catch (EvalError & e) {
|
|
||||||
auto msg = e.msg();
|
|
||||||
// Transmits the error we got from the previous evaluation
|
|
||||||
// in the JSON output.
|
|
||||||
reply["error"] = filterANSIEscapes(msg, true);
|
|
||||||
// Don't forget to print it into the STDERR log, this is
|
|
||||||
// what's shown in the Hydra UI.
|
|
||||||
printError(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
writeLine(to.get(), reply.dump());
|
|
||||||
|
|
||||||
/* If our RSS exceeds the maximum, exit. The master will
|
|
||||||
start a new process. */
|
|
||||||
struct rusage r;
|
|
||||||
getrusage(RUSAGE_SELF, &r);
|
|
||||||
if ((size_t) r.ru_maxrss > maxMemorySize * 1024) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
writeLine(to.get(), "restart");
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char * * argv)
|
|
||||||
{
|
|
||||||
/* Prevent undeclared dependencies in the evaluation via
|
|
||||||
$NIX_PATH. */
|
|
||||||
unsetenv("NIX_PATH");
|
|
||||||
|
|
||||||
return handleExceptions(argv[0], [&]() {
|
|
||||||
|
|
||||||
auto config = std::make_unique<HydraConfig>();
|
|
||||||
|
|
||||||
auto nrWorkers = config->getIntOption("evaluator_workers", 1);
|
|
||||||
maxMemorySize = config->getIntOption("evaluator_max_memory_size", 4096);
|
|
||||||
|
|
||||||
initNix();
|
|
||||||
initGC();
|
|
||||||
|
|
||||||
myArgs.parseCmdline(argvToStrings(argc, argv));
|
|
||||||
|
|
||||||
auto pureEval = config->getBoolOption("evaluator_pure_eval", myArgs.flake);
|
|
||||||
|
|
||||||
/* FIXME: The build hook in conjunction with import-from-derivation is causing "unexpected EOF" during eval */
|
|
||||||
settings.builders = "";
|
|
||||||
|
|
||||||
/* Prevent access to paths outside of the Nix search path and
|
|
||||||
to the environment. */
|
|
||||||
evalSettings.restrictEval = true;
|
|
||||||
|
|
||||||
/* When building a flake, use pure evaluation (no access to
|
|
||||||
'getEnv', 'currentSystem' etc. */
|
|
||||||
evalSettings.pureEval = pureEval;
|
|
||||||
|
|
||||||
if (myArgs.dryRun) settings.readOnlyMode = true;
|
|
||||||
|
|
||||||
if (myArgs.releaseExpr == "") throw UsageError("no expression specified");
|
|
||||||
|
|
||||||
if (gcRootsDir == "") printMsg(lvlError, "warning: `--gc-roots-dir' not specified");
|
|
||||||
|
|
||||||
struct State
|
|
||||||
{
|
|
||||||
std::set<std::string> todo{""};
|
|
||||||
std::set<std::string> active;
|
|
||||||
nlohmann::json jobs;
|
|
||||||
std::exception_ptr exc;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::condition_variable wakeup;
|
|
||||||
|
|
||||||
Sync<State> state_;
|
|
||||||
|
|
||||||
/* Start a handler thread per worker process. */
|
|
||||||
auto handler = [&]()
|
|
||||||
{
|
|
||||||
pid_t pid = -1;
|
|
||||||
try {
|
|
||||||
AutoCloseFD from, to;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
|
|
||||||
/* Start a new worker process if necessary. */
|
|
||||||
if (pid == -1) {
|
|
||||||
Pipe toPipe, fromPipe;
|
|
||||||
toPipe.create();
|
|
||||||
fromPipe.create();
|
|
||||||
pid = startProcess(
|
|
||||||
[&,
|
|
||||||
to{std::make_shared<AutoCloseFD>(std::move(fromPipe.writeSide))},
|
|
||||||
from{std::make_shared<AutoCloseFD>(std::move(toPipe.readSide))}
|
|
||||||
]()
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
EvalState state(myArgs.searchPath, openStore());
|
|
||||||
Bindings & autoArgs = *myArgs.getAutoArgs(state);
|
|
||||||
worker(state, autoArgs, *to, *from);
|
|
||||||
} catch (Error & e) {
|
|
||||||
nlohmann::json err;
|
|
||||||
auto msg = e.msg();
|
|
||||||
err["error"] = filterANSIEscapes(msg, true);
|
|
||||||
printError(msg);
|
|
||||||
writeLine(to->get(), err.dump());
|
|
||||||
// Don't forget to print it into the STDERR log, this is
|
|
||||||
// what's shown in the Hydra UI.
|
|
||||||
writeLine(to->get(), "restart");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ProcessOptions { .allowVfork = false });
|
|
||||||
from = std::move(fromPipe.readSide);
|
|
||||||
to = std::move(toPipe.writeSide);
|
|
||||||
debug("created worker process %d", pid);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Check whether the existing worker process is still there. */
|
|
||||||
auto s = readLine(from.get());
|
|
||||||
if (s == "restart") {
|
|
||||||
pid = -1;
|
|
||||||
continue;
|
|
||||||
} else if (s != "next") {
|
|
||||||
auto json = nlohmann::json::parse(s);
|
|
||||||
throw Error("worker error: %s", (std::string) json["error"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Wait for a job name to become available. */
|
|
||||||
std::string attrPath;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
checkInterrupt();
|
|
||||||
auto state(state_.lock());
|
|
||||||
if ((state->todo.empty() && state->active.empty()) || state->exc) {
|
|
||||||
writeLine(to.get(), "exit");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!state->todo.empty()) {
|
|
||||||
attrPath = *state->todo.begin();
|
|
||||||
state->todo.erase(state->todo.begin());
|
|
||||||
state->active.insert(attrPath);
|
|
||||||
break;
|
|
||||||
} else
|
|
||||||
state.wait(wakeup);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tell the worker to evaluate it. */
|
|
||||||
writeLine(to.get(), "do " + attrPath);
|
|
||||||
|
|
||||||
/* Wait for the response. */
|
|
||||||
auto response = nlohmann::json::parse(readLine(from.get()));
|
|
||||||
|
|
||||||
/* Handle the response. */
|
|
||||||
StringSet newAttrs;
|
|
||||||
|
|
||||||
if (response.find("job") != response.end()) {
|
|
||||||
auto state(state_.lock());
|
|
||||||
state->jobs[attrPath] = response["job"];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.find("attrs") != response.end()) {
|
|
||||||
for (auto & i : response["attrs"]) {
|
|
||||||
std::string path = i;
|
|
||||||
if (path.find(".") != std::string::npos){
|
|
||||||
path = "\"" + path + "\"";
|
|
||||||
}
|
|
||||||
auto s = (attrPath.empty() ? "" : attrPath + ".") + (std::string) path;
|
|
||||||
newAttrs.insert(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.find("error") != response.end()) {
|
|
||||||
auto state(state_.lock());
|
|
||||||
state->jobs[attrPath]["error"] = response["error"];
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add newly discovered job names to the queue. */
|
|
||||||
{
|
|
||||||
auto state(state_.lock());
|
|
||||||
state->active.erase(attrPath);
|
|
||||||
for (auto & s : newAttrs)
|
|
||||||
state->todo.insert(s);
|
|
||||||
wakeup.notify_all();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (...) {
|
|
||||||
check_pid_status_nonblocking(pid);
|
|
||||||
auto state(state_.lock());
|
|
||||||
state->exc = std::current_exception();
|
|
||||||
wakeup.notify_all();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
std::vector<std::thread> threads;
|
|
||||||
for (size_t i = 0; i < nrWorkers; i++)
|
|
||||||
threads.emplace_back(std::thread(handler));
|
|
||||||
|
|
||||||
for (auto & thread : threads)
|
|
||||||
thread.join();
|
|
||||||
|
|
||||||
auto state(state_.lock());
|
|
||||||
|
|
||||||
if (state->exc)
|
|
||||||
std::rethrow_exception(state->exc);
|
|
||||||
|
|
||||||
/* For aggregate jobs that have named consistuents
|
|
||||||
(i.e. constituents that are a job name rather than a
|
|
||||||
derivation), look up the referenced job and add it to the
|
|
||||||
dependencies of the aggregate derivation. */
|
|
||||||
auto store = openStore();
|
|
||||||
|
|
||||||
for (auto i = state->jobs.begin(); i != state->jobs.end(); ++i) {
|
|
||||||
auto jobName = i.key();
|
|
||||||
auto & job = i.value();
|
|
||||||
|
|
||||||
auto named = job.find("namedConstituents");
|
|
||||||
if (named == job.end()) continue;
|
|
||||||
|
|
||||||
std::unordered_map<std::string, std::string> brokenJobs;
|
|
||||||
auto getNonBrokenJobOrRecordError = [&brokenJobs, &jobName, &state](
|
|
||||||
const std::string & childJobName) -> std::optional<nlohmann::json> {
|
|
||||||
auto childJob = state->jobs.find(childJobName);
|
|
||||||
if (childJob == state->jobs.end()) {
|
|
||||||
printError("aggregate job '%s' references non-existent job '%s'", jobName, childJobName);
|
|
||||||
brokenJobs[childJobName] = "does not exist";
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
if (childJob->find("error") != childJob->end()) {
|
|
||||||
std::string error = (*childJob)["error"];
|
|
||||||
printError("aggregate job '%s' references broken job '%s': %s", jobName, childJobName, error);
|
|
||||||
brokenJobs[childJobName] = error;
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
return *childJob;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (myArgs.dryRun) {
|
|
||||||
for (std::string jobName2 : *named) {
|
|
||||||
auto job2 = getNonBrokenJobOrRecordError(jobName2);
|
|
||||||
if (!job2) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
std::string drvPath2 = (*job2)["drvPath"];
|
|
||||||
job["constituents"].push_back(drvPath2);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
auto drvPath = store->parseStorePath((std::string) job["drvPath"]);
|
|
||||||
auto drv = store->readDerivation(drvPath);
|
|
||||||
|
|
||||||
for (std::string jobName2 : *named) {
|
|
||||||
auto job2 = getNonBrokenJobOrRecordError(jobName2);
|
|
||||||
if (!job2) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
auto drvPath2 = store->parseStorePath((std::string) (*job2)["drvPath"]);
|
|
||||||
auto drv2 = store->readDerivation(drvPath2);
|
|
||||||
job["constituents"].push_back(store->printStorePath(drvPath2));
|
|
||||||
drv.inputDrvs[drvPath2] = {drv2.outputs.begin()->first};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (brokenJobs.empty()) {
|
|
||||||
std::string drvName(drvPath.name());
|
|
||||||
assert(hasSuffix(drvName, drvExtension));
|
|
||||||
drvName.resize(drvName.size() - drvExtension.size());
|
|
||||||
|
|
||||||
auto hashModulo = hashDerivationModulo(*store, drv, true);
|
|
||||||
if (hashModulo.kind != DrvHash::Kind::Regular) continue;
|
|
||||||
auto h = hashModulo.hashes.find("out");
|
|
||||||
if (h == hashModulo.hashes.end()) continue;
|
|
||||||
auto outPath = store->makeOutputPath("out", h->second, drvName);
|
|
||||||
drv.env["out"] = store->printStorePath(outPath);
|
|
||||||
drv.outputs.insert_or_assign("out", DerivationOutput::InputAddressed { .path = outPath });
|
|
||||||
auto newDrvPath = store->printStorePath(writeDerivation(*store, drv));
|
|
||||||
|
|
||||||
debug("rewrote aggregate derivation %s -> %s", store->printStorePath(drvPath), newDrvPath);
|
|
||||||
|
|
||||||
job["drvPath"] = newDrvPath;
|
|
||||||
job["outputs"]["out"] = store->printStorePath(outPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
job.erase("namedConstituents");
|
|
||||||
|
|
||||||
/* Register the derivation as a GC root. !!! This
|
|
||||||
registers roots for jobs that we may have already
|
|
||||||
done. */
|
|
||||||
auto localStore = store.dynamic_pointer_cast<LocalFSStore>();
|
|
||||||
if (gcRootsDir != "" && localStore) {
|
|
||||||
auto drvPath = job["drvPath"].get<std::string>();
|
|
||||||
Path root = gcRootsDir + "/" + std::string(baseNameOf(drvPath));
|
|
||||||
if (!pathExists(root))
|
|
||||||
localStore->addPermRoot(localStore->parseStorePath(drvPath), root);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!brokenJobs.empty()) {
|
|
||||||
std::stringstream ss;
|
|
||||||
for (const auto& [jobName, error] : brokenJobs) {
|
|
||||||
ss << jobName << ": " << error << "\n";
|
|
||||||
}
|
|
||||||
job["error"] = ss.str();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::cout << state->jobs.dump(2) << "\n";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
bin_PROGRAMS = hydra-evaluator
|
|
||||||
|
|
||||||
hydra_evaluator_SOURCES = hydra-evaluator.cc
|
|
||||||
hydra_evaluator_LDADD = $(NIX_LIBS) -lpqxx
|
|
||||||
hydra_evaluator_CXXFLAGS = $(NIX_CFLAGS) -Wall -I ../libhydra -Wno-deprecated-declarations
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
#include "db.hh"
|
#include "db.hh"
|
||||||
#include "hydra-config.hh"
|
#include "hydra-config.hh"
|
||||||
#include "pool.hh"
|
#include <nix/util/pool.hh>
|
||||||
#include "shared.hh"
|
#include <nix/main/shared.hh>
|
||||||
|
#include <nix/util/signals.hh>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
@@ -37,7 +38,7 @@ class JobsetId {
|
|||||||
friend bool operator!= (const JobsetId & lhs, const JobsetName & rhs);
|
friend bool operator!= (const JobsetId & lhs, const JobsetName & rhs);
|
||||||
|
|
||||||
std::string display() const {
|
std::string display() const {
|
||||||
return str(format("%1%:%2% (jobset#%3%)") % project % jobset % id);
|
return boost::str(boost::format("%1%:%2% (jobset#%3%)") % project % jobset % id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
bool operator==(const JobsetId & lhs, const JobsetId & rhs)
|
bool operator==(const JobsetId & lhs, const JobsetId & rhs)
|
||||||
@@ -179,10 +180,8 @@ struct Evaluator
|
|||||||
{
|
{
|
||||||
auto conn(dbPool.get());
|
auto conn(dbPool.get());
|
||||||
pqxx::work txn(*conn);
|
pqxx::work txn(*conn);
|
||||||
txn.exec_params0
|
txn.exec("update Jobsets set startTime = $1 where id = $2",
|
||||||
("update Jobsets set startTime = $1 where id = $2",
|
pqxx::params{now, jobset.name.id}).no_rows();
|
||||||
now,
|
|
||||||
jobset.name.id);
|
|
||||||
txn.commit();
|
txn.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +232,7 @@ struct Evaluator
|
|||||||
pqxx::work txn(*conn);
|
pqxx::work txn(*conn);
|
||||||
|
|
||||||
if (jobset.evaluation_style == EvaluationStyle::ONE_AT_A_TIME) {
|
if (jobset.evaluation_style == EvaluationStyle::ONE_AT_A_TIME) {
|
||||||
auto evaluation_res = txn.exec_params
|
auto evaluation_res = txn.exec
|
||||||
("select id from JobsetEvals "
|
("select id from JobsetEvals "
|
||||||
"where jobset_id = $1 "
|
"where jobset_id = $1 "
|
||||||
"order by id desc limit 1"
|
"order by id desc limit 1"
|
||||||
@@ -249,7 +248,7 @@ struct Evaluator
|
|||||||
|
|
||||||
auto evaluation_id = evaluation_res[0][0].as<int>();
|
auto evaluation_id = evaluation_res[0][0].as<int>();
|
||||||
|
|
||||||
auto unfinished_build_res = txn.exec_params
|
auto unfinished_build_res = txn.exec
|
||||||
("select id from Builds "
|
("select id from Builds "
|
||||||
"join JobsetEvalMembers "
|
"join JobsetEvalMembers "
|
||||||
" on (JobsetEvalMembers.build = Builds.id) "
|
" on (JobsetEvalMembers.build = Builds.id) "
|
||||||
@@ -419,21 +418,18 @@ struct Evaluator
|
|||||||
/* Clear the trigger time to prevent this
|
/* Clear the trigger time to prevent this
|
||||||
jobset from getting stuck in an endless
|
jobset from getting stuck in an endless
|
||||||
failing eval loop. */
|
failing eval loop. */
|
||||||
txn.exec_params0
|
txn.exec
|
||||||
("update Jobsets set triggerTime = null where id = $1 and startTime is not null and triggerTime <= startTime",
|
("update Jobsets set triggerTime = null where id = $1 and startTime is not null and triggerTime <= startTime",
|
||||||
jobset.name.id);
|
jobset.name.id).no_rows();
|
||||||
|
|
||||||
/* Clear the start time. */
|
/* Clear the start time. */
|
||||||
txn.exec_params0
|
txn.exec
|
||||||
("update Jobsets set startTime = null where id = $1",
|
("update Jobsets set startTime = null where id = $1",
|
||||||
jobset.name.id);
|
jobset.name.id).no_rows();
|
||||||
|
|
||||||
if (!WIFEXITED(status) || WEXITSTATUS(status) > 1) {
|
if (!WIFEXITED(status) || WEXITSTATUS(status) > 1) {
|
||||||
txn.exec_params0
|
txn.exec("update Jobsets set errorMsg = $1, lastCheckedTime = $2, errorTime = $2, fetchErrorMsg = null where id = $3",
|
||||||
("update Jobsets set errorMsg = $1, lastCheckedTime = $2, errorTime = $2, fetchErrorMsg = null where id = $3",
|
pqxx::params{fmt("evaluation %s", statusToString(status)), now, jobset.name.id}).no_rows();
|
||||||
fmt("evaluation %s", statusToString(status)),
|
|
||||||
now,
|
|
||||||
jobset.name.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
txn.commit();
|
txn.commit();
|
||||||
@@ -458,7 +454,7 @@ struct Evaluator
|
|||||||
{
|
{
|
||||||
auto conn(dbPool.get());
|
auto conn(dbPool.get());
|
||||||
pqxx::work txn(*conn);
|
pqxx::work txn(*conn);
|
||||||
txn.exec("update Jobsets set startTime = null");
|
txn.exec("update Jobsets set startTime = null").no_rows();
|
||||||
txn.commit();
|
txn.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
src/hydra-evaluator/meson.build
Normal file
10
src/hydra-evaluator/meson.build
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
hydra_evaluator = executable('hydra-evaluator',
|
||||||
|
'hydra-evaluator.cc',
|
||||||
|
dependencies: [
|
||||||
|
libhydra_dep,
|
||||||
|
nix_util_dep,
|
||||||
|
nix_main_dep,
|
||||||
|
pqxx_dep,
|
||||||
|
],
|
||||||
|
install: true,
|
||||||
|
)
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
bin_PROGRAMS = hydra-queue-runner
|
|
||||||
|
|
||||||
hydra_queue_runner_SOURCES = hydra-queue-runner.cc queue-monitor.cc dispatcher.cc \
|
|
||||||
builder.cc build-result.cc build-remote.cc \
|
|
||||||
hydra-build-result.hh counter.hh state.hh db.hh \
|
|
||||||
nar-extractor.cc nar-extractor.hh
|
|
||||||
hydra_queue_runner_LDADD = $(NIX_LIBS) -lpqxx -lprometheus-cpp-pull -lprometheus-cpp-core
|
|
||||||
hydra_queue_runner_CXXFLAGS = $(NIX_CFLAGS) -Wall -I ../libhydra -Wno-deprecated-declarations
|
|
||||||
@@ -5,127 +5,91 @@
|
|||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
|
|
||||||
#include "build-result.hh"
|
#include <nix/store/build-result.hh>
|
||||||
#include "path.hh"
|
#include <nix/store/path.hh>
|
||||||
#include "serve-protocol.hh"
|
#include <nix/store/legacy-ssh-store.hh>
|
||||||
|
#include <nix/store/serve-protocol.hh>
|
||||||
|
#include <nix/store/serve-protocol-impl.hh>
|
||||||
#include "state.hh"
|
#include "state.hh"
|
||||||
#include "util.hh"
|
#include <nix/util/current-process.hh>
|
||||||
#include "worker-protocol.hh"
|
#include <nix/util/processes.hh>
|
||||||
#include "worker-protocol-impl.hh"
|
#include <nix/util/util.hh>
|
||||||
#include "finally.hh"
|
#include <nix/store/export-import.hh>
|
||||||
#include "url.hh"
|
#include <nix/store/serve-protocol.hh>
|
||||||
|
#include <nix/store/serve-protocol-impl.hh>
|
||||||
|
#include <nix/store/ssh.hh>
|
||||||
|
#include <nix/util/finally.hh>
|
||||||
|
#include <nix/util/url.hh>
|
||||||
|
|
||||||
using namespace nix;
|
using namespace nix;
|
||||||
|
|
||||||
|
bool ::Machine::isLocalhost() const
|
||||||
struct Child
|
|
||||||
{
|
{
|
||||||
Pid pid;
|
return storeUri.params.empty() && std::visit(overloaded {
|
||||||
AutoCloseFD to, from;
|
[](const StoreReference::Auto &) {
|
||||||
};
|
return true;
|
||||||
|
},
|
||||||
|
[](const StoreReference::Specified & s) {
|
||||||
static void append(Strings & dst, const Strings & src)
|
return
|
||||||
{
|
(s.scheme == "local" || s.scheme == "unix") ||
|
||||||
dst.insert(dst.end(), src.begin(), src.end());
|
((s.scheme == "ssh" || s.scheme == "ssh-ng") &&
|
||||||
|
s.authority == "localhost");
|
||||||
|
},
|
||||||
|
}, storeUri.variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Strings extraStoreArgs(std::string & machine)
|
namespace nix::build_remote {
|
||||||
|
|
||||||
|
static std::unique_ptr<SSHMaster::Connection> openConnection(
|
||||||
|
::Machine::ptr machine, SSHMaster & master)
|
||||||
{
|
{
|
||||||
Strings result;
|
Strings command = {"nix-store", "--serve", "--write"};
|
||||||
try {
|
|
||||||
auto parsed = parseURL(machine);
|
|
||||||
if (parsed.scheme != "ssh") {
|
|
||||||
throw SysError("Currently, only (legacy-)ssh stores are supported!");
|
|
||||||
}
|
|
||||||
machine = parsed.authority.value_or("");
|
|
||||||
auto remoteStore = parsed.query.find("remote-store");
|
|
||||||
if (remoteStore != parsed.query.end()) {
|
|
||||||
result = {"--store", shellEscape(remoteStore->second)};
|
|
||||||
}
|
|
||||||
} catch (BadURL &) {
|
|
||||||
// We just try to continue with `machine->sshName` here for backwards compat.
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void openConnection(Machine::ptr machine, Path tmpDir, int stderrFD, Child & child)
|
|
||||||
{
|
|
||||||
std::string pgmName;
|
|
||||||
Pipe to, from;
|
|
||||||
to.create();
|
|
||||||
from.create();
|
|
||||||
|
|
||||||
Strings argv;
|
|
||||||
if (machine->isLocalhost()) {
|
if (machine->isLocalhost()) {
|
||||||
pgmName = "nix-store";
|
command.push_back("--builders");
|
||||||
argv = {"nix-store", "--builders", "", "--serve", "--write"};
|
command.push_back("");
|
||||||
} else {
|
} else {
|
||||||
pgmName = "ssh";
|
auto remoteStore = machine->storeUri.params.find("remote-store");
|
||||||
auto sshName = machine->sshName;
|
if (remoteStore != machine->storeUri.params.end()) {
|
||||||
Strings extraArgs = extraStoreArgs(sshName);
|
command.push_back("--store");
|
||||||
argv = {"ssh", sshName};
|
command.push_back(escapeShellArgAlways(remoteStore->second));
|
||||||
if (machine->sshKey != "") append(argv, {"-i", machine->sshKey});
|
|
||||||
if (machine->sshPublicHostKey != "") {
|
|
||||||
Path fileName = tmpDir + "/host-key";
|
|
||||||
auto p = machine->sshName.find("@");
|
|
||||||
std::string host = p != std::string::npos ? std::string(machine->sshName, p + 1) : machine->sshName;
|
|
||||||
writeFile(fileName, host + " " + machine->sshPublicHostKey + "\n");
|
|
||||||
append(argv, {"-oUserKnownHostsFile=" + fileName});
|
|
||||||
}
|
}
|
||||||
append(argv,
|
|
||||||
{ "-x", "-a", "-oBatchMode=yes", "-oConnectTimeout=60", "-oTCPKeepAlive=yes"
|
|
||||||
, "--", "nix-store", "--serve", "--write" });
|
|
||||||
append(argv, extraArgs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
child.pid = startProcess([&]() {
|
auto ret = master.startCommand(std::move(command), {
|
||||||
restoreProcessContext();
|
"-a", "-oBatchMode=yes", "-oConnectTimeout=60", "-oTCPKeepAlive=yes"
|
||||||
|
|
||||||
if (dup2(to.readSide.get(), STDIN_FILENO) == -1)
|
|
||||||
throw SysError("cannot dup input pipe to stdin");
|
|
||||||
|
|
||||||
if (dup2(from.writeSide.get(), STDOUT_FILENO) == -1)
|
|
||||||
throw SysError("cannot dup output pipe to stdout");
|
|
||||||
|
|
||||||
if (dup2(stderrFD, STDERR_FILENO) == -1)
|
|
||||||
throw SysError("cannot dup stderr");
|
|
||||||
|
|
||||||
execvp(argv.front().c_str(), (char * *) stringsToCharPtrs(argv).data()); // FIXME: remove cast
|
|
||||||
|
|
||||||
throw SysError("cannot start %s", pgmName);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
to.readSide = -1;
|
// XXX: determine the actual max value we can use from /proc.
|
||||||
from.writeSide = -1;
|
|
||||||
|
|
||||||
child.to = to.writeSide.release();
|
// FIXME: Should this be upstreamed into `startCommand` in Nix?
|
||||||
child.from = from.readSide.release();
|
|
||||||
|
int pipesize = 1024 * 1024;
|
||||||
|
|
||||||
|
fcntl(ret->in.get(), F_SETPIPE_SZ, &pipesize);
|
||||||
|
fcntl(ret->out.get(), F_SETPIPE_SZ, &pipesize);
|
||||||
|
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void copyClosureTo(std::timed_mutex & sendMutex, Store & destStore,
|
static void copyClosureTo(
|
||||||
FdSource & from, FdSink & to, const StorePathSet & paths,
|
::Machine::Connection & conn,
|
||||||
bool useSubstitutes = false)
|
Store & destStore,
|
||||||
|
const StorePathSet & paths,
|
||||||
|
SubstituteFlag useSubstitutes = NoSubstitute)
|
||||||
{
|
{
|
||||||
StorePathSet closure;
|
StorePathSet closure;
|
||||||
destStore.computeFSClosure(paths, closure);
|
destStore.computeFSClosure(paths, closure);
|
||||||
|
|
||||||
WorkerProto::WriteConn wconn { .to = to };
|
|
||||||
WorkerProto::ReadConn rconn { .from = from };
|
|
||||||
/* Send the "query valid paths" command with the "lock" option
|
/* Send the "query valid paths" command with the "lock" option
|
||||||
enabled. This prevents a race where the remote host
|
enabled. This prevents a race where the remote host
|
||||||
garbage-collect paths that are already there. Optionally, ask
|
garbage-collect paths that are already there. Optionally, ask
|
||||||
the remote host to substitute missing paths. */
|
the remote host to substitute missing paths. */
|
||||||
// FIXME: substitute output pollutes our build log
|
// FIXME: substitute output pollutes our build log
|
||||||
to << ServeProto::Command::QueryValidPaths << 1 << useSubstitutes;
|
|
||||||
WorkerProto::write(destStore, wconn, closure);
|
|
||||||
to.flush();
|
|
||||||
|
|
||||||
/* Get back the set of paths that are already valid on the remote
|
/* Get back the set of paths that are already valid on the remote
|
||||||
host. */
|
host. */
|
||||||
auto present = WorkerProto::Serialise<StorePathSet>::read(destStore, rconn);
|
auto present = conn.queryValidPaths(
|
||||||
|
destStore, true, closure, useSubstitutes);
|
||||||
|
|
||||||
if (present.size() == closure.size()) return;
|
if (present.size() == closure.size()) return;
|
||||||
|
|
||||||
@@ -137,20 +101,20 @@ static void copyClosureTo(std::timed_mutex & sendMutex, Store & destStore,
|
|||||||
|
|
||||||
printMsg(lvlDebug, "sending %d missing paths", missing.size());
|
printMsg(lvlDebug, "sending %d missing paths", missing.size());
|
||||||
|
|
||||||
std::unique_lock<std::timed_mutex> sendLock(sendMutex,
|
std::unique_lock<std::timed_mutex> sendLock(conn.machine->state->sendLock,
|
||||||
std::chrono::seconds(600));
|
std::chrono::seconds(600));
|
||||||
|
|
||||||
to << ServeProto::Command::ImportPaths;
|
conn.to << ServeProto::Command::ImportPaths;
|
||||||
destStore.exportPaths(missing, to);
|
exportPaths(destStore, missing, conn.to);
|
||||||
to.flush();
|
conn.to.flush();
|
||||||
|
|
||||||
if (readInt(from) != 1)
|
if (readInt(conn.from) != 1)
|
||||||
throw Error("remote machine failed to import closure");
|
throw Error("remote machine failed to import closure");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// FIXME: use Store::topoSortPaths().
|
// FIXME: use Store::topoSortPaths().
|
||||||
StorePaths reverseTopoSortPaths(const std::map<StorePath, ValidPathInfo> & paths)
|
static StorePaths reverseTopoSortPaths(const std::map<StorePath, UnkeyedValidPathInfo> & paths)
|
||||||
{
|
{
|
||||||
StorePaths sorted;
|
StorePaths sorted;
|
||||||
StorePathSet visited;
|
StorePathSet visited;
|
||||||
@@ -178,40 +142,320 @@ StorePaths reverseTopoSortPaths(const std::map<StorePath, ValidPathInfo> & paths
|
|||||||
return sorted;
|
return sorted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static std::pair<Path, AutoCloseFD> openLogFile(const std::string & logDir, const StorePath & drvPath)
|
||||||
|
{
|
||||||
|
std::string base(drvPath.to_string());
|
||||||
|
auto logFile = logDir + "/" + std::string(base, 0, 2) + "/" + std::string(base, 2);
|
||||||
|
|
||||||
|
createDirs(dirOf(logFile));
|
||||||
|
|
||||||
|
AutoCloseFD logFD = open(logFile.c_str(), O_CREAT | O_TRUNC | O_WRONLY, 0666);
|
||||||
|
if (!logFD) throw SysError("creating log file ‘%s’", logFile);
|
||||||
|
|
||||||
|
return {std::move(logFile), std::move(logFD)};
|
||||||
|
}
|
||||||
|
|
||||||
|
static BasicDerivation sendInputs(
|
||||||
|
State & state,
|
||||||
|
Step & step,
|
||||||
|
Store & localStore,
|
||||||
|
Store & destStore,
|
||||||
|
::Machine::Connection & conn,
|
||||||
|
unsigned int & overhead,
|
||||||
|
counter & nrStepsWaiting,
|
||||||
|
counter & nrStepsCopyingTo
|
||||||
|
)
|
||||||
|
{
|
||||||
|
/* Replace the input derivations by their output paths to send a
|
||||||
|
minimal closure to the builder.
|
||||||
|
|
||||||
|
`tryResolve` currently does *not* rewrite input addresses, so it
|
||||||
|
is safe to do this in all cases. (It should probably have a mode
|
||||||
|
to do that, however, but we would not use it here.)
|
||||||
|
*/
|
||||||
|
BasicDerivation basicDrv = ({
|
||||||
|
auto maybeBasicDrv = step.drv->tryResolve(destStore, &localStore);
|
||||||
|
if (!maybeBasicDrv)
|
||||||
|
throw Error(
|
||||||
|
"the derivation '%s' can’t be resolved. It’s probably "
|
||||||
|
"missing some outputs",
|
||||||
|
localStore.printStorePath(step.drvPath));
|
||||||
|
*maybeBasicDrv;
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Ensure that the inputs exist in the destination store. This is
|
||||||
|
a no-op for regular stores, but for the binary cache store,
|
||||||
|
this will copy the inputs to the binary cache from the local
|
||||||
|
store. */
|
||||||
|
if (&localStore != &destStore) {
|
||||||
|
copyClosure(localStore, destStore,
|
||||||
|
step.drv->inputSrcs,
|
||||||
|
NoRepair, NoCheckSigs, NoSubstitute);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto mc1 = std::make_shared<MaintainCount<counter>>(nrStepsWaiting);
|
||||||
|
mc1.reset();
|
||||||
|
MaintainCount<counter> mc2(nrStepsCopyingTo);
|
||||||
|
|
||||||
|
printMsg(lvlDebug, "sending closure of ‘%s’ to ‘%s’",
|
||||||
|
localStore.printStorePath(step.drvPath), conn.machine->storeUri.render());
|
||||||
|
|
||||||
|
auto now1 = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
|
/* Copy the input closure. */
|
||||||
|
if (conn.machine->isLocalhost()) {
|
||||||
|
StorePathSet closure;
|
||||||
|
destStore.computeFSClosure(basicDrv.inputSrcs, closure);
|
||||||
|
copyPaths(destStore, localStore, closure, NoRepair, NoCheckSigs, NoSubstitute);
|
||||||
|
} else {
|
||||||
|
copyClosureTo(conn, destStore, basicDrv.inputSrcs, Substitute);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto now2 = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
|
overhead += std::chrono::duration_cast<std::chrono::milliseconds>(now2 - now1).count();
|
||||||
|
}
|
||||||
|
|
||||||
|
return basicDrv;
|
||||||
|
}
|
||||||
|
|
||||||
|
static BuildResult performBuild(
|
||||||
|
::Machine::Connection & conn,
|
||||||
|
Store & localStore,
|
||||||
|
StorePath drvPath,
|
||||||
|
const BasicDerivation & drv,
|
||||||
|
const ServeProto::BuildOptions & options,
|
||||||
|
counter & nrStepsBuilding
|
||||||
|
)
|
||||||
|
{
|
||||||
|
conn.putBuildDerivationRequest(localStore, drvPath, drv, options);
|
||||||
|
|
||||||
|
BuildResult result;
|
||||||
|
|
||||||
|
time_t startTime, stopTime;
|
||||||
|
|
||||||
|
startTime = time(0);
|
||||||
|
{
|
||||||
|
MaintainCount<counter> mc(nrStepsBuilding);
|
||||||
|
result = ServeProto::Serialise<BuildResult>::read(localStore, conn);
|
||||||
|
}
|
||||||
|
stopTime = time(0);
|
||||||
|
|
||||||
|
if (!result.startTime) {
|
||||||
|
// If the builder gave `startTime = 0`, use our measurements
|
||||||
|
// instead of the builder's.
|
||||||
|
//
|
||||||
|
// Note: this represents the duration of a single round, rather
|
||||||
|
// than all rounds.
|
||||||
|
result.startTime = startTime;
|
||||||
|
result.stopTime = stopTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the protocol was too old to give us `builtOutputs`, initialize
|
||||||
|
// it manually by introspecting the derivation.
|
||||||
|
if (GET_PROTOCOL_MINOR(conn.remoteVersion) < 6)
|
||||||
|
{
|
||||||
|
// If the remote is too old to handle CA derivations, we can’t get this
|
||||||
|
// far anyways
|
||||||
|
assert(drv.type().hasKnownOutputPaths());
|
||||||
|
DerivationOutputsAndOptPaths drvOutputs = drv.outputsAndOptPaths(localStore);
|
||||||
|
// Since this a `BasicDerivation`, `staticOutputHashes` will not
|
||||||
|
// do any real work.
|
||||||
|
auto outputHashes = staticOutputHashes(localStore, drv);
|
||||||
|
if (auto * successP = result.tryGetSuccess()) {
|
||||||
|
for (auto & [outputName, output] : drvOutputs) {
|
||||||
|
auto outputPath = output.second;
|
||||||
|
// We’ve just asserted that the output paths of the derivation
|
||||||
|
// were known
|
||||||
|
assert(outputPath);
|
||||||
|
auto outputHash = outputHashes.at(outputName);
|
||||||
|
auto drvOutput = DrvOutput { outputHash, outputName };
|
||||||
|
successP->builtOutputs.insert_or_assign(
|
||||||
|
std::move(outputName),
|
||||||
|
Realisation { {.outPath = *outputPath}, drvOutput });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void copyPathFromRemote(
|
||||||
|
::Machine::Connection & conn,
|
||||||
|
NarMemberDatas & narMembers,
|
||||||
|
Store & localStore,
|
||||||
|
Store & destStore,
|
||||||
|
const ValidPathInfo & info
|
||||||
|
)
|
||||||
|
{
|
||||||
|
/* Receive the NAR from the remote and add it to the
|
||||||
|
destination store. Meanwhile, extract all the info from the
|
||||||
|
NAR that getBuildOutput() needs. */
|
||||||
|
auto source2 = sinkToSource([&](Sink & sink)
|
||||||
|
{
|
||||||
|
/* Note: we should only send the command to dump the store
|
||||||
|
path to the remote if the NAR is actually going to get read
|
||||||
|
by the destination store, which won't happen if this path
|
||||||
|
is already valid on the destination store. Since this
|
||||||
|
lambda function only gets executed if someone tries to read
|
||||||
|
from source2, we will send the command from here rather
|
||||||
|
than outside the lambda. */
|
||||||
|
conn.to << ServeProto::Command::DumpStorePath << localStore.printStorePath(info.path);
|
||||||
|
conn.to.flush();
|
||||||
|
|
||||||
|
TeeSource tee(conn.from, sink);
|
||||||
|
extractNarData(tee, localStore.printStorePath(info.path), narMembers);
|
||||||
|
});
|
||||||
|
|
||||||
|
destStore.addToStore(info, *source2, NoRepair, NoCheckSigs);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void copyPathsFromRemote(
|
||||||
|
::Machine::Connection & conn,
|
||||||
|
NarMemberDatas & narMembers,
|
||||||
|
Store & localStore,
|
||||||
|
Store & destStore,
|
||||||
|
const std::map<StorePath, UnkeyedValidPathInfo> & infos
|
||||||
|
)
|
||||||
|
{
|
||||||
|
auto pathsSorted = reverseTopoSortPaths(infos);
|
||||||
|
|
||||||
|
for (auto & path : pathsSorted) {
|
||||||
|
auto & info = infos.find(path)->second;
|
||||||
|
copyPathFromRemote(
|
||||||
|
conn, narMembers, localStore, destStore,
|
||||||
|
ValidPathInfo { path, info });
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* using namespace nix::build_remote; */
|
||||||
|
|
||||||
|
void RemoteResult::updateWithBuildResult(const nix::BuildResult & buildResult)
|
||||||
|
{
|
||||||
|
startTime = buildResult.startTime;
|
||||||
|
stopTime = buildResult.stopTime;
|
||||||
|
timesBuilt = buildResult.timesBuilt;
|
||||||
|
|
||||||
|
std::visit(overloaded{
|
||||||
|
[&](const BuildResult::Success & success) {
|
||||||
|
stepStatus = bsSuccess;
|
||||||
|
switch (success.status) {
|
||||||
|
case BuildResult::Success::Built:
|
||||||
|
break;
|
||||||
|
case BuildResult::Success::Substituted:
|
||||||
|
case BuildResult::Success::AlreadyValid:
|
||||||
|
case BuildResult::Success::ResolvesToAlreadyValid:
|
||||||
|
isCached = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assert(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[&](const BuildResult::Failure & failure) {
|
||||||
|
errorMsg = failure.errorMsg;
|
||||||
|
isNonDeterministic = failure.isNonDeterministic;
|
||||||
|
switch (failure.status) {
|
||||||
|
case BuildResult::Failure::PermanentFailure:
|
||||||
|
stepStatus = bsFailed;
|
||||||
|
canCache = true;
|
||||||
|
errorMsg = "";
|
||||||
|
break;
|
||||||
|
case BuildResult::Failure::InputRejected:
|
||||||
|
case BuildResult::Failure::OutputRejected:
|
||||||
|
stepStatus = bsFailed;
|
||||||
|
canCache = true;
|
||||||
|
break;
|
||||||
|
case BuildResult::Failure::TransientFailure:
|
||||||
|
stepStatus = bsFailed;
|
||||||
|
canRetry = true;
|
||||||
|
errorMsg = "";
|
||||||
|
break;
|
||||||
|
case BuildResult::Failure::TimedOut:
|
||||||
|
stepStatus = bsTimedOut;
|
||||||
|
errorMsg = "";
|
||||||
|
break;
|
||||||
|
case BuildResult::Failure::MiscFailure:
|
||||||
|
stepStatus = bsAborted;
|
||||||
|
canRetry = true;
|
||||||
|
break;
|
||||||
|
case BuildResult::Failure::LogLimitExceeded:
|
||||||
|
stepStatus = bsLogLimitExceeded;
|
||||||
|
break;
|
||||||
|
case BuildResult::Failure::NotDeterministic:
|
||||||
|
stepStatus = bsNotDeterministic;
|
||||||
|
canRetry = false;
|
||||||
|
canCache = true;
|
||||||
|
break;
|
||||||
|
case BuildResult::Failure::CachedFailure:
|
||||||
|
case BuildResult::Failure::DependencyFailed:
|
||||||
|
case BuildResult::Failure::NoSubstituters:
|
||||||
|
case BuildResult::Failure::HashMismatch:
|
||||||
|
stepStatus = bsAborted;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assert(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, buildResult.inner);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility guard object to auto-release a semaphore on destruction. */
|
||||||
|
template <typename T>
|
||||||
|
class SemaphoreReleaser {
|
||||||
|
public:
|
||||||
|
SemaphoreReleaser(T* s) : sem(s) {}
|
||||||
|
~SemaphoreReleaser() { sem->release(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
T* sem;
|
||||||
|
};
|
||||||
|
|
||||||
void State::buildRemote(ref<Store> destStore,
|
void State::buildRemote(ref<Store> destStore,
|
||||||
Machine::ptr machine, Step::ptr step,
|
std::unique_ptr<MachineReservation> reservation,
|
||||||
unsigned int maxSilentTime, unsigned int buildTimeout, unsigned int repeats,
|
::Machine::ptr machine, Step::ptr step,
|
||||||
|
const ServeProto::BuildOptions & buildOptions,
|
||||||
RemoteResult & result, std::shared_ptr<ActiveStep> activeStep,
|
RemoteResult & result, std::shared_ptr<ActiveStep> activeStep,
|
||||||
std::function<void(StepState)> updateStep,
|
std::function<void(StepState)> updateStep,
|
||||||
NarMemberDatas & narMembers)
|
NarMemberDatas & narMembers)
|
||||||
{
|
{
|
||||||
assert(BuildResult::TimedOut == 8);
|
assert(BuildResult::Failure::TimedOut == 8);
|
||||||
|
|
||||||
std::string base(step->drvPath.to_string());
|
auto [logFile, logFD] = build_remote::openLogFile(logDir, step->drvPath);
|
||||||
result.logFile = logDir + "/" + std::string(base, 0, 2) + "/" + std::string(base, 2);
|
AutoDelete logFileDel(logFile, false);
|
||||||
AutoDelete autoDelete(result.logFile, false);
|
result.logFile = logFile;
|
||||||
|
|
||||||
createDirs(dirOf(result.logFile));
|
|
||||||
|
|
||||||
AutoCloseFD logFD = open(result.logFile.c_str(), O_CREAT | O_TRUNC | O_WRONLY, 0666);
|
|
||||||
if (!logFD) throw SysError("creating log file ‘%s’", result.logFile);
|
|
||||||
|
|
||||||
nix::Path tmpDir = createTempDir();
|
|
||||||
AutoDelete tmpDirDel(tmpDir, true);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
updateStep(ssConnecting);
|
updateStep(ssConnecting);
|
||||||
|
|
||||||
|
auto storeRef = machine->completeStoreReference();
|
||||||
|
|
||||||
|
auto * pSpecified = std::get_if<StoreReference::Specified>(&storeRef.variant);
|
||||||
|
if (!pSpecified || pSpecified->scheme != "ssh") {
|
||||||
|
throw Error("Currently, only (legacy-)ssh stores are supported!");
|
||||||
|
}
|
||||||
|
|
||||||
|
LegacySSHStoreConfig storeConfig {
|
||||||
|
pSpecified->scheme,
|
||||||
|
pSpecified->authority,
|
||||||
|
storeRef.params
|
||||||
|
};
|
||||||
|
|
||||||
|
auto master = storeConfig.createSSHMaster(
|
||||||
|
false, // no SSH master yet
|
||||||
|
logFD.get());
|
||||||
|
|
||||||
// FIXME: rewrite to use Store.
|
// FIXME: rewrite to use Store.
|
||||||
Child child;
|
auto child = build_remote::openConnection(machine, master);
|
||||||
openConnection(machine, tmpDir, logFD.get(), child);
|
|
||||||
|
|
||||||
{
|
{
|
||||||
auto activeStepState(activeStep->state_.lock());
|
auto activeStepState(activeStep->state_.lock());
|
||||||
if (activeStepState->cancelled) throw Error("step cancelled");
|
if (activeStepState->cancelled) throw Error("step cancelled");
|
||||||
activeStepState->pid = child.pid;
|
activeStepState->pid = child->sshPid;
|
||||||
}
|
}
|
||||||
|
|
||||||
Finally clearPid([&]() {
|
Finally clearPid([&]() {
|
||||||
@@ -226,36 +470,33 @@ void State::buildRemote(ref<Store> destStore,
|
|||||||
process. Meh. */
|
process. Meh. */
|
||||||
});
|
});
|
||||||
|
|
||||||
FdSource from(child.from.get());
|
::Machine::Connection conn {
|
||||||
WorkerProto::ReadConn rconn { .from = from };
|
{
|
||||||
FdSink to(child.to.get());
|
.to = child->in.get(),
|
||||||
WorkerProto::WriteConn wconn { .to = to };
|
.from = child->out.get(),
|
||||||
|
/* Handshake. */
|
||||||
|
.remoteVersion = 0xdadbeef, // FIXME avoid dummy initialize
|
||||||
|
},
|
||||||
|
/*.machine =*/ machine,
|
||||||
|
};
|
||||||
|
|
||||||
Finally updateStats([&]() {
|
Finally updateStats([&]() {
|
||||||
bytesReceived += from.read;
|
bytesReceived += conn.from.read;
|
||||||
bytesSent += to.written;
|
bytesSent += conn.to.written;
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Handshake. */
|
constexpr ServeProto::Version our_version = 0x206;
|
||||||
unsigned int remoteVersion;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
to << SERVE_MAGIC_1 << 0x206;
|
conn.remoteVersion = decltype(conn)::handshake(
|
||||||
to.flush();
|
conn.to,
|
||||||
|
conn.from,
|
||||||
unsigned int magic = readInt(from);
|
our_version,
|
||||||
if (magic != SERVE_MAGIC_2)
|
machine->storeUri.render());
|
||||||
throw Error("protocol mismatch with ‘nix-store --serve’ on ‘%1%’", machine->sshName);
|
|
||||||
remoteVersion = readInt(from);
|
|
||||||
if (GET_PROTOCOL_MAJOR(remoteVersion) != 0x200)
|
|
||||||
throw Error("unsupported ‘nix-store --serve’ protocol version on ‘%1%’", machine->sshName);
|
|
||||||
if (GET_PROTOCOL_MINOR(remoteVersion) < 3 && repeats > 0)
|
|
||||||
throw Error("machine ‘%1%’ does not support repeating a build; please upgrade it to Nix 1.12", machine->sshName);
|
|
||||||
|
|
||||||
} catch (EndOfFile & e) {
|
} catch (EndOfFile & e) {
|
||||||
child.pid.wait();
|
child->sshPid.wait();
|
||||||
std::string s = chomp(readFile(result.logFile));
|
std::string s = chomp(readFile(result.logFile));
|
||||||
throw Error("cannot connect to ‘%1%’: %2%", machine->sshName, s);
|
throw Error("cannot connect to ‘%1%’: %2%", machine->storeUri.render(), s);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -269,62 +510,12 @@ void State::buildRemote(ref<Store> destStore,
|
|||||||
copy the immediate sources of the derivation and the required
|
copy the immediate sources of the derivation and the required
|
||||||
outputs of the input derivations. */
|
outputs of the input derivations. */
|
||||||
updateStep(ssSendingInputs);
|
updateStep(ssSendingInputs);
|
||||||
|
BasicDerivation resolvedDrv = build_remote::sendInputs(*this, *step, *localStore, *destStore, conn, result.overhead, nrStepsWaiting, nrStepsCopyingTo);
|
||||||
|
|
||||||
StorePathSet inputs;
|
logFileDel.cancel();
|
||||||
BasicDerivation basicDrv(*step->drv);
|
|
||||||
|
|
||||||
for (auto & p : step->drv->inputSrcs)
|
|
||||||
inputs.insert(p);
|
|
||||||
|
|
||||||
for (auto & input : step->drv->inputDrvs) {
|
|
||||||
auto drv2 = localStore->readDerivation(input.first);
|
|
||||||
for (auto & name : input.second) {
|
|
||||||
if (auto i = get(drv2.outputs, name)) {
|
|
||||||
auto outPath = i->path(*localStore, drv2.name, name);
|
|
||||||
inputs.insert(*outPath);
|
|
||||||
basicDrv.inputSrcs.insert(*outPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure that the inputs exist in the destination store. This is
|
|
||||||
a no-op for regular stores, but for the binary cache store,
|
|
||||||
this will copy the inputs to the binary cache from the local
|
|
||||||
store. */
|
|
||||||
if (localStore != std::shared_ptr<Store>(destStore)) {
|
|
||||||
copyClosure(*localStore, *destStore,
|
|
||||||
step->drv->inputSrcs,
|
|
||||||
NoRepair, NoCheckSigs, NoSubstitute);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
auto mc1 = std::make_shared<MaintainCount<counter>>(nrStepsWaiting);
|
|
||||||
mc1.reset();
|
|
||||||
MaintainCount<counter> mc2(nrStepsCopyingTo);
|
|
||||||
|
|
||||||
printMsg(lvlDebug, "sending closure of ‘%s’ to ‘%s’",
|
|
||||||
localStore->printStorePath(step->drvPath), machine->sshName);
|
|
||||||
|
|
||||||
auto now1 = std::chrono::steady_clock::now();
|
|
||||||
|
|
||||||
/* Copy the input closure. */
|
|
||||||
if (machine->isLocalhost()) {
|
|
||||||
StorePathSet closure;
|
|
||||||
destStore->computeFSClosure(inputs, closure);
|
|
||||||
copyPaths(*destStore, *localStore, closure, NoRepair, NoCheckSigs, NoSubstitute);
|
|
||||||
} else {
|
|
||||||
copyClosureTo(machine->state->sendLock, *destStore, from, to, inputs, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto now2 = std::chrono::steady_clock::now();
|
|
||||||
|
|
||||||
result.overhead += std::chrono::duration_cast<std::chrono::milliseconds>(now2 - now1).count();
|
|
||||||
}
|
|
||||||
|
|
||||||
autoDelete.cancel();
|
|
||||||
|
|
||||||
/* Truncate the log to get rid of messages about substitutions
|
/* Truncate the log to get rid of messages about substitutions
|
||||||
etc. on the remote system. */
|
etc. on the remote system. */
|
||||||
if (lseek(logFD.get(), SEEK_SET, 0) != 0)
|
if (lseek(logFD.get(), SEEK_SET, 0) != 0)
|
||||||
throw SysError("seeking to the start of log file ‘%s’", result.logFile);
|
throw SysError("seeking to the start of log file ‘%s’", result.logFile);
|
||||||
|
|
||||||
@@ -336,89 +527,21 @@ void State::buildRemote(ref<Store> destStore,
|
|||||||
/* Do the build. */
|
/* Do the build. */
|
||||||
printMsg(lvlDebug, "building ‘%s’ on ‘%s’",
|
printMsg(lvlDebug, "building ‘%s’ on ‘%s’",
|
||||||
localStore->printStorePath(step->drvPath),
|
localStore->printStorePath(step->drvPath),
|
||||||
machine->sshName);
|
machine->storeUri.render());
|
||||||
|
|
||||||
updateStep(ssBuilding);
|
updateStep(ssBuilding);
|
||||||
|
|
||||||
to << ServeProto::Command::BuildDerivation << localStore->printStorePath(step->drvPath);
|
auto buildResult = build_remote::performBuild(
|
||||||
writeDerivation(to, *localStore, basicDrv);
|
conn,
|
||||||
to << maxSilentTime << buildTimeout;
|
*localStore,
|
||||||
if (GET_PROTOCOL_MINOR(remoteVersion) >= 2)
|
step->drvPath,
|
||||||
to << maxLogSize;
|
resolvedDrv,
|
||||||
if (GET_PROTOCOL_MINOR(remoteVersion) >= 3) {
|
buildOptions,
|
||||||
to << repeats // == build-repeat
|
nrStepsBuilding
|
||||||
<< step->isDeterministic; // == enforce-determinism
|
);
|
||||||
}
|
|
||||||
to.flush();
|
|
||||||
|
|
||||||
result.startTime = time(0);
|
result.updateWithBuildResult(buildResult);
|
||||||
int res;
|
|
||||||
{
|
|
||||||
MaintainCount<counter> mc(nrStepsBuilding);
|
|
||||||
res = readInt(from);
|
|
||||||
}
|
|
||||||
result.stopTime = time(0);
|
|
||||||
|
|
||||||
result.errorMsg = readString(from);
|
|
||||||
if (GET_PROTOCOL_MINOR(remoteVersion) >= 3) {
|
|
||||||
result.timesBuilt = readInt(from);
|
|
||||||
result.isNonDeterministic = readInt(from);
|
|
||||||
auto start = readInt(from);
|
|
||||||
auto stop = readInt(from);
|
|
||||||
if (start && start) {
|
|
||||||
/* Note: this represents the duration of a single
|
|
||||||
round, rather than all rounds. */
|
|
||||||
result.startTime = start;
|
|
||||||
result.stopTime = stop;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (GET_PROTOCOL_MINOR(remoteVersion) >= 6) {
|
|
||||||
WorkerProto::Serialise<DrvOutputs>::read(*localStore, rconn);
|
|
||||||
}
|
|
||||||
switch ((BuildResult::Status) res) {
|
|
||||||
case BuildResult::Built:
|
|
||||||
result.stepStatus = bsSuccess;
|
|
||||||
break;
|
|
||||||
case BuildResult::Substituted:
|
|
||||||
case BuildResult::AlreadyValid:
|
|
||||||
result.stepStatus = bsSuccess;
|
|
||||||
result.isCached = true;
|
|
||||||
break;
|
|
||||||
case BuildResult::PermanentFailure:
|
|
||||||
result.stepStatus = bsFailed;
|
|
||||||
result.canCache = true;
|
|
||||||
result.errorMsg = "";
|
|
||||||
break;
|
|
||||||
case BuildResult::InputRejected:
|
|
||||||
case BuildResult::OutputRejected:
|
|
||||||
result.stepStatus = bsFailed;
|
|
||||||
result.canCache = true;
|
|
||||||
break;
|
|
||||||
case BuildResult::TransientFailure:
|
|
||||||
result.stepStatus = bsFailed;
|
|
||||||
result.canRetry = true;
|
|
||||||
result.errorMsg = "";
|
|
||||||
break;
|
|
||||||
case BuildResult::TimedOut:
|
|
||||||
result.stepStatus = bsTimedOut;
|
|
||||||
result.errorMsg = "";
|
|
||||||
break;
|
|
||||||
case BuildResult::MiscFailure:
|
|
||||||
result.stepStatus = bsAborted;
|
|
||||||
result.canRetry = true;
|
|
||||||
break;
|
|
||||||
case BuildResult::LogLimitExceeded:
|
|
||||||
result.stepStatus = bsLogLimitExceeded;
|
|
||||||
break;
|
|
||||||
case BuildResult::NotDeterministic:
|
|
||||||
result.stepStatus = bsNotDeterministic;
|
|
||||||
result.canRetry = false;
|
|
||||||
result.canCache = true;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
result.stepStatus = bsAborted;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (result.stepStatus != bsSuccess) return;
|
if (result.stepStatus != bsSuccess) return;
|
||||||
|
|
||||||
result.errorMsg = "";
|
result.errorMsg = "";
|
||||||
@@ -427,11 +550,33 @@ void State::buildRemote(ref<Store> destStore,
|
|||||||
get a build log. */
|
get a build log. */
|
||||||
if (result.isCached) {
|
if (result.isCached) {
|
||||||
printMsg(lvlInfo, "outputs of ‘%s’ substituted or already valid on ‘%s’",
|
printMsg(lvlInfo, "outputs of ‘%s’ substituted or already valid on ‘%s’",
|
||||||
localStore->printStorePath(step->drvPath), machine->sshName);
|
localStore->printStorePath(step->drvPath), machine->storeUri.render());
|
||||||
unlink(result.logFile.c_str());
|
unlink(result.logFile.c_str());
|
||||||
result.logFile = "";
|
result.logFile = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Throttle CPU-bound work. Opportunistically skip updating the current
|
||||||
|
* step, since this requires a DB roundtrip. */
|
||||||
|
if (!localWorkThrottler.try_acquire()) {
|
||||||
|
MaintainCount<counter> mc(nrStepsWaitingForDownloadSlot);
|
||||||
|
updateStep(ssWaitingForLocalSlot);
|
||||||
|
localWorkThrottler.acquire();
|
||||||
|
}
|
||||||
|
SemaphoreReleaser releaser(&localWorkThrottler);
|
||||||
|
|
||||||
|
/* Once we've started copying outputs, release the machine reservation
|
||||||
|
* so further builds can happen. We do not release the machine earlier
|
||||||
|
* to avoid situations where the queue runner is bottlenecked on
|
||||||
|
* copying outputs and we end up building too many things that we
|
||||||
|
* haven't been able to allow copy slots for. */
|
||||||
|
reservation.reset();
|
||||||
|
wakeDispatcher();
|
||||||
|
|
||||||
|
StorePathSet outputs;
|
||||||
|
if (auto * successP = buildResult.tryGetSuccess())
|
||||||
|
for (auto & [_, realisation] : successP->builtOutputs)
|
||||||
|
outputs.insert(realisation.outPath);
|
||||||
|
|
||||||
/* Copy the output paths. */
|
/* Copy the output paths. */
|
||||||
if (!machine->isLocalhost() || localStore != std::shared_ptr<Store>(destStore)) {
|
if (!machine->isLocalhost() || localStore != std::shared_ptr<Store>(destStore)) {
|
||||||
updateStep(ssReceivingOutputs);
|
updateStep(ssReceivingOutputs);
|
||||||
@@ -440,39 +585,10 @@ void State::buildRemote(ref<Store> destStore,
|
|||||||
|
|
||||||
auto now1 = std::chrono::steady_clock::now();
|
auto now1 = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
StorePathSet outputs;
|
auto infos = conn.queryPathInfos(*localStore, outputs);
|
||||||
for (auto & i : step->drv->outputsAndOptPaths(*localStore)) {
|
|
||||||
if (i.second.second)
|
|
||||||
outputs.insert(*i.second.second);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Get info about each output path. */
|
|
||||||
std::map<StorePath, ValidPathInfo> infos;
|
|
||||||
size_t totalNarSize = 0;
|
size_t totalNarSize = 0;
|
||||||
to << ServeProto::Command::QueryPathInfos;
|
for (auto & [_, info] : infos) totalNarSize += info.narSize;
|
||||||
WorkerProto::write(*localStore, wconn, outputs);
|
|
||||||
to.flush();
|
|
||||||
while (true) {
|
|
||||||
auto storePathS = readString(from);
|
|
||||||
if (storePathS == "") break;
|
|
||||||
auto deriver = readString(from); // deriver
|
|
||||||
auto references = WorkerProto::Serialise<StorePathSet>::read(*localStore, rconn);
|
|
||||||
readLongLong(from); // download size
|
|
||||||
auto narSize = readLongLong(from);
|
|
||||||
auto narHash = Hash::parseAny(readString(from), htSHA256);
|
|
||||||
auto ca = ContentAddress::parseOpt(readString(from));
|
|
||||||
readStrings<StringSet>(from); // sigs
|
|
||||||
ValidPathInfo info(localStore->parseStorePath(storePathS), narHash);
|
|
||||||
assert(outputs.count(info.path));
|
|
||||||
info.references = references;
|
|
||||||
info.narSize = narSize;
|
|
||||||
totalNarSize += info.narSize;
|
|
||||||
info.narHash = narHash;
|
|
||||||
info.ca = ca;
|
|
||||||
if (deriver != "")
|
|
||||||
info.deriver = localStore->parseStorePath(deriver);
|
|
||||||
infos.insert_or_assign(info.path, info);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalNarSize > maxOutputSize) {
|
if (totalNarSize > maxOutputSize) {
|
||||||
result.stepStatus = bsNarSizeLimitExceeded;
|
result.stepStatus = bsNarSizeLimitExceeded;
|
||||||
@@ -481,43 +597,34 @@ void State::buildRemote(ref<Store> destStore,
|
|||||||
|
|
||||||
/* Copy each path. */
|
/* Copy each path. */
|
||||||
printMsg(lvlDebug, "copying outputs of ‘%s’ from ‘%s’ (%d bytes)",
|
printMsg(lvlDebug, "copying outputs of ‘%s’ from ‘%s’ (%d bytes)",
|
||||||
localStore->printStorePath(step->drvPath), machine->sshName, totalNarSize);
|
localStore->printStorePath(step->drvPath), machine->storeUri.render(), totalNarSize);
|
||||||
|
|
||||||
auto pathsSorted = reverseTopoSortPaths(infos);
|
|
||||||
|
|
||||||
for (auto & path : pathsSorted) {
|
|
||||||
auto & info = infos.find(path)->second;
|
|
||||||
|
|
||||||
/* Receive the NAR from the remote and add it to the
|
|
||||||
destination store. Meanwhile, extract all the info from the
|
|
||||||
NAR that getBuildOutput() needs. */
|
|
||||||
auto source2 = sinkToSource([&](Sink & sink)
|
|
||||||
{
|
|
||||||
/* Note: we should only send the command to dump the store
|
|
||||||
path to the remote if the NAR is actually going to get read
|
|
||||||
by the destination store, which won't happen if this path
|
|
||||||
is already valid on the destination store. Since this
|
|
||||||
lambda function only gets executed if someone tries to read
|
|
||||||
from source2, we will send the command from here rather
|
|
||||||
than outside the lambda. */
|
|
||||||
to << ServeProto::Command::DumpStorePath << localStore->printStorePath(path);
|
|
||||||
to.flush();
|
|
||||||
|
|
||||||
TeeSource tee(from, sink);
|
|
||||||
extractNarData(tee, localStore->printStorePath(path), narMembers);
|
|
||||||
});
|
|
||||||
|
|
||||||
destStore->addToStore(info, *source2, NoRepair, NoCheckSigs);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
build_remote::copyPathsFromRemote(conn, narMembers, *localStore, *destStore, infos);
|
||||||
auto now2 = std::chrono::steady_clock::now();
|
auto now2 = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
result.overhead += std::chrono::duration_cast<std::chrono::milliseconds>(now2 - now1).count();
|
result.overhead += std::chrono::duration_cast<std::chrono::milliseconds>(now2 - now1).count();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Register the outputs of the newly built drv */
|
||||||
|
if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations)) {
|
||||||
|
auto outputHashes = staticOutputHashes(*localStore, *step->drv);
|
||||||
|
if (auto * successP = buildResult.tryGetSuccess()) {
|
||||||
|
for (auto & [outputName, realisation] : successP->builtOutputs) {
|
||||||
|
// Register the resolved drv output
|
||||||
|
destStore->registerDrvOutput(realisation);
|
||||||
|
|
||||||
|
// Also register the unresolved one
|
||||||
|
auto unresolvedRealisation = realisation;
|
||||||
|
unresolvedRealisation.signatures.clear();
|
||||||
|
unresolvedRealisation.id.drvHash = outputHashes.at(outputName);
|
||||||
|
destStore->registerDrvOutput(unresolvedRealisation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Shut down the connection. */
|
/* Shut down the connection. */
|
||||||
child.to = -1;
|
child->in = -1;
|
||||||
child.pid.wait();
|
child->sshPid.wait();
|
||||||
|
|
||||||
} catch (Error & e) {
|
} catch (Error & e) {
|
||||||
/* Disable this machine until a certain period of time has
|
/* Disable this machine until a certain period of time has
|
||||||
@@ -531,7 +638,7 @@ void State::buildRemote(ref<Store> destStore,
|
|||||||
info->consecutiveFailures = std::min(info->consecutiveFailures + 1, (unsigned int) 4);
|
info->consecutiveFailures = std::min(info->consecutiveFailures + 1, (unsigned int) 4);
|
||||||
info->lastFailure = now;
|
info->lastFailure = now;
|
||||||
int delta = retryInterval * std::pow(retryBackoff, info->consecutiveFailures - 1) + (rand() % 30);
|
int delta = retryInterval * std::pow(retryBackoff, info->consecutiveFailures - 1) + (rand() % 30);
|
||||||
printMsg(lvlInfo, "will disable machine ‘%1%’ for %2%s", machine->sshName, delta);
|
printMsg(lvlInfo, "will disable machine ‘%1%’ for %2%s", machine->storeUri.render(), delta);
|
||||||
info->disabledUntil = now + std::chrono::seconds(delta);
|
info->disabledUntil = now + std::chrono::seconds(delta);
|
||||||
}
|
}
|
||||||
throw;
|
throw;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#include "hydra-build-result.hh"
|
#include "hydra-build-result.hh"
|
||||||
#include "store-api.hh"
|
#include <nix/store/store-api.hh>
|
||||||
#include "util.hh"
|
#include <nix/util/util.hh>
|
||||||
#include "fs-accessor.hh"
|
#include <nix/util/source-accessor.hh>
|
||||||
|
|
||||||
#include <regex>
|
#include <regex>
|
||||||
|
|
||||||
@@ -11,18 +11,18 @@ using namespace nix;
|
|||||||
BuildOutput getBuildOutput(
|
BuildOutput getBuildOutput(
|
||||||
nix::ref<Store> store,
|
nix::ref<Store> store,
|
||||||
NarMemberDatas & narMembers,
|
NarMemberDatas & narMembers,
|
||||||
const Derivation & drv)
|
const OutputPathMap derivationOutputs)
|
||||||
{
|
{
|
||||||
BuildOutput res;
|
BuildOutput res;
|
||||||
|
|
||||||
/* Compute the closure size. */
|
/* Compute the closure size. */
|
||||||
StorePathSet outputs;
|
StorePathSet outputs;
|
||||||
StorePathSet closure;
|
StorePathSet closure;
|
||||||
for (auto & i : drv.outputsAndOptPaths(*store))
|
for (auto& [outputName, outputPath] : derivationOutputs) {
|
||||||
if (i.second.second) {
|
store->computeFSClosure(outputPath, closure);
|
||||||
store->computeFSClosure(*i.second.second, closure);
|
outputs.insert(outputPath);
|
||||||
outputs.insert(*i.second.second);
|
res.outputs.insert({outputName, outputPath});
|
||||||
}
|
}
|
||||||
for (auto & path : closure) {
|
for (auto & path : closure) {
|
||||||
auto info = store->queryPathInfo(path);
|
auto info = store->queryPathInfo(path);
|
||||||
res.closureSize += info->narSize;
|
res.closureSize += info->narSize;
|
||||||
@@ -51,8 +51,8 @@ BuildOutput getBuildOutput(
|
|||||||
"[[:space:]]+"
|
"[[:space:]]+"
|
||||||
"([a-zA-Z0-9_-]+)" // subtype (e.g. "readme")
|
"([a-zA-Z0-9_-]+)" // subtype (e.g. "readme")
|
||||||
"[[:space:]]+"
|
"[[:space:]]+"
|
||||||
"(\"[^\"]+\"|[^[:space:]\"]+)" // path (may be quoted)
|
"(\"[^\"]+\"|[^[:space:]<>\"]+)" // path (may be quoted)
|
||||||
"([[:space:]]+([^[:space:]]+))?" // entry point
|
"([[:space:]]+([^[:space:]<>]+))?" // entry point
|
||||||
, std::regex::extended);
|
, std::regex::extended);
|
||||||
|
|
||||||
for (auto & output : outputs) {
|
for (auto & output : outputs) {
|
||||||
@@ -63,7 +63,7 @@ BuildOutput getBuildOutput(
|
|||||||
|
|
||||||
auto productsFile = narMembers.find(outputS + "/nix-support/hydra-build-products");
|
auto productsFile = narMembers.find(outputS + "/nix-support/hydra-build-products");
|
||||||
if (productsFile == narMembers.end() ||
|
if (productsFile == narMembers.end() ||
|
||||||
productsFile->second.type != FSAccessor::Type::tRegular)
|
productsFile->second.type != SourceAccessor::Type::tRegular)
|
||||||
continue;
|
continue;
|
||||||
assert(productsFile->second.contents);
|
assert(productsFile->second.contents);
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ BuildOutput getBuildOutput(
|
|||||||
product.type = match[1];
|
product.type = match[1];
|
||||||
product.subtype = match[2];
|
product.subtype = match[2];
|
||||||
std::string s(match[3]);
|
std::string s(match[3]);
|
||||||
product.path = s[0] == '"' ? std::string(s, 1, s.size() - 2) : s;
|
product.path = s[0] == '"' && s.back() == '"' ? std::string(s, 1, s.size() - 2) : s;
|
||||||
product.defaultPath = match[5];
|
product.defaultPath = match[5];
|
||||||
|
|
||||||
/* Ensure that the path exists and points into the Nix
|
/* Ensure that the path exists and points into the Nix
|
||||||
@@ -93,8 +93,10 @@ BuildOutput getBuildOutput(
|
|||||||
if (file == narMembers.end()) continue;
|
if (file == narMembers.end()) continue;
|
||||||
|
|
||||||
product.name = product.path == store->printStorePath(output) ? "" : baseNameOf(product.path);
|
product.name = product.path == store->printStorePath(output) ? "" : baseNameOf(product.path);
|
||||||
|
if (!std::regex_match(product.name, std::regex("[a-zA-Z0-9.@:_ -]*")))
|
||||||
|
product.name = "";
|
||||||
|
|
||||||
if (file->second.type == FSAccessor::Type::tRegular) {
|
if (file->second.type == SourceAccessor::Type::tRegular) {
|
||||||
product.isRegular = true;
|
product.isRegular = true;
|
||||||
product.fileSize = file->second.fileSize.value();
|
product.fileSize = file->second.fileSize.value();
|
||||||
product.sha256hash = file->second.sha256.value();
|
product.sha256hash = file->second.sha256.value();
|
||||||
@@ -107,17 +109,16 @@ BuildOutput getBuildOutput(
|
|||||||
/* If no build products were explicitly declared, then add all
|
/* If no build products were explicitly declared, then add all
|
||||||
outputs as a product of type "nix-build". */
|
outputs as a product of type "nix-build". */
|
||||||
if (!explicitProducts) {
|
if (!explicitProducts) {
|
||||||
for (auto & [name, output] : drv.outputs) {
|
for (auto & [name, output] : derivationOutputs) {
|
||||||
BuildProduct product;
|
BuildProduct product;
|
||||||
auto outPath = output.path(*store, drv.name, name);
|
product.path = store->printStorePath(output);
|
||||||
product.path = store->printStorePath(*outPath);
|
|
||||||
product.type = "nix-build";
|
product.type = "nix-build";
|
||||||
product.subtype = name == "out" ? "" : name;
|
product.subtype = name == "out" ? "" : name;
|
||||||
product.name = outPath->name();
|
product.name = output.name();
|
||||||
|
|
||||||
auto file = narMembers.find(product.path);
|
auto file = narMembers.find(product.path);
|
||||||
assert(file != narMembers.end());
|
assert(file != narMembers.end());
|
||||||
if (file->second.type == FSAccessor::Type::tDirectory)
|
if (file->second.type == SourceAccessor::Type::tDirectory)
|
||||||
res.products.push_back(product);
|
res.products.push_back(product);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,25 +127,34 @@ BuildOutput getBuildOutput(
|
|||||||
for (auto & output : outputs) {
|
for (auto & output : outputs) {
|
||||||
auto file = narMembers.find(store->printStorePath(output) + "/nix-support/hydra-release-name");
|
auto file = narMembers.find(store->printStorePath(output) + "/nix-support/hydra-release-name");
|
||||||
if (file == narMembers.end() ||
|
if (file == narMembers.end() ||
|
||||||
file->second.type != FSAccessor::Type::tRegular)
|
file->second.type != SourceAccessor::Type::tRegular)
|
||||||
continue;
|
continue;
|
||||||
res.releaseName = trim(file->second.contents.value());
|
auto contents = trim(file->second.contents.value());
|
||||||
// FIXME: validate release name
|
if (std::regex_match(contents, std::regex("[a-zA-Z0-9.@:_-]+")))
|
||||||
|
res.releaseName = contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Get metrics. */
|
/* Get metrics. */
|
||||||
for (auto & output : outputs) {
|
for (auto & output : outputs) {
|
||||||
auto file = narMembers.find(store->printStorePath(output) + "/nix-support/hydra-metrics");
|
auto file = narMembers.find(store->printStorePath(output) + "/nix-support/hydra-metrics");
|
||||||
if (file == narMembers.end() ||
|
if (file == narMembers.end() ||
|
||||||
file->second.type != FSAccessor::Type::tRegular)
|
file->second.type != SourceAccessor::Type::tRegular)
|
||||||
continue;
|
continue;
|
||||||
for (auto & line : tokenizeString<Strings>(file->second.contents.value(), "\n")) {
|
for (auto & line : tokenizeString<Strings>(file->second.contents.value(), "\n")) {
|
||||||
auto fields = tokenizeString<std::vector<std::string>>(line);
|
auto fields = tokenizeString<std::vector<std::string>>(line);
|
||||||
if (fields.size() < 2) continue;
|
if (fields.size() < 2) continue;
|
||||||
|
if (!std::regex_match(fields[0], std::regex("[a-zA-Z0-9._-]+")))
|
||||||
|
continue;
|
||||||
BuildMetric metric;
|
BuildMetric metric;
|
||||||
metric.name = fields[0]; // FIXME: validate
|
metric.name = fields[0];
|
||||||
metric.value = atof(fields[1].c_str()); // FIXME
|
try {
|
||||||
|
metric.value = std::stod(fields[1]);
|
||||||
|
} catch (...) {
|
||||||
|
continue; // skip this metric
|
||||||
|
}
|
||||||
metric.unit = fields.size() >= 3 ? fields[2] : "";
|
metric.unit = fields.size() >= 3 ? fields[2] : "";
|
||||||
|
if (!std::regex_match(metric.unit, std::regex("[a-zA-Z0-9._%-]+")))
|
||||||
|
metric.unit = "";
|
||||||
res.metrics[metric.name] = metric;
|
res.metrics[metric.name] = metric;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
#include "state.hh"
|
#include "state.hh"
|
||||||
#include "hydra-build-result.hh"
|
#include "hydra-build-result.hh"
|
||||||
#include "finally.hh"
|
#include <nix/util/finally.hh>
|
||||||
#include "binary-cache-store.hh"
|
#include <nix/store/binary-cache-store.hh>
|
||||||
|
|
||||||
using namespace nix;
|
using namespace nix;
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ void setThreadName(const std::string & name)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void State::builder(MachineReservation::ptr reservation)
|
void State::builder(std::unique_ptr<MachineReservation> reservation)
|
||||||
{
|
{
|
||||||
setThreadName("bld~" + std::string(reservation->step->drvPath.to_string()));
|
setThreadName("bld~" + std::string(reservation->step->drvPath.to_string()));
|
||||||
|
|
||||||
@@ -35,22 +35,20 @@ void State::builder(MachineReservation::ptr reservation)
|
|||||||
activeSteps_.lock()->erase(activeStep);
|
activeSteps_.lock()->erase(activeStep);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
std::string machine = reservation->machine->storeUri.render();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
auto destStore = getDestStore();
|
auto destStore = getDestStore();
|
||||||
res = doBuildStep(destStore, reservation, activeStep);
|
// Might release the reservation.
|
||||||
|
res = doBuildStep(destStore, std::move(reservation), activeStep);
|
||||||
} catch (std::exception & e) {
|
} catch (std::exception & e) {
|
||||||
printMsg(lvlError, "uncaught exception building ‘%s’ on ‘%s’: %s",
|
printMsg(lvlError, "uncaught exception building ‘%s’ on ‘%s’: %s",
|
||||||
localStore->printStorePath(reservation->step->drvPath),
|
localStore->printStorePath(activeStep->step->drvPath),
|
||||||
reservation->machine->sshName,
|
machine,
|
||||||
e.what());
|
e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Release the machine and wake up the dispatcher. */
|
|
||||||
assert(reservation.unique());
|
|
||||||
reservation = 0;
|
|
||||||
wakeDispatcher();
|
|
||||||
|
|
||||||
/* If there was a temporary failure, retry the step after an
|
/* If there was a temporary failure, retry the step after an
|
||||||
exponentially increasing interval. */
|
exponentially increasing interval. */
|
||||||
Step::ptr step = wstep.lock();
|
Step::ptr step = wstep.lock();
|
||||||
@@ -72,11 +70,11 @@ void State::builder(MachineReservation::ptr reservation)
|
|||||||
|
|
||||||
|
|
||||||
State::StepResult State::doBuildStep(nix::ref<Store> destStore,
|
State::StepResult State::doBuildStep(nix::ref<Store> destStore,
|
||||||
MachineReservation::ptr reservation,
|
std::unique_ptr<MachineReservation> reservation,
|
||||||
std::shared_ptr<ActiveStep> activeStep)
|
std::shared_ptr<ActiveStep> activeStep)
|
||||||
{
|
{
|
||||||
auto & step(reservation->step);
|
auto step(reservation->step);
|
||||||
auto & machine(reservation->machine);
|
auto machine(reservation->machine);
|
||||||
|
|
||||||
{
|
{
|
||||||
auto step_(step->state.lock());
|
auto step_(step->state.lock());
|
||||||
@@ -98,8 +96,13 @@ State::StepResult State::doBuildStep(nix::ref<Store> destStore,
|
|||||||
it). */
|
it). */
|
||||||
BuildID buildId;
|
BuildID buildId;
|
||||||
std::optional<StorePath> buildDrvPath;
|
std::optional<StorePath> buildDrvPath;
|
||||||
unsigned int maxSilentTime, buildTimeout;
|
// Other fields set below
|
||||||
unsigned int repeats = step->isDeterministic ? 1 : 0;
|
nix::ServeProto::BuildOptions buildOptions {
|
||||||
|
.maxLogSize = maxLogSize,
|
||||||
|
.nrRepeats = step->isDeterministic ? 1u : 0u,
|
||||||
|
.enforceDeterminism = step->isDeterministic,
|
||||||
|
.keepFailed = false,
|
||||||
|
};
|
||||||
|
|
||||||
auto conn(dbPool.get());
|
auto conn(dbPool.get());
|
||||||
|
|
||||||
@@ -134,18 +137,18 @@ State::StepResult State::doBuildStep(nix::ref<Store> destStore,
|
|||||||
{
|
{
|
||||||
auto i = jobsetRepeats.find(std::make_pair(build2->projectName, build2->jobsetName));
|
auto i = jobsetRepeats.find(std::make_pair(build2->projectName, build2->jobsetName));
|
||||||
if (i != jobsetRepeats.end())
|
if (i != jobsetRepeats.end())
|
||||||
repeats = std::max(repeats, i->second);
|
buildOptions.nrRepeats = std::max(buildOptions.nrRepeats, i->second);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!build) build = *dependents.begin();
|
if (!build) build = *dependents.begin();
|
||||||
|
|
||||||
buildId = build->id;
|
buildId = build->id;
|
||||||
buildDrvPath = build->drvPath;
|
buildDrvPath = build->drvPath;
|
||||||
maxSilentTime = build->maxSilentTime;
|
buildOptions.maxSilentTime = build->maxSilentTime;
|
||||||
buildTimeout = build->buildTimeout;
|
buildOptions.buildTimeout = build->buildTimeout;
|
||||||
|
|
||||||
printInfo("performing step ‘%s’ %d times on ‘%s’ (needed by build %d and %d others)",
|
printInfo("performing step ‘%s’ %d times on ‘%s’ (needed by build %d and %d others)",
|
||||||
localStore->printStorePath(step->drvPath), repeats + 1, machine->sshName, buildId, (dependents.size() - 1));
|
localStore->printStorePath(step->drvPath), buildOptions.nrRepeats + 1, machine->storeUri.render(), buildId, (dependents.size() - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!buildOneDone)
|
if (!buildOneDone)
|
||||||
@@ -173,7 +176,7 @@ State::StepResult State::doBuildStep(nix::ref<Store> destStore,
|
|||||||
unlink(result.logFile.c_str());
|
unlink(result.logFile.c_str());
|
||||||
}
|
}
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
ignoreException();
|
ignoreExceptionInDestructor();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -191,7 +194,7 @@ State::StepResult State::doBuildStep(nix::ref<Store> destStore,
|
|||||||
{
|
{
|
||||||
auto mc = startDbUpdate();
|
auto mc = startDbUpdate();
|
||||||
pqxx::work txn(*conn);
|
pqxx::work txn(*conn);
|
||||||
stepNr = createBuildStep(txn, result.startTime, buildId, step, machine->sshName, bsBusy);
|
stepNr = createBuildStep(txn, result.startTime, buildId, step, machine->storeUri.render(), bsBusy);
|
||||||
txn.commit();
|
txn.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +209,7 @@ State::StepResult State::doBuildStep(nix::ref<Store> destStore,
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
/* FIXME: referring builds may have conflicting timeouts. */
|
/* FIXME: referring builds may have conflicting timeouts. */
|
||||||
buildRemote(destStore, machine, step, maxSilentTime, buildTimeout, repeats, result, activeStep, updateStep, narMembers);
|
buildRemote(destStore, std::move(reservation), machine, step, buildOptions, result, activeStep, updateStep, narMembers);
|
||||||
} catch (Error & e) {
|
} catch (Error & e) {
|
||||||
if (activeStep->state_.lock()->cancelled) {
|
if (activeStep->state_.lock()->cancelled) {
|
||||||
printInfo("marking step %d of build %d as cancelled", stepNr, buildId);
|
printInfo("marking step %d of build %d as cancelled", stepNr, buildId);
|
||||||
@@ -221,7 +224,7 @@ State::StepResult State::doBuildStep(nix::ref<Store> destStore,
|
|||||||
|
|
||||||
if (result.stepStatus == bsSuccess) {
|
if (result.stepStatus == bsSuccess) {
|
||||||
updateStep(ssPostProcessing);
|
updateStep(ssPostProcessing);
|
||||||
res = getBuildOutput(destStore, narMembers, *step->drv);
|
res = getBuildOutput(destStore, narMembers, destStore->queryDerivationOutputMap(step->drvPath, &*localStore));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +251,7 @@ State::StepResult State::doBuildStep(nix::ref<Store> destStore,
|
|||||||
/* Finish the step in the database. */
|
/* Finish the step in the database. */
|
||||||
if (stepNr) {
|
if (stepNr) {
|
||||||
pqxx::work txn(*conn);
|
pqxx::work txn(*conn);
|
||||||
finishBuildStep(txn, result, buildId, stepNr, machine->sshName);
|
finishBuildStep(txn, result, buildId, stepNr, machine->storeUri.render());
|
||||||
txn.commit();
|
txn.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +259,7 @@ State::StepResult State::doBuildStep(nix::ref<Store> destStore,
|
|||||||
issue). Retry a number of times. */
|
issue). Retry a number of times. */
|
||||||
if (result.canRetry) {
|
if (result.canRetry) {
|
||||||
printMsg(lvlError, "possibly transient failure building ‘%s’ on ‘%s’: %s",
|
printMsg(lvlError, "possibly transient failure building ‘%s’ on ‘%s’: %s",
|
||||||
localStore->printStorePath(step->drvPath), machine->sshName, result.errorMsg);
|
localStore->printStorePath(step->drvPath), machine->storeUri.render(), result.errorMsg);
|
||||||
assert(stepNr);
|
assert(stepNr);
|
||||||
bool retry;
|
bool retry;
|
||||||
{
|
{
|
||||||
@@ -275,9 +278,12 @@ State::StepResult State::doBuildStep(nix::ref<Store> destStore,
|
|||||||
|
|
||||||
assert(stepNr);
|
assert(stepNr);
|
||||||
|
|
||||||
for (auto & i : step->drv->outputsAndOptPaths(*localStore)) {
|
for (auto & [outputName, optOutputPath] : destStore->queryPartialDerivationOutputMap(step->drvPath, &*localStore)) {
|
||||||
if (i.second.second)
|
if (!optOutputPath)
|
||||||
addRoot(*i.second.second);
|
throw Error(
|
||||||
|
"Missing output %s for derivation %d which was supposed to have succeeded",
|
||||||
|
outputName, localStore->printStorePath(step->drvPath));
|
||||||
|
addRoot(*optOutputPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Register success in the database for all Build objects that
|
/* Register success in the database for all Build objects that
|
||||||
@@ -398,7 +404,7 @@ void State::failStep(
|
|||||||
Step::ptr step,
|
Step::ptr step,
|
||||||
BuildID buildId,
|
BuildID buildId,
|
||||||
const RemoteResult & result,
|
const RemoteResult & result,
|
||||||
Machine::ptr machine,
|
::Machine::ptr machine,
|
||||||
bool & stepFinished)
|
bool & stepFinished)
|
||||||
{
|
{
|
||||||
/* Register failure in the database for all Build objects that
|
/* Register failure in the database for all Build objects that
|
||||||
@@ -444,7 +450,7 @@ void State::failStep(
|
|||||||
build->finishedInDB)
|
build->finishedInDB)
|
||||||
continue;
|
continue;
|
||||||
createBuildStep(txn,
|
createBuildStep(txn,
|
||||||
0, build->id, step, machine ? machine->sshName : "",
|
0, build->id, step, machine ? machine->storeUri.render() : "",
|
||||||
result.stepStatus, result.errorMsg, buildId == build->id ? 0 : buildId);
|
result.stepStatus, result.errorMsg, buildId == build->id ? 0 : buildId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,13 +458,12 @@ void State::failStep(
|
|||||||
for (auto & build : indirect) {
|
for (auto & build : indirect) {
|
||||||
if (build->finishedInDB) continue;
|
if (build->finishedInDB) continue;
|
||||||
printError("marking build %1% as failed", build->id);
|
printError("marking build %1% as failed", build->id);
|
||||||
txn.exec_params0
|
txn.exec("update Builds set finished = 1, buildStatus = $2, startTime = $3, stopTime = $4, isCachedBuild = $5, notificationPendingSince = $4 where id = $1 and finished = 0",
|
||||||
("update Builds set finished = 1, buildStatus = $2, startTime = $3, stopTime = $4, isCachedBuild = $5, notificationPendingSince = $4 where id = $1 and finished = 0",
|
pqxx::params{build->id,
|
||||||
build->id,
|
|
||||||
(int) (build->drvPath != step->drvPath && result.buildStatus() == bsFailed ? bsDepFailed : result.buildStatus()),
|
(int) (build->drvPath != step->drvPath && result.buildStatus() == bsFailed ? bsDepFailed : result.buildStatus()),
|
||||||
result.startTime,
|
result.startTime,
|
||||||
result.stopTime,
|
result.stopTime,
|
||||||
result.stepStatus == bsCachedFailure ? 1 : 0);
|
result.stepStatus == bsCachedFailure ? 1 : 0}).no_rows();
|
||||||
nrBuildsDone++;
|
nrBuildsDone++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,7 +472,7 @@ void State::failStep(
|
|||||||
if (result.stepStatus != bsCachedFailure && result.canCache)
|
if (result.stepStatus != bsCachedFailure && result.canCache)
|
||||||
for (auto & i : step->drv->outputsAndOptPaths(*localStore))
|
for (auto & i : step->drv->outputsAndOptPaths(*localStore))
|
||||||
if (i.second.second)
|
if (i.second.second)
|
||||||
txn.exec_params0("insert into FailedPaths values ($1)", localStore->printStorePath(*i.second.second));
|
txn.exec("insert into FailedPaths values ($1)", pqxx::params{localStore->printStorePath(*i.second.second)}).no_rows();
|
||||||
|
|
||||||
txn.commit();
|
txn.commit();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
#include "state.hh"
|
#include "state.hh"
|
||||||
|
|
||||||
@@ -39,13 +40,15 @@ void State::dispatcher()
|
|||||||
printMsg(lvlDebug, "dispatcher woken up");
|
printMsg(lvlDebug, "dispatcher woken up");
|
||||||
nrDispatcherWakeups++;
|
nrDispatcherWakeups++;
|
||||||
|
|
||||||
auto now1 = std::chrono::steady_clock::now();
|
auto t_before_work = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
auto sleepUntil = doDispatch();
|
auto sleepUntil = doDispatch();
|
||||||
|
|
||||||
auto now2 = std::chrono::steady_clock::now();
|
auto t_after_work = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
dispatchTimeMs += std::chrono::duration_cast<std::chrono::milliseconds>(now2 - now1).count();
|
prom.dispatcher_time_spent_running.Increment(
|
||||||
|
std::chrono::duration_cast<std::chrono::microseconds>(t_after_work - t_before_work).count());
|
||||||
|
dispatchTimeMs += std::chrono::duration_cast<std::chrono::milliseconds>(t_after_work - t_before_work).count();
|
||||||
|
|
||||||
/* Sleep until we're woken up (either because a runnable build
|
/* Sleep until we're woken up (either because a runnable build
|
||||||
is added, or because a build finishes). */
|
is added, or because a build finishes). */
|
||||||
@@ -59,6 +62,10 @@ void State::dispatcher()
|
|||||||
*dispatcherWakeup_ = false;
|
*dispatcherWakeup_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto t_after_sleep = std::chrono::steady_clock::now();
|
||||||
|
prom.dispatcher_time_spent_waiting.Increment(
|
||||||
|
std::chrono::duration_cast<std::chrono::microseconds>(t_after_sleep - t_after_work).count());
|
||||||
|
|
||||||
} catch (std::exception & e) {
|
} catch (std::exception & e) {
|
||||||
printError("dispatcher: %s", e.what());
|
printError("dispatcher: %s", e.what());
|
||||||
sleep(1);
|
sleep(1);
|
||||||
@@ -85,12 +92,119 @@ system_time State::doDispatch()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
system_time now = std::chrono::system_clock::now();
|
||||||
|
|
||||||
/* Start steps until we're out of steps or slots. */
|
/* Start steps until we're out of steps or slots. */
|
||||||
auto sleepUntil = system_time::max();
|
auto sleepUntil = system_time::max();
|
||||||
bool keepGoing;
|
bool keepGoing;
|
||||||
|
|
||||||
|
/* Sort the runnable steps by priority. Priority is establised
|
||||||
|
as follows (in order of precedence):
|
||||||
|
|
||||||
|
- The global priority of the builds that depend on the
|
||||||
|
step. This allows admins to bump a build to the front of
|
||||||
|
the queue.
|
||||||
|
|
||||||
|
- The lowest used scheduling share of the jobsets depending
|
||||||
|
on the step.
|
||||||
|
|
||||||
|
- The local priority of the build, as set via the build's
|
||||||
|
meta.schedulingPriority field. Note that this is not
|
||||||
|
quite correct: the local priority should only be used to
|
||||||
|
establish priority between builds in the same jobset, but
|
||||||
|
here it's used between steps in different jobsets if they
|
||||||
|
happen to have the same lowest used scheduling share. But
|
||||||
|
that's not very likely.
|
||||||
|
|
||||||
|
- The lowest ID of the builds depending on the step;
|
||||||
|
i.e. older builds take priority over new ones.
|
||||||
|
|
||||||
|
FIXME: O(n lg n); obviously, it would be better to keep a
|
||||||
|
runnable queue sorted by priority. */
|
||||||
|
struct StepInfo
|
||||||
|
{
|
||||||
|
Step::ptr step;
|
||||||
|
bool alreadyScheduled = false;
|
||||||
|
|
||||||
|
/* The lowest share used of any jobset depending on this
|
||||||
|
step. */
|
||||||
|
double lowestShareUsed = 1e9;
|
||||||
|
|
||||||
|
/* Info copied from step->state to ensure that the
|
||||||
|
comparator is a partial ordering (see MachineInfo). */
|
||||||
|
int highestGlobalPriority;
|
||||||
|
int highestLocalPriority;
|
||||||
|
size_t numRequiredSystemFeatures;
|
||||||
|
size_t numRevDeps;
|
||||||
|
BuildID lowestBuildID;
|
||||||
|
|
||||||
|
StepInfo(Step::ptr step, Step::State & step_) : step(step)
|
||||||
|
{
|
||||||
|
for (auto & jobset : step_.jobsets)
|
||||||
|
lowestShareUsed = std::min(lowestShareUsed, jobset->shareUsed());
|
||||||
|
highestGlobalPriority = step_.highestGlobalPriority;
|
||||||
|
highestLocalPriority = step_.highestLocalPriority;
|
||||||
|
numRequiredSystemFeatures = step->requiredSystemFeatures.size();
|
||||||
|
numRevDeps = step_.rdeps.size();
|
||||||
|
lowestBuildID = step_.lowestBuildID;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<StepInfo> runnableSorted;
|
||||||
|
|
||||||
|
struct RunnablePerType
|
||||||
|
{
|
||||||
|
unsigned int count{0};
|
||||||
|
std::chrono::seconds waitTime{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
std::unordered_map<std::string, RunnablePerType> runnablePerType;
|
||||||
|
|
||||||
|
{
|
||||||
|
auto runnable_(runnable.lock());
|
||||||
|
runnableSorted.reserve(runnable_->size());
|
||||||
|
for (auto i = runnable_->begin(); i != runnable_->end(); ) {
|
||||||
|
auto step = i->lock();
|
||||||
|
|
||||||
|
/* Remove dead steps. */
|
||||||
|
if (!step) {
|
||||||
|
i = runnable_->erase(i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
++i;
|
||||||
|
|
||||||
|
auto & r = runnablePerType[step->systemType];
|
||||||
|
r.count++;
|
||||||
|
|
||||||
|
/* Skip previously failed steps that aren't ready
|
||||||
|
to be retried. */
|
||||||
|
auto step_(step->state.lock());
|
||||||
|
r.waitTime += std::chrono::duration_cast<std::chrono::seconds>(now - step_->runnableSince);
|
||||||
|
if (step_->tries > 0 && step_->after > now) {
|
||||||
|
if (step_->after < sleepUntil)
|
||||||
|
sleepUntil = step_->after;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
runnableSorted.emplace_back(step, *step_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort(runnableSorted.begin(), runnableSorted.end(),
|
||||||
|
[](const StepInfo & a, const StepInfo & b)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
a.highestGlobalPriority != b.highestGlobalPriority ? a.highestGlobalPriority > b.highestGlobalPriority :
|
||||||
|
a.lowestShareUsed != b.lowestShareUsed ? a.lowestShareUsed < b.lowestShareUsed :
|
||||||
|
a.highestLocalPriority != b.highestLocalPriority ? a.highestLocalPriority > b.highestLocalPriority :
|
||||||
|
a.numRequiredSystemFeatures != b.numRequiredSystemFeatures ? a.numRequiredSystemFeatures > b.numRequiredSystemFeatures :
|
||||||
|
a.numRevDeps != b.numRevDeps ? a.numRevDeps > b.numRevDeps :
|
||||||
|
a.lowestBuildID < b.lowestBuildID;
|
||||||
|
});
|
||||||
|
|
||||||
do {
|
do {
|
||||||
system_time now = std::chrono::system_clock::now();
|
now = std::chrono::system_clock::now();
|
||||||
|
|
||||||
/* Copy the currentJobs field of each machine. This is
|
/* Copy the currentJobs field of each machine. This is
|
||||||
necessary to ensure that the sort comparator below is
|
necessary to ensure that the sort comparator below is
|
||||||
@@ -98,7 +212,7 @@ system_time State::doDispatch()
|
|||||||
filter out temporarily disabled machines. */
|
filter out temporarily disabled machines. */
|
||||||
struct MachineInfo
|
struct MachineInfo
|
||||||
{
|
{
|
||||||
Machine::ptr machine;
|
::Machine::ptr machine;
|
||||||
unsigned long currentJobs;
|
unsigned long currentJobs;
|
||||||
};
|
};
|
||||||
std::vector<MachineInfo> machinesSorted;
|
std::vector<MachineInfo> machinesSorted;
|
||||||
@@ -138,104 +252,6 @@ system_time State::doDispatch()
|
|||||||
a.currentJobs > b.currentJobs;
|
a.currentJobs > b.currentJobs;
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Sort the runnable steps by priority. Priority is establised
|
|
||||||
as follows (in order of precedence):
|
|
||||||
|
|
||||||
- The global priority of the builds that depend on the
|
|
||||||
step. This allows admins to bump a build to the front of
|
|
||||||
the queue.
|
|
||||||
|
|
||||||
- The lowest used scheduling share of the jobsets depending
|
|
||||||
on the step.
|
|
||||||
|
|
||||||
- The local priority of the build, as set via the build's
|
|
||||||
meta.schedulingPriority field. Note that this is not
|
|
||||||
quite correct: the local priority should only be used to
|
|
||||||
establish priority between builds in the same jobset, but
|
|
||||||
here it's used between steps in different jobsets if they
|
|
||||||
happen to have the same lowest used scheduling share. But
|
|
||||||
that's not very likely.
|
|
||||||
|
|
||||||
- The lowest ID of the builds depending on the step;
|
|
||||||
i.e. older builds take priority over new ones.
|
|
||||||
|
|
||||||
FIXME: O(n lg n); obviously, it would be better to keep a
|
|
||||||
runnable queue sorted by priority. */
|
|
||||||
struct StepInfo
|
|
||||||
{
|
|
||||||
Step::ptr step;
|
|
||||||
|
|
||||||
/* The lowest share used of any jobset depending on this
|
|
||||||
step. */
|
|
||||||
double lowestShareUsed = 1e9;
|
|
||||||
|
|
||||||
/* Info copied from step->state to ensure that the
|
|
||||||
comparator is a partial ordering (see MachineInfo). */
|
|
||||||
int highestGlobalPriority;
|
|
||||||
int highestLocalPriority;
|
|
||||||
BuildID lowestBuildID;
|
|
||||||
|
|
||||||
StepInfo(Step::ptr step, Step::State & step_) : step(step)
|
|
||||||
{
|
|
||||||
for (auto & jobset : step_.jobsets)
|
|
||||||
lowestShareUsed = std::min(lowestShareUsed, jobset->shareUsed());
|
|
||||||
highestGlobalPriority = step_.highestGlobalPriority;
|
|
||||||
highestLocalPriority = step_.highestLocalPriority;
|
|
||||||
lowestBuildID = step_.lowestBuildID;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
std::vector<StepInfo> runnableSorted;
|
|
||||||
|
|
||||||
struct RunnablePerType
|
|
||||||
{
|
|
||||||
unsigned int count{0};
|
|
||||||
std::chrono::seconds waitTime{0};
|
|
||||||
};
|
|
||||||
|
|
||||||
std::unordered_map<std::string, RunnablePerType> runnablePerType;
|
|
||||||
|
|
||||||
{
|
|
||||||
auto runnable_(runnable.lock());
|
|
||||||
runnableSorted.reserve(runnable_->size());
|
|
||||||
for (auto i = runnable_->begin(); i != runnable_->end(); ) {
|
|
||||||
auto step = i->lock();
|
|
||||||
|
|
||||||
/* Remove dead steps. */
|
|
||||||
if (!step) {
|
|
||||||
i = runnable_->erase(i);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
++i;
|
|
||||||
|
|
||||||
auto & r = runnablePerType[step->systemType];
|
|
||||||
r.count++;
|
|
||||||
|
|
||||||
/* Skip previously failed steps that aren't ready
|
|
||||||
to be retried. */
|
|
||||||
auto step_(step->state.lock());
|
|
||||||
r.waitTime += std::chrono::duration_cast<std::chrono::seconds>(now - step_->runnableSince);
|
|
||||||
if (step_->tries > 0 && step_->after > now) {
|
|
||||||
if (step_->after < sleepUntil)
|
|
||||||
sleepUntil = step_->after;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
runnableSorted.emplace_back(step, *step_);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort(runnableSorted.begin(), runnableSorted.end(),
|
|
||||||
[](const StepInfo & a, const StepInfo & b)
|
|
||||||
{
|
|
||||||
return
|
|
||||||
a.highestGlobalPriority != b.highestGlobalPriority ? a.highestGlobalPriority > b.highestGlobalPriority :
|
|
||||||
a.lowestShareUsed != b.lowestShareUsed ? a.lowestShareUsed < b.lowestShareUsed :
|
|
||||||
a.highestLocalPriority != b.highestLocalPriority ? a.highestLocalPriority > b.highestLocalPriority :
|
|
||||||
a.lowestBuildID < b.lowestBuildID;
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Find a machine with a free slot and find a step to run
|
/* Find a machine with a free slot and find a step to run
|
||||||
on it. Once we find such a pair, we restart the outer
|
on it. Once we find such a pair, we restart the outer
|
||||||
loop because the machine sorting will have changed. */
|
loop because the machine sorting will have changed. */
|
||||||
@@ -245,12 +261,14 @@ system_time State::doDispatch()
|
|||||||
if (mi.machine->state->currentJobs >= mi.machine->maxJobs) continue;
|
if (mi.machine->state->currentJobs >= mi.machine->maxJobs) continue;
|
||||||
|
|
||||||
for (auto & stepInfo : runnableSorted) {
|
for (auto & stepInfo : runnableSorted) {
|
||||||
|
if (stepInfo.alreadyScheduled) continue;
|
||||||
|
|
||||||
auto & step(stepInfo.step);
|
auto & step(stepInfo.step);
|
||||||
|
|
||||||
/* Can this machine do this step? */
|
/* Can this machine do this step? */
|
||||||
if (!mi.machine->supportsStep(step)) {
|
if (!mi.machine->supportsStep(step)) {
|
||||||
debug("machine '%s' does not support step '%s' (system type '%s')",
|
debug("machine '%s' does not support step '%s' (system type '%s')",
|
||||||
mi.machine->sshName, localStore->printStorePath(step->drvPath), step->drv->platform);
|
mi.machine->storeUri.render(), localStore->printStorePath(step->drvPath), step->drv->platform);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,10 +289,12 @@ system_time State::doDispatch()
|
|||||||
r.count--;
|
r.count--;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stepInfo.alreadyScheduled = true;
|
||||||
|
|
||||||
/* Make a slot reservation and start a thread to
|
/* Make a slot reservation and start a thread to
|
||||||
do the build. */
|
do the build. */
|
||||||
auto builderThread = std::thread(&State::builder, this,
|
auto builderThread = std::thread(&State::builder, this,
|
||||||
std::make_shared<MachineReservation>(*this, step, mi.machine));
|
std::make_unique<MachineReservation>(*this, step, mi.machine));
|
||||||
builderThread.detach(); // FIXME?
|
builderThread.detach(); // FIXME?
|
||||||
|
|
||||||
keepGoing = true;
|
keepGoing = true;
|
||||||
@@ -428,7 +448,7 @@ void Jobset::pruneSteps()
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
State::MachineReservation::MachineReservation(State & state, Step::ptr step, Machine::ptr machine)
|
State::MachineReservation::MachineReservation(State & state, Step::ptr step, ::Machine::ptr machine)
|
||||||
: state(state), step(step), machine(machine)
|
: state(state), step(step), machine(machine)
|
||||||
{
|
{
|
||||||
machine->state->currentJobs++;
|
machine->state->currentJobs++;
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "hash.hh"
|
#include <nix/util/hash.hh>
|
||||||
#include "derivations.hh"
|
#include <nix/store/derivations.hh>
|
||||||
#include "store-api.hh"
|
#include <nix/store/store-api.hh>
|
||||||
#include "nar-extractor.hh"
|
#include "nar-extractor.hh"
|
||||||
|
|
||||||
struct BuildProduct
|
struct BuildProduct
|
||||||
@@ -36,10 +36,12 @@ struct BuildOutput
|
|||||||
|
|
||||||
std::list<BuildProduct> products;
|
std::list<BuildProduct> products;
|
||||||
|
|
||||||
|
std::map<std::string, nix::StorePath> outputs;
|
||||||
|
|
||||||
std::map<std::string, BuildMetric> metrics;
|
std::map<std::string, BuildMetric> metrics;
|
||||||
};
|
};
|
||||||
|
|
||||||
BuildOutput getBuildOutput(
|
BuildOutput getBuildOutput(
|
||||||
nix::ref<nix::Store> store,
|
nix::ref<nix::Store> store,
|
||||||
NarMemberDatas & narMembers,
|
NarMemberDatas & narMembers,
|
||||||
const nix::Derivation & drv);
|
const nix::OutputPathMap derivationOutputs);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
@@ -10,15 +11,16 @@
|
|||||||
|
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include <nix/util/signals.hh>
|
||||||
#include "state.hh"
|
#include "state.hh"
|
||||||
#include "hydra-build-result.hh"
|
#include "hydra-build-result.hh"
|
||||||
#include "store-api.hh"
|
#include <nix/store/store-open.hh>
|
||||||
#include "remote-store.hh"
|
#include <nix/store/remote-store.hh>
|
||||||
|
|
||||||
#include "globals.hh"
|
#include <nix/store/globals.hh>
|
||||||
#include "hydra-config.hh"
|
#include "hydra-config.hh"
|
||||||
#include "s3-binary-cache-store.hh"
|
#include <nix/store/s3-binary-cache-store.hh>
|
||||||
#include "shared.hh"
|
#include <nix/main/shared.hh>
|
||||||
|
|
||||||
using namespace nix;
|
using namespace nix;
|
||||||
using nlohmann::json;
|
using nlohmann::json;
|
||||||
@@ -68,10 +70,31 @@ State::PromMetrics::PromMetrics()
|
|||||||
.Register(*registry)
|
.Register(*registry)
|
||||||
.Add({})
|
.Add({})
|
||||||
)
|
)
|
||||||
, queue_max_id(
|
, dispatcher_time_spent_running(
|
||||||
prometheus::BuildGauge()
|
prometheus::BuildCounter()
|
||||||
.Name("hydraqueuerunner_queue_max_build_id_info")
|
.Name("hydraqueuerunner_dispatcher_time_spent_running")
|
||||||
.Help("Maximum build record ID in the queue")
|
.Help("Time (in micros) spent running the dispatcher")
|
||||||
|
.Register(*registry)
|
||||||
|
.Add({})
|
||||||
|
)
|
||||||
|
, dispatcher_time_spent_waiting(
|
||||||
|
prometheus::BuildCounter()
|
||||||
|
.Name("hydraqueuerunner_dispatcher_time_spent_waiting")
|
||||||
|
.Help("Time (in micros) spent waiting for the dispatcher to obtain work")
|
||||||
|
.Register(*registry)
|
||||||
|
.Add({})
|
||||||
|
)
|
||||||
|
, queue_monitor_time_spent_running(
|
||||||
|
prometheus::BuildCounter()
|
||||||
|
.Name("hydraqueuerunner_queue_monitor_time_spent_running")
|
||||||
|
.Help("Time (in micros) spent running the queue monitor")
|
||||||
|
.Register(*registry)
|
||||||
|
.Add({})
|
||||||
|
)
|
||||||
|
, queue_monitor_time_spent_waiting(
|
||||||
|
prometheus::BuildCounter()
|
||||||
|
.Name("hydraqueuerunner_queue_monitor_time_spent_waiting")
|
||||||
|
.Help("Time (in micros) spent waiting for the queue monitor to obtain work")
|
||||||
.Register(*registry)
|
.Register(*registry)
|
||||||
.Add({})
|
.Add({})
|
||||||
)
|
)
|
||||||
@@ -83,6 +106,7 @@ State::State(std::optional<std::string> metricsAddrOpt)
|
|||||||
: config(std::make_unique<HydraConfig>())
|
: config(std::make_unique<HydraConfig>())
|
||||||
, maxUnsupportedTime(config->getIntOption("max_unsupported_time", 0))
|
, maxUnsupportedTime(config->getIntOption("max_unsupported_time", 0))
|
||||||
, dbPool(config->getIntOption("max_db_connections", 128))
|
, dbPool(config->getIntOption("max_db_connections", 128))
|
||||||
|
, localWorkThrottler(config->getIntOption("max_local_worker_threads", std::min(maxSupportedLocalWorkers, std::max(4u, std::thread::hardware_concurrency()) - 2)))
|
||||||
, maxOutputSize(config->getIntOption("max_output_size", 2ULL << 30))
|
, maxOutputSize(config->getIntOption("max_output_size", 2ULL << 30))
|
||||||
, maxLogSize(config->getIntOption("max_log_size", 64ULL << 20))
|
, maxLogSize(config->getIntOption("max_log_size", 64ULL << 20))
|
||||||
, uploadLogsToBinaryCache(config->getBoolOption("upload_logs_to_binary_cache", false))
|
, uploadLogsToBinaryCache(config->getBoolOption("upload_logs_to_binary_cache", false))
|
||||||
@@ -133,50 +157,29 @@ void State::parseMachines(const std::string & contents)
|
|||||||
oldMachines = *machines_;
|
oldMachines = *machines_;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto line : tokenizeString<Strings>(contents, "\n")) {
|
for (auto && machine_ : nix::Machine::parseConfig({}, contents)) {
|
||||||
line = trim(std::string(line, 0, line.find('#')));
|
auto machine = std::make_shared<::Machine>(std::move(machine_));
|
||||||
auto tokens = tokenizeString<std::vector<std::string>>(line);
|
|
||||||
if (tokens.size() < 3) continue;
|
|
||||||
tokens.resize(8);
|
|
||||||
|
|
||||||
auto machine = std::make_shared<Machine>();
|
|
||||||
machine->sshName = tokens[0];
|
|
||||||
machine->systemTypes = tokenizeString<StringSet>(tokens[1], ",");
|
|
||||||
machine->sshKey = tokens[2] == "-" ? std::string("") : tokens[2];
|
|
||||||
if (tokens[3] != "")
|
|
||||||
machine->maxJobs = string2Int<decltype(machine->maxJobs)>(tokens[3]).value();
|
|
||||||
else
|
|
||||||
machine->maxJobs = 1;
|
|
||||||
machine->speedFactor = atof(tokens[4].c_str());
|
|
||||||
if (tokens[5] == "-") tokens[5] = "";
|
|
||||||
machine->supportedFeatures = tokenizeString<StringSet>(tokens[5], ",");
|
|
||||||
if (tokens[6] == "-") tokens[6] = "";
|
|
||||||
machine->mandatoryFeatures = tokenizeString<StringSet>(tokens[6], ",");
|
|
||||||
for (auto & f : machine->mandatoryFeatures)
|
|
||||||
machine->supportedFeatures.insert(f);
|
|
||||||
if (tokens[7] != "" && tokens[7] != "-")
|
|
||||||
machine->sshPublicHostKey = base64Decode(tokens[7]);
|
|
||||||
|
|
||||||
/* Re-use the State object of the previous machine with the
|
/* Re-use the State object of the previous machine with the
|
||||||
same name. */
|
same name. */
|
||||||
auto i = oldMachines.find(machine->sshName);
|
auto i = oldMachines.find(machine->storeUri.variant);
|
||||||
if (i == oldMachines.end())
|
if (i == oldMachines.end())
|
||||||
printMsg(lvlChatty, "adding new machine ‘%1%’", machine->sshName);
|
printMsg(lvlChatty, "adding new machine ‘%1%’", machine->storeUri.render());
|
||||||
else
|
else
|
||||||
printMsg(lvlChatty, "updating machine ‘%1%’", machine->sshName);
|
printMsg(lvlChatty, "updating machine ‘%1%’", machine->storeUri.render());
|
||||||
machine->state = i == oldMachines.end()
|
machine->state = i == oldMachines.end()
|
||||||
? std::make_shared<Machine::State>()
|
? std::make_shared<::Machine::State>()
|
||||||
: i->second->state;
|
: i->second->state;
|
||||||
newMachines[machine->sshName] = machine;
|
newMachines[machine->storeUri.variant] = machine;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto & m : oldMachines)
|
for (auto & m : oldMachines)
|
||||||
if (newMachines.find(m.first) == newMachines.end()) {
|
if (newMachines.find(m.first) == newMachines.end()) {
|
||||||
if (m.second->enabled)
|
if (m.second->enabled)
|
||||||
printInfo("removing machine ‘%1%’", m.first);
|
printInfo("removing machine ‘%1%’", m.second->storeUri.render());
|
||||||
/* Add a disabled Machine object to make sure stats are
|
/* Add a disabled ::Machine object to make sure stats are
|
||||||
maintained. */
|
maintained. */
|
||||||
auto machine = std::make_shared<Machine>(*(m.second));
|
auto machine = std::make_shared<::Machine>(*(m.second));
|
||||||
machine->enabled = false;
|
machine->enabled = false;
|
||||||
newMachines[m.first] = machine;
|
newMachines[m.first] = machine;
|
||||||
}
|
}
|
||||||
@@ -204,7 +207,7 @@ void State::monitorMachinesFile()
|
|||||||
parseMachines("localhost " +
|
parseMachines("localhost " +
|
||||||
(settings.thisSystem == "x86_64-linux" ? "x86_64-linux,i686-linux" : settings.thisSystem.get())
|
(settings.thisSystem == "x86_64-linux" ? "x86_64-linux,i686-linux" : settings.thisSystem.get())
|
||||||
+ " - " + std::to_string(settings.maxBuildJobs) + " 1 "
|
+ " - " + std::to_string(settings.maxBuildJobs) + " 1 "
|
||||||
+ concatStringsSep(",", settings.systemFeatures.get()));
|
+ concatStringsSep(",", StoreConfig::getDefaultSystemFeatures()));
|
||||||
machinesReadyLock.unlock();
|
machinesReadyLock.unlock();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -273,17 +276,16 @@ void State::monitorMachinesFile()
|
|||||||
void State::clearBusy(Connection & conn, time_t stopTime)
|
void State::clearBusy(Connection & conn, time_t stopTime)
|
||||||
{
|
{
|
||||||
pqxx::work txn(conn);
|
pqxx::work txn(conn);
|
||||||
txn.exec_params0
|
txn.exec("update BuildSteps set busy = 0, status = $1, stopTime = $2 where busy != 0",
|
||||||
("update BuildSteps set busy = 0, status = $1, stopTime = $2 where busy != 0",
|
pqxx::params{(int) bsAborted,
|
||||||
(int) bsAborted,
|
stopTime != 0 ? std::make_optional(stopTime) : std::nullopt}).no_rows();
|
||||||
stopTime != 0 ? std::make_optional(stopTime) : std::nullopt);
|
|
||||||
txn.commit();
|
txn.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
unsigned int State::allocBuildStep(pqxx::work & txn, BuildID buildId)
|
unsigned int State::allocBuildStep(pqxx::work & txn, BuildID buildId)
|
||||||
{
|
{
|
||||||
auto res = txn.exec_params1("select max(stepnr) from BuildSteps where build = $1", buildId);
|
auto res = txn.exec("select max(stepnr) from BuildSteps where build = $1", buildId).one_row();
|
||||||
return res[0].is_null() ? 1 : res[0].as<int>() + 1;
|
return res[0].is_null() ? 1 : res[0].as<int>() + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,9 +296,8 @@ unsigned int State::createBuildStep(pqxx::work & txn, time_t startTime, BuildID
|
|||||||
restart:
|
restart:
|
||||||
auto stepNr = allocBuildStep(txn, buildId);
|
auto stepNr = allocBuildStep(txn, buildId);
|
||||||
|
|
||||||
auto r = txn.exec_params
|
auto r = txn.exec("insert into BuildSteps (build, stepnr, type, drvPath, busy, startTime, system, status, propagatedFrom, errorMsg, stopTime, machine) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) on conflict do nothing",
|
||||||
("insert into BuildSteps (build, stepnr, type, drvPath, busy, startTime, system, status, propagatedFrom, errorMsg, stopTime, machine) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) on conflict do nothing",
|
pqxx::params{buildId,
|
||||||
buildId,
|
|
||||||
stepNr,
|
stepNr,
|
||||||
0, // == build
|
0, // == build
|
||||||
localStore->printStorePath(step->drvPath),
|
localStore->printStorePath(step->drvPath),
|
||||||
@@ -307,14 +308,16 @@ unsigned int State::createBuildStep(pqxx::work & txn, time_t startTime, BuildID
|
|||||||
propagatedFrom != 0 ? std::make_optional(propagatedFrom) : std::nullopt, // internal::params
|
propagatedFrom != 0 ? std::make_optional(propagatedFrom) : std::nullopt, // internal::params
|
||||||
errorMsg != "" ? std::make_optional(errorMsg) : std::nullopt,
|
errorMsg != "" ? std::make_optional(errorMsg) : std::nullopt,
|
||||||
startTime != 0 && status != bsBusy ? std::make_optional(startTime) : std::nullopt,
|
startTime != 0 && status != bsBusy ? std::make_optional(startTime) : std::nullopt,
|
||||||
machine);
|
machine});
|
||||||
|
|
||||||
if (r.affected_rows() == 0) goto restart;
|
if (r.affected_rows() == 0) goto restart;
|
||||||
|
|
||||||
for (auto & [name, output] : step->drv->outputs)
|
for (auto & [name, output] : getDestStore()->queryPartialDerivationOutputMap(step->drvPath, &*localStore))
|
||||||
txn.exec_params0
|
txn.exec("insert into BuildStepOutputs (build, stepnr, name, path) values ($1, $2, $3, $4)",
|
||||||
("insert into BuildStepOutputs (build, stepnr, name, path) values ($1, $2, $3, $4)",
|
pqxx::params{buildId, stepNr, name,
|
||||||
buildId, stepNr, name, localStore->printStorePath(*output.path(*localStore, step->drv->name, name)));
|
output
|
||||||
|
? std::optional { localStore->printStorePath(*output)}
|
||||||
|
: std::nullopt}).no_rows();
|
||||||
|
|
||||||
if (status == bsBusy)
|
if (status == bsBusy)
|
||||||
txn.exec(fmt("notify step_started, '%d\t%d'", buildId, stepNr));
|
txn.exec(fmt("notify step_started, '%d\t%d'", buildId, stepNr));
|
||||||
@@ -325,11 +328,10 @@ unsigned int State::createBuildStep(pqxx::work & txn, time_t startTime, BuildID
|
|||||||
|
|
||||||
void State::updateBuildStep(pqxx::work & txn, BuildID buildId, unsigned int stepNr, StepState stepState)
|
void State::updateBuildStep(pqxx::work & txn, BuildID buildId, unsigned int stepNr, StepState stepState)
|
||||||
{
|
{
|
||||||
if (txn.exec_params
|
if (txn.exec("update BuildSteps set busy = $1 where build = $2 and stepnr = $3 and busy != 0 and status is null",
|
||||||
("update BuildSteps set busy = $1 where build = $2 and stepnr = $3 and busy != 0 and status is null",
|
pqxx::params{(int) stepState,
|
||||||
(int) stepState,
|
|
||||||
buildId,
|
buildId,
|
||||||
stepNr).affected_rows() != 1)
|
stepNr}).affected_rows() != 1)
|
||||||
throw Error("step %d of build %d is in an unexpected state", stepNr, buildId);
|
throw Error("step %d of build %d is in an unexpected state", stepNr, buildId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,44 +341,52 @@ void State::finishBuildStep(pqxx::work & txn, const RemoteResult & result,
|
|||||||
{
|
{
|
||||||
assert(result.startTime);
|
assert(result.startTime);
|
||||||
assert(result.stopTime);
|
assert(result.stopTime);
|
||||||
txn.exec_params0
|
txn.exec("update BuildSteps set busy = 0, status = $1, errorMsg = $4, startTime = $5, stopTime = $6, machine = $7, overhead = $8, timesBuilt = $9, isNonDeterministic = $10 where build = $2 and stepnr = $3",
|
||||||
("update BuildSteps set busy = 0, status = $1, errorMsg = $4, startTime = $5, stopTime = $6, machine = $7, overhead = $8, timesBuilt = $9, isNonDeterministic = $10 where build = $2 and stepnr = $3",
|
pqxx::params{(int) result.stepStatus, buildId, stepNr,
|
||||||
(int) result.stepStatus, buildId, stepNr,
|
|
||||||
result.errorMsg != "" ? std::make_optional(result.errorMsg) : std::nullopt,
|
result.errorMsg != "" ? std::make_optional(result.errorMsg) : std::nullopt,
|
||||||
result.startTime, result.stopTime,
|
result.startTime, result.stopTime,
|
||||||
machine != "" ? std::make_optional(machine) : std::nullopt,
|
machine != "" ? std::make_optional(machine) : std::nullopt,
|
||||||
result.overhead != 0 ? std::make_optional(result.overhead) : std::nullopt,
|
result.overhead != 0 ? std::make_optional(result.overhead) : std::nullopt,
|
||||||
result.timesBuilt > 0 ? std::make_optional(result.timesBuilt) : std::nullopt,
|
result.timesBuilt > 0 ? std::make_optional(result.timesBuilt) : std::nullopt,
|
||||||
result.timesBuilt > 1 ? std::make_optional(result.isNonDeterministic) : std::nullopt);
|
result.timesBuilt > 1 ? std::make_optional(result.isNonDeterministic) : std::nullopt}).no_rows();
|
||||||
assert(result.logFile.find('\t') == std::string::npos);
|
assert(result.logFile.find('\t') == std::string::npos);
|
||||||
txn.exec(fmt("notify step_finished, '%d\t%d\t%s'",
|
txn.exec(fmt("notify step_finished, '%d\t%d\t%s'",
|
||||||
buildId, stepNr, result.logFile));
|
buildId, stepNr, result.logFile));
|
||||||
|
|
||||||
|
if (result.stepStatus == bsSuccess) {
|
||||||
|
// Update the corresponding `BuildStepOutputs` row to add the output path
|
||||||
|
auto res = txn.exec("select drvPath from BuildSteps where build = $1 and stepnr = $2", pqxx::params{buildId, stepNr}).one_row();
|
||||||
|
assert(res.size());
|
||||||
|
StorePath drvPath = localStore->parseStorePath(res[0].as<std::string>());
|
||||||
|
// If we've finished building, all the paths should be known
|
||||||
|
for (auto & [name, output] : getDestStore()->queryDerivationOutputMap(drvPath, &*localStore))
|
||||||
|
txn.exec("update BuildStepOutputs set path = $4 where build = $1 and stepnr = $2 and name = $3",
|
||||||
|
pqxx::params{buildId, stepNr, name, localStore->printStorePath(output)}).no_rows();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
int State::createSubstitutionStep(pqxx::work & txn, time_t startTime, time_t stopTime,
|
int State::createSubstitutionStep(pqxx::work & txn, time_t startTime, time_t stopTime,
|
||||||
Build::ptr build, const StorePath & drvPath, const std::string & outputName, const StorePath & storePath)
|
Build::ptr build, const StorePath & drvPath, const nix::Derivation drv, const std::string & outputName, const StorePath & storePath)
|
||||||
{
|
{
|
||||||
restart:
|
restart:
|
||||||
auto stepNr = allocBuildStep(txn, build->id);
|
auto stepNr = allocBuildStep(txn, build->id);
|
||||||
|
|
||||||
auto r = txn.exec_params
|
auto r = txn.exec("insert into BuildSteps (build, stepnr, type, drvPath, busy, status, startTime, stopTime) values ($1, $2, $3, $4, $5, $6, $7, $8) on conflict do nothing",
|
||||||
("insert into BuildSteps (build, stepnr, type, drvPath, busy, status, startTime, stopTime) values ($1, $2, $3, $4, $5, $6, $7, $8) on conflict do nothing",
|
pqxx::params{build->id,
|
||||||
build->id,
|
|
||||||
stepNr,
|
stepNr,
|
||||||
1, // == substitution
|
1, // == substitution
|
||||||
(localStore->printStorePath(drvPath)),
|
(localStore->printStorePath(drvPath)),
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
startTime,
|
startTime,
|
||||||
stopTime);
|
stopTime});
|
||||||
|
|
||||||
if (r.affected_rows() == 0) goto restart;
|
if (r.affected_rows() == 0) goto restart;
|
||||||
|
|
||||||
txn.exec_params0
|
txn.exec("insert into BuildStepOutputs (build, stepnr, name, path) values ($1, $2, $3, $4)",
|
||||||
("insert into BuildStepOutputs (build, stepnr, name, path) values ($1, $2, $3, $4)",
|
pqxx::params{build->id, stepNr, outputName,
|
||||||
build->id, stepNr, outputName,
|
localStore->printStorePath(storePath)}).no_rows();
|
||||||
localStore->printStorePath(storePath));
|
|
||||||
|
|
||||||
return stepNr;
|
return stepNr;
|
||||||
}
|
}
|
||||||
@@ -443,49 +453,54 @@ void State::markSucceededBuild(pqxx::work & txn, Build::ptr build,
|
|||||||
{
|
{
|
||||||
if (build->finishedInDB) return;
|
if (build->finishedInDB) return;
|
||||||
|
|
||||||
if (txn.exec_params("select 1 from Builds where id = $1 and finished = 0", build->id).empty()) return;
|
if (txn.exec("select 1 from Builds where id = $1 and finished = 0", pqxx::params{build->id}).empty()) return;
|
||||||
|
|
||||||
txn.exec_params0
|
txn.exec("update Builds set finished = 1, buildStatus = $2, startTime = $3, stopTime = $4, size = $5, closureSize = $6, releaseName = $7, isCachedBuild = $8, notificationPendingSince = $4 where id = $1",
|
||||||
("update Builds set finished = 1, buildStatus = $2, startTime = $3, stopTime = $4, size = $5, closureSize = $6, releaseName = $7, isCachedBuild = $8, notificationPendingSince = $4 where id = $1",
|
pqxx::params{build->id,
|
||||||
build->id,
|
|
||||||
(int) (res.failed ? bsFailedWithOutput : bsSuccess),
|
(int) (res.failed ? bsFailedWithOutput : bsSuccess),
|
||||||
startTime,
|
startTime,
|
||||||
stopTime,
|
stopTime,
|
||||||
res.size,
|
res.size,
|
||||||
res.closureSize,
|
res.closureSize,
|
||||||
res.releaseName != "" ? std::make_optional(res.releaseName) : std::nullopt,
|
res.releaseName != "" ? std::make_optional(res.releaseName) : std::nullopt,
|
||||||
isCachedBuild ? 1 : 0);
|
isCachedBuild ? 1 : 0}).no_rows();
|
||||||
|
|
||||||
txn.exec_params0("delete from BuildProducts where build = $1", build->id);
|
for (auto & [outputName, outputPath] : res.outputs) {
|
||||||
|
txn.exec("update BuildOutputs set path = $3 where build = $1 and name = $2",
|
||||||
|
pqxx::params{build->id,
|
||||||
|
outputName,
|
||||||
|
localStore->printStorePath(outputPath)}
|
||||||
|
).no_rows();
|
||||||
|
}
|
||||||
|
|
||||||
|
txn.exec("delete from BuildProducts where build = $1", pqxx::params{build->id}).no_rows();
|
||||||
|
|
||||||
unsigned int productNr = 1;
|
unsigned int productNr = 1;
|
||||||
for (auto & product : res.products) {
|
for (auto & product : res.products) {
|
||||||
txn.exec_params0
|
txn.exec("insert into BuildProducts (build, productnr, type, subtype, fileSize, sha256hash, path, name, defaultPath) values ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
|
||||||
("insert into BuildProducts (build, productnr, type, subtype, fileSize, sha256hash, path, name, defaultPath) values ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
|
pqxx::params{build->id,
|
||||||
build->id,
|
|
||||||
productNr++,
|
productNr++,
|
||||||
product.type,
|
product.type,
|
||||||
product.subtype,
|
product.subtype,
|
||||||
product.fileSize ? std::make_optional(*product.fileSize) : std::nullopt,
|
product.fileSize ? std::make_optional(*product.fileSize) : std::nullopt,
|
||||||
product.sha256hash ? std::make_optional(product.sha256hash->to_string(Base16, false)) : std::nullopt,
|
product.sha256hash ? std::make_optional(product.sha256hash->to_string(HashFormat::Base16, false)) : std::nullopt,
|
||||||
product.path,
|
product.path,
|
||||||
product.name,
|
product.name,
|
||||||
product.defaultPath);
|
product.defaultPath}).no_rows();
|
||||||
}
|
}
|
||||||
|
|
||||||
txn.exec_params0("delete from BuildMetrics where build = $1", build->id);
|
txn.exec("delete from BuildMetrics where build = $1", pqxx::params{build->id}).no_rows();
|
||||||
|
|
||||||
for (auto & metric : res.metrics) {
|
for (auto & metric : res.metrics) {
|
||||||
txn.exec_params0
|
txn.exec("insert into BuildMetrics (build, name, unit, value, project, jobset, job, timestamp) values ($1, $2, $3, $4, $5, $6, $7, $8)",
|
||||||
("insert into BuildMetrics (build, name, unit, value, project, jobset, job, timestamp) values ($1, $2, $3, $4, $5, $6, $7, $8)",
|
pqxx::params{build->id,
|
||||||
build->id,
|
|
||||||
metric.second.name,
|
metric.second.name,
|
||||||
metric.second.unit != "" ? std::make_optional(metric.second.unit) : std::nullopt,
|
metric.second.unit != "" ? std::make_optional(metric.second.unit) : std::nullopt,
|
||||||
metric.second.value,
|
metric.second.value,
|
||||||
build->projectName,
|
build->projectName,
|
||||||
build->jobsetName,
|
build->jobsetName,
|
||||||
build->jobName,
|
build->jobName,
|
||||||
build->timestamp);
|
build->timestamp}).no_rows();
|
||||||
}
|
}
|
||||||
|
|
||||||
nrBuildsDone++;
|
nrBuildsDone++;
|
||||||
@@ -497,7 +512,7 @@ bool State::checkCachedFailure(Step::ptr step, Connection & conn)
|
|||||||
pqxx::work txn(conn);
|
pqxx::work txn(conn);
|
||||||
for (auto & i : step->drv->outputsAndOptPaths(*localStore))
|
for (auto & i : step->drv->outputsAndOptPaths(*localStore))
|
||||||
if (i.second.second)
|
if (i.second.second)
|
||||||
if (!txn.exec_params("select 1 from FailedPaths where path = $1", localStore->printStorePath(*i.second.second)).empty())
|
if (!txn.exec("select 1 from FailedPaths where path = $1", pqxx::params{localStore->printStorePath(*i.second.second)}).empty())
|
||||||
return true;
|
return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -522,12 +537,12 @@ void State::notifyBuildFinished(pqxx::work & txn, BuildID buildId,
|
|||||||
|
|
||||||
std::shared_ptr<PathLocks> State::acquireGlobalLock()
|
std::shared_ptr<PathLocks> State::acquireGlobalLock()
|
||||||
{
|
{
|
||||||
Path lockPath = hydraData + "/queue-runner/lock";
|
auto lockPath = std::filesystem::path(hydraData) / "queue-runner/lock";
|
||||||
|
|
||||||
createDirs(dirOf(lockPath));
|
createDirs(lockPath.parent_path());
|
||||||
|
|
||||||
auto lock = std::make_shared<PathLocks>();
|
auto lock = std::make_shared<PathLocks>();
|
||||||
if (!lock->lockPaths(PathSet({lockPath}), "", false)) return 0;
|
if (!lock->lockPaths({lockPath}, "", false)) return 0;
|
||||||
|
|
||||||
return lock;
|
return lock;
|
||||||
}
|
}
|
||||||
@@ -546,6 +561,7 @@ void State::dumpStatus(Connection & conn)
|
|||||||
{"nrActiveSteps", activeSteps_.lock()->size()},
|
{"nrActiveSteps", activeSteps_.lock()->size()},
|
||||||
{"nrStepsBuilding", nrStepsBuilding.load()},
|
{"nrStepsBuilding", nrStepsBuilding.load()},
|
||||||
{"nrStepsCopyingTo", nrStepsCopyingTo.load()},
|
{"nrStepsCopyingTo", nrStepsCopyingTo.load()},
|
||||||
|
{"nrStepsWaitingForDownloadSlot", nrStepsWaitingForDownloadSlot.load()},
|
||||||
{"nrStepsCopyingFrom", nrStepsCopyingFrom.load()},
|
{"nrStepsCopyingFrom", nrStepsCopyingFrom.load()},
|
||||||
{"nrStepsWaiting", nrStepsWaiting.load()},
|
{"nrStepsWaiting", nrStepsWaiting.load()},
|
||||||
{"nrUnsupportedSteps", nrUnsupportedSteps.load()},
|
{"nrUnsupportedSteps", nrUnsupportedSteps.load()},
|
||||||
@@ -587,6 +603,7 @@ void State::dumpStatus(Connection & conn)
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
auto machines_json = json::object();
|
||||||
auto machines_(machines.lock());
|
auto machines_(machines.lock());
|
||||||
for (auto & i : *machines_) {
|
for (auto & i : *machines_) {
|
||||||
auto & m(i.second);
|
auto & m(i.second);
|
||||||
@@ -613,8 +630,9 @@ void State::dumpStatus(Connection & conn)
|
|||||||
machine["avgStepTime"] = (float) s->totalStepTime / s->nrStepsDone;
|
machine["avgStepTime"] = (float) s->totalStepTime / s->nrStepsDone;
|
||||||
machine["avgStepBuildTime"] = (float) s->totalStepBuildTime / s->nrStepsDone;
|
machine["avgStepBuildTime"] = (float) s->totalStepBuildTime / s->nrStepsDone;
|
||||||
}
|
}
|
||||||
statusJson["machines"][m->sshName] = machine;
|
machines_json[m->storeUri.render()] = machine;
|
||||||
}
|
}
|
||||||
|
statusJson["machines"] = machines_json;
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -673,6 +691,7 @@ void State::dumpStatus(Connection & conn)
|
|||||||
: 0.0},
|
: 0.0},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#if NIX_WITH_S3_SUPPORT
|
||||||
auto s3Store = dynamic_cast<S3BinaryCacheStore *>(&*store);
|
auto s3Store = dynamic_cast<S3BinaryCacheStore *>(&*store);
|
||||||
if (s3Store) {
|
if (s3Store) {
|
||||||
auto & s3Stats = s3Store->getS3Stats();
|
auto & s3Stats = s3Store->getS3Stats();
|
||||||
@@ -698,14 +717,15 @@ void State::dumpStatus(Connection & conn)
|
|||||||
+ s3Stats.getBytes / (1024.0 * 1024.0 * 1024.0) * 0.09},
|
+ s3Stats.getBytes / (1024.0 * 1024.0 * 1024.0) * 0.09},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto mc = startDbUpdate();
|
auto mc = startDbUpdate();
|
||||||
pqxx::work txn(conn);
|
pqxx::work txn(conn);
|
||||||
// FIXME: use PostgreSQL 9.5 upsert.
|
// FIXME: use PostgreSQL 9.5 upsert.
|
||||||
txn.exec("delete from SystemStatus where what = 'queue-runner'");
|
txn.exec("delete from SystemStatus where what = 'queue-runner'").no_rows();
|
||||||
txn.exec_params0("insert into SystemStatus values ('queue-runner', $1)", statusJson.dump());
|
txn.exec("insert into SystemStatus values ('queue-runner', $1)", pqxx::params{statusJson.dump()}).no_rows();
|
||||||
txn.exec("notify status_dumped");
|
txn.exec("notify status_dumped");
|
||||||
txn.commit();
|
txn.commit();
|
||||||
}
|
}
|
||||||
@@ -770,7 +790,7 @@ void State::unlock()
|
|||||||
|
|
||||||
{
|
{
|
||||||
pqxx::work txn(*conn);
|
pqxx::work txn(*conn);
|
||||||
txn.exec("delete from SystemStatus where what = 'queue-runner'");
|
txn.exec("delete from SystemStatus where what = 'queue-runner'").no_rows();
|
||||||
txn.commit();
|
txn.commit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -800,7 +820,7 @@ void State::run(BuildID buildOne)
|
|||||||
<< metricsAddr << "/metrics (port " << exposerPort << ")"
|
<< metricsAddr << "/metrics (port " << exposerPort << ")"
|
||||||
<< std::endl;
|
<< std::endl;
|
||||||
|
|
||||||
Store::Params localParams;
|
Store::Config::Params localParams;
|
||||||
localParams["max-connections"] = "16";
|
localParams["max-connections"] = "16";
|
||||||
localParams["max-connection-age"] = "600";
|
localParams["max-connection-age"] = "600";
|
||||||
localStore = openStore(getEnv("NIX_REMOTE").value_or(""), localParams);
|
localStore = openStore(getEnv("NIX_REMOTE").value_or(""), localParams);
|
||||||
@@ -848,11 +868,10 @@ void State::run(BuildID buildOne)
|
|||||||
pqxx::work txn(*conn);
|
pqxx::work txn(*conn);
|
||||||
for (auto & step : steps) {
|
for (auto & step : steps) {
|
||||||
printMsg(lvlError, "cleaning orphaned step %d of build %d", step.second, step.first);
|
printMsg(lvlError, "cleaning orphaned step %d of build %d", step.second, step.first);
|
||||||
txn.exec_params0
|
txn.exec("update BuildSteps set busy = 0, status = $1 where build = $2 and stepnr = $3 and busy != 0",
|
||||||
("update BuildSteps set busy = 0, status = $1 where build = $2 and stepnr = $3 and busy != 0",
|
pqxx::params{(int) bsAborted,
|
||||||
(int) bsAborted,
|
|
||||||
step.first,
|
step.first,
|
||||||
step.second);
|
step.second}).no_rows();
|
||||||
}
|
}
|
||||||
txn.commit();
|
txn.commit();
|
||||||
} catch (std::exception & e) {
|
} catch (std::exception & e) {
|
||||||
@@ -882,10 +901,17 @@ void State::run(BuildID buildOne)
|
|||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
auto conn(dbPool.get());
|
auto conn(dbPool.get());
|
||||||
receiver dumpStatus_(*conn, "dump_status");
|
try {
|
||||||
while (true) {
|
receiver dumpStatus_(*conn, "dump_status");
|
||||||
conn->await_notification();
|
while (true) {
|
||||||
dumpStatus(*conn);
|
conn->await_notification();
|
||||||
|
dumpStatus(*conn);
|
||||||
|
}
|
||||||
|
} catch (pqxx::broken_connection & connEx) {
|
||||||
|
printMsg(lvlError, "main thread: %s", connEx.what());
|
||||||
|
printMsg(lvlError, "main thread: Reconnecting in 10s");
|
||||||
|
conn.markBad();
|
||||||
|
sleep(10);
|
||||||
}
|
}
|
||||||
} catch (std::exception & e) {
|
} catch (std::exception & e) {
|
||||||
printMsg(lvlError, "main thread: %s", e.what());
|
printMsg(lvlError, "main thread: %s", e.what());
|
||||||
|
|||||||
24
src/hydra-queue-runner/meson.build
Normal file
24
src/hydra-queue-runner/meson.build
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
srcs = files(
|
||||||
|
'builder.cc',
|
||||||
|
'build-remote.cc',
|
||||||
|
'build-result.cc',
|
||||||
|
'dispatcher.cc',
|
||||||
|
'hydra-queue-runner.cc',
|
||||||
|
'nar-extractor.cc',
|
||||||
|
'queue-monitor.cc',
|
||||||
|
)
|
||||||
|
|
||||||
|
hydra_queue_runner = executable('hydra-queue-runner',
|
||||||
|
'hydra-queue-runner.cc',
|
||||||
|
srcs,
|
||||||
|
dependencies: [
|
||||||
|
libhydra_dep,
|
||||||
|
nix_util_dep,
|
||||||
|
nix_store_dep,
|
||||||
|
nix_main_dep,
|
||||||
|
pqxx_dep,
|
||||||
|
prom_cpp_core_dep,
|
||||||
|
prom_cpp_pull_dep,
|
||||||
|
],
|
||||||
|
install: true,
|
||||||
|
)
|
||||||
@@ -1,12 +1,51 @@
|
|||||||
#include "nar-extractor.hh"
|
#include "nar-extractor.hh"
|
||||||
|
|
||||||
#include "archive.hh"
|
#include <nix/util/archive.hh>
|
||||||
|
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
using namespace nix;
|
using namespace nix;
|
||||||
|
|
||||||
struct Extractor : ParseSink
|
|
||||||
|
struct NarMemberConstructor : CreateRegularFileSink
|
||||||
|
{
|
||||||
|
NarMemberData & curMember;
|
||||||
|
|
||||||
|
HashSink hashSink = HashSink { HashAlgorithm::SHA256 };
|
||||||
|
|
||||||
|
std::optional<uint64_t> expectedSize;
|
||||||
|
|
||||||
|
NarMemberConstructor(NarMemberData & curMember)
|
||||||
|
: curMember(curMember)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
void isExecutable() override
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void preallocateContents(uint64_t size) override
|
||||||
|
{
|
||||||
|
expectedSize = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
void operator () (std::string_view data) override
|
||||||
|
{
|
||||||
|
assert(expectedSize);
|
||||||
|
*curMember.fileSize += data.size();
|
||||||
|
hashSink(data);
|
||||||
|
if (curMember.contents) {
|
||||||
|
curMember.contents->append(data);
|
||||||
|
}
|
||||||
|
assert(curMember.fileSize <= expectedSize);
|
||||||
|
if (curMember.fileSize == expectedSize) {
|
||||||
|
auto [hash, len] = hashSink.finish();
|
||||||
|
assert(curMember.fileSize == len);
|
||||||
|
curMember.sha256 = hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Extractor : FileSystemObjectSink
|
||||||
{
|
{
|
||||||
std::unordered_set<Path> filesToKeep {
|
std::unordered_set<Path> filesToKeep {
|
||||||
"/nix-support/hydra-build-products",
|
"/nix-support/hydra-build-products",
|
||||||
@@ -15,58 +54,40 @@ struct Extractor : ParseSink
|
|||||||
};
|
};
|
||||||
|
|
||||||
NarMemberDatas & members;
|
NarMemberDatas & members;
|
||||||
NarMemberData * curMember = nullptr;
|
std::filesystem::path prefix;
|
||||||
Path prefix;
|
|
||||||
|
Path toKey(const CanonPath & path)
|
||||||
|
{
|
||||||
|
std::filesystem::path p = prefix;
|
||||||
|
// Conditional to avoid trailing slash
|
||||||
|
if (!path.isRoot()) p /= path.rel();
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
Extractor(NarMemberDatas & members, const Path & prefix)
|
Extractor(NarMemberDatas & members, const Path & prefix)
|
||||||
: members(members), prefix(prefix)
|
: members(members), prefix(prefix)
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
void createDirectory(const Path & path) override
|
void createDirectory(const CanonPath & path) override
|
||||||
{
|
{
|
||||||
members.insert_or_assign(prefix + path, NarMemberData { .type = FSAccessor::Type::tDirectory });
|
members.insert_or_assign(toKey(path), NarMemberData { .type = SourceAccessor::Type::tDirectory });
|
||||||
}
|
}
|
||||||
|
|
||||||
void createRegularFile(const Path & path) override
|
void createRegularFile(const CanonPath & path, std::function<void(CreateRegularFileSink &)> func) override
|
||||||
{
|
{
|
||||||
curMember = &members.insert_or_assign(prefix + path, NarMemberData {
|
NarMemberConstructor nmc {
|
||||||
.type = FSAccessor::Type::tRegular,
|
members.insert_or_assign(toKey(path), NarMemberData {
|
||||||
.fileSize = 0,
|
.type = SourceAccessor::Type::tRegular,
|
||||||
.contents = filesToKeep.count(path) ? std::optional("") : std::nullopt,
|
.fileSize = 0,
|
||||||
}).first->second;
|
.contents = filesToKeep.count(path.abs()) ? std::optional("") : std::nullopt,
|
||||||
|
}).first->second,
|
||||||
|
};
|
||||||
|
func(nmc);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<uint64_t> expectedSize;
|
void createSymlink(const CanonPath & path, const std::string & target) override
|
||||||
std::unique_ptr<HashSink> hashSink;
|
|
||||||
|
|
||||||
void preallocateContents(uint64_t size) override
|
|
||||||
{
|
{
|
||||||
expectedSize = size;
|
members.insert_or_assign(toKey(path), NarMemberData { .type = SourceAccessor::Type::tSymlink });
|
||||||
hashSink = std::make_unique<HashSink>(htSHA256);
|
|
||||||
}
|
|
||||||
|
|
||||||
void receiveContents(std::string_view data) override
|
|
||||||
{
|
|
||||||
assert(expectedSize);
|
|
||||||
assert(curMember);
|
|
||||||
assert(hashSink);
|
|
||||||
*curMember->fileSize += data.size();
|
|
||||||
(*hashSink)(data);
|
|
||||||
if (curMember->contents) {
|
|
||||||
curMember->contents->append(data);
|
|
||||||
}
|
|
||||||
assert(curMember->fileSize <= expectedSize);
|
|
||||||
if (curMember->fileSize == expectedSize) {
|
|
||||||
auto [hash, len] = hashSink->finish();
|
|
||||||
assert(curMember->fileSize == len);
|
|
||||||
curMember->sha256 = hash;
|
|
||||||
hashSink.reset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void createSymlink(const Path & path, const std::string & target) override
|
|
||||||
{
|
|
||||||
members.insert_or_assign(prefix + path, NarMemberData { .type = FSAccessor::Type::tSymlink });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "fs-accessor.hh"
|
#include <nix/util/source-accessor.hh>
|
||||||
#include "types.hh"
|
#include <nix/util/types.hh>
|
||||||
#include "serialise.hh"
|
#include <nix/util/serialise.hh>
|
||||||
#include "hash.hh"
|
#include <nix/util/hash.hh>
|
||||||
|
|
||||||
struct NarMemberData
|
struct NarMemberData
|
||||||
{
|
{
|
||||||
nix::FSAccessor::Type type;
|
nix::SourceAccessor::Type type;
|
||||||
std::optional<uint64_t> fileSize;
|
std::optional<uint64_t> fileSize;
|
||||||
std::optional<std::string> contents;
|
std::optional<std::string> contents;
|
||||||
std::optional<nix::Hash> sha256;
|
std::optional<nix::Hash> sha256;
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
#include "state.hh"
|
#include "state.hh"
|
||||||
#include "hydra-build-result.hh"
|
#include "hydra-build-result.hh"
|
||||||
#include "globals.hh"
|
#include <nix/store/derived-path.hh>
|
||||||
|
#include <nix/store/globals.hh>
|
||||||
|
#include <nix/store/parsed-derivations.hh>
|
||||||
|
#include <nix/util/thread-pool.hh>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <signal.h>
|
||||||
|
|
||||||
using namespace nix;
|
using namespace nix;
|
||||||
|
|
||||||
@@ -10,8 +14,14 @@ using namespace nix;
|
|||||||
void State::queueMonitor()
|
void State::queueMonitor()
|
||||||
{
|
{
|
||||||
while (true) {
|
while (true) {
|
||||||
|
auto conn(dbPool.get());
|
||||||
try {
|
try {
|
||||||
queueMonitorLoop();
|
queueMonitorLoop(*conn);
|
||||||
|
} catch (pqxx::broken_connection & e) {
|
||||||
|
printMsg(lvlError, "queue monitor: %s", e.what());
|
||||||
|
printMsg(lvlError, "queue monitor: Reconnecting in 10s");
|
||||||
|
conn.markBad();
|
||||||
|
sleep(10);
|
||||||
} catch (std::exception & e) {
|
} catch (std::exception & e) {
|
||||||
printError("queue monitor: %s", e.what());
|
printError("queue monitor: %s", e.what());
|
||||||
sleep(10); // probably a DB problem, so don't retry right away
|
sleep(10); // probably a DB problem, so don't retry right away
|
||||||
@@ -20,53 +30,58 @@ void State::queueMonitor()
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void State::queueMonitorLoop()
|
void State::queueMonitorLoop(Connection & conn)
|
||||||
{
|
{
|
||||||
auto conn(dbPool.get());
|
receiver buildsAdded(conn, "builds_added");
|
||||||
|
receiver buildsRestarted(conn, "builds_restarted");
|
||||||
receiver buildsAdded(*conn, "builds_added");
|
receiver buildsCancelled(conn, "builds_cancelled");
|
||||||
receiver buildsRestarted(*conn, "builds_restarted");
|
receiver buildsDeleted(conn, "builds_deleted");
|
||||||
receiver buildsCancelled(*conn, "builds_cancelled");
|
receiver buildsBumped(conn, "builds_bumped");
|
||||||
receiver buildsDeleted(*conn, "builds_deleted");
|
receiver jobsetSharesChanged(conn, "jobset_shares_changed");
|
||||||
receiver buildsBumped(*conn, "builds_bumped");
|
|
||||||
receiver jobsetSharesChanged(*conn, "jobset_shares_changed");
|
|
||||||
|
|
||||||
auto destStore = getDestStore();
|
auto destStore = getDestStore();
|
||||||
|
|
||||||
unsigned int lastBuildId = 0;
|
|
||||||
|
|
||||||
bool quit = false;
|
bool quit = false;
|
||||||
while (!quit) {
|
while (!quit) {
|
||||||
|
auto t_before_work = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
localStore->clearPathInfoCache();
|
localStore->clearPathInfoCache();
|
||||||
|
|
||||||
bool done = getQueuedBuilds(*conn, destStore, lastBuildId);
|
bool done = getQueuedBuilds(conn, destStore);
|
||||||
|
|
||||||
if (buildOne && buildOneDone) quit = true;
|
if (buildOne && buildOneDone) quit = true;
|
||||||
|
|
||||||
|
auto t_after_work = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
|
prom.queue_monitor_time_spent_running.Increment(
|
||||||
|
std::chrono::duration_cast<std::chrono::microseconds>(t_after_work - t_before_work).count());
|
||||||
|
|
||||||
/* Sleep until we get notification from the database about an
|
/* Sleep until we get notification from the database about an
|
||||||
event. */
|
event. */
|
||||||
if (done && !quit) {
|
if (done && !quit) {
|
||||||
conn->await_notification();
|
conn.await_notification();
|
||||||
nrQueueWakeups++;
|
nrQueueWakeups++;
|
||||||
} else
|
} else
|
||||||
conn->get_notifs();
|
conn.get_notifs();
|
||||||
|
|
||||||
if (auto lowestId = buildsAdded.get()) {
|
if (auto lowestId = buildsAdded.get()) {
|
||||||
lastBuildId = std::min(lastBuildId, static_cast<unsigned>(std::stoul(*lowestId) - 1));
|
|
||||||
printMsg(lvlTalkative, "got notification: new builds added to the queue");
|
printMsg(lvlTalkative, "got notification: new builds added to the queue");
|
||||||
}
|
}
|
||||||
if (buildsRestarted.get()) {
|
if (buildsRestarted.get()) {
|
||||||
printMsg(lvlTalkative, "got notification: builds restarted");
|
printMsg(lvlTalkative, "got notification: builds restarted");
|
||||||
lastBuildId = 0; // check all builds
|
|
||||||
}
|
}
|
||||||
if (buildsCancelled.get() || buildsDeleted.get() || buildsBumped.get()) {
|
if (buildsCancelled.get() || buildsDeleted.get() || buildsBumped.get()) {
|
||||||
printMsg(lvlTalkative, "got notification: builds cancelled or bumped");
|
printMsg(lvlTalkative, "got notification: builds cancelled or bumped");
|
||||||
processQueueChange(*conn);
|
processQueueChange(conn);
|
||||||
}
|
}
|
||||||
if (jobsetSharesChanged.get()) {
|
if (jobsetSharesChanged.get()) {
|
||||||
printMsg(lvlTalkative, "got notification: jobset shares changed");
|
printMsg(lvlTalkative, "got notification: jobset shares changed");
|
||||||
processJobsetSharesChange(*conn);
|
processJobsetSharesChange(conn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto t_after_sleep = std::chrono::steady_clock::now();
|
||||||
|
prom.queue_monitor_time_spent_waiting.Increment(
|
||||||
|
std::chrono::duration_cast<std::chrono::microseconds>(t_after_sleep - t_after_work).count());
|
||||||
}
|
}
|
||||||
|
|
||||||
exit(0);
|
exit(0);
|
||||||
@@ -80,39 +95,31 @@ struct PreviousFailure : public std::exception {
|
|||||||
|
|
||||||
|
|
||||||
bool State::getQueuedBuilds(Connection & conn,
|
bool State::getQueuedBuilds(Connection & conn,
|
||||||
ref<Store> destStore, unsigned int & lastBuildId)
|
ref<Store> destStore)
|
||||||
{
|
{
|
||||||
prom.queue_checks_started.Increment();
|
prom.queue_checks_started.Increment();
|
||||||
|
|
||||||
printInfo("checking the queue for builds > %d...", lastBuildId);
|
printInfo("checking the queue for builds...");
|
||||||
|
|
||||||
/* Grab the queued builds from the database, but don't process
|
/* Grab the queued builds from the database, but don't process
|
||||||
them yet (since we don't want a long-running transaction). */
|
them yet (since we don't want a long-running transaction). */
|
||||||
std::vector<BuildID> newIDs;
|
std::vector<BuildID> newIDs;
|
||||||
std::map<BuildID, Build::ptr> newBuildsByID;
|
std::unordered_map<BuildID, Build::ptr> newBuildsByID;
|
||||||
std::multimap<StorePath, BuildID> newBuildsByPath;
|
std::multimap<StorePath, BuildID> newBuildsByPath;
|
||||||
|
|
||||||
unsigned int newLastBuildId = lastBuildId;
|
|
||||||
|
|
||||||
{
|
{
|
||||||
pqxx::work txn(conn);
|
pqxx::work txn(conn);
|
||||||
|
|
||||||
auto res = txn.exec_params
|
auto res = txn.exec("select builds.id, builds.jobset_id, jobsets.project as project, "
|
||||||
("select builds.id, builds.jobset_id, jobsets.project as project, "
|
|
||||||
"jobsets.name as jobset, job, drvPath, maxsilent, timeout, timestamp, "
|
"jobsets.name as jobset, job, drvPath, maxsilent, timeout, timestamp, "
|
||||||
"globalPriority, priority from Builds "
|
"globalPriority, priority from Builds "
|
||||||
"inner join jobsets on builds.jobset_id = jobsets.id "
|
"inner join jobsets on builds.jobset_id = jobsets.id "
|
||||||
"where builds.id > $1 and finished = 0 order by globalPriority desc, builds.id",
|
"where finished = 0 order by globalPriority desc, random()");
|
||||||
lastBuildId);
|
|
||||||
|
|
||||||
for (auto const & row : res) {
|
for (auto const & row : res) {
|
||||||
auto builds_(builds.lock());
|
auto builds_(builds.lock());
|
||||||
BuildID id = row["id"].as<BuildID>();
|
BuildID id = row["id"].as<BuildID>();
|
||||||
if (buildOne && id != buildOne) continue;
|
if (buildOne && id != buildOne) continue;
|
||||||
if (id > newLastBuildId) {
|
|
||||||
newLastBuildId = id;
|
|
||||||
prom.queue_max_id.Set(id);
|
|
||||||
}
|
|
||||||
if (builds_->count(id)) continue;
|
if (builds_->count(id)) continue;
|
||||||
|
|
||||||
auto build = std::make_shared<Build>(
|
auto build = std::make_shared<Build>(
|
||||||
@@ -152,11 +159,10 @@ bool State::getQueuedBuilds(Connection & conn,
|
|||||||
if (!build->finishedInDB) {
|
if (!build->finishedInDB) {
|
||||||
auto mc = startDbUpdate();
|
auto mc = startDbUpdate();
|
||||||
pqxx::work txn(conn);
|
pqxx::work txn(conn);
|
||||||
txn.exec_params0
|
txn.exec("update Builds set finished = 1, buildStatus = $2, startTime = $3, stopTime = $3 where id = $1 and finished = 0",
|
||||||
("update Builds set finished = 1, buildStatus = $2, startTime = $3, stopTime = $3 where id = $1 and finished = 0",
|
pqxx::params{build->id,
|
||||||
build->id,
|
|
||||||
(int) bsAborted,
|
(int) bsAborted,
|
||||||
time(0));
|
time(0)}).no_rows();
|
||||||
txn.commit();
|
txn.commit();
|
||||||
build->finishedInDB = true;
|
build->finishedInDB = true;
|
||||||
nrBuildsDone++;
|
nrBuildsDone++;
|
||||||
@@ -186,32 +192,33 @@ bool State::getQueuedBuilds(Connection & conn,
|
|||||||
derivation path, then by output path. */
|
derivation path, then by output path. */
|
||||||
BuildID propagatedFrom = 0;
|
BuildID propagatedFrom = 0;
|
||||||
|
|
||||||
auto res = txn.exec_params1
|
auto res = txn.exec("select max(build) from BuildSteps where drvPath = $1 and startTime != 0 and stopTime != 0 and status = 1",
|
||||||
("select max(build) from BuildSteps where drvPath = $1 and startTime != 0 and stopTime != 0 and status = 1",
|
pqxx::params{localStore->printStorePath(ex.step->drvPath)}).one_row();
|
||||||
localStore->printStorePath(ex.step->drvPath));
|
|
||||||
if (!res[0].is_null()) propagatedFrom = res[0].as<BuildID>();
|
if (!res[0].is_null()) propagatedFrom = res[0].as<BuildID>();
|
||||||
|
|
||||||
if (!propagatedFrom) {
|
if (!propagatedFrom) {
|
||||||
for (auto & i : ex.step->drv->outputsAndOptPaths(*localStore)) {
|
for (auto & [outputName, optOutputPath] : destStore->queryPartialDerivationOutputMap(ex.step->drvPath, &*localStore)) {
|
||||||
if (i.second.second) {
|
constexpr std::string_view common = "select max(s.build) from BuildSteps s join BuildStepOutputs o on s.build = o.build where startTime != 0 and stopTime != 0 and status = 1";
|
||||||
auto res = txn.exec_params
|
auto res = optOutputPath
|
||||||
("select max(s.build) from BuildSteps s join BuildStepOutputs o on s.build = o.build where path = $1 and startTime != 0 and stopTime != 0 and status = 1",
|
? txn.exec(
|
||||||
localStore->printStorePath(*i.second.second));
|
std::string { common } + " and path = $1",
|
||||||
if (!res[0][0].is_null()) {
|
pqxx::params{localStore->printStorePath(*optOutputPath)})
|
||||||
propagatedFrom = res[0][0].as<BuildID>();
|
: txn.exec(
|
||||||
break;
|
std::string { common } + " and drvPath = $1 and name = $2",
|
||||||
}
|
pqxx::params{localStore->printStorePath(ex.step->drvPath), outputName});
|
||||||
|
if (!res[0][0].is_null()) {
|
||||||
|
propagatedFrom = res[0][0].as<BuildID>();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createBuildStep(txn, 0, build->id, ex.step, "", bsCachedFailure, "", propagatedFrom);
|
createBuildStep(txn, 0, build->id, ex.step, "", bsCachedFailure, "", propagatedFrom);
|
||||||
txn.exec_params
|
txn.exec("update Builds set finished = 1, buildStatus = $2, startTime = $3, stopTime = $3, isCachedBuild = 1, notificationPendingSince = $3 "
|
||||||
("update Builds set finished = 1, buildStatus = $2, startTime = $3, stopTime = $3, isCachedBuild = 1, notificationPendingSince = $3 "
|
|
||||||
"where id = $1 and finished = 0",
|
"where id = $1 and finished = 0",
|
||||||
build->id,
|
pqxx::params{build->id,
|
||||||
(int) (ex.step->drvPath == build->drvPath ? bsFailed : bsDepFailed),
|
(int) (ex.step->drvPath == build->drvPath ? bsFailed : bsDepFailed),
|
||||||
time(0));
|
time(0)}).no_rows();
|
||||||
notifyBuildFinished(txn, build->id, {});
|
notifyBuildFinished(txn, build->id, {});
|
||||||
txn.commit();
|
txn.commit();
|
||||||
build->finishedInDB = true;
|
build->finishedInDB = true;
|
||||||
@@ -236,12 +243,10 @@ bool State::getQueuedBuilds(Connection & conn,
|
|||||||
/* If we didn't get a step, it means the step's outputs are
|
/* If we didn't get a step, it means the step's outputs are
|
||||||
all valid. So we mark this as a finished, cached build. */
|
all valid. So we mark this as a finished, cached build. */
|
||||||
if (!step) {
|
if (!step) {
|
||||||
auto drv = localStore->readDerivation(build->drvPath);
|
BuildOutput res = getBuildOutputCached(conn, destStore, build->drvPath);
|
||||||
BuildOutput res = getBuildOutputCached(conn, destStore, drv);
|
|
||||||
|
|
||||||
for (auto & i : drv.outputsAndOptPaths(*localStore))
|
for (auto & i : destStore->queryDerivationOutputMap(build->drvPath, &*localStore))
|
||||||
if (i.second.second)
|
addRoot(i.second);
|
||||||
addRoot(*i.second.second);
|
|
||||||
|
|
||||||
{
|
{
|
||||||
auto mc = startDbUpdate();
|
auto mc = startDbUpdate();
|
||||||
@@ -292,7 +297,7 @@ bool State::getQueuedBuilds(Connection & conn,
|
|||||||
try {
|
try {
|
||||||
createBuild(build);
|
createBuild(build);
|
||||||
} catch (Error & e) {
|
} catch (Error & e) {
|
||||||
e.addTrace({}, hintfmt("while loading build %d: ", build->id));
|
e.addTrace({}, HintFmt("while loading build %d: ", build->id));
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,15 +317,13 @@ bool State::getQueuedBuilds(Connection & conn,
|
|||||||
|
|
||||||
/* Stop after a certain time to allow priority bumps to be
|
/* Stop after a certain time to allow priority bumps to be
|
||||||
processed. */
|
processed. */
|
||||||
if (std::chrono::system_clock::now() > start + std::chrono::seconds(600)) {
|
if (std::chrono::system_clock::now() > start + std::chrono::seconds(60)) {
|
||||||
prom.queue_checks_early_exits.Increment();
|
prom.queue_checks_early_exits.Increment();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prom.queue_checks_finished.Increment();
|
prom.queue_checks_finished.Increment();
|
||||||
|
|
||||||
lastBuildId = newBuildsByID.empty() ? newLastBuildId : newBuildsByID.begin()->first - 1;
|
|
||||||
return newBuildsByID.empty();
|
return newBuildsByID.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,6 +402,34 @@ void State::processQueueChange(Connection & conn)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::map<DrvOutput, std::optional<StorePath>> State::getMissingRemotePaths(
|
||||||
|
ref<Store> destStore,
|
||||||
|
const std::map<DrvOutput, std::optional<StorePath>> & paths)
|
||||||
|
{
|
||||||
|
Sync<std::map<DrvOutput, std::optional<StorePath>>> missing_;
|
||||||
|
ThreadPool tp;
|
||||||
|
|
||||||
|
for (auto & [output, maybeOutputPath] : paths) {
|
||||||
|
if (!maybeOutputPath) {
|
||||||
|
auto missing(missing_.lock());
|
||||||
|
missing->insert({output, maybeOutputPath});
|
||||||
|
} else {
|
||||||
|
tp.enqueue([&] {
|
||||||
|
if (!destStore->isValidPath(*maybeOutputPath)) {
|
||||||
|
auto missing(missing_.lock());
|
||||||
|
missing->insert({output, maybeOutputPath});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tp.process();
|
||||||
|
|
||||||
|
auto missing(missing_.lock());
|
||||||
|
return *missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Step::ptr State::createStep(ref<Store> destStore,
|
Step::ptr State::createStep(ref<Store> destStore,
|
||||||
Connection & conn, Build::ptr build, const StorePath & drvPath,
|
Connection & conn, Build::ptr build, const StorePath & drvPath,
|
||||||
Build::ptr referringBuild, Step::ptr referringStep, std::set<StorePath> & finishedDrvs,
|
Build::ptr referringBuild, Step::ptr referringStep, std::set<StorePath> & finishedDrvs,
|
||||||
@@ -457,17 +488,24 @@ Step::ptr State::createStep(ref<Store> destStore,
|
|||||||
it's not runnable yet, and other threads won't make it
|
it's not runnable yet, and other threads won't make it
|
||||||
runnable while step->created == false. */
|
runnable while step->created == false. */
|
||||||
step->drv = std::make_unique<Derivation>(localStore->readDerivation(drvPath));
|
step->drv = std::make_unique<Derivation>(localStore->readDerivation(drvPath));
|
||||||
step->parsedDrv = std::make_unique<ParsedDerivation>(drvPath, *step->drv);
|
DerivationOptions<nix::SingleDerivedPath> drvOptions;
|
||||||
|
try {
|
||||||
|
drvOptions = derivationOptionsFromStructuredAttrs(
|
||||||
|
*localStore,
|
||||||
|
step->drv->inputDrvs,
|
||||||
|
step->drv->env,
|
||||||
|
get(step->drv->structuredAttrs));
|
||||||
|
} catch (Error & e) {
|
||||||
|
e.addTrace({}, "while parsing derivation '%s'", localStore->printStorePath(drvPath));
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
step->preferLocalBuild = step->parsedDrv->willBuildLocally(*localStore);
|
step->preferLocalBuild = drvOptions.willBuildLocally(*localStore, *step->drv);
|
||||||
step->isDeterministic = getOr(step->drv->env, "isDetermistic", "0") == "1";
|
step->isDeterministic = getOr(step->drv->env, "isDetermistic", "0") == "1";
|
||||||
|
|
||||||
step->systemType = step->drv->platform;
|
step->systemType = step->drv->platform;
|
||||||
{
|
{
|
||||||
auto i = step->drv->env.find("requiredSystemFeatures");
|
StringSet features = step->requiredSystemFeatures = drvOptions.getRequiredSystemFeatures(*step->drv);
|
||||||
StringSet features;
|
|
||||||
if (i != step->drv->env.end())
|
|
||||||
features = step->requiredSystemFeatures = tokenizeString<std::set<std::string>>(i->second);
|
|
||||||
if (step->preferLocalBuild)
|
if (step->preferLocalBuild)
|
||||||
features.insert("local");
|
features.insert("local");
|
||||||
if (!features.empty()) {
|
if (!features.empty()) {
|
||||||
@@ -481,26 +519,40 @@ Step::ptr State::createStep(ref<Store> destStore,
|
|||||||
throw PreviousFailure{step};
|
throw PreviousFailure{step};
|
||||||
|
|
||||||
/* Are all outputs valid? */
|
/* Are all outputs valid? */
|
||||||
bool valid = true;
|
auto outputHashes = staticOutputHashes(*localStore, *(step->drv));
|
||||||
DerivationOutputs missing;
|
std::map<DrvOutput, std::optional<StorePath>> paths;
|
||||||
for (auto & i : step->drv->outputs)
|
for (auto & [outputName, maybeOutputPath] : destStore->queryPartialDerivationOutputMap(drvPath, &*localStore)) {
|
||||||
if (!destStore->isValidPath(*i.second.path(*localStore, step->drv->name, i.first))) {
|
auto outputHash = outputHashes.at(outputName);
|
||||||
valid = false;
|
paths.insert({{outputHash, outputName}, maybeOutputPath});
|
||||||
missing.insert_or_assign(i.first, i.second);
|
}
|
||||||
}
|
|
||||||
|
auto missing = getMissingRemotePaths(destStore, paths);
|
||||||
|
bool valid = missing.empty();
|
||||||
|
|
||||||
/* Try to copy the missing paths from the local store or from
|
/* Try to copy the missing paths from the local store or from
|
||||||
substitutes. */
|
substitutes. */
|
||||||
if (!missing.empty()) {
|
if (!missing.empty()) {
|
||||||
|
|
||||||
size_t avail = 0;
|
size_t avail = 0;
|
||||||
for (auto & i : missing) {
|
for (auto & [i, pathOpt] : missing) {
|
||||||
auto path = i.second.path(*localStore, step->drv->name, i.first);
|
// If we don't know the output path from the destination
|
||||||
if (/* localStore != destStore && */ localStore->isValidPath(*path))
|
// store, see if the local store can tell us.
|
||||||
|
if (/* localStore != destStore && */ !pathOpt && experimentalFeatureSettings.isEnabled(Xp::CaDerivations))
|
||||||
|
if (auto maybeRealisation = localStore->queryRealisation(i))
|
||||||
|
pathOpt = maybeRealisation->outPath;
|
||||||
|
|
||||||
|
if (!pathOpt) {
|
||||||
|
// No hope of getting the store object if we don't know
|
||||||
|
// the path.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto & path = *pathOpt;
|
||||||
|
|
||||||
|
if (/* localStore != destStore && */ localStore->isValidPath(path))
|
||||||
avail++;
|
avail++;
|
||||||
else if (useSubstitutes) {
|
else if (useSubstitutes) {
|
||||||
SubstitutablePathInfos infos;
|
SubstitutablePathInfos infos;
|
||||||
localStore->querySubstitutablePathInfos({{*path, {}}}, infos);
|
localStore->querySubstitutablePathInfos({{path, {}}}, infos);
|
||||||
if (infos.size() == 1)
|
if (infos.size() == 1)
|
||||||
avail++;
|
avail++;
|
||||||
}
|
}
|
||||||
@@ -508,26 +560,29 @@ Step::ptr State::createStep(ref<Store> destStore,
|
|||||||
|
|
||||||
if (missing.size() == avail) {
|
if (missing.size() == avail) {
|
||||||
valid = true;
|
valid = true;
|
||||||
for (auto & i : missing) {
|
for (auto & [i, pathOpt] : missing) {
|
||||||
auto path = i.second.path(*localStore, step->drv->name, i.first);
|
// If we found everything, then we should know the path
|
||||||
|
// to every missing store object now.
|
||||||
|
assert(pathOpt);
|
||||||
|
auto & path = *pathOpt;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
time_t startTime = time(0);
|
time_t startTime = time(0);
|
||||||
|
|
||||||
if (localStore->isValidPath(*path))
|
if (localStore->isValidPath(path))
|
||||||
printInfo("copying output ‘%1%’ of ‘%2%’ from local store",
|
printInfo("copying output ‘%1%’ of ‘%2%’ from local store",
|
||||||
localStore->printStorePath(*path),
|
localStore->printStorePath(path),
|
||||||
localStore->printStorePath(drvPath));
|
localStore->printStorePath(drvPath));
|
||||||
else {
|
else {
|
||||||
printInfo("substituting output ‘%1%’ of ‘%2%’",
|
printInfo("substituting output ‘%1%’ of ‘%2%’",
|
||||||
localStore->printStorePath(*path),
|
localStore->printStorePath(path),
|
||||||
localStore->printStorePath(drvPath));
|
localStore->printStorePath(drvPath));
|
||||||
localStore->ensurePath(*path);
|
localStore->ensurePath(path);
|
||||||
// FIXME: should copy directly from substituter to destStore.
|
// FIXME: should copy directly from substituter to destStore.
|
||||||
}
|
}
|
||||||
|
|
||||||
copyClosure(*localStore, *destStore,
|
copyClosure(*localStore, *destStore,
|
||||||
StorePathSet { *path },
|
StorePathSet { path },
|
||||||
NoRepair, CheckSigs, NoSubstitute);
|
NoRepair, CheckSigs, NoSubstitute);
|
||||||
|
|
||||||
time_t stopTime = time(0);
|
time_t stopTime = time(0);
|
||||||
@@ -535,13 +590,13 @@ Step::ptr State::createStep(ref<Store> destStore,
|
|||||||
{
|
{
|
||||||
auto mc = startDbUpdate();
|
auto mc = startDbUpdate();
|
||||||
pqxx::work txn(conn);
|
pqxx::work txn(conn);
|
||||||
createSubstitutionStep(txn, startTime, stopTime, build, drvPath, "out", *path);
|
createSubstitutionStep(txn, startTime, stopTime, build, drvPath, *(step->drv), "out", path);
|
||||||
txn.commit();
|
txn.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Error & e) {
|
} catch (Error & e) {
|
||||||
printError("while copying/substituting output ‘%s’ of ‘%s’: %s",
|
printError("while copying/substituting output ‘%s’ of ‘%s’: %s",
|
||||||
localStore->printStorePath(*path),
|
localStore->printStorePath(path),
|
||||||
localStore->printStorePath(drvPath),
|
localStore->printStorePath(drvPath),
|
||||||
e.what());
|
e.what());
|
||||||
valid = false;
|
valid = false;
|
||||||
@@ -561,7 +616,7 @@ Step::ptr State::createStep(ref<Store> destStore,
|
|||||||
printMsg(lvlDebug, "creating build step ‘%1%’", localStore->printStorePath(drvPath));
|
printMsg(lvlDebug, "creating build step ‘%1%’", localStore->printStorePath(drvPath));
|
||||||
|
|
||||||
/* Create steps for the dependencies. */
|
/* Create steps for the dependencies. */
|
||||||
for (auto & i : step->drv->inputDrvs) {
|
for (auto & i : step->drv->inputDrvs.map) {
|
||||||
auto dep = createStep(destStore, conn, build, i.first, 0, step, finishedDrvs, newSteps, newRunnable);
|
auto dep = createStep(destStore, conn, build, i.first, 0, step, finishedDrvs, newSteps, newRunnable);
|
||||||
if (dep) {
|
if (dep) {
|
||||||
auto step_(step->state.lock());
|
auto step_(step->state.lock());
|
||||||
@@ -596,10 +651,8 @@ Jobset::ptr State::createJobset(pqxx::work & txn,
|
|||||||
if (i != jobsets_->end()) return i->second;
|
if (i != jobsets_->end()) return i->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto res = txn.exec_params1
|
auto res = txn.exec("select schedulingShares from Jobsets where id = $1",
|
||||||
("select schedulingShares from Jobsets where id = $1",
|
pqxx::params{jobsetID}).one_row();
|
||||||
jobsetID);
|
|
||||||
if (res.empty()) throw Error("missing jobset - can't happen");
|
|
||||||
|
|
||||||
auto shares = res["schedulingShares"].as<unsigned int>();
|
auto shares = res["schedulingShares"].as<unsigned int>();
|
||||||
|
|
||||||
@@ -607,11 +660,10 @@ Jobset::ptr State::createJobset(pqxx::work & txn,
|
|||||||
jobset->setShares(shares);
|
jobset->setShares(shares);
|
||||||
|
|
||||||
/* Load the build steps from the last 24 hours. */
|
/* Load the build steps from the last 24 hours. */
|
||||||
auto res2 = txn.exec_params
|
auto res2 = txn.exec("select s.startTime, s.stopTime from BuildSteps s join Builds b on build = id "
|
||||||
("select s.startTime, s.stopTime from BuildSteps s join Builds b on build = id "
|
|
||||||
"where s.startTime is not null and s.stopTime > $1 and jobset_id = $2",
|
"where s.startTime is not null and s.stopTime > $1 and jobset_id = $2",
|
||||||
time(0) - Jobset::schedulingWindow * 10,
|
pqxx::params{time(0) - Jobset::schedulingWindow * 10,
|
||||||
jobsetID);
|
jobsetID});
|
||||||
for (auto const & row : res2) {
|
for (auto const & row : res2) {
|
||||||
time_t startTime = row["startTime"].as<time_t>();
|
time_t startTime = row["startTime"].as<time_t>();
|
||||||
time_t stopTime = row["stopTime"].as<time_t>();
|
time_t stopTime = row["stopTime"].as<time_t>();
|
||||||
@@ -640,17 +692,18 @@ void State::processJobsetSharesChange(Connection & conn)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
BuildOutput State::getBuildOutputCached(Connection & conn, nix::ref<nix::Store> destStore, const nix::Derivation & drv)
|
BuildOutput State::getBuildOutputCached(Connection & conn, nix::ref<nix::Store> destStore, const nix::StorePath & drvPath)
|
||||||
{
|
{
|
||||||
|
auto derivationOutputs = destStore->queryDerivationOutputMap(drvPath, &*localStore);
|
||||||
|
|
||||||
{
|
{
|
||||||
pqxx::work txn(conn);
|
pqxx::work txn(conn);
|
||||||
|
|
||||||
for (auto & [name, output] : drv.outputsAndOptPaths(*localStore)) {
|
for (auto & [name, output] : derivationOutputs) {
|
||||||
auto r = txn.exec_params
|
auto r = txn.exec("select id, buildStatus, releaseName, closureSize, size from Builds b "
|
||||||
("select id, buildStatus, releaseName, closureSize, size from Builds b "
|
|
||||||
"join BuildOutputs o on b.id = o.build "
|
"join BuildOutputs o on b.id = o.build "
|
||||||
"where finished = 1 and (buildStatus = 0 or buildStatus = 6) and path = $1",
|
"where finished = 1 and (buildStatus = 0 or buildStatus = 6) and path = $1",
|
||||||
localStore->printStorePath(*output.second));
|
pqxx::params{localStore->printStorePath(output)});
|
||||||
if (r.empty()) continue;
|
if (r.empty()) continue;
|
||||||
BuildID id = r[0][0].as<BuildID>();
|
BuildID id = r[0][0].as<BuildID>();
|
||||||
|
|
||||||
@@ -662,9 +715,8 @@ BuildOutput State::getBuildOutputCached(Connection & conn, nix::ref<nix::Store>
|
|||||||
res.closureSize = r[0][3].is_null() ? 0 : r[0][3].as<uint64_t>();
|
res.closureSize = r[0][3].is_null() ? 0 : r[0][3].as<uint64_t>();
|
||||||
res.size = r[0][4].is_null() ? 0 : r[0][4].as<uint64_t>();
|
res.size = r[0][4].is_null() ? 0 : r[0][4].as<uint64_t>();
|
||||||
|
|
||||||
auto products = txn.exec_params
|
auto products = txn.exec("select type, subtype, fileSize, sha256hash, path, name, defaultPath from BuildProducts where build = $1 order by productnr",
|
||||||
("select type, subtype, fileSize, sha256hash, path, name, defaultPath from BuildProducts where build = $1 order by productnr",
|
pqxx::params{id});
|
||||||
id);
|
|
||||||
|
|
||||||
for (auto row : products) {
|
for (auto row : products) {
|
||||||
BuildProduct product;
|
BuildProduct product;
|
||||||
@@ -677,7 +729,7 @@ BuildOutput State::getBuildOutputCached(Connection & conn, nix::ref<nix::Store>
|
|||||||
product.fileSize = row[2].as<off_t>();
|
product.fileSize = row[2].as<off_t>();
|
||||||
}
|
}
|
||||||
if (!row[3].is_null())
|
if (!row[3].is_null())
|
||||||
product.sha256hash = Hash::parseAny(row[3].as<std::string>(), htSHA256);
|
product.sha256hash = Hash::parseAny(row[3].as<std::string>(), HashAlgorithm::SHA256);
|
||||||
if (!row[4].is_null())
|
if (!row[4].is_null())
|
||||||
product.path = row[4].as<std::string>();
|
product.path = row[4].as<std::string>();
|
||||||
product.name = row[5].as<std::string>();
|
product.name = row[5].as<std::string>();
|
||||||
@@ -686,9 +738,8 @@ BuildOutput State::getBuildOutputCached(Connection & conn, nix::ref<nix::Store>
|
|||||||
res.products.emplace_back(product);
|
res.products.emplace_back(product);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto metrics = txn.exec_params
|
auto metrics = txn.exec("select name, unit, value from BuildMetrics where build = $1",
|
||||||
("select name, unit, value from BuildMetrics where build = $1",
|
pqxx::params{id});
|
||||||
id);
|
|
||||||
|
|
||||||
for (auto row : metrics) {
|
for (auto row : metrics) {
|
||||||
BuildMetric metric;
|
BuildMetric metric;
|
||||||
@@ -704,5 +755,5 @@ BuildOutput State::getBuildOutputCached(Connection & conn, nix::ref<nix::Store>
|
|||||||
}
|
}
|
||||||
|
|
||||||
NarMemberDatas narMembers;
|
NarMemberDatas narMembers;
|
||||||
return getBuildOutput(destStore, narMembers, drv);
|
return getBuildOutput(destStore, narMembers, derivationOutputs);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <queue>
|
#include <queue>
|
||||||
#include <regex>
|
#include <regex>
|
||||||
|
#include <semaphore>
|
||||||
|
|
||||||
#include <prometheus/counter.h>
|
#include <prometheus/counter.h>
|
||||||
#include <prometheus/gauge.h>
|
#include <prometheus/gauge.h>
|
||||||
@@ -14,13 +15,19 @@
|
|||||||
|
|
||||||
#include "db.hh"
|
#include "db.hh"
|
||||||
|
|
||||||
#include "parsed-derivations.hh"
|
#include <nix/store/derivations.hh>
|
||||||
#include "pathlocks.hh"
|
#include <nix/store/derivation-options.hh>
|
||||||
#include "pool.hh"
|
#include <nix/store/pathlocks.hh>
|
||||||
#include "build-result.hh"
|
#include <nix/util/pool.hh>
|
||||||
#include "store-api.hh"
|
#include <nix/store/build-result.hh>
|
||||||
#include "sync.hh"
|
#include <nix/store/store-api.hh>
|
||||||
|
#include <nix/util/sync.hh>
|
||||||
#include "nar-extractor.hh"
|
#include "nar-extractor.hh"
|
||||||
|
#include <nix/store/serve-protocol.hh>
|
||||||
|
#include <nix/store/serve-protocol-impl.hh>
|
||||||
|
#include <nix/store/serve-protocol-connection.hh>
|
||||||
|
#include <nix/store/machines.hh>
|
||||||
|
#include <nix/store/globals.hh>
|
||||||
|
|
||||||
|
|
||||||
typedef unsigned int BuildID;
|
typedef unsigned int BuildID;
|
||||||
@@ -54,6 +61,7 @@ typedef enum {
|
|||||||
ssConnecting = 10,
|
ssConnecting = 10,
|
||||||
ssSendingInputs = 20,
|
ssSendingInputs = 20,
|
||||||
ssBuilding = 30,
|
ssBuilding = 30,
|
||||||
|
ssWaitingForLocalSlot = 35,
|
||||||
ssReceivingOutputs = 40,
|
ssReceivingOutputs = 40,
|
||||||
ssPostProcessing = 50,
|
ssPostProcessing = 50,
|
||||||
} StepState;
|
} StepState;
|
||||||
@@ -78,6 +86,8 @@ struct RemoteResult
|
|||||||
{
|
{
|
||||||
return stepStatus == bsCachedFailure ? bsFailed : stepStatus;
|
return stepStatus == bsCachedFailure ? bsFailed : stepStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void updateWithBuildResult(const nix::BuildResult &);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -162,8 +172,7 @@ struct Step
|
|||||||
|
|
||||||
nix::StorePath drvPath;
|
nix::StorePath drvPath;
|
||||||
std::unique_ptr<nix::Derivation> drv;
|
std::unique_ptr<nix::Derivation> drv;
|
||||||
std::unique_ptr<nix::ParsedDerivation> parsedDrv;
|
nix::StringSet requiredSystemFeatures;
|
||||||
std::set<std::string> requiredSystemFeatures;
|
|
||||||
bool preferLocalBuild;
|
bool preferLocalBuild;
|
||||||
bool isDeterministic;
|
bool isDeterministic;
|
||||||
std::string systemType; // concatenation of drv.platform and requiredSystemFeatures
|
std::string systemType; // concatenation of drv.platform and requiredSystemFeatures
|
||||||
@@ -231,18 +240,10 @@ void getDependents(Step::ptr step, std::set<Build::ptr> & builds, std::set<Step:
|
|||||||
void visitDependencies(std::function<void(Step::ptr)> visitor, Step::ptr step);
|
void visitDependencies(std::function<void(Step::ptr)> visitor, Step::ptr step);
|
||||||
|
|
||||||
|
|
||||||
struct Machine
|
struct Machine : nix::Machine
|
||||||
{
|
{
|
||||||
typedef std::shared_ptr<Machine> ptr;
|
typedef std::shared_ptr<Machine> ptr;
|
||||||
|
|
||||||
bool enabled{true};
|
|
||||||
|
|
||||||
std::string sshName, sshKey;
|
|
||||||
std::set<std::string> systemTypes, supportedFeatures, mandatoryFeatures;
|
|
||||||
unsigned int maxJobs = 1;
|
|
||||||
float speedFactor = 1.0;
|
|
||||||
std::string sshPublicHostKey;
|
|
||||||
|
|
||||||
struct State {
|
struct State {
|
||||||
typedef std::shared_ptr<State> ptr;
|
typedef std::shared_ptr<State> ptr;
|
||||||
counter currentJobs{0};
|
counter currentJobs{0};
|
||||||
@@ -292,11 +293,13 @@ struct Machine
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isLocalhost()
|
bool isLocalhost() const;
|
||||||
{
|
|
||||||
std::regex r("^(ssh://|ssh-ng://)?localhost$");
|
// A connection to a machine
|
||||||
return std::regex_search(sshName, r);
|
struct Connection : nix::ServeProto::BasicClientConnection {
|
||||||
}
|
// Backpointer to the machine
|
||||||
|
ptr machine;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -350,9 +353,13 @@ private:
|
|||||||
|
|
||||||
/* The build machines. */
|
/* The build machines. */
|
||||||
std::mutex machinesReadyLock;
|
std::mutex machinesReadyLock;
|
||||||
typedef std::map<std::string, Machine::ptr> Machines;
|
typedef std::map<nix::StoreReference::Variant, Machine::ptr> Machines;
|
||||||
nix::Sync<Machines> machines; // FIXME: use atomic_shared_ptr
|
nix::Sync<Machines> machines; // FIXME: use atomic_shared_ptr
|
||||||
|
|
||||||
|
/* Throttler for CPU-bound local work. */
|
||||||
|
static constexpr unsigned int maxSupportedLocalWorkers = 1024;
|
||||||
|
std::counting_semaphore<maxSupportedLocalWorkers> localWorkThrottler;
|
||||||
|
|
||||||
/* Various stats. */
|
/* Various stats. */
|
||||||
time_t startedAt;
|
time_t startedAt;
|
||||||
counter nrBuildsRead{0};
|
counter nrBuildsRead{0};
|
||||||
@@ -362,6 +369,7 @@ private:
|
|||||||
counter nrStepsDone{0};
|
counter nrStepsDone{0};
|
||||||
counter nrStepsBuilding{0};
|
counter nrStepsBuilding{0};
|
||||||
counter nrStepsCopyingTo{0};
|
counter nrStepsCopyingTo{0};
|
||||||
|
counter nrStepsWaitingForDownloadSlot{0};
|
||||||
counter nrStepsCopyingFrom{0};
|
counter nrStepsCopyingFrom{0};
|
||||||
counter nrStepsWaiting{0};
|
counter nrStepsWaiting{0};
|
||||||
counter nrUnsupportedSteps{0};
|
counter nrUnsupportedSteps{0};
|
||||||
@@ -392,7 +400,6 @@ private:
|
|||||||
|
|
||||||
struct MachineReservation
|
struct MachineReservation
|
||||||
{
|
{
|
||||||
typedef std::shared_ptr<MachineReservation> ptr;
|
|
||||||
State & state;
|
State & state;
|
||||||
Step::ptr step;
|
Step::ptr step;
|
||||||
Machine::ptr machine;
|
Machine::ptr machine;
|
||||||
@@ -430,7 +437,7 @@ private:
|
|||||||
|
|
||||||
/* How often the build steps of a jobset should be repeated in
|
/* How often the build steps of a jobset should be repeated in
|
||||||
order to detect non-determinism. */
|
order to detect non-determinism. */
|
||||||
std::map<std::pair<std::string, std::string>, unsigned int> jobsetRepeats;
|
std::map<std::pair<std::string, std::string>, size_t> jobsetRepeats;
|
||||||
|
|
||||||
bool uploadLogsToBinaryCache;
|
bool uploadLogsToBinaryCache;
|
||||||
|
|
||||||
@@ -450,7 +457,12 @@ private:
|
|||||||
prometheus::Counter& queue_steps_created;
|
prometheus::Counter& queue_steps_created;
|
||||||
prometheus::Counter& queue_checks_early_exits;
|
prometheus::Counter& queue_checks_early_exits;
|
||||||
prometheus::Counter& queue_checks_finished;
|
prometheus::Counter& queue_checks_finished;
|
||||||
prometheus::Gauge& queue_max_id;
|
|
||||||
|
prometheus::Counter& dispatcher_time_spent_running;
|
||||||
|
prometheus::Counter& dispatcher_time_spent_waiting;
|
||||||
|
|
||||||
|
prometheus::Counter& queue_monitor_time_spent_running;
|
||||||
|
prometheus::Counter& queue_monitor_time_spent_waiting;
|
||||||
|
|
||||||
PromMetrics();
|
PromMetrics();
|
||||||
};
|
};
|
||||||
@@ -485,23 +497,28 @@ private:
|
|||||||
const std::string & machine);
|
const std::string & machine);
|
||||||
|
|
||||||
int createSubstitutionStep(pqxx::work & txn, time_t startTime, time_t stopTime,
|
int createSubstitutionStep(pqxx::work & txn, time_t startTime, time_t stopTime,
|
||||||
Build::ptr build, const nix::StorePath & drvPath, const std::string & outputName, const nix::StorePath & storePath);
|
Build::ptr build, const nix::StorePath & drvPath, const nix::Derivation drv, const std::string & outputName, const nix::StorePath & storePath);
|
||||||
|
|
||||||
void updateBuild(pqxx::work & txn, Build::ptr build, BuildStatus status);
|
void updateBuild(pqxx::work & txn, Build::ptr build, BuildStatus status);
|
||||||
|
|
||||||
void queueMonitor();
|
void queueMonitor();
|
||||||
|
|
||||||
void queueMonitorLoop();
|
void queueMonitorLoop(Connection & conn);
|
||||||
|
|
||||||
/* Check the queue for new builds. */
|
/* Check the queue for new builds. */
|
||||||
bool getQueuedBuilds(Connection & conn,
|
bool getQueuedBuilds(Connection & conn, nix::ref<nix::Store> destStore);
|
||||||
nix::ref<nix::Store> destStore, unsigned int & lastBuildId);
|
|
||||||
|
|
||||||
/* Handle cancellation, deletion and priority bumps. */
|
/* Handle cancellation, deletion and priority bumps. */
|
||||||
void processQueueChange(Connection & conn);
|
void processQueueChange(Connection & conn);
|
||||||
|
|
||||||
BuildOutput getBuildOutputCached(Connection & conn, nix::ref<nix::Store> destStore,
|
BuildOutput getBuildOutputCached(Connection & conn, nix::ref<nix::Store> destStore,
|
||||||
const nix::Derivation & drv);
|
const nix::StorePath & drvPath);
|
||||||
|
|
||||||
|
/* Returns paths missing from the remote store. Paths are processed in
|
||||||
|
* parallel to work around the possible latency of remote stores. */
|
||||||
|
std::map<nix::DrvOutput, std::optional<nix::StorePath>> getMissingRemotePaths(
|
||||||
|
nix::ref<nix::Store> destStore,
|
||||||
|
const std::map<nix::DrvOutput, std::optional<nix::StorePath>> & paths);
|
||||||
|
|
||||||
Step::ptr createStep(nix::ref<nix::Store> store,
|
Step::ptr createStep(nix::ref<nix::Store> store,
|
||||||
Connection & conn, Build::ptr build, const nix::StorePath & drvPath,
|
Connection & conn, Build::ptr build, const nix::StorePath & drvPath,
|
||||||
@@ -532,19 +549,19 @@ private:
|
|||||||
|
|
||||||
void abortUnsupported();
|
void abortUnsupported();
|
||||||
|
|
||||||
void builder(MachineReservation::ptr reservation);
|
void builder(std::unique_ptr<MachineReservation> reservation);
|
||||||
|
|
||||||
/* Perform the given build step. Return true if the step is to be
|
/* Perform the given build step. Return true if the step is to be
|
||||||
retried. */
|
retried. */
|
||||||
enum StepResult { sDone, sRetry, sMaybeCancelled };
|
enum StepResult { sDone, sRetry, sMaybeCancelled };
|
||||||
StepResult doBuildStep(nix::ref<nix::Store> destStore,
|
StepResult doBuildStep(nix::ref<nix::Store> destStore,
|
||||||
MachineReservation::ptr reservation,
|
std::unique_ptr<MachineReservation> reservation,
|
||||||
std::shared_ptr<ActiveStep> activeStep);
|
std::shared_ptr<ActiveStep> activeStep);
|
||||||
|
|
||||||
void buildRemote(nix::ref<nix::Store> destStore,
|
void buildRemote(nix::ref<nix::Store> destStore,
|
||||||
|
std::unique_ptr<MachineReservation> reservation,
|
||||||
Machine::ptr machine, Step::ptr step,
|
Machine::ptr machine, Step::ptr step,
|
||||||
unsigned int maxSilentTime, unsigned int buildTimeout,
|
const nix::ServeProto::BuildOptions & buildOptions,
|
||||||
unsigned int repeats,
|
|
||||||
RemoteResult & result, std::shared_ptr<ActiveStep> activeStep,
|
RemoteResult & result, std::shared_ptr<ActiveStep> activeStep,
|
||||||
std::function<void(StepState)> updateStep,
|
std::function<void(StepState)> updateStep,
|
||||||
NarMemberDatas & narMembers);
|
NarMemberDatas & narMembers);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ use strict;
|
|||||||
use warnings;
|
use warnings;
|
||||||
use base 'Hydra::Base::Controller::REST';
|
use base 'Hydra::Base::Controller::REST';
|
||||||
use List::SomeUtils qw(any);
|
use List::SomeUtils qw(any);
|
||||||
use Nix::Store;
|
|
||||||
use Hydra::Helper::Nix;
|
use Hydra::Helper::Nix;
|
||||||
use Hydra::Helper::CatalystUtils;
|
use Hydra::Helper::CatalystUtils;
|
||||||
|
|
||||||
@@ -30,7 +29,7 @@ sub getChannelData {
|
|||||||
my $outputs = {};
|
my $outputs = {};
|
||||||
foreach my $output (@outputs) {
|
foreach my $output (@outputs) {
|
||||||
my $outPath = $output->get_column("outpath");
|
my $outPath = $output->get_column("outpath");
|
||||||
next if $checkValidity && !isValidPath($outPath);
|
next if $checkValidity && !$MACHINE_LOCAL_STORE->isValidPath($outPath);
|
||||||
$outputs->{$output->get_column("outname")} = $outPath;
|
$outputs->{$output->get_column("outname")} = $outPath;
|
||||||
push @storePaths, $outPath;
|
push @storePaths, $outPath;
|
||||||
# Put the system type in the manifest (for top-level
|
# Put the system type in the manifest (for top-level
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ sub get_legacy_ldap_config {
|
|||||||
"hydra_bump-to-front" => [ "bump-to-front" ],
|
"hydra_bump-to-front" => [ "bump-to-front" ],
|
||||||
"hydra_cancel-build" => [ "cancel-build" ],
|
"hydra_cancel-build" => [ "cancel-build" ],
|
||||||
"hydra_create-projects" => [ "create-projects" ],
|
"hydra_create-projects" => [ "create-projects" ],
|
||||||
|
"hydra_eval-jobset" => [ "eval-jobset" ],
|
||||||
"hydra_restart-jobs" => [ "restart-jobs" ],
|
"hydra_restart-jobs" => [ "restart-jobs" ],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -159,6 +160,7 @@ sub valid_roles {
|
|||||||
"bump-to-front",
|
"bump-to-front",
|
||||||
"cancel-build",
|
"cancel-build",
|
||||||
"create-projects",
|
"create-projects",
|
||||||
|
"eval-jobset",
|
||||||
"restart-jobs",
|
"restart-jobs",
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ use DateTime;
|
|||||||
use Digest::SHA qw(sha256_hex);
|
use Digest::SHA qw(sha256_hex);
|
||||||
use Text::Diff;
|
use Text::Diff;
|
||||||
use IPC::Run qw(run);
|
use IPC::Run qw(run);
|
||||||
|
use Digest::SHA qw(hmac_sha256_hex);
|
||||||
|
use String::Compare::ConstantTime qw(equals);
|
||||||
|
use IPC::Run3;
|
||||||
|
|
||||||
|
|
||||||
sub api : Chained('/') PathPart('api') CaptureArgs(0) {
|
sub api : Chained('/') PathPart('api') CaptureArgs(0) {
|
||||||
@@ -216,8 +219,13 @@ sub scmdiff : Path('/api/scmdiff') Args(0) {
|
|||||||
} elsif ($type eq "git") {
|
} elsif ($type eq "git") {
|
||||||
my $clonePath = getSCMCacheDir . "/git/" . sha256_hex($uri);
|
my $clonePath = getSCMCacheDir . "/git/" . sha256_hex($uri);
|
||||||
die if ! -d $clonePath;
|
die if ! -d $clonePath;
|
||||||
$diff .= `(cd $clonePath; git --git-dir .git log $rev1..$rev2)`;
|
my ($stdout1, $stderr1);
|
||||||
$diff .= `(cd $clonePath; git --git-dir .git diff $rev1..$rev2)`;
|
run3(['git', '--git-dir', '.git', '-C', $clonePath, 'log', "$rev1..$rev2"], \undef, \$stdout1, \$stderr1);
|
||||||
|
$diff .= $stdout1 if $? == 0;
|
||||||
|
|
||||||
|
my ($stdout2, $stderr2);
|
||||||
|
run3(['git', '--git-dir', '.git', '-C', $clonePath, 'diff', "$rev1..$rev2"], \undef, \$stdout2, \$stderr2);
|
||||||
|
$diff .= $stdout2 if $? == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
$c->stash->{'plain'} = { data => (scalar $diff) || " " };
|
$c->stash->{'plain'} = { data => (scalar $diff) || " " };
|
||||||
@@ -239,6 +247,8 @@ sub triggerJobset {
|
|||||||
sub push : Chained('api') PathPart('push') Args(0) {
|
sub push : Chained('api') PathPart('push') Args(0) {
|
||||||
my ($self, $c) = @_;
|
my ($self, $c) = @_;
|
||||||
|
|
||||||
|
requirePost($c);
|
||||||
|
|
||||||
$c->{stash}->{json}->{jobsetsTriggered} = [];
|
$c->{stash}->{json}->{jobsetsTriggered} = [];
|
||||||
|
|
||||||
my $force = exists $c->request->query_params->{force};
|
my $force = exists $c->request->query_params->{force};
|
||||||
@@ -246,19 +256,24 @@ sub push : Chained('api') PathPart('push') Args(0) {
|
|||||||
foreach my $s (@jobsets) {
|
foreach my $s (@jobsets) {
|
||||||
my ($p, $j) = parseJobsetName($s);
|
my ($p, $j) = parseJobsetName($s);
|
||||||
my $jobset = $c->model('DB::Jobsets')->find($p, $j);
|
my $jobset = $c->model('DB::Jobsets')->find($p, $j);
|
||||||
|
requireEvalJobsetPrivileges($c, $jobset->project);
|
||||||
next unless defined $jobset && ($force || ($jobset->project->enabled && $jobset->enabled));
|
next unless defined $jobset && ($force || ($jobset->project->enabled && $jobset->enabled));
|
||||||
triggerJobset($self, $c, $jobset, $force);
|
triggerJobset($self, $c, $jobset, $force);
|
||||||
}
|
}
|
||||||
|
|
||||||
my @repos = split /,/, ($c->request->query_params->{repos} // "");
|
my @repos = split /,/, ($c->request->query_params->{repos} // "");
|
||||||
foreach my $r (@repos) {
|
foreach my $r (@repos) {
|
||||||
triggerJobset($self, $c, $_, $force) foreach $c->model('DB::Jobsets')->search(
|
my @jobsets = $c->model('DB::Jobsets')->search(
|
||||||
{ 'project.enabled' => 1, 'me.enabled' => 1 },
|
{ 'project.enabled' => 1, 'me.enabled' => 1 },
|
||||||
{
|
{
|
||||||
join => 'project',
|
join => 'project',
|
||||||
where => \ [ 'exists (select 1 from JobsetInputAlts where project = me.project and jobset = me.name and value = ?)', [ 'value', $r ] ],
|
where => \ [ 'exists (select 1 from JobsetInputAlts where project = me.project and jobset = me.name and value = ?)', [ 'value', $r ] ],
|
||||||
order_by => 'me.id DESC'
|
order_by => 'me.id DESC'
|
||||||
});
|
});
|
||||||
|
foreach my $jobset (@jobsets) {
|
||||||
|
requireEvalJobsetPrivileges($c, $jobset->project);
|
||||||
|
triggerJobset($self, $c, $jobset, $force)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$self->status_ok(
|
$self->status_ok(
|
||||||
@@ -267,13 +282,84 @@ sub push : Chained('api') PathPart('push') Args(0) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub verifyWebhookSignature {
|
||||||
|
my ($c, $platform, $header_name, $signature_prefix) = @_;
|
||||||
|
|
||||||
|
# Get secrets from config
|
||||||
|
my $webhook_config = $c->config->{webhooks} // {};
|
||||||
|
my $platform_config = $webhook_config->{$platform} // {};
|
||||||
|
my $secrets = $platform_config->{secret};
|
||||||
|
|
||||||
|
# Normalize to array
|
||||||
|
$secrets = [] unless defined $secrets;
|
||||||
|
$secrets = [$secrets] unless ref($secrets) eq 'ARRAY';
|
||||||
|
|
||||||
|
# Trim whitespace from secrets
|
||||||
|
my @secrets = grep { defined && length } map { s/^\s+|\s+$//gr } @$secrets;
|
||||||
|
|
||||||
|
if (@secrets) {
|
||||||
|
my $signature = $c->request->header($header_name);
|
||||||
|
|
||||||
|
if (!$signature) {
|
||||||
|
$c->log->warn("Webhook authentication failed for $platform: Missing signature from IP " . $c->request->address);
|
||||||
|
$c->response->status(401);
|
||||||
|
$c->stash->{json} = { error => "Missing webhook signature" };
|
||||||
|
$c->forward('View::JSON');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the raw body content from the buffered PSGI input
|
||||||
|
# For JSON requests, Catalyst will have already read and buffered the body
|
||||||
|
my $input = $c->request->env->{'psgi.input'};
|
||||||
|
$input->seek(0, 0);
|
||||||
|
local $/;
|
||||||
|
my $payload = <$input>;
|
||||||
|
$input->seek(0, 0); # Reset for any other consumers
|
||||||
|
|
||||||
|
unless (defined $payload && length $payload) {
|
||||||
|
$c->log->warn("Webhook authentication failed for $platform: Empty request body from IP " . $c->request->address);
|
||||||
|
$c->response->status(400);
|
||||||
|
$c->stash->{json} = { error => "Empty request body" };
|
||||||
|
$c->forward('View::JSON');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
my $valid = 0;
|
||||||
|
for my $secret (@secrets) {
|
||||||
|
my $expected = $signature_prefix . hmac_sha256_hex($payload, $secret);
|
||||||
|
if (equals($signature, $expected)) {
|
||||||
|
$valid = 1;
|
||||||
|
last;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$valid) {
|
||||||
|
$c->log->warn("Webhook authentication failed for $platform: Invalid signature from IP " . $c->request->address);
|
||||||
|
$c->response->status(401);
|
||||||
|
$c->stash->{json} = { error => "Invalid webhook signature" };
|
||||||
|
$c->forward('View::JSON');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
$c->log->warn("Webhook authentication failed for $platform: Unable to validate signature from IP " . $c->request->address . " because no secrets are configured");
|
||||||
|
$c->response->status(401);
|
||||||
|
$c->stash->{json} = { error => "Invalid webhook signature" };
|
||||||
|
$c->forward('View::JSON');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sub push_github : Chained('api') PathPart('push-github') Args(0) {
|
sub push_github : Chained('api') PathPart('push-github') Args(0) {
|
||||||
my ($self, $c) = @_;
|
my ($self, $c) = @_;
|
||||||
|
|
||||||
$c->{stash}->{json}->{jobsetsTriggered} = [];
|
$c->{stash}->{json}->{jobsetsTriggered} = [];
|
||||||
|
|
||||||
|
return unless verifyWebhookSignature($c, 'github', 'X-Hub-Signature-256', 'sha256=');
|
||||||
|
|
||||||
my $in = $c->request->{data};
|
my $in = $c->request->{data};
|
||||||
my $owner = $in->{repository}->{owner}->{name} or die;
|
my $owner = ($in->{repository}->{owner}->{name} // $in->{repository}->{owner}->{login}) or die;
|
||||||
my $repo = $in->{repository}->{name} or die;
|
my $repo = $in->{repository}->{name} or die;
|
||||||
print STDERR "got push from GitHub repository $owner/$repo\n";
|
print STDERR "got push from GitHub repository $owner/$repo\n";
|
||||||
|
|
||||||
@@ -285,6 +371,26 @@ sub push_github : Chained('api') PathPart('push-github') Args(0) {
|
|||||||
$c->response->body("");
|
$c->response->body("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub push_gitea : Chained('api') PathPart('push-gitea') Args(0) {
|
||||||
|
my ($self, $c) = @_;
|
||||||
|
|
||||||
|
$c->{stash}->{json}->{jobsetsTriggered} = [];
|
||||||
|
|
||||||
|
# Note: Gitea doesn't use sha256= prefix
|
||||||
|
return unless verifyWebhookSignature($c, 'gitea', 'X-Gitea-Signature', '');
|
||||||
|
|
||||||
|
my $in = $c->request->{data};
|
||||||
|
my $url = $in->{repository}->{clone_url} or die;
|
||||||
|
$url =~ s/.git$//;
|
||||||
|
print STDERR "got push from Gitea repository $url\n";
|
||||||
|
|
||||||
|
triggerJobset($self, $c, $_, 0) foreach $c->model('DB::Jobsets')->search(
|
||||||
|
{ 'project.enabled' => 1, 'me.enabled' => 1 },
|
||||||
|
{ join => 'project'
|
||||||
|
, where => \ [ 'me.flake like ? or exists (select 1 from JobsetInputAlts where project = me.project and jobset = me.name and value like ?)', [ 'flake', "%$url%"], [ 'value', "%$url%" ] ]
|
||||||
|
});
|
||||||
|
$c->response->body("");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
1;
|
1;
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ use File::Basename;
|
|||||||
use File::LibMagic;
|
use File::LibMagic;
|
||||||
use File::stat;
|
use File::stat;
|
||||||
use Data::Dump qw(dump);
|
use Data::Dump qw(dump);
|
||||||
use Nix::Store;
|
|
||||||
use Nix::Config;
|
|
||||||
use List::SomeUtils qw(all);
|
use List::SomeUtils qw(all);
|
||||||
use Encode;
|
use Encode;
|
||||||
use JSON::PP;
|
use JSON::PP;
|
||||||
|
use IPC::Run qw(run);
|
||||||
|
use IPC::Run3;
|
||||||
|
use WWW::Form::UrlEncoded::PP qw();
|
||||||
|
|
||||||
use feature 'state';
|
use feature 'state';
|
||||||
|
|
||||||
@@ -78,14 +79,16 @@ sub build_GET {
|
|||||||
|
|
||||||
$c->stash->{template} = 'build.tt';
|
$c->stash->{template} = 'build.tt';
|
||||||
$c->stash->{isLocalStore} = isLocalStore();
|
$c->stash->{isLocalStore} = isLocalStore();
|
||||||
|
# XXX: If the derivation is content-addressed then this will always return
|
||||||
|
# false because `$_->path` will be empty
|
||||||
$c->stash->{available} =
|
$c->stash->{available} =
|
||||||
$c->stash->{isLocalStore}
|
$c->stash->{isLocalStore}
|
||||||
? all { isValidPath($_->path) } $build->buildoutputs->all
|
? all { $_->path && $MACHINE_LOCAL_STORE->isValidPath($_->path) } $build->buildoutputs->all
|
||||||
: 1;
|
: 1;
|
||||||
$c->stash->{drvAvailable} = isValidPath $build->drvpath;
|
$c->stash->{drvAvailable} = $MACHINE_LOCAL_STORE->isValidPath($build->drvpath);
|
||||||
|
|
||||||
if ($build->finished && $build->iscachedbuild) {
|
if ($build->finished && $build->iscachedbuild) {
|
||||||
my $path = ($build->buildoutputs)[0]->path or die;
|
my $path = ($build->buildoutputs)[0]->path or undef;
|
||||||
my $cachedBuildStep = findBuildStepByOutPath($self, $c, $path);
|
my $cachedBuildStep = findBuildStepByOutPath($self, $c, $path);
|
||||||
if (defined $cachedBuildStep) {
|
if (defined $cachedBuildStep) {
|
||||||
$c->stash->{cachedBuild} = $cachedBuildStep->build;
|
$c->stash->{cachedBuild} = $cachedBuildStep->build;
|
||||||
@@ -139,7 +142,7 @@ sub view_nixlog : Chained('buildChain') PathPart('nixlog') {
|
|||||||
$c->stash->{step} = $step;
|
$c->stash->{step} = $step;
|
||||||
|
|
||||||
my $drvPath = $step->drvpath;
|
my $drvPath = $step->drvpath;
|
||||||
my $log_uri = $c->uri_for($c->controller('Root')->action_for("log"), [basename($drvPath)]);
|
my $log_uri = $c->uri_for($c->controller('Root')->action_for("log"), [WWW::Form::UrlEncoded::PP::url_encode(basename($drvPath))]);
|
||||||
showLog($c, $mode, $log_uri);
|
showLog($c, $mode, $log_uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +151,7 @@ sub view_log : Chained('buildChain') PathPart('log') {
|
|||||||
my ($self, $c, $mode) = @_;
|
my ($self, $c, $mode) = @_;
|
||||||
|
|
||||||
my $drvPath = $c->stash->{build}->drvpath;
|
my $drvPath = $c->stash->{build}->drvpath;
|
||||||
my $log_uri = $c->uri_for($c->controller('Root')->action_for("log"), [basename($drvPath)]);
|
my $log_uri = $c->uri_for($c->controller('Root')->action_for("log"), [WWW::Form::UrlEncoded::PP::url_encode(basename($drvPath))]);
|
||||||
showLog($c, $mode, $log_uri);
|
showLog($c, $mode, $log_uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +212,7 @@ sub checkPath {
|
|||||||
sub serveFile {
|
sub serveFile {
|
||||||
my ($c, $path) = @_;
|
my ($c, $path) = @_;
|
||||||
|
|
||||||
my $res = run(cmd => ["nix", "--experimental-features", "nix-command",
|
my $res = runCommand(cmd => ["nix", "--experimental-features", "nix-command",
|
||||||
"ls-store", "--store", getStoreUri(), "--json", "$path"]);
|
"ls-store", "--store", getStoreUri(), "--json", "$path"]);
|
||||||
|
|
||||||
if ($res->{status}) {
|
if ($res->{status}) {
|
||||||
@@ -233,8 +236,11 @@ sub serveFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
elsif ($ls->{type} eq "regular") {
|
elsif ($ls->{type} eq "regular") {
|
||||||
|
# Have the hosted data considered its own origin to avoid being a giant
|
||||||
|
# XSS hole.
|
||||||
|
$c->response->header('Content-Security-Policy' => 'sandbox allow-scripts');
|
||||||
|
|
||||||
$c->stash->{'plain'} = { data => grab(cmd => ["nix", "--experimental-features", "nix-command",
|
$c->stash->{'plain'} = { data => readIntoSocket(cmd => ["nix", "--experimental-features", "nix-command",
|
||||||
"store", "cat", "--store", getStoreUri(), "$path"]) };
|
"store", "cat", "--store", getStoreUri(), "$path"]) };
|
||||||
|
|
||||||
# Detect MIME type.
|
# Detect MIME type.
|
||||||
@@ -306,7 +312,7 @@ sub output : Chained('buildChain') PathPart Args(1) {
|
|||||||
error($c, "This build is not finished yet.") unless $build->finished;
|
error($c, "This build is not finished yet.") unless $build->finished;
|
||||||
my $output = $build->buildoutputs->find({name => $outputName});
|
my $output = $build->buildoutputs->find({name => $outputName});
|
||||||
notFound($c, "This build has no output named ‘$outputName’") unless defined $output;
|
notFound($c, "This build has no output named ‘$outputName’") unless defined $output;
|
||||||
gone($c, "Output is no longer available.") unless isValidPath $output->path;
|
gone($c, "Output is no longer available.") unless $MACHINE_LOCAL_STORE->isValidPath($output->path);
|
||||||
|
|
||||||
$c->response->header('Content-Disposition', "attachment; filename=\"build-${\$build->id}-${\$outputName}.nar.bz2\"");
|
$c->response->header('Content-Disposition', "attachment; filename=\"build-${\$build->id}-${\$outputName}.nar.bz2\"");
|
||||||
$c->stash->{current_view} = 'NixNAR';
|
$c->stash->{current_view} = 'NixNAR';
|
||||||
@@ -344,19 +350,21 @@ sub contents : Chained('buildChain') PathPart Args(1) {
|
|||||||
|
|
||||||
notFound($c, "Product $path has disappeared.") unless -e $path;
|
notFound($c, "Product $path has disappeared.") unless -e $path;
|
||||||
|
|
||||||
# Sanitize $path to prevent shell injection attacks.
|
|
||||||
$path =~ /^\/[\/[A-Za-z0-9_\-\.=+:]+$/ or die "Filename contains illegal characters.\n";
|
|
||||||
|
|
||||||
# FIXME: don't use shell invocations below.
|
|
||||||
|
|
||||||
# FIXME: use nix store cat
|
# FIXME: use nix store cat
|
||||||
|
|
||||||
my $res;
|
my $res;
|
||||||
|
|
||||||
if ($product->type eq "nix-build" && -d $path) {
|
if ($product->type eq "nix-build" && -d $path) {
|
||||||
# FIXME: use nix ls-store -R --json
|
# FIXME: use nix ls-store -R --json
|
||||||
$res = `cd '$path' && find . -print0 | xargs -0 ls -ld --`;
|
# We need to use a pipe between find and xargs, so we'll use IPC::Run
|
||||||
error($c, "`ls -lR' error: $?") if $? != 0;
|
my $error;
|
||||||
|
# Run find with absolute path and post-process to get relative paths
|
||||||
|
my $success = run(['find', $path, '-print0'], '|', ['xargs', '-0', 'ls', '-ld', '--'], \$res, \$error);
|
||||||
|
error($c, "`find $path -print0 | xargs -0 ls -ld --' error: $error") unless $success;
|
||||||
|
|
||||||
|
# Strip the base path to show relative paths
|
||||||
|
my $escaped_path = quotemeta($path);
|
||||||
|
$res =~ s/^(.*\s)$escaped_path(\/|$)/$1.$2/mg;
|
||||||
|
|
||||||
#my $baseuri = $c->uri_for('/build', $c->stash->{build}->id, 'download', $product->productnr);
|
#my $baseuri = $c->uri_for('/build', $c->stash->{build}->id, 'download', $product->productnr);
|
||||||
#$baseuri .= "/".$product->name if $product->name;
|
#$baseuri .= "/".$product->name if $product->name;
|
||||||
@@ -364,34 +372,59 @@ sub contents : Chained('buildChain') PathPart Args(1) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
elsif ($path =~ /\.rpm$/) {
|
elsif ($path =~ /\.rpm$/) {
|
||||||
$res = `rpm --query --info --package '$path'`;
|
my ($stdout1, $stderr1);
|
||||||
error($c, "RPM error: $?") if $? != 0;
|
run3(['rpm', '--query', '--info', '--package', $path], \undef, \$stdout1, \$stderr1);
|
||||||
|
error($c, "RPM error: $stderr1") if $? != 0;
|
||||||
|
$res = $stdout1;
|
||||||
|
|
||||||
$res .= "===\n";
|
$res .= "===\n";
|
||||||
$res .= `rpm --query --list --verbose --package '$path'`;
|
|
||||||
error($c, "RPM error: $?") if $? != 0;
|
my ($stdout2, $stderr2);
|
||||||
|
run3(['rpm', '--query', '--list', '--verbose', '--package', $path], \undef, \$stdout2, \$stderr2);
|
||||||
|
error($c, "RPM error: $stderr2") if $? != 0;
|
||||||
|
$res .= $stdout2;
|
||||||
}
|
}
|
||||||
|
|
||||||
elsif ($path =~ /\.deb$/) {
|
elsif ($path =~ /\.deb$/) {
|
||||||
$res = `dpkg-deb --info '$path'`;
|
my ($stdout1, $stderr1);
|
||||||
error($c, "`dpkg-deb' error: $?") if $? != 0;
|
run3(['dpkg-deb', '--info', $path], \undef, \$stdout1, \$stderr1);
|
||||||
|
error($c, "`dpkg-deb' error: $stderr1") if $? != 0;
|
||||||
|
$res = $stdout1;
|
||||||
|
|
||||||
$res .= "===\n";
|
$res .= "===\n";
|
||||||
$res .= `dpkg-deb --contents '$path'`;
|
|
||||||
error($c, "`dpkg-deb' error: $?") if $? != 0;
|
my ($stdout2, $stderr2);
|
||||||
|
run3(['dpkg-deb', '--contents', $path], \undef, \$stdout2, \$stderr2);
|
||||||
|
error($c, "`dpkg-deb' error: $stderr2") if $? != 0;
|
||||||
|
$res .= $stdout2;
|
||||||
}
|
}
|
||||||
|
|
||||||
elsif ($path =~ /\.(tar(\.gz|\.bz2|\.xz|\.lzma)?|tgz)$/ ) {
|
elsif ($path =~ /\.(tar(\.gz|\.bz2|\.xz|\.lzma)?|tgz)$/ ) {
|
||||||
$res = `tar tvfa '$path'`;
|
my ($stdout, $stderr);
|
||||||
error($c, "`tar' error: $?") if $? != 0;
|
run3(['tar', 'tvfa', $path], \undef, \$stdout, \$stderr);
|
||||||
|
error($c, "`tar' error: $stderr") if $? != 0;
|
||||||
|
$res = $stdout;
|
||||||
}
|
}
|
||||||
|
|
||||||
elsif ($path =~ /\.(zip|jar)$/ ) {
|
elsif ($path =~ /\.(zip|jar)$/ ) {
|
||||||
$res = `unzip -v '$path'`;
|
my ($stdout, $stderr);
|
||||||
error($c, "`unzip' error: $?") if $? != 0;
|
run3(['unzip', '-v', $path], \undef, \$stdout, \$stderr);
|
||||||
|
error($c, "`unzip' error: $stderr") if $? != 0;
|
||||||
|
$res = $stdout;
|
||||||
}
|
}
|
||||||
|
|
||||||
elsif ($path =~ /\.iso$/ ) {
|
elsif ($path =~ /\.iso$/ ) {
|
||||||
$res = `isoinfo -d -i '$path' && isoinfo -l -R -i '$path'`;
|
# Run first isoinfo command
|
||||||
error($c, "`isoinfo' error: $?") if $? != 0;
|
my ($stdout1, $stderr1);
|
||||||
|
run3(['isoinfo', '-d', '-i', $path], \undef, \$stdout1, \$stderr1);
|
||||||
|
error($c, "`isoinfo' error: $stderr1") if $? != 0;
|
||||||
|
$res = $stdout1;
|
||||||
|
|
||||||
|
# Run second isoinfo command
|
||||||
|
my ($stdout2, $stderr2);
|
||||||
|
run3(['isoinfo', '-l', '-R', '-i', $path], \undef, \$stdout2, \$stderr2);
|
||||||
|
error($c, "`isoinfo' error: $stderr2") if $? != 0;
|
||||||
|
$res .= $stdout2;
|
||||||
}
|
}
|
||||||
|
|
||||||
else {
|
else {
|
||||||
@@ -423,7 +456,7 @@ sub getDependencyGraph {
|
|||||||
};
|
};
|
||||||
$$done{$path} = $node;
|
$$done{$path} = $node;
|
||||||
my @refs;
|
my @refs;
|
||||||
foreach my $ref (queryReferences($path)) {
|
foreach my $ref ($MACHINE_LOCAL_STORE->queryReferences($path)) {
|
||||||
next if $ref eq $path;
|
next if $ref eq $path;
|
||||||
next unless $runtime || $ref =~ /\.drv$/;
|
next unless $runtime || $ref =~ /\.drv$/;
|
||||||
getDependencyGraph($self, $c, $runtime, $done, $ref);
|
getDependencyGraph($self, $c, $runtime, $done, $ref);
|
||||||
@@ -431,7 +464,7 @@ sub getDependencyGraph {
|
|||||||
}
|
}
|
||||||
# Show in reverse topological order to flatten the graph.
|
# Show in reverse topological order to flatten the graph.
|
||||||
# Should probably do a proper BFS.
|
# Should probably do a proper BFS.
|
||||||
my @sorted = reverse topoSortPaths(@refs);
|
my @sorted = reverse $MACHINE_LOCAL_STORE->topoSortPaths(@refs);
|
||||||
$node->{refs} = [map { $$done{$_} } @sorted];
|
$node->{refs} = [map { $$done{$_} } @sorted];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,7 +477,7 @@ sub build_deps : Chained('buildChain') PathPart('build-deps') {
|
|||||||
my $build = $c->stash->{build};
|
my $build = $c->stash->{build};
|
||||||
my $drvPath = $build->drvpath;
|
my $drvPath = $build->drvpath;
|
||||||
|
|
||||||
error($c, "Derivation no longer available.") unless isValidPath $drvPath;
|
error($c, "Derivation no longer available.") unless $MACHINE_LOCAL_STORE->isValidPath($drvPath);
|
||||||
|
|
||||||
$c->stash->{buildTimeGraph} = getDependencyGraph($self, $c, 0, {}, $drvPath);
|
$c->stash->{buildTimeGraph} = getDependencyGraph($self, $c, 0, {}, $drvPath);
|
||||||
|
|
||||||
@@ -459,7 +492,7 @@ sub runtime_deps : Chained('buildChain') PathPart('runtime-deps') {
|
|||||||
|
|
||||||
requireLocalStore($c);
|
requireLocalStore($c);
|
||||||
|
|
||||||
error($c, "Build outputs no longer available.") unless all { isValidPath($_) } @outPaths;
|
error($c, "Build outputs no longer available.") unless all { $MACHINE_LOCAL_STORE->isValidPath($_) } @outPaths;
|
||||||
|
|
||||||
my $done = {};
|
my $done = {};
|
||||||
$c->stash->{runtimeGraph} = [ map { getDependencyGraph($self, $c, 1, $done, $_) } @outPaths ];
|
$c->stash->{runtimeGraph} = [ map { getDependencyGraph($self, $c, 1, $done, $_) } @outPaths ];
|
||||||
@@ -479,7 +512,7 @@ sub nix : Chained('buildChain') PathPart('nix') CaptureArgs(0) {
|
|||||||
if (isLocalStore) {
|
if (isLocalStore) {
|
||||||
foreach my $out ($build->buildoutputs) {
|
foreach my $out ($build->buildoutputs) {
|
||||||
notFound($c, "Path " . $out->path . " is no longer available.")
|
notFound($c, "Path " . $out->path . " is no longer available.")
|
||||||
unless isValidPath($out->path);
|
unless $MACHINE_LOCAL_STORE->isValidPath($out->path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -364,6 +364,21 @@ sub evals_GET {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub errors :Chained('jobsetChain') :PathPart('errors') :Args(0) :ActionClass('REST') { }
|
||||||
|
|
||||||
|
sub errors_GET {
|
||||||
|
my ($self, $c) = @_;
|
||||||
|
|
||||||
|
$c->stash->{template} = 'eval-error.tt';
|
||||||
|
|
||||||
|
my $jobsetName = $c->stash->{params}->{name};
|
||||||
|
$c->stash->{jobset} = $c->stash->{project}->jobsets->find(
|
||||||
|
{ name => $jobsetName },
|
||||||
|
{ '+columns' => { 'errormsg' => 'errormsg' } }
|
||||||
|
);
|
||||||
|
|
||||||
|
$self->status_ok($c, entity => $c->stash->{jobset});
|
||||||
|
}
|
||||||
|
|
||||||
# Redirect to the latest finished evaluation of this jobset.
|
# Redirect to the latest finished evaluation of this jobset.
|
||||||
sub latest_eval : Chained('jobsetChain') PathPart('latest-eval') {
|
sub latest_eval : Chained('jobsetChain') PathPart('latest-eval') {
|
||||||
|
|||||||
@@ -76,7 +76,9 @@ sub view_GET {
|
|||||||
$c->stash->{removed} = $diff->{removed};
|
$c->stash->{removed} = $diff->{removed};
|
||||||
$c->stash->{unfinished} = $diff->{unfinished};
|
$c->stash->{unfinished} = $diff->{unfinished};
|
||||||
$c->stash->{aborted} = $diff->{aborted};
|
$c->stash->{aborted} = $diff->{aborted};
|
||||||
$c->stash->{failed} = $diff->{failed};
|
$c->stash->{totalAborted} = $diff->{totalAborted};
|
||||||
|
$c->stash->{totalFailed} = $diff->{totalFailed};
|
||||||
|
$c->stash->{totalQueued} = $diff->{totalQueued};
|
||||||
|
|
||||||
$c->stash->{full} = ($c->req->params->{full} || "0") eq "1";
|
$c->stash->{full} = ($c->req->params->{full} || "0") eq "1";
|
||||||
|
|
||||||
@@ -86,6 +88,17 @@ sub view_GET {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub errors :Chained('evalChain') :PathPart('errors') :Args(0) :ActionClass('REST') { }
|
||||||
|
|
||||||
|
sub errors_GET {
|
||||||
|
my ($self, $c) = @_;
|
||||||
|
|
||||||
|
$c->stash->{template} = 'eval-error.tt';
|
||||||
|
|
||||||
|
$c->stash->{eval} = $c->model('DB::JobsetEvals')->find($c->stash->{eval}->id, { prefetch => 'evaluationerror' });
|
||||||
|
|
||||||
|
$self->status_ok($c, entity => $c->stash->{eval});
|
||||||
|
}
|
||||||
|
|
||||||
sub create_jobset : Chained('evalChain') PathPart('create-jobset') Args(0) {
|
sub create_jobset : Chained('evalChain') PathPart('create-jobset') Args(0) {
|
||||||
my ($self, $c) = @_;
|
my ($self, $c) = @_;
|
||||||
|
|||||||
@@ -9,15 +9,21 @@ use Hydra::Helper::CatalystUtils;
|
|||||||
use Hydra::View::TT;
|
use Hydra::View::TT;
|
||||||
use Nix::Store;
|
use Nix::Store;
|
||||||
use Nix::Config;
|
use Nix::Config;
|
||||||
|
use Number::Bytes::Human qw(format_bytes);
|
||||||
use Encode;
|
use Encode;
|
||||||
use File::Basename;
|
use File::Basename;
|
||||||
use JSON::MaybeXS;
|
use JSON::MaybeXS;
|
||||||
|
use HTML::Entities;
|
||||||
|
use IPC::Run3;
|
||||||
use List::Util qw[min max];
|
use List::Util qw[min max];
|
||||||
use List::SomeUtils qw{any};
|
use List::SomeUtils qw{any};
|
||||||
use Net::Prometheus;
|
use Net::Prometheus;
|
||||||
use Types::Standard qw/StrMatch/;
|
use Types::Standard qw/StrMatch/;
|
||||||
|
use WWW::Form::UrlEncoded::PP qw();
|
||||||
|
|
||||||
use constant NARINFO_REGEX => qr{^([a-z0-9]{32})\.narinfo$};
|
use constant NARINFO_REGEX => qr{^([a-z0-9]{32})\.narinfo$};
|
||||||
|
# e.g.: https://hydra.example.com/realisations/sha256:a62128132508a3a32eef651d6467695944763602f226ac630543e947d9feb140!out.doi
|
||||||
|
use constant REALISATIONS_REGEX => qr{^(sha256:[a-z0-9]{64}![a-z]+)\.doi$};
|
||||||
|
|
||||||
# Put this controller at top-level.
|
# Put this controller at top-level.
|
||||||
__PACKAGE__->config->{namespace} = '';
|
__PACKAGE__->config->{namespace} = '';
|
||||||
@@ -32,6 +38,7 @@ sub noLoginNeeded {
|
|||||||
|
|
||||||
return $whitelisted ||
|
return $whitelisted ||
|
||||||
$c->request->path eq "api/push-github" ||
|
$c->request->path eq "api/push-github" ||
|
||||||
|
$c->request->path eq "api/push-gitea" ||
|
||||||
$c->request->path eq "google-login" ||
|
$c->request->path eq "google-login" ||
|
||||||
$c->request->path eq "github-redirect" ||
|
$c->request->path eq "github-redirect" ||
|
||||||
$c->request->path eq "github-login" ||
|
$c->request->path eq "github-login" ||
|
||||||
@@ -47,11 +54,13 @@ sub begin :Private {
|
|||||||
$c->stash->{curUri} = $c->request->uri;
|
$c->stash->{curUri} = $c->request->uri;
|
||||||
$c->stash->{version} = $ENV{"HYDRA_RELEASE"} || "<devel>";
|
$c->stash->{version} = $ENV{"HYDRA_RELEASE"} || "<devel>";
|
||||||
$c->stash->{nixVersion} = $ENV{"NIX_RELEASE"} || "<devel>";
|
$c->stash->{nixVersion} = $ENV{"NIX_RELEASE"} || "<devel>";
|
||||||
|
$c->stash->{nixEvalJobsVersion} = $ENV{"NIX_EVAL_JOBS_RELEASE"} || "<devel>";
|
||||||
$c->stash->{curTime} = time;
|
$c->stash->{curTime} = time;
|
||||||
$c->stash->{logo} = defined $c->config->{hydra_logo} ? "/logo" : "";
|
$c->stash->{logo} = defined $c->config->{hydra_logo} ? "/logo" : "";
|
||||||
$c->stash->{tracker} = defined $c->config->{tracker} ? $c->config->{tracker} : "";
|
$c->stash->{tracker} = defined $c->config->{tracker} ? $c->config->{tracker} : "";
|
||||||
$c->stash->{flashMsg} = $c->flash->{flashMsg};
|
$c->stash->{flashMsg} = $c->flash->{flashMsg};
|
||||||
$c->stash->{successMsg} = $c->flash->{successMsg};
|
$c->stash->{successMsg} = $c->flash->{successMsg};
|
||||||
|
$c->stash->{localStore} = isLocalStore;
|
||||||
|
|
||||||
$c->stash->{isPrivateHydra} = $c->config->{private} // "0" ne "0";
|
$c->stash->{isPrivateHydra} = $c->config->{private} // "0" ne "0";
|
||||||
|
|
||||||
@@ -77,7 +86,7 @@ sub begin :Private {
|
|||||||
$_->supportedInputTypes($c->stash->{inputTypes}) foreach @{$c->hydra_plugins};
|
$_->supportedInputTypes($c->stash->{inputTypes}) foreach @{$c->hydra_plugins};
|
||||||
|
|
||||||
# XSRF protection: require POST requests to have the same origin.
|
# XSRF protection: require POST requests to have the same origin.
|
||||||
if ($c->req->method eq "POST" && $c->req->path ne "api/push-github") {
|
if ($c->req->method eq "POST" && $c->req->path ne "api/push-github" && $c->req->path ne "api/push-gitea") {
|
||||||
my $referer = $c->req->header('Referer');
|
my $referer = $c->req->header('Referer');
|
||||||
$referer //= $c->req->header('Origin');
|
$referer //= $c->req->header('Origin');
|
||||||
my $base = $c->req->base;
|
my $base = $c->req->base;
|
||||||
@@ -157,7 +166,7 @@ sub status_GET {
|
|||||||
{ "buildsteps.busy" => { '!=', 0 } },
|
{ "buildsteps.busy" => { '!=', 0 } },
|
||||||
{ order_by => ["globalpriority DESC", "id"],
|
{ order_by => ["globalpriority DESC", "id"],
|
||||||
join => "buildsteps",
|
join => "buildsteps",
|
||||||
columns => [@buildListColumns]
|
columns => [@buildListColumns, 'buildsteps.drvpath', 'buildsteps.type']
|
||||||
})]
|
})]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -169,8 +178,14 @@ sub queue_runner_status_GET {
|
|||||||
my ($self, $c) = @_;
|
my ($self, $c) = @_;
|
||||||
|
|
||||||
#my $status = from_json($c->model('DB::SystemStatus')->find('queue-runner')->status);
|
#my $status = from_json($c->model('DB::SystemStatus')->find('queue-runner')->status);
|
||||||
my $status = decode_json(`hydra-queue-runner --status`);
|
my ($stdout, $stderr);
|
||||||
if ($?) { $status->{status} = "unknown"; }
|
run3(['hydra-queue-runner', '--status'], \undef, \$stdout, \$stderr);
|
||||||
|
my $status;
|
||||||
|
if ($? != 0) {
|
||||||
|
$status = { status => "unknown" };
|
||||||
|
} else {
|
||||||
|
$status = decode_json($stdout);
|
||||||
|
}
|
||||||
my $json = JSON->new->pretty()->canonical();
|
my $json = JSON->new->pretty()->canonical();
|
||||||
|
|
||||||
$c->stash->{template} = 'queue-runner-status.tt';
|
$c->stash->{template} = 'queue-runner-status.tt';
|
||||||
@@ -183,8 +198,10 @@ sub machines :Local Args(0) {
|
|||||||
my ($self, $c) = @_;
|
my ($self, $c) = @_;
|
||||||
my $machines = getMachines;
|
my $machines = getMachines;
|
||||||
|
|
||||||
# Add entry for localhost.
|
# Add entry for localhost. The implicit addition is not needed with queue runner v2
|
||||||
$machines->{''} //= {};
|
if (not $c->config->{'queue_runner_endpoint'}) {
|
||||||
|
$machines->{''} //= {};
|
||||||
|
}
|
||||||
delete $machines->{'localhost'};
|
delete $machines->{'localhost'};
|
||||||
|
|
||||||
my $status = $c->model('DB::SystemStatus')->find("queue-runner");
|
my $status = $c->model('DB::SystemStatus')->find("queue-runner");
|
||||||
@@ -192,9 +209,11 @@ sub machines :Local Args(0) {
|
|||||||
my $ms = decode_json($status->status)->{"machines"};
|
my $ms = decode_json($status->status)->{"machines"};
|
||||||
foreach my $name (keys %{$ms}) {
|
foreach my $name (keys %{$ms}) {
|
||||||
$name = "" if $name eq "localhost";
|
$name = "" if $name eq "localhost";
|
||||||
$machines->{$name} //= {disabled => 1};
|
my $outName = $name;
|
||||||
$machines->{$name}->{nrStepsDone} = $ms->{$name}->{nrStepsDone};
|
$outName = "" if $name eq "ssh://localhost";
|
||||||
$machines->{$name}->{avgStepBuildTime} = $ms->{$name}->{avgStepBuildTime} // 0;
|
$machines->{$outName} //= {disabled => 1};
|
||||||
|
$machines->{$outName}->{nrStepsDone} = $ms->{$name}->{nrStepsDone};
|
||||||
|
$machines->{$outName}->{avgStepBuildTime} = $ms->{$name}->{avgStepBuildTime} // 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,6 +226,19 @@ sub machines :Local Args(0) {
|
|||||||
"where busy != 0 order by machine, stepnr",
|
"where busy != 0 order by machine, stepnr",
|
||||||
{ Slice => {} });
|
{ Slice => {} });
|
||||||
$c->stash->{template} = 'machine-status.tt';
|
$c->stash->{template} = 'machine-status.tt';
|
||||||
|
$c->stash->{human_bytes} = sub {
|
||||||
|
my ($bytes) = @_;
|
||||||
|
return format_bytes($bytes, si => 1);
|
||||||
|
};
|
||||||
|
$c->stash->{pretty_load} = sub {
|
||||||
|
my ($load) = @_;
|
||||||
|
return sprintf('%.2f', $load);
|
||||||
|
};
|
||||||
|
$c->stash->{pretty_percent} = sub {
|
||||||
|
my ($percent) = @_;
|
||||||
|
my $ret = sprintf('%.2f', $percent);
|
||||||
|
return (' ' x (6 - length($ret))) . encode_entities($ret);
|
||||||
|
};
|
||||||
$self->status_ok($c, entity => $c->stash->{machines});
|
$self->status_ok($c, entity => $c->stash->{machines});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,7 +358,7 @@ sub nar :Local :Args(1) {
|
|||||||
else {
|
else {
|
||||||
$path = $Nix::Config::storeDir . "/$path";
|
$path = $Nix::Config::storeDir . "/$path";
|
||||||
|
|
||||||
gone($c, "Path " . $path . " is no longer available.") unless isValidPath($path);
|
gone($c, "Path " . $path . " is no longer available.") unless $MACHINE_LOCAL_STORE->isValidPath($path);
|
||||||
|
|
||||||
$c->stash->{current_view} = 'NixNAR';
|
$c->stash->{current_view} = 'NixNAR';
|
||||||
$c->stash->{storePath} = $path;
|
$c->stash->{storePath} = $path;
|
||||||
@@ -355,6 +387,33 @@ sub nix_cache_info :Path('nix-cache-info') :Args(0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sub realisations :Path('realisations') :Args(StrMatch[REALISATIONS_REGEX]) {
|
||||||
|
my ($self, $c, $realisation) = @_;
|
||||||
|
|
||||||
|
if (!isLocalStore) {
|
||||||
|
notFound($c, "There is no binary cache here.");
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
my ($rawDrvOutput) = $realisation =~ REALISATIONS_REGEX;
|
||||||
|
my $rawRealisation = $MACHINE_LOCAL_STORE->queryRawRealisation($rawDrvOutput);
|
||||||
|
|
||||||
|
if (!$rawRealisation) {
|
||||||
|
$c->response->status(404);
|
||||||
|
$c->response->content_type('text/plain');
|
||||||
|
$c->stash->{plain}->{data} = "does not exist\n";
|
||||||
|
$c->forward('Hydra::View::Plain');
|
||||||
|
setCacheHeaders($c, 60 * 60);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$c->response->content_type('text/plain');
|
||||||
|
$c->stash->{plain}->{data} = $rawRealisation;
|
||||||
|
$c->forward('Hydra::View::Plain');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
sub narinfo :Path :Args(StrMatch[NARINFO_REGEX]) {
|
sub narinfo :Path :Args(StrMatch[NARINFO_REGEX]) {
|
||||||
my ($self, $c, $narinfo) = @_;
|
my ($self, $c, $narinfo) = @_;
|
||||||
|
|
||||||
@@ -366,7 +425,7 @@ sub narinfo :Path :Args(StrMatch[NARINFO_REGEX]) {
|
|||||||
my ($hash) = $narinfo =~ NARINFO_REGEX;
|
my ($hash) = $narinfo =~ NARINFO_REGEX;
|
||||||
|
|
||||||
die("Hash length was not 32") if length($hash) != 32;
|
die("Hash length was not 32") if length($hash) != 32;
|
||||||
my $path = queryPathFromHashPart($hash);
|
my $path = $MACHINE_LOCAL_STORE->queryPathFromHashPart($hash);
|
||||||
|
|
||||||
if (!$path) {
|
if (!$path) {
|
||||||
$c->response->status(404);
|
$c->response->status(404);
|
||||||
@@ -524,7 +583,7 @@ sub log :Local :Args(1) {
|
|||||||
my $logPrefix = $c->config->{log_prefix};
|
my $logPrefix = $c->config->{log_prefix};
|
||||||
|
|
||||||
if (defined $logPrefix) {
|
if (defined $logPrefix) {
|
||||||
$c->res->redirect($logPrefix . "log/" . basename($drvPath));
|
$c->res->redirect($logPrefix . "log/" . WWW::Form::UrlEncoded::PP::url_encode(basename($drvPath)));
|
||||||
} else {
|
} else {
|
||||||
notFound($c, "The build log of $drvPath is not available.");
|
notFound($c, "The build log of $drvPath is not available.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,11 +106,11 @@ sub doEmailLogin {
|
|||||||
my $allowed_domains = $c->config->{allowed_domains} // ($c->config->{persona_allowed_domains} // "");
|
my $allowed_domains = $c->config->{allowed_domains} // ($c->config->{persona_allowed_domains} // "");
|
||||||
if ($allowed_domains ne "") {
|
if ($allowed_domains ne "") {
|
||||||
my $email_ok = 0;
|
my $email_ok = 0;
|
||||||
my @domains = split ',', $allowed_domains;
|
my @domains = split /,/, $allowed_domains;
|
||||||
map { $_ =~ s/^\s*(.*?)\s*$/$1/ } @domains;
|
map { $_ =~ s/^\s*(.*?)\s*$/$1/ } @domains;
|
||||||
|
|
||||||
foreach my $domain (@domains) {
|
foreach my $domain (@domains) {
|
||||||
$email_ok = $email_ok || ((split '@', $email)[1] eq $domain);
|
$email_ok = $email_ok || ((split /@/, $email)[1] eq $domain);
|
||||||
}
|
}
|
||||||
error($c, "Your email address does not belong to a domain that is allowed to log in.\n")
|
error($c, "Your email address does not belong to a domain that is allowed to log in.\n")
|
||||||
unless $email_ok;
|
unless $email_ok;
|
||||||
|
|||||||
@@ -32,12 +32,26 @@ sub buildDiff {
|
|||||||
removed => [],
|
removed => [],
|
||||||
unfinished => [],
|
unfinished => [],
|
||||||
aborted => [],
|
aborted => [],
|
||||||
failed => [],
|
|
||||||
|
# These summary counters cut across the categories to determine whether
|
||||||
|
# actions such as "Restart all failed" or "Bump queue" are available.
|
||||||
|
totalAborted => 0,
|
||||||
|
totalFailed => 0,
|
||||||
|
totalQueued => 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
my $n = 0;
|
my $n = 0;
|
||||||
foreach my $build (@{$builds}) {
|
foreach my $build (@{$builds}) {
|
||||||
my $aborted = $build->finished != 0 && ($build->buildstatus == 3 || $build->buildstatus == 4);
|
my $aborted = $build->finished != 0 && (
|
||||||
|
# aborted
|
||||||
|
$build->buildstatus == 3
|
||||||
|
# cancelled
|
||||||
|
|| $build->buildstatus == 4
|
||||||
|
# timeout
|
||||||
|
|| $build->buildstatus == 7
|
||||||
|
# log limit exceeded
|
||||||
|
|| $build->buildstatus == 10
|
||||||
|
);
|
||||||
my $d;
|
my $d;
|
||||||
my $found = 0;
|
my $found = 0;
|
||||||
while ($n < scalar(@{$builds2})) {
|
while ($n < scalar(@{$builds2})) {
|
||||||
@@ -71,8 +85,15 @@ sub buildDiff {
|
|||||||
} else {
|
} else {
|
||||||
push @{$ret->{new}}, $build if !$found;
|
push @{$ret->{new}}, $build if !$found;
|
||||||
}
|
}
|
||||||
if (defined $build->buildstatus && $build->buildstatus != 0) {
|
|
||||||
push @{$ret->{failed}}, $build;
|
if ($build->finished != 0 && $build->buildstatus != 0) {
|
||||||
|
if ($aborted) {
|
||||||
|
++$ret->{totalAborted};
|
||||||
|
} else {
|
||||||
|
++$ret->{totalFailed};
|
||||||
|
}
|
||||||
|
} elsif ($build->finished == 0) {
|
||||||
|
++$ret->{totalQueued};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ our @EXPORT = qw(
|
|||||||
forceLogin requireUser requireProjectOwner requireRestartPrivileges requireAdmin requirePost isAdmin isProjectOwner
|
forceLogin requireUser requireProjectOwner requireRestartPrivileges requireAdmin requirePost isAdmin isProjectOwner
|
||||||
requireBumpPrivileges
|
requireBumpPrivileges
|
||||||
requireCancelBuildPrivileges
|
requireCancelBuildPrivileges
|
||||||
|
requireEvalJobsetPrivileges
|
||||||
trim
|
trim
|
||||||
getLatestFinishedEval getFirstEval
|
getLatestFinishedEval getFirstEval
|
||||||
paramToList
|
paramToList
|
||||||
@@ -186,6 +187,27 @@ sub isProjectOwner {
|
|||||||
defined $c->model('DB::ProjectMembers')->find({ project => $project, userName => $c->user->username }));
|
defined $c->model('DB::ProjectMembers')->find({ project => $project, userName => $c->user->username }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub hasEvalJobsetRole {
|
||||||
|
my ($c) = @_;
|
||||||
|
return $c->user_exists && $c->check_user_roles("eval-jobset");
|
||||||
|
}
|
||||||
|
|
||||||
|
sub mayEvalJobset {
|
||||||
|
my ($c, $project) = @_;
|
||||||
|
return
|
||||||
|
$c->user_exists &&
|
||||||
|
(isAdmin($c) ||
|
||||||
|
hasEvalJobsetRole($c) ||
|
||||||
|
isProjectOwner($c, $project));
|
||||||
|
}
|
||||||
|
|
||||||
|
sub requireEvalJobsetPrivileges {
|
||||||
|
my ($c, $project) = @_;
|
||||||
|
requireUser($c);
|
||||||
|
accessDenied($c, "Only the project members, administrators, and accounts with eval-jobset privileges can perform this operation.")
|
||||||
|
unless mayEvalJobset($c, $project);
|
||||||
|
}
|
||||||
|
|
||||||
sub hasCancelBuildRole {
|
sub hasCancelBuildRole {
|
||||||
my ($c) = @_;
|
my ($c) = @_;
|
||||||
return $c->user_exists && $c->check_user_roles('cancel-build');
|
return $c->user_exists && $c->check_user_roles('cancel-build');
|
||||||
@@ -272,7 +294,7 @@ sub requireAdmin {
|
|||||||
|
|
||||||
sub requirePost {
|
sub requirePost {
|
||||||
my ($c) = @_;
|
my ($c) = @_;
|
||||||
error($c, "Request must be POSTed.") if $c->request->method ne "POST";
|
error($c, "Request must be POSTed.", 405) if $c->request->method ne "POST";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,14 @@ use Nix::Store;
|
|||||||
use Encode;
|
use Encode;
|
||||||
use Sys::Hostname::Long;
|
use Sys::Hostname::Long;
|
||||||
use IPC::Run;
|
use IPC::Run;
|
||||||
|
use IPC::Run3;
|
||||||
|
use LWP::UserAgent;
|
||||||
|
use JSON::MaybeXS;
|
||||||
use UUID4::Tiny qw(is_uuid4_string);
|
use UUID4::Tiny qw(is_uuid4_string);
|
||||||
|
|
||||||
our @ISA = qw(Exporter);
|
our @ISA = qw(Exporter);
|
||||||
our @EXPORT = qw(
|
our @EXPORT = qw(
|
||||||
|
addToStore
|
||||||
cancelBuilds
|
cancelBuilds
|
||||||
constructRunCommandLogPath
|
constructRunCommandLogPath
|
||||||
findLog
|
findLog
|
||||||
@@ -36,12 +40,16 @@ our @EXPORT = qw(
|
|||||||
jobsetOverview
|
jobsetOverview
|
||||||
jobsetOverview_
|
jobsetOverview_
|
||||||
pathIsInsidePrefix
|
pathIsInsidePrefix
|
||||||
|
readIntoSocket
|
||||||
readNixFile
|
readNixFile
|
||||||
registerRoot
|
registerRoot
|
||||||
restartBuilds
|
restartBuilds
|
||||||
run
|
runCommand
|
||||||
|
$MACHINE_LOCAL_STORE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
our $MACHINE_LOCAL_STORE = Nix::Store->new();
|
||||||
|
|
||||||
|
|
||||||
sub getHydraHome {
|
sub getHydraHome {
|
||||||
my $dir = $ENV{"HYDRA_HOME"} or die "The HYDRA_HOME directory does not exist!\n";
|
my $dir = $ENV{"HYDRA_HOME"} or die "The HYDRA_HOME directory does not exist!\n";
|
||||||
@@ -171,6 +179,9 @@ sub getDrvLogPath {
|
|||||||
for ($fn . $bucketed, $fn . $bucketed . ".bz2") {
|
for ($fn . $bucketed, $fn . $bucketed . ".bz2") {
|
||||||
return $_ if -f $_;
|
return $_ if -f $_;
|
||||||
}
|
}
|
||||||
|
for ($fn . $bucketed, $fn . $bucketed . ".zst") {
|
||||||
|
return $_ if -f $_;
|
||||||
|
}
|
||||||
return undef;
|
return undef;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +198,10 @@ sub findLog {
|
|||||||
|
|
||||||
return undef if scalar @outPaths == 0;
|
return undef if scalar @outPaths == 0;
|
||||||
|
|
||||||
|
# Filter out any NULLs. Content-addressed derivations
|
||||||
|
# that haven't built yet or failed to build may have a NULL outPath.
|
||||||
|
@outPaths = grep {defined} @outPaths;
|
||||||
|
|
||||||
my @steps = $c->model('DB::BuildSteps')->search(
|
my @steps = $c->model('DB::BuildSteps')->search(
|
||||||
{ path => { -in => [@outPaths] } },
|
{ path => { -in => [@outPaths] } },
|
||||||
{ select => ["drvpath"]
|
{ select => ["drvpath"]
|
||||||
@@ -286,8 +301,7 @@ sub getEvals {
|
|||||||
|
|
||||||
my @evals = $evals_result_set->search(
|
my @evals = $evals_result_set->search(
|
||||||
{ hasnewbuilds => 1 },
|
{ hasnewbuilds => 1 },
|
||||||
{ order_by => "$me.id DESC", rows => $rows, offset => $offset
|
{ order_by => "$me.id DESC", rows => $rows, offset => $offset });
|
||||||
, prefetch => { evaluationerror => [ ] } });
|
|
||||||
my @res = ();
|
my @res = ();
|
||||||
my $cache = {};
|
my $cache = {};
|
||||||
|
|
||||||
@@ -330,37 +344,68 @@ sub getEvals {
|
|||||||
|
|
||||||
sub getMachines {
|
sub getMachines {
|
||||||
my %machines = ();
|
my %machines = ();
|
||||||
|
my $config = getHydraConfig();
|
||||||
|
|
||||||
my @machinesFiles = split /:/, ($ENV{"NIX_REMOTE_SYSTEMS"} || "/etc/nix/machines");
|
if ($config->{'queue_runner_endpoint'}) {
|
||||||
|
my $ua = LWP::UserAgent->new();
|
||||||
|
my $resp = $ua->get($config->{'queue_runner_endpoint'} . "/status/machines");
|
||||||
|
if (not $resp->is_success) {
|
||||||
|
print STDERR "Unable to ask queue runner for machines\n";
|
||||||
|
return \%machines;
|
||||||
|
}
|
||||||
|
|
||||||
for my $machinesFile (@machinesFiles) {
|
my $data = decode_json($resp->decoded_content) or return \%machines;
|
||||||
next unless -e $machinesFile;
|
my $machinesData = $data->{machines};
|
||||||
open(my $conf, "<", $machinesFile) or die;
|
|
||||||
while (my $line = <$conf>) {
|
|
||||||
chomp($line);
|
|
||||||
$line =~ s/\#.*$//g;
|
|
||||||
next if $line =~ /^\s*$/;
|
|
||||||
my @tokens = split /\s+/, $line;
|
|
||||||
|
|
||||||
if (!defined($tokens[5]) || $tokens[5] eq "-") {
|
foreach my $machineName (keys %$machinesData) {
|
||||||
$tokens[5] = "";
|
my $machine = %$machinesData{$machineName};
|
||||||
}
|
$machines{$machineName} =
|
||||||
my @supportedFeatures = split(/,/, $tokens[5] || "");
|
{ systemTypes => $machine->{systems}
|
||||||
|
, maxJobs => $machine->{maxJobs}
|
||||||
if (!defined($tokens[6]) || $tokens[6] eq "-") {
|
, speedFactor => $machine->{speedFactor}
|
||||||
$tokens[6] = "";
|
, supportedFeatures => [ @{$machine->{supportedFeatures}}, @{$machine->{mandatoryFeatures}} ]
|
||||||
}
|
, mandatoryFeatures => [ @{$machine->{mandatoryFeatures}} ]
|
||||||
my @mandatoryFeatures = split(/,/, $tokens[6] || "");
|
# New fields for the machine status
|
||||||
$machines{$tokens[0]} =
|
, primarySystemType => $machine->{systems}[0]
|
||||||
{ systemTypes => [ split(/,/, $tokens[1]) ]
|
, hasCapacity => $machine->{hasCapacity}
|
||||||
, sshKeys => $tokens[2]
|
, hasDynamicCapacity => $machine->{hasDynamicCapacity}
|
||||||
, maxJobs => int($tokens[3])
|
, hasStaticCapacity => $machine->{hasStaticCapacity}
|
||||||
, speedFactor => 1.0 * (defined $tokens[4] ? int($tokens[4]) : 1)
|
, score => $machine->{score}
|
||||||
, supportedFeatures => [ @supportedFeatures, @mandatoryFeatures ]
|
, stats => $machine->{stats}
|
||||||
, mandatoryFeatures => [ @mandatoryFeatures ]
|
, memTotal => $machine->{totalMem}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
close $conf;
|
} else {
|
||||||
|
my @machinesFiles = split /:/, ($ENV{"NIX_REMOTE_SYSTEMS"} || "/etc/nix/machines");
|
||||||
|
|
||||||
|
for my $machinesFile (@machinesFiles) {
|
||||||
|
next unless -e $machinesFile;
|
||||||
|
open(my $conf, "<", $machinesFile) or die;
|
||||||
|
while (my $line = <$conf>) {
|
||||||
|
chomp($line);
|
||||||
|
$line =~ s/\#.*$//g;
|
||||||
|
next if $line =~ /^\s*$/;
|
||||||
|
my @tokens = split /\s+/, $line;
|
||||||
|
|
||||||
|
if (!defined($tokens[5]) || $tokens[5] eq "-") {
|
||||||
|
$tokens[5] = "";
|
||||||
|
}
|
||||||
|
my @supportedFeatures = split(/,/, $tokens[5] || "");
|
||||||
|
|
||||||
|
if (!defined($tokens[6]) || $tokens[6] eq "-") {
|
||||||
|
$tokens[6] = "";
|
||||||
|
}
|
||||||
|
my @mandatoryFeatures = split(/,/, $tokens[6] || "");
|
||||||
|
$machines{$tokens[0]} =
|
||||||
|
{ systemTypes => [ split(/,/, $tokens[1]) ]
|
||||||
|
, maxJobs => int($tokens[3])
|
||||||
|
, speedFactor => 1.0 * (defined $tokens[4] ? int($tokens[4]) : 1)
|
||||||
|
, supportedFeatures => [ @supportedFeatures, @mandatoryFeatures ]
|
||||||
|
, mandatoryFeatures => [ @mandatoryFeatures ]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
close $conf;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return \%machines;
|
return \%machines;
|
||||||
@@ -407,11 +452,21 @@ sub pathIsInsidePrefix {
|
|||||||
return $cur;
|
return $cur;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub readIntoSocket{
|
||||||
|
my (%args) = @_;
|
||||||
|
my $sock;
|
||||||
|
|
||||||
|
eval {
|
||||||
|
open($sock, "-|", @{$args{cmd}}) or die q(failed to open socket from command:\n $x);
|
||||||
|
};
|
||||||
|
|
||||||
|
return $sock;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
sub run {
|
sub runCommand {
|
||||||
my (%args) = @_;
|
my (%args) = @_;
|
||||||
my $res = { stdout => "", stderr => "" };
|
my $res = { stdout => "", stderr => "" };
|
||||||
my $stdin = "";
|
my $stdin = "";
|
||||||
@@ -451,7 +506,7 @@ sub run {
|
|||||||
|
|
||||||
sub grab {
|
sub grab {
|
||||||
my (%args) = @_;
|
my (%args) = @_;
|
||||||
my $res = run(%args, grabStderr => 0);
|
my $res = runCommand(%args, grabStderr => 0);
|
||||||
if ($res->{status}) {
|
if ($res->{status}) {
|
||||||
my $msgloc = "(in an indeterminate location)";
|
my $msgloc = "(in an indeterminate location)";
|
||||||
if (defined $args{dir}) {
|
if (defined $args{dir}) {
|
||||||
@@ -494,7 +549,7 @@ sub restartBuilds {
|
|||||||
$builds = $builds->search({ finished => 1 });
|
$builds = $builds->search({ finished => 1 });
|
||||||
|
|
||||||
foreach my $build ($builds->search({}, { columns => ["drvpath"] })) {
|
foreach my $build ($builds->search({}, { columns => ["drvpath"] })) {
|
||||||
next if !isValidPath($build->drvpath);
|
next if !$MACHINE_LOCAL_STORE->isValidPath($build->drvpath);
|
||||||
registerRoot $build->drvpath;
|
registerRoot $build->drvpath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -561,4 +616,14 @@ sub constructRunCommandLogPath {
|
|||||||
return "$hydra_path/runcommand-logs/$bucket/$uuid";
|
return "$hydra_path/runcommand-logs/$bucket/$uuid";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sub addToStore {
|
||||||
|
my ($path) = @_;
|
||||||
|
|
||||||
|
my ($stdout, $stderr);
|
||||||
|
run3(['nix-store', '--add', $path], \undef, \$stdout, \$stderr);
|
||||||
|
die "cannot add path $path to the Nix store: $stderr\n" if $? != 0;
|
||||||
|
return trim($stdout);
|
||||||
|
}
|
||||||
|
|
||||||
1;
|
1;
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use Digest::SHA qw(sha256_hex);
|
|||||||
use File::Path;
|
use File::Path;
|
||||||
use Hydra::Helper::Exec;
|
use Hydra::Helper::Exec;
|
||||||
use Hydra::Helper::Nix;
|
use Hydra::Helper::Nix;
|
||||||
use Nix::Store;
|
|
||||||
|
|
||||||
sub supportedInputTypes {
|
sub supportedInputTypes {
|
||||||
my ($self, $inputTypes) = @_;
|
my ($self, $inputTypes) = @_;
|
||||||
@@ -38,9 +37,9 @@ sub fetchInput {
|
|||||||
(my $cachedInput) = $self->{db}->resultset('CachedBazaarInputs')->search(
|
(my $cachedInput) = $self->{db}->resultset('CachedBazaarInputs')->search(
|
||||||
{uri => $uri, revision => $revision});
|
{uri => $uri, revision => $revision});
|
||||||
|
|
||||||
addTempRoot($cachedInput->storepath) if defined $cachedInput;
|
$MACHINE_LOCAL_STORE->addTempRoot($cachedInput->storepath) if defined $cachedInput;
|
||||||
|
|
||||||
if (defined $cachedInput && isValidPath($cachedInput->storepath)) {
|
if (defined $cachedInput && $MACHINE_LOCAL_STORE->isValidPath($cachedInput->storepath)) {
|
||||||
$storePath = $cachedInput->storepath;
|
$storePath = $cachedInput->storepath;
|
||||||
$sha256 = $cachedInput->sha256hash;
|
$sha256 = $cachedInput->sha256hash;
|
||||||
} else {
|
} else {
|
||||||
@@ -58,7 +57,7 @@ sub fetchInput {
|
|||||||
($sha256, $storePath) = split ' ', $stdout;
|
($sha256, $storePath) = split ' ', $stdout;
|
||||||
|
|
||||||
# FIXME: time window between nix-prefetch-bzr and addTempRoot.
|
# FIXME: time window between nix-prefetch-bzr and addTempRoot.
|
||||||
addTempRoot($storePath);
|
$MACHINE_LOCAL_STORE->addTempRoot($storePath);
|
||||||
|
|
||||||
$self->{db}->txn_do(sub {
|
$self->{db}->txn_do(sub {
|
||||||
$self->{db}->resultset('CachedBazaarInputs')->create(
|
$self->{db}->resultset('CachedBazaarInputs')->create(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use HTTP::Request;
|
|||||||
use LWP::UserAgent;
|
use LWP::UserAgent;
|
||||||
use JSON::MaybeXS;
|
use JSON::MaybeXS;
|
||||||
use Hydra::Helper::CatalystUtils;
|
use Hydra::Helper::CatalystUtils;
|
||||||
|
use Hydra::Helper::Nix;
|
||||||
use File::Temp;
|
use File::Temp;
|
||||||
use POSIX qw(strftime);
|
use POSIX qw(strftime);
|
||||||
|
|
||||||
@@ -43,14 +44,11 @@ sub fetchInput {
|
|||||||
my $ua = LWP::UserAgent->new();
|
my $ua = LWP::UserAgent->new();
|
||||||
_iterate("https://api.bitbucket.com/2.0/repositories/$owner/$repo/pullrequests?state=OPEN", $auth, \%pulls, $ua);
|
_iterate("https://api.bitbucket.com/2.0/repositories/$owner/$repo/pullrequests?state=OPEN", $auth, \%pulls, $ua);
|
||||||
my $tempdir = File::Temp->newdir("bitbucket-pulls" . "XXXXX", TMPDIR => 1);
|
my $tempdir = File::Temp->newdir("bitbucket-pulls" . "XXXXX", TMPDIR => 1);
|
||||||
my $filename = "$tempdir/bitbucket-pulls.json";
|
my $filename = "$tempdir/bitbucket-pulls-sorted.json";
|
||||||
open(my $fh, ">", $filename) or die "Cannot open $filename for writing: $!";
|
open(my $fh, ">", $filename) or die "Cannot open $filename for writing: $!";
|
||||||
print $fh encode_json \%pulls;
|
print $fh JSON::MaybeXS->new(canonical => 1, pretty => 1)->encode(\%pulls);
|
||||||
close $fh;
|
close $fh;
|
||||||
system("jq -S . < $filename > $tempdir/bitbucket-pulls-sorted.json");
|
my $storePath = addToStore($filename);
|
||||||
my $storePath = trim(`nix-store --add "$tempdir/bitbucket-pulls-sorted.json"`
|
|
||||||
or die "cannot copy path $filename to the Nix store.\n");
|
|
||||||
chomp $storePath;
|
|
||||||
my $timestamp = time;
|
my $timestamp = time;
|
||||||
return { storePath => $storePath, revision => strftime "%Y%m%d%H%M%S", gmtime($timestamp) };
|
return { storePath => $storePath, revision => strftime "%Y%m%d%H%M%S", gmtime($timestamp) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,24 @@ use Hydra::Helper::CatalystUtils;
|
|||||||
sub stepFinished {
|
sub stepFinished {
|
||||||
my ($self, $step, $logPath) = @_;
|
my ($self, $step, $logPath) = @_;
|
||||||
|
|
||||||
my $doCompress = $self->{config}->{'compress_build_logs'} // "1";
|
my $doCompress = $self->{config}->{'compress_build_logs'} // '1';
|
||||||
|
my $silent = $self->{config}->{'compress_build_logs_silent'} // '0';
|
||||||
|
my $compression = $self->{config}->{'compress_build_logs_compression'} // 'bzip2';
|
||||||
|
|
||||||
if ($doCompress eq "1" && -e $logPath) {
|
if (not -e $logPath or $doCompress ne "1") {
|
||||||
print STDERR "compressing ‘$logPath’...\n";
|
return;
|
||||||
system("bzip2", "--force", $logPath);
|
}
|
||||||
|
|
||||||
|
if ($silent ne '1') {
|
||||||
|
print STDERR "compressing '$logPath' with $compression...\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($compression eq 'bzip2') {
|
||||||
|
system('bzip2', '--force', $logPath);
|
||||||
|
} elsif ($compression eq 'zstd') {
|
||||||
|
system('zstd', '--rm', '--quiet', '-T0', $logPath);
|
||||||
|
} else {
|
||||||
|
print STDERR "unknown compression type '$compression'\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use Digest::SHA qw(sha256_hex);
|
|||||||
use File::Path;
|
use File::Path;
|
||||||
use Hydra::Helper::Exec;
|
use Hydra::Helper::Exec;
|
||||||
use Hydra::Helper::Nix;
|
use Hydra::Helper::Nix;
|
||||||
use Nix::Store;
|
use IPC::Run3;
|
||||||
|
|
||||||
sub supportedInputTypes {
|
sub supportedInputTypes {
|
||||||
my ($self, $inputTypes) = @_;
|
my ($self, $inputTypes) = @_;
|
||||||
@@ -32,7 +32,7 @@ sub fetchInput {
|
|||||||
my $stdout = ""; my $stderr = ""; my $res;
|
my $stdout = ""; my $stderr = ""; my $res;
|
||||||
if (! -d $clonePath) {
|
if (! -d $clonePath) {
|
||||||
# Clone the repository.
|
# Clone the repository.
|
||||||
$res = run(timeout => 600,
|
$res = runCommand(timeout => 600,
|
||||||
cmd => ["darcs", "get", "--lazy", $uri, $clonePath],
|
cmd => ["darcs", "get", "--lazy", $uri, $clonePath],
|
||||||
dir => $ENV{"TMPDIR"});
|
dir => $ENV{"TMPDIR"});
|
||||||
die "Error getting darcs repo at `$uri':\n$stderr" if $res->{status};
|
die "Error getting darcs repo at `$uri':\n$stderr" if $res->{status};
|
||||||
@@ -58,7 +58,7 @@ sub fetchInput {
|
|||||||
{uri => $uri, revision => $revision},
|
{uri => $uri, revision => $revision},
|
||||||
{rows => 1});
|
{rows => 1});
|
||||||
|
|
||||||
if (defined $cachedInput && isValidPath($cachedInput->storepath)) {
|
if (defined $cachedInput && $MACHINE_LOCAL_STORE->isValidPath($cachedInput->storepath)) {
|
||||||
$storePath = $cachedInput->storepath;
|
$storePath = $cachedInput->storepath;
|
||||||
$sha256 = $cachedInput->sha256hash;
|
$sha256 = $cachedInput->sha256hash;
|
||||||
$revision = $cachedInput->revision;
|
$revision = $cachedInput->revision;
|
||||||
@@ -71,12 +71,15 @@ sub fetchInput {
|
|||||||
(system "darcs", "get", "--lazy", $clonePath, "$tmpDir/export", "--quiet",
|
(system "darcs", "get", "--lazy", $clonePath, "$tmpDir/export", "--quiet",
|
||||||
"--to-match", "hash $revision") == 0
|
"--to-match", "hash $revision") == 0
|
||||||
or die "darcs export failed";
|
or die "darcs export failed";
|
||||||
$revCount = `darcs changes --count --repodir $tmpDir/export`; chomp $revCount;
|
my ($stdout, $stderr);
|
||||||
die "darcs changes --count failed" if $? != 0;
|
run3(['darcs', 'changes', '--count', '--repodir', "$tmpDir/export"], \undef, \$stdout, \$stderr);
|
||||||
|
die "darcs changes --count failed: $stderr\n" if $? != 0;
|
||||||
|
$revCount = $stdout;
|
||||||
|
chomp $revCount;
|
||||||
|
|
||||||
system "rm", "-rf", "$tmpDir/export/_darcs";
|
system "rm", "-rf", "$tmpDir/export/_darcs";
|
||||||
$storePath = addToStore("$tmpDir/export", 1, "sha256");
|
$storePath = $MACHINE_LOCAL_STORE->addToStore("$tmpDir/export", 1, "sha256");
|
||||||
$sha256 = queryPathHash($storePath);
|
$sha256 = $MACHINE_LOCAL_STORE->queryPathHash($storePath);
|
||||||
$sha256 =~ s/sha256://;
|
$sha256 =~ s/sha256://;
|
||||||
|
|
||||||
$self->{db}->txn_do(sub {
|
$self->{db}->txn_do(sub {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ sub buildFinished {
|
|||||||
|
|
||||||
my $to = $build->jobset->emailoverride ne "" ? $build->jobset->emailoverride : $build->maintainers;
|
my $to = $build->jobset->emailoverride ne "" ? $build->jobset->emailoverride : $build->maintainers;
|
||||||
|
|
||||||
foreach my $address (split ",", ($to // "")) {
|
foreach my $address (split /,/, ($to // "")) {
|
||||||
$address = trim $address;
|
$address = trim $address;
|
||||||
|
|
||||||
$addresses{$address} //= { builds => [] };
|
$addresses{$address} //= { builds => [] };
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ sub _parseValue {
|
|||||||
$start_options = 2;
|
$start_options = 2;
|
||||||
}
|
}
|
||||||
foreach my $option (@parts[$start_options .. $#parts]) {
|
foreach my $option (@parts[$start_options .. $#parts]) {
|
||||||
(my $key, my $value) = split('=', $option);
|
(my $key, my $value) = split(/=/, $option);
|
||||||
$options->{$key} = $value;
|
$options->{$key} = $value;
|
||||||
}
|
}
|
||||||
return ($uri, $branch, $deepClone, $options);
|
return ($uri, $branch, $deepClone, $options);
|
||||||
@@ -137,8 +137,8 @@ sub fetchInput {
|
|||||||
my $res;
|
my $res;
|
||||||
if (! -d $clonePath) {
|
if (! -d $clonePath) {
|
||||||
# Clone everything and fetch the branch.
|
# Clone everything and fetch the branch.
|
||||||
$res = run(cmd => ["git", "init", $clonePath]);
|
$res = runCommand(cmd => ["git", "init", $clonePath]);
|
||||||
$res = run(cmd => ["git", "remote", "add", "origin", "--", $uri], dir => $clonePath) unless $res->{status};
|
$res = runCommand(cmd => ["git", "remote", "add", "origin", "--", $uri], dir => $clonePath) unless $res->{status};
|
||||||
die "error creating git repo in `$clonePath':\n$res->{stderr}" if $res->{status};
|
die "error creating git repo in `$clonePath':\n$res->{stderr}" if $res->{status};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,9 +146,9 @@ sub fetchInput {
|
|||||||
# the remote branch for whatever the repository state is. This command mirrors
|
# the remote branch for whatever the repository state is. This command mirrors
|
||||||
# only one branch of the remote repository.
|
# only one branch of the remote repository.
|
||||||
my $localBranch = _isHash($branch) ? "_hydra_tmp" : $branch;
|
my $localBranch = _isHash($branch) ? "_hydra_tmp" : $branch;
|
||||||
$res = run(cmd => ["git", "fetch", "-fu", "origin", "+$branch:$localBranch"], dir => $clonePath,
|
$res = runCommand(cmd => ["git", "fetch", "-fu", "origin", "+$branch:$localBranch"], dir => $clonePath,
|
||||||
timeout => $cfg->{timeout});
|
timeout => $cfg->{timeout});
|
||||||
$res = run(cmd => ["git", "fetch", "-fu", "origin"], dir => $clonePath, timeout => $cfg->{timeout}) if $res->{status};
|
$res = runCommand(cmd => ["git", "fetch", "-fu", "origin"], dir => $clonePath, timeout => $cfg->{timeout}) if $res->{status};
|
||||||
die "error fetching latest change from git repo at `$uri':\n$res->{stderr}" if $res->{status};
|
die "error fetching latest change from git repo at `$uri':\n$res->{stderr}" if $res->{status};
|
||||||
|
|
||||||
# If deepClone is defined, then we look at the content of the repository
|
# If deepClone is defined, then we look at the content of the repository
|
||||||
@@ -156,16 +156,16 @@ sub fetchInput {
|
|||||||
if (defined $deepClone) {
|
if (defined $deepClone) {
|
||||||
|
|
||||||
# Is the target branch a topgit branch?
|
# Is the target branch a topgit branch?
|
||||||
$res = run(cmd => ["git", "ls-tree", "-r", "$branch", ".topgit"], dir => $clonePath);
|
$res = runCommand(cmd => ["git", "ls-tree", "-r", "$branch", ".topgit"], dir => $clonePath);
|
||||||
|
|
||||||
if ($res->{stdout} ne "") {
|
if ($res->{stdout} ne "") {
|
||||||
# Checkout the branch to look at its content.
|
# Checkout the branch to look at its content.
|
||||||
$res = run(cmd => ["git", "checkout", "--force", "$branch"], dir => $clonePath);
|
$res = runCommand(cmd => ["git", "checkout", "--force", "$branch"], dir => $clonePath);
|
||||||
die "error checking out Git branch '$branch' at `$uri':\n$res->{stderr}" if $res->{status};
|
die "error checking out Git branch '$branch' at `$uri':\n$res->{stderr}" if $res->{status};
|
||||||
|
|
||||||
# This is a TopGit branch. Fetch all the topic branches so
|
# This is a TopGit branch. Fetch all the topic branches so
|
||||||
# that builders can run "tg patch" and similar.
|
# that builders can run "tg patch" and similar.
|
||||||
$res = run(cmd => ["tg", "remote", "--populate", "origin"], dir => $clonePath, timeout => $cfg->{timeout});
|
$res = runCommand(cmd => ["tg", "remote", "--populate", "origin"], dir => $clonePath, timeout => $cfg->{timeout});
|
||||||
print STDERR "warning: `tg remote --populate origin' failed:\n$res->{stderr}" if $res->{status};
|
print STDERR "warning: `tg remote --populate origin' failed:\n$res->{stderr}" if $res->{status};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,9 +186,9 @@ sub fetchInput {
|
|||||||
{uri => $uri, branch => $branch, revision => $revision, isdeepclone => defined($deepClone) ? 1 : 0},
|
{uri => $uri, branch => $branch, revision => $revision, isdeepclone => defined($deepClone) ? 1 : 0},
|
||||||
{rows => 1});
|
{rows => 1});
|
||||||
|
|
||||||
addTempRoot($cachedInput->storepath) if defined $cachedInput;
|
$MACHINE_LOCAL_STORE->addTempRoot($cachedInput->storepath) if defined $cachedInput;
|
||||||
|
|
||||||
if (defined $cachedInput && isValidPath($cachedInput->storepath)) {
|
if (defined $cachedInput && $MACHINE_LOCAL_STORE->isValidPath($cachedInput->storepath)) {
|
||||||
$storePath = $cachedInput->storepath;
|
$storePath = $cachedInput->storepath;
|
||||||
$sha256 = $cachedInput->sha256hash;
|
$sha256 = $cachedInput->sha256hash;
|
||||||
$revision = $cachedInput->revision;
|
$revision = $cachedInput->revision;
|
||||||
@@ -217,7 +217,7 @@ sub fetchInput {
|
|||||||
($sha256, $storePath) = split ' ', grab(cmd => ["nix-prefetch-git", $clonePath, $revision], chomp => 1);
|
($sha256, $storePath) = split ' ', grab(cmd => ["nix-prefetch-git", $clonePath, $revision], chomp => 1);
|
||||||
|
|
||||||
# FIXME: time window between nix-prefetch-git and addTempRoot.
|
# FIXME: time window between nix-prefetch-git and addTempRoot.
|
||||||
addTempRoot($storePath);
|
$MACHINE_LOCAL_STORE->addTempRoot($storePath);
|
||||||
|
|
||||||
$self->{db}->txn_do(sub {
|
$self->{db}->txn_do(sub {
|
||||||
$self->{db}->resultset('CachedGitInputs')->update_or_create(
|
$self->{db}->resultset('CachedGitInputs')->update_or_create(
|
||||||
@@ -265,7 +265,7 @@ sub getCommits {
|
|||||||
|
|
||||||
my $res = [];
|
my $res = [];
|
||||||
foreach my $line (split /\n/, $out) {
|
foreach my $line (split /\n/, $out) {
|
||||||
my ($revision, $author, $email, $date) = split "\t", $line;
|
my ($revision, $author, $email, $date) = split /\t/, $line;
|
||||||
push @$res, { revision => $revision, author => decode("utf-8", $author), email => $email };
|
push @$res, { revision => $revision, author => decode("utf-8", $author), email => $email };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,9 +63,9 @@ sub common {
|
|||||||
my $accessToken = $self->{config}->{gitea_authorization}->{$repoOwner};
|
my $accessToken = $self->{config}->{gitea_authorization}->{$repoOwner};
|
||||||
|
|
||||||
my $rev = $i->revision;
|
my $rev = $i->revision;
|
||||||
my $domain = URI->new($i->uri)->host;
|
|
||||||
my $host;
|
my $host;
|
||||||
unless (defined $gitea_url) {
|
unless (defined $gitea_url) {
|
||||||
|
my $domain = URI->new($i->uri)->host;
|
||||||
$host = "https://$domain";
|
$host = "https://$domain";
|
||||||
} else {
|
} else {
|
||||||
$host = $gitea_url->value;
|
$host = $gitea_url->value;
|
||||||
@@ -88,10 +88,6 @@ sub buildQueued {
|
|||||||
common(@_, [], 0);
|
common(@_, [], 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
sub buildStarted {
|
|
||||||
common(@_, [], 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
sub buildFinished {
|
sub buildFinished {
|
||||||
common(@_, 2);
|
common(@_, 2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use HTTP::Request;
|
|||||||
use LWP::UserAgent;
|
use LWP::UserAgent;
|
||||||
use JSON::MaybeXS;
|
use JSON::MaybeXS;
|
||||||
use Hydra::Helper::CatalystUtils;
|
use Hydra::Helper::CatalystUtils;
|
||||||
|
use Hydra::Helper::Nix;
|
||||||
use File::Temp;
|
use File::Temp;
|
||||||
use POSIX qw(strftime);
|
use POSIX qw(strftime);
|
||||||
|
|
||||||
@@ -30,10 +31,10 @@ sub _iterate {
|
|||||||
$pulls->{$pull->{number}} = $pull;
|
$pulls->{$pull->{number}} = $pull;
|
||||||
}
|
}
|
||||||
# TODO Make Link header parsing more robust!!!
|
# TODO Make Link header parsing more robust!!!
|
||||||
my @links = split ',', ($res->header("Link") // "");
|
my @links = split /,/, ($res->header("Link") // "");
|
||||||
my $next = "";
|
my $next = "";
|
||||||
foreach my $link (@links) {
|
foreach my $link (@links) {
|
||||||
my ($url, $rel) = split ";", $link;
|
my ($url, $rel) = split /;/, $link;
|
||||||
if (trim($rel) eq 'rel="next"') {
|
if (trim($rel) eq 'rel="next"') {
|
||||||
$next = substr trim($url), 1, -1;
|
$next = substr trim($url), 1, -1;
|
||||||
last;
|
last;
|
||||||
@@ -58,9 +59,7 @@ sub fetchInput {
|
|||||||
print $fh JSON->new->utf8->canonical->encode(\%pulls);
|
print $fh JSON->new->utf8->canonical->encode(\%pulls);
|
||||||
close $fh;
|
close $fh;
|
||||||
|
|
||||||
my $storePath = trim(`nix-store --add "$filename"`
|
my $storePath = addToStore($filename);
|
||||||
or die "cannot copy path $filename to the Nix store.\n");
|
|
||||||
chomp $storePath;
|
|
||||||
my $timestamp = time;
|
my $timestamp = time;
|
||||||
return { storePath => $storePath, revision => strftime "%Y%m%d%H%M%S", gmtime($timestamp) };
|
return { storePath => $storePath, revision => strftime "%Y%m%d%H%M%S", gmtime($timestamp) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use HTTP::Request;
|
|||||||
use LWP::UserAgent;
|
use LWP::UserAgent;
|
||||||
use JSON::MaybeXS;
|
use JSON::MaybeXS;
|
||||||
use Hydra::Helper::CatalystUtils;
|
use Hydra::Helper::CatalystUtils;
|
||||||
|
use Hydra::Helper::Nix;
|
||||||
use File::Temp;
|
use File::Temp;
|
||||||
use POSIX qw(strftime);
|
use POSIX qw(strftime);
|
||||||
|
|
||||||
@@ -17,9 +18,8 @@ tags) from GitHub following a certain naming scheme
|
|||||||
|
|
||||||
=head1 DESCRIPTION
|
=head1 DESCRIPTION
|
||||||
|
|
||||||
This plugin reads the list of branches or tags using GitHub's REST API. The name
|
This plugin reads the list of branches or tags using GitHub's REST API. This
|
||||||
of the reference must follow a particular prefix. This list is stored in the
|
list is stored in the nix-store and used as an input to declarative jobsets.
|
||||||
nix-store and used as an input to declarative jobsets.
|
|
||||||
|
|
||||||
=head1 CONFIGURATION
|
=head1 CONFIGURATION
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ The declarative project C<spec.json> file must contains an input such as
|
|||||||
|
|
||||||
"pulls": {
|
"pulls": {
|
||||||
"type": "github_refs",
|
"type": "github_refs",
|
||||||
"value": "[owner] [repo] heads|tags - [prefix]",
|
"value": "[owner] [repo] [type] - [prefix]",
|
||||||
"emailresponsible": false
|
"emailresponsible": false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,12 +41,11 @@ In the above snippet, C<[owner]> is the repository owner and C<[repo]> is the
|
|||||||
repository name. Also note a literal C<->, which is placed there for the future
|
repository name. Also note a literal C<->, which is placed there for the future
|
||||||
use.
|
use.
|
||||||
|
|
||||||
C<heads|tags> denotes that one of these two is allowed, that is, the third
|
C<[type]> is the type of ref to list. Typical values are "heads", "tags", and
|
||||||
position should hold either the C<heads> or the C<tags> keyword. In case of the former, the plugin
|
"pull". "." will include all types.
|
||||||
will fetch all branches, while in case of the latter, it will fetch the tags.
|
|
||||||
|
|
||||||
C<prefix> denotes the prefix the reference name must start with, in order to be
|
C<prefix> denotes the prefix the reference name must start with, in order to be
|
||||||
included.
|
included. "." will include all references.
|
||||||
|
|
||||||
For example, C<"value": "nixos hydra heads - release/"> refers to
|
For example, C<"value": "nixos hydra heads - release/"> refers to
|
||||||
L<https://github.com/nixos/hydra> repository, and will fetch all branches that
|
L<https://github.com/nixos/hydra> repository, and will fetch all branches that
|
||||||
@@ -84,10 +83,10 @@ sub _iterate {
|
|||||||
$refs->{$ref_name} = $ref;
|
$refs->{$ref_name} = $ref;
|
||||||
}
|
}
|
||||||
# TODO Make Link header parsing more robust!!!
|
# TODO Make Link header parsing more robust!!!
|
||||||
my @links = split ',', $res->header("Link");
|
my @links = split /,/, $res->header("Link");
|
||||||
my $next = "";
|
my $next = "";
|
||||||
foreach my $link (@links) {
|
foreach my $link (@links) {
|
||||||
my ($url, $rel) = split ";", $link;
|
my ($url, $rel) = split /;/, $link;
|
||||||
if (trim($rel) eq 'rel="next"') {
|
if (trim($rel) eq 'rel="next"') {
|
||||||
$next = substr trim($url), 1, -1;
|
$next = substr trim($url), 1, -1;
|
||||||
last;
|
last;
|
||||||
@@ -101,8 +100,6 @@ sub fetchInput {
|
|||||||
return undef if $input_type ne "github_refs";
|
return undef if $input_type ne "github_refs";
|
||||||
|
|
||||||
my ($owner, $repo, $type, $fut, $prefix) = split ' ', $value;
|
my ($owner, $repo, $type, $fut, $prefix) = split ' ', $value;
|
||||||
die "type field is neither 'heads' nor 'tags', but '$type'"
|
|
||||||
unless $type eq 'heads' or $type eq 'tags';
|
|
||||||
|
|
||||||
my $auth = $self->{config}->{github_authorization}->{$owner};
|
my $auth = $self->{config}->{github_authorization}->{$owner};
|
||||||
my $githubEndpoint = $self->{config}->{github_endpoint} // "https://api.github.com";
|
my $githubEndpoint = $self->{config}->{github_endpoint} // "https://api.github.com";
|
||||||
@@ -110,14 +107,11 @@ sub fetchInput {
|
|||||||
my $ua = LWP::UserAgent->new();
|
my $ua = LWP::UserAgent->new();
|
||||||
_iterate("$githubEndpoint/repos/$owner/$repo/git/matching-refs/$type/$prefix?per_page=100", $auth, \%refs, $ua);
|
_iterate("$githubEndpoint/repos/$owner/$repo/git/matching-refs/$type/$prefix?per_page=100", $auth, \%refs, $ua);
|
||||||
my $tempdir = File::Temp->newdir("github-refs" . "XXXXX", TMPDIR => 1);
|
my $tempdir = File::Temp->newdir("github-refs" . "XXXXX", TMPDIR => 1);
|
||||||
my $filename = "$tempdir/github-refs.json";
|
my $filename = "$tempdir/github-refs-sorted.json";
|
||||||
open(my $fh, ">", $filename) or die "Cannot open $filename for writing: $!";
|
open(my $fh, ">", $filename) or die "Cannot open $filename for writing: $!";
|
||||||
print $fh encode_json \%refs;
|
print $fh JSON::MaybeXS->new(canonical => 1, pretty => 1)->encode(\%refs);
|
||||||
close $fh;
|
close $fh;
|
||||||
system("jq -S . < $filename > $tempdir/github-refs-sorted.json");
|
my $storePath = addToStore($filename);
|
||||||
my $storePath = trim(qx{nix-store --add "$tempdir/github-refs-sorted.json"}
|
|
||||||
or die "cannot copy path $filename to the Nix store.\n");
|
|
||||||
chomp $storePath;
|
|
||||||
my $timestamp = time;
|
my $timestamp = time;
|
||||||
return { storePath => $storePath, revision => strftime "%Y%m%d%H%M%S", gmtime($timestamp) };
|
return { storePath => $storePath, revision => strftime "%Y%m%d%H%M%S", gmtime($timestamp) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use HTTP::Request;
|
|||||||
use LWP::UserAgent;
|
use LWP::UserAgent;
|
||||||
use JSON::MaybeXS;
|
use JSON::MaybeXS;
|
||||||
use Hydra::Helper::CatalystUtils;
|
use Hydra::Helper::CatalystUtils;
|
||||||
|
use Hydra::Helper::Nix;
|
||||||
use File::Temp;
|
use File::Temp;
|
||||||
use POSIX qw(strftime);
|
use POSIX qw(strftime);
|
||||||
|
|
||||||
@@ -48,10 +49,10 @@ sub _iterate {
|
|||||||
$pulls->{$pull->{iid}} = $pull;
|
$pulls->{$pull->{iid}} = $pull;
|
||||||
}
|
}
|
||||||
# TODO Make Link header parsing more robust!!!
|
# TODO Make Link header parsing more robust!!!
|
||||||
my @links = split ',', $res->header("Link");
|
my @links = split /,/, $res->header("Link");
|
||||||
my $next = "";
|
my $next = "";
|
||||||
foreach my $link (@links) {
|
foreach my $link (@links) {
|
||||||
my ($url, $rel) = split ";", $link;
|
my ($url, $rel) = split /;/, $link;
|
||||||
if (trim($rel) eq 'rel="next"') {
|
if (trim($rel) eq 'rel="next"') {
|
||||||
$next = substr trim($url), 1, -1;
|
$next = substr trim($url), 1, -1;
|
||||||
last;
|
last;
|
||||||
@@ -81,14 +82,11 @@ sub fetchInput {
|
|||||||
_iterate($url, $baseUrl, \%pulls, $ua, $target_repo_url);
|
_iterate($url, $baseUrl, \%pulls, $ua, $target_repo_url);
|
||||||
|
|
||||||
my $tempdir = File::Temp->newdir("gitlab-pulls" . "XXXXX", TMPDIR => 1);
|
my $tempdir = File::Temp->newdir("gitlab-pulls" . "XXXXX", TMPDIR => 1);
|
||||||
my $filename = "$tempdir/gitlab-pulls.json";
|
my $filename = "$tempdir/gitlab-pulls-sorted.json";
|
||||||
open(my $fh, ">", $filename) or die "Cannot open $filename for writing: $!";
|
open(my $fh, ">", $filename) or die "Cannot open $filename for writing: $!";
|
||||||
print $fh encode_json \%pulls;
|
print $fh JSON::MaybeXS->new(canonical => 1, pretty => 1, utf8 => 1)->encode(\%pulls);
|
||||||
close $fh;
|
close $fh;
|
||||||
system("jq -S . < $filename > $tempdir/gitlab-pulls-sorted.json");
|
my $storePath = addToStore($filename);
|
||||||
my $storePath = trim(`nix-store --add "$tempdir/gitlab-pulls-sorted.json"`
|
|
||||||
or die "cannot copy path $filename to the Nix store.\n");
|
|
||||||
chomp $storePath;
|
|
||||||
my $timestamp = time;
|
my $timestamp = time;
|
||||||
return { storePath => $storePath, revision => strftime "%Y%m%d%H%M%S", gmtime($timestamp) };
|
return { storePath => $storePath, revision => strftime "%Y%m%d%H%M%S", gmtime($timestamp) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use Digest::SHA qw(sha256_hex);
|
|||||||
use File::Path;
|
use File::Path;
|
||||||
use Hydra::Helper::Nix;
|
use Hydra::Helper::Nix;
|
||||||
use Hydra::Helper::Exec;
|
use Hydra::Helper::Exec;
|
||||||
use Nix::Store;
|
|
||||||
use Fcntl qw(:flock);
|
use Fcntl qw(:flock);
|
||||||
|
|
||||||
sub supportedInputTypes {
|
sub supportedInputTypes {
|
||||||
@@ -68,9 +67,9 @@ sub fetchInput {
|
|||||||
(my $cachedInput) = $self->{db}->resultset('CachedHgInputs')->search(
|
(my $cachedInput) = $self->{db}->resultset('CachedHgInputs')->search(
|
||||||
{uri => $uri, branch => $branch, revision => $revision});
|
{uri => $uri, branch => $branch, revision => $revision});
|
||||||
|
|
||||||
addTempRoot($cachedInput->storepath) if defined $cachedInput;
|
$MACHINE_LOCAL_STORE->addTempRoot($cachedInput->storepath) if defined $cachedInput;
|
||||||
|
|
||||||
if (defined $cachedInput && isValidPath($cachedInput->storepath)) {
|
if (defined $cachedInput && $MACHINE_LOCAL_STORE->isValidPath($cachedInput->storepath)) {
|
||||||
$storePath = $cachedInput->storepath;
|
$storePath = $cachedInput->storepath;
|
||||||
$sha256 = $cachedInput->sha256hash;
|
$sha256 = $cachedInput->sha256hash;
|
||||||
} else {
|
} else {
|
||||||
@@ -85,7 +84,7 @@ sub fetchInput {
|
|||||||
($sha256, $storePath) = split ' ', $stdout;
|
($sha256, $storePath) = split ' ', $stdout;
|
||||||
|
|
||||||
# FIXME: time window between nix-prefetch-hg and addTempRoot.
|
# FIXME: time window between nix-prefetch-hg and addTempRoot.
|
||||||
addTempRoot($storePath);
|
$MACHINE_LOCAL_STORE->addTempRoot($storePath);
|
||||||
|
|
||||||
$self->{db}->txn_do(sub {
|
$self->{db}->txn_do(sub {
|
||||||
$self->{db}->resultset('CachedHgInputs')->update_or_create(
|
$self->{db}->resultset('CachedHgInputs')->update_or_create(
|
||||||
@@ -127,7 +126,7 @@ sub getCommits {
|
|||||||
my $res = [];
|
my $res = [];
|
||||||
foreach my $line (split /\n/, $out) {
|
foreach my $line (split /\n/, $out) {
|
||||||
if ($line ne "") {
|
if ($line ne "") {
|
||||||
my ($revision, $author, $email) = split "\t", $line;
|
my ($revision, $author, $email) = split /\t/, $line;
|
||||||
push @$res, { revision => $revision, author => $author, email => $email };
|
push @$res, { revision => $revision, author => $author, email => $email };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,32 +5,44 @@ use warnings;
|
|||||||
use parent 'Hydra::Plugin';
|
use parent 'Hydra::Plugin';
|
||||||
use POSIX qw(strftime);
|
use POSIX qw(strftime);
|
||||||
use Hydra::Helper::Nix;
|
use Hydra::Helper::Nix;
|
||||||
use Nix::Store;
|
use IPC::Run3;
|
||||||
|
|
||||||
sub supportedInputTypes {
|
sub supportedInputTypes {
|
||||||
my ($self, $inputTypes) = @_;
|
my ($self, $inputTypes) = @_;
|
||||||
$inputTypes->{'path'} = 'Local path or URL';
|
$inputTypes->{'path'} = 'Local path or URL';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub _parseValue {
|
||||||
|
# The input is a local path or URL, optionally followed by a
|
||||||
|
# time period specified in seconds.
|
||||||
|
my ($config, $value) = @_;
|
||||||
|
my @parts = split ' ', $value;
|
||||||
|
(my $uri, my $freq) = @parts;
|
||||||
|
# By default don't check a path more often than every 30 seconds,
|
||||||
|
# but the second path argument can change that value or the global
|
||||||
|
# path_input_cache_validity_seconds configuration, in that order.
|
||||||
|
my $timeout = defined $freq ? $freq : ($config->{path_input_cache_validity_seconds} // 30);
|
||||||
|
|
||||||
|
return ($uri, $timeout);
|
||||||
|
}
|
||||||
|
|
||||||
sub fetchInput {
|
sub fetchInput {
|
||||||
my ($self, $type, $name, $value) = @_;
|
my ($self, $type, $name, $value) = @_;
|
||||||
|
|
||||||
return undef if $type ne "path";
|
return undef if $type ne "path";
|
||||||
|
|
||||||
my $uri = $value;
|
my ($uri, $timeout) = _parseValue($self->{config}, $value);
|
||||||
|
|
||||||
my $timestamp = time;
|
my $timestamp = time;
|
||||||
my $sha256;
|
my $sha256;
|
||||||
my $storePath;
|
my $storePath;
|
||||||
|
|
||||||
my $timeout = $self->{config}->{path_input_cache_validity_seconds} // 30;
|
|
||||||
|
|
||||||
# Some simple caching: don't check a path more than once every N seconds.
|
# Some simple caching: don't check a path more than once every N seconds.
|
||||||
(my $cachedInput) = $self->{db}->resultset('CachedPathInputs')->search(
|
(my $cachedInput) = $self->{db}->resultset('CachedPathInputs')->search(
|
||||||
{srcpath => $uri, lastseen => {">", $timestamp - $timeout}},
|
{srcpath => $uri, lastseen => {">", $timestamp - $timeout}},
|
||||||
{rows => 1, order_by => "lastseen DESC"});
|
{rows => 1, order_by => "lastseen DESC"});
|
||||||
|
|
||||||
if (defined $cachedInput && isValidPath($cachedInput->storepath)) {
|
if (defined $cachedInput && $MACHINE_LOCAL_STORE->isValidPath($cachedInput->storepath)) {
|
||||||
$storePath = $cachedInput->storepath;
|
$storePath = $cachedInput->storepath;
|
||||||
$sha256 = $cachedInput->sha256hash;
|
$sha256 = $cachedInput->sha256hash;
|
||||||
$timestamp = $cachedInput->timestamp;
|
$timestamp = $cachedInput->timestamp;
|
||||||
@@ -38,15 +50,20 @@ sub fetchInput {
|
|||||||
|
|
||||||
print STDERR "copying input ", $name, " from $uri\n";
|
print STDERR "copying input ", $name, " from $uri\n";
|
||||||
if ( $uri =~ /^\// ) {
|
if ( $uri =~ /^\// ) {
|
||||||
$storePath = `nix-store --add "$uri"`
|
$storePath = addToStore($uri);
|
||||||
or die "cannot copy path $uri to the Nix store.\n";
|
|
||||||
} else {
|
} else {
|
||||||
$storePath = `PRINT_PATH=1 nix-prefetch-url "$uri" | tail -n 1`
|
# Run nix-prefetch-url with PRINT_PATH=1
|
||||||
or die "cannot fetch $uri to the Nix store.\n";
|
my ($stdout, $stderr);
|
||||||
|
local $ENV{PRINT_PATH} = 1;
|
||||||
|
run3(['nix-prefetch-url', $uri], \undef, \$stdout, \$stderr);
|
||||||
|
die "cannot fetch $uri to the Nix store: $stderr\n" if $? != 0;
|
||||||
|
# Get the last line (which is the store path)
|
||||||
|
my @output_lines = split /\n/, $stdout;
|
||||||
|
$storePath = $output_lines[-1] if @output_lines;
|
||||||
}
|
}
|
||||||
chomp $storePath;
|
chomp $storePath;
|
||||||
|
|
||||||
$sha256 = (queryPathInfo($storePath, 0))[1] or die;
|
$sha256 = ($MACHINE_LOCAL_STORE->queryPathInfo($storePath, 0))[1] or die;
|
||||||
|
|
||||||
($cachedInput) = $self->{db}->resultset('CachedPathInputs')->search(
|
($cachedInput) = $self->{db}->resultset('CachedPathInputs')->search(
|
||||||
{srcpath => $uri, sha256hash => $sha256});
|
{srcpath => $uri, sha256hash => $sha256});
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ sub isBuildEligibleForDynamicRunCommand {
|
|||||||
sub configSectionMatches {
|
sub configSectionMatches {
|
||||||
my ($name, $project, $jobset, $job) = @_;
|
my ($name, $project, $jobset, $job) = @_;
|
||||||
|
|
||||||
my @elems = split ':', $name;
|
my @elems = split /:/, $name;
|
||||||
|
|
||||||
die "invalid section name '$name'\n" if scalar(@elems) > 3;
|
die "invalid section name '$name'\n" if scalar(@elems) > 3;
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ use File::Temp;
|
|||||||
use File::Basename;
|
use File::Basename;
|
||||||
use Fcntl;
|
use Fcntl;
|
||||||
use IO::File;
|
use IO::File;
|
||||||
|
use IPC::Run qw(run);
|
||||||
|
use IPC::Run3;
|
||||||
use Net::Amazon::S3;
|
use Net::Amazon::S3;
|
||||||
use Net::Amazon::S3::Client;
|
use Net::Amazon::S3::Client;
|
||||||
use Digest::SHA;
|
use Digest::SHA;
|
||||||
@@ -14,6 +16,7 @@ use Nix::Config;
|
|||||||
use Nix::Store;
|
use Nix::Store;
|
||||||
use Hydra::Model::DB;
|
use Hydra::Model::DB;
|
||||||
use Hydra::Helper::CatalystUtils;
|
use Hydra::Helper::CatalystUtils;
|
||||||
|
use Hydra::Helper::Nix;
|
||||||
|
|
||||||
sub isEnabled {
|
sub isEnabled {
|
||||||
my ($self) = @_;
|
my ($self) = @_;
|
||||||
@@ -26,11 +29,11 @@ my %compressors = ();
|
|||||||
$compressors{"none"} = "";
|
$compressors{"none"} = "";
|
||||||
|
|
||||||
if (defined($Nix::Config::bzip2)) {
|
if (defined($Nix::Config::bzip2)) {
|
||||||
$compressors{"bzip2"} = "| $Nix::Config::bzip2",
|
$compressors{"bzip2"} = "$Nix::Config::bzip2",
|
||||||
}
|
}
|
||||||
|
|
||||||
if (defined($Nix::Config::xz)) {
|
if (defined($Nix::Config::xz)) {
|
||||||
$compressors{"xz"} = "| $Nix::Config::xz",
|
$compressors{"xz"} = "$Nix::Config::xz",
|
||||||
}
|
}
|
||||||
|
|
||||||
my $lockfile = Hydra::Model::DB::getHydraPath . "/.hydra-s3backup.lock";
|
my $lockfile = Hydra::Model::DB::getHydraPath . "/.hydra-s3backup.lock";
|
||||||
@@ -92,7 +95,7 @@ sub buildFinished {
|
|||||||
my $hash = substr basename($path), 0, 32;
|
my $hash = substr basename($path), 0, 32;
|
||||||
my ($deriver, $narHash, $time, $narSize, $refs) = queryPathInfo($path, 0);
|
my ($deriver, $narHash, $time, $narSize, $refs) = queryPathInfo($path, 0);
|
||||||
my $system;
|
my $system;
|
||||||
if (defined $deriver and isValidPath($deriver)) {
|
if (defined $deriver and $MACHINE_LOCAL_STORE->isValidPath($deriver)) {
|
||||||
$system = derivationFromPath($deriver)->{platform};
|
$system = derivationFromPath($deriver)->{platform};
|
||||||
}
|
}
|
||||||
foreach my $reference (@{$refs}) {
|
foreach my $reference (@{$refs}) {
|
||||||
@@ -110,7 +113,16 @@ sub buildFinished {
|
|||||||
}
|
}
|
||||||
next unless @incomplete_buckets;
|
next unless @incomplete_buckets;
|
||||||
my $compressor = $compressors{$compression_type};
|
my $compressor = $compressors{$compression_type};
|
||||||
system("$Nix::Config::binDir/nix-store --dump $path $compressor > $tempdir/nar") == 0 or die;
|
if ($compressor eq "") {
|
||||||
|
# No compression - use IPC::Run3 to redirect stdout to file
|
||||||
|
run3(["$Nix::Config::binDir/nix-store", "--dump", $path],
|
||||||
|
\undef, "$tempdir/nar", \undef) or die "nix-store --dump failed: $!";
|
||||||
|
} else {
|
||||||
|
# With compression - use IPC::Run to pipe nix-store output to compressor
|
||||||
|
my $dump_cmd = ["$Nix::Config::binDir/nix-store", "--dump", $path];
|
||||||
|
my $compress_cmd = [$compressor];
|
||||||
|
run($dump_cmd, '|', $compress_cmd, '>', "$tempdir/nar") or die "Pipeline failed: $?";
|
||||||
|
}
|
||||||
my $digest = Digest::SHA->new(256);
|
my $digest = Digest::SHA->new(256);
|
||||||
$digest->addfile("$tempdir/nar");
|
$digest->addfile("$tempdir/nar");
|
||||||
my $file_hash = $digest->hexdigest;
|
my $file_hash = $digest->hexdigest;
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use Digest::SHA qw(sha256_hex);
|
|||||||
use Hydra::Helper::Exec;
|
use Hydra::Helper::Exec;
|
||||||
use Hydra::Helper::Nix;
|
use Hydra::Helper::Nix;
|
||||||
use IPC::Run;
|
use IPC::Run;
|
||||||
use Nix::Store;
|
|
||||||
|
|
||||||
sub supportedInputTypes {
|
sub supportedInputTypes {
|
||||||
my ($self, $inputTypes) = @_;
|
my ($self, $inputTypes) = @_;
|
||||||
@@ -45,9 +44,9 @@ sub fetchInput {
|
|||||||
(my $cachedInput) = $self->{db}->resultset('CachedSubversionInputs')->search(
|
(my $cachedInput) = $self->{db}->resultset('CachedSubversionInputs')->search(
|
||||||
{uri => $uri, revision => $revision});
|
{uri => $uri, revision => $revision});
|
||||||
|
|
||||||
addTempRoot($cachedInput->storepath) if defined $cachedInput;
|
$MACHINE_LOCAL_STORE->addTempRoot($cachedInput->storepath) if defined $cachedInput;
|
||||||
|
|
||||||
if (defined $cachedInput && isValidPath($cachedInput->storepath)) {
|
if (defined $cachedInput && $MACHINE_LOCAL_STORE->isValidPath($cachedInput->storepath)) {
|
||||||
$storePath = $cachedInput->storepath;
|
$storePath = $cachedInput->storepath;
|
||||||
$sha256 = $cachedInput->sha256hash;
|
$sha256 = $cachedInput->sha256hash;
|
||||||
} else {
|
} else {
|
||||||
@@ -62,16 +61,16 @@ sub fetchInput {
|
|||||||
die "error checking out Subversion repo at `$uri':\n$stderr" if $res;
|
die "error checking out Subversion repo at `$uri':\n$stderr" if $res;
|
||||||
|
|
||||||
if ($type eq "svn-checkout") {
|
if ($type eq "svn-checkout") {
|
||||||
$storePath = addToStore($wcPath, 1, "sha256");
|
$storePath = $MACHINE_LOCAL_STORE->addToStore($wcPath, 1, "sha256");
|
||||||
} else {
|
} else {
|
||||||
# Hm, if the Nix Perl bindings supported filters in
|
# Hm, if the Nix Perl bindings supported filters in
|
||||||
# addToStore(), then we wouldn't need to make a copy here.
|
# addToStore(), then we wouldn't need to make a copy here.
|
||||||
my $tmpDir = File::Temp->newdir("hydra-svn-export.XXXXXX", CLEANUP => 1, TMPDIR => 1) or die;
|
my $tmpDir = File::Temp->newdir("hydra-svn-export.XXXXXX", CLEANUP => 1, TMPDIR => 1) or die;
|
||||||
(system "svn", "export", $wcPath, "$tmpDir/source", "--quiet") == 0 or die "svn export failed";
|
(system "svn", "export", $wcPath, "$tmpDir/source", "--quiet") == 0 or die "svn export failed";
|
||||||
$storePath = addToStore("$tmpDir/source", 1, "sha256");
|
$storePath = $MACHINE_LOCAL_STORE->addToStore("$tmpDir/source", 1, "sha256");
|
||||||
}
|
}
|
||||||
|
|
||||||
$sha256 = queryPathHash($storePath); $sha256 =~ s/sha256://;
|
$sha256 = $MACHINE_LOCAL_STORE->queryPathHash($storePath); $sha256 =~ s/sha256://;
|
||||||
|
|
||||||
$self->{db}->txn_do(sub {
|
$self->{db}->txn_do(sub {
|
||||||
$self->{db}->resultset('CachedSubversionInputs')->update_or_create(
|
$self->{db}->resultset('CachedSubversionInputs')->update_or_create(
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ __PACKAGE__->table("buildoutputs");
|
|||||||
=head2 path
|
=head2 path
|
||||||
|
|
||||||
data_type: 'text'
|
data_type: 'text'
|
||||||
is_nullable: 0
|
is_nullable: 1
|
||||||
|
|
||||||
=cut
|
=cut
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ __PACKAGE__->add_columns(
|
|||||||
"name",
|
"name",
|
||||||
{ data_type => "text", is_nullable => 0 },
|
{ data_type => "text", is_nullable => 0 },
|
||||||
"path",
|
"path",
|
||||||
{ data_type => "text", is_nullable => 0 },
|
{ data_type => "text", is_nullable => 1 },
|
||||||
);
|
);
|
||||||
|
|
||||||
=head1 PRIMARY KEY
|
=head1 PRIMARY KEY
|
||||||
@@ -94,8 +94,8 @@ __PACKAGE__->belongs_to(
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-08-26 12:02:36
|
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2022-06-30 12:02:32
|
||||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:gU+kZ6A0ISKpaXGRGve8mg
|
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Jsabm3YTcI7YvCuNdKP5Ng
|
||||||
|
|
||||||
my %hint = (
|
my %hint = (
|
||||||
columns => [
|
columns => [
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ __PACKAGE__->table("buildstepoutputs");
|
|||||||
=head2 path
|
=head2 path
|
||||||
|
|
||||||
data_type: 'text'
|
data_type: 'text'
|
||||||
is_nullable: 0
|
is_nullable: 1
|
||||||
|
|
||||||
=cut
|
=cut
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ __PACKAGE__->add_columns(
|
|||||||
"name",
|
"name",
|
||||||
{ data_type => "text", is_nullable => 0 },
|
{ data_type => "text", is_nullable => 0 },
|
||||||
"path",
|
"path",
|
||||||
{ data_type => "text", is_nullable => 0 },
|
{ data_type => "text", is_nullable => 1 },
|
||||||
);
|
);
|
||||||
|
|
||||||
=head1 PRIMARY KEY
|
=head1 PRIMARY KEY
|
||||||
@@ -119,8 +119,8 @@ __PACKAGE__->belongs_to(
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-08-26 12:02:36
|
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2022-06-30 12:02:32
|
||||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:gxp8rOjpRVen4YbIjomHTw
|
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Bad70CRTt7zb2GGuRoQ++Q
|
||||||
|
|
||||||
|
|
||||||
# You can replace this text with custom code or comments, and it will be preserved on regeneration
|
# You can replace this text with custom code or comments, and it will be preserved on regeneration
|
||||||
|
|||||||
@@ -563,7 +563,7 @@ makeQueries('', "");
|
|||||||
makeQueries('ForProject', "and jobset_id in (select id from jobsets j where j.project = ?)");
|
makeQueries('ForProject', "and jobset_id in (select id from jobsets j where j.project = ?)");
|
||||||
makeQueries('ForJobset', "and jobset_id = ?");
|
makeQueries('ForJobset', "and jobset_id = ?");
|
||||||
makeQueries('ForJob', "and jobset_id = ? and job = ?");
|
makeQueries('ForJob', "and jobset_id = ? and job = ?");
|
||||||
makeQueries('ForJobName', "and jobset_id = (select id from jobsets j where j.name = ?) and job = ?");
|
makeQueries('ForJobName', "and jobset_id = (select id from jobsets j where j.project = ? and j.name = ?) and job = ?");
|
||||||
|
|
||||||
sub as_json {
|
sub as_json {
|
||||||
my ($self) = @_;
|
my ($self) = @_;
|
||||||
|
|||||||
@@ -105,4 +105,6 @@ __PACKAGE__->add_column(
|
|||||||
"+id" => { retrieve_on_insert => 1 }
|
"+id" => { retrieve_on_insert => 1 }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
__PACKAGE__->mk_group_accessors('column' => 'has_error');
|
||||||
|
|
||||||
1;
|
1;
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ __PACKAGE__->table("jobsetevalinputs");
|
|||||||
data_type: 'text'
|
data_type: 'text'
|
||||||
is_nullable: 1
|
is_nullable: 1
|
||||||
|
|
||||||
|
=head2 shortRevLength
|
||||||
|
|
||||||
|
data_type: 'number'
|
||||||
|
is_nullable: 1
|
||||||
|
|
||||||
=head2 value
|
=head2 value
|
||||||
|
|
||||||
data_type: 'text'
|
data_type: 'text'
|
||||||
@@ -102,6 +107,8 @@ __PACKAGE__->add_columns(
|
|||||||
{ data_type => "text", is_nullable => 1 },
|
{ data_type => "text", is_nullable => 1 },
|
||||||
"revision",
|
"revision",
|
||||||
{ data_type => "text", is_nullable => 1 },
|
{ data_type => "text", is_nullable => 1 },
|
||||||
|
"shortRevLength",
|
||||||
|
{ data_type => "integer", is_nullable => 1 },
|
||||||
"value",
|
"value",
|
||||||
{ data_type => "text", is_nullable => 1 },
|
{ data_type => "text", is_nullable => 1 },
|
||||||
"dependency",
|
"dependency",
|
||||||
@@ -183,4 +190,28 @@ sub json_hint {
|
|||||||
return \%hint;
|
return \%hint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Revision to be rendered by the frontend
|
||||||
|
sub frontend_revision() {
|
||||||
|
my ($self) = @_;
|
||||||
|
my $type = $self->get_column('type');
|
||||||
|
if ($type eq 'svn' or $type eq 'svn-checkout' or $type eq 'bzr' or $type eq 'bzr-checkout') {
|
||||||
|
return 'r' . $self->get_column('revision');
|
||||||
|
} elsif ($type eq 'git') {
|
||||||
|
# Find the longest revision length of this URI
|
||||||
|
my $schema = $self->result_source->schema;
|
||||||
|
my $maxLength = $schema
|
||||||
|
->resultset('JobsetEvalInputs')
|
||||||
|
->search({ uri => $self->get_column('uri')})
|
||||||
|
->get_column('shortRevLength')
|
||||||
|
->max;
|
||||||
|
# Fall back to a fixed value if there was no value
|
||||||
|
return substr($self->get_column('revision'), 0, $maxLength || 12);
|
||||||
|
} elsif ($type eq 'bzr') {
|
||||||
|
return substr($self->get_column('revision'), 0, 12);
|
||||||
|
} else {
|
||||||
|
return $self->get_column('revision');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
1;
|
1;
|
||||||
|
|||||||
@@ -386,6 +386,8 @@ __PACKAGE__->add_column(
|
|||||||
"+id" => { retrieve_on_insert => 1 }
|
"+id" => { retrieve_on_insert => 1 }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
__PACKAGE__->mk_group_accessors('column' => 'has_error');
|
||||||
|
|
||||||
sub supportsDynamicRunCommand {
|
sub supportsDynamicRunCommand {
|
||||||
my ($self) = @_;
|
my ($self) = @_;
|
||||||
|
|
||||||
|
|||||||
30
src/lib/Hydra/Schema/ResultSet/EvaluationErrors.pm
Normal file
30
src/lib/Hydra/Schema/ResultSet/EvaluationErrors.pm
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package Hydra::Schema::ResultSet::EvaluationErrors;
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use utf8;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use parent 'DBIx::Class::ResultSet';
|
||||||
|
|
||||||
|
use Storable qw(dclone);
|
||||||
|
|
||||||
|
__PACKAGE__->load_components('Helper::ResultSet::RemoveColumns');
|
||||||
|
|
||||||
|
# Exclude expensive error message values unless explicitly requested, and
|
||||||
|
# replace them with a summary field describing their presence/absence.
|
||||||
|
sub search_rs {
|
||||||
|
my ( $class, $query, $attrs ) = @_;
|
||||||
|
|
||||||
|
if ($attrs) {
|
||||||
|
$attrs = dclone($attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
unless (exists $attrs->{'select'} || exists $attrs->{'columns'}) {
|
||||||
|
$attrs->{'+columns'}->{'has_error'} = "errormsg != ''";
|
||||||
|
}
|
||||||
|
unless (exists $attrs->{'+columns'}->{'errormsg'}) {
|
||||||
|
push @{ $attrs->{'remove_columns'} }, 'errormsg';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $class->next::method($query, $attrs);
|
||||||
|
}
|
||||||
30
src/lib/Hydra/Schema/ResultSet/Jobsets.pm
Normal file
30
src/lib/Hydra/Schema/ResultSet/Jobsets.pm
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package Hydra::Schema::ResultSet::Jobsets;
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use utf8;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use parent 'DBIx::Class::ResultSet';
|
||||||
|
|
||||||
|
use Storable qw(dclone);
|
||||||
|
|
||||||
|
__PACKAGE__->load_components('Helper::ResultSet::RemoveColumns');
|
||||||
|
|
||||||
|
# Exclude expensive error message values unless explicitly requested, and
|
||||||
|
# replace them with a summary field describing their presence/absence.
|
||||||
|
sub search_rs {
|
||||||
|
my ( $class, $query, $attrs ) = @_;
|
||||||
|
|
||||||
|
if ($attrs) {
|
||||||
|
$attrs = dclone($attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
unless (exists $attrs->{'select'} || exists $attrs->{'columns'}) {
|
||||||
|
$attrs->{'+columns'}->{'has_error'} = "errormsg != ''";
|
||||||
|
}
|
||||||
|
unless (exists $attrs->{'+columns'}->{'errormsg'}) {
|
||||||
|
push @{ $attrs->{'remove_columns'} }, 'errormsg';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $class->next::method($query, $attrs);
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ use MIME::Base64;
|
|||||||
use Nix::Manifest;
|
use Nix::Manifest;
|
||||||
use Nix::Store;
|
use Nix::Store;
|
||||||
use Nix::Utils;
|
use Nix::Utils;
|
||||||
|
use Hydra::Helper::Nix;
|
||||||
use base qw/Catalyst::View/;
|
use base qw/Catalyst::View/;
|
||||||
|
|
||||||
sub process {
|
sub process {
|
||||||
@@ -17,7 +18,7 @@ sub process {
|
|||||||
|
|
||||||
$c->response->content_type('text/x-nix-narinfo'); # !!! check MIME type
|
$c->response->content_type('text/x-nix-narinfo'); # !!! check MIME type
|
||||||
|
|
||||||
my ($deriver, $narHash, $time, $narSize, $refs) = queryPathInfo($storePath, 1);
|
my ($deriver, $narHash, $time, $narSize, $refs) = $MACHINE_LOCAL_STORE->queryPathInfo($storePath, 1);
|
||||||
|
|
||||||
my $info;
|
my $info;
|
||||||
$info .= "StorePath: $storePath\n";
|
$info .= "StorePath: $storePath\n";
|
||||||
@@ -28,8 +29,8 @@ sub process {
|
|||||||
$info .= "References: " . join(" ", map { basename $_ } @{$refs}) . "\n";
|
$info .= "References: " . join(" ", map { basename $_ } @{$refs}) . "\n";
|
||||||
if (defined $deriver) {
|
if (defined $deriver) {
|
||||||
$info .= "Deriver: " . basename $deriver . "\n";
|
$info .= "Deriver: " . basename $deriver . "\n";
|
||||||
if (isValidPath($deriver)) {
|
if ($MACHINE_LOCAL_STORE->isValidPath($deriver)) {
|
||||||
my $drv = derivationFromPath($deriver);
|
my $drv = $MACHINE_LOCAL_STORE->derivationFromPath($deriver);
|
||||||
$info .= "System: $drv->{platform}\n";
|
$info .= "System: $drv->{platform}\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ sub process {
|
|||||||
|
|
||||||
my $tail = int($c->stash->{tail} // "0");
|
my $tail = int($c->stash->{tail} // "0");
|
||||||
|
|
||||||
if ($logPath =~ /\.bz2$/) {
|
if ($logPath =~ /\.zst$/) {
|
||||||
|
my $doTail = $tail ? "| tail -n '$tail'" : "";
|
||||||
|
open($fh, "-|", "zstd -dc < '$logPath' $doTail") or die;
|
||||||
|
} elsif ($logPath =~ /\.bz2$/) {
|
||||||
my $doTail = $tail ? "| tail -n '$tail'" : "";
|
my $doTail = $tail ? "| tail -n '$tail'" : "";
|
||||||
open($fh, "-|", "bzip2 -dc < '$logPath' $doTail") or die;
|
open($fh, "-|", "bzip2 -dc < '$logPath' $doTail") or die;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use base 'Catalyst::View::TT';
|
|||||||
use Template::Plugin::HTML;
|
use Template::Plugin::HTML;
|
||||||
use Hydra::Helper::Nix;
|
use Hydra::Helper::Nix;
|
||||||
use Time::Seconds;
|
use Time::Seconds;
|
||||||
|
use Digest::SHA qw(sha1_hex);
|
||||||
|
|
||||||
__PACKAGE__->config(
|
__PACKAGE__->config(
|
||||||
TEMPLATE_EXTENSION => '.tt',
|
TEMPLATE_EXTENSION => '.tt',
|
||||||
@@ -25,8 +26,14 @@ __PACKAGE__->config(
|
|||||||
makeNameTextForJobset
|
makeNameTextForJobset
|
||||||
relativeDuration
|
relativeDuration
|
||||||
stripSSHUser
|
stripSSHUser
|
||||||
|
metricDivId
|
||||||
/]);
|
/]);
|
||||||
|
|
||||||
|
sub metricDivId {
|
||||||
|
my ($self, $c, $text) = @_;
|
||||||
|
return "metric-" . sha1_hex($text);
|
||||||
|
}
|
||||||
|
|
||||||
sub buildLogExists {
|
sub buildLogExists {
|
||||||
my ($self, $c, $build) = @_;
|
my ($self, $c, $build) = @_;
|
||||||
return 1 if defined $c->config->{log_prefix};
|
return 1 if defined $c->config->{log_prefix};
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
PERL_MODULES = \
|
|
||||||
$(wildcard *.pm) \
|
|
||||||
$(wildcard Hydra/*.pm) \
|
|
||||||
$(wildcard Hydra/Helper/*.pm) \
|
|
||||||
$(wildcard Hydra/Model/*.pm) \
|
|
||||||
$(wildcard Hydra/View/*.pm) \
|
|
||||||
$(wildcard Hydra/Schema/*.pm) \
|
|
||||||
$(wildcard Hydra/Schema/Result/*.pm) \
|
|
||||||
$(wildcard Hydra/Schema/ResultSet/*.pm) \
|
|
||||||
$(wildcard Hydra/Controller/*.pm) \
|
|
||||||
$(wildcard Hydra/Base/*.pm) \
|
|
||||||
$(wildcard Hydra/Base/Controller/*.pm) \
|
|
||||||
$(wildcard Hydra/Script/*.pm) \
|
|
||||||
$(wildcard Hydra/Component/*.pm) \
|
|
||||||
$(wildcard Hydra/Event/*.pm) \
|
|
||||||
$(wildcard Hydra/Plugin/*.pm)
|
|
||||||
|
|
||||||
EXTRA_DIST = \
|
|
||||||
$(PERL_MODULES)
|
|
||||||
|
|
||||||
hydradir = $(libexecdir)/hydra/lib
|
|
||||||
nobase_hydra_DATA = $(PERL_MODULES)
|
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package Perl::Critic::Policy::Hydra::ProhibitShellInvokingSystemCalls;
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
use constant;
|
||||||
|
|
||||||
|
use Perl::Critic::Utils qw{ :severities :classification :ppi };
|
||||||
|
use base 'Perl::Critic::Policy';
|
||||||
|
|
||||||
|
our $VERSION = '1.000';
|
||||||
|
|
||||||
|
use constant DESC => q{Shell-invoking system calls are prohibited};
|
||||||
|
use constant EXPL => q{Use list form system() or IPC::Run3 for better security. String form invokes shell and is vulnerable to injection};
|
||||||
|
|
||||||
|
sub supported_parameters { return () }
|
||||||
|
sub default_severity { return $SEVERITY_HIGHEST }
|
||||||
|
sub default_themes { return qw( hydra security ) }
|
||||||
|
sub applies_to { return 'PPI::Token::Word' }
|
||||||
|
|
||||||
|
sub violates {
|
||||||
|
my ( $self, $elem, undef ) = @_;
|
||||||
|
|
||||||
|
# Only check system() and exec() calls
|
||||||
|
return () unless $elem->content() =~ /^(system|exec)$/;
|
||||||
|
return () unless is_function_call($elem);
|
||||||
|
|
||||||
|
# Skip method calls (->system or ->exec)
|
||||||
|
my $prev = $elem->sprevious_sibling();
|
||||||
|
return () if $prev && $prev->isa('PPI::Token::Operator') && $prev->content() eq '->';
|
||||||
|
|
||||||
|
# Get first argument after function name, skipping whitespace
|
||||||
|
my $args = $elem->snext_sibling();
|
||||||
|
return () unless $args;
|
||||||
|
$args = $args->snext_sibling() while $args && $args->isa('PPI::Token::Whitespace');
|
||||||
|
|
||||||
|
# For parenthesized calls, look inside
|
||||||
|
my $search_elem = $args;
|
||||||
|
if ($args && $args->isa('PPI::Structure::List')) {
|
||||||
|
$search_elem = $args->schild(0);
|
||||||
|
return () unless $search_elem;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if it's list form (has comma)
|
||||||
|
my $current = $search_elem;
|
||||||
|
if ($current && $current->isa('PPI::Statement')) {
|
||||||
|
# Look through statement children
|
||||||
|
for my $child ($current->schildren()) {
|
||||||
|
return () if $child->isa('PPI::Token::Operator') && $child->content() eq ',';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
# Look through siblings for non-parenthesized calls
|
||||||
|
while ($current) {
|
||||||
|
return () if $current->isa('PPI::Token::Operator') && $current->content() eq ',';
|
||||||
|
last if $current->isa('PPI::Token::Structure') && $current->content() eq ';';
|
||||||
|
$current = $current->snext_sibling();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if first arg is array variable
|
||||||
|
my $first = $search_elem->isa('PPI::Statement') ?
|
||||||
|
$search_elem->schild(0) : $search_elem;
|
||||||
|
return () if $first && $first->isa('PPI::Token::Symbol') && $first->content() =~ /^[@]/;
|
||||||
|
|
||||||
|
# Check if it's a safe single-word command
|
||||||
|
if ($first && $first->isa('PPI::Token::Quote')) {
|
||||||
|
my $content = $first->string();
|
||||||
|
return () if $content =~ /^[a-zA-Z0-9_\-\.\/]+$/;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $self->violation( DESC, EXPL, $elem );
|
||||||
|
}
|
||||||
|
|
||||||
|
1;
|
||||||
|
|
||||||
|
__END__
|
||||||
|
|
||||||
|
=pod
|
||||||
|
|
||||||
|
=head1 NAME
|
||||||
|
|
||||||
|
Perl::Critic::Policy::Hydra::ProhibitShellInvokingSystemCalls - Prohibit shell-invoking system() and exec() calls
|
||||||
|
|
||||||
|
=head1 DESCRIPTION
|
||||||
|
|
||||||
|
This policy prohibits the use of C<system()> and C<exec()> functions when called with a single string argument,
|
||||||
|
which invokes the shell and is vulnerable to injection attacks.
|
||||||
|
|
||||||
|
The list form (e.g., C<system('ls', '-la')>) is allowed as it executes directly without shell interpretation.
|
||||||
|
For better error handling and output capture, consider using C<IPC::Run3>.
|
||||||
|
|
||||||
|
=head1 CONFIGURATION
|
||||||
|
|
||||||
|
This Policy is not configurable except for the standard options.
|
||||||
|
|
||||||
|
=head1 AUTHOR
|
||||||
|
|
||||||
|
Hydra Development Team
|
||||||
|
|
||||||
|
=head1 COPYRIGHT
|
||||||
|
|
||||||
|
Copyright (c) 2025 Hydra Development Team. All rights reserved.
|
||||||
|
|
||||||
|
=cut
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
#include <pqxx/pqxx>
|
#include <pqxx/pqxx>
|
||||||
|
|
||||||
#include "util.hh"
|
#include <nix/util/environment-variables.hh>
|
||||||
|
#include <nix/util/util.hh>
|
||||||
|
|
||||||
|
|
||||||
struct Connection : pqxx::connection
|
struct Connection : pqxx::connection
|
||||||
@@ -26,19 +27,20 @@ struct Connection : pqxx::connection
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
class receiver : public pqxx::notification_receiver
|
class receiver
|
||||||
{
|
{
|
||||||
std::optional<std::string> status;
|
std::optional<std::string> status;
|
||||||
|
pqxx::connection & conn;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
receiver(pqxx::connection_base & c, const std::string & channel)
|
receiver(pqxx::connection_base & c, const std::string & channel)
|
||||||
: pqxx::notification_receiver(c, channel) { }
|
: conn(static_cast<pqxx::connection &>(c))
|
||||||
|
|
||||||
void operator() (const std::string & payload, int pid) override
|
|
||||||
{
|
{
|
||||||
status = payload;
|
conn.listen(channel, [this](pqxx::notification n) {
|
||||||
};
|
status = std::string(n.payload);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<std::string> get() {
|
std::optional<std::string> get() {
|
||||||
auto s = status;
|
auto s = status;
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
|
|
||||||
#include "util.hh"
|
#include <nix/util/file-system.hh>
|
||||||
|
#include <nix/util/util.hh>
|
||||||
|
|
||||||
struct HydraConfig
|
struct HydraConfig
|
||||||
{
|
{
|
||||||
|
|||||||
5
src/libhydra/meson.build
Normal file
5
src/libhydra/meson.build
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
libhydra_inc = include_directories('.')
|
||||||
|
|
||||||
|
libhydra_dep = declare_dependency(
|
||||||
|
include_directories: [libhydra_inc],
|
||||||
|
)
|
||||||
77
src/meson.build
Normal file
77
src/meson.build
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Native code
|
||||||
|
subdir('libhydra')
|
||||||
|
subdir('hydra-evaluator')
|
||||||
|
subdir('hydra-queue-runner')
|
||||||
|
|
||||||
|
hydra_libexecdir = get_option('libexecdir') / 'hydra'
|
||||||
|
|
||||||
|
# Data and interpreted
|
||||||
|
foreach dir : ['lib', 'root']
|
||||||
|
install_subdir(dir,
|
||||||
|
install_dir: hydra_libexecdir,
|
||||||
|
)
|
||||||
|
endforeach
|
||||||
|
subdir('sql')
|
||||||
|
subdir('ttf')
|
||||||
|
|
||||||
|
# Static files for website
|
||||||
|
|
||||||
|
hydra_libexecdir_static = hydra_libexecdir / 'root' / 'static'
|
||||||
|
|
||||||
|
## Bootstrap
|
||||||
|
|
||||||
|
bootstrap_name = 'bootstrap-4.3.1-dist'
|
||||||
|
bootstrap = custom_target(
|
||||||
|
'extract-bootstrap',
|
||||||
|
input: 'root' / (bootstrap_name + '.zip'),
|
||||||
|
output: bootstrap_name,
|
||||||
|
command: ['unzip', '-u', '-d', '@OUTDIR@', '@INPUT@'],
|
||||||
|
)
|
||||||
|
custom_target(
|
||||||
|
'name-bootstrap',
|
||||||
|
input: bootstrap,
|
||||||
|
output: 'bootstrap',
|
||||||
|
command: ['cp', '-r', '@INPUT@' , '@OUTPUT@'],
|
||||||
|
install: true,
|
||||||
|
install_dir: hydra_libexecdir_static,
|
||||||
|
)
|
||||||
|
|
||||||
|
## Flot
|
||||||
|
|
||||||
|
custom_target(
|
||||||
|
'extract-flot',
|
||||||
|
input: 'root' / 'flot-0.8.3.zip',
|
||||||
|
output: 'flot',
|
||||||
|
command: ['unzip', '-u', '-d', '@OUTDIR@', '@INPUT@'],
|
||||||
|
install: true,
|
||||||
|
install_dir: hydra_libexecdir_static / 'js',
|
||||||
|
)
|
||||||
|
|
||||||
|
## Fontawesome
|
||||||
|
|
||||||
|
fontawesome_name = 'fontawesome-free-5.10.2-web'
|
||||||
|
fontawesome = custom_target(
|
||||||
|
'extract-fontawesome',
|
||||||
|
input: 'root' / (fontawesome_name + '.zip'),
|
||||||
|
output: fontawesome_name,
|
||||||
|
command: ['unzip', '-u', '-d', '@OUTDIR@', '@INPUT@'],
|
||||||
|
)
|
||||||
|
custom_target(
|
||||||
|
'name-fontawesome',
|
||||||
|
input: fontawesome,
|
||||||
|
output: 'fontawesome',
|
||||||
|
command: ['cp', '-r', '@INPUT@' , '@OUTPUT@'],
|
||||||
|
install: true,
|
||||||
|
install_dir: hydra_libexecdir_static,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Scripts
|
||||||
|
|
||||||
|
install_subdir('script',
|
||||||
|
install_dir: get_option('bindir'),
|
||||||
|
exclude_files: [
|
||||||
|
'hydra-dev-server',
|
||||||
|
],
|
||||||
|
install_mode: 'rwxr-xr-x',
|
||||||
|
strip_directory: true,
|
||||||
|
)
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
TEMPLATES = $(wildcard *.tt)
|
|
||||||
STATIC = \
|
|
||||||
$(wildcard static/images/*) \
|
|
||||||
$(wildcard static/css/*) \
|
|
||||||
static/js/bootbox.min.js \
|
|
||||||
static/js/popper.min.js \
|
|
||||||
static/js/common.js \
|
|
||||||
static/js/jquery/jquery-3.4.1.min.js \
|
|
||||||
static/js/jquery/jquery-ui-1.10.4.min.js
|
|
||||||
|
|
||||||
FLOT = flot-0.8.3.zip
|
|
||||||
BOOTSTRAP = bootstrap-4.3.1-dist.zip
|
|
||||||
FONTAWESOME = fontawesome-free-5.10.2-web.zip
|
|
||||||
|
|
||||||
ZIPS = $(FLOT) $(BOOTSTRAP) $(FONTAWESOME)
|
|
||||||
|
|
||||||
EXTRA_DIST = $(TEMPLATES) $(STATIC) $(ZIPS)
|
|
||||||
|
|
||||||
hydradir = $(libexecdir)/hydra/root
|
|
||||||
nobase_hydra_DATA = $(EXTRA_DIST)
|
|
||||||
|
|
||||||
all:
|
|
||||||
mkdir -p $(srcdir)/static/js
|
|
||||||
unzip -u -d $(srcdir)/static $(BOOTSTRAP)
|
|
||||||
rm -rf $(srcdir)/static/bootstrap
|
|
||||||
mv $(srcdir)/static/$(basename $(BOOTSTRAP)) $(srcdir)/static/bootstrap
|
|
||||||
unzip -u -d $(srcdir)/static/js $(FLOT)
|
|
||||||
unzip -u -d $(srcdir)/static $(FONTAWESOME)
|
|
||||||
rm -rf $(srcdir)/static/fontawesome
|
|
||||||
mv $(srcdir)/static/$(basename $(FONTAWESOME)) $(srcdir)/static/fontawesome
|
|
||||||
|
|
||||||
install-data-local: $(ZIPS)
|
|
||||||
mkdir -p $(hydradir)/static/js
|
|
||||||
cp -prvd $(srcdir)/static/js/* $(hydradir)/static/js
|
|
||||||
mkdir -p $(hydradir)/static/bootstrap
|
|
||||||
cp -prvd $(srcdir)/static/bootstrap/* $(hydradir)/static/bootstrap
|
|
||||||
mkdir -p $(hydradir)/static/fontawesome/{css,webfonts}
|
|
||||||
cp -prvd $(srcdir)/static/fontawesome/css/* $(hydradir)/static/fontawesome/css
|
|
||||||
cp -prvd $(srcdir)/static/fontawesome/webfonts/* $(hydradir)/static/fontawesome/webfonts
|
|
||||||
@@ -11,7 +11,7 @@ titleHTML="Latest builds" _
|
|||||||
"") %]
|
"") %]
|
||||||
[% PROCESS common.tt %]
|
[% PROCESS common.tt %]
|
||||||
|
|
||||||
<p>Showing builds [% (page - 1) * resultsPerPage + 1 %] - [% (page - 1) * resultsPerPage + builds.size %] out of [% total %] in order of descending finish time.</p>
|
<p>Showing builds [% (page - 1) * resultsPerPage + 1 %] - [% (page - 1) * resultsPerPage + builds.size %] out of [% HTML.escape(total) %] in order of descending finish time.</p>
|
||||||
|
|
||||||
[% INCLUDE renderBuildList hideProjectName=project hideJobsetName=jobset hideJobName=job %]
|
[% INCLUDE renderBuildList hideProjectName=project hideJobsetName=jobset hideJobName=job %]
|
||||||
[% INCLUDE renderPager %]
|
[% INCLUDE renderPager %]
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<div id="hydra-signin" class="modal hide fade" tabindex="-1" role="dialog" aria-hidden="true">
|
<div id="hydra-signin" class="modal hide fade" tabindex="-1" role="dialog" aria-hidden="true">
|
||||||
<div class="modal-dialog" role="document">
|
<div class="modal-dialog" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<form>
|
<form id="signin-form">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="username" class="col-form-label">User name</label>
|
<label for="username" class="col-form-label">User name</label>
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button id="do-signin" type="button" class="btn btn-primary">Sign in</button>
|
<button type="submit" class="btn btn-primary">Sign in</button>
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -57,10 +57,11 @@
|
|||||||
|
|
||||||
function finishSignOut() { }
|
function finishSignOut() { }
|
||||||
|
|
||||||
$("#do-signin").click(function() {
|
$("#signin-form").submit(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
requestJSON({
|
requestJSON({
|
||||||
url: "[% c.uri_for('/login') %]",
|
url: "[% c.uri_for('/login') %]",
|
||||||
data: $(this).parents("form").serialize(),
|
data: $(this).serialize(),
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ END;
|
|||||||
seen.${step.drvpath} = 1;
|
seen.${step.drvpath} = 1;
|
||||||
log = c.uri_for('/build' build.id 'nixlog' step.stepnr); %]
|
log = c.uri_for('/build' build.id 'nixlog' step.stepnr); %]
|
||||||
<tr>
|
<tr>
|
||||||
<td>[% step.stepnr %]</td>
|
<td>[% HTML.escape(step.stepnr) %]</td>
|
||||||
<td>
|
<td>
|
||||||
[% IF step.type == 0 %]
|
[% IF step.type == 0 %]
|
||||||
Build of <tt>[% INCLUDE renderOutputs outputs=step.buildstepoutputs %]</tt>
|
Build of <tt>[% INCLUDE renderOutputs outputs=step.buildstepoutputs %]</tt>
|
||||||
@@ -61,21 +61,7 @@ END;
|
|||||||
<td>[% IF step.busy != 0 || ((step.machine || step.starttime) && (step.status == 0 || step.status == 1 || step.status == 3 || step.status == 4 || step.status == 7)); INCLUDE renderMachineName machine=step.machine; ELSE; "<em>n/a</em>"; END %]</td>
|
<td>[% IF step.busy != 0 || ((step.machine || step.starttime) && (step.status == 0 || step.status == 1 || step.status == 3 || step.status == 4 || step.status == 7)); INCLUDE renderMachineName machine=step.machine; ELSE; "<em>n/a</em>"; END %]</td>
|
||||||
<td class="step-status">
|
<td class="step-status">
|
||||||
[% IF step.busy != 0 %]
|
[% IF step.busy != 0 %]
|
||||||
[% IF step.busy == 1 %]
|
[% INCLUDE renderBusyStatus %]
|
||||||
<strong>Preparing</strong>
|
|
||||||
[% ELSIF step.busy == 10 %]
|
|
||||||
<strong>Connecting</strong>
|
|
||||||
[% ELSIF step.busy == 20 %]
|
|
||||||
<strong>Sending inputs</strong>
|
|
||||||
[% ELSIF step.busy == 30 %]
|
|
||||||
<strong>Building</strong>
|
|
||||||
[% ELSIF step.busy == 40 %]
|
|
||||||
<strong>Receiving outputs</strong>
|
|
||||||
[% ELSIF step.busy == 50 %]
|
|
||||||
<strong>Post-processing</strong>
|
|
||||||
[% ELSE %]
|
|
||||||
<strong>Unknown state</strong>
|
|
||||||
[% END %]
|
|
||||||
[% ELSIF step.status == 0 %]
|
[% ELSIF step.status == 0 %]
|
||||||
[% IF step.isnondeterministic %]
|
[% IF step.isnondeterministic %]
|
||||||
<span class="warn">Succeeded with non-determistic result</span>
|
<span class="warn">Succeeded with non-determistic result</span>
|
||||||
@@ -100,7 +86,7 @@ END;
|
|||||||
[% ELSIF step.status == 11 %]
|
[% ELSIF step.status == 11 %]
|
||||||
<span class="error">Output limit exceeded</span>
|
<span class="error">Output limit exceeded</span>
|
||||||
[% ELSIF step.status == 12 %]
|
[% ELSIF step.status == 12 %]
|
||||||
<span class="error">Non-determinism detected</span> [% IF step.timesbuilt %] after [% step.timesbuilt %] times[% END %]
|
<span class="error">Non-determinism detected</span> [% IF step.timesbuilt %] after [% HTML.escape(step.timesbuilt) %] times[% END %]
|
||||||
[% ELSIF step.errormsg %]
|
[% ELSIF step.errormsg %]
|
||||||
<span class="error">Failed</span>: <em>[% HTML.escape(step.errormsg) %]</em>
|
<span class="error">Failed</span>: <em>[% HTML.escape(step.errormsg) %]</em>
|
||||||
[% ELSE %]
|
[% ELSE %]
|
||||||
@@ -126,16 +112,16 @@ END;
|
|||||||
[% IF c.user_exists %]
|
[% IF c.user_exists %]
|
||||||
[% IF available %]
|
[% IF available %]
|
||||||
[% IF build.keep %]
|
[% IF build.keep %]
|
||||||
<a class="dropdown-item" href="[% c.uri_for('/build' build.id 'keep' 0) %]">Unkeep</a>
|
<a class="dropdown-item" [% HTML.attributes(href => c.uri_for('/build' build.id 'keep' 0)) %]>Unkeep</a>
|
||||||
[% ELSE %]
|
[% ELSE %]
|
||||||
<a class="dropdown-item" href="[% c.uri_for('/build' build.id 'keep' 1) %]">Keep</a>
|
<a class="dropdown-item" [% HTML.attributes(href => c.uri_for('/build' build.id 'keep' 1)) %]>Keep</a>
|
||||||
[% END %]
|
[% END %]
|
||||||
[% END %]
|
[% END %]
|
||||||
[% IF build.finished %]
|
[% IF build.finished %]
|
||||||
<a class="dropdown-item" href="[% c.uri_for('/build' build.id 'restart') %]">Restart</a>
|
<a class="dropdown-item" [% HTML.attributes(href => c.uri_for('/build' build.id 'restart')) %]>Restart</a>
|
||||||
[% ELSE %]
|
[% ELSE %]
|
||||||
<a class="dropdown-item" href="[% c.uri_for('/build' build.id 'cancel') %]">Cancel</a>
|
<a class="dropdown-item" [% HTML.attributes(href => c.uri_for('/build' build.id 'cancel')) %]>Cancel</a>
|
||||||
<a class="dropdown-item" href="[% c.uri_for('/build' build.id 'bump') %]">Bump up</a>
|
<a class="dropdown-item" [% HTML.attributes(href => c.uri_for('/build' build.id 'bump')) %]>Bump up</a>
|
||||||
[% END %]
|
[% END %]
|
||||||
[% END %]
|
[% END %]
|
||||||
</div>
|
</div>
|
||||||
@@ -146,7 +132,7 @@ END;
|
|||||||
<li class="nav-item"><a class="nav-link" href="#tabs-details" data-toggle="tab">Details</a></li>
|
<li class="nav-item"><a class="nav-link" href="#tabs-details" data-toggle="tab">Details</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="#tabs-buildinputs" data-toggle="tab">Inputs</a></li>
|
<li class="nav-item"><a class="nav-link" href="#tabs-buildinputs" data-toggle="tab">Inputs</a></li>
|
||||||
[% IF steps.size() > 0 %]<li class="nav-item"><a class="nav-link" href="#tabs-buildsteps" data-toggle="tab">Build Steps</a></li>[% END %]
|
[% IF steps.size() > 0 %]<li class="nav-item"><a class="nav-link" href="#tabs-buildsteps" data-toggle="tab">Build Steps</a></li>[% END %]
|
||||||
[% IF build.dependents %]<li class="nav-item"><a class="nav-link" href="#tabs-usedby" data-toggle="tab">Used By</a></li>[% END%]
|
[% IF build.dependents %]<li class="nav-item"><a class="nav-link" href="#tabs-usedby" data-toggle="tab">Used By</a></li>[% END %]
|
||||||
[% IF drvAvailable %]<li class="nav-item"><a class="nav-link" href="#tabs-build-deps" data-toggle="tab">Build Dependencies</a></li>[% END %]
|
[% IF drvAvailable %]<li class="nav-item"><a class="nav-link" href="#tabs-build-deps" data-toggle="tab">Build Dependencies</a></li>[% END %]
|
||||||
[% IF localStore && available %]<li class="nav-item"><a class="nav-link" href="#tabs-runtime-deps" data-toggle="tab">Runtime Dependencies</a></li>[% END %]
|
[% IF localStore && available %]<li class="nav-item"><a class="nav-link" href="#tabs-runtime-deps" data-toggle="tab">Runtime Dependencies</a></li>[% END %]
|
||||||
[% IF runcommandlogProblem || runcommandlogs.size() > 0 %]<li class="nav-item"><a class="nav-link" href="#tabs-runcommandlogs" data-toggle="tab">RunCommand Logs[% IF runcommandlogProblem %] <span class="badge badge-warning">Disabled</span>[% END %]</a></li>[% END %]
|
[% IF runcommandlogProblem || runcommandlogs.size() > 0 %]<li class="nav-item"><a class="nav-link" href="#tabs-runcommandlogs" data-toggle="tab">RunCommand Logs[% IF runcommandlogProblem %] <span class="badge badge-warning">Disabled</span>[% END %]</a></li>[% END %]
|
||||||
@@ -165,7 +151,7 @@ END;
|
|||||||
<table class="info-table">
|
<table class="info-table">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Build ID:</th>
|
<th>Build ID:</th>
|
||||||
<td>[% build.id %]</td>
|
<td>[% HTML.escape(build.id) %]</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Status:</th>
|
<th>Status:</th>
|
||||||
@@ -182,9 +168,9 @@ END;
|
|||||||
END;
|
END;
|
||||||
%];
|
%];
|
||||||
[%+ IF nrFinished == nrConstituents && nrFailedConstituents == 0 %]
|
[%+ IF nrFinished == nrConstituents && nrFailedConstituents == 0 %]
|
||||||
all [% nrConstituents %] constituent builds succeeded
|
all [% HTML.escape(nrConstituents) %] constituent builds succeeded
|
||||||
[% ELSE %]
|
[% ELSE %]
|
||||||
[% nrFailedConstituents %] out of [% nrConstituents %] constituent builds failed
|
[% HTML.escape(nrFailedConstituents) %] out of [% HTML.escape(nrConstituents) %] constituent builds failed
|
||||||
[% IF nrFinished < nrConstituents %]
|
[% IF nrFinished < nrConstituents %]
|
||||||
([% nrConstituents - nrFinished %] still pending)
|
([% nrConstituents - nrFinished %] still pending)
|
||||||
[% END %]
|
[% END %]
|
||||||
@@ -194,25 +180,25 @@ END;
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>System:</th>
|
<th>System:</th>
|
||||||
<td><tt>[% build.system %]</tt></td>
|
<td><tt>[% build.system | html %]</tt></td>
|
||||||
</tr>
|
</tr>
|
||||||
[% IF build.releasename %]
|
[% IF build.releasename %]
|
||||||
<tr>
|
<tr>
|
||||||
<th>Release name:</th>
|
<th>Release name:</th>
|
||||||
<td><tt>[% HTML.escape(build.releasename) %]</tt></td>
|
<td><tt>[% build.releasename | html %]</tt></td>
|
||||||
</tr>
|
</tr>
|
||||||
[% ELSE %]
|
[% ELSE %]
|
||||||
<tr>
|
<tr>
|
||||||
<th>Nix name:</th>
|
<th>Nix name:</th>
|
||||||
<td><tt>[% build.nixname %]</tt></td>
|
<td><tt>[% build.nixname | html %]</tt></td>
|
||||||
</tr>
|
</tr>
|
||||||
[% END %]
|
[% END %]
|
||||||
[% IF eval %]
|
[% IF eval %]
|
||||||
<tr>
|
<tr>
|
||||||
<th>Part of:</th>
|
<th>Part of:</th>
|
||||||
<td>
|
<td>
|
||||||
<a href="[% c.uri_for(c.controller('JobsetEval').action_for('view'), [eval.id]) %]">evaluation [% eval.id %]</a>
|
<a [% HTML.attributes(href => c.uri_for(c.controller('JobsetEval').action_for('view'), [eval.id])) %]>evaluation [% HTML.escape(eval.id) %]</a>
|
||||||
[% IF nrEvals > 1 +%] (and <a href="[% c.uri_for('/build' build.id 'evals') %]">[% nrEvals - 1 %] others</a>)[% END %]
|
[% IF nrEvals > 1 +%] (and <a [% HTML.attributes(href => c.uri_for('/build' build.id 'evals')) %]>[% nrEvals - 1 %] others</a>)[% END %]
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
[% END %]
|
[% END %]
|
||||||
@@ -240,9 +226,9 @@ END;
|
|||||||
<th>Logfile:</th>
|
<th>Logfile:</th>
|
||||||
<td>
|
<td>
|
||||||
[% actualLog = cachedBuildStep ? c.uri_for('/build' cachedBuild.id 'nixlog' cachedBuildStep.stepnr) : c.uri_for('/build' build.id 'log') %]
|
[% actualLog = cachedBuildStep ? c.uri_for('/build' cachedBuild.id 'nixlog' cachedBuildStep.stepnr) : c.uri_for('/build' build.id 'log') %]
|
||||||
<a class="btn btn-secondary btn-sm" href="[%actualLog%]">pretty</a>
|
<a class="btn btn-secondary btn-sm" [% HTML.attributes(href => actualLog) %]>pretty</a>
|
||||||
<a class="btn btn-secondary btn-sm" href="[%actualLog%]/raw">raw</a>
|
<a class="btn btn-secondary btn-sm" [% HTML.attributes(href => actualLog _ "/raw") %]>raw</a>
|
||||||
<a class="btn btn-secondary btn-sm" href="[%actualLog%]/tail">tail</a>
|
<a class="btn btn-secondary btn-sm" [% HTML.attributes(href => actualLog _ "/tail") %]>tail</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
[% END %]
|
[% END %]
|
||||||
@@ -350,12 +336,12 @@ END;
|
|||||||
[% IF eval.nixexprinput %]
|
[% IF eval.nixexprinput %]
|
||||||
<tr>
|
<tr>
|
||||||
<th>Nix expression:</th>
|
<th>Nix expression:</th>
|
||||||
<td>file <tt>[% HTML.escape(eval.nixexprpath) %]</tt> in input <tt>[% HTML.escape(eval.nixexprinput) %]</tt></td>
|
<td>file <tt>[% eval.nixexprpath | html %]</tt> in input <tt>[% eval.nixexprinput | html %]</tt></td>
|
||||||
</tr>
|
</tr>
|
||||||
[% END %]
|
[% END %]
|
||||||
<tr>
|
<tr>
|
||||||
<th>Nix name:</th>
|
<th>Nix name:</th>
|
||||||
<td><tt>[% build.nixname %]</tt></td>
|
<td><tt>[% build.nixname | html %]</tt></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Short description:</th>
|
<th>Short description:</th>
|
||||||
@@ -375,11 +361,11 @@ END;
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>System:</th>
|
<th>System:</th>
|
||||||
<td><tt>[% build.system %]</tt></td>
|
<td><tt>[% build.system | html %]</tt></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Derivation store path:</th>
|
<th>Derivation store path:</th>
|
||||||
<td><tt>[% build.drvpath %]</tt></td>
|
<td><tt>[% build.drvpath | html %]</tt></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Output store paths:</th>
|
<th>Output store paths:</th>
|
||||||
@@ -390,14 +376,14 @@ END;
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Closure size:</th>
|
<th>Closure size:</th>
|
||||||
<td>[% mibs(build.closuresize / (1024 * 1024)) %] MiB
|
<td>[% mibs(build.closuresize / (1024 * 1024)) %] MiB
|
||||||
(<a href="[%chartsURL%]">history</a>)</td>
|
(<a [% HTML.attributes(href => chartsURL) %]>history</a>)</td>
|
||||||
</tr>
|
</tr>
|
||||||
[% END %]
|
[% END %]
|
||||||
[% IF build.finished && build.closuresize %]
|
[% IF build.finished && build.closuresize %]
|
||||||
<tr>
|
<tr>
|
||||||
<th>Output size:</th>
|
<th>Output size:</th>
|
||||||
<td>[% mibs(build.size / (1024 * 1024)) %] MiB
|
<td>[% mibs(build.size / (1024 * 1024)) %] MiB
|
||||||
(<a href="[%chartsURL%]">history</a>)</td>
|
(<a [% HTML.attributes(href => chartsURL) %]>history</a>)</td>
|
||||||
</tr>
|
</tr>
|
||||||
[% END %]
|
[% END %]
|
||||||
[% IF build.finished && build.buildproducts %]
|
[% IF build.finished && build.buildproducts %]
|
||||||
@@ -426,9 +412,9 @@ END;
|
|||||||
<tbody>
|
<tbody>
|
||||||
[% FOREACH metric IN build.buildmetrics %]
|
[% FOREACH metric IN build.buildmetrics %]
|
||||||
<tr>
|
<tr>
|
||||||
<td><tt><a class="row-link" [% HTML.attributes(href => c.uri_for('/job' project.name jobset.name job 'metric' metric.name)) %]">[%HTML.escape(metric.name)%]</a></tt></td>
|
<td><tt><a class="row-link" [% HTML.attributes(href => c.uri_for('/job' project.name jobset.name job 'metric' metric.name)) %]">[% metric.name | html %]</a></tt></td>
|
||||||
<td style="text-align: right">[%metric.value%]</td>
|
<td style="text-align: right">[% HTML.escape(metric.value) %]</td>
|
||||||
<td>[%metric.unit%]</td>
|
<td>[% HTML.escape(metric.unit) %]</td>
|
||||||
</tr>
|
</tr>
|
||||||
[% END %]
|
[% END %]
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -470,8 +456,8 @@ END;
|
|||||||
[% FOREACH input IN build.dependents %]
|
[% FOREACH input IN build.dependents %]
|
||||||
<tr>
|
<tr>
|
||||||
<td>[% INCLUDE renderFullBuildLink build=input.build %]</td>
|
<td>[% INCLUDE renderFullBuildLink build=input.build %]</td>
|
||||||
<td><tt>[% input.name %]</tt></td>
|
<td><tt>[% input.name | html %]</tt></td>
|
||||||
<td><tt>[% input.build.system %]</tt></td>
|
<td><tt>[% input.build.system | html %]</tt></td>
|
||||||
<td>[% INCLUDE renderDateTime timestamp = input.build.timestamp %]</td>
|
<td>[% INCLUDE renderDateTime timestamp = input.build.timestamp %]</td>
|
||||||
</tr>
|
</tr>
|
||||||
[% END %]
|
[% END %]
|
||||||
@@ -498,7 +484,7 @@ END;
|
|||||||
[% ELSIF runcommandlogProblem == "disabled-jobset" %]
|
[% ELSIF runcommandlogProblem == "disabled-jobset" %]
|
||||||
This jobset does not enable Dynamic RunCommand support.
|
This jobset does not enable Dynamic RunCommand support.
|
||||||
[% ELSE %]
|
[% ELSE %]
|
||||||
Dynamic RunCommand is not enabled: [% runcommandlogProblem %].
|
Dynamic RunCommand is not enabled: [% HTML.escape(runcommandlogProblem) %].
|
||||||
[% END %]
|
[% END %]
|
||||||
</div>
|
</div>
|
||||||
[% END %]
|
[% END %]
|
||||||
@@ -517,18 +503,18 @@ END;
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex flex-column mr-auto align-self-center">
|
<div class="d-flex flex-column mr-auto align-self-center">
|
||||||
<div><tt>[% runcommandlog.command | html%]</tt></div>
|
<div><tt>[% runcommandlog.command | html %]</tt></div>
|
||||||
<div>
|
<div>
|
||||||
[% IF not runcommandlog.is_running() %]
|
[% IF not runcommandlog.is_running() %]
|
||||||
[% IF runcommandlog.did_fail_with_signal() %]
|
[% IF runcommandlog.did_fail_with_signal() %]
|
||||||
Exit signal: [% runcommandlog.signal %]
|
Exit signal: [% runcommandlog.signal | html %]
|
||||||
[% IF runcommandlog.core_dumped %]
|
[% IF runcommandlog.core_dumped %]
|
||||||
(Core Dumped)
|
(Core Dumped)
|
||||||
[% END %]
|
[% END %]
|
||||||
[% ELSIF runcommandlog.did_fail_with_exec_error() %]
|
[% ELSIF runcommandlog.did_fail_with_exec_error() %]
|
||||||
Exec error: [% runcommandlog.error_number %]
|
Exec error: [% runcommandlog.error_number | html %]
|
||||||
[% ELSIF not runcommandlog.did_succeed() %]
|
[% ELSIF not runcommandlog.did_succeed() %]
|
||||||
Exit code: [% runcommandlog.exit_code %]
|
Exit code: [% runcommandlog.exit_code | html %]
|
||||||
[% END %]
|
[% END %]
|
||||||
[% END %]
|
[% END %]
|
||||||
</div>
|
</div>
|
||||||
@@ -546,9 +532,9 @@ END;
|
|||||||
[% IF runcommandlog.uuid != undef %]
|
[% IF runcommandlog.uuid != undef %]
|
||||||
[% runLog = c.uri_for('/build', build.id, 'runcommandlog', runcommandlog.uuid) %]
|
[% runLog = c.uri_for('/build', build.id, 'runcommandlog', runcommandlog.uuid) %]
|
||||||
<div>
|
<div>
|
||||||
<a class="btn btn-secondary btn-sm" href="[% runLog %]">pretty</a>
|
<a class="btn btn-secondary btn-sm" [% HTML.attributes(href => runLog) %]>pretty</a>
|
||||||
<a class="btn btn-secondary btn-sm" href="[% runLog %]/raw">raw</a>
|
<a class="btn btn-secondary btn-sm" [% HTML.attributes(href => runLog) %]/raw">raw</a>
|
||||||
<a class="btn btn-secondary btn-sm" href="[% runLog %]/tail">tail</a>
|
<a class="btn btn-secondary btn-sm" [% HTML.attributes(href => runLog) %]/tail">tail</a>
|
||||||
</div>
|
</div>
|
||||||
[% END %]
|
[% END %]
|
||||||
</div>
|
</div>
|
||||||
@@ -577,17 +563,17 @@ END;
|
|||||||
|
|
||||||
[% IF eval.flake %]
|
[% IF eval.flake %]
|
||||||
|
|
||||||
<p>If you have <a href='https://nixos.org/nix/download.html'>Nix
|
<p>If you have <a href='https://nixos.org/download/'>Nix
|
||||||
installed</a>, you can reproduce this build on your own machine by
|
installed</a>, you can reproduce this build on your own machine by
|
||||||
running the following command:</p>
|
running the following command:</p>
|
||||||
|
|
||||||
<div class="card bg-light"><div class="card-body p-2"><code>
|
<div class="card bg-light"><div class="card-body p-2"><code>
|
||||||
<span class="shell-prompt"># </span>nix build [% HTML.escape(eval.flake) %]#hydraJobs.[% HTML.escape(job) %]
|
<span class="shell-prompt"># </span>nix build '[% HTML.escape(eval.flake) %]#hydraJobs.[% HTML.escape(job) %]'
|
||||||
</code></div></div>
|
</code></div></div>
|
||||||
|
|
||||||
[% ELSE %]
|
[% ELSE %]
|
||||||
|
|
||||||
<p>If you have <a href='https://nixos.org/nix/download.html'>Nix
|
<p>If you have <a href='https://nixos.org/download/'>Nix
|
||||||
installed</a>, you can reproduce this build on your own machine by
|
installed</a>, you can reproduce this build on your own machine by
|
||||||
downloading <a [% HTML.attributes(href => url) %]>a script</a>
|
downloading <a [% HTML.attributes(href => url) %]>a script</a>
|
||||||
that checks out all inputs of the build and then invokes Nix to
|
that checks out all inputs of the build and then invokes Nix to
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user