Resolve edge E2E host routing in CI
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 || "");
|
||||||
|
|||||||
Reference in New Issue
Block a user