Merge branch 'master' of https://github.com/backstage/backstage into migrate-google-pubsub-alpha-metrics

This commit is contained in:
Kurt King
2026-04-04 13:33:39 -06:00
81 changed files with 1222 additions and 1851 deletions
@@ -63,6 +63,7 @@
"@octokit/auth-callback": "^5.0.0",
"@octokit/core": "^5.2.0",
"@octokit/graphql": "^7.0.2",
"@octokit/plugin-retry": "^6.0.0",
"@octokit/plugin-throttling": "^8.1.3",
"@octokit/rest": "^19.0.3",
"@octokit/webhooks-types": "^7.6.1",
@@ -41,6 +41,7 @@ import {
} from './github';
import { Octokit } from '@octokit/core';
import { throttling } from '@octokit/plugin-throttling';
import { retry } from '@octokit/plugin-retry';
jest.mock('@octokit/core', () => ({
...jest.requireActual('@octokit/core'),
@@ -1009,9 +1010,9 @@ describe('github', () => {
baseUrl,
logger,
});
it('should return a graphql client with throttling', async () => {
it('should return a graphql client with throttling and retry', async () => {
expect(client).toBeDefined();
expect(Octokit.plugin).toHaveBeenCalledWith(throttling);
expect(Octokit.plugin).toHaveBeenCalledWith(throttling, retry);
});
it('should return a graphql client with the correct options', async () => {
@@ -30,6 +30,7 @@ import { DeferredEntity } from '@backstage/plugin-catalog-node';
import { Octokit } from '@octokit/core';
import { LoggerService } from '@backstage/backend-plugin-api';
import { throttling } from '@octokit/plugin-throttling';
import { retry } from '@octokit/plugin-retry';
/**
* Configuration for GitHub GraphQL API page sizes.
@@ -874,7 +875,7 @@ export const createReplaceEntitiesOperation =
};
/**
* Creates a GraphQL Client with Throttling
* Creates a GraphQL Client with Throttling and Retries
*/
export const createGraphqlClient = (args: {
headers:
@@ -886,7 +887,7 @@ export const createGraphqlClient = (args: {
logger: LoggerService;
}): typeof graphql => {
const { headers, baseUrl, logger } = args;
const ThrottledOctokit = Octokit.plugin(throttling);
const ThrottledOctokit = Octokit.plugin(throttling, retry);
const octokit = new ThrottledOctokit({
throttle: {
onRateLimit: (retryAfter, rateLimitData, _, retryCount) => {
@@ -47,6 +47,7 @@ type PartialDeep<T> = T extends (...args: unknown[]) => unknown
jest.mock('../lib/github', () => {
return {
getOrganizationRepositories: jest.fn(),
createGraphqlClient: jest.fn().mockReturnValue(jest.fn()),
};
});
class PersistingTaskRunner implements SchedulerServiceTaskRunner {
@@ -32,13 +32,13 @@ import {
import { LocationSpec } from '@backstage/plugin-catalog-common';
import { graphql } from '@octokit/graphql';
import * as uuid from 'uuid';
import {
GithubEntityProviderConfig,
readProviderConfigs,
} from './GithubEntityProviderConfig';
import {
createGraphqlClient,
getOrganizationRepositories,
getOrganizationRepository,
RepositoryResponse,
@@ -249,9 +249,10 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber {
url: orgUrl,
});
return graphql.defaults({
baseUrl: this.integration.apiBaseUrl,
return createGraphqlClient({
headers,
baseUrl: this.integration.apiBaseUrl!,
logger: this.logger,
});
}
@@ -43,10 +43,7 @@ import {
UrlReaderService,
} from '@backstage/backend-plugin-api';
import { Config, readDurationFromConfig } from '@backstage/config';
import {
catalogPermissions,
RESOURCE_TYPE_CATALOG_ENTITY,
} from '@backstage/plugin-catalog-common/alpha';
import { catalogPermissions } from '@backstage/plugin-catalog-common/alpha';
import {
CatalogProcessor,
CatalogProcessorParser,
@@ -55,15 +52,7 @@ import {
ScmLocationAnalyzer,
} from '@backstage/plugin-catalog-node';
import { EventsService } from '@backstage/plugin-events-node';
import {
Permission,
PermissionAuthorizer,
toPermissionEvaluator,
} from '@backstage/plugin-permission-common';
import {
createConditionTransformer,
createPermissionIntegrationRouter,
} from '@backstage/plugin-permission-node';
import { createConditionTransformer } from '@backstage/plugin-permission-node';
import { durationToMilliseconds } from '@backstage/types';
import { DefaultCatalogDatabase } from '../database/DefaultCatalogDatabase';
import { DefaultProcessingDatabase } from '../database/DefaultProcessingDatabase';
@@ -111,7 +100,6 @@ import { DefaultRefreshService } from './DefaultRefreshService';
import { entitiesResponseToObjects } from './response';
import {
catalogEntityPermissionResourceRef,
CatalogPermissionRuleInput,
CatalogScmEventsService,
} from '@backstage/plugin-catalog-node/alpha';
import { filterAndSortProcessors, filterProviders } from './util';
@@ -124,8 +112,8 @@ export type CatalogEnvironment = {
database: DatabaseService;
config: RootConfigService;
reader: UrlReaderService;
permissions: PermissionsService | PermissionAuthorizer;
permissionsRegistry?: PermissionsRegistryService;
permissions: PermissionsService;
permissionsRegistry: PermissionsRegistryService;
scheduler: SchedulerService;
auth: AuthService;
httpAuth: HttpAuthService;
@@ -177,8 +165,6 @@ export class CatalogBuilder {
}) => Promise<void> | void;
private processingInterval: ProcessingIntervalFunction;
private locationAnalyzer: LocationAnalyzer | undefined = undefined;
private readonly permissions: Permission[];
private readonly permissionRules: CatalogPermissionRuleInput[];
private allowedLocationType: string[];
/**
@@ -199,8 +185,6 @@ export class CatalogBuilder {
this.locationAnalyzers = [];
this.processorsReplace = false;
this.parser = undefined;
this.permissions = [...catalogPermissions];
this.permissionRules = Object.values(catalogPermissionRules);
this.allowedLocationType = ['url'];
this.processingInterval = CatalogBuilder.getDefaultProcessingInterval(
@@ -375,33 +359,6 @@ export class CatalogBuilder {
return this;
}
/**
* Adds additional permissions. See
* {@link @backstage/plugin-permission-node#Permission}.
*
* @param permissions - Additional permissions
*/
addPermissions(...permissions: Array<Permission | Array<Permission>>) {
this.permissions.push(...permissions.flat());
return this;
}
/**
* Adds additional permission rules. Permission rules are used to evaluate
* catalog resources against queries. See
* {@link @backstage/plugin-permission-node#PermissionRule}.
*
* @param permissionRules - Additional permission rules
*/
addPermissionRules(
...permissionRules: Array<
CatalogPermissionRuleInput | Array<CatalogPermissionRuleInput>
>
) {
this.permissionRules.push(...permissionRules.flat());
return this;
}
/**
* Sets up the allowed location types from being registered via the location service.
*
@@ -479,16 +436,6 @@ export class CatalogBuilder {
enableRelationsCompatibility,
});
let permissionsService: PermissionsService;
if ('authorizeConditional' in permissions) {
permissionsService = permissions as PermissionsService;
} else {
logger.warn(
'PermissionAuthorizer is deprecated. Please use an instance of PermissionEvaluator instead of PermissionAuthorizer in PluginEnvironment#permissions',
);
permissionsService = toPermissionEvaluator(permissions);
}
const orchestrator = new DefaultCatalogProcessingOrchestrator({
processors,
integrations,
@@ -500,14 +447,12 @@ export class CatalogBuilder {
const entitiesCatalog = new AuthorizedEntitiesCatalog(
unauthorizedEntitiesCatalog,
permissionsService,
permissionsRegistry
? createConditionTransformer(
permissionsRegistry.getPermissionRuleset(
catalogEntityPermissionResourceRef,
),
)
: createConditionTransformer(this.permissionRules),
permissions,
createConditionTransformer(
permissionsRegistry.getPermissionRuleset(
catalogEntityPermissionResourceRef,
),
),
);
const getResources = async (resourceRefs: string[]) => {
@@ -519,24 +464,12 @@ export class CatalogBuilder {
return entitiesResponseToObjects(items).map(e => e || undefined);
};
let permissionIntegrationRouter:
| ReturnType<typeof createPermissionIntegrationRouter>
| undefined;
if (permissionsRegistry) {
permissionsRegistry.addResourceType({
resourceRef: catalogEntityPermissionResourceRef,
getResources,
permissions: this.permissions,
rules: this.permissionRules,
});
} else {
permissionIntegrationRouter = createPermissionIntegrationRouter({
resourceType: RESOURCE_TYPE_CATALOG_ENTITY,
getResources,
permissions: this.permissions,
rules: this.permissionRules,
});
}
permissionsRegistry.addResourceType({
resourceRef: catalogEntityPermissionResourceRef,
getResources,
permissions: [...catalogPermissions],
rules: Object.values(catalogPermissionRules),
});
const scmEventHandlingConfig = readScmEventHandlingConfig(config);
const locationStore = new DefaultLocationStore(
@@ -589,7 +522,7 @@ export class CatalogBuilder {
this.locationAnalyzer ??
new AuthorizedLocationAnalyzer(
new RepoLocationAnalyzer(logger, integrations, this.locationAnalyzers),
permissionsService,
permissions,
);
const locationService = new AuthorizedLocationService(
new DefaultLocationService(locationStore, orchestrator, {
@@ -599,11 +532,11 @@ export class CatalogBuilder {
'catalog.defaultLocationConflictStrategy',
) as 'refresh' | 'reject') || 'reject',
}),
permissionsService,
permissions,
);
const refreshService = new AuthorizedRefreshService(
new DefaultRefreshService({ database: catalogDatabase }),
permissionsService,
permissions,
);
const router = await createRouter({
@@ -614,10 +547,9 @@ export class CatalogBuilder {
refreshService,
logger,
config,
permissionIntegrationRouter,
auth,
httpAuth,
permissionsService,
permissionsService: permissions,
auditor,
enableRelationsCompatibility,
});
@@ -36,13 +36,9 @@ import {
import {
CatalogModelExtensionPoint,
catalogModelExtensionPoint,
CatalogPermissionExtensionPoint,
catalogPermissionExtensionPoint,
CatalogPermissionRuleInput,
catalogScmEventsServiceRef,
} from '@backstage/plugin-catalog-node/alpha';
import { eventsServiceRef } from '@backstage/plugin-events-node';
import { Permission } from '@backstage/plugin-permission-common';
import { merge } from 'lodash';
import { CatalogBuilder } from './CatalogBuilder';
import {
@@ -66,33 +62,6 @@ class CatalogLocationsExtensionPointImpl
}
}
class CatalogPermissionExtensionPointImpl
implements CatalogPermissionExtensionPoint
{
#permissions = new Array<Permission>();
#permissionRules = new Array<CatalogPermissionRuleInput>();
addPermissions(...permission: Array<Permission | Array<Permission>>): void {
this.#permissions.push(...permission.flat());
}
addPermissionRules(
...rules: Array<
CatalogPermissionRuleInput | Array<CatalogPermissionRuleInput>
>
): void {
this.#permissionRules.push(...rules.flat());
}
get permissions() {
return this.#permissions;
}
get permissionRules() {
return this.#permissionRules;
}
}
class CatalogModelExtensionPointImpl implements CatalogModelExtensionPoint {
#fieldValidators: Partial<Validators> = {};
@@ -189,12 +158,6 @@ export const catalogPlugin = createBackendPlugin({
},
});
const permissionExtensions = new CatalogPermissionExtensionPointImpl();
env.registerExtensionPoint(
catalogPermissionExtensionPoint,
permissionExtensions,
);
const modelExtensions = new CatalogModelExtensionPointImpl();
env.registerExtensionPoint(catalogModelExtensionPoint, modelExtensions);
@@ -282,8 +245,6 @@ export const catalogPlugin = createBackendPlugin({
} else {
builder.addLocationAnalyzers(...scmLocationAnalyzers);
}
builder.addPermissions(...permissionExtensions.permissions);
builder.addPermissionRules(...permissionExtensions.permissionRules);
builder.setFieldFormatValidators(modelExtensions.fieldValidators);
if (locationTypeExtensions.allowedLocationTypes) {
@@ -31,17 +31,10 @@ import {
} from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import { NotFoundError } from '@backstage/errors';
import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common/alpha';
import { LocationAnalyzer } from '@backstage/plugin-catalog-node';
import { AuthorizeResult } from '@backstage/plugin-permission-common';
import {
createPermissionIntegrationRouter,
createPermissionRule,
} from '@backstage/plugin-permission-node';
import express from 'express';
import { Server } from 'node:http';
import request from 'supertest';
import { z } from 'zod/v3';
import { Cursor, EntitiesCatalog } from '../catalog/types';
import { applyDatabaseMigrations } from '../database/migrations';
import { DbLocationsRow } from '../database/tables';
@@ -98,7 +91,6 @@ describe('createRouter readonly disabled', () => {
logger: mockServices.logger.mock(),
refreshService,
config: new ConfigReader(undefined),
permissionIntegrationRouter: express.Router(),
auth: mockServices.auth(),
httpAuth: mockServices.httpAuth(),
locationAnalyzer,
@@ -1386,7 +1378,6 @@ describe('createRouter readonly and raw json enabled', () => {
readonly: true,
},
}),
permissionIntegrationRouter: express.Router(),
auth: mockServices.auth(),
httpAuth: mockServices.httpAuth(),
orchestrator: { process: jest.fn() },
@@ -1558,110 +1549,6 @@ describe('createRouter readonly and raw json enabled', () => {
});
});
describe('NextRouter permissioning', () => {
let entitiesCatalog: jest.Mocked<EntitiesCatalog>;
let locationService: jest.Mocked<LocationService>;
let app: express.Express;
let refreshService: RefreshService;
const permissionsService = mockServices.permissions();
const fakeRule = createPermissionRule({
name: 'FAKE_RULE',
description: 'fake rule',
resourceType: RESOURCE_TYPE_CATALOG_ENTITY,
paramsSchema: z.object({
foo: z.string(),
}),
apply: () => true,
toQuery: () => ({ key: '', values: [] }),
});
beforeAll(async () => {
entitiesCatalog = {
entities: jest.fn(),
entitiesBatch: jest.fn(),
removeEntityByUid: jest.fn(),
entityAncestry: jest.fn(),
facets: jest.fn(),
queryEntities: jest.fn(),
};
locationService = {
getLocation: jest.fn(),
createLocation: jest.fn(),
queryLocations: jest.fn(),
listLocations: jest.fn(),
deleteLocation: jest.fn(),
getLocationByEntity: jest.fn(),
};
refreshService = { refresh: jest.fn() };
const router = await createRouter({
entitiesCatalog,
locationService,
logger: mockServices.logger.mock(),
refreshService,
config: new ConfigReader(undefined),
permissionIntegrationRouter: createPermissionIntegrationRouter({
resourceType: RESOURCE_TYPE_CATALOG_ENTITY,
rules: [fakeRule],
getResources: jest.fn((resourceRefs: string[]) =>
Promise.resolve(
resourceRefs.map(resourceRef => ({ id: resourceRef })),
),
),
}),
auth: mockServices.auth(),
httpAuth: mockServices.httpAuth(),
orchestrator: { process: jest.fn() },
permissionsService,
auditor: mockServices.auditor.mock(),
});
app = express().use(router);
});
afterEach(() => {
jest.clearAllMocks();
});
it('accepts and evaluates conditions at the apply-conditions endpoint', async () => {
const spideySense: Entity = {
apiVersion: 'a',
kind: 'component',
metadata: {
name: 'spidey-sense',
},
};
entitiesCatalog.entities.mockResolvedValueOnce({
entities: { type: 'object', entities: [spideySense] },
pageInfo: { hasNextPage: false },
});
const requestBody = {
items: [
{
id: '123',
resourceType: 'catalog-entity',
resourceRef: 'component:default/spidey-sense',
conditions: {
rule: 'FAKE_RULE',
resourceType: 'catalog-entity',
params: {
foo: 'user:default/spiderman',
},
},
},
],
};
const response = await request(app)
.post('/.well-known/backstage/permissions/apply-conditions')
.send(requestBody);
expect(response.status).toBe(200);
expect(response.body).toEqual({
items: [{ id: '123', result: AuthorizeResult.ALLOW }],
});
});
});
describe('POST /locations/by-query works end to end', () => {
const databases = TestDatabases.create();
@@ -79,7 +79,6 @@ export interface RouterOptions {
refreshService?: RefreshService;
logger: LoggerService;
config: Config;
permissionIntegrationRouter?: express.Router;
auth: AuthService;
httpAuth: HttpAuthService;
permissionsService: PermissionsService;
@@ -108,7 +107,6 @@ export async function createRouter(
refreshService,
config,
logger,
permissionIntegrationRouter,
permissionsService,
auth,
httpAuth,
@@ -156,10 +154,6 @@ export async function createRouter(
});
}
if (permissionIntegrationRouter) {
router.use(permissionIntegrationRouter);
}
if (entitiesCatalog) {
router
.get('/entities', async (req, res) => {
+2
View File
@@ -165,6 +165,7 @@ See below the complete list of available configs:
| `maxDepth` | A maximum number of levels of relations to display in the graph. | `number` | yes | `1` |
| `unidirectional` | Shows only relations that are from the source to the target entity. | `boolean` | yes | `true` |
| `mergeRelations` | Merge the relations line into a single one. | `boolean` | yes | `true` |
| `showArrowHeads` | Show arrowheads on the relation lines | `boolean` | yes | `false` |
| `direction` | Render direction of the graph. | `TB` \| `BT` \| `LR` \| `RL` | yes | `'LR'` |
| `relationPairs` | A list of [pairs of entity relations](https://backstage.io/docs/features/software-catalog/well-known-relations#relations), used to define which relations are merged together and which the primary relation is. | `[string[], string[]]` | yes | Show all entity [relations](https://backstage.io/docs/features/software-catalog/well-known-relations#relations). |
| `zoom` | Controls zoom behavior of graph. | `enabled` \| `disabled` \| `enable-on-click` | yes | `'enabled'` |
@@ -265,6 +266,7 @@ See below the complete list of available configs:
| `maxDepth` | A maximum number of levels of relations to display in the graph. | `number` | yes | `1` |
| `unidirectional` | Shows only relations that are from the source to the target entity. | `boolean` | yes | `true` |
| `mergeRelations` | Merge the relations line into a single one. | `boolean` | yes | `true` |
| `showArrowHeads` | Show arrowheads on the relation lines | `boolean` | yes | `false` |
| `direction` | Render direction of the graph. | `TB` \| `BT` \| `LR` \| `RL` | yes | `'LR'` |
| `relationPairs` | A list of [pairs of entity relations](https://backstage.io/docs/features/software-catalog/well-known-relations#relations), used to define which relations are merged together and which the primary relation is. | `[string[], string[]]` | yes | Show all entity [relations](https://backstage.io/docs/features/software-catalog/well-known-relations#relations). |
| `zoom` | Controls zoom behavior of graph. | `enabled` \| `disabled` \| `enable-on-click` | yes | `'enabled'` |
@@ -88,6 +88,7 @@ const _default: OverridableFrontendPlugin<
maxDepth: number | undefined;
unidirectional: boolean | undefined;
mergeRelations: boolean | undefined;
showArrowHeads: boolean | undefined;
direction: 'TB' | 'BT' | 'LR' | 'RL' | undefined;
relationPairs: [string, string][] | undefined;
zoom: 'disabled' | 'enabled' | 'enable-on-click' | undefined;
@@ -103,6 +104,7 @@ const _default: OverridableFrontendPlugin<
direction?: 'TB' | 'BT' | 'LR' | 'RL' | undefined;
zoom?: 'disabled' | 'enabled' | 'enable-on-click' | undefined;
title?: string | undefined;
showArrowHeads?: boolean | undefined;
relations?: string[] | undefined;
maxDepth?: number | undefined;
kinds?: string[] | undefined;
@@ -152,6 +154,7 @@ const _default: OverridableFrontendPlugin<
maxDepth: number | undefined;
unidirectional: boolean | undefined;
mergeRelations: boolean | undefined;
showArrowHeads: boolean | undefined;
direction: 'TB' | 'BT' | 'LR' | 'RL' | undefined;
showFilters: boolean | undefined;
curve: 'curveStepBefore' | 'curveMonotoneX' | undefined;
@@ -166,6 +169,7 @@ const _default: OverridableFrontendPlugin<
curve?: 'curveStepBefore' | 'curveMonotoneX' | undefined;
direction?: 'TB' | 'BT' | 'LR' | 'RL' | undefined;
zoom?: 'disabled' | 'enabled' | 'enable-on-click' | undefined;
showArrowHeads?: boolean | undefined;
relations?: string[] | undefined;
maxDepth?: number | undefined;
rootEntityRefs?: string[] | undefined;
+2
View File
@@ -37,6 +37,7 @@ const CatalogGraphEntityCard = EntityCardBlueprint.makeWithOverrides({
maxDepth: z => z.number().optional(),
unidirectional: z => z.boolean().optional(),
mergeRelations: z => z.boolean().optional(),
showArrowHeads: z => z.boolean().optional(),
direction: z => z.nativeEnum(Direction).optional(),
relationPairs: z => z.array(z.tuple([z.string(), z.string()])).optional(),
zoom: z => z.enum(['enabled', 'disabled', 'enable-on-click']).optional(),
@@ -66,6 +67,7 @@ const CatalogGraphPage = PageBlueprint.makeWithOverrides({
maxDepth: z => z.number().optional(),
unidirectional: z => z.boolean().optional(),
mergeRelations: z => z.boolean().optional(),
showArrowHeads: z => z.boolean().optional(),
direction: z => z.nativeEnum(Direction).optional(),
showFilters: z => z.boolean().optional(),
curve: z => z.enum(['curveStepBefore', 'curveMonotoneX']).optional(),
@@ -68,6 +68,7 @@ export const CatalogGraphCard = (
maxDepth = 1,
unidirectional = true,
mergeRelations = true,
showArrowHeads,
direction = Direction.LEFT_RIGHT,
kinds,
relations,
@@ -147,6 +148,7 @@ export const CatalogGraphCard = (
relationPairs={relationPairs}
entityFilter={entityFilter}
zoom={zoom}
showArrowHeads={showArrowHeads}
/>
</EntityInfoCard>
);
@@ -131,7 +131,7 @@ export const CatalogGraphPage = (
};
} & Partial<EntityRelationsGraphProps>,
) => {
const { relationPairs, initialState, entityFilter } = props;
const { relationPairs, initialState, entityFilter, showArrowHeads } = props;
const { t } = useTranslationRef(catalogGraphTranslationRef);
const navigate = useNavigate();
const classes = useStyles();
@@ -260,6 +260,7 @@ export const CatalogGraphPage = (
}
mergeRelations={mergeRelations}
unidirectional={unidirectional}
showArrowHeads={showArrowHeads}
onNodeClick={onNodeClick}
direction={direction}
relationPairs={relationPairs}
-23
View File
@@ -11,10 +11,7 @@ import { CatalogProcessorParser } from '@backstage/plugin-catalog-node';
import { EntitiesSearchFilter } from '@backstage/plugin-catalog-node';
import { Entity } from '@backstage/catalog-model';
import { ExtensionPoint } from '@backstage/backend-plugin-api';
import { Permission } from '@backstage/plugin-permission-common';
import { PermissionResourceRef } from '@backstage/plugin-permission-node';
import { PermissionRule } from '@backstage/plugin-permission-node';
import { PermissionRuleParams } from '@backstage/plugin-permission-common';
import { ServiceRef } from '@backstage/backend-plugin-api';
import { Validators } from '@backstage/catalog-model';
@@ -47,26 +44,6 @@ export interface CatalogModelExtensionPoint {
// @alpha (undocumented)
export const catalogModelExtensionPoint: ExtensionPoint<CatalogModelExtensionPoint>;
// @alpha @deprecated (undocumented)
export interface CatalogPermissionExtensionPoint {
// (undocumented)
addPermissionRules(
...rules: Array<
CatalogPermissionRuleInput | Array<CatalogPermissionRuleInput>
>
): void;
// (undocumented)
addPermissions(...permissions: Array<Permission | Array<Permission>>): void;
}
// @alpha @deprecated (undocumented)
export const catalogPermissionExtensionPoint: ExtensionPoint<CatalogPermissionExtensionPoint>;
// @alpha @deprecated (undocumented)
export type CatalogPermissionRuleInput<
TParams extends PermissionRuleParams = PermissionRuleParams,
> = PermissionRule<Entity, EntitiesSearchFilter, 'catalog-entity', TParams>;
// @alpha @deprecated (undocumented)
export type CatalogProcessingExtensionPoint = CatalogProcessingExtensionPoint_2;
-3
View File
@@ -100,8 +100,5 @@ export const catalogAnalysisExtensionPoint = _catalogAnalysisExtensionPoint;
export type { CatalogModelExtensionPoint } from './extensions';
export { catalogModelExtensionPoint } from './extensions';
export type { CatalogPermissionRuleInput } from './extensions';
export type { CatalogPermissionExtensionPoint } from './extensions';
export { catalogPermissionExtensionPoint } from './extensions';
export * from './scmEvents';
-36
View File
@@ -19,17 +19,11 @@ import { Entity, Validators } from '@backstage/catalog-model';
import {
CatalogProcessor,
CatalogProcessorParser,
EntitiesSearchFilter,
EntityProvider,
PlaceholderResolver,
LocationAnalyzer,
ScmLocationAnalyzer,
} from '@backstage/plugin-catalog-node';
import {
Permission,
PermissionRuleParams,
} from '@backstage/plugin-permission-common';
import { PermissionRule } from '@backstage/plugin-permission-node';
/**
* @public
@@ -163,33 +157,3 @@ export const catalogModelExtensionPoint =
createExtensionPoint<CatalogModelExtensionPoint>({
id: 'catalog.model',
});
/**
* @alpha
* @deprecated Use the `coreServices.permissionsRegistry` instead.
*/
export type CatalogPermissionRuleInput<
TParams extends PermissionRuleParams = PermissionRuleParams,
> = PermissionRule<Entity, EntitiesSearchFilter, 'catalog-entity', TParams>;
/**
* @alpha
* @deprecated Use the `coreServices.permissionsRegistry` instead.
*/
export interface CatalogPermissionExtensionPoint {
addPermissions(...permissions: Array<Permission | Array<Permission>>): void;
addPermissionRules(
...rules: Array<
CatalogPermissionRuleInput | Array<CatalogPermissionRuleInput>
>
): void;
}
/**
* @alpha
* @deprecated Use the `coreServices.permissionsRegistry` instead.
*/
export const catalogPermissionExtensionPoint =
createExtensionPoint<CatalogPermissionExtensionPoint>({
id: 'catalog.permission',
});
+2 -2
View File
@@ -5651,7 +5651,7 @@
- `step`: The name of the step that was run
- `result`: A string describing whether the task ran successfully, failed, or was skipped
You can find a guide for running Prometheus metrics here: https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/prometheus-metrics.md
You can find a guide for running Prometheus metrics here: https://github.com/backstage/backstage/blob/384b7bac2e/contrib/docs/tutorials/prometheus-metrics.md
- 5921b5ce49: - The GitLab Project ID for the `publish:gitlab:merge-request` action is now passed through the query parameter `project` in the `repoUrl`. It still allows people to not use the `projectid` and use the `repoUrl` with the `owner` and `repo` query parameters instead. This makes it easier to publish to repositories instead of writing the full path to the project.
- 5025d2e8b6: Adds the ability to pass (an optional) array of strings that will be applied to the newly scaffolded repository as topic labels.
@@ -5744,7 +5744,7 @@
- `step`: The name of the step that was run
- `result`: A string describing whether the task ran successfully, failed, or was skipped
You can find a guide for running Prometheus metrics here: https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/prometheus-metrics.md
You can find a guide for running Prometheus metrics here: https://github.com/backstage/backstage/blob/384b7bac2e/contrib/docs/tutorials/prometheus-metrics.md
### Patch Changes
-1
View File
@@ -78,7 +78,6 @@
"@backstage/plugin-scaffolder-common": "workspace:^",
"@backstage/plugin-scaffolder-node": "workspace:^",
"@backstage/types": "workspace:^",
"@opentelemetry/api": "^1.9.0",
"@types/luxon": "^3.0.0",
"express": "^4.22.0",
"fs-extra": "^11.2.0",
@@ -63,6 +63,7 @@ import {
import {
actionsServiceRef,
actionsRegistryServiceRef,
metricsServiceRef,
} from '@backstage/backend-plugin-api/alpha';
import { createScaffolderActions } from './actions';
@@ -151,6 +152,7 @@ export const scaffolderPlugin = createBackendPlugin({
actionsRegistry: actionsServiceRef,
actionsRegistryService: actionsRegistryServiceRef,
scaffolderService: scaffolderServiceRef,
metrics: metricsServiceRef,
},
async init({
logger,
@@ -168,6 +170,7 @@ export const scaffolderPlugin = createBackendPlugin({
actionsRegistry,
actionsRegistryService,
scaffolderService,
metrics,
}) {
const log = loggerToWinstonLogger(logger);
const integrations = ScmIntegrations.fromConfig(config);
@@ -244,6 +247,7 @@ export const scaffolderPlugin = createBackendPlugin({
events,
auditor,
actionsRegistry,
metrics,
});
httpRouter.use(router);
},
@@ -19,6 +19,7 @@ import {
BackstageCredentials,
LoggerService,
} from '@backstage/backend-plugin-api';
import type { MetricsService } from '@backstage/backend-plugin-api/alpha';
import type { UserEntity } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { ScmIntegrations } from '@backstage/integration';
@@ -81,6 +82,7 @@ export type TemplateTesterCreateOptions = {
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
permissions?: PermissionEvaluator;
config?: Config;
metrics: MetricsService;
};
/**
@@ -39,7 +39,10 @@ import {
mockCredentials,
mockServices,
} from '@backstage/backend-test-utils';
import { actionsRegistryServiceMock } from '@backstage/backend-test-utils/alpha';
import {
actionsRegistryServiceMock,
metricsServiceMock,
} from '@backstage/backend-test-utils/alpha';
describe('NunjucksWorkflowRunner', () => {
let actionRegistry: TemplateActionRegistry;
@@ -249,6 +252,7 @@ describe('NunjucksWorkflowRunner', () => {
logger,
permissions: mockedPermissionApi,
config,
metrics: metricsServiceMock.mock(),
});
});
@@ -23,7 +23,6 @@ import {
TaskStep,
} from '@backstage/plugin-scaffolder-common';
import { JsonArray, JsonObject, JsonValue } from '@backstage/types';
import { metrics } from '@opentelemetry/api';
import fs from 'fs-extra';
import { validate as validateJsonSchema } from 'jsonschema';
import nunjucks from 'nunjucks';
@@ -42,6 +41,7 @@ import type {
LoggerService,
PermissionsService,
} from '@backstage/backend-plugin-api';
import type { MetricsService } from '@backstage/backend-plugin-api/alpha';
import { UserEntity } from '@backstage/catalog-model';
import {
AuthorizeResult,
@@ -78,6 +78,7 @@ type NunjucksWorkflowRunnerOptions = {
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
permissions?: PermissionsService;
config?: Config;
metrics: MetricsService;
};
type TemplateContext = {
@@ -188,6 +189,8 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
secrets?: Record<string, string>;
} = { parameters: {}, secrets: {} };
private readonly tracker: ReturnType<typeof scaffoldingTracker>;
constructor(options: NunjucksWorkflowRunnerOptions) {
this.options = options;
this.defaultTemplateFilters = convertFiltersToRecord(
@@ -195,10 +198,9 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
integrations: this.options.integrations,
}),
);
this.tracker = scaffoldingTracker(options.metrics);
}
private readonly tracker = scaffoldingTracker();
async getEnvironmentConfig(): Promise<{
parameters: JsonObject;
secrets?: TaskSecrets;
@@ -700,7 +702,7 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
}
}
function scaffoldingTracker() {
function scaffoldingTracker(metrics: MetricsService) {
// prom-client metrics are deprecated in favour of OpenTelemetry metrics.
const promTaskCount = createCounterMetric({
name: 'scaffolder_task_count',
@@ -723,23 +725,22 @@ function scaffoldingTracker() {
labelNames: ['template', 'step', 'result'],
});
const meter = metrics.getMeter('default');
const taskCount = meter.createCounter('scaffolder.task.count', {
description: 'Count of task runs',
const taskCount = metrics.createCounter('scaffolder.task.count', {
description: 'Total number of scaffolder tasks executed',
});
const taskDuration = meter.createHistogram('scaffolder.task.duration', {
description: 'Duration of a task run',
unit: 'seconds',
const taskDuration = metrics.createHistogram('scaffolder.task.duration', {
description: 'Time taken to complete a scaffolder task end-to-end',
unit: 's',
});
const stepCount = meter.createCounter('scaffolder.step.count', {
description: 'Count of step runs',
const stepCount = metrics.createCounter('scaffolder.step.count', {
description: 'Total number of individual scaffolder action steps executed',
});
const stepDuration = meter.createHistogram('scaffolder.step.duration', {
description: 'Duration of a step runs',
unit: 'seconds',
const stepDuration = metrics.createHistogram('scaffolder.step.duration', {
description: 'Time taken to complete a single scaffolder action step',
unit: 's',
});
async function taskStart(task: TaskContext) {
@@ -36,6 +36,7 @@ import { WorkflowRunner } from './types';
import ObservableImpl from 'zen-observable';
import waitForExpect from 'wait-for-expect';
import { mockServices } from '@backstage/backend-test-utils';
import { metricsServiceMock } from '@backstage/backend-test-utils/alpha';
import { loggerToWinstonLogger } from '../../util/loggerToWinstonLogger';
jest.mock('./NunjucksWorkflowRunner');
@@ -93,6 +94,7 @@ describe('TaskWorker', () => {
integrations,
taskBroker: broker,
actionRegistry,
metrics: metricsServiceMock.mock(),
});
await broker.dispatch({
@@ -124,6 +126,7 @@ describe('TaskWorker', () => {
integrations,
taskBroker: broker,
actionRegistry,
metrics: metricsServiceMock.mock(),
});
const { taskId } = await broker.dispatch({
@@ -174,6 +177,7 @@ describe('TaskWorker', () => {
},
},
}),
metrics: metricsServiceMock.mock(),
});
await taskWorker.runOneTask({
@@ -261,6 +265,7 @@ describe('Concurrent TaskWorker', () => {
taskBroker: broker,
actionRegistry,
concurrentTasksLimit: expectedConcurrentTasks,
metrics: metricsServiceMock.mock(),
});
taskWorker.start();
@@ -307,6 +312,7 @@ describe('Cancellable TaskWorker', () => {
integrations,
taskBroker,
actionRegistry,
metrics: metricsServiceMock.mock(),
});
const steps = [...Array(10)].map(n => ({
@@ -15,6 +15,7 @@
*/
import { AuditorService, LoggerService } from '@backstage/backend-plugin-api';
import type { MetricsService } from '@backstage/backend-plugin-api/alpha';
import { assertError, InputError, stringifyError } from '@backstage/errors';
import { ScmIntegrations } from '@backstage/integration';
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
@@ -78,6 +79,7 @@ export type CreateWorkerOptions = {
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
permissions?: PermissionEvaluator;
gracefulShutdown?: boolean;
metrics: MetricsService;
};
/**
@@ -123,6 +125,7 @@ export class TaskWorker {
additionalTemplateGlobals,
permissions,
gracefulShutdown,
metrics,
} = options;
const workflowRunner = new NunjucksWorkflowRunner({
@@ -135,6 +138,7 @@ export class TaskWorker {
additionalTemplateGlobals,
permissions,
config,
metrics,
});
return new TaskWorker({
@@ -58,7 +58,10 @@ import {
import { createDefaultFilters } from '../lib/templating/filters/createDefaultFilters';
import { createRouter } from './router';
import { DatabaseTaskStore } from '../scaffolder/tasks/DatabaseTaskStore';
import { actionsRegistryServiceMock } from '@backstage/backend-test-utils/alpha';
import {
actionsRegistryServiceMock,
metricsServiceMock,
} from '@backstage/backend-test-utils/alpha';
import { ActionsService } from '@backstage/backend-plugin-api/alpha';
function createDatabase(): DatabaseService {
@@ -229,6 +232,7 @@ const createTestRouter = async (
createDebugLogAction(),
],
actionsRegistry: overrides.actionsRegistry ?? actionsRegistryServiceMock(),
metrics: metricsServiceMock.mock(),
});
router.use(mockErrorHandler());
@@ -131,7 +131,10 @@ import {
scaffolderTaskRules,
scaffolderTemplateRules,
} from './rules';
import { ActionsService } from '@backstage/backend-plugin-api/alpha';
import {
ActionsService,
MetricsService,
} from '@backstage/backend-plugin-api/alpha';
/**
* RouterOptions
@@ -165,6 +168,7 @@ export interface RouterOptions {
auditor?: AuditorService;
autocompleteHandlers?: Record<string, AutocompleteHandler>;
actionsRegistry: ActionsService;
metrics: MetricsService;
}
function isSupportedTemplate(entity: TemplateEntityV1beta3) {
@@ -256,6 +260,7 @@ export async function createRouter(
httpAuth,
auditor,
actionsRegistry,
metrics,
} = options;
const concurrentTasksLimit =
@@ -344,6 +349,7 @@ export async function createRouter(
concurrentTasksLimit,
permissions,
gracefulShutdown,
metrics,
...templateExtensions,
});
@@ -375,6 +381,7 @@ export async function createRouter(
workingDirectory,
permissions,
config,
metrics,
...templateExtensions,
});