@backstage/backend-defaults 0.16.0-next.1 → 0.16.0

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 CHANGED
@@ -1,5 +1,53 @@
1
1
  # @backstage/backend-defaults
2
2
 
3
+ ## 0.16.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 42960f1: The actions registry invoke endpoint now accepts direct user credentials in addition to service principals, enabling CLI and other direct user clients to invoke actions.
8
+ - 0e7d8f9: The scheduler service now uses the metrics service to create metrics, providing plugin-scoped attribution.
9
+ - 527cf88: **BREAKING** Removed deprecated `BitbucketUrlReader`. Use the `BitbucketCloudUrlReader` or the `BitbucketServerUrlReader` instead.
10
+
11
+ ### Patch Changes
12
+
13
+ - cc8348e: Added permissions integration to the actions registry. Actions registered with a `visibilityPermission` field are now checked against the permissions framework when listing and invoking. Denied actions are filtered from list results, and invoking a denied action returns a `404 Not Found` as if the action does not exist. Permissions are automatically registered with the `PermissionsRegistryService` so they appear in the permission policy system.
14
+ - dee4283: Added `pluginId` field to `ActionsServiceAction` type, populated from the registering plugin's metadata.
15
+ - 015668c: Added `cancelTask` method to the `SchedulerService` interface and implementation, allowing cancellation of currently running scheduled tasks. For global tasks, the database lock is released and a periodic liveness check aborts the running task function. For local tasks, the task's abort signal is triggered directly. A new `POST /.backstage/scheduler/v1/tasks/:id/cancel` endpoint is also available.
16
+ - 638e6c7: chore(deps): bump `yauzl` from 3.2.0 to 3.2.1
17
+ - 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1
18
+ - 62f0a53: Fixed error forwarding in the actions registry so that known errors like `InputError` and `NotFoundError` thrown by actions preserve their original status codes and messages instead of being wrapped in `ForwardedError` and coerced to 500.
19
+ - d933f62: Add configurable throttling and retry mechanism for GitLab integration.
20
+ - b99158a: Fixed `yarn backstage-cli config:check --strict --config app-config.yaml` config validation error by adding
21
+ an optional `default` type discriminator to PostgreSQL connection configuration,
22
+ allowing `config:check` to properly validate `default` connection configurations.
23
+ - 1ee5b28: Adds an alpha `MetricsService` to provide a unified interface for metrics instrumentation across Backstage plugins.
24
+ - 5fcbef2: Updated dependency `express-rate-limit` to `^8.0.0`.
25
+ - a49a40d: Updated dependency `zod` to `^3.25.76 || ^4.0.0` & migrated to `/v3` or `/v4` imports.
26
+ - Updated dependencies
27
+ - @backstage/backend-plugin-api@1.8.0
28
+ - @backstage/cli-node@0.3.0
29
+ - @backstage/integration@2.0.0
30
+ - @backstage/config-loader@1.10.9
31
+ - @backstage/plugin-permission-common@0.9.7
32
+ - @backstage/plugin-permission-node@0.10.11
33
+ - @backstage/plugin-auth-node@0.6.14
34
+ - @backstage/backend-app-api@1.6.0
35
+ - @backstage/plugin-events-node@0.4.20
36
+
37
+ ## 0.16.0-next.2
38
+
39
+ ### Patch Changes
40
+
41
+ - 015668c: Added `cancelTask` method to the `SchedulerService` interface and implementation, allowing cancellation of currently running scheduled tasks. For global tasks, the database lock is released and a periodic liveness check aborts the running task function. For local tasks, the task's abort signal is triggered directly. A new `POST /.backstage/scheduler/v1/tasks/:id/cancel` endpoint is also available.
42
+ - 5fcbef2: Updated dependency `express-rate-limit` to `^8.0.0`.
43
+ - Updated dependencies
44
+ - @backstage/backend-plugin-api@1.8.0-next.1
45
+ - @backstage/integration@2.0.0-next.2
46
+ - @backstage/backend-app-api@1.6.0-next.1
47
+ - @backstage/plugin-auth-node@0.6.14-next.2
48
+ - @backstage/plugin-events-node@0.4.20-next.1
49
+ - @backstage/plugin-permission-node@0.10.11-next.1
50
+
3
51
  ## 0.16.0-next.1
4
52
 
5
53
  ### Minor Changes
@@ -2,9 +2,10 @@
2
2
 
3
3
  var Router = require('express-promise-router');
4
4
  var express = require('express');
5
- var zod = require('zod');
5
+ var v3 = require('zod/v3');
6
6
  var zodToJsonSchema = require('zod-to-json-schema');
7
7
  var errors = require('@backstage/errors');
8
+ var pluginPermissionCommon = require('@backstage/plugin-permission-common');
8
9
 
9
10
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
10
11
 
@@ -17,28 +18,50 @@ class DefaultActionsRegistryService {
17
18
  httpAuth;
18
19
  auth;
19
20
  metadata;
20
- constructor(logger, httpAuth, auth, metadata) {
21
+ permissions;
22
+ permissionsRegistry;
23
+ constructor(logger, httpAuth, auth, metadata, permissions, permissionsRegistry) {
21
24
  this.logger = logger;
22
25
  this.httpAuth = httpAuth;
23
26
  this.auth = auth;
24
27
  this.metadata = metadata;
28
+ this.permissions = permissions;
29
+ this.permissionsRegistry = permissionsRegistry;
25
30
  }
26
31
  static create({
27
32
  httpAuth,
28
33
  logger,
29
34
  auth,
30
- metadata
35
+ metadata,
36
+ permissions,
37
+ permissionsRegistry
31
38
  }) {
32
- return new DefaultActionsRegistryService(logger, httpAuth, auth, metadata);
39
+ return new DefaultActionsRegistryService(
40
+ logger,
41
+ httpAuth,
42
+ auth,
43
+ metadata,
44
+ permissions,
45
+ permissionsRegistry
46
+ );
33
47
  }
34
48
  createRouter() {
35
49
  const router = Router__default.default();
36
50
  router.use(express.json());
37
- router.get("/.backstage/actions/v1/actions", (_, res) => {
51
+ router.get("/.backstage/actions/v1/actions", async (req, res) => {
52
+ const credentials = await this.httpAuth.credentials(req);
53
+ const entries = Array.from(this.actions.entries());
54
+ const allowedActions = await this.filterByPermissions(
55
+ entries,
56
+ credentials
57
+ );
38
58
  return res.json({
39
- actions: Array.from(this.actions.entries()).map(([id, action]) => ({
59
+ actions: allowedActions.map(([id, action]) => ({
40
60
  id,
41
- ...action,
61
+ name: action.name,
62
+ title: action.title,
63
+ description: action.description,
64
+ pluginId: this.metadata.getId(),
42
65
  attributes: {
43
66
  // Inspired by the @modelcontextprotocol/sdk defaults for the hints.
44
67
  // https://github.com/modelcontextprotocol/typescript-sdk/blob/dd69efa1de8646bb6b195ff8d5f52e13739f4550/src/types.ts#L777-L812
@@ -47,8 +70,8 @@ class DefaultActionsRegistryService {
47
70
  readOnly: action.attributes?.readOnly ?? false
48
71
  },
49
72
  schema: {
50
- input: action.schema?.input ? zodToJsonSchema__default.default(action.schema.input(zod.z)) : zodToJsonSchema__default.default(zod.z.object({})),
51
- output: action.schema?.output ? zodToJsonSchema__default.default(action.schema.output(zod.z)) : zodToJsonSchema__default.default(zod.z.object({}))
73
+ input: action.schema?.input ? zodToJsonSchema__default.default(action.schema.input(v3.z)) : zodToJsonSchema__default.default(v3.z.object({})),
74
+ output: action.schema?.output ? zodToJsonSchema__default.default(action.schema.output(v3.z)) : zodToJsonSchema__default.default(v3.z.object({}))
52
75
  }
53
76
  }))
54
77
  });
@@ -57,22 +80,27 @@ class DefaultActionsRegistryService {
57
80
  "/.backstage/actions/v1/actions/:actionId/invoke",
58
81
  async (req, res) => {
59
82
  const credentials = await this.httpAuth.credentials(req);
60
- if (this.auth.isPrincipal(credentials, "user")) {
61
- if (!credentials.principal.actor) {
62
- throw new errors.NotAllowedError(
63
- `Actions must be invoked by a service, not a user`
64
- );
65
- }
66
- } else if (this.auth.isPrincipal(credentials, "none")) {
83
+ if (this.auth.isPrincipal(credentials, "none")) {
67
84
  throw new errors.NotAllowedError(
68
- `Actions must be invoked by a service, not an anonymous request`
85
+ `Actions must be invoked by an authenticated principal, not an anonymous request`
69
86
  );
70
87
  }
71
88
  const action = this.actions.get(req.params.actionId);
72
89
  if (!action) {
73
90
  throw new errors.NotFoundError(`Action "${req.params.actionId}" not found`);
74
91
  }
75
- const input = action.schema?.input ? action.schema.input(zod.z).safeParse(req.body) : { success: true, data: void 0 };
92
+ if (action.visibilityPermission) {
93
+ const [decision] = await this.permissions.authorize(
94
+ [{ permission: action.visibilityPermission }],
95
+ { credentials }
96
+ );
97
+ if (decision.result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
98
+ throw new errors.NotFoundError(
99
+ `Action "${req.params.actionId}" not found`
100
+ );
101
+ }
102
+ }
103
+ const input = action.schema?.input ? action.schema.input(v3.z).safeParse(req.body) : { success: true, data: void 0 };
76
104
  if (!input.success) {
77
105
  throw new errors.InputError(
78
106
  `Invalid input to action "${req.params.actionId}"`,
@@ -84,7 +112,7 @@ class DefaultActionsRegistryService {
84
112
  credentials,
85
113
  logger: this.logger
86
114
  });
87
- const output = action.schema?.output ? action.schema.output(zod.z).safeParse(result?.output) : { success: true, data: result?.output };
115
+ const output = action.schema?.output ? action.schema.output(v3.z).safeParse(result?.output) : { success: true, data: result?.output };
88
116
  if (!output.success) {
89
117
  throw new errors.InputError(
90
118
  `Invalid output from action "${req.params.actionId}"`,
@@ -101,8 +129,29 @@ class DefaultActionsRegistryService {
101
129
  if (this.actions.has(id)) {
102
130
  throw new Error(`Action with id "${id}" is already registered`);
103
131
  }
132
+ if (options.visibilityPermission) {
133
+ this.permissionsRegistry.addPermissions([options.visibilityPermission]);
134
+ }
104
135
  this.actions.set(id, options);
105
136
  }
137
+ async filterByPermissions(entries, credentials) {
138
+ const permissionedEntries = entries.filter(
139
+ ([_, action]) => action.visibilityPermission
140
+ );
141
+ if (permissionedEntries.length === 0) {
142
+ return entries;
143
+ }
144
+ const decisions = await this.permissions.authorize(
145
+ permissionedEntries.map(([_, action]) => ({
146
+ permission: action.visibilityPermission
147
+ })),
148
+ { credentials }
149
+ );
150
+ const deniedIds = new Set(
151
+ permissionedEntries.filter((_, index) => decisions[index].result !== pluginPermissionCommon.AuthorizeResult.ALLOW).map(([id]) => id)
152
+ );
153
+ return entries.filter(([id]) => !deniedIds.has(id));
154
+ }
106
155
  }
107
156
 
108
157
  exports.DefaultActionsRegistryService = DefaultActionsRegistryService;
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultActionsRegistryService.cjs.js","sources":["../../../../src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AuthService,\n HttpAuthService,\n LoggerService,\n PluginMetadataService,\n} from '@backstage/backend-plugin-api';\nimport PromiseRouter from 'express-promise-router';\nimport { Router, json } from 'express';\nimport { z, AnyZodObject } from 'zod';\nimport zodToJsonSchema from 'zod-to-json-schema';\nimport {\n ActionsRegistryActionOptions,\n ActionsRegistryService,\n} from '@backstage/backend-plugin-api/alpha';\nimport { InputError, NotAllowedError, NotFoundError } from '@backstage/errors';\n\nexport class DefaultActionsRegistryService implements ActionsRegistryService {\n private actions: Map<string, ActionsRegistryActionOptions<any, any>> =\n new Map();\n\n private readonly logger: LoggerService;\n private readonly httpAuth: HttpAuthService;\n private readonly auth: AuthService;\n private readonly metadata: PluginMetadataService;\n\n private constructor(\n logger: LoggerService,\n httpAuth: HttpAuthService,\n auth: AuthService,\n metadata: PluginMetadataService,\n ) {\n this.logger = logger;\n this.httpAuth = httpAuth;\n this.auth = auth;\n this.metadata = metadata;\n }\n\n static create({\n httpAuth,\n logger,\n auth,\n metadata,\n }: {\n httpAuth: HttpAuthService;\n logger: LoggerService;\n auth: AuthService;\n metadata: PluginMetadataService;\n }): DefaultActionsRegistryService {\n return new DefaultActionsRegistryService(logger, httpAuth, auth, metadata);\n }\n\n createRouter(): Router {\n const router = PromiseRouter();\n router.use(json());\n\n router.get('/.backstage/actions/v1/actions', (_, res) => {\n return res.json({\n actions: Array.from(this.actions.entries()).map(([id, action]) => ({\n id,\n ...action,\n attributes: {\n // Inspired by the @modelcontextprotocol/sdk defaults for the hints.\n // https://github.com/modelcontextprotocol/typescript-sdk/blob/dd69efa1de8646bb6b195ff8d5f52e13739f4550/src/types.ts#L777-L812\n destructive: action.attributes?.destructive ?? true,\n idempotent: action.attributes?.idempotent ?? false,\n readOnly: action.attributes?.readOnly ?? false,\n },\n schema: {\n input: action.schema?.input\n ? zodToJsonSchema(action.schema.input(z))\n : zodToJsonSchema(z.object({})),\n output: action.schema?.output\n ? zodToJsonSchema(action.schema.output(z))\n : zodToJsonSchema(z.object({})),\n },\n })),\n });\n });\n\n router.post(\n '/.backstage/actions/v1/actions/:actionId/invoke',\n async (req, res) => {\n const credentials = await this.httpAuth.credentials(req);\n if (this.auth.isPrincipal(credentials, 'user')) {\n if (!credentials.principal.actor) {\n throw new NotAllowedError(\n `Actions must be invoked by a service, not a user`,\n );\n }\n } else if (this.auth.isPrincipal(credentials, 'none')) {\n throw new NotAllowedError(\n `Actions must be invoked by a service, not an anonymous request`,\n );\n }\n\n const action = this.actions.get(req.params.actionId);\n\n if (!action) {\n throw new NotFoundError(`Action \"${req.params.actionId}\" not found`);\n }\n\n const input = action.schema?.input\n ? action.schema.input(z).safeParse(req.body)\n : ({ success: true, data: undefined } as const);\n\n if (!input.success) {\n throw new InputError(\n `Invalid input to action \"${req.params.actionId}\"`,\n input.error,\n );\n }\n\n const result = await action.action({\n input: input.data,\n credentials,\n logger: this.logger,\n });\n\n const output = action.schema?.output\n ? action.schema.output(z).safeParse(result?.output)\n : ({ success: true, data: result?.output } as const);\n\n if (!output.success) {\n throw new InputError(\n `Invalid output from action \"${req.params.actionId}\"`,\n output.error,\n );\n }\n\n res.json({ output: output.data });\n },\n );\n return router;\n }\n\n register<\n TInputSchema extends AnyZodObject,\n TOutputSchema extends AnyZodObject,\n >(options: ActionsRegistryActionOptions<TInputSchema, TOutputSchema>): void {\n const id = `${this.metadata.getId()}:${options.name}`;\n\n if (this.actions.has(id)) {\n throw new Error(`Action with id \"${id}\" is already registered`);\n }\n\n this.actions.set(id, options);\n }\n}\n"],"names":["PromiseRouter","json","zodToJsonSchema","z","NotAllowedError","NotFoundError","InputError"],"mappings":"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"}
1
+ {"version":3,"file":"DefaultActionsRegistryService.cjs.js","sources":["../../../../src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AuthService,\n BackstageCredentials,\n HttpAuthService,\n LoggerService,\n PermissionsRegistryService,\n PermissionsService,\n PluginMetadataService,\n} from '@backstage/backend-plugin-api';\nimport PromiseRouter from 'express-promise-router';\nimport { Router, json } from 'express';\nimport { z, AnyZodObject } from 'zod/v3';\nimport zodToJsonSchema from 'zod-to-json-schema';\nimport {\n ActionsRegistryActionOptions,\n ActionsRegistryService,\n} from '@backstage/backend-plugin-api/alpha';\nimport { InputError, NotAllowedError, NotFoundError } from '@backstage/errors';\nimport { AuthorizeResult } from '@backstage/plugin-permission-common';\n\ntype ActionEntry = [string, ActionsRegistryActionOptions<any, any>];\n\nexport class DefaultActionsRegistryService implements ActionsRegistryService {\n private actions: Map<string, ActionsRegistryActionOptions<any, any>> =\n new Map();\n\n private readonly logger: LoggerService;\n private readonly httpAuth: HttpAuthService;\n private readonly auth: AuthService;\n private readonly metadata: PluginMetadataService;\n private readonly permissions: PermissionsService;\n private readonly permissionsRegistry: PermissionsRegistryService;\n\n private constructor(\n logger: LoggerService,\n httpAuth: HttpAuthService,\n auth: AuthService,\n metadata: PluginMetadataService,\n permissions: PermissionsService,\n permissionsRegistry: PermissionsRegistryService,\n ) {\n this.logger = logger;\n this.httpAuth = httpAuth;\n this.auth = auth;\n this.metadata = metadata;\n this.permissions = permissions;\n this.permissionsRegistry = permissionsRegistry;\n }\n\n static create({\n httpAuth,\n logger,\n auth,\n metadata,\n permissions,\n permissionsRegistry,\n }: {\n httpAuth: HttpAuthService;\n logger: LoggerService;\n auth: AuthService;\n metadata: PluginMetadataService;\n permissions: PermissionsService;\n permissionsRegistry: PermissionsRegistryService;\n }): DefaultActionsRegistryService {\n return new DefaultActionsRegistryService(\n logger,\n httpAuth,\n auth,\n metadata,\n permissions,\n permissionsRegistry,\n );\n }\n\n createRouter(): Router {\n const router = PromiseRouter();\n router.use(json());\n\n router.get('/.backstage/actions/v1/actions', async (req, res) => {\n const credentials = await this.httpAuth.credentials(req);\n const entries = Array.from(this.actions.entries());\n\n const allowedActions = await this.filterByPermissions(\n entries,\n credentials,\n );\n\n return res.json({\n actions: allowedActions.map(([id, action]) => ({\n id,\n name: action.name,\n title: action.title,\n description: action.description,\n pluginId: this.metadata.getId(),\n attributes: {\n // Inspired by the @modelcontextprotocol/sdk defaults for the hints.\n // https://github.com/modelcontextprotocol/typescript-sdk/blob/dd69efa1de8646bb6b195ff8d5f52e13739f4550/src/types.ts#L777-L812\n destructive: action.attributes?.destructive ?? true,\n idempotent: action.attributes?.idempotent ?? false,\n readOnly: action.attributes?.readOnly ?? false,\n },\n schema: {\n input: action.schema?.input\n ? zodToJsonSchema(action.schema.input(z))\n : zodToJsonSchema(z.object({})),\n output: action.schema?.output\n ? zodToJsonSchema(action.schema.output(z))\n : zodToJsonSchema(z.object({})),\n },\n })),\n });\n });\n\n router.post(\n '/.backstage/actions/v1/actions/:actionId/invoke',\n async (req, res) => {\n const credentials = await this.httpAuth.credentials(req);\n if (this.auth.isPrincipal(credentials, 'none')) {\n throw new NotAllowedError(\n `Actions must be invoked by an authenticated principal, not an anonymous request`,\n );\n }\n\n const action = this.actions.get(req.params.actionId);\n\n if (!action) {\n throw new NotFoundError(`Action \"${req.params.actionId}\" not found`);\n }\n\n if (action.visibilityPermission) {\n const [decision] = await this.permissions.authorize(\n [{ permission: action.visibilityPermission }],\n { credentials },\n );\n if (decision.result !== AuthorizeResult.ALLOW) {\n throw new NotFoundError(\n `Action \"${req.params.actionId}\" not found`,\n );\n }\n }\n\n const input = action.schema?.input\n ? action.schema.input(z).safeParse(req.body)\n : ({ success: true, data: undefined } as const);\n\n if (!input.success) {\n throw new InputError(\n `Invalid input to action \"${req.params.actionId}\"`,\n input.error,\n );\n }\n\n const result = await action.action({\n input: input.data,\n credentials,\n logger: this.logger,\n });\n\n const output = action.schema?.output\n ? action.schema.output(z).safeParse(result?.output)\n : ({ success: true, data: result?.output } as const);\n\n if (!output.success) {\n throw new InputError(\n `Invalid output from action \"${req.params.actionId}\"`,\n output.error,\n );\n }\n\n res.json({ output: output.data });\n },\n );\n return router;\n }\n\n register<\n TInputSchema extends AnyZodObject,\n TOutputSchema extends AnyZodObject,\n >(options: ActionsRegistryActionOptions<TInputSchema, TOutputSchema>): void {\n const id = `${this.metadata.getId()}:${options.name}`;\n\n if (this.actions.has(id)) {\n throw new Error(`Action with id \"${id}\" is already registered`);\n }\n\n if (options.visibilityPermission) {\n this.permissionsRegistry.addPermissions([options.visibilityPermission]);\n }\n\n this.actions.set(id, options);\n }\n\n private async filterByPermissions(\n entries: ActionEntry[],\n credentials: BackstageCredentials,\n ): Promise<ActionEntry[]> {\n const permissionedEntries = entries.filter(\n ([_, action]) => action.visibilityPermission,\n );\n\n if (permissionedEntries.length === 0) {\n return entries;\n }\n\n const decisions = await this.permissions.authorize(\n permissionedEntries.map(([_, action]) => ({\n permission: action.visibilityPermission!,\n })),\n { credentials },\n );\n\n const deniedIds = new Set(\n permissionedEntries\n .filter((_, index) => decisions[index].result !== AuthorizeResult.ALLOW)\n .map(([id]) => id),\n );\n\n return entries.filter(([id]) => !deniedIds.has(id));\n }\n}\n"],"names":["PromiseRouter","json","zodToJsonSchema","z","NotAllowedError","NotFoundError","AuthorizeResult","InputError"],"mappings":"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"}
@@ -11,14 +11,26 @@ const actionsRegistryServiceFactory = backendPluginApi.createServiceFactory({
11
11
  httpRouter: backendPluginApi.coreServices.httpRouter,
12
12
  httpAuth: backendPluginApi.coreServices.httpAuth,
13
13
  logger: backendPluginApi.coreServices.logger,
14
- auth: backendPluginApi.coreServices.auth
14
+ auth: backendPluginApi.coreServices.auth,
15
+ permissions: backendPluginApi.coreServices.permissions,
16
+ permissionsRegistry: backendPluginApi.coreServices.permissionsRegistry
15
17
  },
16
- factory: ({ metadata, httpRouter, httpAuth, logger, auth }) => {
18
+ factory: ({
19
+ metadata,
20
+ httpRouter,
21
+ httpAuth,
22
+ logger,
23
+ auth,
24
+ permissions,
25
+ permissionsRegistry
26
+ }) => {
17
27
  const actionsRegistryService = DefaultActionsRegistryService.DefaultActionsRegistryService.create({
18
28
  httpAuth,
19
29
  logger,
20
30
  auth,
21
- metadata
31
+ metadata,
32
+ permissions,
33
+ permissionsRegistry
22
34
  });
23
35
  httpRouter.use(actionsRegistryService.createRouter());
24
36
  return actionsRegistryService;
@@ -1 +1 @@
1
- {"version":3,"file":"actionsRegistryServiceFactory.cjs.js","sources":["../../../../src/alpha/entrypoints/actionsRegistry/actionsRegistryServiceFactory.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n coreServices,\n createServiceFactory,\n} from '@backstage/backend-plugin-api';\nimport { DefaultActionsRegistryService } from './DefaultActionsRegistryService';\nimport { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha';\n\n/**\n * @public\n */\nexport const actionsRegistryServiceFactory = createServiceFactory({\n service: actionsRegistryServiceRef,\n deps: {\n metadata: coreServices.pluginMetadata,\n httpRouter: coreServices.httpRouter,\n httpAuth: coreServices.httpAuth,\n logger: coreServices.logger,\n auth: coreServices.auth,\n },\n factory: ({ metadata, httpRouter, httpAuth, logger, auth }) => {\n const actionsRegistryService = DefaultActionsRegistryService.create({\n httpAuth,\n logger,\n auth,\n metadata,\n });\n\n httpRouter.use(actionsRegistryService.createRouter());\n\n return actionsRegistryService;\n },\n});\n"],"names":["createServiceFactory","actionsRegistryServiceRef","coreServices","DefaultActionsRegistryService"],"mappings":";;;;;;AA0BO,MAAM,gCAAgCA,qCAAA,CAAqB;AAAA,EAChE,OAAA,EAASC,+BAAA;AAAA,EACT,IAAA,EAAM;AAAA,IACJ,UAAUC,6BAAA,CAAa,cAAA;AAAA,IACvB,YAAYA,6BAAA,CAAa,UAAA;AAAA,IACzB,UAAUA,6BAAA,CAAa,QAAA;AAAA,IACvB,QAAQA,6BAAA,CAAa,MAAA;AAAA,IACrB,MAAMA,6BAAA,CAAa;AAAA,GACrB;AAAA,EACA,OAAA,EAAS,CAAC,EAAE,QAAA,EAAU,YAAY,QAAA,EAAU,MAAA,EAAQ,MAAK,KAAM;AAC7D,IAAA,MAAM,sBAAA,GAAyBC,4DAA8B,MAAA,CAAO;AAAA,MAClE,QAAA;AAAA,MACA,MAAA;AAAA,MACA,IAAA;AAAA,MACA;AAAA,KACD,CAAA;AAED,IAAA,UAAA,CAAW,GAAA,CAAI,sBAAA,CAAuB,YAAA,EAAc,CAAA;AAEpD,IAAA,OAAO,sBAAA;AAAA,EACT;AACF,CAAC;;;;"}
1
+ {"version":3,"file":"actionsRegistryServiceFactory.cjs.js","sources":["../../../../src/alpha/entrypoints/actionsRegistry/actionsRegistryServiceFactory.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n coreServices,\n createServiceFactory,\n} from '@backstage/backend-plugin-api';\nimport { DefaultActionsRegistryService } from './DefaultActionsRegistryService';\nimport { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha';\n\n/**\n * @public\n */\nexport const actionsRegistryServiceFactory = createServiceFactory({\n service: actionsRegistryServiceRef,\n deps: {\n metadata: coreServices.pluginMetadata,\n httpRouter: coreServices.httpRouter,\n httpAuth: coreServices.httpAuth,\n logger: coreServices.logger,\n auth: coreServices.auth,\n permissions: coreServices.permissions,\n permissionsRegistry: coreServices.permissionsRegistry,\n },\n factory: ({\n metadata,\n httpRouter,\n httpAuth,\n logger,\n auth,\n permissions,\n permissionsRegistry,\n }) => {\n const actionsRegistryService = DefaultActionsRegistryService.create({\n httpAuth,\n logger,\n auth,\n metadata,\n permissions,\n permissionsRegistry,\n });\n\n httpRouter.use(actionsRegistryService.createRouter());\n\n return actionsRegistryService;\n },\n});\n"],"names":["createServiceFactory","actionsRegistryServiceRef","coreServices","DefaultActionsRegistryService"],"mappings":";;;;;;AA0BO,MAAM,gCAAgCA,qCAAA,CAAqB;AAAA,EAChE,OAAA,EAASC,+BAAA;AAAA,EACT,IAAA,EAAM;AAAA,IACJ,UAAUC,6BAAA,CAAa,cAAA;AAAA,IACvB,YAAYA,6BAAA,CAAa,UAAA;AAAA,IACzB,UAAUA,6BAAA,CAAa,QAAA;AAAA,IACvB,QAAQA,6BAAA,CAAa,MAAA;AAAA,IACrB,MAAMA,6BAAA,CAAa,IAAA;AAAA,IACnB,aAAaA,6BAAA,CAAa,WAAA;AAAA,IAC1B,qBAAqBA,6BAAA,CAAa;AAAA,GACpC;AAAA,EACA,SAAS,CAAC;AAAA,IACR,QAAA;AAAA,IACA,UAAA;AAAA,IACA,QAAA;AAAA,IACA,MAAA;AAAA,IACA,IAAA;AAAA,IACA,WAAA;AAAA,IACA;AAAA,GACF,KAAM;AACJ,IAAA,MAAM,sBAAA,GAAyBC,4DAA8B,MAAA,CAAO;AAAA,MAClE,QAAA;AAAA,MACA,MAAA;AAAA,MACA,IAAA;AAAA,MACA,QAAA;AAAA,MACA,WAAA;AAAA,MACA;AAAA,KACD,CAAA;AAED,IAAA,UAAA,CAAW,GAAA,CAAI,sBAAA,CAAuB,YAAA,EAAc,CAAA;AAEpD,IAAA,OAAO,sBAAA;AAAA,EACT;AACF,CAAC;;;;"}
@@ -1,10 +1,10 @@
1
1
  'use strict';
2
2
 
3
- var zod = require('zod');
3
+ var v3 = require('zod/v3');
4
4
 
5
- const severityLogLevelMappingsSchema = zod.z.record(
6
- zod.z.enum(["low", "medium", "high", "critical"]),
7
- zod.z.enum(["debug", "info", "warn", "error"])
5
+ const severityLogLevelMappingsSchema = v3.z.record(
6
+ v3.z.enum(["low", "medium", "high", "critical"]),
7
+ v3.z.enum(["debug", "info", "warn", "error"])
8
8
  );
9
9
  const CONFIG_ROOT_KEY = "backend.auditor";
10
10
 
@@ -1 +1 @@
1
- {"version":3,"file":"types.cjs.js","sources":["../../../src/entrypoints/auditor/types.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { z } from 'zod';\n\n/** @internal */\nexport const severityLogLevelMappingsSchema = z.record(\n z.enum(['low', 'medium', 'high', 'critical']),\n z.enum(['debug', 'info', 'warn', 'error']),\n);\n\n/** @internal */\nexport const CONFIG_ROOT_KEY = 'backend.auditor';\n"],"names":["z"],"mappings":";;;;AAmBO,MAAM,iCAAiCA,KAAA,CAAE,MAAA;AAAA,EAC9CA,MAAE,IAAA,CAAK,CAAC,OAAO,QAAA,EAAU,MAAA,EAAQ,UAAU,CAAC,CAAA;AAAA,EAC5CA,MAAE,IAAA,CAAK,CAAC,SAAS,MAAA,EAAQ,MAAA,EAAQ,OAAO,CAAC;AAC3C;AAGO,MAAM,eAAA,GAAkB;;;;;"}
1
+ {"version":3,"file":"types.cjs.js","sources":["../../../src/entrypoints/auditor/types.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { z } from 'zod/v3';\n\n/** @internal */\nexport const severityLogLevelMappingsSchema = z.record(\n z.enum(['low', 'medium', 'high', 'critical']),\n z.enum(['debug', 'info', 'warn', 'error']),\n);\n\n/** @internal */\nexport const CONFIG_ROOT_KEY = 'backend.auditor';\n"],"names":["z"],"mappings":";;;;AAmBO,MAAM,iCAAiCA,IAAA,CAAE,MAAA;AAAA,EAC9CA,KAAE,IAAA,CAAK,CAAC,OAAO,QAAA,EAAU,MAAA,EAAQ,UAAU,CAAC,CAAA;AAAA,EAC5CA,KAAE,IAAA,CAAK,CAAC,SAAS,MAAA,EAAQ,MAAA,EAAQ,OAAO,CAAC;AAC3C;AAGO,MAAM,eAAA,GAAkB;;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"utils.cjs.js","sources":["../../../src/entrypoints/auditor/utils.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { Config } from '@backstage/config';\nimport { InputError } from '@backstage/errors';\nimport { z } from 'zod';\nimport { CONFIG_ROOT_KEY, severityLogLevelMappingsSchema } from './types';\n\n/**\n * Gets the `backend.auditor.severityLogLevelMappings` configuration.\n *\n * @param config - The root Backstage {@link @backstage/config#Config} object.\n * @returns The validated severity-to-log-level mappings.\n * @throws error - {@link @backstage/errors#InputError} if the mapping configuration is invalid.\n */\nexport function getSeverityLogLevelMappings(config: Config) {\n const auditorConfig = config.getOptionalConfig(CONFIG_ROOT_KEY);\n\n const severityLogLevelMappings = {\n low:\n auditorConfig?.getOptionalString('severityLogLevelMappings.low') ??\n 'debug',\n medium:\n auditorConfig?.getOptionalString('severityLogLevelMappings.medium') ??\n 'info',\n high:\n auditorConfig?.getOptionalString('severityLogLevelMappings.high') ??\n 'info',\n critical:\n auditorConfig?.getOptionalString('severityLogLevelMappings.critical') ??\n 'info',\n } as Required<z.infer<typeof severityLogLevelMappingsSchema>>;\n\n const res = severityLogLevelMappingsSchema.safeParse(\n severityLogLevelMappings,\n );\n if (!res.success) {\n const key = res.error.issues.at(0)?.path.at(0) as string;\n const value = (\n res.error.issues.at(0) as unknown as Record<PropertyKey, unknown>\n ).received as string;\n const validKeys = (\n res.error.issues.at(0) as unknown as Record<PropertyKey, unknown>\n ).options as string[];\n throw new InputError(\n `The configuration value for 'backend.auditor.severityLogLevelMappings.${key}' was given an invalid value: '${value}'. Expected one of the following valid values: '${validKeys.join(\n ', ',\n )}'.`,\n );\n }\n\n return severityLogLevelMappings;\n}\n"],"names":["CONFIG_ROOT_KEY","severityLogLevelMappingsSchema","InputError"],"mappings":";;;;;AA4BO,SAAS,4BAA4B,MAAA,EAAgB;AAC1D,EAAA,MAAM,aAAA,GAAgB,MAAA,CAAO,iBAAA,CAAkBA,qBAAe,CAAA;AAE9D,EAAA,MAAM,wBAAA,GAA2B;AAAA,IAC/B,GAAA,EACE,aAAA,EAAe,iBAAA,CAAkB,8BAA8B,CAAA,IAC/D,OAAA;AAAA,IACF,MAAA,EACE,aAAA,EAAe,iBAAA,CAAkB,iCAAiC,CAAA,IAClE,MAAA;AAAA,IACF,IAAA,EACE,aAAA,EAAe,iBAAA,CAAkB,+BAA+B,CAAA,IAChE,MAAA;AAAA,IACF,QAAA,EACE,aAAA,EAAe,iBAAA,CAAkB,mCAAmC,CAAA,IACpE;AAAA,GACJ;AAEA,EAAA,MAAM,MAAMC,oCAAA,CAA+B,SAAA;AAAA,IACzC;AAAA,GACF;AACA,EAAA,IAAI,CAAC,IAAI,OAAA,EAAS;AAChB,IAAA,MAAM,GAAA,GAAM,IAAI,KAAA,CAAM,MAAA,CAAO,GAAG,CAAC,CAAA,EAAG,IAAA,CAAK,EAAA,CAAG,CAAC,CAAA;AAC7C,IAAA,MAAM,QACJ,GAAA,CAAI,KAAA,CAAM,MAAA,CAAO,EAAA,CAAG,CAAC,CAAA,CACrB,QAAA;AACF,IAAA,MAAM,YACJ,GAAA,CAAI,KAAA,CAAM,MAAA,CAAO,EAAA,CAAG,CAAC,CAAA,CACrB,OAAA;AACF,IAAA,MAAM,IAAIC,iBAAA;AAAA,MACR,CAAA,sEAAA,EAAyE,GAAG,CAAA,+BAAA,EAAkC,KAAK,mDAAmD,SAAA,CAAU,IAAA;AAAA,QAC9K;AAAA,OACD,CAAA,EAAA;AAAA,KACH;AAAA,EACF;AAEA,EAAA,OAAO,wBAAA;AACT;;;;"}
1
+ {"version":3,"file":"utils.cjs.js","sources":["../../../src/entrypoints/auditor/utils.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { Config } from '@backstage/config';\nimport { InputError } from '@backstage/errors';\nimport { z } from 'zod/v3';\nimport { CONFIG_ROOT_KEY, severityLogLevelMappingsSchema } from './types';\n\n/**\n * Gets the `backend.auditor.severityLogLevelMappings` configuration.\n *\n * @param config - The root Backstage {@link @backstage/config#Config} object.\n * @returns The validated severity-to-log-level mappings.\n * @throws error - {@link @backstage/errors#InputError} if the mapping configuration is invalid.\n */\nexport function getSeverityLogLevelMappings(config: Config) {\n const auditorConfig = config.getOptionalConfig(CONFIG_ROOT_KEY);\n\n const severityLogLevelMappings = {\n low:\n auditorConfig?.getOptionalString('severityLogLevelMappings.low') ??\n 'debug',\n medium:\n auditorConfig?.getOptionalString('severityLogLevelMappings.medium') ??\n 'info',\n high:\n auditorConfig?.getOptionalString('severityLogLevelMappings.high') ??\n 'info',\n critical:\n auditorConfig?.getOptionalString('severityLogLevelMappings.critical') ??\n 'info',\n } as Required<z.infer<typeof severityLogLevelMappingsSchema>>;\n\n const res = severityLogLevelMappingsSchema.safeParse(\n severityLogLevelMappings,\n );\n if (!res.success) {\n const key = res.error.issues.at(0)?.path.at(0) as string;\n const value = (\n res.error.issues.at(0) as unknown as Record<PropertyKey, unknown>\n ).received as string;\n const validKeys = (\n res.error.issues.at(0) as unknown as Record<PropertyKey, unknown>\n ).options as string[];\n throw new InputError(\n `The configuration value for 'backend.auditor.severityLogLevelMappings.${key}' was given an invalid value: '${value}'. Expected one of the following valid values: '${validKeys.join(\n ', ',\n )}'.`,\n );\n }\n\n return severityLogLevelMappings;\n}\n"],"names":["CONFIG_ROOT_KEY","severityLogLevelMappingsSchema","InputError"],"mappings":";;;;;AA4BO,SAAS,4BAA4B,MAAA,EAAgB;AAC1D,EAAA,MAAM,aAAA,GAAgB,MAAA,CAAO,iBAAA,CAAkBA,qBAAe,CAAA;AAE9D,EAAA,MAAM,wBAAA,GAA2B;AAAA,IAC/B,GAAA,EACE,aAAA,EAAe,iBAAA,CAAkB,8BAA8B,CAAA,IAC/D,OAAA;AAAA,IACF,MAAA,EACE,aAAA,EAAe,iBAAA,CAAkB,iCAAiC,CAAA,IAClE,MAAA;AAAA,IACF,IAAA,EACE,aAAA,EAAe,iBAAA,CAAkB,+BAA+B,CAAA,IAChE,MAAA;AAAA,IACF,QAAA,EACE,aAAA,EAAe,iBAAA,CAAkB,mCAAmC,CAAA,IACpE;AAAA,GACJ;AAEA,EAAA,MAAM,MAAMC,oCAAA,CAA+B,SAAA;AAAA,IACzC;AAAA,GACF;AACA,EAAA,IAAI,CAAC,IAAI,OAAA,EAAS;AAChB,IAAA,MAAM,GAAA,GAAM,IAAI,KAAA,CAAM,MAAA,CAAO,GAAG,CAAC,CAAA,EAAG,IAAA,CAAK,EAAA,CAAG,CAAC,CAAA;AAC7C,IAAA,MAAM,QACJ,GAAA,CAAI,KAAA,CAAM,MAAA,CAAO,EAAA,CAAG,CAAC,CAAA,CACrB,QAAA;AACF,IAAA,MAAM,YACJ,GAAA,CAAI,KAAA,CAAM,MAAA,CAAO,EAAA,CAAG,CAAC,CAAA,CACrB,OAAA;AACF,IAAA,MAAM,IAAIC,iBAAA;AAAA,MACR,CAAA,sEAAA,EAAyE,GAAG,CAAA,+BAAA,EAAkC,KAAK,mDAAmD,SAAA,CAAU,IAAA;AAAA,QAC9K;AAAA,OACD,CAAA,EAAA;AAAA,KACH;AAAA,EACF;AAEA,EAAA,OAAO,wBAAA;AACT;;;;"}
@@ -7,6 +7,7 @@ var util = require('./util.cjs.js');
7
7
 
8
8
  class LocalTaskWorker {
9
9
  abortWait;
10
+ taskAbortController;
10
11
  #taskState = {
11
12
  status: "idle"
12
13
  };
@@ -59,6 +60,12 @@ class LocalTaskWorker {
59
60
  }
60
61
  this.abortWait.abort();
61
62
  }
63
+ cancel() {
64
+ if (!this.taskAbortController) {
65
+ throw new errors.ConflictError(`Task ${this.taskId} is not running`);
66
+ }
67
+ this.taskAbortController.abort();
68
+ }
62
69
  taskState() {
63
70
  return this.#taskState;
64
71
  }
@@ -87,10 +94,10 @@ class LocalTaskWorker {
87
94
  * Makes a single attempt at running the task to completion.
88
95
  */
89
96
  async runOnce(settings, signal) {
90
- const taskAbortController = util.delegateAbortController(signal);
97
+ this.taskAbortController = util.delegateAbortController(signal);
91
98
  const timeoutDuration = luxon.Duration.fromISO(settings.timeoutAfterDuration);
92
99
  const timeoutHandle = setTimeout(() => {
93
- taskAbortController.abort();
100
+ this.taskAbortController?.abort();
94
101
  }, timeoutDuration.as("milliseconds"));
95
102
  this.#taskState = {
96
103
  status: "running",
@@ -103,7 +110,7 @@ class LocalTaskWorker {
103
110
  status: "running"
104
111
  };
105
112
  try {
106
- await this.fn(taskAbortController.signal);
113
+ await this.fn(this.taskAbortController.signal);
107
114
  this.#taskState.lastRunEndedAt = luxon.DateTime.utc().toISO();
108
115
  this.#taskState.lastRunError = void 0;
109
116
  } catch (e) {
@@ -111,7 +118,8 @@ class LocalTaskWorker {
111
118
  this.#taskState.lastRunError = util.serializeError(e);
112
119
  }
113
120
  clearTimeout(timeoutHandle);
114
- taskAbortController.abort();
121
+ this.taskAbortController.abort();
122
+ this.taskAbortController = void 0;
115
123
  }
116
124
  /**
117
125
  * Sleeps until it's time to run the task again.
@@ -1 +1 @@
1
- {"version":3,"file":"LocalTaskWorker.cjs.js","sources":["../../../../src/entrypoints/scheduler/lib/LocalTaskWorker.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { SchedulerServiceTaskFunction } from '@backstage/backend-plugin-api';\nimport { ConflictError } from '@backstage/errors';\nimport { CronTime } from 'cron';\nimport { DateTime, Duration } from 'luxon';\nimport { TaskSettingsV2, TaskApiTasksResponse } from './types';\nimport { delegateAbortController, serializeError, sleep } from './util';\n\n/**\n * Implements tasks that run locally without cross-host collaboration.\n *\n * @private\n */\nexport class LocalTaskWorker {\n private abortWait: AbortController | undefined;\n #taskState: Exclude<TaskApiTasksResponse['taskState'], null> = {\n status: 'idle',\n };\n #workerState: TaskApiTasksResponse['workerState'] = {\n status: 'idle',\n };\n\n private readonly taskId: string;\n private readonly fn: SchedulerServiceTaskFunction;\n private readonly logger: LoggerService;\n\n constructor(\n taskId: string,\n fn: SchedulerServiceTaskFunction,\n logger: LoggerService,\n ) {\n this.taskId = taskId;\n this.fn = fn;\n this.logger = logger;\n }\n\n start(settings: TaskSettingsV2, options: { signal: AbortSignal }) {\n this.logger.info(\n `Registered scheduled task: ${this.taskId}, ${JSON.stringify(settings)}`,\n );\n\n (async () => {\n let attemptNum = 1;\n for (;;) {\n try {\n await this.performInitialWait(settings, options.signal);\n\n while (!options.signal.aborted) {\n const startTime = process.hrtime();\n await this.runOnce(settings, options.signal);\n const timeTaken = process.hrtime(startTime);\n await this.waitUntilNext(\n settings,\n (timeTaken[0] + timeTaken[1] / 1e9) * 1000,\n options.signal,\n );\n }\n\n this.logger.info(`Task worker finished: ${this.taskId}`);\n attemptNum = 0;\n break;\n } catch (e) {\n attemptNum += 1;\n this.logger.warn(\n `Task worker failed unexpectedly, attempt number ${attemptNum}, ${e}`,\n );\n await sleep(Duration.fromObject({ seconds: 1 }));\n }\n }\n })();\n }\n\n trigger(): void {\n if (!this.abortWait) {\n throw new ConflictError(`Task ${this.taskId} is currently running`);\n }\n this.abortWait.abort();\n }\n\n taskState(): TaskApiTasksResponse['taskState'] {\n return this.#taskState;\n }\n\n workerState(): TaskApiTasksResponse['workerState'] {\n return this.#workerState;\n }\n\n /**\n * Does the once-at-startup initial wait, if configured.\n */\n private async performInitialWait(\n settings: TaskSettingsV2,\n signal: AbortSignal,\n ): Promise<void> {\n if (settings.initialDelayDuration) {\n const parsedDuration = Duration.fromISO(settings.initialDelayDuration);\n\n this.#taskState = {\n status: 'idle',\n startsAt: DateTime.utc().plus(parsedDuration).toISO()!,\n lastRunEndedAt: this.#taskState.lastRunEndedAt,\n lastRunError: this.#taskState.lastRunError,\n };\n this.#workerState = {\n status: 'initial-wait',\n };\n\n await this.sleep(parsedDuration, signal);\n }\n }\n\n /**\n * Makes a single attempt at running the task to completion.\n */\n private async runOnce(\n settings: TaskSettingsV2,\n signal: AbortSignal,\n ): Promise<void> {\n // Abort the task execution either if the worker is stopped, or if the\n // task timeout is hit\n const taskAbortController = delegateAbortController(signal);\n const timeoutDuration = Duration.fromISO(settings.timeoutAfterDuration);\n const timeoutHandle = setTimeout(() => {\n taskAbortController.abort();\n }, timeoutDuration.as('milliseconds'));\n\n this.#taskState = {\n status: 'running',\n startedAt: DateTime.utc().toISO()!,\n timesOutAt: DateTime.utc().plus(timeoutDuration).toISO()!,\n lastRunEndedAt: this.#taskState.lastRunEndedAt,\n lastRunError: this.#taskState.lastRunError,\n };\n this.#workerState = {\n status: 'running',\n };\n\n try {\n await this.fn(taskAbortController.signal);\n this.#taskState.lastRunEndedAt = DateTime.utc().toISO()!;\n this.#taskState.lastRunError = undefined;\n } catch (e) {\n this.#taskState.lastRunEndedAt = DateTime.utc().toISO()!;\n this.#taskState.lastRunError = serializeError(e);\n }\n\n // release resources\n clearTimeout(timeoutHandle);\n taskAbortController.abort();\n }\n\n /**\n * Sleeps until it's time to run the task again.\n */\n private async waitUntilNext(\n settings: TaskSettingsV2,\n lastRunMillis: number,\n signal: AbortSignal,\n ) {\n if (signal.aborted) {\n return;\n }\n\n const isCron = !settings.cadence.startsWith('P');\n let dt: number;\n\n if (isCron) {\n const nextRun = +new CronTime(settings.cadence).sendAt().toJSDate();\n dt = nextRun - Date.now();\n } else {\n dt =\n Duration.fromISO(settings.cadence).as('milliseconds') - lastRunMillis;\n }\n\n dt = Math.max(dt, 0);\n const startsAt = DateTime.now().plus(Duration.fromMillis(dt));\n\n this.#taskState = {\n status: 'idle',\n startsAt: startsAt.toISO()!,\n lastRunEndedAt: this.#taskState.lastRunEndedAt,\n lastRunError: this.#taskState.lastRunError,\n };\n this.#workerState = {\n status: 'idle',\n };\n\n this.logger.debug(\n `task: ${this.taskId} will next occur around ${startsAt}`,\n );\n\n await this.sleep(Duration.fromMillis(dt), signal);\n }\n\n private async sleep(\n duration: Duration,\n abortSignal: AbortSignal,\n ): Promise<void> {\n this.abortWait = delegateAbortController(abortSignal);\n await sleep(duration, this.abortWait.signal);\n this.abortWait.abort(); // cleans up resources\n this.abortWait = undefined;\n }\n}\n"],"names":["sleep","Duration","ConflictError","DateTime","delegateAbortController","serializeError","CronTime"],"mappings":"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"}
1
+ {"version":3,"file":"LocalTaskWorker.cjs.js","sources":["../../../../src/entrypoints/scheduler/lib/LocalTaskWorker.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { SchedulerServiceTaskFunction } from '@backstage/backend-plugin-api';\nimport { ConflictError } from '@backstage/errors';\nimport { CronTime } from 'cron';\nimport { DateTime, Duration } from 'luxon';\nimport { TaskSettingsV2, TaskApiTasksResponse } from './types';\nimport { delegateAbortController, serializeError, sleep } from './util';\n\n/**\n * Implements tasks that run locally without cross-host collaboration.\n *\n * @private\n */\nexport class LocalTaskWorker {\n private abortWait: AbortController | undefined;\n private taskAbortController: AbortController | undefined;\n #taskState: Exclude<TaskApiTasksResponse['taskState'], null> = {\n status: 'idle',\n };\n #workerState: TaskApiTasksResponse['workerState'] = {\n status: 'idle',\n };\n\n private readonly taskId: string;\n private readonly fn: SchedulerServiceTaskFunction;\n private readonly logger: LoggerService;\n\n constructor(\n taskId: string,\n fn: SchedulerServiceTaskFunction,\n logger: LoggerService,\n ) {\n this.taskId = taskId;\n this.fn = fn;\n this.logger = logger;\n }\n\n start(settings: TaskSettingsV2, options: { signal: AbortSignal }) {\n this.logger.info(\n `Registered scheduled task: ${this.taskId}, ${JSON.stringify(settings)}`,\n );\n\n (async () => {\n let attemptNum = 1;\n for (;;) {\n try {\n await this.performInitialWait(settings, options.signal);\n\n while (!options.signal.aborted) {\n const startTime = process.hrtime();\n await this.runOnce(settings, options.signal);\n const timeTaken = process.hrtime(startTime);\n await this.waitUntilNext(\n settings,\n (timeTaken[0] + timeTaken[1] / 1e9) * 1000,\n options.signal,\n );\n }\n\n this.logger.info(`Task worker finished: ${this.taskId}`);\n attemptNum = 0;\n break;\n } catch (e) {\n attemptNum += 1;\n this.logger.warn(\n `Task worker failed unexpectedly, attempt number ${attemptNum}, ${e}`,\n );\n await sleep(Duration.fromObject({ seconds: 1 }));\n }\n }\n })();\n }\n\n trigger(): void {\n if (!this.abortWait) {\n throw new ConflictError(`Task ${this.taskId} is currently running`);\n }\n this.abortWait.abort();\n }\n\n cancel(): void {\n if (!this.taskAbortController) {\n throw new ConflictError(`Task ${this.taskId} is not running`);\n }\n this.taskAbortController.abort();\n }\n\n taskState(): TaskApiTasksResponse['taskState'] {\n return this.#taskState;\n }\n\n workerState(): TaskApiTasksResponse['workerState'] {\n return this.#workerState;\n }\n\n /**\n * Does the once-at-startup initial wait, if configured.\n */\n private async performInitialWait(\n settings: TaskSettingsV2,\n signal: AbortSignal,\n ): Promise<void> {\n if (settings.initialDelayDuration) {\n const parsedDuration = Duration.fromISO(settings.initialDelayDuration);\n\n this.#taskState = {\n status: 'idle',\n startsAt: DateTime.utc().plus(parsedDuration).toISO()!,\n lastRunEndedAt: this.#taskState.lastRunEndedAt,\n lastRunError: this.#taskState.lastRunError,\n };\n this.#workerState = {\n status: 'initial-wait',\n };\n\n await this.sleep(parsedDuration, signal);\n }\n }\n\n /**\n * Makes a single attempt at running the task to completion.\n */\n private async runOnce(\n settings: TaskSettingsV2,\n signal: AbortSignal,\n ): Promise<void> {\n // Abort the task execution either if the worker is stopped, or if the\n // task timeout is hit\n this.taskAbortController = delegateAbortController(signal);\n const timeoutDuration = Duration.fromISO(settings.timeoutAfterDuration);\n const timeoutHandle = setTimeout(() => {\n this.taskAbortController?.abort();\n }, timeoutDuration.as('milliseconds'));\n\n this.#taskState = {\n status: 'running',\n startedAt: DateTime.utc().toISO()!,\n timesOutAt: DateTime.utc().plus(timeoutDuration).toISO()!,\n lastRunEndedAt: this.#taskState.lastRunEndedAt,\n lastRunError: this.#taskState.lastRunError,\n };\n this.#workerState = {\n status: 'running',\n };\n\n try {\n await this.fn(this.taskAbortController.signal);\n this.#taskState.lastRunEndedAt = DateTime.utc().toISO()!;\n this.#taskState.lastRunError = undefined;\n } catch (e) {\n this.#taskState.lastRunEndedAt = DateTime.utc().toISO()!;\n this.#taskState.lastRunError = serializeError(e);\n }\n\n // release resources\n clearTimeout(timeoutHandle);\n this.taskAbortController.abort();\n this.taskAbortController = undefined;\n }\n\n /**\n * Sleeps until it's time to run the task again.\n */\n private async waitUntilNext(\n settings: TaskSettingsV2,\n lastRunMillis: number,\n signal: AbortSignal,\n ) {\n if (signal.aborted) {\n return;\n }\n\n const isCron = !settings.cadence.startsWith('P');\n let dt: number;\n\n if (isCron) {\n const nextRun = +new CronTime(settings.cadence).sendAt().toJSDate();\n dt = nextRun - Date.now();\n } else {\n dt =\n Duration.fromISO(settings.cadence).as('milliseconds') - lastRunMillis;\n }\n\n dt = Math.max(dt, 0);\n const startsAt = DateTime.now().plus(Duration.fromMillis(dt));\n\n this.#taskState = {\n status: 'idle',\n startsAt: startsAt.toISO()!,\n lastRunEndedAt: this.#taskState.lastRunEndedAt,\n lastRunError: this.#taskState.lastRunError,\n };\n this.#workerState = {\n status: 'idle',\n };\n\n this.logger.debug(\n `task: ${this.taskId} will next occur around ${startsAt}`,\n );\n\n await this.sleep(Duration.fromMillis(dt), signal);\n }\n\n private async sleep(\n duration: Duration,\n abortSignal: AbortSignal,\n ): Promise<void> {\n this.abortWait = delegateAbortController(abortSignal);\n await sleep(duration, this.abortWait.signal);\n this.abortWait.abort(); // cleans up resources\n this.abortWait = undefined;\n }\n}\n"],"names":["sleep","Duration","ConflictError","DateTime","delegateAbortController","serializeError","CronTime"],"mappings":"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"}
@@ -62,6 +62,15 @@ class PluginTaskSchedulerImpl {
62
62
  const knex = await this.databaseFactory();
63
63
  await TaskWorker.TaskWorker.trigger(knex, id);
64
64
  }
65
+ async cancelTask(id) {
66
+ const localTask = this.localWorkersById.get(id);
67
+ if (localTask) {
68
+ localTask.cancel();
69
+ return;
70
+ }
71
+ const knex = await this.databaseFactory();
72
+ await TaskWorker.TaskWorker.cancel(knex, id);
73
+ }
65
74
  async scheduleTask(task) {
66
75
  util.validateId(task.id);
67
76
  const scope = task.scope ?? "global";
@@ -135,6 +144,14 @@ class PluginTaskSchedulerImpl {
135
144
  res.status(200).end();
136
145
  }
137
146
  );
147
+ router.post(
148
+ "/.backstage/scheduler/v1/tasks/:id/cancel",
149
+ async (req, res) => {
150
+ const { id } = req.params;
151
+ await this.cancelTask(id);
152
+ res.status(200).end();
153
+ }
154
+ );
138
155
  return router;
139
156
  }
140
157
  instrumentedFunction(task, scope) {
@@ -1 +1 @@
1
- {"version":3,"file":"PluginTaskSchedulerImpl.cjs.js","sources":["../../../../src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n LoggerService,\n RootLifecycleService,\n SchedulerService,\n SchedulerServiceTaskDescriptor,\n SchedulerServiceTaskFunction,\n SchedulerServiceTaskInvocationDefinition,\n SchedulerServiceTaskRunner,\n SchedulerServiceTaskScheduleDefinition,\n} from '@backstage/backend-plugin-api';\nimport { trace } from '@opentelemetry/api';\nimport {\n MetricsService,\n MetricsServiceCounter,\n MetricsServiceGauge,\n MetricsServiceHistogram,\n} from '@backstage/backend-plugin-api/alpha';\nimport { Knex } from 'knex';\nimport { Duration } from 'luxon';\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport { LocalTaskWorker } from './LocalTaskWorker';\nimport { TaskWorker } from './TaskWorker';\nimport { TaskSettingsV2, TaskApiTasksResponse } from './types';\nimport { delegateAbortController, TRACER_ID, validateId } from './util';\n\nconst tracer = trace.getTracer(TRACER_ID);\n\n/**\n * Implements the actual task management.\n */\nexport class PluginTaskSchedulerImpl implements SchedulerService {\n private readonly localWorkersById = new Map<string, LocalTaskWorker>();\n private readonly globalWorkersById = new Map<string, TaskWorker>();\n private readonly allScheduledTasks: SchedulerServiceTaskDescriptor[] = [];\n private readonly shutdownInitiated: Promise<boolean>;\n\n private readonly counter: MetricsServiceCounter;\n private readonly duration: MetricsServiceHistogram;\n private readonly lastStarted: MetricsServiceGauge;\n private readonly lastCompleted: MetricsServiceGauge;\n\n private readonly pluginId: string;\n private readonly databaseFactory: () => Promise<Knex>;\n private readonly logger: LoggerService;\n\n constructor(\n pluginId: string,\n databaseFactory: () => Promise<Knex>,\n logger: LoggerService,\n metrics: MetricsService,\n rootLifecycle: RootLifecycleService,\n ) {\n this.pluginId = pluginId;\n this.databaseFactory = databaseFactory;\n this.logger = logger;\n this.counter = metrics.createCounter('backend_tasks.task.runs.count', {\n description: 'Total number of times a task has been run',\n });\n this.duration = metrics.createHistogram(\n 'backend_tasks.task.runs.duration',\n {\n description: 'Histogram of task run durations',\n unit: 'seconds',\n },\n );\n this.lastStarted = metrics.createGauge('backend_tasks.task.runs.started', {\n description: 'Epoch timestamp seconds when the task was last started',\n unit: 'seconds',\n });\n this.lastCompleted = metrics.createGauge(\n 'backend_tasks.task.runs.completed',\n {\n description: 'Epoch timestamp seconds when the task was last completed',\n unit: 'seconds',\n },\n );\n this.shutdownInitiated = new Promise(shutdownInitiated => {\n rootLifecycle.addShutdownHook(() => shutdownInitiated(true));\n });\n }\n\n async triggerTask(id: string): Promise<void> {\n const localTask = this.localWorkersById.get(id);\n if (localTask) {\n localTask.trigger();\n return;\n }\n\n const knex = await this.databaseFactory();\n await TaskWorker.trigger(knex, id);\n }\n\n async scheduleTask(\n task: SchedulerServiceTaskScheduleDefinition &\n SchedulerServiceTaskInvocationDefinition,\n ): Promise<void> {\n validateId(task.id);\n const scope = task.scope ?? 'global';\n\n const settings: TaskSettingsV2 = {\n version: 2,\n cadence: parseDuration(task.frequency),\n initialDelayDuration:\n task.initialDelay && parseDuration(task.initialDelay),\n timeoutAfterDuration: parseDuration(task.timeout),\n };\n\n // Delegated abort controller that will abort either when the provided\n // controller aborts, or when a root lifecycle shutdown happens\n const abortController = delegateAbortController(task.signal);\n this.shutdownInitiated.then(() => abortController.abort());\n\n if (scope === 'global') {\n const knex = await this.databaseFactory();\n const worker = new TaskWorker(\n task.id,\n this.instrumentedFunction(task, scope),\n knex,\n this.logger.child({ task: task.id }),\n );\n await worker.start(settings, { signal: abortController.signal });\n this.globalWorkersById.set(task.id, worker);\n } else {\n const worker = new LocalTaskWorker(\n task.id,\n this.instrumentedFunction(task, scope),\n this.logger.child({ task: task.id }),\n );\n worker.start(settings, { signal: abortController.signal });\n this.localWorkersById.set(task.id, worker);\n }\n\n this.allScheduledTasks.push({\n id: task.id,\n scope: scope,\n settings: settings,\n });\n }\n\n createScheduledTaskRunner(\n schedule: SchedulerServiceTaskScheduleDefinition,\n ): SchedulerServiceTaskRunner {\n return {\n run: async task => {\n await this.scheduleTask({ ...task, ...schedule });\n },\n };\n }\n\n async getScheduledTasks(): Promise<SchedulerServiceTaskDescriptor[]> {\n return this.allScheduledTasks;\n }\n\n getRouter(): express.Router {\n const router = Router();\n\n router.get('/.backstage/scheduler/v1/tasks', async (_, res) => {\n const globalState = await TaskWorker.taskStates(\n await this.databaseFactory(),\n );\n\n const tasks = new Array<TaskApiTasksResponse>();\n for (const task of this.allScheduledTasks) {\n tasks.push({\n taskId: task.id,\n pluginId: this.pluginId,\n scope: task.scope,\n settings: task.settings,\n taskState:\n this.localWorkersById.get(task.id)?.taskState() ??\n globalState.get(task.id) ??\n null,\n workerState:\n this.localWorkersById.get(task.id)?.workerState() ??\n this.globalWorkersById.get(task.id)?.workerState() ??\n null,\n });\n }\n\n res.json({ tasks });\n });\n\n router.post(\n '/.backstage/scheduler/v1/tasks/:id/trigger',\n async (req, res) => {\n const { id } = req.params;\n await this.triggerTask(id);\n res.status(200).end();\n },\n );\n\n return router;\n }\n\n private instrumentedFunction(\n task: SchedulerServiceTaskInvocationDefinition,\n scope: string,\n ): SchedulerServiceTaskFunction {\n return async abort => {\n const labels: Record<string, string> = {\n taskId: task.id,\n scope,\n };\n this.counter.add(1, { ...labels, result: 'started' });\n this.lastStarted.record(Date.now() / 1000, { taskId: task.id });\n\n const startTime = process.hrtime();\n\n try {\n await tracer.startActiveSpan(`task ${task.id}`, async span => {\n try {\n span.setAttributes(labels);\n await task.fn(abort);\n } catch (error) {\n if (error instanceof Error) {\n span.recordException(error);\n }\n throw error;\n } finally {\n span.end();\n }\n });\n labels.result = 'completed';\n } catch (ex) {\n labels.result = 'failed';\n throw ex;\n } finally {\n const delta = process.hrtime(startTime);\n const endTime = delta[0] + delta[1] / 1e9;\n this.counter.add(1, labels);\n this.duration.record(endTime, labels);\n this.lastCompleted.record(Date.now() / 1000, labels);\n }\n };\n }\n}\n\nexport function parseDuration(\n frequency: SchedulerServiceTaskScheduleDefinition['frequency'],\n): string {\n if (typeof frequency === 'object' && 'cron' in frequency) {\n return frequency.cron;\n }\n if (typeof frequency === 'object' && 'trigger' in frequency) {\n return frequency.trigger;\n }\n\n const parsed = Duration.isDuration(frequency)\n ? frequency\n : Duration.fromObject(frequency);\n\n if (!parsed.isValid) {\n throw new Error(\n `Invalid duration, ${parsed.invalidReason}: ${parsed.invalidExplanation}`,\n );\n }\n\n return parsed.toISO()!;\n}\n"],"names":["trace","TRACER_ID","TaskWorker","validateId","delegateAbortController","LocalTaskWorker","Router","Duration"],"mappings":"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"}
1
+ {"version":3,"file":"PluginTaskSchedulerImpl.cjs.js","sources":["../../../../src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n LoggerService,\n RootLifecycleService,\n SchedulerService,\n SchedulerServiceTaskDescriptor,\n SchedulerServiceTaskFunction,\n SchedulerServiceTaskInvocationDefinition,\n SchedulerServiceTaskRunner,\n SchedulerServiceTaskScheduleDefinition,\n} from '@backstage/backend-plugin-api';\nimport { trace } from '@opentelemetry/api';\nimport {\n MetricsService,\n MetricsServiceCounter,\n MetricsServiceGauge,\n MetricsServiceHistogram,\n} from '@backstage/backend-plugin-api/alpha';\nimport { Knex } from 'knex';\nimport { Duration } from 'luxon';\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport { LocalTaskWorker } from './LocalTaskWorker';\nimport { TaskWorker } from './TaskWorker';\nimport { TaskSettingsV2, TaskApiTasksResponse } from './types';\nimport { delegateAbortController, TRACER_ID, validateId } from './util';\n\nconst tracer = trace.getTracer(TRACER_ID);\n\n/**\n * Implements the actual task management.\n */\nexport class PluginTaskSchedulerImpl implements SchedulerService {\n private readonly localWorkersById = new Map<string, LocalTaskWorker>();\n private readonly globalWorkersById = new Map<string, TaskWorker>();\n private readonly allScheduledTasks: SchedulerServiceTaskDescriptor[] = [];\n private readonly shutdownInitiated: Promise<boolean>;\n\n private readonly counter: MetricsServiceCounter;\n private readonly duration: MetricsServiceHistogram;\n private readonly lastStarted: MetricsServiceGauge;\n private readonly lastCompleted: MetricsServiceGauge;\n\n private readonly pluginId: string;\n private readonly databaseFactory: () => Promise<Knex>;\n private readonly logger: LoggerService;\n\n constructor(\n pluginId: string,\n databaseFactory: () => Promise<Knex>,\n logger: LoggerService,\n metrics: MetricsService,\n rootLifecycle: RootLifecycleService,\n ) {\n this.pluginId = pluginId;\n this.databaseFactory = databaseFactory;\n this.logger = logger;\n this.counter = metrics.createCounter('backend_tasks.task.runs.count', {\n description: 'Total number of times a task has been run',\n });\n this.duration = metrics.createHistogram(\n 'backend_tasks.task.runs.duration',\n {\n description: 'Histogram of task run durations',\n unit: 'seconds',\n },\n );\n this.lastStarted = metrics.createGauge('backend_tasks.task.runs.started', {\n description: 'Epoch timestamp seconds when the task was last started',\n unit: 'seconds',\n });\n this.lastCompleted = metrics.createGauge(\n 'backend_tasks.task.runs.completed',\n {\n description: 'Epoch timestamp seconds when the task was last completed',\n unit: 'seconds',\n },\n );\n this.shutdownInitiated = new Promise(shutdownInitiated => {\n rootLifecycle.addShutdownHook(() => shutdownInitiated(true));\n });\n }\n\n async triggerTask(id: string): Promise<void> {\n const localTask = this.localWorkersById.get(id);\n if (localTask) {\n localTask.trigger();\n return;\n }\n\n const knex = await this.databaseFactory();\n await TaskWorker.trigger(knex, id);\n }\n\n async cancelTask(id: string): Promise<void> {\n const localTask = this.localWorkersById.get(id);\n if (localTask) {\n localTask.cancel();\n return;\n }\n\n const knex = await this.databaseFactory();\n await TaskWorker.cancel(knex, id);\n }\n\n async scheduleTask(\n task: SchedulerServiceTaskScheduleDefinition &\n SchedulerServiceTaskInvocationDefinition,\n ): Promise<void> {\n validateId(task.id);\n const scope = task.scope ?? 'global';\n\n const settings: TaskSettingsV2 = {\n version: 2,\n cadence: parseDuration(task.frequency),\n initialDelayDuration:\n task.initialDelay && parseDuration(task.initialDelay),\n timeoutAfterDuration: parseDuration(task.timeout),\n };\n\n // Delegated abort controller that will abort either when the provided\n // controller aborts, or when a root lifecycle shutdown happens\n const abortController = delegateAbortController(task.signal);\n this.shutdownInitiated.then(() => abortController.abort());\n\n if (scope === 'global') {\n const knex = await this.databaseFactory();\n const worker = new TaskWorker(\n task.id,\n this.instrumentedFunction(task, scope),\n knex,\n this.logger.child({ task: task.id }),\n );\n await worker.start(settings, { signal: abortController.signal });\n this.globalWorkersById.set(task.id, worker);\n } else {\n const worker = new LocalTaskWorker(\n task.id,\n this.instrumentedFunction(task, scope),\n this.logger.child({ task: task.id }),\n );\n worker.start(settings, { signal: abortController.signal });\n this.localWorkersById.set(task.id, worker);\n }\n\n this.allScheduledTasks.push({\n id: task.id,\n scope: scope,\n settings: settings,\n });\n }\n\n createScheduledTaskRunner(\n schedule: SchedulerServiceTaskScheduleDefinition,\n ): SchedulerServiceTaskRunner {\n return {\n run: async task => {\n await this.scheduleTask({ ...task, ...schedule });\n },\n };\n }\n\n async getScheduledTasks(): Promise<SchedulerServiceTaskDescriptor[]> {\n return this.allScheduledTasks;\n }\n\n getRouter(): express.Router {\n const router = Router();\n\n router.get('/.backstage/scheduler/v1/tasks', async (_, res) => {\n const globalState = await TaskWorker.taskStates(\n await this.databaseFactory(),\n );\n\n const tasks = new Array<TaskApiTasksResponse>();\n for (const task of this.allScheduledTasks) {\n tasks.push({\n taskId: task.id,\n pluginId: this.pluginId,\n scope: task.scope,\n settings: task.settings,\n taskState:\n this.localWorkersById.get(task.id)?.taskState() ??\n globalState.get(task.id) ??\n null,\n workerState:\n this.localWorkersById.get(task.id)?.workerState() ??\n this.globalWorkersById.get(task.id)?.workerState() ??\n null,\n });\n }\n\n res.json({ tasks });\n });\n\n router.post(\n '/.backstage/scheduler/v1/tasks/:id/trigger',\n async (req, res) => {\n const { id } = req.params;\n await this.triggerTask(id);\n res.status(200).end();\n },\n );\n\n router.post(\n '/.backstage/scheduler/v1/tasks/:id/cancel',\n async (req, res) => {\n const { id } = req.params;\n await this.cancelTask(id);\n res.status(200).end();\n },\n );\n\n return router;\n }\n\n private instrumentedFunction(\n task: SchedulerServiceTaskInvocationDefinition,\n scope: string,\n ): SchedulerServiceTaskFunction {\n return async abort => {\n const labels: Record<string, string> = {\n taskId: task.id,\n scope,\n };\n this.counter.add(1, { ...labels, result: 'started' });\n this.lastStarted.record(Date.now() / 1000, { taskId: task.id });\n\n const startTime = process.hrtime();\n\n try {\n await tracer.startActiveSpan(`task ${task.id}`, async span => {\n try {\n span.setAttributes(labels);\n await task.fn(abort);\n } catch (error) {\n if (error instanceof Error) {\n span.recordException(error);\n }\n throw error;\n } finally {\n span.end();\n }\n });\n labels.result = 'completed';\n } catch (ex) {\n labels.result = 'failed';\n throw ex;\n } finally {\n const delta = process.hrtime(startTime);\n const endTime = delta[0] + delta[1] / 1e9;\n this.counter.add(1, labels);\n this.duration.record(endTime, labels);\n this.lastCompleted.record(Date.now() / 1000, labels);\n }\n };\n }\n}\n\nexport function parseDuration(\n frequency: SchedulerServiceTaskScheduleDefinition['frequency'],\n): string {\n if (typeof frequency === 'object' && 'cron' in frequency) {\n return frequency.cron;\n }\n if (typeof frequency === 'object' && 'trigger' in frequency) {\n return frequency.trigger;\n }\n\n const parsed = Duration.isDuration(frequency)\n ? frequency\n : Duration.fromObject(frequency);\n\n if (!parsed.isValid) {\n throw new Error(\n `Invalid duration, ${parsed.invalidReason}: ${parsed.invalidExplanation}`,\n );\n }\n\n return parsed.toISO()!;\n}\n"],"names":["trace","TRACER_ID","TaskWorker","validateId","delegateAbortController","LocalTaskWorker","Router","Duration"],"mappings":"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"}
@@ -93,6 +93,28 @@ class TaskWorker {
93
93
  throw new errors.ConflictError(`Task ${taskId} is currently running`);
94
94
  }
95
95
  }
96
+ static async cancel(knex, taskId) {
97
+ const [row] = await knex(tables.DB_TASKS_TABLE).where("id", "=", taskId).select("settings_json", "current_run_ticket");
98
+ if (!row) {
99
+ throw new errors.NotFoundError(`Task ${taskId} does not exist`);
100
+ }
101
+ if (!row.current_run_ticket) {
102
+ throw new errors.ConflictError(`Task ${taskId} is not running`);
103
+ }
104
+ const settings = types.taskSettingsV2Schema.parse(JSON.parse(row.settings_json));
105
+ const nextRun = TaskWorker.computeNextRunStartAt(knex, settings);
106
+ const updatedRows = await knex(tables.DB_TASKS_TABLE).where("id", "=", taskId).where("current_run_ticket", "=", row.current_run_ticket).update({
107
+ next_run_start_at: nextRun,
108
+ current_run_ticket: knex.raw("null"),
109
+ current_run_started_at: knex.raw("null"),
110
+ current_run_expires_at: knex.raw("null"),
111
+ last_run_ended_at: knex.fn.now(),
112
+ last_run_error_json: util.serializeError(new Error("Task was cancelled"))
113
+ });
114
+ if (updatedRows < 1) {
115
+ throw new errors.ConflictError(`Task ${taskId} is not running`);
116
+ }
117
+ }
96
118
  static async taskStates(knex) {
97
119
  const rows = await knex(tables.DB_TASKS_TABLE);
98
120
  return new Map(
@@ -143,6 +165,16 @@ class TaskWorker {
143
165
  const timeoutHandle = setTimeout(() => {
144
166
  taskAbortController.abort();
145
167
  }, luxon.Duration.fromISO(taskSettings.timeoutAfterDuration).as("milliseconds"));
168
+ let livenessHandle;
169
+ const scheduleLivenessCheck = () => {
170
+ livenessHandle = setTimeout(async () => {
171
+ await this.checkLiveness(ticket, taskAbortController);
172
+ if (!taskAbortController.signal.aborted) {
173
+ scheduleLivenessCheck();
174
+ }
175
+ }, this.workCheckFrequency.as("milliseconds"));
176
+ };
177
+ scheduleLivenessCheck();
146
178
  try {
147
179
  this.#workerState = {
148
180
  status: "running"
@@ -158,6 +190,7 @@ class TaskWorker {
158
190
  status: "idle"
159
191
  };
160
192
  clearTimeout(timeoutHandle);
193
+ clearTimeout(livenessHandle);
161
194
  }
162
195
  await this.tryReleaseTask(ticket, taskSettings);
163
196
  return { result: "completed" };
@@ -181,7 +214,7 @@ class TaskWorker {
181
214
  if (isCron) {
182
215
  const time = new cron.CronTime(settings.cadence).sendAt().minus({ seconds: 1 }).toUTC();
183
216
  const timeConverted = luxon.DateTime.fromJSDate(time.toJSDate());
184
- nextStartAt = this.nextRunAtRaw(timeConverted);
217
+ nextStartAt = TaskWorker.nextRunAtRaw(this.knex, timeConverted);
185
218
  startAt ||= nextStartAt;
186
219
  } else if (isManual) {
187
220
  nextStartAt = this.knex.raw("null");
@@ -222,6 +255,26 @@ class TaskWorker {
222
255
  }
223
256
  );
224
257
  }
258
+ /**
259
+ * Checks whether the current task ticket is still valid in the database.
260
+ * If the ticket has been cleared (e.g. by cancellation or janitor cleanup),
261
+ * aborts the task execution.
262
+ */
263
+ async checkLiveness(ticket, taskAbortController) {
264
+ try {
265
+ const [row] = await this.knex(tables.DB_TASKS_TABLE).where("id", "=", this.taskId).select("current_run_ticket");
266
+ if (!row || row.current_run_ticket !== ticket) {
267
+ this.logger.info(
268
+ `Task ticket for "${this.taskId}" is no longer valid; aborting execution`
269
+ );
270
+ taskAbortController.abort();
271
+ }
272
+ } catch (e) {
273
+ this.logger.warn(
274
+ `Failed to check liveness for task "${this.taskId}", ${e}`
275
+ );
276
+ }
277
+ }
225
278
  /**
226
279
  * Check if the task is ready to run
227
280
  */
@@ -274,40 +327,35 @@ class TaskWorker {
274
327
  });
275
328
  return rows === 1;
276
329
  }
277
- async tryReleaseTask(ticket, settings, error) {
330
+ static computeNextRunStartAt(knex, settings) {
278
331
  const isManual = settings?.cadence === "manual";
279
332
  const isDuration = settings?.cadence.startsWith("P");
280
333
  const isCron = !isManual && !isDuration;
281
- let nextRun;
282
334
  if (isCron) {
283
335
  const time = new cron.CronTime(settings.cadence).sendAt().toUTC();
284
- this.logger.debug(`task: ${this.taskId} will next occur around ${time}`);
285
336
  const timeConverted = luxon.DateTime.fromJSDate(time.toJSDate());
286
- nextRun = this.nextRunAtRaw(timeConverted);
287
- } else if (isManual) {
288
- nextRun = this.knex.raw("null");
289
- } else {
290
- const dt = luxon.Duration.fromISO(settings.cadence).as("seconds");
291
- this.logger.debug(
292
- `task: ${this.taskId} will next occur around ${luxon.DateTime.now().plus({
293
- seconds: dt
294
- })}`
337
+ return TaskWorker.nextRunAtRaw(knex, timeConverted);
338
+ }
339
+ if (isManual) {
340
+ return knex.raw("null");
341
+ }
342
+ const dt = luxon.Duration.fromISO(settings.cadence).as("seconds");
343
+ if (knex.client.config.client.includes("sqlite3")) {
344
+ return knex.raw(`max(datetime(next_run_start_at, ?), datetime('now'))`, [
345
+ `+${dt} seconds`
346
+ ]);
347
+ }
348
+ if (knex.client.config.client.includes("mysql")) {
349
+ return knex.raw(
350
+ `greatest(next_run_start_at + interval ${dt} second, now())`
295
351
  );
296
- if (this.knex.client.config.client.includes("sqlite3")) {
297
- nextRun = this.knex.raw(
298
- `max(datetime(next_run_start_at, ?), datetime('now'))`,
299
- [`+${dt} seconds`]
300
- );
301
- } else if (this.knex.client.config.client.includes("mysql")) {
302
- nextRun = this.knex.raw(
303
- `greatest(next_run_start_at + interval ${dt} second, now())`
304
- );
305
- } else {
306
- nextRun = this.knex.raw(
307
- `greatest(next_run_start_at + interval '${dt} seconds', now())`
308
- );
309
- }
310
352
  }
353
+ return knex.raw(
354
+ `greatest(next_run_start_at + interval '${dt} seconds', now())`
355
+ );
356
+ }
357
+ async tryReleaseTask(ticket, settings, error) {
358
+ const nextRun = TaskWorker.computeNextRunStartAt(this.knex, settings);
311
359
  const rows = await this.knex(tables.DB_TASKS_TABLE).where("id", "=", this.taskId).where("current_run_ticket", "=", ticket).update({
312
360
  next_run_start_at: nextRun,
313
361
  current_run_ticket: this.knex.raw("null"),
@@ -318,13 +366,14 @@ class TaskWorker {
318
366
  });
319
367
  return rows === 1;
320
368
  }
321
- nextRunAtRaw(time) {
322
- if (this.knex.client.config.client.includes("sqlite3")) {
323
- return this.knex.raw("datetime(?)", [time.toISO()]);
324
- } else if (this.knex.client.config.client.includes("mysql")) {
325
- return this.knex.raw(`?`, [time.toSQL({ includeOffset: false })]);
369
+ static nextRunAtRaw(knex, time) {
370
+ if (knex.client.config.client.includes("sqlite3")) {
371
+ return knex.raw("datetime(?)", [time.toISO()]);
372
+ }
373
+ if (knex.client.config.client.includes("mysql")) {
374
+ return knex.raw(`?`, [time.toSQL({ includeOffset: false })]);
326
375
  }
327
- return this.knex.raw(`?`, [time.toISO()]);
376
+ return knex.raw(`?`, [time.toISO()]);
328
377
  }
329
378
  }
330
379
 
@@ -1 +1 @@
1
- {"version":3,"file":"TaskWorker.cjs.js","sources":["../../../../src/entrypoints/scheduler/lib/TaskWorker.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { ConflictError, NotFoundError } from '@backstage/errors';\nimport { CronTime } from 'cron';\nimport { Knex } from 'knex';\nimport { DateTime, Duration } from 'luxon';\nimport { v4 as uuid } from 'uuid';\nimport { DB_TASKS_TABLE, DbTasksRow } from '../database/tables';\nimport {\n TaskSettingsV2,\n taskSettingsV2Schema,\n TaskApiTasksResponse,\n} from './types';\nimport {\n delegateAbortController,\n nowPlus,\n sleep,\n dbTime,\n serializeError,\n} from './util';\nimport { SchedulerServiceTaskFunction } from '@backstage/backend-plugin-api';\n\nconst DEFAULT_WORK_CHECK_FREQUENCY = Duration.fromObject({ seconds: 5 });\n\n/**\n * Implements tasks that run across worker hosts, with collaborative locking.\n *\n * @private\n */\nexport class TaskWorker {\n #workerState: TaskApiTasksResponse['workerState'] = {\n status: 'idle',\n };\n private readonly taskId: string;\n private readonly fn: SchedulerServiceTaskFunction;\n private readonly knex: Knex;\n private readonly logger: LoggerService;\n private readonly workCheckFrequency: Duration;\n\n constructor(\n taskId: string,\n fn: SchedulerServiceTaskFunction,\n knex: Knex,\n logger: LoggerService,\n workCheckFrequency: Duration = DEFAULT_WORK_CHECK_FREQUENCY,\n ) {\n this.taskId = taskId;\n this.fn = fn;\n this.knex = knex;\n this.logger = logger;\n this.workCheckFrequency = workCheckFrequency;\n }\n\n async start(settings: TaskSettingsV2, options: { signal: AbortSignal }) {\n try {\n await this.persistTask(settings);\n } catch (e) {\n throw new Error(`Failed to persist task, ${e}`);\n }\n\n this.logger.info(\n `Registered scheduled task: ${this.taskId}, ${JSON.stringify(settings)}`,\n );\n\n let workCheckFrequency = this.workCheckFrequency;\n const isDuration = settings?.cadence.startsWith('P');\n if (isDuration) {\n const cadence = Duration.fromISO(settings.cadence);\n if (cadence < workCheckFrequency) {\n workCheckFrequency = cadence;\n }\n }\n\n (async () => {\n let attemptNum = 1;\n for (;;) {\n try {\n await this.performInitialWait(settings, options.signal);\n\n while (!options.signal.aborted) {\n const runResult = await this.runOnce(options.signal);\n if (runResult.result === 'abort') {\n break;\n }\n await sleep(workCheckFrequency, options.signal);\n }\n\n this.logger.info(`Task worker finished: ${this.taskId}`);\n attemptNum = 0;\n break;\n } catch (e) {\n attemptNum += 1;\n this.logger.warn(\n `Task worker failed unexpectedly, attempt number ${attemptNum}, ${e}`,\n );\n await sleep(Duration.fromObject({ seconds: 1 }));\n }\n }\n })();\n }\n\n /**\n * Does the once-at-startup initial wait, if configured.\n */\n private async performInitialWait(\n settings: TaskSettingsV2,\n signal: AbortSignal,\n ): Promise<void> {\n if (settings.initialDelayDuration) {\n this.#workerState = {\n status: 'initial-wait',\n };\n await sleep(Duration.fromISO(settings.initialDelayDuration), signal);\n }\n this.#workerState = {\n status: 'idle',\n };\n }\n\n static async trigger(knex: Knex, taskId: string): Promise<void> {\n // check if task exists\n const rows = await knex<DbTasksRow>(DB_TASKS_TABLE)\n .select(knex.raw(1))\n .where('id', '=', taskId);\n if (rows.length !== 1) {\n throw new NotFoundError(`Task ${taskId} does not exist`);\n }\n\n const updatedRows = await knex<DbTasksRow>(DB_TASKS_TABLE)\n .where('id', '=', taskId)\n .whereNull('current_run_ticket')\n .update({\n next_run_start_at: knex.fn.now(),\n });\n if (updatedRows < 1) {\n throw new ConflictError(`Task ${taskId} is currently running`);\n }\n }\n\n static async taskStates(\n knex: Knex,\n ): Promise<Map<string, TaskApiTasksResponse['taskState']>> {\n const rows = await knex<DbTasksRow>(DB_TASKS_TABLE);\n return new Map(\n rows.map(row => {\n const startedAt = row.current_run_started_at\n ? dbTime(row.current_run_started_at).toISO()!\n : undefined;\n const timesOutAt = row.current_run_expires_at\n ? dbTime(row.current_run_expires_at).toISO()!\n : undefined;\n const startsAt = row.next_run_start_at\n ? dbTime(row.next_run_start_at).toISO()!\n : undefined;\n const lastRunEndedAt = row.last_run_ended_at\n ? dbTime(row.last_run_ended_at).toISO()!\n : undefined;\n const lastRunError = row.last_run_error_json || undefined;\n\n return [\n row.id,\n startedAt\n ? {\n status: 'running',\n startedAt,\n timesOutAt,\n lastRunEndedAt,\n lastRunError,\n }\n : {\n status: 'idle',\n startsAt,\n lastRunEndedAt,\n lastRunError,\n },\n ];\n }),\n );\n }\n\n workerState(): TaskApiTasksResponse['workerState'] {\n return this.#workerState;\n }\n\n /**\n * Makes a single attempt at running the task to completion, if ready.\n *\n * @returns The outcome of the attempt\n */\n private async runOnce(\n signal: AbortSignal,\n ): Promise<\n | { result: 'not-ready-yet' }\n | { result: 'abort' }\n | { result: 'failed' }\n | { result: 'completed' }\n > {\n const findResult = await this.findReadyTask();\n if (\n findResult.result === 'not-ready-yet' ||\n findResult.result === 'abort'\n ) {\n return findResult;\n }\n\n const taskSettings = findResult.settings;\n const ticket = uuid();\n\n const claimed = await this.tryClaimTask(ticket, taskSettings);\n if (!claimed) {\n return { result: 'not-ready-yet' };\n }\n\n // Abort the task execution either if the worker is stopped, or if the\n // task timeout is hit\n const taskAbortController = delegateAbortController(signal);\n const timeoutHandle = setTimeout(() => {\n taskAbortController.abort();\n }, Duration.fromISO(taskSettings.timeoutAfterDuration).as('milliseconds'));\n\n try {\n this.#workerState = {\n status: 'running',\n };\n await this.fn(taskAbortController.signal);\n taskAbortController.abort(); // releases resources\n } catch (e) {\n this.logger.error(e);\n await this.tryReleaseTask(ticket, taskSettings, e);\n return { result: 'failed' };\n } finally {\n this.#workerState = {\n status: 'idle',\n };\n clearTimeout(timeoutHandle);\n }\n\n await this.tryReleaseTask(ticket, taskSettings);\n return { result: 'completed' };\n }\n\n /**\n * Perform the initial store of the task info\n */\n async persistTask(settings: TaskSettingsV2) {\n // Perform an initial parse to ensure that we will definitely be able to\n // read it back again.\n taskSettingsV2Schema.parse(settings);\n\n const isManual = settings?.cadence === 'manual';\n const isDuration = settings?.cadence.startsWith('P');\n const isCron = !isManual && !isDuration;\n\n let startAt: Knex.Raw | undefined;\n let nextStartAt: Knex.Raw | undefined;\n if (settings.initialDelayDuration) {\n startAt = nowPlus(\n Duration.fromISO(settings.initialDelayDuration),\n this.knex,\n );\n }\n\n if (isCron) {\n const time = new CronTime(settings.cadence)\n .sendAt()\n .minus({ seconds: 1 }) // immediately, if \"* * * * * *\"\n .toUTC();\n // We make a conversion here to make typescript happy, because the luxon versions of the cron library and here may not be the same\n const timeConverted = DateTime.fromJSDate(time.toJSDate());\n\n nextStartAt = this.nextRunAtRaw(timeConverted);\n startAt ||= nextStartAt;\n } else if (isManual) {\n nextStartAt = this.knex.raw('null');\n startAt ||= nextStartAt;\n } else {\n startAt ||= this.knex.fn.now();\n nextStartAt = nowPlus(Duration.fromISO(settings.cadence), this.knex);\n }\n\n this.logger.debug(`task: ${this.taskId} configured to run at: ${startAt}`);\n\n // It's OK if the task already exists; if it does, just replace its\n // settings with the new value and start the loop as usual.\n const settingsJson = JSON.stringify(settings);\n await this.knex<DbTasksRow>(DB_TASKS_TABLE)\n .insert({\n id: this.taskId,\n settings_json: settingsJson,\n next_run_start_at: startAt,\n })\n .onConflict('id')\n .merge(\n this.knex.client.config.client.includes('mysql')\n ? {\n settings_json: settingsJson,\n next_run_start_at: this.knex.raw(\n `CASE WHEN ?? < ?? THEN ?? ELSE ?? END`,\n [\n nextStartAt,\n 'next_run_start_at',\n nextStartAt,\n 'next_run_start_at',\n ],\n ),\n }\n : {\n settings_json: this.knex.ref('excluded.settings_json'),\n next_run_start_at: this.knex.raw(\n `CASE WHEN ?? < ?? THEN ?? ELSE ?? END`,\n [\n nextStartAt,\n `${DB_TASKS_TABLE}.next_run_start_at`,\n nextStartAt,\n `${DB_TASKS_TABLE}.next_run_start_at`,\n ],\n ),\n },\n );\n }\n\n /**\n * Check if the task is ready to run\n */\n async findReadyTask(): Promise<\n | { result: 'not-ready-yet' }\n | { result: 'abort' }\n | { result: 'ready'; settings: TaskSettingsV2 }\n > {\n const [row] = await this.knex<DbTasksRow>(DB_TASKS_TABLE)\n .where('id', '=', this.taskId)\n .select({\n settingsJson: 'settings_json',\n ready: this.knex.raw(\n `CASE\n WHEN next_run_start_at <= ? AND current_run_ticket IS NULL THEN TRUE\n ELSE FALSE\n END`,\n [this.knex.fn.now()],\n ),\n });\n\n if (!row) {\n this.logger.info(\n 'No longer able to find task; aborting and assuming that it has been unregistered or expired',\n );\n return { result: 'abort' };\n } else if (!row.ready) {\n return { result: 'not-ready-yet' };\n }\n\n try {\n const obj = JSON.parse(row.settingsJson);\n const settings = taskSettingsV2Schema.parse(obj);\n return { result: 'ready', settings };\n } catch (e) {\n this.logger.info(\n `Task \"${this.taskId}\" is no longer able to parse task settings; aborting and assuming that a ` +\n `newer version of the task has been issued and being handled by other workers, ${e}`,\n );\n return { result: 'abort' };\n }\n }\n\n /**\n * Attempts to claim a task that's ready for execution, on this worker's\n * behalf. We should not attempt to perform the work unless the claim really\n * goes through.\n *\n * @param ticket - A globally unique string that changes for each invocation\n * @param settings - The settings of the task to claim\n * @returns True if it was successfully claimed\n */\n async tryClaimTask(\n ticket: string,\n settings: TaskSettingsV2,\n ): Promise<boolean> {\n const startedAt = this.knex.fn.now();\n const expiresAt = settings.timeoutAfterDuration\n ? nowPlus(Duration.fromISO(settings.timeoutAfterDuration), this.knex)\n : this.knex.raw('null');\n\n const rows = await this.knex<DbTasksRow>(DB_TASKS_TABLE)\n .where('id', '=', this.taskId)\n .whereNull('current_run_ticket')\n .update({\n current_run_ticket: ticket,\n current_run_started_at: startedAt,\n current_run_expires_at: expiresAt,\n });\n\n return rows === 1;\n }\n\n async tryReleaseTask(\n ticket: string,\n settings: TaskSettingsV2,\n error?: Error,\n ): Promise<boolean> {\n const isManual = settings?.cadence === 'manual';\n const isDuration = settings?.cadence.startsWith('P');\n const isCron = !isManual && !isDuration;\n\n let nextRun: Knex.Raw;\n if (isCron) {\n const time = new CronTime(settings.cadence).sendAt().toUTC();\n this.logger.debug(`task: ${this.taskId} will next occur around ${time}`);\n // We make a conversion here to make typescript happy, because the luxon versions of the cron library and here may not be the same\n const timeConverted = DateTime.fromJSDate(time.toJSDate());\n\n nextRun = this.nextRunAtRaw(timeConverted);\n } else if (isManual) {\n nextRun = this.knex.raw('null');\n } else {\n const dt = Duration.fromISO(settings.cadence).as('seconds');\n this.logger.debug(\n `task: ${this.taskId} will next occur around ${DateTime.now().plus({\n seconds: dt,\n })}`,\n );\n\n if (this.knex.client.config.client.includes('sqlite3')) {\n nextRun = this.knex.raw(\n `max(datetime(next_run_start_at, ?), datetime('now'))`,\n [`+${dt} seconds`],\n );\n } else if (this.knex.client.config.client.includes('mysql')) {\n nextRun = this.knex.raw(\n `greatest(next_run_start_at + interval ${dt} second, now())`,\n );\n } else {\n nextRun = this.knex.raw(\n `greatest(next_run_start_at + interval '${dt} seconds', now())`,\n );\n }\n }\n\n const rows = await this.knex<DbTasksRow>(DB_TASKS_TABLE)\n .where('id', '=', this.taskId)\n .where('current_run_ticket', '=', ticket)\n .update({\n next_run_start_at: nextRun,\n current_run_ticket: this.knex.raw('null'),\n current_run_started_at: this.knex.raw('null'),\n current_run_expires_at: this.knex.raw('null'),\n last_run_ended_at: this.knex.fn.now(),\n last_run_error_json: error\n ? serializeError(error)\n : this.knex.raw('null'),\n });\n\n return rows === 1;\n }\n\n private nextRunAtRaw(time: DateTime): Knex.Raw {\n if (this.knex.client.config.client.includes('sqlite3')) {\n return this.knex.raw('datetime(?)', [time.toISO()]);\n } else if (this.knex.client.config.client.includes('mysql')) {\n return this.knex.raw(`?`, [time.toSQL({ includeOffset: false })]);\n }\n return this.knex.raw(`?`, [time.toISO()]);\n }\n}\n"],"names":["Duration","sleep","DB_TASKS_TABLE","NotFoundError","ConflictError","dbTime","uuid","delegateAbortController","taskSettingsV2Schema","nowPlus","CronTime","DateTime","serializeError"],"mappings":"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"}
1
+ {"version":3,"file":"TaskWorker.cjs.js","sources":["../../../../src/entrypoints/scheduler/lib/TaskWorker.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { ConflictError, NotFoundError } from '@backstage/errors';\nimport { CronTime } from 'cron';\nimport { Knex } from 'knex';\nimport { DateTime, Duration } from 'luxon';\nimport { v4 as uuid } from 'uuid';\nimport { DB_TASKS_TABLE, DbTasksRow } from '../database/tables';\nimport {\n TaskSettingsV2,\n taskSettingsV2Schema,\n TaskApiTasksResponse,\n} from './types';\nimport {\n delegateAbortController,\n nowPlus,\n sleep,\n dbTime,\n serializeError,\n} from './util';\nimport { SchedulerServiceTaskFunction } from '@backstage/backend-plugin-api';\n\nconst DEFAULT_WORK_CHECK_FREQUENCY = Duration.fromObject({ seconds: 5 });\n\n/**\n * Implements tasks that run across worker hosts, with collaborative locking.\n *\n * @private\n */\nexport class TaskWorker {\n #workerState: TaskApiTasksResponse['workerState'] = {\n status: 'idle',\n };\n private readonly taskId: string;\n private readonly fn: SchedulerServiceTaskFunction;\n private readonly knex: Knex;\n private readonly logger: LoggerService;\n private readonly workCheckFrequency: Duration;\n\n constructor(\n taskId: string,\n fn: SchedulerServiceTaskFunction,\n knex: Knex,\n logger: LoggerService,\n workCheckFrequency: Duration = DEFAULT_WORK_CHECK_FREQUENCY,\n ) {\n this.taskId = taskId;\n this.fn = fn;\n this.knex = knex;\n this.logger = logger;\n this.workCheckFrequency = workCheckFrequency;\n }\n\n async start(settings: TaskSettingsV2, options: { signal: AbortSignal }) {\n try {\n await this.persistTask(settings);\n } catch (e) {\n throw new Error(`Failed to persist task, ${e}`);\n }\n\n this.logger.info(\n `Registered scheduled task: ${this.taskId}, ${JSON.stringify(settings)}`,\n );\n\n let workCheckFrequency = this.workCheckFrequency;\n const isDuration = settings?.cadence.startsWith('P');\n if (isDuration) {\n const cadence = Duration.fromISO(settings.cadence);\n if (cadence < workCheckFrequency) {\n workCheckFrequency = cadence;\n }\n }\n\n (async () => {\n let attemptNum = 1;\n for (;;) {\n try {\n await this.performInitialWait(settings, options.signal);\n\n while (!options.signal.aborted) {\n const runResult = await this.runOnce(options.signal);\n if (runResult.result === 'abort') {\n break;\n }\n await sleep(workCheckFrequency, options.signal);\n }\n\n this.logger.info(`Task worker finished: ${this.taskId}`);\n attemptNum = 0;\n break;\n } catch (e) {\n attemptNum += 1;\n this.logger.warn(\n `Task worker failed unexpectedly, attempt number ${attemptNum}, ${e}`,\n );\n await sleep(Duration.fromObject({ seconds: 1 }));\n }\n }\n })();\n }\n\n /**\n * Does the once-at-startup initial wait, if configured.\n */\n private async performInitialWait(\n settings: TaskSettingsV2,\n signal: AbortSignal,\n ): Promise<void> {\n if (settings.initialDelayDuration) {\n this.#workerState = {\n status: 'initial-wait',\n };\n await sleep(Duration.fromISO(settings.initialDelayDuration), signal);\n }\n this.#workerState = {\n status: 'idle',\n };\n }\n\n static async trigger(knex: Knex, taskId: string): Promise<void> {\n // check if task exists\n const rows = await knex<DbTasksRow>(DB_TASKS_TABLE)\n .select(knex.raw(1))\n .where('id', '=', taskId);\n if (rows.length !== 1) {\n throw new NotFoundError(`Task ${taskId} does not exist`);\n }\n\n const updatedRows = await knex<DbTasksRow>(DB_TASKS_TABLE)\n .where('id', '=', taskId)\n .whereNull('current_run_ticket')\n .update({\n next_run_start_at: knex.fn.now(),\n });\n if (updatedRows < 1) {\n throw new ConflictError(`Task ${taskId} is currently running`);\n }\n }\n\n static async cancel(knex: Knex, taskId: string): Promise<void> {\n const [row] = await knex<DbTasksRow>(DB_TASKS_TABLE)\n .where('id', '=', taskId)\n .select('settings_json', 'current_run_ticket');\n if (!row) {\n throw new NotFoundError(`Task ${taskId} does not exist`);\n }\n if (!row.current_run_ticket) {\n throw new ConflictError(`Task ${taskId} is not running`);\n }\n\n const settings = taskSettingsV2Schema.parse(JSON.parse(row.settings_json));\n const nextRun = TaskWorker.computeNextRunStartAt(knex, settings);\n\n const updatedRows = await knex<DbTasksRow>(DB_TASKS_TABLE)\n .where('id', '=', taskId)\n .where('current_run_ticket', '=', row.current_run_ticket)\n .update({\n next_run_start_at: nextRun,\n current_run_ticket: knex.raw('null'),\n current_run_started_at: knex.raw('null'),\n current_run_expires_at: knex.raw('null'),\n last_run_ended_at: knex.fn.now(),\n last_run_error_json: serializeError(new Error('Task was cancelled')),\n });\n if (updatedRows < 1) {\n throw new ConflictError(`Task ${taskId} is not running`);\n }\n }\n\n static async taskStates(\n knex: Knex,\n ): Promise<Map<string, TaskApiTasksResponse['taskState']>> {\n const rows = await knex<DbTasksRow>(DB_TASKS_TABLE);\n return new Map(\n rows.map(row => {\n const startedAt = row.current_run_started_at\n ? dbTime(row.current_run_started_at).toISO()!\n : undefined;\n const timesOutAt = row.current_run_expires_at\n ? dbTime(row.current_run_expires_at).toISO()!\n : undefined;\n const startsAt = row.next_run_start_at\n ? dbTime(row.next_run_start_at).toISO()!\n : undefined;\n const lastRunEndedAt = row.last_run_ended_at\n ? dbTime(row.last_run_ended_at).toISO()!\n : undefined;\n const lastRunError = row.last_run_error_json || undefined;\n\n return [\n row.id,\n startedAt\n ? {\n status: 'running',\n startedAt,\n timesOutAt,\n lastRunEndedAt,\n lastRunError,\n }\n : {\n status: 'idle',\n startsAt,\n lastRunEndedAt,\n lastRunError,\n },\n ];\n }),\n );\n }\n\n workerState(): TaskApiTasksResponse['workerState'] {\n return this.#workerState;\n }\n\n /**\n * Makes a single attempt at running the task to completion, if ready.\n *\n * @returns The outcome of the attempt\n */\n private async runOnce(\n signal: AbortSignal,\n ): Promise<\n | { result: 'not-ready-yet' }\n | { result: 'abort' }\n | { result: 'failed' }\n | { result: 'completed' }\n > {\n const findResult = await this.findReadyTask();\n if (\n findResult.result === 'not-ready-yet' ||\n findResult.result === 'abort'\n ) {\n return findResult;\n }\n\n const taskSettings = findResult.settings;\n const ticket = uuid();\n\n const claimed = await this.tryClaimTask(ticket, taskSettings);\n if (!claimed) {\n return { result: 'not-ready-yet' };\n }\n\n // Abort the task execution either if the worker is stopped, or if the\n // task timeout is hit, or if the task ticket was lost (e.g. due to\n // cancellation from another host)\n const taskAbortController = delegateAbortController(signal);\n const timeoutHandle = setTimeout(() => {\n taskAbortController.abort();\n }, Duration.fromISO(taskSettings.timeoutAfterDuration).as('milliseconds'));\n let livenessHandle: ReturnType<typeof setTimeout> | undefined;\n const scheduleLivenessCheck = () => {\n livenessHandle = setTimeout(async () => {\n await this.checkLiveness(ticket, taskAbortController);\n if (!taskAbortController.signal.aborted) {\n scheduleLivenessCheck();\n }\n }, this.workCheckFrequency.as('milliseconds'));\n };\n scheduleLivenessCheck();\n\n try {\n this.#workerState = {\n status: 'running',\n };\n await this.fn(taskAbortController.signal);\n taskAbortController.abort(); // releases resources\n } catch (e) {\n this.logger.error(e);\n await this.tryReleaseTask(ticket, taskSettings, e);\n return { result: 'failed' };\n } finally {\n this.#workerState = {\n status: 'idle',\n };\n clearTimeout(timeoutHandle);\n clearTimeout(livenessHandle);\n }\n\n await this.tryReleaseTask(ticket, taskSettings);\n return { result: 'completed' };\n }\n\n /**\n * Perform the initial store of the task info\n */\n async persistTask(settings: TaskSettingsV2) {\n // Perform an initial parse to ensure that we will definitely be able to\n // read it back again.\n taskSettingsV2Schema.parse(settings);\n\n const isManual = settings?.cadence === 'manual';\n const isDuration = settings?.cadence.startsWith('P');\n const isCron = !isManual && !isDuration;\n\n let startAt: Knex.Raw | undefined;\n let nextStartAt: Knex.Raw | undefined;\n if (settings.initialDelayDuration) {\n startAt = nowPlus(\n Duration.fromISO(settings.initialDelayDuration),\n this.knex,\n );\n }\n\n if (isCron) {\n const time = new CronTime(settings.cadence)\n .sendAt()\n .minus({ seconds: 1 }) // immediately, if \"* * * * * *\"\n .toUTC();\n // We make a conversion here to make typescript happy, because the luxon versions of the cron library and here may not be the same\n const timeConverted = DateTime.fromJSDate(time.toJSDate());\n\n nextStartAt = TaskWorker.nextRunAtRaw(this.knex, timeConverted);\n startAt ||= nextStartAt;\n } else if (isManual) {\n nextStartAt = this.knex.raw('null');\n startAt ||= nextStartAt;\n } else {\n startAt ||= this.knex.fn.now();\n nextStartAt = nowPlus(Duration.fromISO(settings.cadence), this.knex);\n }\n\n this.logger.debug(`task: ${this.taskId} configured to run at: ${startAt}`);\n\n // It's OK if the task already exists; if it does, just replace its\n // settings with the new value and start the loop as usual.\n const settingsJson = JSON.stringify(settings);\n await this.knex<DbTasksRow>(DB_TASKS_TABLE)\n .insert({\n id: this.taskId,\n settings_json: settingsJson,\n next_run_start_at: startAt,\n })\n .onConflict('id')\n .merge(\n this.knex.client.config.client.includes('mysql')\n ? {\n settings_json: settingsJson,\n next_run_start_at: this.knex.raw(\n `CASE WHEN ?? < ?? THEN ?? ELSE ?? END`,\n [\n nextStartAt,\n 'next_run_start_at',\n nextStartAt,\n 'next_run_start_at',\n ],\n ),\n }\n : {\n settings_json: this.knex.ref('excluded.settings_json'),\n next_run_start_at: this.knex.raw(\n `CASE WHEN ?? < ?? THEN ?? ELSE ?? END`,\n [\n nextStartAt,\n `${DB_TASKS_TABLE}.next_run_start_at`,\n nextStartAt,\n `${DB_TASKS_TABLE}.next_run_start_at`,\n ],\n ),\n },\n );\n }\n\n /**\n * Checks whether the current task ticket is still valid in the database.\n * If the ticket has been cleared (e.g. by cancellation or janitor cleanup),\n * aborts the task execution.\n */\n private async checkLiveness(\n ticket: string,\n taskAbortController: AbortController,\n ): Promise<void> {\n try {\n const [row] = await this.knex<DbTasksRow>(DB_TASKS_TABLE)\n .where('id', '=', this.taskId)\n .select('current_run_ticket');\n\n if (!row || row.current_run_ticket !== ticket) {\n this.logger.info(\n `Task ticket for \"${this.taskId}\" is no longer valid; aborting execution`,\n );\n taskAbortController.abort();\n }\n } catch (e) {\n this.logger.warn(\n `Failed to check liveness for task \"${this.taskId}\", ${e}`,\n );\n }\n }\n\n /**\n * Check if the task is ready to run\n */\n async findReadyTask(): Promise<\n | { result: 'not-ready-yet' }\n | { result: 'abort' }\n | { result: 'ready'; settings: TaskSettingsV2 }\n > {\n const [row] = await this.knex<DbTasksRow>(DB_TASKS_TABLE)\n .where('id', '=', this.taskId)\n .select({\n settingsJson: 'settings_json',\n ready: this.knex.raw(\n `CASE\n WHEN next_run_start_at <= ? AND current_run_ticket IS NULL THEN TRUE\n ELSE FALSE\n END`,\n [this.knex.fn.now()],\n ),\n });\n\n if (!row) {\n this.logger.info(\n 'No longer able to find task; aborting and assuming that it has been unregistered or expired',\n );\n return { result: 'abort' };\n } else if (!row.ready) {\n return { result: 'not-ready-yet' };\n }\n\n try {\n const obj = JSON.parse(row.settingsJson);\n const settings = taskSettingsV2Schema.parse(obj);\n return { result: 'ready', settings };\n } catch (e) {\n this.logger.info(\n `Task \"${this.taskId}\" is no longer able to parse task settings; aborting and assuming that a ` +\n `newer version of the task has been issued and being handled by other workers, ${e}`,\n );\n return { result: 'abort' };\n }\n }\n\n /**\n * Attempts to claim a task that's ready for execution, on this worker's\n * behalf. We should not attempt to perform the work unless the claim really\n * goes through.\n *\n * @param ticket - A globally unique string that changes for each invocation\n * @param settings - The settings of the task to claim\n * @returns True if it was successfully claimed\n */\n async tryClaimTask(\n ticket: string,\n settings: TaskSettingsV2,\n ): Promise<boolean> {\n const startedAt = this.knex.fn.now();\n const expiresAt = settings.timeoutAfterDuration\n ? nowPlus(Duration.fromISO(settings.timeoutAfterDuration), this.knex)\n : this.knex.raw('null');\n\n const rows = await this.knex<DbTasksRow>(DB_TASKS_TABLE)\n .where('id', '=', this.taskId)\n .whereNull('current_run_ticket')\n .update({\n current_run_ticket: ticket,\n current_run_started_at: startedAt,\n current_run_expires_at: expiresAt,\n });\n\n return rows === 1;\n }\n\n private static computeNextRunStartAt(\n knex: Knex,\n settings: TaskSettingsV2,\n ): Knex.Raw {\n const isManual = settings?.cadence === 'manual';\n const isDuration = settings?.cadence.startsWith('P');\n const isCron = !isManual && !isDuration;\n\n if (isCron) {\n const time = new CronTime(settings.cadence).sendAt().toUTC();\n const timeConverted = DateTime.fromJSDate(time.toJSDate());\n return TaskWorker.nextRunAtRaw(knex, timeConverted);\n }\n\n if (isManual) {\n return knex.raw('null');\n }\n\n const dt = Duration.fromISO(settings.cadence).as('seconds');\n\n if (knex.client.config.client.includes('sqlite3')) {\n return knex.raw(`max(datetime(next_run_start_at, ?), datetime('now'))`, [\n `+${dt} seconds`,\n ]);\n }\n\n if (knex.client.config.client.includes('mysql')) {\n return knex.raw(\n `greatest(next_run_start_at + interval ${dt} second, now())`,\n );\n }\n\n return knex.raw(\n `greatest(next_run_start_at + interval '${dt} seconds', now())`,\n );\n }\n\n async tryReleaseTask(\n ticket: string,\n settings: TaskSettingsV2,\n error?: Error,\n ): Promise<boolean> {\n const nextRun = TaskWorker.computeNextRunStartAt(this.knex, settings);\n\n const rows = await this.knex<DbTasksRow>(DB_TASKS_TABLE)\n .where('id', '=', this.taskId)\n .where('current_run_ticket', '=', ticket)\n .update({\n next_run_start_at: nextRun,\n current_run_ticket: this.knex.raw('null'),\n current_run_started_at: this.knex.raw('null'),\n current_run_expires_at: this.knex.raw('null'),\n last_run_ended_at: this.knex.fn.now(),\n last_run_error_json: error\n ? serializeError(error)\n : this.knex.raw('null'),\n });\n\n return rows === 1;\n }\n\n private static nextRunAtRaw(knex: Knex, time: DateTime): Knex.Raw {\n if (knex.client.config.client.includes('sqlite3')) {\n return knex.raw('datetime(?)', [time.toISO()]);\n }\n if (knex.client.config.client.includes('mysql')) {\n return knex.raw(`?`, [time.toSQL({ includeOffset: false })]);\n }\n return knex.raw(`?`, [time.toISO()]);\n }\n}\n"],"names":["Duration","sleep","DB_TASKS_TABLE","NotFoundError","ConflictError","taskSettingsV2Schema","serializeError","dbTime","uuid","delegateAbortController","nowPlus","CronTime","DateTime"],"mappings":"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"}
@@ -2,7 +2,7 @@
2
2
 
3
3
  var cron = require('cron');
4
4
  var luxon = require('luxon');
5
- var zod = require('zod');
5
+ var v3 = require('zod/v3');
6
6
 
7
7
  function isValidOptionalDurationString(d) {
8
8
  try {
@@ -25,33 +25,33 @@ function isValidCronFormat(c) {
25
25
  function isValidTrigger(t) {
26
26
  return t === "manual";
27
27
  }
28
- zod.z.object({
29
- version: zod.z.literal(1),
30
- initialDelayDuration: zod.z.string().optional().refine(isValidOptionalDurationString, {
28
+ v3.z.object({
29
+ version: v3.z.literal(1),
30
+ initialDelayDuration: v3.z.string().optional().refine(isValidOptionalDurationString, {
31
31
  message: "Invalid duration, expecting ISO Period"
32
32
  }),
33
- recurringAtMostEveryDuration: zod.z.string().refine(isValidOptionalDurationString, {
33
+ recurringAtMostEveryDuration: v3.z.string().refine(isValidOptionalDurationString, {
34
34
  message: "Invalid duration, expecting ISO Period"
35
35
  }),
36
- timeoutAfterDuration: zod.z.string().refine(isValidOptionalDurationString, {
36
+ timeoutAfterDuration: v3.z.string().refine(isValidOptionalDurationString, {
37
37
  message: "Invalid duration, expecting ISO Period"
38
38
  })
39
39
  });
40
- const taskSettingsV2Schema = zod.z.object({
41
- version: zod.z.literal(2),
42
- cadence: zod.z.string().refine(isValidCronFormat, { message: "Invalid cron" }).or(
43
- zod.z.string().refine(isValidTrigger, {
40
+ const taskSettingsV2Schema = v3.z.object({
41
+ version: v3.z.literal(2),
42
+ cadence: v3.z.string().refine(isValidCronFormat, { message: "Invalid cron" }).or(
43
+ v3.z.string().refine(isValidTrigger, {
44
44
  message: "Invalid trigger, expecting 'manual'"
45
45
  })
46
46
  ).or(
47
- zod.z.string().refine(isValidOptionalDurationString, {
47
+ v3.z.string().refine(isValidOptionalDurationString, {
48
48
  message: "Invalid duration, expecting ISO Period"
49
49
  })
50
50
  ),
51
- timeoutAfterDuration: zod.z.string().refine(isValidOptionalDurationString, {
51
+ timeoutAfterDuration: v3.z.string().refine(isValidOptionalDurationString, {
52
52
  message: "Invalid duration, expecting ISO Period"
53
53
  }),
54
- initialDelayDuration: zod.z.string().optional().refine(isValidOptionalDurationString, {
54
+ initialDelayDuration: v3.z.string().optional().refine(isValidOptionalDurationString, {
55
55
  message: "Invalid duration, expecting ISO Period"
56
56
  })
57
57
  });
@@ -1 +1 @@
1
- {"version":3,"file":"types.cjs.js","sources":["../../../../src/entrypoints/scheduler/lib/types.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { JsonObject } from '@backstage/types';\nimport { CronTime } from 'cron';\nimport { Duration } from 'luxon';\nimport { z } from 'zod';\n\nfunction isValidOptionalDurationString(d: string | undefined): boolean {\n try {\n return !d || Duration.fromISO(d).isValid;\n } catch {\n return false;\n }\n}\n\nfunction isValidCronFormat(c: string | undefined): boolean {\n try {\n if (!c) {\n return false;\n }\n // parse cron format to ensure it's a valid format.\n // eslint-disable-next-line no-new\n new CronTime(c);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction isValidTrigger(t: string): boolean {\n return t === 'manual';\n}\n\nexport const taskSettingsV1Schema = z.object({\n version: z.literal(1),\n initialDelayDuration: z\n .string()\n .optional()\n .refine(isValidOptionalDurationString, {\n message: 'Invalid duration, expecting ISO Period',\n }),\n recurringAtMostEveryDuration: z\n .string()\n .refine(isValidOptionalDurationString, {\n message: 'Invalid duration, expecting ISO Period',\n }),\n timeoutAfterDuration: z.string().refine(isValidOptionalDurationString, {\n message: 'Invalid duration, expecting ISO Period',\n }),\n});\n\n/**\n * The properties that control a scheduled task (version 1).\n */\nexport type TaskSettingsV1 = z.infer<typeof taskSettingsV1Schema>;\n\nexport const taskSettingsV2Schema = z.object({\n version: z.literal(2),\n cadence: z\n .string()\n .refine(isValidCronFormat, { message: 'Invalid cron' })\n .or(\n z.string().refine(isValidTrigger, {\n message: \"Invalid trigger, expecting 'manual'\",\n }),\n )\n .or(\n z.string().refine(isValidOptionalDurationString, {\n message: 'Invalid duration, expecting ISO Period',\n }),\n ),\n timeoutAfterDuration: z.string().refine(isValidOptionalDurationString, {\n message: 'Invalid duration, expecting ISO Period',\n }),\n initialDelayDuration: z\n .string()\n .optional()\n .refine(isValidOptionalDurationString, {\n message: 'Invalid duration, expecting ISO Period',\n }),\n});\n\n/**\n * The properties that control a scheduled task (version 2).\n */\nexport type TaskSettingsV2 = z.infer<typeof taskSettingsV2Schema>;\n\n/**\n * The shape of a task definition as returned by the service's REST API.\n */\nexport interface TaskApiTasksResponse {\n taskId: string;\n pluginId: string;\n scope: 'global' | 'local';\n settings: { version: number } & JsonObject;\n taskState:\n | {\n status: 'running';\n startedAt: string;\n timesOutAt?: string;\n lastRunError?: string;\n lastRunEndedAt?: string;\n }\n | {\n status: 'idle';\n startsAt?: string;\n lastRunError?: string;\n lastRunEndedAt?: string;\n }\n | null;\n workerState:\n | {\n status: 'initial-wait';\n }\n | {\n status: 'idle';\n }\n | {\n status: 'running';\n }\n | null;\n}\n"],"names":["Duration","CronTime","z"],"mappings":";;;;;;AAqBA,SAAS,8BAA8B,CAAA,EAAgC;AACrE,EAAA,IAAI;AACF,IAAA,OAAO,CAAC,CAAA,IAAKA,cAAA,CAAS,OAAA,CAAQ,CAAC,CAAA,CAAE,OAAA;AAAA,EACnC,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAEA,SAAS,kBAAkB,CAAA,EAAgC;AACzD,EAAA,IAAI;AACF,IAAA,IAAI,CAAC,CAAA,EAAG;AACN,MAAA,OAAO,KAAA;AAAA,IACT;AAGA,IAAA,IAAIC,cAAS,CAAC,CAAA;AACd,IAAA,OAAO,IAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAEA,SAAS,eAAe,CAAA,EAAoB;AAC1C,EAAA,OAAO,CAAA,KAAM,QAAA;AACf;AAEoCC,MAAE,MAAA,CAAO;AAAA,EAC3C,OAAA,EAASA,KAAA,CAAE,OAAA,CAAQ,CAAC,CAAA;AAAA,EACpB,sBAAsBA,KAAA,CACnB,MAAA,GACA,QAAA,EAAS,CACT,OAAO,6BAAA,EAA+B;AAAA,IACrC,OAAA,EAAS;AAAA,GACV,CAAA;AAAA,EACH,4BAAA,EAA8BA,KAAA,CAC3B,MAAA,EAAO,CACP,OAAO,6BAAA,EAA+B;AAAA,IACrC,OAAA,EAAS;AAAA,GACV,CAAA;AAAA,EACH,oBAAA,EAAsBA,KAAA,CAAE,MAAA,EAAO,CAAE,OAAO,6BAAA,EAA+B;AAAA,IACrE,OAAA,EAAS;AAAA,GACV;AACH,CAAC;AAOM,MAAM,oBAAA,GAAuBA,MAAE,MAAA,CAAO;AAAA,EAC3C,OAAA,EAASA,KAAA,CAAE,OAAA,CAAQ,CAAC,CAAA;AAAA,EACpB,OAAA,EAASA,KAAA,CACN,MAAA,EAAO,CACP,MAAA,CAAO,mBAAmB,EAAE,OAAA,EAAS,cAAA,EAAgB,CAAA,CACrD,EAAA;AAAA,IACCA,KAAA,CAAE,MAAA,EAAO,CAAE,MAAA,CAAO,cAAA,EAAgB;AAAA,MAChC,OAAA,EAAS;AAAA,KACV;AAAA,GACH,CACC,EAAA;AAAA,IACCA,KAAA,CAAE,MAAA,EAAO,CAAE,MAAA,CAAO,6BAAA,EAA+B;AAAA,MAC/C,OAAA,EAAS;AAAA,KACV;AAAA,GACH;AAAA,EACF,oBAAA,EAAsBA,KAAA,CAAE,MAAA,EAAO,CAAE,OAAO,6BAAA,EAA+B;AAAA,IACrE,OAAA,EAAS;AAAA,GACV,CAAA;AAAA,EACD,sBAAsBA,KAAA,CACnB,MAAA,GACA,QAAA,EAAS,CACT,OAAO,6BAAA,EAA+B;AAAA,IACrC,OAAA,EAAS;AAAA,GACV;AACL,CAAC;;;;"}
1
+ {"version":3,"file":"types.cjs.js","sources":["../../../../src/entrypoints/scheduler/lib/types.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { JsonObject } from '@backstage/types';\nimport { CronTime } from 'cron';\nimport { Duration } from 'luxon';\nimport { z } from 'zod/v3';\n\nfunction isValidOptionalDurationString(d: string | undefined): boolean {\n try {\n return !d || Duration.fromISO(d).isValid;\n } catch {\n return false;\n }\n}\n\nfunction isValidCronFormat(c: string | undefined): boolean {\n try {\n if (!c) {\n return false;\n }\n // parse cron format to ensure it's a valid format.\n // eslint-disable-next-line no-new\n new CronTime(c);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction isValidTrigger(t: string): boolean {\n return t === 'manual';\n}\n\nexport const taskSettingsV1Schema = z.object({\n version: z.literal(1),\n initialDelayDuration: z\n .string()\n .optional()\n .refine(isValidOptionalDurationString, {\n message: 'Invalid duration, expecting ISO Period',\n }),\n recurringAtMostEveryDuration: z\n .string()\n .refine(isValidOptionalDurationString, {\n message: 'Invalid duration, expecting ISO Period',\n }),\n timeoutAfterDuration: z.string().refine(isValidOptionalDurationString, {\n message: 'Invalid duration, expecting ISO Period',\n }),\n});\n\n/**\n * The properties that control a scheduled task (version 1).\n */\nexport type TaskSettingsV1 = z.infer<typeof taskSettingsV1Schema>;\n\nexport const taskSettingsV2Schema = z.object({\n version: z.literal(2),\n cadence: z\n .string()\n .refine(isValidCronFormat, { message: 'Invalid cron' })\n .or(\n z.string().refine(isValidTrigger, {\n message: \"Invalid trigger, expecting 'manual'\",\n }),\n )\n .or(\n z.string().refine(isValidOptionalDurationString, {\n message: 'Invalid duration, expecting ISO Period',\n }),\n ),\n timeoutAfterDuration: z.string().refine(isValidOptionalDurationString, {\n message: 'Invalid duration, expecting ISO Period',\n }),\n initialDelayDuration: z\n .string()\n .optional()\n .refine(isValidOptionalDurationString, {\n message: 'Invalid duration, expecting ISO Period',\n }),\n});\n\n/**\n * The properties that control a scheduled task (version 2).\n */\nexport type TaskSettingsV2 = z.infer<typeof taskSettingsV2Schema>;\n\n/**\n * The shape of a task definition as returned by the service's REST API.\n */\nexport interface TaskApiTasksResponse {\n taskId: string;\n pluginId: string;\n scope: 'global' | 'local';\n settings: { version: number } & JsonObject;\n taskState:\n | {\n status: 'running';\n startedAt: string;\n timesOutAt?: string;\n lastRunError?: string;\n lastRunEndedAt?: string;\n }\n | {\n status: 'idle';\n startsAt?: string;\n lastRunError?: string;\n lastRunEndedAt?: string;\n }\n | null;\n workerState:\n | {\n status: 'initial-wait';\n }\n | {\n status: 'idle';\n }\n | {\n status: 'running';\n }\n | null;\n}\n"],"names":["Duration","CronTime","z"],"mappings":";;;;;;AAqBA,SAAS,8BAA8B,CAAA,EAAgC;AACrE,EAAA,IAAI;AACF,IAAA,OAAO,CAAC,CAAA,IAAKA,cAAA,CAAS,OAAA,CAAQ,CAAC,CAAA,CAAE,OAAA;AAAA,EACnC,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAEA,SAAS,kBAAkB,CAAA,EAAgC;AACzD,EAAA,IAAI;AACF,IAAA,IAAI,CAAC,CAAA,EAAG;AACN,MAAA,OAAO,KAAA;AAAA,IACT;AAGA,IAAA,IAAIC,cAAS,CAAC,CAAA;AACd,IAAA,OAAO,IAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAEA,SAAS,eAAe,CAAA,EAAoB;AAC1C,EAAA,OAAO,CAAA,KAAM,QAAA;AACf;AAEoCC,KAAE,MAAA,CAAO;AAAA,EAC3C,OAAA,EAASA,IAAA,CAAE,OAAA,CAAQ,CAAC,CAAA;AAAA,EACpB,sBAAsBA,IAAA,CACnB,MAAA,GACA,QAAA,EAAS,CACT,OAAO,6BAAA,EAA+B;AAAA,IACrC,OAAA,EAAS;AAAA,GACV,CAAA;AAAA,EACH,4BAAA,EAA8BA,IAAA,CAC3B,MAAA,EAAO,CACP,OAAO,6BAAA,EAA+B;AAAA,IACrC,OAAA,EAAS;AAAA,GACV,CAAA;AAAA,EACH,oBAAA,EAAsBA,IAAA,CAAE,MAAA,EAAO,CAAE,OAAO,6BAAA,EAA+B;AAAA,IACrE,OAAA,EAAS;AAAA,GACV;AACH,CAAC;AAOM,MAAM,oBAAA,GAAuBA,KAAE,MAAA,CAAO;AAAA,EAC3C,OAAA,EAASA,IAAA,CAAE,OAAA,CAAQ,CAAC,CAAA;AAAA,EACpB,OAAA,EAASA,IAAA,CACN,MAAA,EAAO,CACP,MAAA,CAAO,mBAAmB,EAAE,OAAA,EAAS,cAAA,EAAgB,CAAA,CACrD,EAAA;AAAA,IACCA,IAAA,CAAE,MAAA,EAAO,CAAE,MAAA,CAAO,cAAA,EAAgB;AAAA,MAChC,OAAA,EAAS;AAAA,KACV;AAAA,GACH,CACC,EAAA;AAAA,IACCA,IAAA,CAAE,MAAA,EAAO,CAAE,MAAA,CAAO,6BAAA,EAA+B;AAAA,MAC/C,OAAA,EAAS;AAAA,KACV;AAAA,GACH;AAAA,EACF,oBAAA,EAAsBA,IAAA,CAAE,MAAA,EAAO,CAAE,OAAO,6BAAA,EAA+B;AAAA,IACrE,OAAA,EAAS;AAAA,GACV,CAAA;AAAA,EACD,sBAAsBA,IAAA,CACnB,MAAA,GACA,QAAA,EAAS,CACT,OAAO,6BAAA,EAA+B;AAAA,IACrC,OAAA,EAAS;AAAA,GACV;AACL,CAAC;;;;"}
@@ -2,7 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var version = "0.16.0-next.1";
5
+ var version = "0.16.0";
6
6
  var packageinfo = {
7
7
  version: version};
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/backend-defaults",
3
- "version": "0.16.0-next.1",
3
+ "version": "0.16.0",
4
4
  "description": "Backend defaults used by Backstage backend apps",
5
5
  "backstage": {
6
6
  "role": "node-library"
@@ -217,19 +217,20 @@
217
217
  "@aws-sdk/types": "^3.347.0",
218
218
  "@azure/identity": "^4.0.0",
219
219
  "@azure/storage-blob": "^12.5.0",
220
- "@backstage/backend-app-api": "1.5.1-next.0",
221
- "@backstage/backend-dev-utils": "0.1.7",
222
- "@backstage/backend-plugin-api": "1.7.1-next.0",
223
- "@backstage/cli-node": "0.2.19-next.1",
224
- "@backstage/config": "1.3.6",
225
- "@backstage/config-loader": "1.10.9-next.0",
226
- "@backstage/errors": "1.2.7",
227
- "@backstage/integration": "2.0.0-next.1",
228
- "@backstage/integration-aws-node": "0.1.20",
229
- "@backstage/plugin-auth-node": "0.6.14-next.1",
230
- "@backstage/plugin-events-node": "0.4.20-next.0",
231
- "@backstage/plugin-permission-node": "0.10.11-next.0",
232
- "@backstage/types": "1.2.2",
220
+ "@backstage/backend-app-api": "^1.6.0",
221
+ "@backstage/backend-dev-utils": "^0.1.7",
222
+ "@backstage/backend-plugin-api": "^1.8.0",
223
+ "@backstage/cli-node": "^0.3.0",
224
+ "@backstage/config": "^1.3.6",
225
+ "@backstage/config-loader": "^1.10.9",
226
+ "@backstage/errors": "^1.2.7",
227
+ "@backstage/integration": "^2.0.0",
228
+ "@backstage/integration-aws-node": "^0.1.20",
229
+ "@backstage/plugin-auth-node": "^0.6.14",
230
+ "@backstage/plugin-events-node": "^0.4.20",
231
+ "@backstage/plugin-permission-common": "^0.9.7",
232
+ "@backstage/plugin-permission-node": "^0.10.11",
233
+ "@backstage/types": "^1.2.2",
233
234
  "@google-cloud/storage": "^7.0.0",
234
235
  "@keyv/memcache": "^2.0.1",
235
236
  "@keyv/redis": "^4.0.1",
@@ -248,7 +249,7 @@
248
249
  "cron": "^3.0.0",
249
250
  "express": "^4.22.0",
250
251
  "express-promise-router": "^4.1.0",
251
- "express-rate-limit": "^7.5.0",
252
+ "express-rate-limit": "^8.2.2",
252
253
  "fs-extra": "^11.2.0",
253
254
  "git-url-parse": "^15.0.0",
254
255
  "helmet": "^6.0.0",
@@ -277,16 +278,16 @@
277
278
  "uuid": "^11.0.0",
278
279
  "winston": "^3.2.1",
279
280
  "winston-transport": "^4.5.0",
280
- "yauzl": "^3.0.0",
281
+ "yauzl": "^3.2.1",
281
282
  "yn": "^4.0.0",
282
- "zod": "^3.25.76",
283
+ "zod": "^3.25.76 || ^4.0.0",
283
284
  "zod-to-json-schema": "^3.25.1"
284
285
  },
285
286
  "devDependencies": {
286
287
  "@aws-sdk/util-stream-node": "^3.350.0",
287
- "@backstage/backend-plugin-api": "1.7.1-next.0",
288
- "@backstage/backend-test-utils": "1.11.1-next.1",
289
- "@backstage/cli": "0.36.0-next.1",
288
+ "@backstage/backend-plugin-api": "^1.8.0",
289
+ "@backstage/backend-test-utils": "^1.11.1",
290
+ "@backstage/cli": "^0.36.0",
290
291
  "@google-cloud/cloud-sql-connector": "^1.4.0",
291
292
  "@types/archiver": "^7.0.0",
292
293
  "@types/base64-stream": "^1.0.2",