Resolve edge E2E host routing in CI

This commit is contained in:
Jeppe Bundgaard
2026-04-24 20:27:44 +02:00
parent ebe1299089
commit 288627243b
3 changed files with 88 additions and 18 deletions
+14
View File
@@ -1,5 +1,19 @@
services: services:
edge-broker:
labels:
- "traefik.http.routers.edge-broker-local-ci.rule=PathPrefix(`/api/edge-broker`)"
- "traefik.http.routers.edge-broker-local-ci.entrypoints=web"
- "traefik.http.routers.edge-broker-local-ci.middlewares=secure-headers@file,edge-broker-strip-local"
- "traefik.http.routers.edge-broker-local-ci.priority=190"
- "traefik.http.routers.edge-broker-local-ci.service=edge-broker"
caddy: caddy:
labels:
- "traefik.http.routers.local-api-ci.rule=PathPrefix(`/api`)"
- "traefik.http.routers.local-api-ci.entrypoints=web"
- "traefik.http.routers.local-api-ci.middlewares=strip-api-prefix@file,secure-headers@file"
- "traefik.http.routers.local-api-ci.priority=90"
- "traefik.http.routers.local-api-ci.service=caddy"
volumes: volumes:
- ci_php_app:/var/www/html - ci_php_app:/var/www/html
+1 -6
View File
@@ -219,12 +219,7 @@ jobs:
vendor/bin/pest tests/Integration/EdgeGateway --colors=always" vendor/bin/pest tests/Integration/EdgeGateway --colors=always"
- name: Run edge gateway E2E smoke - name: Run edge gateway E2E smoke
run: | run: node scripts/edge-gateway-e2e.mjs
docker_host="$(ip route show default 2>/dev/null | awk '/default/ { print $3; exit }')"
if [ -z "${docker_host}" ]; then
docker_host="localhost"
fi
EDGE_GATEWAY_E2E_BASE_URL="http://${docker_host}:18080/api" node scripts/edge-gateway-e2e.mjs
- name: Tear down local stack - name: Tear down local stack
if: always() if: always()
+73 -12
View File
@@ -75,6 +75,63 @@ function normalizeBaseUrl(url) {
return String(url || "").replace(/\/+$/, ""); return String(url || "").replace(/\/+$/, "");
} }
function baseUrlWithHost(baseUrl, host) {
const url = new URL(normalizeBaseUrl(baseUrl));
url.hostname = host;
return normalizeBaseUrl(url.toString());
}
async function readDefaultGatewayHost() {
if (process.platform === "win32") {
return null;
}
try {
const routeTable = await fs.readFile("/proc/net/route", "utf8");
const route = routeTable
.split(/\r?\n/)
.map((line) => line.trim().split(/\s+/))
.find((fields) => fields[1] === "00000000" && /^[0-9A-Fa-f]{8}$/.test(fields[2] || ""));
if (!route) {
return null;
}
const gateway = route[2];
const octets = [
gateway.slice(6, 8),
gateway.slice(4, 6),
gateway.slice(2, 4),
gateway.slice(0, 2),
].map((octet) => Number.parseInt(octet, 16));
if (octets.some((octet) => !Number.isInteger(octet)) || octets.every((octet) => octet === 0)) {
return null;
}
return octets.join(".");
} catch {
return null;
}
}
async function candidateApiBaseUrls(baseUrl) {
const normalized = normalizeBaseUrl(baseUrl);
const candidates = [normalized];
const url = new URL(normalized);
if (["localhost", "127.0.0.1", "::1"].includes(url.hostname)) {
const gatewayHost = await readDefaultGatewayHost();
if (gatewayHost) {
candidates.push(baseUrlWithHost(normalized, gatewayHost));
}
candidates.push(baseUrlWithHost(normalized, "host.docker.internal"));
}
return [...new Set(candidates)];
}
async function waitForCondition(predicate, { timeoutMs = 30_000, intervalMs = 500, message = "Timed out" } = {}) { async function waitForCondition(predicate, { timeoutMs = 30_000, intervalMs = 500, message = "Timed out" } = {}) {
const deadline = Date.now() + timeoutMs; const deadline = Date.now() + timeoutMs;
@@ -97,25 +154,29 @@ async function ensureComposeServices(rootDir, composeProject) {
} }
async function waitForApiReady(baseUrl, attempts = 60) { async function waitForApiReady(baseUrl, attempts = 60) {
const root = normalizeBaseUrl(baseUrl); const candidates = await candidateApiBaseUrls(baseUrl);
let lastError = "API never responded"; let lastError = "API never responded";
for (let attempt = 0; attempt < attempts; attempt += 1) { for (let attempt = 0; attempt < attempts; attempt += 1) {
try { for (const root of candidates) {
const response = await fetch(`${root}/ping`); try {
if (response.ok) { const response = await fetch(`${root}/ping`, {
return; signal: AbortSignal.timeout(1000),
} });
if (response.ok) {
return root;
}
lastError = `Unexpected ping status ${response.status}`; lastError = `${root}/ping returned HTTP ${response.status}`;
} catch (error) { } catch (error) {
lastError = error instanceof Error ? error.message : String(error); lastError = `${root}/ping failed: ${error instanceof Error ? error.message : String(error)}`;
}
} }
await new Promise((resolve) => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
} }
throw new Error(`API did not become ready at ${root}/ping: ${lastError}`); throw new Error(`API did not become ready at ${candidates.map((candidate) => `${candidate}/ping`).join(", ")}: ${lastError}`);
} }
function parseLastJsonLine(output) { function parseLastJsonLine(output) {
@@ -345,7 +406,7 @@ async function main() {
const containerName = `truckwash-edge-e2e-${runId}`; const containerName = `truckwash-edge-e2e-${runId}`;
const configDir = path.join(rootDir, ".tmp", "edge-gateway-e2e", runId); const configDir = path.join(rootDir, ".tmp", "edge-gateway-e2e", runId);
const configFilePath = path.join(configDir, DEFAULT_CONFIG_FILE_NAME); const configFilePath = path.join(configDir, DEFAULT_CONFIG_FILE_NAME);
const baseUrl = process.env.EDGE_GATEWAY_E2E_BASE_URL || DEFAULT_HOST_API_URL; let baseUrl = process.env.EDGE_GATEWAY_E2E_BASE_URL || DEFAULT_HOST_API_URL;
const composeProject = const composeProject =
process.env.EDGE_GATEWAY_E2E_COMPOSE_PROJECT process.env.EDGE_GATEWAY_E2E_COMPOSE_PROJECT
|| path.basename(rootDir); || path.basename(rootDir);
@@ -357,7 +418,7 @@ async function main() {
try { try {
await ensureComposeServices(rootDir, composeProject); await ensureComposeServices(rootDir, composeProject);
await waitForApiReady(baseUrl); baseUrl = await waitForApiReady(baseUrl);
fixture = await runPhpFixture(rootDir, composeProject, "create"); fixture = await runPhpFixture(rootDir, composeProject, "create");
const authToken = String(fixture.auth_token || ""); const authToken = String(fixture.auth_token || "");