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