diff --git a/ docker-compose.production.yml b/ docker-compose.production.yml deleted file mode 100644 index 6e0da79..0000000 --- a/ docker-compose.production.yml +++ /dev/null @@ -1,6 +0,0 @@ -version: "3.9" -services: - well-known: - build: - context: docker/vault - image: git.nixc.us/colin/vault:production \ No newline at end of file diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..fe01e84 --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,143 @@ +labels: + hostname: "macmini7" +clone: + git: + image: woodpeckerci/plugin-git + settings: + partial: false + depth: 1 +steps: + # Build Step for staging Branch + build-staging: + name: build-staging + image: woodpeckerci/plugin-docker-buildx + secrets: [REGISTRY_USER, REGISTRY_PASSWORD] + volumes: + - /var/run/docker.sock:/var/run/docker.sock + commands: + - echo "Building application for staging branch" + - echo "$${REGISTRY_PASSWORD}" | docker login -u "$${REGISTRY_USER}" --password-stdin git.nixc.us + - echo compose build + - docker compose -f docker-compose.staging.yml build + when: + - branch: main + - event: push + - path: + include: [ 'stack.production.yml', 'stack.staging.yml', 'docker-compose.staging.yml', 'docker-compose.production.yml', 'Dockerfile', '*.tests.ts' ] + + deploy-new: + name: deploy-new + when: + - branch: main + - path: + include: [ 'stack.production.yml', 'stack.staging.yml', 'docker-compose.staging.yml', 'docker-compose.production.yml', 'Dockerfile', '*.tests.ts' ] + image: woodpeckerci/plugin-docker-buildx + secrets: [REGISTRY_USER, REGISTRY_PASSWORD] + volumes: + - /var/run/docker.sock:/var/run/docker.sock + commands: + - echo "$${REGISTRY_PASSWORD}" | docker login -u "$${REGISTRY_USER}" --password-stdin git.nixc.us + - echo compose push + - docker compose -f docker-compose.staging.yml push + # - docker stack deploy --with-registry-auth -c ./stack.staging.yml $${CI_REPO_NAME}-staging + + # # Wait for Deploy to Complete + # wait-for-deploy-staging: + # name: wait-for-deploy-staging + # image: woodpeckerci/plugin-git + # commands: + # - echo "Waiting for staging deploy step to complete rollout." + # - sleep 60 + # when: + # - branch: main + # - event: push + + # # Run Automated Tests on staging Branch + # test-staging: + # name: run-tests-staging + # image: git.nixc.us/colin/playwright:latest + # secrets: [ base_url ] + # when: + # - branch: main + # - event: push + # - path: + # include: [ 'tests/', 'src/','docker-compose.staging.yml', 'docker-compose.production.yml', '*.tests.ts' ] # Specify paths relevant to tests + # volumes: + # - /var/run/docker.sock:/var/run/docker.sock:ro + + cleanup-staging: + name: cleanup-staging + when: + - branch: main + - path: + include: [ 'stack.production.yml', 'stack.staging.yml', 'docker-compose.staging.yml', 'docker-compose.production.yml', 'Dockerfile', '*.tests.ts' ] + image: woodpeckerci/plugin-docker-buildx + secrets: [REGISTRY_USER, REGISTRY_PASSWORD] + volumes: + - /var/run/docker.sock:/var/run/docker.sock + commands: + # - docker stack rm $${CI_REPO_NAME}-staging + ## added fault tolerance for docker stack rm + # - for i in {1..5}; do docker stack rm ${CI_REPO_NAME}-staging && break || sleep 10; done + - docker compose -f docker-compose.staging.yml down + - docker compose -f docker-compose.staging.yml rm -f + + # Build Step for staging Branch + build-build-push-production: + name: build-build-push-production + image: woodpeckerci/plugin-docker-buildx + secrets: [REGISTRY_USER, REGISTRY_PASSWORD] + volumes: + - /var/run/docker.sock:/var/run/docker.sock + commands: + - echo "Building application for staging branch" + - echo "$${REGISTRY_PASSWORD}" | docker login -u "$${REGISTRY_USER}" --password-stdin git.nixc.us + - echo compose build + - docker compose -f docker-compose.production.yml build + - docker compose -f docker-compose.production.yml push + when: + - branch: production + - event: push + - path: + include: [ 'stack.production.yml', 'stack.staging.yml', 'docker-compose.staging.yml', 'docker-compose.production.yml', 'Dockerfile', '*.tests.ts' ] + + # Deploy to Production Branch + deploy-production: + name: deploy-production + image: woodpeckerci/plugin-docker-buildx + secrets: [REGISTRY_USER, REGISTRY_PASSWORD] + volumes: + - /var/run/docker.sock:/var/run/docker.sock + commands: + - echo "$${REGISTRY_PASSWORD}" | docker login -u "$${REGISTRY_USER}" --password-stdin git.nixc.us + # - docker stack deploy --with-registry-auth -c ./stack.production.yml $${CI_REPO_NAME} + when: + - branch: production + - event: push + # - path: + # include: [ 'stack.production.yml', 'stack.staging.yml', 'docker-compose.staging.yml', 'docker-compose.production.yml', 'Dockerfile', '*.tests.ts' ] + + + # # Wait for Deploy to Complete + # wait-for-deploy-production: + # name: wait-for-deploy-production + # image: woodpeckerci/plugin-git + # commands: + # - echo "Waiting for deploy step to complete rollout." + # - sleep 60 + # when: + # - branch: production + # - event: push + + # # Run Post-Deployment Smoke Tests + # post-deploy-smoke-tests-git-nixc-us: + # name: run-post-deploy-smoke-tests-git-nixc-us + # image: git.nixc.us/colin/playwright:latest + # # secrets: [TEST_USER, TEST_PASSWORD] + # environment: + # - BASE_URL=https://git.nixc.us + # when: + # - branch: production + # - event: push + # # - path: + # # include: [ 'stack.production.yml', 'stack.staging.yml', 'docker-compose.staging.yml', 'docker-compose.production.yml', 'Dockerfile', '*.tests.ts' ] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..bfb225e --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ + +# bench plugin + +### Docker Run + +Run the Docker image using the `docker run` command. I'll provide examples for different scenarios: + +1. **Testing a Single URL:** + + ```bash + docker run -e TARGET_URLS="http://example.com" -e MAX_TIME=1 -e VARIANCE=2 -e TEST_MODE="report" git.nixc.us/colin/bench:production + ``` + +2. **Testing Multiple URLs:** + + ```bash + docker run -e TARGET_URLS="http://example.com,http://example2.com" -e MAX_TIME=1 -e VARIANCE=2 -e TEST_MODE="report" git.nixc.us/colin/bench:production + ``` + +3. **Using Fail-Fast Mode:** + + ```bash + docker run -e TARGET_URLS="http://example.com" -e MAX_TIME=1 -e VARIANCE=2 -e TEST_MODE="fail-fast" git.nixc.us/colin/bench:production + ``` + +### Docker Compose + +Example `docker-compose.yml` file that sets up the same configuration: + +```yaml +version: '3' + +services: + apachebench: + image: git.nixc.us/colin/bench:production + environment: + TARGET_URLS: "http://example.com,http://example2.com" # Comma-separated list of URLs + MAX_TIME: 1 # Maximum acceptable response time + VARIANCE: 2 # Acceptable time variance + TEST_MODE: "report" # Can be "report" or "fail-fast" + NUM_REQUESTS: 500 # Number of requests to perform + CONCURRENCY: 20 # Number of multiple requests to make at a time + +``` + +Run the service defined in the `docker-compose.yml` file, use: + +```bash +docker-compose up +``` + +### Customizing the Configuration + +You can modify the `TARGET_URLS`, `MAX_TIME`, `VARIANCE`, and `TEST_MODE` environment variables to suit your specific testing needs. Here's what each variable represents: + +- `TARGET_URLS`: A comma-separated list of URLs to test. +- `MAX_TIME`: The target maximum response time in seconds. +- `VARIANCE`: The acceptable variance in response time in seconds. +- `TEST_MODE`: The mode of operation, either `report` to test all URLs regardless of individual results, or `fail-fast` to stop testing as soon as a URL fails the performance criteria. + +This setup allows you to run performance tests in a Docker environment, providing an automated way to benchmark and analyze the performance of various web services. \ No newline at end of file diff --git a/docker-compose.production.yml b/docker-compose.production.yml new file mode 100644 index 0000000..d5df34c --- /dev/null +++ b/docker-compose.production.yml @@ -0,0 +1,6 @@ +version: "3.9" +services: + bench: + build: + context: docker/bench + image: git.nixc.us/colin/bench:production diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml new file mode 100644 index 0000000..e795c51 --- /dev/null +++ b/docker-compose.staging.yml @@ -0,0 +1,6 @@ +version: "3.9" +services: + bench: + build: + context: docker/bench + image: git.nixc.us/colin/bench:staging diff --git a/docker/bench/Dockerfile b/docker/bench/Dockerfile new file mode 100644 index 0000000..c0c5fe6 --- /dev/null +++ b/docker/bench/Dockerfile @@ -0,0 +1,5 @@ +FROM alpine:latest +RUN apk --no-cache add apache2-utils curl ca-certificates bash +COPY run-ab.sh /run-ab.sh +RUN chmod +x /run-ab.sh +ENTRYPOINT ["/run-ab.sh"] diff --git a/docker/bench/run-ab.sh b/docker/bench/run-ab.sh new file mode 100644 index 0000000..3730d09 --- /dev/null +++ b/docker/bench/run-ab.sh @@ -0,0 +1,141 @@ +#!/bin/bash + +DEFAULT_MAX_TIME=1 +DEFAULT_VARIANCE=2 +DEFAULT_TTFB_MAX=500 +DEFAULT_TEST_MODE="report" +DEFAULT_NUM_REQUESTS=100 +DEFAULT_CONCURRENCY=10 + +TARGET_URLS=${TARGET_URLS:-""} +MAX_TIME=${MAX_TIME:-$DEFAULT_MAX_TIME} +VARIANCE=${VARIANCE:-$DEFAULT_VARIANCE} +TTFB_MAX=${TTFB_MAX:-$DEFAULT_TTFB_MAX} +TEST_MODE=${TEST_MODE:-$DEFAULT_TEST_MODE} +NUM_REQUESTS=${NUM_REQUESTS:-$DEFAULT_NUM_REQUESTS} +CONCURRENCY=${CONCURRENCY:-$DEFAULT_CONCURRENCY} + +declare -a failed_urls + +process_sitemap() { + IFS=',' read -r -a domains <<< "$TARGET_URLS" + for domain in "${domains[@]}"; do + + domain="${domain%/}" + sitemap=$(curl -s "${domain}/sitemap.xml") + while IFS= read -r loc; do + url=$(echo "$loc" | sed -e 's|||g' -e 's|||g' | xargs) + if [[ "$url" =~ ^https?:// ]]; then + new_url="$url" + else + url="${url#/}" + new_url="${domain}/${url}" + fi + new_url=$(echo "$new_url" | sed -e 's|//|/|g' -e 's|:/|://|') + if [[ ! "${TARGET_URLS}" =~ "${new_url}" ]]; then + TARGET_URLS+=",${new_url}" + fi + done < <(echo "$sitemap" | grep -oE ".+?") + done +} + +process_sitemap + +measure_ttfb() { + local url=$1 + local -a times=() + local min max sum=0 mean ttfb mean_ttfb + for i in {1..5}; do + ttfb=$(curl -o /dev/null -s -w '%{time_starttransfer}\n' "$url" | awk '{print $1 * 1000}') + times+=("$ttfb") + sum=$(echo "$sum + $ttfb" | bc) + done + min=$(printf '%s\n' "${times[@]}" | sort -n | head -1) + max=$(printf '%s\n' "${times[@]}" | sort -n | tail -1) + mean=$(echo "scale=3; $sum / 5" | bc) + + echo "$mean" +} + +run_ab() { + local url=$1 + local result=$(ab -n $NUM_REQUESTS -c $CONCURRENCY "$url" 2>&1) + local avg_time=$(echo "$result" | grep 'Time per request' | head -1 | awk '{print $4}') + if [ -z "$avg_time" ] || [ "$(echo "$avg_time > ($MAX_TIME + $VARIANCE) * 1000" | bc)" -eq 1 ]; then + echo "ApacheBench test failed for $url. Average time: ${avg_time:-N/A} milliseconds." + return 1 + else + echo "ApacheBench test passed for $url. Average time: $avg_time milliseconds." + return 0 + fi +} + +log_file="/log/scan.log" +mkdir -p $(dirname "$log_file") +cat /dev/null > $"$log_file" + +IFS=',' read -ra URLS <<< "$TARGET_URLS" +for url in "${URLS[@]}"; do + mean_ttfb=$(measure_ttfb "$url") + if [ -z "$mean_ttfb" ] || [ "$(echo "$mean_ttfb > $TTFB_MAX" | bc)" -eq 1 ]; then + echo "TTFB test failed for $url. Mean TTFB: ${mean_ttfb}ms exceeds maximum of ${TTFB_MAX}ms." | tee -a "$log_file" + failed_urls+=("TTFB failure: $url") + else + echo "TTFB test passed for $url. Mean TTFB: ${mean_ttfb}ms." | tee -a "$log_file" + fi + run_ab_result=$(run_ab "$url") + echo "$run_ab_result" | tee -a "$log_file" + if [[ "$run_ab_result" == *"failed"* ]]; then + failed_urls+=("ApacheBench failure: $url") + fi + if [ "$TEST_MODE" = "fail-fast" ] && [ ${#failed_urls[@]} -gt 0 ]; then + echo "Exiting due to fail-fast mode with failures." | tee -a "$log_file" + for failed_url in "${failed_urls[@]}"; do + echo "- $failed_url" | tee -a "$log_file" + done + exit 1 + fi +done + +if [ ${#failed_urls[@]} -gt 0 ]; then + echo "Summary of failed URLs:" | tee -a "$log_file" + for failed_url in "${failed_urls[@]}"; do + echo "- $failed_url" | tee -a "$log_file" + done + exit 1 +else + echo "All URLs passed the performance and TTFB tests." | tee -a "$log_file" + exit 0 +fi + +parse_to_csv() { + local input="$1" + local output="$2" + : > "$output" + grep -Eo 'https?://[^ ]+' "$input" | sort -u | while read -r url; do + local ttfb_line=$(grep -m 1 "$url" "$input" | grep 'TTFB test passed' | awk -F'Mean TTFB: ' '{print $2}') + local ab_line=$(grep -m 1 "$url" "$input" | grep 'ApacheBench test passed' | awk -F'Average time: ' '{print $2}') + local ttfb_test=${ttfb_line%ms.*} + local ab_test=${ab_line% milliseconds.*} + if [[ -n "$ttfb_test" || -n "$ab_test" ]]; then + if [[ -z "$ttfb_test" ]]; then ttfb_test="N/A"; fi + if [[ -z "$ab_test" ]]; then ab_test="N/A"; fi + echo "$url,$ttfb_test,$ab_test" >> "$output" + fi + done +} + +parse_to_csv "/logs/scan.log" "/logs/scan.csv" + +upload_to_hastebin() { + local file=$1 + local haste_url="https://haste.nixc.us/documents" + local response=$(curl -X POST -s --data-binary @"$file" $haste_url) + local key=$(echo $response | awk -F '"' '{print $4}') + if [[ ! -z "$key" ]]; then + echo "Uploaded to hastebin: https://haste.nixc.us/$key" + else + echo "Failed to upload to hastebin." + fi +} +upload_to_hastebin "/logs/scan.csv" \ No newline at end of file