cloudron 4.13.1 → 4.14.3
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 +4 -1
- package/package.json +2 -2
- package/src/actions.js +103 -24
- package/src/appstore-actions.js +14 -0
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.
|
|
3
|
+
"version": "4.14.3",
|
|
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.
|
|
21
|
+
"cloudron-manifestformat": "^5.15.1",
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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 (
|
|
455
|
-
|
|
456
|
-
|
|
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 (
|
|
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.
|
|
601
|
+
location: domainObject.subdomain, // LEGACY
|
|
602
|
+
subdomain: domainObject.subdomain,
|
|
562
603
|
domain: domainObject.domain,
|
|
563
|
-
|
|
604
|
+
secondaryDomains,
|
|
605
|
+
portBindings,
|
|
564
606
|
accessRestriction: null
|
|
565
607
|
};
|
|
566
608
|
|
|
@@ -610,20 +652,48 @@ async function configure(options) {
|
|
|
610
652
|
|
|
611
653
|
const domainObject = await selectDomain(location, options);
|
|
612
654
|
|
|
655
|
+
const secondaryDomains = {};
|
|
656
|
+
if (app.secondaryDomains) { // only valid post 7.1
|
|
657
|
+
app.secondaryDomains.forEach(sd => {
|
|
658
|
+
secondaryDomains[sd.environmentVariable] = {
|
|
659
|
+
subdomain: sd.subdomain,
|
|
660
|
+
domain: sd.domain
|
|
661
|
+
};
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
613
665
|
const data = {
|
|
614
|
-
location: domainObject.
|
|
666
|
+
location: domainObject.subdomain, // LEGACY
|
|
667
|
+
subdomain: domainObject.subdomain,
|
|
615
668
|
domain: domainObject.domain,
|
|
616
|
-
portBindings: app.portBindings
|
|
669
|
+
portBindings: app.portBindings,
|
|
670
|
+
secondaryDomains
|
|
617
671
|
};
|
|
618
672
|
|
|
673
|
+
// secondary domains
|
|
674
|
+
if (options.secondaryDomains) {
|
|
675
|
+
// ask the user for port values if the ports are different in the app and the manifest
|
|
676
|
+
if (typeof options.secondaryDomains === 'string') {
|
|
677
|
+
data.secondaryDomains = {};
|
|
678
|
+
for (const kv of options.secondaryDomains.split(',')) {
|
|
679
|
+
const tmp = kv.split('=');
|
|
680
|
+
data.secondaryDomains[tmp[0]] = await selectDomain(tmp[1], options);
|
|
681
|
+
}
|
|
682
|
+
} else {
|
|
683
|
+
data.secondaryDomains = await querySecondaryDomains(app, app.manifest, options);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
for (const binding in data.secondaryDomains) console.log(`Secondary domain ${binding}: ${data.secondaryDomains[binding].subdomain}.${data.secondaryDomains[binding].domain}`);
|
|
687
|
+
}
|
|
688
|
+
|
|
619
689
|
// port bindings
|
|
620
690
|
if (options.portBindings) {
|
|
621
|
-
|
|
691
|
+
let portBindings = app.portBindings;
|
|
622
692
|
// ask the user for port values if the ports are different in the app and the manifest
|
|
623
693
|
if (typeof options.portBindings === 'string') {
|
|
624
694
|
portBindings = {};
|
|
625
695
|
options.portBindings.split(',').forEach(function (kv) {
|
|
626
|
-
|
|
696
|
+
const tmp = kv.split('=');
|
|
627
697
|
if (isNaN(parseInt(tmp[1], 10))) return; // disable the port
|
|
628
698
|
portBindings[tmp[0]] = parseInt(tmp[1], 10);
|
|
629
699
|
});
|
|
@@ -908,6 +978,7 @@ async function inspect(options) {
|
|
|
908
978
|
for (const app of response.body.apps) {
|
|
909
979
|
const response2 = await createRequest('GET', `/api/v1/apps/${app.id}`, options);
|
|
910
980
|
if (response2.statusCode !== 200) return exit(`Failed to list app: ${requestError(response2)}`);
|
|
981
|
+
response2.body.location = response2.body.location || response2.body.subdomain; // LEGACY support
|
|
911
982
|
apps.push(response2.body);
|
|
912
983
|
}
|
|
913
984
|
|
|
@@ -1146,12 +1217,20 @@ async function clone(options) {
|
|
|
1146
1217
|
|
|
1147
1218
|
if (!options.backup) return exit('Use --backup to specify the backup id');
|
|
1148
1219
|
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1220
|
+
const location = options.location || readlineSync.question('Cloned app location: ', {});
|
|
1221
|
+
const secondaryDomains = await querySecondaryDomains(app, app.manifest, options);
|
|
1222
|
+
const portBindings = queryPortBindings(app, app.manifest);
|
|
1223
|
+
const backupId = options.backup;
|
|
1152
1224
|
|
|
1153
1225
|
const domainObject = await selectDomain(location, options);
|
|
1154
|
-
const data = {
|
|
1226
|
+
const data = {
|
|
1227
|
+
backupId,
|
|
1228
|
+
location: domainObject.subdomain, // LEGACY
|
|
1229
|
+
subdomain: domainObject.subdomain,
|
|
1230
|
+
domain: domainObject.domain,
|
|
1231
|
+
secondaryDomains,
|
|
1232
|
+
portBindings
|
|
1233
|
+
};
|
|
1155
1234
|
const request = createRequest('POST', `/api/v1/apps/${app.id}/clone`, options);
|
|
1156
1235
|
const response = await request.send(data);
|
|
1157
1236
|
if (response.statusCode !== 201) return exit(`Failed to list apps: ${requestError(response)}`);
|
package/src/appstore-actions.js
CHANGED
|
@@ -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
|
}
|