diff --git a/.changeset/five-donkeys-brake.md b/.changeset/five-donkeys-brake.md new file mode 100644 index 0000000000..274e704888 --- /dev/null +++ b/.changeset/five-donkeys-brake.md @@ -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.` 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); + // ... +} +``` diff --git a/docs/tutorials/configuring-plugin-databases.md b/docs/tutorials/configuring-plugin-databases.md new file mode 100644 index 0000000000..27b800b3d2 --- /dev/null +++ b/docs/tutorials/configuring-plugin-databases.md @@ -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_` 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.`**. 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! diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 068220cad3..fcddb8dda5 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -100,6 +100,12 @@ export function createServiceBuilder(_module: NodeModule): ServiceBuilderImpl; // @public (undocumented) export function createStatusCheckRouter(options: StatusCheckRouterOptions): Promise; +// @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 { diff --git a/packages/backend-common/config.d.ts b/packages/backend-common/config.d.ts index 875660a594..b42ce3eca5 100644 --- a/packages/backend-common/config.d.ts +++ b/packages/backend-common/config.d.ts @@ -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?: diff --git a/packages/backend-common/src/database/DatabaseManager.test.ts b/packages/backend-common/src/database/DatabaseManager.test.ts new file mode 100644 index 0000000000..8f3331403d --- /dev/null +++ b/packages/backend-common/src/database/DatabaseManager.test.ts @@ -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'), + ); + }); + }); +}); diff --git a/packages/backend-common/src/database/DatabaseManager.ts b/packages/backend-common/src/database/DatabaseManager.ts new file mode 100644 index 0000000000..9e13f565d3 --- /dev/null +++ b/packages/backend-common/src/database/DatabaseManager.ts @@ -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.` 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 { + 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 { + 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 { + 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), + ); + } +} diff --git a/packages/backend-common/src/database/SingleConnection.test.ts b/packages/backend-common/src/database/SingleConnection.test.ts index 3466b0f79d..852dd33944 100644 --- a/packages/backend-common/src/database/SingleConnection.test.ts +++ b/packages/backend-common/src/database/SingleConnection.test.ts @@ -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'); + }); }); }); diff --git a/packages/backend-common/src/database/SingleConnection.ts b/packages/backend-common/src/database/SingleConnection.ts index de8270455e..5bdd99c0ae 100644 --- a/packages/backend-common/src/database/SingleConnection.ts +++ b/packages/backend-common/src/database/SingleConnection.ts @@ -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 { - return _this.getDatabase(pluginId); - }, - }; - } - - private async getDatabase(pluginId: string): Promise { - 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; diff --git a/packages/backend-common/src/database/connection.test.ts b/packages/backend-common/src/database/connection.test.ts index 6ab163ff7f..fa7ccd480a 100644 --- a/packages/backend-common/src/database/connection.test.ts +++ b/packages/backend-common/src/database/connection.test.ts @@ -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(); + }); + }); }); diff --git a/packages/backend-common/src/database/connection.ts b/packages/backend-common/src/database/connection.ts index fb16915cf4..46f040d9d0 100644 --- a/packages/backend-common/src/database/connection.ts +++ b/packages/backend-common/src/database/connection.ts @@ -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 = { + 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 -) { +): Promise { 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 { + 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 { + if (typeof connection === 'undefined' || connection === null) { + return {}; + } + + return typeof connection === 'string' || connection instanceof String + ? parseConnectionString(connection as string, client) + : connection; } diff --git a/packages/backend-common/src/database/connectors/defaultNameOverride.test.ts b/packages/backend-common/src/database/connectors/defaultNameOverride.test.ts new file mode 100644 index 0000000000..b41736153a --- /dev/null +++ b/packages/backend-common/src/database/connectors/defaultNameOverride.test.ts @@ -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, + ); + }); +}); diff --git a/packages/backend-common/src/database/connectors/defaultNameOverride.ts b/packages/backend-common/src/database/connectors/defaultNameOverride.ts new file mode 100644 index 0000000000..6296010c76 --- /dev/null +++ b/packages/backend-common/src/database/connectors/defaultNameOverride.ts @@ -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 { + return { + connection: { + database: name, + }, + }; +} diff --git a/packages/backend-common/src/database/connectors/index.ts b/packages/backend-common/src/database/connectors/index.ts new file mode 100644 index 0000000000..f314bb5004 --- /dev/null +++ b/packages/backend-common/src/database/connectors/index.ts @@ -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'; diff --git a/packages/backend-common/src/database/mysql.test.ts b/packages/backend-common/src/database/connectors/mysql.test.ts similarity index 100% rename from packages/backend-common/src/database/mysql.test.ts rename to packages/backend-common/src/database/connectors/mysql.test.ts diff --git a/packages/backend-common/src/database/mysql.ts b/packages/backend-common/src/database/connectors/mysql.ts similarity index 89% rename from packages/backend-common/src/database/mysql.ts rename to packages/backend-common/src/database/connectors/mysql.ts index be4632baf6..60f1e09ce0 100644 --- a/packages/backend-common/src/database/mysql.ts +++ b/packages/backend-common/src/database/connectors/mysql.ts @@ -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, +}); diff --git a/packages/backend-common/src/database/postgres.test.ts b/packages/backend-common/src/database/connectors/postgres.test.ts similarity index 100% rename from packages/backend-common/src/database/postgres.test.ts rename to packages/backend-common/src/database/connectors/postgres.test.ts diff --git a/packages/backend-common/src/database/postgres.ts b/packages/backend-common/src/database/connectors/postgres.ts similarity index 87% rename from packages/backend-common/src/database/postgres.ts rename to packages/backend-common/src/database/connectors/postgres.ts index e05eab86e5..011e40579b 100644 --- a/packages/backend-common/src/database/postgres.ts +++ b/packages/backend-common/src/database/connectors/postgres.ts @@ -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, +}); diff --git a/packages/backend-common/src/database/sqlite3.test.ts b/packages/backend-common/src/database/connectors/sqlite3.test.ts similarity index 100% rename from packages/backend-common/src/database/sqlite3.test.ts rename to packages/backend-common/src/database/connectors/sqlite3.test.ts diff --git a/packages/backend-common/src/database/sqlite3.ts b/packages/backend-common/src/database/connectors/sqlite3.ts similarity index 75% rename from packages/backend-common/src/database/sqlite3.ts rename to packages/backend-common/src/database/connectors/sqlite3.ts index d4169e3899..c9e86c80da 100644 --- a/packages/backend-common/src/database/sqlite3.ts +++ b/packages/backend-common/src/database/connectors/sqlite3.ts @@ -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 { + 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, +}); diff --git a/packages/backend-common/src/database/index.ts b/packages/backend-common/src/database/index.ts index c81153aa62..bfb14e7353 100644 --- a/packages/backend-common/src/database/index.ts +++ b/packages/backend-common/src/database/index.ts @@ -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'; diff --git a/packages/backend-common/src/database/types.ts b/packages/backend-common/src/database/types.ts index bf3ceb2786..995f98f27d 100644 --- a/packages/backend-common/src/database/types.ts +++ b/packages/backend-common/src/database/types.ts @@ -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; } + +/** + * 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; + /** + * createNameOverride provides a partial knex config sufficient to override a + * database name. + */ + createNameOverride(name: string): Partial; + /** + * 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 + ): Promise; +} diff --git a/packages/backend-test-utils/src/database/TestDatabases.test.ts b/packages/backend-test-utils/src/database/TestDatabases.test.ts index 7c1e6e1bdd..7a51111b02 100644 --- a/packages/backend-test-utils/src/database/TestDatabases.test.ts +++ b/packages/backend-test-utils/src/database/TestDatabases.test.ts @@ -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 }, diff --git a/packages/backend-test-utils/src/database/TestDatabases.ts b/packages/backend-test-utils/src/database/TestDatabases.ts index ab5846d84d..0ba6bffc0b 100644 --- a/packages/backend-test-utils/src/database/TestDatabases.ts +++ b/packages/backend-test-utils/src/database/TestDatabases.ts @@ -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 { - const databaseManager = SingleConnectionDatabaseManager.fromConfig( + const databaseManager = DatabaseManager.fromConfig( new ConfigReader({ backend: { database: { diff --git a/packages/backend-test-utils/src/database/types.ts b/packages/backend-test-utils/src/database/types.ts index 791cf09b7b..91b5939765 100644 --- a/packages/backend-test-utils/src/database/types.ts +++ b/packages/backend-test-utils/src/database/types.ts @@ -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; - databaseManager: SingleConnectionDatabaseManager; + databaseManager: DatabaseManager; connections: Array; }; - export const allDatabases: Record< TestDatabaseId, TestDatabaseProperties diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index d43c8f0101..67149c8163 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -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 => { diff --git a/packages/create-app/templates/default-app/packages/backend/src/index.ts b/packages/create-app/templates/default-app/packages/backend/src/index.ts index 3d8849fa75..f2b14b23f9 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/index.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/index.ts @@ -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 }); diff --git a/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.test.ts b/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.test.ts index 688eeb4ba2..fec2ea7afa 100644 --- a/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.test.ts +++ b/plugins/code-coverage-backend/src/service/CodeCoverageDatabase.test.ts @@ -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: { diff --git a/plugins/code-coverage-backend/src/service/router.test.ts b/plugins/code-coverage-backend/src/service/router.test.ts index e18cd75c2f..340502d1e6 100644 --- a/plugins/code-coverage-backend/src/service/router.test.ts +++ b/plugins/code-coverage-backend/src/service/router.test.ts @@ -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: { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts index 686dfc052f..5ed997bb25 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts @@ -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 { - const manager = SingleConnectionDatabaseManager.fromConfig( + const manager = DatabaseManager.fromConfig( new ConfigReader({ backend: { database: { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts index c6ad1a8105..f8343e7d3c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts @@ -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 { - const manager = SingleConnectionDatabaseManager.fromConfig( + const manager = DatabaseManager.fromConfig( new ConfigReader({ backend: { database: { diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 9ca2eef9e6..9349e076fd 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -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: {