Merge pull request #1860 from spotify/rugvip/config

config,config-loader: add support for suffixed config files and multiple roots
This commit is contained in:
Patrik Oldsberg
2020-08-10 11:51:07 +02:00
committed by GitHub
14 changed files with 222 additions and 63 deletions
@@ -0,0 +1,116 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const pathExists = jest.fn();
jest.mock('fs-extra', () => ({ pathExists }));
import { resolveStaticConfig } from './resolver';
describe('resolveStaticConfig', () => {
afterEach(() => {
jest.resetAllMocks();
});
it('should resolve no files for empty roots', async () => {
const resolved = await resolveStaticConfig({
env: 'development',
rootPaths: [],
});
expect(resolved).toEqual([]);
expect(pathExists).not.toHaveBeenCalled();
});
it('should resolve a single app-config', async () => {
pathExists.mockImplementation(async (path: string) =>
['/repo/app-config.yaml'].includes(path),
);
const resolved = await resolveStaticConfig({
env: 'development',
rootPaths: ['/repo'],
});
expect(resolved).toEqual(['/repo/app-config.yaml']);
expect(pathExists).toHaveBeenCalledTimes(4);
});
it('should resolve a app-configs in different directories', async () => {
pathExists.mockImplementation(async (path: string) =>
['/repo/app-config.yaml', '/repo/packages/a/app-config.yaml'].includes(
path,
),
);
const resolved = await resolveStaticConfig({
env: 'development',
rootPaths: [
'/repo',
'/other-repo',
'/repo/packages/a',
'/repo/packages/b',
],
});
expect(resolved).toEqual([
'/repo/app-config.yaml',
'/repo/packages/a/app-config.yaml',
]);
expect(pathExists).toHaveBeenCalledTimes(16);
});
it('should resolve env and local configs', async () => {
pathExists.mockImplementation(async (path: string) =>
[
'/repo/app-config.yaml',
'/repo/app-config.local.yaml',
'/repo/app-config.production.yaml',
'/repo/app-config.production.local.yaml',
'/repo/app-config.development.local.yaml',
'/repo/packages/a/app-config.development.yaml',
'/repo/packages/a/app-config.local.yaml',
].includes(path),
);
const resolved = await resolveStaticConfig({
env: 'development',
rootPaths: ['/repo', '/repo/packages/a'],
});
expect(resolved).toEqual([
'/repo/app-config.yaml',
'/repo/app-config.local.yaml',
'/repo/app-config.development.local.yaml',
'/repo/packages/a/app-config.local.yaml',
'/repo/packages/a/app-config.development.yaml',
]);
expect(pathExists).toHaveBeenCalledTimes(8);
});
it('resolves suffixed configs in the correct order', async () => {
pathExists.mockImplementation(async () => true);
const resolved = await resolveStaticConfig({
env: 'production',
rootPaths: ['/repo'],
});
expect(resolved).toEqual([
'/repo/app-config.yaml',
'/repo/app-config.local.yaml',
'/repo/app-config.production.yaml',
'/repo/app-config.production.local.yaml',
]);
expect(pathExists).toHaveBeenCalledTimes(4);
});
});
+30 -6
View File
@@ -15,21 +15,45 @@
*/
import { resolve as resolvePath } from 'path';
import { pathExists } from 'fs-extra';
type ResolveOptions = {
// Root path for search for app-config.yaml
rootPath: string;
// Root paths to search for config files. Config from earlier paths has lower priority.
rootPaths: string[];
// The environment that we're loading config for, e.g. 'development', 'production'.
env: string;
};
/**
* Resolves all configuration files that should be loaded in the given environment.
*
* For each root directory, search for the default app-config.yaml, along with suffixed
* NODE_ENV and local variants, e.g. app-config.production.yaml or app-config.development.local.yaml
*
* The priority order of config loaded through suffixes is `env > local > none`, meaning that
* for example app-config.development.yaml has higher priority than `app-config.local.yaml`.
*
*/
export async function resolveStaticConfig(
options: ResolveOptions,
): Promise<string[]> {
// TODO: We'll want this to be a bit more elaborate, probably adding configs for
// specific env, and maybe local config for plugins.
const configPath = resolvePath(options.rootPath, 'app-config.yaml');
const filePaths = [
`app-config.yaml`,
`app-config.local.yaml`,
`app-config.${options.env}.yaml`,
`app-config.${options.env}.local.yaml`,
];
return [configPath];
const resolvedPaths = [];
for (const rootPath of options.rootPaths) {
for (const filePath of filePaths) {
const path = resolvePath(rootPath, filePath);
if (await pathExists(path)) {
resolvedPaths.push(path);
}
}
}
return resolvedPaths;
}
+7 -4
View File
@@ -25,8 +25,11 @@ import {
} from './lib';
export type LoadConfigOptions = {
// Root path for search for app-config.yaml
rootPath: string;
// Root paths to search for config files. Config from earlier paths has lower priority.
rootPaths: string[];
// The environment that we're loading config for, e.g. 'development', 'production'.
env: string;
// Whether to read secrets or omit them, defaults to false.
shouldReadSecrets?: boolean;
@@ -63,8 +66,6 @@ export async function loadConfig(
): Promise<AppConfig[]> {
const configs = [];
configs.push(...readEnv(process.env));
const configPaths = await resolveStaticConfig(options);
try {
@@ -86,5 +87,7 @@ export async function loadConfig(
);
}
configs.push(...readEnv(process.env));
return configs;
}