Merge pull request 'Add logging of JSON requests, CI and Dockerization' (#2) from axum-migration into main
continuous-integration/drone/push Build is passing Details

Reviewed-on: #2
This commit is contained in:
Dorian 2024-02-23 14:33:22 -05:00
commit 6775667f80
7 changed files with 282 additions and 40 deletions

4
.dockerignore Normal file
View File

@ -0,0 +1,4 @@
target/
Cargo.lock
**/*.rs.bk
.idea

59
.drone.yml Normal file
View File

@ -0,0 +1,59 @@
---
kind: pipeline
type: docker
name: mirror-server
steps:
- name: create-build-image
image: plugins/docker
settings:
username:
from_secret: docker-username
password:
from_secret: docker-password
registry: code.birch-tree.net
repo: code.birch-tree.net/dorian/mirror-server
target: BUILD
tags:
- build
cache_from:
- code.birch-tree.net/dorian/mirror-server:build
- name: test
image: code.birch-tree.net/dorian/mirror-server:build
commands:
- cargo test
depends_on:
- create-build-image
- name: create-release-image
image: plugins/docker
settings:
username:
from_secret: docker-username
password:
from_secret: docker-password
registry: code.birch-tree.net
repo: code.birch-tree.net/dorian/mirror-server
tags:
- 0.3.0
- latest
cache_from:
- code.birch-tree.net/dorian/mirror-server:latest
- code.birch-tree.net/dorian/mirror-server:build
depends_on:
- test
- name: create-debian-package
image: code.birch-tree.net/dorian/mirror-server:build
commands:
- ./publish-deb.sh
environment:
USERNAME: dorian
PASSWORD:
from_secret: gitea-release-password
depends_on:
- test
image_pull_secrets:
- docker-config

View File

@ -25,3 +25,7 @@ tower = "0.4"
tower-http = { version = "0.5", features = ["trace"] }
tracing = "0.1"
tracing-subscriber = "0.3"
[dev-dependencies]
axum-test = "14.0"
rstest = "0.18"

38
Dockerfile Normal file
View File

@ -0,0 +1,38 @@
FROM rust:1.76 AS BUILD
ENV APP_NAME=mirror-server
ENV APP_HOME=/srv/${APP_NAME}
RUN apt update \
&& apt install -y curl \
&& cargo install cargo-deb
# Setup working env.
RUN mkdir -p ${APP_HOME}
WORKDIR ${APP_HOME}
# Create build target cache.
RUN USER=root cargo init --bin .
COPY ["Cargo.toml", "./"]
RUN cargo build \
&& cargo build --tests \
&& cargo build --release \
&& rm src/*.rs
# Bring in the source
COPY ["src", "./src/"]
# Build the example API.
RUN cargo build --release
# Create the minimal server image.
FROM debian:buster-slim AS SERVER
ENV APP_NAME=mirror-server
ENV APP_HOME=/srv/${APP_NAME}
RUN apt update
RUN mkdir -p ${APP_HOME}
WORKDIR ${APP_HOME}
COPY --from=BUILD ${APP_HOME}/target/release/${APP_NAME} /usr/local/bin/
CMD ${APP_NAME}

View File

@ -2,6 +2,8 @@
A simple server for mirroring HTTP requests for testing.
[![Build Status](https://ci.birch-tree.net/api/badges/dorian/mirror-server/status.svg)](https://ci.birch-tree.net/dorian/mirror-server)
## Getting Started
* Use the latest stable version of Rust using rustup.
@ -29,5 +31,7 @@ sudo dpkg -i "mirror-server-${VERSION}_amd64.deb"
* [x] Migrate actix to axum for easier maintainability.
* [x] Add mirroring of JSON request.
* [x] Add logging to server.
* [ ] Add convenience path to access logging for a certain date / records.
* [ ] Dockerize mirror-server for easier distribution.
* [x] Create Docker image for mirror-server for easier distribution.
* [x] Add publishing of DEB and Docker image to DroneCI.
* [ ] Add documentation to the API.
* [ ] Publish crate on <https://crates.io/>.

View File

@ -1,22 +1,28 @@
#!/usr/bin/env bash
#!/usr/bin/bash
read -r -p "Username: " USERNAME
read -r -s -p "Password: " PASSWORD
_pkg_name=mirror-server
_pkg_version=0.2.0
_deb_file="${_pkg_name}_${_pkg_version}_amd64.deb"
_deb_path="target/debian"
_gitea_server="code.birch-tree.net"
if [[ ! -f "${_deb_path}/${_deb_file}" ]];
then
echo "Run cargo deb first!"
if [[ -z "${USERNAME}" ]]; then
echo "Set the USERNAME for Gitea"
exit 1
fi
curl --user "${USERNAME}:${PASSWORD}" \
--upload-file "${_deb_path}/${_deb_file}" \
-X PUT \
"https://${_gitea_server}/api/packages/${USERNAME}/generic/${_pkg_name}/${_pkg_version}/${_deb_file}"
if [[ -z "${PASSWORD}" ]]; then
echo "Set the PASSWORD for Gitea"
exit 1
fi
echo 'Create the Debian package...'
pkg_full_path=$(cargo deb)
pkg_filename=$(basename "${pkg_full_path}")
pkg_name=$(echo "${pkg_filename}" | awk -F _ '{print $1}')
pkg_version=$(echo "${pkg_filename}" | awk -F _ '{print $2}')
gitea_server="code.birch-tree.net"
curl --user "${USERNAME}:${PASSWORD}" \
--upload-file "${pkg_full_path}" \
--silent \
-X PUT \
"https://${gitea_server}/api/packages/${USERNAME}/generic/${pkg_name}/${pkg_version}/${pkg_filename}"
echo "Published ${pkg_name} v${pkg_version} ===> ${pkg_filename}"

View File

@ -1,14 +1,14 @@
use axum::{
extract::{Host, OriginalUri, Json},
extract::{Host, Json, OriginalUri},
http::{header::HeaderMap, Method},
Router,
};
use clap::Parser;
use serde::Serialize;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
use tower_http::trace;
use tracing::{Level, info, warn};
use tracing::{info, warn, Level};
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
@ -22,7 +22,7 @@ struct CliArgs {
ips: String,
}
#[derive(Serialize)]
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct EchoResponse {
method: String,
path: String,
@ -35,35 +35,46 @@ struct EchoResponse {
async fn echo_request(
method: Method,
original_uri: OriginalUri,
host: Host,
Host(host): Host,
header_map: HeaderMap,
body: Option<Json<Value>>
body: Option<Json<Value>>,
) -> Json<EchoResponse> {
let method = method.to_string();
let host = host.0;
let host = host;
let path = original_uri.path().to_string();
let headers = header_map
.iter()
.map(|(name, value)| (name.to_string(), value.to_str().unwrap_or("").to_string()))
.collect();
let json_body = match body {
let body = match body {
None => {
warn!("Received a non-JSON body.");
None
},
Some(Json(value)) => Some(value),
}
Some(Json(value)) => {
info!("JSON request: {}", value.to_string());
Some(value)
}
};
let response = EchoResponse {
method,
host,
path,
headers,
body: json_body,
body,
};
Json(response)
}
fn app() -> Router {
Router::new().fallback(echo_request).layer(
trace::TraceLayer::new_for_http()
.make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
.on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
)
}
#[tokio::main]
async fn main() {
let cli_args = CliArgs::parse();
@ -71,20 +82,136 @@ async fn main() {
let listen_on = format!("{ip}:{port}", ip = listen_on.0, port = listen_on.1);
// From https://stackoverflow.com/questions/75009289/how-to-enable-logging-tracing-with-axum
tracing_subscriber::fmt()
.with_max_level(Level::INFO)
.init();
let app = Router::new().fallback(echo_request).layer(
trace::TraceLayer::new_for_http()
.make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
.on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
);
tracing_subscriber::fmt().with_max_level(Level::INFO).init();
info!("Starting the mirror-server to listen to {}", listen_on);
let listener = tokio::net::TcpListener::bind(&listen_on)
.await
.unwrap_or_else(|_| panic!("Failed to binding to {}", listen_on));
axum::serve(listener, app)
axum::serve(listener, app())
.await
.expect("Server should start");
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::{HeaderName, HeaderValue, Method, StatusCode};
use axum_test::TestServer;
use rstest::rstest;
#[tokio::test]
async fn handles_simple_get_request() {
let expected_headers: BTreeMap<String, String> = BTreeMap::new();
let expected_response = EchoResponse {
method: "GET".to_string(),
path: "/".to_string(),
host: "localhost".to_string(),
body: None,
headers: expected_headers,
};
let server = TestServer::new(app()).unwrap();
let response = server.get("/").await;
response.assert_status(StatusCode::OK);
response.assert_json::<EchoResponse>(&expected_response);
}
#[rstest]
#[case::single_level("/test")]
#[case::multiple_level("/test/multiple/levels/")]
#[case::slash_ending("/test/")]
#[tokio::test]
async fn handles_different_urls(#[case] url: String) {
let expected_headers: BTreeMap<String, String> = BTreeMap::new();
let expected_response = EchoResponse {
method: "GET".to_string(),
path: url.clone(),
host: "localhost".to_string(),
body: None,
headers: expected_headers,
};
let server = TestServer::new(app()).unwrap();
let response = server.get(&url).await;
response.assert_status(StatusCode::OK);
response.assert_json::<EchoResponse>(&expected_response);
}
#[rstest]
#[case::post(Method::POST)]
#[case::put(Method::PUT)]
#[case::patch(Method::PATCH)]
#[case::delete(Method::DELETE)]
#[tokio::test]
async fn handles_different_http_methods(#[case] http_method: Method) {
let expected_headers: BTreeMap<String, String> = BTreeMap::new();
let expected_response = EchoResponse {
method: http_method.to_string(),
path: "/testing".to_string(),
host: "localhost".to_string(),
body: None,
headers: expected_headers,
};
let server = TestServer::new(app()).unwrap();
let response = server.method(http_method, "/testing").await;
response.assert_status(StatusCode::OK);
response.assert_json::<EchoResponse>(&expected_response);
}
#[tokio::test]
async fn handle_non_json_request() {
let mut expected_headers: BTreeMap<String, String> = BTreeMap::new();
expected_headers.insert("content-type".to_string(), "text/plain".to_string());
let expected_response = EchoResponse {
method: "POST".to_string(),
path: "/".to_string(),
host: "localhost".to_string(),
body: None,
headers: expected_headers,
};
let server = TestServer::new(app()).unwrap();
let response = server.post("/").text("hello world").await;
response.assert_status(StatusCode::OK);
response.assert_json::<EchoResponse>(&expected_response);
}
#[tokio::test]
async fn handle_json_request() {
let test_json = serde_json::json!({"hello": "world"});
let mut expected_headers: BTreeMap<String, String> = BTreeMap::new();
expected_headers.insert("content-type".to_string(), "application/json".to_string());
let expected_response = EchoResponse {
method: "POST".to_string(),
path: "/".to_string(),
host: "localhost".to_string(),
body: Some(test_json.clone()),
headers: expected_headers,
};
let server = TestServer::new(app()).unwrap();
let response = server.post("/").json(&test_json).await;
response.assert_status(StatusCode::OK);
response.assert_json::<EchoResponse>(&expected_response);
}
#[tokio::test]
async fn handle_extra_headers() {
let mut expected_headers: BTreeMap<String, String> = BTreeMap::new();
expected_headers.insert("x-test-message".to_string(), "Howdy!".to_string());
let expected_response = EchoResponse {
method: "GET".to_string(),
path: "/".to_string(),
host: "localhost".to_string(),
body: None,
headers: expected_headers,
};
let server = TestServer::new(app()).unwrap();
let response = server
.get("/")
.add_header(
HeaderName::from_static("x-test-message"),
HeaderValue::from_static("Howdy!"),
)
.await;
response.assert_status(StatusCode::OK);
response.assert_json::<EchoResponse>(&expected_response);
}
}