cloudron 4.13.0-1 → 4.14.1

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.
package/bin/cloudron CHANGED
@@ -27,6 +27,7 @@ function collectArgs(value, collected) {
27
27
  return collected;
28
28
  }
29
29
 
30
+ // TODO when updating to commander v8 we require https://github.com/tj/commander.js/pull/1670
30
31
  program.version(version)
31
32
  .option('--server <server>', 'Cloudron domain')
32
33
  .option('--token <token>', 'Cloudron token')
@@ -73,6 +74,7 @@ program.command('configure')
73
74
  .option('--no-wait', 'Wait for healthcheck to succeed [false]', false)
74
75
  .option('-p, --port-bindings [PORT=port,...]', 'Query port bindings')
75
76
  .option('-l, --location <location>', 'Location')
77
+ .option('-s, --secondary-domains [DOMAIN=domain,...]', 'Query/Set secondary domains')
76
78
  .action(actions.configure);
77
79
 
78
80
  program.command('debug [cmd...]')
@@ -128,8 +130,9 @@ program.command('install')
128
130
  .description('Install or update app')
129
131
  .option('--image <docker image>', 'Docker image')
130
132
  .option('--no-wait', 'Wait for healthcheck to succeed [false]', false)
131
- .option('-p, --port-bindings [PORT=port,...]', 'Query port bindings')
133
+ .option('-p, --port-bindings [PORT=port,...]', 'Query/Set port bindings')
132
134
  .option('-l, --location <domain>', 'Subdomain or full domain')
135
+ .option('-s, --secondary-domains [DOMAIN=domain,...]', 'Query/Set secondary domains')
133
136
  .option('--appstore-id <appid[@version]>', 'Use app from the store')
134
137
  .option('--no-sso', 'Disable Cloudron SSO [false]', false)
135
138
  .option('--debug [cmd]', 'Enable debug mode')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudron",
3
- "version": "4.13.0-1",
3
+ "version": "4.14.1",
4
4
  "license": "MIT",
5
5
  "description": "Cloudron Commandline Tool",
6
6
  "main": "main.js",
@@ -18,7 +18,7 @@
18
18
  "author": "Cloudron Developers <support@cloudron.io>",
19
19
  "dependencies": {
20
20
  "async": "^3.2.3",
21
- "cloudron-manifestformat": "^5.14.0",
21
+ "cloudron-manifestformat": "^5.15.0",
22
22
  "commander": "^6.1.0",
23
23
  "debug": "^4.3.3",
24
24
  "delay": "^5.0.0",
package/src/actions.js CHANGED
@@ -67,6 +67,10 @@ const NO_APP_FOUND_ERROR_STRING = 'Could not determine app. Use --app to specify
67
67
  // options for the request module
68
68
  function requestOptions(options) {
69
69
  const adminFqdn = options.parent.server || config.apiEndpoint();
70
+
71
+ // ensure config can return the correct section
72
+ config.setActive(adminFqdn);
73
+
70
74
  const token = options.parent.token || config.token();
71
75
  const rejectUnauthorized = !(options.parent.allowSelfsigned || options.parent.acceptSelfsigned || config.allowSelfsigned());
72
76
 
@@ -82,7 +86,7 @@ function createRequest(method, apiPath, options) {
82
86
  if (url.includes('?')) url += '&'; else url += '?';
83
87
  url += `access_token=${token}`;
84
88
  const request = superagent(method, url);
85
- if (rejectUnauthorized) request.disableTLSCerts();
89
+ if (!rejectUnauthorized) request.disableTLSCerts();
86
90
  request.ok(() => true);
87
91
  return request;
88
92
  }
@@ -106,6 +110,7 @@ async function selectDomain(location, options) {
106
110
  const domains = response.body.domains;
107
111
 
108
112
  let domain;
113
+ let subdomain = location;
109
114
  let matchingDomain = domains
110
115
  .map(function (d) { return d.domain; } )
111
116
  .sort(function(a, b) { return a.length < b.length; })
@@ -113,7 +118,7 @@ async function selectDomain(location, options) {
113
118
 
114
119
  if (matchingDomain) {
115
120
  domain = matchingDomain;
116
- location = location.slice(0, -matchingDomain.length-1);
121
+ subdomain = location.slice(0, -matchingDomain.length-1);
117
122
  } else { // use the admin domain
118
123
  domain = domains
119
124
  .map(function (d) { return d.domain; } )
@@ -121,7 +126,7 @@ async function selectDomain(location, options) {
121
126
  .find(function (d) { return adminFqdn.endsWith(d); });
122
127
  }
123
128
 
124
- return { location, domain };
129
+ return { subdomain, domain };
125
130
  }
126
131
 
127
132
  async function stopActiveTask(app, options) {
@@ -197,7 +202,7 @@ async function getApp(options) {
197
202
  const response = await createRequest('GET', '/api/v1/apps', options);
198
203
  if (response.statusCode !== 200) throw new Error(`Failed to get apps: ${requestError(response)}`);
199
204
 
200
- const match = response.body.apps.filter(function (m) { return m.location === app || m.fqdn === app; });
205
+ const match = response.body.apps.filter(function (m) { return m.subdomain === app || m.location === app || m.fqdn === app; });
201
206
  if (match.length == 0) throw new Error(`App at location ${app} not found`);
202
207
 
203
208
  return match[0];
@@ -363,11 +368,15 @@ async function login(adminFqdn, options) {
363
368
 
364
369
  config.setActive(adminFqdn);
365
370
 
371
+ const rejectUnauthorized = !(options.parent.allowSelfsigned || options.parent.acceptSelfsigned);
366
372
  let token = config.token();
367
373
  if (token) { // check if the token is not expired
368
- const [error, response] = await safe(superagent.get(`https://${adminFqdn}/api/v1/profile?access_token=${token}`)
374
+ const request = superagent.get(`https://${adminFqdn}/api/v1/profile?access_token=${token}`)
369
375
  .timeout(60000)
370
- .ok(() => true));
376
+ .ok(() => true);
377
+ if (!rejectUnauthorized) request.disableTLSCerts();
378
+
379
+ const [error, response] = await safe(request);
371
380
  if (error) return exit(error);
372
381
  if (response.status === 200) {
373
382
  console.log('Existing token still valid.');
@@ -380,7 +389,6 @@ async function login(adminFqdn, options) {
380
389
  if (!token) {
381
390
  const username = options.username || readlineSync.question('Username: ', {});
382
391
  const password = options.password || readlineSync.question('Password: ', { noEchoBack: true });
383
- const rejectUnauthorized = !(options.parent.allowSelfsigned || options.parent.acceptSelfsigned);
384
392
 
385
393
  const [error, result] = await safe(authenticate(adminFqdn, username, password, { rejectUnauthorized, askForTotpToken: false }));
386
394
  if (error) return exit(`Failed to login: ${error.message}`);
@@ -448,12 +456,25 @@ async function list(options) {
448
456
  console.log(t.toString());
449
457
  }
450
458
 
459
+ async function querySecondaryDomains(app, manifest, options) {
460
+ const secondaryDomains = {};
461
+
462
+ if(!manifest.httpPorts) return secondaryDomains;
463
+
464
+ for (const env in manifest.httpPorts) {
465
+ const defaultDomain = (app && app.secondaryDomains && app.secondaryDomains[env]) ? app.secondaryDomains[env] : (manifest.httpPorts[env].defaultValue || '');
466
+ const input = readlineSync.question(`${manifest.httpPorts[env].description} (default: "${defaultDomain}"): `, {});
467
+ secondaryDomains[env] = await selectDomain(input, options);
468
+ }
469
+ return secondaryDomains;
470
+ }
471
+
451
472
  function queryPortBindings(app, manifest) {
452
- const portBindings = { };
473
+ const portBindings = {};
453
474
  const allPorts = _.extend({}, manifest.tcpPorts, manifest.udpPorts);
454
- for (var env in allPorts) {
455
- var defaultPort = (app && app.portBindings && app.portBindings[env]) ? app.portBindings[env] : (allPorts[env].defaultValue || '');
456
- var port = readlineSync.question(allPorts[env].description + ' (default ' + env + '=' + defaultPort + '. "x" to disable): ', {});
475
+ for (const env in allPorts) {
476
+ const defaultPort = (app && app.portBindings && app.portBindings[env]) ? app.portBindings[env] : (allPorts[env].defaultValue || '');
477
+ const port = readlineSync.question(allPorts[env].description + ' (default ' + env + '=' + defaultPort + '. "x" to disable): ', {});
457
478
  if (port === '') {
458
479
  portBindings[env] = defaultPort;
459
480
  } else if (isNaN(parseInt(port, 10))) {
@@ -530,6 +551,27 @@ async function install(options) {
530
551
 
531
552
  const domainObject = await selectDomain(location, options);
532
553
 
554
+ // secondary domains
555
+ let secondaryDomains = {};
556
+ if (options.secondaryDomains) {
557
+ // ask the user for port values if the ports are different in the app and the manifest
558
+ if (typeof options.secondaryDomains === 'string') {
559
+ secondaryDomains = {};
560
+ for (const kv of options.secondaryDomains.split(',')) {
561
+ const tmp = kv.split('=');
562
+ secondaryDomains[tmp[0]] = await selectDomain(tmp[1], options);
563
+ }
564
+ } else {
565
+ secondaryDomains = await querySecondaryDomains(null /* existing app */, manifest, options);
566
+ }
567
+ } else if (manifest.httpPorts) { // just put in defaults
568
+ for (const env in manifest.httpPorts) {
569
+ secondaryDomains[env] = await selectDomain(manifest.httpPorts[env].defaultValue, options);
570
+ }
571
+ }
572
+
573
+ for (const binding in secondaryDomains) console.log(`Secondary domain ${binding}: ${secondaryDomains[binding].subdomain}.${secondaryDomains[binding].domain}`);
574
+
533
575
  // port bindings
534
576
  let portBindings = {};
535
577
  if (options.portBindings) {
@@ -551,16 +593,16 @@ async function install(options) {
551
593
  }
552
594
  }
553
595
 
554
- for (let binding in portBindings) {
555
- console.log('%s: %s', binding, portBindings[binding]);
556
- }
596
+ for (const binding in portBindings) console.log(`Port ${binding}: ${portBindings[binding]}`);
557
597
 
558
598
  const data = {
559
599
  appStoreId: options.appstoreId || '', // note case change
560
600
  manifest: options.appstoreId ? null : manifest, // cloudron ignores manifest anyway if appStoreId is set
561
- location: domainObject.location,
601
+ location: domainObject.subdomain, // LEGACY
602
+ subdomain: domainObject.subdomain,
562
603
  domain: domainObject.domain,
563
- portBindings: portBindings,
604
+ secondaryDomains,
605
+ portBindings,
564
606
  accessRestriction: null
565
607
  };
566
608
 
@@ -610,20 +652,46 @@ async function configure(options) {
610
652
 
611
653
  const domainObject = await selectDomain(location, options);
612
654
 
655
+ const secondaryDomains = {};
656
+ app.secondaryDomains.forEach(sd => {
657
+ secondaryDomains[sd.environmentVariable] = {
658
+ subdomain: sd.subdomain,
659
+ domain: sd.domain
660
+ };
661
+ });
662
+
613
663
  const data = {
614
- location: domainObject.location,
664
+ location: domainObject.subdomain, // LEGACY
665
+ subdomain: domainObject.subdomain,
615
666
  domain: domainObject.domain,
616
- portBindings: app.portBindings
667
+ portBindings: app.portBindings,
668
+ secondaryDomains
617
669
  };
618
670
 
671
+ // secondary domains
672
+ if (options.secondaryDomains) {
673
+ // ask the user for port values if the ports are different in the app and the manifest
674
+ if (typeof options.secondaryDomains === 'string') {
675
+ data.secondaryDomains = {};
676
+ for (const kv of options.secondaryDomains.split(',')) {
677
+ const tmp = kv.split('=');
678
+ data.secondaryDomains[tmp[0]] = await selectDomain(tmp[1], options);
679
+ }
680
+ } else {
681
+ data.secondaryDomains = await querySecondaryDomains(app, app.manifest, options);
682
+ }
683
+
684
+ for (const binding in data.secondaryDomains) console.log(`Secondary domain ${binding}: ${data.secondaryDomains[binding].subdomain}.${data.secondaryDomains[binding].domain}`);
685
+ }
686
+
619
687
  // port bindings
620
688
  if (options.portBindings) {
621
- var portBindings = app.portBindings;
689
+ let portBindings = app.portBindings;
622
690
  // ask the user for port values if the ports are different in the app and the manifest
623
691
  if (typeof options.portBindings === 'string') {
624
692
  portBindings = {};
625
693
  options.portBindings.split(',').forEach(function (kv) {
626
- var tmp = kv.split('=');
694
+ const tmp = kv.split('=');
627
695
  if (isNaN(parseInt(tmp[1], 10))) return; // disable the port
628
696
  portBindings[tmp[0]] = parseInt(tmp[1], 10);
629
697
  });
@@ -733,7 +801,8 @@ async function debug(cmd, options) {
733
801
  console.log('\n');
734
802
  console.log(options.limitMemory ? 'Limiting memory' : 'Setting unlimited memory');
735
803
 
736
- const response2 = await createRequest('POST', `/api/v1/apps/${app.id}/configure/memory_limit`, options);
804
+ const request2 = createRequest('POST', `/api/v1/apps/${app.id}/configure/memory_limit`, options);
805
+ const response2 = await request2.send({ memoryLimit });
737
806
  if (response2.statusCode !== 202) return exit(`Failed to set memory limit: ${requestError(response2)}`);
738
807
 
739
808
  await waitForTask(response2.body.taskId, options);
@@ -907,6 +976,7 @@ async function inspect(options) {
907
976
  for (const app of response.body.apps) {
908
977
  const response2 = await createRequest('GET', `/api/v1/apps/${app.id}`, options);
909
978
  if (response2.statusCode !== 200) return exit(`Failed to list app: ${requestError(response2)}`);
979
+ response2.body.location = response2.body.location || response2.body.subdomain; // LEGACY support
910
980
  apps.push(response2.body);
911
981
  }
912
982
 
@@ -1145,12 +1215,20 @@ async function clone(options) {
1145
1215
 
1146
1216
  if (!options.backup) return exit('Use --backup to specify the backup id');
1147
1217
 
1148
- let location = options.location || readlineSync.question('Cloned app location: ', {});
1149
- let portBindings = queryPortBindings(app, app.manifest);
1150
- let backupId = options.backup;
1218
+ const location = options.location || readlineSync.question('Cloned app location: ', {});
1219
+ const secondaryDomains = await querySecondaryDomains(app, app.manifest, options);
1220
+ const portBindings = queryPortBindings(app, app.manifest);
1221
+ const backupId = options.backup;
1151
1222
 
1152
1223
  const domainObject = await selectDomain(location, options);
1153
- const data = { backupId, domain: domainObject.domain, location: domainObject.location, portBindings: portBindings };
1224
+ const data = {
1225
+ backupId,
1226
+ location: domainObject.subdomain, // LEGACY
1227
+ subdomain: domainObject.subdomain,
1228
+ domain: domainObject.domain,
1229
+ secondaryDomains,
1230
+ portBindings
1231
+ };
1154
1232
  const request = createRequest('POST', `/api/v1/apps/${app.id}/clone`, options);
1155
1233
  const response = await request.send(data);
1156
1234
  if (response.statusCode !== 201) return exit(`Failed to list apps: ${requestError(response)}`);
@@ -367,6 +367,19 @@ function approveVersion(appstoreId, version) {
367
367
 
368
368
  console.log('Approved.');
369
369
  console.log('');
370
+
371
+ superagentEnd(function () {
372
+ return superagent.get(createUrl('/api/v1/developers/apps/' + appstoreId + '/versions/' + version)).query({ accessToken: config.appStoreToken() });
373
+ }, function (error, result) {
374
+ if (error && !error.response) exit(util.format('Failed to list apps: %s', error.message));
375
+ if (result.statusCode !== 200) exit(util.format('Failed to list apps: %s message: %s', result.statusCode, result.text));
376
+
377
+ console.log('Changelog for forum update: ' + result.body.manifest.forumUrl);
378
+ console.log('');
379
+ console.log('[' + version + ']');
380
+ console.log(result.body.manifest.changelog);
381
+ console.log('');
382
+ });
370
383
  });
371
384
  }
372
385
 
@@ -505,6 +518,7 @@ function approve(options) {
505
518
  if (!version) return exit('--appstore-id must be of the format id@version');
506
519
 
507
520
  console.log('Approving ' + id + '@' + version);
521
+
508
522
  approveVersion(id, version);
509
523
  });
510
524
  }