Skip to content

Commit

Permalink
Merge pull request #212 from MatrixAI/feature-eng-302-polykey-docspol…
Browse files Browse the repository at this point in the history
…ykey-secrets-env-command-doesnt-align-with

Align `secrets env` command with design in documentation
  • Loading branch information
tegefaulkes committed Jun 28, 2024
2 parents 0cd74eb + 9768aaa commit f511693
Show file tree
Hide file tree
Showing 7 changed files with 330 additions and 189 deletions.
2 changes: 1 addition & 1 deletion npmDepsHash
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sha256-KFydumA3XA3dLkx+27/XVAZ79+ISJf4LpESBOh9qGEI=
sha256-eDUzTAx7aYrCZinucP9pNHaayK66WNiPz3vOs9H5xNI=
33 changes: 33 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
"@matrixai/errors": "^1.2.0",
"@matrixai/logger": "^3.1.0",
"@matrixai/exec": "^0.1.4",
"@fast-check/jest": "^1.1.0",
"@swc/core": "1.3.82",
"@swc/jest": "^0.2.29",
"@types/jest": "^29.5.2",
Expand Down
206 changes: 88 additions & 118 deletions src/secrets/CommandEnv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,71 +7,47 @@ import * as binUtils from '../utils';
import * as binErrors from '../errors';
import CommandPolykey from '../CommandPolykey';
import * as binOptions from '../utils/options';

const description = `
Run a command with the given secrets and env variables. If no command is specified then the variables are printed to stdout in the format specified by env-format.
When selecting secrets with --env secrets with invalid names can be selected. By default when these are encountered then the command will throw an error. This behaviour can be modified with '--env-invalid'. the invalid name can be silently dropped with 'ignore' or logged out with 'warn'
Duplicate secret names can be specified, by default with 'overwrite' the env variable will be overwritten with the latest found secret of that name. It can be specified to 'keep' the first found secret of that name, 'error' to throw if there is a duplicate and 'warn' to log a warning while overwriting.
`;

const helpText = `
This command has two main ways of functioning. Executing a provided command or outputting formatted env variables to] stdout.
Running the command with 'polykey secrets env --env vault:secret -- some command' will do process replacement to run 'some command' while providing environment variables selected by '-e' to that process. Note that process replacement is only supported on unix systems such as linux or macos. When running on windows a child process will be used.
Running the command with 'polykey secrets env --env vault:secret --env-format <format>' will output the environment variables to stdout with the given <format>. The following formats are supported, 'auto', 'json', 'unix', 'cmd' and 'powershell'.
'auto' will automatically detect the current platform and select the appropriate format. This is 'unix' for unix based systems and 'cmd' for windows.
'json' Will format the environment variables as a json object in the form {'key': 'value'}.
'unix' Will format the environment variables as a '.env' file for use on unix systems. It will include comments before each variable showing the secret path used for that variable.
'cmd' Will format the environment variables as a '.bat' file for use on windows cmd. It will include comments before each variable showing the secret path used for that variable.
'powershell' Will format the environment variables as a '.ps1' file for use on windows Powershell. It will include comments before each variable showing the secret path used for that variable.
`;
import * as binParsers from '../utils/parsers';

class CommandEnv extends CommandPolykey {
constructor(...args: ConstructorParameters<typeof CommandPolykey>) {
super(...args);
this.name('env');
this.description(description);
this.description(
`Run a command with the given secrets and env variables using process replacement. If no command is specified then the variables are printed to stdout in the format specified by env-format.`,
);
this.addOption(binOptions.nodeId);
this.addOption(binOptions.clientHost);
this.addOption(binOptions.clientPort);
this.addOption(binOptions.envVariables);
this.addOption(binOptions.envFormat);
this.addOption(binOptions.envInvalid);
this.addOption(binOptions.envDuplicate);
this.argument('[cmd] [argv...]', 'command and arguments');
this.addHelpText('after', helpText);
this.argument(
'<args...>',
'command and arguments formatted as [envPaths...][cmd][cmdArgs...]',
binParsers.testParse,
);
this.action(async (args: Array<string>, options) => {
const [cmd, ...argv] = args;
const { default: PolykeyClient } = await import(
'polykey/dist/PolykeyClient'
);
const {
env: envVariables,
envInvalid,
envDuplicate,
envFormat,
}: {
env: Array<[string, string, string?]>;
envInvalid: 'error' | 'warn' | 'ignore';
envDuplicate: 'keep' | 'overwrite' | 'warn' | 'error';
envFormat: 'auto' | 'unix' | 'cmd' | 'powershell' | 'json';
} = options;

// There are a few stages here
// 1. parse the desired secrets
// 2. obtain the desired secrets
// 3. switching behaviour here based on parameters
// a. exec the command with the provided env variables from the secrets
// b. output the env variables in the desired format

const { default: PolykeyClient } = await import(
'polykey/dist/PolykeyClient'
);
const [envVariables, [cmd, ...argv]] = args;
const clientOptions = await binProcessors.processClientOptions(
options.nodePath,
options.nodeId,
Expand Down Expand Up @@ -101,95 +77,92 @@ class CommandEnv extends CommandPolykey {
});

// Getting envs
const [envp, envpPath] = await binUtils.retryAuthentication(
async (auth) => {
const responseStream =
await pkClient.rpcClient.methods.vaultsSecretsEnv();
// Writing desired secrets
const secretRenameMap = new Map<string, string | undefined>();
const writeP = (async () => {
const writer = responseStream.writable.getWriter();
let first = true;
for (const envVariable of envVariables) {
const [nameOrId, secretName, secretNameNew] = envVariable;
secretRenameMap.set(secretName, secretNameNew);
await writer.write({
nameOrId,
secretName,
metadata: first ? auth : undefined,
});
first = false;
}
await writer.close();
})();
const [envp] = await binUtils.retryAuthentication(async (auth) => {
const responseStream =
await pkClient.rpcClient.methods.vaultsSecretsEnv();
// Writing desired secrets
const secretRenameMap = new Map<string, string | undefined>();
const writeP = (async () => {
const writer = responseStream.writable.getWriter();
let first = true;
for (const envVariable of envVariables) {
const [nameOrId, secretName, secretNameNew] = envVariable;
secretRenameMap.set(secretName, secretNameNew);
await writer.write({
nameOrId,
secretName,
metadata: first ? auth : undefined,
});
first = false;
}
await writer.close();
})();

const envp: Record<string, string> = {};
const envpPath: Record<
string,
{
nameOrId: string;
secretName: string;
}
> = {};
for await (const value of responseStream.readable) {
const { nameOrId, secretName, secretContent } = value;
let newName = secretRenameMap.get(secretName);
if (newName == null) {
const secretEnvName = path.basename(secretName);
// Validating name
if (!binUtils.validEnvRegex.test(secretEnvName)) {
switch (envInvalid) {
case 'error':
throw new binErrors.ErrorPolykeyCLIInvalidEnvName(
`The following env variable name (${secretEnvName}) is invalid`,
);
case 'warn':
this.logger.warn(
`The following env variable name (${secretEnvName}) is invalid and was dropped`,
);
// Fallthrough
case 'ignore':
continue;
default:
utils.never();
}
}
newName = secretEnvName;
}
// Handling duplicate names
if (envp[newName] != null) {
switch (envDuplicate) {
// Continue without modifying
const envp: Record<string, string> = {};
const envpPath: Record<
string,
{
nameOrId: string;
secretName: string;
}
> = {};
for await (const value of responseStream.readable) {
const { nameOrId, secretName, secretContent } = value;
let newName = secretRenameMap.get(secretName);
if (newName == null) {
const secretEnvName = path.basename(secretName);
// Validating name
if (!binUtils.validEnvRegex.test(secretEnvName)) {
switch (envInvalid) {
case 'error':
throw new binErrors.ErrorPolykeyCLIDuplicateEnvName(
`The env variable (${newName}) is duplicate`,
throw new binErrors.ErrorPolykeyCLIInvalidEnvName(
`The following env variable name (${secretEnvName}) is invalid`,
);
// Fallthrough
case 'keep':
continue;
// Log a warning and overwrite
case 'warn':
this.logger.warn(
`The env variable (${newName}) is duplicate, overwriting`,
`The following env variable name (${secretEnvName}) is invalid and was dropped`,
);
// Fallthrough
case 'overwrite':
break;
case 'ignore':
continue;
default:
utils.never();
}
}
envp[newName] = secretContent;
envpPath[newName] = {
nameOrId,
secretName,
};
newName = secretEnvName;
}
await writeP;
return [envp, envpPath];
},
meta,
);
// Handling duplicate names
if (envp[newName] != null) {
switch (envDuplicate) {
// Continue without modifying
case 'error':
throw new binErrors.ErrorPolykeyCLIDuplicateEnvName(
`The env variable (${newName}) is duplicate`,
);
// Fallthrough
case 'keep':
continue;
// Log a warning and overwrite
case 'warn':
this.logger.warn(
`The env variable (${newName}) is duplicate, overwriting`,
);
// Fallthrough
case 'overwrite':
break;
default:
utils.never();
}
}
envp[newName] = secretContent;
envpPath[newName] = {
nameOrId,
secretName,
};
}
await writeP;
return [envp, envpPath];
}, meta);
// End connection early to avoid errors on server
await pkClient.stop();

Expand Down Expand Up @@ -238,8 +211,7 @@ class CommandEnv extends CommandPolykey {
// Formatting as a .env file
let data = '';
for (const [key, value] of Object.entries(envp)) {
data += `# ${envpPath[key].nameOrId}:${envpPath[key].secretName}\n`;
data += `${key}="${value}"\n`;
data += `${key}='${value}'\n`;
}
process.stdout.write(
binUtils.outputFormatter({
Expand All @@ -254,7 +226,6 @@ class CommandEnv extends CommandPolykey {
// Formatting as a .bat file for windows cmd
let data = '';
for (const [key, value] of Object.entries(envp)) {
data += `REM ${envpPath[key].nameOrId}:${envpPath[key].secretName}\n`;
data += `set "${key}=${value}"\n`;
}
process.stdout.write(
Expand All @@ -270,7 +241,6 @@ class CommandEnv extends CommandPolykey {
// Formatting as a .bat file for windows cmd
let data = '';
for (const [key, value] of Object.entries(envp)) {
data += `# ${envpPath[key].nameOrId}:${envpPath[key].secretName}\n`;
data += `\$env:${key} = '${value}'\n`;
}
process.stdout.write(
Expand Down
Loading

0 comments on commit f511693

Please sign in to comment.