UNPKG

25.5 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const crypto = require("crypto");
4const _ = require("underscore");
5const os = require("os");
6const path = require("path");
7const request = require("request-promise");
8const docker_cli_js_1 = require("docker-cli-js");
9const cron_1 = require("cron");
10const uuid = require("uuid/v5");
11const sbase = require("@nodeswork/sbase");
12const logger = require("@nodeswork/logger");
13const utils_1 = require("@nodeswork/utils");
14const applet = require("@nodeswork/applet");
15const errors = require("./errors");
16const utils_2 = require("./utils");
17const server_1 = require("./server");
18const socket_1 = require("./socket");
19const paths_1 = require("./paths");
20const env = require("./env");
21const compareVersion = require("compare-version");
22const latestVersion = require('latest-version');
23const LOG = logger.getLogger();
24const APPLET_MANAGER_KEY = 'appletManager';
25const containerVersion = require('../package.json').version;
26const isRunning = require('is-running');
27const machineId = require('node-machine-id').machineIdSync;
28const UUID_NAMESPACE = '5daabcd8-f17e-568c-aa6f-da9d92c7032c';
29const NAME_REGEX = /^na-(\w+)-([0-9.]+)-([\w-]+)_([0-9.]+)-(\w+)$/;
30class AppletManager {
31 constructor(options) {
32 this.options = options;
33 this.docker = new docker_cli_js_1.Docker();
34 this.cronJobs = [];
35 if (this.options.debug) {
36 LOG.level = 'debug';
37 }
38 const configPath = path.join(options.appPath, 'config.json');
39 LOG.debug('Load configuration from configPath', configPath);
40 this.ls = utils_2.localStorage(configPath);
41 let amOptions = this.ls.getItemSync(APPLET_MANAGER_KEY);
42 let running = false;
43 if (amOptions == null) {
44 amOptions = this.options;
45 LOG.debug('Initialize Applet Manager Options to local:', amOptions);
46 this.ls.setItemSync(APPLET_MANAGER_KEY, amOptions);
47 }
48 else {
49 LOG.debug('Got Applet Manager Options from local:', amOptions);
50 if (amOptions.pid) {
51 running = isRunning(amOptions.pid);
52 }
53 this.options.token = amOptions.token;
54 }
55 if (running && (this.options.appPath !== amOptions.appPath ||
56 this.options.nodesworkServer !== amOptions.nodesworkServer ||
57 this.options.port !== amOptions.port)) {
58 throw new utils_1.NodesworkError('Configuration does not match with existing Applet Manager', {
59 newOption: this.options,
60 runningOption: _.pick(amOptions, 'appPath', 'nodesworkServer', 'port'),
61 });
62 }
63 if (running) {
64 this.options.pid = amOptions.pid;
65 this.options.token = amOptions.token;
66 }
67 else {
68 this.options.pid = null;
69 }
70 this.serverApi = request.defaults({
71 headers: {
72 'device-token': this.options.token,
73 },
74 baseUrl: this.options.nodesworkServer,
75 json: true,
76 jar: true,
77 });
78 }
79 authenticated() {
80 return this.options.token != null;
81 }
82 /**
83 * Authenticate the container by email and password.
84 *
85 * @throws UNAUTHENTICATED_ERROR
86 */
87 async authenticate(options) {
88 try {
89 const resp = await request.post({
90 baseUrl: this.options.nodesworkServer,
91 uri: '/v1/u/user/login',
92 body: {
93 email: options.email,
94 password: options.password,
95 },
96 json: true,
97 jar: true,
98 });
99 LOG.debug('Login successfully');
100 }
101 catch (e) {
102 if (e.name === 'RequestError') {
103 throw new utils_1.NodesworkError('Server is not available', {
104 path: this.options.nodesworkServer + '/v1/u/user/login',
105 }, e);
106 }
107 else if (e.statusCode === 401) {
108 throw new utils_1.NodesworkError('Wrong password');
109 }
110 else if (e.statusCode === 422) {
111 throw new utils_1.NodesworkError('Wrong email');
112 }
113 else {
114 throw e;
115 }
116 }
117 try {
118 const mid = machineId();
119 LOG.debug('Got machine id:', mid);
120 const deviceIdentifier = crypto.createHash('md5')
121 .update(mid)
122 .update(options.email)
123 .digest('hex');
124 let operatingSystem = {
125 Darwin: 'MacOS',
126 Windows_NT: 'Windows',
127 Linux: 'Linux',
128 }[os.type()];
129 const device = {
130 deviceType: 'UserDevice',
131 deviceIdentifier,
132 os: operatingSystem,
133 osVersion: os.release(),
134 containerVersion,
135 name: options.deviceName,
136 };
137 LOG.debug('Collected device information:', device);
138 const resp = await request.post({
139 baseUrl: this.options.nodesworkServer,
140 uri: '/v1/u/devices',
141 body: device,
142 json: true,
143 jar: true,
144 });
145 LOG.debug('Device registered successfully', resp);
146 this.options.token = resp.token;
147 this.ls.setItemSync(APPLET_MANAGER_KEY, this.options);
148 LOG.debug('Save token to local', this.options);
149 await this.updateDevice();
150 }
151 catch (e) {
152 if (e.name === 'RequestError') {
153 throw new utils_1.NodesworkError('Server is not available', {
154 path: this.options.nodesworkServer + '/v1/u/user/login',
155 }, e);
156 }
157 else {
158 throw e;
159 }
160 }
161 return;
162 }
163 isStarted() {
164 return this.options.pid != null;
165 }
166 /**
167 * Start the container.
168 *
169 * @throws UNAUTHENTICATED_ERROR
170 */
171 async startServer() {
172 if (this.options.pid != null) {
173 console.log('daemon has already started');
174 return;
175 }
176 if (this.options.token == null) {
177 throw errors.UNAUTHENTICATED_ERROR;
178 }
179 await this.checkEnvironment();
180 // Start the applet manager.
181 this.options.pid = process.pid;
182 this.ls.setItemSync(APPLET_MANAGER_KEY, this.options);
183 server_1.app.appletManager = this;
184 server_1.server.listen(this.options.port);
185 socket_1.connectSocket(this.options.nodesworkServer, this.options.token, this);
186 LOG.info(`Server is started at http://localhost:${this.options.port}`);
187 await this.updateDevice();
188 }
189 /**
190 * Stop the container.
191 *
192 * @throws UNAUTHENTICATED_ERROR
193 */
194 async stopServer() {
195 // Stop the applet manager.
196 if (this.options.pid) {
197 process.kill(this.options.pid);
198 this.options.pid = null;
199 await utils_2.sleep(1000);
200 }
201 }
202 async install(options) {
203 const docker = new docker_cli_js_1.Docker();
204 const cmd = `build -t ${imageName(options)} ` +
205 `--build-arg base=${env.DOCKER_NODE_REPO} ` +
206 `--build-arg package=${options.packageName} ` +
207 `--build-arg version=${options.version} ` +
208 `docker/${options.naType}/${options.naVersion}`;
209 LOG.debug('Execute command to install applet', { cmd });
210 try {
211 const result = await docker.command(cmd);
212 LOG.debug('Execute build command log', result);
213 await this.updateDevice();
214 }
215 catch (e) {
216 throw e;
217 }
218 }
219 async images() {
220 const docker = new docker_cli_js_1.Docker();
221 const images = await docker.command('images');
222 return _.chain(images.images)
223 .filter((image) => {
224 const [na, naType, naVersion, ...others] = image.repository.split('-');
225 return na === 'na' && (naType === 'npm') && env.SUPPORTED_NA_NPM_VERSIONS.indexOf(naVersion) >= 0;
226 })
227 .map((image) => {
228 const [na, naType, naVersion, ...others] = image.repository.split('-');
229 return {
230 naType,
231 naVersion,
232 packageName: others.join('-'),
233 version: image.tag,
234 };
235 })
236 .value();
237 }
238 async run(options) {
239 await this.checkEnvironment();
240 const uniqueName = this.name(options);
241 const rmCmd = `rm ${uniqueName}`;
242 LOG.debug('Execute command to rm applet', { cmd: rmCmd });
243 try {
244 const docker = new docker_cli_js_1.Docker();
245 const result = await docker.command(rmCmd);
246 LOG.debug('Execute build command log', result);
247 }
248 catch (e) {
249 LOG.debug('Container does not exist');
250 }
251 const image = imageName(options);
252 const cmd = `run --name ${uniqueName} --network nodeswork -d -e ${applet.constants.environmentKeys.APPLET_ID}=${options.appletId} -e ${applet.constants.environmentKeys.APPLET_TOKEN}=${options.appletToken} ${image}`;
253 LOG.debug('Execute command to run applet', { cmd });
254 try {
255 const docker = new docker_cli_js_1.Docker();
256 const result = await docker.command(cmd);
257 LOG.debug('Execute run command result', result);
258 await this.updateDevice();
259 }
260 catch (e) {
261 LOG.error('Execute run command failed', e);
262 throw e;
263 }
264 }
265 async kill(options) {
266 const uniqueName = this.name(options);
267 const cmd = `stop ${uniqueName}`;
268 LOG.debug('Execute command to run applet', { cmd });
269 try {
270 const docker = new docker_cli_js_1.Docker();
271 const result = await docker.command(cmd);
272 LOG.debug('Execute build command log', result);
273 await this.updateDevice();
274 }
275 catch (e) {
276 throw e;
277 }
278 }
279 async ps() {
280 // await this.checkEnvironment();
281 const psResult = await this.docker.command('ps');
282 const psApplets = _.chain(psResult.containerList)
283 .map((container) => {
284 const image = parseAppletImage(container.names);
285 if (image == null) {
286 return null;
287 }
288 const port = parseMappingPort(container.ports) || 28900;
289 return _.extend(image, { port, status: container.status });
290 })
291 .filter(_.identity)
292 .value();
293 const networkResults = Object.values((await this.docker.command('network inspect nodeswork')).object[0].Containers);
294 return _.filter(psApplets, (psApplet) => {
295 const appletName = this.name(psApplet);
296 const networkResult = _.find(networkResults, (result) => {
297 return result.Name === appletName;
298 });
299 if (networkResult == null) {
300 LOG.warn(`Applet ${appletName} is running but not in the correct network`);
301 return false;
302 }
303 psApplet.ip = networkResult.IPv4Address.split('/')[0];
304 return true;
305 });
306 }
307 async refreshWorkerCrons() {
308 const self = this;
309 try {
310 const userApplets = await this.serverApi.get({
311 uri: '/v1/d/user-applets',
312 });
313 const newJobs = _
314 .chain(userApplets)
315 .map((ua) => {
316 const appletConfig = ua.config.appletConfig;
317 const image = {
318 naType: appletConfig.naType,
319 naVersion: appletConfig.naVersion,
320 packageName: appletConfig.packageName,
321 version: appletConfig.version,
322 };
323 return _.map(appletConfig.workers, (workerConfig) => {
324 const worker = {
325 handler: workerConfig.handler,
326 name: workerConfig.name,
327 };
328 return {
329 jobUUID: uuid([
330 ua.applet._id,
331 ua._id,
332 image.naType,
333 image.naVersion,
334 image.packageName,
335 image.version,
336 worker.handler,
337 worker.name,
338 ].join(':'), UUID_NAMESPACE),
339 appletId: ua.applet._id,
340 userApplet: ua._id,
341 image,
342 worker,
343 schedule: workerConfig.schedule,
344 };
345 });
346 })
347 .flatten()
348 .filter((x) => x.schedule != null)
349 .value();
350 LOG.debug('Fetch applets for current device successfully', newJobs);
351 for (const cron of this.cronJobs) {
352 const u = _.find(newJobs, (newJob) => newJob.jobUUID === cron.jobUUID);
353 if (u == null) {
354 cron.cronJob.stop();
355 LOG.info('Stop cron job successfully', _.omit(cron, 'cronJob'));
356 }
357 }
358 for (const newJob of newJobs) {
359 const cron = _.find(this.cronJobs, (c) => newJob.jobUUID === c.jobUUID);
360 if (cron == null) {
361 const cronJob = (function (c) {
362 try {
363 c.cronJob = new cron_1.CronJob({
364 cronTime: c.schedule,
365 onTick: async () => {
366 LOG.debug('Run cron job', _.omit(c, 'cronJob'));
367 try {
368 await self.executeCronJob(c);
369 LOG.info('Run cron job successfully', _.omit(c, 'cronJob'));
370 }
371 catch (e) {
372 LOG.error('Run cron job failed', _.pick(e, 'name', 'statusCode', 'error', 'message'), _.omit(c, 'cronJob'));
373 }
374 },
375 start: true,
376 });
377 }
378 catch (e) {
379 LOG.error('Create cron job failed', _.omit(c, 'cronJob'));
380 }
381 LOG.info('Create cron job successfully', _.omit(c, 'cronJob'));
382 return c;
383 })(newJob);
384 this.cronJobs.push(cronJob);
385 }
386 }
387 this.cronJobs = _.filter(this.cronJobs, (cronJob) => {
388 return cronJob.cronJob.running;
389 });
390 }
391 catch (e) {
392 throw e;
393 }
394 }
395 async executeCronJob(job) {
396 try {
397 const accounts = await this.serverApi.get({
398 uri: `/v1/d/user-applets/${job.userApplet}/accounts`,
399 });
400 LOG.debug('Fetch accounts successfully', accounts);
401 const payload = {
402 accounts,
403 };
404 const result = await this.work({
405 userApplet: job.userApplet,
406 route: {
407 appletId: job.appletId,
408 naType: job.image.naType,
409 naVersion: job.image.naVersion,
410 packageName: job.image.packageName,
411 version: job.image.version,
412 },
413 worker: job.worker,
414 payload,
415 });
416 LOG.info('Execute cron job successfully.', {
417 job: _.omit(job, 'cronJob'),
418 result,
419 });
420 }
421 catch (e) {
422 throw e;
423 }
424 }
425 async updateExecutionMetrics(executionId, options) {
426 return await this.serverApi.post({
427 uri: `/v1/d/executions/${executionId}/metrics`,
428 body: {
429 dimensions: options.dimensions,
430 name: options.name,
431 value: options.value,
432 },
433 });
434 }
435 async work(options) {
436 LOG.debug('Get work request', options);
437 const execution = await this.serverApi.post({
438 uri: `/v1/d/user-applets/${options.userApplet}/execute`,
439 body: {
440 worker: options.worker,
441 },
442 });
443 const headers = {};
444 headers[applet.constants.headers.request.EXECUTION_ID] = execution._id;
445 const requestOptions = {
446 appletId: options.route.appletId,
447 naType: options.route.naType,
448 naVersion: options.route.naVersion,
449 packageName: options.route.packageName,
450 version: options.route.version,
451 uri: `/workers/${options.worker.handler}/${options.worker.name}`,
452 method: 'POST',
453 body: options.payload,
454 headers,
455 };
456 try {
457 const result = await this.request(requestOptions);
458 this.updateExecutionMetrics(execution._id, {
459 dimensions: {
460 status: 'SUCCESS',
461 },
462 name: 'result',
463 value: utils_1.metrics.Count(1),
464 });
465 return result;
466 }
467 catch (e) {
468 this.updateExecutionMetrics(execution._id, {
469 dimensions: {
470 status: 'ERROR',
471 },
472 name: 'result',
473 value: utils_1.metrics.Count(1),
474 });
475 throw e;
476 }
477 }
478 async request(options) {
479 LOG.info('Get request', { options });
480 const routeAddress = await this.route(options);
481 if (routeAddress == null) {
482 throw new utils_1.NodesworkError('Applet is not running');
483 }
484 const headers = _.extend({}, options.headers);
485 headers[sbase.constants.headers.request.NODESWORK_FORWARDED_TO] = (routeAddress.route);
486 const requestOptions = {
487 uri: routeAddress.target + options.uri,
488 method: options.method,
489 proxy: routeAddress.target,
490 body: options.body,
491 headers,
492 json: true,
493 };
494 LOG.debug('Request options', requestOptions);
495 const resp = await request(requestOptions);
496 LOG.debug('Request response', resp);
497 return resp;
498 }
499 async operateAccount(options) {
500 const requestOptions = {
501 uri: `/v1/d/applets/${options.appletId}/accounts/${options.accountId}/operate`,
502 body: options.body,
503 };
504 return await this.serverApi.post(requestOptions);
505 }
506 async route(options) {
507 if (this.options.dev) {
508 try {
509 const devServer = await request({
510 uri: 'http://localhost:28900/sstats',
511 json: true,
512 });
513 if (devServer.applet &&
514 devServer.applet.packageName === options.packageName &&
515 compareVersion(devServer.applet.packageVersion, options.version) >= 0) {
516 return {
517 route: 'localhost:28900',
518 target: 'http://localhost:28900',
519 };
520 }
521 }
522 catch (e) {
523 // Fallback
524 }
525 }
526 return {
527 route: `${this.name(options)}:28900`,
528 target: paths_1.containerProxyUrl,
529 };
530 }
531 async updateDevice() {
532 if (this.options.token == null) {
533 throw errors.UNAUTHENTICATED_ERROR;
534 }
535 const installedApplets = await this.images();
536 const runningApplets = await this.ps();
537 try {
538 const resp = await this.serverApi.post({
539 uri: '/v1/d/devices',
540 body: {
541 installedApplets,
542 runningApplets,
543 },
544 });
545 LOG.debug('Update device successfully');
546 }
547 catch (e) {
548 throw e;
549 }
550 await this.refreshWorkerCrons();
551 }
552 async checkEnvironment() {
553 // Step 1: Check network configuration
554 const networks = await this.docker.command('network ls');
555 const targetNetwork = _.find(networks.network, (c) => c.name === 'nodeswork');
556 if (targetNetwork == null) {
557 LOG.debug('network is not setup, creating');
558 await this.docker.command('network create nodeswork');
559 }
560 const inspect = await this.docker.command('network inspect nodeswork');
561 LOG.debug('inspecting network', inspect.object[0]);
562 const IPAMConfig = inspect.object[0].IPAM.Config[0];
563 this.network = {
564 subnet: IPAMConfig.Subnet,
565 gateway: IPAMConfig.Gateway,
566 containers: inspect.object[0].Containers,
567 };
568 LOG.debug('Network configuration', this.network);
569 // Step 2: Check pre installed containers
570 // Step 2.1: Check proxy container
571 const containers = await this.docker.command('ps');
572 const proxyContainer = _.find(containers.containerList, (container) => container.names === 'nodeswork-container-proxy');
573 if (proxyContainer == null) {
574 LOG.debug('Container proxy is not running, starting');
575 await this.installContainerProxy();
576 }
577 else {
578 const version = proxyContainer.image.split(':')[1];
579 const lVersion = await latestVersion('@nodeswork/container-proxy');
580 this.containerProxy = {
581 version,
582 latestVersion: lVersion,
583 };
584 }
585 const proxyInNetwork = _.find(this.network.containers, (container) => {
586 return container.Name === 'nodeswork-container-proxy';
587 });
588 if (proxyInNetwork == null) {
589 LOG.debug('Proxy container is not in network');
590 await this.docker.command(`network connect nodeswork nodeswork-container-proxy`);
591 }
592 LOG.debug('Container Proxy configuration', this.containerProxy);
593 // await this.ensureMongo({ prefix: 'nodeswork', port: 28330 });
594 LOG.debug('Environment setup correctly');
595 }
596 async ensureMongo(options) {
597 LOG.debug('Ensure mongo', options);
598 const name = `${options.prefix}-mongo`;
599 let containers = await this.docker.command('ps');
600 let container = _.find(containers.containerList, (c) => {
601 return c.names === name;
602 });
603 if (container != null) {
604 LOG.debug('Mongo is already running');
605 return options.port;
606 }
607 LOG.debug('Mongo is not running');
608 containers = await this.docker.command('ps -a');
609 container = _.find(containers.containerList, (c) => {
610 return c.names === name;
611 });
612 if (container != null) {
613 LOG.debug('Mongo is stopped, starting');
614 await this.docker.command(`start ${name}`);
615 return options.port;
616 }
617 LOG.debug('Start Mongo instance');
618 await this.docker.command(`run --name ${name} -p ${options.port}:27017 -d mongo`);
619 LOG.debug('Mongo is running');
620 return options.port;
621 }
622 name(options) {
623 return `na-${options.naType}-${options.naVersion}-${options.packageName}_${options.version}-${options.appletId}`;
624 }
625 async installContainerProxy() {
626 const version = await latestVersion('@nodeswork/container-proxy');
627 LOG.debug('Fetched latest version container-proxy', { version });
628 const output = await this.docker.command(`build -t nodeswork-container-proxy:${version} docker/container-proxy --build-arg version=${version} --build-arg base=${env.DOCKER_NODE_REPO}`);
629 LOG.debug('Building container proxy', output);
630 try {
631 await this.docker.command(`rm nodeswork-container-proxy`);
632 }
633 catch (e) {
634 LOG.debug('Remove container proxy error', e);
635 }
636 try {
637 // sudo ifconfig lo0 alias 172.16.222.111
638 await this.docker.command(`run --name nodeswork-container-proxy -d -e NAM_HOST=172.16.222.111:28310 -e SUB_NET=${this.network.subnet} -p 28320:28320 nodeswork-container-proxy:${version}`);
639 }
640 catch (e) {
641 LOG.debug('Remove container proxy error', e);
642 }
643 this.containerProxy = { version, latestVersion: version };
644 }
645}
646exports.AppletManager = AppletManager;
647function imageName(image) {
648 return `na-${image.naType}-${image.naVersion}-${image.packageName}\
649:${image.version}`;
650}
651function parseAppletImage(name) {
652 const result = NAME_REGEX.exec(name);
653 if (result == null) {
654 return null;
655 }
656 return {
657 naType: result[1],
658 naVersion: result[2],
659 packageName: result[3],
660 version: result[4],
661 appletId: result[5],
662 };
663}
664function parseMappingPort(ports) {
665 return parseInt(ports.split(':')[1]);
666}
667
668//# sourceMappingURL=applet-manager.js.map