#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::fmt;
use std::error::Error;
use use_oci_annotation::Annotation;
use use_oci_platform::{OciArchitecture, OciOs};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ConfigError {
Empty,
InvalidEnv,
InvalidPort,
}
impl fmt::Display for ConfigError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("OCI config value cannot be empty"),
Self::InvalidEnv => formatter.write_str("invalid OCI config environment entry"),
Self::InvalidPort => formatter.write_str("invalid OCI exposed port"),
}
}
}
impl Error for ConfigError {}
macro_rules! text_value {
($name:ident) => {
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct $name(String);
impl $name {
pub fn new(value: impl AsRef<str>) -> Result<Self, ConfigError> {
let trimmed = value.as_ref().trim();
if trimmed.is_empty() {
return Err(ConfigError::Empty);
}
if trimmed.contains('\0') {
return Err(ConfigError::InvalidEnv);
}
Ok(Self(trimmed.to_string()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for $name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
};
}
text_value!(ConfigUser);
text_value!(Entrypoint);
text_value!(Command);
text_value!(WorkingDir);
text_value!(VolumePath);
text_value!(StopSignal);
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct EnvVar {
key: String,
value: String,
}
impl EnvVar {
pub fn new(key: impl AsRef<str>, value: impl AsRef<str>) -> Result<Self, ConfigError> {
let key = key.as_ref().trim();
if key.is_empty()
|| key.contains('=')
|| key
.bytes()
.any(|byte| byte.is_ascii_control() || byte.is_ascii_whitespace())
{
return Err(ConfigError::InvalidEnv);
}
let value = value.as_ref();
if value.contains('\0') {
return Err(ConfigError::InvalidEnv);
}
Ok(Self {
key: key.to_string(),
value: value.to_string(),
})
}
#[must_use]
pub fn key(&self) -> &str {
&self.key
}
#[must_use]
pub fn value(&self) -> &str {
&self.value
}
}
impl fmt::Display for EnvVar {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}={}", self.key, self.value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ExposedPort(String);
impl ExposedPort {
pub fn new(value: impl AsRef<str>) -> Result<Self, ConfigError> {
let trimmed = value.as_ref().trim().to_ascii_lowercase();
let Some((number, protocol)) = trimmed.split_once('/') else {
return Err(ConfigError::InvalidPort);
};
if number.parse::<u16>().is_err() || !matches!(protocol, "tcp" | "udp" | "sctp") {
return Err(ConfigError::InvalidPort);
}
Ok(Self(trimmed))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OciImageConfig {
architecture: OciArchitecture,
os: OciOs,
user: Option<ConfigUser>,
env: Vec<EnvVar>,
entrypoint: Vec<Entrypoint>,
command: Vec<Command>,
working_dir: Option<WorkingDir>,
exposed_ports: Vec<ExposedPort>,
labels: Vec<Annotation>,
volumes: Vec<VolumePath>,
stop_signal: Option<StopSignal>,
annotations: Vec<Annotation>,
}
impl OciImageConfig {
#[must_use]
pub fn new(architecture: OciArchitecture, os: OciOs) -> Self {
Self {
architecture,
os,
user: None,
env: Vec::new(),
entrypoint: Vec::new(),
command: Vec::new(),
working_dir: None,
exposed_ports: Vec::new(),
labels: Vec::new(),
volumes: Vec::new(),
stop_signal: None,
annotations: Vec::new(),
}
}
#[must_use]
pub fn with_user(mut self, user: ConfigUser) -> Self {
self.user = Some(user);
self
}
#[must_use]
pub fn with_env(mut self, env: EnvVar) -> Self {
self.env.push(env);
self
}
#[must_use]
pub fn with_entrypoint(mut self, entrypoint: Entrypoint) -> Self {
self.entrypoint.push(entrypoint);
self
}
#[must_use]
pub fn with_command(mut self, command: Command) -> Self {
self.command.push(command);
self
}
#[must_use]
pub fn with_exposed_port(mut self, port: ExposedPort) -> Self {
self.exposed_ports.push(port);
self
}
#[must_use]
pub fn with_annotation(mut self, annotation: Annotation) -> Self {
self.annotations.push(annotation);
self
}
#[must_use]
pub const fn architecture(&self) -> &OciArchitecture {
&self.architecture
}
#[must_use]
pub const fn os(&self) -> &OciOs {
&self.os
}
#[must_use]
pub fn env(&self) -> &[EnvVar] {
&self.env
}
#[must_use]
pub fn annotations(&self) -> &[Annotation] {
&self.annotations
}
}
#[cfg(test)]
mod tests {
use super::{ConfigError, EnvVar, ExposedPort, OciImageConfig};
use use_oci_platform::{OciArchitecture, OciOs};
#[test]
fn models_image_config_metadata() -> Result<(), Box<dyn std::error::Error>> {
let config = OciImageConfig::new(OciArchitecture::Amd64, OciOs::Linux)
.with_env(EnvVar::new("RUST_LOG", "info")?)
.with_exposed_port(ExposedPort::new("8080/tcp")?);
assert_eq!(config.architecture(), &OciArchitecture::Amd64);
assert_eq!(config.env()[0].to_string(), "RUST_LOG=info");
assert_eq!(
EnvVar::new("BAD KEY", "value"),
Err(ConfigError::InvalidEnv)
);
assert_eq!(ExposedPort::new("tcp/8080"), Err(ConfigError::InvalidPort));
Ok(())
}
}