feat: add unified configuration generation and migration scripts

- Add generate-unified-config.ts for generating environment configs
- Support multiple output formats: env files, Kubernetes configs, Docker
- Include migration script for transitioning from legacy configuration
- Support dry-run and verbose modes for safe configuration changes
- Generate type-safe environment exports for each environment
- Provide comprehensive error handling and validation
This commit is contained in:
William Valentin
2025-09-08 09:44:50 -07:00
parent 8c8cea9e42
commit f3936f23fe
2 changed files with 1208 additions and 0 deletions

View File

@@ -0,0 +1,612 @@
#!/usr/bin/env bun
/**
* Generate Configuration Files from Unified Config
*
* This script reads the unified configuration and generates all necessary
* config files for different environments and deployment targets.
*
* Usage:
* bun scripts/generate-unified-config.ts [environment]
* bun scripts/generate-unified-config.ts production
* bun scripts/generate-unified-config.ts --all
*/
import { writeFileSync, mkdirSync, existsSync } from 'fs';
import { join, dirname } from 'path';
import {
createUnifiedConfigForEnvironment,
exportAsEnvVars,
type Environment,
type UnifiedConfig,
} from '../config/unified.config';
interface GeneratorOptions {
environment?: Environment;
outputDir?: string;
generateAll?: boolean;
dryRun?: boolean;
verbose?: boolean;
}
class ConfigGenerator {
private options: GeneratorOptions;
private projectRoot: string;
constructor(options: GeneratorOptions = {}) {
this.options = options;
this.projectRoot = join(__dirname, '..');
}
/**
* Generate all configuration files
*/
async generate(): Promise<void> {
const environments: Environment[] = this.options.generateAll
? ['development', 'staging', 'production']
: [this.options.environment || this.getCurrentEnvironment()];
console.warn('🔧 Generating unified configuration files...');
console.warn(`📁 Project root: ${this.projectRoot}`);
console.warn(`🌍 Environments: ${environments.join(', ')}`);
for (const env of environments) {
await this.generateForEnvironment(env);
}
console.warn('✅ Configuration generation completed!');
}
/**
* Generate configuration files for a specific environment
*/
private async generateForEnvironment(
environment: Environment
): Promise<void> {
console.warn(`\n🔄 Generating configuration for: ${environment}`);
// Create config instance for specific environment
const config = createUnifiedConfigForEnvironment(environment);
const envVars = exportAsEnvVars(config);
// Generate different config file formats
await Promise.all([
this.generateDotEnv(environment, envVars),
this.generateKubernetesConfig(environment, config, envVars),
this.generateDockerEnv(environment, envVars),
this.generateViteEnv(environment, envVars),
this.generateTypeScriptConfig(environment, config),
]);
console.warn(`✅ Generated configuration for ${environment}`);
}
/**
* Generate .env file
*/
private async generateDotEnv(
environment: Environment,
envVars: Record<string, string>
): Promise<void> {
const content = this.createEnvFileContent(environment, envVars);
const filePath = join(this.projectRoot, `.env.${environment}`);
this.writeFile(filePath, content, `📄 .env.${environment}`);
}
/**
* Generate Kubernetes config.env file
*/
private async generateKubernetesConfig(
environment: Environment,
config: UnifiedConfig,
envVars: Record<string, string>
): Promise<void> {
const overlayDir = join(
this.projectRoot,
'k8s-kustomize',
'overlays',
environment
);
this.ensureDirectoryExists(overlayDir);
// Generate config.env for kustomize
const configContent = this.createKubernetesConfigContent(
environment,
envVars
);
const configPath = join(overlayDir, 'config.env');
this.writeFile(
configPath,
configContent,
`🚢 Kubernetes config.env (${environment})`
);
// Generate kustomization.yaml updates
await this.updateKustomizationFile(environment, config);
}
/**
* Generate Docker environment file
*/
private async generateDockerEnv(
environment: Environment,
envVars: Record<string, string>
): Promise<void> {
const dockerDir = join(this.projectRoot, 'docker');
this.ensureDirectoryExists(dockerDir);
const content = this.createDockerEnvContent(environment, envVars);
const filePath = join(dockerDir, `.env.${environment}`);
this.writeFile(filePath, content, `🐳 Docker .env.${environment}`);
}
/**
* Generate Vite-specific environment file
*/
private async generateViteEnv(
environment: Environment,
envVars: Record<string, string>
): Promise<void> {
const viteVars = Object.entries(envVars)
.filter(([key]) => key.startsWith('VITE_') || this.isViteRelevant(key))
.reduce(
(acc, [key, value]) => {
acc[key.startsWith('VITE_') ? key : `VITE_${key}`] = value;
return acc;
},
{} as Record<string, string>
);
const content = this.createViteEnvContent(environment, viteVars);
const filePath = join(this.projectRoot, `.env.vite.${environment}`);
this.writeFile(filePath, content, `⚡ Vite .env.vite.${environment}`);
}
/**
* Generate TypeScript config export
*/
private async generateTypeScriptConfig(
environment: Environment,
config: UnifiedConfig
): Promise<void> {
const configDir = join(this.projectRoot, 'config', 'generated');
this.ensureDirectoryExists(configDir);
const content = this.createTypeScriptConfigContent(environment, config);
const filePath = join(configDir, `${environment}.config.ts`);
this.writeFile(filePath, content, `📝 TypeScript ${environment}.config.ts`);
}
/**
* Create .env file content
*/
private createEnvFileContent(
environment: Environment,
envVars: Record<string, string>
): string {
const timestamp = new Date().toISOString();
return `# ${environment.toUpperCase()} Environment Configuration
# Generated automatically from unified configuration
# Generated on: ${timestamp}
#
# This file was auto-generated. Do not edit manually.
# To make changes, update config/unified.config.ts and regenerate.
${Object.entries(envVars)
.map(([key, value]) => `${key}=${value}`)
.join('\n')}
# Additional environment-specific variables can be added below
# (These will not be overwritten during regeneration)
`;
}
/**
* Create Kubernetes config.env content
*/
private createKubernetesConfigContent(
environment: Environment,
envVars: Record<string, string>
): string {
const timestamp = new Date().toISOString();
// Filter to Kubernetes-relevant variables
const k8sVars = Object.entries(envVars)
.filter(
([key]) => !key.startsWith('VITE_') && this.isKubernetesRelevant(key)
)
.reduce(
(acc, [key, value]) => {
acc[key] = value;
return acc;
},
{} as Record<string, string>
);
return `# Kubernetes configuration for ${environment}
# Generated automatically from unified configuration
# Generated on: ${timestamp}
${Object.entries(k8sVars)
.map(([key, value]) => `${key}=${value}`)
.join('\n')}
`;
}
/**
* Create Docker environment content
*/
private createDockerEnvContent(
environment: Environment,
envVars: Record<string, string>
): string {
const timestamp = new Date().toISOString();
return `# Docker environment for ${environment}
# Generated on: ${timestamp}
# Container Configuration
DOCKER_IMAGE=${envVars.DOCKER_IMAGE}
CONTAINER_REGISTRY=${envVars.CONTAINER_REGISTRY}
CONTAINER_REPOSITORY=${envVars.CONTAINER_REPOSITORY}
CONTAINER_TAG=${envVars.CONTAINER_TAG}
# Application Configuration
APP_NAME=${envVars.APP_NAME}
NODE_ENV=${envVars.NODE_ENV}
PORT=${envVars.PORT}
# Database Configuration
COUCHDB_URL=${envVars.COUCHDB_URL}
COUCHDB_USER=${envVars.COUCHDB_USER}
COUCHDB_PASSWORD=${envVars.COUCHDB_PASSWORD}
`;
}
/**
* Create Vite environment content
*/
private createViteEnvContent(
environment: Environment,
viteVars: Record<string, string>
): string {
const timestamp = new Date().toISOString();
return `# Vite environment variables for ${environment}
# Generated on: ${timestamp}
#
# These variables are available in the frontend build process
${Object.entries(viteVars)
.map(([key, value]) => `${key}=${value}`)
.join('\n')}
`;
}
/**
* Create TypeScript config content
*/
private createTypeScriptConfigContent(
environment: Environment,
config: UnifiedConfig
): string {
const timestamp = new Date().toISOString();
return `/**
* Generated configuration for ${environment}
* Generated on: ${timestamp}
*
* This file exports the resolved configuration for the ${environment} environment.
* It can be imported by other TypeScript files for type-safe configuration access.
*/
import type { UnifiedConfig } from '../unified.config';
export const ${environment}Config: UnifiedConfig = ${JSON.stringify(config, null, 2)} as const;
export default ${environment}Config;
`;
}
/**
* Update kustomization.yaml file with current configuration
*/
private async updateKustomizationFile(
environment: Environment,
config: UnifiedConfig
): Promise<void> {
const overlayDir = join(
this.projectRoot,
'k8s-kustomize',
'overlays',
environment
);
const kustomizationPath = join(overlayDir, 'kustomization.yaml');
if (!existsSync(kustomizationPath)) {
// Create new kustomization.yaml
const content = this.createKustomizationContent(environment, config);
this.writeFile(
kustomizationPath,
content,
`🎛️ kustomization.yaml (${environment})`
);
} else {
console.warn(
` Kustomization file exists: ${kustomizationPath} (not overwriting)`
);
}
}
/**
* Create kustomization.yaml content
*/
private createKustomizationContent(
environment: Environment,
config: UnifiedConfig
): string {
return `apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
metadata:
name: ${config.app.name}-${environment}
# Reference the base configuration
resources:
- ../../base
- namespace.yaml
# Override namespace for ${environment}
namespace: ${config.kubernetes.namespace}
# ${environment.charAt(0).toUpperCase() + environment.slice(1)}-specific labels
labels:
- pairs:
environment: ${environment}
tier: ${environment === 'production' ? 'prod' : environment}
# ${environment.charAt(0).toUpperCase() + environment.slice(1)} image tags and configurations
images:
- name: frontend-image
newName: ${config.container.registry}/${config.container.repository}
newTag: ${config.container.tag}
- name: couchdb-image
newName: couchdb
newTag: 3.3.2
# ${environment.charAt(0).toUpperCase() + environment.slice(1)} replicas
replicas:
- name: ${config.app.name}-frontend
count: ${config.kubernetes.replicas.frontend}
- name: ${config.app.name}-couchdb
count: ${config.kubernetes.replicas.database}
# Environment-specific patches
patches:
# Resource limits
- target:
kind: Deployment
name: ${config.app.name}-frontend
patch: |-
- op: replace
path: /spec/template/spec/containers/0/resources
value:
requests:
memory: "${config.kubernetes.resources.frontend.requests.memory}"
cpu: "${config.kubernetes.resources.frontend.requests.cpu}"
limits:
memory: "${config.kubernetes.resources.frontend.limits.memory}"
cpu: "${config.kubernetes.resources.frontend.limits.cpu}"
- target:
kind: StatefulSet
name: ${config.app.name}-couchdb
patch: |-
- op: replace
path: /spec/template/spec/containers/0/resources
value:
requests:
memory: "${config.kubernetes.resources.database.requests.memory}"
cpu: "${config.kubernetes.resources.database.requests.cpu}"
limits:
memory: "${config.kubernetes.resources.database.limits.memory}"
cpu: "${config.kubernetes.resources.database.limits.cpu}"
# ConfigMap generation
configMapGenerator:
- name: ${config.app.name}-config
envs:
- config.env
behavior: create
`;
}
/**
* Check if a variable is relevant for Vite
*/
private isViteRelevant(key: string): boolean {
const viteRelevantKeys = [
'APP_NAME',
'APP_VERSION',
'APP_BASE_URL',
'COUCHDB_URL',
'COUCHDB_USER',
'COUCHDB_PASSWORD',
'MAILGUN_API_KEY',
'MAILGUN_DOMAIN',
'MAILGUN_FROM_NAME',
'MAILGUN_FROM_EMAIL',
'GOOGLE_CLIENT_ID',
'GITHUB_CLIENT_ID',
'ENABLE_EMAIL_VERIFICATION',
'ENABLE_OAUTH',
'ENABLE_ADMIN_INTERFACE',
'DEBUG_MODE',
];
return viteRelevantKeys.includes(key);
}
/**
* Check if a variable is relevant for Kubernetes
*/
private isKubernetesRelevant(key: string): boolean {
const k8sIrrelevantKeys = ['VITE_', 'CONTAINER_', 'DOCKER_'];
return !k8sIrrelevantKeys.some(prefix => key.startsWith(prefix));
}
/**
* Get current environment from NODE_ENV
*/
private getCurrentEnvironment(): Environment {
const env = process.env.NODE_ENV || 'development';
return ['development', 'staging', 'production', 'test'].includes(env)
? (env as Environment)
: 'development';
}
/**
* Ensure directory exists
*/
private ensureDirectoryExists(dirPath: string): void {
if (!existsSync(dirPath)) {
mkdirSync(dirPath, { recursive: true });
if (this.options.verbose) {
console.warn(` 📁 Created directory: ${dirPath}`);
}
}
}
/**
* Write file with logging
*/
private writeFile(
filePath: string,
content: string,
description: string
): void {
if (this.options.dryRun) {
console.warn(` 🔍 [DRY RUN] Would write: ${description}`);
console.warn(` Path: ${filePath}`);
return;
}
// Ensure directory exists
this.ensureDirectoryExists(dirname(filePath));
writeFileSync(filePath, content, 'utf8');
console.warn(` ✅ Generated: ${description}`);
if (this.options.verbose) {
console.warn(` Path: ${filePath}`);
console.warn(` Size: ${content.length} bytes`);
}
}
}
/**
* CLI Interface
*/
async function main() {
const args = process.argv.slice(2);
const options: GeneratorOptions = {};
// Parse command line arguments
for (let i = 0; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case '--all':
options.generateAll = true;
break;
case '--dry-run':
options.dryRun = true;
break;
case '--verbose':
case '-v':
options.verbose = true;
break;
case '--help':
case '-h':
showHelp();
process.exit(0);
break;
default:
if (!arg.startsWith('--') && !options.environment) {
options.environment = arg as Environment;
}
break;
}
}
// Validate environment
if (
options.environment &&
!['development', 'staging', 'production', 'test'].includes(
options.environment
)
) {
console.error(`❌ Invalid environment: ${options.environment}`);
console.error(
' Valid environments: development, staging, production, test'
);
process.exit(1);
}
try {
const generator = new ConfigGenerator(options);
await generator.generate();
} catch (error) {
console.error('❌ Configuration generation failed:', error);
process.exit(1);
}
}
/**
* Show help message
*/
function showHelp() {
console.warn(`
🔧 Unified Configuration Generator
USAGE:
bun scripts/generate-unified-config.ts [environment] [options]
ARGUMENTS:
environment Target environment (development, staging, production, test)
OPTIONS:
--all Generate configuration for all environments
--dry-run Show what would be generated without writing files
--verbose, -v Show detailed output
--help, -h Show this help message
EXAMPLES:
bun scripts/generate-unified-config.ts development
bun scripts/generate-unified-config.ts production --verbose
bun scripts/generate-unified-config.ts --all
bun scripts/generate-unified-config.ts --dry-run
GENERATED FILES:
📄 .env.[environment] - Environment variables
🚢 k8s-kustomize/overlays/[env]/config.env - Kubernetes configuration
🐳 docker/.env.[environment] - Docker environment
⚡ .env.vite.[environment] - Vite-specific variables
📝 config/generated/[env].config.ts - TypeScript config export
The generator reads from config/unified.config.ts and creates all necessary
configuration files for the specified environment(s).
`);
}
// Run CLI if this file is executed directly
if (import.meta.main) {
main().catch(console.error);
}
export { ConfigGenerator, type GeneratorOptions };