packages/config-loader: added secret reading option + initial validation and file handler

This commit is contained in:
Patrik Oldsberg
2020-06-12 15:52:27 +02:00
parent ed8436717a
commit fcc514e909
3 changed files with 83 additions and 11 deletions
+4 -2
View File
@@ -32,11 +32,13 @@
"dependencies": {
"@backstage/config": "^0.1.1-alpha.7",
"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"
},
"files": [
"dist/**/*.{js,d.ts}"
+76 -9
View File
@@ -15,8 +15,9 @@
*/
import fs from 'fs-extra';
import * as yup from 'yup';
import yaml from 'yaml';
import { resolve as resolvePath } from 'path';
import { resolve as resolvePath, dirname } from 'path';
import { AppConfig, JsonObject, JsonValue } from '@backstage/config';
import { findRootPath } from './paths';
import { LoadConfigOptions } from './types';
@@ -79,6 +80,11 @@ export function readEnv(env: {
type ReadFileFunc = (path: string) => Promise<string>;
type ReaderContext = {
shouldReadSecrets: boolean;
readFile: ReadFileFunc;
};
function isObject(obj: JsonValue | undefined): obj is JsonObject {
if (typeof obj !== 'object') {
return false;
@@ -88,8 +94,54 @@ function isObject(obj: JsonValue | undefined): obj is JsonObject {
return obj !== null;
}
export async function readConfigFile(filePath: string, readFile: ReadFileFunc) {
const configYaml = await readFile(filePath);
type FileSecret = {
file: string;
};
type Secret = FileSecret;
const secretLoaderSchemas = {
file: yup.object({
file: yup.string().required(),
}),
};
const secretSchema = yup.lazy<object>(value => {
if (typeof value !== 'object' || value === null) {
return yup.object().required();
}
const loaderTypes = Object.keys(
secretLoaderSchemas,
) as (keyof typeof 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',
);
});
export async function readSecret(
data: JsonObject,
readFile: ReadFileFunc,
): Promise<string | undefined> {
const secret = secretSchema.validateSync(data) as Secret;
if ('file' in secret) {
return readFile(secret.file);
}
throw new Error('Secret was left unhandled');
}
export async function readConfigFile(filePath: string, context: ReaderContext) {
const configYaml = await context.readFile(filePath);
const config = yaml.parse(configYaml);
async function transform(
@@ -115,10 +167,18 @@ export async function readConfigFile(filePath: string, readFile: ReadFileFunc) {
if ('$secret' in obj) {
if (!isObject(obj.$secret)) {
throw TypeError(`Secret expected object at secret ${path}.$secret`);
throw TypeError(`Expected object at secret ${path}.$secret`);
}
return undefined;
if (!context.shouldReadSecrets) {
return undefined;
}
try {
return await readSecret(obj.$secret, context.readFile);
} catch (error) {
throw new Error(`Invalid secret at ${path}: ${error.message}`);
}
}
const out: JsonObject = {};
@@ -143,6 +203,8 @@ export async function readConfigFile(filePath: string, readFile: ReadFileFunc) {
export async function loadStaticConfig(
options: LoadConfigOptions,
): Promise<AppConfig[]> {
const { shouldReadSecrets = false } = options;
// TODO: We'll want this to be a bit more elaborate, probably adding configs for
// specific env, and maybe local config for plugins.
let { configPath } = options;
@@ -154,13 +216,18 @@ export async function loadStaticConfig(
}
try {
const rootPath = configPath;
const config = await readConfigFile(configPath, (path: string) => {
return fs.readFile(resolvePath(rootPath, path), 'utf8');
const rootPath = dirname(configPath);
const config = await readConfigFile(configPath, {
shouldReadSecrets,
readFile: (path: string) => {
return fs.readFile(resolvePath(rootPath, path), 'utf8');
},
});
return [config];
} catch (error) {
throw new Error(`Failed to read static configuration file, ${error}`);
throw new Error(
`Failed to read static configuration file: ${error.message}`,
);
}
}
+3
View File
@@ -17,4 +17,7 @@
export type LoadConfigOptions = {
// Config path, defaults to app-config.yaml in project root
configPath?: string;
// Whether to read secrets or omit them, defaults to false.
shouldReadSecrets?: boolean;
};