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