diff --git a/.github/workflows/infratest.yml b/.github/workflows/infratest.yml new file mode 100644 index 0000000..9ef2e15 --- /dev/null +++ b/.github/workflows/infratest.yml @@ -0,0 +1,19 @@ +name: Linux Distribution Test Pipeline + +on: [push, pull_request] + +jobs: + infratest: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .env file + run: | + cp .env.example .env + + - name: Build and run infratest + run: | + cd infratest + cargo run diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 28942ce..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,127 +0,0 @@ -name: Linux Distribution Test Pipeline - -on: [push, pull_request] - -jobs: - test: - runs-on: ubuntu-latest - - services: - mongodb: - image: mongo:5.0.6 - ports: - - 27017:27017 - options: >- - --health-cmd mongo - --health-interval 10s - --health-timeout 5s - --health-retries 5 - env: - MONGO_INITDB_ROOT_USERNAME: root - MONGO_INITDB_ROOT_PASSWORD: pass - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup .env file - run: | - cp .env.example .env - - - name: Build and run server container - run: | - docker build -t server -f images/server-ci.Dockerfile . - docker run -d --network host --name server-container server - - - name: Wait for server to start - run: | - TIMEOUT=60 - ELAPSED=0 - - until curl --output /dev/null --silent --fail http://localhost:3000; do - if [ "$ELAPSED" -ge "$TIMEOUT" ]; then - echo "Server did not start within $TIMEOUT seconds. But continuing..." - exit 0 - fi - echo "Waiting for server..." - sleep 5 - ELAPSED=$((ELAPSED + 5)) - done - echo "Server is up!" - continue-on-error: true - - - name: Build and run Ubuntu 24.04 container - run: | - docker build -t ubuntu24.04 -f images/ubuntu24.04.Dockerfile scripts/ - docker run --name ubuntu24.04-test --network host ubuntu24.04 || true - - - name: Build and run Fedora 42 container - run: | - docker build -t fedora42 -f images/fedora42.Dockerfile scripts/ - docker run --name fedora42-test --network host fedora42 || true - - - name: Build and run Debian 12 container - run: | - docker build -t debian12 -f images/debian12.Dockerfile scripts/ - docker run --name debian12-test --network host debian12 || true - - - name: Build and run Ubuntu 24.04 container for checking multiple package support - run: | - docker build -t ubuntu24.04-multi -f images/ubuntu24.04-multi-package.Dockerfile scripts/ - docker run --name ubuntu24.04-multitest --network host ubuntu24.04-multi || true - - - name: Build and run Fedora 42 container for checking multiple package support - run: | - docker build -t fedora42 -f images/fedora42-multi-package.Dockerfile scripts/ - docker run --name fedora42-multitest --network host fedora42 || true - - - name: Build and run Debian 12 container for checking multiple package support - run: | - docker build -t debian12-multi -f images/debian12-multi-package.Dockerfile scripts/ - docker run --name debian12-multitest --network host debian12-multi || true - - - name: Build and run OpenSuse Tumbleweed container for checking multiple package support - run: | - docker build -t tumbleweed-multi -f images/tumbleweed-multi-package.Dockerfile scripts/ - docker run --name tumbleweed-multitest --network host tumbleweed-multi || true - - - name: Check server logs - run: | - docker logs server-container || true - - - name: Check client containers' statuses - run: | - if [ "$(docker inspect -f '{{.State.ExitCode}}' ubuntu24.04-test)" -ne 0 ]; then - echo "Test on Ubuntu 24.04 failed" - exit 1 - fi - - if [ "$(docker inspect -f '{{.State.ExitCode}}' fedora42-test)" -ne 0 ]; then - echo "Test on Fedora 42 failed" - exit 1 - fi - - if [ "$(docker inspect -f '{{.State.ExitCode}}' debian12-test)" -ne 0 ]; then - echo "Test on Debian 12 failed" - exit 1 - fi - - if [ "$(docker inspect -f '{{.State.ExitCode}}' ubuntu24.04-multitest)" -ne 0 ]; then - echo "Test on Ubuntu 23.04 for multiple package support failed" - exit 1 - fi - - if [ "$(docker inspect -f '{{.State.ExitCode}}' fedora42-multitest)" -ne 0 ]; then - echo "Test on Fedora 42 failed multiple package support failed" - exit 1 - fi - - if [ "$(docker inspect -f '{{.State.ExitCode}}' debian12-multitest)" -ne 0 ]; then - echo "Test on Debian 12 for multiple package support failed" - exit 1 - fi - - if [ "$(docker inspect -f '{{.State.ExitCode}}' tumbleweed-multitest)" -ne 0 ]; then - echo "Test on OpenSuse Tumbleweed for multiple package support failed" - exit 1 - fi diff --git a/.gitignore b/.gitignore index 2dda9f2..b20b9f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Generated by Cargo # will have compiled files and executables /target/ +/infratest/target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html @@ -13,3 +14,6 @@ packhub.asc secret_key.asc .env key.gpg + +.DS_Store +**/.DS_Store diff --git a/images/debian12-multi-package.Dockerfile b/images/debian12-multi-package.Dockerfile deleted file mode 100644 index a0de12b..0000000 --- a/images/debian12-multi-package.Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM debian:12 - -WORKDIR /app - -COPY . . - -ENV DIST=debian - -ENTRYPOINT ["./check_apt_multiple.sh"] diff --git a/images/debian12.Dockerfile b/images/debian12.Dockerfile deleted file mode 100644 index c73b038..0000000 --- a/images/debian12.Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM debian:12 - -WORKDIR /app - -COPY . . - -ENV DIST=debian - -ENTRYPOINT ["./check_apt.sh"] diff --git a/images/fedora42-multi-package.Dockerfile b/images/fedora42-multi-package.Dockerfile deleted file mode 100644 index 3796956..0000000 --- a/images/fedora42-multi-package.Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM fedora:42 - -WORKDIR /app - -COPY . . - -ENTRYPOINT ["./check_dnf_multiple.sh"] diff --git a/images/fedora42.Dockerfile b/images/fedora42.Dockerfile deleted file mode 100644 index c70a7b7..0000000 --- a/images/fedora42.Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM fedora:42 - -WORKDIR /app - -COPY . . - -ENTRYPOINT ["./check_dnf.sh"] diff --git a/images/tumbleweed-multi-package.Dockerfile b/images/tumbleweed-multi-package.Dockerfile deleted file mode 100644 index 4b9648a..0000000 --- a/images/tumbleweed-multi-package.Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM opensuse/tumbleweed:latest - -WORKDIR /app - -COPY . . - -ENTRYPOINT ["./check_zypper_multiple.sh"] diff --git a/images/ubuntu24.04-multi-package.Dockerfile b/images/ubuntu24.04-multi-package.Dockerfile deleted file mode 100644 index 07b8a94..0000000 --- a/images/ubuntu24.04-multi-package.Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM ubuntu:24.04 - -WORKDIR /app - -COPY . . - -ENV DIST=ubuntu - -ENTRYPOINT ["./check_apt_multiple.sh"] diff --git a/images/ubuntu24.04.Dockerfile b/images/ubuntu24.04.Dockerfile deleted file mode 100644 index 83f61b3..0000000 --- a/images/ubuntu24.04.Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM ubuntu:24.04 - -WORKDIR /app - -COPY . . - -ENV DIST=ubuntu - -ENTRYPOINT ["./check_apt.sh"] diff --git a/infratest/Cargo.toml b/infratest/Cargo.toml new file mode 100644 index 0000000..a219335 --- /dev/null +++ b/infratest/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "infratest" +version = "0.1.0" +edition = "2024" + +[dependencies] +bollard = "0.19" +tokio = { version = "1.0", features = ["full"] } +futures-util = "0.3" +anyhow = "1.0" +reqwest = { version = "0.12", features = ["stream", "rustls-tls"] } +tar = "0.4" +testcontainers-modules = { version = "0.13", features = ["mongo"] } +pretty_env_logger = "0.4" +testcontainers = { git = "https://github.com/mominul/testcontainers-rs.git", branch = "platform_config", features = ["http_wait"] } + +[patch.crates-io] +testcontainers = { git = "https://github.com/mominul/testcontainers-rs.git", branch = "platform_config" } diff --git a/images/server-ci.Dockerfile b/infratest/images/server-ci.Dockerfile similarity index 62% rename from images/server-ci.Dockerfile rename to infratest/images/server-ci.Dockerfile index dace67e..1d1d420 100644 --- a/images/server-ci.Dockerfile +++ b/infratest/images/server-ci.Dockerfile @@ -10,4 +10,6 @@ RUN cargo build EXPOSE 3000 -ENTRYPOINT ["scripts/run_server.sh"] +COPY --chmod=0755 run_server.sh /sbin/run_server + +ENTRYPOINT ["/sbin/run_server"] diff --git a/scripts/check_apt.sh b/infratest/scripts/check_apt.sh similarity index 100% rename from scripts/check_apt.sh rename to infratest/scripts/check_apt.sh diff --git a/scripts/check_apt_multiple.sh b/infratest/scripts/check_apt_multiple.sh similarity index 100% rename from scripts/check_apt_multiple.sh rename to infratest/scripts/check_apt_multiple.sh diff --git a/scripts/check_dnf.sh b/infratest/scripts/check_dnf.sh similarity index 100% rename from scripts/check_dnf.sh rename to infratest/scripts/check_dnf.sh diff --git a/scripts/check_dnf_multiple.sh b/infratest/scripts/check_dnf_multiple.sh similarity index 100% rename from scripts/check_dnf_multiple.sh rename to infratest/scripts/check_dnf_multiple.sh diff --git a/scripts/check_zypper_multiple.sh b/infratest/scripts/check_zypper_multiple.sh similarity index 100% rename from scripts/check_zypper_multiple.sh rename to infratest/scripts/check_zypper_multiple.sh diff --git a/infratest/scripts/run_server.sh b/infratest/scripts/run_server.sh new file mode 100755 index 0000000..bd634e2 --- /dev/null +++ b/infratest/scripts/run_server.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +./target/debug/packhub --generate-keys diff --git a/infratest/src/containers.rs b/infratest/src/containers.rs new file mode 100644 index 0000000..9d8fcb1 --- /dev/null +++ b/infratest/src/containers.rs @@ -0,0 +1,151 @@ +use std::{path::Path, pin::Pin, time::Duration}; + +use anyhow::Result; +use testcontainers_modules::{ + mongo::Mongo, + testcontainers::{ + ContainerAsync, GenericBuildableImage, GenericImage, ImageExt, + core::IntoContainerPort, + runners::{AsyncBuilder, AsyncRunner}, + }, +}; +use tokio::{ + io::{AsyncBufRead, AsyncReadExt}, + time::sleep, +}; + +use crate::distro; + +pub(crate) async fn streaming_print(mut stream: Pin>, prefix: &str) { + let mut buffer = [0; 1024]; + while let Ok(n) = stream.read(&mut buffer).await { + if n == 0 { + break; + } + print!("{prefix}: {}", String::from_utf8_lossy(&buffer[..n])); + } +} + +pub(crate) async fn setup_mongo_container() -> Result> { + let container = Mongo::default() + .with_network("host") + .with_mapped_port(27017, 27017.tcp()) + .with_env_var("MONGO_INITDB_ROOT_USERNAME", "root") + .with_env_var("MONGO_INITDB_ROOT_PASSWORD", "pass") + .start() + .await?; + + println!("MongoDB container started"); + + Ok(container) +} + +pub(crate) async fn server_health_check() { + let timeout_secs = 60; + let mut elapsed = 0; + + let client = reqwest::Client::new(); + + loop { + let resp = client.get("http://localhost:3000").send().await; + + match resp { + Ok(r) if r.status().is_success() => { + println!("Server is up!"); + break; + } + _ => { + if elapsed >= timeout_secs { + println!( + "Error: Server did not start within {} seconds.", + timeout_secs + ); + return; + } + println!("Waiting for server..."); + sleep(Duration::from_secs(5)).await; + elapsed += 5; + } + } + } +} + +pub(crate) async fn setup_server() -> Result> { + let container = GenericBuildableImage::new("packhub-test-server", "latest") + .with_dockerfile("images/server-ci.Dockerfile") + .with_file("./scripts/run_server.sh", "run_server.sh") + .with_file("../src", "src") + .with_file("../pages", "pages") + .with_file("../templates", "templates") + .with_file("../data/self-signed-certs", "data/self-signed-certs") + .with_file("../.env", ".env") + .with_file("../Cargo.toml", "Cargo.toml") + .build_image() + .await? + .with_network("host") + .with_mapped_port(3000, 3000.tcp()) + .start() + .await?; + + server_health_check().await; + + let stdout = container.stdout(true); + tokio::spawn(async move { + streaming_print(stdout, "server").await; + }); + + let stderr = container.stderr(true); + + tokio::spawn(async move { + streaming_print(stderr, "server").await; + }); + + Ok(container) +} + +pub(crate) async fn setup_distro(distro: distro::Distro) -> Result { + let mut container = GenericImage::new(&distro.image, &distro.tag) + .with_entrypoint(&format!("./{}", &distro.script)) + .with_platform("linux/amd64") + .with_copy_to( + &distro.script, + Path::new(&format!("scripts/{}", &distro.script)), + ) + .with_network("host"); + + if let Some(ref env) = distro.env { + for (key, value) in env { + container = container.with_env_var(key, value); + } + } + + let container = container.start().await?; + + let stdout = container.stdout(true); + + let name = format!("{}({}:{})", &distro.name, &distro.image, &distro.tag); + + let name2 = name.clone(); + + tokio::spawn(async move { + streaming_print(stdout, &name).await; + }); + + let stderr = container.stderr(true); + + tokio::spawn(async move { + streaming_print(stderr, &name2).await; + }); + + loop { + let Some(code) = container.exit_code().await? else { + continue; + }; + + if code == 0 { + return Ok(distro); + } else { + return Err(anyhow::Error::new(distro::DistroError { distro, code })); + } + } +} diff --git a/infratest/src/distro.rs b/infratest/src/distro.rs new file mode 100644 index 0000000..2b3d050 --- /dev/null +++ b/infratest/src/distro.rs @@ -0,0 +1,48 @@ +#[derive(Debug, Hash, PartialEq, Eq)] +pub(crate) struct Distro { + pub(crate) name: String, + pub(crate) image: String, + pub(crate) tag: String, + pub(crate) env: Option>, + pub(crate) script: String, +} + +#[derive(Debug)] +pub(crate) struct DistroError { + pub(crate) distro: Distro, + pub(crate) code: i64, +} + +impl std::fmt::Display for DistroError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Distribution Container {}({}:{}) exited with error code: {}", + self.distro.name, self.distro.image, self.distro.tag, self.code + ) + } +} + +impl std::error::Error for DistroError {} + +impl Distro { + pub(crate) fn new( + name: &str, + image: &str, + tag: &str, + script: &str, + env: Option<&[(&str, &str)]>, + ) -> Self { + Distro { + name: name.to_owned(), + image: image.to_owned(), + tag: tag.to_owned(), + env: env.map(|env| { + env.into_iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect::>() + }), + script: script.to_owned(), + } + } +} diff --git a/infratest/src/infra.rs b/infratest/src/infra.rs new file mode 100644 index 0000000..f63ca49 --- /dev/null +++ b/infratest/src/infra.rs @@ -0,0 +1,86 @@ +use anyhow::{Result, bail}; +use testcontainers::{ContainerAsync, GenericImage}; +use testcontainers_modules::mongo::Mongo; +use tokio::task::{JoinHandle, JoinSet}; + +use crate::{ + containers::{setup_distro, setup_mongo_container, setup_server}, + distro::{Distro, DistroError}, +}; + +pub struct InfraTest { + #[allow(dead_code)] + mongo: ContainerAsync, + #[allow(dead_code)] + server: ContainerAsync, + distro: JoinSet>, +} + +pub(crate) async fn flatten(handle: JoinHandle>) -> Result { + match handle.await { + Ok(Ok(result)) => Ok(result), + Ok(Err(err)) => Err(err), + Err(err) => bail!("handling failed: {err}"), + } +} + +impl InfraTest { + pub(crate) async fn setup_infra() -> Result { + let handle1 = tokio::spawn(setup_mongo_container()); + let handle2 = tokio::spawn(setup_server()); + + let (mongo, server) = match tokio::try_join!(flatten(handle1), flatten(handle2)) { + Ok((mongo, server)) => (mongo, server), + Err(err) => bail!("Failed to setup infrastructure: {err}"), + }; + + Ok(Self { + mongo, + server, + distro: JoinSet::new(), + }) + } + + pub(crate) fn add_distro(&mut self, distro: Distro) { + self.distro.spawn(async move { setup_distro(distro).await }); + } + + pub(crate) async fn run_distros(&mut self) -> Result<()> { + let mut success = Vec::new(); + let mut failed = Vec::new(); + while let Some(result) = self.distro.join_next().await { + match result { + Ok(Ok(distro)) => success.push(distro), + Ok(Err(err)) => { + match err.downcast::() { + Ok(err) => failed.push(err), + Err(_) => bail!("Unexpected error in running distro"), + }; + } + Err(err) => bail!("Distro task failed: {err}"), + }; + } + + println!(); + + for distro in success { + println!( + "Successfully ran distro {}({}:{})", + distro.name, distro.image, distro.tag + ); + } + + if !failed.is_empty() { + println!(); + for err in failed { + println!( + "Failed to run distro {}({}:{}) exit code: {}", + err.distro.name, err.distro.image, err.distro.tag, err.code + ); + } + bail!("Failed to run distros"); + } + + Ok(()) + } +} diff --git a/infratest/src/main.rs b/infratest/src/main.rs new file mode 100644 index 0000000..14e8bfb --- /dev/null +++ b/infratest/src/main.rs @@ -0,0 +1,72 @@ +use anyhow::Result; + +use crate::{distro::Distro, infra::InfraTest}; + +mod containers; +mod distro; +mod infra; + +#[tokio::main] +async fn main() -> Result<()> { + let mut infra = InfraTest::setup_infra().await?; + + infra.add_distro(Distro::new( + "single-pkg", + "ubuntu", + "24.04", + "check_apt.sh", + Some(&[("DIST", "ubuntu")]), + )); + + infra.add_distro(Distro::new( + "multi-pkg", + "ubuntu", + "24.04", + "check_apt_multiple.sh", + Some(&[("DIST", "ubuntu")]), + )); + + infra.add_distro(Distro::new( + "single-pkg", + "debian", + "12", + "check_apt.sh", + Some(&[("DIST", "debian")]), + )); + + infra.add_distro(Distro::new( + "multi-pkg", + "debian", + "12", + "check_apt_multiple.sh", + Some(&[("DIST", "debian")]), + )); + + infra.add_distro(Distro::new( + "single-pkg", + "fedora", + "42", + "check_dnf.sh", + None, + )); + + infra.add_distro(Distro::new( + "multi-pkg", + "fedora", + "42", + "check_dnf_multiple.sh", + None, + )); + + infra.add_distro(Distro::new( + "multi-pkg", + "opensuse/tumbleweed", + "latest", + "check_zypper_multiple.sh", + None, + )); + + infra.run_distros().await?; + + Ok(()) +} diff --git a/scripts/run_server.sh b/scripts/run_server.sh deleted file mode 100755 index cacc7c8..0000000 --- a/scripts/run_server.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -./target/debug/packhub --generate-keys \ No newline at end of file