use std::fmt;
use crate::{
DeploymentResourceId, Diagnostic, DiagnosticCode, PodmanLensResult, ResourceKind, SensitiveInputReference,
};
const MAX_ARGUMENTS: usize = 128;
const MAX_ARGUMENT_BYTES: usize = 4096;
const MAX_VALUE_BYTES: usize = 4096;
const MAX_LABELS: usize = 128;
const MAX_ENVIRONMENT: usize = 128;
const MAX_PATH_BYTES: usize = 4096;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ArgumentArray(Vec<String>);
impl ArgumentArray {
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<String>>();
if arguments.is_empty()
|| arguments.len() > MAX_ARGUMENTS
|| arguments
.iter()
.any(|argument| !valid_non_control(argument, MAX_ARGUMENT_BYTES))
{
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self(arguments))
}
#[must_use]
pub fn values(&self) -> &[String] {
&self.0
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ContainerUser(String);
impl ContainerUser {
pub fn new(value: impl Into<String>) -> PodmanLensResult<Self> {
let value = value.into();
let mut components = value.split(':');
let Some(user) = components.next() else {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
};
let group = components.next();
if value.len() > MAX_VALUE_BYTES
|| !valid_user_component(user)
|| group.is_some_and(|component| !valid_user_component(component))
|| components.next().is_some()
{
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AbsoluteContainerPath(String);
impl AbsoluteContainerPath {
pub fn new(value: impl Into<String>) -> PodmanLensResult<Self> {
let value = value.into();
if !is_absolute_normalized_path(&value) {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ContainerWorkdir(AbsoluteContainerPath);
impl ContainerWorkdir {
#[must_use]
pub const fn new(path: AbsoluteContainerPath) -> Self {
Self(path)
}
#[must_use]
pub fn path(&self) -> &AbsoluteContainerPath {
&self.0
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ContainerHostname(String);
impl ContainerHostname {
pub fn new(value: impl Into<String>) -> PodmanLensResult<Self> {
let value = value.into();
if value.is_empty()
|| value.len() > 253
|| value.split('.').any(|label| {
label.is_empty()
|| label.len() > 63
|| label.starts_with('-')
|| label.ends_with('-')
|| !label.bytes().all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
})
{
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LabelKey(String);
impl LabelKey {
pub fn new(value: impl Into<String>) -> PodmanLensResult<Self> {
let value = value.into();
if value.is_empty() || !valid_non_control(&value, MAX_VALUE_BYTES) || value.contains('=') {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PublicLabelValue(String);
impl PublicLabelValue {
pub fn new(value: impl Into<String>) -> PodmanLensResult<Self> {
let value = value.into();
if !valid_value(&value) {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Label {
key: LabelKey,
value: PublicLabelValue,
}
impl Label {
#[must_use]
pub const fn new(key: LabelKey, value: PublicLabelValue) -> Self {
Self { key, value }
}
#[must_use]
pub fn key(&self) -> &LabelKey {
&self.key
}
#[must_use]
pub fn value(&self) -> &PublicLabelValue {
&self.value
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EnvironmentName(String);
impl EnvironmentName {
pub fn new(value: impl Into<String>) -> PodmanLensResult<Self> {
let value = value.into();
let mut bytes = value.bytes();
let Some(first) = bytes.next() else {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
};
if value.len() > 256
|| !(first.is_ascii_alphabetic() || first == b'_')
|| !bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
{
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PublicEnvironmentValue(String);
impl PublicEnvironmentValue {
pub fn new(value: impl Into<String>) -> PodmanLensResult<Self> {
let value = value.into();
if !valid_value(&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 SensitiveInlineEnvironmentValue(String);
impl SensitiveInlineEnvironmentValue {
pub fn new(value: impl Into<String>) -> PodmanLensResult<Self> {
let value = value.into();
if !valid_value(&value) {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self(value))
}
}
impl fmt::Debug for SensitiveInlineEnvironmentValue {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("SensitiveInlineEnvironmentValue([redacted])")
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum DeploymentEnvironmentValue {
Public(PublicEnvironmentValue),
SensitiveInline(SensitiveInlineEnvironmentValue),
External(SensitiveInputReference),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EnvironmentAssignment {
name: EnvironmentName,
value: DeploymentEnvironmentValue,
}
impl EnvironmentAssignment {
#[must_use]
pub const fn new(name: EnvironmentName, value: DeploymentEnvironmentValue) -> Self {
Self { name, value }
}
#[must_use]
pub fn name(&self) -> &EnvironmentName {
&self.name
}
#[must_use]
pub fn value(&self) -> &DeploymentEnvironmentValue {
&self.value
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum RestartPolicy {
No,
OnFailure,
Always,
UnlessStopped,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum NamedVolumeCopyMode {
Copy,
NoCopy,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum MountAccess {
ReadWrite,
ReadOnly,
}
impl MountAccess {
#[must_use]
pub const fn is_read_only(self) -> bool {
matches!(self, Self::ReadOnly)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VolumeSubpath(String);
impl VolumeSubpath {
pub fn new(value: impl Into<String>) -> PodmanLensResult<Self> {
let value = value.into();
if value.len() > MAX_PATH_BYTES
|| !value.starts_with('/')
|| value.contains('\\')
|| value.chars().any(char::is_control)
|| value
.split('/')
.skip(1)
.any(|component| component.is_empty() || matches!(component, "." | ".."))
{
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NamedVolumeMount {
source: DeploymentResourceId,
destination: AbsoluteContainerPath,
access: MountAccess,
copy_mode: NamedVolumeCopyMode,
subpath: Option<VolumeSubpath>,
}
impl NamedVolumeMount {
pub fn new(
source: DeploymentResourceId,
destination: AbsoluteContainerPath,
access: MountAccess,
copy_mode: NamedVolumeCopyMode,
) -> PodmanLensResult<Self> {
if source.kind() != ResourceKind::Volume {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self {
source,
destination,
access,
copy_mode,
subpath: None,
})
}
pub fn set_subpath(&mut self, subpath: VolumeSubpath) -> PodmanLensResult<()> {
if self.subpath.is_some() || self.copy_mode == NamedVolumeCopyMode::NoCopy {
return Err(Diagnostic::new(DiagnosticCode::DeploymentUnsupportedCombination));
}
self.subpath = Some(subpath);
Ok(())
}
#[must_use]
pub fn source(&self) -> &DeploymentResourceId {
&self.source
}
#[must_use]
pub fn destination(&self) -> &AbsoluteContainerPath {
&self.destination
}
#[must_use]
pub const fn is_read_only(&self) -> bool {
self.access.is_read_only()
}
#[must_use]
pub const fn access(&self) -> MountAccess {
self.access
}
#[must_use]
pub const fn copy_mode(&self) -> NamedVolumeCopyMode {
self.copy_mode
}
#[must_use]
pub fn subpath(&self) -> Option<&VolumeSubpath> {
self.subpath.as_ref()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BindMount {
source: AbsoluteContainerPath,
destination: AbsoluteContainerPath,
access: MountAccess,
}
impl BindMount {
#[must_use]
pub const fn new(source: AbsoluteContainerPath, destination: AbsoluteContainerPath, access: MountAccess) -> Self {
Self {
source,
destination,
access,
}
}
#[must_use]
pub fn source(&self) -> &AbsoluteContainerPath {
&self.source
}
#[must_use]
pub fn destination(&self) -> &AbsoluteContainerPath {
&self.destination
}
#[must_use]
pub const fn access(&self) -> MountAccess {
self.access
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TmpfsMount {
destination: AbsoluteContainerPath,
access: MountAccess,
}
impl TmpfsMount {
#[must_use]
pub const fn new(destination: AbsoluteContainerPath, access: MountAccess) -> Self {
Self { destination, access }
}
#[must_use]
pub fn destination(&self) -> &AbsoluteContainerPath {
&self.destination
}
#[must_use]
pub const fn access(&self) -> MountAccess {
self.access
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum MountIntent {
NamedVolume(NamedVolumeMount),
Bind(BindMount),
Tmpfs(TmpfsMount),
}
impl From<NamedVolumeMount> for MountIntent {
fn from(mount: NamedVolumeMount) -> Self {
Self::NamedVolume(mount)
}
}
impl From<BindMount> for MountIntent {
fn from(mount: BindMount) -> Self {
Self::Bind(mount)
}
}
impl From<TmpfsMount> for MountIntent {
fn from(mount: TmpfsMount) -> Self {
Self::Tmpfs(mount)
}
}
impl MountIntent {
#[must_use]
pub fn destination(&self) -> &AbsoluteContainerPath {
match self {
Self::NamedVolume(mount) => mount.destination(),
Self::Bind(mount) => mount.destination(),
Self::Tmpfs(mount) => mount.destination(),
}
}
#[must_use]
pub fn volume_source(&self) -> Option<&DeploymentResourceId> {
match self {
Self::NamedVolume(mount) => Some(mount.source()),
Self::Bind(_) | Self::Tmpfs(_) => None,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct UnixId(u32);
impl UnixId {
pub 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 get(self) -> u32 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SecretMode(u16);
impl SecretMode {
pub fn new(value: u16) -> PodmanLensResult<Self> {
if value > 0o777 {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self(value))
}
#[must_use]
pub const fn get(self) -> u16 {
self.0
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum SecretGrant {
Mount {
source: DeploymentResourceId,
target: Option<AbsoluteContainerPath>,
uid: Option<UnixId>,
gid: Option<UnixId>,
mode: Option<SecretMode>,
},
Environment {
source: DeploymentResourceId,
target: EnvironmentName,
},
}
impl SecretGrant {
pub fn mount(source: DeploymentResourceId) -> PodmanLensResult<Self> {
if source.kind() != ResourceKind::Secret {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self::Mount {
source,
target: None,
uid: None,
gid: None,
mode: None,
})
}
pub fn environment(source: DeploymentResourceId, target: EnvironmentName) -> PodmanLensResult<Self> {
if source.kind() != ResourceKind::Secret {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self::Environment { source, target })
}
pub fn set_mount_target(&mut self, target: AbsoluteContainerPath) -> PodmanLensResult<()> {
match self {
Self::Mount { target: slot, .. } if slot.is_none() => {
*slot = Some(target);
Ok(())
}
Self::Mount { .. } | Self::Environment { .. } => {
Err(Diagnostic::new(DiagnosticCode::DeploymentUnsupportedCombination))
}
}
}
pub fn set_mount_uid(&mut self, uid: UnixId) -> PodmanLensResult<()> {
set_secret_mount_option(self, uid, |grant| match grant {
Self::Mount { uid, .. } => uid,
Self::Environment { .. } => unreachable!("environment grants are rejected before access"),
})
}
pub fn set_mount_gid(&mut self, gid: UnixId) -> PodmanLensResult<()> {
set_secret_mount_option(self, gid, |grant| match grant {
Self::Mount { gid, .. } => gid,
Self::Environment { .. } => unreachable!("environment grants are rejected before access"),
})
}
pub fn set_mount_mode(&mut self, mode: SecretMode) -> PodmanLensResult<()> {
set_secret_mount_option(self, mode, |grant| match grant {
Self::Mount { mode, .. } => mode,
Self::Environment { .. } => unreachable!("environment grants are rejected before access"),
})
}
#[must_use]
pub fn source(&self) -> &DeploymentResourceId {
match self {
Self::Mount { source, .. } | Self::Environment { source, .. } => source,
}
}
#[must_use]
pub fn mount_target(&self) -> Option<&AbsoluteContainerPath> {
match self {
Self::Mount { target, .. } => target.as_ref(),
Self::Environment { .. } => None,
}
}
#[must_use]
pub fn environment_target(&self) -> Option<&EnvironmentName> {
match self {
Self::Environment { target, .. } => Some(target),
Self::Mount { .. } => None,
}
}
#[must_use]
pub fn mount_uid(&self) -> Option<UnixId> {
match self {
Self::Mount { uid, .. } => *uid,
Self::Environment { .. } => None,
}
}
#[must_use]
pub fn mount_gid(&self) -> Option<UnixId> {
match self {
Self::Mount { gid, .. } => *gid,
Self::Environment { .. } => None,
}
}
#[must_use]
pub fn mount_mode(&self) -> Option<SecretMode> {
match self {
Self::Mount { mode, .. } => *mode,
Self::Environment { .. } => None,
}
}
}
fn set_secret_mount_option<T: Eq>(
grant: &mut SecretGrant,
value: T,
member: impl FnOnce(&mut SecretGrant) -> &mut Option<T>,
) -> PodmanLensResult<()> {
if !matches!(grant, SecretGrant::Mount { .. }) {
return Err(Diagnostic::new(DiagnosticCode::DeploymentUnsupportedCombination));
}
let slot = member(grant);
if slot.is_some() {
return Err(Diagnostic::new(DiagnosticCode::DeploymentUnsupportedCombination));
}
*slot = Some(value);
Ok(())
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ContainerSettings {
command: Option<ArgumentArray>,
entrypoint: Option<ArgumentArray>,
user: Option<ContainerUser>,
workdir: Option<ContainerWorkdir>,
hostname: Option<ContainerHostname>,
labels: Vec<Label>,
environment: Vec<EnvironmentAssignment>,
restart_policy: Option<RestartPolicy>,
}
impl ContainerSettings {
pub fn set_command(&mut self, command: ArgumentArray) -> PodmanLensResult<()> {
set_once(&mut self.command, command)
}
pub fn set_entrypoint(&mut self, entrypoint: ArgumentArray) -> PodmanLensResult<()> {
set_once(&mut self.entrypoint, entrypoint)
}
pub fn set_user(&mut self, user: ContainerUser) -> PodmanLensResult<()> {
set_once(&mut self.user, user)
}
pub fn set_workdir(&mut self, workdir: ContainerWorkdir) -> PodmanLensResult<()> {
set_once(&mut self.workdir, workdir)
}
pub fn set_hostname(&mut self, hostname: ContainerHostname) -> PodmanLensResult<()> {
set_once(&mut self.hostname, hostname)
}
pub fn add_label(&mut self, label: Label) -> PodmanLensResult<()> {
if self.labels.len() == MAX_LABELS {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
if self.labels.iter().any(|existing| existing.key == label.key) {
return Err(Diagnostic::new(DiagnosticCode::DeploymentDuplicateResource));
}
self.labels.push(label);
Ok(())
}
pub fn add_environment(&mut self, assignment: EnvironmentAssignment) -> PodmanLensResult<()> {
if self.environment.len() == MAX_ENVIRONMENT {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
if self.environment.iter().any(|existing| existing.name == assignment.name) {
return Err(Diagnostic::new(DiagnosticCode::DeploymentDuplicateResource));
}
self.environment.push(assignment);
Ok(())
}
pub fn set_restart_policy(&mut self, restart_policy: RestartPolicy) -> PodmanLensResult<()> {
set_once(&mut self.restart_policy, restart_policy)
}
#[must_use]
pub fn command(&self) -> Option<&ArgumentArray> {
self.command.as_ref()
}
#[must_use]
pub fn entrypoint(&self) -> Option<&ArgumentArray> {
self.entrypoint.as_ref()
}
#[must_use]
pub fn user(&self) -> Option<&ContainerUser> {
self.user.as_ref()
}
#[must_use]
pub fn workdir(&self) -> Option<&ContainerWorkdir> {
self.workdir.as_ref()
}
#[must_use]
pub fn hostname(&self) -> Option<&ContainerHostname> {
self.hostname.as_ref()
}
#[must_use]
pub fn labels(&self) -> &[Label] {
&self.labels
}
#[must_use]
pub fn environment(&self) -> &[EnvironmentAssignment] {
&self.environment
}
#[must_use]
pub const fn restart_policy(&self) -> Option<RestartPolicy> {
self.restart_policy
}
}
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 valid_value(value: &str) -> bool {
valid_non_control(value, MAX_VALUE_BYTES)
}
fn valid_non_control(value: &str, maximum_bytes: usize) -> bool {
value.len() <= maximum_bytes && !value.chars().any(char::is_control)
}
fn valid_user_component(value: &str) -> bool {
!value.is_empty()
&& value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
}
fn is_absolute_normalized_path(value: &str) -> bool {
value.len() <= MAX_PATH_BYTES
&& value.starts_with('/')
&& !value.contains('\\')
&& !value.chars().any(char::is_control)
&& (value == "/"
|| value
.split('/')
.skip(1)
.all(|component| !component.is_empty() && component != "." && component != ".."))
}