From d8eb3f4ff1885301a40e789aa9dac56b268f51f6 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:31:44 +0200 Subject: [PATCH 1/8] simplify ova 1 --- .github/workflows/test-ova.yml | 10 +- ova/defguard.pkr.hcl | 32 ++--- ova/files/defguard-init.service | 5 +- ...lone.yaml => docker-compose.template.yaml} | 8 +- ova/files/docker-compose.yaml | 60 --------- ova/files/docker-setup.sh | 3 +- ova/files/generate-compose.sh | 55 ++++++++ ova/files/generate-env.sh | 22 +++- ova/files/lib.sh | 46 +++++++ ova/files/start.sh | 54 -------- ova/test/test-deployment-modes.sh | 21 +-- ova/tests/config.bats | 49 ------- ova/tests/generate-compose.bats | 121 ++++++++++++++++++ ova/tests/generate-env.bats | 31 +++++ ova/tests/helpers.bash | 22 ++-- ova/tests/integration.bats | 19 +-- ova/tests/start.bats | 73 ----------- ova/tests/stub/docker | 9 -- 18 files changed, 338 insertions(+), 302 deletions(-) rename ova/files/{docker-compose.standalone.yaml => docker-compose.template.yaml} (77%) delete mode 100644 ova/files/docker-compose.yaml create mode 100755 ova/files/generate-compose.sh create mode 100644 ova/files/lib.sh delete mode 100644 ova/files/start.sh delete mode 100644 ova/tests/config.bats create mode 100644 ova/tests/generate-compose.bats delete mode 100644 ova/tests/start.bats delete mode 100755 ova/tests/stub/docker 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..32cdea9 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" { @@ -116,15 +116,15 @@ build { 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/defguard/init", + "sudo mv /tmp/docker-compose.template.yaml /opt/defguard/init/docker-compose.template.yaml", + "sudo mv /tmp/lib.sh /opt/defguard/init/lib.sh", + "sudo mv /tmp/generate-env.sh /opt/defguard/init/generate-env.sh", + "sudo mv /tmp/generate-compose.sh /opt/defguard/init/generate-compose.sh", + "sudo chmod +x /opt/defguard/init/generate-env.sh /opt/defguard/init/generate-compose.sh", + "echo 'DEFGUARD_CORE_TAG=${var.core_tag}' | sudo tee /opt/defguard/init/.image-tags > /dev/null", + "echo 'DEFGUARD_PROXY_TAG=${var.proxy_tag}' | sudo tee -a /opt/defguard/init/.image-tags > /dev/null", + "echo 'DEFGUARD_GATEWAY_TAG=${var.gateway_tag}' | sudo tee -a /opt/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 +139,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/defguard", + "sudo chmod -R go-w /opt/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..967ca5f 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/defguard/init/generate-env.sh +ExecStart=/bin/bash /opt/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 77% rename from ova/files/docker-compose.standalone.yaml rename to ova/files/docker-compose.template.yaml index 3559385..bd8a7ed 100644 --- a/ova/files/docker-compose.standalone.yaml +++ b/ova/files/docker-compose.template.yaml @@ -7,6 +7,10 @@ services: 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: @@ -20,9 +24,11 @@ services: - ./.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 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..5386511 --- /dev/null +++ b/ova/files/generate-compose.sh @@ -0,0 +1,55 @@ +#!/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:-/opt/defguard/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. Requesting JSON output +# and stripping the key with jq avoids that; JSON is valid YAML, so the result +# is written directly as the final docker-compose.yml. +{ + echo "# Generated by defguard OVA first-boot init on $(date -u +%FT%TZ)." + echo "# Selected profiles: ${_profiles[*]}" + docker compose -f "$TEMPLATE" --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 + ' +} > "$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..1a75eb8 100644 --- a/ova/files/generate-env.sh +++ b/ova/files/generate-env.sh @@ -3,6 +3,7 @@ # If .env already exists (e.g. provided via cloud-init), this script does nothing. STACK_DIR="${DEFGUARD_STACK_DIR:-/opt/stacks/defguard}" +INIT_DIR="${DEFGUARD_INIT_DIR:-/opt/defguard/init}" ENV_FILE="$STACK_DIR/.env" if [ -f "$ENV_FILE" ]; then @@ -12,16 +13,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..e787d3b 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,12 @@ wait_services() { } verify_mode() { - local mode="$1" ip="$2" profile="${PROFILE[$mode]}" names actual + local mode="$1" ip="$2" 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 + # active-profiles is consumed and deleted by generate-compose.sh once + # docker-compose.yml is materialized + vm_ssh "$ip" "test ! -e /opt/stacks/defguard/active-profiles" \ + || { log "$mode: active-profiles unexpectedly present (should be consumed and deleted)"; return 1; } names="$(wait_services "$ip" "${EXPECT[$mode]}")" \ || { log "$mode: expected services did not all start; running: $(tr '\n' ' ' <<<"$names")"; return 1; } @@ -125,6 +121,11 @@ verify_mode() { has_service "$names" "$svc" \ && { log "$mode: unexpected service '$svc' is running"; return 1; } done + + actual="$(vm_ssh "$ip" "ls -A /opt/stacks/defguard" | sort | tr '\n' ' ' | sed 's/ $//')" + [ "$actual" = ".env .volumes docker-compose.yml" ] \ + || { log "$mode: /opt/stacks/defguard contains '$actual', expected only docker-compose.yml, .env, .volumes"; return 1; } + 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..5571d17 --- /dev/null +++ b/ova/tests/generate-compose.bats @@ -0,0 +1,121 @@ +#!/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" { + 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" ] +} + +@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' +} diff --git a/ova/tests/generate-env.bats b/ova/tests/generate-env.bats index 2575aea..ecc306d 100644 --- a/ova/tests/generate-env.bats +++ b/ova/tests/generate-env.bats @@ -50,3 +50,34 @@ 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" +} diff --git a/ova/tests/helpers.bash b/ova/tests/helpers.bash index a92775f..1186dc1 100644 --- a/ova/tests/helpers.bash +++ b/ova/tests/helpers.bash @@ -4,23 +4,27 @@ 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. +# STACK_DIR starts genuinely empty (matching real first-boot state); INIT_DIR +# gets a copy of the generator machinery so tests never write into the repo. make_stack() { STACK_DIR="$(mktemp -d)" + INIT_DIR="$(mktemp -d)" 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() { [ -n "${STACK_DIR:-}" ] && rm -rf "$STACK_DIR" + [ -n "${INIT_DIR:-}" ] && rm -rf "$INIT_DIR" return 0 } # 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 From 8d5cc1644f42ece957ac3153c274c09e8810b785 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:34:16 +0200 Subject: [PATCH 2/8] trigger test builds --- .github/workflows/build-ova.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build-ova.yml b/.github/workflows/build-ova.yml index 3ea8e43..a45f2f7 100644 --- a/.github/workflows/build-ova.yml +++ b/.github/workflows/build-ova.yml @@ -1,6 +1,12 @@ name: Build OVF Image on: + push: + branches: + - simplify-ova + paths: + - "ova/**" + - ".github/workflows/build-ova.yml" workflow_dispatch: inputs: core_tag: From ce28d1dc34b40cb732f606b36b1a26d5e6c4aa7d Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:26:06 +0200 Subject: [PATCH 3/8] attempt 2 --- ova/defguard.pkr.hcl | 23 +++++++++++------------ ova/files/defguard-init.service | 4 ++-- ova/files/generate-compose.sh | 15 +++++++++------ ova/files/generate-env.sh | 2 +- ova/tests/generate-compose.bats | 3 ++- ova/tests/helpers.bash | 6 ++---- 6 files changed, 27 insertions(+), 26 deletions(-) diff --git a/ova/defguard.pkr.hcl b/ova/defguard.pkr.hcl index 32cdea9..46fb548 100644 --- a/ova/defguard.pkr.hcl +++ b/ova/defguard.pkr.hcl @@ -115,16 +115,15 @@ build { provisioner "shell" { inline = [ "sudo bash /tmp/docker-setup.sh", - "sudo mkdir -p /opt/stacks/defguard", - "sudo mkdir -p /opt/defguard/init", - "sudo mv /tmp/docker-compose.template.yaml /opt/defguard/init/docker-compose.template.yaml", - "sudo mv /tmp/lib.sh /opt/defguard/init/lib.sh", - "sudo mv /tmp/generate-env.sh /opt/defguard/init/generate-env.sh", - "sudo mv /tmp/generate-compose.sh /opt/defguard/init/generate-compose.sh", - "sudo chmod +x /opt/defguard/init/generate-env.sh /opt/defguard/init/generate-compose.sh", - "echo 'DEFGUARD_CORE_TAG=${var.core_tag}' | sudo tee /opt/defguard/init/.image-tags > /dev/null", - "echo 'DEFGUARD_PROXY_TAG=${var.proxy_tag}' | sudo tee -a /opt/defguard/init/.image-tags > /dev/null", - "echo 'DEFGUARD_GATEWAY_TAG=${var.gateway_tag}' | sudo tee -a /opt/defguard/init/.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,8 +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/defguard", - "sudo chmod -R go-w /opt/defguard/init", + "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 967ca5f..5b9c939 100644 --- a/ova/files/defguard-init.service +++ b/ova/files/defguard-init.service @@ -8,8 +8,8 @@ Type=oneshot WorkingDirectory=/opt/stacks/defguard StandardOutput=append:/var/log/defguard-startup.log StandardError=append:/var/log/defguard-startup.log -ExecStart=/bin/bash /opt/defguard/init/generate-env.sh -ExecStart=/bin/bash /opt/defguard/init/generate-compose.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] diff --git a/ova/files/generate-compose.sh b/ova/files/generate-compose.sh index 5386511..a43d839 100755 --- a/ova/files/generate-compose.sh +++ b/ova/files/generate-compose.sh @@ -6,7 +6,7 @@ set -euo pipefail STACK_DIR="${DEFGUARD_STACK_DIR:-/opt/stacks/defguard}" -INIT_DIR="${DEFGUARD_INIT_DIR:-/opt/defguard/init}" +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" @@ -33,13 +33,14 @@ 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. Requesting JSON output -# and stripping the key with jq avoids that; JSON is valid YAML, so the result -# is written directly as the final docker-compose.yml. +# `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. { echo "# Generated by defguard OVA first-boot init on $(date -u +%FT%TZ)." echo "# Selected profiles: ${_profiles[*]}" - docker compose -f "$TEMPLATE" --project-directory "$STACK_DIR" --env-file "$STACK_DIR/.env" "${_profile_args[@]}" \ + 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) @@ -48,8 +49,10 @@ for p in "${_profiles[@]}"; do _profile_args+=(--profile "$p"); done else (.services.edge // empty) |= (.ports += ["50051:50051"]) end - ' + ' \ + | docker compose -f - -p defguard --project-directory "$STACK_DIR" config } > "$COMPOSE_FILE" rm -f "$PROFILES_FILE" "$ENABLE_DOCKER_MGMT_FILE" +rm -rf "$INIT_DIR" echo "DefGuard: generated $COMPOSE_FILE for profiles: ${_profiles[*]}" diff --git a/ova/files/generate-env.sh b/ova/files/generate-env.sh index 1a75eb8..5ea0937 100644 --- a/ova/files/generate-env.sh +++ b/ova/files/generate-env.sh @@ -3,7 +3,7 @@ # If .env already exists (e.g. provided via cloud-init), this script does nothing. STACK_DIR="${DEFGUARD_STACK_DIR:-/opt/stacks/defguard}" -INIT_DIR="${DEFGUARD_INIT_DIR:-/opt/defguard/init}" +INIT_DIR="${DEFGUARD_INIT_DIR:-$STACK_DIR/init}" ENV_FILE="$STACK_DIR/.env" if [ -f "$ENV_FILE" ]; then diff --git a/ova/tests/generate-compose.bats b/ova/tests/generate-compose.bats index 5571d17..be0bb8f 100644 --- a/ova/tests/generate-compose.bats +++ b/ova/tests/generate-compose.bats @@ -96,13 +96,14 @@ generated_json() { [ "$(cat "$STACK_DIR/docker-compose.yml")" = "$before" ] } -@test "flag files are removed after a successful run" { +@test "flag files and the init dir are removed after a successful run" { 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" ] } @test "empty/whitespace active-profiles falls back to full stack" { diff --git a/ova/tests/helpers.bash b/ova/tests/helpers.bash index 1186dc1..b9d4322 100644 --- a/ova/tests/helpers.bash +++ b/ova/tests/helpers.bash @@ -5,11 +5,10 @@ FILES_DIR="$OVA_DIR/files" STUB_DIR="$OVA_DIR/tests/stub" # DEFGUARD_STACK_DIR/DEFGUARD_INIT_DIR redirect the scripts at temp dirs. -# STACK_DIR starts genuinely empty (matching real first-boot state); INIT_DIR -# gets a copy of the generator machinery so tests never write into the repo. make_stack() { STACK_DIR="$(mktemp -d)" - INIT_DIR="$(mktemp -d)" + INIT_DIR="$STACK_DIR/init" + mkdir -p "$INIT_DIR" export DEFGUARD_STACK_DIR="$STACK_DIR" export DEFGUARD_INIT_DIR="$INIT_DIR" cp "$FILES_DIR/docker-compose.template.yaml" "$INIT_DIR/" @@ -18,7 +17,6 @@ make_stack() { teardown_stack() { [ -n "${STACK_DIR:-}" ] && rm -rf "$STACK_DIR" - [ -n "${INIT_DIR:-}" ] && rm -rf "$INIT_DIR" return 0 } From a65289af42ef39cb09d05fcadb56c7611f050395 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Fri, 17 Jul 2026 10:30:48 +0200 Subject: [PATCH 4/8] minor tweaks --- ova/files/generate-compose.sh | 6 +++-- ova/files/generate-env.sh | 1 + ova/test/test-deployment-modes.sh | 37 +++++++++++++++++++++++++++++-- ova/tests/generate-compose.bats | 23 +++++++++++++++++-- ova/tests/generate-env.bats | 8 +++++++ 5 files changed, 69 insertions(+), 6 deletions(-) diff --git a/ova/files/generate-compose.sh b/ova/files/generate-compose.sh index a43d839..e11c87e 100755 --- a/ova/files/generate-compose.sh +++ b/ova/files/generate-compose.sh @@ -37,6 +37,8 @@ for p in "${_profiles[@]}"; do _profile_args+=(--profile "$p"); done # 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[*]}" @@ -51,8 +53,8 @@ for p in "${_profiles[@]}"; do _profile_args+=(--profile "$p"); done end ' \ | docker compose -f - -p defguard --project-directory "$STACK_DIR" config -} > "$COMPOSE_FILE" +} > "$TMP_COMPOSE_FILE" +mv "$TMP_COMPOSE_FILE" "$COMPOSE_FILE" rm -f "$PROFILES_FILE" "$ENABLE_DOCKER_MGMT_FILE" -rm -rf "$INIT_DIR" echo "DefGuard: generated $COMPOSE_FILE for profiles: ${_profiles[*]}" diff --git a/ova/files/generate-env.sh b/ova/files/generate-env.sh index 5ea0937..581fe30 100644 --- a/ova/files/generate-env.sh +++ b/ova/files/generate-env.sh @@ -1,6 +1,7 @@ #!/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}" diff --git a/ova/test/test-deployment-modes.sh b/ova/test/test-deployment-modes.sh index e787d3b..0f3c7a5 100755 --- a/ova/test/test-deployment-modes.sh +++ b/ova/test/test-deployment-modes.sh @@ -123,8 +123,41 @@ verify_mode() { done actual="$(vm_ssh "$ip" "ls -A /opt/stacks/defguard" | sort | tr '\n' ' ' | sed 's/ $//')" - [ "$actual" = ".env .volumes docker-compose.yml" ] \ - || { log "$mode: /opt/stacks/defguard contains '$actual', expected only docker-compose.yml, .env, .volumes"; return 1; } + [ "$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/generate-compose.bats b/ova/tests/generate-compose.bats index be0bb8f..375a7b7 100644 --- a/ova/tests/generate-compose.bats +++ b/ova/tests/generate-compose.bats @@ -96,14 +96,33 @@ generated_json() { [ "$(cat "$STACK_DIR/docker-compose.yml")" = "$before" ] } -@test "flag files and the init dir are removed after a successful run" { +@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" ] + [ -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" { diff --git a/ova/tests/generate-env.bats b/ova/tests/generate-env.bats index ecc306d..a854981 100644 --- a/ova/tests/generate-env.bats +++ b/ova/tests/generate-env.bats @@ -81,3 +81,11 @@ teardown() { 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" ] +} From 4bbb792231d198ae6d9aaed83c3aa2ab671bf014 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Fri, 17 Jul 2026 12:16:02 +0200 Subject: [PATCH 5/8] run e2e tests --- .github/workflows/build-ova.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build-ova.yml b/.github/workflows/build-ova.yml index a45f2f7..c74bbd1 100644 --- a/.github/workflows/build-ova.yml +++ b/.github/workflows/build-ova.yml @@ -98,7 +98,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 }} From 5766ecff0a597b316baeaed1cafb946763144bc0 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Fri, 17 Jul 2026 12:51:36 +0200 Subject: [PATCH 6/8] fix e2e tests --- ova/test/test-deployment-modes.sh | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/ova/test/test-deployment-modes.sh b/ova/test/test-deployment-modes.sh index 0f3c7a5..66ba006 100755 --- a/ova/test/test-deployment-modes.sh +++ b/ova/test/test-deployment-modes.sh @@ -108,11 +108,6 @@ wait_services() { verify_mode() { local mode="$1" ip="$2" names actual - # active-profiles is consumed and deleted by generate-compose.sh once - # docker-compose.yml is materialized - vm_ssh "$ip" "test ! -e /opt/stacks/defguard/active-profiles" \ - || { log "$mode: active-profiles unexpectedly present (should be consumed and deleted)"; return 1; } - names="$(wait_services "$ip" "${EXPECT[$mode]}")" \ || { log "$mode: expected services did not all start; running: $(tr '\n' ' ' <<<"$names")"; return 1; } @@ -122,7 +117,10 @@ verify_mode() { && { log "$mode: unexpected service '$svc' is running"; return 1; } done - actual="$(vm_ssh "$ip" "ls -A /opt/stacks/defguard" | sort | tr '\n' ' ' | sed 's/ $//')" + 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; } From f0a29f8fde36dc5dfb4e61bbd89ddad498d75101 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:33:20 +0200 Subject: [PATCH 7/8] cleanup --- .github/workflows/build-ova.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/build-ova.yml b/.github/workflows/build-ova.yml index c74bbd1..3e3afa9 100644 --- a/.github/workflows/build-ova.yml +++ b/.github/workflows/build-ova.yml @@ -1,12 +1,6 @@ name: Build OVF Image on: - push: - branches: - - simplify-ova - paths: - - "ova/**" - - ".github/workflows/build-ova.yml" workflow_dispatch: inputs: core_tag: From a9b4074667c2189c91b69ada42f6588351867fc4 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Mon, 20 Jul 2026 09:58:58 +0200 Subject: [PATCH 8/8] dont bake tags into compose --- ova/files/docker-compose.template.yaml | 6 +++--- ova/files/generate-compose.sh | 7 +++++++ ova/tests/generate-compose.bats | 17 +++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/ova/files/docker-compose.template.yaml b/ova/files/docker-compose.template.yaml index bd8a7ed..052e442 100644 --- a/ova/files/docker-compose.template.yaml +++ b/ova/files/docker-compose.template.yaml @@ -2,7 +2,7 @@ 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 @@ -19,7 +19,7 @@ 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: @@ -33,7 +33,7 @@ services: 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/generate-compose.sh b/ova/files/generate-compose.sh index e11c87e..189f8da 100755 --- a/ova/files/generate-compose.sh +++ b/ova/files/generate-compose.sh @@ -54,6 +54,13 @@ trap 'rm -f "$TMP_COMPOSE_FILE"' EXIT ' \ | 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" diff --git a/ova/tests/generate-compose.bats b/ova/tests/generate-compose.bats index 375a7b7..4a3acae 100644 --- a/ova/tests/generate-compose.bats +++ b/ova/tests/generate-compose.bats @@ -139,3 +139,20 @@ generated_json() { [ "$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" ] +}