Compare commits

...

10 Commits

Author SHA1 Message Date
a9bdfe9373 6.0.3 2023-10-20 17:21:52 +02:00
2017d51f11 fix(core): update 2023-10-20 17:21:51 +02:00
765011ad2a 6.0.2 2023-08-09 17:47:21 +02:00
d807cc6de2 fix(core): update 2023-08-09 17:47:20 +02:00
53721a41c2 6.0.1 2023-08-09 14:50:33 +02:00
c9f79e6ea4 fix(core): update 2023-08-09 14:50:32 +02:00
3c7e3e2589 6.0.0 2023-08-09 13:24:50 +02:00
205d27f9a0 BREAKING CHANGE(core): update 2023-08-09 13:24:49 +02:00
56ce78f794 5.0.5 2023-08-09 12:49:52 +02:00
9d33054f03 fix(core): update 2023-08-09 12:49:52 +02:00
8 changed files with 1921 additions and 693 deletions

View File

@ -119,6 +119,6 @@ jobs:
run: | run: |
npmci node install stable npmci node install stable
npmci npm install npmci npm install
pnpm install -g @gitzone/tsdoc pnpm install -g @git.zone/tsdoc
npmci command tsdoc npmci command tsdoc
continue-on-error: true continue-on-error: true

View File

@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/qenv", "name": "@push.rocks/qenv",
"version": "5.0.4", "version": "6.0.3",
"private": false, "private": false,
"description": "easy promised environments", "description": "easy promised environments",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",
@ -27,15 +27,16 @@
}, },
"homepage": "https://gitlab.com/pushrocks/qenv#README", "homepage": "https://gitlab.com/pushrocks/qenv#README",
"devDependencies": { "devDependencies": {
"@gitzone/tsbuild": "^2.1.66", "@git.zone/tsbuild": "^2.1.66",
"@gitzone/tsrun": "^1.2.44", "@git.zone/tsrun": "^1.2.44",
"@gitzone/tstest": "^1.0.77", "@git.zone/tstest": "^1.0.77",
"@push.rocks/tapbundle": "^5.0.12", "@push.rocks/tapbundle": "^5.0.15",
"@types/node": "^20.4.6" "@types/node": "^20.8.7"
}, },
"dependencies": { "dependencies": {
"@configvault.io/interfaces": "^1.0.2", "@api.global/typedrequest": "^3.0.1",
"@push.rocks/smartfile": "^10.0.28", "@configvault.io/interfaces": "^1.0.13",
"@push.rocks/smartfile": "^10.0.33",
"@push.rocks/smartlog": "^3.0.3", "@push.rocks/smartlog": "^3.0.3",
"@push.rocks/smartpath": "^5.0.11" "@push.rocks/smartpath": "^5.0.11"
}, },

2255
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -18,18 +18,18 @@ tap.test('should create a new class', async () => {
}); });
tap.test('key1 should be not be overwritten since it is already present', async () => { tap.test('key1 should be not be overwritten since it is already present', async () => {
expect(testQenv.getEnvVarRequired('key1')).toEqual('original'); expect(await testQenv.getEnvVarOnDemand('key1')).toEqual('original');
expect(testQenv.getEnvVarOnDemand('key1')).toEqual('original'); expect(await testQenv.getEnvVarOnDemand('key1')).toEqual('original');
}); });
tap.test('key2 should be read from Yml', async () => { tap.test('key2 should be read from Yml', async () => {
expect(testQenv.getEnvVarRequired('key2')).toEqual('fromJson'); expect(await testQenv.getEnvVarOnDemand('key2')).toEqual('fromJson');
expect(testQenv.getEnvVarOnDemand('key2')).toEqual('fromJson'); expect(await testQenv.getEnvVarOnDemand('key2')).toEqual('fromJson');
}); });
tap.test('keyValueObjectArray should hold all retrieved values', async () => { tap.test('keyValueObjectArray should hold all retrieved values', async () => {
expect(testQenv.keyValueObject.key1).toEqual('original'); expect(await testQenv.keyValueObject.key1).toEqual('original');
expect(testQenv.keyValueObject.key2).toEqual('fromJson'); expect(await testQenv.keyValueObject.key2).toEqual('fromJson');
}); });
tap.start(); tap.start();

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/qenv', name: '@push.rocks/qenv',
version: '5.0.4', version: '6.0.3',
description: 'easy promised environments' description: 'easy promised environments'
} }

View File

@ -0,0 +1,30 @@
import * as plugins from './qenv.plugins.js';
export class ConfigVaultAdapter {
public configVaultUrl: string;
constructor(configVaultUrl?: string) {
this.configVaultUrl = configVaultUrl;
}
public async getConfigBundle(): Promise<plugins.configvaultInterfaces.data.IConfigBundle> {
if (this.configVaultUrl) {
console.log(`ConfigVault specified through constructor`)
} else if (process.env['CONFIGVAULT_URL']) {
this.configVaultUrl = process.env['CONFIGVAULT_URL'];
} else {
return null;
}
const parsedUrl = new URL(this.configVaultUrl);
const tr =
new plugins.typedrequest.TypedRequest<plugins.configvaultInterfaces.requests.IReq_GetEnvBundle>(
`${parsedUrl.host}/typedrequest`,
'getEnvBundle'
);
const response = await tr.fire({
authorization: parsedUrl.pathname.replace('/', ''),
})
}
}

View File

@ -1,9 +1,8 @@
import { ConfigVaultAdapter } from './qenv.classes.configvaultadapter.js';
import * as plugins from './qenv.plugins.js'; import * as plugins from './qenv.plugins.js';
/** export type TEnvVarRef = string | (() => Promise<string>);
* class Qenv
* allows to make assertions about the environments while being more flexibel in how to meet them
*/
export class Qenv { export class Qenv {
public requiredEnvVars: string[] = []; public requiredEnvVars: string[] = [];
public availableEnvVars: string[] = []; public availableEnvVars: string[] = [];
@ -11,12 +10,24 @@ export class Qenv {
public keyValueObject: { [key: string]: any } = {}; public keyValueObject: { [key: string]: any } = {};
public logger = new plugins.smartlog.ConsoleLog(); public logger = new plugins.smartlog.ConsoleLog();
// filePaths public configVaultAdapter: ConfigVaultAdapter;
public qenvFilePathAbsolute: string; public qenvFilePathAbsolute: string;
public envFilePathAbsolute: string; public envFilePathAbsolute: string;
constructor(qenvFileBasePathArg = process.cwd(), envFileBasePathArg, failOnMissing = true) { constructor(
// lets make sure paths are absolute qenvFileBasePathArg: string = process.cwd(),
envFileBasePathArg: string,
failOnMissing: boolean = true
) {
this.configVaultAdapter = new ConfigVaultAdapter();
this.initializeFilePaths(qenvFileBasePathArg, envFileBasePathArg);
this.loadRequiredEnvVars();
this.loadAvailableEnvVars();
this.checkForMissingEnvVars(failOnMissing);
}
private initializeFilePaths(qenvFileBasePathArg: string, envFileBasePathArg: string) {
this.qenvFilePathAbsolute = plugins.path.join( this.qenvFilePathAbsolute = plugins.path.join(
plugins.path.resolve(qenvFileBasePathArg), plugins.path.resolve(qenvFileBasePathArg),
'qenv.yml' 'qenv.yml'
@ -25,20 +36,39 @@ export class Qenv {
plugins.path.resolve(envFileBasePathArg), plugins.path.resolve(envFileBasePathArg),
'env.json' 'env.json'
); );
}
this.getRequiredEnvVars(); private loadRequiredEnvVars() {
this.getAvailableEnvVars(); 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!');
}
}
}
this.missingEnvVars = this.getMissingEnvVars(); 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)
);
// handle missing variables
if (this.missingEnvVars.length > 0) { if (this.missingEnvVars.length > 0) {
console.info('Required Env Vars are:'); console.info('Required Env Vars are:', this.requiredEnvVars);
console.log(this.requiredEnvVars); console.error('Missing Env Vars:', this.missingEnvVars);
console.error('However some Env variables could not be resolved:');
console.log(this.missingEnvVars);
if (failOnMissing) { if (failOnMissing) {
this.logger.log('error', 'Exiting!'); this.logger.log('error', 'Exiting due to missing env vars!');
process.exit(1); process.exit(1);
} else { } else {
this.logger.log('warn', 'qenv is not set to fail on missing environment variables'); this.logger.log('warn', 'qenv is not set to fail on missing environment variables');
@ -46,150 +76,118 @@ export class Qenv {
} }
} }
/** public async getEnvVarOnDemand(
* only gets an environment variable if it is required within a read qenv.yml file envVarNameOrNames: TEnvVarRef | TEnvVarRef[]
* @param envVarName ): Promise<string | undefined> {
*/ if (Array.isArray(envVarNameOrNames)) {
public getEnvVarRequired(envVarName): string { for (const envVarName of envVarNameOrNames) {
return this.keyValueObject[envVarName]; 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 {
* tries to get any env var even if it is not required console.warn('requesting env var sync leaves out potentially important async env sources.');
* @param wantedEnvVar
*/
public getEnvVarOnDemand(wantedEnvVar: string): string {
let envVarFromEnvironmentVariable: string;
let envVarFromEnvJsonFile: string;
let envVarFromDockerSecret: string;
let dockerSecretJson: string;
// env var check if (Array.isArray(envVarNameOrNames)) {
if (process.env[wantedEnvVar]) { for (const envVarName of envVarNameOrNames) {
this.availableEnvVars.push(wantedEnvVar); const value = this.tryGetEnvVarSync(envVarName);
envVarFromEnvironmentVariable = process.env[wantedEnvVar]; if (value) {
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();
} }
// env file check return (
// lets determine the actual env yml this.getFromEnvironmentVariable(envVarRefArg) ||
let envJsonFileAsObject; 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 {
try { try {
envJsonFileAsObject = plugins.smartfile.fs.toObjectSync(this.envFilePathAbsolute); const envJson = plugins.smartfile.fs.toObjectSync(this.envFilePathAbsolute);
} catch (err) { const value = envJson[envVarName];
envJsonFileAsObject = {}; if (typeof value === 'object') {
} return 'base64Object:' + this.encodeBase64(value);
if (envJsonFileAsObject.hasOwnProperty(wantedEnvVar)) { }
envVarFromEnvJsonFile = envJsonFileAsObject[wantedEnvVar]; return value;
} catch (error) {
return undefined;
} }
}
// docker secret check private getFromDockerSecret(envVarName: string): string | undefined {
if ( const secretPath = `/run/secrets/${envVarName}`;
plugins.smartfile.fs.isDirectory('/run') && if (plugins.smartfile.fs.fileExistsSync(secretPath)) {
plugins.smartfile.fs.isDirectory('/run/secrets') && return plugins.smartfile.fs.toStringSync(secretPath);
plugins.smartfile.fs.fileExistsSync(`/run/secrets/${wantedEnvVar}`)
) {
envVarFromDockerSecret = plugins.smartfile.fs.toStringSync(`/run/secrets/${wantedEnvVar}`);
} }
return undefined;
}
// docker secret.json private getFromDockerSecretJson(envVarName: string): string | undefined {
if ( if (plugins.smartfile.fs.isDirectory('/run/secrets')) {
plugins.smartfile.fs.isDirectory('/run') &&
plugins.smartfile.fs.isDirectory('/run/secrets')
) {
const availableSecrets = plugins.smartfile.fs.listAllItemsSync('/run/secrets'); const availableSecrets = plugins.smartfile.fs.listAllItemsSync('/run/secrets');
for (const secret of availableSecrets) { for (const secret of availableSecrets) {
if (secret.includes('secret.json') && !envVarFromDockerSecret) { if (secret.includes('secret.json')) {
const secretObject = plugins.smartfile.fs.toObjectSync(`/run/secrets/${secret}`); const secretObject = plugins.smartfile.fs.toObjectSync(`/run/secrets/${secret}`);
envVarFromDockerSecret = secretObject[wantedEnvVar]; const value = secretObject[envVarName];
if (typeof value === 'object') {
return 'base64Object:' + this.encodeBase64(value);
}
return value;
} }
} }
} }
return undefined;
// warn if there is more than one candidate
const availableCcandidates: any[] = [];
[
envVarFromEnvironmentVariable,
envVarFromEnvJsonFile,
envVarFromDockerSecret,
dockerSecretJson,
].forEach((candidate) => {
if (candidate) {
availableCcandidates.push(candidate);
}
});
if (availableCcandidates.length > 1) {
this.logger.log(
'warn',
`found multiple candidates for ${wantedEnvVar} Choosing in the order of envVar, envFileVar, dockerSecret, dockerSecretJson`
);
console.log(availableCcandidates);
}
switch (true) {
case !!envVarFromEnvironmentVariable:
this.logger.log('ok', `found ${wantedEnvVar} as environment variable`);
return envVarFromEnvironmentVariable;
case !!envVarFromEnvJsonFile:
this.logger.log('ok', `found ${wantedEnvVar} as env.json variable`);
return envVarFromEnvJsonFile;
case !!envVarFromDockerSecret:
this.logger.log('ok', `found ${wantedEnvVar} as docker secret`);
return envVarFromDockerSecret;
case !!dockerSecretJson:
this.logger.log('ok', `found ${wantedEnvVar} as docker secret.json`);
return dockerSecretJson;
default:
this.logger.log(
'warn',
`could not find the wanted environment variable ${wantedEnvVar} anywhere`
);
return;
}
} }
/** private encodeBase64(data: any): string {
* gets the required env values const jsonString = JSON.stringify(data);
*/ return Buffer.from(jsonString).toString('base64');
private getRequiredEnvVars = () => { }
let qenvFile: any = {};
if (plugins.smartfile.fs.fileExistsSync(this.qenvFilePathAbsolute)) {
qenvFile = plugins.smartfile.fs.toObjectSync(this.qenvFilePathAbsolute);
}
if (!qenvFile || !qenvFile.required || !Array.isArray(qenvFile.required)) {
this.logger.log(
'warn',
`qenv (promised environment): ./qenv.yml File does not contain a 'required' Array! This might be ok though.`
);
} else {
for (const keyArg of Object.keys(qenvFile.required)) {
this.requiredEnvVars.push(qenvFile.required[keyArg]);
}
}
};
/** private decodeBase64(encodedString: string): any {
* gets the available env vars const decodedString = Buffer.from(encodedString, 'base64').toString('utf-8');
*/ return JSON.parse(decodedString);
private getAvailableEnvVars = () => { }
for (const requiredEnvVar of this.requiredEnvVars) {
const chosenVar = this.getEnvVarOnDemand(requiredEnvVar);
if (chosenVar) {
this.availableEnvVars.push(requiredEnvVar);
this.keyValueObject[requiredEnvVar] = chosenVar;
}
}
};
/**
* gets missing env vars
*/
private getMissingEnvVars = (): string[] => {
const missingEnvVars: string[] = [];
for (const envVar of this.requiredEnvVars) {
if (!this.availableEnvVars.includes(envVar)) {
missingEnvVars.push(envVar);
}
}
return missingEnvVars;
};
} }

View File

@ -3,8 +3,20 @@ import * as path from 'path';
export { path }; export { path };
// @api.global scope
import * as typedrequest from '@api.global/typedrequest';
export {
typedrequest,
}
// @pushrocks scope // @pushrocks scope
import * as smartfile from '@push.rocks/smartfile'; import * as smartfile from '@push.rocks/smartfile';
import * as smartlog from '@push.rocks/smartlog'; import * as smartlog from '@push.rocks/smartlog';
export { smartfile, smartlog }; export { smartfile, smartlog };
// @configvault.io scope
import * as configvaultInterfaces from '@configvault.io/interfaces';
export { configvaultInterfaces };