Resolve edge E2E host routing in CI
This commit is contained in:
@@ -1,5 +1,19 @@
|
||||
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:
|
||||
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:
|
||||
- ci_php_app:/var/www/html
|
||||
|
||||
|
||||
@@ -219,12 +219,7 @@ jobs:
|
||||
vendor/bin/pest tests/Integration/EdgeGateway --colors=always"
|
||||
|
||||
- name: Run edge gateway E2E smoke
|
||||
run: |
|
||||
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
|
||||
run: node scripts/edge-gateway-e2e.mjs
|
||||
|
||||
- name: Tear down local stack
|
||||
if: always()
|
||||
|
||||
@@ -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 || "");
|
||||
|
||||
Reference in New Issue
Block a user