Merge pull request #30152 from backstage/blam/actions-cleanup/15

`scaffolder-backend`: 🧹 use `CatalogService` in default actions, and cleanup older exports that are no longer required.
This commit is contained in:
Ben Lambert
2025-06-05 16:00:48 +02:00
committed by GitHub
28 changed files with 320 additions and 603 deletions
+36
View File
@@ -0,0 +1,36 @@
---
'@backstage/plugin-scaffolder-backend-module-github': minor
---
**BREAKING CHANGES**
The `createGithubEnvironmentAction` action no longer requires an `AuthService`, and now accepts a `CatalogService` instead of `CatalogClient`.
Unless you're providing your own override action to the default, this should be a non-breaking change.
You can migrate using the following if you're getting typescript errors:
```ts
import { catalogServiceRef } from '@backstage/plugin-catalog-node';
import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha';
export const myModule = createBackendModule({
pluginId: 'scaffolder',
moduleId: 'test',
register({ registerInit }) {
registerInit({
deps: {
scaffolder: scaffolderActionsExtensionPoint,
catalog: catalogServiceRef,
},
async init({ scaffolder, catalog }) {
scaffolder.addActions(
createGithubEnvironmentAction({
catalog,
}),
);
},
});
},
});
```
+42
View File
@@ -0,0 +1,42 @@
---
'@backstage/plugin-scaffolder-backend': major
---
**BREAKING CHANGES**
- The `createBuiltinActions` method has been removed, as this should no longer be needed with the new backend system route, and was only useful when passing the default list of actions again in the old backend system. You should be able to rely on the default behaviour of the new backend system which is to merge the actions.
- The `createCatalogRegisterAction` and `createFetchCatalogEntityAction` actions no longer require an `AuthService`, and now accepts a `CatalogService` instead of `CatalogClient`.
Unless you're providing your own override action to the default, this should be a non-breaking change.
You can migrate using the following if you're getting typescript errors:
```ts
import { catalogServiceRef } from '@backstage/plugin-catalog-node';
import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha';
export const myModule = createBackendModule({
pluginId: 'scaffolder',
moduleId: 'test',
register({ registerInit }) {
registerInit({
deps: {
scaffolder: scaffolderActionsExtensionPoint,
catalog: catalogServiceRef,
},
async init({ scaffolder, catalog }) {
scaffolder.addActions(
createCatalogRegisterAction({
catalog,
}),
createFetchCatalogEntityAction({
catalog,
integrations,
}),
);
},
});
},
});
```
+19
View File
@@ -24,11 +24,14 @@ import {
configApiRef,
createApiFactory,
discoveryApiRef,
fetchApiRef,
} from '@backstage/core-plugin-api';
import { AuthProxyDiscoveryApi } from './AuthProxyDiscoveryApi';
import { formDecoratorsApiRef } from '@backstage/plugin-scaffolder/alpha';
import { DefaultScaffolderFormDecoratorsApi } from '@backstage/plugin-scaffolder/alpha';
import { mockDecorator } from './components/scaffolder/decorators';
import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react';
import { ScaffolderClient } from '@backstage/plugin-scaffolder';
export const apis: AnyApiFactory[] = [
createApiFactory({
@@ -42,6 +45,22 @@ export const apis: AnyApiFactory[] = [
factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
}),
createApiFactory({
api: scaffolderApiRef,
deps: {
discoveryApi: discoveryApiRef,
fetchApi: fetchApiRef,
scmIntegrationsApi: scmIntegrationsApiRef,
},
factory: ({ discoveryApi, fetchApi, scmIntegrationsApi }) =>
new ScaffolderClient({
useLongPollingLogs: true,
discoveryApi,
fetchApi,
scmIntegrationsApi,
}),
}),
createApiFactory({
api: formDecoratorsApiRef,
deps: {},
@@ -43,11 +43,11 @@
},
"dependencies": {
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/catalog-client": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/integration": "workspace:^",
"@backstage/plugin-catalog-node": "workspace:^",
"@backstage/plugin-scaffolder-node": "workspace:^",
"@backstage/types": "workspace:^",
"@octokit/webhooks": "^10.9.2",
@@ -3,9 +3,8 @@
> 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 { BackendFeature } from '@backstage/backend-plugin-api';
import { CatalogApi } from '@backstage/catalog-client';
import { CatalogService } from '@backstage/plugin-catalog-node';
import { Config } from '@backstage/config';
import { createPullRequest } from 'octokit-plugin-create-pull-request';
import { GithubCredentialsProvider } from '@backstage/integration';
@@ -111,8 +110,7 @@ export function createGithubDeployKeyAction(options: {
// @public
export function createGithubEnvironmentAction(options: {
integrations: ScmIntegrationRegistry;
catalogClient?: CatalogApi;
auth?: AuthService;
catalog: CatalogService;
}): TemplateAction<
{
repoUrl: string;
@@ -16,11 +16,11 @@
import { createGithubEnvironmentAction } from './githubEnvironment';
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils';
import { ConfigReader } from '@backstage/config';
import { ScmIntegrations } from '@backstage/integration';
import yaml from 'yaml';
import { examples } from './gitHubEnvironment.examples';
import { CatalogApi } from '@backstage/catalog-client';
const mockOctokit = {
rest: {
@@ -42,9 +42,7 @@ const mockOctokit = {
},
},
};
const mockCatalogClient: Partial<CatalogApi> = {
getEntitiesByRefs: jest.fn(),
};
jest.mock('octokit', () => ({
Octokit: class {
constructor() {
@@ -52,9 +50,6 @@ jest.mock('octokit', () => ({
}
},
}));
jest.mock('@backstage/catalog-client', () => ({
CatalogClient: mockCatalogClient,
}));
const publicKey = '2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvvcCU=';
@@ -72,6 +67,7 @@ describe('github:environment:create examples', () => {
let action: TemplateAction<any, any, any>;
const mockContext = createMockActionContext();
const mockCatalogService = catalogServiceMock.mock();
beforeEach(() => {
mockOctokit.rest.actions.getEnvironmentPublicKey.mockResolvedValue({
@@ -95,15 +91,17 @@ describe('github:environment:create examples', () => {
id: 2,
},
});
(mockCatalogClient.getEntitiesByRefs as jest.Mock).mockResolvedValue({
mockCatalogService.getEntitiesByRefs.mockResolvedValue({
items: [
{
apiVersion: 'v1',
kind: 'User',
metadata: {
name: 'johndoe',
},
},
{
apiVersion: 'v1',
kind: 'Group',
metadata: {
name: 'team-a',
@@ -114,7 +112,7 @@ describe('github:environment:create examples', () => {
action = createGithubEnvironmentAction({
integrations,
catalogClient: mockCatalogClient as CatalogApi,
catalog: mockCatalogService,
});
});
@@ -19,10 +19,10 @@ import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
import { ConfigReader } from '@backstage/config';
import { ScmIntegrations } from '@backstage/integration';
import { CatalogApi } from '@backstage/catalog-client';
import { mockCredentials, mockServices } from '@backstage/backend-test-utils';
import { mockCredentials } from '@backstage/backend-test-utils';
import { Octokit } from 'octokit';
import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils';
const octokitMock = Octokit as unknown as jest.Mock;
@@ -47,18 +47,10 @@ const mockOctokit = {
},
};
const mockCatalogClient: Partial<CatalogApi> = {
getEntitiesByRefs: jest.fn(),
};
jest.mock('octokit', () => ({
Octokit: jest.fn(),
}));
jest.mock('@backstage/catalog-client', () => ({
CatalogClient: mockCatalogClient,
}));
const publicKey = '2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvvcCU=';
describe('github:environment:create', () => {
@@ -72,14 +64,10 @@ describe('github:environment:create', () => {
});
const integrations = ScmIntegrations.fromConfig(config);
const mockCatalogService = catalogServiceMock.mock();
const credentials = mockCredentials.user();
const token = mockCredentials.service.token({
onBehalfOf: credentials,
targetPluginId: 'catalog',
});
let action: TemplateAction<any, any, any>;
const mockContext = createMockActionContext({
@@ -87,7 +75,6 @@ describe('github:environment:create', () => {
repoUrl: 'github.com?repo=repository&owner=owner',
name: 'envname',
},
secrets: { backstageToken: token },
});
beforeEach(() => {
@@ -114,15 +101,18 @@ describe('github:environment:create', () => {
id: 2,
},
});
(mockCatalogClient.getEntitiesByRefs as jest.Mock).mockResolvedValue({
mockCatalogService.getEntitiesByRefs.mockResolvedValue({
items: [
{
apiVersion: '1',
kind: 'User',
metadata: {
name: 'johndoe',
},
},
{
apiVersion: '1',
kind: 'Group',
metadata: {
name: 'team-a',
@@ -133,8 +123,7 @@ describe('github:environment:create', () => {
action = createGithubEnvironmentAction({
integrations,
catalogClient: mockCatalogClient as CatalogApi,
auth: mockServices.auth(),
catalog: mockCatalogService,
});
});
@@ -496,11 +485,11 @@ describe('github:environment:create', () => {
},
});
expect(mockCatalogClient.getEntitiesByRefs).toHaveBeenCalledWith(
expect(mockCatalogService.getEntitiesByRefs).toHaveBeenCalledWith(
{
entityRefs: ['group:default/team-a', 'user:default/johndoe'],
},
{ token },
{ credentials },
);
expect(
@@ -24,9 +24,8 @@ import { getOctokitOptions } from '../util';
import { Octokit } from 'octokit';
import Sodium from 'libsodium-wrappers';
import { examples } from './gitHubEnvironment.examples';
import { CatalogApi } from '@backstage/catalog-client';
import { Entity } from '@backstage/catalog-model';
import { AuthService } from '@backstage/backend-plugin-api';
import { CatalogService } from '@backstage/plugin-catalog-node';
/**
* Creates an `github:environment:create` Scaffolder action that creates a Github Environment.
@@ -35,10 +34,9 @@ import { AuthService } from '@backstage/backend-plugin-api';
*/
export function createGithubEnvironmentAction(options: {
integrations: ScmIntegrationRegistry;
catalogClient?: CatalogApi;
auth?: AuthService;
catalog: CatalogService;
}) {
const { integrations, catalogClient, auth } = options;
const { integrations, catalog } = options;
// For more information on how to define custom actions, see
// https://backstage.io/docs/features/software-templates/writing-custom-actions
return createTemplateAction({
@@ -147,11 +145,6 @@ Wildcard characters will not match \`/\`. For example, to match tags that begin
reviewers,
} = ctx.input;
const { token } = (await auth?.getPluginRequestToken({
onBehalfOf: await ctx.getInitiatorCredentials(),
targetPluginId: 'catalog',
})) ?? { token: ctx.secrets?.backstageToken };
// When environment creation step is executed right after a repo publish step, the repository might not be available immediately.
// Add a 2-second delay before initiating the steps in this action.
await new Promise(resolve => setTimeout(resolve, 2000));
@@ -190,12 +183,12 @@ Wildcard characters will not match \`/\`. For example, to match tags that begin
if (reviewers) {
let reviewersEntityRefs: Array<Entity | undefined> = [];
// Fetch reviewers from Catalog
const catalogResponse = await catalogClient?.getEntitiesByRefs(
const catalogResponse = await catalog.getEntitiesByRefs(
{
entityRefs: reviewers,
},
{
token,
credentials: await ctx.getInitiatorCredentials(),
},
);
if (catalogResponse?.items?.length) {
@@ -39,8 +39,8 @@ import {
DefaultGithubCredentialsProvider,
ScmIntegrations,
} from '@backstage/integration';
import { CatalogClient } from '@backstage/catalog-client';
import { createHandleAutocompleteRequest } from './autocomplete/autocomplete';
import { catalogServiceRef } from '@backstage/plugin-catalog-node';
/**
* @public
@@ -54,17 +54,13 @@ export const githubModule = createBackendModule({
deps: {
scaffolder: scaffolderActionsExtensionPoint,
config: coreServices.rootConfig,
discovery: coreServices.discovery,
auth: coreServices.auth,
catalog: catalogServiceRef,
autocomplete: scaffolderAutocompleteExtensionPoint,
},
async init({ scaffolder, config, discovery, auth, autocomplete }) {
async init({ scaffolder, config, autocomplete, catalog }) {
const integrations = ScmIntegrations.fromConfig(config);
const githubCredentialsProvider =
DefaultGithubCredentialsProvider.fromIntegrations(integrations);
const catalogClient = new CatalogClient({
discoveryApi: discovery,
});
scaffolder.addActions(
createGithubActionsDispatchAction({
@@ -80,8 +76,7 @@ export const githubModule = createBackendModule({
}),
createGithubEnvironmentAction({
integrations,
catalogClient,
auth,
catalog,
}),
createGithubIssuesLabelAction({
integrations,
-1
View File
@@ -64,7 +64,6 @@
"@backstage/backend-common": "^0.25.0",
"@backstage/backend-defaults": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/catalog-client": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
+6 -25
View File
@@ -7,7 +7,7 @@ import { AuditorService } from '@backstage/backend-plugin-api';
import { AuthService } from '@backstage/backend-plugin-api';
import { BackendFeature } from '@backstage/backend-plugin-api';
import { BackstageCredentials } from '@backstage/backend-plugin-api';
import { CatalogApi } from '@backstage/catalog-client';
import { CatalogService } from '@backstage/plugin-catalog-node';
import { Config } from '@backstage/config';
import { DatabaseService } from '@backstage/backend-plugin-api';
import { Duration } from 'luxon';
@@ -16,7 +16,7 @@ import { HumanDuration } from '@backstage/types';
import { JsonObject } from '@backstage/types';
import { JsonValue } from '@backstage/types';
import { Knex } from 'knex';
import { Logger } from 'winston';
import { LoggerService } from '@backstage/backend-plugin-api';
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
import { PermissionRule } from '@backstage/plugin-permission-node';
import { PermissionRuleParams } from '@backstage/plugin-permission-common';
@@ -51,28 +51,10 @@ export type ActionPermissionRuleInput<
TParams
>;
// @public
export const createBuiltinActions: (
options: CreateBuiltInActionsOptions,
) => TemplateAction[];
// @public
export interface CreateBuiltInActionsOptions {
additionalTemplateFilters?: Record<string, TemplateFilter>;
// (undocumented)
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
auth?: AuthService;
catalogClient: CatalogApi;
config: Config;
integrations: ScmIntegrations;
reader: UrlReaderService;
}
// @public
export function createCatalogRegisterAction(options: {
catalogClient: CatalogApi;
catalog: CatalogService;
integrations: ScmIntegrations;
auth?: AuthService;
}): TemplateAction<
| {
catalogInfoUrl: string;
@@ -124,8 +106,7 @@ export function createDebugLogAction(): TemplateAction<
// @public
export function createFetchCatalogEntityAction(options: {
catalogClient: CatalogApi;
auth?: AuthService;
catalog: CatalogService;
}): TemplateAction<
{
entityRef?: string | undefined;
@@ -290,7 +271,7 @@ export type CreateWorkerOptions = {
actionRegistry: TemplateActionRegistry;
integrations: ScmIntegrations;
workingDirectory: string;
logger: Logger;
logger: LoggerService;
auditor?: AuditorService;
additionalTemplateFilters?: Record<string, TemplateFilter>;
concurrentTasksLimit?: number;
@@ -427,7 +408,7 @@ export class TaskManager implements TaskContext {
task: CurrentClaimedTask,
storage: TaskStore,
abortSignal: AbortSignal,
logger: Logger,
logger: LoggerService,
auth?: AuthService,
config?: Config,
additionalWorkspaceProviders?: Record<string, WorkspaceProvider>,
@@ -19,7 +19,7 @@ import {
createBackendPlugin,
} from '@backstage/backend-plugin-api';
import { ScmIntegrations } from '@backstage/integration';
import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha';
import { catalogServiceRef } from '@backstage/plugin-catalog-node';
import { eventsServiceRef } from '@backstage/plugin-events-node';
import { TaskBroker, TemplateAction } from '@backstage/plugin-scaffolder-node';
import {
@@ -137,7 +137,7 @@ export const scaffolderPlugin = createBackendPlugin({
httpRouter: coreServices.httpRouter,
httpAuth: coreServices.httpAuth,
auditor: coreServices.auditor,
catalogClient: catalogServiceRef,
catalog: catalogServiceRef,
events: eventsServiceRef,
},
async init({
@@ -149,7 +149,7 @@ export const scaffolderPlugin = createBackendPlugin({
auth,
httpRouter,
httpAuth,
catalogClient,
catalog,
permissions,
events,
auditor,
@@ -191,8 +191,8 @@ export const scaffolderPlugin = createBackendPlugin({
createDebugLogAction(),
createWaitAction(),
// todo(blam): maybe these should be a -catalog module?
createCatalogRegisterAction({ catalogClient, integrations, auth }),
createFetchCatalogEntityAction({ catalogClient, auth }),
createCatalogRegisterAction({ catalog, integrations }),
createFetchCatalogEntityAction({ catalog }),
createCatalogWriteAction(),
createFilesystemDeleteAction(),
createFilesystemRenameAction(),
@@ -206,11 +206,10 @@ export const scaffolderPlugin = createBackendPlugin({
);
const router = await createRouter({
logger: log,
logger,
config,
database,
catalogClient,
reader,
catalog,
lifecycle,
actions,
taskBroker,
@@ -19,7 +19,7 @@ import { Entity } from '@backstage/catalog-model';
import { createFetchCatalogEntityAction } from './fetch';
import { examples } from './fetch.examples';
import yaml from 'yaml';
import { mockCredentials, mockServices } from '@backstage/backend-test-utils';
import { mockCredentials } from '@backstage/backend-test-utils';
import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils';
describe('catalog:fetch examples', () => {
@@ -31,27 +31,19 @@ describe('catalog:fetch examples', () => {
},
} as Entity;
const catalogClient = catalogServiceMock({ entities: [entity] });
const catalogMock = catalogServiceMock({ entities: [entity] });
const action = createFetchCatalogEntityAction({
catalogClient,
auth: mockServices.auth(),
catalog: catalogMock,
});
const credentials = mockCredentials.user();
const token = mockCredentials.service.token({
onBehalfOf: credentials,
targetPluginId: 'catalog',
});
const mockContext = createMockActionContext({
secrets: { backstageToken: token },
});
const mockContext = createMockActionContext();
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(catalogClient, 'getEntityByRef');
jest.spyOn(catalogMock, 'getEntityByRef');
});
describe('fetch single entity', () => {
@@ -61,9 +53,9 @@ describe('catalog:fetch examples', () => {
input: yaml.parse(examples[0].example).steps[0].input,
});
expect(catalogClient.getEntityByRef).toHaveBeenCalledWith(
expect(catalogMock.getEntityByRef).toHaveBeenCalledWith(
'component:default/name',
{ token },
{ credentials },
);
expect(mockContext.output).toHaveBeenCalledWith('entity', entity);
});
@@ -17,7 +17,7 @@
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
import { Entity } from '@backstage/catalog-model';
import { createFetchCatalogEntityAction } from './fetch';
import { mockCredentials, mockServices } from '@backstage/backend-test-utils';
import { mockCredentials } from '@backstage/backend-test-utils';
import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils';
describe('catalog:fetch', () => {
@@ -31,14 +31,7 @@ describe('catalog:fetch', () => {
const credentials = mockCredentials.user();
const token = mockCredentials.service.token({
onBehalfOf: credentials,
targetPluginId: 'catalog',
});
const mockContext = createMockActionContext({
secrets: { backstageToken: token },
});
const mockContext = createMockActionContext();
beforeEach(() => {
jest.resetAllMocks();
@@ -46,11 +39,11 @@ describe('catalog:fetch', () => {
describe('fetch single entity', () => {
it('should return entity from catalog', async () => {
const catalogClient = catalogServiceMock({ entities: [component] });
jest.spyOn(catalogClient, 'getEntityByRef');
const catalogMock = catalogServiceMock({ entities: [component] });
jest.spyOn(catalogMock, 'getEntityByRef');
const action = createFetchCatalogEntityAction({
catalogClient,
auth: mockServices.auth(),
catalog: catalogMock,
});
await action.handler({
@@ -60,21 +53,20 @@ describe('catalog:fetch', () => {
},
});
expect(catalogClient.getEntityByRef).toHaveBeenCalledWith(
expect(catalogMock.getEntityByRef).toHaveBeenCalledWith(
'component:default/test',
{ token },
{ credentials },
);
expect(mockContext.output).toHaveBeenCalledWith('entity', component);
});
it('should throw error if entity fetch fails from catalog and optional is false', async () => {
const catalogClient = catalogServiceMock.mock({
const catalogMock = catalogServiceMock.mock({
getEntityByRef: () => Promise.reject(new Error('Not found')),
});
jest.spyOn(catalogClient, 'getEntityByRef');
const action = createFetchCatalogEntityAction({
catalogClient,
auth: mockServices.auth(),
catalog: catalogMock,
});
await expect(
@@ -86,19 +78,19 @@ describe('catalog:fetch', () => {
}),
).rejects.toThrow('Not found');
expect(catalogClient.getEntityByRef).toHaveBeenCalledWith(
expect(catalogMock.getEntityByRef).toHaveBeenCalledWith(
'component:default/test',
{ token },
{ credentials },
);
expect(mockContext.output).not.toHaveBeenCalled();
});
it('should throw error if entity not in catalog and optional is false', async () => {
const catalogClient = catalogServiceMock({ entities: [] });
jest.spyOn(catalogClient, 'getEntityByRef');
const catalogMock = catalogServiceMock({ entities: [] });
jest.spyOn(catalogMock, 'getEntityByRef');
const action = createFetchCatalogEntityAction({
catalogClient,
auth: mockServices.auth(),
catalog: catalogMock,
});
await expect(
@@ -110,9 +102,9 @@ describe('catalog:fetch', () => {
}),
).rejects.toThrow('Entity component:default/test not found');
expect(catalogClient.getEntityByRef).toHaveBeenCalledWith(
expect(catalogMock.getEntityByRef).toHaveBeenCalledWith(
'component:default/test',
{ token },
{ credentials },
);
expect(mockContext.output).not.toHaveBeenCalled();
});
@@ -125,11 +117,12 @@ describe('catalog:fetch', () => {
namespace: 'ns',
},
} as Entity;
const catalogClient = catalogServiceMock({ entities: [entity] });
jest.spyOn(catalogClient, 'getEntityByRef');
const catalogMock = catalogServiceMock({ entities: [entity] });
jest.spyOn(catalogMock, 'getEntityByRef');
const action = createFetchCatalogEntityAction({
catalogClient,
auth: mockServices.auth(),
catalog: catalogMock,
});
await action.handler({
@@ -141,21 +134,21 @@ describe('catalog:fetch', () => {
},
});
expect(catalogClient.getEntityByRef).toHaveBeenCalledWith(
'group:ns/test',
{ token },
);
expect(catalogMock.getEntityByRef).toHaveBeenCalledWith('group:ns/test', {
credentials,
});
expect(mockContext.output).toHaveBeenCalledWith('entity', entity);
});
});
describe('fetch multiple entities', () => {
it('should return entities from catalog', async () => {
const catalogClient = catalogServiceMock({ entities: [component] });
jest.spyOn(catalogClient, 'getEntitiesByRefs');
const catalogMock = catalogServiceMock({ entities: [component] });
jest.spyOn(catalogMock, 'getEntitiesByRefs');
const action = createFetchCatalogEntityAction({
catalogClient,
auth: mockServices.auth(),
catalog: catalogMock,
});
await action.handler({
@@ -165,19 +158,18 @@ describe('catalog:fetch', () => {
},
});
expect(catalogClient.getEntitiesByRefs).toHaveBeenCalledWith(
expect(catalogMock.getEntitiesByRefs).toHaveBeenCalledWith(
{ entityRefs: ['component:default/test'] },
{ token },
{ credentials },
);
expect(mockContext.output).toHaveBeenCalledWith('entities', [component]);
});
it('should throw error if undefined is returned for some entity', async () => {
const catalogClient = catalogServiceMock({ entities: [component] });
jest.spyOn(catalogClient, 'getEntitiesByRefs');
const catalogMock = catalogServiceMock({ entities: [component] });
jest.spyOn(catalogMock, 'getEntitiesByRefs');
const action = createFetchCatalogEntityAction({
catalogClient,
auth: mockServices.auth(),
catalog: catalogMock,
});
await expect(
@@ -190,21 +182,21 @@ describe('catalog:fetch', () => {
}),
).rejects.toThrow('Entity component:default/test2 not found');
expect(catalogClient.getEntitiesByRefs).toHaveBeenCalledWith(
expect(catalogMock.getEntitiesByRefs).toHaveBeenCalledWith(
{
entityRefs: ['component:default/test', 'component:default/test2'],
},
{ token },
{ credentials },
);
expect(mockContext.output).not.toHaveBeenCalled();
});
it('should return null in case some of the entities not found and optional is true', async () => {
const catalogClient = catalogServiceMock({ entities: [component] });
jest.spyOn(catalogClient, 'getEntitiesByRefs');
const catalogMock = catalogServiceMock({ entities: [component] });
jest.spyOn(catalogMock, 'getEntitiesByRefs');
const action = createFetchCatalogEntityAction({
catalogClient,
auth: mockServices.auth(),
catalog: catalogMock,
});
await action.handler({
@@ -215,9 +207,9 @@ describe('catalog:fetch', () => {
},
});
expect(catalogClient.getEntitiesByRefs).toHaveBeenCalledWith(
expect(catalogMock.getEntitiesByRefs).toHaveBeenCalledWith(
{ entityRefs: ['component:default/test', 'component:default/test2'] },
{ token },
{ credentials },
);
expect(mockContext.output).toHaveBeenCalledWith('entities', [
component,
@@ -240,13 +232,14 @@ describe('catalog:fetch', () => {
},
kind: 'User',
} as Entity;
const catalogClient = catalogServiceMock({
const catalogMock = catalogServiceMock({
entities: [entity1, entity2],
});
jest.spyOn(catalogClient, 'getEntitiesByRefs');
jest.spyOn(catalogMock, 'getEntitiesByRefs');
const action = createFetchCatalogEntityAction({
catalogClient,
auth: mockServices.auth(),
catalog: catalogMock,
});
await action.handler({
@@ -258,9 +251,9 @@ describe('catalog:fetch', () => {
},
});
expect(catalogClient.getEntitiesByRefs).toHaveBeenCalledWith(
expect(catalogMock.getEntitiesByRefs).toHaveBeenCalledWith(
{ entityRefs: ['group:ns/test', 'user:default/test'] },
{ token },
{ credentials },
);
expect(mockContext.output).toHaveBeenCalledWith('entities', [
@@ -14,11 +14,10 @@
* limitations under the License.
*/
import { CatalogApi } from '@backstage/catalog-client';
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import { parseEntityRef, stringifyEntityRef } from '@backstage/catalog-model';
import { examples } from './fetch.examples';
import { AuthService } from '@backstage/backend-plugin-api';
import { CatalogService } from '@backstage/plugin-catalog-node';
const id = 'catalog:fetch';
@@ -28,10 +27,9 @@ const id = 'catalog:fetch';
* @public
*/
export function createFetchCatalogEntityAction(options: {
catalogClient: CatalogApi;
auth?: AuthService;
catalog: CatalogService;
}) {
const { catalogClient, auth } = options;
const { catalog } = options;
return createTemplateAction({
id,
@@ -95,18 +93,13 @@ export function createFetchCatalogEntityAction(options: {
throw new Error('Missing entity reference or references');
}
const { token } = (await auth?.getPluginRequestToken({
onBehalfOf: await ctx.getInitiatorCredentials(),
targetPluginId: 'catalog',
})) ?? { token: ctx.secrets?.backstageToken };
if (entityRef) {
const entity = await catalogClient.getEntityByRef(
const entity = await catalog.getEntityByRef(
stringifyEntityRef(
parseEntityRef(entityRef, { defaultKind, defaultNamespace }),
),
{
token,
credentials: await ctx.getInitiatorCredentials(),
},
);
@@ -117,7 +110,7 @@ export function createFetchCatalogEntityAction(options: {
}
if (entityRefs) {
const entities = await catalogClient.getEntitiesByRefs(
const entities = await catalog.getEntitiesByRefs(
{
entityRefs: entityRefs.map(ref =>
stringifyEntityRef(
@@ -125,9 +118,7 @@ export function createFetchCatalogEntityAction(options: {
),
),
},
{
token,
},
{ credentials: await ctx.getInitiatorCredentials() },
);
const finalEntities = entities.items.map((e, i) => {
@@ -21,7 +21,7 @@ import { createCatalogRegisterAction } from './register';
import { Entity } from '@backstage/catalog-model';
import { examples } from './register.examples';
import yaml from 'yaml';
import { mockCredentials, mockServices } from '@backstage/backend-test-utils';
import { mockCredentials } from '@backstage/backend-test-utils';
import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils';
describe('catalog:register', () => {
@@ -33,28 +33,22 @@ describe('catalog:register', () => {
}),
);
const catalogClient = catalogServiceMock.mock();
const catalogMock = catalogServiceMock.mock();
const action = createCatalogRegisterAction({
integrations,
catalogClient,
auth: mockServices.auth(),
catalog: catalogMock,
});
const credentials = mockCredentials.user();
const token = mockCredentials.service.token({
onBehalfOf: credentials,
targetPluginId: 'catalog',
});
const mockContext = createMockActionContext();
beforeEach(() => {
jest.resetAllMocks();
});
it('should register location in catalog', async () => {
catalogClient.addLocation
catalogMock.addLocation
.mockResolvedValueOnce({
location: null as any,
entities: [],
@@ -71,21 +65,22 @@ describe('catalog:register', () => {
} as Entity,
],
});
await action.handler({
...mockContext,
input: yaml.parse(examples[0].example).steps[0].input,
});
expect(catalogClient.addLocation).toHaveBeenNthCalledWith(
expect(catalogMock.addLocation).toHaveBeenNthCalledWith(
1,
{
type: 'url',
target:
'http://github.com/backstage/backstage/blob/master/catalog-info.yaml',
},
{ token },
{ credentials },
);
expect(catalogClient.addLocation).toHaveBeenNthCalledWith(
expect(catalogMock.addLocation).toHaveBeenNthCalledWith(
2,
{
dryRun: true,
@@ -93,7 +88,7 @@ describe('catalog:register', () => {
target:
'http://github.com/backstage/backstage/blob/master/catalog-info.yaml',
},
{ token },
{ credentials },
);
expect(mockContext.output).toHaveBeenCalledWith(
@@ -19,7 +19,7 @@ import { ConfigReader } from '@backstage/config';
import { ScmIntegrations } from '@backstage/integration';
import { createCatalogRegisterAction } from './register';
import { Entity } from '@backstage/catalog-model';
import { mockCredentials, mockServices } from '@backstage/backend-test-utils';
import { mockCredentials } from '@backstage/backend-test-utils';
import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils';
describe('catalog:register', () => {
@@ -31,21 +31,15 @@ describe('catalog:register', () => {
}),
);
const catalogClient = catalogServiceMock.mock();
const catalogMock = catalogServiceMock.mock();
const action = createCatalogRegisterAction({
integrations,
catalogClient,
auth: mockServices.auth(),
catalog: catalogMock,
});
const credentials = mockCredentials.user();
const token = mockCredentials.service.token({
onBehalfOf: credentials,
targetPluginId: 'catalog',
});
const mockContext = createMockActionContext();
beforeEach(() => {
@@ -66,7 +60,7 @@ describe('catalog:register', () => {
});
it('should register location in catalog', async () => {
catalogClient.addLocation
catalogMock.addLocation
.mockResolvedValueOnce({
location: null as any,
entities: [],
@@ -90,22 +84,22 @@ describe('catalog:register', () => {
},
});
expect(catalogClient.addLocation).toHaveBeenNthCalledWith(
expect(catalogMock.addLocation).toHaveBeenNthCalledWith(
1,
{
type: 'url',
target: 'http://foo/var',
},
{ token },
{ credentials },
);
expect(catalogClient.addLocation).toHaveBeenNthCalledWith(
expect(catalogMock.addLocation).toHaveBeenNthCalledWith(
2,
{
dryRun: true,
type: 'url',
target: 'http://foo/var',
},
{ token },
{ credentials },
);
expect(mockContext.output).toHaveBeenCalledWith(
@@ -119,7 +113,7 @@ describe('catalog:register', () => {
});
it('should return entityRef with the Component entity and not the generated location', async () => {
catalogClient.addLocation
catalogMock.addLocation
.mockResolvedValueOnce({
location: null as any,
entities: [],
@@ -170,7 +164,7 @@ describe('catalog:register', () => {
});
it('should return entityRef with the next non-generated entity if no Component kind can be found', async () => {
catalogClient.addLocation
catalogMock.addLocation
.mockResolvedValueOnce({
location: null as any,
entities: [],
@@ -214,7 +208,7 @@ describe('catalog:register', () => {
});
it('should return entityRef with the first entity if no non-generated entities can be found', async () => {
catalogClient.addLocation
catalogMock.addLocation
.mockResolvedValueOnce({
location: null as any,
entities: [],
@@ -251,7 +245,7 @@ describe('catalog:register', () => {
});
it('should not return entityRef if there are no entities', async () => {
catalogClient.addLocation
catalogMock.addLocation
.mockResolvedValueOnce({
location: null as any,
entities: [],
@@ -273,7 +267,7 @@ describe('catalog:register', () => {
});
it('should ignore failures when dry running the location in the catalog if `optional` is set', async () => {
catalogClient.addLocation
catalogMock.addLocation
.mockRejectedValueOnce(new Error('Not found'))
.mockRejectedValueOnce(new Error('Not found'));
await action.handler({
@@ -284,22 +278,22 @@ describe('catalog:register', () => {
},
});
expect(catalogClient.addLocation).toHaveBeenNthCalledWith(
expect(catalogMock.addLocation).toHaveBeenNthCalledWith(
1,
{
type: 'url',
target: 'http://foo/var',
},
{ token },
{ credentials },
);
expect(catalogClient.addLocation).toHaveBeenNthCalledWith(
expect(catalogMock.addLocation).toHaveBeenNthCalledWith(
2,
{
dryRun: true,
type: 'url',
target: 'http://foo/var',
},
{ token },
{ credentials },
);
expect(mockContext.output).toHaveBeenCalledWith(
@@ -309,7 +303,7 @@ describe('catalog:register', () => {
});
it('should fetch entities when adding location in the catalog fails and `optional` is set', async () => {
catalogClient.addLocation
catalogMock.addLocation
.mockRejectedValueOnce(new Error('Already registered'))
.mockResolvedValueOnce({
location: null as any,
@@ -331,22 +325,22 @@ describe('catalog:register', () => {
},
});
expect(catalogClient.addLocation).toHaveBeenNthCalledWith(
expect(catalogMock.addLocation).toHaveBeenNthCalledWith(
1,
{
type: 'url',
target: 'http://foo/var',
},
{ token },
{ credentials },
);
expect(catalogClient.addLocation).toHaveBeenNthCalledWith(
expect(catalogMock.addLocation).toHaveBeenNthCalledWith(
2,
{
dryRun: true,
type: 'url',
target: 'http://foo/var',
},
{ token },
{ credentials },
);
expect(mockContext.output).toHaveBeenCalledWith(
@@ -16,11 +16,10 @@
import { InputError } from '@backstage/errors';
import { ScmIntegrations } from '@backstage/integration';
import { CatalogApi } from '@backstage/catalog-client';
import { stringifyEntityRef, Entity } from '@backstage/catalog-model';
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import { examples } from './register.examples';
import { AuthService } from '@backstage/backend-plugin-api';
import { CatalogService } from '@backstage/plugin-catalog-node';
const id = 'catalog:register';
@@ -29,11 +28,10 @@ const id = 'catalog:register';
* @public
*/
export function createCatalogRegisterAction(options: {
catalogClient: CatalogApi;
catalog: CatalogService;
integrations: ScmIntegrations;
auth?: AuthService;
}) {
const { catalogClient, integrations, auth } = options;
const { catalog, integrations } = options;
return createTemplateAction({
id,
@@ -99,19 +97,14 @@ export function createCatalogRegisterAction(options: {
ctx.logger.info(`Registering ${catalogInfoUrl} in the catalog`);
const { token } = (await auth?.getPluginRequestToken({
onBehalfOf: await ctx.getInitiatorCredentials(),
targetPluginId: 'catalog',
})) ?? { token: ctx.secrets?.backstageToken };
try {
// 1st try to register the location, this will throw an error if the location already exists (see catch)
await catalogClient.addLocation(
await catalog.addLocation(
{
type: 'url',
target: catalogInfoUrl,
},
token ? { token } : {},
{ credentials: await ctx.getInitiatorCredentials() },
);
} catch (e) {
if (!input.optional) {
@@ -122,13 +115,13 @@ export function createCatalogRegisterAction(options: {
try {
// 2nd retry the registration as a dry run, this will not throw an error if the location already exists
const result = await catalogClient.addLocation(
const result = await catalog.addLocation(
{
dryRun: true,
type: 'url',
target: catalogInfoUrl,
},
token ? { token } : {},
{ credentials: await ctx.getInitiatorCredentials() },
);
if (result.entities.length) {
@@ -1,268 +0,0 @@
/*
* Copyright 2021 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 { CatalogApi } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import {
DefaultGithubCredentialsProvider,
GithubCredentialsProvider,
ScmIntegrations,
} from '@backstage/integration';
import {
TemplateAction,
TemplateFilter,
TemplateGlobal,
} from '@backstage/plugin-scaffolder-node';
import {
createCatalogRegisterAction,
createCatalogWriteAction,
createFetchCatalogEntityAction,
} from './catalog';
import { createDebugLogAction, createWaitAction } from './debug';
import {
createFetchPlainAction,
createFetchPlainFileAction,
createFetchTemplateAction,
createFetchTemplateFileAction,
} from './fetch';
import {
createFilesystemDeleteAction,
createFilesystemReadDirAction,
createFilesystemRenameAction,
} from './filesystem';
import {
createGithubActionsDispatchAction,
createGithubAutolinksAction,
createGithubDeployKeyAction,
createGithubEnvironmentAction,
createGithubIssuesLabelAction,
createGithubRepoCreateAction,
createGithubRepoPushAction,
createGithubWebhookAction,
createPublishGithubAction,
createPublishGithubPullRequestAction,
} from '@backstage/plugin-scaffolder-backend-module-github';
import { createPublishAzureAction } from '@backstage/plugin-scaffolder-backend-module-azure';
import { createPublishBitbucketAction } from '@backstage/plugin-scaffolder-backend-module-bitbucket';
import {
createPublishBitbucketCloudAction,
createBitbucketPipelinesRunAction,
createPublishBitbucketCloudPullRequestAction,
} from '@backstage/plugin-scaffolder-backend-module-bitbucket-cloud';
import {
createPublishBitbucketServerAction,
createPublishBitbucketServerPullRequestAction,
} from '@backstage/plugin-scaffolder-backend-module-bitbucket-server';
import {
createPublishGerritAction,
createPublishGerritReviewAction,
} from '@backstage/plugin-scaffolder-backend-module-gerrit';
import {
createPublishGitlabAction,
createGitlabRepoPushAction,
createPublishGitlabMergeRequestAction,
} from '@backstage/plugin-scaffolder-backend-module-gitlab';
import { createPublishGiteaAction } from '@backstage/plugin-scaffolder-backend-module-gitea';
import { AuthService, UrlReaderService } from '@backstage/backend-plugin-api';
/**
* The options passed to {@link createBuiltinActions}
* @public
*/
export interface CreateBuiltInActionsOptions {
/**
* The {@link @backstage/backend-plugin-api#UrlReaderService} interface that will be used in the default actions.
*/
reader: UrlReaderService;
/**
* The {@link @backstage/integrations#ScmIntegrations} that will be used in the default actions.
*/
integrations: ScmIntegrations;
/**
* The {@link @backstage/catalog-client#CatalogApi} that will be used in the default actions.
*/
catalogClient: CatalogApi;
/**
* The {@link @backstage/backend-plugin-api#AuthService} that will be used in the default actions.
*/
auth?: AuthService;
/**
* The {@link @backstage/config#Config} that will be used in the default actions.
*/
config: Config;
/**
* Additional custom filters that will be passed to the nunjucks template engine for use in
* Template Manifests and also template skeleton files when using `fetch:template`.
*/
additionalTemplateFilters?: Record<string, TemplateFilter>;
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
}
/**
* A function to generate create a list of default actions that the scaffolder provides.
* Is called internally in the default setup, but can be used when adding your own actions or overriding the default ones
*
* TODO(blam): version 2 of the scaffolder shouldn't ship with the additional modules. We should ship the basics, and let people install
* modules for the providers they want to use.
* @public
* @returns A list of actions that can be used in the scaffolder
*
*/
export const createBuiltinActions = (
options: CreateBuiltInActionsOptions,
): TemplateAction[] => {
const {
reader,
integrations,
catalogClient,
auth,
config,
additionalTemplateFilters,
additionalTemplateGlobals,
} = options;
const githubCredentialsProvider: GithubCredentialsProvider =
DefaultGithubCredentialsProvider.fromIntegrations(integrations);
const actions = [
createFetchPlainAction({
reader,
integrations,
}),
createFetchPlainFileAction({
reader,
integrations,
}),
createFetchTemplateAction({
integrations,
reader,
additionalTemplateFilters,
additionalTemplateGlobals,
}),
createFetchTemplateFileAction({
integrations,
reader,
additionalTemplateFilters,
additionalTemplateGlobals,
}),
createPublishGerritAction({
integrations,
config,
}),
createPublishGerritReviewAction({
integrations,
config,
}),
createPublishGiteaAction({
integrations,
config,
}),
createPublishGithubAction({
integrations,
config,
githubCredentialsProvider,
}),
createPublishGithubPullRequestAction({
integrations,
githubCredentialsProvider,
config,
}),
createPublishGitlabAction({
integrations,
config,
}),
createPublishGitlabMergeRequestAction({
integrations,
}),
createGitlabRepoPushAction({
integrations,
}),
createPublishBitbucketAction({
integrations,
config,
}),
createPublishBitbucketCloudAction({
integrations,
config,
}),
createPublishBitbucketCloudPullRequestAction({ integrations, config }),
createPublishBitbucketServerAction({
integrations,
config,
}),
createPublishBitbucketServerPullRequestAction({
integrations,
config,
}),
createPublishAzureAction({
integrations,
config,
}),
createDebugLogAction(),
createWaitAction(),
createCatalogRegisterAction({ catalogClient, integrations, auth }),
createFetchCatalogEntityAction({ catalogClient, auth }),
createCatalogWriteAction(),
createFilesystemDeleteAction(),
createFilesystemReadDirAction(),
createFilesystemRenameAction(),
createGithubActionsDispatchAction({
integrations,
githubCredentialsProvider,
}),
createGithubWebhookAction({
integrations,
githubCredentialsProvider,
}),
createGithubIssuesLabelAction({
integrations,
githubCredentialsProvider,
}),
createGithubRepoCreateAction({
integrations,
githubCredentialsProvider,
}),
createGithubRepoPushAction({
integrations,
config,
githubCredentialsProvider,
}),
createGithubEnvironmentAction({
integrations,
catalogClient,
}),
createGithubDeployKeyAction({
integrations,
}),
createGithubAutolinksAction({
integrations,
githubCredentialsProvider,
}),
createBitbucketPipelinesRunAction({
integrations,
}),
];
return actions as TemplateAction[];
};
@@ -15,7 +15,6 @@
*/
export * from './catalog';
export * from './createBuiltinActions';
export * from './debug';
export * from './fetch';
export * from './filesystem';
@@ -17,6 +17,7 @@
import {
AuditorService,
BackstageCredentials,
LoggerService,
} from '@backstage/backend-plugin-api';
import type { UserEntity } from '@backstage/catalog-model';
import { ScmIntegrations } from '@backstage/integration';
@@ -36,7 +37,6 @@ import fs from 'fs-extra';
import path from 'path';
import { fileURLToPath } from 'url';
import { v4 as uuid } from 'uuid';
import { Logger } from 'winston';
import { TemplateActionRegistry } from '../actions';
import { NunjucksWorkflowRunner } from '../tasks/NunjucksWorkflowRunner';
import { DecoratedActionsRegistry } from './DecoratedActionsRegistry';
@@ -61,7 +61,7 @@ interface DryRunResult {
/** @internal */
export type TemplateTesterCreateOptions = {
logger: Logger;
logger: LoggerService;
auditor?: AuditorService;
integrations: ScmIntegrations;
actionRegistry: TemplateActionRegistry;
@@ -18,6 +18,7 @@ import {
AuditorService,
AuthService,
BackstageCredentials,
LoggerService,
} from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
import { TaskSpec } from '@backstage/plugin-scaffolder-common';
@@ -38,7 +39,6 @@ import {
Observable,
createDeferred,
} from '@backstage/types';
import { Logger } from 'winston';
import ObservableImpl from 'zen-observable';
import { DefaultWorkspaceService, WorkspaceService } from './WorkspaceService';
import { readDuration } from './helper';
@@ -71,7 +71,7 @@ export class TaskManager implements TaskContext {
task: CurrentClaimedTask,
storage: TaskStore,
abortSignal: AbortSignal,
logger: Logger,
logger: LoggerService,
auth?: AuthService,
config?: Config,
additionalWorkspaceProviders?: Record<string, WorkspaceProvider>,
@@ -100,7 +100,7 @@ export class TaskManager implements TaskContext {
private readonly task: CurrentClaimedTask,
private readonly storage: TaskStore,
private readonly signal: AbortSignal,
private readonly logger: Logger,
private readonly logger: LoggerService,
private readonly workspaceService: WorkspaceService,
private readonly auth?: AuthService,
) {}
@@ -269,7 +269,7 @@ export interface CurrentClaimedTask {
export class StorageTaskBroker implements TaskBroker {
constructor(
private readonly storage: TaskStore,
private readonly logger: Logger,
private readonly logger: LoggerService,
private readonly config?: Config,
private readonly auth?: AuthService,
private readonly additionalWorkspaceProviders?: Record<
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { AuditorService } from '@backstage/backend-plugin-api';
import { AuditorService, LoggerService } from '@backstage/backend-plugin-api';
import { assertError, stringifyError } from '@backstage/errors';
import { ScmIntegrations } from '@backstage/integration';
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
@@ -25,7 +25,6 @@ import {
TemplateGlobal,
} from '@backstage/plugin-scaffolder-node';
import PQueue from 'p-queue';
import { Logger } from 'winston';
import { TemplateActionRegistry } from '../actions';
import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner';
import { WorkflowRunner } from './types';
@@ -43,7 +42,7 @@ export type TaskWorkerOptions = {
};
concurrentTasksLimit: number;
permissions?: PermissionEvaluator;
logger?: Logger;
logger?: LoggerService;
auditor?: AuditorService;
gracefulShutdown?: boolean;
};
@@ -58,7 +57,7 @@ export type CreateWorkerOptions = {
actionRegistry: TemplateActionRegistry;
integrations: ScmIntegrations;
workingDirectory: string;
logger: Logger;
logger: LoggerService;
auditor?: AuditorService;
additionalTemplateFilters?: Record<string, TemplateFilter>;
/**
@@ -86,7 +85,7 @@ export type CreateWorkerOptions = {
*/
export class TaskWorker {
private taskQueue: PQueue;
private logger: Logger | undefined;
private logger: LoggerService | undefined;
private auditor: AuditorService | undefined;
private stopWorkers: boolean;
@@ -14,7 +14,10 @@
* limitations under the License.
*/
import { CatalogApi } from '@backstage/catalog-client';
import {
BackstageCredentials,
LoggerService,
} from '@backstage/backend-plugin-api';
import {
ANNOTATION_LOCATION,
ANNOTATION_SOURCE_LOCATION,
@@ -25,14 +28,14 @@ import {
} from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { assertError, InputError, NotFoundError } from '@backstage/errors';
import { CatalogService } from '@backstage/plugin-catalog-node';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import fs from 'fs-extra';
import os from 'os';
import { Logger } from 'winston';
export async function getWorkingDirectory(
config: Config,
logger: Logger,
logger: LoggerService,
): Promise<string> {
if (!config.has('backend.workingDirectory')) {
return os.tmpdir();
@@ -89,16 +92,18 @@ export function getEntityBaseUrl(entity: Entity): string | undefined {
*/
export async function findTemplate(options: {
entityRef: CompoundEntityRef;
token?: string;
catalogApi: CatalogApi;
catalog: CatalogService;
credentials: BackstageCredentials;
}): Promise<TemplateEntityV1beta3> {
const { entityRef, token, catalogApi } = options;
const { entityRef, catalog, credentials } = options;
if (entityRef.kind.toLocaleLowerCase('en-US') !== 'template') {
throw new InputError(`Invalid kind, only 'Template' kind is supported`);
}
const template = await catalogApi.getEntityByRef(entityRef, { token });
const template = await catalog.getEntityByRef(entityRef, {
credentials,
});
if (!template) {
throw new NotFoundError(
`Template ${stringifyEntityRef(entityRef)} not found`,
@@ -26,6 +26,10 @@ import ObservableImpl from 'zen-observable';
* Due to a circular dependency between this plugin and the
* plugin-scaffolder-backend-module-cookiecutter plugin, it results in an error:
* TypeError: _pluginscaffolderbackend.createTemplateAction is not a function
*
* TODO: These tests need refactoring. Seems like the identityApi tests don't do anything different anymore.
* And there's very little value re-reunning all the tests again with just additional template filters and values.
* Let's break them out into better tests. Didn't want to do it in the same PR i'm working on right now.
*/
import {
parseEntityRef,
@@ -33,6 +37,7 @@ import {
UserEntity,
} from '@backstage/catalog-model';
import {
createTemplateAction,
TaskBroker,
TemplateFilter,
TemplateGlobal,
@@ -53,7 +58,6 @@ import {
createTemplateGlobalFunction,
createTemplateGlobalValue,
} from '@backstage/plugin-scaffolder-node/alpha';
import { UrlReaders } from '@backstage/backend-defaults/urlReader';
import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils';
import { EventsService } from '@backstage/plugin-events-node';
import { DatabaseService } from '@backstage/backend-plugin-api';
@@ -96,11 +100,6 @@ function createDatabase(): DatabaseService {
).forPlugin('scaffolder');
}
const mockUrlReader = UrlReaders.default({
logger: mockServices.logger.mock(),
config: new ConfigReader({}),
});
const config = new ConfigReader({});
describe.each([
@@ -188,7 +187,7 @@ describe.each([
let app: express.Express;
let loggerSpy: jest.SpyInstance;
let taskBroker: TaskBroker;
const catalogClient = catalogServiceMock.mock();
const catalogMock = catalogServiceMock.mock();
const permissionApi = {
authorize: jest.fn(),
authorizeConditional: jest.fn(),
@@ -200,10 +199,6 @@ describe.each([
} as unknown as EventsService;
const credentials = mockCredentials.user();
const token = mockCredentials.service.token({
onBehalfOf: credentials,
targetPluginId: 'catalog',
});
const getMockTemplate = (): TemplateEntityV1beta3 => ({
apiVersion: 'scaffolder.backstage.io/v1beta3',
@@ -303,8 +298,7 @@ describe.each([
logger: logger,
config: new ConfigReader({}),
database: createDatabase(),
catalogClient,
reader: mockUrlReader,
catalog: catalogMock,
taskBroker,
permissions: permissionApi,
auth,
@@ -312,10 +306,23 @@ describe.each([
events,
additionalTemplateFilters,
additionalTemplateGlobals,
actions: [
createTemplateAction({
id: 'test',
description: 'test',
schema: {
input: z =>
z.object({
test: z.string(),
}),
},
handler: async () => {},
}),
],
});
app = express().use(router);
catalogClient.getEntityByRef.mockImplementation(async ref => {
catalogMock.getEntityByRef.mockImplementation(async ref => {
const { kind } = parseEntityRef(ref);
if (kind.toLocaleLowerCase() === 'template') {
@@ -355,7 +362,7 @@ describe.each([
const response = await request(app).get('/v2/actions').send();
expect(response.status).toEqual(200);
expect(response.body[0].id).toBeDefined();
expect(response.body.length).toBeGreaterThan(8);
expect(response.body.length).toBe(1);
});
});
@@ -447,7 +454,6 @@ describe.each([
expect.objectContaining({
createdBy: 'user:default/mock',
secrets: {
backstageToken: token,
__initiatorCredentials: JSON.stringify(credentials),
},
@@ -599,7 +605,6 @@ describe.each([
status: 'completed',
createdAt: '',
secrets: {
backstageToken: token,
__initiatorCredentials: JSON.stringify(credentials),
},
createdBy: '',
@@ -851,9 +856,9 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{
directoryContents: [],
});
expect(catalogClient.getEntityByRef).toHaveBeenCalledTimes(1);
expect(catalogMock.getEntityByRef).toHaveBeenCalledTimes(1);
expect(catalogClient.getEntityByRef).toHaveBeenCalledWith(
expect(catalogMock.getEntityByRef).toHaveBeenCalledWith(
'user:default/mock',
expect.anything(),
);
@@ -879,16 +884,28 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{
logger: logger,
config: new ConfigReader({}),
database: createDatabase(),
catalogClient,
reader: mockUrlReader,
catalog: catalogMock,
taskBroker,
permissions: permissionApi,
auth,
httpAuth,
actions: [
createTemplateAction({
id: 'test',
description: 'test',
schema: {
input: z =>
z.object({
test: z.string(),
}),
},
handler: async () => {},
}),
],
});
app = express().use(router);
catalogClient.getEntityByRef.mockImplementation(async ref => {
catalogMock.getEntityByRef.mockImplementation(async ref => {
const { kind } = parseEntityRef(ref);
if (kind.toLocaleLowerCase() === 'template') {
@@ -927,7 +944,7 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{
const response = await request(app).get('/v2/actions').send();
expect(response.status).toEqual(200);
expect(response.body[0].id).toBeDefined();
expect(response.body.length).toBeGreaterThan(8);
expect(response.body.length).toBe(1);
});
});
@@ -1099,7 +1116,6 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{
expect.objectContaining({
createdBy: 'user:default/mock',
secrets: {
backstageToken: token,
__initiatorCredentials: JSON.stringify(credentials),
},
@@ -1169,7 +1185,6 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{
expect.objectContaining({
createdBy: 'user:default/mock',
secrets: {
backstageToken: token,
__initiatorCredentials: JSON.stringify(credentials),
},
@@ -1258,7 +1273,6 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{
expect.objectContaining({
createdBy: 'user:default/mock',
secrets: {
backstageToken: token,
__initiatorCredentials: JSON.stringify(credentials),
},
@@ -1402,7 +1416,6 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{
status: 'completed',
createdAt: '',
secrets: {
backstageToken: token,
__initiatorCredentials: JSON.stringify(credentials),
},
createdBy: '',
@@ -1649,8 +1662,7 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{
logger: loggerToWinstonLogger(mockServices.logger.mock()),
config: new ConfigReader({}),
database: createDatabase(),
catalogClient,
reader: mockUrlReader,
catalog: catalogMock,
taskBroker,
permissions: permissionApi,
auth,
@@ -21,12 +21,11 @@ import {
DatabaseService,
HttpAuthService,
LifecycleService,
LoggerService,
PermissionsService,
resolveSafeChildPath,
SchedulerService,
UrlReaderService,
} from '@backstage/backend-plugin-api';
import { CatalogApi } from '@backstage/catalog-client';
import {
CompoundEntityRef,
Entity,
@@ -81,10 +80,8 @@ import { validate } from 'jsonschema';
import { Duration } from 'luxon';
import { pathToFileURL } from 'url';
import { v4 as uuid } from 'uuid';
import { Logger } from 'winston';
import { z } from 'zod';
import {
createBuiltinActions,
DatabaseTaskStore,
TaskWorker,
TemplateActionRegistry,
@@ -115,17 +112,17 @@ import {
isTemplatePermissionRuleInput,
TemplatePermissionRuleInput,
} from './permissions';
import { CatalogService } from '@backstage/plugin-catalog-node';
/**
* RouterOptions
*/
export interface RouterOptions {
logger: Logger;
logger: LoggerService;
config: Config;
reader: UrlReaderService;
lifecycle?: LifecycleService;
database: DatabaseService;
catalogClient: CatalogApi;
catalog: CatalogService;
scheduler?: SchedulerService;
actions?: TemplateAction<any, any, any>[];
/**
@@ -180,9 +177,8 @@ export async function createRouter(
const {
logger: parentLogger,
config,
reader,
database,
catalogClient,
catalog,
actions,
scheduler,
additionalTemplateFilters,
@@ -285,18 +281,7 @@ export async function createRouter(
workers.push(worker);
}
const actionsToRegister = Array.isArray(actions)
? actions
: createBuiltinActions({
integrations,
catalogClient,
reader,
config,
auth,
...templateExtensions,
});
actionsToRegister.forEach(action => actionRegistry.register(action));
actions?.forEach(action => actionRegistry.register(action));
const launchWorkers = () => workers.forEach(worker => worker.start());
@@ -370,16 +355,7 @@ export async function createRouter(
try {
const credentials = await httpAuth.credentials(req);
const { token } = await auth.getPluginRequestToken({
onBehalfOf: credentials,
targetPluginId: 'catalog',
});
const template = await authorizeTemplate(
req.params,
token,
credentials,
);
const template = await authorizeTemplate(req.params, credentials);
const parameters = [template.spec.parameters ?? []].flat();
@@ -458,17 +434,12 @@ export async function createRouter(
permissionService: permissions,
});
const { token } = await auth.getPluginRequestToken({
onBehalfOf: credentials,
targetPluginId: 'catalog',
});
const userEntityRef = auth.isPrincipal(credentials, 'user')
? credentials.principal.userEntityRef
: undefined;
const userEntity = userEntityRef
? await catalogClient.getEntityByRef(userEntityRef, { token })
? await catalog.getEntityByRef(userEntityRef, { credentials })
: undefined;
let auditLog = `Scaffolding task for ${templateRef}`;
@@ -481,7 +452,6 @@ export async function createRouter(
const template = await authorizeTemplate(
{ kind, namespace, name },
token,
credentials,
);
@@ -529,7 +499,7 @@ export async function createRouter(
const secrets: InternalTaskSecrets = {
...req.body.secrets,
backstageToken: token,
backstageToken: (credentials as any).token,
__initiatorCredentials: JSON.stringify({
...credentials,
// credentials.token is nonenumerable and will not be serialized, so we need to add it explicitly
@@ -887,17 +857,12 @@ export async function createRouter(
throw new InputError('Input template is not a template');
}
const { token } = await auth.getPluginRequestToken({
onBehalfOf: credentials,
targetPluginId: 'catalog',
});
const userEntityRef = auth.isPrincipal(credentials, 'user')
? credentials.principal.userEntityRef
: undefined;
const userEntity = userEntityRef
? await catalogClient.getEntityByRef(userEntityRef, { token })
? await catalog.getEntityByRef(userEntityRef, { credentials })
: undefined;
const templateRef: string = `${template.kind}:${
@@ -963,7 +928,7 @@ export async function createRouter(
})),
secrets: {
...body.secrets,
...(token && { backstageToken: token }),
backstageToken: (credentials as any).token,
},
credentials,
});
@@ -1026,13 +991,12 @@ export async function createRouter(
async function authorizeTemplate(
entityRef: CompoundEntityRef,
token: string | undefined,
credentials: BackstageCredentials,
) {
const template = await findTemplate({
catalogApi: catalogClient,
catalog,
entityRef,
token,
credentials,
});
if (!isSupportedTemplate(template)) {
+1 -2
View File
@@ -7601,12 +7601,12 @@ __metadata:
dependencies:
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
"@backstage/catalog-client": "workspace:^"
"@backstage/catalog-model": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/integration": "workspace:^"
"@backstage/plugin-catalog-node": "workspace:^"
"@backstage/plugin-scaffolder-node": "workspace:^"
"@backstage/plugin-scaffolder-node-test-utils": "workspace:^"
"@backstage/types": "workspace:^"
@@ -7721,7 +7721,6 @@ __metadata:
"@backstage/backend-defaults": "workspace:^"
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
"@backstage/catalog-client": "workspace:^"
"@backstage/catalog-model": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"