cloudron 5.4.2 → 5.6.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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  'use strict';
4
4
 
5
- const program = require('commander'),
5
+ const { program } = require('commander'),
6
6
  appstoreActions = require('../src/appstore-actions.js');
7
7
 
8
8
  program.version(require('../package.json').version);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudron",
3
- "version": "5.4.2",
3
+ "version": "5.6.0",
4
4
  "license": "MIT",
5
5
  "description": "Cloudron Commandline Tool",
6
6
  "main": "main.js",
@@ -10,40 +10,39 @@
10
10
  "url": "https://git.cloudron.io/cloudron/cloudron-cli.git"
11
11
  },
12
12
  "scripts": {
13
- "test": "./node_modules/.bin/mocha test/test.js"
13
+ "test": "mocha test/test.js"
14
14
  },
15
15
  "bin": {
16
- "cloudron": "./bin/cloudron"
16
+ "cloudron": "bin/cloudron"
17
17
  },
18
18
  "author": "Cloudron Developers <support@cloudron.io>",
19
19
  "dependencies": {
20
- "async": "^3.2.4",
21
- "cloudron-manifestformat": "^5.20.0",
22
- "commander": "^9.5.0",
23
- "debug": "^4.3.4",
24
- "delay": "^5.0.0",
20
+ "async": "^3.2.5",
21
+ "cloudron-manifestformat": "^5.24.0",
22
+ "commander": "^12.1.0",
23
+ "debug": "^4.3.5",
25
24
  "easy-table": "^1.2.0",
26
- "ejs": "^3.1.9",
25
+ "ejs": "^3.1.10",
27
26
  "eventsource": "^2.0.2",
28
- "micromatch": "^4.0.5",
27
+ "micromatch": "^4.0.7",
29
28
  "once": "^1.4.0",
30
29
  "open": "^8.4.0",
31
30
  "progress": "^2.0.3",
32
31
  "progress-stream": "^2.0.0",
33
32
  "readline-sync": "^1.4.10",
34
- "safetydance": "^2.2.0",
35
- "split": "^1.0.1",
36
- "superagent": "^8.0.9",
33
+ "safetydance": "^2.4.0",
34
+ "split2": "^4.2.0",
35
+ "superagent": "^9.0.2",
37
36
  "tar-fs": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.12.0.tgz",
38
37
  "underscore": "^1.13.6"
39
38
  },
40
39
  "engines": {
41
- "node": ">= 14.x.x"
40
+ "node": ">= 18.x.x"
42
41
  },
43
42
  "devDependencies": {
44
43
  "expect.js": "^0.3.1",
45
44
  "memorystream": "^0.3.1",
46
- "mocha": "^10.2.0",
47
- "rimraf": "^3.0.2"
45
+ "mocha": "^10.4.0",
46
+ "rimraf": "^5.0.7"
48
47
  }
49
48
  }
package/src/actions.js CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  const assert = require('assert'),
4
4
  config = require('./config.js'),
5
- delay = require('delay'),
6
5
  ejs = require('ejs'),
7
6
  { exit, locateManifest } = require('./helper.js'),
8
7
  EventSource = require('eventsource'),
@@ -17,10 +16,12 @@ const assert = require('assert'),
17
16
  readlineSync = require('readline-sync'),
18
17
  safe = require('safetydance'),
19
18
  spawn = require('child_process').spawn,
20
- split = require('split'),
19
+ semver = require('semver'),
20
+ split = require('split2'),
21
21
  superagent = require('superagent'),
22
22
  Table = require('easy-table'),
23
23
  tar = require('tar-fs'),
24
+ timers = require('timers/promises'),
24
25
  zlib = require('zlib'),
25
26
  _ = require('underscore');
26
27
 
@@ -109,9 +110,10 @@ async function selectDomain(location, options) {
109
110
 
110
111
  let domain;
111
112
  let subdomain = location;
112
- let matchingDomain = domains
113
+ // find longest matching domain name. this maps to DNS where subdomain is a zone of it's own
114
+ const matchingDomain = domains
113
115
  .map(function (d) { return d.domain; } )
114
- .sort(function(a, b) { return a.length < b.length; })
116
+ .sort(function(a, b) { return b.length - a.length; })
115
117
  .find(function (d) { return location.endsWith(d); });
116
118
 
117
119
  if (matchingDomain) {
@@ -120,7 +122,7 @@ async function selectDomain(location, options) {
120
122
  } else { // use the admin domain
121
123
  domain = domains
122
124
  .map(function (d) { return d.domain; } )
123
- .sort(function(a, b) { return a.length < b.length; })
125
+ .sort(function(a, b) { return b.length - a.length; })
124
126
  .find(function (d) { return adminFqdn.endsWith(d); });
125
127
  }
126
128
 
@@ -146,7 +148,7 @@ async function selectAppWithRepository(repository, options) {
146
148
  const response = await createRequest('GET', '/api/v1/apps', options);
147
149
  if (response.statusCode !== 200) throw new Error(`Failed to install app: ${requestError(response)}`);
148
150
 
149
- let matchingApps = response.body.apps.filter(function (app) {
151
+ const matchingApps = response.body.apps.filter(function (app) {
150
152
  return !app.appStoreId && app.manifest.dockerImage.startsWith(repository); // never select apps from the store
151
153
  });
152
154
 
@@ -161,7 +163,6 @@ async function selectAppWithRepository(repository, options) {
161
163
  });
162
164
 
163
165
  let index = -1;
164
- // eslint-disable-next-line no-constant-condition
165
166
  while (true) {
166
167
  index = parseInt(readlineSync.question('Choose app [0-' + (matchingApps.length-1) + ']: ', {}), 10);
167
168
  if (isNaN(index) || index < 0 || index > matchingApps.length-1) console.log('Invalid selection');
@@ -215,9 +216,8 @@ async function waitForHealthy(appId, options) {
215
216
 
216
217
  process.stdout.write('\n => ' + 'Wait for health check ');
217
218
 
218
- // eslint-disable-next-line no-constant-condition
219
219
  while (true) {
220
- await delay(1000);
220
+ await timers.setTimeout(1000);
221
221
  const response = await createRequest('GET', `/api/v1/apps/${appId}`, options);
222
222
  if (response.statusCode !== 200) throw new Error(`Failed to get app: ${requestError(response)}`);
223
223
 
@@ -240,7 +240,6 @@ async function waitForTask(taskId, options) {
240
240
 
241
241
  let currentMessage = '';
242
242
 
243
- // eslint-disable-next-line no-constant-condition
244
243
  while (true) {
245
244
  const response = await createRequest('GET', `/api/v1/tasks/${taskId}`, options);
246
245
  if (response.statusCode !== 200) throw new Error(`Failed to get task: ${requestError(response)}`);
@@ -271,7 +270,7 @@ async function waitForTask(taskId, options) {
271
270
 
272
271
  currentMessage = message;
273
272
 
274
- await delay(1000);
273
+ await timers.setTimeout(1000);
275
274
  }
276
275
  }
277
276
 
@@ -522,8 +521,13 @@ function parseDebugCommand(cmd) {
522
521
  }
523
522
 
524
523
  async function downloadManifest(appstoreId) {
525
- const parts = appstoreId.split('@');
526
- const url = config.appStoreOrigin() + '/api/v1/apps/' + parts[0] + (parts[1] ? '/versions/' + parts[1] : '');
524
+ const [ id, version ] = appstoreId.split('@');
525
+
526
+ if (!manifestFormat.isId(id)) throw new Error('Invalid appstore ID');
527
+ if (!semver.valid(version)) throw new Error('Invalid appstore version, not a semver');
528
+
529
+ // encodeURIComponent required to protect against passing junk in the URL (e.g appstoreId=org.wordpress.cloudronapp?foo=bar)
530
+ const url = config.appStoreOrigin() + '/api/v1/apps/' + id + (version ? '/versions/' + version : '');
527
531
 
528
532
  const [error, response] = await safe(superagent.get(url).ok(() => true));
529
533
  if (error) throw new Error(`Failed to list apps from appstore: ${error.message}`);
@@ -597,7 +601,7 @@ async function install(localOptions, cmd) {
597
601
 
598
602
  for (const binding in secondaryDomains) console.log(`Secondary domain ${binding}: ${secondaryDomains[binding].subdomain}.${secondaryDomains[binding].domain}`);
599
603
 
600
- let aliasDomains = [];
604
+ const aliasDomains = [];
601
605
  if (options.aliasDomains) {
602
606
  for (const aliasDomain of options.aliasDomains.split(',')) {
603
607
  aliasDomains.push(await selectDomain(aliasDomain, options));
@@ -611,7 +615,7 @@ async function install(localOptions, cmd) {
611
615
  if (typeof options.portBindings === 'string') {
612
616
  portBindings = { };
613
617
  options.portBindings.split(',').forEach(function (kv) {
614
- let tmp = kv.split('=');
618
+ const tmp = kv.split('=');
615
619
  if (isNaN(parseInt(tmp[1], 10))) return; // disable the port
616
620
  portBindings[tmp[0]] = parseInt(tmp[1], 10);
617
621
  });
@@ -620,7 +624,7 @@ async function install(localOptions, cmd) {
620
624
  }
621
625
  } else { // just put in defaults
622
626
  const allPorts = _.extend({}, manifest.tcpPorts, manifest.udpPorts);
623
- for (let portName in allPorts) {
627
+ for (const portName in allPorts) {
624
628
  portBindings[portName] = allPorts[portName].defaultValue;
625
629
  }
626
630
  }
@@ -681,7 +685,7 @@ async function configure(localOptions, cmd) {
681
685
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
682
686
 
683
687
  if (!options.location) options.location = readlineSync.question(`Enter new location (default: ${app.fqdn}): `, { });
684
- let location = options.location || app.subdomain;
688
+ const location = options.location || app.subdomain;
685
689
 
686
690
  const domainObject = await selectDomain(location, options);
687
691
 
@@ -743,7 +747,7 @@ async function configure(localOptions, cmd) {
743
747
  portBindings = queryPortBindings(app, app.manifest);
744
748
  }
745
749
 
746
- for (let binding in portBindings) {
750
+ for (const binding in portBindings) {
747
751
  console.log('%s: %s', binding, portBindings[binding]);
748
752
  }
749
753
 
@@ -963,7 +967,7 @@ async function logs(localOptions, cmd) {
963
967
  }
964
968
 
965
969
  if (tail) {
966
- const url = `${apiPath}?access_token=${token}&lines=10`;
970
+ const url = `${apiPath}?access_token=${token}&lines=10&format=json`;
967
971
  var es = new EventSource(url, { rejectUnauthorized }); // not sure why this is needed
968
972
 
969
973
  es.on('message', function (e) { // e { type, data, lastEventId }. lastEventId is the timestamp
@@ -976,16 +980,21 @@ async function logs(localOptions, cmd) {
976
980
  exit(error);
977
981
  });
978
982
  } else {
979
- const url = `${apiPath}?access_token=${token}&lines=${lines}`;
983
+ const url = `${apiPath}?access_token=${token}&lines=${lines}&format=json`;
980
984
 
981
985
  const req = superagent.get(url, { rejectUnauthorized });
982
986
  req.on('response', function (response) {
983
987
  if (response.statusCode !== 200) return exit(`Failed to get logs: ${requestError(response)}`);
984
988
  });
985
- req.pipe(split(JSON.parse))
989
+ req.on('error', (error) => exit(`Pipe error: ${error.message}`));
990
+
991
+ const jsonStream = split((line) => JSON.parse(line));
992
+ jsonStream
986
993
  .on('data', logPrinter)
987
- .on('error', process.exit)
994
+ .on('error', (error) => exit(`JSON parse error: ${error.message}`))
988
995
  .on('end', process.exit);
996
+
997
+ req.pipe(jsonStream);
989
998
  }
990
999
  }
991
1000
 
@@ -1012,7 +1021,7 @@ async function inspect(localOptions, cmd) {
1012
1021
  const response = await createRequest('GET', '/api/v1/apps', options);
1013
1022
  if (response.statusCode !== 200) return exit(`Failed to list apps: ${requestError(response)}`);
1014
1023
 
1015
- let apps = [];
1024
+ const apps = [];
1016
1025
 
1017
1026
  for (const app of response.body.apps) {
1018
1027
  const response2 = await createRequest('GET', `/api/v1/apps/${app.id}`, options);
@@ -1171,7 +1180,8 @@ async function importApp(localOptions, cmd) {
1171
1180
  let data = {};
1172
1181
 
1173
1182
  if (!options.inPlace) {
1174
- let { remotePath, backupFormat, backupConfig, backupPath, backupKey } = options;
1183
+ let { remotePath, backupFormat, backupConfig } = options;
1184
+ const { backupPath, backupKey } = options;
1175
1185
  let backupFolder;
1176
1186
 
1177
1187
  if (backupPath) {
@@ -1223,7 +1233,7 @@ async function exportApp(localOptions, cmd) {
1223
1233
  const app = await getApp(options);
1224
1234
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
1225
1235
 
1226
- let data = {};
1236
+ const data = {};
1227
1237
 
1228
1238
  if (!options.snapshot) return exit('--snapshot is required');
1229
1239
 
@@ -1301,7 +1311,7 @@ function demuxStream(stream, stdout, stderr) {
1301
1311
  // cat ~/tmp/fantome.tar.gz | cloudron exec -- bash -c "tar zxf - -C /tmp" - must extrack ok
1302
1312
  async function exec(args, localOptions, cmd) {
1303
1313
  let stdin = localOptions._stdin || process.stdin; // hack for 'push', 'pull' to reuse this function
1304
- let stdout = localOptions._stdout || process.stdout; // hack for 'push', 'pull' to reuse this function
1314
+ const stdout = localOptions._stdout || process.stdout; // hack for 'push', 'pull' to reuse this function
1305
1315
 
1306
1316
  const options = cmd.optsWithGlobals();
1307
1317
  let tty = !!options.tty;
@@ -1550,9 +1560,9 @@ async function envSet(envVars, localOptions, cmd) {
1550
1560
  const response = await createRequest('GET', `/api/v1/apps/${app.id}`, options);
1551
1561
  if (response.statusCode !== 200) return exit(`Failed to get app: ${requestError(response)}`);
1552
1562
 
1553
- let env = response.body.env;
1563
+ const env = response.body.env;
1554
1564
  envVars.forEach(envVar => {
1555
- let m = envVar.match(/(.*?)=(.*)/);
1565
+ const m = envVar.match(/(.*?)=(.*)/);
1556
1566
  if (!m) return exit(`Expecting KEY=VALUE pattern. Got ${envVar}`);
1557
1567
  env[m[1]] = m[2];
1558
1568
  });
@@ -1572,7 +1582,7 @@ async function envUnset(envNames, localOptions, cmd) {
1572
1582
  const response = await createRequest('GET', `/api/v1/apps/${app.id}`, options);
1573
1583
  if (response.statusCode !== 200) return exit(`Failed to get app: ${requestError(response)}`);
1574
1584
 
1575
- let env = response.body.env;
1585
+ const env = response.body.env;
1576
1586
  envNames.forEach(name => delete env[name]);
1577
1587
 
1578
1588
  await setEnv(app, env, options);