Merge branch 'master' into add-default-enviroment
This commit is contained in:
@@ -1,5 +1,100 @@
|
||||
# @backstage/plugin-scaffolder-backend
|
||||
|
||||
## 3.0.1-next.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/plugin-catalog-node@1.20.0-next.1
|
||||
- @backstage/backend-defaults@0.13.1-next.1
|
||||
- @backstage/plugin-scaffolder-backend-module-gitlab@0.10.0-next.1
|
||||
- @backstage/backend-plugin-api@1.5.0-next.1
|
||||
- @backstage/plugin-permission-common@0.9.3-next.1
|
||||
- @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.14-next.1
|
||||
- @backstage/plugin-scaffolder-backend-module-github@0.9.2-next.1
|
||||
- @backstage/backend-openapi-utils@0.6.3-next.1
|
||||
- @backstage/plugin-auth-node@0.6.9-next.1
|
||||
- @backstage/plugin-events-node@0.4.17-next.1
|
||||
- @backstage/plugin-permission-node@0.10.6-next.1
|
||||
- @backstage/plugin-scaffolder-backend-module-azure@0.2.15-next.1
|
||||
- @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.16-next.1
|
||||
- @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.15-next.1
|
||||
- @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.15-next.1
|
||||
- @backstage/plugin-scaffolder-backend-module-gerrit@0.2.15-next.1
|
||||
- @backstage/plugin-scaffolder-backend-module-gitea@0.2.15-next.1
|
||||
- @backstage/plugin-scaffolder-node@0.12.1-next.1
|
||||
|
||||
## 3.0.1-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility.
|
||||
- Updated dependencies
|
||||
- @backstage/backend-defaults@0.13.1-next.0
|
||||
- @backstage/plugin-events-node@0.4.17-next.0
|
||||
- @backstage/plugin-bitbucket-cloud-common@0.3.4-next.0
|
||||
- @backstage/plugin-auth-node@0.6.9-next.0
|
||||
- @backstage/config@1.3.6-next.0
|
||||
- @backstage/catalog-model@1.7.6-next.0
|
||||
- @backstage/integration@1.18.2-next.0
|
||||
- @backstage/plugin-scaffolder-node@0.12.1-next.0
|
||||
- @backstage/plugin-permission-node@0.10.6-next.0
|
||||
- @backstage/backend-openapi-utils@0.6.3-next.0
|
||||
- @backstage/backend-plugin-api@1.4.5-next.0
|
||||
- @backstage/errors@1.2.7
|
||||
- @backstage/types@1.2.2
|
||||
- @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.14-next.0
|
||||
- @backstage/plugin-catalog-node@1.19.2-next.0
|
||||
- @backstage/plugin-permission-common@0.9.3-next.0
|
||||
- @backstage/plugin-scaffolder-backend-module-azure@0.2.15-next.0
|
||||
- @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.16-next.0
|
||||
- @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.15-next.0
|
||||
- @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.15-next.0
|
||||
- @backstage/plugin-scaffolder-backend-module-gerrit@0.2.15-next.0
|
||||
- @backstage/plugin-scaffolder-backend-module-gitea@0.2.15-next.0
|
||||
- @backstage/plugin-scaffolder-backend-module-github@0.9.2-next.0
|
||||
- @backstage/plugin-scaffolder-backend-module-gitlab@0.9.7-next.0
|
||||
- @backstage/plugin-scaffolder-common@1.7.3-next.0
|
||||
|
||||
## 3.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 9b81a90: **BREAKING** - Removing the deprecated types and interfaces, there's no replacement for these types, and hopefully not currently used as they offer no value with the plugin being on the new backend system and no way to consume them.
|
||||
|
||||
Affected types: `CreateWorkerOptions`, `CurrentClaimedTask`, `DatabaseTaskStore`, `DatabaseTaskStoreOptions`, `TaskManager`, `TaskStore`, `TaskStoreCreateTaskOptions`, `TaskStoreCreateTaskResult`, `TaskStoreEmitOptions`, `TaskStoreListEventsOptions`, `TaskStoreRecoverTaskOptions`, `TaskStoreShutDownTaskOptions`, `TaskWorker` and `TemplateActionRegistry`.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f222a2e: Fixed distributed actions not being visible in the scaffolder template actions.
|
||||
|
||||
Depending on the plugin startup order, some of the distributed actions were not being registered correctly,
|
||||
causing them to be invisible in the scaffolder template actions list.
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/backend-defaults@0.13.0
|
||||
- @backstage/integration@1.18.1
|
||||
- @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.14
|
||||
- @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.14
|
||||
- @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.15
|
||||
- @backstage/plugin-scaffolder-backend-module-gerrit@0.2.14
|
||||
- @backstage/plugin-scaffolder-backend-module-github@0.9.1
|
||||
- @backstage/plugin-scaffolder-backend-module-gitlab@0.9.6
|
||||
- @backstage/plugin-scaffolder-backend-module-azure@0.2.14
|
||||
- @backstage/plugin-scaffolder-backend-module-gitea@0.2.14
|
||||
- @backstage/plugin-scaffolder-node@0.12.0
|
||||
- @backstage/config@1.3.5
|
||||
- @backstage/plugin-bitbucket-cloud-common@0.3.3
|
||||
- @backstage/backend-openapi-utils@0.6.2
|
||||
- @backstage/backend-plugin-api@1.4.4
|
||||
- @backstage/plugin-auth-node@0.6.8
|
||||
- @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.13
|
||||
- @backstage/plugin-catalog-node@1.19.1
|
||||
- @backstage/plugin-events-node@0.4.16
|
||||
- @backstage/plugin-permission-common@0.9.2
|
||||
- @backstage/plugin-permission-node@0.10.5
|
||||
- @backstage/plugin-scaffolder-common@1.7.2
|
||||
|
||||
## 3.0.0-next.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-scaffolder-backend",
|
||||
"version": "3.0.0-next.1",
|
||||
"version": "3.0.1-next.1",
|
||||
"description": "The Backstage backend plugin that helps you create new things",
|
||||
"backstage": {
|
||||
"role": "backend-plugin",
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* 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 { ConflictError, NotFoundError } from '@backstage/errors';
|
||||
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
|
||||
import { DefaultTemplateActionRegistry } from './TemplateActionRegistry';
|
||||
import { mockCredentials, mockServices } from '@backstage/backend-test-utils';
|
||||
import { actionsRegistryServiceMock } from '@backstage/backend-test-utils/alpha';
|
||||
|
||||
describe('DefaultTemplateActionRegistry', () => {
|
||||
let registry: DefaultTemplateActionRegistry;
|
||||
let mockActionsService: ReturnType<typeof actionsRegistryServiceMock>;
|
||||
let mockLogger: ReturnType<typeof mockServices.logger.mock>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockActionsService = actionsRegistryServiceMock();
|
||||
mockLogger = mockServices.logger.mock();
|
||||
|
||||
registry = new DefaultTemplateActionRegistry(
|
||||
mockActionsService,
|
||||
mockLogger,
|
||||
);
|
||||
});
|
||||
|
||||
describe('register', () => {
|
||||
it('should register a template action successfully', () => {
|
||||
const action = createTemplateAction({
|
||||
id: 'test-action',
|
||||
description: 'Test action',
|
||||
handler: jest.fn(),
|
||||
});
|
||||
|
||||
expect(() => registry.register(action)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw ConflictError when registering action with duplicate ID', () => {
|
||||
const action1 = createTemplateAction({
|
||||
id: 'duplicate-action',
|
||||
description: 'First action',
|
||||
handler: jest.fn(),
|
||||
});
|
||||
|
||||
const action2 = createTemplateAction({
|
||||
id: 'duplicate-action',
|
||||
description: 'Second action',
|
||||
handler: jest.fn(),
|
||||
});
|
||||
|
||||
registry.register(action1);
|
||||
|
||||
expect(() => registry.register(action2)).toThrow(ConflictError);
|
||||
expect(() => registry.register(action2)).toThrow(
|
||||
"Template action with ID 'duplicate-action' has already been registered",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should return a registered action', async () => {
|
||||
const action = createTemplateAction({
|
||||
id: 'test-action',
|
||||
description: 'Test action',
|
||||
handler: jest.fn(),
|
||||
});
|
||||
|
||||
registry.register(action);
|
||||
const result = await registry.get('test-action', {
|
||||
credentials: mockCredentials.user(),
|
||||
});
|
||||
|
||||
expect(result).toBe(action);
|
||||
});
|
||||
|
||||
it('should return action from ActionsService', async () => {
|
||||
mockActionsService.register({
|
||||
name: 'service-action',
|
||||
title: 'Service Action',
|
||||
description: 'Service action',
|
||||
schema: {
|
||||
input: z => z.object({}),
|
||||
output: z => z.object({}),
|
||||
},
|
||||
attributes: {
|
||||
readOnly: true,
|
||||
destructive: false,
|
||||
},
|
||||
action: async () => ({ output: {} }),
|
||||
});
|
||||
|
||||
const result = await registry.get('test:service-action', {
|
||||
credentials: mockCredentials.user(),
|
||||
});
|
||||
|
||||
expect(result.id).toBe('test:service-action');
|
||||
expect(result.description).toBe('Service action');
|
||||
expect(result.supportsDryRun).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw NotFoundError when action is not found', async () => {
|
||||
await expect(
|
||||
registry.get('non-existent-action', {
|
||||
credentials: mockCredentials.user(),
|
||||
}),
|
||||
).rejects.toThrow(NotFoundError);
|
||||
await expect(
|
||||
registry.get('non-existent-action', {
|
||||
credentials: mockCredentials.user(),
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
"Template action with ID 'non-existent-action' is not registered",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('list', () => {
|
||||
it('should return empty map when no actions are registered', async () => {
|
||||
const result = await registry.list({
|
||||
credentials: mockCredentials.user(),
|
||||
});
|
||||
|
||||
expect(result).toBeInstanceOf(Map);
|
||||
expect(result.size).toBe(0);
|
||||
});
|
||||
|
||||
it('should return all registered actions', async () => {
|
||||
const action1 = createTemplateAction({
|
||||
id: 'action-1',
|
||||
description: 'First action',
|
||||
handler: jest.fn(),
|
||||
});
|
||||
|
||||
const action2 = createTemplateAction({
|
||||
id: 'action-2',
|
||||
description: 'Second action',
|
||||
handler: jest.fn(),
|
||||
});
|
||||
|
||||
registry.register(action1);
|
||||
registry.register(action2);
|
||||
|
||||
const result = await registry.list({
|
||||
credentials: mockCredentials.user(),
|
||||
});
|
||||
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get('action-1')).toBe(action1);
|
||||
expect(result.get('action-2')).toBe(action2);
|
||||
});
|
||||
|
||||
it('should include actions from ActionsService', async () => {
|
||||
mockActionsService.register({
|
||||
name: 'service-action',
|
||||
title: 'Service Action',
|
||||
description: 'Service action',
|
||||
schema: {
|
||||
input: z => z.object({}),
|
||||
output: z => z.object({}),
|
||||
},
|
||||
attributes: {
|
||||
readOnly: true,
|
||||
destructive: false,
|
||||
},
|
||||
action: async () => ({ output: {} }),
|
||||
});
|
||||
|
||||
const result = await registry.list({
|
||||
credentials: mockCredentials.user(),
|
||||
});
|
||||
|
||||
expect(result.size).toBe(1);
|
||||
const action = result.get('test:service-action');
|
||||
expect(action).toBeDefined();
|
||||
expect(action!.id).toBe('test:service-action');
|
||||
expect(action!.description).toBe('Service action');
|
||||
expect(action!.supportsDryRun).toBe(true);
|
||||
});
|
||||
|
||||
it('should prioritize locally registered actions over service actions', async () => {
|
||||
const localAction = createTemplateAction({
|
||||
id: 'test:same-id',
|
||||
description: 'Local action',
|
||||
handler: jest.fn(),
|
||||
});
|
||||
|
||||
mockActionsService.register({
|
||||
name: 'same-id',
|
||||
title: 'Same ID',
|
||||
description: 'Service action',
|
||||
schema: {
|
||||
input: z => z.object({}),
|
||||
output: z => z.object({}),
|
||||
},
|
||||
action: async () => ({ output: {} }),
|
||||
});
|
||||
|
||||
registry.register(localAction);
|
||||
const result = await registry.list({
|
||||
credentials: mockCredentials.user(),
|
||||
});
|
||||
|
||||
expect(result.get('test:same-id')).toBe(localAction);
|
||||
expect(mockLogger.warn).toHaveBeenCalledWith(
|
||||
"Template action with ID 'test:same-id' has already been registered, skipping action provided by actions service",
|
||||
);
|
||||
});
|
||||
|
||||
it('should set supportsDryRun to false for destructive actions', async () => {
|
||||
mockActionsService.register({
|
||||
name: 'destructive-action',
|
||||
title: 'Destructive Action',
|
||||
description: 'Destructive action',
|
||||
schema: {
|
||||
input: z => z.object({}),
|
||||
output: z => z.object({}),
|
||||
},
|
||||
attributes: {
|
||||
readOnly: false,
|
||||
destructive: true,
|
||||
},
|
||||
action: async () => ({ output: {} }),
|
||||
});
|
||||
|
||||
const result = await registry.list({
|
||||
credentials: mockCredentials.user(),
|
||||
});
|
||||
const action = result.get('test:destructive-action');
|
||||
|
||||
expect(action!.supportsDryRun).toBe(false);
|
||||
});
|
||||
|
||||
it('should set supportsDryRun to false for non-readonly actions', async () => {
|
||||
mockActionsService.register({
|
||||
name: 'non-readonly-action',
|
||||
title: 'Non-readonly Action',
|
||||
description: 'Non-readonly action',
|
||||
schema: {
|
||||
input: z => z.object({}),
|
||||
output: z => z.object({}),
|
||||
},
|
||||
attributes: {
|
||||
readOnly: false,
|
||||
destructive: false,
|
||||
},
|
||||
action: async () => ({ output: {} }),
|
||||
});
|
||||
|
||||
const result = await registry.list({
|
||||
credentials: mockCredentials.user(),
|
||||
});
|
||||
const action = result.get('test:non-readonly-action');
|
||||
|
||||
expect(action!.supportsDryRun).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -16,13 +16,40 @@
|
||||
|
||||
import { ConflictError, NotFoundError } from '@backstage/errors';
|
||||
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
|
||||
import { ActionsService } from '@backstage/backend-plugin-api/alpha';
|
||||
import {
|
||||
BackstageCredentials,
|
||||
LoggerService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { isPlainObject } from 'lodash';
|
||||
import { Schema } from 'jsonschema';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface TemplateActionRegistry {
|
||||
register(action: TemplateAction<any, any, any>): void;
|
||||
get(
|
||||
actionId: string,
|
||||
options: { credentials: BackstageCredentials },
|
||||
): Promise<TemplateAction<any, any, any>>;
|
||||
list(options: {
|
||||
credentials: BackstageCredentials;
|
||||
}): Promise<Map<string, TemplateAction<any, any, any>>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry of all registered template actions.
|
||||
*/
|
||||
export class TemplateActionRegistry {
|
||||
export class DefaultTemplateActionRegistry implements TemplateActionRegistry {
|
||||
private readonly actions = new Map<string, TemplateAction>();
|
||||
|
||||
constructor(
|
||||
private readonly actionsRegistry: ActionsService,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
register(action: TemplateAction<any, any, any>) {
|
||||
if (this.actions.has(action.id)) {
|
||||
throw new ConflictError(
|
||||
@@ -33,8 +60,11 @@ export class TemplateActionRegistry {
|
||||
this.actions.set(action.id, action);
|
||||
}
|
||||
|
||||
get(actionId: string): TemplateAction<any, any, any> {
|
||||
const action = this.actions.get(actionId);
|
||||
async get(
|
||||
actionId: string,
|
||||
options: { credentials: BackstageCredentials },
|
||||
): Promise<TemplateAction<any, any, any>> {
|
||||
const action = (await this.list(options)).get(actionId);
|
||||
if (!action) {
|
||||
throw new NotFoundError(
|
||||
`Template action with ID '${actionId}' is not registered. See https://backstage.io/docs/features/software-templates/builtin-actions/ on how to add a new action module.`,
|
||||
@@ -43,7 +73,49 @@ export class TemplateActionRegistry {
|
||||
return action;
|
||||
}
|
||||
|
||||
list(): TemplateAction<any, any, any>[] {
|
||||
return [...this.actions.values()];
|
||||
async list(options: {
|
||||
credentials: BackstageCredentials;
|
||||
}): Promise<Map<string, TemplateAction<any, any, any>>> {
|
||||
const ret = new Map(this.actions);
|
||||
|
||||
const { actions } = await this.actionsRegistry.list({
|
||||
credentials: options.credentials,
|
||||
});
|
||||
|
||||
for (const action of actions) {
|
||||
if (ret.has(action.id)) {
|
||||
this.logger.warn(
|
||||
`Template action with ID '${action.id}' has already been registered, skipping action provided by actions service`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
ret.set(action.id, {
|
||||
id: action.id,
|
||||
description: action.description,
|
||||
examples: [],
|
||||
supportsDryRun:
|
||||
action.attributes?.readOnly === true &&
|
||||
action.attributes?.destructive === false,
|
||||
handler: async ctx => {
|
||||
const { output } = await this.actionsRegistry.invoke({
|
||||
id: action.id,
|
||||
input: ctx.input,
|
||||
credentials: await ctx.getInitiatorCredentials(),
|
||||
});
|
||||
|
||||
if (isPlainObject(output)) {
|
||||
for (const [key, value] of Object.entries(output as JsonObject)) {
|
||||
ctx.output(key as keyof typeof output, value);
|
||||
}
|
||||
}
|
||||
},
|
||||
schema: {
|
||||
input: action.schema.input as Schema,
|
||||
output: action.schema.output as Schema,
|
||||
},
|
||||
});
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,4 +16,7 @@
|
||||
|
||||
export * from './builtin';
|
||||
|
||||
export { TemplateActionRegistry } from './TemplateActionRegistry';
|
||||
export {
|
||||
type TemplateActionRegistry,
|
||||
DefaultTemplateActionRegistry,
|
||||
} from './TemplateActionRegistry';
|
||||
|
||||
@@ -16,24 +16,48 @@
|
||||
|
||||
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
|
||||
import { TemplateActionRegistry } from '../actions';
|
||||
import { BackstageCredentials } from '@backstage/backend-plugin-api';
|
||||
|
||||
/** @internal */
|
||||
export class DecoratedActionsRegistry extends TemplateActionRegistry {
|
||||
export class DecoratedActionsRegistry implements TemplateActionRegistry {
|
||||
private readonly innerActions: Map<string, TemplateAction> = new Map();
|
||||
private readonly innerRegistry: TemplateActionRegistry;
|
||||
|
||||
constructor(
|
||||
private readonly innerRegistry: TemplateActionRegistry,
|
||||
innerRegistry: TemplateActionRegistry,
|
||||
extraActions: Array<TemplateAction>,
|
||||
) {
|
||||
super();
|
||||
this.innerRegistry = innerRegistry;
|
||||
for (const action of extraActions) {
|
||||
this.register(action);
|
||||
this.innerActions.set(action.id, action);
|
||||
}
|
||||
}
|
||||
|
||||
get(actionId: string): TemplateAction {
|
||||
async get(
|
||||
actionId: string,
|
||||
options: { credentials: BackstageCredentials },
|
||||
): Promise<TemplateAction> {
|
||||
try {
|
||||
return super.get(actionId);
|
||||
} catch {
|
||||
return this.innerRegistry.get(actionId);
|
||||
return await this.innerRegistry.get(actionId, options);
|
||||
} catch (e) {
|
||||
if (!this.innerActions.has(actionId)) {
|
||||
throw e;
|
||||
}
|
||||
return this.innerActions.get(actionId)!;
|
||||
}
|
||||
}
|
||||
|
||||
async list(options: {
|
||||
credentials: BackstageCredentials;
|
||||
}): Promise<Map<string, TemplateAction<any, any, any>>> {
|
||||
const inner = await this.innerRegistry.list(options);
|
||||
return new Map<string, TemplateAction<any, any, any>>([
|
||||
...inner,
|
||||
...this.innerActions,
|
||||
]);
|
||||
}
|
||||
|
||||
register(action: TemplateAction<any, any, any>): void {
|
||||
this.innerRegistry.register(action);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,9 +42,9 @@ import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { TemplateActionRegistry } from '../actions';
|
||||
import { NunjucksWorkflowRunner } from '../tasks/NunjucksWorkflowRunner';
|
||||
import { DecoratedActionsRegistry } from './DecoratedActionsRegistry';
|
||||
import { TemplateActionRegistry } from '../actions';
|
||||
|
||||
interface DryRunInput {
|
||||
spec: TaskSpec;
|
||||
|
||||
@@ -19,11 +19,15 @@ import { TaskStore } from './types';
|
||||
import { WorkspaceProvider } from '@backstage/plugin-scaffolder-node/alpha';
|
||||
|
||||
export class DatabaseWorkspaceProvider implements WorkspaceProvider {
|
||||
private readonly storage: TaskStore;
|
||||
|
||||
static create(storage: TaskStore) {
|
||||
return new DatabaseWorkspaceProvider(storage);
|
||||
}
|
||||
|
||||
private constructor(private readonly storage: TaskStore) {}
|
||||
private constructor(storage: TaskStore) {
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
public async serializeWorkspace(options: {
|
||||
path: string;
|
||||
|
||||
@@ -15,15 +15,18 @@
|
||||
*/
|
||||
|
||||
import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner';
|
||||
import { TemplateActionRegistry } from '../actions';
|
||||
import {
|
||||
DefaultTemplateActionRegistry,
|
||||
TemplateActionRegistry,
|
||||
} from '../actions';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { TaskSpec } from '@backstage/plugin-scaffolder-common';
|
||||
import {
|
||||
createTemplateAction,
|
||||
TaskSecrets,
|
||||
TaskContext,
|
||||
TaskSecrets,
|
||||
} from '@backstage/plugin-scaffolder-node';
|
||||
import { UserEntity } from '@backstage/catalog-model';
|
||||
import {
|
||||
@@ -36,6 +39,7 @@ import {
|
||||
mockCredentials,
|
||||
mockServices,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import { actionsRegistryServiceMock } from '@backstage/backend-test-utils/alpha';
|
||||
|
||||
describe('NunjucksWorkflowRunner', () => {
|
||||
let actionRegistry: TemplateActionRegistry;
|
||||
@@ -114,7 +118,10 @@ describe('NunjucksWorkflowRunner', () => {
|
||||
// This one is ESM-only
|
||||
stripAnsi = await import('strip-ansi').then(m => m.default);
|
||||
|
||||
actionRegistry = new TemplateActionRegistry();
|
||||
actionRegistry = new DefaultTemplateActionRegistry(
|
||||
actionsRegistryServiceMock(),
|
||||
mockServices.logger.mock(),
|
||||
);
|
||||
fakeActionHandler = jest.fn();
|
||||
fakeTaskLog = jest.fn();
|
||||
|
||||
|
||||
@@ -62,8 +62,8 @@ import { createCounterMetric, createHistogramMetric } from '../../util/metrics';
|
||||
import { BackstageLoggerTransport, WinstonLogger } from './logger';
|
||||
import { convertFiltersToRecord } from '../../util/templating';
|
||||
import {
|
||||
CheckpointState,
|
||||
CheckpointContext,
|
||||
CheckpointState,
|
||||
} from '@backstage/plugin-scaffolder-node/alpha';
|
||||
import { Config } from '@backstage/config';
|
||||
import { resolveDefaultEnvironment } from '../../lib/defaultEnvironment';
|
||||
@@ -135,12 +135,14 @@ const isActionAuthorized = createConditionAuthorizer(
|
||||
|
||||
export class NunjucksWorkflowRunner implements WorkflowRunner {
|
||||
private readonly defaultTemplateFilters: Record<string, TemplateFilter>;
|
||||
private readonly options: NunjucksWorkflowRunnerOptions;
|
||||
private environment: {
|
||||
parameters: JsonObject;
|
||||
secrets?: Record<string, string>;
|
||||
} = { parameters: {}, secrets: {} };
|
||||
|
||||
constructor(private readonly options: NunjucksWorkflowRunnerOptions) {
|
||||
constructor(options: NunjucksWorkflowRunnerOptions) {
|
||||
this.options = options;
|
||||
this.defaultTemplateFilters = convertFiltersToRecord(
|
||||
createDefaultFilters({
|
||||
integrations: this.options.integrations,
|
||||
@@ -274,7 +276,9 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
|
||||
return;
|
||||
}
|
||||
const action: TemplateAction<JsonObject> =
|
||||
this.options.actionRegistry.get(step.action);
|
||||
await this.options.actionRegistry.get(step.action, {
|
||||
credentials: await task.getInitiatorCredentials(),
|
||||
});
|
||||
const { taskLogger } = createStepLogger({
|
||||
task,
|
||||
step,
|
||||
|
||||
@@ -84,15 +84,29 @@ export class TaskManager implements TaskContext {
|
||||
return agent;
|
||||
}
|
||||
|
||||
private readonly task: CurrentClaimedTask;
|
||||
private readonly storage: TaskStore;
|
||||
private readonly signal: AbortSignal;
|
||||
private readonly logger: LoggerService;
|
||||
private readonly workspaceService: WorkspaceService;
|
||||
private readonly auth?: AuthService;
|
||||
|
||||
// Runs heartbeat internally
|
||||
private constructor(
|
||||
private readonly task: CurrentClaimedTask,
|
||||
private readonly storage: TaskStore,
|
||||
private readonly signal: AbortSignal,
|
||||
private readonly logger: LoggerService,
|
||||
private readonly workspaceService: WorkspaceService,
|
||||
private readonly auth?: AuthService,
|
||||
) {}
|
||||
task: CurrentClaimedTask,
|
||||
storage: TaskStore,
|
||||
signal: AbortSignal,
|
||||
logger: LoggerService,
|
||||
workspaceService: WorkspaceService,
|
||||
auth?: AuthService,
|
||||
) {
|
||||
this.task = task;
|
||||
this.storage = storage;
|
||||
this.signal = signal;
|
||||
this.logger = logger;
|
||||
this.workspaceService = workspaceService;
|
||||
this.auth = auth;
|
||||
}
|
||||
|
||||
get taskId() {
|
||||
return this.task.taskId;
|
||||
@@ -249,17 +263,31 @@ export interface CurrentClaimedTask {
|
||||
}
|
||||
|
||||
export class StorageTaskBroker implements TaskBroker {
|
||||
private readonly storage: TaskStore;
|
||||
private readonly logger: LoggerService;
|
||||
private readonly config?: Config;
|
||||
private readonly auth?: AuthService;
|
||||
private readonly additionalWorkspaceProviders?: Record<
|
||||
string,
|
||||
WorkspaceProvider
|
||||
>;
|
||||
private readonly auditor?: AuditorService;
|
||||
|
||||
constructor(
|
||||
private readonly storage: TaskStore,
|
||||
private readonly logger: LoggerService,
|
||||
private readonly config?: Config,
|
||||
private readonly auth?: AuthService,
|
||||
private readonly additionalWorkspaceProviders?: Record<
|
||||
string,
|
||||
WorkspaceProvider
|
||||
>,
|
||||
private readonly auditor?: AuditorService,
|
||||
) {}
|
||||
storage: TaskStore,
|
||||
logger: LoggerService,
|
||||
config?: Config,
|
||||
auth?: AuthService,
|
||||
additionalWorkspaceProviders?: Record<string, WorkspaceProvider>,
|
||||
auditor?: AuditorService,
|
||||
) {
|
||||
this.storage = storage;
|
||||
this.logger = logger;
|
||||
this.config = config;
|
||||
this.auth = auth;
|
||||
this.additionalWorkspaceProviders = additionalWorkspaceProviders;
|
||||
this.auditor = auditor;
|
||||
}
|
||||
|
||||
async list(options?: {
|
||||
createdBy?: string;
|
||||
|
||||
@@ -90,11 +90,16 @@ export class TaskWorker {
|
||||
private parameterAuditTransform: ParameterAuditTransform;
|
||||
private stopWorkers: boolean;
|
||||
|
||||
private readonly options: TaskWorkerOptions & {
|
||||
parameterAuditTransform: ParameterAuditTransform;
|
||||
};
|
||||
|
||||
private constructor(
|
||||
private readonly options: TaskWorkerOptions & {
|
||||
options: TaskWorkerOptions & {
|
||||
parameterAuditTransform: ParameterAuditTransform;
|
||||
},
|
||||
) {
|
||||
this.options = options;
|
||||
this.stopWorkers = false;
|
||||
this.logger = options.logger;
|
||||
this.auditor = options.auditor;
|
||||
|
||||
@@ -49,11 +49,19 @@ export class DefaultWorkspaceService implements WorkspaceService {
|
||||
return new DefaultWorkspaceService(task, workspaceProvider, config);
|
||||
}
|
||||
|
||||
private readonly task: CurrentClaimedTask;
|
||||
private readonly workspaceProvider: WorkspaceProvider;
|
||||
private readonly config?: Config;
|
||||
|
||||
private constructor(
|
||||
private readonly task: CurrentClaimedTask,
|
||||
private readonly workspaceProvider: WorkspaceProvider,
|
||||
private readonly config?: Config,
|
||||
) {}
|
||||
task: CurrentClaimedTask,
|
||||
workspaceProvider: WorkspaceProvider,
|
||||
config?: Config,
|
||||
) {
|
||||
this.task = task;
|
||||
this.workspaceProvider = workspaceProvider;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public async serializeWorkspace(options: { path: string }): Promise<void> {
|
||||
if (this.isWorkspaceSerializationEnabled()) {
|
||||
|
||||
@@ -44,13 +44,20 @@ interface WinstonLoggerOptions {
|
||||
// This is a workaround for being able to preserve the log format of the root logger.
|
||||
// Will revisit all of this implementation once we can break the router to use only `LoggerService`.
|
||||
export class BackstageLoggerTransport extends Transport {
|
||||
private readonly backstageLogger: LoggerService;
|
||||
private readonly taskContext: TaskContext;
|
||||
private readonly stepId: string;
|
||||
|
||||
constructor(
|
||||
private readonly backstageLogger: LoggerService,
|
||||
private readonly taskContext: TaskContext,
|
||||
private readonly stepId: string,
|
||||
backstageLogger: LoggerService,
|
||||
taskContext: TaskContext,
|
||||
stepId: string,
|
||||
opts?: TransportStreamOptions,
|
||||
) {
|
||||
super(opts);
|
||||
this.backstageLogger = backstageLogger;
|
||||
this.taskContext = taskContext;
|
||||
this.stepId = stepId;
|
||||
}
|
||||
|
||||
log(info: TransformableInfo, callback: VoidFunction) {
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
resolveSafeChildPath,
|
||||
SchedulerService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { Schema } from 'jsonschema';
|
||||
import { validate } from 'jsonschema';
|
||||
import {
|
||||
CompoundEntityRef,
|
||||
Entity,
|
||||
@@ -41,10 +41,10 @@ import { ScmIntegrations } from '@backstage/integration';
|
||||
import { EventsService } from '@backstage/plugin-events-node';
|
||||
|
||||
import {
|
||||
createConditionAuthorizer,
|
||||
createPermissionIntegrationRouter,
|
||||
createConditionTransformer,
|
||||
ConditionTransformer,
|
||||
createConditionAuthorizer,
|
||||
createConditionTransformer,
|
||||
createPermissionIntegrationRouter,
|
||||
} from '@backstage/plugin-permission-node';
|
||||
import {
|
||||
TaskSpec,
|
||||
@@ -53,11 +53,11 @@ import {
|
||||
} from '@backstage/plugin-scaffolder-common';
|
||||
import {
|
||||
RESOURCE_TYPE_SCAFFOLDER_ACTION,
|
||||
RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,
|
||||
RESOURCE_TYPE_SCAFFOLDER_TASK,
|
||||
RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,
|
||||
scaffolderActionPermissions,
|
||||
scaffolderTaskPermissions,
|
||||
scaffolderPermissions,
|
||||
scaffolderTaskPermissions,
|
||||
scaffolderTemplatePermissions,
|
||||
taskCancelPermission,
|
||||
taskCreatePermission,
|
||||
@@ -67,6 +67,7 @@ import {
|
||||
} from '@backstage/plugin-scaffolder-common/alpha';
|
||||
import {
|
||||
TaskBroker,
|
||||
TaskFilters,
|
||||
TaskStatus,
|
||||
TemplateAction,
|
||||
TemplateFilter,
|
||||
@@ -80,15 +81,14 @@ import {
|
||||
} from '@backstage/plugin-scaffolder-node/alpha';
|
||||
import { HumanDuration, JsonObject } from '@backstage/types';
|
||||
import express from 'express';
|
||||
import { validate } from 'jsonschema';
|
||||
import { Duration } from 'luxon';
|
||||
import { pathToFileURL } from 'url';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
DatabaseTaskStore,
|
||||
DefaultTemplateActionRegistry,
|
||||
TaskWorker,
|
||||
TemplateActionRegistry,
|
||||
} from '../scaffolder';
|
||||
import { createDryRunner } from '../scaffolder/dryrun';
|
||||
import { StorageTaskBroker } from '../scaffolder/tasks/StorageTaskBroker';
|
||||
@@ -115,25 +115,22 @@ import {
|
||||
} from '../util/templating';
|
||||
import { createDefaultFilters } from '../lib/templating/filters/createDefaultFilters';
|
||||
import {
|
||||
ScaffolderPermissionRuleInput,
|
||||
TaskPermissionRuleInput,
|
||||
isTaskPermissionRuleInput,
|
||||
ActionPermissionRuleInput,
|
||||
isActionPermissionRuleInput,
|
||||
isTaskPermissionRuleInput,
|
||||
isTemplatePermissionRuleInput,
|
||||
ScaffolderPermissionRuleInput,
|
||||
TaskPermissionRuleInput,
|
||||
TemplatePermissionRuleInput,
|
||||
} from './permissions';
|
||||
import { CatalogService } from '@backstage/plugin-catalog-node';
|
||||
|
||||
import {
|
||||
scaffolderActionRules,
|
||||
scaffolderTemplateRules,
|
||||
scaffolderTaskRules,
|
||||
scaffolderTemplateRules,
|
||||
} from './rules';
|
||||
|
||||
import { TaskFilters } from '@backstage/plugin-scaffolder-node';
|
||||
import { ActionsService } from '@backstage/backend-plugin-api/alpha';
|
||||
import { isPlainObject } from 'lodash';
|
||||
|
||||
/**
|
||||
* RouterOptions
|
||||
@@ -272,7 +269,10 @@ export async function createRouter(
|
||||
taskBroker = options.taskBroker;
|
||||
}
|
||||
|
||||
const actionRegistry = new TemplateActionRegistry();
|
||||
const actionRegistry = new DefaultTemplateActionRegistry(
|
||||
actionsRegistry,
|
||||
logger,
|
||||
);
|
||||
|
||||
const templateExtensions = {
|
||||
additionalTemplateFilters: convertFiltersToRecord(
|
||||
@@ -306,48 +306,10 @@ export async function createRouter(
|
||||
workers.push(worker);
|
||||
}
|
||||
|
||||
// TODO(blam): it's a little unfortunate that you have to restart the scaffolder
|
||||
// backend in order to pick these up. We should really just make `ActionsRegistry.get()` async
|
||||
// and then we can move this logic into the there instead.
|
||||
// But we can't make those changes until next major.
|
||||
// Alternatively, we could look at setting up a periodic task that refreshes the actions registry, but
|
||||
// not feeling that it's worth the complexity.
|
||||
const { actions: distributedActions } = await actionsRegistry.list({
|
||||
credentials: await auth.getOwnServiceCredentials(),
|
||||
});
|
||||
|
||||
for (const action of actions) {
|
||||
actionRegistry.register(action);
|
||||
}
|
||||
|
||||
for (const action of distributedActions) {
|
||||
actionRegistry.register({
|
||||
id: action.id,
|
||||
description: action.description,
|
||||
examples: [],
|
||||
supportsDryRun:
|
||||
action.attributes?.readOnly === true &&
|
||||
action.attributes?.destructive === false,
|
||||
handler: async ctx => {
|
||||
const { output } = await actionsRegistry.invoke({
|
||||
id: action.id,
|
||||
input: ctx.input,
|
||||
credentials: await ctx.getInitiatorCredentials(),
|
||||
});
|
||||
|
||||
if (isPlainObject(output)) {
|
||||
for (const [key, value] of Object.entries(output as JsonObject)) {
|
||||
ctx.output(key as keyof typeof output, value);
|
||||
}
|
||||
}
|
||||
},
|
||||
schema: {
|
||||
input: action.schema.input as Schema,
|
||||
output: action.schema.output as Schema,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const launchWorkers = () => workers.forEach(worker => worker.start());
|
||||
|
||||
const shutdownWorkers = async () => {
|
||||
@@ -480,16 +442,20 @@ export async function createRouter(
|
||||
eventId: 'action-fetch',
|
||||
request: req,
|
||||
});
|
||||
const credentials = await httpAuth.credentials(req);
|
||||
|
||||
try {
|
||||
const actionsList = actionRegistry.list().map(action => {
|
||||
return {
|
||||
id: action.id,
|
||||
description: action.description,
|
||||
examples: action.examples,
|
||||
schema: action.schema,
|
||||
};
|
||||
});
|
||||
const list = await actionRegistry.list({ credentials });
|
||||
const actionsList = Array.from(list.values())
|
||||
.map(action => {
|
||||
return {
|
||||
id: action.id,
|
||||
description: action.description,
|
||||
examples: action.examples,
|
||||
schema: action.schema,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.id.localeCompare(b.id));
|
||||
|
||||
await auditorEvent?.success();
|
||||
|
||||
|
||||
@@ -20,11 +20,11 @@ import { Logger as WinstonLogger, createLogger } from 'winston';
|
||||
import Transport, { TransportStreamOptions } from 'winston-transport';
|
||||
|
||||
class BackstageLoggerTransport extends Transport {
|
||||
constructor(
|
||||
private readonly backstageLogger: LoggerService,
|
||||
opts?: TransportStreamOptions,
|
||||
) {
|
||||
private readonly backstageLogger: LoggerService;
|
||||
|
||||
constructor(backstageLogger: LoggerService, opts?: TransportStreamOptions) {
|
||||
super(opts);
|
||||
this.backstageLogger = backstageLogger;
|
||||
}
|
||||
|
||||
log(info: unknown, callback: VoidFunction) {
|
||||
|
||||
Reference in New Issue
Block a user