@backstage/config-loader
Advanced tools
Comparing version 0.5.1 to 0.6.0
# @backstage/config-loader | ||
## 0.6.0 | ||
### Minor Changes | ||
- 82c66b8cd: Fix bug where `${...}` was not being escaped to `${...}` | ||
Add support for environment variable substitution in `$include`, `$file` and | ||
`$env` transform values. | ||
- This change allows for including dynamic paths, such as environment specific | ||
secrets by using the same environment variable substitution (`${..}`) already | ||
supported outside of the various include transforms. | ||
- If you are currently using the syntax `${...}` in your include transform values, | ||
you will need to escape the substitution by using `${...}` instead to maintain | ||
the same behavior. | ||
## 0.5.1 | ||
@@ -4,0 +20,0 @@ |
@@ -5,3 +5,3 @@ 'use strict'; | ||
var yaml2 = require('yaml'); | ||
var yaml = require('yaml'); | ||
var path = require('path'); | ||
@@ -16,3 +16,3 @@ var Ajv = require('ajv'); | ||
var yaml2__default = /*#__PURE__*/_interopDefaultLegacy(yaml2); | ||
var yaml__default = /*#__PURE__*/_interopDefaultLegacy(yaml); | ||
var Ajv__default = /*#__PURE__*/_interopDefaultLegacy(Ajv); | ||
@@ -135,6 +135,6 @@ var mergeAllOf__default = /*#__PURE__*/_interopDefaultLegacy(mergeAllOf); | ||
".json": async (content) => JSON.parse(content), | ||
".yaml": async (content) => yaml2__default['default'].parse(content), | ||
".yml": async (content) => yaml2__default['default'].parse(content) | ||
".yaml": async (content) => yaml__default['default'].parse(content), | ||
".yml": async (content) => yaml__default['default'].parse(content) | ||
}; | ||
function createIncludeTransform(env, readFile) { | ||
function createIncludeTransform(env, readFile, substitute) { | ||
return async (input, baseDir) => { | ||
@@ -152,6 +152,11 @@ if (!isObject(input)) { | ||
} | ||
const includeValue = input[includeKey]; | ||
if (typeof includeValue !== "string") { | ||
const rawIncludedValue = input[includeKey]; | ||
if (typeof rawIncludedValue !== "string") { | ||
throw new Error(`${includeKey} include value is not a string`); | ||
} | ||
const substituteResults = await substitute(rawIncludedValue, baseDir); | ||
const includeValue = substituteResults.applied ? substituteResults.value : rawIncludedValue; | ||
if (includeValue === void 0 || typeof includeValue !== "string") { | ||
throw new Error(`${includeKey} substitution value was undefined`); | ||
} | ||
switch (includeKey) { | ||
@@ -178,5 +183,5 @@ case "$file": | ||
} | ||
const path2 = path.resolve(baseDir, filePath); | ||
const content = await readFile(path2); | ||
const newBaseDir = path.dirname(path2); | ||
const path$1 = path.resolve(baseDir, filePath); | ||
const content = await readFile(path$1); | ||
const newBaseDir = path.dirname(path$1); | ||
const parts = dataPath ? dataPath.split(".") : []; | ||
@@ -213,5 +218,10 @@ let value; | ||
} | ||
const parts = input.split(/(?<!\$)\$\{([^{}]+)\}/); | ||
const parts = input.split(/(\$?\$\{[^{}]*\})/); | ||
for (let i = 1; i < parts.length; i += 2) { | ||
parts[i] = await env(parts[i].trim()); | ||
const part = parts[i]; | ||
if (part.startsWith("$$")) { | ||
parts[i] = part.slice(1); | ||
} else { | ||
parts[i] = await env(part.slice(2, -1).trim()); | ||
} | ||
} | ||
@@ -230,3 +240,3 @@ if (parts.some((part) => part === void 0)) { | ||
const visibilityByPath = new Map(); | ||
const ajv2 = new Ajv__default['default']({ | ||
const ajv = new Ajv__default['default']({ | ||
allErrors: true, | ||
@@ -258,3 +268,3 @@ allowUnionTypes: true, | ||
try { | ||
ajv2.compile(schema.value); | ||
ajv.compile(schema.value); | ||
} catch (error) { | ||
@@ -264,25 +274,9 @@ throw new Error(`Schema at ${schema.path} is invalid, ${error}`); | ||
} | ||
const merged = mergeAllOf__default['default']({allOf: schemas.map((_) => _.value)}, { | ||
ignoreAdditionalProperties: true, | ||
resolvers: { | ||
visibility(values, path) { | ||
const hasFrontend = values.some((_) => _ === "frontend"); | ||
const hasSecret = values.some((_) => _ === "secret"); | ||
if (hasFrontend && hasSecret) { | ||
throw new Error(`Config schema visibility is both 'frontend' and 'secret' for ${path.join("/")}`); | ||
} else if (hasFrontend) { | ||
return "frontend"; | ||
} else if (hasSecret) { | ||
return "secret"; | ||
} | ||
return "backend"; | ||
} | ||
} | ||
}); | ||
const validate = ajv2.compile(merged); | ||
const merged = mergeConfigSchemas(schemas.map((_) => _.value)); | ||
const validate = ajv.compile(merged); | ||
return (configs) => { | ||
var _a; | ||
const config2 = config.ConfigReader.fromConfigs(configs).get(); | ||
const config$1 = config.ConfigReader.fromConfigs(configs).get(); | ||
visibilityByPath.clear(); | ||
const valid = validate(config2); | ||
const valid = validate(config$1); | ||
if (!valid) { | ||
@@ -303,2 +297,22 @@ const errors = (_a = validate.errors) != null ? _a : []; | ||
} | ||
function mergeConfigSchemas(schemas) { | ||
const merged = mergeAllOf__default['default']({allOf: schemas}, { | ||
ignoreAdditionalProperties: true, | ||
resolvers: { | ||
visibility(values, path) { | ||
const hasFrontend = values.some((_) => _ === "frontend"); | ||
const hasSecret = values.some((_) => _ === "secret"); | ||
if (hasFrontend && hasSecret) { | ||
throw new Error(`Config schema visibility is both 'frontend' and 'secret' for ${path.join("/")}`); | ||
} else if (hasFrontend) { | ||
return "frontend"; | ||
} else if (hasSecret) { | ||
return "secret"; | ||
} | ||
return "backend"; | ||
} | ||
} | ||
}); | ||
return merged; | ||
} | ||
@@ -345,7 +359,7 @@ const req = typeof __non_webpack_require__ === "undefined" ? require : __non_webpack_require__; | ||
} else { | ||
const path2 = path.resolve(path.dirname(pkgPath), pkg.configSchema); | ||
const value = await fs__default['default'].readJson(path2); | ||
const path$1 = path.resolve(path.dirname(pkgPath), pkg.configSchema); | ||
const value = await fs__default['default'].readJson(path$1); | ||
schemas.push({ | ||
value, | ||
path: path.relative(currentDir, path2) | ||
path: path.relative(currentDir, path$1) | ||
}); | ||
@@ -382,3 +396,3 @@ } | ||
}); | ||
const tsSchemas = paths.map((path2) => { | ||
const tsSchemas = paths.map((path$1) => { | ||
let value; | ||
@@ -389,3 +403,3 @@ try { | ||
validationKeywords: ["visibility"] | ||
}, [path2.split(path.sep).join("/")]); | ||
}, [path$1.split(path.sep).join("/")]); | ||
} catch (error) { | ||
@@ -397,5 +411,5 @@ if (error.message !== "type Config not found") { | ||
if (!value) { | ||
throw new Error(`Invalid schema in ${path2}, missing Config export`); | ||
throw new Error(`Invalid schema in ${path$1}, missing Config export`); | ||
} | ||
return {path: path2, value}; | ||
return {path: path$1, value}; | ||
}); | ||
@@ -515,7 +529,8 @@ return tsSchemas; | ||
const dir = path.dirname(configPath); | ||
const readFile = (path2) => fs__default['default'].readFile(path.resolve(dir, path2), "utf8"); | ||
const input = yaml2__default['default'].parse(await readFile(configPath)); | ||
const readFile = (path$1) => fs__default['default'].readFile(path.resolve(dir, path$1), "utf8"); | ||
const input = yaml__default['default'].parse(await readFile(configPath)); | ||
const substitutionTransform = createSubstitutionTransform(env); | ||
const data = await applyConfigTransforms(dir, input, [ | ||
createIncludeTransform(env, readFile), | ||
createSubstitutionTransform(env) | ||
createIncludeTransform(env, readFile, substitutionTransform), | ||
substitutionTransform | ||
]); | ||
@@ -533,3 +548,4 @@ configs.push({data, context: path.basename(configPath)}); | ||
exports.loadConfigSchema = loadConfigSchema; | ||
exports.mergeConfigSchemas = mergeConfigSchemas; | ||
exports.readEnvConfig = readEnvConfig; | ||
//# sourceMappingURL=index.cjs.js.map |
import { AppConfig, JsonObject } from '@backstage/config'; | ||
import { JSONSchema7 } from 'json-schema'; | ||
@@ -66,2 +67,8 @@ /** | ||
/** | ||
* Given a list of configuration schemas from packages, merge them | ||
* into a single json schema. | ||
*/ | ||
declare function mergeConfigSchemas(schemas: JSONSchema7[]): JSONSchema7; | ||
declare type Options = { | ||
@@ -91,2 +98,2 @@ dependencies: string[]; | ||
export { ConfigSchema, ConfigVisibility, LoadConfigOptions, loadConfig, loadConfigSchema, readEnvConfig }; | ||
export { ConfigSchema, ConfigVisibility, LoadConfigOptions, loadConfig, loadConfigSchema, mergeConfigSchemas, readEnvConfig }; |
@@ -1,2 +0,2 @@ | ||
import yaml2 from 'yaml'; | ||
import yaml from 'yaml'; | ||
import { extname, resolve, dirname, sep, relative, isAbsolute, basename } from 'path'; | ||
@@ -122,6 +122,6 @@ import Ajv from 'ajv'; | ||
".json": async (content) => JSON.parse(content), | ||
".yaml": async (content) => yaml2.parse(content), | ||
".yml": async (content) => yaml2.parse(content) | ||
".yaml": async (content) => yaml.parse(content), | ||
".yml": async (content) => yaml.parse(content) | ||
}; | ||
function createIncludeTransform(env, readFile) { | ||
function createIncludeTransform(env, readFile, substitute) { | ||
return async (input, baseDir) => { | ||
@@ -139,6 +139,11 @@ if (!isObject(input)) { | ||
} | ||
const includeValue = input[includeKey]; | ||
if (typeof includeValue !== "string") { | ||
const rawIncludedValue = input[includeKey]; | ||
if (typeof rawIncludedValue !== "string") { | ||
throw new Error(`${includeKey} include value is not a string`); | ||
} | ||
const substituteResults = await substitute(rawIncludedValue, baseDir); | ||
const includeValue = substituteResults.applied ? substituteResults.value : rawIncludedValue; | ||
if (includeValue === void 0 || typeof includeValue !== "string") { | ||
throw new Error(`${includeKey} substitution value was undefined`); | ||
} | ||
switch (includeKey) { | ||
@@ -165,5 +170,5 @@ case "$file": | ||
} | ||
const path2 = resolve(baseDir, filePath); | ||
const content = await readFile(path2); | ||
const newBaseDir = dirname(path2); | ||
const path = resolve(baseDir, filePath); | ||
const content = await readFile(path); | ||
const newBaseDir = dirname(path); | ||
const parts = dataPath ? dataPath.split(".") : []; | ||
@@ -200,5 +205,10 @@ let value; | ||
} | ||
const parts = input.split(/(?<!\$)\$\{([^{}]+)\}/); | ||
const parts = input.split(/(\$?\$\{[^{}]*\})/); | ||
for (let i = 1; i < parts.length; i += 2) { | ||
parts[i] = await env(parts[i].trim()); | ||
const part = parts[i]; | ||
if (part.startsWith("$$")) { | ||
parts[i] = part.slice(1); | ||
} else { | ||
parts[i] = await env(part.slice(2, -1).trim()); | ||
} | ||
} | ||
@@ -217,3 +227,3 @@ if (parts.some((part) => part === void 0)) { | ||
const visibilityByPath = new Map(); | ||
const ajv2 = new Ajv({ | ||
const ajv = new Ajv({ | ||
allErrors: true, | ||
@@ -245,3 +255,3 @@ allowUnionTypes: true, | ||
try { | ||
ajv2.compile(schema.value); | ||
ajv.compile(schema.value); | ||
} catch (error) { | ||
@@ -251,25 +261,9 @@ throw new Error(`Schema at ${schema.path} is invalid, ${error}`); | ||
} | ||
const merged = mergeAllOf({allOf: schemas.map((_) => _.value)}, { | ||
ignoreAdditionalProperties: true, | ||
resolvers: { | ||
visibility(values, path) { | ||
const hasFrontend = values.some((_) => _ === "frontend"); | ||
const hasSecret = values.some((_) => _ === "secret"); | ||
if (hasFrontend && hasSecret) { | ||
throw new Error(`Config schema visibility is both 'frontend' and 'secret' for ${path.join("/")}`); | ||
} else if (hasFrontend) { | ||
return "frontend"; | ||
} else if (hasSecret) { | ||
return "secret"; | ||
} | ||
return "backend"; | ||
} | ||
} | ||
}); | ||
const validate = ajv2.compile(merged); | ||
const merged = mergeConfigSchemas(schemas.map((_) => _.value)); | ||
const validate = ajv.compile(merged); | ||
return (configs) => { | ||
var _a; | ||
const config2 = ConfigReader.fromConfigs(configs).get(); | ||
const config = ConfigReader.fromConfigs(configs).get(); | ||
visibilityByPath.clear(); | ||
const valid = validate(config2); | ||
const valid = validate(config); | ||
if (!valid) { | ||
@@ -290,2 +284,22 @@ const errors = (_a = validate.errors) != null ? _a : []; | ||
} | ||
function mergeConfigSchemas(schemas) { | ||
const merged = mergeAllOf({allOf: schemas}, { | ||
ignoreAdditionalProperties: true, | ||
resolvers: { | ||
visibility(values, path) { | ||
const hasFrontend = values.some((_) => _ === "frontend"); | ||
const hasSecret = values.some((_) => _ === "secret"); | ||
if (hasFrontend && hasSecret) { | ||
throw new Error(`Config schema visibility is both 'frontend' and 'secret' for ${path.join("/")}`); | ||
} else if (hasFrontend) { | ||
return "frontend"; | ||
} else if (hasSecret) { | ||
return "secret"; | ||
} | ||
return "backend"; | ||
} | ||
} | ||
}); | ||
return merged; | ||
} | ||
@@ -332,7 +346,7 @@ const req = typeof __non_webpack_require__ === "undefined" ? require : __non_webpack_require__; | ||
} else { | ||
const path2 = resolve(dirname(pkgPath), pkg.configSchema); | ||
const value = await fs.readJson(path2); | ||
const path = resolve(dirname(pkgPath), pkg.configSchema); | ||
const value = await fs.readJson(path); | ||
schemas.push({ | ||
value, | ||
path: relative(currentDir, path2) | ||
path: relative(currentDir, path) | ||
}); | ||
@@ -369,3 +383,3 @@ } | ||
}); | ||
const tsSchemas = paths.map((path2) => { | ||
const tsSchemas = paths.map((path) => { | ||
let value; | ||
@@ -376,3 +390,3 @@ try { | ||
validationKeywords: ["visibility"] | ||
}, [path2.split(sep).join("/")]); | ||
}, [path.split(sep).join("/")]); | ||
} catch (error) { | ||
@@ -384,5 +398,5 @@ if (error.message !== "type Config not found") { | ||
if (!value) { | ||
throw new Error(`Invalid schema in ${path2}, missing Config export`); | ||
throw new Error(`Invalid schema in ${path}, missing Config export`); | ||
} | ||
return {path: path2, value}; | ||
return {path, value}; | ||
}); | ||
@@ -502,7 +516,8 @@ return tsSchemas; | ||
const dir = dirname(configPath); | ||
const readFile = (path2) => fs.readFile(resolve(dir, path2), "utf8"); | ||
const input = yaml2.parse(await readFile(configPath)); | ||
const readFile = (path) => fs.readFile(resolve(dir, path), "utf8"); | ||
const input = yaml.parse(await readFile(configPath)); | ||
const substitutionTransform = createSubstitutionTransform(env); | ||
const data = await applyConfigTransforms(dir, input, [ | ||
createIncludeTransform(env, readFile), | ||
createSubstitutionTransform(env) | ||
createIncludeTransform(env, readFile, substitutionTransform), | ||
substitutionTransform | ||
]); | ||
@@ -518,3 +533,3 @@ configs.push({data, context: basename(configPath)}); | ||
export { loadConfig, loadConfigSchema, readEnvConfig }; | ||
export { loadConfig, loadConfigSchema, mergeConfigSchemas, readEnvConfig }; | ||
//# sourceMappingURL=index.esm.js.map |
{ | ||
"name": "@backstage/config-loader", | ||
"description": "Config loading functionality used by Backstage backend, and CLI", | ||
"version": "0.5.1", | ||
"version": "0.6.0", | ||
"private": false, | ||
@@ -35,2 +35,3 @@ "publishConfig": { | ||
"@backstage/config": "^0.1.1", | ||
"@types/json-schema": "^7.0.6", | ||
"ajv": "^7.0.3", | ||
@@ -40,3 +41,3 @@ "fs-extra": "^9.0.0", | ||
"json-schema-merge-allof": "^0.7.0", | ||
"typescript-json-schema": "^0.47.0", | ||
"typescript-json-schema": "^0.49.0", | ||
"yaml": "^1.9.2", | ||
@@ -47,6 +48,5 @@ "yup": "^0.29.3" | ||
"@types/jest": "^26.0.7", | ||
"@types/json-schema": "^7.0.6", | ||
"@types/json-schema-merge-allof": "^0.6.0", | ||
"@types/mock-fs": "^4.10.0", | ||
"@types/node": "^12.0.0", | ||
"@types/node": "^14.14.32", | ||
"@types/yup": "^0.29.8", | ||
@@ -58,3 +58,4 @@ "mock-fs": "^4.13.0" | ||
], | ||
"gitHead": "ae7b2a28d60159b94eda7020b41e7c3a86c8fb3a", | ||
"module": "dist/index.esm.js" | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
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
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
146710
6
9
1118
10
+ Added@types/json-schema@^7.0.6
+ Addedarg@4.1.3(transitive)
+ Addedbuffer-from@1.1.2(transitive)
+ Addedcreate-require@1.1.1(transitive)
+ Addeddiff@4.0.2(transitive)
+ Addedmake-error@1.3.6(transitive)
+ Addedsource-map@0.6.1(transitive)
+ Addedsource-map-support@0.5.21(transitive)
+ Addedts-node@9.1.1(transitive)
+ Addedtypescript-json-schema@0.49.0(transitive)
+ Addedyn@3.1.1(transitive)
- Removedtypescript-json-schema@0.47.0(transitive)