Skip to content

Commit 17dc0be

Browse files
committed
feat: secrets env command
1 parent f6f8159 commit 17dc0be

6 files changed

Lines changed: 578 additions & 184 deletions

File tree

npmDepsHash

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
sha256-zPxb3VUEeVjeWsIpBlszq3RWeZh3/0ICKRw/SyMCKsw=
1+
sha256-LZ3QekhwOZj3IkbnZVdC/lbUvwhbJXROzd9oTxrMs7s=

src/secrets/CommandEnv.ts

Lines changed: 170 additions & 179 deletions
Original file line numberDiff line numberDiff line change
@@ -1,179 +1,170 @@
1-
// Import { spawn } from 'child_process';
2-
// import Logger, { LogLevel, StreamHandler } from '@matrixai/logger';
3-
// import * as vaultsPB from 'polykey/dist/proto/js/polykey/v1/vaults/vaults_pb';
4-
// import * as secretsPB from 'polykey/dist/proto/js/polykey/v1/secrets/secrets_pb';
5-
// import PolykeyClient from 'polykey/dist/PolykeyClient';
6-
// import * as utils from 'polykey/dist/utils';
7-
// import * as binUtils from '../utils';
8-
// import * as CLIErrors from '../errors';
9-
// import * as grpcErrors from 'polykey/dist/grpc/errors';
10-
11-
// import CommandPolykey from '../CommandPolykey';
12-
// import * as binOptions from '../utils/options';
13-
14-
// class CommandEnv extends CommandPolykey {
15-
// constructor(...args: ConstructorParameters<typeof CommandPolykey>) {
16-
// super(...args);
17-
// this.name('env');
18-
// this.description('Secrets Env');
19-
// this.option(
20-
// '--command <command>',
21-
// 'In the environment of the derivation, run the shell command cmd in an interactive shell (Use --run to use a non-interactive shell instead)',
22-
// );
23-
// this.option(
24-
// '--run <run>',
25-
// 'In the environment of the derivation, run the shell command cmd in a non-interactive shell, meaning (among other things) that if you hit Ctrl-C while the command is running, the shell exits (Use --command to use an interactive shell instead)',
26-
// );
27-
// this.arguments(
28-
// "Secrets to inject into env, of the format '<vaultName>:<secretPath>[=<variableName>]', you can also control what the environment variable will be called using '[<variableName>]' (defaults to upper, snake case of the original secret name)",
29-
// );
30-
// this.addOption(binOptions.nodeId);
31-
// this.addOption(binOptions.clientHost);
32-
// this.addOption(binOptions.clientPort);
33-
// this.action(async (options, command) => {
34-
35-
// });
36-
// }
37-
// }
38-
39-
// export default CommandEnv;
40-
41-
// OLD COMMAND
42-
// const env = binUtils.createCommand('env', {
43-
// description: 'Runs a modified environment with injected secrets',
44-
// nodePath: true,
45-
// verbose: true,
46-
// format: true,
47-
// });
48-
// env.option(
49-
// '--command <command>',
50-
// 'In the environment of the derivation, run the shell command cmd in an interactive shell (Use --run to use a non-interactive shell instead)',
51-
// );
52-
// env.option(
53-
// '--run <run>',
54-
// 'In the environment of the derivation, run the shell command cmd in a non-interactive shell, meaning (among other things) that if you hit Ctrl-C while the command is running, the shell exits (Use --command to use an interactive shell instead)',
55-
// );
56-
// env.arguments(
57-
// "Secrets to inject into env, of the format '<vaultName>:<secretPath>[=<variableName>]', you can also control what the environment variable will be called using '[<variableName>]' (defaults to upper, snake case of the original secret name)",
58-
// );
59-
// env.action(async (options, command) => {
60-
// const clientConfig = {};
61-
// clientConfig['logger'] = new Logger('CLI Logger', LogLevel.WARN, [
62-
// new StreamHandler(),
63-
// ]);
64-
// if (options.verbose) {
65-
// clientConfig['logger'].setLevel(LogLevel.DEBUG);
66-
// }
67-
// clientConfig['nodePath'] = options.nodePath
68-
// ? options.nodePath
69-
// : utils.getDefaultNodePath();
70-
71-
// const client = await PolykeyClient.createPolykeyClient(clientConfig);
72-
// const vaultMessage = new vaultsPB.Vault();
73-
// const secretMessage = new secretsPB.Secret();
74-
// secretMessage.setVault(vaultMessage);
75-
// const secretPathList: string[] = Array.from<string>(command.args.values());
76-
77-
// try {
78-
// if (secretPathList.length < 1) {
79-
// throw new CLIErrors.ErrorSecretsUndefined();
80-
// }
81-
82-
// const parsedPathList: {
83-
// vaultName: string;
84-
// secretName: string;
85-
// variableName: string;
86-
// }[] = [];
87-
88-
// for (const path of secretPathList) {
89-
// if (!binUtils.pathRegex.test(path)) {
90-
// throw new CLIErrors.ErrorSecretPathFormat();
91-
// }
92-
93-
// const [, vaultName, secretName, variableName] = path.match(
94-
// binUtils.pathRegex,
95-
// )!;
96-
// parsedPathList.push({
97-
// vaultName,
98-
// secretName,
99-
// variableName:
100-
// variableName ?? secretName.toUpperCase().replace('-', '_'),
101-
// });
102-
// }
103-
104-
// const secretEnv = { ...process.env };
105-
106-
// await client.start({});
107-
// const grpcClient = client.grpcClient;
108-
109-
// for (const obj of parsedPathList) {
110-
// vaultMessage.setNameOrId(obj.vaultName);
111-
// secretMessage.setSecretName(obj.secretName);
112-
// const res = await binUtils.unaryCallCARL<secretsPB.Secret>(
113-
// client,
114-
// attemptUnaryCall(client, grpcClient.vaultsSecretsGet),
115-
// )(secretMessage);
116-
117-
// const secret = res.getSecretName();
118-
// secretEnv[obj.variableName] = secret;
119-
// }
120-
121-
// const shellPath = process.env.SHELL ?? 'sh';
122-
// const args: string[] = [];
123-
124-
// if (options.command && options.run) {
125-
// throw new CLIErrors.ErrorInvalidArguments(
126-
// 'Only one of --command or --run can be specified',
127-
// );
128-
// } else if (options.command) {
129-
// args.push('-i');
130-
// args.push('-c');
131-
// args.push(`"${options.command}"`);
132-
// } else if (options.run) {
133-
// args.push('-c');
134-
// args.push(`"${options.run}"`);
135-
// }
136-
137-
// const shell = spawn(shellPath, args, {
138-
// stdio: 'inherit',
139-
// env: secretEnv,
140-
// shell: true,
141-
// });
142-
143-
// shell.on('close', (code) => {
144-
// if (code !== 0) {
145-
// process.stdout.write(
146-
// binUtils.outputFormatter({
147-
// type: options.format === 'json' ? 'json' : 'list',
148-
// data: [`Terminated with ${code}`],
149-
// }),
150-
// );
151-
// }
152-
// });
153-
// } catch (err) {
154-
// if (err instanceof grpcErrors.ErrorGRPCClientTimeout) {
155-
// process.stderr.write(`${err.message}\n`);
156-
// }
157-
// if (err instanceof grpcErrors.ErrorGRPCServerNotStarted) {
158-
// process.stderr.write(`${err.message}\n`);
159-
// } else {
160-
// process.stderr.write(
161-
// binUtils.outputFormatter({
162-
// type: 'error',
163-
// description: err.description,
164-
// message: err.message,
165-
// }),
166-
// );
167-
// throw err;
168-
// }
169-
// } finally {
170-
// await client.stop();
171-
// options.nodePath = undefined;
172-
// options.verbose = undefined;
173-
// options.format = undefined;
174-
// options.command = undefined;
175-
// options.run = undefined;
176-
// }
177-
// });
178-
179-
// export default env;
1+
import type PolykeyClient from 'polykey/dist/PolykeyClient';
2+
import path from 'path';
3+
import * as utils from 'polykey/dist/utils';
4+
import { exec } from '@matrixai/exec';
5+
import * as binProcessors from '../utils/processors';
6+
import * as binUtils from '../utils';
7+
import CommandPolykey from '../CommandPolykey';
8+
import * as binOptions from '../utils/options';
9+
10+
class CommandEnv extends CommandPolykey {
11+
constructor(...args: ConstructorParameters<typeof CommandPolykey>) {
12+
super(...args);
13+
this.name('env');
14+
this.description(
15+
'Run a command with the given secrets and env variables. If no command is specified then the variables are printed to stdout.',
16+
);
17+
this.addOption(binOptions.nodeId);
18+
this.addOption(binOptions.clientHost);
19+
this.addOption(binOptions.clientPort);
20+
this.addOption(binOptions.envVariables);
21+
this.addOption(binOptions.envFormat);
22+
this.argument('[cmd] [argv...]', 'command and arguments');
23+
this.action(async (args: Array<string>, options) => {
24+
const [cmd, ...argv] = args;
25+
const {
26+
env: envVariables,
27+
outputFormat,
28+
}: {
29+
env: Array<[string, string, string?]>;
30+
outputFormat: 'dotenv' | 'json' | 'prepend';
31+
} = options;
32+
33+
// There are a few stages here
34+
// 1. parse the desired secrets
35+
// 2. obtain the desired secrets
36+
// 3. switching behaviour here based on parameters
37+
// a. exec the command with the provided env variables from the secrets
38+
// b. output the env variables in the desired format
39+
40+
const { default: PolykeyClient } = await import(
41+
'polykey/dist/PolykeyClient'
42+
);
43+
const clientOptions = await binProcessors.processClientOptions(
44+
options.nodePath,
45+
options.nodeId,
46+
options.clientHost,
47+
options.clientPort,
48+
this.fs,
49+
this.logger.getChild(binProcessors.processClientOptions.name),
50+
);
51+
const meta = await binProcessors.processAuthentication(
52+
options.passwordFile,
53+
this.fs,
54+
);
55+
56+
let pkClient: PolykeyClient;
57+
this.exitHandlers.handlers.push(async () => {
58+
if (pkClient != null) await pkClient.stop();
59+
});
60+
try {
61+
pkClient = await PolykeyClient.createPolykeyClient({
62+
nodeId: clientOptions.nodeId,
63+
host: clientOptions.clientHost,
64+
port: clientOptions.clientPort,
65+
options: {
66+
nodePath: options.nodePath,
67+
},
68+
logger: this.logger.getChild(PolykeyClient.name),
69+
});
70+
71+
// Getting envs
72+
const envp = await binUtils.retryAuthentication(async (auth) => {
73+
const responseStream =
74+
await pkClient.rpcClient.methods.vaultsSecretsEnv();
75+
// Writing desired secrets
76+
const secretRenameMap = new Map<string, string | undefined>();
77+
const writeP = (async () => {
78+
const writer = responseStream.writable.getWriter();
79+
let first = true;
80+
for (const envVariable of envVariables) {
81+
const [nameOrId, secretName, secretNameNew] = envVariable;
82+
secretRenameMap.set(secretName, secretNameNew);
83+
await writer.write({
84+
nameOrId,
85+
secretName,
86+
metadata: first ? auth : undefined,
87+
});
88+
first = false;
89+
}
90+
await writer.close();
91+
})();
92+
93+
const envp: Record<string, string> = {};
94+
for await (const value of responseStream.readable) {
95+
const { secretName, secretContent } = value;
96+
let newName = secretRenameMap.get(secretName);
97+
newName = newName ?? path.basename(secretName);
98+
envp[newName] = secretContent;
99+
}
100+
await writeP;
101+
return envp;
102+
}, meta);
103+
// End connection early to avoid errors on server
104+
await pkClient.stop();
105+
106+
// Here we want to switch between the different usages
107+
if (cmd != null) {
108+
// If a cmd is provided then we default to exec it
109+
exec.execvp(cmd, argv, envp);
110+
} else {
111+
// Otherwise we switch between output formats
112+
switch (outputFormat) {
113+
case 'dotenv':
114+
{
115+
// Formatting as a .env file
116+
let data = '';
117+
for (const [key, value] of Object.entries(envp)) {
118+
data += `${key}="${value}"\n`;
119+
}
120+
process.stdout.write(
121+
binUtils.outputFormatter({
122+
type: 'raw',
123+
data,
124+
}),
125+
);
126+
}
127+
break;
128+
case 'prepend':
129+
{
130+
// Formatting as a command input
131+
let first = true;
132+
let data = '';
133+
for (const [key, value] of Object.entries(envp)) {
134+
data += `${first ? '' : ' '}${key}="${value}"`;
135+
first = false;
136+
}
137+
process.stdout.write(
138+
binUtils.outputFormatter({
139+
type: 'raw',
140+
data,
141+
}),
142+
);
143+
}
144+
break;
145+
case 'json':
146+
{
147+
const data = {};
148+
for (const [key, value] of Object.entries(envp)) {
149+
data[key] = value;
150+
}
151+
process.stdout.write(
152+
binUtils.outputFormatter({
153+
type: 'json',
154+
data: data,
155+
}),
156+
);
157+
}
158+
break;
159+
default:
160+
utils.never();
161+
}
162+
}
163+
} finally {
164+
if (pkClient! != null) await pkClient.stop();
165+
}
166+
});
167+
}
168+
}
169+
170+
export default CommandEnv;

src/secrets/CommandSecrets.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import CommandCreate from './CommandCreate';
22
import CommandDelete from './CommandDelete';
33
import CommandDir from './CommandDir';
44
import CommandEdit from './CommandEdit';
5-
// Import CommandEnv from './CommandEnv';
5+
import CommandEnv from './CommandEnv';
66
import CommandGet from './CommandGet';
77
import CommandList from './CommandList';
88
import CommandMkdir from './CommandMkdir';
@@ -20,7 +20,7 @@ class CommandSecrets extends CommandPolykey {
2020
this.addCommand(new CommandDelete(...args));
2121
this.addCommand(new CommandDir(...args));
2222
this.addCommand(new CommandEdit(...args));
23-
// This.addCommand(new CommandEnv(...args));
23+
this.addCommand(new CommandEnv(...args));
2424
this.addCommand(new CommandGet(...args));
2525
this.addCommand(new CommandList(...args));
2626
this.addCommand(new CommandMkdir(...args));

0 commit comments

Comments
 (0)