/** * K8s-aligned Configuration Loader * Loads configuration from YAML files and secrets from mounted files * Replaces environment variable based configuration for production k8s compatibility */ import * as yaml from 'js-yaml'; import * as fs from 'fs'; import * as path from 'path'; import { z } from 'zod'; import { logger } from '../logging/logger'; // Configuration schema definition const configSchema = z.object({ // Server configuration server: z.object({ name: z.string(), port: z.number(), environment: z.string(), node_env: z.string(), }), // Database configuration database: z.object({ host: z.string(), port: z.number(), name: z.string(), user: z.string(), pool_size: z.number().optional().default(20), }), // Redis configuration redis: z.object({ host: z.string(), port: z.number(), db: z.number().optional().default(0), }), // Auth0 configuration auth0: z.object({ domain: z.string(), audience: z.string(), }), // External APIs configuration (optional) external: z.object({ vpic: z.object({ url: z.string(), timeout: z.string(), }).optional(), }).optional(), // Service configuration service: z.object({ name: z.string(), }), // CORS configuration cors: z.object({ origins: z.array(z.string()), allow_credentials: z.boolean(), max_age: z.number(), }), // Frontend configuration frontend: z.object({ api_base_url: z.string(), auth0: z.object({ domain: z.string(), audience: z.string(), }), }), // Health check configuration health: z.object({ endpoints: z.object({ basic: z.string(), ready: z.string(), live: z.string(), startup: z.string(), }), probes: z.object({ startup: z.object({ initial_delay: z.string(), period: z.string(), timeout: z.string(), failure_threshold: z.number(), }), readiness: z.object({ period: z.string(), timeout: z.string(), failure_threshold: z.number(), }), liveness: z.object({ period: z.string(), timeout: z.string(), failure_threshold: z.number(), }), }), }), // Logging configuration logging: z.object({ level: z.string(), format: z.string(), destinations: z.array(z.string()), }), // Performance configuration performance: z.object({ request_timeout: z.string(), max_request_size: z.string(), compression_enabled: z.boolean(), circuit_breaker: z.object({ enabled: z.boolean(), failure_threshold: z.number(), timeout: z.string(), }), }), }); // Secrets schema definition const secretsSchema = z.object({ postgres_password: z.string(), auth0_client_secret: z.string(), google_maps_api_key: z.string(), }); type Config = z.infer; type Secrets = z.infer; export interface AppConfiguration { config: Config; secrets: Secrets; // Convenience accessors for common patterns getDatabaseUrl(): string; getRedisUrl(): string; getAuth0Config(): { domain: string; audience: string; clientSecret: string }; } class ConfigurationLoader { private configPath: string; private secretsDir: string; private cachedConfig: AppConfiguration | null = null; constructor() { this.configPath = process.env['CONFIG_PATH'] || '/app/config/production.yml'; this.secretsDir = process.env['SECRETS_DIR'] || '/run/secrets'; } private loadYamlConfig(): Config { if (!fs.existsSync(this.configPath)) { throw new Error(`Configuration file not found at ${this.configPath}`); } try { const fileContents = fs.readFileSync(this.configPath, 'utf8'); const yamlData = yaml.load(fileContents) as any; return configSchema.parse(yamlData); } catch (error) { logger.error(`Failed to load configuration from ${this.configPath}`, { error }); throw new Error(`Configuration loading failed: ${error}`); } } private loadSecrets(): Secrets { const secrets: Partial = {}; const secretFiles = [ 'postgres-password', 'auth0-client-secret', 'google-maps-api-key', ]; for (const secretFile of secretFiles) { const secretPath = path.join(this.secretsDir, secretFile); const secretKey = secretFile.replace(/-/g, '_') as keyof Secrets; if (fs.existsSync(secretPath)) { try { const secretValue = fs.readFileSync(secretPath, 'utf8').trim(); (secrets as any)[secretKey] = secretValue; } catch (error) { logger.error(`Failed to read secret file ${secretPath}`, { error }); } } else { logger.error(`Secret file not found: ${secretPath}`); } } try { return secretsSchema.parse(secrets); } catch (error) { logger.error('Secrets validation failed', { error }); throw new Error(`Secrets loading failed: ${error}`); } } public load(): AppConfiguration { if (this.cachedConfig) { return this.cachedConfig; } const config = this.loadYamlConfig(); const secrets = this.loadSecrets(); this.cachedConfig = { config, secrets, getDatabaseUrl(): string { return `postgresql://${config.database.user}:${secrets.postgres_password}@${config.database.host}:${config.database.port}/${config.database.name}`; }, getRedisUrl(): string { return `redis://${config.redis.host}:${config.redis.port}/${config.redis.db}`; }, getAuth0Config() { return { domain: config.auth0.domain, audience: config.auth0.audience, clientSecret: secrets.auth0_client_secret, }; }, }; logger.info('Configuration loaded successfully', { configSource: 'yaml', secretsSource: 'files', }); return this.cachedConfig; } } // Export singleton instance const configLoader = new ConfigurationLoader(); export const appConfig = configLoader.load(); // Export types for use in other modules export type { Config, Secrets };