@backstage/plugin-scaffolder-backend 1.26.0-next.1 → 1.26.0-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/alpha/package.json +1 -1
- package/dist/ScaffolderPlugin.cjs.js +168 -0
- package/dist/ScaffolderPlugin.cjs.js.map +1 -0
- package/dist/alpha.cjs.js +7 -196
- package/dist/alpha.cjs.js.map +1 -1
- package/dist/deprecated.cjs.js +15 -0
- package/dist/deprecated.cjs.js.map +1 -0
- package/dist/index.cjs.js +57 -134
- package/dist/index.cjs.js.map +1 -1
- package/dist/lib/templating/SecureTemplater.cjs.js +169 -0
- package/dist/lib/templating/SecureTemplater.cjs.js.map +1 -0
- package/dist/lib/templating/filters.cjs.js +26 -0
- package/dist/lib/templating/filters.cjs.js.map +1 -0
- package/dist/lib/templating/helpers.cjs.js +13 -0
- package/dist/lib/templating/helpers.cjs.js.map +1 -0
- package/dist/scaffolder/actions/TemplateActionRegistry.cjs.js +30 -0
- package/dist/scaffolder/actions/TemplateActionRegistry.cjs.js.map +1 -0
- package/dist/scaffolder/actions/builtin/catalog/fetch.cjs.js +93 -0
- package/dist/scaffolder/actions/builtin/catalog/fetch.cjs.js.map +1 -0
- package/dist/scaffolder/actions/builtin/catalog/fetch.examples.cjs.js +43 -0
- package/dist/scaffolder/actions/builtin/catalog/fetch.examples.cjs.js.map +1 -0
- package/dist/scaffolder/actions/builtin/catalog/register.cjs.js +142 -0
- package/dist/scaffolder/actions/builtin/catalog/register.cjs.js.map +1 -0
- package/dist/scaffolder/actions/builtin/catalog/register.examples.cjs.js +28 -0
- package/dist/scaffolder/actions/builtin/catalog/register.examples.cjs.js.map +1 -0
- package/dist/scaffolder/actions/builtin/catalog/write.cjs.js +74 -0
- package/dist/scaffolder/actions/builtin/catalog/write.cjs.js.map +1 -0
- package/dist/scaffolder/actions/builtin/catalog/write.examples.cjs.js +56 -0
- package/dist/scaffolder/actions/builtin/catalog/write.examples.cjs.js.map +1 -0
- package/dist/scaffolder/actions/builtin/createBuiltinActions.cjs.js +156 -0
- package/dist/scaffolder/actions/builtin/createBuiltinActions.cjs.js.map +1 -0
- package/dist/scaffolder/actions/builtin/debug/log.cjs.js +66 -0
- package/dist/scaffolder/actions/builtin/debug/log.cjs.js.map +1 -0
- package/dist/scaffolder/actions/builtin/debug/log.examples.cjs.js +58 -0
- package/dist/scaffolder/actions/builtin/debug/log.examples.cjs.js.map +1 -0
- package/dist/scaffolder/actions/builtin/debug/wait.cjs.js +66 -0
- package/dist/scaffolder/actions/builtin/debug/wait.cjs.js.map +1 -0
- package/dist/scaffolder/actions/builtin/debug/wait.examples.cjs.js +58 -0
- package/dist/scaffolder/actions/builtin/debug/wait.examples.cjs.js.map +1 -0
- package/dist/scaffolder/actions/builtin/fetch/plain.cjs.js +56 -0
- package/dist/scaffolder/actions/builtin/fetch/plain.cjs.js.map +1 -0
- package/dist/scaffolder/actions/builtin/fetch/plain.examples.cjs.js +44 -0
- package/dist/scaffolder/actions/builtin/fetch/plain.examples.cjs.js.map +1 -0
- package/dist/scaffolder/actions/builtin/fetch/plainFile.cjs.js +56 -0
- package/dist/scaffolder/actions/builtin/fetch/plainFile.cjs.js.map +1 -0
- package/dist/scaffolder/actions/builtin/fetch/plainFile.examples.cjs.js +29 -0
- package/dist/scaffolder/actions/builtin/fetch/plainFile.examples.cjs.js.map +1 -0
- package/dist/scaffolder/actions/builtin/fetch/template.cjs.js +241 -0
- package/dist/scaffolder/actions/builtin/fetch/template.cjs.js.map +1 -0
- package/dist/scaffolder/actions/builtin/fetch/template.examples.cjs.js +35 -0
- package/dist/scaffolder/actions/builtin/fetch/template.examples.cjs.js.map +1 -0
- package/dist/scaffolder/actions/builtin/fetch/templateFile.cjs.js +119 -0
- package/dist/scaffolder/actions/builtin/fetch/templateFile.cjs.js.map +1 -0
- package/dist/scaffolder/actions/builtin/fetch/templateFile.examples.cjs.js +34 -0
- package/dist/scaffolder/actions/builtin/fetch/templateFile.examples.cjs.js.map +1 -0
- package/dist/scaffolder/actions/builtin/filesystem/delete.cjs.js +54 -0
- package/dist/scaffolder/actions/builtin/filesystem/delete.cjs.js.map +1 -0
- package/dist/scaffolder/actions/builtin/filesystem/delete.examples.cjs.js +44 -0
- package/dist/scaffolder/actions/builtin/filesystem/delete.examples.cjs.js.map +1 -0
- package/dist/scaffolder/actions/builtin/filesystem/rename.cjs.js +83 -0
- package/dist/scaffolder/actions/builtin/filesystem/rename.cjs.js.map +1 -0
- package/dist/scaffolder/actions/builtin/filesystem/rename.examples.cjs.js +48 -0
- package/dist/scaffolder/actions/builtin/filesystem/rename.examples.cjs.js.map +1 -0
- package/dist/scaffolder/actions/deprecated.cjs.js +74 -0
- package/dist/scaffolder/actions/deprecated.cjs.js.map +1 -0
- package/dist/scaffolder/dryrun/DecoratedActionsRegistry.cjs.js +57 -0
- package/dist/scaffolder/dryrun/DecoratedActionsRegistry.cjs.js.map +1 -0
- package/dist/scaffolder/dryrun/createDryRunner.cjs.js +97 -0
- package/dist/scaffolder/dryrun/createDryRunner.cjs.js.map +1 -0
- package/dist/scaffolder/tasks/DatabaseTaskStore.cjs.js +430 -0
- package/dist/scaffolder/tasks/DatabaseTaskStore.cjs.js.map +1 -0
- package/dist/scaffolder/tasks/DatabaseWorkspaceProvider.cjs.js +22 -0
- package/dist/scaffolder/tasks/DatabaseWorkspaceProvider.cjs.js.map +1 -0
- package/dist/scaffolder/tasks/NunjucksWorkflowRunner.cjs.js +545 -0
- package/dist/scaffolder/tasks/NunjucksWorkflowRunner.cjs.js.map +1 -0
- package/dist/scaffolder/tasks/StorageTaskBroker.cjs.js +318 -0
- package/dist/scaffolder/tasks/StorageTaskBroker.cjs.js.map +1 -0
- package/dist/scaffolder/tasks/TaskWorker.cjs.js +110 -0
- package/dist/scaffolder/tasks/TaskWorker.cjs.js.map +1 -0
- package/dist/scaffolder/tasks/WorkspaceService.cjs.js +50 -0
- package/dist/scaffolder/tasks/WorkspaceService.cjs.js.map +1 -0
- package/dist/scaffolder/tasks/dbUtil.cjs.js +20 -0
- package/dist/scaffolder/tasks/dbUtil.cjs.js.map +1 -0
- package/dist/scaffolder/tasks/helper.cjs.js +46 -0
- package/dist/scaffolder/tasks/helper.cjs.js.map +1 -0
- package/dist/scaffolder/tasks/logger.cjs.js +156 -0
- package/dist/scaffolder/tasks/logger.cjs.js.map +1 -0
- package/dist/scaffolder/tasks/taskRecoveryHelper.cjs.js +18 -0
- package/dist/scaffolder/tasks/taskRecoveryHelper.cjs.js.map +1 -0
- package/dist/service/conditionExports.cjs.js +26 -0
- package/dist/service/conditionExports.cjs.js.map +1 -0
- package/dist/service/helpers.cjs.js +92 -0
- package/dist/service/helpers.cjs.js.map +1 -0
- package/dist/service/router.cjs.js +640 -0
- package/dist/service/router.cjs.js.map +1 -0
- package/dist/service/rules.cjs.js +97 -0
- package/dist/service/rules.cjs.js.map +1 -0
- package/dist/util/checkPermissions.cjs.js +25 -0
- package/dist/util/checkPermissions.cjs.js.map +1 -0
- package/dist/util/metrics.cjs.js +24 -0
- package/dist/util/metrics.cjs.js.map +1 -0
- package/package.json +24 -24
- package/dist/cjs/router-BqZK9yax.cjs.js +0 -4101
- package/dist/cjs/router-BqZK9yax.cjs.js.map +0 -1
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var backendCommon = require('@backstage/backend-common');
|
|
4
|
+
var catalogModel = require('@backstage/catalog-model');
|
|
5
|
+
var config = require('@backstage/config');
|
|
6
|
+
var errors = require('@backstage/errors');
|
|
7
|
+
var integration = require('@backstage/integration');
|
|
8
|
+
var pluginScaffolderCommon = require('@backstage/plugin-scaffolder-common');
|
|
9
|
+
var alpha = require('@backstage/plugin-scaffolder-common/alpha');
|
|
10
|
+
var express = require('express');
|
|
11
|
+
var Router = require('express-promise-router');
|
|
12
|
+
var jsonschema = require('jsonschema');
|
|
13
|
+
var zod = require('zod');
|
|
14
|
+
require('@backstage/plugin-scaffolder-node');
|
|
15
|
+
require('../scaffolder/actions/builtin/catalog/register.examples.cjs.js');
|
|
16
|
+
require('fs-extra');
|
|
17
|
+
require('yaml');
|
|
18
|
+
require('@backstage/backend-plugin-api');
|
|
19
|
+
require('../scaffolder/actions/builtin/catalog/write.examples.cjs.js');
|
|
20
|
+
require('../scaffolder/actions/builtin/catalog/fetch.examples.cjs.js');
|
|
21
|
+
var createBuiltinActions = require('../scaffolder/actions/builtin/createBuiltinActions.cjs.js');
|
|
22
|
+
require('path');
|
|
23
|
+
require('../scaffolder/actions/builtin/debug/log.examples.cjs.js');
|
|
24
|
+
require('fs');
|
|
25
|
+
var luxon = require('luxon');
|
|
26
|
+
require('../scaffolder/actions/builtin/debug/wait.examples.cjs.js');
|
|
27
|
+
require('../scaffolder/actions/builtin/fetch/plain.examples.cjs.js');
|
|
28
|
+
require('../scaffolder/actions/builtin/fetch/plainFile.examples.cjs.js');
|
|
29
|
+
require('globby');
|
|
30
|
+
require('isbinaryfile');
|
|
31
|
+
require('isolated-vm');
|
|
32
|
+
require('lodash/get');
|
|
33
|
+
require('../scaffolder/actions/builtin/fetch/template.examples.cjs.js');
|
|
34
|
+
require('../scaffolder/actions/builtin/fetch/templateFile.examples.cjs.js');
|
|
35
|
+
require('../scaffolder/actions/builtin/filesystem/delete.examples.cjs.js');
|
|
36
|
+
require('../scaffolder/actions/builtin/filesystem/rename.examples.cjs.js');
|
|
37
|
+
require('@backstage/plugin-scaffolder-backend-module-github');
|
|
38
|
+
require('@backstage/plugin-scaffolder-backend-module-gitlab');
|
|
39
|
+
require('@backstage/plugin-scaffolder-backend-module-azure');
|
|
40
|
+
require('@backstage/plugin-scaffolder-backend-module-bitbucket');
|
|
41
|
+
require('@backstage/plugin-scaffolder-backend-module-bitbucket-cloud');
|
|
42
|
+
require('@backstage/plugin-scaffolder-backend-module-bitbucket-server');
|
|
43
|
+
require('@backstage/plugin-scaffolder-backend-module-gerrit');
|
|
44
|
+
var TemplateActionRegistry = require('../scaffolder/actions/TemplateActionRegistry.cjs.js');
|
|
45
|
+
var DatabaseTaskStore = require('../scaffolder/tasks/DatabaseTaskStore.cjs.js');
|
|
46
|
+
var StorageTaskBroker = require('../scaffolder/tasks/StorageTaskBroker.cjs.js');
|
|
47
|
+
var TaskWorker = require('../scaffolder/tasks/TaskWorker.cjs.js');
|
|
48
|
+
var createDryRunner = require('../scaffolder/dryrun/createDryRunner.cjs.js');
|
|
49
|
+
var helpers = require('./helpers.cjs.js');
|
|
50
|
+
var pluginPermissionNode = require('@backstage/plugin-permission-node');
|
|
51
|
+
var rules = require('./rules.cjs.js');
|
|
52
|
+
var checkPermissions = require('../util/checkPermissions.cjs.js');
|
|
53
|
+
|
|
54
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
55
|
+
|
|
56
|
+
var express__default = /*#__PURE__*/_interopDefaultCompat(express);
|
|
57
|
+
var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
|
|
58
|
+
|
|
59
|
+
function isTemplatePermissionRuleInput(permissionRule) {
|
|
60
|
+
return permissionRule.resourceType === alpha.RESOURCE_TYPE_SCAFFOLDER_TEMPLATE;
|
|
61
|
+
}
|
|
62
|
+
function isActionPermissionRuleInput(permissionRule) {
|
|
63
|
+
return permissionRule.resourceType === alpha.RESOURCE_TYPE_SCAFFOLDER_ACTION;
|
|
64
|
+
}
|
|
65
|
+
function isSupportedTemplate(entity) {
|
|
66
|
+
return entity.apiVersion === "scaffolder.backstage.io/v1beta3";
|
|
67
|
+
}
|
|
68
|
+
function buildDefaultIdentityClient(options) {
|
|
69
|
+
return {
|
|
70
|
+
getIdentity: async ({ request }) => {
|
|
71
|
+
const header = request.headers.authorization;
|
|
72
|
+
const { logger } = options;
|
|
73
|
+
if (!header) {
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const token = header.match(/^Bearer\s(\S+\.\S+\.\S+)$/i)?.[1];
|
|
78
|
+
if (!token) {
|
|
79
|
+
throw new TypeError("Expected Bearer with JWT");
|
|
80
|
+
}
|
|
81
|
+
const [_header, rawPayload, _signature] = token.split(".");
|
|
82
|
+
const payload = JSON.parse(
|
|
83
|
+
Buffer.from(rawPayload, "base64").toString()
|
|
84
|
+
);
|
|
85
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
|
86
|
+
throw new TypeError("Malformed JWT payload");
|
|
87
|
+
}
|
|
88
|
+
const sub = payload.sub;
|
|
89
|
+
if (typeof sub !== "string") {
|
|
90
|
+
throw new TypeError("Expected string sub claim");
|
|
91
|
+
}
|
|
92
|
+
if (sub === "backstage-server") {
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
catalogModel.parseEntityRef(sub);
|
|
96
|
+
return {
|
|
97
|
+
identity: {
|
|
98
|
+
userEntityRef: sub,
|
|
99
|
+
ownershipEntityRefs: [],
|
|
100
|
+
type: "user"
|
|
101
|
+
},
|
|
102
|
+
token
|
|
103
|
+
};
|
|
104
|
+
} catch (e) {
|
|
105
|
+
logger.error(`Invalid authorization header: ${errors.stringifyError(e)}`);
|
|
106
|
+
return void 0;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const readDuration = (config$1, key, defaultValue) => {
|
|
112
|
+
if (config$1.has(key)) {
|
|
113
|
+
return config.readDurationFromConfig(config$1, { key });
|
|
114
|
+
}
|
|
115
|
+
return defaultValue;
|
|
116
|
+
};
|
|
117
|
+
async function createRouter(options) {
|
|
118
|
+
const router = Router__default.default();
|
|
119
|
+
router.use(express__default.default.json({ limit: "10MB" }));
|
|
120
|
+
const {
|
|
121
|
+
logger: parentLogger,
|
|
122
|
+
config,
|
|
123
|
+
reader,
|
|
124
|
+
database,
|
|
125
|
+
catalogClient,
|
|
126
|
+
actions,
|
|
127
|
+
taskWorkers,
|
|
128
|
+
scheduler,
|
|
129
|
+
additionalTemplateFilters,
|
|
130
|
+
additionalTemplateGlobals,
|
|
131
|
+
additionalWorkspaceProviders,
|
|
132
|
+
permissions,
|
|
133
|
+
permissionRules,
|
|
134
|
+
discovery = backendCommon.HostDiscovery.fromConfig(config),
|
|
135
|
+
identity = buildDefaultIdentityClient(options),
|
|
136
|
+
autocompleteHandlers = {}
|
|
137
|
+
} = options;
|
|
138
|
+
const { auth, httpAuth } = backendCommon.createLegacyAuthAdapters({
|
|
139
|
+
...options,
|
|
140
|
+
identity,
|
|
141
|
+
discovery
|
|
142
|
+
});
|
|
143
|
+
const concurrentTasksLimit = options.concurrentTasksLimit ?? options.config.getOptionalNumber("scaffolder.concurrentTasksLimit");
|
|
144
|
+
const logger = parentLogger.child({ plugin: "scaffolder" });
|
|
145
|
+
const workingDirectory = await helpers.getWorkingDirectory(config, logger);
|
|
146
|
+
const integrations = integration.ScmIntegrations.fromConfig(config);
|
|
147
|
+
let taskBroker;
|
|
148
|
+
if (!options.taskBroker) {
|
|
149
|
+
const databaseTaskStore = await DatabaseTaskStore.DatabaseTaskStore.create({ database });
|
|
150
|
+
taskBroker = new StorageTaskBroker.StorageTaskBroker(
|
|
151
|
+
databaseTaskStore,
|
|
152
|
+
logger,
|
|
153
|
+
config,
|
|
154
|
+
auth,
|
|
155
|
+
additionalWorkspaceProviders
|
|
156
|
+
);
|
|
157
|
+
if (scheduler && databaseTaskStore.listStaleTasks) {
|
|
158
|
+
await scheduler.scheduleTask({
|
|
159
|
+
id: "close_stale_tasks",
|
|
160
|
+
frequency: readDuration(
|
|
161
|
+
config,
|
|
162
|
+
"scaffolder.taskTimeoutJanitorFrequency",
|
|
163
|
+
{
|
|
164
|
+
minutes: 5
|
|
165
|
+
}
|
|
166
|
+
),
|
|
167
|
+
timeout: { minutes: 15 },
|
|
168
|
+
fn: async () => {
|
|
169
|
+
const { tasks } = await databaseTaskStore.listStaleTasks({
|
|
170
|
+
timeoutS: luxon.Duration.fromObject(
|
|
171
|
+
readDuration(config, "scaffolder.taskTimeout", {
|
|
172
|
+
hours: 24
|
|
173
|
+
})
|
|
174
|
+
).as("seconds")
|
|
175
|
+
});
|
|
176
|
+
for (const task of tasks) {
|
|
177
|
+
await databaseTaskStore.shutdownTask(task);
|
|
178
|
+
logger.info(`Successfully closed stale task ${task.taskId}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
taskBroker = options.taskBroker;
|
|
185
|
+
}
|
|
186
|
+
const actionRegistry = new TemplateActionRegistry.TemplateActionRegistry();
|
|
187
|
+
const workers = [];
|
|
188
|
+
if (concurrentTasksLimit !== 0) {
|
|
189
|
+
for (let i = 0; i < (taskWorkers || 1); i++) {
|
|
190
|
+
const worker = await TaskWorker.TaskWorker.create({
|
|
191
|
+
taskBroker,
|
|
192
|
+
actionRegistry,
|
|
193
|
+
integrations,
|
|
194
|
+
logger,
|
|
195
|
+
workingDirectory,
|
|
196
|
+
additionalTemplateFilters,
|
|
197
|
+
additionalTemplateGlobals,
|
|
198
|
+
concurrentTasksLimit,
|
|
199
|
+
permissions
|
|
200
|
+
});
|
|
201
|
+
workers.push(worker);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const actionsToRegister = Array.isArray(actions) ? actions : createBuiltinActions.createBuiltinActions({
|
|
205
|
+
integrations,
|
|
206
|
+
catalogClient,
|
|
207
|
+
reader,
|
|
208
|
+
config,
|
|
209
|
+
additionalTemplateFilters,
|
|
210
|
+
additionalTemplateGlobals,
|
|
211
|
+
auth
|
|
212
|
+
});
|
|
213
|
+
actionsToRegister.forEach((action) => actionRegistry.register(action));
|
|
214
|
+
const launchWorkers = () => workers.forEach((worker) => worker.start());
|
|
215
|
+
const shutdownWorkers = () => {
|
|
216
|
+
workers.forEach((worker) => worker.stop());
|
|
217
|
+
};
|
|
218
|
+
if (options.lifecycle) {
|
|
219
|
+
options.lifecycle.addStartupHook(launchWorkers);
|
|
220
|
+
options.lifecycle.addShutdownHook(shutdownWorkers);
|
|
221
|
+
} else {
|
|
222
|
+
launchWorkers();
|
|
223
|
+
}
|
|
224
|
+
const dryRunner = createDryRunner.createDryRunner({
|
|
225
|
+
actionRegistry,
|
|
226
|
+
integrations,
|
|
227
|
+
logger,
|
|
228
|
+
workingDirectory,
|
|
229
|
+
additionalTemplateFilters,
|
|
230
|
+
additionalTemplateGlobals,
|
|
231
|
+
permissions
|
|
232
|
+
});
|
|
233
|
+
const templateRules = Object.values(
|
|
234
|
+
rules.scaffolderTemplateRules
|
|
235
|
+
);
|
|
236
|
+
const actionRules = Object.values(
|
|
237
|
+
rules.scaffolderActionRules
|
|
238
|
+
);
|
|
239
|
+
if (permissionRules) {
|
|
240
|
+
templateRules.push(
|
|
241
|
+
...permissionRules.filter(isTemplatePermissionRuleInput)
|
|
242
|
+
);
|
|
243
|
+
actionRules.push(...permissionRules.filter(isActionPermissionRuleInput));
|
|
244
|
+
}
|
|
245
|
+
const isAuthorized = pluginPermissionNode.createConditionAuthorizer(Object.values(templateRules));
|
|
246
|
+
const permissionIntegrationRouter = pluginPermissionNode.createPermissionIntegrationRouter({
|
|
247
|
+
resources: [
|
|
248
|
+
{
|
|
249
|
+
resourceType: alpha.RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,
|
|
250
|
+
permissions: alpha.scaffolderTemplatePermissions,
|
|
251
|
+
rules: templateRules
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
resourceType: alpha.RESOURCE_TYPE_SCAFFOLDER_ACTION,
|
|
255
|
+
permissions: alpha.scaffolderActionPermissions,
|
|
256
|
+
rules: actionRules
|
|
257
|
+
}
|
|
258
|
+
],
|
|
259
|
+
permissions: alpha.scaffolderTaskPermissions
|
|
260
|
+
});
|
|
261
|
+
router.use(permissionIntegrationRouter);
|
|
262
|
+
router.get(
|
|
263
|
+
"/v2/templates/:namespace/:kind/:name/parameter-schema",
|
|
264
|
+
async (req, res) => {
|
|
265
|
+
const credentials = await httpAuth.credentials(req);
|
|
266
|
+
const { token } = await auth.getPluginRequestToken({
|
|
267
|
+
onBehalfOf: credentials,
|
|
268
|
+
targetPluginId: "catalog"
|
|
269
|
+
});
|
|
270
|
+
const template = await authorizeTemplate(
|
|
271
|
+
req.params,
|
|
272
|
+
token,
|
|
273
|
+
credentials
|
|
274
|
+
);
|
|
275
|
+
const parameters = [template.spec.parameters ?? []].flat();
|
|
276
|
+
const presentation = template.spec.presentation;
|
|
277
|
+
res.json({
|
|
278
|
+
title: template.metadata.title ?? template.metadata.name,
|
|
279
|
+
...presentation ? { presentation } : {},
|
|
280
|
+
description: template.metadata.description,
|
|
281
|
+
"ui:options": template.metadata["ui:options"],
|
|
282
|
+
steps: parameters.map((schema) => ({
|
|
283
|
+
title: schema.title ?? "Please enter the following information",
|
|
284
|
+
description: schema.description,
|
|
285
|
+
schema
|
|
286
|
+
}))
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
).get("/v2/actions", async (_req, res) => {
|
|
290
|
+
const actionsList = actionRegistry.list().map((action) => {
|
|
291
|
+
return {
|
|
292
|
+
id: action.id,
|
|
293
|
+
description: action.description,
|
|
294
|
+
examples: action.examples,
|
|
295
|
+
schema: action.schema
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
res.json(actionsList);
|
|
299
|
+
}).post("/v2/tasks", async (req, res) => {
|
|
300
|
+
const templateRef = req.body.templateRef;
|
|
301
|
+
const { kind, namespace, name } = catalogModel.parseEntityRef(templateRef, {
|
|
302
|
+
defaultKind: "template"
|
|
303
|
+
});
|
|
304
|
+
const credentials = await httpAuth.credentials(req);
|
|
305
|
+
await checkPermissions.checkPermission({
|
|
306
|
+
credentials,
|
|
307
|
+
permissions: [alpha.taskCreatePermission],
|
|
308
|
+
permissionService: permissions
|
|
309
|
+
});
|
|
310
|
+
const { token } = await auth.getPluginRequestToken({
|
|
311
|
+
onBehalfOf: credentials,
|
|
312
|
+
targetPluginId: "catalog"
|
|
313
|
+
});
|
|
314
|
+
const userEntityRef = auth.isPrincipal(credentials, "user") ? credentials.principal.userEntityRef : void 0;
|
|
315
|
+
const userEntity = userEntityRef ? await catalogClient.getEntityByRef(userEntityRef, { token }) : void 0;
|
|
316
|
+
let auditLog = `Scaffolding task for ${templateRef}`;
|
|
317
|
+
if (userEntityRef) {
|
|
318
|
+
auditLog += ` created by ${userEntityRef}`;
|
|
319
|
+
}
|
|
320
|
+
logger.info(auditLog);
|
|
321
|
+
const values = req.body.values;
|
|
322
|
+
const template = await authorizeTemplate(
|
|
323
|
+
{ kind, namespace, name },
|
|
324
|
+
token,
|
|
325
|
+
credentials
|
|
326
|
+
);
|
|
327
|
+
for (const parameters of [template.spec.parameters ?? []].flat()) {
|
|
328
|
+
const result2 = jsonschema.validate(values, parameters);
|
|
329
|
+
if (!result2.valid) {
|
|
330
|
+
res.status(400).json({ errors: result2.errors });
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const baseUrl = helpers.getEntityBaseUrl(template);
|
|
335
|
+
const taskSpec = {
|
|
336
|
+
apiVersion: template.apiVersion,
|
|
337
|
+
steps: template.spec.steps.map((step, index) => ({
|
|
338
|
+
...step,
|
|
339
|
+
id: step.id ?? `step-${index + 1}`,
|
|
340
|
+
name: step.name ?? step.action
|
|
341
|
+
})),
|
|
342
|
+
EXPERIMENTAL_recovery: template.spec.EXPERIMENTAL_recovery,
|
|
343
|
+
output: template.spec.output ?? {},
|
|
344
|
+
parameters: values,
|
|
345
|
+
user: {
|
|
346
|
+
entity: userEntity,
|
|
347
|
+
ref: userEntityRef
|
|
348
|
+
},
|
|
349
|
+
templateInfo: {
|
|
350
|
+
entityRef: catalogModel.stringifyEntityRef({ kind, name, namespace }),
|
|
351
|
+
baseUrl,
|
|
352
|
+
entity: {
|
|
353
|
+
metadata: template.metadata
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
const secrets = {
|
|
358
|
+
...req.body.secrets,
|
|
359
|
+
backstageToken: token,
|
|
360
|
+
__initiatorCredentials: JSON.stringify(credentials)
|
|
361
|
+
};
|
|
362
|
+
const result = await taskBroker.dispatch({
|
|
363
|
+
spec: taskSpec,
|
|
364
|
+
createdBy: userEntityRef,
|
|
365
|
+
secrets
|
|
366
|
+
});
|
|
367
|
+
res.status(201).json({ id: result.taskId });
|
|
368
|
+
}).get("/v2/tasks", async (req, res) => {
|
|
369
|
+
const credentials = await httpAuth.credentials(req);
|
|
370
|
+
await checkPermissions.checkPermission({
|
|
371
|
+
credentials,
|
|
372
|
+
permissions: [alpha.taskReadPermission],
|
|
373
|
+
permissionService: permissions
|
|
374
|
+
});
|
|
375
|
+
if (!taskBroker.list) {
|
|
376
|
+
throw new Error(
|
|
377
|
+
"TaskBroker does not support listing tasks, please implement the list method on the TaskBroker."
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
const createdBy = helpers.parseStringsParam(req.query.createdBy, "createdBy");
|
|
381
|
+
const status = helpers.parseStringsParam(req.query.status, "status");
|
|
382
|
+
const order = helpers.parseStringsParam(req.query.order, "order")?.map((item) => {
|
|
383
|
+
const match = item.match(/^(asc|desc):(.+)$/);
|
|
384
|
+
if (!match) {
|
|
385
|
+
throw new errors.InputError(
|
|
386
|
+
`Invalid order parameter "${item}", expected "<asc or desc>:<field name>"`
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
order: match[1],
|
|
391
|
+
field: match[2]
|
|
392
|
+
};
|
|
393
|
+
});
|
|
394
|
+
const limit = helpers.parseNumberParam(req.query.limit, "limit");
|
|
395
|
+
const offset = helpers.parseNumberParam(req.query.offset, "offset");
|
|
396
|
+
const tasks = await taskBroker.list({
|
|
397
|
+
filters: {
|
|
398
|
+
createdBy,
|
|
399
|
+
status: status ? status : void 0
|
|
400
|
+
},
|
|
401
|
+
order,
|
|
402
|
+
pagination: {
|
|
403
|
+
limit: limit ? limit[0] : void 0,
|
|
404
|
+
offset: offset ? offset[0] : void 0
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
res.status(200).json(tasks);
|
|
408
|
+
}).get("/v2/tasks/:taskId", async (req, res) => {
|
|
409
|
+
const credentials = await httpAuth.credentials(req);
|
|
410
|
+
await checkPermissions.checkPermission({
|
|
411
|
+
credentials,
|
|
412
|
+
permissions: [alpha.taskReadPermission],
|
|
413
|
+
permissionService: permissions
|
|
414
|
+
});
|
|
415
|
+
const { taskId } = req.params;
|
|
416
|
+
const task = await taskBroker.get(taskId);
|
|
417
|
+
if (!task) {
|
|
418
|
+
throw new errors.NotFoundError(`Task with id ${taskId} does not exist`);
|
|
419
|
+
}
|
|
420
|
+
delete task.secrets;
|
|
421
|
+
res.status(200).json(task);
|
|
422
|
+
}).post("/v2/tasks/:taskId/cancel", async (req, res) => {
|
|
423
|
+
const credentials = await httpAuth.credentials(req);
|
|
424
|
+
await checkPermissions.checkPermission({
|
|
425
|
+
credentials,
|
|
426
|
+
permissions: [alpha.taskCancelPermission, alpha.taskReadPermission],
|
|
427
|
+
permissionService: permissions
|
|
428
|
+
});
|
|
429
|
+
const { taskId } = req.params;
|
|
430
|
+
await taskBroker.cancel?.(taskId);
|
|
431
|
+
res.status(200).json({ status: "cancelled" });
|
|
432
|
+
}).post("/v2/tasks/:taskId/retry", async (req, res) => {
|
|
433
|
+
const credentials = await httpAuth.credentials(req);
|
|
434
|
+
await checkPermissions.checkPermission({
|
|
435
|
+
credentials,
|
|
436
|
+
permissions: [alpha.taskCreatePermission, alpha.taskReadPermission],
|
|
437
|
+
permissionService: permissions
|
|
438
|
+
});
|
|
439
|
+
const { taskId } = req.params;
|
|
440
|
+
await taskBroker.retry?.(taskId);
|
|
441
|
+
res.status(201).json({ id: taskId });
|
|
442
|
+
}).get("/v2/tasks/:taskId/eventstream", async (req, res) => {
|
|
443
|
+
const credentials = await httpAuth.credentials(req);
|
|
444
|
+
await checkPermissions.checkPermission({
|
|
445
|
+
credentials,
|
|
446
|
+
permissions: [alpha.taskReadPermission],
|
|
447
|
+
permissionService: permissions
|
|
448
|
+
});
|
|
449
|
+
const { taskId } = req.params;
|
|
450
|
+
const after = req.query.after !== void 0 ? Number(req.query.after) : void 0;
|
|
451
|
+
logger.debug(`Event stream observing taskId '${taskId}' opened`);
|
|
452
|
+
res.writeHead(200, {
|
|
453
|
+
Connection: "keep-alive",
|
|
454
|
+
"Cache-Control": "no-cache",
|
|
455
|
+
"Content-Type": "text/event-stream"
|
|
456
|
+
});
|
|
457
|
+
const subscription = taskBroker.event$({ taskId, after }).subscribe({
|
|
458
|
+
error: (error) => {
|
|
459
|
+
logger.error(
|
|
460
|
+
`Received error from event stream when observing taskId '${taskId}', ${error}`
|
|
461
|
+
);
|
|
462
|
+
res.end();
|
|
463
|
+
},
|
|
464
|
+
next: ({ events }) => {
|
|
465
|
+
let shouldUnsubscribe = false;
|
|
466
|
+
for (const event of events) {
|
|
467
|
+
res.write(
|
|
468
|
+
`event: ${event.type}
|
|
469
|
+
data: ${JSON.stringify(event)}
|
|
470
|
+
|
|
471
|
+
`
|
|
472
|
+
);
|
|
473
|
+
if (event.type === "completion" && !event.isTaskRecoverable) {
|
|
474
|
+
shouldUnsubscribe = true;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
res.flush?.();
|
|
478
|
+
if (shouldUnsubscribe) {
|
|
479
|
+
subscription.unsubscribe();
|
|
480
|
+
res.end();
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
req.on("close", () => {
|
|
485
|
+
subscription.unsubscribe();
|
|
486
|
+
logger.debug(`Event stream observing taskId '${taskId}' closed`);
|
|
487
|
+
});
|
|
488
|
+
}).get("/v2/tasks/:taskId/events", async (req, res) => {
|
|
489
|
+
const credentials = await httpAuth.credentials(req);
|
|
490
|
+
await checkPermissions.checkPermission({
|
|
491
|
+
credentials,
|
|
492
|
+
permissions: [alpha.taskReadPermission],
|
|
493
|
+
permissionService: permissions
|
|
494
|
+
});
|
|
495
|
+
const { taskId } = req.params;
|
|
496
|
+
const after = Number(req.query.after) || void 0;
|
|
497
|
+
const timeout = setTimeout(() => {
|
|
498
|
+
res.json([]);
|
|
499
|
+
}, 3e4);
|
|
500
|
+
const subscription = taskBroker.event$({ taskId, after }).subscribe({
|
|
501
|
+
error: (error) => {
|
|
502
|
+
logger.error(
|
|
503
|
+
`Received error from event stream when observing taskId '${taskId}', ${error}`
|
|
504
|
+
);
|
|
505
|
+
},
|
|
506
|
+
next: ({ events }) => {
|
|
507
|
+
clearTimeout(timeout);
|
|
508
|
+
subscription.unsubscribe();
|
|
509
|
+
res.json(events);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
req.on("close", () => {
|
|
513
|
+
subscription.unsubscribe();
|
|
514
|
+
clearTimeout(timeout);
|
|
515
|
+
});
|
|
516
|
+
}).post("/v2/dry-run", async (req, res) => {
|
|
517
|
+
const credentials = await httpAuth.credentials(req);
|
|
518
|
+
await checkPermissions.checkPermission({
|
|
519
|
+
credentials,
|
|
520
|
+
permissions: [alpha.taskCreatePermission],
|
|
521
|
+
permissionService: permissions
|
|
522
|
+
});
|
|
523
|
+
const bodySchema = zod.z.object({
|
|
524
|
+
template: zod.z.unknown(),
|
|
525
|
+
values: zod.z.record(zod.z.unknown()),
|
|
526
|
+
secrets: zod.z.record(zod.z.string()).optional(),
|
|
527
|
+
directoryContents: zod.z.array(
|
|
528
|
+
zod.z.object({ path: zod.z.string(), base64Content: zod.z.string() })
|
|
529
|
+
)
|
|
530
|
+
});
|
|
531
|
+
const body = await bodySchema.parseAsync(req.body).catch((e) => {
|
|
532
|
+
throw new errors.InputError(`Malformed request: ${e}`);
|
|
533
|
+
});
|
|
534
|
+
const template = body.template;
|
|
535
|
+
if (!await pluginScaffolderCommon.templateEntityV1beta3Validator.check(template)) {
|
|
536
|
+
throw new errors.InputError("Input template is not a template");
|
|
537
|
+
}
|
|
538
|
+
const { token } = await auth.getPluginRequestToken({
|
|
539
|
+
onBehalfOf: credentials,
|
|
540
|
+
targetPluginId: "catalog"
|
|
541
|
+
});
|
|
542
|
+
const userEntityRef = auth.isPrincipal(credentials, "user") ? credentials.principal.userEntityRef : void 0;
|
|
543
|
+
const userEntity = userEntityRef ? await catalogClient.getEntityByRef(userEntityRef, { token }) : void 0;
|
|
544
|
+
for (const parameters of [template.spec.parameters ?? []].flat()) {
|
|
545
|
+
const result2 = jsonschema.validate(body.values, parameters);
|
|
546
|
+
if (!result2.valid) {
|
|
547
|
+
res.status(400).json({ errors: result2.errors });
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const steps = template.spec.steps.map((step, index) => ({
|
|
552
|
+
...step,
|
|
553
|
+
id: step.id ?? `step-${index + 1}`,
|
|
554
|
+
name: step.name ?? step.action
|
|
555
|
+
}));
|
|
556
|
+
const result = await dryRunner({
|
|
557
|
+
spec: {
|
|
558
|
+
apiVersion: template.apiVersion,
|
|
559
|
+
steps,
|
|
560
|
+
output: template.spec.output ?? {},
|
|
561
|
+
parameters: body.values,
|
|
562
|
+
user: {
|
|
563
|
+
entity: userEntity,
|
|
564
|
+
ref: userEntityRef
|
|
565
|
+
}
|
|
566
|
+
},
|
|
567
|
+
directoryContents: (body.directoryContents ?? []).map((file) => ({
|
|
568
|
+
path: file.path,
|
|
569
|
+
content: Buffer.from(file.base64Content, "base64")
|
|
570
|
+
})),
|
|
571
|
+
secrets: {
|
|
572
|
+
...body.secrets,
|
|
573
|
+
...token && { backstageToken: token }
|
|
574
|
+
},
|
|
575
|
+
credentials
|
|
576
|
+
});
|
|
577
|
+
res.status(200).json({
|
|
578
|
+
...result,
|
|
579
|
+
steps,
|
|
580
|
+
directoryContents: result.directoryContents.map((file) => ({
|
|
581
|
+
path: file.path,
|
|
582
|
+
executable: file.executable,
|
|
583
|
+
base64Content: file.content.toString("base64")
|
|
584
|
+
}))
|
|
585
|
+
});
|
|
586
|
+
}).post("/v2/autocomplete/:provider/:resource", async (req, res) => {
|
|
587
|
+
const { token, context } = req.body;
|
|
588
|
+
const { provider, resource } = req.params;
|
|
589
|
+
if (!token) throw new errors.InputError("Missing token query parameter");
|
|
590
|
+
if (!autocompleteHandlers[provider]) {
|
|
591
|
+
throw new errors.InputError(`Unsupported provider: ${provider}`);
|
|
592
|
+
}
|
|
593
|
+
const { results } = await autocompleteHandlers[provider]({
|
|
594
|
+
resource,
|
|
595
|
+
token,
|
|
596
|
+
context
|
|
597
|
+
});
|
|
598
|
+
res.status(200).json({ results });
|
|
599
|
+
});
|
|
600
|
+
const app = express__default.default();
|
|
601
|
+
app.set("logger", logger);
|
|
602
|
+
app.use("/", router);
|
|
603
|
+
async function authorizeTemplate(entityRef, token, credentials) {
|
|
604
|
+
const template = await helpers.findTemplate({
|
|
605
|
+
catalogApi: catalogClient,
|
|
606
|
+
entityRef,
|
|
607
|
+
token
|
|
608
|
+
});
|
|
609
|
+
if (!isSupportedTemplate(template)) {
|
|
610
|
+
throw new errors.InputError(
|
|
611
|
+
`Unsupported apiVersion field in schema entity, ${template.apiVersion}`
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
if (!permissions) {
|
|
615
|
+
return template;
|
|
616
|
+
}
|
|
617
|
+
const [parameterDecision, stepDecision] = await permissions.authorizeConditional(
|
|
618
|
+
[
|
|
619
|
+
{ permission: alpha.templateParameterReadPermission },
|
|
620
|
+
{ permission: alpha.templateStepReadPermission }
|
|
621
|
+
],
|
|
622
|
+
{ credentials }
|
|
623
|
+
);
|
|
624
|
+
if (Array.isArray(template.spec.parameters)) {
|
|
625
|
+
template.spec.parameters = template.spec.parameters.filter(
|
|
626
|
+
(step) => isAuthorized(parameterDecision, step)
|
|
627
|
+
);
|
|
628
|
+
} else if (template.spec.parameters && !isAuthorized(parameterDecision, template.spec.parameters)) {
|
|
629
|
+
template.spec.parameters = void 0;
|
|
630
|
+
}
|
|
631
|
+
template.spec.steps = template.spec.steps.filter(
|
|
632
|
+
(step) => isAuthorized(stepDecision, step)
|
|
633
|
+
);
|
|
634
|
+
return template;
|
|
635
|
+
}
|
|
636
|
+
return app;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
exports.createRouter = createRouter;
|
|
640
|
+
//# sourceMappingURL=router.cjs.js.map
|