Skip to main content

podman_lens/
runtime.rs

1//! Bounded, redaction-safe native container runtime intent retained before rendering exists.
2
3use std::fmt;
4
5use crate::{Diagnostic, DiagnosticCode, Label, PodmanLensResult, SensitiveInputReference};
6
7const MAX_ITEMS: usize = 64;
8const MAX_BYTES: usize = 4096;
9
10/// An explicitly caller-declassified health command string.
11#[derive(Clone, Debug, Eq, PartialEq)]
12pub struct PublicHealthCommand(String);
13
14impl PublicHealthCommand {
15    /// Creates one bounded, non-control public health command value.
16    ///
17    /// # Errors
18    ///
19    /// Returns `PLN0034` for an empty, oversized, or control-containing value.
20    pub fn new(value: impl Into<String>) -> PodmanLensResult<Self> {
21        let value = value.into();
22        if value.is_empty() || !valid_text(&value) {
23            return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
24        }
25        Ok(Self(value))
26    }
27
28    /// Returns the public command value.
29    #[must_use]
30    pub fn as_str(&self) -> &str {
31        &self.0
32    }
33}
34
35/// A health command value retained only for planning and never exposed by `Debug`.
36#[derive(Clone, Eq, PartialEq)]
37pub struct SensitiveInlineHealthCommand(String);
38
39impl SensitiveInlineHealthCommand {
40    /// Creates one bounded sensitive health command value.
41    ///
42    /// # Errors
43    ///
44    /// Returns `PLN0034` for an empty, oversized, or control-containing value.
45    pub fn new(value: impl Into<String>) -> PodmanLensResult<Self> {
46        let value = value.into();
47        if value.is_empty() || !valid_text(&value) {
48            return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
49        }
50        Ok(Self(value))
51    }
52}
53
54impl fmt::Debug for SensitiveInlineHealthCommand {
55    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
56        formatter.write_str("SensitiveInlineHealthCommand([redacted])")
57    }
58}
59
60/// An explicitly caller-declassified direct health command array.
61#[derive(Clone, Debug, Eq, PartialEq)]
62pub struct PublicHealthArgumentArray(Vec<String>);
63
64impl PublicHealthArgumentArray {
65    /// Creates a nonempty, bounded direct health command array.
66    ///
67    /// # Errors
68    ///
69    /// Returns `PLN0034` for invalid array boundaries or unsafe arguments.
70    pub fn new<I, S>(arguments: I) -> PodmanLensResult<Self>
71    where
72        I: IntoIterator<Item = S>,
73        S: Into<String>,
74    {
75        let arguments = arguments.into_iter().map(Into::into).collect::<Vec<_>>();
76        validate_arguments(&arguments)?;
77        Ok(Self(arguments))
78    }
79
80    /// Returns direct command arguments in declared order.
81    #[must_use]
82    pub fn values(&self) -> &[String] {
83        &self.0
84    }
85}
86
87/// A sensitive inline direct health command array that redacts every argument in `Debug`.
88#[derive(Clone, Eq, PartialEq)]
89pub struct SensitiveInlineHealthArgumentArray(Vec<String>);
90
91impl SensitiveInlineHealthArgumentArray {
92    /// Creates a nonempty, bounded sensitive direct health command array.
93    ///
94    /// # Errors
95    ///
96    /// Returns `PLN0034` for invalid array boundaries or unsafe arguments.
97    pub fn new<I, S>(arguments: I) -> PodmanLensResult<Self>
98    where
99        I: IntoIterator<Item = S>,
100        S: Into<String>,
101    {
102        let arguments = arguments.into_iter().map(Into::into).collect::<Vec<_>>();
103        validate_arguments(&arguments)?;
104        Ok(Self(arguments))
105    }
106}
107
108impl fmt::Debug for SensitiveInlineHealthArgumentArray {
109    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
110        formatter.write_str("SensitiveInlineHealthArgumentArray([redacted])")
111    }
112}
113
114/// One declared health command, preserving shell and exec syntax distinctions.
115#[derive(Clone, Debug, Eq, PartialEq)]
116#[non_exhaustive]
117pub enum HealthCommand {
118    /// A caller-authorized shell command.
119    Shell(PublicHealthCommand),
120    /// A caller-authorized direct executable and argument array.
121    Exec(PublicHealthArgumentArray),
122    /// An inline sensitive shell command, redacted in all debug output.
123    SensitiveInlineShell(SensitiveInlineHealthCommand),
124    /// An inline sensitive direct command, redacted in all debug output.
125    SensitiveInlineExec(SensitiveInlineHealthArgumentArray),
126    /// A caller-owned sensitive shell-command source, redacted in all debug output.
127    ExternalShell(SensitiveInputReference),
128    /// A caller-owned sensitive direct-command source, redacted in all debug output.
129    ExternalExec(SensitiveInputReference),
130}
131
132/// A positive health duration encoded in bounded native signed nanoseconds.
133#[derive(Clone, Copy, Debug, Eq, PartialEq)]
134pub struct HealthDuration(i64);
135
136impl HealthDuration {
137    /// Creates one positive native duration.
138    ///
139    /// # Errors
140    ///
141    /// Returns `PLN0034` for zero or values outside the native signed range.
142    pub fn new(nanoseconds: u64) -> PodmanLensResult<Self> {
143        if nanoseconds == 0 || nanoseconds > i64::MAX as u64 {
144            return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
145        }
146        Ok(Self(i64::try_from(nanoseconds).map_err(|_| {
147            Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent)
148        })?))
149    }
150
151    /// Returns nanoseconds.
152    #[must_use]
153    pub const fn nanoseconds(&self) -> i64 {
154        self.0
155    }
156}
157
158/// A bounded native normal-health retry count.
159#[derive(Clone, Copy, Debug, Eq, PartialEq)]
160pub struct HealthRetries(u32);
161
162impl HealthRetries {
163    /// Creates one retry count.
164    ///
165    /// # Errors
166    ///
167    /// Returns `PLN0034` for zero or outside the native signed range.
168    pub const fn new(value: u32) -> PodmanLensResult<Self> {
169        if value == 0 || value > i32::MAX as u32 {
170            return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
171        }
172        Ok(Self(value))
173    }
174    /// Returns the count.
175    #[must_use]
176    pub const fn value(&self) -> u32 {
177        self.0
178    }
179}
180
181/// A bounded startup-health retry count. Zero is valid.
182#[derive(Clone, Copy, Debug, Eq, PartialEq)]
183pub struct StartupHealthRetries(u32);
184
185impl StartupHealthRetries {
186    /// Creates one startup retry count.
187    ///
188    /// # Errors
189    ///
190    /// Returns `PLN0034` outside the native signed range.
191    pub const fn new(value: u32) -> PodmanLensResult<Self> {
192        if value > i32::MAX as u32 {
193            return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
194        }
195        Ok(Self(value))
196    }
197    /// Returns the count.
198    #[must_use]
199    pub const fn value(&self) -> u32 {
200        self.0
201    }
202}
203
204/// A bounded startup-health success threshold. Zero is valid.
205#[derive(Clone, Copy, Debug, Eq, PartialEq)]
206pub struct StartupHealthSuccesses(u32);
207
208impl StartupHealthSuccesses {
209    /// Creates one threshold.
210    ///
211    /// # Errors
212    ///
213    /// Returns `PLN0034` outside the native signed range.
214    pub const fn new(value: u32) -> PodmanLensResult<Self> {
215        if value > i32::MAX as u32 {
216            return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
217        }
218        Ok(Self(value))
219    }
220    /// Returns the count.
221    #[must_use]
222    pub const fn value(&self) -> u32 {
223        self.0
224    }
225}
226
227/// A health interval encoded as disabled native `0` or a positive duration.
228#[derive(Clone, Copy, Debug, Eq, PartialEq)]
229#[non_exhaustive]
230pub enum HealthInterval {
231    /// Disables scheduled checks with native interval `0`.
232    Disabled,
233    /// Schedules checks at a positive interval.
234    Every(HealthDuration),
235}
236
237/// A native health timeout of at least one second.
238#[derive(Clone, Copy, Debug, Eq, PartialEq)]
239pub struct HealthTimeout(HealthDuration);
240
241impl HealthTimeout {
242    /// Creates one timeout of at least one second.
243    ///
244    /// # Errors
245    ///
246    /// Returns `PLN0034` below one second or outside the native signed range.
247    pub fn new(nanoseconds: u64) -> PodmanLensResult<Self> {
248        if nanoseconds < 1_000_000_000 {
249            return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
250        }
251        Ok(Self(HealthDuration::new(nanoseconds)?))
252    }
253    /// Returns nanoseconds.
254    #[must_use]
255    pub const fn nanoseconds(&self) -> i64 {
256        self.0.nanoseconds()
257    }
258}
259
260/// A normal health start period; native zero is valid.
261#[derive(Clone, Copy, Debug, Eq, PartialEq)]
262pub struct HealthStartPeriod(i64);
263
264impl HealthStartPeriod {
265    /// Creates one bounded start period.
266    ///
267    /// # Errors
268    ///
269    /// Returns `PLN0034` outside the native signed range.
270    pub fn new(nanoseconds: u64) -> PodmanLensResult<Self> {
271        if nanoseconds > i64::MAX as u64 {
272            return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
273        }
274        Ok(Self(i64::try_from(nanoseconds).map_err(|_| {
275            Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent)
276        })?))
277    }
278    /// Returns nanoseconds.
279    #[must_use]
280    pub const fn nanoseconds(&self) -> i64 {
281        self.0
282    }
283}
284
285/// The action after normal health failure.
286#[derive(Clone, Copy, Debug, Eq, PartialEq)]
287#[non_exhaustive]
288pub enum HealthOnFailure {
289    /// Retains an unhealthy state without stopping the container.
290    None,
291    /// Kills the container.
292    Kill,
293    /// Restarts the container.
294    Restart,
295    /// Stops the container.
296    Stop,
297}
298
299/// A normal health command plus bounded timing and failure fields.
300#[derive(Clone, Debug, Eq, PartialEq)]
301pub struct ConfiguredHealthCheck {
302    command: HealthCommand,
303    interval: Option<HealthInterval>,
304    timeout: Option<HealthTimeout>,
305    retries: Option<HealthRetries>,
306    start_period: Option<HealthStartPeriod>,
307    on_failure: Option<HealthOnFailure>,
308}
309
310#[allow(clippy::missing_errors_doc)]
311impl ConfiguredHealthCheck {
312    /// Starts one normal health check.
313    #[must_use]
314    pub const fn new(command: HealthCommand) -> Self {
315        Self {
316            command,
317            interval: None,
318            timeout: None,
319            retries: None,
320            start_period: None,
321            on_failure: None,
322        }
323    }
324    /// Sets interval once.
325    pub fn set_interval(&mut self, value: HealthInterval) -> PodmanLensResult<()> {
326        set_once(&mut self.interval, value)
327    }
328    /// Sets timeout once.
329    pub fn set_timeout(&mut self, value: HealthTimeout) -> PodmanLensResult<()> {
330        set_once(&mut self.timeout, value)
331    }
332    /// Sets retries once.
333    pub fn set_retries(&mut self, value: HealthRetries) -> PodmanLensResult<()> {
334        set_once(&mut self.retries, value)
335    }
336    /// Sets start period once.
337    pub fn set_start_period(&mut self, value: HealthStartPeriod) -> PodmanLensResult<()> {
338        set_once(&mut self.start_period, value)
339    }
340    /// Sets the failure action once.
341    pub fn set_on_failure(&mut self, value: HealthOnFailure) -> PodmanLensResult<()> {
342        set_once(&mut self.on_failure, value)
343    }
344    /// Returns command.
345    #[must_use]
346    pub const fn command(&self) -> &HealthCommand {
347        &self.command
348    }
349    /// Returns interval.
350    #[must_use]
351    pub const fn interval(&self) -> Option<HealthInterval> {
352        self.interval
353    }
354    /// Returns timeout.
355    #[must_use]
356    pub const fn timeout(&self) -> Option<HealthTimeout> {
357        self.timeout
358    }
359    /// Returns retries.
360    #[must_use]
361    pub const fn retries(&self) -> Option<HealthRetries> {
362        self.retries
363    }
364    /// Returns start period.
365    #[must_use]
366    pub const fn start_period(&self) -> Option<HealthStartPeriod> {
367        self.start_period
368    }
369    /// Returns failure action.
370    #[must_use]
371    pub const fn on_failure(&self) -> Option<HealthOnFailure> {
372        self.on_failure
373    }
374}
375
376/// Normal container health behavior.
377#[derive(Clone, Debug, Eq, PartialEq)]
378#[non_exhaustive]
379pub enum HealthCheck {
380    /// Explicitly disables health checking.
381    Disabled,
382    /// Uses the specified command.
383    Command(ConfiguredHealthCheck),
384}
385
386/// Startup-only health behavior; it cannot disable or replace normal health configuration.
387#[derive(Clone, Debug, Eq, PartialEq)]
388pub struct StartupHealthCheck {
389    command: HealthCommand,
390    interval: Option<HealthInterval>,
391    timeout: Option<HealthTimeout>,
392    retries: Option<StartupHealthRetries>,
393    successes: Option<StartupHealthSuccesses>,
394}
395
396#[allow(clippy::missing_errors_doc)]
397impl StartupHealthCheck {
398    /// Creates startup health behavior from a command.
399    #[must_use]
400    pub const fn new(command: HealthCommand) -> Self {
401        Self {
402            command,
403            interval: None,
404            timeout: None,
405            retries: None,
406            successes: None,
407        }
408    }
409
410    /// Sets interval once.
411    pub fn set_interval(&mut self, value: HealthInterval) -> PodmanLensResult<()> {
412        set_once(&mut self.interval, value)
413    }
414    /// Sets timeout once.
415    pub fn set_timeout(&mut self, value: HealthTimeout) -> PodmanLensResult<()> {
416        set_once(&mut self.timeout, value)
417    }
418    /// Sets retries once.
419    pub fn set_retries(&mut self, value: StartupHealthRetries) -> PodmanLensResult<()> {
420        set_once(&mut self.retries, value)
421    }
422    /// Sets success threshold once.
423    pub fn set_successes(&mut self, value: StartupHealthSuccesses) -> PodmanLensResult<()> {
424        set_once(&mut self.successes, value)
425    }
426
427    /// Returns the declared startup command.
428    #[must_use]
429    pub const fn command(&self) -> &HealthCommand {
430        &self.command
431    }
432
433    /// Returns interval.
434    #[must_use]
435    pub const fn interval(&self) -> Option<HealthInterval> {
436        self.interval
437    }
438    /// Returns timeout.
439    #[must_use]
440    pub const fn timeout(&self) -> Option<HealthTimeout> {
441        self.timeout
442    }
443    /// Returns retries.
444    #[must_use]
445    pub const fn retries(&self) -> Option<StartupHealthRetries> {
446        self.retries
447    }
448    /// Returns success threshold.
449    #[must_use]
450    pub const fn successes(&self) -> Option<StartupHealthSuccesses> {
451        self.successes
452    }
453}
454
455/// The bounded supported container logging drivers.
456#[derive(Clone, Copy, Debug, Eq, PartialEq)]
457#[non_exhaustive]
458pub enum LogDriver {
459    /// Podman's journald integration.
460    Journald,
461    /// Podman's local file driver.
462    K8sFile,
463}
464
465/// A bounded maximum log size in bytes.
466#[derive(Clone, Copy, Debug, Eq, PartialEq)]
467pub struct LogSize(i64);
468
469impl LogSize {
470    /// Creates a non-zero maximum log size.
471    ///
472    /// # Errors
473    ///
474    /// Returns `PLN0034` for zero values.
475    pub fn new(bytes: u64) -> PodmanLensResult<Self> {
476        if bytes == 0 || bytes > i64::MAX as u64 {
477            return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
478        }
479        Ok(Self(i64::try_from(bytes).map_err(|_| {
480            Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent)
481        })?))
482    }
483
484    /// Returns the exact byte limit.
485    #[must_use]
486    pub const fn bytes(&self) -> i64 {
487        self.0
488    }
489}
490
491/// Bounded logging intent. Journald labels retain caller-public key/value pairs.
492#[derive(Clone, Debug, Default, Eq, PartialEq)]
493pub struct LoggingSettings {
494    driver: Option<LogDriver>,
495    max_size: Option<LogSize>,
496    journald_labels: Vec<Label>,
497}
498
499#[allow(clippy::missing_errors_doc)] // Every mutator has the same bounded duplicate/conflict contract.
500impl LoggingSettings {
501    /// Sets one driver, rejecting duplicate or conflicting assignments.
502    pub fn set_driver(&mut self, driver: LogDriver) -> PodmanLensResult<()> {
503        set_once(&mut self.driver, driver)
504    }
505
506    /// Sets one maximum size, rejecting duplicate or conflicting assignments.
507    pub fn set_max_size(&mut self, size: LogSize) -> PodmanLensResult<()> {
508        set_once(&mut self.max_size, size)
509    }
510
511    /// Adds one public journald label, rejecting duplicate keys.
512    pub fn add_journald_label(&mut self, label: Label) -> PodmanLensResult<()> {
513        if self.journald_labels.len() == MAX_ITEMS {
514            return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
515        }
516        if self
517            .journald_labels
518            .iter()
519            .any(|existing| existing.key() == label.key())
520        {
521            return Err(Diagnostic::new(DiagnosticCode::DeploymentDuplicateResource));
522        }
523        self.journald_labels.push(label);
524        Ok(())
525    }
526
527    /// Returns the selected driver.
528    #[must_use]
529    pub const fn driver(&self) -> Option<LogDriver> {
530        self.driver
531    }
532
533    /// Returns the selected size limit.
534    #[must_use]
535    pub const fn max_size(&self) -> Option<LogSize> {
536        self.max_size
537    }
538
539    /// Returns declared public journald labels in source order.
540    #[must_use]
541    pub fn journald_labels(&self) -> &[Label] {
542        &self.journald_labels
543    }
544}
545
546/// A bounded Linux capability name.
547#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
548pub struct LinuxCapability(&'static str);
549
550impl LinuxCapability {
551    /// Creates an exact reviewed prefix-free capability name.
552    ///
553    /// # Errors
554    ///
555    /// Returns `PLN0034` for `CAP_`-prefixed, unknown, or legacy spellings.
556    pub fn new(value: &str) -> PodmanLensResult<Self> {
557        CAPABILITIES
558            .iter()
559            .copied()
560            .find(|known| *known == value)
561            .map(Self)
562            .ok_or_else(|| Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent))
563    }
564
565    /// Returns the canonical capability name.
566    #[must_use]
567    pub const fn as_str(&self) -> &'static str {
568        self.0
569    }
570}
571
572const CAPABILITIES: [&str; 41] = [
573    "AUDIT_CONTROL",
574    "AUDIT_READ",
575    "AUDIT_WRITE",
576    "BLOCK_SUSPEND",
577    "BPF",
578    "CHECKPOINT_RESTORE",
579    "CHOWN",
580    "DAC_OVERRIDE",
581    "DAC_READ_SEARCH",
582    "FOWNER",
583    "FSETID",
584    "IPC_LOCK",
585    "IPC_OWNER",
586    "KILL",
587    "LEASE",
588    "LINUX_IMMUTABLE",
589    "MAC_ADMIN",
590    "MAC_OVERRIDE",
591    "MKNOD",
592    "NET_ADMIN",
593    "NET_BIND_SERVICE",
594    "NET_BROADCAST",
595    "NET_RAW",
596    "PERFMON",
597    "SETFCAP",
598    "SETGID",
599    "SETPCAP",
600    "SETUID",
601    "SYS_ADMIN",
602    "SYS_BOOT",
603    "SYS_CHROOT",
604    "SYS_MODULE",
605    "SYS_NICE",
606    "SYS_PACCT",
607    "SYS_PTRACE",
608    "SYS_RAWIO",
609    "SYS_RESOURCE",
610    "SYS_TIME",
611    "SYS_TTY_CONFIG",
612    "SYSLOG",
613    "WAKE_ALARM",
614];
615
616/// Container security settings that do not depend on host paths or execution.
617#[derive(Clone, Debug, Default, Eq, PartialEq)]
618pub struct SecuritySettings {
619    privileged: Option<bool>,
620    no_new_privileges: Option<bool>,
621    read_only_filesystem: Option<bool>,
622    read_write_tmpfs: Option<bool>,
623    cap_add: Vec<LinuxCapability>,
624    cap_drop: Vec<LinuxCapability>,
625}
626
627#[allow(clippy::missing_errors_doc)] // Every collection mutator uses the same bounded duplicate contract.
628impl SecuritySettings {
629    /// Enables or disables privileged execution explicitly.
630    pub fn set_privileged(&mut self, enabled: bool) -> PodmanLensResult<()> {
631        set_once(&mut self.privileged, enabled)
632    }
633
634    /// Enables or disables no-new-privileges explicitly.
635    pub fn set_no_new_privileges(&mut self, enabled: bool) -> PodmanLensResult<()> {
636        set_once(&mut self.no_new_privileges, enabled)
637    }
638
639    /// Enables or disables a read-only container filesystem explicitly.
640    pub fn set_read_only_filesystem(&mut self, enabled: bool) -> PodmanLensResult<()> {
641        set_once(&mut self.read_only_filesystem, enabled)
642    }
643
644    /// Adds one capability, rejecting duplicates and bounded overflow.
645    pub fn add_capability(&mut self, capability: LinuxCapability) -> PodmanLensResult<()> {
646        add_distinct(&mut self.cap_add, capability)
647    }
648
649    /// Drops one capability, rejecting duplicates and bounded overflow.
650    pub fn drop_capability(&mut self, capability: LinuxCapability) -> PodmanLensResult<()> {
651        add_distinct(&mut self.cap_drop, capability)
652    }
653
654    /// Records explicit writable-tmpfs behavior.
655    pub fn set_read_write_tmpfs(&mut self, enabled: bool) -> PodmanLensResult<()> {
656        set_once(&mut self.read_write_tmpfs, enabled)
657    }
658
659    /// Returns privileged state.
660    #[must_use]
661    pub const fn privileged(&self) -> Option<bool> {
662        self.privileged
663    }
664    /// Returns no-new-privileges state.
665    #[must_use]
666    pub const fn no_new_privileges(&self) -> Option<bool> {
667        self.no_new_privileges
668    }
669    /// Returns read-only filesystem state.
670    #[must_use]
671    pub const fn read_only_filesystem(&self) -> Option<bool> {
672        self.read_only_filesystem
673    }
674    /// Returns added capabilities.
675    #[must_use]
676    pub fn cap_add(&self) -> &[LinuxCapability] {
677        &self.cap_add
678    }
679    /// Returns dropped capabilities.
680    #[must_use]
681    pub fn cap_drop(&self) -> &[LinuxCapability] {
682        &self.cap_drop
683    }
684    /// Returns explicitly requested read-write-tmpfs behavior.
685    #[must_use]
686    pub const fn read_write_tmpfs(&self) -> Option<bool> {
687        self.read_write_tmpfs
688    }
689}
690
691/// One finite or explicitly unlimited resource limit.
692#[derive(Clone, Copy, Debug, Eq, PartialEq)]
693#[non_exhaustive]
694pub enum RlimitValue {
695    /// An exact finite limit; zero is valid.
696    Finite(u64),
697    /// An explicit unlimited limit, supported by semantic planning from Podman 5.6 onward.
698    Unlimited,
699}
700
701impl RlimitValue {
702    /// Creates an exact finite limit; zero is valid.
703    #[must_use]
704    pub const fn finite(value: u64) -> Self {
705        Self::Finite(value)
706    }
707}
708
709/// The bounded supported rlimit names.
710#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
711#[non_exhaustive]
712pub enum RlimitKind {
713    /// Maximum open file descriptors.
714    NoFile,
715    /// Maximum processes.
716    NProc,
717}
718
719/// One rlimit pair.
720#[derive(Clone, Copy, Debug, Eq, PartialEq)]
721pub struct Rlimit {
722    kind: RlimitKind,
723    soft: RlimitValue,
724    hard: RlimitValue,
725}
726
727#[allow(clippy::missing_errors_doc)] // Constructor documents the only ordering failure.
728impl Rlimit {
729    /// Creates one rlimit. Finite soft limits must not exceed finite hard limits.
730    pub fn new(kind: RlimitKind, soft: RlimitValue, hard: RlimitValue) -> PodmanLensResult<Self> {
731        if matches!((soft, hard), (RlimitValue::Unlimited, RlimitValue::Finite(_)))
732            || matches!((soft, hard), (RlimitValue::Finite(soft), RlimitValue::Finite(hard)) if soft > hard)
733        {
734            return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
735        }
736        Ok(Self { kind, soft, hard })
737    }
738    /// Returns rlimit kind.
739    #[must_use]
740    pub const fn kind(&self) -> RlimitKind {
741        self.kind
742    }
743    /// Returns soft limit.
744    #[must_use]
745    pub const fn soft(&self) -> RlimitValue {
746        self.soft
747    }
748    /// Returns hard limit.
749    #[must_use]
750    pub const fn hard(&self) -> RlimitValue {
751        self.hard
752    }
753}
754
755/// Bounded container CPU, memory, PID, and rlimit declarations.
756#[derive(Clone, Debug, Default, Eq, PartialEq)]
757pub struct ContainerResourceControls {
758    cpu_shares: Option<i64>,
759    cpu_period: Option<i64>,
760    cpu_quota: Option<i64>,
761    memory_bytes: Option<i64>,
762    pids: Option<i64>,
763    rlimits: Vec<Rlimit>,
764}
765
766#[allow(clippy::missing_errors_doc)] // Setters share the bounded duplicate/conflict contract.
767impl ContainerResourceControls {
768    /// Sets direct native CPU shares in the reviewed CFS range.
769    pub fn set_cpu_shares(&mut self, value: u32) -> PodmanLensResult<()> {
770        if !(2..=262_144).contains(&value) {
771            return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
772        }
773        set_once(&mut self.cpu_shares, i64::from(value))
774    }
775    /// Sets direct native CPU period in the reviewed 1ms–1s range.
776    pub fn set_cpu_period(&mut self, value: u64) -> PodmanLensResult<()> {
777        if !(1_000..=1_000_000).contains(&value) {
778            return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
779        }
780        let value = signed_positive(value)?;
781        set_once(&mut self.cpu_period, value)
782    }
783    /// Sets a positive direct native CPU quota in microseconds, at least one millisecond.
784    pub fn set_cpu_quota(&mut self, value: i64) -> PodmanLensResult<()> {
785        if value < 1_000 {
786            return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
787        }
788        set_once(&mut self.cpu_quota, value)
789    }
790    /// Sets positive memory bytes within the native signed range.
791    pub fn set_memory_bytes(&mut self, value: u64) -> PodmanLensResult<()> {
792        set_once(&mut self.memory_bytes, signed_positive(value)?)
793    }
794    /// Sets direct native PID limit; `-1` is unlimited.
795    pub fn set_pids(&mut self, value: i64) -> PodmanLensResult<()> {
796        if value == 0 || value < -1 {
797            return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
798        }
799        set_once(&mut self.pids, value)
800    }
801    /// Adds rlimit, rejecting duplicate kinds and bounded overflow.
802    pub fn add_rlimit(&mut self, value: Rlimit) -> PodmanLensResult<()> {
803        if self.rlimits.len() == MAX_ITEMS {
804            return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
805        }
806        if self.rlimits.iter().any(|existing| existing.kind == value.kind) {
807            return Err(Diagnostic::new(DiagnosticCode::DeploymentDuplicateResource));
808        }
809        self.rlimits.push(value);
810        Ok(())
811    }
812    /// Returns direct native CPU shares.
813    #[must_use]
814    pub const fn cpu_shares(&self) -> Option<i64> {
815        self.cpu_shares
816    }
817    /// Returns direct native CPU period.
818    #[must_use]
819    pub const fn cpu_period(&self) -> Option<i64> {
820        self.cpu_period
821    }
822    /// Returns the positive direct native CPU quota in microseconds.
823    #[must_use]
824    pub const fn cpu_quota(&self) -> Option<i64> {
825        self.cpu_quota
826    }
827    /// Returns memory bytes.
828    #[must_use]
829    pub const fn memory_bytes(&self) -> Option<i64> {
830        self.memory_bytes
831    }
832    /// Returns PID limit.
833    #[must_use]
834    pub const fn pids(&self) -> Option<i64> {
835        self.pids
836    }
837    /// Returns rlimits.
838    #[must_use]
839    pub fn rlimits(&self) -> &[Rlimit] {
840        &self.rlimits
841    }
842}
843
844/// One private or host namespace mode.
845#[derive(Clone, Copy, Debug, Eq, PartialEq)]
846#[non_exhaustive]
847pub enum NamespaceMode {
848    /// Creates a private namespace.
849    Private,
850    /// Joins the host namespace.
851    Host,
852}
853
854/// One IPC namespace mode for an unpodded container.
855#[derive(Clone, Copy, Debug, Eq, PartialEq)]
856#[non_exhaustive]
857pub enum IpcNamespaceMode {
858    /// Creates a private IPC namespace.
859    Private,
860    /// Joins the host IPC namespace.
861    Host,
862    /// Makes a private IPC namespace shareable.
863    Shareable,
864    /// Disables IPC namespace use.
865    None,
866}
867
868/// Explicit namespace intent for an unpodded container.
869#[derive(Clone, Debug, Default, Eq, PartialEq)]
870pub struct ContainerNamespaceSettings {
871    pid: Option<NamespaceMode>,
872    ipc: Option<IpcNamespaceMode>,
873    uts: Option<NamespaceMode>,
874    cgroup: Option<NamespaceMode>,
875}
876
877#[allow(clippy::missing_errors_doc)]
878impl ContainerNamespaceSettings {
879    /// Sets PID namespace mode once.
880    pub fn set_pid(&mut self, value: NamespaceMode) -> PodmanLensResult<()> {
881        set_once(&mut self.pid, value)
882    }
883    /// Sets IPC namespace mode once.
884    pub fn set_ipc(&mut self, value: IpcNamespaceMode) -> PodmanLensResult<()> {
885        set_once(&mut self.ipc, value)
886    }
887    /// Sets UTS namespace mode once.
888    pub fn set_uts(&mut self, value: NamespaceMode) -> PodmanLensResult<()> {
889        set_once(&mut self.uts, value)
890    }
891    /// Sets cgroup namespace mode once.
892    pub fn set_cgroup(&mut self, value: NamespaceMode) -> PodmanLensResult<()> {
893        set_once(&mut self.cgroup, value)
894    }
895    /// Returns PID namespace mode.
896    #[must_use]
897    pub const fn pid(&self) -> Option<NamespaceMode> {
898        self.pid
899    }
900    /// Returns IPC namespace mode.
901    #[must_use]
902    pub const fn ipc(&self) -> Option<IpcNamespaceMode> {
903        self.ipc
904    }
905    /// Returns UTS namespace mode.
906    #[must_use]
907    pub const fn uts(&self) -> Option<NamespaceMode> {
908        self.uts
909    }
910    /// Returns cgroup namespace mode.
911    #[must_use]
912    pub const fn cgroup(&self) -> Option<NamespaceMode> {
913        self.cgroup
914    }
915    pub(crate) fn is_empty(&self) -> bool {
916        self == &Self::default()
917    }
918}
919
920/// Container-only bounded runtime intent consumed by version-aware renderers.
921#[derive(Clone, Debug, Default, Eq, PartialEq)]
922pub struct ContainerRuntimeSettings {
923    health: Option<HealthCheck>,
924    startup_health: Option<StartupHealthCheck>,
925    logging: LoggingSettings,
926    security: SecuritySettings,
927    resources: ContainerResourceControls,
928    namespaces: ContainerNamespaceSettings,
929}
930
931#[allow(clippy::missing_errors_doc)] // Health setters share the bounded duplicate/conflict contract.
932impl ContainerRuntimeSettings {
933    /// Sets normal health behavior, rejecting duplicate or conflicting assignments.
934    pub fn set_health(&mut self, value: HealthCheck) -> PodmanLensResult<()> {
935        set_once(&mut self.health, value)
936    }
937    /// Sets startup health behavior, rejecting duplicate or conflicting assignments.
938    pub fn set_startup_health(&mut self, value: StartupHealthCheck) -> PodmanLensResult<()> {
939        set_once(&mut self.startup_health, value)
940    }
941    /// Returns normal health behavior.
942    #[must_use]
943    pub fn health(&self) -> Option<&HealthCheck> {
944        self.health.as_ref()
945    }
946    /// Returns startup health behavior.
947    #[must_use]
948    pub fn startup_health(&self) -> Option<&StartupHealthCheck> {
949        self.startup_health.as_ref()
950    }
951    /// Returns logging settings.
952    #[must_use]
953    pub fn logging(&self) -> &LoggingSettings {
954        &self.logging
955    }
956    /// Returns mutable logging settings.
957    #[must_use]
958    pub fn logging_mut(&mut self) -> &mut LoggingSettings {
959        &mut self.logging
960    }
961    /// Returns security settings.
962    #[must_use]
963    pub fn security(&self) -> &SecuritySettings {
964        &self.security
965    }
966    /// Returns mutable security settings.
967    #[must_use]
968    pub fn security_mut(&mut self) -> &mut SecuritySettings {
969        &mut self.security
970    }
971    /// Returns resource controls.
972    #[must_use]
973    pub fn resources(&self) -> &ContainerResourceControls {
974        &self.resources
975    }
976    /// Returns mutable resource controls.
977    #[must_use]
978    pub fn resources_mut(&mut self) -> &mut ContainerResourceControls {
979        &mut self.resources
980    }
981    /// Returns namespace settings.
982    #[must_use]
983    pub fn namespaces(&self) -> &ContainerNamespaceSettings {
984        &self.namespaces
985    }
986    /// Returns mutable namespace settings.
987    #[must_use]
988    pub fn namespaces_mut(&mut self) -> &mut ContainerNamespaceSettings {
989        &mut self.namespaces
990    }
991}
992
993fn set_once<T: Eq>(slot: &mut Option<T>, value: T) -> PodmanLensResult<()> {
994    match slot {
995        None => {
996            *slot = Some(value);
997            Ok(())
998        }
999        Some(existing) if *existing == value => Err(Diagnostic::new(DiagnosticCode::DeploymentDuplicateResource)),
1000        Some(_) => Err(Diagnostic::new(DiagnosticCode::DeploymentUnsupportedCombination)),
1001    }
1002}
1003fn add_distinct<T: Eq>(values: &mut Vec<T>, value: T) -> PodmanLensResult<()> {
1004    if values.len() == MAX_ITEMS {
1005        return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
1006    }
1007    if values.contains(&value) {
1008        return Err(Diagnostic::new(DiagnosticCode::DeploymentDuplicateResource));
1009    }
1010    values.push(value);
1011    Ok(())
1012}
1013fn signed_positive(value: u64) -> PodmanLensResult<i64> {
1014    if value == 0 || value > i64::MAX as u64 {
1015        return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
1016    }
1017    i64::try_from(value).map_err(|_| Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent))
1018}
1019fn validate_arguments(values: &[String]) -> PodmanLensResult<()> {
1020    if values.is_empty()
1021        || values.len() > MAX_ITEMS
1022        || values.first().is_some_and(String::is_empty)
1023        || values.iter().any(|value| !valid_text(value))
1024    {
1025        return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
1026    }
1027    Ok(())
1028}
1029fn valid_text(value: &str) -> bool {
1030    value.len() <= MAX_BYTES && !value.chars().any(char::is_control)
1031}