Skip to main content

use_oci_hook/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// Errors returned when hook metadata is invalid.
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum HookError {
10    Empty,
11    UnknownKind,
12    InvalidEnv,
13    InvalidTimeout,
14}
15
16impl fmt::Display for HookError {
17    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
18        match self {
19            Self::Empty => formatter.write_str("OCI hook value cannot be empty"),
20            Self::UnknownKind => formatter.write_str("unknown OCI hook kind"),
21            Self::InvalidEnv => formatter.write_str("invalid OCI hook environment entry"),
22            Self::InvalidTimeout => formatter.write_str("invalid OCI hook timeout"),
23        }
24    }
25}
26
27impl Error for HookError {}
28
29/// OCI runtime hook lifecycle phase.
30#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
31pub enum HookKind {
32    Prestart,
33    CreateRuntime,
34    CreateContainer,
35    StartContainer,
36    Poststart,
37    Poststop,
38}
39
40impl HookKind {
41    /// Returns the OCI hook kind label.
42    #[must_use]
43    pub const fn as_str(self) -> &'static str {
44        match self {
45            Self::Prestart => "prestart",
46            Self::CreateRuntime => "createRuntime",
47            Self::CreateContainer => "createContainer",
48            Self::StartContainer => "startContainer",
49            Self::Poststart => "poststart",
50            Self::Poststop => "poststop",
51        }
52    }
53}
54
55impl fmt::Display for HookKind {
56    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
57        formatter.write_str(self.as_str())
58    }
59}
60
61impl FromStr for HookKind {
62    type Err = HookError;
63
64    fn from_str(value: &str) -> Result<Self, Self::Err> {
65        let key = value.trim().replace(['-', '_'], "").to_ascii_lowercase();
66        match key.as_str() {
67            "prestart" => Ok(Self::Prestart),
68            "createruntime" => Ok(Self::CreateRuntime),
69            "createcontainer" => Ok(Self::CreateContainer),
70            "startcontainer" => Ok(Self::StartContainer),
71            "poststart" => Ok(Self::Poststart),
72            "poststop" => Ok(Self::Poststop),
73            "" => Err(HookError::Empty),
74            _ => Err(HookError::UnknownKind),
75        }
76    }
77}
78
79macro_rules! text_part {
80    ($name:ident) => {
81        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
82        pub struct $name(String);
83
84        impl $name {
85            /// Creates a non-empty hook text value.
86            pub fn new(value: impl AsRef<str>) -> Result<Self, HookError> {
87                let trimmed = value.as_ref().trim();
88                if trimmed.is_empty() {
89                    return Err(HookError::Empty);
90                }
91                if trimmed.contains('\0') {
92                    return Err(HookError::InvalidEnv);
93                }
94                Ok(Self(trimmed.to_string()))
95            }
96
97            /// Returns the text value.
98            #[must_use]
99            pub fn as_str(&self) -> &str {
100                &self.0
101            }
102        }
103
104        impl AsRef<str> for $name {
105            fn as_ref(&self) -> &str {
106                self.as_str()
107            }
108        }
109
110        impl fmt::Display for $name {
111            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
112                formatter.write_str(self.as_str())
113            }
114        }
115    };
116}
117
118text_part!(HookPath);
119text_part!(HookArg);
120text_part!(HookEnv);
121
122/// Hook timeout in seconds.
123#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
124pub struct HookTimeoutSeconds(u32);
125
126impl HookTimeoutSeconds {
127    /// Creates a positive timeout value.
128    pub const fn new(value: u32) -> Result<Self, HookError> {
129        if value == 0 {
130            Err(HookError::InvalidTimeout)
131        } else {
132            Ok(Self(value))
133        }
134    }
135
136    /// Returns the timeout seconds.
137    #[must_use]
138    pub const fn as_u32(self) -> u32 {
139        self.0
140    }
141}
142
143/// OCI hook metadata. This type does not execute hooks.
144#[derive(Clone, Debug, Eq, PartialEq)]
145pub struct OciHook {
146    kind: HookKind,
147    path: HookPath,
148    args: Vec<HookArg>,
149    env: Vec<HookEnv>,
150    timeout: Option<HookTimeoutSeconds>,
151}
152
153impl OciHook {
154    /// Creates hook metadata.
155    #[must_use]
156    pub fn new(kind: HookKind, path: HookPath) -> Self {
157        Self {
158            kind,
159            path,
160            args: Vec::new(),
161            env: Vec::new(),
162            timeout: None,
163        }
164    }
165
166    /// Adds an argument.
167    #[must_use]
168    pub fn with_arg(mut self, arg: HookArg) -> Self {
169        self.args.push(arg);
170        self
171    }
172
173    /// Adds an environment entry.
174    #[must_use]
175    pub fn with_env(mut self, env: HookEnv) -> Self {
176        self.env.push(env);
177        self
178    }
179
180    /// Adds a timeout.
181    #[must_use]
182    pub const fn with_timeout(mut self, timeout: HookTimeoutSeconds) -> Self {
183        self.timeout = Some(timeout);
184        self
185    }
186
187    /// Returns the hook kind.
188    #[must_use]
189    pub const fn kind(&self) -> HookKind {
190        self.kind
191    }
192
193    /// Returns the hook path.
194    #[must_use]
195    pub const fn path(&self) -> &HookPath {
196        &self.path
197    }
198
199    /// Returns hook args.
200    #[must_use]
201    pub fn args(&self) -> &[HookArg] {
202        &self.args
203    }
204
205    /// Returns hook environment entries.
206    #[must_use]
207    pub fn env(&self) -> &[HookEnv] {
208        &self.env
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::{HookArg, HookKind, HookPath, HookTimeoutSeconds, OciHook};
215
216    #[test]
217    fn models_hook_metadata_without_execution() -> Result<(), Box<dyn std::error::Error>> {
218        let hook = OciHook::new(HookKind::Prestart, HookPath::new("/bin/check")?)
219            .with_arg(HookArg::new("--dry-run")?)
220            .with_timeout(HookTimeoutSeconds::new(5)?);
221
222        assert_eq!(hook.kind().to_string(), "prestart");
223        assert_eq!(hook.path().as_str(), "/bin/check");
224        assert_eq!(hook.args().len(), 1);
225        assert_eq!(
226            "create-runtime".parse::<HookKind>()?,
227            HookKind::CreateRuntime
228        );
229        Ok(())
230    }
231}