42fa9554 Andy McCall 1 month ago ↶
1
2
3
4
5
use crate :: api:: ApiError;
6
use crate :: db:: { self , flow as fdb} ;
7
use crate :: repos:: RepoName;
8
use crate :: Shared;
9
use axum:: extract:: { Path, State} ;
10
use axum:: http:: { HeaderMap, StatusCode} ;
11
use axum:: response:: { IntoResponse, Response} ;
12
use axum:: Json;
13
use flow_core:: api:: { Claim, ClaimRequest, Finish, LogBatch, MAX_RECORDS , MAX_RECORD_TEXT } ;
14
use flow_core:: { Pipeline, PIPELINE_PATH } ;
15
use oxy_core:: hash:: ChangeHash;
16
17
18
pub const JOB_TOKEN_PREFIX : & str = " flowjob_" ;
19
20
pub const JOB_TOKEN_MAX_AGE_MS : i64 = 2 * 3600 * 1000 ;
21
9008bf6c Andy McCall 1 month ago ↶
22
23
24
pub fn enabled ( app : & Shared, row : & db:: repos:: RepoRow) -> bool {
166f6e9b Andy McCall 15 days ago
25
26
if row.is_drop ( ) {
27
return false ;
28
}
9008bf6c Andy McCall 1 month ago ↶
29
match row.flow.as_str ( ) {
1870e4fa Andy McCall 20 days ago ↶
30
31
" on" | " own" => true ,
9008bf6c Andy McCall 1 month ago ↶
32
" off" => false ,
33
_ => app.config.flow.allows ( & row.owner, & row.name) ,
34
}
35
}
36
42fa9554 Andy McCall 1 month ago ↶
37
38
39
pub async fn on_push ( app : & Shared, row : & db:: repos:: RepoRow, bookmark : & str , state : & str , heads : & [ChangeHash], actor : & str ) {
c889911b Andy McCall 1 month ago ↶
40
if heads.is_empty ( ) | | ! enabled ( app, row) {
42fa9554 Andy McCall 1 month ago ↶
41
return ;
42
}
c889911b Andy McCall 1 month ago ↶
43
44
let started = match bookmark.strip_prefix ( " tags/" ) {
45
Some ( tag) => start_run_with ( app, row, bookmark, state, heads, actor, & format! ( " tag:{tag} " ) , None ) .await,
46
None => {
367f34b8 Andy McCall 15 days ago ↶
47
48
49
if let Some ( ( change, marker) ) = skip_asked ( app, row, bookmark, heads) .await {
50
skip_run ( app, row, bookmark, state, heads, actor, & change, marker) .await;
51
return ;
52
}
a9f1c0bf Andy McCall 17 days ago ↶
53
c889911b Andy McCall 1 month ago ↶
54
let touched = touched_since_last_run ( app, row, bookmark, heads) .await;
a9f1c0bf Andy McCall 17 days ago ↶
55
start_run_scoped ( app, row, bookmark, state, heads, actor, " push" , touched.as_deref ( ) ) .await
c889911b Andy McCall 1 month ago ↶
56
}
57
} ;
58
if let Err ( e) = started {
1edac4b6 Andy McCall 1 month ago ↶
59
tracing:: warn! ( " flow: starting a run of {}/{}: {e}" , row.owner, row.name) ;
367f34b8 Andy McCall 15 days ago ↶
60
}
61
}
62
5588bb36 Andy McCall 15 days ago ↶
63
64
65
66
fn reusable ( job : & flow_core:: Job) -> bool {
67
job.approval.is_none ( ) & & job.environment.is_none ( ) & & job.secrets.is_empty ( ) & & job.identities.is_empty ( )
68
}
69
367f34b8 Andy McCall 15 days ago ↶
70
71
72
pub const SKIP_MARKERS : [ & str ; 3 ] = [ " [skip flow]" , " [skip ci]" , " [ci skip]" ] ;
73
3b3a51af Andy McCall 15 days ago ↶
74
75
76
367f34b8 Andy McCall 15 days ago ↶
77
pub fn skip_marker ( message : & str ) -> Option< & 'static str > {
3b3a51af Andy McCall 15 days ago ↶
78
let m = outside_code ( message) .to_lowercase ( ) ;
367f34b8 Andy McCall 15 days ago ↶
79
SKIP_MARKERS .into_iter ( ) .find ( | k | m.contains ( k) )
80
}
81
3b3a51af Andy McCall 15 days ago ↶
82
83
84
fn outside_code ( text : & str ) -> String {
85
let mut out = String :: new( ) ;
86
let mut fenced = false ;
87
for line in text.lines ( ) {
88
if line.trim_start ( ) .starts_with ( " ```" ) {
89
fenced = ! fenced;
90
continue ;
91
}
92
if fenced {
93
continue ;
94
}
95
96
97
let pieces: Vec< & str > = line.split ( ' `' ) .collect ( ) ;
98
let closed = pieces.len ( ) % 2 = = 1 ;
99
for ( i, piece) in pieces.iter ( ) .enumerate ( ) {
100
let in_code = i % 2 = = 1 & & ( closed | | i + 1 < pieces.len ( ) ) ;
101
if ! in_code {
102
out.push_str ( piece) ;
103
out.push ( ' ' ) ;
104
}
105
}
106
out.push ( ' \n ' ) ;
107
}
108
out
109
}
110
367f34b8 Andy McCall 15 days ago ↶
111
112
113
pub fn skip_allowed ( row : & db:: repos:: RepoRow, bookmark : & str ) -> bool {
114
match row.flow_skip.as_str ( ) {
115
" never" => false ,
116
" others" => bookmark ! = row.default_bookmark,
117
_ => true ,
118
}
119
}
120
121
122
123
async fn skip_asked ( app : & Shared, row : & db:: repos:: RepoRow, bookmark : & str , heads : & [ChangeHash]) -> Option< ( ChangeHash, & 'static str ) > {
124
if ! skip_allowed ( row, bookmark) {
125
return None ;
126
}
127
let repo = RepoName:: new( & row.owner, & row.name) ? ;
128
let h = heads.to_vec ( ) ;
129
let newest = app
130
.repos
131
.read ( & repo, move | r, _ | {
132
let mut best: Option< ( oxy_core:: objects:: Hlc, ChangeHash, String) > = None ;
133
for x in & h {
134
let ( c, _ ) = r.store ( ) .get_change_lazy ( x) ? ;
135
if best.as_ref ( ) .is_none_or ( | ( t , _, _) | c.hlc > * t ) {
136
best = Some ( ( c.hlc, * x, c.message.clone ( ) ) ) ;
137
}
138
}
139
Ok ( best)
140
} )
141
.await
142
.ok ( ) ? ? ;
143
skip_marker ( & newest.2 ) .map ( | m | ( newest.1 , m) )
144
}
145
146
147
148
149
# [ allow ( clippy::too_many_arguments ) ]
150
async fn skip_run (
151
app : & Shared,
152
row : & db:: repos:: RepoRow,
153
bookmark : & str ,
154
state : & str ,
155
heads : & [ChangeHash],
156
actor : & str ,
157
change : & ChangeHash,
158
marker : & str ,
159
) {
160
let mut hex: Vec< String> = heads.iter ( ) .map ( | h | h.to_hex ( ) ) .collect ( ) ;
161
hex.sort ( ) ;
162
let heads_s = hex.join ( " +" ) ;
163
let why = format! ( " {marker} in change {} , pushed by {actor} " , change.short ( 12 ) ) ;
164
let base =
165
db:: flow:: NewRun { repo_id: row.id, bookmark, state, heads: & heads_s, pipeline_hash: " " , actor, trigger: " push" , error: None } ;
166
if let Err ( e) = db:: flow:: create_skipped_run( & app.db, base, & why) .await {
167
tracing:: warn! ( " flow: recording a skipped run of {}/{}: {e}" , row.owner, row.name) ;
1edac4b6 Andy McCall 1 month ago ↶
168
}
169
}
170
171
172
0650d94e Andy McCall 25 days ago ↶
173
174
175
pub async fn run_bookmark ( app : & Shared, row : & db:: repos:: RepoRow, bookmark : & str , actor : & str , jobs : & [String]) -> Result< i64 , String> {
1edac4b6 Andy McCall 1 month ago ↶
176
if ! enabled ( app, row) {
177
return Err ( " OxygenFlow is not enabled for this repository" .into ( ) ) ;
178
}
179
let repo = RepoName:: new( & row.owner, & row.name) .ok_or ( " bad repository name" ) ? ;
180
let b = bookmark.to_string ( ) ;
181
let found = app
182
.repos
183
.read ( & repo, move | r, _ | {
184
let Some ( st) = r.view ( ) ? .bookmarks.get ( & b) .copied ( ) else { return Ok ( None ) } ;
185
Ok ( Some ( ( st, r.store ( ) .get_state ( & st) ? .heads) ) )
186
} )
187
.await
188
.map_err ( | e | e.to_string ( ) ) ? ;
189
let Some ( ( state, heads) ) = found else { return Err ( format! ( " no bookmark {bookmark} " ) ) } ;
190
if heads.is_empty ( ) {
191
return Err ( format! ( " {bookmark} has no changes yet" ) ) ;
192
}
0650d94e Andy McCall 25 days ago ↶
193
start_run_at ( app, row, bookmark, & state.to_hex ( ) , & heads, actor, " manual" , None , None , jobs)
1edac4b6 Andy McCall 1 month ago ↶
194
.await?
195
.ok_or_else ( | | format! ( " {bookmark} has no {PIPELINE_PATH} " ) )
196
}
197
4f692533 Andy McCall 1 month ago ↶
198
199
0650d94e Andy McCall 25 days ago ↶
200
pub async fn run_change ( app : & Shared, row : & db:: repos:: RepoRow, change : & str , actor : & str , jobs : & [String]) -> Result< i64 , String> {
4f692533 Andy McCall 1 month ago ↶
201
if ! enabled ( app, row) {
202
return Err ( " OxygenFlow is not enabled for this repository" .into ( ) ) ;
203
}
204
let h = ChangeHash:: from_hex( change.trim ( ) .trim_start_matches ( ' #' ) ) .ok_or ( " give the full hash of a change" ) ? ;
205
let repo = RepoName:: new( & row.owner, & row.name) .ok_or ( " bad repository name" ) ? ;
df15b747 Andy McCall 1 month ago ↶
206
let known = app.repos.read ( & repo, move | r, _ | Ok ( r.store ( ) .get_change_meta ( & h) .is_ok ( ) ) ) .await.map_err ( | e | e.to_string ( ) ) ? ;
4f692533 Andy McCall 1 month ago ↶
207
if ! known {
208
return Err ( format! ( " no change {} in this repository" , & change.trim ( ) [ .. 12. min ( change.trim ( ) .len ( ) ) ] ) ) ;
209
}
210
let state = oxy_core:: hash:: hash_object( oxy_core:: hash:: ObjectType:: State, & oxy_core:: objects:: StateObject:: new( vec! [ h] ) .encode ( ) ) ;
0650d94e Andy McCall 25 days ago ↶
211
start_run_at ( app, row, " " , & state.to_hex ( ) , & [ h] , actor, " manual" , None , None , jobs)
4f692533 Andy McCall 1 month ago ↶
212
.await?
213
.ok_or_else ( | | format! ( " that change's state has no {PIPELINE_PATH} " ) )
214
}
215
1edac4b6 Andy McCall 1 month ago ↶
216
217
218
pub async fn rerun ( app : & Shared, row : & db:: repos:: RepoRow, run : & fdb:: Run, actor : & str ) -> Result< i64 , String> {
219
if ! enabled ( app, row) {
220
return Err ( " OxygenFlow is not enabled for this repository" .into ( ) ) ;
221
}
222
if run.live ( ) {
223
return Err ( format! ( " run #{} is still {} " , run.number, run.status) ) ;
224
}
225
let heads: Vec< ChangeHash> = run.head_list ( ) .into_iter ( ) .filter_map ( ChangeHash:: from_hex) .collect ( ) ;
226
if heads.is_empty ( ) {
227
return Err ( " the run names no changes" .into ( ) ) ;
228
}
c889911b Andy McCall 1 month ago ↶
229
230
let trigger = format! ( " rerun:{} :{} " , run.number, original_event ( run) ) ;
231
start_run ( app, row, & run.bookmark, & run.state, & heads, actor, & trigger)
1edac4b6 Andy McCall 1 month ago ↶
232
.await?
233
.ok_or_else ( | | format! ( " that state has no {PIPELINE_PATH} " ) )
745d225d Andy McCall 1 month ago ↶
234
}
235
e8fa5369 Andy McCall 22 days ago ↶
236
237
238
239
240
pub async fn rerun_job ( app : & Shared, row : & db:: repos:: RepoRow, run : & fdb:: Run, job : & str , actor : & str ) -> Result< usize , String> {
241
if ! enabled ( app, row) {
242
return Err ( " OxygenFlow is not enabled for this repository" .into ( ) ) ;
243
}
244
if run.live ( ) {
245
return Err ( format! ( " run #{} is still {} : wait for it, or cancel it, first" , run.number, run.status) ) ;
246
}
247
let changed = fdb:: rerun_job( & app.db, run.id, job) .await.map_err ( | e | {
248
tracing:: warn! ( " flow: rerunning {job} of run #{} of {}/{}: {e}" , run.number, row.owner, row.name) ;
249
" the job could not be queued again" .to_string ( )
250
} ) ? ;
251
if changed.is_empty ( ) {
252
return Err ( format! ( " run #{} has no job {job:?} " , run.number) ) ;
253
}
254
for j in & changed {
255
report_check ( app, row, & run.state, run.number, & j.name, " queued" , & format! ( " queued again by {actor} " ) ) .await;
256
}
257
Ok ( changed.len ( ) )
258
}
259
745d225d Andy McCall 1 month ago ↶
260
261
pub async fn cancel ( app : & Shared, row : & db:: repos:: RepoRow, run : & fdb:: Run, actor : & str ) -> Result< ( ) , String> {
262
if ! run.live ( ) {
263
return Err ( format! ( " run #{} is already {} " , run.number, run.status) ) ;
264
}
265
cancel_with ( app, run, & format! ( " cancelled by {actor} " ) ) .await.map_err ( | e | {
266
tracing:: warn! ( " flow: cancelling run #{} of {}/{}: {e}" , run.number, row.owner, row.name) ;
267
" the run could not be cancelled" .to_string ( )
268
} )
269
}
270
271
async fn cancel_with ( app : & Shared, run : & fdb:: Run, why : & str ) -> sqlx:: Result< ( ) > {
272
let changed = fdb:: cancel_run( & app.db, run.id, why) .await? ;
273
report_changed ( app, run.id, changed) .await
c889911b Andy McCall 1 month ago ↶
274
}
275
417184c7 Andy McCall 20 days ago ↶
276
277
278
279
280
pub async fn may_decide (
281
app : & Shared,
282
row : & db:: repos:: RepoRow,
283
run : & fdb:: Run,
284
job : & fdb:: Job,
285
who : & db:: accounts:: Account,
286
) -> Result< flow_core:: Approval, String> {
287
let spec: flow_core:: Job = serde_json:: from_str( & job.spec) .map_err ( | e | format! ( " stored job: {e} " ) ) ? ;
288
let Some ( approval) = spec.approval else { return Err ( format! ( " {} does not wait for approval" , job.name) ) } ;
289
if job.status ! = " queued" | | job.approval ! = " waiting" {
290
return Err ( format! ( " {} is not waiting for approval" , job.name) ) ;
291
}
292
let jobs = fdb:: jobs( & app.db, run.id) .await.map_err ( | e | e.to_string ( ) ) ? ;
293
if let Some ( d) = job.depends_on ( ) .iter ( ) .find ( | d | ! jobs.iter ( ) .any ( | j | & j.name = = * d & & j.status = = " passed" ) ) {
294
return Err ( format! ( " {} waits for {d} first" , job.name) ) ;
295
}
296
let need = crate :: auth:: roles:: Role:: parse( & approval.by) .unwrap_or ( crate :: auth:: roles:: Role:: Maintain) ;
297
let role = crate :: auth:: roles:: role_for( & app.db, Some ( who) , row) .await.map_err ( | e | e.to_string ( ) ) ? ;
298
if role.is_none_or ( | r | r < need ) {
299
return Err ( format! ( " approving {} takes the {} role on {} /{} " , job.name, approval.by, row.owner, row.name) ) ;
300
}
301
if approval.others_only & & who.username = = run.actor {
302
return Err ( format! ( " {} must be approved by someone other than {} , whose push started the run" , job.name, run.actor) ) ;
303
}
304
Ok ( approval)
305
}
306
307
308
309
310
pub async fn decide (
311
app : & Shared,
312
row : & db:: repos:: RepoRow,
313
run : & fdb:: Run,
314
name : & str ,
315
who : & db:: accounts:: Account,
316
approve : bool ,
05f1a85a Andy McCall 18 days ago ↶
317
via : Option< & str > ,
417184c7 Andy McCall 20 days ago ↶
318
) -> Result< String, String> {
05f1a85a Andy McCall 18 days ago ↶
319
decide_with ( app, row, run, name, who, approve, Through { seal: None , via } ) .await
47e37402 Andy McCall 20 days ago ↶
320
}
321
05f1a85a Andy McCall 18 days ago ↶
322
323
324
# [ derive ( Clone, Copy, Default ) ]
325
struct Through < 'a > {
326
seal : Option< & 'a str > ,
327
via : Option< & 'a str > ,
328
}
329
47e37402 Andy McCall 20 days ago ↶
330
331
332
async fn decide_with (
333
app : & Shared,
334
row : & db:: repos:: RepoRow,
335
run : & fdb:: Run,
336
name : & str ,
337
who : & db:: accounts:: Account,
338
approve : bool ,
05f1a85a Andy McCall 18 days ago ↶
339
through : Through< '_ > ,
47e37402 Andy McCall 20 days ago ↶
340
) -> Result< String, String> {
05f1a85a Andy McCall 18 days ago ↶
341
let Through { seal, via } = through;
417184c7 Andy McCall 20 days ago ↶
342
let jobs = fdb:: jobs( & app.db, run.id) .await.map_err ( | e | e.to_string ( ) ) ? ;
343
let Some ( job) = jobs.into_iter ( ) .find ( | j | j.name = = name ) else { return Err ( format! ( " run #{} has no job {name} " , run.number) ) } ;
47e37402 Andy McCall 20 days ago ↶
344
let approval = may_decide ( app, row, run, & job, who) .await? ;
4a68f106 Andy McCall 17 days ago ↶
345
346
347
348
if let ( Some ( agent) , None ) = ( via, seal) {
349
if ! row.agents_decide_gates {
350
return Err ( format! (
351
" {name} is decided by a person here, not through an agent's token ({agent} ): decide it yourself on the run page, or a maintainer can let agents decide approval gates in Settings › OxygenFlow"
352
) ) ;
353
}
354
}
47e37402 Andy McCall 20 days ago ↶
355
if approve & & approval.sealed & & seal.is_none ( ) {
356
return Err ( format! (
357
" {name} takes an approval signed with your key: in a checkout of {} /{} , run oxy seal --flow {} /{} /{} /flow/{} --job {name} " ,
358
row.owner,
359
row.name,
360
app.config.base_url ( ) ,
361
row.owner,
362
row.name,
363
run.number
364
) ) ;
365
}
417184c7 Andy McCall 20 days ago ↶
366
let target = format! ( " {} /{} #{} {name} " , row.owner, row.name, run.number) ;
47e37402 Andy McCall 20 days ago ↶
367
let sealed = seal.map ( | r | format! ( " , sealed (approval {} )" , & r[ .. 12. min ( r.len ( ) ) ] ) ) .unwrap_or_default ( ) ;
05f1a85a Andy McCall 18 days ago ↶
368
369
let by = match via {
370
Some ( v) => format! ( " {} via {v} " , who.username) ,
371
None => who.username.clone ( ) ,
372
} ;
373
let detail = [ seal.unwrap_or ( " " ) , & via.map ( | v | format! ( " via {v} " ) ) .unwrap_or_default ( ) ]
374
.iter ( )
375
.filter ( | s | ! s.is_empty ( ) )
376
.cloned ( )
377
.collect:: < Vec< _ > > ( )
378
.join ( " , " ) ;
417184c7 Andy McCall 20 days ago ↶
379
let said = if approve {
05f1a85a Andy McCall 18 days ago ↶
380
if ! fdb:: approve_job( & app.db, job.id, & who.username, seal.unwrap_or ( " " ) , via.unwrap_or ( " " ) ) .await.map_err ( | e | e.to_string ( ) ) ? {
417184c7 Andy McCall 20 days ago ↶
381
return Err ( format! ( " {name} was decided on meanwhile" ) ) ;
382
}
05f1a85a Andy McCall 18 days ago ↶
383
db:: accounts:: audit( & app.db, Some ( who) , " flow.approved" , & target, & detail) .await.ok ( ) ;
384
format! ( " approved by {by} {sealed} " )
417184c7 Andy McCall 20 days ago ↶
385
} else {
05f1a85a Andy McCall 18 days ago ↶
386
fdb:: note_rejection( & app.db, job.id, & who.username, via.unwrap_or ( " " ) ) .await.map_err ( | e | e.to_string ( ) ) ? ;
387
let why = format! ( " rejected by {by} {sealed} " ) ;
417184c7 Andy McCall 20 days ago ↶
388
let changed =
389
fdb:: finish( & app.db, job.id, fdb:: Outcome { status: " cancelled" , exit_code: None , error: & why, receipt: " " , receipt_hash: " " } )
390
.await
391
.map_err ( | e | e.to_string ( ) ) ? ;
392
report_changed ( app, run.id, changed) .await.ok ( ) ;
05f1a85a Andy McCall 18 days ago ↶
393
db:: accounts:: audit( & app.db, Some ( who) , " flow.rejected" , & target, & detail) .await.ok ( ) ;
417184c7 Andy McCall 20 days ago ↶
394
why
395
} ;
396
let record = flow_core:: api:: Record { t: 0 , stream: " flow" .into ( ) , text: said.clone ( ) } ;
397
fdb:: append_log( & app.db, job.id, & [ record] , MAX_RECORDS as i64 ) .await.ok ( ) ;
398
Ok ( format! ( " {name} : {said} " ) )
399
}
400
dc59d1b5 Andy McCall 20 days ago ↶
401
402
403
404
async fn tell_approvers ( app : & Shared, row : & db:: repos:: RepoRow, run : & fdb:: Run) {
405
const TOLD : & str = " waiting for approval; the people who may approve were told" ;
406
let Ok ( jobs) = fdb:: jobs( & app.db, run.id) .await else { return } ;
407
for job in jobs.iter ( ) .filter ( | j | j.status = = " queued" & & j.approval = = " waiting" ) {
408
if ! job.depends_on ( ) .iter ( ) .all ( | d | jobs.iter ( ) .any ( | j | & j.name = = d & & j.status = = " passed" ) ) {
409
continue ;
410
}
411
let already = fdb:: log( & app.db, job.id, 0 , 50 ) .await.unwrap_or_default ( ) .iter ( ) .any ( | l | l.text = = TOLD ) ;
412
if already {
413
continue ;
414
}
415
let mut targets = Vec :: new( ) ;
416
for a in db:: accounts:: accounts( & app.db) .await.unwrap_or_default ( ) .iter ( ) .filter ( | a | ! a.blocked ( ) ) {
417
if may_decide ( app, row, run, job, a) .await.is_ok ( ) {
418
targets.push ( ( a.id, db:: notify:: Reason:: Participating) ) ;
419
}
420
}
421
let record = flow_core:: api:: Record { t: 0 , stream: " flow" .into ( ) , text: TOLD .into ( ) } ;
422
fdb:: append_log( & app.db, job.id, & [ record] , MAX_RECORDS as i64 ) .await.ok ( ) ;
423
let title = format! ( " {} /{} run #{} : {} waits for your approval" , row.owner, row.name, run.number, job.name) ;
424
let url = format! ( " /{} /{} /flow/{} " , row.owner, row.name, run.number) ;
425
crate :: notify:: tell( app, " flow.approval" , & title, & url, & run.actor, Some ( row.id) , targets) .await;
426
}
47e37402 Andy McCall 20 days ago ↶
427
}
428
429
430
431
432
pub async fn approval_statement (
433
app : & Shared,
434
row : & db:: repos:: RepoRow,
435
run : & fdb:: Run,
436
name : & str ,
437
) -> Result< ( oxy_core:: approval:: Statement, fdb:: Job) , String> {
438
let jobs = fdb:: jobs( & app.db, run.id) .await.map_err ( | e | e.to_string ( ) ) ? ;
439
let Some ( job) = jobs.into_iter ( ) .find ( | j | j.name = = name ) else { return Err ( format! ( " run #{} has no job {name} " , run.number) ) } ;
440
let spec: flow_core:: Job = serde_json:: from_str( & job.spec) .map_err ( | e | format! ( " stored job: {e} " ) ) ? ;
441
let sandbox: flow_core:: Sandbox = serde_json:: from_str( & job.sandbox) .map_err ( | e | format! ( " stored sandbox: {e} " ) ) ? ;
442
let repo_name = RepoName:: new( & row.owner, & row.name) .ok_or ( " bad repository name" ) ? ;
443
let repo = app.repos.read ( & repo_name, | r , _| Ok ( r.id ( ) ? ) ) .await.map_err ( | e | e.to_string ( ) ) ? ;
444
let state = oxy_core:: hash:: ObjectRef:: from_hex( & run.state) .ok_or ( " the run has no state" ) ? ;
445
let inputs = artifact_inputs ( app, & job, & spec) .await? .iter ( ) .filter_map ( | i | oxy_core:: hash:: ObjectRef:: from_hex( & i.blob) ) .collect ( ) ;
446
let statement = oxy_core:: approval:: Statement {
447
repo,
448
run: format! ( " {} /{} /{} /flow/{} " , app.config.base_url ( ) , row.owner, row.name, run.number) ,
449
job: name.to_string ( ) ,
450
job_hash: oxy_core:: hash:: ObjectRef( spec.hash ( & sandbox) ) ,
451
state,
452
inputs,
453
environment: spec.environment.clone ( ) .unwrap_or_default ( ) ,
454
approve: true ,
455
} ;
456
Ok ( ( statement, job) )
457
}
458
317d776c Andy McCall 18 days ago ↶
459
460
461
462
463
464
async fn linked_through_chain (
465
app : & Shared,
466
repo : & RepoName,
467
signer : & oxy_core:: identity:: Identity,
468
links : & [( String , String ) ],
469
) -> Option< ( String, Vec< oxy_core:: identity:: Identity> ) > {
470
let mut chain = vec! [ signer.clone ( ) ] ;
471
let mut cur = signer.clone ( ) ;
472
for _ in 0 .. 64 {
473
let prev = cur.previous? ;
474
let hex = prev.to_hex ( ) ;
475
let found = match db:: accounts:: identity_by_key( & app.db, & hex) .await.ok ( ) ? {
476
Some ( l) => oxy_core:: identity:: Identity:: decode( & l.object) .ok ( ) ,
477
None => app.repos.read ( repo, move | r, _ | Ok ( r.store ( ) .get_identity ( & prev) .ok ( ) ) ) .await.ok ( ) .flatten ( ) ,
478
} ;
479
let id = found? ;
480
if id.hash ( ) ! = prev | | id.ed25519_pk ! = cur.ed25519_pk | | id.mldsa65_pk ! = cur.mldsa65_pk {
481
return None ;
482
}
483
if let Some ( ( _ , u) ) = links.iter ( ) .find ( | ( k , _) | * k = = hex) {
484
return Some ( ( u.clone ( ) , chain) ) ;
485
}
486
chain.push ( id.clone ( ) ) ;
487
cur = id;
488
}
489
None
490
}
491
47e37402 Andy McCall 20 days ago ↶
492
493
494
495
496
pub async fn decide_sealed ( app : & Shared, row : & db:: repos:: RepoRow, run : & fdb:: Run, name : & str , signed_hex : & str ) -> Result< String, String> {
497
let bytes = hex:: decode( signed_hex.trim ( ) ) .map_err ( | _| " the signed approval is not hex" .to_string ( ) ) ? ;
498
let sa = oxy_core:: approval:: SignedApproval:: decode( & bytes) .map_err ( | e | format! ( " not a signed approval: {e} " ) ) ? ;
499
sa.verify ( ) .map_err ( | e | format! ( " it does not verify: {e} " ) ) ? ;
500
let ( mut expected, _ ) = approval_statement ( app, row, run, name) .await? ;
501
let given = sa.statement ( ) .map_err ( | e | e.to_string ( ) ) ? ;
502
expected.approve = given.approve;
503
if ! sa.covers ( & expected) {
504
return Err ( format! (
505
" it approves something else than job {name} of run #{} as it is now (another run, state, plan or environment): sign again" ,
506
run.number
507
) ) ;
508
}
509
let key = sa.signer.hash ( ) .to_hex ( ) ;
510
let links = db:: accounts:: identity_accounts( & app.db) .await.map_err ( | e | e.to_string ( ) ) ? ;
511
let repo_name = RepoName:: new( & row.owner, & row.name) .ok_or ( " bad repository name" ) ? ;
317d776c Andy McCall 18 days ago ↶
512
let username = match links.iter ( ) .find ( | ( k , _) | * k = = key) {
513
Some ( ( _ , u) ) => u.clone ( ) ,
514
515
516
None => match linked_through_chain ( app, & repo_name, & sa.signer, & links) .await {
517
Some ( ( u, chain) ) => {
518
if let Ok ( Some ( acc) ) = db:: accounts:: account_by_name( & app.db, & u) .await {
519
for id in & chain {
520
let k = id.hash ( ) .to_hex ( ) ;
521
if let Ok ( None ) = db:: accounts:: identity_by_key( & app.db, & k) .await {
522
db:: accounts:: link_identity( & app.db, acc.id, & k, & id.name, & id.email, & id.encode ( ) ) .await.ok ( ) ;
523
}
524
}
525
}
526
u
527
}
528
None => {
529
return Err ( format! (
530
" key {} is not linked to an account here, nor a renewal of a key that is (link it under Settings › Keys; an agent's delegated key cannot approve)" ,
531
& key[ .. 12 ]
532
) )
533
}
534
} ,
535
} ;
536
let who = db:: accounts:: account_by_name( & app.db, & username) .await.map_err ( | e | e.to_string ( ) ) ? .ok_or ( " the key's account is gone" ) ? ;
47e37402 Andy McCall 20 days ago ↶
537
let jobs = fdb:: jobs( & app.db, run.id) .await.map_err ( | e | e.to_string ( ) ) ? ;
538
let job = jobs.iter ( ) .find ( | j | j.name = = name ) .ok_or ( " no such job" ) ? ;
539
may_decide ( app, row, run, job, & who) .await? ;
965b950f Andy McCall 18 days ago ↶
540
let ( rid, signers) = ( sa.attest.repo, sa.sigs.iter ( ) .map ( | s | s.key ) .collect:: < Vec< _ > > ( ) ) ;
47e37402 Andy McCall 20 days ago ↶
541
let kept = app.repos.write ( & repo_name, move | r, _ | Ok ( r.add_approval ( & sa) ? ) ) .await.map_err ( | e | e.to_string ( ) ) ? ;
965b950f Andy McCall 18 days ago ↶
542
crate :: tlog:: log( app, vec! [ crate :: tlog:: Pending { kind: " approval" , repo: rid, repo_row: Some ( row.id) , object: kept, signers } ] ) .await;
05f1a85a Andy McCall 18 days ago ↶
543
decide_with ( app, row, run, name, & who, given.approve, Through { seal: Some ( & kept.to_hex ( ) ) , via: None } ) .await
dc59d1b5 Andy McCall 20 days ago ↶
544
}
545
417184c7 Andy McCall 20 days ago ↶
546
547
548
pub async fn expire_approvals ( app : & Shared, now : i64 ) -> sqlx:: Result< usize > {
549
let mut ended = 0 ;
550
for job in fdb:: waiting_for_approval( & app.db) .await? {
551
let Ok ( spec) = serde_json:: from_str:: < flow_core:: Job> ( & job.spec) else { continue } ;
552
let Some ( approval) = spec.approval else { continue } ;
553
let Some ( run) = fdb:: run_by_id( & app.db, job.run_id) .await? else { continue } ;
554
let jobs = fdb:: jobs( & app.db, run.id) .await? ;
555
let wanted = job.depends_on ( ) ;
556
let deps: Vec< & fdb:: Job> = jobs.iter ( ) .filter ( | j | wanted.contains ( & j.name) ) .collect ( ) ;
557
if deps.iter ( ) .any ( | d | d.status ! = " passed" ) {
558
continue ;
559
}
560
let since = deps.iter ( ) .filter_map ( | d | d.finished_ms ) .max ( ) .unwrap_or ( run.created_ms) ;
561
if now - since < approval.expires_s as i64 * 1000 {
562
continue ;
563
}
564
let hours = approval.expires_s as f64 / 3600. 0 ;
565
let why =
566
format! ( " no approval within {} " , if hours > = 1. 0 { format! ( " {hours:.0} h" ) } else { format! ( " {} m" , approval.expires_s / 60 ) } ) ;
567
let changed =
568
fdb:: finish( & app.db, job.id, fdb:: Outcome { status: " cancelled" , exit_code: None , error: & why, receipt: " " , receipt_hash: " " } )
569
.await? ;
570
report_changed ( app, run.id, changed) .await? ;
571
ended + = 1 ;
572
}
573
Ok ( ended)
a9f1c0bf Andy McCall 17 days ago ↶
574
}
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
fn supersedes ( trigger : & str ) -> bool {
597
matches! ( trigger, " push" | " proposal" )
417184c7 Andy McCall 20 days ago ↶
598
}
599
a9f1c0bf Andy McCall 17 days ago ↶
600
601
602
50928fab Andy McCall 1 day ago ↶
603
604
605
606
a9f1c0bf Andy McCall 17 days ago ↶
607
async fn supersede ( app : & Shared, row : & db:: repos:: RepoRow, bookmark : & str , trigger : & str , number : i64 ) {
50928fab Andy McCall 1 day ago ↶
608
let running_too = ! row.flow_keep_running & & ! ( trigger = = " push" & & bookmark = = row.default_bookmark) ;
609
let Ok ( older) = fdb:: live_runs_before( & app.db, row.id, bookmark, trigger, number, running_too) .await else { return } ;
745d225d Andy McCall 1 month ago ↶
610
for run in older {
611
if let Err ( e) = cancel_with ( app, & run, & format! ( " superseded by run #{number} " ) ) .await {
612
tracing:: warn! ( " flow: superseding run #{} of {}/{}: {e}" , run.number, row.owner, row.name) ;
613
}
614
}
615
}
616
2abea308 Andy McCall 17 days ago ↶
617
618
619
620
621
622
pub async fn moot_after_landing ( app : & Shared, row : & db:: repos:: RepoRow, p : & db:: proposals:: ProposalRow) {
623
let now = db:: now_ms( ) ;
624
let mut moot = Vec :: new( ) ;
625
let same_repo = p.source_repo_id.is_none_or ( | id | id = = row.id ) ;
626
if p.source_kind = = " bookmark" & & same_repo & & ! p.source_bookmark.is_empty ( ) & & p.source_bookmark ! = p.target {
627
moot.extend ( fdb:: unstarted_runs_on( & app.db, row.id, & p.source_bookmark, " push" , now) .await.unwrap_or_default ( ) ) ;
628
}
629
moot.extend ( fdb:: unstarted_runs_on( & app.db, row.id, & format! ( " proposal/{} " , p.number) , " proposal" , now) .await.unwrap_or_default ( ) ) ;
630
for run in moot {
631
if let Err ( e) = cancel_with ( app, & run, & format! ( " moot: proposal #{} landed" , p.number) ) .await {
632
tracing:: warn! ( " flow: cancelling run #{} of {}/{}: {e}" , run.number, row.owner, row.name) ;
633
}
634
}
635
}
636
c889911b Andy McCall 1 month ago ↶
637
638
639
fn event_of < 'a > ( trigger : & 'a str , bookmark : & 'a str ) -> flow_core:: pipeline:: Event< 'a > {
640
use flow_core:: pipeline:: Event;
641
let t = match trigger.strip_prefix ( " rerun:" ) {
642
Some ( rest) => rest.split_once ( ' :' ) .map ( | ( _, e ) | e ) .unwrap_or ( " manual" ) ,
643
None => trigger,
644
} ;
645
if let Some ( tag) = t.strip_prefix ( " tag:" ) {
646
Event:: Tag( tag)
647
} else if t = = " push" {
648
Event:: Push( bookmark)
649
} else if t = = " schedule" {
650
Event:: Schedule
651
} else {
652
Event:: Manual
653
}
1edac4b6 Andy McCall 1 month ago ↶
654
}
655
c889911b Andy McCall 1 month ago ↶
656
657
fn original_event ( run : & fdb:: Run) -> String {
658
let t = run.trigger.as_str ( ) ;
659
match t.strip_prefix ( " rerun:" ) {
660
Some ( rest) => rest.split_once ( ' :' ) .map ( | ( _, e ) | e.to_string ( ) ) .unwrap_or_else ( | | " manual" .into ( ) ) ,
661
None => t.to_string ( ) ,
662
}
663
}
664
d7ce41ac Andy McCall 1 month ago ↶
665
666
667
668
669
async fn touched_since_last_run ( app : & Shared, row : & db:: repos:: RepoRow, bookmark : & str , heads : & [ChangeHash]) -> Option< Vec< String> > {
670
let last = fdb:: latest_on( & app.db, row.id, bookmark) .await.ok ( ) ? ? ;
671
let old: Vec< ChangeHash> = last.head_list ( ) .into_iter ( ) .filter_map ( ChangeHash:: from_hex) .collect ( ) ;
672
if old.is_empty ( ) {
673
return None ;
674
}
675
let repo = RepoName:: new( & row.owner, & row.name) ? ;
676
let new = heads.to_vec ( ) ;
677
app.repos
678
.read ( & repo, move | r, cache| {
679
let fresh = r.negotiate ( & old, & new) ? ;
680
let loaded = cache.load ( r, & new) ? ;
681
let by_node: std:: collections:: HashMap< oxy_core:: ids:: NodeId, & String> =
682
loaded.snapshot.files.iter ( ) .map ( | ( p , f ) | ( f.node, p) ) .collect ( ) ;
683
let mut paths = std:: collections:: BTreeSet:: new( ) ;
684
for h in fresh {
685
let ( c, _ ) = r.store ( ) .get_change_lazy ( & h) ? ;
686
let nodes = c.edits.iter ( ) .map ( | e | e.node ( ) ) .chain ( c.tree_ops.iter ( ) .map ( | o | o.node ( ) ) ) ;
687
for n in nodes {
688
if let Some ( p) = by_node.get ( & n) {
689
paths.insert ( ( * p) .clone ( ) ) ;
690
}
691
}
692
}
693
Ok ( paths.into_iter ( ) .collect:: < Vec< String> > ( ) )
694
} )
695
.await
696
.ok ( )
697
}
698
699
700
701
fn in_scope ( scope : & [String], touched : Option< & [ String] > ) -> bool {
702
let Some ( touched) = touched else { return true } ;
703
if scope.is_empty ( ) {
704
return true ;
705
}
706
let mut b = globset:: GlobSetBuilder:: new( ) ;
707
for g in scope {
708
match globset:: Glob:: new( g) {
709
Ok ( g) => {
710
b.add ( g) ;
711
}
712
Err ( _ ) => return true ,
713
}
714
}
715
let Ok ( set) = b.build ( ) else { return true } ;
716
touched.iter ( ) .any ( | p | set.is_match ( p) )
717
}
718
719
720
721
# [ allow ( clippy::too_many_arguments ) ]
722
pub async fn start_run_scoped (
723
app : & Shared,
724
row : & db:: repos:: RepoRow,
725
bookmark : & str ,
726
state : & str ,
727
heads : & [ChangeHash],
728
actor : & str ,
729
trigger : & str ,
730
touched : Option< & [ String] > ,
731
) -> Result< Option< i64 > , String> {
732
start_run_with ( app, row, bookmark, state, heads, actor, trigger, touched) .await
733
}
734
1edac4b6 Andy McCall 1 month ago ↶
735
736
737
738
739
740
741
pub async fn start_run (
742
app : & Shared,
743
row : & db:: repos:: RepoRow,
744
bookmark : & str ,
745
state : & str ,
746
heads : & [ChangeHash],
747
actor : & str ,
748
trigger : & str ,
749
) -> Result< Option< i64 > , String> {
d7ce41ac Andy McCall 1 month ago ↶
750
start_run_with ( app, row, bookmark, state, heads, actor, trigger, None ) .await
751
}
752
753
# [ allow ( clippy::too_many_arguments ) ]
754
async fn start_run_with (
755
app : & Shared,
756
row : & db:: repos:: RepoRow,
757
bookmark : & str ,
758
state : & str ,
759
heads : & [ChangeHash],
760
actor : & str ,
761
trigger : & str ,
762
touched : Option< & [ String] > ,
763
) -> Result< Option< i64 > , String> {
0650d94e Andy McCall 25 days ago ↶
764
start_run_at ( app, row, bookmark, state, heads, actor, trigger, touched, None , & [ ] ) .await
348f8c4b Andy McCall 1 month ago ↶
765
}
766
0650d94e Andy McCall 25 days ago ↶
767
768
348f8c4b Andy McCall 1 month ago ↶
769
# [ allow ( clippy::too_many_arguments ) ]
770
async fn start_run_at (
771
app : & Shared,
772
row : & db:: repos:: RepoRow,
773
bookmark : & str ,
774
state : & str ,
775
heads : & [ChangeHash],
776
actor : & str ,
777
trigger : & str ,
778
touched : Option< & [ String] > ,
779
at : Option< flow_core:: pipeline:: When> ,
0650d94e Andy McCall 25 days ago ↶
780
only : & [String],
348f8c4b Andy McCall 1 month ago ↶
781
) -> Result< Option< i64 > , String> {
1edac4b6 Andy McCall 1 month ago ↶
782
let repo = RepoName:: new( & row.owner, & row.name) .ok_or ( " bad repository name" ) ? ;
42fa9554 Andy McCall 1 month ago ↶
783
let h = heads.to_vec ( ) ;
784
let text = app
785
.repos
786
.read ( & repo, move | r, cache| {
787
let loaded = cache.load ( r, & h) ? ;
788
Ok ( match loaded.snapshot.files.get ( PIPELINE_PATH ) .map ( | f | & f.content) {
789
Some ( oxy_core:: state:: FileContent:: Text { bytes, .. } ) => Some ( String :: from_utf8_lossy( bytes) .into_owned ( ) ) ,
790
_ => None ,
791
} )
792
} )
1edac4b6 Andy McCall 1 month ago ↶
793
.await
794
.map_err ( | e | format! ( " reading the pipeline: {e} " ) ) ? ;
795
let Some ( text) = text else { return Ok ( None ) } ;
42fa9554 Andy McCall 1 month ago ↶
796
let mut hex: Vec< String> = heads.iter ( ) .map ( | h | h.to_hex ( ) ) .collect ( ) ;
797
hex.sort ( ) ;
798
let heads_s = hex.join ( " +" ) ;
1edac4b6 Andy McCall 1 month ago ↶
799
let base = db:: flow:: NewRun { repo_id: row.id, bookmark, state, heads: & heads_s, pipeline_hash: " " , actor, trigger, error: None } ;
800
let id = match Pipeline:: parse( & text) {
5f3fa151 Andy McCall 10 days ago ↶
801
802
803
804
Err ( _ ) if at.is_some ( ) => return Ok ( None ) ,
42fa9554 Andy McCall 1 month ago ↶
805
Err ( problems) => {
806
let msg = problems.to_string ( ) ;
1edac4b6 Andy McCall 1 month ago ↶
807
db:: flow:: create_run( & app.db, db:: flow:: NewRun { error: Some ( & msg) , .. base } , & [ ] ) .await.map_err ( | e | e.to_string ( ) ) ?
42fa9554 Andy McCall 1 month ago ↶
808
}
809
Ok ( p) => {
810
let depths = p.depths ( ) ;
d7ce41ac Andy McCall 1 month ago ↶
811
812
c889911b Andy McCall 1 month ago ↶
813
814
348f8c4b Andy McCall 1 month ago ↶
815
let event = match at {
816
Some ( w) => flow_core:: pipeline:: Event:: ScheduleAt( w) ,
817
None => event_of ( trigger, bookmark) ,
818
} ;
0650d94e Andy McCall 25 days ago ↶
819
let starts = | j : & flow_core:: Job| p.on_of ( j) .matches ( event) & & in_scope ( & j.scope, touched) ;
820
let mut keep: std:: collections:: HashSet< String> = if only.is_empty ( ) {
821
p.order ( ) .into_iter ( ) .filter ( | j | starts ( j) ) .map ( | j | j.name.clone ( ) ) .collect ( )
822
} else {
823
824
let startable: Vec< & str > = p.order ( ) .into_iter ( ) .filter ( | j | starts ( j) ) .map ( | j | j.name.as_str ( ) ) .collect ( ) ;
825
for name in only {
826
if ! startable.contains ( & name.as_str ( ) ) {
827
let what = if p.jobs.contains_key ( name) {
828
" is not started by hand (no `on { manual }`)"
829
} else {
830
" is not in the pipeline"
831
} ;
832
return Err ( format! ( " job {name:?} {what} ; jobs a manual run can start: {} " , startable.join ( " , " ) ) ) ;
833
}
834
}
835
only.iter ( ) .cloned ( ) .collect ( )
836
} ;
d7ce41ac Andy McCall 1 month ago ↶
837
loop {
838
let needed: Vec< String> = p
839
.order ( )
840
.into_iter ( )
841
.filter ( | j | keep.contains ( & j.name) )
842
.flat_map ( | j | j.depends_on.iter ( ) .cloned ( ) )
843
.filter ( | d | ! keep.contains ( d) )
844
.collect ( ) ;
845
if needed.is_empty ( ) {
846
break ;
847
}
848
keep.extend ( needed) ;
849
}
44a3d2eb Andy McCall 24 days ago ↶
850
let mut jobs: Vec< fdb:: NewJob> = p
42fa9554 Andy McCall 1 month ago ↶
851
.order ( )
852
.into_iter ( )
d7ce41ac Andy McCall 1 month ago ↶
853
.filter ( | j | keep.contains ( & j.name) )
42fa9554 Andy McCall 1 month ago ↶
854
.map ( | j | {
855
let sb = & p.sandboxes[ & j.sandbox] ;
856
fdb:: NewJob {
857
name: j.name.clone ( ) ,
858
depth: depths[ j.name.as_str ( ) ] as i64 ,
859
depends: j.depends_on.clone ( ) ,
860
spec: serde_json:: to_string( j) .unwrap_or_default ( ) ,
861
sandbox: serde_json:: to_string( sb) .unwrap_or_default ( ) ,
862
runtime: sb.runtime.as_str ( ) .into ( ) ,
44a3d2eb Andy McCall 24 days ago ↶
863
need_disk_mib: None ,
5588bb36 Andy McCall 15 days ago ↶
864
reused: None ,
42fa9554 Andy McCall 1 month ago ↶
865
}
866
} )
867
.collect ( ) ;
44a3d2eb Andy McCall 24 days ago ↶
868
869
for j in & mut jobs {
870
j.need_disk_mib = fdb:: learned_disk_need( & app.db, row.id, & j.name) .await.map_err ( | e | e.to_string ( ) ) ? ;
871
}
c889911b Andy McCall 1 month ago ↶
872
if jobs.is_empty ( ) {
873
d7ce41ac Andy McCall 1 month ago ↶
874
return Ok ( None ) ;
5588bb36 Andy McCall 15 days ago ↶
875
}
876
877
878
879
880
881
if trigger = = " push" & & row.flow_reuse {
882
let consumed: std:: collections:: HashSet< & str > = p
883
.order ( )
884
.into_iter ( )
885
.filter ( | j | keep.contains ( & j.name) )
886
.flat_map ( | j | j.inputs.iter ( ) .map ( | i | i.from.as_str ( ) ) )
887
.collect ( ) ;
888
for j in & mut jobs {
889
let Some ( spec) = p.jobs.get ( & j.name) else { continue } ;
890
if ! reusable ( spec) | | consumed.contains ( j.name.as_str ( ) ) {
891
continue ;
892
}
893
j.reused =
894
fdb:: passed_on_state( & app.db, row.id, state, & j.name, & j.spec, & j.sandbox) .await.map_err ( | e | e.to_string ( ) ) ? ;
91c306aa Andy McCall 8 days ago ↶
895
896
42cd46c9 Andy McCall 8 days ago ↶
897
if j.reused.is_none ( ) & & row.forked_from.is_some ( ) {
898
j.reused =
899
fdb:: passed_upstream( & app.db, row, state, & j.name, & j.spec, & j.sandbox) .await.map_err ( | e | e.to_string ( ) ) ? ;
900
}
91c306aa Andy McCall 8 days ago ↶
901
if j.reused.is_none ( ) {
902
j.reused =
903
fdb:: passed_elsewhere( & app.db, row, state, & j.name, & j.spec, & j.sandbox) .await.map_err ( | e | e.to_string ( ) ) ? ;
904
}
06b75635 Andy McCall 8 days ago ↶
905
906
907
if let Some ( ( from, attest) ) = j.reused.as_ref ( ) .and_then ( | r | Some ( ( r.repo.clone ( ) ? , r.receipt_attest.clone ( ) ) ) ) {
908
if let Err ( why) = receipt_holds ( app, & from, & attest) .await {
909
tracing:: info! ( " flow: not reusing {from}'s pass of {} on {state}: {why}" , j.name) ;
910
j.reused = None ;
911
}
912
}
5588bb36 Andy McCall 15 days ago ↶
913
}
d7ce41ac Andy McCall 1 month ago ↶
914
}
42fa9554 Andy McCall 1 month ago ↶
915
let hash = hex:: encode( p.hash ( ) ) ;
1edac4b6 Andy McCall 1 month ago ↶
916
let id =
917
db:: flow:: create_run( & app.db, db:: flow:: NewRun { pipeline_hash: & hash, .. base } , & jobs) .await.map_err ( | e | e.to_string ( ) ) ? ;
918
if let Ok ( Some ( run) ) = fdb:: run_by_id( & app.db, id) .await {
5588bb36 Andy McCall 15 days ago ↶
919
for j in jobs.iter ( ) .filter ( | j | j.reused.is_none ( ) ) {
a0d4bbfb Andy McCall 1 month ago ↶
920
report_check ( app, row, state, run.number, & j.name, " queued" , " waiting for a runner" ) .await;
42fa9554 Andy McCall 1 month ago ↶
921
}
5588bb36 Andy McCall 15 days ago ↶
922
923
924
if jobs.iter ( ) .any ( | j | j.reused.is_some ( ) ) {
925
let reused: Vec< fdb:: Job> =
926
fdb:: jobs( & app.db, run.id) .await.unwrap_or_default ( ) .into_iter ( ) .filter ( | j | j.reused_from ( ) .is_some ( ) ) .collect ( ) ;
927
if let Err ( e) = report_changed ( app, run.id, reused) .await {
928
tracing:: warn! ( " flow: reporting reused passes of run #{}: {e}" , run.number) ;
929
}
930
}
1c8276b9 Andy McCall 24 days ago ↶
931
images_check ( app, row, state, run.number, & jobs) .await;
42fa9554 Andy McCall 1 month ago ↶
932
}
933
id
934
}
935
} ;
1edac4b6 Andy McCall 1 month ago ↶
936
let run = fdb:: run_by_id( & app.db, id) .await.map_err ( | e | e.to_string ( ) ) ? .ok_or ( " the run vanished" ) ? ;
a9f1c0bf Andy McCall 17 days ago ↶
937
if supersedes ( trigger) {
938
supersede ( app, row, bookmark, trigger, run.number) .await;
939
}
1edac4b6 Andy McCall 1 month ago ↶
940
Ok ( Some ( run.number) )
42fa9554 Andy McCall 1 month ago ↶
941
}
942
1c8276b9 Andy McCall 24 days ago ↶
943
944
945
946
947
async fn images_check ( app : & Shared, row : & db:: repos:: RepoRow, state : & str , run_number : i64 , jobs : & [fdb:: NewJob]) {
948
let mut pinned: Vec< String> = jobs
949
.iter ( )
950
.filter_map ( | j | serde_json:: from_str:: < serde_json:: Value> ( & j.sandbox) .ok ( ) )
951
.filter_map ( | v | v[ " rootfs" ] .as_str ( ) .map ( String :: from) )
952
.filter ( | r | ! r.is_empty ( ) )
953
.collect ( ) ;
954
pinned.sort ( ) ;
955
pinned.dedup ( ) ;
956
if pinned.is_empty ( ) {
957
return ;
958
}
959
let mut held = crate :: images:: held( & app.db) .await.unwrap_or_default ( ) ;
960
961
962
let runners: Vec< db:: runners:: Runner> =
963
db:: runners:: list( & app.db) .await.unwrap_or_default ( ) .into_iter ( ) .filter ( | r | r.state ! = " disabled" ) .collect ( ) ;
964
if runners.is_empty ( ) | | runners.iter ( ) .any ( | r | r.image_list ( ) .is_none ( ) ) {
965
return ;
966
}
967
for r in & runners {
968
held.extend ( r.image_list ( ) .unwrap_or_default ( ) ) ;
969
}
970
let missing: Vec< & String> = pinned.iter ( ) .filter ( | h | ! held.contains ( * h) ) .collect ( ) ;
971
972
973
let failing_before = db:: hooks:: checks_for_state( & app.db, row.id, state)
974
.await
975
.unwrap_or_default ( )
976
.iter ( )
977
.any ( | c | c.name = = " pipeline/images" & & c.status ! = " passed" ) ;
978
if missing.is_empty ( ) & & ! failing_before {
979
return ;
980
}
981
let ( status, summary) = if missing.is_empty ( ) {
982
(
983
" passed" ,
984
format! ( " {} now held" , if pinned.len ( ) = = 1 { " its image is" .to_string ( ) } else { format! ( " all {} images are" , pinned.len ( ) ) } ) ,
985
)
986
} else {
987
let names: Vec< String> = missing.iter ( ) .map ( | h | h[ .. 12. min ( h.len ( ) ) ] .to_string ( ) ) .collect ( ) ;
988
( " failed" , format! ( " neither the hub nor a runner holds image {} : send it with flow rootfs push HASH" , names.join ( " , " ) ) )
989
} ;
990
let url = format! ( " {} /{} /{} /flow/{run_number} " , app.config.base_url ( ) , row.owner, row.name) ;
991
if let Ok ( c) = db:: hooks:: upsert_check(
992
& app.db,
993
row.id,
994
db:: hooks:: CheckReport { state, name: " pipeline/images" , status, summary: & summary, url: & url, reporter: None , seal: " " } ,
995
)
996
.await
997
{
998
let api = crate :: api:: check_of( & c) ;
33d98147 Andy McCall 16 days ago
999
app.bus.proposal ( row.id, hub_api:: ProposalEvent { number: 0 , kind: " check" .into ( ) , check: Some ( api.clone ( ) ) } ) ;
1c8276b9 Andy McCall 24 days ago ↶
1000
crate :: hooks:: emit( app, row, " check" , " flow" , serde_json:: json! ( { " check" : api } ) ) .await;
1001
}
1002
}
1003
a0d4bbfb Andy McCall 1 month ago ↶
1004
1005
async fn report_check ( app : & Shared, row : & db:: repos:: RepoRow, state : & str , run_number : i64 , job : & str , status : & str , summary : & str ) {
1006
let url = format! ( " {} /{} /{} /flow/{run_number} " , app.config.base_url ( ) , row.owner, row.name) ;
42fa9554 Andy McCall 1 month ago ↶
1007
let name = format! ( " flow/{job} " ) ;
1008
let c = db:: hooks:: upsert_check(
1009
& app.db,
1010
row.id,
1011
db:: hooks:: CheckReport { state, name: & name, status, summary, url: & url, reporter: None , seal: " " } ,
1012
)
1013
.await;
1014
if let Ok ( c) = c {
1015
let api = crate :: api:: check_of( & c) ;
33d98147 Andy McCall 16 days ago
1016
app.bus.proposal ( row.id, hub_api:: ProposalEvent { number: 0 , kind: " check" .into ( ) , check: Some ( api.clone ( ) ) } ) ;
42fa9554 Andy McCall 1 month ago ↶
1017
crate :: hooks:: emit( app, row, " check" , " flow" , serde_json:: json! ( { " check" : api } ) ) .await;
1018
}
1019
}
1020
1021
1022
fn check_status ( job_status : & str ) -> & 'static str {
1023
match job_status {
1024
" queued" => " queued" ,
1025
" running" => " running" ,
1026
" passed" => " passed" ,
1027
" skipped" => " skipped" ,
745d225d Andy McCall 1 month ago ↶
1028
" cancelled" => " cancelled" ,
42fa9554 Andy McCall 1 month ago ↶
1029
_ => " failed" ,
1030
}
1031
}
1032
b6b7e4cf Andy McCall 1 month ago ↶
1033
1034
1035
92a797b2 Andy McCall 24 days ago ↶
1036
pub ( crate ) async fn runner_auth ( app : & Shared, headers : & HeaderMap) -> Result< db:: runners:: Runner, ApiError> {
42fa9554 Andy McCall 1 month ago ↶
1037
let Some ( t) = oxy_server:: bearer_token( headers) else {
1038
return Err ( ApiError( StatusCode:: UNAUTHORIZED , " a runner token is required" .into ( ) ) ) ;
b6b7e4cf Andy McCall 1 month ago ↶
1039
} ;
1040
let hash = blake3:: hash( t.as_bytes ( ) ) .to_hex ( ) .to_string ( ) ;
1041
let runner = match db:: runners:: by_token( & app.db, & hash) .await? {
1042
Some ( r) => r,
1043
None if app.config.flow.runner_tokens.iter ( ) .any ( | x | crate :: auth:: mac_equal( x, t) ) => {
1044
db:: runners:: configured( & app.db, & hash, " runner" ) .await?
1045
}
1046
None => return Err ( ApiError( StatusCode:: UNAUTHORIZED , " not a runner token" .into ( ) ) ) ,
42fa9554 Andy McCall 1 month ago ↶
1047
} ;
b6b7e4cf Andy McCall 1 month ago ↶
1048
if runner.state = = " disabled" {
1049
return Err ( ApiError( StatusCode:: FORBIDDEN , format! ( " runner {} is disabled on this hub" , runner.name) ) ) ;
42fa9554 Andy McCall 1 month ago ↶
1050
}
b6b7e4cf Andy McCall 1 month ago ↶
1051
Ok ( runner)
42fa9554 Andy McCall 1 month ago ↶
1052
}
1053
b6b7e4cf Andy McCall 1 month ago ↶
1054
1055
1056
1057
pub async fn register (
1058
State( app ) : State< Shared> ,
1059
Json( req ) : Json< flow_core:: api:: Register> ,
1060
) -> Result< Json< flow_core:: api:: Registered> , ApiError> {
1061
let bytes = hex:: decode( req.identity.trim ( ) ) .map_err ( | _| ApiError( StatusCode:: BAD_REQUEST , " identity: not hex" .into ( ) ) ) ? ;
1062
let identity = oxy_core:: identity:: Identity:: decode( & bytes) .map_err ( | e | ApiError( StatusCode:: BAD_REQUEST , format! ( " identity: {e} " ) ) ) ? ;
1063
let name: String = req.name.chars ( ) .filter ( | c | c.is_ascii_alphanumeric ( ) | | matches! ( c, ' -' | ' _' | ' .' ) ) .take ( 64 ) .collect ( ) ;
1064
if name.is_empty ( ) {
1065
return Err ( ApiError( StatusCode:: BAD_REQUEST , " give the runner a name of letters, digits, - _ or ." .into ( ) ) ) ;
1066
}
1067
let credential = format! ( " flowrunner_{} " , crate :: auth:: random_hex( 32 ) ) ;
1068
let hash = blake3:: hash( credential.as_bytes ( ) ) .to_hex ( ) .to_string ( ) ;
1069
let reg = blake3:: hash( req.token.trim ( ) .as_bytes ( ) ) .to_hex ( ) .to_string ( ) ;
1070
let runner =
1071
db:: runners:: register( & app.db, & reg, & name, & hash, & identity.hash ( ) .to_hex ( ) , & identity.name, " " ) .await? .ok_or_else ( | | {
1072
ApiError( StatusCode:: FORBIDDEN , " that registration token is unknown, used or expired; create one on Admin → Runners" .into ( ) )
1073
} ) ? ;
1074
db:: runners:: seen( & app.db, runner.id, None , Some ( & req.info) ) .await? ;
1075
tracing:: info! ( " flow: runner {} registered (key {})" , runner.name, & runner.key[ .. 12 ] ) ;
1076
Ok ( Json( flow_core:: api:: Registered { id: runner.id, name: runner.name, credential } ) )
1077
}
1078
42fa9554 Andy McCall 1 month ago ↶
1079
1080
pub async fn claim ( State( app ) : State< Shared> , headers : HeaderMap, Json( req ) : Json< ClaimRequest> ) -> Result< Response, ApiError> {
b6b7e4cf Andy McCall 1 month ago ↶
1081
let me = runner_auth ( & app, & headers) .await? ;
1082
let claimed: String = req.runner.chars ( ) .filter ( | c | ! c.is_control ( ) ) .take ( 64 ) .collect ( ) ;
1083
db:: runners:: seen( & app.db, me.id, Some ( & claimed) , req.info.as_ref ( ) ) .await? ;
eef677c4 Andy McCall 1 month ago ↶
1084
if let Some ( holding) = & req.holding {
1085
end_jobs_not_held ( & app, & me, holding) .await? ;
1086
}
b6b7e4cf Andy McCall 1 month ago ↶
1087
1088
if me.state = = " draining" {
1089
return Ok ( StatusCode:: NO_CONTENT .into_response ( ) ) ;
1090
}
1091
let runner = if me.source = = " registered" { me.name.clone ( ) } else { claimed } ;
42fa9554 Andy McCall 1 month ago ↶
1092
let secret = format! ( " {JOB_TOKEN_PREFIX} {} " , crate :: auth:: random_hex( 32 ) ) ;
efd29a00 Andy McCall 1 month ago ↶
1093
1094
1095
if let Some ( archs) = & req.image_archs {
1096
fdb:: record_image_archs( & app.db, archs) .await? ;
1097
}
1098
let arch = req.info.as_ref ( ) .map ( | i | i.arch.clone ( ) ) .filter ( | a | ! a.is_empty ( ) ) .unwrap_or_else ( | | me.arch.clone ( ) ) ;
4d2e44d7 Andy McCall 1 month ago ↶
1099
1100
1101
let profile = profile_of ( & req, & arch) ;
92a797b2 Andy McCall 24 days ago ↶
1102
db:: runners:: set_offer( & app.db, me.id, & req.runtimes, req.images.as_deref ( ) , req.fetches_images, & profile) .await? ;
2fb70d01 Andy McCall 24 days ago ↶
1103
if let Some ( f) = & req.facts {
1104
db:: runners:: set_facts( & app.db, me.id, f) .await? ;
1105
}
a46d15e7 Andy McCall 23 days ago ↶
1106
app.swarm.claimed ( me.id, req.swarm.as_ref ( ) ) ;
c2be148c Andy McCall 22 days ago ↶
1107
1108
4fa0da12 Andy McCall 22 days ago ↶
1109
if let Some ( c) = & req.caches {
c2be148c Andy McCall 22 days ago ↶
1110
let kept = | slot : & str | req.cache_times.as_ref ( ) .and_then ( | t | t.get ( slot) ) .map_or ( 0 , | ms | * ms as i64 ) ;
4fa0da12 Andy McCall 22 days ago ↶
1111
CACHES
1112
.lock ( )
1113
.unwrap_or_else ( | p | p.into_inner ( ) )
1114
.get_or_insert_with ( Default :: default)
c2be148c Andy McCall 22 days ago ↶
1115
.insert ( ( app.repos.root ( ) .display ( ) .to_string ( ) , me.id) , c.iter ( ) .map ( | s | ( s.clone ( ) , kept ( s) ) ) .collect ( ) ) ;
4fa0da12 Andy McCall 22 days ago ↶
1116
}
b0612e7d Andy McCall 1 month ago ↶
1117
let room = fdb:: Room {
4d2e44d7 Andy McCall 1 month ago ↶
1118
declined: fdb:: declined_by( & app.db, me.id, & profile) .await? ,
b0612e7d Andy McCall 1 month ago ↶
1119
memory_mib: req.free_memory_mib,
1120
cpus: req.free_cpus,
1121
images: req.images.as_ref ( ) .map ( | i | i.iter ( ) .map ( | h | h.to_ascii_lowercase ( ) ) .collect ( ) ) ,
efd29a00 Andy McCall 1 month ago ↶
1122
arch,
1123
image_archs: fdb:: image_archs( & app.db) .await? ,
951aed50 Andy McCall 9 days ago ↶
1124
1125
fetchable: if req.fetches_images { crate :: images:: reach( & app) .await? .for_runner ( me.id) } else { Default :: default( ) } ,
44a3d2eb Andy McCall 24 days ago ↶
1126
free_disk_mib: req.facts.as_ref ( ) .and_then ( | f | f.free_disk_mib ) ,
b0612e7d Andy McCall 1 month ago ↶
1127
} ;
7f5f6c05 Andy McCall 1 month ago ↶
1128
1129
1130
let mut labels: Vec< String> = match & req.info {
c4c87f48 Andy McCall 1 month ago ↶
1131
Some ( i) => i.labels.clone ( ) ,
7f5f6c05 Andy McCall 1 month ago ↶
1132
None => me.reported_label_list ( ) .into_iter ( ) .map ( String :: from) .collect ( ) ,
c4c87f48 Andy McCall 1 month ago ↶
1133
} ;
7f5f6c05 Andy McCall 1 month ago ↶
1134
for l in me.admin_label_list ( ) {
1135
if ! labels.iter ( ) .any ( | x | x = = l ) {
1136
labels.push ( l.to_string ( ) ) ;
1137
}
1138
}
501d8b26 Andy McCall 24 days ago ↶
1139
1140
1141
let runners = db:: runners:: list( & app.db) .await? ;
1142
let me_now = runners.iter ( ) .find ( | r | r.id = = me.id ) .cloned ( ) .unwrap_or_else ( | | me.clone ( ) ) ;
1143
let running: std:: collections:: HashMap< i64 , i64 > = db:: runners:: running( & app.db) .await? .into_iter ( ) .collect ( ) ;
4fa0da12 Andy McCall 22 days ago ↶
1144
let queue = fdb:: queue( & app.db) .await? ;
1145
let owners: std:: collections:: HashMap< i64 , ( String, i64 ) > = queue.iter ( ) .map ( | q | ( q.job_id, ( q.owner.clone ( ) , q.repo_id) ) ) .collect ( ) ;
1146
1147
let slots: std:: collections:: HashMap< i64 , String> =
1148
queue.iter ( ) .map ( | q | ( q.job_id, flow_core:: api:: cache_slot( & format! ( " {} /{} " , q.owner, q.name) , & q.job) ) ) .collect ( ) ;
951aed50 Andy McCall 9 days ago ↶
1149
let ( archs, reach) = ( fdb:: image_archs( & app.db) .await? , crate :: images:: reach( & app) .await? ) ;
501d8b26 Andy McCall 24 days ago ↶
1150
let now = db:: now_ms( ) ;
951aed50 Andy McCall 9 days ago ↶
1151
let fleet = Fleet { image_archs: & archs, reach: & reach, now } ;
c95446a1 Andy McCall 24 days ago ↶
1152
let hub_key = app.repos.root ( ) .display ( ) .to_string ( ) ;
501d8b26 Andy McCall 24 days ago ↶
1153
let better = | j : & fdb:: Job| -> bool {
1154
let Some ( ( owner, repo_id) ) = owners.get ( & j.id) else { return false } ;
1155
let rootfs = rootfs_of ( j) ;
1156
let mut holds = HELD .lock ( ) .unwrap_or_else ( | p | p.into_inner ( ) ) ;
1157
let holds = holds.get_or_insert_with ( Default :: default) ;
1158
holds.retain ( | _, t | now - * t < 600_000 ) ;
c95446a1 Andy McCall 24 days ago ↶
1159
if holds.get ( & ( hub_key.clone ( ) , j.id) ) .is_some_and ( | t | now - * t > = BETTER_HOLD_MS ) {
501d8b26 Andy McCall 24 days ago ↶
1160
return false ;
1161
}
4fa0da12 Andy McCall 22 days ago ↶
1162
let slot = slots.get ( & j.id) .map ( String :: as_str) .unwrap_or ( " " ) ;
c2be148c Andy McCall 22 days ago ↶
1163
let mine = fit ( & hub_key, & me_now, & rootfs, slot) ;
501d8b26 Andy McCall 24 days ago ↶
1164
let other = runners.iter ( ) .any ( | r | {
1165
r.id ! = me.id
1166
& & r.state = = " active"
1167
& & r.last_seen_ms.is_some_and ( | t | now - t < POLLING_MS )
1168
& & running.get ( & r.id) .copied ( ) .unwrap_or ( 0 ) < r.max_jobs.max ( 1 )
c2be148c Andy McCall 22 days ago ↶
1169
& & fit ( & hub_key, r, & rootfs, slot) .beats ( & mine)
501d8b26 Andy McCall 24 days ago ↶
1170
& & runner_lacks ( j, owner, * repo_id, r, & fleet, & [ ] ) .is_none ( )
1171
} ) ;
1172
if other {
c95446a1 Andy McCall 24 days ago ↶
1173
holds.entry ( ( hub_key.clone ( ) , j.id) ) .or_insert ( now) ;
501d8b26 Andy McCall 24 days ago ↶
1174
}
1175
other
1176
} ;
c4c87f48 Andy McCall 1 month ago ↶
1177
let Some ( job) =
501d8b26 Andy McCall 24 days ago ↶
1178
fdb:: claim_ranked( & app.db, & req.runtimes, & runner, & crate :: auth:: token_hash( & secret) , room, & me.scope ( ) , & labels, & better) .await?
c4c87f48 Andy McCall 1 month ago ↶
1179
else {
42fa9554 Andy McCall 1 month ago ↶
1180
return Ok ( StatusCode:: NO_CONTENT .into_response ( ) ) ;
1181
} ;
1ec29691 Andy McCall 8 days ago ↶
1182
1183
fdb:: set_runner_version( & app.db, job.id, & crate :: runner_updates:: version_of( & me_now.version) ) .await? ;
501d8b26 Andy McCall 24 days ago ↶
1184
c95446a1 Andy McCall 24 days ago ↶
1185
let waited =
1186
HELD .lock ( ) .unwrap_or_else ( | p | p.into_inner ( ) ) .as_mut ( ) .and_then ( | h | h.remove ( & ( hub_key.clone ( ) , job.id) ) ) .map ( | t | now - t ) ;
501d8b26 Andy McCall 24 days ago ↶
1187
let rootfs = rootfs_of ( & job) ;
d7050703 Andy McCall 10 days ago ↶
1188
1189
let mut why = Vec :: new( ) ;
1190
if ! rootfs.is_empty ( ) {
1191
why.push ( if me_now.image_list ( ) .is_some_and ( | have | have.contains ( & rootfs) ) {
1192
" holds the image" .to_string ( )
1193
} else {
1194
" fetches the image from the hub" .to_string ( )
1195
} ) ;
1196
}
c2be148c Andy McCall 22 days ago ↶
1197
if let Some ( kept) = slots.get ( & job.id) .and_then ( | slot | cache_kept ( & hub_key, me.id, slot) ) {
1198
why.insert (
1199
0 ,
1200
if kept > 0 { format! ( " holds its build cache (kept {} ago)" , ago ( now - kept) ) } else { " holds its build cache" .into ( ) } ,
1201
) ;
4fa0da12 Andy McCall 22 days ago ↶
1202
}
501d8b26 Andy McCall 24 days ago ↶
1203
match running.get ( & me.id) .copied ( ) .unwrap_or ( 0 ) {
1204
0 => why.push ( " idle" .into ( ) ) ,
1205
n => why.push ( format! ( " running {n} other job{} " , if n = = 1 { " " } else { " s" } ) ) ,
1206
}
1207
if let Some ( sp) = me_now.speed {
1208
why.push ( format! ( " speed {sp} (class {} )" , me_now.speed_class.unwrap_or ( 0 ) ) ) ;
1209
}
1210
if let Some ( w) = waited {
1211
why.push ( format! ( " after {} s held for a better runner" , w / 1000 ) ) ;
1212
}
1213
fdb:: set_placed( & app.db, job.id, & why.join ( " , " ) ) .await? ;
b6b7e4cf Andy McCall 1 month ago ↶
1214
db:: runners:: set_job_runner( & app.db, job.id, me.id) .await? ;
42fa9554 Andy McCall 1 month ago ↶
1215
let run = fdb:: run_by_id( & app.db, job.run_id) .await? .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " no such run" .into ( ) ) ) ? ;
1216
let row =
1217
db:: repos:: get_by_id( & app.db, run.repo_id) .await? .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " no such repository" .into ( ) ) ) ? ;
a0d4bbfb Andy McCall 1 month ago ↶
1218
report_check ( & app, & row, & run.state, run.number, & job.name, " running" , & format! ( " running on {runner} " ) ) .await;
42fa9554 Andy McCall 1 month ago ↶
1219
let bad = | e : serde_json:: Error| ApiError( StatusCode:: INTERNAL_SERVER_ERROR , format! ( " stored job: {e} " ) ) ;
5dead1f4 Andy McCall 1 month ago ↶
1220
1221
1222
let limits = limits ( & app) .await;
1223
let spec: flow_core:: pipeline:: Job = serde_json:: from_str( & job.spec) .map_err ( bad) ? ;
1224
let sandbox: flow_core:: pipeline:: Sandbox = serde_json:: from_str( & job.sandbox) .map_err ( bad) ? ;
7f5f6c05 Andy McCall 1 month ago ↶
1225
let mut timeout_s = spec.timeout_s.or ( sandbox.timeout_s) .unwrap_or ( limits.default_s) .min ( limits.max_s) .max ( 60 ) ;
1226
1227
if let Some ( m) = me.timeout_minutes.filter ( | m | * m > 0 ) {
1228
timeout_s = timeout_s.min ( m as u64 * 60 ) .max ( 60 ) ;
1229
}
5dead1f4 Andy McCall 1 month ago ↶
1230
fdb:: set_job_timeout( & app.db, job.id, timeout_s) .await? ;
42fa9554 Andy McCall 1 month ago ↶
1231
let claim = Claim {
1232
job_id: job.id,
1233
run_id: run.id,
1234
repo: format! ( " {} /{} " , row.owner, row.name) ,
1235
clone_url: format! ( " {} /{} /{} " , app.config.base_url ( ) , row.owner, row.name) ,
1236
token: secret,
1237
heads: run.head_list ( ) .into_iter ( ) .map ( String :: from) .collect ( ) ,
1238
pipeline_hash: run.pipeline_hash.clone ( ) ,
5dead1f4 Andy McCall 1 month ago ↶
1239
job: spec,
1240
sandbox,
1241
timeout_s,
fd3db644 Andy McCall 1 month ago ↶
1242
subrepos: vec! [ ] ,
9a065bc0 Andy McCall 1 month ago ↶
1243
state: run.state.clone ( ) ,
1244
bookmark: run.bookmark.clone ( ) ,
1245
run_number: run.number,
2a83f530 Andy McCall 1 month ago ↶
1246
1247
env: crate :: variables:: env_for_repo( & app.db, & row) .await? ,
4ac01a73 Andy McCall 1 month ago ↶
1248
secrets: vec! [ ] ,
cd437c4f Andy McCall 20 days ago ↶
1249
inputs: vec! [ ] ,
3b650652 Andy McCall 10 days ago ↶
1250
1251
1252
expected_ms: fdb:: expected_ms( & app.db, row.id, & job.name) .await? .map ( | ms | ms as u64 ) ,
e4203e14 Andy McCall 10 days ago ↶
1253
1254
checkpoint: if job.handbacks > 0 { fdb:: checkpoint( & app.db, job.id) .await? .map ( | a | a.blob ) } else { None } ,
42fa9554 Andy McCall 1 month ago ↶
1255
} ;
fd3db644 Andy McCall 1 month ago ↶
1256
let mut claim = claim;
4ac01a73 Andy McCall 1 month ago ↶
1257
let ( inputs, ids, mut left_out) = subrepo_inputs ( & app, & row, & run) .await;
1258
1259
1260
let names: Vec< String> = claim.job.secrets.iter ( ) .map ( | s | s.name.clone ( ) ) .collect ( ) ;
1261
let found = crate :: secrets:: for_job( & app, & row, & names) .await? ;
d1727c57 Andy McCall 1 month ago ↶
1262
b644e175 Andy McCall 20 days ago ↶
1263
1264
1265
let environment = claim.job.environment.clone ( ) .filter ( | _| job.approval = = " approved" ) ;
1266
if let ( Some ( e) , None , false ) = ( & claim.job.environment, & environment, claim.job.identities.is_empty ( ) ) {
1267
left_out.push ( format! (
1268
" environment {e} : not in the job's identity tokens, because the job was not approved (give it an approval block)"
1269
) ) ;
1270
}
d1727c57 Andy McCall 1 month ago ↶
1271
if ! claim.job.identities.is_empty ( ) {
1272
let issuer = app.oidc ( ) .await.map_err ( | e | ApiError( StatusCode:: INTERNAL_SERVER_ERROR , format! ( " oidc: {e} " ) ) ) ? ;
1273
for i in & claim.job.identities {
1274
let token = issuer.mint ( & crate :: oidc:: JobClaims {
1275
issuer: app.config.base_url ( ) ,
1276
audience: & i.audience,
1277
owner: & row.owner,
1278
name: & row.name,
1279
visibility: & row.visibility,
1280
bookmark: & run.bookmark,
1281
run_number: run.number,
1282
job: & job.name,
1283
state: & run.state,
1284
runner: & runner,
1285
actor: & run.actor,
b644e175 Andy McCall 20 days ago ↶
1286
environment: environment.as_deref ( ) ,
47e37402 Andy McCall 20 days ago ↶
1287
approved_by: ( job.approval = = " approved" ) .then_some ( job.approved_by.as_str ( ) ) ,
1288
approval: ( ! job.approval_seal.is_empty ( ) ) .then_some ( job.approval_seal.as_str ( ) ) ,
d1727c57 Andy McCall 1 month ago ↶
1289
now_ms: db:: now_ms( ) ,
1290
ttl_s: timeout_s,
1291
} ) ;
1292
claim.secrets.push ( flow_core:: api:: SecretInput {
1293
name: format! ( " identity:{} " , i.audience) ,
1294
path: i.mount_path.clone ( ) ,
1295
value: token,
1296
} ) ;
1297
}
1298
}
4ac01a73 Andy McCall 1 month ago ↶
1299
for s in & claim.job.secrets {
1300
match found.iter ( ) .find ( | ( n , _) | * n = = s.name) {
1301
Some ( ( name, value) ) => {
1302
claim.secrets.push ( flow_core:: api:: SecretInput { name: name.clone ( ) , path: s.mount_path.clone ( ) , value: value.clone ( ) } )
1303
}
1304
None => left_out.push ( format! (
1305
" secret {:?} is not set for this repository, its owner or the site; {} will be missing" ,
1306
s.name, s.mount_path
1307
) ) ,
1308
}
1309
}
fd3db644 Andy McCall 1 month ago ↶
1310
if ! ids.is_empty ( ) {
1311
fdb:: set_also_read( & app.db, job.id, & ids) .await? ;
1312
}
afd0be38 Andy McCall 1 month ago ↶
1313
1314
1315
if ! left_out.is_empty ( ) {
1316
let records: Vec< flow_core:: api:: Record> =
1317
left_out.into_iter ( ) .map ( | text | flow_core:: api:: Record { t: 0 , stream: " flow" .into ( ) , text } ) .collect ( ) ;
1318
fdb:: append_log( & app.db, job.id, & records, MAX_RECORDS as i64 ) .await? ;
1319
}
fd3db644 Andy McCall 1 month ago ↶
1320
claim.subrepos = inputs;
cd437c4f Andy McCall 20 days ago ↶
1321
1322
1323
match artifact_inputs ( & app, & job, & claim.job) .await {
1324
Ok ( found) => claim.inputs = found,
1325
Err ( why) => {
1326
let error = format! ( " inputs: {why} " ) ;
1327
fdb:: finish( & app.db, job.id, fdb:: Outcome { status: " error" , exit_code: None , error: & error, receipt: " " , receipt_hash: " " } )
1328
.await? ;
1329
return Err ( ApiError( StatusCode:: CONFLICT , error) ) ;
1330
}
1331
}
42fa9554 Andy McCall 1 month ago ↶
1332
Ok ( Json( claim) .into_response ( ) )
cd437c4f Andy McCall 20 days ago ↶
1333
}
1334
1335
1336
1337
1338
async fn artifact_inputs ( app : & Shared, job : & fdb:: Job, spec : & flow_core:: Job) -> Result< Vec< flow_core:: api:: ArtifactInput> , String> {
1339
if spec.inputs.is_empty ( ) {
1340
return Ok ( vec! [ ] ) ;
1341
}
1342
let jobs = fdb:: jobs( & app.db, job.run_id) .await.map_err ( | e | e.to_string ( ) ) ? ;
1343
let mut out = Vec :: new( ) ;
1344
for i in & spec.inputs {
1345
let Some ( producer) = jobs.iter ( ) .find ( | j | j.name = = i.from ) else {
1346
return Err ( format! ( " {:?} is not a job of this run" , i.from) ) ;
1347
} ;
1348
if producer.status ! = " passed" | | producer.receipt_hash.is_empty ( ) {
1349
return Err ( format! ( " {} has not passed with a receipt, so {} cannot be taken from it" , i.from, i.artifact) ) ;
1350
}
1351
let artifacts = fdb:: artifacts( & app.db, producer.id) .await.map_err ( | e | e.to_string ( ) ) ? ;
1352
let Some ( a) = artifacts.iter ( ) .find ( | a | a.name = = i.artifact ) else {
1353
return Err ( format! ( " {} kept no artifact {} " , i.from, i.artifact) ) ;
1354
} ;
1355
out.push ( flow_core:: api:: ArtifactInput {
1356
artifact: i.artifact.clone ( ) ,
1357
from: i.from.clone ( ) ,
1358
path: i.path.clone ( ) ,
1359
blob: a.blob.clone ( ) ,
1360
} ) ;
1361
}
1362
Ok ( out)
1363
}
1364
1365
1366
1367
pub async fn input ( State( app ) : State< Shared> , headers : HeaderMap, Path( (id , index ) ) : Path< ( i64 , usize ) > ) -> Result< Response, ApiError> {
1368
let runner = runner_auth ( & app, & headers) .await? ;
1369
let job = running_job ( & app, id) .await? ;
1370
1371
1372
if runner.source = = " registered" & & job.runner ! = runner.name {
1373
return Err ( ApiError( StatusCode:: FORBIDDEN , " this job is held by another runner" .into ( ) ) ) ;
1374
}
1375
let spec: flow_core:: Job =
1376
serde_json:: from_str( & job.spec) .map_err ( | e | ApiError( StatusCode:: INTERNAL_SERVER_ERROR , format! ( " stored job: {e} " ) ) ) ? ;
1377
let found = artifact_inputs ( & app, & job, & spec) .await.map_err ( | why | ApiError( StatusCode:: CONFLICT , why) ) ? ;
1378
let Some ( i) = found.get ( index) else {
1379
return Err ( ApiError( StatusCode:: NOT_FOUND , format! ( " job {} has no input {index} " , job.name) ) ) ;
1380
} ;
1381
let run = fdb:: run_by_id( & app.db, job.run_id) .await? .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " no such run" .into ( ) ) ) ? ;
1382
let row =
1383
db:: repos:: get_by_id( & app.db, run.repo_id) .await? .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " no such repository" .into ( ) ) ) ? ;
1384
let repo = RepoName:: new( & row.owner, & row.name) .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " bad repository name" .into ( ) ) ) ? ;
1385
let blob = oxy_core:: hash:: ObjectRef:: from_hex( & i.blob)
1386
.ok_or_else ( | | ApiError( StatusCode:: INTERNAL_SERVER_ERROR , " a kept artifact has a bad blob" .into ( ) ) ) ? ;
1387
let bytes = app
1388
.repos
1389
.read ( & repo, move | r, _ | Ok ( r.read_blob ( & blob) ? ) )
1390
.await
1391
.map_err ( | e | ApiError( StatusCode:: INTERNAL_SERVER_ERROR , format! ( " read the artifact: {e} " ) ) ) ? ;
1392
Ok ( ( [ ( axum:: http:: header:: CONTENT_TYPE , " application/octet-stream" ) ] , bytes) .into_response ( ) )
fd3db644 Andy McCall 1 month ago ↶
1393
}
1394
4d2e44d7 Andy McCall 1 month ago ↶
1395
1396
1397
fn profile_of ( req : & ClaimRequest, arch : & str ) -> String {
1398
let sorted = | v : & [String]| {
1399
let mut v = v.to_vec ( ) ;
1400
v.sort ( ) ;
1401
v
1402
} ;
1403
let info = req.info.as_ref ( ) ;
1404
let v = serde_json:: json! ( {
1405
" arch" : arch,
1406
" tiers" : sorted ( & req.runtimes) ,
1407
" images" : req.images.as_deref ( ) .map ( sorted) ,
1408
" labels" : info.map ( | i | sorted ( & i.labels) ) ,
1409
" memory_mib" : info.and_then ( | i | i.memory_mib ) ,
1410
" cpus" : info.and_then ( | i | i.cpus ) ,
1411
} ) ;
1412
blake3:: hash( v.to_string ( ) .as_bytes ( ) ) .to_hex ( ) .to_string ( )
1413
}
1414
1415
1416
pub const UNPLACEABLE_AFTER : usize = 3 ;
1417
ab4df6aa Andy McCall 10 days ago ↶
1418
1419
1420
1421
pub const LEASE_MS : i64 = 3 * 60_000 ;
1422
1423
1424
1425
pub const MAX_HANDBACKS : i64 = 5 ;
1426
1427
1428
1429
1430
1431
pub async fn hand_back_job ( app : & Shared, id : i64 , runner : Option< i64 > , why : & str ) -> sqlx:: Result< bool > {
1432
let Some ( before) = fdb:: job( & app.db, id) .await? else { return Ok ( false ) } ;
1433
let Some ( n) = fdb:: hand_back( & app.db, id, runner) .await? else { return Ok ( false ) } ;
1434
let t = before.started_ms.map ( | s | ( db:: now_ms( ) - s) .max ( 0 ) as u64 ) .unwrap_or ( 0 ) ;
1435
let record = flow_core:: api:: Record { t, stream: " flow" .into ( ) , text: format! ( " ── {why} ; queued again ahead of new work" ) } ;
1436
fdb:: append_log( & app.db, id, & [ record] , MAX_RECORDS as i64 ) .await.ok ( ) ;
1437
tracing:: info! ( " flow: job {id} handed back ({n}): {why}" ) ;
1438
if n > MAX_HANDBACKS {
1439
let error = format! ( " handed back {n} times; the last: {why} " ) ;
1440
let changed =
1441
fdb:: finish( & app.db, id, fdb:: Outcome { status: " error" , exit_code: None , error: & error, receipt: " " , receipt_hash: " " } )
1442
.await? ;
1443
report_changed ( app, before.run_id, changed) .await? ;
1444
} else if let Some ( run) = fdb:: run_by_id( & app.db, before.run_id) .await? {
1445
if let Some ( row) = db:: repos:: get_by_id( & app.db, run.repo_id) .await? {
1446
let summary = format! ( " {why} ; queued again ahead of new work" ) ;
1447
report_check ( app, & row, & run.state, run.number, & before.name, " queued" , & summary) .await;
1448
}
1449
}
1450
Ok ( true )
1451
}
1452
1453
1454
1455
pub async fn requeue_lapsed ( app : & Shared) -> sqlx:: Result< usize > {
1456
let lapsed = fdb:: running_on_silent_runners( & app.db, db:: now_ms( ) - LEASE_MS ) .await? ;
1457
let mut n = 0 ;
1458
for ( job, runner) in lapsed {
1459
let why = format! ( " the runner {runner} has not been heard from for {} minutes" , LEASE_MS / 60_000 ) ;
1460
if hand_back_job ( app, job.id, None , & why) .await? {
1461
n + = 1 ;
1462
}
1463
}
1464
Ok ( n)
1465
}
1466
e4203e14 Andy McCall 10 days ago ↶
1467
1468
1469
1470
pub async fn save_checkpoint (
1471
State( app ) : State< Shared> ,
1472
headers : HeaderMap,
1473
Path( id ) : Path< i64 > ,
1474
body : axum:: body:: Bytes,
1475
) -> Result< Response, ApiError> {
1476
let runner = runner_auth ( & app, & headers) .await? ;
1477
let job = running_job ( & app, id) .await? ;
1478
if runner.source = = " registered" & & job.runner ! = runner.name {
1479
return Err ( ApiError( StatusCode:: FORBIDDEN , " this job is held by another runner" .into ( ) ) ) ;
1480
}
1481
let spec: flow_core:: Job =
1482
serde_json:: from_str( & job.spec) .map_err ( | e | ApiError( StatusCode:: INTERNAL_SERVER_ERROR , format! ( " stored job: {e} " ) ) ) ? ;
1483
if spec.checkpoints.is_empty ( ) {
1484
return Err ( ApiError( StatusCode:: BAD_REQUEST , format! ( " job {} declares no checkpoint" , job.name) ) ) ;
1485
}
1486
let repo = job_repo ( & app, & job) .await? ;
1487
let content = blake3:: hash( & body) .to_hex ( ) .to_string ( ) ;
1488
let size = body.len ( ) as i64 ;
1489
let blob = app
1490
.repos
1491
.write ( & repo, move | r, _ | Ok ( r.store_blob ( & body) ? ) )
1492
.await
1493
.map_err ( | e | ApiError( StatusCode:: INTERNAL_SERVER_ERROR , format! ( " keep the checkpoint: {e} " ) ) ) ? ;
1494
let a = fdb:: Artifact {
1495
job_id: job.id,
1496
name: fdb:: CHECKPOINT .into ( ) ,
1497
path: spec.checkpoints.join ( " " ) ,
1498
blob: blob.to_hex ( ) ,
1499
size,
1500
content,
1501
created_ms: db:: now_ms( ) ,
1502
} ;
1503
fdb:: add_artifact( & app.db, & a) .await? ;
1504
Ok ( Json( serde_json:: json! ( { " blob" : a.blob } ) ) .into_response ( ) )
1505
}
1506
1507
1508
1509
pub async fn checkpoint ( State( app ) : State< Shared> , headers : HeaderMap, Path( id ) : Path< i64 > ) -> Result< Response, ApiError> {
1510
let runner = runner_auth ( & app, & headers) .await? ;
1511
let job = running_job ( & app, id) .await? ;
1512
if runner.source = = " registered" & & job.runner ! = runner.name {
1513
return Err ( ApiError( StatusCode:: FORBIDDEN , " this job is held by another runner" .into ( ) ) ) ;
1514
}
1515
let a = fdb:: checkpoint( & app.db, job.id) .await? .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " this job has no checkpoint" .into ( ) ) ) ? ;
1516
let repo = job_repo ( & app, & job) .await? ;
1517
let blob = oxy_core:: hash:: ObjectRef:: from_hex( & a.blob)
1518
.ok_or_else ( | | ApiError( StatusCode:: INTERNAL_SERVER_ERROR , " a kept checkpoint has a bad blob" .into ( ) ) ) ? ;
1519
let bytes = app
1520
.repos
1521
.read ( & repo, move | r, _ | Ok ( r.read_blob ( & blob) ? ) )
1522
.await
1523
.map_err ( | e | ApiError( StatusCode:: INTERNAL_SERVER_ERROR , format! ( " read the checkpoint: {e} " ) ) ) ? ;
1524
Ok ( ( [ ( axum:: http:: header:: CONTENT_TYPE , " application/octet-stream" ) ] , bytes) .into_response ( ) )
1525
}
1526
1527
1528
async fn job_repo ( app : & Shared, job : & fdb:: Job) -> Result< RepoName, ApiError> {
1529
let run = fdb:: run_by_id( & app.db, job.run_id) .await? .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " no such run" .into ( ) ) ) ? ;
1530
let row =
1531
db:: repos:: get_by_id( & app.db, run.repo_id) .await? .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " no such repository" .into ( ) ) ) ? ;
1532
RepoName:: new( & row.owner, & row.name) .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " bad repository name" .into ( ) ) )
1533
}
1534
ab4df6aa Andy McCall 10 days ago ↶
1535
1536
1537
1538
1539
pub async fn release (
1540
State( app ) : State< Shared> ,
1541
headers : HeaderMap,
1542
Path( id ) : Path< i64 > ,
1543
Json( d ) : Json< flow_core:: api:: Decline> ,
1544
) -> Result< StatusCode, ApiError> {
1545
let me = runner_auth ( & app, & headers) .await? ;
1546
db:: runners:: seen( & app.db, me.id, None , None ) .await? ;
1547
let reason: String = d.reason.chars ( ) .map ( | c | if c.is_control ( ) { ' ' } else { c } ) .take ( 500 ) .collect ( ) ;
1548
let why = match reason.trim ( ) {
1549
" " => format! ( " handed back by {} " , me.name) ,
1550
r => format! ( " handed back by {} : {r} " , me.name) ,
1551
} ;
1552
if ! hand_back_job ( & app, id, Some ( me.id) , & why) .await? {
1553
return Err ( ApiError( StatusCode:: CONFLICT , " that job is not running on this runner" .into ( ) ) ) ;
1554
}
1555
Ok ( StatusCode:: NO_CONTENT )
1556
}
1557
4d2e44d7 Andy McCall 1 month ago ↶
1558
1559
1560
1561
1562
1563
e9b9619c Andy McCall 19 days ago ↶
1564
1565
1566
pub async fn heartbeat ( State( app ) : State< Shared> , headers : HeaderMap) -> Result< StatusCode, ApiError> {
1567
let me = runner_auth ( & app, & headers) .await? ;
1568
db:: runners:: seen( & app.db, me.id, None , None ) .await? ;
1569
Ok ( StatusCode:: NO_CONTENT )
1570
}
1571
8501fd58 Andy McCall 10 days ago ↶
1572
1573
1574
pub async fn runner_update ( State( app ) : State< Shared> , headers : HeaderMap) -> Result< Json< flow_core:: api:: UpdateAnswer> , ApiError> {
1575
let me = runner_auth ( & app, & headers) .await? ;
1576
db:: runners:: seen( & app.db, me.id, None , None ) .await? ;
1ec29691 Andy McCall 8 days ago ↶
1577
let levels = crate :: runner_updates:: Levels:: load( & app.db) .await? ;
1578
let policy = levels.resolve ( & me) ;
8501fd58 Andy McCall 10 days ago ↶
1579
let hour = time:: OffsetDateTime:: now_utc( ) .hour ( ) ;
1ec29691 Andy McCall 8 days ago ↶
1580
let ( offer, note) = crate :: runner_updates:: offer( & app.db, & levels, & me, & policy, hour, db:: now_ms( ) , true ) .await? ;
c26267b3 Andy McCall 7 days ago ↶
1581
if let Some ( o) = & offer {
1582
crate :: runner_updates:: share( & app, o) ;
1583
}
8501fd58 Andy McCall 10 days ago ↶
1584
Ok ( Json( flow_core:: api:: UpdateAnswer { policy, offer, note } ) )
1585
}
1586
1587
1588
1589
1590
pub async fn report_update (
1591
State( app ) : State< Shared> ,
1592
headers : HeaderMap,
1593
Json( r ) : Json< flow_core:: api:: UpdateReport> ,
1594
) -> Result< StatusCode, ApiError> {
1595
let me = runner_auth ( & app, & headers) .await? ;
1596
let note: String = r.note.chars ( ) .map ( | c | if c.is_control ( ) { ' ' } else { c } ) .take ( 500 ) .collect ( ) ;
1597
let note = note.trim ( ) ;
1598
if note.is_empty ( ) {
1599
return Err ( ApiError( StatusCode:: BAD_REQUEST , " say what happened (note)" .into ( ) ) ) ;
1600
}
1601
db:: runner_updates:: report( & app.db, me.id, note) .await? ;
faf5246c Andy McCall 10 days ago ↶
1602
1603
if let Some ( v) = crate :: runner_updates:: rolled_back_version( note) {
1604
let why = note.split_once ( " : " ) .map ( | ( _, w ) | w ) .unwrap_or ( " rolled back" ) ;
1ec29691 Andy McCall 8 days ago ↶
1605
db:: runner_updates:: hold( & app.db, & v, & me.name, & format! ( " it rolled back on {} ({why} )" , me.name) ) .await? ;
1606
db:: accounts:: audit( & app.db, None , " runner.update.held" , & v, & format! ( " it rolled back on {} : {why} " , me.name) ) .await.ok ( ) ;
faf5246c Andy McCall 10 days ago ↶
1607
}
8501fd58 Andy McCall 10 days ago ↶
1608
db:: accounts:: audit( & app.db, None , " runner.update" , & me.name, note) .await.ok ( ) ;
1609
Ok ( StatusCode:: NO_CONTENT )
1610
}
1611
4d2e44d7 Andy McCall 1 month ago ↶
1612
pub async fn decline (
1613
State( app ) : State< Shared> ,
1614
headers : HeaderMap,
1615
Path( id ) : Path< i64 > ,
1616
Json( d ) : Json< flow_core:: api:: Decline> ,
1617
) -> Result< StatusCode, ApiError> {
1618
let me = runner_auth ( & app, & headers) .await? ;
1619
db:: runners:: seen( & app.db, me.id, None , None ) .await? ;
1620
let reason: String = d.reason.chars ( ) .map ( | c | if c.is_control ( ) { ' ' } else { c } ) .take ( 500 ) .collect ( ) ;
1621
let reason = match reason.trim ( ) {
1622
" " => " no reason given" .to_string ( ) ,
1623
r => r.to_string ( ) ,
1624
} ;
1625
let Some ( all) = fdb:: decline( & app.db, id, me.id, & me.name, & me.profile, & reason) .await? else {
1626
return Err ( ApiError( StatusCode:: CONFLICT , " that job is not running on this runner" .into ( ) ) ) ;
1627
} ;
1628
tracing:: info! ( " flow: runner {} declined job {id}: {reason}" , me.name) ;
1629
let job = fdb:: job( & app.db, id) .await? .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " no such job" .into ( ) ) ) ? ;
1630
let runners: std:: collections:: BTreeSet< i64 > = all.iter ( ) .map ( | d | d.runner_id ) .collect ( ) ;
1631
if runners.len ( ) > = UNPLACEABLE_AFTER {
1632
let error = format! (
1633
" unplaceable: declined by {} runners: {} " ,
1634
runners.len ( ) ,
1635
all.iter ( ) .map ( | d | format! ( " {} : {} " , d.runner, d.reason) ) .collect:: < Vec< _ > > ( ) .join ( " ; " )
1636
) ;
1637
let changed =
1638
fdb:: finish( & app.db, id, fdb:: Outcome { status: " error" , exit_code: None , error: & error, receipt: " " , receipt_hash: " " } )
1639
.await? ;
1640
report_changed ( & app, job.run_id, changed) .await? ;
1641
} else if let Some ( run) = fdb:: run_by_id( & app.db, job.run_id) .await? {
1642
if let Some ( row) = db:: repos:: get_by_id( & app.db, run.repo_id) .await? {
1643
let summary = format! ( " declined by {} : {reason} ; waiting for another runner" , me.name) ;
1644
report_check ( & app, & row, & run.state, run.number, & job.name, " queued" , & summary) .await;
1645
fdb:: set_waiting( & app.db, id, & summary) .await? ;
1646
}
1647
}
1648
Ok ( StatusCode:: NO_CONTENT )
501d8b26 Andy McCall 24 days ago ↶
1649
}
1650
1651
1652
1653
1654
1655
1656
1657
pub struct Fleet < 'a > {
1658
pub image_archs : & 'a std:: collections:: HashMap< String, String> ,
951aed50 Andy McCall 9 days ago ↶
1659
1660
pub reach : & 'a crate:: images:: Reach,
501d8b26 Andy McCall 24 days ago ↶
1661
pub now : i64 ,
1662
}
1663
1664
1665
1666
1667
pub fn runner_lacks (
1668
job : & fdb:: Job,
1669
owner : & str ,
1670
repo_id : i64 ,
1671
r : & db:: runners:: Runner,
1672
fleet : & Fleet,
1673
declines : & [fdb:: DeclineRow],
1674
) -> Option< String> {
1675
let sandbox: serde_json:: Value = serde_json:: from_str( & job.sandbox) .unwrap_or_default ( ) ;
1676
let rootfs = sandbox[ " rootfs" ] .as_str ( ) .unwrap_or ( " " ) ;
1677
let wanted_arch = job.arch ( ) .or_else ( | | fleet.image_archs.get ( rootfs) .cloned ( ) ) ;
1678
let need = | k : & str | sandbox[ k] .as_u64 ( ) .unwrap_or ( 0 ) ;
1679
if r.state = = " draining" {
1680
return Some ( " is draining" .into ( ) ) ;
1681
}
1682
let scope = r.scope ( ) ;
1683
if ( ! scope.owner.is_empty ( ) & & scope.owner ! = owner) | | scope.repo_id.is_some_and ( | id | id ! = repo_id ) {
1684
return Some ( " serves other repositories" .into ( ) ) ;
1685
}
1686
let tiers = r.tier_list ( ) ;
1687
if ! tiers.is_empty ( ) & & ! tiers.contains ( & job.runtime.as_str ( ) ) {
1688
return Some ( format! ( " has no {} tier (it offers {} )" , job.runtime, tiers.join ( " , " ) ) ) ;
1689
}
1690
if let Some ( w) = wanted_arch.as_deref ( ) .filter ( | w | ! r.arch.is_empty ( ) & & * w ! = r.arch) {
1691
return Some ( format! ( " is {} ; the job needs {w} " , r.arch) ) ;
1692
}
56b8dbe6 Andy McCall 10 days ago ↶
1693
951aed50 Andy McCall 9 days ago ↶
1694
let fetches = r.fetches_images & & fleet.reach.fetchable ( r.id, rootfs) ;
56b8dbe6 Andy McCall 10 days ago ↶
1695
if ! rootfs.is_empty ( ) & & ! fetches & & r.image_list ( ) .is_some_and ( | have | ! have.iter ( ) .any ( | h | h = = rootfs ) ) {
951aed50 Andy McCall 9 days ago ↶
1696
let why = if ! r.fetches_images {
1697
" "
1698
} else if fleet.reach.peer_only ( rootfs) {
1699
" (only runners hold it, none in this runner's swarm site: send it to the hub with flow rootfs push)"
1700
} else {
1701
" (nor does the hub: send it with flow rootfs push)"
1702
} ;
501d8b26 Andy McCall 24 days ago ↶
1703
return Some ( format! ( " does not hold image {} {why} " , & rootfs[ .. 12. min ( rootfs.len ( ) ) ] ) ) ;
1704
}
1705
let labels = r.label_list ( ) ;
1706
if let Some ( l) = job.needs ( ) .iter ( ) .find ( | l | ! labels.contains ( & l.as_str ( ) ) ) {
1707
return Some ( format! ( " lacks label {l} " ) ) ;
1708
}
1709
if let Some ( m) = r.memory_mib.filter ( | m | ( * m as u64 ) < need ( " memory_mib" ) ) {
1710
return Some ( format! ( " has {m} MiB; the job needs {} MiB" , need ( " memory_mib" ) ) ) ;
1711
}
1712
if let Some ( c) = r.cpus.filter ( | c | ( * c as u64 ) < need ( " cpus" ) ) {
1713
return Some ( format! ( " has {c} CPUs; the job needs {} " , need ( " cpus" ) ) ) ;
1714
}
1715
1716
if let ( Some ( n) , Some ( free) ) = ( job.need_disk_mib, r.free_disk_mib) {
1717
if free < n {
1718
return Some ( format! (
1719
" has {} free; the job needs ~{} (its recent runs took up to {} )" ,
1720
mib ( free) ,
1721
mib ( n) ,
1722
mib ( ( n - 64 ) * 4 / 5 )
1723
) ) ;
1724
}
1725
}
1726
if let Some ( d) = declines.iter ( ) .find ( | d | d.runner_id = = r.id & & fleet.now - d.ms < fdb:: DECLINE_HOLDS_MS ) {
1727
return Some ( format! ( " declined it: {} " , d.reason) ) ;
1728
}
1729
None
4d2e44d7 Andy McCall 1 month ago ↶
1730
}
1731
1732
1733
1734
1735
92a797b2 Andy McCall 24 days ago ↶
1736
# [ allow ( clippy::too_many_arguments ) ]
4d2e44d7 Andy McCall 1 month ago ↶
1737
pub fn why_waiting (
1738
job : & fdb:: Job,
1739
owner : & str ,
1740
repo_id : i64 ,
1741
runners : & [db:: runners:: Runner],
1742
image_archs : & std:: collections:: HashMap< String, String> ,
1743
declines : & [fdb:: DeclineRow],
1744
now : i64 ,
951aed50 Andy McCall 9 days ago ↶
1745
reach : & crate:: images:: Reach,
4d2e44d7 Andy McCall 1 month ago ↶
1746
) -> String {
951aed50 Andy McCall 9 days ago ↶
1747
let fleet = Fleet { image_archs, reach, now } ;
4d2e44d7 Andy McCall 1 month ago ↶
1748
let online: Vec< & db:: runners:: Runner> = runners.iter ( ) .filter ( | r | r.state ! = " disabled" & & r.online ( ) ) .collect ( ) ;
1749
if online.is_empty ( ) {
1750
return " no runner is online" .into ( ) ;
1751
}
1752
let mut why = Vec :: new( ) ;
1753
for r in & online {
501d8b26 Andy McCall 24 days ago ↶
1754
match runner_lacks ( job, owner, repo_id, r, & fleet, declines) {
4d2e44d7 Andy McCall 1 month ago ↶
1755
None => return String :: new( ) ,
1756
Some ( w) => why.push ( format! ( " {} {w} " , r.name) ) ,
1757
}
1758
}
1759
let more = why.len ( ) .saturating_sub ( 4 ) ;
1760
why.truncate ( 4 ) ;
1761
let mut text = format! ( " no runner can take it: {} " , why.join ( " ; " ) ) ;
1762
if more > 0 {
1763
text.push_str ( & format! ( " ; and {more} more" ) ) ;
1764
}
1765
text
501d8b26 Andy McCall 24 days ago ↶
1766
}
1767
58cbf424 Andy McCall 17 days ago ↶
1768
1769
1770
1771
1772
# [ allow ( clippy::too_many_arguments ) ]
1773
pub fn why_still_waiting (
1774
job : & fdb:: Job,
1775
owner : & str ,
1776
repo_id : i64 ,
1777
runners : & [db:: runners:: Runner],
951aed50 Andy McCall 9 days ago ↶
1778
fleet : ( &std ::collections ::HashMap <String , String >, &crate ::images ::Reach , i64 ) ,
58cbf424 Andy McCall 17 days ago ↶
1779
declines : & [fdb:: DeclineRow],
1780
running : & std:: collections:: HashMap< i64 , i64 > ,
1781
ahead : usize ,
794ff94f Andy McCall 11 days ago ↶
1782
behind_urgent : & [String],
58cbf424 Andy McCall 17 days ago ↶
1783
) -> String {
951aed50 Andy McCall 9 days ago ↶
1784
let fleet = Fleet { image_archs: fleet.0 , reach: fleet.1 , now: fleet.2 } ;
58cbf424 Andy McCall 17 days ago ↶
1785
let mut busy = Vec :: new( ) ;
1786
for r in runners.iter ( ) .filter ( | r | r.state ! = " disabled" & & r.online ( ) ) {
1787
if runner_lacks ( job, owner, repo_id, r, & fleet, declines) .is_some ( ) {
1788
continue ;
1789
}
1790
let n = running.get ( & r.id) .copied ( ) .unwrap_or ( 0 ) ;
1791
let slots = r.max_jobs.max ( 1 ) ;
1792
if n > = slots {
1793
busy.push ( format! ( " {} busy ({n} of {slots} )" , r.name) ) ;
1794
} else {
5d66f27c Andy McCall 9 days ago ↶
1795
busy.push ( format! ( " {} has a free slot ({n} of {slots} ) but not the memory, CPUs or disk for it now" , r.name) ) ;
58cbf424 Andy McCall 17 days ago ↶
1796
}
1797
}
1798
let mut parts = Vec :: new( ) ;
1799
if ! busy.is_empty ( ) {
1800
parts.push ( format! ( " the runners that can take it are busy: {} " , busy.join ( " ; " ) ) ) ;
1801
}
1802
if ahead > 0 {
794ff94f Andy McCall 11 days ago ↶
1803
let after = match behind_urgent {
1804
[ ] => String :: new( ) ,
1805
[ one] => format! ( " (after expedited run {one} )" ) ,
1806
many => format! ( " (after expedited runs {} )" , many.join ( " , " ) ) ,
1807
} ;
1808
parts.push ( format! ( " {ahead} ready {} first{after} " , if ahead = = 1 { " job comes" } else { " jobs come" } ) ) ;
58cbf424 Andy McCall 17 days ago ↶
1809
}
1810
parts.join ( " ; " )
1811
}
1812
501d8b26 Andy McCall 24 days ago ↶
1813
1814
1815
const BETTER_HOLD_MS : i64 = 15_000 ;
1816
1817
const POLLING_MS : i64 = 10_000 ;
1818
1819
c95446a1 Andy McCall 24 days ago ↶
1820
1821
static HELD : std:: sync:: Mutex< Option< std:: collections:: HashMap< ( String, i64 ) , i64 > > > = std:: sync:: Mutex:: new( None ) ;
4fa0da12 Andy McCall 22 days ago ↶
1822
1823
1824
1825
c2be148c Andy McCall 22 days ago ↶
1826
1827
type CacheSlots = std:: collections:: HashMap< ( String, i64 ) , std:: collections:: HashMap< String, i64 > > ;
4fa0da12 Andy McCall 22 days ago ↶
1828
static CACHES : std:: sync:: Mutex< Option< CacheSlots> > = std:: sync:: Mutex:: new( None ) ;
1829
c2be148c Andy McCall 22 days ago ↶
1830
1831
1832
fn cache_kept ( hub : & str , runner : i64 , slot : & str ) -> Option< i64 > {
4fa0da12 Andy McCall 22 days ago ↶
1833
CACHES
1834
.lock ( )
1835
.unwrap_or_else ( | p | p.into_inner ( ) )
1836
.as_ref ( )
1837
.and_then ( | c | c.get ( & ( hub.to_string ( ) , runner) ) )
c2be148c Andy McCall 22 days ago ↶
1838
.and_then ( | held | held.get ( slot) .copied ( ) )
1839
}
1840
1841
1842
1843
const FRESHER_MS : i64 = 60_000 ;
1844
1845
1846
struct Fit {
1847
1848
cache : Option< i64 > ,
1849
image : bool ,
1850
speed : i64 ,
1851
}
1852
1853
impl Fit {
1854
1855
1856
1857
fn beats ( & self , other : & Fit) -> bool {
1858
match ( self .cache, other.cache) {
1859
( Some ( _ ) , None ) => return true ,
1860
( None , Some ( _ ) ) => return false ,
1861
( Some ( a) , Some ( b) ) if a > 0 & & b > 0 & & ( a - b) .abs ( ) > = FRESHER_MS => return a > b,
1862
_ => { }
1863
}
1864
( self .image, self .speed) > ( other.image, other.speed)
1865
}
1866
}
1867
1868
1869
1870
fn fit ( hub : & str , r : & db:: runners:: Runner, rootfs : & str , slot : & str ) -> Fit {
1871
Fit {
1872
cache: cache_kept ( hub, r.id, slot) ,
1873
image: r.image_list ( ) .is_some_and ( | have | have.iter ( ) .any ( | h | h = = rootfs ) ) ,
1874
speed: r.speed_class.unwrap_or ( 0 ) ,
1875
}
4fa0da12 Andy McCall 22 days ago ↶
1876
}
501d8b26 Andy McCall 24 days ago ↶
1877
c2be148c Andy McCall 22 days ago ↶
1878
1879
fn ago ( ms : i64 ) -> String {
1880
let s = ms.max ( 0 ) / 1000 ;
1881
match s {
1882
0 .. = 59 => format! ( " {s} s" ) ,
1883
60 .. = 3599 => format! ( " {} min" , s / 60 ) ,
1884
3600 .. = 172_799 => format! ( " {} h" , s / 3600 ) ,
1885
_ => format! ( " {} days" , s / 86_400 ) ,
1886
}
501d8b26 Andy McCall 24 days ago ↶
1887
}
1888
1889
fn rootfs_of ( job : & fdb:: Job) -> String {
1890
serde_json:: from_str:: < serde_json:: Value> ( & job.sandbox) .ok ( ) .and_then ( | v | v[ " rootfs" ] .as_str ( ) .map ( String :: from) ) .unwrap_or_default ( )
44a3d2eb Andy McCall 24 days ago ↶
1891
}
1892
1893
1894
fn mib ( m : i64 ) -> String {
1895
if m > = 1024 {
1896
format! ( " {:.1} GiB" , m as f64 / 1024. 0 )
1897
} else {
1898
format! ( " {m} MiB" )
1899
}
4d2e44d7 Andy McCall 1 month ago ↶
1900
}
1901
1902
1903
pub async fn explain_waits ( app : & Shared) -> sqlx:: Result< ( ) > {
1904
let ready = fdb:: ready_queued( & app.db) .await? ;
1905
if ready.is_empty ( ) {
1906
return Ok ( ( ) ) ;
1907
}
1908
let runners = db:: runners:: list( & app.db) .await? ;
1909
let archs = fdb:: image_archs( & app.db) .await? ;
951aed50 Andy McCall 9 days ago ↶
1910
let held = crate :: images:: reach( app) .await? ;
4d2e44d7 Andy McCall 1 month ago ↶
1911
let now = db:: now_ms( ) ;
58cbf424 Andy McCall 17 days ago ↶
1912
let ranks = fdb:: queued_ranks( & app.db) .await? ;
1913
let running = fdb:: running_by_runner( & app.db) .await? ;
1914
1915
let order: Vec< ( i64 , i64 ) > = ready.iter ( ) .map ( | ( j , _, _) | ( ranks.get ( & j.id) .copied ( ) .unwrap_or ( 2 ) , j.id) ) .collect ( ) ;
794ff94f Andy McCall 11 days ago ↶
1916
let run_of: std:: collections:: HashMap< i64 , i64 > = ready.iter ( ) .map ( | ( j , _, _) | ( j.id, j.run_id) ) .collect ( ) ;
1917
1918
let mut urgent: std:: collections:: BTreeMap< i64 , String> = std:: collections:: BTreeMap:: new( ) ;
1919
for ( j, _ , _ ) in ready.iter ( ) .filter ( | ( j , _, _) | ranks.get ( & j.id) = = Some ( & 0 ) ) {
1920
if urgent.contains_key ( & j.run_id) {
1921
continue ;
1922
}
1923
if let Some ( run) = fdb:: run_by_id( & app.db, j.run_id) .await? {
1924
if let Some ( row) = db:: repos:: get_by_id( & app.db, run.repo_id) .await? {
1925
urgent.insert ( j.run_id, format! ( " {} /{} #{} " , row.owner, row.name, run.number) ) ;
1926
}
1927
}
1928
}
1929
1930
let mut changed = std:: collections:: BTreeSet:: new( ) ;
4d2e44d7 Andy McCall 1 month ago ↶
1931
for ( job, owner, repo_id) in ready {
1932
let declines = fdb:: declines_of_job( & app.db, job.id) .await? ;
58cbf424 Andy McCall 17 days ago ↶
1933
let mut why = why_waiting ( & job, & owner, repo_id, & runners, & archs, & declines, now, & held) ;
1934
if why.is_empty ( ) {
1935
let mine = ( ranks.get ( & job.id) .copied ( ) .unwrap_or ( 2 ) , job.id) ;
1936
let ahead = order.iter ( ) .filter ( | o | * * o < mine) .count ( ) ;
794ff94f Andy McCall 11 days ago ↶
1937
let mut behind: Vec< String> = Vec :: new( ) ;
1938
if mine.0 > 0 {
1939
for o in order.iter ( ) .filter ( | o | o.0 = = 0 & & * * o < mine ) {
1940
let Some ( r) = run_of.get ( & o.1 ) .filter ( | r | * * r ! = job.run_id) else { continue } ;
1941
if let Some ( label) = urgent.get ( r) .filter ( | l | ! behind.contains ( l) ) {
1942
behind.push ( label.clone ( ) ) ;
1943
}
1944
}
1945
}
1946
why = why_still_waiting ( & job, & owner, repo_id, & runners, ( & archs, & held, now) , & declines, & running, ahead, & behind) ;
58cbf424 Andy McCall 17 days ago ↶
1947
}
67040b15 Andy McCall 25 days ago ↶
1948
if why = = job.waiting {
1949
continue ;
1950
}
4d2e44d7 Andy McCall 1 month ago ↶
1951
fdb:: set_waiting( & app.db, job.id, & why) .await? ;
794ff94f Andy McCall 11 days ago ↶
1952
changed.insert ( job.run_id) ;
67040b15 Andy McCall 25 days ago ↶
1953
58cbf424 Andy McCall 17 days ago ↶
1954
1955
1956
1957
let kind = | s : & str | s.chars ( ) .filter ( | c | ! c.is_ascii_digit ( ) ) .collect:: < String> ( ) ;
1958
if kind ( & why) = = kind ( & job.waiting) {
1959
continue ;
1960
}
67040b15 Andy McCall 25 days ago ↶
1961
if let ( Some ( run) , Some ( row) ) = ( fdb:: run_by_id( & app.db, job.run_id) .await? , db:: repos:: get_by_id( & app.db, repo_id) .await? ) {
1962
let summary = if why.is_empty ( ) { " waiting for a runner" } else { why.as_str ( ) } ;
1963
report_check ( app, & row, & run.state, run.number, & job.name, " queued" , summary) .await;
1964
}
794ff94f Andy McCall 11 days ago ↶
1965
}
1966
for run_id in changed {
1967
app.bus.live ( & crate :: events:: topic:: run( run_id) , " jobs" ) ;
4d2e44d7 Andy McCall 1 month ago ↶
1968
}
1969
Ok ( ( ) )
1970
}
1971
1972
1973
pub async fn placement ( app : Shared) {
1974
loop {
1975
tokio:: time:: sleep( std:: time:: Duration:: from_secs( 15 ) ) .await;
1976
if let Err ( e) = explain_waits ( & app) .await {
1977
tracing:: warn! ( " flow: placement pass: {e}" ) ;
1978
}
1979
}
1980
}
1981
25091e5c Andy McCall 1 month ago ↶
1982
f677d627 Andy McCall 23 days ago ↶
1983
pub ( crate ) async fn pins_in (
25091e5c Andy McCall 1 month ago ↶
1984
app : & Shared,
1985
name : crate:: repos:: RepoName,
1986
heads : Vec< oxy_core:: hash:: ChangeHash> ,
5d54cf74 Andy McCall 20 days ago ↶
1987
) -> Result< Vec< ( String, [u8 ; 16], Vec< oxy_core:: hash:: ChangeHash> ) > , String> {
25091e5c Andy McCall 1 month ago ↶
1988
app.repos
fd3db644 Andy McCall 1 month ago ↶
1989
.read ( & name, move | repo, cache| {
1990
let loaded = cache.load ( repo, & heads) ? ;
1991
let mut out = Vec :: new( ) ;
1992
for ( path, f) in & loaded.snapshot.files {
1993
if f.kind ! = oxy_core:: objects:: NodeKind:: Subrepo {
1994
continue ;
1995
}
1996
if let oxy_core:: state:: FileContent:: Opaque { blobs } = & f.content {
1997
if let Some ( b) = blobs.first ( ) {
1998
if let Ok ( oxy_core:: subrepo:: Pin:: Oxy { repo: id, heads, .. } ) = repo
1999
.read_blob ( b)
2000
.map_err ( crate :: repos:: RepoError:: Oxy)
2001
.and_then ( | bytes | oxy_core:: subrepo:: Pin:: decode( & bytes) .map_err ( | e | crate :: repos:: RepoError:: Oxy( e.into ( ) ) ) )
2002
{
2003
out.push ( ( path.clone ( ) , id, heads) ) ;
2004
}
2005
}
2006
}
2007
}
2008
Ok ( out)
2009
} )
2010
.await
5d54cf74 Andy McCall 20 days ago ↶
2011
.map_err ( | e | e.to_string ( ) )
25091e5c Andy McCall 1 month ago ↶
2012
}
2013
2014
2015
const MAX_SUBREPO_DEPTH : usize = 3 ;
2016
2017
2018
2019
afd0be38 Andy McCall 1 month ago ↶
2020
2021
2022
async fn subrepo_inputs (
2023
app : & Shared,
2024
row : & db:: repos:: RepoRow,
2025
run : & fdb:: Run,
2026
) -> ( Vec< flow_core:: api:: SubrepoInput> , Vec< i64 > , Vec< String> ) {
2027
let Some ( name) = crate :: repos:: RepoName:: new( & row.owner, & row.name) else { return ( vec! [ ] , vec! [ ] , vec! [ ] ) } ;
25091e5c Andy McCall 1 month ago ↶
2028
let heads: Vec< oxy_core:: hash:: ChangeHash> = run.head_list ( ) .into_iter ( ) .filter_map ( oxy_core:: hash:: ChangeHash:: from_hex) .collect ( ) ;
fd3db644 Andy McCall 1 month ago ↶
2029
let pusher = db:: accounts:: account_by_name( & app.db, & run.actor) .await.ok ( ) .flatten ( ) ;
25091e5c Andy McCall 1 month ago ↶
2030
let rows = db:: repos:: list( & app.db) .await.unwrap_or_default ( ) ;
fd3db644 Andy McCall 1 month ago ↶
2031
let mut inputs = Vec :: new( ) ;
2032
let mut ids = Vec :: new( ) ;
afd0be38 Andy McCall 1 month ago ↶
2033
let mut left_out = Vec :: new( ) ;
2034
let actor = if run.actor.is_empty ( ) { " the run's starter" .to_string ( ) } else { run.actor.clone ( ) } ;
25091e5c Andy McCall 1 month ago ↶
2035
2036
let mut queue = std:: collections:: VecDeque:: from( [ ( String :: new( ) , name, heads, 0 usize ) ] ) ;
2037
while let Some ( ( prefix, name, heads, depth) ) = queue.pop_front ( ) {
2038
if depth = = MAX_SUBREPO_DEPTH {
2039
continue ;
2040
}
5d54cf74 Andy McCall 20 days ago ↶
2041
for ( path, id, pinned) in pins_in ( app, name, heads) .await.unwrap_or_default ( ) {
25091e5c Andy McCall 1 month ago ↶
2042
let path = if prefix.is_empty ( ) { path } else { format! ( " {prefix} /{path} " ) } ;
afd0be38 Andy McCall 1 month ago ↶
2043
let mut hosted = false ;
25091e5c Andy McCall 1 month ago ↶
2044
for other in & rows {
2045
let Some ( other_name) = crate :: repos:: RepoName:: new( & other.owner, & other.name) else { continue } ;
2046
let matches = app.repos.read ( & other_name, move | r, _ | Ok ( r.id ( ) .ok ( ) = = Some ( id) ) ) .await.unwrap_or ( false ) ;
2047
if ! matches {
2048
continue ;
2049
}
afd0be38 Andy McCall 1 month ago ↶
2050
hosted = true ;
25091e5c Andy McCall 1 month ago ↶
2051
let role = crate :: auth:: roles:: role_for( & app.db, pusher.as_ref ( ) , other) .await.ok ( ) .flatten ( ) ;
180dc43a Andy McCall 23 days ago ↶
2052
2053
2054
2055
let diffused = run.actor = = crate :: diffusion:: ACCOUNT
2056
& & run.bookmark.starts_with ( " diffusion/" )
5d90e0e0 Andy McCall 20 days ago
2057
& & sqlx:: query_scalar:: < _ , i64 > ( " SELECT COUNT(*) FROM diffusion_pins WHERE consumer_id = $1 AND upstream_repo_id = $2" )
180dc43a Andy McCall 23 days ago ↶
2058
.bind ( run.repo_id)
2059
.bind ( other.id)
2060
.fetch_one ( & app.db)
2061
.await
2062
.unwrap_or ( 0 )
2063
> 0 ;
2064
if diffused | | crate :: auth:: roles:: allows( role, crate :: auth:: Access:: Read) {
25091e5c Andy McCall 1 month ago ↶
2065
inputs.push ( flow_core:: api:: SubrepoInput {
2066
path: path.clone ( ) ,
2067
repo: format! ( " {} /{} " , other.owner, other.name) ,
2068
clone_url: format! ( " {} /{} /{} " , app.config.base_url ( ) , other.owner, other.name) ,
2069
heads: pinned.iter ( ) .map ( | h | h.to_hex ( ) ) .collect ( ) ,
2070
} ) ;
2071
if ! ids.contains ( & other.id) {
2072
ids.push ( other.id) ;
2073
}
2074
queue.push_back ( ( path.clone ( ) , other_name, pinned.clone ( ) , depth + 1 ) ) ;
afd0be38 Andy McCall 1 month ago ↶
2075
} else {
2076
left_out.push ( format! (
2077
" subrepo {path} ({} /{} ) is left out: {actor} cannot read it, so the job has no files there" ,
2078
other.owner, other.name
2079
) ) ;
25091e5c Andy McCall 1 month ago ↶
2080
}
2081
break ;
afd0be38 Andy McCall 1 month ago ↶
2082
}
2083
if ! hosted {
2084
left_out.push ( format! ( " subrepo {path} is left out: the repository it pins is not on this hub" ) ) ;
fd3db644 Andy McCall 1 month ago ↶
2085
}
2086
}
2087
}
afd0be38 Andy McCall 1 month ago ↶
2088
( inputs, ids, left_out)
42fa9554 Andy McCall 1 month ago ↶
2089
}
2090
2091
async fn running_job ( app : & Shared, id : i64 ) -> Result< fdb:: Job, ApiError> {
2092
match fdb:: job( & app.db, id) .await? {
2093
Some ( j) if j.status = = " running" => Ok ( j) ,
2094
Some ( _ ) => Err ( ApiError( StatusCode:: CONFLICT , " this job is not running" .into ( ) ) ) ,
2095
None => Err ( ApiError( StatusCode:: NOT_FOUND , " no such job" .into ( ) ) ) ,
2096
}
2097
}
2098
2099
2100
pub async fn log (
2101
State( app ) : State< Shared> ,
2102
headers : HeaderMap,
2103
Path( id ) : Path< i64 > ,
2104
Json( mut batch ) : Json< LogBatch> ,
2105
) -> Result< StatusCode, ApiError> {
d94449e4 Andy McCall 1 month ago ↶
2106
let me = runner_auth ( & app, & headers) .await? ;
42fa9554 Andy McCall 1 month ago ↶
2107
let job = running_job ( & app, id) .await? ;
d94449e4 Andy McCall 1 month ago ↶
2108
2109
2110
db:: runners:: seen( & app.db, me.id, None , None ) .await? ;
745d225d Andy McCall 1 month ago ↶
2111
2112
2113
if batch.records.is_empty ( ) {
2114
return Ok ( StatusCode:: NO_CONTENT ) ;
2115
}
4ac01a73 Andy McCall 1 month ago ↶
2116
2117
let values: Vec< String> = match serde_json:: from_str:: < flow_core:: Job> ( & job.spec) {
2118
Ok ( spec) if ! spec.secrets.is_empty ( ) => {
2119
let run = fdb:: run_by_id( & app.db, job.run_id) .await? ;
2120
let row = match run {
2121
Some ( run) => db:: repos:: get_by_id( & app.db, run.repo_id) .await? ,
2122
None => None ,
2123
} ;
2124
match row {
2125
Some ( row) => {
2126
let names: Vec< String> = spec.secrets.iter ( ) .map ( | s | s.name.clone ( ) ) .collect ( ) ;
2127
crate :: secrets:: for_job( & app, & row, & names) .await? .into_iter ( ) .map ( | ( _, v ) | v ) .collect ( )
2128
}
2129
None => vec! [ ] ,
2130
}
2131
}
2132
_ => vec! [ ] ,
2133
} ;
d1727c57 Andy McCall 1 month ago ↶
2134
let kid = app.oidc.get ( ) .map ( | i | i.kid.clone ( ) ) ;
42fa9554 Andy McCall 1 month ago ↶
2135
for r in & mut batch.records {
4ac01a73 Andy McCall 1 month ago ↶
2136
if ! values.is_empty ( ) {
2137
r.text = crate :: secrets:: mask( & r.text, & values) ;
d1727c57 Andy McCall 1 month ago ↶
2138
}
2139
if let Some ( kid) = & kid {
2140
r.text = crate :: oidc:: mask( & r.text, kid) ;
4ac01a73 Andy McCall 1 month ago ↶
2141
}
42fa9554 Andy McCall 1 month ago ↶
2142
if r.text.len ( ) > MAX_RECORD_TEXT {
2143
let mut cut = MAX_RECORD_TEXT ;
2144
while ! r.text.is_char_boundary ( cut) {
2145
cut - = 1 ;
2146
}
2147
r.text.truncate ( cut) ;
2148
}
2149
if ! matches! ( r.stream.as_str ( ) , " out" | " err" | " console" | " flow" ) {
2150
r.stream = " console" .into ( ) ;
2151
}
2152
}
2153
let kept = fdb:: append_log( & app.db, job.id, & batch.records, MAX_RECORDS as i64 ) .await? ;
2154
if kept < batch.records.len ( ) & & job.log_count < MAX_RECORDS as i64 {
2155
tracing:: info! ( " flow: job {} reached the log limit" , job.id) ;
2156
}
2157
Ok ( StatusCode:: NO_CONTENT )
2158
}
2159
a3c8288b Andy McCall 1 month ago ↶
2160
2161
pub const MAX_ARTIFACT_BYTES : usize = 512 < < 20 ;
2162
2163
2164
2165
2166
pub async fn artifact (
2167
State( app ) : State< Shared> ,
2168
headers : HeaderMap,
2169
Path( (id , name ) ) : Path< ( i64 , String) > ,
2170
body: axum:: body:: Bytes,
2171
) -> Result< Response, ApiError> {
b6b7e4cf Andy McCall 1 month ago ↶
2172
runner_auth ( & app, & headers) .await? ;
a3c8288b Andy McCall 1 month ago ↶
2173
let job = running_job ( & app, id) .await? ;
2174
let spec: flow_core:: Job =
2175
serde_json:: from_str( & job.spec) .map_err ( | e | ApiError( StatusCode:: INTERNAL_SERVER_ERROR , format! ( " stored job: {e} " ) ) ) ? ;
2176
let Some ( out) = spec.outputs.iter ( ) .find ( | o | o.name = = name ) else {
2177
return Err ( ApiError( StatusCode:: BAD_REQUEST , format! ( " job {} declares no artifact {name:?} " , job.name) ) ) ;
2178
} ;
2179
let run = fdb:: run_by_id( & app.db, job.run_id) .await? .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " no such run" .into ( ) ) ) ? ;
2180
let row =
2181
db:: repos:: get_by_id( & app.db, run.repo_id) .await? .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " no such repository" .into ( ) ) ) ? ;
2182
let repo = RepoName:: new( & row.owner, & row.name) .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " bad repository name" .into ( ) ) ) ? ;
a36bab6b Andy McCall 25 days ago ↶
2183
2184
2185
let report = match out.kind {
2186
flow_core:: OutputKind:: Report => Some ( report_manifest ( & app, & repo, & name, & body) .await? ) ,
2187
flow_core:: OutputKind:: Artifact => None ,
2188
} ;
a3c8288b Andy McCall 1 month ago ↶
2189
let content = blake3:: hash( & body) .to_hex ( ) .to_string ( ) ;
2190
let size = body.len ( ) as i64 ;
a36bab6b Andy McCall 25 days ago ↶
2191
let artifact = report.is_none ( ) ;
2192
let report_bytes = ( artifact & & name = = crate :: coverage:: ARTIFACT ) .then ( | | body.clone ( ) ) ;
2193
let junit_bytes = ( artifact & & name = = crate :: junit:: ARTIFACT ) .then ( | | body.clone ( ) ) ;
a3c8288b Andy McCall 1 month ago ↶
2194
let blob = app
2195
.repos
2196
.write ( & repo, move | r, _ | Ok ( r.store_blob ( & body) ? ) )
2197
.await
2198
.map_err ( | e | ApiError( StatusCode:: INTERNAL_SERVER_ERROR , format! ( " keep the artifact: {e} " ) ) ) ? ;
2199
let a = fdb:: Artifact { job_id: job.id, name, path: out.path.clone ( ) , blob: blob.to_hex ( ) , size, content, created_ms: db:: now_ms( ) } ;
2200
fdb:: add_artifact( & app.db, & a) .await? ;
a36bab6b Andy McCall 25 days ago ↶
2201
if let Some ( ( manifest, declaration) ) = report {
2202
let origin = crate :: reports:: Origin {
2203
repo_id: row.id,
2204
run_id: run.id,
2205
run_number: run.number,
2206
job_id: job.id,
2207
job: & job.name,
2208
bookmark: & run.bookmark,
2209
state: & run.state,
2210
} ;
2211
let said = match declaration {
2212
Ok ( d) => match crate :: reports:: keep( & app, & origin, & a.name, & d, & manifest, & a.blob) .await {
c8df33a6 Andy McCall 24 days ago ↶
2213
Ok ( id) => {
2214
report_threshold_checks ( & app, & row, & run, & a.name, id, & d.metrics) .await;
a36bab6b Andy McCall 25 days ago ↶
2215
let s = if d.metrics.len ( ) = = 1 { " " } else { " s" } ;
2216
format! ( " report {} : {} \" {} \" , {} metric{s} " , a.name, d.kind, d.title, d.metrics.len ( ) )
2217
}
2218
Err ( e) => format! ( " report {} : not indexed: {e} " , a.name) ,
2219
} ,
2220
Err ( why) => {
2221
let d = crate :: reports:: Declaration:: unreadable( & a.name, & why) ;
2222
crate :: reports:: keep( & app, & origin, & a.name, & d, & manifest, & a.blob) .await.ok ( ) ;
2223
format! ( " report {} : report.toml: {why} " , a.name)
2224
}
2225
} ;
2226
let record = flow_core:: api:: Record { t: 0 , stream: " flow" .into ( ) , text: said } ;
2227
fdb:: append_log( & app.db, job.id, & [ record] , MAX_RECORDS as i64 ) .await.ok ( ) ;
2228
}
554ccacf Andy McCall 1 month ago ↶
2229
2230
2231
if let Some ( bytes) = report_bytes {
2232
let said = match read_coverage ( & app, & repo, & row, & run, & job, & a.blob, bytes) .await {
2233
Ok ( text) | Err ( text) => text,
2234
} ;
2235
let record = flow_core:: api:: Record { t: 0 , stream: " flow" .into ( ) , text: said } ;
2236
fdb:: append_log( & app.db, job.id, & [ record] , MAX_RECORDS as i64 ) .await.ok ( ) ;
2237
}
0b203bbf Andy McCall 1 month ago ↶
2238
2239
if let Some ( bytes) = junit_bytes {
2240
let said = match read_junit ( & app, & row, & run, & job, & a.blob, bytes) .await {
2241
Ok ( text) | Err ( text) => text,
2242
} ;
2243
let record = flow_core:: api:: Record { t: 0 , stream: " flow" .into ( ) , text: said } ;
2244
fdb:: append_log( & app.db, job.id, & [ record] , MAX_RECORDS as i64 ) .await.ok ( ) ;
2245
}
a3c8288b Andy McCall 1 month ago ↶
2246
Ok ( Json( serde_json:: json! ( { " blob" : a.blob } ) ) .into_response ( ) )
a36bab6b Andy McCall 25 days ago ↶
2247
}
2248
c8df33a6 Andy McCall 24 days ago ↶
2249
2250
2251
2252
async fn report_threshold_checks (
2253
app : & Shared,
2254
row : & db:: repos:: RepoRow,
2255
run : & fdb:: Run,
2256
name : & str ,
2257
id : i64 ,
2258
metrics : & [crate:: reports:: Metric],
2259
) {
2260
let url = format! ( " {} /{} /{} /reports/{name} ?id={id} " , app.config.base_url ( ) , row.owner, row.name) ;
2261
for m in metrics.iter ( ) .filter ( | m | m.fail_above.is_some ( ) | | m.fail_below.is_some ( ) ) {
2262
let unit = if m.unit.is_empty ( ) { String :: new( ) } else { format! ( " {} " , m.unit) } ;
2263
let above = m.fail_above.filter ( | a | m.value > * a ) ;
2264
let below = m.fail_below.filter ( | b | m.value < * b ) ;
2265
let ( status, summary) = match ( above, below) {
2266
( Some ( a) , _ ) => ( " failed" , format! ( " {} {unit} , above {a} " , m.value) ) ,
2267
( _ , Some ( b) ) => ( " failed" , format! ( " {} {unit} , below {b} " , m.value) ) ,
2268
_ => {
2269
let limits: Vec< String> = [ m.fail_above.map ( | a | format! ( " at most {a} " ) ) , m.fail_below.map ( | b | format! ( " at least {b} " ) ) ]
2270
.into_iter ( )
2271
.flatten ( )
2272
.collect ( ) ;
2273
( " passed" , format! ( " {} {unit} ({} )" , m.value, limits.join ( " , " ) ) )
2274
}
2275
} ;
2276
let check = format! ( " report/{name} /{} " , m.key) ;
2277
if let Ok ( c) = db:: hooks:: upsert_check(
2278
& app.db,
2279
row.id,
2280
db:: hooks:: CheckReport { state: & run.state, name: & check, status, summary: & summary, url: & url, reporter: None , seal: " " } ,
2281
)
2282
.await
2283
{
2284
let api = crate :: api:: check_of( & c) ;
33d98147 Andy McCall 16 days ago
2285
app.bus.proposal ( row.id, hub_api:: ProposalEvent { number: 0 , kind: " check" .into ( ) , check: Some ( api.clone ( ) ) } ) ;
c8df33a6 Andy McCall 24 days ago ↶
2286
crate :: hooks:: emit( app, row, " check" , " flow" , serde_json:: json! ( { " check" : api } ) ) .await;
2287
}
2288
}
2289
}
2290
a36bab6b Andy McCall 25 days ago ↶
2291
2292
2293
2294
async fn report_manifest (
2295
app : & Shared,
2296
repo : & RepoName,
2297
name : & str ,
2298
body : & [u8 ],
2299
) -> Result< ( crate:: reports:: Manifest, Result< crate:: reports:: Declaration, String> ) , ApiError> {
2300
let manifest = crate :: reports:: parse_manifest( body) .map_err ( | e | ApiError( StatusCode:: BAD_REQUEST , e) ) ? ;
2301
let refs: Vec< ( String, oxy_core:: hash:: ObjectRef) > =
2302
manifest.files.iter ( ) .filter_map ( | f | Some ( ( f.path.clone ( ) , oxy_core:: hash:: ObjectRef:: from_hex( & f.blob) ? ) ) ) .collect ( ) ;
2303
let declaration = manifest.blob ( " report.toml" ) .and_then ( oxy_core:: hash:: ObjectRef:: from_hex) ;
2304
let found = app
2305
.repos
2306
.read ( repo, move | r, _ | {
2307
if let Some ( ( path, _ ) ) = refs.iter ( ) .find ( | ( _, b ) | ! r.store ( ) .has ( b) ) {
2308
return Ok ( Err ( path.clone ( ) ) ) ;
2309
}
2310
Ok ( Ok ( declaration.map ( | b | r.read_blob ( & b) ) .transpose ( ) ? ) )
2311
} )
2312
.await
2313
.map_err ( | e | ApiError( StatusCode:: INTERNAL_SERVER_ERROR , format! ( " read the report: {e} " ) ) ) ? ;
2314
let toml = found.map_err ( | path | ApiError( StatusCode:: BAD_REQUEST , format! ( " report file {path} was not uploaded; upload it first" ) ) ) ? ;
2315
let declaration = crate :: reports:: parse_declaration( name, & toml.unwrap_or_default ( ) ) ;
2316
Ok ( ( manifest, declaration) )
2317
}
2318
2319
2320
2321
2322
2323
pub async fn report_file (
2324
State( app ) : State< Shared> ,
2325
headers : HeaderMap,
2326
Path( (id , name ) ) : Path< ( i64 , String) > ,
2327
body: axum:: body:: Bytes,
2328
) -> Result< Response, ApiError> {
2329
runner_auth ( & app, & headers) .await? ;
2330
let job = running_job ( & app, id) .await? ;
2331
let spec: flow_core:: Job =
2332
serde_json:: from_str( & job.spec) .map_err ( | e | ApiError( StatusCode:: INTERNAL_SERVER_ERROR , format! ( " stored job: {e} " ) ) ) ? ;
2333
if ! spec.outputs.iter ( ) .any ( | o | o.name = = name & & o.kind = = flow_core:: OutputKind:: Report ) {
2334
return Err ( ApiError( StatusCode:: BAD_REQUEST , format! ( " job {} declares no report {name:?} " , job.name) ) ) ;
2335
}
2336
let path = headers.get ( " x-flow-report-path" ) .and_then ( | v | v.to_str ( ) .ok ( ) ) .unwrap_or_default ( ) ;
2337
if ! crate :: reports:: path_ok( path) {
2338
return Err ( ApiError( StatusCode:: BAD_REQUEST , format! ( " report file {path:?} : give its path in X-Flow-Report-Path" ) ) ) ;
2339
}
2340
let run = fdb:: run_by_id( & app.db, job.run_id) .await? .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " no such run" .into ( ) ) ) ? ;
2341
let row =
2342
db:: repos:: get_by_id( & app.db, run.repo_id) .await? .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " no such repository" .into ( ) ) ) ? ;
2343
let repo = RepoName:: new( & row.owner, & row.name) .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " bad repository name" .into ( ) ) ) ? ;
2344
let blob = app
2345
.repos
2346
.write ( & repo, move | r, _ | Ok ( r.store_blob ( & body) ? ) )
2347
.await
2348
.map_err ( | e | ApiError( StatusCode:: INTERNAL_SERVER_ERROR , format! ( " keep the report file: {e} " ) ) ) ? ;
2349
Ok ( Json( serde_json:: json! ( { " blob" : blob.to_hex ( ) } ) ) .into_response ( ) )
0b203bbf Andy McCall 1 month ago ↶
2350
}
2351
2352
2353
async fn read_junit (
2354
app : & Shared,
2355
row : & db:: repos:: RepoRow,
2356
run : & fdb:: Run,
2357
job : & fdb:: Job,
2358
blob : & str ,
2359
bytes : axum:: body:: Bytes,
2360
) -> Result< String, String> {
2361
if bytes.len ( ) > crate :: junit:: MAX_REPORT {
2362
return Err ( format! ( " tests: the report is {} MiB; at most {} MiB is read" , bytes.len ( ) > > 20 , crate :: junit:: MAX_REPORT > > 20 ) ) ;
2363
}
2364
let text = String :: from_utf8( bytes.to_vec ( ) ) .map_err ( | _| " tests: the report is not UTF-8 text (JUnit XML is expected)" .to_string ( ) ) ? ;
2365
let cases = tokio:: task:: spawn_blocking( move | | crate :: junit:: parse( & text) )
2366
.await
2367
.map_err ( | e | format! ( " tests: {e} " ) ) ?
2368
.map_err ( | e | format! ( " tests: {e} " ) ) ? ;
2369
let origin = crate :: junit:: Origin {
2370
repo_id: row.id,
2371
run_id: run.id,
2372
run_number: run.number,
2373
job_id: job.id,
2374
job: & job.name,
2375
bookmark: & run.bookmark,
2376
state: & run.state,
2377
blob,
2378
} ;
2379
crate :: junit:: keep( app, & origin, & cases) .await.map_err ( | e | format! ( " tests: {e} " ) ) ? ;
2380
let r = crate :: junit:: for_run( & app.db, run.id)
2381
.await
2382
.map_err ( | e | format! ( " tests: {e} " ) ) ?
2383
.into_iter ( )
2384
.find ( | r | r.job_id = = job.id )
2385
.ok_or_else ( | | " tests: the report was not kept" .to_string ( ) ) ? ;
3d5c825a Andy McCall 1 month ago ↶
2386
2387
2388
2389
let base = crate :: junit:: baseline( & app.db, & r, & row.default_bookmark) .await.map_err ( | e | format! ( " tests: {e} " ) ) ? ;
2390
let newly = match & base {
2391
Some ( b) if r.broken ( ) > 0 => crate :: junit:: newly_failing( & app.db, r.id, b.id) .await.map_err ( | e | format! ( " tests: {e} " ) ) ? ,
2392
_ => vec! [ ] ,
2393
} ;
2394
let ( status, summary) = if r.broken ( ) = = 0 {
2395
( " passed" , format! ( " {} passed" , r.passed) )
2396
} else if let Some ( b) = & base {
2397
let names: Vec< & str > = newly.iter ( ) .take ( 3 ) .map ( String :: as_str) .collect ( ) ;
2398
let more = if newly.len ( ) > 3 { format! ( " , +{} " , newly.len ( ) - 3 ) } else { String :: new( ) } ;
2399
let new = if newly.is_empty ( ) {
2400
format! ( " none new against {} run #{} " , row.default_bookmark, b.run_number)
2401
} else {
2402
format! ( " {} new against {} : {} {more} " , newly.len ( ) , row.default_bookmark, names.join ( " , " ) )
2403
} ;
2404
( " failed" , format! ( " {} failing; {new} " , r.broken ( ) ) )
2405
} else {
2406
( " failed" , format! ( " {} failing" , r.broken ( ) ) )
2407
} ;
2408
let url = format! ( " {} /{} /{} /tests?report={} " , app.config.base_url ( ) , row.owner, row.name, r.id) ;
2409
if let Ok ( c) = db:: hooks:: upsert_check(
2410
& app.db,
2411
row.id,
2412
db:: hooks:: CheckReport { state: & run.state, name: " tests" , status, summary: & summary, url: & url, reporter: None , seal: " " } ,
2413
)
2414
.await
2415
{
2416
let api = crate :: api:: check_of( & c) ;
33d98147 Andy McCall 16 days ago
2417
app.bus.proposal ( row.id, hub_api:: ProposalEvent { number: 0 , kind: " check" .into ( ) , check: Some ( api.clone ( ) ) } ) ;
3d5c825a Andy McCall 1 month ago ↶
2418
crate :: hooks:: emit( app, row, " check" , " flow" , serde_json:: json! ( { " check" : api } ) ) .await;
2419
}
0b203bbf Andy McCall 1 month ago ↶
2420
Ok ( format! ( " tests: {} in {} " , r.summary ( ) , human_ms ( r.time_ms) ) )
554ccacf Andy McCall 1 month ago ↶
2421
}
2422
2423
2424
async fn read_coverage (
2425
app : & Shared,
2426
repo : & RepoName,
2427
row : & db:: repos:: RepoRow,
2428
run : & fdb:: Run,
2429
job : & fdb:: Job,
2430
blob : & str ,
2431
bytes : axum:: body:: Bytes,
2432
) -> Result< String, String> {
2433
if bytes.len ( ) > crate :: coverage:: MAX_REPORT {
2434
return Err ( format! (
2435
" coverage: the report is {} MiB; at most {} MiB is read" ,
2436
bytes.len ( ) > > 20 ,
2437
crate :: coverage:: MAX_REPORT > > 20
2438
) ) ;
2439
}
7fff30c4 Andy McCall 1 month ago ↶
2440
let text = String :: from_utf8( bytes.to_vec ( ) )
17dc8de4 Andy McCall 10 days ago ↶
2441
.map_err ( | _| " coverage: the report is not text (LCOV, Cobertura XML or JaCoCo XML is expected)" .to_string ( ) ) ? ;
7fff30c4 Andy McCall 1 month ago ↶
2442
let ( files, outside, format) = tokio:: task:: spawn_blocking( move | | crate :: coverage:: parse( & text) )
554ccacf Andy McCall 1 month ago ↶
2443
.await
2444
.map_err ( | e | format! ( " coverage: {e} " ) ) ?
2445
.map_err ( | e | format! ( " coverage: {e} " ) ) ? ;
2446
let heads: Vec< ChangeHash> = run.head_list ( ) .into_iter ( ) .filter_map ( ChangeHash:: from_hex) .collect ( ) ;
2447
let in_state: std:: collections:: HashSet< String> = app
2448
.repos
2449
.read ( repo, move | r, cache| Ok ( cache.load ( r, & heads) ? .snapshot.files.keys ( ) .cloned ( ) .collect ( ) ) )
2450
.await
2451
.map_err ( | e | format! ( " coverage: the run's files could not be read: {e} " ) ) ? ;
2452
let heads: Vec< String> = run.head_list ( ) .into_iter ( ) .map ( String :: from) .collect ( ) ;
2453
let origin = crate :: coverage:: Origin {
2454
repo_id: row.id,
40dd3f86 Andy McCall 24 days ago ↶
2455
run_id: Some ( run.id) ,
554ccacf Andy McCall 1 month ago ↶
2456
run_number: run.number,
40dd3f86 Andy McCall 24 days ago ↶
2457
job_id: Some ( job.id) ,
2458
uploader: " " ,
554ccacf Andy McCall 1 month ago ↶
2459
job: & job.name,
2460
bookmark: & run.bookmark,
2461
state: & run.state,
2462
heads: & heads,
2463
blob,
7fff30c4 Andy McCall 1 month ago ↶
2464
format,
554ccacf Andy McCall 1 month ago ↶
2465
} ;
2466
let id = crate :: coverage:: keep( app, & origin, files, outside, & in_state) .await.map_err ( | e | format! ( " coverage: {e} " ) ) ? ;
2467
let Some ( r) = crate :: coverage:: report( & app.db, row.id, id) .await.map_err ( | e | format! ( " coverage: {e} " ) ) ? else {
2468
return Err ( " coverage: the report was not kept" .into ( ) ) ;
2469
} ;
7edaff01 Andy McCall 1 month ago ↶
2470
2471
2472
let base = crate :: coverage:: baseline( & app.db, & r, & row.default_bookmark) .await.map_err ( | e | format! ( " coverage: {e} " ) ) ? ;
2473
let rules = crate :: coverage:: rules( & app.db, row.id) .await.map_err ( | e | format! ( " coverage: {e} " ) ) ? ;
2474
let ( status, summary) = crate :: coverage:: verdict( & r, base.as_ref ( ) , & row.default_bookmark, rules) ;
2475
let url = format! ( " {} /{} /{} /coverage?report={} " , app.config.base_url ( ) , row.owner, row.name, r.id) ;
2476
if let Ok ( c) = db:: hooks:: upsert_check(
2477
& app.db,
2478
row.id,
2479
db:: hooks:: CheckReport { state: & run.state, name: " coverage" , status, summary: & summary, url: & url, reporter: None , seal: " " } ,
2480
)
2481
.await
2482
{
2483
let api = crate :: api:: check_of( & c) ;
33d98147 Andy McCall 16 days ago
2484
app.bus.proposal ( row.id, hub_api:: ProposalEvent { number: 0 , kind: " check" .into ( ) , check: Some ( api.clone ( ) ) } ) ;
7edaff01 Andy McCall 1 month ago ↶
2485
crate :: hooks:: emit( app, row, " check" , " flow" , serde_json:: json! ( { " check" : api } ) ) .await;
2486
}
554ccacf Andy McCall 1 month ago ↶
2487
let pct = r.percent ( ) .map ( | p | format! ( " {p:.1} %" ) ) .unwrap_or_else ( | | " no lines" .into ( ) ) ;
2488
let dropped =
2489
if r.dropped > 0 { format! ( " ({} named files are not in this state and were left out)" , r.dropped) } else { String :: new( ) } ;
2490
Ok ( format! ( " coverage: {pct} of lines ({} of {} ) in {} files{dropped} " , r.lines_hit, r.lines_found, r.files) )
a3c8288b Andy McCall 1 month ago ↶
2491
}
2492
42fa9554 Andy McCall 1 month ago ↶
2493
2494
pub async fn finish (
2495
State( app ) : State< Shared> ,
2496
headers : HeaderMap,
2497
Path( id ) : Path< i64 > ,
2498
Json( f ) : Json< Finish> ,
2499
) -> Result< StatusCode, ApiError> {
b6b7e4cf Andy McCall 1 month ago ↶
2500
let me = runner_auth ( & app, & headers) .await? ;
42fa9554 Andy McCall 1 month ago ↶
2501
let job = running_job ( & app, id) .await? ;
d94449e4 Andy McCall 1 month ago ↶
2502
db:: runners:: seen( & app.db, me.id, None , None ) .await? ;
42fa9554 Andy McCall 1 month ago ↶
2503
let status = match ( f.exit_code, & f.error) {
2504
( Some ( 0 ) , None ) => " passed" ,
2505
( Some ( _ ) , _ ) => " failed" ,
2506
( None , _ ) => " error" ,
2507
} ;
2508
let ( receipt, receipt_hash) = match & f.receipt {
2509
Some ( r) if r.job_name = = job.name & & Some ( r.exit_code as i32 ) = = f.exit_code => {
d29070fa Andy McCall 1 month ago ↶
2510
2511
2512
2513
let kept: Vec< String> =
2514
if r.exit_code = = 0 { fdb:: artifacts( & app.db, job.id) .await? .into_iter ( ) .map ( | a | a.blob ) .collect ( ) } else { vec! [ ] } ;
a3c8288b Andy McCall 1 month ago ↶
2515
if r.outputs.iter ( ) .map ( hex:: encode) .collect:: < Vec< _ > > ( ) ! = kept {
2516
return Err ( ApiError( StatusCode:: BAD_REQUEST , " the receipt names other outputs than the artifacts uploaded" .into ( ) ) ) ;
2517
}
42fa9554 Andy McCall 1 month ago ↶
2518
( serde_json:: to_string( r) .unwrap_or_default ( ) , hex:: encode( r.hash ( ) ) )
2519
}
2520
Some ( _ ) => return Err ( ApiError( StatusCode:: BAD_REQUEST , " the receipt does not match the job" .into ( ) ) ) ,
2521
None => ( String :: new( ) , String :: new( ) ) ,
2522
} ;
2523
let error = f.error.clone ( ) .unwrap_or_default ( ) ;
44a3d2eb Andy McCall 24 days ago ↶
2524
fdb:: set_peaks( & app.db, job.id, f.peak_disk_mib, f.peak_memory_mib) .await? ;
04683947 Andy McCall 9 days ago ↶
2525
fdb:: set_phases( & app.db, job.id, & f.phases) .await? ;
42fa9554 Andy McCall 1 month ago ↶
2526
let changed = fdb:: finish(
2527
& app.db,
2528
job.id,
2529
fdb:: Outcome { status, exit_code: f.exit_code.map ( i64 :: from) , error: & error, receipt: & receipt, receipt_hash: & receipt_hash } ,
2530
)
2531
.await? ;
2532
if let Some ( run) = fdb:: run_by_id( & app.db, job.run_id) .await? {
2533
if let Some ( row) = db:: repos:: get_by_id( & app.db, run.repo_id) .await? {
cc19697f Andy McCall 1 month ago ↶
2534
if let ( Some ( r) , Some ( signed) ) = ( & f.receipt, & f.signed_receipt) {
b6b7e4cf Andy McCall 1 month ago ↶
2535
let ( attest, signer, note) = match keep_receipt ( & app, & me, & row, & run, r, signed) .await {
cc19697f Andy McCall 1 month ago ↶
2536
Ok ( ( attest, signer) ) => ( attest, signer, String :: new( ) ) ,
2537
Err ( e) => {
2538
tracing:: warn! ( " flow: job {} receipt not kept: {e}" , job.id) ;
2539
( String :: new( ) , String :: new( ) , e)
2540
}
2541
} ;
2542
fdb:: set_receipt( & app.db, job.id, & attest, & signer, & note) .await? ;
2543
}
42fa9554 Andy McCall 1 month ago ↶
2544
}
2545
}
070a7b64 Andy McCall 1 month ago ↶
2546
report_changed ( & app, job.run_id, changed) .await? ;
42fa9554 Andy McCall 1 month ago ↶
2547
Ok ( StatusCode:: NO_CONTENT )
070a7b64 Andy McCall 1 month ago ↶
2548
}
2549
2550
2551
async fn report_changed ( app : & Shared, run_id : i64 , changed : Vec< fdb:: Job> ) -> sqlx:: Result< ( ) > {
2552
let Some ( run) = fdb:: run_by_id( & app.db, run_id) .await? else {
2553
return Ok ( ( ) ) ;
2554
} ;
2555
let Some ( row) = db:: repos:: get_by_id( & app.db, run.repo_id) .await? else {
2556
return Ok ( ( ) ) ;
2557
} ;
4a4400c5 Andy McCall 22 days ago ↶
2558
let mut errored_on: Vec< i64 > = Vec :: new( ) ;
dc59d1b5 Andy McCall 20 days ago ↶
2559
let some_passed = changed.iter ( ) .any ( | j | j.status = = " passed" ) ;
7e01a766 Andy McCall 20 days ago ↶
2560
if ! changed.is_empty ( ) {
33d98147 Andy McCall 16 days ago
2561
app.bus.live ( & crate :: events:: topic:: run( run_id) , " jobs" ) ;
7e01a766 Andy McCall 20 days ago ↶
2562
}
070a7b64 Andy McCall 1 month ago ↶
2563
for j in changed {
2564
let summary = match j.status.as_str ( ) {
42cd46c9 Andy McCall 8 days ago ↶
2565
" passed" => match ( j.reused_from ( ) , j.reused_repo ( ) ) {
2566
( Some ( n) , Some ( r) ) => format! ( " passed in {r} run #{n} on this state, reused" ) ,
2567
( Some ( n) , None ) => format! ( " passed in run #{n} on this state, reused" ) ,
2568
( None , _ ) => format! ( " passed in {} " , duration ( & j) ) ,
5588bb36 Andy McCall 15 days ago ↶
2569
} ,
070a7b64 Andy McCall 1 month ago ↶
2570
" failed" => format! ( " exit {} after {} " , j.exit_code.unwrap_or ( - 1 ) , duration ( & j) ) ,
2571
" skipped" => " skipped: a dependency did not pass" .into ( ) ,
745d225d Andy McCall 1 month ago ↶
2572
" cancelled" => j.error.clone ( ) ,
070a7b64 Andy McCall 1 month ago ↶
2573
_ => format! ( " could not run: {} " , j.error) ,
2574
} ;
2575
report_check ( app, & row, & run.state, run.number, & j.name, check_status ( & j.status) , & summary) .await;
4a4400c5 Andy McCall 22 days ago ↶
2576
if j.status = = " error" {
2577
if let Some ( r) = runner_of ( app, j.id) .await {
2578
errored_on.push ( r) ;
2579
}
2580
}
2581
}
2582
errored_on.dedup ( ) ;
2583
for r in errored_on {
2584
drain_if_erroring ( app, r) .await;
dc59d1b5 Andy McCall 20 days ago ↶
2585
}
2586
if some_passed {
2587
tell_approvers ( app, & row, & run) .await;
4a4400c5 Andy McCall 22 days ago ↶
2588
}
2589
if let Some ( done) = fdb:: run_by_id( & app.db, run_id) .await? {
2590
if done.status = = " failed" & & ! done.live ( ) {
2591
tell_failed ( app, & row, & done) .await;
2592
}
bff9106e Andy McCall 22 days ago ↶
2593
2594
if done.status = = " passed" {
2595
let before: Option< String> = sqlx:: query_scalar(
5d90e0e0 Andy McCall 20 days ago
2596
" SELECT status FROM flow_runs WHERE repo_id = $1 AND bookmark = $2 AND id < $3 AND status IN ('passed', 'failed')
bff9106e Andy McCall 22 days ago ↶
2597
ORDER BY id DESC LIMIT 1" ,
2598
)
2599
.bind ( done.repo_id)
2600
.bind ( & done.bookmark)
2601
.bind ( done.id)
2602
.fetch_optional ( & app.db)
2603
.await? ;
2604
2605
let url = format! ( " /{} /{} /flow/{} " , row.owner, row.name, done.number) ;
5d90e0e0 Andy McCall 20 days ago
2606
let failed_here: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM notifications WHERE kind = 'flow.failed' AND url = $1" )
bff9106e Andy McCall 22 days ago ↶
2607
.bind ( & url)
2608
.fetch_one ( & app.db)
2609
.await? ;
2610
if before.as_deref ( ) = = Some ( " failed" ) | | failed_here > 0 {
2611
tell_fixed ( app, & row, & done) .await;
2612
}
2613
}
070a7b64 Andy McCall 1 month ago ↶
2614
}
a7e6e8e6 Andy McCall 23 days ago ↶
2615
2616
2617
if let Some ( done) = fdb:: run_by_id( & app.db, run_id) .await? {
2618
if done.status = = " passed" & & done.bookmark = = row.default_bookmark {
2619
let heads: Vec< ChangeHash> = done.head_list ( ) .iter ( ) .filter_map ( | h | ChangeHash:: from_hex( h) ) .collect ( ) ;
2620
let ( app, row) = ( app.clone ( ) , row.clone ( ) ) ;
2621
tokio:: spawn( async move { crate :: diffusion:: propagate( & app, & row, heads, true ) .await } ) ;
2622
}
f4d6ef41 Andy McCall 22 days ago ↶
2623
2624
2625
if let Some ( path) = done.bookmark.strip_prefix ( " diffusion/" ) .filter ( | _| done.status = = " passed" ) {
2626
tokio:: spawn( crate :: diffusion:: try_land( app.clone ( ) , row.clone ( ) , path.to_string ( ) ) ) ;
2627
}
a7e6e8e6 Andy McCall 23 days ago ↶
2628
}
070a7b64 Andy McCall 1 month ago ↶
2629
Ok ( ( ) )
4a4400c5 Andy McCall 22 days ago ↶
2630
}
2631
b1860d60 Andy McCall 8 days ago ↶
2632
2633
2634
2635
2636
pub async fn expedite (
2637
app : & Shared,
2638
row : & db:: repos:: RepoRow,
2639
run : & fdb:: Run,
2640
me : & db:: accounts:: Account,
2641
reason : & str ,
2642
) -> Result< ( ) , String> {
2643
let reason: String = reason.chars ( ) .filter ( | c | ! c.is_control ( ) ) .take ( 200 ) .collect ( ) ;
2644
let reason = reason.trim ( ) ;
2645
if reason.is_empty ( ) {
2646
return Err ( " Say why the run is urgent." .into ( ) ) ;
2647
}
2648
let owner = & row.owner;
2649
2650
2651
let cap: i64 = db:: accounts:: setting( & app.db, " flow_expedite_cap" ) .await.ok ( ) .flatten ( ) .and_then ( | v | v.parse ( ) .ok ( ) ) .unwrap_or ( 2 ) ;
2652
let live = fdb:: expedited_live( & app.db, owner) .await.map_err ( | e | e.to_string ( ) ) ? ;
2653
if live > = cap {
2654
return Err ( format! ( " {owner} already has {live} expedited run(s) going, the most at once ({cap} ); wait for one to end." ) ) ;
2655
}
2656
let note = format! ( " expedited by {} : {reason} " , me.username) ;
2657
if fdb:: expedite( & app.db, run.id, & note) .await.map_err ( | e | e.to_string ( ) ) ? {
2658
db:: accounts:: audit( & app.db, Some ( me) , " flow.expedite" , & format! ( " {owner} /{} " , row.name) , & format! ( " #{} : {reason} " , run.number) )
2659
.await
2660
.ok ( ) ;
2661
2662
if let Err ( e) = explain_waits ( app) .await {
2663
tracing:: warn! ( " flow: placing after an expedite: {e}" ) ;
2664
}
2665
app.bus.live ( & crate :: events:: topic:: run( run.id) , " jobs" ) ;
06b75635 Andy McCall 8 days ago ↶
2666
}
2667
Ok ( ( ) )
2668
}
2669
2670
2671
2672
2673
2674
pub async fn receipt_holds ( app : & Shared, from : & str , attest : & str ) -> Result< ( ) , String> {
2675
if attest.is_empty ( ) {
2676
return Err ( " no signed receipt was kept" .into ( ) ) ;
2677
}
2678
let repo = crate :: repos:: RepoName:: parse( from) .ok_or ( " not a repository name" ) ? ;
2679
let at = oxy_core:: hash:: ObjectRef:: from_hex( attest) .ok_or ( " not a receipt's hash" ) ? ;
2680
let info = app.repos.read ( & repo, move | r, _ | Ok ( r.receipt ( & at) ? ) ) .await.map_err ( | e | format! ( " the receipt: {e} " ) ) ? ;
2681
if let Some ( problem) = info.problem {
2682
return Err ( format! ( " its receipt does not verify: {problem} " ) ) ;
2683
}
2684
if info.revoked {
2685
return Err ( " its runner's key is revoked" .into ( ) ) ;
b1860d60 Andy McCall 8 days ago ↶
2686
}
2687
Ok ( ( ) )
2688
}
2689
4a4400c5 Andy McCall 22 days ago ↶
2690
2691
2692
pub const ACCOUNT : & str = " oxygenflow" ;
2693
2694
async fn flow_account ( pool : & db:: Pool) -> Option< db:: accounts:: Account> {
2695
if let Ok ( Some ( a) ) = db:: accounts:: account_by_name( pool, ACCOUNT ) .await {
2696
return Some ( a) ;
2697
}
2698
db:: accounts:: create_account( pool, ACCOUNT , " oxygenflow@localhost" , " !" , " " , false ) .await.ok ( )
2699
}
2700
2701
async fn runner_of ( app : & Shared, job_id : i64 ) -> Option< i64 > {
5d90e0e0 Andy McCall 20 days ago
2702
sqlx:: query_scalar( " SELECT runner_id FROM flow_jobs WHERE id = $1" ) .bind ( job_id) .fetch_optional ( & app.db) .await.ok ( ) .flatten ( ) .flatten ( )
4a4400c5 Andy McCall 22 days ago ↶
2703
}
2704
2705
2706
2707
2708
async fn tell_failed ( app : & Shared, row : & db:: repos:: RepoRow, run : & fdb:: Run) {
2709
let Some ( me) = flow_account ( & app.db) .await else { return } ;
2710
let jobs = fdb:: jobs( & app.db, run.id) .await.unwrap_or_default ( ) ;
2711
let broken: Vec< String> = jobs.iter ( ) .filter ( | j | matches! ( j.status.as_str ( ) , " failed" | " error" ) ) .map ( | j | j.name.clone ( ) ) .collect ( ) ;
2712
let pusher = db:: accounts:: account_by_name( & app.db, & run.actor) .await.ok ( ) .flatten ( ) ;
2713
let on = if run.bookmark.is_empty ( ) { String :: new( ) } else { format! ( " on {} " , run.bookmark) } ;
2714
crate :: notify:: deliver(
2715
app,
2716
crate :: notify:: Event {
2717
kind: " flow.failed" ,
2718
title: format! ( " {} /{} : run #{} {on} failed ({} )" , row.owner, row.name, run.number, broken.join ( " , " ) ) ,
bff9106e Andy McCall 22 days ago ↶
2719
url: format! ( " /{} /{} /flow/{} " , row.owner, row.name, run.number) ,
2720
actor: & me,
2721
repo_id: row.id,
2722
thread: None ,
2723
mention_text: " " ,
2724
direct: pusher.into_iter ( ) .map ( | a | a.id ) .collect ( ) ,
2725
} ,
2726
)
2727
.await;
2728
}
2729
2730
2731
2732
async fn tell_fixed ( app : & Shared, row : & db:: repos:: RepoRow, run : & fdb:: Run) {
2733
let Some ( me) = flow_account ( & app.db) .await else { return } ;
2734
let pusher = db:: accounts:: account_by_name( & app.db, & run.actor) .await.ok ( ) .flatten ( ) ;
2735
let on = if run.bookmark.is_empty ( ) { String :: new( ) } else { format! ( " on {} " , run.bookmark) } ;
2736
crate :: notify:: deliver(
2737
app,
2738
crate :: notify:: Event {
2739
kind: " flow.fixed" ,
2740
title: format! ( " {} /{} : run #{} {on} passed: fixed" , row.owner, row.name, run.number) ,
4a4400c5 Andy McCall 22 days ago ↶
2741
url: format! ( " /{} /{} /flow/{} " , row.owner, row.name, run.number) ,
2742
actor: & me,
2743
repo_id: row.id,
2744
thread: None ,
2745
mention_text: " " ,
2746
direct: pusher.into_iter ( ) .map ( | a | a.id ) .collect ( ) ,
2747
} ,
2748
)
2749
.await;
2750
}
2751
2752
2753
2754
2755
async fn drain_if_erroring ( app : & Shared, runner_id : i64 ) {
2756
let n = app.config.flow.drain_after_errors;
2757
let Ok ( Some ( last) ) = db:: runners:: erroring( & app.db, runner_id, n) .await else { return } ;
2758
let Ok ( Some ( r) ) = db:: runners:: get( & app.db, runner_id) .await else { return } ;
2759
if r.state ! = " active" | | db:: runners:: set_state( & app.db, runner_id, " draining" ) .await.is_err ( ) {
2760
return ;
2761
}
2762
tracing:: warn! ( " flow: runner {} drained after {n} errors in a row (last: {last})" , r.name) ;
2763
let Some ( me) = flow_account ( & app.db) .await else { return } ;
ea14c4ec Andy McCall 20 days ago ↶
2764
let admins: Vec< i64 > = sqlx:: query_scalar( " SELECT id FROM accounts WHERE is_admin = TRUE" ) .fetch_all ( & app.db) .await.unwrap_or_default ( ) ;
4a4400c5 Andy McCall 22 days ago ↶
2765
crate :: notify:: deliver(
2766
app,
2767
crate :: notify:: Event {
2768
kind: " flow.runner_drained" ,
2769
title: format! ( " Runner {} drained: its last {n} jobs ended in errors (last: {last} )" , r.name) ,
2770
url: format! ( " /admin/runners/{runner_id} " ) ,
2771
actor: & me,
2772
repo_id: 0 ,
2773
thread: None ,
2774
mention_text: " " ,
2775
direct: admins,
2776
} ,
2777
)
2778
.await;
070a7b64 Andy McCall 1 month ago ↶
2779
}
2780
5dead1f4 Andy McCall 1 month ago ↶
2781
2782
2783
pub struct Limits {
2784
2785
pub default_s : u64 ,
2786
2787
pub max_s : u64 ,
2788
2789
2790
2791
2792
pub grace_ms : i64 ,
2793
}
2794
2795
pub const SETTING_DEFAULT : & str = " flow.job_timeout_minutes" ;
2796
pub const SETTING_MAX : & str = " flow.job_timeout_max_minutes" ;
2797
pub const SETTING_GRACE : & str = " flow.lost_grace_minutes" ;
2798
2799
pub async fn limits ( app : & Shared) -> Limits {
2800
let minutes = | key : & 'static str | async move {
2801
db:: accounts:: setting( & app.db, key) .await.ok ( ) .flatten ( ) .and_then ( | v | v.parse:: < u64 > ( ) .ok ( ) ) .filter ( | m | * m > 0 )
2802
} ;
2803
let default_m = minutes ( SETTING_DEFAULT ) .await.unwrap_or ( app.config.flow.job_timeout_minutes.max ( 1 ) ) ;
2804
let max_m = minutes ( SETTING_MAX ) .await.unwrap_or ( default_m.max ( 6 * 60 ) ) .max ( default_m) ;
2805
let grace_m = minutes ( SETTING_GRACE ) .await.unwrap_or ( 5 ) ;
2806
Limits { default_s: default_m * 60 , max_s: max_m * 60 , grace_ms: grace_m as i64 * 60_000 }
2807
}
070a7b64 Andy McCall 1 month ago ↶
2808
5dead1f4 Andy McCall 1 month ago ↶
2809
070a7b64 Andy McCall 1 month ago ↶
2810
2811
2812
pub async fn reap_lost_jobs ( app : & Shared) -> sqlx:: Result< usize > {
5dead1f4 Andy McCall 1 month ago ↶
2813
let limits = limits ( app) .await;
2814
let lost = fdb:: running_past_limit( & app.db, db:: now_ms( ) , limits.default_s, limits.grace_ms) .await? ;
070a7b64 Andy McCall 1 month ago ↶
2815
let n = lost.len ( ) ;
2816
for job in lost {
2817
let error = format! (
2818
" lost: still running {} minutes after it started, past the time limit; the runner{} stopped reporting it" ,
2819
( db:: now_ms( ) - job.started_ms.unwrap_or ( 0 ) ) / 60_000 ,
2820
if job.runner.is_empty ( ) { String :: new( ) } else { format! ( " {} " , job.runner) } ,
2821
) ;
2822
let changed =
2823
fdb:: finish( & app.db, job.id, fdb:: Outcome { status: " error" , exit_code: None , error: & error, receipt: " " , receipt_hash: " " } )
2824
.await? ;
2825
tracing:: warn! ( " flow: job {} {error}" , job.id) ;
2826
report_changed ( app, job.run_id, changed) .await? ;
2827
}
2828
Ok ( n)
eef677c4 Andy McCall 1 month ago ↶
2829
}
2830
2831
2832
2833
2834
async fn end_jobs_not_held ( app : & Shared, runner : & db:: runners:: Runner, holding : & [i64 ]) -> sqlx:: Result< ( ) > {
2835
for job in fdb:: running_on( & app.db, runner.id) .await? {
2836
if holding.contains ( & job.id) {
2837
continue ;
2838
}
2839
let error = format! ( " lost: the runner {} no longer holds this job (it restarted, or never received it)" , runner.name) ;
2840
let changed =
2841
fdb:: finish( & app.db, job.id, fdb:: Outcome { status: " error" , exit_code: None , error: & error, receipt: " " , receipt_hash: " " } )
2842
.await? ;
2843
tracing:: warn! ( " flow: job {} {error}" , job.id) ;
2844
report_changed ( app, job.run_id, changed) .await? ;
2845
}
2846
Ok ( ( ) )
348f8c4b Andy McCall 1 month ago ↶
2847
}
2848
2849
2850
2851
2852
pub async fn run_due ( app : & Shared, when : flow_core:: pipeline:: When) -> usize {
2853
let Ok ( rows) = db:: repos:: list( & app.db) .await else { return 0 } ;
2854
let mut started = 0 ;
2855
for row in rows.into_iter ( ) .filter ( | r | enabled ( app, r) ) {
2856
let Some ( repo) = RepoName:: new( & row.owner, & row.name) else { continue } ;
2857
let b = row.default_bookmark.clone ( ) ;
2858
let found = app
2859
.repos
2860
.read ( & repo, move | r, _ | {
2861
let Some ( st) = r.view ( ) ? .bookmarks.get ( & b) .copied ( ) else { return Ok ( None ) } ;
2862
Ok ( Some ( ( st, r.store ( ) .get_state ( & st) ? .heads) ) )
2863
} )
2864
.await;
2865
let Ok ( Some ( ( state, heads) ) ) = found else { continue } ;
2866
if heads.is_empty ( ) {
2867
continue ;
2868
}
0650d94e Andy McCall 25 days ago ↶
2869
match start_run_at ( app, & row, & row.default_bookmark, & state.to_hex ( ) , & heads, & row.owner, " schedule" , None , Some ( when) , & [ ] ) .await {
348f8c4b Andy McCall 1 month ago ↶
2870
Ok ( Some ( _ ) ) => started + = 1 ,
2871
Ok ( None ) => { }
2872
Err ( e) => tracing:: warn! ( " flow: scheduled run of {}/{}: {e}" , row.owner, row.name) ,
2873
}
2874
}
2875
started
2876
}
2877
2878
2879
pub fn when_now ( ) -> flow_core:: pipeline:: When {
2880
let now = time:: OffsetDateTime:: now_utc( ) ;
2881
flow_core:: pipeline:: When {
2882
minute: now.minute ( ) as u32 ,
2883
hour: now.hour ( ) as u32 ,
2884
day: now.day ( ) as u32 ,
2885
month: u8 :: from( now.month ( ) ) as u32 ,
2886
weekday: now.weekday ( ) .number_days_from_sunday ( ) as u32 ,
2887
}
2888
}
2889
2890
2891
pub async fn scheduler ( app : Shared) {
2892
let mut last = None ;
2893
loop {
2894
tokio:: time:: sleep( std:: time:: Duration:: from_secs( 15 ) ) .await;
2895
let minute = time:: OffsetDateTime:: now_utc( ) .unix_timestamp ( ) / 60 ;
2896
if last = = Some ( minute) {
2897
continue ;
2898
}
2899
last = Some ( minute) ;
2900
let n = run_due ( & app, when_now ( ) ) .await;
2901
if n > 0 {
2902
tracing:: info! ( " flow: {n} scheduled run(s) started" ) ;
2903
}
2904
}
070a7b64 Andy McCall 1 month ago ↶
2905
}
2906
2907
2908
pub async fn reaper ( app : Shared) {
2909
loop {
2910
tokio:: time:: sleep( std:: time:: Duration:: from_secs( 60 ) ) .await;
ab4df6aa Andy McCall 10 days ago ↶
2911
if let Err ( e) = requeue_lapsed ( & app) .await {
2912
tracing:: warn! ( " flow: looking for lapsed leases: {e}" ) ;
2913
}
070a7b64 Andy McCall 1 month ago ↶
2914
if let Err ( e) = reap_lost_jobs ( & app) .await {
2915
tracing:: warn! ( " flow: looking for lost jobs: {e}" ) ;
417184c7 Andy McCall 20 days ago ↶
2916
}
2917
match expire_approvals ( & app, db:: now_ms( ) ) .await {
2918
Ok ( n) if n > 0 => tracing:: info! ( " flow: {n} approvals expired" ) ,
2919
Ok ( _ ) => { }
2920
Err ( e) => tracing:: warn! ( " flow: expiring approvals: {e}" ) ,
070a7b64 Andy McCall 1 month ago ↶
2921
}
2922
}
cc19697f Andy McCall 1 month ago ↶
2923
}
2924
2925
2926
2927
2928
2929
2930
pub async fn keep_receipt (
2931
app : & Shared,
b6b7e4cf Andy McCall 1 month ago ↶
2932
runner : & db:: runners:: Runner,
cc19697f Andy McCall 1 month ago ↶
2933
row : & db:: repos:: RepoRow,
2934
run : & fdb:: Run,
2935
receipt : & flow_core:: Receipt,
2936
signed_hex : & str ,
2937
) -> Result< ( String, String) , String> {
2938
use oxy_core:: receipt:: SignedReceipt;
2939
let bytes = hex:: decode( signed_hex) .map_err ( | _| " the signed receipt is not hex" .to_string ( ) ) ? ;
2940
let sr = SignedReceipt:: decode( & bytes) .map_err ( | e | format! ( " not a signed receipt: {e} " ) ) ? ;
2941
sr.verify ( ) .map_err ( | e | format! ( " it does not verify: {e} " ) ) ? ;
2942
if sr.receipt.hash ( ) .0 ! = receipt.hash ( ) {
2943
return Err ( " it signs another receipt than the job's" .into ( ) ) ;
2944
}
2945
let heads: Vec< oxy_core:: hash:: ChangeHash> = run.head_list ( ) .into_iter ( ) .filter_map ( oxy_core:: hash:: ChangeHash:: from_hex) .collect ( ) ;
2946
let state = oxy_core:: hash:: hash_object( oxy_core:: hash:: ObjectType:: State, & oxy_core:: objects:: StateObject:: new( heads) .encode ( ) ) ;
2947
if sr.state ( ) ! = state {
2948
return Err ( " it is for another state than the run's" .into ( ) ) ;
2949
}
2c31915f Andy McCall 18 days ago ↶
2950
let signer = match & sr.certificate {
2951
2952
2953
Some ( c) => {
2954
let cert = & c.cert.cert;
2955
if cert.issuer ! = app.hub_key.key | | c.issuer.hash ( ) ! = app.hub_key.key {
2956
return Err ( " its certificate was not issued by this hub" .into ( ) ) ;
2957
}
2958
let subject = format! ( " repo:{} /{} :bookmark:{} " , row.owner, row.name, run.bookmark) ;
2959
if cert.subject ! = subject {
2960
return Err ( format! ( " its certificate is for {} , not {subject} " , cert.subject) ) ;
2961
}
2962
if cert.claim ( " run" ) ! = Some ( run.number.to_string ( ) .as_str ( ) ) | | cert.claim ( " job" ) ! = Some ( sr.receipt.job_name.as_str ( ) ) {
2963
return Err ( " its certificate is for another run or job" .into ( ) ) ;
2964
}
2965
2966
if ! cert.covers ( oxy_core:: identity:: Scope:: SEAL , db:: now_ms( ) as u64 ) {
2967
return Err ( " its certificate does not cover now: certify the key just before signing the receipt" .into ( ) ) ;
2968
}
2969
format! ( " {} (short-lived key)" , runner.name)
2970
}
2971
None => {
2972
let key = sr.runner.hash ( ) .to_hex ( ) ;
2973
let bound = db:: runners:: bind_key( & app.db, runner.id, & key, & sr.runner.name) .await.map_err ( | e | e.to_string ( ) ) ? ;
2974
if bound ! = key {
2975
return Err ( format! ( " signed by {} , not the key this runner signs with ({} )" , & key[ .. 12 ] , & bound[ .. 12. min ( bound.len ( ) ) ] ) ) ;
2976
}
2977
sr.runner.name.clone ( )
2978
}
2979
} ;
cc19697f Andy McCall 1 month ago ↶
2980
let name = RepoName:: new( & row.owner, & row.name) .ok_or ( " bad repository name" ) ? ;
965b950f Andy McCall 18 days ago ↶
2981
let ( rid, signers) = ( sr.attest.repo, sr.sigs.iter ( ) .map ( | s | s.key ) .collect:: < Vec< _ > > ( ) ) ;
2982
let cert = sr.certificate.as_ref ( ) .map ( | c | ( c.cert.cert.hash ( ) , c.cert.cert.issuer) ) ;
cc19697f Andy McCall 1 month ago ↶
2983
let attest = app.repos.write ( & name, move | repo, _ | Ok ( repo.add_receipt ( & sr) ? ) ) .await.map_err ( | e | e.to_string ( ) ) ? ;
965b950f Andy McCall 18 days ago ↶
2984
2985
2986
let mut entries = vec! [ crate :: tlog:: Pending { kind: " receipt" , repo: rid, repo_row: Some ( row.id) , object: attest, signers } ] ;
2987
if let Some ( ( c, issuer) ) = cert {
2988
entries.push ( crate :: tlog:: Pending { kind: " certificate" , repo: rid, repo_row: Some ( row.id) , object: c, signers: vec! [ issuer] } ) ;
2989
}
2990
crate :: tlog:: log( app, entries) .await;
cc19697f Andy McCall 1 month ago ↶
2991
Ok ( ( attest.to_hex ( ) , signer) )
42fa9554 Andy McCall 1 month ago ↶
2992
}
2993
2994
2995
pub fn duration ( j : & fdb:: Job) -> String {
2996
match ( j.started_ms, j.finished_ms) {
2997
( Some ( a) , Some ( b) ) => human_ms ( b - a) ,
2998
( Some ( a) , None ) => human_ms ( db:: now_ms( ) - a) ,
2999
_ => " –" .into ( ) ,
3000
}
3001
}
3002
3003
pub fn human_ms ( ms : i64 ) -> String {
3004
let ms = ms.max ( 0 ) ;
3005
if ms < 1000 {
3006
format! ( " {ms} ms" )
3007
} else if ms < 60_000 {
3008
format! ( " {:.1} s" , ms as f64 / 1000. 0 )
3009
} else {
3010
format! ( " {} min {} s" , ms / 60_000 , ( ms % 60_000 ) / 1000 )
3011
}
3012
}
bf3d0754 Andy McCall 1 month ago ↶
3013
3014
3015
3016
3017
# [ derive ( Clone, Debug, Default, PartialEq, Eq ) ]
3018
pub struct QueueInfo {
3019
3020
pub position : Option< usize > ,
3021
3022
pub running : Vec< String> ,
3023
pub ahead : Vec< String> ,
3024
3025
pub hidden : usize ,
3026
}
3027
3028
impl QueueInfo {
3029
3030
pub fn place ( & self ) -> Option< String> {
3031
self .position.map ( | n | {
3032
let suffix = match ( n % 10 , n % 100 ) {
3033
( 1 , 11 ) | ( 2 , 12 ) | ( 3 , 13 ) => " th" ,
3034
( 1 , _ ) => " st" ,
3035
( 2 , _ ) => " nd" ,
3036
( 3 , _ ) => " rd" ,
3037
_ => " th" ,
3038
} ;
3039
format! ( " {n} {suffix} in line" )
3040
} )
3041
}
3042
}
3043
3044
3045
pub fn queue_info ( queue : & [fdb:: QueueEntry], repo_id : i64 , run_id : i64 ) -> QueueInfo {
3046
let mut info = QueueInfo:: default( ) ;
3047
let first = queue.iter ( ) .filter ( | e | e.status = = " queued" ) .position ( | e | e.run_id = = run_id ) ;
3048
info.position = first.map ( | i | i + 1 ) ;
3049
let label = | e : & fdb:: QueueEntry| format! ( " {} /{} #{} {} " , e.owner, e.name, e.run_number, e.job) ;
3050
let mut queued_seen = 0 ;
3051
for e in queue {
3052
if e.run_id = = run_id {
3053
continue ;
3054
}
3055
let ahead = match e.status.as_str ( ) {
3056
" running" => true ,
3057
_ => {
3058
queued_seen + = 1 ;
3059
first.is_some_and ( | f | queued_seen < = f )
3060
}
3061
} ;
3062
if ! ahead {
3063
continue ;
3064
}
3065
if e.repo_id ! = repo_id & & e.visibility ! = " public" {
3066
info.hidden + = 1 ;
3067
} else if e.status = = " running" {
3068
info.running.push ( label ( e) ) ;
3069
} else {
3070
info.ahead.push ( label ( e) ) ;
3071
}
3072
}
3073
info
3074
}
3075
3076
# [ cfg ( test ) ]
3077
mod queue_tests {
3078
use super :: * ;
3079
3080
fn e ( job_id : i64 , run_id : i64 , repo_id : i64 , vis : & str , job : & str , status : & str ) -> fdb:: QueueEntry {
3081
fdb:: QueueEntry {
3082
job_id,
3083
run_id,
3084
repo_id,
3085
owner: " o" .into ( ) ,
3086
name: format! ( " r{repo_id} " ) ,
3087
visibility: vis.into ( ) ,
3088
run_number: run_id,
3089
job: job.into ( ) ,
3090
status: status.into ( ) ,
b66e70b7 Andy McCall 1 month ago ↶
3091
runner: String :: new( ) ,
c4c87f48 Andy McCall 1 month ago ↶
3092
spec: String :: new( ) ,
3093
sandbox: String :: new( ) ,
e9b9619c Andy McCall 19 days ago ↶
3094
started_ms: None ,
107eaba5 Andy McCall 19 days ago ↶
3095
priority_rank: 2 ,
3096
priority_note: String :: new( ) ,
bf3d0754 Andy McCall 1 month ago ↶
3097
}
3098
}
3099
3100
# [ test ]
3101
fn a_run_learns_its_place_and_what_is_ahead_of_it ( ) {
3102
3103
let q = vec! [
3104
e ( 1 , 10 , 1 , " public" , " build" , " running" ) ,
3105
e ( 2 , 11 , 2 , " private" , " test" , " running" ) ,
3106
e ( 3 , 10 , 1 , " public" , " test" , " queued" ) ,
3107
e ( 4 , 12 , 3 , " private" , " lint" , " queued" ) ,
3108
e ( 5 , 13 , 4 , " public" , " build" , " queued" ) ,
3109
e ( 6 , 13 , 4 , " public" , " test" , " queued" ) ,
3110
e ( 7 , 14 , 5 , " public" , " late" , " queued" ) ,
3111
] ;
3112
let info = queue_info ( & q, 4 , 13 ) ;
3113
assert_eq! ( info.position, Some ( 3 ) ) ;
3114
assert_eq! ( info.place ( ) .as_deref ( ) , Some ( " 3rd in line" ) ) ;
3115
assert_eq! ( info.running, [ " o/r1 #10 build" ] ) ;
3116
assert_eq! ( info.ahead, [ " o/r1 #10 test" ] , " queued ahead, not behind" ) ;
3117
assert_eq! ( info.hidden, 2 , " private repositories are counted, not named" ) ;
3118
3119
assert_eq! ( queue_info ( & q, 3 , 12 ) .ahead, [ " o/r1 #10 test" ] ) ;
3120
assert_eq! ( queue_info ( & q, 3 , 12 ) .position, Some ( 2 ) ) ;
3121
3122
assert_eq! ( queue_info ( & q, 1 , 99 ) .position, None ) ;
3123
let ordinal = | n | QueueInfo { position: Some ( n) , .. Default :: default( ) } .place ( ) .unwrap ( ) ;
3124
assert_eq! (
3125
[ ordinal ( 1 ) , ordinal ( 2 ) , ordinal ( 11 ) , ordinal ( 22 ) , ordinal ( 113 ) ] ,
3126
[ " 1st in line" , " 2nd in line" , " 11th in line" , " 22nd in line" , " 113th in line" ]
4d2e44d7 Andy McCall 1 month ago ↶
3127
) ;
3128
}
3129
}
3130
3131
# [ cfg ( test ) ]
3132
mod placement_tests {
3133
use super :: * ;
3134
3135
# [ test ]
3136
fn a_waiting_job_says_what_each_runner_lacks ( ) {
3137
use crate :: db:: runners:: Runner;
3138
let rootfs = " ab" .repeat ( 32 ) ;
3139
let job = fdb:: Job {
3140
id: 7 ,
3141
runtime: " syscall" .into ( ) ,
3142
sandbox: serde_json:: json! ( { " rootfs" : rootfs, " memory_mib" : 4096 , " cpus" : 2 } ) .to_string ( ) ,
3143
spec: serde_json:: json! ( { " name" : " build" } ) .to_string ( ) ,
3144
.. Default :: default( )
3145
} ;
3146
let now = db:: now_ms( ) ;
3147
let runner = | id : i64 , name : & str | Runner {
3148
id,
3149
name: name.into ( ) ,
3150
state: " active" .into ( ) ,
3151
last_seen_ms: Some ( now) ,
3152
arch: " x86_64" .into ( ) ,
3153
tiers: " microvm,syscall" .into ( ) ,
3154
images: serde_json:: to_string( & [ & rootfs] ) .unwrap ( ) ,
3155
memory_mib: Some ( 16384 ) ,
3156
cpus: Some ( 8 ) ,
3157
.. Default :: default( )
3158
} ;
3159
let archs = std:: collections:: HashMap:: new( ) ;
92a797b2 Andy McCall 24 days ago ↶
3160
let why = | runners : & [Runner], declines : & [fdb:: DeclineRow]| {
3161
why_waiting ( & job, " andy" , 1 , runners, & archs, declines, now, & Default :: default( ) )
3162
} ;
4d2e44d7 Andy McCall 1 month ago ↶
3163
assert_eq! ( why ( & [ ] , & [ ] ) , " no runner is online" ) ;
3164
assert_eq! ( why ( & [ runner ( 1 , " earth-1" ) ] , & [ ] ) , " " , " one can take it: it only waits its turn" ) ;
3165
let microvm_only = Runner { tiers: " microvm" .into ( ) , .. runner ( 1 , " earth-1" ) } ;
3166
let riscv = Runner { arch: " riscv64" .into ( ) , .. runner ( 2 , " narvi" ) } ;
3167
let small = Runner { memory_mib: Some ( 2048 ) , .. runner ( 3 , " pi" ) } ;
3168
let other = Runner { scope_owner: " bob" .into ( ) , .. runner ( 4 , " bobs" ) } ;
3169
let offline = Runner { last_seen_ms: Some ( now - 3_600_000 ) , .. runner ( 5 , " gone" ) } ;
3170
assert_eq! (
3171
why ( & [ microvm_only, small, other, offline] , & [ ] ) ,
3172
" no runner can take it: earth-1 has no syscall tier (it offers microvm); pi has 2048 MiB; the job needs 4096 MiB; bobs serves other repositories"
3173
) ;
3174
let archs_known: std:: collections:: HashMap< String, String> = [ ( rootfs.clone ( ) , " x86_64" .to_string ( ) ) ] .into ( ) ;
3175
assert_eq! (
92a797b2 Andy McCall 24 days ago ↶
3176
why_waiting ( & job, " andy" , 1 , & [ riscv] , & archs_known, & [ ] , now, & Default :: default( ) ) ,
4d2e44d7 Andy McCall 1 month ago ↶
3177
" no runner can take it: narvi is riscv64; the job needs x86_64"
3178
) ;
3179
let without = Runner { images: " []" .into ( ) , .. runner ( 6 , " new" ) } ;
56b8dbe6 Andy McCall 10 days ago ↶
3180
assert_eq! ( why ( std:: slice:: from_ref( & without) , & [ ] ) , " no runner can take it: new does not hold image abababababab" ) ;
3181
3182
let wasm = fdb:: Job { runtime: " wasm" .into ( ) , sandbox: r # "{"memory_mib":256}"# .into ( ) , .. job.clone ( ) } ;
3183
let wasm_runner = Runner { tiers: " microvm,wasm" .into ( ) , .. without } ;
3184
assert_eq! ( why_waiting ( & wasm, " andy" , 1 , & [ wasm_runner] , & archs, & [ ] , now, & Default :: default( ) ) , " " , " it only waits its turn" ) ;
4d2e44d7 Andy McCall 1 month ago ↶
3185
let d = fdb:: DeclineRow {
3186
job_id: 7 ,
3187
runner_id: 1 ,
3188
runner: " earth-1" .into ( ) ,
3189
reason: " only 512 MiB of memory is free here" .into ( ) ,
3190
ms: now,
3191
} ;
3192
assert_eq! (
3193
why ( & [ runner ( 1 , " earth-1" ) ] , std:: slice:: from_ref( & d) ) ,
3194
" no runner can take it: earth-1 declined it: only 512 MiB of memory is free here"
bf3d0754 Andy McCall 1 month ago ↶
3195
) ;
4d2e44d7 Andy McCall 1 month ago ↶
3196
let old = fdb:: DeclineRow { ms: now - fdb:: DECLINE_HOLDS_MS - 1 , .. d } ;
3197
assert_eq! ( why ( & [ runner ( 1 , " earth-1" ) ] , & [ old] ) , " " , " an old decline no longer holds" ) ;
bf3d0754 Andy McCall 1 month ago ↶
3198
}
3199
}