@backstage/config-loader
Advanced tools
Comparing version 0.1.1-alpha.7 to 0.1.1-alpha.9
@@ -8,8 +8,9 @@ 'use strict'; | ||
var fs = _interopDefault(require('fs-extra')); | ||
var path = require('path'); | ||
var yaml2 = _interopDefault(require('yaml')); | ||
var path = require('path'); | ||
var yup = require('yup'); | ||
function findRootPath(topPath) { | ||
let path2 = topPath; | ||
for (let i = 0; i < 1000; i++) { | ||
for (let i = 0; i < 1e3; i++) { | ||
const packagePath = path.resolve(path2, "package.json"); | ||
@@ -36,2 +37,63 @@ const exists = fs.pathExistsSync(packagePath); | ||
async function resolveStaticConfig(options) { | ||
let {configPath} = options; | ||
if (!configPath) { | ||
configPath = path.resolve(findRootPath(fs.realpathSync(process.cwd())), "app-config.yaml"); | ||
} | ||
return [configPath]; | ||
} | ||
function isObject(obj) { | ||
if (typeof obj !== "object") { | ||
return false; | ||
} else if (Array.isArray(obj)) { | ||
return false; | ||
} | ||
return obj !== null; | ||
} | ||
async function readConfigFile(filePath, ctx) { | ||
const configYaml = await ctx.readFile(filePath); | ||
const config2 = yaml2.parse(configYaml); | ||
async function transform(obj, path) { | ||
if (typeof obj !== "object") { | ||
return obj; | ||
} else if (obj === null) { | ||
return void 0; | ||
} else if (Array.isArray(obj)) { | ||
const arr = new Array(); | ||
for (const [index, value] of obj.entries()) { | ||
const out2 = await transform(value, `${path}[${index}]`); | ||
if (out2 !== void 0) { | ||
arr.push(out2); | ||
} | ||
} | ||
return arr; | ||
} | ||
if ("$secret" in obj) { | ||
if (!isObject(obj.$secret)) { | ||
throw TypeError(`Expected object at secret ${path}.$secret`); | ||
} | ||
try { | ||
return await ctx.readSecret(obj.$secret); | ||
} catch (error) { | ||
throw new Error(`Invalid secret at ${path}: ${error.message}`); | ||
} | ||
} | ||
const out = {}; | ||
for (const [key, value] of Object.entries(obj)) { | ||
const result = await transform(value, `${path}.${key}`); | ||
if (result !== void 0) { | ||
out[key] = result; | ||
} | ||
} | ||
return out; | ||
} | ||
const finalConfig = await transform(config2, ""); | ||
if (!isObject(finalConfig)) { | ||
throw new TypeError("Expected object at config root"); | ||
} | ||
return finalConfig; | ||
} | ||
const ENV_PREFIX = "APP_CONFIG_"; | ||
@@ -79,19 +141,100 @@ const CONFIG_KEY_PART_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z][a-z0-9]*)*$/i; | ||
} | ||
async function readStaticConfig(options) { | ||
let {configPath} = options; | ||
if (!configPath) { | ||
configPath = path.resolve(findRootPath(fs.realpathSync(process.cwd())), "app-config.yaml"); | ||
const secretLoaderSchemas = { | ||
file: yup.object({ | ||
file: yup.string().required() | ||
}), | ||
env: yup.object({ | ||
env: yup.string().required() | ||
}), | ||
data: yup.object({ | ||
data: yup.string().required(), | ||
path: yup.lazy((value) => { | ||
if (typeof value === "string") { | ||
return yup.string().required(); | ||
} | ||
return yup.array().of(yup.string().required()).required(); | ||
}) | ||
}) | ||
}; | ||
const secretSchema = yup.lazy((value) => { | ||
if (typeof value !== "object" || value === null) { | ||
return yup.object().required().label("secret"); | ||
} | ||
try { | ||
const configYaml = await fs.readFile(configPath, "utf8"); | ||
const config2 = yaml2.parse(configYaml); | ||
return [config2]; | ||
} catch (error) { | ||
throw new Error(`Failed to read static configuration file, ${error}`); | ||
const loaderTypes = Object.keys(secretLoaderSchemas); | ||
for (const key of loaderTypes) { | ||
if (key in value) { | ||
return secretLoaderSchemas[key]; | ||
} | ||
} | ||
throw new yup.ValidationError(`Secret must contain one of '${loaderTypes.join("', '")}'`, value, "$secret"); | ||
}); | ||
const dataSecretParser = { | ||
".json": async (content) => JSON.parse(content), | ||
".yaml": async (content) => yaml2.parse(content), | ||
".yml": async (content) => yaml2.parse(content) | ||
}; | ||
async function readSecret(data, ctx) { | ||
const secret = secretSchema.validateSync(data, {strict: true}); | ||
if ("file" in secret) { | ||
return ctx.readFile(secret.file); | ||
} | ||
if ("env" in secret) { | ||
return ctx.env[secret.env]; | ||
} | ||
if ("data" in secret) { | ||
const ext = path.extname(secret.data); | ||
const parser = dataSecretParser[ext]; | ||
if (!parser) { | ||
throw new Error(`No data secret parser available for extension ${ext}`); | ||
} | ||
const content = await ctx.readFile(secret.data); | ||
const {path: path2} = secret; | ||
const parts = typeof path2 === "string" ? path2.split(".") : path2; | ||
let value = await parser(content); | ||
for (const [index, part] of parts.entries()) { | ||
if (!isObject(value)) { | ||
const errPath = parts.slice(0, index).join("."); | ||
throw new Error(`Value is not an object at ${errPath} in ${secret.data}`); | ||
} | ||
value = value[part]; | ||
} | ||
return String(value); | ||
} | ||
throw new Error("Secret was left unhandled"); | ||
} | ||
class Context { | ||
constructor(options) { | ||
this.options = options; | ||
} | ||
get env() { | ||
return this.options.env; | ||
} | ||
async readFile(path2) { | ||
return fs.readFile(path.resolve(this.options.rootPath, path2), "utf8"); | ||
} | ||
async readSecret(desc) { | ||
if (!this.options.shouldReadSecrets) { | ||
return void 0; | ||
} | ||
return readSecret(desc, this); | ||
} | ||
} | ||
async function loadConfig(options = {}) { | ||
const configs = []; | ||
configs.push(...readEnv(process.env)); | ||
configs.push(...await readStaticConfig(options)); | ||
const configPaths = await resolveStaticConfig(options); | ||
try { | ||
for (const configPath of configPaths) { | ||
const config2 = await readConfigFile(configPath, new Context({ | ||
env: process.env, | ||
rootPath: path.dirname(configPath), | ||
shouldReadSecrets: Boolean(options.shouldReadSecrets) | ||
})); | ||
configs.push(config2); | ||
} | ||
} catch (error) { | ||
throw new Error(`Failed to read static configuration file: ${error.message}`); | ||
} | ||
return configs; | ||
@@ -98,0 +241,0 @@ } |
@@ -5,6 +5,6 @@ import { AppConfig } from '@backstage/config'; | ||
configPath?: string; | ||
shouldReadSecrets?: boolean; | ||
}; | ||
declare function loadConfig(options?: LoadConfigOptions): Promise<AppConfig[]>; | ||
export { LoadConfigOptions, loadConfig }; |
import fs from 'fs-extra'; | ||
import { resolve, dirname, extname } from 'path'; | ||
import yaml2 from 'yaml'; | ||
import { resolve, dirname } from 'path'; | ||
import { object, string, lazy, array, ValidationError } from 'yup'; | ||
function findRootPath(topPath) { | ||
let path2 = topPath; | ||
for (let i = 0; i < 1000; i++) { | ||
for (let i = 0; i < 1e3; i++) { | ||
const packagePath = resolve(path2, "package.json"); | ||
@@ -29,2 +30,63 @@ const exists = fs.pathExistsSync(packagePath); | ||
async function resolveStaticConfig(options) { | ||
let {configPath} = options; | ||
if (!configPath) { | ||
configPath = resolve(findRootPath(fs.realpathSync(process.cwd())), "app-config.yaml"); | ||
} | ||
return [configPath]; | ||
} | ||
function isObject(obj) { | ||
if (typeof obj !== "object") { | ||
return false; | ||
} else if (Array.isArray(obj)) { | ||
return false; | ||
} | ||
return obj !== null; | ||
} | ||
async function readConfigFile(filePath, ctx) { | ||
const configYaml = await ctx.readFile(filePath); | ||
const config2 = yaml2.parse(configYaml); | ||
async function transform(obj, path) { | ||
if (typeof obj !== "object") { | ||
return obj; | ||
} else if (obj === null) { | ||
return void 0; | ||
} else if (Array.isArray(obj)) { | ||
const arr = new Array(); | ||
for (const [index, value] of obj.entries()) { | ||
const out2 = await transform(value, `${path}[${index}]`); | ||
if (out2 !== void 0) { | ||
arr.push(out2); | ||
} | ||
} | ||
return arr; | ||
} | ||
if ("$secret" in obj) { | ||
if (!isObject(obj.$secret)) { | ||
throw TypeError(`Expected object at secret ${path}.$secret`); | ||
} | ||
try { | ||
return await ctx.readSecret(obj.$secret); | ||
} catch (error) { | ||
throw new Error(`Invalid secret at ${path}: ${error.message}`); | ||
} | ||
} | ||
const out = {}; | ||
for (const [key, value] of Object.entries(obj)) { | ||
const result = await transform(value, `${path}.${key}`); | ||
if (result !== void 0) { | ||
out[key] = result; | ||
} | ||
} | ||
return out; | ||
} | ||
const finalConfig = await transform(config2, ""); | ||
if (!isObject(finalConfig)) { | ||
throw new TypeError("Expected object at config root"); | ||
} | ||
return finalConfig; | ||
} | ||
const ENV_PREFIX = "APP_CONFIG_"; | ||
@@ -72,19 +134,100 @@ const CONFIG_KEY_PART_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z][a-z0-9]*)*$/i; | ||
} | ||
async function readStaticConfig(options) { | ||
let {configPath} = options; | ||
if (!configPath) { | ||
configPath = resolve(findRootPath(fs.realpathSync(process.cwd())), "app-config.yaml"); | ||
const secretLoaderSchemas = { | ||
file: object({ | ||
file: string().required() | ||
}), | ||
env: object({ | ||
env: string().required() | ||
}), | ||
data: object({ | ||
data: string().required(), | ||
path: lazy((value) => { | ||
if (typeof value === "string") { | ||
return string().required(); | ||
} | ||
return array().of(string().required()).required(); | ||
}) | ||
}) | ||
}; | ||
const secretSchema = lazy((value) => { | ||
if (typeof value !== "object" || value === null) { | ||
return object().required().label("secret"); | ||
} | ||
try { | ||
const configYaml = await fs.readFile(configPath, "utf8"); | ||
const config2 = yaml2.parse(configYaml); | ||
return [config2]; | ||
} catch (error) { | ||
throw new Error(`Failed to read static configuration file, ${error}`); | ||
const loaderTypes = Object.keys(secretLoaderSchemas); | ||
for (const key of loaderTypes) { | ||
if (key in value) { | ||
return secretLoaderSchemas[key]; | ||
} | ||
} | ||
throw new ValidationError(`Secret must contain one of '${loaderTypes.join("', '")}'`, value, "$secret"); | ||
}); | ||
const dataSecretParser = { | ||
".json": async (content) => JSON.parse(content), | ||
".yaml": async (content) => yaml2.parse(content), | ||
".yml": async (content) => yaml2.parse(content) | ||
}; | ||
async function readSecret(data, ctx) { | ||
const secret = secretSchema.validateSync(data, {strict: true}); | ||
if ("file" in secret) { | ||
return ctx.readFile(secret.file); | ||
} | ||
if ("env" in secret) { | ||
return ctx.env[secret.env]; | ||
} | ||
if ("data" in secret) { | ||
const ext = extname(secret.data); | ||
const parser = dataSecretParser[ext]; | ||
if (!parser) { | ||
throw new Error(`No data secret parser available for extension ${ext}`); | ||
} | ||
const content = await ctx.readFile(secret.data); | ||
const {path: path2} = secret; | ||
const parts = typeof path2 === "string" ? path2.split(".") : path2; | ||
let value = await parser(content); | ||
for (const [index, part] of parts.entries()) { | ||
if (!isObject(value)) { | ||
const errPath = parts.slice(0, index).join("."); | ||
throw new Error(`Value is not an object at ${errPath} in ${secret.data}`); | ||
} | ||
value = value[part]; | ||
} | ||
return String(value); | ||
} | ||
throw new Error("Secret was left unhandled"); | ||
} | ||
class Context { | ||
constructor(options) { | ||
this.options = options; | ||
} | ||
get env() { | ||
return this.options.env; | ||
} | ||
async readFile(path2) { | ||
return fs.readFile(resolve(this.options.rootPath, path2), "utf8"); | ||
} | ||
async readSecret(desc) { | ||
if (!this.options.shouldReadSecrets) { | ||
return void 0; | ||
} | ||
return readSecret(desc, this); | ||
} | ||
} | ||
async function loadConfig(options = {}) { | ||
const configs = []; | ||
configs.push(...readEnv(process.env)); | ||
configs.push(...await readStaticConfig(options)); | ||
const configPaths = await resolveStaticConfig(options); | ||
try { | ||
for (const configPath of configPaths) { | ||
const config2 = await readConfigFile(configPath, new Context({ | ||
env: process.env, | ||
rootPath: dirname(configPath), | ||
shouldReadSecrets: Boolean(options.shouldReadSecrets) | ||
})); | ||
configs.push(config2); | ||
} | ||
} catch (error) { | ||
throw new Error(`Failed to read static configuration file: ${error.message}`); | ||
} | ||
return configs; | ||
@@ -91,0 +234,0 @@ } |
{ | ||
"name": "@backstage/config-loader", | ||
"description": "Config loading functionality used by Backstage backend, and CLI", | ||
"version": "0.1.1-alpha.7", | ||
"version": "0.1.1-alpha.9", | ||
"private": false, | ||
@@ -35,7 +35,9 @@ "publishConfig": { | ||
"fs-extra": "^9.0.0", | ||
"yaml": "^1.9.2" | ||
"yaml": "^1.9.2", | ||
"yup": "^0.28.5" | ||
}, | ||
"devDependencies": { | ||
"@types/jest": "^25.2.2", | ||
"@types/node": "^12.0.0" | ||
"@types/node": "^12.0.0", | ||
"@types/yup": "^0.28.2" | ||
}, | ||
@@ -45,4 +47,4 @@ "files": [ | ||
], | ||
"gitHead": "e61d09d545e4f4fa68cc204c9db0e0fd4eac84c3", | ||
"gitHead": "89168d4d367e89017eb8992bef0db5b2c6317371", | ||
"module": "dist/index.esm.js" | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
27440
462
4
3
6
1
+ Addedyup@^0.28.5
+ Added@babel/runtime@7.26.0(transitive)
+ Addedfn-name@3.0.0(transitive)
+ Addedlodash-es@4.17.21(transitive)
+ Addedproperty-expr@2.0.6(transitive)
+ Addedregenerator-runtime@0.14.1(transitive)
+ Addedsynchronous-promise@2.0.17(transitive)
+ Addedtoposort@2.0.2(transitive)
+ Addedyup@0.28.5(transitive)