280 lines
8.8 KiB
TypeScript
280 lines
8.8 KiB
TypeScript
import { CloudlyAdapter } from './qenv.classes.configvaultadapter.js';
|
|
import * as plugins from './qenv.plugins.js';
|
|
|
|
export type TEnvVarRef = string | (() => Promise<string>);
|
|
type TKeyValueObject = Record<string, any>;
|
|
|
|
export class Qenv {
|
|
public requiredEnvVars: string[] = [];
|
|
public availableEnvVars: string[] = [];
|
|
public missingEnvVars: string[] = [];
|
|
public keyValueObject: TKeyValueObject = {};
|
|
public logger = new plugins.smartlog.ConsoleLog();
|
|
|
|
public cloudlyAdapter: CloudlyAdapter;
|
|
|
|
public qenvFilePathAbsolute = '';
|
|
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) {
|
|
const envFileBasePath = plugins.path.resolve(envFileBasePathArg);
|
|
|
|
const envFileJsonPath = plugins.path.join(envFileBasePath, 'env.json');
|
|
const envFileYmlPath = plugins.path.join(envFileBasePath, 'env.yml');
|
|
const envFileYamlPath = plugins.path.join(envFileBasePath, 'env.yaml');
|
|
|
|
const envFileJsonExists = this.fileExists(envFileJsonPath);
|
|
const envFileYmlExists = this.fileExists(envFileYmlPath);
|
|
const envFileYamlExists = this.fileExists(envFileYamlPath);
|
|
|
|
if (envFileJsonExists && (envFileYmlExists || envFileYamlExists)) {
|
|
this.logger.log('warn', 'Both env.json and env.yml files exist! Using env.json');
|
|
this.envFilePathAbsolute = envFileJsonPath;
|
|
} else if (envFileJsonExists) {
|
|
this.envFilePathAbsolute = envFileJsonPath;
|
|
} else if (envFileYmlExists) {
|
|
this.envFilePathAbsolute = envFileYmlPath;
|
|
} else if (envFileYamlExists) {
|
|
this.envFilePathAbsolute = envFileYamlPath;
|
|
}
|
|
}
|
|
}
|
|
|
|
private loadRequiredEnvVars() {
|
|
if (this.fileExists(this.qenvFilePathAbsolute)) {
|
|
const qenvFile = this.readObjectFromFile(this.qenvFilePathAbsolute);
|
|
const requiredEnvVars = qenvFile.required;
|
|
if (Array.isArray(requiredEnvVars)) {
|
|
this.requiredEnvVars.push(
|
|
...requiredEnvVars.filter((envVar): envVar is string => typeof envVar === 'string')
|
|
);
|
|
} 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 !== undefined) {
|
|
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<string | undefined> {
|
|
if (Array.isArray(envVarNameOrNames)) {
|
|
for (const envVarName of envVarNameOrNames) {
|
|
const value = await this.tryGetEnvVar(envVarName);
|
|
if (value !== undefined) {
|
|
return value;
|
|
}
|
|
}
|
|
return undefined;
|
|
} else {
|
|
return await this.tryGetEnvVar(envVarNameOrNames);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Like getEnvVarOnDemand, but throws an error if the env var is not set.
|
|
* @param envVarNameOrNames
|
|
* @returns
|
|
*/
|
|
public async getEnvVarOnDemandStrict(
|
|
envVarNameOrNames: TEnvVarRef | TEnvVarRef[]
|
|
): Promise<string> {
|
|
const value = await this.getEnvVarOnDemand(envVarNameOrNames);
|
|
if (value === undefined) {
|
|
throw new Error(`Env var ${envVarNameOrNames} is not set!`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
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 !== undefined) {
|
|
return value;
|
|
}
|
|
}
|
|
return undefined;
|
|
} else {
|
|
return this.tryGetEnvVarSync(envVarNameOrNames);
|
|
}
|
|
}
|
|
|
|
public async getEnvVarOnDemandAsObject(envVarNameOrNames: string | string[]): Promise<any> {
|
|
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<string | undefined> {
|
|
if (typeof envVarRefArg === 'function') {
|
|
return await envVarRefArg();
|
|
}
|
|
|
|
const sources = [
|
|
this.getFromEnvironmentVariable(envVarRefArg),
|
|
this.getFromEnvYamlOrJsonFile(envVarRefArg),
|
|
this.getFromDockerSecret(envVarRefArg),
|
|
this.getFromDockerSecretJson(envVarRefArg)
|
|
];
|
|
|
|
for (const value of sources) {
|
|
if (value !== undefined) {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
private tryGetEnvVarSync(envVarName: string): string | undefined {
|
|
const sources = [
|
|
this.getFromEnvironmentVariable(envVarName),
|
|
this.getFromEnvYamlOrJsonFile(envVarName),
|
|
this.getFromDockerSecret(envVarName),
|
|
this.getFromDockerSecretJson(envVarName)
|
|
];
|
|
|
|
for (const value of sources) {
|
|
if (value !== undefined) {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
private getFromEnvironmentVariable(envVarName: string): string | undefined {
|
|
return process.env[envVarName];
|
|
}
|
|
|
|
private getFromEnvYamlOrJsonFile(envVarName: string): string | undefined {
|
|
if (!this.fileExists(this.envFilePathAbsolute)) {
|
|
return undefined;
|
|
}
|
|
try {
|
|
const envJson = this.readObjectFromFile(this.envFilePathAbsolute);
|
|
const value = envJson[envVarName];
|
|
if (value === undefined) {
|
|
return undefined;
|
|
}
|
|
if (typeof value === 'object' && value !== null) {
|
|
return 'base64Object:' + this.encodeBase64(value);
|
|
}
|
|
return String(value);
|
|
} catch (error) {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
private getFromDockerSecret(envVarName: string): string | undefined {
|
|
const secretPath = `/run/secrets/${envVarName}`;
|
|
if (this.fileExists(secretPath)) {
|
|
return plugins.fs.readFileSync(secretPath, 'utf8');
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
private getFromDockerSecretJson(envVarName: string): string | undefined {
|
|
if (this.directoryExists('/run/secrets')) {
|
|
const availableSecrets = plugins.fs.readdirSync('/run/secrets');
|
|
for (const secret of availableSecrets) {
|
|
if (secret.includes('secret.json')) {
|
|
const secretObject = this.readObjectFromFile(`/run/secrets/${secret}`);
|
|
const value = secretObject[envVarName];
|
|
if (value === undefined) {
|
|
continue;
|
|
}
|
|
if (typeof value === 'object' && value !== null) {
|
|
return 'base64Object:' + this.encodeBase64(value);
|
|
}
|
|
return String(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);
|
|
}
|
|
|
|
private fileExists(filePath: string | undefined): filePath is string {
|
|
if (!filePath) {
|
|
return false;
|
|
}
|
|
return plugins.fs.existsSync(filePath);
|
|
}
|
|
|
|
private directoryExists(directoryPath: string): boolean {
|
|
try {
|
|
return plugins.fs.statSync(directoryPath).isDirectory();
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private readObjectFromFile(filePath: string): TKeyValueObject {
|
|
const fileString = plugins.fs.readFileSync(filePath, 'utf8');
|
|
const parsedObject = filePath.endsWith('.json')
|
|
? JSON.parse(fileString)
|
|
: plugins.yaml.parse(fileString);
|
|
return typeof parsedObject === 'object' && parsedObject !== null ? parsedObject : {};
|
|
}
|
|
}
|