From 11a3c99a0eb64b04dee9de1ba0f327ac6e2ef319 Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Thu, 29 Feb 2024 10:29:06 +1100 Subject: [PATCH 1/4] fix: reverted changes to `--format` and added `--env-format` to `secrets env` command `--format` still affects the logging format and `--env-format` is used to select the env output. [ci skip] --- src/secrets/CommandEnv.ts | 16 ++++------------ src/utils/options.ts | 8 ++++++++ tests/secrets/env.test.ts | 8 ++++---- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/secrets/CommandEnv.ts b/src/secrets/CommandEnv.ts index bd52464a..dcf7d745 100644 --- a/src/secrets/CommandEnv.ts +++ b/src/secrets/CommandEnv.ts @@ -11,15 +11,6 @@ import * as binOptions from '../utils/options'; class CommandEnv extends CommandPolykey { constructor(...args: ConstructorParameters) { super(...args); - - // Modify the --format choices to include `dotenv` and `prepend` - // @ts-ignore: missing type for `options` - const formatOption = this.options.filter((v) => v.short === '-f')[0]; - if (formatOption == null) { - utils.never('The -f --format option should exist'); - } - formatOption.argChoices.push('dotenv', 'prepend'); - this.name('env'); this.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.\n\nWhen 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'\n\nDuplicate 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.`, @@ -28,6 +19,7 @@ class CommandEnv extends CommandPolykey { 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'); @@ -37,12 +29,12 @@ class CommandEnv extends CommandPolykey { env: envVariables, envInvalid, envDuplicate, - format, + envFormat, }: { env: Array<[string, string, string?]>; envInvalid: 'error' | 'warn' | 'ignore'; envDuplicate: 'keep' | 'overwrite' | 'warn' | 'error'; - format: 'human' | 'dotenv' | 'json' | 'prepend'; + envFormat: 'human' | 'dotenv' | 'json' | 'prepend'; } = options; // There are a few stages here @@ -191,7 +183,7 @@ class CommandEnv extends CommandPolykey { } } else { // Otherwise we switch between output formats - switch (format) { + switch (envFormat) { case 'human': // Fallthrough case 'dotenv': diff --git a/src/utils/options.ts b/src/utils/options.ts index 746ad351..a5c90e32 100644 --- a/src/utils/options.ts +++ b/src/utils/options.ts @@ -207,6 +207,13 @@ const envVariables = new commander.Option('-e --env ', 'specify envs') }, ); +const envFormat = new commander.Option( + '-ef --env-format ', + 'Select how the env variables are formatted on stdout if no command is specified', +) + .choices(['human', 'json', 'dotenv', 'prepend']) + .default('human'); + const envInvalid = new commander.Option( '-ei --env-invalid ', 'How invalid env variable names are handled when retrieving secrets. `error` will throw, `warn` will log a warning and drop and `ignore` will silently drop.', @@ -250,6 +257,7 @@ export { depth, commitId, envVariables, + envFormat, envInvalid, envDuplicate, }; diff --git a/tests/secrets/env.test.ts b/tests/secrets/env.test.ts index 5ff5db46..ce93225e 100644 --- a/tests/secrets/env.test.ts +++ b/tests/secrets/env.test.ts @@ -310,7 +310,7 @@ describe('commandEnv', () => { dataDir, '-e', `${vaultName}:.`, - '--format', + '--env-format', 'human', ]; @@ -339,7 +339,7 @@ describe('commandEnv', () => { dataDir, '-e', `${vaultName}:.`, - '--format', + '--env-format', 'dotenv', ]; @@ -368,7 +368,7 @@ describe('commandEnv', () => { dataDir, '-e', `${vaultName}:.`, - '--format', + '--env-format', 'json', ]; @@ -399,7 +399,7 @@ describe('commandEnv', () => { dataDir, '-e', `${vaultName}:.`, - '--format', + '--env-format', 'prepend', ]; From 4e2686b378dee3bd13d04ca394c5d68f43b5edcd Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Thu, 29 Feb 2024 12:00:15 +1100 Subject: [PATCH 2/4] feat: added windows CMD formats to `secrets env command` [ci skip] --- src/secrets/CommandEnv.ts | 199 ++++++++++++++++++++++++++------------ src/utils/options.ts | 13 ++- tests/secrets/env.test.ts | 193 +++++++++++++++++++++++++++++++----- 3 files changed, 317 insertions(+), 88 deletions(-) diff --git a/src/secrets/CommandEnv.ts b/src/secrets/CommandEnv.ts index dcf7d745..e0d86d01 100644 --- a/src/secrets/CommandEnv.ts +++ b/src/secrets/CommandEnv.ts @@ -34,7 +34,14 @@ class CommandEnv extends CommandPolykey { env: Array<[string, string, string?]>; envInvalid: 'error' | 'warn' | 'ignore'; envDuplicate: 'keep' | 'overwrite' | 'warn' | 'error'; - envFormat: 'human' | 'dotenv' | 'json' | 'prepend'; + envFormat: + | 'human' + | 'dotenv' + | 'dotbat' + | 'dotps' + | 'json' + | 'prepend' + | 'prependCmd'; } = options; // There are a few stages here @@ -76,81 +83,95 @@ class CommandEnv extends CommandPolykey { }); // Getting envs - const envp = await binUtils.retryAuthentication(async (auth) => { - const responseStream = - await pkClient.rpcClient.methods.vaultsSecretsEnv(); - // Writing desired secrets - const secretRenameMap = new Map(); - 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, envpPath] = await binUtils.retryAuthentication( + async (auth) => { + const responseStream = + await pkClient.rpcClient.methods.vaultsSecretsEnv(); + // Writing desired secrets + const secretRenameMap = new Map(); + 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 = {}; - for await (const value of responseStream.readable) { - const { 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) { + const envp: Record = {}; + 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 case 'error': - throw new binErrors.ErrorPolykeyCLIInvalidEnvName( - `The following env variable name (${secretEnvName}) is invalid`, + 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 following env variable name (${secretEnvName}) is invalid and was dropped`, + `The env variable (${newName}) is duplicate, overwriting`, ); // Fallthrough - case 'ignore': - continue; + case 'overwrite': + break; default: utils.never(); } } - newName = secretEnvName; - } - // 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, + }; } - envp[newName] = secretContent; - } - await writeP; - return envp; - }, meta); + await writeP; + return [envp, envpPath]; + }, + meta, + ); // End connection early to avoid errors on server await pkClient.stop(); @@ -191,6 +212,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`; } process.stdout.write( @@ -201,6 +223,38 @@ class CommandEnv extends CommandPolykey { ); } break; + case 'dotbat': + { + // 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( + binUtils.outputFormatter({ + type: 'raw', + data, + }), + ); + } + break; + case 'dotps': + { + // 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( + binUtils.outputFormatter({ + type: 'raw', + data, + }), + ); + } + break; case 'prepend': { // Formatting as a command input @@ -218,6 +272,23 @@ class CommandEnv extends CommandPolykey { ); } break; + case 'prependCmd': + { + // Formatting as a command input + let first = true; + let data = ''; + for (const [key, value] of Object.entries(envp)) { + data += `${first ? '' : ' & '}set "${key}=${value}"`; + first = false; + } + process.stdout.write( + binUtils.outputFormatter({ + type: 'raw', + data, + }), + ); + } + break; case 'json': { const data = {}; diff --git a/src/utils/options.ts b/src/utils/options.ts index a5c90e32..6f6d81dd 100644 --- a/src/utils/options.ts +++ b/src/utils/options.ts @@ -211,8 +211,17 @@ const envFormat = new commander.Option( '-ef --env-format ', 'Select how the env variables are formatted on stdout if no command is specified', ) - .choices(['human', 'json', 'dotenv', 'prepend']) - .default('human'); + .choices([ + 'platform', + 'human', + 'json', + 'dotenv', + 'dotbat', + 'dotps', + 'prepend', + 'prependCmd', + ]) + .default('platform'); const envInvalid = new commander.Option( '-ei --env-invalid ', diff --git a/tests/secrets/env.test.ts b/tests/secrets/env.test.ts index ce93225e..f125d2bb 100644 --- a/tests/secrets/env.test.ts +++ b/tests/secrets/env.test.ts @@ -68,6 +68,8 @@ describe('commandEnv', () => { dataDir, '-e', `${vaultName}:SECRET`, + '--env-format', + 'human', '--', 'node', '-e', @@ -95,6 +97,8 @@ describe('commandEnv', () => { '-e', `${vaultName}:SECRET1`, `${vaultName}:SECRET2`, + '--env-format', + 'human', '--', 'node', '-e', @@ -124,6 +128,8 @@ describe('commandEnv', () => { dataDir, '-e', `${vaultName}:dir1`, + '--env-format', + 'human', '--', 'node', '-e', @@ -151,6 +157,8 @@ describe('commandEnv', () => { dataDir, '-e', `${vaultName}:SECRET=SECRET_NEW`, + '--env-format', + 'human', '--', 'node', '-e', @@ -179,6 +187,8 @@ describe('commandEnv', () => { dataDir, '-e', `${vaultName}:dir1=SECRET_NEW`, + '--env-format', + 'human', '--', 'node', '-e', @@ -212,6 +222,8 @@ describe('commandEnv', () => { `${vaultName}:SECRET1`, `${vaultName}:SECRET2`, `${vaultName}:dir1`, + '--env-format', + 'human', '--', 'node', '-e', @@ -240,6 +252,8 @@ describe('commandEnv', () => { dataDir, '-e', `${vaultName}:SECRET1`, + '--env-format', + 'human', '--', 'node', '-e', @@ -277,6 +291,8 @@ describe('commandEnv', () => { `${vaultName}:SECRET2=SECRET1`, `${vaultName}:SECRET3=SECRET4`, `${vaultName}:dir1`, + '--env-format', + 'human', '--', 'node', '-e', @@ -322,15 +338,24 @@ describe('commandEnv', () => { expect(result.stdout).toContain('SECRET4="this is the secret4"'); }); test('should output dotenv format', async () => { - const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); + const vaultId1 = await polykeyAgent.vaultManager.createVault( + `${vaultName}1`, + ); + const vaultId2 = await polykeyAgent.vaultManager.createVault( + `${vaultName}2`, + ); - await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { - await vaultOps.addSecret(vault, 'SECRET1', 'this is the secret1'); - await vaultOps.addSecret(vault, 'SECRET2', 'this is the secret2'); - await vaultOps.mkdir(vault, 'dir1'); - await vaultOps.addSecret(vault, 'dir1/SECRET3', 'this is the secret3'); - await vaultOps.addSecret(vault, 'dir1/SECRET4', 'this is the secret4'); - }); + await polykeyAgent.vaultManager.withVaults( + [vaultId1, vaultId2], + async (vault1, vault2) => { + await vaultOps.addSecret(vault1, 'SECRET1', 'this is the secret1'); + await vaultOps.addSecret(vault2, 'SECRET2', 'this is the secret2'); + await vaultOps.mkdir(vault1, 'dir1'); + await vaultOps.mkdir(vault2, 'dir1'); + await vaultOps.addSecret(vault1, 'dir1/SECRET3', 'this is the secret3'); + await vaultOps.addSecret(vault2, 'dir1/SECRET4', 'this is the secret4'); + }, + ); command = [ 'secrets', @@ -338,18 +363,109 @@ describe('commandEnv', () => { '-np', dataDir, '-e', - `${vaultName}:.`, + `${vaultName}1:.`, + `${vaultName}2:.`, '--env-format', 'dotenv', ]; const result = await testUtils.pkExec([...command]); expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('# vault1:SECRET1'); expect(result.stdout).toContain('SECRET1="this is the secret1"'); + expect(result.stdout).toContain('# vault2:SECRET2'); expect(result.stdout).toContain('SECRET2="this is the secret2"'); + expect(result.stdout).toContain('# vault1:dir1/SECRET3'); expect(result.stdout).toContain('SECRET3="this is the secret3"'); + expect(result.stdout).toContain('# vault2:dir1/SECRET4'); expect(result.stdout).toContain('SECRET4="this is the secret4"'); }); + test('should output dotbat format', async () => { + const vaultId1 = await polykeyAgent.vaultManager.createVault( + `${vaultName}1`, + ); + const vaultId2 = await polykeyAgent.vaultManager.createVault( + `${vaultName}2`, + ); + + await polykeyAgent.vaultManager.withVaults( + [vaultId1, vaultId2], + async (vault1, vault2) => { + await vaultOps.addSecret(vault1, 'SECRET1', 'this is the secret1'); + await vaultOps.addSecret(vault2, 'SECRET2', 'this is the secret2'); + await vaultOps.mkdir(vault1, 'dir1'); + await vaultOps.mkdir(vault2, 'dir1'); + await vaultOps.addSecret(vault1, 'dir1/SECRET3', 'this is the secret3'); + await vaultOps.addSecret(vault2, 'dir1/SECRET4', 'this is the secret4'); + }, + ); + + command = [ + 'secrets', + 'env', + '-np', + dataDir, + '-e', + `${vaultName}1:.`, + `${vaultName}2:.`, + '--env-format', + 'dotbat', + ]; + + const result = await testUtils.pkExec([...command]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('REM vault1:SECRET1'); + expect(result.stdout).toContain('set "SECRET1=this is the secret1"'); + expect(result.stdout).toContain('REM vault2:SECRET2'); + expect(result.stdout).toContain('set "SECRET2=this is the secret2"'); + expect(result.stdout).toContain('REM vault1:dir1/SECRET3'); + expect(result.stdout).toContain('set "SECRET3=this is the secret3"'); + expect(result.stdout).toContain('REM vault2:dir1/SECRET4'); + expect(result.stdout).toContain('set "SECRET4=this is the secret4"'); + }); + test('should output dotps format', async () => { + const vaultId1 = await polykeyAgent.vaultManager.createVault( + `${vaultName}1`, + ); + const vaultId2 = await polykeyAgent.vaultManager.createVault( + `${vaultName}2`, + ); + + await polykeyAgent.vaultManager.withVaults( + [vaultId1, vaultId2], + async (vault1, vault2) => { + await vaultOps.addSecret(vault1, 'SECRET1', 'this is the secret1'); + await vaultOps.addSecret(vault2, 'SECRET2', 'this is the secret2'); + await vaultOps.mkdir(vault1, 'dir1'); + await vaultOps.mkdir(vault2, 'dir1'); + await vaultOps.addSecret(vault1, 'dir1/SECRET3', 'this is the secret3'); + await vaultOps.addSecret(vault2, 'dir1/SECRET4', 'this is the secret4'); + }, + ); + + command = [ + 'secrets', + 'env', + '-np', + dataDir, + '-e', + `${vaultName}1:.`, + `${vaultName}2:.`, + '--env-format', + 'dotps', + ]; + + const result = await testUtils.pkExec([...command]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('# vault1:SECRET1'); + expect(result.stdout).toContain(`$env:SECRET1 = 'this is the secret1'`); + expect(result.stdout).toContain('# vault2:SECRET2'); + expect(result.stdout).toContain(`$env:SECRET2 = 'this is the secret2'`); + expect(result.stdout).toContain('# vault1:dir1/SECRET3'); + expect(result.stdout).toContain(`$env:SECRET3 = 'this is the secret3'`); + expect(result.stdout).toContain('# vault2:dir1/SECRET4'); + expect(result.stdout).toContain(`$env:SECRET4 = 'this is the secret4'`); + }); test('should output json format', async () => { const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); @@ -409,6 +525,34 @@ describe('commandEnv', () => { 'SECRET1="this is the secret1" SECRET2="this is the secret2" SECRET3="this is the secret3" SECRET4="this is the secret4"', ); }); + test('should output prependCmd format', async () => { + const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); + + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + await vaultOps.addSecret(vault, 'SECRET1', 'this is the secret1'); + await vaultOps.addSecret(vault, 'SECRET2', 'this is the secret2'); + await vaultOps.mkdir(vault, 'dir1'); + await vaultOps.addSecret(vault, 'dir1/SECRET3', 'this is the secret3'); + await vaultOps.addSecret(vault, 'dir1/SECRET4', 'this is the secret4'); + }); + + command = [ + 'secrets', + 'env', + '-np', + dataDir, + '-e', + `${vaultName}:.`, + '--env-format', + 'prependCmd', + ]; + + const result = await testUtils.pkExec([...command]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toBe( + 'set "SECRET1=this is the secret1" & set "SECRET2=this is the secret2" & set "SECRET3=this is the secret3" & set "SECRET4=this is the secret4"', + ); + }); test('testing valid and invalid rename inputs', async () => { const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); @@ -429,25 +573,14 @@ describe('commandEnv', () => { const invalid = ['123', '123abc', '123_123', '123_abc', '123 abc', ' ']; - command = [ - 'secrets', - 'env', - '-np', - dataDir, - '-e', - `${vaultName}:SECRET=one_123_ABC`, - '--', - 'node', - '-e', - 'console.log(JSON.stringify(process.env))', - ]; - // Checking valid const result = await testUtils.pkExec([ 'secrets', 'env', '-np', dataDir, + '--env-format', + 'human', '-e', ...valid.map((v) => `${vaultName}:SECRET=${v}`), ]); @@ -460,6 +593,8 @@ describe('commandEnv', () => { 'env', '-np', dataDir, + '--env-format', + 'human', '-e', `${vaultName}:SECRET=${nameNew}`, ]); @@ -479,6 +614,8 @@ describe('commandEnv', () => { 'env', '-np', dataDir, + '--env-format', + 'human', '-ei', 'error', '-e', @@ -499,6 +636,8 @@ describe('commandEnv', () => { 'env', '-np', dataDir, + '--env-format', + 'human', '-ei', 'warn', '-e', @@ -523,6 +662,8 @@ describe('commandEnv', () => { 'env', '-np', dataDir, + '--env-format', + 'human', '-ei', 'ignore', '-e', @@ -549,6 +690,8 @@ describe('commandEnv', () => { 'env', '-np', dataDir, + '--env-format', + 'human', '-ed', 'error', '-e', @@ -572,6 +715,8 @@ describe('commandEnv', () => { 'env', '-np', dataDir, + '--env-format', + 'human', '-ed', 'warn', '-e', @@ -597,6 +742,8 @@ describe('commandEnv', () => { 'env', '-np', dataDir, + '--env-format', + 'human', '-ed', 'keep', '-e', @@ -620,6 +767,8 @@ describe('commandEnv', () => { 'env', '-np', dataDir, + '--env-format', + 'human', '-ed', 'overwrite', '-e', From fd5cf7af2fb42d70c39637473473cb3099b00707 Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Thu, 29 Feb 2024 15:57:05 +1100 Subject: [PATCH 3/4] feat: switching default output based on platform [ci skip] --- src/secrets/CommandEnv.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/secrets/CommandEnv.ts b/src/secrets/CommandEnv.ts index e0d86d01..2cb457ca 100644 --- a/src/secrets/CommandEnv.ts +++ b/src/secrets/CommandEnv.ts @@ -35,6 +35,7 @@ class CommandEnv extends CommandPolykey { envInvalid: 'error' | 'warn' | 'ignore'; envDuplicate: 'keep' | 'overwrite' | 'warn' | 'error'; envFormat: + | 'platform' | 'human' | 'dotenv' | 'dotbat' @@ -176,9 +177,9 @@ class CommandEnv extends CommandPolykey { await pkClient.stop(); // Here we want to switch between the different usages + const platform = os.platform(); if (cmd != null) { // If a cmd is| provided then we default to exec it - const platform = os.platform(); switch (platform) { case 'linux': // Fallthrough @@ -204,7 +205,17 @@ class CommandEnv extends CommandPolykey { } } else { // Otherwise we switch between output formats - switch (envFormat) { + // If set to platform then we need to infer the format + let format = envFormat; + if (envFormat === 'platform') { + const platformFormatMap = { + darwin: 'dotenv', + linux: 'dotenv', + win32: 'dotbat', + }; + format = platformFormatMap[platform] ?? 'dotenv'; + } + switch (format) { case 'human': // Fallthrough case 'dotenv': From 6b56ce1f2a585784c27804390cd2432ba71a96de Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Thu, 29 Feb 2024 17:39:51 +1100 Subject: [PATCH 4/4] feat: expanded help text and fixed '--env-format' choice names [ci skip] --- src/secrets/CommandEnv.ts | 95 ++++++++++++++-------------------- src/utils/options.ts | 11 +--- tests/secrets/env.test.ts | 104 +++++++++----------------------------- 3 files changed, 63 insertions(+), 147 deletions(-) diff --git a/src/secrets/CommandEnv.ts b/src/secrets/CommandEnv.ts index 2cb457ca..d0a46396 100644 --- a/src/secrets/CommandEnv.ts +++ b/src/secrets/CommandEnv.ts @@ -8,13 +8,37 @@ 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 ' will output the environment variables to stdout with the given . The following formats are supported, 'platform', 'json', 'unix', 'cmd' and 'powershell'. + +'platform' 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. +`; + class CommandEnv extends CommandPolykey { constructor(...args: ConstructorParameters) { super(...args); this.name('env'); - this.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.\n\nWhen 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'\n\nDuplicate 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.`, - ); + this.description(description); this.addOption(binOptions.nodeId); this.addOption(binOptions.clientHost); this.addOption(binOptions.clientPort); @@ -23,6 +47,7 @@ class CommandEnv extends CommandPolykey { this.addOption(binOptions.envInvalid); this.addOption(binOptions.envDuplicate); this.argument('[cmd] [argv...]', 'command and arguments'); + this.addHelpText('after', helpText); this.action(async (args: Array, options) => { const [cmd, ...argv] = args; const { @@ -34,15 +59,7 @@ class CommandEnv extends CommandPolykey { env: Array<[string, string, string?]>; envInvalid: 'error' | 'warn' | 'ignore'; envDuplicate: 'keep' | 'overwrite' | 'warn' | 'error'; - envFormat: - | 'platform' - | 'human' - | 'dotenv' - | 'dotbat' - | 'dotps' - | 'json' - | 'prepend' - | 'prependCmd'; + envFormat: 'platform' | 'unix' | 'cmd' | 'powershell' | 'json'; } = options; // There are a few stages here @@ -208,17 +225,15 @@ class CommandEnv extends CommandPolykey { // If set to platform then we need to infer the format let format = envFormat; if (envFormat === 'platform') { - const platformFormatMap = { - darwin: 'dotenv', - linux: 'dotenv', - win32: 'dotbat', - }; - format = platformFormatMap[platform] ?? 'dotenv'; + format = + { + darwin: 'unix', + linux: 'unix', + win32: 'cmd', + }[platform] ?? 'unix'; } switch (format) { - case 'human': - // Fallthrough - case 'dotenv': + case 'unix': { // Formatting as a .env file let data = ''; @@ -234,7 +249,7 @@ class CommandEnv extends CommandPolykey { ); } break; - case 'dotbat': + case 'cmd': { // Formatting as a .bat file for windows cmd let data = ''; @@ -250,7 +265,7 @@ class CommandEnv extends CommandPolykey { ); } break; - case 'dotps': + case 'powershell': { // Formatting as a .bat file for windows cmd let data = ''; @@ -266,40 +281,6 @@ class CommandEnv extends CommandPolykey { ); } break; - case 'prepend': - { - // Formatting as a command input - let first = true; - let data = ''; - for (const [key, value] of Object.entries(envp)) { - data += `${first ? '' : ' '}${key}="${value}"`; - first = false; - } - process.stdout.write( - binUtils.outputFormatter({ - type: 'raw', - data, - }), - ); - } - break; - case 'prependCmd': - { - // Formatting as a command input - let first = true; - let data = ''; - for (const [key, value] of Object.entries(envp)) { - data += `${first ? '' : ' & '}set "${key}=${value}"`; - first = false; - } - process.stdout.write( - binUtils.outputFormatter({ - type: 'raw', - data, - }), - ); - } - break; case 'json': { const data = {}; diff --git a/src/utils/options.ts b/src/utils/options.ts index 6f6d81dd..522082bf 100644 --- a/src/utils/options.ts +++ b/src/utils/options.ts @@ -211,16 +211,7 @@ const envFormat = new commander.Option( '-ef --env-format ', 'Select how the env variables are formatted on stdout if no command is specified', ) - .choices([ - 'platform', - 'human', - 'json', - 'dotenv', - 'dotbat', - 'dotps', - 'prepend', - 'prependCmd', - ]) + .choices(['platform', 'json', 'unix', 'cmd', 'powershell']) .default('platform'); const envInvalid = new commander.Option( diff --git a/tests/secrets/env.test.ts b/tests/secrets/env.test.ts index f125d2bb..a171b7b4 100644 --- a/tests/secrets/env.test.ts +++ b/tests/secrets/env.test.ts @@ -69,7 +69,7 @@ describe('commandEnv', () => { '-e', `${vaultName}:SECRET`, '--env-format', - 'human', + 'unix', '--', 'node', '-e', @@ -98,7 +98,7 @@ describe('commandEnv', () => { `${vaultName}:SECRET1`, `${vaultName}:SECRET2`, '--env-format', - 'human', + 'unix', '--', 'node', '-e', @@ -129,7 +129,7 @@ describe('commandEnv', () => { '-e', `${vaultName}:dir1`, '--env-format', - 'human', + 'unix', '--', 'node', '-e', @@ -158,7 +158,7 @@ describe('commandEnv', () => { '-e', `${vaultName}:SECRET=SECRET_NEW`, '--env-format', - 'human', + 'unix', '--', 'node', '-e', @@ -188,7 +188,7 @@ describe('commandEnv', () => { '-e', `${vaultName}:dir1=SECRET_NEW`, '--env-format', - 'human', + 'unix', '--', 'node', '-e', @@ -223,7 +223,7 @@ describe('commandEnv', () => { `${vaultName}:SECRET2`, `${vaultName}:dir1`, '--env-format', - 'human', + 'unix', '--', 'node', '-e', @@ -253,7 +253,7 @@ describe('commandEnv', () => { '-e', `${vaultName}:SECRET1`, '--env-format', - 'human', + 'unix', '--', 'node', '-e', @@ -292,7 +292,7 @@ describe('commandEnv', () => { `${vaultName}:SECRET3=SECRET4`, `${vaultName}:dir1`, '--env-format', - 'human', + 'unix', '--', 'node', '-e', @@ -327,7 +327,7 @@ describe('commandEnv', () => { '-e', `${vaultName}:.`, '--env-format', - 'human', + 'unix', ]; const result = await testUtils.pkExec([...command]); @@ -337,7 +337,7 @@ describe('commandEnv', () => { expect(result.stdout).toContain('SECRET3="this is the secret3"'); expect(result.stdout).toContain('SECRET4="this is the secret4"'); }); - test('should output dotenv format', async () => { + test('should output unix format', async () => { const vaultId1 = await polykeyAgent.vaultManager.createVault( `${vaultName}1`, ); @@ -366,7 +366,7 @@ describe('commandEnv', () => { `${vaultName}1:.`, `${vaultName}2:.`, '--env-format', - 'dotenv', + 'unix', ]; const result = await testUtils.pkExec([...command]); @@ -380,7 +380,7 @@ describe('commandEnv', () => { expect(result.stdout).toContain('# vault2:dir1/SECRET4'); expect(result.stdout).toContain('SECRET4="this is the secret4"'); }); - test('should output dotbat format', async () => { + test('should output cmd format', async () => { const vaultId1 = await polykeyAgent.vaultManager.createVault( `${vaultName}1`, ); @@ -409,7 +409,7 @@ describe('commandEnv', () => { `${vaultName}1:.`, `${vaultName}2:.`, '--env-format', - 'dotbat', + 'cmd', ]; const result = await testUtils.pkExec([...command]); @@ -423,7 +423,7 @@ describe('commandEnv', () => { expect(result.stdout).toContain('REM vault2:dir1/SECRET4'); expect(result.stdout).toContain('set "SECRET4=this is the secret4"'); }); - test('should output dotps format', async () => { + test('should output powershell format', async () => { const vaultId1 = await polykeyAgent.vaultManager.createVault( `${vaultName}1`, ); @@ -452,7 +452,7 @@ describe('commandEnv', () => { `${vaultName}1:.`, `${vaultName}2:.`, '--env-format', - 'dotps', + 'powershell', ]; const result = await testUtils.pkExec([...command]); @@ -497,62 +497,6 @@ describe('commandEnv', () => { SECRET4: 'this is the secret4', }); }); - test('should output prepend format', async () => { - const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); - - await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { - await vaultOps.addSecret(vault, 'SECRET1', 'this is the secret1'); - await vaultOps.addSecret(vault, 'SECRET2', 'this is the secret2'); - await vaultOps.mkdir(vault, 'dir1'); - await vaultOps.addSecret(vault, 'dir1/SECRET3', 'this is the secret3'); - await vaultOps.addSecret(vault, 'dir1/SECRET4', 'this is the secret4'); - }); - - command = [ - 'secrets', - 'env', - '-np', - dataDir, - '-e', - `${vaultName}:.`, - '--env-format', - 'prepend', - ]; - - const result = await testUtils.pkExec([...command]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toBe( - 'SECRET1="this is the secret1" SECRET2="this is the secret2" SECRET3="this is the secret3" SECRET4="this is the secret4"', - ); - }); - test('should output prependCmd format', async () => { - const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); - - await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { - await vaultOps.addSecret(vault, 'SECRET1', 'this is the secret1'); - await vaultOps.addSecret(vault, 'SECRET2', 'this is the secret2'); - await vaultOps.mkdir(vault, 'dir1'); - await vaultOps.addSecret(vault, 'dir1/SECRET3', 'this is the secret3'); - await vaultOps.addSecret(vault, 'dir1/SECRET4', 'this is the secret4'); - }); - - command = [ - 'secrets', - 'env', - '-np', - dataDir, - '-e', - `${vaultName}:.`, - '--env-format', - 'prependCmd', - ]; - - const result = await testUtils.pkExec([...command]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toBe( - 'set "SECRET1=this is the secret1" & set "SECRET2=this is the secret2" & set "SECRET3=this is the secret3" & set "SECRET4=this is the secret4"', - ); - }); test('testing valid and invalid rename inputs', async () => { const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); @@ -580,7 +524,7 @@ describe('commandEnv', () => { '-np', dataDir, '--env-format', - 'human', + 'unix', '-e', ...valid.map((v) => `${vaultName}:SECRET=${v}`), ]); @@ -594,7 +538,7 @@ describe('commandEnv', () => { '-np', dataDir, '--env-format', - 'human', + 'unix', '-e', `${vaultName}:SECRET=${nameNew}`, ]); @@ -615,7 +559,7 @@ describe('commandEnv', () => { '-np', dataDir, '--env-format', - 'human', + 'unix', '-ei', 'error', '-e', @@ -637,7 +581,7 @@ describe('commandEnv', () => { '-np', dataDir, '--env-format', - 'human', + 'unix', '-ei', 'warn', '-e', @@ -663,7 +607,7 @@ describe('commandEnv', () => { '-np', dataDir, '--env-format', - 'human', + 'unix', '-ei', 'ignore', '-e', @@ -691,7 +635,7 @@ describe('commandEnv', () => { '-np', dataDir, '--env-format', - 'human', + 'unix', '-ed', 'error', '-e', @@ -716,7 +660,7 @@ describe('commandEnv', () => { '-np', dataDir, '--env-format', - 'human', + 'unix', '-ed', 'warn', '-e', @@ -743,7 +687,7 @@ describe('commandEnv', () => { '-np', dataDir, '--env-format', - 'human', + 'unix', '-ed', 'keep', '-e', @@ -768,7 +712,7 @@ describe('commandEnv', () => { '-np', dataDir, '--env-format', - 'human', + 'unix', '-ed', 'overwrite', '-e',