@red-hat-developer-hub/cli-module-install-dynamic-plugins 0.2.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 +7 -0
- package/README.md +126 -0
- package/bin/install-dynamic-plugins +32 -0
- package/dist/catalog-index.cjs.js +242 -0
- package/dist/catalog-index.cjs.js.map +1 -0
- package/dist/command.cjs.js +12 -0
- package/dist/command.cjs.js.map +1 -0
- package/dist/concurrency.cjs.js +86 -0
- package/dist/concurrency.cjs.js.map +1 -0
- package/dist/errors.cjs.js +11 -0
- package/dist/errors.cjs.js.map +1 -0
- package/dist/image-cache.cjs.js +116 -0
- package/dist/image-cache.cjs.js.map +1 -0
- package/dist/image-resolver.cjs.js +24 -0
- package/dist/image-resolver.cjs.js.map +1 -0
- package/dist/index.cjs.js +20 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/installer-npm.cjs.js +111 -0
- package/dist/installer-npm.cjs.js.map +1 -0
- package/dist/installer-oci.cjs.js +106 -0
- package/dist/installer-oci.cjs.js.map +1 -0
- package/dist/installer.cjs.js +426 -0
- package/dist/installer.cjs.js.map +1 -0
- package/dist/integrity.cjs.js +79 -0
- package/dist/integrity.cjs.js.map +1 -0
- package/dist/lock-file.cjs.js +100 -0
- package/dist/lock-file.cjs.js.map +1 -0
- package/dist/log.cjs.js +9 -0
- package/dist/log.cjs.js.map +1 -0
- package/dist/merger.cjs.js +333 -0
- package/dist/merger.cjs.js.map +1 -0
- package/dist/npm-key.cjs.js +44 -0
- package/dist/npm-key.cjs.js.map +1 -0
- package/dist/oci-key.cjs.js +102 -0
- package/dist/oci-key.cjs.js.map +1 -0
- package/dist/package.json.cjs.js +104 -0
- package/dist/package.json.cjs.js.map +1 -0
- package/dist/plugin-hash.cjs.js +85 -0
- package/dist/plugin-hash.cjs.js.map +1 -0
- package/dist/run.cjs.js +37 -0
- package/dist/run.cjs.js.map +1 -0
- package/dist/skopeo.cjs.js +87 -0
- package/dist/skopeo.cjs.js.map +1 -0
- package/dist/tar-extract.cjs.js +155 -0
- package/dist/tar-extract.cjs.js.map +1 -0
- package/dist/types.cjs.js +45 -0
- package/dist/types.cjs.js.map +1 -0
- package/dist/util.cjs.js +56 -0
- package/dist/util.cjs.js.map +1 -0
- package/dist/which.cjs.js +45 -0
- package/dist/which.cjs.js.map +1 -0
- package/package.json +68 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @red-hat-developer-hub/cli-module-install-dynamic-plugins
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2d59f34: Initial release. TypeScript/Node.js port of the RHDH init-container installer (originally Python; see [redhat-developer/rhdh#4574](https://github.com/redhat-developer/rhdh/pull/4574)), packaged as a Backstage CLI module. The `install` command is registered through `createCliModule` so the package is auto-discovered by `backstage-cli` when listed as a dependency. The package also ships a self-contained esbuild bundle as its `bin`, so direct `npx install-dynamic-plugins <dir>` invocations (and RHDH's init-container `COPY` of the `.cjs`) stay fast and don't require `@backstage/cli-node` at runtime. Env vars, on-disk layout, `plugin-hash` format, and tar/OCI security guards are byte-compatible with the previous Python implementation.
|
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# cli-module-install-dynamic-plugins
|
|
2
|
+
|
|
3
|
+
Backstage CLI module that downloads, extracts, and configures RHDH dynamic plugins listed in a `dynamic-plugins.yaml` file.
|
|
4
|
+
|
|
5
|
+
This package replaces the previous Python implementation (`install-dynamic-plugins.py`) with a TypeScript/Node.js implementation. The runtime contract — input config, output `app-config.dynamic-plugins.yaml`, on-disk layout, hash-based change detection, lock file — is **unchanged**.
|
|
6
|
+
|
|
7
|
+
The package has two invocation paths, both running the same `installer.ts` pipeline:
|
|
8
|
+
|
|
9
|
+
- **`bin/install-dynamic-plugins` → fast-path** that loads `dist/installer.cjs.js` directly. Direct `npx install-dynamic-plugins` and any host that resolves the bin via `node_modules/.bin/...` hits this path — bypasses `@backstage/cli-node`'s `runCliModule` dispatch (~80 ms saved on cold start).
|
|
10
|
+
- **`main: dist/index.cjs.js` → `createCliModule(...)`**, exposed for `backstage-cli` discovery. When a host project lists this package as a dependency, `backstage-cli install <dynamic-plugins-root>` is registered automatically.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
### Direct (bundled bin)
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npx @red-hat-developer-hub/cli-module-install-dynamic-plugins ./dynamic-plugins-root
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or install globally:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm install -g @red-hat-developer-hub/cli-module-install-dynamic-plugins
|
|
24
|
+
install-dynamic-plugins ./dynamic-plugins-root
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Via `backstage-cli` discovery
|
|
28
|
+
|
|
29
|
+
When the package is a dependency of a project that uses `backstage-cli`, the `install` command is registered automatically:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
backstage-cli install ./dynamic-plugins-root
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Runtime requirements: Node.js 22 or 24, and `skopeo` on `PATH` for OCI plugin support. `npm` is also expected on `PATH` for NPM-sourced plugins.
|
|
36
|
+
|
|
37
|
+
## How RHDH consumes it
|
|
38
|
+
|
|
39
|
+
The init container invokes the wrapper `install-dynamic-plugins.sh /dynamic-plugins-root`, which delegates to the bin installed via `yarn install` from this package (see [redhat-developer/rhdh#4908](https://github.com/redhat-developer/rhdh/pull/4908)). Node.js is already present in the runtime image (it runs the Backstage backend), and `skopeo` is installed for OCI inspection — no new system packages are required.
|
|
40
|
+
|
|
41
|
+
## Architecture
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
src/
|
|
45
|
+
├── index.ts # createCliModule default export (backstage-cli discovery)
|
|
46
|
+
├── command.ts # loader for the `install` command (used by cli-module)
|
|
47
|
+
├── installer.ts # install pipeline + main() — the single source of truth
|
|
48
|
+
├── log.ts # uniform stdout logger
|
|
49
|
+
├── errors.ts # InstallException
|
|
50
|
+
├── types.ts # PluginSpec / Plugin / PluginMap / PullPolicy + constants
|
|
51
|
+
├── util.ts # shared helpers (fileExists, isInside, isPlainObject, tar filters)
|
|
52
|
+
├── run.ts # subprocess wrapper with structured errors
|
|
53
|
+
├── concurrency.ts # Semaphore + mapConcurrent + getWorkers()
|
|
54
|
+
├── which.ts # PATH lookup (no `which` dep)
|
|
55
|
+
├── skopeo.ts # Skopeo wrapper with promise-based inspect cache
|
|
56
|
+
├── image-resolver.ts # registry.access.redhat.com → quay.io fallback
|
|
57
|
+
├── image-cache.ts # OciImageCache — share OCI tarballs across plugins
|
|
58
|
+
├── tar-extract.ts # streaming OCI / NPM extraction with security checks
|
|
59
|
+
├── npm-key.ts # NPM package-spec parsing
|
|
60
|
+
├── oci-key.ts # OCI package-spec parsing + {{inherit}} + auto-path
|
|
61
|
+
├── integrity.ts # streaming SRI integrity verification
|
|
62
|
+
├── merger.ts # plugin merging + deep-merge with conflict detection
|
|
63
|
+
├── plugin-hash.ts # hash for change-detection ("already installed?")
|
|
64
|
+
├── installer-oci.ts # install one OCI plugin
|
|
65
|
+
├── installer-npm.ts # install one NPM (or local) plugin
|
|
66
|
+
├── catalog-index.ts # CATALOG_INDEX_IMAGE extraction
|
|
67
|
+
└── lock-file.ts # exclusive lock + SIGTERM cleanup
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Concurrency strategy (resource-conscious)
|
|
71
|
+
|
|
72
|
+
OCI plugin downloads are parallelized via `mapConcurrent`. NPM `npm pack` calls stay sequential because the upstream npm registry throttles parallel fetches.
|
|
73
|
+
|
|
74
|
+
The default worker count comes from `getWorkers()`:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
Math.max(1, Math.min(Math.floor(availableParallelism() / 2), 6))
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`availableParallelism()` honours cgroup CPU limits, so init containers in OpenShift won't try to use 16 workers on a 0.5 CPU pod. Override with `DYNAMIC_PLUGINS_WORKERS=<n>`.
|
|
81
|
+
|
|
82
|
+
### Memory budget
|
|
83
|
+
|
|
84
|
+
All tar extraction is streaming via `node-tar` — large layers never load into RAM. SHA verification streams chunks through `node:crypto`. A typical 10-plugin run sits around 20–80 MB peak RSS, comfortably below an init-container memory limit of 512 Mi.
|
|
85
|
+
|
|
86
|
+
### Security checks (parity with the previous Python script)
|
|
87
|
+
|
|
88
|
+
| Check | Source |
|
|
89
|
+
| --------------------------------------------------------------------- | ------------------------------------ |
|
|
90
|
+
| Path-traversal in plugin path (`..`, absolute paths) | `tar-extract.ts` |
|
|
91
|
+
| Per-entry size cap (zip bomb) — `MAX_ENTRY_SIZE`, default 20 MB | `tar-extract.ts`, `catalog-index.ts` |
|
|
92
|
+
| Symlink / hardlink target must stay inside destination | `tar-extract.ts` |
|
|
93
|
+
| Reject device files / FIFOs / unknown entry types | `tar-extract.ts` |
|
|
94
|
+
| `package/` prefix enforced for NPM tarballs | `tar-extract.ts` |
|
|
95
|
+
| SRI integrity verification (`sha256` / `sha384` / `sha512`) | `integrity.ts` |
|
|
96
|
+
| Registry fallback: `registry.access.redhat.com/rhdh` → `quay.io/rhdh` | `image-resolver.ts` |
|
|
97
|
+
|
|
98
|
+
## Environment variables
|
|
99
|
+
|
|
100
|
+
| Variable | Default | Purpose |
|
|
101
|
+
| --------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
102
|
+
| `MAX_ENTRY_SIZE` | `20000000` | Per-entry byte limit when extracting tarballs |
|
|
103
|
+
| `SKIP_INTEGRITY_CHECK` | `false` | When `true`, skip the SRI integrity check for remote NPM packages |
|
|
104
|
+
| `CATALOG_INDEX_IMAGE` | _(unset)_ | OCI image to extract `dynamic-plugins.default.yaml` and catalog entities from |
|
|
105
|
+
| `CATALOG_ENTITIES_EXTRACT_DIR` | `$TMPDIR/extensions` | Where to extract `catalog-entities/` from the catalog-index image |
|
|
106
|
+
| `DYNAMIC_PLUGINS_WORKERS` | `auto` | Worker count override for parallel OCI downloads (`auto` uses `availableParallelism()/2`, capped at 6) |
|
|
107
|
+
| `DYNAMIC_PLUGINS_LOCK_TIMEOUT_MS` | `600000` (10 min) | Max time to wait for the lock file before aborting with an error |
|
|
108
|
+
|
|
109
|
+
## Development
|
|
110
|
+
|
|
111
|
+
From the workspace root:
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
yarn install
|
|
115
|
+
yarn tsc # type-check
|
|
116
|
+
yarn test # Jest unit tests (166 tests)
|
|
117
|
+
yarn workspace @red-hat-developer-hub/cli-module-install-dynamic-plugins build
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`yarn build` runs `backstage-cli package build` and emits the unbundled `dist/*.cjs.js` + type declarations. The package is published as-is; no committed bundle.
|
|
121
|
+
|
|
122
|
+
## Compatibility notes
|
|
123
|
+
|
|
124
|
+
- The **input contract** matches the previous Python script exactly: same `dynamic-plugins.yaml` schema (`includes`, `plugins`, `package`, `pluginConfig`, `disabled`, `pullPolicy`, `forceDownload`, `integrity`).
|
|
125
|
+
- The **output contract** matches: same `app-config.dynamic-plugins.yaml`, same plugin directory layout, same `dynamic-plugin-config.hash` / `dynamic-plugin-image.hash` files.
|
|
126
|
+
- `{{inherit}}` semantics, OCI path auto-detection, registry fallback, integrity algorithms, lock-file behaviour are preserved.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Copyright Red Hat, Inc.
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
const fs = require('node:fs');
|
|
20
|
+
|
|
21
|
+
/* eslint-disable-next-line no-restricted-syntax */
|
|
22
|
+
const isLocal = fs.existsSync(path.resolve(__dirname, '../src'));
|
|
23
|
+
|
|
24
|
+
if (isLocal) {
|
|
25
|
+
require('@backstage/cli-node/config/nodeTransform.cjs');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { runCliModule } = require('@backstage/cli-node');
|
|
29
|
+
const cliModule = require(isLocal ? '../src/index' : '..').default;
|
|
30
|
+
const pkg = require('../package.json');
|
|
31
|
+
|
|
32
|
+
runCliModule({ module: cliModule, name: pkg.name, version: pkg.version });
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var fs = require('node:fs/promises');
|
|
4
|
+
var os = require('node:os');
|
|
5
|
+
var path = require('node:path');
|
|
6
|
+
var tar = require('tar');
|
|
7
|
+
var errors = require('./errors.cjs.js');
|
|
8
|
+
var log = require('./log.cjs.js');
|
|
9
|
+
var imageResolver = require('./image-resolver.cjs.js');
|
|
10
|
+
var types = require('./types.cjs.js');
|
|
11
|
+
var util = require('./util.cjs.js');
|
|
12
|
+
|
|
13
|
+
function _interopNamespaceCompat(e) {
|
|
14
|
+
if (e && typeof e === 'object' && 'default' in e) return e;
|
|
15
|
+
var n = Object.create(null);
|
|
16
|
+
if (e) {
|
|
17
|
+
Object.keys(e).forEach(function (k) {
|
|
18
|
+
if (k !== 'default') {
|
|
19
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
20
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
21
|
+
enumerable: true,
|
|
22
|
+
get: function () { return e[k]; }
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
n.default = e;
|
|
28
|
+
return Object.freeze(n);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
var fs__namespace = /*#__PURE__*/_interopNamespaceCompat(fs);
|
|
32
|
+
var os__namespace = /*#__PURE__*/_interopNamespaceCompat(os);
|
|
33
|
+
var path__namespace = /*#__PURE__*/_interopNamespaceCompat(path);
|
|
34
|
+
var tar__namespace = /*#__PURE__*/_interopNamespaceCompat(tar);
|
|
35
|
+
|
|
36
|
+
async function extractCatalogIndex(skopeo, image, mountDir, entitiesDir) {
|
|
37
|
+
log.log(`
|
|
38
|
+
======= Extracting catalog index from ${image}`);
|
|
39
|
+
const tempDir = path__namespace.join(mountDir, ".catalog-index-temp");
|
|
40
|
+
await fs__namespace.mkdir(tempDir, { recursive: true });
|
|
41
|
+
const tempDirAbs = path__namespace.resolve(tempDir);
|
|
42
|
+
await extractCatalogIndexLayers(skopeo, image, tempDirAbs);
|
|
43
|
+
const dpdy = path__namespace.join(tempDir, types.DPDY_FILENAME);
|
|
44
|
+
if (!await util.fileExists(dpdy)) {
|
|
45
|
+
throw new errors.InstallException(
|
|
46
|
+
`dynamic-plugins.default.yaml not found in ${image}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
log.log(" ==> Extracted dynamic-plugins.default.yaml");
|
|
50
|
+
for (const sub of [
|
|
51
|
+
"catalog-entities/extensions",
|
|
52
|
+
"catalog-entities/marketplace"
|
|
53
|
+
]) {
|
|
54
|
+
const src = path__namespace.join(tempDir, sub);
|
|
55
|
+
if (await util.fileExists(src)) {
|
|
56
|
+
await fs__namespace.mkdir(entitiesDir, { recursive: true });
|
|
57
|
+
const dst = path__namespace.join(entitiesDir, "catalog-entities");
|
|
58
|
+
await fs__namespace.rm(dst, { recursive: true, force: true });
|
|
59
|
+
await copyDir(src, dst);
|
|
60
|
+
log.log(` ==> Extracted catalog entities from ${sub}`);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return dpdy;
|
|
65
|
+
}
|
|
66
|
+
async function extractCatalogIndexLayers(skopeo, image, destDirAbs) {
|
|
67
|
+
const resolved = await imageResolver.resolveImage(skopeo, image);
|
|
68
|
+
const workDir = await fs__namespace.mkdtemp(
|
|
69
|
+
path__namespace.join(os__namespace.tmpdir(), "rhdh-catalog-index-")
|
|
70
|
+
);
|
|
71
|
+
try {
|
|
72
|
+
const url = resolved.startsWith(types.DOCKER_PROTO) ? resolved : `${types.DOCKER_PROTO}${resolved.replace(types.OCI_PROTO, "")}`;
|
|
73
|
+
const localDir = path__namespace.join(workDir, "idx");
|
|
74
|
+
log.log(" ==> Downloading catalog index image");
|
|
75
|
+
await skopeo.copy(url, `dir:${localDir}`);
|
|
76
|
+
const manifestPath = path__namespace.join(localDir, "manifest.json");
|
|
77
|
+
if (!await util.fileExists(manifestPath)) {
|
|
78
|
+
throw new errors.InstallException(
|
|
79
|
+
`manifest.json not found in catalog index image ${image}`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const manifest = JSON.parse(
|
|
83
|
+
await fs__namespace.readFile(manifestPath, "utf8")
|
|
84
|
+
);
|
|
85
|
+
const layers = manifest.layers ?? [];
|
|
86
|
+
let pending = null;
|
|
87
|
+
for (const layer of layers) {
|
|
88
|
+
if (pending) break;
|
|
89
|
+
const digest = layer.digest;
|
|
90
|
+
if (!digest) continue;
|
|
91
|
+
const [, fname] = digest.split(":");
|
|
92
|
+
if (!fname) continue;
|
|
93
|
+
const layerPath = path__namespace.join(localDir, fname);
|
|
94
|
+
if (!await util.fileExists(layerPath)) continue;
|
|
95
|
+
await tar__namespace.x({
|
|
96
|
+
file: layerPath,
|
|
97
|
+
cwd: destDirAbs,
|
|
98
|
+
preservePaths: false,
|
|
99
|
+
// The filter captures `pending` (a single-write latch) and runs
|
|
100
|
+
// synchronously inside the awaited tar.x call — iterations are
|
|
101
|
+
// serialised, so the closure-in-loop hazard the rule guards against
|
|
102
|
+
// does not apply.
|
|
103
|
+
// eslint-disable-next-line no-loop-func
|
|
104
|
+
filter: (filePath, entry) => {
|
|
105
|
+
if (pending) return false;
|
|
106
|
+
const stat = entry;
|
|
107
|
+
if (stat.size > types.MAX_ENTRY_SIZE) {
|
|
108
|
+
pending = new errors.InstallException(`Zip bomb detected in ${filePath}`);
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
if (stat.type === "SymbolicLink" || stat.type === "Link") {
|
|
112
|
+
const linkTarget = path__namespace.resolve(destDirAbs, stat.linkpath ?? "");
|
|
113
|
+
if (!util.isInside(linkTarget, destDirAbs)) return false;
|
|
114
|
+
}
|
|
115
|
+
const memberPath = path__namespace.resolve(destDirAbs, filePath);
|
|
116
|
+
if (!util.isInside(memberPath, destDirAbs)) return false;
|
|
117
|
+
return util.isAllowedEntryType(stat.type);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
if (pending) throw pending;
|
|
122
|
+
} finally {
|
|
123
|
+
await fs__namespace.rm(workDir, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function extractExtraCatalogIndex(skopeo, image, subdirectory, parentDir, previouslyUsedBy) {
|
|
127
|
+
if (!isSafeSubdirectoryName(subdirectory)) {
|
|
128
|
+
throw new errors.InstallException(
|
|
129
|
+
`Refusing to extract extra catalog index into unsafe subdirectory '${subdirectory}'`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
log.log(
|
|
133
|
+
`
|
|
134
|
+
======= Extracting extra catalog index '${subdirectory}' from ${image}`
|
|
135
|
+
);
|
|
136
|
+
if (previouslyUsedBy) {
|
|
137
|
+
log.log(
|
|
138
|
+
` ==> WARNING: Subdirectory '${subdirectory}' was already used by '${previouslyUsedBy}'. The previous extraction will be overwritten.`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const workDir = await fs__namespace.mkdtemp(
|
|
142
|
+
path__namespace.join(os__namespace.tmpdir(), "rhdh-extra-catalog-index-")
|
|
143
|
+
);
|
|
144
|
+
try {
|
|
145
|
+
const extractedDir = path__namespace.join(workDir, "extracted");
|
|
146
|
+
await fs__namespace.mkdir(extractedDir, { recursive: true });
|
|
147
|
+
await extractCatalogIndexLayers(skopeo, image, extractedDir);
|
|
148
|
+
const subdirParent = path__namespace.join(parentDir, subdirectory);
|
|
149
|
+
log.log(` ==> Extracting extensions catalog entities to ${subdirParent}`);
|
|
150
|
+
let sourceDir = null;
|
|
151
|
+
for (const sub of [
|
|
152
|
+
"catalog-entities/extensions",
|
|
153
|
+
"catalog-entities/marketplace"
|
|
154
|
+
]) {
|
|
155
|
+
const candidate = path__namespace.join(extractedDir, sub);
|
|
156
|
+
if (await util.fileExists(candidate)) {
|
|
157
|
+
sourceDir = candidate;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!sourceDir) {
|
|
162
|
+
log.log(
|
|
163
|
+
` ==> WARNING: Extra catalog index image ${image} does not have neither 'catalog-entities/extensions/' nor 'catalog-entities/marketplace/' directory`
|
|
164
|
+
);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
await fs__namespace.mkdir(subdirParent, { recursive: true });
|
|
168
|
+
const dst = path__namespace.join(subdirParent, "catalog-entities");
|
|
169
|
+
await fs__namespace.rm(dst, { recursive: true, force: true });
|
|
170
|
+
await copyDir(sourceDir, dst);
|
|
171
|
+
log.log(
|
|
172
|
+
` ==> Successfully extracted extensions catalog entities from extra index image to ${subdirParent}`
|
|
173
|
+
);
|
|
174
|
+
} finally {
|
|
175
|
+
await fs__namespace.rm(workDir, { recursive: true, force: true });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function imageRefToSubdirectory(imageRef) {
|
|
179
|
+
return imageRef.replaceAll(/[/:@]/g, "_");
|
|
180
|
+
}
|
|
181
|
+
function parseExtraCatalogIndexImages(raw) {
|
|
182
|
+
const out = [];
|
|
183
|
+
for (const rawEntry of raw.split(",")) {
|
|
184
|
+
const entry = rawEntry.trim();
|
|
185
|
+
if (!entry) continue;
|
|
186
|
+
let name;
|
|
187
|
+
let imageRef;
|
|
188
|
+
const eq = entry.indexOf("=");
|
|
189
|
+
if (eq === -1) {
|
|
190
|
+
imageRef = entry;
|
|
191
|
+
name = imageRefToSubdirectory(imageRef);
|
|
192
|
+
} else {
|
|
193
|
+
name = entry.slice(0, eq).trim();
|
|
194
|
+
imageRef = entry.slice(eq + 1).trim();
|
|
195
|
+
}
|
|
196
|
+
if (!imageRef) {
|
|
197
|
+
log.log(
|
|
198
|
+
`WARNING: Skipping EXTRA_CATALOG_INDEX_IMAGES entry with empty image reference: '${entry}'`
|
|
199
|
+
);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (!isSafeSubdirectoryName(name)) {
|
|
203
|
+
log.log(
|
|
204
|
+
String.raw`WARNING: Skipping EXTRA_CATALOG_INDEX_IMAGES entry with unsafe subdirectory name '${name}' in '${entry}'. Names must be non-empty and must not contain '/', '\\', or '..'.`
|
|
205
|
+
);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
out.push([name, imageRef]);
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
function isSafeSubdirectoryName(name) {
|
|
213
|
+
if (!name || name === "." || name === "..") return false;
|
|
214
|
+
return !/[/\\]/.test(name);
|
|
215
|
+
}
|
|
216
|
+
async function cleanupCatalogIndexTemp(mountDir) {
|
|
217
|
+
await fs__namespace.rm(path__namespace.join(mountDir, ".catalog-index-temp"), {
|
|
218
|
+
recursive: true,
|
|
219
|
+
force: true
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
async function copyDir(src, dst) {
|
|
223
|
+
await fs__namespace.mkdir(dst, { recursive: true });
|
|
224
|
+
const entries = await fs__namespace.readdir(src, { withFileTypes: true });
|
|
225
|
+
for (const entry of entries) {
|
|
226
|
+
const s = path__namespace.join(src, entry.name);
|
|
227
|
+
const d = path__namespace.join(dst, entry.name);
|
|
228
|
+
if (entry.isDirectory()) {
|
|
229
|
+
await copyDir(s, d);
|
|
230
|
+
} else if (entry.isFile()) {
|
|
231
|
+
await fs__namespace.copyFile(s, d);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
exports.cleanupCatalogIndexTemp = cleanupCatalogIndexTemp;
|
|
237
|
+
exports.extractCatalogIndex = extractCatalogIndex;
|
|
238
|
+
exports.extractCatalogIndexLayers = extractCatalogIndexLayers;
|
|
239
|
+
exports.extractExtraCatalogIndex = extractExtraCatalogIndex;
|
|
240
|
+
exports.imageRefToSubdirectory = imageRefToSubdirectory;
|
|
241
|
+
exports.parseExtraCatalogIndexImages = parseExtraCatalogIndexImages;
|
|
242
|
+
//# sourceMappingURL=catalog-index.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-index.cjs.js","sources":["../src/catalog-index.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 * as fs from 'node:fs/promises';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport * as tar from 'tar';\nimport { InstallException } from './errors';\nimport { log } from './log';\nimport { resolveImage } from './image-resolver';\nimport { type Skopeo } from './skopeo';\nimport {\n DOCKER_PROTO,\n DPDY_FILENAME,\n MAX_ENTRY_SIZE,\n OCI_PROTO,\n} from './types';\nimport { fileExists, isAllowedEntryType, isInside } from './util';\n\ntype OciManifest = {\n layers?: Array<{ digest: string }>;\n};\n\n/**\n * Extract the plugin catalog index OCI image (when `CATALOG_INDEX_IMAGE` is\n * set). Produces:\n * - `<mountDir>/.catalog-index-temp/dynamic-plugins.default.yaml`\n * - `<entitiesDir>/catalog-entities/` (if present in the image)\n *\n * Returns the absolute path to the extracted `dynamic-plugins.default.yaml`,\n * which the caller will substitute into `includes[]`.\n */\nexport async function extractCatalogIndex(\n skopeo: Skopeo,\n image: string,\n mountDir: string,\n entitiesDir: string,\n): Promise<string> {\n log(`\\n======= Extracting catalog index from ${image}`);\n const tempDir = path.join(mountDir, '.catalog-index-temp');\n await fs.mkdir(tempDir, { recursive: true });\n const tempDirAbs = path.resolve(tempDir);\n\n await extractCatalogIndexLayers(skopeo, image, tempDirAbs);\n\n const dpdy = path.join(tempDir, DPDY_FILENAME);\n if (!(await fileExists(dpdy))) {\n throw new InstallException(\n `dynamic-plugins.default.yaml not found in ${image}`,\n );\n }\n log('\\t==> Extracted dynamic-plugins.default.yaml');\n\n // Also surface catalog entities if present.\n for (const sub of [\n 'catalog-entities/extensions',\n 'catalog-entities/marketplace',\n ]) {\n const src = path.join(tempDir, sub);\n if (await fileExists(src)) {\n await fs.mkdir(entitiesDir, { recursive: true });\n const dst = path.join(entitiesDir, 'catalog-entities');\n await fs.rm(dst, { recursive: true, force: true });\n await copyDir(src, dst);\n log(`\\t==> Extracted catalog entities from ${sub}`);\n break;\n }\n }\n return dpdy;\n}\n\n/**\n * Pull an OCI image with `skopeo copy` and untar every layer into `destDirAbs`.\n * Shared by the primary `extractCatalogIndex` and the per-image\n * `extractExtraCatalogIndex` flows. Applies the same security filter as\n * `extractCatalogIndex` (per-entry size cap, path-traversal rejection,\n * link-target containment, allowed-type whitelist).\n */\nexport async function extractCatalogIndexLayers(\n skopeo: Skopeo,\n image: string,\n destDirAbs: string,\n): Promise<void> {\n const resolved = await resolveImage(skopeo, image);\n const workDir = await fs.mkdtemp(\n path.join(os.tmpdir(), 'rhdh-catalog-index-'),\n );\n try {\n const url = resolved.startsWith(DOCKER_PROTO)\n ? resolved\n : `${DOCKER_PROTO}${resolved.replace(OCI_PROTO, '')}`;\n const localDir = path.join(workDir, 'idx');\n log('\\t==> Downloading catalog index image');\n await skopeo.copy(url, `dir:${localDir}`);\n\n const manifestPath = path.join(localDir, 'manifest.json');\n if (!(await fileExists(manifestPath))) {\n throw new InstallException(\n `manifest.json not found in catalog index image ${image}`,\n );\n }\n\n const manifest = JSON.parse(\n await fs.readFile(manifestPath, 'utf8'),\n ) as OciManifest;\n const layers = manifest.layers ?? [];\n\n let pending: InstallException | null = null;\n for (const layer of layers) {\n if (pending) break;\n const digest = layer.digest;\n if (!digest) continue;\n const [, fname] = digest.split(':');\n if (!fname) continue;\n const layerPath = path.join(localDir, fname);\n if (!(await fileExists(layerPath))) continue;\n\n await tar.x({\n file: layerPath,\n cwd: destDirAbs,\n preservePaths: false,\n // The filter captures `pending` (a single-write latch) and runs\n // synchronously inside the awaited tar.x call — iterations are\n // serialised, so the closure-in-loop hazard the rule guards against\n // does not apply.\n // eslint-disable-next-line no-loop-func\n filter: (filePath, entry) => {\n if (pending) return false;\n const stat = entry as tar.ReadEntry;\n\n if (stat.size > MAX_ENTRY_SIZE) {\n pending = new InstallException(`Zip bomb detected in ${filePath}`);\n return false;\n }\n\n if (stat.type === 'SymbolicLink' || stat.type === 'Link') {\n const linkTarget = path.resolve(destDirAbs, stat.linkpath ?? '');\n if (!isInside(linkTarget, destDirAbs)) return false;\n }\n\n // Reject any entry that would resolve outside destDirAbs.\n const memberPath = path.resolve(destDirAbs, filePath);\n if (!isInside(memberPath, destDirAbs)) return false;\n\n return isAllowedEntryType(stat.type);\n },\n });\n }\n if (pending) throw pending;\n } finally {\n await fs.rm(workDir, { recursive: true, force: true });\n }\n}\n\n/**\n * Extract an extra catalog index image (driven by `EXTRA_CATALOG_INDEX_IMAGES`).\n * Unlike `extractCatalogIndex`, this does NOT require a\n * `dynamic-plugins.default.yaml` — extra images contribute catalog entities\n * for the Extensions UI only.\n *\n * Writes catalog entities to `<parentDir>/<subdirectory>/catalog-entities`,\n * overwriting any prior content at that path. When the source image carries\n * neither `catalog-entities/extensions/` nor `catalog-entities/marketplace/`,\n * a warning is logged and the function returns without throwing.\n *\n * `previouslyUsedBy` should be the image ref that previously mapped to this\n * subdirectory name in the same `EXTRA_CATALOG_INDEX_IMAGES` invocation;\n * pass `null` on first use. When non-null, an overwrite warning is logged\n * AFTER the extraction header (matches the Python fix-up commit ordering).\n */\nexport async function extractExtraCatalogIndex(\n skopeo: Skopeo,\n image: string,\n subdirectory: string,\n parentDir: string,\n previouslyUsedBy: string | null,\n): Promise<void> {\n if (!isSafeSubdirectoryName(subdirectory)) {\n throw new InstallException(\n `Refusing to extract extra catalog index into unsafe subdirectory '${subdirectory}'`,\n );\n }\n log(\n `\\n======= Extracting extra catalog index '${subdirectory}' from ${image}`,\n );\n if (previouslyUsedBy) {\n log(\n `\\t==> WARNING: Subdirectory '${subdirectory}' was already used by '${previouslyUsedBy}'. ` +\n `The previous extraction will be overwritten.`,\n );\n }\n\n const workDir = await fs.mkdtemp(\n path.join(os.tmpdir(), 'rhdh-extra-catalog-index-'),\n );\n try {\n const extractedDir = path.join(workDir, 'extracted');\n await fs.mkdir(extractedDir, { recursive: true });\n await extractCatalogIndexLayers(skopeo, image, extractedDir);\n\n const subdirParent = path.join(parentDir, subdirectory);\n log(`\\t==> Extracting extensions catalog entities to ${subdirParent}`);\n\n let sourceDir: string | null = null;\n for (const sub of [\n 'catalog-entities/extensions',\n 'catalog-entities/marketplace',\n ]) {\n const candidate = path.join(extractedDir, sub);\n if (await fileExists(candidate)) {\n sourceDir = candidate;\n break;\n }\n }\n\n if (!sourceDir) {\n log(\n `\\t==> WARNING: Extra catalog index image ${image} does not have neither ` +\n `'catalog-entities/extensions/' nor 'catalog-entities/marketplace/' directory`,\n );\n return;\n }\n\n await fs.mkdir(subdirParent, { recursive: true });\n const dst = path.join(subdirParent, 'catalog-entities');\n await fs.rm(dst, { recursive: true, force: true });\n await copyDir(sourceDir, dst);\n log(\n `\\t==> Successfully extracted extensions catalog entities from extra index image to ${subdirParent}`,\n );\n } finally {\n await fs.rm(workDir, { recursive: true, force: true });\n }\n}\n\n/**\n * Convert an OCI image reference to a filesystem-safe subdirectory name by\n * replacing `/`, `:`, and `@` with `_`. Matches the Python\n * `image_ref_to_subdirectory` helper so the on-disk layout is identical\n * between the two implementations.\n */\nexport function imageRefToSubdirectory(imageRef: string): string {\n return imageRef.replaceAll(/[/:@]/g, '_');\n}\n\n/**\n * Parse the `EXTRA_CATALOG_INDEX_IMAGES` env var. Each comma-separated entry\n * is either a plain image reference (subdirectory auto-derived via\n * `imageRefToSubdirectory`) or `<name>=<image_ref>` (explicit subdirectory\n * name). Empty entries and empty image_refs are skipped with a warning —\n * the caller still consumes the rest of the list.\n */\nexport function parseExtraCatalogIndexImages(\n raw: string,\n): Array<[name: string, imageRef: string]> {\n const out: Array<[string, string]> = [];\n for (const rawEntry of raw.split(',')) {\n const entry = rawEntry.trim();\n if (!entry) continue;\n let name: string;\n let imageRef: string;\n const eq = entry.indexOf('=');\n if (eq === -1) {\n imageRef = entry;\n name = imageRefToSubdirectory(imageRef);\n } else {\n name = entry.slice(0, eq).trim();\n imageRef = entry.slice(eq + 1).trim();\n }\n if (!imageRef) {\n log(\n `WARNING: Skipping EXTRA_CATALOG_INDEX_IMAGES entry with empty image reference: '${entry}'`,\n );\n continue;\n }\n if (!isSafeSubdirectoryName(name)) {\n log(\n String.raw`WARNING: Skipping EXTRA_CATALOG_INDEX_IMAGES entry with unsafe subdirectory name '${name}' in '${entry}'. Names must be non-empty and must not contain '/', '\\\\', or '..'.`,\n );\n continue;\n }\n out.push([name, imageRef]);\n }\n return out;\n}\n\n/**\n * Reject subdirectory names that are empty or could escape `<parentDir>` once\n * passed to `path.join` (path separators or `..` segments). Mirrors the\n * defensive check applied to plugin paths during tar extraction.\n */\nfunction isSafeSubdirectoryName(name: string): boolean {\n if (!name || name === '.' || name === '..') return false;\n return !/[/\\\\]/.test(name);\n}\n\nexport async function cleanupCatalogIndexTemp(mountDir: string): Promise<void> {\n await fs.rm(path.join(mountDir, '.catalog-index-temp'), {\n recursive: true,\n force: true,\n });\n}\n\nasync function copyDir(src: string, dst: string): Promise<void> {\n await fs.mkdir(dst, { recursive: true });\n const entries = await fs.readdir(src, { withFileTypes: true });\n for (const entry of entries) {\n const s = path.join(src, entry.name);\n const d = path.join(dst, entry.name);\n if (entry.isDirectory()) {\n await copyDir(s, d);\n } else if (entry.isFile()) {\n await fs.copyFile(s, d);\n }\n }\n}\n"],"names":["log","path","fs","DPDY_FILENAME","fileExists","InstallException","resolveImage","os","DOCKER_PROTO","OCI_PROTO","tar","MAX_ENTRY_SIZE","isInside","isAllowedEntryType"],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var installer = require('./installer.cjs.js');
|
|
6
|
+
|
|
7
|
+
var command = async ({ args, info }) => {
|
|
8
|
+
await installer.main(args, info.usage);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
exports.default = command;
|
|
12
|
+
//# sourceMappingURL=command.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command.cjs.js","sources":["../src/command.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 type { CliCommandContext } from '@backstage/cli-node';\nimport { main } from './installer';\n\nexport default async ({ args, info }: CliCommandContext): Promise<void> => {\n await main(args, info.usage);\n};\n"],"names":["main"],"mappings":";;;;;;AAkBA,cAAe,OAAO,EAAE,IAAA,EAAM,IAAA,EAAK,KAAwC;AACzE,EAAA,MAAMA,cAAA,CAAK,IAAA,EAAM,IAAA,CAAK,KAAK,CAAA;AAC7B,CAAA;;;;"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var os = require('node:os');
|
|
4
|
+
|
|
5
|
+
function _interopNamespaceCompat(e) {
|
|
6
|
+
if (e && typeof e === 'object' && 'default' in e) return e;
|
|
7
|
+
var n = Object.create(null);
|
|
8
|
+
if (e) {
|
|
9
|
+
Object.keys(e).forEach(function (k) {
|
|
10
|
+
if (k !== 'default') {
|
|
11
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
12
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function () { return e[k]; }
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
n.default = e;
|
|
20
|
+
return Object.freeze(n);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
var os__namespace = /*#__PURE__*/_interopNamespaceCompat(os);
|
|
24
|
+
|
|
25
|
+
class Semaphore {
|
|
26
|
+
available;
|
|
27
|
+
queue = [];
|
|
28
|
+
constructor(max) {
|
|
29
|
+
if (max < 1) throw new RangeError(`Semaphore max must be >= 1, got ${max}`);
|
|
30
|
+
this.available = max;
|
|
31
|
+
}
|
|
32
|
+
async acquire() {
|
|
33
|
+
if (this.available > 0) {
|
|
34
|
+
this.available--;
|
|
35
|
+
return void 0;
|
|
36
|
+
}
|
|
37
|
+
return new Promise((resolve) => this.queue.push(resolve));
|
|
38
|
+
}
|
|
39
|
+
release() {
|
|
40
|
+
const next = this.queue.shift();
|
|
41
|
+
if (next) next();
|
|
42
|
+
else this.available++;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function mapConcurrent(items, limit, fn) {
|
|
46
|
+
const sem = new Semaphore(Math.max(1, limit));
|
|
47
|
+
return Promise.all(
|
|
48
|
+
items.map(async (item) => {
|
|
49
|
+
await sem.acquire();
|
|
50
|
+
try {
|
|
51
|
+
return { ok: true, value: await fn(item), item };
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return { ok: false, error: err, item };
|
|
54
|
+
} finally {
|
|
55
|
+
sem.release();
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const MAX_OCI_WORKERS = 6;
|
|
61
|
+
const MAX_NPM_WORKERS = 3;
|
|
62
|
+
function getWorkers() {
|
|
63
|
+
return resolveWorkers(process.env.DYNAMIC_PLUGINS_WORKERS, MAX_OCI_WORKERS);
|
|
64
|
+
}
|
|
65
|
+
function getNpmWorkers() {
|
|
66
|
+
return resolveWorkers(
|
|
67
|
+
process.env.DYNAMIC_PLUGINS_NPM_WORKERS,
|
|
68
|
+
MAX_NPM_WORKERS
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
function resolveWorkers(rawEnv, cap) {
|
|
72
|
+
const env = rawEnv ?? "auto";
|
|
73
|
+
if (env !== "auto") {
|
|
74
|
+
const n = Number.parseInt(env, 10);
|
|
75
|
+
if (!Number.isFinite(n) || n < 1) return 1;
|
|
76
|
+
return n;
|
|
77
|
+
}
|
|
78
|
+
const cpus = typeof os__namespace.availableParallelism === "function" ? os__namespace.availableParallelism() : os__namespace.cpus().length;
|
|
79
|
+
return Math.max(1, Math.min(Math.floor(cpus / 2), cap));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
exports.Semaphore = Semaphore;
|
|
83
|
+
exports.getNpmWorkers = getNpmWorkers;
|
|
84
|
+
exports.getWorkers = getWorkers;
|
|
85
|
+
exports.mapConcurrent = mapConcurrent;
|
|
86
|
+
//# sourceMappingURL=concurrency.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"concurrency.cjs.js","sources":["../src/concurrency.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 * as os from 'node:os';\n\n/**\n * Minimal semaphore for bounding concurrent async work.\n * Matches the Python `ThreadPoolExecutor(max_workers=N)` worker model from\n * install-dynamic-plugins-fast.py — single-threaded JS means no lock needed\n * on the counter itself.\n */\nexport class Semaphore {\n private available: number;\n private readonly queue: Array<() => void> = [];\n\n constructor(max: number) {\n if (max < 1) throw new RangeError(`Semaphore max must be >= 1, got ${max}`);\n this.available = max;\n }\n\n async acquire(): Promise<void> {\n if (this.available > 0) {\n this.available--;\n return undefined;\n }\n return new Promise<void>(resolve => this.queue.push(resolve));\n }\n\n release(): void {\n const next = this.queue.shift();\n if (next) next();\n else this.available++;\n }\n}\n\nexport type Outcome<T, Item> =\n | { ok: true; value: T; item: Item }\n | { ok: false; error: Error; item: Item };\n\n/**\n * Run `fn` over `items` with at most `limit` concurrent executions.\n * Returns every outcome — errors are captured, not thrown, so one failure\n * does not cancel the others. Mirrors the behaviour of fast.py's parallel install loop.\n */\nexport async function mapConcurrent<Item, T>(\n items: readonly Item[],\n limit: number,\n fn: (item: Item) => Promise<T>,\n): Promise<Array<Outcome<T, Item>>> {\n const sem = new Semaphore(Math.max(1, limit));\n return Promise.all(\n items.map(async item => {\n await sem.acquire();\n try {\n return { ok: true as const, value: await fn(item), item };\n } catch (err) {\n return { ok: false as const, error: err as Error, item };\n } finally {\n sem.release();\n }\n }),\n );\n}\n\n/** Conservative upper bound for parallel `skopeo` calls. */\nconst MAX_OCI_WORKERS = 6;\n\n/**\n * Lower cap for NPM installs because `npm pack` hits the public registry\n * and shares a single CLI cache (`~/.npm/_cacache`) — excessive concurrency\n * triggers throttling and cache contention without a wall-clock benefit.\n */\nconst MAX_NPM_WORKERS = 3;\n\n/**\n * Worker count selection, honouring `DYNAMIC_PLUGINS_WORKERS` env and cgroup\n * CPU limits (via `availableParallelism`). Conservative default for OpenShift\n * init containers: half of available CPUs, capped at `MAX_OCI_WORKERS`.\n */\nexport function getWorkers(): number {\n return resolveWorkers(process.env.DYNAMIC_PLUGINS_WORKERS, MAX_OCI_WORKERS);\n}\n\n/**\n * Worker count for concurrent NPM installs. Override via\n * `DYNAMIC_PLUGINS_NPM_WORKERS` (set to `1` to restore the original\n * sequential behaviour).\n */\nexport function getNpmWorkers(): number {\n return resolveWorkers(\n process.env.DYNAMIC_PLUGINS_NPM_WORKERS,\n MAX_NPM_WORKERS,\n );\n}\n\nfunction resolveWorkers(rawEnv: string | undefined, cap: number): number {\n const env = rawEnv ?? 'auto';\n if (env !== 'auto') {\n const n = Number.parseInt(env, 10);\n if (!Number.isFinite(n) || n < 1) return 1;\n return n;\n }\n const cpus =\n typeof os.availableParallelism === 'function'\n ? os.availableParallelism()\n : os.cpus().length;\n return Math.max(1, Math.min(Math.floor(cpus / 2), cap));\n}\n"],"names":["os"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAuBO,MAAM,SAAA,CAAU;AAAA,EACb,SAAA;AAAA,EACS,QAA2B,EAAC;AAAA,EAE7C,YAAY,GAAA,EAAa;AACvB,IAAA,IAAI,MAAM,CAAA,EAAG,MAAM,IAAI,UAAA,CAAW,CAAA,gCAAA,EAAmC,GAAG,CAAA,CAAE,CAAA;AAC1E,IAAA,IAAA,CAAK,SAAA,GAAY,GAAA;AAAA,EACnB;AAAA,EAEA,MAAM,OAAA,GAAyB;AAC7B,IAAA,IAAI,IAAA,CAAK,YAAY,CAAA,EAAG;AACtB,MAAA,IAAA,CAAK,SAAA,EAAA;AACL,MAAA,OAAO,MAAA;AAAA,IACT;AACA,IAAA,OAAO,IAAI,OAAA,CAAc,CAAA,OAAA,KAAW,KAAK,KAAA,CAAM,IAAA,CAAK,OAAO,CAAC,CAAA;AAAA,EAC9D;AAAA,EAEA,OAAA,GAAgB;AACd,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,KAAA,EAAM;AAC9B,IAAA,IAAI,MAAM,IAAA,EAAK;AAAA,SACV,IAAA,CAAK,SAAA,EAAA;AAAA,EACZ;AACF;AAWA,eAAsB,aAAA,CACpB,KAAA,EACA,KAAA,EACA,EAAA,EACkC;AAClC,EAAA,MAAM,MAAM,IAAI,SAAA,CAAU,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,CAAC,CAAA;AAC5C,EAAA,OAAO,OAAA,CAAQ,GAAA;AAAA,IACb,KAAA,CAAM,GAAA,CAAI,OAAM,IAAA,KAAQ;AACtB,MAAA,MAAM,IAAI,OAAA,EAAQ;AAClB,MAAA,IAAI;AACF,QAAA,OAAO,EAAE,IAAI,IAAA,EAAe,KAAA,EAAO,MAAM,EAAA,CAAG,IAAI,GAAG,IAAA,EAAK;AAAA,MAC1D,SAAS,GAAA,EAAK;AACZ,QAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAgB,KAAA,EAAO,KAAc,IAAA,EAAK;AAAA,MACzD,CAAA,SAAE;AACA,QAAA,GAAA,CAAI,OAAA,EAAQ;AAAA,MACd;AAAA,IACF,CAAC;AAAA,GACH;AACF;AAGA,MAAM,eAAA,GAAkB,CAAA;AAOxB,MAAM,eAAA,GAAkB,CAAA;AAOjB,SAAS,UAAA,GAAqB;AACnC,EAAA,OAAO,cAAA,CAAe,OAAA,CAAQ,GAAA,CAAI,uBAAA,EAAyB,eAAe,CAAA;AAC5E;AAOO,SAAS,aAAA,GAAwB;AACtC,EAAA,OAAO,cAAA;AAAA,IACL,QAAQ,GAAA,CAAI,2BAAA;AAAA,IACZ;AAAA,GACF;AACF;AAEA,SAAS,cAAA,CAAe,QAA4B,GAAA,EAAqB;AACvE,EAAA,MAAM,MAAM,MAAA,IAAU,MAAA;AACtB,EAAA,IAAI,QAAQ,MAAA,EAAQ;AAClB,IAAA,MAAM,CAAA,GAAI,MAAA,CAAO,QAAA,CAAS,GAAA,EAAK,EAAE,CAAA;AACjC,IAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,CAAC,CAAA,IAAK,CAAA,GAAI,GAAG,OAAO,CAAA;AACzC,IAAA,OAAO,CAAA;AAAA,EACT;AACA,EAAA,MAAM,IAAA,GACJ,OAAOA,aAAA,CAAG,oBAAA,KAAyB,UAAA,GAC/BA,cAAG,oBAAA,EAAqB,GACxBA,aAAA,CAAG,IAAA,EAAK,CAAE,MAAA;AAChB,EAAA,OAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,KAAA,CAAM,IAAA,GAAO,CAAC,CAAA,EAAG,GAAG,CAAC,CAAA;AACxD;;;;;;;"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.cjs.js","sources":["../src/errors.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\nexport class InstallException extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'InstallException';\n }\n}\n"],"names":[],"mappings":";;AAgBO,MAAM,yBAAyB,KAAA,CAAM;AAAA,EAC1C,YAAY,OAAA,EAAiB;AAC3B,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,kBAAA;AAAA,EACd;AACF;;;;"}
|