Skip to content

Commit

Permalink
feat: Docker in docker (#24)
Browse files Browse the repository at this point in the history
Co-authored-by: ndesaunais <[email protected]>
Co-authored-by: igor <[email protected]>

💚 add podman tests

✏️ fix log

💚 try fix ci

💚 reduce minimal podman version

💚 try other runner

💚 try fix ci

💚 try with pipx

💚 decrease podman min version

💚 fix ci
  • Loading branch information
ndaWefox authored and ilaborie committed Jul 15, 2024
1 parent a66c304 commit 17f691b
Show file tree
Hide file tree
Showing 33 changed files with 711 additions and 217 deletions.
42 changes: 42 additions & 0 deletions .devcontainer/dev-container-setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/bin/sh

set -e
# set -x

export DEBIAN_FRONTEND=noninteractive

# Install dependencies
apt-get update -y -qq
apt-get install -y -qq \
bash \
curl \
unzip \
time \
gettext \
ca-certificates \
gnupg \
lsb-release

# Install docker
# from https://docs.docker.com/engine/install/debian/
#shellcheck disable=SC2174
mkdir -m 0755 -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
$(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list >/dev/null
apt-get update -y -qq
apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# docker --version

# Clean up
time rm -rf /var/lib/apt/lists

# Install Rust complementary tool
rustup --version
rustup toolchain list
rustup component add clippy rustfmt

cargo --version
cargo clippy --version
cargo fmt --version
42 changes: 39 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@

on:
push:
branches: [ main ]
branches: [main]
pull_request:
branches: [ main ]
branches: [main]

name: Continuous integration

Expand Down Expand Up @@ -35,6 +34,28 @@ jobs:
- name: Tests
run: cargo test --verbose

tests_with_podman:
runs-on: ubuntu-24.04
strategy:
matrix:
include:
- rust: stable

steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
- name: Install podman compose
run: |
pipx install podman-compose
- name: Build
run: cargo build --verbose --features ensure-podman
- name: Documentation
run: cargo doc --verbose --features ensure-podman
- name: Tests
run: cargo test --verbose --features ensure-podman

clippy:
runs-on: ubuntu-latest
steps:
Expand Down Expand Up @@ -62,3 +83,18 @@ jobs:
- run: cargo +nightly hack generate-lockfile --remove-dev-deps -Z direct-minimal-versions
- name: Build
run: cargo build --verbose --all-features

docker_in_docker:
runs-on: ubuntu-latest
container:
image: public.ecr.aws/docker/library/rust:1.76

steps:
- uses: actions/checkout@v4
- name: Setup container
run: |
.devcontainer/dev-container-setup.sh
- name: Build
run: cargo build --verbose
- name: Tests
run: cargo test -- --test should_work_dind --nocapture
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ unsafe_code = "forbid"
missing_docs = "warn"

[workspace.lints.clippy]
perf = "warn"
pedantic = "warn"
cargo = "warn"
perf = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
cargo = { level = "warn", priority = -1 }

undocumented_unsafe_blocks = "deny"

Expand Down
1 change: 1 addition & 0 deletions rustainers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ very-long-tests = []
async-trait = "0.1.74"
hex = { version = "0.4.3", features = ["serde"] }
indexmap = "2.1.0"
ipnetwork = "0.20.0"
path-absolutize = "3.1.1"
regex = { version = "1.10.3", optional = true }
reqwest = { version = "0.11.24" }
Expand Down
2 changes: 1 addition & 1 deletion rustainers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ async fn do_something_with_postgres(url: String) -> anyhow::Result<()> {
}
```

## Livecycle of a container
## Lifecycle of a container

When you start a _runnable image_, the runner first check the state of the container.
It may already exists, or we may need to create it.
Expand Down
3 changes: 2 additions & 1 deletion rustainers/examples/minio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use tracing::{info, Level};

use rustainers::images::Minio;
use rustainers::runner::{RunOption, Runner};
use rustainers::Container;

mod common;
pub use self::common::*;
Expand Down Expand Up @@ -38,7 +39,7 @@ async fn main() -> anyhow::Result<()> {
Ok(())
}

async fn do_something_in_minio(minio: &Minio, bucket_name: &str) -> anyhow::Result<()> {
async fn do_something_in_minio(minio: &Container<Minio>, bucket_name: &str) -> anyhow::Result<()> {
let endpoint = minio.endpoint().await?;
info!("Using MinIO at {endpoint}");
let s3 = AmazonS3Builder::from_env()
Expand Down
3 changes: 2 additions & 1 deletion rustainers/examples/mongo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use mongodb::bson::{doc, Document};
use mongodb::{options::ClientOptions, Client};
use rustainers::images::Mongo;
use rustainers::runner::{RunOption, Runner};
use rustainers::Container;
mod common;
pub use self::common::*;

Expand All @@ -30,7 +31,7 @@ async fn main() -> anyhow::Result<()> {
Ok(())
}

async fn do_something_in_mongo(mongo: &Mongo) -> anyhow::Result<()> {
async fn do_something_in_mongo(mongo: &Container<Mongo>) -> anyhow::Result<()> {
let endpoint = mongo.endpoint().await?;
info!("Using Mongo at {endpoint}");

Expand Down
3 changes: 2 additions & 1 deletion rustainers/examples/postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use tracing::{info, warn, Level};

use rustainers::images::Postgres;
use rustainers::runner::{RunOption, Runner};
use rustainers::Container;

mod common;
pub use self::common::*;
Expand All @@ -30,7 +31,7 @@ async fn main() -> anyhow::Result<()> {
Ok(())
}

async fn do_something_in_postgres(pg: &Postgres) -> anyhow::Result<()> {
async fn do_something_in_postgres(pg: &Container<Postgres>) -> anyhow::Result<()> {
let config = pg.config().await?;

// Connect to the database.
Expand Down
3 changes: 2 additions & 1 deletion rustainers/examples/redis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use tracing::{info, Level};

use rustainers::images::Redis;
use rustainers::runner::{RunOption, Runner};
use rustainers::Container;

mod common;
pub use self::common::*;
Expand All @@ -30,7 +31,7 @@ async fn main() -> anyhow::Result<()> {
Ok(())
}

async fn do_something_in_redis(redis: &Redis) -> anyhow::Result<()> {
async fn do_something_in_redis(redis: &Container<Redis>) -> anyhow::Result<()> {
let endpoint = redis.endpoint().await?;
info!("Using Redis at {endpoint}");
let client = Client::open(endpoint)?;
Expand Down
11 changes: 10 additions & 1 deletion rustainers/src/container/id.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::fmt::{Debug, Display};
use std::ops::Deref;
use std::str::FromStr;

use serde::{Deserialize, Serialize};
Expand All @@ -17,9 +18,17 @@ use crate::{Id, IdError};
///
/// Note that the [`Display`] view truncate the id,
/// to have the full [`String`] you need to use the [`Into`] or [`From`] implementation.
#[derive(Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[derive(Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Hash)]
pub struct ContainerId(Id);

impl Deref for ContainerId {
type Target = Id;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl From<ContainerId> for String {
fn from(value: ContainerId) -> Self {
String::from(value.0)
Expand Down
80 changes: 73 additions & 7 deletions rustainers/src/container/network.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use ipnetwork::IpNetwork;
use std::borrow::Cow;
use std::fmt::Display;
use std::net::Ipv4Addr;
Expand Down Expand Up @@ -137,17 +138,57 @@ mod serde_ip {
}
}

/// A Network as described by the runner inspect command on .NetworkSettings.Networks
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub(crate) struct ContainerNetwork {
pub(crate) struct NetworkDetails {
#[serde(alias = "IPAddress")]
/// Network Ip address
pub(crate) ip_address: Option<Ip>,

/// Network gateway
#[serde(alias = "Gateway")]
pub(crate) gateway: Option<Ip>,

/// Network id
#[serde(alias = "NetworkID")]
pub(crate) id: Option<String>,
}

/// A Container as described by the runner inspect command on .Containers
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub(crate) struct HostContainer {
#[serde(alias = "Name")]
/// Container name
pub(crate) name: Option<String>,
}

/// A Network as described by the runner network command
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct NetworkInfo {
/// Name of the network
#[serde(alias = "Name")]
pub(crate) name: String,

/// Id of the network
#[serde(alias = "ID")]
pub(crate) id: ContainerId,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct IpamNetworkConfig {
#[serde(alias = "Subnet")]
pub(crate) subnet: Option<IpNetwork>,

#[serde(alias = "Gateway")]
pub(crate) gateway: Option<Ipv4Addr>,
}

#[cfg(test)]
#[allow(clippy::ignored_unit_patterns)]
mod tests {
use assert2::check;
use assert2::{check, let_assert};
use rstest::rstest;
use std::collections::HashMap;

use super::*;

Expand All @@ -162,11 +203,36 @@ mod tests {
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).expect("json");
let ip = result.ip_address.expect("IP v4").0;
#[rstest]
#[case::docker(include_str!("../../tests/assets/docker-inspect-network.json"))]
#[case::podman(include_str!("../../tests/assets/podman-inspect-network.json"))]
fn should_deserialize_network_details(#[case] json: &str) {
let result = serde_json::from_str::<NetworkDetails>(json);
let_assert!(Ok(network_detail) = result);
let ip = network_detail.ip_address.expect("IP v4").0;
check!(ip == Ipv4Addr::from([172_u8, 29, 0, 2]));
}

#[test]
fn should_deserialize_network_info() {
let json = include_str!("../../tests/assets/docker-network.json");
let result = serde_json::from_str::<NetworkInfo>(json);
let_assert!(Ok(network_info) = result);
let expected = "b79a7ee6fe69".parse::<ContainerId>();
let_assert!(Ok(expected_id) = expected);
check!(network_info.id == expected_id);
}

#[test]
fn should_deserialize_host_containers() {
let json = include_str!("../../tests/assets/docker-inspect-containers.json");
let result = serde_json::from_str::<HashMap<ContainerId, HostContainer>>(json);
let_assert!(Ok(containers) = result);
let id = "f7bbcdb277f7cc880b84219c959a5d28169ebb8c41dd32c08a9195a3c79e8d5e"
.parse::<ContainerId>();
let_assert!(Ok(container_id) = id);
let_assert!(Some(host) = containers.get(&container_id));
let_assert!(Some(container_name) = &host.name);
check!(container_name == &"dockerindocker".to_string());
}
}
2 changes: 1 addition & 1 deletion rustainers/src/container/wait_condition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub enum WaitStrategy {

/// Wait until log match a pattern
LogMatch {
///
/// the type of io
io: StdIoKind,
/// The matcher
matcher: LogMatcher,
Expand Down
29 changes: 28 additions & 1 deletion rustainers/src/id.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::fmt::{Debug, Display};
use std::hash::Hash;
use std::str::FromStr;

use hex::{decode, encode, FromHex};
Expand All @@ -13,13 +14,24 @@ use crate::IdError;
/// Note because some version of Docker CLI return truncated value,
/// we need to store the size of the id.
///
/// `PartialEq`, `Eq` and `Hash` implementation is based on all fields (size included)
///
/// Most usage of this type is done with the string representation.
///
/// Note that the [`Display`] view truncate the id,
/// to have the full [`String`] you need to use the [`Into`] or [`From`] implementation.
#[derive(Clone, Copy, PartialEq, Eq)]
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Id([u8; 32], usize);

impl Id {
/// Is ids are the same, they could have different size
#[allow(clippy::indexing_slicing)]
pub fn same(&self, other: &Self) -> bool {
let size = self.1.min(other.1);
self.0[..size] == other.0[..size]
}
}

impl From<Id> for String {
fn from(value: Id) -> Self {
let Id(data, size) = value;
Expand Down Expand Up @@ -148,6 +160,21 @@ mod tests {
check!(id.to_string() == short);
}

#[test]
fn should_compare_prefix() {
let id0 = "c94f6f8d4ef2".parse::<Id>().expect("valid id");
let id1 = "c94f6f8d4ef25b80584b9457ca24b964032681895b3a6fd7cd24fd40fad4895e"
.parse::<Id>()
.expect("valid id");
check!(id0.same(&id1) == true, "same prefix");

let id0 = "c94f6f8d4ef200".parse::<Id>().expect("valid id");
let id1 = "c94f6f8d4ef25b80584b9457ca24b964032681895b3a6fd7cd24fd40fad4895e"
.parse::<Id>()
.expect("valid id");
check!(id0.same(&id1) == false, "different prefix");
}

#[rstest]
#[case::normal("\"c94f6f8d4ef25b80584b9457ca24b964032681895b3a6fd7cd24fd40fad4895e\"")]
#[case::short("\"637ceb59b7a0\"")]
Expand Down
Loading

0 comments on commit 17f691b

Please sign in to comment.