Skip to content

Commit 7425adb

Browse files
petermetzhanxu12
authored andcommitted
fix(cmd-api-server): plugins interfere with API server deps hyperledger-cacti#1192
Migrates to the lmify package to install plugins at runtime instead of doing it via vanilla npm which was causing problems with conflicting dependency versions where the API server would want semver 7.x and one of the plugins (through some transient dependency of the plugin itself) would install semver 5.x which would then cause the API server to break down at runtime due to the breaking changes between semver 7 and 5. The magic sauce is the --prefix option of npm which, when specified instructs npm to ignore the usual parent directory traversal algorithm when evaluating/resolving dependency trees and instead just do a full installation to the specified directory path as dictated by the --prefix option. This means that we can install each plugin in their own directory the code being isolated from the API server and also from other plugins that might also interfere. Fixes hyperledger-cacti#1192 Depends on hyperledger-cacti#1203 Signed-off-by: Peter Somogyvari <[email protected]> Signed-off-by: hxlaf <[email protected]>
1 parent 0dc846a commit 7425adb

17 files changed

+484
-512
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"@openapitools/openapi-generator-cli": "2.3.3",
7979
"@types/fs-extra": "9.0.11",
8080
"@types/jasminewd2": "2.0.10",
81+
"@types/node": "15.14.7",
8182
"@types/node-fetch": "2.5.4",
8283
"@types/tape": "4.13.0",
8384
"@types/tape-promise": "4.0.1",

packages/cactus-cmd-api-server/Dockerfile

+2-2
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ EXPOSE 3000 4000
5050

5151
USER $APP_USER
5252

53-
RUN npm i @hyperledger/cactus-cmd-api-server@${NPM_PKG_VERSION} --production
53+
RUN npm i @elenaizaguirre/cactus-cmd-api-server@${NPM_PKG_VERSION}
5454

5555
ENTRYPOINT ["/sbin/tini", "--"]
56-
CMD ["node", "node_modules/@hyperledger/cactus-cmd-api-server/dist/lib/main/typescript/cmd/cactus-api.js"]
56+
CMD ["node", "node_modules/@elenaizaguirre/cactus-cmd-api-server/dist/lib/main/typescript/cmd/cactus-api.js"]
5757
HEALTHCHECK --interval=5s --timeout=5s --start-period=1s --retries=30 CMD /healthcheck.sh

packages/cactus-cmd-api-server/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,11 @@
8787
"express-jwt": "6.0.0",
8888
"express-jwt-authz": "2.4.1",
8989
"express-openapi-validator": "3.10.0",
90+
"fs-extra": "10.0.0",
9091
"http-status-codes": "2.1.4",
9192
"jose": "1.28.1",
93+
"lmify": "0.3.0",
9294
"node-forge": "0.10.0",
93-
"npm": "7.19.1",
9495
"prom-client": "13.1.0",
9596
"rxjs": "7.1.0",
9697
"semver": "7.3.2",
@@ -111,7 +112,6 @@
111112
"@types/jsonwebtoken": "8.5.1",
112113
"@types/multer": "1.4.5",
113114
"@types/node-forge": "0.9.3",
114-
"@types/npm": "2.0.32",
115115
"@types/passport": "1.0.6",
116116
"@types/passport-oauth2": "1.4.10",
117117
"@types/passport-saml": "1.1.2",

packages/cactus-cmd-api-server/src/main/typescript/api-server.ts

+65-51
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import path from "path";
21
import type { AddressInfo } from "net";
2+
import type { Server as SecureServer } from "https";
3+
import os from "os";
4+
import path from "path";
35
import tls from "tls";
46
import { Server, createServer } from "http";
5-
import type { Server as SecureServer } from "https";
67
import { createServer as createSecureServer } from "https";
78
import { gte } from "semver";
8-
import npm from "npm";
9+
import lmify from "lmify";
10+
import fs from "fs-extra";
911
import expressHttpProxy from "express-http-proxy";
1012
import type { Application, Request, Response, RequestHandler } from "express";
1113
import express from "express";
@@ -41,7 +43,9 @@ import { PrometheusExporter } from "./prometheus-exporter/prometheus-exporter";
4143
import { AuthorizerFactory } from "./authzn/authorizer-factory";
4244
import { WatchHealthcheckV1 } from "./generated/openapi/typescript-axios";
4345
import { WatchHealthcheckV1Endpoint } from "./web-services/watch-healthcheck-v1-endpoint";
46+
import { RuntimeError } from "run-time-error";
4447
export interface IApiServerConstructorOptions {
48+
pluginManagerOptions?: { pluginsPath: string };
4549
pluginRegistry?: PluginRegistry;
4650
httpServerApi?: Server | SecureServer;
4751
wsServerApi?: SocketIoServer;
@@ -71,6 +75,7 @@ export class ApiServer {
7175
private readonly wsApi: SocketIoServer;
7276
private readonly expressApi: Application;
7377
private readonly expressCockpit: Application;
78+
private readonly pluginsPath: string;
7479
public prometheusExporter: PrometheusExporter;
7580

7681
public get className(): string {
@@ -127,6 +132,23 @@ export class ApiServer {
127132
label: "api-server",
128133
level: options.config.logLevel,
129134
});
135+
136+
const defaultPluginsPath = path.join(
137+
os.tmpdir(),
138+
"org",
139+
"hyperledger",
140+
"cactus",
141+
"plugins",
142+
);
143+
144+
const { pluginsPath } = {
145+
...{ pluginsPath: defaultPluginsPath },
146+
...JSON.parse(this.options.config.pluginManagerOptionsJson),
147+
...this.options.pluginManagerOptions,
148+
} as { pluginsPath: string };
149+
150+
this.pluginsPath = pluginsPath;
151+
this.log.debug("pluginsPath: %o", pluginsPath);
130152
}
131153

132154
public getPrometheusExporter(): PrometheusExporter {
@@ -256,8 +278,16 @@ export class ApiServer {
256278

257279
await this.installPluginPackage(pluginImport);
258280

281+
const packagePath = path.join(
282+
this.pluginsPath,
283+
options.instanceId,
284+
"node_modules",
285+
packageName,
286+
);
287+
this.log.debug("Package path: %o", packagePath);
288+
259289
// eslint-disable-next-line @typescript-eslint/no-var-requires
260-
const pluginPackage = require(/* webpackIgnore: true */ packageName);
290+
const pluginPackage = require(/* webpackIgnore: true */ packagePath);
261291
const createPluginFactory = pluginPackage.createPluginFactory as PluginFactoryFactory;
262292

263293
const pluginFactoryOptions: IPluginFactoryOptions = {
@@ -276,54 +306,38 @@ export class ApiServer {
276306
const fnTag = `ApiServer#installPluginPackage()`;
277307
const { packageName: pkgName } = pluginImport;
278308

279-
const npmLogHandler = (message: unknown) => {
280-
this.log.debug(`${fnTag} [npm-log]:`, message);
281-
};
282-
283-
const cleanUpNpmLogHandler = () => {
284-
npm.off("log", npmLogHandler);
285-
};
286-
309+
const instanceId = pluginImport.options.instanceId;
310+
const pluginPackageDir = path.join(this.pluginsPath, instanceId);
287311
try {
288-
this.log.info(`Installing ${pkgName} for plugin import`, pluginImport);
289-
npm.on("log", npmLogHandler);
290-
291-
await new Promise<void>((resolve, reject) => {
292-
npm.load((err?: Error) => {
293-
if (err) {
294-
this.log.error(`${fnTag} npm load fail:`, err);
295-
const { message, stack } = err;
296-
reject(new Error(`${fnTag} npm load fail: ${message}: ${stack}`));
297-
} else {
298-
// do not touch package.json
299-
npm.config.set("save", false);
300-
// do not touch package-lock.json
301-
npm.config.set("package-lock", false);
302-
// do not waste resources on running an audit
303-
npm.config.set("audit", false);
304-
// do not wast resources on rendering a progress bar
305-
npm.config.set("progress", false);
306-
resolve();
307-
}
308-
});
309-
});
310-
311-
await new Promise<unknown>((resolve, reject) => {
312-
const npmInstallHandler = (errInstall?: Error, result?: unknown) => {
313-
if (errInstall) {
314-
this.log.error(`${fnTag} npm install failed:`, errInstall);
315-
const { message: m, stack } = errInstall;
316-
reject(new Error(`${fnTag} npm install fail: ${m}: ${stack}`));
317-
} else {
318-
this.log.info(`Installed ${pkgName} OK`, result);
319-
resolve(result);
320-
}
321-
};
322-
323-
npm.commands.install([pkgName], npmInstallHandler);
324-
});
325-
} finally {
326-
cleanUpNpmLogHandler();
312+
await fs.mkdirp(pluginPackageDir);
313+
this.log.debug(`${pkgName} plugin package dir: %o`, pluginPackageDir);
314+
} catch (ex) {
315+
const errorMessage =
316+
"Could not create plugin installation directory, check the file-system permissions.";
317+
throw new RuntimeError(errorMessage, ex);
318+
}
319+
try {
320+
lmify.setPackageManager("npm");
321+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
322+
// @ts-expect-error
323+
lmify.setRootDir(pluginPackageDir);
324+
this.log.debug(`Installing ${pkgName} for plugin import`, pluginImport);
325+
const out = await lmify.install([
326+
pkgName,
327+
"--production",
328+
"--audit=false",
329+
"--progress=false",
330+
"--fund=false",
331+
`--prefix=${pluginPackageDir}`,
332+
// "--ignore-workspace-root-check",
333+
]);
334+
this.log.debug("%o install result: %o", pkgName, out);
335+
if (out.exitCode !== 0) {
336+
throw new RuntimeError("Non-zero exit code: ", JSON.stringify(out));
337+
}
338+
this.log.info(`Installed ${pkgName} OK`);
339+
} catch (ex) {
340+
throw new RuntimeError(`${fnTag} plugin install fail: ${pkgName}`, ex);
327341
}
328342
}
329343

packages/cactus-cmd-api-server/src/main/typescript/config/config-service.ts

+10
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ convict.addFormat(FORMAT_PLUGIN_ARRAY);
2626
convict.addFormat(ipaddress);
2727

2828
export interface ICactusApiServerOptions {
29+
pluginManagerOptionsJson: string;
2930
authorizationProtocol: AuthorizationProtocol;
3031
authorizationConfigJson: IAuthorizationConfig;
3132
configFile: string;
@@ -88,6 +89,14 @@ export class ConfigService {
8889

8990
private static getConfigSchema(): Schema<ICactusApiServerOptions> {
9091
return {
92+
pluginManagerOptionsJson: {
93+
doc:
94+
"Can be used to override npm registry and authentication details for example. See https://www.npmjs.com/package/live-plugin-manager#pluginmanagerconstructoroptions-partialpluginmanageroptions for further details.",
95+
format: "*",
96+
default: "{}",
97+
env: "PLUGIN_MANAGER_OPTIONS_JSON",
98+
arg: "plugin-manager-options-json",
99+
},
91100
authorizationProtocol: {
92101
doc:
93102
"The name of the authorization protocol to use. Accepted values" +
@@ -518,6 +527,7 @@ export class ConfigService {
518527
};
519528

520529
return {
530+
pluginManagerOptionsJson: "{}",
521531
authorizationProtocol: AuthorizationProtocol.JSON_WEB_TOKEN,
522532
authorizationConfigJson,
523533
configFile: ".config.json",

packages/cactus-cmd-api-server/src/test/typescript/benchmark/artillery-api-benchmark.test.ts

+15-5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
import { promisify } from "util";
2+
import { unlinkSync, readFileSync } from "fs";
3+
4+
import { exec } from "child_process";
5+
import path from "path";
6+
17
import test, { Test } from "tape-promise/tape";
28
import { v4 as uuidv4 } from "uuid";
39
import { JWK } from "jose";
@@ -23,11 +29,6 @@ const log = LoggerProvider.getOrCreate({
2329
label: "logger-test",
2430
});
2531

26-
import { promisify } from "util";
27-
import { unlinkSync, readFileSync } from "fs";
28-
29-
import { exec } from "child_process";
30-
3132
const shell_exec = promisify(exec);
3233

3334
const artilleryScriptLocation =
@@ -47,9 +48,18 @@ test("Start API server, and run Artillery benchmark test.", async (t: Test) => {
4748

4849
log.info("Generating Config...");
4950

51+
const pluginsPath = path.join(
52+
__dirname, // start at the current file's path
53+
"../../../../../../", // walk back up to the project root
54+
".tmp/test/cmd-api-server/artillery-api-benchmark_test", // the dir path from the root
55+
uuidv4(), // then a random directory to ensure proper isolation
56+
);
57+
const pluginManagerOptionsJson = JSON.stringify({ pluginsPath });
58+
5059
const configService = new ConfigService();
5160
const apiServerOptions = configService.newExampleConfig();
5261
apiServerOptions.authorizationProtocol = AuthorizationProtocol.NONE;
62+
apiServerOptions.pluginManagerOptionsJson = pluginManagerOptionsJson;
5363
apiServerOptions.configFile = "";
5464
apiServerOptions.apiCorsDomainCsv = "*";
5565
apiServerOptions.apiPort = 4000;

packages/cactus-cmd-api-server/src/test/typescript/integration/jwt-endpoint-authorization.test.ts

+10
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import path from "path";
12
import test, { Test } from "tape-promise/tape";
23
import { v4 as uuidv4 } from "uuid";
34
import { JWK, JWT } from "jose";
@@ -64,9 +65,18 @@ test(testCase, async (t: Test) => {
6465
},
6566
};
6667

68+
const pluginsPath = path.join(
69+
__dirname, // start at the current file's path
70+
"../../../../../../", // walk back up to the project root
71+
".tmp/test/cmd-api-server/jwt-endpoint-authorization_test", // the dir path from the root
72+
uuidv4(), // then a random directory to ensure proper isolation
73+
);
74+
const pluginManagerOptionsJson = JSON.stringify({ pluginsPath });
75+
6776
const configService = new ConfigService();
6877
const apiSrvOpts = configService.newExampleConfig();
6978
apiSrvOpts.authorizationProtocol = AuthorizationProtocol.JSON_WEB_TOKEN;
79+
apiSrvOpts.pluginManagerOptionsJson = pluginManagerOptionsJson;
7080
apiSrvOpts.authorizationConfigJson = authorizationConfig;
7181
apiSrvOpts.configFile = "";
7282
apiSrvOpts.apiCorsDomainCsv = "*";

packages/cactus-cmd-api-server/src/test/typescript/integration/remote-plugin-imports.test.ts

+10
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717

1818
import { DefaultApi } from "@hyperledger/cactus-plugin-keychain-vault";
1919
import { Configuration, PluginImportType } from "@hyperledger/cactus-core-api";
20+
import path from "path";
2021

2122
test("NodeJS API server + Rust plugin work together", async (t: Test) => {
2223
const vaultTestContainer = new VaultTestServer({});
@@ -57,9 +58,18 @@ test("NodeJS API server + Rust plugin work together", async (t: Test) => {
5758
});
5859
const apiClient = new DefaultApi(configuration);
5960

61+
const pluginsPath = path.join(
62+
__dirname, // start at the current file's path
63+
"../../../../../../", // walk back up to the project root
64+
".tmp/test/cmd-api-server/remote-plugin-imports_test", // the dir path from the root
65+
uuidv4(), // then a random directory to ensure proper isolation
66+
);
67+
const pluginManagerOptionsJson = JSON.stringify({ pluginsPath });
68+
6069
const configService = new ConfigService();
6170
const apiServerOptions = configService.newExampleConfig();
6271
apiServerOptions.authorizationProtocol = AuthorizationProtocol.NONE;
72+
apiServerOptions.pluginManagerOptionsJson = pluginManagerOptionsJson;
6373
apiServerOptions.configFile = "";
6474
apiServerOptions.apiCorsDomainCsv = "*";
6575
apiServerOptions.apiPort = 0;

packages/cactus-cmd-api-server/src/test/typescript/unit/config/config-service-example-config-validity.test.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
1-
import { LoggerProvider } from "@hyperledger/cactus-common";
1+
import path from "path";
22
import test, { Test } from "tape-promise/tape";
3+
import { v4 as uuidv4 } from "uuid";
4+
import { LoggerProvider } from "@hyperledger/cactus-common";
35

46
import { IAuthorizationConfig } from "../../../../main/typescript/public-api";
57
import { ApiServer } from "../../../../main/typescript/public-api";
68
import { ConfigService } from "../../../../main/typescript/public-api";
79

810
test("Generates valid example config for the API server", async (t: Test) => {
11+
const pluginsPath = path.join(
12+
__dirname,
13+
"../../../../../../", // walk back up to the project root
14+
".tmp/test/test-cmd-api-server/config-service-example-config-validity_test/", // the dir path from the root
15+
uuidv4(), // then a random directory to ensure proper isolation
16+
);
17+
const pluginManagerOptionsJson = JSON.stringify({ pluginsPath });
18+
919
const configService = new ConfigService();
1020
t.ok(configService, "Instantiated ConfigService truthy OK");
1121

1222
const exampleConfig = configService.newExampleConfig();
1323
t.ok(exampleConfig, "configService.newExampleConfig() truthy OK");
1424

25+
exampleConfig.pluginManagerOptionsJson = pluginManagerOptionsJson;
26+
1527
// FIXME - this hack should not be necessary, we need to re-think how we
1628
// do configuration parsing. The convict library may not be the path forward.
1729
exampleConfig.authorizationConfigJson = (JSON.stringify(

0 commit comments

Comments
 (0)