UNPKG

cfn-forge

Version:

CloudFormation deployment automation tool with git-based workflows

384 lines (327 loc) 11 kB
/** * 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;