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 ,
4d2e44d7 Andy McCall 1 month ago ↶
36
37
pub tiers : String,
38
39
pub images : String,
40
41
pub profile : String,
b6b7e4cf Andy McCall 1 month ago ↶
42
}
43
74be9796 Andy McCall 1 month ago ↶
44
45
# [ derive ( Clone, Debug, Default, PartialEq, Eq ) ]
46
pub struct Scope {
47
48
pub owner : String,
49
50
pub repo_id : Option< i64 > ,
51
}
52
53
impl Scope {
54
pub fn is_site ( & self ) -> bool {
55
self .owner.is_empty ( ) & & self .repo_id.is_none ( )
56
}
57
}
58
b6b7e4cf Andy McCall 1 month ago ↶
59
60
pub const ONLINE_MS : i64 = 90_000 ;
61
62
impl Runner {
63
pub fn online ( & self ) -> bool {
64
self .last_seen_ms.is_some_and ( | t | now_ms ( ) - t < ONLINE_MS )
65
}
66
67
68
pub fn status ( & self ) -> & 'static str {
69
match self .state.as_str ( ) {
70
" disabled" => " disabled" ,
71
" draining" => " draining" ,
72
_ if self .online ( ) => " online" ,
73
_ => " offline" ,
74
}
75
}
76
7f5f6c05 Andy McCall 1 month ago ↶
77
78
b6b7e4cf Andy McCall 1 month ago ↶
79
pub fn label_list ( & self ) -> Vec< & str > {
7f5f6c05 Andy McCall 1 month ago ↶
80
let mut out: Vec< & str > = self .labels.split ( ' ,' ) .filter ( | l | ! l.is_empty ( ) ) .collect ( ) ;
81
for l in self .admin_label_list ( ) {
82
if ! out.contains ( & l) {
83
out.push ( l) ;
84
}
85
}
86
out
87
}
88
89
pub fn admin_label_list ( & self ) -> Vec< & str > {
90
self .admin_labels.split ( ' ,' ) .filter ( | l | ! l.is_empty ( ) ) .collect ( )
91
}
92
93
94
pub fn reported_label_list ( & self ) -> Vec< & str > {
b6b7e4cf Andy McCall 1 month ago ↶
95
self .labels.split ( ' ,' ) .filter ( | l | ! l.is_empty ( ) ) .collect ( )
96
}
74be9796 Andy McCall 1 month ago ↶
97
98
pub fn scope ( & self ) -> Scope {
99
Scope { owner: self .scope_owner.clone ( ) , repo_id: self .scope_repo_id }
100
}
b6b7e4cf Andy McCall 1 month ago ↶
101
}
102
103
pub async fn by_token ( pool : & Pool, token_hash : & str ) -> sqlx:: Result< Option< Runner> > {
104
sqlx:: query_as( " SELECT * FROM flow_runners WHERE token_hash = ?" ) .bind ( token_hash) .fetch_optional ( pool) .await
105
}
106
107
pub async fn get ( pool : & Pool, id : i64 ) -> sqlx:: Result< Option< Runner> > {
108
sqlx:: query_as( " SELECT * FROM flow_runners WHERE id = ?" ) .bind ( id) .fetch_optional ( pool) .await
109
}
110
111
pub async fn list ( pool : & Pool) -> sqlx:: Result< Vec< Runner> > {
112
sqlx:: query_as( " SELECT * FROM flow_runners ORDER BY name, id" ) .fetch_all ( pool) .await
113
}
114
115
116
pub async fn configured ( pool : & Pool, token_hash : & str , name : & str ) -> sqlx:: Result< Runner> {
117
sqlx:: query( " INSERT OR IGNORE INTO flow_runners (name, token_hash, source, created_ms) VALUES (?, ?, 'config', ?)" )
118
.bind ( name)
119
.bind ( token_hash)
120
.bind ( now_ms ( ) )
121
.execute ( pool)
122
.await? ;
123
Ok ( by_token ( pool, token_hash) .await? .expect ( " just made" ) )
124
}
125
126
127
7f5f6c05 Andy McCall 1 month ago ↶
128
129
pub async fn set_limits ( pool : & Pool, id : i64 , admin_labels : & str , timeout_minutes : Option< i64 > ) -> sqlx:: Result< ( ) > {
130
sqlx:: query( " UPDATE flow_runners SET admin_labels = ?, timeout_minutes = ? WHERE id = ?" )
131
.bind ( admin_labels)
132
.bind ( timeout_minutes)
133
.bind ( id)
134
.execute ( pool)
135
.await? ;
136
Ok ( ( ) )
137
}
138
b6b7e4cf Andy McCall 1 month ago ↶
139
pub async fn seen ( pool : & Pool, id : i64 , name : Option< & str > , info : Option< & flow_core:: api:: RunnerInfo> ) -> sqlx:: Result< ( ) > {
140
let now = now_ms ( ) ;
141
match info {
142
Some ( i) => {
143
sqlx:: query(
144
" UPDATE flow_runners SET arch = ?, os = ?, version = ?, labels = ?, max_jobs = ?, memory_mib = ?, cpus = ?, last_seen_ms = ? WHERE id = ?" ,
145
)
146
.bind ( & i.arch)
147
.bind ( & i.os)
148
.bind ( & i.version)
149
.bind ( i.labels.join ( " ," ) )
150
.bind ( i.max_jobs as i64 )
151
.bind ( i.memory_mib.map ( i64 :: from) )
152
.bind ( i.cpus.map ( i64 :: from) )
153
.bind ( now)
154
.bind ( id)
155
.execute ( pool)
156
.await? ;
157
}
158
None => {
159
sqlx:: query( " UPDATE flow_runners SET last_seen_ms = ? WHERE id = ?" ) .bind ( now) .bind ( id) .execute ( pool) .await? ;
160
}
161
}
162
if let Some ( n) = name {
163
sqlx:: query( " UPDATE flow_runners SET name = ? WHERE id = ? AND source = 'config'" ) .bind ( n) .bind ( id) .execute ( pool) .await? ;
164
}
4d2e44d7 Andy McCall 1 month ago ↶
165
Ok ( ( ) )
166
}
167
168
169
170
92a797b2 Andy McCall 24 days ago ↶
171
pub async fn set_offer (
172
pool : & Pool,
173
id : i64 ,
174
tiers : & [String],
175
images : Option< & [ String] > ,
176
fetches_images : bool ,
177
profile : & str ,
178
) -> sqlx:: Result< ( ) > {
4d2e44d7 Andy McCall 1 month ago ↶
179
let images = images.map ( | i | serde_json:: to_string( i) .unwrap_or_default ( ) ) .unwrap_or_default ( ) ;
92a797b2 Andy McCall 24 days ago ↶
180
sqlx:: query( " UPDATE flow_runners SET tiers = ?, images = ?, fetches_images = ?, profile = ? WHERE id = ?" )
4d2e44d7 Andy McCall 1 month ago ↶
181
.bind ( tiers.join ( " ," ) )
182
.bind ( images)
92a797b2 Andy McCall 24 days ago ↶
183
.bind ( fetches_images)
4d2e44d7 Andy McCall 1 month ago ↶
184
.bind ( profile)
185
.bind ( id)
186
.execute ( pool)
187
.await? ;
b6b7e4cf Andy McCall 1 month ago ↶
188
Ok ( ( ) )
4d2e44d7 Andy McCall 1 month ago ↶
189
}
190
191
impl Runner {
192
pub fn tier_list ( & self ) -> Vec< & str > {
193
self .tiers.split ( ' ,' ) .filter ( | t | ! t.is_empty ( ) ) .collect ( )
194
}
195
196
197
pub fn image_list ( & self ) -> Option< Vec< String> > {
198
if self .images.is_empty ( ) {
199
None
200
} else {
201
serde_json:: from_str( & self .images) .ok ( )
202
}
203
}
b6b7e4cf Andy McCall 1 month ago ↶
204
}
205
206
pub async fn set_state ( pool : & Pool, id : i64 , state : & str ) -> sqlx:: Result< ( ) > {
207
sqlx:: query( " UPDATE flow_runners SET state = ? WHERE id = ?" ) .bind ( state) .bind ( id) .execute ( pool) .await? ;
208
Ok ( ( ) )
209
}
210
211
212
pub async fn remove ( pool : & Pool, id : i64 ) -> sqlx:: Result< ( ) > {
213
sqlx:: query( " DELETE FROM flow_runners WHERE id = ?" ) .bind ( id) .execute ( pool) .await? ;
214
Ok ( ( ) )
215
}
216
217
218
219
pub async fn bind_key ( pool : & Pool, id : i64 , key : & str , name : & str ) -> sqlx:: Result< String> {
220
sqlx:: query( " UPDATE flow_runners SET key = ?, key_name = ? WHERE id = ? AND key = ''" )
221
.bind ( key)
222
.bind ( name)
223
.bind ( id)
224
.execute ( pool)
225
.await? ;
226
sqlx:: query_scalar( " SELECT key FROM flow_runners WHERE id = ?" ) .bind ( id) .fetch_one ( pool) .await
227
}
228
229
230
pub async fn keys ( pool : & Pool) -> sqlx:: Result< Vec< String> > {
231
sqlx:: query_scalar( " SELECT key FROM flow_runners WHERE key != ''" ) .fetch_all ( pool) .await
232
}
233
74be9796 Andy McCall 1 month ago ↶
234
235
pub async fn add_registration ( pool : & Pool, token_hash : & str , by : & str , scope : & Scope) -> sqlx:: Result< ( ) > {
b6b7e4cf Andy McCall 1 month ago ↶
236
let now = now_ms ( ) ;
74be9796 Andy McCall 1 month ago ↶
237
sqlx:: query(
238
" INSERT INTO flow_runner_registrations (token_hash, created_by, created_ms, expires_ms, scope_owner, scope_repo_id) VALUES (?, ?, ?, ?, ?, ?)" ,
239
)
240
.bind ( token_hash)
241
.bind ( by)
242
.bind ( now)
243
.bind ( now + 24 * 3_600_000 )
244
.bind ( & scope.owner)
245
.bind ( scope.repo_id)
246
.execute ( pool)
247
.await? ;
248
Ok ( ( ) )
249
}
250
7974adb0 Andy McCall 1 month ago ↶
251
252
253
pub async fn serving ( pool : & Pool, owner : & str , repo_id : i64 ) -> sqlx:: Result< Vec< Runner> > {
254
sqlx:: query_as(
255
" SELECT * FROM flow_runners WHERE (scope_owner = '' AND scope_repo_id IS NULL) \
256
OR (scope_owner = ? AND scope_repo_id IS NULL) OR scope_repo_id = ? ORDER BY name" ,
257
)
258
.bind ( owner)
259
.bind ( repo_id)
260
.fetch_all ( pool)
261
.await
262
}
263
264
265
266
pub async fn serving_owner ( pool : & Pool, owner : & str ) -> sqlx:: Result< Vec< Runner> > {
267
sqlx:: query_as( " SELECT * FROM flow_runners WHERE (scope_owner = '' AND scope_repo_id IS NULL) OR scope_owner = ? ORDER BY name" )
268
.bind ( owner)
269
.fetch_all ( pool)
270
.await
271
}
272
74be9796 Andy McCall 1 month ago ↶
273
274
pub async fn set_scope ( pool : & Pool, id : i64 , scope : & Scope) -> sqlx:: Result< ( ) > {
275
sqlx:: query( " UPDATE flow_runners SET scope_owner = ?, scope_repo_id = ? WHERE id = ?" )
276
.bind ( & scope.owner)
277
.bind ( scope.repo_id)
278
.bind ( id)
b6b7e4cf Andy McCall 1 month ago ↶
279
.execute ( pool)
280
.await? ;
281
Ok ( ( ) )
282
}
283
284
285
pub async fn pending_registrations ( pool : & Pool) -> sqlx:: Result< Vec< ( String, i64 , i64 ) > > {
286
sqlx:: query_as( " SELECT created_by, created_ms, expires_ms FROM flow_runner_registrations WHERE used_ms IS NULL AND expires_ms > ? ORDER BY created_ms DESC" )
287
.bind ( now_ms ( ) )
288
.fetch_all ( pool)
289
.await
290
}
291
292
293
294
# [ allow ( clippy::too_many_arguments ) ]
295
pub async fn register (
296
pool : & Pool,
297
registration : & str ,
298
name : & str ,
299
token_hash : & str ,
300
key : & str ,
301
key_name : & str ,
302
by_default : & str ,
303
) -> sqlx:: Result< Option< Runner> > {
304
let mut tx = super :: begin_write( pool) .await? ;
305
let now = now_ms ( ) ;
74be9796 Andy McCall 1 month ago ↶
306
let found: Option< ( String, String, Option< i64 > ) > = sqlx:: query_as(
307
" SELECT created_by, scope_owner, scope_repo_id FROM flow_runner_registrations WHERE token_hash = ? AND used_ms IS NULL AND expires_ms > ?" ,
308
)
309
.bind ( registration)
310
.bind ( now)
311
.fetch_optional ( & mut * tx)
312
.await? ;
313
let Some ( ( who, scope_owner, scope_repo_id) ) = found else { return Ok ( None ) } ;
b6b7e4cf Andy McCall 1 month ago ↶
314
let id: i64 = sqlx:: query_scalar(
74be9796 Andy McCall 1 month ago ↶
315
" INSERT INTO flow_runners (name, token_hash, source, key, key_name, created_ms, created_by, scope_owner, scope_repo_id) VALUES (?, ?, 'registered', ?, ?, ?, ?, ?, ?) RETURNING id" ,
b6b7e4cf Andy McCall 1 month ago ↶
316
)
317
.bind ( name)
318
.bind ( token_hash)
319
.bind ( key)
320
.bind ( key_name)
321
.bind ( now)
322
.bind ( if who.is_empty ( ) { by_default } else { & who } )
74be9796 Andy McCall 1 month ago ↶
323
.bind ( scope_owner)
324
.bind ( scope_repo_id)
b6b7e4cf Andy McCall 1 month ago ↶
325
.fetch_one ( & mut * tx)
326
.await? ;
327
sqlx:: query( " UPDATE flow_runner_registrations SET used_ms = ?, runner_id = ? WHERE token_hash = ?" )
328
.bind ( now)
329
.bind ( id)
330
.bind ( registration)
331
.execute ( & mut * tx)
332
.await? ;
333
tx.commit ( ) .await? ;
334
get ( pool, id) .await
335
}
336
337
338
# [ derive ( Clone, Debug, FromRow ) ]
339
pub struct RunnerJob {
340
pub owner : String,
341
pub repo : String,
342
pub run_number : i64 ,
343
pub job : String,
344
pub status : String,
345
pub started_ms : Option< i64 > ,
346
pub finished_ms : Option< i64 > ,
347
pub receipt_attest : String,
348
}
349
350
351
pub async fn history ( pool : & Pool, id : i64 , limit : i64 , before : Option< i64 > ) -> sqlx:: Result< Vec< RunnerJob> > {
352
sqlx:: query_as(
353
" 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, \
354
j.finished_ms AS finished_ms, j.receipt_attest AS receipt_attest \
355
FROM flow_jobs j JOIN flow_runs r ON r.id = j.run_id JOIN repositories p ON p.id = r.repo_id \
356
WHERE j.runner_id = ? AND (? IS NULL OR j.started_ms < ?) ORDER BY j.started_ms DESC LIMIT ?" ,
357
)
358
.bind ( id)
359
.bind ( before)
360
.bind ( before)
361
.bind ( limit)
362
.fetch_all ( pool)
363
.await
364
}
365
366
367
pub async fn totals ( pool : & Pool, id : i64 ) -> sqlx:: Result< ( i64 , i64 , i64 ) > {
368
sqlx:: query_as(
369
" SELECT COUNT(*), COALESCE(SUM(status = 'passed'), 0), COALESCE(SUM(finished_ms - started_ms), 0) \
370
FROM flow_jobs WHERE runner_id = ? AND finished_ms IS NOT NULL" ,
371
)
372
.bind ( id)
373
.fetch_one ( pool)
374
.await
375
}
376
377
378
pub async fn running ( pool : & Pool) -> sqlx:: Result< Vec< ( i64 , i64 ) > > {
379
sqlx:: query_as( " SELECT runner_id, COUNT(*) FROM flow_jobs WHERE status = 'running' AND runner_id IS NOT NULL GROUP BY runner_id" )
380
.fetch_all ( pool)
381
.await
382
}
383
384
pub async fn set_job_runner ( pool : & Pool, job_id : i64 , runner_id : i64 ) -> sqlx:: Result< ( ) > {
385
sqlx:: query( " UPDATE flow_jobs SET runner_id = ? WHERE id = ?" ) .bind ( runner_id) .bind ( job_id) .execute ( pool) .await? ;
386
Ok ( ( ) )
387
}