@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.
Files changed (27) hide show
  1. package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkLocalAccess.js +1 -0
  2. package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkLocalAccess.js.map +1 -1
  3. package/dist/apps/cli/src/apps/cli/commands/project/setup/setupLocalAccess.d.ts +1 -1
  4. package/dist/apps/cli/src/apps/cli/commands/project/setup/setupLocalAccess.js +3 -1
  5. package/dist/apps/cli/src/apps/cli/commands/project/setup/setupLocalAccess.js.map +1 -1
  6. package/dist/apps/cli/src/localAccess/declared.d.ts +6 -0
  7. package/dist/apps/cli/src/localAccess/declared.js +15 -1
  8. package/dist/apps/cli/src/localAccess/declared.js.map +1 -1
  9. package/dist/apps/cli/src/localAccess/keyFiles.d.ts +4 -2
  10. package/dist/apps/cli/src/localAccess/keyFiles.js +12 -8
  11. package/dist/apps/cli/src/localAccess/keyFiles.js.map +1 -1
  12. package/dist/bundles/catenv/index.js +56 -10
  13. package/dist/bundles/cli/index.js +60 -11
  14. package/dist/bundles/skills/catladder-secrets/SKILL.md +11 -0
  15. package/dist/packages/pipeline/src/localAccess/index.d.ts +7 -0
  16. package/dist/packages/pipeline/src/localAccess/index.js +29 -1
  17. package/dist/packages/pipeline/src/localAccess/index.js.map +1 -1
  18. package/dist/packages/pipeline/src/types/config.d.ts +20 -2
  19. package/dist/skills/catladder-secrets/SKILL.md +11 -0
  20. package/dist/tsconfig.tsbuildinfo +1 -1
  21. package/package.json +1 -1
  22. package/src/apps/cli/commands/project/doctor/{checkLocalAccess.test.ts → __tests__/checkLocalAccess.test.ts} +19 -4
  23. package/src/apps/cli/commands/project/doctor/checkLocalAccess.ts +2 -0
  24. package/src/apps/cli/commands/project/setup/setupLocalAccess.ts +4 -1
  25. package/src/localAccess/__tests__/keyFiles.test.ts +59 -0
  26. package/src/localAccess/declared.ts +20 -1
  27. package/src/localAccess/keyFiles.ts +21 -9
package/package.json CHANGED
@@ -53,7 +53,7 @@
53
53
  }
54
54
  ],
55
55
  "license": "MIT",
56
- "version": "0.0.0-main-0d7b031b",
56
+ "version": "0.0.0-main-018e2dee",
57
57
  "scripts": {
58
58
  "lint": "eslint \"src/**/*.ts\"",
59
59
  "lint:fix": "eslint \"src/**/*.ts\" --fix",
@@ -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("../../../../../gcloud/serviceAccounts", () => ({
5
+ vi.mock("../../../../../../gcloud/serviceAccounts", () => ({
6
6
  accountExists: async () => state.exists,
7
7
  }));
8
- vi.mock("../../../../../gcloud/localAccess", async (orig) => ({
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 "./checkLocalAccess";
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({ vault: { type: "gitlab" } } as any));
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 read-only local access (`localAccess` in catladder.ts)
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 { getLocalAccessGcpEnvs } from "@catladder/pipeline";
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 { getDeclaredLocalAccessEnvs } from "./declared";
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), never by ADC.
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
- getDeclaredLocalAccessEnvs(config, componentName)
43
- .map((env): [string, string] => [
44
- getLocalAccessKeyFileVarName(env),
45
- join(folder, getLocalAccessKeyFileName(env, componentName)),
46
- ])
47
- .filter(([, path]) => existsSync(path)),
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