use std::{
collections::{BTreeMap, BTreeSet},
error::Error,
fmt,
net::IpAddr,
};
use crate::{ImageAcquisition, ImageBuild, ImageReference, ProtectedString, Provenance, Sourced};
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ModelError {
EmptyValue(&'static str),
ContainsNul(&'static str),
ReversedSpan {
start: usize,
end: usize,
},
DuplicateResource {
kind: &'static str,
name: String,
},
DuplicateServiceGroupMember {
group: String,
service: String,
},
UnknownServiceGroupMember {
group: String,
service: String,
},
ServiceInMultipleGroups {
service: String,
existing: String,
replacement: String,
},
UnknownImageAcquisitionReference {
service: String,
acquisition: String,
},
UnknownImageBuildReference {
service: String,
build: String,
},
UnknownVolumeImageAcquisitionReference {
volume: String,
acquisition: String,
},
UnknownVolumeImageBuildReference {
volume: String,
build: String,
},
UnknownArtifactDependencyNode {
kind: &'static str,
name: String,
},
ImageArtifactDependencyCycle {
nodes: Vec<String>,
},
InvalidImageReference(&'static str),
ZeroContainerPort,
UnknownNetworkAttachmentIndex {
index: usize,
len: usize,
},
UnknownServiceGroupRuntimeNetworkIndex {
index: usize,
len: usize,
},
RootfsImageSourceConflict {
service: String,
source: &'static str,
},
InvalidHealthcheckRetries,
}
impl fmt::Display for ModelError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyValue(kind) => write!(formatter, "{kind} must not be empty"),
Self::ContainsNul(kind) => write!(formatter, "{kind} must not contain a NUL byte"),
Self::ReversedSpan { start, end } => {
write!(formatter, "source span end {end} is before start {start}")
}
Self::DuplicateResource { kind, name } => {
write!(formatter, "duplicate {kind} `{name}`")
}
Self::DuplicateServiceGroupMember { group, service } => {
write!(
formatter,
"service group `{group}` contains duplicate member `{service}`"
)
}
Self::UnknownServiceGroupMember { group, service } => {
write!(
formatter,
"service group `{group}` references unknown service `{service}`"
)
}
Self::ServiceInMultipleGroups {
service,
existing,
replacement,
} => write!(
formatter,
"service `{service}` belongs to both service groups `{existing}` and `{replacement}`"
),
Self::UnknownImageAcquisitionReference { service, acquisition } => write!(
formatter,
"service `{service}` references unknown image acquisition `{acquisition}`"
),
Self::UnknownImageBuildReference { service, build } => {
write!(
formatter,
"service `{service}` references unknown image build `{build}`"
)
}
Self::UnknownVolumeImageAcquisitionReference { volume, acquisition } => write!(
formatter,
"volume `{volume}` references unknown image acquisition `{acquisition}`"
),
Self::UnknownVolumeImageBuildReference { volume, build } => {
write!(formatter, "volume `{volume}` references unknown image build `{build}`")
}
Self::UnknownArtifactDependencyNode { kind, name } => {
write!(formatter, "artifact dependency references unknown {kind} `{name}`")
}
Self::ImageArtifactDependencyCycle { nodes } => {
write!(formatter, "image-artifact dependency cycle: {}", nodes.join(" -> "))
}
Self::InvalidImageReference(reason) => write!(formatter, "invalid image reference: {reason}"),
Self::ZeroContainerPort => formatter.write_str("container port must not be zero"),
Self::UnknownNetworkAttachmentIndex { index, len } => {
write!(
formatter,
"network attachment index {index} is outside collection length {len}"
)
}
Self::UnknownServiceGroupRuntimeNetworkIndex { index, len } => {
write!(
formatter,
"group-runtime network attachment index {index} is outside collection length {len}"
)
}
Self::RootfsImageSourceConflict { service, source } => write!(
formatter,
"service `{service}` combines rootfs with image source `{source}`"
),
Self::InvalidHealthcheckRetries => {
formatter.write_str("health-check retries must be a non-negative decimal integer")
}
}
}
}
impl Error for ModelError {}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Identifier(String);
impl Identifier {
pub fn new(value: impl Into<String>) -> Result<Self, ModelError> {
let value = value.into();
validate_text("identifier", &value)?;
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ResourceOwnership {
Application,
External,
Implicit,
Uncertain,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Volume {
name: Identifier,
ownership: ResourceOwnership,
runtime_name: Option<Sourced<ProtectedString>>,
service_name: Option<Sourced<ProtectedString>>,
driver: Option<Sourced<ProtectedString>>,
device: Option<Sourced<ProtectedString>>,
type_spelling: Option<Sourced<ProtectedString>>,
options: Option<Sourced<ProtectedString>>,
labels: Option<Vec<Sourced<MetadataLabel>>>,
labels_origins: Vec<Provenance>,
copy: Option<Sourced<bool>>,
containers_conf_modules: Option<Vec<Sourced<ProtectedString>>>,
containers_conf_modules_origins: Vec<Provenance>,
global_args: Option<Vec<Sourced<ProtectedString>>>,
global_args_origins: Vec<Provenance>,
podman_args: Option<Vec<Sourced<ProtectedString>>>,
podman_args_origins: Vec<Provenance>,
user: Option<Sourced<ProtectedString>>,
group: Option<Sourced<ProtectedString>>,
uid: Option<Sourced<ProtectedString>>,
gid: Option<Sourced<ProtectedString>>,
image_source: Option<Sourced<VolumeImageSource>>,
}
impl Volume {
#[must_use]
pub const fn new(name: Identifier, ownership: ResourceOwnership) -> Self {
Self {
name,
ownership,
runtime_name: None,
service_name: None,
driver: None,
device: None,
type_spelling: None,
options: None,
labels: None,
labels_origins: Vec::new(),
copy: None,
containers_conf_modules: None,
containers_conf_modules_origins: Vec::new(),
global_args: None,
global_args_origins: Vec::new(),
podman_args: None,
podman_args_origins: Vec::new(),
user: None,
group: None,
uid: None,
gid: None,
image_source: None,
}
}
#[must_use]
pub const fn name(&self) -> &Identifier {
&self.name
}
#[must_use]
pub const fn ownership(&self) -> ResourceOwnership {
self.ownership
}
pub fn set_runtime_name(&mut self, name: Sourced<ProtectedString>) {
self.runtime_name = Some(name);
}
#[must_use]
pub const fn runtime_name(&self) -> Option<&Sourced<ProtectedString>> {
self.runtime_name.as_ref()
}
pub fn set_service_name(&mut self, name: Sourced<ProtectedString>) {
self.service_name = Some(name);
}
#[must_use]
pub const fn service_name(&self) -> Option<&Sourced<ProtectedString>> {
self.service_name.as_ref()
}
pub fn set_driver(&mut self, driver: Sourced<ProtectedString>) {
self.driver = Some(driver);
}
#[must_use]
pub const fn driver(&self) -> Option<&Sourced<ProtectedString>> {
self.driver.as_ref()
}
pub fn set_device(&mut self, device: Sourced<ProtectedString>) {
self.device = Some(device);
}
#[must_use]
pub const fn device(&self) -> Option<&Sourced<ProtectedString>> {
self.device.as_ref()
}
pub fn set_volume_type(&mut self, volume_type: Sourced<ProtectedString>) {
self.type_spelling = Some(volume_type);
}
#[must_use]
pub const fn volume_type(&self) -> Option<&Sourced<ProtectedString>> {
self.type_spelling.as_ref()
}
pub fn set_options(&mut self, options: Sourced<ProtectedString>) {
self.options = Some(options);
}
#[must_use]
pub const fn options(&self) -> Option<&Sourced<ProtectedString>> {
self.options.as_ref()
}
pub fn set_labels(&mut self, labels: Vec<Sourced<MetadataLabel>>) {
self.set_labels_with_origins(labels, Vec::new());
}
pub fn set_labels_with_origins(&mut self, labels: Vec<Sourced<MetadataLabel>>, origins: Vec<Provenance>) {
self.labels = Some(labels);
self.labels_origins = origins;
}
pub fn add_label(&mut self, label: Sourced<MetadataLabel>) {
self.labels.get_or_insert_default().push(label);
}
#[must_use]
pub fn labels(&self) -> Option<&[Sourced<MetadataLabel>]> {
self.labels.as_deref()
}
#[must_use]
pub fn labels_origins(&self) -> &[Provenance] {
&self.labels_origins
}
pub fn set_copy(&mut self, copy: Sourced<bool>) {
self.copy = Some(copy);
}
#[must_use]
pub const fn copy(&self) -> Option<&Sourced<bool>> {
self.copy.as_ref()
}
pub fn set_containers_conf_modules(&mut self, values: Vec<Sourced<ProtectedString>>) {
self.set_containers_conf_modules_with_origins(values, Vec::new());
}
pub fn set_containers_conf_modules_with_origins(
&mut self,
values: Vec<Sourced<ProtectedString>>,
origins: Vec<Provenance>,
) {
self.containers_conf_modules = Some(values);
self.containers_conf_modules_origins = origins;
}
#[must_use]
pub fn containers_conf_modules(&self) -> Option<&[Sourced<ProtectedString>]> {
self.containers_conf_modules.as_deref()
}
#[must_use]
pub fn containers_conf_modules_origins(&self) -> &[Provenance] {
&self.containers_conf_modules_origins
}
pub fn set_global_args(&mut self, values: Vec<Sourced<ProtectedString>>) {
self.set_global_args_with_origins(values, Vec::new());
}
pub fn set_global_args_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
self.global_args = Some(values);
self.global_args_origins = origins;
}
#[must_use]
pub fn global_args(&self) -> Option<&[Sourced<ProtectedString>]> {
self.global_args.as_deref()
}
#[must_use]
pub fn global_args_origins(&self) -> &[Provenance] {
&self.global_args_origins
}
pub fn set_podman_args(&mut self, values: Vec<Sourced<ProtectedString>>) {
self.set_podman_args_with_origins(values, Vec::new());
}
pub fn set_podman_args_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
self.podman_args = Some(values);
self.podman_args_origins = origins;
}
#[must_use]
pub fn podman_args(&self) -> Option<&[Sourced<ProtectedString>]> {
self.podman_args.as_deref()
}
#[must_use]
pub fn podman_args_origins(&self) -> &[Provenance] {
&self.podman_args_origins
}
pub fn set_user(&mut self, user: Sourced<ProtectedString>) {
self.user = Some(user);
}
#[must_use]
pub const fn user(&self) -> Option<&Sourced<ProtectedString>> {
self.user.as_ref()
}
pub fn set_group(&mut self, group: Sourced<ProtectedString>) {
self.group = Some(group);
}
#[must_use]
pub const fn group(&self) -> Option<&Sourced<ProtectedString>> {
self.group.as_ref()
}
pub fn set_uid(&mut self, uid: Sourced<ProtectedString>) {
self.uid = Some(uid);
}
#[must_use]
pub const fn uid(&self) -> Option<&Sourced<ProtectedString>> {
self.uid.as_ref()
}
pub fn set_gid(&mut self, gid: Sourced<ProtectedString>) {
self.gid = Some(gid);
}
#[must_use]
pub const fn gid(&self) -> Option<&Sourced<ProtectedString>> {
self.gid.as_ref()
}
pub fn set_image_source(&mut self, image_source: Sourced<VolumeImageSource>) -> Result<(), ModelError> {
image_source.value().validate()?;
self.image_source = Some(image_source);
Ok(())
}
#[must_use]
pub const fn image_source(&self) -> Option<&Sourced<VolumeImageSource>> {
self.image_source.as_ref()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum VolumeImageSource {
Literal(ProtectedString),
ImageAcquisition(Identifier),
ImageBuild(Identifier),
}
impl VolumeImageSource {
fn validate(&self) -> Result<(), ModelError> {
if let Self::Literal(image) = self {
validate_text("volume image", image.expose())?;
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[non_exhaustive]
pub enum ArtifactDependencyNode {
Volume(Identifier),
ImageAcquisition(Identifier),
ImageBuild(Identifier),
}
impl ArtifactDependencyNode {
fn kind_and_name(&self) -> (&'static str, &Identifier) {
match self {
Self::Volume(name) => ("volume", name),
Self::ImageAcquisition(name) => ("image acquisition", name),
Self::ImageBuild(name) => ("image build", name),
}
}
fn display_name(&self) -> String {
let (kind, name) = self.kind_and_name();
format!("{kind}:{}", name.as_str())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ArtifactDependency {
source: Sourced<ArtifactDependencyNode>,
target: Sourced<ArtifactDependencyNode>,
}
impl ArtifactDependency {
#[must_use]
pub const fn new(source: Sourced<ArtifactDependencyNode>, target: Sourced<ArtifactDependencyNode>) -> Self {
Self { source, target }
}
#[must_use]
pub const fn source(&self) -> &Sourced<ArtifactDependencyNode> {
&self.source
}
#[must_use]
pub const fn target(&self) -> &Sourced<ArtifactDependencyNode> {
&self.target
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Network {
name: Identifier,
ownership: ResourceOwnership,
runtime_name: Option<Sourced<ProtectedString>>,
driver: Option<Sourced<ProtectedString>>,
driver_options: Option<Vec<Sourced<NetworkDriverOption>>>,
driver_options_origins: Vec<Provenance>,
labels: Option<Vec<Sourced<MetadataLabel>>>,
labels_origins: Vec<Provenance>,
internal: Option<Sourced<bool>>,
ipv6: Option<Sourced<bool>>,
ipam_driver: Option<Sourced<ProtectedString>>,
ipam_configs: Option<Vec<Sourced<NetworkIpamConfig>>>,
ipam_configs_origins: Vec<Provenance>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NetworkDriverOption {
name: Sourced<Identifier>,
value: Sourced<ProtectedString>,
}
impl NetworkDriverOption {
pub fn new(name: Sourced<Identifier>, value: Sourced<ProtectedString>) -> Result<Self, ModelError> {
validate_no_nul("network driver option value", value.value().expose())?;
Ok(Self { name, value })
}
#[must_use]
pub const fn name(&self) -> &Sourced<Identifier> {
&self.name
}
#[must_use]
pub const fn value(&self) -> &Sourced<ProtectedString> {
&self.value
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NetworkIpamConfig {
subnet: Sourced<ProtectedString>,
gateway: Option<Sourced<ProtectedString>>,
ip_range: Option<Sourced<ProtectedString>>,
}
impl NetworkIpamConfig {
pub fn new(subnet: Sourced<ProtectedString>) -> Result<Self, ModelError> {
validate_text("network IPAM subnet", subnet.value().expose())?;
Ok(Self {
subnet,
gateway: None,
ip_range: None,
})
}
#[must_use]
pub const fn subnet(&self) -> &Sourced<ProtectedString> {
&self.subnet
}
pub fn set_gateway(&mut self, gateway: Sourced<ProtectedString>) -> Result<(), ModelError> {
validate_text("network IPAM gateway", gateway.value().expose())?;
self.gateway = Some(gateway);
Ok(())
}
#[must_use]
pub const fn gateway(&self) -> Option<&Sourced<ProtectedString>> {
self.gateway.as_ref()
}
pub fn set_ip_range(&mut self, ip_range: Sourced<ProtectedString>) -> Result<(), ModelError> {
validate_text("network IPAM IP range", ip_range.value().expose())?;
self.ip_range = Some(ip_range);
Ok(())
}
#[must_use]
pub const fn ip_range(&self) -> Option<&Sourced<ProtectedString>> {
self.ip_range.as_ref()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ConfigMaterial {
File(ProtectedString),
Environment(ProtectedString),
Content(ProtectedString),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Config {
name: Identifier,
ownership: ResourceOwnership,
runtime_name: Option<Sourced<ProtectedString>>,
material: Option<Sourced<ConfigMaterial>>,
}
impl Config {
#[must_use]
pub const fn new(name: Identifier, ownership: ResourceOwnership) -> Self {
Self {
name,
ownership,
runtime_name: None,
material: None,
}
}
#[must_use]
pub const fn name(&self) -> &Identifier {
&self.name
}
#[must_use]
pub const fn ownership(&self) -> ResourceOwnership {
self.ownership
}
pub fn set_runtime_name(&mut self, name: Sourced<ProtectedString>) {
self.runtime_name = Some(name);
}
#[must_use]
pub const fn runtime_name(&self) -> Option<&Sourced<ProtectedString>> {
self.runtime_name.as_ref()
}
pub fn set_material(&mut self, material: Sourced<ConfigMaterial>) {
self.material = Some(material);
}
#[must_use]
pub const fn material(&self) -> Option<&Sourced<ConfigMaterial>> {
self.material.as_ref()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum SecretMaterial {
File(ProtectedString),
Environment(ProtectedString),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Secret {
name: Identifier,
ownership: ResourceOwnership,
runtime_name: Option<Sourced<ProtectedString>>,
material: Option<Sourced<SecretMaterial>>,
}
impl Secret {
#[must_use]
pub const fn new(name: Identifier, ownership: ResourceOwnership) -> Self {
Self {
name,
ownership,
runtime_name: None,
material: None,
}
}
#[must_use]
pub const fn name(&self) -> &Identifier {
&self.name
}
#[must_use]
pub const fn ownership(&self) -> ResourceOwnership {
self.ownership
}
pub fn set_runtime_name(&mut self, name: Sourced<ProtectedString>) {
self.runtime_name = Some(name);
}
#[must_use]
pub const fn runtime_name(&self) -> Option<&Sourced<ProtectedString>> {
self.runtime_name.as_ref()
}
pub fn set_material(&mut self, material: Sourced<SecretMaterial>) {
self.material = Some(material);
}
#[must_use]
pub const fn material(&self) -> Option<&Sourced<SecretMaterial>> {
self.material.as_ref()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ResourceGrantSyntax {
Short,
Long,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResourceGrant {
source: ProtectedString,
syntax: ResourceGrantSyntax,
target: Option<Sourced<ProtectedString>>,
uid: Option<Sourced<ProtectedString>>,
gid: Option<Sourced<ProtectedString>>,
mode: Option<Sourced<ProtectedString>>,
}
impl ResourceGrant {
pub fn new(source: ProtectedString, syntax: ResourceGrantSyntax) -> Result<Self, ModelError> {
validate_text("resource grant source", source.expose())?;
Ok(Self {
source,
syntax,
target: None,
uid: None,
gid: None,
mode: None,
})
}
#[must_use]
pub const fn source(&self) -> &ProtectedString {
&self.source
}
#[must_use]
pub const fn syntax(&self) -> ResourceGrantSyntax {
self.syntax
}
pub fn set_target(&mut self, target: Sourced<ProtectedString>) {
self.target = Some(target);
}
#[must_use]
pub const fn target(&self) -> Option<&Sourced<ProtectedString>> {
self.target.as_ref()
}
pub fn set_uid(&mut self, uid: Sourced<ProtectedString>) {
self.uid = Some(uid);
}
#[must_use]
pub const fn uid(&self) -> Option<&Sourced<ProtectedString>> {
self.uid.as_ref()
}
pub fn set_gid(&mut self, gid: Sourced<ProtectedString>) {
self.gid = Some(gid);
}
#[must_use]
pub const fn gid(&self) -> Option<&Sourced<ProtectedString>> {
self.gid.as_ref()
}
pub fn set_mode(&mut self, mode: Sourced<ProtectedString>) {
self.mode = Some(mode);
}
#[must_use]
pub const fn mode(&self) -> Option<&Sourced<ProtectedString>> {
self.mode.as_ref()
}
}
impl Network {
#[must_use]
pub const fn new(name: Identifier, ownership: ResourceOwnership) -> Self {
Self {
name,
ownership,
runtime_name: None,
driver: None,
driver_options: None,
driver_options_origins: Vec::new(),
labels: None,
labels_origins: Vec::new(),
internal: None,
ipv6: None,
ipam_driver: None,
ipam_configs: None,
ipam_configs_origins: Vec::new(),
}
}
#[must_use]
pub const fn name(&self) -> &Identifier {
&self.name
}
#[must_use]
pub const fn ownership(&self) -> ResourceOwnership {
self.ownership
}
pub fn set_runtime_name(&mut self, name: Sourced<ProtectedString>) {
self.runtime_name = Some(name);
}
#[must_use]
pub const fn runtime_name(&self) -> Option<&Sourced<ProtectedString>> {
self.runtime_name.as_ref()
}
pub fn set_driver(&mut self, driver: Sourced<ProtectedString>) {
self.driver = Some(driver);
}
#[must_use]
pub const fn driver(&self) -> Option<&Sourced<ProtectedString>> {
self.driver.as_ref()
}
pub fn set_driver_options(&mut self, options: Vec<Sourced<NetworkDriverOption>>) {
self.driver_options = Some(options);
self.driver_options_origins.clear();
}
pub fn set_driver_options_with_origins(
&mut self,
options: Vec<Sourced<NetworkDriverOption>>,
origins: Vec<Provenance>,
) {
self.driver_options = Some(options);
self.driver_options_origins = origins;
}
pub fn add_driver_option(&mut self, option: Sourced<NetworkDriverOption>) {
self.driver_options.get_or_insert_default().push(option);
}
#[must_use]
pub fn driver_options(&self) -> Option<&[Sourced<NetworkDriverOption>]> {
self.driver_options.as_deref()
}
#[must_use]
pub fn driver_options_origins(&self) -> &[Provenance] {
&self.driver_options_origins
}
pub fn set_labels(&mut self, labels: Vec<Sourced<MetadataLabel>>) {
self.labels = Some(labels);
self.labels_origins.clear();
}
pub fn set_labels_with_origins(&mut self, labels: Vec<Sourced<MetadataLabel>>, origins: Vec<Provenance>) {
self.labels = Some(labels);
self.labels_origins = origins;
}
pub fn add_label(&mut self, label: Sourced<MetadataLabel>) {
self.labels.get_or_insert_default().push(label);
}
#[must_use]
pub fn labels(&self) -> Option<&[Sourced<MetadataLabel>]> {
self.labels.as_deref()
}
#[must_use]
pub fn labels_origins(&self) -> &[Provenance] {
&self.labels_origins
}
pub fn set_internal(&mut self, internal: Sourced<bool>) {
self.internal = Some(internal);
}
#[must_use]
pub const fn internal(&self) -> Option<&Sourced<bool>> {
self.internal.as_ref()
}
pub fn set_ipv6(&mut self, ipv6: Sourced<bool>) {
self.ipv6 = Some(ipv6);
}
#[must_use]
pub const fn ipv6(&self) -> Option<&Sourced<bool>> {
self.ipv6.as_ref()
}
pub fn set_ipam_driver(&mut self, driver: Sourced<ProtectedString>) {
self.ipam_driver = Some(driver);
}
#[must_use]
pub const fn ipam_driver(&self) -> Option<&Sourced<ProtectedString>> {
self.ipam_driver.as_ref()
}
pub fn set_ipam_configs(&mut self, configs: Vec<Sourced<NetworkIpamConfig>>) {
self.ipam_configs = Some(configs);
self.ipam_configs_origins.clear();
}
pub fn set_ipam_configs_with_origins(
&mut self,
configs: Vec<Sourced<NetworkIpamConfig>>,
origins: Vec<Provenance>,
) {
self.ipam_configs = Some(configs);
self.ipam_configs_origins = origins;
}
pub fn add_ipam_config(&mut self, config: Sourced<NetworkIpamConfig>) {
self.ipam_configs.get_or_insert_default().push(config);
}
#[must_use]
pub fn ipam_configs(&self) -> Option<&[Sourced<NetworkIpamConfig>]> {
self.ipam_configs.as_deref()
}
#[must_use]
pub fn ipam_configs_origins(&self) -> &[Provenance] {
&self.ipam_configs_origins
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ServiceGroup {
name: Identifier,
ownership: ResourceOwnership,
members: Vec<Sourced<Identifier>>,
runtime: Option<Sourced<ServiceGroupRuntime>>,
}
impl ServiceGroup {
#[must_use]
pub const fn new(name: Identifier, ownership: ResourceOwnership) -> Self {
Self {
name,
ownership,
members: Vec::new(),
runtime: None,
}
}
#[must_use]
pub const fn name(&self) -> &Identifier {
&self.name
}
#[must_use]
pub const fn ownership(&self) -> ResourceOwnership {
self.ownership
}
pub fn add_member(&mut self, member: Sourced<Identifier>) -> Result<(), ModelError> {
if self.members.iter().any(|candidate| candidate.value() == member.value()) {
return Err(ModelError::DuplicateServiceGroupMember {
group: self.name.as_str().to_owned(),
service: member.value().as_str().to_owned(),
});
}
self.members.push(member);
Ok(())
}
#[must_use]
pub fn members(&self) -> &[Sourced<Identifier>] {
&self.members
}
pub fn set_runtime(&mut self, runtime: Sourced<ServiceGroupRuntime>) {
self.runtime = Some(runtime);
}
#[must_use]
pub const fn runtime(&self) -> Option<&Sourced<ServiceGroupRuntime>> {
self.runtime.as_ref()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum GroupExitPolicy {
Stop,
Continue,
Raw(ProtectedString),
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ServiceGroupRuntime {
runtime_name: Option<Sourced<ProtectedString>>,
service_name: Option<Sourced<ProtectedString>>,
host_mappings: Option<Vec<Sourced<HostMapping>>>,
host_mappings_origins: Vec<Provenance>,
ports: Option<Vec<Sourced<Port>>>,
ports_origins: Vec<Provenance>,
networks: Option<Vec<Sourced<NetworkAttachment>>>,
networks_origins: Vec<Provenance>,
user_namespace: Option<Sourced<ProtectedString>>,
mounts: Option<Vec<Sourced<Mount>>>,
mounts_origins: Vec<Provenance>,
shm_size: Option<Sourced<ProtectedString>>,
exit_policy: Option<Sourced<GroupExitPolicy>>,
stop_timeout: Option<Sourced<StopTimeout>>,
}
impl ServiceGroupRuntime {
#[must_use]
pub const fn new() -> Self {
Self {
runtime_name: None,
service_name: None,
host_mappings: None,
host_mappings_origins: Vec::new(),
ports: None,
ports_origins: Vec::new(),
networks: None,
networks_origins: Vec::new(),
user_namespace: None,
mounts: None,
mounts_origins: Vec::new(),
shm_size: None,
exit_policy: None,
stop_timeout: None,
}
}
pub fn set_runtime_name(&mut self, name: Sourced<ProtectedString>) {
self.runtime_name = Some(name);
}
#[must_use]
pub const fn runtime_name(&self) -> Option<&Sourced<ProtectedString>> {
self.runtime_name.as_ref()
}
pub fn set_service_name(&mut self, name: Sourced<ProtectedString>) {
self.service_name = Some(name);
}
#[must_use]
pub const fn service_name(&self) -> Option<&Sourced<ProtectedString>> {
self.service_name.as_ref()
}
pub fn set_host_mappings(&mut self, values: Vec<Sourced<HostMapping>>) {
self.set_host_mappings_with_origins(values, Vec::new());
}
pub fn set_host_mappings_with_origins(&mut self, values: Vec<Sourced<HostMapping>>, origins: Vec<Provenance>) {
self.host_mappings = Some(values);
self.host_mappings_origins = origins;
}
pub fn add_host_mapping(&mut self, value: Sourced<HostMapping>) {
self.host_mappings.get_or_insert_default().push(value);
}
#[must_use]
pub fn host_mappings(&self) -> Option<&[Sourced<HostMapping>]> {
self.host_mappings.as_deref()
}
#[must_use]
pub fn host_mappings_origins(&self) -> &[Provenance] {
&self.host_mappings_origins
}
pub fn set_ports(&mut self, values: Vec<Sourced<Port>>) {
self.set_ports_with_origins(values, Vec::new());
}
pub fn set_ports_with_origins(&mut self, values: Vec<Sourced<Port>>, origins: Vec<Provenance>) {
self.ports = Some(values);
self.ports_origins = origins;
}
pub fn add_port(&mut self, value: Sourced<Port>) {
self.ports.get_or_insert_default().push(value);
}
#[must_use]
pub fn ports(&self) -> Option<&[Sourced<Port>]> {
self.ports.as_deref()
}
#[must_use]
pub fn ports_origins(&self) -> &[Provenance] {
&self.ports_origins
}
pub fn set_networks(&mut self, values: Vec<Sourced<NetworkAttachment>>) {
self.set_networks_with_origins(values, Vec::new());
}
pub fn set_networks_with_origins(&mut self, values: Vec<Sourced<NetworkAttachment>>, origins: Vec<Provenance>) {
self.networks = Some(values);
self.networks_origins = origins;
}
pub fn add_network(&mut self, value: Sourced<NetworkAttachment>) {
self.networks.get_or_insert_default().push(value);
}
pub fn replace_network(
&mut self,
index: usize,
value: Sourced<NetworkAttachment>,
) -> Result<Sourced<NetworkAttachment>, ModelError> {
let len = self.networks.as_ref().map_or(0, Vec::len);
let Some(networks) = self.networks.as_mut() else {
return Err(ModelError::UnknownServiceGroupRuntimeNetworkIndex { index, len });
};
let Some(slot) = networks.get_mut(index) else {
return Err(ModelError::UnknownServiceGroupRuntimeNetworkIndex { index, len });
};
Ok(std::mem::replace(slot, value))
}
#[must_use]
pub fn networks(&self) -> Option<&[Sourced<NetworkAttachment>]> {
self.networks.as_deref()
}
#[must_use]
pub fn networks_origins(&self) -> &[Provenance] {
&self.networks_origins
}
pub fn set_user_namespace(&mut self, value: Sourced<ProtectedString>) {
self.user_namespace = Some(value);
}
#[must_use]
pub const fn user_namespace(&self) -> Option<&Sourced<ProtectedString>> {
self.user_namespace.as_ref()
}
pub fn set_mounts(&mut self, values: Vec<Sourced<Mount>>) {
self.set_mounts_with_origins(values, Vec::new());
}
pub fn set_mounts_with_origins(&mut self, values: Vec<Sourced<Mount>>, origins: Vec<Provenance>) {
self.mounts = Some(values);
self.mounts_origins = origins;
}
pub fn add_mount(&mut self, value: Sourced<Mount>) {
self.mounts.get_or_insert_default().push(value);
}
#[must_use]
pub fn mounts(&self) -> Option<&[Sourced<Mount>]> {
self.mounts.as_deref()
}
#[must_use]
pub fn mounts_origins(&self) -> &[Provenance] {
&self.mounts_origins
}
pub fn set_shm_size(&mut self, value: Sourced<ProtectedString>) {
self.shm_size = Some(value);
}
#[must_use]
pub const fn shm_size(&self) -> Option<&Sourced<ProtectedString>> {
self.shm_size.as_ref()
}
pub fn set_exit_policy(&mut self, value: Sourced<GroupExitPolicy>) {
self.exit_policy = Some(value);
}
#[must_use]
pub const fn exit_policy(&self) -> Option<&Sourced<GroupExitPolicy>> {
self.exit_policy.as_ref()
}
pub fn set_stop_timeout(&mut self, value: Sourced<StopTimeout>) {
self.stop_timeout = Some(value);
}
#[must_use]
pub const fn stop_timeout(&self) -> Option<&Sourced<StopTimeout>> {
self.stop_timeout.as_ref()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Command {
Exec(Vec<ProtectedString>),
Shell(ProtectedString),
Empty,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum StartupNotification {
Runtime,
Application,
Healthy,
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Entrypoint {
Exec(Vec<ProtectedString>),
Shell(ProtectedString),
Empty,
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum PullPolicy {
Always,
Missing,
Never,
IfNotPresent,
Build,
Daily,
Weekly,
Every(ProtectedString),
Raw(ProtectedString),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StopTimeout(String);
impl StopTimeout {
pub fn new(value: impl Into<String>) -> Result<Self, ModelError> {
let value = value.into();
validate_text("stop timeout", &value)?;
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ExposedPort {
container: u16,
protocol: Protocol,
}
impl ExposedPort {
pub fn new(container: u16, protocol: Protocol) -> Result<Self, ModelError> {
if container == 0 {
return Err(ModelError::ZeroContainerPort);
}
Ok(Self { container, protocol })
}
#[must_use]
pub const fn container(&self) -> u16 {
self.container
}
#[must_use]
pub const fn protocol(&self) -> &Protocol {
&self.protocol
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum RestartPolicy {
Never,
Always,
OnFailure {
maximum_retries: Option<std::num::NonZeroU64>,
},
UnlessStopped,
}
impl RestartPolicy {
#[must_use]
pub const fn on_failure(maximum_retries: Option<std::num::NonZeroU64>) -> Self {
Self::OnFailure { maximum_retries }
}
#[must_use]
pub const fn maximum_retries(self) -> Option<std::num::NonZeroU64> {
match self {
Self::OnFailure { maximum_retries } => maximum_retries,
Self::Never | Self::Always | Self::UnlessStopped => None,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum HealthcheckCommand {
Exec(Vec<ProtectedString>),
Shell(ProtectedString),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HealthcheckDuration(String);
impl HealthcheckDuration {
pub fn new(value: impl Into<String>) -> Result<Self, ModelError> {
let value = value.into();
validate_text("health-check duration", &value)?;
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HealthcheckRetries(String);
impl HealthcheckRetries {
pub fn new(value: impl Into<String>) -> Result<Self, ModelError> {
let value = value.into();
validate_text("health-check retries", &value)?;
if !value.bytes().all(|byte| byte.is_ascii_digit()) {
return Err(ModelError::InvalidHealthcheckRetries);
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Healthcheck {
command: Option<Sourced<HealthcheckCommand>>,
disabled: Option<Sourced<bool>>,
interval: Option<Sourced<HealthcheckDuration>>,
timeout: Option<Sourced<HealthcheckDuration>>,
retries: Option<Sourced<HealthcheckRetries>>,
start_period: Option<Sourced<HealthcheckDuration>>,
start_interval: Option<Sourced<HealthcheckDuration>>,
}
impl Healthcheck {
#[must_use]
pub const fn new() -> Self {
Self {
command: None,
disabled: None,
interval: None,
timeout: None,
retries: None,
start_period: None,
start_interval: None,
}
}
pub fn set_command(&mut self, command: Sourced<HealthcheckCommand>) {
self.command = Some(command);
}
#[must_use]
pub const fn command(&self) -> Option<&Sourced<HealthcheckCommand>> {
self.command.as_ref()
}
pub fn set_disabled(&mut self, disabled: Sourced<bool>) {
self.disabled = Some(disabled);
}
#[must_use]
pub const fn disabled(&self) -> Option<&Sourced<bool>> {
self.disabled.as_ref()
}
pub fn set_interval(&mut self, interval: Sourced<HealthcheckDuration>) {
self.interval = Some(interval);
}
#[must_use]
pub const fn interval(&self) -> Option<&Sourced<HealthcheckDuration>> {
self.interval.as_ref()
}
pub fn set_timeout(&mut self, timeout: Sourced<HealthcheckDuration>) {
self.timeout = Some(timeout);
}
#[must_use]
pub const fn timeout(&self) -> Option<&Sourced<HealthcheckDuration>> {
self.timeout.as_ref()
}
pub fn set_retries(&mut self, retries: Sourced<HealthcheckRetries>) {
self.retries = Some(retries);
}
#[must_use]
pub const fn retries(&self) -> Option<&Sourced<HealthcheckRetries>> {
self.retries.as_ref()
}
pub fn set_start_period(&mut self, start_period: Sourced<HealthcheckDuration>) {
self.start_period = Some(start_period);
}
#[must_use]
pub const fn start_period(&self) -> Option<&Sourced<HealthcheckDuration>> {
self.start_period.as_ref()
}
pub fn set_start_interval(&mut self, start_interval: Sourced<HealthcheckDuration>) {
self.start_interval = Some(start_interval);
}
#[must_use]
pub const fn start_interval(&self) -> Option<&Sourced<HealthcheckDuration>> {
self.start_interval.as_ref()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum EnvironmentValue {
Literal(ProtectedString),
Host,
Unset,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EnvironmentVariable {
name: Identifier,
value: EnvironmentValue,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum EnvironmentFileSyntax {
Short,
Long,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum EnvironmentFileFormat {
Raw,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EnvironmentFile {
path: ProtectedString,
syntax: EnvironmentFileSyntax,
required: Option<Sourced<bool>>,
format: Option<Sourced<EnvironmentFileFormat>>,
}
impl EnvironmentFile {
pub fn new(path: ProtectedString, syntax: EnvironmentFileSyntax) -> Result<Self, ModelError> {
validate_text("environment-file path", path.expose())?;
Ok(Self {
path,
syntax,
required: None,
format: None,
})
}
#[must_use]
pub const fn path(&self) -> &ProtectedString {
&self.path
}
#[must_use]
pub const fn syntax(&self) -> EnvironmentFileSyntax {
self.syntax
}
pub fn set_required(&mut self, required: Sourced<bool>) {
self.required = Some(required);
}
#[must_use]
pub const fn required(&self) -> Option<&Sourced<bool>> {
self.required.as_ref()
}
#[must_use]
pub fn is_required(&self) -> bool {
self.required.as_ref().is_none_or(|required| *required.value())
}
pub fn set_format(&mut self, format: Sourced<EnvironmentFileFormat>) {
self.format = Some(format);
}
#[must_use]
pub const fn format(&self) -> Option<&Sourced<EnvironmentFileFormat>> {
self.format.as_ref()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MetadataLabel {
name: Identifier,
value: ProtectedString,
}
impl MetadataLabel {
#[must_use]
pub const fn new(name: Identifier, value: ProtectedString) -> Self {
Self { name, value }
}
#[must_use]
pub const fn name(&self) -> &Identifier {
&self.name
}
#[must_use]
pub const fn value(&self) -> &ProtectedString {
&self.value
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Annotation {
name: Sourced<Identifier>,
value: Sourced<ProtectedString>,
}
impl Annotation {
#[must_use]
pub const fn new(name: Sourced<Identifier>, value: Sourced<ProtectedString>) -> Self {
Self { name, value }
}
#[must_use]
pub const fn name(&self) -> &Sourced<Identifier> {
&self.name
}
#[must_use]
pub const fn value(&self) -> &Sourced<ProtectedString> {
&self.value
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LoggingOption {
name: Sourced<Identifier>,
value: Sourced<ProtectedString>,
}
impl LoggingOption {
#[must_use]
pub const fn new(name: Sourced<Identifier>, value: Sourced<ProtectedString>) -> Self {
Self { name, value }
}
#[must_use]
pub const fn name(&self) -> &Sourced<Identifier> {
&self.name
}
#[must_use]
pub const fn value(&self) -> &Sourced<ProtectedString> {
&self.value
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Logging {
driver: Option<Sourced<ProtectedString>>,
options: Option<Vec<Sourced<LoggingOption>>>,
options_origins: Vec<Provenance>,
}
impl Logging {
#[must_use]
pub const fn new() -> Self {
Self {
driver: None,
options: None,
options_origins: Vec::new(),
}
}
pub fn set_driver(&mut self, driver: Sourced<ProtectedString>) {
self.driver = Some(driver);
}
#[must_use]
pub const fn driver(&self) -> Option<&Sourced<ProtectedString>> {
self.driver.as_ref()
}
pub fn set_options(&mut self, options: Vec<Sourced<LoggingOption>>) {
self.options = Some(options);
self.options_origins.clear();
}
pub fn add_option(&mut self, option: Sourced<LoggingOption>) {
self.options.get_or_insert_default().push(option);
}
pub fn set_options_with_origins(&mut self, options: Vec<Sourced<LoggingOption>>, origins: Vec<Provenance>) {
self.options = Some(options);
self.options_origins = origins;
}
#[must_use]
pub fn options(&self) -> Option<&[Sourced<LoggingOption>]> {
self.options.as_deref()
}
#[must_use]
pub fn options_origins(&self) -> &[Provenance] {
&self.options_origins
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ReloadAction {
Command(Command),
Signal(ProtectedString),
}
impl EnvironmentVariable {
#[must_use]
pub const fn new(name: Identifier, value: EnvironmentValue) -> Self {
Self { name, value }
}
#[must_use]
pub const fn name(&self) -> &Identifier {
&self.name
}
#[must_use]
pub const fn value(&self) -> &EnvironmentValue {
&self.value
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HostAddress {
raw: String,
kind: HostAddressKind,
}
impl HostAddress {
pub fn new(raw: impl Into<String>) -> Result<Self, ModelError> {
let raw = raw.into();
validate_text("host mapping address", &raw)?;
let unbracketed = raw
.strip_prefix('[')
.and_then(|value| value.strip_suffix(']'))
.unwrap_or(&raw);
let kind = if raw == "host-gateway" {
HostAddressKind::HostGateway
} else {
match unbracketed.parse::<IpAddr>() {
Ok(IpAddr::V4(_)) => HostAddressKind::Ipv4,
Ok(IpAddr::V6(_)) => HostAddressKind::Ipv6 {
bracketed: raw.starts_with('[') && raw.ends_with(']'),
},
Err(_) => HostAddressKind::Other,
}
};
Ok(Self { raw, kind })
}
#[must_use]
pub fn raw(&self) -> &str {
&self.raw
}
#[must_use]
pub const fn kind(&self) -> HostAddressKind {
self.kind
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum HostAddressKind {
Ipv4,
Ipv6 {
bracketed: bool,
},
HostGateway,
Other,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HostMapping {
hostname: Identifier,
address: HostAddress,
}
impl HostMapping {
#[must_use]
pub const fn new(hostname: Identifier, address: HostAddress) -> Self {
Self { hostname, address }
}
#[must_use]
pub const fn hostname(&self) -> &Identifier {
&self.hostname
}
#[must_use]
pub const fn address(&self) -> &HostAddress {
&self.address
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Protocol {
Tcp,
Udp,
Sctp,
Other(String),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Port {
container: u16,
published: Option<u16>,
host_address: Option<String>,
protocol: Protocol,
}
impl Port {
pub fn new(
container: u16,
published: Option<u16>,
host_address: Option<String>,
protocol: Protocol,
) -> Result<Self, ModelError> {
if container == 0 {
return Err(ModelError::ZeroContainerPort);
}
Ok(Self {
container,
published,
host_address,
protocol,
})
}
#[must_use]
pub const fn container(&self) -> u16 {
self.container
}
#[must_use]
pub const fn published(&self) -> Option<u16> {
self.published
}
#[must_use]
pub fn host_address(&self) -> Option<&str> {
self.host_address.as_deref()
}
#[must_use]
pub const fn protocol(&self) -> &Protocol {
&self.protocol
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum MountSource {
Volume(Identifier),
HostPath(String),
Anonymous,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum SelinuxRelabel {
Shared,
Private,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Mount {
source: MountSource,
target: String,
read_only: bool,
selinux_relabel: Option<SelinuxRelabel>,
}
impl Mount {
pub fn new(source: MountSource, target: impl Into<String>, read_only: bool) -> Result<Self, ModelError> {
let target = target.into();
validate_text("mount target", &target)?;
Ok(Self {
source,
target,
read_only,
selinux_relabel: None,
})
}
#[must_use]
pub const fn source(&self) -> &MountSource {
&self.source
}
#[must_use]
pub fn target(&self) -> &str {
&self.target
}
#[must_use]
pub const fn read_only(&self) -> bool {
self.read_only
}
pub fn set_selinux_relabel(&mut self, relabel: SelinuxRelabel) {
self.selinux_relabel = Some(relabel);
}
#[must_use]
pub const fn selinux_relabel(&self) -> Option<SelinuxRelabel> {
self.selinux_relabel
}
}
#[derive(Clone, Eq, PartialEq)]
pub struct NetworkAttachment {
network: Identifier,
aliases: Vec<String>,
alias_sensitivities: Vec<bool>,
alias_origins: Vec<Vec<Provenance>>,
ipv4_address: Option<Sourced<ProtectedString>>,
ipv6_address: Option<Sourced<ProtectedString>>,
}
impl NetworkAttachment {
#[must_use]
pub const fn new(network: Identifier, aliases: Vec<String>) -> Self {
Self {
network,
aliases,
alias_sensitivities: Vec::new(),
alias_origins: Vec::new(),
ipv4_address: None,
ipv6_address: None,
}
}
#[must_use]
pub fn with_sourced_aliases(network: Identifier, aliases: Vec<Sourced<ProtectedString>>) -> Self {
let mut attachment = Self::new(network, Vec::new());
attachment.set_aliases_with_provenance(aliases);
attachment
}
#[must_use]
pub const fn network(&self) -> &Identifier {
&self.network
}
#[must_use]
pub fn aliases(&self) -> &[String] {
&self.aliases
}
#[must_use]
pub fn alias_origins(&self) -> &[Vec<Provenance>] {
&self.alias_origins
}
#[must_use]
pub fn alias_sensitivities(&self) -> &[bool] {
&self.alias_sensitivities
}
pub fn set_aliases_with_provenance(&mut self, aliases: Vec<Sourced<ProtectedString>>) {
self.aliases = aliases.iter().map(|alias| alias.value().expose().to_owned()).collect();
self.alias_sensitivities = aliases.iter().map(|alias| alias.value().is_sensitive()).collect();
self.alias_origins = aliases.into_iter().map(|alias| alias.origins().to_vec()).collect();
}
pub fn add_alias(&mut self, alias: &Sourced<ProtectedString>) {
self.aliases.push(alias.value().expose().to_owned());
self.alias_sensitivities.push(alias.value().is_sensitive());
self.alias_origins.push(alias.origins().to_vec());
}
pub fn set_ipv4_address(&mut self, address: Sourced<ProtectedString>) {
self.ipv4_address = Some(address);
}
#[must_use]
pub const fn ipv4_address(&self) -> Option<&Sourced<ProtectedString>> {
self.ipv4_address.as_ref()
}
pub fn set_ipv6_address(&mut self, address: Sourced<ProtectedString>) {
self.ipv6_address = Some(address);
}
#[must_use]
pub const fn ipv6_address(&self) -> Option<&Sourced<ProtectedString>> {
self.ipv6_address.as_ref()
}
}
impl fmt::Debug for NetworkAttachment {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let aliases = self
.aliases
.iter()
.enumerate()
.map(|(index, alias)| {
if self.alias_sensitivities.get(index).copied().unwrap_or(false) {
"[REDACTED]"
} else {
alias.as_str()
}
})
.collect::<Vec<_>>();
formatter
.debug_struct("NetworkAttachment")
.field("network", &self.network)
.field("aliases", &aliases)
.field("alias_origins", &self.alias_origins)
.field("ipv4_address", &self.ipv4_address)
.field("ipv6_address", &self.ipv6_address)
.finish()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ServiceDependencyCondition {
Started,
Healthy,
CompletedSuccessfully,
Other(ProtectedString),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ServiceDependency {
service: Identifier,
condition: Option<Sourced<ServiceDependencyCondition>>,
restart: Option<Sourced<bool>>,
required: Option<Sourced<bool>>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct KernelParameter {
name: ProtectedString,
value: ProtectedString,
}
impl KernelParameter {
#[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 ResourceLimit {
name: ProtectedString,
soft: Option<Sourced<ProtectedString>>,
hard: Option<Sourced<ProtectedString>>,
}
impl ResourceLimit {
#[must_use]
pub const fn new(
name: ProtectedString,
soft: Option<Sourced<ProtectedString>>,
hard: Option<Sourced<ProtectedString>>,
) -> Self {
Self { name, soft, hard }
}
#[must_use]
pub const fn name(&self) -> &ProtectedString {
&self.name
}
#[must_use]
pub const fn soft(&self) -> Option<&Sourced<ProtectedString>> {
self.soft.as_ref()
}
#[must_use]
pub const fn hard(&self) -> Option<&Sourced<ProtectedString>> {
self.hard.as_ref()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Device {
Short(ProtectedString),
Long {
source: Option<Sourced<ProtectedString>>,
target: Option<Sourced<ProtectedString>>,
permissions: Option<Sourced<ProtectedString>>,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum SecurityOption {
AppArmor(ProtectedString),
NoNewPrivileges(bool),
SeccompProfile(ProtectedString),
SecurityLabelDisable(bool),
SecurityLabelFileType(ProtectedString),
SecurityLabelLevel(ProtectedString),
SecurityLabelNested(bool),
SecurityLabelType(ProtectedString),
Mask(ProtectedString),
Unmask(ProtectedString),
}
impl ServiceDependency {
#[must_use]
pub const fn new(service: Identifier) -> Self {
Self {
service,
condition: None,
restart: None,
required: None,
}
}
#[must_use]
pub const fn service(&self) -> &Identifier {
&self.service
}
pub fn set_condition(&mut self, condition: Sourced<ServiceDependencyCondition>) {
self.condition = Some(condition);
}
#[must_use]
pub const fn condition(&self) -> Option<&Sourced<ServiceDependencyCondition>> {
self.condition.as_ref()
}
pub fn set_restart(&mut self, restart: Sourced<bool>) {
self.restart = Some(restart);
}
#[must_use]
pub const fn restart(&self) -> Option<&Sourced<bool>> {
self.restart.as_ref()
}
pub fn set_required(&mut self, required: Sourced<bool>) {
self.required = Some(required);
}
#[must_use]
pub const fn required(&self) -> Option<&Sourced<bool>> {
self.required.as_ref()
}
#[must_use]
pub fn is_required(&self) -> bool {
self.required.as_ref().is_none_or(|required| *required.value())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Service {
name: Identifier,
runtime_name: Option<Sourced<ProtectedString>>,
rootfs: Option<Sourced<ProtectedString>>,
image: Option<Sourced<ImageReference>>,
image_acquisition: Option<Sourced<Identifier>>,
image_build: Option<Sourced<Identifier>>,
command: Option<Sourced<Command>>,
startup_notification: Option<Sourced<StartupNotification>>,
entrypoint: Option<Sourced<Entrypoint>>,
run_init: Option<Sourced<bool>>,
stop_timeout: Option<Sourced<StopTimeout>>,
pull_policy: Option<Sourced<PullPolicy>>,
memory_limit: Option<Sourced<ProtectedString>>,
exposed_ports: Option<Vec<Sourced<ExposedPort>>>,
exposed_ports_origins: Vec<Provenance>,
restart_policy: Option<Sourced<RestartPolicy>>,
healthcheck: Option<Sourced<Healthcheck>>,
labels: Vec<Sourced<MetadataLabel>>,
annotations: Option<Vec<Sourced<Annotation>>>,
annotations_origins: Vec<Provenance>,
logging: Option<Sourced<Logging>>,
reload_action: Option<Sourced<ReloadAction>>,
user: Option<Sourced<ProtectedString>>,
group: Option<Sourced<ProtectedString>>,
user_namespace: Option<Sourced<ProtectedString>>,
supplementary_groups: Vec<Sourced<ProtectedString>>,
working_directory: Option<Sourced<ProtectedString>>,
read_only_root_filesystem: Option<Sourced<bool>>,
hostname: Option<Sourced<ProtectedString>>,
dns_servers: Option<Vec<Sourced<ProtectedString>>>,
dns_servers_origins: Vec<Provenance>,
dns_options: Option<Vec<Sourced<ProtectedString>>>,
dns_options_origins: Vec<Provenance>,
dns_search_domains: Option<Vec<Sourced<ProtectedString>>>,
dns_search_domains_origins: Vec<Provenance>,
security_options: Option<Vec<Sourced<SecurityOption>>>,
security_options_origins: Vec<Provenance>,
pids_limit: Option<Sourced<ProtectedString>>,
shm_size: Option<Sourced<ProtectedString>>,
cap_add: Option<Vec<Sourced<ProtectedString>>>,
cap_add_origins: Vec<Provenance>,
cap_drop: Option<Vec<Sourced<ProtectedString>>>,
cap_drop_origins: Vec<Provenance>,
tmpfs: Option<Vec<Sourced<ProtectedString>>>,
tmpfs_origins: Vec<Provenance>,
sysctls: Option<Vec<Sourced<KernelParameter>>>,
sysctls_origins: Vec<Provenance>,
ulimits: Option<Vec<Sourced<ResourceLimit>>>,
ulimits_origins: Vec<Provenance>,
devices: Option<Vec<Sourced<Device>>>,
devices_origins: Vec<Provenance>,
stop_signal: Option<Sourced<ProtectedString>>,
podman_args: Option<Vec<Sourced<ProtectedString>>>,
podman_args_origins: Vec<Provenance>,
environment: Vec<Sourced<EnvironmentVariable>>,
environment_files: Vec<Sourced<EnvironmentFile>>,
host_mappings: Vec<Sourced<HostMapping>>,
ports: Vec<Sourced<Port>>,
mounts: Vec<Sourced<Mount>>,
config_grants: Vec<Sourced<ResourceGrant>>,
secret_grants: Vec<Sourced<ResourceGrant>>,
networks: Vec<Sourced<NetworkAttachment>>,
dependencies: Vec<Sourced<ServiceDependency>>,
}
impl Service {
#[must_use]
pub const fn new(name: Identifier) -> Self {
Self {
name,
runtime_name: None,
rootfs: None,
image: None,
image_acquisition: None,
image_build: None,
command: None,
startup_notification: None,
entrypoint: None,
run_init: None,
stop_timeout: None,
pull_policy: None,
memory_limit: None,
exposed_ports: None,
exposed_ports_origins: Vec::new(),
restart_policy: None,
healthcheck: None,
labels: Vec::new(),
annotations: None,
annotations_origins: Vec::new(),
logging: None,
reload_action: None,
user: None,
group: None,
user_namespace: None,
supplementary_groups: Vec::new(),
working_directory: None,
read_only_root_filesystem: None,
hostname: None,
dns_servers: None,
dns_servers_origins: Vec::new(),
dns_options: None,
dns_options_origins: Vec::new(),
dns_search_domains: None,
dns_search_domains_origins: Vec::new(),
security_options: None,
security_options_origins: Vec::new(),
pids_limit: None,
shm_size: None,
cap_add: None,
cap_add_origins: Vec::new(),
cap_drop: None,
cap_drop_origins: Vec::new(),
tmpfs: None,
tmpfs_origins: Vec::new(),
sysctls: None,
sysctls_origins: Vec::new(),
ulimits: None,
ulimits_origins: Vec::new(),
devices: None,
devices_origins: Vec::new(),
stop_signal: None,
podman_args: None,
podman_args_origins: Vec::new(),
environment: Vec::new(),
environment_files: Vec::new(),
host_mappings: Vec::new(),
ports: Vec::new(),
mounts: Vec::new(),
config_grants: Vec::new(),
secret_grants: Vec::new(),
networks: Vec::new(),
dependencies: Vec::new(),
}
}
#[must_use]
pub const fn name(&self) -> &Identifier {
&self.name
}
pub fn set_runtime_name(&mut self, name: Sourced<ProtectedString>) {
self.runtime_name = Some(name);
}
#[must_use]
pub const fn runtime_name(&self) -> Option<&Sourced<ProtectedString>> {
self.runtime_name.as_ref()
}
pub fn set_rootfs(&mut self, rootfs: Sourced<ProtectedString>) -> Result<(), ModelError> {
self.ensure_rootfs_is_compatible()?;
self.rootfs = Some(rootfs);
Ok(())
}
#[must_use]
pub const fn rootfs(&self) -> Option<&Sourced<ProtectedString>> {
self.rootfs.as_ref()
}
pub fn set_image(&mut self, image: Sourced<ImageReference>) {
self.image = Some(image);
}
#[must_use]
pub const fn image(&self) -> Option<&Sourced<ImageReference>> {
self.image.as_ref()
}
pub fn set_image_acquisition(&mut self, acquisition: Sourced<Identifier>) {
self.image_acquisition = Some(acquisition);
}
#[must_use]
pub const fn image_acquisition(&self) -> Option<&Sourced<Identifier>> {
self.image_acquisition.as_ref()
}
pub fn set_image_build(&mut self, build: Sourced<Identifier>) {
self.image_build = Some(build);
}
#[must_use]
pub const fn image_build(&self) -> Option<&Sourced<Identifier>> {
self.image_build.as_ref()
}
pub fn set_command(&mut self, command: Sourced<Command>) {
self.command = Some(command);
}
#[must_use]
pub const fn command(&self) -> Option<&Sourced<Command>> {
self.command.as_ref()
}
pub fn set_startup_notification(&mut self, notification: Sourced<StartupNotification>) {
self.startup_notification = Some(notification);
}
#[must_use]
pub const fn startup_notification(&self) -> Option<&Sourced<StartupNotification>> {
self.startup_notification.as_ref()
}
pub fn set_entrypoint(&mut self, entrypoint: Sourced<Entrypoint>) {
self.entrypoint = Some(entrypoint);
}
#[must_use]
pub const fn entrypoint(&self) -> Option<&Sourced<Entrypoint>> {
self.entrypoint.as_ref()
}
pub fn set_run_init(&mut self, run_init: Sourced<bool>) {
self.run_init = Some(run_init);
}
#[must_use]
pub const fn run_init(&self) -> Option<&Sourced<bool>> {
self.run_init.as_ref()
}
pub fn set_stop_timeout(&mut self, timeout: Sourced<StopTimeout>) {
self.stop_timeout = Some(timeout);
}
#[must_use]
pub const fn stop_timeout(&self) -> Option<&Sourced<StopTimeout>> {
self.stop_timeout.as_ref()
}
pub fn set_pull_policy(&mut self, policy: Sourced<PullPolicy>) {
self.pull_policy = Some(policy);
}
#[must_use]
pub const fn pull_policy(&self) -> Option<&Sourced<PullPolicy>> {
self.pull_policy.as_ref()
}
pub fn set_memory_limit(&mut self, limit: Sourced<ProtectedString>) {
self.memory_limit = Some(limit);
}
#[must_use]
pub const fn memory_limit(&self) -> Option<&Sourced<ProtectedString>> {
self.memory_limit.as_ref()
}
pub fn set_exposed_ports(&mut self, ports: Vec<Sourced<ExposedPort>>) {
self.exposed_ports = Some(ports);
self.exposed_ports_origins.clear();
}
pub fn set_exposed_ports_with_origins(&mut self, ports: Vec<Sourced<ExposedPort>>, origins: Vec<Provenance>) {
self.exposed_ports = Some(ports);
self.exposed_ports_origins = origins;
}
pub fn add_exposed_port(&mut self, port: Sourced<ExposedPort>) {
self.exposed_ports.get_or_insert_default().push(port);
}
#[must_use]
pub fn exposed_ports(&self) -> Option<&[Sourced<ExposedPort>]> {
self.exposed_ports.as_deref()
}
#[must_use]
pub fn exposed_ports_origins(&self) -> &[Provenance] {
&self.exposed_ports_origins
}
pub fn set_restart_policy(&mut self, restart_policy: Sourced<RestartPolicy>) {
self.restart_policy = Some(restart_policy);
}
#[must_use]
pub const fn restart_policy(&self) -> Option<&Sourced<RestartPolicy>> {
self.restart_policy.as_ref()
}
pub fn set_healthcheck(&mut self, healthcheck: Sourced<Healthcheck>) {
self.healthcheck = Some(healthcheck);
}
#[must_use]
pub const fn healthcheck(&self) -> Option<&Sourced<Healthcheck>> {
self.healthcheck.as_ref()
}
pub fn add_label(&mut self, label: Sourced<MetadataLabel>) {
self.labels.push(label);
}
#[must_use]
pub fn labels(&self) -> &[Sourced<MetadataLabel>] {
&self.labels
}
pub fn set_annotations(&mut self, annotations: Vec<Sourced<Annotation>>) {
self.annotations = Some(annotations);
self.annotations_origins.clear();
}
pub fn add_annotation(&mut self, annotation: Sourced<Annotation>) {
self.annotations.get_or_insert_default().push(annotation);
}
pub fn set_annotations_with_origins(&mut self, annotations: Vec<Sourced<Annotation>>, origins: Vec<Provenance>) {
self.annotations = Some(annotations);
self.annotations_origins = origins;
}
#[must_use]
pub fn annotations(&self) -> Option<&[Sourced<Annotation>]> {
self.annotations.as_deref()
}
#[must_use]
pub fn annotations_origins(&self) -> &[Provenance] {
&self.annotations_origins
}
pub fn set_logging(&mut self, logging: Sourced<Logging>) {
self.logging = Some(logging);
}
#[must_use]
pub const fn logging(&self) -> Option<&Sourced<Logging>> {
self.logging.as_ref()
}
pub fn set_reload_action(&mut self, reload_action: Sourced<ReloadAction>) {
self.reload_action = Some(reload_action);
}
#[must_use]
pub const fn reload_action(&self) -> Option<&Sourced<ReloadAction>> {
self.reload_action.as_ref()
}
pub fn set_user(&mut self, user: Sourced<ProtectedString>) {
self.user = Some(user);
}
#[must_use]
pub const fn user(&self) -> Option<&Sourced<ProtectedString>> {
self.user.as_ref()
}
pub fn set_group(&mut self, group: Sourced<ProtectedString>) {
self.group = Some(group);
}
#[must_use]
pub const fn group(&self) -> Option<&Sourced<ProtectedString>> {
self.group.as_ref()
}
pub fn set_user_namespace(&mut self, user_namespace: Sourced<ProtectedString>) {
self.user_namespace = Some(user_namespace);
}
#[must_use]
pub const fn user_namespace(&self) -> Option<&Sourced<ProtectedString>> {
self.user_namespace.as_ref()
}
pub fn add_supplementary_group(&mut self, group: Sourced<ProtectedString>) {
self.supplementary_groups.push(group);
}
#[must_use]
pub fn supplementary_groups(&self) -> &[Sourced<ProtectedString>] {
&self.supplementary_groups
}
pub fn set_working_directory(&mut self, working_directory: Sourced<ProtectedString>) {
self.working_directory = Some(working_directory);
}
#[must_use]
pub const fn working_directory(&self) -> Option<&Sourced<ProtectedString>> {
self.working_directory.as_ref()
}
pub fn set_read_only_root_filesystem(&mut self, read_only: Sourced<bool>) {
self.read_only_root_filesystem = Some(read_only);
}
#[must_use]
pub const fn read_only_root_filesystem(&self) -> Option<&Sourced<bool>> {
self.read_only_root_filesystem.as_ref()
}
pub fn set_hostname(&mut self, hostname: Sourced<ProtectedString>) {
self.hostname = Some(hostname);
}
#[must_use]
pub const fn hostname(&self) -> Option<&Sourced<ProtectedString>> {
self.hostname.as_ref()
}
pub fn set_dns_servers_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
self.dns_servers = Some(values);
self.dns_servers_origins = origins;
}
pub fn set_dns_servers(&mut self, values: Vec<Sourced<ProtectedString>>) {
self.set_dns_servers_with_origins(values, Vec::new());
}
#[must_use]
pub fn dns_servers(&self) -> Option<&[Sourced<ProtectedString>]> {
self.dns_servers.as_deref()
}
#[must_use]
pub fn dns_servers_origins(&self) -> &[Provenance] {
&self.dns_servers_origins
}
pub fn set_dns_options_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
self.dns_options = Some(values);
self.dns_options_origins = origins;
}
pub fn set_dns_options(&mut self, values: Vec<Sourced<ProtectedString>>) {
self.set_dns_options_with_origins(values, Vec::new());
}
#[must_use]
pub fn dns_options(&self) -> Option<&[Sourced<ProtectedString>]> {
self.dns_options.as_deref()
}
#[must_use]
pub fn dns_options_origins(&self) -> &[Provenance] {
&self.dns_options_origins
}
pub fn set_dns_search_domains_with_origins(
&mut self,
values: Vec<Sourced<ProtectedString>>,
origins: Vec<Provenance>,
) {
self.dns_search_domains = Some(values);
self.dns_search_domains_origins = origins;
}
pub fn set_dns_search_domains(&mut self, values: Vec<Sourced<ProtectedString>>) {
self.set_dns_search_domains_with_origins(values, Vec::new());
}
#[must_use]
pub fn dns_search_domains(&self) -> Option<&[Sourced<ProtectedString>]> {
self.dns_search_domains.as_deref()
}
#[must_use]
pub fn dns_search_domains_origins(&self) -> &[Provenance] {
&self.dns_search_domains_origins
}
pub fn set_security_options_with_origins(
&mut self,
values: Vec<Sourced<SecurityOption>>,
origins: Vec<Provenance>,
) {
self.security_options = Some(values);
self.security_options_origins = origins;
}
pub fn set_security_options(&mut self, values: Vec<Sourced<SecurityOption>>) {
self.set_security_options_with_origins(values, Vec::new());
}
#[must_use]
pub fn security_options(&self) -> Option<&[Sourced<SecurityOption>]> {
self.security_options.as_deref()
}
#[must_use]
pub fn security_options_origins(&self) -> &[Provenance] {
&self.security_options_origins
}
pub fn set_pids_limit(&mut self, limit: Sourced<ProtectedString>) {
self.pids_limit = Some(limit);
}
#[must_use]
pub const fn pids_limit(&self) -> Option<&Sourced<ProtectedString>> {
self.pids_limit.as_ref()
}
pub fn set_shm_size(&mut self, size: Sourced<ProtectedString>) {
self.shm_size = Some(size);
}
#[must_use]
pub const fn shm_size(&self) -> Option<&Sourced<ProtectedString>> {
self.shm_size.as_ref()
}
pub fn set_cap_add(&mut self, values: Vec<Sourced<ProtectedString>>) {
self.cap_add = Some(values);
self.cap_add_origins.clear();
}
pub fn set_cap_add_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
self.cap_add = Some(values);
self.cap_add_origins = origins;
}
#[must_use]
pub fn cap_add(&self) -> Option<&[Sourced<ProtectedString>]> {
self.cap_add.as_deref()
}
#[must_use]
pub fn cap_add_origins(&self) -> &[Provenance] {
&self.cap_add_origins
}
pub fn set_cap_drop(&mut self, values: Vec<Sourced<ProtectedString>>) {
self.cap_drop = Some(values);
self.cap_drop_origins.clear();
}
pub fn set_cap_drop_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
self.cap_drop = Some(values);
self.cap_drop_origins = origins;
}
#[must_use]
pub fn cap_drop(&self) -> Option<&[Sourced<ProtectedString>]> {
self.cap_drop.as_deref()
}
#[must_use]
pub fn cap_drop_origins(&self) -> &[Provenance] {
&self.cap_drop_origins
}
pub fn set_tmpfs(&mut self, values: Vec<Sourced<ProtectedString>>) {
self.tmpfs = Some(values);
self.tmpfs_origins.clear();
}
pub fn set_tmpfs_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
self.tmpfs = Some(values);
self.tmpfs_origins = origins;
}
#[must_use]
pub fn tmpfs(&self) -> Option<&[Sourced<ProtectedString>]> {
self.tmpfs.as_deref()
}
#[must_use]
pub fn tmpfs_origins(&self) -> &[Provenance] {
&self.tmpfs_origins
}
pub fn set_sysctls(&mut self, values: Vec<Sourced<KernelParameter>>) {
self.sysctls = Some(values);
self.sysctls_origins.clear();
}
pub fn set_sysctls_with_origins(&mut self, values: Vec<Sourced<KernelParameter>>, origins: Vec<Provenance>) {
self.sysctls = Some(values);
self.sysctls_origins = origins;
}
#[must_use]
pub fn sysctls(&self) -> Option<&[Sourced<KernelParameter>]> {
self.sysctls.as_deref()
}
#[must_use]
pub fn sysctls_origins(&self) -> &[Provenance] {
&self.sysctls_origins
}
pub fn set_ulimits(&mut self, values: Vec<Sourced<ResourceLimit>>) {
self.ulimits = Some(values);
self.ulimits_origins.clear();
}
pub fn set_ulimits_with_origins(&mut self, values: Vec<Sourced<ResourceLimit>>, origins: Vec<Provenance>) {
self.ulimits = Some(values);
self.ulimits_origins = origins;
}
#[must_use]
pub fn ulimits(&self) -> Option<&[Sourced<ResourceLimit>]> {
self.ulimits.as_deref()
}
#[must_use]
pub fn ulimits_origins(&self) -> &[Provenance] {
&self.ulimits_origins
}
pub fn set_devices(&mut self, values: Vec<Sourced<Device>>) {
self.devices = Some(values);
self.devices_origins.clear();
}
pub fn set_devices_with_origins(&mut self, values: Vec<Sourced<Device>>, origins: Vec<Provenance>) {
self.devices = Some(values);
self.devices_origins = origins;
}
#[must_use]
pub fn devices(&self) -> Option<&[Sourced<Device>]> {
self.devices.as_deref()
}
#[must_use]
pub fn devices_origins(&self) -> &[Provenance] {
&self.devices_origins
}
pub fn set_stop_signal(&mut self, signal: Sourced<ProtectedString>) {
self.stop_signal = Some(signal);
}
#[must_use]
pub const fn stop_signal(&self) -> Option<&Sourced<ProtectedString>> {
self.stop_signal.as_ref()
}
pub fn set_podman_args(&mut self, values: Vec<Sourced<ProtectedString>>) {
self.set_podman_args_with_origins(values, Vec::new());
}
pub fn set_podman_args_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
self.podman_args = Some(values);
self.podman_args_origins = origins;
}
pub fn add_podman_arg(&mut self, value: Sourced<ProtectedString>) {
self.podman_args.get_or_insert_default().push(value);
}
#[must_use]
pub fn podman_args(&self) -> Option<&[Sourced<ProtectedString>]> {
self.podman_args.as_deref()
}
#[must_use]
pub fn podman_args_origins(&self) -> &[Provenance] {
&self.podman_args_origins
}
pub fn add_environment(&mut self, value: Sourced<EnvironmentVariable>) {
self.environment.push(value);
}
#[must_use]
pub fn environment(&self) -> &[Sourced<EnvironmentVariable>] {
&self.environment
}
pub fn add_environment_file(&mut self, value: Sourced<EnvironmentFile>) {
self.environment_files.push(value);
}
#[must_use]
pub fn environment_files(&self) -> &[Sourced<EnvironmentFile>] {
&self.environment_files
}
pub fn add_host_mapping(&mut self, value: Sourced<HostMapping>) {
self.host_mappings.push(value);
}
#[must_use]
pub fn host_mappings(&self) -> &[Sourced<HostMapping>] {
&self.host_mappings
}
pub fn add_port(&mut self, value: Sourced<Port>) {
self.ports.push(value);
}
#[must_use]
pub fn ports(&self) -> &[Sourced<Port>] {
&self.ports
}
pub fn add_mount(&mut self, value: Sourced<Mount>) {
self.mounts.push(value);
}
#[must_use]
pub fn mounts(&self) -> &[Sourced<Mount>] {
&self.mounts
}
pub fn add_config_grant(&mut self, value: Sourced<ResourceGrant>) {
self.config_grants.push(value);
}
#[must_use]
pub fn config_grants(&self) -> &[Sourced<ResourceGrant>] {
&self.config_grants
}
pub fn add_secret_grant(&mut self, value: Sourced<ResourceGrant>) {
self.secret_grants.push(value);
}
#[must_use]
pub fn secret_grants(&self) -> &[Sourced<ResourceGrant>] {
&self.secret_grants
}
pub fn add_network(&mut self, value: Sourced<NetworkAttachment>) {
self.networks.push(value);
}
pub fn replace_network(
&mut self,
index: usize,
value: Sourced<NetworkAttachment>,
) -> Result<Sourced<NetworkAttachment>, ModelError> {
let len = self.networks.len();
let Some(slot) = self.networks.get_mut(index) else {
return Err(ModelError::UnknownNetworkAttachmentIndex { index, len });
};
Ok(std::mem::replace(slot, value))
}
#[must_use]
pub fn networks(&self) -> &[Sourced<NetworkAttachment>] {
&self.networks
}
pub fn add_dependency(&mut self, value: Sourced<ServiceDependency>) {
self.dependencies.push(value);
}
#[must_use]
pub fn dependencies(&self) -> &[Sourced<ServiceDependency>] {
&self.dependencies
}
pub fn validate_image_source_exclusivity(&self) -> Result<(), ModelError> {
if self.rootfs.is_some() {
self.ensure_rootfs_is_compatible()?;
}
Ok(())
}
fn ensure_rootfs_is_compatible(&self) -> Result<(), ModelError> {
let source = if self.image.is_some() {
Some("image")
} else if self.image_acquisition.is_some() {
Some("image acquisition")
} else if self.image_build.is_some() {
Some("image build")
} else {
None
};
if let Some(source) = source {
return Err(ModelError::RootfsImageSourceConflict {
service: self.name.as_str().to_owned(),
source,
});
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Application {
name: Identifier,
image_acquisitions: Vec<Sourced<ImageAcquisition>>,
image_builds: Vec<Sourced<ImageBuild>>,
services: Vec<Sourced<Service>>,
service_groups: Vec<Sourced<ServiceGroup>>,
volumes: Vec<Sourced<Volume>>,
networks: Vec<Sourced<Network>>,
configs: Vec<Sourced<Config>>,
secrets: Vec<Sourced<Secret>>,
}
impl Application {
#[must_use]
pub const fn new(name: Identifier) -> Self {
Self {
name,
image_acquisitions: Vec::new(),
image_builds: Vec::new(),
services: Vec::new(),
service_groups: Vec::new(),
volumes: Vec::new(),
networks: Vec::new(),
configs: Vec::new(),
secrets: Vec::new(),
}
}
#[must_use]
pub const fn name(&self) -> &Identifier {
&self.name
}
pub fn add_image_acquisition(&mut self, acquisition: Sourced<ImageAcquisition>) -> Result<(), ModelError> {
ensure_unique(
"image acquisition",
acquisition.value().name(),
self.image_acquisitions.iter().map(|candidate| candidate.value().name()),
)?;
self.image_acquisitions.push(acquisition);
Ok(())
}
#[must_use]
pub fn image_acquisitions(&self) -> &[Sourced<ImageAcquisition>] {
&self.image_acquisitions
}
pub fn add_image_build(&mut self, build: Sourced<ImageBuild>) -> Result<(), ModelError> {
ensure_unique(
"image build",
build.value().name(),
self.image_builds.iter().map(|candidate| candidate.value().name()),
)?;
self.image_builds.push(build);
Ok(())
}
#[must_use]
pub fn image_builds(&self) -> &[Sourced<ImageBuild>] {
&self.image_builds
}
pub fn validate_image_artifact_references(&self) -> Result<(), ModelError> {
for service in &self.services {
if let Some(acquisition) = service.value().image_acquisition() {
if !self.contains_image_acquisition(acquisition.value()) {
return Err(ModelError::UnknownImageAcquisitionReference {
service: service.value().name().as_str().to_owned(),
acquisition: acquisition.value().as_str().to_owned(),
});
}
}
if let Some(build) = service.value().image_build() {
if !self.contains_image_build(build.value()) {
return Err(ModelError::UnknownImageBuildReference {
service: service.value().name().as_str().to_owned(),
build: build.value().as_str().to_owned(),
});
}
}
}
for volume in &self.volumes {
let Some(source) = volume.value().image_source() else {
continue;
};
match source.value() {
VolumeImageSource::Literal(_) => {}
VolumeImageSource::ImageAcquisition(acquisition) => {
if !self.contains_image_acquisition(acquisition) {
return Err(ModelError::UnknownVolumeImageAcquisitionReference {
volume: volume.value().name().as_str().to_owned(),
acquisition: acquisition.as_str().to_owned(),
});
}
}
VolumeImageSource::ImageBuild(build) => {
if !self.contains_image_build(build) {
return Err(ModelError::UnknownVolumeImageBuildReference {
volume: volume.value().name().as_str().to_owned(),
build: build.as_str().to_owned(),
});
}
}
}
}
Ok(())
}
pub fn validate_image_artifact_dependencies(
&self,
dependencies: &[Sourced<ArtifactDependency>],
) -> Result<(), ModelError> {
self.validate_image_artifact_references()?;
let mut graph = BTreeMap::<ArtifactDependencyNode, BTreeSet<ArtifactDependencyNode>>::new();
for dependency in dependencies {
let source = dependency.value().source().value();
let target = dependency.value().target().value();
self.validate_artifact_dependency_node(source)?;
self.validate_artifact_dependency_node(target)?;
graph.entry(source.clone()).or_default().insert(target.clone());
graph.entry(target.clone()).or_default();
}
let mut state = BTreeMap::<ArtifactDependencyNode, VisitState>::new();
let mut path = Vec::new();
for node in graph.keys() {
if state.get(node).is_some_and(|state| *state == VisitState::Finished) {
continue;
}
if let Some(cycle) = detect_artifact_cycle(node, &graph, &mut state, &mut path) {
return Err(ModelError::ImageArtifactDependencyCycle {
nodes: cycle.into_iter().map(|node| node.display_name()).collect(),
});
}
}
Ok(())
}
fn contains_image_acquisition(&self, name: &Identifier) -> bool {
self.image_acquisitions
.iter()
.any(|candidate| candidate.value().name() == name)
}
fn contains_image_build(&self, name: &Identifier) -> bool {
self.image_builds
.iter()
.any(|candidate| candidate.value().name() == name)
}
fn validate_artifact_dependency_node(&self, node: &ArtifactDependencyNode) -> Result<(), ModelError> {
let (kind, name) = node.kind_and_name();
let exists = match node {
ArtifactDependencyNode::Volume(_) => self.volumes.iter().any(|volume| volume.value().name() == name),
ArtifactDependencyNode::ImageAcquisition(_) => self.contains_image_acquisition(name),
ArtifactDependencyNode::ImageBuild(_) => self.contains_image_build(name),
};
if exists {
Ok(())
} else {
Err(ModelError::UnknownArtifactDependencyNode {
kind,
name: name.as_str().to_owned(),
})
}
}
pub fn add_service(&mut self, service: Sourced<Service>) -> Result<(), ModelError> {
ensure_unique(
"service",
service.value().name(),
self.services.iter().map(|candidate| candidate.value().name()),
)?;
service.value().validate_image_source_exclusivity()?;
if let Some(acquisition) = service.value().image_acquisition() {
if !self
.image_acquisitions
.iter()
.any(|candidate| candidate.value().name() == acquisition.value())
{
return Err(ModelError::UnknownImageAcquisitionReference {
service: service.value().name().as_str().to_owned(),
acquisition: acquisition.value().as_str().to_owned(),
});
}
}
if let Some(build) = service.value().image_build() {
if !self
.image_builds
.iter()
.any(|candidate| candidate.value().name() == build.value())
{
return Err(ModelError::UnknownImageBuildReference {
service: service.value().name().as_str().to_owned(),
build: build.value().as_str().to_owned(),
});
}
}
self.services.push(service);
Ok(())
}
#[must_use]
pub fn services(&self) -> &[Sourced<Service>] {
&self.services
}
pub fn add_service_group(&mut self, group: Sourced<ServiceGroup>) -> Result<(), ModelError> {
ensure_unique(
"service group",
group.value().name(),
self.service_groups.iter().map(|candidate| candidate.value().name()),
)?;
for member in group.value().members() {
if !self
.services
.iter()
.any(|service| service.value().name() == member.value())
{
return Err(ModelError::UnknownServiceGroupMember {
group: group.value().name().as_str().to_owned(),
service: member.value().as_str().to_owned(),
});
}
if let Some(existing) = self.service_groups.iter().find(|candidate| {
candidate
.value()
.members()
.iter()
.any(|candidate_member| candidate_member.value() == member.value())
}) {
return Err(ModelError::ServiceInMultipleGroups {
service: member.value().as_str().to_owned(),
existing: existing.value().name().as_str().to_owned(),
replacement: group.value().name().as_str().to_owned(),
});
}
}
self.service_groups.push(group);
Ok(())
}
#[must_use]
pub fn service_groups(&self) -> &[Sourced<ServiceGroup>] {
&self.service_groups
}
pub fn add_volume(&mut self, volume: Sourced<Volume>) -> Result<(), ModelError> {
ensure_unique(
"volume",
volume.value().name(),
self.volumes.iter().map(|candidate| candidate.value().name()),
)?;
self.volumes.push(volume);
Ok(())
}
#[must_use]
pub fn volumes(&self) -> &[Sourced<Volume>] {
&self.volumes
}
pub fn add_network(&mut self, network: Sourced<Network>) -> Result<(), ModelError> {
ensure_unique(
"network",
network.value().name(),
self.networks.iter().map(|candidate| candidate.value().name()),
)?;
self.networks.push(network);
Ok(())
}
#[must_use]
pub fn networks(&self) -> &[Sourced<Network>] {
&self.networks
}
pub fn add_config(&mut self, config: Sourced<Config>) -> Result<(), ModelError> {
ensure_unique(
"config",
config.value().name(),
self.configs.iter().map(|candidate| candidate.value().name()),
)?;
self.configs.push(config);
Ok(())
}
#[must_use]
pub fn configs(&self) -> &[Sourced<Config>] {
&self.configs
}
pub fn add_secret(&mut self, secret: Sourced<Secret>) -> Result<(), ModelError> {
ensure_unique(
"secret",
secret.value().name(),
self.secrets.iter().map(|candidate| candidate.value().name()),
)?;
self.secrets.push(secret);
Ok(())
}
#[must_use]
pub fn secrets(&self) -> &[Sourced<Secret>] {
&self.secrets
}
}
#[derive(Clone, Copy, Eq, PartialEq)]
enum VisitState {
Visiting,
Finished,
}
fn detect_artifact_cycle(
node: &ArtifactDependencyNode,
graph: &BTreeMap<ArtifactDependencyNode, BTreeSet<ArtifactDependencyNode>>,
state: &mut BTreeMap<ArtifactDependencyNode, VisitState>,
path: &mut Vec<ArtifactDependencyNode>,
) -> Option<Vec<ArtifactDependencyNode>> {
if state.get(node).is_some_and(|state| *state == VisitState::Visiting) {
let index = path.iter().position(|candidate| candidate == node)?;
let mut cycle = path[index..].to_vec();
cycle.push(node.clone());
return Some(cycle);
}
if state.get(node).is_some_and(|state| *state == VisitState::Finished) {
return None;
}
state.insert(node.clone(), VisitState::Visiting);
path.push(node.clone());
if let Some(targets) = graph.get(node) {
for target in targets {
if let Some(cycle) = detect_artifact_cycle(target, graph, state, path) {
return Some(cycle);
}
}
}
path.pop();
state.insert(node.clone(), VisitState::Finished);
None
}
fn ensure_unique<'a>(
kind: &'static str,
name: &Identifier,
existing: impl Iterator<Item = &'a Identifier>,
) -> Result<(), ModelError> {
if existing.into_iter().any(|candidate| candidate == name) {
return Err(ModelError::DuplicateResource {
kind,
name: name.as_str().to_owned(),
});
}
Ok(())
}
fn validate_text(kind: &'static str, value: &str) -> Result<(), ModelError> {
if value.is_empty() {
return Err(ModelError::EmptyValue(kind));
}
validate_no_nul(kind, value)
}
fn validate_no_nul(kind: &'static str, value: &str) -> Result<(), ModelError> {
if value.contains('\0') {
return Err(ModelError::ContainsNul(kind));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{
Annotation, Application, ArtifactDependency, ArtifactDependencyNode, Command, Config, ConfigMaterial, Device,
Entrypoint, EnvironmentFile, EnvironmentFileFormat, EnvironmentFileSyntax, ExposedPort, GroupExitPolicy,
HealthcheckDuration, HealthcheckRetries, HostAddress, HostAddressKind, HostMapping, Identifier,
KernelParameter, Logging, LoggingOption, MetadataLabel, ModelError, Mount, MountSource, Network,
NetworkAttachment, NetworkDriverOption, NetworkIpamConfig, Protocol, PullPolicy, ReloadAction, ResourceGrant,
ResourceGrantSyntax, ResourceLimit, ResourceOwnership, RestartPolicy, Secret, SecretMaterial, SecurityOption,
Service, ServiceDependency, ServiceDependencyCondition, ServiceGroup, ServiceGroupRuntime, StartupNotification,
StopTimeout, Volume, VolumeImageSource,
};
use crate::{ImageAcquisition, ImageBuild, ImageReference, ProtectedString, Sourced};
#[test]
fn preserves_service_order_and_rejects_duplicate_names() -> Result<(), String> {
let mut application = Application::new(id("example")?);
application
.add_service(Sourced::generated(Service::new(id("web")?)))
.map_err(|error| error.to_string())?;
application
.add_service(Sourced::generated(Service::new(id("database")?)))
.map_err(|error| error.to_string())?;
let names: Vec<_> = application
.services()
.iter()
.map(|service| service.value().name().as_str())
.collect();
assert_eq!(names, ["web", "database"]);
let duplicate = application.add_service(Sourced::generated(Service::new(id("web")?)));
assert!(matches!(duplicate, Err(ModelError::DuplicateResource { .. })));
Ok(())
}
#[test]
fn keeps_the_service_key_and_explicit_runtime_name_distinct() -> Result<(), String> {
let mut service = Service::new(id("web")?);
service.set_runtime_name(Sourced::generated(ProtectedString::plain("production-web")));
assert_eq!(service.name().as_str(), "web");
assert_eq!(
service.runtime_name().map(|name| name.value().expose()),
Some("production-web")
);
Ok(())
}
#[test]
fn network_keeps_logical_and_runtime_names_and_literal_flags_distinct() -> Result<(), String> {
let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
let origin = crate::Provenance::source(source);
let mut network = Network::new(id("frontend")?, ResourceOwnership::Application);
network.set_runtime_name(Sourced::from_source(
ProtectedString::plain("production-frontend"),
origin.clone(),
));
network.set_driver(Sourced::from_source(ProtectedString::plain("bridge"), origin.clone()));
network.set_internal(Sourced::from_source(true, origin.clone()));
network.set_ipv6(Sourced::from_source(false, origin.clone()));
network.set_ipam_driver(Sourced::from_source(ProtectedString::plain("default"), origin));
assert_eq!(network.name().as_str(), "frontend");
assert_eq!(
network.runtime_name().map(|value| value.value().expose()),
Some("production-frontend")
);
assert_eq!(network.driver().map(|value| value.value().expose()), Some("bridge"));
assert_eq!(network.internal().map(Sourced::value), Some(&true));
assert_eq!(network.ipv6().map(Sourced::value), Some(&false));
assert_eq!(
network.ipam_driver().map(|value| value.value().expose()),
Some("default")
);
Ok(())
}
#[test]
fn network_collections_retain_resets_provenance_and_redact_protected_values() -> Result<(), String> {
let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
let origin = crate::Provenance::source(source);
let mut network = Network::new(id("frontend")?, ResourceOwnership::Application);
let option = NetworkDriverOption::new(
Sourced::from_source(id("com.example.token")?, origin.clone()),
Sourced::from_source(ProtectedString::sensitive("never-print-this"), origin.clone()),
)
.map_err(|error| error.to_string())?;
let label = MetadataLabel::new(id("com.example.label")?, ProtectedString::sensitive("also-private"));
network
.set_driver_options_with_origins(vec![Sourced::from_source(option, origin.clone())], vec![origin.clone()]);
network.set_labels_with_origins(vec![Sourced::from_source(label, origin.clone())], vec![origin.clone()]);
network.set_ipam_configs_with_origins(Vec::new(), vec![origin]);
assert_eq!(network.driver_options().map(<[_]>::len), Some(1));
assert_eq!(network.labels().map(<[_]>::len), Some(1));
assert_eq!(network.ipam_configs().map(<[_]>::len), Some(0));
assert_eq!(network.driver_options_origins().len(), 1);
assert_eq!(network.labels_origins().len(), 1);
assert_eq!(network.ipam_configs_origins().len(), 1);
let debug = format!("{network:?}");
assert!(!debug.contains("never-print-this"));
assert!(!debug.contains("also-private"));
assert!(debug.contains("[REDACTED]"));
network.set_driver_options(Vec::new());
network.set_labels(Vec::new());
network.set_ipam_configs(Vec::new());
assert_eq!(network.driver_options().map(<[_]>::len), Some(0));
assert_eq!(network.labels().map(<[_]>::len), Some(0));
assert_eq!(network.ipam_configs().map(<[_]>::len), Some(0));
assert!(network.driver_options_origins().is_empty());
assert!(network.labels_origins().is_empty());
assert!(network.ipam_configs_origins().is_empty());
Ok(())
}
#[test]
fn network_ipam_rows_preserve_association_order_and_reject_subnetless_values() -> Result<(), String> {
let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
let origin = crate::Provenance::source(source);
let mut first = NetworkIpamConfig::new(Sourced::from_source(
ProtectedString::plain("10.10.0.0/24"),
origin.clone(),
))
.map_err(|error| error.to_string())?;
first
.set_gateway(Sourced::from_source(
ProtectedString::plain("10.10.0.1"),
origin.clone(),
))
.map_err(|error| error.to_string())?;
let mut second = NetworkIpamConfig::new(Sourced::from_source(
ProtectedString::plain("fd00:10::/64"),
origin.clone(),
))
.map_err(|error| error.to_string())?;
second
.set_ip_range(Sourced::from_source(
ProtectedString::plain("fd00:10::100/120"),
origin.clone(),
))
.map_err(|error| error.to_string())?;
let mut network = Network::new(id("frontend")?, ResourceOwnership::Application);
network.set_ipam_configs_with_origins(
vec![
Sourced::from_source(first, origin.clone()),
Sourced::from_source(second, origin),
],
Vec::new(),
);
let rows = network
.ipam_configs()
.ok_or_else(|| "IPAM configs were omitted".to_owned())?;
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].value().subnet().value().expose(), "10.10.0.0/24");
assert_eq!(
rows[0].value().gateway().map(|value| value.value().expose()),
Some("10.10.0.1")
);
assert_eq!(rows[0].value().ip_range(), None);
assert_eq!(rows[1].value().subnet().value().expose(), "fd00:10::/64");
assert_eq!(rows[1].value().gateway(), None);
assert_eq!(
rows[1].value().ip_range().map(|value| value.value().expose()),
Some("fd00:10::100/120")
);
assert!(matches!(
NetworkIpamConfig::new(Sourced::generated(ProtectedString::plain(""))),
Err(ModelError::EmptyValue("network IPAM subnet"))
));
assert!(matches!(
NetworkIpamConfig::new(Sourced::generated(ProtectedString::plain("10.0.0.0/24\0bad"))),
Err(ModelError::ContainsNul("network IPAM subnet"))
));
assert!(matches!(
NetworkDriverOption::new(
Sourced::generated(id("option")?),
Sourced::generated(ProtectedString::plain("bad\0value")),
),
Err(ModelError::ContainsNul("network driver option value"))
));
Ok(())
}
#[test]
fn image_artifact_resources_are_ordered_unique_and_referenced_explicitly() -> Result<(), String> {
let mut application = Application::new(id("example")?);
application
.add_image_acquisition(Sourced::generated(ImageAcquisition::new(id("base-image")?)))
.map_err(|error| error.to_string())?;
application
.add_image_build(Sourced::generated(ImageBuild::new(id("web-build")?)))
.map_err(|error| error.to_string())?;
let mut web = Service::new(id("web")?);
web.set_image_acquisition(Sourced::generated(id("base-image")?));
web.set_image_build(Sourced::generated(id("web-build")?));
application
.add_service(Sourced::generated(web))
.map_err(|error| error.to_string())?;
assert_eq!(
application.image_acquisitions()[0].value().name().as_str(),
"base-image"
);
assert_eq!(application.image_builds()[0].value().name().as_str(), "web-build");
assert!(matches!(
application.add_image_build(Sourced::generated(ImageBuild::new(id("web-build")?))),
Err(ModelError::DuplicateResource {
kind: "image build",
..
})
));
let mut missing = Service::new(id("missing")?);
missing.set_image_build(Sourced::generated(id("absent-build")?));
assert!(matches!(
application.add_service(Sourced::generated(missing)),
Err(ModelError::UnknownImageBuildReference { .. })
));
Ok(())
}
#[test]
fn volume_keeps_logical_runtime_and_service_names_and_local_fields_distinct() -> Result<(), String> {
let origin = crate::Provenance::source(crate::SourceId::new("data.volume").map_err(|error| error.to_string())?);
let mut volume = Volume::new(id("data")?, ResourceOwnership::Application);
volume.set_runtime_name(Sourced::from_source(
ProtectedString::plain("production-data"),
origin.clone(),
));
volume.set_service_name(Sourced::from_source(
ProtectedString::plain("data-volume.service"),
origin.clone(),
));
volume.set_driver(Sourced::from_source(ProtectedString::plain("local"), origin.clone()));
volume.set_device(Sourced::from_source(
ProtectedString::plain("/srv/data"),
origin.clone(),
));
volume.set_volume_type(Sourced::from_source(ProtectedString::plain("none"), origin.clone()));
volume.set_options(Sourced::from_source(ProtectedString::plain("bind"), origin.clone()));
assert_eq!(volume.name().as_str(), "data");
assert_eq!(
volume.runtime_name().map(|name| name.value().expose()),
Some("production-data")
);
assert_eq!(
volume.service_name().map(|name| name.value().expose()),
Some("data-volume.service")
);
assert_eq!(volume.driver().map(|value| value.value().expose()), Some("local"));
assert_eq!(volume.device().map(|value| value.value().expose()), Some("/srv/data"));
assert_eq!(volume.volume_type().map(|value| value.value().expose()), Some("none"));
assert_eq!(volume.options().map(|value| value.value().expose()), Some("bind"));
assert_eq!(
volume.options().map(Sourced::origins),
Some(std::slice::from_ref(&origin))
);
Ok(())
}
#[test]
fn volume_preserves_resets_order_protected_values_and_identity_dimensions() -> Result<(), String> {
let origin = crate::Provenance::source(crate::SourceId::new("data.volume").map_err(|error| error.to_string())?);
let mut volume = Volume::new(id("data")?, ResourceOwnership::Application);
assert!(volume.labels().is_none());
assert!(volume.containers_conf_modules().is_none());
assert!(volume.global_args().is_none());
assert!(volume.podman_args().is_none());
volume.set_labels_with_origins(Vec::new(), vec![origin.clone()]);
volume.set_containers_conf_modules_with_origins(Vec::new(), vec![origin.clone()]);
volume.set_global_args_with_origins(
vec![
Sourced::from_source(ProtectedString::plain("--first"), origin.clone()),
Sourced::from_source(ProtectedString::sensitive("--token=never-print"), origin.clone()),
],
vec![origin.clone()],
);
volume.set_podman_args_with_origins(
vec![
Sourced::from_source(ProtectedString::plain("--replace"), origin.clone()),
Sourced::from_source(ProtectedString::sensitive("--secret=never-print"), origin.clone()),
],
vec![origin.clone()],
);
volume.set_user(Sourced::from_source(
ProtectedString::plain("named-user"),
origin.clone(),
));
volume.set_group(Sourced::from_source(
ProtectedString::plain("named-group"),
origin.clone(),
));
volume.set_uid(Sourced::from_source(ProtectedString::plain("1001"), origin.clone()));
volume.set_gid(Sourced::from_source(ProtectedString::plain("1002"), origin));
assert_eq!(volume.labels().map(<[_]>::len), Some(0));
assert_eq!(volume.containers_conf_modules().map(<[_]>::len), Some(0));
assert_eq!(volume.global_args().map(<[_]>::len), Some(2));
assert_eq!(volume.podman_args().map(<[_]>::len), Some(2));
assert_eq!(volume.user().map(|value| value.value().expose()), Some("named-user"));
assert_eq!(volume.group().map(|value| value.value().expose()), Some("named-group"));
assert_eq!(volume.uid().map(|value| value.value().expose()), Some("1001"));
assert_eq!(volume.gid().map(|value| value.value().expose()), Some("1002"));
let debug = format!("{volume:?}");
assert!(!debug.contains("never-print"));
assert!(debug.contains("[REDACTED]"));
Ok(())
}
#[test]
fn volume_copy_and_image_sources_preserve_absence_and_typed_distinctions() -> Result<(), String> {
let origin =
crate::Provenance::source(crate::SourceId::new("cache.volume").map_err(|error| error.to_string())?);
let mut volume = Volume::new(id("cache")?, ResourceOwnership::Application);
assert_eq!(volume.copy(), None);
volume.set_copy(Sourced::from_source(false, origin.clone()));
assert_eq!(volume.copy().map(Sourced::value), Some(&false));
volume.set_copy(Sourced::from_source(true, origin.clone()));
assert_eq!(volume.copy().map(Sourced::value), Some(&true));
volume
.set_image_source(Sourced::from_source(
VolumeImageSource::Literal(ProtectedString::sensitive("registry.example/private:1")),
origin.clone(),
))
.map_err(|error| error.to_string())?;
assert!(matches!(
volume.image_source().map(Sourced::value),
Some(VolumeImageSource::Literal(_))
));
assert!(!format!("{volume:?}").contains("registry.example/private:1"));
volume
.set_image_source(Sourced::from_source(
VolumeImageSource::ImageAcquisition(id("cache-image")?),
origin.clone(),
))
.map_err(|error| error.to_string())?;
assert!(matches!(
volume.image_source().map(Sourced::value),
Some(VolumeImageSource::ImageAcquisition(name)) if name.as_str() == "cache-image"
));
volume
.set_image_source(Sourced::from_source(
VolumeImageSource::ImageBuild(id("cache-build")?),
origin,
))
.map_err(|error| error.to_string())?;
assert!(matches!(
volume.image_source().map(Sourced::value),
Some(VolumeImageSource::ImageBuild(name)) if name.as_str() == "cache-build"
));
Ok(())
}
#[test]
fn volume_artifact_validation_is_deferred_and_explicit_edges_find_cycles() -> Result<(), String> {
let mut application = Application::new(id("example")?);
let mut volume = Volume::new(id("cache")?, ResourceOwnership::Application);
volume
.set_image_source(Sourced::generated(VolumeImageSource::ImageBuild(id("cache-build")?)))
.map_err(|error| error.to_string())?;
application
.add_volume(Sourced::generated(volume))
.map_err(|error| error.to_string())?;
assert!(matches!(
application.validate_image_artifact_references(),
Err(ModelError::UnknownVolumeImageBuildReference { .. })
));
application
.add_image_build(Sourced::generated(ImageBuild::new(id("cache-build")?)))
.map_err(|error| error.to_string())?;
application
.validate_image_artifact_references()
.map_err(|error| error.to_string())?;
let volume_node = ArtifactDependencyNode::Volume(id("cache")?);
let build_node = ArtifactDependencyNode::ImageBuild(id("cache-build")?);
let dependencies = vec![
Sourced::generated(ArtifactDependency::new(
Sourced::generated(volume_node.clone()),
Sourced::generated(build_node.clone()),
)),
Sourced::generated(ArtifactDependency::new(
Sourced::generated(build_node),
Sourced::generated(volume_node),
)),
];
assert!(matches!(
application.validate_image_artifact_dependencies(&dependencies),
Err(ModelError::ImageArtifactDependencyCycle { .. })
));
let missing = vec![Sourced::generated(ArtifactDependency::new(
Sourced::generated(ArtifactDependencyNode::ImageBuild(id("cache-build")?)),
Sourced::generated(ArtifactDependencyNode::Volume(id("missing")?)),
))];
assert!(matches!(
application.validate_image_artifact_dependencies(&missing),
Err(ModelError::UnknownArtifactDependencyNode { kind: "volume", .. })
));
Ok(())
}
#[test]
fn volume_rejects_invalid_literal_image_values() -> Result<(), String> {
let mut volume = Volume::new(id("data")?, ResourceOwnership::Application);
assert!(matches!(
volume.set_image_source(Sourced::generated(VolumeImageSource::Literal(ProtectedString::plain(
""
)))),
Err(ModelError::EmptyValue("volume image"))
));
assert!(matches!(
volume.set_image_source(Sourced::generated(VolumeImageSource::Literal(ProtectedString::plain(
"bad\0image"
)))),
Err(ModelError::ContainsNul("volume image"))
));
Ok(())
}
#[test]
fn collection_resets_retain_explicit_emptiness_and_clear_stale_origins() -> Result<(), String> {
let origin =
crate::Provenance::source(crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?);
let mut service = Service::new(id("web")?);
service.set_cap_add_with_origins(Vec::new(), vec![origin.clone()]);
service.set_cap_drop_with_origins(Vec::new(), vec![origin.clone()]);
service.set_tmpfs_with_origins(Vec::new(), vec![origin.clone()]);
service.set_sysctls_with_origins(Vec::new(), vec![origin.clone()]);
service.set_ulimits_with_origins(Vec::new(), vec![origin.clone()]);
service.set_devices_with_origins(Vec::new(), vec![origin]);
assert_eq!(service.cap_add().map(<[_]>::len), Some(0));
assert_eq!(service.cap_drop().map(<[_]>::len), Some(0));
assert_eq!(service.tmpfs().map(<[_]>::len), Some(0));
assert_eq!(service.sysctls().map(<[_]>::len), Some(0));
assert_eq!(service.ulimits().map(<[_]>::len), Some(0));
assert_eq!(service.devices().map(<[_]>::len), Some(0));
assert_eq!(service.cap_add_origins().len(), 1);
assert_eq!(service.cap_drop_origins().len(), 1);
assert_eq!(service.tmpfs_origins().len(), 1);
assert_eq!(service.sysctls_origins().len(), 1);
assert_eq!(service.ulimits_origins().len(), 1);
assert_eq!(service.devices_origins().len(), 1);
service.set_cap_add(Vec::new());
service.set_cap_drop(Vec::new());
service.set_tmpfs(Vec::new());
service.set_sysctls(Vec::<Sourced<KernelParameter>>::new());
service.set_ulimits(Vec::<Sourced<ResourceLimit>>::new());
service.set_devices(Vec::<Sourced<Device>>::new());
assert!(service.cap_add_origins().is_empty());
assert!(service.cap_drop_origins().is_empty());
assert!(service.tmpfs_origins().is_empty());
assert!(service.sysctls_origins().is_empty());
assert!(service.ulimits_origins().is_empty());
assert!(service.devices_origins().is_empty());
Ok(())
}
#[test]
fn restart_policy_keeps_unlimited_and_finite_on_failure_distinct() {
let finite = std::num::NonZeroU64::new(4);
assert_eq!(RestartPolicy::on_failure(None).maximum_retries(), None);
assert_eq!(RestartPolicy::on_failure(finite).maximum_retries(), finite);
assert_eq!(RestartPolicy::Always.maximum_retries(), None);
}
#[test]
fn metadata_labels_preserve_empty_and_protected_values() -> Result<(), String> {
let empty = MetadataLabel::new(id("com.example.empty")?, ProtectedString::plain(""));
let protected = MetadataLabel::new(id("com.example.token")?, ProtectedString::sensitive("never-print-this"));
let mut service = Service::new(id("web")?);
service.add_label(Sourced::generated(empty));
service.add_label(Sourced::generated(protected));
assert_eq!(service.labels()[0].value().value().expose(), "");
let debug = format!("{:?}", service.labels()[1]);
assert!(!debug.contains("never-print-this"));
assert!(debug.contains("[REDACTED]"));
Ok(())
}
#[test]
fn environment_files_preserve_order_options_provenance_and_redaction() -> Result<(), String> {
let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
let origin = crate::Provenance::source(source);
let mut service = Service::new(id("web")?);
service.add_environment_file(Sourced::from_source(
EnvironmentFile::new(ProtectedString::plain("./base.env"), EnvironmentFileSyntax::Short)
.map_err(|error| error.to_string())?,
origin.clone(),
));
let mut local = EnvironmentFile::new(ProtectedString::sensitive("./private.env"), EnvironmentFileSyntax::Long)
.map_err(|error| error.to_string())?;
local.set_required(Sourced::from_source(false, origin.clone()));
local.set_format(Sourced::from_source(EnvironmentFileFormat::Raw, origin.clone()));
service.add_environment_file(Sourced::from_source(local, origin));
assert_eq!(service.environment_files().len(), 2);
assert_eq!(service.environment_files()[0].value().path().expose(), "./base.env");
assert_eq!(
service.environment_files()[0].value().syntax(),
EnvironmentFileSyntax::Short
);
assert!(service.environment_files()[0].value().is_required());
let local = service.environment_files()[1].value();
assert_eq!(local.syntax(), EnvironmentFileSyntax::Long);
assert!(!local.is_required());
assert_eq!(local.required().map_or(0, |value| value.origins().len()), 1);
assert!(matches!(
local.format().map(Sourced::value),
Some(EnvironmentFileFormat::Raw)
));
let debug = format!("{service:?}");
assert!(!debug.contains("private.env"));
assert!(debug.contains("[REDACTED]"));
assert!(matches!(
EnvironmentFile::new(ProtectedString::plain(""), EnvironmentFileSyntax::Short),
Err(ModelError::EmptyValue("environment-file path"))
));
Ok(())
}
#[test]
fn service_groups_preserve_order_and_reject_ambiguous_membership() -> Result<(), String> {
let mut application = Application::new(id("example")?);
for name in ["web", "worker"] {
application
.add_service(Sourced::generated(Service::new(id(name)?)))
.map_err(|error| error.to_string())?;
}
let mut frontend = ServiceGroup::new(id("frontend")?, ResourceOwnership::Uncertain);
frontend
.add_member(Sourced::generated(id("web")?))
.map_err(|error| error.to_string())?;
assert!(matches!(
frontend.add_member(Sourced::generated(id("web")?)),
Err(ModelError::DuplicateServiceGroupMember { .. })
));
application
.add_service_group(Sourced::generated(frontend))
.map_err(|error| error.to_string())?;
assert_eq!(application.service_groups()[0].value().name().as_str(), "frontend");
assert_eq!(
application.service_groups()[0].value().members()[0].value().as_str(),
"web"
);
let mut conflicting = ServiceGroup::new(id("backend")?, ResourceOwnership::Application);
conflicting
.add_member(Sourced::generated(id("web")?))
.map_err(|error| error.to_string())?;
assert!(matches!(
application.add_service_group(Sourced::generated(conflicting)),
Err(ModelError::ServiceInMultipleGroups { .. })
));
let mut missing = ServiceGroup::new(id("missing")?, ResourceOwnership::External);
missing
.add_member(Sourced::generated(id("database")?))
.map_err(|error| error.to_string())?;
assert!(matches!(
application.add_service_group(Sourced::generated(missing)),
Err(ModelError::UnknownServiceGroupMember { .. })
));
Ok(())
}
#[test]
fn group_runtime_keeps_group_names_and_pod_settings_distinct() -> Result<(), String> {
let source = crate::SourceId::new("pod.pod").map_err(|error| error.to_string())?;
let origin = crate::Provenance::source(source);
let mut group = ServiceGroup::new(id("frontend")?, ResourceOwnership::Application);
let mut runtime = ServiceGroupRuntime::new();
runtime.set_runtime_name(Sourced::from_source(
ProtectedString::plain("production-frontend"),
origin.clone(),
));
runtime.set_service_name(Sourced::from_source(
ProtectedString::plain("frontend-pod"),
origin.clone(),
));
runtime.set_host_mappings_with_origins(
vec![Sourced::from_source(
HostMapping::new(
id("host.docker.internal")?,
HostAddress::new("host-gateway").map_err(|error| error.to_string())?,
),
origin.clone(),
)],
vec![origin.clone()],
);
runtime.set_ports_with_origins(Vec::new(), vec![origin.clone()]);
runtime.set_networks_with_origins(
vec![Sourced::from_source(
NetworkAttachment::with_sourced_aliases(
id("edge")?,
vec![Sourced::from_source(
ProtectedString::sensitive("private-alias"),
origin.clone(),
)],
),
origin.clone(),
)],
vec![origin.clone()],
);
runtime.set_user_namespace(Sourced::from_source(ProtectedString::plain("keep-id"), origin.clone()));
runtime.set_mounts_with_origins(
vec![Sourced::from_source(
Mount::new(MountSource::Anonymous, "/cache", false).map_err(|error| error.to_string())?,
origin.clone(),
)],
vec![origin.clone()],
);
runtime.set_shm_size(Sourced::from_source(ProtectedString::sensitive("64m"), origin.clone()));
runtime.set_exit_policy(Sourced::from_source(
GroupExitPolicy::Raw(ProtectedString::sensitive("preserve-this")),
origin.clone(),
));
runtime.set_stop_timeout(Sourced::from_source(
StopTimeout::new("30s").map_err(|error| error.to_string())?,
origin.clone(),
));
assert!(matches!(
runtime.replace_network(1, Sourced::generated(NetworkAttachment::new(id("other")?, Vec::new()))),
Err(ModelError::UnknownServiceGroupRuntimeNetworkIndex { index: 1, len: 1 })
));
group.set_runtime(Sourced::from_source(runtime, origin));
let runtime = group
.runtime()
.ok_or_else(|| "group runtime was omitted".to_owned())?
.value();
assert_eq!(group.name().as_str(), "frontend");
assert_eq!(
runtime.runtime_name().map(|name| name.value().expose()),
Some("production-frontend")
);
assert_eq!(
runtime.service_name().map(|name| name.value().expose()),
Some("frontend-pod")
);
assert_eq!(runtime.host_mappings().map(<[_]>::len), Some(1));
assert_eq!(runtime.ports().map(<[_]>::len), Some(0));
assert_eq!(runtime.networks_origins().len(), 1);
assert_eq!(runtime.mounts().map(<[_]>::len), Some(1));
assert!(matches!(
runtime.exit_policy().map(Sourced::value),
Some(GroupExitPolicy::Raw(_))
));
let debug = format!("{group:?}");
for sensitive in ["private-alias", "64m", "preserve-this"] {
assert!(!debug.contains(sensitive));
}
assert!(debug.contains("[REDACTED]"));
Ok(())
}
#[test]
fn rootfs_startup_notification_and_podman_args_preserve_safe_contracts() -> Result<(), String> {
let source = crate::SourceId::new("web.container").map_err(|error| error.to_string())?;
let origin = crate::Provenance::source(source);
let mut service = Service::new(id("web")?);
service.set_startup_notification(Sourced::from_source(StartupNotification::Healthy, origin.clone()));
service.set_podman_args_with_origins(
vec![
Sourced::from_source(ProtectedString::plain("--replace"), origin.clone()),
Sourced::from_source(ProtectedString::sensitive("--secret=never-print"), origin.clone()),
Sourced::from_source(ProtectedString::plain("--replace"), origin.clone()),
],
vec![origin.clone()],
);
assert_eq!(service.podman_args().map(<[_]>::len), Some(3));
assert_eq!(service.podman_args_origins(), std::slice::from_ref(&origin));
assert!(matches!(
service.startup_notification().map(Sourced::value),
Some(StartupNotification::Healthy)
));
assert!(!format!("{service:?}").contains("never-print"));
let mut with_image = Service::new(id("image-first")?);
with_image.set_image(Sourced::generated(
ImageReference::parse("example.invalid/web:1").map_err(|error| error.to_string())?,
));
assert!(matches!(
with_image.set_rootfs(Sourced::generated(ProtectedString::plain("/srv/rootfs"))),
Err(ModelError::RootfsImageSourceConflict { source: "image", .. })
));
let mut with_rootfs = Service::new(id("rootfs-first")?);
with_rootfs
.set_rootfs(Sourced::generated(ProtectedString::sensitive("/private/rootfs")))
.map_err(|error| error.to_string())?;
with_rootfs.set_image_build(Sourced::generated(id("web-build")?));
let mut application = Application::new(id("example")?);
application
.add_image_build(Sourced::generated(ImageBuild::new(id("web-build")?)))
.map_err(|error| error.to_string())?;
assert!(matches!(
application.add_service(Sourced::generated(with_rootfs)),
Err(ModelError::RootfsImageSourceConflict {
source: "image build",
..
})
));
Ok(())
}
#[test]
fn validates_raw_preserving_healthcheck_scalars() -> Result<(), String> {
let duration = HealthcheckDuration::new("1m30s").map_err(|error| error.to_string())?;
let retries = HealthcheckRetries::new("003").map_err(|error| error.to_string())?;
assert_eq!(duration.as_str(), "1m30s");
assert_eq!(retries.as_str(), "003");
assert_eq!(
HealthcheckRetries::new("three"),
Err(ModelError::InvalidHealthcheckRetries)
);
assert!(matches!(
HealthcheckDuration::new(""),
Err(ModelError::EmptyValue("health-check duration"))
));
Ok(())
}
#[test]
fn preserves_ordered_dependency_edges_and_field_provenance() -> Result<(), String> {
let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
let origin = crate::Provenance::source(source);
let mut service = Service::new(id("web")?);
let mut database = ServiceDependency::new(id("database")?);
database.set_condition(Sourced::from_source(
ServiceDependencyCondition::Healthy,
origin.clone(),
));
database.set_required(Sourced::from_source(true, origin.clone()));
service.add_dependency(Sourced::from_source(database, origin.clone()));
let cache = ServiceDependency::new(id("cache")?);
assert!(cache.is_required());
service.add_dependency(Sourced::from_source(cache, origin));
assert_eq!(
service
.dependencies()
.iter()
.map(|dependency| dependency.value().service().as_str())
.collect::<Vec<_>>(),
["database", "cache"]
);
assert!(matches!(
service.dependencies()[0].value().condition().map(Sourced::value),
Some(ServiceDependencyCondition::Healthy)
));
assert_eq!(service.dependencies()[0].origins().len(), 1);
assert_eq!(
service.dependencies()[0]
.value()
.condition()
.map_or(0, |condition| condition.origins().len()),
1
);
Ok(())
}
#[test]
fn retains_execution_identity_context_order_provenance_and_redaction() -> Result<(), String> {
let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
let origin = crate::Provenance::source(source);
let mut service = Service::new(id("web")?);
service.set_user(Sourced::from_source(ProtectedString::sensitive("1001"), origin.clone()));
service.set_group(Sourced::from_source(ProtectedString::plain("1002"), origin.clone()));
service.set_user_namespace(Sourced::from_source(ProtectedString::plain("keep-id"), origin.clone()));
service.add_supplementary_group(Sourced::from_source(ProtectedString::plain("audio"), origin.clone()));
service.add_supplementary_group(Sourced::from_source(ProtectedString::plain("44"), origin.clone()));
service.set_working_directory(Sourced::from_source(ProtectedString::plain("/srv/app"), origin.clone()));
service.set_read_only_root_filesystem(Sourced::from_source(true, origin));
assert_eq!(service.user().map(|value| value.value().expose()), Some("1001"));
assert_eq!(service.group().map(|value| value.value().expose()), Some("1002"));
assert_eq!(
service.user_namespace().map(|value| value.value().expose()),
Some("keep-id")
);
assert_eq!(
service
.supplementary_groups()
.iter()
.map(|group| group.value().expose())
.collect::<Vec<_>>(),
["audio", "44"]
);
assert_eq!(
service.working_directory().map(|value| value.value().expose()),
Some("/srv/app")
);
assert_eq!(service.read_only_root_filesystem().map(Sourced::value), Some(&true));
assert_eq!(service.user().map_or(0, |value| value.origins().len()), 1);
let debug = format!("{service:?}");
assert!(!debug.contains("1001"));
assert!(debug.contains("[REDACTED]"));
Ok(())
}
#[test]
fn retains_config_secret_resources_grants_provenance_and_redaction() -> Result<(), String> {
let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
let origin = crate::Provenance::source(source);
let mut application = Application::new(id("example")?);
let mut config = Config::new(id("settings")?, ResourceOwnership::Application);
config.set_material(Sourced::from_source(
ConfigMaterial::Content(ProtectedString::sensitive("private-config")),
origin.clone(),
));
application
.add_config(Sourced::from_source(config, origin.clone()))
.map_err(|error| error.to_string())?;
let mut secret = Secret::new(id("password")?, ResourceOwnership::External);
secret.set_runtime_name(Sourced::from_source(
ProtectedString::plain("production-password"),
origin.clone(),
));
secret.set_material(Sourced::from_source(
SecretMaterial::Environment(ProtectedString::sensitive("private-environment-name")),
origin.clone(),
));
application
.add_secret(Sourced::from_source(secret, origin.clone()))
.map_err(|error| error.to_string())?;
let mut service = Service::new(id("web")?);
service.add_config_grant(Sourced::from_source(
ResourceGrant::new(ProtectedString::plain("settings"), ResourceGrantSyntax::Short)
.map_err(|error| error.to_string())?,
origin.clone(),
));
let mut secret_grant = ResourceGrant::new(
ProtectedString::sensitive("private-grant-source"),
ResourceGrantSyntax::Long,
)
.map_err(|error| error.to_string())?;
secret_grant.set_target(Sourced::from_source(
ProtectedString::plain("database-password"),
origin.clone(),
));
secret_grant.set_uid(Sourced::from_source(ProtectedString::plain("1001"), origin.clone()));
secret_grant.set_gid(Sourced::from_source(ProtectedString::plain("1002"), origin.clone()));
secret_grant.set_mode(Sourced::from_source(ProtectedString::plain("0440"), origin.clone()));
service.add_secret_grant(Sourced::from_source(secret_grant, origin.clone()));
application
.add_service(Sourced::from_source(service, origin))
.map_err(|error| error.to_string())?;
assert_eq!(application.configs().len(), 1);
assert_eq!(application.secrets().len(), 1);
assert_eq!(application.services()[0].value().config_grants().len(), 1);
let grant = &application.services()[0].value().secret_grants()[0];
assert_eq!(grant.value().syntax(), ResourceGrantSyntax::Long);
assert_eq!(
grant.value().target().map(|value| value.value().expose()),
Some("database-password")
);
assert_eq!(grant.value().uid().map_or(0, |value| value.origins().len()), 1);
assert_eq!(grant.origins().len(), 1);
let debug = format!("{application:?}");
for secret in ["private-config", "private-environment-name", "private-grant-source"] {
assert!(!debug.contains(secret));
}
assert!(debug.contains("[REDACTED]"));
assert!(matches!(
ResourceGrant::new(ProtectedString::plain(""), ResourceGrantSyntax::Short),
Err(ModelError::EmptyValue("resource grant source"))
));
assert!(matches!(
application.add_config(Sourced::generated(Config::new(
id("settings")?,
ResourceOwnership::External,
))),
Err(ModelError::DuplicateResource { kind: "config", .. })
));
assert!(matches!(
application.add_secret(Sourced::generated(Secret::new(
id("password")?,
ResourceOwnership::External,
))),
Err(ModelError::DuplicateResource { kind: "secret", .. })
));
Ok(())
}
#[test]
fn host_mappings_preserve_order_spelling_and_runtime_tokens() -> Result<(), String> {
let mut service = Service::new(id("web")?);
service.add_host_mapping(Sourced::generated(HostMapping::new(
id("host.docker.internal")?,
HostAddress::new("host-gateway").map_err(|error| error.to_string())?,
)));
service.add_host_mapping(Sourced::generated(HostMapping::new(
id("ipv6")?,
HostAddress::new("[::1]").map_err(|error| error.to_string())?,
)));
assert_eq!(service.host_mappings().len(), 2);
assert_eq!(
service.host_mappings()[0].value().address().kind(),
HostAddressKind::HostGateway
);
assert_eq!(service.host_mappings()[1].value().address().raw(), "[::1]");
assert_eq!(
service.host_mappings()[1].value().address().kind(),
HostAddressKind::Ipv6 { bracketed: true }
);
assert!(matches!(HostAddress::new(""), Err(ModelError::EmptyValue(_))));
Ok(())
}
#[test]
fn dns_collections_preserve_order_provenance_and_explicit_empty_state() -> Result<(), String> {
let mut service = Service::new(id("web")?);
assert!(service.dns_servers().is_none());
service.set_dns_servers(Vec::new());
assert!(matches!(service.dns_servers(), Some(values) if values.is_empty()));
service.set_dns_options(vec![
Sourced::generated(ProtectedString::plain("ndots:5")),
Sourced::generated(ProtectedString::sensitive("rotate")),
]);
service.set_dns_search_domains(vec![Sourced::generated(ProtectedString::plain("example.test"))]);
assert_eq!(
service
.dns_options()
.unwrap_or_default()
.iter()
.map(|value| value.value().expose())
.collect::<Vec<_>>(),
["ndots:5", "rotate"]
);
assert!(!format!("{service:?}").contains("rotate"));
Ok(())
}
#[test]
fn security_options_preserve_empty_order_duplicates_provenance_and_redaction() -> Result<(), String> {
let origin =
crate::Provenance::source(crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?);
let mut service = Service::new(id("web")?);
assert!(service.security_options().is_none());
service.set_security_options_with_origins(Vec::new(), vec![origin.clone()]);
assert_eq!(service.security_options().map(<[_]>::len), Some(0));
assert_eq!(service.security_options_origins(), std::slice::from_ref(&origin));
service.set_security_options_with_origins(
vec![
Sourced::from_source(
SecurityOption::AppArmor(ProtectedString::sensitive("apparmor-secret")),
origin.clone(),
),
Sourced::from_source(SecurityOption::NoNewPrivileges(true), origin.clone()),
Sourced::from_source(
SecurityOption::SeccompProfile(ProtectedString::sensitive("seccomp-secret")),
origin.clone(),
),
Sourced::from_source(SecurityOption::SecurityLabelDisable(false), origin.clone()),
Sourced::from_source(
SecurityOption::SecurityLabelFileType(ProtectedString::sensitive("file-type-secret")),
origin.clone(),
),
Sourced::from_source(
SecurityOption::SecurityLabelLevel(ProtectedString::sensitive("level-secret")),
origin.clone(),
),
Sourced::from_source(SecurityOption::SecurityLabelNested(true), origin.clone()),
Sourced::from_source(
SecurityOption::SecurityLabelType(ProtectedString::sensitive("type-secret")),
origin.clone(),
),
Sourced::from_source(
SecurityOption::Mask(ProtectedString::sensitive("mask-secret")),
origin.clone(),
),
Sourced::from_source(
SecurityOption::Unmask(ProtectedString::sensitive("unmask-secret")),
origin.clone(),
),
Sourced::from_source(
SecurityOption::Mask(ProtectedString::sensitive("mask-secret")),
origin.clone(),
),
],
vec![origin.clone()],
);
let options = service.security_options().unwrap_or_default();
assert_eq!(options.len(), 11);
assert!(
matches!(options[0].value(), SecurityOption::AppArmor(profile) if profile.expose() == "apparmor-secret")
);
assert!(matches!(options[1].value(), SecurityOption::NoNewPrivileges(true)));
assert!(
matches!(options[2].value(), SecurityOption::SeccompProfile(profile) if profile.expose() == "seccomp-secret")
);
assert!(matches!(
options[3].value(),
SecurityOption::SecurityLabelDisable(false)
));
assert!(
matches!(options[4].value(), SecurityOption::SecurityLabelFileType(profile) if profile.expose() == "file-type-secret")
);
assert!(
matches!(options[5].value(), SecurityOption::SecurityLabelLevel(profile) if profile.expose() == "level-secret")
);
assert!(matches!(options[6].value(), SecurityOption::SecurityLabelNested(true)));
assert!(
matches!(options[7].value(), SecurityOption::SecurityLabelType(profile) if profile.expose() == "type-secret")
);
assert!(matches!(options[8].value(), SecurityOption::Mask(path) if path.expose() == "mask-secret"));
assert!(matches!(options[9].value(), SecurityOption::Unmask(path) if path.expose() == "unmask-secret"));
assert!(matches!(options[10].value(), SecurityOption::Mask(path) if path.expose() == "mask-secret"));
assert_eq!(options[0].origins(), std::slice::from_ref(&origin));
assert_eq!(service.security_options_origins(), std::slice::from_ref(&origin));
let debug = format!("{service:?}");
for secret in [
"apparmor-secret",
"seccomp-secret",
"file-type-secret",
"level-secret",
"type-secret",
"mask-secret",
"unmask-secret",
] {
assert!(!debug.contains(secret));
}
assert!(debug.contains("[REDACTED]"));
service.set_security_options(Vec::new());
assert_eq!(service.security_options().map(<[_]>::len), Some(0));
assert!(service.security_options_origins().is_empty());
Ok(())
}
#[test]
fn retains_entrypoint_run_init_stop_pull_memory_and_exposed_port_intent() -> Result<(), String> {
let origin =
crate::Provenance::source(crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?);
let mut service = Service::new(id("web")?);
service.set_command(Sourced::from_source(
Command::Exec(vec![ProtectedString::plain("serve")]),
origin.clone(),
));
service.set_entrypoint(Sourced::from_source(
Entrypoint::Shell(ProtectedString::sensitive("/bin/sh -c private-entrypoint")),
origin.clone(),
));
service.set_run_init(Sourced::from_source(true, origin.clone()));
service.set_stop_timeout(Sourced::from_source(
StopTimeout::new("01m30s").map_err(|error| error.to_string())?,
origin.clone(),
));
service.set_pull_policy(Sourced::from_source(
PullPolicy::Every(ProtectedString::sensitive("12h")),
origin.clone(),
));
service.set_memory_limit(Sourced::from_source(
ProtectedString::sensitive("512MiB"),
origin.clone(),
));
assert!(service.exposed_ports().is_none());
service.set_exposed_ports_with_origins(Vec::new(), vec![origin.clone()]);
assert_eq!(service.exposed_ports().map(<[_]>::len), Some(0));
assert_eq!(service.exposed_ports_origins(), std::slice::from_ref(&origin));
service.add_exposed_port(Sourced::from_source(
ExposedPort::new(8080, Protocol::Tcp).map_err(|error| error.to_string())?,
origin.clone(),
));
service.add_exposed_port(Sourced::from_source(
ExposedPort::new(8080, Protocol::Tcp).map_err(|error| error.to_string())?,
origin,
));
assert!(matches!(service.command().map(Sourced::value), Some(Command::Exec(_))));
assert!(matches!(
service.entrypoint().map(Sourced::value),
Some(Entrypoint::Shell(_))
));
assert_eq!(service.run_init().map(Sourced::value), Some(&true));
assert_eq!(
service.stop_timeout().map(|timeout| timeout.value().as_str()),
Some("01m30s")
);
assert!(matches!(
service.pull_policy().map(Sourced::value),
Some(PullPolicy::Every(_))
));
assert_eq!(
service.memory_limit().map(|limit| limit.value().expose()),
Some("512MiB")
);
let exposed_ports = service.exposed_ports().ok_or("missing exposed ports")?;
assert_eq!(exposed_ports.len(), 2);
assert_eq!(exposed_ports[0].value().container(), 8080);
assert_eq!(exposed_ports[0].value().protocol(), &Protocol::Tcp);
assert!(matches!(
ExposedPort::new(0, Protocol::Udp),
Err(ModelError::ZeroContainerPort)
));
assert!(matches!(
StopTimeout::new(""),
Err(ModelError::EmptyValue("stop timeout"))
));
let debug = format!("{service:?}");
for secret in ["private-entrypoint", "512MiB", "12h"] {
assert!(!debug.contains(secret));
}
assert!(debug.contains("[REDACTED]"));
Ok(())
}
#[test]
fn annotations_and_logging_preserve_empty_order_field_provenance_and_redaction() -> Result<(), String> {
let origin =
crate::Provenance::source(crate::SourceId::new("quadlet.container").map_err(|error| error.to_string())?);
let mut service = Service::new(id("web")?);
assert!(service.annotations().is_none());
service.set_annotations_with_origins(Vec::new(), vec![origin.clone()]);
assert_eq!(service.annotations().map(<[_]>::len), Some(0));
assert_eq!(service.annotations_origins(), std::slice::from_ref(&origin));
service.set_annotations_with_origins(
vec![
Sourced::from_source(
Annotation::new(
Sourced::from_source(id("io.example.first")?, origin.clone()),
Sourced::from_source(ProtectedString::sensitive("annotation-secret"), origin.clone()),
),
origin.clone(),
),
Sourced::from_source(
Annotation::new(
Sourced::from_source(id("io.example.second")?, origin.clone()),
Sourced::from_source(ProtectedString::plain(""), origin.clone()),
),
origin.clone(),
),
],
vec![origin.clone()],
);
let annotations = service.annotations().unwrap_or_default();
assert_eq!(annotations.len(), 2);
assert_eq!(annotations[0].value().name().value().as_str(), "io.example.first");
assert_eq!(annotations[1].value().value().value().expose(), "");
assert_eq!(annotations[0].value().name().origins(), std::slice::from_ref(&origin));
assert_eq!(annotations[0].value().value().origins(), std::slice::from_ref(&origin));
let mut logging = Logging::new();
assert!(logging.options().is_none());
logging.set_driver(Sourced::from_source(ProtectedString::plain("journald"), origin.clone()));
logging.set_options_with_origins(
vec![
Sourced::from_source(
LoggingOption::new(
Sourced::from_source(id("tag")?, origin.clone()),
Sourced::from_source(ProtectedString::sensitive("logging-secret"), origin.clone()),
),
origin.clone(),
),
Sourced::from_source(
LoggingOption::new(
Sourced::from_source(id("labels")?, origin.clone()),
Sourced::from_source(ProtectedString::plain(""), origin.clone()),
),
origin.clone(),
),
],
vec![origin.clone()],
);
service.set_logging(Sourced::from_source(logging, origin));
let logging = service.logging().map(Sourced::value).ok_or("missing logging")?;
assert_eq!(logging.driver().map(|driver| driver.value().expose()), Some("journald"));
assert_eq!(logging.options().map(<[_]>::len), Some(2));
assert_eq!(
logging.options().unwrap_or_default()[0].value().name().value().as_str(),
"tag"
);
assert_eq!(logging.options_origins().len(), 1);
let debug = format!("{service:?}");
assert!(!debug.contains("annotation-secret"));
assert!(!debug.contains("logging-secret"));
assert!(debug.contains("[REDACTED]"));
Ok(())
}
#[test]
fn network_attachments_keep_legacy_constructor_and_add_source_aware_addresses_aliases() -> Result<(), String> {
let origin =
crate::Provenance::source(crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?);
let legacy = NetworkAttachment::new(id("legacy")?, vec!["legacy.alias".to_owned()]);
assert_eq!(legacy.aliases(), ["legacy.alias"]);
assert!(legacy.alias_origins().is_empty());
let mut attachment = NetworkAttachment::with_sourced_aliases(
id("frontend")?,
vec![
Sourced::from_source(ProtectedString::plain("web"), origin.clone()),
Sourced::from_source(ProtectedString::sensitive("private-alias"), origin.clone()),
],
);
attachment.set_ipv4_address(Sourced::from_source(
ProtectedString::plain("192.0.2.10"),
origin.clone(),
));
attachment.set_ipv6_address(Sourced::from_source(ProtectedString::plain("2001:db8::10"), origin));
let metrics = Sourced::generated(ProtectedString::plain("metrics"));
attachment.add_alias(&metrics);
assert_eq!(attachment.aliases(), ["web", "private-alias", "metrics"]);
assert_eq!(attachment.alias_sensitivities(), [false, true, false]);
assert_eq!(attachment.alias_origins().len(), 3);
assert_eq!(attachment.alias_origins()[0].len(), 1);
assert!(attachment.alias_origins()[2].is_empty());
assert_eq!(
attachment.ipv4_address().map(|address| address.value().expose()),
Some("192.0.2.10")
);
assert_eq!(
attachment.ipv6_address().map(|address| address.value().expose()),
Some("2001:db8::10")
);
let debug = format!("{attachment:?}");
assert!(!debug.contains("private-alias"));
assert!(debug.contains("[REDACTED]"));
let mut service = Service::new(id("web")?);
service.add_network(Sourced::generated(legacy));
let previous = service
.replace_network(0, Sourced::generated(attachment))
.map_err(|error| error.to_string())?;
assert_eq!(previous.value().network().as_str(), "legacy");
assert_eq!(service.networks()[0].value().network().as_str(), "frontend");
assert!(matches!(
service.replace_network(1, Sourced::generated(NetworkAttachment::new(id("unused")?, Vec::new()))),
Err(ModelError::UnknownNetworkAttachmentIndex { index: 1, len: 1 })
));
Ok(())
}
#[test]
fn reload_action_is_one_explicit_command_or_signal() -> Result<(), String> {
let origin =
crate::Provenance::source(crate::SourceId::new("quadlet.container").map_err(|error| error.to_string())?);
let mut service = Service::new(id("web")?);
service.set_reload_action(Sourced::from_source(
ReloadAction::Command(Command::Exec(vec![ProtectedString::plain("reload")])),
origin.clone(),
));
assert!(matches!(
service.reload_action().map(Sourced::value),
Some(ReloadAction::Command(Command::Exec(_)))
));
service.set_reload_action(Sourced::from_source(
ReloadAction::Signal(ProtectedString::sensitive("SIGHUP")),
origin,
));
assert!(matches!(
service.reload_action().map(Sourced::value),
Some(ReloadAction::Signal(_))
));
let debug = format!("{service:?}");
assert!(!debug.contains("SIGHUP"));
assert!(debug.contains("[REDACTED]"));
Ok(())
}
fn id(value: &str) -> Result<Identifier, String> {
Identifier::new(value).map_err(|error| error.to_string())
}
}