@red-hat-developer-hub/e2e-test-utils 2.1.13 → 2.1.14
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.
- package/dist/playwright/global-setup.d.ts +5 -0
- package/dist/playwright/global-setup.d.ts.map +1 -1
- package/dist/playwright/global-setup.js +2 -4
- package/dist/playwright/helpers/github-session.test.js +1 -1
- package/dist/playwright/tests/global-setup.test.d.ts +2 -0
- package/dist/playwright/tests/global-setup.test.d.ts.map +1 -0
- package/dist/playwright/tests/global-setup.test.js +68 -0
- package/dist/secrets/bitwarden.d.ts +34 -0
- package/dist/secrets/bitwarden.d.ts.map +1 -0
- package/dist/secrets/bitwarden.js +193 -0
- package/dist/secrets/cli.d.ts +11 -0
- package/dist/secrets/cli.d.ts.map +1 -0
- package/dist/secrets/cli.js +98 -0
- package/dist/secrets/command.d.ts +15 -0
- package/dist/secrets/command.d.ts.map +1 -0
- package/dist/secrets/command.js +30 -0
- package/dist/secrets/config.d.ts +33 -0
- package/dist/secrets/config.d.ts.map +1 -0
- package/dist/secrets/config.js +156 -0
- package/dist/secrets/environment.d.ts +10 -0
- package/dist/secrets/environment.d.ts.map +1 -0
- package/dist/secrets/environment.js +53 -0
- package/dist/secrets/exec.d.ts +18 -0
- package/dist/secrets/exec.d.ts.map +1 -0
- package/dist/secrets/exec.js +50 -0
- package/dist/secrets/index.d.ts +5 -0
- package/dist/secrets/index.d.ts.map +1 -0
- package/dist/secrets/index.js +4 -0
- package/dist/secrets/tests/bitwarden.integration.test.d.ts +2 -0
- package/dist/secrets/tests/bitwarden.integration.test.d.ts.map +1 -0
- package/dist/secrets/tests/bitwarden.integration.test.js +50 -0
- package/dist/secrets/tests/bitwarden.test.d.ts +2 -0
- package/dist/secrets/tests/bitwarden.test.d.ts.map +1 -0
- package/dist/secrets/tests/bitwarden.test.js +418 -0
- package/dist/secrets/tests/cli.integration.test.d.ts +2 -0
- package/dist/secrets/tests/cli.integration.test.d.ts.map +1 -0
- package/dist/secrets/tests/cli.integration.test.js +70 -0
- package/dist/secrets/tests/cli.test.d.ts +2 -0
- package/dist/secrets/tests/cli.test.d.ts.map +1 -0
- package/dist/secrets/tests/cli.test.js +42 -0
- package/dist/secrets/tests/config.test.d.ts +2 -0
- package/dist/secrets/tests/config.test.d.ts.map +1 -0
- package/dist/secrets/tests/config.test.js +105 -0
- package/dist/secrets/tests/environment.test.d.ts +2 -0
- package/dist/secrets/tests/environment.test.d.ts.map +1 -0
- package/dist/secrets/tests/environment.test.js +119 -0
- package/dist/secrets/tests/exec.test.d.ts +2 -0
- package/dist/secrets/tests/exec.test.d.ts.map +1 -0
- package/dist/secrets/tests/exec.test.js +87 -0
- package/package.json +9 -2
- package/dist/utils/vault.d.ts +0 -16
- package/dist/utils/vault.d.ts.map +0 -1
- package/dist/utils/vault.js +0 -94
|
@@ -4,4 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { type FullConfig } from "@playwright/test";
|
|
6
6
|
export default function globalSetup(config: FullConfig): Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* Loads .env files from each project's e2e-tests directory.
|
|
9
|
+
* Uses `override: true` so local .env values take priority over inherited values.
|
|
10
|
+
*/
|
|
11
|
+
export declare function loadDotenvFromProjects(config: FullConfig): void;
|
|
7
12
|
//# 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;AAEH,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;
|
|
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,CAO3E;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAS/D"}
|
|
@@ -9,7 +9,6 @@ import { $ } from "../utils/bash.js";
|
|
|
9
9
|
import { KeycloakHelper } from "../deployment/keycloak/index.js";
|
|
10
10
|
import { installRHDHOperator } from "../deployment/rhdh/operator-setup.js";
|
|
11
11
|
import { DEFAULT_KEYCLOAK_CONFIG, DEFAULT_RHDH_CLIENT, DEFAULT_USERS, } from "../deployment/keycloak/constants.js";
|
|
12
|
-
import { loadLocalVaultSecrets } from "../utils/vault.js";
|
|
13
12
|
const REQUIRED_BINARIES = ["oc", "kubectl", "helm"];
|
|
14
13
|
async function checkRequiredBinaries() {
|
|
15
14
|
const missingBinaries = [];
|
|
@@ -65,7 +64,6 @@ async function deployKeycloak() {
|
|
|
65
64
|
export default async function globalSetup(config) {
|
|
66
65
|
console.log("Running global setup...");
|
|
67
66
|
await checkRequiredBinaries();
|
|
68
|
-
await loadLocalVaultSecrets();
|
|
69
67
|
loadDotenvFromProjects(config);
|
|
70
68
|
await setClusterRouterBaseEnv();
|
|
71
69
|
await Promise.all([installRHDHOperator(), deployKeycloak()]);
|
|
@@ -73,9 +71,9 @@ export default async function globalSetup(config) {
|
|
|
73
71
|
}
|
|
74
72
|
/**
|
|
75
73
|
* Loads .env files from each project's e2e-tests directory.
|
|
76
|
-
* Uses `override: true` so local .env values take priority over
|
|
74
|
+
* Uses `override: true` so local .env values take priority over inherited values.
|
|
77
75
|
*/
|
|
78
|
-
function loadDotenvFromProjects(config) {
|
|
76
|
+
export function loadDotenvFromProjects(config) {
|
|
79
77
|
const seen = new Set();
|
|
80
78
|
for (const project of config.projects) {
|
|
81
79
|
// testDir points to e2e-tests/tests, go up one level to e2e-tests/
|
|
@@ -25,7 +25,7 @@ describe("github session file naming", () => {
|
|
|
25
25
|
// Otherwise it becomes a directory that does not exist and the write fails.
|
|
26
26
|
assert.strictEqual(path.dirname(githubSessionFile("org/user")), process.cwd());
|
|
27
27
|
});
|
|
28
|
-
it("does not throw when the
|
|
28
|
+
it("does not throw when the GitHub user id is unset", () => {
|
|
29
29
|
// loginAsGithubUser defaults to `process.env.VAULT_GH_USER_ID as string`, and the
|
|
30
30
|
// cast hides the undefined. Building the path must not be where that surfaces —
|
|
31
31
|
// a TypeError here points nowhere near the missing variable.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"global-setup.test.d.ts","sourceRoot":"","sources":["../../../src/playwright/tests/global-setup.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/* eslint-disable playwright/expect-expect, playwright/no-conditional-in-test -- this is a node:test regression suite */
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import { loadDotenvFromProjects } from "../global-setup.js";
|
|
8
|
+
test("workspace .env values override inherited values", async (context) => {
|
|
9
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "global-setup-test-"));
|
|
10
|
+
const e2eRoot = path.join(root, "e2e-tests");
|
|
11
|
+
const testDir = path.join(e2eRoot, "tests");
|
|
12
|
+
await mkdir(testDir, { recursive: true });
|
|
13
|
+
await writeFile(path.join(e2eRoot, ".env"), "VAULT_GITHUB_TOKEN=dotenv-value\nBW_SESSION=dotenv-session\nVAULT_TOKEN=dotenv-token\nLOCAL_ONLY=dotenv-only\n");
|
|
14
|
+
const previousToken = process.env.VAULT_GITHUB_TOKEN;
|
|
15
|
+
const previousSession = process.env.BW_SESSION;
|
|
16
|
+
const previousProviderToken = process.env.VAULT_TOKEN;
|
|
17
|
+
const previousLocal = process.env.LOCAL_ONLY;
|
|
18
|
+
const previousCi = process.env.CI;
|
|
19
|
+
try {
|
|
20
|
+
await context.test("local .env values override inherited values", () => {
|
|
21
|
+
delete process.env.CI;
|
|
22
|
+
process.env.VAULT_GITHUB_TOKEN = "bitwarden-value";
|
|
23
|
+
process.env.BW_SESSION = "parent-session";
|
|
24
|
+
process.env.VAULT_TOKEN = "parent-token";
|
|
25
|
+
delete process.env.LOCAL_ONLY;
|
|
26
|
+
loadDotenvFromProjects({ projects: [{ testDir }] });
|
|
27
|
+
assert.equal(process.env.VAULT_GITHUB_TOKEN, "dotenv-value");
|
|
28
|
+
assert.equal(process.env.BW_SESSION, "dotenv-session");
|
|
29
|
+
assert.equal(process.env.VAULT_TOKEN, "dotenv-token");
|
|
30
|
+
assert.equal(process.env.LOCAL_ONLY, "dotenv-only");
|
|
31
|
+
});
|
|
32
|
+
await context.test("workspace .env values override CI values", () => {
|
|
33
|
+
process.env.CI = "true";
|
|
34
|
+
process.env.VAULT_GITHUB_TOKEN = "ci-value";
|
|
35
|
+
process.env.BW_SESSION = "ci-session";
|
|
36
|
+
process.env.VAULT_TOKEN = "ci-token";
|
|
37
|
+
delete process.env.LOCAL_ONLY;
|
|
38
|
+
loadDotenvFromProjects({ projects: [{ testDir }] });
|
|
39
|
+
assert.equal(process.env.VAULT_GITHUB_TOKEN, "dotenv-value");
|
|
40
|
+
assert.equal(process.env.BW_SESSION, "dotenv-session");
|
|
41
|
+
assert.equal(process.env.VAULT_TOKEN, "dotenv-token");
|
|
42
|
+
assert.equal(process.env.LOCAL_ONLY, "dotenv-only");
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
if (previousToken === undefined)
|
|
47
|
+
delete process.env.VAULT_GITHUB_TOKEN;
|
|
48
|
+
else
|
|
49
|
+
process.env.VAULT_GITHUB_TOKEN = previousToken;
|
|
50
|
+
if (previousSession === undefined)
|
|
51
|
+
delete process.env.BW_SESSION;
|
|
52
|
+
else
|
|
53
|
+
process.env.BW_SESSION = previousSession;
|
|
54
|
+
if (previousProviderToken === undefined)
|
|
55
|
+
delete process.env.VAULT_TOKEN;
|
|
56
|
+
else
|
|
57
|
+
process.env.VAULT_TOKEN = previousProviderToken;
|
|
58
|
+
if (previousLocal === undefined)
|
|
59
|
+
delete process.env.LOCAL_ONLY;
|
|
60
|
+
else
|
|
61
|
+
process.env.LOCAL_ONLY = previousLocal;
|
|
62
|
+
if (previousCi === undefined)
|
|
63
|
+
delete process.env.CI;
|
|
64
|
+
else
|
|
65
|
+
process.env.CI = previousCi;
|
|
66
|
+
await rm(root, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type CommandResult } from "./command.js";
|
|
2
|
+
import { type ExpandedSecretSelector, type ReadableCollectionId } from "./config.js";
|
|
3
|
+
export interface BitwardenCommandRunner {
|
|
4
|
+
(command: string, args: readonly string[], options?: {
|
|
5
|
+
env?: NodeJS.ProcessEnv;
|
|
6
|
+
input?: string;
|
|
7
|
+
}): Promise<CommandResult>;
|
|
8
|
+
}
|
|
9
|
+
export interface BitwardenSecret {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
value: string;
|
|
13
|
+
selector: ExpandedSecretSelector;
|
|
14
|
+
}
|
|
15
|
+
export interface BitwardenAttachment {
|
|
16
|
+
id: string;
|
|
17
|
+
fileName: string;
|
|
18
|
+
}
|
|
19
|
+
export interface BitwardenClientOptions {
|
|
20
|
+
command?: string;
|
|
21
|
+
env?: NodeJS.ProcessEnv;
|
|
22
|
+
runner?: BitwardenCommandRunner;
|
|
23
|
+
}
|
|
24
|
+
export declare class BitwardenClient {
|
|
25
|
+
private readonly command;
|
|
26
|
+
private readonly env;
|
|
27
|
+
private readonly runner;
|
|
28
|
+
constructor(options?: BitwardenClientOptions);
|
|
29
|
+
read(collectionId: ReadableCollectionId, selectors: readonly ExpandedSecretSelector[]): Promise<BitwardenSecret[]>;
|
|
30
|
+
private resolveCollection;
|
|
31
|
+
private readSelector;
|
|
32
|
+
private runOrThrow;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=bitwarden.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bitwarden.d.ts","sourceRoot":"","sources":["../../src/secrets/bitwarden.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EAEL,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EAC1B,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,sBAAsB;IACrC,CACE,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,CAAC,EAAE;QACR,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;QACxB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,GACA,OAAO,CAAC,aAAa,CAAC,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,sBAAsB,CAAC;CAClC;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;CAClB;AAQD,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,MAAM,CAAC,EAAE,sBAAsB,CAAC;CACjC;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAoB;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAyB;gBAEpC,OAAO,GAAE,sBAA2B;IAM1C,IAAI,CACR,YAAY,EAAE,oBAAoB,EAClC,SAAS,EAAE,SAAS,sBAAsB,EAAE,GAC3C,OAAO,CAAC,eAAe,EAAE,CAAC;YA4Cf,iBAAiB;YA0CjB,YAAY;YA0HZ,UAAU;CAazB"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { runCommand } from "./command.js";
|
|
2
|
+
import { getCollectionMapping, } from "./config.js";
|
|
3
|
+
export class BitwardenClient {
|
|
4
|
+
command;
|
|
5
|
+
env;
|
|
6
|
+
runner;
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.command = options.command ?? "bw";
|
|
9
|
+
this.env = { ...process.env, ...options.env };
|
|
10
|
+
this.runner = options.runner ?? runCommand;
|
|
11
|
+
}
|
|
12
|
+
async read(collectionId, selectors) {
|
|
13
|
+
const mapping = getCollectionMapping(collectionId);
|
|
14
|
+
if (!this.env.BW_SESSION?.trim()) {
|
|
15
|
+
throw new Error("BW_SESSION is required and must contain an unlocked Bitwarden session");
|
|
16
|
+
}
|
|
17
|
+
await this.runOrThrow(["--version"], "Bitwarden CLI is unavailable");
|
|
18
|
+
const status = await this.runOrThrow(["status"], "Bitwarden session status could not be checked");
|
|
19
|
+
const statusJson = parseJson(status, "Bitwarden status");
|
|
20
|
+
if (!isRecord(statusJson) || statusJson.status !== "unlocked") {
|
|
21
|
+
throw new Error("BW_SESSION is missing or Bitwarden is not unlocked");
|
|
22
|
+
}
|
|
23
|
+
await this.runOrThrow(["sync"], "Bitwarden sync failed");
|
|
24
|
+
const collection = await this.resolveCollection(mapping.bitwardenCollection);
|
|
25
|
+
const result = [];
|
|
26
|
+
const names = new Set();
|
|
27
|
+
for (const selector of selectors) {
|
|
28
|
+
const items = await this.readSelector(collection, selector);
|
|
29
|
+
if (items.length === 0 && !selector.optional) {
|
|
30
|
+
throw new Error(`Required prefix has no matching item: ${selector.prefix}`);
|
|
31
|
+
}
|
|
32
|
+
for (const item of items) {
|
|
33
|
+
if (names.has(item.name)) {
|
|
34
|
+
throw new Error(`Duplicate Bitwarden item name: ${item.name}`);
|
|
35
|
+
}
|
|
36
|
+
names.add(item.name);
|
|
37
|
+
result.push(item);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
async resolveCollection(collectionName) {
|
|
43
|
+
const result = await this.runOrThrow(["list", "collections"], "Bitwarden collection listing failed");
|
|
44
|
+
const values = parseJson(result, "Bitwarden collection list");
|
|
45
|
+
if (!Array.isArray(values)) {
|
|
46
|
+
throw new Error("Bitwarden returned an invalid collection list");
|
|
47
|
+
}
|
|
48
|
+
const matches = values.filter((value) => isRecord(value) && value.name === collectionName);
|
|
49
|
+
if (matches.length === 0) {
|
|
50
|
+
throw new Error(`Bitwarden collection not found: ${collectionName}`);
|
|
51
|
+
}
|
|
52
|
+
if (matches.length > 1) {
|
|
53
|
+
throw new Error(`Bitwarden collection name is ambiguous: ${collectionName}`);
|
|
54
|
+
}
|
|
55
|
+
const match = matches[0];
|
|
56
|
+
if (typeof match.id !== "string" ||
|
|
57
|
+
typeof match.name !== "string" ||
|
|
58
|
+
typeof match.organizationId !== "string") {
|
|
59
|
+
throw new Error(`Bitwarden collection has invalid metadata: ${collectionName}`);
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
id: match.id,
|
|
63
|
+
name: match.name,
|
|
64
|
+
organizationId: match.organizationId,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async readSelector(collection, selector) {
|
|
68
|
+
const result = await this.runOrThrow([
|
|
69
|
+
"list",
|
|
70
|
+
"items",
|
|
71
|
+
"--collectionid",
|
|
72
|
+
collection.id,
|
|
73
|
+
"--search",
|
|
74
|
+
selector.prefix,
|
|
75
|
+
], `Bitwarden item listing failed for ${selector.prefix}`);
|
|
76
|
+
const values = parseJson(result, `Bitwarden item list for ${selector.prefix}`);
|
|
77
|
+
if (!Array.isArray(values)) {
|
|
78
|
+
throw new Error(`Bitwarden returned an invalid item list for ${selector.prefix}`);
|
|
79
|
+
}
|
|
80
|
+
const ids = values.flatMap((value) => {
|
|
81
|
+
if (!isRecord(value) ||
|
|
82
|
+
typeof value.id !== "string" ||
|
|
83
|
+
value.id.length === 0) {
|
|
84
|
+
throw new Error(`Bitwarden returned an item without an id for ${selector.prefix}`);
|
|
85
|
+
}
|
|
86
|
+
if (typeof value.name === "string" &&
|
|
87
|
+
!value.name.startsWith(selector.prefix)) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
return [value.id];
|
|
91
|
+
});
|
|
92
|
+
if (new Set(ids).size !== ids.length) {
|
|
93
|
+
throw new Error(`Duplicate Bitwarden item id for ${selector.prefix}`);
|
|
94
|
+
}
|
|
95
|
+
const items = [];
|
|
96
|
+
for (const id of ids) {
|
|
97
|
+
const itemResult = await this.runOrThrow(["get", "item", id], `Bitwarden item read failed for ${selector.prefix}`);
|
|
98
|
+
const value = parseJson(itemResult, `Bitwarden item ${id}`);
|
|
99
|
+
if (!isRecord(value)) {
|
|
100
|
+
throw new Error(`Bitwarden returned an invalid item for ${selector.prefix}`);
|
|
101
|
+
}
|
|
102
|
+
if (typeof value.name !== "string" ||
|
|
103
|
+
!value.name.startsWith(selector.prefix)) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (value.id !== id ||
|
|
107
|
+
value.type !== 2 ||
|
|
108
|
+
!Array.isArray(value.collectionIds) ||
|
|
109
|
+
value.collectionIds.length !== 1 ||
|
|
110
|
+
value.collectionIds[0] !== collection.id ||
|
|
111
|
+
value.organizationId !== collection.organizationId) {
|
|
112
|
+
throw new Error(`Bitwarden item ${typeof value.name === "string" ? value.name : id} is not a secure note in the selected collection`);
|
|
113
|
+
}
|
|
114
|
+
const attachment = parseAttachment(value.attachments, selector.prefix);
|
|
115
|
+
if (attachment === undefined) {
|
|
116
|
+
if (typeof value.notes !== "string") {
|
|
117
|
+
throw new Error(`Bitwarden item ${value.name} is not a secure note in the selected collection`);
|
|
118
|
+
}
|
|
119
|
+
items.push({
|
|
120
|
+
id,
|
|
121
|
+
name: value.name,
|
|
122
|
+
value: value.notes,
|
|
123
|
+
selector,
|
|
124
|
+
});
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (value.notes !== null &&
|
|
128
|
+
(typeof value.notes !== "string" || value.notes.length > 0)) {
|
|
129
|
+
throw new Error(`Bitwarden item notes must be null or empty when an attachment is present for ${selector.prefix}`);
|
|
130
|
+
}
|
|
131
|
+
if (attachment.fileName !== attachmentFileName(value.name)) {
|
|
132
|
+
throw new Error(`Bitwarden attachment filename does not match the item basename for ${selector.prefix}`);
|
|
133
|
+
}
|
|
134
|
+
const attachmentResult = await this.runOrThrow(["get", "attachment", attachment.fileName, "--itemid", id, "--raw"], `Bitwarden attachment read failed for ${selector.prefix}`);
|
|
135
|
+
items.push({
|
|
136
|
+
id,
|
|
137
|
+
name: value.name,
|
|
138
|
+
value: attachmentResult.stdout,
|
|
139
|
+
selector,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return items;
|
|
143
|
+
}
|
|
144
|
+
async runOrThrow(args, message) {
|
|
145
|
+
let result;
|
|
146
|
+
try {
|
|
147
|
+
result = await this.runner(this.command, args, { env: this.env });
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
throw new Error(message);
|
|
151
|
+
}
|
|
152
|
+
if (result.status !== 0)
|
|
153
|
+
throw new Error(message);
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function parseJson(result, label) {
|
|
158
|
+
try {
|
|
159
|
+
return JSON.parse(result.stdout);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
throw new Error(`${label} returned invalid JSON`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function isRecord(value) {
|
|
166
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
167
|
+
}
|
|
168
|
+
function parseAttachment(value, prefix) {
|
|
169
|
+
if (value === undefined || value === null)
|
|
170
|
+
return undefined;
|
|
171
|
+
if (!Array.isArray(value)) {
|
|
172
|
+
throw new Error(`Bitwarden item has invalid attachment metadata for ${prefix}`);
|
|
173
|
+
}
|
|
174
|
+
if (value.length === 0)
|
|
175
|
+
return undefined;
|
|
176
|
+
if (value.length !== 1) {
|
|
177
|
+
throw new Error(`Bitwarden item must have exactly one attachment for ${prefix}`);
|
|
178
|
+
}
|
|
179
|
+
const attachment = value[0];
|
|
180
|
+
if (!isRecord(attachment) ||
|
|
181
|
+
typeof attachment.id !== "string" ||
|
|
182
|
+
attachment.id.length === 0 ||
|
|
183
|
+
typeof attachment.fileName !== "string" ||
|
|
184
|
+
attachment.fileName.length === 0) {
|
|
185
|
+
throw new Error(`Bitwarden item has invalid attachment metadata for ${prefix}`);
|
|
186
|
+
}
|
|
187
|
+
return { id: attachment.id, fileName: attachment.fileName };
|
|
188
|
+
}
|
|
189
|
+
function attachmentFileName(name) {
|
|
190
|
+
const basename = name.split("/").at(-1) ?? name;
|
|
191
|
+
const sanitized = basename.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
192
|
+
return sanitized || "secret";
|
|
193
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export interface ExecCliArguments {
|
|
3
|
+
command: "exec";
|
|
4
|
+
profilePath: string;
|
|
5
|
+
workspaces: string[];
|
|
6
|
+
executable: string;
|
|
7
|
+
args: string[];
|
|
8
|
+
}
|
|
9
|
+
export declare function parseCliArguments(argv: readonly string[]): ExecCliArguments;
|
|
10
|
+
export declare function main(argv?: readonly string[]): Promise<number>;
|
|
11
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/secrets/cli.ts"],"names":[],"mappings":";AASA,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAUD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,gBAAgB,CAkD3E;AAED,wBAAsB,IAAI,CACxB,IAAI,GAAE,SAAS,MAAM,EAA0B,GAC9C,OAAO,CAAC,MAAM,CAAC,CAwBjB"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { executeCommand } from "./exec.js";
|
|
7
|
+
import { parseProfile } from "./config.js";
|
|
8
|
+
const HELP = `Usage:
|
|
9
|
+
rhdh-e2e-secrets exec --profile <profile.json> [--workspace <name> ...] -- <command> [args...]
|
|
10
|
+
|
|
11
|
+
Requirements:
|
|
12
|
+
BW_SESSION must contain an already unlocked Bitwarden CLI session.
|
|
13
|
+
The bw CLI must be installed and available on PATH.
|
|
14
|
+
`;
|
|
15
|
+
export function parseCliArguments(argv) {
|
|
16
|
+
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
17
|
+
throw new Error(HELP);
|
|
18
|
+
}
|
|
19
|
+
if (argv[0] !== "exec") {
|
|
20
|
+
throw new Error(`Unsupported command: ${argv[0]}`);
|
|
21
|
+
}
|
|
22
|
+
const delimiter = argv.indexOf("--");
|
|
23
|
+
if (delimiter === -1) {
|
|
24
|
+
throw new Error("A command after -- is required");
|
|
25
|
+
}
|
|
26
|
+
const options = argv.slice(1, delimiter);
|
|
27
|
+
const command = argv[delimiter + 1];
|
|
28
|
+
const args = argv.slice(delimiter + 2);
|
|
29
|
+
let profilePath;
|
|
30
|
+
const workspaces = [];
|
|
31
|
+
for (let index = 0; index < options.length; index++) {
|
|
32
|
+
const option = options[index];
|
|
33
|
+
if (option === "--profile" || option === "--workspace") {
|
|
34
|
+
const value = options[++index];
|
|
35
|
+
if (!value)
|
|
36
|
+
throw new Error(`${option} requires a value`);
|
|
37
|
+
if (option === "--profile")
|
|
38
|
+
profilePath = value;
|
|
39
|
+
else
|
|
40
|
+
workspaces.push(value);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (option.startsWith("--profile=")) {
|
|
44
|
+
profilePath = option.slice("--profile=".length);
|
|
45
|
+
if (!profilePath)
|
|
46
|
+
throw new Error("--profile requires a value");
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (option.startsWith("--workspace=")) {
|
|
50
|
+
const workspace = option.slice("--workspace=".length);
|
|
51
|
+
if (!workspace)
|
|
52
|
+
throw new Error("--workspace requires a value");
|
|
53
|
+
workspaces.push(workspace);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
throw new Error(`Unknown option: ${option}`);
|
|
57
|
+
}
|
|
58
|
+
if (!profilePath)
|
|
59
|
+
throw new Error("--profile is required");
|
|
60
|
+
if (!command)
|
|
61
|
+
throw new Error("A non-empty command after -- is required");
|
|
62
|
+
return {
|
|
63
|
+
command: "exec",
|
|
64
|
+
profilePath,
|
|
65
|
+
workspaces,
|
|
66
|
+
executable: command,
|
|
67
|
+
args,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
71
|
+
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
72
|
+
console.log(HELP);
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const parsed = parseCliArguments(argv);
|
|
77
|
+
const profileValue = JSON.parse(await readFile(resolve(parsed.profilePath), "utf8"));
|
|
78
|
+
const profile = parseProfile(profileValue);
|
|
79
|
+
return await executeCommand({
|
|
80
|
+
profile,
|
|
81
|
+
workspaces: parsed.workspaces,
|
|
82
|
+
command: parsed.executable,
|
|
83
|
+
args: parsed.args,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
console.error(error instanceof Error ? error.message : "Secret execution failed");
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (process.argv[1] &&
|
|
92
|
+
realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
|
|
93
|
+
void main().then((exitCode) => {
|
|
94
|
+
process.exitCode = exitCode;
|
|
95
|
+
}, () => {
|
|
96
|
+
process.exitCode = 1;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface CommandResult {
|
|
2
|
+
status: number | null;
|
|
3
|
+
stdout: string;
|
|
4
|
+
stderr: string;
|
|
5
|
+
error?: Error;
|
|
6
|
+
}
|
|
7
|
+
export interface CommandOptions {
|
|
8
|
+
cwd?: string;
|
|
9
|
+
env?: NodeJS.ProcessEnv;
|
|
10
|
+
input?: string;
|
|
11
|
+
stdio?: "pipe" | "inherit";
|
|
12
|
+
}
|
|
13
|
+
export type CommandRunner = (command: string, args: readonly string[], options?: CommandOptions) => Promise<CommandResult>;
|
|
14
|
+
export declare const runCommand: CommandRunner;
|
|
15
|
+
//# sourceMappingURL=command.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../../src/secrets/command.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED,MAAM,MAAM,aAAa,GAAG,CAC1B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,CAAC,EAAE,cAAc,KACrB,OAAO,CAAC,aAAa,CAAC,CAAC;AAE5B,eAAO,MAAM,UAAU,EAAE,aA6BrB,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export const runCommand = (command, args, options = {}) => new Promise((resolve) => {
|
|
3
|
+
const child = spawn(command, args, {
|
|
4
|
+
cwd: options.cwd,
|
|
5
|
+
env: options.env,
|
|
6
|
+
stdio: options.stdio === "inherit" ? "inherit" : "pipe",
|
|
7
|
+
});
|
|
8
|
+
let stdout = "";
|
|
9
|
+
let stderr = "";
|
|
10
|
+
if (options.stdio !== "inherit") {
|
|
11
|
+
child.stdout?.setEncoding("utf8");
|
|
12
|
+
child.stderr?.setEncoding("utf8");
|
|
13
|
+
child.stdout?.on("data", (chunk) => {
|
|
14
|
+
stdout += chunk;
|
|
15
|
+
});
|
|
16
|
+
child.stderr?.on("data", (chunk) => {
|
|
17
|
+
stderr += chunk;
|
|
18
|
+
});
|
|
19
|
+
if (options.input !== undefined)
|
|
20
|
+
child.stdin?.end(options.input);
|
|
21
|
+
else
|
|
22
|
+
child.stdin?.end();
|
|
23
|
+
}
|
|
24
|
+
child.once("error", (error) => {
|
|
25
|
+
resolve({ status: null, stdout, stderr, error });
|
|
26
|
+
});
|
|
27
|
+
child.once("close", (status) => {
|
|
28
|
+
resolve({ status, stdout, stderr });
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type ReadableCollectionId = "rhdh-qe" | "rhdh-test-instance" | "rhdh-plugin-export-overlays";
|
|
2
|
+
export interface CollectionMapping {
|
|
3
|
+
id: ReadableCollectionId;
|
|
4
|
+
bitwardenCollection: string;
|
|
5
|
+
}
|
|
6
|
+
export interface SecretProfile {
|
|
7
|
+
schemaVersion: 1;
|
|
8
|
+
collection: ReadableCollectionId;
|
|
9
|
+
selectors: readonly SecretSelector[];
|
|
10
|
+
}
|
|
11
|
+
export interface SecretSelector {
|
|
12
|
+
prefix: string;
|
|
13
|
+
destination: EnvironmentDestination;
|
|
14
|
+
optional?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface EnvironmentDestination {
|
|
17
|
+
kind: "environment";
|
|
18
|
+
stripPrefix: string;
|
|
19
|
+
requirePrefix?: string;
|
|
20
|
+
keyTransform: "identity" | "legacy-env";
|
|
21
|
+
}
|
|
22
|
+
export interface ExpandedSecretSelector extends SecretSelector {
|
|
23
|
+
optional: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface ExpandedSecretProfile {
|
|
26
|
+
schemaVersion: 1;
|
|
27
|
+
collection: ReadableCollectionId;
|
|
28
|
+
selectors: readonly ExpandedSecretSelector[];
|
|
29
|
+
}
|
|
30
|
+
export declare function getCollectionMapping(collection: string): CollectionMapping;
|
|
31
|
+
export declare function parseProfile(value: unknown): SecretProfile;
|
|
32
|
+
export declare function expandProfile(profile: SecretProfile, workspaces?: readonly string[]): ExpandedSecretProfile;
|
|
33
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/secrets/config.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,oBAAoB,GAC5B,SAAS,GACT,oBAAoB,GACpB,6BAA6B,CAAC;AAElC,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,oBAAoB,CAAC;IACzB,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE,CAAC,CAAC;IACjB,UAAU,EAAE,oBAAoB,CAAC;IACjC,SAAS,EAAE,SAAS,cAAc,EAAE,CAAC;CACtC;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,sBAAsB,CAAC;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,aAAa,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,UAAU,GAAG,YAAY,CAAC;CACzC;AAED,MAAM,WAAW,sBAAuB,SAAQ,cAAc;IAC5D,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,CAAC,CAAC;IACjB,UAAU,EAAE,oBAAoB,CAAC;IACjC,SAAS,EAAE,SAAS,sBAAsB,EAAE,CAAC;CAC9C;AAqBD,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,iBAAiB,CAY1E;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,aAAa,CAuB1D;AAED,wBAAgB,aAAa,CAC3B,OAAO,EAAE,aAAa,EACtB,UAAU,GAAE,SAAS,MAAM,EAAO,GACjC,qBAAqB,CA4CvB"}
|