use std::fmt;
use crate::{Diagnostic, DiagnosticCode, Label, PodmanLensResult, SensitiveInputReference};
const MAX_ITEMS: usize = 64;
const MAX_BYTES: usize = 4096;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PublicHealthCommand(String);
impl PublicHealthCommand {
pub fn new(value: impl Into<String>) -> PodmanLensResult<Self> {
let value = value.into();
if value.is_empty() || !valid_text(&value) {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Eq, PartialEq)]
pub struct SensitiveInlineHealthCommand(String);
impl SensitiveInlineHealthCommand {
pub fn new(value: impl Into<String>) -> PodmanLensResult<Self> {
let value = value.into();
if value.is_empty() || !valid_text(&value) {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self(value))
}
}
impl fmt::Debug for SensitiveInlineHealthCommand {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("SensitiveInlineHealthCommand([redacted])")
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PublicHealthArgumentArray(Vec<String>);
impl PublicHealthArgumentArray {
pub fn new<I, S>(arguments: I) -> PodmanLensResult<Self>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let arguments = arguments.into_iter().map(Into::into).collect::<Vec<_>>();
validate_arguments(&arguments)?;
Ok(Self(arguments))
}
#[must_use]
pub fn values(&self) -> &[String] {
&self.0
}
}
#[derive(Clone, Eq, PartialEq)]
pub struct SensitiveInlineHealthArgumentArray(Vec<String>);
impl SensitiveInlineHealthArgumentArray {
pub fn new<I, S>(arguments: I) -> PodmanLensResult<Self>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let arguments = arguments.into_iter().map(Into::into).collect::<Vec<_>>();
validate_arguments(&arguments)?;
Ok(Self(arguments))
}
}
impl fmt::Debug for SensitiveInlineHealthArgumentArray {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("SensitiveInlineHealthArgumentArray([redacted])")
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum HealthCommand {
Shell(PublicHealthCommand),
Exec(PublicHealthArgumentArray),
SensitiveInlineShell(SensitiveInlineHealthCommand),
SensitiveInlineExec(SensitiveInlineHealthArgumentArray),
ExternalShell(SensitiveInputReference),
ExternalExec(SensitiveInputReference),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct HealthDuration(i64);
impl HealthDuration {
pub fn new(nanoseconds: u64) -> PodmanLensResult<Self> {
if nanoseconds == 0 || nanoseconds > i64::MAX as u64 {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self(i64::try_from(nanoseconds).map_err(|_| {
Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent)
})?))
}
#[must_use]
pub const fn nanoseconds(&self) -> i64 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct HealthRetries(u32);
impl HealthRetries {
pub const fn new(value: u32) -> PodmanLensResult<Self> {
if value == 0 || value > i32::MAX as u32 {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self(value))
}
#[must_use]
pub const fn value(&self) -> u32 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct StartupHealthRetries(u32);
impl StartupHealthRetries {
pub const fn new(value: u32) -> PodmanLensResult<Self> {
if value > i32::MAX as u32 {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self(value))
}
#[must_use]
pub const fn value(&self) -> u32 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct StartupHealthSuccesses(u32);
impl StartupHealthSuccesses {
pub const fn new(value: u32) -> PodmanLensResult<Self> {
if value > i32::MAX as u32 {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self(value))
}
#[must_use]
pub const fn value(&self) -> u32 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum HealthInterval {
Disabled,
Every(HealthDuration),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct HealthTimeout(HealthDuration);
impl HealthTimeout {
pub fn new(nanoseconds: u64) -> PodmanLensResult<Self> {
if nanoseconds < 1_000_000_000 {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self(HealthDuration::new(nanoseconds)?))
}
#[must_use]
pub const fn nanoseconds(&self) -> i64 {
self.0.nanoseconds()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct HealthStartPeriod(i64);
impl HealthStartPeriod {
pub fn new(nanoseconds: u64) -> PodmanLensResult<Self> {
if nanoseconds > i64::MAX as u64 {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self(i64::try_from(nanoseconds).map_err(|_| {
Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent)
})?))
}
#[must_use]
pub const fn nanoseconds(&self) -> i64 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum HealthOnFailure {
None,
Kill,
Restart,
Stop,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConfiguredHealthCheck {
command: HealthCommand,
interval: Option<HealthInterval>,
timeout: Option<HealthTimeout>,
retries: Option<HealthRetries>,
start_period: Option<HealthStartPeriod>,
on_failure: Option<HealthOnFailure>,
}
#[allow(clippy::missing_errors_doc)]
impl ConfiguredHealthCheck {
#[must_use]
pub const fn new(command: HealthCommand) -> Self {
Self {
command,
interval: None,
timeout: None,
retries: None,
start_period: None,
on_failure: None,
}
}
pub fn set_interval(&mut self, value: HealthInterval) -> PodmanLensResult<()> {
set_once(&mut self.interval, value)
}
pub fn set_timeout(&mut self, value: HealthTimeout) -> PodmanLensResult<()> {
set_once(&mut self.timeout, value)
}
pub fn set_retries(&mut self, value: HealthRetries) -> PodmanLensResult<()> {
set_once(&mut self.retries, value)
}
pub fn set_start_period(&mut self, value: HealthStartPeriod) -> PodmanLensResult<()> {
set_once(&mut self.start_period, value)
}
pub fn set_on_failure(&mut self, value: HealthOnFailure) -> PodmanLensResult<()> {
set_once(&mut self.on_failure, value)
}
#[must_use]
pub const fn command(&self) -> &HealthCommand {
&self.command
}
#[must_use]
pub const fn interval(&self) -> Option<HealthInterval> {
self.interval
}
#[must_use]
pub const fn timeout(&self) -> Option<HealthTimeout> {
self.timeout
}
#[must_use]
pub const fn retries(&self) -> Option<HealthRetries> {
self.retries
}
#[must_use]
pub const fn start_period(&self) -> Option<HealthStartPeriod> {
self.start_period
}
#[must_use]
pub const fn on_failure(&self) -> Option<HealthOnFailure> {
self.on_failure
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum HealthCheck {
Disabled,
Command(ConfiguredHealthCheck),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StartupHealthCheck {
command: HealthCommand,
interval: Option<HealthInterval>,
timeout: Option<HealthTimeout>,
retries: Option<StartupHealthRetries>,
successes: Option<StartupHealthSuccesses>,
}
#[allow(clippy::missing_errors_doc)]
impl StartupHealthCheck {
#[must_use]
pub const fn new(command: HealthCommand) -> Self {
Self {
command,
interval: None,
timeout: None,
retries: None,
successes: None,
}
}
pub fn set_interval(&mut self, value: HealthInterval) -> PodmanLensResult<()> {
set_once(&mut self.interval, value)
}
pub fn set_timeout(&mut self, value: HealthTimeout) -> PodmanLensResult<()> {
set_once(&mut self.timeout, value)
}
pub fn set_retries(&mut self, value: StartupHealthRetries) -> PodmanLensResult<()> {
set_once(&mut self.retries, value)
}
pub fn set_successes(&mut self, value: StartupHealthSuccesses) -> PodmanLensResult<()> {
set_once(&mut self.successes, value)
}
#[must_use]
pub const fn command(&self) -> &HealthCommand {
&self.command
}
#[must_use]
pub const fn interval(&self) -> Option<HealthInterval> {
self.interval
}
#[must_use]
pub const fn timeout(&self) -> Option<HealthTimeout> {
self.timeout
}
#[must_use]
pub const fn retries(&self) -> Option<StartupHealthRetries> {
self.retries
}
#[must_use]
pub const fn successes(&self) -> Option<StartupHealthSuccesses> {
self.successes
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum LogDriver {
Journald,
K8sFile,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct LogSize(i64);
impl LogSize {
pub fn new(bytes: u64) -> PodmanLensResult<Self> {
if bytes == 0 || bytes > i64::MAX as u64 {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self(i64::try_from(bytes).map_err(|_| {
Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent)
})?))
}
#[must_use]
pub const fn bytes(&self) -> i64 {
self.0
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct LoggingSettings {
driver: Option<LogDriver>,
max_size: Option<LogSize>,
journald_labels: Vec<Label>,
}
#[allow(clippy::missing_errors_doc)] impl LoggingSettings {
pub fn set_driver(&mut self, driver: LogDriver) -> PodmanLensResult<()> {
set_once(&mut self.driver, driver)
}
pub fn set_max_size(&mut self, size: LogSize) -> PodmanLensResult<()> {
set_once(&mut self.max_size, size)
}
pub fn add_journald_label(&mut self, label: Label) -> PodmanLensResult<()> {
if self.journald_labels.len() == MAX_ITEMS {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
if self
.journald_labels
.iter()
.any(|existing| existing.key() == label.key())
{
return Err(Diagnostic::new(DiagnosticCode::DeploymentDuplicateResource));
}
self.journald_labels.push(label);
Ok(())
}
#[must_use]
pub const fn driver(&self) -> Option<LogDriver> {
self.driver
}
#[must_use]
pub const fn max_size(&self) -> Option<LogSize> {
self.max_size
}
#[must_use]
pub fn journald_labels(&self) -> &[Label] {
&self.journald_labels
}
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct LinuxCapability(&'static str);
impl LinuxCapability {
pub fn new(value: &str) -> PodmanLensResult<Self> {
CAPABILITIES
.iter()
.copied()
.find(|known| *known == value)
.map(Self)
.ok_or_else(|| Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent))
}
#[must_use]
pub const fn as_str(&self) -> &'static str {
self.0
}
}
const CAPABILITIES: [&str; 41] = [
"AUDIT_CONTROL",
"AUDIT_READ",
"AUDIT_WRITE",
"BLOCK_SUSPEND",
"BPF",
"CHECKPOINT_RESTORE",
"CHOWN",
"DAC_OVERRIDE",
"DAC_READ_SEARCH",
"FOWNER",
"FSETID",
"IPC_LOCK",
"IPC_OWNER",
"KILL",
"LEASE",
"LINUX_IMMUTABLE",
"MAC_ADMIN",
"MAC_OVERRIDE",
"MKNOD",
"NET_ADMIN",
"NET_BIND_SERVICE",
"NET_BROADCAST",
"NET_RAW",
"PERFMON",
"SETFCAP",
"SETGID",
"SETPCAP",
"SETUID",
"SYS_ADMIN",
"SYS_BOOT",
"SYS_CHROOT",
"SYS_MODULE",
"SYS_NICE",
"SYS_PACCT",
"SYS_PTRACE",
"SYS_RAWIO",
"SYS_RESOURCE",
"SYS_TIME",
"SYS_TTY_CONFIG",
"SYSLOG",
"WAKE_ALARM",
];
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct SecuritySettings {
privileged: Option<bool>,
no_new_privileges: Option<bool>,
read_only_filesystem: Option<bool>,
read_write_tmpfs: Option<bool>,
cap_add: Vec<LinuxCapability>,
cap_drop: Vec<LinuxCapability>,
}
#[allow(clippy::missing_errors_doc)] impl SecuritySettings {
pub fn set_privileged(&mut self, enabled: bool) -> PodmanLensResult<()> {
set_once(&mut self.privileged, enabled)
}
pub fn set_no_new_privileges(&mut self, enabled: bool) -> PodmanLensResult<()> {
set_once(&mut self.no_new_privileges, enabled)
}
pub fn set_read_only_filesystem(&mut self, enabled: bool) -> PodmanLensResult<()> {
set_once(&mut self.read_only_filesystem, enabled)
}
pub fn add_capability(&mut self, capability: LinuxCapability) -> PodmanLensResult<()> {
add_distinct(&mut self.cap_add, capability)
}
pub fn drop_capability(&mut self, capability: LinuxCapability) -> PodmanLensResult<()> {
add_distinct(&mut self.cap_drop, capability)
}
pub fn set_read_write_tmpfs(&mut self, enabled: bool) -> PodmanLensResult<()> {
set_once(&mut self.read_write_tmpfs, enabled)
}
#[must_use]
pub const fn privileged(&self) -> Option<bool> {
self.privileged
}
#[must_use]
pub const fn no_new_privileges(&self) -> Option<bool> {
self.no_new_privileges
}
#[must_use]
pub const fn read_only_filesystem(&self) -> Option<bool> {
self.read_only_filesystem
}
#[must_use]
pub fn cap_add(&self) -> &[LinuxCapability] {
&self.cap_add
}
#[must_use]
pub fn cap_drop(&self) -> &[LinuxCapability] {
&self.cap_drop
}
#[must_use]
pub const fn read_write_tmpfs(&self) -> Option<bool> {
self.read_write_tmpfs
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum RlimitValue {
Finite(u64),
Unlimited,
}
impl RlimitValue {
#[must_use]
pub const fn finite(value: u64) -> Self {
Self::Finite(value)
}
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
#[non_exhaustive]
pub enum RlimitKind {
NoFile,
NProc,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Rlimit {
kind: RlimitKind,
soft: RlimitValue,
hard: RlimitValue,
}
#[allow(clippy::missing_errors_doc)] impl Rlimit {
pub fn new(kind: RlimitKind, soft: RlimitValue, hard: RlimitValue) -> PodmanLensResult<Self> {
if matches!((soft, hard), (RlimitValue::Unlimited, RlimitValue::Finite(_)))
|| matches!((soft, hard), (RlimitValue::Finite(soft), RlimitValue::Finite(hard)) if soft > hard)
{
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self { kind, soft, hard })
}
#[must_use]
pub const fn kind(&self) -> RlimitKind {
self.kind
}
#[must_use]
pub const fn soft(&self) -> RlimitValue {
self.soft
}
#[must_use]
pub const fn hard(&self) -> RlimitValue {
self.hard
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ContainerResourceControls {
cpu_shares: Option<i64>,
cpu_period: Option<i64>,
cpu_quota: Option<i64>,
memory_bytes: Option<i64>,
pids: Option<i64>,
rlimits: Vec<Rlimit>,
}
#[allow(clippy::missing_errors_doc)] impl ContainerResourceControls {
pub fn set_cpu_shares(&mut self, value: u32) -> PodmanLensResult<()> {
if !(2..=262_144).contains(&value) {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
set_once(&mut self.cpu_shares, i64::from(value))
}
pub fn set_cpu_period(&mut self, value: u64) -> PodmanLensResult<()> {
if !(1_000..=1_000_000).contains(&value) {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
let value = signed_positive(value)?;
set_once(&mut self.cpu_period, value)
}
pub fn set_cpu_quota(&mut self, value: i64) -> PodmanLensResult<()> {
if value < 1_000 {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
set_once(&mut self.cpu_quota, value)
}
pub fn set_memory_bytes(&mut self, value: u64) -> PodmanLensResult<()> {
set_once(&mut self.memory_bytes, signed_positive(value)?)
}
pub fn set_pids(&mut self, value: i64) -> PodmanLensResult<()> {
if value == 0 || value < -1 {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
set_once(&mut self.pids, value)
}
pub fn add_rlimit(&mut self, value: Rlimit) -> PodmanLensResult<()> {
if self.rlimits.len() == MAX_ITEMS {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
if self.rlimits.iter().any(|existing| existing.kind == value.kind) {
return Err(Diagnostic::new(DiagnosticCode::DeploymentDuplicateResource));
}
self.rlimits.push(value);
Ok(())
}
#[must_use]
pub const fn cpu_shares(&self) -> Option<i64> {
self.cpu_shares
}
#[must_use]
pub const fn cpu_period(&self) -> Option<i64> {
self.cpu_period
}
#[must_use]
pub const fn cpu_quota(&self) -> Option<i64> {
self.cpu_quota
}
#[must_use]
pub const fn memory_bytes(&self) -> Option<i64> {
self.memory_bytes
}
#[must_use]
pub const fn pids(&self) -> Option<i64> {
self.pids
}
#[must_use]
pub fn rlimits(&self) -> &[Rlimit] {
&self.rlimits
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum NamespaceMode {
Private,
Host,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum IpcNamespaceMode {
Private,
Host,
Shareable,
None,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ContainerNamespaceSettings {
pid: Option<NamespaceMode>,
ipc: Option<IpcNamespaceMode>,
uts: Option<NamespaceMode>,
cgroup: Option<NamespaceMode>,
}
#[allow(clippy::missing_errors_doc)]
impl ContainerNamespaceSettings {
pub fn set_pid(&mut self, value: NamespaceMode) -> PodmanLensResult<()> {
set_once(&mut self.pid, value)
}
pub fn set_ipc(&mut self, value: IpcNamespaceMode) -> PodmanLensResult<()> {
set_once(&mut self.ipc, value)
}
pub fn set_uts(&mut self, value: NamespaceMode) -> PodmanLensResult<()> {
set_once(&mut self.uts, value)
}
pub fn set_cgroup(&mut self, value: NamespaceMode) -> PodmanLensResult<()> {
set_once(&mut self.cgroup, value)
}
#[must_use]
pub const fn pid(&self) -> Option<NamespaceMode> {
self.pid
}
#[must_use]
pub const fn ipc(&self) -> Option<IpcNamespaceMode> {
self.ipc
}
#[must_use]
pub const fn uts(&self) -> Option<NamespaceMode> {
self.uts
}
#[must_use]
pub const fn cgroup(&self) -> Option<NamespaceMode> {
self.cgroup
}
pub(crate) fn is_empty(&self) -> bool {
self == &Self::default()
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ContainerRuntimeSettings {
health: Option<HealthCheck>,
startup_health: Option<StartupHealthCheck>,
logging: LoggingSettings,
security: SecuritySettings,
resources: ContainerResourceControls,
namespaces: ContainerNamespaceSettings,
}
#[allow(clippy::missing_errors_doc)] impl ContainerRuntimeSettings {
pub fn set_health(&mut self, value: HealthCheck) -> PodmanLensResult<()> {
set_once(&mut self.health, value)
}
pub fn set_startup_health(&mut self, value: StartupHealthCheck) -> PodmanLensResult<()> {
set_once(&mut self.startup_health, value)
}
#[must_use]
pub fn health(&self) -> Option<&HealthCheck> {
self.health.as_ref()
}
#[must_use]
pub fn startup_health(&self) -> Option<&StartupHealthCheck> {
self.startup_health.as_ref()
}
#[must_use]
pub fn logging(&self) -> &LoggingSettings {
&self.logging
}
#[must_use]
pub fn logging_mut(&mut self) -> &mut LoggingSettings {
&mut self.logging
}
#[must_use]
pub fn security(&self) -> &SecuritySettings {
&self.security
}
#[must_use]
pub fn security_mut(&mut self) -> &mut SecuritySettings {
&mut self.security
}
#[must_use]
pub fn resources(&self) -> &ContainerResourceControls {
&self.resources
}
#[must_use]
pub fn resources_mut(&mut self) -> &mut ContainerResourceControls {
&mut self.resources
}
#[must_use]
pub fn namespaces(&self) -> &ContainerNamespaceSettings {
&self.namespaces
}
#[must_use]
pub fn namespaces_mut(&mut self) -> &mut ContainerNamespaceSettings {
&mut self.namespaces
}
}
fn set_once<T: Eq>(slot: &mut Option<T>, value: T) -> PodmanLensResult<()> {
match slot {
None => {
*slot = Some(value);
Ok(())
}
Some(existing) if *existing == value => Err(Diagnostic::new(DiagnosticCode::DeploymentDuplicateResource)),
Some(_) => Err(Diagnostic::new(DiagnosticCode::DeploymentUnsupportedCombination)),
}
}
fn add_distinct<T: Eq>(values: &mut Vec<T>, value: T) -> PodmanLensResult<()> {
if values.len() == MAX_ITEMS {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
if values.contains(&value) {
return Err(Diagnostic::new(DiagnosticCode::DeploymentDuplicateResource));
}
values.push(value);
Ok(())
}
fn signed_positive(value: u64) -> PodmanLensResult<i64> {
if value == 0 || value > i64::MAX as u64 {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
i64::try_from(value).map_err(|_| Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent))
}
fn validate_arguments(values: &[String]) -> PodmanLensResult<()> {
if values.is_empty()
|| values.len() > MAX_ITEMS
|| values.first().is_some_and(String::is_empty)
|| values.iter().any(|value| !valid_text(value))
{
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(())
}
fn valid_text(value: &str) -> bool {
value.len() <= MAX_BYTES && !value.chars().any(char::is_control)
}