Skip to content

Commit

Permalink
feat: Add network option
Browse files Browse the repository at this point in the history
  • Loading branch information
ilaborie committed Jan 1, 2024
1 parent 9a2ad16 commit 7ccc0e4
Show file tree
Hide file tree
Showing 12 changed files with 487 additions and 8 deletions.
2 changes: 1 addition & 1 deletion rustainers/src/compose/temp_dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ impl TemporaryDirectory {
Ok(())
}

// FIXME create from an existing path, with keeping the permissions
// TODO create from an existing path, with keeping the permissions

/// Detach the temp. directory.
///
Expand Down
3 changes: 3 additions & 0 deletions rustainers/src/container/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ pub(crate) use self::process::ContainerProcess;
mod wait_condition;
pub use self::wait_condition::*;

mod network;
pub use self::network::*;

mod health;
pub(crate) use self::health::ContainerHealth;

Expand Down
172 changes: 172 additions & 0 deletions rustainers/src/container/network.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
use std::borrow::Cow;
use std::fmt::Display;
use std::net::Ipv4Addr;

use serde::{Deserialize, Serialize};

use crate::ContainerId;

/// Network settings
///
/// See [docker reference](https://docs.docker.com/engine/reference/run/#network-settings)
///
/// # Examples
///
/// ```
/// # use rustainers::{ContainerId, Network};
/// // Default network is bridge
/// assert_eq!(Network::default(), Network::Bridge);
///
/// // A network based on a container
/// let container_id = "123abc".parse::<ContainerId>().unwrap();
/// assert_eq!(Network::from(container_id), Network::Container(container_id));
///
/// // A custom network
/// assert_eq!(Network::from("my-network"), Network::Custom(String::from("my-network")));
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum Network {
/// Create a network stack on the default Docker bridge
#[default]
Bridge,
/// No networking
None,
/// Reuse another container's network stack
Container(ContainerId), // TODO could be ContainerName
/// Use the Docker host network stack
Host,
/// Connect to a user-definined network
Custom(String),
}

impl Network {
pub(crate) fn cmd_arg(&self) -> Cow<'static, str> {
match self {
Self::Bridge => Cow::Borrowed("--network=bridge"),
Self::None => Cow::Borrowed("--network=none"),
Self::Container(c) => Cow::Owned(format!("--network=container:{c}")),
Self::Host => Cow::Borrowed("--network=host"),
Self::Custom(name) => Cow::Owned(format!("--network={name}")),
}
}

pub(crate) fn name(&self) -> Option<&str> {
match self {
Self::Bridge => Some("bridge"),
Self::None => Some("none"),
Self::Container(_) => None,
Self::Host => Some("host"),
Self::Custom(c) => Some(c),
}
}
}

impl Display for Network {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Bridge => write!(f, "bridge"),
Self::None => write!(f, "none"),
Self::Container(c) => write!(f, "container:{c}"),
Self::Host => write!(f, "host"),
Self::Custom(c) => write!(f, "{c}"),
}
}
}

impl From<&str> for Network {
fn from(value: &str) -> Self {
Self::Custom(String::from(value))
}
}

impl From<String> for Network {
fn from(value: String) -> Self {
Self::Custom(value)
}
}

impl From<ContainerId> for Network {
fn from(value: ContainerId) -> Self {
Self::Container(value)
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Ip(pub(crate) Ipv4Addr);

mod serde_ip {
use std::net::Ipv4Addr;

use serde::de::Visitor;
use serde::{Deserialize, Serialize};

use super::Ip;

impl Serialize for Ip {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.0.to_string())
}
}

struct IpVisitor;
impl<'de> Visitor<'de> for IpVisitor {
type Value = Ip;

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("an IPv4 as string")
}

fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
v.parse::<Ipv4Addr>().map(Ip).map_err(E::custom)
}
}

impl<'de> Deserialize<'de> for Ip {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_str(IpVisitor)
}
}
}

#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub(crate) struct ContainerNetwork {
#[serde(alias = "IPAddress")]
pub(crate) ip_address: Option<Ip>,
}

#[cfg(test)]
#[allow(clippy::ignored_unit_patterns)]
mod tests {
use assert2::check;
use rstest::rstest;

use super::*;

#[rstest]
#[case::bridge(Network::Bridge, "--network=bridge")]
#[case::none(Network::None, "--network=none")]
#[case::container(Network::Container("123456".parse().unwrap()), "--network=container:123456")]
#[case::host(Network::Host, "--network=host")]
#[case::custom("user-defined-net".into(), "--network=user-defined-net")]
fn should_provide_arg(#[case] network: Network, #[case] expected: &str) {
let arg = network.cmd_arg();
check!(arg.as_ref() == expected);
}

#[test]
fn should_deserialize_container_network() {
let json = include_str!("../../tests/assets/docker-inspect-network.json");
let result = serde_json::from_str::<ContainerNetwork>(json).unwrap();
let ip = result.ip_address.unwrap().0;
check!(ip == Ipv4Addr::from([172_u8, 29, 0, 2]));
}
}
1 change: 0 additions & 1 deletion rustainers/src/container/runnable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ pub struct RunnableContainer {
/// The ports mapping
#[builder(default, setter(transform = |args: impl IntoIterator<Item = ExposedPort>| args.into_iter().collect()))]
pub(crate) port_mappings: Vec<ExposedPort>,
// TODO networks
// TODO volumes
// TODO entrypoint
}
Expand Down
50 changes: 49 additions & 1 deletion rustainers/src/runner/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::path::PathBuf;

use crate::cmd::CommandError;
use crate::version::Version;
use crate::{ContainerId, IdError, Port, RunnableContainer};
use crate::{ContainerId, IdError, Network, Port, RunnableContainer};

use super::Runner;

Expand Down Expand Up @@ -53,6 +53,54 @@ pub enum RunnerError {
source: Box<ContainerError>,
},

/// Fail to exec a container
#[error("Fail to create network '{name}' because {source}\nrunner: {runner}")]
CreateNetworkError {
/// The runner
runner: Runner,
/// The network name
name: String,
/// The source error
source: Box<ContainerError>,
},

/// Fail to retrieve container IP in a specific network
#[error(
"Fail to retrieve container {container} IP for network '{network}' because {source}\nrunner: {runner}"
)]
FindNetworkIpError {
/// The runner
runner: Runner,
/// The network
network: Box<Network>,
/// The container,
container: Box<ContainerId>,
/// The source error
source: Box<ContainerError>,
},

/// Expected a network name
#[error("Fail to retrieve container {container} IP because we expect a network with name, got {network}")]
ExpectedNetworkNameError {
/// The runner
runner: Runner,
/// The network
network: Box<Network>,
/// The container,
container: ContainerId,
},

/// No IP found
#[error("No IP found for container {container} and network {network}")]
NoNetworkIp {
/// The runner
runner: Runner,
/// The network
network: Box<Network>,
/// The container,
container: ContainerId,
},

/// Fail to stop a container
#[error("Fail to stop container {id} because {source}\nrunner: {runner}")]
StopError {
Expand Down
33 changes: 29 additions & 4 deletions rustainers/src/runner/inner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use tracing::{info, trace, warn};

use crate::cmd::Cmd;
use crate::{
ContainerHealth, ContainerId, ContainerProcess, ContainerState, ContainerStatus, Port,
RunnableContainer, WaitStrategy,
ContainerHealth, ContainerId, ContainerNetwork, ContainerProcess, ContainerState,
ContainerStatus, Network, Port, RunnableContainer, WaitStrategy,
};

use super::{ContainerError, RunOption};
Expand All @@ -35,12 +35,21 @@ pub(crate) trait InnerRunner: Display + Debug + Send + Sync {
Ok(result)
}

#[tracing::instrument(level = "debug", skip(self), fields(runner = %self))]
async fn create_network(&self, name: &str) -> Result<(), ContainerError> {
let mut cmd = self.command();
cmd.push_args(["network", "create", name]);
cmd.status().await?;
Ok(())
}

#[tracing::instrument(level = "debug", skip(self, image), fields(runner = %self, image = %image))]
async fn create_and_start(
&self,
image: &RunnableContainer,
remove: bool,
name: Option<&str>,
network: &Network,
) -> Result<ContainerId, ContainerError> {
let mut cmd = self.command();
cmd.push_args(["run", "--detach"]);
Expand Down Expand Up @@ -72,6 +81,10 @@ pub(crate) trait InnerRunner: Display + Debug + Send + Sync {
cmd.push_args(hc.to_vec());
}

// Network
let network = network.cmd_arg();
cmd.push_arg(network.as_ref());

// descriptor (name:tag or other alternatives)
cmd.push_arg(descriptor);

Expand Down Expand Up @@ -145,6 +158,15 @@ pub(crate) trait InnerRunner: Display + Debug + Send + Sync {
self.inspect(id, ".State").await
}

async fn network_ip(
&self,
id: ContainerId,
network: &str,
) -> Result<ContainerNetwork, ContainerError> {
let path = format!(".NetworkSettings.Networks.{network}");
self.inspect(id, &path).await
}

#[tracing::instrument(level = "debug", skip(self, id), fields(runner = %self, id = %id))]
async fn wait_ready(
&self,
Expand Down Expand Up @@ -239,6 +261,7 @@ pub(crate) trait InnerRunner: Display + Debug + Send + Sync {
wait_interval,
remove,
name,
network,
} = options;

// Container name
Expand Down Expand Up @@ -269,11 +292,13 @@ pub(crate) trait InnerRunner: Display + Debug + Send + Sync {
// Need cleanup before restarting the container
Some((ContainerStatus::Dead, id)) => {
self.rm(id).await?;
self.create_and_start(image, remove, container_name).await?
self.create_and_start(image, remove, container_name, &network)
.await?
}
// Need to create and start the container
Some((ContainerStatus::Unknown, _)) | None => {
self.create_and_start(image, remove, container_name).await?
self.create_and_start(image, remove, container_name, &network)
.await?
}
};

Expand Down
Loading

0 comments on commit 7ccc0e4

Please sign in to comment.