1use crate::{Identifier, ProtectedString, Provenance, ResourceLimit, Sourced};
7
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10#[non_exhaustive]
11pub enum BuildSyntax {
12 Scalar,
14 Structured,
16 Mapping,
18 Sequence,
20 Repeated,
22}
23
24#[derive(Clone, Debug, Eq, PartialEq)]
30pub struct BuildSettingValues<T> {
31 syntax: BuildSyntax,
32 values: Vec<Sourced<T>>,
33}
34
35impl<T> BuildSettingValues<T> {
36 #[must_use]
38 pub const fn new(syntax: BuildSyntax, values: Vec<Sourced<T>>) -> Self {
39 Self { syntax, values }
40 }
41
42 #[must_use]
44 pub const fn syntax(&self) -> BuildSyntax {
45 self.syntax
46 }
47
48 #[must_use]
50 pub fn values(&self) -> &[Sourced<T>] {
51 &self.values
52 }
53}
54
55#[derive(Clone, Debug, Eq, PartialEq)]
61pub struct ImageArtifactAssignment {
62 name: ProtectedString,
63 value: Option<ProtectedString>,
64}
65
66impl ImageArtifactAssignment {
67 #[must_use]
69 pub const fn new(name: ProtectedString, value: Option<ProtectedString>) -> Self {
70 Self { name, value }
71 }
72
73 #[must_use]
75 pub const fn name(&self) -> &ProtectedString {
76 &self.name
77 }
78
79 #[must_use]
81 pub const fn value(&self) -> Option<&ProtectedString> {
82 self.value.as_ref()
83 }
84}
85
86#[derive(Clone, Debug, Eq, PartialEq)]
88pub struct BuildContext {
89 name: ProtectedString,
90 value: ProtectedString,
91}
92
93impl BuildContext {
94 #[must_use]
96 pub const fn new(name: ProtectedString, value: ProtectedString) -> Self {
97 Self { name, value }
98 }
99
100 #[must_use]
102 pub const fn name(&self) -> &ProtectedString {
103 &self.name
104 }
105
106 #[must_use]
108 pub const fn value(&self) -> &ProtectedString {
109 &self.value
110 }
111}
112
113#[derive(Clone, Debug, Eq, PartialEq)]
115pub struct SourceBuildSecret {
116 source: ProtectedString,
117 target: Option<ProtectedString>,
118 uid: Option<ProtectedString>,
119 gid: Option<ProtectedString>,
120 mode: Option<ProtectedString>,
121}
122
123impl SourceBuildSecret {
124 #[must_use]
126 pub const fn new(source: ProtectedString) -> Self {
127 Self {
128 source,
129 target: None,
130 uid: None,
131 gid: None,
132 mode: None,
133 }
134 }
135
136 #[must_use]
138 pub const fn source(&self) -> &ProtectedString {
139 &self.source
140 }
141
142 pub fn set_target(&mut self, target: ProtectedString) {
144 self.target = Some(target);
145 }
146
147 #[must_use]
149 pub const fn target(&self) -> Option<&ProtectedString> {
150 self.target.as_ref()
151 }
152
153 pub fn set_uid(&mut self, uid: ProtectedString) {
155 self.uid = Some(uid);
156 }
157
158 #[must_use]
160 pub const fn uid(&self) -> Option<&ProtectedString> {
161 self.uid.as_ref()
162 }
163
164 pub fn set_gid(&mut self, gid: ProtectedString) {
166 self.gid = Some(gid);
167 }
168
169 #[must_use]
171 pub const fn gid(&self) -> Option<&ProtectedString> {
172 self.gid.as_ref()
173 }
174
175 pub fn set_mode(&mut self, mode: ProtectedString) {
177 self.mode = Some(mode);
178 }
179
180 #[must_use]
182 pub const fn mode(&self) -> Option<&ProtectedString> {
183 self.mode.as_ref()
184 }
185}
186
187#[derive(Clone, Debug, Eq, PartialEq)]
189#[non_exhaustive]
190pub enum BuildAttestation {
191 Boolean(bool),
193 Value(ProtectedString),
195}
196
197#[derive(Clone, Debug, Eq, PartialEq)]
202#[non_exhaustive]
203pub enum SourceBuildSetting {
204 AdditionalContexts(BuildSettingValues<BuildContext>),
206 Arguments(BuildSettingValues<ImageArtifactAssignment>),
208 CacheFrom(BuildSettingValues<ProtectedString>),
210 CacheTo(BuildSettingValues<ProtectedString>),
212 Context(ProtectedString),
214 RecipeFile(ProtectedString),
216 InlineRecipe(ProtectedString),
218 Entitlements(BuildSettingValues<ProtectedString>),
220 ExtraHosts(BuildSettingValues<ImageArtifactAssignment>),
222 Isolation(ProtectedString),
224 Labels(BuildSettingValues<ImageArtifactAssignment>),
226 Network(ProtectedString),
228 NoCache(bool),
230 NoCacheFilters(BuildSettingValues<ProtectedString>),
232 Platforms(BuildSettingValues<ProtectedString>),
234 Privileged(bool),
236 Provenance(BuildAttestation),
238 Pull(bool),
240 Sbom(BuildAttestation),
242 Secrets(BuildSettingValues<SourceBuildSecret>),
244 ShmSize(ProtectedString),
246 Ssh(BuildSettingValues<ProtectedString>),
248 Tags(BuildSettingValues<ProtectedString>),
250 Target(ProtectedString),
252 Ulimits(BuildSettingValues<ResourceLimit>),
254}
255
256#[derive(Clone, Debug, Eq, PartialEq)]
261#[non_exhaustive]
262pub enum BuildSourceDeclaration {
263 Scalar(ProtectedString),
265 Structured(Vec<Sourced<SourceBuildSetting>>),
267}
268
269impl BuildSourceDeclaration {
270 #[must_use]
272 pub const fn syntax(&self) -> BuildSyntax {
273 match self {
274 Self::Scalar(_) => BuildSyntax::Scalar,
275 Self::Structured(_) => BuildSyntax::Structured,
276 }
277 }
278
279 #[must_use]
281 pub fn structured_settings(&self) -> Option<&[Sourced<SourceBuildSetting>]> {
282 match self {
283 Self::Scalar(_) => None,
284 Self::Structured(settings) => Some(settings),
285 }
286 }
287}
288
289#[derive(Clone, Debug, Eq, PartialEq)]
294#[non_exhaustive]
295pub enum ImageAcquisitionSetting {
296 Image(ProtectedString),
298 ImageTags(BuildSettingValues<ProtectedString>),
300 ServiceName(ProtectedString),
302 AllTags(bool),
304 Architecture(ProtectedString),
306 AuthFile(ProtectedString),
308 CertificateDirectory(ProtectedString),
310 ContainersConfigModules(BuildSettingValues<ProtectedString>),
312 Credentials(ProtectedString),
314 DecryptionKey(ProtectedString),
316 GlobalArguments(BuildSettingValues<ProtectedString>),
318 OperatingSystem(ProtectedString),
320}
321
322#[derive(Clone, Debug, Eq, PartialEq)]
324pub struct ImageAcquisition {
325 name: Identifier,
326 settings: Option<Vec<Sourced<ImageAcquisitionSetting>>>,
327 settings_origins: Vec<Provenance>,
328}
329
330impl ImageAcquisition {
331 #[must_use]
333 pub const fn new(name: Identifier) -> Self {
334 Self {
335 name,
336 settings: None,
337 settings_origins: Vec::new(),
338 }
339 }
340
341 #[must_use]
343 pub const fn name(&self) -> &Identifier {
344 &self.name
345 }
346
347 pub fn set_settings(&mut self, settings: Vec<Sourced<ImageAcquisitionSetting>>) {
349 self.settings = Some(settings);
350 self.settings_origins.clear();
351 }
352
353 pub fn set_settings_with_origins(
355 &mut self,
356 settings: Vec<Sourced<ImageAcquisitionSetting>>,
357 origins: Vec<Provenance>,
358 ) {
359 self.settings = Some(settings);
360 self.settings_origins = origins;
361 }
362
363 #[must_use]
365 pub fn settings(&self) -> Option<&[Sourced<ImageAcquisitionSetting>]> {
366 self.settings.as_deref()
367 }
368
369 #[must_use]
371 pub fn settings_origins(&self) -> &[Provenance] {
372 &self.settings_origins
373 }
374}
375
376#[derive(Clone, Debug, Eq, PartialEq)]
381#[non_exhaustive]
382pub enum ImageBuildSetting {
383 ImageTags(BuildSettingValues<ProtectedString>),
385 Network(ProtectedString),
387 Labels(BuildSettingValues<ImageArtifactAssignment>),
389 RecipeFile(ProtectedString),
391 SetWorkingDirectory(ProtectedString),
393 Target(ProtectedString),
395 BuildArguments(BuildSettingValues<ImageArtifactAssignment>),
397 Secrets(BuildSettingValues<ProtectedString>),
399 Architecture(ProtectedString),
401 Variant(ProtectedString),
403 PullPolicy(ProtectedString),
405 Retry(ProtectedString),
407 RetryDelay(ProtectedString),
409 TlsVerify(bool),
411 ForceRemove(bool),
413 AuthFile(ProtectedString),
415 IgnoreFile(ProtectedString),
417 ServiceName(ProtectedString),
419 GroupAdd(BuildSettingValues<ProtectedString>),
421 DnsServers(BuildSettingValues<ProtectedString>),
423 DnsOptions(BuildSettingValues<ProtectedString>),
425 DnsSearchDomains(BuildSettingValues<ProtectedString>),
427 Annotations(BuildSettingValues<ImageArtifactAssignment>),
429 Environment(BuildSettingValues<ImageArtifactAssignment>),
431 ContainersConfigModules(BuildSettingValues<ProtectedString>),
433 GlobalArguments(BuildSettingValues<ProtectedString>),
435 Volumes(BuildSettingValues<ProtectedString>),
437 RuntimeArguments(BuildSettingValues<ProtectedString>),
439}
440
441#[derive(Clone, Debug, Eq, PartialEq)]
443pub struct ImageBuild {
444 name: Identifier,
445 source_declaration: Option<Sourced<BuildSourceDeclaration>>,
446 settings: Option<Vec<Sourced<ImageBuildSetting>>>,
447 settings_origins: Vec<Provenance>,
448}
449
450impl ImageBuild {
451 #[must_use]
453 pub const fn new(name: Identifier) -> Self {
454 Self {
455 name,
456 source_declaration: None,
457 settings: None,
458 settings_origins: Vec::new(),
459 }
460 }
461
462 #[must_use]
464 pub const fn name(&self) -> &Identifier {
465 &self.name
466 }
467
468 pub fn set_source_declaration(&mut self, declaration: Sourced<BuildSourceDeclaration>) {
470 self.source_declaration = Some(declaration);
471 }
472
473 #[must_use]
475 pub const fn source_declaration(&self) -> Option<&Sourced<BuildSourceDeclaration>> {
476 self.source_declaration.as_ref()
477 }
478
479 pub fn set_settings(&mut self, settings: Vec<Sourced<ImageBuildSetting>>) {
481 self.settings = Some(settings);
482 self.settings_origins.clear();
483 }
484
485 pub fn set_settings_with_origins(&mut self, settings: Vec<Sourced<ImageBuildSetting>>, origins: Vec<Provenance>) {
487 self.settings = Some(settings);
488 self.settings_origins = origins;
489 }
490
491 #[must_use]
493 pub fn settings(&self) -> Option<&[Sourced<ImageBuildSetting>]> {
494 self.settings.as_deref()
495 }
496
497 #[must_use]
499 pub fn settings_origins(&self) -> &[Provenance] {
500 &self.settings_origins
501 }
502}
503
504#[cfg(test)]
505mod tests {
506 use super::{
507 BuildSettingValues, BuildSourceDeclaration, BuildSyntax, ImageAcquisition, ImageAcquisitionSetting,
508 ImageArtifactAssignment, ImageBuild, ImageBuildSetting, SourceBuildSetting,
509 };
510 use crate::{Identifier, ProtectedString, Provenance, SourceId, Sourced};
511
512 fn origin() -> Result<Provenance, String> {
513 SourceId::new("build.yaml")
514 .map(Provenance::source)
515 .map_err(|error| error.to_string())
516 }
517
518 #[test]
519 fn native_text_settings_preserve_working_directory_and_pull_policy() -> Result<(), String> {
520 let origin = origin()?;
521 let mut build = ImageBuild::new(Identifier::new("web-build").map_err(|error| error.to_string())?);
522 build.set_settings(vec![
523 Sourced::from_source(
524 ImageBuildSetting::SetWorkingDirectory(ProtectedString::plain("unit")),
525 origin.clone(),
526 ),
527 Sourced::from_source(ImageBuildSetting::PullPolicy(ProtectedString::plain("newer")), origin),
528 ]);
529 let settings = build.settings().ok_or("explicit settings")?;
530 assert!(matches!(
531 settings[0].value(),
532 ImageBuildSetting::SetWorkingDirectory(value) if value.expose() == "unit"
533 ));
534 assert!(matches!(
535 settings[1].value(),
536 ImageBuildSetting::PullPolicy(value) if value.expose() == "newer"
537 ));
538 Ok(())
539 }
540
541 #[test]
542 fn per_key_empty_reset_is_distinct_from_omission_and_retains_order_and_provenance() -> Result<(), String> {
543 let origin = origin()?;
544 let mut build = ImageBuild::new(Identifier::new("web-build").map_err(|error| error.to_string())?);
545 assert_eq!(build.source_declaration(), None);
546 build.set_source_declaration(Sourced::from_source(
547 BuildSourceDeclaration::Structured(vec![
548 Sourced::from_source(
549 SourceBuildSetting::Tags(BuildSettingValues::new(BuildSyntax::Sequence, Vec::new())),
550 origin.clone(),
551 ),
552 Sourced::from_source(
553 SourceBuildSetting::Arguments(BuildSettingValues::new(BuildSyntax::Mapping, Vec::new())),
554 origin.clone(),
555 ),
556 ]),
557 origin.clone(),
558 ));
559 let declaration = build.source_declaration().ok_or("structured declaration")?;
560 let settings = declaration.value().structured_settings().ok_or("structured settings")?;
561 assert_eq!(settings.len(), 2);
562 assert!(matches!(
563 settings[0].value(),
564 SourceBuildSetting::Tags(values) if values.values().is_empty() && values.syntax() == BuildSyntax::Sequence
565 ));
566 assert!(matches!(
567 settings[1].value(),
568 SourceBuildSetting::Arguments(values) if values.values().is_empty() && values.syntax() == BuildSyntax::Mapping
569 ));
570 assert_eq!(settings[0].origins(), std::slice::from_ref(&origin));
571 Ok(())
572 }
573
574 #[test]
575 fn scalar_and_structured_source_declarations_remain_distinct() -> Result<(), String> {
576 let origin = origin()?;
577 let mut build = ImageBuild::new(Identifier::new("web-build").map_err(|error| error.to_string())?);
578 build.set_source_declaration(Sourced::from_source(
579 BuildSourceDeclaration::Scalar(ProtectedString::plain("./web")),
580 origin,
581 ));
582 assert_eq!(
583 build
584 .source_declaration()
585 .map(|declaration| declaration.value().syntax()),
586 Some(BuildSyntax::Scalar)
587 );
588 assert_eq!(
589 build
590 .source_declaration()
591 .and_then(|declaration| declaration.value().structured_settings()),
592 None
593 );
594 Ok(())
595 }
596
597 #[test]
598 fn repeated_acquisition_values_and_sensitive_settings_retain_duplicates_and_redact() -> Result<(), String> {
599 let origin = origin()?;
600 let mut acquisition = ImageAcquisition::new(Identifier::new("base-image").map_err(|error| error.to_string())?);
601 acquisition.set_settings_with_origins(
602 vec![
603 Sourced::from_source(
604 ImageAcquisitionSetting::ImageTags(BuildSettingValues::new(
605 BuildSyntax::Repeated,
606 vec![
607 Sourced::from_source(ProtectedString::plain("web:one"), origin.clone()),
608 Sourced::from_source(ProtectedString::plain("web:one"), origin.clone()),
609 ],
610 )),
611 origin.clone(),
612 ),
613 Sourced::from_source(
614 ImageAcquisitionSetting::Credentials(ProtectedString::sensitive("operator:secret")),
615 origin.clone(),
616 ),
617 ],
618 vec![origin.clone()],
619 );
620 let settings = acquisition.settings().ok_or("explicit settings")?;
621 assert!(matches!(
622 settings[0].value(),
623 ImageAcquisitionSetting::ImageTags(values) if values.values().len() == 2 && values.values()[0] == values.values()[1]
624 ));
625 assert_eq!(acquisition.settings_origins(), std::slice::from_ref(&origin));
626 let assignment = ImageArtifactAssignment::new(
627 ProtectedString::plain("TOKEN"),
628 Some(ProtectedString::sensitive("build-secret")),
629 );
630 let target_secrets = ImageBuildSetting::Secrets(BuildSettingValues::new(
631 BuildSyntax::Repeated,
632 vec![Sourced::from_source(
633 ProtectedString::sensitive("target-build-secret"),
634 origin.clone(),
635 )],
636 ));
637 let source_ssh = SourceBuildSetting::Ssh(BuildSettingValues::new(
638 BuildSyntax::Sequence,
639 vec![Sourced::from_source(
640 ProtectedString::sensitive("ssh-agent-secret"),
641 origin.clone(),
642 )],
643 ));
644 let auth = ImageAcquisitionSetting::AuthFile(ProtectedString::sensitive("auth-file-secret"));
645 let decryption = ImageAcquisitionSetting::DecryptionKey(ProtectedString::sensitive("decryption-secret"));
646 let debug = format!("{acquisition:?} {assignment:?} {target_secrets:?} {source_ssh:?} {auth:?} {decryption:?}");
647 for secret in [
648 "operator:secret",
649 "build-secret",
650 "target-build-secret",
651 "ssh-agent-secret",
652 "auth-file-secret",
653 "decryption-secret",
654 ] {
655 assert!(!debug.contains(secret));
656 }
657 assert!(debug.contains("[REDACTED]"));
658 Ok(())
659 }
660
661 #[test]
662 fn malformed_or_empty_assignments_are_retained_for_adapter_diagnostics() {
663 let assignment = ImageArtifactAssignment::new(ProtectedString::plain(""), Some(ProtectedString::plain("")));
664 assert_eq!(assignment.name().expose(), "");
665 assert_eq!(assignment.value().map(ProtectedString::expose), Some(""));
666 }
667}