use crate::{Identifier, ProtectedString, Provenance, ResourceLimit, Sourced};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum BuildSyntax {
Scalar,
Structured,
Mapping,
Sequence,
Repeated,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BuildSettingValues<T> {
syntax: BuildSyntax,
values: Vec<Sourced<T>>,
}
impl<T> BuildSettingValues<T> {
#[must_use]
pub const fn new(syntax: BuildSyntax, values: Vec<Sourced<T>>) -> Self {
Self { syntax, values }
}
#[must_use]
pub const fn syntax(&self) -> BuildSyntax {
self.syntax
}
#[must_use]
pub fn values(&self) -> &[Sourced<T>] {
&self.values
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ImageArtifactAssignment {
name: ProtectedString,
value: Option<ProtectedString>,
}
impl ImageArtifactAssignment {
#[must_use]
pub const fn new(name: ProtectedString, value: Option<ProtectedString>) -> Self {
Self { name, value }
}
#[must_use]
pub const fn name(&self) -> &ProtectedString {
&self.name
}
#[must_use]
pub const fn value(&self) -> Option<&ProtectedString> {
self.value.as_ref()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BuildContext {
name: ProtectedString,
value: ProtectedString,
}
impl BuildContext {
#[must_use]
pub const fn new(name: ProtectedString, value: ProtectedString) -> Self {
Self { name, value }
}
#[must_use]
pub const fn name(&self) -> &ProtectedString {
&self.name
}
#[must_use]
pub const fn value(&self) -> &ProtectedString {
&self.value
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SourceBuildSecret {
source: ProtectedString,
target: Option<ProtectedString>,
uid: Option<ProtectedString>,
gid: Option<ProtectedString>,
mode: Option<ProtectedString>,
}
impl SourceBuildSecret {
#[must_use]
pub const fn new(source: ProtectedString) -> Self {
Self {
source,
target: None,
uid: None,
gid: None,
mode: None,
}
}
#[must_use]
pub const fn source(&self) -> &ProtectedString {
&self.source
}
pub fn set_target(&mut self, target: ProtectedString) {
self.target = Some(target);
}
#[must_use]
pub const fn target(&self) -> Option<&ProtectedString> {
self.target.as_ref()
}
pub fn set_uid(&mut self, uid: ProtectedString) {
self.uid = Some(uid);
}
#[must_use]
pub const fn uid(&self) -> Option<&ProtectedString> {
self.uid.as_ref()
}
pub fn set_gid(&mut self, gid: ProtectedString) {
self.gid = Some(gid);
}
#[must_use]
pub const fn gid(&self) -> Option<&ProtectedString> {
self.gid.as_ref()
}
pub fn set_mode(&mut self, mode: ProtectedString) {
self.mode = Some(mode);
}
#[must_use]
pub const fn mode(&self) -> Option<&ProtectedString> {
self.mode.as_ref()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum BuildAttestation {
Boolean(bool),
Value(ProtectedString),
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum SourceBuildSetting {
AdditionalContexts(BuildSettingValues<BuildContext>),
Arguments(BuildSettingValues<ImageArtifactAssignment>),
CacheFrom(BuildSettingValues<ProtectedString>),
CacheTo(BuildSettingValues<ProtectedString>),
Context(ProtectedString),
RecipeFile(ProtectedString),
InlineRecipe(ProtectedString),
Entitlements(BuildSettingValues<ProtectedString>),
ExtraHosts(BuildSettingValues<ImageArtifactAssignment>),
Isolation(ProtectedString),
Labels(BuildSettingValues<ImageArtifactAssignment>),
Network(ProtectedString),
NoCache(bool),
NoCacheFilters(BuildSettingValues<ProtectedString>),
Platforms(BuildSettingValues<ProtectedString>),
Privileged(bool),
Provenance(BuildAttestation),
Pull(bool),
Sbom(BuildAttestation),
Secrets(BuildSettingValues<SourceBuildSecret>),
ShmSize(ProtectedString),
Ssh(BuildSettingValues<ProtectedString>),
Tags(BuildSettingValues<ProtectedString>),
Target(ProtectedString),
Ulimits(BuildSettingValues<ResourceLimit>),
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum BuildSourceDeclaration {
Scalar(ProtectedString),
Structured(Vec<Sourced<SourceBuildSetting>>),
}
impl BuildSourceDeclaration {
#[must_use]
pub const fn syntax(&self) -> BuildSyntax {
match self {
Self::Scalar(_) => BuildSyntax::Scalar,
Self::Structured(_) => BuildSyntax::Structured,
}
}
#[must_use]
pub fn structured_settings(&self) -> Option<&[Sourced<SourceBuildSetting>]> {
match self {
Self::Scalar(_) => None,
Self::Structured(settings) => Some(settings),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ImageAcquisitionSetting {
Image(ProtectedString),
ImageTags(BuildSettingValues<ProtectedString>),
ServiceName(ProtectedString),
AllTags(bool),
Architecture(ProtectedString),
AuthFile(ProtectedString),
CertificateDirectory(ProtectedString),
ContainersConfigModules(BuildSettingValues<ProtectedString>),
Credentials(ProtectedString),
DecryptionKey(ProtectedString),
GlobalArguments(BuildSettingValues<ProtectedString>),
OperatingSystem(ProtectedString),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ImageAcquisition {
name: Identifier,
settings: Option<Vec<Sourced<ImageAcquisitionSetting>>>,
settings_origins: Vec<Provenance>,
}
impl ImageAcquisition {
#[must_use]
pub const fn new(name: Identifier) -> Self {
Self {
name,
settings: None,
settings_origins: Vec::new(),
}
}
#[must_use]
pub const fn name(&self) -> &Identifier {
&self.name
}
pub fn set_settings(&mut self, settings: Vec<Sourced<ImageAcquisitionSetting>>) {
self.settings = Some(settings);
self.settings_origins.clear();
}
pub fn set_settings_with_origins(
&mut self,
settings: Vec<Sourced<ImageAcquisitionSetting>>,
origins: Vec<Provenance>,
) {
self.settings = Some(settings);
self.settings_origins = origins;
}
#[must_use]
pub fn settings(&self) -> Option<&[Sourced<ImageAcquisitionSetting>]> {
self.settings.as_deref()
}
#[must_use]
pub fn settings_origins(&self) -> &[Provenance] {
&self.settings_origins
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ImageBuildSetting {
ImageTags(BuildSettingValues<ProtectedString>),
Network(ProtectedString),
Labels(BuildSettingValues<ImageArtifactAssignment>),
RecipeFile(ProtectedString),
SetWorkingDirectory(ProtectedString),
Target(ProtectedString),
BuildArguments(BuildSettingValues<ImageArtifactAssignment>),
Secrets(BuildSettingValues<ProtectedString>),
Architecture(ProtectedString),
Variant(ProtectedString),
PullPolicy(ProtectedString),
Retry(ProtectedString),
RetryDelay(ProtectedString),
TlsVerify(bool),
ForceRemove(bool),
AuthFile(ProtectedString),
IgnoreFile(ProtectedString),
ServiceName(ProtectedString),
GroupAdd(BuildSettingValues<ProtectedString>),
DnsServers(BuildSettingValues<ProtectedString>),
DnsOptions(BuildSettingValues<ProtectedString>),
DnsSearchDomains(BuildSettingValues<ProtectedString>),
Annotations(BuildSettingValues<ImageArtifactAssignment>),
Environment(BuildSettingValues<ImageArtifactAssignment>),
ContainersConfigModules(BuildSettingValues<ProtectedString>),
GlobalArguments(BuildSettingValues<ProtectedString>),
Volumes(BuildSettingValues<ProtectedString>),
RuntimeArguments(BuildSettingValues<ProtectedString>),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ImageBuild {
name: Identifier,
source_declaration: Option<Sourced<BuildSourceDeclaration>>,
settings: Option<Vec<Sourced<ImageBuildSetting>>>,
settings_origins: Vec<Provenance>,
}
impl ImageBuild {
#[must_use]
pub const fn new(name: Identifier) -> Self {
Self {
name,
source_declaration: None,
settings: None,
settings_origins: Vec::new(),
}
}
#[must_use]
pub const fn name(&self) -> &Identifier {
&self.name
}
pub fn set_source_declaration(&mut self, declaration: Sourced<BuildSourceDeclaration>) {
self.source_declaration = Some(declaration);
}
#[must_use]
pub const fn source_declaration(&self) -> Option<&Sourced<BuildSourceDeclaration>> {
self.source_declaration.as_ref()
}
pub fn set_settings(&mut self, settings: Vec<Sourced<ImageBuildSetting>>) {
self.settings = Some(settings);
self.settings_origins.clear();
}
pub fn set_settings_with_origins(&mut self, settings: Vec<Sourced<ImageBuildSetting>>, origins: Vec<Provenance>) {
self.settings = Some(settings);
self.settings_origins = origins;
}
#[must_use]
pub fn settings(&self) -> Option<&[Sourced<ImageBuildSetting>]> {
self.settings.as_deref()
}
#[must_use]
pub fn settings_origins(&self) -> &[Provenance] {
&self.settings_origins
}
}
#[cfg(test)]
mod tests {
use super::{
BuildSettingValues, BuildSourceDeclaration, BuildSyntax, ImageAcquisition, ImageAcquisitionSetting,
ImageArtifactAssignment, ImageBuild, ImageBuildSetting, SourceBuildSetting,
};
use crate::{Identifier, ProtectedString, Provenance, SourceId, Sourced};
fn origin() -> Result<Provenance, String> {
SourceId::new("build.yaml")
.map(Provenance::source)
.map_err(|error| error.to_string())
}
#[test]
fn native_text_settings_preserve_working_directory_and_pull_policy() -> Result<(), String> {
let origin = origin()?;
let mut build = ImageBuild::new(Identifier::new("web-build").map_err(|error| error.to_string())?);
build.set_settings(vec![
Sourced::from_source(
ImageBuildSetting::SetWorkingDirectory(ProtectedString::plain("unit")),
origin.clone(),
),
Sourced::from_source(ImageBuildSetting::PullPolicy(ProtectedString::plain("newer")), origin),
]);
let settings = build.settings().ok_or("explicit settings")?;
assert!(matches!(
settings[0].value(),
ImageBuildSetting::SetWorkingDirectory(value) if value.expose() == "unit"
));
assert!(matches!(
settings[1].value(),
ImageBuildSetting::PullPolicy(value) if value.expose() == "newer"
));
Ok(())
}
#[test]
fn per_key_empty_reset_is_distinct_from_omission_and_retains_order_and_provenance() -> Result<(), String> {
let origin = origin()?;
let mut build = ImageBuild::new(Identifier::new("web-build").map_err(|error| error.to_string())?);
assert_eq!(build.source_declaration(), None);
build.set_source_declaration(Sourced::from_source(
BuildSourceDeclaration::Structured(vec![
Sourced::from_source(
SourceBuildSetting::Tags(BuildSettingValues::new(BuildSyntax::Sequence, Vec::new())),
origin.clone(),
),
Sourced::from_source(
SourceBuildSetting::Arguments(BuildSettingValues::new(BuildSyntax::Mapping, Vec::new())),
origin.clone(),
),
]),
origin.clone(),
));
let declaration = build.source_declaration().ok_or("structured declaration")?;
let settings = declaration.value().structured_settings().ok_or("structured settings")?;
assert_eq!(settings.len(), 2);
assert!(matches!(
settings[0].value(),
SourceBuildSetting::Tags(values) if values.values().is_empty() && values.syntax() == BuildSyntax::Sequence
));
assert!(matches!(
settings[1].value(),
SourceBuildSetting::Arguments(values) if values.values().is_empty() && values.syntax() == BuildSyntax::Mapping
));
assert_eq!(settings[0].origins(), std::slice::from_ref(&origin));
Ok(())
}
#[test]
fn scalar_and_structured_source_declarations_remain_distinct() -> Result<(), String> {
let origin = origin()?;
let mut build = ImageBuild::new(Identifier::new("web-build").map_err(|error| error.to_string())?);
build.set_source_declaration(Sourced::from_source(
BuildSourceDeclaration::Scalar(ProtectedString::plain("./web")),
origin,
));
assert_eq!(
build
.source_declaration()
.map(|declaration| declaration.value().syntax()),
Some(BuildSyntax::Scalar)
);
assert_eq!(
build
.source_declaration()
.and_then(|declaration| declaration.value().structured_settings()),
None
);
Ok(())
}
#[test]
fn repeated_acquisition_values_and_sensitive_settings_retain_duplicates_and_redact() -> Result<(), String> {
let origin = origin()?;
let mut acquisition = ImageAcquisition::new(Identifier::new("base-image").map_err(|error| error.to_string())?);
acquisition.set_settings_with_origins(
vec![
Sourced::from_source(
ImageAcquisitionSetting::ImageTags(BuildSettingValues::new(
BuildSyntax::Repeated,
vec![
Sourced::from_source(ProtectedString::plain("web:one"), origin.clone()),
Sourced::from_source(ProtectedString::plain("web:one"), origin.clone()),
],
)),
origin.clone(),
),
Sourced::from_source(
ImageAcquisitionSetting::Credentials(ProtectedString::sensitive("operator:secret")),
origin.clone(),
),
],
vec![origin.clone()],
);
let settings = acquisition.settings().ok_or("explicit settings")?;
assert!(matches!(
settings[0].value(),
ImageAcquisitionSetting::ImageTags(values) if values.values().len() == 2 && values.values()[0] == values.values()[1]
));
assert_eq!(acquisition.settings_origins(), std::slice::from_ref(&origin));
let assignment = ImageArtifactAssignment::new(
ProtectedString::plain("TOKEN"),
Some(ProtectedString::sensitive("build-secret")),
);
let target_secrets = ImageBuildSetting::Secrets(BuildSettingValues::new(
BuildSyntax::Repeated,
vec![Sourced::from_source(
ProtectedString::sensitive("target-build-secret"),
origin.clone(),
)],
));
let source_ssh = SourceBuildSetting::Ssh(BuildSettingValues::new(
BuildSyntax::Sequence,
vec![Sourced::from_source(
ProtectedString::sensitive("ssh-agent-secret"),
origin.clone(),
)],
));
let auth = ImageAcquisitionSetting::AuthFile(ProtectedString::sensitive("auth-file-secret"));
let decryption = ImageAcquisitionSetting::DecryptionKey(ProtectedString::sensitive("decryption-secret"));
let debug = format!("{acquisition:?} {assignment:?} {target_secrets:?} {source_ssh:?} {auth:?} {decryption:?}");
for secret in [
"operator:secret",
"build-secret",
"target-build-secret",
"ssh-agent-secret",
"auth-file-secret",
"decryption-secret",
] {
assert!(!debug.contains(secret));
}
assert!(debug.contains("[REDACTED]"));
Ok(())
}
#[test]
fn malformed_or_empty_assignments_are_retained_for_adapter_diagnostics() {
let assignment = ImageArtifactAssignment::new(ProtectedString::plain(""), Some(ProtectedString::plain("")));
assert_eq!(assignment.name().expose(), "");
assert_eq!(assignment.value().map(ProtectedString::expose), Some(""));
}
}