From 5cf35d194818a4bb72db158e94d569c4ca8ec5dc Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Fri, 28 Jul 2023 14:22:57 +0200 Subject: [PATCH 1/5] Remove shared environment code & documentation Signed-off-by: Philipp Hugenroth --- .../architecture/02-backends.md | 2 +- .../building-backends/01-index.md | 32 ------ .../blog/2023-02-15-backend-system-alpha.mdx | 1 - .../src/CreateBackend.test.ts | 82 +++++--------- .../backend-defaults/src/CreateBackend.ts | 22 ---- .../wiring/createSharedEnvironment.test.ts | 100 ----------------- .../src/wiring/createSharedEnvironment.ts | 102 ------------------ .../backend-plugin-api/src/wiring/index.ts | 5 - 8 files changed, 26 insertions(+), 320 deletions(-) delete mode 100644 packages/backend-plugin-api/src/wiring/createSharedEnvironment.test.ts delete mode 100644 packages/backend-plugin-api/src/wiring/createSharedEnvironment.ts diff --git a/docs/backend-system/architecture/02-backends.md b/docs/backend-system/architecture/02-backends.md index b6079d71b4..70ea8f7836 100644 --- a/docs/backend-system/architecture/02-backends.md +++ b/docs/backend-system/architecture/02-backends.md @@ -37,4 +37,4 @@ At a high level, when you call `createBackend`, it will create a new backend ins Underneath the hood, `createBackend` calls `createSpecializedBackend` from `@backstage/backend-app-api` which is responsible for actually creating the backend instance, but with no services or no features. You can think of `createBackend` more of a 'batteries included' approach, and `createSpecializedBackend` a little more low level. -As mentioned previously there's also the ability to create multiple of these backends in your project so that you can split apart your backend and deploy different backends that can scale independently of each other. For instance you might choose to deploy a backend with only the catalog plugin enabled, and one with just the scaffolder plugin enabled. We've provided some tools to be able to share services and defaults across your backend system, and you can find out more about that in the [shared environments docs](../building-backends/01-index.md#shared-environments). +As mentioned previously there's also the ability to create multiple of these backends in your project so that you can split apart your backend and deploy different backends that can scale independently of each other. For instance you might choose to deploy a backend with only the catalog plugin enabled, and one with just the scaffolder plugin enabled. diff --git a/docs/backend-system/building-backends/01-index.md b/docs/backend-system/building-backends/01-index.md index 66bf68ab07..0f23ae0006 100644 --- a/docs/backend-system/building-backends/01-index.md +++ b/docs/backend-system/building-backends/01-index.md @@ -148,35 +148,3 @@ backend.start(); ``` We've now split the backend into two separate deployments, but we still need to make sure that they can communicate with each other. This is the hard and somewhat tedious part, as Backstage currently doesn't provide an out of the box solution that solves this. You'll need to manually configure the two backends with custom implementations of the `DiscoveryService` and have them return the correct URLs for each other. Likewise, you'll also need to provide a custom implementation of the `DiscoveryApi` in the frontend, unless you surface the two backends via a proxy that handles the routing instead. - -### Shared Environments - -To make it a bit easier to manage multiple backends, it's possible to create a shared environment that can be used across multiple backends. You would typically house it in a separate package that can be referenced by backends in your monorepo, or published to a package registry for broader use. - -A shared environment contains a set of service implementations that should be used across all backends. These services will override the default ones, but if a service is provided directly to the backend, it will override the one in the shared environment. - -A shared environment is defined using `createSharedEnvironment`. In this example we place it in a new and separate package called `backend-env`: - -```ts -// packages/backend-env/src/index.ts -import { createSharedEnvironment } from '@backstage/backend-plugin-api'; -import { customDiscoveryServiceFactory } from './customDiscoveryServiceFactory'; - -export const env = createSharedEnvironment({ - services: [ - customDiscoveryServiceFactory(), // custom DiscoveryService implementation - ], -}); -``` - -And passed on to backends using the `env` option: - -```ts -// packages/backend-b/src/index.ts, imports omitted -import { env } from '@internal/backend-env'; - -const backend = createBackend({ env }); - -backend.add(scaffolderPlugin()); -backend.start(); -``` diff --git a/microsite/blog/2023-02-15-backend-system-alpha.mdx b/microsite/blog/2023-02-15-backend-system-alpha.mdx index 2aef5596a0..28ff7bdf6b 100644 --- a/microsite/blog/2023-02-15-backend-system-alpha.mdx +++ b/microsite/blog/2023-02-15-backend-system-alpha.mdx @@ -26,7 +26,6 @@ When we set out on this project, we had a few primary goals. First, we intended - Make it easier to create and maintain backend installations. - Align how plugins provide points of customization and how those customizations are installed. - Make it much easier to maintain plugins, in particular keeping the API stable. -- Simplify the process of splitting plugins out into separate deployments with shared environments. - Improve the local development and testing experience. Prioritizing simplicity is often a guiding principle that we use for designs that span multiple ownership roles. We decide on which parts of the system that we think are the most important to have as simple as possible, or, viewed from the opposite end, where in the system we put necessary complexity. In this case, we optimized for keeping the backend setup as simple as possible, followed by modules and plugins, then libraries, and lastly the framework itself. What this guidance means in practice is that when there is complexity that needs to be added to implement a certain feature, we place as much of it as possible within the framework itself, then libraries, plugins and modules, and if absolutely needed, the backend setup. diff --git a/packages/backend-defaults/src/CreateBackend.test.ts b/packages/backend-defaults/src/CreateBackend.test.ts index 946de90b2b..88e6bddd97 100644 --- a/packages/backend-defaults/src/CreateBackend.test.ts +++ b/packages/backend-defaults/src/CreateBackend.test.ts @@ -19,10 +19,9 @@ import { createBackendPlugin, createServiceFactory, createServiceRef, - createSharedEnvironment, } from '@backstage/backend-plugin-api'; -import { mockServices } from '@backstage/backend-test-utils'; import { createBackend } from './CreateBackend'; +import { mockServices } from '@backstage/backend-test-utils'; const fooServiceRef = createServiceRef({ id: 'foo', scope: 'root' }); const barServiceRef = createServiceRef({ id: 'bar', scope: 'root' }); @@ -86,65 +85,34 @@ describe('createBackend', () => { ).toThrow('The core.pluginMetadata service cannot be overridden'); }); - it('should throw if an unsupported InternalSharedEnvironment version is passed in', () => { - expect(() => - createBackend({ - env: {} as any, - }), - ).toThrow( - "Shared environment version 'undefined' is invalid or not supported", - ); - expect(() => - createBackend({ - env: { version: {} } as any, - }), - ).toThrow( - "Shared environment version '[object Object]' is invalid or not supported", - ); - expect(() => - createBackend({ - env: { version: 'v2' } as any, - }), - ).toThrow("Shared environment version 'v2' is invalid or not supported"); - }); - it('should prioritize services correctly', async () => { const backend = createBackend({ - env: createSharedEnvironment({ - services: [ - createServiceFactory({ - service: coreServices.rootHttpRouter, - deps: {}, - async factory() { - return { - use() {}, - }; - }, - }), - mockServices.config.factory({ - data: { root: 'root-env' }, - }), - createServiceFactory({ - service: fooServiceRef, - deps: {}, - async factory() { - return 'foo-env'; - }, - }), - createServiceFactory({ - service: barServiceRef, - deps: {}, - async factory() { - return 'bar-env'; - }, - }), - ], - })(), services: [ + createServiceFactory({ + service: coreServices.rootHttpRouter, + deps: {}, + async factory() { + return { + use() {}, + }; + }, + }), + mockServices.config.factory({ + data: { root: 'root-backend' }, + }), createServiceFactory({ service: fooServiceRef, deps: {}, - factory: async () => 'foo-backend', + async factory() { + return 'foo-backend'; + }, + }), + createServiceFactory({ + service: barServiceRef, + deps: {}, + async factory() { + return 'bar-backend'; + }, }), ], }); @@ -161,9 +129,9 @@ describe('createBackend', () => { bar: barServiceRef, }, async init({ config, foo, bar }) { - expect(config.get('root')).toBe('root-env'); + expect(config.get('root')).toBe('root-backend'); expect(foo).toBe('foo-backend'); - expect(bar).toBe('bar-env'); + expect(bar).toBe('bar-backend'); }, }); }, diff --git a/packages/backend-defaults/src/CreateBackend.ts b/packages/backend-defaults/src/CreateBackend.ts index 480f365216..7051b58ae5 100644 --- a/packages/backend-defaults/src/CreateBackend.ts +++ b/packages/backend-defaults/src/CreateBackend.ts @@ -36,13 +36,8 @@ import { import { ServiceFactory, ServiceFactoryOrFunction, - SharedBackendEnvironment, } from '@backstage/backend-plugin-api'; -// Internal import of the type to avoid needing to export this. -// eslint-disable-next-line @backstage/no-forbidden-package-imports -import type { InternalSharedBackendEnvironment } from '@backstage/backend-plugin-api/src/wiring/createSharedEnvironment'; - export const defaultServiceFactories = [ cacheServiceFactory(), configServiceFactory(), @@ -65,7 +60,6 @@ export const defaultServiceFactories = [ * @public */ export interface CreateBackendOptions { - env?: SharedBackendEnvironment; services?: ServiceFactoryOrFunction[]; } @@ -81,22 +75,6 @@ export function createBackend(options?: CreateBackendOptions): Backend { ); services.push(...providedServices); - // Middle priority: Services from the shared environment - if (options?.env) { - const env = options.env as unknown as InternalSharedBackendEnvironment; - if (env.version !== 'v1') { - throw new Error( - `Shared environment version '${env.version}' is invalid or not supported`, - ); - } - - const environmentServices = - env.services?.filter( - sf => !services.some(({ service }) => sf.service.id === service.id), - ) ?? []; - services.push(...environmentServices); - } - // 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), diff --git a/packages/backend-plugin-api/src/wiring/createSharedEnvironment.test.ts b/packages/backend-plugin-api/src/wiring/createSharedEnvironment.test.ts deleted file mode 100644 index 381a4799d2..0000000000 --- a/packages/backend-plugin-api/src/wiring/createSharedEnvironment.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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 { - createServiceFactory, - createServiceRef, - ServiceFactoryOrFunction, -} from '../services'; -import { - createSharedEnvironment, - InternalSharedBackendEnvironment, -} from './createSharedEnvironment'; - -const fooService = createServiceRef({ id: 'foo', scope: 'root' }); -const fooFactory = createServiceFactory({ - service: fooService, - deps: {}, - async factory() { - return 'foo'; - }, -}); - -const barService = createServiceRef({ id: 'bar', scope: 'root' }); -const barFactory = createServiceFactory({ - service: barService, - deps: {}, - async factory() { - return 0xba5; - }, -}); - -describe('createSharedEnvironment', () => { - it('should create an empty shared environment', () => { - const env = createSharedEnvironment({}); - expect(env).toBeDefined(); - const internalEnv = env() as unknown as InternalSharedBackendEnvironment; - expect(internalEnv).toEqual({ - $$type: '@backstage/SharedBackendEnvironment', - version: 'v1', - services: undefined, - }); - }); - - it('should create a shared environment with services', () => { - const env = createSharedEnvironment({ - services: [fooFactory, barFactory()], - }); - const internalEnv = env() as unknown as InternalSharedBackendEnvironment; - expect(internalEnv.version).toBe('v1'); - expect(internalEnv.services?.length).toBe(2); - expect(internalEnv.services?.[0]?.service.id).toBe('foo'); - expect(internalEnv.services?.[1]?.service.id).toBe('bar'); - }); - - it('should create a shared environment with options', () => { - const env = createSharedEnvironment((options?: { withFoo?: boolean }) => { - const services = new Array(); - if (options?.withFoo) { - services.push(fooFactory()); - } - services.push(barFactory); - return { services }; - }); - const internalEnv1 = env() as unknown as InternalSharedBackendEnvironment; - expect(internalEnv1.version).toBe('v1'); - expect(internalEnv1.services?.length).toBe(1); - expect(internalEnv1.services?.[0]?.service.id).toBe('bar'); - - const internalEnv2 = env({ - withFoo: true, - }) as unknown as InternalSharedBackendEnvironment; - expect(internalEnv2.version).toBe('v1'); - expect(internalEnv2.services?.length).toBe(2); - expect(internalEnv2.services?.[0]?.service.id).toBe('foo'); - expect(internalEnv2.services?.[1]?.service.id).toBe('bar'); - }); - - it('should not allow duplicate service factories', () => { - expect(() => - createSharedEnvironment({ - services: [fooFactory, fooFactory()], - })(), - ).toThrow( - "Duplicate service implementations provided in shared environment for 'foo'", - ); - }); -}); diff --git a/packages/backend-plugin-api/src/wiring/createSharedEnvironment.ts b/packages/backend-plugin-api/src/wiring/createSharedEnvironment.ts deleted file mode 100644 index 57f0533523..0000000000 --- a/packages/backend-plugin-api/src/wiring/createSharedEnvironment.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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 { ServiceFactory, ServiceFactoryOrFunction } from '../services'; - -/** - * The configuration options passed to {@link createSharedEnvironment}. - * - * @public - */ -export interface SharedBackendEnvironmentConfig { - services?: ServiceFactoryOrFunction[]; -} - -/** - * An opaque type that represents the contents of a shared backend environment. - * - * @public - */ -export interface SharedBackendEnvironment { - $$type: '@backstage/SharedBackendEnvironment'; - - // NOTE: This type is opaque in order to allow for future API evolution without - // cluttering the external API. For example we might want to add support - // for more powerful callback based backend modifications. - // - // By making this opaque we also ensure that the type doesn't become an input - // type that we need to care about, as it would otherwise be possible to pass - // a custom environment definition to `createBackend`, which we don't want. -} - -/** - * This type is NOT supposed to be used by anyone except internally by the - * backend-app-api package. - * - * @internal - */ -export interface InternalSharedBackendEnvironment { - version: 'v1'; - services?: ServiceFactory[]; -} - -/** - * Creates a shared backend environment which can be used to create multiple - * backends. - * - * @public - */ -export function createSharedEnvironment< - TOptions extends [options?: object] = [], ->( - config: - | SharedBackendEnvironmentConfig - | ((...params: TOptions) => SharedBackendEnvironmentConfig), -): (...options: TOptions) => SharedBackendEnvironment { - const configCallback = typeof config === 'function' ? config : () => config; - - return (...options) => { - const actualConfig = configCallback(...options); - const services = actualConfig?.services?.map(sf => - typeof sf === 'function' ? sf() : sf, - ); - - const exists = new Set(); - const duplicates = new Set(); - for (const { service } of services ?? []) { - if (exists.has(service.id)) { - duplicates.add(service.id); - } else { - exists.add(service.id); - } - } - - if (duplicates.size > 0) { - const dupStr = [...duplicates].map(id => `'${id}'`).join(', '); - throw new Error( - `Duplicate service implementations provided in shared environment for ${dupStr}`, - ); - } - - // Here to ensure type safety in this internal implementation. - const env: SharedBackendEnvironment & InternalSharedBackendEnvironment = { - $$type: '@backstage/SharedBackendEnvironment', - version: 'v1', - services, - }; - return env; - }; -} diff --git a/packages/backend-plugin-api/src/wiring/index.ts b/packages/backend-plugin-api/src/wiring/index.ts index 242301ea4d..9cb767b8ac 100644 --- a/packages/backend-plugin-api/src/wiring/index.ts +++ b/packages/backend-plugin-api/src/wiring/index.ts @@ -14,11 +14,6 @@ * limitations under the License. */ -export { createSharedEnvironment } from './createSharedEnvironment'; -export type { - SharedBackendEnvironment, - SharedBackendEnvironmentConfig, -} from './createSharedEnvironment'; export type { BackendModuleConfig, BackendPluginConfig, From d008aefef8089e9aaeda25839ed7dd5df55aabf6 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Fri, 28 Jul 2023 14:48:38 +0200 Subject: [PATCH 2/5] Build api-reports & add changeset Signed-off-by: Philipp Hugenroth --- .changeset/rich-zoos-occur.md | 6 ++++++ packages/backend-defaults/api-report.md | 3 --- packages/backend-plugin-api/api-report.md | 21 --------------------- 3 files changed, 6 insertions(+), 24 deletions(-) create mode 100644 .changeset/rich-zoos-occur.md diff --git a/.changeset/rich-zoos-occur.md b/.changeset/rich-zoos-occur.md new file mode 100644 index 0000000000..65d21710d1 --- /dev/null +++ b/.changeset/rich-zoos-occur.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-plugin-api': minor +'@backstage/backend-defaults': minor +--- + +Removing shared environments concept from the new experimental backend system. diff --git a/packages/backend-defaults/api-report.md b/packages/backend-defaults/api-report.md index 07218f610e..6cc58de663 100644 --- a/packages/backend-defaults/api-report.md +++ b/packages/backend-defaults/api-report.md @@ -5,15 +5,12 @@ ```ts import { Backend } from '@backstage/backend-app-api'; import { ServiceFactoryOrFunction } from '@backstage/backend-plugin-api'; -import { SharedBackendEnvironment } from '@backstage/backend-plugin-api'; // @public (undocumented) export function createBackend(options?: CreateBackendOptions): Backend; // @public (undocumented) export interface CreateBackendOptions { - // (undocumented) - env?: SharedBackendEnvironment; // (undocumented) services?: ServiceFactoryOrFunction[]; } diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index b3cbef993c..093b4acec4 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -222,15 +222,6 @@ export function createServiceRef( config: ServiceRefConfig, ): ServiceRef; -// @public -export function createSharedEnvironment< - TOptions extends [options?: object] = [], ->( - config: - | SharedBackendEnvironmentConfig - | ((...params: TOptions) => SharedBackendEnvironmentConfig), -): (...options: TOptions) => SharedBackendEnvironment; - // @public export interface DatabaseService { getClient(): Promise; @@ -476,18 +467,6 @@ export interface ServiceRefConfig { scope?: TScope; } -// @public -export interface SharedBackendEnvironment { - // (undocumented) - $$type: '@backstage/SharedBackendEnvironment'; -} - -// @public -export interface SharedBackendEnvironmentConfig { - // (undocumented) - services?: ServiceFactoryOrFunction[]; -} - // @public export interface TokenManagerService { authenticate(token: string): Promise; From e66544d53a7d315e874bed051bb3b656daa29c5d Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Fri, 28 Jul 2023 15:37:01 +0200 Subject: [PATCH 3/5] Remove unneeded test Signed-off-by: Philipp Hugenroth --- .../src/CreateBackend.test.ts | 56 ------------------- 1 file changed, 56 deletions(-) diff --git a/packages/backend-defaults/src/CreateBackend.test.ts b/packages/backend-defaults/src/CreateBackend.test.ts index 88e6bddd97..28276dbae1 100644 --- a/packages/backend-defaults/src/CreateBackend.test.ts +++ b/packages/backend-defaults/src/CreateBackend.test.ts @@ -84,60 +84,4 @@ describe('createBackend', () => { }), ).toThrow('The core.pluginMetadata service cannot be overridden'); }); - - it('should prioritize services correctly', async () => { - const backend = createBackend({ - services: [ - createServiceFactory({ - service: coreServices.rootHttpRouter, - deps: {}, - async factory() { - return { - use() {}, - }; - }, - }), - mockServices.config.factory({ - data: { root: 'root-backend' }, - }), - createServiceFactory({ - service: fooServiceRef, - deps: {}, - async factory() { - return 'foo-backend'; - }, - }), - createServiceFactory({ - service: barServiceRef, - deps: {}, - async factory() { - return 'bar-backend'; - }, - }), - ], - }); - - expect.assertions(3); - backend.add( - createBackendPlugin({ - pluginId: 'test', - register(reg) { - reg.registerInit({ - deps: { - config: coreServices.config, - foo: fooServiceRef, - bar: barServiceRef, - }, - async init({ config, foo, bar }) { - expect(config.get('root')).toBe('root-backend'); - expect(foo).toBe('foo-backend'); - expect(bar).toBe('bar-backend'); - }, - }); - }, - })(), - ); - - await backend.start(); - }); }); From 041e02e2ba8d66b6900b6e7b75528f56c825c7b3 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Fri, 28 Jul 2023 15:40:52 +0200 Subject: [PATCH 4/5] Remove unused imports & vars Signed-off-by: Philipp Hugenroth --- packages/backend-defaults/src/CreateBackend.test.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/backend-defaults/src/CreateBackend.test.ts b/packages/backend-defaults/src/CreateBackend.test.ts index 28276dbae1..de04d4e350 100644 --- a/packages/backend-defaults/src/CreateBackend.test.ts +++ b/packages/backend-defaults/src/CreateBackend.test.ts @@ -16,15 +16,9 @@ import { coreServices, - createBackendPlugin, createServiceFactory, - createServiceRef, } from '@backstage/backend-plugin-api'; import { createBackend } from './CreateBackend'; -import { mockServices } from '@backstage/backend-test-utils'; - -const fooServiceRef = createServiceRef({ id: 'foo', scope: 'root' }); -const barServiceRef = createServiceRef({ id: 'bar', scope: 'root' }); describe('createBackend', () => { it('should not throw when overriding a default service implementation', () => { From 80f78e85b7653ab52990866cc779c37f437265fc Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Fri, 28 Jul 2023 15:42:54 +0200 Subject: [PATCH 5/5] Change changeset to indicate breaking change Signed-off-by: Philipp Hugenroth --- .changeset/rich-zoos-occur.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/rich-zoos-occur.md b/.changeset/rich-zoos-occur.md index 65d21710d1..979acb93c6 100644 --- a/.changeset/rich-zoos-occur.md +++ b/.changeset/rich-zoos-occur.md @@ -3,4 +3,4 @@ '@backstage/backend-defaults': minor --- -Removing shared environments concept from the new experimental backend system. +**BREAKING**: Removing shared environments concept from the new experimental backend system.