e0918b88 Andy McCall 1 month ago
1
2
3
5cf1e8ae Andy McCall 10 days ago ↶
4
use flow:: { facts, home, images, microvm, runner, syscall, wait, wasm, workspace} ;
e0918b88 Andy McCall 1 month ago
5
6
use anyhow:: { bail, Context, Result } ;
7
use clap:: { Parser, Subcommand} ;
8
use flow_core:: api:: Record;
9
use flow_core:: { Pipeline, Runtime, PIPELINE_PATH } ;
10
use home:: Home;
11
use std:: collections:: BTreeSet;
12
use std:: path:: { Path, PathBuf} ;
13
use std:: time:: Duration;
14
15
# [ derive ( Parser ) ]
2d025bbf Andy McCall 1 month ago ↶
16
# [ command ( name = " flow" , version = env!(" OXY_VERSION_LONG" ) , about = " OxygenFlow: hermetic pipelines for Oxygen repositories" )]
e0918b88 Andy McCall 1 month ago
17
struct Cli {
18
19
#[arg(long, global = true)]
20
home : Option< PathBuf> ,
21
#[command(subcommand)]
22
cmd : Cmd,
23
}
24
25
# [ derive ( Subcommand ) ]
f98a8f78 Andy McCall 1 month ago ↶
26
enum RunnerCmd {
27
28
Register {
29
30
hub: String ,
31
32
# [ arg ( long ) ]
33
token: Option< String> ,
34
35
# [ arg ( long ) ]
36
name: Option< String> ,
37
38
# [ arg ( long ) ]
39
ca: Option< PathBuf> ,
40
41
# [ arg ( long ) ]
42
label: Vec< String> ,
43
} ,
62829d15 Andy McCall 24 days ago ↶
44
45
46
Bench,
4dd392d1 Andy McCall 9 days ago ↶
47
48
49
50
Drain,
85275e1e Andy McCall 10 days ago ↶
51
52
53
54
55
Update {
56
57
# [ arg ( long ) ]
58
from: PathBuf,
59
60
61
# [ arg ( long, default_value_t = 600 ) ]
62
notice: u64 ,
63
64
# [ arg ( long ) ]
65
dry_run: bool ,
66
67
# [ arg ( long ) ]
68
allow_older: bool ,
69
70
# [ arg ( long ) ]
71
exe: Option< PathBuf> ,
72
} ,
f98a8f78 Andy McCall 1 month ago ↶
73
}
74
75
# [ derive ( Subcommand ) ]
f3b5b86e Andy McCall 1 day ago ↶
76
enum DepsCmd {
77
78
79
Check {
80
81
# [ arg ( default_value = " Cargo.lock" ) ]
82
lock: PathBuf,
83
# [ command ( flatten ) ]
84
image: DepsSource,
85
} ,
86
87
List {
88
# [ command ( flatten ) ]
89
image: DepsSource,
90
} ,
91
}
92
93
94
95
96
# [ derive ( clap::Args ) ]
97
# [ group ( multiple = false ) ]
98
struct DepsSource {
99
100
#[arg(long, value_name = "FILE")]
101
list : Option< PathBuf> ,
102
103
#[arg(long, value_name = "DIR")]
104
vendor : Option< PathBuf> ,
105
106
107
#[arg(long = "image-lock", value_name = "LOCK")]
108
locks : Vec< PathBuf> ,
109
}
110
111
impl DepsSource {
112
fn crates ( & self ) -> Result< BTreeSet< flow:: deps:: Crate> > {
113
let read = | p : & Path| std:: fs:: read_to_string( p) .with_context ( | | p.display ( ) .to_string ( ) ) ;
114
if let Some ( f) = & self .list {
115
return Ok ( flow:: deps:: from_list( & read ( f) ? ) ) ;
116
}
117
if ! self .locks.is_empty ( ) {
118
let mut all = BTreeSet:: new( ) ;
119
for l in & self .locks {
120
all.extend ( flow:: deps:: from_lock( & read ( l) ? ) .map_err ( | e | anyhow:: anyhow! ( " {}: {e}" , l.display ( ) ) ) ? ) ;
121
}
122
return Ok ( all) ;
123
}
124
125
if self .vendor.is_none ( ) & & Path:: new( " /opt/crates.txt" ) .is_file ( ) {
126
return Ok ( flow:: deps:: from_list( & read ( Path:: new( " /opt/crates.txt" ) ) ? ) ) ;
127
}
128
let dir = self .vendor.clone ( ) .unwrap_or_else ( | | PathBuf:: from( " /opt/vendor" ) ) ;
129
if self .vendor.is_none ( ) & & ! dir.is_dir ( ) {
130
bail! ( " say what the image holds: --list crates.txt (published with the image), --vendor DIR, or --image-lock for each lock file it is built from; inside a job, /opt/vendor is read" ) ;
131
}
132
flow:: deps:: from_vendor( & dir) .map_err ( | e | anyhow:: anyhow! ( e) )
133
}
134
}
135
136
# [ derive ( Subcommand ) ]
e0918b88 Andy McCall 1 month ago
137
enum Cmd {
138
139
Check {
140
141
file: Option< PathBuf> ,
142
} ,
143
144
Hash { file: PathBuf } ,
f3b5b86e Andy McCall 1 day ago ↶
145
146
Deps {
147
# [ command ( subcommand ) ]
148
cmd: DepsCmd,
149
} ,
e0918b88 Andy McCall 1 month ago
150
151
Rootfs {
152
# [ command ( subcommand ) ]
153
cmd: RootfsCmd,
154
} ,
155
156
Run {
157
158
jobs: Vec< String> ,
159
160
# [ arg ( long, default_value = " ." ) ]
161
dir: PathBuf,
162
163
# [ arg ( long, default_value_t = 60 ) ]
164
timeout: u64 ,
c58efc6a Andy McCall 1 month ago ↶
165
166
167
# [ arg ( long = " var" , value_name = " NAME=VALUE" ) ]
168
vars: Vec< String> ,
e2c12561 Andy McCall 1 month ago ↶
169
170
171
# [ arg ( long = " secret" , value_name = " NAME=VALUE" ) ]
172
secrets: Vec< String> ,
f98a8f78 Andy McCall 1 month ago ↶
173
} ,
174
175
176
Runner {
177
# [ command ( subcommand ) ]
178
cmd: Option< RunnerCmd> ,
e0918b88 Andy McCall 1 month ago
179
} ,
9e8f4a56 Andy McCall 1 month ago ↶
180
181
182
183
Wait {
184
185
url: String ,
186
187
state: String ,
188
189
# [ arg ( long, default_value_t = 60 ) ]
190
timeout: u64 ,
191
} ,
ea56c56e Andy McCall 1 month ago ↶
192
193
194
Tiers,
195
196
# [ command ( name = " __sandbox" , hide = true ) ]
197
Sandbox { spec: PathBuf } ,
198
199
# [ command ( name = " __sandbox-probe" , hide = true ) ]
200
SandboxProbe,
494a1b9a Andy McCall 9 days ago ↶
201
202
# [ command ( name = " __rename" , hide = true ) ]
203
Rename { from: PathBuf, to: PathBuf } ,
28cc603f Andy McCall 10 days ago ↶
204
205
# [ command ( name = " __wasm" , hide = true ) ]
206
Wasm { spec: PathBuf } ,
e0918b88 Andy McCall 1 month ago
207
}
208
209
# [ derive ( Subcommand ) ]
210
enum RootfsCmd {
211
212
Import {
213
tarball: PathBuf,
214
215
# [ arg ( long ) ]
216
sha256: Option< String> ,
a72abeee Andy McCall 1 month ago ↶
217
218
219
220
# [ arg ( long = " env" , value_name = " NAME=VALUE" ) ]
221
env: Vec< String> ,
e0918b88 Andy McCall 1 month ago
222
} ,
223
224
List,
4cf0c99a Andy McCall 24 days ago ↶
225
e38bb032 Andy McCall 24 days ago ↶
226
227
4cf0c99a Andy McCall 24 days ago ↶
228
Push {
229
230
hash: String ,
e38bb032 Andy McCall 24 days ago ↶
231
4cf0c99a Andy McCall 24 days ago ↶
232
# [ arg ( long ) ]
233
hub: Option< String> ,
234
} ,
235
e38bb032 Andy McCall 24 days ago ↶
236
4cf0c99a Andy McCall 24 days ago ↶
237
Pull {
238
hash: String ,
239
# [ arg ( long ) ]
240
hub: Option< String> ,
241
} ,
e0918b88 Andy McCall 1 month ago
242
}
243
244
fn main ( ) {
245
let cli = Cli:: parse( ) ;
246
if let Err ( e) = run ( cli) {
247
eprintln! ( " flow: {e:#}" ) ;
248
std:: process:: exit( 1 ) ;
249
}
250
}
251
252
fn run ( cli : Cli) -> Result< ( ) > {
253
match cli.cmd {
ea56c56e Andy McCall 1 month ago ↶
254
Cmd:: Sandbox { spec } => std:: process:: exit( syscall:: sandbox_main( & spec) ) ,
255
Cmd:: SandboxProbe => std:: process:: exit( syscall:: probe_main( ) ) ,
494a1b9a Andy McCall 9 days ago ↶
256
Cmd:: Rename { from, to } => {
257
flow:: portable:: rename( & from, & to) .with_context ( | | format! ( " renaming {} to {} " , from.display ( ) , to.display ( ) ) ) ? ;
258
Ok ( ( ) )
259
}
28cc603f Andy McCall 10 days ago ↶
260
Cmd:: Wasm { spec } => std:: process:: exit( wasm:: wasm_main( & spec) ) ,
e0918b88 Andy McCall 1 month ago
261
Cmd:: Check { file } => check ( & file.unwrap_or_else ( | | PathBuf:: from( PIPELINE_PATH ) ) ) ,
f3b5b86e Andy McCall 1 day ago ↶
262
Cmd:: Deps { cmd: DepsCmd:: List { image } } => {
263
print! ( " {} " , flow:: deps:: render_list( & image.crates ( ) ? ) ) ;
264
Ok ( ( ) )
265
}
266
Cmd:: Deps { cmd: DepsCmd:: Check { lock, image } } => {
267
let text = std:: fs:: read_to_string( & lock) .with_context ( | | lock.display ( ) .to_string ( ) ) ? ;
268
let need = flow:: deps:: from_lock( & text) .map_err ( | e | anyhow:: anyhow! ( " {}: {e}" , lock.display ( ) ) ) ? ;
269
let missing = flow:: deps:: missing( & need, & image.crates ( ) ? ) ;
270
if missing.is_empty ( ) {
271
println! ( " deps: all {} crates of {} are in the image" , need.len ( ) , lock.display ( ) ) ;
272
return Ok ( ( ) ) ;
273
}
274
eprintln! ( " deps: {} of {} crates of {} are not in the image:" , missing.len ( ) , need.len ( ) , lock.display ( ) ) ;
275
for ( ( name, version) , has) in & missing {
276
let instead = match has.as_slice ( ) {
277
[ ] => " the image has no version of it" .to_string ( ) ,
278
vs => format! ( " the image has {} : cargo update -p {name} @{version} --precise {} " , vs.join ( " , " ) , vs.last ( ) .unwrap ( ) ) ,
279
} ;
280
eprintln! ( " {name} {version} ({instead})" ) ;
281
}
282
eprintln! ( " pin a version the image has, or rebuild the image with this workspace's lock (images/rust/build.sh)" ) ;
283
std:: process:: exit( 1 ) ;
284
}
e0918b88 Andy McCall 1 month ago
285
Cmd:: Hash { file } => {
286
println! ( " {} " , home:: hash_file( & file) ? ) ;
287
Ok ( ( ) )
288
}
289
Cmd:: Rootfs { cmd } => {
290
let home = Home:: locate( cli.home.as_deref ( ) ) ? ;
291
match cmd {
a72abeee Andy McCall 1 month ago ↶
292
RootfsCmd:: Import { tarball, sha256, env } => rootfs_import ( & home, & tarball, sha256.as_deref ( ) , & env) ,
4cf0c99a Andy McCall 24 days ago ↶
293
RootfsCmd:: Push { hash, hub } => {
294
let client = image_client ( & home, hub) ? ;
295
eprintln! ( " Sending image {} …" , & hash[ .. 12. min ( hash.len ( ) ) ] ) ;
296
let m = client.push ( & home, & hash) ? ;
297
println! (
298
" Sent image {} : {} chunks the hub lacked, {} in {:.1} s" ,
299
& hash[ .. 12 ] ,
300
m.chunks,
301
images:: human( m.bytes) ,
302
m.elapsed.as_secs_f64 ( )
303
) ;
304
Ok ( ( ) )
305
}
306
RootfsCmd:: Pull { hash, hub } => {
307
let client = image_client ( & home, hub) ? ;
308
match client.fetch ( & home, & hash) ? {
309
Some ( m) => {
310
println! (
311
" Fetched image {} : {} chunks, {} in {:.1} s" ,
312
& hash[ .. 12 ] ,
313
m.chunks,
314
images:: human( m.bytes) ,
315
m.elapsed.as_secs_f64 ( )
316
) ;
317
Ok ( ( ) )
318
}
319
None => anyhow:: bail! ( " the hub does not hold image {}; send it with flow rootfs push" , & hash[ .. 12. min ( hash.len ( ) ) ] ) ,
320
}
321
}
e0918b88 Andy McCall 1 month ago
322
RootfsCmd:: List => {
323
for e in std:: fs:: read_dir( home.root.join ( " cas" ) ) ? .flatten ( ) {
324
let size = e.metadata ( ) .map ( | m | m.len ( ) ) .unwrap_or ( 0 ) ;
325
if let Some ( h) = e.file_name ( ) .to_string_lossy ( ) .strip_suffix ( " .ext4" ) {
326
println! ( " cas://blob/{h} {} MiB" , size > > 20 ) ;
327
}
328
}
329
Ok ( ( ) )
330
}
331
}
332
}
e2c12561 Andy McCall 1 month ago ↶
333
Cmd:: Run { jobs, dir, timeout, vars, secrets } => {
e0918b88 Andy McCall 1 month ago
334
let home = Home:: locate( cli.home.as_deref ( ) ) ? ;
e2c12561 Andy McCall 1 month ago ↶
335
let pairs = | what : & str , list : & [String]| {
336
list.iter ( )
337
.map ( | e | {
338
e.split_once ( ' =' )
339
.map ( | ( k , v ) | ( k.to_string ( ) , v.to_string ( ) ) )
340
.ok_or_else ( | | anyhow:: anyhow! ( " --{what} {e:?}: expected NAME=VALUE" ) )
341
} )
342
.collect:: < Result< Vec< _ > > > ( )
343
} ;
344
let ( vars, secrets) = ( pairs ( " var" , & vars) ? , pairs ( " secret" , & secrets) ? ) ;
345
run_local ( & home, & dir, & jobs, Duration:: from_secs( timeout * 60 ) , & vars, & secrets)
f98a8f78 Andy McCall 1 month ago ↶
346
}
62829d15 Andy McCall 24 days ago ↶
347
Cmd:: Runner { cmd: Some ( RunnerCmd:: Bench) } => {
348
let home = Home:: locate( cli.home.as_deref ( ) ) ? ;
349
let ( speed, ms) = facts:: bench( ) ;
350
let f = facts:: now( & home, Some ( speed) ) ;
351
println! ( " speed {speed} (class {} ), benchmark {ms} ms" , flow_core:: api:: Facts:: class_of( speed) ) ;
352
println! ( " free disk {} MiB at {} " , f.free_disk_mib.unwrap_or ( 0 ) , home.root.display ( ) ) ;
a63d3dcd Andy McCall 9 days ago ↶
353
match f.load_x100 {
354
Some ( l) => println! ( " load {:.2} " , l as f64 / 100. 0 ) ,
355
None => println! ( " load not measured here" ) ,
356
}
62829d15 Andy McCall 24 days ago ↶
357
if let Some ( k) = f.kernel {
358
println! ( " kernel {k} " ) ;
359
}
360
Ok ( ( ) )
361
}
f98a8f78 Andy McCall 1 month ago ↶
362
Cmd:: Runner { cmd: Some ( RunnerCmd:: Register { hub, token, name, ca, label } ) } => {
363
let home = Home:: locate( cli.home.as_deref ( ) ) ? ;
364
if std:: env:: var_os( " OXY_HOME" ) .is_none ( ) {
365
std:: env:: set_var( " OXY_HOME" , home.root.join ( " oxy" ) ) ;
366
}
367
let name = name.unwrap_or_else ( | | {
368
std:: fs:: read_to_string( " /etc/hostname" ) .map ( | h | h.trim ( ) .to_string ( ) ) .unwrap_or_else ( | _| " flow" .into ( ) )
369
} ) ;
370
let token = match token {
371
Some ( t) => t,
372
None => {
373
eprintln! ( " Paste the registration token from the hub's Admin → Runners, then press Enter:" ) ;
374
let mut line = String :: new( ) ;
375
std:: io:: stdin( ) .read_line ( & mut line) ? ;
376
line.trim ( ) .to_string ( )
377
}
378
} ;
379
let got = runner:: register( & home, & hub, & token, & name, ca, label) ? ;
380
println! ( " registered as {} (runner {} ); start it with `flow runner`" , got.name, got.id) ;
85275e1e Andy McCall 10 days ago ↶
381
Ok ( ( ) )
382
}
4dd392d1 Andy McCall 9 days ago ↶
383
Cmd:: Runner { cmd: Some ( RunnerCmd:: Drain) } => {
384
let home = Home:: locate( cli.home.as_deref ( ) ) ? ;
385
if flow:: update:: ask_to_drain( & home.root) ? {
386
println! ( " asked the runner to drain: it claims nothing more and stops once its jobs are done and reported" ) ;
387
} else {
388
println! ( " no runner of this home is running; the next one starts afresh" ) ;
389
}
390
Ok ( ( ) )
391
}
85275e1e Andy McCall 10 days ago ↶
392
Cmd:: Runner { cmd: Some ( RunnerCmd:: Update { from, notice, dry_run, allow_older, exe } ) } => {
393
let home = Home:: locate( cli.home.as_deref ( ) ) ? ;
394
let exe = match exe {
395
Some ( e) => e,
396
None => std:: env:: current_exe( ) .context ( " finding this flow binary" ) ? ,
397
} ;
398
let done = flow:: update:: install( & home, & exe, & from, Duration:: from_secs( notice) , dry_run, allow_older) ? ;
399
if dry_run {
400
println! (
401
" flow {} checks out: it would replace flow {} at {} (dry run: nothing changed)" ,
402
done.to,
403
done.from,
404
done.exe.display ( )
405
) ;
406
return Ok ( ( ) ) ;
407
}
408
println! ( " flow {} installed at {} ; flow {} kept as {} " , done.to, done.exe.display ( ) , done.from, flow:: update:: PREV ) ;
409
match done.told {
410
Some ( pid) => println! ( " the runner (pid {pid} ) leaves within {notice} s, then its unit starts flow {} " , done.to) ,
411
None => println! (
412
" no runner of this home was told (none runs, or one from before runner updates, which does not listen): \
413
restart its unit, e.g. `systemctl restart --no-block flow-runner`; it drains and starts flow {} " ,
414
done.to
415
) ,
416
}
f98a8f78 Andy McCall 1 month ago ↶
417
Ok ( ( ) )
e0918b88 Andy McCall 1 month ago
418
}
f98a8f78 Andy McCall 1 month ago ↶
419
Cmd:: Runner { cmd: None } => {
e0918b88 Andy McCall 1 month ago
420
let home = Home:: locate( cli.home.as_deref ( ) ) ? ;
85275e1e Andy McCall 10 days ago ↶
421
422
423
if let flow:: update:: Start:: RolledBack( p) = flow:: update:: on_start( & home.root) ? {
3554afef Andy McCall 10 days ago ↶
424
# [ cfg ( unix ) ]
425
{
426
use std:: os:: unix:: process:: CommandExt;
427
let err = std:: process:: Command:: new( & p.exe) .args ( std:: env:: args_os( ) .skip ( 1 ) ) .exec ( ) ;
428
bail! ( " starting flow {} again after the rollback: {err}" , p.from) ;
429
}
494a1b9a Andy McCall 9 days ago ↶
430
3554afef Andy McCall 10 days ago ↶
431
# [ cfg ( not ( unix ) ) ]
494a1b9a Andy McCall 9 days ago ↶
432
{
433
let status = std:: process:: Command:: new( & p.exe)
434
.args ( std:: env:: args_os( ) .skip ( 1 ) )
435
.status ( )
436
.with_context ( | | format! ( " starting flow {} again after the rollback" , p.from) ) ? ;
437
std:: process:: exit( status.code ( ) .unwrap_or ( 1 ) ) ;
438
}
85275e1e Andy McCall 10 days ago ↶
439
}
e0918b88 Andy McCall 1 month ago
440
441
if std:: env:: var_os( " OXY_HOME" ) .is_none ( ) {
442
std:: env:: set_var( " OXY_HOME" , home.root.join ( " oxy" ) ) ;
443
}
444
runner:: serve( & home)
9e8f4a56 Andy McCall 1 month ago ↶
445
}
ea56c56e Andy McCall 1 month ago ↶
446
Cmd:: Tiers => {
447
let home = Home:: locate( cli.home.as_deref ( ) ) ? ;
448
match runner:: tiers( & home) {
449
Ok ( ( have, missing) ) => {
450
for t in have {
451
println! ( " {t} : available" ) ;
452
}
453
for why in missing {
454
println! ( " {} " , why.replacen ( " : " , " : unavailable: " , 1 ) ) ;
455
}
456
}
457
Err ( why) => println! ( " {why} " ) ,
458
}
459
Ok ( ( ) )
460
}
9e8f4a56 Andy McCall 1 month ago ↶
461
Cmd:: Wait { url, state, timeout } => {
462
let token = std:: env:: var( " OXY_TOKEN" ) .ok ( ) ;
463
let ca = std:: env:: var_os( " OXY_CA" ) .map ( PathBuf:: from) ;
464
let code =
465
wait:: wait( & url, & state, token.as_deref ( ) , ca.as_deref ( ) , Duration:: from_secs( timeout * 60 ) , Duration:: from_secs( 15 ) ) ? ;
466
std:: process:: exit( code) ;
e0918b88 Andy McCall 1 month ago
467
}
468
}
4cf0c99a Andy McCall 24 days ago ↶
469
}
470
e38bb032 Andy McCall 24 days ago ↶
471
472
473
474
4cf0c99a Andy McCall 24 days ago ↶
475
fn image_client ( home : & Home, hub : Option< String> ) -> Result< images:: Client> {
476
let runner = home.config.runner.clone ( ) ;
e38bb032 Andy McCall 24 days ago ↶
477
let hub = hub
478
.or_else ( | | std:: env:: var( " OH_HUB" ) .ok ( ) .filter ( | h | ! h.is_empty ( ) ) )
479
.or_else ( | | runner.as_ref ( ) .map ( | r | r.hub.clone ( ) ) )
480
.context ( " name the hub with --hub https://HUB (or set OH_HUB)" ) ? ;
481
let ( saved_token, saved_ca) = oxy_core:: credentials:: Credentials:: user( ) .lookup ( & hub) ;
4cf0c99a Andy McCall 24 days ago ↶
482
let token = std:: env:: var( " OXY_TOKEN" )
483
.ok ( )
e38bb032 Andy McCall 24 days ago ↶
484
.filter ( | t | ! t.is_empty ( ) )
485
.or ( saved_token)
4cf0c99a Andy McCall 24 days ago ↶
486
.or_else ( | | runner.as_ref ( ) .map ( | r | r.token.clone ( ) ) )
e38bb032 Andy McCall 24 days ago ↶
487
.with_context ( | | format! ( " no token for {hub} : sign in with `oh auth login --hub {hub} `, or set OXY_TOKEN" ) ) ? ;
488
let ca = std:: env:: var_os( " OXY_CA" ) .map ( PathBuf:: from) .or ( saved_ca) .or_else ( | | runner.as_ref ( ) .and_then ( | r | r.ca.clone ( ) ) ) ;
4cf0c99a Andy McCall 24 days ago ↶
489
images:: Client:: new( & hub, & token, ca.as_deref ( ) )
e0918b88 Andy McCall 1 month ago
490
}
491
492
fn load ( file : & Path) -> Result< Pipeline> {
493
let src = std:: fs:: read_to_string( file) .with_context ( | | format! ( " read {} " , file.display ( ) ) ) ? ;
3b9dddc9 Andy McCall 24 days ago ↶
494
Pipeline:: parse( & src) .map_err ( | p | anyhow:: anyhow! ( " {}" , not_valid ( file, & p.to_string ( ) ) ) )
e0918b88 Andy McCall 1 month ago
495
}
496
3b9dddc9 Andy McCall 24 days ago ↶
497
498
499
500
fn not_valid ( file : & Path, problems : & str ) -> String {
501
if problems.contains ( " unknown " ) {
502
format! (
503
" {} uses something this flow ({} ) does not know:\n {problems} \n \
504
If the pipeline was written for a newer OxygenFlow, update flow (build it from oxygenflow main); \
505
otherwise fix the lines named." ,
506
file.display ( ) ,
507
env! ( " OXY_VERSION_LONG" )
508
)
509
} else {
510
format! ( " {} is not valid:\n {problems} " , file.display ( ) )
511
}
512
}
513
e0918b88 Andy McCall 1 month ago
514
fn check ( file : & Path) -> Result< ( ) > {
515
let p = load ( file) ? ;
516
let depths = p.depths ( ) ;
517
for j in p.order ( ) {
518
let sb = & p.sandboxes[ & j.sandbox] ;
519
let after = if j.depends_on.is_empty ( ) { String :: new( ) } else { format! ( " after {} " , j.depends_on.join ( " , " ) ) } ;
520
println! ( " {} {} {} [{} ]{after} " , " " .repeat ( depths[ j.name.as_str ( ) ] ) , j.name, j.exec.join ( " " ) , sb.runtime.as_str ( ) ) ;
521
}
522
println! ( " pipeline {} " , hex:: encode( p.hash ( ) ) ) ;
523
Ok ( ( ) )
524
}
525
a72abeee Andy McCall 1 month ago ↶
526
527
528
fn image_env_file ( env : & [String]) -> Result< String> {
529
let mut out = String :: new( ) ;
530
for e in env {
531
let Some ( ( k, v) ) = e.split_once ( ' =' ) else { bail! ( " --env {e:?}: expected NAME=value" ) } ;
532
let ok = ! k.is_empty ( ) & & ! k.starts_with ( | c : char | c.is_ascii_digit ( ) ) & & k.bytes ( ) .all ( | b | b.is_ascii_alphanumeric ( ) | | b = = b ' _' ) ;
533
if ! ok | | v.contains ( ' \n ' ) | | v.contains ( ' \0 ' ) {
534
bail! ( " --env {e:?}: a name is letters, digits and _, and a value is one line" ) ;
535
}
536
out.push_str ( & format! ( " {k} ={v} \n " ) ) ;
537
}
538
Ok ( out)
539
}
540
1abadd4a Andy McCall 1 month ago ↶
541
542
fn elf_arch ( path : & Path) -> Option< & 'static str > {
0077c748 Andy McCall 22 days ago ↶
543
let mut head = [ 0 u8 ; 20 ] ;
544
std:: io:: Read:: read_exact( & mut std:: fs:: File:: open( path) .ok ( ) ? , & mut head) .ok ( ) ? ;
545
jet:: rootfs:: elf_arch( & head)
1abadd4a Andy McCall 1 month ago ↶
546
}
547
548
549
550
fn tree_arch ( tree : & Path) -> Option< & 'static str > {
551
for rel in [ " bin/sh" , " bin/busybox" , " usr/bin/env" , " bin/bash" , " usr/bin/sh" ] {
552
let p = tree.join ( rel) ;
553
let p = std:: fs:: canonicalize( & p) .unwrap_or ( p) ;
554
if let Some ( a) = elf_arch ( & p) {
555
return Some ( a) ;
556
}
557
}
558
None
559
}
560
a72abeee Andy McCall 1 month ago ↶
561
fn rootfs_import ( home : & Home, tarball : & Path, sha256 : Option< & str > , env : & [String]) -> Result< ( ) > {
562
let env_file = image_env_file ( env) ? ;
e0918b88 Andy McCall 1 month ago
563
if let Some ( want) = sha256 {
564
let out = std:: process:: Command:: new( " sha256sum" ) .arg ( tarball) .output ( ) .context ( " run sha256sum" ) ? ;
565
let got = String :: from_utf8_lossy( & out.stdout) .split_whitespace ( ) .next ( ) .unwrap_or ( " " ) .to_string ( ) ;
566
if got ! = want.to_lowercase ( ) {
567
bail! ( " {} has SHA-256 {got}, expected {want}" , tarball.display ( ) ) ;
568
}
569
}
570
let tmp = home.root.join ( " work" ) .join ( format! ( " rootfs-{} " , std:: process:: id( ) ) ) ;
571
let _ = std:: fs:: remove_dir_all( & tmp) ;
572
let tree = tmp.join ( " tree" ) ;
573
std:: fs:: create_dir_all( & tree) ? ;
574
let result = ( | | -> Result< String> {
575
let status = std:: process:: Command:: new( " tar" ) .arg ( " -xf" ) .arg ( tarball) .arg ( " -C" ) .arg ( & tree) .arg ( " --no-same-owner" ) .status ( ) ? ;
576
if ! status.success ( ) {
577
bail! ( " tar could not unpack {}" , tarball.display ( ) ) ;
1abadd4a Andy McCall 1 month ago ↶
578
}
579
580
581
let arch = tree_arch ( & tree) .unwrap_or ( " unknown" ) ;
582
if arch ! = " unknown" & & arch ! = std:: env:: consts:: ARCH {
583
bail! (
584
" {} is a {arch} image; this runner is {}, and jobs here run no other architecture" ,
585
tarball.display ( ) ,
586
std:: env:: consts:: ARCH
587
) ;
e0918b88 Andy McCall 1 month ago
588
}
589
590
for d in [ " src" , " tmp" , " proc" , " dev" , " run" ] {
591
std:: fs:: create_dir_all( tree.join ( d) ) ? ;
a72abeee Andy McCall 1 month ago ↶
592
}
593
if ! env_file.is_empty ( ) {
594
std:: fs:: create_dir_all( tree.join ( " etc/flow" ) ) ? ;
595
std:: fs:: write( tree.join ( " etc/flow/env" ) , & env_file) ? ;
e0918b88 Andy McCall 1 month ago
596
}
597
let used: u64 = ignore:: WalkBuilder:: new( & tree)
598
.hidden ( false )
599
.ignore ( false )
600
.git_ignore ( false )
601
.build ( )
602
.flatten ( )
603
.filter_map ( | e | e.metadata ( ) .ok ( ) )
604
.map ( | m | m.len ( ) .max ( 4096 ) )
605
.sum ( ) ;
606
let mib = used * 13 / 10 / ( 1 < < 20 ) + 64 ;
607
let img = tmp.join ( " rootfs.ext4" ) ;
608
let status = std:: process:: Command:: new( " mkfs.ext4" )
609
.args ( [ " -q" , " -F" , " -L" , " rootfs" , " -E" , " root_owner=0:0" , " -d" ] )
610
.arg ( & tree)
611
.arg ( & img)
612
.arg ( format! ( " {mib} M" ) )
613
.status ( )
614
.context ( " run mkfs.ext4" ) ? ;
615
if ! status.success ( ) {
616
bail! ( " mkfs.ext4 failed" ) ;
617
}
618
let hash = home:: hash_file( & img) ? ;
619
std:: fs:: rename( & img, home.rootfs ( & hash) ) ? ;
1abadd4a Andy McCall 1 month ago ↶
620
let manifest = serde_json:: json! ( { " arch" : if arch = = " unknown" { std:: env:: consts:: ARCH } else { arch } , " imported_ms" : std:: time:: SystemTime:: now( ) .duration_since ( std:: time:: UNIX_EPOCH ) .map ( | d | d.as_millis ( ) as u64 ) .unwrap_or ( 0 ) } ) ;
621
std:: fs:: write( home.image_manifest ( & hash) , manifest.to_string ( ) ) ? ;
e0918b88 Andy McCall 1 month ago
622
Ok ( hash)
623
} ) ( ) ;
624
let _ = std:: fs:: remove_dir_all( & tmp) ;
625
let hash = result? ;
626
println! ( " rootfs \" cas://blob/{hash} \" " ) ;
627
Ok ( ( ) )
628
}
629
e2c12561 Andy McCall 1 month ago ↶
630
fn run_local (
631
home : & Home,
632
dir : & Path,
633
only : & [String],
634
timeout : Duration,
635
vars : & [( String , String ) ],
636
secrets : & [( String , String ) ],
637
) -> Result< ( ) > {
e0918b88 Andy McCall 1 month ago
638
let p = load ( & dir.join ( PIPELINE_PATH ) ) ? ;
639
640
let mut wanted: BTreeSet< String> = only.iter ( ) .cloned ( ) .collect ( ) ;
641
for name in only {
642
if ! p.jobs.contains_key ( name) {
643
bail! ( " no job {name:?} in the pipeline" ) ;
644
}
645
}
646
if ! only.is_empty ( ) {
647
loop {
648
let more: Vec< String> = wanted.iter ( ) .flat_map ( | j | p.jobs[ j] .depends_on.clone ( ) ) .filter ( | d | ! wanted.contains ( d) ) .collect ( ) ;
649
if more.is_empty ( ) {
650
break ;
651
}
652
wanted.extend ( more) ;
653
}
654
}
655
let scratch = home.root.join ( " work" ) .join ( format! ( " local-{} " , std:: process:: id( ) ) ) ;
656
let _ = std:: fs:: remove_dir_all( & scratch) ;
657
std:: fs:: create_dir_all( scratch.join ( " tree" ) ) ? ;
658
workspace:: copy_tree( dir, & scratch.join ( " tree" ) ) ? ;
659
let mut failed: BTreeSet< String> = BTreeSet:: new( ) ;
660
let mut worst = 0 ;
661
for job in p.order ( ) {
662
if ! only.is_empty ( ) & & ! wanted.contains ( & job.name) {
663
continue ;
664
}
665
if let Some ( d) = job.depends_on.iter ( ) .find ( | d | failed.contains ( * d) ) {
666
println! ( " ── {} skipped: {d} failed" , job.name) ;
667
failed.insert ( job.name.clone ( ) ) ;
668
continue ;
669
}
670
let sb = & p.sandboxes[ & job.sandbox] ;
671
println! ( " ── {} ({} )" , job.name, job.exec.join ( " " ) ) ;
672
let js = scratch.join ( & job.name) ;
673
std:: fs:: create_dir_all( & js) ? ;
c58efc6a Andy McCall 1 month ago ↶
674
let job = & job
675
.in_sandbox ( sb)
676
.with_flow_env ( & [ ( " FLOW_JOB" .into ( ) , job.name.clone ( ) ) , ( " FLOW_RUN" .into ( ) , " local" .into ( ) ) ] )
677
.with_flow_env ( vars) ;
720eb819 Andy McCall 1 month ago ↶
678
679
for i in & job.identities {
680
println! ( " ── {} : identity {:?} is not available locally; {} will be missing" , job.name, i.audience, i.mount_path) ;
681
}
e2c12561 Andy McCall 1 month ago ↶
682
683
let inputs = job
684
.secrets
685
.iter ( )
686
.map ( | s | {
687
let value = secrets.iter ( ) .find ( | ( k , _) | * k = = s.name) .map ( | ( _, v ) | v.clone ( ) ) ;
688
value
689
.map ( | value | flow_core:: api:: SecretInput { name: s.name.clone ( ) , path: s.mount_path.clone ( ) , value } )
690
.ok_or_else ( | | anyhow:: anyhow! ( " job {}: secret {:?} is not given; pass --secret {}=VALUE" , job.name, s.name, s.name) )
691
} )
692
.collect:: < Result< Vec< _ > > > ( ) ? ;
ea56c56e Andy McCall 1 month ago ↶
693
28cc603f Andy McCall 10 days ago ↶
694
let out = if sb.runtime = = Runtime:: Wasm {
695
workspace:: copy_tree( & scratch.join ( " tree" ) , & js.join ( " tree" ) ) ? ;
696
wasm:: run( sb, job, & js.join ( " tree" ) , & js, timeout, & mut | r: Record| print_record ( & r) ) ?
697
} else if sb.runtime = = Runtime:: Syscall {
ea56c56e Andy McCall 1 month ago ↶
698
workspace:: copy_tree( & scratch.join ( " tree" ) , & js.join ( " tree" ) ) ? ;
699
let group = format! ( " local-{} -{} " , std:: process:: id( ) , js.file_name ( ) .unwrap_or_default ( ) .to_string_lossy ( ) ) ;
700
syscall:: run( home, sb, job, & inputs, & js.join ( " tree" ) , & js, timeout, & mut | r: Record| print_record ( & r) , & group) ?
701
} else {
702
workspace:: image( & scratch.join ( " tree" ) , & js.join ( " workspace.ext4" ) ) ? ;
703
microvm:: run( home, sb, job, & inputs, & js.join ( " workspace.ext4" ) , & js, timeout, & mut | r: Record| print_record ( & r) ) ?
704
} ;
705
let _ = workspace:: remove_tree( & js) ;
e0918b88 Andy McCall 1 month ago
706
match out.exit_code {
707
Some ( 0 ) => println! ( " ── {} passed in {} ms" , job.name, out.duration_ms) ,
708
other => {
709
println! (
710
" ── {} failed ({} ) in {} ms" ,
711
job.name,
712
other.map ( | c | format! ( " exit {c} " ) ) .unwrap_or_else ( | | " no exit code" .into ( ) ) ,
713
out.duration_ms
714
) ;
715
failed.insert ( job.name.clone ( ) ) ;
716
worst = 1 ;
717
}
718
}
719
}
ea56c56e Andy McCall 1 month ago ↶
720
let _ = workspace:: remove_tree( & scratch) ;
e0918b88 Andy McCall 1 month ago
721
if worst ! = 0 {
722
std:: process:: exit( worst) ;
723
}
724
Ok ( ( ) )
725
}
726
727
fn print_record ( r : & Record) {
728
match r.stream.as_str ( ) {
729
" out" => println! ( " {} " , r.text) ,
730
" err" => eprintln! ( " {}" , r.text) ,
731
s => eprintln! ( " [{s}] {}" , r.text) ,
a72abeee Andy McCall 1 month ago ↶
732
}
733
}
734
735
# [ cfg ( test ) ]
736
mod tests {
3b9dddc9 Andy McCall 24 days ago ↶
737
# [ test ]
738
fn a_pipeline_newer_than_flow_is_named_as_such ( ) {
739
let m = super :: not_valid( std:: path:: Path:: new( " .flow/pipeline.kdl" ) , " line 92: unknown output `sbom`; outputs are …" ) ;
740
assert! ( m.contains ( " does not know" ) & & m.contains ( env! ( " OXY_VERSION_LONG" ) ) & & m.contains ( " newer OxygenFlow" ) , " {m}" ) ;
741
let m = super :: not_valid( std:: path:: Path:: new( " .flow/pipeline.kdl" ) , " line 3: a job needs a sandbox" ) ;
742
assert! ( m.contains ( " is not valid" ) & & ! m.contains ( " newer" ) , " {m}" ) ;
743
}
744
a72abeee Andy McCall 1 month ago ↶
745
use super :: image_env_file;
746
747
# [ test ]
748
fn image_environments_are_checked_lines ( ) {
749
let f = image_env_file ( & [ " PATH=/opt/bin:/usr/bin" .into ( ) , " X16EMU=/opt/x16emu" .into ( ) , " EMPTY=" .into ( ) ] ) .unwrap ( ) ;
750
assert_eq! ( f, " PATH=/opt/bin:/usr/bin\n X16EMU=/opt/x16emu\n EMPTY=\n " ) ;
751
assert! ( image_env_file ( & [ " NOEQUALS" .into ( ) ] ) .is_err ( ) ) ;
752
assert! ( image_env_file ( & [ " 1X=y" .into ( ) ] ) .is_err ( ) ) ;
753
assert! ( image_env_file ( & [ " X=two\n lines" .into ( ) ] ) .is_err ( ) ) ;
754
assert_eq! ( image_env_file ( & [ ] ) .unwrap ( ) , " " ) ;
e0918b88 Andy McCall 1 month ago
755
}
756
}