mirror of
https://github.com/morten-olsen/with-ssm.git
synced 2026-02-08 00:46:23 +01:00
init
This commit is contained in:
45
src/start.ts
Normal file
45
src/start.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import yargs from 'yargs/yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
|
||||
import { exec } from './utils/exec.js';
|
||||
import { getEnv } from './utils/env.js';
|
||||
import { replaceParams } from './utils/ssm.js';
|
||||
|
||||
const argv = await yargs(hideBin(process.argv))
|
||||
.usage('Usage: $0 [options] -- <command>')
|
||||
.option('region', {
|
||||
type: 'string',
|
||||
description: 'The AWS region to use for SSM.',
|
||||
})
|
||||
.option('profile', {
|
||||
type: 'string',
|
||||
description: 'The AWS profile to use from your credentials file.',
|
||||
})
|
||||
.option('file', {
|
||||
alias: 'f',
|
||||
type: 'string',
|
||||
description: 'The file to use for environment variables. (multiple files can be specified)',
|
||||
default: ['.env', '.env.with-ssm'],
|
||||
})
|
||||
.demandCommand(1, 'Error: You must provide a command to execute after --')
|
||||
.alias('h', 'help')
|
||||
.epilogue('For more information, check the documentation.')
|
||||
.parse();
|
||||
|
||||
const command = argv._[0] as string;
|
||||
const commandArgs = argv._.slice(1).map(String);
|
||||
|
||||
if (!command) {
|
||||
console.error('No command provided');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const files = argv.file && Array.isArray(argv.file) ? argv.file : [argv.file];
|
||||
const hostEnv = await getEnv(files);
|
||||
const env = await replaceParams(hostEnv);
|
||||
|
||||
exec({
|
||||
command,
|
||||
env,
|
||||
args: commandArgs,
|
||||
});
|
||||
14
src/utils/cli.ts
Normal file
14
src/utils/cli.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
const splitArgs = (args: string[]) => {
|
||||
const separatorIndex = args.indexOf('--');
|
||||
const actionArgs = args.slice(0, separatorIndex);
|
||||
const command = args[separatorIndex + 1];
|
||||
const commandArgs = args.slice(separatorIndex + 2);
|
||||
|
||||
return {
|
||||
actionArgs,
|
||||
command,
|
||||
commandArgs,
|
||||
};
|
||||
};
|
||||
|
||||
export { splitArgs };
|
||||
5
src/utils/debug.ts
Normal file
5
src/utils/debug.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { debuglog } from 'node:util';
|
||||
|
||||
const debug = debuglog('with-ssm');
|
||||
|
||||
export { debug };
|
||||
25
src/utils/env.ts
Normal file
25
src/utils/env.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { existsSync } from 'node:fs';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
import { parse } from 'dotenv';
|
||||
|
||||
import { debug } from './debug.js';
|
||||
|
||||
const getEnv = async (files: string[]) => {
|
||||
const env = { ...process.env };
|
||||
|
||||
for (const file of files) {
|
||||
if (existsSync(resolve(process.cwd(), file))) {
|
||||
debug(`Loading ${file}`);
|
||||
const content = await readFile(file, 'utf8');
|
||||
const parsed = parse(content);
|
||||
debug(`Parsed ${file}\n${JSON.stringify(parsed, null, 2)}`);
|
||||
Object.assign(env, parsed);
|
||||
}
|
||||
}
|
||||
|
||||
return env;
|
||||
};
|
||||
|
||||
export { getEnv };
|
||||
30
src/utils/exec.ts
Normal file
30
src/utils/exec.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
type ExecOptions = {
|
||||
command: string;
|
||||
args: string[];
|
||||
cwd?: string;
|
||||
env?: Record<string, string | undefined>;
|
||||
};
|
||||
|
||||
const exec = (options: ExecOptions) => {
|
||||
const { command, args, cwd, env } = options;
|
||||
const child = spawn(command, args, { cwd, env, stdio: 'inherit' });
|
||||
const killChild = () => child.kill();
|
||||
|
||||
process.on('SIGINT', killChild);
|
||||
process.on('SIGTERM', killChild);
|
||||
child.on('exit', (code) => {
|
||||
process.exit(code ?? 1);
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
console.error(`[with-ssm] ❌ Failed to start command "${command}".`);
|
||||
console.error(err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
return child;
|
||||
};
|
||||
|
||||
export { exec };
|
||||
59
src/utils/ssm.ts
Normal file
59
src/utils/ssm.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { GetParametersCommand, SSMClient } from '@aws-sdk/client-ssm';
|
||||
|
||||
import { debug } from './debug.js';
|
||||
|
||||
const PREFIX = 'SSM:';
|
||||
|
||||
type ReplaceParamsOptions = {
|
||||
region?: string;
|
||||
profile?: string;
|
||||
};
|
||||
|
||||
const replaceParams = async (
|
||||
env: Record<string, string | undefined>,
|
||||
{ region, profile }: ReplaceParamsOptions = {},
|
||||
) => {
|
||||
const ssm = new SSMClient({
|
||||
region,
|
||||
profile,
|
||||
});
|
||||
|
||||
const names = Object.entries(env)
|
||||
.filter(([, value]) => value?.startsWith(PREFIX))
|
||||
.map(([, value]) => value?.slice(PREFIX.length))
|
||||
.filter((value) => value !== undefined);
|
||||
|
||||
debug(`Replacing ${names.length} parameters`);
|
||||
debug(`Names: ${names.join(', ')}`);
|
||||
|
||||
if (names.length === 0) {
|
||||
return env;
|
||||
}
|
||||
|
||||
const command = new GetParametersCommand({
|
||||
Names: names,
|
||||
WithDecryption: true,
|
||||
});
|
||||
|
||||
const response = await ssm.send(command);
|
||||
if (response.InvalidParameters?.length || 0 > 0) {
|
||||
console.error('Invalid SSM parameters', response.InvalidParameters);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const params = response.Parameters ?? [];
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(env).map(([key, value]) => {
|
||||
if (value?.startsWith(PREFIX)) {
|
||||
const param = params.find((param) => `SSM:${param.Name}` === value);
|
||||
debug(`Replacing ${key} with ${param?.Value}`);
|
||||
return [key, param?.Value];
|
||||
}
|
||||
|
||||
return [key, value];
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export { replaceParams };
|
||||
Reference in New Issue
Block a user