import { CloudlyAdapter } from './qenv.classes.configvaultadapter.js'; import * as plugins from './qenv.plugins.js'; export type TEnvVarRef = string | (() => Promise); export class Qenv { public requiredEnvVars: string[] = []; public availableEnvVars: string[] = []; public missingEnvVars: string[] = []; public keyValueObject: { [key: string]: any } = {}; public logger = new plugins.smartlog.ConsoleLog(); public cloudlyAdapter: CloudlyAdapter; public qenvFilePathAbsolute: string; public envFilePathAbsolute: string; constructor( qenvFileBasePathArg: string = process.cwd(), envFileBasePathArg?: string, failOnMissing: boolean = true ) { this.cloudlyAdapter = new CloudlyAdapter(); this.initializeFilePaths(qenvFileBasePathArg, envFileBasePathArg); this.loadRequiredEnvVars(); this.loadAvailableEnvVars(); this.checkForMissingEnvVars(failOnMissing); } private initializeFilePaths(qenvFileBasePathArg: string, envFileBasePathArg: string) { this.qenvFilePathAbsolute = plugins.path.join( plugins.path.resolve(qenvFileBasePathArg), 'qenv.yml' ); if (envFileBasePathArg) { this.envFilePathAbsolute = plugins.path.join( plugins.path.resolve(envFileBasePathArg), 'env.json' ); } } private loadRequiredEnvVars() { if (plugins.smartfile.fs.fileExistsSync(this.qenvFilePathAbsolute)) { const qenvFile = plugins.smartfile.fs.toObjectSync(this.qenvFilePathAbsolute); if (qenvFile?.required && Array.isArray(qenvFile.required)) { this.requiredEnvVars.push(...qenvFile.required); } else { this.logger.log('warn', 'qenv.yml does not contain a "required" Array!'); } } } private loadAvailableEnvVars() { for (const envVar of this.requiredEnvVars) { const value = this.getEnvVarOnDemand(envVar); if (value) { this.availableEnvVars.push(envVar); this.keyValueObject[envVar] = value; } } } private checkForMissingEnvVars(failOnMissing: boolean) { this.missingEnvVars = this.requiredEnvVars.filter( (envVar) => !this.availableEnvVars.includes(envVar) ); if (this.missingEnvVars.length > 0) { console.info('Required Env Vars are:', this.requiredEnvVars); console.error('Missing Env Vars:', this.missingEnvVars); if (failOnMissing) { this.logger.log('error', 'Exiting due to missing env vars!'); process.exit(1); } else { this.logger.log('warn', 'qenv is not set to fail on missing environment variables'); } } } public async getEnvVarOnDemand( envVarNameOrNames: TEnvVarRef | TEnvVarRef[] ): Promise { if (Array.isArray(envVarNameOrNames)) { for (const envVarName of envVarNameOrNames) { const value = await this.tryGetEnvVar(envVarName); if (value) { return value; } } return undefined; } else { return await this.tryGetEnvVar(envVarNameOrNames); } } public getEnvVarOnDemandSync(envVarNameOrNames: string | string[]): string | undefined { console.warn('requesting env var sync leaves out potentially important async env sources.'); if (Array.isArray(envVarNameOrNames)) { for (const envVarName of envVarNameOrNames) { const value = this.tryGetEnvVarSync(envVarName); if (value) { return value; } } return undefined; } else { return this.tryGetEnvVarSync(envVarNameOrNames); } } public async getEnvVarOnDemandAsObject(envVarNameOrNames: string | string[]): Promise { const rawValue = await this.getEnvVarOnDemand(envVarNameOrNames); if (rawValue && rawValue.startsWith('base64Object:')) { const base64Part = rawValue.split('base64Object:')[1]; return this.decodeBase64(base64Part); } return rawValue; } private async tryGetEnvVar(envVarRefArg: TEnvVarRef): Promise { if (typeof envVarRefArg === 'function') { return await envVarRefArg(); } return ( this.getFromEnvironmentVariable(envVarRefArg) || this.getFromEnvJsonFile(envVarRefArg) || this.getFromDockerSecret(envVarRefArg) || this.getFromDockerSecretJson(envVarRefArg) ); } private tryGetEnvVarSync(envVarName: string): string | undefined { return ( this.getFromEnvironmentVariable(envVarName) || this.getFromEnvJsonFile(envVarName) || this.getFromDockerSecret(envVarName) || this.getFromDockerSecretJson(envVarName) ); } private getFromEnvironmentVariable(envVarName: string): string | undefined { return process.env[envVarName]; } private getFromEnvJsonFile(envVarName: string): string | undefined { if (!plugins.smartfile.fs.fileExistsSync(this.envFilePathAbsolute)) { return undefined; } try { const envJson = plugins.smartfile.fs.toObjectSync(this.envFilePathAbsolute); const value = envJson[envVarName]; if (typeof value === 'object') { return 'base64Object:' + this.encodeBase64(value); } return value; } catch (error) { return undefined; } } private getFromDockerSecret(envVarName: string): string | undefined { const secretPath = `/run/secrets/${envVarName}`; if (plugins.smartfile.fs.fileExistsSync(secretPath)) { return plugins.smartfile.fs.toStringSync(secretPath); } return undefined; } private getFromDockerSecretJson(envVarName: string): string | undefined { if (plugins.smartfile.fs.isDirectory('/run/secrets')) { const availableSecrets = plugins.smartfile.fs.listAllItemsSync('/run/secrets'); for (const secret of availableSecrets) { if (secret.includes('secret.json')) { const secretObject = plugins.smartfile.fs.toObjectSync(`/run/secrets/${secret}`); const value = secretObject[envVarName]; if (typeof value === 'object') { return 'base64Object:' + this.encodeBase64(value); } return value; } } } return undefined; } private encodeBase64(data: any): string { const jsonString = JSON.stringify(data); return Buffer.from(jsonString).toString('base64'); } private decodeBase64(encodedString: string): any { const decodedString = Buffer.from(encodedString, 'base64').toString('utf-8'); return JSON.parse(decodedString); } }