@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
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var node_fs = require('node:fs');
|
|
4
|
+
var fs = require('node:fs/promises');
|
|
5
|
+
var os = require('node:os');
|
|
6
|
+
var path = require('node:path');
|
|
7
|
+
var cleye = require('cleye');
|
|
8
|
+
var yaml = require('yaml');
|
|
9
|
+
var catalogIndex = require('./catalog-index.cjs.js');
|
|
10
|
+
var concurrency = require('./concurrency.cjs.js');
|
|
11
|
+
var errors = require('./errors.cjs.js');
|
|
12
|
+
var imageCache = require('./image-cache.cjs.js');
|
|
13
|
+
var installerNpm = require('./installer-npm.cjs.js');
|
|
14
|
+
var installerOci = require('./installer-oci.cjs.js');
|
|
15
|
+
var lockFile = require('./lock-file.cjs.js');
|
|
16
|
+
var log = require('./log.cjs.js');
|
|
17
|
+
var merger = require('./merger.cjs.js');
|
|
18
|
+
var pluginHash = require('./plugin-hash.cjs.js');
|
|
19
|
+
var skopeo = require('./skopeo.cjs.js');
|
|
20
|
+
var types = require('./types.cjs.js');
|
|
21
|
+
var util = require('./util.cjs.js');
|
|
22
|
+
|
|
23
|
+
function _interopNamespaceCompat(e) {
|
|
24
|
+
if (e && typeof e === 'object' && 'default' in e) return e;
|
|
25
|
+
var n = Object.create(null);
|
|
26
|
+
if (e) {
|
|
27
|
+
Object.keys(e).forEach(function (k) {
|
|
28
|
+
if (k !== 'default') {
|
|
29
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
30
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
31
|
+
enumerable: true,
|
|
32
|
+
get: function () { return e[k]; }
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
n.default = e;
|
|
38
|
+
return Object.freeze(n);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
var fs__namespace = /*#__PURE__*/_interopNamespaceCompat(fs);
|
|
42
|
+
var os__namespace = /*#__PURE__*/_interopNamespaceCompat(os);
|
|
43
|
+
var path__namespace = /*#__PURE__*/_interopNamespaceCompat(path);
|
|
44
|
+
|
|
45
|
+
const CONFIG_FILE = "dynamic-plugins.yaml";
|
|
46
|
+
async function main(args = process.argv.slice(2), programName = "install-dynamic-plugins") {
|
|
47
|
+
const argv = cleye.cli(
|
|
48
|
+
{
|
|
49
|
+
name: programName,
|
|
50
|
+
parameters: ["<dynamic-plugins-root>"],
|
|
51
|
+
help: {
|
|
52
|
+
description: "Install RHDH dynamic plugins listed in dynamic-plugins.yaml into the given directory."
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
void 0,
|
|
56
|
+
args
|
|
57
|
+
);
|
|
58
|
+
const root = path__namespace.resolve(argv._.dynamicPluginsRoot);
|
|
59
|
+
const lockPath = path__namespace.join(root, types.LOCK_FILENAME);
|
|
60
|
+
lockFile.registerLockCleanup(lockPath);
|
|
61
|
+
await fs__namespace.mkdir(root, { recursive: true });
|
|
62
|
+
await lockFile.createLock(lockPath);
|
|
63
|
+
let exitCode = 0;
|
|
64
|
+
try {
|
|
65
|
+
exitCode = await runInstaller(root);
|
|
66
|
+
} finally {
|
|
67
|
+
await catalogIndex.cleanupCatalogIndexTemp(root).catch(() => void 0);
|
|
68
|
+
await lockFile.removeLock(lockPath).catch(() => void 0);
|
|
69
|
+
}
|
|
70
|
+
process.exit(exitCode);
|
|
71
|
+
}
|
|
72
|
+
async function runInstaller(root) {
|
|
73
|
+
const skopeo$1 = new skopeo.Skopeo();
|
|
74
|
+
const workers = concurrency.getWorkers();
|
|
75
|
+
log.log(`======= Workers: ${workers} (CPUs: ${os__namespace.cpus().length})`);
|
|
76
|
+
const configFileAbs = path__namespace.resolve(CONFIG_FILE);
|
|
77
|
+
const configDir = path__namespace.dirname(configFileAbs);
|
|
78
|
+
const globalConfigFile = path__namespace.join(root, types.GLOBAL_CONFIG_FILENAME);
|
|
79
|
+
log.log(`======= Config file: ${configFileAbs}`);
|
|
80
|
+
const entitiesDir = process.env.CATALOG_ENTITIES_EXTRACT_DIR ?? path__namespace.join(os__namespace.tmpdir(), "extensions");
|
|
81
|
+
const catalogDpdy = await maybeExtractCatalogIndex(skopeo$1, root, entitiesDir);
|
|
82
|
+
await maybeExtractExtraCatalogIndexes(skopeo$1, entitiesDir);
|
|
83
|
+
const content = await loadDynamicPluginsConfig(
|
|
84
|
+
configFileAbs,
|
|
85
|
+
globalConfigFile
|
|
86
|
+
);
|
|
87
|
+
if (!content) return 0;
|
|
88
|
+
const imageCache$1 = new imageCache.OciImageCache(
|
|
89
|
+
skopeo$1,
|
|
90
|
+
await fs__namespace.mkdtemp(path__namespace.join(os__namespace.tmpdir(), "rhdh-oci-cache-"))
|
|
91
|
+
);
|
|
92
|
+
const allPlugins = await loadAllPlugins(
|
|
93
|
+
content,
|
|
94
|
+
configFileAbs,
|
|
95
|
+
configDir,
|
|
96
|
+
catalogDpdy,
|
|
97
|
+
imageCache$1
|
|
98
|
+
);
|
|
99
|
+
const installed = await readInstalledPluginHashes(root);
|
|
100
|
+
const globalConfig = {
|
|
101
|
+
dynamicPlugins: { rootDirectory: "dynamic-plugins-root" }
|
|
102
|
+
};
|
|
103
|
+
const { oci, npm, skipped } = categorize(allPlugins);
|
|
104
|
+
handleSkippedLocals(skipped, globalConfig);
|
|
105
|
+
const skipIntegrity = (process.env.SKIP_INTEGRITY_CHECK ?? "").toLowerCase() === "true";
|
|
106
|
+
const errors = [];
|
|
107
|
+
await installOci(
|
|
108
|
+
oci,
|
|
109
|
+
root,
|
|
110
|
+
imageCache$1,
|
|
111
|
+
installed,
|
|
112
|
+
workers,
|
|
113
|
+
globalConfig,
|
|
114
|
+
errors
|
|
115
|
+
);
|
|
116
|
+
await installNpm(npm, root, skipIntegrity, installed, globalConfig, errors);
|
|
117
|
+
return finalizeInstall(
|
|
118
|
+
errors,
|
|
119
|
+
globalConfigFile,
|
|
120
|
+
globalConfig,
|
|
121
|
+
root,
|
|
122
|
+
installed
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
async function maybeExtractCatalogIndex(skopeo, root, entitiesDir) {
|
|
126
|
+
const catalogImage = process.env.CATALOG_INDEX_IMAGE ?? "";
|
|
127
|
+
if (!catalogImage) return null;
|
|
128
|
+
return catalogIndex.extractCatalogIndex(skopeo, catalogImage, root, entitiesDir);
|
|
129
|
+
}
|
|
130
|
+
async function maybeExtractExtraCatalogIndexes(skopeo, entitiesDir) {
|
|
131
|
+
const raw = process.env.EXTRA_CATALOG_INDEX_IMAGES ?? "";
|
|
132
|
+
if (!raw) return;
|
|
133
|
+
const extraParent = path__namespace.join(entitiesDir, "extra");
|
|
134
|
+
const seen = /* @__PURE__ */ new Map();
|
|
135
|
+
for (const [name, imageRef] of catalogIndex.parseExtraCatalogIndexImages(raw)) {
|
|
136
|
+
const prev = seen.get(name) ?? null;
|
|
137
|
+
seen.set(name, imageRef);
|
|
138
|
+
await catalogIndex.extractExtraCatalogIndex(skopeo, imageRef, name, extraParent, prev);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function loadDynamicPluginsConfig(configFileAbs, globalConfigFile) {
|
|
142
|
+
if (!await util.fileExists(configFileAbs)) {
|
|
143
|
+
log.log(`No ${CONFIG_FILE} found at ${configFileAbs}. Skipping.`);
|
|
144
|
+
await fs__namespace.writeFile(globalConfigFile, "");
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
const rawContent = await fs__namespace.readFile(configFileAbs, "utf8");
|
|
148
|
+
const content = yaml.parse(rawContent);
|
|
149
|
+
if (!content) {
|
|
150
|
+
log.log(`${configFileAbs} is empty. Skipping.`);
|
|
151
|
+
await fs__namespace.writeFile(globalConfigFile, "");
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
if (!util.isPlainObject(content)) {
|
|
155
|
+
throw new errors.InstallException(
|
|
156
|
+
`${configFileAbs} must contain a YAML mapping, got ${typeof content}`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return content;
|
|
160
|
+
}
|
|
161
|
+
async function loadAllPlugins(content, configFileAbs, configDir, catalogDpdy, imageCache) {
|
|
162
|
+
const allPlugins = {};
|
|
163
|
+
const includes = resolveIncludes(
|
|
164
|
+
content.includes ?? [],
|
|
165
|
+
configDir,
|
|
166
|
+
catalogDpdy
|
|
167
|
+
);
|
|
168
|
+
const includeLists = [];
|
|
169
|
+
for (const inc of includes) {
|
|
170
|
+
if (!await util.fileExists(inc)) {
|
|
171
|
+
log.log(`WARNING: include file ${inc} not found, skipping`);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
log.log(`
|
|
175
|
+
======= Including plugins from ${inc}`);
|
|
176
|
+
const parsed = yaml.parse(
|
|
177
|
+
await fs__namespace.readFile(inc, "utf8")
|
|
178
|
+
);
|
|
179
|
+
if (parsed && !util.isPlainObject(parsed)) {
|
|
180
|
+
throw new errors.InstallException(`${inc} must contain a mapping`);
|
|
181
|
+
}
|
|
182
|
+
const plugins = parsed?.plugins ?? [];
|
|
183
|
+
if (!Array.isArray(plugins)) {
|
|
184
|
+
throw new errors.InstallException(
|
|
185
|
+
`${inc} must contain a 'plugins' list (got ${typeof plugins})`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
includeLists.push([inc, plugins]);
|
|
189
|
+
}
|
|
190
|
+
const mainPlugins = content.plugins ?? [];
|
|
191
|
+
const disabledRegistries = merger.preMergeOciDisabledState(
|
|
192
|
+
includeLists,
|
|
193
|
+
mainPlugins,
|
|
194
|
+
configFileAbs
|
|
195
|
+
);
|
|
196
|
+
for (const [inc, plugins] of includeLists) {
|
|
197
|
+
for (const plugin of merger.filterDisabledOciPlugins(
|
|
198
|
+
plugins,
|
|
199
|
+
disabledRegistries
|
|
200
|
+
)) {
|
|
201
|
+
await merger.mergePlugin(
|
|
202
|
+
plugin,
|
|
203
|
+
allPlugins,
|
|
204
|
+
inc,
|
|
205
|
+
/* level */
|
|
206
|
+
0,
|
|
207
|
+
imageCache
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
for (const plugin of merger.filterDisabledOciPlugins(
|
|
212
|
+
mainPlugins,
|
|
213
|
+
disabledRegistries
|
|
214
|
+
)) {
|
|
215
|
+
await merger.mergePlugin(
|
|
216
|
+
plugin,
|
|
217
|
+
allPlugins,
|
|
218
|
+
configFileAbs,
|
|
219
|
+
/* level */
|
|
220
|
+
1,
|
|
221
|
+
imageCache
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
for (const p of Object.values(allPlugins)) {
|
|
225
|
+
p.plugin_hash = pluginHash.computePluginHash(p);
|
|
226
|
+
}
|
|
227
|
+
return allPlugins;
|
|
228
|
+
}
|
|
229
|
+
function resolveIncludes(rawIncludes, configDir, catalogDpdy) {
|
|
230
|
+
const includes = rawIncludes.map(
|
|
231
|
+
(inc) => path__namespace.isAbsolute(inc) ? inc : path__namespace.resolve(configDir, inc)
|
|
232
|
+
);
|
|
233
|
+
if (catalogDpdy) {
|
|
234
|
+
const idx = includes.findIndex((inc) => path__namespace.basename(inc) === types.DPDY_FILENAME);
|
|
235
|
+
if (idx !== -1) includes[idx] = catalogDpdy;
|
|
236
|
+
}
|
|
237
|
+
return includes;
|
|
238
|
+
}
|
|
239
|
+
async function finalizeInstall(errors, globalConfigFile, globalConfig, root, installed) {
|
|
240
|
+
if (errors.length > 0) {
|
|
241
|
+
log.log(`
|
|
242
|
+
======= ${errors.length} plugin(s) failed:`);
|
|
243
|
+
for (const err of errors) log.log(` - ${err}`);
|
|
244
|
+
log.log(
|
|
245
|
+
`
|
|
246
|
+
======= Skipping ${types.GLOBAL_CONFIG_FILENAME} write and cleanup because of install failures. Fix the errors above and re-run; the previous successful state is preserved.`
|
|
247
|
+
);
|
|
248
|
+
return 1;
|
|
249
|
+
}
|
|
250
|
+
await fs__namespace.writeFile(globalConfigFile, yaml.stringify(globalConfig));
|
|
251
|
+
await cleanupRemoved(root, installed);
|
|
252
|
+
log.log("\n======= All plugins installed successfully");
|
|
253
|
+
return 0;
|
|
254
|
+
}
|
|
255
|
+
function categorize(allPlugins) {
|
|
256
|
+
const oci = [];
|
|
257
|
+
const npm = [];
|
|
258
|
+
const skipped = [];
|
|
259
|
+
for (const plugin of Object.values(allPlugins)) {
|
|
260
|
+
if (plugin.disabled) {
|
|
261
|
+
log.log(`
|
|
262
|
+
======= Skipping disabled plugin ${plugin.package}`);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (plugin.package.startsWith(types.OCI_PROTO)) {
|
|
266
|
+
oci.push(plugin);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (plugin.package.startsWith("./")) {
|
|
270
|
+
const localPath = path__namespace.join(process.cwd(), plugin.package.slice(2));
|
|
271
|
+
if (node_fs.existsSync(localPath)) npm.push(plugin);
|
|
272
|
+
else skipped.push(plugin);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
npm.push(plugin);
|
|
276
|
+
}
|
|
277
|
+
return { oci, npm, skipped };
|
|
278
|
+
}
|
|
279
|
+
function handleSkippedLocals(skipped, globalConfig) {
|
|
280
|
+
if (skipped.length === 0) return;
|
|
281
|
+
log.log(
|
|
282
|
+
`
|
|
283
|
+
======= Skipping ${skipped.length} local plugins (directories not found)`
|
|
284
|
+
);
|
|
285
|
+
for (const plugin of skipped) {
|
|
286
|
+
const abs = path__namespace.join(process.cwd(), plugin.package.slice(2));
|
|
287
|
+
log.log(` ==> ${plugin.package} (not found at ${abs})`);
|
|
288
|
+
if (util.isPlainObject(plugin.pluginConfig)) {
|
|
289
|
+
merger.deepMerge(plugin.pluginConfig, globalConfig);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async function installOci(plugins, root, imageCache, installed, workers, globalConfig, errors) {
|
|
294
|
+
await runInstallPipeline({
|
|
295
|
+
plugins,
|
|
296
|
+
workers,
|
|
297
|
+
label: "OCI",
|
|
298
|
+
installFn: (plugin) => installerOci.installOciPlugin(plugin, root, imageCache, installed),
|
|
299
|
+
installed,
|
|
300
|
+
globalConfig,
|
|
301
|
+
errors
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
async function installNpm(plugins, root, skipIntegrity, installed, globalConfig, errors) {
|
|
305
|
+
await runInstallPipeline({
|
|
306
|
+
plugins,
|
|
307
|
+
workers: concurrency.getNpmWorkers(),
|
|
308
|
+
label: "NPM",
|
|
309
|
+
installFn: (plugin) => installerNpm.installNpmPlugin(plugin, root, skipIntegrity, installed),
|
|
310
|
+
installed,
|
|
311
|
+
globalConfig,
|
|
312
|
+
errors
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
async function runInstallPipeline(args) {
|
|
316
|
+
const {
|
|
317
|
+
plugins,
|
|
318
|
+
workers,
|
|
319
|
+
label,
|
|
320
|
+
installFn,
|
|
321
|
+
installed,
|
|
322
|
+
globalConfig,
|
|
323
|
+
errors
|
|
324
|
+
} = args;
|
|
325
|
+
if (plugins.length === 0) return;
|
|
326
|
+
const needsWork = partitionDefinitelyNoOp(
|
|
327
|
+
plugins,
|
|
328
|
+
installed,
|
|
329
|
+
globalConfig,
|
|
330
|
+
errors
|
|
331
|
+
);
|
|
332
|
+
if (needsWork.length === 0) return;
|
|
333
|
+
const workerSuffix = workers === 1 ? "" : "s";
|
|
334
|
+
log.log(
|
|
335
|
+
`
|
|
336
|
+
======= Installing ${needsWork.length} ${label} plugin(s) (${workers} worker${workerSuffix})`
|
|
337
|
+
);
|
|
338
|
+
const results = await concurrency.mapConcurrent(needsWork, workers, async (plugin) => {
|
|
339
|
+
log.log(`
|
|
340
|
+
======= Installing ${label} plugin ${plugin.package}`);
|
|
341
|
+
return installFn(plugin);
|
|
342
|
+
});
|
|
343
|
+
applyInstallOutcomes(results, globalConfig, errors);
|
|
344
|
+
}
|
|
345
|
+
function partitionDefinitelyNoOp(plugins, installed, globalConfig, errors) {
|
|
346
|
+
const needsWork = [];
|
|
347
|
+
for (const plugin of plugins) {
|
|
348
|
+
if (definitelyNoOp(plugin, installed)) {
|
|
349
|
+
log.log(` ==> ${plugin.package}: already installed, skipping`);
|
|
350
|
+
installed.delete(plugin.plugin_hash);
|
|
351
|
+
mergeConfigSafely(
|
|
352
|
+
plugin.pluginConfig,
|
|
353
|
+
globalConfig,
|
|
354
|
+
plugin.package,
|
|
355
|
+
errors
|
|
356
|
+
);
|
|
357
|
+
} else {
|
|
358
|
+
needsWork.push(plugin);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return needsWork;
|
|
362
|
+
}
|
|
363
|
+
function applyInstallOutcomes(results, globalConfig, errors) {
|
|
364
|
+
for (const outcome of results) {
|
|
365
|
+
if (!outcome.ok) {
|
|
366
|
+
errors.push(`${outcome.item.package}: ${outcome.error.message}`);
|
|
367
|
+
log.log(` ==> ERROR: ${outcome.item.package}: ${outcome.error.message}`);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const { value, item } = outcome;
|
|
371
|
+
const merged = mergeConfigSafely(
|
|
372
|
+
value.pluginConfig,
|
|
373
|
+
globalConfig,
|
|
374
|
+
item.package,
|
|
375
|
+
errors
|
|
376
|
+
);
|
|
377
|
+
if (merged && value.pluginPath) {
|
|
378
|
+
log.log(` ==> Installed ${item.package}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function mergeConfigSafely(pluginConfig, globalConfig, pkg, errors) {
|
|
383
|
+
if (!util.isPlainObject(pluginConfig)) return true;
|
|
384
|
+
try {
|
|
385
|
+
merger.deepMerge(pluginConfig, globalConfig);
|
|
386
|
+
return true;
|
|
387
|
+
} catch (err) {
|
|
388
|
+
errors.push(`${pkg}: ${err.message}`);
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function definitelyNoOp(plugin, installed) {
|
|
393
|
+
if (!plugin.plugin_hash || !installed.has(plugin.plugin_hash)) return false;
|
|
394
|
+
if (plugin.forceDownload) return false;
|
|
395
|
+
return types.effectivePullPolicy(plugin) !== types.PullPolicy.ALWAYS;
|
|
396
|
+
}
|
|
397
|
+
async function cleanupRemoved(root, installed) {
|
|
398
|
+
for (const [, dir] of installed) {
|
|
399
|
+
const pluginDir = path__namespace.join(root, dir);
|
|
400
|
+
log.log(`
|
|
401
|
+
======= Removing obsolete plugin ${dir}`);
|
|
402
|
+
await fs__namespace.rm(pluginDir, { recursive: true, force: true });
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async function readInstalledPluginHashes(root) {
|
|
406
|
+
const installed = /* @__PURE__ */ new Map();
|
|
407
|
+
let entries;
|
|
408
|
+
try {
|
|
409
|
+
entries = await fs__namespace.readdir(root);
|
|
410
|
+
} catch {
|
|
411
|
+
return installed;
|
|
412
|
+
}
|
|
413
|
+
for (const entry of entries) {
|
|
414
|
+
const hashFile = path__namespace.join(root, entry, types.CONFIG_HASH_FILE);
|
|
415
|
+
try {
|
|
416
|
+
const hash = (await fs__namespace.readFile(hashFile, "utf8")).trim();
|
|
417
|
+
if (hash) installed.set(hash, entry);
|
|
418
|
+
} catch {
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return installed;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
exports.finalizeInstall = finalizeInstall;
|
|
425
|
+
exports.main = main;
|
|
426
|
+
//# sourceMappingURL=installer.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"installer.cjs.js","sources":["../src/installer.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 { existsSync } from 'node:fs';\nimport * as fs from 'node:fs/promises';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport { cli } from 'cleye';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport {\n cleanupCatalogIndexTemp,\n extractCatalogIndex,\n extractExtraCatalogIndex,\n parseExtraCatalogIndexImages,\n} from './catalog-index';\nimport {\n getNpmWorkers,\n getWorkers,\n mapConcurrent,\n type Outcome,\n} from './concurrency';\nimport { InstallException } from './errors';\nimport { OciImageCache } from './image-cache';\nimport { installNpmPlugin } from './installer-npm';\nimport { installOciPlugin } from './installer-oci';\nimport { createLock, registerLockCleanup, removeLock } from './lock-file';\nimport { log } from './log';\nimport {\n deepMerge,\n filterDisabledOciPlugins,\n mergePlugin,\n preMergeOciDisabledState,\n} from './merger';\nimport { computePluginHash } from './plugin-hash';\nimport { Skopeo } from './skopeo';\nimport {\n CONFIG_HASH_FILE,\n DPDY_FILENAME,\n type DynamicPluginsConfig,\n effectivePullPolicy,\n GLOBAL_CONFIG_FILENAME,\n LOCK_FILENAME,\n OCI_PROTO,\n type Plugin,\n type PluginMap,\n type PluginSpec,\n PullPolicy,\n} from './types';\nimport { fileExists, isPlainObject } from './util';\n\nconst CONFIG_FILE = 'dynamic-plugins.yaml';\n\n/**\n * Entry point for the install command. `args` is the argv slice after the\n * command path; `programName` overrides the `--help` usage line so it matches\n * the actual invocation (e.g. `install-dynamic-plugins install` when called\n * via the cli-module discovery path). Exported for the cli-module command\n * loader and unit tests; not part of the package's public API.\n *\n * @internal\n */\nexport async function main(\n args: string[] = process.argv.slice(2),\n programName = 'install-dynamic-plugins',\n): Promise<void> {\n const argv = cli(\n {\n name: programName,\n parameters: ['<dynamic-plugins-root>'],\n help: {\n description:\n 'Install RHDH dynamic plugins listed in dynamic-plugins.yaml into the given directory.',\n },\n },\n undefined,\n args,\n );\n const root = path.resolve(argv._.dynamicPluginsRoot);\n const lockPath = path.join(root, LOCK_FILENAME);\n registerLockCleanup(lockPath);\n await fs.mkdir(root, { recursive: true });\n await createLock(lockPath);\n\n let exitCode = 0;\n try {\n exitCode = await runInstaller(root);\n } finally {\n await cleanupCatalogIndexTemp(root).catch(() => undefined);\n await removeLock(lockPath).catch(() => undefined);\n }\n process.exit(exitCode);\n}\n\nasync function runInstaller(root: string): Promise<number> {\n const skopeo = new Skopeo();\n const workers = getWorkers();\n log(`======= Workers: ${workers} (CPUs: ${os.cpus().length})`);\n\n // Resolve the config file path against CWD at startup so the dependency on\n // CWD is explicit in the operator log; includes are resolved relative to\n // the config file's directory (matches the Python installer).\n const configFileAbs = path.resolve(CONFIG_FILE);\n const configDir = path.dirname(configFileAbs);\n const globalConfigFile = path.join(root, GLOBAL_CONFIG_FILENAME);\n log(`======= Config file: ${configFileAbs}`);\n\n const entitiesDir =\n process.env.CATALOG_ENTITIES_EXTRACT_DIR ??\n path.join(os.tmpdir(), 'extensions');\n const catalogDpdy = await maybeExtractCatalogIndex(skopeo, root, entitiesDir);\n await maybeExtractExtraCatalogIndexes(skopeo, entitiesDir);\n const content = await loadDynamicPluginsConfig(\n configFileAbs,\n globalConfigFile,\n );\n if (!content) return 0;\n\n const imageCache = new OciImageCache(\n skopeo,\n await fs.mkdtemp(path.join(os.tmpdir(), 'rhdh-oci-cache-')),\n );\n\n const allPlugins = await loadAllPlugins(\n content,\n configFileAbs,\n configDir,\n catalogDpdy,\n imageCache,\n );\n const installed = await readInstalledPluginHashes(root);\n const globalConfig: Record<string, unknown> = {\n dynamicPlugins: { rootDirectory: 'dynamic-plugins-root' },\n };\n const { oci, npm, skipped } = categorize(allPlugins);\n handleSkippedLocals(skipped, globalConfig);\n\n const skipIntegrity =\n (process.env.SKIP_INTEGRITY_CHECK ?? '').toLowerCase() === 'true';\n const errors: string[] = [];\n await installOci(\n oci,\n root,\n imageCache,\n installed,\n workers,\n globalConfig,\n errors,\n );\n await installNpm(npm, root, skipIntegrity, installed, globalConfig, errors);\n\n return finalizeInstall(\n errors,\n globalConfigFile,\n globalConfig,\n root,\n installed,\n );\n}\n\n/** Optional `CATALOG_INDEX_IMAGE` extraction — returns the path to the\n * extracted `dynamic-plugins.default.yaml`, or `null` when the env var is\n * unset. */\nasync function maybeExtractCatalogIndex(\n skopeo: Skopeo,\n root: string,\n entitiesDir: string,\n): Promise<string | null> {\n const catalogImage = process.env.CATALOG_INDEX_IMAGE ?? '';\n if (!catalogImage) return null;\n return extractCatalogIndex(skopeo, catalogImage, root, entitiesDir);\n}\n\n/**\n * Optional `EXTRA_CATALOG_INDEX_IMAGES` extraction. Each entry is extracted\n * into an isolated subdirectory under `<entitiesDir>/extra/` so multiple\n * indexes can coexist without clobbering the primary index's\n * `catalog-entities/`. Duplicate subdirectory names within the same env-var\n * value emit an overwrite warning (handled by `extractExtraCatalogIndex`).\n */\nasync function maybeExtractExtraCatalogIndexes(\n skopeo: Skopeo,\n entitiesDir: string,\n): Promise<void> {\n const raw = process.env.EXTRA_CATALOG_INDEX_IMAGES ?? '';\n if (!raw) return;\n const extraParent = path.join(entitiesDir, 'extra');\n const seen = new Map<string, string>();\n for (const [name, imageRef] of parseExtraCatalogIndexImages(raw)) {\n const prev = seen.get(name) ?? null;\n seen.set(name, imageRef);\n await extractExtraCatalogIndex(skopeo, imageRef, name, extraParent, prev);\n }\n}\n\n/** Read and parse `dynamic-plugins.yaml`. Writes an empty global config and\n * returns `null` for the two short-circuit cases (file missing, file empty)\n * so the caller can early-exit with code 0. */\nasync function loadDynamicPluginsConfig(\n configFileAbs: string,\n globalConfigFile: string,\n): Promise<DynamicPluginsConfig | null> {\n if (!(await fileExists(configFileAbs))) {\n log(`No ${CONFIG_FILE} found at ${configFileAbs}. Skipping.`);\n await fs.writeFile(globalConfigFile, '');\n return null;\n }\n const rawContent = await fs.readFile(configFileAbs, 'utf8');\n const content = parseYaml(rawContent) as DynamicPluginsConfig | null;\n if (!content) {\n log(`${configFileAbs} is empty. Skipping.`);\n await fs.writeFile(globalConfigFile, '');\n return null;\n }\n if (!isPlainObject(content)) {\n throw new InstallException(\n `${configFileAbs} must contain a YAML mapping, got ${typeof content}`,\n );\n }\n return content;\n}\n\n/** Resolve include paths, substitute the catalog-index placeholder, merge\n * everything into a single `PluginMap`, and compute change-detection hashes.\n *\n * Two-phase to match the Python pre-merge OCI-disable pass: load every\n * include file's plugin list into memory FIRST, compute the effectively\n * disabled OCI registries, then filter those entries out of every list\n * before merging. Without this pass an OCI plugin marked `disabled: true`\n * at level 1 would still trigger a `skopeo` round-trip during the level-0\n * merge — wasted work and a footgun in restricted-network init containers.\n */\nasync function loadAllPlugins(\n content: DynamicPluginsConfig,\n configFileAbs: string,\n configDir: string,\n catalogDpdy: string | null,\n imageCache: OciImageCache,\n): Promise<PluginMap> {\n const allPlugins: PluginMap = {};\n const includes = resolveIncludes(\n content.includes ?? [],\n configDir,\n catalogDpdy,\n );\n\n const includeLists: Array<[string, PluginSpec[]]> = [];\n for (const inc of includes) {\n if (!(await fileExists(inc))) {\n log(`WARNING: include file ${inc} not found, skipping`);\n continue;\n }\n log(`\\n======= Including plugins from ${inc}`);\n const parsed = parseYaml(\n await fs.readFile(inc, 'utf8'),\n ) as DynamicPluginsConfig | null;\n if (parsed && !isPlainObject(parsed)) {\n throw new InstallException(`${inc} must contain a mapping`);\n }\n const plugins = parsed?.plugins ?? [];\n if (!Array.isArray(plugins)) {\n throw new InstallException(\n `${inc} must contain a 'plugins' list (got ${typeof plugins})`,\n );\n }\n includeLists.push([inc, plugins]);\n }\n const mainPlugins = content.plugins ?? [];\n\n const disabledRegistries = preMergeOciDisabledState(\n includeLists,\n mainPlugins,\n configFileAbs,\n );\n\n for (const [inc, plugins] of includeLists) {\n for (const plugin of filterDisabledOciPlugins(\n plugins,\n disabledRegistries,\n )) {\n await mergePlugin(plugin, allPlugins, inc, /* level */ 0, imageCache);\n }\n }\n for (const plugin of filterDisabledOciPlugins(\n mainPlugins,\n disabledRegistries,\n )) {\n await mergePlugin(\n plugin,\n allPlugins,\n configFileAbs,\n /* level */ 1,\n imageCache,\n );\n }\n\n for (const p of Object.values(allPlugins)) {\n p.plugin_hash = computePluginHash(p);\n }\n return allPlugins;\n}\n\nfunction resolveIncludes(\n rawIncludes: readonly string[],\n configDir: string,\n catalogDpdy: string | null,\n): string[] {\n const includes = rawIncludes.map(inc =>\n path.isAbsolute(inc) ? inc : path.resolve(configDir, inc),\n );\n if (catalogDpdy) {\n const idx = includes.findIndex(inc => path.basename(inc) === DPDY_FILENAME);\n if (idx !== -1) includes[idx] = catalogDpdy;\n }\n return includes;\n}\n\n/**\n * Write the global config, prune obsolete plugin dirs, and compute the exit\n * code. Exported for unit tests; not part of the package's public API.\n *\n * @internal\n */\nexport async function finalizeInstall(\n errors: string[],\n globalConfigFile: string,\n globalConfig: Record<string, unknown>,\n root: string,\n installed: Map<string, string>,\n): Promise<number> {\n if (errors.length > 0) {\n log(`\\n======= ${errors.length} plugin(s) failed:`);\n for (const err of errors) log(` - ${err}`);\n // Skip writing the global config and cleaning up previously-installed\n // plugin dirs so the filesystem does not end up in an \"almost valid\"\n // state. Exit 1 is enough for init containers to block startup, but a\n // manual restart of the main container (or a deployment that does not\n // enforce init-container success) could otherwise pick up a partial\n // config — e.g. a frontend plugin without its backend dep, yielding a\n // broken UI. Preserving the prior state makes the next run a clean retry.\n log(\n `\\n======= Skipping ${GLOBAL_CONFIG_FILENAME} write and cleanup because of install failures. ` +\n `Fix the errors above and re-run; the previous successful state is preserved.`,\n );\n return 1;\n }\n\n await fs.writeFile(globalConfigFile, stringifyYaml(globalConfig));\n await cleanupRemoved(root, installed);\n\n log('\\n======= All plugins installed successfully');\n return 0;\n}\n\ntype Categorized = {\n oci: Plugin[];\n npm: Plugin[];\n skipped: Plugin[];\n};\n\nfunction categorize(allPlugins: PluginMap): Categorized {\n const oci: Plugin[] = [];\n const npm: Plugin[] = [];\n const skipped: Plugin[] = [];\n for (const plugin of Object.values(allPlugins)) {\n if (plugin.disabled) {\n log(`\\n======= Skipping disabled plugin ${plugin.package}`);\n continue;\n }\n if (plugin.package.startsWith(OCI_PROTO)) {\n oci.push(plugin);\n continue;\n }\n if (plugin.package.startsWith('./')) {\n const localPath = path.join(process.cwd(), plugin.package.slice(2));\n if (existsSync(localPath)) npm.push(plugin);\n else skipped.push(plugin);\n continue;\n }\n npm.push(plugin);\n }\n return { oci, npm, skipped };\n}\n\nfunction handleSkippedLocals(\n skipped: Plugin[],\n globalConfig: Record<string, unknown>,\n): void {\n if (skipped.length === 0) return;\n log(\n `\\n======= Skipping ${skipped.length} local plugins (directories not found)`,\n );\n for (const plugin of skipped) {\n const abs = path.join(process.cwd(), plugin.package.slice(2));\n log(`\\t==> ${plugin.package} (not found at ${abs})`);\n if (isPlainObject(plugin.pluginConfig)) {\n deepMerge(plugin.pluginConfig, globalConfig);\n }\n }\n}\n\ntype InstallOutcome = {\n pluginPath: string | null;\n pluginConfig: Record<string, unknown>;\n};\n\nasync function installOci(\n plugins: Plugin[],\n root: string,\n imageCache: OciImageCache,\n installed: Map<string, string>,\n workers: number,\n globalConfig: Record<string, unknown>,\n errors: string[],\n): Promise<void> {\n await runInstallPipeline({\n plugins,\n workers,\n label: 'OCI',\n installFn: plugin => installOciPlugin(plugin, root, imageCache, installed),\n installed,\n globalConfig,\n errors,\n });\n}\n\nasync function installNpm(\n plugins: Plugin[],\n root: string,\n skipIntegrity: boolean,\n installed: Map<string, string>,\n globalConfig: Record<string, unknown>,\n errors: string[],\n): Promise<void> {\n // `npm pack` writes the tarball to `cwd` with a package-derived filename\n // (`<name>-<version>.tgz`), so concurrent invocations against different\n // packages don't clash on the filename. The npm CLI cache\n // (`~/.npm/_cacache`) handles its own locking. Cap defaults to 3 to keep\n // the registry happy — override with `DYNAMIC_PLUGINS_NPM_WORKERS=1` to\n // restore the original sequential behaviour.\n await runInstallPipeline({\n plugins,\n workers: getNpmWorkers(),\n label: 'NPM',\n installFn: plugin =>\n installNpmPlugin(plugin, root, skipIntegrity, installed),\n installed,\n globalConfig,\n errors,\n });\n}\n\ntype RunInstallPipelineArgs = {\n plugins: Plugin[];\n workers: number;\n label: 'OCI' | 'NPM';\n installFn: (plugin: Plugin) => Promise<InstallOutcome>;\n installed: Map<string, string>;\n globalConfig: Record<string, unknown>;\n errors: string[];\n};\n\n/**\n * Shared install pipeline used by both `installOci` and `installNpm`:\n * 1. Synchronous pre-pass that short-circuits \"definitely no-op\" plugins\n * (hash present, no force, pull policy not Always) without spinning\n * up the worker pool — avoids the parallel-skopeo / parallel-npm-pack\n * overhead in the steady-state restart case.\n * 2. `mapConcurrent` over the plugins that actually need work, capped by\n * `workers`.\n * 3. Single-pass over the outcomes that records errors and merges plugin\n * configs into the global config.\n *\n * Keeping both categories on this shared body so a behaviour change (a new\n * fast-path filter, a different log format, an extra error pathway) does\n * not have to be made twice in two slightly-divergent copies.\n */\nasync function runInstallPipeline(args: RunInstallPipelineArgs): Promise<void> {\n const {\n plugins,\n workers,\n label,\n installFn,\n installed,\n globalConfig,\n errors,\n } = args;\n if (plugins.length === 0) return;\n\n const needsWork = partitionDefinitelyNoOp(\n plugins,\n installed,\n globalConfig,\n errors,\n );\n if (needsWork.length === 0) return;\n\n const workerSuffix = workers === 1 ? '' : 's';\n log(\n `\\n======= Installing ${needsWork.length} ${label} plugin(s) (${workers} worker${workerSuffix})`,\n );\n\n const results = await mapConcurrent(needsWork, workers, async plugin => {\n log(`\\n======= Installing ${label} plugin ${plugin.package}`);\n return installFn(plugin);\n });\n\n applyInstallOutcomes(results, globalConfig, errors);\n}\n\n/**\n * Synchronous pre-pass: drop plugins that are definitely a no-op (hash on\n * disk, not forced, not Always-pull) without paying the worker-pool /\n * Promise overhead, and return the remaining plugins that actually need\n * installation work. Side-effect: removes the no-op plugins from\n * `installed` and merges their `pluginConfig` into `globalConfig`.\n */\nfunction partitionDefinitelyNoOp(\n plugins: Plugin[],\n installed: Map<string, string>,\n globalConfig: Record<string, unknown>,\n errors: string[],\n): Plugin[] {\n const needsWork: Plugin[] = [];\n for (const plugin of plugins) {\n if (definitelyNoOp(plugin, installed)) {\n log(`\\t==> ${plugin.package}: already installed, skipping`);\n installed.delete(plugin.plugin_hash);\n mergeConfigSafely(\n plugin.pluginConfig,\n globalConfig,\n plugin.package,\n errors,\n );\n } else {\n needsWork.push(plugin);\n }\n }\n return needsWork;\n}\n\n/**\n * Drain a `mapConcurrent` outcome list: record errors, merge configs into\n * the global config, log success lines. Pulled out of `runInstallPipeline`\n * to keep that orchestrator small enough to read top-to-bottom.\n */\nfunction applyInstallOutcomes(\n results: ReadonlyArray<Outcome<InstallOutcome, Plugin>>,\n globalConfig: Record<string, unknown>,\n errors: string[],\n): void {\n for (const outcome of results) {\n if (!outcome.ok) {\n errors.push(`${outcome.item.package}: ${outcome.error.message}`);\n log(`\\t==> ERROR: ${outcome.item.package}: ${outcome.error.message}`);\n continue;\n }\n const { value, item } = outcome;\n const merged = mergeConfigSafely(\n value.pluginConfig,\n globalConfig,\n item.package,\n errors,\n );\n if (merged && value.pluginPath) {\n log(`\\t==> Installed ${item.package}`);\n }\n }\n}\n\n/**\n * Merge `pluginConfig` into `globalConfig` if it is a plain object. Returns\n * `false` and pushes onto `errors` when `deepMerge` raises a key conflict —\n * the caller uses this to skip the \"Installed\" success log so the operator\n * sees only the error line for the affected plugin.\n */\nfunction mergeConfigSafely(\n pluginConfig: Record<string, unknown> | undefined,\n globalConfig: Record<string, unknown>,\n pkg: string,\n errors: string[],\n): boolean {\n if (!isPlainObject(pluginConfig)) return true;\n try {\n deepMerge(pluginConfig, globalConfig);\n return true;\n } catch (err) {\n errors.push(`${pkg}: ${(err as Error).message}`);\n return false;\n }\n}\n\n/**\n * Cheap synchronous check: a plugin is \"definitely\" a no-op when its hash\n * is already on disk, the user did not force a re-download, and the pull\n * policy doesn't demand a remote-digest comparison. ALWAYS-pull plugins\n * still go through the regular install path because they need a\n * `skopeo inspect` to compare local vs remote digest.\n *\n * Type guard: narrows `plugin.plugin_hash` to a non-undefined `string`\n * inside the `if (definitelyNoOp(...))` branch so the caller does not\n * need a `as string` cast on the subsequent `installed.delete` call.\n */\nfunction definitelyNoOp(\n plugin: Plugin,\n installed: Map<string, string>,\n): plugin is Plugin & { plugin_hash: string } {\n if (!plugin.plugin_hash || !installed.has(plugin.plugin_hash)) return false;\n if (plugin.forceDownload) return false;\n return effectivePullPolicy(plugin) !== PullPolicy.ALWAYS;\n}\n\nasync function cleanupRemoved(\n root: string,\n installed: Map<string, string>,\n): Promise<void> {\n for (const [, dir] of installed) {\n const pluginDir = path.join(root, dir);\n log(`\\n======= Removing obsolete plugin ${dir}`);\n await fs.rm(pluginDir, { recursive: true, force: true });\n }\n}\n\nasync function readInstalledPluginHashes(\n root: string,\n): Promise<Map<string, string>> {\n const installed = new Map<string, string>();\n let entries: string[];\n try {\n entries = await fs.readdir(root);\n } catch {\n return installed;\n }\n for (const entry of entries) {\n const hashFile = path.join(root, entry, CONFIG_HASH_FILE);\n try {\n const hash = (await fs.readFile(hashFile, 'utf8')).trim();\n if (hash) installed.set(hash, entry);\n } catch {\n /* not a plugin dir */\n }\n }\n return installed;\n}\n"],"names":["cli","path","LOCK_FILENAME","registerLockCleanup","fs","createLock","cleanupCatalogIndexTemp","removeLock","skopeo","Skopeo","getWorkers","log","os","GLOBAL_CONFIG_FILENAME","imageCache","OciImageCache","extractCatalogIndex","parseExtraCatalogIndexImages","extractExtraCatalogIndex","fileExists","parseYaml","isPlainObject","InstallException","preMergeOciDisabledState","filterDisabledOciPlugins","mergePlugin","computePluginHash","DPDY_FILENAME","stringifyYaml","OCI_PROTO","existsSync","deepMerge","installOciPlugin","getNpmWorkers","installNpmPlugin","mapConcurrent","effectivePullPolicy","PullPolicy","CONFIG_HASH_FILE"],"mappings":"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"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var node_crypto = require('node:crypto');
|
|
4
|
+
var node_fs = require('node:fs');
|
|
5
|
+
var promises = require('node:stream/promises');
|
|
6
|
+
var errors = require('./errors.cjs.js');
|
|
7
|
+
var types = require('./types.cjs.js');
|
|
8
|
+
|
|
9
|
+
async function verifyIntegrity(pkg, archive, integrity) {
|
|
10
|
+
const dash = integrity.indexOf("-");
|
|
11
|
+
if (dash === -1) {
|
|
12
|
+
throw new errors.InstallException(
|
|
13
|
+
`Package integrity for ${pkg} must be a string of the form <algorithm>-<hash>`
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
const algo = integrity.slice(0, dash);
|
|
17
|
+
const expected = integrity.slice(dash + 1);
|
|
18
|
+
if (!isRecognizedAlgorithm(algo)) {
|
|
19
|
+
throw new errors.InstallException(
|
|
20
|
+
`${pkg}: Provided Package integrity algorithm ${algo} is not supported, please use one of following algorithms ${types.RECOGNIZED_ALGORITHMS.join(", ")} instead`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
if (!isValidBase64(expected)) {
|
|
24
|
+
throw new errors.InstallException(
|
|
25
|
+
`${pkg}: Provided Package integrity hash ${expected} is not a valid base64 encoding`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
const hash = node_crypto.createHash(algo);
|
|
29
|
+
await promises.pipeline(node_fs.createReadStream(archive), hash);
|
|
30
|
+
const actual = hash.digest("base64");
|
|
31
|
+
if (actual !== expected) {
|
|
32
|
+
throw new errors.InstallException(
|
|
33
|
+
`${pkg}: integrity check failed \u2014 got ${algo}-${actual}, expected ${integrity}`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function isRecognizedAlgorithm(value) {
|
|
38
|
+
return types.RECOGNIZED_ALGORITHMS.includes(value);
|
|
39
|
+
}
|
|
40
|
+
function isValidBase64(value) {
|
|
41
|
+
if (value.length === 0) return false;
|
|
42
|
+
if (!isBase64Shape(value)) return false;
|
|
43
|
+
try {
|
|
44
|
+
const buf = Buffer.from(value, "base64");
|
|
45
|
+
return stripTrailingEquals(buf.toString("base64")) === stripTrailingEquals(value);
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const EQUALS = 61;
|
|
51
|
+
function isBase64Shape(value) {
|
|
52
|
+
let paddingCount = 0;
|
|
53
|
+
for (let i = 0; i < value.length; i++) {
|
|
54
|
+
const c = value.codePointAt(i) ?? 0;
|
|
55
|
+
if (c === EQUALS) {
|
|
56
|
+
paddingCount++;
|
|
57
|
+
if (paddingCount > 2) return false;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (paddingCount > 0) return false;
|
|
61
|
+
if (!isBase64Char(c)) return false;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
function isBase64Char(c) {
|
|
66
|
+
return c >= 65 && c <= 90 || // A-Z
|
|
67
|
+
c >= 97 && c <= 122 || // a-z
|
|
68
|
+
c >= 48 && c <= 57 || // 0-9
|
|
69
|
+
c === 43 || // +
|
|
70
|
+
c === 47;
|
|
71
|
+
}
|
|
72
|
+
function stripTrailingEquals(s) {
|
|
73
|
+
let end = s.length;
|
|
74
|
+
while (end > 0 && s.codePointAt(end - 1) === EQUALS) end--;
|
|
75
|
+
return s.slice(0, end);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
exports.verifyIntegrity = verifyIntegrity;
|
|
79
|
+
//# sourceMappingURL=integrity.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"integrity.cjs.js","sources":["../src/integrity.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 { createHash } from 'node:crypto';\nimport { createReadStream } from 'node:fs';\nimport { pipeline } from 'node:stream/promises';\nimport { InstallException } from './errors';\nimport { RECOGNIZED_ALGORITHMS, type Algorithm } from './types';\n\n/**\n * Verify an NPM package archive matches the declared SRI-style integrity string.\n *\n * Uses streaming `createHash` so large archives never load into memory — safe\n * for the tight init-container memory budgets on OpenShift.\n */\nexport async function verifyIntegrity(\n pkg: string,\n archive: string,\n integrity: string,\n): Promise<void> {\n const dash = integrity.indexOf('-');\n if (dash === -1) {\n throw new InstallException(\n `Package integrity for ${pkg} must be a string of the form <algorithm>-<hash>`,\n );\n }\n const algo = integrity.slice(0, dash);\n const expected = integrity.slice(dash + 1);\n\n if (!isRecognizedAlgorithm(algo)) {\n throw new InstallException(\n `${pkg}: Provided Package integrity algorithm ${algo} is not supported, ` +\n `please use one of following algorithms ${RECOGNIZED_ALGORITHMS.join(', ')} instead`,\n );\n }\n if (!isValidBase64(expected)) {\n throw new InstallException(\n `${pkg}: Provided Package integrity hash ${expected} is not a valid base64 encoding`,\n );\n }\n\n const hash = createHash(algo);\n await pipeline(createReadStream(archive), hash);\n const actual = hash.digest('base64');\n\n if (actual !== expected) {\n throw new InstallException(\n `${pkg}: integrity check failed — got ${algo}-${actual}, expected ${integrity}`,\n );\n }\n}\n\nfunction isRecognizedAlgorithm(value: string): value is Algorithm {\n return (RECOGNIZED_ALGORITHMS as readonly string[]).includes(value);\n}\n\n/**\n * Validate a base64 string without regex (avoids Sonar ReDoS flags and is\n * genuinely linear-time). Accepts the standard base64 alphabet plus up to two\n * trailing `=` padding characters; requires the round-trip encoding to match\n * so malformed padding is rejected.\n */\nfunction isValidBase64(value: string): boolean {\n if (value.length === 0) return false;\n if (!isBase64Shape(value)) return false;\n try {\n const buf = Buffer.from(value, 'base64');\n return (\n stripTrailingEquals(buf.toString('base64')) === stripTrailingEquals(value)\n );\n } catch {\n return false;\n }\n}\n\nconst EQUALS = 0x3d;\n\nfunction isBase64Shape(value: string): boolean {\n let paddingCount = 0;\n for (let i = 0; i < value.length; i++) {\n const c = value.codePointAt(i) ?? 0;\n if (c === EQUALS) {\n paddingCount++;\n if (paddingCount > 2) return false;\n continue;\n }\n // Padding, once started, must run to the end of the string.\n if (paddingCount > 0) return false;\n if (!isBase64Char(c)) return false;\n }\n return true;\n}\n\nfunction isBase64Char(c: number): boolean {\n return (\n (c >= 0x41 && c <= 0x5a) || // A-Z\n (c >= 0x61 && c <= 0x7a) || // a-z\n (c >= 0x30 && c <= 0x39) || // 0-9\n c === 0x2b || // +\n c === 0x2f // /\n );\n}\n\nfunction stripTrailingEquals(s: string): string {\n let end = s.length;\n while (end > 0 && s.codePointAt(end - 1) === EQUALS) end--;\n return s.slice(0, end);\n}\n"],"names":["InstallException","RECOGNIZED_ALGORITHMS","createHash","pipeline","createReadStream"],"mappings":"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"}
|