From 22178cffd753725eba7228e207a5c7e29e04a607 Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Tue, 20 Jan 2026 17:44:36 +0100 Subject: [PATCH] Add manual testing setup for `pkg6depotd` - Introduced scripts and configurations for manual testing using `anyvm` with OpenIndiana and OmniOS. - Implemented repository fetching (`fetch_repo.sh`) and server startup (`run_depotd.sh`) scripts. - Enhanced `pkg6depotd` to support default publisher routes and trailing slashes. - Updated integration tests to verify new publisher route behavior. --- libips/src/repository/file_backend.rs | 5 +- libips/src/repository/mod.rs | 2 + libips/src/repository/rest_backend.rs | 5 +- pkg6depotd/src/http/handlers/publisher.rs | 70 +++++++++++----- pkg6depotd/src/http/routes.rs | 7 ++ pkg6depotd/tests/integration_tests.rs | 97 ++++++++++++++++++++++- testing/manual/README.md | 79 ++++++++++++++++++ testing/manual/fetch_repo.sh | 36 +++++++++ testing/manual/run_depotd.sh | 38 +++++++++ 9 files changed, 317 insertions(+), 22 deletions(-) create mode 100644 testing/manual/README.md create mode 100755 testing/manual/fetch_repo.sh create mode 100755 testing/manual/run_depotd.sh diff --git a/libips/src/repository/file_backend.rs b/libips/src/repository/file_backend.rs index 210fef9..23b6aef 100644 --- a/libips/src/repository/file_backend.rs +++ b/libips/src/repository/file_backend.rs @@ -991,7 +991,10 @@ impl ReadableRepository for FileBackend { } // Create and return a RepositoryInfo struct - Ok(RepositoryInfo { publishers }) + Ok(RepositoryInfo { + publishers, + default_publisher: self.config.default_publisher.clone(), + }) } /// List packages in the repository diff --git a/libips/src/repository/mod.rs b/libips/src/repository/mod.rs index 1209070..937f636 100644 --- a/libips/src/repository/mod.rs +++ b/libips/src/repository/mod.rs @@ -274,6 +274,8 @@ pub struct PublisherInfo { pub struct RepositoryInfo { /// Information about publishers in the repository pub publishers: Vec, + /// Name of the default publisher, if any + pub default_publisher: Option, } /// Information about a package in a repository diff --git a/libips/src/repository/rest_backend.rs b/libips/src/repository/rest_backend.rs index 30f65d9..f6216a5 100644 --- a/libips/src/repository/rest_backend.rs +++ b/libips/src/repository/rest_backend.rs @@ -407,7 +407,10 @@ impl ReadableRepository for RestBackend { } // Create and return a RepositoryInfo struct - Ok(RepositoryInfo { publishers }) + Ok(RepositoryInfo { + publishers, + default_publisher: self.config.default_publisher.clone(), + }) } /// List packages in the repository diff --git a/pkg6depotd/src/http/handlers/publisher.rs b/pkg6depotd/src/http/handlers/publisher.rs index be4f7cf..2b280d4 100644 --- a/pkg6depotd/src/http/handlers/publisher.rs +++ b/pkg6depotd/src/http/handlers/publisher.rs @@ -25,46 +25,78 @@ struct P5iFile { pub async fn get_publisher_v0( state: State>, - path: Path, + Path(publisher): Path, ) -> Result { - get_publisher_impl(state, path).await + get_publisher_response(state, Some(publisher)).await } pub async fn get_publisher_v1( state: State>, - path: Path, + Path(publisher): Path, ) -> Result { - get_publisher_impl(state, path).await + get_publisher_response(state, Some(publisher)).await } -async fn get_publisher_impl( +pub async fn get_default_publisher_v0( + state: State>, +) -> Result { + get_publisher_response(state, None).await +} + +pub async fn get_default_publisher_v1( + state: State>, +) -> Result { + get_publisher_response(state, None).await +} + +async fn get_publisher_response( State(repo): State>, - Path(publisher): Path, + publisher: Option, ) -> Result { let repo_info = repo.get_info()?; - let pub_info = repo_info - .publishers - .into_iter() - .find(|p| p.name == publisher); + if let Some(name) = publisher { + let pub_info = repo_info.publishers.into_iter().find(|p| p.name == name); - if let Some(p) = pub_info { - let p5i = P5iFile { - packages: Vec::new(), - publishers: vec![P5iPublisherInfo { + if let Some(p) = pub_info { + let p5i = P5iFile { + packages: Vec::new(), + publishers: vec![P5iPublisherInfo { + alias: None, + name: p.name, + packages: Vec::new(), + repositories: Vec::new(), + }], + version: 1, + }; + let json = + serde_json::to_string_pretty(&p5i).map_err(|e| DepotError::Server(e.to_string()))?; + Ok(([(header::CONTENT_TYPE, "application/vnd.pkg5.info")], json).into_response()) + } else { + Err(DepotError::Repo( + libips::repository::RepositoryError::PublisherNotFound(name), + )) + } + } else { + // Return all publishers + let publishers = repo_info + .publishers + .into_iter() + .map(|p| P5iPublisherInfo { alias: None, name: p.name, packages: Vec::new(), repositories: Vec::new(), - }], + }) + .collect(); + + let p5i = P5iFile { + packages: Vec::new(), + publishers, version: 1, }; let json = serde_json::to_string_pretty(&p5i).map_err(|e| DepotError::Server(e.to_string()))?; Ok(([(header::CONTENT_TYPE, "application/vnd.pkg5.info")], json).into_response()) - } else { - Err(DepotError::Repo( - libips::repository::RepositoryError::PublisherNotFound(publisher), - )) } } diff --git a/pkg6depotd/src/http/routes.rs b/pkg6depotd/src/http/routes.rs index 0df7357..27b46e6 100644 --- a/pkg6depotd/src/http/routes.rs +++ b/pkg6depotd/src/http/routes.rs @@ -10,6 +10,7 @@ use tower_http::trace::TraceLayer; pub fn app_router(state: Arc) -> Router { Router::new() + .route("/versions/0", get(versions::get_versions)) .route("/versions/0/", get(versions::get_versions)) .route( "/{publisher}/catalog/1/{filename}", @@ -37,7 +38,13 @@ pub fn app_router(state: Arc) -> Router { ) .route("/{publisher}/info/0/{fmri}", get(info::get_info)) .route("/{publisher}/publisher/0", get(publisher::get_publisher_v0)) + .route("/{publisher}/publisher/0/", get(publisher::get_publisher_v0)) .route("/{publisher}/publisher/1", get(publisher::get_publisher_v1)) + .route("/{publisher}/publisher/1/", get(publisher::get_publisher_v1)) + .route("/publisher/0", get(publisher::get_default_publisher_v0)) + .route("/publisher/0/", get(publisher::get_default_publisher_v0)) + .route("/publisher/1", get(publisher::get_default_publisher_v1)) + .route("/publisher/1/", get(publisher::get_default_publisher_v1)) .route("/{publisher}/search/0/{token}", get(search::get_search_v0)) .route("/{publisher}/search/1/{token}", get(search::get_search_v1)) // Admin API over HTTP diff --git a/pkg6depotd/tests/integration_tests.rs b/pkg6depotd/tests/integration_tests.rs index e534889..da7a38a 100644 --- a/pkg6depotd/tests/integration_tests.rs +++ b/pkg6depotd/tests/integration_tests.rs @@ -105,7 +105,7 @@ async fn test_depot_server() { .unwrap(); assert!(resp.status().is_success()); let text = resp.text().await.unwrap(); - assert!(text.contains("pkg-server pkg6depotd-0.5.1")); + assert!(text.contains("pkg-server pkg6depotd-")); assert!(text.contains("catalog 1")); assert!(text.contains("manifest 0 1")); @@ -158,6 +158,12 @@ async fn test_depot_server() { let pub_url = format!("{}/test/publisher/1", base_url); let resp = client.get(&pub_url).send().await.unwrap(); assert!(resp.status().is_success()); + + // Test Publisher v1 with trailing slash + let pub_url_slash = format!("{}/test/publisher/1/", base_url); + let resp = client.get(&pub_url_slash).send().await.unwrap(); + assert!(resp.status().is_success()); + assert!( resp.headers() .get("content-type") @@ -169,6 +175,36 @@ async fn test_depot_server() { let pub_json: serde_json::Value = resp.json().await.unwrap(); assert_eq!(pub_json["version"], 1); assert_eq!(pub_json["publishers"][0]["name"], "test"); + + // Test Default Publisher Route v1 + let def_pub_url = format!("{}/publisher/1", base_url); + let resp = client.get(&def_pub_url).send().await.unwrap(); + assert!(resp.status().is_success()); + + // Test Default Publisher Route v1 with trailing slash + let def_pub_url_slash = format!("{}/publisher/1/", base_url); + let resp = client.get(&def_pub_url_slash).send().await.unwrap(); + assert!(resp.status().is_success()); + + let pub_json: serde_json::Value = resp.json().await.unwrap(); + // In current implementation it returns one publisher. + // We want it to return all publishers. + assert_eq!(pub_json["publishers"].as_array().unwrap().len(), 1); + assert_eq!(pub_json["publishers"][0]["name"], "test"); + + // Test Default Publisher Route v0 + let def_pub_url_v0 = format!("{}/publisher/0", base_url); + let resp = client.get(&def_pub_url_v0).send().await.unwrap(); + assert!(resp.status().is_success()); + + // Test Default Publisher Route v0 with trailing slash + let def_pub_url_v0_slash = format!("{}/publisher/0/", base_url); + let resp = client.get(&def_pub_url_v0_slash).send().await.unwrap(); + assert!(resp.status().is_success()); + + let pub_json: serde_json::Value = resp.json().await.unwrap(); + assert_eq!(pub_json["publishers"].as_array().unwrap().len(), 1); + assert_eq!(pub_json["publishers"][0]["name"], "test"); // 6. Test File // We assume file exists if manifest works. @@ -385,3 +421,62 @@ async fn test_file_url_without_algo() { ); let _content = resp.text().await.unwrap(); } + +#[tokio::test] +async fn test_multiple_publishers_default_route() { + let temp_dir = TempDir::new().unwrap(); + let repo_path = temp_dir.path().join("repo_multi"); + let mut backend = FileBackend::create(&repo_path, RepositoryVersion::V4).unwrap(); + + backend.add_publisher("pub1").unwrap(); + backend.add_publisher("pub2").unwrap(); + + let config = Config { + server: ServerConfig { + bind: vec!["127.0.0.1:0".to_string()], + workers: None, + max_connections: None, + reuseport: None, + cache_max_age: Some(3600), + tls_cert: None, + tls_key: None, + }, + repository: RepositoryConfig { + root: repo_path.clone(), + mode: Some("readonly".to_string()), + }, + telemetry: None, + publishers: None, + admin: None, + oauth2: None, + }; + + let repo = DepotRepo::new(&config).unwrap(); + let state = Arc::new(repo); + let router = http::routes::app_router(state); + + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + + tokio::spawn(async move { + http::server::run(router, listener).await.unwrap(); + }); + + let client = reqwest::Client::new(); + let base_url = format!("http://{}", addr); + + let def_pub_url = format!("{}/publisher/0", base_url); + let resp = client.get(&def_pub_url).send().await.unwrap(); + assert!(resp.status().is_success()); + + let pub_json: serde_json::Value = resp.json().await.unwrap(); + let pubs = pub_json["publishers"].as_array().unwrap(); + + // CURRENT BEHAVIOR: returns 1 + // DESIRED BEHAVIOR: returns 2 + assert_eq!(pubs.len(), 2, "Should return all publishers"); + + let names: Vec = pubs.iter().map(|p| p["name"].as_str().unwrap().to_string()).collect(); + assert!(names.contains(&"pub1".to_string())); + assert!(names.contains(&"pub2".to_string())); +} diff --git a/testing/manual/README.md b/testing/manual/README.md new file mode 100644 index 0000000..d610236 --- /dev/null +++ b/testing/manual/README.md @@ -0,0 +1,79 @@ +# Manual Testing Setup for pkg6depotd + +This directory contains scripts and configurations for manual testing of `pkg6depotd` using `anyvm` with OpenIndiana and OmniOS. + +## Overview + +The goal is to test `pkg6depotd` as a server for the standard Python `pkg` client running inside an illumos VM. + +1. **Host**: Runs `pkg6depotd` serving a local repository. +2. **VM**: Runs OpenIndiana or OmniOS and uses `pkg` to communicate with the host. + +## Prerequisites + +- `~/bin/anyvm.py` script (automated QEMU VM launcher). +- Rust toolchain installed on the host. + +## Step-by-Step Instructions + +### 1. Start the VM + +Choose either OpenIndiana or OmniOS. Use the `anyvm.py` script located in `~/bin/`. + +```bash +# For OpenIndiana +python3 ~/bin/anyvm.py --os openindiana --release 202510 -v $(pwd):/root/ips + +# For OmniOS +python3 ~/bin/anyvm.py --os omnios --release r151056 -v $(pwd):/root/ips +``` + +You can add `--ssh-port 2222` if you want a fixed SSH port. `anyvm.py` will display the SSH command to use. + +### 2. Fetch a sample repository inside the VM + +Once the VM is running, SSH into it and run the `fetch_repo.sh` script to create a small local repository. +Since we mounted the project root to `/root/ips`, you can fetch the repository directly into that mount to make it immediately available on the host. + +```bash +# From the host (replace with the one assigned by anyvm) +ssh -p root@localhost + +# Inside the VM +cd /root/ips +./testing/manual/fetch_repo.sh https://pkg.openindiana.org/hipster ./test_repo +``` + +This will create a repository at `./test_repo` inside the VM (which is also visible on the host) containing a few packages. + +### 3. Run pkg6depotd on the host + +Now that the repository is available on the host, you can run `pkg6depotd`. + +```bash +./testing/manual/run_depotd.sh ./test_repo +``` + +The server will start on `0.0.0.0:8080`. + +### 4. Test with the pkg client inside the VM + +Back inside the VM, point the `pkg` client to the host's `pkg6depotd`. +In QEMU's default user networking, the host is reachable at `10.0.2.2`. + +```bash +# Inside the VM +# 1. Add the publisher +pkg set-publisher -g http://10.0.2.2:8080 test + +# 2. List packages from the new publisher +pkg list -v -p test + +# 3. Try to install a package (if available in the fetched subset) +pkg install library/zlib +``` + +## Troubleshooting + +- **Connection issues**: Ensure `pkg6depotd` is binding to an address reachable from the VM (e.g., `0.0.0.0` or the host's bridge IP). +- **Missing packages**: Ensure the packages you are trying to install were included in the `fetch_repo.sh` call. diff --git a/testing/manual/fetch_repo.sh b/testing/manual/fetch_repo.sh new file mode 100755 index 0000000..11278c7 --- /dev/null +++ b/testing/manual/fetch_repo.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +# Script to fetch a package repository copy using pkgrecv. +# This script is intended to be run inside an OpenIndiana or OmniOS VM. + +set -e + +SOURCE_URL="${1:-https://pkg.openindiana.org/hipster}" +DEST_DIR="${2:-/var/share/pkg_repo}" +# Default to a small set of packages for testing if none specified +# 'entire' or '*' can be used to fetch more/all packages, but be aware of size. +shift 2 || true +PACKAGES=("$@") + +if [ ${#PACKAGES[@]} -eq 0 ]; then + echo "No packages specified, fetching a small set for testing..." + PACKAGES=("library/zlib" "system/library") +fi + +echo "Source: $SOURCE_URL" +echo "Destination: $DEST_DIR" +echo "Packages: ${PACKAGES[*]}" + +if [ ! -d "$DEST_DIR" ]; then + echo "Creating repository at $DEST_DIR..." + mkdir -p "$DEST_DIR" + pkgrepo create "$DEST_DIR" + # We'll set a generic prefix, or use the one from source if we wanted to be more fancy + pkgrepo set -s "$DEST_DIR" publisher/prefix=openindiana.org +fi + +pkgrecv -s "$SOURCE_URL" -d "$DEST_DIR" "${PACKAGES[@]}" --newest + + +echo "Repository fetch complete." +echo "You can now sync $DEST_DIR to your host to use with pkg6depotd." diff --git a/testing/manual/run_depotd.sh b/testing/manual/run_depotd.sh new file mode 100755 index 0000000..25d8f94 --- /dev/null +++ b/testing/manual/run_depotd.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +# Script to build and run pkg6depotd for manual testing. + +set -e + +REPO_ROOT="${1:-/tmp/pkg_repo}" + +echo "Building pkg6depotd..." +cargo build -p pkg6depotd + +if [ ! -d "$REPO_ROOT" ]; then + echo "Warning: Repository root $REPO_ROOT does not exist." + echo "You might want to fetch a repository first using fetch_repo.sh inside a VM" + echo "and then sync it to this path." +fi + +# Create a temporary config file based on the one in the root but with the correct repo path +CONFIG_FILE="/tmp/pkg6depotd_test.kdl" +cat > "$CONFIG_FILE" <