@red-hat-developer-hub/e2e-test-utils 1.1.32 → 1.1.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,5 +2,6 @@
2
2
  * Global setup for Playwright tests.
3
3
  * This file runs once before all tests.
4
4
  */
5
- export default function globalSetup(): Promise<void>;
5
+ import { type FullConfig } from "@playwright/test";
6
+ export default function globalSetup(config: FullConfig): Promise<void>;
6
7
  //# sourceMappingURL=global-setup.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"global-setup.d.ts","sourceRoot":"","sources":["../../src/playwright/global-setup.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA2EH,wBAA8B,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAMzD"}
1
+ {"version":3,"file":"global-setup.d.ts","sourceRoot":"","sources":["../../src/playwright/global-setup.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AA6EnD,wBAA8B,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ3E"}
@@ -2,10 +2,13 @@
2
2
  * Global setup for Playwright tests.
3
3
  * This file runs once before all tests.
4
4
  */
5
+ import dotenv from "dotenv";
6
+ import { resolve } from "path";
5
7
  import { KubernetesClientHelper } from "../utils/kubernetes-client.js";
6
8
  import { $ } from "../utils/bash.js";
7
9
  import { KeycloakHelper } from "../deployment/keycloak/index.js";
8
10
  import { DEFAULT_KEYCLOAK_CONFIG, DEFAULT_RHDH_CLIENT, DEFAULT_USERS, } from "../deployment/keycloak/constants.js";
11
+ import { loadLocalVaultSecrets } from "../utils/vault.js";
9
12
  const REQUIRED_BINARIES = ["oc", "kubectl", "helm"];
10
13
  async function checkRequiredBinaries() {
11
14
  const missingBinaries = [];
@@ -58,10 +61,27 @@ async function deployKeycloak() {
58
61
  testPassword: DEFAULT_USERS[0].password,
59
62
  });
60
63
  }
61
- export default async function globalSetup() {
64
+ export default async function globalSetup(config) {
62
65
  console.log("Running global setup...");
63
66
  await checkRequiredBinaries();
67
+ await loadLocalVaultSecrets();
68
+ loadDotenvFromProjects(config);
64
69
  await setClusterRouterBaseEnv();
65
70
  await deployKeycloak();
66
71
  console.log("Global setup completed successfully");
67
72
  }
73
+ /**
74
+ * Loads .env files from each project's e2e-tests directory.
75
+ * Uses `override: true` so local .env values take priority over Vault secrets.
76
+ */
77
+ function loadDotenvFromProjects(config) {
78
+ const seen = new Set();
79
+ for (const project of config.projects) {
80
+ // testDir points to e2e-tests/tests, go up one level to e2e-tests/
81
+ const e2eRoot = resolve(project.testDir, "..");
82
+ if (seen.has(e2eRoot))
83
+ continue;
84
+ seen.add(e2eRoot);
85
+ dotenv.config({ path: resolve(e2eRoot, ".env"), override: true });
86
+ }
87
+ }
@@ -10,7 +10,8 @@ import type { Reporter, Suite, TestCase, TestResult } from "@playwright/test/rep
10
10
  * Falls back in onEnd() to clean up any projects that didn't complete naturally
11
11
  * (e.g., interrupted runs, maxFailures).
12
12
  *
13
- * Only active when process.env.CI === "true".
13
+ * Diagnostic log collection runs always (CI and local).
14
+ * Namespace deletion only runs when process.env.CI === "true".
14
15
  *
15
16
  * By default, deletes the namespace matching the project name.
16
17
  * For custom namespaces, consumers can register them via registerTeardownNamespace().
@@ -18,6 +19,7 @@ import type { Reporter, Suite, TestCase, TestResult } from "@playwright/test/rep
18
19
  export default class TeardownReporter implements Reporter {
19
20
  private _projectTestCounts;
20
21
  private _projectCompleted;
22
+ private _projectsWithFailures;
21
23
  private _pendingDeletions;
22
24
  onBegin(_config: unknown, suite: Suite): void;
23
25
  onTestEnd(test: TestCase, result: TestResult): void;
@@ -1 +1 @@
1
- {"version":3,"file":"teardown-reporter.d.ts","sourceRoot":"","sources":["../../src/playwright/teardown-reporter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,UAAU,EACX,MAAM,2BAA2B,CAAC;AAInC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,OAAO,OAAO,gBAAiB,YAAW,QAAQ;IACvD,OAAO,CAAC,kBAAkB,CAA6B;IACvD,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,iBAAiB,CAAoC;IAE7D,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI;IAa7C,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI;IA0B7C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAed,wBAAwB;CAuBvC"}
1
+ {"version":3,"file":"teardown-reporter.d.ts","sourceRoot":"","sources":["../../src/playwright/teardown-reporter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,UAAU,EACX,MAAM,2BAA2B,CAAC;AAKnC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,OAAO,OAAO,gBAAiB,YAAW,QAAQ;IACvD,OAAO,CAAC,kBAAkB,CAA6B;IACvD,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,qBAAqB,CAAqB;IAClD,OAAO,CAAC,iBAAiB,CAAoC;IAE7D,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI;IAa7C,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI;IA6B7C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAcd,wBAAwB;CAwCvC"}
@@ -1,3 +1,4 @@
1
+ import path from "path";
1
2
  import { KubernetesClientHelper } from "../utils/kubernetes-client.js";
2
3
  import { getTeardownNamespaces } from "./teardown-namespaces.js";
3
4
  /**
@@ -11,7 +12,8 @@ import { getTeardownNamespaces } from "./teardown-namespaces.js";
11
12
  * Falls back in onEnd() to clean up any projects that didn't complete naturally
12
13
  * (e.g., interrupted runs, maxFailures).
13
14
  *
14
- * Only active when process.env.CI === "true".
15
+ * Diagnostic log collection runs always (CI and local).
16
+ * Namespace deletion only runs when process.env.CI === "true".
15
17
  *
16
18
  * By default, deletes the namespace matching the project name.
17
19
  * For custom namespaces, consumers can register them via registerTeardownNamespace().
@@ -19,6 +21,7 @@ import { getTeardownNamespaces } from "./teardown-namespaces.js";
19
21
  export default class TeardownReporter {
20
22
  _projectTestCounts = new Map();
21
23
  _projectCompleted = new Map();
24
+ _projectsWithFailures = new Set();
22
25
  _pendingDeletions = new Map();
23
26
  onBegin(_config, suite) {
24
27
  for (const test of suite.allTests()) {
@@ -30,8 +33,6 @@ export default class TeardownReporter {
30
33
  }
31
34
  }
32
35
  onTestEnd(test, result) {
33
- if (process.env.CI !== "true")
34
- return;
35
36
  const project = test.parent.project();
36
37
  if (!project)
37
38
  return;
@@ -41,23 +42,25 @@ export default class TeardownReporter {
41
42
  if (!isDone)
42
43
  return;
43
44
  const name = project.name;
45
+ if (result.status !== "passed" && result.status !== "skipped") {
46
+ this._projectsWithFailures.add(name);
47
+ }
44
48
  const completed = (this._projectCompleted.get(name) ?? 0) + 1;
45
49
  this._projectCompleted.set(name, completed);
46
- // Start deletion immediately (fire-and-forget here, awaited in onEnd)
50
+ // Start cleanup immediately (fire-and-forget here, awaited in onEnd)
47
51
  if (completed === this._projectTestCounts.get(name) &&
48
52
  !this._pendingDeletions.has(name)) {
49
53
  this._pendingDeletions.set(name, this._deleteProjectNamespaces(name));
50
54
  }
51
55
  }
52
56
  async onEnd() {
53
- if (process.env.CI !== "true")
54
- return;
55
- // Await all in-flight deletions started from onTestEnd
57
+ // Await all in-flight cleanups started from onTestEnd
56
58
  await Promise.all(this._pendingDeletions.values());
57
59
  // Fallback: clean up projects that didn't complete naturally
58
- // (e.g., interrupted run, maxFailures hit)
60
+ // (e.g., interrupted run, maxFailures hit) — always collect diagnostics
59
61
  for (const [project] of this._projectTestCounts) {
60
62
  if (!this._pendingDeletions.has(project)) {
63
+ this._projectsWithFailures.add(project);
61
64
  await this._deleteProjectNamespaces(project);
62
65
  }
63
66
  }
@@ -68,14 +71,24 @@ export default class TeardownReporter {
68
71
  k8sClient = new KubernetesClientHelper();
69
72
  }
70
73
  catch (error) {
71
- console.error(`[TeardownReporter] Cannot connect to cluster, skipping teardown:`, error);
74
+ console.error(`[TeardownReporter] Cannot connect to cluster, skipping cleanup:`, error);
72
75
  return;
73
76
  }
74
77
  const customNamespaces = getTeardownNamespaces(projectName);
75
78
  const namespaces = customNamespaces.length > 0 ? customNamespaces : [projectName];
76
- for (const ns of namespaces) {
77
- console.log(`[TeardownReporter] Deleting namespace "${ns}" (project: ${projectName})`);
78
- await k8sClient.deleteNamespace(ns);
79
+ // Collect diagnostic logs on failure (always, regardless of CI)
80
+ if (this._projectsWithFailures.has(projectName)) {
81
+ for (const ns of namespaces) {
82
+ const outputDir = path.join("node_modules", ".cache", "e2e-test-results", "logs", projectName);
83
+ await k8sClient.collectDiagnosticLogs(ns, outputDir);
84
+ }
85
+ }
86
+ // Delete namespaces only in CI
87
+ if (process.env.CI === "true") {
88
+ for (const ns of namespaces) {
89
+ console.log(`[TeardownReporter] Deleting namespace "${ns}" (project: ${projectName})`);
90
+ await k8sClient.deleteNamespace(ns);
91
+ }
79
92
  }
80
93
  }
81
94
  }
@@ -88,6 +88,15 @@ declare class KubernetesClientHelper {
88
88
  * @param pollIntervalMs - How often to check pod status (default: 5000)
89
89
  */
90
90
  waitForPodsWithFailureDetection(namespace: string, labelSelector: string, timeoutSeconds?: number, pollIntervalMs?: number): Promise<void>;
91
+ /**
92
+ * Collects diagnostic logs for all resources in a namespace and saves them as files.
93
+ * Uses kubectl for cross-platform compatibility (works on OpenShift, EKS, GKE, etc.).
94
+ * OpenShift-specific resources (routes) are collected on a best-effort basis.
95
+ *
96
+ * @param namespace - Namespace to collect diagnostics from
97
+ * @param outputDir - Directory to write log files to (defaults to playwright-report/logs/<namespace>)
98
+ */
99
+ collectDiagnosticLogs(namespace: string, outputDir?: string): Promise<void>;
91
100
  /**
92
101
  * Check if a pod is in a failure state. Returns failure info or null if healthy.
93
102
  */
@@ -1 +1 @@
1
- {"version":3,"file":"kubernetes-client.d.ts","sourceRoot":"","sources":["../../src/utils/kubernetes-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,GAAG,MAAM,yBAAyB,CAAC;AAO/C;;GAEG;AACH,cAAM,sBAAsB;IAC1B,OAAO,CAAC,GAAG,CAAiB;IAC5B,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,iBAAiB,CAAuB;;IAqChD;;OAEG;IACG,uBAAuB,CAC3B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC;IA8C3B;;OAEG;IACG,0BAA0B,CAC9B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC;IA+B3B;;UAEM;IAmBN;;OAEG;IAsBH;;OAEG;YACW,YAAY;IA8B1B;;OAEG;IACG,wBAAwB,CAC5B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC;IA0ChB;;OAEG;IACG,qBAAqB,CACzB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,EAC7C,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC;IAqChB;;OAEG;IACG,eAAe,CACnB,SAAS,EAAE,MAAM,EACjB,eAAe,GAAE,OAAc,EAC/B,cAAc,GAAE,MAAY,GAC3B,OAAO,CAAC,IAAI,CAAC;IAyBhB;;OAEG;YACW,yBAAyB;IAsCvC;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IA2BxB;;OAEG;IACG,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAc3E;;OAEG;IACG,uBAAuB,CAC3B,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,cAAc,GAAE,MAAY,EAC5B,cAAc,GAAE,MAAa,GAC5B,OAAO,CAAC,OAAO,CAAC;IAiBnB;;;OAGG;IACG,uBAAuB,IAAI,OAAO,CAAC,MAAM,CAAC;IAuBhD;;;;;;OAMG;IACG,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAmBxE;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAmBxB;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAQnC;IAEH;;;;;;;;OAQG;IACG,+BAA+B,CACnC,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,EACrB,cAAc,GAAE,MAAY,EAC5B,cAAc,GAAE,MAAa,GAC5B,OAAO,CAAC,IAAI,CAAC;IA2FhB;;OAEG;IACH,OAAO,CAAC,gBAAgB;CA8BzB;AAED,OAAO,EAAE,sBAAsB,EAAE,CAAC"}
1
+ {"version":3,"file":"kubernetes-client.d.ts","sourceRoot":"","sources":["../../src/utils/kubernetes-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,GAAG,MAAM,yBAAyB,CAAC;AAO/C;;GAEG;AACH,cAAM,sBAAsB;IAC1B,OAAO,CAAC,GAAG,CAAiB;IAC5B,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,iBAAiB,CAAuB;;IAqChD;;OAEG;IACG,uBAAuB,CAC3B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC;IA8C3B;;OAEG;IACG,0BAA0B,CAC9B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC;IA+B3B;;UAEM;IAmBN;;OAEG;IAsBH;;OAEG;YACW,YAAY;IA8B1B;;OAEG;IACG,wBAAwB,CAC5B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC;IA0ChB;;OAEG;IACG,qBAAqB,CACzB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,EAC7C,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC;IAqChB;;OAEG;IACG,eAAe,CACnB,SAAS,EAAE,MAAM,EACjB,eAAe,GAAE,OAAc,EAC/B,cAAc,GAAE,MAAY,GAC3B,OAAO,CAAC,IAAI,CAAC;IAyBhB;;OAEG;YACW,yBAAyB;IAsCvC;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IA2BxB;;OAEG;IACG,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAc3E;;OAEG;IACG,uBAAuB,CAC3B,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,cAAc,GAAE,MAAY,EAC5B,cAAc,GAAE,MAAa,GAC5B,OAAO,CAAC,OAAO,CAAC;IAiBnB;;;OAGG;IACG,uBAAuB,IAAI,OAAO,CAAC,MAAM,CAAC;IAuBhD;;;;;;OAMG;IACG,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAmBxE;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAmBxB;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAQnC;IAEH;;;;;;;;OAQG;IACG,+BAA+B,CACnC,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,EACrB,cAAc,GAAE,MAAY,EAC5B,cAAc,GAAE,MAAa,GAC5B,OAAO,CAAC,IAAI,CAAC;IAwFhB;;;;;;;OAOG;IACG,qBAAqB,CACzB,SAAS,EAAE,MAAM,EACjB,SAAS,GAAE,MAMV,GACA,OAAO,CAAC,IAAI,CAAC;IAiGhB;;OAEG;IACH,OAAO,CAAC,gBAAgB;CA8BzB;AAED,OAAO,EAAE,sBAAsB,EAAE,CAAC"}
@@ -509,13 +509,11 @@ class KubernetesClientHelper {
509
509
  }
510
510
  await new Promise((r) => setTimeout(r, pollIntervalMs));
511
511
  }
512
- // Timeout reached - collect diagnostic info before throwing
512
+ // Timeout reached - print diagnostics to stdio before throwing
513
513
  console.log(`\n[K8sHelper] ═══ Pod Diagnostics (timeout reached) ═══`);
514
514
  try {
515
515
  console.log(`\n[K8sHelper] ─── Pod Status ───`);
516
516
  await $ `oc get pods -n ${namespace} -l ${labelSelector} -o wide`;
517
- console.log(`\n[K8sHelper] ─── Namespace Events ───`);
518
- await $ `oc get events -n ${namespace} --sort-by='.lastTimestamp'`;
519
517
  console.log(`\n[K8sHelper] ─── Pod Logs ───`);
520
518
  await $ `oc logs -n ${namespace} -l ${labelSelector} --all-containers --tail=100 2>&1 || true`;
521
519
  }
@@ -525,6 +523,75 @@ class KubernetesClientHelper {
525
523
  console.log(`\n[K8sHelper] ═══ End Pod Diagnostics ═══\n`);
526
524
  throw new Error(`Timeout waiting for pods (${labelSelector}) after ${timeoutSeconds}s`);
527
525
  }
526
+ /**
527
+ * Collects diagnostic logs for all resources in a namespace and saves them as files.
528
+ * Uses kubectl for cross-platform compatibility (works on OpenShift, EKS, GKE, etc.).
529
+ * OpenShift-specific resources (routes) are collected on a best-effort basis.
530
+ *
531
+ * @param namespace - Namespace to collect diagnostics from
532
+ * @param outputDir - Directory to write log files to (defaults to playwright-report/logs/<namespace>)
533
+ */
534
+ async collectDiagnosticLogs(namespace, outputDir = path.join("node_modules", ".cache", "e2e-test-results", "logs", namespace)) {
535
+ fs.mkdirSync(outputDir, { recursive: true });
536
+ console.log(`[K8sHelper] Collecting diagnostic logs for "${namespace}" → ${outputDir}`);
537
+ const quiet = $({
538
+ stdio: ["pipe", "pipe", "pipe"],
539
+ timeout: "20s",
540
+ });
541
+ const save = async (filePath, cmd) => {
542
+ try {
543
+ const result = await cmd;
544
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
545
+ fs.writeFileSync(filePath, result.stdout);
546
+ }
547
+ catch {
548
+ // ignore — resource type may not exist on this cluster
549
+ }
550
+ };
551
+ await Promise.allSettled([
552
+ save(path.join(outputDir, "events.txt"), quiet `kubectl get events -n ${namespace} --sort-by='.lastTimestamp'`),
553
+ save(path.join(outputDir, "pods.txt"), quiet `kubectl get pods -n ${namespace} -o wide`),
554
+ save(path.join(outputDir, "describe-pods.txt"), quiet `kubectl describe pods -n ${namespace}`),
555
+ save(path.join(outputDir, "deployments.txt"), quiet `kubectl get deployments -n ${namespace} -o wide`),
556
+ save(path.join(outputDir, "describe-deployments.txt"), quiet `kubectl describe deployments -n ${namespace}`),
557
+ save(path.join(outputDir, "statefulsets.txt"), quiet `kubectl get statefulsets -n ${namespace} -o wide`),
558
+ save(path.join(outputDir, "routes.txt"), quiet `kubectl get routes -n ${namespace} -o wide`),
559
+ ]);
560
+ try {
561
+ const pods = (await this._k8sApi.listNamespacedPod({ namespace })).items;
562
+ const saveLogs = async (filePath, cmd) => {
563
+ try {
564
+ const result = await cmd;
565
+ if (result.stdout.trim()) {
566
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
567
+ fs.writeFileSync(filePath, result.stdout);
568
+ }
569
+ }
570
+ catch {
571
+ // ignore — container may not have started or no previous logs
572
+ }
573
+ };
574
+ await Promise.allSettled(pods
575
+ .filter((pod) => pod.metadata?.name)
576
+ .flatMap((pod) => {
577
+ const podName = pod.metadata.name;
578
+ const podDir = path.join(outputDir, "pods", podName);
579
+ const containers = [
580
+ ...(pod.spec?.initContainers ?? []),
581
+ ...(pod.spec?.containers ?? []),
582
+ ];
583
+ return containers
584
+ .filter((c) => c.name)
585
+ .flatMap((c) => [
586
+ saveLogs(path.join(podDir, `${c.name}.log`), quiet `kubectl logs ${podName} -n ${namespace} -c ${c.name}`),
587
+ saveLogs(path.join(podDir, `${c.name}.previous.log`), quiet `kubectl logs ${podName} -n ${namespace} -c ${c.name} --previous`),
588
+ ]);
589
+ }));
590
+ }
591
+ catch {
592
+ // ignore
593
+ }
594
+ }
528
595
  /**
529
596
  * Check if a pod is in a failure state. Returns failure info or null if healthy.
530
597
  */
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Loads secrets from HashiCorp Vault into process.env.
3
+ * Only runs when `VAULT=1` or `VAULT=true` is set. Handles OIDC login automatically.
4
+ *
5
+ * Fetches secrets from:
6
+ * - Global path: `<basePath>/global`
7
+ * - Per-workspace paths: `<basePath>/workspaces/<name>`
8
+ *
9
+ * Configure via env vars:
10
+ * - `VAULT_ADDR` — Vault server URL (default: https://vault.ci.openshift.org)
11
+ * - `VAULT_BASE_PATH` — Base path in Vault (default: selfservice/rhdh-plugin-export-overlays)
12
+ *
13
+ * Security: Only key names are logged, never secret values.
14
+ */
15
+ export declare function loadLocalVaultSecrets(): Promise<void>;
16
+ //# sourceMappingURL=vault.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vault.d.ts","sourceRoot":"","sources":["../../src/utils/vault.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;GAaG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAiE3D"}
@@ -0,0 +1,94 @@
1
+ import { $ } from "./bash.js";
2
+ const VAULT_ADDR_DEFAULT = "https://vault.ci.openshift.org";
3
+ const VAULT_BASE_PATH_DEFAULT = "selfservice/rhdh-plugin-export-overlays";
4
+ /**
5
+ * Loads secrets from HashiCorp Vault into process.env.
6
+ * Only runs when `VAULT=1` or `VAULT=true` is set. Handles OIDC login automatically.
7
+ *
8
+ * Fetches secrets from:
9
+ * - Global path: `<basePath>/global`
10
+ * - Per-workspace paths: `<basePath>/workspaces/<name>`
11
+ *
12
+ * Configure via env vars:
13
+ * - `VAULT_ADDR` — Vault server URL (default: https://vault.ci.openshift.org)
14
+ * - `VAULT_BASE_PATH` — Base path in Vault (default: selfservice/rhdh-plugin-export-overlays)
15
+ *
16
+ * Security: Only key names are logged, never secret values.
17
+ */
18
+ export async function loadLocalVaultSecrets() {
19
+ if (process.env.VAULT !== "1" && process.env.VAULT !== "true")
20
+ return;
21
+ const vaultAddr = process.env.VAULT_ADDR || VAULT_ADDR_DEFAULT;
22
+ const basePath = process.env.VAULT_BASE_PATH || VAULT_BASE_PATH_DEFAULT;
23
+ process.env.VAULT_ADDR = vaultAddr;
24
+ // Check vault CLI is installed
25
+ const whichResult = await vaultCmd `command -v vault`;
26
+ if (whichResult.exitCode !== 0) {
27
+ throw new Error("vault CLI not found. Install from https://developer.hashicorp.com/vault/downloads");
28
+ }
29
+ // Check if already logged in
30
+ const tokenCheck = await vaultCmd `vault token lookup`;
31
+ if (tokenCheck.exitCode !== 0) {
32
+ console.log("Vault: not logged in, starting OIDC login...");
33
+ // vault login needs inherited stdio for browser-based OIDC flow
34
+ await $ `vault login -no-print -method=oidc`;
35
+ const retryCheck = await vaultCmd `vault token lookup`;
36
+ if (retryCheck.exitCode !== 0) {
37
+ throw new Error("Vault login failed. Run manually:\n export VAULT_ADDR='" +
38
+ vaultAddr +
39
+ "'\n vault login -method=oidc");
40
+ }
41
+ }
42
+ // Check access by fetching global secrets first
43
+ const globalResult = await vaultCmd `vault kv get -format=json -mount=kv ${basePath}/global`;
44
+ if (globalResult.stderr.includes("permission denied")) {
45
+ console.log("Vault: permission denied. Request access in Slack: #rhdh-e2e-tests");
46
+ return;
47
+ }
48
+ console.log("Loading secrets from vault...");
49
+ // Load global secrets
50
+ loadSecretsFromResult(globalResult, "global");
51
+ // List and fetch per-workspace secrets
52
+ const listResult = await vaultCmd `vault kv list -format=json -mount=kv ${basePath}/workspaces`;
53
+ if (listResult.exitCode === 0) {
54
+ const workspaces = JSON.parse(listResult.stdout);
55
+ await Promise.all(workspaces.map((ws) => {
56
+ const name = ws.replace(/\/$/, "");
57
+ return exportSecretsFromPath(`${basePath}/workspaces/${name}`, name);
58
+ }));
59
+ }
60
+ else {
61
+ console.log(" No workspace-specific secrets found");
62
+ }
63
+ console.log("Vault secrets loaded successfully.");
64
+ }
65
+ /** Runs a shell command with piped stdio and nothrow, for capturing vault CLI output. */
66
+ const vaultCmd = $({
67
+ stdio: ["pipe", "pipe", "pipe"],
68
+ nothrow: true,
69
+ });
70
+ async function exportSecretsFromPath(vaultPath, label) {
71
+ const result = await vaultCmd `vault kv get -format=json -mount=kv ${vaultPath}`;
72
+ loadSecretsFromResult(result, label);
73
+ }
74
+ function loadSecretsFromResult(result, label) {
75
+ if (result.exitCode !== 0) {
76
+ console.log(` No secrets at: ${label}`);
77
+ return;
78
+ }
79
+ const json = JSON.parse(result.stdout);
80
+ const secrets = json?.data?.data;
81
+ if (!secrets) {
82
+ console.log(` No secrets at: ${label}`);
83
+ return;
84
+ }
85
+ console.log(` From: ${label}`);
86
+ for (const [key, value] of Object.entries(secrets)) {
87
+ if (key.startsWith("secretsync/"))
88
+ continue;
89
+ if (!key.startsWith("VAULT_"))
90
+ continue;
91
+ const safeKey = key.replace(/[.\-/]/g, "_");
92
+ process.env[safeKey] = value;
93
+ }
94
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@red-hat-developer-hub/e2e-test-utils",
3
- "version": "1.1.32",
3
+ "version": "1.1.34",
4
4
  "description": "Test utilities for RHDH E2E tests",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -107,6 +107,7 @@
107
107
  "@eslint/js": "10.0.1",
108
108
  "@keycloak/keycloak-admin-client": "26.5.6",
109
109
  "@kubernetes/client-node": "1.4.0",
110
+ "dotenv": "17.4.2",
110
111
  "eslint": "10.2.0",
111
112
  "eslint-plugin-check-file": "3.3.1",
112
113
  "eslint-plugin-playwright": "2.10.1",