@red-hat-developer-hub/backstage-plugin-catalog-techdoc-url-reader-backend 0.6.2 → 0.7.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 +14 -0
- package/README.md +56 -0
- package/dist/plugin.cjs.js +209 -13
- package/dist/plugin.cjs.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @red-hat-developer-hub/backstage-plugin-catalog-techdoc-url-reader-backend
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3f3a377: Add OCI skill documentation URL reader: read skill source configurations from `catalog.providers.skillsCatalogProvider`, match the exact discovery-resolved connector origin and complete base pathname followed by `/skilldocs/<sourceId>/<digest>`, authenticate via service-to-service tokens, and generate TechDocs-ready MkDocs projects with response ETag caching.
|
|
8
|
+
|
|
9
|
+
Reject off-origin URLs, malformed route encoding, authenticated redirects, and successful skill-docs responses lacking a nonblank ETag. Preserve discovery deployment prefixes and prevent skill routes from falling through to legacy model-card matching.
|
|
10
|
+
|
|
11
|
+
## 0.6.3
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 6088511: Backstage version bump to v1.54.6
|
|
16
|
+
|
|
3
17
|
## 0.6.2
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -28,6 +28,62 @@ backend.add(
|
|
|
28
28
|
);
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
## OCI Skill Documentation URL Reader
|
|
32
|
+
|
|
33
|
+
In addition to model-card TechDocs, this plugin reads OCI skill documentation
|
|
34
|
+
from configured connector destinations.
|
|
35
|
+
|
|
36
|
+
### Configuration
|
|
37
|
+
|
|
38
|
+
Skill documentation sources are read from the
|
|
39
|
+
`catalog.providers.skillsCatalogProvider` config section. Only sources with
|
|
40
|
+
`sourceType: 'oci'` are matched:
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
catalog:
|
|
44
|
+
providers:
|
|
45
|
+
skillsCatalogProvider:
|
|
46
|
+
production:
|
|
47
|
+
sources:
|
|
48
|
+
- connectorPluginId: skill-image-connector
|
|
49
|
+
sourceId: quay-public
|
|
50
|
+
sourceType: oci
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### URL pattern
|
|
54
|
+
|
|
55
|
+
At service initialization, the reader resolves each configured OCI connector's
|
|
56
|
+
base URL through Backstage discovery. The synchronous predicate and direct
|
|
57
|
+
`readUrl` calls require that exact origin (scheme, host, and port) and the full
|
|
58
|
+
discovered base pathname. For example:
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
https://rhdh.example/prefix/api/skill-image-connector/skilldocs/<sourceId>/<digest>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The `/prefix/api/skill-image-connector` base is preserved; `/api/<pluginId>` is
|
|
65
|
+
not hardcoded. Only trailing slashes on the discovered base are normalized. No
|
|
66
|
+
additional destination configuration is required. Directly constructed readers
|
|
67
|
+
must receive trusted connector bases; without them, skill documentation is not
|
|
68
|
+
matched. Normal backend registration supplies these bases automatically.
|
|
69
|
+
|
|
70
|
+
where `<digest>` is a `sha256:<64 hex chars>` OCI manifest digest. URLs
|
|
71
|
+
with query strings, fragments, URL credentials, extra path segments, or
|
|
72
|
+
invalid digest formats are rejected. Each route segment is decoded once;
|
|
73
|
+
malformed percent encoding is rejected without throwing from reader selection.
|
|
74
|
+
Skill-documentation routes cannot fall through to legacy model-card substring
|
|
75
|
+
matching, including when source IDs contain `modelcard`. Untrusted destinations
|
|
76
|
+
are rejected before obtaining or sending a service token.
|
|
77
|
+
|
|
78
|
+
The reader authenticates with the connector plugin via
|
|
79
|
+
`auth.getPluginRequestToken`, rejects redirects without forwarding service
|
|
80
|
+
tokens, and propagates `404` as `NotFoundError` and `304` as
|
|
81
|
+
`NotModifiedError`. The connector's response `ETag` is passed through for
|
|
82
|
+
TechDocs caching. A successful skill-docs response without a nonblank ETag is a
|
|
83
|
+
protocol error; the reader does not invent a validator or silently use an empty
|
|
84
|
+
one. Model-card requests also reject redirects; their existing ETag behavior is
|
|
85
|
+
preserved.
|
|
86
|
+
|
|
31
87
|
## Development
|
|
32
88
|
|
|
33
89
|
This plugin backend can be started in a standalone mode from directly in this
|
package/dist/plugin.cjs.js
CHANGED
|
@@ -14,11 +14,12 @@ var platformPath__default = /*#__PURE__*/_interopDefaultCompat(platformPath);
|
|
|
14
14
|
var os__default = /*#__PURE__*/_interopDefaultCompat(os);
|
|
15
15
|
|
|
16
16
|
class ModelCatalogBridgeUrlReaderServiceReadTreeResponse {
|
|
17
|
-
constructor(workDir, etag, buffer, logger) {
|
|
17
|
+
constructor(workDir, etag, buffer, logger, siteName) {
|
|
18
18
|
this.workDir = workDir;
|
|
19
19
|
this.etag = etag;
|
|
20
20
|
this.buffer = buffer;
|
|
21
21
|
this.logger = logger;
|
|
22
|
+
this.siteName = siteName ?? "Model Card";
|
|
22
23
|
this.logger = logger.child({
|
|
23
24
|
source: 'ModelCatalogBridgeUrlReaderServiceReadTreeResponse"'
|
|
24
25
|
});
|
|
@@ -27,6 +28,7 @@ class ModelCatalogBridgeUrlReaderServiceReadTreeResponse {
|
|
|
27
28
|
etag;
|
|
28
29
|
buffer;
|
|
29
30
|
logger;
|
|
31
|
+
siteName;
|
|
30
32
|
async files() {
|
|
31
33
|
throw new Error(
|
|
32
34
|
"ModelCatalogBridgeUrlReaderServiceReadTreeResponse does not implement files"
|
|
@@ -42,7 +44,11 @@ class ModelCatalogBridgeUrlReaderServiceReadTreeResponse {
|
|
|
42
44
|
platformPath__default.default.join(this.workDir, "backstage-")
|
|
43
45
|
);
|
|
44
46
|
const mkdocsPath = platformPath__default.default.join(dir, "mkdocs.yml");
|
|
45
|
-
const
|
|
47
|
+
const escapedSiteName = this.siteName.replace(/'/g, "''");
|
|
48
|
+
const mkdocsContent = `site_name: '${escapedSiteName}'
|
|
49
|
+
nav:
|
|
50
|
+
- Home: index.md
|
|
51
|
+
`;
|
|
46
52
|
try {
|
|
47
53
|
await fs__default.default.promises.writeFile(mkdocsPath, mkdocsContent);
|
|
48
54
|
} catch (error) {
|
|
@@ -100,24 +106,82 @@ function readBridgeConfig(id, config) {
|
|
|
100
106
|
defaultLifecycle: safeGetOptionalString(config, "default-lifecycle")
|
|
101
107
|
};
|
|
102
108
|
}
|
|
109
|
+
const DIGEST_RE = /^sha256:[0-9a-f]{64}$/;
|
|
110
|
+
function readSkillSourceConfigs(config) {
|
|
111
|
+
const providerConfigs = config.getOptionalConfig(
|
|
112
|
+
"catalog.providers.skillsCatalogProvider"
|
|
113
|
+
);
|
|
114
|
+
if (!providerConfigs) {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
const result = [];
|
|
118
|
+
const seen = /* @__PURE__ */ new Set();
|
|
119
|
+
for (const providerId of providerConfigs.keys()) {
|
|
120
|
+
const providerConfig = providerConfigs.getConfig(providerId);
|
|
121
|
+
const sourcesConfig = providerConfig.getOptionalConfigArray("sources");
|
|
122
|
+
if (!sourcesConfig) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
for (const sourceConfig of sourcesConfig) {
|
|
126
|
+
const sourceType = safeGetOptionalString(sourceConfig, "sourceType");
|
|
127
|
+
if (sourceType !== "oci") {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const connectorPluginId = safeGetOptionalString(
|
|
131
|
+
sourceConfig,
|
|
132
|
+
"connectorPluginId"
|
|
133
|
+
);
|
|
134
|
+
if (!connectorPluginId) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const sourceId = safeGetOptionalString(sourceConfig, "sourceId");
|
|
138
|
+
if (!sourceId.trim() || sourceId.trim() === "." || sourceId.trim() === "..") {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const key = `${connectorPluginId}:${sourceId}`;
|
|
142
|
+
if (seen.has(key)) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
seen.add(key);
|
|
146
|
+
result.push({ connectorPluginId, sourceId });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
103
151
|
class ModeCatalogBridgeTechdocUrlReader {
|
|
104
152
|
workDir;
|
|
105
153
|
logger;
|
|
106
154
|
bridgeConfigs;
|
|
155
|
+
skillSourceConfigs;
|
|
107
156
|
// TODO we are limited in which core services can be passed into the ReaderFactory, so as a work around we
|
|
108
157
|
// define these globals that are set in the plugin init method; if we merge this plugin with the
|
|
109
158
|
// kserve-kubeflow-connector plugins, and bypass the need for plugin to plugin REST calls, we can
|
|
110
159
|
// bypass the need for the auth service.
|
|
111
160
|
static auth;
|
|
161
|
+
/**
|
|
162
|
+
* Legacy factory for model-card URLs only; skill documentation fails closed
|
|
163
|
+
* without discovery-resolved connector bases. Register
|
|
164
|
+
* `catalogTechdocUrlReaderPlugin` for OCI skill documentation support.
|
|
165
|
+
*/
|
|
112
166
|
static factory = ({ config, logger }) => {
|
|
113
167
|
const reader = new ModeCatalogBridgeTechdocUrlReader(config, logger);
|
|
114
168
|
return [{ reader, predicate: reader.bridgePredicate }];
|
|
115
169
|
};
|
|
116
|
-
constructor(config, logger) {
|
|
170
|
+
constructor(config, logger, trustedConnectorBases = /* @__PURE__ */ new Map()) {
|
|
117
171
|
this.logger = logger.child({
|
|
118
172
|
source: 'ModeCatalogBridgeTechdocUrlReader"'
|
|
119
173
|
});
|
|
120
174
|
this.bridgeConfigs = readBridgeConfigs(config);
|
|
175
|
+
this.skillSourceConfigs = readSkillSourceConfigs(config).flatMap((source) => {
|
|
176
|
+
const base = trustedConnectorBases.get(source.connectorPluginId);
|
|
177
|
+
if (!base) return [];
|
|
178
|
+
const connectorBase = new URL(base);
|
|
179
|
+
if (!["http:", "https:"].includes(connectorBase.protocol) || connectorBase.username || connectorBase.password || connectorBase.search || connectorBase.hash)
|
|
180
|
+
throw new Error(
|
|
181
|
+
`Invalid discovered connector base for ${source.connectorPluginId}`
|
|
182
|
+
);
|
|
183
|
+
return [{ ...source, connectorBase }];
|
|
184
|
+
});
|
|
121
185
|
this.workDir = os__default.default.tmpdir();
|
|
122
186
|
const bkend = config.getOptionalString("backend.workingDirectory");
|
|
123
187
|
if (bkend) {
|
|
@@ -125,6 +189,7 @@ class ModeCatalogBridgeTechdocUrlReader {
|
|
|
125
189
|
}
|
|
126
190
|
}
|
|
127
191
|
matchingBridgeConfig(url) {
|
|
192
|
+
if (url.pathname.includes("/skilldocs/")) return void 0;
|
|
128
193
|
for (let index = 0; index < this.bridgeConfigs.length; index++) {
|
|
129
194
|
const bc = this.bridgeConfigs[index];
|
|
130
195
|
if (url.pathname.includes("modelcard") && url.pathname.includes(bc.id)) {
|
|
@@ -133,28 +198,72 @@ class ModeCatalogBridgeTechdocUrlReader {
|
|
|
133
198
|
}
|
|
134
199
|
return void 0;
|
|
135
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* Match a URL against configured OCI skill source destinations.
|
|
203
|
+
*
|
|
204
|
+
* Requires the discovered origin and complete base pathname, followed by
|
|
205
|
+
* /skilldocs/<sourceId>/<digest>
|
|
206
|
+
*
|
|
207
|
+
* Rejects URLs with query strings, fragments, URL credentials,
|
|
208
|
+
* extra path segments, or invalid digest format.
|
|
209
|
+
*/
|
|
210
|
+
matchingSkillSourceConfig(url) {
|
|
211
|
+
if (url.username || url.password) return void 0;
|
|
212
|
+
if (url.search || url.hash) return void 0;
|
|
213
|
+
const pathname = url.pathname;
|
|
214
|
+
for (const source of this.skillSourceConfigs) {
|
|
215
|
+
if (url.origin !== source.connectorBase.origin) continue;
|
|
216
|
+
const basePath = `${source.connectorBase.pathname.replace(
|
|
217
|
+
/\/+$/,
|
|
218
|
+
""
|
|
219
|
+
)}/skilldocs/`;
|
|
220
|
+
if (!pathname.startsWith(basePath)) continue;
|
|
221
|
+
const remainder = pathname.substring(basePath.length);
|
|
222
|
+
const parts = remainder.split("/");
|
|
223
|
+
if (parts.length !== 2 || parts[0] === "" || parts[1] === "") continue;
|
|
224
|
+
let sourceId;
|
|
225
|
+
let digest;
|
|
226
|
+
try {
|
|
227
|
+
sourceId = decodeURIComponent(parts[0]);
|
|
228
|
+
digest = decodeURIComponent(parts[1]);
|
|
229
|
+
} catch {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (sourceId !== source.sourceId) continue;
|
|
233
|
+
if (!DIGEST_RE.test(digest)) continue;
|
|
234
|
+
return source;
|
|
235
|
+
}
|
|
236
|
+
return void 0;
|
|
237
|
+
}
|
|
136
238
|
bridgePredicate = (url) => {
|
|
137
|
-
return this.matchingBridgeConfig(url) !== void 0;
|
|
239
|
+
return this.matchingBridgeConfig(url) !== void 0 || this.matchingSkillSourceConfig(url) !== void 0;
|
|
138
240
|
};
|
|
139
241
|
async readUrl(url, options) {
|
|
140
242
|
this.logger.info(`ModelCatalogBridgeTechdocUrlReader.readUrl of ${url}`);
|
|
141
243
|
const parsedUrl = new URL(url);
|
|
142
244
|
const bc = this.matchingBridgeConfig(parsedUrl);
|
|
143
|
-
if (
|
|
144
|
-
|
|
245
|
+
if (bc) {
|
|
246
|
+
return this.readModelCardUrl(url, bc, options);
|
|
247
|
+
}
|
|
248
|
+
const sc = this.matchingSkillSourceConfig(parsedUrl);
|
|
249
|
+
if (sc) {
|
|
250
|
+
return this.readSkillDocsUrl(url, sc, options);
|
|
145
251
|
}
|
|
252
|
+
throw new Error(`No matching config for ${url}`);
|
|
253
|
+
}
|
|
254
|
+
async readModelCardUrl(url, bc, options) {
|
|
146
255
|
let response;
|
|
147
256
|
try {
|
|
148
257
|
const token = await ModeCatalogBridgeTechdocUrlReader.auth.getPluginRequestToken({
|
|
149
258
|
onBehalfOf: await ModeCatalogBridgeTechdocUrlReader.auth.getOwnServiceCredentials(),
|
|
150
259
|
targetPluginId: bc.id
|
|
151
260
|
});
|
|
152
|
-
const tok = token.token;
|
|
153
261
|
this.logger.info(`Using service-to-service token for ${url}`);
|
|
154
262
|
response = await fetch(url, {
|
|
155
263
|
method: "GET",
|
|
264
|
+
redirect: "error",
|
|
156
265
|
headers: {
|
|
157
|
-
Authorization: `Bearer ${
|
|
266
|
+
Authorization: `Bearer ${token.token}`
|
|
158
267
|
},
|
|
159
268
|
signal: options?.signal
|
|
160
269
|
});
|
|
@@ -173,6 +282,57 @@ class ModeCatalogBridgeTechdocUrlReader {
|
|
|
173
282
|
}
|
|
174
283
|
throw new Error(message);
|
|
175
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* Fetch OCI skill documentation from the connector.
|
|
287
|
+
*
|
|
288
|
+
* - Authenticates with `auth.getPluginRequestToken` targeting the
|
|
289
|
+
* connector plugin's registered pluginId.
|
|
290
|
+
* - Forwards the caller's ETag as `If-None-Match` for conditional requests.
|
|
291
|
+
* - Rejects redirects without forwarding service tokens.
|
|
292
|
+
* - Propagates 404 as NotFoundError, 304 as NotModifiedError.
|
|
293
|
+
*/
|
|
294
|
+
async readSkillDocsUrl(url, source, options) {
|
|
295
|
+
const headers = {};
|
|
296
|
+
let response;
|
|
297
|
+
try {
|
|
298
|
+
const token = await ModeCatalogBridgeTechdocUrlReader.auth.getPluginRequestToken({
|
|
299
|
+
onBehalfOf: await ModeCatalogBridgeTechdocUrlReader.auth.getOwnServiceCredentials(),
|
|
300
|
+
targetPluginId: source.connectorPluginId
|
|
301
|
+
});
|
|
302
|
+
headers.Authorization = `Bearer ${token.token}`;
|
|
303
|
+
if (options?.etag) {
|
|
304
|
+
headers["If-None-Match"] = options.etag;
|
|
305
|
+
}
|
|
306
|
+
this.logger.info(
|
|
307
|
+
`Fetching OCI skill documentation from ${url} (connector: ${source.connectorPluginId})`
|
|
308
|
+
);
|
|
309
|
+
response = await fetch(url, {
|
|
310
|
+
method: "GET",
|
|
311
|
+
headers,
|
|
312
|
+
redirect: "error",
|
|
313
|
+
signal: options?.signal
|
|
314
|
+
});
|
|
315
|
+
} catch (e) {
|
|
316
|
+
throw new Error(`Unable to read ${url}, ${e}`);
|
|
317
|
+
}
|
|
318
|
+
if (response.ok) {
|
|
319
|
+
if (!response.headers.get("etag")?.trim()) {
|
|
320
|
+
await response.body?.cancel();
|
|
321
|
+
throw new Error(
|
|
322
|
+
"Skill documentation response is missing a nonblank ETag"
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
return urlReader.ReadUrlResponseFactory.fromResponse(response);
|
|
326
|
+
}
|
|
327
|
+
const message = `could not read ${url}, ${response.status} ${response.statusText}`;
|
|
328
|
+
if (response.status === 404) {
|
|
329
|
+
throw new errors.NotFoundError(message);
|
|
330
|
+
}
|
|
331
|
+
if (response.status === 304) {
|
|
332
|
+
throw new errors.NotModifiedError();
|
|
333
|
+
}
|
|
334
|
+
throw new Error(message);
|
|
335
|
+
}
|
|
176
336
|
async search(url, options) {
|
|
177
337
|
this.logger.info(
|
|
178
338
|
`ModelCatalogBridgeTechdocUrlReader.search of ${url} with options ${JSON.stringify(
|
|
@@ -185,17 +345,33 @@ class ModeCatalogBridgeTechdocUrlReader {
|
|
|
185
345
|
}
|
|
186
346
|
async readTree(url, options) {
|
|
187
347
|
this.logger.info(`ModelCatalogBridgeTechdocUrlReader.readTree of ${url}`);
|
|
348
|
+
const parsedUrl = new URL(url);
|
|
188
349
|
const data = await this.readUrl(url, options);
|
|
350
|
+
const isSkillDocs = this.matchingBridgeConfig(parsedUrl) === void 0 && this.matchingSkillSourceConfig(parsedUrl) !== void 0;
|
|
351
|
+
if (isSkillDocs) {
|
|
352
|
+
if (!data.etag?.trim()) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
"Skill documentation response is missing a nonblank ETag"
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
return new ModelCatalogBridgeUrlReaderServiceReadTreeResponse(
|
|
358
|
+
this.workDir,
|
|
359
|
+
data.etag,
|
|
360
|
+
data.buffer(),
|
|
361
|
+
this.logger,
|
|
362
|
+
"Skill Documentation"
|
|
363
|
+
);
|
|
364
|
+
}
|
|
189
365
|
const et = options?.etag;
|
|
190
366
|
if (!et) {
|
|
191
|
-
return
|
|
367
|
+
return new ModelCatalogBridgeUrlReaderServiceReadTreeResponse(
|
|
192
368
|
this.workDir,
|
|
193
369
|
"",
|
|
194
370
|
data.buffer(),
|
|
195
371
|
this.logger
|
|
196
372
|
);
|
|
197
373
|
}
|
|
198
|
-
return
|
|
374
|
+
return new ModelCatalogBridgeUrlReaderServiceReadTreeResponse(
|
|
199
375
|
this.workDir,
|
|
200
376
|
et,
|
|
201
377
|
data.buffer(),
|
|
@@ -207,12 +383,31 @@ const catalogTechdocUrlReaderPlugin = backendPluginApi.createServiceFactory({
|
|
|
207
383
|
service: urlReader.urlReaderFactoriesServiceRef,
|
|
208
384
|
deps: {
|
|
209
385
|
logger: backendPluginApi.coreServices.logger,
|
|
210
|
-
auth: backendPluginApi.coreServices.auth
|
|
386
|
+
auth: backendPluginApi.coreServices.auth,
|
|
387
|
+
discovery: backendPluginApi.coreServices.discovery,
|
|
388
|
+
config: backendPluginApi.coreServices.rootConfig
|
|
211
389
|
},
|
|
212
|
-
async factory({ logger, auth }) {
|
|
390
|
+
async factory({ logger, auth, discovery, config }) {
|
|
213
391
|
logger.child({ source: 'catalogTechdocUrlReaderPlugin"' }).info("Registering the model catalog bridge URL reader ");
|
|
214
392
|
ModeCatalogBridgeTechdocUrlReader.auth = auth;
|
|
215
|
-
|
|
393
|
+
const trustedConnectorBases = /* @__PURE__ */ new Map();
|
|
394
|
+
const connectorIds = new Set(
|
|
395
|
+
readSkillSourceConfigs(config).map((source) => source.connectorPluginId)
|
|
396
|
+
);
|
|
397
|
+
for (const connectorId of connectorIds) {
|
|
398
|
+
trustedConnectorBases.set(
|
|
399
|
+
connectorId,
|
|
400
|
+
await discovery.getBaseUrl(connectorId)
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
return ({ config: readerConfig, logger: readerLogger }) => {
|
|
404
|
+
const reader = new ModeCatalogBridgeTechdocUrlReader(
|
|
405
|
+
readerConfig,
|
|
406
|
+
readerLogger,
|
|
407
|
+
trustedConnectorBases
|
|
408
|
+
);
|
|
409
|
+
return [{ reader, predicate: reader.bridgePredicate }];
|
|
410
|
+
};
|
|
216
411
|
}
|
|
217
412
|
});
|
|
218
413
|
|
|
@@ -221,4 +416,5 @@ exports.ModelCatalogBridgeUrlReaderServiceReadTreeResponse = ModelCatalogBridgeU
|
|
|
221
416
|
exports.catalogTechdocUrlReaderPlugin = catalogTechdocUrlReaderPlugin;
|
|
222
417
|
exports.readBridgeConfig = readBridgeConfig;
|
|
223
418
|
exports.readBridgeConfigs = readBridgeConfigs;
|
|
419
|
+
exports.readSkillSourceConfigs = readSkillSourceConfigs;
|
|
224
420
|
//# sourceMappingURL=plugin.cjs.js.map
|
package/dist/plugin.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.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 ReaderFactory,\n ReadUrlResponseFactory,\n urlReaderFactoriesServiceRef,\n} from '@backstage/backend-defaults/urlReader';\nimport {\n coreServices,\n createServiceFactory,\n UrlReaderService,\n UrlReaderServiceReadUrlOptions,\n UrlReaderServiceSearchOptions,\n UrlReaderServiceSearchResponse,\n UrlReaderServiceReadUrlResponse,\n UrlReaderServiceReadTreeOptions,\n UrlReaderServiceReadTreeResponse,\n UrlReaderServiceReadTreeResponseFile,\n UrlReaderServiceReadTreeResponseDirOptions,\n LoggerService,\n AuthService,\n} from '@backstage/backend-plugin-api';\nimport { Config } from '@backstage/config';\nimport { NotFoundError, NotModifiedError } from '@backstage/errors';\nimport { Readable } from 'stream';\nimport fs from 'fs';\nimport platformPath from 'path';\nimport os from 'os';\n\nexport class ModelCatalogBridgeUrlReaderServiceReadTreeResponse\n implements UrlReaderServiceReadTreeResponse\n{\n constructor(\n private readonly workDir: string,\n public readonly etag: string,\n private readonly buffer: Promise<Buffer>,\n private readonly logger: LoggerService,\n ) {\n this.logger = logger.child({\n source: 'ModelCatalogBridgeUrlReaderServiceReadTreeResponse\"',\n });\n }\n async files(): Promise<UrlReaderServiceReadTreeResponseFile[]> {\n throw new Error(\n 'ModelCatalogBridgeUrlReaderServiceReadTreeResponse does not implement files',\n );\n }\n async archive(): Promise<Readable> {\n throw new Error(\n 'ModelCatalogBridgeUrlReaderServiceReadTreeResponse does not implement archive',\n );\n }\n\n async dir(\n options?: UrlReaderServiceReadTreeResponseDirOptions,\n ): Promise<string> {\n const dir =\n options?.targetDir ??\n (await fs.promises.mkdtemp(\n platformPath.join(this.workDir, 'backstage-'),\n ));\n\n // Write a minimal mkdocs.yml that the TechDocs builder (mkdocs)\n // requires at the project root.\n const mkdocsPath = platformPath.join(dir, 'mkdocs.yml');\n const mkdocsContent = 'site_name: Model Card\\nnav:\\n - Home: index.md\\n';\n try {\n await fs.promises.writeFile(mkdocsPath, mkdocsContent);\n } catch (error) {\n this.logger.error(`Error writing mkdocs.yml to ${mkdocsPath}:`, error);\n }\n\n const subDir = platformPath.join(dir, 'docs');\n const filePath = platformPath.join(subDir, 'index.md');\n try {\n fs.mkdirSync(subDir);\n const buf = await this.buffer;\n await fs.promises.writeFile(filePath, buf);\n } catch (error) {\n this.logger.error(`Error writing to file ${filePath}:`, error);\n }\n\n return dir;\n }\n}\n\nexport type BridgeConfig = {\n id: string;\n name: string;\n kubeflowModelCatalogUrl: string;\n defaultOwner: string;\n defaultLifecycle: string;\n};\n\nexport function readBridgeConfigs(config: Config): BridgeConfig[] {\n const configs = config.getOptionalConfig('catalog.providers.modelCatalog');\n if (!configs) {\n return [];\n }\n const result: BridgeConfig[] = [];\n for (const connectorId of configs.keys()) {\n const connectorConfig = configs.getConfig(connectorId);\n for (const clusterKey of connectorConfig.keys()) {\n const clusterConfig = connectorConfig.getOptionalConfig(clusterKey);\n if (!clusterConfig) {\n continue;\n }\n if (clusterConfig.has('frequency') || clusterConfig.has('timeout')) {\n continue;\n }\n result.push(readBridgeConfig(connectorId, clusterConfig));\n }\n }\n return result;\n}\n\nfunction safeGetOptionalString(config: Config, key: string): string {\n try {\n return config.getOptionalString(key) ?? '';\n } catch {\n // Backstage ConfigReader throws TypeError for empty-string values\n // (e.g. from env var substitution like ${VAR:-}).\n return '';\n }\n}\n\nexport function readBridgeConfig(id: string, config: Config): BridgeConfig {\n return {\n id,\n name: safeGetOptionalString(config, 'name'),\n kubeflowModelCatalogUrl: safeGetOptionalString(\n config,\n 'kubeflow-model-catalog-url',\n ),\n defaultOwner: safeGetOptionalString(config, 'default-owner'),\n defaultLifecycle: safeGetOptionalString(config, 'default-lifecycle'),\n };\n}\n\nexport class ModeCatalogBridgeTechdocUrlReader implements UrlReaderService {\n private readonly workDir: string;\n private readonly logger: LoggerService;\n private readonly bridgeConfigs: BridgeConfig[];\n\n // TODO we are limited in which core services can be passed into the ReaderFactory, so as a work around we\n // define these globals that are set in the plugin init method; if we merge this plugin with the\n // kserve-kubeflow-connector plugins, and bypass the need for plugin to plugin REST calls, we can\n // bypass the need for the auth service.\n static auth: AuthService;\n\n static factory: ReaderFactory = ({ config, logger }) => {\n const reader = new ModeCatalogBridgeTechdocUrlReader(config, logger);\n return [{ reader, predicate: reader.bridgePredicate }];\n };\n\n constructor(config: Config, logger: LoggerService) {\n this.logger = logger.child({\n source: 'ModeCatalogBridgeTechdocUrlReader\"',\n });\n this.bridgeConfigs = readBridgeConfigs(config);\n this.workDir = os.tmpdir();\n\n const bkend = config.getOptionalString('backend.workingDirectory');\n if (bkend) {\n this.workDir = bkend;\n }\n }\n\n private matchingBridgeConfig(url: URL): BridgeConfig | undefined {\n for (let index = 0; index < this.bridgeConfigs.length; index++) {\n const bc = this.bridgeConfigs[index];\n if (url.pathname.includes('modelcard') && url.pathname.includes(bc.id)) {\n return bc;\n }\n }\n return undefined;\n }\n\n bridgePredicate = (url: URL): boolean => {\n return this.matchingBridgeConfig(url) !== undefined;\n };\n\n async readUrl(\n url: string,\n options?: UrlReaderServiceReadUrlOptions,\n ): Promise<UrlReaderServiceReadUrlResponse> {\n this.logger.info(`ModelCatalogBridgeTechdocUrlReader.readUrl of ${url}`);\n const parsedUrl = new URL(url);\n const bc = this.matchingBridgeConfig(parsedUrl);\n if (!bc) {\n throw new Error(`No matching bridge config for ${url}`);\n }\n let response: Response;\n try {\n const token =\n await ModeCatalogBridgeTechdocUrlReader.auth.getPluginRequestToken({\n onBehalfOf:\n await ModeCatalogBridgeTechdocUrlReader.auth.getOwnServiceCredentials(),\n targetPluginId: bc.id,\n });\n const tok = token.token;\n this.logger.info(`Using service-to-service token for ${url}`);\n response = await fetch(url, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${tok}`,\n },\n signal: options?.signal as any,\n });\n } catch (e) {\n throw new Error(`Unable to read ${url}, ${e}`);\n }\n\n if (response.ok) {\n return ReadUrlResponseFactory.fromResponse(response);\n }\n\n const message = `could not read ${url}, ${response.status} ${response.statusText}`;\n if (response.status === 404) {\n throw new NotFoundError(message);\n }\n if (response.status === 304) {\n throw new NotModifiedError();\n }\n throw new Error(message);\n }\n\n async search(\n url: string,\n options?: UrlReaderServiceSearchOptions,\n ): Promise<UrlReaderServiceSearchResponse> {\n this.logger.info(\n `ModelCatalogBridgeTechdocUrlReader.search of ${url} with options ${JSON.stringify(\n options,\n )}`,\n );\n\n throw new Error(\n 'ModeCatalogBridgeTechdocUrlReader does not implement search',\n );\n // const { pathname } = new URL(url);\n\n // if (pathname.match(/[*?]/)) {\n // throw new Error('Unsupported search pattern URL');\n // }\n\n // try {\n // const data = await this.readUrl(url, options);\n\n // return {\n // files: [\n // {\n // url: url,\n // content: data.buffer,\n // lastModifiedAt: data.lastModifiedAt,\n // },\n // ],\n // etag: data.etag ?? '',\n // };\n // } catch (error) {\n // assertError(error);\n // if (error.name === 'NotFoundError') {\n // return {\n // files: [],\n // etag: '',\n // };\n // }\n // throw error;\n // }\n }\n\n async readTree(\n url: string,\n options?: UrlReaderServiceReadTreeOptions,\n ): Promise<UrlReaderServiceReadTreeResponse> {\n this.logger.info(`ModelCatalogBridgeTechdocUrlReader.readTree of ${url}`);\n const data = await this.readUrl(url, options);\n\n const et = options?.etag;\n if (!et) {\n return await new ModelCatalogBridgeUrlReaderServiceReadTreeResponse(\n this.workDir,\n '',\n data.buffer(),\n this.logger,\n );\n }\n return await new ModelCatalogBridgeUrlReaderServiceReadTreeResponse(\n this.workDir,\n et,\n data.buffer(),\n this.logger,\n );\n }\n}\n\n/**\n * catalogTechdocUrlReaderPlugin define the factory for the ModelCatalogBridgeUrlReader service\n *\n * @public\n */\nexport const catalogTechdocUrlReaderPlugin = createServiceFactory({\n service: urlReaderFactoriesServiceRef,\n deps: {\n logger: coreServices.logger,\n auth: coreServices.auth,\n },\n async factory({ logger, auth }) {\n logger\n .child({ source: 'catalogTechdocUrlReaderPlugin\"' })\n .info('Registering the model catalog bridge URL reader ');\n\n ModeCatalogBridgeTechdocUrlReader.auth = auth;\n\n return ModeCatalogBridgeTechdocUrlReader.factory;\n },\n});\n"],"names":["fs","platformPath","os","ReadUrlResponseFactory","NotFoundError","NotModifiedError","createServiceFactory","urlReaderFactoriesServiceRef","coreServices"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.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 ReaderFactory,\n ReadUrlResponseFactory,\n urlReaderFactoriesServiceRef,\n} from '@backstage/backend-defaults/urlReader';\nimport {\n coreServices,\n createServiceFactory,\n UrlReaderService,\n UrlReaderServiceReadUrlOptions,\n UrlReaderServiceSearchOptions,\n UrlReaderServiceSearchResponse,\n UrlReaderServiceReadUrlResponse,\n UrlReaderServiceReadTreeOptions,\n UrlReaderServiceReadTreeResponse,\n UrlReaderServiceReadTreeResponseFile,\n UrlReaderServiceReadTreeResponseDirOptions,\n LoggerService,\n AuthService,\n} from '@backstage/backend-plugin-api';\nimport { Config } from '@backstage/config';\nimport { NotFoundError, NotModifiedError } from '@backstage/errors';\nimport { Readable } from 'stream';\nimport fs from 'fs';\nimport platformPath from 'path';\nimport os from 'os';\n\nexport class ModelCatalogBridgeUrlReaderServiceReadTreeResponse\n implements UrlReaderServiceReadTreeResponse\n{\n private readonly siteName: string;\n\n constructor(\n private readonly workDir: string,\n public readonly etag: string,\n private readonly buffer: Promise<Buffer>,\n private readonly logger: LoggerService,\n siteName?: string,\n ) {\n this.siteName = siteName ?? 'Model Card';\n this.logger = logger.child({\n source: 'ModelCatalogBridgeUrlReaderServiceReadTreeResponse\"',\n });\n }\n async files(): Promise<UrlReaderServiceReadTreeResponseFile[]> {\n throw new Error(\n 'ModelCatalogBridgeUrlReaderServiceReadTreeResponse does not implement files',\n );\n }\n async archive(): Promise<Readable> {\n throw new Error(\n 'ModelCatalogBridgeUrlReaderServiceReadTreeResponse does not implement archive',\n );\n }\n\n async dir(\n options?: UrlReaderServiceReadTreeResponseDirOptions,\n ): Promise<string> {\n const dir =\n options?.targetDir ??\n (await fs.promises.mkdtemp(\n platformPath.join(this.workDir, 'backstage-'),\n ));\n\n // Write a minimal mkdocs.yml that the TechDocs builder (mkdocs)\n // requires at the project root.\n const mkdocsPath = platformPath.join(dir, 'mkdocs.yml');\n const escapedSiteName = this.siteName.replace(/'/g, \"''\");\n const mkdocsContent = `site_name: '${escapedSiteName}'\\nnav:\\n - Home: index.md\\n`;\n try {\n await fs.promises.writeFile(mkdocsPath, mkdocsContent);\n } catch (error) {\n this.logger.error(`Error writing mkdocs.yml to ${mkdocsPath}:`, error);\n }\n\n const subDir = platformPath.join(dir, 'docs');\n const filePath = platformPath.join(subDir, 'index.md');\n try {\n fs.mkdirSync(subDir);\n const buf = await this.buffer;\n await fs.promises.writeFile(filePath, buf);\n } catch (error) {\n this.logger.error(`Error writing to file ${filePath}:`, error);\n }\n\n return dir;\n }\n}\n\nexport type BridgeConfig = {\n id: string;\n name: string;\n kubeflowModelCatalogUrl: string;\n defaultOwner: string;\n defaultLifecycle: string;\n};\n\nexport function readBridgeConfigs(config: Config): BridgeConfig[] {\n const configs = config.getOptionalConfig('catalog.providers.modelCatalog');\n if (!configs) {\n return [];\n }\n const result: BridgeConfig[] = [];\n for (const connectorId of configs.keys()) {\n const connectorConfig = configs.getConfig(connectorId);\n for (const clusterKey of connectorConfig.keys()) {\n const clusterConfig = connectorConfig.getOptionalConfig(clusterKey);\n if (!clusterConfig) {\n continue;\n }\n if (clusterConfig.has('frequency') || clusterConfig.has('timeout')) {\n continue;\n }\n result.push(readBridgeConfig(connectorId, clusterConfig));\n }\n }\n return result;\n}\n\nfunction safeGetOptionalString(config: Config, key: string): string {\n try {\n return config.getOptionalString(key) ?? '';\n } catch {\n // Backstage ConfigReader throws TypeError for empty-string values\n // (e.g. from env var substitution like ${VAR:-}).\n return '';\n }\n}\n\nexport function readBridgeConfig(id: string, config: Config): BridgeConfig {\n return {\n id,\n name: safeGetOptionalString(config, 'name'),\n kubeflowModelCatalogUrl: safeGetOptionalString(\n config,\n 'kubeflow-model-catalog-url',\n ),\n defaultOwner: safeGetOptionalString(config, 'default-owner'),\n defaultLifecycle: safeGetOptionalString(config, 'default-lifecycle'),\n };\n}\n\n// ─── OCI skill TechDocs source configuration ────────────────────────\n\n/** Digest format: sha256:<64 lowercase hex digits>. */\nconst DIGEST_RE = /^sha256:[0-9a-f]{64}$/;\n\nexport type SkillSourceConfig = {\n /** Backstage plugin ID of the connector (e.g. 'skill-image-connector'). */\n connectorPluginId: string;\n /** Configured source identifier. */\n sourceId: string;\n};\n\n/**\n * Read OCI skill source configurations from\n * `catalog.providers.skillsCatalogProvider.*.sources[]`.\n *\n * Only `oci` source types are supported;\n * `npx` sources are skipped.\n *\n * Source IDs that are blank, `.`, or `..` are rejected because URL\n * parsing normalizes literal and encoded dot segments, making them\n * unreachable as stable path components.\n */\nexport function readSkillSourceConfigs(config: Config): SkillSourceConfig[] {\n const providerConfigs = config.getOptionalConfig(\n 'catalog.providers.skillsCatalogProvider',\n );\n if (!providerConfigs) {\n return [];\n }\n\n const result: SkillSourceConfig[] = [];\n const seen = new Set<string>();\n\n for (const providerId of providerConfigs.keys()) {\n const providerConfig = providerConfigs.getConfig(providerId);\n const sourcesConfig = providerConfig.getOptionalConfigArray('sources');\n if (!sourcesConfig) {\n continue;\n }\n\n for (const sourceConfig of sourcesConfig) {\n const sourceType = safeGetOptionalString(sourceConfig, 'sourceType');\n if (sourceType !== 'oci') {\n continue; // npx and other source types are not supported\n }\n\n const connectorPluginId = safeGetOptionalString(\n sourceConfig,\n 'connectorPluginId',\n );\n if (!connectorPluginId) {\n continue;\n }\n\n const sourceId = safeGetOptionalString(sourceConfig, 'sourceId');\n if (\n !sourceId.trim() ||\n sourceId.trim() === '.' ||\n sourceId.trim() === '..'\n ) {\n continue; // Reject blank, dot, and dot-dot source IDs\n }\n\n // Deduplicate across provider instances\n const key = `${connectorPluginId}:${sourceId}`;\n if (seen.has(key)) {\n continue;\n }\n seen.add(key);\n\n result.push({ connectorPluginId, sourceId });\n }\n }\n\n return result;\n}\n\nexport class ModeCatalogBridgeTechdocUrlReader implements UrlReaderService {\n private readonly workDir: string;\n private readonly logger: LoggerService;\n private readonly bridgeConfigs: BridgeConfig[];\n private readonly skillSourceConfigs: Array<\n SkillSourceConfig & { connectorBase: URL }\n >;\n\n // TODO we are limited in which core services can be passed into the ReaderFactory, so as a work around we\n // define these globals that are set in the plugin init method; if we merge this plugin with the\n // kserve-kubeflow-connector plugins, and bypass the need for plugin to plugin REST calls, we can\n // bypass the need for the auth service.\n static auth: AuthService;\n\n /**\n * Legacy factory for model-card URLs only; skill documentation fails closed\n * without discovery-resolved connector bases. Register\n * `catalogTechdocUrlReaderPlugin` for OCI skill documentation support.\n */\n static factory: ReaderFactory = ({ config, logger }) => {\n const reader = new ModeCatalogBridgeTechdocUrlReader(config, logger);\n return [{ reader, predicate: reader.bridgePredicate }];\n };\n\n constructor(\n config: Config,\n logger: LoggerService,\n trustedConnectorBases: ReadonlyMap<string, string> = new Map(),\n ) {\n this.logger = logger.child({\n source: 'ModeCatalogBridgeTechdocUrlReader\"',\n });\n this.bridgeConfigs = readBridgeConfigs(config);\n // Bases come only from service discovery during factory initialization.\n // Direct construction without them fails closed for skill documentation.\n this.skillSourceConfigs = readSkillSourceConfigs(config).flatMap(source => {\n const base = trustedConnectorBases.get(source.connectorPluginId);\n if (!base) return [];\n const connectorBase = new URL(base);\n if (\n !['http:', 'https:'].includes(connectorBase.protocol) ||\n connectorBase.username ||\n connectorBase.password ||\n connectorBase.search ||\n connectorBase.hash\n )\n throw new Error(\n `Invalid discovered connector base for ${source.connectorPluginId}`,\n );\n return [{ ...source, connectorBase }];\n });\n this.workDir = os.tmpdir();\n\n const bkend = config.getOptionalString('backend.workingDirectory');\n if (bkend) {\n this.workDir = bkend;\n }\n }\n\n private matchingBridgeConfig(url: URL): BridgeConfig | undefined {\n // Skill documentation must never fall through to the legacy substring matcher.\n if (url.pathname.includes('/skilldocs/')) return undefined;\n for (let index = 0; index < this.bridgeConfigs.length; index++) {\n const bc = this.bridgeConfigs[index];\n if (url.pathname.includes('modelcard') && url.pathname.includes(bc.id)) {\n return bc;\n }\n }\n return undefined;\n }\n\n /**\n * Match a URL against configured OCI skill source destinations.\n *\n * Requires the discovered origin and complete base pathname, followed by\n * /skilldocs/<sourceId>/<digest>\n *\n * Rejects URLs with query strings, fragments, URL credentials,\n * extra path segments, or invalid digest format.\n */\n private matchingSkillSourceConfig(url: URL): SkillSourceConfig | undefined {\n // Reject URL credentials\n if (url.username || url.password) return undefined;\n // Reject query strings and fragments\n if (url.search || url.hash) return undefined;\n\n const pathname = url.pathname;\n\n for (const source of this.skillSourceConfigs) {\n if (url.origin !== source.connectorBase.origin) continue;\n const basePath = `${source.connectorBase.pathname.replace(\n /\\/+$/,\n '',\n )}/skilldocs/`;\n if (!pathname.startsWith(basePath)) continue;\n\n const remainder = pathname.substring(basePath.length);\n // remainder must be exactly <encodedSourceId>/<encodedDigest>\n const parts = remainder.split('/');\n if (parts.length !== 2 || parts[0] === '' || parts[1] === '') continue;\n\n let sourceId: string;\n let digest: string;\n try {\n sourceId = decodeURIComponent(parts[0]);\n digest = decodeURIComponent(parts[1]);\n } catch {\n // Reader predicates reject malformed input rather than breaking selection.\n continue;\n }\n\n // Validate source ID matches configuration\n if (sourceId !== source.sourceId) continue;\n\n // Validate digest format: sha256:<64 lowercase hex digits>\n if (!DIGEST_RE.test(digest)) continue;\n\n return source;\n }\n return undefined;\n }\n\n bridgePredicate = (url: URL): boolean => {\n return (\n this.matchingBridgeConfig(url) !== undefined ||\n this.matchingSkillSourceConfig(url) !== undefined\n );\n };\n\n async readUrl(\n url: string,\n options?: UrlReaderServiceReadUrlOptions,\n ): Promise<UrlReaderServiceReadUrlResponse> {\n this.logger.info(`ModelCatalogBridgeTechdocUrlReader.readUrl of ${url}`);\n const parsedUrl = new URL(url);\n\n // Try model-card match first (preserve existing behavior)\n const bc = this.matchingBridgeConfig(parsedUrl);\n if (bc) {\n return this.readModelCardUrl(url, bc, options);\n }\n\n // Try OCI skill docs match\n const sc = this.matchingSkillSourceConfig(parsedUrl);\n if (sc) {\n return this.readSkillDocsUrl(url, sc, options);\n }\n\n throw new Error(`No matching config for ${url}`);\n }\n\n private async readModelCardUrl(\n url: string,\n bc: BridgeConfig,\n options?: UrlReaderServiceReadUrlOptions,\n ): Promise<UrlReaderServiceReadUrlResponse> {\n let response: Response;\n try {\n const token =\n await ModeCatalogBridgeTechdocUrlReader.auth.getPluginRequestToken({\n onBehalfOf:\n await ModeCatalogBridgeTechdocUrlReader.auth.getOwnServiceCredentials(),\n targetPluginId: bc.id,\n });\n this.logger.info(`Using service-to-service token for ${url}`);\n response = await fetch(url, {\n method: 'GET',\n redirect: 'error',\n headers: {\n Authorization: `Bearer ${token.token}`,\n },\n signal: options?.signal as any,\n });\n } catch (e) {\n throw new Error(`Unable to read ${url}, ${e}`);\n }\n\n if (response.ok) {\n return ReadUrlResponseFactory.fromResponse(response);\n }\n\n const message = `could not read ${url}, ${response.status} ${response.statusText}`;\n if (response.status === 404) {\n throw new NotFoundError(message);\n }\n if (response.status === 304) {\n throw new NotModifiedError();\n }\n throw new Error(message);\n }\n\n /**\n * Fetch OCI skill documentation from the connector.\n *\n * - Authenticates with `auth.getPluginRequestToken` targeting the\n * connector plugin's registered pluginId.\n * - Forwards the caller's ETag as `If-None-Match` for conditional requests.\n * - Rejects redirects without forwarding service tokens.\n * - Propagates 404 as NotFoundError, 304 as NotModifiedError.\n */\n private async readSkillDocsUrl(\n url: string,\n source: SkillSourceConfig,\n options?: UrlReaderServiceReadUrlOptions,\n ): Promise<UrlReaderServiceReadUrlResponse> {\n const headers: Record<string, string> = {};\n\n let response: Response;\n try {\n const token =\n await ModeCatalogBridgeTechdocUrlReader.auth.getPluginRequestToken({\n onBehalfOf:\n await ModeCatalogBridgeTechdocUrlReader.auth.getOwnServiceCredentials(),\n targetPluginId: source.connectorPluginId,\n });\n headers.Authorization = `Bearer ${token.token}`;\n\n // Forward caller's ETag for conditional requests (D8)\n if (options?.etag) {\n headers['If-None-Match'] = options.etag;\n }\n\n this.logger.info(\n `Fetching OCI skill documentation from ${url} (connector: ${source.connectorPluginId})`,\n );\n response = await fetch(url, {\n method: 'GET',\n headers,\n redirect: 'error',\n signal: options?.signal as any,\n });\n } catch (e) {\n throw new Error(`Unable to read ${url}, ${e}`);\n }\n\n if (response.ok) {\n if (!response.headers.get('etag')?.trim()) {\n await response.body?.cancel();\n throw new Error(\n 'Skill documentation response is missing a nonblank ETag',\n );\n }\n return ReadUrlResponseFactory.fromResponse(response);\n }\n\n const message = `could not read ${url}, ${response.status} ${response.statusText}`;\n if (response.status === 404) {\n throw new NotFoundError(message);\n }\n if (response.status === 304) {\n throw new NotModifiedError();\n }\n throw new Error(message);\n }\n\n async search(\n url: string,\n options?: UrlReaderServiceSearchOptions,\n ): Promise<UrlReaderServiceSearchResponse> {\n this.logger.info(\n `ModelCatalogBridgeTechdocUrlReader.search of ${url} with options ${JSON.stringify(\n options,\n )}`,\n );\n\n throw new Error(\n 'ModeCatalogBridgeTechdocUrlReader does not implement search',\n );\n // const { pathname } = new URL(url);\n\n // if (pathname.match(/[*?]/)) {\n // throw new Error('Unsupported search pattern URL');\n // }\n\n // try {\n // const data = await this.readUrl(url, options);\n\n // return {\n // files: [\n // {\n // url: url,\n // content: data.buffer,\n // lastModifiedAt: data.lastModifiedAt,\n // },\n // ],\n // etag: data.etag ?? '',\n // };\n // } catch (error) {\n // assertError(error);\n // if (error.name === 'NotFoundError') {\n // return {\n // files: [],\n // etag: '',\n // };\n // }\n // throw error;\n // }\n }\n\n async readTree(\n url: string,\n options?: UrlReaderServiceReadTreeOptions,\n ): Promise<UrlReaderServiceReadTreeResponse> {\n this.logger.info(`ModelCatalogBridgeTechdocUrlReader.readTree of ${url}`);\n const parsedUrl = new URL(url);\n\n const data = await this.readUrl(url, options);\n\n // Mirror readUrl's priority: bridge config first, then skill docs\n const isSkillDocs =\n this.matchingBridgeConfig(parsedUrl) === undefined &&\n this.matchingSkillSourceConfig(parsedUrl) !== undefined;\n\n if (isSkillDocs) {\n // Use the connector's response ETag for TechDocs caching (D8).\n if (!data.etag?.trim()) {\n throw new Error(\n 'Skill documentation response is missing a nonblank ETag',\n );\n }\n return new ModelCatalogBridgeUrlReaderServiceReadTreeResponse(\n this.workDir,\n data.etag,\n data.buffer(),\n this.logger,\n 'Skill Documentation',\n );\n }\n\n // Preserve existing model-card behavior\n const et = options?.etag;\n if (!et) {\n return new ModelCatalogBridgeUrlReaderServiceReadTreeResponse(\n this.workDir,\n '',\n data.buffer(),\n this.logger,\n );\n }\n return new ModelCatalogBridgeUrlReaderServiceReadTreeResponse(\n this.workDir,\n et,\n data.buffer(),\n this.logger,\n );\n }\n}\n\n/**\n * catalogTechdocUrlReaderPlugin define the factory for the ModelCatalogBridgeUrlReader service\n *\n * @public\n */\nexport const catalogTechdocUrlReaderPlugin = createServiceFactory({\n service: urlReaderFactoriesServiceRef,\n deps: {\n logger: coreServices.logger,\n auth: coreServices.auth,\n discovery: coreServices.discovery,\n config: coreServices.rootConfig,\n },\n async factory({ logger, auth, discovery, config }) {\n logger\n .child({ source: 'catalogTechdocUrlReaderPlugin\"' })\n .info('Registering the model catalog bridge URL reader ');\n\n ModeCatalogBridgeTechdocUrlReader.auth = auth;\n\n // Reader predicates are synchronous, so resolve trusted destinations before\n // publishing the factory. Do not infer a destination from annotation text.\n const trustedConnectorBases = new Map<string, string>();\n const connectorIds = new Set(\n readSkillSourceConfigs(config).map(source => source.connectorPluginId),\n );\n for (const connectorId of connectorIds) {\n trustedConnectorBases.set(\n connectorId,\n await discovery.getBaseUrl(connectorId),\n );\n }\n return ({ config: readerConfig, logger: readerLogger }) => {\n const reader = new ModeCatalogBridgeTechdocUrlReader(\n readerConfig,\n readerLogger,\n trustedConnectorBases,\n );\n return [{ reader, predicate: reader.bridgePredicate }];\n };\n },\n});\n"],"names":["fs","platformPath","os","ReadUrlResponseFactory","NotFoundError","NotModifiedError","createServiceFactory","urlReaderFactoriesServiceRef","coreServices"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@red-hat-developer-hub/backstage-plugin-catalog-techdoc-url-reader-backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "dist/index.cjs.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"postpack": "backstage-cli package postpack"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@backstage/backend-defaults": "^0.17.
|
|
37
|
+
"@backstage/backend-defaults": "^0.17.8",
|
|
38
38
|
"@backstage/backend-plugin-api": "^1.10.0",
|
|
39
39
|
"@backstage/catalog-client": "^1.16.1",
|
|
40
40
|
"@backstage/config": "^1.3.8",
|