diff --git a/.github/workflows/build-cicd.yaml b/.github/workflows/build-cicd.yaml index 2041fc9..da0fb4f 100644 --- a/.github/workflows/build-cicd.yaml +++ b/.github/workflows/build-cicd.yaml @@ -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 -# diff --git a/Dockerfile b/Dockerfile index c5a9c8a..b1bd591 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,10 +5,10 @@ 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/* @@ -16,12 +16,20 @@ RUN apt-get update \ # 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"] - diff --git a/README.md b/README.md index 2b1320f..f099b3c 100644 --- a/README.md +++ b/README.md @@ -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**. diff --git a/src/main.rs b/src/main.rs index a8b0c50..f78aaf7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -394,6 +394,15 @@ async fn main() -> Result<(), Box> { 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() @@ -529,8 +538,10 @@ async fn main() -> Result<(), Box> { 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 { @@ -559,13 +570,22 @@ async fn main() -> Result<(), Box> { 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); }