use std::{error::Error, fmt};
use boxferry_model::{
HealthcheckCommand, HealthcheckDuration, HealthcheckRetries, Identifier, ImageReference, Mount, NetworkAttachment,
Port, ProtectedString, RestartPolicy, SourceId,
};
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum RuntimeImplementation {
Docker,
Podman,
Other(Identifier),
}
impl RuntimeImplementation {
#[must_use]
pub fn as_str(&self) -> &str {
match self {
Self::Docker => "docker",
Self::Podman => "podman",
Self::Other(name) => name.as_str(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum EffectiveCommand {
Exec(Vec<ProtectedString>),
Empty,
}
impl EffectiveCommand {
#[must_use]
pub fn exec<I, S>(arguments: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self::Exec(
arguments
.into_iter()
.map(|argument| ProtectedString::sensitive(argument.into()))
.collect(),
)
}
#[must_use]
pub fn arguments(&self) -> Option<&[ProtectedString]> {
match self {
Self::Exec(arguments) => Some(arguments),
Self::Empty => None,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RuntimeEnvironmentVariable {
name: Identifier,
value: ProtectedString,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RuntimeMetadataLabel {
name: Identifier,
value: ProtectedString,
}
impl RuntimeMetadataLabel {
#[must_use]
pub fn new(name: Identifier, value: impl Into<String>) -> Self {
Self {
name,
value: ProtectedString::sensitive(value),
}
}
#[must_use]
pub const fn name(&self) -> &Identifier {
&self.name
}
#[must_use]
pub const fn value(&self) -> &ProtectedString {
&self.value
}
}
impl RuntimeEnvironmentVariable {
#[must_use]
pub fn new(name: Identifier, value: impl Into<String>) -> Self {
Self {
name,
value: ProtectedString::sensitive(value),
}
}
#[must_use]
pub const fn name(&self) -> &Identifier {
&self.name
}
#[must_use]
pub const fn value(&self) -> &ProtectedString {
&self.value
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct RuntimeHealthcheck {
command: Option<HealthcheckCommand>,
disabled: Option<bool>,
interval: Option<HealthcheckDuration>,
timeout: Option<HealthcheckDuration>,
retries: Option<HealthcheckRetries>,
start_period: Option<HealthcheckDuration>,
start_interval: Option<HealthcheckDuration>,
}
impl RuntimeHealthcheck {
#[must_use]
pub const fn new() -> Self {
Self {
command: None,
disabled: None,
interval: None,
timeout: None,
retries: None,
start_period: None,
start_interval: None,
}
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.command.is_none()
&& self.disabled.is_none()
&& self.interval.is_none()
&& self.timeout.is_none()
&& self.retries.is_none()
&& self.start_period.is_none()
&& self.start_interval.is_none()
}
pub fn set_command(&mut self, command: HealthcheckCommand) {
self.command = Some(command);
}
#[must_use]
pub const fn command(&self) -> Option<&HealthcheckCommand> {
self.command.as_ref()
}
pub fn set_disabled(&mut self, disabled: bool) {
self.disabled = Some(disabled);
}
#[must_use]
pub const fn disabled(&self) -> Option<bool> {
self.disabled
}
pub fn set_interval(&mut self, interval: HealthcheckDuration) {
self.interval = Some(interval);
}
#[must_use]
pub const fn interval(&self) -> Option<&HealthcheckDuration> {
self.interval.as_ref()
}
pub fn set_timeout(&mut self, timeout: HealthcheckDuration) {
self.timeout = Some(timeout);
}
#[must_use]
pub const fn timeout(&self) -> Option<&HealthcheckDuration> {
self.timeout.as_ref()
}
pub fn set_retries(&mut self, retries: HealthcheckRetries) {
self.retries = Some(retries);
}
#[must_use]
pub const fn retries(&self) -> Option<&HealthcheckRetries> {
self.retries.as_ref()
}
pub fn set_start_period(&mut self, start_period: HealthcheckDuration) {
self.start_period = Some(start_period);
}
#[must_use]
pub const fn start_period(&self) -> Option<&HealthcheckDuration> {
self.start_period.as_ref()
}
pub fn set_start_interval(&mut self, start_interval: HealthcheckDuration) {
self.start_interval = Some(start_interval);
}
#[must_use]
pub const fn start_interval(&self) -> Option<&HealthcheckDuration> {
self.start_interval.as_ref()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CreationEvidence {
source_id: SourceId,
arguments: Vec<ProtectedString>,
}
impl CreationEvidence {
#[must_use]
pub fn new<I, S>(source_id: SourceId, arguments: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self {
source_id,
arguments: arguments
.into_iter()
.map(|argument| ProtectedString::sensitive(argument.into()))
.collect(),
}
}
#[must_use]
pub const fn source_id(&self) -> &SourceId {
&self.source_id
}
#[must_use]
pub fn arguments(&self) -> &[ProtectedString] {
&self.arguments
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ImageObservation {
source_id: SourceId,
command: Option<EffectiveCommand>,
environment: Option<Vec<RuntimeEnvironmentVariable>>,
labels: Option<Vec<RuntimeMetadataLabel>>,
user: Option<ProtectedString>,
working_directory: Option<ProtectedString>,
healthcheck: Option<RuntimeHealthcheck>,
}
impl ImageObservation {
#[must_use]
pub const fn new(source_id: SourceId) -> Self {
Self {
source_id,
command: None,
environment: None,
labels: None,
user: None,
working_directory: None,
healthcheck: None,
}
}
#[must_use]
pub const fn source_id(&self) -> &SourceId {
&self.source_id
}
pub fn set_command(&mut self, command: EffectiveCommand) {
self.command = Some(command);
}
#[must_use]
pub const fn command(&self) -> Option<&EffectiveCommand> {
self.command.as_ref()
}
pub fn set_environment(&mut self, environment: Vec<RuntimeEnvironmentVariable>) {
self.environment = Some(environment);
}
#[must_use]
pub fn environment(&self) -> Option<&[RuntimeEnvironmentVariable]> {
self.environment.as_deref()
}
pub fn set_labels(&mut self, labels: Vec<RuntimeMetadataLabel>) {
self.labels = Some(labels);
}
#[must_use]
pub fn labels(&self) -> Option<&[RuntimeMetadataLabel]> {
self.labels.as_deref()
}
pub fn set_user(&mut self, user: impl Into<String>) {
self.user = Some(ProtectedString::sensitive(user));
}
#[must_use]
pub const fn user(&self) -> Option<&ProtectedString> {
self.user.as_ref()
}
pub fn set_working_directory(&mut self, working_directory: impl Into<String>) {
self.working_directory = Some(ProtectedString::sensitive(working_directory));
}
#[must_use]
pub const fn working_directory(&self) -> Option<&ProtectedString> {
self.working_directory.as_ref()
}
pub fn set_healthcheck(&mut self, healthcheck: RuntimeHealthcheck) {
self.healthcheck = Some(healthcheck);
}
#[must_use]
pub const fn healthcheck(&self) -> Option<&RuntimeHealthcheck> {
self.healthcheck.as_ref()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ContainerObservation {
source_id: SourceId,
name: Identifier,
image: Option<ImageReference>,
image_source_id: Option<SourceId>,
command: Option<EffectiveCommand>,
restart_policy: Option<RestartPolicy>,
environment: Option<Vec<RuntimeEnvironmentVariable>>,
labels: Option<Vec<RuntimeMetadataLabel>>,
user: Option<ProtectedString>,
working_directory: Option<ProtectedString>,
healthcheck: Option<RuntimeHealthcheck>,
read_only_root_filesystem: Option<bool>,
ports: Vec<Port>,
mounts: Vec<Mount>,
networks: Vec<NetworkAttachment>,
pod_source_id: Option<SourceId>,
creation_evidence: Option<CreationEvidence>,
}
impl ContainerObservation {
#[must_use]
pub const fn new(source_id: SourceId, name: Identifier) -> Self {
Self {
source_id,
name,
image: None,
image_source_id: None,
command: None,
restart_policy: None,
environment: None,
labels: None,
user: None,
working_directory: None,
healthcheck: None,
read_only_root_filesystem: None,
ports: Vec::new(),
mounts: Vec::new(),
networks: Vec::new(),
pod_source_id: None,
creation_evidence: None,
}
}
#[must_use]
pub const fn source_id(&self) -> &SourceId {
&self.source_id
}
#[must_use]
pub const fn name(&self) -> &Identifier {
&self.name
}
pub fn set_image(&mut self, image: ImageReference, image_source_id: Option<SourceId>) {
self.image = Some(image);
self.image_source_id = image_source_id;
}
#[must_use]
pub const fn image(&self) -> Option<&ImageReference> {
self.image.as_ref()
}
#[must_use]
pub const fn image_source_id(&self) -> Option<&SourceId> {
self.image_source_id.as_ref()
}
pub fn set_command(&mut self, command: EffectiveCommand) {
self.command = Some(command);
}
#[must_use]
pub const fn command(&self) -> Option<&EffectiveCommand> {
self.command.as_ref()
}
pub fn set_restart_policy(&mut self, restart_policy: RestartPolicy) {
self.restart_policy = Some(restart_policy);
}
#[must_use]
pub const fn restart_policy(&self) -> Option<RestartPolicy> {
self.restart_policy
}
pub fn set_environment(&mut self, environment: Vec<RuntimeEnvironmentVariable>) {
self.environment = Some(environment);
}
#[must_use]
pub fn environment(&self) -> Option<&[RuntimeEnvironmentVariable]> {
self.environment.as_deref()
}
pub fn set_labels(&mut self, labels: Vec<RuntimeMetadataLabel>) {
self.labels = Some(labels);
}
#[must_use]
pub fn labels(&self) -> Option<&[RuntimeMetadataLabel]> {
self.labels.as_deref()
}
pub fn set_user(&mut self, user: impl Into<String>) {
self.user = Some(ProtectedString::sensitive(user));
}
#[must_use]
pub const fn user(&self) -> Option<&ProtectedString> {
self.user.as_ref()
}
pub fn set_working_directory(&mut self, working_directory: impl Into<String>) {
self.working_directory = Some(ProtectedString::sensitive(working_directory));
}
#[must_use]
pub const fn working_directory(&self) -> Option<&ProtectedString> {
self.working_directory.as_ref()
}
pub fn set_healthcheck(&mut self, healthcheck: RuntimeHealthcheck) {
self.healthcheck = Some(healthcheck);
}
#[must_use]
pub const fn healthcheck(&self) -> Option<&RuntimeHealthcheck> {
self.healthcheck.as_ref()
}
pub fn set_read_only_root_filesystem(&mut self, read_only: bool) {
self.read_only_root_filesystem = Some(read_only);
}
#[must_use]
pub const fn read_only_root_filesystem(&self) -> Option<bool> {
self.read_only_root_filesystem
}
pub fn add_port(&mut self, port: Port) {
self.ports.push(port);
}
#[must_use]
pub fn ports(&self) -> &[Port] {
&self.ports
}
pub fn add_mount(&mut self, mount: Mount) {
self.mounts.push(mount);
}
#[must_use]
pub fn mounts(&self) -> &[Mount] {
&self.mounts
}
pub fn add_network(&mut self, network: NetworkAttachment) {
self.networks.push(network);
}
#[must_use]
pub fn networks(&self) -> &[NetworkAttachment] {
&self.networks
}
pub fn set_pod_source_id(&mut self, source_id: SourceId) {
self.pod_source_id = Some(source_id);
}
#[must_use]
pub const fn pod_source_id(&self) -> Option<&SourceId> {
self.pod_source_id.as_ref()
}
pub fn set_creation_evidence(&mut self, evidence: CreationEvidence) {
self.creation_evidence = Some(evidence);
}
#[must_use]
pub const fn creation_evidence(&self) -> Option<&CreationEvidence> {
self.creation_evidence.as_ref()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NetworkObservation {
source_id: SourceId,
name: Identifier,
}
impl NetworkObservation {
#[must_use]
pub const fn new(source_id: SourceId, name: Identifier) -> Self {
Self { source_id, name }
}
#[must_use]
pub const fn source_id(&self) -> &SourceId {
&self.source_id
}
#[must_use]
pub const fn name(&self) -> &Identifier {
&self.name
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VolumeObservation {
source_id: SourceId,
name: Identifier,
}
impl VolumeObservation {
#[must_use]
pub const fn new(source_id: SourceId, name: Identifier) -> Self {
Self { source_id, name }
}
#[must_use]
pub const fn source_id(&self) -> &SourceId {
&self.source_id
}
#[must_use]
pub const fn name(&self) -> &Identifier {
&self.name
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PodObservation {
source_id: SourceId,
name: Identifier,
members: Vec<SourceId>,
creation_evidence: Option<CreationEvidence>,
}
impl PodObservation {
#[must_use]
pub const fn new(source_id: SourceId, name: Identifier) -> Self {
Self {
source_id,
name,
members: Vec::new(),
creation_evidence: None,
}
}
#[must_use]
pub const fn source_id(&self) -> &SourceId {
&self.source_id
}
#[must_use]
pub const fn name(&self) -> &Identifier {
&self.name
}
pub fn add_member(&mut self, source_id: SourceId) {
self.members.push(source_id);
}
#[must_use]
pub fn members(&self) -> &[SourceId] {
&self.members
}
pub fn set_creation_evidence(&mut self, evidence: CreationEvidence) {
self.creation_evidence = Some(evidence);
}
#[must_use]
pub const fn creation_evidence(&self) -> Option<&CreationEvidence> {
self.creation_evidence.as_ref()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RuntimeSnapshot {
application_name: Identifier,
implementation: RuntimeImplementation,
containers: Vec<ContainerObservation>,
images: Vec<ImageObservation>,
networks: Vec<NetworkObservation>,
volumes: Vec<VolumeObservation>,
pods: Vec<PodObservation>,
}
impl RuntimeSnapshot {
#[must_use]
pub const fn new(application_name: Identifier, implementation: RuntimeImplementation) -> Self {
Self {
application_name,
implementation,
containers: Vec::new(),
images: Vec::new(),
networks: Vec::new(),
volumes: Vec::new(),
pods: Vec::new(),
}
}
#[must_use]
pub const fn application_name(&self) -> &Identifier {
&self.application_name
}
#[must_use]
pub const fn implementation(&self) -> &RuntimeImplementation {
&self.implementation
}
pub fn add_container(&mut self, container: ContainerObservation) -> Result<(), RuntimeSnapshotError> {
self.ensure_source_unique(container.source_id())?;
Self::ensure_name_unique(
"container",
container.name(),
self.containers.iter().map(ContainerObservation::name),
)?;
self.containers.push(container);
Ok(())
}
#[must_use]
pub fn containers(&self) -> &[ContainerObservation] {
&self.containers
}
pub fn add_image(&mut self, image: ImageObservation) -> Result<(), RuntimeSnapshotError> {
self.ensure_source_unique(image.source_id())?;
self.images.push(image);
Ok(())
}
#[must_use]
pub fn images(&self) -> &[ImageObservation] {
&self.images
}
pub fn add_network(&mut self, network: NetworkObservation) -> Result<(), RuntimeSnapshotError> {
self.ensure_source_unique(network.source_id())?;
Self::ensure_name_unique(
"network",
network.name(),
self.networks.iter().map(NetworkObservation::name),
)?;
self.networks.push(network);
Ok(())
}
#[must_use]
pub fn networks(&self) -> &[NetworkObservation] {
&self.networks
}
pub fn add_volume(&mut self, volume: VolumeObservation) -> Result<(), RuntimeSnapshotError> {
self.ensure_source_unique(volume.source_id())?;
Self::ensure_name_unique(
"volume",
volume.name(),
self.volumes.iter().map(VolumeObservation::name),
)?;
self.volumes.push(volume);
Ok(())
}
#[must_use]
pub fn volumes(&self) -> &[VolumeObservation] {
&self.volumes
}
pub fn add_pod(&mut self, pod: PodObservation) -> Result<(), RuntimeSnapshotError> {
self.ensure_source_unique(pod.source_id())?;
Self::ensure_name_unique("pod", pod.name(), self.pods.iter().map(PodObservation::name))?;
self.pods.push(pod);
Ok(())
}
#[must_use]
pub fn pods(&self) -> &[PodObservation] {
&self.pods
}
fn ensure_source_unique(&self, source_id: &SourceId) -> Result<(), RuntimeSnapshotError> {
if self.source_ids().any(|candidate| candidate == source_id) {
return Err(RuntimeSnapshotError::DuplicateSourceIdentity {
source_id: source_id.clone(),
});
}
Ok(())
}
fn ensure_name_unique<'a>(
kind: &'static str,
name: &Identifier,
existing: impl Iterator<Item = &'a Identifier>,
) -> Result<(), RuntimeSnapshotError> {
if existing.into_iter().any(|candidate| candidate == name) {
return Err(RuntimeSnapshotError::DuplicateResource {
kind,
name: name.as_str().to_owned(),
});
}
Ok(())
}
fn source_ids(&self) -> impl Iterator<Item = &SourceId> {
self.containers
.iter()
.map(ContainerObservation::source_id)
.chain(self.images.iter().map(ImageObservation::source_id))
.chain(self.networks.iter().map(NetworkObservation::source_id))
.chain(self.volumes.iter().map(VolumeObservation::source_id))
.chain(self.pods.iter().map(PodObservation::source_id))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum RuntimeSnapshotError {
DuplicateSourceIdentity {
source_id: SourceId,
},
DuplicateResource {
kind: &'static str,
name: String,
},
}
impl fmt::Display for RuntimeSnapshotError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::DuplicateSourceIdentity { source_id } => {
write!(formatter, "duplicate runtime source identity `{}`", source_id.as_str())
}
Self::DuplicateResource { kind, name } => write!(formatter, "duplicate runtime {kind} `{name}`"),
}
}
}
impl Error for RuntimeSnapshotError {}
#[cfg(test)]
mod tests {
use boxferry_model::{Identifier, SourceId};
use super::{
ContainerObservation, CreationEvidence, EffectiveCommand, RuntimeEnvironmentVariable, RuntimeImplementation,
RuntimeMetadataLabel, RuntimeSnapshot,
};
#[test]
fn inspected_values_are_redacted_by_default() -> Result<(), String> {
let command = EffectiveCommand::exec(["server", "--password=never-print-this"]);
let environment = RuntimeEnvironmentVariable::new(id("PASSWORD")?, "never-print-this");
let label = RuntimeMetadataLabel::new(id("com.example.token")?, "never-print-this");
let mut container = ContainerObservation::new(source("runtime:podman:container:web")?, id("web")?);
container.set_user("never-print-this");
container.set_working_directory("/never-print-this");
let evidence = CreationEvidence::new(
source("runtime:podman:create:web")?,
["--env", "PASSWORD=never-print-this"],
);
for debug in [
format!("{command:?}"),
format!("{environment:?}"),
format!("{label:?}"),
format!("{container:?}"),
format!("{evidence:?}"),
] {
assert!(!debug.contains("never-print-this"));
assert!(debug.contains("[REDACTED]"));
}
Ok(())
}
#[test]
fn snapshot_rejects_ambiguous_source_identities_and_names() -> Result<(), String> {
let mut snapshot = RuntimeSnapshot::new(id("example")?, RuntimeImplementation::Podman);
snapshot
.add_container(ContainerObservation::new(source("runtime:container:web")?, id("web")?))
.map_err(|error| error.to_string())?;
let duplicate_source = snapshot
.add_container(ContainerObservation::new(
source("runtime:container:web")?,
id("worker")?,
))
.err()
.ok_or("duplicate source must fail")?;
assert!(duplicate_source.to_string().contains("source identity"));
let duplicate_name = snapshot
.add_container(ContainerObservation::new(
source("runtime:container:web-2")?,
id("web")?,
))
.err()
.ok_or("duplicate name must fail")?;
assert!(duplicate_name.to_string().contains("container `web`"));
Ok(())
}
fn id(value: &str) -> Result<Identifier, String> {
Identifier::new(value).map_err(|error| error.to_string())
}
fn source(value: &str) -> Result<SourceId, String> {
SourceId::new(value).map_err(|error| error.to_string())
}
}