- 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
613 lines
16 KiB
TypeScript
613 lines
16 KiB
TypeScript
#!/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 };
|