use std::{collections::BTreeSet, error::Error, fmt};
use crate::{
diagnostic::Diagnostic,
model::{
ArtifactKey, BuildKey, ContainerKey, EntryKind, ImageKey, KubeKey, NetworkKey, PodKey, QuadletDocument,
QuadletKey, QuadletParseResult, QuadletUnitType, SectionKind, SystemdUnitKey, TypedModelError, VolumeKey,
},
source::SourceId,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EntryValue(String);
impl EntryValue {
pub fn new(value: impl Into<String>) -> Result<Self, RenderError> {
let value = value.into();
if value.bytes().any(|byte| matches!(byte, 0 | b'\n' | b'\r')) {
return Err(RenderError::InvalidValue);
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Eq, PartialEq)]
pub struct EnvironmentAssignment {
name: String,
value: String,
rendered: String,
}
impl fmt::Debug for EnvironmentAssignment {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("EnvironmentAssignment")
.field("name", &self.name)
.field("value", &"<redacted environment value>")
.field("rendered", &"<redacted environment assignment>")
.finish()
}
}
impl EnvironmentAssignment {
pub fn new(name: impl Into<String>, value: impl Into<String>) -> Result<Self, EnvironmentAssignmentError> {
let name = name.into();
if !is_environment_name(&name) {
return Err(EnvironmentAssignmentError::InvalidName);
}
let value = value.into();
for character in value.chars() {
match character {
'\0' => return Err(EnvironmentAssignmentError::Nul),
'\r' => return Err(EnvironmentAssignmentError::CarriageReturn),
'\n' => return Err(EnvironmentAssignmentError::LineFeed),
'%' => return Err(EnvironmentAssignmentError::Specifier),
_ if character.is_control() => return Err(EnvironmentAssignmentError::ControlCharacter),
_ => {}
}
}
let escaped = value.replace('\\', "\\\\").replace('"', "\\\"");
let rendered = format!("\"{name}={escaped}\"");
Ok(Self { name, value, rendered })
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn value(&self) -> &str {
&self.value
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.rendered
}
}
impl From<EnvironmentAssignment> for EntryValue {
fn from(assignment: EnvironmentAssignment) -> Self {
Self(assignment.rendered)
}
}
#[derive(Clone, Eq, PartialEq)]
pub struct EnvironmentAssignments {
assignments: Vec<EnvironmentAssignment>,
rendered: String,
}
impl fmt::Debug for EnvironmentAssignments {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("EnvironmentAssignments")
.field("assignment_count", &self.assignments.len())
.field("rendered", &"<redacted environment assignments>")
.finish()
}
}
impl EnvironmentAssignments {
pub fn new(
assignments: impl IntoIterator<Item = EnvironmentAssignment>,
) -> Result<Self, EnvironmentAssignmentsError> {
let assignments: Vec<_> = assignments.into_iter().collect();
if assignments.is_empty() {
return Err(EnvironmentAssignmentsError::Empty);
}
let rendered = assignments
.iter()
.map(EnvironmentAssignment::as_str)
.collect::<Vec<_>>()
.join(" ");
Ok(Self { assignments, rendered })
}
#[must_use]
pub fn assignments(&self) -> &[EnvironmentAssignment] {
&self.assignments
}
#[must_use]
pub fn iter(&self) -> impl ExactSizeIterator<Item = &EnvironmentAssignment> {
self.assignments.iter()
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.rendered
}
}
impl From<EnvironmentAssignments> for EntryValue {
fn from(assignments: EnvironmentAssignments) -> Self {
Self(assignments.rendered)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct EnvironmentReset {
_private: (),
}
impl EnvironmentReset {
#[must_use]
pub const fn new() -> Self {
Self { _private: () }
}
#[must_use]
pub const fn as_str(self) -> &'static str {
""
}
}
impl From<EnvironmentReset> for EntryValue {
fn from(_: EnvironmentReset) -> Self {
Self(String::new())
}
}
#[derive(Clone, Eq, PartialEq)]
#[non_exhaustive]
pub enum ContainerEnvironmentDirective {
Assignment(EnvironmentAssignment),
Assignments(EnvironmentAssignments),
Reset(EnvironmentReset),
}
impl fmt::Debug for ContainerEnvironmentDirective {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Assignment(_) => formatter.write_str("Assignment(<redacted environment assignment>)"),
Self::Assignments(assignments) => formatter
.debug_struct("Assignments")
.field("assignment_count", &assignments.assignments.len())
.field("values", &"<redacted environment assignments>")
.finish(),
Self::Reset(_) => formatter.write_str("Reset"),
}
}
}
#[derive(Clone, Default, Eq, PartialEq)]
pub struct ContainerEnvironmentPlan {
directives: Vec<ContainerEnvironmentDirective>,
}
impl ContainerEnvironmentPlan {
#[must_use]
pub const fn new() -> Self {
Self { directives: Vec::new() }
}
pub fn push_assignment(&mut self, assignment: EnvironmentAssignment) {
self.directives
.push(ContainerEnvironmentDirective::Assignment(assignment));
}
pub fn push_assignments(&mut self, assignments: EnvironmentAssignments) {
self.directives
.push(ContainerEnvironmentDirective::Assignments(assignments));
}
pub fn push_reset(&mut self) {
self.directives
.push(ContainerEnvironmentDirective::Reset(EnvironmentReset::new()));
}
#[must_use]
pub fn directives(&self) -> &[ContainerEnvironmentDirective] {
&self.directives
}
#[must_use]
pub fn sorted_by_name(&self) -> Self {
fn flush(assignments: &mut Vec<EnvironmentAssignment>, directives: &mut Vec<ContainerEnvironmentDirective>) {
assignments.sort_by(|left, right| left.name().cmp(right.name()));
directives.extend(assignments.drain(..).map(ContainerEnvironmentDirective::Assignment));
}
let mut directives = Vec::with_capacity(self.directives.len());
let mut assignments = Vec::new();
for directive in &self.directives {
match directive {
ContainerEnvironmentDirective::Assignment(assignment) => {
assignments.push(assignment.clone());
}
ContainerEnvironmentDirective::Assignments(group) => {
assignments.extend(group.iter().cloned());
}
ContainerEnvironmentDirective::Reset(reset) => {
flush(&mut assignments, &mut directives);
directives.push(ContainerEnvironmentDirective::Reset(*reset));
}
}
}
flush(&mut assignments, &mut directives);
Self { directives }
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&str> {
let mut effective = None;
for directive in &self.directives {
match directive {
ContainerEnvironmentDirective::Assignment(assignment) => {
if assignment.name() == name {
effective = Some(assignment.value());
}
}
ContainerEnvironmentDirective::Assignments(assignments) => {
for assignment in assignments.iter() {
if assignment.name() == name {
effective = Some(assignment.value());
}
}
}
ContainerEnvironmentDirective::Reset(_) => effective = None,
}
}
effective
}
#[must_use]
pub fn contains(&self, name: &str) -> bool {
self.get(name).is_some()
}
#[must_use]
pub fn len(&self) -> usize {
let mut names = BTreeSet::new();
for directive in &self.directives {
match directive {
ContainerEnvironmentDirective::Assignment(assignment) => {
names.insert(assignment.name());
}
ContainerEnvironmentDirective::Assignments(assignments) => {
names.extend(assignments.iter().map(EnvironmentAssignment::name));
}
ContainerEnvironmentDirective::Reset(_) => names.clear(),
}
}
names.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl fmt::Debug for ContainerEnvironmentPlan {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ContainerEnvironmentPlan")
.field("directives", &self.directives)
.finish()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum EnvironmentAssignmentsError {
Empty,
}
impl fmt::Display for EnvironmentAssignmentsError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("an environment assignment group must not be empty"),
}
}
}
impl Error for EnvironmentAssignmentsError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum EnvironmentAssignmentError {
InvalidName,
Nul,
CarriageReturn,
LineFeed,
ControlCharacter,
Specifier,
}
impl fmt::Display for EnvironmentAssignmentError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidName => formatter.write_str("environment names must match ASCII [A-Za-z_][A-Za-z0-9_]*"),
Self::Nul => formatter.write_str("environment values must not contain NUL bytes"),
Self::CarriageReturn => formatter.write_str("environment values must not contain carriage returns"),
Self::LineFeed => formatter.write_str("environment values must not contain line feeds"),
Self::ControlCharacter => formatter.write_str("environment values must not contain control characters"),
Self::Specifier => formatter.write_str("environment values must not contain systemd specifiers"),
}
}
}
impl Error for EnvironmentAssignmentError {}
fn is_environment_name(name: &str) -> bool {
let mut bytes = name.bytes();
matches!(bytes.next(), Some(byte) if byte.is_ascii_alphabetic() || byte == b'_')
&& bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PidsLimit(String);
impl PidsLimit {
#[must_use]
pub fn unlimited() -> Self {
Self("-1".to_owned())
}
pub fn finite(limit: impl Into<String>) -> Result<Self, PidsLimitError> {
let limit = limit.into();
if limit.is_empty() {
return Err(PidsLimitError::Empty);
}
if !limit.bytes().all(|byte| byte.is_ascii_digit()) {
return Err(PidsLimitError::NonDecimal);
}
if !limit.bytes().any(|byte| byte != b'0') {
return Err(PidsLimitError::Zero);
}
Ok(Self(limit))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<PidsLimit> for EntryValue {
fn from(limit: PidsLimit) -> Self {
Self(limit.0)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum PidsLimitError {
Empty,
NonDecimal,
Zero,
}
impl fmt::Display for PidsLimitError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("a finite process-ID limit must not be empty"),
Self::NonDecimal => formatter.write_str("a finite process-ID limit must contain only ASCII decimal digits"),
Self::Zero => formatter.write_str("a finite process-ID limit must be positive"),
}
}
}
impl Error for PidsLimitError {}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ShmSize(String);
impl ShmSize {
pub fn new(size: impl Into<String>) -> Result<Self, ShmSizeError> {
let size = size.into();
if size.is_empty() {
return Err(ShmSizeError::Empty);
}
let amount = match size.as_bytes().last() {
Some(b'b' | b'k' | b'm' | b'g') => &size[..size.len() - 1],
_ => size.as_str(),
};
if amount.is_empty() || !amount.bytes().all(|byte| byte.is_ascii_digit()) {
return Err(ShmSizeError::InvalidFormat);
}
Ok(Self(size))
}
#[must_use]
pub fn unlimited() -> Self {
Self("0".to_owned())
}
#[must_use]
pub fn is_unlimited(&self) -> bool {
let amount = match self.0.as_bytes().last() {
Some(b'b' | b'k' | b'm' | b'g') => &self.0[..self.0.len() - 1],
_ => self.0.as_str(),
};
amount.bytes().all(|byte| byte == b'0')
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<ShmSize> for EntryValue {
fn from(size: ShmSize) -> Self {
Self(size.0)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ShmSizeError {
Empty,
InvalidFormat,
}
impl fmt::Display for ShmSizeError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("a shared-memory size must not be empty"),
Self::InvalidFormat => formatter
.write_str("a shared-memory size must be an ASCII decimal amount with optional unit b, k, m, or g"),
}
}
}
impl Error for ShmSizeError {}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Memory(String);
impl Memory {
pub fn new(memory: impl Into<String>) -> Result<Self, MemoryError> {
let memory = memory.into();
if memory.is_empty() {
return Err(MemoryError::Empty);
}
let amount = match memory.as_bytes().last() {
Some(b'b' | b'k' | b'm' | b'g') => &memory[..memory.len() - 1],
_ => memory.as_str(),
};
if amount.is_empty() || !amount.bytes().all(|byte| byte.is_ascii_digit()) {
return Err(MemoryError::InvalidFormat);
}
if !amount.bytes().any(|byte| byte != b'0') {
return Err(MemoryError::Zero);
}
Ok(Self(memory))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<Memory> for EntryValue {
fn from(memory: Memory) -> Self {
Self(memory.0)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum MemoryError {
Empty,
InvalidFormat,
Zero,
}
impl fmt::Display for MemoryError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("a memory limit must not be empty"),
Self::InvalidFormat => {
formatter.write_str("a memory limit must be an ASCII decimal amount with optional unit b, k, m, or g")
}
Self::Zero => formatter.write_str("a memory limit must be positive"),
}
}
}
impl Error for MemoryError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum SystemdSection {
Unit,
Service,
Install,
}
impl SystemdSection {
const fn kind(self) -> SectionKind {
match self {
Self::Unit => SectionKind::Unit,
Self::Service => SectionKind::Service,
Self::Install => SectionKind::Install,
}
}
}
#[derive(Clone, Eq, PartialEq)]
struct GeneratedEntry {
section: SectionKind,
kind: EntryKind,
key: String,
value: EntryValue,
}
impl fmt::Debug for GeneratedEntry {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut debug = formatter.debug_struct("GeneratedEntry");
debug
.field("section", &self.section)
.field("kind", &self.kind)
.field("key", &self.key);
if self.kind.has_sensitive_value() {
debug.field("value", &"<redacted sensitive value>")
} else {
debug.field("value", &self.value)
};
debug.finish()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct QuadletDocumentBuilder {
unit_type: QuadletUnitType,
entries: Vec<GeneratedEntry>,
}
impl QuadletDocumentBuilder {
#[must_use]
pub const fn new(unit_type: QuadletUnitType) -> Self {
Self {
unit_type,
entries: Vec::new(),
}
}
#[must_use]
pub const fn unit_type(&self) -> QuadletUnitType {
self.unit_type
}
pub fn push_container(&mut self, key: ContainerKey, value: EntryValue) -> Result<(), RenderError> {
let attempted = container_key_name(key);
if let Some(existing) = match key {
ContainerKey::ReloadCmd => self.entries.iter().find_map(|entry| {
(entry.kind == EntryKind::Container(ContainerKey::ReloadSignal)).then_some("ReloadSignal")
}),
ContainerKey::ReloadSignal => self
.entries
.iter()
.find_map(|entry| (entry.kind == EntryKind::Container(ContainerKey::ReloadCmd)).then_some("ReloadCmd")),
_ => None,
} {
return Err(RenderError::ConflictingSingletons {
existing: existing.to_owned(),
attempted: attempted.to_owned(),
});
}
self.push_native(
QuadletUnitType::Container,
SectionKind::Container,
EntryKind::Container(key),
container_key_name(key),
value,
)
}
pub fn push_container_environment(&mut self, assignment: EnvironmentAssignment) -> Result<(), RenderError> {
self.push_container(ContainerKey::Environment, assignment.into())
}
pub fn push_container_environment_assignments(
&mut self,
assignments: EnvironmentAssignments,
) -> Result<(), RenderError> {
self.push_container(ContainerKey::Environment, assignments.into())
}
pub fn push_container_environment_reset(&mut self) -> Result<(), RenderError> {
self.push_container(ContainerKey::Environment, EnvironmentReset::new().into())
}
pub fn push_container_environment_plan(&mut self, plan: &ContainerEnvironmentPlan) -> Result<(), RenderError> {
if self.unit_type != QuadletUnitType::Container {
return Err(RenderError::WrongUnitType {
document: self.unit_type,
entry: QuadletUnitType::Container,
});
}
for directive in plan.directives() {
match directive {
ContainerEnvironmentDirective::Assignment(assignment) => {
self.push_container_environment(assignment.clone())?;
}
ContainerEnvironmentDirective::Assignments(assignments) => {
self.push_container_environment_assignments(assignments.clone())?;
}
ContainerEnvironmentDirective::Reset(_) => self.push_container_environment_reset()?,
}
}
Ok(())
}
pub fn push_pod(&mut self, key: PodKey, value: EntryValue) -> Result<(), RenderError> {
self.push_native(
QuadletUnitType::Pod,
SectionKind::Pod,
EntryKind::Pod(key),
pod_key_name(key),
value,
)
}
pub fn push_network(&mut self, key: NetworkKey, value: EntryValue) -> Result<(), RenderError> {
self.push_native(
QuadletUnitType::Network,
SectionKind::Network,
EntryKind::Network(key),
network_key_name(key),
value,
)
}
pub fn push_volume(&mut self, key: VolumeKey, value: EntryValue) -> Result<(), RenderError> {
self.push_native(
QuadletUnitType::Volume,
SectionKind::Volume,
EntryKind::Volume(key),
volume_key_name(key),
value,
)
}
pub fn push_build(&mut self, key: BuildKey, value: EntryValue) -> Result<(), RenderError> {
self.push_native(
QuadletUnitType::Build,
SectionKind::Build,
EntryKind::Build(key),
build_key_name(key),
value,
)
}
pub fn push_image(&mut self, key: ImageKey, value: EntryValue) -> Result<(), RenderError> {
self.push_native(
QuadletUnitType::Image,
SectionKind::Image,
EntryKind::Image(key),
image_key_name(key),
value,
)
}
pub fn push_kube(&mut self, key: KubeKey, value: EntryValue) -> Result<(), RenderError> {
self.push_native(
QuadletUnitType::Kube,
SectionKind::Kube,
EntryKind::Kube(key),
kube_key_name(key),
value,
)
}
pub fn push_artifact(&mut self, key: ArtifactKey, value: EntryValue) -> Result<(), RenderError> {
self.push_native(
QuadletUnitType::Artifact,
SectionKind::Artifact,
EntryKind::Artifact(key),
artifact_key_name(key),
value,
)
}
pub fn push_quadlet(&mut self, key: QuadletKey, value: EntryValue) -> Result<(), RenderError> {
self.push_generated(
SectionKind::Quadlet,
EntryKind::Quadlet(key),
quadlet_key_name(key),
value,
)
}
pub fn push_systemd(
&mut self,
section: SystemdSection,
key: impl Into<String>,
value: EntryValue,
) -> Result<(), RenderError> {
let key = key.into();
if key.is_empty() || !key.bytes().all(|byte| byte.is_ascii_alphanumeric()) {
return Err(RenderError::InvalidKey(key));
}
self.entries.push(GeneratedEntry {
section: section.kind(),
kind: EntryKind::GenericSystemd,
key,
value,
});
Ok(())
}
pub fn push_systemd_unit(&mut self, key: SystemdUnitKey, value: EntryValue) -> Result<(), RenderError> {
self.push_generated(SectionKind::Unit, EntryKind::SystemdUnit(key), key.name(), value)
}
pub fn build(&self, source_id: SourceId) -> Result<GeneratedQuadletDocument, RenderError> {
let text = self.render_text();
let parsed = QuadletDocument::parse(self.unit_type, source_id, text).map_err(RenderError::TypedModel)?;
if !parsed.is_valid() {
let mut diagnostics = parsed.syntax().diagnostics().to_vec();
diagnostics.extend_from_slice(parsed.model_diagnostics());
return Err(RenderError::InvalidDocument(diagnostics));
}
Ok(GeneratedQuadletDocument { parsed })
}
fn push_native(
&mut self,
required: QuadletUnitType,
section: SectionKind,
kind: EntryKind,
key: &'static str,
value: EntryValue,
) -> Result<(), RenderError> {
if self.unit_type != required {
return Err(RenderError::WrongUnitType {
document: self.unit_type,
entry: required,
});
}
self.push_generated(section, kind, key, value)
}
fn push_generated(
&mut self,
section: SectionKind,
kind: EntryKind,
key: &str,
value: EntryValue,
) -> Result<(), RenderError> {
if !kind.is_repeatable() && self.entries.iter().any(|entry| entry.kind == kind) {
return Err(RenderError::DuplicateSingleton(key.to_owned()));
}
self.entries.push(GeneratedEntry {
section,
kind,
key: key.to_owned(),
value,
});
Ok(())
}
fn render_text(&self) -> String {
let native = self.unit_type.native_section();
let sections = [
SectionKind::Unit,
SectionKind::Quadlet,
native,
SectionKind::Service,
SectionKind::Install,
];
let mut output = String::new();
let mut wrote_section = false;
for section in sections {
let entries: Vec<_> = self.entries.iter().filter(|entry| entry.section == section).collect();
if entries.is_empty() && section != native {
continue;
}
if wrote_section {
output.push('\n');
}
wrote_section = true;
output.push('[');
output.push_str(section_name(section));
output.push_str("]\n");
for entry in entries {
output.push_str(&entry.key);
output.push('=');
output.push_str(entry.value.as_str());
output.push('\n');
}
}
output
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GeneratedQuadletDocument {
parsed: QuadletParseResult,
}
impl GeneratedQuadletDocument {
#[must_use]
pub fn text(&self) -> &str {
self.parsed.syntax().document().render_preserved()
}
#[must_use]
pub const fn document(&self) -> &QuadletDocument {
self.parsed.document()
}
#[must_use]
pub const fn parse_result(&self) -> &QuadletParseResult {
&self.parsed
}
#[must_use]
pub fn into_parse_result(self) -> QuadletParseResult {
self.parsed
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum RenderError {
InvalidValue,
InvalidKey(String),
WrongUnitType {
document: QuadletUnitType,
entry: QuadletUnitType,
},
DuplicateSingleton(String),
ConflictingSingletons {
existing: String,
attempted: String,
},
InvalidDocument(Vec<Diagnostic>),
TypedModel(TypedModelError),
}
impl RenderError {
#[must_use]
pub fn diagnostics(&self) -> &[Diagnostic] {
match self {
Self::InvalidDocument(diagnostics) => diagnostics,
_ => &[],
}
}
}
impl fmt::Display for RenderError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidValue => formatter.write_str("generated Quadlet values must fit on one physical line"),
Self::InvalidKey(key) => write!(formatter, "invalid generic systemd key `{key}`"),
Self::WrongUnitType { document, entry } => {
write!(formatter, "cannot add a {entry:?} entry to a {document:?} document")
}
Self::DuplicateSingleton(key) => write!(formatter, "singleton Quadlet key `{key}` is repeated"),
Self::ConflictingSingletons { existing, attempted } => {
write!(
formatter,
"singleton Quadlet keys `{existing}` and `{attempted}` conflict"
)
}
Self::InvalidDocument(diagnostics) => {
write!(
formatter,
"generated Quadlet document has {} diagnostic(s)",
diagnostics.len()
)
}
Self::TypedModel(error) => write!(formatter, "generated Quadlet model is inconsistent: {error}"),
}
}
}
impl Error for RenderError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::TypedModel(error) => Some(error),
_ => None,
}
}
}
const fn container_key_name(key: ContainerKey) -> &'static str {
match key {
ContainerKey::AddHost => "AddHost",
ContainerKey::Image => "Image",
ContainerKey::Exec => "Exec",
ContainerKey::Environment => "Environment",
ContainerKey::EnvironmentFile => "EnvironmentFile",
ContainerKey::Label => "Label",
ContainerKey::Secret => "Secret",
ContainerKey::PublishPort => "PublishPort",
ContainerKey::Volume => "Volume",
ContainerKey::Network => "Network",
ContainerKey::Pod => "Pod",
ContainerKey::HealthCmd => "HealthCmd",
ContainerKey::Notify => "Notify",
ContainerKey::HealthInterval => "HealthInterval",
ContainerKey::HealthRetries => "HealthRetries",
ContainerKey::HealthStartPeriod => "HealthStartPeriod",
ContainerKey::HealthTimeout => "HealthTimeout",
ContainerKey::PodmanArgs => "PodmanArgs",
ContainerKey::User => "User",
ContainerKey::Group => "Group",
ContainerKey::UserNS => "UserNS",
ContainerKey::GroupAdd => "GroupAdd",
ContainerKey::WorkingDir => "WorkingDir",
ContainerKey::ReadOnly => "ReadOnly",
ContainerKey::Rootfs => "Rootfs",
ContainerKey::ContainerName => "ContainerName",
ContainerKey::Entrypoint => "Entrypoint",
ContainerKey::RunInit => "RunInit",
ContainerKey::StopSignal => "StopSignal",
ContainerKey::StopTimeout => "StopTimeout",
ContainerKey::Pull => "Pull",
ContainerKey::PidsLimit => "PidsLimit",
ContainerKey::HostName => "HostName",
ContainerKey::ShmSize => "ShmSize",
ContainerKey::DropCapability => "DropCapability",
ContainerKey::AddCapability => "AddCapability",
ContainerKey::Tmpfs => "Tmpfs",
ContainerKey::Sysctl => "Sysctl",
ContainerKey::Ulimit => "Ulimit",
ContainerKey::AddDevice => "AddDevice",
ContainerKey::Memory => "Memory",
ContainerKey::DNS => "DNS",
ContainerKey::DNSOption => "DNSOption",
ContainerKey::DNSSearch => "DNSSearch",
ContainerKey::ExposeHostPort => "ExposeHostPort",
ContainerKey::Annotation => "Annotation",
ContainerKey::AppArmor => "AppArmor",
ContainerKey::NoNewPrivileges => "NoNewPrivileges",
ContainerKey::SeccompProfile => "SeccompProfile",
ContainerKey::SecurityLabelDisable => "SecurityLabelDisable",
ContainerKey::SecurityLabelFileType => "SecurityLabelFileType",
ContainerKey::SecurityLabelLevel => "SecurityLabelLevel",
ContainerKey::SecurityLabelNested => "SecurityLabelNested",
ContainerKey::SecurityLabelType => "SecurityLabelType",
ContainerKey::Mask => "Mask",
ContainerKey::Unmask => "Unmask",
ContainerKey::LogDriver => "LogDriver",
ContainerKey::LogOpt => "LogOpt",
ContainerKey::IP => "IP",
ContainerKey::IP6 => "IP6",
ContainerKey::NetworkAlias => "NetworkAlias",
ContainerKey::ReloadCmd => "ReloadCmd",
ContainerKey::ReloadSignal => "ReloadSignal",
ContainerKey::AutoUpdate => "AutoUpdate",
ContainerKey::CgroupsMode => "CgroupsMode",
ContainerKey::EnvironmentHost => "EnvironmentHost",
ContainerKey::GIDMap => "GIDMap",
ContainerKey::HttpProxy => "HttpProxy",
ContainerKey::Mount => "Mount",
ContainerKey::ReadOnlyTmpfs => "ReadOnlyTmpfs",
ContainerKey::Retry => "Retry",
ContainerKey::RetryDelay => "RetryDelay",
ContainerKey::StartWithPod => "StartWithPod",
ContainerKey::SubGIDMap => "SubGIDMap",
ContainerKey::SubUIDMap => "SubUIDMap",
ContainerKey::Timezone => "Timezone",
ContainerKey::UIDMap => "UIDMap",
ContainerKey::HealthOnFailure => "HealthOnFailure",
ContainerKey::ContainersConfModule => "ContainersConfModule",
ContainerKey::GlobalArgs => "GlobalArgs",
ContainerKey::HealthLogDestination => "HealthLogDestination",
ContainerKey::HealthMaxLogCount => "HealthMaxLogCount",
ContainerKey::HealthMaxLogSize => "HealthMaxLogSize",
ContainerKey::HealthStartupCmd => "HealthStartupCmd",
ContainerKey::HealthStartupInterval => "HealthStartupInterval",
ContainerKey::HealthStartupRetries => "HealthStartupRetries",
ContainerKey::HealthStartupSuccess => "HealthStartupSuccess",
ContainerKey::HealthStartupTimeout => "HealthStartupTimeout",
ContainerKey::ImageVolume => "ImageVolume",
ContainerKey::ServiceName => "ServiceName",
}
}
const fn build_key_name(key: BuildKey) -> &'static str {
match key {
BuildKey::ImageTag => "ImageTag",
BuildKey::SetWorkingDirectory => "SetWorkingDirectory",
BuildKey::File => "File",
BuildKey::Target => "Target",
BuildKey::Network => "Network",
BuildKey::Label => "Label",
BuildKey::BuildArg => "BuildArg",
BuildKey::Secret => "Secret",
BuildKey::Arch => "Arch",
BuildKey::Variant => "Variant",
BuildKey::Pull => "Pull",
BuildKey::PodmanArgs => "PodmanArgs",
BuildKey::Retry => "Retry",
BuildKey::RetryDelay => "RetryDelay",
BuildKey::TLSVerify => "TLSVerify",
BuildKey::ForceRM => "ForceRM",
BuildKey::GroupAdd => "GroupAdd",
BuildKey::DNS => "DNS",
BuildKey::DNSOption => "DNSOption",
BuildKey::DNSSearch => "DNSSearch",
BuildKey::AuthFile => "AuthFile",
BuildKey::IgnoreFile => "IgnoreFile",
BuildKey::Annotation => "Annotation",
BuildKey::Environment => "Environment",
BuildKey::ContainersConfModule => "ContainersConfModule",
BuildKey::GlobalArgs => "GlobalArgs",
BuildKey::ServiceName => "ServiceName",
BuildKey::Volume => "Volume",
}
}
const fn image_key_name(key: ImageKey) -> &'static str {
match key {
ImageKey::Image => "Image",
ImageKey::ImageTag => "ImageTag",
ImageKey::ServiceName => "ServiceName",
ImageKey::AllTags => "AllTags",
ImageKey::Arch => "Arch",
ImageKey::AuthFile => "AuthFile",
ImageKey::CertDir => "CertDir",
ImageKey::ContainersConfModule => "ContainersConfModule",
ImageKey::Creds => "Creds",
ImageKey::DecryptionKey => "DecryptionKey",
ImageKey::GlobalArgs => "GlobalArgs",
ImageKey::OS => "OS",
ImageKey::PodmanArgs => "PodmanArgs",
ImageKey::Policy => "Policy",
ImageKey::Retry => "Retry",
ImageKey::RetryDelay => "RetryDelay",
ImageKey::TLSVerify => "TLSVerify",
ImageKey::Variant => "Variant",
}
}
const fn kube_key_name(key: KubeKey) -> &'static str {
match key {
KubeKey::AutoUpdate => "AutoUpdate",
KubeKey::ConfigMap => "ConfigMap",
KubeKey::ContainersConfModule => "ContainersConfModule",
KubeKey::ExitCodePropagation => "ExitCodePropagation",
KubeKey::GlobalArgs => "GlobalArgs",
KubeKey::KubeDownForce => "KubeDownForce",
KubeKey::LogDriver => "LogDriver",
KubeKey::Network => "Network",
KubeKey::PodmanArgs => "PodmanArgs",
KubeKey::PublishPort => "PublishPort",
KubeKey::ServiceName => "ServiceName",
KubeKey::SetWorkingDirectory => "SetWorkingDirectory",
KubeKey::UserNS => "UserNS",
KubeKey::Yaml => "Yaml",
KubeKey::LogOpt => "LogOpt",
KubeKey::RemapGid => "RemapGid",
KubeKey::RemapUid => "RemapUid",
KubeKey::RemapUidSize => "RemapUidSize",
KubeKey::RemapUsers => "RemapUsers",
}
}
const fn artifact_key_name(key: ArtifactKey) -> &'static str {
match key {
ArtifactKey::Artifact => "Artifact",
ArtifactKey::AuthFile => "AuthFile",
ArtifactKey::CertDir => "CertDir",
ArtifactKey::Creds => "Creds",
ArtifactKey::DecryptionKey => "DecryptionKey",
ArtifactKey::Quiet => "Quiet",
ArtifactKey::Retry => "Retry",
ArtifactKey::RetryDelay => "RetryDelay",
ArtifactKey::ServiceName => "ServiceName",
ArtifactKey::TLSVerify => "TLSVerify",
ArtifactKey::ContainersConfModule => "ContainersConfModule",
ArtifactKey::GlobalArgs => "GlobalArgs",
ArtifactKey::PodmanArgs => "PodmanArgs",
}
}
const fn quadlet_key_name(key: QuadletKey) -> &'static str {
match key {
QuadletKey::DefaultDependencies => "DefaultDependencies",
}
}
const fn pod_key_name(key: PodKey) -> &'static str {
match key {
PodKey::AddHost => "AddHost",
PodKey::PodName => "PodName",
PodKey::PublishPort => "PublishPort",
PodKey::Network => "Network",
PodKey::Volume => "Volume",
PodKey::UserNS => "UserNS",
PodKey::ShmSize => "ShmSize",
PodKey::ExitPolicy => "ExitPolicy",
PodKey::StopTimeout => "StopTimeout",
PodKey::ServiceName => "ServiceName",
PodKey::ContainersConfModule => "ContainersConfModule",
PodKey::DNS => "DNS",
PodKey::DNSOption => "DNSOption",
PodKey::DNSSearch => "DNSSearch",
PodKey::GIDMap => "GIDMap",
PodKey::GlobalArgs => "GlobalArgs",
PodKey::HostName => "HostName",
PodKey::IP => "IP",
PodKey::IP6 => "IP6",
PodKey::Label => "Label",
PodKey::NetworkAlias => "NetworkAlias",
PodKey::PodmanArgs => "PodmanArgs",
PodKey::SubGIDMap => "SubGIDMap",
PodKey::SubUIDMap => "SubUIDMap",
PodKey::UIDMap => "UIDMap",
}
}
const fn network_key_name(key: NetworkKey) -> &'static str {
match key {
NetworkKey::NetworkName => "NetworkName",
NetworkKey::Driver => "Driver",
NetworkKey::Options => "Options",
NetworkKey::Internal => "Internal",
NetworkKey::IPv6 => "IPv6",
NetworkKey::IPAMDriver => "IPAMDriver",
NetworkKey::Subnet => "Subnet",
NetworkKey::Gateway => "Gateway",
NetworkKey::IPRange => "IPRange",
NetworkKey::Label => "Label",
NetworkKey::ContainersConfModule => "ContainersConfModule",
NetworkKey::DisableDNS => "DisableDNS",
NetworkKey::DNS => "DNS",
NetworkKey::GlobalArgs => "GlobalArgs",
NetworkKey::InterfaceName => "InterfaceName",
NetworkKey::NetworkDeleteOnStop => "NetworkDeleteOnStop",
NetworkKey::PodmanArgs => "PodmanArgs",
NetworkKey::ServiceName => "ServiceName",
}
}
const fn volume_key_name(key: VolumeKey) -> &'static str {
match key {
VolumeKey::VolumeName => "VolumeName",
VolumeKey::Driver => "Driver",
VolumeKey::Options => "Options",
VolumeKey::Label => "Label",
VolumeKey::Device => "Device",
VolumeKey::Type => "Type",
VolumeKey::Copy => "Copy",
VolumeKey::ContainersConfModule => "ContainersConfModule",
VolumeKey::GlobalArgs => "GlobalArgs",
VolumeKey::PodmanArgs => "PodmanArgs",
VolumeKey::User => "User",
VolumeKey::Group => "Group",
VolumeKey::UID => "UID",
VolumeKey::GID => "GID",
VolumeKey::ServiceName => "ServiceName",
VolumeKey::Image => "Image",
}
}
const fn section_name(section: SectionKind) -> &'static str {
match section {
SectionKind::Unit => "Unit",
SectionKind::Container => "Container",
SectionKind::Pod => "Pod",
SectionKind::Network => "Network",
SectionKind::Volume => "Volume",
SectionKind::Build => "Build",
SectionKind::Image => "Image",
SectionKind::Kube => "Kube",
SectionKind::Artifact => "Artifact",
SectionKind::Quadlet => "Quadlet",
SectionKind::Service => "Service",
SectionKind::Install => "Install",
SectionKind::Unknown => "Unknown",
}
}