UNPKG

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