Skip to content

Commit 6ab83dd

Browse files
committed
fixes
1 parent 7e950a9 commit 6ab83dd

8 files changed

Lines changed: 105 additions & 26 deletions

File tree

.github/workflows/cockpit-plugins.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@ on:
55
branches:
66
- main
77
paths:
8-
- ".github/workflows/cockpit-plugins.yml"
9-
- "scripts/build-cockpit-plugins-bundle.sh"
108
- "ansible/roles/cockpit_plugins/defaults/main.yml"
11-
tags:
12-
- "*"
139
workflow_dispatch:
1410
permissions:
1511
contents: write

ansible/roles/cockpit_plugins/defaults/main.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ terrarium_cockpit_plugin_build_deps:
1717

1818
terrarium_cockpit_plugin_bundle_enabled: true
1919
terrarium_cockpit_plugin_bundle_repo: terion-name/terrarium
20-
terrarium_cockpit_plugin_bundle_main_ref: cockpit-plugins-main
2120

2221
terrarium_cockpit_zfs_repo: https://github.com/45Drives/cockpit-zfs.git
2322
terrarium_cockpit_zfs_ref: v1.2.21-3

ansible/roles/lxd/tasks/main.yml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
- name: Install LXD snap
2-
ansible.builtin.command: "snap install lxd --channel={{ terrarium_lxd_snap_channel }}"
3-
args:
4-
creates: /snap/bin/lxd
2+
community.general.snap:
3+
name: lxd
4+
channel: "{{ terrarium_lxd_snap_channel }}"
5+
state: present
6+
register: terrarium_lxd_snap_install
7+
until: terrarium_lxd_snap_install is succeeded
8+
retries: 30
9+
delay: 10
510

611
- name: Enable LXD UI
712
ansible.builtin.command: snap set lxd ui.enable=true

ansible/roles/oauth2_proxy/tasks/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@
115115
(
116116
terrarium_root_domain
117117
if terrarium_root_domain | length > 0
118-
else (terrarium_public_ip | replace('.', '-') ~ '.traefik.me')
118+
else (terrarium_manage_domain | regex_replace('^[^.]+\\.', ''))
119119
)
120120
}}
121121

scripts/terrarium-zitadel-sync.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,23 @@ const DEFAULT_TOFU_IMAGE = "ghcr.io/opentofu/opentofu:1.10.6";
1212
const WAIT_INTERVAL_MS = 5000;
1313
const WAIT_ATTEMPTS = 36;
1414

15+
function isRetriableZitadelApiError(message: string): boolean {
16+
const lowered = message.toLowerCase();
17+
return [
18+
"failed to connect",
19+
"connection refused",
20+
"empty reply from server",
21+
"timed out",
22+
"timeout was reached",
23+
"bad gateway",
24+
"service unavailable",
25+
"gateway timeout",
26+
"http 502",
27+
"http 503",
28+
"http 504"
29+
].some((needle) => lowered.includes(needle));
30+
}
31+
1532
async function dockerRun(args: string[]): Promise<string> {
1633
return await runText(["docker", ...args], PREFIX);
1734
}
@@ -166,8 +183,22 @@ async function zitadelApi<T>(
166183
if (body !== undefined && method !== "GET") {
167184
cmd.push("-d", JSON.stringify(body));
168185
}
169-
const stdout = await runText(cmd, PREFIX);
170-
return JSON.parse(stdout) as T;
186+
187+
let lastError = "";
188+
for (let attempt = 0; attempt < WAIT_ATTEMPTS; attempt += 1) {
189+
const result = await runAllowFailure(cmd);
190+
if (result.exitCode === 0) {
191+
return JSON.parse(result.stdout) as T;
192+
}
193+
194+
lastError = result.stderr.trim() || result.stdout.trim() || `ZITADEL API ${method} ${path} failed`;
195+
if (!isRetriableZitadelApiError(lastError)) {
196+
throw new Error(lastError);
197+
}
198+
await Bun.sleep(WAIT_INTERVAL_MS);
199+
}
200+
201+
throw new Error(`timed out waiting for ZITADEL API ${method} ${path}: ${lastError}`);
171202
}
172203

173204
async function lookupProjectId(authDomain: string, pat: string): Promise<string> {

tests/integration/assertions/browser.ts

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,47 @@ type LoginOptions = {
88
};
99

1010
async function firstVisible(page: Page, selectors: string[]): Promise<string> {
11-
for (const selector of selectors) {
12-
const locator = page.locator(selector).first();
13-
if (await locator.isVisible().catch(() => false)) {
14-
return selector;
11+
const deadline = Date.now() + 30000;
12+
13+
while (Date.now() < deadline) {
14+
for (const selector of selectors) {
15+
const locator = page.locator(selector).first();
16+
if (await locator.isVisible().catch(() => false)) {
17+
return selector;
18+
}
1519
}
20+
21+
await page.waitForTimeout(500);
1622
}
23+
1724
throw new Error(`none of the selectors were visible: ${selectors.join(", ")}`);
1825
}
1926

2027
async function clickFirst(page: Page, selectors: string[]): Promise<void> {
21-
for (const selector of selectors) {
22-
const locator = page.locator(selector).first();
23-
if (await locator.isVisible().catch(() => false)) {
24-
await locator.click();
25-
return;
28+
const deadline = Date.now() + 30000;
29+
30+
while (Date.now() < deadline) {
31+
for (const selector of selectors) {
32+
const locator = page.locator(selector).first();
33+
if (await locator.isVisible().catch(() => false)) {
34+
await locator.click();
35+
return;
36+
}
2637
}
38+
39+
await page.waitForTimeout(500);
2740
}
41+
2842
throw new Error(`none of the click selectors were visible: ${selectors.join(", ")}`);
2943
}
3044

45+
async function typeInto(page: Page, selector: string, value: string): Promise<void> {
46+
const locator = page.locator(selector).first();
47+
await locator.click();
48+
await locator.clear();
49+
await locator.pressSequentially(value, { delay: 30 });
50+
}
51+
3152
/** Runs a browser flow and preserves screenshots for post-failure inspection. */
3253
export async function withBrowser<T>(outputDir: string, runFlow: (browser: Browser) => Promise<T>): Promise<T> {
3354
mkdirSync(outputDir, { recursive: true });
@@ -50,17 +71,19 @@ export async function loginThroughZitadel(url: string, user: OidcTestUser, optio
5071
'input[type="email"]',
5172
'input[name="loginName"]',
5273
'input[name="username"]',
53-
'input[autocomplete="username"]'
74+
'input[autocomplete="username"]',
75+
'[data-testid="username-text-input"]'
5476
]);
55-
await page.fill(emailSelector, user.email);
77+
await typeInto(page, emailSelector, user.email);
5678
await clickFirst(page, ['button:has-text("Next")', 'button:has-text("Continue")', 'button:has-text("Sign in")']);
5779

5880
const passwordSelector = await firstVisible(page, [
5981
'input[type="password"]',
6082
'input[name="password"]',
61-
'input[autocomplete="current-password"]'
83+
'input[autocomplete="current-password"]',
84+
'[data-testid="password-text-input"]'
6285
]);
63-
await page.fill(passwordSelector, user.password);
86+
await typeInto(page, passwordSelector, user.password);
6487
await clickFirst(page, ['button:has-text("Sign in")', 'button:has-text("Login")', 'button:has-text("Continue")']);
6588

6689
await page.waitForLoadState("networkidle", { timeout: 120000 });

tests/integration/assertions/http.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { runAllowFailure } from "../lib/process";
2+
13
/** Polls an HTTP endpoint until it returns one of the expected status codes. */
24
export async function waitForHttpStatus(url: string, expectedStatuses: number[], timeoutMs = 180000): Promise<Response> {
35
const deadline = Date.now() + timeoutMs;
@@ -17,6 +19,29 @@ export async function waitForHttpStatus(url: string, expectedStatuses: number[],
1719
throw new Error(`timed out waiting for ${url} to return one of [${expectedStatuses.join(", ")}], last status: ${lastResponse?.status ?? "none"}`);
1820
}
1921

22+
/** Polls an HTTPS endpoint with certificate verification disabled until it returns an expected status. */
23+
export async function waitForHttpStatusInsecure(url: string, expectedStatuses: number[], timeoutMs = 180000): Promise<number> {
24+
const deadline = Date.now() + timeoutMs;
25+
let lastStatus = "";
26+
27+
while (Date.now() < deadline) {
28+
const result = await runAllowFailure(["curl", "-k", "-sS", "-o", "/dev/null", "-w", "%{http_code}", "--max-time", "20", url]);
29+
const status = (result.stdout || "").trim();
30+
lastStatus = status || lastStatus;
31+
32+
if (result.exitCode === 0) {
33+
const numericStatus = Number(status);
34+
if (expectedStatuses.includes(numericStatus)) {
35+
return numericStatus;
36+
}
37+
}
38+
39+
await Bun.sleep(5000);
40+
}
41+
42+
throw new Error(`timed out waiting for ${url} to return one of [${expectedStatuses.join(", ")}], last status: ${lastStatus || "none"}`);
43+
}
44+
2045
/** Fetches an endpoint and throws when the response body does not contain the expected text. */
2146
export async function expectHttpBodyContains(url: string, needle: string): Promise<void> {
2247
const response = await fetch(url, { redirect: "follow" });

tests/integration/scenarios/common.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { randomUUID } from "node:crypto";
44
import { IntegrationContext } from "../context";
55
import type { DomainBundle, ExternalOidcFixture, ManagedHost, ServerRecord, VolumeRecord } from "../types";
66
import { SshHost } from "../remote/ssh";
7-
import { expectHttpBodyContains, waitForHttpStatus } from "../assertions/http";
7+
import { expectHttpBodyContains, waitForHttpStatus, waitForHttpStatusInsecure } from "../assertions/http";
88
import { expectCockpitLogin, expectProtectedRoute, expectTraefikDashboard } from "../assertions/browser";
99
import { expectRemoteContains, expectSystemdActive } from "../assertions/host";
1010
import { collectHostArtifacts } from "../cleanup";
@@ -202,7 +202,7 @@ export async function readLocalZitadelAdmin(host: SshHost): Promise<{ email: str
202202
export async function waitForTerrariumPublicEndpoints(host: ManagedHost, includeAuth: boolean): Promise<void> {
203203
await waitForHttpStatus(`https://${host.domains.manage}`, [302, 303]);
204204
await waitForHttpStatus(`https://${host.domains.proxy}`, [302, 303]);
205-
await waitForHttpStatus(`https://${host.domains.lxd}`, [200, 302]);
205+
await waitForHttpStatusInsecure(`https://${host.domains.lxd}`, [200, 302]);
206206
if (includeAuth) {
207207
await waitForHttpStatus(`https://${host.domains.auth}/.well-known/openid-configuration`, [200]);
208208
}

0 commit comments

Comments
 (0)