@backstage/plugin-catalog-backend 3.0.2-next.0 → 3.1.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 +42 -1
- package/dist/actions/createGetCatalogEntityAction.cjs.js +5 -0
- package/dist/actions/createGetCatalogEntityAction.cjs.js.map +1 -1
- package/dist/database/operations/stitcher/markForStitching.cjs.js +57 -20
- package/dist/database/operations/stitcher/markForStitching.cjs.js.map +1 -1
- package/dist/ingestion/CatalogRules.cjs.js +20 -2
- package/dist/ingestion/CatalogRules.cjs.js.map +1 -1
- package/dist/schema/openapi/generated/router.cjs.js +1 -1
- package/dist/schema/openapi/generated/router.cjs.js.map +1 -1
- package/dist/service/CatalogBuilder.cjs.js +55 -4
- package/dist/service/CatalogBuilder.cjs.js.map +1 -1
- package/package.json +19 -19
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
# @backstage/plugin-catalog-backend
|
|
2
2
|
|
|
3
|
+
## 3.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 9b40a55: Add support for specifying an entity `spec.type` in `catalog.rules` and `catalog.locations.rules` within the catalog configuration.
|
|
8
|
+
|
|
9
|
+
For example, this enables allowing all `Template` entities with the type `website`:
|
|
10
|
+
|
|
11
|
+
```diff
|
|
12
|
+
catalog:
|
|
13
|
+
rules:
|
|
14
|
+
- allow:
|
|
15
|
+
- Component
|
|
16
|
+
- API
|
|
17
|
+
- Resource
|
|
18
|
+
- System
|
|
19
|
+
- Domain
|
|
20
|
+
- Location
|
|
21
|
+
+ - allow:
|
|
22
|
+
+ - kind: Template
|
|
23
|
+
+ spec.type: website
|
|
24
|
+
locations:
|
|
25
|
+
- type: url
|
|
26
|
+
pattern: https://github.com/org/*\/blob/master/*.yaml
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- 37b4eaf: The 'get-catalog-entity' action now throws a ConflictError instead of generic Error if multiple entities are found, so MCP call doesn't fail with 500.
|
|
32
|
+
- 2bbd24f: Order catalog processors by priority.
|
|
33
|
+
|
|
34
|
+
This change enables the ordering of catalog processors by their priority,
|
|
35
|
+
allowing for more control over the catalog processing sequence.
|
|
36
|
+
The default priority is set to 20, and processors can be assigned a custom
|
|
37
|
+
priority to influence their execution order. Lower number indicates higher priority.
|
|
38
|
+
The priority can be set by implementing the `getPriority` method in the processor class
|
|
39
|
+
or by adding a `catalog.processors.<processorName>.priority` configuration
|
|
40
|
+
in the `app-config.yaml` file. The configuration takes precedence over the method.
|
|
41
|
+
|
|
42
|
+
- e934a27: Updating `catalog:get-catalog-entity` action to be `readOnly` and non destructive
|
|
43
|
+
- 0efcc97: Updated generated schemas
|
|
44
|
+
- 2204f5b: Prevent deadlock in catalog deferred stitching
|
|
45
|
+
- 58874c4: Add support to disable catalog providers and processors via configuration
|
|
46
|
+
- a4c82ad: Only run provider orphan cleanup if the engine is started in the first place
|
|
47
|
+
- Updated dependencies
|
|
48
|
+
- @backstage/plugin-catalog-node@1.19.0
|
|
49
|
+
- @backstage/catalog-client@1.12.0
|
|
50
|
+
- @backstage/plugin-events-node@0.4.15
|
|
51
|
+
- @backstage/integration@1.18.0
|
|
52
|
+
- @backstage/types@1.2.2
|
|
53
|
+
- @backstage/backend-openapi-utils@0.6.1
|
|
54
|
+
- @backstage/backend-plugin-api@1.4.3
|
|
55
|
+
- @backstage/plugin-permission-node@0.10.4
|
|
56
|
+
|
|
57
|
+
## 3.0.2-next.1
|
|
58
|
+
|
|
59
|
+
### Patch Changes
|
|
60
|
+
|
|
61
|
+
- 2204f5b: Prevent deadlock in catalog deferred stitching
|
|
62
|
+
- Updated dependencies
|
|
63
|
+
- @backstage/catalog-client@1.12.0-next.0
|
|
64
|
+
- @backstage/plugin-catalog-node@1.19.0-next.1
|
|
65
|
+
- @backstage/integration@1.18.0-next.0
|
|
66
|
+
|
|
3
67
|
## 3.0.2-next.0
|
|
4
68
|
|
|
5
69
|
### Patch Changes
|
package/config.d.ts
CHANGED
|
@@ -38,8 +38,11 @@ export interface Config {
|
|
|
38
38
|
* Allow entities of these particular kinds.
|
|
39
39
|
*
|
|
40
40
|
* E.g. ["Component", "API", "Template", "Location"]
|
|
41
|
+
*
|
|
42
|
+
* You can also specify the type of the entity by using an object with `kind` and optional `spec.type` properties.
|
|
43
|
+
* E.g. [{ kind: "Component", 'spec.type': "service" }]
|
|
41
44
|
*/
|
|
42
|
-
allow: Array<string>;
|
|
45
|
+
allow: Array<string | { kind: string; 'spec.type'?: string }>;
|
|
43
46
|
/**
|
|
44
47
|
* Limit this rule to a specific location
|
|
45
48
|
*
|
|
@@ -218,5 +221,43 @@ export interface Config {
|
|
|
218
221
|
* housing catalog-info files.
|
|
219
222
|
*/
|
|
220
223
|
processingInterval?: HumanDuration | false;
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Catalog provide specific configuration.
|
|
227
|
+
* Additional configuration for providers are specified in the catalog
|
|
228
|
+
* modules.
|
|
229
|
+
*/
|
|
230
|
+
providers?: {
|
|
231
|
+
/**
|
|
232
|
+
* Name is the provider ID, e.g. "bitbucketServer"
|
|
233
|
+
*/
|
|
234
|
+
[name: string]: {
|
|
235
|
+
/**
|
|
236
|
+
* Whether the provider is enabled or not. Defaults to true.
|
|
237
|
+
*/
|
|
238
|
+
enabled?: boolean;
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
/**
|
|
242
|
+
* Configuration for entity processors. Additional configuration for
|
|
243
|
+
* processors are specified in the catalog modules.
|
|
244
|
+
*/
|
|
245
|
+
processors?: {
|
|
246
|
+
/**
|
|
247
|
+
* Name is the processor ID, e.g. "catalog-processor"
|
|
248
|
+
*/
|
|
249
|
+
[name: string]: {
|
|
250
|
+
/**
|
|
251
|
+
* Whether the processor is enabled or not. Defaults to true.
|
|
252
|
+
*/
|
|
253
|
+
enabled?: boolean;
|
|
254
|
+
/**
|
|
255
|
+
* The priority of the processor, which is used to determine the order in which
|
|
256
|
+
* processors are run. The default priority is 20, and lower value means
|
|
257
|
+
* that the processor runs earlier.
|
|
258
|
+
*/
|
|
259
|
+
priority?: number;
|
|
260
|
+
};
|
|
261
|
+
};
|
|
221
262
|
};
|
|
222
263
|
}
|
|
@@ -10,6 +10,11 @@ const createGetCatalogEntityAction = ({
|
|
|
10
10
|
actionsRegistry.register({
|
|
11
11
|
name: "get-catalog-entity",
|
|
12
12
|
title: "Get Catalog Entity",
|
|
13
|
+
attributes: {
|
|
14
|
+
destructive: false,
|
|
15
|
+
readOnly: true,
|
|
16
|
+
idempotent: true
|
|
17
|
+
},
|
|
13
18
|
description: `
|
|
14
19
|
This allows you to get a single entity from the software catalog.
|
|
15
20
|
Each entity in the software catalog has a unique name, kind, and namespace. The default namespace is "default".
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createGetCatalogEntityAction.cjs.js","sources":["../../src/actions/createGetCatalogEntityAction.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 */\nimport { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha';\nimport { stringifyEntityRef } from '@backstage/catalog-model';\nimport { ConflictError, InputError } from '@backstage/errors';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\n\nexport const createGetCatalogEntityAction = ({\n catalog,\n actionsRegistry,\n}: {\n catalog: CatalogService;\n actionsRegistry: ActionsRegistryService;\n}) => {\n actionsRegistry.register({\n name: 'get-catalog-entity',\n title: 'Get Catalog Entity',\n description: `\nThis allows you to get a single entity from the software catalog.\nEach entity in the software catalog has a unique name, kind, and namespace. The default namespace is \"default\".\nEach entity is identified by a unique entity reference, which is a string of the form \"kind:namespace/name\".\n `,\n schema: {\n input: z =>\n z.object({\n kind: z\n .string()\n .describe(\n 'The kind of the entity to query. If the kind is unknown it can be omitted.',\n )\n .optional(),\n namespace: z\n .string()\n .describe(\n 'The namespace of the entity to query. If the namespace is unknown it can be omitted.',\n )\n .optional(),\n name: z.string().describe('The name of the entity to query'),\n }),\n // TODO: is there a better way to do this?\n output: z => z.object({}).passthrough(),\n },\n action: async ({ input, credentials }) => {\n const filter: Record<string, string> = { 'metadata.name': input.name };\n\n if (input.kind) {\n filter.kind = input.kind;\n }\n\n if (input.namespace) {\n filter['metadata.namespace'] = input.namespace;\n }\n\n const { items } = await catalog.queryEntities(\n { filter },\n {\n credentials,\n },\n );\n\n if (items.length === 0) {\n throw new InputError(`No entity found with name \"${input.name}\"`);\n }\n\n if (items.length > 1) {\n throw new ConflictError(\n `Multiple entities found with name \"${\n input.name\n }\", please provide more specific filters. Entities found: ${items\n .map(item => `\"${stringifyEntityRef(item)}\"`)\n .join(', ')}`,\n );\n }\n\n const [entity] = items;\n\n return {\n output: entity,\n };\n },\n });\n};\n"],"names":["InputError","ConflictError","stringifyEntityRef"],"mappings":";;;;;AAoBO,MAAM,+BAA+B,CAAC;AAAA,EAC3C,OAAA;AAAA,EACA;AACF,CAAA,KAGM;AACJ,EAAA,eAAA,CAAgB,QAAA,CAAS;AAAA,IACvB,IAAA,EAAM,oBAAA;AAAA,IACN,KAAA,EAAO,oBAAA;AAAA,IACP,WAAA,EAAa;AAAA;AAAA;AAAA;AAAA,IAAA,CAAA;AAAA,IAKb,MAAA,EAAQ;AAAA,MACN,KAAA,EAAO,CAAA,CAAA,KACL,CAAA,CAAE,MAAA,CAAO;AAAA,QACP,IAAA,EAAM,CAAA,CACH,MAAA,EAAO,CACP,QAAA;AAAA,UACC;AAAA,UAED,QAAA,EAAS;AAAA,QACZ,SAAA,EAAW,CAAA,CACR,MAAA,EAAO,CACP,QAAA;AAAA,UACC;AAAA,UAED,QAAA,EAAS;AAAA,QACZ,IAAA,EAAM,CAAA,CAAE,MAAA,EAAO,CAAE,SAAS,iCAAiC;AAAA,OAC5D,CAAA;AAAA;AAAA,MAEH,QAAQ,CAAA,CAAA,KAAK,CAAA,CAAE,OAAO,EAAE,EAAE,WAAA;AAAY,KACxC;AAAA,IACA,MAAA,EAAQ,OAAO,EAAE,KAAA,EAAO,aAAY,KAAM;AACxC,MAAA,MAAM,MAAA,GAAiC,EAAE,eAAA,EAAiB,KAAA,CAAM,IAAA,EAAK;AAErE,MAAA,IAAI,MAAM,IAAA,EAAM;AACd,QAAA,MAAA,CAAO,OAAO,KAAA,CAAM,IAAA;AAAA,MACtB;AAEA,MAAA,IAAI,MAAM,SAAA,EAAW;AACnB,QAAA,MAAA,CAAO,oBAAoB,IAAI,KAAA,CAAM,SAAA;AAAA,MACvC;AAEA,MAAA,MAAM,EAAE,KAAA,EAAM,GAAI,MAAM,OAAA,CAAQ,aAAA;AAAA,QAC9B,EAAE,MAAA,EAAO;AAAA,QACT;AAAA,UACE;AAAA;AACF,OACF;AAEA,MAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AACtB,QAAA,MAAM,IAAIA,iBAAA,CAAW,CAAA,2BAAA,EAA8B,KAAA,CAAM,IAAI,CAAA,CAAA,CAAG,CAAA;AAAA,MAClE;AAEA,MAAA,IAAI,KAAA,CAAM,SAAS,CAAA,EAAG;AACpB,QAAA,MAAM,IAAIC,oBAAA;AAAA,UACR,CAAA,mCAAA,EACE,KAAA,CAAM,IACR,CAAA,yDAAA,EAA4D,MACzD,GAAA,CAAI,CAAA,IAAA,KAAQ,CAAA,CAAA,EAAIC,+BAAA,CAAmB,IAAI,CAAC,CAAA,CAAA,CAAG,CAAA,CAC3C,IAAA,CAAK,IAAI,CAAC,CAAA;AAAA,SACf;AAAA,MACF;AAEA,MAAA,MAAM,CAAC,MAAM,CAAA,GAAI,KAAA;AAEjB,MAAA,OAAO;AAAA,QACL,MAAA,EAAQ;AAAA,OACV;AAAA,IACF;AAAA,GACD,CAAA;AACH;;;;"}
|
|
1
|
+
{"version":3,"file":"createGetCatalogEntityAction.cjs.js","sources":["../../src/actions/createGetCatalogEntityAction.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 */\nimport { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha';\nimport { stringifyEntityRef } from '@backstage/catalog-model';\nimport { ConflictError, InputError } from '@backstage/errors';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\n\nexport const createGetCatalogEntityAction = ({\n catalog,\n actionsRegistry,\n}: {\n catalog: CatalogService;\n actionsRegistry: ActionsRegistryService;\n}) => {\n actionsRegistry.register({\n name: 'get-catalog-entity',\n title: 'Get Catalog Entity',\n attributes: {\n destructive: false,\n readOnly: true,\n idempotent: true,\n },\n description: `\nThis allows you to get a single entity from the software catalog.\nEach entity in the software catalog has a unique name, kind, and namespace. The default namespace is \"default\".\nEach entity is identified by a unique entity reference, which is a string of the form \"kind:namespace/name\".\n `,\n schema: {\n input: z =>\n z.object({\n kind: z\n .string()\n .describe(\n 'The kind of the entity to query. If the kind is unknown it can be omitted.',\n )\n .optional(),\n namespace: z\n .string()\n .describe(\n 'The namespace of the entity to query. If the namespace is unknown it can be omitted.',\n )\n .optional(),\n name: z.string().describe('The name of the entity to query'),\n }),\n // TODO: is there a better way to do this?\n output: z => z.object({}).passthrough(),\n },\n action: async ({ input, credentials }) => {\n const filter: Record<string, string> = { 'metadata.name': input.name };\n\n if (input.kind) {\n filter.kind = input.kind;\n }\n\n if (input.namespace) {\n filter['metadata.namespace'] = input.namespace;\n }\n\n const { items } = await catalog.queryEntities(\n { filter },\n {\n credentials,\n },\n );\n\n if (items.length === 0) {\n throw new InputError(`No entity found with name \"${input.name}\"`);\n }\n\n if (items.length > 1) {\n throw new ConflictError(\n `Multiple entities found with name \"${\n input.name\n }\", please provide more specific filters. Entities found: ${items\n .map(item => `\"${stringifyEntityRef(item)}\"`)\n .join(', ')}`,\n );\n }\n\n const [entity] = items;\n\n return {\n output: entity,\n };\n },\n });\n};\n"],"names":["InputError","ConflictError","stringifyEntityRef"],"mappings":";;;;;AAoBO,MAAM,+BAA+B,CAAC;AAAA,EAC3C,OAAA;AAAA,EACA;AACF,CAAA,KAGM;AACJ,EAAA,eAAA,CAAgB,QAAA,CAAS;AAAA,IACvB,IAAA,EAAM,oBAAA;AAAA,IACN,KAAA,EAAO,oBAAA;AAAA,IACP,UAAA,EAAY;AAAA,MACV,WAAA,EAAa,KAAA;AAAA,MACb,QAAA,EAAU,IAAA;AAAA,MACV,UAAA,EAAY;AAAA,KACd;AAAA,IACA,WAAA,EAAa;AAAA;AAAA;AAAA;AAAA,IAAA,CAAA;AAAA,IAKb,MAAA,EAAQ;AAAA,MACN,KAAA,EAAO,CAAA,CAAA,KACL,CAAA,CAAE,MAAA,CAAO;AAAA,QACP,IAAA,EAAM,CAAA,CACH,MAAA,EAAO,CACP,QAAA;AAAA,UACC;AAAA,UAED,QAAA,EAAS;AAAA,QACZ,SAAA,EAAW,CAAA,CACR,MAAA,EAAO,CACP,QAAA;AAAA,UACC;AAAA,UAED,QAAA,EAAS;AAAA,QACZ,IAAA,EAAM,CAAA,CAAE,MAAA,EAAO,CAAE,SAAS,iCAAiC;AAAA,OAC5D,CAAA;AAAA;AAAA,MAEH,QAAQ,CAAA,CAAA,KAAK,CAAA,CAAE,OAAO,EAAE,EAAE,WAAA;AAAY,KACxC;AAAA,IACA,MAAA,EAAQ,OAAO,EAAE,KAAA,EAAO,aAAY,KAAM;AACxC,MAAA,MAAM,MAAA,GAAiC,EAAE,eAAA,EAAiB,KAAA,CAAM,IAAA,EAAK;AAErE,MAAA,IAAI,MAAM,IAAA,EAAM;AACd,QAAA,MAAA,CAAO,OAAO,KAAA,CAAM,IAAA;AAAA,MACtB;AAEA,MAAA,IAAI,MAAM,SAAA,EAAW;AACnB,QAAA,MAAA,CAAO,oBAAoB,IAAI,KAAA,CAAM,SAAA;AAAA,MACvC;AAEA,MAAA,MAAM,EAAE,KAAA,EAAM,GAAI,MAAM,OAAA,CAAQ,aAAA;AAAA,QAC9B,EAAE,MAAA,EAAO;AAAA,QACT;AAAA,UACE;AAAA;AACF,OACF;AAEA,MAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AACtB,QAAA,MAAM,IAAIA,iBAAA,CAAW,CAAA,2BAAA,EAA8B,KAAA,CAAM,IAAI,CAAA,CAAA,CAAG,CAAA;AAAA,MAClE;AAEA,MAAA,IAAI,KAAA,CAAM,SAAS,CAAA,EAAG;AACpB,QAAA,MAAM,IAAIC,oBAAA;AAAA,UACR,CAAA,mCAAA,EACE,KAAA,CAAM,IACR,CAAA,yDAAA,EAA4D,MACzD,GAAA,CAAI,CAAA,IAAA,KAAQ,CAAA,CAAA,EAAIC,+BAAA,CAAmB,IAAI,CAAC,CAAA,CAAA,CAAG,CAAA,CAC3C,IAAA,CAAK,IAAI,CAAC,CAAA;AAAA,SACf;AAAA,MACF;AAEA,MAAA,MAAM,CAAC,MAAM,CAAA,GAAI,KAAA;AAEjB,MAAA,OAAO;AAAA,QACL,MAAA,EAAQ;AAAA,OACV;AAAA,IACF;AAAA,GACD,CAAA;AACH;;;;"}
|
|
@@ -2,14 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
var splitToChunks = require('lodash/chunk');
|
|
4
4
|
var uuid = require('uuid');
|
|
5
|
+
var errors = require('@backstage/errors');
|
|
6
|
+
var promises = require('timers/promises');
|
|
5
7
|
|
|
6
8
|
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
7
9
|
|
|
8
10
|
var splitToChunks__default = /*#__PURE__*/_interopDefaultCompat(splitToChunks);
|
|
9
11
|
|
|
12
|
+
const UPDATE_CHUNK_SIZE = 100;
|
|
13
|
+
const DEADLOCK_RETRY_ATTEMPTS = 3;
|
|
14
|
+
const DEADLOCK_BASE_DELAY_MS = 25;
|
|
15
|
+
const POSTGRES_DEADLOCK_SQLSTATE = "40P01";
|
|
16
|
+
function isDeadlockError(knex, e) {
|
|
17
|
+
if (knex.client.config.client.includes("pg")) {
|
|
18
|
+
return errors.isError(e) && e.code === POSTGRES_DEADLOCK_SQLSTATE;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
10
22
|
async function markForStitching(options) {
|
|
11
|
-
const entityRefs =
|
|
12
|
-
const entityIds =
|
|
23
|
+
const entityRefs = sortSplit(options.entityRefs);
|
|
24
|
+
const entityIds = sortSplit(options.entityIds);
|
|
13
25
|
const knex = options.knex;
|
|
14
26
|
const mode = options.strategy.mode;
|
|
15
27
|
if (mode === "immediate") {
|
|
@@ -20,43 +32,68 @@ async function markForStitching(options) {
|
|
|
20
32
|
"entity_id",
|
|
21
33
|
knex("refresh_state").select("entity_id").whereIn("entity_ref", chunk)
|
|
22
34
|
);
|
|
23
|
-
await
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
await retryOnDeadlock(async () => {
|
|
36
|
+
await knex.table("refresh_state").update({
|
|
37
|
+
result_hash: "force-stitching",
|
|
38
|
+
next_update_at: knex.fn.now()
|
|
39
|
+
}).whereIn("entity_ref", chunk);
|
|
40
|
+
}, knex);
|
|
27
41
|
}
|
|
28
42
|
for (const chunk of entityIds) {
|
|
29
43
|
await knex.table("final_entities").update({
|
|
30
44
|
hash: "force-stitching"
|
|
31
45
|
}).whereIn("entity_id", chunk);
|
|
32
|
-
await
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
await retryOnDeadlock(async () => {
|
|
47
|
+
await knex.table("refresh_state").update({
|
|
48
|
+
result_hash: "force-stitching",
|
|
49
|
+
next_update_at: knex.fn.now()
|
|
50
|
+
}).whereIn("entity_id", chunk);
|
|
51
|
+
}, knex);
|
|
36
52
|
}
|
|
37
53
|
} else if (mode === "deferred") {
|
|
38
54
|
const ticket = uuid.v4();
|
|
39
55
|
for (const chunk of entityRefs) {
|
|
40
|
-
await
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
await retryOnDeadlock(async () => {
|
|
57
|
+
await knex("refresh_state").update({
|
|
58
|
+
next_stitch_at: knex.fn.now(),
|
|
59
|
+
next_stitch_ticket: ticket
|
|
60
|
+
}).whereIn("entity_ref", chunk);
|
|
61
|
+
}, knex);
|
|
44
62
|
}
|
|
45
63
|
for (const chunk of entityIds) {
|
|
46
|
-
await
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
64
|
+
await retryOnDeadlock(async () => {
|
|
65
|
+
await knex("refresh_state").update({
|
|
66
|
+
next_stitch_at: knex.fn.now(),
|
|
67
|
+
next_stitch_ticket: ticket
|
|
68
|
+
}).whereIn("entity_id", chunk);
|
|
69
|
+
}, knex);
|
|
50
70
|
}
|
|
51
71
|
} else {
|
|
52
72
|
throw new Error(`Unknown stitching strategy mode ${mode}`);
|
|
53
73
|
}
|
|
54
74
|
}
|
|
55
|
-
function
|
|
75
|
+
function sortSplit(input) {
|
|
56
76
|
if (!input) {
|
|
57
77
|
return [];
|
|
58
78
|
}
|
|
59
|
-
|
|
79
|
+
const array = Array.isArray(input) ? input.slice() : [...input];
|
|
80
|
+
array.sort();
|
|
81
|
+
return splitToChunks__default.default(array, UPDATE_CHUNK_SIZE);
|
|
82
|
+
}
|
|
83
|
+
async function retryOnDeadlock(fn, knex, retries = DEADLOCK_RETRY_ATTEMPTS, baseMs = DEADLOCK_BASE_DELAY_MS) {
|
|
84
|
+
let attempt = 0;
|
|
85
|
+
for (; ; ) {
|
|
86
|
+
try {
|
|
87
|
+
return await fn();
|
|
88
|
+
} catch (e) {
|
|
89
|
+
if (isDeadlockError(knex, e) && attempt < retries) {
|
|
90
|
+
await promises.setTimeout(baseMs * Math.pow(2, attempt));
|
|
91
|
+
attempt++;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
throw e;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
60
97
|
}
|
|
61
98
|
|
|
62
99
|
exports.markForStitching = markForStitching;
|
|
@@ -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 { DbFinalEntitiesRow, DbRefreshStateRow } from '../../tables';\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
|
|
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 { ErrorLike, isError } from '@backstage/errors';\nimport { StitchingStrategy } from '../../../stitching/types';\nimport { setTimeout as sleep } from 'timers/promises';\nimport { DbFinalEntitiesRow, DbRefreshStateRow } from '../../tables';\n\nconst UPDATE_CHUNK_SIZE = 100; // Smaller chunks reduce contention\nconst DEADLOCK_RETRY_ATTEMPTS = 3;\nconst DEADLOCK_BASE_DELAY_MS = 25;\n\n// PostgreSQL deadlock error code\nconst POSTGRES_DEADLOCK_SQLSTATE = '40P01';\n\n/**\n * Checks if the given error is a deadlock error for the database engine in use.\n */\nfunction isDeadlockError(\n knex: Knex | Knex.Transaction,\n e: unknown,\n): e is ErrorLike {\n if (knex.client.config.client.includes('pg')) {\n // PostgreSQL deadlock detection\n return isError(e) && e.code === POSTGRES_DEADLOCK_SQLSTATE;\n }\n\n // Add more database engine checks here as needed\n return false;\n}\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(\n 'entity_id',\n knex<DbRefreshStateRow>('refresh_state')\n .select('entity_id')\n .whereIn('entity_ref', chunk),\n );\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 refresh state rows; it just needs to\n // be uniquely generated for every new stitch request.\n const ticket = uuid();\n\n // Update by primary key in deterministic order to avoid deadlocks\n for (const chunk of entityRefs) {\n await retryOnDeadlock(async () => {\n await knex<DbRefreshStateRow>('refresh_state')\n .update({\n next_stitch_at: knex.fn.now(),\n next_stitch_ticket: ticket,\n })\n .whereIn('entity_ref', chunk);\n }, knex);\n }\n\n for (const chunk of entityIds) {\n await retryOnDeadlock(async () => {\n await knex<DbRefreshStateRow>('refresh_state')\n .update({\n next_stitch_at: knex.fn.now(),\n next_stitch_ticket: ticket,\n })\n .whereIn('entity_id', chunk);\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\nasync function retryOnDeadlock<T>(\n fn: () => Promise<T>,\n knex: Knex | Knex.Transaction,\n retries = DEADLOCK_RETRY_ATTEMPTS,\n baseMs = DEADLOCK_BASE_DELAY_MS,\n): Promise<T> {\n let attempt = 0;\n for (;;) {\n try {\n return await fn();\n } catch (e: unknown) {\n if (isDeadlockError(knex, e) && attempt < retries) {\n await sleep(baseMs * Math.pow(2, attempt));\n attempt++;\n continue;\n }\n throw e;\n }\n }\n}\n"],"names":["isError","uuid","splitToChunks","sleep"],"mappings":"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"}
|
|
@@ -2,11 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
var path = require('path');
|
|
4
4
|
var minimatch = require('minimatch');
|
|
5
|
+
var zod = require('zod');
|
|
5
6
|
|
|
6
7
|
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
7
8
|
|
|
8
9
|
var path__default = /*#__PURE__*/_interopDefaultCompat(path);
|
|
9
10
|
|
|
11
|
+
const allowRuleParser = zod.z.array(
|
|
12
|
+
zod.z.object({
|
|
13
|
+
kind: zod.z.string(),
|
|
14
|
+
"spec.type": zod.z.string().optional()
|
|
15
|
+
}).or(zod.z.string()).transform((val) => typeof val === "string" ? { kind: val } : val)
|
|
16
|
+
);
|
|
10
17
|
class DefaultCatalogRulesEnforcer {
|
|
11
18
|
constructor(rules) {
|
|
12
19
|
this.rules = rules;
|
|
@@ -39,6 +46,9 @@ class DefaultCatalogRulesEnforcer {
|
|
|
39
46
|
* catalog:
|
|
40
47
|
* rules:
|
|
41
48
|
* - allow: [Component, API]
|
|
49
|
+
* - allow:
|
|
50
|
+
* - kind: Resource
|
|
51
|
+
* 'spec.type': database
|
|
42
52
|
* - allow: [Template]
|
|
43
53
|
* locations:
|
|
44
54
|
* - type: url
|
|
@@ -63,7 +73,7 @@ class DefaultCatalogRulesEnforcer {
|
|
|
63
73
|
const rules = new Array();
|
|
64
74
|
if (config.has("catalog.rules")) {
|
|
65
75
|
const globalRules = config.getConfigArray("catalog.rules").map((ruleConf) => ({
|
|
66
|
-
allow: ruleConf.
|
|
76
|
+
allow: allowRuleParser.parse(ruleConf.get("allow")),
|
|
67
77
|
locations: ruleConf.getOptionalConfigArray("locations")?.map((locationConfig) => {
|
|
68
78
|
const location = {
|
|
69
79
|
pattern: locationConfig.getOptionalString("pattern"),
|
|
@@ -139,9 +149,17 @@ class DefaultCatalogRulesEnforcer {
|
|
|
139
149
|
return true;
|
|
140
150
|
}
|
|
141
151
|
for (const matcher of matchers) {
|
|
142
|
-
if (entity
|
|
152
|
+
if (entity.kind?.toLocaleLowerCase("en-US") !== matcher.kind.toLocaleLowerCase("en-US")) {
|
|
143
153
|
continue;
|
|
144
154
|
}
|
|
155
|
+
if (matcher["spec.type"]) {
|
|
156
|
+
if (typeof entity.spec?.type !== "string") {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (matcher["spec.type"].toLocaleLowerCase("en-US") !== entity.spec.type.toLocaleLowerCase("en-US")) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
145
163
|
return true;
|
|
146
164
|
}
|
|
147
165
|
return false;
|
|
@@ -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 'path';\nimport { LocationSpec } from '@backstage/plugin-catalog-common';\nimport { minimatch } from 'minimatch';\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: Array<{\n kind: string;\n }>;\n locations?: Array<{\n exact?: string;\n type: string;\n pattern?: string;\n }>;\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\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: [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: ruleConf.getStringArray('allow').map(kind => ({ kind })),\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 constructor(private readonly rules: CatalogRule[]) {}\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?: { kind: string }[]): boolean {\n if (!matchers) {\n return true;\n }\n\n for (const matcher of matchers) {\n if (entity?.kind?.toLowerCase() !== matcher.kind.toLowerCase()) {\n continue;\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":["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 '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 constructor(private readonly rules: CatalogRule[]) {}\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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"}
|
|
@@ -7,7 +7,7 @@ const spec = {
|
|
|
7
7
|
info: {
|
|
8
8
|
title: "catalog",
|
|
9
9
|
version: "1",
|
|
10
|
-
description: "The API surface consists of a few distinct groups of functionality. Each has a\ndedicated section below.\n\n
|
|
10
|
+
description: "The API surface consists of a few distinct groups of functionality. Each has a\ndedicated section below.\n\n:::note Note \n This page only describes some of the most commonly used parts of the API, and is a work in progress.\n:::\n\nAll of the URL paths in this article are assumed to be on top of some base URL\npointing at your catalog installation. For example, if the path given in a\nsection below is `/entities`, and the catalog is located at\n`http://localhost:7007/api/catalog` during local development, the full URL would\nbe `http://localhost:7007/api/catalog/entities`. The actual URL may vary from\none organization to the other, especially in production, but is commonly your\n`backend.baseUrl` in your app config, plus `/api/catalog` at the end.\n\nSome or all of the endpoints may accept or require an `Authorization` header\nwith a `Bearer` token, which should then be the Backstage token returned by the\n[`identity API`](https://backstage.io/docs/reference/core-plugin-api.identityapiref).\n",
|
|
11
11
|
license: {
|
|
12
12
|
name: "Apache-2.0",
|
|
13
13
|
url: "http://www.apache.org/licenses/LICENSE-2.0.html"
|