@backstage/plugin-catalog-backend 3.5.0-next.1 → 3.5.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 +64 -0
- package/config.d.ts +9 -0
- package/dist/database/operations/stitcher/getDeferredStitchableEntities.cjs.js +6 -6
- package/dist/database/operations/stitcher/getDeferredStitchableEntities.cjs.js.map +1 -1
- package/dist/database/operations/stitcher/markDeferredStitchCompleted.cjs.js +1 -4
- package/dist/database/operations/stitcher/markDeferredStitchCompleted.cjs.js.map +1 -1
- package/dist/database/operations/stitcher/markForStitching.cjs.js +19 -8
- package/dist/database/operations/stitcher/markForStitching.cjs.js.map +1 -1
- package/dist/database/operations/stitcher/performStitching.cjs.js +12 -8
- package/dist/database/operations/stitcher/performStitching.cjs.js.map +1 -1
- package/dist/ingestion/CatalogRules.cjs.js +6 -6
- package/dist/ingestion/CatalogRules.cjs.js.map +1 -1
- package/dist/permissions/rules/createPropertyRule.cjs.js +4 -4
- package/dist/permissions/rules/createPropertyRule.cjs.js.map +1 -1
- package/dist/permissions/rules/hasAnnotation.cjs.js +4 -4
- package/dist/permissions/rules/hasAnnotation.cjs.js.map +1 -1
- package/dist/permissions/rules/hasLabel.cjs.js +4 -4
- package/dist/permissions/rules/hasLabel.cjs.js.map +1 -1
- package/dist/permissions/rules/isEntityKind.cjs.js +3 -3
- package/dist/permissions/rules/isEntityKind.cjs.js.map +1 -1
- package/dist/permissions/rules/isEntityOwner.cjs.js +3 -3
- package/dist/permissions/rules/isEntityOwner.cjs.js.map +1 -1
- package/dist/providers/DefaultLocationStore.cjs.js +14 -2
- package/dist/providers/DefaultLocationStore.cjs.js.map +1 -1
- package/dist/schema/openapi/generated/router.cjs.js +19 -0
- package/dist/schema/openapi/generated/router.cjs.js.map +1 -1
- package/dist/service/AuthorizedLocationService.cjs.js.map +1 -1
- package/dist/service/CatalogBuilder.cjs.js +4 -1
- package/dist/service/CatalogBuilder.cjs.js.map +1 -1
- package/dist/service/DefaultEntitiesCatalog.cjs.js +43 -38
- package/dist/service/DefaultEntitiesCatalog.cjs.js.map +1 -1
- package/dist/service/DefaultLocationService.cjs.js +6 -3
- package/dist/service/DefaultLocationService.cjs.js.map +1 -1
- package/dist/service/createRouter.cjs.js +10 -8
- package/dist/service/createRouter.cjs.js.map +1 -1
- package/dist/service/util.cjs.js +21 -21
- package/dist/service/util.cjs.js.map +1 -1
- package/dist/stitching/progressTracker.cjs.js +3 -1
- package/dist/stitching/progressTracker.cjs.js.map +1 -1
- package/migrations/20260214000000_search_fk_final_entities.js +220 -36
- package/migrations/20260215000000_move_stitch_queue.js +174 -0
- package/package.json +21 -21
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
# @backstage/plugin-catalog-backend
|
|
2
2
|
|
|
3
|
+
## 3.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- a6b2819: Added `query-catalog-entities` action to the catalog backend actions registry. Supports predicate-based filtering with `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators.
|
|
8
|
+
- 972f686: Added support for predicate-based filtering on the `/entities/by-refs` endpoint via the `query` field in the request body. Supports `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators.
|
|
9
|
+
- 5d95e8e: Add an `onConflict` option to location creation that can refresh an existing location instead of throwing a conflict error.
|
|
10
|
+
- 56c908e: Added support for predicate-based filtering on the `/entity-facets` endpoint via a new `POST` method. Supports `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators.
|
|
11
|
+
- 0fbcf23: Migrated OpenAPI schemas to 3.1.
|
|
12
|
+
- bf71677: Added opentelemetry metrics for SCM events:
|
|
13
|
+
|
|
14
|
+
- `catalog.events.scm.messages` with attribute `eventType`: Counter for the number of SCM events actually received by the catalog backend. The `eventType` is currently either `location` or `repository`.
|
|
15
|
+
|
|
16
|
+
- 51e23eb: Added predicate-based entity filtering via POST /entities/by-query endpoint.
|
|
17
|
+
|
|
18
|
+
Supports `$all`, `$any`, `$not`, `$exists`, `$in`, `$hasPrefix`, and (partially) `$contains` operators for expressive entity queries. Integrated into the existing `queryEntities` flow with full cursor-based pagination, permission enforcement, and `totalItems` support.
|
|
19
|
+
|
|
20
|
+
The catalog client's `queryEntities()` method automatically routes to the POST endpoint when a `query` predicate is provided.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- a91bd1b: Improved catalog entity deletion so parent invalidation and deferred relation restitch scheduling are coordinated more safely.
|
|
25
|
+
- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1
|
|
26
|
+
- 7416e8b: Moved stitch queue concerns out of `refresh_state` and `final_entities` into a dedicated `stitch_queue` table with `entity_ref` as the primary key. The `stitch_ticket` is used for optimistic concurrency control. When a stitch completes successfully and the ticket hasn't changed, the corresponding row is deleted from the queue. The migration handles existing data and is fully reversible.
|
|
27
|
+
- fbf382f: Minor internal optimisation
|
|
28
|
+
- 1ee5b28: Migrates existing catalog metrics to use the alpha MetricsService. This release is a 1:1 migration with no breaking changes.
|
|
29
|
+
- 72747b4: Deprecated two processors as they have been moved to the Community Plugins repo with their own backend modules:
|
|
30
|
+
|
|
31
|
+
- `AnnotateScmSlugEntityProcessor`: Use `@backstage-community/plugin-catalog-backend-module-annotate-scm-slug` instead
|
|
32
|
+
- `CodeOwnersProcessor`: Use `@backstage-community/plugin-catalog-backend-module-codeowners` instead
|
|
33
|
+
|
|
34
|
+
- 3644b72: Make the `search` foreign key catalog migration non-blocking on large tables by using batch deletes and PostgreSQL `NOT VALID`/`VALIDATE` to reduce lock duration
|
|
35
|
+
- a49a40d: Updated dependency `zod` to `^3.25.76 || ^4.0.0` & migrated to `/v3` or `/v4` imports.
|
|
36
|
+
- 3181973: Changed the `search` table foreign key to point to `final_entities` instead of `refresh_state`
|
|
37
|
+
- Updated dependencies
|
|
38
|
+
- @backstage/backend-plugin-api@1.8.0
|
|
39
|
+
- @backstage/catalog-client@1.14.0
|
|
40
|
+
- @backstage/integration@2.0.0
|
|
41
|
+
- @backstage/plugin-catalog-node@2.1.0
|
|
42
|
+
- @backstage/filter-predicates@0.1.1
|
|
43
|
+
- @backstage/plugin-permission-common@0.9.7
|
|
44
|
+
- @backstage/plugin-permission-node@0.10.11
|
|
45
|
+
- @backstage/catalog-model@1.7.7
|
|
46
|
+
- @backstage/backend-openapi-utils@0.6.7
|
|
47
|
+
- @backstage/plugin-events-node@0.4.20
|
|
48
|
+
|
|
49
|
+
## 3.5.0-next.2
|
|
50
|
+
|
|
51
|
+
### Minor Changes
|
|
52
|
+
|
|
53
|
+
- 5d95e8e: Add an `onConflict` option to location creation that can refresh an existing location instead of throwing a conflict error.
|
|
54
|
+
|
|
55
|
+
### Patch Changes
|
|
56
|
+
|
|
57
|
+
- 7416e8b: Moved stitch queue concerns out of `refresh_state` and `final_entities` into a dedicated `stitch_queue` table with `entity_ref` as the primary key. The `stitch_ticket` is used for optimistic concurrency control. When a stitch completes successfully and the ticket hasn't changed, the corresponding row is deleted from the queue. The migration handles existing data and is fully reversible.
|
|
58
|
+
- Updated dependencies
|
|
59
|
+
- @backstage/backend-plugin-api@1.8.0-next.1
|
|
60
|
+
- @backstage/catalog-client@1.14.0-next.2
|
|
61
|
+
- @backstage/integration@2.0.0-next.2
|
|
62
|
+
- @backstage/backend-openapi-utils@0.6.7-next.1
|
|
63
|
+
- @backstage/plugin-catalog-node@2.1.0-next.2
|
|
64
|
+
- @backstage/plugin-events-node@0.4.20-next.1
|
|
65
|
+
- @backstage/plugin-permission-node@0.10.11-next.1
|
|
66
|
+
|
|
3
67
|
## 3.5.0-next.1
|
|
4
68
|
|
|
5
69
|
### Minor Changes
|
package/config.d.ts
CHANGED
|
@@ -191,6 +191,15 @@ export interface Config {
|
|
|
191
191
|
stitchTimeout?: HumanDuration | string;
|
|
192
192
|
};
|
|
193
193
|
|
|
194
|
+
/**
|
|
195
|
+
* The strategy to use when there is a conflict with a location being registered.
|
|
196
|
+
*
|
|
197
|
+
* The default value is "reject".
|
|
198
|
+
*
|
|
199
|
+
* The "refresh" strategy will refresh the existing location instead of throwing a conflict error.
|
|
200
|
+
*/
|
|
201
|
+
defaultLocationConflictStrategy?: 'refresh' | 'reject';
|
|
202
|
+
|
|
194
203
|
/**
|
|
195
204
|
* The interval at which the catalog should process its entities.
|
|
196
205
|
* @remarks
|
|
@@ -5,27 +5,27 @@ var conversion = require('../../conversion.cjs.js');
|
|
|
5
5
|
|
|
6
6
|
async function getDeferredStitchableEntities(options) {
|
|
7
7
|
const { knex, batchSize, stitchTimeout } = options;
|
|
8
|
-
let itemsQuery = knex("
|
|
8
|
+
let itemsQuery = knex("stitch_queue").select(
|
|
9
9
|
"entity_ref",
|
|
10
10
|
"next_stitch_at",
|
|
11
|
-
"
|
|
11
|
+
"stitch_ticket"
|
|
12
12
|
);
|
|
13
13
|
if (["mysql", "mysql2", "pg"].includes(knex.client.config.client)) {
|
|
14
14
|
itemsQuery = itemsQuery.forUpdate().skipLocked();
|
|
15
15
|
}
|
|
16
|
-
const items = await itemsQuery.
|
|
16
|
+
const items = await itemsQuery.where("next_stitch_at", "<=", knex.fn.now()).orderBy("next_stitch_at", "asc").limit(batchSize);
|
|
17
17
|
if (!items.length) {
|
|
18
18
|
return [];
|
|
19
19
|
}
|
|
20
|
-
await knex("
|
|
20
|
+
await knex("stitch_queue").whereIn(
|
|
21
21
|
"entity_ref",
|
|
22
22
|
items.map((i) => i.entity_ref)
|
|
23
|
-
).
|
|
23
|
+
).update({
|
|
24
24
|
next_stitch_at: nowPlus(knex, stitchTimeout)
|
|
25
25
|
});
|
|
26
26
|
return items.map((i) => ({
|
|
27
27
|
entityRef: i.entity_ref,
|
|
28
|
-
stitchTicket: i.
|
|
28
|
+
stitchTicket: i.stitch_ticket,
|
|
29
29
|
stitchRequestedAt: conversion.timestampToDateTime(i.next_stitch_at)
|
|
30
30
|
}));
|
|
31
31
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"getDeferredStitchableEntities.cjs.js","sources":["../../../../src/database/operations/stitcher/getDeferredStitchableEntities.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { durationToMilliseconds, HumanDuration } from '@backstage/types';\nimport { Knex } from 'knex';\nimport { DateTime } from 'luxon';\nimport { timestampToDateTime } from '../../conversion';\nimport {
|
|
1
|
+
{"version":3,"file":"getDeferredStitchableEntities.cjs.js","sources":["../../../../src/database/operations/stitcher/getDeferredStitchableEntities.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { durationToMilliseconds, HumanDuration } from '@backstage/types';\nimport { Knex } from 'knex';\nimport { DateTime } from 'luxon';\nimport { timestampToDateTime } from '../../conversion';\nimport { DbStitchQueueRow } from '../../tables';\n\n// TODO(freben): There is no retry counter or similar. If items start\n// perpetually crashing during stitching, they'll just get silently retried over\n// and over again, for better or worse. This will be visible in metrics though.\n\n/**\n * Finds entities that are marked for deferred stitching.\n *\n * @remarks\n *\n * This assumes that the stitching strategy is set to deferred.\n *\n * They are expected to already have the stitch_ticket set (by\n * markForStitching) so that their tickets can be returned with each item.\n *\n * All returned items have their next_stitch_at updated to be moved forward by\n * the given timeout duration. This has the effect that they will be picked up\n * for stitching again in the future, if it hasn't completed by that point for\n * some reason (restarts, crashes, etc).\n */\nexport async function getDeferredStitchableEntities(options: {\n knex: Knex | Knex.Transaction;\n batchSize: number;\n stitchTimeout: HumanDuration;\n}): Promise<\n Array<{\n entityRef: string;\n stitchTicket: string;\n stitchRequestedAt: DateTime; // the time BEFORE moving it forward by the timeout\n }>\n> {\n const { knex, batchSize, stitchTimeout } = options;\n\n let itemsQuery = knex<DbStitchQueueRow>('stitch_queue').select(\n 'entity_ref',\n 'next_stitch_at',\n 'stitch_ticket',\n );\n\n // This avoids duplication of work because of race conditions and is\n // also fast because locked rows are ignored rather than blocking.\n // It's only available in MySQL and PostgreSQL\n if (['mysql', 'mysql2', 'pg'].includes(knex.client.config.client)) {\n itemsQuery = itemsQuery.forUpdate().skipLocked();\n }\n\n const items = await itemsQuery\n .where('next_stitch_at', '<=', knex.fn.now())\n .orderBy('next_stitch_at', 'asc')\n .limit(batchSize);\n\n if (!items.length) {\n return [];\n }\n\n await knex<DbStitchQueueRow>('stitch_queue')\n .whereIn(\n 'entity_ref',\n items.map(i => i.entity_ref),\n )\n .update({\n next_stitch_at: nowPlus(knex, stitchTimeout),\n });\n\n return items.map(i => ({\n entityRef: i.entity_ref,\n stitchTicket: i.stitch_ticket,\n stitchRequestedAt: timestampToDateTime(i.next_stitch_at),\n }));\n}\n\nfunction nowPlus(knex: Knex, duration: HumanDuration): Knex.Raw {\n const seconds = durationToMilliseconds(duration) / 1000;\n if (knex.client.config.client.includes('sqlite3')) {\n return knex.raw(`datetime('now', ?)`, [`${seconds} seconds`]);\n } else if (knex.client.config.client.includes('mysql')) {\n return knex.raw(`now() + interval ${seconds} second`);\n }\n return knex.raw(`now() + interval '${seconds} seconds'`);\n}\n"],"names":["timestampToDateTime","durationToMilliseconds"],"mappings":";;;;;AAyCA,eAAsB,8BAA8B,OAAA,EAUlD;AACA,EAAA,MAAM,EAAE,IAAA,EAAM,SAAA,EAAW,aAAA,EAAc,GAAI,OAAA;AAE3C,EAAA,IAAI,UAAA,GAAa,IAAA,CAAuB,cAAc,CAAA,CAAE,MAAA;AAAA,IACtD,YAAA;AAAA,IACA,gBAAA;AAAA,IACA;AAAA,GACF;AAKA,EAAA,IAAI,CAAC,OAAA,EAAS,QAAA,EAAU,IAAI,CAAA,CAAE,SAAS,IAAA,CAAK,MAAA,CAAO,MAAA,CAAO,MAAM,CAAA,EAAG;AACjE,IAAA,UAAA,GAAa,UAAA,CAAW,SAAA,EAAU,CAAE,UAAA,EAAW;AAAA,EACjD;AAEA,EAAA,MAAM,QAAQ,MAAM,UAAA,CACjB,KAAA,CAAM,gBAAA,EAAkB,MAAM,IAAA,CAAK,EAAA,CAAG,GAAA,EAAK,EAC3C,OAAA,CAAQ,gBAAA,EAAkB,KAAK,CAAA,CAC/B,MAAM,SAAS,CAAA;AAElB,EAAA,IAAI,CAAC,MAAM,MAAA,EAAQ;AACjB,IAAA,OAAO,EAAC;AAAA,EACV;AAEA,EAAA,MAAM,IAAA,CAAuB,cAAc,CAAA,CACxC,OAAA;AAAA,IACC,YAAA;AAAA,IACA,KAAA,CAAM,GAAA,CAAI,CAAA,CAAA,KAAK,CAAA,CAAE,UAAU;AAAA,IAE5B,MAAA,CAAO;AAAA,IACN,cAAA,EAAgB,OAAA,CAAQ,IAAA,EAAM,aAAa;AAAA,GAC5C,CAAA;AAEH,EAAA,OAAO,KAAA,CAAM,IAAI,CAAA,CAAA,MAAM;AAAA,IACrB,WAAW,CAAA,CAAE,UAAA;AAAA,IACb,cAAc,CAAA,CAAE,aAAA;AAAA,IAChB,iBAAA,EAAmBA,8BAAA,CAAoB,CAAA,CAAE,cAAc;AAAA,GACzD,CAAE,CAAA;AACJ;AAEA,SAAS,OAAA,CAAQ,MAAY,QAAA,EAAmC;AAC9D,EAAA,MAAM,OAAA,GAAUC,4BAAA,CAAuB,QAAQ,CAAA,GAAI,GAAA;AACnD,EAAA,IAAI,KAAK,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,QAAA,CAAS,SAAS,CAAA,EAAG;AACjD,IAAA,OAAO,KAAK,GAAA,CAAI,CAAA,kBAAA,CAAA,EAAsB,CAAC,CAAA,EAAG,OAAO,UAAU,CAAC,CAAA;AAAA,EAC9D,WAAW,IAAA,CAAK,MAAA,CAAO,OAAO,MAAA,CAAO,QAAA,CAAS,OAAO,CAAA,EAAG;AACtD,IAAA,OAAO,IAAA,CAAK,GAAA,CAAI,CAAA,iBAAA,EAAoB,OAAO,CAAA,OAAA,CAAS,CAAA;AAAA,EACtD;AACA,EAAA,OAAO,IAAA,CAAK,GAAA,CAAI,CAAA,kBAAA,EAAqB,OAAO,CAAA,SAAA,CAAW,CAAA;AACzD;;;;"}
|
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
async function markDeferredStitchCompleted(option) {
|
|
4
4
|
const { knex, entityRef, stitchTicket } = option;
|
|
5
|
-
await knex("
|
|
6
|
-
next_stitch_at: null,
|
|
7
|
-
next_stitch_ticket: null
|
|
8
|
-
}).where("entity_ref", "=", entityRef).andWhere("next_stitch_ticket", "=", stitchTicket);
|
|
5
|
+
await knex("stitch_queue").where("entity_ref", "=", entityRef).andWhere("stitch_ticket", "=", stitchTicket).delete();
|
|
9
6
|
}
|
|
10
7
|
|
|
11
8
|
exports.markDeferredStitchCompleted = markDeferredStitchCompleted;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markDeferredStitchCompleted.cjs.js","sources":["../../../../src/database/operations/stitcher/markDeferredStitchCompleted.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { Knex } from 'knex';\nimport {
|
|
1
|
+
{"version":3,"file":"markDeferredStitchCompleted.cjs.js","sources":["../../../../src/database/operations/stitcher/markDeferredStitchCompleted.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { Knex } from 'knex';\nimport { DbStitchQueueRow } from '../../tables';\n\n/**\n * Marks a single entity as having been stitched.\n *\n * @remarks\n *\n * This assumes that the stitching strategy is set to deferred.\n *\n * The row is only deleted from stitch_queue if the ticket hasn't changed. If\n * it has, it means that a new stitch request has been made, and the entity\n * should be stitched once more some time in the future - or is indeed already\n * being stitched concurrently with ourselves.\n */\nexport async function markDeferredStitchCompleted(option: {\n knex: Knex | Knex.Transaction;\n entityRef: string;\n stitchTicket: string;\n}): Promise<void> {\n const { knex, entityRef, stitchTicket } = option;\n\n await knex<DbStitchQueueRow>('stitch_queue')\n .where('entity_ref', '=', entityRef)\n .andWhere('stitch_ticket', '=', stitchTicket)\n .delete();\n}\n"],"names":[],"mappings":";;AA+BA,eAAsB,4BAA4B,MAAA,EAIhC;AAChB,EAAA,MAAM,EAAE,IAAA,EAAM,SAAA,EAAW,YAAA,EAAa,GAAI,MAAA;AAE1C,EAAA,MAAM,IAAA,CAAuB,cAAc,CAAA,CACxC,KAAA,CAAM,YAAA,EAAc,GAAA,EAAK,SAAS,CAAA,CAClC,QAAA,CAAS,eAAA,EAAiB,GAAA,EAAK,YAAY,EAC3C,MAAA,EAAO;AACZ;;;;"}
|
|
@@ -41,18 +41,29 @@ async function markForStitching(options) {
|
|
|
41
41
|
const ticket = uuid.v4();
|
|
42
42
|
for (const chunk of entityRefs) {
|
|
43
43
|
await util.retryOnDeadlock(async () => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
if (chunk.length > 0) {
|
|
45
|
+
await knex("stitch_queue").insert(
|
|
46
|
+
chunk.map((ref) => ({
|
|
47
|
+
entity_ref: ref,
|
|
48
|
+
stitch_ticket: ticket,
|
|
49
|
+
next_stitch_at: knex.fn.now()
|
|
50
|
+
}))
|
|
51
|
+
).onConflict("entity_ref").merge(["next_stitch_at", "stitch_ticket"]);
|
|
52
|
+
}
|
|
48
53
|
}, knex);
|
|
49
54
|
}
|
|
50
55
|
for (const chunk of entityIds) {
|
|
51
56
|
await util.retryOnDeadlock(async () => {
|
|
52
|
-
await knex("refresh_state").
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
const refreshStateRows = await knex("refresh_state").select("entity_ref").whereIn("entity_id", chunk);
|
|
58
|
+
if (refreshStateRows.length > 0) {
|
|
59
|
+
await knex("stitch_queue").insert(
|
|
60
|
+
refreshStateRows.map((row) => ({
|
|
61
|
+
entity_ref: row.entity_ref,
|
|
62
|
+
stitch_ticket: ticket,
|
|
63
|
+
next_stitch_at: knex.fn.now()
|
|
64
|
+
}))
|
|
65
|
+
).onConflict("entity_ref").merge(["next_stitch_at", "stitch_ticket"]);
|
|
66
|
+
}
|
|
56
67
|
}, knex);
|
|
57
68
|
}
|
|
58
69
|
} else {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markForStitching.cjs.js","sources":["../../../../src/database/operations/stitcher/markForStitching.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { Knex } from 'knex';\nimport splitToChunks from 'lodash/chunk';\nimport { v4 as uuid } from 'uuid';\nimport { StitchingStrategy } from '../../../stitching/types';\nimport {
|
|
1
|
+
{"version":3,"file":"markForStitching.cjs.js","sources":["../../../../src/database/operations/stitcher/markForStitching.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { Knex } from 'knex';\nimport splitToChunks from 'lodash/chunk';\nimport { v4 as uuid } from 'uuid';\nimport { StitchingStrategy } from '../../../stitching/types';\nimport {\n DbFinalEntitiesRow,\n DbRefreshStateRow,\n DbStitchQueueRow,\n} from '../../tables';\nimport { retryOnDeadlock } from '../../util';\n\nconst UPDATE_CHUNK_SIZE = 100; // Smaller chunks reduce contention\n\n/**\n * Marks a number of entities for stitching some time in the near\n * future.\n *\n * @remarks\n */\nexport async function markForStitching(options: {\n knex: Knex | Knex.Transaction;\n strategy: StitchingStrategy;\n entityRefs?: Iterable<string>;\n entityIds?: Iterable<string>;\n}): Promise<void> {\n const entityRefs = sortSplit(options.entityRefs);\n const entityIds = sortSplit(options.entityIds);\n const knex = options.knex;\n const mode = options.strategy.mode;\n\n if (mode === 'immediate') {\n for (const chunk of entityRefs) {\n await knex\n .table<DbFinalEntitiesRow>('final_entities')\n .update({\n hash: 'force-stitching',\n })\n .whereIn('entity_ref', chunk);\n await retryOnDeadlock(async () => {\n await knex\n .table<DbRefreshStateRow>('refresh_state')\n .update({\n result_hash: 'force-stitching',\n next_update_at: knex.fn.now(),\n })\n .whereIn('entity_ref', chunk);\n }, knex);\n }\n\n for (const chunk of entityIds) {\n await knex\n .table<DbFinalEntitiesRow>('final_entities')\n .update({\n hash: 'force-stitching',\n })\n .whereIn('entity_id', chunk);\n await retryOnDeadlock(async () => {\n await knex\n .table<DbRefreshStateRow>('refresh_state')\n .update({\n result_hash: 'force-stitching',\n next_update_at: knex.fn.now(),\n })\n .whereIn('entity_id', chunk);\n }, knex);\n }\n } else if (mode === 'deferred') {\n // It's OK that this is shared across stitch_queue rows; it just needs to\n // be uniquely generated for every new stitch request.\n const ticket = uuid();\n\n for (const chunk of entityRefs) {\n await retryOnDeadlock(async () => {\n if (chunk.length > 0) {\n await knex<DbStitchQueueRow>('stitch_queue')\n .insert(\n chunk.map(ref => ({\n entity_ref: ref,\n stitch_ticket: ticket,\n next_stitch_at: knex.fn.now(),\n })),\n )\n .onConflict('entity_ref')\n .merge(['next_stitch_at', 'stitch_ticket']);\n }\n }, knex);\n }\n\n for (const chunk of entityIds) {\n await retryOnDeadlock(async () => {\n // Look up entity_refs from refresh_state by entity_id\n const refreshStateRows = await knex<DbRefreshStateRow>('refresh_state')\n .select('entity_ref')\n .whereIn('entity_id', chunk);\n\n if (refreshStateRows.length > 0) {\n await knex<DbStitchQueueRow>('stitch_queue')\n .insert(\n refreshStateRows.map(row => ({\n entity_ref: row.entity_ref,\n stitch_ticket: ticket,\n next_stitch_at: knex.fn.now(),\n })),\n )\n .onConflict('entity_ref')\n .merge(['next_stitch_at', 'stitch_ticket']);\n }\n }, knex);\n }\n } else {\n throw new Error(`Unknown stitching strategy mode ${mode}`);\n }\n}\n\nfunction sortSplit(input: Iterable<string> | undefined): string[][] {\n if (!input) {\n return [];\n }\n const array = Array.isArray(input) ? input.slice() : [...input];\n array.sort();\n return splitToChunks(array, UPDATE_CHUNK_SIZE);\n}\n"],"names":["retryOnDeadlock","uuid","splitToChunks"],"mappings":"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"}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
var catalogClient = require('@backstage/catalog-client');
|
|
4
4
|
var catalogModel = require('@backstage/catalog-model');
|
|
5
|
-
var uuid = require('uuid');
|
|
6
5
|
var buildEntitySearch = require('./buildEntitySearch.cjs.js');
|
|
7
6
|
var markDeferredStitchCompleted = require('./markDeferredStitchCompleted.cjs.js');
|
|
8
7
|
var util = require('./util.cjs.js');
|
|
@@ -14,7 +13,7 @@ const scriptProtocolPattern = (
|
|
|
14
13
|
);
|
|
15
14
|
async function performStitching(options) {
|
|
16
15
|
const { knex, logger, entityRef } = options;
|
|
17
|
-
const stitchTicket = options.stitchTicket
|
|
16
|
+
const stitchTicket = options.stitchTicket;
|
|
18
17
|
let removeFromStitchQueueOnCompletion = options.strategy.mode === "deferred";
|
|
19
18
|
try {
|
|
20
19
|
const entityResult = await knex("refresh_state").where({ entity_ref: entityRef }).limit(1).select("entity_id");
|
|
@@ -25,9 +24,8 @@ async function performStitching(options) {
|
|
|
25
24
|
await knex("final_entities").insert({
|
|
26
25
|
entity_id: entityResult[0].entity_id,
|
|
27
26
|
hash: "",
|
|
28
|
-
entity_ref: entityRef
|
|
29
|
-
|
|
30
|
-
}).onConflict("entity_id").merge(["stitch_ticket"]);
|
|
27
|
+
entity_ref: entityRef
|
|
28
|
+
}).onConflict("entity_id").ignore();
|
|
31
29
|
} catch (error) {
|
|
32
30
|
if (backendPluginApi.isDatabaseConflictError(error)) {
|
|
33
31
|
logger.debug(`Skipping stitching of ${entityRef}, conflict`, error);
|
|
@@ -121,11 +119,17 @@ async function performStitching(options) {
|
|
|
121
119
|
entity.metadata.etag = hash;
|
|
122
120
|
}
|
|
123
121
|
const searchEntries = buildEntitySearch.buildEntitySearch(entityId, entity);
|
|
124
|
-
|
|
122
|
+
let updateQuery = knex("final_entities").update({
|
|
125
123
|
final_entity: JSON.stringify(entity),
|
|
126
124
|
hash,
|
|
127
125
|
last_updated_at: knex.fn.now()
|
|
128
|
-
}).where("entity_id", entityId)
|
|
126
|
+
}).where("entity_id", entityId);
|
|
127
|
+
if (options.strategy.mode === "deferred" && stitchTicket) {
|
|
128
|
+
updateQuery = updateQuery.whereExists(
|
|
129
|
+
knex("stitch_queue").where("stitch_queue.entity_ref", entityRef).where("stitch_queue.stitch_ticket", stitchTicket).select(knex.raw("1"))
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
const amountOfRowsChanged = await updateQuery;
|
|
129
133
|
if (amountOfRowsChanged === 0) {
|
|
130
134
|
logger.debug(`Entity ${entityRef} is already stitched, skipping write.`);
|
|
131
135
|
return "abandoned";
|
|
@@ -139,7 +143,7 @@ async function performStitching(options) {
|
|
|
139
143
|
removeFromStitchQueueOnCompletion = false;
|
|
140
144
|
throw error;
|
|
141
145
|
} finally {
|
|
142
|
-
if (removeFromStitchQueueOnCompletion) {
|
|
146
|
+
if (removeFromStitchQueueOnCompletion && stitchTicket) {
|
|
143
147
|
await markDeferredStitchCompleted.markDeferredStitchCompleted({
|
|
144
148
|
knex,
|
|
145
149
|
entityRef,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"performStitching.cjs.js","sources":["../../../../src/database/operations/stitcher/performStitching.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from '@backstage/catalog-client';\nimport {\n ANNOTATION_EDIT_URL,\n ANNOTATION_VIEW_URL,\n EntityRelation,\n} from '@backstage/catalog-model';\nimport { AlphaEntity, EntityStatusItem } from '@backstage/catalog-model/alpha';\nimport { SerializedError } from '@backstage/errors';\nimport { Knex } from 'knex';\nimport { v4 as uuid } from 'uuid';\nimport { StitchingStrategy } from '../../../stitching/types';\nimport {\n DbFinalEntitiesRow,\n DbRefreshStateRow,\n DbSearchRow,\n} from '../../tables';\nimport { buildEntitySearch } from './buildEntitySearch';\nimport { markDeferredStitchCompleted } from './markDeferredStitchCompleted';\nimport { BATCH_SIZE, generateStableHash } from './util';\nimport {\n LoggerService,\n isDatabaseConflictError,\n} from '@backstage/backend-plugin-api';\n\n// See https://github.com/facebook/react/blob/f0cf832e1d0c8544c36aa8b310960885a11a847c/packages/react-dom-bindings/src/shared/sanitizeURL.js\nconst scriptProtocolPattern =\n // eslint-disable-next-line no-control-regex\n /^[\\u0000-\\u001F ]*j[\\r\\n\\t]*a[\\r\\n\\t]*v[\\r\\n\\t]*a[\\r\\n\\t]*s[\\r\\n\\t]*c[\\r\\n\\t]*r[\\r\\n\\t]*i[\\r\\n\\t]*p[\\r\\n\\t]*t[\\r\\n\\t]*\\:/i;\n\n/**\n * Performs the act of stitching - to take all of the various outputs from the\n * ingestion process, and stitching them together into the final entity JSON\n * shape.\n */\nexport async function performStitching(options: {\n knex: Knex | Knex.Transaction;\n logger: LoggerService;\n strategy: StitchingStrategy;\n entityRef: string;\n stitchTicket?: string;\n}): Promise<'changed' | 'unchanged' | 'abandoned'> {\n const { knex, logger, entityRef } = options;\n const stitchTicket = options.stitchTicket ?? uuid();\n\n // In deferred mode, the entity is removed from the stitch queue on ANY\n // completion, except when an exception is thrown. In the latter case, the\n // entity will be retried at a later time.\n let removeFromStitchQueueOnCompletion = options.strategy.mode === 'deferred';\n\n try {\n const entityResult = await knex<DbRefreshStateRow>('refresh_state')\n .where({ entity_ref: entityRef })\n .limit(1)\n .select('entity_id');\n if (!entityResult.length) {\n // Entity does no exist in refresh state table, no stitching required.\n return 'abandoned';\n }\n\n // Insert stitching ticket that will be compared before inserting the final entity.\n try {\n await knex<DbFinalEntitiesRow>('final_entities')\n .insert({\n entity_id: entityResult[0].entity_id,\n hash: '',\n entity_ref: entityRef,\n stitch_ticket: stitchTicket,\n })\n .onConflict('entity_id')\n .merge(['stitch_ticket']);\n } catch (error) {\n // It's possible to hit a race where a refresh_state table delete + insert\n // is done just after we read the entity_id from it. This conflict is safe\n // to ignore because the current stitching operation will be triggered by\n // the old entry, and the new entry will trigger it's own stitching that\n // will update the entity.\n if (isDatabaseConflictError(error)) {\n logger.debug(`Skipping stitching of ${entityRef}, conflict`, error);\n return 'abandoned';\n }\n\n throw error;\n }\n\n // Selecting from refresh_state and final_entities should yield exactly\n // one row (except in abnormal cases where the stitch was invoked for\n // something that didn't exist at all, in which case it's zero rows).\n // The join with the temporary incoming_references still gives one row.\n const [processedResult, relationsResult] = await Promise.all([\n knex\n .with('incoming_references', function incomingReferences(builder) {\n return builder\n .from('refresh_state_references')\n .where({ target_entity_ref: entityRef })\n .count({ count: '*' });\n })\n .select({\n entityId: 'refresh_state.entity_id',\n processedEntity: 'refresh_state.processed_entity',\n errors: 'refresh_state.errors',\n incomingReferenceCount: 'incoming_references.count',\n previousHash: 'final_entities.hash',\n })\n .from('refresh_state')\n .where({ 'refresh_state.entity_ref': entityRef })\n .crossJoin(knex.raw('incoming_references'))\n .leftOuterJoin('final_entities', {\n 'final_entities.entity_id': 'refresh_state.entity_id',\n }),\n knex\n .distinct({\n relationType: 'type',\n relationTarget: 'target_entity_ref',\n })\n .from('relations')\n .where({ source_entity_ref: entityRef })\n .orderBy('relationType', 'asc')\n .orderBy('relationTarget', 'asc'),\n ]);\n\n // If there were no rows returned, it would mean that there was no\n // matching row even in the refresh_state. This can happen for example\n // if we emit a relation to something that hasn't been ingested yet.\n // It's safe to ignore this stitch attempt in that case.\n if (!processedResult.length) {\n logger.debug(\n `Unable to stitch ${entityRef}, item does not exist in refresh state table`,\n );\n return 'abandoned';\n }\n\n const {\n entityId,\n processedEntity,\n errors,\n incomingReferenceCount,\n previousHash,\n } = processedResult[0];\n\n // If there was no processed entity in place, the target hasn't been\n // through the processing steps yet. It's safe to ignore this stitch\n // attempt in that case, since another stitch will be triggered when\n // that processing has finished.\n if (!processedEntity) {\n logger.debug(\n `Unable to stitch ${entityRef}, the entity has not yet been processed`,\n );\n return 'abandoned';\n }\n\n // Grab the processed entity and stitch all of the relevant data into\n // it\n const entity = JSON.parse(processedEntity) as AlphaEntity;\n const isOrphan = Number(incomingReferenceCount) === 0;\n let statusItems: EntityStatusItem[] = [];\n\n if (isOrphan) {\n logger.debug(`${entityRef} is an orphan`);\n entity.metadata.annotations = {\n ...entity.metadata.annotations,\n ['backstage.io/orphan']: 'true',\n };\n }\n if (errors) {\n const parsedErrors = JSON.parse(errors) as SerializedError[];\n if (Array.isArray(parsedErrors) && parsedErrors.length) {\n statusItems = parsedErrors.map(e => ({\n type: ENTITY_STATUS_CATALOG_PROCESSING_TYPE,\n level: 'error',\n message: `${e.name}: ${e.message}`,\n error: e,\n }));\n }\n }\n // We opt to do this check here as we otherwise can't guarantee that it will be run after all processors\n for (const annotation of [ANNOTATION_VIEW_URL, ANNOTATION_EDIT_URL]) {\n const value = entity.metadata.annotations?.[annotation];\n if (typeof value === 'string' && scriptProtocolPattern.test(value)) {\n entity.metadata.annotations![annotation] =\n 'https://backstage.io/annotation-rejected-for-security-reasons';\n }\n }\n\n // TODO: entityRef is lower case and should be uppercase in the final\n // result\n entity.relations = relationsResult\n .filter(row => row.relationType /* exclude null row, if relevant */)\n .map<EntityRelation>(row => ({\n type: row.relationType!,\n targetRef: row.relationTarget!,\n }));\n if (statusItems.length) {\n entity.status = {\n ...entity.status,\n items: [...(entity.status?.items ?? []), ...statusItems],\n };\n }\n\n // If the output entity was actually not changed, just abort\n const hash = generateStableHash(entity);\n if (hash === previousHash) {\n logger.debug(`Skipped stitching of ${entityRef}, no changes`);\n return 'unchanged';\n }\n\n entity.metadata.uid = entityId;\n if (!entity.metadata.etag) {\n // If the original data source did not have its own etag handling,\n // use the hash as a good-quality etag\n entity.metadata.etag = hash;\n }\n\n // This may throw if the entity is invalid, so we call it before\n // the final_entities write, even though we may end up not needing\n // to write the search index.\n const searchEntries = buildEntitySearch(entityId, entity);\n\n const amountOfRowsChanged = await knex<DbFinalEntitiesRow>('final_entities')\n .update({\n final_entity: JSON.stringify(entity),\n hash,\n last_updated_at: knex.fn.now(),\n })\n .where('entity_id', entityId)\n .where('stitch_ticket', stitchTicket);\n\n if (amountOfRowsChanged === 0) {\n logger.debug(`Entity ${entityRef} is already stitched, skipping write.`);\n return 'abandoned';\n }\n\n await knex.transaction(async trx => {\n await trx<DbSearchRow>('search').where({ entity_id: entityId }).delete();\n await trx.batchInsert('search', searchEntries, BATCH_SIZE);\n });\n\n return 'changed';\n } catch (error) {\n removeFromStitchQueueOnCompletion = false;\n throw error;\n } finally {\n if (removeFromStitchQueueOnCompletion) {\n await markDeferredStitchCompleted({\n knex: knex,\n entityRef,\n stitchTicket,\n });\n }\n }\n}\n"],"names":["uuid","isDatabaseConflictError","ENTITY_STATUS_CATALOG_PROCESSING_TYPE","ANNOTATION_VIEW_URL","ANNOTATION_EDIT_URL","generateStableHash","buildEntitySearch","BATCH_SIZE","markDeferredStitchCompleted"],"mappings":";;;;;;;;;;AAyCA,MAAM,qBAAA;AAAA;AAAA,EAEJ;AAAA,CAAA;AAOF,eAAsB,iBAAiB,OAAA,EAMY;AACjD,EAAA,MAAM,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAA,EAAU,GAAI,OAAA;AACpC,EAAA,MAAM,YAAA,GAAe,OAAA,CAAQ,YAAA,IAAgBA,OAAA,EAAK;AAKlD,EAAA,IAAI,iCAAA,GAAoC,OAAA,CAAQ,QAAA,CAAS,IAAA,KAAS,UAAA;AAElE,EAAA,IAAI;AACF,IAAA,MAAM,YAAA,GAAe,MAAM,IAAA,CAAwB,eAAe,EAC/D,KAAA,CAAM,EAAE,UAAA,EAAY,SAAA,EAAW,CAAA,CAC/B,KAAA,CAAM,CAAC,CAAA,CACP,OAAO,WAAW,CAAA;AACrB,IAAA,IAAI,CAAC,aAAa,MAAA,EAAQ;AAExB,MAAA,OAAO,WAAA;AAAA,IACT;AAGA,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,CAAyB,gBAAgB,CAAA,CAC5C,MAAA,CAAO;AAAA,QACN,SAAA,EAAW,YAAA,CAAa,CAAC,CAAA,CAAE,SAAA;AAAA,QAC3B,IAAA,EAAM,EAAA;AAAA,QACN,UAAA,EAAY,SAAA;AAAA,QACZ,aAAA,EAAe;AAAA,OAChB,EACA,UAAA,CAAW,WAAW,EACtB,KAAA,CAAM,CAAC,eAAe,CAAC,CAAA;AAAA,IAC5B,SAAS,KAAA,EAAO;AAMd,MAAA,IAAIC,wCAAA,CAAwB,KAAK,CAAA,EAAG;AAClC,QAAA,MAAA,CAAO,KAAA,CAAM,CAAA,sBAAA,EAAyB,SAAS,CAAA,UAAA,CAAA,EAAc,KAAK,CAAA;AAClE,QAAA,OAAO,WAAA;AAAA,MACT;AAEA,MAAA,MAAM,KAAA;AAAA,IACR;AAMA,IAAA,MAAM,CAAC,eAAA,EAAiB,eAAe,CAAA,GAAI,MAAM,QAAQ,GAAA,CAAI;AAAA,MAC3D,IAAA,CACG,IAAA,CAAK,qBAAA,EAAuB,SAAS,mBAAmB,OAAA,EAAS;AAChE,QAAA,OAAO,OAAA,CACJ,IAAA,CAAK,0BAA0B,CAAA,CAC/B,MAAM,EAAE,iBAAA,EAAmB,SAAA,EAAW,CAAA,CACtC,KAAA,CAAM,EAAE,KAAA,EAAO,KAAK,CAAA;AAAA,MACzB,CAAC,EACA,MAAA,CAAO;AAAA,QACN,QAAA,EAAU,yBAAA;AAAA,QACV,eAAA,EAAiB,gCAAA;AAAA,QACjB,MAAA,EAAQ,sBAAA;AAAA,QACR,sBAAA,EAAwB,2BAAA;AAAA,QACxB,YAAA,EAAc;AAAA,OACf,CAAA,CACA,IAAA,CAAK,eAAe,CAAA,CACpB,KAAA,CAAM,EAAE,0BAAA,EAA4B,SAAA,EAAW,CAAA,CAC/C,UAAU,IAAA,CAAK,GAAA,CAAI,qBAAqB,CAAC,CAAA,CACzC,cAAc,gBAAA,EAAkB;AAAA,QAC/B,0BAAA,EAA4B;AAAA,OAC7B,CAAA;AAAA,MACH,KACG,QAAA,CAAS;AAAA,QACR,YAAA,EAAc,MAAA;AAAA,QACd,cAAA,EAAgB;AAAA,OACjB,CAAA,CACA,IAAA,CAAK,WAAW,CAAA,CAChB,MAAM,EAAE,iBAAA,EAAmB,SAAA,EAAW,EACtC,OAAA,CAAQ,cAAA,EAAgB,KAAK,CAAA,CAC7B,OAAA,CAAQ,kBAAkB,KAAK;AAAA,KACnC,CAAA;AAMD,IAAA,IAAI,CAAC,gBAAgB,MAAA,EAAQ;AAC3B,MAAA,MAAA,CAAO,KAAA;AAAA,QACL,oBAAoB,SAAS,CAAA,4CAAA;AAAA,OAC/B;AACA,MAAA,OAAO,WAAA;AAAA,IACT;AAEA,IAAA,MAAM;AAAA,MACJ,QAAA;AAAA,MACA,eAAA;AAAA,MACA,MAAA;AAAA,MACA,sBAAA;AAAA,MACA;AAAA,KACF,GAAI,gBAAgB,CAAC,CAAA;AAMrB,IAAA,IAAI,CAAC,eAAA,EAAiB;AACpB,MAAA,MAAA,CAAO,KAAA;AAAA,QACL,oBAAoB,SAAS,CAAA,uCAAA;AAAA,OAC/B;AACA,MAAA,OAAO,WAAA;AAAA,IACT;AAIA,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,eAAe,CAAA;AACzC,IAAA,MAAM,QAAA,GAAW,MAAA,CAAO,sBAAsB,CAAA,KAAM,CAAA;AACpD,IAAA,IAAI,cAAkC,EAAC;AAEvC,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,SAAS,CAAA,aAAA,CAAe,CAAA;AACxC,MAAA,MAAA,CAAO,SAAS,WAAA,GAAc;AAAA,QAC5B,GAAG,OAAO,QAAA,CAAS,WAAA;AAAA,QACnB,CAAC,qBAAqB,GAAG;AAAA,OAC3B;AAAA,IACF;AACA,IAAA,IAAI,MAAA,EAAQ;AACV,MAAA,MAAM,YAAA,GAAe,IAAA,CAAK,KAAA,CAAM,MAAM,CAAA;AACtC,MAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,YAAY,CAAA,IAAK,aAAa,MAAA,EAAQ;AACtD,QAAA,WAAA,GAAc,YAAA,CAAa,IAAI,CAAA,CAAA,MAAM;AAAA,UACnC,IAAA,EAAMC,mDAAA;AAAA,UACN,KAAA,EAAO,OAAA;AAAA,UACP,SAAS,CAAA,EAAG,CAAA,CAAE,IAAI,CAAA,EAAA,EAAK,EAAE,OAAO,CAAA,CAAA;AAAA,UAChC,KAAA,EAAO;AAAA,SACT,CAAE,CAAA;AAAA,MACJ;AAAA,IACF;AAEA,IAAA,KAAA,MAAW,UAAA,IAAc,CAACC,gCAAA,EAAqBC,gCAAmB,CAAA,EAAG;AACnE,MAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,QAAA,CAAS,WAAA,GAAc,UAAU,CAAA;AACtD,MAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,qBAAA,CAAsB,IAAA,CAAK,KAAK,CAAA,EAAG;AAClE,QAAA,MAAA,CAAO,QAAA,CAAS,WAAA,CAAa,UAAU,CAAA,GACrC,+DAAA;AAAA,MACJ;AAAA,IACF;AAIA,IAAA,MAAA,CAAO,YAAY,eAAA,CAChB,MAAA;AAAA,MAAO,SAAO,GAAA,CAAI;AAAA;AAAA,KAAgD,CAClE,IAAoB,CAAA,GAAA,MAAQ;AAAA,MAC3B,MAAM,GAAA,CAAI,YAAA;AAAA,MACV,WAAW,GAAA,CAAI;AAAA,KACjB,CAAE,CAAA;AACJ,IAAA,IAAI,YAAY,MAAA,EAAQ;AACtB,MAAA,MAAA,CAAO,MAAA,GAAS;AAAA,QACd,GAAG,MAAA,CAAO,MAAA;AAAA,QACV,KAAA,EAAO,CAAC,GAAI,MAAA,CAAO,QAAQ,KAAA,IAAS,EAAC,EAAI,GAAG,WAAW;AAAA,OACzD;AAAA,IACF;AAGA,IAAA,MAAM,IAAA,GAAOC,wBAAmB,MAAM,CAAA;AACtC,IAAA,IAAI,SAAS,YAAA,EAAc;AACzB,MAAA,MAAA,CAAO,KAAA,CAAM,CAAA,qBAAA,EAAwB,SAAS,CAAA,YAAA,CAAc,CAAA;AAC5D,MAAA,OAAO,WAAA;AAAA,IACT;AAEA,IAAA,MAAA,CAAO,SAAS,GAAA,GAAM,QAAA;AACtB,IAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,IAAA,EAAM;AAGzB,MAAA,MAAA,CAAO,SAAS,IAAA,GAAO,IAAA;AAAA,IACzB;AAKA,IAAA,MAAM,aAAA,GAAgBC,mCAAA,CAAkB,QAAA,EAAU,MAAM,CAAA;AAExD,IAAA,MAAM,mBAAA,GAAsB,MAAM,IAAA,CAAyB,gBAAgB,EACxE,MAAA,CAAO;AAAA,MACN,YAAA,EAAc,IAAA,CAAK,SAAA,CAAU,MAAM,CAAA;AAAA,MACnC,IAAA;AAAA,MACA,eAAA,EAAiB,IAAA,CAAK,EAAA,CAAG,GAAA;AAAI,KAC9B,EACA,KAAA,CAAM,WAAA,EAAa,QAAQ,CAAA,CAC3B,KAAA,CAAM,iBAAiB,YAAY,CAAA;AAEtC,IAAA,IAAI,wBAAwB,CAAA,EAAG;AAC7B,MAAA,MAAA,CAAO,KAAA,CAAM,CAAA,OAAA,EAAU,SAAS,CAAA,qCAAA,CAAuC,CAAA;AACvE,MAAA,OAAO,WAAA;AAAA,IACT;AAEA,IAAA,MAAM,IAAA,CAAK,WAAA,CAAY,OAAM,GAAA,KAAO;AAClC,MAAA,MAAM,GAAA,CAAiB,QAAQ,CAAA,CAAE,KAAA,CAAM,EAAE,SAAA,EAAW,QAAA,EAAU,CAAA,CAAE,MAAA,EAAO;AACvE,MAAA,MAAM,GAAA,CAAI,WAAA,CAAY,QAAA,EAAU,aAAA,EAAeC,eAAU,CAAA;AAAA,IAC3D,CAAC,CAAA;AAED,IAAA,OAAO,SAAA;AAAA,EACT,SAAS,KAAA,EAAO;AACd,IAAA,iCAAA,GAAoC,KAAA;AACpC,IAAA,MAAM,KAAA;AAAA,EACR,CAAA,SAAE;AACA,IAAA,IAAI,iCAAA,EAAmC;AACrC,MAAA,MAAMC,uDAAA,CAA4B;AAAA,QAChC,IAAA;AAAA,QACA,SAAA;AAAA,QACA;AAAA,OACD,CAAA;AAAA,IACH;AAAA,EACF;AACF;;;;"}
|
|
1
|
+
{"version":3,"file":"performStitching.cjs.js","sources":["../../../../src/database/operations/stitcher/performStitching.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from '@backstage/catalog-client';\nimport {\n ANNOTATION_EDIT_URL,\n ANNOTATION_VIEW_URL,\n EntityRelation,\n} from '@backstage/catalog-model';\nimport { AlphaEntity, EntityStatusItem } from '@backstage/catalog-model/alpha';\nimport { SerializedError } from '@backstage/errors';\nimport { Knex } from 'knex';\nimport { StitchingStrategy } from '../../../stitching/types';\nimport {\n DbFinalEntitiesRow,\n DbRefreshStateRow,\n DbSearchRow,\n DbStitchQueueRow,\n} from '../../tables';\nimport { buildEntitySearch } from './buildEntitySearch';\nimport { markDeferredStitchCompleted } from './markDeferredStitchCompleted';\nimport { BATCH_SIZE, generateStableHash } from './util';\nimport {\n LoggerService,\n isDatabaseConflictError,\n} from '@backstage/backend-plugin-api';\n\n// See https://github.com/facebook/react/blob/f0cf832e1d0c8544c36aa8b310960885a11a847c/packages/react-dom-bindings/src/shared/sanitizeURL.js\nconst scriptProtocolPattern =\n // eslint-disable-next-line no-control-regex\n /^[\\u0000-\\u001F ]*j[\\r\\n\\t]*a[\\r\\n\\t]*v[\\r\\n\\t]*a[\\r\\n\\t]*s[\\r\\n\\t]*c[\\r\\n\\t]*r[\\r\\n\\t]*i[\\r\\n\\t]*p[\\r\\n\\t]*t[\\r\\n\\t]*\\:/i;\n\n/**\n * Performs the act of stitching - to take all of the various outputs from the\n * ingestion process, and stitching them together into the final entity JSON\n * shape.\n */\nexport async function performStitching(options: {\n knex: Knex | Knex.Transaction;\n logger: LoggerService;\n strategy: StitchingStrategy;\n entityRef: string;\n stitchTicket?: string;\n}): Promise<'changed' | 'unchanged' | 'abandoned'> {\n const { knex, logger, entityRef } = options;\n const stitchTicket = options.stitchTicket;\n\n // In deferred mode, the entity is removed from the stitch queue on ANY\n // completion, except when an exception is thrown. In the latter case, the\n // entity will be retried at a later time.\n let removeFromStitchQueueOnCompletion = options.strategy.mode === 'deferred';\n\n try {\n const entityResult = await knex<DbRefreshStateRow>('refresh_state')\n .where({ entity_ref: entityRef })\n .limit(1)\n .select('entity_id');\n if (!entityResult.length) {\n // Entity does no exist in refresh state table, no stitching required.\n return 'abandoned';\n }\n\n // Ensure that a final_entities row exists for this entity.\n try {\n await knex<DbFinalEntitiesRow>('final_entities')\n .insert({\n entity_id: entityResult[0].entity_id,\n hash: '',\n entity_ref: entityRef,\n })\n .onConflict('entity_id')\n .ignore();\n } catch (error) {\n // It's possible to hit a race where a refresh_state table delete + insert\n // is done just after we read the entity_id from it. This conflict is safe\n // to ignore because the current stitching operation will be triggered by\n // the old entry, and the new entry will trigger it's own stitching that\n // will update the entity.\n if (isDatabaseConflictError(error)) {\n logger.debug(`Skipping stitching of ${entityRef}, conflict`, error);\n return 'abandoned';\n }\n\n throw error;\n }\n\n // Selecting from refresh_state and final_entities should yield exactly\n // one row (except in abnormal cases where the stitch was invoked for\n // something that didn't exist at all, in which case it's zero rows).\n // The join with the temporary incoming_references still gives one row.\n const [processedResult, relationsResult] = await Promise.all([\n knex\n .with('incoming_references', function incomingReferences(builder) {\n return builder\n .from('refresh_state_references')\n .where({ target_entity_ref: entityRef })\n .count({ count: '*' });\n })\n .select({\n entityId: 'refresh_state.entity_id',\n processedEntity: 'refresh_state.processed_entity',\n errors: 'refresh_state.errors',\n incomingReferenceCount: 'incoming_references.count',\n previousHash: 'final_entities.hash',\n })\n .from('refresh_state')\n .where({ 'refresh_state.entity_ref': entityRef })\n .crossJoin(knex.raw('incoming_references'))\n .leftOuterJoin('final_entities', {\n 'final_entities.entity_id': 'refresh_state.entity_id',\n }),\n knex\n .distinct({\n relationType: 'type',\n relationTarget: 'target_entity_ref',\n })\n .from('relations')\n .where({ source_entity_ref: entityRef })\n .orderBy('relationType', 'asc')\n .orderBy('relationTarget', 'asc'),\n ]);\n\n // If there were no rows returned, it would mean that there was no\n // matching row even in the refresh_state. This can happen for example\n // if we emit a relation to something that hasn't been ingested yet.\n // It's safe to ignore this stitch attempt in that case.\n if (!processedResult.length) {\n logger.debug(\n `Unable to stitch ${entityRef}, item does not exist in refresh state table`,\n );\n return 'abandoned';\n }\n\n const {\n entityId,\n processedEntity,\n errors,\n incomingReferenceCount,\n previousHash,\n } = processedResult[0];\n\n // If there was no processed entity in place, the target hasn't been\n // through the processing steps yet. It's safe to ignore this stitch\n // attempt in that case, since another stitch will be triggered when\n // that processing has finished.\n if (!processedEntity) {\n logger.debug(\n `Unable to stitch ${entityRef}, the entity has not yet been processed`,\n );\n return 'abandoned';\n }\n\n // Grab the processed entity and stitch all of the relevant data into\n // it\n const entity = JSON.parse(processedEntity) as AlphaEntity;\n const isOrphan = Number(incomingReferenceCount) === 0;\n let statusItems: EntityStatusItem[] = [];\n\n if (isOrphan) {\n logger.debug(`${entityRef} is an orphan`);\n entity.metadata.annotations = {\n ...entity.metadata.annotations,\n ['backstage.io/orphan']: 'true',\n };\n }\n if (errors) {\n const parsedErrors = JSON.parse(errors) as SerializedError[];\n if (Array.isArray(parsedErrors) && parsedErrors.length) {\n statusItems = parsedErrors.map(e => ({\n type: ENTITY_STATUS_CATALOG_PROCESSING_TYPE,\n level: 'error',\n message: `${e.name}: ${e.message}`,\n error: e,\n }));\n }\n }\n // We opt to do this check here as we otherwise can't guarantee that it will be run after all processors\n for (const annotation of [ANNOTATION_VIEW_URL, ANNOTATION_EDIT_URL]) {\n const value = entity.metadata.annotations?.[annotation];\n if (typeof value === 'string' && scriptProtocolPattern.test(value)) {\n entity.metadata.annotations![annotation] =\n 'https://backstage.io/annotation-rejected-for-security-reasons';\n }\n }\n\n // TODO: entityRef is lower case and should be uppercase in the final\n // result\n entity.relations = relationsResult\n .filter(row => row.relationType /* exclude null row, if relevant */)\n .map<EntityRelation>(row => ({\n type: row.relationType!,\n targetRef: row.relationTarget!,\n }));\n if (statusItems.length) {\n entity.status = {\n ...entity.status,\n items: [...(entity.status?.items ?? []), ...statusItems],\n };\n }\n\n // If the output entity was actually not changed, just abort\n const hash = generateStableHash(entity);\n if (hash === previousHash) {\n logger.debug(`Skipped stitching of ${entityRef}, no changes`);\n return 'unchanged';\n }\n\n entity.metadata.uid = entityId;\n if (!entity.metadata.etag) {\n // If the original data source did not have its own etag handling,\n // use the hash as a good-quality etag\n entity.metadata.etag = hash;\n }\n\n // This may throw if the entity is invalid, so we call it before\n // the final_entities write, even though we may end up not needing\n // to write the search index.\n const searchEntries = buildEntitySearch(entityId, entity);\n\n let updateQuery = knex<DbFinalEntitiesRow>('final_entities')\n .update({\n final_entity: JSON.stringify(entity),\n hash,\n last_updated_at: knex.fn.now(),\n })\n .where('entity_id', entityId);\n\n // In deferred mode, guard against concurrent stitchers by checking that\n // the stitch_ticket in stitch_queue still matches what we were given.\n if (options.strategy.mode === 'deferred' && stitchTicket) {\n updateQuery = updateQuery.whereExists(\n knex<DbStitchQueueRow>('stitch_queue')\n .where('stitch_queue.entity_ref', entityRef)\n .where('stitch_queue.stitch_ticket', stitchTicket)\n .select(knex.raw('1')),\n );\n }\n\n const amountOfRowsChanged = await updateQuery;\n\n if (amountOfRowsChanged === 0) {\n logger.debug(`Entity ${entityRef} is already stitched, skipping write.`);\n return 'abandoned';\n }\n\n await knex.transaction(async trx => {\n await trx<DbSearchRow>('search').where({ entity_id: entityId }).delete();\n await trx.batchInsert('search', searchEntries, BATCH_SIZE);\n });\n\n return 'changed';\n } catch (error) {\n removeFromStitchQueueOnCompletion = false;\n throw error;\n } finally {\n if (removeFromStitchQueueOnCompletion && stitchTicket) {\n await markDeferredStitchCompleted({\n knex: knex,\n entityRef,\n stitchTicket,\n });\n }\n }\n}\n"],"names":["isDatabaseConflictError","ENTITY_STATUS_CATALOG_PROCESSING_TYPE","ANNOTATION_VIEW_URL","ANNOTATION_EDIT_URL","generateStableHash","buildEntitySearch","BATCH_SIZE","markDeferredStitchCompleted"],"mappings":"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"}
|
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
var path = require('node:path');
|
|
4
4
|
var minimatch = require('minimatch');
|
|
5
|
-
var
|
|
5
|
+
var v3 = require('zod/v3');
|
|
6
6
|
|
|
7
7
|
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
8
8
|
|
|
9
9
|
var path__default = /*#__PURE__*/_interopDefaultCompat(path);
|
|
10
10
|
|
|
11
|
-
const allowRuleParser =
|
|
12
|
-
|
|
13
|
-
kind:
|
|
14
|
-
"spec.type":
|
|
15
|
-
}).or(
|
|
11
|
+
const allowRuleParser = v3.z.array(
|
|
12
|
+
v3.z.object({
|
|
13
|
+
kind: v3.z.string(),
|
|
14
|
+
"spec.type": v3.z.string().optional()
|
|
15
|
+
}).or(v3.z.string()).transform((val) => typeof val === "string" ? { kind: val } : val)
|
|
16
16
|
);
|
|
17
17
|
class DefaultCatalogRulesEnforcer {
|
|
18
18
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CatalogRules.cjs.js","sources":["../../src/ingestion/CatalogRules.ts"],"sourcesContent":["/*\n * Copyright 2020 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 { Config } from '@backstage/config';\nimport { Entity } from '@backstage/catalog-model';\nimport path from 'node:path';\nimport { LocationSpec } from '@backstage/plugin-catalog-common';\nimport { minimatch } from 'minimatch';\nimport { z } from 'zod';\n\n/**\n * Rules to apply to catalog entities.\n *\n * An undefined list of matchers means match all, an empty list of matchers means match none.\n */\nexport type CatalogRule = {\n allow: CatalogRuleAllow[];\n locations?: Array<{\n exact?: string;\n type: string;\n pattern?: string;\n }>;\n};\n\ntype CatalogRuleAllow = {\n kind: string;\n 'spec.type'?: string;\n};\n\n/**\n * Decides whether an entity from a given location is allowed to enter the\n * catalog, according to some rule set.\n */\nexport type CatalogRulesEnforcer = {\n isAllowed(entity: Entity, location: LocationSpec): boolean;\n};\n\nconst allowRuleParser = z.array(\n z\n .object({\n kind: z.string(),\n 'spec.type': z.string().optional(),\n })\n .or(z.string())\n .transform(val => (typeof val === 'string' ? { kind: val } : val)),\n);\n\n/**\n * Implements the default catalog rule set, consuming the config keys\n * `catalog.rules` and `catalog.locations.[].rules`.\n */\nexport class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer {\n /**\n * Default rules used by the catalog.\n *\n * Denies any location from specifying user or group entities.\n */\n static readonly defaultRules: CatalogRule[] = [\n {\n allow: ['Component', 'API', 'Location'].map(kind => ({ kind })),\n },\n ];\n\n /**\n * Loads catalog rules from config.\n *\n * This reads `catalog.rules` and defaults to the default rules if no value is present.\n * The value of the config should be a list of config objects, each with a single `allow`\n * field which in turn is a list of entity kinds to allow.\n *\n * If there is no matching rule to allow an ingested entity, it will be rejected by the catalog.\n *\n * It also reads in rules from `catalog.locations`, where each location can have a list\n * of rules for that specific location, specified in a `rules` field.\n *\n * For example:\n *\n * ```yaml\n * catalog:\n * rules:\n * - allow: [Component, API]\n * - allow:\n * - kind: Resource\n * 'spec.type': database\n * - allow: [Template]\n * locations:\n * - type: url\n * pattern: https://github.com/org/*\\/blob/master/template.yaml\n * - allow: [Location]\n * locations:\n * - type: url\n * pattern: https://github.com/org/repo/blob/master/location.yaml\n *\n * locations:\n * - type: url\n * target: https://github.com/org/repo/blob/master/users.yaml\n * rules:\n * - allow: [User, Group]\n * - type: url\n * target: https://github.com/org/repo/blob/master/systems.yaml\n * rules:\n * - allow: [System]\n * ```\n */\n static fromConfig(config: Config) {\n const rules = new Array<CatalogRule>();\n\n if (config.has('catalog.rules')) {\n const globalRules = config\n .getConfigArray('catalog.rules')\n .map(ruleConf => ({\n allow: allowRuleParser.parse(ruleConf.get('allow')),\n locations: ruleConf\n .getOptionalConfigArray('locations')\n ?.map(locationConfig => {\n const location = {\n pattern: locationConfig.getOptionalString('pattern'),\n type: locationConfig.getString('type'),\n exact: locationConfig.getOptionalString('exact'),\n };\n if (location.pattern && location.exact) {\n throw new Error(\n 'A catalog rule location cannot have both exact and pattern values',\n );\n }\n return location;\n }),\n }));\n rules.push(...globalRules);\n } else {\n rules.push(...DefaultCatalogRulesEnforcer.defaultRules);\n }\n\n if (config.has('catalog.locations')) {\n const locationRules = config\n .getConfigArray('catalog.locations')\n .flatMap(locConf => {\n if (!locConf.has('rules')) {\n return [];\n }\n const type = locConf.getString('type');\n const exact = resolveTarget(type, locConf.getString('target'));\n\n return locConf.getConfigArray('rules').map(ruleConf => ({\n allow: ruleConf.getStringArray('allow').map(kind => ({ kind })),\n locations: [{ type, exact }],\n }));\n });\n\n rules.push(...locationRules);\n }\n\n return new DefaultCatalogRulesEnforcer(rules);\n }\n\n private readonly rules: CatalogRule[];\n\n constructor(rules: CatalogRule[]) {\n this.rules = rules;\n }\n\n /**\n * Checks whether a specific entity/location combination is allowed\n * according to the configured rules.\n */\n isAllowed(entity: Entity, location: LocationSpec) {\n for (const rule of this.rules) {\n if (!this.matchLocation(location, rule.locations)) {\n continue;\n }\n\n if (this.matchEntity(entity, rule.allow)) {\n return true;\n }\n }\n\n return false;\n }\n\n private matchLocation(\n location: LocationSpec,\n matchers?: { exact?: string; type: string; pattern?: string }[],\n ): boolean {\n if (!matchers) {\n return true;\n }\n\n for (const matcher of matchers) {\n if (matcher.type !== location?.type) {\n continue;\n }\n if (matcher.exact && matcher.exact !== location?.target) {\n continue;\n }\n if (\n matcher.pattern &&\n !minimatch(location?.target, matcher.pattern, {\n nocase: true,\n dot: true,\n })\n ) {\n continue;\n }\n return true;\n }\n\n return false;\n }\n\n private matchEntity(entity: Entity, matchers?: CatalogRuleAllow[]): boolean {\n if (!matchers) {\n return true;\n }\n\n for (const matcher of matchers) {\n if (\n entity.kind?.toLocaleLowerCase('en-US') !==\n matcher.kind.toLocaleLowerCase('en-US')\n ) {\n continue;\n }\n\n if (matcher['spec.type']) {\n if (typeof entity.spec?.type !== 'string') {\n continue;\n }\n if (\n matcher['spec.type'].toLocaleLowerCase('en-US') !==\n entity.spec.type.toLocaleLowerCase('en-US')\n ) {\n continue;\n }\n }\n\n return true;\n }\n\n return false;\n }\n}\n\nfunction resolveTarget(type: string, target: string): string {\n if (type !== 'file') {\n return target;\n }\n\n return path.resolve(target);\n}\n"],"names":["z","minimatch","path"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CatalogRules.cjs.js","sources":["../../src/ingestion/CatalogRules.ts"],"sourcesContent":["/*\n * Copyright 2020 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 { Config } from '@backstage/config';\nimport { Entity } from '@backstage/catalog-model';\nimport path from 'node:path';\nimport { LocationSpec } from '@backstage/plugin-catalog-common';\nimport { minimatch } from 'minimatch';\nimport { z } from 'zod/v3';\n\n/**\n * Rules to apply to catalog entities.\n *\n * An undefined list of matchers means match all, an empty list of matchers means match none.\n */\nexport type CatalogRule = {\n allow: CatalogRuleAllow[];\n locations?: Array<{\n exact?: string;\n type: string;\n pattern?: string;\n }>;\n};\n\ntype CatalogRuleAllow = {\n kind: string;\n 'spec.type'?: string;\n};\n\n/**\n * Decides whether an entity from a given location is allowed to enter the\n * catalog, according to some rule set.\n */\nexport type CatalogRulesEnforcer = {\n isAllowed(entity: Entity, location: LocationSpec): boolean;\n};\n\nconst allowRuleParser = z.array(\n z\n .object({\n kind: z.string(),\n 'spec.type': z.string().optional(),\n })\n .or(z.string())\n .transform(val => (typeof val === 'string' ? { kind: val } : val)),\n);\n\n/**\n * Implements the default catalog rule set, consuming the config keys\n * `catalog.rules` and `catalog.locations.[].rules`.\n */\nexport class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer {\n /**\n * Default rules used by the catalog.\n *\n * Denies any location from specifying user or group entities.\n */\n static readonly defaultRules: CatalogRule[] = [\n {\n allow: ['Component', 'API', 'Location'].map(kind => ({ kind })),\n },\n ];\n\n /**\n * Loads catalog rules from config.\n *\n * This reads `catalog.rules` and defaults to the default rules if no value is present.\n * The value of the config should be a list of config objects, each with a single `allow`\n * field which in turn is a list of entity kinds to allow.\n *\n * If there is no matching rule to allow an ingested entity, it will be rejected by the catalog.\n *\n * It also reads in rules from `catalog.locations`, where each location can have a list\n * of rules for that specific location, specified in a `rules` field.\n *\n * For example:\n *\n * ```yaml\n * catalog:\n * rules:\n * - allow: [Component, API]\n * - allow:\n * - kind: Resource\n * 'spec.type': database\n * - allow: [Template]\n * locations:\n * - type: url\n * pattern: https://github.com/org/*\\/blob/master/template.yaml\n * - allow: [Location]\n * locations:\n * - type: url\n * pattern: https://github.com/org/repo/blob/master/location.yaml\n *\n * locations:\n * - type: url\n * target: https://github.com/org/repo/blob/master/users.yaml\n * rules:\n * - allow: [User, Group]\n * - type: url\n * target: https://github.com/org/repo/blob/master/systems.yaml\n * rules:\n * - allow: [System]\n * ```\n */\n static fromConfig(config: Config) {\n const rules = new Array<CatalogRule>();\n\n if (config.has('catalog.rules')) {\n const globalRules = config\n .getConfigArray('catalog.rules')\n .map(ruleConf => ({\n allow: allowRuleParser.parse(ruleConf.get('allow')),\n locations: ruleConf\n .getOptionalConfigArray('locations')\n ?.map(locationConfig => {\n const location = {\n pattern: locationConfig.getOptionalString('pattern'),\n type: locationConfig.getString('type'),\n exact: locationConfig.getOptionalString('exact'),\n };\n if (location.pattern && location.exact) {\n throw new Error(\n 'A catalog rule location cannot have both exact and pattern values',\n );\n }\n return location;\n }),\n }));\n rules.push(...globalRules);\n } else {\n rules.push(...DefaultCatalogRulesEnforcer.defaultRules);\n }\n\n if (config.has('catalog.locations')) {\n const locationRules = config\n .getConfigArray('catalog.locations')\n .flatMap(locConf => {\n if (!locConf.has('rules')) {\n return [];\n }\n const type = locConf.getString('type');\n const exact = resolveTarget(type, locConf.getString('target'));\n\n return locConf.getConfigArray('rules').map(ruleConf => ({\n allow: ruleConf.getStringArray('allow').map(kind => ({ kind })),\n locations: [{ type, exact }],\n }));\n });\n\n rules.push(...locationRules);\n }\n\n return new DefaultCatalogRulesEnforcer(rules);\n }\n\n private readonly rules: CatalogRule[];\n\n constructor(rules: CatalogRule[]) {\n this.rules = rules;\n }\n\n /**\n * Checks whether a specific entity/location combination is allowed\n * according to the configured rules.\n */\n isAllowed(entity: Entity, location: LocationSpec) {\n for (const rule of this.rules) {\n if (!this.matchLocation(location, rule.locations)) {\n continue;\n }\n\n if (this.matchEntity(entity, rule.allow)) {\n return true;\n }\n }\n\n return false;\n }\n\n private matchLocation(\n location: LocationSpec,\n matchers?: { exact?: string; type: string; pattern?: string }[],\n ): boolean {\n if (!matchers) {\n return true;\n }\n\n for (const matcher of matchers) {\n if (matcher.type !== location?.type) {\n continue;\n }\n if (matcher.exact && matcher.exact !== location?.target) {\n continue;\n }\n if (\n matcher.pattern &&\n !minimatch(location?.target, matcher.pattern, {\n nocase: true,\n dot: true,\n })\n ) {\n continue;\n }\n return true;\n }\n\n return false;\n }\n\n private matchEntity(entity: Entity, matchers?: CatalogRuleAllow[]): boolean {\n if (!matchers) {\n return true;\n }\n\n for (const matcher of matchers) {\n if (\n entity.kind?.toLocaleLowerCase('en-US') !==\n matcher.kind.toLocaleLowerCase('en-US')\n ) {\n continue;\n }\n\n if (matcher['spec.type']) {\n if (typeof entity.spec?.type !== 'string') {\n continue;\n }\n if (\n matcher['spec.type'].toLocaleLowerCase('en-US') !==\n entity.spec.type.toLocaleLowerCase('en-US')\n ) {\n continue;\n }\n }\n\n return true;\n }\n\n return false;\n }\n}\n\nfunction resolveTarget(type: string, target: string): string {\n if (type !== 'file') {\n return target;\n }\n\n return path.resolve(target);\n}\n"],"names":["z","minimatch","path"],"mappings":"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"}
|
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
var alpha = require('@backstage/plugin-catalog-node/alpha');
|
|
4
4
|
var pluginPermissionNode = require('@backstage/plugin-permission-node');
|
|
5
5
|
var lodash = require('lodash');
|
|
6
|
-
var
|
|
6
|
+
var v3 = require('zod/v3');
|
|
7
7
|
|
|
8
8
|
const createPropertyRule = (propertyType) => pluginPermissionNode.createPermissionRule({
|
|
9
9
|
name: `HAS_${propertyType.toUpperCase()}`,
|
|
10
10
|
description: `Allow entities with the specified ${propertyType} subfield`,
|
|
11
11
|
resourceRef: alpha.catalogEntityPermissionResourceRef,
|
|
12
|
-
paramsSchema:
|
|
13
|
-
key:
|
|
14
|
-
value:
|
|
12
|
+
paramsSchema: v3.z.object({
|
|
13
|
+
key: v3.z.string().describe(`Property within the entities ${propertyType} to match on`),
|
|
14
|
+
value: v3.z.string().optional().describe(`Value of the given property to match on`)
|
|
15
15
|
}),
|
|
16
16
|
apply: (resource, { key, value }) => {
|
|
17
17
|
const foundValue = lodash.get(resource[propertyType], key);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createPropertyRule.cjs.js","sources":["../../../src/permissions/rules/createPropertyRule.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 { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha';\nimport { createPermissionRule } from '@backstage/plugin-permission-node';\nimport { get } from 'lodash';\nimport { z } from 'zod';\n\nexport const createPropertyRule = (propertyType: 'metadata' | 'spec') =>\n createPermissionRule({\n name: `HAS_${propertyType.toUpperCase()}`,\n description: `Allow entities with the specified ${propertyType} subfield`,\n resourceRef: catalogEntityPermissionResourceRef,\n paramsSchema: z.object({\n key: z\n .string()\n .describe(`Property within the entities ${propertyType} to match on`),\n value: z\n .string()\n .optional()\n .describe(`Value of the given property to match on`),\n }),\n apply: (resource, { key, value }) => {\n const foundValue = get(resource[propertyType], key);\n\n if (Array.isArray(foundValue)) {\n if (value !== undefined) {\n return foundValue.includes(value);\n }\n return foundValue.length > 0;\n }\n if (value !== undefined) {\n return value === foundValue;\n }\n return !!foundValue;\n },\n toQuery: ({ key, value }) => ({\n key: `${propertyType}.${key}`,\n ...(value !== undefined && { values: [value] }),\n }),\n });\n"],"names":["createPermissionRule","catalogEntityPermissionResourceRef","z","get"],"mappings":";;;;;;;AAqBO,MAAM,kBAAA,GAAqB,CAAC,YAAA,KACjCA,yCAAA,CAAqB;AAAA,EACnB,IAAA,EAAM,CAAA,IAAA,EAAO,YAAA,CAAa,WAAA,EAAa,CAAA,CAAA;AAAA,EACvC,WAAA,EAAa,qCAAqC,YAAY,CAAA,SAAA,CAAA;AAAA,EAC9D,WAAA,EAAaC,wCAAA;AAAA,EACb,YAAA,EAAcC,
|
|
1
|
+
{"version":3,"file":"createPropertyRule.cjs.js","sources":["../../../src/permissions/rules/createPropertyRule.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 { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha';\nimport { createPermissionRule } from '@backstage/plugin-permission-node';\nimport { get } from 'lodash';\nimport { z } from 'zod/v3';\n\nexport const createPropertyRule = (propertyType: 'metadata' | 'spec') =>\n createPermissionRule({\n name: `HAS_${propertyType.toUpperCase()}`,\n description: `Allow entities with the specified ${propertyType} subfield`,\n resourceRef: catalogEntityPermissionResourceRef,\n paramsSchema: z.object({\n key: z\n .string()\n .describe(`Property within the entities ${propertyType} to match on`),\n value: z\n .string()\n .optional()\n .describe(`Value of the given property to match on`),\n }),\n apply: (resource, { key, value }) => {\n const foundValue = get(resource[propertyType], key);\n\n if (Array.isArray(foundValue)) {\n if (value !== undefined) {\n return foundValue.includes(value);\n }\n return foundValue.length > 0;\n }\n if (value !== undefined) {\n return value === foundValue;\n }\n return !!foundValue;\n },\n toQuery: ({ key, value }) => ({\n key: `${propertyType}.${key}`,\n ...(value !== undefined && { values: [value] }),\n }),\n });\n"],"names":["createPermissionRule","catalogEntityPermissionResourceRef","z","get"],"mappings":";;;;;;;AAqBO,MAAM,kBAAA,GAAqB,CAAC,YAAA,KACjCA,yCAAA,CAAqB;AAAA,EACnB,IAAA,EAAM,CAAA,IAAA,EAAO,YAAA,CAAa,WAAA,EAAa,CAAA,CAAA;AAAA,EACvC,WAAA,EAAa,qCAAqC,YAAY,CAAA,SAAA,CAAA;AAAA,EAC9D,WAAA,EAAaC,wCAAA;AAAA,EACb,YAAA,EAAcC,KAAE,MAAA,CAAO;AAAA,IACrB,KAAKA,IAAA,CACF,MAAA,GACA,QAAA,CAAS,CAAA,6BAAA,EAAgC,YAAY,CAAA,YAAA,CAAc,CAAA;AAAA,IACtE,OAAOA,IAAA,CACJ,MAAA,GACA,QAAA,EAAS,CACT,SAAS,CAAA,uCAAA,CAAyC;AAAA,GACtD,CAAA;AAAA,EACD,OAAO,CAAC,QAAA,EAAU,EAAE,GAAA,EAAK,OAAM,KAAM;AACnC,IAAA,MAAM,UAAA,GAAaC,UAAA,CAAI,QAAA,CAAS,YAAY,GAAG,GAAG,CAAA;AAElD,IAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,UAAU,CAAA,EAAG;AAC7B,MAAA,IAAI,UAAU,MAAA,EAAW;AACvB,QAAA,OAAO,UAAA,CAAW,SAAS,KAAK,CAAA;AAAA,MAClC;AACA,MAAA,OAAO,WAAW,MAAA,GAAS,CAAA;AAAA,IAC7B;AACA,IAAA,IAAI,UAAU,MAAA,EAAW;AACvB,MAAA,OAAO,KAAA,KAAU,UAAA;AAAA,IACnB;AACA,IAAA,OAAO,CAAC,CAAC,UAAA;AAAA,EACX,CAAA;AAAA,EACA,OAAA,EAAS,CAAC,EAAE,GAAA,EAAK,OAAM,MAAO;AAAA,IAC5B,GAAA,EAAK,CAAA,EAAG,YAAY,CAAA,CAAA,EAAI,GAAG,CAAA,CAAA;AAAA,IAC3B,GAAI,KAAA,KAAU,MAAA,IAAa,EAAE,MAAA,EAAQ,CAAC,KAAK,CAAA;AAAE,GAC/C;AACF,CAAC;;;;"}
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
var alpha = require('@backstage/plugin-catalog-node/alpha');
|
|
4
4
|
var pluginPermissionNode = require('@backstage/plugin-permission-node');
|
|
5
|
-
var
|
|
5
|
+
var v3 = require('zod/v3');
|
|
6
6
|
|
|
7
7
|
const hasAnnotation = pluginPermissionNode.createPermissionRule({
|
|
8
8
|
name: "HAS_ANNOTATION",
|
|
9
9
|
description: "Allow entities with the specified annotation",
|
|
10
10
|
resourceRef: alpha.catalogEntityPermissionResourceRef,
|
|
11
|
-
paramsSchema:
|
|
12
|
-
annotation:
|
|
13
|
-
value:
|
|
11
|
+
paramsSchema: v3.z.object({
|
|
12
|
+
annotation: v3.z.string().describe("Name of the annotation to match on"),
|
|
13
|
+
value: v3.z.string().optional().describe("Value of the annotation to match on")
|
|
14
14
|
}),
|
|
15
15
|
apply: (resource, { annotation, value }) => !!resource.metadata.annotations?.hasOwnProperty(annotation) && (value === void 0 ? true : resource.metadata.annotations?.[annotation] === value),
|
|
16
16
|
toQuery: ({ annotation, value }) => value === void 0 ? {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hasAnnotation.cjs.js","sources":["../../../src/permissions/rules/hasAnnotation.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 { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha';\nimport { createPermissionRule } from '@backstage/plugin-permission-node';\nimport { z } from 'zod';\n\n/**\n * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which\n * filters for the presence of an annotation on a given entity.\n *\n * If a value is given, it filters for the annotation value, too.\n *\n * @alpha\n */\nexport const hasAnnotation = createPermissionRule({\n name: 'HAS_ANNOTATION',\n description: 'Allow entities with the specified annotation',\n resourceRef: catalogEntityPermissionResourceRef,\n paramsSchema: z.object({\n annotation: z.string().describe('Name of the annotation to match on'),\n value: z\n .string()\n .optional()\n .describe('Value of the annotation to match on'),\n }),\n apply: (resource, { annotation, value }) =>\n !!resource.metadata.annotations?.hasOwnProperty(annotation) &&\n (value === undefined\n ? true\n : resource.metadata.annotations?.[annotation] === value),\n toQuery: ({ annotation, value }) =>\n value === undefined\n ? {\n key: `metadata.annotations.${annotation}`,\n }\n : {\n key: `metadata.annotations.${annotation}`,\n values: [value],\n },\n});\n"],"names":["createPermissionRule","catalogEntityPermissionResourceRef","z"],"mappings":";;;;;;AA4BO,MAAM,gBAAgBA,yCAAA,CAAqB;AAAA,EAChD,IAAA,EAAM,gBAAA;AAAA,EACN,WAAA,EAAa,8CAAA;AAAA,EACb,WAAA,EAAaC,wCAAA;AAAA,EACb,YAAA,EAAcC,
|
|
1
|
+
{"version":3,"file":"hasAnnotation.cjs.js","sources":["../../../src/permissions/rules/hasAnnotation.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 { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha';\nimport { createPermissionRule } from '@backstage/plugin-permission-node';\nimport { z } from 'zod/v3';\n\n/**\n * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which\n * filters for the presence of an annotation on a given entity.\n *\n * If a value is given, it filters for the annotation value, too.\n *\n * @alpha\n */\nexport const hasAnnotation = createPermissionRule({\n name: 'HAS_ANNOTATION',\n description: 'Allow entities with the specified annotation',\n resourceRef: catalogEntityPermissionResourceRef,\n paramsSchema: z.object({\n annotation: z.string().describe('Name of the annotation to match on'),\n value: z\n .string()\n .optional()\n .describe('Value of the annotation to match on'),\n }),\n apply: (resource, { annotation, value }) =>\n !!resource.metadata.annotations?.hasOwnProperty(annotation) &&\n (value === undefined\n ? true\n : resource.metadata.annotations?.[annotation] === value),\n toQuery: ({ annotation, value }) =>\n value === undefined\n ? {\n key: `metadata.annotations.${annotation}`,\n }\n : {\n key: `metadata.annotations.${annotation}`,\n values: [value],\n },\n});\n"],"names":["createPermissionRule","catalogEntityPermissionResourceRef","z"],"mappings":";;;;;;AA4BO,MAAM,gBAAgBA,yCAAA,CAAqB;AAAA,EAChD,IAAA,EAAM,gBAAA;AAAA,EACN,WAAA,EAAa,8CAAA;AAAA,EACb,WAAA,EAAaC,wCAAA;AAAA,EACb,YAAA,EAAcC,KAAE,MAAA,CAAO;AAAA,IACrB,UAAA,EAAYA,IAAA,CAAE,MAAA,EAAO,CAAE,SAAS,oCAAoC,CAAA;AAAA,IACpE,OAAOA,IAAA,CACJ,MAAA,GACA,QAAA,EAAS,CACT,SAAS,qCAAqC;AAAA,GAClD,CAAA;AAAA,EACD,KAAA,EAAO,CAAC,QAAA,EAAU,EAAE,YAAY,KAAA,EAAM,KACpC,CAAC,CAAC,QAAA,CAAS,QAAA,CAAS,aAAa,cAAA,CAAe,UAAU,MACzD,KAAA,KAAU,MAAA,GACP,OACA,QAAA,CAAS,QAAA,CAAS,WAAA,GAAc,UAAU,CAAA,KAAM,KAAA,CAAA;AAAA,EACtD,SAAS,CAAC,EAAE,YAAY,KAAA,EAAM,KAC5B,UAAU,MAAA,GACN;AAAA,IACE,GAAA,EAAK,wBAAwB,UAAU,CAAA;AAAA,GACzC,GACA;AAAA,IACE,GAAA,EAAK,wBAAwB,UAAU,CAAA,CAAA;AAAA,IACvC,MAAA,EAAQ,CAAC,KAAK;AAAA;AAExB,CAAC;;;;"}
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
var alpha = require('@backstage/plugin-catalog-node/alpha');
|
|
4
4
|
var pluginPermissionNode = require('@backstage/plugin-permission-node');
|
|
5
|
-
var
|
|
5
|
+
var v3 = require('zod/v3');
|
|
6
6
|
|
|
7
7
|
const hasLabel = pluginPermissionNode.createPermissionRule({
|
|
8
8
|
name: "HAS_LABEL",
|
|
9
9
|
description: "Allow entities with the specified label",
|
|
10
10
|
resourceRef: alpha.catalogEntityPermissionResourceRef,
|
|
11
|
-
paramsSchema:
|
|
12
|
-
label:
|
|
13
|
-
value:
|
|
11
|
+
paramsSchema: v3.z.object({
|
|
12
|
+
label: v3.z.string().describe("Name of the label to match on"),
|
|
13
|
+
value: v3.z.string().optional().describe("Value of the label to match on")
|
|
14
14
|
}),
|
|
15
15
|
apply: (resource, { label, value }) => !!resource.metadata.labels?.hasOwnProperty(label) && (value === void 0 ? true : resource.metadata.labels?.[label] === value),
|
|
16
16
|
toQuery: ({ label, value }) => value === void 0 ? {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hasLabel.cjs.js","sources":["../../../src/permissions/rules/hasLabel.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 { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha';\nimport { createPermissionRule } from '@backstage/plugin-permission-node';\nimport { z } from 'zod';\n\n/**\n * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which\n * filters for entities with a specified label in its metadata.\n * @alpha\n */\nexport const hasLabel = createPermissionRule({\n name: 'HAS_LABEL',\n description: 'Allow entities with the specified label',\n resourceRef: catalogEntityPermissionResourceRef,\n paramsSchema: z.object({\n label: z.string().describe('Name of the label to match on'),\n value: z.string().optional().describe('Value of the label to match on'),\n }),\n apply: (resource, { label, value }) =>\n !!resource.metadata.labels?.hasOwnProperty(label) &&\n (value === undefined ? true : resource.metadata.labels?.[label] === value),\n toQuery: ({ label, value }) =>\n value === undefined\n ? {\n key: `metadata.labels.${label}`,\n }\n : {\n key: `metadata.labels.${label}`,\n values: [value],\n },\n});\n"],"names":["createPermissionRule","catalogEntityPermissionResourceRef","z"],"mappings":";;;;;;AAyBO,MAAM,WAAWA,yCAAA,CAAqB;AAAA,EAC3C,IAAA,EAAM,WAAA;AAAA,EACN,WAAA,EAAa,yCAAA;AAAA,EACb,WAAA,EAAaC,wCAAA;AAAA,EACb,YAAA,EAAcC,
|
|
1
|
+
{"version":3,"file":"hasLabel.cjs.js","sources":["../../../src/permissions/rules/hasLabel.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 { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha';\nimport { createPermissionRule } from '@backstage/plugin-permission-node';\nimport { z } from 'zod/v3';\n\n/**\n * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which\n * filters for entities with a specified label in its metadata.\n * @alpha\n */\nexport const hasLabel = createPermissionRule({\n name: 'HAS_LABEL',\n description: 'Allow entities with the specified label',\n resourceRef: catalogEntityPermissionResourceRef,\n paramsSchema: z.object({\n label: z.string().describe('Name of the label to match on'),\n value: z.string().optional().describe('Value of the label to match on'),\n }),\n apply: (resource, { label, value }) =>\n !!resource.metadata.labels?.hasOwnProperty(label) &&\n (value === undefined ? true : resource.metadata.labels?.[label] === value),\n toQuery: ({ label, value }) =>\n value === undefined\n ? {\n key: `metadata.labels.${label}`,\n }\n : {\n key: `metadata.labels.${label}`,\n values: [value],\n },\n});\n"],"names":["createPermissionRule","catalogEntityPermissionResourceRef","z"],"mappings":";;;;;;AAyBO,MAAM,WAAWA,yCAAA,CAAqB;AAAA,EAC3C,IAAA,EAAM,WAAA;AAAA,EACN,WAAA,EAAa,yCAAA;AAAA,EACb,WAAA,EAAaC,wCAAA;AAAA,EACb,YAAA,EAAcC,KAAE,MAAA,CAAO;AAAA,IACrB,KAAA,EAAOA,IAAA,CAAE,MAAA,EAAO,CAAE,SAAS,+BAA+B,CAAA;AAAA,IAC1D,OAAOA,IAAA,CAAE,MAAA,GAAS,QAAA,EAAS,CAAE,SAAS,gCAAgC;AAAA,GACvE,CAAA;AAAA,EACD,KAAA,EAAO,CAAC,QAAA,EAAU,EAAE,OAAO,KAAA,EAAM,KAC/B,CAAC,CAAC,QAAA,CAAS,QAAA,CAAS,QAAQ,cAAA,CAAe,KAAK,MAC/C,KAAA,KAAU,MAAA,GAAY,OAAO,QAAA,CAAS,QAAA,CAAS,MAAA,GAAS,KAAK,CAAA,KAAM,KAAA,CAAA;AAAA,EACtE,SAAS,CAAC,EAAE,OAAO,KAAA,EAAM,KACvB,UAAU,MAAA,GACN;AAAA,IACE,GAAA,EAAK,mBAAmB,KAAK,CAAA;AAAA,GAC/B,GACA;AAAA,IACE,GAAA,EAAK,mBAAmB,KAAK,CAAA,CAAA;AAAA,IAC7B,MAAA,EAAQ,CAAC,KAAK;AAAA;AAExB,CAAC;;;;"}
|