Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { FeatureDiscoveryService } from '@backstage/backend-plugin-api/alpha';
+import { ServiceFactory } from '@backstage/backend-plugin-api';
+
+// @alpha (undocumented)
+export const featureDiscoveryServiceFactory: () => ServiceFactory<
+ FeatureDiscoveryService,
+ 'root'
+>;
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md
index 5f452f9e03..0fa1ff376e 100644
--- a/packages/backend-app-api/api-report.md
+++ b/packages/backend-app-api/api-report.md
@@ -9,7 +9,6 @@ import type { AppConfig } from '@backstage/config';
import { BackendFeature } from '@backstage/backend-plugin-api';
import { CacheClient } from '@backstage/backend-common';
import { Config } from '@backstage/config';
-import { ConfigService } from '@backstage/backend-plugin-api';
import { CorsOptions } from 'cors';
import { ErrorRequestHandler } from 'express';
import { Express as Express_2 } from 'express';
@@ -30,6 +29,7 @@ import { PluginEndpointDiscovery } from '@backstage/backend-common';
import { RemoteConfigSourceOptions } from '@backstage/config-loader';
import { RequestHandler } from 'express';
import { RequestListener } from 'http';
+import { RootConfigService } from '@backstage/backend-plugin-api';
import { RootHttpRouterService } from '@backstage/backend-plugin-api';
import { RootLifecycleService } from '@backstage/backend-plugin-api';
import { RootLoggerService } from '@backstage/backend-plugin-api';
@@ -43,7 +43,7 @@ import { UrlReader } from '@backstage/backend-common';
// @public (undocumented)
export interface Backend {
// (undocumented)
- add(feature: BackendFeature): void;
+ add(feature: BackendFeature | (() => BackendFeature)): void;
// (undocumented)
start(): Promise
;
// (undocumented)
@@ -53,17 +53,6 @@ export interface Backend {
// @public (undocumented)
export const cacheServiceFactory: () => ServiceFactory;
-// @public (undocumented)
-export interface ConfigFactoryOptions {
- argv?: string[];
- remote?: Pick;
-}
-
-// @public (undocumented)
-export const configServiceFactory: (
- options?: ConfigFactoryOptions | undefined,
-) => ServiceFactory;
-
// @public (undocumented)
export function createConfigSecretEnumerator(options: {
logger: LoggerService;
@@ -92,7 +81,7 @@ export function createSpecializedBackend(
// @public (undocumented)
export interface CreateSpecializedBackendOptions {
// (undocumented)
- services: ServiceFactoryOrFunction[];
+ defaultServiceFactories: ServiceFactoryOrFunction[];
}
// @public (undocumented)
@@ -224,7 +213,7 @@ export interface MiddlewareFactoryErrorOptions {
// @public
export interface MiddlewareFactoryOptions {
// (undocumented)
- config: ConfigService;
+ config: RootConfigService;
// (undocumented)
logger: LoggerService;
}
@@ -244,12 +233,23 @@ export function readHelmetOptions(config?: Config): HelmetOptions;
// @public
export function readHttpServerOptions(config?: Config): HttpServerOptions;
+// @public (undocumented)
+export interface RootConfigFactoryOptions {
+ argv?: string[];
+ remote?: Pick;
+}
+
+// @public (undocumented)
+export const rootConfigServiceFactory: (
+ options?: RootConfigFactoryOptions | undefined,
+) => ServiceFactory;
+
// @public (undocumented)
export interface RootHttpRouterConfigureContext {
// (undocumented)
app: Express_2;
// (undocumented)
- config: ConfigService;
+ config: RootConfigService;
// (undocumented)
lifecycle: LifecycleService;
// (undocumented)
diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json
index 6f8a5cf9e8..82e32311aa 100644
--- a/packages/backend-app-api/package.json
+++ b/packages/backend-app-api/package.json
@@ -1,17 +1,30 @@
{
"name": "@backstage/backend-app-api",
"description": "Core API used by Backstage backend apps",
- "version": "0.4.6-next.0",
+ "version": "0.5.0",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
- "access": "public",
- "main": "dist/index.cjs.js",
- "types": "dist/index.d.ts"
+ "access": "public"
},
"backstage": {
"role": "node-library"
},
+ "exports": {
+ ".": "./src/index.ts",
+ "./alpha": "./src/alpha.ts",
+ "./package.json": "./package.json"
+ },
+ "typesVersions": {
+ "*": {
+ "alpha": [
+ "src/alpha.ts"
+ ],
+ "package.json": [
+ "package.json"
+ ]
+ }
+ },
"homepage": "https://backstage.io",
"repository": {
"type": "git",
@@ -36,6 +49,7 @@
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/backend-tasks": "workspace:^",
"@backstage/cli-common": "workspace:^",
+ "@backstage/cli-node": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/config-loader": "workspace:^",
"@backstage/errors": "workspace:^",
@@ -73,6 +87,7 @@
"@types/node-forge": "^1.3.0",
"@types/stoppable": "^1.1.0",
"http-errors": "^2.0.0",
+ "mock-fs": "^5.2.0",
"supertest": "^6.1.3"
},
"files": [
diff --git a/packages/backend-app-api/src/alpha.ts b/packages/backend-app-api/src/alpha.ts
new file mode 100644
index 0000000000..a334214738
--- /dev/null
+++ b/packages/backend-app-api/src/alpha.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2023 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.
+ */
+
+export { featureDiscoveryServiceFactory } from './alpha/featureDiscoveryServiceFactory';
diff --git a/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts b/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts
new file mode 100644
index 0000000000..5d4b1fc551
--- /dev/null
+++ b/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2023 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 mockFs from 'mock-fs';
+import { resolve as resolvePath, dirname } from 'path';
+import { startTestBackend, mockServices } from '@backstage/backend-test-utils';
+import { featureDiscoveryServiceFactory } from './featureDiscoveryServiceFactory';
+import {
+ coreServices,
+ createServiceFactory,
+} from '@backstage/backend-plugin-api';
+
+const rootDir = dirname(process.argv[1]);
+
+describe('featureDiscoveryServiceFactory', () => {
+ beforeEach(() => {
+ mockFs({
+ [rootDir]: {
+ 'package.json': JSON.stringify({
+ name: 'example-app',
+ dependencies: {
+ 'detected-plugin': '0.0.0',
+ 'detected-module': '0.0.0',
+ },
+ }),
+ },
+ [resolvePath(rootDir, 'node_modules/detected-plugin')]: {
+ 'package.json': JSON.stringify({
+ name: 'detected-plugin',
+ main: 'index.js',
+ backstage: {
+ role: 'backend-plugin',
+ },
+ }),
+ 'index.js': `
+ const { createBackendPlugin, coreServices } = require('@backstage/backend-plugin-api');
+ exports.detectedPlugin = createBackendPlugin({
+ pluginId: 'detected',
+ register(env) {
+ env.registerInit({
+ deps: { identity: coreServices.identity },
+ async init({ identity }) {
+ identity.getIdentity('detected-plugin');
+ },
+ });
+ },
+ });
+ `,
+ },
+ [resolvePath(rootDir, 'node_modules/detected-module')]: {
+ 'package.json': JSON.stringify({
+ name: 'detected-module',
+ main: 'index.js',
+ backstage: {
+ role: 'backend-module',
+ },
+ }),
+ 'index.js': `
+ const { createBackendModule, coreServices } = require('@backstage/backend-plugin-api');
+ exports.detectedModuleDerp = createBackendModule({
+ pluginId: 'detected',
+ moduleId: 'derp',
+ register(env) {
+ env.registerInit({
+ deps: { identity: coreServices.identity },
+ async init({ identity }) {
+ identity.getIdentity('detected-module');
+ },
+ });
+ },
+ });
+ `,
+ },
+ });
+ });
+
+ afterEach(() => {
+ mockFs.restore();
+ });
+
+ it('should detect plugin and module packages', async () => {
+ const fn = jest.fn().mockResolvedValue({});
+
+ await startTestBackend({
+ features: [
+ createServiceFactory({
+ service: coreServices.identity,
+ deps: {},
+ factory: () => ({ getIdentity: fn }),
+ }),
+ featureDiscoveryServiceFactory(),
+ mockServices.rootConfig.factory({
+ data: { backend: { packages: 'all' } },
+ }),
+ ],
+ });
+
+ expect(fn).toHaveBeenCalledWith('detected-plugin');
+ expect(fn).toHaveBeenCalledWith('detected-module');
+ });
+});
diff --git a/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.ts b/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.ts
new file mode 100644
index 0000000000..f9ee7edc5f
--- /dev/null
+++ b/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.ts
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2023 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 {
+ BackendFeature,
+ RootConfigService,
+ coreServices,
+ createServiceFactory,
+} from '@backstage/backend-plugin-api';
+import {
+ featureDiscoveryServiceRef,
+ FeatureDiscoveryService,
+} from '@backstage/backend-plugin-api/alpha';
+import { resolve as resolvePath, dirname } from 'path';
+import fs from 'fs-extra';
+import { BackstagePackageJson } from '@backstage/cli-node';
+
+const LOADED_PACKAGE_ROLES = ['backend-plugin', 'backend-module'];
+
+/** @internal */
+async function findClosestPackageDir(
+ searchDir: string,
+): Promise {
+ let path = searchDir;
+
+ // Some confidence check to avoid infinite loop
+ for (let i = 0; i < 1000; i++) {
+ const packagePath = resolvePath(path, 'package.json');
+ const exists = await fs.pathExists(packagePath);
+ if (exists) {
+ return path;
+ }
+
+ const newPath = dirname(path);
+ if (newPath === path) {
+ return undefined;
+ }
+ path = newPath;
+ }
+
+ throw new Error(
+ `Iteration limit reached when searching for root package.json at ${searchDir}`,
+ );
+}
+
+/** @internal */
+class PackageDiscoveryService implements FeatureDiscoveryService {
+ constructor(private readonly config: RootConfigService) {}
+
+ async getBackendFeatures(): Promise<{ features: Array }> {
+ if (this.config.getOptionalString('backend.packages') !== 'all') {
+ return { features: [] };
+ }
+
+ const packageDir = await findClosestPackageDir(process.argv[1]);
+ if (!packageDir) {
+ throw new Error('Package discovery failed to find package.json');
+ }
+ const { dependencies } = require(resolvePath(
+ packageDir,
+ 'package.json',
+ )) as BackstagePackageJson;
+ const dependencyNames = Object.keys(dependencies || {});
+
+ const features: BackendFeature[] = [];
+
+ for (const name of dependencyNames) {
+ const depPkg = require(require.resolve(`${name}/package.json`, {
+ paths: [packageDir],
+ })) as BackstagePackageJson;
+ if (!LOADED_PACKAGE_ROLES.includes(depPkg?.backstage?.role ?? '')) {
+ continue;
+ }
+ const depModule = require(require.resolve(name, { paths: [packageDir] }));
+ for (const exportValue of Object.values(depModule)) {
+ if (isBackendFeature(exportValue)) {
+ features.push(exportValue);
+ }
+ if (isBackendFeatureFactory(exportValue)) {
+ features.push(exportValue());
+ }
+ }
+ }
+
+ return { features };
+ }
+}
+
+/** @alpha */
+export const featureDiscoveryServiceFactory = createServiceFactory({
+ service: featureDiscoveryServiceRef,
+ deps: {
+ config: coreServices.rootConfig,
+ },
+ factory({ config }) {
+ return new PackageDiscoveryService(config);
+ },
+});
+
+function isBackendFeature(value: unknown): value is BackendFeature {
+ return (
+ !!value &&
+ typeof value === 'object' &&
+ (value as BackendFeature).$$type === '@backstage/BackendFeature'
+ );
+}
+
+function isBackendFeatureFactory(
+ value: unknown,
+): value is () => BackendFeature {
+ return (
+ !!value &&
+ typeof value === 'function' &&
+ (value as any).$$type === '@backstage/BackendFeatureFactory'
+ );
+}
diff --git a/packages/backend-app-api/src/config/ObservableConfigProxy.ts b/packages/backend-app-api/src/config/ObservableConfigProxy.ts
index dd3334c9a5..8c8d3b553f 100644
--- a/packages/backend-app-api/src/config/ObservableConfigProxy.ts
+++ b/packages/backend-app-api/src/config/ObservableConfigProxy.ts
@@ -14,12 +14,11 @@
* limitations under the License.
*/
-import { ConfigService } from '@backstage/backend-plugin-api';
-import { ConfigReader } from '@backstage/config';
+import { Config, ConfigReader } from '@backstage/config';
import { JsonValue } from '@backstage/types';
-export class ObservableConfigProxy implements ConfigService {
- private config: ConfigService = new ConfigReader({});
+export class ObservableConfigProxy implements Config {
+ private config: Config = new ConfigReader({});
private readonly subscribers: (() => void)[] = [];
@@ -32,7 +31,7 @@ export class ObservableConfigProxy implements ConfigService {
}
}
- setConfig(config: ConfigService) {
+ setConfig(config: Config) {
if (this.parent) {
throw new Error('immutable');
}
@@ -62,9 +61,9 @@ export class ObservableConfigProxy implements ConfigService {
};
}
- private select(required: true): ConfigService;
- private select(required: false): ConfigService | undefined;
- private select(required: boolean): ConfigService | undefined {
+ private select(required: true): Config;
+ private select(required: false): Config | undefined;
+ private select(required: boolean): Config | undefined {
if (this.parent && this.parentKey) {
if (required) {
return this.parent.select(true).getConfig(this.parentKey);
@@ -87,19 +86,19 @@ export class ObservableConfigProxy implements ConfigService {
getOptional(key?: string): T | undefined {
return this.select(false)?.getOptional(key);
}
- getConfig(key: string): ConfigService {
+ getConfig(key: string): Config {
return new ObservableConfigProxy(this, key);
}
- getOptionalConfig(key: string): ConfigService | undefined {
+ getOptionalConfig(key: string): Config | undefined {
if (this.select(false)?.has(key)) {
return new ObservableConfigProxy(this, key);
}
return undefined;
}
- getConfigArray(key: string): ConfigService[] {
+ getConfigArray(key: string): Config[] {
return this.select(true).getConfigArray(key);
}
- getOptionalConfigArray(key: string): ConfigService[] | undefined {
+ getOptionalConfigArray(key: string): Config[] | undefined {
return this.select(false)?.getOptionalConfigArray(key);
}
getNumber(key: string): number {
diff --git a/packages/backend-app-api/src/http/MiddlewareFactory.ts b/packages/backend-app-api/src/http/MiddlewareFactory.ts
index 774e784d1d..bd78d77045 100644
--- a/packages/backend-app-api/src/http/MiddlewareFactory.ts
+++ b/packages/backend-app-api/src/http/MiddlewareFactory.ts
@@ -14,7 +14,10 @@
* limitations under the License.
*/
-import { ConfigService, LoggerService } from '@backstage/backend-plugin-api';
+import {
+ RootConfigService,
+ LoggerService,
+} from '@backstage/backend-plugin-api';
import {
Request,
Response,
@@ -47,7 +50,7 @@ import { NotImplementedError } from '@backstage/errors';
* @public
*/
export interface MiddlewareFactoryOptions {
- config: ConfigService;
+ config: RootConfigService;
logger: LoggerService;
}
@@ -78,7 +81,7 @@ export interface MiddlewareFactoryErrorOptions {
* @public
*/
export class MiddlewareFactory {
- #config: ConfigService;
+ #config: RootConfigService;
#logger: LoggerService;
/**
diff --git a/packages/backend-app-api/src/lib/DependencyGraph.test.ts b/packages/backend-app-api/src/lib/DependencyGraph.test.ts
new file mode 100644
index 0000000000..65efac0252
--- /dev/null
+++ b/packages/backend-app-api/src/lib/DependencyGraph.test.ts
@@ -0,0 +1,245 @@
+/*
+ * Copyright 2023 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 { DependencyGraph } from './DependencyGraph';
+
+describe('DependencyGraph', () => {
+ it('should be empty', async () => {
+ const empty = DependencyGraph.fromMap({});
+ expect(empty.findUnsatisfiedDeps()).toEqual([]);
+ expect(empty.detectCircularDependency()).toBeUndefined();
+ await expect(
+ empty.parallelTopologicalTraversal(async id => id),
+ ).resolves.toEqual([]);
+ });
+
+ describe('detectCircularDependency', () => {
+ it('should return undefined with not deps', () => {
+ expect(
+ DependencyGraph.fromMap({
+ 1: {},
+ 2: {},
+ 3: {},
+ 4: {},
+ }).detectCircularDependency(),
+ ).toBeUndefined();
+ });
+
+ it('should return undefined with no circular deps', async () => {
+ expect(
+ DependencyGraph.fromMap({
+ 1: { provides: ['a'] },
+ 2: { consumes: ['a'], provides: ['b', 'c'] },
+ 3: { consumes: ['b'] },
+ 4: { consumes: ['c'] },
+ }).detectCircularDependency(),
+ ).toBeUndefined();
+ });
+
+ it('should detect an immediate circular dep', async () => {
+ expect(
+ DependencyGraph.fromMap({
+ 1: { provides: ['a'], consumes: ['a'] },
+ }).detectCircularDependency(),
+ ).toEqual(['1', '1']);
+ });
+
+ it('should detect a small circular dep', async () => {
+ expect(
+ DependencyGraph.fromMap({
+ 1: { provides: ['a'], consumes: ['b'] },
+ 2: { provides: ['b'], consumes: ['a'] },
+ }).detectCircularDependency(),
+ ).toEqual(['1', '2', '1']);
+ });
+
+ it('should detect a larger distant circular dep', async () => {
+ expect(
+ DependencyGraph.fromMap({
+ 1: { provides: ['a'] },
+ 2: { provides: ['b'], consumes: ['a', 'e'] },
+ 3: { provides: ['c'], consumes: ['b'] },
+ 4: { provides: ['d', 'e'], consumes: ['c', 'a'] },
+ }).detectCircularDependency(),
+ ).toEqual(['2', '3', '4', '2']);
+ });
+ });
+
+ describe('findUnsatisfiedDeps', () => {
+ it('should return nothing with no deps', () => {
+ expect(
+ DependencyGraph.fromMap({
+ 1: {},
+ 2: {},
+ 3: {},
+ 4: {},
+ }).findUnsatisfiedDeps(),
+ ).toEqual([]);
+ });
+
+ it('should return nothing when all deps are satisfied', async () => {
+ expect(
+ DependencyGraph.fromMap({
+ 1: { provides: ['a'] },
+ 2: { consumes: ['a'], provides: ['b', 'c'] },
+ 3: { consumes: ['b'] },
+ 4: { consumes: ['c'] },
+ }).findUnsatisfiedDeps(),
+ ).toEqual([]);
+ });
+
+ it('should find a single unsatisfied dep', async () => {
+ expect(
+ DependencyGraph.fromMap({
+ 1: { consumes: ['a'] },
+ }).findUnsatisfiedDeps(),
+ ).toEqual([{ value: '1', unsatisfied: ['a'] }]);
+ });
+
+ it('should handle circular dependencies', async () => {
+ expect(
+ DependencyGraph.fromMap({
+ 1: { consumes: ['a'], provides: ['a'] },
+ }).findUnsatisfiedDeps(),
+ ).toEqual([]);
+
+ expect(
+ DependencyGraph.fromMap({
+ 1: { consumes: ['a'], provides: ['b'] },
+ 2: { consumes: ['b'], provides: ['a'] },
+ }).findUnsatisfiedDeps(),
+ ).toEqual([]);
+
+ expect(
+ DependencyGraph.fromMap({
+ 1: { consumes: ['a'] },
+ 2: { consumes: ['b'], provides: ['c'] },
+ 3: { consumes: ['c'], provides: ['a', 'b'] },
+ }).findUnsatisfiedDeps(),
+ ).toEqual([]);
+ });
+
+ it('should find multiple unsatisfied deps for one node', async () => {
+ expect(
+ DependencyGraph.fromMap({
+ 1: { provides: ['a'], consumes: ['b'] },
+ 2: { provides: ['b'], consumes: ['a', 'd', 'e'] },
+ }).findUnsatisfiedDeps(),
+ ).toEqual([{ value: '2', unsatisfied: ['d', 'e'] }]);
+ });
+
+ it('should find multiple unsatisfied deps for multiple nodes', async () => {
+ expect(
+ DependencyGraph.fromMap({
+ 1: { provides: ['a'] },
+ 2: { provides: ['b'], consumes: ['a', 'd', 'e'] },
+ 3: { provides: [], consumes: ['b'] },
+ 4: { provides: [], consumes: ['c', 'a'] },
+ }).findUnsatisfiedDeps(),
+ ).toEqual([
+ { value: '2', unsatisfied: ['d', 'e'] },
+ { value: '4', unsatisfied: ['c'] },
+ ]);
+ });
+ });
+
+ describe('parallelTopologicalTraversal', () => {
+ it('should traverse with no deps', async () => {
+ await expect(
+ DependencyGraph.fromMap({
+ 1: {},
+ 2: {},
+ 3: {},
+ 4: {},
+ }).parallelTopologicalTraversal(async id => id),
+ ).resolves.toEqual(['1', '2', '3', '4']);
+ });
+
+ it('should traverse with a few deps', async () => {
+ await expect(
+ DependencyGraph.fromMap({
+ 1: { provides: ['a'] },
+ 2: { consumes: ['a'], provides: ['b', 'c'] },
+ 3: { consumes: ['b'] },
+ 4: { consumes: ['c'] },
+ }).parallelTopologicalTraversal(async id => id),
+ ).resolves.toEqual(['1', '2', '3', '4']);
+ });
+
+ it('should traverse in reverse', async () => {
+ await expect(
+ DependencyGraph.fromMap({
+ 1: { consumes: ['c'] },
+ 2: { provides: ['c'], consumes: ['b'] },
+ 3: { provides: ['b'], consumes: ['a'] },
+ 4: { provides: ['a'] },
+ }).parallelTopologicalTraversal(async id => id),
+ ).resolves.toEqual(['4', '3', '2', '1']);
+ });
+
+ it('should execute in parallel', async () => {
+ await expect(
+ DependencyGraph.fromMap({
+ 1: { provides: ['a'] },
+ 2: { provides: ['b'], consumes: ['a'] },
+ 3: { provides: ['c'], consumes: ['a'] },
+ 4: { consumes: ['b'] },
+ 5: { consumes: ['c'] },
+ }).parallelTopologicalTraversal(async id => id),
+ ).resolves.toEqual(['1', '2', '3', '4', '5']);
+
+ // Same as above, but with 2 being delayed
+ await expect(
+ DependencyGraph.fromMap({
+ 1: { provides: ['a'] },
+ 2: { provides: ['b'], consumes: ['a'] },
+ 3: { provides: ['c'], consumes: ['a'] },
+ 4: { consumes: ['b'] },
+ 5: { consumes: ['c'] },
+ }).parallelTopologicalTraversal(async id => {
+ // When delaying 2 we expect 3 and 5 to complete before 2 and 4
+ if (id === '2') {
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
+ return id;
+ }),
+ ).resolves.toEqual(['1', '3', '5', '2', '4']);
+ });
+
+ it('should detect circular dependencies', async () => {
+ await expect(
+ DependencyGraph.fromMap({
+ 1: { provides: ['a'], consumes: ['a'] },
+ }).parallelTopologicalTraversal(async id => id),
+ ).rejects.toThrow('Circular dependency detected');
+
+ await expect(
+ DependencyGraph.fromMap({
+ 1: { provides: ['a'], consumes: ['b'] },
+ 2: { provides: ['b'], consumes: ['a'] },
+ }).parallelTopologicalTraversal(async id => id),
+ ).rejects.toThrow('Circular dependency detected');
+
+ await expect(
+ DependencyGraph.fromMap({
+ 1: { provides: ['a'] },
+ 2: { provides: ['c'], consumes: ['a', 'b'] },
+ 3: { provides: ['b'], consumes: ['a', 'c'] },
+ }).parallelTopologicalTraversal(async id => id),
+ ).rejects.toThrow('Circular dependency detected');
+ });
+ });
+});
diff --git a/packages/backend-app-api/src/lib/DependencyGraph.ts b/packages/backend-app-api/src/lib/DependencyGraph.ts
new file mode 100644
index 0000000000..8fd679f555
--- /dev/null
+++ b/packages/backend-app-api/src/lib/DependencyGraph.ts
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2023 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.
+ */
+
+interface NodeInput {
+ value: T;
+ consumes?: Iterable;
+ provides?: Iterable;
+}
+
+/** @internal */
+class Node {
+ static from(input: NodeInput) {
+ return new Node(
+ input.value,
+ input.consumes ? new Set(input.consumes) : new Set(),
+ input.provides ? new Set(input.provides) : new Set(),
+ );
+ }
+
+ private constructor(
+ readonly value: T,
+ readonly consumes: Set,
+ readonly provides: Set,
+ ) {}
+}
+
+/**
+ * Internal helper to help validate and traverse a dependency graph.
+ * @internal
+ */
+export class DependencyGraph {
+ static fromMap(
+ nodes: Record, 'value'>>,
+ ): DependencyGraph {
+ return this.fromIterable(
+ Object.entries(nodes).map(([key, node]) => ({
+ value: String(key),
+ ...node,
+ })),
+ );
+ }
+
+ static fromIterable(
+ nodeInputs: Iterable>,
+ ): DependencyGraph {
+ const nodes = new Array>();
+ for (const nodeInput of nodeInputs) {
+ nodes.push(Node.from(nodeInput));
+ }
+
+ return new DependencyGraph(nodes);
+ }
+
+ #nodes: Array>;
+ #allProvided: Set;
+
+ private constructor(nodes: Array>) {
+ this.#nodes = nodes;
+ this.#allProvided = new Set();
+
+ for (const node of this.#nodes.values()) {
+ for (const produced of node.provides) {
+ this.#allProvided.add(produced);
+ }
+ }
+ }
+
+ // Find all nodes that consume dependencies that are not provided by any other node
+ findUnsatisfiedDeps(): Array<{ value: T; unsatisfied: string[] }> {
+ const unsatisfiedDependencies = [];
+ for (const node of this.#nodes.values()) {
+ const unsatisfied = Array.from(node.consumes).filter(
+ id => !this.#allProvided.has(id),
+ );
+ if (unsatisfied.length > 0) {
+ unsatisfiedDependencies.push({ value: node.value, unsatisfied });
+ }
+ }
+ return unsatisfiedDependencies;
+ }
+
+ // Detect circular dependencies within the graph, returning the path of nodes that
+ // form a cycle, with the same node as the first and last element of the array.
+ detectCircularDependency(): T[] | undefined {
+ for (const startNode of this.#nodes) {
+ const visited = new Set>();
+ const stack = new Array<[node: Node, path: T[]]>([
+ startNode,
+ [startNode.value],
+ ]);
+
+ while (stack.length > 0) {
+ const [node, path] = stack.pop()!;
+ if (visited.has(node)) {
+ continue;
+ }
+ visited.add(node);
+ for (const produced of node.provides) {
+ const consumerNodes = this.#nodes.filter(other =>
+ other.consumes.has(produced),
+ );
+ for (const consumer of consumerNodes) {
+ if (consumer === startNode) {
+ return [...path, startNode.value];
+ }
+ if (!visited.has(consumer)) {
+ stack.push([consumer, [...path, consumer.value]]);
+ }
+ }
+ }
+ }
+ }
+ return undefined;
+ }
+
+ /**
+ * Traverses the dependency graph in topological order, calling the provided
+ * function for each node and waiting for it to resolve.
+ *
+ * The nodes are traversed in parallel, but in such a way that no node is
+ * visited before all of its dependencies.
+ *
+ * Dependencies of nodes that are not produced by any other nodes will be ignored.
+ */
+ async parallelTopologicalTraversal(
+ fn: (value: T) => Promise,
+ ): Promise {
+ const allProvided = this.#allProvided;
+ const producedSoFar = new Set();
+ const waiting = new Set(this.#nodes.values());
+ const visited = new Set>();
+ const results = new Array();
+ let inFlight = 0; // Keep track of how many callbacks are in flight, so that we know if we got stuck
+
+ // Find all nodes that have no dependencies that have not already been produced by visited nodes
+ async function processMoreNodes() {
+ if (waiting.size === 0) {
+ return;
+ }
+ const nodesToProcess = [];
+ for (const node of waiting) {
+ let ready = true;
+ for (const consumed of node.consumes) {
+ if (allProvided.has(consumed) && !producedSoFar.has(consumed)) {
+ ready = false;
+ continue;
+ }
+ }
+ if (ready) {
+ nodesToProcess.push(node);
+ }
+ }
+
+ for (const node of nodesToProcess) {
+ waiting.delete(node);
+ }
+
+ if (nodesToProcess.length === 0 && inFlight === 0) {
+ // We expect the caller to check for circular dependencies before
+ // traversal, so this error should never happen
+ throw new Error('Circular dependency detected');
+ }
+
+ await Promise.all(nodesToProcess.map(processNode));
+ }
+
+ // Process an individual node, and then add its produced dependencies to the set of available products
+ async function processNode(node: Node) {
+ visited.add(node);
+ inFlight += 1;
+
+ const result = await fn(node.value);
+ results.push(result);
+
+ node.provides.forEach(produced => producedSoFar.add(produced));
+ inFlight -= 1;
+ await processMoreNodes();
+ }
+
+ await processMoreNodes();
+
+ return results;
+ }
+}
diff --git a/packages/backend-app-api/src/services/implementations/cache/cacheServiceFactory.ts b/packages/backend-app-api/src/services/implementations/cache/cacheServiceFactory.ts
index 2208249280..b91356a13a 100644
--- a/packages/backend-app-api/src/services/implementations/cache/cacheServiceFactory.ts
+++ b/packages/backend-app-api/src/services/implementations/cache/cacheServiceFactory.ts
@@ -24,7 +24,7 @@ import {
export const cacheServiceFactory = createServiceFactory({
service: coreServices.cache,
deps: {
- config: coreServices.config,
+ config: coreServices.rootConfig,
plugin: coreServices.pluginMetadata,
},
async createRootContext({ config }) {
diff --git a/packages/backend-app-api/src/services/implementations/config/index.ts b/packages/backend-app-api/src/services/implementations/config/index.ts
index 690f1e2d36..1775ef2efc 100644
--- a/packages/backend-app-api/src/services/implementations/config/index.ts
+++ b/packages/backend-app-api/src/services/implementations/config/index.ts
@@ -14,5 +14,5 @@
* limitations under the License.
*/
-export { configServiceFactory } from './configServiceFactory';
-export type { ConfigFactoryOptions } from './configServiceFactory';
+export { rootConfigServiceFactory } from './rootConfigServiceFactory';
+export type { RootConfigFactoryOptions } from './rootConfigServiceFactory';
diff --git a/packages/backend-app-api/src/services/implementations/config/configServiceFactory.ts b/packages/backend-app-api/src/services/implementations/config/rootConfigServiceFactory.ts
similarity index 87%
rename from packages/backend-app-api/src/services/implementations/config/configServiceFactory.ts
rename to packages/backend-app-api/src/services/implementations/config/rootConfigServiceFactory.ts
index d3b29ce545..1fa8d792ad 100644
--- a/packages/backend-app-api/src/services/implementations/config/configServiceFactory.ts
+++ b/packages/backend-app-api/src/services/implementations/config/rootConfigServiceFactory.ts
@@ -24,7 +24,7 @@ import {
} from '@backstage/config-loader';
/** @public */
-export interface ConfigFactoryOptions {
+export interface RootConfigFactoryOptions {
/**
* Process arguments to use instead of the default `process.argv()`.
*/
@@ -37,9 +37,9 @@ export interface ConfigFactoryOptions {
}
/** @public */
-export const configServiceFactory = createServiceFactory(
- (options?: ConfigFactoryOptions) => ({
- service: coreServices.config,
+export const rootConfigServiceFactory = createServiceFactory(
+ (options?: RootConfigFactoryOptions) => ({
+ service: coreServices.rootConfig,
deps: {},
async factory() {
const source = ConfigSources.default({
diff --git a/packages/backend-app-api/src/services/implementations/database/databaseServiceFactory.ts b/packages/backend-app-api/src/services/implementations/database/databaseServiceFactory.ts
index 05e2f1deb9..139609b6c1 100644
--- a/packages/backend-app-api/src/services/implementations/database/databaseServiceFactory.ts
+++ b/packages/backend-app-api/src/services/implementations/database/databaseServiceFactory.ts
@@ -25,7 +25,7 @@ import { ConfigReader } from '@backstage/config';
export const databaseServiceFactory = createServiceFactory({
service: coreServices.database,
deps: {
- config: coreServices.config,
+ config: coreServices.rootConfig,
lifecycle: coreServices.lifecycle,
pluginMetadata: coreServices.pluginMetadata,
},
diff --git a/packages/backend-app-api/src/services/implementations/discovery/discoveryServiceFactory.ts b/packages/backend-app-api/src/services/implementations/discovery/discoveryServiceFactory.ts
index 7e2bb6b21e..6bdc4b4856 100644
--- a/packages/backend-app-api/src/services/implementations/discovery/discoveryServiceFactory.ts
+++ b/packages/backend-app-api/src/services/implementations/discovery/discoveryServiceFactory.ts
@@ -24,7 +24,7 @@ import {
export const discoveryServiceFactory = createServiceFactory({
service: coreServices.discovery,
deps: {
- config: coreServices.config,
+ config: coreServices.rootConfig,
},
async factory({ config }) {
return HostDiscovery.fromConfig(config);
diff --git a/packages/backend-app-api/src/services/implementations/permissions/permissionsServiceFactory.ts b/packages/backend-app-api/src/services/implementations/permissions/permissionsServiceFactory.ts
index 38cba73eda..3e5b7da9dd 100644
--- a/packages/backend-app-api/src/services/implementations/permissions/permissionsServiceFactory.ts
+++ b/packages/backend-app-api/src/services/implementations/permissions/permissionsServiceFactory.ts
@@ -24,7 +24,7 @@ import { ServerPermissionClient } from '@backstage/plugin-permission-node';
export const permissionsServiceFactory = createServiceFactory({
service: coreServices.permissions,
deps: {
- config: coreServices.config,
+ config: coreServices.rootConfig,
discovery: coreServices.discovery,
tokenManager: coreServices.tokenManager,
},
diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterServiceFactory.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterServiceFactory.ts
index 5eaf8aa152..4c410e25ea 100644
--- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterServiceFactory.ts
+++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterServiceFactory.ts
@@ -15,7 +15,7 @@
*/
import {
- ConfigService,
+ RootConfigService,
coreServices,
createServiceFactory,
LifecycleService,
@@ -36,7 +36,7 @@ export interface RootHttpRouterConfigureContext {
app: Express;
middleware: MiddlewareFactory;
routes: RequestHandler;
- config: ConfigService;
+ config: RootConfigService;
logger: LoggerService;
lifecycle: LifecycleService;
}
@@ -70,7 +70,7 @@ export const rootHttpRouterServiceFactory = createServiceFactory(
(options?: RootHttpRouterFactoryOptions) => ({
service: coreServices.rootHttpRouter,
deps: {
- config: coreServices.config,
+ config: coreServices.rootConfig,
rootLogger: coreServices.rootLogger,
lifecycle: coreServices.rootLifecycle,
},
diff --git a/packages/backend-app-api/src/services/implementations/rootLogger/rootLoggerServiceFactory.ts b/packages/backend-app-api/src/services/implementations/rootLogger/rootLoggerServiceFactory.ts
index 11bbd58050..bd45df383f 100644
--- a/packages/backend-app-api/src/services/implementations/rootLogger/rootLoggerServiceFactory.ts
+++ b/packages/backend-app-api/src/services/implementations/rootLogger/rootLoggerServiceFactory.ts
@@ -26,7 +26,7 @@ import { createConfigSecretEnumerator } from '../../../config';
export const rootLoggerServiceFactory = createServiceFactory({
service: coreServices.rootLogger,
deps: {
- config: coreServices.config,
+ config: coreServices.rootConfig,
},
async factory({ config }) {
const logger = WinstonLogger.create({
diff --git a/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts b/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts
index 7af9de44da..d771c5dd11 100644
--- a/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts
+++ b/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts
@@ -61,8 +61,7 @@ describe('schedulerFactory', () => {
});
await startTestBackend({
- features: [plugin()],
- services: [subject],
+ features: [plugin(), subject],
});
});
});
diff --git a/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.ts b/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.ts
index b3726176b6..7417c18e23 100644
--- a/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.ts
+++ b/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.ts
@@ -24,7 +24,7 @@ import { ServerTokenManager } from '@backstage/backend-common';
export const tokenManagerServiceFactory = createServiceFactory({
service: coreServices.tokenManager,
deps: {
- config: coreServices.config,
+ config: coreServices.rootConfig,
logger: coreServices.rootLogger,
},
createRootContext({ config, logger }) {
diff --git a/packages/backend-app-api/src/services/implementations/urlReader/urlReaderServiceFactory.ts b/packages/backend-app-api/src/services/implementations/urlReader/urlReaderServiceFactory.ts
index 83e594aa0b..6da71ac0fc 100644
--- a/packages/backend-app-api/src/services/implementations/urlReader/urlReaderServiceFactory.ts
+++ b/packages/backend-app-api/src/services/implementations/urlReader/urlReaderServiceFactory.ts
@@ -24,7 +24,7 @@ import {
export const urlReaderServiceFactory = createServiceFactory({
service: coreServices.urlReader,
deps: {
- config: coreServices.config,
+ config: coreServices.rootConfig,
logger: coreServices.logger,
},
async factory({ config, logger }) {
diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts
index b05194f594..791b9b1c3a 100644
--- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts
+++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts
@@ -20,10 +20,15 @@ import {
coreServices,
createBackendPlugin,
createBackendModule,
+ createExtensionPoint,
} from '@backstage/backend-plugin-api';
import { BackendInitializer } from './BackendInitializer';
-import { ServiceRegistry } from './ServiceRegistry';
-import { rootLifecycleServiceFactory } from '../services/implementations';
+
+import {
+ lifecycleServiceFactory,
+ loggerServiceFactory,
+ rootLifecycleServiceFactory,
+} from '../services/implementations';
const rootRef = createServiceRef<{ x: number }>({
id: '1',
@@ -44,12 +49,23 @@ class MockLogger {
}
}
+const baseFactories = [
+ lifecycleServiceFactory(),
+ rootLifecycleServiceFactory(),
+ createServiceFactory({
+ service: coreServices.rootLogger,
+ deps: {},
+ factory: () => new MockLogger(),
+ })(),
+ loggerServiceFactory(),
+];
+
describe('BackendInitializer', () => {
it('should initialize root scoped services', async () => {
const rootFactory = jest.fn();
const pluginFactory = jest.fn();
- const registry = new ServiceRegistry([
+ const services = [
createServiceFactory({
service: rootRef,
deps: {},
@@ -66,17 +82,76 @@ describe('BackendInitializer', () => {
deps: {},
factory: () => new MockLogger(),
})(),
- ]);
+ ];
- const init = new BackendInitializer(registry);
+ const init = new BackendInitializer(services);
await init.start();
expect(rootFactory).toHaveBeenCalled();
expect(pluginFactory).not.toHaveBeenCalled();
});
+ it('should initialize modules with extension points', async () => {
+ expect.assertions(3);
+
+ const extensionPoint = createExtensionPoint<{ values: string[] }>({
+ id: 'a',
+ });
+ const init = new BackendInitializer(baseFactories);
+
+ init.add(
+ createBackendModule({
+ pluginId: 'test',
+ moduleId: 'modA',
+ register(reg) {
+ reg.registerInit({
+ deps: { extension: extensionPoint },
+ async init({ extension }) {
+ expect(extension.values).toEqual(['b']);
+ extension.values.push('a');
+ },
+ });
+ },
+ })(),
+ );
+
+ init.add(
+ createBackendModule({
+ pluginId: 'test',
+ moduleId: 'modB',
+ register(reg) {
+ const values = ['b'];
+ reg.registerExtensionPoint(extensionPoint, { values });
+ reg.registerInit({
+ deps: {},
+ async init() {
+ expect(values).toEqual(['b', 'a', 'c']);
+ },
+ });
+ },
+ })(),
+ );
+
+ init.add(
+ createBackendModule({
+ pluginId: 'test',
+ moduleId: 'modC',
+ register(reg) {
+ reg.registerInit({
+ deps: { extension: extensionPoint },
+ async init({ extension }) {
+ expect(extension.values).toEqual(['b', 'a']);
+ extension.values.push('c');
+ },
+ });
+ },
+ })(),
+ );
+ await init.start();
+ });
+
it('should forward errors when plugins fail to start', async () => {
- const init = new BackendInitializer(new ServiceRegistry([]));
+ const init = new BackendInitializer([]);
init.add(
createBackendPlugin({
pluginId: 'test',
@@ -96,7 +171,7 @@ describe('BackendInitializer', () => {
});
it('should forward errors when modules fail to start', async () => {
- const init = new BackendInitializer(new ServiceRegistry([]));
+ const init = new BackendInitializer([]);
init.add(
createBackendModule({
pluginId: 'test',
@@ -117,7 +192,7 @@ describe('BackendInitializer', () => {
});
it('should reject duplicate plugins', async () => {
- const init = new BackendInitializer(new ServiceRegistry([]));
+ const init = new BackendInitializer([]);
init.add(
createBackendPlugin({
pluginId: 'test',
@@ -146,7 +221,7 @@ describe('BackendInitializer', () => {
});
it('should reject duplicate modules', async () => {
- const init = new BackendInitializer(new ServiceRegistry([]));
+ const init = new BackendInitializer([]);
init.add(
createBackendModule({
pluginId: 'test',
@@ -175,4 +250,78 @@ describe('BackendInitializer', () => {
"Module 'mod' for plugin 'test' is already registered",
);
});
+
+ it('should reject modules with circular dependencies', async () => {
+ const extA = createExtensionPoint({ id: 'a' });
+ const extB = createExtensionPoint({ id: 'b' });
+ const init = new BackendInitializer([
+ rootLifecycleServiceFactory(),
+ createServiceFactory({
+ service: coreServices.rootLogger,
+ deps: {},
+ factory: () => new MockLogger(),
+ })(),
+ ]);
+ init.add(
+ createBackendModule({
+ pluginId: 'test',
+ moduleId: 'modA',
+ register(reg) {
+ reg.registerExtensionPoint(extA, 'a');
+ reg.registerInit({
+ deps: { ext: extB },
+ async init() {},
+ });
+ },
+ })(),
+ );
+ init.add(
+ createBackendModule({
+ pluginId: 'test',
+ moduleId: 'modB',
+ register(reg) {
+ reg.registerExtensionPoint(extB, 'b');
+ reg.registerInit({
+ deps: { ext: extA },
+ async init() {},
+ });
+ },
+ })(),
+ );
+ await expect(init.start()).rejects.toThrow(
+ "Circular dependency detected for modules of plugin 'test', 'modA' -> 'modB' -> 'modA'",
+ );
+ });
+
+ it('should reject modules that depend on extension points other plugins', async () => {
+ const init = new BackendInitializer(baseFactories);
+ const extA = createExtensionPoint({ id: 'a' });
+ init.add(
+ createBackendPlugin({
+ pluginId: 'testA',
+ register(reg) {
+ reg.registerExtensionPoint(extA, 'a');
+ reg.registerInit({
+ deps: {},
+ async init() {},
+ });
+ },
+ })(),
+ );
+ init.add(
+ createBackendModule({
+ pluginId: 'testB',
+ moduleId: 'mod',
+ register(reg) {
+ reg.registerInit({
+ deps: { ext: extA },
+ async init() {},
+ });
+ },
+ })(),
+ );
+ await expect(init.start()).rejects.toThrow(
+ "Extension point registered for plugin 'testA' may not be used by module for plugin 'testB'",
+ );
+ });
});
diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts
index 8c2b75e887..93d2f6942a 100644
--- a/packages/backend-app-api/src/wiring/BackendInitializer.ts
+++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts
@@ -19,6 +19,7 @@ import {
ExtensionPoint,
coreServices,
ServiceRef,
+ ServiceFactory,
} from '@backstage/backend-plugin-api';
import { BackendLifecycleImpl } from '../services/implementations/rootLifecycle/rootLifecycleServiceFactory';
import { BackendPluginLifecycleImpl } from '../services/implementations/lifecycle/lifecycleServiceFactory';
@@ -26,7 +27,10 @@ import { EnumerableServiceHolder, ServiceOrExtensionPoint } from './types';
// Direct internal import to avoid duplication
// eslint-disable-next-line @backstage/no-forbidden-package-imports
import { InternalBackendFeature } from '@backstage/backend-plugin-api/src/wiring/types';
-import { ForwardedError } from '@backstage/errors';
+import { ForwardedError, ConflictError } from '@backstage/errors';
+import { featureDiscoveryServiceRef } from '@backstage/backend-plugin-api/alpha';
+import { DependencyGraph } from '../lib/DependencyGraph';
+import { ServiceRegistry } from './ServiceRegistry';
export interface BackendRegisterInit {
consumes: Set;
@@ -40,11 +44,16 @@ export interface BackendRegisterInit {
export class BackendInitializer {
#startPromise?: Promise;
#features = new Array();
- #extensionPoints = new Map, unknown>();
- #serviceHolder: EnumerableServiceHolder;
+ #extensionPoints = new Map<
+ ExtensionPoint,
+ { impl: unknown; pluginId: string }
+ >();
+ #serviceHolder: EnumerableServiceHolder | undefined;
+ #providedServiceFactories = new Array();
+ #defaultApiFactories: ServiceFactory[];
- constructor(serviceHolder: EnumerableServiceHolder) {
- this.#serviceHolder = serviceHolder;
+ constructor(defaultApiFactories: ServiceFactory[]) {
+ this.#defaultApiFactories = defaultApiFactories;
}
async #getInitDeps(
@@ -55,13 +64,16 @@ export class BackendInitializer {
const missingRefs = new Set();
for (const [name, ref] of Object.entries(deps)) {
- const extensionPoint = this.#extensionPoints.get(
- ref as ExtensionPoint,
- );
- if (extensionPoint) {
- result.set(name, extensionPoint);
+ const ep = this.#extensionPoints.get(ref as ExtensionPoint);
+ if (ep) {
+ if (ep.pluginId !== pluginId) {
+ throw new Error(
+ `Extension point registered for plugin '${ep.pluginId}' may not be used by module for plugin '${pluginId}'`,
+ );
+ }
+ result.set(name, ep.impl);
} else {
- const impl = await this.#serviceHolder.get(
+ const impl = await this.#serviceHolder!.get(
ref as ServiceRef,
pluginId,
);
@@ -87,18 +99,44 @@ export class BackendInitializer {
if (this.#startPromise) {
throw new Error('feature can not be added after the backend has started');
}
+ this.#addFeature(feature);
+ }
+
+ #addFeature(feature: BackendFeature) {
if (feature.$$type !== '@backstage/BackendFeature') {
throw new Error(
`Failed to add feature, invalid type '${feature.$$type}'`,
);
}
- const internalFeature = feature as InternalBackendFeature;
- if (internalFeature.version !== 'v1') {
+
+ if (isServiceFactory(feature)) {
+ if (feature.service.id === coreServices.pluginMetadata.id) {
+ throw new Error(
+ `The ${coreServices.pluginMetadata.id} service cannot be overridden`,
+ );
+ }
+ if (
+ this.#providedServiceFactories.find(
+ sf => sf.service.id === feature.service.id,
+ )
+ ) {
+ throw new Error(
+ `Duplicate service implementations provided for ${feature.service.id}`,
+ );
+ }
+ this.#providedServiceFactories.push(feature);
+ } else if (isInternalBackendFeature(feature)) {
+ if (feature.version !== 'v1') {
+ throw new Error(
+ `Failed to add feature, invalid version '${feature.version}'`,
+ );
+ }
+ this.#features.push(feature);
+ } else {
throw new Error(
- `Failed to add feature, invalid version '${internalFeature.version}'`,
+ `Failed to add feature, invalid feature ${JSON.stringify(feature)}`,
);
}
- this.#features.push(internalFeature);
}
async start(): Promise {
@@ -129,6 +167,23 @@ export class BackendInitializer {
}
async #doStart(): Promise {
+ this.#serviceHolder = new ServiceRegistry([
+ ...this.#defaultApiFactories,
+ ...this.#providedServiceFactories,
+ ]);
+
+ const featureDiscovery = await this.#serviceHolder.get(
+ featureDiscoveryServiceRef,
+ 'root',
+ );
+
+ if (featureDiscovery) {
+ const { features } = await featureDiscovery.getBackendFeatures();
+ for (const feature of features) {
+ this.#addFeature(feature);
+ }
+ }
+
// Initialize all root scoped services
for (const ref of this.#serviceHolder.getServiceRefs()) {
if (ref.scope === 'root') {
@@ -144,14 +199,17 @@ export class BackendInitializer {
for (const r of feature.getRegistrations()) {
const provides = new Set>();
- if (r.type === 'plugin') {
+ if (r.type === 'plugin' || r.type === 'module') {
for (const [extRef, extImpl] of r.extensionPoints) {
if (this.#extensionPoints.has(extRef)) {
throw new Error(
`ExtensionPoint with ID '${extRef.id}' is already registered`,
);
}
- this.#extensionPoints.set(extRef, extImpl);
+ this.#extensionPoints.set(extRef, {
+ impl: extImpl,
+ pluginId: r.pluginId,
+ });
provides.add(extRef);
}
}
@@ -193,21 +251,41 @@ export class BackendInitializer {
await Promise.all(
allPluginIds.map(async pluginId => {
// Modules are initialized before plugins, so that they can provide extension to the plugin
- const modules = moduleInits.get(pluginId) ?? [];
- await Promise.all(
- Array.from(modules).map(async ([moduleId, moduleInit]) => {
- const moduleDeps = await this.#getInitDeps(
- moduleInit.init.deps,
- pluginId,
+ const modules = moduleInits.get(pluginId);
+ if (modules) {
+ const tree = DependencyGraph.fromIterable(
+ Array.from(modules).map(([moduleId, moduleInit]) => ({
+ value: { moduleId, moduleInit },
+ // Relationships are reversed at this point since we're only interested in the extension points.
+ // If a modules provides extension point A we want it to be initialized AFTER all modules
+ // that depend on extension point A, so that they can provide their extensions.
+ consumes: Array.from(moduleInit.provides).map(p => p.id),
+ provides: Array.from(moduleInit.consumes).map(c => c.id),
+ })),
+ );
+ const circular = tree.detectCircularDependency();
+ if (circular) {
+ throw new ConflictError(
+ `Circular dependency detected for modules of plugin '${pluginId}', ${circular
+ .map(({ moduleId }) => `'${moduleId}'`)
+ .join(' -> ')}`,
);
- await moduleInit.init.func(moduleDeps).catch(error => {
- throw new ForwardedError(
- `Module '${moduleId}' for plugin '${pluginId}' startup failed`,
- error,
+ }
+ await tree.parallelTopologicalTraversal(
+ async ({ moduleId, moduleInit }) => {
+ const moduleDeps = await this.#getInitDeps(
+ moduleInit.init.deps,
+ pluginId,
);
- });
- }),
- );
+ await moduleInit.init.func(moduleDeps).catch(error => {
+ throw new ForwardedError(
+ `Module '${moduleId}' for plugin '${pluginId}' startup failed`,
+ error,
+ );
+ });
+ },
+ );
+ }
// Once all modules have been initialized, we can initialize the plugin itself
const pluginInit = pluginInits.get(pluginId);
@@ -259,7 +337,12 @@ export class BackendInitializer {
if (!this.#startPromise) {
return;
}
- await this.#startPromise;
+
+ try {
+ await this.#startPromise;
+ } catch (error) {
+ // The startup failed, but we may still want to do cleanup so we continue silently
+ }
const lifecycleService = await this.#getRootLifecycleImpl();
await lifecycleService.shutdown();
@@ -267,7 +350,7 @@ export class BackendInitializer {
// Bit of a hacky way to grab the lifecycle services, potentially find a nicer way to do this
async #getRootLifecycleImpl(): Promise {
- const lifecycleService = await this.#serviceHolder.get(
+ const lifecycleService = await this.#serviceHolder!.get(
coreServices.rootLifecycle,
'root',
);
@@ -280,7 +363,7 @@ export class BackendInitializer {
async #getPluginLifecycleImpl(
pluginId: string,
): Promise {
- const lifecycleService = await this.#serviceHolder.get(
+ const lifecycleService = await this.#serviceHolder!.get(
coreServices.lifecycle,
pluginId,
);
@@ -290,3 +373,15 @@ export class BackendInitializer {
throw new Error('Unexpected plugin lifecycle service implementation');
}
}
+
+function isServiceFactory(feature: BackendFeature): feature is ServiceFactory {
+ return !!(feature as ServiceFactory).service;
+}
+
+function isInternalBackendFeature(
+ feature: BackendFeature,
+): feature is InternalBackendFeature {
+ return (
+ typeof (feature as InternalBackendFeature).getRegistrations === 'function'
+ );
+}
diff --git a/packages/backend-app-api/src/wiring/BackstageBackend.ts b/packages/backend-app-api/src/wiring/BackstageBackend.ts
index c53f47b685..0ea55bc1c7 100644
--- a/packages/backend-app-api/src/wiring/BackstageBackend.ts
+++ b/packages/backend-app-api/src/wiring/BackstageBackend.ts
@@ -14,22 +14,19 @@
* limitations under the License.
*/
-import { ServiceFactory, BackendFeature } from '@backstage/backend-plugin-api';
+import { BackendFeature, ServiceFactory } from '@backstage/backend-plugin-api';
import { BackendInitializer } from './BackendInitializer';
-import { ServiceRegistry } from './ServiceRegistry';
import { Backend } from './types';
export class BackstageBackend implements Backend {
- #services: ServiceRegistry;
#initializer: BackendInitializer;
- constructor(apiFactories: ServiceFactory[]) {
- this.#services = new ServiceRegistry(apiFactories);
- this.#initializer = new BackendInitializer(this.#services);
+ constructor(defaultServiceFactories: ServiceFactory[]) {
+ this.#initializer = new BackendInitializer(defaultServiceFactories);
}
- add(feature: BackendFeature): void {
- this.#initializer.add(feature);
+ add(feature: BackendFeature | (() => BackendFeature)): void {
+ this.#initializer.add(typeof feature === 'function' ? feature() : feature);
}
async start(): Promise {
diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts
index e698ade348..b60a24c8d8 100644
--- a/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts
+++ b/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts
@@ -31,7 +31,7 @@ const sf1 = createServiceFactory({
async factory() {
return { x: 1 };
},
-})();
+});
const ref2 = createServiceRef<{ x: number }>({
scope: 'root',
@@ -43,14 +43,14 @@ const sf2 = createServiceFactory({
async factory() {
return { x: 2 };
},
-})();
+});
const sf2b = createServiceFactory({
service: ref2,
deps: {},
async factory() {
return { x: 22 };
},
-})();
+});
const refDefault1 = createServiceRef<{ x: number }>({
id: '1',
@@ -61,7 +61,7 @@ const refDefault1 = createServiceRef<{ x: number }>({
async factory() {
return { x: 10 };
},
- })(),
+ }),
});
const refDefault2a = createServiceRef<{ x: number }>({
@@ -95,7 +95,7 @@ describe('ServiceRegistry', () => {
});
it('should return an implementation for a registered ref', async () => {
- const registry = new ServiceRegistry([sf1]);
+ const registry = new ServiceRegistry([sf1()]);
await expect(registry.get(ref1, 'catalog')).resolves.toEqual({ x: 1 });
await expect(registry.get(ref1, 'scaffolder')).resolves.toEqual({ x: 1 });
expect(await registry.get(ref1, 'catalog')).toBe(
@@ -110,7 +110,7 @@ describe('ServiceRegistry', () => {
});
it('should handle multiple factories with different serviceRefs', async () => {
- const registry = new ServiceRegistry([sf1, sf2]);
+ const registry = new ServiceRegistry([sf1(), sf2()]);
await expect(registry.get(ref1, 'catalog')).resolves.toEqual({
x: 1,
@@ -131,7 +131,7 @@ describe('ServiceRegistry', () => {
return { x: 2 };
},
});
- const registry = new ServiceRegistry([factory(), sf1]);
+ const registry = new ServiceRegistry([factory(), sf1()]);
await expect(registry.get(ref2, 'catalog')).rejects.toThrow(
"Failed to instantiate 'root' scoped service '2' because it depends on 'plugin' scoped service '1'.",
);
@@ -145,7 +145,7 @@ describe('ServiceRegistry', () => {
return { x: rootDep.x };
},
});
- const registry = new ServiceRegistry([factory(), sf2]);
+ const registry = new ServiceRegistry([factory(), sf2()]);
await expect(registry.get(ref1, 'catalog')).resolves.toEqual({
x: 2,
});
@@ -160,7 +160,7 @@ describe('ServiceRegistry', () => {
return { x: rootDep.x };
},
});
- const registry = new ServiceRegistry([factory(), sf2]);
+ const registry = new ServiceRegistry([factory(), sf2()]);
await expect(registry.get(ref, 'catalog')).resolves.toEqual({
x: 2,
});
@@ -182,7 +182,7 @@ describe('ServiceRegistry', () => {
});
it('should use the last factory for each ref', async () => {
- const registry = new ServiceRegistry([sf2, sf2b]);
+ const registry = new ServiceRegistry([sf2(), sf2b()]);
await expect(registry.get(ref2, 'catalog')).resolves.toEqual({
x: 22,
});
@@ -196,7 +196,7 @@ describe('ServiceRegistry', () => {
});
it('should not use the defaultFactory from the ref if provided to the registry', async () => {
- const registry = new ServiceRegistry([sf1]);
+ const registry = new ServiceRegistry([sf1()]);
await expect(registry.get(refDefault1, 'catalog')).resolves.toEqual({
x: 1,
});
diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.ts
index b5b55f67cf..b6de05b361 100644
--- a/packages/backend-app-api/src/wiring/ServiceRegistry.ts
+++ b/packages/backend-app-api/src/wiring/ServiceRegistry.ts
@@ -39,7 +39,7 @@ function toInternalServiceFactory(
factory: ServiceFactory,
): InternalServiceFactory {
const f = factory as InternalServiceFactory;
- if (f.$$type !== '@backstage/ServiceFactory') {
+ if (f.$$type !== '@backstage/BackendFeature') {
throw new Error(`Invalid service factory, bad type '${f.$$type}'`);
}
if (f.version !== 'v1') {
@@ -49,10 +49,10 @@ function toInternalServiceFactory(
}
const pluginMetadataServiceFactory = createServiceFactory(
- (options: { pluginId: string }) => ({
+ (options?: { pluginId: string }) => ({
service: coreServices.pluginMetadata,
deps: {},
- factory: async () => ({ getId: () => options.pluginId }),
+ factory: async () => ({ getId: () => options?.pluginId! }),
}),
);
diff --git a/packages/backend-app-api/src/wiring/createSpecializedBackend.test.ts b/packages/backend-app-api/src/wiring/createSpecializedBackend.test.ts
index dc9a0cc085..1fd7fcbbec 100644
--- a/packages/backend-app-api/src/wiring/createSpecializedBackend.test.ts
+++ b/packages/backend-app-api/src/wiring/createSpecializedBackend.test.ts
@@ -22,13 +22,15 @@ import { createSpecializedBackend } from './createSpecializedBackend';
describe('createSpecializedBackend', () => {
it('should create a backend without services', () => {
- expect(() => createSpecializedBackend({ services: [] })).not.toThrow();
+ expect(() =>
+ createSpecializedBackend({ defaultServiceFactories: [] }),
+ ).not.toThrow();
});
it('should throw on duplicate service implementations', () => {
expect(() =>
createSpecializedBackend({
- services: [
+ defaultServiceFactories: [
createServiceFactory({
service: coreServices.rootLifecycle,
deps: {},
@@ -55,7 +57,7 @@ describe('createSpecializedBackend', () => {
it('should throw when providing a plugin metadata service implementation', () => {
expect(() =>
createSpecializedBackend({
- services: [
+ defaultServiceFactories: [
createServiceFactory({
service: coreServices.pluginMetadata,
deps: {},
diff --git a/packages/backend-app-api/src/wiring/createSpecializedBackend.ts b/packages/backend-app-api/src/wiring/createSpecializedBackend.ts
index 15e51f7c4c..f5c4d8b152 100644
--- a/packages/backend-app-api/src/wiring/createSpecializedBackend.ts
+++ b/packages/backend-app-api/src/wiring/createSpecializedBackend.ts
@@ -24,7 +24,7 @@ import { Backend, CreateSpecializedBackendOptions } from './types';
export function createSpecializedBackend(
options: CreateSpecializedBackendOptions,
): Backend {
- const services = options.services.map(sf =>
+ const services = options.defaultServiceFactories.map(sf =>
typeof sf === 'function' ? sf() : sf,
);
diff --git a/packages/backend-app-api/src/wiring/types.ts b/packages/backend-app-api/src/wiring/types.ts
index ffedb40324..552a69a792 100644
--- a/packages/backend-app-api/src/wiring/types.ts
+++ b/packages/backend-app-api/src/wiring/types.ts
@@ -25,7 +25,7 @@ import {
* @public
*/
export interface Backend {
- add(feature: BackendFeature): void;
+ add(feature: BackendFeature | (() => BackendFeature)): void;
start(): Promise;
stop(): Promise;
}
@@ -34,7 +34,7 @@ export interface Backend {
* @public
*/
export interface CreateSpecializedBackendOptions {
- services: ServiceFactoryOrFunction[];
+ defaultServiceFactories: ServiceFactoryOrFunction[];
}
export interface ServiceHolder {
diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md
index 2977d54c01..030223606f 100644
--- a/packages/backend-common/CHANGELOG.md
+++ b/packages/backend-common/CHANGELOG.md
@@ -1,5 +1,56 @@
# @backstage/backend-common
+## 0.19.2
+
+### Patch Changes
+
+- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config`
+- 443afcf7f567: To improve performance, `GerritUrlReader.readTree()` now uses Gitiles to fetch an archive instead of cloning the repository.
+ If `gitilesBaseUrl` is not configured, `readTree` still uses Git to clone the repository.
+
+ Added `stripFirstDirectory` option to `ReadTreeResponseFactory.fromTarArchive()`, allowing to disable stripping first directory
+ for `tar` archives.
+
+- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result.
+- Updated dependencies
+ - @backstage/backend-app-api@0.5.0
+ - @backstage/config-loader@1.4.0
+ - @backstage/backend-plugin-api@0.6.0
+ - @backstage/integration@1.6.0
+ - @backstage/integration-aws-node@0.1.5
+ - @backstage/backend-dev-utils@0.1.1
+ - @backstage/cli-common@0.1.12
+ - @backstage/config@1.0.8
+ - @backstage/errors@1.2.1
+ - @backstage/types@1.1.0
+
+## 0.19.2-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-app-api@0.5.0-next.2
+ - @backstage/backend-plugin-api@0.6.0-next.2
+ - @backstage/config-loader@1.4.0-next.1
+
+## 0.19.2-next.1
+
+### Patch Changes
+
+- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config`
+- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result.
+- Updated dependencies
+ - @backstage/config-loader@1.4.0-next.1
+ - @backstage/backend-app-api@0.5.0-next.1
+ - @backstage/backend-plugin-api@0.6.0-next.1
+ - @backstage/integration@1.5.1
+ - @backstage/integration-aws-node@0.1.5
+ - @backstage/backend-dev-utils@0.1.1
+ - @backstage/cli-common@0.1.12
+ - @backstage/config@1.0.8
+ - @backstage/errors@1.2.1
+ - @backstage/types@1.1.0
+
## 0.19.2-next.0
### Patch Changes
diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md
index d40027cba9..f8e535c8a7 100644
--- a/packages/backend-common/api-report.md
+++ b/packages/backend-common/api-report.md
@@ -18,7 +18,6 @@ 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 { Config } from '@backstage/config';
-import { ConfigService } from '@backstage/backend-plugin-api';
import cors from 'cors';
import Docker from 'dockerode';
import { ErrorRequestHandler } from 'express';
@@ -51,6 +50,7 @@ import { ReadTreeResponseFile } from '@backstage/backend-plugin-api';
import { ReadUrlOptions } from '@backstage/backend-plugin-api';
import { ReadUrlResponse } from '@backstage/backend-plugin-api';
import { RequestHandler } from 'express';
+import { RootConfigService } from '@backstage/backend-plugin-api';
import { Router } from 'express';
import { SchedulerService } from '@backstage/backend-plugin-api';
import { SearchOptions } from '@backstage/backend-plugin-api';
@@ -525,7 +525,7 @@ export const legacyPlugin: (
TransformedEnv<
{
cache: CacheClient;
- config: ConfigService;
+ config: RootConfigService;
database: PluginDatabaseManager;
discovery: PluginEndpointDiscovery;
logger: LoggerService;
@@ -610,7 +610,9 @@ export interface ReadTreeResponseFactory {
): Promise;
// (undocumented)
fromTarArchive(
- options: ReadTreeResponseFactoryOptions,
+ options: ReadTreeResponseFactoryOptions & {
+ stripFirstDirectory?: boolean;
+ },
): Promise;
// (undocumented)
fromZipArchive(
diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json
index 6e561fa564..fd9b9cb0df 100644
--- a/packages/backend-common/package.json
+++ b/packages/backend-common/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
- "version": "0.19.2-next.0",
+ "version": "0.19.2",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
diff --git a/packages/backend-common/src/legacy.ts b/packages/backend-common/src/legacy.ts
index 408966f3bc..df1030e6b6 100644
--- a/packages/backend-common/src/legacy.ts
+++ b/packages/backend-common/src/legacy.ts
@@ -108,7 +108,7 @@ export function makeLegacyPlugin<
export const legacyPlugin = makeLegacyPlugin(
{
cache: coreServices.cache,
- config: coreServices.config,
+ config: coreServices.rootConfig,
database: coreServices.database,
discovery: coreServices.discovery,
logger: coreServices.logger,
diff --git a/packages/backend-common/src/reading/GerritUrlReader.test.ts b/packages/backend-common/src/reading/GerritUrlReader.test.ts
index 4e0b97fc58..9de9aac33d 100644
--- a/packages/backend-common/src/reading/GerritUrlReader.test.ts
+++ b/packages/backend-common/src/reading/GerritUrlReader.test.ts
@@ -37,14 +37,16 @@ const treeResponseFactory = DefaultReadTreeResponseFactory.create({
config: new ConfigReader({}),
});
+const cloneMock = jest.fn(() => Promise.resolve());
jest.mock('../scm', () => ({
Git: {
fromAuth: () => ({
- clone: jest.fn(() => Promise.resolve({})),
+ clone: cloneMock,
}),
},
}));
+// Gerrit processor without a gitilesBaseUrl configured
const gerritProcessor = new GerritUrlReader(
new GerritIntegration(
readGerritIntegrationConfig(
@@ -57,6 +59,21 @@ const gerritProcessor = new GerritUrlReader(
'/tmp',
);
+// Gerrit processor with a gitilesBaseUrl configured.
+// Use to test readTree with Gitiles archive download.
+const gerritProcessorWithGitiles = new GerritUrlReader(
+ new GerritIntegration(
+ readGerritIntegrationConfig(
+ new ConfigReader({
+ host: 'gerrit.com',
+ gitilesBaseUrl: 'https://gerrit.com/gitiles',
+ }),
+ ),
+ ),
+ { treeResponseFactory },
+ '/tmp',
+);
+
const createReader = (config: JsonObject): UrlReaderPredicateTuple[] => {
return GerritUrlReader.factory({
config: new ConfigReader(config),
@@ -217,9 +234,17 @@ describe('GerritUrlReader', () => {
path.resolve(__dirname, '__fixtures__/gerrit/branch-info-response.txt'),
);
const treeUrl = 'https://gerrit.com/app/web/+/refs/heads/master/';
+ const treeUrlGitiles =
+ 'https://gerrit.com/gitiles/app/web/+/refs/heads/master/';
const etag = '52432507a70b677b5674b019c9a46b2e9f29d0a1';
- const mkdocsContent = 'great content';
+ const mkdocsContent = 'a repo fetched using git clone';
const mdContent = 'doc';
+ const repoArchiveBuffer = fs.readFileSync(
+ path.resolve(__dirname, '__fixtures__/gerrit/gerrit-master.tar.gz'),
+ );
+ const repoArchiveDocsBuffer = fs.readFileSync(
+ path.resolve(__dirname, '__fixtures__/gerrit/gerrit-master-docs.tar.gz'),
+ );
beforeEach(() => {
mockFs({
@@ -229,6 +254,35 @@ describe('GerritUrlReader', () => {
});
const spy = jest.spyOn(fs, 'mkdtemp');
spy.mockImplementation(() => '/tmp/gerrit-clone-123abc');
+
+ worker.use(
+ rest.get(
+ 'https://gerrit.com/gitiles/app/web/\\+archive/refs/heads/master.tar.gz',
+ (_, res, ctx) =>
+ res(
+ ctx.status(200),
+ ctx.set('Content-Type', 'application/x-gzip'),
+ ctx.set(
+ 'content-disposition',
+ 'attachment; filename=web-refs/heads/master.tar.gz',
+ ),
+ ctx.body(repoArchiveBuffer),
+ ),
+ ),
+ rest.get(
+ 'https://gerrit.com/gitiles/app/web/\\+archive/refs/heads/master/docs.tar.gz',
+ (_, res, ctx) =>
+ res(
+ ctx.status(200),
+ ctx.set('Content-Type', 'application/x-gzip'),
+ ctx.set(
+ 'content-disposition',
+ 'attachment; filename=web-refs/heads/master-docs.tar.gz',
+ ),
+ ctx.body(repoArchiveDocsBuffer),
+ ),
+ ),
+ );
});
afterEach(() => {
@@ -236,7 +290,32 @@ describe('GerritUrlReader', () => {
jest.clearAllMocks();
});
- it('reads the wanted files correctly.', async () => {
+ it('reads the wanted files correctly using gitiles.', async () => {
+ worker.use(
+ rest.get(branchAPIUrl, (_, res, ctx) => {
+ return res(ctx.status(200), ctx.body(branchAPIresponse));
+ }),
+ );
+
+ const response = await gerritProcessorWithGitiles.readTree(
+ treeUrlGitiles,
+ );
+
+ expect(response.etag).toBe(etag);
+
+ const files = await response.files();
+ expect(files.length).toBe(2);
+
+ const docsYaml = await files[0].content();
+ expect(docsYaml.toString()).toBe('# Test\n');
+
+ const mdFile = await files[1].content();
+ expect(mdFile.toString()).toBe('site_name: Test\n');
+
+ expect(cloneMock).not.toHaveBeenCalled();
+ });
+
+ it('reads the wanted files correctly using git clone.', async () => {
worker.use(
rest.get(branchAPIUrl, (_, res, ctx) => {
return res(ctx.status(200), ctx.body(branchAPIresponse));
@@ -255,6 +334,8 @@ describe('GerritUrlReader', () => {
const mdFile = await files[1].content();
expect(mdFile.toString()).toBe(mdContent);
+
+ expect(cloneMock).toHaveBeenCalled();
});
it('throws NotModifiedError for matching etags.', async () => {
@@ -291,7 +372,29 @@ describe('GerritUrlReader', () => {
await expect(gerritProcessor.readTree(treeUrl)).rejects.toThrow(Error);
});
- it('should returns wanted files with a subpath', async () => {
+ it('should returns wanted files with a subpath using gitiles', async () => {
+ worker.use(
+ rest.get(branchAPIUrl, (_, res, ctx) => {
+ return res(ctx.status(200), ctx.body(branchAPIresponse));
+ }),
+ );
+
+ const response = await gerritProcessorWithGitiles.readTree(
+ `${treeUrlGitiles}/docs`,
+ );
+
+ expect(response.etag).toBe(etag);
+
+ const files = await response.files();
+ expect(files.length).toBe(1);
+
+ const mdFile = await files[0].content();
+ expect(mdFile.toString()).toBe('# Test\n');
+
+ expect(cloneMock).not.toHaveBeenCalled();
+ });
+
+ it('should returns wanted files with a subpath using git clone', async () => {
worker.use(
rest.get(branchAPIUrl, (_, res, ctx) => {
return res(ctx.status(200), ctx.body(branchAPIresponse));
@@ -307,6 +410,8 @@ describe('GerritUrlReader', () => {
const mdFile = await files[0].content();
expect(mdFile.toString()).toBe(mdContent);
+
+ expect(cloneMock).toHaveBeenCalled();
});
});
});
diff --git a/packages/backend-common/src/reading/GerritUrlReader.ts b/packages/backend-common/src/reading/GerritUrlReader.ts
index 8ac5452a4c..f4a5fdb9f5 100644
--- a/packages/backend-common/src/reading/GerritUrlReader.ts
+++ b/packages/backend-common/src/reading/GerritUrlReader.ts
@@ -14,16 +14,17 @@
* limitations under the License.
*/
-import { Git } from '../scm';
import { NotFoundError, NotModifiedError } from '@backstage/errors';
import {
GerritIntegration,
- getGerritCloneRepoUrl,
+ ScmIntegrations,
+ buildGerritGitilesArchiveUrl,
getGerritBranchApiUrl,
+ getGerritCloneRepoUrl,
getGerritFileContentsApiUrl,
getGerritRequestOptions,
- parseGerritJsonResponse,
parseGerritGitilesUrl,
+ parseGerritJsonResponse,
} from '@backstage/integration';
import { Base64Decode } from 'base64-stream';
import concatStream from 'concat-stream';
@@ -31,20 +32,20 @@ import fs from 'fs-extra';
import fetch, { Response } from 'node-fetch';
import os from 'os';
import { join as joinPath } from 'path';
+import { Readable, pipeline as pipelineCb } from 'stream';
import tar from 'tar';
-import { pipeline as pipelineCb, Readable } from 'stream';
import { promisify } from 'util';
+import { Git } from '../scm';
import {
- ReaderFactory,
ReadTreeOptions,
ReadTreeResponse,
ReadTreeResponseFactory,
ReadUrlOptions,
ReadUrlResponse,
+ ReaderFactory,
SearchResponse,
UrlReader,
} from './types';
-import { ScmIntegrations } from '@backstage/integration';
const pipeline = promisify(pipelineCb);
@@ -59,6 +60,8 @@ const createTemporaryDirectory = async (workDir: string): Promise =>
* way we are depending on that there is a Gitiles installation somewhere
* that we can link to. It is perfectly possible to integrate Gerrit with
* Backstage without Gitiles since all API calls goes directly to Gerrit.
+ * However if Gitiles is configured, readTree will use it to fetch
+ * an archive instead of cloning the repository.
*
* The "host" variable in the config is the Gerrit host. The address where
* Gitiles is installed may be on the same host but it could be on a
@@ -125,6 +128,7 @@ export class GerritUrlReader implements UrlReader {
} catch (e) {
throw new Error(`Unable to read gerrit file ${url}, ${e}`);
}
+
if (response.ok) {
let responseBody: string;
return {
@@ -152,7 +156,6 @@ export class GerritUrlReader implements UrlReader {
url: string,
options?: ReadTreeOptions,
): Promise {
- const { filePath } = parseGerritGitilesUrl(this.integration.config, url);
const apiUrl = getGerritBranchApiUrl(this.integration.config, url);
let response: Response;
try {
@@ -180,6 +183,30 @@ export class GerritUrlReader implements UrlReader {
throw new NotModifiedError();
}
+ if (
+ this.integration.config.gitilesBaseUrl !== this.integration.config.baseUrl
+ ) {
+ return this.readTreeFromGitiles(url, branchInfo.revision, options);
+ }
+ return this.readTreeFromGitClone(url, branchInfo.revision, options);
+ }
+
+ async search(): Promise {
+ throw new Error('GerritReader does not implement search');
+ }
+
+ toString() {
+ const { host, password } = this.integration.config;
+ return `gerrit{host=${host},authed=${Boolean(password)}}`;
+ }
+
+ private async readTreeFromGitClone(
+ url: string,
+ revision: string,
+ options?: ReadTreeOptions,
+ ) {
+ const { filePath } = parseGerritGitilesUrl(this.integration.config, url);
+
const git = Git.fromAuth({
username: this.integration.config.username,
password: this.integration.config.password,
@@ -192,7 +219,7 @@ export class GerritUrlReader implements UrlReader {
await git.clone({
url: cloneUrl,
dir: joinPath(tempDir, 'repo'),
- ref: branchInfo.revision,
+ ref: revision,
depth: 1,
});
@@ -206,7 +233,7 @@ export class GerritUrlReader implements UrlReader {
return await this.deps.treeResponseFactory.fromTarArchive({
stream: tarArchive,
subpath: filePath === '/' ? undefined : filePath,
- etag: branchInfo.revision,
+ etag: revision,
filter: options?.filter,
});
} catch (error) {
@@ -216,12 +243,47 @@ export class GerritUrlReader implements UrlReader {
}
}
- async search(): Promise {
- throw new Error('GerritReader does not implement search');
- }
+ private async readTreeFromGitiles(
+ url: string,
+ revision: string,
+ options?: ReadTreeOptions,
+ ) {
+ const { branch, filePath, project } = parseGerritGitilesUrl(
+ this.integration.config,
+ url,
+ );
+ const archiveUrl = buildGerritGitilesArchiveUrl(
+ this.integration.config,
+ project,
+ branch,
+ filePath,
+ );
+ const archiveResponse = await fetch(archiveUrl, {
+ ...getGerritRequestOptions(this.integration.config),
+ // TODO(freben): The signal cast is there because pre-3.x versions of
+ // node-fetch have a very slightly deviating AbortSignal type signature.
+ // The difference does not affect us in practice however. The cast can
+ // be removed after we support ESM for CLI dependencies and migrate to
+ // version 3 of node-fetch.
+ // https://github.com/backstage/backstage/issues/8242
+ signal: options?.signal as any,
+ });
- toString() {
- const { host, password } = this.integration.config;
- return `gerrit{host=${host},authed=${Boolean(password)}}`;
+ if (archiveResponse.status === 404) {
+ throw new NotFoundError(`Not found: ${archiveUrl}`);
+ }
+
+ if (!archiveResponse.ok) {
+ throw new Error(
+ `${url} could not be read as ${archiveUrl}, ${archiveResponse.status} ${archiveResponse.statusText}`,
+ );
+ }
+
+ return await this.deps.treeResponseFactory.fromTarArchive({
+ stream: archiveResponse.body as unknown as Readable,
+ etag: revision,
+ filter: options?.filter,
+ stripFirstDirectory: false,
+ });
}
}
diff --git a/packages/backend-common/src/reading/__fixtures__/gerrit/gerrit-master-docs.tar.gz b/packages/backend-common/src/reading/__fixtures__/gerrit/gerrit-master-docs.tar.gz
new file mode 100644
index 0000000000..425cf0876b
Binary files /dev/null and b/packages/backend-common/src/reading/__fixtures__/gerrit/gerrit-master-docs.tar.gz differ
diff --git a/packages/backend-common/src/reading/__fixtures__/gerrit/gerrit-master.tar.gz b/packages/backend-common/src/reading/__fixtures__/gerrit/gerrit-master.tar.gz
new file mode 100644
index 0000000000..8a27502734
Binary files /dev/null and b/packages/backend-common/src/reading/__fixtures__/gerrit/gerrit-master.tar.gz differ
diff --git a/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts b/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts
index 11a5ef168a..90c58c0efa 100644
--- a/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts
+++ b/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts
@@ -37,7 +37,9 @@ export class DefaultReadTreeResponseFactory implements ReadTreeResponseFactory {
constructor(private readonly workDir: string) {}
async fromTarArchive(
- options: ReadTreeResponseFactoryOptions,
+ options: ReadTreeResponseFactoryOptions & {
+ stripFirstDirectory?: boolean;
+ },
): Promise {
return new TarArchiveResponse(
options.stream,
@@ -45,6 +47,7 @@ export class DefaultReadTreeResponseFactory implements ReadTreeResponseFactory {
this.workDir,
options.etag,
options.filter,
+ options.stripFirstDirectory ?? true,
);
}
diff --git a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts
index e1529c196a..294ec22609 100644
--- a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts
+++ b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts
@@ -44,6 +44,7 @@ export class TarArchiveResponse implements ReadTreeResponse {
private readonly workDir: string,
public readonly etag: string,
private readonly filter?: (path: string, info: { size: number }) => boolean,
+ private readonly stripFirstDirectory: boolean = true,
) {
if (subPath) {
if (!subPath.endsWith('/')) {
@@ -81,7 +82,9 @@ export class TarArchiveResponse implements ReadTreeResponse {
// File path relative to the root extracted directory. Will remove the
// top level dir name from the path since its name is hard to predetermine.
- const relativePath = stripFirstDirectoryFromPath(entry.path);
+ const relativePath = this.stripFirstDirectory
+ ? stripFirstDirectoryFromPath(entry.path)
+ : entry.path;
if (this.subPath) {
if (!relativePath.startsWith(this.subPath)) {
@@ -148,7 +151,10 @@ export class TarArchiveResponse implements ReadTreeResponse {
// Equivalent of tar --strip-components=N
// When no subPath is given, remove just 1 top level directory
- const strip = this.subPath ? this.subPath.split('/').length : 1;
+ let strip = this.subPath ? this.subPath.split('/').length : 1;
+ if (!this.stripFirstDirectory) {
+ strip--;
+ }
let filterError: Error | undefined = undefined;
await pipeline(
@@ -164,7 +170,9 @@ export class TarArchiveResponse implements ReadTreeResponse {
// File path relative to the root extracted directory. Will remove the
// top level dir name from the path since its name is hard to predetermine.
- const relativePath = stripFirstDirectoryFromPath(path);
+ const relativePath = this.stripFirstDirectory
+ ? stripFirstDirectoryFromPath(path)
+ : path;
if (this.subPath && !relativePath.startsWith(this.subPath)) {
return false;
}
diff --git a/packages/backend-common/src/reading/types.ts b/packages/backend-common/src/reading/types.ts
index 7f4d407fd0..90a3c6287e 100644
--- a/packages/backend-common/src/reading/types.ts
+++ b/packages/backend-common/src/reading/types.ts
@@ -116,7 +116,13 @@ export type FromReadableArrayOptions = Array<{
*/
export interface ReadTreeResponseFactory {
fromTarArchive(
- options: ReadTreeResponseFactoryOptions,
+ options: ReadTreeResponseFactoryOptions & {
+ /**
+ * Strip the first parent directory of a tar archive.
+ * Defaults to true.
+ */
+ stripFirstDirectory?: boolean;
+ },
): Promise;
fromZipArchive(
options: ReadTreeResponseFactoryOptions,
diff --git a/packages/backend-defaults/CHANGELOG.md b/packages/backend-defaults/CHANGELOG.md
index 1ff580965f..af86313d11 100644
--- a/packages/backend-defaults/CHANGELOG.md
+++ b/packages/backend-defaults/CHANGELOG.md
@@ -1,5 +1,54 @@
# @backstage/backend-defaults
+## 0.2.0
+
+### Minor Changes
+
+- d008aefef808: **BREAKING**: Removing shared environments concept from the new experimental backend system.
+- a6d7983f349c: **BREAKING**: Removed the `services` option from `createBackend`. Service factories are now `BackendFeature`s and should be installed with `backend.add(...)` instead. The following should be migrated:
+
+ ```ts
+ const backend = createBackend({ services: [myCustomServiceFactory] });
+ ```
+
+ To instead pass the service factory via `backend.add(...)`:
+
+ ```ts
+ const backend = createBackend();
+ backend.add(customRootLoggerServiceFactory);
+ ```
+
+### Patch Changes
+
+- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config`
+- Updated dependencies
+ - @backstage/backend-common@0.19.2
+ - @backstage/backend-app-api@0.5.0
+ - @backstage/backend-plugin-api@0.6.0
+
+## 0.2.0-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-app-api@0.5.0-next.2
+ - @backstage/backend-plugin-api@0.6.0-next.2
+ - @backstage/backend-common@0.19.2-next.2
+
+## 0.2.0-next.1
+
+### Minor Changes
+
+- d008aefef808: **BREAKING**: Removing shared environments concept from the new experimental backend system.
+
+### Patch Changes
+
+- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config`
+- Updated dependencies
+ - @backstage/backend-common@0.19.2-next.1
+ - @backstage/backend-app-api@0.5.0-next.1
+ - @backstage/backend-plugin-api@0.6.0-next.1
+
## 0.1.13-next.0
### Patch Changes
diff --git a/packages/backend-defaults/api-report.md b/packages/backend-defaults/api-report.md
index 6cc58de663..ab9cc5775c 100644
--- a/packages/backend-defaults/api-report.md
+++ b/packages/backend-defaults/api-report.md
@@ -4,14 +4,7 @@
```ts
import { Backend } from '@backstage/backend-app-api';
-import { ServiceFactoryOrFunction } from '@backstage/backend-plugin-api';
// @public (undocumented)
-export function createBackend(options?: CreateBackendOptions): Backend;
-
-// @public (undocumented)
-export interface CreateBackendOptions {
- // (undocumented)
- services?: ServiceFactoryOrFunction[];
-}
+export function createBackend(): Backend;
```
diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json
index 318b00c7f2..48464ac81e 100644
--- a/packages/backend-defaults/package.json
+++ b/packages/backend-defaults/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-defaults",
"description": "Backend defaults used by Backstage backend apps",
- "version": "0.1.13-next.0",
+ "version": "0.2.0",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
diff --git a/packages/backend-defaults/src/CreateBackend.test.ts b/packages/backend-defaults/src/CreateBackend.test.ts
index de04d4e350..0b36b57069 100644
--- a/packages/backend-defaults/src/CreateBackend.test.ts
+++ b/packages/backend-defaults/src/CreateBackend.test.ts
@@ -22,60 +22,63 @@ import { createBackend } from './CreateBackend';
describe('createBackend', () => {
it('should not throw when overriding a default service implementation', () => {
- expect(() =>
- createBackend({
- services: [
- createServiceFactory({
- service: coreServices.rootLifecycle,
- deps: {},
- factory: async () => ({
- addStartupHook: () => {},
- addShutdownHook: () => {},
- }),
+ const backend = createBackend();
+
+ expect(() => {
+ backend.add(
+ createServiceFactory({
+ service: coreServices.rootLifecycle,
+ deps: {},
+ factory: async () => ({
+ addStartupHook: () => {},
+ addShutdownHook: () => {},
}),
- ],
- }),
- ).not.toThrow();
+ }),
+ );
+ }).not.toThrow();
});
it('should throw on duplicate service implementations', () => {
- expect(() =>
- createBackend({
- services: [
- createServiceFactory({
- service: coreServices.rootLifecycle,
- deps: {},
- factory: async () => ({
- addStartupHook: () => {},
- addShutdownHook: () => {},
- }),
- }),
- createServiceFactory({
- service: coreServices.rootLifecycle,
- deps: {},
- factory: async () => ({
- addStartupHook: () => {},
- addShutdownHook: () => {},
- }),
- }),
- ],
+ const backend = createBackend();
+
+ backend.add(
+ createServiceFactory({
+ service: coreServices.rootLifecycle,
+ deps: {},
+ factory: async () => ({
+ addStartupHook: () => {},
+ addShutdownHook: () => {},
+ }),
}),
- ).toThrow(
+ );
+
+ expect(() => {
+ backend.add(
+ createServiceFactory({
+ service: coreServices.rootLifecycle,
+ deps: {},
+ factory: async () => ({
+ addStartupHook: () => {},
+ addShutdownHook: () => {},
+ }),
+ }),
+ );
+ }).toThrow(
'Duplicate service implementations provided for core.rootLifecycle',
);
});
it('should throw when providing a plugin metadata service implementation', () => {
+ const backend = createBackend();
+
expect(() =>
- createBackend({
- services: [
- createServiceFactory({
- service: coreServices.pluginMetadata,
- deps: {},
- factory: async () => ({ getId: () => 'test' }),
- }),
- ],
- }),
+ backend.add(
+ createServiceFactory({
+ service: coreServices.pluginMetadata,
+ deps: {},
+ factory: async () => ({ getId: () => 'test' }),
+ }),
+ ),
).toThrow('The core.pluginMetadata service cannot be overridden');
});
});
diff --git a/packages/backend-defaults/src/CreateBackend.ts b/packages/backend-defaults/src/CreateBackend.ts
index 7051b58ae5..c07d1cc38a 100644
--- a/packages/backend-defaults/src/CreateBackend.ts
+++ b/packages/backend-defaults/src/CreateBackend.ts
@@ -17,7 +17,7 @@
import {
Backend,
cacheServiceFactory,
- configServiceFactory,
+ rootConfigServiceFactory,
createSpecializedBackend,
databaseServiceFactory,
discoveryServiceFactory,
@@ -33,14 +33,10 @@ import {
urlReaderServiceFactory,
identityServiceFactory,
} from '@backstage/backend-app-api';
-import {
- ServiceFactory,
- ServiceFactoryOrFunction,
-} from '@backstage/backend-plugin-api';
export const defaultServiceFactories = [
cacheServiceFactory(),
- configServiceFactory(),
+ rootConfigServiceFactory(),
databaseServiceFactory(),
discoveryServiceFactory(),
httpRouterServiceFactory(),
@@ -59,27 +55,6 @@ export const defaultServiceFactories = [
/**
* @public
*/
-export interface CreateBackendOptions {
- services?: ServiceFactoryOrFunction[];
-}
-
-/**
- * @public
- */
-export function createBackend(options?: CreateBackendOptions): Backend {
- const services = new Array();
-
- // Highest priority: Services passed directly to createBackend
- const providedServices = (options?.services ?? []).map(sf =>
- typeof sf === 'function' ? sf() : sf,
- );
- services.push(...providedServices);
-
- // Lowest priority: Default services that are not already provided by environment or directly to createBackend
- const defaultServices = defaultServiceFactories.filter(
- sf => !services.some(({ service }) => service.id === sf.service.id),
- );
- services.push(...defaultServices);
-
- return createSpecializedBackend({ services });
+export function createBackend(): Backend {
+ return createSpecializedBackend({ defaultServiceFactories });
}
diff --git a/packages/backend-defaults/src/index.ts b/packages/backend-defaults/src/index.ts
index 833878744e..d3e36cf0fb 100644
--- a/packages/backend-defaults/src/index.ts
+++ b/packages/backend-defaults/src/index.ts
@@ -25,5 +25,4 @@
// TODO(Rugvip): Remove this once backend-common is no longer used by backend-app-api
import '@backstage/backend-common';
-export type { CreateBackendOptions } from './CreateBackend';
export { createBackend } from './CreateBackend';
diff --git a/packages/backend-next/.snyk b/packages/backend-next/.snyk
new file mode 100644
index 0000000000..ea5abd653d
--- /dev/null
+++ b/packages/backend-next/.snyk
@@ -0,0 +1,10 @@
+# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
+version: v1.22.1
+# ignores vulnerabilities until expiry date; change duration by modifying expiry date
+ignore:
+ SNYK-JS-ISOLATEDVM-3037320:
+ - '*':
+ reason: We do not pass any V8 cache data, and are therefore unaffected by this vulnerability
+ expires: 2033-07-02T16:55:57.077Z
+ created: 2023-07-02T16:55:57.077Z
+patch: {}
diff --git a/packages/backend-next/CHANGELOG.md b/packages/backend-next/CHANGELOG.md
index 9a9be3b3e2..2c0fefb9f3 100644
--- a/packages/backend-next/CHANGELOG.md
+++ b/packages/backend-next/CHANGELOG.md
@@ -1,5 +1,101 @@
# example-backend-next
+## 0.0.14
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-search-backend-module-techdocs@0.1.4
+ - @backstage/plugin-search-backend-module-catalog@0.1.4
+ - @backstage/plugin-search-backend-module-explore@0.1.4
+ - @backstage/plugin-azure-devops-backend@0.3.27
+ - @backstage/plugin-kubernetes-backend@0.11.3
+ - @backstage/plugin-lighthouse-backend@0.2.4
+ - @backstage/plugin-permission-backend@0.5.23
+ - @backstage/plugin-scaffolder-backend@1.16.0
+ - @backstage/backend-defaults@0.2.0
+ - @backstage/plugin-devtools-backend@0.1.3
+ - @backstage/plugin-techdocs-backend@1.6.5
+ - @backstage/plugin-catalog-backend@1.12.0
+ - @backstage/plugin-badges-backend@0.2.3
+ - @backstage/plugin-search-backend@1.4.0
+ - @backstage/plugin-proxy-backend@0.3.0
+ - @backstage/plugin-todo-backend@0.2.0
+ - @backstage/plugin-app-backend@0.3.48
+ - @backstage/backend-plugin-api@0.6.0
+ - @backstage/plugin-catalog-backend-module-unprocessed@0.2.0
+ - @backstage/plugin-entity-feedback-backend@0.1.6
+ - @backstage/plugin-search-backend-node@1.2.4
+ - @backstage/plugin-linguist-backend@0.4.0
+ - @backstage/plugin-auth-node@0.2.17
+ - @backstage/backend-tasks@0.5.5
+ - @backstage/plugin-adr-backend@0.3.6
+ - @backstage/plugin-permission-node@0.7.11
+ - @backstage/plugin-permission-common@0.7.7
+
+## 0.0.14-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-search-backend-module-techdocs@0.1.4-next.2
+ - @backstage/plugin-search-backend-module-catalog@0.1.4-next.2
+ - @backstage/plugin-search-backend-module-explore@0.1.4-next.2
+ - @backstage/plugin-scaffolder-backend@1.15.2-next.2
+ - @backstage/plugin-catalog-backend@1.12.0-next.2
+ - @backstage/backend-plugin-api@0.6.0-next.2
+ - @backstage/plugin-proxy-backend@0.3.0-next.2
+ - @backstage/backend-tasks@0.5.5-next.2
+ - @backstage/plugin-app-backend@0.3.48-next.2
+ - @backstage/plugin-linguist-backend@0.4.0-next.2
+ - @backstage/plugin-techdocs-backend@1.6.5-next.2
+ - @backstage/backend-defaults@0.2.0-next.2
+ - @backstage/plugin-adr-backend@0.3.6-next.2
+ - @backstage/plugin-azure-devops-backend@0.3.27-next.2
+ - @backstage/plugin-badges-backend@0.2.3-next.2
+ - @backstage/plugin-catalog-backend-module-unprocessed@0.2.0-next.2
+ - @backstage/plugin-devtools-backend@0.1.3-next.2
+ - @backstage/plugin-entity-feedback-backend@0.1.6-next.2
+ - @backstage/plugin-kubernetes-backend@0.11.3-next.2
+ - @backstage/plugin-lighthouse-backend@0.2.4-next.2
+ - @backstage/plugin-permission-backend@0.5.23-next.2
+ - @backstage/plugin-permission-node@0.7.11-next.2
+ - @backstage/plugin-search-backend@1.4.0-next.2
+ - @backstage/plugin-search-backend-node@1.2.4-next.2
+ - @backstage/plugin-todo-backend@0.2.0-next.2
+ - @backstage/plugin-auth-node@0.2.17-next.2
+
+## 0.0.14-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-search-backend-module-techdocs@0.1.4-next.1
+ - @backstage/plugin-search-backend-module-catalog@0.1.4-next.1
+ - @backstage/plugin-search-backend-module-explore@0.1.4-next.1
+ - @backstage/plugin-azure-devops-backend@0.3.27-next.1
+ - @backstage/plugin-kubernetes-backend@0.11.3-next.1
+ - @backstage/plugin-lighthouse-backend@0.2.4-next.1
+ - @backstage/plugin-permission-backend@0.5.23-next.1
+ - @backstage/plugin-scaffolder-backend@1.15.2-next.1
+ - @backstage/backend-defaults@0.2.0-next.1
+ - @backstage/plugin-devtools-backend@0.1.3-next.1
+ - @backstage/plugin-techdocs-backend@1.6.5-next.1
+ - @backstage/plugin-catalog-backend@1.12.0-next.1
+ - @backstage/plugin-badges-backend@0.2.3-next.1
+ - @backstage/plugin-search-backend@1.4.0-next.1
+ - @backstage/plugin-todo-backend@0.2.0-next.1
+ - @backstage/plugin-app-backend@0.3.48-next.1
+ - @backstage/plugin-catalog-backend-module-unprocessed@0.2.0-next.1
+ - @backstage/plugin-entity-feedback-backend@0.1.6-next.1
+ - @backstage/plugin-search-backend-node@1.2.4-next.1
+ - @backstage/plugin-linguist-backend@0.3.2-next.1
+ - @backstage/plugin-auth-node@0.2.17-next.1
+ - @backstage/backend-tasks@0.5.5-next.1
+ - @backstage/plugin-adr-backend@0.3.6-next.1
+ - @backstage/plugin-permission-node@0.7.11-next.1
+ - @backstage/plugin-permission-common@0.7.7
+
## 0.0.14-next.0
### Patch Changes
diff --git a/packages/backend-next/package.json b/packages/backend-next/package.json
index 3043a96ebd..c2b923ed26 100644
--- a/packages/backend-next/package.json
+++ b/packages/backend-next/package.json
@@ -1,6 +1,6 @@
{
"name": "example-backend-next",
- "version": "0.0.14-next.0",
+ "version": "0.0.14",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -26,6 +26,7 @@
},
"dependencies": {
"@backstage/backend-defaults": "workspace:^",
+ "@backstage/backend-plugin-api": "workspace:^",
"@backstage/backend-tasks": "workspace:^",
"@backstage/plugin-adr-backend": "workspace:^",
"@backstage/plugin-app-backend": "workspace:^",
@@ -42,6 +43,7 @@
"@backstage/plugin-permission-backend": "workspace:^",
"@backstage/plugin-permission-common": "workspace:^",
"@backstage/plugin-permission-node": "workspace:^",
+ "@backstage/plugin-proxy-backend": "workspace:^",
"@backstage/plugin-scaffolder-backend": "workspace:^",
"@backstage/plugin-search-backend": "workspace:^",
"@backstage/plugin-search-backend-module-catalog": "workspace:^",
diff --git a/packages/backend-next/src/index.ts b/packages/backend-next/src/index.ts
index fa7561e211..f3b1ea06f8 100644
--- a/packages/backend-next/src/index.ts
+++ b/packages/backend-next/src/index.ts
@@ -36,13 +36,13 @@ import { badgesPlugin } from '@backstage/plugin-badges-backend';
import { azureDevOpsPlugin } from '@backstage/plugin-azure-devops-backend';
import { linguistPlugin } from '@backstage/plugin-linguist-backend';
import { devtoolsPlugin } from '@backstage/plugin-devtools-backend';
-import { TaskScheduleDefinition } from '@backstage/backend-tasks';
import { adrPlugin } from '@backstage/plugin-adr-backend';
import { lighthousePlugin } from '@backstage/plugin-lighthouse-backend';
+import { proxyPlugin } from '@backstage/plugin-proxy-backend';
const backend = createBackend();
-backend.add(appPlugin({ appPackageName: 'example-app' }));
+backend.add(appPlugin());
// Badges
backend.add(badgesPlugin());
@@ -57,20 +57,7 @@ backend.add(devtoolsPlugin());
backend.add(entityFeedbackPlugin());
// Linguist
-const linguistSchedule: TaskScheduleDefinition = {
- frequency: { minutes: 2 },
- timeout: { minutes: 15 },
- initialDelay: { seconds: 15 },
-};
-
-backend.add(
- linguistPlugin({
- schedule: linguistSchedule,
- age: { days: 30 },
- batchSize: 2,
- useSourceLocation: false,
- }),
-);
+backend.add(linguistPlugin());
// Todo
backend.add(todoPlugin());
@@ -98,6 +85,9 @@ backend.add(kubernetesPlugin());
// Lighthouse
backend.add(lighthousePlugin());
+// Proxy
+backend.add(proxyPlugin());
+
// Permissions
backend.add(permissionPlugin());
backend.add(permissionModuleAllowAllPolicy());
diff --git a/packages/backend-openapi-utils/CHANGELOG.md b/packages/backend-openapi-utils/CHANGELOG.md
index bd1d7f380a..fc3e52fa92 100644
--- a/packages/backend-openapi-utils/CHANGELOG.md
+++ b/packages/backend-openapi-utils/CHANGELOG.md
@@ -1,5 +1,22 @@
# @backstage/backend-openapi-utils
+## 0.0.3
+
+### Patch Changes
+
+- ebeb77586975: Add a new `createRouter` method for generating an `express` router that validates against your spec. Also fixes a bug with the query parameters type resolution.
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/errors@1.2.1
+
+## 0.0.3-next.1
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/errors@1.2.1
+
## 0.0.3-next.0
### Patch Changes
diff --git a/packages/backend-openapi-utils/package.json b/packages/backend-openapi-utils/package.json
index 9c83356c83..102d8c36d6 100644
--- a/packages/backend-openapi-utils/package.json
+++ b/packages/backend-openapi-utils/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-openapi-utils",
"description": "OpenAPI typescript support.",
- "version": "0.0.3-next.0",
+ "version": "0.0.3",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/packages/backend-plugin-api/CHANGELOG.md b/packages/backend-plugin-api/CHANGELOG.md
index a77cdb71eb..9ea8b95c03 100644
--- a/packages/backend-plugin-api/CHANGELOG.md
+++ b/packages/backend-plugin-api/CHANGELOG.md
@@ -1,5 +1,63 @@
# @backstage/backend-plugin-api
+## 0.6.0
+
+### Minor Changes
+
+- c49785f00cab: **BREAKING**: It is no longer possible to declare options as being required with `createServiceFactory`.
+- 629cbd194a87: **BREAKING**: Renamed `coreServices.config` to `coreServices.rootConfig`.
+- 51987dbdaf87: **BREAKING**: Removed the ability to define options for plugins and modules. Existing options should be migrated to instead use either static configuration or extension points.
+- d008aefef808: **BREAKING**: Removing shared environments concept from the new experimental backend system.
+
+### Patch Changes
+
+- c7aa4ff1793c: Allow modules to register extension points.
+- cc9256a33bcc: Added new experimental `featureDiscoveryServiceRef`, available as an `/alpha` export.
+- a6d7983f349c: **BREAKING**: Removed the `services` option from `createBackend`. Service factories are now `BackendFeature`s and should be installed with `backend.add(...)` instead. The following should be migrated:
+
+ ```ts
+ const backend = createBackend({ services: [myCustomServiceFactory] });
+ ```
+
+ To instead pass the service factory via `backend.add(...)`:
+
+ ```ts
+ const backend = createBackend();
+ backend.add(customRootLoggerServiceFactory);
+ ```
+
+- Updated dependencies
+ - @backstage/plugin-auth-node@0.2.17
+ - @backstage/backend-tasks@0.5.5
+ - @backstage/config@1.0.8
+ - @backstage/types@1.1.0
+ - @backstage/plugin-permission-common@0.7.7
+
+## 0.6.0-next.2
+
+### Patch Changes
+
+- cc9256a33bcc: Added new experimental `featureDiscoveryServiceRef`, available as an `/alpha` export.
+- Updated dependencies
+ - @backstage/backend-tasks@0.5.5-next.2
+ - @backstage/plugin-auth-node@0.2.17-next.2
+
+## 0.6.0-next.1
+
+### Minor Changes
+
+- 629cbd194a87: **BREAKING**: Renamed `coreServices.config` to `coreServices.rootConfig`.
+- d008aefef808: **BREAKING**: Removing shared environments concept from the new experimental backend system.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-auth-node@0.2.17-next.1
+ - @backstage/backend-tasks@0.5.5-next.1
+ - @backstage/config@1.0.8
+ - @backstage/types@1.1.0
+ - @backstage/plugin-permission-common@0.7.7
+
## 0.5.5-next.0
### Patch Changes
diff --git a/packages/backend-plugin-api/alpha-api-report.md b/packages/backend-plugin-api/alpha-api-report.md
new file mode 100644
index 0000000000..81b378a671
--- /dev/null
+++ b/packages/backend-plugin-api/alpha-api-report.md
@@ -0,0 +1,24 @@
+## API Report File for "@backstage/backend-plugin-api"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { BackendFeature } from '@backstage/backend-plugin-api';
+import { ServiceRef } from '@backstage/backend-plugin-api';
+
+// @alpha (undocumented)
+export interface FeatureDiscoveryService {
+ // (undocumented)
+ getBackendFeatures(): Promise<{
+ features: Array;
+ }>;
+}
+
+// @alpha
+export const featureDiscoveryServiceRef: ServiceRef<
+ FeatureDiscoveryService,
+ 'root'
+>;
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md
index 093b4acec4..17cc521499 100644
--- a/packages/backend-plugin-api/api-report.md
+++ b/packages/backend-plugin-api/api-report.md
@@ -31,6 +31,11 @@ export interface BackendModuleConfig {
// @public
export interface BackendModuleRegistrationPoints {
+ // (undocumented)
+ registerExtensionPoint(
+ ref: ExtensionPoint,
+ impl: TExtensionPoint,
+ ): void;
// (undocumented)
registerInit<
Deps extends {
@@ -93,13 +98,10 @@ export type CacheServiceSetOptions = {
ttl?: number;
};
-// @public (undocumented)
-export interface ConfigService extends Config {}
-
// @public
export namespace coreServices {
const cache: ServiceRef;
- const config: ServiceRef;
+ const rootConfig: ServiceRef;
const database: ServiceRef;
const discovery: ServiceRef;
const httpRouter: ServiceRef;
@@ -117,14 +119,14 @@ export namespace coreServices {
}
// @public
-export function createBackendModule(
- config: BackendModuleConfig | ((...params: TOptions) => BackendModuleConfig),
-): (...params: TOptions) => BackendFeature;
+export function createBackendModule(
+ config: BackendModuleConfig,
+): () => BackendFeature;
// @public
-export function createBackendPlugin(
- config: BackendPluginConfig | ((...params: TOptions) => BackendPluginConfig),
-): (...params: TOptions) => BackendFeature;
+export function createBackendPlugin(
+ config: BackendPluginConfig,
+): () => BackendFeature;
// @public
export function createExtensionPoint(
@@ -155,18 +157,6 @@ export function createServiceFactory<
config: (options?: TOpts) => RootServiceFactoryConfig,
): (options?: TOpts) => ServiceFactory;
-// @public
-export function createServiceFactory<
- TService,
- TImpl extends TService,
- TDeps extends {
- [name in string]: ServiceRef;
- },
- TOpts extends object | undefined = undefined,
->(
- config: (options: TOpts) => RootServiceFactoryConfig,
-): (options: TOpts) => ServiceFactory;
-
// @public
export function createServiceFactory<
TService,
@@ -195,23 +185,6 @@ export function createServiceFactory<
) => PluginServiceFactoryConfig,
): (options?: TOpts) => ServiceFactory;
-// @public
-export function createServiceFactory<
- TService,
- TImpl extends TService,
- TDeps extends {
- [name in string]: ServiceRef;
- },
- TContext = undefined,
- TOpts extends object | undefined = undefined,
->(
- config:
- | PluginServiceFactoryConfig
- | ((
- options: TOpts,
- ) => PluginServiceFactoryConfig),
-): (options: TOpts) => ServiceFactory;
-
// @public
export function createServiceRef(
config: ServiceRefConfig,
@@ -380,6 +353,9 @@ export type ReadUrlResponse = {
lastModifiedAt?: Date;
};
+// @public (undocumented)
+export interface RootConfigService extends Config {}
+
// @public (undocumented)
export interface RootHttpRouterService {
use(path: string, handler: Handler): void;
@@ -433,9 +409,7 @@ export type SearchResponseFile = {
export interface ServiceFactory<
TService = unknown,
TScope extends 'plugin' | 'root' = 'plugin' | 'root',
-> {
- // (undocumented)
- $$type: '@backstage/ServiceFactory';
+> extends BackendFeature {
// (undocumented)
service: ServiceRef;
}
diff --git a/packages/backend-plugin-api/package.json b/packages/backend-plugin-api/package.json
index 91936a4c16..44a9d5c69c 100644
--- a/packages/backend-plugin-api/package.json
+++ b/packages/backend-plugin-api/package.json
@@ -1,17 +1,30 @@
{
"name": "@backstage/backend-plugin-api",
"description": "Core API used by Backstage backend plugins",
- "version": "0.5.5-next.0",
+ "version": "0.6.0",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
- "access": "public",
- "main": "dist/index.cjs.js",
- "types": "dist/index.d.ts"
+ "access": "public"
},
"backstage": {
"role": "node-library"
},
+ "exports": {
+ ".": "./src/index.ts",
+ "./alpha": "./src/alpha.ts",
+ "./package.json": "./package.json"
+ },
+ "typesVersions": {
+ "*": {
+ "alpha": [
+ "src/alpha.ts"
+ ],
+ "package.json": [
+ "package.json"
+ ]
+ }
+ },
"homepage": "https://backstage.io",
"repository": {
"type": "git",
diff --git a/packages/backend-plugin-api/src/alpha.ts b/packages/backend-plugin-api/src/alpha.ts
new file mode 100644
index 0000000000..baee739f49
--- /dev/null
+++ b/packages/backend-plugin-api/src/alpha.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023 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 {
+ BackendFeature,
+ createServiceRef,
+} from '@backstage/backend-plugin-api';
+
+/** @alpha */
+export interface FeatureDiscoveryService {
+ getBackendFeatures(): Promise<{ features: Array }>;
+}
+
+/**
+ * An optional service that can be used to dynamically load in additional BackendFeatures at runtime.
+ * @alpha
+ */
+export const featureDiscoveryServiceRef =
+ createServiceRef({
+ id: 'core.featureDiscovery',
+ scope: 'root',
+ });
diff --git a/packages/backend-plugin-api/src/index.ts b/packages/backend-plugin-api/src/index.ts
index 452fa046f9..8e0b23d20b 100644
--- a/packages/backend-plugin-api/src/index.ts
+++ b/packages/backend-plugin-api/src/index.ts
@@ -21,4 +21,5 @@
*/
export * from './services';
+export type { BackendFeature } from './types';
export * from './wiring';
diff --git a/packages/backend-plugin-api/src/services/definitions/ConfigService.ts b/packages/backend-plugin-api/src/services/definitions/RootConfigService.ts
similarity index 92%
rename from packages/backend-plugin-api/src/services/definitions/ConfigService.ts
rename to packages/backend-plugin-api/src/services/definitions/RootConfigService.ts
index db9aa88dda..02343e4ee8 100644
--- a/packages/backend-plugin-api/src/services/definitions/ConfigService.ts
+++ b/packages/backend-plugin-api/src/services/definitions/RootConfigService.ts
@@ -19,4 +19,4 @@ import { Config } from '@backstage/config';
/**
* @public
*/
-export interface ConfigService extends Config {}
+export interface RootConfigService extends Config {}
diff --git a/packages/backend-plugin-api/src/services/definitions/coreServices.ts b/packages/backend-plugin-api/src/services/definitions/coreServices.ts
index 7dad94cebf..b42795d052 100644
--- a/packages/backend-plugin-api/src/services/definitions/coreServices.ts
+++ b/packages/backend-plugin-api/src/services/definitions/coreServices.ts
@@ -32,13 +32,13 @@ export namespace coreServices {
});
/**
- * The service reference for the root scoped {@link ConfigService}.
+ * The service reference for the root scoped {@link RootConfigService}.
*
* @public
*/
- export const config = createServiceRef<
- import('./ConfigService').ConfigService
- >({ id: 'core.config', scope: 'root' });
+ export const rootConfig = createServiceRef<
+ import('./RootConfigService').RootConfigService
+ >({ id: 'core.rootConfig', scope: 'root' });
/**
* The service reference for the plugin scoped {@link DatabaseService}.
diff --git a/packages/backend-plugin-api/src/services/definitions/index.ts b/packages/backend-plugin-api/src/services/definitions/index.ts
index ab0f0b84b1..98be8211db 100644
--- a/packages/backend-plugin-api/src/services/definitions/index.ts
+++ b/packages/backend-plugin-api/src/services/definitions/index.ts
@@ -20,7 +20,7 @@ export type {
CacheServiceOptions,
CacheServiceSetOptions,
} from './CacheService';
-export type { ConfigService } from './ConfigService';
+export type { RootConfigService } from './RootConfigService';
export type { DatabaseService } from './DatabaseService';
export type { DiscoveryService } from './DiscoveryService';
export type { HttpRouterService } from './HttpRouterService';
diff --git a/packages/backend-plugin-api/src/services/system/types.test.ts b/packages/backend-plugin-api/src/services/system/types.test.ts
index d3e9b9dd4e..4d0432505e 100644
--- a/packages/backend-plugin-api/src/services/system/types.test.ts
+++ b/packages/backend-plugin-api/src/services/system/types.test.ts
@@ -26,7 +26,7 @@ interface TestOptions {
function unused(..._any: any[]) {}
describe('createServiceFactory', () => {
- it('should create a sync factory with no options', () => {
+ it('should create a sync factory', () => {
const metaFactory = createServiceFactory({
service: ref,
deps: {},
@@ -51,7 +51,7 @@ describe('createServiceFactory', () => {
metaFactory();
});
- it('should create a sync root factory with no options', () => {
+ it('should create a sync root factory', () => {
const metaFactory = createServiceFactory({
service: rootDep,
deps: {},
@@ -75,7 +75,7 @@ describe('createServiceFactory', () => {
metaFactory();
});
- it('should create a factory with no options', () => {
+ it('should create a factory', () => {
const metaFactory = createServiceFactory({
service: ref,
deps: {},
@@ -100,7 +100,7 @@ describe('createServiceFactory', () => {
metaFactory();
});
- it('should create a factory with optional options', () => {
+ it('should create a factory with options', () => {
const metaFactory = createServiceFactory((_opts?: { x: number }) => ({
service: ref,
deps: {},
@@ -124,7 +124,8 @@ describe('createServiceFactory', () => {
metaFactory();
});
- it('should create a factory with required options', () => {
+ it('should not be allowed to require options', () => {
+ // @ts-expect-error
const metaFactory = createServiceFactory((_opts: { x: number }) => ({
service: ref,
deps: {},
@@ -134,23 +135,9 @@ describe('createServiceFactory', () => {
},
}));
expect(metaFactory).toEqual(expect.any(Function));
-
- // @ts-expect-error
- metaFactory('string');
- // @ts-expect-error
- metaFactory({});
- metaFactory({ x: 1 });
- // @ts-expect-error
- metaFactory({ x: 1, y: 2 });
- // @ts-expect-error
- metaFactory(null);
- // @ts-expect-error
- metaFactory(undefined);
- // @ts-expect-error
- metaFactory();
});
- it('should create a factory with optional options as interface', () => {
+ it('should create a factory with options as interface', () => {
const metaFactory = createServiceFactory((_opts?: TestOptions) => ({
service: ref,
deps: {},
@@ -174,32 +161,6 @@ describe('createServiceFactory', () => {
metaFactory();
});
- it('should create a factory with required options as interface', () => {
- const metaFactory = createServiceFactory((_opts: TestOptions) => ({
- service: ref,
- deps: {},
- async createRootContext() {},
- async factory() {
- return 'x';
- },
- }));
- expect(metaFactory).toEqual(expect.any(Function));
-
- // @ts-expect-error
- metaFactory('string');
- // @ts-expect-error
- metaFactory({});
- metaFactory({ x: 1 });
- // @ts-expect-error
- metaFactory({ x: 1, y: 2 });
- // @ts-expect-error
- metaFactory(null);
- // @ts-expect-error
- metaFactory(undefined);
- // @ts-expect-error
- metaFactory();
- });
-
it('should create root scoped factory with dependencies', () => {
const metaFactory = createServiceFactory({
service: createServiceRef({ id: 'foo', scope: 'root' }),
@@ -226,7 +187,7 @@ describe('createServiceFactory', () => {
metaFactory();
});
- it('should create root scoped factory with dependencies and optional options', () => {
+ it('should create root scoped factory with dependencies and options', () => {
const metaFactory = createServiceFactory((_options?: TestOptions) => ({
service: createServiceRef({ id: 'foo', scope: 'root' }),
deps: {
@@ -325,49 +286,7 @@ describe('createServiceFactory', () => {
metaFactory();
});
- it('should create factory with required options and dependencies', () => {
- const metaFactory = createServiceFactory((_opts: TestOptions) => ({
- service: ref,
- deps: {
- root: rootDep,
- plugin: pluginDep,
- },
- async createRootContext({ root }) {
- const root1: number = root;
- // @ts-expect-error
- const root2: string = root;
- unused(root1, root2);
- return { root };
- },
- async factory({ plugin }, { root }) {
- const root1: number = root;
- // @ts-expect-error
- const root2: string = root;
- const plugin3: boolean = plugin;
- // @ts-expect-error
- const plugin4: number = plugin;
- unused(root1, root2, plugin3, plugin4);
- return 'x';
- },
- }));
- expect(metaFactory).toEqual(expect.any(Function));
-
- // @ts-expect-error
- metaFactory('string');
- // @ts-expect-error
- metaFactory({});
- metaFactory({ x: 1 });
- // @ts-expect-error
- metaFactory({ x: 1, y: 2 });
- // @ts-expect-error
- metaFactory(null);
- // @ts-expect-error
- metaFactory(undefined);
- // @ts-expect-error
- metaFactory();
- });
-
- it('should create factory with optional options and dependencies', () => {
+ it('should create factory with options and dependencies', () => {
const metaFactory = createServiceFactory((_opts?: TestOptions) => ({
service: ref,
deps: {
diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts
index 190ef7c13e..bea53dc1ed 100644
--- a/packages/backend-plugin-api/src/services/system/types.ts
+++ b/packages/backend-plugin-api/src/services/system/types.ts
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+import { BackendFeature } from '../../types';
+
/**
* TODO
*
@@ -51,9 +53,7 @@ export type ServiceRef<
export interface ServiceFactory<
TService = unknown,
TScope extends 'plugin' | 'root' = 'plugin' | 'root',
-> {
- $$type: '@backstage/ServiceFactory';
-
+> extends BackendFeature {
service: ServiceRef;
}
@@ -191,20 +191,6 @@ export function createServiceFactory<
>(
config: (options?: TOpts) => RootServiceFactoryConfig,
): (options?: TOpts) => ServiceFactory;
-/**
- * Creates a root scoped service factory with required options.
- *
- * @public
- * @param config - The service factory configuration.
- */
-export function createServiceFactory<
- TService,
- TImpl extends TService,
- TDeps extends { [name in string]: ServiceRef },
- TOpts extends object | undefined = undefined,
->(
- config: (options: TOpts) => RootServiceFactoryConfig,
-): (options: TOpts) => ServiceFactory;
/**
* Creates a plugin scoped service factory without options.
*
@@ -237,25 +223,6 @@ export function createServiceFactory<
options?: TOpts,
) => PluginServiceFactoryConfig,
): (options?: TOpts) => ServiceFactory;
-/**
- * Creates a plugin scoped service factory with required options.
- *
- * @public
- * @param config - The service factory configuration.
- */
-export function createServiceFactory<
- TService,
- TImpl extends TService,
- TDeps extends { [name in string]: ServiceRef },
- TContext = undefined,
- TOpts extends object | undefined = undefined,
->(
- config:
- | PluginServiceFactoryConfig
- | ((
- options: TOpts,
- ) => PluginServiceFactoryConfig),
-): (options: TOpts) => ServiceFactory;
export function createServiceFactory<
TService,
TImpl extends TService,
@@ -281,7 +248,7 @@ export function createServiceFactory<
if (anyConf.service.scope === 'root') {
const c = anyConf as RootServiceFactoryConfig;
return {
- $$type: '@backstage/ServiceFactory',
+ $$type: '@backstage/BackendFeature',
version: 'v1',
service: c.service,
deps: c.deps,
@@ -295,7 +262,7 @@ export function createServiceFactory<
TDeps
>;
return {
- $$type: '@backstage/ServiceFactory',
+ $$type: '@backstage/BackendFeature',
version: 'v1',
service: c.service,
...('createRootContext' in c
diff --git a/packages/backend-plugin-api/src/types.ts b/packages/backend-plugin-api/src/types.ts
new file mode 100644
index 0000000000..59f1106fab
--- /dev/null
+++ b/packages/backend-plugin-api/src/types.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023 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.
+ */
+
+/** @internal */
+export interface BackendFeatureFactory<
+ TOptions extends [options?: object] = [],
+> {
+ (...options: TOptions): BackendFeature;
+ $$type: '@backstage/BackendFeatureFactory';
+}
+
+/** @public */
+export interface BackendFeature {
+ // NOTE: This type is opaque in order to simplify future API evolution.
+ $$type: '@backstage/BackendFeature';
+}
diff --git a/packages/backend-plugin-api/src/wiring/factories.test.ts b/packages/backend-plugin-api/src/wiring/factories.test.ts
index ae199d84e4..1fd05a1605 100644
--- a/packages/backend-plugin-api/src/wiring/factories.test.ts
+++ b/packages/backend-plugin-api/src/wiring/factories.test.ts
@@ -33,22 +33,19 @@ describe('createExtensionPoint', () => {
describe('createBackendPlugin', () => {
it('should create a BackendPlugin', () => {
- const plugin = createBackendPlugin((_options: { a: string }) => ({
+ const plugin = createBackendPlugin({
pluginId: 'x',
register(r) {
r.registerInit({ deps: {}, async init() {} });
},
- }));
+ });
expect(plugin).toBeDefined();
- expect(plugin({ a: 'a' })).toBeDefined();
- expect(plugin({ a: 'a' })).toEqual({
+ expect(plugin()).toEqual({
$$type: '@backstage/BackendFeature',
version: 'v1',
getRegistrations: expect.any(Function),
});
- expect(
- (plugin({ a: 'a' }) as InternalBackendFeature).getRegistrations(),
- ).toEqual([
+ expect((plugin() as InternalBackendFeature).getRegistrations()).toEqual([
{
type: 'plugin',
pluginId: 'x',
@@ -60,95 +57,32 @@ describe('createBackendPlugin', () => {
},
]);
// @ts-expect-error
- expect(plugin()).toBeDefined();
- // @ts-expect-error
- expect(plugin({ b: 'b' })).toBeDefined();
- });
-
- it('should create plugins with optional options', () => {
- const plugin = createBackendPlugin((_options?: { a: string }) => ({
- pluginId: 'x',
- register() {},
- }));
- expect(plugin).toBeDefined();
expect(plugin({ a: 'a' })).toBeDefined();
- expect(plugin()).toBeDefined();
- // @ts-expect-error
- expect(plugin({ b: 'b' })).toBeDefined();
- });
-
- it('should create plugins without options', () => {
- const plugin = createBackendPlugin({
- pluginId: 'x',
- register() {},
- });
- expect(plugin).toBeDefined();
- // @ts-expect-error
- expect(plugin({ a: 'a' })).toBeDefined();
- // @ts-expect-error
- expect(plugin({})).toBeDefined();
- });
-
- it('should create a BackendPlugin with options as interface', () => {
- interface TestOptions {
- a: string;
- }
- const plugin = createBackendPlugin((_options: TestOptions) => ({
- pluginId: 'x',
- register() {},
- }));
- expect(plugin).toBeDefined();
- expect(plugin({ a: 'a' })).toBeDefined();
- expect(plugin({ a: 'a' })).toEqual({
- $$type: '@backstage/BackendFeature',
- version: 'v1',
- getRegistrations: expect.any(Function),
- });
- // @ts-expect-error
- expect(plugin()).toBeDefined();
- // @ts-expect-error
- expect(plugin({ b: 'b' })).toBeDefined();
- });
-
- it('should create plugins with optional options as interface', () => {
- interface TestOptions {
- a: string;
- }
- const plugin = createBackendPlugin((_options?: TestOptions) => ({
- pluginId: 'x',
- register() {},
- }));
- expect(plugin).toBeDefined();
- expect(plugin({ a: 'a' })).toBeDefined();
- expect(plugin()).toBeDefined();
- // @ts-expect-error
- expect(plugin({ b: 'b' })).toBeDefined();
});
});
describe('createBackendModule', () => {
it('should create a BackendModule', () => {
- const mod = createBackendModule((_options: { a: string }) => ({
+ const mod = createBackendModule({
pluginId: 'x',
moduleId: 'y',
register(r) {
r.registerInit({ deps: {}, async init() {} });
},
- }));
+ });
expect(mod).toBeDefined();
- expect(mod({ a: 'a' })).toBeDefined();
- expect(mod({ a: 'a' })).toEqual({
+ expect(mod()).toBeDefined();
+ expect(mod()).toEqual({
$$type: '@backstage/BackendFeature',
version: 'v1',
getRegistrations: expect.any(Function),
});
- expect(
- (mod({ a: 'a' }) as InternalBackendFeature).getRegistrations(),
- ).toEqual([
+ expect((mod() as InternalBackendFeature).getRegistrations()).toEqual([
{
type: 'module',
pluginId: 'x',
moduleId: 'y',
+ extensionPoints: [],
init: {
deps: expect.any(Object),
func: expect.any(Function),
@@ -156,72 +90,6 @@ describe('createBackendModule', () => {
},
]);
// @ts-expect-error
- expect(mod()).toBeDefined();
- // @ts-expect-error
- expect(mod({ b: 'b' })).toBeDefined();
- });
-
- it('should create modules with optional options', () => {
- const mod = createBackendModule((_options?: { a: string }) => ({
- pluginId: 'x',
- moduleId: 'y',
- register() {},
- }));
- expect(mod).toBeDefined();
expect(mod({ a: 'a' })).toBeDefined();
- expect(mod()).toBeDefined();
- // @ts-expect-error
- expect(mod({ b: 'b' })).toBeDefined();
- });
-
- it('should create modules without options', () => {
- const mod = createBackendModule({
- pluginId: 'x',
- moduleId: 'y',
- register() {},
- });
- expect(mod).toBeDefined();
- // @ts-expect-error
- expect(mod({ a: 'a' })).toBeDefined();
- // @ts-expect-error
- expect(mod({})).toBeDefined();
- });
-
- it('should create a BackendModule as interface', () => {
- interface TestOptions {
- a: string;
- }
- const mod = createBackendModule((_options: TestOptions) => ({
- pluginId: 'x',
- moduleId: 'y',
- register() {},
- }));
- expect(mod).toBeDefined();
- expect(mod({ a: 'a' })).toBeDefined();
- expect(mod({ a: 'a' })).toEqual({
- $$type: '@backstage/BackendFeature',
- version: 'v1',
- getRegistrations: expect.any(Function),
- });
- // @ts-expect-error
- expect(mod()).toBeDefined();
- // @ts-expect-error
- expect(mod({ b: 'b' })).toBeDefined();
- });
-
- it('should create modules with optional options as interface', () => {
- interface TestOptions {
- a: string;
- }
- const mod = createBackendModule((_options?: TestOptions) => ({
- pluginId: 'x',
- moduleId: 'y',
- register() {},
- }));
- expect(mod).toBeDefined();
- expect(mod({ a: 'a' })).toBeDefined();
- expect(mod()).toBeDefined();
- // @ts-expect-error
- expect(mod({ b: 'b' })).toBeDefined();
});
});
diff --git a/packages/backend-plugin-api/src/wiring/factories.ts b/packages/backend-plugin-api/src/wiring/factories.ts
index 928c933af0..9fa0f87bab 100644
--- a/packages/backend-plugin-api/src/wiring/factories.ts
+++ b/packages/backend-plugin-api/src/wiring/factories.ts
@@ -14,12 +14,11 @@
* limitations under the License.
*/
+import { BackendFeature, BackendFeatureFactory } from '../types';
import {
BackendModuleRegistrationPoints,
BackendPluginRegistrationPoints,
- BackendFeature,
ExtensionPoint,
- InternalBackendFeature,
InternalBackendModuleRegistration,
InternalBackendPluginRegistration,
} from './types';
@@ -85,13 +84,10 @@ export interface BackendPluginConfig {
* @see {@link https://backstage.io/docs/backend-system/architecture/plugins | The architecture of plugins}
* @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns}
*/
-export function createBackendPlugin(
- config: BackendPluginConfig | ((...params: TOptions) => BackendPluginConfig),
-): (...params: TOptions) => BackendFeature {
- const configCallback = typeof config === 'function' ? config : () => config;
- return (...options: TOptions): InternalBackendFeature => {
- const c = configCallback(...options);
-
+export function createBackendPlugin(
+ config: BackendPluginConfig,
+): () => BackendFeature {
+ const factory: BackendFeatureFactory = () => {
let registrations: InternalBackendPluginRegistration[];
return {
@@ -106,7 +102,7 @@ export function createBackendPlugin(
let init: InternalBackendPluginRegistration['init'] | undefined =
undefined;
- c.register({
+ config.register({
registerExtensionPoint(ext, impl) {
if (init) {
throw new Error(
@@ -128,14 +124,14 @@ export function createBackendPlugin(
if (!init) {
throw new Error(
- `registerInit was not called by register in ${c.pluginId}`,
+ `registerInit was not called by register in ${config.pluginId}`,
);
}
registrations = [
{
type: 'plugin',
- pluginId: c.pluginId,
+ pluginId: config.pluginId,
extensionPoints,
init,
},
@@ -144,6 +140,9 @@ export function createBackendPlugin(
},
};
};
+ factory.$$type = '@backstage/BackendFeatureFactory';
+
+ return factory;
}
/**
@@ -155,13 +154,14 @@ export function createBackendPlugin(
*/
export interface BackendModuleConfig {
/**
- * The ID of this plugin.
+ * Should exactly match the `id` of the plugin that the module extends.
*
* @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns}
*/
pluginId: string;
+
/**
- * Should exactly match the `id` of the plugin that the module extends.
+ * The ID of this module, used to identify the module and ensure that it is not installed twice.
*/
moduleId: string;
register(reg: BackendModuleRegistrationPoints): void;
@@ -174,13 +174,10 @@ export interface BackendModuleConfig {
* @see {@link https://backstage.io/docs/backend-system/architecture/modules | The architecture of modules}
* @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns}
*/
-export function createBackendModule(
- config: BackendModuleConfig | ((...params: TOptions) => BackendModuleConfig),
-): (...params: TOptions) => BackendFeature {
- const configCallback = typeof config === 'function' ? config : () => config;
- return (...options: TOptions): InternalBackendFeature => {
- const c = configCallback(...options);
-
+export function createBackendModule(
+ config: BackendModuleConfig,
+): () => BackendFeature {
+ const factory: BackendFeatureFactory = () => {
let registrations: InternalBackendModuleRegistration[];
return {
@@ -190,10 +187,20 @@ export function createBackendModule(
if (registrations) {
return registrations;
}
+ const extensionPoints: InternalBackendPluginRegistration['extensionPoints'] =
+ [];
let init: InternalBackendModuleRegistration['init'] | undefined =
undefined;
- c.register({
+ config.register({
+ registerExtensionPoint(ext, impl) {
+ if (init) {
+ throw new Error(
+ 'registerExtensionPoint called after registerInit',
+ );
+ }
+ extensionPoints.push([ext, impl]);
+ },
registerInit(regInit) {
if (init) {
throw new Error('registerInit must only be called once');
@@ -207,15 +214,16 @@ export function createBackendModule(
if (!init) {
throw new Error(
- `registerInit was not called by register in ${c.moduleId} module for ${c.pluginId}`,
+ `registerInit was not called by register in ${config.moduleId} module for ${config.pluginId}`,
);
}
registrations = [
{
type: 'module',
- pluginId: c.pluginId,
- moduleId: c.moduleId,
+ pluginId: config.pluginId,
+ moduleId: config.moduleId,
+ extensionPoints,
init,
},
];
@@ -223,4 +231,7 @@ export function createBackendModule(
},
};
};
+ factory.$$type = '@backstage/BackendFeatureFactory';
+
+ return factory;
}
diff --git a/packages/backend-plugin-api/src/wiring/index.ts b/packages/backend-plugin-api/src/wiring/index.ts
index 9cb767b8ac..49f15c0b55 100644
--- a/packages/backend-plugin-api/src/wiring/index.ts
+++ b/packages/backend-plugin-api/src/wiring/index.ts
@@ -27,6 +27,5 @@ export {
export type {
BackendModuleRegistrationPoints,
BackendPluginRegistrationPoints,
- BackendFeature,
ExtensionPoint,
} from './types';
diff --git a/packages/backend-plugin-api/src/wiring/types.ts b/packages/backend-plugin-api/src/wiring/types.ts
index c81ee3ceb7..55d43ccb7c 100644
--- a/packages/backend-plugin-api/src/wiring/types.ts
+++ b/packages/backend-plugin-api/src/wiring/types.ts
@@ -15,6 +15,7 @@
*/
import { ServiceRef } from '../services/system/types';
+import { BackendFeature } from '../types';
/**
* TODO
@@ -59,6 +60,10 @@ export interface BackendPluginRegistrationPoints {
* @public
*/
export interface BackendModuleRegistrationPoints {
+ registerExtensionPoint(
+ ref: ExtensionPoint,
+ impl: TExtensionPoint,
+ ): void;
registerInit(options: {
deps: {
[name in keyof Deps]: ServiceRef | ExtensionPoint;
@@ -67,12 +72,6 @@ export interface BackendModuleRegistrationPoints {
}): void;
}
-/** @public */
-export interface BackendFeature {
- // NOTE: This type is opaque in order to simplify future API evolution.
- $$type: '@backstage/BackendFeature';
-}
-
/** @internal */
export interface InternalBackendFeature extends BackendFeature {
version: 'v1';
@@ -97,6 +96,7 @@ export interface InternalBackendModuleRegistration {
pluginId: string;
moduleId: string;
type: 'module';
+ extensionPoints: Array, unknown]>;
init: {
deps: Record | ExtensionPoint>;
func(deps: Record): Promise;
diff --git a/packages/backend-tasks/CHANGELOG.md b/packages/backend-tasks/CHANGELOG.md
index ec1dd319ea..11a039e053 100644
--- a/packages/backend-tasks/CHANGELOG.md
+++ b/packages/backend-tasks/CHANGELOG.md
@@ -1,5 +1,34 @@
# @backstage/backend-tasks
+## 0.5.5
+
+### Patch Changes
+
+- dfd1b6b2fc33: Make `readTaskScheduleDefinitionFromConfig` properly handle bad inputs
+- Updated dependencies
+ - @backstage/backend-common@0.19.2
+ - @backstage/config@1.0.8
+ - @backstage/errors@1.2.1
+ - @backstage/types@1.1.0
+
+## 0.5.5-next.2
+
+### Patch Changes
+
+- dfd1b6b2fc33: Make `readTaskScheduleDefinitionFromConfig` properly handle bad inputs
+- Updated dependencies
+ - @backstage/backend-common@0.19.2-next.2
+
+## 0.5.5-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.19.2-next.1
+ - @backstage/config@1.0.8
+ - @backstage/errors@1.2.1
+ - @backstage/types@1.1.0
+
## 0.5.5-next.0
### Patch Changes
diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json
index 4c0e4fd27e..ca9129078e 100644
--- a/packages/backend-tasks/package.json
+++ b/packages/backend-tasks/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-tasks",
"description": "Common distributed task management library for Backstage backends",
- "version": "0.5.5-next.0",
+ "version": "0.5.5",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
diff --git a/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.test.ts b/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.test.ts
index fc2a045614..21f51954b5 100644
--- a/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.test.ts
+++ b/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.test.ts
@@ -76,7 +76,7 @@ describe('readTaskScheduleDefinitionFromConfig', () => {
);
});
- it('invalid frequency value', () => {
+ it('invalid frequency key', () => {
const config = new ConfigReader({
frequency: {
invalid: 'value',
@@ -89,6 +89,19 @@ describe('readTaskScheduleDefinitionFromConfig', () => {
);
});
+ it('invalid frequency value', () => {
+ const config = new ConfigReader({
+ frequency: {
+ minutes: 'value',
+ },
+ timeout: 'PT3M',
+ });
+
+ expect(() => readTaskScheduleDefinitionFromConfig(config)).toThrow(
+ "Unable to convert config value for key 'frequency.minutes' in 'mock-config' to a number",
+ );
+ });
+
it('frequency value with additional invalid prop', () => {
const config = new ConfigReader({
frequency: {
diff --git a/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts b/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts
index 1448a328f8..b31cf68f74 100644
--- a/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts
+++ b/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts
@@ -15,7 +15,7 @@
*/
import { Config } from '@backstage/config';
-import { HumanDuration, JsonObject } from '@backstage/types';
+import { HumanDuration } from '@backstage/types';
import { TaskScheduleDefinition } from './types';
import { Duration } from 'luxon';
@@ -31,29 +31,48 @@ const propsOfHumanDuration = [
];
function convertToHumanDuration(config: Config, key: string): HumanDuration {
- const props = config.getConfig(key).keys();
- if (!props.find(prop => propsOfHumanDuration.includes(prop))) {
+ // Ensures that the root is an object
+ const root = config.getConfig(key);
+
+ const result: Record = {};
+ let found = false;
+ for (const prop of propsOfHumanDuration) {
+ const value = root.getOptionalNumber(prop);
+ if (value !== undefined) {
+ result[prop] = value;
+ found = true;
+ }
+ }
+
+ if (!found) {
throw new Error(
`HumanDuration needs at least one of: ${propsOfHumanDuration}`,
);
}
- const invalidProps = props.filter(
- prop => !propsOfHumanDuration.includes(prop),
- );
+ const invalidProps = root
+ .keys()
+ .filter(prop => !propsOfHumanDuration.includes(prop));
if (invalidProps.length > 0) {
throw new Error(
`HumanDuration does not contain properties: ${invalidProps}`,
);
}
- return config.get(key) as HumanDuration;
+ return result as HumanDuration;
}
function readDuration(config: Config, key: string): Duration | HumanDuration {
- return typeof config.get(key) === 'string'
- ? Duration.fromISO(config.getString(key))
- : convertToHumanDuration(config, key);
+ if (typeof config.get(key) === 'string') {
+ const value = config.getString(key);
+ const duration = Duration.fromISO(value);
+ if (!duration.isValid) {
+ throw new Error(`Invalid duration: ${value}`);
+ }
+ return duration;
+ }
+
+ return convertToHumanDuration(config, key);
}
function readCronOrDuration(
diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md
index 2d40873bb9..d6eec9f7ed 100644
--- a/packages/backend-test-utils/CHANGELOG.md
+++ b/packages/backend-test-utils/CHANGELOG.md
@@ -1,5 +1,61 @@
# @backstage/backend-test-utils
+## 0.2.0
+
+### Minor Changes
+
+- b9c57a4f857e: **BREAKING**: Renamed `mockServices.config` to `mockServices.rootConfig`.
+- a6d7983f349c: **BREAKING**: Removed the `services` option from `createBackend`. Service factories are now `BackendFeature`s and should be installed with `backend.add(...)` instead. The following should be migrated:
+
+ ```ts
+ const backend = createBackend({ services: [myCustomServiceFactory] });
+ ```
+
+ To instead pass the service factory via `backend.add(...)`:
+
+ ```ts
+ const backend = createBackend();
+ backend.add(customRootLoggerServiceFactory);
+ ```
+
+### Patch Changes
+
+- ae9304818136: Add needed constants and constructs to support PostgreSQL version 14 as test database
+- Updated dependencies
+ - @backstage/backend-common@0.19.2
+ - @backstage/backend-app-api@0.5.0
+ - @backstage/backend-plugin-api@0.6.0
+ - @backstage/plugin-auth-node@0.2.17
+ - @backstage/config@1.0.8
+ - @backstage/types@1.1.0
+
+## 0.2.0-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-app-api@0.5.0-next.2
+ - @backstage/backend-plugin-api@0.6.0-next.2
+ - @backstage/backend-common@0.19.2-next.2
+ - @backstage/plugin-auth-node@0.2.17-next.2
+
+## 0.2.0-next.1
+
+### Minor Changes
+
+- b9c57a4f857e: **BREAKING**: Renamed `mockServices.config` to `mockServices.rootConfig`.
+
+### Patch Changes
+
+- ae9304818136: Add needed constants and constructs to support PostgreSQL version 14 as test database
+- Updated dependencies
+ - @backstage/backend-common@0.19.2-next.1
+ - @backstage/plugin-auth-node@0.2.17-next.1
+ - @backstage/backend-app-api@0.5.0-next.1
+ - @backstage/backend-plugin-api@0.6.0-next.1
+ - @backstage/config@1.0.8
+ - @backstage/types@1.1.0
+
## 0.1.40-next.0
### Patch Changes
diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md
index 358b51071f..d086b1d2ae 100644
--- a/packages/backend-test-utils/api-report.md
+++ b/packages/backend-test-utils/api-report.md
@@ -6,7 +6,6 @@
import { Backend } from '@backstage/backend-app-api';
import { BackendFeature } from '@backstage/backend-plugin-api';
import { CacheService } from '@backstage/backend-plugin-api';
-import { ConfigService } from '@backstage/backend-plugin-api';
import { DatabaseService } from '@backstage/backend-plugin-api';
import { ExtendedHttpServer } from '@backstage/backend-app-api';
import { ExtensionPoint } from '@backstage/backend-plugin-api';
@@ -18,10 +17,10 @@ import { Knex } from 'knex';
import { LifecycleService } from '@backstage/backend-plugin-api';
import { LoggerService } from '@backstage/backend-plugin-api';
import { PermissionsService } from '@backstage/backend-plugin-api';
+import { RootConfigService } from '@backstage/backend-plugin-api';
import { RootLifecycleService } from '@backstage/backend-plugin-api';
import { SchedulerService } from '@backstage/backend-plugin-api';
import { ServiceFactory } from '@backstage/backend-plugin-api';
-import { ServiceRef } from '@backstage/backend-plugin-api';
import { TokenManagerService } from '@backstage/backend-plugin-api';
import { UrlReaderService } from '@backstage/backend-plugin-api';
@@ -36,19 +35,6 @@ export namespace mockServices {
factory: () => ServiceFactory;
}
// (undocumented)
- export function config(options?: config.Options): ConfigService;
- // (undocumented)
- export namespace config {
- // (undocumented)
- export type Options = {
- data?: JsonObject;
- };
- const // (undocumented)
- factory: (
- options?: Options | undefined,
- ) => ServiceFactory;
- }
- // (undocumented)
export namespace database {
const // (undocumented)
factory: () => ServiceFactory;
@@ -83,6 +69,19 @@ export namespace mockServices {
factory: () => ServiceFactory;
}
// (undocumented)
+ export function rootConfig(options?: rootConfig.Options): RootConfigService;
+ // (undocumented)
+ export namespace rootConfig {
+ // (undocumented)
+ export type Options = {
+ data?: JsonObject;
+ };
+ const // (undocumented)
+ factory: (
+ options?: Options | undefined,
+ ) => ServiceFactory;
+ }
+ // (undocumented)
export namespace rootLifecycle {
const // (undocumented)
factory: () => ServiceFactory;
@@ -127,11 +126,8 @@ export function setupRequestMockHandlers(worker: {
}): void;
// @public (undocumented)
-export function startTestBackend<
- TServices extends any[],
- TExtensionPoints extends any[],
->(
- options: TestBackendOptions,
+export function startTestBackend(
+ options: TestBackendOptions,
): Promise;
// @public (undocumented)
@@ -140,10 +136,7 @@ export interface TestBackend extends Backend {
}
// @public (undocumented)
-export interface TestBackendOptions<
- TServices extends any[],
- TExtensionPoints extends any[],
-> {
+export interface TestBackendOptions {
// (undocumented)
extensionPoints?: readonly [
...{
@@ -154,20 +147,12 @@ export interface TestBackendOptions<
},
];
// (undocumented)
- features?: BackendFeature[];
- // (undocumented)
- services?: readonly [
- ...{
- [index in keyof TServices]:
- | ServiceFactory
- | (() => ServiceFactory)
- | [ServiceRef, Partial];
- },
- ];
+ features?: Array BackendFeature)>;
}
// @public
export type TestDatabaseId =
+ | 'POSTGRES_14'
| 'POSTGRES_13'
| 'POSTGRES_12'
| 'POSTGRES_11'
diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json
index d9d904357b..02cb1d5cf9 100644
--- a/packages/backend-test-utils/package.json
+++ b/packages/backend-test-utils/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-test-utils",
"description": "Test helpers library for Backstage backends",
- "version": "0.1.40-next.0",
+ "version": "0.2.0",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
diff --git a/packages/backend-test-utils/src/database/TestDatabases.test.ts b/packages/backend-test-utils/src/database/TestDatabases.test.ts
index be29a42806..d766d38815 100644
--- a/packages/backend-test-utils/src/database/TestDatabases.test.ts
+++ b/packages/backend-test-utils/src/database/TestDatabases.test.ts
@@ -59,6 +59,37 @@ describe('TestDatabases', () => {
describe('each connect', () => {
const dbs = TestDatabases.create();
+ itIfDocker(
+ 'obeys a provided connection string for postgres 14',
+ async () => {
+ const { host, port, user, password, stop } =
+ await startPostgresContainer('postgres:14');
+
+ try {
+ // Leave a mark
+ process.env.BACKSTAGE_TEST_DATABASE_POSTGRES14_CONNECTION_STRING = `postgresql://${user}:${password}@${host}:${port}`;
+ const input = await dbs.init('POSTGRES_14');
+ await input.schema.createTable('a', table =>
+ table.string('x').primary(),
+ );
+ await input.insert({ x: 'y' }).into('a');
+
+ // Look for the mark
+ const database = input.client.config.connection.database;
+ const output = knexFactory({
+ client: 'pg',
+ connection: { host, port, user, password, database },
+ });
+ // eslint-disable-next-line jest/no-standalone-expect
+ await expect(output.select('x').from('a')).resolves.toEqual([
+ { x: 'y' },
+ ]);
+ } finally {
+ await stop();
+ }
+ },
+ );
+
itIfDocker(
'obeys a provided connection string for postgres 13',
async () => {
diff --git a/packages/backend-test-utils/src/database/types.ts b/packages/backend-test-utils/src/database/types.ts
index 8c1d6f5193..a7bfdeb12d 100644
--- a/packages/backend-test-utils/src/database/types.ts
+++ b/packages/backend-test-utils/src/database/types.ts
@@ -24,6 +24,7 @@ import { getDockerImageForName } from '../util/getDockerImageForName';
* @public
*/
export type TestDatabaseId =
+ | 'POSTGRES_14'
| 'POSTGRES_13'
| 'POSTGRES_12'
| 'POSTGRES_11'
@@ -46,6 +47,13 @@ export type Instance = {
export const allDatabases: Record =
Object.freeze({
+ POSTGRES_14: {
+ name: 'Postgres 14.x',
+ driver: 'pg',
+ dockerImageName: getDockerImageForName('postgres:14'),
+ connectionStringEnvironmentVariableName:
+ 'BACKSTAGE_TEST_DATABASE_POSTGRES14_CONNECTION_STRING',
+ },
POSTGRES_13: {
name: 'Postgres 13.x',
driver: 'pg',
diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts
index 8a3125214c..0455b9f1ae 100644
--- a/packages/backend-test-utils/src/next/services/mockServices.ts
+++ b/packages/backend-test-utils/src/next/services/mockServices.ts
@@ -15,7 +15,7 @@
*/
import {
- ConfigService,
+ RootConfigService,
coreServices,
createServiceFactory,
IdentityService,
@@ -61,13 +61,13 @@ function simpleFactory<
* @public
*/
export namespace mockServices {
- export function config(options?: config.Options): ConfigService {
+ export function rootConfig(options?: rootConfig.Options): RootConfigService {
return new ConfigReader(options?.data, 'mock-config');
}
- export namespace config {
+ export namespace rootConfig {
export type Options = { data?: JsonObject };
- export const factory = simpleFactory(coreServices.config, config);
+ export const factory = simpleFactory(coreServices.rootConfig, rootConfig);
}
export function rootLogger(options?: rootLogger.Options): LoggerService {
diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts
index aa93a7dae1..b127712927 100644
--- a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts
+++ b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts
@@ -31,7 +31,6 @@ import { startTestBackend } from './TestBackend';
let globalTestBackendHasBeenStopped = false;
beforeAll(async () => {
await startTestBackend({
- services: [],
features: [
createBackendModule({
moduleId: 'test.module',
@@ -46,7 +45,7 @@ beforeAll(async () => {
},
});
},
- })(),
+ }),
],
});
});
@@ -68,17 +67,37 @@ describe('TestBackend', () => {
const extensionPoint5 = createExtensionPoint({ id: 'b5' });
await expect(
startTestBackend({
- services: [
+ features: [
// @ts-expect-error
[extensionPoint1, { a: 'a' }],
- [serviceRef, { a: 'a' }],
- [serviceRef, { a: 'a', b: 'b' }],
- // @ts-expect-error
- [serviceRef, { c: 'c' }],
- // @ts-expect-error
- [serviceRef, { a: 'a', c: 'c' }],
- // @ts-expect-error
- [serviceRef, { a: 'a', b: 'b', c: 'c' }],
+ createServiceFactory(() => ({
+ service: serviceRef,
+ deps: {},
+ // @ts-expect-error
+ factory: async () => ({ a: 'a' }),
+ })),
+ createServiceFactory(() => ({
+ service: serviceRef,
+ deps: {},
+ factory: async () => ({ a: 'a', b: 'b' }),
+ })),
+ createServiceFactory(() => ({
+ service: serviceRef,
+ deps: {},
+ // @ts-expect-error
+ factory: async () => ({ c: 'c' }),
+ })),
+ createServiceFactory(() => ({
+ service: serviceRef,
+ deps: {},
+ // @ts-expect-error
+ factory: async () => ({ a: 'a', c: 'c' }),
+ })),
+ createServiceFactory(() => ({
+ service: serviceRef,
+ deps: {},
+ factory: async () => ({ a: 'a', b: 'b', c: 'c' }),
+ })),
],
extensionPoints: [
// @ts-expect-error
@@ -124,8 +143,7 @@ describe('TestBackend', () => {
});
await startTestBackend({
- services: [sf],
- features: [testModule()],
+ features: [testModule(), sf()],
});
expect(testFn).toHaveBeenCalledWith('winning');
@@ -150,7 +168,6 @@ describe('TestBackend', () => {
});
const backend = await startTestBackend({
- services: [],
features: [testModule()],
});
@@ -168,7 +185,7 @@ describe('TestBackend', () => {
env.registerInit({
deps: {
cache: coreServices.cache,
- config: coreServices.config,
+ config: coreServices.rootConfig,
database: coreServices.database,
discovery: coreServices.discovery,
httpRouter: coreServices.httpRouter,
@@ -192,7 +209,6 @@ describe('TestBackend', () => {
});
await startTestBackend({
- services: [],
features: [testPlugin()],
});
});
@@ -220,4 +236,89 @@ describe('TestBackend', () => {
expect(res.status).toEqual(200);
expect(res.body).toEqual({ message: 'pong' });
});
+
+ it('should provide extension point implementations', async () => {
+ expect.assertions(3);
+
+ const extensionPointA = createExtensionPoint({ id: 'a' });
+ const extensionPointB = createExtensionPoint({ id: 'b' });
+ await expect(
+ startTestBackend({
+ extensionPoints: [
+ [extensionPointA, 'a'],
+ [extensionPointB, 'b'],
+ ],
+ features: [
+ createBackendModule({
+ pluginId: 'testA',
+ moduleId: 'test',
+ register(reg) {
+ reg.registerInit({
+ deps: { ext: extensionPointA },
+ async init({ ext }) {
+ expect(ext).toBe('a');
+ },
+ });
+ },
+ }),
+ createBackendModule({
+ pluginId: 'testB',
+ moduleId: 'test',
+ register(reg) {
+ reg.registerInit({
+ deps: { ext: extensionPointB },
+ async init({ ext }) {
+ expect(ext).toBe('b');
+ },
+ });
+ },
+ }),
+ ],
+ }),
+ ).resolves.not.toBeUndefined();
+ });
+
+ it('should reject extension point used by multiple plugins', async () => {
+ const extensionPointA = createExtensionPoint({ id: 'a' });
+ await expect(
+ startTestBackend({
+ extensionPoints: [[extensionPointA, 'a']],
+ features: [
+ createBackendModule({
+ pluginId: 'testA',
+ moduleId: 'test',
+ register(reg) {
+ reg.registerInit({
+ deps: { ext: extensionPointA },
+ async init() {},
+ });
+ },
+ }),
+ createBackendModule({
+ pluginId: 'testB',
+ moduleId: 'test',
+ register(reg) {
+ reg.registerInit({
+ deps: { ext: extensionPointA },
+ async init() {},
+ });
+ },
+ }),
+ ],
+ }),
+ ).rejects.toThrow(
+ "Extension point registered for plugin 'testA' may not be used by module for plugin 'testB'",
+ );
+ });
+
+ it('should reject extension point not used by any plugin', async () => {
+ const extensionPointA = createExtensionPoint({ id: 'a' });
+ await expect(
+ startTestBackend({
+ extensionPoints: [[extensionPointA, 'a']],
+ }),
+ ).rejects.toThrow(
+ "Unable to determine the plugin ID of extension point(s) 'a'. Tested extension points must be depended on by one or more tested modules.",
+ );
+ });
});
diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.ts
index e8df15862f..97da3543ae 100644
--- a/packages/backend-test-utils/src/next/wiring/TestBackend.ts
+++ b/packages/backend-test-utils/src/next/wiring/TestBackend.ts
@@ -24,31 +24,21 @@ import {
} from '@backstage/backend-app-api';
import { HostDiscovery } from '@backstage/backend-common';
import {
- ServiceFactory,
- ServiceRef,
createServiceFactory,
BackendFeature,
ExtensionPoint,
coreServices,
- createBackendPlugin,
+ createBackendModule,
} from '@backstage/backend-plugin-api';
import { mockServices } from '../services';
import { ConfigReader } from '@backstage/config';
import express from 'express';
+// Direct internal import to avoid duplication
+// eslint-disable-next-line @backstage/no-forbidden-package-imports
+import { InternalBackendFeature } from '@backstage/backend-plugin-api/src/wiring/types';
/** @public */
-export interface TestBackendOptions<
- TServices extends any[],
- TExtensionPoints extends any[],
-> {
- services?: readonly [
- ...{
- [index in keyof TServices]:
- | ServiceFactory
- | (() => ServiceFactory)
- | [ServiceRef, Partial];
- },
- ];
+export interface TestBackendOptions {
extensionPoints?: readonly [
...{
[index in keyof TExtensionPoints]: [
@@ -57,7 +47,7 @@ export interface TestBackendOptions<
];
},
];
- features?: BackendFeature[];
+ features?: Array BackendFeature)>;
}
/** @public */
@@ -73,7 +63,7 @@ export interface TestBackend extends Backend {
const defaultServiceFactories = [
mockServices.cache.factory(),
- mockServices.config.factory(),
+ mockServices.rootConfig.factory(),
mockServices.database.factory(),
mockServices.httpRouter.factory(),
mockServices.identity.factory(),
@@ -87,28 +77,115 @@ const defaultServiceFactories = [
mockServices.urlReader.factory(),
];
+/**
+ * Given a set of extension points and plugins, find
+ * @returns
+ */
+function createExtensionPointTestModules(
+ features: Array BackendFeature)>,
+ extensionPointTuples?: readonly [
+ ref: ExtensionPoint,
+ impl: unknown,
+ ][],
+): Array<() => BackendFeature> {
+ if (!extensionPointTuples) {
+ return [];
+ }
+
+ const registrations = features.flatMap(featureOrFunction => {
+ const feature =
+ typeof featureOrFunction === 'function'
+ ? featureOrFunction()
+ : featureOrFunction;
+
+ if (feature.$$type !== '@backstage/BackendFeature') {
+ throw new Error(
+ `Failed to add feature, invalid type '${feature.$$type}'`,
+ );
+ }
+
+ if (isInternalBackendFeature(feature)) {
+ if (feature.version !== 'v1') {
+ throw new Error(
+ `Failed to add feature, invalid version '${feature.version}'`,
+ );
+ }
+ return feature.getRegistrations();
+ }
+ return [];
+ });
+
+ const extensionPointMap = new Map(
+ extensionPointTuples.map(ep => [ep[0].id, ep]),
+ );
+ const extensionPointsToSort = new Set(extensionPointMap.keys());
+ const extensionPointsByPlugin = new Map();
+
+ for (const registration of registrations) {
+ if (registration.type === 'module') {
+ const testDep = Object.values(registration.init.deps).filter(dep =>
+ extensionPointsToSort.has(dep.id),
+ );
+ if (testDep.length > 0) {
+ let points = extensionPointsByPlugin.get(registration.pluginId);
+ if (!points) {
+ points = [];
+ extensionPointsByPlugin.set(registration.pluginId, points);
+ }
+ for (const { id } of testDep) {
+ points.push(id);
+ extensionPointsToSort.delete(id);
+ }
+ }
+ }
+ }
+
+ if (extensionPointsToSort.size > 0) {
+ const list = Array.from(extensionPointsToSort)
+ .map(id => `'${id}'`)
+ .join(', ');
+ throw new Error(
+ `Unable to determine the plugin ID of extension point(s) ${list}. ` +
+ 'Tested extension points must be depended on by one or more tested modules.',
+ );
+ }
+
+ const modules = [];
+
+ for (const [pluginId, pluginExtensionPointIds] of extensionPointsByPlugin) {
+ modules.push(
+ createBackendModule({
+ pluginId,
+ moduleId: 'testExtensionPointRegistration',
+ register(reg) {
+ for (const id of pluginExtensionPointIds) {
+ const tuple = extensionPointMap.get(id)!;
+ reg.registerExtensionPoint(...tuple);
+ }
+
+ reg.registerInit({ deps: {}, async init() {} });
+ },
+ }),
+ );
+ }
+
+ return modules;
+}
+
const backendInstancesToCleanUp = new Array();
/** @public */
-export async function startTestBackend<
- TServices extends any[],
- TExtensionPoints extends any[],
->(
- options: TestBackendOptions,
+export async function startTestBackend(
+ options: TestBackendOptions,
): Promise {
- const {
- services = [],
- extensionPoints = [],
- features = [],
- ...otherOptions
- } = options;
+ const { extensionPoints, features = [], ...otherOptions } = options;
let server: ExtendedHttpServer;
const rootHttpRouterFactory = createServiceFactory({
service: coreServices.rootHttpRouter,
deps: {
- config: coreServices.config,
+ config: coreServices.rootConfig,
lifecycle: coreServices.rootLifecycle,
rootLogger: coreServices.rootLogger,
},
@@ -157,55 +234,20 @@ export async function startTestBackend<
},
});
- const factories = services.map(serviceDef => {
- if (Array.isArray(serviceDef)) {
- // if type is ExtensionPoint?
- // do something differently?
- const [ref, impl] = serviceDef;
- if (ref.scope === 'plugin') {
- return createServiceFactory({
- service: ref as ServiceRef,
- deps: {},
- factory: async () => impl,
- })();
- }
- return createServiceFactory({
- service: ref as ServiceRef,
- deps: {},
- factory: async () => impl,
- })();
- }
- if (typeof serviceDef === 'function') {
- return serviceDef();
- }
- return serviceDef as ServiceFactory;
- });
-
- for (const factory of defaultServiceFactories) {
- if (!factories.some(f => f.service.id === factory.service.id)) {
- factories.push(factory);
- }
- }
-
const backend = createSpecializedBackend({
...otherOptions,
- services: [...factories, rootHttpRouterFactory, discoveryFactory],
+ defaultServiceFactories: [
+ ...defaultServiceFactories,
+ rootHttpRouterFactory,
+ discoveryFactory,
+ ],
});
backendInstancesToCleanUp.push(backend);
- backend.add(
- createBackendPlugin({
- pluginId: `---test-extension-point-registrar`,
- register(reg) {
- for (const [ref, impl] of extensionPoints) {
- reg.registerExtensionPoint(ref, impl);
- }
-
- reg.registerInit({ deps: {}, async init() {} });
- },
- })(),
- );
+ for (const m of createExtensionPointTestModules(features, extensionPoints)) {
+ backend.add(m);
+ }
for (const feature of features) {
backend.add(feature);
@@ -248,3 +290,11 @@ function registerTestHooks() {
}
registerTestHooks();
+
+function isInternalBackendFeature(
+ feature: BackendFeature,
+): feature is InternalBackendFeature {
+ return (
+ typeof (feature as InternalBackendFeature).getRegistrations === 'function'
+ );
+}
diff --git a/packages/backend/.snyk b/packages/backend/.snyk
new file mode 100644
index 0000000000..ea5abd653d
--- /dev/null
+++ b/packages/backend/.snyk
@@ -0,0 +1,10 @@
+# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
+version: v1.22.1
+# ignores vulnerabilities until expiry date; change duration by modifying expiry date
+ignore:
+ SNYK-JS-ISOLATEDVM-3037320:
+ - '*':
+ reason: We do not pass any V8 cache data, and are therefore unaffected by this vulnerability
+ expires: 2033-07-02T16:55:57.077Z
+ created: 2023-07-02T16:55:57.077Z
+patch: {}
diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md
index d4359d6ece..76ac221f69 100644
--- a/packages/backend/CHANGELOG.md
+++ b/packages/backend/CHANGELOG.md
@@ -1,5 +1,164 @@
# example-backend
+## 0.2.86
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-search-backend-module-elasticsearch@1.3.3
+ - @backstage/plugin-search-backend-module-pg@0.5.9
+ - @backstage/plugin-azure-devops-backend@0.3.27
+ - @backstage/plugin-kubernetes-backend@0.11.3
+ - @backstage/plugin-lighthouse-backend@0.2.4
+ - @backstage/plugin-permission-backend@0.5.23
+ - @backstage/plugin-scaffolder-backend@1.16.0
+ - @backstage/plugin-devtools-backend@0.1.3
+ - @backstage/plugin-techdocs-backend@1.6.5
+ - @backstage/backend-common@0.19.2
+ - @backstage/plugin-catalog-backend@1.12.0
+ - @backstage/plugin-badges-backend@0.2.3
+ - @backstage/plugin-events-backend@0.2.9
+ - @backstage/plugin-search-backend@1.4.0
+ - @backstage/plugin-kafka-backend@0.2.41
+ - @backstage/plugin-proxy-backend@0.3.0
+ - @backstage/plugin-todo-backend@0.2.0
+ - @backstage/plugin-app-backend@0.3.48
+ - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.1
+ - @backstage/plugin-auth-backend@0.18.6
+ - @backstage/plugin-explore-backend@0.0.10
+ - @backstage/plugin-catalog-backend-module-unprocessed@0.2.0
+ - @backstage/plugin-scaffolder-backend-module-rails@0.4.17
+ - @backstage/plugin-entity-feedback-backend@0.1.6
+ - @backstage/plugin-code-coverage-backend@0.2.14
+ - @backstage/plugin-search-backend-node@1.2.4
+ - @backstage/plugin-linguist-backend@0.4.0
+ - @backstage/plugin-playlist-backend@0.3.4
+ - @backstage/plugin-jenkins-backend@0.2.3
+ - @backstage/plugin-nomad-backend@0.1.2
+ - @backstage/plugin-catalog-node@1.4.1
+ - @backstage/plugin-events-node@0.2.9
+ - @backstage/plugin-auth-node@0.2.17
+ - @backstage/integration@1.6.0
+ - @backstage/backend-tasks@0.5.5
+ - example-app@0.2.86
+ - @backstage/plugin-adr-backend@0.3.6
+ - @backstage/plugin-azure-sites-backend@0.1.10
+ - @backstage/plugin-graphql-backend@0.1.38
+ - @backstage/plugin-permission-node@0.7.11
+ - @backstage/plugin-rollbar-backend@0.1.45
+ - @backstage/plugin-tech-insights-backend@0.5.14
+ - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.32
+ - @backstage/plugin-tech-insights-node@0.4.6
+ - @backstage/catalog-client@1.4.3
+ - @backstage/catalog-model@1.4.1
+ - @backstage/config@1.0.8
+ - @backstage/plugin-permission-common@0.7.7
+ - @backstage/plugin-search-common@1.2.5
+
+## 0.2.86-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-auth-backend@0.18.6-next.2
+ - @backstage/plugin-scaffolder-backend@1.15.2-next.2
+ - @backstage/plugin-explore-backend@0.0.10-next.2
+ - @backstage/plugin-catalog-backend@1.12.0-next.2
+ - @backstage/plugin-proxy-backend@0.3.0-next.2
+ - @backstage/backend-tasks@0.5.5-next.2
+ - @backstage/plugin-app-backend@0.3.48-next.2
+ - @backstage/plugin-linguist-backend@0.4.0-next.2
+ - @backstage/plugin-techdocs-backend@1.6.5-next.2
+ - @backstage/backend-common@0.19.2-next.2
+ - example-app@0.2.86-next.2
+ - @backstage/plugin-adr-backend@0.3.6-next.2
+ - @backstage/plugin-azure-devops-backend@0.3.27-next.2
+ - @backstage/plugin-badges-backend@0.2.3-next.2
+ - @backstage/plugin-catalog-backend-module-unprocessed@0.2.0-next.2
+ - @backstage/plugin-catalog-node@1.4.1-next.2
+ - @backstage/plugin-devtools-backend@0.1.3-next.2
+ - @backstage/plugin-entity-feedback-backend@0.1.6-next.2
+ - @backstage/plugin-events-backend@0.2.9-next.2
+ - @backstage/plugin-events-node@0.2.9-next.2
+ - @backstage/plugin-kafka-backend@0.2.41-next.2
+ - @backstage/plugin-kubernetes-backend@0.11.3-next.2
+ - @backstage/plugin-lighthouse-backend@0.2.4-next.2
+ - @backstage/plugin-permission-backend@0.5.23-next.2
+ - @backstage/plugin-permission-node@0.7.11-next.2
+ - @backstage/plugin-search-backend@1.4.0-next.2
+ - @backstage/plugin-search-backend-module-elasticsearch@1.3.3-next.2
+ - @backstage/plugin-search-backend-module-pg@0.5.9-next.2
+ - @backstage/plugin-search-backend-node@1.2.4-next.2
+ - @backstage/plugin-todo-backend@0.2.0-next.2
+ - @backstage/plugin-tech-insights-backend@0.5.14-next.2
+ - @backstage/plugin-tech-insights-node@0.4.6-next.2
+ - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.1-next.2
+ - @backstage/plugin-scaffolder-backend-module-rails@0.4.17-next.2
+ - @backstage/plugin-auth-node@0.2.17-next.2
+ - @backstage/plugin-azure-sites-backend@0.1.10-next.2
+ - @backstage/plugin-code-coverage-backend@0.2.14-next.2
+ - @backstage/plugin-graphql-backend@0.1.38-next.2
+ - @backstage/plugin-jenkins-backend@0.2.3-next.2
+ - @backstage/plugin-nomad-backend@0.1.2-next.2
+ - @backstage/plugin-playlist-backend@0.3.4-next.2
+ - @backstage/plugin-rollbar-backend@0.1.45-next.2
+ - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.32-next.2
+
+## 0.2.86-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-search-backend-module-elasticsearch@1.3.3-next.1
+ - @backstage/plugin-search-backend-module-pg@0.5.9-next.1
+ - @backstage/plugin-azure-devops-backend@0.3.27-next.1
+ - @backstage/plugin-kubernetes-backend@0.11.3-next.1
+ - @backstage/plugin-lighthouse-backend@0.2.4-next.1
+ - @backstage/plugin-permission-backend@0.5.23-next.1
+ - @backstage/plugin-scaffolder-backend@1.15.2-next.1
+ - @backstage/plugin-devtools-backend@0.1.3-next.1
+ - @backstage/plugin-techdocs-backend@1.6.5-next.1
+ - @backstage/backend-common@0.19.2-next.1
+ - @backstage/plugin-catalog-backend@1.12.0-next.1
+ - @backstage/plugin-badges-backend@0.2.3-next.1
+ - @backstage/plugin-events-backend@0.2.9-next.1
+ - @backstage/plugin-search-backend@1.4.0-next.1
+ - @backstage/plugin-kafka-backend@0.2.41-next.1
+ - @backstage/plugin-proxy-backend@0.2.42-next.1
+ - @backstage/plugin-todo-backend@0.2.0-next.1
+ - @backstage/plugin-app-backend@0.3.48-next.1
+ - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.1-next.1
+ - @backstage/plugin-catalog-backend-module-unprocessed@0.2.0-next.1
+ - @backstage/plugin-scaffolder-backend-module-rails@0.4.17-next.1
+ - @backstage/plugin-entity-feedback-backend@0.1.6-next.1
+ - @backstage/plugin-code-coverage-backend@0.2.14-next.1
+ - @backstage/plugin-search-backend-node@1.2.4-next.1
+ - @backstage/plugin-linguist-backend@0.3.2-next.1
+ - @backstage/plugin-playlist-backend@0.3.4-next.1
+ - @backstage/plugin-explore-backend@0.0.10-next.1
+ - @backstage/plugin-jenkins-backend@0.2.3-next.1
+ - @backstage/plugin-nomad-backend@0.1.2-next.1
+ - @backstage/plugin-catalog-node@1.4.1-next.1
+ - @backstage/plugin-events-node@0.2.9-next.1
+ - @backstage/plugin-auth-node@0.2.17-next.1
+ - @backstage/plugin-auth-backend@0.18.6-next.1
+ - @backstage/backend-tasks@0.5.5-next.1
+ - @backstage/plugin-adr-backend@0.3.6-next.1
+ - @backstage/plugin-azure-sites-backend@0.1.10-next.1
+ - @backstage/plugin-graphql-backend@0.1.38-next.1
+ - @backstage/plugin-permission-node@0.7.11-next.1
+ - @backstage/plugin-rollbar-backend@0.1.45-next.1
+ - @backstage/plugin-tech-insights-backend@0.5.14-next.1
+ - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.32-next.1
+ - @backstage/plugin-tech-insights-node@0.4.6-next.1
+ - example-app@0.2.86-next.1
+ - @backstage/integration@1.5.1
+ - @backstage/catalog-client@1.4.3
+ - @backstage/catalog-model@1.4.1
+ - @backstage/config@1.0.8
+ - @backstage/plugin-permission-common@0.7.7
+ - @backstage/plugin-search-common@1.2.5
+
## 0.2.86-next.0
### Patch Changes
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 1c99a1bdf3..9e0896233d 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "example-backend",
- "version": "0.2.86-next.0",
+ "version": "0.2.86",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/packages/cli-node/CHANGELOG.md b/packages/cli-node/CHANGELOG.md
index fc2876fa76..8a7af72d8d 100644
--- a/packages/cli-node/CHANGELOG.md
+++ b/packages/cli-node/CHANGELOG.md
@@ -1,5 +1,25 @@
# @backstage/cli-node
+## 0.1.3
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/cli-common@0.1.12
+ - @backstage/errors@1.2.1
+ - @backstage/types@1.1.0
+
+## 0.1.3-next.0
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/cli-common@0.1.12
+ - @backstage/errors@1.2.1
+ - @backstage/types@1.1.0
+
## 0.1.2
### Patch Changes
diff --git a/packages/cli-node/package.json b/packages/cli-node/package.json
index b5ba1855a8..589139a141 100644
--- a/packages/cli-node/package.json
+++ b/packages/cli-node/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli-node",
"description": "Node.js library for Backstage CLIs",
- "version": "0.1.2",
+ "version": "0.1.3",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md
index 2652a40bde..9294e55efd 100644
--- a/packages/cli/CHANGELOG.md
+++ b/packages/cli/CHANGELOG.md
@@ -1,5 +1,41 @@
# @backstage/cli
+## 0.22.10
+
+### Patch Changes
+
+- 3f67cefb4780: Reload the frontend when app config changes
+- cebbf8a27f3c: Enable to print the config schema not merged with the `--no-merge` flag
+- 5c28ebc79fd6: Updated dependency `esbuild` to `^0.19.0`.
+- 971bdd6a4732: Bumped internal `nodemon` dependency.
+- Updated dependencies
+ - @backstage/config-loader@1.4.0
+ - @backstage/cli-node@0.1.3
+ - @backstage/integration@1.6.0
+ - @backstage/catalog-model@1.4.1
+ - @backstage/cli-common@0.1.12
+ - @backstage/config@1.0.8
+ - @backstage/errors@1.2.1
+ - @backstage/eslint-plugin@0.1.3
+ - @backstage/release-manifests@0.0.9
+ - @backstage/types@1.1.0
+
+## 0.22.10-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/config-loader@1.4.0-next.1
+ - @backstage/cli-node@0.1.3-next.0
+ - @backstage/integration@1.5.1
+ - @backstage/catalog-model@1.4.1
+ - @backstage/cli-common@0.1.12
+ - @backstage/config@1.0.8
+ - @backstage/errors@1.2.1
+ - @backstage/eslint-plugin@0.1.3
+ - @backstage/release-manifests@0.0.9
+ - @backstage/types@1.1.0
+
## 0.22.10-next.0
### Patch Changes
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 8b78b16900..95095fe78c 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
- "version": "0.22.10-next.0",
+ "version": "0.22.10",
"publishConfig": {
"access": "public"
},
@@ -78,7 +78,7 @@
"cross-spawn": "^7.0.3",
"css-loader": "^6.5.1",
"diff": "^5.0.0",
- "esbuild": "^0.18.0",
+ "esbuild": "^0.19.0",
"esbuild-loader": "^2.18.0",
"eslint": "^8.6.0",
"eslint-config-prettier": "^8.3.0",
@@ -167,7 +167,7 @@
"del": "^7.0.0",
"mock-fs": "^5.1.0",
"msw": "^1.0.0",
- "nodemon": "^2.0.2",
+ "nodemon": "^3.0.1",
"ts-node": "^10.0.0",
"type-fest": "^2.19.0"
},
diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md
index 0782876942..909bb71b37 100644
--- a/packages/config-loader/CHANGELOG.md
+++ b/packages/config-loader/CHANGELOG.md
@@ -1,5 +1,89 @@
# @backstage/config-loader
+## 1.4.0
+
+### Minor Changes
+
+- 2f1859585998: Loading invalid TypeScript configuration schemas will now throw an error rather than silently being ignored.
+
+ In particular this includes defining any additional types other than `Config` in the schema file, or use of unsupported types such as `Record` or `Partial`.
+
+- cd514545d1d0: Adds a new `deepVisibility` schema keyword that sets child visibility recursively to the defined value, respecting preexisting values or child `deepVisibility`.
+
+ Example usage:
+
+ ```ts
+ export interface Config {
+ /**
+ * Enforces a default of `secret` instead of `backend` for this object.
+ * @deepVisibility secret
+ */
+ mySecretProperty: {
+ type: 'object';
+ properties: {
+ secretValue: {
+ type: 'string';
+ };
+
+ verySecretProperty: {
+ type: 'string';
+ };
+ };
+ };
+ }
+ ```
+
+ Example of a schema that would not be allowed:
+
+ ```ts
+ export interface Config {
+ /**
+ * Set the top level property to secret, enforcing a default of `secret` instead of `backend` for this object.
+ * @deepVisibility secret
+ */
+ mySecretProperty: {
+ type: 'object';
+ properties: {
+ frontendUrl: {
+ /**
+ * We can NOT override the visibility to reveal a property to the front end.
+ * @visibility frontend
+ */
+ type: 'string';
+ };
+
+ verySecretProperty: {
+ type: 'string';
+ };
+ };
+ };
+ }
+ ```
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/cli-common@0.1.12
+ - @backstage/config@1.0.8
+ - @backstage/errors@1.2.1
+ - @backstage/types@1.1.0
+
+## 1.4.0-next.1
+
+### Minor Changes
+
+- 2f1859585998: Loading invalid TypeScript configuration schemas will now throw an error rather than silently being ignored.
+
+ In particular this includes defining any additional types other than `Config` in the schema file, or use of unsupported types such as `Record` or `Partial`.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/cli-common@0.1.12
+ - @backstage/config@1.0.8
+ - @backstage/errors@1.2.1
+ - @backstage/types@1.1.0
+
## 1.4.0-next.0
### Minor Changes
diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json
index 51a4a5a6cd..7fba2d8acb 100644
--- a/packages/config-loader/package.json
+++ b/packages/config-loader/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/config-loader",
"description": "Config loading functionality used by Backstage backend, and CLI",
- "version": "1.4.0-next.0",
+ "version": "1.4.0",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md
index 1de645fb9e..b09076f773 100644
--- a/packages/core-app-api/CHANGELOG.md
+++ b/packages/core-app-api/CHANGELOG.md
@@ -1,5 +1,16 @@
# @backstage/core-app-api
+## 1.9.1
+
+### Patch Changes
+
+- 9ae4e7e63836: Fixed a bug that could cause `navigate` analytics events to be misattributed to the plugin mounted on the root route (e.g. the `home` plugin at `/`) when the route that was navigated to wasn't associated with a routable extension.
+- Updated dependencies
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/config@1.0.8
+ - @backstage/types@1.1.0
+ - @backstage/version-bridge@1.0.4
+
## 1.9.1-next.0
### Patch Changes
diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json
index 15dca326e0..ff43d81329 100644
--- a/packages/core-app-api/package.json
+++ b/packages/core-app-api/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-app-api",
"description": "Core app API used by Backstage apps",
- "version": "1.9.1-next.0",
+ "version": "1.9.1",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md
index 4ebd92ceb6..af5e2b8d6a 100644
--- a/packages/core-components/CHANGELOG.md
+++ b/packages/core-components/CHANGELOG.md
@@ -1,5 +1,17 @@
# @backstage/core-components
+## 0.13.4
+
+### Patch Changes
+
+- 3d86be999fdf: Prefer simple `theme.spacing` without string interpolation
+- Updated dependencies
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/config@1.0.8
+ - @backstage/errors@1.2.1
+ - @backstage/theme@0.4.1
+ - @backstage/version-bridge@1.0.4
+
## 0.13.4-next.0
### Patch Changes
diff --git a/packages/core-components/package.json b/packages/core-components/package.json
index c9b1584b85..641a4e7270 100644
--- a/packages/core-components/package.json
+++ b/packages/core-components/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-components",
"description": "Core components used by Backstage plugins and apps",
- "version": "0.13.4-next.0",
+ "version": "0.13.4",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md
index 9a65a15f98..9ddd48393b 100644
--- a/packages/create-app/CHANGELOG.md
+++ b/packages/create-app/CHANGELOG.md
@@ -1,5 +1,32 @@
# @backstage/create-app
+## 0.5.4
+
+### Patch Changes
+
+- b441642fbe0d: Bumped create-app version.
+- 572abc7edf55: Bumped create-app version.
+- 74f77f151a96: Bumped create-app version.
+- 5cc0ac5ef3d1: Bump to a newer version of the `concurrently` library
+- 46c9a798e41d: Updated the `app-config.yaml` template to use `proxy.endpoints`.
+- 971bdd6a4732: Bumped internal `nodemon` dependency.
+- Updated dependencies
+ - @backstage/cli-common@0.1.12
+
+## 0.5.4-next.2
+
+### Patch Changes
+
+- Bumped create-app version.
+
+## 0.5.4-next.1
+
+### Patch Changes
+
+- Bumped create-app version.
+- Updated dependencies
+ - @backstage/cli-common@0.1.12
+
## 0.5.4-next.0
### Patch Changes
diff --git a/packages/create-app/package.json b/packages/create-app/package.json
index fb203edf26..4f629d951e 100644
--- a/packages/create-app/package.json
+++ b/packages/create-app/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/create-app",
"description": "A CLI that helps you create your own Backstage app",
- "version": "0.5.4-next.0",
+ "version": "0.5.4",
"publishConfig": {
"access": "public"
},
@@ -49,7 +49,7 @@
"@types/node": "^16.11.26",
"@types/recursive-readdir": "^2.2.0",
"mock-fs": "^5.1.1",
- "nodemon": "^2.0.2",
+ "nodemon": "^3.0.1",
"ts-node": "^10.0.0"
},
"nodemonConfig": {
diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs
index 194549f079..08d2abff59 100644
--- a/packages/create-app/templates/default-app/app-config.yaml.hbs
+++ b/packages/create-app/templates/default-app/app-config.yaml.hbs
@@ -46,9 +46,10 @@ integrations:
proxy:
### Example for how to add a proxy endpoint for the frontend.
### A typical reason to do this is to handle HTTPS and CORS for internal services.
- # '/test':
- # target: 'https://example.com'
- # changeOrigin: true
+ # endpoints:
+ # '/test':
+ # target: 'https://example.com'
+ # changeOrigin: true
# Reference documentation http://backstage.io/docs/features/techdocs/configuration
# Note: After experimenting with basic setup, use CI/CD to generate docs
diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs
index 0682114a35..c53ed6970f 100644
--- a/packages/create-app/templates/default-app/package.json.hbs
+++ b/packages/create-app/templates/default-app/package.json.hbs
@@ -31,7 +31,7 @@
"devDependencies": {
"@backstage/cli": "^{{version '@backstage/cli'}}",
"@spotify/prettier-config": "^12.0.0",
- "concurrently": "^6.0.0",
+ "concurrently": "^8.0.0",
"lerna": "^4.0.0",
"node-gyp": "^9.0.0",
"prettier": "^2.3.2",
diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md
index 45df686876..99fb4f1372 100644
--- a/packages/dev-utils/CHANGELOG.md
+++ b/packages/dev-utils/CHANGELOG.md
@@ -1,5 +1,44 @@
# @backstage/dev-utils
+## 1.0.18
+
+### Patch Changes
+
+- 254ad469f053: Removed deprecated calls to `app.getProvider()` and `app.getRouter()` in `DevAppBuilder`
+- Updated dependencies
+ - @backstage/core-app-api@1.9.1
+ - @backstage/integration-react@1.1.16
+ - @backstage/core-components@0.13.4
+ - @backstage/plugin-catalog-react@1.8.1
+ - @backstage/app-defaults@1.4.2
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/test-utils@1.4.2
+ - @backstage/catalog-model@1.4.1
+ - @backstage/theme@0.4.1
+
+## 1.0.18-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.8.1-next.1
+ - @backstage/integration-react@1.1.16-next.1
+
+## 1.0.18-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/integration-react@1.1.16-next.1
+ - @backstage/app-defaults@1.4.2-next.0
+ - @backstage/catalog-model@1.4.1
+ - @backstage/core-app-api@1.9.1-next.0
+ - @backstage/core-components@0.13.4-next.0
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/test-utils@1.4.2-next.0
+ - @backstage/theme@0.4.1
+ - @backstage/plugin-catalog-react@1.8.1-next.0
+
## 1.0.18-next.0
### Patch Changes
diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json
index d7e12209c3..44b16b4a62 100644
--- a/packages/dev-utils/package.json
+++ b/packages/dev-utils/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/dev-utils",
"description": "Utilities for developing Backstage plugins.",
- "version": "1.0.18-next.0",
+ "version": "1.0.18",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx
index 36e87855ed..5f2b21b8d2 100644
--- a/packages/dev-utils/src/devApp/render.tsx
+++ b/packages/dev-utils/src/devApp/render.tsx
@@ -15,7 +15,7 @@
*/
import { createApp } from '@backstage/app-defaults';
-import { FlatRoutes } from '@backstage/core-app-api';
+import { AppRouter, FlatRoutes } from '@backstage/core-app-api';
import {
AlertDisplay,
OAuthRequestDialog,
@@ -195,35 +195,30 @@ export class DevAppBuilder {
},
});
- const AppProvider = app.getProvider();
- const AppRouter = app.getRouter();
+ const DevApp = (
+ <>
+
+
+ {this.rootChildren}
+
+
+
+
+ {this.sidebarItems}
+
+
+
+
+
+ {this.routes}
+ } />
+
+
+
+ >
+ );
- const DevApp = () => {
- return (
-
-
-
- {this.rootChildren}
-
-
-
-
- {this.sidebarItems}
-
-
-
-
-
- {this.routes}
- } />
-
-
-
-
- );
- };
-
- return DevApp;
+ return app.createRoot(DevApp);
}
/**
diff --git a/packages/e2e-test/CHANGELOG.md b/packages/e2e-test/CHANGELOG.md
index f6f29ff3f2..6bf5527c4b 100644
--- a/packages/e2e-test/CHANGELOG.md
+++ b/packages/e2e-test/CHANGELOG.md
@@ -1,5 +1,30 @@
# e2e-test
+## 0.2.6
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/create-app@0.5.4
+ - @backstage/cli-common@0.1.12
+ - @backstage/errors@1.2.1
+
+## 0.2.6-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/create-app@0.5.4-next.2
+
+## 0.2.6-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/create-app@0.5.4-next.1
+ - @backstage/cli-common@0.1.12
+ - @backstage/errors@1.2.1
+
## 0.2.6-next.0
### Patch Changes
diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json
index 48ef90db6f..48d4643394 100644
--- a/packages/e2e-test/package.json
+++ b/packages/e2e-test/package.json
@@ -1,7 +1,7 @@
{
"name": "e2e-test",
"description": "E2E test for verifying Backstage packages",
- "version": "0.2.6-next.0",
+ "version": "0.2.6",
"private": true,
"backstage": {
"role": "cli"
@@ -35,7 +35,7 @@
"cross-fetch": "^3.1.5",
"fs-extra": "10.1.0",
"handlebars": "^4.7.3",
- "pgtools": "^0.3.0",
+ "pgtools": "^1.0.0",
"puppeteer": "^17.0.0",
"tree-kill": "^1.2.2"
},
@@ -44,7 +44,7 @@
"@types/fs-extra": "^9.0.1",
"@types/node": "^16.11.26",
"@types/puppeteer": "^5.4.4",
- "nodemon": "^2.0.2",
+ "nodemon": "^3.0.1",
"ts-node": "^10.0.0"
},
"nodemonConfig": {
diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md
index 4f275a8750..9d08c09bd9 100644
--- a/packages/integration-react/CHANGELOG.md
+++ b/packages/integration-react/CHANGELOG.md
@@ -1,5 +1,29 @@
# @backstage/integration-react
+## 1.1.16
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/integration@1.6.0
+ - @backstage/core-components@0.13.4
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/config@1.0.8
+ - @backstage/theme@0.4.1
+
+## 1.1.16-next.1
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/integration@1.5.1
+ - @backstage/config@1.0.8
+ - @backstage/core-components@0.13.4-next.0
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/theme@0.4.1
+
## 1.1.16-next.0
### Patch Changes
diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json
index b39d4fcb0c..ec68a20f36 100644
--- a/packages/integration-react/package.json
+++ b/packages/integration-react/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/integration-react",
"description": "Frontend package for managing integrations towards external systems",
- "version": "1.1.16-next.0",
+ "version": "1.1.16",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md
index d1f3f46306..b4b97f579e 100644
--- a/packages/integration/CHANGELOG.md
+++ b/packages/integration/CHANGELOG.md
@@ -1,5 +1,18 @@
# @backstage/integration
+## 1.6.0
+
+### Minor Changes
+
+- 443afcf7f567: Added `buildGerritGitilesArchiveUrl()` to construct a Gitiles URL to download an archive.
+ Gitiles URL that uses an authenticated prefix (`/a/`) can now be parsed by the integration.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/config@1.0.8
+ - @backstage/errors@1.2.1
+
## 1.5.1
### Patch Changes
diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md
index c2501b4ac2..7266950ac4 100644
--- a/packages/integration/api-report.md
+++ b/packages/integration/api-report.md
@@ -172,6 +172,14 @@ export type BitbucketServerIntegrationConfig = {
password?: string;
};
+// @public
+export function buildGerritGitilesArchiveUrl(
+ config: GerritIntegrationConfig,
+ project: string,
+ branch: string,
+ filePath: string,
+): string;
+
// @public
export class DefaultGithubCredentialsProvider
implements GithubCredentialsProvider
diff --git a/packages/integration/package.json b/packages/integration/package.json
index e02e070266..b0da382abd 100644
--- a/packages/integration/package.json
+++ b/packages/integration/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/integration",
"description": "Helpers for managing integrations towards external systems",
- "version": "1.5.1",
+ "version": "1.6.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/packages/integration/src/gerrit/core.test.ts b/packages/integration/src/gerrit/core.test.ts
index 5a829e88f9..c00bae384f 100644
--- a/packages/integration/src/gerrit/core.test.ts
+++ b/packages/integration/src/gerrit/core.test.ts
@@ -20,6 +20,7 @@ import fetch from 'cross-fetch';
import { setupRequestMockHandlers } from '../helpers';
import { GerritIntegrationConfig } from './config';
import {
+ buildGerritGitilesArchiveUrl,
buildGerritGitilesUrl,
getGerritBranchApiUrl,
getGerritCloneRepoUrl,
@@ -33,6 +34,41 @@ describe('gerrit core', () => {
const worker = setupServer();
setupRequestMockHandlers(worker);
+ describe('buildGerritGitilesArchiveUrl', () => {
+ const config: GerritIntegrationConfig = {
+ host: 'gerrit.com',
+ gitilesBaseUrl: 'https://gerrit.com/gitiles',
+ };
+ it('can create an archive url for a branch', () => {
+ expect(buildGerritGitilesArchiveUrl(config, 'repo', 'dev', '')).toEqual(
+ 'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev.tar.gz',
+ );
+
+ expect(buildGerritGitilesArchiveUrl(config, 'repo', 'dev', '/')).toEqual(
+ 'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev.tar.gz',
+ );
+ });
+ it('can create an archive url for a specific directory', () => {
+ expect(
+ buildGerritGitilesArchiveUrl(config, 'repo', 'dev', 'docs'),
+ ).toEqual(
+ 'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz',
+ );
+ });
+ it('can create an authenticated url when auth is enabled', () => {
+ const authConfig = {
+ ...config,
+ username: 'username',
+ password: 'password',
+ };
+ expect(
+ buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'),
+ ).toEqual(
+ 'https://gerrit.com/a/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz',
+ );
+ });
+ });
+
describe('buildGerritGitilesUrl', () => {
it('can create an url from arguments', () => {
const config: GerritIntegrationConfig = {
@@ -86,6 +122,25 @@ describe('gerrit core', () => {
);
expect(rootPath).toEqual('/');
});
+ it('can parse a valid authenticated gitiles url.', () => {
+ const config: GerritIntegrationConfig = {
+ host: 'gerrit.com',
+ gitilesBaseUrl: 'https://gerrit.com/gitiles',
+ };
+ const { branch, filePath, project } = parseGerritGitilesUrl(
+ config,
+ 'https://gerrit.com/a/gitiles/web/project/+/refs/heads/master/README.md',
+ );
+ expect(project).toEqual('web/project');
+ expect(branch).toEqual('master');
+ expect(filePath).toEqual('README.md');
+
+ const { filePath: rootPath } = parseGerritGitilesUrl(
+ config,
+ 'https://gerrit.com/gitiles/web/project/+/refs/heads/master',
+ );
+ expect(rootPath).toEqual('/');
+ });
it('throws on incorrect gitiles urls.', () => {
const config: GerritIntegrationConfig = {
host: 'gerrit.com',
diff --git a/packages/integration/src/gerrit/core.ts b/packages/integration/src/gerrit/core.ts
index 0403d8fc75..107d5a4469 100644
--- a/packages/integration/src/gerrit/core.ts
+++ b/packages/integration/src/gerrit/core.ts
@@ -37,6 +37,7 @@ const GERRIT_BODY_PREFIX = ")]}'";
*
* Gitiles url:
* https://g.com/optional_path/\{project\}/+/refs/heads/\{branch\}/\{filePath\}
+ * https://g.com/a/optional_path/\{project\}/+/refs/heads/\{branch\}/\{filePath\}
*
*
* @param url - An URL pointing to a file stored in git.
@@ -47,7 +48,17 @@ export function parseGerritGitilesUrl(
config: GerritIntegrationConfig,
url: string,
): { branch: string; filePath: string; project: string } {
- const urlPath = url.replace(config.gitilesBaseUrl!, '');
+ const baseUrlParse = new URL(config.gitilesBaseUrl!);
+ const urlParse = new URL(url);
+
+ // Remove the gerrit authentication prefix '/a/' from the url
+ // In case of the gitilesBaseUrl is https://review.gerrit.com/plugins/gitiles
+ // and the url provided is https://review.gerrit.com/a/plugins/gitiles/...
+ // remove the prefix only if the pathname start with '/a/'
+ const urlPath = urlParse.pathname
+ .substring(urlParse.pathname.startsWith('/a/') ? 2 : 0)
+ .replace(baseUrlParse.pathname, '');
+
const parts = urlPath.split('/').filter(p => !!p);
const projectEndIndex = parts.indexOf('+');
@@ -91,6 +102,28 @@ export function buildGerritGitilesUrl(
}/${project}/+/refs/heads/${branch}/${trimStart(filePath, '/')}`;
}
+/**
+ * Build a Gerrit Gitiles archive url that targets a specific branch and path
+ *
+ * @param config - A Gerrit provider config.
+ * @param project - The name of the git project
+ * @param branch - The branch we will target.
+ * @param filePath - The absolute file path.
+ * @public
+ */
+export function buildGerritGitilesArchiveUrl(
+ config: GerritIntegrationConfig,
+ project: string,
+ branch: string,
+ filePath: string,
+): string {
+ const archiveName =
+ filePath === '/' || filePath === '' ? '.tar.gz' : `/${filePath}.tar.gz`;
+ return `${getGitilesAuthenticationUrl(
+ config,
+ )}/${project}/+archive/refs/heads/${branch}${archiveName}`;
+}
+
/**
* Return the authentication prefix.
*
@@ -109,6 +142,26 @@ export function getAuthenticationPrefix(
return config.password ? '/a/' : '/';
}
+/**
+ * Return the authentication gitiles url.
+ *
+ * @remarks
+ *
+ * To authenticate with a password the API url must be prefixed with "/a/".
+ * If no password is set anonymous access (without the prefix) will
+ * be used.
+ *
+ * @param config - A Gerrit provider config.
+ */
+export function getGitilesAuthenticationUrl(
+ config: GerritIntegrationConfig,
+): string {
+ const parsedUrl = new URL(config.gitilesBaseUrl!);
+ return `${parsedUrl.protocol}//${parsedUrl.host}${getAuthenticationPrefix(
+ config,
+ )}${parsedUrl.pathname.substring(1)}`;
+}
+
/**
* Return the url to get branch info from the Gerrit API.
*
diff --git a/packages/integration/src/gerrit/index.ts b/packages/integration/src/gerrit/index.ts
index 127c949c4e..6c6295f7a3 100644
--- a/packages/integration/src/gerrit/index.ts
+++ b/packages/integration/src/gerrit/index.ts
@@ -19,6 +19,7 @@ export {
readGerritIntegrationConfigs,
} from './config';
export {
+ buildGerritGitilesArchiveUrl,
getGerritBranchApiUrl,
getGerritCloneRepoUrl,
getGerritFileContentsApiUrl,
diff --git a/packages/repo-tools/CHANGELOG.md b/packages/repo-tools/CHANGELOG.md
index b8f37f4977..ddebca0746 100644
--- a/packages/repo-tools/CHANGELOG.md
+++ b/packages/repo-tools/CHANGELOG.md
@@ -1,5 +1,27 @@
# @backstage/repo-tools
+## 0.3.3
+
+### Patch Changes
+
+- 75702e85862a: Bumped `@microsoft/api-extractor` dependency to `^7.36.4`, and `@microsoft/api-documenter` to `^7.22.33`.
+- 1f3337ebc707: Introducing a new, experimental command `backstage-repo-tools generate-catalog-info`, which can be used to create standardized `catalog-info.yaml` files for each Backstage package in a Backstage monorepo. It can also be used to automatically fix existing `catalog-info.yaml` files with the correct metadata (including `metadata.name`, `metadata.title`, and `metadata.description` introspected from the package's `package.json`, as well as `spec.owner` introspected from `CODEOWNERS`), e.g. in a post-commit hook.
+- ebeb77586975: Update `schema openapi generate` command to now create a default router that can be imported and used directly.
+- Updated dependencies
+ - @backstage/cli-node@0.1.3
+ - @backstage/catalog-model@1.4.1
+ - @backstage/cli-common@0.1.12
+ - @backstage/errors@1.2.1
+
+## 0.3.3-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/cli-node@0.1.3-next.0
+ - @backstage/cli-common@0.1.12
+ - @backstage/errors@1.2.1
+
## 0.3.3-next.0
### Patch Changes
diff --git a/packages/repo-tools/cli-report.md b/packages/repo-tools/cli-report.md
index a0df5e721c..9c87bd8dff 100644
--- a/packages/repo-tools/cli-report.md
+++ b/packages/repo-tools/cli-report.md
@@ -14,6 +14,7 @@ Options:
Commands:
api-reports [options] [paths...]
type-deps
+ generate-catalog-info [options]
schema [command]
help [command]
```
@@ -36,6 +37,16 @@ Options:
-h, --help
```
+### `backstage-repo-tools generate-catalog-info`
+
+```
+Usage: backstage-repo-tools generate-catalog-info [options]
+
+Options:
+ --dry-run
+ -h, --help
+```
+
### `backstage-repo-tools schema`
```
diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json
index c05cd30c75..82099ea621 100644
--- a/packages/repo-tools/package.json
+++ b/packages/repo-tools/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/repo-tools",
"description": "CLI for Backstage repo tooling ",
- "version": "0.3.3-next.0",
+ "version": "0.3.3",
"publishConfig": {
"access": "public"
},
@@ -32,12 +32,13 @@
"dependencies": {
"@apidevtools/swagger-parser": "^10.1.0",
"@apisyouwonthate/style-guide": "^1.4.0",
+ "@backstage/catalog-model": "workspace:^",
"@backstage/cli-common": "workspace:^",
"@backstage/cli-node": "workspace:^",
"@backstage/errors": "workspace:^",
"@manypkg/get-packages": "^1.1.3",
- "@microsoft/api-documenter": "^7.19.27",
- "@microsoft/api-extractor": "^7.33.7",
+ "@microsoft/api-documenter": "^7.22.33",
+ "@microsoft/api-extractor": "^7.36.4",
"@stoplight/spectral-core": "^1.18.0",
"@stoplight/spectral-formatters": "^1.1.0",
"@stoplight/spectral-functions": "^1.7.2",
@@ -46,6 +47,7 @@
"@stoplight/spectral-runtime": "^1.1.2",
"@stoplight/types": "^13.14.0",
"chalk": "^4.0.0",
+ "codeowners-utils": "^1.0.2",
"commander": "^9.1.0",
"fs-extra": "10.1.0",
"glob": "^8.0.3",
@@ -54,7 +56,8 @@
"lodash": "^4.17.21",
"minimatch": "^5.1.1",
"p-limit": "^3.0.2",
- "ts-node": "^10.0.0"
+ "ts-node": "^10.0.0",
+ "yaml-diff-patch": "^2.0.0"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
diff --git a/packages/repo-tools/src/commands/generate-catalog-info/codeowners.ts b/packages/repo-tools/src/commands/generate-catalog-info/codeowners.ts
new file mode 100644
index 0000000000..f485e872e9
--- /dev/null
+++ b/packages/repo-tools/src/commands/generate-catalog-info/codeowners.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2023 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 {
+ CodeOwnersEntry,
+ CODEOWNERS_PATHS,
+ matchFile as matchCodeowner,
+ parse as parseCodeowners,
+} from 'codeowners-utils';
+import { relative as relativePath, resolve as resolvePath } from 'path';
+import { readFile } from './utils';
+
+export async function loadCodeowners(): Promise {
+ const maybeFiles = await Promise.allSettled(
+ CODEOWNERS_PATHS.map(async path =>
+ readFile(resolvePath('.', path), { encoding: 'utf-8' }),
+ ),
+ );
+ const file = maybeFiles.find(
+ maybeFile => maybeFile.status === 'fulfilled',
+ ) as PromiseFulfilledResult | undefined;
+
+ if (!file) {
+ throw new Error(
+ 'This utility expects a CODEOWNERS file, but no such file was found.',
+ );
+ }
+
+ return parseCodeowners(file.value);
+}
+
+export function getPossibleCodeowners(
+ codeowners: CodeOwnersEntry[],
+ relPath: string,
+): string[] {
+ const codeownerMaybe = matchCodeowner(relPath, codeowners);
+ return codeownerMaybe
+ ? codeownerMaybe.owners.map(
+ owner => (owner.match(/(?:\@[^\/]+\/)?([^\@\/]*)$/) || [])[1],
+ )
+ : [];
+}
+
+export function getOwnerFromCodeowners(
+ codeowners: CodeOwnersEntry[],
+ absPath: string,
+): string {
+ const relPath = relativePath('.', absPath);
+ const possibleOwners = getPossibleCodeowners(codeowners, relPath);
+ const owner = possibleOwners.slice(-1)[0];
+
+ if (!owner) {
+ throw new Error(`${relPath} isn't owned by anyone in CODEOWNERS`);
+ }
+
+ return owner;
+}
diff --git a/packages/repo-tools/src/commands/generate-catalog-info/generate-catalog-info.ts b/packages/repo-tools/src/commands/generate-catalog-info/generate-catalog-info.ts
new file mode 100644
index 0000000000..08f73898c8
--- /dev/null
+++ b/packages/repo-tools/src/commands/generate-catalog-info/generate-catalog-info.ts
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2022 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 YAML from 'js-yaml';
+import pLimit from 'p-limit';
+import { relative as relativePath, resolve as resolvePath } from 'path';
+import { yamlOverwrite } from 'yaml-diff-patch';
+import chalk from 'chalk';
+import { PackageGraph, PackageRole } from '@backstage/cli-node';
+import { Entity } from '@backstage/catalog-model';
+import {
+ getOwnerFromCodeowners,
+ getPossibleCodeowners,
+ loadCodeowners,
+} from './codeowners';
+import {
+ BackstagePackageJson,
+ isBackstagePackage,
+ readFile,
+ writeFile,
+} from './utils';
+import { CodeOwnersEntry } from 'codeowners-utils';
+
+type CreateFixPackageInfoYamlsOptions = {
+ dryRun?: boolean;
+};
+
+export default async (opts: CreateFixPackageInfoYamlsOptions) => {
+ const { dryRun = false } = opts;
+ const packages = await PackageGraph.listTargetPackages();
+ const codeowners = await loadCodeowners();
+ const limit = pLimit(10);
+
+ const results = await Promise.allSettled(
+ packages.map(({ packageJson, dir }) =>
+ limit(async () => {
+ if (!isBackstagePackage(packageJson)) {
+ return;
+ }
+
+ // Check if there is already a corresponding catalog-info.yaml
+ const infoYamlPath = resolvePath(dir, 'catalog-info.yaml');
+ let yamlString = '';
+
+ try {
+ yamlString = await readFile(infoYamlPath, { encoding: 'utf-8' });
+ } catch (e) {
+ if (e.code === 'ENOENT') {
+ await createCatalogInfoYaml({
+ yamlPath: infoYamlPath,
+ packageJson,
+ codeowners,
+ dryRun,
+ });
+ return;
+ }
+
+ throw e;
+ }
+
+ await fixCatalogInfoYaml({
+ yamlPath: infoYamlPath,
+ packageJson,
+ codeowners,
+ yamlString,
+ dryRun,
+ });
+ }),
+ ),
+ );
+
+ const rejects = results.filter(
+ r => r.status === 'rejected',
+ ) as PromiseRejectedResult[];
+ if (rejects.length > 0) {
+ // Problems encountered. Print details here.
+ console.error(
+ chalk.red('Unable to create or fix catalog-info.yaml files\n'),
+ );
+ rejects.forEach(reject => console.error(` ${reject.reason}`));
+ console.error();
+ process.exit(1);
+ }
+};
+
+type CreateOptions = {
+ yamlPath: string;
+ packageJson: BackstagePackageJson;
+ codeowners: CodeOwnersEntry[];
+ dryRun: boolean;
+};
+
+type FixOptions = CreateOptions & {
+ yamlString: string;
+};
+
+type BackstagePackageEntity = Entity & {
+ spec: {
+ type: 'backstage-package';
+ backstageRole: PackageRole;
+ lifecycle: string;
+ owner: string;
+ [key: string]: any;
+ };
+};
+
+function createCatalogInfoYaml(options: CreateOptions) {
+ const { codeowners, dryRun, packageJson, yamlPath } = options;
+ const owner = getOwnerFromCodeowners(codeowners, yamlPath);
+ const entity = createOrMergeEntity(packageJson, owner);
+
+ return dryRun
+ ? Promise.resolve(console.error(`Create ${relativePath('.', yamlPath)}`))
+ : writeFile(yamlPath, YAML.dump(entity));
+}
+
+function fixCatalogInfoYaml(options: FixOptions) {
+ const { codeowners, dryRun, packageJson, yamlPath, yamlString } = options;
+ const possibleOwners = getPossibleCodeowners(
+ codeowners,
+ relativePath('.', yamlPath),
+ );
+ const safeName = packageJson.name
+ .replace(/[^a-z0-9_\-\.]+/g, '-')
+ .replace(/^[^a-z0-9]|[^a-z0-9]$/g, '');
+ let yamlJson: BackstagePackageEntity;
+
+ try {
+ yamlJson = YAML.load(yamlString) as BackstagePackageEntity;
+ } catch (e) {
+ throw new Error(`Unable to parse ${relativePath('.', yamlPath)}: ${e}`);
+ }
+
+ const badOwner = !possibleOwners.includes(yamlJson.spec?.owner);
+ const badTitle = yamlJson.metadata.title !== packageJson.name;
+ const badName = yamlJson.metadata.name !== safeName;
+ const badType =
+ yamlJson.spec?.type !== `backstage-${packageJson.backstage.role}`;
+ const badDesc = yamlJson.metadata.description !== packageJson.description;
+
+ if (badOwner || badTitle || badName || badType || badDesc) {
+ const owner = badOwner
+ ? getOwnerFromCodeowners(codeowners, yamlPath)
+ : yamlJson.spec?.owner;
+ const newJson = createOrMergeEntity(packageJson, owner, yamlJson);
+ return dryRun
+ ? Promise.resolve(console.error(`Update ${relativePath('.', yamlPath)}`))
+ : writeFile(yamlPath, yamlOverwrite(yamlString, newJson));
+ }
+
+ return Promise.resolve();
+}
+
+/**
+ * Canonical representation on how to create (or update) a backstage-package
+ * component.
+ */
+function createOrMergeEntity(
+ packageJson: BackstagePackageJson,
+ owner: string,
+ existingEntity: BackstagePackageEntity | Record = {},
+): BackstagePackageEntity {
+ const safeEntityName = packageJson.name
+ .replace(/[^a-z0-9_\-\.]+/g, '-')
+ .replace(/^[^a-z0-9]|[^a-z0-9]$/g, '');
+
+ return {
+ ...existingEntity,
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Component',
+ metadata: {
+ ...existingEntity.metadata,
+ // Provide default name/title/description values.
+ name: safeEntityName,
+ title: packageJson.name,
+ ...(packageJson.description
+ ? { description: packageJson.description }
+ : undefined),
+ },
+ spec: {
+ lifecycle: 'experimental',
+ ...existingEntity.spec,
+ type: `backstage-${packageJson.backstage.role}`,
+ owner,
+ },
+ };
+}
diff --git a/packages/repo-tools/src/commands/generate-catalog-info/utils.ts b/packages/repo-tools/src/commands/generate-catalog-info/utils.ts
new file mode 100644
index 0000000000..f446f4e867
--- /dev/null
+++ b/packages/repo-tools/src/commands/generate-catalog-info/utils.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2023 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 fs from 'fs';
+import { Package } from '@manypkg/get-packages';
+import {
+ BackstagePackageJson as BackstagePackageJsonActual,
+ PackageRole,
+} from '@backstage/cli-node';
+import { promisify } from 'util';
+
+export const readFile = promisify(fs.readFile);
+export const writeFile = promisify(fs.writeFile);
+
+export type BackstagePackageJson = BackstagePackageJsonActual & {
+ description?: string;
+ backstage: {
+ role: PackageRole;
+ };
+};
+
+export function isBackstagePackage(
+ packageJson: Package['packageJson'],
+): packageJson is BackstagePackageJson {
+ return (
+ packageJson &&
+ packageJson.hasOwnProperty('backstage') &&
+ (packageJson as any)?.backstage?.role !== 'undefined'
+ );
+}
diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts
index e19ffde4b8..7aa574d291 100644
--- a/packages/repo-tools/src/commands/index.ts
+++ b/packages/repo-tools/src/commands/index.ts
@@ -96,6 +96,21 @@ export function registerCommands(program: Command) {
.description('Find inconsistencies in types of all packages and plugins')
.action(lazy(() => import('./type-deps/type-deps').then(m => m.default)));
+ program
+ .command('generate-catalog-info')
+ .option(
+ '--dry-run',
+ 'Shows what would happen without actually writing any yaml.',
+ )
+ .description('Create or fix info yaml files for all backstage packages')
+ .action(
+ lazy(() =>
+ import('./generate-catalog-info/generate-catalog-info').then(
+ m => m.default,
+ ),
+ ),
+ );
+
registerSchemaCommand(program);
}
diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md
index 26f2246d13..6728d9de41 100644
--- a/packages/techdocs-cli-embedded-app/CHANGELOG.md
+++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md
@@ -1,5 +1,53 @@
# techdocs-cli-embedded-app
+## 0.2.85
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-app-api@1.9.1
+ - @backstage/integration-react@1.1.16
+ - @backstage/cli@0.22.10
+ - @backstage/core-components@0.13.4
+ - @backstage/plugin-catalog@1.12.1
+ - @backstage/app-defaults@1.4.2
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/test-utils@1.4.2
+ - @backstage/plugin-techdocs@1.6.6
+ - @backstage/catalog-model@1.4.1
+ - @backstage/config@1.0.8
+ - @backstage/theme@0.4.1
+ - @backstage/plugin-techdocs-react@1.1.9
+
+## 0.2.85-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog@1.12.1-next.2
+ - @backstage/plugin-techdocs@1.6.6-next.2
+ - @backstage/cli@0.22.10-next.1
+ - @backstage/integration-react@1.1.16-next.1
+
+## 0.2.85-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/integration-react@1.1.16-next.1
+ - @backstage/cli@0.22.10-next.1
+ - @backstage/plugin-catalog@1.12.1-next.1
+ - @backstage/plugin-techdocs@1.6.6-next.1
+ - @backstage/app-defaults@1.4.2-next.0
+ - @backstage/catalog-model@1.4.1
+ - @backstage/config@1.0.8
+ - @backstage/core-app-api@1.9.1-next.0
+ - @backstage/core-components@0.13.4-next.0
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/test-utils@1.4.2-next.0
+ - @backstage/theme@0.4.1
+ - @backstage/plugin-techdocs-react@1.1.9-next.0
+
## 0.2.85-next.0
### Patch Changes
diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json
index 1b6af7f9e9..c8097fca3b 100644
--- a/packages/techdocs-cli-embedded-app/package.json
+++ b/packages/techdocs-cli-embedded-app/package.json
@@ -1,6 +1,6 @@
{
"name": "techdocs-cli-embedded-app",
- "version": "0.2.85-next.0",
+ "version": "0.2.85",
"private": true,
"backstage": {
"role": "frontend"
diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md
index daefe75e38..8334aa54b6 100644
--- a/packages/techdocs-cli/CHANGELOG.md
+++ b/packages/techdocs-cli/CHANGELOG.md
@@ -1,5 +1,36 @@
# @techdocs/cli
+## 1.4.5
+
+### Patch Changes
+
+- 971bdd6a4732: Bumped internal `nodemon` dependency.
+- Updated dependencies
+ - @backstage/backend-common@0.19.2
+ - @backstage/plugin-techdocs-node@1.7.4
+ - @backstage/catalog-model@1.4.1
+ - @backstage/cli-common@0.1.12
+ - @backstage/config@1.0.8
+
+## 1.4.5-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.19.2-next.2
+ - @backstage/plugin-techdocs-node@1.7.4-next.2
+
+## 1.4.5-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.19.2-next.1
+ - @backstage/plugin-techdocs-node@1.7.4-next.1
+ - @backstage/catalog-model@1.4.1
+ - @backstage/cli-common@0.1.12
+ - @backstage/config@1.0.8
+
## 1.4.5-next.0
### Patch Changes
diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json
index d59450992b..3dc8eda540 100644
--- a/packages/techdocs-cli/package.json
+++ b/packages/techdocs-cli/package.json
@@ -1,7 +1,7 @@
{
"name": "@techdocs/cli",
"description": "Utility CLI for managing TechDocs sites in Backstage.",
- "version": "1.4.5-next.0",
+ "version": "1.4.5",
"publishConfig": {
"access": "public"
},
@@ -46,7 +46,7 @@
"@types/webpack-env": "^1.15.3",
"cypress": "^10.0.0",
"find-process": "^1.4.5",
- "nodemon": "^2.0.2",
+ "nodemon": "^3.0.1",
"techdocs-cli-embedded-app": "link:../techdocs-cli-embedded-app",
"ts-node": "^10.0.0"
},
diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md
index a3fb8ba382..d3e28b30d2 100644
--- a/packages/test-utils/CHANGELOG.md
+++ b/packages/test-utils/CHANGELOG.md
@@ -1,5 +1,18 @@
# @backstage/test-utils
+## 1.4.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-app-api@1.9.1
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/config@1.0.8
+ - @backstage/theme@0.4.1
+ - @backstage/types@1.1.0
+ - @backstage/plugin-permission-common@0.7.7
+ - @backstage/plugin-permission-react@0.4.14
+
## 1.4.2-next.0
### Patch Changes
diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json
index ef074d13b2..73ef2c5268 100644
--- a/packages/test-utils/package.json
+++ b/packages/test-utils/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/test-utils",
"description": "Utilities to test Backstage plugins and apps.",
- "version": "1.4.2-next.0",
+ "version": "1.4.2",
"publishConfig": {
"access": "public"
},
diff --git a/plugins/adr-backend/CHANGELOG.md b/plugins/adr-backend/CHANGELOG.md
index c63b8bd687..e038706836 100644
--- a/plugins/adr-backend/CHANGELOG.md
+++ b/plugins/adr-backend/CHANGELOG.md
@@ -1,5 +1,43 @@
# @backstage/plugin-adr-backend
+## 0.3.6
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.19.2
+ - @backstage/backend-plugin-api@0.6.0
+ - @backstage/integration@1.6.0
+ - @backstage/catalog-client@1.4.3
+ - @backstage/catalog-model@1.4.1
+ - @backstage/config@1.0.8
+ - @backstage/errors@1.2.1
+ - @backstage/plugin-adr-common@0.2.12
+ - @backstage/plugin-search-common@1.2.5
+
+## 0.3.6-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-plugin-api@0.6.0-next.2
+ - @backstage/backend-common@0.19.2-next.2
+
+## 0.3.6-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.19.2-next.1
+ - @backstage/backend-plugin-api@0.6.0-next.1
+ - @backstage/integration@1.5.1
+ - @backstage/catalog-client@1.4.3
+ - @backstage/catalog-model@1.4.1
+ - @backstage/config@1.0.8
+ - @backstage/errors@1.2.1
+ - @backstage/plugin-adr-common@0.2.11
+ - @backstage/plugin-search-common@1.2.5
+
## 0.3.6-next.0
### Patch Changes
diff --git a/plugins/adr-backend/package.json b/plugins/adr-backend/package.json
index e7f390b790..c15eafd6cf 100644
--- a/plugins/adr-backend/package.json
+++ b/plugins/adr-backend/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-adr-backend",
- "version": "0.3.6-next.0",
+ "version": "0.3.6",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/adr-common/CHANGELOG.md b/plugins/adr-common/CHANGELOG.md
index 9b8d5dbf74..dfe54806fe 100644
--- a/plugins/adr-common/CHANGELOG.md
+++ b/plugins/adr-common/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/plugin-adr-common
+## 0.2.12
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/integration@1.6.0
+ - @backstage/catalog-model@1.4.1
+ - @backstage/plugin-search-common@1.2.5
+
## 0.2.11
### Patch Changes
diff --git a/plugins/adr-common/package.json b/plugins/adr-common/package.json
index 2def65cf55..25e02845ef 100644
--- a/plugins/adr-common/package.json
+++ b/plugins/adr-common/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-adr-common",
"description": "Common functionalities for the adr plugin",
- "version": "0.2.11",
+ "version": "0.2.12",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/adr/CHANGELOG.md b/plugins/adr/CHANGELOG.md
index fbbf4c13de..e25c80d4d2 100644
--- a/plugins/adr/CHANGELOG.md
+++ b/plugins/adr/CHANGELOG.md
@@ -1,5 +1,45 @@
# @backstage/plugin-adr
+## 0.6.4
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/integration-react@1.1.16
+ - @backstage/core-components@0.13.4
+ - @backstage/plugin-catalog-react@1.8.1
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/plugin-search-react@1.6.4
+ - @backstage/catalog-model@1.4.1
+ - @backstage/theme@0.4.1
+ - @backstage/plugin-adr-common@0.2.12
+ - @backstage/plugin-search-common@1.2.5
+
+## 0.6.4-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.8.1-next.1
+ - @backstage/integration-react@1.1.16-next.1
+
+## 0.6.4-next.1
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/integration-react@1.1.16-next.1
+ - @backstage/catalog-model@1.4.1
+ - @backstage/core-components@0.13.4-next.0
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/theme@0.4.1
+ - @backstage/plugin-adr-common@0.2.11
+ - @backstage/plugin-catalog-react@1.8.1-next.0
+ - @backstage/plugin-search-common@1.2.5
+ - @backstage/plugin-search-react@1.6.4-next.0
+
## 0.6.4-next.0
### Patch Changes
diff --git a/plugins/adr/package.json b/plugins/adr/package.json
index b032ccad7b..37bf13619a 100644
--- a/plugins/adr/package.json
+++ b/plugins/adr/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-adr",
- "version": "0.6.4-next.0",
+ "version": "0.6.4",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/airbrake-backend/CHANGELOG.md b/plugins/airbrake-backend/CHANGELOG.md
index c0f695f372..7eea9d8287 100644
--- a/plugins/airbrake-backend/CHANGELOG.md
+++ b/plugins/airbrake-backend/CHANGELOG.md
@@ -1,5 +1,35 @@
# @backstage/plugin-airbrake-backend
+## 0.2.21
+
+### Patch Changes
+
+- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config`
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/backend-common@0.19.2
+ - @backstage/backend-plugin-api@0.6.0
+ - @backstage/config@1.0.8
+
+## 0.2.21-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-plugin-api@0.6.0-next.2
+ - @backstage/backend-common@0.19.2-next.2
+
+## 0.2.21-next.1
+
+### Patch Changes
+
+- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config`
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/backend-common@0.19.2-next.1
+ - @backstage/backend-plugin-api@0.6.0-next.1
+ - @backstage/config@1.0.8
+
## 0.2.21-next.0
### Patch Changes
diff --git a/plugins/airbrake-backend/package.json b/plugins/airbrake-backend/package.json
index 226126531b..530de9a77a 100644
--- a/plugins/airbrake-backend/package.json
+++ b/plugins/airbrake-backend/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-airbrake-backend",
- "version": "0.2.21-next.0",
+ "version": "0.2.21",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/airbrake-backend/src/plugin.ts b/plugins/airbrake-backend/src/plugin.ts
index 79850c1272..90e790c05f 100644
--- a/plugins/airbrake-backend/src/plugin.ts
+++ b/plugins/airbrake-backend/src/plugin.ts
@@ -32,7 +32,7 @@ export const airbrakePlugin = createBackendPlugin({
register(env) {
env.registerInit({
deps: {
- config: coreServices.config,
+ config: coreServices.rootConfig,
logger: coreServices.logger,
httpRouter: coreServices.httpRouter,
},
diff --git a/plugins/airbrake/CHANGELOG.md b/plugins/airbrake/CHANGELOG.md
index e5b52f1932..b4d5afccdc 100644
--- a/plugins/airbrake/CHANGELOG.md
+++ b/plugins/airbrake/CHANGELOG.md
@@ -1,5 +1,41 @@
# @backstage/plugin-airbrake
+## 0.3.21
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/dev-utils@1.0.18
+ - @backstage/core-components@0.13.4
+ - @backstage/plugin-catalog-react@1.8.1
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/test-utils@1.4.2
+ - @backstage/catalog-model@1.4.1
+ - @backstage/theme@0.4.1
+
+## 0.3.21-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.8.1-next.1
+ - @backstage/dev-utils@1.0.18-next.2
+
+## 0.3.21-next.1
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/dev-utils@1.0.18-next.1
+ - @backstage/catalog-model@1.4.1
+ - @backstage/core-components@0.13.4-next.0
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/test-utils@1.4.2-next.0
+ - @backstage/theme@0.4.1
+ - @backstage/plugin-catalog-react@1.8.1-next.0
+
## 0.3.21-next.0
### Patch Changes
diff --git a/plugins/airbrake/package.json b/plugins/airbrake/package.json
index 8a156c775d..5ccd8bd076 100644
--- a/plugins/airbrake/package.json
+++ b/plugins/airbrake/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-airbrake",
- "version": "0.3.21-next.0",
+ "version": "0.3.21",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/allure/CHANGELOG.md b/plugins/allure/CHANGELOG.md
index caa81f04cf..cd7c7973b5 100644
--- a/plugins/allure/CHANGELOG.md
+++ b/plugins/allure/CHANGELOG.md
@@ -1,5 +1,36 @@
# @backstage/plugin-allure
+## 0.1.37
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/core-components@0.13.4
+ - @backstage/plugin-catalog-react@1.8.1
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/catalog-model@1.4.1
+ - @backstage/theme@0.4.1
+
+## 0.1.37-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.8.1-next.1
+
+## 0.1.37-next.1
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/catalog-model@1.4.1
+ - @backstage/core-components@0.13.4-next.0
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/theme@0.4.1
+ - @backstage/plugin-catalog-react@1.8.1-next.0
+
## 0.1.37-next.0
### Patch Changes
diff --git a/plugins/allure/package.json b/plugins/allure/package.json
index 7a65d9f707..881d4d0c9d 100644
--- a/plugins/allure/package.json
+++ b/plugins/allure/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-allure",
"description": "A Backstage plugin that integrates with Allure",
- "version": "0.1.37-next.0",
+ "version": "0.1.37",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/analytics-module-ga/CHANGELOG.md b/plugins/analytics-module-ga/CHANGELOG.md
index d7c09cddb1..b6ceb1c28d 100644
--- a/plugins/analytics-module-ga/CHANGELOG.md
+++ b/plugins/analytics-module-ga/CHANGELOG.md
@@ -1,5 +1,27 @@
# @backstage/plugin-analytics-module-ga
+## 0.1.32
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/core-components@0.13.4
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/config@1.0.8
+ - @backstage/theme@0.4.1
+
+## 0.1.32-next.1
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/config@1.0.8
+ - @backstage/core-components@0.13.4-next.0
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/theme@0.4.1
+
## 0.1.32-next.0
### Patch Changes
diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json
index 8fb8d42a9e..434f231545 100644
--- a/plugins/analytics-module-ga/package.json
+++ b/plugins/analytics-module-ga/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-analytics-module-ga",
- "version": "0.1.32-next.0",
+ "version": "0.1.32",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/analytics-module-ga4/CHANGELOG.md b/plugins/analytics-module-ga4/CHANGELOG.md
index 9715015579..bc08d64809 100644
--- a/plugins/analytics-module-ga4/CHANGELOG.md
+++ b/plugins/analytics-module-ga4/CHANGELOG.md
@@ -1,5 +1,27 @@
# @backstage/plugin-analytics-module-ga4
+## 0.1.3
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/core-components@0.13.4
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/config@1.0.8
+ - @backstage/theme@0.4.1
+
+## 0.1.3-next.1
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/config@1.0.8
+ - @backstage/core-components@0.13.4-next.0
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/theme@0.4.1
+
## 0.1.3-next.0
### Patch Changes
diff --git a/plugins/analytics-module-ga4/package.json b/plugins/analytics-module-ga4/package.json
index 09e7240142..63f03e440b 100644
--- a/plugins/analytics-module-ga4/package.json
+++ b/plugins/analytics-module-ga4/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-analytics-module-ga4",
- "version": "0.1.3-next.0",
+ "version": "0.1.3",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/analytics-module-newrelic-browser/CHANGELOG.md b/plugins/analytics-module-newrelic-browser/CHANGELOG.md
index beea288700..3090cd3fc2 100644
--- a/plugins/analytics-module-newrelic-browser/CHANGELOG.md
+++ b/plugins/analytics-module-newrelic-browser/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-analytics-module-newrelic-browser
+## 0.0.1
+
+### Patch Changes
+
+- ec7357258853: Introduced the New Relic Browser analytics module. Check out the plugins [README.md](https://github.com/backstage/backstage/tree/master/plugins/analytics-module-newrelic-browser) for more details!
+- Updated dependencies
+ - @backstage/core-components@0.13.4
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/config@1.0.8
+
## 0.0.1-next.0
### Patch Changes
diff --git a/plugins/analytics-module-newrelic-browser/package.json b/plugins/analytics-module-newrelic-browser/package.json
index 677d3b8e16..ba72a1628b 100644
--- a/plugins/analytics-module-newrelic-browser/package.json
+++ b/plugins/analytics-module-newrelic-browser/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-analytics-module-newrelic-browser",
- "version": "0.0.1-next.0",
+ "version": "0.0.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/apache-airflow/CHANGELOG.md b/plugins/apache-airflow/CHANGELOG.md
index ef1d34dc23..e23c4a7c0c 100644
--- a/plugins/apache-airflow/CHANGELOG.md
+++ b/plugins/apache-airflow/CHANGELOG.md
@@ -1,5 +1,23 @@
# @backstage/plugin-apache-airflow
+## 0.2.14
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/core-components@0.13.4
+ - @backstage/core-plugin-api@1.5.3
+
+## 0.2.14-next.1
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/core-components@0.13.4-next.0
+ - @backstage/core-plugin-api@1.5.3
+
## 0.2.14-next.0
### Patch Changes
diff --git a/plugins/apache-airflow/package.json b/plugins/apache-airflow/package.json
index 60afaa9a24..281d49c781 100644
--- a/plugins/apache-airflow/package.json
+++ b/plugins/apache-airflow/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-apache-airflow",
- "version": "0.2.14-next.0",
+ "version": "0.2.14",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md
index 68923046f5..f84d57542b 100644
--- a/plugins/api-docs/CHANGELOG.md
+++ b/plugins/api-docs/CHANGELOG.md
@@ -1,5 +1,37 @@
# @backstage/plugin-api-docs
+## 0.9.7
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.13.4
+ - @backstage/plugin-catalog@1.12.1
+ - @backstage/plugin-catalog-react@1.8.1
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/catalog-model@1.4.1
+ - @backstage/theme@0.4.1
+
+## 0.9.7-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.8.1-next.1
+ - @backstage/plugin-catalog@1.12.1-next.2
+
+## 0.9.7-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog@1.12.1-next.1
+ - @backstage/catalog-model@1.4.1
+ - @backstage/core-components@0.13.4-next.0
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/theme@0.4.1
+ - @backstage/plugin-catalog-react@1.8.1-next.0
+
## 0.9.7-next.0
### Patch Changes
diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json
index 625fbd3138..1fe4133515 100644
--- a/plugins/api-docs/package.json
+++ b/plugins/api-docs/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-api-docs",
"description": "A Backstage plugin that helps represent API entities in the frontend",
- "version": "0.9.7-next.0",
+ "version": "0.9.7",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/apollo-explorer/CHANGELOG.md b/plugins/apollo-explorer/CHANGELOG.md
index 0051c8a9e7..0833267e83 100644
--- a/plugins/apollo-explorer/CHANGELOG.md
+++ b/plugins/apollo-explorer/CHANGELOG.md
@@ -1,5 +1,25 @@
# @backstage/plugin-apollo-explorer
+## 0.1.14
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/core-components@0.13.4
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/theme@0.4.1
+
+## 0.1.14-next.1
+
+### Patch Changes
+
+- 12a8c94eda8d: Add package repository and homepage metadata
+- Updated dependencies
+ - @backstage/core-components@0.13.4-next.0
+ - @backstage/core-plugin-api@1.5.3
+ - @backstage/theme@0.4.1
+
## 0.1.14-next.0
### Patch Changes
diff --git a/plugins/apollo-explorer/package.json b/plugins/apollo-explorer/package.json
index b9eba3278d..aae09fe19a 100644
--- a/plugins/apollo-explorer/package.json
+++ b/plugins/apollo-explorer/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-apollo-explorer",
- "version": "0.1.14-next.0",
+ "version": "0.1.14",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md
index 7e048ba4d5..2ac0068ccc 100644
--- a/plugins/app-backend/CHANGELOG.md
+++ b/plugins/app-backend/CHANGELOG.md
@@ -1,5 +1,42 @@
# @backstage/plugin-app-backend
+## 0.3.48
+
+### Patch Changes
+
+- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config`
+- d564ad142b17: Migrated the alpha `appBackend` export to use static configuration and extension points rather than accepting options.
+- Updated dependencies
+ - @backstage/backend-common@0.19.2
+ - @backstage/config-loader@1.4.0
+ - @backstage/backend-plugin-api@0.6.0
+ - @backstage/plugin-app-node@0.1.0
+ - @backstage/config@1.0.8
+ - @backstage/types@1.1.0
+
+## 0.3.48-next.2
+
+### Patch Changes
+
+- d564ad142b17: Migrated the alpha `appBackend` export to use static configuration and extension points rather than accepting options.
+- Updated dependencies
+ - @backstage/plugin-app-node@0.1.0-next.0
+ - @backstage/backend-plugin-api@0.6.0-next.2
+ - @backstage/backend-common@0.19.2-next.2
+ - @backstage/config-loader@1.4.0-next.1
+
+## 0.3.48-next.1
+
+### Patch Changes
+
+- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config`
+- Updated dependencies
+ - @backstage/backend-common@0.19.2-next.1
+ - @backstage/config-loader@1.4.0-next.1
+ - @backstage/backend-plugin-api@0.6.0-next.1
+ - @backstage/config@1.0.8
+ - @backstage/types@1.1.0
+
## 0.3.48-next.0
### Patch Changes
diff --git a/plugins/app-backend/alpha-api-report.md b/plugins/app-backend/alpha-api-report.md
index 20a2902557..5fb0546a79 100644
--- a/plugins/app-backend/alpha-api-report.md
+++ b/plugins/app-backend/alpha-api-report.md
@@ -4,18 +4,9 @@
```ts
import { BackendFeature } from '@backstage/backend-plugin-api';
-import express from 'express';
// @alpha
-export const appPlugin: (options: AppPluginOptions) => BackendFeature;
-
-// @alpha (undocumented)
-export type AppPluginOptions = {
- appPackageName?: string;
- staticFallbackHandler?: express.Handler;
- disableConfigInjection?: boolean;
- disableStaticFallbackCache?: boolean;
-};
+export const appPlugin: () => BackendFeature;
// (No @packageDocumentation comment for this package)
```
diff --git a/plugins/app-backend/config.d.ts b/plugins/app-backend/config.d.ts
new file mode 100644
index 0000000000..dc11bc4e0a
--- /dev/null
+++ b/plugins/app-backend/config.d.ts
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2023 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.
+ */
+
+export interface Config {
+ app?: {
+ /**
+ * The name of the app package (in most Backstage repositories, this is the
+ * "name" field in `packages/app/package.json`) that content should be served
+ * from. The same app package should be added as a dependency to the backend
+ * package in order for it to be accessible at runtime.
+ *
+ * In a typical setup with a single app package, this will default to 'app'.
+ */
+ packageName?: string;
+
+ /**
+ * Disables the configuration injection. This can be useful if you're running in an environment
+ * with a read-only filesystem, or for some other reason don't want configuration to be injected.
+ *
+ * Note that this will cause the configuration used when building the app bundle to be used, unless
+ * a separate configuration loading strategy is set up.
+ *
+ * This also disables configuration injection though `APP_CONFIG_` environment variables.
+ */
+ disableConfigInjection?: boolean;
+
+ /**
+ * By default the app backend plugin will cache previously deployed static assets in the database.
+ * If you disable this, it is recommended to set a `staticFallbackHandler` instead.
+ */
+ disableStaticFallbackCache?: boolean;
+ };
+}
diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json
index 5574c8aba3..05db6bb37d 100644
--- a/plugins/app-backend/package.json
+++ b/plugins/app-backend/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-app-backend",
"description": "A Backstage backend plugin that serves the Backstage frontend app",
- "version": "0.3.48-next.0",
+ "version": "0.3.48",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -49,6 +49,7 @@
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/config-loader": "workspace:^",
+ "@backstage/plugin-app-node": "workspace:^",
"@backstage/types": "workspace:^",
"@types/express": "^4.17.6",
"express": "^4.17.1",
@@ -73,8 +74,10 @@
"node-fetch": "^2.6.7",
"supertest": "^6.1.3"
},
+ "configSchema": "config.d.ts",
"files": [
"dist",
+ "config.d.ts",
"migrations/**/*.{js,d.ts}",
"static"
]
diff --git a/plugins/app-backend/src/alpha.ts b/plugins/app-backend/src/alpha.ts
index cc8276b853..edb5c8caf5 100644
--- a/plugins/app-backend/src/alpha.ts
+++ b/plugins/app-backend/src/alpha.ts
@@ -15,4 +15,3 @@
*/
export { appPlugin } from './service/appPlugin';
-export type { AppPluginOptions } from './service/appPlugin';
diff --git a/plugins/app-backend/src/service/appPlugin.test.ts b/plugins/app-backend/src/service/appPlugin.test.ts
index 518f56b7f9..b6a972b4c7 100644
--- a/plugins/app-backend/src/service/appPlugin.test.ts
+++ b/plugins/app-backend/src/service/appPlugin.test.ts
@@ -17,7 +17,7 @@
import mockFs from 'mock-fs';
import { resolve as resolvePath } from 'path';
import fetch from 'node-fetch';
-import { startTestBackend } from '@backstage/backend-test-utils';
+import { mockServices, startTestBackend } from '@backstage/backend-test-utils';
import { appPlugin } from './appPlugin';
describe('appPlugin', () => {
@@ -40,9 +40,13 @@ describe('appPlugin', () => {
it('boots', async () => {
const { server } = await startTestBackend({
features: [
- appPlugin({
- appPackageName: 'app',
- disableStaticFallbackCache: true,
+ appPlugin(),
+ mockServices.rootConfig.factory({
+ data: {
+ app: {
+ disableStaticFallbackCache: true,
+ },
+ },
}),
],
});
diff --git a/plugins/app-backend/src/service/appPlugin.ts b/plugins/app-backend/src/service/appPlugin.ts
index a29f8fd5a8..9ba2ea6d6c 100644
--- a/plugins/app-backend/src/service/appPlugin.ts
+++ b/plugins/app-backend/src/service/appPlugin.ts
@@ -21,84 +21,49 @@ import {
} from '@backstage/backend-plugin-api';
import { createRouter } from './router';
import { loggerToWinstonLogger } from '@backstage/backend-common';
-
-/** @alpha */
-export type AppPluginOptions = {
- /**
- * The name of the app package (in most Backstage repositories, this is the
- * "name" field in `packages/app/package.json`) that content should be served
- * from. The same app package should be added as a dependency to the backend
- * package in order for it to be accessible at runtime.
- *
- * In a typical setup with a single app package, this will default to 'app'.
- */
- appPackageName?: string;
-
- /**
- * A request handler to handle requests for static content that are not present in the app bundle.
- *
- * This can be used to avoid issues with clients on older deployment versions trying to access lazy
- * loaded content that is no longer present. Typically the requests would fall back to a long-term
- * object store where all recently deployed versions of the app are present.
- *
- * Another option is to provide a `database` that will take care of storing the static assets instead.
- *
- * If both `database` and `staticFallbackHandler` are provided, the `database` will attempt to serve
- * static assets first, and if they are not found, the `staticFallbackHandler` will be called.
- */
- staticFallbackHandler?: express.Handler;
-
- /**
- * Disables the configuration injection. This can be useful if you're running in an environment
- * with a read-only filesystem, or for some other reason don't want configuration to be injected.
- *
- * Note that this will cause the configuration used when building the app bundle to be used, unless
- * a separate configuration loading strategy is set up.
- *
- * This also disables configuration injection though `APP_CONFIG_` environment variables.
- */
- disableConfigInjection?: boolean;
-
- /**
- * By default the app backend plugin will cache previously deployed static assets in the database.
- * If you disable this, it is recommended to set a `staticFallbackHandler` instead.
- */
- disableStaticFallbackCache?: boolean;
-};
+import { staticFallbackHandlerExtensionPoint } from '@backstage/plugin-app-node';
/**
* The App plugin is responsible for serving the frontend app bundle and static assets.
* @alpha
*/
-export const appPlugin = createBackendPlugin((options: AppPluginOptions) => ({
+export const appPlugin = createBackendPlugin({
pluginId: 'app',
register(env) {
+ let staticFallbackHandler: express.Handler | undefined;
+
+ env.registerExtensionPoint(staticFallbackHandlerExtensionPoint, {
+ setStaticFallbackHandler(handler) {
+ if (staticFallbackHandler) {
+ throw new Error(
+ 'Attempted to install a static fallback handler for the app-backend twice',
+ );
+ }
+ staticFallbackHandler = handler;
+ },
+ });
+
env.registerInit({
deps: {
logger: coreServices.logger,
- config: coreServices.config,
+ config: coreServices.rootConfig,
database: coreServices.database,
httpRouter: coreServices.httpRouter,
},
async init({ logger, config, database, httpRouter }) {
- const {
- appPackageName,
- staticFallbackHandler,
- disableConfigInjection,
- disableStaticFallbackCache,
- } = options;
+ const appPackageName =
+ config.getOptionalString('app.packageName') ?? 'app';
const winstonLogger = loggerToWinstonLogger(logger);
const router = await createRouter({
logger: winstonLogger,
config,
- database: disableStaticFallbackCache ? undefined : database,
- appPackageName: appPackageName ?? 'app',
+ database,
+ appPackageName,
staticFallbackHandler,
- disableConfigInjection,
});
httpRouter.use(router);
},
});
},
-}));
+});
diff --git a/plugins/app-backend/src/service/router.ts b/plugins/app-backend/src/service/router.ts
index 4e94138439..40e291c19d 100644
--- a/plugins/app-backend/src/service/router.ts
+++ b/plugins/app-backend/src/service/router.ts
@@ -92,6 +92,13 @@ export async function createRouter(
): Promise {
const { config, logger, appPackageName, staticFallbackHandler } = options;
+ const disableConfigInjection =
+ options.disableConfigInjection ??
+ config.getOptionalBoolean('app.disableConfigInjection');
+ const disableStaticFallbackCache = config.getOptionalBoolean(
+ 'app.disableStaticFallbackCache',
+ );
+
const appDistDir = resolvePackagePath(appPackageName, 'dist');
const staticDir = resolvePath(appDistDir, 'static');
@@ -107,7 +114,7 @@ export async function createRouter(
logger.info(`Serving static app content from ${appDistDir}`);
- if (!options.disableConfigInjection) {
+ if (!disableConfigInjection) {
const appConfigs = await readConfigs({
config,
appDistDir,
@@ -131,7 +138,7 @@ export async function createRouter(
}),
);
- if (options.database) {
+ if (options.database && !disableStaticFallbackCache) {
const store = await StaticAssetsStore.create({
logger,
database: options.database,
diff --git a/plugins/app-node/.eslintrc.js b/plugins/app-node/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/plugins/app-node/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/app-node/CHANGELOG.md b/plugins/app-node/CHANGELOG.md
new file mode 100644
index 0000000000..1a2fd84689
--- /dev/null
+++ b/plugins/app-node/CHANGELOG.md
@@ -0,0 +1,23 @@
+# @backstage/plugin-app-node
+
+## 0.1.0
+
+### Minor Changes
+
+- 9fbe95ef6503: Added the `app` plugin node library, initially providing an extension point that can be used to configure a static fallback handler.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-plugin-api@0.6.0
+
+## 0.1.0-next.0
+
+### Minor Changes
+
+- 9fbe95ef6503: Added the `app` plugin node library, initially providing an extension point that can be used to configure a static fallback handler.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-plugin-api@0.6.0-next.2
diff --git a/plugins/app-node/README.md b/plugins/app-node/README.md
new file mode 100644
index 0000000000..5c606540c0
--- /dev/null
+++ b/plugins/app-node/README.md
@@ -0,0 +1,5 @@
+# @backstage/plugin-app-node
+
+Welcome to the Node.js library package for the app plugin!
+
+_This plugin was created through the Backstage CLI_
diff --git a/plugins/app-node/api-report.md b/plugins/app-node/api-report.md
new file mode 100644
index 0000000000..504c9d48c1
--- /dev/null
+++ b/plugins/app-node/api-report.md
@@ -0,0 +1,16 @@
+## API Report File for "@backstage/plugin-app-node"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { ExtensionPoint } from '@backstage/backend-plugin-api';
+import { Handler } from 'express';
+
+// @public
+export interface StaticFallbackHandlerExtensionPoint {
+ setStaticFallbackHandler(handler: Handler): void;
+}
+
+// @public
+export const staticFallbackHandlerExtensionPoint: ExtensionPoint;
+```
diff --git a/plugins/app-node/package.json b/plugins/app-node/package.json
new file mode 100644
index 0000000000..842086ecee
--- /dev/null
+++ b/plugins/app-node/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "@backstage/plugin-app-node",
+ "description": "Node.js library for the app plugin",
+ "version": "0.1.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.cjs.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "node-library"
+ },
+ "scripts": {
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "clean": "backstage-cli package clean",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack"
+ },
+ "devDependencies": {
+ "@backstage/cli": "workspace:^"
+ },
+ "files": [
+ "dist"
+ ],
+ "dependencies": {
+ "@backstage/backend-plugin-api": "workspace:^",
+ "@types/express": "^4.17.6",
+ "express": "^4.17.1"
+ }
+}
diff --git a/plugins/app-node/src/extensions.ts b/plugins/app-node/src/extensions.ts
new file mode 100644
index 0000000000..6a75a5b791
--- /dev/null
+++ b/plugins/app-node/src/extensions.ts
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2023 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 { createExtensionPoint } from '@backstage/backend-plugin-api';
+import { Handler } from 'express';
+
+/**
+ * The interface for {@link staticFallbackHandlerExtensionPoint}.
+ *
+ * @public
+ */
+export interface StaticFallbackHandlerExtensionPoint {
+ /**
+ * Sets the static fallback handler. This can only be done once.
+ */
+ setStaticFallbackHandler(handler: Handler): void;
+}
+
+/**
+ * An extension point the exposes the ability to configure a static fallback handler for the app backend.
+ *
+ * The static fallback handler is a request handler to handle requests for static content that is not
+ * present in the app bundle.
+ *
+ * This can be used to avoid issues with clients on older deployment versions trying to access lazy
+ * loaded content that is no longer present. Typically the requests would fall back to a long-term
+ * object store where all recently deployed versions of the app are present.
+ *
+ * Another option is to provide a `database` that will take care of storing the static assets instead.
+ *
+ * If both `database` and `staticFallbackHandler` are provided, the `database` will attempt to serve
+ * static assets first, and if they are not found, the `staticFallbackHandler` will be called.
+ *
+ * @public
+ */
+export const staticFallbackHandlerExtensionPoint =
+ createExtensionPoint({
+ id: 'app.staticFallbackHandler',
+ });
diff --git a/plugins/app-node/src/index.ts b/plugins/app-node/src/index.ts
new file mode 100644
index 0000000000..c5189fa8b9
--- /dev/null
+++ b/plugins/app-node/src/index.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 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.
+ */
+
+/**
+ * Node.js library for the app plugin.
+ *
+ * @packageDocumentation
+ */
+
+export {
+ staticFallbackHandlerExtensionPoint,
+ type StaticFallbackHandlerExtensionPoint,
+} from './extensions';
diff --git a/plugins/app-node/src/setupTests.ts b/plugins/app-node/src/setupTests.ts
new file mode 100644
index 0000000000..4b9026cde5
--- /dev/null
+++ b/plugins/app-node/src/setupTests.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2023 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.
+ */
+export {};
diff --git a/plugins/auth-backend-module-gcp-iap-provider/.eslintrc.js b/plugins/auth-backend-module-gcp-iap-provider/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/plugins/auth-backend-module-gcp-iap-provider/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/auth-backend-module-gcp-iap-provider/README.md b/plugins/auth-backend-module-gcp-iap-provider/README.md
new file mode 100644
index 0000000000..a5f3ce1d4c
--- /dev/null
+++ b/plugins/auth-backend-module-gcp-iap-provider/README.md
@@ -0,0 +1,5 @@
+# Auth Backend Module - GCP IAP Provider
+
+## Links
+
+- [The Backstage homepage](https://backstage.io)
diff --git a/plugins/auth-backend-module-gcp-iap-provider/api-report.md b/plugins/auth-backend-module-gcp-iap-provider/api-report.md
new file mode 100644
index 0000000000..02b77cea36
--- /dev/null
+++ b/plugins/auth-backend-module-gcp-iap-provider/api-report.md
@@ -0,0 +1,46 @@
+## API Report File for "@backstage/plugin-auth-backend-module-gcp-iap-provider"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { BackendFeature } from '@backstage/backend-plugin-api';
+import { JsonPrimitive } from '@backstage/types';
+import { ProxyAuthenticator } from '@backstage/plugin-auth-node';
+import { SignInResolverFactory } from '@backstage/plugin-auth-node';
+
+// @public (undocumented)
+export const authModuleGcpIapProvider: () => BackendFeature;
+
+// @public (undocumented)
+export const gcpIapAuthenticator: ProxyAuthenticator<
+ {
+ jwtHeader: string;
+ tokenValidator: (token: string) => Promise;
+ },
+ {
+ iapToken: GcpIapTokenInfo;
+ }
+>;
+
+// @public
+export type GcpIapResult = {
+ iapToken: GcpIapTokenInfo;
+};
+
+// @public
+export namespace gcpIapSignInResolvers {
+ const emailMatchingUserEntityAnnotation: SignInResolverFactory<
+ GcpIapResult,
+ unknown
+ >;
+}
+
+// @public
+export type GcpIapTokenInfo = {
+ sub: string;
+ email: string;
+ [key: string]: JsonPrimitive;
+};
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/plugins/auth-backend-module-gcp-iap-provider/config.d.ts b/plugins/auth-backend-module-gcp-iap-provider/config.d.ts
new file mode 100644
index 0000000000..945378ada6
--- /dev/null
+++ b/plugins/auth-backend-module-gcp-iap-provider/config.d.ts
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+export interface Config {
+ auth?: {
+ providers?: {
+ /**
+ * Configuration for the Google Cloud Platform Identity-Aware Proxy (IAP) auth provider.
+ */
+ gcpIap?: {
+ [authEnv: string]: {
+ /**
+ * The audience to use when validating incoming JWT tokens.
+ * See https://backstage.io/docs/auth/google/gcp-iap-auth
+ */
+ audience: string;
+
+ /**
+ * The name of the header to read the JWT token from, defaults to `'x-goog-iap-jwt-assertion'`.
+ */
+ jwtHeader?: string;
+ };
+ };
+ };
+ };
+}
diff --git a/plugins/auth-backend-module-gcp-iap-provider/package.json b/plugins/auth-backend-module-gcp-iap-provider/package.json
new file mode 100644
index 0000000000..9c073a6cb8
--- /dev/null
+++ b/plugins/auth-backend-module-gcp-iap-provider/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "@backstage/plugin-auth-backend-module-gcp-iap-provider",
+ "description": "A GCP IAP auth provider module for the Backstage auth backend",
+ "version": "0.0.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.cjs.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "backend-plugin-module"
+ },
+ "homepage": "https://backstage.io",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/backstage/backstage",
+ "directory": "plugins/auth-backend-module-gcp-iap-provider"
+ },
+ "keywords": [
+ "backstage"
+ ],
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack",
+ "clean": "backstage-cli package clean"
+ },
+ "dependencies": {
+ "@backstage/backend-plugin-api": "workspace:^",
+ "@backstage/errors": "workspace:^",
+ "@backstage/plugin-auth-node": "workspace:^",
+ "@backstage/types": "workspace:^",
+ "google-auth-library": "^8.0.0"
+ },
+ "devDependencies": {
+ "@backstage/backend-test-utils": "workspace:^",
+ "@backstage/cli": "workspace:^",
+ "express": "^4.18.2",
+ "msw": "^1.0.0",
+ "supertest": "^6.1.3"
+ },
+ "files": [
+ "dist",
+ "config.d.ts"
+ ],
+ "configSchema": "config.d.ts"
+}
diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/authenticator.test.ts b/plugins/auth-backend-module-gcp-iap-provider/src/authenticator.test.ts
new file mode 100644
index 0000000000..51efbe1a04
--- /dev/null
+++ b/plugins/auth-backend-module-gcp-iap-provider/src/authenticator.test.ts
@@ -0,0 +1,91 @@
+/*
+ * 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 { mockServices } from '@backstage/backend-test-utils';
+import { Request } from 'express';
+import { gcpIapAuthenticator } from './authenticator';
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+jest.mock('./helpers', () => ({
+ createTokenValidator() {
+ return async () => ({ sub: 's', email: 'e' });
+ },
+}));
+
+describe('GcpIapProvider', () => {
+ it('should find default JWT header', async () => {
+ const ctx = await gcpIapAuthenticator.initialize({
+ config: mockServices.rootConfig({ data: { audience: 'my-audience' } }),
+ });
+ await expect(
+ gcpIapAuthenticator.authenticate(
+ {
+ req: {
+ header(name: string) {
+ return name === 'x-goog-iap-jwt-assertion'
+ ? 'my-token'
+ : undefined;
+ },
+ } as Request,
+ },
+ ctx,
+ ),
+ ).resolves.toEqual({ result: { iapToken: { sub: 's', email: 'e' } } });
+ });
+
+ it('should find custom JWT header', async () => {
+ const jwtHeader = 'x-custom-header';
+ const ctx = await gcpIapAuthenticator.initialize({
+ config: mockServices.rootConfig({
+ data: { audience: 'my-audience', jwtHeader },
+ }),
+ });
+ await expect(
+ gcpIapAuthenticator.authenticate(
+ {
+ req: {
+ header(name: string) {
+ return name === jwtHeader ? 'my-token' : undefined;
+ },
+ } as Request,
+ },
+ ctx,
+ ),
+ ).resolves.toEqual({ result: { iapToken: { sub: 's', email: 'e' } } });
+ });
+
+ it('should throw if header is missing', async () => {
+ const ctx = await gcpIapAuthenticator.initialize({
+ config: mockServices.rootConfig({
+ data: { audience: 'my-audience' },
+ }),
+ });
+ await expect(
+ gcpIapAuthenticator.authenticate(
+ {
+ req: {
+ header(_name: string) {
+ return undefined;
+ },
+ } as Request,
+ },
+ ctx,
+ ),
+ ).rejects.toThrow('Missing Google IAP header');
+ });
+});
diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/authenticator.ts b/plugins/auth-backend-module-gcp-iap-provider/src/authenticator.ts
new file mode 100644
index 0000000000..ca104ded86
--- /dev/null
+++ b/plugins/auth-backend-module-gcp-iap-provider/src/authenticator.ts
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2023 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 { AuthenticationError } from '@backstage/errors';
+import { createProxyAuthenticator } from '@backstage/plugin-auth-node';
+import { createTokenValidator } from './helpers';
+import { GcpIapResult } from './types';
+
+/**
+ * The header name used by the IAP.
+ */
+const DEFAULT_IAP_JWT_HEADER = 'x-goog-iap-jwt-assertion';
+
+/** @public */
+export const gcpIapAuthenticator = createProxyAuthenticator({
+ defaultProfileTransform: async (result: GcpIapResult) => {
+ return { profile: { email: result.iapToken.email } };
+ },
+ async initialize({ config }) {
+ const audience = config.getString('audience');
+ const jwtHeader =
+ config.getOptionalString('jwtHeader') ?? DEFAULT_IAP_JWT_HEADER;
+
+ const tokenValidator = createTokenValidator(audience);
+
+ return { jwtHeader, tokenValidator };
+ },
+ async authenticate({ req }, { jwtHeader, tokenValidator }) {
+ const token = req.header(jwtHeader);
+
+ if (!token || typeof token !== 'string') {
+ throw new AuthenticationError('Missing Google IAP header');
+ }
+
+ const iapToken = await tokenValidator(token);
+
+ return { result: { iapToken } };
+ },
+});
diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/helpers.test.ts b/plugins/auth-backend-module-gcp-iap-provider/src/helpers.test.ts
new file mode 100644
index 0000000000..4079f8f7d3
--- /dev/null
+++ b/plugins/auth-backend-module-gcp-iap-provider/src/helpers.test.ts
@@ -0,0 +1,124 @@
+/*
+ * 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 { OAuth2Client } from 'google-auth-library';
+import { createTokenValidator } from './helpers';
+
+const mockJwt = 'a.b.c';
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+describe('helpers', () => {
+ describe('createTokenValidator', () => {
+ it('runs the happy path', async () => {
+ const mockClient = {
+ getIapPublicKeys: async () => ({ pubkeys: '' }),
+ verifySignedJwtWithCertsAsync: async () => ({
+ getPayload: () => ({ sub: 's', email: 'e@mail.com' }),
+ }),
+ };
+ const validator = createTokenValidator(
+ 'a',
+ mockClient as unknown as OAuth2Client,
+ );
+ await expect(validator(mockJwt)).resolves.toMatchObject({
+ sub: 's',
+ email: 'e@mail.com',
+ });
+ });
+
+ it('throws if listing keys fail', async () => {
+ const mockClient = {
+ getIapPublicKeys: async () => {
+ throw new Error('NOPE');
+ },
+ };
+ const validator = createTokenValidator(
+ 'a',
+ mockClient as unknown as OAuth2Client,
+ );
+ await expect(validator(mockJwt)).rejects.toThrow(
+ 'Unable to list Google IAP token verification keys, Error: NOPE',
+ );
+ });
+
+ it('throws if the verifying signature fails', async () => {
+ const mockClient = {
+ getIapPublicKeys: async () => ({ pubkeys: '' }),
+ verifySignedJwtWithCertsAsync: async () => {
+ throw new Error('NOPE');
+ },
+ };
+ const validator = createTokenValidator(
+ 'a',
+ mockClient as unknown as OAuth2Client,
+ );
+ await expect(validator(mockJwt)).rejects.toThrow(
+ 'Google IAP token verification failed, Error: NOPE',
+ );
+ });
+
+ it('rejects empty payload', async () => {
+ const mockClient = {
+ getIapPublicKeys: async () => ({ pubkeys: '' }),
+ verifySignedJwtWithCertsAsync: async () => ({
+ getPayload: () => undefined,
+ }),
+ };
+ const validator = createTokenValidator(
+ 'a',
+ mockClient as unknown as OAuth2Client,
+ );
+ await expect(validator(mockJwt)).rejects.toThrow(
+ 'Google IAP token verification failed, token had no payload',
+ );
+ });
+
+ it('rejects payload without subject', async () => {
+ const mockClient = {
+ getIapPublicKeys: async () => ({ pubkeys: '' }),
+ verifySignedJwtWithCertsAsync: async () => ({
+ getPayload: () => ({ email: 'e@mail.com' }),
+ }),
+ };
+ const validator = createTokenValidator(
+ 'a',
+ mockClient as unknown as OAuth2Client,
+ );
+ await expect(validator(mockJwt)).rejects.toThrow(
+ 'Google IAP token payload is missing subject claim',
+ );
+ });
+
+ it('rejects payload without email', async () => {
+ const mockClient = {
+ getIapPublicKeys: async () => ({ pubkeys: '' }),
+ verifySignedJwtWithCertsAsync: async () => ({
+ getPayload: () => ({ sub: 's' }),
+ }),
+ };
+ const validator = createTokenValidator(
+ 'a',
+ mockClient as unknown as OAuth2Client,
+ );
+ await expect(validator(mockJwt)).rejects.toThrow(
+ 'Google IAP token payload is missing email claim',
+ );
+ });
+ });
+});
diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/helpers.ts b/plugins/auth-backend-module-gcp-iap-provider/src/helpers.ts
new file mode 100644
index 0000000000..54db7117f9
--- /dev/null
+++ b/plugins/auth-backend-module-gcp-iap-provider/src/helpers.ts
@@ -0,0 +1,67 @@
+/*
+ * 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 { AuthenticationError } from '@backstage/errors';
+import { OAuth2Client } from 'google-auth-library';
+import { GcpIapTokenInfo } from './types';
+
+export function createTokenValidator(
+ audience: string,
+ providedClient?: OAuth2Client,
+): (token: string) => Promise {
+ const client = providedClient ?? new OAuth2Client();
+
+ return async function tokenValidator(token) {
+ // TODO(freben): Rate limit the public key reads. It may be sensible to
+ // cache these for some reasonable time rather than asking for the public
+ // keys on every single sign-in. But since the rate of events here is so
+ // slow, I decided to keep it simple for now.
+ const response = await client.getIapPublicKeys().catch(error => {
+ throw new AuthenticationError(
+ `Unable to list Google IAP token verification keys, ${error}`,
+ );
+ });
+ const ticket = await client
+ .verifySignedJwtWithCertsAsync(token, response.pubkeys, audience, [
+ 'https://cloud.google.com/iap',
+ ])
+ .catch(error => {
+ throw new AuthenticationError(
+ `Google IAP token verification failed, ${error}`,
+ );
+ });
+
+ const payload = ticket.getPayload();
+ if (!payload) {
+ throw new AuthenticationError(
+ 'Google IAP token verification failed, token had no payload',
+ );
+ }
+
+ if (!payload.sub) {
+ throw new AuthenticationError(
+ 'Google IAP token payload is missing subject claim',
+ );
+ }
+ if (!payload.email) {
+ throw new AuthenticationError(
+ 'Google IAP token payload is missing email claim',
+ );
+ }
+
+ return payload as unknown as GcpIapTokenInfo;
+ };
+}
diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/index.ts b/plugins/auth-backend-module-gcp-iap-provider/src/index.ts
new file mode 100644
index 0000000000..be6626a815
--- /dev/null
+++ b/plugins/auth-backend-module-gcp-iap-provider/src/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2023 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.
+ */
+
+export { gcpIapAuthenticator } from './authenticator';
+export { authModuleGcpIapProvider } from './module';
+export { gcpIapSignInResolvers } from './resolvers';
+export { type GcpIapResult, type GcpIapTokenInfo } from './types';
diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/module.ts b/plugins/auth-backend-module-gcp-iap-provider/src/module.ts
new file mode 100644
index 0000000000..90771eecb9
--- /dev/null
+++ b/plugins/auth-backend-module-gcp-iap-provider/src/module.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 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 { createBackendModule } from '@backstage/backend-plugin-api';
+import {
+ authProvidersExtensionPoint,
+ commonSignInResolvers,
+ createProxyAuthProviderFactory,
+} from '@backstage/plugin-auth-node';
+import { gcpIapAuthenticator } from './authenticator';
+import { gcpIapSignInResolvers } from './resolvers';
+
+/** @public */
+export const authModuleGcpIapProvider = createBackendModule({
+ pluginId: 'auth',
+ moduleId: 'gcpIapProvider',
+ register(reg) {
+ reg.registerInit({
+ deps: {
+ providers: authProvidersExtensionPoint,
+ },
+ async init({ providers }) {
+ providers.registerProvider({
+ providerId: 'gcpIap',
+ factory: createProxyAuthProviderFactory({
+ authenticator: gcpIapAuthenticator,
+ signInResolverFactories: {
+ ...gcpIapSignInResolvers,
+ ...commonSignInResolvers,
+ },
+ }),
+ });
+ },
+ });
+ },
+});
diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts b/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts
new file mode 100644
index 0000000000..32ab2b6cc7
--- /dev/null
+++ b/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 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 {
+ createSignInResolverFactory,
+ SignInInfo,
+} from '@backstage/plugin-auth-node';
+import { GcpIapResult } from './types';
+
+/**
+ * Available sign-in resolvers for the Google auth provider.
+ *
+ * @public
+ */
+export namespace gcpIapSignInResolvers {
+ /**
+ * Looks up the user by matching their email to the `google.com/email` annotation.
+ */
+ export const emailMatchingUserEntityAnnotation = createSignInResolverFactory({
+ create() {
+ return async (info: SignInInfo, ctx) => {
+ const email = info.result.iapToken.email;
+
+ if (!email) {
+ throw new Error('Google IAP sign-in result is missing email');
+ }
+
+ return ctx.signInWithCatalogUser({
+ annotations: {
+ 'google.com/email': email,
+ },
+ });
+ };
+ },
+ });
+}
diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/types.ts b/plugins/auth-backend-module-gcp-iap-provider/src/types.ts
new file mode 100644
index 0000000000..a967a4d579
--- /dev/null
+++ b/plugins/auth-backend-module-gcp-iap-provider/src/types.ts
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023 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 { JsonPrimitive } from '@backstage/types';
+
+/**
+ * The data extracted from an IAP token.
+ *
+ * @public
+ */
+export type GcpIapTokenInfo = {
+ /**
+ * The unique, stable identifier for the user.
+ */
+ sub: string;
+ /**
+ * User email address.
+ */
+ email: string;
+ /**
+ * Other fields.
+ */
+ [key: string]: JsonPrimitive;
+};
+
+/**
+ * The result of the initial auth challenge. This is the input to the auth
+ * callbacks.
+ *
+ * @public
+ */
+export type GcpIapResult = {
+ /**
+ * The data extracted from the IAP token header.
+ */
+ iapToken: GcpIapTokenInfo;
+};
diff --git a/plugins/auth-backend-module-google-provider/.eslintrc.js b/plugins/auth-backend-module-google-provider/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/plugins/auth-backend-module-google-provider/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/auth-backend-module-google-provider/README.md b/plugins/auth-backend-module-google-provider/README.md
new file mode 100644
index 0000000000..e031482f64
--- /dev/null
+++ b/plugins/auth-backend-module-google-provider/README.md
@@ -0,0 +1,5 @@
+# Auth Backend Module - Google Provider
+
+## Links
+
+- [The Backstage homepage](https://backstage.io)
diff --git a/plugins/auth-backend-module-google-provider/api-report.md b/plugins/auth-backend-module-google-provider/api-report.md
new file mode 100644
index 0000000000..8040c44832
--- /dev/null
+++ b/plugins/auth-backend-module-google-provider/api-report.md
@@ -0,0 +1,31 @@
+## API Report File for "@backstage/plugin-auth-backend-module-google-provider"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { BackendFeature } from '@backstage/backend-plugin-api';
+import { OAuthAuthenticator } from '@backstage/plugin-auth-node';
+import { OAuthAuthenticatorResult } from '@backstage/plugin-auth-node';
+import { PassportOAuthAuthenticatorHelper } from '@backstage/plugin-auth-node';
+import { PassportProfile } from '@backstage/plugin-auth-node';
+import { SignInResolverFactory } from '@backstage/plugin-auth-node';
+
+// @public (undocumented)
+export const authModuleGoogleProvider: () => BackendFeature;
+
+// @public (undocumented)
+export const googleAuthenticator: OAuthAuthenticator<
+ PassportOAuthAuthenticatorHelper,
+ PassportProfile
+>;
+
+// @public
+export namespace googleSignInResolvers {
+ const emailMatchingUserEntityAnnotation: SignInResolverFactory<
+ OAuthAuthenticatorResult,
+ unknown
+ >;
+}
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/plugins/auth-backend-module-google-provider/config.d.ts b/plugins/auth-backend-module-google-provider/config.d.ts
new file mode 100644
index 0000000000..6a1716c5bf
--- /dev/null
+++ b/plugins/auth-backend-module-google-provider/config.d.ts
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+
+export interface Config {
+ /** Configuration options for the auth plugin */
+ auth?: {
+ providers?: {
+ google?: {
+ [authEnv: string]: {
+ clientId: string;
+ /**
+ * @visibility secret
+ */
+ clientSecret: string;
+ callbackUrl?: string;
+ };
+ };
+ };
+ };
+}
diff --git a/plugins/auth-backend-module-google-provider/package.json b/plugins/auth-backend-module-google-provider/package.json
new file mode 100644
index 0000000000..2104af5124
--- /dev/null
+++ b/plugins/auth-backend-module-google-provider/package.json
@@ -0,0 +1,52 @@
+{
+ "name": "@backstage/plugin-auth-backend-module-google-provider",
+ "description": "A Google auth provider module for the Backstage auth backend",
+ "version": "0.0.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.cjs.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "backend-plugin-module"
+ },
+ "homepage": "https://backstage.io",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/backstage/backstage",
+ "directory": "plugins/auth-backend-module-google-provider"
+ },
+ "keywords": [
+ "backstage"
+ ],
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack",
+ "clean": "backstage-cli package clean"
+ },
+ "dependencies": {
+ "@backstage/backend-plugin-api": "workspace:^",
+ "@backstage/plugin-auth-node": "workspace:^",
+ "google-auth-library": "^8.0.0",
+ "passport-google-oauth20": "^2.0.0"
+ },
+ "devDependencies": {
+ "@backstage/backend-test-utils": "workspace:^",
+ "@backstage/cli": "workspace:^",
+ "@types/passport-google-oauth20": "^2.0.3",
+ "msw": "^1.0.0",
+ "supertest": "^6.1.3"
+ },
+ "files": [
+ "dist",
+ "config.d.ts"
+ ],
+ "configSchema": "config.d.ts"
+}
diff --git a/plugins/auth-backend-module-google-provider/src/authenticator.ts b/plugins/auth-backend-module-google-provider/src/authenticator.ts
new file mode 100644
index 0000000000..a428519fee
--- /dev/null
+++ b/plugins/auth-backend-module-google-provider/src/authenticator.ts
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2023 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 {
+ PassportOAuthAuthenticatorHelper,
+ PassportOAuthDoneCallback,
+ PassportProfile,
+ createOAuthAuthenticator,
+} from '@backstage/plugin-auth-node';
+import { OAuth2Client } from 'google-auth-library';
+import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
+
+/** @public */
+export const googleAuthenticator = createOAuthAuthenticator({
+ defaultProfileTransform:
+ PassportOAuthAuthenticatorHelper.defaultProfileTransform,
+ initialize({ callbackUrl, config }) {
+ const clientId = config.getString('clientId');
+ const clientSecret = config.getString('clientSecret');
+
+ return PassportOAuthAuthenticatorHelper.from(
+ new GoogleStrategy(
+ {
+ clientID: clientId,
+ clientSecret: clientSecret,
+ callbackURL: callbackUrl,
+ passReqToCallback: false,
+ },
+ (
+ accessToken: string,
+ refreshToken: string,
+ params: any,
+ fullProfile: PassportProfile,
+ done: PassportOAuthDoneCallback,
+ ) => {
+ done(
+ undefined,
+ {
+ fullProfile,
+ params,
+ accessToken,
+ },
+ {
+ refreshToken,
+ },
+ );
+ },
+ ),
+ );
+ },
+
+ async start(input, helper) {
+ return helper.start(input, {
+ accessType: 'offline',
+ prompt: 'consent',
+ });
+ },
+
+ async authenticate(input, helper) {
+ return helper.authenticate(input);
+ },
+
+ async refresh(input, helper) {
+ return helper.refresh(input);
+ },
+
+ async logout(input) {
+ if (input.refreshToken) {
+ const oauthClient = new OAuth2Client();
+ await oauthClient.revokeToken(input.refreshToken);
+ }
+ },
+});
diff --git a/plugins/auth-backend-module-google-provider/src/index.ts b/plugins/auth-backend-module-google-provider/src/index.ts
new file mode 100644
index 0000000000..9999dd2bc5
--- /dev/null
+++ b/plugins/auth-backend-module-google-provider/src/index.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2023 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.
+ */
+
+export { googleAuthenticator } from './authenticator';
+export { authModuleGoogleProvider } from './module';
+export { googleSignInResolvers } from './resolvers';
diff --git a/plugins/auth-backend-module-google-provider/src/module.ts b/plugins/auth-backend-module-google-provider/src/module.ts
new file mode 100644
index 0000000000..0354f99880
--- /dev/null
+++ b/plugins/auth-backend-module-google-provider/src/module.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 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 { createBackendModule } from '@backstage/backend-plugin-api';
+import {
+ authProvidersExtensionPoint,
+ commonSignInResolvers,
+ createOAuthProviderFactory,
+} from '@backstage/plugin-auth-node';
+import { googleAuthenticator } from './authenticator';
+import { googleSignInResolvers } from './resolvers';
+
+/** @public */
+export const authModuleGoogleProvider = createBackendModule({
+ pluginId: 'auth',
+ moduleId: 'googleProvider',
+ register(reg) {
+ reg.registerInit({
+ deps: {
+ providers: authProvidersExtensionPoint,
+ },
+ async init({ providers }) {
+ providers.registerProvider({
+ providerId: 'google',
+ factory: createOAuthProviderFactory({
+ authenticator: googleAuthenticator,
+ signInResolverFactories: {
+ ...googleSignInResolvers,
+ ...commonSignInResolvers,
+ },
+ }),
+ });
+ },
+ });
+ },
+});
diff --git a/plugins/auth-backend-module-google-provider/src/resolvers.ts b/plugins/auth-backend-module-google-provider/src/resolvers.ts
new file mode 100644
index 0000000000..b19fc4d49f
--- /dev/null
+++ b/plugins/auth-backend-module-google-provider/src/resolvers.ts
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2023 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 {
+ createSignInResolverFactory,
+ OAuthAuthenticatorResult,
+ PassportProfile,
+ SignInInfo,
+} from '@backstage/plugin-auth-node';
+
+/**
+ * Available sign-in resolvers for the Google auth provider.
+ *
+ * @public
+ */
+export namespace googleSignInResolvers {
+ /**
+ * Looks up the user by matching their email to the `google.com/email` annotation.
+ */
+ export const emailMatchingUserEntityAnnotation = createSignInResolverFactory({
+ create() {
+ return async (
+ info: SignInInfo>,
+ ctx,
+ ) => {
+ const { profile } = info;
+
+ if (!profile.email) {
+ throw new Error('Google profile contained no email');
+ }
+
+ return ctx.signInWithCatalogUser({
+ annotations: {
+ 'google.com/email': profile.email,
+ },
+ });
+ };
+ },
+ });
+}
diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md
index 60555ef74f..5504f162e6 100644
--- a/plugins/auth-backend/CHANGELOG.md
+++ b/plugins/auth-backend/CHANGELOG.md
@@ -1,5 +1,45 @@
# @backstage/plugin-auth-backend
+## 0.18.6
+
+### Patch Changes
+
+- 16452cd007ae: Updated `frameHandler` to return `undefined` when using the redirect flow instead of returning `postMessageReponse` which was causing errors
+- 9dad4b0e61bd: Updated config schema to match what was being used in code
+- bb70a9c3886a: Add frontend visibility to provider objects in `auth` config.
+- Updated dependencies
+ - @backstage/backend-common@0.19.2
+ - @backstage/plugin-auth-node@0.2.17
+ - @backstage/catalog-client@1.4.3
+ - @backstage/catalog-model@1.4.1
+ - @backstage/config@1.0.8
+ - @backstage/errors@1.2.1
+ - @backstage/types@1.1.0
+
+## 0.18.6-next.2
+
+### Patch Changes
+
+- 16452cd007ae: Updated `frameHandler` to return `undefined` when using the redirect flow instead of returning `postMessageReponse` which was causing errors
+- bb70a9c3886a: Add frontend visibility to provider objects in `auth` config.
+- Updated dependencies
+ - @backstage/backend-common@0.19.2-next.2
+ - @backstage/plugin-auth-node@0.2.17-next.2
+
+## 0.18.6-next.1
+
+### Patch Changes
+
+- 9dad4b0e61bd: Updated config schema to match what was being used in code
+- Updated dependencies
+ - @backstage/backend-common@0.19.2-next.1
+ - @backstage/plugin-auth-node@0.2.17-next.1
+ - @backstage/catalog-client@1.4.3
+ - @backstage/catalog-model@1.4.1
+ - @backstage/config@1.0.8
+ - @backstage/errors@1.2.1
+ - @backstage/types@1.1.0
+
## 0.18.6-next.0
### Patch Changes
diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md
index 80db2e0b71..5eb038c436 100644
--- a/plugins/auth-backend/api-report.md
+++ b/plugins/auth-backend/api-report.md
@@ -5,98 +5,69 @@
```ts
///
-import { BackstageIdentityResponse } from '@backstage/plugin-auth-node';
+import { AuthProviderConfig as AuthProviderConfig_2 } from '@backstage/plugin-auth-node';
+import { AuthProviderFactory as AuthProviderFactory_2 } from '@backstage/plugin-auth-node';
+import { AuthProviderRouteHandlers as AuthProviderRouteHandlers_2 } from '@backstage/plugin-auth-node';
+import { AuthResolverCatalogUserQuery as AuthResolverCatalogUserQuery_2 } from '@backstage/plugin-auth-node';
+import { AuthResolverContext as AuthResolverContext_2 } from '@backstage/plugin-auth-node';
import { BackstageSignInResult } from '@backstage/plugin-auth-node';
import { CacheService } from '@backstage/backend-plugin-api';
import { CatalogApi } from '@backstage/catalog-client';
+import { ClientAuthResponse } from '@backstage/plugin-auth-node';
import { Config } from '@backstage/config';
+import { CookieConfigurer as CookieConfigurer_2 } from '@backstage/plugin-auth-node';
+import { decodeOAuthState } from '@backstage/plugin-auth-node';
+import { encodeOAuthState } from '@backstage/plugin-auth-node';
import { Entity } from '@backstage/catalog-model';
import express from 'express';
-import { GetEntitiesRequest } from '@backstage/catalog-client';
+import { GcpIapResult as GcpIapResult_2 } from '@backstage/plugin-auth-backend-module-gcp-iap-provider';
+import { GcpIapTokenInfo as GcpIapTokenInfo_2 } from '@backstage/plugin-auth-backend-module-gcp-iap-provider';
import { IncomingHttpHeaders } from 'http';
-import { JsonValue } from '@backstage/types';
-import { Logger } from 'winston';
+import { LoggerService } from '@backstage/backend-plugin-api';
+import { OAuthEnvironmentHandler as OAuthEnvironmentHandler_2 } from '@backstage/plugin-auth-node';
+import { OAuthState as OAuthState_2 } from '@backstage/plugin-auth-node';
import { PluginDatabaseManager } from '@backstage/backend-common';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
+import { prepareBackstageIdentityResponse as prepareBackstageIdentityResponse_2 } from '@backstage/plugin-auth-node';
import { Profile } from 'passport';
+import { ProfileInfo as ProfileInfo_2 } from '@backstage/plugin-auth-node';
+import { SignInInfo as SignInInfo_2 } from '@backstage/plugin-auth-node';
+import { SignInResolver as SignInResolver_2 } from '@backstage/plugin-auth-node';
import { TokenManager } from '@backstage/backend-common';
+import { TokenParams as TokenParams_2 } from '@backstage/plugin-auth-node';
import { TokenSet } from 'openid-client';
import { UserEntity } from '@backstage/catalog-model';
import { UserinfoResponse } from 'openid-client';
+import { WebMessageResponse as WebMessageResponse_2 } from '@backstage/plugin-auth-node';
-// @public
+// @public @deprecated
export type AuthHandler = (
input: TAuthResult,
context: AuthResolverContext,
) => Promise;
-// @public
+// @public @deprecated
export type AuthHandlerResult = {
profile: ProfileInfo;
};
-// @public (undocumented)
-export type AuthProviderConfig = {
- baseUrl: string;
- appUrl: string;
- isOriginAllowed: (origin: string) => boolean;
- cookieConfigurer?: CookieConfigurer;
-};
+// @public @deprecated (undocumented)
+export type AuthProviderConfig = AuthProviderConfig_2;
-// @public (undocumented)
-export type AuthProviderFactory = (options: {
- providerId: string;
- globalConfig: AuthProviderConfig;
- config: Config;
- logger: Logger;
- resolverContext: AuthResolverContext;
-}) => AuthProviderRouteHandlers;
+// @public @deprecated (undocumented)
+export type AuthProviderFactory = AuthProviderFactory_2;
-// @public
-export interface AuthProviderRouteHandlers {
- frameHandler(req: express.Request, res: express.Response): Promise;
- logout?(req: express.Request, res: express.Response): Promise;
- refresh?(req: express.Request, res: express.Response): Promise;
- start(req: express.Request, res: express.Response): Promise;
-}
+// @public @deprecated (undocumented)
+export type AuthProviderRouteHandlers = AuthProviderRouteHandlers_2;
-// @public
-export type AuthResolverCatalogUserQuery =
- | {
- entityRef:
- | string
- | {
- kind?: string;
- namespace?: string;
- name: string;
- };
- }
- | {
- annotations: Record;
- }
- | {
- filter: Exclude;
- };
+// @public @deprecated (undocumented)
+export type AuthResolverCatalogUserQuery = AuthResolverCatalogUserQuery_2;
-// @public
-export type AuthResolverContext = {
- issueToken(params: TokenParams): Promise<{
- token: string;
- }>;
- findCatalogUser(query: AuthResolverCatalogUserQuery): Promise<{
- entity: Entity;
- }>;
- signInWithCatalogUser(
- query: AuthResolverCatalogUserQuery,
- ): Promise;
-};
+// @public @deprecated (undocumented)
+export type AuthResolverContext = AuthResolverContext_2;
-// @public (undocumented)
-export type AuthResponse = {
- providerInfo: ProviderInfo;
- profile: ProfileInfo;
- backstageIdentity?: BackstageIdentityResponse;
-};
+// @public @deprecated (undocumented)
+export type AuthResponse = ClientAuthResponse;
// @public (undocumented)
export type AwsAlbResult = {
@@ -151,7 +122,7 @@ export class CatalogIdentityClient {
findUser(query: { annotations: Record }): Promise;
resolveCatalogMembership(query: {
entityRefs: string[];
- logger?: Logger;
+ logger?: LoggerService;
}): Promise;
}
@@ -191,18 +162,8 @@ export type CloudflareAccessResult = {
token: string;
};
-// @public
-export type CookieConfigurer = (ctx: {
- providerId: string;
- baseUrl: string;
- callbackUrl: string;
- appOrigin: string;
-}) => {
- domain: string;
- path: string;
- secure: boolean;
- sameSite?: 'none' | 'lax' | 'strict';
-};
+// @public @deprecated (undocumented)
+export type CookieConfigurer = CookieConfigurer_2;
// @public
export function createAuthProviderIntegration<
@@ -235,23 +196,17 @@ export type EasyAuthResult = {
accessToken?: string;
};
-// @public (undocumented)
-export const encodeState: (state: OAuthState) => string;
+// @public @deprecated (undocumented)
+export const encodeState: typeof encodeOAuthState;
-// @public (undocumented)
+// @public @deprecated (undocumented)
export const ensuresXRequestedWith: (req: express.Request) => boolean;
-// @public
-export type GcpIapResult = {
- iapToken: GcpIapTokenInfo;
-};
+// @public @deprecated
+export type GcpIapResult = GcpIapResult_2;
-// @public
-export type GcpIapTokenInfo = {
- sub: string;
- email: string;
- [key: string]: JsonValue;
-};
+// @public @deprecated
+export type GcpIapTokenInfo = GcpIapTokenInfo_2;
// @public
export function getDefaultOwnershipEntityRefs(entity: Entity): string[];
@@ -276,7 +231,7 @@ export type OAuth2ProxyResult = {
getHeader(name: string): string | undefined;
};
-// @public (undocumented)
+// @public @deprecated (undocumented)
export class OAuthAdapter implements AuthProviderRouteHandlers {
constructor(handlers: OAuthHandlers, options: OAuthAdapterOptions);
// (undocumented)
@@ -298,7 +253,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
start(req: express.Request, res: express.Response): Promise;
}
-// @public (undocumented)
+// @public @deprecated (undocumented)
export type OAuthAdapterOptions = {
providerId: string;
persistScopes?: boolean;
@@ -309,25 +264,10 @@ export type OAuthAdapterOptions = {
callbackUrl: string;
};
-// @public (undocumented)
-export class OAuthEnvironmentHandler implements AuthProviderRouteHandlers {
- constructor(handlers: Map);
- // (undocumented)
- frameHandler(req: express.Request, res: express.Response): Promise;
- // (undocumented)
- logout(req: express.Request, res: express.Response): Promise;
- // (undocumented)
- static mapConfig(
- config: Config,
- factoryFunc: (envConfig: Config) => AuthProviderRouteHandlers,
- ): OAuthEnvironmentHandler;
- // (undocumented)
- refresh(req: express.Request, res: express.Response): Promise;
- // (undocumented)
- start(req: express.Request, res: express.Response): Promise;
-}
+// @public @deprecated (undocumented)
+export const OAuthEnvironmentHandler: typeof OAuthEnvironmentHandler_2;
-// @public
+// @public @deprecated (undocumented)
export interface OAuthHandlers {
handler(req: express.Request): Promise<{
response: OAuthResponse;
@@ -341,12 +281,12 @@ export interface OAuthHandlers {
start(req: OAuthStartRequest): Promise;
}
-// @public (undocumented)
+// @public @deprecated (undocumented)
export type OAuthLogoutRequest = express.Request<{}> & {
refreshToken: string;
};
-// @public (undocumented)
+// @public @deprecated (undocumented)
export type OAuthProviderInfo = {
accessToken: string;
idToken?: string;
@@ -354,59 +294,53 @@ export type OAuthProviderInfo = {
scope: string;
};
-// @public
+// @public @deprecated
export type OAuthProviderOptions = {
clientId: string;
clientSecret: string;
callbackUrl: string;
};
-// @public (undocumented)
+// @public @deprecated (undocumented)
export type OAuthRefreshRequest = express.Request<{}> & {
scope: string;
refreshToken: string;
};
-// @public
+// @public @deprecated (undocumented)
export type OAuthResponse = {
profile: ProfileInfo;
providerInfo: OAuthProviderInfo;
backstageIdentity?: BackstageSignInResult;
};
-// @public (undocumented)
+// @public @deprecated (undocumented)
export type OAuthResult = {
fullProfile: Profile;
params: {
id_token?: string;
scope: string;
+ token_type?: string;
expires_in: number;
};
accessToken: string;
refreshToken?: string;
};
-// @public (undocumented)
+// @public @deprecated (undocumented)
export type OAuthStartRequest = express.Request<{}> & {
scope: string;
state: OAuthState;
};
-// @public (undocumented)
+// @public @deprecated (undocumented)
export type OAuthStartResponse = {
url: string;
status?: number;
};
-// @public (undocumented)
-export type OAuthState = {
- nonce: string;
- env: string;
- origin?: string;
- scope?: string;
- redirectUrl?: string;
- flow?: string;
-};
+// @public @deprecated (undocumented)
+export type OAuthState = OAuthState_2;
// @public
export type OidcAuthResult = {
@@ -414,24 +348,18 @@ export type OidcAuthResult = {
userinfo: UserinfoResponse;
};
-// @public (undocumented)
+// @public @deprecated (undocumented)
export const postMessageResponse: (
res: express.Response,
appOrigin: string,
response: WebMessageResponse,
) => void;
-// @public
-export function prepareBackstageIdentityResponse(
- result: BackstageSignInResult,
-): BackstageIdentityResponse;
+// @public @deprecated (undocumented)
+export const prepareBackstageIdentityResponse: typeof prepareBackstageIdentityResponse_2;
-// @public
-export type ProfileInfo = {
- email?: string;
- displayName?: string;
- picture?: string;
-};
+// @public @deprecated (undocumented)
+export type ProfileInfo = ProfileInfo_2;
// @public (undocumented)
export type ProviderFactories = {
@@ -452,7 +380,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: never;
}>;
auth0: Readonly<{
@@ -467,7 +395,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: never;
}>;
awsAlb: Readonly<{
@@ -480,7 +408,7 @@ export const providers: Readonly<{
};
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: never;
}>;
bitbucket: Readonly<{
@@ -495,7 +423,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: Readonly<{
usernameMatchingUserEntityAnnotation(): SignInResolver;
userIdMatchingUserEntityAnnotation(): SignInResolver;
@@ -513,7 +441,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: Readonly<{
emailMatchingUserEntityProfileEmail: () => SignInResolver;
}>;
@@ -525,18 +453,18 @@ export const providers: Readonly<{
resolver: SignInResolver;
};
cache?: CacheService | undefined;
- }) => AuthProviderFactory;
+ }) => AuthProviderFactory_2;
resolvers: Readonly<{
emailMatchingUserEntityProfileEmail: () => SignInResolver;
}>;
}>;
gcpIap: Readonly<{
create: (options: {
- authHandler?: AuthHandler | undefined;
+ authHandler?: AuthHandler | undefined;
signIn: {
- resolver: SignInResolver;
+ resolver: SignInResolver;
};
- }) => AuthProviderFactory;
+ }) => AuthProviderFactory_2;
resolvers: never;
}>;
github: Readonly<{
@@ -552,7 +480,7 @@ export const providers: Readonly<{
stateEncoder?: StateEncoder | undefined;
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: Readonly<{
usernameMatchingUserEntityName: () => SignInResolver;
}>;
@@ -569,7 +497,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: never;
}>;
google: Readonly<{
@@ -584,11 +512,11 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: Readonly<{
- emailLocalPartMatchingUserEntityName: () => SignInResolver;
- emailMatchingUserEntityProfileEmail: () => SignInResolver;
- emailMatchingUserEntityAnnotation(): SignInResolver;
+ emailMatchingUserEntityProfileEmail: () => SignInResolver_2;
+ emailLocalPartMatchingUserEntityName: () => SignInResolver_2;
+ emailMatchingUserEntityAnnotation: () => SignInResolver_2;
}>;
}>;
microsoft: Readonly<{
@@ -603,7 +531,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: Readonly<{
emailLocalPartMatchingUserEntityName: () => SignInResolver;
emailMatchingUserEntityProfileEmail: () => SignInResolver;
@@ -622,7 +550,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: never;
}>;
oauth2Proxy: Readonly<{
@@ -631,7 +559,7 @@ export const providers: Readonly<{
signIn: {
resolver: SignInResolver>;
};
- }) => AuthProviderFactory;
+ }) => AuthProviderFactory_2;
resolvers: never;
}>;
oidc: Readonly<{
@@ -646,7 +574,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: Readonly<{
emailLocalPartMatchingUserEntityName: () => SignInResolver;
emailMatchingUserEntityProfileEmail: () => SignInResolver;
@@ -664,7 +592,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: Readonly<{
emailLocalPartMatchingUserEntityName: () => SignInResolver;
emailMatchingUserEntityProfileEmail: () => SignInResolver;
@@ -683,7 +611,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: never;
}>;
saml: Readonly<{
@@ -698,7 +626,7 @@ export const providers: Readonly<{
| undefined;
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: Readonly<{
nameIdMatchingUserEntityName(): SignInResolver;
}>;
@@ -713,13 +641,13 @@ export const providers: Readonly<{
};
}
| undefined,
- ) => AuthProviderFactory;
+ ) => AuthProviderFactory_2;
resolvers: never;
}>;
}>;
-// @public (undocumented)
-export const readState: (stateString: string) => OAuthState;
+// @public @deprecated (undocumented)
+export const readState: typeof decodeOAuthState;
// @public (undocumented)
export interface RouterOptions {
@@ -732,7 +660,7 @@ export interface RouterOptions {
// (undocumented)
discovery: PluginEndpointDiscovery;
// (undocumented)
- logger: Logger;
+ logger: LoggerService;
// (undocumented)
providerFactories?: ProviderFactories;
// (undocumented)
@@ -746,42 +674,23 @@ export type SamlAuthResult = {
fullProfile: any;
};
-// @public
-export type SignInInfo = {
- profile: ProfileInfo;
- result: TAuthResult;
-};
+// @public @deprecated (undocumented)
+export type SignInInfo = SignInInfo_2;
-// @public
-export type SignInResolver = (
- info: SignInInfo,
- context: AuthResolverContext,
-) => Promise;
+// @public @deprecated (undocumented)
+export type SignInResolver = SignInResolver_2;
-// @public (undocumented)
+// @public @deprecated (undocumented)
export type StateEncoder = (req: OAuthStartRequest) => Promise<{
encodedState: string;
}>;
-// @public
-export type TokenParams = {
- claims: {
- sub: string;
- ent?: string[];
- } & Record;
-};
+// @public @deprecated (undocumented)
+export type TokenParams = TokenParams_2;
-// @public (undocumented)
+// @public @deprecated (undocumented)
export const verifyNonce: (req: express.Request, providerId: string) => void;
-// @public
-export type WebMessageResponse =
- | {
- type: 'authorization_response';
- response: AuthResponse;
- }
- | {
- type: 'authorization_response';
- error: Error;
- };
+// @public @deprecated (undocumented)
+export type WebMessageResponse = WebMessageResponse_2;
```
diff --git a/plugins/auth-backend/architecture.drawio.svg b/plugins/auth-backend/architecture.drawio.svg
new file mode 100644
index 0000000000..d709935e3e
--- /dev/null
+++ b/plugins/auth-backend/architecture.drawio.svg
@@ -0,0 +1,262 @@
+
diff --git a/plugins/auth-backend/config.d.ts b/plugins/auth-backend/config.d.ts
index d059f2ec04..4ebf7c9de1 100644
--- a/plugins/auth-backend/config.d.ts
+++ b/plugins/auth-backend/config.d.ts
@@ -62,6 +62,7 @@ export interface Config {
* @additionalProperties true
*/
providers?: {
+ /** @visibility frontend */
google?: {
[authEnv: string]: {
clientId: string;
@@ -72,6 +73,7 @@ export interface Config {
callbackUrl?: string;
};
};
+ /** @visibility frontend */
github?: {
[authEnv: string]: {
clientId: string;
@@ -83,6 +85,7 @@ export interface Config {
enterpriseInstanceUrl?: string;
};
};
+ /** @visibility frontend */
gitlab?: {
[authEnv: string]: {
clientId: string;
@@ -94,6 +97,7 @@ export interface Config {
callbackUrl?: string;
};
};
+ /** @visibility frontend */
saml?: {
entryPoint: string;
logoutUrl?: string;
@@ -117,6 +121,7 @@ export interface Config {
digestAlgorithm?: string;
acceptedClockSkewMs?: number;
};
+ /** @visibility frontend */
okta?: {
[authEnv: string]: {
clientId: string;
@@ -130,6 +135,7 @@ export interface Config {
callbackUrl?: string;
};
};
+ /** @visibility frontend */
oauth2?: {
[authEnv: string]: {
clientId: string;
@@ -143,6 +149,7 @@ export interface Config {
disableRefresh?: boolean;
};
};
+ /** @visibility frontend */
oidc?: {
[authEnv: string]: {
clientId: string;
@@ -158,6 +165,7 @@ export interface Config {
prompt?: string;
};
};
+ /** @visibility frontend */
auth0?: {
[authEnv: string]: {
clientId: string;
@@ -172,6 +180,7 @@ export interface Config {
connectionScope?: string;
};
};
+ /** @visibility frontend */
microsoft?: {
[authEnv: string]: {
clientId: string;
@@ -183,6 +192,7 @@ export interface Config {
callbackUrl?: string;
};
};
+ /** @visibility frontend */
onelogin?: {
[authEnv: string]: {
clientId: string;
@@ -194,10 +204,12 @@ export interface Config {
callbackUrl?: string;
};
};
+ /** @visibility frontend */
awsalb?: {
iss?: string;
region: string;
};
+ /** @visibility frontend */
cfaccess?: {
teamName: string;
};
diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json
index 4e7f80afaf..c8d1633a47 100644
--- a/plugins/auth-backend/package.json
+++ b/plugins/auth-backend/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-auth-backend",
"description": "A Backstage backend plugin that handles authentication",
- "version": "0.18.6-next.0",
+ "version": "0.18.6",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -33,10 +33,13 @@
},
"dependencies": {
"@backstage/backend-common": "workspace:^",
+ "@backstage/backend-plugin-api": "workspace:^",
"@backstage/catalog-client": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
+ "@backstage/plugin-auth-backend-module-gcp-iap-provider": "workspace:^",
+ "@backstage/plugin-auth-backend-module-google-provider": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"@backstage/types": "workspace:^",
"@davidzemon/passport-okta-oauth": "^0.0.5",
diff --git a/plugins/auth-backend/src/identity/FirestoreKeyStore.ts b/plugins/auth-backend/src/identity/FirestoreKeyStore.ts
index a5479a6470..b5f4f5dcfb 100644
--- a/plugins/auth-backend/src/identity/FirestoreKeyStore.ts
+++ b/plugins/auth-backend/src/identity/FirestoreKeyStore.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { Logger } from 'winston';
+import { LoggerService } from '@backstage/backend-plugin-api';
import {
DocumentData,
Firestore,
@@ -57,7 +57,7 @@ export class FirestoreKeyStore implements KeyStore {
static async verifyConnection(
keyStore: FirestoreKeyStore,
- logger?: Logger,
+ logger?: LoggerService,
): Promise {
try {
await keyStore.verify();
diff --git a/plugins/auth-backend/src/identity/KeyStores.ts b/plugins/auth-backend/src/identity/KeyStores.ts
index 4cd8f91842..cc35c0d84e 100644
--- a/plugins/auth-backend/src/identity/KeyStores.ts
+++ b/plugins/auth-backend/src/identity/KeyStores.ts
@@ -15,7 +15,7 @@
*/
import { pickBy } from 'lodash';
-import { Logger } from 'winston';
+import { LoggerService } from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
@@ -26,7 +26,7 @@ import { MemoryKeyStore } from './MemoryKeyStore';
import { KeyStore } from './types';
type Options = {
- logger: Logger;
+ logger: LoggerService;
database: AuthDatabase;
};
diff --git a/plugins/auth-backend/src/identity/TokenFactory.ts b/plugins/auth-backend/src/identity/TokenFactory.ts
index f80b79b6bc..ca16f8821f 100644
--- a/plugins/auth-backend/src/identity/TokenFactory.ts
+++ b/plugins/auth-backend/src/identity/TokenFactory.ts
@@ -18,14 +18,14 @@ import { AuthenticationError } from '@backstage/errors';
import { exportJWK, generateKeyPair, importJWK, JWK, SignJWT } from 'jose';
import { DateTime } from 'luxon';
import { v4 as uuid } from 'uuid';
-import { Logger } from 'winston';
+import { LoggerService } from '@backstage/backend-plugin-api';
import { AnyJWK, KeyStore, TokenIssuer, TokenParams } from './types';
const MS_IN_S = 1000;
type Options = {
- logger: Logger;
+ logger: LoggerService;
/** Value of the issuer claim in issued tokens */
issuer: string;
/** Key store used for storing signing keys */
@@ -57,7 +57,7 @@ type Options = {
*/
export class TokenFactory implements TokenIssuer {
private readonly issuer: string;
- private readonly logger: Logger;
+ private readonly logger: LoggerService;
private readonly keyStore: KeyStore;
private readonly keyDurationSeconds: number;
private readonly algorithm: string;
diff --git a/plugins/auth-backend/src/identity/types.ts b/plugins/auth-backend/src/identity/types.ts
index e325f74c81..fcfc0345cc 100644
--- a/plugins/auth-backend/src/identity/types.ts
+++ b/plugins/auth-backend/src/identity/types.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { JsonValue } from '@backstage/types';
+import { TokenParams as _TokenParams } from '@backstage/plugin-auth-node';
/** Represents any form of serializable JWK */
export interface AnyJWK extends Record {
@@ -25,26 +25,10 @@ export interface AnyJWK extends Record {
}
/**
- * Parameters used to issue new ID Tokens
- *
* @public
+ * @deprecated import from `@backstage/plugin-auth-node` instead
*/
-export type TokenParams = {
- /**
- * The claims that will be embedded within the token. At a minimum, this should include
- * the subject claim, `sub`. It is common to also list entity ownership relations in the
- * `ent` list. Additional claims may also be added at the developer's discretion except
- * for the following list, which will be overwritten by the TokenIssuer: `iss`, `aud`,
- * `iat`, and `exp`. The Backstage team also maintains the right add new claims in the future
- * without listing the change as a "breaking change".
- */
- claims: {
- /** The token subject, i.e. User ID */
- sub: string;
- /** A list of entity references that the user claims ownership through */
- ent?: string[];
- } & Record;
-};
+export type TokenParams = _TokenParams;
/**
* A TokenIssuer is able to issue verifiable ID Tokens on demand.
diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts
index 494e8b4d38..ff983e27a5 100644
--- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts
+++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { Logger } from 'winston';
+import { LoggerService } from '@backstage/backend-plugin-api';
import { ConflictError, NotFoundError } from '@backstage/errors';
import { CatalogApi } from '@backstage/catalog-client';
import {
@@ -78,7 +78,7 @@ export class CatalogIdentityClient {
*/
async resolveCatalogMembership(query: {
entityRefs: string[];
- logger?: Logger;
+ logger?: LoggerService;
}): Promise {
const { entityRefs, logger } = query;
const resolvedEntityRefs = entityRefs
diff --git a/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts b/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts
index a0c437815d..a61d51b27b 100644
--- a/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts
+++ b/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts
@@ -125,7 +125,7 @@ describe('oauth helpers', () => {
postMessageResponse(mockResponse, appOrigin, data);
expect(responseBody.match(/.postMessage\(/g)).toHaveLength(2);
expect(
- responseBody.match(/.postMessage\([a-zA-z.()]*, \'\*\'\)/g),
+ responseBody.match(/.postMessage\([a-zA-Z.()]*, \'\*\'\)/g),
).toHaveLength(1);
const errData: WebMessageResponse = {
@@ -135,7 +135,7 @@ describe('oauth helpers', () => {
postMessageResponse(mockResponse, appOrigin, errData);
expect(responseBody.match(/.postMessage\(/g)).toHaveLength(2);
expect(
- responseBody.match(/.postMessage\([a-zA-z.()]*, \'\*\'\)/g),
+ responseBody.match(/.postMessage\([a-zA-Z.()]*, \'\*\'\)/g),
).toHaveLength(1);
});
diff --git a/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts b/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts
index 94ac4fba15..2f1770a3d4 100644
--- a/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts
+++ b/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts
@@ -24,7 +24,10 @@ export const safelyEncodeURIComponent = (value: string) => {
return encodeURIComponent(value).replace(/'/g, '%27');
};
-/** @public */
+/**
+ * @public
+ * @deprecated Use `sendWebMessageResponse` from `@backstage/plugin-auth-node` instead
+ */
export const postMessageResponse = (
res: express.Response,
appOrigin: string,
@@ -69,7 +72,10 @@ export const postMessageResponse = (
res.end(``);
};
-/** @public */
+/**
+ * @public
+ * @deprecated Use inline logic to check that the `X-Requested-With` header is set to `'XMLHttpRequest'` instead.
+ */
export const ensuresXRequestedWith = (req: express.Request) => {
const requiredHeader = req.header('X-Requested-With');
if (!requiredHeader || requiredHeader !== 'XMLHttpRequest') {
diff --git a/plugins/auth-backend/src/lib/flow/types.ts b/plugins/auth-backend/src/lib/flow/types.ts
index f67198a1b0..36f3033b21 100644
--- a/plugins/auth-backend/src/lib/flow/types.ts
+++ b/plugins/auth-backend/src/lib/flow/types.ts
@@ -14,20 +14,10 @@
* limitations under the License.
*/
-import { AuthResponse } from '../../providers/types';
+import { WebMessageResponse as _WebMessageResponse } from '@backstage/plugin-auth-node';
/**
- * Payload sent as a post message after the auth request is complete.
- * If successful then has a valid payload with Auth information else contains an error.
- *
* @public
+ * @deprecated import from `@backstage/plugin-auth-node` instead
*/
-export type WebMessageResponse =
- | {
- type: 'authorization_response';
- response: AuthResponse;
- }
- | {
- type: 'authorization_response';
- error: Error;
- };
+export type WebMessageResponse = _WebMessageResponse;
diff --git a/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.test.ts b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.test.ts
new file mode 100644
index 0000000000..5c5de6a890
--- /dev/null
+++ b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.test.ts
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2023 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 { AuthResolverContext } from '@backstage/plugin-auth-node';
+import { AuthHandler } from '../../providers';
+import { OAuthResult } from '../oauth';
+import { PassportProfile } from '../passport/types';
+import { adaptLegacyOAuthHandler } from './adaptLegacyOAuthHandler';
+
+describe('adaptLegacyOAuthHandler', () => {
+ it('should pass through undefined', () => {
+ expect(adaptLegacyOAuthHandler(undefined)).toBeUndefined();
+ });
+
+ it('should convert an old auth handler to a new profile transform', () => {
+ const authHandler: AuthHandler = jest.fn();
+ const profileTransform = adaptLegacyOAuthHandler(authHandler);
+
+ profileTransform?.(
+ {
+ fullProfile: { id: 'id' } as PassportProfile,
+ session: {
+ accessToken: 'token',
+ expiresInSeconds: 3,
+ scope: 'sco pe',
+ tokenType: 'bear',
+ idToken: 'id-token',
+ refreshToken: 'refresh-token',
+ },
+ },
+ { ctx: 'ctx' } as unknown as AuthResolverContext,
+ );
+
+ expect(authHandler).toHaveBeenCalledWith(
+ {
+ fullProfile: { id: 'id' },
+ accessToken: 'token',
+ params: {
+ scope: 'sco pe',
+ id_token: 'id-token',
+ expires_in: 3,
+ token_type: 'bear',
+ },
+ },
+ { ctx: 'ctx' },
+ );
+ });
+});
diff --git a/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.ts b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.ts
new file mode 100644
index 0000000000..37f10ff184
--- /dev/null
+++ b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.ts
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2023 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 {
+ OAuthAuthenticatorResult,
+ ProfileTransform,
+} from '@backstage/plugin-auth-node';
+import { AuthHandler } from '../../providers';
+import { OAuthResult } from '../oauth';
+import { PassportProfile } from '../passport/types';
+
+/** @internal */
+export function adaptLegacyOAuthHandler(
+ authHandler?: AuthHandler,
+): ProfileTransform> | undefined {
+ return (
+ authHandler &&
+ (async (result, ctx) =>
+ authHandler(
+ {
+ fullProfile: result.fullProfile,
+ accessToken: result.session.accessToken,
+ params: {
+ scope: result.session.scope,
+ id_token: result.session.idToken,
+ token_type: result.session.tokenType,
+ expires_in: result.session.expiresInSeconds,
+ },
+ },
+ ctx,
+ ))
+ );
+}
diff --git a/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.test.ts b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.test.ts
new file mode 100644
index 0000000000..749cf96cbb
--- /dev/null
+++ b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.test.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2023 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 {
+ AuthResolverContext,
+ PassportProfile,
+} from '@backstage/plugin-auth-node';
+import { adaptLegacyOAuthSignInResolver } from './adaptLegacyOAuthSignInResolver';
+
+describe('adaptLegacyOAuthSignInResolver', () => {
+ it('should pass through undefined', () => {
+ expect(adaptLegacyOAuthSignInResolver(undefined)).toBeUndefined();
+ });
+
+ it('should convert a legacy resolver to a new one', () => {
+ const legacyResolver = jest.fn();
+
+ const newResolver = adaptLegacyOAuthSignInResolver(legacyResolver);
+
+ newResolver?.(
+ {
+ profile: { email: 'em@i.l' },
+ result: {
+ fullProfile: { id: 'id' } as PassportProfile,
+ session: {
+ accessToken: 'token',
+ expiresInSeconds: 3,
+ scope: 'sco pe',
+ tokenType: 'bear',
+ idToken: 'id-token',
+ refreshToken: 'refresh-token',
+ },
+ },
+ },
+ { ctx: 'ctx' } as unknown as AuthResolverContext,
+ );
+
+ expect(legacyResolver).toHaveBeenCalledWith(
+ {
+ profile: { email: 'em@i.l' },
+ result: {
+ fullProfile: { id: 'id' },
+ accessToken: 'token',
+ refreshToken: 'refresh-token',
+ params: {
+ scope: 'sco pe',
+ id_token: 'id-token',
+ expires_in: 3,
+ token_type: 'bear',
+ },
+ },
+ },
+ { ctx: 'ctx' },
+ );
+ });
+});
diff --git a/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.ts b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.ts
new file mode 100644
index 0000000000..e0318464ed
--- /dev/null
+++ b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 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 {
+ OAuthAuthenticatorResult,
+ PassportProfile,
+ SignInResolver,
+} from '@backstage/plugin-auth-node';
+import { OAuthResult } from '../oauth';
+
+/** @internal */
+export function adaptLegacyOAuthSignInResolver(
+ signInResolver?: SignInResolver,
+): SignInResolver