Merge branch 'master' into feat/dynatrace-plugin-synthetics

Signed-off-by: Isaiah Thiessen <isaiah.thiessen@telus.com>
This commit is contained in:
Isaiah Thiessen
2022-08-15 10:36:49 -07:00
51 changed files with 962 additions and 439 deletions
@@ -14,10 +14,22 @@
* limitations under the License.
*/
import Knex from 'knex';
import Knex, { Knex as KnexType } from 'knex';
import { DatabaseKeyStore } from './DatabaseKeyStore';
import { DateTime } from 'luxon';
function createDatabaseManager(
client: KnexType,
skipMigrations: boolean = false,
) {
return {
getClient: async () => client,
migrations: {
skip: skipMigrations,
},
};
}
function createDB() {
const knex = Knex({
client: 'better-sqlite3',
@@ -38,8 +50,10 @@ const keyBase = {
describe('DatabaseKeyStore', () => {
it('should store a key', async () => {
const database = createDB();
const store = await DatabaseKeyStore.create({ database });
const client = createDB();
const store = await DatabaseKeyStore.create({
database: createDatabaseManager(client),
});
const key = {
kid: '123',
@@ -59,8 +73,10 @@ describe('DatabaseKeyStore', () => {
});
it('should remove stored keys', async () => {
const database = createDB();
const store = await DatabaseKeyStore.create({ database });
const client = createDB();
const store = await DatabaseKeyStore.create({
database: createDatabaseManager(client),
});
const key1 = { kid: '1', ...keyBase };
const key2 = { kid: '2', ...keyBase };
@@ -14,7 +14,10 @@
* limitations under the License.
*/
import { resolvePackagePath } from '@backstage/backend-common';
import {
PluginDatabaseManager,
resolvePackagePath,
} from '@backstage/backend-common';
import { Knex } from 'knex';
import { DateTime } from 'luxon';
import { AnyJWK, KeyStore, StoredKey } from './types';
@@ -33,7 +36,7 @@ type Row = {
};
type Options = {
database: Knex;
database: PluginDatabaseManager;
};
const parseDate = (date: string | Date) => {
@@ -54,29 +57,32 @@ const parseDate = (date: string | Date) => {
export class DatabaseKeyStore implements KeyStore {
static async create(options: Options): Promise<DatabaseKeyStore> {
const { database } = options;
const client = await database.getClient();
await database.migrate.latest({
directory: migrationsDir,
});
if (!database.migrations?.skip) {
await client.migrate.latest({
directory: migrationsDir,
});
}
return new DatabaseKeyStore(options);
return new DatabaseKeyStore(client);
}
private readonly database: Knex;
private readonly client: Knex;
private constructor(options: Options) {
this.database = options.database;
private constructor(client: Knex) {
this.client = client;
}
async addKey(key: AnyJWK): Promise<void> {
await this.database<Row>(TABLE).insert({
await this.client<Row>(TABLE).insert({
kid: key.kid,
key: JSON.stringify(key),
});
}
async listKeys(): Promise<{ items: StoredKey[] }> {
const rows = await this.database<Row>(TABLE).select();
const rows = await this.client<Row>(TABLE).select();
return {
items: rows.map(row => ({
@@ -87,6 +93,6 @@ export class DatabaseKeyStore implements KeyStore {
}
async removeKeys(kids: string[]): Promise<void> {
await this.database(TABLE).delete().whereIn('kid', kids);
await this.client(TABLE).delete().whereIn('kid', kids);
}
}
@@ -53,9 +53,7 @@ export class KeyStores {
throw new Error('This KeyStore provider requires a database');
}
return await DatabaseKeyStore.create({
database: await database.getClient(),
});
return await DatabaseKeyStore.create({ database });
}
if (provider === 'memory') {
+2 -2
View File
@@ -5,7 +5,7 @@
```ts
/// <reference types="node" />
import { BackendRegistrable } from '@backstage/backend-plugin-api';
import { BackendFeature } from '@backstage/backend-plugin-api';
import { CatalogApi } from '@backstage/catalog-client';
import { CatalogEntityDocument } from '@backstage/plugin-catalog-common';
import { CatalogProcessor } from '@backstage/plugin-catalog-node';
@@ -224,7 +224,7 @@ export type CatalogPermissionRule<TParams extends unknown[] = unknown[]> =
PermissionRule<Entity, EntitiesSearchFilter, 'catalog-entity', TParams>;
// @alpha
export const catalogPlugin: (option: unknown) => BackendRegistrable;
export const catalogPlugin: (options?: unknown) => BackendFeature;
// @public (undocumented)
export interface CatalogProcessingEngine {
@@ -27,7 +27,7 @@ import { CatalogBuilder } from './CatalogBuilder';
import {
CatalogProcessor,
CatalogProcessingExtensionPoint,
catalogProcessingExtentionPoint,
catalogProcessingExtensionPoint,
EntityProvider,
} from '@backstage/plugin-catalog-node';
@@ -62,7 +62,7 @@ export const catalogPlugin = createBackendPlugin({
const processingExtensions = new CatalogExtensionPointImpl();
// plugins depending on this API will be initialized before this plugins init method is executed.
env.registerExtensionPoint(
catalogProcessingExtentionPoint,
catalogProcessingExtensionPoint,
processingExtensions,
);
+2 -2
View File
@@ -7,8 +7,8 @@
import { CompoundEntityRef } from '@backstage/catalog-model';
import { Entity } from '@backstage/catalog-model';
import { ExtensionPoint } from '@backstage/backend-plugin-api';
import { JsonValue } from '@backstage/types';
import { ServiceRef } from '@backstage/backend-plugin-api';
// @alpha (undocumented)
export interface CatalogProcessingExtensionPoint {
@@ -19,7 +19,7 @@ export interface CatalogProcessingExtensionPoint {
}
// @alpha (undocumented)
export const catalogProcessingExtentionPoint: ServiceRef<CatalogProcessingExtensionPoint>;
export const catalogProcessingExtensionPoint: ExtensionPoint<CatalogProcessingExtensionPoint>;
// @public (undocumented)
export type CatalogProcessor = {
+3 -3
View File
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createServiceRef } from '@backstage/backend-plugin-api';
import { createExtensionPoint } from '@backstage/backend-plugin-api';
import { EntityProvider } from './api';
import { CatalogProcessor } from './api/processor';
@@ -28,7 +28,7 @@ export interface CatalogProcessingExtensionPoint {
/**
* @alpha
*/
export const catalogProcessingExtentionPoint =
createServiceRef<CatalogProcessingExtensionPoint>({
export const catalogProcessingExtensionPoint =
createExtensionPoint<CatalogProcessingExtensionPoint>({
id: 'catalog.processing',
});
+1 -1
View File
@@ -21,6 +21,6 @@
*/
export type { CatalogProcessingExtensionPoint } from './extensions';
export { catalogProcessingExtentionPoint } from './extensions';
export { catalogProcessingExtensionPoint } from './extensions';
export * from './api';
export * from './processing';
+3 -1
View File
@@ -8,6 +8,7 @@
import { ApiRef } from '@backstage/core-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { FetchApi } from '@backstage/core-plugin-api';
import { RouteRef } from '@backstage/core-plugin-api';
// Warning: (ae-missing-release-tag) "CodeClimateApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -74,7 +75,8 @@ export const mockData: CodeClimateData;
//
// @public (undocumented)
export class ProductionCodeClimateApi implements CodeClimateApi {
constructor(discoveryApi: DiscoveryApi);
// Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts
constructor(options: Options);
// (undocumented)
fetchAllData(options: {
apiUrl: string;
+23 -9
View File
@@ -22,7 +22,7 @@ import {
CodeClimateIssuesData,
} from './code-climate-data';
import { CodeClimateApi } from './code-climate-api';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';
import { Duration } from 'luxon';
import humanizeDuration from 'humanize-duration';
@@ -34,8 +34,19 @@ const codeSmellsQuery = `${basicIssuesOptions}&${categoriesFilter}=Complexity`;
const duplicationQuery = `${basicIssuesOptions}&${categoriesFilter}=Duplication`;
const otherIssuesQuery = `${basicIssuesOptions}&${categoriesFilter}=Bug%20Risk`;
type Options = {
discoveryApi: DiscoveryApi;
fetchApi: FetchApi;
};
export class ProductionCodeClimateApi implements CodeClimateApi {
constructor(private readonly discoveryApi: DiscoveryApi) {}
private readonly discoveryApi: DiscoveryApi;
private readonly fetchApi: FetchApi;
constructor(options: Options) {
this.discoveryApi = options.discoveryApi;
this.fetchApi = options.fetchApi;
}
async fetchAllData(options: {
apiUrl: string;
@@ -44,7 +55,6 @@ export class ProductionCodeClimateApi implements CodeClimateApi {
testReportID: string;
}): Promise<any> {
const { apiUrl, repoID, snapshotID, testReportID } = options;
const [
maintainabilityResponse,
testCoverageResponse,
@@ -52,15 +62,19 @@ export class ProductionCodeClimateApi implements CodeClimateApi {
duplicationResponse,
otherIssuesResponse,
] = await Promise.all([
await fetch(`${apiUrl}/repos/${repoID}/snapshots/${snapshotID}`),
await fetch(`${apiUrl}/repos/${repoID}/test_reports/${testReportID}`),
await fetch(
await this.fetchApi.fetch(
`${apiUrl}/repos/${repoID}/snapshots/${snapshotID}`,
),
await this.fetchApi.fetch(
`${apiUrl}/repos/${repoID}/test_reports/${testReportID}`,
),
await this.fetchApi.fetch(
`${apiUrl}/repos/${repoID}/snapshots/${snapshotID}/issues?${codeSmellsQuery}`,
),
await fetch(
await this.fetchApi.fetch(
`${apiUrl}/repos/${repoID}/snapshots/${snapshotID}/issues?${duplicationQuery}`,
),
await fetch(
await this.fetchApi.fetch(
`${apiUrl}/repos/${repoID}/snapshots/${snapshotID}/issues?${otherIssuesQuery}`,
),
]);
@@ -109,7 +123,7 @@ export class ProductionCodeClimateApi implements CodeClimateApi {
'proxy',
)}/codeclimate/api`;
const repoResponse = await fetch(`${apiUrl}/repos/${repoID}`);
const repoResponse = await this.fetchApi.fetch(`${apiUrl}/repos/${repoID}`);
if (!repoResponse.ok) {
throw new Error('Failed fetching Code Climate info');
+4 -3
View File
@@ -20,7 +20,7 @@ import {
createPlugin,
createRouteRef,
discoveryApiRef,
identityApiRef,
fetchApiRef,
createComponentExtension,
} from '@backstage/core-plugin-api';
@@ -37,9 +37,10 @@ export const codeClimatePlugin = createPlugin({
api: codeClimateApiRef,
deps: {
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
fetchApi: fetchApiRef,
},
factory: ({ discoveryApi }) => new ProductionCodeClimateApi(discoveryApi),
factory: ({ discoveryApi, fetchApi }) =>
new ProductionCodeClimateApi({ discoveryApi, fetchApi }),
}),
],
routes: {
+1 -4
View File
@@ -21,7 +21,6 @@
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"prettier": "@spotify/prettier-config",
"dependencies": {
"@backstage/catalog-model": "^1.0.3",
"@backstage/core-components": "^0.11.0-next.2",
@@ -44,7 +43,6 @@
"@backstage/core-app-api": "^1.0.5-next.0",
"@backstage/dev-utils": "^1.0.5-next.1",
"@backstage/test-utils": "^1.1.3-next.0",
"@spotify/prettier-config": "^14.0.0",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",
@@ -52,8 +50,7 @@
"@types/node": "*",
"@types/react": "^16.13.1 || ^17.0.0",
"cross-fetch": "^3.1.5",
"msw": "^0.44.0",
"prettier": "^2.7.1"
"msw": "^0.44.0"
},
"files": [
"dist"
@@ -22,7 +22,9 @@ import { GraphQLEndpoint } from '../../lib/api';
import { BackstageTheme } from '@backstage/theme';
import { Progress } from '@backstage/core-components';
const GraphiQL = React.lazy(() => import('graphiql'));
const GraphiQL = React.lazy(() =>
import('graphiql').then(m => ({ default: m.GraphiQL })),
);
const useStyles = makeStyles<BackstageTheme>(theme => ({
root: {
+2 -2
View File
@@ -5,7 +5,7 @@
```ts
/// <reference types="node" />
import { BackendRegistrable } from '@backstage/backend-plugin-api';
import { BackendFeature } from '@backstage/backend-plugin-api';
import { CatalogApi } from '@backstage/catalog-client';
import { CatalogProcessor } from '@backstage/plugin-catalog-backend';
import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend';
@@ -566,7 +566,7 @@ export type RunCommandOptions = {
};
// @alpha
export const scaffolderCatalogModule: (option: unknown) => BackendRegistrable;
export const scaffolderCatalogModule: (options?: unknown) => BackendFeature;
// @public (undocumented)
export class ScaffolderEntitiesProcessor implements CatalogProcessor {
@@ -14,27 +14,19 @@
* limitations under the License.
*/
import { BackendInitRegistry } from '@backstage/backend-plugin-api';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node';
import { ScaffolderEntitiesProcessor } from '../processor';
import { scaffolderCatalogModule } from './ScaffolderCatalogModule';
import { startTestBackend } from '@backstage/backend-test-utils';
describe('ScaffolderCatalogModule', () => {
it('should register the extension point', () => {
// TODO(jhaals): clean this up and add test helpers for backend system.
const ext = scaffolderCatalogModule({});
expect(ext.id).toBe('catalog.scaffolder.module');
const registry: jest.Mocked<BackendInitRegistry> = {
registerInit: jest.fn(),
} as any;
ext.register(registry);
const extensionPoint = {
addProcessor: jest.fn(),
};
registry.registerInit.mock.calls[0][0].init({
catalogProcessingExtensionPoint: extensionPoint,
it('should register the extension point', async () => {
const extensionPoint = { addProcessor: jest.fn() };
await startTestBackend({
extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]],
features: [scaffolderCatalogModule({})],
});
expect(extensionPoint.addProcessor).toHaveBeenCalledWith(
new ScaffolderEntitiesProcessor(),
);
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { createBackendModule } from '@backstage/backend-plugin-api';
import { catalogProcessingExtentionPoint } from '@backstage/plugin-catalog-node';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node';
import { ScaffolderEntitiesProcessor } from '../processor';
/**
@@ -27,12 +27,10 @@ export const scaffolderCatalogModule = createBackendModule({
register(env) {
env.registerInit({
deps: {
catalogProcessingExtensionPoint: catalogProcessingExtentionPoint,
catalog: catalogProcessingExtensionPoint,
},
async init({ catalogProcessingExtensionPoint }) {
catalogProcessingExtensionPoint.addProcessor(
new ScaffolderEntitiesProcessor(),
);
async init({ catalog }) {
catalog.addProcessor(new ScaffolderEntitiesProcessor());
},
});
},
@@ -267,6 +267,57 @@ describe('createRouter', () => {
);
});
it('should not throw when an invalid authorization header is passed', async () => {
const broker = taskBroker.dispatch as jest.Mocked<TaskBroker>['dispatch'];
const mockToken = 'blob.eyJzdWIiOiIiLCJuYW1lIjoiSm9obiBEb2UifQ.blob';
await request(app)
.post('/v2/tasks')
.set('Authorization', `Bearer ${mockToken}`)
.send({
templateRef: stringifyEntityRef({
kind: 'template',
name: 'create-react-app-template',
}),
values: {
required: 'required-value',
},
});
expect(broker).toHaveBeenCalledWith(
expect.objectContaining({
createdBy: undefined,
secrets: {
backstageToken: undefined,
},
spec: {
apiVersion: mockTemplate.apiVersion,
steps: mockTemplate.spec.steps.map((step, index) => ({
...step,
id: step.id ?? `step-${index + 1}`,
name: step.name ?? step.action,
})),
output: mockTemplate.spec.output ?? {},
parameters: {
required: 'required-value',
},
user: {
entity: undefined,
ref: undefined,
},
templateInfo: {
entityRef: stringifyEntityRef({
kind: 'Template',
namespace: 'Default',
name: mockTemplate.metadata?.name,
}),
baseUrl: 'https://dev.azure.com',
},
},
}),
);
});
it('should not decorate a user when no backstage auth is passed', async () => {
const broker = taskBroker.dispatch as jest.Mocked<TaskBroker>['dispatch'];
@@ -146,7 +146,10 @@ export async function createRouter(
'/v2/templates/:namespace/:kind/:name/parameter-schema',
async (req, res) => {
const { namespace, kind, name } = req.params;
const { token } = parseBearerToken(req.headers.authorization);
const { token } = parseBearerToken({
header: req.headers.authorization,
logger,
});
const template = await findTemplate({
catalogApi: catalogClient,
entityRef: { kind, namespace, name },
@@ -187,9 +190,10 @@ export async function createRouter(
const { kind, namespace, name } = parseEntityRef(templateRef, {
defaultKind: 'template',
});
const { token, entityRef: userEntityRef } = parseBearerToken(
req.headers.authorization,
);
const { token, entityRef: userEntityRef } = parseBearerToken({
header: req.headers.authorization,
logger,
});
const userEntity = userEntityRef
? await catalogClient.getEntityByRef(userEntityRef, { token })
@@ -389,7 +393,10 @@ export async function createRouter(
throw new InputError('Input template is not a template');
}
const { token } = parseBearerToken(req.headers.authorization);
const { token } = parseBearerToken({
header: req.headers.authorization,
logger,
});
for (const parameters of [template.spec.parameters ?? []].flat()) {
const result = validate(body.values, parameters);
@@ -440,7 +447,13 @@ export async function createRouter(
return app;
}
function parseBearerToken(header?: string): {
function parseBearerToken({
header,
logger,
}: {
header?: string;
logger: Logger;
}): {
token?: string;
entityRef?: string;
} {
@@ -472,8 +485,12 @@ function parseBearerToken(header?: string): {
throw new TypeError('Expected string sub claim');
}
// Check that it's a valid ref, otherwise this will throw.
parseEntityRef(sub);
return { entityRef: sub, token };
} catch (e) {
throw new InputError(`Invalid authorization header: ${stringifyError(e)}`);
logger.error(`Invalid authorization header: ${stringifyError(e)}`);
return {};
}
}