Merge remote-tracking branch 'upstream/master'

Signed-off-by: Matteo Silvestri <matteosilv@gmail.com>
This commit is contained in:
Matteo Silvestri
2022-09-01 12:13:28 +02:00
600 changed files with 32104 additions and 165333 deletions
+15
View File
@@ -1,5 +1,20 @@
# @backstage/backend-common
## 0.15.1-next.1
### Patch Changes
- c3c90280be: Added a keep-alive refresh loop to the `DatabaseManager`, keeping the connection
pool up.
- c3c90280be: The options part of `DatabaseManager.fromConfig` now accepts an optional logger
field.
- 60b85d8ade: Updated dependency `helmet` to `^6.0.0`.
Please note that these policies are no longer applied by default:
helmet.contentSecurityPolicy no longer sets block-all-mixed-content directive by default
helmet.expectCt is no longer set by default. It can, however, be explicitly enabled. It will be removed in Helmet 7.
## 0.15.1-next.0
### Patch Changes
+1
View File
@@ -252,6 +252,7 @@ export class DatabaseManager {
// @public
export type DatabaseManagerOptions = {
migrations?: PluginDatabaseManager['migrations'];
logger?: Logger;
};
// @public
+5 -6
View File
@@ -1,10 +1,9 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
"version": "0.15.1-next.0",
"version": "0.15.1-next.1",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
@@ -61,7 +60,7 @@
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"fs-extra": "10.1.0",
"git-url-parse": "^12.0.0",
"git-url-parse": "^13.0.0",
"helmet": "^6.0.0",
"isomorphic-git": "^1.8.0",
"jose": "^4.6.0",
@@ -95,8 +94,8 @@
}
},
"devDependencies": {
"@backstage/backend-test-utils": "^0.1.28-next.0",
"@backstage/cli": "^0.18.2-next.0",
"@backstage/backend-test-utils": "^0.1.28-next.1",
"@backstage/cli": "^0.19.0-next.1",
"@types/archiver": "^5.1.0",
"@types/base64-stream": "^1.0.2",
"@types/compression": "^1.7.0",
@@ -116,7 +115,7 @@
"better-sqlite3": "^7.5.0",
"http-errors": "^2.0.0",
"mock-fs": "^5.1.0",
"msw": "^0.45.0",
"msw": "^0.46.0",
"mysql2": "^2.2.5",
"recursive-readdir": "^2.2.2",
"supertest": "^6.1.3"
+2 -2
View File
@@ -65,7 +65,7 @@ describe('CacheManager', () => {
const config = new ConfigReader({ backend: {} });
expect(() => {
CacheManager.fromConfig(config);
}).not.toThrowError();
}).not.toThrow();
});
it('throws on unknown cache store', () => {
@@ -74,7 +74,7 @@ describe('CacheManager', () => {
});
expect(() => {
CacheManager.fromConfig(config);
}).toThrowError();
}).toThrow();
});
});
@@ -37,12 +37,12 @@ describe('AbortContext', () => {
expect(child.abortSignal.aborted).toBe(false);
expect(Math.abs(+child.deadline! - deadline)).toBeLessThan(50);
expect(childListener).toBeCalledTimes(0);
expect(childListener).toHaveBeenCalledTimes(0);
jest.advanceTimersByTime(timeout + 1);
expect(child.abortSignal.aborted).toBe(true);
expect(childListener).toBeCalledTimes(1);
expect(childListener).toHaveBeenCalledTimes(1);
});
it('results in minimum deadline when parent triggers sooner', async () => {
@@ -66,15 +66,15 @@ describe('AbortContext', () => {
expect(child.abortSignal.aborted).toBe(false);
expect(Math.abs(+parent.deadline! - parentDeadline)).toBeLessThan(50);
expect(Math.abs(+child.deadline! - childDeadline)).toBeLessThan(50);
expect(parentListener).toBeCalledTimes(0);
expect(childListener).toBeCalledTimes(0);
expect(parentListener).toHaveBeenCalledTimes(0);
expect(childListener).toHaveBeenCalledTimes(0);
jest.advanceTimersByTime(parentTimeout + 1);
expect(parent.abortSignal.aborted).toBe(true);
expect(child.abortSignal.aborted).toBe(true);
expect(parentListener).toBeCalledTimes(1);
expect(childListener).toBeCalledTimes(1);
expect(parentListener).toHaveBeenCalledTimes(1);
expect(childListener).toHaveBeenCalledTimes(1);
});
it('results in minimum deadline when child triggers sooner', async () => {
@@ -98,22 +98,22 @@ describe('AbortContext', () => {
expect(child.abortSignal.aborted).toBe(false);
expect(Math.abs(+parent.deadline! - parentDeadline)).toBeLessThan(50);
expect(Math.abs(+child.deadline! - childDeadline)).toBeLessThan(50);
expect(parentListener).toBeCalledTimes(0);
expect(childListener).toBeCalledTimes(0);
expect(parentListener).toHaveBeenCalledTimes(0);
expect(childListener).toHaveBeenCalledTimes(0);
jest.advanceTimersByTime(childTimeout + 1);
expect(parent.abortSignal.aborted).toBe(false);
expect(child.abortSignal.aborted).toBe(true);
expect(parentListener).toBeCalledTimes(0);
expect(childListener).toBeCalledTimes(1);
expect(parentListener).toHaveBeenCalledTimes(0);
expect(childListener).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(parentTimeout - childTimeout + 1);
expect(parent.abortSignal.aborted).toBe(true);
expect(child.abortSignal.aborted).toBe(true);
expect(parentListener).toBeCalledTimes(1);
expect(childListener).toBeCalledTimes(1);
expect(parentListener).toHaveBeenCalledTimes(1);
expect(childListener).toHaveBeenCalledTimes(1);
});
it('child carries over parent signal state if parent was already aborted and had no deadline', async () => {
@@ -133,13 +133,13 @@ describe('AbortContext', () => {
child.abortSignal.addEventListener('abort', childListener);
expect(child.abortSignal.aborted).toBe(true);
expect(childListener).toBeCalledTimes(0);
expect(childListener).toHaveBeenCalledTimes(0);
expect(Math.abs(+child.deadline! - childDeadline)).toBeLessThan(50);
jest.advanceTimersByTime(childTimeout + 1);
expect(child.abortSignal.aborted).toBe(true);
expect(childListener).toBeCalledTimes(0); // still
expect(childListener).toHaveBeenCalledTimes(0); // still
});
it('child carries over parent signal state if parent was already aborted and had a deadline', async () => {
@@ -175,15 +175,15 @@ describe('AbortContext', () => {
expect(parent.abortSignal.aborted).toBe(false);
expect(child.abortSignal.aborted).toBe(false);
expect(parentListener).toBeCalledTimes(0);
expect(childListener).toBeCalledTimes(0);
expect(parentListener).toHaveBeenCalledTimes(0);
expect(childListener).toHaveBeenCalledTimes(0);
parentController.abort();
expect(parent.abortSignal.aborted).toBe(true);
expect(child.abortSignal.aborted).toBe(true);
expect(parentListener).toBeCalledTimes(1);
expect(childListener).toBeCalledTimes(1);
expect(parentListener).toHaveBeenCalledTimes(1);
expect(childListener).toHaveBeenCalledTimes(1);
});
it('does not signal parent when child is aborted', async () => {
@@ -201,15 +201,15 @@ describe('AbortContext', () => {
expect(parent.abortSignal.aborted).toBe(false);
expect(child.abortSignal.aborted).toBe(false);
expect(parentListener).toBeCalledTimes(0);
expect(childListener).toBeCalledTimes(0);
expect(parentListener).toHaveBeenCalledTimes(0);
expect(childListener).toHaveBeenCalledTimes(0);
childController.abort();
expect(parent.abortSignal.aborted).toBe(false);
expect(child.abortSignal.aborted).toBe(true);
expect(parentListener).toBeCalledTimes(0);
expect(childListener).toBeCalledTimes(1);
expect(parentListener).toHaveBeenCalledTimes(0);
expect(childListener).toHaveBeenCalledTimes(1);
});
it('child carries over parent signal state if parent was already aborted', async () => {
@@ -227,13 +227,13 @@ describe('AbortContext', () => {
expect(parent.abortSignal.aborted).toBe(true);
expect(child.abortSignal.aborted).toBe(true);
expect(childListener).toBeCalledTimes(0);
expect(childListener).toHaveBeenCalledTimes(0);
childController.abort();
expect(parent.abortSignal.aborted).toBe(true);
expect(child.abortSignal.aborted).toBe(true);
expect(childListener).toBeCalledTimes(0);
expect(childListener).toHaveBeenCalledTimes(0);
});
it('child carries over given signal state if it was already aborted', async () => {
@@ -247,7 +247,7 @@ describe('AbortContext', () => {
child.abortSignal.addEventListener('abort', childListener);
expect(child.abortSignal.aborted).toBe(true);
expect(childListener).toBeCalledTimes(0);
expect(childListener).toHaveBeenCalledTimes(0);
});
});
@@ -267,15 +267,15 @@ describe('AbortContext', () => {
expect(parent.abortSignal.aborted).toBe(false);
expect(child.abortSignal.aborted).toBe(false);
expect(parentListener).toBeCalledTimes(0);
expect(childListener).toBeCalledTimes(0);
expect(parentListener).toHaveBeenCalledTimes(0);
expect(childListener).toHaveBeenCalledTimes(0);
parentController.abort();
expect(parent.abortSignal.aborted).toBe(true);
expect(child.abortSignal.aborted).toBe(true);
expect(parentListener).toBeCalledTimes(1);
expect(childListener).toBeCalledTimes(1);
expect(parentListener).toHaveBeenCalledTimes(1);
expect(childListener).toHaveBeenCalledTimes(1);
});
it('does not signal parent when child is aborted', async () => {
@@ -293,15 +293,15 @@ describe('AbortContext', () => {
expect(parent.abortSignal.aborted).toBe(false);
expect(child.abortSignal.aborted).toBe(false);
expect(parentListener).toBeCalledTimes(0);
expect(childListener).toBeCalledTimes(0);
expect(parentListener).toHaveBeenCalledTimes(0);
expect(childListener).toHaveBeenCalledTimes(0);
childController.abort();
expect(parent.abortSignal.aborted).toBe(false);
expect(child.abortSignal.aborted).toBe(true);
expect(parentListener).toBeCalledTimes(0);
expect(childListener).toBeCalledTimes(1);
expect(parentListener).toHaveBeenCalledTimes(0);
expect(childListener).toHaveBeenCalledTimes(1);
});
it('child carries over parent signal state if parent was already aborted', async () => {
@@ -319,13 +319,13 @@ describe('AbortContext', () => {
expect(parent.abortSignal.aborted).toBe(true);
expect(child.abortSignal.aborted).toBe(true);
expect(childListener).toBeCalledTimes(0);
expect(childListener).toHaveBeenCalledTimes(0);
childController.abort();
expect(parent.abortSignal.aborted).toBe(true);
expect(child.abortSignal.aborted).toBe(true);
expect(childListener).toBeCalledTimes(0);
expect(childListener).toHaveBeenCalledTimes(0);
});
it('child carries over given signal state if it was already aborted', async () => {
@@ -339,7 +339,7 @@ describe('AbortContext', () => {
child.abortSignal.addEventListener('abort', childListener);
expect(child.abortSignal.aborted).toBe(true);
expect(childListener).toBeCalledTimes(0);
expect(childListener).toHaveBeenCalledTimes(0);
});
});
});
@@ -29,6 +29,8 @@ import {
} from './connection';
import { PluginDatabaseManager } from './types';
import path from 'path';
import { Logger } from 'winston';
import { stringifyError } from '@backstage/errors';
/**
* Provides a config lookup path for a plugin's config block.
@@ -44,6 +46,7 @@ function pluginPath(pluginId: string): string {
*/
export type DatabaseManagerOptions = {
migrations?: PluginDatabaseManager['migrations'];
logger?: Logger;
};
/**
@@ -339,6 +342,31 @@ export class DatabaseManager {
schemaOverrides,
);
return createDatabaseClient(pluginConfig, databaseClientOverrides);
const client = createDatabaseClient(pluginConfig, databaseClientOverrides);
this.startKeepaliveLoop(pluginId, client);
return client;
}
private startKeepaliveLoop(pluginId: string, client: Knex): void {
let lastKeepaliveFailed = false;
setInterval(() => {
client.raw('select 1').then(
() => {
lastKeepaliveFailed = false;
},
(error: unknown) => {
if (!lastKeepaliveFailed) {
lastKeepaliveFailed = true;
this.options?.logger?.warn(
`Database keepalive failed for plugin ${pluginId}, ${stringifyError(
error,
)}`,
);
}
},
);
}, 60 * 1000);
}
}
@@ -110,7 +110,7 @@ describe('database connection', () => {
connection: '',
}),
),
).toThrowError();
).toThrow();
});
it('throws an error without a connection', () => {
@@ -120,7 +120,7 @@ describe('database connection', () => {
client: 'pg',
}),
),
).toThrowError();
).toThrow();
});
});
@@ -147,7 +147,7 @@ describe('database connection', () => {
});
it('throws an error for unknown connection', () => {
expect(() => createNameOverride('unknown', 'testname')).toThrowError();
expect(() => createNameOverride('unknown', 'testname')).toThrow();
});
});
@@ -194,7 +194,7 @@ describe('postgres', () => {
'postgresql://postgres:pass@localhost:5432/dbname?sslrootcert=/path/to/file',
),
),
).toThrowError(/no such file or directory/);
).toThrow(/no such file or directory/);
});
});
@@ -34,7 +34,7 @@ export function requestLoggingHandler(logger?: Logger): RequestHandler {
return morgan('combined', {
stream: {
write(message: String) {
actualLogger.info(message.trimRight());
actualLogger.info(message.trimEnd());
},
},
});
@@ -551,7 +551,7 @@ describe('GithubUrlReader', () => {
credentialsProvider: mockCredentialsProvider,
},
);
}).toThrowError('must configure an explicit apiBaseUrl');
}).toThrow('must configure an explicit apiBaseUrl');
});
});
@@ -127,7 +127,7 @@ describe('GcsUrlReader', () => {
it('throws if search url looks truly glob-y', async () => {
const glob = 'https://storage.cloud.google.com/bucket/**/path*';
await expect(() => reader.search(glob)).rejects.toThrowError(
await expect(() => reader.search(glob)).rejects.toThrow(
'GcsUrlReader only supports prefix-based searches',
);
});
@@ -55,7 +55,7 @@ describe('ReadUrlResponseFactory', () => {
it('buffer cannot be called after stream is called', async () => {
const response = await ReadUrlResponseFactory.fromReadable(readable);
response.stream!();
expect(() => response.buffer()).toThrowError(ConflictError);
expect(() => response.buffer()).toThrow(ConflictError);
});
it('stream returns expected data', async () => {
@@ -68,7 +68,7 @@ describe('ReadUrlResponseFactory', () => {
it('stream cannot be called after buffer is called', async () => {
const response = await ReadUrlResponseFactory.fromReadable(readable);
response.buffer();
expect(() => response.stream!()).toThrowError(ConflictError);
expect(() => response.stream!()).toThrow(ConflictError);
});
});
@@ -64,7 +64,7 @@ describe('UrlReaderPredicateMux', () => {
it('throws an error if no predicate matches', async () => {
const mux = new UrlReaderPredicateMux(getVoidLogger());
await expect(mux.readUrl('http://foo/1')).rejects.toThrowError(
await expect(mux.readUrl('http://foo/1')).rejects.toThrow(
/^Reading from 'http:\/\/foo\/1' is not allowed. You may/,
);
@@ -80,7 +80,7 @@ describe('UrlReaderPredicateMux', () => {
await expect(mux.readUrl('http://foo/1')).resolves.toBeUndefined();
await expect(mux.readUrl('http://bar/1')).rejects.toThrowError(
await expect(mux.readUrl('http://bar/1')).rejects.toThrow(
/^Reading from 'http:\/\/bar\/1' is not allowed. You may/,
);
});
@@ -66,9 +66,9 @@ describe('ServerTokenManager', () => {
const tokenManager = ServerTokenManager.fromConfig(configWithSecret, {
logger,
});
await expect(
tokenManager.authenticate('random-string'),
).rejects.toThrowError(/invalid server token/i);
await expect(tokenManager.authenticate('random-string')).rejects.toThrow(
/invalid server token/i,
);
});
it('should validate server tokens created by a different instance using the same secret', async () => {
@@ -129,7 +129,7 @@ describe('ServerTokenManager', () => {
const { token } = await tokenManager1.getToken();
await expect(tokenManager2.authenticate(token)).rejects.toThrowError(
await expect(tokenManager2.authenticate(token)).rejects.toThrow(
/invalid server token/i,
);
});
@@ -145,7 +145,7 @@ describe('ServerTokenManager', () => {
const { token } = await noopTokenManager.getToken();
await expect(tokenManager.authenticate(token)).rejects.toThrowError(
await expect(tokenManager.authenticate(token)).rejects.toThrow(
/invalid server token/i,
);
});
@@ -164,7 +164,7 @@ describe('ServerTokenManager', () => {
const { token } = await tokenManager2.getToken();
await expect(tokenManager1.authenticate(token)).rejects.toThrowError(
await expect(tokenManager1.authenticate(token)).rejects.toThrow(
/invalid server token/i,
);
});