sls-db-migrations
Version:
Serverless wrapper for node.js db-migrations framework
163 lines (134 loc) • 6.85 kB
JavaScript
const { Command, CommanderError } = require('commander')
const { LambdaClient, InvokeCommand, InvokeCommandInput } = require('@aws-sdk/client-lambda');
const fs = require('fs');
const ConfigResolver = require('../lib/configResolver');
const { EnvironmentValueResolver } = require('../lib/configResolver/environment');
const { SSMParameterValueResolver } = require('../lib/configResolver/ssm');
const { SSMClient } = require('@aws-sdk/client-ssm');
const LAMBDA_FAILURE_EXIT_CODE = 2;
const INVALID_CONFIG_REFERENCE_EXIT_CODE = 3;
const constructMigrationPayload = (command, bucket, archivePath, config, env, countOrSpecification, scope) => {
return {
bucket, archivePath, command,
configOptions: {
config, env
},
commandOptions: { countOrSpecification }
}
}
const invokeLambda = async (lambdaNameOrArn, region, payload) => {
const lambdaClient = new LambdaClient({ region: region });
return lambdaClient.send(new InvokeCommand(
{
FunctionName: lambdaNameOrArn,
InvocationType: 'RequestResponse',
Payload: JSON.stringify(payload)
}))
}
const handleLambdaResponse = async (invokeCommandOutput) => {
const payload = JSON.parse(Buffer.from(invokeCommandOutput.Payload));
if (invokeCommandOutput.StatusCode > 300) {
console.log('Error executing lambda...');
}
if (invokeCommandOutput.LogResult) {
console.log('Latest Lambda logs:')
const lambdaFunctionLog = Buffer.from(invokeCommandOutput.LogResult, 'base64')
console.log(lambdaFunctionLog)
}
if (!payload.success) {
console.log(`Error executing the command: ${payload.message}`);
console.log(payload.stack)
throw new Error('An errored occurred when executing the command. Check the output above or the lambda logs for info.')
} else {
console.log('Successfully executed the command');
}
}
// --- CLI ---
const readLocalConfigFromPath = async (path, awsRegion) => {
const config = JSON.parse(fs.readFileSync(path, 'utf8'));
const environmentValueResolver = new EnvironmentValueResolver();
const ssmClient = new SSMClient({ region: awsRegion });
const ssmParameterValueResolver = new SSMParameterValueResolver(ssmClient);
const configResolver = new ConfigResolver([environmentValueResolver, ssmParameterValueResolver]);
const resolvedConfig = await configResolver.resolveConfig(config);
return resolvedConfig
}
const addCommonOptions = (command) => {
return command.requiredOption('-l --lambda <arn or name>', 'the arn or name of the lambda')
.option('-r --region <aws region>', 'the region where the AWS related requests will be sent to. Used in all CLI\'s interactions with AWS')
.requiredOption('--bucket <bucket name>', 'the S3 bucket where migrations are stored')
.requiredOption('--archive-path <path/to/migrate.zip>', 'the file path inside the bucket to the migration archive')
.option('--config-path <path/to/database.json>', 'the file path inside the migration archive or in the local system')
.option('--read-local-config', 'if specified, then the config is read from config-path in the local path.' +
'This takes precedence over the zipped database.json')
}
const program = new Command();
program.version('0.1.1')
const up = program.command('up')
.argument('<env>', 'the environment to use based on the config file in the specified archive')
.argument('[countOrSpecification]', 'the count of migrations to apply or the migration name upto which the migration is to be done')
addCommonOptions(up)
.action(async (env, countOrSpecification, options, command) => {
const { bucket, archivePath, configPath, lambda, readLocalConfig, region } = options;
let resolvedConfig = configPath;
if (readLocalConfig) {
try {
console.log(`Reading config and resolving values...`)
const configObject = await readLocalConfigFromPath(configPath, region)
if (!configObject || !configObject[env]) {
throw Error(`Environment ${env} not found in config`)
}
resolvedConfig = configObject
} catch (error) {
console.log(error)
process.exitCode = INVALID_CONFIG_REFERENCE_EXIT_CODE
process.exit(INVALID_CONFIG_REFERENCE_EXIT_CODE)
}
}
const payload = constructMigrationPayload("up", bucket, archivePath, resolvedConfig, env, countOrSpecification)
try {
console.log(`Invoking Lambda for "up" command with environment: ${env}...`)
const invokeCommandOutput = await invokeLambda(lambda, region, payload)
await handleLambdaResponse(invokeCommandOutput)
console.log('Done.')
} catch (e) {
console.log(e)
process.exitCode = LAMBDA_FAILURE_EXIT_CODE
process.exit(LAMBDA_FAILURE_EXIT_CODE)
}
})
const down = program.command('down')
.argument('<env>', 'the environment to use based on the config file in the specified archive')
.argument('[countOrSpecification]', 'the count of migrations to apply or the migration name upto which the migration is to be done')
addCommonOptions(down)
.action(async (env, countOrSpecification, options, command) => {
const { bucket, archivePath, configPath, lambda, readLocalConfig, region } = options;
let resolvedConfig = configPath;
if (readLocalConfig) {
try {
console.log(`Reading config and resolving values...`)
const configObject = await readLocalConfigFromPath(configPath, region)
if (!configObject || !configObject[env]) {
throw Error(`Environment ${env} not found in config`)
}
resolvedConfig = configObject
} catch (error) {
console.log(error)
process.exitCode = INVALID_CONFIG_REFERENCE_EXIT_CODE
process.exit(INVALID_CONFIG_REFERENCE_EXIT_CODE)
}
}
const payload = constructMigrationPayload("down", bucket, archivePath, resolvedConfig, env, countOrSpecification)
try {
console.log(`Invoking Lambda for "down" command with environment: ${env}...`)
const invokeCommandOutput = await invokeLambda(lambda, region, payload)
await handleLambdaResponse(invokeCommandOutput)
console.log('Done.')
} catch (error) {
console.log(error)
process.exitCode = LAMBDA_FAILURE_EXIT_CODE
process.exit(LAMBDA_FAILURE_EXIT_CODE)
}
})
program.parseAsync(process.argv);