config-loader: refactor source types

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-03-31 11:35:51 +02:00
parent ef1137943b
commit 160ff1a532
9 changed files with 70 additions and 48 deletions
+1 -1
View File
@@ -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(
+1 -1
View File
@@ -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 };
+1 -1
View File
@@ -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>;