document the catalog read-write split
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
@@ -43,6 +43,7 @@ bugfixes
|
||||
builtins
|
||||
bundler
|
||||
bundlers
|
||||
bursty
|
||||
callout
|
||||
CDNs
|
||||
Chai
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# Catalog Read-Write Split
|
||||
|
||||
This article gives an overview of how you might split your catalog deployment into two - one which handles only reads (the read part of the API), and another that handles everything else (the write part of the API, and processing and ingestion etc).
|
||||
|
||||
This is useful for several reasons:
|
||||
|
||||
- The performance characteristics and scaling needs of those two concerns typically differ significantly
|
||||
- Moving the bursty processing and ingestion load on a separate deployment can make the important read part of the API have more deterministic response times
|
||||
- It opens up for easier cross region deployment of read nodes, while having write nodes in a single "home" region
|
||||
- It also opens up for primary/secondary split of the underlying database, with streaming replication
|
||||
|
||||
## Overview
|
||||
|
||||
This is a rough overview of what we will go through:
|
||||
|
||||
- Ensure that all frontend code uses `catalogApiRef`, and all backend code uses `catalogServiceRef`, for talking to the catalog
|
||||
- Add a separate discovery name for the write deployment
|
||||
- Register both frontend and backend implementations of the catalog client that know to send traffic to the right discovery name as needed
|
||||
- Set up the backend itself with dedicated read and write configuration
|
||||
- Split the deployment into two
|
||||
- Follow up with a database split, if desired
|
||||
|
||||
## 1. Prerequisite: Establish usage of catalog client refs
|
||||
|
||||
First, ensure that all of your code uses _injected_ catalog clients for its catalog communications needs instead of making their own with `new`. For frontend code that means leveraging `catalogApiRef`, and for backend it means leveraging `catalogServiceRef`. This is what enables the sort of wholesale injection of custom behaviors that we are about to do, and is a strongly recommended best practice either way.
|
||||
|
||||
- ✅ Search through your projects and ensure that there are no `new CatalogClient` calls. If you find any, consider rewriting that code to instead having the client injected through leveraging the corresponding ref.
|
||||
|
||||
- ✅ Search through your projects and ensure that you do _not_ import the `catalogServiceRef` that specifically is in the `@backstage/plugin-catalog-node/alpha` export. That one has been deprecated for a long time and will not be targeted by this guide. If you find any, consider rewriting that code to instead inject the client from `@backstage/plugin-catalog-node` (note the lack of `/alpha`).
|
||||
|
||||
> [!TIP]
|
||||
> If you find usages of `new CatalogClient` in tests, you can normally leave those as-is. You can however consider to later try out the `catalogApiMock` from `@backstage/plugin-catalog-react/testUtils` for frontend tests, and `catalogServiceMock` from `@backstage/plugin-catalog-node/testUtils` for backend tests. These allow you to inject a client that behaves like a complete fake catalog backend with entities in it, without complex mocking.
|
||||
|
||||
## 2. Decide your deployment split structure
|
||||
|
||||
We will be defining a fake plugin ID called `catalog-write`. In addition to your current single catalog deployment you will add a second one that receives traffic for `catalog-write`. You need to decide upon what your ingress to those two deployments will look like. This is highly company dependent, so we cannot give general guidance.
|
||||
|
||||
This guide will however NOT split the code base into two. It will instead change your catalog backend code in such a way that it can support either deployment. This is all very flexible and you can tweak it as you see fit, but that's what worked well for us.
|
||||
|
||||
This guide will assume that when all is said and done, requests for the `catalog` plugin ID resolve to the URL `https://catalog-read.example.net/api/catalog`, and requests for the `catalog-write` plugin ID resolve to the URL `https://catalog-write.example.net/api/catalog`. This is just for illustration and you shall change these to their actual correct values at your company.
|
||||
|
||||
- ✅ Copy the contents of [the `app-config.yaml` file](./workspace/app-config.yaml) to your own. We will assume that these exact settings are applied both to your frontend and your catalog (read AND write) deployment. For now, if you are applying this guide step by step rather than all at once, you can set the URLs in there to point to your good old pre-split deployment and only update them again after the split.
|
||||
|
||||
> [!TIP]
|
||||
> As a future refactor, you could instead break out this behavior into a custom injected discovery implementation in a library that all your projects can import. That takes a few more steps and is left as a separate exercise. If you already have such a library in your internal NPM, feel free to put it there to begin with and import from there.
|
||||
|
||||
## 3. Get split-aware catalog clients in place
|
||||
|
||||
Now let's put clients in place whose methods send traffic to either `catalog` or `catalog-write` as needed.
|
||||
|
||||
- ✅ Copy [the `catalogApi.ts` file](./workspace/packages/app/src/catalogApi.ts) into your `packages/app/src` directory of the project where you develop your Backstage frontend. Then reference that file's API factory in your `packages/app/src/apis.ts` file, [as illustrated here](./workspace/packages/app/src/apis.ts).
|
||||
|
||||
- ✅ Copy [the `catalogService.ts` file](./workspace/packages/backend/src/catalogService.ts) into your `packages/backend/src` directory of the project where you develop your Software Catalog backend. We will reference the file itself in the step below.
|
||||
|
||||
After step 4 below is complete, these will be automatically picked up and used by all callers.
|
||||
|
||||
> [!TIP]
|
||||
> As a future refactor, you could instead break out these clients into a node and a react library that all your projects can import. That takes a few more steps and is left as a separate exercise. If you already have such libraries in your internal NPM, feel free to put them there to begin with and import from there.
|
||||
|
||||
## 4. Split the catalog backend
|
||||
|
||||
We'll now perform the actual split of the catalog backend. You will make separate config files that apply to each, and adapt the backend code to handle both cases seamlessly.
|
||||
|
||||
- ✅ Duplicate your deployment. It's highly company dependent how this is done. Some may for example duplicate some declarative `Deployment` and `Service` k8s manifests etc, but it depends.
|
||||
|
||||
- ✅ Copy [the `app-config.catalog-read.yaml`] and [the `app-config.catalog-write.yaml`] files into your project root. You must set up your read and write deployments to load their respective files with the `--config` flag, _in addition to_ the `app-config.yaml` file and possibly `app-config.production.yaml` that they already loaded.
|
||||
|
||||
- ✅ Copy [the `config.d.ts`](./workspace/config.d.ts) file to your catalog backend directory at `packages/backend`. You should refer to it in the `package.json`'s `"files"` and `"configSchema"` fields [as per usual](https://backstage.io/docs/conf/defining).
|
||||
|
||||
- ✅ Update your `packages/backend/src/index.ts` file to match [the proposed structure here](./workspace/packages/backend/src/index.ts). Note how it applies the custom catalog service we copied in the previous step, and has a separate section for write-only concerns.
|
||||
|
||||
## 5. Region and database split
|
||||
|
||||
After the above is deployed and working, you are well set up for some next step.
|
||||
|
||||
- Scaling your readers across multiple regions and add geo-aware routing to them, while keeping writers in a "home" region
|
||||
- Splitting the database server into a primary that the write nodes connect to, and regional secondaries with streaming replication from the primary
|
||||
|
||||
But all of this is left as an exercise for the reader.
|
||||
|
||||
## 6. Help make this smoother
|
||||
|
||||
Congratulations on successfully performing your split!
|
||||
|
||||
We would love [some help](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md) with making this process easier for others. For example, if read nodes could automatically transparently proxy traffic to write nodes as needed, we would need far less fiddling with clients at a relatively small extra cost.
|
||||
@@ -0,0 +1,12 @@
|
||||
# This file should be given to the catalog read nodes.
|
||||
|
||||
backend:
|
||||
database:
|
||||
# disable running database migrations for every plugin/module
|
||||
skipMigrations: true
|
||||
|
||||
catalog:
|
||||
role: read
|
||||
|
||||
# disable processing engine
|
||||
processingInterval: false
|
||||
@@ -0,0 +1,4 @@
|
||||
# This file should be given to the catalog write nodes.
|
||||
|
||||
catalog:
|
||||
role: write
|
||||
@@ -0,0 +1,19 @@
|
||||
# This is in your regular app-config.
|
||||
|
||||
# This introduces a made-up "catalog-write" plugin ID that the frontend catalog
|
||||
# API implementation can use to resolve where to find the write nodes.
|
||||
discovery:
|
||||
endpoints:
|
||||
#
|
||||
# TODO: Replace with actual URLs
|
||||
#
|
||||
# NOTE: Each target can also be an object with "internal" and "external"
|
||||
# keys with different URLs, if they should be different when called by
|
||||
# backends and the frontend, respectively.
|
||||
#
|
||||
- target: https://catalog-read.example.net/api/catalog
|
||||
plugins:
|
||||
- catalog
|
||||
- target: https://catalog-write.example.net/api/catalog
|
||||
plugins:
|
||||
- catalog-write
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface Config {
|
||||
catalog: {
|
||||
role: 'read' | 'write';
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { AnyApiFactory } from '@backstage/core-plugin-api';
|
||||
import { catalogApiFactory } from './catalogApi';
|
||||
|
||||
// This file is already present in a typical Backstage app. We are just adding
|
||||
// the catalog API factory here to ensure that the frontend sends requests to
|
||||
// the correct set of catalog nodes depending on whether it's a read or a write
|
||||
// operation.
|
||||
export const apis: AnyApiFactory[] = [
|
||||
// ...other APIs here...
|
||||
catalogApiFactory,
|
||||
];
|
||||
@@ -0,0 +1,174 @@
|
||||
import {
|
||||
AddLocationRequest,
|
||||
AddLocationResponse,
|
||||
CatalogApi,
|
||||
CatalogClient,
|
||||
CatalogRequestOptions,
|
||||
GetEntitiesByRefsRequest,
|
||||
GetEntitiesByRefsResponse,
|
||||
GetEntitiesRequest,
|
||||
GetEntitiesResponse,
|
||||
GetEntityAncestorsRequest,
|
||||
GetEntityAncestorsResponse,
|
||||
GetEntityFacetsRequest,
|
||||
GetEntityFacetsResponse,
|
||||
GetLocationsResponse,
|
||||
Location,
|
||||
QueryEntitiesRequest,
|
||||
QueryEntitiesResponse,
|
||||
ValidateEntityResponse,
|
||||
} from '@backstage/catalog-client';
|
||||
import { CompoundEntityRef, Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
createApiFactory,
|
||||
DiscoveryApi,
|
||||
discoveryApiRef,
|
||||
FetchApi,
|
||||
fetchApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
|
||||
/**
|
||||
* Implements the catalog client for frontends, by using a custom service
|
||||
* discovery to achieve read/write segregation.
|
||||
*/
|
||||
export class ReadWriteSplitCatalogClient implements CatalogApi {
|
||||
#write: CatalogApi;
|
||||
#read: CatalogApi;
|
||||
|
||||
constructor(options: { discoveryApi: DiscoveryApi; fetchApi: FetchApi }) {
|
||||
this.#read = new CatalogClient({
|
||||
fetchApi: options.fetchApi,
|
||||
discoveryApi: {
|
||||
// This uses the normal "catalog" plugin ID
|
||||
getBaseUrl: () => options.discoveryApi.getBaseUrl('catalog'),
|
||||
},
|
||||
});
|
||||
this.#write = new CatalogClient({
|
||||
fetchApi: options.fetchApi,
|
||||
discoveryApi: {
|
||||
// This uses the made-up "catalog-write" plugin ID (see app-config.yaml
|
||||
// which defines a custom discovery endpoint for this)
|
||||
getBaseUrl: () => options.discoveryApi.getBaseUrl('catalog-write'),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getEntities(
|
||||
request?: GetEntitiesRequest,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<GetEntitiesResponse> {
|
||||
return this.#read.getEntities(request, options);
|
||||
}
|
||||
|
||||
getEntitiesByRefs(
|
||||
request: GetEntitiesByRefsRequest,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<GetEntitiesByRefsResponse> {
|
||||
return this.#read.getEntitiesByRefs(request, options);
|
||||
}
|
||||
|
||||
queryEntities(
|
||||
request?: QueryEntitiesRequest,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<QueryEntitiesResponse> {
|
||||
return this.#read.queryEntities(request, options);
|
||||
}
|
||||
|
||||
getEntityAncestors(
|
||||
request: GetEntityAncestorsRequest,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<GetEntityAncestorsResponse> {
|
||||
return this.#read.getEntityAncestors(request, options);
|
||||
}
|
||||
|
||||
getEntityByRef(
|
||||
entityRef: string | CompoundEntityRef,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<Entity | undefined> {
|
||||
return this.#read.getEntityByRef(entityRef, options);
|
||||
}
|
||||
|
||||
removeEntityByUid(
|
||||
uid: string,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<void> {
|
||||
return this.#write.removeEntityByUid(uid, options);
|
||||
}
|
||||
|
||||
refreshEntity(
|
||||
entityRef: string,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<void> {
|
||||
return this.#write.refreshEntity(entityRef, options);
|
||||
}
|
||||
|
||||
getEntityFacets(
|
||||
request: GetEntityFacetsRequest,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<GetEntityFacetsResponse> {
|
||||
return this.#read.getEntityFacets(request, options);
|
||||
}
|
||||
|
||||
getLocations(
|
||||
request?: {},
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<GetLocationsResponse> {
|
||||
return this.#read.getLocations(request, options);
|
||||
}
|
||||
|
||||
getLocationById(
|
||||
id: string,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<Location | undefined> {
|
||||
return this.#read.getLocationById(id, options);
|
||||
}
|
||||
|
||||
getLocationByRef(
|
||||
locationRef: string,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<Location | undefined> {
|
||||
return this.#read.getLocationByRef(locationRef, options);
|
||||
}
|
||||
|
||||
addLocation(
|
||||
location: AddLocationRequest,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<AddLocationResponse> {
|
||||
return this.#write.addLocation(location, options);
|
||||
}
|
||||
|
||||
removeLocationById(
|
||||
id: string,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<void> {
|
||||
return this.#write.removeLocationById(id, options);
|
||||
}
|
||||
|
||||
getLocationByEntity(
|
||||
entityRef: string | CompoundEntityRef,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<Location | undefined> {
|
||||
return this.#read.getLocationByEntity(entityRef, options);
|
||||
}
|
||||
|
||||
validateEntity(
|
||||
entity: Entity,
|
||||
locationRef: string,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<ValidateEntityResponse> {
|
||||
return this.#write.validateEntity(entity, locationRef, options);
|
||||
}
|
||||
}
|
||||
|
||||
// You should pass this to createApp.
|
||||
export const catalogApiFactory = createApiFactory({
|
||||
api: catalogApiRef,
|
||||
deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },
|
||||
factory: ({ discoveryApi, fetchApi }) => {
|
||||
return new ReadWriteSplitCatalogClient({
|
||||
discoveryApi,
|
||||
fetchApi,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,231 @@
|
||||
import {
|
||||
AuthService,
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
DiscoveryService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
AddLocationRequest,
|
||||
AddLocationResponse,
|
||||
CatalogApi,
|
||||
CatalogClient,
|
||||
CatalogRequestOptions,
|
||||
GetEntitiesByRefsRequest,
|
||||
GetEntitiesByRefsResponse,
|
||||
GetEntitiesRequest,
|
||||
GetEntitiesResponse,
|
||||
GetEntityAncestorsRequest,
|
||||
GetEntityAncestorsResponse,
|
||||
GetEntityFacetsRequest,
|
||||
GetEntityFacetsResponse,
|
||||
GetLocationsResponse,
|
||||
Location,
|
||||
QueryEntitiesRequest,
|
||||
QueryEntitiesResponse,
|
||||
ValidateEntityResponse,
|
||||
} from '@backstage/catalog-client';
|
||||
import { CompoundEntityRef, Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
CatalogService,
|
||||
CatalogServiceRequestOptions,
|
||||
catalogServiceRef,
|
||||
} from '@backstage/plugin-catalog-node';
|
||||
|
||||
/**
|
||||
* Implements the catalog client for backends, by using a custom service
|
||||
* discovery to achieve read/write segregation.
|
||||
*/
|
||||
export class ReadWriteSplitCatalogService implements CatalogService {
|
||||
readonly #auth: AuthService;
|
||||
readonly #catalogRead: CatalogApi;
|
||||
readonly #catalogWrite: CatalogApi;
|
||||
|
||||
constructor(options: { auth: AuthService; discovery: DiscoveryService }) {
|
||||
this.#auth = options.auth;
|
||||
this.#catalogRead = new CatalogClient({
|
||||
discoveryApi: {
|
||||
// This uses the normal "catalog" plugin ID
|
||||
getBaseUrl: () => options.discovery.getBaseUrl('catalog'),
|
||||
},
|
||||
});
|
||||
this.#catalogWrite = new CatalogClient({
|
||||
discoveryApi: {
|
||||
// This uses the made-up "catalog-write" plugin ID (see app-config.yaml
|
||||
// which defines a custom discovery endpoint for this)
|
||||
getBaseUrl: () => options.discovery.getBaseUrl('catalog-write'),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getEntities(
|
||||
request: GetEntitiesRequest | undefined,
|
||||
options: CatalogServiceRequestOptions,
|
||||
): Promise<GetEntitiesResponse> {
|
||||
return this.#catalogRead.getEntities(
|
||||
request,
|
||||
await this.#getOptions(options),
|
||||
);
|
||||
}
|
||||
|
||||
async getEntitiesByRefs(
|
||||
request: GetEntitiesByRefsRequest,
|
||||
options: CatalogServiceRequestOptions,
|
||||
): Promise<GetEntitiesByRefsResponse> {
|
||||
return this.#catalogRead.getEntitiesByRefs(
|
||||
request,
|
||||
await this.#getOptions(options),
|
||||
);
|
||||
}
|
||||
|
||||
async queryEntities(
|
||||
request: QueryEntitiesRequest | undefined,
|
||||
options: CatalogServiceRequestOptions,
|
||||
): Promise<QueryEntitiesResponse> {
|
||||
return this.#catalogRead.queryEntities(
|
||||
request,
|
||||
await this.#getOptions(options),
|
||||
);
|
||||
}
|
||||
|
||||
async getEntityAncestors(
|
||||
request: GetEntityAncestorsRequest,
|
||||
options: CatalogServiceRequestOptions,
|
||||
): Promise<GetEntityAncestorsResponse> {
|
||||
return this.#catalogRead.getEntityAncestors(
|
||||
request,
|
||||
await this.#getOptions(options),
|
||||
);
|
||||
}
|
||||
|
||||
async getEntityByRef(
|
||||
entityRef: string | CompoundEntityRef,
|
||||
options: CatalogServiceRequestOptions,
|
||||
): Promise<Entity | undefined> {
|
||||
return this.#catalogRead.getEntityByRef(
|
||||
entityRef,
|
||||
await this.#getOptions(options),
|
||||
);
|
||||
}
|
||||
|
||||
async removeEntityByUid(
|
||||
uid: string,
|
||||
options: CatalogServiceRequestOptions,
|
||||
): Promise<void> {
|
||||
return this.#catalogWrite.removeEntityByUid(
|
||||
uid,
|
||||
await this.#getOptions(options),
|
||||
);
|
||||
}
|
||||
|
||||
async refreshEntity(
|
||||
entityRef: string,
|
||||
options: CatalogServiceRequestOptions,
|
||||
): Promise<void> {
|
||||
return this.#catalogWrite.refreshEntity(
|
||||
entityRef,
|
||||
await this.#getOptions(options),
|
||||
);
|
||||
}
|
||||
|
||||
async getEntityFacets(
|
||||
request: GetEntityFacetsRequest,
|
||||
options: CatalogServiceRequestOptions,
|
||||
): Promise<GetEntityFacetsResponse> {
|
||||
return this.#catalogRead.getEntityFacets(
|
||||
request,
|
||||
await this.#getOptions(options),
|
||||
);
|
||||
}
|
||||
|
||||
async getLocations(
|
||||
options: CatalogRequestOptions,
|
||||
): Promise<GetLocationsResponse> {
|
||||
return this.#catalogRead.getLocations(options);
|
||||
}
|
||||
|
||||
async getLocationById(
|
||||
id: string,
|
||||
options: CatalogServiceRequestOptions,
|
||||
): Promise<Location | undefined> {
|
||||
return this.#catalogRead.getLocationById(
|
||||
id,
|
||||
await this.#getOptions(options),
|
||||
);
|
||||
}
|
||||
|
||||
async getLocationByRef(
|
||||
locationRef: string,
|
||||
options: CatalogServiceRequestOptions,
|
||||
): Promise<Location | undefined> {
|
||||
return this.#catalogRead.getLocationByRef(
|
||||
locationRef,
|
||||
await this.#getOptions(options),
|
||||
);
|
||||
}
|
||||
|
||||
async addLocation(
|
||||
location: AddLocationRequest,
|
||||
options: CatalogServiceRequestOptions,
|
||||
): Promise<AddLocationResponse> {
|
||||
return this.#catalogWrite.addLocation(
|
||||
location,
|
||||
await this.#getOptions(options),
|
||||
);
|
||||
}
|
||||
|
||||
async removeLocationById(
|
||||
id: string,
|
||||
options: CatalogServiceRequestOptions,
|
||||
): Promise<void> {
|
||||
return this.#catalogWrite.removeLocationById(
|
||||
id,
|
||||
await this.#getOptions(options),
|
||||
);
|
||||
}
|
||||
|
||||
async getLocationByEntity(
|
||||
entityRef: string | CompoundEntityRef,
|
||||
options: CatalogServiceRequestOptions,
|
||||
): Promise<Location | undefined> {
|
||||
return this.#catalogRead.getLocationByEntity(
|
||||
entityRef,
|
||||
await this.#getOptions(options),
|
||||
);
|
||||
}
|
||||
|
||||
async validateEntity(
|
||||
entity: Entity,
|
||||
locationRef: string,
|
||||
options: CatalogServiceRequestOptions,
|
||||
): Promise<ValidateEntityResponse> {
|
||||
return this.#catalogWrite.validateEntity(
|
||||
entity,
|
||||
locationRef,
|
||||
await this.#getOptions(options),
|
||||
);
|
||||
}
|
||||
|
||||
async #getOptions(
|
||||
options: CatalogServiceRequestOptions,
|
||||
): Promise<CatalogRequestOptions> {
|
||||
return this.#auth.getPluginRequestToken({
|
||||
onBehalfOf: options.credentials,
|
||||
targetPluginId: 'catalog',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// You should register this in your backend.
|
||||
export const catalogService = createServiceFactory({
|
||||
service: catalogServiceRef,
|
||||
deps: {
|
||||
auth: coreServices.auth,
|
||||
discovery: coreServices.discovery,
|
||||
},
|
||||
async factory({ auth, discovery }) {
|
||||
return new ReadWriteSplitCatalogService({
|
||||
auth,
|
||||
discovery,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { createBackend } from '@backstage/backend-defaults';
|
||||
import {
|
||||
coreServices,
|
||||
createBackendFeatureLoader,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { catalogService } from './catalogService';
|
||||
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
backend.add(catalogService);
|
||||
// Here at the root level, you can add more features that you want to apply to
|
||||
// BOTH the read and write nodes, as needed.
|
||||
|
||||
backend.add(
|
||||
createBackendFeatureLoader({
|
||||
deps: { config: coreServices.rootConfig },
|
||||
async *loader({ config }) {
|
||||
if (config.getString('catalog.role') !== 'write') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Here inside the loader, you can add more features that you want to
|
||||
// apply to ONLY the write nodes, as needed. This is where providers and
|
||||
// processors go, for example. The incremental ingestion provider line
|
||||
// below is just an example.
|
||||
yield import(
|
||||
'@backstage/plugin-catalog-backend-module-incremental-ingestion'
|
||||
);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
backend.start();
|
||||
Reference in New Issue
Block a user