Remove shared environment code & documentation
Signed-off-by: Philipp Hugenroth <philipph@spotify.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<string>({ id: 'foo', scope: 'root' });
|
||||
const barServiceRef = createServiceRef<string>({ 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');
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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<string>({ id: 'foo', scope: 'root' });
|
||||
const fooFactory = createServiceFactory({
|
||||
service: fooService,
|
||||
deps: {},
|
||||
async factory() {
|
||||
return 'foo';
|
||||
},
|
||||
});
|
||||
|
||||
const barService = createServiceRef<number>({ 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<ServiceFactoryOrFunction>();
|
||||
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'",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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<string>();
|
||||
const duplicates = new Set<string>();
|
||||
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;
|
||||
};
|
||||
}
|
||||
@@ -14,11 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { createSharedEnvironment } from './createSharedEnvironment';
|
||||
export type {
|
||||
SharedBackendEnvironment,
|
||||
SharedBackendEnvironmentConfig,
|
||||
} from './createSharedEnvironment';
|
||||
export type {
|
||||
BackendModuleConfig,
|
||||
BackendPluginConfig,
|
||||
|
||||
Reference in New Issue
Block a user