use std::net::IpAddr;
use crate::{DeploymentResourceId, Diagnostic, DiagnosticCode, PodmanLensResult, ResourceKind};
const MAX_ATTACHMENTS: usize = 32;
const MAX_ALIASES: usize = 64;
const MAX_DNS_VALUES: usize = 16;
const MAX_PORT_MAPPINGS: usize = 128;
const MAX_HOST_ALIASES: usize = 128;
const MAX_IPAM_SUBNETS: usize = 32;
const MAX_ROUTES: usize = 64;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NetworkCidr(String);
impl NetworkCidr {
pub fn new(value: impl Into<String>) -> PodmanLensResult<Self> {
let value = value.into();
let Some((address, prefix)) = value.split_once('/') else {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
};
let Ok(address) = address.parse::<IpAddr>() else {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
};
let Ok(prefix) = prefix.parse::<u8>() else {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
};
if value.len() > 128 || prefix > if address.is_ipv4() { 32 } else { 128 } {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
let network = masked_address(address, prefix);
Ok(Self(format!("{network}/{prefix}")))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn has_address_family(&self, address: IpAddr) -> bool {
self.0.split_once('/').is_some_and(|(network, _)| {
network
.parse::<IpAddr>()
.is_ok_and(|network| network.is_ipv4() == address.is_ipv4())
})
}
#[must_use]
pub fn contains(&self, address: IpAddr) -> bool {
let Some((network, prefix)) = self.0.split_once('/') else {
return false;
};
let Ok(prefix) = prefix.parse::<u8>() else {
return false;
};
let Ok(network) = network.parse::<IpAddr>() else {
return false;
};
if network.is_ipv4() != address.is_ipv4() {
return false;
}
network == masked_address(address, prefix)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StaticMacAddress(String);
impl StaticMacAddress {
pub fn new(value: impl AsRef<str>) -> PodmanLensResult<Self> {
let value = value.as_ref();
if value.len() != 17
|| value.split(':').count() != 6
|| value
.split(':')
.any(|part| part.len() != 2 || u8::from_str_radix(part, 16).is_err())
{
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self(value.to_ascii_lowercase()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NetworkAttachment {
network: DeploymentResourceId,
aliases: Vec<String>,
static_ipv4: Option<IpAddr>,
static_ipv6: Option<IpAddr>,
static_mac: Option<StaticMacAddress>,
}
impl NetworkAttachment {
pub fn new(network: DeploymentResourceId) -> PodmanLensResult<Self> {
if network.kind() != ResourceKind::Network {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self {
network,
aliases: Vec::new(),
static_ipv4: None,
static_ipv6: None,
static_mac: None,
})
}
pub fn add_alias(&mut self, alias: impl Into<String>) -> PodmanLensResult<()> {
let alias = alias.into();
if alias.is_empty()
|| alias.len() > 253
|| alias.chars().any(char::is_control)
|| alias.contains([',', ':', '='])
{
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
if self.aliases.len() == MAX_ALIASES || self.aliases.contains(&alias) {
return Err(Diagnostic::new(DiagnosticCode::DeploymentDuplicateResource));
}
self.aliases.push(alias);
Ok(())
}
pub fn set_static_ipv4(&mut self, address: IpAddr) -> PodmanLensResult<()> {
if !address.is_ipv4() || self.static_ipv4.is_some() {
return Err(Diagnostic::new(DiagnosticCode::DeploymentUnsupportedCombination));
}
self.static_ipv4 = Some(address);
Ok(())
}
pub fn set_static_ipv6(&mut self, address: IpAddr) -> PodmanLensResult<()> {
if !address.is_ipv6() || self.static_ipv6.is_some() {
return Err(Diagnostic::new(DiagnosticCode::DeploymentUnsupportedCombination));
}
self.static_ipv6 = Some(address);
Ok(())
}
pub fn set_static_mac(&mut self, address: StaticMacAddress) -> PodmanLensResult<()> {
if self.static_mac.is_some() {
return Err(Diagnostic::new(DiagnosticCode::DeploymentUnsupportedCombination));
}
self.static_mac = Some(address);
Ok(())
}
#[must_use]
pub fn network(&self) -> &DeploymentResourceId {
&self.network
}
#[must_use]
pub fn aliases(&self) -> &[String] {
&self.aliases
}
#[must_use]
pub const fn static_ipv4(&self) -> Option<IpAddr> {
self.static_ipv4
}
#[must_use]
pub const fn static_ipv6(&self) -> Option<IpAddr> {
self.static_ipv6
}
#[must_use]
pub fn static_mac(&self) -> Option<&StaticMacAddress> {
self.static_mac.as_ref()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum PortProtocol {
Tcp,
Udp,
Sctp,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PortMapping {
host_ip: Option<IpAddr>,
host_port: u16,
container_port: u16,
protocol: PortProtocol,
}
impl PortMapping {
pub fn new(
host_ip: Option<IpAddr>,
host_port: u16,
container_port: u16,
protocol: PortProtocol,
) -> PodmanLensResult<Self> {
if host_port == 0 || container_port == 0 {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self {
host_ip,
host_port,
container_port,
protocol,
})
}
#[must_use]
pub const fn host_ip(&self) -> Option<IpAddr> {
self.host_ip
}
#[must_use]
pub const fn host_port(&self) -> u16 {
self.host_port
}
#[must_use]
pub const fn container_port(&self) -> u16 {
self.container_port
}
#[must_use]
pub const fn protocol(&self) -> PortProtocol {
self.protocol
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct DnsConfiguration {
servers: Vec<IpAddr>,
search: Vec<String>,
options: Vec<String>,
}
impl DnsConfiguration {
pub fn add_server(&mut self, server: IpAddr) -> PodmanLensResult<()> {
add_distinct(&mut self.servers, server, MAX_DNS_VALUES)
}
pub fn add_search(&mut self, domain: impl Into<String>) -> PodmanLensResult<()> {
add_text(&mut self.search, domain.into(), MAX_DNS_VALUES)
}
pub fn add_option(&mut self, option: impl Into<String>) -> PodmanLensResult<()> {
add_text(&mut self.options, option.into(), MAX_DNS_VALUES)
}
#[must_use]
pub fn servers(&self) -> &[IpAddr] {
&self.servers
}
#[must_use]
pub fn search(&self) -> &[String] {
&self.search
}
#[must_use]
pub fn options(&self) -> &[String] {
&self.options
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HostAlias {
address: IpAddr,
hostname: String,
}
impl HostAlias {
pub fn new(address: IpAddr, hostname: impl Into<String>) -> PodmanLensResult<Self> {
let hostname = hostname.into();
if !is_hostname(&hostname) {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self { address, hostname })
}
#[must_use]
pub const fn address(&self) -> IpAddr {
self.address
}
#[must_use]
pub fn hostname(&self) -> &str {
&self.hostname
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NetworkSubnet {
subnet: NetworkCidr,
gateway: Option<IpAddr>,
range: Option<(IpAddr, IpAddr)>,
}
impl NetworkSubnet {
#[must_use]
pub const fn new(subnet: NetworkCidr) -> Self {
Self {
subnet,
gateway: None,
range: None,
}
}
pub fn set_gateway(&mut self, gateway: IpAddr) -> PodmanLensResult<()> {
if !self.subnet.contains(gateway) || self.gateway.is_some() {
return Err(Diagnostic::new(DiagnosticCode::DeploymentUnsupportedCombination));
}
self.gateway = Some(gateway);
Ok(())
}
pub fn set_range(&mut self, start: IpAddr, end: IpAddr) -> PodmanLensResult<()> {
if !self.subnet.contains(start)
|| !self.subnet.contains(end)
|| !address_precedes_or_equals(start, end)
|| self.range.is_some()
{
return Err(Diagnostic::new(DiagnosticCode::DeploymentUnsupportedCombination));
}
self.range = Some((start, end));
Ok(())
}
#[must_use]
pub fn subnet(&self) -> &NetworkCidr {
&self.subnet
}
#[must_use]
pub const fn gateway(&self) -> Option<IpAddr> {
self.gateway
}
#[must_use]
pub const fn range(&self) -> Option<(IpAddr, IpAddr)> {
self.range
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum RouteType {
Unicast,
Blackhole,
Unreachable,
Prohibit,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NetworkRoute {
destination: NetworkCidr,
gateway: Option<IpAddr>,
route_type: RouteType,
metric: Option<u32>,
}
impl NetworkRoute {
pub fn new(destination: NetworkCidr, gateway: Option<IpAddr>, route_type: RouteType) -> PodmanLensResult<Self> {
let valid_gateway = match route_type {
RouteType::Unicast => gateway.is_some_and(|gateway| destination.has_address_family(gateway)),
RouteType::Blackhole | RouteType::Unreachable | RouteType::Prohibit => gateway.is_none(),
};
if !valid_gateway {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
Ok(Self {
destination,
gateway,
route_type,
metric: None,
})
}
pub fn set_metric(&mut self, metric: u32) -> PodmanLensResult<()> {
if self.metric.is_some() {
return Err(Diagnostic::new(DiagnosticCode::DeploymentUnsupportedCombination));
}
self.metric = Some(metric);
Ok(())
}
#[must_use]
pub fn destination(&self) -> &NetworkCidr {
&self.destination
}
#[must_use]
pub const fn gateway(&self) -> Option<IpAddr> {
self.gateway
}
#[must_use]
pub const fn route_type(&self) -> RouteType {
self.route_type
}
#[must_use]
pub const fn metric(&self) -> Option<u32> {
self.metric
}
}
pub(crate) fn add_attachment(
values: &mut Vec<NetworkAttachment>,
attachment: NetworkAttachment,
) -> PodmanLensResult<()> {
if values.len() == MAX_ATTACHMENTS || values.iter().any(|existing| existing.network == attachment.network) {
return Err(Diagnostic::new(DiagnosticCode::DeploymentDuplicateResource));
}
values.push(attachment);
Ok(())
}
pub(crate) fn add_port(values: &mut Vec<PortMapping>, mapping: PortMapping) -> PodmanLensResult<()> {
if values.len() == MAX_PORT_MAPPINGS
|| values.iter().any(|existing| {
existing.host_ip == mapping.host_ip
&& existing.host_port == mapping.host_port
&& existing.protocol == mapping.protocol
})
{
return Err(Diagnostic::new(DiagnosticCode::DeploymentDuplicateResource));
}
values.push(mapping);
Ok(())
}
pub(crate) fn add_host(values: &mut Vec<HostAlias>, alias: HostAlias) -> PodmanLensResult<()> {
add_distinct(values, alias, MAX_HOST_ALIASES)
}
pub(crate) fn add_subnet(values: &mut Vec<NetworkSubnet>, subnet: NetworkSubnet) -> PodmanLensResult<()> {
if values.len() == MAX_IPAM_SUBNETS || values.iter().any(|existing| existing.subnet == subnet.subnet) {
return Err(Diagnostic::new(DiagnosticCode::DeploymentDuplicateResource));
}
values.push(subnet);
Ok(())
}
pub(crate) fn add_route(values: &mut Vec<NetworkRoute>, route: NetworkRoute) -> PodmanLensResult<()> {
add_distinct(values, route, MAX_ROUTES)
}
fn add_distinct<T: Eq>(values: &mut Vec<T>, value: T, maximum: usize) -> PodmanLensResult<()> {
if values.len() == maximum || values.contains(&value) {
return Err(Diagnostic::new(DiagnosticCode::DeploymentDuplicateResource));
}
values.push(value);
Ok(())
}
fn add_text(values: &mut Vec<String>, value: String, maximum: usize) -> PodmanLensResult<()> {
if value.is_empty() || value.len() > 253 || value.chars().any(char::is_control) {
return Err(Diagnostic::new(DiagnosticCode::InvalidDeploymentIntent));
}
add_distinct(values, value, maximum)
}
fn is_hostname(value: &str) -> bool {
!value.is_empty()
&& value.len() <= 253
&& value.split('.').all(|label| {
!label.is_empty()
&& label.len() <= 63
&& !label.starts_with('-')
&& !label.ends_with('-')
&& label.bytes().all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
})
}
fn address_precedes_or_equals(start: IpAddr, end: IpAddr) -> bool {
match (start, end) {
(IpAddr::V4(start), IpAddr::V4(end)) => u32::from(start) <= u32::from(end),
(IpAddr::V6(start), IpAddr::V6(end)) => u128::from(start) <= u128::from(end),
_ => false,
}
}
fn masked_address(address: IpAddr, prefix: u8) -> IpAddr {
match address {
IpAddr::V4(address) => {
let mask = if prefix == 0 { 0 } else { u32::MAX << (32 - prefix) };
IpAddr::V4(std::net::Ipv4Addr::from(u32::from(address) & mask))
}
IpAddr::V6(address) => {
let mask = if prefix == 0 { 0 } else { u128::MAX << (128 - prefix) };
IpAddr::V6(std::net::Ipv6Addr::from(u128::from(address) & mask))
}
}
}