b6b7e4cf Andy McCall 1 month ago ↶
1
2
3
4
use super :: { now_ms, Pool} ;
5
use sqlx:: FromRow;
6
4d2e44d7 Andy McCall 1 month ago ↶
7
# [ derive ( Clone, Debug, Default, FromRow ) ]
b6b7e4cf Andy McCall 1 month ago ↶
8
pub struct Runner {
9
pub id : i64 ,
10
pub name : String,
11
pub token_hash : String,
12
pub source : String,
13
pub key : String,
14
pub key_name : String,
15
pub arch : String,
16
pub os : String,
17
pub version : String,
18
pub labels : String,
19
pub max_jobs : i64 ,
20
pub memory_mib : Option< i64 > ,
21
pub cpus : Option< i64 > ,
22
pub state : String,
23
pub created_ms : i64 ,
24
pub created_by : String,
25
pub last_seen_ms : Option< i64 > ,
74be9796 Andy McCall 1 month ago ↶
26
27
pub scope_owner : String,
28
29
pub scope_repo_id : Option< i64 > ,
7f5f6c05 Andy McCall 1 month ago ↶
30
31
pub admin_labels : String,
32
33
pub timeout_minutes : Option< i64 > ,
92a797b2 Andy McCall 24 days ago ↶
34
35
pub fetches_images : bool ,
2fb70d01 Andy McCall 24 days ago ↶
36
37
38
pub free_disk_mib : Option< i64 > ,
39
pub load_x100 : Option< i64 > ,
40
pub speed : Option< i64 > ,
41
pub speed_class : Option< i64 > ,
42
pub kernel : String,
4d2e44d7 Andy McCall 1 month ago ↶
43
44
pub tiers : String,
45
46
pub images : String,
47
48
pub profile : String,
8501fd58 Andy McCall 10 days ago ↶
49
50
pub update_note : String,
51
pub update_ms : Option< i64 > ,
b6b7e4cf Andy McCall 1 month ago ↶
52
}
53
74be9796 Andy McCall 1 month ago ↶
54
55
# [ derive ( Clone, Debug, Default, PartialEq, Eq ) ]
56
pub struct Scope {
57
58
pub owner : String,
59
60
pub repo_id : Option< i64 > ,
61
}
62
63
impl Scope {
64
pub fn is_site ( & self ) -> bool {
65
self .owner.is_empty ( ) & & self .repo_id.is_none ( )
66
}
67
}
68
b6b7e4cf Andy McCall 1 month ago ↶
69
70
pub const ONLINE_MS : i64 = 90_000 ;
71
72
impl Runner {
73
pub fn online ( & self ) -> bool {
74
self .last_seen_ms.is_some_and ( | t | now_ms ( ) - t < ONLINE_MS )
75
}
76
e9b9619c Andy McCall 19 days ago ↶
77
78
79
pub fn connectivity ( & self ) -> & 'static str {
80
if self .online ( ) {
81
" online"
82
} else {
83
" offline"
84
}
85
}
86
b6b7e4cf Andy McCall 1 month ago ↶
87
88
pub fn status ( & self ) -> & 'static str {
89
match self .state.as_str ( ) {
90
" disabled" => " disabled" ,
91
" draining" => " draining" ,
92
_ if self .online ( ) => " online" ,
93
_ => " offline" ,
94
}
95
}
96
7f5f6c05 Andy McCall 1 month ago ↶
97
98
b6b7e4cf Andy McCall 1 month ago ↶
99
pub fn label_list ( & self ) -> Vec< & str > {
7f5f6c05 Andy McCall 1 month ago ↶
100
let mut out: Vec< & str > = self .labels.split ( ' ,' ) .filter ( | l | ! l.is_empty ( ) ) .collect ( ) ;
101
for l in self .admin_label_list ( ) {
102
if ! out.contains ( & l) {
103
out.push ( l) ;
104
}
105
}
106
out
107
}
108
109
pub fn admin_label_list ( & self ) -> Vec< & str > {
110
self .admin_labels.split ( ' ,' ) .filter ( | l | ! l.is_empty ( ) ) .collect ( )
111
}
112
113
114
pub fn reported_label_list ( & self ) -> Vec< & str > {
b6b7e4cf Andy McCall 1 month ago ↶
115
self .labels.split ( ' ,' ) .filter ( | l | ! l.is_empty ( ) ) .collect ( )
116
}
74be9796 Andy McCall 1 month ago ↶
117
118
pub fn scope ( & self ) -> Scope {
119
Scope { owner: self .scope_owner.clone ( ) , repo_id: self .scope_repo_id }
120
}
b6b7e4cf Andy McCall 1 month ago ↶
121
}
122
123
pub async fn by_token ( pool : & Pool, token_hash : & str ) -> sqlx:: Result< Option< Runner> > {
5d90e0e0 Andy McCall 20 days ago
124
sqlx:: query_as( " SELECT * FROM flow_runners WHERE token_hash = $1" ) .bind ( token_hash) .fetch_optional ( pool) .await
b6b7e4cf Andy McCall 1 month ago ↶
125
}
126
127
pub async fn get ( pool : & Pool, id : i64 ) -> sqlx:: Result< Option< Runner> > {
5d90e0e0 Andy McCall 20 days ago
128
sqlx:: query_as( " SELECT * FROM flow_runners WHERE id = $1" ) .bind ( id) .fetch_optional ( pool) .await
b6b7e4cf Andy McCall 1 month ago ↶
129
}
130
131
pub async fn list ( pool : & Pool) -> sqlx:: Result< Vec< Runner> > {
132
sqlx:: query_as( " SELECT * FROM flow_runners ORDER BY name, id" ) .fetch_all ( pool) .await
133
}
134
135
136
pub async fn configured ( pool : & Pool, token_hash : & str , name : & str ) -> sqlx:: Result< Runner> {
d1bf2d69 Andy McCall 20 days ago ↶
137
sqlx:: query( " INSERT INTO flow_runners (name, token_hash, source, created_ms) VALUES ($1, $2, 'config', $3) ON CONFLICT DO NOTHING" )
b6b7e4cf Andy McCall 1 month ago ↶
138
.bind ( name)
139
.bind ( token_hash)
140
.bind ( now_ms ( ) )
141
.execute ( pool)
142
.await? ;
143
Ok ( by_token ( pool, token_hash) .await? .expect ( " just made" ) )
144
}
145
146
147
7f5f6c05 Andy McCall 1 month ago ↶
148
149
pub async fn set_limits ( pool : & Pool, id : i64 , admin_labels : & str , timeout_minutes : Option< i64 > ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
150
sqlx:: query( " UPDATE flow_runners SET admin_labels = $1, timeout_minutes = $2 WHERE id = $3" )
7f5f6c05 Andy McCall 1 month ago ↶
151
.bind ( admin_labels)
152
.bind ( timeout_minutes)
153
.bind ( id)
154
.execute ( pool)
155
.await? ;
156
Ok ( ( ) )
157
}
158
b6b7e4cf Andy McCall 1 month ago ↶
159
pub async fn seen ( pool : & Pool, id : i64 , name : Option< & str > , info : Option< & flow_core:: api:: RunnerInfo> ) -> sqlx:: Result< ( ) > {
160
let now = now_ms ( ) ;
161
match info {
162
Some ( i) => {
163
sqlx:: query(
5d90e0e0 Andy McCall 20 days ago
164
" UPDATE flow_runners SET arch = $1, os = $2, version = $3, labels = $4, max_jobs = $5, memory_mib = $6, cpus = $7, last_seen_ms = $8 WHERE id = $9" ,
b6b7e4cf Andy McCall 1 month ago ↶
165
)
166
.bind ( & i.arch)
167
.bind ( & i.os)
168
.bind ( & i.version)
169
.bind ( i.labels.join ( " ," ) )
170
.bind ( i.max_jobs as i64 )
171
.bind ( i.memory_mib.map ( i64 :: from) )
172
.bind ( i.cpus.map ( i64 :: from) )
173
.bind ( now)
174
.bind ( id)
175
.execute ( pool)
176
.await? ;
177
}
178
None => {
5d90e0e0 Andy McCall 20 days ago
179
sqlx:: query( " UPDATE flow_runners SET last_seen_ms = $1 WHERE id = $2" ) .bind ( now) .bind ( id) .execute ( pool) .await? ;
b6b7e4cf Andy McCall 1 month ago ↶
180
}
181
}
182
if let Some ( n) = name {
5d90e0e0 Andy McCall 20 days ago
183
sqlx:: query( " UPDATE flow_runners SET name = $1 WHERE id = $2 AND source = 'config'" ) .bind ( n) .bind ( id) .execute ( pool) .await? ;
b6b7e4cf Andy McCall 1 month ago ↶
184
}
4d2e44d7 Andy McCall 1 month ago ↶
185
Ok ( ( ) )
186
}
187
2fb70d01 Andy McCall 24 days ago ↶
188
189
pub async fn set_facts ( pool : & Pool, id : i64 , f : & flow_core:: api:: Facts) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
190
sqlx:: query( " UPDATE flow_runners SET free_disk_mib = $1, load_x100 = $2, speed = $3, speed_class = $4, kernel = $5 WHERE id = $6" )
2fb70d01 Andy McCall 24 days ago ↶
191
.bind ( f.free_disk_mib.map ( | v | v.min ( i64 :: MAX as u64 ) as i64 ) )
192
.bind ( f.load_x100.map ( i64 :: from) )
193
.bind ( f.speed.map ( i64 :: from) )
194
.bind ( f.speed_class.map ( i64 :: from) )
195
.bind ( f.kernel.clone ( ) .unwrap_or_default ( ) )
196
.bind ( id)
197
.execute ( pool)
198
.await? ;
199
Ok ( ( ) )
200
}
201
202
impl Runner {
203
204
205
pub fn facts_line ( & self ) -> String {
206
let mut out = Vec :: new( ) ;
207
if let Some ( s) = self .speed {
208
out.push ( format! ( " speed {s} (class {} )" , self .speed_class.unwrap_or ( 0 ) ) ) ;
209
}
210
if let Some ( l) = self .load_x100 {
211
out.push ( format! ( " load {:.2} " , l as f64 / 100. 0 ) ) ;
212
}
213
if let Some ( d) = self .free_disk_mib {
214
out.push ( if d > = 1024 { format! ( " {} GiB free" , d / 1024 ) } else { format! ( " {d} MiB free" ) } ) ;
215
}
216
out.join ( " · " )
217
}
218
}
219
4d2e44d7 Andy McCall 1 month ago ↶
220
221
222
92a797b2 Andy McCall 24 days ago ↶
223
pub async fn set_offer (
224
pool : & Pool,
225
id : i64 ,
226
tiers : & [String],
227
images : Option< & [ String] > ,
228
fetches_images : bool ,
229
profile : & str ,
230
) -> sqlx:: Result< ( ) > {
4d2e44d7 Andy McCall 1 month ago ↶
231
let images = images.map ( | i | serde_json:: to_string( i) .unwrap_or_default ( ) ) .unwrap_or_default ( ) ;
5d90e0e0 Andy McCall 20 days ago
232
sqlx:: query( " UPDATE flow_runners SET tiers = $1, images = $2, fetches_images = $3, profile = $4 WHERE id = $5" )
4d2e44d7 Andy McCall 1 month ago ↶
233
.bind ( tiers.join ( " ," ) )
234
.bind ( images)
92a797b2 Andy McCall 24 days ago ↶
235
.bind ( fetches_images)
4d2e44d7 Andy McCall 1 month ago ↶
236
.bind ( profile)
237
.bind ( id)
238
.execute ( pool)
239
.await? ;
b6b7e4cf Andy McCall 1 month ago ↶
240
Ok ( ( ) )
4d2e44d7 Andy McCall 1 month ago ↶
241
}
242
243
impl Runner {
244
pub fn tier_list ( & self ) -> Vec< & str > {
245
self .tiers.split ( ' ,' ) .filter ( | t | ! t.is_empty ( ) ) .collect ( )
246
}
247
248
249
pub fn image_list ( & self ) -> Option< Vec< String> > {
250
if self .images.is_empty ( ) {
251
None
252
} else {
253
serde_json:: from_str( & self .images) .ok ( )
254
}
255
}
b6b7e4cf Andy McCall 1 month ago ↶
256
}
257
258
pub async fn set_state ( pool : & Pool, id : i64 , state : & str ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
259
sqlx:: query( " UPDATE flow_runners SET state = $1, state_ms = $2 WHERE id = $3" )
4a4400c5 Andy McCall 22 days ago ↶
260
.bind ( state)
261
.bind ( super :: now_ms( ) )
262
.bind ( id)
263
.execute ( pool)
264
.await? ;
b6b7e4cf Andy McCall 1 month ago ↶
265
Ok ( ( ) )
4a4400c5 Andy McCall 22 days ago ↶
266
}
267
268
269
270
pub async fn erroring ( pool : & Pool, id : i64 , n : u32 ) -> sqlx:: Result< Option< String> > {
271
if n = = 0 {
272
return Ok ( None ) ;
273
}
274
let last: Vec< ( String, String) > = sqlx:: query_as(
275
" SELECT j.status, j.error FROM flow_jobs j JOIN flow_runners r ON r.id = j.runner_id
5d90e0e0 Andy McCall 20 days ago
276
WHERE j.runner_id = $1 AND j.finished_ms IS NOT NULL AND j.finished_ms >= r.state_ms
277
ORDER BY j.finished_ms DESC, j.id DESC LIMIT $2" ,
4a4400c5 Andy McCall 22 days ago ↶
278
)
279
.bind ( id)
280
.bind ( n as i64 )
281
.fetch_all ( pool)
282
.await? ;
283
Ok ( ( last.len ( ) = = n as usize & & last.iter ( ) .all ( | ( s , _) | s = = " error" ) ) .then ( | | last[ 0 ] .1. clone ( ) ) )
b6b7e4cf Andy McCall 1 month ago ↶
284
}
285
286
287
pub async fn remove ( pool : & Pool, id : i64 ) -> sqlx:: Result< ( ) > {
8501fd58 Andy McCall 10 days ago ↶
288
sqlx:: query( " DELETE FROM runner_group_members WHERE runner_id = $1" ) .bind ( id) .execute ( pool) .await? ;
289
sqlx:: query( " DELETE FROM runner_update_policies WHERE scope = 'runner' AND key = $1" ) .bind ( id.to_string ( ) ) .execute ( pool) .await? ;
5d90e0e0 Andy McCall 20 days ago
290
sqlx:: query( " DELETE FROM flow_runners WHERE id = $1" ) .bind ( id) .execute ( pool) .await? ;
b6b7e4cf Andy McCall 1 month ago ↶
291
Ok ( ( ) )
292
}
293
294
295
296
pub async fn bind_key ( pool : & Pool, id : i64 , key : & str , name : & str ) -> sqlx:: Result< String> {
5d90e0e0 Andy McCall 20 days ago
297
sqlx:: query( " UPDATE flow_runners SET key = $1, key_name = $2 WHERE id = $3 AND key = ''" )
b6b7e4cf Andy McCall 1 month ago ↶
298
.bind ( key)
299
.bind ( name)
300
.bind ( id)
301
.execute ( pool)
302
.await? ;
5d90e0e0 Andy McCall 20 days ago
303
sqlx:: query_scalar( " SELECT key FROM flow_runners WHERE id = $1" ) .bind ( id) .fetch_one ( pool) .await
b6b7e4cf Andy McCall 1 month ago ↶
304
}
305
306
307
pub async fn keys ( pool : & Pool) -> sqlx:: Result< Vec< String> > {
308
sqlx:: query_scalar( " SELECT key FROM flow_runners WHERE key != ''" ) .fetch_all ( pool) .await
309
}
310
74be9796 Andy McCall 1 month ago ↶
311
312
pub async fn add_registration ( pool : & Pool, token_hash : & str , by : & str , scope : & Scope) -> sqlx:: Result< ( ) > {
b6b7e4cf Andy McCall 1 month ago ↶
313
let now = now_ms ( ) ;
74be9796 Andy McCall 1 month ago ↶
314
sqlx:: query(
5d90e0e0 Andy McCall 20 days ago
315
" INSERT INTO flow_runner_registrations (token_hash, created_by, created_ms, expires_ms, scope_owner, scope_repo_id) VALUES ($1, $2, $3, $4, $5, $6)" ,
74be9796 Andy McCall 1 month ago ↶
316
)
317
.bind ( token_hash)
318
.bind ( by)
319
.bind ( now)
320
.bind ( now + 24 * 3_600_000 )
321
.bind ( & scope.owner)
322
.bind ( scope.repo_id)
323
.execute ( pool)
324
.await? ;
325
Ok ( ( ) )
326
}
327
7974adb0 Andy McCall 1 month ago ↶
328
329
330
pub async fn serving ( pool : & Pool, owner : & str , repo_id : i64 ) -> sqlx:: Result< Vec< Runner> > {
331
sqlx:: query_as(
332
" SELECT * FROM flow_runners WHERE (scope_owner = '' AND scope_repo_id IS NULL) \
5d90e0e0 Andy McCall 20 days ago
333
OR (scope_owner = $1 AND scope_repo_id IS NULL) OR scope_repo_id = $2 ORDER BY name" ,
7974adb0 Andy McCall 1 month ago ↶
334
)
335
.bind ( owner)
336
.bind ( repo_id)
337
.fetch_all ( pool)
338
.await
339
}
340
341
342
343
pub async fn serving_owner ( pool : & Pool, owner : & str ) -> sqlx:: Result< Vec< Runner> > {
5d90e0e0 Andy McCall 20 days ago
344
sqlx:: query_as( " SELECT * FROM flow_runners WHERE (scope_owner = '' AND scope_repo_id IS NULL) OR scope_owner = $1 ORDER BY name" )
7974adb0 Andy McCall 1 month ago ↶
345
.bind ( owner)
346
.fetch_all ( pool)
347
.await
348
}
349
74be9796 Andy McCall 1 month ago ↶
350
351
pub async fn set_scope ( pool : & Pool, id : i64 , scope : & Scope) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
352
sqlx:: query( " UPDATE flow_runners SET scope_owner = $1, scope_repo_id = $2 WHERE id = $3" )
74be9796 Andy McCall 1 month ago ↶
353
.bind ( & scope.owner)
354
.bind ( scope.repo_id)
355
.bind ( id)
b6b7e4cf Andy McCall 1 month ago ↶
356
.execute ( pool)
357
.await? ;
358
Ok ( ( ) )
359
}
360
361
362
pub async fn pending_registrations ( pool : & Pool) -> sqlx:: Result< Vec< ( String, i64 , i64 ) > > {
5d90e0e0 Andy McCall 20 days ago
363
sqlx:: query_as( " SELECT created_by, created_ms, expires_ms FROM flow_runner_registrations WHERE used_ms IS NULL AND expires_ms > $1 ORDER BY created_ms DESC" )
b6b7e4cf Andy McCall 1 month ago ↶
364
.bind ( now_ms ( ) )
365
.fetch_all ( pool)
366
.await
367
}
368
369
370
371
# [ allow ( clippy::too_many_arguments ) ]
372
pub async fn register (
373
pool : & Pool,
374
registration : & str ,
375
name : & str ,
376
token_hash : & str ,
377
key : & str ,
378
key_name : & str ,
379
by_default : & str ,
380
) -> sqlx:: Result< Option< Runner> > {
381
let mut tx = super :: begin_write( pool) .await? ;
382
let now = now_ms ( ) ;
74be9796 Andy McCall 1 month ago ↶
383
let found: Option< ( String, String, Option< i64 > ) > = sqlx:: query_as(
5d90e0e0 Andy McCall 20 days ago
384
" SELECT created_by, scope_owner, scope_repo_id FROM flow_runner_registrations WHERE token_hash = $1 AND used_ms IS NULL AND expires_ms > $2" ,
74be9796 Andy McCall 1 month ago ↶
385
)
386
.bind ( registration)
387
.bind ( now)
388
.fetch_optional ( & mut * tx)
389
.await? ;
390
let Some ( ( who, scope_owner, scope_repo_id) ) = found else { return Ok ( None ) } ;
b6b7e4cf Andy McCall 1 month ago ↶
391
let id: i64 = sqlx:: query_scalar(
5d90e0e0 Andy McCall 20 days ago
392
" INSERT INTO flow_runners (name, token_hash, source, key, key_name, created_ms, created_by, scope_owner, scope_repo_id) VALUES ($1, $2, 'registered', $3, $4, $5, $6, $7, $8) RETURNING id" ,
b6b7e4cf Andy McCall 1 month ago ↶
393
)
394
.bind ( name)
395
.bind ( token_hash)
396
.bind ( key)
397
.bind ( key_name)
398
.bind ( now)
399
.bind ( if who.is_empty ( ) { by_default } else { & who } )
74be9796 Andy McCall 1 month ago ↶
400
.bind ( scope_owner)
401
.bind ( scope_repo_id)
b6b7e4cf Andy McCall 1 month ago ↶
402
.fetch_one ( & mut * tx)
403
.await? ;
5d90e0e0 Andy McCall 20 days ago
404
sqlx:: query( " UPDATE flow_runner_registrations SET used_ms = $1, runner_id = $2 WHERE token_hash = $3" )
b6b7e4cf Andy McCall 1 month ago ↶
405
.bind ( now)
406
.bind ( id)
407
.bind ( registration)
408
.execute ( & mut * tx)
409
.await? ;
410
tx.commit ( ) .await? ;
411
get ( pool, id) .await
412
}
413
414
415
# [ derive ( Clone, Debug, FromRow ) ]
416
pub struct RunnerJob {
417
pub owner : String,
418
pub repo : String,
419
pub run_number : i64 ,
420
pub job : String,
421
pub status : String,
422
pub started_ms : Option< i64 > ,
423
pub finished_ms : Option< i64 > ,
424
pub receipt_attest : String,
425
}
426
427
428
pub async fn history ( pool : & Pool, id : i64 , limit : i64 , before : Option< i64 > ) -> sqlx:: Result< Vec< RunnerJob> > {
429
sqlx:: query_as(
430
" SELECT p.owner AS owner, p.name AS repo, r.number AS run_number, j.name AS job, j.status AS status, j.started_ms AS started_ms, \
431
j.finished_ms AS finished_ms, j.receipt_attest AS receipt_attest \
432
FROM flow_jobs j JOIN flow_runs r ON r.id = j.run_id JOIN repositories p ON p.id = r.repo_id \
5d90e0e0 Andy McCall 20 days ago
433
WHERE j.runner_id = $1 AND ($2 IS NULL OR j.started_ms < $3) ORDER BY j.started_ms DESC LIMIT $4" ,
b6b7e4cf Andy McCall 1 month ago ↶
434
)
435
.bind ( id)
436
.bind ( before)
437
.bind ( before)
438
.bind ( limit)
439
.fetch_all ( pool)
440
.await
441
}
442
443
444
pub async fn totals ( pool : & Pool, id : i64 ) -> sqlx:: Result< ( i64 , i64 , i64 ) > {
445
sqlx:: query_as(
3b1bc27f Andy McCall 20 days ago
446
" SELECT COUNT(*), COALESCE(SUM(CASE WHEN status = 'passed' THEN 1 ELSE 0 END), 0), CAST(COALESCE(SUM(finished_ms - started_ms), 0) AS BIGINT) \
5d90e0e0 Andy McCall 20 days ago
447
FROM flow_jobs WHERE runner_id = $1 AND finished_ms IS NOT NULL" ,
b6b7e4cf Andy McCall 1 month ago ↶
448
)
449
.bind ( id)
450
.fetch_one ( pool)
451
.await
452
}
453
454
455
pub async fn running ( pool : & Pool) -> sqlx:: Result< Vec< ( i64 , i64 ) > > {
456
sqlx:: query_as( " SELECT runner_id, COUNT(*) FROM flow_jobs WHERE status = 'running' AND runner_id IS NOT NULL GROUP BY runner_id" )
457
.fetch_all ( pool)
458
.await
459
}
460
461
pub async fn set_job_runner ( pool : & Pool, job_id : i64 , runner_id : i64 ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
462
sqlx:: query( " UPDATE flow_jobs SET runner_id = $1 WHERE id = $2" ) .bind ( runner_id) .bind ( job_id) .execute ( pool) .await? ;
b6b7e4cf Andy McCall 1 month ago ↶
463
Ok ( ( ) )
464
}