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