@catladder/cli 0.0.0-next-4e7c6848 → 0.0.0-next-8b9ad5ca
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/bundles/catenv/index.js +617 -19
- package/dist/bundles/cli/index.js +811 -18
- package/dist/cli/src/adapters/baseContext.d.ts +4 -0
- package/dist/cli/src/adapters/baseContext.js +5 -0
- package/dist/cli/src/adapters/baseContext.js.map +1 -1
- package/dist/cli/src/adapters/nonInteractive.d.ts +9 -0
- package/dist/cli/src/adapters/nonInteractive.js +32 -0
- package/dist/cli/src/adapters/nonInteractive.js.map +1 -0
- package/dist/cli/src/adapters/programmatic.d.ts +1 -0
- package/dist/cli/src/adapters/programmatic.js +6 -1
- package/dist/cli/src/adapters/programmatic.js.map +1 -1
- package/dist/cli/src/adapters/terminal.d.ts +1 -0
- package/dist/cli/src/adapters/terminal.js +10 -1
- package/dist/cli/src/adapters/terminal.js.map +1 -1
- package/dist/cli/src/adapters/vaultManagerAccess.d.ts +8 -0
- package/dist/cli/src/adapters/vaultManagerAccess.js +26 -0
- package/dist/cli/src/adapters/vaultManagerAccess.js.map +1 -0
- package/dist/cli/src/apps/catenv/catenv.d.ts +2 -0
- package/dist/cli/src/apps/catenv/catenv.js +8 -1
- package/dist/cli/src/apps/catenv/catenv.js.map +1 -1
- package/dist/cli/src/apps/catenv/printVariables.d.ts +2 -2
- package/dist/cli/src/apps/catenv/printVariables.js +4 -4
- package/dist/cli/src/apps/catenv/printVariables.js.map +1 -1
- package/dist/cli/src/apps/catenv/types.d.ts +9 -0
- package/dist/cli/src/apps/catenv/writeDotEnvFiles.d.ts +2 -2
- package/dist/cli/src/apps/catenv/writeDotEnvFiles.js +1 -1
- package/dist/cli/src/apps/catenv/writeDotEnvFiles.js.map +1 -1
- package/dist/cli/src/apps/cli/commands/project/commandConfigSecrets.js +52 -3
- package/dist/cli/src/apps/cli/commands/project/commandConfigSecrets.js.map +1 -1
- package/dist/cli/src/catenv.js +3 -2
- package/dist/cli/src/catenv.js.map +1 -1
- package/dist/cli/src/commands/index.d.ts +1 -0
- package/dist/cli/src/commands/index.js +8 -1
- package/dist/cli/src/commands/index.js.map +1 -1
- package/dist/cli/src/commands/project/commandSecretsSyncGithub.d.ts +37 -0
- package/dist/cli/src/commands/project/commandSecretsSyncGithub.js +127 -0
- package/dist/cli/src/commands/project/commandSecretsSyncGithub.js.map +1 -0
- package/dist/cli/src/config/getProjectConfig.d.ts +1 -1
- package/dist/cli/src/config/getProjectConfig.js +12 -2
- package/dist/cli/src/config/getProjectConfig.js.map +1 -1
- package/dist/cli/src/core/types.d.ts +15 -0
- package/dist/cli/src/core/types.js +8 -1
- package/dist/cli/src/core/types.js.map +1 -1
- package/dist/cli/src/git/gitProjectInformation.d.ts +1 -1
- package/dist/cli/src/git/gitProjectInformation.js +3 -3
- package/dist/cli/src/git/gitProjectInformation.js.map +1 -1
- package/dist/cli/src/utils/github.d.ts +15 -0
- package/dist/cli/src/utils/github.js +54 -0
- package/dist/cli/src/utils/github.js.map +1 -0
- package/dist/cli/src/vault/BitwardenVault.d.ts +24 -0
- package/dist/cli/src/vault/BitwardenVault.js +107 -0
- package/dist/cli/src/vault/BitwardenVault.js.map +1 -0
- package/dist/cli/src/vault/GitlabVault.d.ts +11 -0
- package/dist/cli/src/vault/GitlabVault.js +24 -0
- package/dist/cli/src/vault/GitlabVault.js.map +1 -0
- package/dist/cli/src/vault/VaultManager.d.ts +43 -0
- package/dist/cli/src/vault/VaultManager.js +131 -0
- package/dist/cli/src/vault/VaultManager.js.map +1 -0
- package/dist/cli/src/vault/bw.d.ts +15 -0
- package/dist/cli/src/vault/bw.js +89 -0
- package/dist/cli/src/vault/bw.js.map +1 -0
- package/dist/cli/src/vault/cache.d.ts +38 -0
- package/dist/cli/src/vault/cache.js +53 -0
- package/dist/cli/src/vault/cache.js.map +1 -0
- package/dist/cli/src/vault/index.d.ts +5 -0
- package/dist/cli/src/vault/index.js +48 -0
- package/dist/cli/src/vault/index.js.map +1 -0
- package/dist/cli/src/vault/types.d.ts +33 -0
- package/dist/cli/src/vault/types.js +5 -0
- package/dist/cli/src/vault/types.js.map +1 -0
- package/dist/pipeline/src/backends/index.d.ts +17 -1
- package/dist/pipeline/src/backends/index.js +33 -1
- package/dist/pipeline/src/backends/index.js.map +1 -1
- package/dist/pipeline/src/constants.js +1 -1
- package/dist/pipeline/src/deploy/cloudRun/index.js +11 -4
- package/dist/pipeline/src/deploy/cloudRun/index.js.map +1 -1
- package/dist/pipeline/src/types/config.d.ts +37 -3
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/adapters/baseContext.ts +10 -0
- package/src/adapters/nonInteractive.ts +30 -0
- package/src/adapters/programmatic.ts +7 -1
- package/src/adapters/terminal.ts +9 -1
- package/src/adapters/vaultManagerAccess.ts +25 -0
- package/src/apps/catenv/catenv.ts +5 -1
- package/src/apps/catenv/printVariables.ts +16 -6
- package/src/apps/catenv/types.ts +9 -0
- package/src/apps/catenv/writeDotEnvFiles.ts +3 -3
- package/src/apps/cli/commands/project/commandConfigSecrets.ts +94 -9
- package/src/catenv.ts +22 -2
- package/src/commands/index.ts +1 -0
- package/src/commands/project/commandSecretsSyncGithub.ts +149 -0
- package/src/config/getProjectConfig.ts +21 -7
- package/src/core/types.ts +23 -0
- package/src/git/gitProjectInformation.ts +5 -3
- package/src/utils/github.ts +59 -0
- package/src/vault/BitwardenVault.ts +161 -0
- package/src/vault/GitlabVault.ts +25 -0
- package/src/vault/VaultManager.ts +211 -0
- package/src/vault/bw.ts +110 -0
- package/src/vault/cache.ts +80 -0
- package/src/vault/index.ts +5 -0
- package/src/vault/types.ts +42 -0
package/package.json
CHANGED
|
@@ -6,6 +6,8 @@ import type {
|
|
|
6
6
|
InputsSchema,
|
|
7
7
|
OptionalInput,
|
|
8
8
|
} from "../core/types";
|
|
9
|
+
import type { VaultManager } from "../vault/VaultManager";
|
|
10
|
+
import { createVaultManagerGetter } from "./vaultManagerAccess";
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Base class for CommandContext implementations.
|
|
@@ -19,9 +21,17 @@ export abstract class BaseContext<TInputs extends InputsSchema>
|
|
|
19
21
|
|
|
20
22
|
constructor(protected readonly command: CommandDef<TInputs>) {}
|
|
21
23
|
|
|
24
|
+
abstract readonly interactive: boolean;
|
|
25
|
+
|
|
22
26
|
abstract log(message: string): void;
|
|
23
27
|
abstract confirm(message: string): Promise<boolean>;
|
|
24
28
|
|
|
29
|
+
private readonly vaultManagerGetter = createVaultManagerGetter(() => this);
|
|
30
|
+
|
|
31
|
+
getVaultManager(): Promise<VaultManager> {
|
|
32
|
+
return this.vaultManagerGetter();
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
/**
|
|
26
36
|
* Resolve a single input value. Called once per input (result is cached).
|
|
27
37
|
* Implementations check pre-supplied values, prompt interactively, use defaults, or throw.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { IO } from "../core/types";
|
|
2
|
+
import { NonInteractiveError } from "../core/types";
|
|
3
|
+
import type { SecretsMode } from "../vault";
|
|
4
|
+
import { createVaultManagerGetter } from "./vaultManagerAccess";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* the context of non-interactive runs (catenv via direnv): logging
|
|
8
|
+
* goes to stderr, any required interaction fails with a clear error.
|
|
9
|
+
*/
|
|
10
|
+
export function createNonInteractiveIO(
|
|
11
|
+
options: { vaultMode?: SecretsMode } = {},
|
|
12
|
+
): IO {
|
|
13
|
+
const io: IO = {
|
|
14
|
+
interactive: false,
|
|
15
|
+
getVaultManager: createVaultManagerGetter(() => io, {
|
|
16
|
+
mode: options.vaultMode,
|
|
17
|
+
}),
|
|
18
|
+
log(message: string): void {
|
|
19
|
+
// stderr: stdout may be evaluated (direnv)
|
|
20
|
+
console.error(message);
|
|
21
|
+
},
|
|
22
|
+
async confirm(message: string): Promise<boolean> {
|
|
23
|
+
throw new NonInteractiveError(`confirmation ("${message}")`);
|
|
24
|
+
},
|
|
25
|
+
async promptDirect(spec): Promise<never> {
|
|
26
|
+
throw new NonInteractiveError(`input "${spec.name}"`);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
return io;
|
|
30
|
+
}
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
} from "../core/types";
|
|
8
8
|
import { MissingInputError } from "../core/types";
|
|
9
9
|
import { BaseContext } from "./baseContext";
|
|
10
|
+
import { createVaultManagerGetter } from "./vaultManagerAccess";
|
|
10
11
|
|
|
11
12
|
export interface ProgrammaticOptions {
|
|
12
13
|
/** Pre-supplied values keyed by input name */
|
|
@@ -21,6 +22,8 @@ class ProgrammaticContext<
|
|
|
21
22
|
private inputs: Record<string, unknown>;
|
|
22
23
|
private onLog?: (message: string) => void;
|
|
23
24
|
|
|
25
|
+
readonly interactive = false;
|
|
26
|
+
|
|
24
27
|
constructor(command: CommandDef<TInputs>, options: ProgrammaticOptions) {
|
|
25
28
|
super(command);
|
|
26
29
|
this.inputs = options.inputs ?? {};
|
|
@@ -66,7 +69,9 @@ export function createProgrammaticContext<TInputs extends InputsSchema>(
|
|
|
66
69
|
export function createProgrammaticIO(options: ProgrammaticOptions = {}): IO {
|
|
67
70
|
const { inputs = {}, onLog } = options;
|
|
68
71
|
|
|
69
|
-
|
|
72
|
+
const io: IO = {
|
|
73
|
+
interactive: false,
|
|
74
|
+
getVaultManager: createVaultManagerGetter(() => io),
|
|
70
75
|
log(message: string): void {
|
|
71
76
|
onLog?.(message);
|
|
72
77
|
},
|
|
@@ -85,4 +90,5 @@ export function createProgrammaticIO(options: ProgrammaticOptions = {}): IO {
|
|
|
85
90
|
throw new MissingInputError(spec.name, spec.message);
|
|
86
91
|
},
|
|
87
92
|
};
|
|
93
|
+
return io;
|
|
88
94
|
}
|
package/src/adapters/terminal.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
} from "../core/types";
|
|
9
9
|
import { MissingInputError } from "../core/types";
|
|
10
10
|
import { BaseContext } from "./baseContext";
|
|
11
|
+
import { createVaultManagerGetter } from "./vaultManagerAccess";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Resolve choices from a choices function.
|
|
@@ -93,6 +94,10 @@ class TerminalContext<
|
|
|
93
94
|
super(command);
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
get interactive(): boolean {
|
|
98
|
+
return this.options.interactive ?? process.stdin.isTTY === true;
|
|
99
|
+
}
|
|
100
|
+
|
|
96
101
|
log(message: string): void {
|
|
97
102
|
console.log(message);
|
|
98
103
|
}
|
|
@@ -180,7 +185,9 @@ export function createTerminalContext<TInputs extends InputsSchema>(
|
|
|
180
185
|
* Create a bare IO instance for terminal helper functions.
|
|
181
186
|
*/
|
|
182
187
|
export function createTerminalIO(options: TerminalOptions = {}): IO {
|
|
183
|
-
|
|
188
|
+
const io: IO = {
|
|
189
|
+
interactive: options.interactive ?? process.stdin.isTTY === true,
|
|
190
|
+
getVaultManager: createVaultManagerGetter(() => io),
|
|
184
191
|
log(message: string): void {
|
|
185
192
|
console.log(message);
|
|
186
193
|
},
|
|
@@ -204,4 +211,5 @@ export function createTerminalIO(options: TerminalOptions = {}): IO {
|
|
|
204
211
|
throw new MissingInputError(spec.name, spec.message);
|
|
205
212
|
},
|
|
206
213
|
};
|
|
214
|
+
return io;
|
|
207
215
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getProjectConfig } from "../config/getProjectConfig";
|
|
2
|
+
import type { IO } from "../core/types";
|
|
3
|
+
import type { VaultManagerOptions } from "../vault/VaultManager";
|
|
4
|
+
import { VaultManager } from "../vault/VaultManager";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* lazily creates the vault manager of a context (once per context),
|
|
8
|
+
* loading the project config on first use
|
|
9
|
+
*/
|
|
10
|
+
export const createVaultManagerGetter = (
|
|
11
|
+
io: () => IO,
|
|
12
|
+
options: Omit<VaultManagerOptions, "io"> = {},
|
|
13
|
+
): (() => Promise<VaultManager>) => {
|
|
14
|
+
let managerPromise: Promise<VaultManager> | undefined;
|
|
15
|
+
return () =>
|
|
16
|
+
(managerPromise ??= (async () => {
|
|
17
|
+
const config = await getProjectConfig();
|
|
18
|
+
if (!config) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
"no catladder config found — the secrets vault needs a project config",
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return new VaultManager(config, { ...options, io: io() });
|
|
24
|
+
})());
|
|
25
|
+
};
|
|
@@ -8,10 +8,13 @@ import {
|
|
|
8
8
|
createCatenvContext,
|
|
9
9
|
generatePipelineFiles,
|
|
10
10
|
} from "@catladder/pipeline";
|
|
11
|
+
import type { SecretsMode } from "../../vault";
|
|
12
|
+
import { createNonInteractiveIO } from "../../adapters/nonInteractive";
|
|
11
13
|
|
|
12
14
|
type Options = {
|
|
13
15
|
verbose?: boolean;
|
|
14
16
|
printVariables?: boolean;
|
|
17
|
+
vaultMode?: SecretsMode;
|
|
15
18
|
};
|
|
16
19
|
export default async (choice?: Choice, options?: Options) => {
|
|
17
20
|
const config = await getProjectConfig();
|
|
@@ -19,7 +22,8 @@ export default async (choice?: Choice, options?: Options) => {
|
|
|
19
22
|
return;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
const
|
|
25
|
+
const io = createNonInteractiveIO({ vaultMode: options?.vaultMode });
|
|
26
|
+
const context = { ...createCatenvContext(config), io };
|
|
23
27
|
if (options?.verbose) {
|
|
24
28
|
printVerboseBanner();
|
|
25
29
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Config } from "@catladder/pipeline";
|
|
2
|
+
import type { IO } from "../../core/types";
|
|
3
|
+
import type { CatenvCliContext } from "./types";
|
|
2
4
|
import { getEnvVarsResolved } from "../../config/getProjectConfig";
|
|
3
5
|
import type { Choice } from "./types";
|
|
4
6
|
import {
|
|
@@ -7,21 +9,25 @@ import {
|
|
|
7
9
|
sanitizeEnvVarName,
|
|
8
10
|
} from "./utils";
|
|
9
11
|
|
|
10
|
-
const getAllVariablesToPrint = async (
|
|
12
|
+
const getAllVariablesToPrint = async (
|
|
13
|
+
io: IO,
|
|
14
|
+
config: Config,
|
|
15
|
+
choice?: Choice,
|
|
16
|
+
) => {
|
|
11
17
|
const { env, currentComponent } = await getCurrentComponentAndEnvFromChoice(
|
|
12
18
|
config,
|
|
13
19
|
choice,
|
|
14
20
|
);
|
|
15
21
|
|
|
16
22
|
if (currentComponent) {
|
|
17
|
-
return await getEnvVarsResolved(
|
|
23
|
+
return await getEnvVarsResolved(io, env, currentComponent);
|
|
18
24
|
} else {
|
|
19
25
|
// when in a monorep and not in a subapp, merge all env vars.
|
|
20
26
|
// this is not 100% correct, but better than not exporting any vars at all
|
|
21
27
|
// so we also add prefixed variants
|
|
22
28
|
return await Object.keys(config.components).reduce(
|
|
23
29
|
async (acc, componentName) => {
|
|
24
|
-
const subappvars = await getEnvVarsResolved(
|
|
30
|
+
const subappvars = await getEnvVarsResolved(io, env, componentName);
|
|
25
31
|
delete subappvars["_ALL_ENV_VAR_KEYS"];
|
|
26
32
|
return {
|
|
27
33
|
...(await acc),
|
|
@@ -41,10 +47,14 @@ const getAllVariablesToPrint = async (config: Config, choice?: Choice) => {
|
|
|
41
47
|
};
|
|
42
48
|
|
|
43
49
|
export const printVariables = async (
|
|
44
|
-
context:
|
|
50
|
+
context: CatenvCliContext,
|
|
45
51
|
choice?: Choice,
|
|
46
52
|
) => {
|
|
47
|
-
const variables = await getAllVariablesToPrint(
|
|
53
|
+
const variables = await getAllVariablesToPrint(
|
|
54
|
+
context.io,
|
|
55
|
+
context.config,
|
|
56
|
+
choice,
|
|
57
|
+
);
|
|
48
58
|
|
|
49
59
|
console.log(makeExportKeyValuestring(variables));
|
|
50
60
|
};
|
package/src/apps/catenv/types.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import type { CatenvContext } from "@catladder/pipeline";
|
|
2
|
+
import type { IO } from "../../core/types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* the pipeline's catenv context extended with the cli io context
|
|
6
|
+
* (non-interactive when run via direnv)
|
|
7
|
+
*/
|
|
8
|
+
export type CatenvCliContext = CatenvContext & { io: IO };
|
|
9
|
+
|
|
1
10
|
import type { VariableValue } from "@catladder/pipeline";
|
|
2
11
|
|
|
3
12
|
export type Choice = {
|
|
@@ -7,10 +7,10 @@ import {
|
|
|
7
7
|
getCurrentComponentAndEnvFromChoice,
|
|
8
8
|
makeKeyValueString,
|
|
9
9
|
} from "./utils";
|
|
10
|
-
import type {
|
|
10
|
+
import type { CatenvCliContext } from "./types";
|
|
11
11
|
|
|
12
12
|
export const writeDotEnvFiles = async (
|
|
13
|
-
context:
|
|
13
|
+
context: CatenvCliContext,
|
|
14
14
|
choice?: Choice,
|
|
15
15
|
) => {
|
|
16
16
|
const { env, currentComponent } = await getCurrentComponentAndEnvFromChoice(
|
|
@@ -32,7 +32,7 @@ export const writeDotEnvFiles = async (
|
|
|
32
32
|
const gitRoot = await getGitRoot();
|
|
33
33
|
|
|
34
34
|
for (const componentName of componentsToActuallyWriteDotEnvNow) {
|
|
35
|
-
const variables = await getEnvVarsResolved(
|
|
35
|
+
const variables = await getEnvVarsResolved(context.io, env, componentName);
|
|
36
36
|
delete variables["_ALL_ENV_VAR_KEYS"];
|
|
37
37
|
const componentDir = getComponentFullPath(
|
|
38
38
|
gitRoot,
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
getEnabledPipelineTypes,
|
|
3
|
+
getSecretVarName,
|
|
4
|
+
getVaultConfig,
|
|
5
|
+
} from "@catladder/pipeline";
|
|
6
|
+
import type { Config } from "@catladder/pipeline";
|
|
2
7
|
import { stripIndents } from "common-tags";
|
|
3
8
|
import { difference } from "lodash-es";
|
|
4
9
|
import type { IO } from "../../../../core/types";
|
|
@@ -8,9 +13,15 @@ import {
|
|
|
8
13
|
getEnvVarsResolved,
|
|
9
14
|
getJobOnlyEnvVarsResolved,
|
|
10
15
|
getProjectComponents,
|
|
16
|
+
getProjectConfig,
|
|
11
17
|
parseChoice,
|
|
12
18
|
} from "../../../../config/getProjectConfig";
|
|
19
|
+
import {
|
|
20
|
+
getConfiguredGithubRepo,
|
|
21
|
+
pushSecretsToGithub,
|
|
22
|
+
} from "../../../../commands/project/commandSecretsSyncGithub";
|
|
13
23
|
import { editAsFile } from "../../../../utils/editAsFile";
|
|
24
|
+
import { isGhAuthenticated } from "../../../../utils/github";
|
|
14
25
|
import { upsertAllVariables } from "../../../../utils/gitlab";
|
|
15
26
|
import { delay } from "../../../../utils/promise";
|
|
16
27
|
|
|
@@ -169,27 +180,101 @@ const doItFor = async (
|
|
|
169
180
|
}
|
|
170
181
|
}
|
|
171
182
|
}
|
|
183
|
+
const config = await getProjectConfig();
|
|
184
|
+
if (!config) {
|
|
185
|
+
throw new Error("no catladder config found");
|
|
186
|
+
}
|
|
187
|
+
|
|
172
188
|
instance.log("");
|
|
173
|
-
instance.log("
|
|
189
|
+
instance.log("writing all secrets to the vault, please wait...");
|
|
174
190
|
instance.log("");
|
|
175
191
|
for (const [componentName, envs] of Object.entries(envAndComponents)) {
|
|
176
192
|
for (const env of envs) {
|
|
177
|
-
instance.log("
|
|
178
|
-
await
|
|
179
|
-
instance
|
|
180
|
-
|
|
181
|
-
env,
|
|
182
|
-
componentName,
|
|
183
|
-
);
|
|
193
|
+
instance.log("writing " + env + ":" + componentName + "...\n");
|
|
194
|
+
await (
|
|
195
|
+
await instance.getVaultManager()
|
|
196
|
+
).writeSecrets(env, componentName, valuesToEdit[componentName][env]);
|
|
184
197
|
instance.log("");
|
|
185
198
|
instance.log("✅ " + env + ":" + componentName);
|
|
186
199
|
instance.log("--------------------------------\n");
|
|
187
200
|
}
|
|
188
201
|
}
|
|
202
|
+
|
|
203
|
+
await mirrorSecretsToCiBackends(
|
|
204
|
+
instance,
|
|
205
|
+
config,
|
|
206
|
+
envAndComponents,
|
|
207
|
+
valuesToEdit,
|
|
208
|
+
);
|
|
209
|
+
|
|
189
210
|
instance.log("done! 😻");
|
|
190
211
|
instance.log("");
|
|
191
212
|
};
|
|
192
213
|
|
|
214
|
+
/**
|
|
215
|
+
* the vault is the source of truth; every enabled CI backend receives
|
|
216
|
+
* a mirrored copy of the edited secrets (gitlab variables, github
|
|
217
|
+
* secrets), so all CI systems stay in sync
|
|
218
|
+
*/
|
|
219
|
+
const mirrorSecretsToCiBackends = async (
|
|
220
|
+
instance: IO,
|
|
221
|
+
config: Config,
|
|
222
|
+
envAndComponents: { [componentName: string]: string[] },
|
|
223
|
+
valuesToEdit: Vars,
|
|
224
|
+
) => {
|
|
225
|
+
const enabled = getEnabledPipelineTypes(config);
|
|
226
|
+
|
|
227
|
+
// gitlab mirror (unless gitlab is the vault itself, then it is
|
|
228
|
+
// already written)
|
|
229
|
+
if (getVaultConfig(config).type !== "gitlab" && enabled.includes("gitlab")) {
|
|
230
|
+
for (const [componentName, envs] of Object.entries(envAndComponents)) {
|
|
231
|
+
for (const env of envs) {
|
|
232
|
+
await upsertAllVariables(
|
|
233
|
+
instance,
|
|
234
|
+
valuesToEdit[componentName][env],
|
|
235
|
+
env,
|
|
236
|
+
componentName,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
instance.log("✅ mirrored to gitlab");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!enabled.includes("github")) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const secrets = Object.entries(envAndComponents).flatMap(
|
|
247
|
+
([componentName, envs]) =>
|
|
248
|
+
envs.flatMap((env) =>
|
|
249
|
+
Object.entries(valuesToEdit[componentName][env] ?? {}).map(
|
|
250
|
+
([key, value]) => ({
|
|
251
|
+
name: getSecretVarName(env, componentName, key),
|
|
252
|
+
value:
|
|
253
|
+
typeof value === "string" ? value : JSON.stringify(value ?? ""),
|
|
254
|
+
}),
|
|
255
|
+
),
|
|
256
|
+
),
|
|
257
|
+
);
|
|
258
|
+
if (secrets.length === 0) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (!(await isGhAuthenticated())) {
|
|
262
|
+
instance.log(
|
|
263
|
+
"⚠️ github pipeline is enabled, but the github cli (gh) is not authenticated — run `project secrets-sync-github` later to mirror the secrets",
|
|
264
|
+
);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const repo = await getConfiguredGithubRepo();
|
|
268
|
+
if (!repo) {
|
|
269
|
+
instance.log(
|
|
270
|
+
"⚠️ github pipeline is enabled, but no github repository was found — run `project secrets-sync-github` later to mirror the secrets",
|
|
271
|
+
);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
instance.log(`mirroring ${secrets.length} secrets to github ('${repo}')...`);
|
|
275
|
+
await pushSecretsToGithub(instance, repo, secrets);
|
|
276
|
+
};
|
|
277
|
+
|
|
193
278
|
export const projectConfigSecrets = async (io: IO, envComponent?: string) => {
|
|
194
279
|
if (!envComponent) {
|
|
195
280
|
const allEnvAndcomponents = await getAllComponentsWithAllEnvsHierarchical();
|
package/src/catenv.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
1
|
+
import { Command, Option } from "commander";
|
|
2
2
|
import catenv from "./apps/catenv/catenv";
|
|
3
3
|
import { parseChoice } from "./config/parseChoice";
|
|
4
4
|
import packageInfos from "./packageInfos";
|
|
5
|
+
import type { SecretsMode } from "./vault";
|
|
5
6
|
|
|
6
7
|
const program = new Command();
|
|
7
8
|
|
|
@@ -12,14 +13,33 @@ program
|
|
|
12
13
|
.argument("[envComponent]", "env or env:component")
|
|
13
14
|
.option("-v, --verbose", "verbose output")
|
|
14
15
|
.option("-p, --print-variables", "print variables")
|
|
16
|
+
.addOption(
|
|
17
|
+
new Option(
|
|
18
|
+
"--vault-mode <mode>",
|
|
19
|
+
[
|
|
20
|
+
"how to interact with the secrets vault:",
|
|
21
|
+
"auto: contact the vault when cached secrets are missing (may prompt to unlock)",
|
|
22
|
+
"no-prompt: like auto, but fail instead of prompting",
|
|
23
|
+
"offline: never contact the vault, use only locally cached secrets",
|
|
24
|
+
"refresh: ignore the local cache once and refresh it from the vault",
|
|
25
|
+
].join("\n"),
|
|
26
|
+
)
|
|
27
|
+
.choices(["auto", "no-prompt", "offline", "refresh"])
|
|
28
|
+
.default("auto"),
|
|
29
|
+
)
|
|
15
30
|
.action(
|
|
16
31
|
(
|
|
17
32
|
envComponent: string | undefined,
|
|
18
|
-
opts: {
|
|
33
|
+
opts: {
|
|
34
|
+
verbose?: boolean;
|
|
35
|
+
printVariables?: boolean;
|
|
36
|
+
vaultMode: SecretsMode;
|
|
37
|
+
},
|
|
19
38
|
) => {
|
|
20
39
|
catenv(envComponent ? parseChoice(envComponent) : null, {
|
|
21
40
|
verbose: opts.verbose ?? false,
|
|
22
41
|
printVariables: opts.printVariables ?? false,
|
|
42
|
+
vaultMode: opts.vaultMode,
|
|
23
43
|
}).then(() => {
|
|
24
44
|
// we have to exit manually, because we have some file watches
|
|
25
45
|
process.exit();
|
package/src/commands/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ export { commandOpenGit } from "./project/commandOpenGit";
|
|
|
15
15
|
export { commandCloudSqlProxy } from "./project/commandCloudSqlProxy";
|
|
16
16
|
export { commandConfigSecrets } from "./project/commandConfigSecrets";
|
|
17
17
|
export { commandSecretsClearBackups } from "./project/commandSecretsClearBackups";
|
|
18
|
+
export { commandSecretsSyncGithub } from "./project/commandSecretsSyncGithub";
|
|
18
19
|
export { commandOpenLogs } from "./project/commandOpenLogs";
|
|
19
20
|
export { commandTriggerCronjobGeneral } from "./project/k8s/commandTriggerCronjob";
|
|
20
21
|
export { commandRenewToken } from "./project/commandRenewToken";
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getEnabledPipelineTypes,
|
|
3
|
+
getPipelineGitRemote,
|
|
4
|
+
getSecretVarName,
|
|
5
|
+
} from "@catladder/pipeline";
|
|
6
|
+
import {
|
|
7
|
+
getAllComponentsWithAllEnvsHierarchical,
|
|
8
|
+
getEnvironment,
|
|
9
|
+
getProjectConfig,
|
|
10
|
+
} from "../../config/getProjectConfig";
|
|
11
|
+
import { defineCommand } from "../../core/defineCommand";
|
|
12
|
+
import type { IO } from "../../core/types";
|
|
13
|
+
import {
|
|
14
|
+
getGithubRepoFromRemote,
|
|
15
|
+
isGhAuthenticated,
|
|
16
|
+
setGithubSecret,
|
|
17
|
+
} from "../../utils/github";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* all secrets declared in the config, with their values read from the
|
|
21
|
+
* vault (github secrets are write-only and can never act as a source)
|
|
22
|
+
*/
|
|
23
|
+
export const collectSecretsFromVault = async (io: IO) => {
|
|
24
|
+
const envAndComponents = await getAllComponentsWithAllEnvsHierarchical();
|
|
25
|
+
|
|
26
|
+
const names: string[] = [];
|
|
27
|
+
for (const [componentName, envs] of Object.entries(envAndComponents)) {
|
|
28
|
+
for (const env of envs) {
|
|
29
|
+
const { secretEnvVarKeys, jobOnlyVars } = await getEnvironment(
|
|
30
|
+
env,
|
|
31
|
+
componentName,
|
|
32
|
+
);
|
|
33
|
+
const keys = [
|
|
34
|
+
...secretEnvVarKeys,
|
|
35
|
+
...jobOnlyVars.build.secretEnvVarKeys,
|
|
36
|
+
...jobOnlyVars.deploy.secretEnvVarKeys,
|
|
37
|
+
];
|
|
38
|
+
keys.forEach(({ key }) =>
|
|
39
|
+
names.push(getSecretVarName(env, componentName, key)),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const values = await (await io.getVaultManager()).readSecrets(names, io);
|
|
45
|
+
const secrets = names
|
|
46
|
+
.filter((name) => values[name] !== undefined)
|
|
47
|
+
.map((name) => ({ name, value: values[name] }));
|
|
48
|
+
const missing = names.filter((name) => values[name] === undefined);
|
|
49
|
+
return { secrets, missing };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* pushes secrets to github repository secrets via the gh cli
|
|
54
|
+
*/
|
|
55
|
+
export const pushSecretsToGithub = async (
|
|
56
|
+
io: IO,
|
|
57
|
+
repo: string,
|
|
58
|
+
secrets: { name: string; value: string }[],
|
|
59
|
+
) => {
|
|
60
|
+
for (const { name, value } of secrets) {
|
|
61
|
+
await setGithubSecret(repo, name, value);
|
|
62
|
+
io.log(`✅ ${name}`);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* resolves the github repository from the configured git remote
|
|
68
|
+
* (`pipelines.github.gitRemote`, defaults to origin)
|
|
69
|
+
*/
|
|
70
|
+
export const getConfiguredGithubRepo = async (): Promise<
|
|
71
|
+
string | undefined
|
|
72
|
+
> => {
|
|
73
|
+
const config = await getProjectConfig();
|
|
74
|
+
if (!config) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
return getGithubRepoFromRemote(getPipelineGitRemote(config, "github"));
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* copies the catladder secrets from the vault to github actions
|
|
82
|
+
* repository secrets, keeping the exact same names, so the github
|
|
83
|
+
* pipeline can reference them as ${{ secrets.<NAME> }}.
|
|
84
|
+
*/
|
|
85
|
+
export const commandSecretsSyncGithub = defineCommand({
|
|
86
|
+
name: "project secrets-sync-github",
|
|
87
|
+
description:
|
|
88
|
+
"copies all secrets from the vault to github repo secrets (same names), for the github pipeline",
|
|
89
|
+
group: "project",
|
|
90
|
+
inputs: {
|
|
91
|
+
repo: {
|
|
92
|
+
type: "string",
|
|
93
|
+
message: "github repository (owner/name)",
|
|
94
|
+
positional: true,
|
|
95
|
+
required: false,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
execute: async (ctx) => {
|
|
99
|
+
const config = await getProjectConfig();
|
|
100
|
+
if (!config) {
|
|
101
|
+
throw new Error("no catladder config found");
|
|
102
|
+
}
|
|
103
|
+
const enabled = getEnabledPipelineTypes(config);
|
|
104
|
+
if (!enabled.includes("github")) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
"the github pipeline is not enabled in `pipelines` — nothing to sync to",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (!(await isGhAuthenticated())) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
"the github cli (gh) is not installed or not authenticated — run `gh auth login` first",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const repo = (await ctx.get("repo")) ?? (await getConfiguredGithubRepo());
|
|
115
|
+
if (!repo) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`no github repository found on the '${getPipelineGitRemote(config, "github")}' remote — pass it as owner/name or configure pipelines.github.gitRemote`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const { secrets, missing } = await collectSecretsFromVault(ctx);
|
|
122
|
+
|
|
123
|
+
if (missing.length > 0) {
|
|
124
|
+
ctx.log(
|
|
125
|
+
`⚠️ ${missing.length} secrets are not set in the vault and will be skipped:`,
|
|
126
|
+
);
|
|
127
|
+
missing.forEach((name) => ctx.log(` - ${name}`));
|
|
128
|
+
ctx.log("");
|
|
129
|
+
}
|
|
130
|
+
if (secrets.length === 0) {
|
|
131
|
+
ctx.log("no secrets to sync 🤷");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
ctx.log(`about to set ${secrets.length} secrets on '${repo}':`);
|
|
136
|
+
secrets.forEach(({ name }) => ctx.log(` - ${name}`));
|
|
137
|
+
ctx.log("");
|
|
138
|
+
const confirmed = await ctx.confirm("continue? 🤔");
|
|
139
|
+
if (!confirmed) {
|
|
140
|
+
throw new Error("abort");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await pushSecretsToGithub(ctx, repo, secrets);
|
|
144
|
+
ctx.log("");
|
|
145
|
+
ctx.log(
|
|
146
|
+
"done! 😻 the github pipeline can use them as ${{ secrets.<NAME> }}",
|
|
147
|
+
);
|
|
148
|
+
},
|
|
149
|
+
});
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "@catladder/pipeline";
|
|
13
13
|
|
|
14
14
|
import type { IO } from "../core/types";
|
|
15
|
-
import {
|
|
15
|
+
import { getVariableValueByRawName } from "../utils/gitlab";
|
|
16
16
|
|
|
17
17
|
import { getGitRoot } from "../utils/projects";
|
|
18
18
|
|
|
@@ -121,10 +121,24 @@ export const getGitlabVar = async (
|
|
|
121
121
|
};
|
|
122
122
|
|
|
123
123
|
const resolveSecrets = async (
|
|
124
|
-
io: IO
|
|
124
|
+
io: IO,
|
|
125
125
|
varSets: EnvironmentEnvVars[],
|
|
126
126
|
): Promise<Record<string, string>> => {
|
|
127
|
-
|
|
127
|
+
// the secret variables actually referenced by the env var values —
|
|
128
|
+
// when they are all in the local cache, the vault is not contacted
|
|
129
|
+
const referencedNames = [
|
|
130
|
+
...new Set(
|
|
131
|
+
varSets.flatMap(({ envVars }) =>
|
|
132
|
+
Object.values(envVars).flatMap(
|
|
133
|
+
(value) => `${value}`.match(/\$CL_\w+/g) ?? [],
|
|
134
|
+
),
|
|
135
|
+
),
|
|
136
|
+
),
|
|
137
|
+
].map((reference) => reference.slice(1));
|
|
138
|
+
|
|
139
|
+
const vaultManager = await io.getVaultManager();
|
|
140
|
+
const secrets = await vaultManager.readSecrets(referencedNames, io);
|
|
141
|
+
const secretEntries = Object.entries(secrets);
|
|
128
142
|
|
|
129
143
|
return Object.fromEntries(
|
|
130
144
|
varSets.flatMap(({ envVars, secretEnvVarKeys }) =>
|
|
@@ -137,9 +151,9 @@ const resolveSecrets = async (
|
|
|
137
151
|
)
|
|
138
152
|
.map(([key, value]) => [
|
|
139
153
|
key,
|
|
140
|
-
|
|
141
|
-
(acc,
|
|
142
|
-
acc.replace(new RegExp("\\$" +
|
|
154
|
+
secretEntries.reduce(
|
|
155
|
+
(acc, [name, secretValue]) =>
|
|
156
|
+
acc.replace(new RegExp("\\$" + name, "g"), secretValue),
|
|
143
157
|
`${value}`,
|
|
144
158
|
),
|
|
145
159
|
]),
|
|
@@ -148,7 +162,7 @@ const resolveSecrets = async (
|
|
|
148
162
|
};
|
|
149
163
|
|
|
150
164
|
export const getEnvVarsResolved = async (
|
|
151
|
-
io: IO
|
|
165
|
+
io: IO,
|
|
152
166
|
env: string,
|
|
153
167
|
componentName: string | null,
|
|
154
168
|
) => {
|