move over cache and database services

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2024-05-21 12:39:27 +02:00
parent 8ed28ffcdf
commit 02103becc6
41 changed files with 469 additions and 500 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/backend-defaults': minor
'@backstage/backend-common': minor
---
Deprecated and moved over core services to `@backstage/backend-defaults`
+2 -2
View File
@@ -8,7 +8,7 @@
import type { AppConfig } from '@backstage/config';
import { AuthService } from '@backstage/backend-plugin-api';
import { BackendFeature } from '@backstage/backend-plugin-api';
import { CacheClient } from '@backstage/backend-common';
import { CacheService } from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
import { ConfigSchema } from '@backstage/config-loader';
import { CorsOptions } from 'cors';
@@ -66,7 +66,7 @@ export interface Backend {
}
// @public @deprecated (undocumented)
export const cacheServiceFactory: () => ServiceFactory<CacheClient, 'plugin'>;
export const cacheServiceFactory: () => ServiceFactory<CacheService, 'plugin'>;
// @public (undocumented)
export function createConfigSecretEnumerator(options: {
+29 -18
View File
@@ -17,11 +17,12 @@ import { BackendFeature } from '@backstage/backend-plugin-api';
import { BitbucketCloudIntegration } from '@backstage/integration';
import { BitbucketIntegration } from '@backstage/integration';
import { BitbucketServerIntegration } from '@backstage/integration';
import { CacheService as CacheClient } from '@backstage/backend-plugin-api';
import { CacheServiceOptions as CacheClientOptions } from '@backstage/backend-plugin-api';
import { CacheServiceSetOptions as CacheClientSetOptions } from '@backstage/backend-plugin-api';
import { CacheService } from '@backstage/backend-plugin-api';
import { CacheServiceOptions } from '@backstage/backend-plugin-api';
import type { CacheServiceSetOptions } from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
import cors from 'cors';
import { DiscoveryService } from '@backstage/backend-plugin-api';
import Docker from 'dockerode';
import { ErrorRequestHandler } from 'express';
import express from 'express';
@@ -44,7 +45,6 @@ import { LoggerService } from '@backstage/backend-plugin-api';
import { MergeResult } from 'isomorphic-git';
import { PermissionsService } from '@backstage/backend-plugin-api';
import { DatabaseService as PluginDatabaseManager } from '@backstage/backend-plugin-api';
import { DiscoveryService as PluginEndpointDiscovery } from '@backstage/backend-plugin-api';
import { PluginMetadataService } from '@backstage/backend-plugin-api';
import { PushResult } from 'isomorphic-git';
import { Readable } from 'stream';
@@ -193,18 +193,26 @@ export class BitbucketUrlReader implements UrlReader {
toString(): string;
}
export { CacheClient };
// @public @deprecated (undocumented)
export type CacheClient = CacheService;
export { CacheClientOptions };
// @public @deprecated (undocumented)
export type CacheClientOptions = CacheServiceOptions;
export { CacheClientSetOptions };
// @public @deprecated (undocumented)
export type CacheClientSetOptions = CacheServiceSetOptions;
// @public
export class CacheManager {
forPlugin(pluginId: string): PluginCacheManager;
forPlugin(pluginId: string): {
getClient(options?: CacheServiceOptions): CacheService;
};
static fromConfig(
config: Config,
options?: CacheManagerOptions,
options?: {
logger?: LoggerService;
onError?: (err: Error) => void;
},
): CacheManager;
}
@@ -214,10 +222,10 @@ export type CacheManagerOptions = {
onError?: (err: Error) => void;
};
// @public (undocumented)
export function cacheToPluginCacheManager(
cache: CacheClient,
): PluginCacheManager;
// @public
export function cacheToPluginCacheManager(cache: CacheService): {
getClient(options?: CacheServiceOptions): CacheService;
};
// @public @deprecated
export const coloredFormat: winston.Logform.Format;
@@ -575,10 +583,10 @@ export const legacyPlugin: (
default: LegacyCreateRouter<
TransformedEnv<
{
cache: CacheClient;
cache: CacheService;
config: RootConfigService;
database: PluginDatabaseManager;
discovery: PluginEndpointDiscovery;
discovery: DiscoveryService;
logger: LoggerService;
permissions: PermissionsService;
scheduler: SchedulerService;
@@ -588,7 +596,9 @@ export const legacyPlugin: (
},
{
logger: (log: LoggerService) => Logger;
cache: (cache: CacheClient) => PluginCacheManager;
cache: (cache: CacheService) => {
getClient(options?: CacheServiceOptions | undefined): CacheService;
};
}
>
>;
@@ -639,12 +649,13 @@ export function notFoundHandler(): RequestHandler;
// @public (undocumented)
export interface PluginCacheManager {
// (undocumented)
getClient(options?: CacheClientOptions): CacheClient;
getClient(options?: CacheServiceOptions): CacheService;
}
export { PluginDatabaseManager };
export { PluginEndpointDiscovery };
// @public @deprecated (undocumented)
export type PluginEndpointDiscovery = DiscoveryService;
// @public
export interface PullOptions {
-384
View File
@@ -1,384 +0,0 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 Keyv from 'keyv';
import KeyvMemcache from '@keyv/memcache';
import KeyvRedis from '@keyv/redis';
import { DefaultCacheClient } from './CacheClient';
import { CacheManager } from './CacheManager';
jest.createMockFromModule('keyv');
jest.mock('keyv');
jest.createMockFromModule('@keyv/memcache');
jest.mock('@keyv/memcache');
jest.createMockFromModule('@keyv/redis');
jest.mock('@keyv/redis');
jest.mock('./CacheClient', () => {
return {
DefaultCacheClient: jest.fn(),
};
});
const globalDefaultTtl = 1234;
describe('CacheManager', () => {
const defaultConfigOptions = {
backend: {
cache: {
store: 'memory',
defaultTtl: globalDefaultTtl,
},
},
};
const defaultConfig = () => new ConfigReader(defaultConfigOptions);
afterEach(() => jest.resetAllMocks());
describe('CacheManager.fromConfig', () => {
it('accesses the backend.cache key', () => {
const getOptionalString = jest.fn();
const getOptionalBoolean = jest.fn();
const getOptionalNumber = jest.fn();
const config = defaultConfig();
config.getOptionalString = getOptionalString;
config.getOptionalBoolean = getOptionalBoolean;
config.getOptionalNumber = getOptionalNumber;
CacheManager.fromConfig(config);
expect(getOptionalString.mock.calls[0][0]).toEqual('backend.cache.store');
expect(getOptionalString.mock.calls[1][0]).toEqual(
'backend.cache.connection',
);
expect(getOptionalBoolean.mock.calls[0][0]).toEqual(
'backend.cache.useRedisSets',
);
expect(getOptionalNumber.mock.calls[0][0]).toEqual(
'backend.cache.defaultTtl',
);
});
it('does not require the backend.cache key', () => {
const config = new ConfigReader({ backend: {} });
expect(() => {
CacheManager.fromConfig(config);
}).not.toThrow();
});
it('throws on unknown cache store', () => {
const config = new ConfigReader({
backend: { cache: { store: 'notreal' } },
});
expect(() => {
CacheManager.fromConfig(config);
}).toThrow();
});
});
describe('CacheManager.forPlugin', () => {
const manager = CacheManager.fromConfig(defaultConfig());
it('connects to a cache store scoped to the plugin', async () => {
const pluginId = 'test1';
manager.forPlugin(pluginId).getClient();
const client = DefaultCacheClient as jest.Mock;
expect(client).toHaveBeenCalledTimes(1);
});
it('attaches error handler to client', () => {
const pluginId = 'error-test';
manager.forPlugin(pluginId).getClient();
const client = DefaultCacheClient as jest.Mock;
const mockCalls = client.mock.calls.splice(-1);
const realClient = mockCalls[0][0] as Keyv;
expect(realClient.on).toHaveBeenCalledWith('error', expect.any(Function));
});
it('provides different plugins different cache clients', async () => {
const plugin1Id = 'test1';
const plugin2Id = 'test2';
const expectedTtl = 3600;
manager.forPlugin(plugin1Id).getClient({ defaultTtl: expectedTtl });
manager.forPlugin(plugin2Id).getClient({ defaultTtl: expectedTtl });
const client = DefaultCacheClient as jest.Mock;
const cache = Keyv as unknown as jest.Mock;
expect(cache).toHaveBeenCalledTimes(2);
expect(client).toHaveBeenCalledTimes(2);
const plugin1CallArgs = cache.mock.calls[0];
const plugin2CallArgs = cache.mock.calls[1];
expect(plugin1CallArgs[0].namespace).not.toEqual(
plugin2CallArgs[0].namespace,
);
});
});
describe('CacheManager.forPlugin stores', () => {
it('returns memory client when no cache is configured', () => {
const manager = CacheManager.fromConfig(
new ConfigReader({ backend: {} }),
);
const expectedTtl = 3600;
const expectedNamespace = 'test-plugin';
manager
.forPlugin(expectedNamespace)
.getClient({ defaultTtl: expectedTtl });
const cache = Keyv as unknown as jest.Mock;
const mockCalls = cache.mock.calls.splice(-1);
const callArgs = mockCalls[0];
expect(callArgs[0]).toMatchObject({
ttl: expectedTtl,
namespace: expectedNamespace,
});
});
it('returns memory client when explicitly configured', () => {
const manager = CacheManager.fromConfig(defaultConfig());
const expectedTtl = 3600;
const expectedNamespace = 'test-plugin';
manager
.forPlugin(expectedNamespace)
.getClient({ defaultTtl: expectedTtl });
const cache = Keyv as unknown as jest.Mock;
const mockCalls = cache.mock.calls.splice(-1);
const callArgs = mockCalls[0];
expect(callArgs[0]).toMatchObject({
ttl: expectedTtl,
namespace: expectedNamespace,
});
});
it('returns memory client with a global defaultTtl when explicitly configured', () => {
const manager = CacheManager.fromConfig(defaultConfig());
const expectedNamespace = 'test-plugin';
manager.forPlugin(expectedNamespace).getClient();
const cache = Keyv as unknown as jest.Mock;
const mockCalls = cache.mock.calls.splice(-1);
const callArgs = mockCalls[0];
expect(callArgs[0]).toMatchObject({
ttl: globalDefaultTtl,
namespace: expectedNamespace,
});
});
it('shares memory across multiple instances of the memory client', () => {
const manager = CacheManager.fromConfig(defaultConfig());
const plugin = 'test-plugin';
// Instantiate two in-memory clients.
manager.forPlugin(plugin).getClient({ defaultTtl: 10 });
manager.forPlugin(plugin).getClient({ defaultTtl: 10 });
const cache = Keyv as unknown as jest.Mock;
const mockCall2 = cache.mock.calls.splice(-1)[0][0];
const mockCall1 = cache.mock.calls.splice(-1)[0][0];
// Note: .toBe() checks referential identity of object instances.
expect(mockCall1.store).toBe(mockCall2.store);
});
it('returns a memcache client when configured', () => {
const expectedHost = '127.0.0.1:11211';
const manager = CacheManager.fromConfig(
new ConfigReader({
backend: {
cache: {
store: 'memcache',
connection: expectedHost,
},
},
}),
);
const expectedTtl = 3600;
manager.forPlugin('test').getClient({ defaultTtl: expectedTtl });
const cache = Keyv as unknown as jest.Mock;
const mockCacheCalls = cache.mock.calls.splice(-1);
expect(mockCacheCalls[0][0]).toMatchObject({
ttl: expectedTtl,
});
expect(mockCacheCalls[0][0].store).toBeInstanceOf(KeyvMemcache);
const memcache = KeyvMemcache as unknown as jest.Mock;
const mockMemcacheCalls = memcache.mock.calls.splice(-1);
expect(mockMemcacheCalls[0][0]).toEqual(expectedHost);
});
it('returns a memcache client with a global defaultTtl when configured', () => {
const expectedHost = '127.0.0.1:11211';
const manager = CacheManager.fromConfig(
new ConfigReader({
backend: {
cache: {
store: 'memcache',
connection: expectedHost,
defaultTtl: globalDefaultTtl,
},
},
}),
);
manager.forPlugin('test').getClient();
const cache = Keyv as unknown as jest.Mock;
const mockCacheCalls = cache.mock.calls.splice(-1);
expect(mockCacheCalls[0][0]).toMatchObject({
ttl: globalDefaultTtl,
});
expect(mockCacheCalls[0][0].store).toBeInstanceOf(KeyvMemcache);
const memcache = KeyvMemcache as unknown as jest.Mock;
const mockMemcacheCalls = memcache.mock.calls.splice(-1);
expect(mockMemcacheCalls[0][0]).toEqual(expectedHost);
});
it('returns a Redis client when configured', () => {
const redisConnection = 'redis://127.0.0.1:6379';
const manager = CacheManager.fromConfig(
new ConfigReader({
backend: {
cache: {
store: 'redis',
connection: redisConnection,
},
},
}),
);
const expectedTtl = 3600;
manager.forPlugin('test').getClient({ defaultTtl: expectedTtl });
const cache = Keyv as unknown as jest.Mock;
const mockCacheCalls = cache.mock.calls.splice(-1);
expect(mockCacheCalls[0][0]).toMatchObject({
ttl: expectedTtl,
});
expect(mockCacheCalls[0][0].store).toBeInstanceOf(KeyvRedis);
const redis = KeyvRedis as unknown as jest.Mock;
const mockRedisCalls = redis.mock.calls.splice(-1);
expect(mockRedisCalls[0][0]).toEqual(redisConnection);
});
it('returns a Redis client with a global defaultTtl when configured', () => {
const redisConnection = 'redis://127.0.0.1:6379';
const manager = CacheManager.fromConfig(
new ConfigReader({
backend: {
cache: {
store: 'redis',
connection: redisConnection,
defaultTtl: globalDefaultTtl,
},
},
}),
);
manager.forPlugin('test').getClient();
const cache = Keyv as unknown as jest.Mock;
const mockCacheCalls = cache.mock.calls.splice(-1);
expect(mockCacheCalls[0][0]).toMatchObject({
ttl: globalDefaultTtl,
});
expect(mockCacheCalls[0][0].store).toBeInstanceOf(KeyvRedis);
const redis = KeyvRedis as unknown as jest.Mock;
const mockRedisCalls = redis.mock.calls.splice(-1);
expect(mockRedisCalls[0][0]).toEqual(redisConnection);
});
it('returns a Redis client when configured with useRedisSets flag', () => {
const redisConnection = 'redis://127.0.0.1:6379';
const useRedisSets = false;
const manager = CacheManager.fromConfig(
new ConfigReader({
backend: {
cache: {
store: 'redis',
connection: redisConnection,
useRedisSets: useRedisSets,
},
},
}),
);
const expectedTtl = 3600;
manager.forPlugin('test').getClient({ defaultTtl: expectedTtl });
const cache = Keyv as unknown as jest.Mock;
const mockCacheCalls = cache.mock.calls.splice(-1);
expect(mockCacheCalls[0][0]).toMatchObject({
ttl: expectedTtl,
useRedisSets: useRedisSets,
});
expect(mockCacheCalls[0][0].store).toBeInstanceOf(KeyvRedis);
const redis = KeyvRedis as unknown as jest.Mock;
const mockRedisCalls = redis.mock.calls.splice(-1);
expect(mockRedisCalls[0][0]).toEqual(redisConnection);
});
});
describe('connection errors', () => {
it('uses provided logger', () => {
// Set up and inject mock logger.
const mockLogger = { child: jest.fn(), error: jest.fn() };
mockLogger.child.mockImplementation(() => mockLogger as any);
const manager = CacheManager.fromConfig(defaultConfig(), {
logger: mockLogger as any,
});
// Set up a cache client using the configured manager.
manager.forPlugin('error-logger-test').getClient();
// Retrieve the error handler attached to the cache client.
const client = DefaultCacheClient as jest.Mock;
const mockCalls = client.mock.calls.splice(-1);
const realClient = mockCalls[0][0] as Keyv;
const realOnError = realClient.on as jest.Mock;
const realHandler = realOnError.mock.calls.splice(-1)[0][1];
// Invoke the actual error handler.
const expectedError = new Error('some error');
realHandler(expectedError);
expect(mockLogger.error).toHaveBeenCalledWith(
'Failed to create cache client',
expectedError,
);
});
it('calls provided handler', () => {
// Set up and inject mock logger.
const mockHandler = jest.fn();
const manager = CacheManager.fromConfig(defaultConfig(), {
onError: mockHandler,
});
// Set up a cache client using the configured manager.
manager.forPlugin('error-handler-test').getClient();
// Retrieve the error handler attached to the cache client.
const client = DefaultCacheClient as jest.Mock;
const mockCalls = client.mock.calls.splice(-1);
const realClient = mockCalls[0][0] as Keyv;
const realOnError = realClient.on as jest.Mock;
const realHandler = realOnError.mock.calls.splice(-1)[0][1];
// Invoke the actual error handler.
const expectedError = new Error('some error');
realHandler(expectedError);
expect(mockHandler).toHaveBeenCalledWith(expectedError);
});
});
});
@@ -0,0 +1,34 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 {
CacheService,
CacheServiceOptions,
} from '@backstage/backend-plugin-api';
/**
* Compatibility wrapper for going from a new-backend cache service to the
* old-backend plugin cache manager.
*
* @public
*/
export function cacheToPluginCacheManager(cache: CacheService): {
getClient(options?: CacheServiceOptions): CacheService;
} {
return {
getClient: (opts: CacheServiceOptions) => cache.withOptions(opts),
};
}
+3 -8
View File
@@ -14,11 +14,6 @@
* limitations under the License.
*/
export { CacheManager, cacheToPluginCacheManager } from './CacheManager';
export type {
CacheClient,
CacheClientSetOptions,
PluginCacheManager,
CacheManagerOptions,
CacheClientOptions,
} from './types';
export { cacheToPluginCacheManager } from './cacheToPluginCacheManager';
export * from './reexport';
export * from './types';
+29
View File
@@ -0,0 +1,29 @@
/*
* Copyright 2024 The Backstage Authors
*
* 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.
*/
/*
* NOTE(freben): This is a temporary hack. We use cross-package imports so that
* we do not have to maintain double implementations for the time being, until
* backend-common is properly removed.
*/
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
export { CacheManager } from '../../../backend-defaults/src/entrypoints/cache/CacheManager';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
export {
type PluginCacheManager,
type CacheManagerOptions,
} from '../../../backend-defaults/src/entrypoints/cache/types';
+12 -25
View File
@@ -14,39 +14,26 @@
* limitations under the License.
*/
import { LoggerService } from '@backstage/backend-plugin-api';
import {
import type {
CacheService,
CacheServiceSetOptions,
CacheServiceOptions,
} from '@backstage/backend-plugin-api';
export type {
CacheService as CacheClient,
CacheServiceSetOptions as CacheClientSetOptions,
CacheServiceOptions as CacheClientOptions,
} from '@backstage/backend-plugin-api';
/**
* Options given when constructing a {@link CacheManager}.
*
* @public
* @deprecated Use `CacheService` from the `@backstage/backend-plugin-api` package instead
*/
export type CacheManagerOptions = {
/**
* An optional logger for use by the PluginCacheManager.
*/
logger?: LoggerService;
/**
* An optional handler for connection errors emitted from the underlying data
* store.
*/
onError?: (err: Error) => void;
};
export type CacheClient = CacheService;
/**
* @public
* @deprecated Use `CacheServiceSetOptions` from the `@backstage/backend-plugin-api` package instead
*/
export interface PluginCacheManager {
getClient(options?: CacheServiceOptions): CacheService;
}
export type CacheClientSetOptions = CacheServiceSetOptions;
/**
* @public
* @deprecated Use `CacheServiceOptions` from the `@backstage/backend-plugin-api` package instead
*/
export type CacheClientOptions = CacheServiceOptions;
@@ -14,10 +14,5 @@
* limitations under the License.
*/
export { DatabaseManager, dropDatabase } from './DatabaseManager';
export type {
DatabaseManagerOptions,
LegacyRootDatabaseService,
} from './DatabaseManager';
export * from './reexport';
export type { PluginDatabaseManager } from './types';
@@ -0,0 +1,37 @@
/*
* Copyright 2024 The Backstage Authors
*
* 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.
*/
/*
* NOTE(freben): This is a temporary hack. We use cross-package imports so that
* we do not have to maintain double implementations for the time being, until
* backend-common is properly removed. When it is, the impleemntation should be
* moved into this part of the repo instead.
*/
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import {
DatabaseManager,
dropDatabase,
type DatabaseManagerOptions,
type LegacyRootDatabaseService,
} from '../../../backend-defaults/src/entrypoints/database/DatabaseManager';
export {
DatabaseManager,
dropDatabase,
type DatabaseManagerOptions,
type LegacyRootDatabaseService,
};
@@ -15,8 +15,13 @@
*/
import { HostDiscovery as _HostDiscovery } from '@backstage/backend-app-api';
import { DiscoveryService } from '@backstage/backend-plugin-api';
export type { DiscoveryService as PluginEndpointDiscovery } from '@backstage/backend-plugin-api';
/**
* @public
* @deprecated Use `DiscoveryService` from `@backstage/backend-plugin-api` instead
*/
export type PluginEndpointDiscovery = DiscoveryService;
/**
* HostDiscovery is a basic PluginEndpointDiscovery implementation
@@ -40,6 +45,6 @@ export const HostDiscovery = _HostDiscovery;
* resolved to the same host, so there won't be any balancing of internal traffic.
*
* @public
* @deprecated Use {@link HostDiscovery} instead
* @deprecated Use `HostDiscovery` from `@backstage/backend-defaults/discovery` instead
*/
export const SingleHostDiscovery = _HostDiscovery;
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {
HostDiscovery,
SingleHostDiscovery,
+31 -2
View File
@@ -3,11 +3,40 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { CacheClient } from '@backstage/backend-common';
import { CacheService } from '@backstage/backend-plugin-api';
import { CacheServiceOptions } from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
import { LoggerService } from '@backstage/backend-plugin-api';
import { ServiceFactory } from '@backstage/backend-plugin-api';
// @public
export class CacheManager {
forPlugin(pluginId: string): {
getClient(options?: CacheServiceOptions): CacheService;
};
static fromConfig(
config: Config,
options?: {
logger?: LoggerService;
onError?: (err: Error) => void;
},
): CacheManager;
}
// @public
export type CacheManagerOptions = {
logger?: LoggerService;
onError?: (err: Error) => void;
};
// @public (undocumented)
export const cacheServiceFactory: () => ServiceFactory<CacheClient, 'plugin'>;
export const cacheServiceFactory: () => ServiceFactory<CacheService, 'plugin'>;
// @public (undocumented)
export interface PluginCacheManager {
// (undocumented)
getClient(options?: CacheServiceOptions): CacheService;
}
// (No @packageDocumentation comment for this package)
```
@@ -3,14 +3,51 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { Config } from '@backstage/config';
import { DatabaseService } from '@backstage/backend-plugin-api';
import { LifecycleService } from '@backstage/backend-plugin-api';
import { LoggerService } from '@backstage/backend-plugin-api';
import { PluginDatabaseManager } from '@backstage/backend-common';
import { PluginMetadataService } from '@backstage/backend-plugin-api';
import { ServiceFactory } from '@backstage/backend-plugin-api';
// @public
export class DatabaseManager implements LegacyRootDatabaseService {
forPlugin(
pluginId: string,
deps?: {
lifecycle: LifecycleService;
pluginMetadata: PluginMetadataService;
},
): DatabaseService;
static fromConfig(
config: Config,
options?: DatabaseManagerOptions,
): DatabaseManager;
}
// @public
export type DatabaseManagerOptions = {
migrations?: DatabaseService['migrations'];
logger?: LoggerService;
};
// @public (undocumented)
export const databaseServiceFactory: () => ServiceFactory<
PluginDatabaseManager,
'plugin'
>;
// @public
export function dropDatabase(
dbConfig: Config,
...databaseNames: string[]
): Promise<void>;
// @public
export type LegacyRootDatabaseService = {
forPlugin(pluginId: string): DatabaseService;
};
// (No @packageDocumentation comment for this package)
```
+12 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-defaults",
"description": "Backend defaults used by Backstage backend apps",
"version": "0.2.19-next.0",
"description": "Backend defaults used by Backstage backend apps",
"backstage": {
"role": "node-library"
},
@@ -84,6 +84,7 @@
"dependencies": {
"@backstage/backend-app-api": "workspace:^",
"@backstage/backend-common": "workspace:^",
"@backstage/backend-dev-utils": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/config-loader": "workspace:^",
@@ -91,12 +92,22 @@
"@backstage/plugin-events-node": "workspace:^",
"@backstage/plugin-permission-node": "workspace:^",
"@backstage/types": "workspace:^",
"@keyv/memcache": "^1.3.5",
"@keyv/redis": "^2.5.3",
"@opentelemetry/api": "^1.3.0",
"better-sqlite3": "^9.0.0",
"cron": "^3.0.0",
"fs-extra": "^11.2.0",
"keyv": "^4.5.2",
"knex": "^3.0.0",
"lodash": "^4.17.21",
"luxon": "^3.0.0",
"mysql2": "^3.0.0",
"p-limit": "^3.1.0",
"pg": "^8.11.3",
"pg-connection-string": "^2.3.0",
"uuid": "^9.0.0",
"yn": "^4.0.0",
"zod": "^3.22.4"
},
"devDependencies": {
@@ -14,9 +14,9 @@
* limitations under the License.
*/
import { ConfigReader } from '@backstage/config';
import { CacheManager } from './CacheManager';
import { mockServices } from '@backstage/backend-test-utils';
import KeyvRedis from '@keyv/redis';
import { CacheManager } from './CacheManager';
// This test is in a separate file because the main test file uses other mocking
// that might interfere with this one.
@@ -24,24 +24,27 @@ import KeyvRedis from '@keyv/redis';
// Contrived code because it's hard to spy on a default export
jest.mock('@keyv/redis', () => {
const ActualKeyvRedis = jest.requireActual('@keyv/redis');
return jest
.fn()
.mockImplementation((...args: any[]) => new ActualKeyvRedis(...args));
return jest.fn((...args: any[]) => {
return new ActualKeyvRedis(...args);
});
});
describe('CacheManager integration', () => {
describe('redis', () => {
it('only creates one underlying connection', async () => {
const connection =
process.env.BACKSTAGE_TEST_CACHE_REDIS7_CONNECTION_STRING;
if (!connection) {
return;
}
const manager = CacheManager.fromConfig(
new ConfigReader({
backend: {
cache: {
store: 'redis',
// no actual connection errors will be seen since we don't interact with it
connection: 'redis://localhost:6379',
},
mockServices.rootConfig({
data: {
backend: { cache: { store: 'redis', connection } },
},
}),
{ onError: e => expect(e).not.toBeDefined() },
);
manager.forPlugin('p1').getClient();
@@ -56,20 +59,18 @@ describe('CacheManager integration', () => {
// TODO(freben): This could be frameworkified as TestCaches just like
// TestDatabases, but that will have to come some other day
const connection =
process.env.BACKSTAGE_TEST_CACHE_REDIS_CONNECTION_STRING;
process.env.BACKSTAGE_TEST_CACHE_REDIS7_CONNECTION_STRING;
if (!connection) {
return;
}
const manager = CacheManager.fromConfig(
new ConfigReader({
backend: {
cache: {
store: 'redis',
connection,
},
mockServices.rootConfig({
data: {
backend: { cache: { store: 'redis', connection } },
},
}),
{ onError: e => expect(e).not.toBeDefined() },
);
const plugin1 = manager.forPlugin('p1').getClient();
@@ -14,21 +14,25 @@
* limitations under the License.
*/
import { Config } from '@backstage/config';
import Keyv from 'keyv';
import KeyvMemcache from '@keyv/memcache';
import KeyvRedis from '@keyv/redis';
import {
CacheService,
CacheServiceOptions,
LoggerService,
} from '@backstage/backend-plugin-api';
import { getRootLogger } from '../logging';
import { Config } from '@backstage/config';
import Keyv from 'keyv';
import { DefaultCacheClient } from './CacheClient';
import { CacheManagerOptions, PluginCacheManager } from './types';
import { CacheManagerOptions } from './types';
type StoreFactory = (pluginId: string, defaultTtl: number | undefined) => Keyv;
/*
* TODO(freben): This class intentionally inlines the CacheManagerOptions and
* PluginCacheManager types, to not break the api reports in backend-common
* which re-exports it. When backend-common is deprecated, we can stop inlining
* those types.
*/
/**
* Implements a Cache Manager which will automatically create new cache clients
* for plugins when requested. All requested cache clients are created with the
@@ -47,7 +51,7 @@ export class CacheManager {
memory: this.createMemoryStoreFactory(),
};
private readonly logger: LoggerService;
private readonly logger?: LoggerService;
private readonly store: keyof CacheManager['storeFactories'];
private readonly connection: string;
private readonly useRedisSets: boolean;
@@ -62,7 +66,18 @@ export class CacheManager {
*/
static fromConfig(
config: Config,
options: CacheManagerOptions = {},
options: {
/**
* An optional logger for use by the PluginCacheManager.
*/
logger?: LoggerService;
/**
* An optional handler for connection errors emitted from the underlying data
* store.
*/
onError?: (err: Error) => void;
} = {},
): CacheManager {
// If no `backend.cache` config is provided, instantiate the CacheManager
// with an in-memory cache client.
@@ -72,27 +87,26 @@ export class CacheManager {
config.getOptionalString('backend.cache.connection') || '';
const useRedisSets =
config.getOptionalBoolean('backend.cache.useRedisSets') ?? true;
// TODO: Make logger required and remove the default logger after moving this class to the `backstage-defaults`package
const logger = (options.logger || getRootLogger()).child({
const logger = options.logger?.child({
type: 'cacheManager',
});
return new CacheManager(
store,
connectionString,
useRedisSets,
logger,
options.onError,
logger,
defaultTtl,
);
}
private constructor(
/** @internal */
constructor(
store: string,
connectionString: string,
useRedisSets: boolean,
logger: LoggerService,
errorHandler: CacheManagerOptions['onError'],
logger?: LoggerService,
defaultTtl?: number,
) {
if (!this.storeFactories.hasOwnProperty(store)) {
@@ -112,7 +126,9 @@ export class CacheManager {
* @param pluginId - The plugin that the cache manager should be created for.
* Plugin names should be unique.
*/
forPlugin(pluginId: string): PluginCacheManager {
forPlugin(pluginId: string): {
getClient(options?: CacheServiceOptions): CacheService;
} {
return {
getClient: (defaultOptions = {}) => {
const clientFactory = (options: CacheServiceOptions) => {
@@ -124,7 +140,7 @@ export class CacheManager {
// Always provide an error handler to avoid stopping the process.
concreteClient.on('error', (err: Error) => {
// In all cases, just log the error.
this.logger.error('Failed to create cache client', err);
this.logger?.error('Failed to create cache client', err);
// Invoke any custom error handler if provided.
if (typeof this.errorHandler === 'function') {
@@ -149,7 +165,8 @@ export class CacheManager {
}
private createRedisStoreFactory(): StoreFactory {
let store: KeyvRedis | undefined;
const KeyvRedis = require('@keyv/redis');
let store: typeof KeyvRedis | undefined;
return (pluginId, defaultTtl) => {
if (!store) {
store = new KeyvRedis(this.connection);
@@ -164,7 +181,8 @@ export class CacheManager {
}
private createMemcacheStoreFactory(): StoreFactory {
let store: KeyvMemcache | undefined;
const KeyvMemcache = require('@keyv/memcache');
let store: typeof KeyvMemcache | undefined;
return (pluginId, defaultTtl) => {
if (!store) {
store = new KeyvMemcache(this.connection);
@@ -187,12 +205,3 @@ export class CacheManager {
});
}
}
/** @public */
export function cacheToPluginCacheManager(
cache: CacheService,
): PluginCacheManager {
return {
getClient: (opts: CacheServiceOptions) => cache.withOptions(opts),
};
}
@@ -14,11 +14,11 @@
* limitations under the License.
*/
import { CacheManager } from '@backstage/backend-common';
import {
coreServices,
createServiceFactory,
} from '@backstage/backend-plugin-api';
import { CacheManager } from './CacheManager';
/**
* @public
@@ -28,9 +28,10 @@ export const cacheServiceFactory = createServiceFactory({
deps: {
config: coreServices.rootConfig,
plugin: coreServices.pluginMetadata,
logger: coreServices.rootLogger,
},
async createRootContext({ config }) {
return CacheManager.fromConfig(config);
async createRootContext({ config, logger }) {
return CacheManager.fromConfig(config, { logger });
},
async factory({ plugin }, manager) {
return manager.forPlugin(plugin.getId()).getClient();
@@ -15,3 +15,5 @@
*/
export { cacheServiceFactory } from './cacheServiceFactory';
export { CacheManager } from './CacheManager';
export type { CacheManagerOptions, PluginCacheManager } from './types';
@@ -0,0 +1,46 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 { LoggerService } from '@backstage/backend-plugin-api';
import {
CacheService,
CacheServiceOptions,
} from '@backstage/backend-plugin-api';
/**
* Options given when constructing a {@link CacheManager}.
*
* @public
*/
export type CacheManagerOptions = {
/**
* An optional logger for use by the PluginCacheManager.
*/
logger?: LoggerService;
/**
* An optional handler for connection errors emitted from the underlying data
* store.
*/
onError?: (err: Error) => void;
};
/**
* @public
*/
export interface PluginCacheManager {
getClient(options?: CacheServiceOptions): CacheService;
}
@@ -41,7 +41,7 @@ function pluginPath(pluginId: string): string {
* @public
*/
export type DatabaseManagerOptions = {
migrations?: PluginDatabaseManager['migrations'];
migrations?: DatabaseService['migrations'];
logger?: LoggerService;
};
@@ -15,3 +15,9 @@
*/
export { databaseServiceFactory } from './databaseServiceFactory';
export {
DatabaseManager,
type DatabaseManagerOptions,
type LegacyRootDatabaseService,
dropDatabase,
} from './DatabaseManager';
@@ -0,0 +1,100 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 {
LifecycleService,
PluginMetadataService,
} from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
import { Knex } from 'knex';
export type { DatabaseService as PluginDatabaseManager } from '@backstage/backend-plugin-api';
/**
* Manages an underlying Knex database driver.
*/
export interface DatabaseConnector {
/**
* Provides an instance of a knex database connector.
*/
createClient(
dbConfig: Config,
overrides?: Partial<Knex.Config>,
deps?: {
lifecycle: LifecycleService;
pluginMetadata: PluginMetadataService;
},
): Knex;
/**
* Provides a partial knex config sufficient to override a database name.
*/
createNameOverride(name: string): Partial<Knex.Config>;
/**
* Provides a partial knex config sufficient to override a PostgreSQL schema
* name within utilizing the `searchPath` knex configuration.
*/
createSchemaOverride?(name: string): Partial<Knex.Config>;
/**
* Produces a knex connection config object representing a database connection
* string.
*/
parseConnectionString(
connectionString: string,
client?: string,
): Knex.StaticConnectionConfig;
/**
* 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>;
/**
* Performs a side-effect to ensure schema names passed in are present.
*
* Calling this function on schemas which already exist should do nothing.
* Missing schemas should be created if needed.
*/
ensureSchemaExists?(
dbConfig: Config,
...schemas: Array<string>
): Promise<void>;
/**
* Deletes databases.
*/
dropDatabase?(dbConfig: Config, ...databases: Array<string>): Promise<void>;
}
export interface Connector {
getClient(
pluginId: string,
deps?: {
lifecycle: LifecycleService;
pluginMetadata: PluginMetadataService;
},
): Promise<Knex>;
dropDatabase(...databaseNames: string[]): Promise<void>;
}
+2 -2
View File
@@ -14,7 +14,7 @@ import { AwsAlbResult as AwsAlbResult_2 } from '@backstage/plugin-auth-backend-m
import { AzureEasyAuthResult } from '@backstage/plugin-auth-backend-module-azure-easyauth-provider';
import { BackendFeature } from '@backstage/backend-plugin-api';
import { BackstageSignInResult } from '@backstage/plugin-auth-node';
import { CacheClient } from '@backstage/backend-common';
import { CacheService } from '@backstage/backend-plugin-api';
import { CatalogApi } from '@backstage/catalog-client';
import { ClientAuthResponse } from '@backstage/plugin-auth-node';
import { cloudflareAccessSignInResolvers } from '@backstage/plugin-auth-backend-module-cloudflare-access-provider';
@@ -452,7 +452,7 @@ export const providers: Readonly<{
signIn: {
resolver: SignInResolver_2<CloudflareAccessResult>;
};
cache?: CacheClient | undefined;
cache?: CacheService | undefined;
}) => AuthProviderFactory_2;
resolvers: Readonly<cloudflareAccessSignInResolvers>;
}>;
+11
View File
@@ -3430,6 +3430,7 @@ __metadata:
dependencies:
"@backstage/backend-app-api": "workspace:^"
"@backstage/backend-common": "workspace:^"
"@backstage/backend-dev-utils": "workspace:^"
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
"@backstage/cli": "workspace:^"
@@ -3439,13 +3440,23 @@ __metadata:
"@backstage/plugin-events-node": "workspace:^"
"@backstage/plugin-permission-node": "workspace:^"
"@backstage/types": "workspace:^"
"@keyv/memcache": ^1.3.5
"@keyv/redis": ^2.5.3
"@opentelemetry/api": ^1.3.0
better-sqlite3: ^9.0.0
cron: ^3.0.0
fs-extra: ^11.2.0
keyv: ^4.5.2
knex: ^3.0.0
lodash: ^4.17.21
luxon: ^3.0.0
mysql2: ^3.0.0
p-limit: ^3.1.0
pg: ^8.11.3
pg-connection-string: ^2.3.0
uuid: ^9.0.0
wait-for-expect: ^3.0.2
yn: ^4.0.0
zod: ^3.22.4
languageName: unknown
linkType: soft