fix: deflake haproxy bats tests

The byjg/easy-haproxy image polls Docker for label changes every 10
seconds by default, which races with the haproxy bats suite and
intermittently produces curl exit 7. Expose `refresh-conf` as a
global-only haproxy property that maps to `EASYHAPROXY_REFRESH_CONF`,
lower it to 2 seconds in the bats setup, and wrap the localhost HTTP
assertions in a retry loop so checks wait for haproxy to converge
rather than failing on the first attempt.
This commit is contained in:
Jose Diaz-Gonzalez
2026-04-30 15:03:40 -04:00
parent 8f8a23aac3
commit 53ef8c7780
7 changed files with 81 additions and 13 deletions

View File

@@ -113,6 +113,16 @@ dokku haproxy:set --global log-level DEBUG
After modifying, the Haproxy container will need to be restarted.
### Changing the Haproxy refresh interval
Haproxy polls the Docker API for label changes every `10` seconds by default. The interval may be changed by setting the `refresh-conf` property with the `--global` flag:
```shell
dokku haproxy:set --global refresh-conf 5
```
The `refresh-conf` property is global-only and cannot be set on a per-app basis. Setting an empty value will reset it to the default. After modifying, the Haproxy container will need to be restarted.
### Label Management
The Haproxy plugin allows you to add custom container labels to apps. These labels are injected into containers during deployment and can be used to configure Haproxy behavior beyond what the plugin provides by default.

View File

@@ -72,6 +72,7 @@ cmd-haproxy-report-single() {
"--haproxy-letsencrypt-email: $(fn-haproxy-letsencrypt-email)"
"--haproxy-letsencrypt-server: $(fn-haproxy-letsencrypt-server)"
"--haproxy-log-level: $(fn-haproxy-log-level)"
"--haproxy-refresh-conf: $(fn-haproxy-refresh-conf)"
)
fn-report-validate-format "$FORMAT" "$INFO_FLAG"

View File

@@ -38,7 +38,8 @@ fn-haproxy-template-compose-file() {
local SIGIL_PARAMS=(HAPROXY_IMAGE="$(fn-haproxy-image)"
HAPROXY_LOG_LEVEL="$(fn-haproxy-log-level)"
HAPROXY_LETSENCRYPT_EMAIL="$(fn-haproxy-letsencrypt-email)"
HAPROXY_LETSENCRYPT_SERVER="$(fn-haproxy-letsencrypt-server)")
HAPROXY_LETSENCRYPT_SERVER="$(fn-haproxy-letsencrypt-server)"
HAPROXY_REFRESH_CONF="$(fn-haproxy-refresh-conf)")
sigil -f "$COMPOSE_TEMPLATE" "${SIGIL_PARAMS[@]}" | cat -s >"$OUTPUT_PATH"
}
@@ -60,3 +61,7 @@ fn-haproxy-letsencrypt-email() {
fn-haproxy-letsencrypt-server() {
fn-plugin-property-get-default "haproxy" "--global" "letsencrypt-server" "https://acme-v02.api.letsencrypt.org/directory"
}
fn-haproxy-refresh-conf() {
fn-plugin-property-get-default "haproxy" "--global" "refresh-conf" "10"
}

View File

@@ -9,13 +9,14 @@ cmd-haproxy-set() {
declare cmd="haproxy:set"
[[ "$1" == "$cmd" ]] && shift 1
declare APP="$1" KEY="$2" VALUE="$3"
local VALID_KEYS=("image" "log-level" "letsencrypt-email" "letsencrypt-server")
local GLOBAL_KEYS=("image" "log-level" "letsencrypt-email" "letsencrypt-server")
local VALID_KEYS=("image" "log-level" "letsencrypt-email" "letsencrypt-server" "refresh-conf")
local GLOBAL_KEYS=("image" "log-level" "letsencrypt-email" "letsencrypt-server" "refresh-conf")
local GLOBAL_ONLY_KEYS=("refresh-conf")
[[ -z "$KEY" ]] && dokku_log_fail "No key specified"
if ! fn-in-array "$KEY" "${VALID_KEYS[@]}"; then
dokku_log_fail "Invalid key specified, valid keys include: image log-level letsencrypt-email letsencrypt-server"
dokku_log_fail "Invalid key specified, valid keys include: image log-level letsencrypt-email letsencrypt-server refresh-conf"
fi
if ! fn-in-array "$KEY" "${GLOBAL_KEYS[@]}"; then
@@ -25,6 +26,10 @@ cmd-haproxy-set() {
verify_app_name "$APP"
fi
if fn-in-array "$KEY" "${GLOBAL_ONLY_KEYS[@]}" && [[ "$APP" != "--global" ]]; then
dokku_log_fail "The key '$KEY' can only be set globally"
fi
if [[ -n "$VALUE" ]]; then
dokku_log_info2_quiet "Setting ${KEY} to ${VALUE}"
fn-plugin-property-write "haproxy" "$APP" "$KEY" "$VALUE"

View File

@@ -7,6 +7,7 @@ services:
- EASYHAPROXY_DISCOVER=docker
- EASYHAPROXY_LABEL_PREFIX=haproxy
- EASYHAPROXY_LOG_LEVEL={{ $.HAPROXY_LOG_LEVEL }}
- EASYHAPROXY_REFRESH_CONF={{ $.HAPROXY_REFRESH_CONF }}
- CERTBOT_LOG_LEVEL={{ $.HAPROXY_LOG_LEVEL }}
- HAPROXY_LOG_LEVEL={{ $.HAPROXY_LOG_LEVEL }}
{{ if $.HAPROXY_LETSENCRYPT_EMAIL }}

View File

@@ -7,6 +7,7 @@ setup() {
dokku nginx:stop
dokku haproxy:set --global letsencrypt-server https://acme-staging-v02.api.letsencrypt.org/directory
dokku haproxy:set --global letsencrypt-email
dokku haproxy:set --global refresh-conf 2
dokku haproxy:start
create_app
}
@@ -15,6 +16,7 @@ teardown() {
global_teardown
destroy_app
dokku haproxy:stop
dokku haproxy:set --global refresh-conf
dokku nginx:start
}
@@ -32,6 +34,42 @@ teardown() {
assert_output "$help_output"
}
@test "(haproxy) refresh-conf" {
run /bin/bash -c "dokku haproxy:set $TEST_APP refresh-conf 2"
echo "output: $output"
echo "status: $status"
assert_failure
assert_output_contains "can only be set globally"
run /bin/bash -c "dokku haproxy:set --global refresh-conf 5"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku haproxy:report --global --haproxy-refresh-conf"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "5"
run /bin/bash -c "dokku haproxy:show-config"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "EASYHAPROXY_REFRESH_CONF=5"
run /bin/bash -c "dokku haproxy:set --global refresh-conf"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku haproxy:report --global --haproxy-refresh-conf"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "10"
}
@test "(haproxy) log-level" {
run /bin/bash -c "dokku haproxy:set --global log-level DEBUG"
echo "output: $output"
@@ -60,11 +98,7 @@ teardown() {
echo "status: $status"
assert_success
run /bin/bash -c "curl --silent $(dokku url $TEST_APP)"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "python/http.server"
assert_http_localhost_response_contains "http" "$TEST_APP.$DOKKU_DOMAIN" "80" "/" "python/http.server"
}
@test "(haproxy) multiple domains" {
@@ -88,8 +122,6 @@ teardown() {
echo "status: $status"
assert_success
sleep 5
run /bin/bash -c "dokku logs $TEST_APP"
echo "output: $output"
echo "status: $status"

View File

@@ -249,10 +249,17 @@ assert_http_success() {
assert_http_localhost_response() {
local scheme="$1" domain="$2" port="${3:-80}" path="${4:-}" content="${5:-}" status_code="${6:-200}"
run curl --connect-to "$domain:$port:localhost:$port" -kSso /dev/null -w "%{http_code}" "$scheme://$domain:$port$path"
local retries="${HTTP_ASSERT_RETRIES:-30}" attempt=1
while [[ "$attempt" -lt "$retries" ]]; do
run curl --connect-to "$domain:$port:localhost:$port" -kSso /dev/null -w "%{http_code}" "$scheme://$domain:$port$path"
[[ "$output" == "$status_code" ]] && break
sleep 1
attempt=$((attempt + 1))
done
echo "curl: curl --connect-to $domain:$port:localhost:$port -kSso /dev/null -w %{http_code} $scheme://$domain:$port$path"
echo "output: $output"
echo "status: $status"
echo "attempts: $attempt"
assert_output "$status_code"
if [[ -n "$content" ]]; then
@@ -265,10 +272,17 @@ assert_http_localhost_response() {
assert_http_localhost_response_contains() {
local scheme="$1" domain="$2" port="${3:-80}" path="${4:-}" content="${5:-}" status_code="${6:-200}"
run curl --connect-to "$domain:$port:localhost:$port" -kSso /dev/null -w "%{http_code}" "$scheme://$domain:$port$path"
local retries="${HTTP_ASSERT_RETRIES:-30}" attempt=1
while [[ "$attempt" -lt "$retries" ]]; do
run curl --connect-to "$domain:$port:localhost:$port" -kSso /dev/null -w "%{http_code}" "$scheme://$domain:$port$path"
[[ "$output" == "$status_code" ]] && break
sleep 1
attempt=$((attempt + 1))
done
echo "curl: curl --connect-to $domain:$port:localhost:$port -kSso /dev/null -w %{http_code} $scheme://$domain:$port$path"
echo "output: $output"
echo "status: $status"
echo "attempts: $attempt"
assert_output "$status_code"
if [[ -n "$content" ]]; then