e0918b88 Andy McCall 1 month ago
1
2
3
4
5
use serde:: { Deserialize, Serialize} ;
6
use std:: collections:: { BTreeMap, BTreeSet} ;
7
use std:: fmt;
8
9
10
pub const PIPELINE_PATH : & str = " .flow/pipeline.kdl" ;
11
12
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize, Deserialize ) ]
13
pub struct Pipeline {
14
pub schema_version : u32 ,
15
16
pub sandboxes : BTreeMap< String, Sandbox> ,
17
18
pub jobs : BTreeMap< String, Job> ,
bf7bb1b8 Andy McCall 1 month ago ↶
19
20
21
#[serde(default, skip_serializing_if = "Option : : is_none")]
22
pub on: Option< On> ,
23
}
24
25
26
# [ derive ( Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize ) ]
27
pub struct On {
28
29
#[serde(default, skip_serializing_if = "Option : : is_none")]
30
pub push: Option< Vec< String> > ,
31
32
#[serde(default, skip_serializing_if = "Option : : is_none")]
33
pub tag: Option< Vec< String> > ,
34
35
#[serde(default, skip_serializing_if = "std : : ops:: Not :: not")]
36
pub manual: bool ,
37
38
#[serde(default, skip_serializing_if = "Vec : : is_empty")]
39
pub schedule: Vec< String> ,
e0918b88 Andy McCall 1 month ago
40
}
41
bf7bb1b8 Andy McCall 1 month ago ↶
42
43
# [ derive ( Clone, Copy, Debug, PartialEq, Eq ) ]
44
pub enum Event <'a> {
45
46
Push( & 'a str ) ,
47
48
Tag( & 'a str ) ,
49
50
Manual,
51
52
Schedule,
53
}
54
55
impl On {
56
57
pub fn default_on ( ) -> On {
58
On { push: Some ( vec! [ ] ) , tag: None , manual: true , schedule: vec! [ ] }
59
}
60
61
pub fn matches ( & self , event : Event< '_ > ) -> bool {
62
let any = | globs : & Option< Vec< String> > , name : & str | match globs {
63
None => false ,
64
Some ( g) if g.is_empty ( ) => true ,
65
Some ( g) => g.iter ( ) .any ( | p | globset:: Glob:: new( p) .is_ok_and ( | p | p.compile_matcher ( ) .is_match ( name) ) ) ,
66
} ;
67
match event {
68
Event:: Push( b) => any ( & self .push, b) ,
69
Event:: Tag( t) => any ( & self .tag, t) ,
70
Event:: Manual => self .manual,
71
Event:: Schedule => ! self .schedule.is_empty ( ) ,
72
}
73
}
74
}
75
76
impl Pipeline {
77
78
pub fn on_of ( & self , job : & Job) -> On {
79
job.on.clone ( ) .or_else ( | | self .on.clone ( ) ) .unwrap_or_else ( On:: default_on)
80
}
81
}
82
e0918b88 Andy McCall 1 month ago
83
# [ derive ( Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize ) ]
84
# [ serde ( rename_all = " lowercase" ) ]
85
pub enum Runtime {
86
87
Wasm,
88
89
Syscall,
90
91
Microvm,
92
}
93
94
impl Runtime {
95
pub fn as_str ( self ) -> & 'static str {
96
match self {
97
Runtime:: Wasm => " wasm" ,
98
Runtime:: Syscall => " syscall" ,
99
Runtime:: Microvm => " microvm" ,
100
}
101
}
102
}
103
104
# [ derive ( Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize ) ]
105
# [ serde ( rename_all = " lowercase" ) ]
106
pub enum Network {
107
Isolated,
108
Loopback,
109
Egress,
110
}
111
112
impl Network {
113
pub fn as_str ( self ) -> & 'static str {
114
match self {
115
Network:: Isolated => " isolated" ,
116
Network:: Loopback => " loopback" ,
117
Network:: Egress => " egress" ,
118
}
119
}
120
}
121
122
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize, Deserialize ) ]
123
pub struct Sandbox {
124
pub name : String,
125
pub runtime : Runtime,
126
127
pub rootfs : String,
128
pub memory_mib : u32 ,
129
pub cpus : u32 ,
130
pub network : Network,
a72abeee Andy McCall 1 month ago ↶
131
132
133
#[serde(default, skip_serializing_if = "BTreeMap : : is_empty")]
134
pub env: BTreeMap< String, String> ,
e576e086 Andy McCall 1 month ago ↶
135
136
#[serde(default, skip_serializing_if = "Vec : : is_empty")]
137
pub runs_on: Vec< String> ,
ea81796b Andy McCall 20 days ago ↶
138
139
140
141
#[serde(default, skip_serializing_if = "Vec : : is_empty")]
142
pub egress_allow: Vec< String> ,
02b127d2 Andy McCall 1 month ago ↶
143
144
145
#[serde(default, skip_serializing_if = "Option : : is_none")]
146
pub timeout_s: Option< u64 > ,
1abadd4a Andy McCall 1 month ago ↶
147
148
149
#[serde(default, skip_serializing_if = "Option : : is_none")]
150
pub arch: Option< String> ,
151
}
152
153
154
pub fn arch_ok ( a : & str ) -> bool {
155
( 2 .. = 16 ) .contains ( & a.len ( ) ) & & a.bytes ( ) .all ( | b | b.is_ascii_lowercase ( ) | | b.is_ascii_digit ( ) | | b = = b ' _' )
e0918b88 Andy McCall 1 month ago
156
}
157
158
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize, Deserialize ) ]
159
pub struct Job {
160
pub name : String,
161
pub sandbox : String,
162
163
pub scope : Vec< String> ,
164
165
pub depends_on : Vec< String> ,
166
pub env : BTreeMap< String, String> ,
167
168
pub exec : Vec< String> ,
fc10d9e8 Andy McCall 1 month ago ↶
169
170
#[serde(default, skip_serializing_if = "Vec : : is_empty")]
171
pub mounts: Vec< Mount> ,
a67b97e9 Andy McCall 1 month ago ↶
172
173
#[serde(default, skip_serializing_if = "Vec : : is_empty")]
174
pub outputs: Vec< Output> ,
46fe2c81 Andy McCall 20 days ago ↶
175
176
177
178
#[serde(default, skip_serializing_if = "Vec : : is_empty")]
179
pub inputs: Vec< Input> ,
e2089721 Andy McCall 20 days ago ↶
180
181
182
#[serde(default, skip_serializing_if = "Option : : is_none")]
183
pub approval: Option< Approval> ,
f4e51e63 Andy McCall 20 days ago ↶
184
185
186
187
#[serde(default, skip_serializing_if = "Option : : is_none")]
188
pub environment: Option< String> ,
e2c12561 Andy McCall 1 month ago ↶
189
190
191
#[serde(default, skip_serializing_if = "Vec : : is_empty")]
192
pub secrets: Vec< Secret> ,
720eb819 Andy McCall 1 month ago ↶
193
194
195
#[serde(default, skip_serializing_if = "Vec : : is_empty")]
196
pub identities: Vec< Identity> ,
e576e086 Andy McCall 1 month ago ↶
197
198
199
#[serde(default, skip_serializing_if = "Vec : : is_empty")]
200
pub runs_on: Vec< String> ,
02b127d2 Andy McCall 1 month ago ↶
201
202
203
#[serde(default, skip_serializing_if = "Option : : is_none")]
204
pub timeout_s: Option< u64 > ,
bf7bb1b8 Andy McCall 1 month ago ↶
205
206
#[serde(default, skip_serializing_if = "Option : : is_none")]
207
pub on: Option< On> ,
e87c1c13 Andy McCall 19 days ago ↶
208
209
210
211
#[serde(default, skip_serializing_if = "Option : : is_none")]
212
pub priority: Option< String> ,
f201e1bc Andy McCall 11 days ago ↶
213
214
215
216
#[serde(default, skip_serializing_if = "Vec : : is_empty")]
217
pub checkpoints: Vec< String> ,
fc10d9e8 Andy McCall 1 month ago ↶
218
}
219
c4285535 Andy McCall 1 month ago ↶
220
impl Job {
a72abeee Andy McCall 1 month ago ↶
221
222
pub fn in_sandbox ( & self , sandbox : & Sandbox) -> Job {
223
let mut job = self .clone ( ) ;
224
for ( k, v) in & sandbox.env {
225
job.env.entry ( k.clone ( ) ) .or_insert_with ( | | v.clone ( ) ) ;
226
}
e576e086 Andy McCall 1 month ago ↶
227
job.runs_on.extend ( sandbox.runs_on.iter ( ) .cloned ( ) ) ;
228
job.runs_on.sort ( ) ;
229
job.runs_on.dedup ( ) ;
02b127d2 Andy McCall 1 month ago ↶
230
job.timeout_s = job.timeout_s.or ( sandbox.timeout_s) ;
a72abeee Andy McCall 1 month ago ↶
231
job
232
}
233
c4285535 Andy McCall 1 month ago ↶
234
235
236
237
pub fn with_flow_env ( & self , vars : & [( String , String ) ]) -> Job {
238
let mut job = self .clone ( ) ;
239
for ( k, v) in vars {
240
job.env.entry ( k.clone ( ) ) .or_insert_with ( | | v.clone ( ) ) ;
241
}
242
job
243
}
244
}
245
fc10d9e8 Andy McCall 1 month ago ↶
246
d60aac6c Andy McCall 23 days ago ↶
247
248
249
fc10d9e8 Andy McCall 1 month ago ↶
250
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize, Deserialize ) ]
251
pub struct Mount {
252
pub source : MountSource,
253
254
pub target : String,
255
pub size_mib : u32 ,
d60aac6c Andy McCall 23 days ago ↶
256
257
258
#[serde(default, skip_serializing_if = "Option : : is_none")]
259
pub cache: Option< String> ,
260
261
#[serde(default, skip_serializing_if = "Vec : : is_empty")]
262
pub key: Vec< String> ,
fc10d9e8 Andy McCall 1 month ago ↶
263
}
264
265
# [ derive ( Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize ) ]
266
# [ serde ( rename_all = " lowercase" ) ]
267
pub enum MountSource {
268
269
Tmpfs,
270
271
Disk,
272
}
273
274
impl MountSource {
275
pub fn as_str ( self ) -> & 'static str {
276
match self {
277
MountSource:: Tmpfs => " ephemeral://tmpfs" ,
278
MountSource:: Disk => " ephemeral://disk" ,
279
}
280
}
a67b97e9 Andy McCall 1 month ago ↶
281
}
282
283
284
285
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize, Deserialize ) ]
286
pub struct Output {
287
288
pub name : String,
289
290
pub path : String,
9a6fac2b Andy McCall 1 month ago ↶
291
292
293
#[serde(default, skip_serializing_if = "std : : ops:: Not :: not")]
294
pub always: bool ,
6d53fb7d Andy McCall 25 days ago ↶
295
296
297
#[serde(default, skip_serializing_if = "OutputKind : : is_artifact")]
298
pub kind: OutputKind,
46fe2c81 Andy McCall 20 days ago ↶
299
}
300
301
302
303
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize, Deserialize ) ]
304
pub struct Input {
305
306
pub artifact : String,
307
308
pub from : String,
309
310
pub path : String,
e2089721 Andy McCall 20 days ago ↶
311
}
312
313
314
315
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize, Deserialize ) ]
316
pub struct Approval {
317
318
pub by : String,
319
320
#[serde(default, skip_serializing_if = "std : : ops:: Not :: not")]
321
pub others_only: bool ,
322
323
pub expires_s : u64 ,
2be1fe4a Andy McCall 20 days ago ↶
324
325
326
327
#[serde(default, skip_serializing_if = "std : : ops:: Not :: not")]
328
pub sealed: bool ,
e2c12561 Andy McCall 1 month ago ↶
329
}
46fe2c81 Andy McCall 20 days ago ↶
330
e2089721 Andy McCall 20 days ago ↶
331
332
const MAX_APPROVAL_S : u64 = 30 * 24 * 3600 ;
333
46fe2c81 Andy McCall 20 days ago ↶
334
335
const MAX_INPUTS : usize = 8 ;
e2c12561 Andy McCall 1 month ago ↶
336
6d53fb7d Andy McCall 25 days ago ↶
337
338
# [ derive ( Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize ) ]
339
# [ serde ( rename_all = " lowercase" ) ]
340
pub enum OutputKind {
341
# [ default ]
342
Artifact,
343
Report,
344
}
345
346
impl OutputKind {
347
pub fn is_artifact ( & self ) -> bool {
348
* self = = OutputKind:: Artifact
349
}
350
351
pub fn word ( & self ) -> & 'static str {
352
match self {
353
OutputKind:: Artifact => " artifact" ,
354
OutputKind:: Report => " report" ,
355
}
356
}
357
}
358
e2c12561 Andy McCall 1 month ago ↶
359
360
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize, Deserialize ) ]
361
pub struct Secret {
362
363
pub name : String,
364
365
366
pub mount_path : String,
a67b97e9 Andy McCall 1 month ago ↶
367
}
368
720eb819 Andy McCall 1 month ago ↶
369
370
371
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize, Deserialize ) ]
372
pub struct Identity {
373
374
pub audience : String,
375
376
377
pub mount_path : String,
378
}
379
e2c12561 Andy McCall 1 month ago ↶
380
381
pub fn secret_path_ok ( p : & str ) -> bool {
382
p.strip_prefix ( " /run/secrets/" ) .is_some_and ( | rest | ! rest.is_empty ( ) & & rest.split ( ' /' ) .all ( | c | ! c.is_empty ( ) & & c ! = " ." & & c ! = " .." ) )
383
}
384
a67b97e9 Andy McCall 1 month ago ↶
385
386
pub const MAX_OUTPUTS : usize = 16 ;
387
388
389
390
fn output_path_ok ( p : & str ) -> bool {
391
p.strip_prefix ( " /src/" ) .is_some_and ( | rest | {
392
! rest.is_empty ( )
393
& & rest.split ( ' /' ) .all ( | c | ! c.is_empty ( ) & & c ! = " ." & & c ! = " .." )
394
& & p.bytes ( ) .all ( | b | b.is_ascii_graphic ( ) & & b ! = b ' "' & & b ! = b ' \\ ' )
395
} )
fc10d9e8 Andy McCall 1 month ago ↶
396
}
397
398
399
pub const MAX_MOUNTS : usize = 4 ;
e2c12561 Andy McCall 1 month ago ↶
400
401
pub const MAX_SECRETS : usize = 8 ;
720eb819 Andy McCall 1 month ago ↶
402
403
pub const MAX_IDENTITIES : usize = 4 ;
fc10d9e8 Andy McCall 1 month ago ↶
404
405
pub const MIN_MOUNT_MIB : u64 = 16 ;
406
pub const MAX_MOUNT_MIB : u64 = 64 * 1024 ;
407
408
409
fn mount_target_ok ( t : & str ) -> bool {
410
let under = t.strip_prefix ( " /src/" ) .or_else ( | | t.strip_prefix ( " /tmp/" ) ) ;
411
under.is_some_and ( | rest | ! rest.is_empty ( ) & & rest.split ( ' /' ) .all ( | c | ! c.is_empty ( ) & & c ! = " ." & & c ! = " .." ) )
e0918b88 Andy McCall 1 month ago
412
}
413
414
415
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize, Deserialize ) ]
416
pub struct Problem {
417
pub line : usize ,
418
pub message : String,
419
}
420
421
impl fmt:: Display for Problem {
422
fn fmt ( & self , f : & mut fmt:: Formatter< '_ > ) -> fmt:: Result {
423
write! ( f, " line {} : {} " , self .line, self .message)
424
}
425
}
426
427
428
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize, Deserialize, thiserror::Error ) ]
429
pub struct Problems ( pub Vec< Problem> ) ;
430
431
impl fmt:: Display for Problems {
432
fn fmt ( & self , f : & mut fmt:: Formatter< '_ > ) -> fmt:: Result {
433
let lines: Vec< String> = self .0. iter ( ) .map ( | p | p.to_string ( ) ) .collect ( ) ;
434
f.write_str ( & lines.join ( " \n " ) )
435
}
436
}
437
438
const MIN_MEMORY_MIB : u64 = 64 ;
439
const MAX_MEMORY_MIB : u64 = 16 * 1024 ;
440
const MAX_CPUS : i128 = 16 ;
441
442
443
const LATER_TOP : & [ & str ] = & [ " publish" , " secret" ] ;
e2c12561 Andy McCall 1 month ago ↶
444
const LATER_JOB : & [ & str ] = & [ " attest" ] ;
e0918b88 Andy McCall 1 month ago
445
446
struct Ctx < 'a > {
447
src : & 'a str ,
448
problems : Vec< Problem> ,
449
}
450
451
impl Ctx < '_ > {
452
fn line ( & self , span : SourceSpan) -> usize {
453
let at = span.offset ( ) .min ( self .src.len ( ) ) ;
454
self .src[ .. at] .bytes ( ) .filter ( | b | * b = = b ' \n ' ) .count ( ) + 1
455
}
456
457
fn err ( & mut self , span : SourceSpan, message : impl Into< String> ) {
458
let line = self .line ( span) ;
459
self .problems.push ( Problem { line, message: message.into ( ) } ) ;
460
}
461
462
463
fn strings ( & mut self , node : & KdlNode) -> Vec< String> {
464
let mut out = Vec :: new( ) ;
465
for e in node.entries ( ) {
466
if let Some ( k) = e.name ( ) {
467
self .err ( e.span ( ) , format! ( " `{} ` takes no property `{} `" , node.name ( ) .value ( ) , k.value ( ) ) ) ;
468
continue ;
469
}
470
match e.value ( ) {
471
KdlValue:: String( s) => out.push ( s.clone ( ) ) ,
472
other => self .err ( e.span ( ) , format! ( " `{} ` takes strings, not {} " , node.name ( ) .value ( ) , kind_of ( other) ) ) ,
473
}
474
}
475
out
476
}
477
478
479
fn one_string ( & mut self , node : & KdlNode) -> Option< String> {
480
let v = self .strings ( node) ;
481
if v.len ( ) ! = 1 {
482
self .err ( node.span ( ) , format! ( " `{} ` takes one string" , node.name ( ) .value ( ) ) ) ;
483
return None ;
484
}
485
v.into_iter ( ) .next ( )
486
}
487
488
fn one_integer ( & mut self , node : & KdlNode) -> Option< i128> {
489
match node.entries ( ) {
490
[ e] if e.name ( ) .is_none ( ) => match e.value ( ) {
491
KdlValue:: Integer( n) => Some ( * n) ,
492
other => {
493
self .err ( e.span ( ) , format! ( " `{} ` takes an integer, not {} " , node.name ( ) .value ( ) , kind_of ( other) ) ) ;
494
None
495
}
496
} ,
497
_ => {
498
self .err ( node.span ( ) , format! ( " `{} ` takes one integer" , node.name ( ) .value ( ) ) ) ;
499
None
500
}
501
}
502
}
503
504
fn no_children ( & mut self , node : & KdlNode) {
505
if node.children ( ) .is_some ( ) {
506
self .err ( node.span ( ) , format! ( " `{} ` takes no block" , node.name ( ) .value ( ) ) ) ;
507
}
508
}
509
510
fn name_ok ( & mut self , span : SourceSpan, what : & str , name : & str ) -> bool {
511
let ok = ! name.is_empty ( ) & & name.len ( ) < = 64 & & name.bytes ( ) .all ( | b | b.is_ascii_alphanumeric ( ) | | b " ._-" .contains ( & b) ) ;
512
if ! ok {
513
self .err ( span, format! ( " {what} name {name:?} must be 1 to 64 of A-Z a-z 0-9 . _ -" ) ) ;
514
}
515
ok
516
}
517
}
518
519
use kdl:: { KdlDocument, KdlNode, KdlValue} ;
520
use miette:: SourceSpan;
521
522
fn kind_of ( v : & KdlValue) -> & 'static str {
523
match v {
524
KdlValue:: String( _ ) => " a string" ,
525
KdlValue:: Integer( _ ) => " an integer" ,
526
KdlValue:: Float( _ ) => " a number" ,
527
KdlValue:: Bool( _ ) => " a boolean" ,
528
KdlValue:: Null => " null" ,
529
}
530
}
531
532
533
fn parse_memory ( s : & str ) -> Option< u64 > {
534
let ( num, unit) = s.split_at ( s.find ( | c : char | ! c.is_ascii_digit ( ) ) ? ) ;
535
let n: u64 = num.parse ( ) .ok ( ) ? ;
536
let mib = match unit {
537
" KiB" => n.is_multiple_of ( 1024 ) .then_some ( n / 1024 ) ? ,
538
" MiB" => n,
539
" GiB" => n.checked_mul ( 1024 ) ? ,
540
_ => return None ,
541
} ;
542
Some ( mib)
543
}
544
b647ab72 Andy McCall 1 month ago ↶
545
546
547
fn foreign_escape_line ( src : & str ) -> Option< usize > {
548
src.lines ( )
549
.position ( | l | {
550
let b = l.as_bytes ( ) ;
551
b.windows ( 2 ) .enumerate ( ) .any ( | ( i , w ) | {
552
w[ 0 ] = = b ' \\ '
553
& & ( i = = 0 | | b[ i - 1 ] ! = b ' \\ ' )
554
& & ( w[ 1 ] = = b ' e' | | w[ 1 ] = = b ' x' | | ( w[ 1 ] = = b ' 0' & & b.get ( i + 2 ) .is_some_and ( u8 :: is_ascii_digit) ) )
555
} )
556
} )
557
.map ( | i | i + 1 )
558
}
559
e0918b88 Andy McCall 1 month ago
560
impl Pipeline {
561
562
pub fn parse ( src : & str ) -> Result< Pipeline, Problems> {
563
let doc = match KdlDocument:: parse_v2( src) {
564
Ok ( d) => d,
565
Err ( e) => {
566
let mut ctx = Ctx { src, problems: Vec :: new( ) } ;
567
for d in & e.diagnostics {
568
let msg = d.message.clone ( ) .or_else ( | | d.label.clone ( ) ) .unwrap_or_else ( | | " not valid KDL 2.0" .into ( ) ) ;
569
ctx.err ( d.span, msg) ;
570
}
571
if ctx.problems.is_empty ( ) {
572
ctx.problems.push ( Problem { line: 1 , message: " not valid KDL 2.0" .into ( ) } ) ;
b647ab72 Andy McCall 1 month ago ↶
573
}
574
if let Some ( line) = foreign_escape_line ( src) {
575
ctx.problems.push ( Problem {
576
line,
577
message: " KDL strings have no \\ 033, \\ e or \\ x escapes: write ESC (for ANSI colours) as \\ u{1b}" .into ( ) ,
578
} ) ;
e0918b88 Andy McCall 1 month ago
579
}
580
return Err ( Problems( ctx.problems) ) ;
581
}
582
} ;
583
let mut ctx = Ctx { src, problems: Vec :: new( ) } ;
584
let mut schema = None ;
585
let mut sandboxes = BTreeMap:: new( ) ;
586
let mut jobs: BTreeMap< String, ( Job, SourceSpan) > = BTreeMap:: new( ) ;
bf7bb1b8 Andy McCall 1 month ago ↶
587
let mut on = None ;
e87c1c13 Andy McCall 19 days ago ↶
588
let mut priority = None ;
e0918b88 Andy McCall 1 month ago
589
for node in doc.nodes ( ) {
590
match node.name ( ) .value ( ) {
e87c1c13 Andy McCall 19 days ago ↶
591
" priority" => {
592
if priority.is_some ( ) {
593
ctx.err ( node.span ( ) , " `priority` is given twice" ) ;
594
}
595
priority = parse_priority ( & mut ctx, node) ;
596
}
bf7bb1b8 Andy McCall 1 month ago ↶
597
" on" => {
598
if on.is_some ( ) {
599
ctx.err ( node.span ( ) , " `on` is given twice" ) ;
600
}
601
on = Some ( parse_on ( & mut ctx, node) ) ;
602
}
e0918b88 Andy McCall 1 month ago
603
" schema-version" => {
604
ctx.no_children ( node) ;
605
match ctx.one_integer ( node) {
606
Some ( 1 ) => schema = Some ( 1 ) ,
607
Some ( n) => ctx.err ( node.span ( ) , format! ( " schema-version {n} is not supported; this runner reads 1" ) ) ,
608
None => { }
609
}
610
}
611
" sandbox" => {
612
if let Some ( sb) = parse_sandbox ( & mut ctx, node) {
613
if sandboxes.contains_key ( & sb.name) {
614
ctx.err ( node.span ( ) , format! ( " sandbox {:?} is declared twice" , sb.name) ) ;
615
} else {
616
sandboxes.insert ( sb.name.clone ( ) , sb) ;
617
}
618
}
619
}
620
" job" => {
621
if let Some ( job) = parse_job ( & mut ctx, node) {
622
if jobs.contains_key ( & job.name) {
623
ctx.err ( node.span ( ) , format! ( " job {:?} is declared twice" , job.name) ) ;
624
} else {
625
jobs.insert ( job.name.clone ( ) , ( job, node.span ( ) ) ) ;
626
}
627
}
628
}
629
n if LATER_TOP .contains ( & n) => ctx.err ( node.span ( ) , format! ( " `{n} ` is not in M0 yet" ) ) ,
e87c1c13 Andy McCall 19 days ago ↶
630
n => ctx.err ( node.span ( ) , format! ( " unknown node `{n} `; expected schema-version, on, priority, sandbox or job" ) ) ,
e0918b88 Andy McCall 1 month ago
631
}
632
}
633
if schema.is_none ( ) & & ! ctx.problems.iter ( ) .any ( | p | p.message.contains ( " schema-version" ) ) {
634
ctx.problems.push ( Problem { line: 1 , message: " `schema-version 1` is required" .into ( ) } ) ;
635
}
636
if jobs.is_empty ( ) & & ctx.problems.is_empty ( ) {
637
ctx.problems.push ( Problem { line: 1 , message: " a pipeline needs at least one job" .into ( ) } ) ;
638
}
639
640
for ( job, span) in jobs.values ( ) {
641
if ! job.sandbox.is_empty ( ) & & ! sandboxes.contains_key ( & job.sandbox) {
642
ctx.err ( * span, format! ( " job {:?} uses sandbox {:?} , which is not declared" , job.name, job.sandbox) ) ;
643
}
644
for d in & job.depends_on {
645
if ! jobs.contains_key ( d) {
646
ctx.err ( * span, format! ( " job {:?} depends on {:?} , which is not declared" , job.name, d) ) ;
647
} else if d = = & job.name {
648
ctx.err ( * span, format! ( " job {:?} depends on itself" , job.name) ) ;
46fe2c81 Andy McCall 20 days ago ↶
649
}
650
}
651
652
653
for i in & job.inputs {
654
let Some ( ( producer, _ ) ) = jobs.get ( & i.from) else {
655
ctx.err ( * span, format! ( " job {:?} takes an input from {:?} , which is not declared" , job.name, i.from) ) ;
656
continue ;
657
} ;
658
if ! job.depends_on.contains ( & i.from) {
659
ctx.err ( * span, format! ( " job {:?} takes {:?} from {:?} : add depends-on \" {} \" " , job.name, i.artifact, i.from, i.from) ) ;
e0918b88 Andy McCall 1 month ago
660
}
46fe2c81 Andy McCall 20 days ago ↶
661
match producer.outputs.iter ( ) .find ( | o | o.name = = i.artifact ) {
662
Some ( o) if o.kind = = OutputKind:: Artifact => { }
663
Some ( _ ) => ctx.err ( * span, format! ( " {:?} of job {:?} is a report; inputs are artifacts" , i.artifact, i.from) ) ,
664
None => ctx.err ( * span, format! ( " job {:?} declares no artifact {:?} " , i.from, i.artifact) ) ,
665
}
e0918b88 Andy McCall 1 month ago
666
}
667
}
668
let graph: BTreeMap< & str , Vec< & str > > =
669
jobs.values ( ) .map ( | ( j , _) | ( j.name.as_str ( ) , j.depends_on.iter ( ) .map ( String :: as_str) .collect ( ) ) ) .collect ( ) ;
670
if let Some ( cycle) = find_cycle ( & graph) {
671
let span = jobs[ cycle[ 0 ] ] .1 ;
672
ctx.err ( span, format! ( " jobs depend on each other in a cycle: {} " , cycle.join ( " → " ) ) ) ;
673
}
674
if ! ctx.problems.is_empty ( ) {
675
ctx.problems.sort_by_key ( | p | p.line ) ;
676
return Err ( Problems( ctx.problems) ) ;
677
}
e87c1c13 Andy McCall 19 days ago ↶
678
679
let jobs = jobs
680
.into_iter ( )
681
.map ( | ( k , (mut j , _) ) | {
682
if j.priority.is_none ( ) {
683
j.priority.clone_from ( & priority) ;
684
}
685
( k, j)
686
} )
687
.collect ( ) ;
688
Ok ( Pipeline { schema_version: 1 , sandboxes, jobs, on } )
e0918b88 Andy McCall 1 month ago
689
}
690
691
692
pub fn order ( & self ) -> Vec< & Job> {
693
let mut done: BTreeSet< & str > = BTreeSet:: new( ) ;
694
let mut out = Vec :: with_capacity( self .jobs.len ( ) ) ;
695
while out.len ( ) < self .jobs.len ( ) {
696
let before = out.len ( ) ;
697
for j in self .jobs.values ( ) {
698
if ! done.contains ( j.name.as_str ( ) ) & & j.depends_on.iter ( ) .all ( | d | done.contains ( d.as_str ( ) ) ) {
699
out.push ( j) ;
700
}
701
}
702
for j in & out[ before.. ] {
703
done.insert ( j.name.as_str ( ) ) ;
704
}
705
if out.len ( ) = = before {
706
break ;
707
}
708
}
709
out
710
}
711
712
713
pub fn depths ( & self ) -> BTreeMap< & str , usize > {
714
let mut depth: BTreeMap< & str , usize > = BTreeMap:: new( ) ;
715
for j in self .order ( ) {
716
let d = j.depends_on.iter ( ) .map ( | x | depth.get ( x.as_str ( ) ) .copied ( ) .unwrap_or ( 0 ) + 1 ) .max ( ) .unwrap_or ( 0 ) ;
717
depth.insert ( j.name.as_str ( ) , d) ;
718
}
719
depth
720
}
721
}
722
723
fn find_cycle < 'a > ( graph : & BTreeMap< & 'a str , Vec< & 'a str > > ) -> Option< Vec< & 'a str > > {
724
# [ derive ( Clone, Copy, PartialEq ) ]
725
enum Mark {
726
Visiting,
727
Done,
728
}
729
fn visit < 'a > (
730
n : & 'a str ,
731
graph : & BTreeMap< & 'a str , Vec< & 'a str > > ,
732
marks : & mut BTreeMap< & 'a str , Mark> ,
733
stack : & mut Vec< & 'a str > ,
734
) -> Option< Vec< & 'a str > > {
735
match marks.get ( n) {
736
Some ( Mark:: Done) => return None ,
737
Some ( Mark:: Visiting) => {
738
let at = stack.iter ( ) .position ( | s | * s = = n) .unwrap_or ( 0 ) ;
739
let mut c = stack[ at.. ] .to_vec ( ) ;
740
c.push ( n) ;
741
return Some ( c) ;
742
}
743
None => { }
744
}
745
marks.insert ( n, Mark:: Visiting) ;
746
stack.push ( n) ;
747
for d in graph.get ( n) .into_iter ( ) .flatten ( ) {
748
if graph.contains_key ( d) & & * d ! = n {
749
if let Some ( c) = visit ( d, graph, marks, stack) {
750
return Some ( c) ;
751
}
752
}
753
}
754
stack.pop ( ) ;
755
marks.insert ( n, Mark:: Done) ;
756
None
757
}
758
let mut marks = BTreeMap:: new( ) ;
759
for n in graph.keys ( ) {
760
let mut stack = Vec :: new( ) ;
761
if let Some ( c) = visit ( n, graph, & mut marks, & mut stack) {
762
return Some ( c) ;
763
}
764
}
765
None
766
}
767
768
fn block < 'a > ( ctx : & mut Ctx, node : & 'a KdlNode) -> & 'a [ KdlNode] {
769
match node.children ( ) {
770
Some ( d) => d.nodes ( ) ,
771
None => {
772
ctx.err ( node.span ( ) , format! ( " `{} ` needs a block {{ … }} " , node.name ( ) .value ( ) ) ) ;
773
& [ ]
774
}
775
}
776
}
777
778
fn parse_sandbox ( ctx : & mut Ctx, node : & KdlNode) -> Option< Sandbox> {
779
let name = ctx.one_string ( node) ? ;
780
let named = ctx.name_ok ( node.span ( ) , " sandbox" , & name) ;
781
let ( mut runtime, mut rootfs, mut memory, mut cpus, mut network) =
782
( Some ( Runtime:: Microvm) , None , Some ( 1024 u32 ) , Some ( 1 u32 ) , Some ( Network:: Isolated) ) ;
783
let mut seen = BTreeSet:: new( ) ;
a72abeee Andy McCall 1 month ago ↶
784
let mut env = BTreeMap:: new( ) ;
e576e086 Andy McCall 1 month ago ↶
785
let mut runs_on = Vec :: new( ) ;
02b127d2 Andy McCall 1 month ago ↶
786
let mut timeout_s = None ;
1abadd4a Andy McCall 1 month ago ↶
787
let mut arch: Option< String> = None ;
ea81796b Andy McCall 20 days ago ↶
788
let mut egress_allow = Vec :: new( ) ;
e0918b88 Andy McCall 1 month ago
789
for c in block ( ctx, node) {
790
let key = c.name ( ) .value ( ) ;
e576e086 Andy McCall 1 month ago ↶
791
if key ! = " runs-on" & & ! seen.insert ( key.to_string ( ) ) {
e0918b88 Andy McCall 1 month ago
792
ctx.err ( c.span ( ) , format! ( " `{key} ` is given twice" ) ) ;
a72abeee Andy McCall 1 month ago ↶
793
}
794
if key = = " env" {
795
parse_env ( ctx, c, & mut env) ;
796
continue ;
e0918b88 Andy McCall 1 month ago
797
}
e576e086 Andy McCall 1 month ago ↶
798
if key = = " runs-on" {
799
ctx.no_children ( c) ;
800
parse_labels ( ctx, c, & mut runs_on) ;
801
continue ;
ea81796b Andy McCall 20 days ago ↶
802
}
803
if key ! = " network" {
804
ctx.no_children ( c) ;
e576e086 Andy McCall 1 month ago ↶
805
}
e0918b88 Andy McCall 1 month ago
806
match key {
807
" runtime" => {
808
runtime = match ctx.one_string ( c) .as_deref ( ) {
809
Some ( " microvm" ) => Some ( Runtime:: Microvm) ,
810
Some ( " syscall" ) | Some ( " gvisor" ) => Some ( Runtime:: Syscall) ,
28cc603f Andy McCall 11 days ago ↶
811
812
Some ( " wasm" ) => Some ( Runtime:: Wasm) ,
e0918b88 Andy McCall 1 month ago
813
Some ( other) => {
814
ctx.err ( c.span ( ) , format! ( " runtime {other:?} : expected \" microvm\" , \" syscall\" or \" wasm\" " ) ) ;
815
None
816
}
817
None => None ,
818
}
819
}
820
" rootfs" => {
821
rootfs = match ctx.one_string ( c) {
822
Some ( r) => match r.strip_prefix ( " cas://blob/" ) {
823
Some ( h) if h.len ( ) = = 64 & & h.bytes ( ) .all ( | b | b.is_ascii_hexdigit ( ) & & ! b.is_ascii_uppercase ( ) ) => {
824
Some ( h.to_string ( ) )
825
}
826
_ => {
827
ctx.err ( c.span ( ) , format! ( " rootfs {r:?} : expected \" cas://blob/\" and 64 lower-case hex digits" ) ) ;
828
None
829
}
830
} ,
831
None => None ,
832
}
833
}
1abadd4a Andy McCall 1 month ago ↶
834
" arch" => {
835
arch = match ctx.one_string ( c) {
836
Some ( a) if arch_ok ( & a) => Some ( a) ,
837
Some ( a) => {
838
ctx.err ( c.span ( ) , format! ( " arch {a:?} is not an architecture name such as x86_64, aarch64 or riscv64" ) ) ;
839
None
840
}
841
None => None ,
842
}
843
}
e0918b88 Andy McCall 1 month ago
844
" memory-limit" => {
845
memory = match ctx.one_string ( c) {
846
Some ( m) => match parse_memory ( & m) {
847
Some ( mib) if ( MIN_MEMORY_MIB .. = MAX_MEMORY_MIB ) .contains ( & mib) => Some ( mib as u32 ) ,
848
Some ( _ ) => {
849
ctx.err ( c.span ( ) , format! ( " memory-limit {m:?} must be from 64MiB to 16GiB" ) ) ;
850
None
851
}
852
None => {
853
ctx.err ( c.span ( ) , format! ( " memory-limit {m:?} : expected a whole number of KiB, MiB or GiB, as \" 4GiB\" " ) ) ;
854
None
855
}
856
} ,
857
None => None ,
858
}
859
}
860
" cpu-limit" => {
861
cpus = match ctx.one_integer ( c) {
862
Some ( n) if ( 1 .. = MAX_CPUS ) .contains ( & n) => Some ( n as u32 ) ,
863
Some ( n) => {
864
ctx.err ( c.span ( ) , format! ( " cpu-limit {n} must be from 1 to {MAX_CPUS} " ) ) ;
865
None
866
}
867
None => None ,
868
}
869
}
870
" network" => {
ea81796b Andy McCall 20 days ago ↶
871
let kind = ctx.one_string ( c) ;
872
if kind.as_deref ( ) ! = Some ( " egress" ) {
873
ctx.no_children ( c) ;
874
}
875
network = match kind.as_deref ( ) {
e0918b88 Andy McCall 1 month ago
876
Some ( " isolated" ) => Some ( Network:: Isolated) ,
eb5c4db6 Andy McCall 1 month ago ↶
877
878
879
Some ( " loopback" ) => Some ( Network:: Loopback) ,
ea81796b Andy McCall 20 days ago ↶
880
881
882
eb5c4db6 Andy McCall 1 month ago ↶
883
Some ( " egress" ) => {
ea81796b Andy McCall 20 days ago ↶
884
egress_allow = parse_egress ( ctx, c) ;
885
Some ( Network:: Egress)
e0918b88 Andy McCall 1 month ago
886
}
887
Some ( other) => {
888
ctx.err ( c.span ( ) , format! ( " network {other:?} : expected \" isolated\" , \" loopback\" or \" egress\" " ) ) ;
889
None
890
}
891
None => None ,
892
}
893
}
02b127d2 Andy McCall 1 month ago ↶
894
" timeout" => timeout_s = parse_timeout ( ctx, c) ,
e0918b88 Andy McCall 1 month ago
895
other => ctx.err ( c.span ( ) , format! ( " unknown sandbox setting `{other} `" ) ) ,
896
}
897
}
28cc603f Andy McCall 11 days ago ↶
898
899
900
let wasm = runtime = = Some ( Runtime:: Wasm) ;
901
if wasm {
902
if seen.contains ( " rootfs" ) {
903
ctx.err ( node.span ( ) , format! ( " sandbox {name:?} : runtime \" wasm\" takes no rootfs; its module is a file the job has" ) ) ;
904
}
905
if network.is_some_and ( | n | n ! = Network:: Isolated ) {
906
ctx.err ( node.span ( ) , format! ( " sandbox {name:?} : runtime \" wasm\" has no network; leave out `network` or say \" isolated\" " ) ) ;
907
}
908
rootfs = Some ( String :: new( ) ) ;
909
network = Some ( Network:: Isolated) ;
910
} else if rootfs.is_none ( ) & & ! seen.contains ( " rootfs" ) {
e0918b88 Andy McCall 1 month ago
911
ctx.err ( node.span ( ) , format! ( " sandbox {name:?} needs a rootfs" ) ) ;
912
}
ea81796b Andy McCall 20 days ago ↶
913
if network = = Some ( Network:: Egress) {
914
if runtime = = Some ( Runtime:: Syscall) {
915
ctx.err ( node.span ( ) , format! ( " sandbox {name:?} : network \" egress\" needs runtime \" microvm\" " ) ) ;
916
}
917
918
if ! runs_on.iter ( ) .any ( | l | l = = EGRESS_LABEL ) {
919
runs_on.push ( EGRESS_LABEL .to_string ( ) ) ;
920
runs_on.sort ( ) ;
921
}
922
}
e0918b88 Andy McCall 1 month ago
923
if ! named {
924
return None ;
e576e086 Andy McCall 1 month ago ↶
925
}
1abadd4a Andy McCall 1 month ago ↶
926
Some ( Sandbox {
927
name,
928
runtime: runtime? ,
929
rootfs: rootfs? ,
930
memory_mib: memory? ,
931
cpus: cpus? ,
932
network: network? ,
933
env,
934
runs_on,
ea81796b Andy McCall 20 days ago ↶
935
egress_allow,
1abadd4a Andy McCall 1 month ago ↶
936
timeout_s,
937
arch,
938
} )
ea81796b Andy McCall 20 days ago ↶
939
}
940
941
942
943
pub const EGRESS_LABEL : & str = " egress" ;
944
945
946
const MAX_EGRESS_HOSTS : usize = 32 ;
947
948
949
950
pub fn egress_pattern_ok ( pattern : & str ) -> bool {
951
let name = pattern.strip_prefix ( " *." ) .unwrap_or ( pattern) ;
952
let labels: Vec< & str > = name.split ( ' .' ) .collect ( ) ;
953
name.len ( ) < = 253
954
& & labels.len ( ) > = 2
955
& & labels.iter ( ) .all ( | l | {
956
! l.is_empty ( )
957
& & l.len ( ) < = 63
958
& & ! l.starts_with ( ' -' )
959
& & ! l.ends_with ( ' -' )
960
& & l.bytes ( ) .all ( | b | b.is_ascii_lowercase ( ) | | b.is_ascii_digit ( ) | | b = = b ' -' )
961
} )
962
}
963
964
965
966
pub fn egress_allows ( patterns : & [String], host : & str ) -> bool {
967
let host = host.trim_end_matches ( ' .' ) .to_ascii_lowercase ( ) ;
968
patterns.iter ( ) .any ( | p | match p.strip_prefix ( " *." ) {
969
Some ( domain) => host.len ( ) > domain.len ( ) + 1 & & host.ends_with ( domain) & & host[ .. host.len ( ) - domain.len ( ) ] .ends_with ( ' .' ) ,
970
None => host = = * p,
971
} )
972
}
973
974
975
fn parse_egress ( ctx : & mut Ctx, node : & KdlNode) -> Vec< String> {
976
let mut out = Vec :: new( ) ;
977
for c in block ( ctx, node) {
978
ctx.no_children ( c) ;
979
if c.name ( ) .value ( ) ! = " allow" {
980
ctx.err ( c.span ( ) , format! ( " unknown egress setting `{} `; egress takes `allow \" host\" ...`" , c.name ( ) .value ( ) ) ) ;
981
continue ;
982
}
983
for h in ctx.strings ( c) {
984
if egress_pattern_ok ( & h) {
985
out.push ( h) ;
986
} else {
987
ctx.err (
988
c.span ( ) ,
989
format! ( " allow {h:?} : expected a lower-case host name such as \" registry.opentofu.org\" , or \" *.\" and a domain" ) ,
990
) ;
991
}
992
}
993
}
994
out.sort ( ) ;
995
out.dedup ( ) ;
996
if out.is_empty ( ) {
997
ctx.err ( node.span ( ) , " network \" egress\" needs at least one `allow \" host\" `" ) ;
998
} else if out.len ( ) > MAX_EGRESS_HOSTS {
999
ctx.err ( node.span ( ) , format! ( " network \" egress\" may allow at most {MAX_EGRESS_HOSTS} hosts" ) ) ;
1000
}
1001
out
bf7bb1b8 Andy McCall 1 month ago ↶
1002
}
1003
1004
1005
1006
fn parse_on ( ctx : & mut Ctx, node : & KdlNode) -> On {
1007
let mut on = On:: default( ) ;
1008
if ! node.entries ( ) .is_empty ( ) {
1009
ctx.err ( node.span ( ) , " `on` takes a block: on { push \" main\" ; tag \" v*\" ; manual }" ) ;
1010
}
1011
let globs = | ctx : & mut Ctx, c : & KdlNode| -> Vec< String> {
1012
let g = ctx.strings ( c) ;
1013
for p in & g {
1014
if let Err ( e) = globset:: Glob:: new( p) {
1015
ctx.err ( c.span ( ) , format! ( " {p:?} : {e} " ) ) ;
1016
}
1017
}
1018
g
1019
} ;
1020
for c in block ( ctx, node) {
1021
ctx.no_children ( c) ;
1022
match c.name ( ) .value ( ) {
1023
" push" => on.push.get_or_insert_with ( Vec :: new) .extend ( globs ( ctx, c) ) ,
1024
" tag" => on.tag.get_or_insert_with ( Vec :: new) .extend ( globs ( ctx, c) ) ,
1025
" manual" => on.manual = true ,
1026
" schedule" => {
1027
for line in ctx.strings ( c) {
1028
if crate :: cron:: Cron:: parse( & line) .is_some ( ) {
1029
on.schedule.push ( line) ;
1030
} else {
1031
ctx.err (
1032
c.span ( ) ,
1033
format! ( " schedule {line:?} : expected five cron fields (minute hour day month weekday), as \" 0 3 * * *\" " ) ,
1034
) ;
1035
}
1036
}
1037
}
1038
other => ctx.err ( c.span ( ) , format! ( " unknown `on` event `{other} `; expected push, tag, manual or schedule" ) ) ,
1039
}
1040
}
1041
if on.push.is_none ( ) & & on.tag.is_none ( ) & & ! on.manual & & on.schedule.is_empty ( ) {
1042
ctx.err ( node.span ( ) , " `on` names no event, so nothing would run it" ) ;
1043
}
1044
on
e576e086 Andy McCall 1 month ago ↶
1045
}
1046
02b127d2 Andy McCall 1 month ago ↶
1047
1048
pub const MAX_TIMEOUT_S : u64 = 24 * 3600 ;
1049
1050
1051
1052
pub fn parse_duration ( s : & str ) -> Option< u64 > {
1053
let ( mut total, mut num, mut last) = ( 0 u64 , String :: new( ) , 0 u8 ) ;
1054
for ch in s.chars ( ) {
1055
if ch.is_ascii_digit ( ) {
1056
num.push ( ch) ;
1057
continue ;
1058
}
1059
let ( unit, mult) = match ch {
1060
' h' => ( 1 , 3600 ) ,
1061
' m' => ( 2 , 60 ) ,
1062
' s' => ( 3 , 1 ) ,
1063
_ => return None ,
1064
} ;
1065
1066
if num.is_empty ( ) | | unit < = last {
1067
return None ;
1068
}
1069
last = unit;
1070
total = total.checked_add ( num.parse:: < u64 > ( ) .ok ( ) ? .checked_mul ( mult) ? ) ? ;
1071
num.clear ( ) ;
1072
}
1073
( num.is_empty ( ) & & total > 0 ) .then_some ( total)
1074
}
1075
1076
e87c1c13 Andy McCall 19 days ago ↶
1077
1078
1079
fn parse_priority ( ctx : & mut Ctx, c : & KdlNode) -> Option< String> {
1080
ctx.no_children ( c) ;
1081
let p = ctx.one_string ( c) ? ;
1082
match p.as_str ( ) {
1083
" low" | " normal" => Some ( p) ,
1084
" high" | " urgent" => {
1085
ctx.err (
1086
c.span ( ) ,
1087
format! ( " priority {p:?} : a pipeline may lower its jobs (`low`, `normal`), not raise them; higher levels are set on the hub, and a person may expedite a run" ) ,
1088
) ;
1089
None
1090
}
1091
_ => {
1092
ctx.err ( c.span ( ) , format! ( " priority {p:?} : expected `low` or `normal`" ) ) ;
1093
None
1094
}
1095
}
1096
}
1097
02b127d2 Andy McCall 1 month ago ↶
1098
fn parse_timeout ( ctx : & mut Ctx, c : & KdlNode) -> Option< u64 > {
1099
let t = ctx.one_string ( c) ? ;
1100
match parse_duration ( & t) {
1101
Some ( secs) if ( 60 .. = MAX_TIMEOUT_S ) .contains ( & secs) => Some ( secs) ,
1102
Some ( _ ) => {
1103
ctx.err ( c.span ( ) , format! ( " timeout {t:?} must be from 1m to 24h" ) ) ;
1104
None
1105
}
1106
None => {
1107
ctx.err ( c.span ( ) , format! ( " timeout {t:?} : expected hours, minutes and seconds, as \" 90m\" or \" 1h30m\" " ) ) ;
1108
None
1109
}
1110
}
1111
}
1112
e576e086 Andy McCall 1 month ago ↶
1113
1114
1115
fn parse_labels ( ctx : & mut Ctx, c : & KdlNode, into : & mut Vec< String> ) {
1116
let labels = ctx.strings ( c) ;
1117
if labels.is_empty ( ) {
1118
ctx.err ( c.span ( ) , " `runs-on` needs one or more labels, as runs-on \" riscv64\" " ) ;
a72abeee Andy McCall 1 month ago ↶
1119
}
e576e086 Andy McCall 1 month ago ↶
1120
for l in labels {
1121
let ok = ! l.is_empty ( )
1122
& & l.len ( ) < = 64
1123
& & l.bytes ( ) .all ( | b | b.is_ascii_lowercase ( ) | | b.is_ascii_digit ( ) | | matches! ( b, b ' -' | b ' _' | b ' .' ) ) ;
1124
if ok {
1125
into.push ( l) ;
1126
} else {
1127
ctx.err ( c.span ( ) , format! ( " runs-on {l:?} : a label is lower-case letters, digits, -, _ and ., at most 64" ) ) ;
1128
}
1129
}
1130
into.sort ( ) ;
1131
into.dedup ( ) ;
a72abeee Andy McCall 1 month ago ↶
1132
}
1133
1134
1135
fn parse_env ( ctx : & mut Ctx, c : & KdlNode, env : & mut BTreeMap< String, String> ) {
1136
if ! c.entries ( ) .is_empty ( ) {
1137
ctx.err ( c.span ( ) , " `env` takes a block of NAME \" value\" lines" ) ;
1138
}
1139
for e in block ( ctx, c) {
1140
let k = e.name ( ) .value ( ) .to_string ( ) ;
1141
let valid =
1142
! k.is_empty ( ) & & ! k.starts_with ( | c : char | c.is_ascii_digit ( ) ) & & k.bytes ( ) .all ( | b | b.is_ascii_alphanumeric ( ) | | b = = b ' _' ) ;
1143
if ! valid {
1144
ctx.err ( e.span ( ) , format! ( " environment name {k:?} must be letters, digits and _" ) ) ;
1145
continue ;
1146
}
1147
ctx.no_children ( e) ;
1148
if let Some ( v) = ctx.one_string ( e) {
1149
if env.insert ( k.clone ( ) , v) .is_some ( ) {
1150
ctx.err ( e.span ( ) , format! ( " environment {k} is set twice" ) ) ;
1151
}
1152
}
e0918b88 Andy McCall 1 month ago
1153
}
1154
}
1155
fc10d9e8 Andy McCall 1 month ago ↶
1156
1157
fn parse_mount ( ctx : & mut Ctx, node : & KdlNode) -> Option< Mount> {
1158
let ( mut source, mut target, mut size) = ( None , None , None ) ;
d60aac6c Andy McCall 23 days ago ↶
1159
let ( mut cache, mut keys) = ( None , Vec :: new( ) ) ;
fc10d9e8 Andy McCall 1 month ago ↶
1160
let mut seen = BTreeSet:: new( ) ;
1161
for c in block ( ctx, node) {
1162
let key = c.name ( ) .value ( ) ;
1163
if ! seen.insert ( key.to_string ( ) ) {
1164
ctx.err ( c.span ( ) , format! ( " `{key} ` is given twice" ) ) ;
1165
continue ;
1166
}
1167
ctx.no_children ( c) ;
1168
match key {
1169
" source" => {
1170
source = match ctx.one_string ( c) .as_deref ( ) {
1171
Some ( " ephemeral://tmpfs" ) => Some ( MountSource:: Tmpfs) ,
1172
Some ( " ephemeral://disk" ) => Some ( MountSource:: Disk) ,
1173
Some ( other) => {
1174
ctx.err (
1175
c.span ( ) ,
1176
format! ( " mount source {other:?} is not in M0 yet; M0 mounts \" ephemeral://tmpfs\" or \" ephemeral://disk\" " ) ,
1177
) ;
1178
None
1179
}
1180
None => None ,
1181
}
1182
}
1183
" target" => {
1184
target = match ctx.one_string ( c) {
1185
Some ( t) if mount_target_ok ( & t) => Some ( t) ,
1186
Some ( t) => {
1187
ctx.err (
1188
c.span ( ) ,
1189
format! ( " mount target {t:?} must be an absolute path under /src or /tmp, without . or .. or a trailing /" ) ,
1190
) ;
1191
None
1192
}
1193
None => None ,
1194
}
1195
}
1196
" size" => {
1197
size = match ctx.one_string ( c) {
1198
Some ( m) => match parse_memory ( & m) {
1199
Some ( mib) if ( MIN_MOUNT_MIB .. = MAX_MOUNT_MIB ) .contains ( & mib) => Some ( mib as u32 ) ,
1200
Some ( _ ) => {
1201
ctx.err ( c.span ( ) , format! ( " mount size {m:?} must be from 16MiB to 64GiB" ) ) ;
1202
None
1203
}
1204
None => {
1205
ctx.err ( c.span ( ) , format! ( " mount size {m:?} : expected a whole number of KiB, MiB or GiB, as \" 8GiB\" " ) ) ;
1206
None
1207
}
1208
} ,
1209
None => None ,
1210
}
1211
}
d60aac6c Andy McCall 23 days ago ↶
1212
" cache" => {
1213
cache = match ctx.one_string ( c) {
1214
Some ( n) if ! n.is_empty ( ) & & n.len ( ) < = 64 & & n.bytes ( ) .all ( | b | b.is_ascii_alphanumeric ( ) | | b " ._-" .contains ( & b) ) => {
1215
Some ( n)
1216
}
1217
Some ( n) => {
1218
ctx.err ( c.span ( ) , format! ( " cache name {n:?} : letters, digits, '.', '_' and '-', at most 64" ) ) ;
1219
None
1220
}
1221
None => None ,
1222
}
1223
}
1224
" key" => {
1225
keys = ctx.strings ( c) ;
1226
if keys.is_empty ( ) {
1227
ctx.err ( c.span ( ) , " `key` names one or more files, as \" Cargo.lock\" " ) ;
1228
}
1229
for k in & keys {
1230
if k.starts_with ( ' /' ) | | k.split ( ' /' ) .any ( | p | p.is_empty ( ) | | p = = " ." | | p = = " .." ) {
1231
ctx.err ( c.span ( ) , format! ( " key file {k:?} must be a path in the repository, without . or .." ) ) ;
1232
}
1233
}
1234
}
fc10d9e8 Andy McCall 1 month ago ↶
1235
" readonly" | " include" => ctx.err ( c.span ( ) , format! ( " mount `{key} ` is not in M0 yet" ) ) ,
1236
other => ctx.err ( c.span ( ) , format! ( " unknown mount setting `{other} `" ) ) ,
1237
}
1238
}
1239
for ( name, missing) in [ ( " source" , source.is_none ( ) ) , ( " target" , target.is_none ( ) ) , ( " size" , size.is_none ( ) ) ] {
1240
if missing & & ! seen.contains ( name) {
1241
ctx.err ( node.span ( ) , format! ( " a mount needs a {name} " ) ) ;
1242
}
d60aac6c Andy McCall 23 days ago ↶
1243
}
1244
if source = = Some ( MountSource:: Tmpfs) & & cache.is_some ( ) {
1245
ctx.err ( node.span ( ) , " only an ephemeral://disk mount can be a cache" ) ;
1246
}
1247
if ! keys.is_empty ( ) & & cache.is_none ( ) {
1248
ctx.err ( node.span ( ) , " `key` goes with `cache`" ) ;
fc10d9e8 Andy McCall 1 month ago ↶
1249
}
d60aac6c Andy McCall 23 days ago ↶
1250
Some ( Mount { source: source? , target: target? , size_mib: size? , cache, key: keys } )
fc10d9e8 Andy McCall 1 month ago ↶
1251
}
1252
a67b97e9 Andy McCall 1 month ago ↶
1253
e2c12561 Andy McCall 1 month ago ↶
1254
1255
1256
1257
fn parse_secret ( ctx : & mut Ctx, node : & KdlNode, job : & mut Job) {
1258
let Some ( name) = ctx.one_string ( node) else {
1259
ctx.err ( node.span ( ) , " `secret` takes a name and a block with mount-path" ) ;
1260
return ;
1261
} ;
1262
if ! ctx.name_ok ( node.span ( ) , " secret" , & name) {
1263
return ;
1264
}
1265
let mut path = None ;
1266
for c in block ( ctx, node) {
1267
ctx.no_children ( c) ;
1268
match c.name ( ) .value ( ) {
1269
" mount-path" => match ctx.one_string ( c) {
1270
Some ( p) if secret_path_ok ( & p) => path = Some ( p) ,
1271
Some ( p) => {
1272
ctx.err ( c.span ( ) , format! ( " secret mount-path {p:?} must be an absolute path under /run/secrets/, without . or .." ) )
1273
}
1274
None => { }
1275
} ,
1276
" vault-ref" => ctx.err ( c.span ( ) , " `vault-ref` is not in M0; the hub supplies the secret of that name" ) ,
1277
other => ctx.err ( c.span ( ) , format! ( " unknown secret setting `{other} `" ) ) ,
1278
}
1279
}
1280
let Some ( mount_path) = path else {
1281
ctx.err ( node.span ( ) , format! ( " secret {name:?} needs a mount-path" ) ) ;
1282
return ;
1283
} ;
1284
if job.secrets.iter ( ) .any ( | s | s.name = = name ) {
1285
ctx.err ( node.span ( ) , format! ( " secret {name:?} is given twice" ) ) ;
720eb819 Andy McCall 1 month ago ↶
1286
} else if job.secrets.iter ( ) .any ( | s | s.mount_path = = mount_path ) | | job.identities.iter ( ) .any ( | i | i.mount_path = = mount_path ) {
1287
ctx.err ( node.span ( ) , format! ( " two files mount at {mount_path:?} " ) ) ;
e2c12561 Andy McCall 1 month ago ↶
1288
} else if job.secrets.len ( ) = = MAX_SECRETS {
1289
ctx.err ( node.span ( ) , format! ( " a job has at most {MAX_SECRETS} secrets" ) ) ;
1290
} else {
1291
job.secrets.push ( Secret { name, mount_path } ) ;
720eb819 Andy McCall 1 month ago ↶
1292
}
1293
}
1294
1295
1296
1297
1298
fn parse_identity ( ctx : & mut Ctx, node : & KdlNode, job : & mut Job) {
1299
let Some ( audience) = ctx.one_string ( node) else {
1300
ctx.err ( node.span ( ) , " `identity` takes an audience and a block with mount-path" ) ;
1301
return ;
1302
} ;
1303
let aud_ok = ! audience.is_empty ( ) & & audience.len ( ) < = 255 & & ! audience.chars ( ) .any ( | c | c.is_whitespace ( ) | | c.is_control ( ) ) ;
1304
if ! aud_ok {
1305
ctx.err ( node.span ( ) , format! ( " identity audience {audience:?} must be 1 to 255 characters without spaces" ) ) ;
1306
return ;
1307
}
1308
let mut path = None ;
1309
for c in block ( ctx, node) {
1310
ctx.no_children ( c) ;
1311
match c.name ( ) .value ( ) {
1312
" mount-path" => match ctx.one_string ( c) {
1313
Some ( p) if secret_path_ok ( & p) => path = Some ( p) ,
1314
Some ( p) => {
1315
ctx.err ( c.span ( ) , format! ( " identity mount-path {p:?} must be an absolute path under /run/secrets/, without . or .." ) )
1316
}
1317
None => { }
1318
} ,
1319
other => ctx.err ( c.span ( ) , format! ( " unknown identity setting `{other} `" ) ) ,
1320
}
1321
}
1322
let Some ( mount_path) = path else {
1323
ctx.err ( node.span ( ) , format! ( " identity {audience:?} needs a mount-path" ) ) ;
1324
return ;
1325
} ;
1326
if job.identities.iter ( ) .any ( | i | i.audience = = audience ) {
1327
ctx.err ( node.span ( ) , format! ( " identity {audience:?} is given twice" ) ) ;
1328
} else if job.identities.iter ( ) .any ( | i | i.mount_path = = mount_path ) | | job.secrets.iter ( ) .any ( | s | s.mount_path = = mount_path ) {
1329
ctx.err ( node.span ( ) , format! ( " two files mount at {mount_path:?} " ) ) ;
1330
} else if job.identities.len ( ) = = MAX_IDENTITIES {
1331
ctx.err ( node.span ( ) , format! ( " a job has at most {MAX_IDENTITIES} identities" ) ) ;
1332
} else {
1333
job.identities.push ( Identity { audience, mount_path } ) ;
e2c12561 Andy McCall 1 month ago ↶
1334
}
1335
}
1336
a67b97e9 Andy McCall 1 month ago ↶
1337
fn parse_output ( ctx : & mut Ctx, node : & KdlNode, job : & mut Job) {
1338
if ! node.entries ( ) .is_empty ( ) {
6d53fb7d Andy McCall 25 days ago ↶
1339
ctx.err ( node.span ( ) , " `output` takes a block of artifacts and reports" ) ;
a67b97e9 Andy McCall 1 month ago ↶
1340
}
1341
for a in block ( ctx, node) {
6d53fb7d Andy McCall 25 days ago ↶
1342
let kind = match a.name ( ) .value ( ) {
1343
" artifact" => OutputKind:: Artifact,
1344
" report" => OutputKind:: Report,
1345
other => {
1346
ctx.err (
1347
a.span ( ) ,
1348
format! ( " unknown output `{other} `; outputs are `artifact \" name\" {{ path \" /src/file\" }} ` and `report \" name\" {{ path \" /src/dir\" }} `" ) ,
1349
) ;
1350
continue ;
1351
}
1352
} ;
1353
let word = kind.word ( ) ;
a67b97e9 Andy McCall 1 month ago ↶
1354
let Some ( name) = ctx.one_string ( a) else { continue } ;
6d53fb7d Andy McCall 25 days ago ↶
1355
if ! ctx.name_ok ( a.span ( ) , word, & name) {
a67b97e9 Andy McCall 1 month ago ↶
1356
continue ;
1357
}
1358
let mut path = None ;
9a6fac2b Andy McCall 1 month ago ↶
1359
let mut always = false ;
a67b97e9 Andy McCall 1 month ago ↶
1360
for c in block ( ctx, a) {
1361
ctx.no_children ( c) ;
1362
match c.name ( ) .value ( ) {
1363
" path" => match ctx.one_string ( c) {
1364
Some ( p) if output_path_ok ( & p) => path = Some ( p) ,
6d53fb7d Andy McCall 25 days ago ↶
1365
Some ( p) => ctx
1366
.err ( c.span ( ) , format! ( " {word} path {p:?} must be an absolute path under /src, without . or .., quotes or spaces" ) ) ,
a67b97e9 Andy McCall 1 month ago ↶
1367
None => { }
1368
} ,
6d53fb7d Andy McCall 25 days ago ↶
1369
" format" if kind = = OutputKind:: Artifact => {
a67b97e9 Andy McCall 1 month ago ↶
1370
if ctx.one_string ( c) .as_deref ( ) ! = Some ( " blob" ) {
1371
ctx.err ( c.span ( ) , " artifact format is \" blob\" " ) ;
1372
}
1373
}
9a6fac2b Andy McCall 1 month ago ↶
1374
" when" => match ctx.one_string ( c) .as_deref ( ) {
1375
Some ( " passed" ) => always = false ,
1376
Some ( " always" ) => always = true ,
6d53fb7d Andy McCall 25 days ago ↶
1377
Some ( _ ) => ctx.err ( c.span ( ) , format! ( " {word} `when` is \" passed\" (the default) or \" always\" " ) ) ,
9a6fac2b Andy McCall 1 month ago ↶
1378
None => { }
1379
} ,
6d53fb7d Andy McCall 25 days ago ↶
1380
other => ctx.err ( c.span ( ) , format! ( " unknown {word} setting `{other} `" ) ) ,
a67b97e9 Andy McCall 1 month ago ↶
1381
}
1382
}
1383
let Some ( path) = path else {
6d53fb7d Andy McCall 25 days ago ↶
1384
ctx.err ( a.span ( ) , format! ( " {word} {name:?} needs a path" ) ) ;
a67b97e9 Andy McCall 1 month ago ↶
1385
continue ;
1386
} ;
1387
if job.outputs.iter ( ) .any ( | o | o.name = = name ) {
6d53fb7d Andy McCall 25 days ago ↶
1388
ctx.err ( a.span ( ) , format! ( " output {name:?} is given twice (artifacts and reports share names)" ) ) ;
a67b97e9 Andy McCall 1 month ago ↶
1389
} else if job.outputs.len ( ) = = MAX_OUTPUTS {
6d53fb7d Andy McCall 25 days ago ↶
1390
ctx.err ( a.span ( ) , format! ( " a job has at most {MAX_OUTPUTS} artifacts and reports" ) ) ;
a67b97e9 Andy McCall 1 month ago ↶
1391
} else {
6d53fb7d Andy McCall 25 days ago ↶
1392
job.outputs.push ( Output { name, path, always, kind } ) ;
e2089721 Andy McCall 20 days ago ↶
1393
}
1394
}
1395
}
1396
1397
1398
2be1fe4a Andy McCall 20 days ago ↶
1399
1400
1401
fn parse_approval ( ctx : & mut Ctx, node : & KdlNode) -> Option< ( Approval, bool ) > {
e2089721 Andy McCall 20 days ago ↶
1402
if ! node.entries ( ) .is_empty ( ) {
1403
ctx.err ( node.span ( ) , " `approval` takes a block: approval { by \" maintain\" ; others-only #true; expires \" 24h\" }" ) ;
1404
}
2be1fe4a Andy McCall 20 days ago ↶
1405
let mut a = Approval { by: " maintain" .into ( ) , others_only: false , expires_s: 24 * 3600 , sealed: false } ;
1406
let mut sealed_said = false ;
1407
let Some ( children) = node.children ( ) else { return Some ( ( a, false ) ) } ;
e2089721 Andy McCall 20 days ago ↶
1408
for c in children.nodes ( ) {
1409
ctx.no_children ( c) ;
1410
match c.name ( ) .value ( ) {
1411
" by" => match ctx.one_string ( c) .as_deref ( ) {
1412
Some ( r @ ( " write" | " maintain" | " admin" ) ) => a.by = r.to_string ( ) ,
1413
Some ( r) => ctx.err ( c.span ( ) , format! ( " approval by {r:?} : expected \" write\" , \" maintain\" or \" admin\" " ) ) ,
1414
None => { }
1415
} ,
1416
" others-only" => match c.entries ( ) {
1417
[ e] if e.name ( ) .is_none ( ) => match e.value ( ) {
1418
KdlValue:: Bool( b) => a.others_only = * b,
1419
_ => ctx.err ( c.span ( ) , " `others-only` takes #true or #false" ) ,
1420
} ,
1421
_ => ctx.err ( c.span ( ) , " `others-only` takes #true or #false" ) ,
2be1fe4a Andy McCall 20 days ago ↶
1422
} ,
1423
" sealed" => match c.entries ( ) {
1424
[ e] if e.name ( ) .is_none ( ) => match e.value ( ) {
1425
KdlValue:: Bool( b) => {
1426
a.sealed = * b;
1427
sealed_said = true ;
1428
}
1429
_ => ctx.err ( c.span ( ) , " `sealed` takes #true or #false" ) ,
1430
} ,
1431
_ => ctx.err ( c.span ( ) , " `sealed` takes #true or #false" ) ,
e2089721 Andy McCall 20 days ago ↶
1432
} ,
1433
" expires" => {
1434
let Some ( t) = ctx.one_string ( c) else { continue } ;
1435
match parse_duration ( & t) {
1436
Some ( secs) if ( 60 .. = MAX_APPROVAL_S ) .contains ( & secs) => a.expires_s = secs,
1437
Some ( _ ) => ctx.err ( c.span ( ) , format! ( " expires {t:?} must be from 1m to 720h" ) ) ,
1438
None => ctx.err ( c.span ( ) , format! ( " expires {t:?} : expected hours, minutes and seconds, as \" 24h\" " ) ) ,
1439
}
2be1fe4a Andy McCall 20 days ago ↶
1440
}
1441
other => {
1442
ctx.err ( c.span ( ) , format! ( " unknown approval setting `{other} `; approval takes `by`, `others-only`, `expires` and `sealed`" ) )
e2089721 Andy McCall 20 days ago ↶
1443
}
46fe2c81 Andy McCall 20 days ago ↶
1444
}
1445
}
2be1fe4a Andy McCall 20 days ago ↶
1446
Some ( ( a, sealed_said) )
46fe2c81 Andy McCall 20 days ago ↶
1447
}
1448
1449
1450
fn parse_input ( ctx : & mut Ctx, node : & KdlNode, job : & mut Job) {
1451
if ! node.entries ( ) .is_empty ( ) {
1452
ctx.err ( node.span ( ) , " `input` takes a block: input { artifact \" tfplan\" from \" plan\" ; path \" /src/tfplan\" }" ) ;
1453
}
1454
let ( mut artifact, mut from, mut path) = ( None , None , None ) ;
1455
for c in block ( ctx, node) {
1456
ctx.no_children ( c) ;
1457
match c.name ( ) .value ( ) {
1458
" artifact" => match ctx.strings ( c) .as_slice ( ) {
1459
[ name, word, producer] if word = = " from" => {
1460
if ctx.name_ok ( c.span ( ) , " artifact" , name) & & ctx.name_ok ( c.span ( ) , " job" , producer) {
1461
artifact = Some ( name.clone ( ) ) ;
1462
from = Some ( producer.clone ( ) ) ;
1463
}
1464
}
1465
_ => ctx.err ( c.span ( ) , " `artifact` names the artifact and its job: artifact \" tfplan\" from \" plan\" " ) ,
1466
} ,
1467
" path" => match ctx.one_string ( c) {
1468
Some ( p) if output_path_ok ( & p) => path = Some ( p) ,
1469
Some ( p) => {
1470
ctx.err ( c.span ( ) , format! ( " input path {p:?} must be an absolute path under /src, without . or .., quotes or spaces" ) )
1471
}
1472
None => { }
1473
} ,
1474
other => ctx.err ( c.span ( ) , format! ( " unknown input setting `{other} `; an input has `artifact` and `path`" ) ) ,
1475
}
1476
}
1477
match ( artifact, from, path) {
1478
( Some ( artifact) , Some ( from) , Some ( path) ) => {
1479
if job.inputs.iter ( ) .any ( | i | i.path = = path ) {
1480
ctx.err ( node.span ( ) , format! ( " two inputs are put at {path:?} " ) ) ;
1481
} else if job.inputs.len ( ) = = MAX_INPUTS {
1482
ctx.err ( node.span ( ) , format! ( " a job takes at most {MAX_INPUTS} inputs" ) ) ;
1483
} else {
1484
job.inputs.push ( Input { artifact, from, path } ) ;
1485
}
1486
}
1487
( a, _ , p) => {
1488
if a.is_some ( ) & & p.is_none ( ) {
1489
ctx.err ( node.span ( ) , " an input needs a `path`" ) ;
1490
} else if a.is_none ( ) & & p.is_some ( ) {
1491
ctx.err ( node.span ( ) , " an input needs `artifact \" NAME\" from \" JOB\" `" ) ;
1492
}
a67b97e9 Andy McCall 1 month ago ↶
1493
}
1494
}
1495
}
1496
e0918b88 Andy McCall 1 month ago
1497
fn parse_job ( ctx : & mut Ctx, node : & KdlNode) -> Option< Job> {
1498
let name = ctx.one_string ( node) ? ;
1499
let named = ctx.name_ok ( node.span ( ) , " job" , & name) ;
2be1fe4a Andy McCall 20 days ago ↶
1500
let mut approval: Option< ( Approval, bool ) > = None ;
fc10d9e8 Andy McCall 1 month ago ↶
1501
let mut job = Job {
1502
name: name.clone ( ) ,
1503
sandbox: String :: new( ) ,
1504
scope: vec! [ ] ,
1505
depends_on: vec! [ ] ,
1506
env: BTreeMap:: new( ) ,
1507
exec: vec! [ ] ,
1508
mounts: vec! [ ] ,
e2c12561 Andy McCall 1 month ago ↶
1509
secrets: vec! [ ] ,
720eb819 Andy McCall 1 month ago ↶
1510
identities: vec! [ ] ,
a67b97e9 Andy McCall 1 month ago ↶
1511
outputs: vec! [ ] ,
46fe2c81 Andy McCall 20 days ago ↶
1512
inputs: vec! [ ] ,
e2089721 Andy McCall 20 days ago ↶
1513
approval: None ,
f4e51e63 Andy McCall 20 days ago ↶
1514
environment: None ,
e576e086 Andy McCall 1 month ago ↶
1515
runs_on: vec! [ ] ,
02b127d2 Andy McCall 1 month ago ↶
1516
timeout_s: None ,
bf7bb1b8 Andy McCall 1 month ago ↶
1517
on: None ,
e87c1c13 Andy McCall 19 days ago ↶
1518
priority: None ,
f201e1bc Andy McCall 11 days ago ↶
1519
checkpoints: Vec :: new( ) ,
fc10d9e8 Andy McCall 1 month ago ↶
1520
} ;
e0918b88 Andy McCall 1 month ago
1521
let mut seen = BTreeSet:: new( ) ;
a67b97e9 Andy McCall 1 month ago ↶
1522
let mut outputs_at = None ;
e0918b88 Andy McCall 1 month ago
1523
for c in block ( ctx, node) {
1524
let key = c.name ( ) .value ( ) ;
720eb819 Andy McCall 1 month ago ↶
1525
if key ! = " scope"
1526
& & key ! = " depends-on"
1527
& & key ! = " mount"
1528
& & key ! = " secret"
1529
& & key ! = " identity"
1530
& & key ! = " runs-on"
f201e1bc Andy McCall 11 days ago ↶
1531
& & key ! = " checkpoint"
720eb819 Andy McCall 1 month ago ↶
1532
& & ! seen.insert ( key.to_string ( ) )
1533
{
e0918b88 Andy McCall 1 month ago
1534
ctx.err ( c.span ( ) , format! ( " `{key} ` is given twice" ) ) ;
1535
}
1536
match key {
1537
" sandbox" => {
1538
ctx.no_children ( c) ;
1539
job.sandbox = ctx.one_string ( c) .unwrap_or_default ( ) ;
f201e1bc Andy McCall 11 days ago ↶
1540
}
1541
" checkpoint" => {
1542
ctx.no_children ( c) ;
1543
for p in ctx.strings ( c) {
1544
if output_path_ok ( & p) & & p.trim_end_matches ( ' /' ) ! = " /src" {
1545
job.checkpoints.push ( p) ;
1546
} else {
1547
ctx.err ( c.span ( ) , format! ( " checkpoint {p:?} must be a directory under /src, without . or .., quotes or spaces" ) ) ;
1548
}
1549
}
e0918b88 Andy McCall 1 month ago
1550
}
1551
" scope" => {
1552
ctx.no_children ( c) ;
1553
for g in ctx.strings ( c) {
1554
match globset:: Glob:: new( & g) {
1555
Ok ( _ ) => job.scope.push ( g) ,
1556
Err ( e) => ctx.err ( c.span ( ) , format! ( " scope {g:?} : {e} " ) ) ,
1557
}
1558
}
1559
}
1560
" depends-on" => {
1561
ctx.no_children ( c) ;
1562
job.depends_on.extend ( ctx.strings ( c) ) ;
e576e086 Andy McCall 1 month ago ↶
1563
}
1564
" runs-on" => {
1565
ctx.no_children ( c) ;
1566
parse_labels ( ctx, c, & mut job.runs_on) ;
02b127d2 Andy McCall 1 month ago ↶
1567
}
1568
" timeout" => {
1569
ctx.no_children ( c) ;
1570
job.timeout_s = parse_timeout ( ctx, c) ;
e0918b88 Andy McCall 1 month ago
1571
}
e87c1c13 Andy McCall 19 days ago ↶
1572
" priority" => job.priority = parse_priority ( ctx, c) ,
bf7bb1b8 Andy McCall 1 month ago ↶
1573
" on" => job.on = Some ( parse_on ( ctx, c) ) ,
a72abeee Andy McCall 1 month ago ↶
1574
" env" => parse_env ( ctx, c, & mut job.env) ,
e0918b88 Andy McCall 1 month ago
1575
" exec" => {
1576
ctx.no_children ( c) ;
1577
job.exec = ctx.strings ( c) ;
1578
if job.exec.is_empty ( ) {
1579
ctx.err ( c.span ( ) , " `exec` needs a program and its arguments" ) ;
1580
} else if job.exec.iter ( ) .any ( | a | a.contains ( ' \0 ' ) ) {
1581
ctx.err ( c.span ( ) , " `exec` arguments cannot contain NUL" ) ;
1582
}
1583
}
fc10d9e8 Andy McCall 1 month ago ↶
1584
" mount" => {
1585
if ! c.entries ( ) .is_empty ( ) {
1586
ctx.err ( c.span ( ) , " `mount` takes a block of source, target and size" ) ;
1587
}
1588
if let Some ( m) = parse_mount ( ctx, c) {
1589
if job.mounts.len ( ) = = MAX_MOUNTS {
1590
ctx.err ( c.span ( ) , format! ( " a job has at most {MAX_MOUNTS} mounts" ) ) ;
1591
} else if let Some ( other) = job.mounts.iter ( ) .find ( | o | {
1592
o.target = = m.target
1593
| | m.target.starts_with ( & format! ( " {} /" , o.target) )
1594
| | o.target.starts_with ( & format! ( " {} /" , m.target) )
1595
} ) {
1596
ctx.err ( c.span ( ) , format! ( " mount {:?} overlaps mount {:?} " , m.target, other.target) ) ;
1597
} else {
1598
job.mounts.push ( m) ;
1599
}
1600
}
1601
}
a67b97e9 Andy McCall 1 month ago ↶
1602
" output" => {
1603
outputs_at = Some ( c.span ( ) ) ;
1604
parse_output ( ctx, c, & mut job) ;
1605
}
46fe2c81 Andy McCall 20 days ago ↶
1606
" input" => parse_input ( ctx, c, & mut job) ,
2be1fe4a Andy McCall 20 days ago ↶
1607
" approval" => approval = parse_approval ( ctx, c) ,
f4e51e63 Andy McCall 20 days ago ↶
1608
" environment" => {
1609
ctx.no_children ( c) ;
1610
if let Some ( e) = ctx.one_string ( c) {
1611
let ok = ! e.is_empty ( ) & & e.len ( ) < = 32 & & e.bytes ( ) .all ( | b | b.is_ascii_lowercase ( ) | | b.is_ascii_digit ( ) | | b = = b ' -' ) ;
1612
if ok {
1613
job.environment = Some ( e) ;
1614
} else {
1615
ctx.err ( c.span ( ) , format! ( " environment {e:?} : 1 to 32 of a-z 0-9 -, as \" prod\" " ) ) ;
1616
}
1617
}
1618
}
e2c12561 Andy McCall 1 month ago ↶
1619
" secret" => parse_secret ( ctx, c, & mut job) ,
720eb819 Andy McCall 1 month ago ↶
1620
" identity" => parse_identity ( ctx, c, & mut job) ,
e0918b88 Andy McCall 1 month ago
1621
n if LATER_JOB .contains ( & n) => ctx.err ( c.span ( ) , format! ( " `{n} ` is not in M0 yet" ) ) ,
1622
other => ctx.err ( c.span ( ) , format! ( " unknown job setting `{other} `" ) ) ,
1623
}
1624
}
2be1fe4a Andy McCall 20 days ago ↶
1625
1626
job.approval = approval.map ( | ( mut a , said ) | {
1627
if ! said {
1628
a.sealed = job.environment.is_some ( ) ;
1629
}
1630
a
1631
} ) ;
fc10d9e8 Andy McCall 1 month ago ↶
1632
job.mounts.sort_by ( | a , b | a.target.cmp ( & b.target) ) ;
a67b97e9 Andy McCall 1 month ago ↶
1633
job.outputs.sort_by ( | a , b | a.name.cmp ( & b.name) ) ;
46fe2c81 Andy McCall 20 days ago ↶
1634
job.inputs.sort_by ( | a , b | a.path.cmp ( & b.path) ) ;
e2c12561 Andy McCall 1 month ago ↶
1635
job.secrets.sort_by ( | a , b | a.name.cmp ( & b.name) ) ;
720eb819 Andy McCall 1 month ago ↶
1636
job.identities.sort_by ( | a , b | a.audience.cmp ( & b.audience) ) ;
a67b97e9 Andy McCall 1 month ago ↶
1637
1638
for o in & job.outputs {
1639
if let Some ( m) = job.mounts.iter ( ) .find ( | m | m.source = = MountSource:: Tmpfs & & o.path.starts_with ( & format! ( " {} /" , m.target) ) ) {
1640
ctx.err (
1641
outputs_at.unwrap_or ( node.span ( ) ) ,
1642
format! (
1643
" artifact {:?} is on the tmpfs mount {:?} , which is gone when the job ends; use ephemeral://disk" ,
1644
o.name, m.target
1645
) ,
1646
) ;
1647
}
1648
}
e0918b88 Andy McCall 1 month ago
1649
if job.sandbox.is_empty ( ) & & ! seen.contains ( " sandbox" ) {
1650
ctx.err ( node.span ( ) , format! ( " job {name:?} needs a sandbox" ) ) ;
1651
}
1652
if job.exec.is_empty ( ) & & ! seen.contains ( " exec" ) {
1653
ctx.err ( node.span ( ) , format! ( " job {name:?} needs an exec" ) ) ;
1654
}
1655
job.depends_on.sort ( ) ;
1656
job.depends_on.dedup ( ) ;
f201e1bc Andy McCall 11 days ago ↶
1657
job.checkpoints.sort ( ) ;
1658
job.checkpoints.dedup ( ) ;
e0918b88 Andy McCall 1 month ago
1659
1660
1661
named.then_some ( job)
1662
}