cfn-forge
Version:
CloudFormation deployment automation tool with git-based workflows
384 lines (327 loc) • 11 kB
JavaScript
/**
* CFN-Forge Parameters Command
* Manage CloudFormation template parameters
*/
const fs = require('fs').promises;
const path = require('path');
const yaml = require('js-yaml');
const { logger } = require('../utils/logger');
/**
* Extract parameter definitions from CloudFormation template
*/
async function extractTemplateParameters(templatePath) {
const op = logger.trackOperation('Template Parameter Extraction', {
templatePath,
});
const startTime = op.start();
try {
const templateContent = await fs.readFile(templatePath, 'utf8');
let template;
try {
// Try YAML first (with CloudFormation schema)
const cfnSchema = yaml.DEFAULT_SCHEMA.extend([
new yaml.Type('!Ref', {
kind: 'scalar',
construct(data) { return { Ref: data }; },
}),
new yaml.Type('!GetAtt', {
kind: 'scalar',
construct(data) { return { 'Fn::GetAtt': data.split('.') }; },
}),
new yaml.Type('!Sub', {
kind: 'scalar',
construct(data) { return { 'Fn::Sub': data }; },
}),
]);
template = yaml.load(templateContent, { schema: cfnSchema });
} catch (yamlError) {
try {
// Try JSON fallback
template = JSON.parse(templateContent);
} catch (jsonError) {
throw new Error(`Template parsing failed: ${yamlError.message}`);
}
}
const parameters = template.Parameters || {};
op.success(startTime, `Extracted ${Object.keys(parameters).length} parameters`);
return {
parameters,
template,
};
} catch (error) {
op.error(error, startTime);
throw error;
}
}
/**
* Display parameters in table format
*/
function displayParametersTable(parameters, currentValues = {}) {
if (Object.keys(parameters).length === 0) {
logger.info('📋 No parameters defined in template');
return;
}
logger.info('📋 CloudFormation Template Parameters:');
logger.info('');
// Calculate column widths
const nameWidth = Math.max(12, Math.max(...Object.keys(parameters).map((n) => n.length)));
const typeWidth = 15;
const valueWidth = 20;
// Header
const header = `${'Name'.padEnd(nameWidth)} | ${'Type'.padEnd(typeWidth)} | ${'Current Value'.padEnd(valueWidth)} | Default | Description`;
logger.info(header);
logger.info('─'.repeat(header.length));
// Parameters
Object.entries(parameters).forEach(([name, def]) => {
const type = def.Type || 'String';
const currentValue = currentValues[name] || '';
const defaultValue = def.Default || '';
const description = def.Description || '';
const row = `${name.padEnd(nameWidth)} | ${type.padEnd(typeWidth)} | ${currentValue.padEnd(valueWidth)} | ${defaultValue} | ${description}`;
logger.info(row);
// Show constraints if any
const constraints = [];
if (def.AllowedValues) {
constraints.push(`Allowed: [${def.AllowedValues.join(', ')}]`);
}
if (def.MinLength) {
constraints.push(`MinLength: ${def.MinLength}`);
}
if (def.MaxLength) {
constraints.push(`MaxLength: ${def.MaxLength}`);
}
if (def.AllowedPattern) {
constraints.push(`Pattern: ${def.AllowedPattern}`);
}
if (constraints.length > 0) {
logger.info(`${' '.repeat(nameWidth + 3)} ${constraints.join(', ')}`);
}
});
}
/**
* Display parameters in detailed format
*/
function displayParametersDetailed(parameters, currentValues = {}) {
if (Object.keys(parameters).length === 0) {
logger.info('📋 No parameters defined in template');
return;
}
logger.info('📋 CloudFormation Template Parameters:');
logger.info('');
Object.entries(parameters).forEach(([name, def]) => {
logger.subsection(name);
console.log(` Type: ${def.Type || 'String'}`);
console.log(` Description: ${def.Description || 'No description provided'}`);
if (currentValues[name]) {
console.log(` Current Value: ${currentValues[name]}`);
}
if (def.Default) {
console.log(` Default: ${def.Default}`);
}
if (def.AllowedValues) {
console.log(` Allowed Values: [${def.AllowedValues.join(', ')}]`);
}
if (def.MinLength || def.MaxLength) {
const lengthConstraint = [
def.MinLength ? `min: ${def.MinLength}` : null,
def.MaxLength ? `max: ${def.MaxLength}` : null,
].filter(Boolean).join(', ');
console.log(` Length: ${lengthConstraint}`);
}
if (def.AllowedPattern) {
console.log(` Pattern: ${def.AllowedPattern}`);
}
if (def.ConstraintDescription) {
console.log(` Constraint: ${def.ConstraintDescription}`);
}
console.log('');
});
}
/**
* Display parameters in JSON format
*/
function displayParametersJSON(parameters, currentValues = {}) {
const output = {
templateParameters: parameters,
currentValues,
parameterCount: Object.keys(parameters).length,
};
logger.json(output);
}
/**
* Load current parameter values from project configuration
*/
async function loadCurrentParameters(environment = null) {
try {
const configContent = await fs.readFile('.cfn-forge.yaml', 'utf8');
const config = yaml.load(configContent);
if (environment) {
return config.environments?.[environment]?.parameters || {};
}
// Merge all environment parameters
const allParams = {};
Object.values(config.environments || {}).forEach((env) => {
if (env.parameters) {
Object.assign(allParams, env.parameters);
}
});
return allParams;
} catch (error) {
return {};
}
}
/**
* Generate parameter file template
*/
async function generateParameterFile(templatePath, outputPath) {
const op = logger.trackOperation('Parameter File Generation', {
templatePath,
outputPath,
});
const startTime = op.start();
try {
const { parameters } = await extractTemplateParameters(templatePath);
const parameterFile = {};
const comments = {
_description: 'CloudFormation parameter values',
_template: path.basename(templatePath),
_generated: new Date().toISOString(),
};
// Add comment object first
Object.assign(parameterFile, comments);
// Add parameters with default values or empty strings
Object.entries(parameters).forEach(([name, def]) => {
if (def.Default !== undefined) {
parameterFile[name] = def.Default;
} else {
parameterFile[name] = '';
}
// Add parameter description as comment
if (def.Description) {
parameterFile[`_${name}_description`] = def.Description;
}
});
// Write parameter file
await fs.writeFile(outputPath, JSON.stringify(parameterFile, null, 2));
op.success(startTime, `Generated parameter file: ${outputPath}`);
logger.success(`Parameter file created: ${outputPath}`);
logger.info(`Edit this file and use with: cfn-forge deploy --parameter-file ${outputPath}`);
} catch (error) {
op.error(error, startTime);
throw error;
}
}
/**
* Validate parameter file format
*/
async function validateParameterFile(filePath) {
const op = logger.trackOperation('Parameter File Validation', {
filePath,
});
const startTime = op.start();
try {
const content = await fs.readFile(filePath, 'utf8');
const parameters = JSON.parse(content);
if (typeof parameters !== 'object' || Array.isArray(parameters)) {
throw new Error('Parameter file must contain a JSON object');
}
// Filter out comment fields
const actualParams = Object.keys(parameters).filter((key) => !key.startsWith('_'));
op.success(startTime, `Validated parameter file with ${actualParams.length} parameters`);
return {
parameters,
actualParameters: actualParams.length,
isValid: true,
};
} catch (error) {
op.error(error, startTime);
throw error;
}
}
/**
* Main parameters command handler
*/
async function parametersCommand(options) {
logger.setContext('command', 'parameters');
logger.setContext('options', options);
try {
// Determine template path
let templatePath = 'cloudformation.yaml';
if (options.template) {
templatePath = options.template;
} else {
// Look for common template names
const commonNames = ['cloudformation.yaml', 'cloudformation.yml', 'template.yaml', 'template.yml'];
let found = false;
for (const name of commonNames) {
try {
await fs.access(name);
templatePath = name;
found = true;
break;
} catch {
// Continue to next name
}
}
if (!found) {
throw new Error(
'No CloudFormation template found.\n'
+ 'Use --template to specify a template file.',
);
}
}
// Handle generate parameter file
if (options.generate) {
const outputPath = options.output || 'parameters.json';
await generateParameterFile(templatePath, outputPath);
return;
}
// Handle validate parameter file
if (options.validate) {
const filePath = options.validate;
await validateParameterFile(filePath);
logger.success(`Parameter file is valid: ${filePath}`);
return;
}
// Extract parameters from template
const { parameters } = await extractTemplateParameters(templatePath);
// Load current parameter values
const currentValues = await loadCurrentParameters(options.environment);
logger.info(`📄 Template: ${path.basename(templatePath)}`);
logger.info('');
// Display parameters based on format
if (options.json) {
displayParametersJSON(parameters, currentValues);
} else if (options.detailed) {
displayParametersDetailed(parameters, currentValues);
} else {
displayParametersTable(parameters, currentValues);
}
// Show usage examples
if (!options.json && Object.keys(parameters).length > 0) {
logger.info('');
logger.subsection('Usage Examples');
const parameterNames = Object.keys(parameters);
const exampleParam = parameterNames[0];
logger.info('💡 Set parameters via CLI:');
logger.info(` cfn-forge deploy --parameters "${exampleParam}=value1,Param2=value2"`);
logger.info('');
logger.info('💡 Use parameter file:');
logger.info(' cfn-forge parameters --generate --output my-params.json');
logger.info(' cfn-forge deploy --parameter-file my-params.json');
logger.info('');
logger.info('💡 Configure in .cfn-forge.yaml:');
logger.info(' environments:');
logger.info(' dev:');
logger.info(' parameters:');
logger.info(` ${exampleParam}: value1`);
}
} catch (error) {
logger.errorWithContext(error, 'Parameters Command');
logger.reportErrorSummary();
process.exit(1);
} finally {
logger.clearContext();
}
}
module.exports = parametersCommand;