diff --git a/.github/workflows/build-ova.yml b/.github/workflows/build-ova.yml index 3ea8e43..3e3afa9 100644 --- a/.github/workflows/build-ova.yml +++ b/.github/workflows/build-ova.yml @@ -92,7 +92,6 @@ jobs: echo "Uploaded: s3://defguard-downloads/ova/${FILENAME}" - name: Test deployment modes on Proxmox - if: ${{ github.event_name == 'workflow_dispatch' }} env: PROXMOX_HOST: ${{ secrets.PROXMOX_HOST }} PROXMOX_SSH_KEY: ${{ secrets.PROXMOX_SSH_KEY }} diff --git a/.github/workflows/test-ova.yml b/.github/workflows/test-ova.yml index 367a560..ee99979 100644 --- a/.github/workflows/test-ova.yml +++ b/.github/workflows/test-ova.yml @@ -22,13 +22,13 @@ jobs: - name: Checkout uses: actions/checkout@v7 - - name: Install bats + - name: Install bats and jq run: | sudo apt-get update -qq - sudo apt-get install -y --no-install-recommends bats + sudo apt-get install -y --no-install-recommends bats jq - name: Run logic and config tests - run: bats ova/tests/start.bats ova/tests/generate-env.bats ova/tests/config.bats ova/tests/firewall.bats + run: bats ova/tests/generate-env.bats ova/tests/generate-compose.bats ova/tests/firewall.bats integration: name: Real bring-up @@ -37,10 +37,10 @@ jobs: - name: Checkout uses: actions/checkout@v7 - - name: Install bats + - name: Install bats and jq run: | sudo apt-get update -qq - sudo apt-get install -y --no-install-recommends bats + sudo apt-get install -y --no-install-recommends bats jq - name: Login to GitHub container registry uses: docker/login-action@v4 diff --git a/ova/defguard.pkr.hcl b/ova/defguard.pkr.hcl index 47c014c..46fb548 100644 --- a/ova/defguard.pkr.hcl +++ b/ova/defguard.pkr.hcl @@ -68,13 +68,13 @@ build { } provisioner "file" { - source = "files/docker-compose.yaml" - destination = "/tmp/docker-compose.yaml" + source = "files/docker-compose.template.yaml" + destination = "/tmp/docker-compose.template.yaml" } provisioner "file" { - source = "files/docker-compose.standalone.yaml" - destination = "/tmp/docker-compose.standalone.yaml" + source = "files/lib.sh" + destination = "/tmp/lib.sh" } provisioner "file" { @@ -83,8 +83,8 @@ build { } provisioner "file" { - source = "files/start.sh" - destination = "/tmp/start.sh" + source = "files/generate-compose.sh" + destination = "/tmp/generate-compose.sh" } provisioner "file" { @@ -115,16 +115,15 @@ build { provisioner "shell" { inline = [ "sudo bash /tmp/docker-setup.sh", - "sudo mkdir -p /opt/stacks/defguard", - "sudo mv /tmp/docker-compose.yaml /opt/stacks/defguard/docker-compose.yaml", - "sudo mv /tmp/docker-compose.standalone.yaml /opt/stacks/defguard/docker-compose.standalone.yaml", - "sudo mv /tmp/generate-env.sh /opt/stacks/defguard/generate-env.sh", - "sudo chmod +x /opt/stacks/defguard/generate-env.sh", - "sudo mv /tmp/start.sh /opt/stacks/defguard/start.sh", - "sudo chmod +x /opt/stacks/defguard/start.sh", - "echo 'DEFGUARD_CORE_TAG=${var.core_tag}' | sudo tee /opt/stacks/defguard/.image-tags > /dev/null", - "echo 'DEFGUARD_PROXY_TAG=${var.proxy_tag}' | sudo tee -a /opt/stacks/defguard/.image-tags > /dev/null", - "echo 'DEFGUARD_GATEWAY_TAG=${var.gateway_tag}' | sudo tee -a /opt/stacks/defguard/.image-tags > /dev/null", + "sudo mkdir -p /opt/stacks/defguard/init", + "sudo mv /tmp/docker-compose.template.yaml /opt/stacks/defguard/init/docker-compose.template.yaml", + "sudo mv /tmp/lib.sh /opt/stacks/defguard/init/lib.sh", + "sudo mv /tmp/generate-env.sh /opt/stacks/defguard/init/generate-env.sh", + "sudo mv /tmp/generate-compose.sh /opt/stacks/defguard/init/generate-compose.sh", + "sudo chmod +x /opt/stacks/defguard/init/generate-env.sh /opt/stacks/defguard/init/generate-compose.sh", + "echo 'DEFGUARD_CORE_TAG=${var.core_tag}' | sudo tee /opt/stacks/defguard/init/.image-tags > /dev/null", + "echo 'DEFGUARD_PROXY_TAG=${var.proxy_tag}' | sudo tee -a /opt/stacks/defguard/init/.image-tags > /dev/null", + "echo 'DEFGUARD_GATEWAY_TAG=${var.gateway_tag}' | sudo tee -a /opt/stacks/defguard/init/.image-tags > /dev/null", "sudo mv /tmp/99-defguard.cfg /etc/cloud/cloud.cfg.d/99-defguard.cfg", "sudo mv /tmp/defguard-init.service /etc/systemd/system/defguard-init.service", "sudo mv /tmp/defguard-firewall.sh /opt/stacks/defguard/defguard-firewall.sh", @@ -139,6 +138,8 @@ build { "sudo systemctl enable defguard-init.service", "sudo systemctl enable defguard-firewall.service", "sudo chown -R ubuntu:ubuntu /opt/stacks/defguard", + "sudo chown -R root:root /opt/stacks/defguard/init", + "sudo chmod -R go-w /opt/stacks/defguard/init", "sudo rm -f /etc/netplan/00-installer-config.yaml /etc/netplan/50-cloud-init.yaml", "sudo cloud-init clean --logs", "sudo rm -f /etc/ssh/ssh_host_*", diff --git a/ova/files/defguard-init.service b/ova/files/defguard-init.service index b759004..5b9c939 100644 --- a/ova/files/defguard-init.service +++ b/ova/files/defguard-init.service @@ -8,8 +8,9 @@ Type=oneshot WorkingDirectory=/opt/stacks/defguard StandardOutput=append:/var/log/defguard-startup.log StandardError=append:/var/log/defguard-startup.log -ExecStart=/bin/bash /opt/stacks/defguard/generate-env.sh -ExecStart=/bin/bash /opt/stacks/defguard/start.sh +ExecStart=/bin/bash /opt/stacks/defguard/init/generate-env.sh +ExecStart=/bin/bash /opt/stacks/defguard/init/generate-compose.sh +ExecStart=/usr/bin/docker compose -f /opt/stacks/defguard/docker-compose.yml up -d [Install] WantedBy=multi-user.target diff --git a/ova/files/docker-compose.standalone.yaml b/ova/files/docker-compose.template.yaml similarity index 65% rename from ova/files/docker-compose.standalone.yaml rename to ova/files/docker-compose.template.yaml index 3559385..052e442 100644 --- a/ova/files/docker-compose.standalone.yaml +++ b/ova/files/docker-compose.template.yaml @@ -2,11 +2,15 @@ services: core: restart: unless-stopped profiles: [core] - image: ghcr.io/defguard/defguard:${DEFGUARD_CORE_TAG:?DEFGUARD_CORE_TAG is required} + image: ghcr.io/defguard/defguard:__DEFGUARD_CORE_TAG__ env_file: .env environment: DEFGUARD_DB_HOST: db DEFGUARD_DB_PORT: 5432 + DEFGUARD_ADOPT_EDGE: ${DEFGUARD_ADOPT_EDGE:-} + DEFGUARD_ADOPT_GATEWAY: ${DEFGUARD_ADOPT_GATEWAY:-} + extra_hosts: + - "host.docker.internal:host-gateway" depends_on: - db ports: @@ -15,19 +19,21 @@ services: edge: restart: unless-stopped profiles: [edge] - image: ghcr.io/defguard/defguard-proxy:${DEFGUARD_PROXY_TAG:?DEFGUARD_PROXY_TAG is required} + image: ghcr.io/defguard/defguard-proxy:__DEFGUARD_PROXY_TAG__ volumes: - ./.volumes/certs/edge:/etc/defguard/certs ports: - "8080:8080" - - "50051:50051" - "443:443" - "80:80" + # 50051 is added conditionally by generate-compose.sh: exposed for segmented + # deployments (core reaches edge from another host), omitted for the full + # all-in-one stack (core reaches edge via the docker-internal network). gateway: restart: unless-stopped profiles: [gateway] - image: ghcr.io/defguard/gateway:${DEFGUARD_GATEWAY_TAG:?DEFGUARD_GATEWAY_TAG is required} + image: ghcr.io/defguard/gateway:__DEFGUARD_GATEWAY_TAG__ cap_add: - NET_ADMIN volumes: diff --git a/ova/files/docker-compose.yaml b/ova/files/docker-compose.yaml deleted file mode 100644 index 6995861..0000000 --- a/ova/files/docker-compose.yaml +++ /dev/null @@ -1,60 +0,0 @@ -services: - core: - restart: unless-stopped - image: ghcr.io/defguard/defguard:${DEFGUARD_CORE_TAG:?DEFGUARD_CORE_TAG is required} - env_file: .env - environment: - DEFGUARD_DB_HOST: db - DEFGUARD_DB_PORT: 5432 - DEFGUARD_ADOPT_EDGE: "edge:50051" - DEFGUARD_ADOPT_GATEWAY: "host.docker.internal:50066" - extra_hosts: - - "host.docker.internal:host-gateway" - depends_on: - - db - - edge - - gateway - ports: - - "8000:8000" - - edge: - restart: unless-stopped - image: ghcr.io/defguard/defguard-proxy:${DEFGUARD_PROXY_TAG:?DEFGUARD_PROXY_TAG is required} - volumes: - - ./.volumes/certs/edge:/etc/defguard/certs - ports: - - "8080:8080" - - "443:443" - - "80:80" - - gateway: - restart: unless-stopped - image: ghcr.io/defguard/gateway:${DEFGUARD_GATEWAY_TAG:?DEFGUARD_GATEWAY_TAG is required} - cap_add: - - NET_ADMIN - volumes: - - ./.volumes/certs/gateway:/etc/defguard/certs - environment: - DEFGUARD_STATS_PERIOD: 10 - HEALTH_PORT: 55003 - network_mode: "host" - - dockge: - image: louislam/dockge:1 - restart: unless-stopped - profiles: [dockge] - ports: - - "5001:5001" - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - ./.volumes/dockge:/app/data - - /opt/stacks:/opt/stacks - environment: - DOCKGE_STACKS_DIR: /opt/stacks - - db: - restart: unless-stopped - image: postgres:18-alpine - env_file: .env - volumes: - - ./.volumes/db:/var/lib/postgresql diff --git a/ova/files/docker-setup.sh b/ova/files/docker-setup.sh index ab56151..40eb776 100644 --- a/ova/files/docker-setup.sh +++ b/ova/files/docker-setup.sh @@ -4,7 +4,8 @@ set -e apt-get update apt-get full-upgrade -y # open-vm-tools: graceful shutdown, guest IP reporting, and time sync under VMware. -apt-get install -y ca-certificates curl open-vm-tools +# jq: strips profiles from generate-compose.sh's flattened docker-compose.yml +apt-get install -y ca-certificates curl open-vm-tools jq install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc chmod a+r /etc/apt/keyrings/docker.asc diff --git a/ova/files/generate-compose.sh b/ova/files/generate-compose.sh new file mode 100755 index 0000000..189f8da --- /dev/null +++ b/ova/files/generate-compose.sh @@ -0,0 +1,67 @@ +#!/bin/bash +# Flattens the profile-gated compose template into a single, self-contained +# /opt/stacks/defguard/docker-compose.yml on first boot, then removes the +# cloud-init mode-selection flag files so the stack directory stays clean. +# If docker-compose.yml already exists, this script does nothing. +set -euo pipefail + +STACK_DIR="${DEFGUARD_STACK_DIR:-/opt/stacks/defguard}" +INIT_DIR="${DEFGUARD_INIT_DIR:-$STACK_DIR/init}" +TEMPLATE="$INIT_DIR/docker-compose.template.yaml" +COMPOSE_FILE="$STACK_DIR/docker-compose.yml" +PROFILES_FILE="$STACK_DIR/active-profiles" +ENABLE_DOCKER_MGMT_FILE="$STACK_DIR/enable-docker-management" + +if [ -f "$COMPOSE_FILE" ]; then + echo "DefGuard: docker-compose.yml already exists, skipping generation." + exit 0 +fi + +# shellcheck source=lib.sh +source "$INIT_DIR/lib.sh" + +mapfile -t _profiles < <(resolve_profiles "$STACK_DIR") + +FULL_STACK=false +if is_full_stack "${_profiles[@]}"; then + FULL_STACK=true +fi + +_profile_args=() +for p in "${_profiles[@]}"; do _profile_args+=(--profile "$p"); done + +# `docker compose config --profile X` (repeatable) filters the emitted service +# list to active-profile + unprofiled services, but surviving services keep +# their `profiles:` key - which would make them invisible to a future plain +# `docker compose up -d` with no COMPOSE_PROFILES set. jq strips that key and +# injects the conditional depends_on/port tweaks; the result is then piped +# back through `docker compose config` (stdin) to render real YAML rather +# than leaving the file as JSON-flavored-as-YAML. +TMP_COMPOSE_FILE="$COMPOSE_FILE.tmp" +trap 'rm -f "$TMP_COMPOSE_FILE"' EXIT +{ + echo "# Generated by defguard OVA first-boot init on $(date -u +%FT%TZ)." + echo "# Selected profiles: ${_profiles[*]}" + docker compose -f "$TEMPLATE" -p defguard --project-directory "$STACK_DIR" --env-file "$STACK_DIR/.env" "${_profile_args[@]}" \ + config --format json \ + | jq --argjson full "$FULL_STACK" ' + del(.services[].profiles) | del(.name) + | if $full then + .services.core.depends_on = ["db", "edge", "gateway"] + else + (.services.edge // empty) |= (.ports += ["50051:50051"]) + end + ' \ + | docker compose -f - -p defguard --project-directory "$STACK_DIR" config +} > "$TMP_COMPOSE_FILE" + +sed -i \ + -e "s/__DEFGUARD_CORE_TAG__/\${DEFGUARD_CORE_TAG}/" \ + -e "s/__DEFGUARD_PROXY_TAG__/\${DEFGUARD_PROXY_TAG}/" \ + -e "s/__DEFGUARD_GATEWAY_TAG__/\${DEFGUARD_GATEWAY_TAG}/" \ + "$TMP_COMPOSE_FILE" + +mv "$TMP_COMPOSE_FILE" "$COMPOSE_FILE" + +rm -f "$PROFILES_FILE" "$ENABLE_DOCKER_MGMT_FILE" +echo "DefGuard: generated $COMPOSE_FILE for profiles: ${_profiles[*]}" diff --git a/ova/files/generate-env.sh b/ova/files/generate-env.sh index be6634f..581fe30 100644 --- a/ova/files/generate-env.sh +++ b/ova/files/generate-env.sh @@ -1,8 +1,10 @@ #!/bin/bash # Generates /opt/stacks/defguard/.env with random secrets on first boot. # If .env already exists (e.g. provided via cloud-init), this script does nothing. +set -euo pipefail STACK_DIR="${DEFGUARD_STACK_DIR:-/opt/stacks/defguard}" +INIT_DIR="${DEFGUARD_INIT_DIR:-$STACK_DIR/init}" ENV_FILE="$STACK_DIR/.env" if [ -f "$ENV_FILE" ]; then @@ -12,16 +14,30 @@ fi echo "DefGuard: generating .env with random secrets..." +# shellcheck source=lib.sh +source "$INIT_DIR/lib.sh" + DB_PASSWORD=$(openssl rand -hex 16) -if [ -f "$STACK_DIR/.image-tags" ]; then - source "$STACK_DIR/.image-tags" +if [ -f "$INIT_DIR/.image-tags" ]; then + source "$INIT_DIR/.image-tags" fi : "${DEFGUARD_CORE_TAG:?DEFGUARD_CORE_TAG is required}" : "${DEFGUARD_PROXY_TAG:?DEFGUARD_PROXY_TAG is required}" : "${DEFGUARD_GATEWAY_TAG:?DEFGUARD_GATEWAY_TAG is required}" +# Only default the adopt targets when core/edge/gateway are genuinely +# co-located on this host; segmented deployments must fill these in manually +# since edge/gateway live on other VMs entirely. +mapfile -t _profiles < <(resolve_profiles "$STACK_DIR") +ADOPT_EDGE="" +ADOPT_GATEWAY="" +if is_full_stack "${_profiles[@]}"; then + ADOPT_EDGE="edge:50051" + ADOPT_GATEWAY="host.docker.internal:50066" +fi + cat > "$ENV_FILE" <&2 + fi + profiles=(core edge gateway) + fi + + if [ -f "$dockge_file" ]; then + profiles+=(dockge) + fi + + printf '%s\n' "${profiles[@]}" +} + +# True (exit 0) only if core, edge, and gateway are ALL present among the given +# profiles (dockge and ordering are irrelevant) - i.e. this is a genuine +# same-host all-in-one deployment, not a segmented one. +is_full_stack() { + local has_core=0 has_edge=0 has_gateway=0 p + for p in "$@"; do + case "$p" in + core) has_core=1 ;; + edge) has_edge=1 ;; + gateway) has_gateway=1 ;; + esac + done + [ "$has_core" = 1 ] && [ "$has_edge" = 1 ] && [ "$has_gateway" = 1 ] +} diff --git a/ova/files/start.sh b/ova/files/start.sh deleted file mode 100644 index 9f5a1e8..0000000 --- a/ova/files/start.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash -# Starts defguard via docker compose. -# Default (no active-profiles file): starts the full all-in-one stack. -# To select specific components, create /opt/stacks/defguard/active-profiles with a -# space or newline-separated list of profiles: core, gateway, edge -# -# To enable the Dockge docker management UI (port 5001), create the file: -# /opt/stacks/defguard/enable-docker-management -# Example cloud-init: -# write_files: -# - path: /opt/stacks/defguard/enable-docker-management -# content: "" - -STACK_DIR="${DEFGUARD_STACK_DIR:-/opt/stacks/defguard}" -PROFILES_FILE="$STACK_DIR/active-profiles" -ENABLE_DOCKER_MGMT_FILE="$STACK_DIR/enable-docker-management" - -# Append the dockge profile if the opt-in flag file is present -_maybe_add_dockge() { - local profiles="$1" - if [ -f "$ENABLE_DOCKER_MGMT_FILE" ]; then - if [ -z "$profiles" ]; then - echo "dockge" - else - echo "${profiles},dockge" - fi - else - echo "$profiles" - fi -} - -if [ ! -f "$PROFILES_FILE" ]; then - COMPOSE_PROFILES=$(_maybe_add_dockge "") - if [ -n "$COMPOSE_PROFILES" ]; then - export COMPOSE_PROFILES - fi - docker compose -f "$STACK_DIR/docker-compose.yaml" up -d -else - COMPOSE_PROFILES=$(tr '[:space:]' ',' < "$PROFILES_FILE" | tr -s ',' | sed 's/,$//') - if [ -z "$COMPOSE_PROFILES" ]; then - echo "Warning: $PROFILES_FILE is empty or contains only whitespace; starting full all-in-one stack." - COMPOSE_PROFILES=$(_maybe_add_dockge "") - if [ -n "$COMPOSE_PROFILES" ]; then - export COMPOSE_PROFILES - else - unset COMPOSE_PROFILES - fi - docker compose -f "$STACK_DIR/docker-compose.yaml" up -d - else - COMPOSE_PROFILES=$(_maybe_add_dockge "$COMPOSE_PROFILES") - export COMPOSE_PROFILES - docker compose -f "$STACK_DIR/docker-compose.standalone.yaml" up -d - fi -fi diff --git a/ova/test/test-deployment-modes.sh b/ova/test/test-deployment-modes.sh index c3d0568..66ba006 100755 --- a/ova/test/test-deployment-modes.sh +++ b/ova/test/test-deployment-modes.sh @@ -30,7 +30,7 @@ MODES=(full core edge gateway) declare -A VMID=( [full]=$((VMID_BASE+1)) [core]=$((VMID_BASE+2)) [edge]=$((VMID_BASE+3)) [gateway]=$((VMID_BASE+4)) ) declare -A IP_LAST=( [full]=150 [core]=151 [edge]=152 [gateway]=153 ) declare -A PROFILE=( [full]="" [core]=core [edge]=edge [gateway]=gateway ) -# Ground truth from ova/files/docker-compose.standalone.yaml (full uses the all-in-one). +# Ground truth from ova/files/docker-compose.template.yaml. declare -A EXPECT=( [full]="core db edge gateway" [core]="core db" [edge]="edge" [gateway]="gateway" ) declare -A FORBID=( [full]="" [core]="edge gateway" [edge]="core db gateway" [gateway]="core db edge" ) @@ -106,16 +106,7 @@ wait_services() { } verify_mode() { - local mode="$1" ip="$2" profile="${PROFILE[$mode]}" names actual - - if [ -z "$profile" ]; then - vm_ssh "$ip" "test ! -e /opt/stacks/defguard/active-profiles" \ - || { log "$mode: active-profiles unexpectedly present"; return 1; } - else - actual="$(vm_ssh "$ip" "cat /opt/stacks/defguard/active-profiles 2>/dev/null" | tr -d '[:space:]')" - [ "$actual" = "$profile" ] \ - || { log "$mode: active-profiles is '$actual', expected '$profile'"; return 1; } - fi + local mode="$1" ip="$2" names actual names="$(wait_services "$ip" "${EXPECT[$mode]}")" \ || { log "$mode: expected services did not all start; running: $(tr '\n' ' ' <<<"$names")"; return 1; } @@ -125,6 +116,47 @@ verify_mode() { has_service "$names" "$svc" \ && { log "$mode: unexpected service '$svc' is running"; return 1; } done + + vm_ssh "$ip" "test ! -e /opt/stacks/defguard/active-profiles" \ + || { log "$mode: active-profiles unexpectedly present (should be consumed and deleted)"; return 1; } + + actual="$(vm_ssh "$ip" "ls -A /opt/stacks/defguard" | LC_ALL=C sort | tr '\n' ' ' | sed 's/ $//')" + [ "$actual" = ".env .volumes defguard-firewall.sh docker-compose.yml init" ] \ + || { log "$mode: /opt/stacks/defguard contains '$actual', expected only docker-compose.yml, .env, .volumes, defguard-firewall.sh, init"; return 1; } + + reprovision_mode "$mode" "$ip" || return 1 + + return 0 +} + +# Exercises the manual recovery path: an operator deletes a +# broken/stale docker-compose.yml and restarts defguard-init.service to +# regenerate it. +reprovision_mode() { + local mode="$1" ip="$2" profile="${PROFILE[$mode]}" names + + log "$mode: exercising recovery (rm docker-compose.yml + restart defguard-init)" + + if [ -n "$profile" ]; then + vm_ssh "$ip" "echo '$profile' | sudo tee /opt/stacks/defguard/active-profiles >/dev/null" + fi + vm_ssh "$ip" "sudo rm -f /opt/stacks/defguard/docker-compose.yml" + vm_ssh "$ip" "sudo systemctl restart defguard-init.service" + + vm_ssh "$ip" "test -e /opt/stacks/defguard/docker-compose.yml" \ + || { log "$mode: recovery did not recreate docker-compose.yml"; return 1; } + vm_ssh "$ip" "test ! -e /opt/stacks/defguard/active-profiles" \ + || { log "$mode: recovery left active-profiles behind"; return 1; } + + names="$(wait_services "$ip" "${EXPECT[$mode]}")" \ + || { log "$mode: recovery: expected services did not come back; running: $(tr '\n' ' ' <<<"$names")"; return 1; } + + local svc + for svc in ${FORBID[$mode]}; do + has_service "$names" "$svc" \ + && { log "$mode: recovery: unexpected service '$svc' is running"; return 1; } + done + return 0 } diff --git a/ova/tests/config.bats b/ova/tests/config.bats deleted file mode 100644 index 0e80c79..0000000 --- a/ova/tests/config.bats +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bats -# `docker compose config` evaluates profiles without pulling images, so the -# expected service set per profile combination can be checked offline. - -load helpers - -setup() { - command -v docker >/dev/null 2>&1 || skip "docker not installed" - docker compose version >/dev/null 2>&1 || skip "docker compose v2 not available" - make_stack - write_env -} - -teardown() { - teardown_stack -} - -# sorted, space-joined active services for a given file ($1) and profiles ($2) -services_for() { - COMPOSE_PROFILES="$2" docker compose -f "$STACK_DIR/$1" config --services 2>/dev/null | sort | xargs -} - -@test "all-in-one without profiles -> core db edge gateway" { - [ "$(services_for docker-compose.yaml "")" = "core db edge gateway" ] -} - -@test "all-in-one with dockge -> core db dockge edge gateway" { - [ "$(services_for docker-compose.yaml "dockge")" = "core db dockge edge gateway" ] -} - -@test "standalone core -> core db" { - [ "$(services_for docker-compose.standalone.yaml "core")" = "core db" ] -} - -@test "standalone core,gateway -> core db gateway" { - [ "$(services_for docker-compose.standalone.yaml "core,gateway")" = "core db gateway" ] -} - -@test "standalone core,edge,gateway -> core db edge gateway" { - [ "$(services_for docker-compose.standalone.yaml "core,edge,gateway")" = "core db edge gateway" ] -} - -@test "standalone core,edge,gateway,dockge -> core db dockge edge gateway" { - [ "$(services_for docker-compose.standalone.yaml "core,edge,gateway,dockge")" = "core db dockge edge gateway" ] -} - -@test "standalone with no profiles -> no services" { - [ "$(services_for docker-compose.standalone.yaml "")" = "" ] -} diff --git a/ova/tests/generate-compose.bats b/ova/tests/generate-compose.bats new file mode 100644 index 0000000..4a3acae --- /dev/null +++ b/ova/tests/generate-compose.bats @@ -0,0 +1,158 @@ +#!/usr/bin/env bats +# `docker compose config` evaluates profiles without pulling images, so +# generate-compose.sh's output can be checked offline. + +load helpers + +setup() { + command -v docker >/dev/null 2>&1 || skip "docker not installed" + docker compose version >/dev/null 2>&1 || skip "docker compose v2 not available" + command -v jq >/dev/null 2>&1 || skip "jq not installed" + make_stack + write_env +} + +teardown() { + teardown_stack +} + +generated_services() { + unset COMPOSE_PROFILES + docker compose -f "$STACK_DIR/docker-compose.yml" config --services 2>/dev/null | sort | xargs +} + +generated_json() { + unset COMPOSE_PROFILES + docker compose -f "$STACK_DIR/docker-compose.yml" config --format json +} + +@test "no active-profiles -> full stack: core db edge gateway" { + run bash "$FILES_DIR/generate-compose.sh" + [ "$status" -eq 0 ] + [ "$(generated_services)" = "core db edge gateway" ] +} + +@test "active-profiles=core -> core db" { + echo "core" > "$STACK_DIR/active-profiles" + run bash "$FILES_DIR/generate-compose.sh" + [ "$status" -eq 0 ] + [ "$(generated_services)" = "core db" ] +} + +@test "active-profiles='core gateway' -> core db gateway" { + printf 'core gateway\n' > "$STACK_DIR/active-profiles" + run bash "$FILES_DIR/generate-compose.sh" + [ "$status" -eq 0 ] + [ "$(generated_services)" = "core db gateway" ] +} + +@test "full stack + dockge flag -> core db dockge edge gateway" { + touch "$STACK_DIR/enable-docker-management" + run bash "$FILES_DIR/generate-compose.sh" + [ "$status" -eq 0 ] + [ "$(generated_services)" = "core db dockge edge gateway" ] +} + +@test "no surviving service carries a profiles key" { + run bash "$FILES_DIR/generate-compose.sh" + [ "$status" -eq 0 ] + [ "$(generated_json | jq '[.services[] | select(has("profiles"))] | length')" -eq 0 ] +} + +@test "full stack: core.depends_on includes edge and gateway" { + run bash "$FILES_DIR/generate-compose.sh" + [ "$status" -eq 0 ] + deps="$(generated_json | jq -r 'if (.services.core.depends_on | type) == "array" then .services.core.depends_on[] else (.services.core.depends_on | keys[]) end' | sort | xargs)" + [ "$deps" = "db edge gateway" ] +} + +@test "segmented core-only: core.depends_on is just db" { + echo "core" > "$STACK_DIR/active-profiles" + run bash "$FILES_DIR/generate-compose.sh" + [ "$status" -eq 0 ] + deps="$(generated_json | jq -r 'if (.services.core.depends_on | type) == "array" then .services.core.depends_on[] else (.services.core.depends_on | keys[]) end' | sort | xargs)" + [ "$deps" = "db" ] +} + +@test "full stack: edge's 50051 port is not exposed" { + run bash "$FILES_DIR/generate-compose.sh" + [ "$status" -eq 0 ] + ! generated_json | jq -e '.services.edge.ports[] | select(.published == "50051")' >/dev/null +} + +@test "segmented edge+gateway (no core): edge's 50051 port is exposed" { + printf 'edge gateway\n' > "$STACK_DIR/active-profiles" + run bash "$FILES_DIR/generate-compose.sh" + [ "$status" -eq 0 ] + generated_json | jq -e '.services.edge.ports[] | select(.published == "50051")' >/dev/null +} + +@test "idempotent: second run is a no-op" { + bash "$FILES_DIR/generate-compose.sh" + before="$(cat "$STACK_DIR/docker-compose.yml")" + echo "core" > "$STACK_DIR/active-profiles" + run bash "$FILES_DIR/generate-compose.sh" + [ "$status" -eq 0 ] + [ "$(cat "$STACK_DIR/docker-compose.yml")" = "$before" ] +} + +@test "flag files are removed after a successful run, init dir is kept" { + echo "core" > "$STACK_DIR/active-profiles" + touch "$STACK_DIR/enable-docker-management" + run bash "$FILES_DIR/generate-compose.sh" + [ "$status" -eq 0 ] + [ ! -f "$STACK_DIR/active-profiles" ] + [ ! -f "$STACK_DIR/enable-docker-management" ] + [ -d "$INIT_DIR" ] + [ -f "$INIT_DIR/lib.sh" ] +} + +@test "failed run leaves no docker-compose.yml behind" { + rm -f "$INIT_DIR/docker-compose.template.yaml" + run bash "$FILES_DIR/generate-compose.sh" + [ "$status" -ne 0 ] + [ ! -e "$STACK_DIR/docker-compose.yml" ] + [ ! -e "$STACK_DIR/docker-compose.yml.tmp" ] +} + +@test "manual rm docker-compose.yml + rerun re-provisions" { + run bash "$FILES_DIR/generate-compose.sh" + [ "$status" -eq 0 ] + rm "$STACK_DIR/docker-compose.yml" + echo "core" > "$STACK_DIR/active-profiles" + run bash "$FILES_DIR/generate-compose.sh" + [ "$status" -eq 0 ] + [ "$(generated_services)" = "core db" ] +} + +@test "empty/whitespace active-profiles falls back to full stack" { + printf ' \n' > "$STACK_DIR/active-profiles" + run bash "$FILES_DIR/generate-compose.sh" + [ "$status" -eq 0 ] + [[ "$output" == *"using full all-in-one stack"* ]] + [ "$(generated_services)" = "core db edge gateway" ] +} + +@test "generated file's leading comment lists the resolved profiles" { + printf 'core gateway\n' > "$STACK_DIR/active-profiles" + run bash "$FILES_DIR/generate-compose.sh" + [ "$status" -eq 0 ] + head -n2 "$STACK_DIR/docker-compose.yml" | grep -q '# Selected profiles: core gateway' +} + +@test "image tags stay live \${VAR} refs, not baked-in literal tags" { + run bash "$FILES_DIR/generate-compose.sh" + [ "$status" -eq 0 ] + grep -q 'image: ghcr.io/defguard/defguard:${DEFGUARD_CORE_TAG}' "$STACK_DIR/docker-compose.yml" + grep -q 'image: ghcr.io/defguard/defguard-proxy:${DEFGUARD_PROXY_TAG}' "$STACK_DIR/docker-compose.yml" + grep -q 'image: ghcr.io/defguard/gateway:${DEFGUARD_GATEWAY_TAG}' "$STACK_DIR/docker-compose.yml" + ! grep 'image:' "$STACK_DIR/docker-compose.yml" | grep -q 'test-core\|test-proxy\|test-gateway' +} + +@test "editing .env after generation changes the tag a plain docker compose sees, without regenerating" { + run bash "$FILES_DIR/generate-compose.sh" + [ "$status" -eq 0 ] + write_env new-core-tag new-proxy-tag new-gateway-tag + image="$(unset COMPOSE_PROFILES; docker compose -f "$STACK_DIR/docker-compose.yml" --project-directory "$STACK_DIR" config --format json | jq -r '.services.core.image')" + [ "$image" = "ghcr.io/defguard/defguard:new-core-tag" ] +} diff --git a/ova/tests/generate-env.bats b/ova/tests/generate-env.bats index 2575aea..a854981 100644 --- a/ova/tests/generate-env.bats +++ b/ova/tests/generate-env.bats @@ -50,3 +50,42 @@ teardown() { [[ "$output" == *"DEFGUARD_CORE_TAG is required"* ]] [ ! -f "$STACK_DIR/.env" ] } + +@test "no active-profiles -> full stack -> ADOPT_EDGE/ADOPT_GATEWAY defaulted" { + write_image_tags + bash "$FILES_DIR/generate-env.sh" + grep -qx 'DEFGUARD_ADOPT_EDGE=edge:50051' "$STACK_DIR/.env" + grep -qx 'DEFGUARD_ADOPT_GATEWAY=host.docker.internal:50066' "$STACK_DIR/.env" +} + +@test "active-profiles=core only -> ADOPT_EDGE/ADOPT_GATEWAY left blank" { + echo "core" > "$STACK_DIR/active-profiles" + write_image_tags + bash "$FILES_DIR/generate-env.sh" + grep -qx 'DEFGUARD_ADOPT_EDGE=' "$STACK_DIR/.env" + grep -qx 'DEFGUARD_ADOPT_GATEWAY=' "$STACK_DIR/.env" +} + +@test "active-profiles='core edge gateway' -> ADOPT_EDGE/ADOPT_GATEWAY defaulted" { + printf 'core\nedge\ngateway\n' > "$STACK_DIR/active-profiles" + write_image_tags + bash "$FILES_DIR/generate-env.sh" + grep -qx 'DEFGUARD_ADOPT_EDGE=edge:50051' "$STACK_DIR/.env" + grep -qx 'DEFGUARD_ADOPT_GATEWAY=host.docker.internal:50066' "$STACK_DIR/.env" +} + +@test "active-profiles='core gateway' (no edge) -> ADOPT_EDGE/ADOPT_GATEWAY left blank" { + printf 'core gateway\n' > "$STACK_DIR/active-profiles" + write_image_tags + bash "$FILES_DIR/generate-env.sh" + grep -qx 'DEFGUARD_ADOPT_EDGE=' "$STACK_DIR/.env" + grep -qx 'DEFGUARD_ADOPT_GATEWAY=' "$STACK_DIR/.env" +} + +@test "fails and writes nothing when lib.sh is missing" { + rm -f "$INIT_DIR/lib.sh" + write_image_tags + run bash "$FILES_DIR/generate-env.sh" + [ "$status" -ne 0 ] + [ ! -f "$STACK_DIR/.env" ] +} diff --git a/ova/tests/helpers.bash b/ova/tests/helpers.bash index a92775f..b9d4322 100644 --- a/ova/tests/helpers.bash +++ b/ova/tests/helpers.bash @@ -4,13 +4,15 @@ OVA_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" FILES_DIR="$OVA_DIR/files" STUB_DIR="$OVA_DIR/tests/stub" -# DEFGUARD_STACK_DIR redirects the scripts at this temp dir; compose files are -# copied in so `docker compose config` and real `up` see the actual definitions. +# DEFGUARD_STACK_DIR/DEFGUARD_INIT_DIR redirect the scripts at temp dirs. make_stack() { STACK_DIR="$(mktemp -d)" + INIT_DIR="$STACK_DIR/init" + mkdir -p "$INIT_DIR" export DEFGUARD_STACK_DIR="$STACK_DIR" - cp "$FILES_DIR/docker-compose.yaml" "$STACK_DIR/" - cp "$FILES_DIR/docker-compose.standalone.yaml" "$STACK_DIR/" + export DEFGUARD_INIT_DIR="$INIT_DIR" + cp "$FILES_DIR/docker-compose.template.yaml" "$INIT_DIR/" + cp "$FILES_DIR/lib.sh" "$INIT_DIR/" } teardown_stack() { @@ -20,7 +22,7 @@ teardown_stack() { # Bake image tags as the Packer build does; generate-env.sh sources this. write_image_tags() { - cat > "$STACK_DIR/.image-tags" < "$INIT_DIR/.image-tags" < "$DOCKER_STUB_LOG" -} - -teardown() { - teardown_stack -} - -@test "no active-profiles, no dockge -> all-in-one, profiles unset" { - run bash "$FILES_DIR/start.sh" - [ "$status" -eq 0 ] - [ "$(last_compose_file)" = "docker-compose.yaml" ] - [ "$(last_profiles)" = "" ] -} - -@test "no active-profiles, dockge enabled -> all-in-one, dockge profile" { - touch "$STACK_DIR/enable-docker-management" - run bash "$FILES_DIR/start.sh" - [ "$status" -eq 0 ] - [ "$(last_compose_file)" = "docker-compose.yaml" ] - [ "$(last_profiles)" = "dockge" ] -} - -@test "active-profiles=core -> standalone, core profile" { - echo "core" > "$STACK_DIR/active-profiles" - run bash "$FILES_DIR/start.sh" - [ "$status" -eq 0 ] - [ "$(last_compose_file)" = "docker-compose.standalone.yaml" ] - [ "$(last_profiles)" = "core" ] -} - -@test "active-profiles='core gateway' (space separated) -> core,gateway" { - printf 'core gateway\n' > "$STACK_DIR/active-profiles" - run bash "$FILES_DIR/start.sh" - [ "$status" -eq 0 ] - [ "$(last_compose_file)" = "docker-compose.standalone.yaml" ] - [ "$(last_profiles)" = "core,gateway" ] -} - -@test "active-profiles multiline + dockge -> profiles plus dockge appended" { - printf 'core\nedge\ngateway\n' > "$STACK_DIR/active-profiles" - touch "$STACK_DIR/enable-docker-management" - run bash "$FILES_DIR/start.sh" - [ "$status" -eq 0 ] - [ "$(last_compose_file)" = "docker-compose.standalone.yaml" ] - [ "$(last_profiles)" = "core,edge,gateway,dockge" ] -} - -@test "empty/whitespace active-profiles -> falls back to all-in-one" { - printf ' \n' > "$STACK_DIR/active-profiles" - run bash "$FILES_DIR/start.sh" - [ "$status" -eq 0 ] - [ "$(last_compose_file)" = "docker-compose.yaml" ] - [ "$(last_profiles)" = "" ] -} - -@test "empty active-profiles + dockge -> all-in-one with dockge" { - printf '\n' > "$STACK_DIR/active-profiles" - touch "$STACK_DIR/enable-docker-management" - run bash "$FILES_DIR/start.sh" - [ "$status" -eq 0 ] - [ "$(last_compose_file)" = "docker-compose.yaml" ] - [ "$(last_profiles)" = "dockge" ] -} diff --git a/ova/tests/stub/docker b/ova/tests/stub/docker deleted file mode 100755 index 2549f92..0000000 --- a/ova/tests/stub/docker +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -# Records `docker compose` invocations instead of running them, so start.sh's -# profile selection can be asserted without a daemon. -log="${DOCKER_STUB_LOG:?DOCKER_STUB_LOG must be set}" -{ - printf 'compose_profiles=%s\n' "${COMPOSE_PROFILES-}" - printf 'args=%s\n' "$*" -} >> "$log" -exit 0