config-loader: refactor source types
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -92,7 +92,7 @@ export async function loadConfig(
|
||||
options: LoadConfigOptions,
|
||||
): Promise<LoadConfigResult> {
|
||||
const source = ConfigSources.default({
|
||||
envFunc: options.experimentalEnvFunc,
|
||||
substitutionFunc: options.experimentalEnvFunc,
|
||||
remote: options.remote,
|
||||
rootDir: options.configRoot,
|
||||
argv: options.configTargets.flatMap(t => [
|
||||
|
||||
@@ -21,15 +21,45 @@ import parseArgs from 'minimist';
|
||||
import { EnvConfigSource } from './EnvConfigSource';
|
||||
import { FileConfigSource } from './FileConfigSource';
|
||||
import { MergedConfigSource } from './MergedConfigSource';
|
||||
import { RemoteConfigSource } from './RemoteConfigSource';
|
||||
import { ConfigSource, EnvFunc } from './types';
|
||||
import {
|
||||
RemoteConfigSource,
|
||||
RemoteConfigSourceOptions,
|
||||
} from './RemoteConfigSource';
|
||||
import { ConfigSource, SubstitutionFunc } from './types';
|
||||
import { ObservableConfigProxy } from './ObservableConfigProxy';
|
||||
import { LoadConfigOptionsRemote } from '../loader';
|
||||
|
||||
export type ConfigSourceTarget =
|
||||
| {
|
||||
type: 'path';
|
||||
target: string;
|
||||
}
|
||||
| {
|
||||
type: 'url';
|
||||
target: string;
|
||||
};
|
||||
|
||||
export interface ClosableConfig extends Config {
|
||||
close(): void;
|
||||
}
|
||||
|
||||
export interface BaseConfigSourcesOptions {
|
||||
rootDir: string;
|
||||
remote?: Pick<RemoteConfigSourceOptions, 'reloadIntervalSeconds'>;
|
||||
substitutionFunc?: SubstitutionFunc;
|
||||
}
|
||||
|
||||
export interface ConfigSourcesDefaultForTargetsOptions
|
||||
extends BaseConfigSourcesOptions {
|
||||
targets: ConfigSourceTarget[];
|
||||
}
|
||||
|
||||
export interface ConfigSourcesDefaultOptions extends BaseConfigSourcesOptions {
|
||||
argv?: string[];
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
|
||||
export class ConfigSources {
|
||||
static parseArgs(
|
||||
argv: string[] = process.argv,
|
||||
): Array<{ type: 'url' | 'path'; target: string }> {
|
||||
static parseArgs(argv: string[] = process.argv): Array<ConfigSourceTarget> {
|
||||
const args: string[] = [parseArgs(argv).config].flat().filter(Boolean);
|
||||
return args.map(target => {
|
||||
try {
|
||||
@@ -42,12 +72,9 @@ export class ConfigSources {
|
||||
});
|
||||
}
|
||||
|
||||
static defaultForTargets(options: {
|
||||
rootDir: string;
|
||||
targets: Array<{ type: 'url' | 'path'; target: string }>;
|
||||
remote?: LoadConfigOptionsRemote;
|
||||
envFunc?: EnvFunc;
|
||||
}): ConfigSource {
|
||||
static defaultForTargets(
|
||||
options: ConfigSourcesDefaultForTargetsOptions,
|
||||
): ConfigSource {
|
||||
const argSources = options.targets.map(arg => {
|
||||
if (arg.type === 'url') {
|
||||
if (!options.remote) {
|
||||
@@ -57,13 +84,13 @@ export class ConfigSources {
|
||||
}
|
||||
return RemoteConfigSource.create({
|
||||
url: arg.target,
|
||||
envFunc: options.envFunc,
|
||||
substitutionFunc: options.substitutionFunc,
|
||||
reloadIntervalSeconds: options.remote.reloadIntervalSeconds,
|
||||
});
|
||||
}
|
||||
return FileConfigSource.create({
|
||||
path: arg.target,
|
||||
envFunc: options.envFunc,
|
||||
substitutionFunc: options.substitutionFunc,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -74,14 +101,14 @@ export class ConfigSources {
|
||||
argSources.push(
|
||||
FileConfigSource.create({
|
||||
path: defaultPath,
|
||||
envFunc: options.envFunc,
|
||||
substitutionFunc: options.substitutionFunc,
|
||||
}),
|
||||
);
|
||||
if (fs.pathExistsSync(localPath)) {
|
||||
argSources.push(
|
||||
FileConfigSource.create({
|
||||
path: localPath,
|
||||
envFunc: options.envFunc,
|
||||
substitutionFunc: options.substitutionFunc,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -90,13 +117,7 @@ export class ConfigSources {
|
||||
return this.merge(argSources);
|
||||
}
|
||||
|
||||
static default(options: {
|
||||
rootDir: string;
|
||||
argv?: string[];
|
||||
remote?: LoadConfigOptionsRemote;
|
||||
env?: Record<string, string>;
|
||||
envFunc?: EnvFunc;
|
||||
}): ConfigSource {
|
||||
static default(options: ConfigSourcesDefaultOptions): ConfigSource {
|
||||
const argSource = this.defaultForTargets({
|
||||
...options,
|
||||
targets: this.parseArgs(options.argv),
|
||||
@@ -111,7 +132,7 @@ export class ConfigSources {
|
||||
return MergedConfigSource.from(sources);
|
||||
}
|
||||
|
||||
static toConfig(source: ConfigSource): Promise<LiveConfig> {
|
||||
static toConfig(source: ConfigSource): Promise<ClosableConfig> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let config: ObservableConfigProxy | undefined = undefined;
|
||||
try {
|
||||
@@ -133,7 +154,3 @@ export class ConfigSources {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface LiveConfig extends Config {
|
||||
close(): void;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
AsyncConfigSourceIterator,
|
||||
ConfigSource,
|
||||
ConfigSourceData,
|
||||
EnvFunc,
|
||||
SubstitutionFunc,
|
||||
ReadConfigDataOptions,
|
||||
} from './types';
|
||||
import { createConfigTransformer } from './transform';
|
||||
@@ -35,7 +35,7 @@ export interface FileConfigSourceOptions {
|
||||
/**
|
||||
* Function used to resolve environment variables.
|
||||
*/
|
||||
envFunc?: EnvFunc;
|
||||
substitutionFunc?: SubstitutionFunc;
|
||||
}
|
||||
|
||||
export class FileConfigSource implements ConfigSource {
|
||||
@@ -47,11 +47,11 @@ export class FileConfigSource implements ConfigSource {
|
||||
}
|
||||
|
||||
readonly #path: string;
|
||||
readonly #envFunc?: EnvFunc;
|
||||
readonly #substitutionFunc?: SubstitutionFunc;
|
||||
|
||||
private constructor(options: FileConfigSourceOptions) {
|
||||
this.#path = options.path;
|
||||
this.#envFunc = options.envFunc;
|
||||
this.#substitutionFunc = options.substitutionFunc;
|
||||
}
|
||||
|
||||
// Work is duplicated across each read, in practice that should not
|
||||
@@ -71,7 +71,7 @@ export class FileConfigSource implements ConfigSource {
|
||||
|
||||
const dir = dirname(this.#path);
|
||||
const transformer = createConfigTransformer({
|
||||
envFunc: this.#envFunc,
|
||||
substitutionFunc: this.#substitutionFunc,
|
||||
async readFile(path) {
|
||||
const fullPath = resolvePath(dir, path);
|
||||
// Any files discovered while reading this config should be watched too
|
||||
|
||||
@@ -23,7 +23,7 @@ import { ConfigTransformer, createConfigTransformer } from './transform';
|
||||
import {
|
||||
AsyncConfigSourceIterator,
|
||||
ConfigSource,
|
||||
EnvFunc,
|
||||
SubstitutionFunc,
|
||||
ReadConfigDataOptions,
|
||||
} from './types';
|
||||
|
||||
@@ -32,7 +32,7 @@ const DEFAULT_RELOAD_INTERVAL_SECONDS = 60;
|
||||
export interface RemoteConfigSourceOptions {
|
||||
url: string;
|
||||
reloadIntervalSeconds?: number;
|
||||
envFunc?: EnvFunc;
|
||||
substitutionFunc?: SubstitutionFunc;
|
||||
}
|
||||
|
||||
export class RemoteConfigSource implements ConfigSource {
|
||||
@@ -56,7 +56,9 @@ export class RemoteConfigSource implements ConfigSource {
|
||||
this.#url = options.url;
|
||||
this.#reloadIntervalSeconds =
|
||||
options.reloadIntervalSeconds ?? DEFAULT_RELOAD_INTERVAL_SECONDS;
|
||||
this.#transformer = createConfigTransformer({ envFunc: options.envFunc });
|
||||
this.#transformer = createConfigTransformer({
|
||||
substitutionFunc: options.substitutionFunc,
|
||||
});
|
||||
}
|
||||
|
||||
async *readConfigData(
|
||||
|
||||
@@ -25,7 +25,7 @@ export type { RemoteConfigSourceOptions } from './RemoteConfigSource';
|
||||
export { StaticConfigSource } from './StaticConfigSource';
|
||||
export type { StaticConfigSourceOptions } from './StaticConfigSource';
|
||||
export type {
|
||||
EnvFunc,
|
||||
SubstitutionFunc as EnvFunc,
|
||||
ConfigSource,
|
||||
ConfigSourceData,
|
||||
ReadConfigDataOptions,
|
||||
|
||||
@@ -20,7 +20,7 @@ import { TransformContext, TransformFunc } from './types';
|
||||
import { isObject } from './utils';
|
||||
import { createSubstitutionTransform } from './substitution';
|
||||
import { createIncludeTransform } from './include';
|
||||
import { EnvFunc } from '../types';
|
||||
import { SubstitutionFunc } from '../types';
|
||||
|
||||
/**
|
||||
* Applies a set of transforms to raw configuration data.
|
||||
@@ -102,15 +102,16 @@ export type ConfigTransformer = (
|
||||
|
||||
/** @internal */
|
||||
export function createConfigTransformer(options: {
|
||||
envFunc?: EnvFunc;
|
||||
substitutionFunc?: SubstitutionFunc;
|
||||
readFile?(path: string): Promise<string>;
|
||||
}): ConfigTransformer {
|
||||
const { envFunc = async name => process.env[name], readFile } = options;
|
||||
const substitutionTransform = createSubstitutionTransform(envFunc);
|
||||
const { substitutionFunc = async name => process.env[name], readFile } =
|
||||
options;
|
||||
const substitutionTransform = createSubstitutionTransform(substitutionFunc);
|
||||
const transforms = [substitutionTransform];
|
||||
if (readFile) {
|
||||
const includeTransform = createIncludeTransform(
|
||||
envFunc,
|
||||
substitutionFunc,
|
||||
readFile,
|
||||
substitutionTransform,
|
||||
);
|
||||
|
||||
@@ -19,7 +19,7 @@ import { extname, dirname, resolve as resolvePath } from 'path';
|
||||
import { JsonObject, JsonValue } from '@backstage/types';
|
||||
import { isObject } from './utils';
|
||||
import { TransformFunc, ReadFileFunc } from './types';
|
||||
import { EnvFunc } from '../types';
|
||||
import { SubstitutionFunc } from '../types';
|
||||
|
||||
// Parsers for each type of included file
|
||||
const includeFileParser: {
|
||||
@@ -34,7 +34,7 @@ const includeFileParser: {
|
||||
* Transforms a include description into the actual included value.
|
||||
*/
|
||||
export function createIncludeTransform(
|
||||
env: EnvFunc,
|
||||
env: SubstitutionFunc,
|
||||
readFile: ReadFileFunc,
|
||||
substitute: TransformFunc,
|
||||
): TransformFunc {
|
||||
|
||||
@@ -16,14 +16,16 @@
|
||||
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import { TransformFunc } from './types';
|
||||
import { EnvFunc } from '../types';
|
||||
import { SubstitutionFunc } from '../types';
|
||||
|
||||
/**
|
||||
* A environment variable substitution transform that transforms e.g. 'token ${MY_TOKEN}'
|
||||
* to 'token abc' if MY_TOKEN is 'abc'. If any of the substituted variables are undefined,
|
||||
* the entire expression ends up undefined.
|
||||
*/
|
||||
export function createSubstitutionTransform(env: EnvFunc): TransformFunc {
|
||||
export function createSubstitutionTransform(
|
||||
env: SubstitutionFunc,
|
||||
): TransformFunc {
|
||||
return async (input: JsonValue) => {
|
||||
if (typeof input !== 'string') {
|
||||
return { applied: false };
|
||||
|
||||
@@ -40,4 +40,4 @@ export interface ConfigSource {
|
||||
readConfigData(options?: ReadConfigDataOptions): AsyncConfigSourceIterator;
|
||||
}
|
||||
|
||||
export type EnvFunc = (name: string) => Promise<string | undefined>;
|
||||
export type SubstitutionFunc = (name: string) => Promise<string | undefined>;
|
||||
|
||||
Reference in New Issue
Block a user