Merge pull request #5700 from minnsoe/add-support-for-plugin-specific-db
Add backwards compatible database manager which allows per plugin config
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
'@backstage/create-app': patch
|
||||
'@backstage/backend-test-utils': patch
|
||||
---
|
||||
|
||||
Deprecates `SingleConnectionDatabaseManager` and provides an API compatible database
|
||||
connection manager, `DatabaseManager`, which allows developers to configure database
|
||||
connections on a per plugin basis.
|
||||
|
||||
The `backend.database` config path allows you to set `prefix` to use an
|
||||
alternate prefix for automatically generated database names, the default is
|
||||
`backstage_plugin_`. Use `backend.database.plugin.<pluginId>` to set plugin
|
||||
specific database connection configuration, e.g.
|
||||
|
||||
```yaml
|
||||
backend:
|
||||
database:
|
||||
client: 'pg',
|
||||
prefix: 'custom_prefix_'
|
||||
connection:
|
||||
host: 'localhost'
|
||||
user: 'foo'
|
||||
password: 'bar'
|
||||
plugin:
|
||||
catalog:
|
||||
connection:
|
||||
database: 'database_name_overriden'
|
||||
scaffolder:
|
||||
client: 'sqlite3'
|
||||
connection: ':memory:'
|
||||
```
|
||||
|
||||
Migrate existing backstage installations by swapping out the database manager in the
|
||||
`packages/backend/src/index.ts` file as shown below:
|
||||
|
||||
```diff
|
||||
import {
|
||||
- SingleConnectionDatabaseManager,
|
||||
+ DatabaseManager,
|
||||
} from '@backstage/backend-common';
|
||||
|
||||
// ...
|
||||
|
||||
function makeCreateEnv(config: Config) {
|
||||
// ...
|
||||
- const databaseManager = SingleConnectionDatabaseManager.fromConfig(config);
|
||||
+ const databaseManager = DatabaseManager.fromConfig(config);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,187 @@
|
||||
---
|
||||
id: configuring-plugin-databases
|
||||
title: Configuring Plugin Databases
|
||||
# prettier-ignore
|
||||
description: Guide on how to configure Backstage databases.
|
||||
---
|
||||
|
||||
This guide covers a variety of production persistence use cases which are
|
||||
supported out of the box by Backstage. The database manager allows the developer
|
||||
to set the client and database connection details on a per plugin basis in
|
||||
addition to the base client and connection configuration. This means that you
|
||||
can use a SQLite 3 in-memory database for a specific plugin whilst using
|
||||
PostgreSQL for everything else and so on.
|
||||
|
||||
By default, Backstage uses automatically created databases for each plugin whose
|
||||
names follow the `backstage_plugin_<pluginId>` pattern, e.g.
|
||||
`backstage_plugin_auth`. You can configure a different database name prefix for
|
||||
use cases where you have multiple deployments running on a shared database
|
||||
instance or cluster.
|
||||
|
||||
With infrastructure defined as code or data (Terraform, AWS CloudFormation,
|
||||
etc.), you may have database credentials which lack permissions to create new
|
||||
databases or you do not have control over the database names. In these
|
||||
instances, you can set the database name and connection information on a per
|
||||
plugin basis as mentioned earlier.
|
||||
|
||||
Backstage supports all of these use cases with the `DatabaseManager` provided by
|
||||
`@backstage/backend-common`. We will now cover how to use and configure
|
||||
Backstage's databases.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Dependencies
|
||||
|
||||
Please ensure the appropriate database drivers are installed in your `backend`
|
||||
package. If you intend to use both `postgres` and `sqlite3`, you can install
|
||||
both of them.
|
||||
|
||||
```shell
|
||||
cd packages/backend
|
||||
|
||||
# install pg if you need postgres
|
||||
yarn add pg
|
||||
|
||||
# install sqlite3 if you intend to set it as the client
|
||||
yarn add sqlite3
|
||||
```
|
||||
|
||||
From an operational perspective, you only need to install drivers for clients
|
||||
that are actively used.
|
||||
|
||||
### Database Manager
|
||||
|
||||
Existing Backstage instances should be updated to use `DatabaseManager` from
|
||||
`@backstage/backend-common` in your `packages/backend/src/index.ts` file, the
|
||||
`SingleConnectionDatabaseManager` has been deprecated. Import the manager and
|
||||
update the references as shown below if this is not the case:
|
||||
|
||||
```diff
|
||||
import {
|
||||
- SingleConnectionDatabaseManager,
|
||||
+ DatabaseManager,
|
||||
} from '@backstage/backend-common';
|
||||
|
||||
// ...
|
||||
|
||||
function makeCreateEnv(config: Config) {
|
||||
// ...
|
||||
- const databaseManager = SingleConnectionDatabaseManager.fromConfig(config);
|
||||
+ const databaseManager = DatabaseManager.fromConfig(config);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
You should set the base database client and connection information in your
|
||||
`app-config.yaml` (or equivalent) file. The base client and configuration is
|
||||
used as the default which is extended for each plugin with the same or unset
|
||||
client type. If a client type is specified for a specific plugin which does not
|
||||
match the base client, the configuration set for the plugin will be used as is
|
||||
without extending the base configuration.
|
||||
|
||||
Client type and configuration for plugins need to be defined under
|
||||
**`backend.database.plugin.<pluginId>`**. As an example, `catalog` is the
|
||||
`pluginId` for the catalog plugin and any configuration defined under that block
|
||||
is specific to that plugin. We will now explore more detailed example
|
||||
configurations below.
|
||||
|
||||
### Minimal In-Memory Configuration
|
||||
|
||||
In the example below, we are using `sqlite3` in-memory databases for all
|
||||
plugins. You may want to use this configuration for testing or other non-durable
|
||||
use cases.
|
||||
|
||||
```yaml
|
||||
backend:
|
||||
database:
|
||||
client: sqlite3
|
||||
connection: ':memory:'
|
||||
```
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
The example below uses PostgreSQL (`pg`) as the database client for all plugins.
|
||||
The `auth` plugin uses a user defined database name instead of the automatically
|
||||
generated one which would have been `backstage_plugin_auth`.
|
||||
|
||||
```yaml
|
||||
backend:
|
||||
database:
|
||||
client: pg
|
||||
connection:
|
||||
host: some.example-pg-instance.tld
|
||||
user: postgres
|
||||
password: password
|
||||
port: 5432
|
||||
plugin:
|
||||
auth:
|
||||
connection:
|
||||
database: pg_auth_set_by_user
|
||||
```
|
||||
|
||||
### Custom Database Name Prefix
|
||||
|
||||
The configuration below uses `example_prefix_` as the database name prefix
|
||||
instead of `backstage_plugin_`. Plugins such as `auth` and `catalog` will use
|
||||
databases named `example_prefix_auth` and `example_prefix_catalog` respectively.
|
||||
|
||||
```yaml
|
||||
backend:
|
||||
database:
|
||||
client: pg
|
||||
connection:
|
||||
host: some.example-pg-instance.tld
|
||||
user: postgres
|
||||
password: password
|
||||
port: 5432
|
||||
prefix: 'example_prefix_'
|
||||
```
|
||||
|
||||
### Connection Configuration Per Plugin
|
||||
|
||||
Both `auth` and `catalog` use connection configuration with different
|
||||
credentials and database names. This type of configuration can be useful for
|
||||
environments with infrastructure as code or data which may provide randomly
|
||||
generated credentials and/or database names.
|
||||
|
||||
```yaml
|
||||
backend:
|
||||
database:
|
||||
client: pg
|
||||
connection: 'postgresql://some.example-pg-instance.tld:5432'
|
||||
plugin:
|
||||
auth:
|
||||
connection: 'postgresql://fort:knox@some.example-pg-instance.tld:5432/unwitting_fox_jumps'
|
||||
catalog:
|
||||
connection: 'postgresql://bank:reserve@some.example-pg-instance.tld:5432/shuffle_ransack_playback'
|
||||
```
|
||||
|
||||
### PostgreSQL and SQLite 3
|
||||
|
||||
The example below uses PostgreSQL (`pg`) as the database client for all plugins
|
||||
except the `auth` plugin which uses `sqlite3`. As the `auth` plugin's client
|
||||
type is different from the base client type, the connection configuration for
|
||||
`auth` is used verbatim without extending the base configuration for PostgreSQL.
|
||||
|
||||
```yaml
|
||||
backend:
|
||||
database:
|
||||
client: pg
|
||||
connection: 'postgresql://foo:bar@some.example-pg-instance.tld:5432'
|
||||
plugin:
|
||||
auth:
|
||||
client: sqlite3
|
||||
connection: ':memory:'
|
||||
```
|
||||
|
||||
## Check Your Databases
|
||||
|
||||
The `DatabaseManager` will attempt to create the databases if they do not exist.
|
||||
If you have set credentials per plugin because the credentials in the base
|
||||
configuration do not have permissions to create databases, you must ensure they
|
||||
exist before starting the service. The service will not be able to create them,
|
||||
it can only use them.
|
||||
|
||||
Good luck!
|
||||
@@ -100,6 +100,12 @@ export function createServiceBuilder(_module: NodeModule): ServiceBuilderImpl;
|
||||
// @public (undocumented)
|
||||
export function createStatusCheckRouter(options: StatusCheckRouterOptions): Promise<express.Router>;
|
||||
|
||||
// @public (undocumented)
|
||||
export class DatabaseManager {
|
||||
forPlugin(pluginId: string): PluginDatabaseManager;
|
||||
static fromConfig(config: Config): DatabaseManager;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export class DockerContainerRunner implements ContainerRunner {
|
||||
constructor({ dockerClient }: {
|
||||
@@ -326,11 +332,8 @@ export type ServiceBuilder = {
|
||||
// @public (undocumented)
|
||||
export function setRootLogger(newLogger: winston.Logger): void;
|
||||
|
||||
// @public
|
||||
export class SingleConnectionDatabaseManager {
|
||||
forPlugin(pluginId: string): PluginDatabaseManager;
|
||||
static fromConfig(config: Config): SingleConnectionDatabaseManager;
|
||||
}
|
||||
// @public @deprecated
|
||||
export const SingleConnectionDatabaseManager: typeof DatabaseManager;
|
||||
|
||||
// @public
|
||||
export class SingleHostDiscovery implements PluginEndpointDiscovery {
|
||||
|
||||
Vendored
+20
-10
@@ -53,20 +53,30 @@ export interface Config {
|
||||
};
|
||||
};
|
||||
|
||||
/** Database connection configuration, select database type using the `client` field */
|
||||
database:
|
||||
| {
|
||||
client: 'sqlite3';
|
||||
connection: ':memory:' | string | { filename: string };
|
||||
}
|
||||
| {
|
||||
client: 'pg';
|
||||
/** Database connection configuration, select base database type using the `client` field */
|
||||
database: {
|
||||
/** Default database client to use */
|
||||
client: 'sqlite3' | 'pg';
|
||||
/**
|
||||
* Base database connection string or Knex object
|
||||
* @secret
|
||||
*/
|
||||
connection: string | object;
|
||||
/** Database name prefix override */
|
||||
prefix?: string;
|
||||
/** Plugin specific database configuration and client override */
|
||||
plugin?: {
|
||||
[pluginId: string]: {
|
||||
/** Database client override */
|
||||
client?: 'sqlite3' | 'pg';
|
||||
/**
|
||||
* PostgreSQL connection string or knex configuration object.
|
||||
* Database connection string or Knex object override
|
||||
* @secret
|
||||
*/
|
||||
connection: string | object;
|
||||
connection?: string | object;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
/** Cache connection configuration, select cache type using the `store` field */
|
||||
cache?:
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
/*
|
||||
* Copyright 2021 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 { ConfigReader } from '@backstage/config';
|
||||
import { omit } from 'lodash';
|
||||
import { createDatabaseClient, ensureDatabaseExists } from './connection';
|
||||
import { DatabaseManager } from './DatabaseManager';
|
||||
|
||||
jest.mock('./connection', () => ({
|
||||
...jest.requireActual('./connection'),
|
||||
createDatabaseClient: jest.fn(),
|
||||
ensureDatabaseExists: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('DatabaseManager', () => {
|
||||
// This is similar to the ts-jest `mocked` helper.
|
||||
const mocked = (f: Function) => f as jest.Mock;
|
||||
|
||||
afterEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('DatabaseManager.fromConfig', () => {
|
||||
it('accesses the backend.database key', () => {
|
||||
const config = new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
client: 'pg',
|
||||
connection: {
|
||||
host: 'localhost',
|
||||
user: 'foo',
|
||||
password: 'bar',
|
||||
database: 'foodb',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const getConfigSpy = jest.spyOn(config, 'getConfig');
|
||||
DatabaseManager.fromConfig(config);
|
||||
|
||||
expect(getConfigSpy).toHaveBeenCalledWith('backend.database');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DatabaseManager.forPlugin', () => {
|
||||
const config = {
|
||||
backend: {
|
||||
database: {
|
||||
client: 'pg',
|
||||
prefix: 'test_prefix_',
|
||||
connection: {
|
||||
host: 'localhost',
|
||||
user: 'foo',
|
||||
password: 'bar',
|
||||
database: 'foodb',
|
||||
},
|
||||
plugin: {
|
||||
testdbname: {
|
||||
connection: {
|
||||
database: 'database_name_overriden',
|
||||
},
|
||||
},
|
||||
differentclient: {
|
||||
client: 'sqlite3',
|
||||
connection: {
|
||||
filename: 'plugin_with_different_client',
|
||||
},
|
||||
},
|
||||
differentclientconnstring: {
|
||||
client: 'sqlite3',
|
||||
connection: ':memory:',
|
||||
},
|
||||
stringoverride: {
|
||||
connection: 'postgresql://testuser:testpass@acme:5432/userdbname',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const manager = DatabaseManager.fromConfig(new ConfigReader(config));
|
||||
|
||||
it('connects to a plugin database using default config', async () => {
|
||||
const pluginId = 'pluginwithoutconfig';
|
||||
|
||||
await manager.forPlugin(pluginId).getClient();
|
||||
expect(mocked(createDatabaseClient)).toHaveBeenCalledTimes(1);
|
||||
|
||||
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
|
||||
const [baseConfig, overrides] = mockCalls[0];
|
||||
|
||||
// default config should be passed through to underlying connector
|
||||
expect(baseConfig.get()).toMatchObject({
|
||||
client: 'pg',
|
||||
connection: omit(config.backend.database.connection, ['database']),
|
||||
});
|
||||
|
||||
// override using database name generated from pluginId and prefix
|
||||
expect(overrides).toMatchObject({
|
||||
connection: {
|
||||
database: `${config.backend.database.prefix}${pluginId}`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('provides a plugin db which uses components from top level connection string', async () => {
|
||||
const testManager = DatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
client: 'pg',
|
||||
connection: 'postgresql://foo:bar@acme:5432/foodb',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await testManager.forPlugin('pluginwithoutconfig').getClient();
|
||||
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
|
||||
const [baseConfig, overrides] = mockCalls[0];
|
||||
|
||||
// parsed connection string **without** db name should be passed through
|
||||
expect(baseConfig.get()).toMatchObject({
|
||||
connection: {
|
||||
host: 'acme',
|
||||
user: 'foo',
|
||||
password: 'bar',
|
||||
port: '5432',
|
||||
},
|
||||
});
|
||||
|
||||
// we expect a pg database name override with ${prefix} followed by pluginId
|
||||
expect(overrides).toHaveProperty(
|
||||
'connection.database',
|
||||
expect.stringContaining('pluginwithoutconfig'),
|
||||
);
|
||||
});
|
||||
|
||||
it('uses top level sqlite database filename if plugin config is not present', async () => {
|
||||
const testManager = DatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
client: 'sqlite3',
|
||||
connection: 'some-file-path',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await testManager.forPlugin('pluginwithoutconfig').getClient();
|
||||
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
|
||||
const [_, overrides] = mockCalls[0];
|
||||
|
||||
expect(overrides).toHaveProperty(
|
||||
'connection.filename',
|
||||
expect.stringContaining('some-file-path'),
|
||||
);
|
||||
});
|
||||
|
||||
it('provides an inmemory sqlite database if top level is also inmemory and plugin config is not present', async () => {
|
||||
const testManager = DatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
client: 'sqlite3',
|
||||
connection: ':memory:',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await testManager.forPlugin('pluginwithoutconfig').getClient();
|
||||
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
|
||||
const [_, overrides] = mockCalls[0];
|
||||
|
||||
expect(overrides).toHaveProperty(
|
||||
'connection.filename',
|
||||
expect.stringContaining(':memory:'),
|
||||
);
|
||||
});
|
||||
|
||||
it('connects to a plugin database using a specific database name', async () => {
|
||||
// testdbname.connection.database is set in config
|
||||
await manager.forPlugin('testdbname').getClient();
|
||||
|
||||
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
|
||||
const [_baseConfig, overrides] = mockCalls[0];
|
||||
|
||||
// simple case where only database name is overriden
|
||||
expect(overrides).toMatchObject({
|
||||
connection: {
|
||||
database: 'database_name_overriden',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('ensure plugin specific database is created', async () => {
|
||||
const pluginId = 'testdbname';
|
||||
// testdbname.connection.database is set in config
|
||||
await manager.forPlugin(pluginId).getClient();
|
||||
|
||||
const mockCalls = mocked(ensureDatabaseExists).mock.calls.splice(-1);
|
||||
const [_, dbname] = mockCalls[0];
|
||||
|
||||
expect(dbname).toEqual(
|
||||
config.backend.database.plugin[pluginId].connection.database,
|
||||
);
|
||||
});
|
||||
|
||||
it('provides different plugins with their own databases', async () => {
|
||||
await manager.forPlugin('plugin1').getClient();
|
||||
await manager.forPlugin('plugin2').getClient();
|
||||
|
||||
expect(mocked(createDatabaseClient)).toHaveBeenCalledTimes(2);
|
||||
|
||||
const mockCalls = mocked(createDatabaseClient).mock.calls;
|
||||
const [plugin1CallArgs, plugin2CallArgs] = mockCalls;
|
||||
|
||||
// database name overrides should be different
|
||||
expect(plugin1CallArgs[1].connection.database).not.toEqual(
|
||||
plugin2CallArgs[1].connection.database,
|
||||
);
|
||||
});
|
||||
|
||||
it('uses plugin connection as base if default client is different from plugin client', async () => {
|
||||
const pluginId = 'differentclient';
|
||||
await manager.forPlugin(pluginId).getClient();
|
||||
|
||||
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
|
||||
const [baseConfig, _overrides] = mockCalls[0];
|
||||
|
||||
// plugin connection should be used as base config, client is different
|
||||
expect(baseConfig.get()).toMatchObject({
|
||||
client: 'sqlite3',
|
||||
connection: config.backend.database.plugin[pluginId].connection,
|
||||
});
|
||||
});
|
||||
|
||||
it('provides database client specific base and override when client set under plugin', async () => {
|
||||
const pluginId = 'differentclient';
|
||||
await manager.forPlugin(pluginId).getClient();
|
||||
|
||||
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
|
||||
const [baseConfig, overrides] = mockCalls[0];
|
||||
|
||||
// plugin client should be sqlite3
|
||||
expect(baseConfig.get().client).toEqual('sqlite3');
|
||||
|
||||
// sqlite3 uses 'filename' instead of 'database'
|
||||
expect(overrides).toHaveProperty('connection.filename');
|
||||
});
|
||||
|
||||
it('provides database client specific base from plugin connection string when client set under plugin', async () => {
|
||||
const pluginId = 'differentclientconnstring';
|
||||
await manager.forPlugin(pluginId).getClient();
|
||||
|
||||
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
|
||||
const [baseConfig, overrides] = mockCalls[0];
|
||||
|
||||
expect(baseConfig.get().client).toEqual('sqlite3');
|
||||
|
||||
expect(overrides).toHaveProperty('connection.filename', ':memory:');
|
||||
});
|
||||
|
||||
it('generates a database name override when prefix is not explicitly set', async () => {
|
||||
const testManager = DatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
client: 'pg',
|
||||
connection: {
|
||||
host: 'localhost',
|
||||
user: 'foo',
|
||||
password: 'bar',
|
||||
database: 'foodb',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await testManager.forPlugin('testplugin').getClient();
|
||||
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
|
||||
const [_baseConfig, overrides] = mockCalls[0];
|
||||
|
||||
expect(overrides).toHaveProperty(
|
||||
'connection.database',
|
||||
expect.stringContaining('backstage_plugin_'),
|
||||
);
|
||||
});
|
||||
|
||||
it('uses values from plugin connection string if top level client should be used', async () => {
|
||||
const pluginId = 'stringoverride';
|
||||
await manager.forPlugin(pluginId).getClient();
|
||||
|
||||
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
|
||||
const [baseConfig, overrides] = mockCalls[0];
|
||||
|
||||
// plugin client should be pg
|
||||
expect(baseConfig.get().client).toEqual('pg');
|
||||
|
||||
expect(overrides).toHaveProperty(
|
||||
'connection.database',
|
||||
expect.stringContaining('userdbname'),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* Copyright 2021 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 { Knex } from 'knex';
|
||||
import { omit } from 'lodash';
|
||||
import { Config, ConfigReader, JsonObject } from '@backstage/config';
|
||||
import {
|
||||
createDatabaseClient,
|
||||
ensureDatabaseExists,
|
||||
createNameOverride,
|
||||
normalizeConnection,
|
||||
} from './connection';
|
||||
import { PluginDatabaseManager } from './types';
|
||||
|
||||
/**
|
||||
* Provides a config lookup path for a plugin's config block.
|
||||
*/
|
||||
function pluginPath(pluginId: string): string {
|
||||
return `plugin.${pluginId}`;
|
||||
}
|
||||
|
||||
export class DatabaseManager {
|
||||
/**
|
||||
* Creates a DatabaseManager from `backend.database` config.
|
||||
*
|
||||
* The database manager allows the user to set connection and client settings on a per pluginId
|
||||
* basis by defining a database config block under `plugin.<pluginId>` in addition to top level
|
||||
* defaults. Optionally, a user may set `prefix` which is used to prefix generated database
|
||||
* names if config is not provided.
|
||||
*
|
||||
* @param config The loaded application configuration.
|
||||
*/
|
||||
static fromConfig(config: Config): DatabaseManager {
|
||||
const databaseConfig = config.getConfig('backend.database');
|
||||
|
||||
return new DatabaseManager(
|
||||
databaseConfig,
|
||||
databaseConfig.getOptionalString('prefix'),
|
||||
);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
private readonly config: Config,
|
||||
private readonly prefix: string = 'backstage_plugin_',
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Generates a PluginDatabaseManager for consumption by plugins.
|
||||
*
|
||||
* @param pluginId The plugin that the database manager should be created for. Plugin names
|
||||
* should be unique as they are used to look up database config overrides under
|
||||
* `backend.database.plugin`.
|
||||
*/
|
||||
forPlugin(pluginId: string): PluginDatabaseManager {
|
||||
const _this = this;
|
||||
|
||||
return {
|
||||
getClient(): Promise<Knex> {
|
||||
return _this.getDatabase(pluginId);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the canonical database name for a given plugin.
|
||||
*
|
||||
* This method provides the effective database name which is determined using global
|
||||
* and plugin specific database config. If no explicit database name is configured,
|
||||
* this method will provide a generated name which is the pluginId prefixed with
|
||||
* 'backstage_plugin_'.
|
||||
*
|
||||
* @param pluginId Lookup the database name for given plugin
|
||||
* @returns String representing the plugin's database name
|
||||
*/
|
||||
private getDatabaseName(pluginId: string): string {
|
||||
const connection = this.getConnectionConfig(pluginId);
|
||||
|
||||
if (this.getClientType(pluginId).client === 'sqlite3') {
|
||||
// sqlite database name should fallback to ':memory:' as a special case
|
||||
return (
|
||||
(connection as Knex.Sqlite3ConnectionConfig)?.filename ?? ':memory:'
|
||||
);
|
||||
}
|
||||
// all other supported databases should fallback to an auto-prefixed name
|
||||
return (
|
||||
(connection as Knex.ConnectionConfig)?.database ??
|
||||
`${this.prefix}${pluginId}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the client type which should be used for a given plugin.
|
||||
*
|
||||
* The client type is determined by plugin specific config if present. Otherwise the base
|
||||
* client is used as the fallback.
|
||||
*
|
||||
* @param pluginId Plugin to get the client type for
|
||||
* @returns Object with client type returned as `client` and boolean representing whether
|
||||
* or not the client was overridden as `overridden`
|
||||
*/
|
||||
private getClientType(
|
||||
pluginId: string,
|
||||
): {
|
||||
client: string;
|
||||
overridden: boolean;
|
||||
} {
|
||||
const pluginClient = this.config.getOptionalString(
|
||||
`${pluginPath(pluginId)}.client`,
|
||||
);
|
||||
|
||||
const baseClient = this.config.getString('client');
|
||||
const client = pluginClient ?? baseClient;
|
||||
return {
|
||||
client,
|
||||
overridden: client !== baseClient,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a Knex connection plugin config by combining base and plugin config.
|
||||
*
|
||||
* This method provides a baseConfig for a plugin database connector. If the client type
|
||||
* has not been overridden, the global connection config will be included with plugin
|
||||
* specific config as the base. Values from the plugin connection take precedence over the
|
||||
* base. Base database name is omitted for all supported databases excluding SQLite.
|
||||
*/
|
||||
private getConnectionConfig(
|
||||
pluginId: string,
|
||||
): Partial<Knex.StaticConnectionConfig> {
|
||||
const { client, overridden } = this.getClientType(pluginId);
|
||||
|
||||
let baseConnection = normalizeConnection(
|
||||
this.config.get('connection'),
|
||||
this.config.getString('client'),
|
||||
);
|
||||
// As databases cannot be shared, the `database` property from the base connection
|
||||
// is omitted. SQLite3's `filename` property is an exception as this is used as a
|
||||
// directory elsewhere so we preserve `filename`.
|
||||
baseConnection = omit(baseConnection, 'database');
|
||||
|
||||
// get and normalize optional plugin specific database connection
|
||||
const connection = normalizeConnection(
|
||||
this.config.getOptional(`${pluginPath(pluginId)}.connection`),
|
||||
client,
|
||||
);
|
||||
|
||||
return {
|
||||
// include base connection if client type has not been overriden
|
||||
...(overridden ? {} : baseConnection),
|
||||
...connection,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a Knex database config for a given plugin.
|
||||
*
|
||||
* This method provides a Knex configuration object along with the plugin's client type.
|
||||
*
|
||||
* @param pluginId The plugin that the database config should correspond with
|
||||
*/
|
||||
private getConfigForPlugin(pluginId: string): Knex.Config {
|
||||
const { client } = this.getClientType(pluginId);
|
||||
|
||||
return {
|
||||
client,
|
||||
connection: this.getConnectionConfig(pluginId),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a partial Knex.Config database name override for a given plugin.
|
||||
*
|
||||
* @param pluginId Target plugin to get database name override
|
||||
* @returns Partial Knex.Config with database name override
|
||||
*/
|
||||
private getDatabaseOverrides(pluginId: string): Knex.Config {
|
||||
return createNameOverride(
|
||||
this.getClientType(pluginId).client,
|
||||
this.getDatabaseName(pluginId),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a scoped Knex client for a plugin as per application config.
|
||||
*
|
||||
* @param pluginId Plugin to get a Knex client for
|
||||
* @returns Promise which resolves to a scoped Knex database client for a plugin
|
||||
*/
|
||||
private async getDatabase(pluginId: string): Promise<Knex> {
|
||||
const pluginConfig = new ConfigReader(
|
||||
this.getConfigForPlugin(pluginId) as JsonObject,
|
||||
);
|
||||
|
||||
const databaseName = this.getDatabaseName(pluginId);
|
||||
try {
|
||||
await ensureDatabaseExists(pluginConfig, databaseName);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to connect to the database to make sure that '${databaseName}' exists, ${error}`,
|
||||
);
|
||||
}
|
||||
|
||||
return createDatabaseClient(
|
||||
pluginConfig,
|
||||
this.getDatabaseOverrides(pluginId),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,10 +15,14 @@
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { createDatabaseClient } from './connection';
|
||||
import { createDatabaseClient, ensureDatabaseExists } from './connection';
|
||||
import { SingleConnectionDatabaseManager } from './SingleConnection';
|
||||
|
||||
jest.mock('./connection');
|
||||
jest.mock('./connection', () => ({
|
||||
...jest.requireActual('./connection'),
|
||||
createDatabaseClient: jest.fn(),
|
||||
ensureDatabaseExists: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('SingleConnectionDatabaseManager', () => {
|
||||
const defaultConfigOptions = {
|
||||
@@ -43,9 +47,8 @@ describe('SingleConnectionDatabaseManager', () => {
|
||||
|
||||
describe('SingleConnectionDatabaseManager.fromConfig', () => {
|
||||
it('accesses the backend.database key', () => {
|
||||
const getConfig = jest.fn();
|
||||
const config = defaultConfig();
|
||||
config.getConfig = getConfig;
|
||||
const getConfig = jest.spyOn(config, 'getConfig');
|
||||
|
||||
SingleConnectionDatabaseManager.fromConfig(config);
|
||||
|
||||
@@ -64,7 +67,6 @@ describe('SingleConnectionDatabaseManager', () => {
|
||||
|
||||
const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1);
|
||||
const callArgs = mockCalls[0];
|
||||
expect(callArgs[0].get()).toEqual(defaultConfigOptions.backend.database);
|
||||
expect(callArgs[1].connection.database).toEqual(
|
||||
`backstage_plugin_${pluginId}`,
|
||||
);
|
||||
@@ -85,5 +87,13 @@ describe('SingleConnectionDatabaseManager', () => {
|
||||
plugin2CallArgs[1].connection.database,
|
||||
);
|
||||
});
|
||||
|
||||
it('ensure plugin database is created', async () => {
|
||||
await manager.forPlugin('test').getClient();
|
||||
const mockCalls = mocked(ensureDatabaseExists).mock.calls.splice(-1);
|
||||
const [_, database] = mockCalls[0];
|
||||
|
||||
expect(database).toEqual('backstage_plugin_test');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,75 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Knex } from 'knex';
|
||||
import { Config } from '@backstage/config';
|
||||
import { createDatabaseClient, ensureDatabaseExists } from './connection';
|
||||
import { PluginDatabaseManager } from './types';
|
||||
import { DatabaseManager } from './DatabaseManager';
|
||||
|
||||
/**
|
||||
* Implements a Database Manager which will automatically create new databases
|
||||
* for plugins when requested. All requested databases are created with the
|
||||
* credentials provided; if the database already exists no attempt to create
|
||||
* the database will be made.
|
||||
*
|
||||
* @deprecated Use `DatabaseManager` from `@backend-common` instead.
|
||||
*/
|
||||
export class SingleConnectionDatabaseManager {
|
||||
/**
|
||||
* Creates a new SingleConnectionDatabaseManager instance by reading from the `backend`
|
||||
* config section, specifically the `.database` key for discovering the management
|
||||
* database configuration.
|
||||
*
|
||||
* @param config The loaded application configuration.
|
||||
*/
|
||||
static fromConfig(config: Config): SingleConnectionDatabaseManager {
|
||||
return new SingleConnectionDatabaseManager(
|
||||
config.getConfig('backend.database'),
|
||||
);
|
||||
}
|
||||
|
||||
private constructor(private readonly config: Config) {}
|
||||
|
||||
/**
|
||||
* Generates a PluginDatabaseManager for consumption by plugins.
|
||||
*
|
||||
* @param pluginId The plugin that the database manager should be created for. Plugin names should be unique.
|
||||
*/
|
||||
forPlugin(pluginId: string): PluginDatabaseManager {
|
||||
const _this = this;
|
||||
|
||||
return {
|
||||
getClient(): Promise<Knex> {
|
||||
return _this.getDatabase(pluginId);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async getDatabase(pluginId: string): Promise<Knex> {
|
||||
const config = this.config;
|
||||
const overrides = SingleConnectionDatabaseManager.getDatabaseOverrides(
|
||||
pluginId,
|
||||
);
|
||||
const overrideConfig = overrides.connection as Knex.ConnectionConfig;
|
||||
await this.ensureDatabase(overrideConfig.database);
|
||||
|
||||
return createDatabaseClient(config, overrides);
|
||||
}
|
||||
|
||||
private static getDatabaseOverrides(pluginId: string): Knex.Config {
|
||||
return {
|
||||
connection: {
|
||||
database: `backstage_plugin_${pluginId}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async ensureDatabase(database: string) {
|
||||
const config = this.config;
|
||||
try {
|
||||
await ensureDatabaseExists(config, database);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to connect to the database to make sure that '${database}' exists, ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
export const SingleConnectionDatabaseManager = DatabaseManager;
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { createDatabaseClient } from './connection';
|
||||
import {
|
||||
createDatabaseClient,
|
||||
createNameOverride,
|
||||
parseConnectionString,
|
||||
} from './connection';
|
||||
|
||||
describe('database connection', () => {
|
||||
describe('createDatabaseClient', () => {
|
||||
@@ -103,4 +107,49 @@ describe('database connection', () => {
|
||||
).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createNameOverride', () => {
|
||||
it('returns Knex config for postgres', () => {
|
||||
expect(createNameOverride('pg', 'testpg')).toHaveProperty(
|
||||
'connection.database',
|
||||
'testpg',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns Knex config for sqlite', () => {
|
||||
expect(createNameOverride('sqlite3', 'testsqlite')).toHaveProperty(
|
||||
'connection.filename',
|
||||
'testsqlite',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns Knex config for mysql', () => {
|
||||
expect(createNameOverride('mysql', 'testmysql')).toHaveProperty(
|
||||
'connection.database',
|
||||
'testmysql',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error for unknown connection', () => {
|
||||
expect(() => createNameOverride('unknown', 'testname')).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseConnectionString', () => {
|
||||
it('returns parsed Knex.StaticConnectionConfig for postgres', () => {
|
||||
expect(
|
||||
parseConnectionString('postgresql://foo:bar@acme:5432/foodb', 'pg'),
|
||||
).toHaveProperty('database', 'foodb');
|
||||
});
|
||||
|
||||
it('returns parsed Knex.StaticConnectionConfig for mysql2', () => {
|
||||
expect(
|
||||
parseConnectionString('mysql://foo:bar@acme:3306/foodb', 'mysql2'),
|
||||
).toHaveProperty('database', 'foodb');
|
||||
});
|
||||
|
||||
it('throws an error if client hint is not provided', () => {
|
||||
expect(() => parseConnectionString('sqlite://')).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,14 +14,28 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { Config, JsonObject } from '@backstage/config';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import knexFactory, { Knex } from 'knex';
|
||||
import { mergeDatabaseConfig } from './config';
|
||||
import { createMysqlDatabaseClient, ensureMysqlDatabaseExists } from './mysql';
|
||||
import { createPgDatabaseClient, ensurePgDatabaseExists } from './postgres';
|
||||
import { createSqliteDatabaseClient } from './sqlite3';
|
||||
import { DatabaseConnector } from './types';
|
||||
|
||||
type DatabaseClient = 'pg' | 'sqlite3' | string;
|
||||
import { mysqlConnector, pgConnector, sqlite3Connector } from './connectors';
|
||||
|
||||
type DatabaseClient = 'pg' | 'sqlite3' | 'mysql' | 'mysql2' | string;
|
||||
|
||||
/**
|
||||
* Mapping of client type to supported database connectors
|
||||
*
|
||||
* Database connectors can be aliased here, for example mysql2 uses
|
||||
* the same connector as mysql.
|
||||
*/
|
||||
const ConnectorMapping: Record<DatabaseClient, DatabaseConnector> = {
|
||||
pg: pgConnector,
|
||||
sqlite3: sqlite3Connector,
|
||||
mysql: mysqlConnector,
|
||||
mysql2: mysqlConnector,
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a knex database connection
|
||||
@@ -35,15 +49,10 @@ export function createDatabaseClient(
|
||||
) {
|
||||
const client: DatabaseClient = dbConfig.getString('client');
|
||||
|
||||
if (client === 'pg') {
|
||||
return createPgDatabaseClient(dbConfig, overrides);
|
||||
} else if (client === 'mysql' || client === 'mysql2') {
|
||||
return createMysqlDatabaseClient(dbConfig, overrides);
|
||||
} else if (client === 'sqlite3') {
|
||||
return createSqliteDatabaseClient(dbConfig, overrides);
|
||||
}
|
||||
|
||||
return knexFactory(mergeDatabaseConfig(dbConfig.get(), overrides));
|
||||
return (
|
||||
ConnectorMapping[client]?.createClient(dbConfig, overrides) ??
|
||||
knexFactory(mergeDatabaseConfig(dbConfig.get(), overrides))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,14 +67,66 @@ export const createDatabase = createDatabaseClient;
|
||||
export async function ensureDatabaseExists(
|
||||
dbConfig: Config,
|
||||
...databases: Array<string>
|
||||
) {
|
||||
): Promise<void> {
|
||||
const client: DatabaseClient = dbConfig.getString('client');
|
||||
|
||||
if (client === 'pg') {
|
||||
return ensurePgDatabaseExists(dbConfig, ...databases);
|
||||
} else if (client === 'mysql' || client === 'mysql2') {
|
||||
return ensureMysqlDatabaseExists(dbConfig, ...databases);
|
||||
return ConnectorMapping[client]?.ensureDatabaseExists?.(
|
||||
dbConfig,
|
||||
...databases,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a Knex.Config object with the provided database name for a given client.
|
||||
*/
|
||||
export function createNameOverride(
|
||||
client: string,
|
||||
name: string,
|
||||
): Partial<Knex.Config> {
|
||||
try {
|
||||
return ConnectorMapping[client].createNameOverride(name);
|
||||
} catch (e) {
|
||||
throw new InputError(
|
||||
`Unable to create database name override for '${client}' connector`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a connection string for a given client and provides a connection config.
|
||||
*/
|
||||
export function parseConnectionString(
|
||||
connectionString: string,
|
||||
client?: string,
|
||||
): Knex.StaticConnectionConfig {
|
||||
if (typeof client === 'undefined' || client === null) {
|
||||
throw new InputError(
|
||||
'Database connection string client type auto-detection is not yet supported.',
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
try {
|
||||
return ConnectorMapping[client].parseConnectionString(connectionString);
|
||||
} catch (e) {
|
||||
throw new InputError(
|
||||
`Unable to parse connection string for '${client}' connector`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a connection config or string into an object which can be passed to Knex.
|
||||
*/
|
||||
export function normalizeConnection(
|
||||
connection: Knex.StaticConnectionConfig | JsonObject | string | undefined,
|
||||
client: string,
|
||||
): Partial<Knex.StaticConnectionConfig> {
|
||||
if (typeof connection === 'undefined' || connection === null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return typeof connection === 'string' || connection instanceof String
|
||||
? parseConnectionString(connection as string, client)
|
||||
: connection;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2021 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 defaultNameOverride from './defaultNameOverride';
|
||||
|
||||
describe('defaultNameOverride()', () => {
|
||||
it('returns a partial knex static connection config with database name', () => {
|
||||
const testDatabaseName = 'testdatabase';
|
||||
expect(defaultNameOverride(testDatabaseName)).toHaveProperty(
|
||||
'connection.database',
|
||||
testDatabaseName,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2021 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 { Knex } from 'knex';
|
||||
|
||||
/**
|
||||
* Provides a partial knex config with database name override.
|
||||
*
|
||||
* Default override for knex database drivers which accept ConnectionConfig
|
||||
* with `connection.database` as the database name field.
|
||||
*
|
||||
* @param name database name to get config override for
|
||||
*/
|
||||
export default function defaultNameOverride(
|
||||
name: string,
|
||||
): Partial<Knex.Config> {
|
||||
return {
|
||||
connection: {
|
||||
database: name,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2021 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.
|
||||
*/
|
||||
export * from './mysql';
|
||||
export * from './postgres';
|
||||
export * from './sqlite3';
|
||||
+18
-3
@@ -14,11 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import knexFactory, { Knex } from 'knex';
|
||||
import yn from 'yn';
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import knexFactory, { Knex } from 'knex';
|
||||
import { mergeDatabaseConfig } from './config';
|
||||
import yn from 'yn';
|
||||
import { mergeDatabaseConfig } from '../config';
|
||||
import { DatabaseConnector } from '../types';
|
||||
import defaultNameOverride from './defaultNameOverride';
|
||||
|
||||
/**
|
||||
* Creates a knex mysql database connection
|
||||
@@ -159,3 +162,15 @@ export async function ensureMysqlDatabaseExists(
|
||||
await admin.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MySQL database connector.
|
||||
*
|
||||
* Exposes database connector functionality via an immutable object.
|
||||
*/
|
||||
export const mysqlConnector: DatabaseConnector = Object.freeze({
|
||||
createClient: createMysqlDatabaseClient,
|
||||
ensureDatabaseExists: ensureMysqlDatabaseExists,
|
||||
createNameOverride: defaultNameOverride,
|
||||
parseConnectionString: parseMysqlConnectionString,
|
||||
});
|
||||
+16
-1
@@ -15,8 +15,11 @@
|
||||
*/
|
||||
|
||||
import knexFactory, { Knex } from 'knex';
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { mergeDatabaseConfig } from './config';
|
||||
import { mergeDatabaseConfig } from '../config';
|
||||
import { DatabaseConnector } from '../types';
|
||||
import defaultNameOverride from './defaultNameOverride';
|
||||
|
||||
/**
|
||||
* Creates a knex postgres database connection
|
||||
@@ -131,3 +134,15 @@ export async function ensurePgDatabaseExists(
|
||||
await admin.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PostgreSQL database connector.
|
||||
*
|
||||
* Exposes database connector functionality via an immutable object.
|
||||
*/
|
||||
export const pgConnector: DatabaseConnector = Object.freeze({
|
||||
createClient: createPgDatabaseClient,
|
||||
ensureDatabaseExists: ensurePgDatabaseExists,
|
||||
createNameOverride: defaultNameOverride,
|
||||
parseConnectionString: parsePgConnectionString,
|
||||
});
|
||||
+38
-5
@@ -13,15 +13,17 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import path from 'path';
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { ensureDirSync } from 'fs-extra';
|
||||
import knexFactory, { Knex } from 'knex';
|
||||
import path from 'path';
|
||||
import { mergeDatabaseConfig } from './config';
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { mergeDatabaseConfig } from '../config';
|
||||
import { DatabaseConnector } from '../types';
|
||||
|
||||
/**
|
||||
* Creates a knex sqlite3 database connection
|
||||
* Creates a knex SQLite3 database connection
|
||||
*
|
||||
* @param dbConfig The database config
|
||||
* @param overrides Additional options to merge with the config
|
||||
@@ -54,7 +56,7 @@ export function createSqliteDatabaseClient(
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a knex sqlite3 connection config
|
||||
* Builds a knex SQLite3 connection config
|
||||
*
|
||||
* @param dbConfig The database config
|
||||
* @param overrides Additional options to merge with the config
|
||||
@@ -99,3 +101,34 @@ export function buildSqliteDatabaseConfig(
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a partial knex SQLite3 config to override database name.
|
||||
*/
|
||||
export function createSqliteNameOverride(name: string): Partial<Knex.Config> {
|
||||
return {
|
||||
connection: parseSqliteConnectionString(name),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a partial knex SQLite3 connection config with database name.
|
||||
*/
|
||||
export function parseSqliteConnectionString(
|
||||
name: string,
|
||||
): Knex.Sqlite3ConnectionConfig {
|
||||
return {
|
||||
filename: name,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* SQLite3 database connector.
|
||||
*
|
||||
* Exposes database connector functionality via an immutable object.
|
||||
*/
|
||||
export const sqlite3Connector: DatabaseConnector = Object.freeze({
|
||||
createClient: createSqliteDatabaseClient,
|
||||
createNameOverride: createSqliteNameOverride,
|
||||
parseConnectionString: parseSqliteConnectionString,
|
||||
});
|
||||
@@ -14,6 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './connection';
|
||||
export * from './types';
|
||||
export * from './SingleConnection';
|
||||
export * from './DatabaseManager';
|
||||
|
||||
/*
|
||||
* Undocumented API surface from connection is being reduced for future deprecation.
|
||||
* Avoid exporting additional symbols.
|
||||
*/
|
||||
export {
|
||||
createDatabaseClient,
|
||||
createDatabase,
|
||||
ensureDatabaseExists,
|
||||
} from './connection';
|
||||
|
||||
export type { PluginDatabaseManager } from './types';
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { Knex } from 'knex';
|
||||
|
||||
/**
|
||||
@@ -28,3 +29,37 @@ export interface PluginDatabaseManager {
|
||||
*/
|
||||
getClient(): Promise<Knex>;
|
||||
}
|
||||
|
||||
/**
|
||||
* DatabaseConnector manages an underlying Knex database driver.
|
||||
*/
|
||||
export interface DatabaseConnector {
|
||||
/**
|
||||
* createClient provides an instance of a knex database connector.
|
||||
*/
|
||||
createClient(dbConfig: Config, overrides?: Partial<Knex.Config>): Knex;
|
||||
/**
|
||||
* createNameOverride provides a partial knex config sufficient to override a
|
||||
* database name.
|
||||
*/
|
||||
createNameOverride(name: string): Partial<Knex.Config>;
|
||||
/**
|
||||
* parseConnectionString produces a knex connection config object representing
|
||||
* a database connection string.
|
||||
*/
|
||||
parseConnectionString(
|
||||
connectionString: string,
|
||||
client?: string,
|
||||
): Knex.StaticConnectionConfig;
|
||||
/**
|
||||
* ensureDatabaseExists performs a side-effect to ensure database names passed in are
|
||||
* present.
|
||||
*
|
||||
* Calling this function on databases which already exist should do nothing.
|
||||
* Missing databases should be created if needed.
|
||||
*/
|
||||
ensureDatabaseExists?(
|
||||
dbConfig: Config,
|
||||
...databases: Array<string>
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ describe('TestDatabases', () => {
|
||||
await input.insert({ x: 'y' }).into('a');
|
||||
|
||||
// Look for the mark
|
||||
const database = 'backstage_plugin_0';
|
||||
const database = 'backstage_plugin_db0';
|
||||
const output = knexFactory({
|
||||
client: 'pg',
|
||||
connection: { host, port, user, password, database },
|
||||
@@ -105,7 +105,7 @@ describe('TestDatabases', () => {
|
||||
await input.insert({ x: 'y' }).into('a');
|
||||
|
||||
// Look for the mark
|
||||
const database = 'backstage_plugin_0';
|
||||
const database = 'backstage_plugin_db0';
|
||||
const output = knexFactory({
|
||||
client: 'pg',
|
||||
connection: { host, port, user, password, database },
|
||||
@@ -139,7 +139,7 @@ describe('TestDatabases', () => {
|
||||
await input.insert({ x: 'y' }).into('a');
|
||||
|
||||
// Look for the mark
|
||||
const database = 'backstage_plugin_0';
|
||||
const database = 'backstage_plugin_db0';
|
||||
const output = knexFactory({
|
||||
client: 'mysql2',
|
||||
connection: { host, port, user, password, database },
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { SingleConnectionDatabaseManager } from '@backstage/backend-common';
|
||||
import { DatabaseManager } from '@backstage/backend-common';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { Knex } from 'knex';
|
||||
import { isDockerDisabledForTests } from '../util/isDockerDisabledForTests';
|
||||
@@ -142,7 +142,7 @@ export class TestDatabases {
|
||||
|
||||
// Ensure that a unique logical database is created in the instance
|
||||
const connection = await instance.databaseManager
|
||||
.forPlugin(String(this.lastDatabaseIndex++))
|
||||
.forPlugin(String(`db${this.lastDatabaseIndex++}`))
|
||||
.getClient();
|
||||
|
||||
instance.connections.push(connection);
|
||||
@@ -157,7 +157,7 @@ export class TestDatabases {
|
||||
if (envVarName) {
|
||||
const connectionString = process.env[envVarName];
|
||||
if (connectionString) {
|
||||
const databaseManager = SingleConnectionDatabaseManager.fromConfig(
|
||||
const databaseManager = DatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
@@ -195,7 +195,7 @@ export class TestDatabases {
|
||||
properties.dockerImageName!,
|
||||
);
|
||||
|
||||
const databaseManager = SingleConnectionDatabaseManager.fromConfig(
|
||||
const databaseManager = DatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
@@ -220,7 +220,7 @@ export class TestDatabases {
|
||||
properties.dockerImageName!,
|
||||
);
|
||||
|
||||
const databaseManager = SingleConnectionDatabaseManager.fromConfig(
|
||||
const databaseManager = DatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
@@ -241,7 +241,7 @@ export class TestDatabases {
|
||||
private async initSqlite(
|
||||
_properties: TestDatabaseProperties,
|
||||
): Promise<Instance> {
|
||||
const databaseManager = SingleConnectionDatabaseManager.fromConfig(
|
||||
const databaseManager = DatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { SingleConnectionDatabaseManager } from '@backstage/backend-common';
|
||||
import { DatabaseManager } from '@backstage/backend-common';
|
||||
import { Knex } from 'knex';
|
||||
|
||||
/**
|
||||
@@ -35,10 +35,9 @@ export type TestDatabaseProperties = {
|
||||
|
||||
export type Instance = {
|
||||
stopContainer?: () => Promise<void>;
|
||||
databaseManager: SingleConnectionDatabaseManager;
|
||||
databaseManager: DatabaseManager;
|
||||
connections: Array<Knex>;
|
||||
};
|
||||
|
||||
export const allDatabases: Record<
|
||||
TestDatabaseId,
|
||||
TestDatabaseProperties
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
getRootLogger,
|
||||
loadBackendConfig,
|
||||
notFoundHandler,
|
||||
SingleConnectionDatabaseManager,
|
||||
DatabaseManager,
|
||||
SingleHostDiscovery,
|
||||
UrlReaders,
|
||||
useHotMemoize,
|
||||
@@ -59,7 +59,7 @@ function makeCreateEnv(config: Config) {
|
||||
|
||||
root.info(`Created UrlReader ${reader}`);
|
||||
|
||||
const databaseManager = SingleConnectionDatabaseManager.fromConfig(config);
|
||||
const databaseManager = DatabaseManager.fromConfig(config);
|
||||
const cacheManager = CacheManager.fromConfig(config);
|
||||
|
||||
return (plugin: string): PluginEnvironment => {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
useHotMemoize,
|
||||
notFoundHandler,
|
||||
CacheManager,
|
||||
SingleConnectionDatabaseManager,
|
||||
DatabaseManager,
|
||||
SingleHostDiscovery,
|
||||
UrlReaders,
|
||||
} from '@backstage/backend-common';
|
||||
@@ -35,8 +35,8 @@ function makeCreateEnv(config: Config) {
|
||||
|
||||
root.info(`Created UrlReader ${reader}`);
|
||||
|
||||
const databaseManager = SingleConnectionDatabaseManager.fromConfig(config);
|
||||
const cacheManager = CacheManager.fromConfig(config);
|
||||
const databaseManager = DatabaseManager.fromConfig(config);
|
||||
|
||||
return (plugin: string): PluginEnvironment => {
|
||||
const logger = root.child({ type: 'plugin', plugin });
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { SingleConnectionDatabaseManager } from '@backstage/backend-common';
|
||||
import { DatabaseManager } from '@backstage/backend-common';
|
||||
import { stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import {
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from './CodeCoverageDatabase';
|
||||
import { JsonCodeCoverage } from './types';
|
||||
|
||||
const db = SingleConnectionDatabaseManager.fromConfig(
|
||||
const db = DatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
|
||||
@@ -20,14 +20,14 @@ import {
|
||||
getVoidLogger,
|
||||
PluginDatabaseManager,
|
||||
PluginEndpointDiscovery,
|
||||
SingleConnectionDatabaseManager,
|
||||
DatabaseManager,
|
||||
UrlReaders,
|
||||
} from '@backstage/backend-common';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { createRouter } from './router';
|
||||
|
||||
function createDatabase(): PluginDatabaseManager {
|
||||
return SingleConnectionDatabaseManager.fromConfig(
|
||||
return DatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
|
||||
@@ -14,17 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
getVoidLogger,
|
||||
SingleConnectionDatabaseManager,
|
||||
} from '@backstage/backend-common';
|
||||
import { getVoidLogger, DatabaseManager } from '@backstage/backend-common';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { DatabaseTaskStore } from './DatabaseTaskStore';
|
||||
import { StorageTaskBroker, TaskAgent } from './StorageTaskBroker';
|
||||
import { TaskSecrets, TaskSpec, DbTaskEventRow } from './types';
|
||||
|
||||
async function createStore(): Promise<DatabaseTaskStore> {
|
||||
const manager = SingleConnectionDatabaseManager.fromConfig(
|
||||
const manager = DatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
|
||||
@@ -14,12 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
getVoidLogger,
|
||||
SingleConnectionDatabaseManager,
|
||||
} from '@backstage/backend-common';
|
||||
import { ConfigReader, JsonObject } from '@backstage/config';
|
||||
import os from 'os';
|
||||
import { getVoidLogger, DatabaseManager } from '@backstage/backend-common';
|
||||
import { ConfigReader, JsonObject } from '@backstage/config';
|
||||
import { createTemplateAction, TemplateActionRegistry } from '../actions';
|
||||
import { RepoSpec } from '../actions/builtin/publish/util';
|
||||
import { DatabaseTaskStore } from './DatabaseTaskStore';
|
||||
@@ -27,7 +24,7 @@ import { StorageTaskBroker } from './StorageTaskBroker';
|
||||
import { TaskWorker } from './TaskWorker';
|
||||
|
||||
async function createStore(): Promise<DatabaseTaskStore> {
|
||||
const manager = SingleConnectionDatabaseManager.fromConfig(
|
||||
const manager = DatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
|
||||
@@ -31,7 +31,7 @@ jest.doMock('fs-extra', () => ({
|
||||
import {
|
||||
getVoidLogger,
|
||||
PluginDatabaseManager,
|
||||
SingleConnectionDatabaseManager,
|
||||
DatabaseManager,
|
||||
UrlReaders,
|
||||
} from '@backstage/backend-common';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
@@ -47,7 +47,7 @@ const createCatalogClient = (templates: any[] = []) =>
|
||||
} as CatalogApi);
|
||||
|
||||
function createDatabase(): PluginDatabaseManager {
|
||||
return SingleConnectionDatabaseManager.fromConfig(
|
||||
return DatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: {
|
||||
|
||||
Reference in New Issue
Block a user