Merge pull request #24916 from backstage/rugvip/events

events-backend: added scaleable events service implementation
This commit is contained in:
Patrik Oldsberg
2024-09-23 16:45:43 +02:00
committed by GitHub
39 changed files with 3547 additions and 64 deletions
+12 -1
View File
@@ -3,6 +3,9 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { AuthService } from '@backstage/backend-plugin-api';
import { DiscoveryService } from '@backstage/backend-plugin-api';
import { LifecycleService } from '@backstage/backend-plugin-api';
import { LoggerService } from '@backstage/backend-plugin-api';
import { ServiceFactory } from '@backstage/backend-plugin-api';
import { ServiceRef } from '@backstage/backend-plugin-api';
@@ -11,7 +14,15 @@ import { ServiceRef } from '@backstage/backend-plugin-api';
export class DefaultEventsService implements EventsService {
// (undocumented)
static create(options: { logger: LoggerService }): DefaultEventsService;
forPlugin(pluginId: string): EventsService;
forPlugin(
pluginId: string,
options?: {
discovery: DiscoveryService;
logger: LoggerService;
auth: AuthService;
lifecycle: LifecycleService;
},
): EventsService;
// (undocumented)
publish(params: EventParams): Promise<void>;
// (undocumented)
+5 -1
View File
@@ -51,7 +51,11 @@
"test": "backstage-cli package test"
},
"dependencies": {
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/types": "workspace:^",
"cross-fetch": "^4.0.0",
"uri-template": "^2.0.0"
},
"devDependencies": {
"@backstage/backend-common": "^0.25.0",
@@ -14,9 +14,320 @@
* limitations under the License.
*/
import { LoggerService } from '@backstage/backend-plugin-api';
import {
AuthService,
DiscoveryService,
LifecycleService,
LoggerService,
} from '@backstage/backend-plugin-api';
import { EventParams } from './EventParams';
import { EventsService, EventsServiceSubscribeOptions } from './EventsService';
import { DefaultApiClient } from '../generated';
import { ResponseError } from '@backstage/errors';
const POLL_BACKOFF_START_MS = 1_000;
const POLL_BACKOFF_MAX_MS = 60_000;
const POLL_BACKOFF_FACTOR = 2;
/**
* Local event bus for subscribers within the same process.
*
* When publishing events we'll keep track of which subscribers we managed to
* reach locally, and forward those subscriber IDs to the events backend if it
* is in use. The events backend will then both avoid forwarding the same events
* to those subscribers again, but also avoid storing the event altogether if
* there are no other subscribers.
* @internal
*/
export class LocalEventBus {
readonly #logger: LoggerService;
readonly #subscribers = new Map<
string,
Omit<EventsServiceSubscribeOptions, 'topics'>[]
>();
constructor(logger: LoggerService) {
this.#logger = logger;
}
async publish(
params: EventParams,
): Promise<{ notifiedSubscribers: string[] }> {
this.#logger.debug(
`Event received: topic=${params.topic}, metadata=${JSON.stringify(
params.metadata,
)}, payload=${JSON.stringify(params.eventPayload)}`,
);
if (!this.#subscribers.has(params.topic)) {
return { notifiedSubscribers: [] };
}
const onEventPromises: Promise<string>[] = [];
this.#subscribers.get(params.topic)?.forEach(subscription => {
onEventPromises.push(
(async () => {
try {
await subscription.onEvent(params);
} catch (error) {
this.#logger.warn(
`Subscriber "${subscription.id}" failed to process event for topic "${params.topic}"`,
error,
);
}
return subscription.id;
})(),
);
});
return { notifiedSubscribers: await Promise.all(onEventPromises) };
}
async subscribe(options: EventsServiceSubscribeOptions): Promise<void> {
options.topics.forEach(topic => {
if (!this.#subscribers.has(topic)) {
this.#subscribers.set(topic, []);
}
this.#subscribers.get(topic)!.push({
id: options.id,
onEvent: options.onEvent,
});
});
}
}
/**
* Plugin specific events bus that delegates to the local bus, as well as the
* events backend if it is available.
*/
class PluginEventsService implements EventsService {
constructor(
private readonly pluginId: string,
private readonly localBus: LocalEventBus,
private readonly logger: LoggerService,
private client?: DefaultApiClient,
private readonly auth?: AuthService,
) {}
async publish(params: EventParams): Promise<void> {
const lock = this.#getShutdownLock();
try {
const { notifiedSubscribers } = await this.localBus.publish(params);
if (!this.client) {
return;
}
const token = await this.#getToken();
if (!token) {
return;
}
const res = await this.client.postEvent(
{
body: {
event: { payload: params.eventPayload, topic: params.topic },
notifiedSubscribers,
},
},
{ token },
);
if (!res.ok) {
if (res.status === 404) {
this.logger.warn(
`Event publish request failed with status 404, events backend not found. Future events will not be persisted.`,
);
delete this.client;
return;
}
throw await ResponseError.fromResponse(res);
}
} finally {
lock.release();
}
}
async subscribe(options: EventsServiceSubscribeOptions): Promise<void> {
const subscriptionId = `${this.pluginId}.${options.id}`;
await this.localBus.subscribe({
id: subscriptionId,
topics: options.topics,
onEvent: options.onEvent,
});
if (!this.client) {
return;
}
const token = await this.#getToken();
if (!token) {
return;
}
const res = await this.client.putSubscription(
{
path: { subscriptionId },
body: { topics: options.topics },
},
{ token },
);
if (!res.ok) {
if (res.status === 404) {
this.logger.warn(
`Event subscribe request failed with status 404, events backend not found. Will only receive events that were sent locally on this process.`,
);
delete this.client;
return;
}
throw await ResponseError.fromResponse(res);
}
this.#startPolling(subscriptionId, options.topics, options.onEvent);
}
#startPolling(
subscriptionId: string,
topics: string[],
onEvent: EventsServiceSubscribeOptions['onEvent'],
) {
let backoffMs = POLL_BACKOFF_START_MS;
const poll = async () => {
if (!this.client) {
return;
}
const lock = this.#getShutdownLock();
try {
const token = await this.#getToken();
if (!token) {
return;
}
const res = await this.client.getSubscriptionEvents(
{
path: { subscriptionId },
},
{ token },
);
if (!res.ok) {
if (res.status === 404) {
this.logger.info(
`Polling event subscription resulted in a 404, recreating subscription`,
);
const putRes = await this.client.putSubscription(
{
path: { subscriptionId },
body: { topics },
},
{ token },
);
if (!putRes.ok) {
throw await ResponseError.fromResponse(res);
}
}
throw await ResponseError.fromResponse(res);
}
backoffMs = POLL_BACKOFF_START_MS;
// 202 means there were no immediately available events, but the
// response will block until either new events are available or the
// request times out. In both cases we should should try to read events
// immediately again
if (res.status === 202) {
lock.release();
await res.body?.getReader()?.closed;
process.nextTick(poll);
} else if (res.status === 200) {
const data = await res.json();
if (data) {
for (const event of data.events ?? []) {
try {
await onEvent({
topic: event.topic,
eventPayload: event.payload,
});
} catch (error) {
this.logger.warn(
`Subscriber "${subscriptionId}" failed to process event for topic "${event.topic}"`,
error,
);
}
}
}
process.nextTick(poll);
} else {
this.logger.warn(
`Unexpected response status ${res.status} from events backend for subscription "${subscriptionId}"`,
);
}
} catch (error) {
this.logger.warn(
`Poll failed for subscription "${subscriptionId}", retrying in ${backoffMs.toFixed(
0,
)}ms`,
error,
);
setTimeout(poll, backoffMs);
backoffMs = Math.min(
backoffMs * POLL_BACKOFF_FACTOR,
POLL_BACKOFF_MAX_MS,
);
} finally {
lock.release();
}
};
poll();
}
async #getToken() {
if (!this.auth) {
throw new Error('Auth service not available');
}
try {
const { token } = await this.auth.getPluginRequestToken({
onBehalfOf: await this.auth.getOwnServiceCredentials(),
targetPluginId: 'events',
});
return token;
} catch (error) {
// This is a bit hacky, but handles the case where new auth is used
// without legacy auth fallback, and the events backend is not installed
if (String(error).includes('Unable to generate legacy token')) {
this.logger.warn(
`The events backend is not available and neither is legacy auth. Future events will not be persisted.`,
);
delete this.client;
return undefined;
}
throw error;
}
}
async shutdown() {
this.#isShuttingDown = true;
await Promise.all(this.#shutdownLocks);
}
#isShuttingDown = false;
#shutdownLocks: Promise<void>[] = [];
// This locking mechanism helps ensure that we are either idle or waiting for
// a blocked events call before shutting down. It increases out changes of
// never dropping any events on shutdown.
#getShutdownLock(): { release(): void } {
if (this.#isShuttingDown) {
throw new Error('Service is shutting down');
}
let release: () => void;
this.#shutdownLocks.push(
new Promise<void>(resolve => {
release = resolve;
}),
);
return { release: release! };
}
}
/**
* In-process event broker which will pass the event to all registered subscribers
@@ -28,15 +339,16 @@ import { EventsService, EventsServiceSubscribeOptions } from './EventsService';
*/
// TODO(pjungermann): add opentelemetry? (see plugins/catalog-backend/src/util/opentelemetry.ts, etc.)
export class DefaultEventsService implements EventsService {
private readonly subscribers = new Map<
string,
Omit<EventsServiceSubscribeOptions, 'topics'>[]
>();
private constructor(private readonly logger: LoggerService) {}
private constructor(
private readonly logger: LoggerService,
private readonly localBus: LocalEventBus,
) {}
static create(options: { logger: LoggerService }): DefaultEventsService {
return new DefaultEventsService(options.logger);
return new DefaultEventsService(
options.logger,
new LocalEventBus(options.logger),
);
}
/**
@@ -45,60 +357,40 @@ export class DefaultEventsService implements EventsService {
*
* @param pluginId - The plugin that the `EventService` should be created for.
*/
forPlugin(pluginId: string): EventsService {
return {
publish: (params: EventParams): Promise<void> => {
return this.publish(params);
},
subscribe: (options: EventsServiceSubscribeOptions): Promise<void> => {
return this.subscribe({
...options,
id: `${pluginId}.${options.id}`,
});
},
};
forPlugin(
pluginId: string,
options?: {
discovery: DiscoveryService;
logger: LoggerService;
auth: AuthService;
lifecycle: LifecycleService;
},
): EventsService {
const client =
options &&
new DefaultApiClient({
discoveryApi: options.discovery,
fetchApi: { fetch }, // use native node fetch
});
const logger = options?.logger ?? this.logger;
const service = new PluginEventsService(
pluginId,
this.localBus,
logger,
client,
options?.auth,
);
options?.lifecycle.addShutdownHook(async () => {
await service.shutdown();
});
return service;
}
async publish(params: EventParams): Promise<void> {
this.logger.debug(
`Event received: topic=${params.topic}, metadata=${JSON.stringify(
params.metadata,
)}, payload=${JSON.stringify(params.eventPayload)}`,
);
if (!this.subscribers.has(params.topic)) {
return;
}
const onEventPromises: Promise<void>[] = [];
this.subscribers.get(params.topic)?.forEach(subscription => {
onEventPromises.push(
(async () => {
try {
await subscription.onEvent(params);
} catch (error) {
this.logger.warn(
`Subscriber "${subscription.id}" failed to process event for topic "${params.topic}"`,
error,
);
}
})(),
);
});
await Promise.all(onEventPromises);
await this.localBus.publish(params);
}
async subscribe(options: EventsServiceSubscribeOptions): Promise<void> {
options.topics.forEach(topic => {
if (!this.subscribers.has(topic)) {
this.subscribers.set(topic, []);
}
this.subscribers.get(topic)!.push({
id: options.id,
onEvent: options.onEvent,
});
});
this.localBus.subscribe(options);
}
}
@@ -0,0 +1,152 @@
/*
* Copyright 2024 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.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { DiscoveryApi } from '../types/discovery';
import { FetchApi } from '../types/fetch';
import crossFetch from 'cross-fetch';
import { pluginId } from '../pluginId';
import * as parser from 'uri-template';
import { GetSubscriptionEvents200Response } from '../models/GetSubscriptionEvents200Response.model';
import { PostEventRequest } from '../models/PostEventRequest.model';
import { PutSubscriptionRequest } from '../models/PutSubscriptionRequest.model';
/**
* Wraps the Response type to convey a type on the json call.
*
* @public
*/
export type TypedResponse<T> = Omit<Response, 'json'> & {
json: () => Promise<T>;
};
/**
* Options you can pass into a request for additional information.
*
* @public
*/
export interface RequestOptions {
token?: string;
}
/**
* no description
*/
export class DefaultApiClient {
private readonly discoveryApi: DiscoveryApi;
private readonly fetchApi: FetchApi;
constructor(options: {
discoveryApi: { getBaseUrl(pluginId: string): Promise<string> };
fetchApi?: { fetch: typeof fetch };
}) {
this.discoveryApi = options.discoveryApi;
this.fetchApi = options.fetchApi || { fetch: crossFetch };
}
/**
* Get new events for the provided subscription
* @param subscriptionId
*/
public async getSubscriptionEvents(
// @ts-ignore
request: {
path: {
subscriptionId: string;
};
},
options?: RequestOptions,
): Promise<TypedResponse<void | GetSubscriptionEvents200Response>> {
const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
const uriTemplate = `/bus/v1/subscriptions/{subscriptionId}/events`;
const uri = parser.parse(uriTemplate).expand({
subscriptionId: request.path.subscriptionId,
});
return await this.fetchApi.fetch(`${baseUrl}${uri}`, {
headers: {
'Content-Type': 'application/json',
...(options?.token && { Authorization: `Bearer ${options?.token}` }),
},
method: 'GET',
});
}
/**
* Publish a new event
* @param postEventRequest
*/
public async postEvent(
// @ts-ignore
request: {
body: PostEventRequest;
},
options?: RequestOptions,
): Promise<TypedResponse<void>> {
const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
const uriTemplate = `/bus/v1/events`;
const uri = parser.parse(uriTemplate).expand({});
return await this.fetchApi.fetch(`${baseUrl}${uri}`, {
headers: {
'Content-Type': 'application/json',
...(options?.token && { Authorization: `Bearer ${options?.token}` }),
},
method: 'POST',
body: JSON.stringify(request.body),
});
}
/**
* Ensures that the subscription exists with the provided configuration
* @param subscriptionId
* @param putSubscriptionRequest
*/
public async putSubscription(
// @ts-ignore
request: {
path: {
subscriptionId: string;
};
body: PutSubscriptionRequest;
},
options?: RequestOptions,
): Promise<TypedResponse<void>> {
const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
const uriTemplate = `/bus/v1/subscriptions/{subscriptionId}`;
const uri = parser.parse(uriTemplate).expand({
subscriptionId: request.path.subscriptionId,
});
return await this.fetchApi.fetch(`${baseUrl}${uri}`, {
headers: {
'Content-Type': 'application/json',
...(options?.token && { Authorization: `Bearer ${options?.token}` }),
},
method: 'PUT',
body: JSON.stringify(request.body),
});
}
}
@@ -0,0 +1,17 @@
/*
* Copyright 2024 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 * from './DefaultApi.client';
@@ -0,0 +1,18 @@
/*
* Copyright 2024 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 * from './apis';
export * from './models';
@@ -0,0 +1,24 @@
/*
* Copyright 2024 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.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
export interface ErrorError {
name: string;
message: string;
}
@@ -0,0 +1,24 @@
/*
* Copyright 2024 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.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
export interface ErrorRequest {
method: string;
url: string;
}
@@ -0,0 +1,23 @@
/*
* Copyright 2024 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.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
export interface ErrorResponse {
statusCode: number;
}
@@ -0,0 +1,30 @@
/*
* Copyright 2024 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.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
export interface Event {
/**
* The topic that the event is published on
*/
topic: string;
/**
* The event payload
*/
payload: any | null;
}
@@ -0,0 +1,24 @@
/*
* Copyright 2024 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.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { Event } from '../models/Event.model';
export interface GetSubscriptionEvents200Response {
events: Array<Event>;
}
@@ -0,0 +1,28 @@
/*
* Copyright 2024 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.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { ErrorError } from '../models/ErrorError.model';
import { ErrorRequest } from '../models/ErrorRequest.model';
import { ErrorResponse } from '../models/ErrorResponse.model';
export interface ModelError {
error: ErrorError;
request: ErrorRequest;
response: ErrorResponse;
}
@@ -0,0 +1,28 @@
/*
* Copyright 2024 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.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { Event } from '../models/Event.model';
export interface PostEventRequest {
event: Event;
/**
* The IDs of subscriptions that have already received this event
*/
notifiedSubscribers?: Array<string>;
}
@@ -0,0 +1,26 @@
/*
* Copyright 2024 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.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
export interface PutSubscriptionRequest {
/**
* The topics to subscribe to
*/
topics: Array<string>;
}
@@ -0,0 +1,24 @@
/*
* Copyright 2024 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 * from '../models/ErrorError.model';
export * from '../models/ErrorRequest.model';
export * from '../models/ErrorResponse.model';
export * from '../models/Event.model';
export * from '../models/GetSubscriptionEvents200Response.model';
export * from '../models/ModelError.model';
export * from '../models/PostEventRequest.model';
export * from '../models/PutSubscriptionRequest.model';
@@ -0,0 +1,17 @@
/*
* Copyright 2024 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 const pluginId = 'events';
@@ -0,0 +1,22 @@
/*
* 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.
*/
/**
* This is a copy of the DiscoveryApi, to avoid importing core-plugin-api.
*/
export type DiscoveryApi = {
getBaseUrl(pluginId: string): Promise<string>;
};
@@ -0,0 +1,22 @@
/*
* 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.
*/
/**
* This is a copy of FetchApi, to avoid importing core-plugin-api.
*/
export type FetchApi = {
fetch: typeof fetch;
};
+14 -2
View File
@@ -38,11 +38,23 @@ export const eventsServiceFactory = createServiceFactory({
deps: {
pluginMetadata: coreServices.pluginMetadata,
rootLogger: coreServices.rootLogger,
discovery: coreServices.discovery,
logger: coreServices.logger,
lifecycle: coreServices.lifecycle,
auth: coreServices.auth,
},
async createRootContext({ rootLogger }) {
return DefaultEventsService.create({ logger: rootLogger });
},
async factory({ pluginMetadata }, eventsService) {
return eventsService.forPlugin(pluginMetadata.getId());
async factory(
{ pluginMetadata, discovery, logger, lifecycle, auth },
eventsService,
) {
return eventsService.forPlugin(pluginMetadata.getId(), {
discovery,
logger,
lifecycle,
auth,
});
},
});