@catladder/cli 0.0.0-main-0d7b031b → 0.0.0-main-018e2dee
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/apps/cli/src/apps/cli/commands/project/doctor/checkLocalAccess.js +1 -0
- package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkLocalAccess.js.map +1 -1
- package/dist/apps/cli/src/apps/cli/commands/project/setup/setupLocalAccess.d.ts +1 -1
- package/dist/apps/cli/src/apps/cli/commands/project/setup/setupLocalAccess.js +3 -1
- package/dist/apps/cli/src/apps/cli/commands/project/setup/setupLocalAccess.js.map +1 -1
- package/dist/apps/cli/src/localAccess/declared.d.ts +6 -0
- package/dist/apps/cli/src/localAccess/declared.js +15 -1
- package/dist/apps/cli/src/localAccess/declared.js.map +1 -1
- package/dist/apps/cli/src/localAccess/keyFiles.d.ts +4 -2
- package/dist/apps/cli/src/localAccess/keyFiles.js +12 -8
- package/dist/apps/cli/src/localAccess/keyFiles.js.map +1 -1
- package/dist/bundles/catenv/index.js +56 -10
- package/dist/bundles/cli/index.js +60 -11
- package/dist/bundles/skills/catladder-secrets/SKILL.md +11 -0
- package/dist/packages/pipeline/src/localAccess/index.d.ts +7 -0
- package/dist/packages/pipeline/src/localAccess/index.js +29 -1
- package/dist/packages/pipeline/src/localAccess/index.js.map +1 -1
- package/dist/packages/pipeline/src/types/config.d.ts +20 -2
- package/dist/skills/catladder-secrets/SKILL.md +11 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/apps/cli/commands/project/doctor/{checkLocalAccess.test.ts → __tests__/checkLocalAccess.test.ts} +19 -4
- package/src/apps/cli/commands/project/doctor/checkLocalAccess.ts +2 -0
- package/src/apps/cli/commands/project/setup/setupLocalAccess.ts +4 -1
- package/src/localAccess/__tests__/keyFiles.test.ts +59 -0
- package/src/localAccess/declared.ts +20 -1
- package/src/localAccess/keyFiles.ts +21 -9
package/package.json
CHANGED
|
@@ -2,15 +2,15 @@ import type { ComponentContext, Config } from "@catladder/pipeline";
|
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
|
|
4
4
|
const state = { exists: true, keys: ["k1"] as string[] };
|
|
5
|
-
vi.mock("
|
|
5
|
+
vi.mock("../../../../../../gcloud/serviceAccounts", () => ({
|
|
6
6
|
accountExists: async () => state.exists,
|
|
7
7
|
}));
|
|
8
|
-
vi.mock("
|
|
8
|
+
vi.mock("../../../../../../gcloud/localAccess", async (orig) => ({
|
|
9
9
|
...(await orig<object>()),
|
|
10
10
|
listUserManagedKeyIds: async () => state.keys,
|
|
11
11
|
}));
|
|
12
12
|
|
|
13
|
-
import { checkLocalAccess } from "
|
|
13
|
+
import { checkLocalAccess } from "../checkLocalAccess";
|
|
14
14
|
|
|
15
15
|
const cloudRun = { type: "google-cloudrun", projectId: "p", region: "r" };
|
|
16
16
|
|
|
@@ -27,6 +27,7 @@ const config = (extra: Partial<Config> = {}) =>
|
|
|
27
27
|
customerName: "acme",
|
|
28
28
|
components: { web: { dir: ".", build: false, deploy: cloudRun } },
|
|
29
29
|
localAccess: { gcp: { presets: ["logs"] } },
|
|
30
|
+
secrets: { vault: { type: "bitwarden" } },
|
|
30
31
|
...extra,
|
|
31
32
|
}) as Config;
|
|
32
33
|
|
|
@@ -99,7 +100,7 @@ describe("checkLocalAccess", () => {
|
|
|
99
100
|
});
|
|
100
101
|
|
|
101
102
|
it("refuses the gitlab vault", async () => {
|
|
102
|
-
const out = await run(config({
|
|
103
|
+
const out = await run(config({ secrets: { vault: "gitlab" } } as any));
|
|
103
104
|
expect(out.join("\n")).toMatch(/fail:.*not a CI variable store/);
|
|
104
105
|
});
|
|
105
106
|
|
|
@@ -109,4 +110,18 @@ describe("checkLocalAccess", () => {
|
|
|
109
110
|
);
|
|
110
111
|
expect(out.join("\n")).toMatch(/fail:.*not allowed/);
|
|
111
112
|
});
|
|
113
|
+
|
|
114
|
+
it("reports a credentialsEnvVar exported by several envs", async () => {
|
|
115
|
+
const out = await run(
|
|
116
|
+
config({
|
|
117
|
+
localAccess: {
|
|
118
|
+
gcp: {
|
|
119
|
+
presets: ["logs"],
|
|
120
|
+
credentialsEnvVar: "GOOGLE_APPLICATION_CREDENTIALS",
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
expect(out.join("\n")).toMatch(/fail:.*declared by both/);
|
|
126
|
+
});
|
|
112
127
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ComponentContext, Config } from "@catladder/pipeline";
|
|
2
2
|
import {
|
|
3
3
|
diffRoleBindings,
|
|
4
|
+
getLocalAccessGcpCredentialsEnvVars,
|
|
4
5
|
getLocalAccessGcpRoles,
|
|
5
6
|
getVaultConfig,
|
|
6
7
|
isOfDeployType,
|
|
@@ -33,6 +34,7 @@ export const checkLocalAccess = async (
|
|
|
33
34
|
let declared: string[];
|
|
34
35
|
try {
|
|
35
36
|
declared = getLocalAccessGcpRoles(config, context.env);
|
|
37
|
+
getLocalAccessGcpCredentialsEnvVars(config, context.name);
|
|
36
38
|
} catch (e) {
|
|
37
39
|
return { label, invalid: e.message as string };
|
|
38
40
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ComponentContext } from "@catladder/pipeline";
|
|
2
2
|
import {
|
|
3
|
+
getLocalAccessGcpCredentialsEnvVars,
|
|
3
4
|
getLocalAccessGcpRoles,
|
|
4
5
|
getVaultConfig,
|
|
5
6
|
isOfDeployType,
|
|
@@ -9,7 +10,7 @@ import type { IO } from "../../../../../core/types";
|
|
|
9
10
|
import { syncLocalAccessServiceAccount } from "../../../../../gcloud/localAccess";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
* provisions the
|
|
13
|
+
* provisions the local access (`localAccess` in catladder.ts)
|
|
13
14
|
* of a google cloud run component/env: a dedicated service account with
|
|
14
15
|
* exactly the declared bindings and its key in the secret store. Also
|
|
15
16
|
* removes what is no longer declared.
|
|
@@ -22,6 +23,8 @@ export const setupLocalAccess = async (
|
|
|
22
23
|
if (!isOfDeployType(config, "google-cloudrun")) return;
|
|
23
24
|
const projectConfig = await getProjectConfig();
|
|
24
25
|
const roles = getLocalAccessGcpRoles(projectConfig, context.env);
|
|
26
|
+
// validates `credentialsEnvVar` (catenv silently skips invalid ones)
|
|
27
|
+
getLocalAccessGcpCredentialsEnvVars(projectConfig, context.name);
|
|
25
28
|
|
|
26
29
|
if (roles.length > 0 && getVaultConfig(projectConfig).type === "gitlab") {
|
|
27
30
|
// the gitlab vault stores secrets as CI variables available to every
|
|
@@ -14,6 +14,7 @@ vi.mock("../../git/gitProjectInformation", () => ({
|
|
|
14
14
|
import {
|
|
15
15
|
getLocalAccessKeyFileName,
|
|
16
16
|
getLocalAccessKeyFileVarName,
|
|
17
|
+
getLocalAccessKeyFileVars,
|
|
17
18
|
writeLocalAccessKeyFiles,
|
|
18
19
|
} from "../keyFiles";
|
|
19
20
|
|
|
@@ -105,3 +106,61 @@ describe("writeLocalAccessKeyFiles", () => {
|
|
|
105
106
|
expect(logs.join("\n")).toMatch(/no local access key for dev:web/);
|
|
106
107
|
});
|
|
107
108
|
});
|
|
109
|
+
|
|
110
|
+
describe("getLocalAccessKeyFileVars", () => {
|
|
111
|
+
const folder = () => join(root, ".catladder", "local-access");
|
|
112
|
+
const keyFile = () => join(folder(), "gcp-dev-web.json");
|
|
113
|
+
const withKey = async () => {
|
|
114
|
+
await mkdir(folder(), { recursive: true });
|
|
115
|
+
await writeFile(keyFile(), "{}");
|
|
116
|
+
};
|
|
117
|
+
const vertexDev = config({
|
|
118
|
+
environments: {
|
|
119
|
+
dev: {
|
|
120
|
+
localAccess: {
|
|
121
|
+
gcp: {
|
|
122
|
+
presets: ["vertex"],
|
|
123
|
+
credentialsEnvVar: "GOOGLE_APPLICATION_CREDENTIALS",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("exports existing key files under the catladder var", async () => {
|
|
131
|
+
await withKey();
|
|
132
|
+
const c = config({
|
|
133
|
+
environments: { dev: { localAccess: { gcp: { presets: ["logs"] } } } },
|
|
134
|
+
});
|
|
135
|
+
expect(await getLocalAccessKeyFileVars(c, "web")).toEqual({
|
|
136
|
+
CATLADDER_LOCAL_ACCESS_GCP_DEV_KEY_FILE: keyFile(),
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("also exports the declared credentialsEnvVar", async () => {
|
|
141
|
+
await withKey();
|
|
142
|
+
expect(await getLocalAccessKeyFileVars(vertexDev, "web")).toEqual({
|
|
143
|
+
CATLADDER_LOCAL_ACCESS_GCP_DEV_KEY_FILE: keyFile(),
|
|
144
|
+
GOOGLE_APPLICATION_CREDENTIALS: keyFile(),
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("exports nothing while the key file is missing", async () => {
|
|
149
|
+
expect(await getLocalAccessKeyFileVars(vertexDev, "web")).toEqual({});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("skips a conflicting credentialsEnvVar instead of throwing", async () => {
|
|
153
|
+
await withKey();
|
|
154
|
+
const c = config({
|
|
155
|
+
localAccess: {
|
|
156
|
+
gcp: {
|
|
157
|
+
presets: ["vertex"],
|
|
158
|
+
credentialsEnvVar: "GOOGLE_APPLICATION_CREDENTIALS",
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
const vars = await getLocalAccessKeyFileVars(c, "web");
|
|
163
|
+
expect(vars).not.toHaveProperty("GOOGLE_APPLICATION_CREDENTIALS");
|
|
164
|
+
expect(vars).toHaveProperty("CATLADDER_LOCAL_ACCESS_GCP_DEV_KEY_FILE");
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { Config } from "@catladder/pipeline";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getLocalAccessGcpCredentialsEnvVars,
|
|
4
|
+
getLocalAccessGcpEnvs,
|
|
5
|
+
} from "@catladder/pipeline";
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* the envs of a component declaring gcp local access. An invalid
|
|
@@ -17,3 +20,19 @@ export const getDeclaredLocalAccessEnvs = (
|
|
|
17
20
|
return [];
|
|
18
21
|
}
|
|
19
22
|
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* the declared `credentialsEnvVar` names of a component, mapped to their
|
|
26
|
+
* env. Empty instead of throwing on an invalid declaration, see
|
|
27
|
+
* {@link getDeclaredLocalAccessEnvs}.
|
|
28
|
+
*/
|
|
29
|
+
export const getDeclaredLocalAccessCredentialsEnvVars = (
|
|
30
|
+
config: Config,
|
|
31
|
+
componentName: string,
|
|
32
|
+
): Record<string, string> => {
|
|
33
|
+
try {
|
|
34
|
+
return getLocalAccessGcpCredentialsEnvVars(config, componentName);
|
|
35
|
+
} catch {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
};
|
|
@@ -8,7 +8,10 @@ import { existsSync } from "fs";
|
|
|
8
8
|
import { join } from "path";
|
|
9
9
|
import type { IO } from "../core/types";
|
|
10
10
|
import { writePrivateFile } from "../utils/writePrivateFile";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
getDeclaredLocalAccessCredentialsEnvVars,
|
|
13
|
+
getDeclaredLocalAccessEnvs,
|
|
14
|
+
} from "./declared";
|
|
12
15
|
import { getProjectRootPath } from "../git/gitProjectInformation";
|
|
13
16
|
|
|
14
17
|
const sanitize = (s: string) => s.replace(/[^A-Za-z0-9]+/g, "_");
|
|
@@ -20,7 +23,8 @@ export const getLocalAccessKeyFileName = (env: string, componentName: string) =>
|
|
|
20
23
|
/**
|
|
21
24
|
* env var exposing the key file path. Deliberately NOT
|
|
22
25
|
* GOOGLE_APPLICATION_CREDENTIALS: the key must only be used where a
|
|
23
|
-
* command opts in (isolated gcloud config
|
|
26
|
+
* command opts in (isolated gcloud config, or a declared
|
|
27
|
+
* `credentialsEnvVar`), never by ADC implicitly.
|
|
24
28
|
*/
|
|
25
29
|
export const getLocalAccessKeyFileVarName = (env: string) =>
|
|
26
30
|
`CATLADDER_LOCAL_ACCESS_GCP_${sanitize(env).toUpperCase()}_KEY_FILE`;
|
|
@@ -31,20 +35,28 @@ const getFolder = async () =>
|
|
|
31
35
|
|
|
32
36
|
/**
|
|
33
37
|
* env vars pointing at the key files that exist for the component
|
|
34
|
-
* (only paths, no secrets)
|
|
38
|
+
* (only paths, no secrets): one per env, plus the declared
|
|
39
|
+
* `credentialsEnvVar` names
|
|
35
40
|
*/
|
|
36
41
|
export const getLocalAccessKeyFileVars = async (
|
|
37
42
|
config: Config,
|
|
38
43
|
componentName: string,
|
|
39
44
|
): Promise<Record<string, string>> => {
|
|
40
45
|
const folder = await getFolder();
|
|
46
|
+
const getPath = (env: string) =>
|
|
47
|
+
join(folder, getLocalAccessKeyFileName(env, componentName));
|
|
41
48
|
return Object.fromEntries(
|
|
42
|
-
|
|
43
|
-
.map(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
[
|
|
50
|
+
...getDeclaredLocalAccessEnvs(config, componentName).map(
|
|
51
|
+
(env): [string, string] => [
|
|
52
|
+
getLocalAccessKeyFileVarName(env),
|
|
53
|
+
getPath(env),
|
|
54
|
+
],
|
|
55
|
+
),
|
|
56
|
+
...Object.entries(
|
|
57
|
+
getDeclaredLocalAccessCredentialsEnvVars(config, componentName),
|
|
58
|
+
).map(([name, env]): [string, string] => [name, getPath(env)]),
|
|
59
|
+
].filter(([, path]) => existsSync(path)),
|
|
48
60
|
);
|
|
49
61
|
};
|
|
50
62
|
|