@red-hat-developer-hub/backstage-plugin-catalog-backend-module-ai-resource-extensions 0.4.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 +39 -0
- package/README.md +128 -0
- package/dist/AIResourceExtensionsProcessor.cjs.js +39 -0
- package/dist/AIResourceExtensionsProcessor.cjs.js.map +1 -0
- package/dist/collectOciErrors.cjs.js +43 -0
- package/dist/collectOciErrors.cjs.js.map +1 -0
- package/dist/index.cjs.js +13 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +57 -0
- package/dist/module.cjs.js +23 -0
- package/dist/module.cjs.js.map +1 -0
- package/package.json +55 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# @red-hat-developer-hub/backstage-plugin-catalog-backend-module-ai-resource-extensions
|
|
2
|
+
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 238650c: Rename package from the accidentally doubled
|
|
8
|
+
`catalog-backend-module-catalog-backend-module-ai-resource-extensions` path/name
|
|
9
|
+
to `catalog-backend-module-ai-resource-extensions`.
|
|
10
|
+
|
|
11
|
+
## 0.3.0
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- 55088f9: Aggregate multiple AIResource extension errors into a single response.
|
|
16
|
+
Extract OCI validation into a shared `collectOciErrors` function called by
|
|
17
|
+
`AIResourceExtensionsProcessor` so that scope and OCI constraint violations
|
|
18
|
+
are reported together instead of stopping at the first failure.
|
|
19
|
+
- 8317e6e: Consolidate OCI location validation into `AIResourceExtensionsProcessor`.
|
|
20
|
+
The standalone `AIResourceOciProcessor` class and its public export have been
|
|
21
|
+
removed; OCI format checks now run via the internal `collectOciErrors` helper
|
|
22
|
+
called by `AIResourceExtensionsProcessor`.
|
|
23
|
+
- 93847ae: Migrate OCI asset location validation from `spec.location` to the standard
|
|
24
|
+
`backstage.io/source-location` annotation using the Backstage location-ref
|
|
25
|
+
form `url:oci://...`. The annotation is parsed with upstream `parseLocationRef`
|
|
26
|
+
for correct whitespace handling. Bare `oci://...` annotations (without the
|
|
27
|
+
`url:` prefix) are rejected with an actionable error.
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- e168046: Add AIResource example YAML files and extend README with schema field
|
|
32
|
+
reference, registration guidance, and OCI validation-only documentation.
|
|
33
|
+
|
|
34
|
+
## 0.2.0
|
|
35
|
+
|
|
36
|
+
### Minor Changes
|
|
37
|
+
|
|
38
|
+
- 6af0114: Add AIResource extension validation via AIResourceExtensionsProcessor, starting with spec.scope
|
|
39
|
+
- 67be6ce: Scaffold new AIResource catalog extension and add upstream module to workspace
|
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# @red-hat-developer-hub/backstage-plugin-catalog-backend-module-ai-resource-extensions
|
|
2
|
+
|
|
3
|
+
A Backstage catalog backend module that validates RHDH-specific extension
|
|
4
|
+
fields on AIResource entities.
|
|
5
|
+
|
|
6
|
+
## Validation
|
|
7
|
+
|
|
8
|
+
The module registers a single catalog processor —
|
|
9
|
+
`AIResourceExtensionsProcessor` — for `AIResource` entities.
|
|
10
|
+
Non-AIResource entities pass through unchanged.
|
|
11
|
+
|
|
12
|
+
### `AIResourceExtensionsProcessor`
|
|
13
|
+
|
|
14
|
+
Validates RHDH extension fields on `AIResource` entities. All constraint
|
|
15
|
+
violations are collected and reported in a single error rather than
|
|
16
|
+
stopping at the first failure.
|
|
17
|
+
|
|
18
|
+
#### `spec.scope`
|
|
19
|
+
|
|
20
|
+
When present, `spec.scope` must be one of:
|
|
21
|
+
|
|
22
|
+
- `organization`
|
|
23
|
+
- `product`
|
|
24
|
+
- `team`
|
|
25
|
+
|
|
26
|
+
Omitting `spec.scope` is valid. Invalid values produce an actionable error
|
|
27
|
+
naming the field, the received value, and the accepted values.
|
|
28
|
+
|
|
29
|
+
#### `backstage.io/source-location` (OCI)
|
|
30
|
+
|
|
31
|
+
When `backstage.io/source-location` uses the location-ref form
|
|
32
|
+
`url:oci://…`, validates that the OCI target is a well-formed
|
|
33
|
+
`oci://registry/repository[:tag|@digest]` URI. Validation is format-only —
|
|
34
|
+
the processor makes no outbound registry or HTTP calls.
|
|
35
|
+
|
|
36
|
+
Bare `oci://…` annotations (without the `url:` prefix) are rejected with
|
|
37
|
+
an actionable error explaining the required `url:oci://…` format. The
|
|
38
|
+
`url:` prefix is required because bare `oci://…` parses as location type
|
|
39
|
+
`oci` with a non-URL target, which would break future UrlReader integration.
|
|
40
|
+
|
|
41
|
+
Non-OCI source-locations (e.g. `url:https://…`) are not subject to OCI
|
|
42
|
+
format rules and pass through without validation.
|
|
43
|
+
|
|
44
|
+
## Schema Fields
|
|
45
|
+
|
|
46
|
+
An `AIResource` entity uses `apiVersion: backstage.io/v1beta1` and
|
|
47
|
+
`kind: AIResource`. The following fields are relevant:
|
|
48
|
+
|
|
49
|
+
| Field | Enforced by this module | Description |
|
|
50
|
+
| ------------------------------ | ----------------------- | ------------------------------------------------------------------------------------------------ |
|
|
51
|
+
| `spec.type` | no¹ | The type of AI asset (e.g. `model`) |
|
|
52
|
+
| `spec.lifecycle` | no¹ | Lifecycle stage (e.g. `experimental`, `production`) |
|
|
53
|
+
| `spec.owner` | no¹ | Owning entity reference (e.g. `team-data-science`) |
|
|
54
|
+
| `spec.scope` | optional, validated | RHDH extension. Accepted values: `organization`, `product`, `team`. Omitting the field is valid. |
|
|
55
|
+
| `backstage.io/source-location` | when target is `oci://` | Standard Backstage annotation. For OCI assets use `url:oci://…` (see below). |
|
|
56
|
+
|
|
57
|
+
¹ These fields are typical for Backstage entities but are **not** validated or
|
|
58
|
+
required by the processors in this module. Entities missing these fields will
|
|
59
|
+
pass through without error.
|
|
60
|
+
|
|
61
|
+
### `spec.scope` Values
|
|
62
|
+
|
|
63
|
+
| Value | Description |
|
|
64
|
+
| -------------- | ----------------------------------------------------- |
|
|
65
|
+
| `organization` | The AI asset is shared across the entire organization |
|
|
66
|
+
| `product` | The AI asset is scoped to a specific product |
|
|
67
|
+
| `team` | The AI asset is scoped to a specific team |
|
|
68
|
+
|
|
69
|
+
## Registering an AIResource Entity
|
|
70
|
+
|
|
71
|
+
AIResource entities are registered through the standard RHDH catalog
|
|
72
|
+
interfaces — no AIResource-specific registration flow is required.
|
|
73
|
+
|
|
74
|
+
### Git-backed entities
|
|
75
|
+
|
|
76
|
+
1. Create a `catalog-info.yaml` with `kind: AIResource`.
|
|
77
|
+
2. Set `backstage.io/source-location` to the repository URL using the
|
|
78
|
+
standard location-ref form (e.g. `url:https://github.com/my-org/my-repo`)
|
|
79
|
+
so consumers can locate the source repository.
|
|
80
|
+
3. Register the entity YAML via the RHDH catalog URL registration UI or
|
|
81
|
+
include it in a catalog location for auto-discovery.
|
|
82
|
+
|
|
83
|
+
See [`examples/ai-resource-git.yaml`](../../examples/ai-resource-git.yaml)
|
|
84
|
+
for a complete example.
|
|
85
|
+
|
|
86
|
+
### OCI-backed entities
|
|
87
|
+
|
|
88
|
+
1. Create a `catalog-info.yaml` with `kind: AIResource`.
|
|
89
|
+
2. Set `backstage.io/source-location` to an OCI reference using the
|
|
90
|
+
location-ref form `url:oci://…`
|
|
91
|
+
(e.g. `url:oci://quay.io/my-org/my-model:latest`).
|
|
92
|
+
|
|
93
|
+
**Important:** use the `url:oci://…` form — not bare `oci://…`. The
|
|
94
|
+
`url:` prefix is required by the Backstage location-ref convention.
|
|
95
|
+
|
|
96
|
+
3. Register the file via the RHDH catalog URL registration UI or include
|
|
97
|
+
it in a catalog location for auto-discovery.
|
|
98
|
+
|
|
99
|
+
See [`examples/ai-resource-oci.yaml`](../../examples/ai-resource-oci.yaml)
|
|
100
|
+
for a complete example.
|
|
101
|
+
|
|
102
|
+
## OCI Validation-Only Behavior
|
|
103
|
+
|
|
104
|
+
The `AIResourceExtensionsProcessor` validates the `backstage.io/source-location`
|
|
105
|
+
annotation when the location-ref target uses the `oci://` scheme. It checks:
|
|
106
|
+
|
|
107
|
+
- The annotation uses the `url:oci://…` location-ref form (not bare `oci://…`).
|
|
108
|
+
- The OCI target is a non-empty string with no leading or trailing whitespace.
|
|
109
|
+
- The URI contains at least a registry and a repository path
|
|
110
|
+
(e.g. `oci://quay.io/org/model:tag`).
|
|
111
|
+
|
|
112
|
+
**The processor makes zero outbound network calls.** It does not contact
|
|
113
|
+
OCI registries, pull manifests, or verify that the referenced artifact
|
|
114
|
+
exists. The catalog indexes the OCI reference as metadata only.
|
|
115
|
+
|
|
116
|
+
This design is intentional for air-gap safety: RHDH deployments in
|
|
117
|
+
disconnected environments can register OCI-backed AI assets without
|
|
118
|
+
requiring network access to the artifact registry at ingestion time.
|
|
119
|
+
The OCI URI serves as a stable reference that downstream tooling can
|
|
120
|
+
resolve when registry access is available.
|
|
121
|
+
|
|
122
|
+
## Public API
|
|
123
|
+
|
|
124
|
+
| Export | Description |
|
|
125
|
+
| ------------------------------- | ------------------------------------------------------- |
|
|
126
|
+
| `AIResourceExtensionsProcessor` | `CatalogProcessor` for RHDH AIResource extension fields |
|
|
127
|
+
| `VALID_AI_RESOURCE_SCOPES` | Readonly tuple of accepted scope values |
|
|
128
|
+
| `AIResourceScope` | Type union of accepted scope values |
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var collectOciErrors = require('./collectOciErrors.cjs.js');
|
|
4
|
+
|
|
5
|
+
const VALID_AI_RESOURCE_SCOPES = [
|
|
6
|
+
"organization",
|
|
7
|
+
"product",
|
|
8
|
+
"team"
|
|
9
|
+
];
|
|
10
|
+
class AIResourceExtensionsProcessor {
|
|
11
|
+
getProcessorName() {
|
|
12
|
+
return "AIResourceExtensionsProcessor";
|
|
13
|
+
}
|
|
14
|
+
async preProcessEntity(entity, _location, _emit) {
|
|
15
|
+
if (entity.kind !== "AIResource") {
|
|
16
|
+
return entity;
|
|
17
|
+
}
|
|
18
|
+
const errors = [];
|
|
19
|
+
const scope = entity.spec?.scope;
|
|
20
|
+
if (scope !== void 0 && !VALID_AI_RESOURCE_SCOPES.includes(scope)) {
|
|
21
|
+
const accepted = VALID_AI_RESOURCE_SCOPES.map((v) => `'${v}'`).join(", ");
|
|
22
|
+
const sanitized = Array.from(String(scope)).filter((c) => c.charCodeAt(0) > 31).join("").slice(0, 200);
|
|
23
|
+
errors.push(
|
|
24
|
+
`spec.scope has invalid value '${sanitized}'; accepted values are ${accepted}`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
errors.push(...collectOciErrors.collectOciErrors(entity));
|
|
28
|
+
if (errors.length > 0) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Validation failed for AIResource entity: ${errors.join("; ")}`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return entity;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
exports.AIResourceExtensionsProcessor = AIResourceExtensionsProcessor;
|
|
38
|
+
exports.VALID_AI_RESOURCE_SCOPES = VALID_AI_RESOURCE_SCOPES;
|
|
39
|
+
//# sourceMappingURL=AIResourceExtensionsProcessor.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AIResourceExtensionsProcessor.cjs.js","sources":["../src/AIResourceExtensionsProcessor.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n CatalogProcessor,\n CatalogProcessorEmit,\n} from '@backstage/plugin-catalog-node';\nimport { Entity } from '@backstage/catalog-model';\nimport { LocationSpec } from '@backstage/plugin-catalog-common';\nimport { collectOciErrors } from './collectOciErrors';\n\n/**\n * Valid values for spec.scope on AIResource entities.\n *\n * @public\n */\nexport const VALID_AI_RESOURCE_SCOPES = [\n 'organization',\n 'product',\n 'team',\n] as const;\n\n/**\n * Type for valid AIResource scope values.\n *\n * @public\n */\nexport type AIResourceScope = (typeof VALID_AI_RESOURCE_SCOPES)[number];\n\n/**\n * A CatalogProcessor that validates RHDH-specific extension\n * fields on AIResource entities.\n *\n * Validates:\n * - `spec.scope`: optional field restricted to 'organization',\n * 'product', or 'team'\n * - `backstage.io/source-location`: OCI URI format when the\n * location-ref target uses the `oci://` scheme\n *\n * All constraint violations are collected and reported in a single\n * error rather than stopping at the first failure.\n *\n * @public\n */\nexport class AIResourceExtensionsProcessor implements CatalogProcessor {\n getProcessorName(): string {\n return 'AIResourceExtensionsProcessor';\n }\n\n async preProcessEntity(\n entity: Entity,\n _location: LocationSpec,\n _emit: CatalogProcessorEmit,\n ): Promise<Entity> {\n if (entity.kind !== 'AIResource') {\n return entity;\n }\n\n const errors: string[] = [];\n\n const scope = entity.spec?.scope;\n if (\n scope !== undefined &&\n !VALID_AI_RESOURCE_SCOPES.includes(scope as AIResourceScope)\n ) {\n const accepted = VALID_AI_RESOURCE_SCOPES.map(v => `'${v}'`).join(', ');\n const sanitized = Array.from(String(scope))\n .filter(c => c.charCodeAt(0) > 0x1f)\n .join('')\n .slice(0, 200);\n errors.push(\n `spec.scope has invalid value '${sanitized}'; accepted values are ${accepted}`,\n );\n }\n\n errors.push(...collectOciErrors(entity));\n\n if (errors.length > 0) {\n throw new Error(\n `Validation failed for AIResource entity: ${errors.join('; ')}`,\n );\n }\n\n return entity;\n }\n}\n"],"names":["collectOciErrors"],"mappings":";;;;AA6BO,MAAM,wBAAA,GAA2B;AAAA,EACtC,cAAA;AAAA,EACA,SAAA;AAAA,EACA;AACF;AAwBO,MAAM,6BAAA,CAA0D;AAAA,EACrE,gBAAA,GAA2B;AACzB,IAAA,OAAO,+BAAA;AAAA,EACT;AAAA,EAEA,MAAM,gBAAA,CACJ,MAAA,EACA,SAAA,EACA,KAAA,EACiB;AACjB,IAAA,IAAI,MAAA,CAAO,SAAS,YAAA,EAAc;AAChC,MAAA,OAAO,MAAA;AAAA,IACT;AAEA,IAAA,MAAM,SAAmB,EAAC;AAE1B,IAAA,MAAM,KAAA,GAAQ,OAAO,IAAA,EAAM,KAAA;AAC3B,IAAA,IACE,UAAU,MAAA,IACV,CAAC,wBAAA,CAAyB,QAAA,CAAS,KAAwB,CAAA,EAC3D;AACA,MAAA,MAAM,QAAA,GAAW,yBAAyB,GAAA,CAAI,CAAA,CAAA,KAAK,IAAI,CAAC,CAAA,CAAA,CAAG,CAAA,CAAE,IAAA,CAAK,IAAI,CAAA;AACtE,MAAA,MAAM,SAAA,GAAY,MAAM,IAAA,CAAK,MAAA,CAAO,KAAK,CAAC,CAAA,CACvC,OAAO,CAAA,CAAA,KAAK,CAAA,CAAE,WAAW,CAAC,CAAA,GAAI,EAAI,CAAA,CAClC,IAAA,CAAK,EAAE,CAAA,CACP,KAAA,CAAM,GAAG,GAAG,CAAA;AACf,MAAA,MAAA,CAAO,IAAA;AAAA,QACL,CAAA,8BAAA,EAAiC,SAAS,CAAA,uBAAA,EAA0B,QAAQ,CAAA;AAAA,OAC9E;AAAA,IACF;AAEA,IAAA,MAAA,CAAO,IAAA,CAAK,GAAGA,iCAAA,CAAiB,MAAM,CAAC,CAAA;AAEvC,IAAA,IAAI,MAAA,CAAO,SAAS,CAAA,EAAG;AACrB,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,yCAAA,EAA4C,MAAA,CAAO,IAAA,CAAK,IAAI,CAAC,CAAA;AAAA,OAC/D;AAAA,IACF;AAEA,IAAA,OAAO,MAAA;AAAA,EACT;AACF;;;;;"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var catalogModel = require('@backstage/catalog-model');
|
|
4
|
+
|
|
5
|
+
function collectOciErrors(entity) {
|
|
6
|
+
const annotation = entity.metadata?.annotations?.[catalogModel.ANNOTATION_SOURCE_LOCATION];
|
|
7
|
+
if (annotation === void 0 || annotation === null) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
let type;
|
|
11
|
+
let target;
|
|
12
|
+
try {
|
|
13
|
+
({ type, target } = catalogModel.parseLocationRef(annotation));
|
|
14
|
+
} catch {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
if (type === "oci") {
|
|
18
|
+
const sanitized = Array.from(annotation.trim()).filter((c) => c.charCodeAt(0) > 31).join("").slice(0, 200);
|
|
19
|
+
return [
|
|
20
|
+
`${catalogModel.ANNOTATION_SOURCE_LOCATION} '${sanitized}' uses a bare oci:// URI; the Backstage location-ref form url:oci://\u2026 is required (e.g. url:oci://quay.io/org/model:tag)`
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
if (type !== "url" || !target.startsWith("oci://")) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
const ociPath = target.slice("oci://".length);
|
|
27
|
+
if (ociPath === "") {
|
|
28
|
+
return [
|
|
29
|
+
`${catalogModel.ANNOTATION_SOURCE_LOCATION} target 'oci://' is not a valid OCI reference; expected format url:oci://registry/repository[:tag|@digest]`
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
const parts = ociPath.split("/");
|
|
33
|
+
if (parts.length < 2 || parts.some((part) => part === "" || /\s/.test(part))) {
|
|
34
|
+
const sanitized = Array.from(target).filter((c) => c.charCodeAt(0) > 31).join("").slice(0, 200);
|
|
35
|
+
return [
|
|
36
|
+
`${catalogModel.ANNOTATION_SOURCE_LOCATION} target '${sanitized}' is not a valid OCI reference; expected format url:oci://registry/repository[:tag|@digest]`
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
exports.collectOciErrors = collectOciErrors;
|
|
43
|
+
//# sourceMappingURL=collectOciErrors.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collectOciErrors.cjs.js","sources":["../src/collectOciErrors.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n ANNOTATION_SOURCE_LOCATION,\n Entity,\n parseLocationRef,\n} from '@backstage/catalog-model';\n\n/**\n * Collects OCI-related validation errors for an AIResource entity\n * without throwing. Returns an array of error messages (empty if valid).\n *\n * Validates the `backstage.io/source-location` annotation when its\n * location-ref target uses the `oci://` scheme. The annotation must\n * use the Backstage location-ref form `url:oci://…`.\n *\n * Uses upstream `parseLocationRef` for parsing, which normalizes\n * leading/trailing whitespace around type and target.\n *\n * @internal\n */\nexport function collectOciErrors(entity: Entity): string[] {\n const annotation = entity.metadata?.annotations?.[ANNOTATION_SOURCE_LOCATION];\n\n if (annotation === undefined || annotation === null) {\n return [];\n }\n\n let type: string;\n let target: string;\n try {\n ({ type, target } = parseLocationRef(annotation));\n } catch {\n // Not a valid location-ref (no colon, empty type/target, etc.)\n // Leave it for other processors to handle.\n return [];\n }\n\n // Detect bare `oci://…` without the required `url:` prefix.\n // parseLocationRef parses `oci://host/path` as type `oci` with a\n // non-URL target, which breaks future UrlReader integration.\n if (type === 'oci') {\n const sanitized = Array.from(annotation.trim())\n .filter(c => c.charCodeAt(0) > 0x1f)\n .join('')\n .slice(0, 200);\n return [\n `${ANNOTATION_SOURCE_LOCATION} '${sanitized}' uses a bare oci:// URI; ` +\n `the Backstage location-ref form url:oci://… is required ` +\n `(e.g. url:oci://quay.io/org/model:tag)`,\n ];\n }\n\n // Only apply OCI validation when the location-ref type is `url` and\n // the target starts with `oci://`.\n if (type !== 'url' || !target.startsWith('oci://')) {\n return [];\n }\n\n const ociPath = target.slice('oci://'.length);\n\n if (ociPath === '') {\n return [\n `${ANNOTATION_SOURCE_LOCATION} target 'oci://' is not a valid OCI ` +\n `reference; expected format url:oci://registry/repository[:tag|@digest]`,\n ];\n }\n\n const parts = ociPath.split('/');\n\n // Reject missing registry/repo, empty segments (e.g. trailing slash),\n // and whitespace inside path segments (e.g. oci:// quay.io/...).\n if (parts.length < 2 || parts.some(part => part === '' || /\\s/.test(part))) {\n const sanitized = Array.from(target)\n .filter(c => c.charCodeAt(0) > 0x1f)\n .join('')\n .slice(0, 200);\n return [\n `${ANNOTATION_SOURCE_LOCATION} target '${sanitized}' is not a valid ` +\n `OCI reference; expected format ` +\n `url:oci://registry/repository[:tag|@digest]`,\n ];\n }\n\n return [];\n}\n"],"names":["ANNOTATION_SOURCE_LOCATION","parseLocationRef"],"mappings":";;;;AAmCO,SAAS,iBAAiB,MAAA,EAA0B;AACzD,EAAA,MAAM,UAAA,GAAa,MAAA,CAAO,QAAA,EAAU,WAAA,GAAcA,uCAA0B,CAAA;AAE5E,EAAA,IAAI,UAAA,KAAe,MAAA,IAAa,UAAA,KAAe,IAAA,EAAM;AACnD,IAAA,OAAO,EAAC;AAAA,EACV;AAEA,EAAA,IAAI,IAAA;AACJ,EAAA,IAAI,MAAA;AACJ,EAAA,IAAI;AACF,IAAA,CAAC,EAAE,IAAA,EAAM,MAAA,EAAO,GAAIC,8BAAiB,UAAU,CAAA;AAAA,EACjD,CAAA,CAAA,MAAQ;AAGN,IAAA,OAAO,EAAC;AAAA,EACV;AAKA,EAAA,IAAI,SAAS,KAAA,EAAO;AAClB,IAAA,MAAM,SAAA,GAAY,MAAM,IAAA,CAAK,UAAA,CAAW,MAAM,CAAA,CAC3C,OAAO,CAAA,CAAA,KAAK,CAAA,CAAE,WAAW,CAAC,CAAA,GAAI,EAAI,CAAA,CAClC,IAAA,CAAK,EAAE,CAAA,CACP,KAAA,CAAM,GAAG,GAAG,CAAA;AACf,IAAA,OAAO;AAAA,MACL,CAAA,EAAGD,uCAA0B,CAAA,EAAA,EAAK,SAAS,CAAA,6HAAA;AAAA,KAG7C;AAAA,EACF;AAIA,EAAA,IAAI,SAAS,KAAA,IAAS,CAAC,MAAA,CAAO,UAAA,CAAW,QAAQ,CAAA,EAAG;AAClD,IAAA,OAAO,EAAC;AAAA,EACV;AAEA,EAAA,MAAM,OAAA,GAAU,MAAA,CAAO,KAAA,CAAM,QAAA,CAAS,MAAM,CAAA;AAE5C,EAAA,IAAI,YAAY,EAAA,EAAI;AAClB,IAAA,OAAO;AAAA,MACL,GAAGA,uCAA0B,CAAA,0GAAA;AAAA,KAE/B;AAAA,EACF;AAEA,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,KAAA,CAAM,GAAG,CAAA;AAI/B,EAAA,IAAI,KAAA,CAAM,MAAA,GAAS,CAAA,IAAK,KAAA,CAAM,IAAA,CAAK,CAAA,IAAA,KAAQ,IAAA,KAAS,EAAA,IAAM,IAAA,CAAK,IAAA,CAAK,IAAI,CAAC,CAAA,EAAG;AAC1E,IAAA,MAAM,YAAY,KAAA,CAAM,IAAA,CAAK,MAAM,CAAA,CAChC,MAAA,CAAO,OAAK,CAAA,CAAE,UAAA,CAAW,CAAC,CAAA,GAAI,EAAI,CAAA,CAClC,IAAA,CAAK,EAAE,CAAA,CACP,KAAA,CAAM,GAAG,GAAG,CAAA;AACf,IAAA,OAAO;AAAA,MACL,CAAA,EAAGA,uCAA0B,CAAA,SAAA,EAAY,SAAS,CAAA,2FAAA;AAAA,KAGpD;AAAA,EACF;AAEA,EAAA,OAAO,EAAC;AACV;;;;"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var module$1 = require('./module.cjs.js');
|
|
6
|
+
var AIResourceExtensionsProcessor = require('./AIResourceExtensionsProcessor.cjs.js');
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
exports.default = module$1.catalogModuleCatalogBackendModuleAiResourceExtensions;
|
|
11
|
+
exports.AIResourceExtensionsProcessor = AIResourceExtensionsProcessor.AIResourceExtensionsProcessor;
|
|
12
|
+
exports.VALID_AI_RESOURCE_SCOPES = AIResourceExtensionsProcessor.VALID_AI_RESOURCE_SCOPES;
|
|
13
|
+
//# sourceMappingURL=index.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as _backstage_backend_plugin_api from '@backstage/backend-plugin-api';
|
|
2
|
+
import { CatalogProcessor, CatalogProcessorEmit } from '@backstage/plugin-catalog-node';
|
|
3
|
+
import { Entity } from '@backstage/catalog-model';
|
|
4
|
+
import { LocationSpec } from '@backstage/plugin-catalog-common';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* catalogModuleCatalogBackendModuleAiResourceExtensions is the catalog-backend-module-ai-resource-extensions backend module for the catalog plugin.
|
|
8
|
+
*
|
|
9
|
+
* Registers {@link AIResourceExtensionsProcessor}, which validates all
|
|
10
|
+
* RHDH extension constraints (spec.scope and OCI source-location) and
|
|
11
|
+
* aggregates errors into a single response.
|
|
12
|
+
*
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
declare const catalogModuleCatalogBackendModuleAiResourceExtensions: _backstage_backend_plugin_api.BackendFeature;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Valid values for spec.scope on AIResource entities.
|
|
19
|
+
*
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
declare const VALID_AI_RESOURCE_SCOPES: readonly ["organization", "product", "team"];
|
|
23
|
+
/**
|
|
24
|
+
* Type for valid AIResource scope values.
|
|
25
|
+
*
|
|
26
|
+
* @public
|
|
27
|
+
*/
|
|
28
|
+
type AIResourceScope = (typeof VALID_AI_RESOURCE_SCOPES)[number];
|
|
29
|
+
/**
|
|
30
|
+
* A CatalogProcessor that validates RHDH-specific extension
|
|
31
|
+
* fields on AIResource entities.
|
|
32
|
+
*
|
|
33
|
+
* Validates:
|
|
34
|
+
* - `spec.scope`: optional field restricted to 'organization',
|
|
35
|
+
* 'product', or 'team'
|
|
36
|
+
* - `backstage.io/source-location`: OCI URI format when the
|
|
37
|
+
* location-ref target uses the `oci://` scheme
|
|
38
|
+
*
|
|
39
|
+
* All constraint violations are collected and reported in a single
|
|
40
|
+
* error rather than stopping at the first failure.
|
|
41
|
+
*
|
|
42
|
+
* @public
|
|
43
|
+
*/
|
|
44
|
+
declare class AIResourceExtensionsProcessor implements CatalogProcessor {
|
|
45
|
+
getProcessorName(): string;
|
|
46
|
+
preProcessEntity(entity: Entity, _location: LocationSpec, _emit: CatalogProcessorEmit): Promise<Entity>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The catalog-backend-module-ai-resource-extensions backend module for the catalog plugin.
|
|
51
|
+
*
|
|
52
|
+
* @packageDocumentation
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
export { AIResourceExtensionsProcessor, VALID_AI_RESOURCE_SCOPES, catalogModuleCatalogBackendModuleAiResourceExtensions as default };
|
|
57
|
+
export type { AIResourceScope };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var backendPluginApi = require('@backstage/backend-plugin-api');
|
|
4
|
+
var pluginCatalogNode = require('@backstage/plugin-catalog-node');
|
|
5
|
+
var AIResourceExtensionsProcessor = require('./AIResourceExtensionsProcessor.cjs.js');
|
|
6
|
+
|
|
7
|
+
const catalogModuleCatalogBackendModuleAiResourceExtensions = backendPluginApi.createBackendModule({
|
|
8
|
+
pluginId: "catalog",
|
|
9
|
+
moduleId: "catalog-backend-module-ai-resource-extensions",
|
|
10
|
+
register(reg) {
|
|
11
|
+
reg.registerInit({
|
|
12
|
+
deps: {
|
|
13
|
+
catalog: pluginCatalogNode.catalogProcessingExtensionPoint
|
|
14
|
+
},
|
|
15
|
+
async init({ catalog }) {
|
|
16
|
+
catalog.addProcessor(new AIResourceExtensionsProcessor.AIResourceExtensionsProcessor());
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
exports.catalogModuleCatalogBackendModuleAiResourceExtensions = catalogModuleCatalogBackendModuleAiResourceExtensions;
|
|
23
|
+
//# sourceMappingURL=module.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module.cjs.js","sources":["../src/module.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\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 { createBackendModule } from '@backstage/backend-plugin-api';\nimport { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node';\n\nimport { AIResourceExtensionsProcessor } from './AIResourceExtensionsProcessor';\n\n/**\n * catalogModuleCatalogBackendModuleAiResourceExtensions is the catalog-backend-module-ai-resource-extensions backend module for the catalog plugin.\n *\n * Registers {@link AIResourceExtensionsProcessor}, which validates all\n * RHDH extension constraints (spec.scope and OCI source-location) and\n * aggregates errors into a single response.\n *\n * @public\n */\nexport const catalogModuleCatalogBackendModuleAiResourceExtensions =\n createBackendModule({\n pluginId: 'catalog',\n moduleId: 'catalog-backend-module-ai-resource-extensions',\n register(reg) {\n reg.registerInit({\n deps: {\n catalog: catalogProcessingExtensionPoint,\n },\n async init({ catalog }) {\n catalog.addProcessor(new AIResourceExtensionsProcessor());\n },\n });\n },\n });\n"],"names":["createBackendModule","catalogProcessingExtensionPoint","AIResourceExtensionsProcessor"],"mappings":";;;;;;AA6BO,MAAM,wDACXA,oCAAA,CAAoB;AAAA,EAClB,QAAA,EAAU,SAAA;AAAA,EACV,QAAA,EAAU,+CAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,OAAA,EAASC;AAAA,OACX;AAAA,MACA,MAAM,IAAA,CAAK,EAAE,OAAA,EAAQ,EAAG;AACtB,QAAA,OAAA,CAAQ,YAAA,CAAa,IAAIC,2DAAA,EAA+B,CAAA;AAAA,MAC1D;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;;;"}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@red-hat-developer-hub/backstage-plugin-catalog-backend-module-ai-resource-extensions",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"description": "The catalog-backend-module-ai-resource-extensions backend module for the catalog plugin.",
|
|
6
|
+
"main": "dist/index.cjs.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public",
|
|
10
|
+
"main": "dist/index.cjs.js",
|
|
11
|
+
"types": "dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/redhat-developer/rhdh-plugins",
|
|
16
|
+
"directory": "workspaces/ai-integrations/plugins/catalog-backend-module-ai-resource-extensions"
|
|
17
|
+
},
|
|
18
|
+
"backstage": {
|
|
19
|
+
"role": "backend-plugin-module",
|
|
20
|
+
"pluginId": "catalog",
|
|
21
|
+
"pluginPackage": "@backstage/plugin-catalog-backend",
|
|
22
|
+
"features": {
|
|
23
|
+
".": "@backstage/BackendFeature"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"start": "backstage-cli package start",
|
|
28
|
+
"build": "backstage-cli package build",
|
|
29
|
+
"lint": "backstage-cli package lint",
|
|
30
|
+
"test": "backstage-cli package test",
|
|
31
|
+
"clean": "backstage-cli package clean",
|
|
32
|
+
"prepack": "backstage-cli package prepack",
|
|
33
|
+
"postpack": "backstage-cli package postpack"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@backstage/backend-plugin-api": "^1.9.2",
|
|
37
|
+
"@backstage/catalog-model": "^1.9.0",
|
|
38
|
+
"@backstage/plugin-catalog-common": "^1.1.10",
|
|
39
|
+
"@backstage/plugin-catalog-node": "^2.2.2"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@backstage/backend-test-utils": "^1.11.4",
|
|
43
|
+
"@backstage/cli": "^0.36.3"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"dist"
|
|
47
|
+
],
|
|
48
|
+
"typesVersions": {
|
|
49
|
+
"*": {
|
|
50
|
+
"package.json": [
|
|
51
|
+
"package.json"
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|