Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 27 additions & 22 deletions .github/workflows/build-cicd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,33 +30,38 @@ jobs:
echo "TAG=${BRANCH_NAME}" >> $GITHUB_OUTPUT
fi

- name: Build and push Docker image
- name: Build Docker image (no push)
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
#platforms: linux/amd64,linux/arm64 # Add linux/arm64 for M3/ARM CPUs
push: true
tags: cbaugus/rust_loadtest:${{ steps.docker_meta.outputs.TAG }}
provenance: true
push: false

- name: Install Syft
run: |
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin

- name: Generate SBOM with Syft
run: |
syft --version
syft "cbaugus/rust_loadtest:${{ steps.docker_meta.outputs.TAG }}" -o cyclonedx-json > sbom.cyclonedx.json

- name: Upload SBOM artifact
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.cyclonedx.json

- name: Push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
tags: cbaugus/rust_loadtest:${{ steps.docker_meta.outputs.TAG }}
provenance: true
push: true

# #Deploy
# deploy:
# needs: build
# runs-on: ubuntu-latest
# steps:
# - name: Checkout code
# uses: actions/checkout@v2
#
# - name: Install nomad (cross-platform)
# uses: gacts/install-nomad@v1.2.0
#
#
# - name: Deploy Nomad Job
# env:
# NOMAD_ADDR: ${{ secrets.NOMAD_ADDR }}
# NOMAD_TOKEN: ${{ secrets.NOMAD_TOKEN }}
# run: |
# nomad job run -address=${NOMAD_ADDR} -token=${NOMAD_TOKEN} ./api.nomad.hcl
#
14 changes: 11 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,31 @@ RUN cargo install --path .

# --- Stage 2: Create the final, smaller runtime image ---
# Use a minimal base image for the final runtime
FROM debian:bullseye-slim
FROM ubuntu:latest
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libssl1.1 \
libssl3 \
ca-certificates \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

# Set the working directory
WORKDIR /usr/local/bin

# Add a non-root user and group
RUN groupadd -r appuser && useradd -r -g appuser appuser

# Copy the compiled binary from the builder stage
COPY --from=builder /usr/local/cargo/bin/rust_loadtest /usr/local/bin/rust_loadtest

# Set ownership of the binary to the non-root user
RUN chown appuser:appuser /usr/local/bin/rust_loadtest

# Expose the Prometheus metrics port
EXPOSE 9090

# Switch to non-root user
USER appuser

# Command to run the application when the container starts
CMD ["/usr/local/bin/rust_loadtest"]

22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,28 @@ docker run --rm \
cbaugus/rust-loadtester:latest
```

### Sending a JSON Payload (e.g., for Login Endpoints)

You can configure the tool to send a JSON body with each POST request (for example, to test login endpoints that expect a JSON payload). This is controlled by two environment variables:

* `SEND_JSON` (Optional, default: false): Set to `"true"` to enable sending a JSON payload in the body of each POST request.
* `JSON_PAYLOAD` (Required if `SEND_JSON=true`): The JSON string to send as the request body.

If `SEND_JSON` is not set or is not `"true"`, requests will be sent without a body.

**Example:**

```bash
docker run --rm \
-e TARGET_URL="https://your-service.com/login" \
-e SEND_JSON="true" \
-e JSON_PAYLOAD='{"username":"testuser","password":"testpass"}' \
-e NUM_CONCURRENT_TASKS="20" \
-e TEST_DURATION="10m" \
-e LOAD_MODEL_TYPE="Concurrent" \
cbaugus/rust-loadtester:latest
```

### Using mTLS (Mutual TLS)

To enable mTLS, you need to provide both a client certificate and a client private key. The private key **must be in PKCS#8 format**.
Expand Down
28 changes: 24 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let url = env::var("TARGET_URL")
.expect("TARGET_URL environment variable must be set");

// --- NEW: Optionally send JSON payload ---
let send_json = env::var("SEND_JSON").unwrap_or_else(|_| "false".to_string()).to_lowercase() == "true";
let json_payload = if send_json {
Some(env::var("JSON_PAYLOAD")
.expect("JSON_PAYLOAD environment variable must be set when SEND_JSON=true"))
} else {
None
};

let num_concurrent_tasks_str = env::var("NUM_CONCURRENT_TASKS")
.unwrap_or_else(|_| "10".to_string());
let num_concurrent_tasks: usize = num_concurrent_tasks_str.parse()
Expand Down Expand Up @@ -529,8 +538,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let url_clone = url.to_string();
let overall_test_duration_clone = overall_test_duration.clone();
let start_time_clone = start_time.clone();
let load_model_clone = load_model.clone(); // Clone load model for each task
let num_concurrent_tasks_clone = num_concurrent_tasks.clone(); // Clone for use in worker task
let load_model_clone = load_model.clone();
let num_concurrent_tasks_clone = num_concurrent_tasks.clone();
let send_json_clone = send_json;
let json_payload_clone = json_payload.clone();

let handle = tokio::spawn(async move {
loop {
Expand Down Expand Up @@ -559,13 +570,22 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {

let request_start_time = time::Instant::now(); // Start timer

match client_clone.get(&url_clone).send().await {
// --- CHANGED: Conditionally send POST with or without JSON ---
let req = client_clone.post(&url_clone);
let req = if send_json_clone {
req.header("Content-Type", "application/json")
.body(json_payload_clone.clone().unwrap())
} else {
req
};

match req.send().await {
Ok(response) => {
let status = response.status().as_u16().to_string();
REQUEST_STATUS_CODES.with_label_values(&[&status]).inc();
// Do not save the JWT token, just drop the response
},
Err(e) => {
// For network errors, we might want a specific label
REQUEST_STATUS_CODES.with_label_values(&["error"]).inc();
eprintln!("Task {}: Request to {} failed: {}", i, url_clone, e);
}
Expand Down