Merge pull request #2994 from spotify/rugvip/conflagrate

config-loader: switch to using --config options to load in config
This commit is contained in:
Patrik Oldsberg
2020-10-22 18:01:21 +02:00
committed by GitHub
38 changed files with 271 additions and 370 deletions
+21
View File
@@ -0,0 +1,21 @@
---
'@backstage/backend-common': minor
'@backstage/cli': minor
'@backstage/config-loader': minor
'example-backend': patch
'@backstage/create-app': patch
---
**BREAKING CHANGE**
The existing loading of additional config files like `app-config.development.yaml` using APP_ENV or NODE_ENV has been removed.
Instead, the CLI and backend process now accept one or more `--config` flags to load config files.
Without passing any flags, `app-config.yaml` and, if it exists, `app-config.local.yaml` will be loaded.
If passing any `--config <path>` flags, only those files will be loaded, **NOT** the default `app-config.yaml` one.
The old behaviour of for example `APP_ENV=development` can be replicated using the following flags:
```bash
--config ../../app-config.yaml --config ../../app-config.development.yaml
```
+10
View File
@@ -6,6 +6,16 @@ If you encounter issues while upgrading to a newer version, don't hesitate to re
## Next Release
### @backstage/cli
- Configuration files are no longer selected through `APP_ENV` or `NODE_ENV`. The default configuration files are `app-config.yaml` and, fix it exists, `app-config.local.yaml` in the repo root. To load a different set of files, use `--config <path>` arguments.
### @backstage/backend-common
- Configuration files are no longer selected through `APP_ENV` or `NODE_ENV`. The default configuration files are `app-config.yaml` and, fix it exists, `app-config.local.yaml` in the repo root. To load a different set of files, use `--config <path>` arguments.
## v0.1.1-alpha.25
> Collect changes for the next release below
### @backstage/cli
-13
View File
@@ -1,13 +0,0 @@
app:
baseUrl: http://localhost:3000
backend:
baseUrl: http://localhost:7000
listen:
port: 7000
cors:
origin: http://localhost:3000
methods: [GET, POST, PUT, DELETE]
credentials: true
csp:
connect-src: ["'self'", 'http:', 'https:']
+6 -2
View File
@@ -1,6 +1,6 @@
app:
title: Backstage Example App
baseUrl: http://localhost:7000
baseUrl: http://localhost:3000
googleAnalyticsTrackingId: # UA-000000-0
backend:
@@ -10,8 +10,12 @@ backend:
database:
client: sqlite3
connection: ':memory:'
cors:
origin: http://localhost:3000
methods: [GET, POST, PUT, DELETE]
credentials: true
csp:
connect-src: ["'self'", 'https:']
connect-src: ["'self'", 'http:', 'https:']
# See README.md in the proxy-backend plugin for information on the configuration format
proxy:
@@ -28,6 +28,9 @@ spec:
image: example-backend:latest
imagePullPolicy: Never
command: [node, packages/backend]
args: [--config, app-config.yaml, --config, k8s-config.yaml]
env:
# We set this to development to make the backend start with incomplete configuration. In a production
# deployment you will want to make sure that you have a full configuration, and remove any plugins that
@@ -35,10 +38,6 @@ spec:
- name: NODE_ENV
value: development
# This makes us load in `app-config.production.yaml` if there is one.
- name: APP_ENV
value: production
# This makes it possible for the app to reach the backend when serving through `kubectl proxy`
# If you expose the service using for example an ingress controller, you should
# switch this out or remove it.
@@ -54,8 +53,8 @@ spec:
volumeMounts:
- name: config-volume
mountPath: /usr/src/app/app-config.local.yaml
subPath: app-config.local.yaml
mountPath: /usr/src/app/k8s-config.yaml
subPath: k8s-config.yaml
resources:
limits:
@@ -77,7 +76,7 @@ spec:
name: backstage-config
items:
- key: app-config
path: app-config.local.yaml
path: k8s-config.yaml
---
apiVersion: v1
kind: ConfigMap
+5 -5
View File
@@ -15,11 +15,11 @@ allowing for customization.
## Supplying Configuration
Configuration is stored in `app-config.yaml` files, with support for suffixes
such as `app-config.production.yaml` to override values for specific
environments. The configuration files themselves contain plain YAML, but with
support for loading in secrets from various sources using for example `$env` and
`$file` keys.
Configuration is stored in YAML files where the defaults are `app-config.yaml`
and `app-config.local.yaml` for local overrides. Other sets of files can by
loaded by passing `--config <path>` flags. The configuration files themselves
contain plain YAML, but with support for loading in secrets from various sources
using for example `$env` and `$file` keys.
It is also possible to supply configuration through environment variables, for
example `APP_CONFIG_app_baseUrl=https://staging.example.com`. However these
+22 -14
View File
@@ -55,20 +55,28 @@ picked up by the serve tasks of `@backstage/cli` for local development, and are
injected by the entrypoint of the nginx container serving the frontend in a
production build.
## File Resolution
## Configuration Files
It is possible to have multiple configuration files, both to support different
environments, but also to define configuration that is local to specific
packages.
packages. The configuration files to load are selected using a `--config <path>`
flag, and it is possible to load any number of files. Paths are relative to the
working directory of the executed process, for example `package/backend`. This
means that to select a config file in the repo root when running the backend,
you would use `--config ../../my-config.yaml`.
All `app-config.yaml` files inside the monorepo root and package root are
considered, as are files with additional `local` and environment affixes such as
`development`, for example `app-config.local.yaml`,
`app-config.production.yaml`, and `app-config.development.local.yaml`. Which
environment config files are loaded is determined by the `APP_ENV` environment
variable, or `NODE_ENV` if it is not set. Local configuration files are always
loaded, but are meant for local development overrides and should typically be
`.gitignore`'d.
If no `config` flags are specified, the default behavior is to load
`app-config.yaml` and, if it exists, `app-config.local.yaml` from the repo root.
In the provided project setup, `app-config.local.yaml` is `.gitignore`'d, making
it a good place to add config overrides and secrets for local development.
Note that if any config flags are provided, the default `app-config.yaml` files
are NOT loaded. To include them you need to explicitly include them with a flag,
for example:
```
yarn start --config ../../app-config.yaml --config ../../app-config.staging.yaml
```
All loaded configuration files are merged together using the following rules:
@@ -84,10 +92,10 @@ order:
- Configuration from the `APP_CONFIG_` environment variables has the highest
priority, followed by files.
- Files inside package directories have higher priority than those in the root
directory.
- Files with environment affixes have higher priority than ones without.
- Files with the `local` affix have higher priority than ones without.
- Files loaded with config flags are ordered by priority, where the last flag
has the highest priority.
- If no config flags are provided, `app-config.local.yaml` has higher priority
than `app-config.yaml`.
## Secrets
-3
View File
@@ -228,9 +228,6 @@ future.
- [`app-config.yaml`](https://github.com/spotify/backstage/tree/master/app-config.yaml) -
Configuration for the app, both frontend and backend
- [`app-config.development.yaml`](https://github.com/spotify/backstage/tree/master/app-config.development.yaml) -
Used for overriding configuration when developing locally.
- [`catalog-info.yaml`](https://github.com/spotify/backstage/tree/master/catalog-info.yaml) -
Description of Backstage in the Backstage Entity format.
+2
View File
@@ -46,6 +46,7 @@
"knex": "^0.21.1",
"lodash": "^4.17.15",
"logform": "^2.1.1",
"minimist": "^1.2.5",
"morgan": "^1.10.0",
"prom-client": "^12.0.0",
"selfsigned": "^1.10.7",
@@ -64,6 +65,7 @@
"@backstage/cli": "^0.1.1-alpha.25",
"@types/compression": "^1.7.0",
"@types/http-errors": "^1.6.3",
"@types/minimist": "^1.2.0",
"@types/morgan": "^1.9.0",
"@types/stoppable": "^1.1.0",
"@types/supertest": "^2.0.8",
+10 -2
View File
@@ -14,24 +14,32 @@
* limitations under the License.
*/
import { resolve as resolvePath } from 'path';
import parseArgs from 'minimist';
import { Logger } from 'winston';
import { findPaths } from '@backstage/cli-common';
import { Config, ConfigReader } from '@backstage/config';
import { loadConfig } from '@backstage/config-loader';
import { Logger } from 'winston';
type Options = {
logger: Logger;
// process.argv or any other overrides
argv: string[];
};
/**
* Load configuration for a Backend
*/
export async function loadBackendConfig(options: Options): Promise<Config> {
const args = parseArgs(options.argv);
const configOpts: string[] = [args.config ?? []].flat();
/* eslint-disable-next-line no-restricted-syntax */
const paths = findPaths(__dirname);
const configs = await loadConfig({
env: process.env.APP_ENV ?? process.env.NODE_ENV ?? 'development',
rootPaths: [paths.targetRoot, paths.targetDir],
configRoot: paths.targetRoot,
configPaths: configOpts.map(opt => resolvePath(opt)),
shouldReadSecrets: true,
});
+4 -1
View File
@@ -64,7 +64,10 @@ function makeCreateEnv(config: Config) {
}
async function main() {
const config = await loadBackendConfig({ logger: getRootLogger() });
const config = await loadBackendConfig({
argv: process.argv,
logger: getRootLogger(),
});
const createEnv = makeCreateEnv(config);
const healthcheckEnv = useHotMemoize(module, () => createEnv('healthcheck'));
+2 -14
View File
@@ -15,27 +15,15 @@
*/
import { Command } from 'commander';
import { loadConfig } from '@backstage/config-loader';
import { ConfigReader } from '@backstage/config';
import { paths } from '../../lib/paths';
import { buildBundle } from '../../lib/bundler';
import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel';
import { loadCliConfig } from '../../lib/config';
export default async (cmd: Command) => {
const appConfigs = await loadConfig({
env: process.env.APP_ENV ?? process.env.NODE_ENV ?? 'production',
rootPaths: [paths.targetRoot, paths.targetDir],
});
console.log(
`Loaded config from ${appConfigs.map(c => c.context).join(', ')}`,
);
await buildBundle({
entry: 'src/index',
parallel: parseParallel(process.env[PARALLEL_ENV_VAR]),
statsJsonEnabled: cmd.stats,
config: ConfigReader.fromConfigs(appConfigs),
appConfigs,
...(await loadCliConfig(cmd.config)),
});
};
+2 -14
View File
@@ -15,26 +15,14 @@
*/
import { Command } from 'commander';
import { loadConfig } from '@backstage/config-loader';
import { ConfigReader } from '@backstage/config';
import { paths } from '../../lib/paths';
import { serveBundle } from '../../lib/bundler';
import { loadCliConfig } from '../../lib/config';
export default async (cmd: Command) => {
const appConfigs = await loadConfig({
env: process.env.APP_ENV ?? process.env.NODE_ENV ?? 'development',
rootPaths: [paths.targetRoot, paths.targetDir],
});
console.log(
`Loaded config from ${appConfigs.map(c => c.context).join(', ')}`,
);
const waitForExit = await serveBundle({
entry: 'src/index',
checksEnabled: cmd.check,
config: ConfigReader.fromConfigs(appConfigs),
appConfigs,
...(await loadCliConfig(cmd.config)),
});
await waitForExit();
-14
View File
@@ -14,28 +14,14 @@
* limitations under the License.
*/
import { ConfigReader } from '@backstage/config';
import { loadConfig } from '@backstage/config-loader';
import { Command } from 'commander';
import { paths } from '../../lib/paths';
import { serveBackend } from '../../lib/bundler/backend';
export default async (cmd: Command) => {
const appConfigs = await loadConfig({
env: process.env.APP_ENV ?? process.env.NODE_ENV ?? 'development',
rootPaths: [paths.targetRoot, paths.targetDir],
});
console.log(
`Loaded config from ${appConfigs.map(c => c.context).join(', ')}`,
);
const waitForExit = await serveBackend({
entry: 'src/index',
checksEnabled: cmd.check,
inspectEnabled: cmd.inspect,
config: ConfigReader.fromConfigs(appConfigs),
appConfigs,
});
await waitForExit();
+3 -14
View File
@@ -15,24 +15,13 @@
*/
import { Command } from 'commander';
import { loadConfig } from '@backstage/config-loader';
import { ConfigReader } from '@backstage/config';
import { paths } from '../../lib/paths';
import { stringify as stringifyYaml } from 'yaml';
import { loadCliConfig } from '../../lib/config';
export default async (cmd: Command) => {
const appConfigs = await loadConfig({
env:
cmd.env ?? process.env.APP_ENV ?? process.env.NODE_ENV ?? 'development',
shouldReadSecrets: cmd.withSecrets ?? false,
rootPaths: [paths.targetRoot, paths.targetDir],
});
const { config } = await loadCliConfig(cmd.config, cmd.withSecrets ?? false);
console.log(
`Loaded config from ${appConfigs.map(c => c.context).join(', ')}`,
);
const flatConfig = ConfigReader.fromConfigs(appConfigs).get();
const flatConfig = config.get();
if (cmd.format === 'json') {
process.stdout.write(`${JSON.stringify(flatConfig, null, 2)}\n`);
+14 -4
View File
@@ -18,16 +18,25 @@ import { CommanderStatic } from 'commander';
import { exitWithError } from '../lib/errors';
export function registerCommands(program: CommanderStatic) {
const configOption = [
'--config <path>',
'Config files to load instead of app-config.yaml',
(opt: string, opts: string[]) => [...opts, opt],
Array<string>(),
] as const;
program
.command('app:build')
.description('Build an app for a production release')
.option('--stats', 'Write bundle stats to output directory')
.option(...configOption)
.action(lazy(() => import('./app/build').then(m => m.default)));
program
.command('app:serve')
.description('Serve an app for local development')
.option('--check', 'Enable type checking and linting')
.option(...configOption)
.action(lazy(() => import('./app/serve').then(m => m.default)));
program
@@ -50,6 +59,8 @@ export function registerCommands(program: CommanderStatic) {
.description('Start local development server with HMR for the backend')
.option('--check', 'Enable type checking and linting')
.option('--inspect', 'Enable debugger')
// We don't actually use the config in the CLI, just pass them on to the NodeJS process
.option(...configOption)
.action(lazy(() => import('./backend/dev').then(m => m.default)));
program
@@ -89,12 +100,14 @@ export function registerCommands(program: CommanderStatic) {
.command('plugin:serve')
.description('Serves the dev/ folder of a plugin')
.option('--check', 'Enable type checking and linting')
.option(...configOption)
.action(lazy(() => import('./plugin/serve').then(m => m.default)));
program
.command('plugin:export')
.description('Exports the dev/ folder of a plugin')
.option('--stats', 'Write bundle stats to output directory')
.option(...configOption)
.action(lazy(() => import('./plugin/export').then(m => m.default)));
program
@@ -131,14 +144,11 @@ export function registerCommands(program: CommanderStatic) {
program
.command('config:print')
.option('--with-secrets', 'Include secrets in the printed configuration')
.option(
'--env <env>',
'The environment to print configuration for [APP_ENV or NODE_ENV or development]',
)
.option(
'--format <format>',
'Format to print the configuration in, either json or yaml [yaml]',
)
.option(...configOption)
.description('Print the app configuration for the current package')
.action(lazy(() => import('./config/print').then(m => m.default)));
+2 -14
View File
@@ -15,25 +15,13 @@
*/
import { Command } from 'commander';
import { loadConfig } from '@backstage/config-loader';
import { ConfigReader } from '@backstage/config';
import { paths } from '../../lib/paths';
import { buildBundle } from '../../lib/bundler';
import { loadCliConfig } from '../../lib/config';
export default async (cmd: Command) => {
const appConfigs = await loadConfig({
env: process.env.APP_ENV ?? process.env.NODE_ENV ?? 'production',
rootPaths: [paths.targetRoot, paths.targetDir],
});
console.log(
`Loaded config from ${appConfigs.map(c => c.context).join(', ')}`,
);
await buildBundle({
entry: 'dev/index',
statsJsonEnabled: cmd.stats,
config: ConfigReader.fromConfigs(appConfigs),
appConfigs,
...(await loadCliConfig(cmd.config)),
});
};
+2 -14
View File
@@ -15,26 +15,14 @@
*/
import { Command } from 'commander';
import { loadConfig } from '@backstage/config-loader';
import { ConfigReader } from '@backstage/config';
import { paths } from '../../lib/paths';
import { serveBundle } from '../../lib/bundler';
import { loadCliConfig } from '../../lib/config';
export default async (cmd: Command) => {
const appConfigs = await loadConfig({
env: process.env.APP_ENV ?? process.env.NODE_ENV ?? 'development',
rootPaths: [paths.targetRoot, paths.targetDir],
});
console.log(
`Loaded config from ${appConfigs.map(c => c.context).join(', ')}`,
);
const waitForExit = await serveBundle({
entry: 'dev/index',
checksEnabled: cmd.check,
config: ConfigReader.fromConfigs(appConfigs),
appConfigs,
...(await loadCliConfig(cmd.config)),
});
await waitForExit();
+2 -6
View File
@@ -17,13 +17,9 @@
import webpack from 'webpack';
import { createBackendConfig } from './config';
import { resolveBundlingPaths } from './paths';
import { ServeOptions } from './types';
import { BackendServeOptions } from './types';
export async function serveBackend(
options: ServeOptions & {
inspectEnabled: boolean;
},
) {
export async function serveBackend(options: BackendServeOptions) {
const paths = resolveBundlingPaths(options);
const config = await createBackendConfig(paths, {
...options,
+12 -4
View File
@@ -27,10 +27,6 @@ export type BundlingOptions = {
parallel?: ParallelOption;
};
export type BackendBundlingOptions = Omit<BundlingOptions, 'baseUrl'> & {
inspectEnabled: boolean;
};
export type ServeOptions = BundlingPathsOptions & {
checksEnabled: boolean;
config: Config;
@@ -43,3 +39,15 @@ export type BuildOptions = BundlingPathsOptions & {
config: Config;
appConfigs: AppConfig[];
};
export type BackendBundlingOptions = {
checksEnabled: boolean;
isDev: boolean;
parallel?: ParallelOption;
inspectEnabled: boolean;
};
export type BackendServeOptions = BundlingPathsOptions & {
checksEnabled: boolean;
inspectEnabled: boolean;
};
+42
View File
@@ -0,0 +1,42 @@
/*
* 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.
*/
import { loadConfig } from '@backstage/config-loader';
import { ConfigReader } from '@backstage/config';
import { paths } from './paths';
export async function loadCliConfig(
configArgs: string[],
shouldReadSecrets: boolean = false,
) {
const configPaths = configArgs.map(arg => paths.resolveTarget(arg));
const appConfigs = await loadConfig({
shouldReadSecrets,
env: process.env.APP_ENV ?? process.env.NODE_ENV ?? 'production',
configRoot: paths.targetRoot,
configPaths,
});
console.log(
`Loaded config from ${appConfigs.map(c => c.context).join(', ')}`,
);
return {
appConfigs,
config: ConfigReader.fromConfigs(appConfigs),
};
}
-1
View File
@@ -14,7 +14,6 @@
* limitations under the License.
*/
export { resolveStaticConfig } from './resolver';
export { readConfigFile } from './reader';
export { readEnvConfig } from './env';
export { readSecret } from './secrets';
@@ -1,119 +0,0 @@
/*
* 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.
*/
import mockFs from 'mock-fs';
import { resolveStaticConfig } from './resolver';
function normalizePaths(paths: string[]) {
return paths.map(p =>
p
.replace(/^[a-z]:/i, '')
.split('\\')
.join('/'),
);
}
describe('resolveStaticConfig', () => {
afterEach(() => {
mockFs.restore();
});
it('should resolve no files for empty roots', async () => {
mockFs({});
const resolved = await resolveStaticConfig({
env: 'development',
rootPaths: [],
});
expect(normalizePaths(resolved)).toEqual([]);
});
it('should resolve a single app-config', async () => {
mockFs({ '/repo/app-config.yaml': '' });
const resolved = await resolveStaticConfig({
env: 'development',
rootPaths: ['/repo'],
});
expect(normalizePaths(resolved)).toEqual(['/repo/app-config.yaml']);
});
it('should resolve a app-configs in different directories', async () => {
mockFs({
'/repo/app-config.yaml': '',
'/repo/packages/a/app-config.yaml': '',
});
const resolved = await resolveStaticConfig({
env: 'development',
rootPaths: [
'/repo',
'/other-repo',
'/repo/packages/a',
'/repo/packages/b',
],
});
expect(normalizePaths(resolved)).toEqual([
'/repo/app-config.yaml',
'/repo/packages/a/app-config.yaml',
]);
});
it('should resolve env and local configs', async () => {
mockFs({
'/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': '',
});
const resolved = await resolveStaticConfig({
env: 'development',
rootPaths: ['/repo', '/repo/packages/a'],
});
expect(normalizePaths(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',
]);
});
it('resolves suffixed configs in the correct order', async () => {
mockFs({
'/repo/app-config.yaml': '',
'/repo/app-config.local.yaml': '',
'/repo/app-config.production.yaml': '',
'/repo/app-config.production.local.yaml': '',
});
const resolved = await resolveStaticConfig({
env: 'production',
rootPaths: ['/repo'],
});
expect(normalizePaths(resolved)).toEqual([
'/repo/app-config.yaml',
'/repo/app-config.local.yaml',
'/repo/app-config.production.yaml',
'/repo/app-config.production.local.yaml',
]);
});
});
@@ -1,59 +0,0 @@
/*
* 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.
*/
import { resolve as resolvePath } from 'path';
import { pathExists } from 'fs-extra';
type ResolveOptions = {
// 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
* APP_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[]> {
const filePaths = [
`app-config.yaml`,
`app-config.local.yaml`,
`app-config.${options.env}.yaml`,
`app-config.${options.env}.local.yaml`,
];
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;
}
+34 -4
View File
@@ -38,10 +38,31 @@ describe('loadConfig', () => {
mockFs.restore();
});
it('load config from default path', async () => {
await expect(
loadConfig({
configRoot: '/root',
configPaths: [],
env: 'production',
shouldReadSecrets: false,
}),
).resolves.toEqual([
{
context: 'app-config.yaml',
data: {
app: {
title: 'Example App',
},
},
},
]);
});
it('loads config without secrets', async () => {
await expect(
loadConfig({
rootPaths: ['/root'],
configRoot: '/root',
configPaths: ['/root/app-config.yaml'],
env: 'production',
shouldReadSecrets: false,
}),
@@ -60,7 +81,8 @@ describe('loadConfig', () => {
it('loads config with secrets', async () => {
await expect(
loadConfig({
rootPaths: ['/root'],
configRoot: '/root',
configPaths: ['/root/app-config.yaml'],
env: 'production',
shouldReadSecrets: true,
}),
@@ -80,7 +102,11 @@ describe('loadConfig', () => {
it('loads development config without secrets', async () => {
await expect(
loadConfig({
rootPaths: ['/root'],
configRoot: '/root',
configPaths: [
'/root/app-config.yaml',
'/root/app-config.development.yaml',
],
env: 'development',
shouldReadSecrets: false,
}),
@@ -105,7 +131,11 @@ describe('loadConfig', () => {
it('loads development config with secrets', async () => {
await expect(
loadConfig({
rootPaths: ['/root'],
configRoot: '/root',
configPaths: [
'/root/app-config.yaml',
'/root/app-config.development.yaml',
],
env: 'development',
shouldReadSecrets: true,
}),
+33 -11
View File
@@ -15,20 +15,18 @@
*/
import fs from 'fs-extra';
import { resolve as resolvePath, dirname } from 'path';
import { resolve as resolvePath, dirname, isAbsolute } from 'path';
import { AppConfig, JsonObject } from '@backstage/config';
import {
resolveStaticConfig,
readConfigFile,
readEnvConfig,
readSecret,
} from './lib';
import { readConfigFile, readEnvConfig, readSecret } from './lib';
export type LoadConfigOptions = {
// Root paths to search for config files. Config from earlier paths has lower priority.
rootPaths: string[];
// The root directory of the config loading context. Used to find default configs.
configRoot: string;
// The environment that we're loading config for, e.g. 'development', 'production'.
// Absolute paths to load config files from. Configs from earlier paths have lower priority.
configPaths: string[];
// TODO(Rugvip): This will be removed in the future, but for now we use it to warn about possible mistakes.
env: string;
// Whether to read secrets or omit them, defaults to false.
@@ -77,13 +75,37 @@ export async function loadConfig(
options: LoadConfigOptions,
): Promise<AppConfig[]> {
const configs = [];
const { configRoot } = options;
const configPaths = options.configPaths.slice();
const configPaths = await resolveStaticConfig(options);
// If no paths are provided, we default to reading
// `app-config.yaml` and, if it exists, `app-config.local.yaml`
if (configPaths.length === 0) {
configPaths.push(resolvePath(configRoot, 'app-config.yaml'));
const localConfig = resolvePath(configRoot, 'app-config.local.yaml');
if (await fs.pathExists(localConfig)) {
configPaths.push(localConfig);
}
const envFile = `app-config.${options.env}.yaml`;
if (await fs.pathExists(resolvePath(configRoot, envFile))) {
console.error(
`Env config file '${envFile}' is not loaded as APP_ENV and NODE_ENV-based config loading has been removed`,
);
console.error(
`To load the config file, use --config <path>, listing every config file that you want to load`,
);
}
}
try {
const secretPaths = new Set<string>();
for (const configPath of configPaths) {
if (!isAbsolute(configPath)) {
throw new Error(`Config load path is not absolute: '${configPath}'`);
}
const config = await readConfigFile(
configPath,
new Context({
@@ -1,13 +0,0 @@
app:
baseUrl: http://localhost:3000
backend:
baseUrl: http://localhost:7000
listen:
port: 7000
cors:
origin: http://localhost:3000
methods: [GET, POST, PUT, DELETE]
credentials: true
csp:
connect-src: ["'self'", 'http:', 'https:']
@@ -1,6 +1,6 @@
app:
title: Scaffolded Backstage App
baseUrl: http://localhost:7000
baseUrl: http://localhost:3000
organization:
name: My Company
@@ -10,7 +10,11 @@ backend:
listen:
port: 7000
csp:
connect-src: ["'self'", 'https:']
connect-src: ["'self'", 'http:', 'https:']
cors:
origin: http://localhost:3000
methods: [GET, POST, PUT, DELETE]
credentials: true
{{#if dbTypeSqlite}}
database:
client: sqlite3
@@ -42,7 +42,10 @@ function makeCreateEnv(config: Config) {
}
async function main() {
const config = await loadBackendConfig({logger: getRootLogger()});
const config = await loadBackendConfig({
argv: process.argv,
logger: getRootLogger(),
});
const createEnv = makeCreateEnv(config);
const catalogEnv = useHotMemoize(module, () => createEnv('catalog'));
@@ -52,16 +55,16 @@ async function main() {
const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs'));
const apiRouter = Router();
apiRouter.use('/catalog', await catalog(catalogEnv))
apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv))
apiRouter.use('/auth', await auth(authEnv))
apiRouter.use('/techdocs', await techdocs(techdocsEnv))
apiRouter.use('/proxy', await proxy(proxyEnv))
apiRouter.use('/catalog', await catalog(catalogEnv));
apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv));
apiRouter.use('/auth', await auth(authEnv));
apiRouter.use('/techdocs', await techdocs(techdocsEnv));
apiRouter.use('/proxy', await proxy(proxyEnv));
apiRouter.use(notFoundHandler());
const service = createServiceBuilder(module)
.loadConfig(config)
.addRouter('/api', apiRouter)
.addRouter('/api', apiRouter);
await service.start().catch(err => {
console.log(err);
@@ -33,7 +33,7 @@ export async function startStandaloneServer(
options: ServerOptions,
): Promise<Server> {
const logger = options.logger.child({ service: 'auth-backend' });
const config = await loadBackendConfig({ logger });
const config = await loadBackendConfig({ logger, argv: process.argv });
const discovery = SingleHostDiscovery.fromConfig(config);
const database = useHotMemoize(module, () => {
@@ -36,7 +36,7 @@ export async function startStandaloneServer(
options: ServerOptions,
): Promise<Server> {
const logger = options.logger.child({ service: 'catalog-backend' });
const config = await loadBackendConfig({ logger });
const config = await loadBackendConfig({ logger, argv: process.argv });
const reader = UrlReaders.default({ logger, config });
const db = useHotMemoize(module, () =>
DatabaseManager.createInMemoryDatabaseConnection(),
@@ -23,7 +23,7 @@ This can be used to run the kubernetes plugin locally against a mock service.
6. Register existing component in Backstage
- https://github.com/mclarke47/dice-roller/blob/master/catalog-info.yaml
Update `app-config.development.yaml` as follows.
Add or update `app-config.local.yaml` with the following:
```yaml
kubernetes:
@@ -45,4 +45,4 @@ Mac copy to clipboard:
kubectl get secret $(kubectl get sa dice-roller -o=json | jq -r .secrets[0].name) -o=json | jq -r '.data["token"]' | base64 --decode | pbcopy
```
Paste into `app-config.development.yaml` `kubernetes.clusters[0].serviceAccountToken`
Paste into `app-config.local.yaml` `kubernetes.clusters[0].serviceAccountToken`
@@ -41,7 +41,7 @@ const mockCreateProxyMiddleware = createProxyMiddleware as jest.MockedFunction<
describe('createRouter', () => {
it('works', async () => {
const logger = winston.createLogger();
const config = await loadBackendConfig({ logger });
const config = await loadBackendConfig({ logger, argv: [] });
const discovery = SingleHostDiscovery.fromConfig(config);
const router = await createRouter({
config,
@@ -36,7 +36,7 @@ export async function startStandaloneServer(
logger.debug('Creating application...');
const config = await loadBackendConfig({ logger });
const config = await loadBackendConfig({ logger, argv: process.argv });
const discovery = SingleHostDiscovery.fromConfig(config);
const router = await createRouter({
config,
@@ -32,7 +32,7 @@ export async function startStandaloneServer(
options: ServerOptions,
): Promise<Server> {
const logger = options.logger.child({ service: 'rollbar-backend' });
const config = await loadBackendConfig({ logger });
const config = await loadBackendConfig({ logger, argv: process.argv });
logger.debug('Creating application...');
@@ -237,7 +237,10 @@ export const getDefaultBranch = async (
repositoryUrl: string,
): Promise<string> => {
// TODO(Rugvip): Config should not be loaded here, pass it in instead
const config = await loadBackendConfig({ logger: getRootLogger() });
const config = await loadBackendConfig({
logger: getRootLogger(),
argv: process.argv,
});
const type = getGitRepoType(repositoryUrl);
try {
+5 -1
View File
@@ -93,7 +93,11 @@ export function getAzureHostToken(
export const getTokenForGitRepo = async (
repositoryUrl: string,
): Promise<string | undefined> => {
const config = await loadBackendConfig({ logger: getRootLogger() });
// TODO(Rugvip): Config should not be loaded here, pass it in instead
const config = await loadBackendConfig({
logger: getRootLogger(),
argv: process.argv,
});
const host = getGitHost(repositoryUrl);
const type = getGitRepoType(repositoryUrl);
+5
View File
@@ -5351,6 +5351,11 @@
resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
"@types/minimist@^1.2.0":
version "1.2.0"
resolved "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6"
integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=
"@types/minipass@*":
version "2.2.0"
resolved "https://registry.npmjs.org/@types/minipass/-/minipass-2.2.0.tgz#51ad404e8eb1fa961f75ec61205796807b6f9651"