Skip to content

Commit 1161c4a

Browse files
committed
fix(ci): hard-bound LXD API probes
1 parent 7591aec commit 1161c4a

4 files changed

Lines changed: 68 additions & 30 deletions

File tree

tests/integration-scenario-timeouts.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ describe("integration scenario timeout guardrails", () => {
77
test("logs and bounds LXD API verification after identity-provider switches", () => {
88
expect(commonSource).toContain("verify ${host.label} LXD API");
99
expect(commonSource).toContain("verified ${host.label} LXD API");
10+
expect(commonSource).toContain("LXD API verification for ${host.label}");
11+
expect(commonSource).toContain("timeoutMs: LXD_API_POLL_TIMEOUT_MS");
1012
expect(commonSource).toContain("external OIDC LXD API for ${host.label}");
1113
expect(commonSource).toContain("local ZITADEL LXD API for ${host.label}");
1214
expect(commonSource.match(/verifyLxdApi\(host, context\)/g)?.length).toBe(2);

tests/integration/lib/process.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,13 @@ describe("integration process helpers", () => {
2323
expect(result.stderr).toContain("command timed out after 100ms");
2424
expect(Date.now() - startedAt).toBeLessThan(5000);
2525
});
26+
27+
test("terminates timed-out process groups with inherited pipes", async () => {
28+
const startedAt = Date.now();
29+
const result = await runAllowFailure(["bash", "-lc", "sleep 10 & exit 0"], { timeoutMs: 100 });
30+
31+
expect(result.exitCode).toBe(124);
32+
expect(result.stderr).toContain("command timed out after 100ms");
33+
expect(Date.now() - startedAt).toBeLessThan(5000);
34+
});
2635
});

tests/integration/lib/process.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,16 @@ async function runOnceAllowFailure(cmd: string[], options: CommandOptions = {}):
6767
let timedOut = false;
6868
let timeout: Timer | undefined;
6969
let killTimer: Timer | undefined;
70+
let forceFinishTimer: Timer | undefined;
71+
72+
const timeoutResult = (): CommandResult => {
73+
const stderrText = Buffer.concat(stderr).toString("utf8");
74+
return {
75+
exitCode: 124,
76+
stdout: Buffer.concat(stdout).toString("utf8"),
77+
stderr: `${stderrText}${stderrText ? "\n" : ""}command timed out after ${options.timeoutMs}ms`
78+
};
79+
};
7080

7181
const finish = (result: CommandResult): void => {
7282
if (settled) {
@@ -79,6 +89,9 @@ async function runOnceAllowFailure(cmd: string[], options: CommandOptions = {}):
7989
if (killTimer) {
8090
clearTimeout(killTimer);
8191
}
92+
if (forceFinishTimer) {
93+
clearTimeout(forceFinishTimer);
94+
}
8295
resolve(result);
8396
};
8497

@@ -108,11 +121,10 @@ async function runOnceAllowFailure(cmd: string[], options: CommandOptions = {}):
108121
});
109122
});
110123
proc.on("close", (code, signal) => {
111-
const stderrText = Buffer.concat(stderr).toString("utf8");
112124
finish({
113125
exitCode: timedOut ? 124 : code ?? (signal ? 128 + signalNumber(signal) : 1),
114126
stdout: Buffer.concat(stdout).toString("utf8"),
115-
stderr: timedOut ? `${stderrText}${stderrText ? "\n" : ""}command timed out after ${options.timeoutMs}ms` : stderrText
127+
stderr: timedOut ? timeoutResult().stderr : Buffer.concat(stderr).toString("utf8")
116128
});
117129
});
118130

@@ -126,6 +138,16 @@ async function runOnceAllowFailure(cmd: string[], options: CommandOptions = {}):
126138
killTimer = setTimeout(() => {
127139
if (!settled) {
128140
killProcess("SIGKILL");
141+
forceFinishTimer = setTimeout(() => {
142+
if (settled) {
143+
return;
144+
}
145+
proc.stdout?.destroy();
146+
proc.stderr?.destroy();
147+
proc.stdin?.destroy();
148+
proc.unref();
149+
finish(timeoutResult());
150+
}, 1000);
129151
}
130152
}, 2000);
131153
}, options.timeoutMs);

tests/integration/scenarios/common.ts

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ type InstallOptions = {
4040
zitadelAdminEmail?: string;
4141
};
4242

43+
const LXD_API_POLL_TIMEOUT_MS = 90 * 1000;
44+
const LXD_API_VERIFY_TIMEOUT_MS = 2 * 60 * 1000;
45+
4346
function baseEmail(ctx: IntegrationContext): string {
4447
return `terrarium+${ctx.config.slug}@${ctx.config.ipDnsDomain}`;
4548
}
@@ -325,34 +328,36 @@ export async function verifyManagementSurfaces(
325328

326329
/** Verifies the public LXD endpoint serves the real API over trusted TLS and does not expose trusted anonymous access. */
327330
export async function verifyLxdApi(host: ManagedHost, context?: IntegrationContext): Promise<void> {
328-
context?.logger.info(`verify ${host.label} LXD API`);
329-
await expectHttpsJson(
330-
`https://${host.domains.lxd}/1.0`,
331-
(body) => {
332-
if (!isObject(body)) {
333-
throw new Error("LXD API root did not return an object");
334-
}
335-
336-
const metadata = body.metadata;
337-
if (!isObject(metadata)) {
338-
throw new Error("LXD API root did not include metadata");
339-
}
340-
341-
if (!Array.isArray(metadata.api_extensions)) {
342-
throw new Error("LXD API root did not include api_extensions");
343-
}
344-
345-
const auth = typeof metadata.auth === "string" ? metadata.auth.toLowerCase() : "";
346-
if (!auth) {
347-
throw new Error("LXD API root did not include auth state");
348-
}
349-
if (auth === "trusted") {
350-
throw new Error("LXD API root allowed trusted anonymous access");
351-
}
352-
},
353-
{ timeoutMs: 300000, resolveIp: host.server.ipv4 }
354-
);
355-
context?.logger.info(`verified ${host.label} LXD API`);
331+
await withStepTimeout(`LXD API verification for ${host.label}`, LXD_API_VERIFY_TIMEOUT_MS, async () => {
332+
context?.logger.info(`verify ${host.label} LXD API`);
333+
await expectHttpsJson(
334+
`https://${host.domains.lxd}/1.0`,
335+
(body) => {
336+
if (!isObject(body)) {
337+
throw new Error("LXD API root did not return an object");
338+
}
339+
340+
const metadata = body.metadata;
341+
if (!isObject(metadata)) {
342+
throw new Error("LXD API root did not include metadata");
343+
}
344+
345+
if (!Array.isArray(metadata.api_extensions)) {
346+
throw new Error("LXD API root did not include api_extensions");
347+
}
348+
349+
const auth = typeof metadata.auth === "string" ? metadata.auth.toLowerCase() : "";
350+
if (!auth) {
351+
throw new Error("LXD API root did not include auth state");
352+
}
353+
if (auth === "trusted") {
354+
throw new Error("LXD API root allowed trusted anonymous access");
355+
}
356+
},
357+
{ timeoutMs: LXD_API_POLL_TIMEOUT_MS, resolveIp: host.server.ipv4 }
358+
);
359+
context?.logger.info(`verified ${host.label} LXD API`);
360+
});
356361
}
357362

358363
/** Verifies a real browser login through LXD's public OIDC flow. */

0 commit comments

Comments
 (0)