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
+73 -12
View File
@@ -75,6 +75,63 @@ function normalizeBaseUrl(url) {
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" } = {}) {
const deadline = Date.now() + timeoutMs;
@@ -97,25 +154,29 @@ async function ensureComposeServices(rootDir, composeProject) {
}
async function waitForApiReady(baseUrl, attempts = 60) {
const root = normalizeBaseUrl(baseUrl);
const candidates = await candidateApiBaseUrls(baseUrl);
let lastError = "API never responded";
for (let attempt = 0; attempt < attempts; attempt += 1) {
try {
const response = await fetch(`${root}/ping`);
if (response.ok) {
return;
}
for (const root of candidates) {
try {
const response = await fetch(`${root}/ping`, {
signal: AbortSignal.timeout(1000),
});
if (response.ok) {
return root;
}
lastError = `Unexpected ping status ${response.status}`;
} catch (error) {
lastError = error instanceof Error ? error.message : String(error);
lastError = `${root}/ping returned HTTP ${response.status}`;
} catch (error) {
lastError = `${root}/ping failed: ${error instanceof Error ? error.message : String(error)}`;
}
}
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) {
@@ -345,7 +406,7 @@ async function main() {
const containerName = `truckwash-edge-e2e-${runId}`;
const configDir = path.join(rootDir, ".tmp", "edge-gateway-e2e", runId);
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 =
process.env.EDGE_GATEWAY_E2E_COMPOSE_PROJECT
|| path.basename(rootDir);
@@ -357,7 +418,7 @@ async function main() {
try {
await ensureComposeServices(rootDir, composeProject);
await waitForApiReady(baseUrl);
baseUrl = await waitForApiReady(baseUrl);
fixture = await runPhpFixture(rootDir, composeProject, "create");
const authToken = String(fixture.auth_token || "");