add test url
This commit is contained in:
1 parent
4d407a963f
commit
9b4388d142
2 files changed
+149
-1
No files matched your search
@@ -34,6 +34,10 @@ DROPPED_LABEL_PREFIXES = (
|
||||
"com.centurylinklabs.watchtower.",
|
||||
"sablier.",
|
||||
)
|
||||
# Cible du test HTTP, déduite des labels Traefik avant leur suppression.
|
||||
HTTP_TARGET_LABEL = "tips-of-mine.ci.http"
|
||||
HTTP_PORT_RE = re.compile(r"^traefik\.http\.services\.([^.]+)\.loadbalancer\.server\.port$")
|
||||
HTTP_SCHEME_RE = re.compile(r"^traefik\.http\.services\.([^.]+)\.loadbalancer\.server\.scheme$")
|
||||
CI_LABELS = {
|
||||
"traefik.enable": "false",
|
||||
"com.centurylinklabs.watchtower.enable": "false",
|
||||
@@ -268,12 +272,38 @@ def relocate_source(source, settings, create_missing, owner=None):
|
||||
return posixpath.join(settings["host_dir"], rel)
|
||||
|
||||
|
||||
def http_target(labels):
|
||||
"""Cibles HTTP d'un service exposé par Traefik : "http:8200,https:8443", "auto" ou None."""
|
||||
if not any(key.startswith("traefik.http.") for key in labels):
|
||||
return None
|
||||
if str(labels.get("traefik.enable", "")).strip().lower() == "false":
|
||||
return None
|
||||
ports, schemes = {}, {}
|
||||
for key, value in labels.items():
|
||||
match = HTTP_PORT_RE.match(key)
|
||||
if match:
|
||||
ports[match.group(1)] = str(value).strip()
|
||||
match = HTTP_SCHEME_RE.match(key)
|
||||
if match:
|
||||
schemes[match.group(1)] = str(value).strip().lower()
|
||||
targets = set()
|
||||
for name, port in ports.items():
|
||||
if port.isdigit():
|
||||
scheme = "https" if schemes.get(name) == "https" else "http"
|
||||
targets.add(f"{scheme}:{port}")
|
||||
return ",".join(sorted(targets)) if targets else "auto"
|
||||
|
||||
|
||||
def rewrite_labels(labels, project):
|
||||
if isinstance(labels, list):
|
||||
labels = dict(item.split("=", 1) if "=" in item else (item, "") for item in labels)
|
||||
kept = {k: v for k, v in (labels or {}).items() if not k.startswith(DROPPED_LABEL_PREFIXES)}
|
||||
labels = labels or {}
|
||||
target = http_target(labels)
|
||||
kept = {k: v for k, v in labels.items() if not k.startswith(DROPPED_LABEL_PREFIXES)}
|
||||
kept.update(CI_LABELS)
|
||||
kept["tips-of-mine.ci.project"] = project
|
||||
if target:
|
||||
kept[HTTP_TARGET_LABEL] = target
|
||||
return kept
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ readonly CI_MARKER_LABEL="tips-of-mine.ci"
|
||||
readonly STABILIZATION_SECONDS=15
|
||||
readonly POLL_SECONDS=5
|
||||
readonly WAIT_TIMEOUT_SECONDS=300
|
||||
readonly HTTP_TIMEOUT_SECONDS=300
|
||||
readonly HTTP_TARGET_LABEL="tips-of-mine.ci.http"
|
||||
readonly MIN_AVAILABLE_MB=3072
|
||||
readonly LOG_LINES=50
|
||||
|
||||
@@ -27,9 +29,51 @@ readonly WORK_DIR="${RUNNER_TEMP:-/tmp}/compose-ci"
|
||||
readonly CI_FILE="${WORK_DIR}/compose.ci.json"
|
||||
readonly COUNT_FILE="${WORK_DIR}/services.count"
|
||||
readonly MASK_FILE="${WORK_DIR}/mask.b64"
|
||||
readonly STARTED_FILE="${WORK_DIR}/started.epoch"
|
||||
readonly SENSITIVE_NAME_RE='PASS|PWD|SECRET|TOKEN|KEY|CREDENTIAL|PRIVATE|AUTH|DSN|URI|URL|SALT|COOKIE'
|
||||
readonly MASK_MIN_LENGTH=6
|
||||
|
||||
# Interroge une URL jusqu'à obtenir une réponse HTTP < 500 ou jusqu'à l'échéance.
|
||||
# Les redirections ne sont pas suivies : une réponse 3xx prouve que l'application répond.
|
||||
PROBE_PY="$(cat <<'PY'
|
||||
import os, ssl, sys, time, urllib.error, urllib.request
|
||||
|
||||
url = os.environ["PROBE_URL"]
|
||||
start = float(os.environ["PROBE_START"])
|
||||
deadline = float(os.environ["PROBE_DEADLINE"])
|
||||
|
||||
|
||||
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
||||
def redirect_request(self, *args, **kwargs):
|
||||
return None
|
||||
|
||||
|
||||
opener = urllib.request.build_opener(
|
||||
NoRedirect, urllib.request.HTTPSHandler(context=ssl._create_unverified_context())
|
||||
)
|
||||
last = "aucune réponse"
|
||||
while True:
|
||||
code = None
|
||||
try:
|
||||
with opener.open(url, timeout=5) as response:
|
||||
code = response.status
|
||||
except urllib.error.HTTPError as error:
|
||||
code = error.code
|
||||
except Exception as error:
|
||||
last = str(getattr(error, "reason", "") or type(error).__name__)
|
||||
if code is not None:
|
||||
if code < 500:
|
||||
print(f"HTTP {code} disponible en {time.time() - start:.0f} s")
|
||||
sys.exit(0)
|
||||
last = f"HTTP {code}"
|
||||
if time.time() >= deadline:
|
||||
print(f"ÉCHEC ({last}) indisponible après {time.time() - start:.0f} s")
|
||||
sys.exit(1)
|
||||
time.sleep(2)
|
||||
PY
|
||||
)"
|
||||
readonly PROBE_PY
|
||||
|
||||
PROJECT=""
|
||||
SERVICE_COUNT=0
|
||||
ENV_FILE=""
|
||||
@@ -108,6 +152,13 @@ helper() {
|
||||
docker run --rm --network none "$@"
|
||||
}
|
||||
|
||||
helper_on() {
|
||||
local network="$1"
|
||||
shift
|
||||
[[ "$network" == "${PROJECT}_"* ]] || die "réseau hors du projet de CI refusé : ${network}"
|
||||
docker run --rm --network "$network" "$@"
|
||||
}
|
||||
|
||||
load_state() {
|
||||
PROJECT="$(project_name)"
|
||||
[[ -f "$COUNT_FILE" ]] \
|
||||
@@ -209,6 +260,7 @@ cmd_up() {
|
||||
return 0
|
||||
fi
|
||||
compose up --detach --build --quiet-pull
|
||||
date +%s > "$STARTED_FILE"
|
||||
}
|
||||
|
||||
evaluate_state() {
|
||||
@@ -248,6 +300,71 @@ evaluate_state() {
|
||||
done
|
||||
}
|
||||
|
||||
single_exposed_port() {
|
||||
local id="$1" entry
|
||||
local -a ports=()
|
||||
for entry in $(docker inspect --format '{{range $p, $v := .Config.ExposedPorts}}{{$p}} {{end}}' "$id"); do
|
||||
[[ "$entry" == */tcp ]] && ports+=("${entry%/tcp}")
|
||||
done
|
||||
(( ${#ports[@]} == 1 )) && printf '%s' "${ports[0]}"
|
||||
}
|
||||
|
||||
probe() {
|
||||
local network="$1" url="$2" started="$3" result status=0
|
||||
result="$(helper_on "$network" \
|
||||
-e PROBE_URL="$url" \
|
||||
-e PROBE_START="$started" \
|
||||
-e PROBE_DEADLINE="$(( started + HTTP_TIMEOUT_SECONDS ))" \
|
||||
"$HELPER_IMAGE" python3 -c "$PROBE_PY" 2>&1)" || status=$?
|
||||
printf '%-70s %s\n' "$url" "$result"
|
||||
return "$status"
|
||||
}
|
||||
|
||||
http_checks() {
|
||||
local started ids id name spec networks network port target scheme tested=0 status=0
|
||||
local -a targets=()
|
||||
[[ -f "$STARTED_FILE" ]] || die "heure de démarrage inconnue : l'étape de démarrage n'a pas abouti"
|
||||
started="$(<"$STARTED_FILE")"
|
||||
|
||||
echo
|
||||
echo "Test HTTP (délai maximal : ${HTTP_TIMEOUT_SECONDS} s après le démarrage)"
|
||||
mapfile -t ids < <(compose ps --quiet | grep -v '^$' || true)
|
||||
for id in "${ids[@]}"; do
|
||||
IFS='|' read -r name spec networks < <(
|
||||
docker inspect --format \
|
||||
"{{.Name}}|{{index .Config.Labels \"${HTTP_TARGET_LABEL}\"}}|{{range \$k, \$v := .NetworkSettings.Networks}}{{\$k}} {{end}}" \
|
||||
"$id"
|
||||
)
|
||||
[[ -n "$spec" ]] || continue
|
||||
name="${name#/}"
|
||||
network="${networks%% *}"
|
||||
if [[ -z "$network" ]]; then
|
||||
echo "${name} : aucun réseau propre, test HTTP ignoré"
|
||||
continue
|
||||
fi
|
||||
if [[ "$spec" == "auto" ]]; then
|
||||
port="$(single_exposed_port "$id" || true)"
|
||||
if [[ -z "$port" ]]; then
|
||||
echo "${name} : port HTTP indéterminable (aucun label de port Traefik, ports exposés multiples ou absents), test HTTP ignoré"
|
||||
continue
|
||||
fi
|
||||
spec="http:${port}"
|
||||
fi
|
||||
IFS=',' read -ra targets <<<"$spec"
|
||||
for target in "${targets[@]}"; do
|
||||
scheme="${target%%:*}"
|
||||
port="${target##*:}"
|
||||
tested=1
|
||||
probe "$network" "${scheme}://${name}:${port}/" "$started" || status=1
|
||||
done
|
||||
done
|
||||
|
||||
if (( ! tested )); then
|
||||
echo "Aucun service exposé par Traefik : test HTTP non applicable"
|
||||
fi
|
||||
return "$status"
|
||||
}
|
||||
|
||||
cmd_verify() {
|
||||
local timeout deadline stable=0
|
||||
load_state
|
||||
@@ -269,6 +386,7 @@ cmd_verify() {
|
||||
if (( stable )); then
|
||||
printf '%s' "$STATE_TABLE"
|
||||
echo "Tous les conteneurs sont démarrés et stables"
|
||||
http_checks || die "au moins un service web ne répond pas dans le délai"
|
||||
return 0
|
||||
fi
|
||||
stable=1
|
||||
|
||||
Reference in new issue
Block a user