UNPKG

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