#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HookError {
Empty,
UnknownKind,
InvalidEnv,
InvalidTimeout,
}
impl fmt::Display for HookError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("OCI hook value cannot be empty"),
Self::UnknownKind => formatter.write_str("unknown OCI hook kind"),
Self::InvalidEnv => formatter.write_str("invalid OCI hook environment entry"),
Self::InvalidTimeout => formatter.write_str("invalid OCI hook timeout"),
}
}
}
impl Error for HookError {}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum HookKind {
Prestart,
CreateRuntime,
CreateContainer,
StartContainer,
Poststart,
Poststop,
}
impl HookKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Prestart => "prestart",
Self::CreateRuntime => "createRuntime",
Self::CreateContainer => "createContainer",
Self::StartContainer => "startContainer",
Self::Poststart => "poststart",
Self::Poststop => "poststop",
}
}
}
impl fmt::Display for HookKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for HookKind {
type Err = HookError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let key = value.trim().replace(['-', '_'], "").to_ascii_lowercase();
match key.as_str() {
"prestart" => Ok(Self::Prestart),
"createruntime" => Ok(Self::CreateRuntime),
"createcontainer" => Ok(Self::CreateContainer),
"startcontainer" => Ok(Self::StartContainer),
"poststart" => Ok(Self::Poststart),
"poststop" => Ok(Self::Poststop),
"" => Err(HookError::Empty),
_ => Err(HookError::UnknownKind),
}
}
}
macro_rules! text_part {
($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, HookError> {
let trimmed = value.as_ref().trim();
if trimmed.is_empty() {
return Err(HookError::Empty);
}
if trimmed.contains('\0') {
return Err(HookError::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_part!(HookPath);
text_part!(HookArg);
text_part!(HookEnv);
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct HookTimeoutSeconds(u32);
impl HookTimeoutSeconds {
pub const fn new(value: u32) -> Result<Self, HookError> {
if value == 0 {
Err(HookError::InvalidTimeout)
} else {
Ok(Self(value))
}
}
#[must_use]
pub const fn as_u32(self) -> u32 {
self.0
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OciHook {
kind: HookKind,
path: HookPath,
args: Vec<HookArg>,
env: Vec<HookEnv>,
timeout: Option<HookTimeoutSeconds>,
}
impl OciHook {
#[must_use]
pub fn new(kind: HookKind, path: HookPath) -> Self {
Self {
kind,
path,
args: Vec::new(),
env: Vec::new(),
timeout: None,
}
}
#[must_use]
pub fn with_arg(mut self, arg: HookArg) -> Self {
self.args.push(arg);
self
}
#[must_use]
pub fn with_env(mut self, env: HookEnv) -> Self {
self.env.push(env);
self
}
#[must_use]
pub const fn with_timeout(mut self, timeout: HookTimeoutSeconds) -> Self {
self.timeout = Some(timeout);
self
}
#[must_use]
pub const fn kind(&self) -> HookKind {
self.kind
}
#[must_use]
pub const fn path(&self) -> &HookPath {
&self.path
}
#[must_use]
pub fn args(&self) -> &[HookArg] {
&self.args
}
#[must_use]
pub fn env(&self) -> &[HookEnv] {
&self.env
}
}
#[cfg(test)]
mod tests {
use super::{HookArg, HookKind, HookPath, HookTimeoutSeconds, OciHook};
#[test]
fn models_hook_metadata_without_execution() -> Result<(), Box<dyn std::error::Error>> {
let hook = OciHook::new(HookKind::Prestart, HookPath::new("/bin/check")?)
.with_arg(HookArg::new("--dry-run")?)
.with_timeout(HookTimeoutSeconds::new(5)?);
assert_eq!(hook.kind().to_string(), "prestart");
assert_eq!(hook.path().as_str(), "/bin/check");
assert_eq!(hook.args().len(), 1);
assert_eq!(
"create-runtime".parse::<HookKind>()?,
HookKind::CreateRuntime
);
Ok(())
}
}